@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.
@@ -1,4 +1,4 @@
1
- [build:core] found 3949 entry points
1
+ [build:core] found 3950 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 204 entry points
4
4
  [build:core:generated] built successfully
@@ -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
- const sanitized = value.replace(/[^0-9.,]/g, "");
305
+ clearCellError(projectId, dateKey);
290
306
  setRawText((prev) => {
291
307
  const projectTexts = { ...prev[projectId] ?? {} };
292
- projectTexts[dateKey] = sanitized;
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 minutes = decimalToMinutes(text);
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: minutesToDecimal(grandTotal) || "0" })
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: minutesToDecimal(Math.round(dailyAverage)) || "0" })
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-amber-600 font-medium", children: t("staff.timesheets.my.unsaved", "Unsaved changes") }),
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__ */ jsx("div", { className: "overflow-x-auto rounded-lg border", children: /* @__PURE__ */ jsxs("table", { className: "w-full text-sm table-fixed", children: [
608
- /* @__PURE__ */ jsxs("colgroup", { children: [
609
- /* @__PURE__ */ jsx("col", { className: viewMode === "weekly" ? "w-[35%]" : "w-[140px] min-w-[140px]" }),
610
- visibleDays.map((date) => /* @__PURE__ */ jsx("col", { className: viewMode === "weekly" ? "" : "w-[36px] min-w-[36px]" }, formatDateKey(date))),
611
- /* @__PURE__ */ jsx("col", { className: viewMode === "weekly" ? "w-[72px]" : "w-[56px] min-w-[56px]" })
612
- ] }),
613
- /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { className: "border-b bg-muted/50", children: [
614
- /* @__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") }),
615
- visibleDays.map((date) => {
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 dateKey = formatDateKey(date);
678
+ const dayName = getLocalizedDayName(date);
658
679
  const weekend = isWeekendDay(date);
659
- const cellMinutes = getCellValue(project.id, dateKey);
660
- const isDirty = dirty[project.id]?.[dateKey] !== void 0;
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
- type: "text",
665
- inputMode: "decimal",
666
- className: `mx-auto block rounded border text-center tabular-nums transition-colors
667
- ${viewMode === "weekly" ? "w-12 px-1 py-0.5 text-xs" : "w-8 px-0 py-1 text-[10px]"}
668
- ${isDirty ? "border-amber-400 bg-amber-50" : "border-muted-foreground/20 bg-transparent"}
669
- ${cellMinutes > 0 ? "font-semibold" : "text-muted-foreground"}
670
- hover:border-muted-foreground/40 focus:border-primary focus:bg-background focus:outline-none`,
671
- value: rawText[project.id]?.[dateKey] ?? minutesToDecimal(cellMinutes),
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("td", { className: "px-3 py-1.5 text-right font-semibold text-xs tabular-nums", children: minutesToDecimal(getRowTotal(project.id)) || "0" })
679
- ] }, project.id)),
680
- /* @__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(
681
- AddRowDropdown,
682
- {
683
- assignedProjects: allAssignedProjects,
684
- visibleProjectIds,
685
- canCreateProject: canManageProjects,
686
- onAddProject: handleAddProject,
687
- onCreateProject: () => setCreateDialogOpen(true)
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("tfoot", { children: /* @__PURE__ */ jsxs("tr", { className: "border-t bg-muted/50 font-semibold", children: [
692
- /* @__PURE__ */ jsx("td", { className: "sticky left-0 z-10 bg-muted px-3 py-2", children: t("staff.timesheets.my.daily_total", "Daily Total") }),
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(