Files
arch-panel-extension-browser/src/views/manage-time-view.js

634 lines
24 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(() => {
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,
});
})();