@open-mercato/core 0.6.8-develop.6973.1.17fe21ccd4 → 0.6.8-develop.6974.1.43440f247a
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/staff/backend/staff/timesheets/page.js +182 -112
- package/dist/modules/staff/backend/staff/timesheets/page.js.map +2 -2
- package/dist/modules/staff/lib/timesheetsDuration.js +45 -0
- package/dist/modules/staff/lib/timesheetsDuration.js.map +7 -0
- package/package.json +7 -7
- package/src/modules/staff/backend/staff/timesheets/page.tsx +127 -38
- package/src/modules/staff/i18n/de.json +6 -0
- package/src/modules/staff/i18n/en.json +6 -0
- package/src/modules/staff/i18n/es.json +6 -0
- package/src/modules/staff/i18n/ko.json +6 -0
- package/src/modules/staff/i18n/pl.json +6 -0
- package/src/modules/staff/lib/timesheetsDuration.ts +56 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -3,6 +3,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { Page, PageBody } from "@open-mercato/ui/backend/Page";
|
|
5
5
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
6
|
+
import { InlineInput } from "@open-mercato/ui/primitives/inline-input";
|
|
6
7
|
import { apiCall, apiCallOrThrow, readApiResultOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
|
|
7
8
|
import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuardedMutation";
|
|
8
9
|
import { flash } from "@open-mercato/ui/backend/FlashMessages";
|
|
@@ -19,6 +20,10 @@ import { TimerBar } from "../../../lib/timesheets-ui/TimerBar.js";
|
|
|
19
20
|
import { AddRowDropdown } from "../../../lib/timesheets-ui/AddRowDropdown.js";
|
|
20
21
|
import { CreateProjectDialog } from "../../../lib/timesheets-ui/CreateProjectDialog.js";
|
|
21
22
|
import { ProjectColorDot } from "../../../lib/timesheets-ui/ProjectColorDot.js";
|
|
23
|
+
import {
|
|
24
|
+
formatMinutesAsDecimal,
|
|
25
|
+
parseDurationInput
|
|
26
|
+
} from "../../../lib/timesheetsDuration.js";
|
|
22
27
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
23
28
|
const logger = createLogger("staff");
|
|
24
29
|
function getMonday(date) {
|
|
@@ -58,21 +63,12 @@ function isWeekendDay(date) {
|
|
|
58
63
|
const d = date.getDay();
|
|
59
64
|
return d === 0 || d === 6;
|
|
60
65
|
}
|
|
61
|
-
function minutesToDecimal(minutes) {
|
|
62
|
-
if (minutes === 0) return "";
|
|
63
|
-
const hours = minutes / 60;
|
|
64
|
-
return hours % 1 === 0 ? String(hours) : hours.toFixed(2).replace(/0+$/, "").replace(/\.$/, "");
|
|
65
|
-
}
|
|
66
|
-
function decimalToMinutes(value) {
|
|
67
|
-
const trimmed = value.trim();
|
|
68
|
-
if (!trimmed) return 0;
|
|
69
|
-
const num = parseFloat(trimmed.replace(",", "."));
|
|
70
|
-
if (isNaN(num) || num < 0) return 0;
|
|
71
|
-
return Math.min(Math.round(num * 60), 1440);
|
|
72
|
-
}
|
|
73
66
|
function getLocalizedDayName(date) {
|
|
74
67
|
return date.toLocaleDateString(void 0, { weekday: "short" });
|
|
75
68
|
}
|
|
69
|
+
function getLocalizedCellDate(date) {
|
|
70
|
+
return date.toLocaleDateString(void 0, { weekday: "long", month: "long", day: "numeric" });
|
|
71
|
+
}
|
|
76
72
|
function getWeekDays(weekStart) {
|
|
77
73
|
return Array.from({ length: 7 }, (_, i) => {
|
|
78
74
|
const d = new Date(weekStart);
|
|
@@ -114,6 +110,7 @@ function MyTimesheetsPage() {
|
|
|
114
110
|
const [rawEntries, setRawEntries] = React.useState([]);
|
|
115
111
|
const [dirty, setDirty] = React.useState({});
|
|
116
112
|
const [rawText, setRawText] = React.useState({});
|
|
113
|
+
const [cellErrors, setCellErrors] = React.useState({});
|
|
117
114
|
const [staffMemberId, setStaffMemberId] = React.useState(null);
|
|
118
115
|
const [staffMemberMissing, setStaffMemberMissing] = React.useState(false);
|
|
119
116
|
const [isInitialLoad, setIsInitialLoad] = React.useState(true);
|
|
@@ -130,6 +127,12 @@ function MyTimesheetsPage() {
|
|
|
130
127
|
contextId: mutationContextId,
|
|
131
128
|
blockedMessage: t("ui.forms.flash.saveBlocked", "Save blocked by validation")
|
|
132
129
|
});
|
|
130
|
+
const durationFormatHint = t(
|
|
131
|
+
"staff.timesheets.my.duration.hint",
|
|
132
|
+
"Enter hours (8 or 1.5), h:mm (1:30) or minutes (90m). Max 24h per day."
|
|
133
|
+
);
|
|
134
|
+
const describeDurationCell = React.useCallback((projectName, date) => t("staff.timesheets.my.duration.cellLabel", "Duration for {project} on {date}").replace("{project}", projectName).replace("{date}", getLocalizedCellDate(date)), [t]);
|
|
135
|
+
const describeDurationError = React.useCallback((reason) => reason === "out_of_range" ? t("staff.timesheets.my.duration.errors.out_of_range", "Maximum 24h per day. Use 1:30 or 90m for shorter entries.") : t("staff.timesheets.my.duration.errors.invalid", "Unrecognised duration. Use 8, 1.5, 1:30, 90m or 1h 30m."), [t]);
|
|
133
136
|
React.useEffect(() => {
|
|
134
137
|
let cancelled = false;
|
|
135
138
|
void (async () => {
|
|
@@ -273,6 +276,7 @@ function MyTimesheetsPage() {
|
|
|
273
276
|
setEntries(map);
|
|
274
277
|
setDirty({});
|
|
275
278
|
setRawText({});
|
|
279
|
+
setCellErrors({});
|
|
276
280
|
} catch (error) {
|
|
277
281
|
logger.error("staff.timesheets.my.load", { err: error });
|
|
278
282
|
flash(t("staff.timesheets.my.errors.load", "Failed to load timesheets."), "error");
|
|
@@ -285,19 +289,54 @@ function MyTimesheetsPage() {
|
|
|
285
289
|
React.useEffect(() => {
|
|
286
290
|
void loadData();
|
|
287
291
|
}, [loadData, scopeVersion]);
|
|
292
|
+
const clearCellError = React.useCallback((projectId, dateKey) => {
|
|
293
|
+
setCellErrors((prev) => {
|
|
294
|
+
const projectErrors = prev[projectId];
|
|
295
|
+
if (!projectErrors || projectErrors[dateKey] === void 0) return prev;
|
|
296
|
+
const nextProjectErrors = { ...projectErrors };
|
|
297
|
+
delete nextProjectErrors[dateKey];
|
|
298
|
+
const next = { ...prev };
|
|
299
|
+
if (Object.keys(nextProjectErrors).length > 0) next[projectId] = nextProjectErrors;
|
|
300
|
+
else delete next[projectId];
|
|
301
|
+
return next;
|
|
302
|
+
});
|
|
303
|
+
}, []);
|
|
288
304
|
const handleCellChange = React.useCallback((projectId, dateKey, value) => {
|
|
289
|
-
|
|
305
|
+
clearCellError(projectId, dateKey);
|
|
290
306
|
setRawText((prev) => {
|
|
291
307
|
const projectTexts = { ...prev[projectId] ?? {} };
|
|
292
|
-
projectTexts[dateKey] =
|
|
308
|
+
projectTexts[dateKey] = value;
|
|
293
309
|
return { ...prev, [projectId]: projectTexts };
|
|
294
310
|
});
|
|
311
|
+
}, [clearCellError]);
|
|
312
|
+
const dropDirtyCell = React.useCallback((projectId, dateKey) => {
|
|
313
|
+
setDirty((prev) => {
|
|
314
|
+
const projectEntries = prev[projectId];
|
|
315
|
+
if (!projectEntries || projectEntries[dateKey] === void 0) return prev;
|
|
316
|
+
const nextProjectEntries = { ...projectEntries };
|
|
317
|
+
delete nextProjectEntries[dateKey];
|
|
318
|
+
const next = { ...prev };
|
|
319
|
+
if (Object.keys(nextProjectEntries).length > 0) next[projectId] = nextProjectEntries;
|
|
320
|
+
else delete next[projectId];
|
|
321
|
+
return next;
|
|
322
|
+
});
|
|
295
323
|
}, []);
|
|
296
324
|
const handleCellBlur = React.useCallback((projectId, dateKey, currentValue) => {
|
|
297
325
|
const editedText = rawText[projectId]?.[dateKey];
|
|
298
326
|
const text = editedText ?? currentValue;
|
|
299
327
|
if (text === void 0) return;
|
|
300
|
-
const
|
|
328
|
+
const parsed = parseDurationInput(text);
|
|
329
|
+
if (!parsed.ok) {
|
|
330
|
+
dropDirtyCell(projectId, dateKey);
|
|
331
|
+
setCellErrors((prev) => {
|
|
332
|
+
const projectErrors = { ...prev[projectId] ?? {} };
|
|
333
|
+
projectErrors[dateKey] = parsed.reason;
|
|
334
|
+
return { ...prev, [projectId]: projectErrors };
|
|
335
|
+
});
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
clearCellError(projectId, dateKey);
|
|
339
|
+
const minutes = parsed.minutes;
|
|
301
340
|
const cellEntries = entries[projectId]?.[dateKey] ?? [];
|
|
302
341
|
const existingMinutes = cellEntries.reduce((sum, e) => sum + e.minutes, 0);
|
|
303
342
|
if (minutes !== existingMinutes || editedText !== void 0 && cellEntries.length > 0) {
|
|
@@ -319,7 +358,7 @@ function MyTimesheetsPage() {
|
|
|
319
358
|
}
|
|
320
359
|
return { ...prev, [projectId]: projectTexts };
|
|
321
360
|
});
|
|
322
|
-
}, [rawText, entries]);
|
|
361
|
+
}, [rawText, entries, clearCellError, dropDirtyCell]);
|
|
323
362
|
const getCellValue = React.useCallback((projectId, dateKey) => {
|
|
324
363
|
const dirtyCell = dirty[projectId]?.[dateKey];
|
|
325
364
|
if (dirtyCell !== void 0) return dirtyCell.minutes;
|
|
@@ -327,11 +366,27 @@ function MyTimesheetsPage() {
|
|
|
327
366
|
return cellEntries.reduce((sum, e) => sum + e.minutes, 0);
|
|
328
367
|
}, [dirty, entries]);
|
|
329
368
|
const hasChanges = Object.keys(dirty).length > 0 || Object.keys(rawText).length > 0;
|
|
369
|
+
const invalidCellCount = React.useMemo(
|
|
370
|
+
() => Object.values(cellErrors).reduce((sum, projectErrors) => sum + Object.keys(projectErrors).length, 0),
|
|
371
|
+
[cellErrors]
|
|
372
|
+
);
|
|
373
|
+
const pendingSummary = React.useMemo(() => {
|
|
374
|
+
let cells = 0;
|
|
375
|
+
let minutes = 0;
|
|
376
|
+
for (const dateMap of Object.values(dirty)) {
|
|
377
|
+
for (const cell of Object.values(dateMap)) {
|
|
378
|
+
cells += 1;
|
|
379
|
+
minutes += cell.minutes;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return { cells, minutes };
|
|
383
|
+
}, [dirty]);
|
|
330
384
|
const handleSave = React.useCallback(async () => {
|
|
331
|
-
if (!hasChanges) return;
|
|
385
|
+
if (!hasChanges || invalidCellCount > 0) return;
|
|
386
|
+
const summary = t("staff.timesheets.my.confirm_save.summary", "{count} cell(s), {total}h in total.").replace("{count}", String(pendingSummary.cells)).replace("{total}", formatMinutesAsDecimal(pendingSummary.minutes) || "0");
|
|
332
387
|
const confirmed = await confirm({
|
|
333
388
|
title: t("staff.timesheets.my.confirm_save.title", "Save changes?"),
|
|
334
|
-
text: t("staff.timesheets.my.confirm_save.body", "Your timesheet entries will be saved.")
|
|
389
|
+
text: `${t("staff.timesheets.my.confirm_save.body", "Your timesheet entries will be saved.")} ${summary}`
|
|
335
390
|
});
|
|
336
391
|
if (!confirmed) return;
|
|
337
392
|
setIsSaving(true);
|
|
@@ -370,7 +425,7 @@ function MyTimesheetsPage() {
|
|
|
370
425
|
} finally {
|
|
371
426
|
setIsSaving(false);
|
|
372
427
|
}
|
|
373
|
-
}, [dirty, entries, hasChanges, confirm, t, loadData, runMutation, mutationContextId, staffMemberId, retryLastMutation]);
|
|
428
|
+
}, [dirty, entries, hasChanges, invalidCellCount, pendingSummary, confirm, t, loadData, runMutation, mutationContextId, staffMemberId, retryLastMutation]);
|
|
374
429
|
const getRowTotal = React.useCallback((projectId) => {
|
|
375
430
|
let total = 0;
|
|
376
431
|
for (const day of visibleDays) {
|
|
@@ -506,6 +561,12 @@ function MyTimesheetsPage() {
|
|
|
506
561
|
delete next[project.id];
|
|
507
562
|
return next;
|
|
508
563
|
});
|
|
564
|
+
setCellErrors((prev) => {
|
|
565
|
+
if (!prev[project.id]) return prev;
|
|
566
|
+
const next = { ...prev };
|
|
567
|
+
delete next[project.id];
|
|
568
|
+
return next;
|
|
569
|
+
});
|
|
509
570
|
} catch (error) {
|
|
510
571
|
logger.error("staff.timesheets.my.removeRow", { err: error });
|
|
511
572
|
flash(t("staff.timesheets.my.removeRow.error", "Could not remove the project. Please try again."), "error");
|
|
@@ -559,7 +620,7 @@ function MyTimesheetsPage() {
|
|
|
559
620
|
/* @__PURE__ */ jsxs("div", { className: "mb-4 grid grid-cols-1 gap-4 sm:grid-cols-4", children: [
|
|
560
621
|
/* @__PURE__ */ jsxs("div", { className: "rounded-lg border bg-card p-4", children: [
|
|
561
622
|
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: viewMode === "weekly" ? t("staff.timesheets.my.weekTotal", "Week Total") : t("staff.timesheets.my.total_hours", "Total Hours") }),
|
|
562
|
-
/* @__PURE__ */ jsx("p", { className: "text-2xl font-semibold", children:
|
|
623
|
+
/* @__PURE__ */ jsx("p", { className: "text-2xl font-semibold", children: formatMinutesAsDecimal(grandTotal) || "0" })
|
|
563
624
|
] }),
|
|
564
625
|
/* @__PURE__ */ jsxs("div", { className: "rounded-lg border bg-card p-4", children: [
|
|
565
626
|
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("staff.timesheets.my.working_days", "Working Days") }),
|
|
@@ -567,7 +628,7 @@ function MyTimesheetsPage() {
|
|
|
567
628
|
] }),
|
|
568
629
|
/* @__PURE__ */ jsxs("div", { className: "rounded-lg border bg-card p-4", children: [
|
|
569
630
|
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("staff.timesheets.my.daily_average", "Daily Average") }),
|
|
570
|
-
/* @__PURE__ */ jsx("p", { className: "text-2xl font-semibold", children:
|
|
631
|
+
/* @__PURE__ */ jsx("p", { className: "text-2xl font-semibold", children: formatMinutesAsDecimal(Math.round(dailyAverage)) || "0" })
|
|
571
632
|
] }),
|
|
572
633
|
/* @__PURE__ */ jsxs("div", { className: "rounded-lg border bg-card p-4", children: [
|
|
573
634
|
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("staff.timesheets.my.status", "Status") }),
|
|
@@ -592,8 +653,8 @@ function MyTimesheetsPage() {
|
|
|
592
653
|
}
|
|
593
654
|
),
|
|
594
655
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
595
|
-
hasChanges && /* @__PURE__ */ jsx("span", { className: "text-xs text-
|
|
596
|
-
/* @__PURE__ */ jsx(Button, { size: "sm", type: "button", onClick: handleSave, disabled: !hasChanges || isSaving, children: isSaving ? t("staff.timesheets.my.saving", "Saving...") : t("staff.timesheets.my.save_changes", "Save Changes") })
|
|
656
|
+
invalidCellCount > 0 ? /* @__PURE__ */ jsx("span", { className: "text-xs text-status-error-text font-medium", children: t("staff.timesheets.my.duration.blocked", "Fix the highlighted durations to save") }) : hasChanges && /* @__PURE__ */ jsx("span", { className: "text-xs text-status-warning-text font-medium", children: t("staff.timesheets.my.unsaved", "Unsaved changes") }),
|
|
657
|
+
/* @__PURE__ */ jsx(Button, { size: "sm", type: "button", onClick: handleSave, disabled: !hasChanges || isSaving || invalidCellCount > 0, children: isSaving ? t("staff.timesheets.my.saving", "Saving...") : t("staff.timesheets.my.save_changes", "Save Changes") })
|
|
597
658
|
] })
|
|
598
659
|
] })
|
|
599
660
|
] }),
|
|
@@ -604,100 +665,109 @@ function MyTimesheetsPage() {
|
|
|
604
665
|
canManageProjects && /* @__PURE__ */ jsx(Button, { asChild: true, children: /* @__PURE__ */ jsx(Link, { href: "/backend/staff/timesheets/projects/create", children: t("staff.timesheets.my.noProjects.createProject", "Create Project") }) }),
|
|
605
666
|
/* @__PURE__ */ jsx(Button, { variant: "outline", asChild: true, children: /* @__PURE__ */ jsx(Link, { href: "/backend/staff/timesheets/projects", children: t("staff.timesheets.my.noProjects.viewProjects", "View Projects") }) })
|
|
606
667
|
] })
|
|
607
|
-
] }) : viewType === "list" ? /* @__PURE__ */ jsx(ListView, { entries: listViewEntries, onEntryUpdated: loadData }) : /* @__PURE__ */
|
|
608
|
-
/* @__PURE__ */ jsxs("
|
|
609
|
-
/* @__PURE__ */
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
/* @__PURE__ */ jsx("
|
|
615
|
-
|
|
616
|
-
const dayName = getLocalizedDayName(date);
|
|
617
|
-
const weekend = isWeekendDay(date);
|
|
618
|
-
return /* @__PURE__ */ jsxs(
|
|
619
|
-
"th",
|
|
620
|
-
{
|
|
621
|
-
className: `py-2 text-center font-medium px-1 ${weekend ? "bg-muted/80 text-muted-foreground" : ""}`,
|
|
622
|
-
children: [
|
|
623
|
-
/* @__PURE__ */ jsx("div", { className: "text-[10px] uppercase text-muted-foreground", children: dayName }),
|
|
624
|
-
/* @__PURE__ */ jsx("div", { className: "text-xs", children: date.getDate() })
|
|
625
|
-
]
|
|
626
|
-
},
|
|
627
|
-
formatDateKey(date)
|
|
628
|
-
);
|
|
629
|
-
}),
|
|
630
|
-
/* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-right font-medium", children: t("staff.timesheets.my.total", "Total") })
|
|
631
|
-
] }) }),
|
|
632
|
-
/* @__PURE__ */ jsxs("tbody", { children: [
|
|
633
|
-
projects.map((project) => /* @__PURE__ */ jsxs("tr", { className: "group border-b hover:bg-muted/30", children: [
|
|
634
|
-
/* @__PURE__ */ jsx("td", { className: "sticky left-0 z-10 bg-background px-3 py-1.5 font-medium text-foreground", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
|
|
635
|
-
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
636
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 truncate", title: project.name, children: [
|
|
637
|
-
/* @__PURE__ */ jsx(ProjectColorDot, { colorKey: project.color, projectName: project.name, size: "sm" }),
|
|
638
|
-
/* @__PURE__ */ jsx("span", { className: "truncate", children: project.name })
|
|
639
|
-
] }),
|
|
640
|
-
project.code && /* @__PURE__ */ jsx("div", { className: "text-[10px] text-muted-foreground", children: project.code })
|
|
641
|
-
] }),
|
|
642
|
-
/* @__PURE__ */ jsx(
|
|
643
|
-
"button",
|
|
644
|
-
{
|
|
645
|
-
type: "button",
|
|
646
|
-
onClick: () => {
|
|
647
|
-
void handleRemoveProject(project);
|
|
648
|
-
},
|
|
649
|
-
"aria-label": t("staff.timesheets.my.removeRow", "Remove from grid"),
|
|
650
|
-
title: t("staff.timesheets.my.removeRow", "Remove from grid"),
|
|
651
|
-
className: "opacity-0 group-hover:opacity-100 focus:opacity-100 inline-flex h-5 w-5 shrink-0 cursor-pointer items-center justify-center rounded text-muted-foreground hover:bg-muted hover:text-foreground transition-opacity",
|
|
652
|
-
children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5" })
|
|
653
|
-
}
|
|
654
|
-
)
|
|
655
|
-
] }) }),
|
|
668
|
+
] }) : viewType === "list" ? /* @__PURE__ */ jsx(ListView, { entries: listViewEntries, onEntryUpdated: loadData }) : /* @__PURE__ */ jsxs("div", { className: "overflow-x-auto rounded-lg border", children: [
|
|
669
|
+
/* @__PURE__ */ jsxs("table", { className: "w-full text-sm table-fixed", children: [
|
|
670
|
+
/* @__PURE__ */ jsxs("colgroup", { children: [
|
|
671
|
+
/* @__PURE__ */ jsx("col", { className: viewMode === "weekly" ? "w-[35%]" : "w-[140px] min-w-[140px]" }),
|
|
672
|
+
visibleDays.map((date) => /* @__PURE__ */ jsx("col", { className: viewMode === "weekly" ? "" : "w-[36px] min-w-[36px]" }, formatDateKey(date))),
|
|
673
|
+
/* @__PURE__ */ jsx("col", { className: viewMode === "weekly" ? "w-[72px]" : "w-[56px] min-w-[56px]" })
|
|
674
|
+
] }),
|
|
675
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { className: "border-b bg-muted/50", children: [
|
|
676
|
+
/* @__PURE__ */ jsx("th", { className: "sticky left-0 z-10 bg-muted px-3 py-2 text-left font-medium", children: t("staff.timesheets.my.project", "Project") }),
|
|
656
677
|
visibleDays.map((date) => {
|
|
657
|
-
const
|
|
678
|
+
const dayName = getLocalizedDayName(date);
|
|
658
679
|
const weekend = isWeekendDay(date);
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
return /* @__PURE__ */ jsx("td", { className: `px-0.5 py-0.5 ${weekend ? "bg-muted/40" : ""}`, children: weekend ? /* @__PURE__ */ jsx("div", { className: "rounded px-1 py-1 text-center text-xs text-muted-foreground/50", children: "-" }) : /* @__PURE__ */ jsx(
|
|
662
|
-
"input",
|
|
680
|
+
return /* @__PURE__ */ jsxs(
|
|
681
|
+
"th",
|
|
663
682
|
{
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
onChange: (e) => handleCellChange(project.id, dateKey, e.target.value),
|
|
673
|
-
onBlur: (event) => handleCellBlur(project.id, dateKey, event.currentTarget.value),
|
|
674
|
-
placeholder: t("staff.timesheets.my.durationPlaceholder", "0")
|
|
675
|
-
}
|
|
676
|
-
) }, dateKey);
|
|
683
|
+
className: `py-2 text-center font-medium px-1 ${weekend ? "bg-muted/80 text-muted-foreground" : ""}`,
|
|
684
|
+
children: [
|
|
685
|
+
/* @__PURE__ */ jsx("div", { className: "text-[10px] uppercase text-muted-foreground", children: dayName }),
|
|
686
|
+
/* @__PURE__ */ jsx("div", { className: "text-xs", children: date.getDate() })
|
|
687
|
+
]
|
|
688
|
+
},
|
|
689
|
+
formatDateKey(date)
|
|
690
|
+
);
|
|
677
691
|
}),
|
|
678
|
-
/* @__PURE__ */ jsx("
|
|
679
|
-
] }
|
|
680
|
-
/* @__PURE__ */
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
692
|
+
/* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-right font-medium", children: t("staff.timesheets.my.total", "Total") })
|
|
693
|
+
] }) }),
|
|
694
|
+
/* @__PURE__ */ jsxs("tbody", { children: [
|
|
695
|
+
projects.map((project) => /* @__PURE__ */ jsxs("tr", { className: "group border-b hover:bg-muted/30", children: [
|
|
696
|
+
/* @__PURE__ */ jsx("td", { className: "sticky left-0 z-10 bg-background px-3 py-1.5 font-medium text-foreground", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
|
|
697
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
698
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 truncate", title: project.name, children: [
|
|
699
|
+
/* @__PURE__ */ jsx(ProjectColorDot, { colorKey: project.color, projectName: project.name, size: "sm" }),
|
|
700
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: project.name })
|
|
701
|
+
] }),
|
|
702
|
+
project.code && /* @__PURE__ */ jsx("div", { className: "text-[10px] text-muted-foreground", children: project.code })
|
|
703
|
+
] }),
|
|
704
|
+
/* @__PURE__ */ jsx(
|
|
705
|
+
"button",
|
|
706
|
+
{
|
|
707
|
+
type: "button",
|
|
708
|
+
onClick: () => {
|
|
709
|
+
void handleRemoveProject(project);
|
|
710
|
+
},
|
|
711
|
+
"aria-label": t("staff.timesheets.my.removeRow", "Remove from grid"),
|
|
712
|
+
title: t("staff.timesheets.my.removeRow", "Remove from grid"),
|
|
713
|
+
className: "opacity-0 group-hover:opacity-100 focus:opacity-100 inline-flex h-5 w-5 shrink-0 cursor-pointer items-center justify-center rounded text-muted-foreground hover:bg-muted hover:text-foreground transition-opacity",
|
|
714
|
+
children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5" })
|
|
715
|
+
}
|
|
716
|
+
)
|
|
717
|
+
] }) }),
|
|
718
|
+
visibleDays.map((date) => {
|
|
719
|
+
const dateKey = formatDateKey(date);
|
|
720
|
+
const weekend = isWeekendDay(date);
|
|
721
|
+
const cellMinutes = getCellValue(project.id, dateKey);
|
|
722
|
+
const isDirty = dirty[project.id]?.[dateKey] !== void 0;
|
|
723
|
+
const cellError = cellErrors[project.id]?.[dateKey];
|
|
724
|
+
const cellErrorMessage = cellError ? describeDurationError(cellError) : void 0;
|
|
725
|
+
return /* @__PURE__ */ jsx("td", { className: `px-0.5 py-0.5 ${weekend ? "bg-muted/40" : ""}`, children: weekend ? /* @__PURE__ */ jsx("div", { className: "rounded px-1 py-1 text-center text-xs text-muted-foreground/50", children: "-" }) : /* @__PURE__ */ jsx(
|
|
726
|
+
InlineInput,
|
|
727
|
+
{
|
|
728
|
+
type: "text",
|
|
729
|
+
inputMode: "text",
|
|
730
|
+
showBorderOnHover: false,
|
|
731
|
+
className: `mx-auto flex h-6 border transition-colors
|
|
732
|
+
${viewMode === "weekly" ? "w-12 px-1" : "w-8 px-0"}
|
|
733
|
+
${cellError ? "bg-status-error-bg" : isDirty ? "border-status-warning-border bg-status-warning-bg" : "border-muted-foreground/20 bg-transparent hover:border-muted-foreground/40"}`,
|
|
734
|
+
inputClassName: `text-center text-xs tabular-nums
|
|
735
|
+
${cellError ? "text-status-error-text" : cellMinutes > 0 ? "font-semibold" : "text-muted-foreground"}`,
|
|
736
|
+
value: rawText[project.id]?.[dateKey] ?? formatMinutesAsDecimal(cellMinutes),
|
|
737
|
+
onChange: (e) => handleCellChange(project.id, dateKey, e.target.value),
|
|
738
|
+
onBlur: (event) => handleCellBlur(project.id, dateKey, event.currentTarget.value),
|
|
739
|
+
placeholder: t("staff.timesheets.my.durationPlaceholder", "0"),
|
|
740
|
+
"aria-invalid": cellError !== void 0,
|
|
741
|
+
"aria-label": describeDurationCell(project.name, date),
|
|
742
|
+
title: cellErrorMessage ?? durationFormatHint
|
|
743
|
+
}
|
|
744
|
+
) }, dateKey);
|
|
745
|
+
}),
|
|
746
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 text-right font-semibold text-xs tabular-nums", children: formatMinutesAsDecimal(getRowTotal(project.id)) || "0" })
|
|
747
|
+
] }, project.id)),
|
|
748
|
+
/* @__PURE__ */ jsx("tr", { className: "border-b", children: /* @__PURE__ */ jsx("td", { colSpan: visibleDays.length + 2, className: "sticky left-0 bg-background px-1 py-0.5", children: /* @__PURE__ */ jsx(
|
|
749
|
+
AddRowDropdown,
|
|
750
|
+
{
|
|
751
|
+
assignedProjects: allAssignedProjects,
|
|
752
|
+
visibleProjectIds,
|
|
753
|
+
canCreateProject: canManageProjects,
|
|
754
|
+
onAddProject: handleAddProject,
|
|
755
|
+
onCreateProject: () => setCreateDialogOpen(true)
|
|
756
|
+
}
|
|
757
|
+
) }) })
|
|
758
|
+
] }),
|
|
759
|
+
/* @__PURE__ */ jsx("tfoot", { children: /* @__PURE__ */ jsxs("tr", { className: "border-t bg-muted/50 font-semibold", children: [
|
|
760
|
+
/* @__PURE__ */ jsx("td", { className: "sticky left-0 z-10 bg-muted px-3 py-2", children: t("staff.timesheets.my.daily_total", "Daily Total") }),
|
|
761
|
+
visibleDays.map((date) => {
|
|
762
|
+
const weekend = isWeekendDay(date);
|
|
763
|
+
const dayMinutes = getDayTotal(date);
|
|
764
|
+
return /* @__PURE__ */ jsx("td", { className: `py-2 text-center text-xs tabular-nums ${weekend ? "text-muted-foreground/50" : ""}`, children: weekend ? "-" : formatMinutesAsDecimal(dayMinutes) || "-" }, formatDateKey(date));
|
|
765
|
+
}),
|
|
766
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-2 text-right tabular-nums font-semibold", children: formatMinutesAsDecimal(grandTotal) || "0" })
|
|
767
|
+
] }) })
|
|
690
768
|
] }),
|
|
691
|
-
/* @__PURE__ */ jsx("
|
|
692
|
-
|
|
693
|
-
visibleDays.map((date) => {
|
|
694
|
-
const weekend = isWeekendDay(date);
|
|
695
|
-
const dayMinutes = getDayTotal(date);
|
|
696
|
-
return /* @__PURE__ */ jsx("td", { className: `py-2 text-center text-xs tabular-nums ${weekend ? "text-muted-foreground/50" : ""}`, children: weekend ? "-" : minutesToDecimal(dayMinutes) || "-" }, formatDateKey(date));
|
|
697
|
-
}),
|
|
698
|
-
/* @__PURE__ */ jsx("td", { className: "px-3 py-2 text-right tabular-nums font-semibold", children: minutesToDecimal(grandTotal) || "0" })
|
|
699
|
-
] }) })
|
|
700
|
-
] }) }) })
|
|
769
|
+
/* @__PURE__ */ jsx("p", { className: "px-3 py-2 text-xs text-muted-foreground", children: durationFormatHint })
|
|
770
|
+
] }) })
|
|
701
771
|
] }),
|
|
702
772
|
ConfirmDialogElement,
|
|
703
773
|
/* @__PURE__ */ jsx(
|