Reestruturação dos arquivos e pastas do código da extensão

This commit is contained in:
Paulo Porto
2026-05-13 23:40:29 -03:00
parent f196067b21
commit c989088ab1
33 changed files with 13154 additions and 11866 deletions

View File

@@ -0,0 +1,633 @@
(() => {
function create(dependencies) {
const {
appState,
escapeHtml,
cleanString,
normalizeString,
formatDateInputDisplay,
getDateInputValue,
formatHours,
getWeekStartDate,
getLocalDateKey,
getTimeManagementDayKey,
isTimeManagementSaveEnabled,
getTimeManagementRows,
getTimeManagementActivityOptions,
findTimeManagementServiceRequestByNumber,
formatTimeManagementServiceRequestLabel,
isNonServiceRequestSrNumber,
findOptionValue,
findOptionLabel,
getTimeManagementTaskTypeOptions,
formatTimeManagementHourPayloadValue,
} = dependencies;
function renderTimeManagementModal() {
const modal = appState.timeManagementModal;
if (!modal) {
return "";
}
const selectedDate = modal.selectedDate || getDateInputValue(new Date());
const selectedDateLabel = formatDateInputDisplay(selectedDate);
const canSaveTimeManagement = isTimeManagementSaveEnabled(modal);
const isSavingTimeManagement = Boolean(modal.isSaving);
return `
<div class="arch-panel-extension-detail-overlay">
<div class="arch-panel-extension-time-management-dialog" role="document">
<header class="arch-panel-extension-detail-head">
<div>
<p class="arch-panel-extension-panel-label">Horas apontadas</p>
<h3 class="arch-panel-extension-panel-title">Manage time</h3>
</div>
</header>
<div class="arch-panel-extension-time-management-toolbar">
<label class="arch-panel-extension-form-field">
<span>Selected date</span>
<span class="arch-panel-extension-date-picker">
<button
type="button"
class="arch-panel-extension-date-step-button"
data-action="time-management-date-week-prev"
aria-label="Previous week"
>
<svg viewBox="0 0 20 20" aria-hidden="true" focusable="false">
<path d="M12.78 4.22a.75.75 0 0 1 0 1.06L8.06 10l4.72 4.72a.75.75 0 1 1-1.06 1.06l-5.25-5.25a.75.75 0 0 1 0-1.06l5.25-5.25a.75.75 0 0 1 1.06 0Z"></path>
</svg>
</button>
<span class="arch-panel-extension-date-input-shell">
<button
type="button"
class="arch-panel-extension-date-picker-button"
data-action="open-time-management-date-picker"
aria-label="Open calendar"
>
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M7 2a1 1 0 0 1 1 1v1h8V3a1 1 0 1 1 2 0v1h1.25A2.75 2.75 0 0 1 22 6.75v11.5A2.75 2.75 0 0 1 19.25 21H4.75A2.75 2.75 0 0 1 2 18.25V6.75A2.75 2.75 0 0 1 4.75 4H6V3a1 1 0 0 1 1-1ZM4.5 9.5v8.75c0 .69.56 1.25 1.25 1.25h12.5c.69 0 1.25-.56 1.25-1.25V9.5h-15Zm1.25-4c-.69 0-1.25.56-1.25 1.25V8h15V6.75c0-.69-.56-1.25-1.25-1.25H17v.5a1 1 0 1 1-2 0v-.5H9v.5a1 1 0 0 1-2 0v-.5H5.75Z"></path>
</svg>
</button>
<input
type="text"
value="${escapeHtml(selectedDateLabel)}"
aria-label="Selected date"
readonly
>
${renderTimeManagementDatePicker(selectedDate)}
</span>
<button
type="button"
class="arch-panel-extension-date-step-button"
data-action="time-management-date-week-next"
aria-label="Next week"
>
<svg viewBox="0 0 20 20" aria-hidden="true" focusable="false">
<path d="M7.22 15.78a.75.75 0 0 1 0-1.06L11.94 10 7.22 5.28a.75.75 0 0 1 1.06-1.06l5.25 5.25a.75.75 0 0 1 0 1.06l-5.25 5.25a.75.75 0 0 1-1.06 0Z"></path>
</svg>
</button>
</span>
</label>
${renderTimeManagementHourIndicators(modal)}
</div>
${renderTimeManagementBody(modal)}
<div class="arch-panel-extension-detail-actions">
<div class="arch-panel-extension-time-management-footer-actions">
<button
type="button"
class="arch-panel-extension-utility-button"
data-action="time-management-add-sr"
>
Add SR
</button>
<button
type="button"
class="arch-panel-extension-utility-button"
data-action="time-management-add-non-service"
>
Add (Non-service)
</button>
<button
type="button"
class="arch-panel-extension-utility-button is-outline"
data-action="time-management-save"
${canSaveTimeManagement && !isSavingTimeManagement ? "" : "disabled"}
>
${isSavingTimeManagement ? "Gravando..." : "Gravar"}
</button>
</div>
${
modal.saveErrorMessage
? `<p class="arch-panel-extension-time-management-save-error">${escapeHtml(modal.saveErrorMessage)}</p>`
: ""
}
<button
type="button"
class="arch-panel-extension-close"
data-action="close-time-management"
aria-label="Fechar apontamento de horas"
>
Fechar
</button>
</div>
</div>
</div>
`;
}
function renderTimeManagementBody(modal) {
if (modal.status === "loading") {
return renderTimeManagementSkeleton();
}
if (modal.status === "error") {
return `
<div class="arch-panel-extension-time-management-card is-error">
<p class="arch-panel-extension-summary-label">Erro na requisição</p>
<p class="arch-panel-extension-detail-subtitle">${escapeHtml(
modal.errorMessage || "Não foi possível carregar o apontamento."
)}</p>
</div>
`;
}
const rows = getTimeManagementRows(modal);
return `
<div class="arch-panel-extension-time-management-card">
${renderTimeManagementHeaderRow(modal.weekDays || [])}
<div class="arch-panel-extension-time-management-rows">
${
rows.length
? rows.map((row, index) => renderTimeManagementEntryRow(modal, row, index)).join("")
: `<div class="arch-panel-extension-empty-state arch-panel-extension-time-management-empty">
Nenhum apontamento de hora neste período
</div>`
}
</div>
</div>
`;
}
function renderTimeManagementHeaderRow(weekDays) {
return `
<div class="arch-panel-extension-time-management-header-row" aria-hidden="true">
<span class="arch-panel-extension-time-management-column-label">Service Request</span>
<span class="arch-panel-extension-time-management-column-label">Activity</span>
<span class="arch-panel-extension-time-management-column-label">Task Type</span>
${(weekDays || []).map((day) => renderTimeManagementDayHeader(day)).join("")}
<span class="arch-panel-extension-time-management-column-label" aria-label="Actions"></span>
</div>
`;
}
function renderTimeManagementDayHeader(day) {
return `
<span class="arch-panel-extension-time-management-day-head">
<strong>${escapeHtml(day.dayLabel || "-")}</strong>
<span>${escapeHtml(day.monthLabel || day.date || "-")}</span>
</span>
`;
}
function renderTimeManagementEntryRow(modal, row, index) {
const rowId = cleanString(row?.id) || `row-${index}`;
const isHighlighted =
cleanString(modal?.highlightRowId) &&
cleanString(modal?.highlightRowId) === rowId;
const selectedSr = cleanString(row?.selectedSrNumber);
const activityOptions = getTimeManagementActivityOptions(modal, selectedSr);
const selectedSrOption = findTimeManagementServiceRequestByNumber(
modal,
selectedSr
);
const isNonServiceRequest = isNonServiceRequestSrNumber(selectedSr);
const selectedSrLabel =
selectedSrOption?.label ||
formatTimeManagementServiceRequestLabel(selectedSrOption) ||
selectedSr;
const selectedActivityValue =
findOptionValue(activityOptions, row?.selectedActivityValue) ||
cleanString(row?.selectedActivityValue) ||
"";
const selectedActivityLabel =
findOptionLabel(activityOptions, selectedActivityValue) || selectedActivityValue;
const taskTypeOptions = getTimeManagementTaskTypeOptions(modal, selectedSr);
const selectedTaskTypeValue =
findOptionValue(taskTypeOptions, row?.selectedTaskTypeValue) ||
cleanString(row?.selectedTaskTypeValue) ||
"";
const selectedTaskTypeLabel =
findOptionLabel(taskTypeOptions, selectedTaskTypeValue) || selectedTaskTypeValue;
return `
<div class="arch-panel-extension-time-management-entry-row is-compact${isHighlighted ? " is-duplicate-highlight" : ""}" data-row-id="${escapeHtml(rowId)}">
<input type="hidden" name="timeEntryId" value="${escapeHtml(row?.timeEntryId || "")}">
<label class="arch-panel-extension-form-field">
${renderSearchableCombobox({
field: "sr",
rowId,
selectedValue: selectedSr,
value: selectedSrLabel,
placeholder: "Search SR",
options: modal.serviceRequests || [],
valueKey: "srNumber",
labelKey: "label",
fallbackLabelKey: "srTitle",
disabled: isNonServiceRequest,
})}
</label>
<label class="arch-panel-extension-form-field">
${renderSearchableCombobox({
field: "activity",
rowId,
selectedValue: selectedActivityValue,
value: selectedActivityLabel,
placeholder: "Search activity",
options: activityOptions,
valueKey: "value",
labelKey: "label",
metaKey: "value",
disabled: isNonServiceRequest,
})}
</label>
<label class="arch-panel-extension-form-field">
${renderSearchableCombobox({
field: "taskType",
rowId,
selectedValue: selectedTaskTypeValue,
value: selectedTaskTypeLabel,
placeholder: "Search task type",
options: taskTypeOptions,
valueKey: "value",
labelKey: "label",
})}
</label>
${(modal.weekDays || [])
.map((day) =>
renderTimeManagementDayInput(day, rowId, row?.dayHours)
)
.join("")}
<button
type="button"
class="arch-panel-extension-time-management-delete"
data-action="time-management-remove-row"
data-row-id="${escapeHtml(rowId)}"
aria-label="Remover linha de apontamento"
title="Remover linha"
>
<svg viewBox="0 0 20 20" aria-hidden="true" focusable="false">
<path d="M7.5 2.5A1.5 1.5 0 0 1 9 1h2a1.5 1.5 0 0 1 1.5 1.5V3H16a.75.75 0 0 1 0 1.5h-.72l-.67 11.33A2.25 2.25 0 0 1 12.36 18H7.64a2.25 2.25 0 0 1-2.25-2.17L4.72 4.5H4A.75.75 0 0 1 4 3h3.5v-.5ZM9 3h2v-.5H9V3Zm-2.78 1.5.66 11.24c.02.42.36.76.76.76h4.72c.4 0 .74-.34.76-.76l.66-11.24H6.22Zm2.53 2.25c.41 0 .75.34.75.75v6a.75.75 0 0 1-1.5 0v-6c0-.41.34-.75.75-.75Zm3.25.75a.75.75 0 0 0-1.5 0v6a.75.75 0 0 0 1.5 0v-6Z"></path>
</svg>
</button>
</div>
`;
}
function renderTimeManagementSkeleton() {
return `
<div class="arch-panel-extension-time-management-card" aria-busy="true">
<div class="arch-panel-extension-time-management-header-row">
${["Service Request", "Activity", "Task Type"]
.map(
(label) =>
`<span class="arch-panel-extension-time-management-column-label">${label}</span>`
)
.join("")}
${Array.from({ length: 7 }, () => `
<span class="arch-panel-extension-time-management-day-head">
<span class="arch-panel-extension-skeleton arch-panel-extension-skeleton-title"></span>
<span class="arch-panel-extension-skeleton arch-panel-extension-skeleton-text"></span>
</span>
`).join("")}
<span></span>
</div>
<div class="arch-panel-extension-time-management-entry-row">
${Array.from({ length: 3 }, () => `
<div class="arch-panel-extension-form-field">
<span class="arch-panel-extension-skeleton arch-panel-extension-skeleton-control"></span>
</div>
`).join("")}
${Array.from({ length: 7 }, () => `
<div class="arch-panel-extension-time-management-day">
<span class="arch-panel-extension-skeleton arch-panel-extension-skeleton-input"></span>
</div>
`).join("")}
<span></span>
</div>
</div>
`;
}
function renderSearchableCombobox({
field,
rowId,
selectedValue,
value,
placeholder,
options,
valueKey,
labelKey,
fallbackLabelKey,
metaKey,
disabled = false,
}) {
const safeField = escapeHtml(field);
const safeRowId = escapeHtml(rowId || "");
const isDisabled = Boolean(disabled);
const normalizedSelectedValue = normalizeString(selectedValue);
const normalizedOptions = (options || [])
.map((item) => ({
value: cleanString(item?.[valueKey]),
label:
cleanString(item?.[labelKey]) ||
cleanString(item?.[fallbackLabelKey]) ||
cleanString(item?.[valueKey]),
meta: cleanString(item?.[metaKey]),
}))
.filter((item) => item.value || item.label);
return `
<span class="arch-panel-extension-combobox${isDisabled ? " is-disabled" : ""}" data-combobox-field="${safeField}" data-row-id="${safeRowId}">
<span class="arch-panel-extension-combobox-value" title="${escapeHtml(value || "")}">
${escapeHtml(value || placeholder || "")}
</span>
<input
type="search"
value=""
placeholder="${escapeHtml(placeholder || "")}"
data-combobox-field="${safeField}"
data-row-id="${safeRowId}"
autocomplete="off"
spellcheck="false"
role="combobox"
aria-autocomplete="list"
aria-expanded="false"
${isDisabled ? "disabled" : ""}
>
<span class="arch-panel-extension-combobox-arrow" aria-hidden="true">
<svg viewBox="0 0 20 20" focusable="false">
<path d="M5.23 7.21a.75.75 0 0 1 1.06.02L10 11.16l3.71-3.93a.75.75 0 1 1 1.08 1.04l-4.25 4.5a.75.75 0 0 1-1.08 0l-4.25-4.5a.75.75 0 0 1 .02-1.06Z"></path>
</svg>
</span>
<span class="arch-panel-extension-combobox-menu" role="listbox">
${
normalizedOptions.length
? normalizedOptions
.map(
(item, index) => {
const isSelected =
normalizedSelectedValue &&
normalizeString(item.value) === normalizedSelectedValue;
const meta =
item.meta && normalizeString(item.meta) !== normalizeString(item.label)
? item.meta
: "";
const optionId = `arch-panel-extension-combobox-${safeField}-${index}`;
return `
<button
id="${optionId}"
type="button"
class="arch-panel-extension-combobox-option${isSelected ? " is-selected" : ""}"
data-action="time-management-combobox-option"
data-field="${safeField}"
data-row-id="${safeRowId}"
data-value="${escapeHtml(item.value)}"
data-search="${escapeHtml(`${item.label} ${item.value}`.toLowerCase())}"
role="option"
aria-selected="${isSelected ? "true" : "false"}"
>
<span class="arch-panel-extension-combobox-option-main">
${escapeHtml(item.label || item.value)}
</span>
${
meta
? `<span class="arch-panel-extension-combobox-option-meta">${escapeHtml(meta)}</span>`
: ""
}
</button>
`;
}
)
.join("")
: `<span class="arch-panel-extension-combobox-empty">No options</span>`
}
</span>
</span>
`;
}
function renderTimeManagementDatePicker(selectedDate) {
const picker = appState.timeManagementDatePicker;
if (!picker.isOpen) {
return "";
}
const monthStart = new Date(picker.year, picker.month, 1);
const calendarStart = getWeekStartDate(monthStart);
const selectedDateKey = getLocalDateKey(selectedDate);
const todayKey = getLocalDateKey(new Date());
const monthLabel = new Intl.DateTimeFormat("en-US", {
month: "long",
year: "numeric",
}).format(monthStart);
const days = Array.from({ length: 42 }, (_, index) => {
const date = new Date(calendarStart);
date.setDate(calendarStart.getDate() + index);
const dateKey = getLocalDateKey(date);
const className = [
"arch-panel-extension-date-picker-day",
date.getMonth() === picker.month ? "" : "is-muted",
dateKey === todayKey ? "is-today" : "",
dateKey === selectedDateKey ? "is-selected" : "",
]
.filter(Boolean)
.join(" ");
return `
<button
type="button"
class="${className}"
data-action="time-management-date-picker-select"
data-date-value="${escapeHtml(dateKey)}"
>
${date.getDate()}
</button>
`;
});
return `
<div class="arch-panel-extension-date-picker-popover">
<div class="arch-panel-extension-date-picker-head">
<button
type="button"
class="arch-panel-extension-calendar-nav"
data-action="time-management-date-picker-prev"
aria-label="Previous month"
>
</button>
<strong>${escapeHtml(monthLabel)}</strong>
<button
type="button"
class="arch-panel-extension-calendar-nav"
data-action="time-management-date-picker-next"
aria-label="Next month"
>
</button>
</div>
<div class="arch-panel-extension-date-picker-weekdays">
${["S", "M", "T", "W", "T", "F", "S"].map((day) => `<span>${day}</span>`).join("")}
</div>
<div class="arch-panel-extension-date-picker-grid">
${days.join("")}
</div>
</div>
`;
}
function renderTimeManagementHourIndicators(modal) {
if (modal?.status === "loading") {
return `
<div class="arch-panel-extension-time-management-hour-indicators" aria-label="Resumo de horas apontadas" aria-busy="true">
${Array.from({ length: 4 }, () => `
<span class="arch-panel-extension-time-management-hour-indicator">
<span class="arch-panel-extension-skeleton arch-panel-extension-skeleton-text"></span>
<strong class="arch-panel-extension-skeleton arch-panel-extension-skeleton-title"></strong>
</span>
`).join("")}
</div>
`;
}
return `
<div class="arch-panel-extension-time-management-hour-indicators" aria-label="Resumo de horas apontadas">
${buildTimeManagementHourIndicators(modal)
.map(
(indicator) => `
<span class="arch-panel-extension-time-management-hour-indicator">
<span>${escapeHtml(indicator.label)}</span>
<strong>${escapeHtml(formatHours(indicator.value))}</strong>
</span>
`
)
.join("")}
</div>
`;
}
function buildTimeManagementHourIndicators(modal) {
const totals = getTimeManagementRows(modal).reduce(
(accumulator, row) => {
const rowTotal = getTimeManagementRowTotalHours(row);
const category = getTimeManagementHourCategory(modal, row);
accumulator[category] += rowTotal;
accumulator.total += rowTotal;
return accumulator;
},
{
service: 0,
nonService: 0,
timeOff: 0,
total: 0,
}
);
return [
{ label: "Service", value: totals.service },
{ label: "Non Service", value: totals.nonService },
{ label: "Time Off", value: totals.timeOff },
{ label: "Total hours", value: totals.total },
];
}
function getTimeManagementRowTotalHours(row) {
return Object.values(row?.dayHours || {}).reduce(
(sum, value) => sum + formatTimeManagementHourPayloadValue(value),
0
);
}
function getTimeManagementHourCategory(modal, row) {
if (isTimeManagementTimeOffRow(modal, row)) {
return "timeOff";
}
return isNonServiceRequestSrNumber(row?.selectedSrNumber)
? "nonService"
: "service";
}
function isTimeManagementTimeOffRow(modal, row) {
const taskTypeValue = cleanString(row?.selectedTaskTypeValue);
const taskType = findTimeManagementTaskTypeByValue(modal, taskTypeValue);
const candidates = [
taskTypeValue,
taskType?.label,
taskType?.tags,
].map((value) => normalizeString(value));
return candidates.some(
(value) =>
value === "TIME_OFF" ||
value === "TIMEOFF" ||
value.includes("TIME_OFF") ||
value.includes("TIME OFF")
);
}
function findTimeManagementTaskTypeByValue(modal, value) {
const normalizedValue = normalizeString(value);
if (!normalizedValue) {
return null;
}
return (modal?.taskTypes || []).find(
(item) => normalizeString(item?.value) === normalizedValue
) || null;
}
function renderTimeManagementDayInput(
day,
rowId = "",
dayHours = {}
) {
const dateKey = getTimeManagementDayKey(day);
const value = cleanString(dayHours?.[dateKey]);
return `
<label class="arch-panel-extension-time-management-day">
<input
type="number"
min="0"
step="1"
value="${escapeHtml(value)}"
placeholder="0"
data-action="time-management-hours-change"
data-row-id="${escapeHtml(rowId || "")}"
data-date-key="${escapeHtml(dateKey)}"
aria-label="Horas apontadas em ${escapeHtml(day.date || "")}"
>
</label>
`;
}
return {
renderTimeManagementModal,
};
}
globalThis.ArchPanelManageTimeView = Object.freeze({
create,
});
})();