@open-mercato/core 0.8.1-develop.7274.1.21a0083828 → 0.8.1-develop.7294.1.0ef99fc518
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/auth/api/users/acl/route.js.map +2 -2
- package/dist/modules/auth/components/AclEditor.js +21 -3
- package/dist/modules/auth/components/AclEditor.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies/page.js +6 -4
- package/dist/modules/customers/backend/customers/companies/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/map/components/DealsMapView.js +6 -4
- package/dist/modules/customers/backend/customers/deals/map/components/DealsMapView.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/page.js +6 -4
- package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ChangeOwnerDialog.js +5 -3
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ChangeOwnerDialog.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js +6 -4
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people/page.js +6 -4
- package/dist/modules/customers/backend/customers/people/page.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/PeopleField.js +9 -2
- package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/hooks.js +4 -2
- package/dist/modules/customers/components/calendar/editor/hooks.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/lookups.js +7 -3
- package/dist/modules/customers/components/calendar/editor/lookups.js.map +2 -2
- package/dist/modules/customers/components/detail/AssignRoleDialog.js +5 -2
- package/dist/modules/customers/components/detail/AssignRoleDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/RoleAssignmentRow.js +4 -2
- package/dist/modules/customers/components/detail/RoleAssignmentRow.js.map +2 -2
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +8 -5
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/DateTimeFields.js +10 -7
- package/dist/modules/customers/components/detail/schedule/DateTimeFields.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/ParticipantsField.js +4 -2
- package/dist/modules/customers/components/detail/schedule/ParticipantsField.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/fieldConfig.js +9 -0
- package/dist/modules/customers/components/detail/schedule/fieldConfig.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/index.js +3 -1
- package/dist/modules/customers/components/detail/schedule/index.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js +11 -4
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js.map +2 -2
- package/dist/modules/customers/components/formConfig.js +17 -9
- package/dist/modules/customers/components/formConfig.js.map +2 -2
- package/dist/modules/customers/data/validators.js +12 -7
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/customers/lib/assignableStaff.js +103 -22
- package/dist/modules/customers/lib/assignableStaff.js.map +2 -2
- package/dist/modules/customers/setup.js +10 -2
- package/dist/modules/customers/setup.js.map +2 -2
- package/dist/modules/data_sync/api/schedules/[id]/route.js +5 -0
- package/dist/modules/data_sync/api/schedules/[id]/route.js.map +2 -2
- package/dist/modules/data_sync/api/schedules/route.js +5 -0
- package/dist/modules/data_sync/api/schedules/route.js.map +2 -2
- package/dist/modules/data_sync/backend/data-sync/page.js +29 -5
- package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
- package/dist/modules/data_sync/components/IntegrationScheduleTab.js +48 -11
- package/dist/modules/data_sync/components/IntegrationScheduleTab.js.map +2 -2
- package/dist/modules/data_sync/data/validators.js +13 -2
- package/dist/modules/data_sync/data/validators.js.map +2 -2
- package/dist/modules/data_sync/lib/schedule-value.js +30 -0
- package/dist/modules/data_sync/lib/schedule-value.js.map +7 -0
- package/dist/modules/eudr/data/validators.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js +3 -4
- package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.js +3 -1
- package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js +2 -1
- package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/page.js +3 -4
- package/dist/modules/portal/frontend/[orgSlug]/portal/page.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/profile/page.js +3 -4
- package/dist/modules/portal/frontend/[orgSlug]/portal/profile/page.js.map +2 -2
- package/dist/modules/portal/lib/navigation.js +3 -4
- package/dist/modules/portal/lib/navigation.js.map +2 -2
- package/dist/modules/query_index/lib/engine.js +79 -14
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/page.js +122 -86
- package/dist/modules/staff/backend/staff/time-tracking/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +38 -17
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/reports/[id]/page.js +45 -18
- package/dist/modules/staff/backend/staff/time-tracking/reports/[id]/page.js.map +2 -2
- package/dist/modules/staff/extension-points.js +16 -0
- package/dist/modules/staff/extension-points.js.map +2 -2
- package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/[id]/page.js +3 -4
- package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/[id]/page.js.map +2 -2
- package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/page.js +3 -4
- package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/page.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/ReportSheet.js +14 -13
- package/dist/modules/staff/lib/time-tracking-ui/ReportSheet.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/TaskListView.js +60 -41
- package/dist/modules/staff/lib/time-tracking-ui/TaskListView.js.map +2 -2
- package/dist/modules/warranty_claims/api/ai/assess/route.js +2 -2
- package/dist/modules/warranty_claims/api/ai/assess/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/ai/draft-reply/route.js +3 -4
- package/dist/modules/warranty_claims/api/ai/draft-reply/route.js.map +3 -3
- package/dist/modules/warranty_claims/api/ai/suggest/route.js +3 -3
- package/dist/modules/warranty_claims/api/ai/suggest/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/assign/route.js +3 -3
- package/dist/modules/warranty_claims/api/assign/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/credit-memo/route.js +2 -2
- package/dist/modules/warranty_claims/api/credit-memo/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/entitlement/route.js +2 -2
- package/dist/modules/warranty_claims/api/entitlement/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/events/route.js +4 -4
- package/dist/modules/warranty_claims/api/events/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/external/claims/route.js +3 -3
- package/dist/modules/warranty_claims/api/external/claims/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/portal/claims/[id]/submit/route.js +2 -2
- package/dist/modules/warranty_claims/api/portal/claims/[id]/submit/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/portal/claims/[id]/withdraw/route.js +2 -2
- package/dist/modules/warranty_claims/api/portal/claims/[id]/withdraw/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/portal/claims/route.js +2 -2
- package/dist/modules/warranty_claims/api/portal/claims/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/portal/events/route.js +2 -2
- package/dist/modules/warranty_claims/api/portal/events/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/receiving/route.js +2 -2
- package/dist/modules/warranty_claims/api/receiving/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/replacement-order/route.js +2 -2
- package/dist/modules/warranty_claims/api/replacement-order/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/return-label/route.js +2 -2
- package/dist/modules/warranty_claims/api/return-label/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/risk/route.js +2 -2
- package/dist/modules/warranty_claims/api/risk/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/sales-return/route.js +2 -2
- package/dist/modules/warranty_claims/api/sales-return/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/settings-general/route.js +4 -4
- package/dist/modules/warranty_claims/api/settings-general/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/stats/route.js +2 -2
- package/dist/modules/warranty_claims/api/stats/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/submit/route.js +3 -3
- package/dist/modules/warranty_claims/api/submit/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/transition/route.js +3 -3
- package/dist/modules/warranty_claims/api/transition/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/vendor-recovery/route.js +3 -3
- package/dist/modules/warranty_claims/api/vendor-recovery/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/vendor-recovery-suggestions/route.js +2 -2
- package/dist/modules/warranty_claims/api/vendor-recovery-suggestions/route.js.map +2 -2
- package/dist/modules/warranty_claims/backend/page.js +4 -2
- package/dist/modules/warranty_claims/backend/page.js.map +2 -2
- package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.js +9 -2
- package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.js.map +2 -2
- package/dist/modules/warranty_claims/backend/warranty_claims/settings/page.js +4 -2
- package/dist/modules/warranty_claims/backend/warranty_claims/settings/page.js.map +2 -2
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.js +3 -4
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.js.map +2 -2
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.js +3 -2
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.js.map +2 -2
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.js +3 -2
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.js.map +2 -2
- package/dist/modules/workflows/components/DefinitionTriggersEditor.js +1 -1
- package/dist/modules/workflows/components/DefinitionTriggersEditor.js.map +2 -2
- package/dist/modules/workflows/frontend/[orgSlug]/portal/tasks/[id]/page.js +2 -2
- package/dist/modules/workflows/frontend/[orgSlug]/portal/tasks/[id]/page.js.map +2 -2
- package/dist/modules/workflows/lib/event-trigger-service.js +56 -1
- package/dist/modules/workflows/lib/event-trigger-service.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/api/users/acl/route.ts +4 -3
- package/src/modules/auth/components/AclEditor.tsx +38 -2
- package/src/modules/auth/i18n/de.json +5 -1
- package/src/modules/auth/i18n/en.json +5 -1
- package/src/modules/auth/i18n/es.json +5 -1
- package/src/modules/auth/i18n/ko.json +5 -1
- package/src/modules/auth/i18n/pl.json +5 -1
- package/src/modules/customers/backend/customers/companies/page.tsx +6 -4
- package/src/modules/customers/backend/customers/deals/map/components/DealsMapView.tsx +6 -4
- package/src/modules/customers/backend/customers/deals/page.tsx +6 -4
- package/src/modules/customers/backend/customers/deals/pipeline/components/ChangeOwnerDialog.tsx +5 -3
- package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +6 -4
- package/src/modules/customers/backend/customers/people/page.tsx +6 -4
- package/src/modules/customers/components/calendar/editor/PeopleField.tsx +9 -2
- package/src/modules/customers/components/calendar/editor/hooks.ts +4 -2
- package/src/modules/customers/components/calendar/editor/lookups.ts +12 -4
- package/src/modules/customers/components/detail/AssignRoleDialog.tsx +4 -1
- package/src/modules/customers/components/detail/RoleAssignmentRow.tsx +4 -2
- package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +15 -7
- package/src/modules/customers/components/detail/schedule/DateTimeFields.tsx +16 -7
- package/src/modules/customers/components/detail/schedule/ParticipantsField.tsx +4 -2
- package/src/modules/customers/components/detail/schedule/fieldConfig.ts +15 -0
- package/src/modules/customers/components/detail/schedule/index.ts +1 -1
- package/src/modules/customers/components/detail/schedule/useScheduleFormState.ts +16 -9
- package/src/modules/customers/components/formConfig.tsx +35 -12
- package/src/modules/customers/data/validators.ts +14 -8
- package/src/modules/customers/i18n/de.json +1 -0
- package/src/modules/customers/i18n/en.json +1 -0
- package/src/modules/customers/i18n/es.json +1 -0
- package/src/modules/customers/i18n/ko.json +1 -0
- package/src/modules/customers/i18n/pl.json +1 -0
- package/src/modules/customers/lib/assignableStaff.ts +172 -34
- package/src/modules/customers/setup.ts +8 -0
- package/src/modules/data_sync/api/schedules/[id]/route.ts +5 -0
- package/src/modules/data_sync/api/schedules/route.ts +5 -0
- package/src/modules/data_sync/backend/data-sync/page.tsx +48 -4
- package/src/modules/data_sync/components/IntegrationScheduleTab.tsx +61 -2
- package/src/modules/data_sync/data/validators.ts +26 -2
- package/src/modules/data_sync/i18n/de.json +2 -0
- package/src/modules/data_sync/i18n/en.json +2 -0
- package/src/modules/data_sync/i18n/es.json +2 -0
- package/src/modules/data_sync/i18n/ko.json +2 -0
- package/src/modules/data_sync/i18n/pl.json +2 -0
- package/src/modules/data_sync/lib/schedule-value.ts +45 -0
- package/src/modules/eudr/data/validators.ts +3 -4
- package/src/modules/portal/frontend/[orgSlug]/portal/dashboard/page.tsx +5 -4
- package/src/modules/portal/frontend/[orgSlug]/portal/invite/page.tsx +3 -1
- package/src/modules/portal/frontend/[orgSlug]/portal/login/page.tsx +2 -1
- package/src/modules/portal/frontend/[orgSlug]/portal/page.tsx +5 -5
- package/src/modules/portal/frontend/[orgSlug]/portal/profile/page.tsx +5 -4
- package/src/modules/portal/i18n/de.json +1 -0
- package/src/modules/portal/i18n/en.json +1 -0
- package/src/modules/portal/i18n/es.json +1 -0
- package/src/modules/portal/i18n/ko.json +1 -0
- package/src/modules/portal/i18n/pl.json +1 -0
- package/src/modules/portal/lib/navigation.ts +8 -3
- package/src/modules/query_index/lib/engine.ts +111 -16
- package/src/modules/staff/backend/staff/time-tracking/page.tsx +161 -130
- package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +55 -43
- package/src/modules/staff/backend/staff/time-tracking/reports/[id]/page.tsx +61 -38
- package/src/modules/staff/extension-points.ts +16 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/time-reports/[id]/page.tsx +3 -4
- package/src/modules/staff/frontend/[orgSlug]/portal/time-reports/page.tsx +3 -4
- package/src/modules/staff/i18n/de.json +3 -0
- package/src/modules/staff/i18n/en.json +3 -0
- package/src/modules/staff/i18n/es.json +3 -0
- package/src/modules/staff/i18n/ko.json +3 -0
- package/src/modules/staff/i18n/pl.json +3 -0
- package/src/modules/staff/lib/time-tracking-ui/ReportSheet.tsx +32 -29
- package/src/modules/staff/lib/time-tracking-ui/TaskListView.tsx +89 -79
- package/src/modules/warranty_claims/api/ai/assess/route.ts +2 -2
- package/src/modules/warranty_claims/api/ai/draft-reply/route.ts +2 -3
- package/src/modules/warranty_claims/api/ai/suggest/route.ts +3 -3
- package/src/modules/warranty_claims/api/assign/route.ts +3 -3
- package/src/modules/warranty_claims/api/credit-memo/route.ts +2 -2
- package/src/modules/warranty_claims/api/entitlement/route.ts +2 -2
- package/src/modules/warranty_claims/api/events/route.ts +4 -4
- package/src/modules/warranty_claims/api/external/claims/route.ts +3 -3
- package/src/modules/warranty_claims/api/portal/claims/[id]/submit/route.ts +2 -2
- package/src/modules/warranty_claims/api/portal/claims/[id]/withdraw/route.ts +2 -2
- package/src/modules/warranty_claims/api/portal/claims/route.ts +2 -2
- package/src/modules/warranty_claims/api/portal/events/route.ts +2 -2
- package/src/modules/warranty_claims/api/receiving/route.ts +2 -2
- package/src/modules/warranty_claims/api/replacement-order/route.ts +2 -2
- package/src/modules/warranty_claims/api/return-label/route.ts +2 -2
- package/src/modules/warranty_claims/api/risk/route.ts +2 -2
- package/src/modules/warranty_claims/api/sales-return/route.ts +2 -2
- package/src/modules/warranty_claims/api/settings-general/route.ts +4 -4
- package/src/modules/warranty_claims/api/stats/route.ts +2 -2
- package/src/modules/warranty_claims/api/submit/route.ts +3 -3
- package/src/modules/warranty_claims/api/transition/route.ts +3 -3
- package/src/modules/warranty_claims/api/vendor-recovery/route.ts +3 -3
- package/src/modules/warranty_claims/api/vendor-recovery-suggestions/route.ts +2 -2
- package/src/modules/warranty_claims/backend/page.tsx +4 -2
- package/src/modules/warranty_claims/backend/warranty_claims/[id]/page.tsx +9 -2
- package/src/modules/warranty_claims/backend/warranty_claims/settings/page.tsx +4 -2
- package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.tsx +5 -4
- package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.tsx +5 -2
- package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.tsx +5 -2
- package/src/modules/workflows/components/DefinitionTriggersEditor.tsx +1 -1
- package/src/modules/workflows/frontend/[orgSlug]/portal/tasks/[id]/page.tsx +2 -2
- package/src/modules/workflows/i18n/de.json +1 -1
- package/src/modules/workflows/i18n/en.json +1 -1
- package/src/modules/workflows/i18n/es.json +1 -1
- package/src/modules/workflows/i18n/ko.json +1 -1
- package/src/modules/workflows/i18n/pl.json +1 -1
- package/src/modules/workflows/lib/event-trigger-service.ts +139 -0
|
@@ -7,6 +7,7 @@ import { Page, PageBody, PageHeader } from "@open-mercato/ui/backend/Page";
|
|
|
7
7
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
8
8
|
import { Progress } from "@open-mercato/ui/primitives/progress";
|
|
9
9
|
import { StatusBadge } from "@open-mercato/ui/primitives/status-badge";
|
|
10
|
+
import { DataTable } from "@open-mercato/ui/backend/DataTable";
|
|
10
11
|
import {
|
|
11
12
|
Select,
|
|
12
13
|
SelectContent,
|
|
@@ -210,6 +211,60 @@ function TimeTrackingMyWorkPage() {
|
|
|
210
211
|
setQuickTargetKey(quickTargets[0].key);
|
|
211
212
|
}, [quickTargetKey, quickTargets]);
|
|
212
213
|
const todayTotal = data.entries.reduce((sum, entry) => sum + entry.durationMinutes, 0);
|
|
214
|
+
const todayEntriesColumns = React.useMemo(
|
|
215
|
+
() => [
|
|
216
|
+
{
|
|
217
|
+
id: "task",
|
|
218
|
+
header: t("staff.time_tracking.myWork.today.columns.task", "Task"),
|
|
219
|
+
cell: ({ row }) => {
|
|
220
|
+
const entry = row.original;
|
|
221
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
222
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
223
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium", children: entry.taskTitle ?? entry.description ?? t("staff.time_tracking.entries.table.noTask", "No task") }),
|
|
224
|
+
entry.isBillable ? null : /* @__PURE__ */ jsx(StatusBadge, { variant: "neutral", children: t("staff.time_tracking.entries.badges.nonBillable", "non-billable") }),
|
|
225
|
+
entry.isLocked ? /* @__PURE__ */ jsxs(StatusBadge, { variant: "neutral", children: [
|
|
226
|
+
/* @__PURE__ */ jsx(Lock, { className: "size-3", "aria-hidden": "true" }),
|
|
227
|
+
t("staff.time_tracking.entries.badges.locked", "locked")
|
|
228
|
+
] }) : null
|
|
229
|
+
] }),
|
|
230
|
+
/* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground", children: [entry.projectName, entry.taskTitle ? entry.description : null].filter((part) => Boolean(part)).join(" \xB7 ") })
|
|
231
|
+
] });
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
id: "clock",
|
|
236
|
+
header: t("staff.time_tracking.myWork.today.columns.time", "Time"),
|
|
237
|
+
meta: { maxWidth: "8rem" },
|
|
238
|
+
cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: formatClockRange(row.original) ?? "\u2014" })
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
id: "duration",
|
|
242
|
+
header: () => /* @__PURE__ */ jsx("span", { className: "block text-right", children: t("staff.time_tracking.myWork.today.columns.duration", "Duration") }),
|
|
243
|
+
meta: { maxWidth: "6rem" },
|
|
244
|
+
cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "block text-right font-mono font-semibold tabular-nums", children: formatDuration(row.original.durationMinutes, "clock") })
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
id: "actions",
|
|
248
|
+
header: "",
|
|
249
|
+
meta: { maxWidth: "3rem" },
|
|
250
|
+
cell: ({ row }) => {
|
|
251
|
+
const entry = row.original;
|
|
252
|
+
return canManageOwn && !entry.isLocked ? /* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx(
|
|
253
|
+
Button,
|
|
254
|
+
{
|
|
255
|
+
type: "button",
|
|
256
|
+
variant: "ghost",
|
|
257
|
+
size: "2xs",
|
|
258
|
+
"aria-label": t("staff.time_tracking.timesheet.list.edit", "Edit"),
|
|
259
|
+
onClick: () => setDialog({ open: true, entryId: entry.id }),
|
|
260
|
+
children: /* @__PURE__ */ jsx(Pencil, { className: "size-3.5", "aria-hidden": "true" })
|
|
261
|
+
}
|
|
262
|
+
) }) : null;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
],
|
|
266
|
+
[canManageOwn, t]
|
|
267
|
+
);
|
|
213
268
|
const submitQuickEntry = React.useCallback(async () => {
|
|
214
269
|
if (!quickTargetKey || quickMinutes === null || quickMinutes <= 0 || quickSaving) return;
|
|
215
270
|
const target = parseTargetKey(quickTargetKey);
|
|
@@ -402,97 +457,78 @@ function TimeTrackingMyWorkPage() {
|
|
|
402
457
|
/* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", asChild: true, children: /* @__PURE__ */ jsx(Link, { href: ENTRIES_HREF, children: t("staff.time_tracking.myWork.today.allEntries", "All entries") }) })
|
|
403
458
|
] })
|
|
404
459
|
] }),
|
|
405
|
-
data.entries.length === 0 ? /* @__PURE__ */ jsx("p", { className: "p-4 text-sm text-muted-foreground", children: t("staff.time_tracking.myWork.today.empty", "Nothing logged today yet.") }) : /* @__PURE__ */ jsxs(
|
|
406
|
-
/* @__PURE__ */
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
type: "button",
|
|
425
|
-
variant: "ghost",
|
|
426
|
-
size: "2xs",
|
|
427
|
-
"aria-label": t("staff.time_tracking.timesheet.list.edit", "Edit"),
|
|
428
|
-
onClick: () => setDialog({ open: true, entryId: entry.id }),
|
|
429
|
-
children: /* @__PURE__ */ jsx(Pencil, { className: "size-3.5", "aria-hidden": "true" })
|
|
460
|
+
data.entries.length === 0 ? /* @__PURE__ */ jsx("p", { className: "p-4 text-sm text-muted-foreground", children: t("staff.time_tracking.myWork.today.empty", "Nothing logged today yet.") }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
461
|
+
/* @__PURE__ */ jsx(
|
|
462
|
+
DataTable,
|
|
463
|
+
{
|
|
464
|
+
embedded: true,
|
|
465
|
+
extensionTableId: extensionPoints.hosts.myWorkTodayEntriesTable.tableId,
|
|
466
|
+
disableRowClick: true,
|
|
467
|
+
columns: todayEntriesColumns,
|
|
468
|
+
data: data.entries
|
|
469
|
+
}
|
|
470
|
+
),
|
|
471
|
+
canManageOwn && quickTargets.length > 0 ? /* @__PURE__ */ jsxs(
|
|
472
|
+
"div",
|
|
473
|
+
{
|
|
474
|
+
className: "flex flex-wrap items-start gap-2 border-t p-3",
|
|
475
|
+
onKeyDown: (event) => {
|
|
476
|
+
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
|
|
477
|
+
event.preventDefault();
|
|
478
|
+
void submitQuickEntry();
|
|
430
479
|
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
"div",
|
|
435
|
-
{
|
|
436
|
-
className: "flex flex-wrap items-start gap-2",
|
|
437
|
-
onKeyDown: (event) => {
|
|
438
|
-
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
|
|
439
|
-
event.preventDefault();
|
|
440
|
-
void submitQuickEntry();
|
|
441
|
-
}
|
|
442
|
-
},
|
|
443
|
-
children: [
|
|
444
|
-
/* @__PURE__ */ jsxs(Select, { value: quickTargetKey, onValueChange: setQuickTargetKey, children: [
|
|
445
|
-
/* @__PURE__ */ jsx(
|
|
446
|
-
SelectTrigger,
|
|
447
|
-
{
|
|
448
|
-
className: "w-72",
|
|
449
|
-
"aria-label": t("staff.time_tracking.myWork.quickEntry.target", "Project or task"),
|
|
450
|
-
children: /* @__PURE__ */ jsx(SelectValue, {})
|
|
451
|
-
}
|
|
452
|
-
),
|
|
453
|
-
/* @__PURE__ */ jsx(SelectContent, { children: quickTargets.map((target) => /* @__PURE__ */ jsx(SelectItem, { value: target.key, children: targetLabel(target) }, target.key)) })
|
|
454
|
-
] }),
|
|
455
|
-
/* @__PURE__ */ jsx("div", { className: "w-32", children: /* @__PURE__ */ jsx(
|
|
456
|
-
DurationInput,
|
|
457
|
-
{
|
|
458
|
-
variant: "compact",
|
|
459
|
-
value: quickMinutes,
|
|
460
|
-
onChange: (minutes) => setQuickMinutes(minutes),
|
|
461
|
-
ariaLabel: t("staff.time_tracking.myWork.quickEntry.duration", "Duration")
|
|
462
|
-
},
|
|
463
|
-
quickEpoch
|
|
464
|
-
) }),
|
|
480
|
+
},
|
|
481
|
+
children: [
|
|
482
|
+
/* @__PURE__ */ jsxs(Select, { value: quickTargetKey, onValueChange: setQuickTargetKey, children: [
|
|
465
483
|
/* @__PURE__ */ jsx(
|
|
466
|
-
|
|
484
|
+
SelectTrigger,
|
|
467
485
|
{
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
onClick: () => void submitQuickEntry(),
|
|
472
|
-
children: t("staff.time_tracking.myWork.quickEntry.save", "Save")
|
|
486
|
+
className: "w-72",
|
|
487
|
+
"aria-label": t("staff.time_tracking.myWork.quickEntry.target", "Project or task"),
|
|
488
|
+
children: /* @__PURE__ */ jsx(SelectValue, {})
|
|
473
489
|
}
|
|
474
490
|
),
|
|
475
|
-
/* @__PURE__ */ jsx(
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
491
|
+
/* @__PURE__ */ jsx(SelectContent, { children: quickTargets.map((target) => /* @__PURE__ */ jsx(SelectItem, { value: target.key, children: targetLabel(target) }, target.key)) })
|
|
492
|
+
] }),
|
|
493
|
+
/* @__PURE__ */ jsx("div", { className: "w-32", children: /* @__PURE__ */ jsx(
|
|
494
|
+
DurationInput,
|
|
495
|
+
{
|
|
496
|
+
variant: "compact",
|
|
497
|
+
value: quickMinutes,
|
|
498
|
+
onChange: (minutes) => setQuickMinutes(minutes),
|
|
499
|
+
ariaLabel: t("staff.time_tracking.myWork.quickEntry.duration", "Duration")
|
|
500
|
+
},
|
|
501
|
+
quickEpoch
|
|
502
|
+
) }),
|
|
503
|
+
/* @__PURE__ */ jsx(
|
|
504
|
+
Button,
|
|
505
|
+
{
|
|
506
|
+
type: "button",
|
|
507
|
+
size: "sm",
|
|
508
|
+
disabled: quickSaving || quickMinutes === null || quickMinutes <= 0,
|
|
509
|
+
onClick: () => void submitQuickEntry(),
|
|
510
|
+
children: t("staff.time_tracking.myWork.quickEntry.save", "Save")
|
|
511
|
+
}
|
|
512
|
+
),
|
|
513
|
+
/* @__PURE__ */ jsx("span", { className: "self-center text-xs text-muted-foreground", children: t("staff.time_tracking.entryDialog.shortcutSave", "\u2318\u21B5 to save") }),
|
|
514
|
+
/* @__PURE__ */ jsx("div", { className: "w-full", children: /* @__PURE__ */ jsx(
|
|
515
|
+
EntryDetailsFields,
|
|
516
|
+
{
|
|
517
|
+
idPrefix: "my-work-quick",
|
|
518
|
+
value: quickDetails,
|
|
519
|
+
onChange: setQuickDetails,
|
|
520
|
+
tagOptions,
|
|
521
|
+
onCreateTag: createTag,
|
|
522
|
+
disabled: quickSaving
|
|
523
|
+
}
|
|
524
|
+
) })
|
|
525
|
+
]
|
|
526
|
+
}
|
|
527
|
+
) : null,
|
|
528
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between border-t bg-muted/40 p-3 text-sm font-medium", children: [
|
|
529
|
+
/* @__PURE__ */ jsx("span", { children: t("staff.time_tracking.myWork.today.total", "Total today") }),
|
|
530
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono tabular-nums", children: formatDuration(todayTotal, "clock") })
|
|
531
|
+
] })
|
|
496
532
|
] })
|
|
497
533
|
] }),
|
|
498
534
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-6", children: [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/staff/backend/staff/time-tracking/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\n/**\n * Screen 1 \u2014 My work, the Team Member's landing page (T5.6).\n *\n * Everything on it comes from one request (`GET \u2026/timesheets/my-work`), so the\n * four KPIs, today's entries and the project budget bars can never disagree with\n * each other the way six independent list calls could.\n *\n * The mockup's five notes:\n *\n * 1. The running-timer pill lives in the top bar and is contributed by the\n * `timer-sidebar-indicator` injection widget, not by this page.\n * 2. **TL-only navigation is absent, not disabled** \u2014 the sidebar is built from\n * page metadata whose `requireFeatures` are evaluated per user, so a TM never\n * receives the Projects / Reports / Settings entries at all (confirmed at\n * `nav.ts`: an unmet feature `continue`s past the entry).\n * 3. The quick-entry row uses the same duration parser as the rest of the module\n * and saves with `\u2318\u21B5`.\n * 4. Budget bars come from the project's own budget; a project without one shows\n * hours and no bar. An `amount` budget is money, so it is absent for a caller\n * without `staff.timesheets.rates.view` rather than rendered at zero.\n * 5. \"Copy yesterday\" duplicates the previous day's entries as drafts to correct\n * (US-D6), and reports what it skipped \u2014 a locked entry is skipped, never\n * silently dropped.\n *\n * Screen 2 (no assignments) is the same page with `projects: []`: it renders\n * `NoTimeProjectAssignments`, which already carries the copy and the single\n * available action.\n */\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { CalendarDays, Copy, Lock, Pencil, Plus } from 'lucide-react'\nimport { Page, PageBody, PageHeader } from '@open-mercato/ui/backend/Page'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Progress } from '@open-mercato/ui/primitives/progress'\nimport { StatusBadge } from '@open-mercato/ui/primitives/status-badge'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { KpiCard } from '@open-mercato/ui/backend/charts'\nimport { ErrorMessage, LoadingMessage } from '@open-mercato/ui/backend/detail'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { createCrud } from '@open-mercato/ui/backend/utils/crud'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'\nimport { extensionPoints } from '@open-mercato/core/modules/staff/extension-points'\nimport { useBackendChrome } from '@open-mercato/ui/backend/BackendChromeProvider'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { hasFeature } from '@open-mercato/shared/security/features'\nimport { formatCurrency } from '@open-mercato/ui/utils/format'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { DurationInput } from '../../../lib/time-tracking-ui/DurationInput'\nimport {\n EntryDetailsFields,\n emptyEntryDetails,\n type EntryDetailsValue,\n} from '../../../lib/time-tracking-ui/EntryDetailsFields'\nimport { buildEntryClocks } from '../../../lib/time-tracking-ui/taskDrawerData'\nimport { slugifyProjectName } from '../../../lib/time-tracking/projectCode'\nimport type { TagOption } from '../../../lib/time-tracking-ui/timeEntryDialogState'\nimport { TimeEntryDialog } from '../../../lib/time-tracking-ui/TimeEntryDialog'\nimport { NoTimeProjectAssignments } from '../../../lib/time-tracking-ui/NoProjectAccess'\nimport { formatDuration } from '../../../lib/time-tracking/duration'\nimport { formatLongDayLabel, todayIso } from '../../../lib/time-tracking-ui/timesheetPeriod'\nimport { buildLogTargets, parseTargetKey, targetLabel } from '../../../lib/time-tracking-ui/timesheetTargets'\nimport { isEntryLockedError } from '../../../lib/time-tracking-ui/timeEntryListData'\n\nconst logger = createLogger('staff').child({ component: 'time-tracking/my-work' })\n\nconst MY_WORK_ENDPOINT = '/api/staff/timesheets/my-work'\nconst ENTRIES_API_PATH = 'staff/timesheets/time-entries'\nconst MANAGE_OWN_FEATURE = 'staff.timesheets.manage_own'\nconst MUTATION_CONTEXT_ID = 'staff.time_tracking.myWork'\nconst TIMESHEET_HREF = '/backend/staff/time-tracking/timesheet'\nconst ENTRIES_HREF = '/backend/staff/time-tracking/entries'\n\ntype MyWorkEntry = {\n id: string\n date: string\n taskId: string | null\n taskTitle: string | null\n timeProjectId: string | null\n projectName: string | null\n description: string | null\n startedAt: string | null\n endedAt: string | null\n durationMinutes: number\n isBillable: boolean\n isLocked: boolean\n}\n\ntype MyWorkProject = {\n id: string\n name: string\n code: string | null\n color: string | null\n customerName: string | null\n myMinutes: number\n totalMinutes: number\n hourlyRate?: number | null\n currencyCode?: string | null\n budget: {\n kind: 'hours' | 'amount'\n budgetValue: number\n usedValue: number\n percent: number\n barPercent: number\n tone: string\n } | null\n}\n\ntype MyWorkTask = {\n id: string\n title: string\n timeProjectId: string\n projectName: string | null\n}\n\ntype MyWorkPayload = {\n staffMember: { id: string; displayName: string } | null\n today: string\n kpis: {\n todayMinutes: number\n weekMinutes: number\n monthMinutes: number\n monthNonBillableMinutes: number\n dailyTargetMinutes: number | null\n weekTargetMinutes: number | null\n monthTargetMinutes: number | null\n weekWorkingDays: number\n monthWorkingDays: number\n nonBillableSharePercent: number | null\n }\n entries: MyWorkEntry[]\n projects: MyWorkProject[]\n recentTasks: MyWorkTask[]\n}\n\nconst EMPTY_PAYLOAD: MyWorkPayload = {\n staffMember: null,\n today: todayIso(),\n kpis: {\n todayMinutes: 0,\n weekMinutes: 0,\n monthMinutes: 0,\n monthNonBillableMinutes: 0,\n dailyTargetMinutes: null,\n weekTargetMinutes: null,\n monthTargetMinutes: null,\n weekWorkingDays: 0,\n monthWorkingDays: 0,\n nonBillableSharePercent: null,\n },\n entries: [],\n projects: [],\n recentTasks: [],\n}\n\nfunction formatClockRange(entry: MyWorkEntry, locale?: string): string | null {\n if (!entry.startedAt || !entry.endedAt) return null\n const format = (iso: string) => {\n const parsed = new Date(iso)\n if (Number.isNaN(parsed.getTime())) return ''\n try {\n return new Intl.DateTimeFormat(locale, { hour: '2-digit', minute: '2-digit', hour12: false }).format(parsed)\n } catch {\n return ''\n }\n }\n const start = format(entry.startedAt)\n const end = format(entry.endedAt)\n return start && end ? `${start} \u2013 ${end}` : null\n}\n\nexport default function TimeTrackingMyWorkPage() {\n const t = useT()\n const scopeVersion = useOrganizationScopeVersion()\n const { payload: chrome } = useBackendChrome()\n const canManageOwn = hasFeature(chrome?.grantedFeatures, MANAGE_OWN_FEATURE)\n\n const [data, setData] = React.useState<MyWorkPayload>(EMPTY_PAYLOAD)\n const [loadState, setLoadState] = React.useState<'loading' | 'ready' | 'error'>('loading')\n const [dialog, setDialog] = React.useState<{ open: boolean; entryId: string | null }>({\n open: false,\n entryId: null,\n })\n const [quickTargetKey, setQuickTargetKey] = React.useState('')\n const [quickMinutes, setQuickMinutes] = React.useState<number | null>(null)\n const [quickEpoch, setQuickEpoch] = React.useState(0)\n const [quickDetails, setQuickDetails] = React.useState<EntryDetailsValue>(() => emptyEntryDetails(''))\n const [tagOptions, setTagOptions] = React.useState<TagOption[]>([])\n\n // The date defaults to the day the page is showing, once that is known.\n React.useEffect(() => {\n if (!data.today) return\n setQuickDetails((current) => (current.date ? current : { ...current, date: data.today }))\n }, [data.today])\n\n React.useEffect(() => {\n let cancelled = false\n void (async () => {\n try {\n const res = await Promise.resolve(\n apiCall<{ items?: Array<Record<string, unknown>> }>('/api/staff/timesheets/tags?page=1&pageSize=100'),\n )\n if (cancelled || !res?.ok) return\n const items = Array.isArray(res.result?.items) ? res.result.items : []\n setTagOptions(\n items\n .map((row: Record<string, unknown>) => {\n const id = typeof row.id === 'string' ? row.id : null\n const label = typeof row.label === 'string' ? row.label : null\n return id && label\n ? { id, label, color: typeof row.color === 'string' ? row.color : null }\n : null\n })\n .filter((tag): tag is TagOption => tag !== null),\n )\n } catch {\n // A missing tag vocabulary leaves the picker empty rather than the page broken.\n }\n })()\n return () => { cancelled = true }\n }, [])\n\n const createTag = React.useCallback(\n async (label: string): Promise<TagOption | null> => {\n try {\n const created = await createCrud('/api/staff/timesheets/tags', {\n label,\n slug: slugifyProjectName(label).toLowerCase(),\n }, { errorMessage: t('staff.time_tracking.entryDialog.errors.createTag', 'Could not create the tag.') })\n const payload = created as unknown as Record<string, unknown> | null\n const raw = payload?.id ?? (payload?.result as Record<string, unknown> | undefined)?.id\n const id = typeof raw === 'string' ? raw : null\n if (!id) return null\n const tag: TagOption = { id, label, color: null }\n setTagOptions((current) => [...current, tag])\n return tag\n } catch (error) {\n logger.warn('staff.time_tracking.my-work tag create failed', { err: error })\n return null\n }\n },\n [t],\n )\n const [quickSaving, setQuickSaving] = React.useState(false)\n\n const loadError = t('staff.time_tracking.myWork.errors.load', 'Failed to load your work summary.')\n\n // A failed request leaves `data` at EMPTY_PAYLOAD, which reads as screen 2 \u2014 \"no\n // assignments\" \u2014 so the failure has to be a state of its own or the page reports a\n // server fault as an empty account.\n const load = React.useCallback(async () => {\n try {\n const result = await readApiResultOrThrow<MyWorkPayload>(MY_WORK_ENDPOINT, undefined, {\n errorMessage: loadError,\n fallback: EMPTY_PAYLOAD,\n })\n setData({ ...EMPTY_PAYLOAD, ...result })\n setLoadState('ready')\n } catch (error) {\n logger.error('staff.time_tracking.my-work load failed', { err: error })\n setLoadState('error')\n }\n }, [loadError])\n\n const retryLoad = React.useCallback(() => {\n setLoadState('loading')\n void load()\n }, [load])\n\n React.useEffect(() => {\n void load()\n }, [load, scopeVersion])\n\n const { runMutation, retryLastMutation } = useGuardedMutation<{\n formId: string\n resourceKind: string\n retryLastMutation: () => Promise<boolean>\n }>({\n contextId: MUTATION_CONTEXT_ID,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n\n const mutationContext = React.useMemo(\n () => ({\n formId: MUTATION_CONTEXT_ID,\n resourceKind: 'staff.timesheets.time_entry',\n retryLastMutation,\n }),\n [retryLastMutation],\n )\n\n const myWorkInjectionContext = React.useMemo(\n () => ({\n staffMemberId: data.staffMember?.id ?? null,\n today: data.today,\n projectIds: data.projects.map((project) => project.id),\n retryLastMutation,\n }),\n [data.projects, data.staffMember?.id, data.today, retryLastMutation],\n )\n\n const quickTargets = React.useMemo(\n () =>\n buildLogTargets(\n data.projects.map((project) => ({\n id: project.id,\n name: project.name,\n code: project.code,\n color: project.color,\n })),\n data.recentTasks.map((task) => ({\n id: task.id,\n title: task.title,\n timeProjectId: task.timeProjectId,\n })),\n ),\n [data.projects, data.recentTasks],\n )\n\n React.useEffect(() => {\n if (quickTargetKey || quickTargets.length === 0) return\n setQuickTargetKey(quickTargets[0].key)\n }, [quickTargetKey, quickTargets])\n\n const todayTotal = data.entries.reduce((sum, entry) => sum + entry.durationMinutes, 0)\n\n const submitQuickEntry = React.useCallback(async () => {\n if (!quickTargetKey || quickMinutes === null || quickMinutes <= 0 || quickSaving) return\n const target = parseTargetKey(quickTargetKey)\n const body = {\n staffMemberId: data.staffMember?.id,\n date: quickDetails.date || data.today,\n timeProjectId: target.timeProjectId,\n taskId: target.taskId,\n durationMinutes: quickMinutes,\n description: quickDetails.description.trim() || null,\n isBillable: quickDetails.isBillable,\n tagIds: quickDetails.tagIds.length > 0 ? quickDetails.tagIds : undefined,\n // Paired or omitted \u2014 a lone start is how the list identifies a running\n // timer, and a manual entry has no segment to stop.\n ...buildEntryClocks({\n date: quickDetails.date || data.today,\n startClock: quickDetails.startClock || null,\n endClock: quickDetails.endClock || null,\n minutes: quickMinutes,\n }),\n source: 'manual',\n }\n setQuickSaving(true)\n try {\n await runMutation({\n operation: () =>\n createCrud(ENTRIES_API_PATH, body, {\n errorMessage: t('staff.timesheets.my.errors.save', 'Failed to save timesheets.'),\n }),\n context: mutationContext,\n mutationPayload: body as unknown as Record<string, unknown>,\n })\n setQuickMinutes(null)\n setQuickEpoch((epoch) => epoch + 1)\n // The date and the billable flag survive; the rest clears. Logging three\n // lines against yesterday should not mean re-picking yesterday each time.\n setQuickDetails((current) => ({\n ...emptyEntryDetails(current.date, current.isBillable),\n }))\n await load()\n } catch (error) {\n logger.error('staff.time_tracking.my-work quick entry failed', { err: error })\n flash(t('staff.timesheets.my.errors.save', 'Failed to save timesheets.'), 'error')\n } finally {\n setQuickSaving(false)\n }\n }, [data.staffMember?.id, data.today, load, mutationContext, quickDetails, quickMinutes, quickSaving, quickTargetKey, runMutation, t])\n\n const copyYesterday = React.useCallback(async () => {\n const today = new Date(`${data.today}T00:00:00.000Z`)\n const yesterday = new Date(today)\n yesterday.setUTCDate(yesterday.getUTCDate() - 1)\n const body = {\n fromDate: yesterday.toISOString().slice(0, 10),\n toDate: data.today,\n }\n try {\n const result = await runMutation({\n operation: () =>\n createCrud<{ copied?: number; skipped?: unknown[] }>(`${ENTRIES_API_PATH}/copy-day`, body, {\n errorMessage: t('staff.time_tracking.entries.errors.copyDay', 'Could not copy the day.'),\n }),\n context: mutationContext,\n mutationPayload: body,\n })\n const copied = (result?.result as { copied?: number } | undefined)?.copied ?? 0\n flash(\n t('staff.time_tracking.myWork.copyDay.done', 'Copied {count} entries from yesterday.', {\n count: String(copied),\n }),\n 'success',\n )\n await load()\n } catch (error) {\n if (isEntryLockedError(error)) {\n flash(\n t(\n 'staff.time_tracking.entries.errors.entryLocked',\n 'This time entry is locked in a closed report and cannot be changed. Unlock the report first.',\n ),\n 'error',\n )\n return\n }\n logger.error('staff.time_tracking.my-work copy day failed', { err: error })\n flash(t('staff.time_tracking.entries.errors.copyDay', 'Could not copy the day.'), 'error')\n }\n }, [data.today, load, mutationContext, runMutation, t])\n\n if (loadState === 'loading') {\n return (\n <Page>\n <PageHeader title={t('staff.time_tracking.nav.my_work', 'My work')} />\n <PageBody>\n <LoadingMessage label={t('staff.timesheets.my.loading', 'Loading timesheets...')} />\n </PageBody>\n </Page>\n )\n }\n\n if (loadState === 'error') {\n return (\n <Page>\n <PageHeader title={t('staff.time_tracking.nav.my_work', 'My work')} />\n <PageBody>\n <ErrorMessage\n label={loadError}\n action={\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={retryLoad}>\n {t('staff.time_tracking.myWork.errors.retry', 'Try again')}\n </Button>\n }\n />\n </PageBody>\n </Page>\n )\n }\n\n const subtitle = [formatLongDayLabel(data.today), data.staffMember?.displayName]\n .filter((part): part is string => Boolean(part))\n .join(' \u00B7 ')\n\n const noAssignments = data.projects.length === 0 && data.entries.length === 0\n\n return (\n <Page>\n <PageHeader\n title={t('staff.time_tracking.nav.my_work', 'My work')}\n description={subtitle}\n actions={\n <div className=\"flex items-center gap-2\">\n <Button variant=\"outline\" asChild>\n <Link href={TIMESHEET_HREF}>\n <CalendarDays className=\"size-4\" aria-hidden=\"true\" />\n {t('staff.time_tracking.nav.timesheet', 'Timesheet')}\n </Link>\n </Button>\n {canManageOwn && !noAssignments ? (\n <Button type=\"button\" onClick={() => setDialog({ open: true, entryId: null })}>\n <Plus className=\"size-4\" aria-hidden=\"true\" />\n {t('staff.time_tracking.entries.actions.add', 'Add entry')}\n </Button>\n ) : null}\n </div>\n }\n />\n <PageBody>\n {noAssignments ? (\n <NoTimeProjectAssignments />\n ) : (\n <div className=\"flex flex-col gap-6\">\n <InjectionSpot\n spotId={extensionPoints.hosts.myWorkBeforeSections.spotId}\n context={myWorkInjectionContext}\n data={data}\n />\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4\">\n <KpiCard\n title={t('staff.time_tracking.myWork.kpi.today', 'Today')}\n value={data.kpis.todayMinutes}\n formatValue={(value) => formatDuration(value, 'clock')}\n comparisonLabel={\n data.kpis.dailyTargetMinutes !== null\n ? t('staff.time_tracking.myWork.kpi.targetGap', 'target {target} \u2014 {gap} to go', {\n target: formatDuration(data.kpis.dailyTargetMinutes, 'clock'),\n gap: formatDuration(\n Math.max(0, data.kpis.dailyTargetMinutes - data.kpis.todayMinutes),\n 'clock',\n ),\n })\n : undefined\n }\n />\n <KpiCard\n title={t('staff.time_tracking.myWork.kpi.week', 'This week')}\n value={data.kpis.weekMinutes}\n formatValue={(value) => formatDuration(value, 'clock')}\n comparisonLabel={\n data.kpis.weekTargetMinutes !== null\n ? t('staff.time_tracking.myWork.kpi.ofTarget', 'of {target}', {\n target: formatDuration(data.kpis.weekTargetMinutes, 'clock'),\n })\n : undefined\n }\n />\n <KpiCard\n title={t('staff.time_tracking.myWork.kpi.month', 'This month')}\n value={data.kpis.monthMinutes}\n formatValue={(value) => formatDuration(value, 'clock')}\n comparisonLabel={t('staff.time_tracking.myWork.kpi.workingDays', '{days} working days', {\n days: String(data.kpis.monthWorkingDays),\n })}\n />\n <KpiCard\n title={t('staff.time_tracking.myWork.kpi.nonBillable', 'Non-billable')}\n value={data.kpis.monthNonBillableMinutes}\n formatValue={(value) => formatDuration(value, 'clock')}\n comparisonLabel={\n data.kpis.nonBillableSharePercent !== null\n ? t('staff.time_tracking.myWork.kpi.shareOfMonth', '{percent}% of the month', {\n percent: String(data.kpis.nonBillableSharePercent),\n })\n : undefined\n }\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 xl:grid-cols-3\">\n <section className=\"rounded-lg border xl:col-span-2\">\n <div className=\"flex flex-wrap items-center justify-between gap-2 border-b p-4\">\n <h2 className=\"text-sm font-semibold\">\n {t('staff.time_tracking.myWork.today.title', 'Today\u2019s entries')}\n </h2>\n <div className=\"flex items-center gap-2\">\n {canManageOwn ? (\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => void copyYesterday()}>\n <Copy className=\"size-4\" aria-hidden=\"true\" />\n {t('staff.time_tracking.myWork.today.copyYesterday', 'Copy yesterday')}\n </Button>\n ) : null}\n <Button variant=\"outline\" size=\"sm\" asChild>\n <Link href={ENTRIES_HREF}>\n {t('staff.time_tracking.myWork.today.allEntries', 'All entries')}\n </Link>\n </Button>\n </div>\n </div>\n\n {data.entries.length === 0 ? (\n <p className=\"p-4 text-sm text-muted-foreground\">\n {t('staff.time_tracking.myWork.today.empty', 'Nothing logged today yet.')}\n </p>\n ) : (\n <table className=\"w-full text-sm\">\n <tbody>\n {data.entries.map((entry) => (\n <tr key={entry.id} className=\"border-b last:border-0\">\n <td className=\"p-3 align-top\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"font-medium\">\n {entry.taskTitle ??\n entry.description ??\n t('staff.time_tracking.entries.table.noTask', 'No task')}\n </span>\n {entry.isBillable ? null : (\n <StatusBadge variant=\"neutral\">\n {t('staff.time_tracking.entries.badges.nonBillable', 'non-billable')}\n </StatusBadge>\n )}\n {entry.isLocked ? (\n <StatusBadge variant=\"neutral\">\n <Lock className=\"size-3\" aria-hidden=\"true\" />\n {t('staff.time_tracking.entries.badges.locked', 'locked')}\n </StatusBadge>\n ) : null}\n </div>\n <div className=\"text-xs text-muted-foreground\">\n {[entry.projectName, entry.taskTitle ? entry.description : null]\n .filter((part): part is string => Boolean(part))\n .join(' \u00B7 ')}\n </div>\n </td>\n <td className=\"w-32 p-3 align-top text-xs text-muted-foreground\">\n {formatClockRange(entry) ?? '\u2014'}\n </td>\n <td className=\"w-20 p-3 text-right align-top font-mono font-semibold tabular-nums\">\n {formatDuration(entry.durationMinutes, 'clock')}\n </td>\n <td className=\"w-16 p-3 text-right align-top\">\n {canManageOwn && !entry.isLocked ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"2xs\"\n aria-label={t('staff.time_tracking.timesheet.list.edit', 'Edit')}\n onClick={() => setDialog({ open: true, entryId: entry.id })}\n >\n <Pencil className=\"size-3.5\" aria-hidden=\"true\" />\n </Button>\n ) : null}\n </td>\n </tr>\n ))}\n {canManageOwn && quickTargets.length > 0 ? (\n <tr>\n <td colSpan={4} className=\"p-3\">\n <div\n className=\"flex flex-wrap items-start gap-2\"\n onKeyDown={(event) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n void submitQuickEntry()\n }\n }}\n >\n <Select value={quickTargetKey} onValueChange={setQuickTargetKey}>\n <SelectTrigger\n className=\"w-72\"\n aria-label={t('staff.time_tracking.myWork.quickEntry.target', 'Project or task')}\n >\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {quickTargets.map((target) => (\n <SelectItem key={target.key} value={target.key}>\n {targetLabel(target)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <div className=\"w-32\">\n <DurationInput\n key={quickEpoch}\n variant=\"compact\"\n value={quickMinutes}\n onChange={(minutes) => setQuickMinutes(minutes)}\n ariaLabel={t('staff.time_tracking.myWork.quickEntry.duration', 'Duration')}\n />\n </div>\n <Button\n type=\"button\"\n size=\"sm\"\n disabled={quickSaving || quickMinutes === null || quickMinutes <= 0}\n onClick={() => void submitQuickEntry()}\n >\n {t('staff.time_tracking.myWork.quickEntry.save', 'Save')}\n </Button>\n <span className=\"self-center text-xs text-muted-foreground\">\n {t('staff.time_tracking.entryDialog.shortcutSave', '\u2318\u21B5 to save')}\n </span>\n {/*\n * The same block the task drawer offers, from the same\n * component: a row that can log an hour but not say what\n * it was for, or when, sends people to the full dialog\n * for the one field they were missing.\n */}\n <div className=\"w-full\">\n <EntryDetailsFields\n idPrefix=\"my-work-quick\"\n value={quickDetails}\n onChange={setQuickDetails}\n tagOptions={tagOptions}\n onCreateTag={createTag}\n disabled={quickSaving}\n />\n </div>\n </div>\n </td>\n </tr>\n ) : null}\n </tbody>\n <tfoot>\n <tr className=\"border-t bg-muted/40 font-medium\">\n <td className=\"p-3\" colSpan={2}>\n {t('staff.time_tracking.myWork.today.total', 'Total today')}\n </td>\n <td className=\"p-3 text-right font-mono tabular-nums\">\n {formatDuration(todayTotal, 'clock')}\n </td>\n <td />\n </tr>\n </tfoot>\n </table>\n )}\n </section>\n\n <div className=\"flex flex-col gap-6\">\n <section className=\"rounded-lg border\">\n <div className=\"flex items-center justify-between border-b p-4\">\n <h2 className=\"text-sm font-semibold\">\n {t('staff.time_tracking.myWork.projects.title', 'My projects')}\n </h2>\n <span className=\"text-xs text-muted-foreground\">{data.projects.length}</span>\n </div>\n <div className=\"flex flex-col gap-4 p-4\">\n {data.projects.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">\n {t('staff.time_tracking.myWork.projects.empty', 'No projects assigned yet.')}\n </p>\n ) : (\n data.projects.map((project) => (\n <div key={project.id} className=\"flex flex-col gap-1\">\n <div className=\"flex items-center justify-between gap-2\">\n <Link\n href={`/backend/staff/time-tracking/projects/${project.id}`}\n className=\"min-w-0 truncate font-medium hover:underline\"\n >\n {project.name}\n </Link>\n <span className=\"shrink-0 font-mono text-xs tabular-nums text-muted-foreground\">\n {formatDuration(project.myMinutes, 'clock')}\n </span>\n </div>\n <p className=\"text-xs text-muted-foreground\">\n {[\n project.customerName,\n project.hourlyRate != null && project.currencyCode\n ? t('staff.time_tracking.myWork.projects.rate', '{rate}/h', {\n rate: formatCurrency(project.hourlyRate, project.currencyCode) ?? '',\n })\n : null,\n ]\n .filter((part): part is string => Boolean(part))\n .join(' \u00B7 ')}\n </p>\n {project.budget ? (\n <>\n <Progress\n value={project.budget.barPercent}\n size=\"sm\"\n tone={\n project.budget.tone === 'destructive'\n ? 'destructive'\n : project.budget.tone === 'warning'\n ? 'warning'\n : 'accent'\n }\n />\n <p className=\"text-xs text-muted-foreground\">\n {t('staff.time_tracking.myWork.projects.budget', '{percent}% of budget ({value})', {\n percent: String(project.budget.percent),\n value:\n project.budget.kind === 'hours'\n ? t('staff.time_tracking.myWork.projects.budgetHours', '{hours} h', {\n hours: String(project.budget.budgetValue),\n })\n : formatCurrency(project.budget.budgetValue, project.currencyCode ?? undefined) ??\n String(project.budget.budgetValue),\n })}\n </p>\n </>\n ) : null}\n </div>\n ))\n )}\n </div>\n </section>\n\n <section className=\"rounded-lg border\">\n <div className=\"border-b p-4\">\n <h2 className=\"text-sm font-semibold\">\n {t('staff.time_tracking.myWork.recentTasks.title', 'Recent tasks')}\n </h2>\n </div>\n <div className=\"flex flex-col gap-2 p-4\">\n {data.recentTasks.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">\n {t('staff.time_tracking.myWork.recentTasks.empty', 'No tasks logged against yet.')}\n </p>\n ) : (\n data.recentTasks.map((task) => (\n <Link\n key={task.id}\n href={`/backend/staff/time-tracking/projects/${task.timeProjectId}/board?task=${task.id}`}\n className=\"flex items-center justify-between gap-2 truncate text-sm hover:underline\"\n >\n <span className=\"truncate\">{task.title}</span>\n <span className=\"shrink-0 text-xs text-muted-foreground\">{task.projectName}</span>\n </Link>\n ))\n )}\n </div>\n </section>\n </div>\n </div>\n <InjectionSpot\n spotId={extensionPoints.hosts.myWorkAfterSections.spotId}\n context={myWorkInjectionContext}\n data={data}\n />\n </div>\n )}\n </PageBody>\n\n <TimeEntryDialog\n open={dialog.open}\n onOpenChange={(open) => setDialog((current) => ({ ...current, open }))}\n entryId={dialog.entryId}\n defaults={{ date: data.today }}\n onSaved={() => {\n void load()\n }}\n />\n </Page>\n )\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\n/**\n * Screen 1 \u2014 My work, the Team Member's landing page (T5.6).\n *\n * Everything on it comes from one request (`GET \u2026/timesheets/my-work`), so the\n * four KPIs, today's entries and the project budget bars can never disagree with\n * each other the way six independent list calls could.\n *\n * The mockup's five notes:\n *\n * 1. The running-timer pill lives in the top bar and is contributed by the\n * `timer-sidebar-indicator` injection widget, not by this page.\n * 2. **TL-only navigation is absent, not disabled** \u2014 the sidebar is built from\n * page metadata whose `requireFeatures` are evaluated per user, so a TM never\n * receives the Projects / Reports / Settings entries at all (confirmed at\n * `nav.ts`: an unmet feature `continue`s past the entry).\n * 3. The quick-entry row uses the same duration parser as the rest of the module\n * and saves with `\u2318\u21B5`.\n * 4. Budget bars come from the project's own budget; a project without one shows\n * hours and no bar. An `amount` budget is money, so it is absent for a caller\n * without `staff.timesheets.rates.view` rather than rendered at zero.\n * 5. \"Copy yesterday\" duplicates the previous day's entries as drafts to correct\n * (US-D6), and reports what it skipped \u2014 a locked entry is skipped, never\n * silently dropped.\n *\n * Screen 2 (no assignments) is the same page with `projects: []`: it renders\n * `NoTimeProjectAssignments`, which already carries the copy and the single\n * available action.\n */\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { CalendarDays, Copy, Lock, Pencil, Plus } from 'lucide-react'\nimport { Page, PageBody, PageHeader } from '@open-mercato/ui/backend/Page'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Progress } from '@open-mercato/ui/primitives/progress'\nimport { StatusBadge } from '@open-mercato/ui/primitives/status-badge'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { KpiCard } from '@open-mercato/ui/backend/charts'\nimport { ErrorMessage, LoadingMessage } from '@open-mercato/ui/backend/detail'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { createCrud } from '@open-mercato/ui/backend/utils/crud'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'\nimport { extensionPoints } from '@open-mercato/core/modules/staff/extension-points'\nimport { useBackendChrome } from '@open-mercato/ui/backend/BackendChromeProvider'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { hasFeature } from '@open-mercato/shared/security/features'\nimport { formatCurrency } from '@open-mercato/ui/utils/format'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { DurationInput } from '../../../lib/time-tracking-ui/DurationInput'\nimport {\n EntryDetailsFields,\n emptyEntryDetails,\n type EntryDetailsValue,\n} from '../../../lib/time-tracking-ui/EntryDetailsFields'\nimport { buildEntryClocks } from '../../../lib/time-tracking-ui/taskDrawerData'\nimport { slugifyProjectName } from '../../../lib/time-tracking/projectCode'\nimport type { TagOption } from '../../../lib/time-tracking-ui/timeEntryDialogState'\nimport { TimeEntryDialog } from '../../../lib/time-tracking-ui/TimeEntryDialog'\nimport { NoTimeProjectAssignments } from '../../../lib/time-tracking-ui/NoProjectAccess'\nimport { formatDuration } from '../../../lib/time-tracking/duration'\nimport { formatLongDayLabel, todayIso } from '../../../lib/time-tracking-ui/timesheetPeriod'\nimport { buildLogTargets, parseTargetKey, targetLabel } from '../../../lib/time-tracking-ui/timesheetTargets'\nimport { isEntryLockedError } from '../../../lib/time-tracking-ui/timeEntryListData'\n\nconst logger = createLogger('staff').child({ component: 'time-tracking/my-work' })\n\nconst MY_WORK_ENDPOINT = '/api/staff/timesheets/my-work'\nconst ENTRIES_API_PATH = 'staff/timesheets/time-entries'\nconst MANAGE_OWN_FEATURE = 'staff.timesheets.manage_own'\nconst MUTATION_CONTEXT_ID = 'staff.time_tracking.myWork'\nconst TIMESHEET_HREF = '/backend/staff/time-tracking/timesheet'\nconst ENTRIES_HREF = '/backend/staff/time-tracking/entries'\n\ntype MyWorkEntry = {\n id: string\n date: string\n taskId: string | null\n taskTitle: string | null\n timeProjectId: string | null\n projectName: string | null\n description: string | null\n startedAt: string | null\n endedAt: string | null\n durationMinutes: number\n isBillable: boolean\n isLocked: boolean\n}\n\ntype MyWorkProject = {\n id: string\n name: string\n code: string | null\n color: string | null\n customerName: string | null\n myMinutes: number\n totalMinutes: number\n hourlyRate?: number | null\n currencyCode?: string | null\n budget: {\n kind: 'hours' | 'amount'\n budgetValue: number\n usedValue: number\n percent: number\n barPercent: number\n tone: string\n } | null\n}\n\ntype MyWorkTask = {\n id: string\n title: string\n timeProjectId: string\n projectName: string | null\n}\n\ntype MyWorkPayload = {\n staffMember: { id: string; displayName: string } | null\n today: string\n kpis: {\n todayMinutes: number\n weekMinutes: number\n monthMinutes: number\n monthNonBillableMinutes: number\n dailyTargetMinutes: number | null\n weekTargetMinutes: number | null\n monthTargetMinutes: number | null\n weekWorkingDays: number\n monthWorkingDays: number\n nonBillableSharePercent: number | null\n }\n entries: MyWorkEntry[]\n projects: MyWorkProject[]\n recentTasks: MyWorkTask[]\n}\n\nconst EMPTY_PAYLOAD: MyWorkPayload = {\n staffMember: null,\n today: todayIso(),\n kpis: {\n todayMinutes: 0,\n weekMinutes: 0,\n monthMinutes: 0,\n monthNonBillableMinutes: 0,\n dailyTargetMinutes: null,\n weekTargetMinutes: null,\n monthTargetMinutes: null,\n weekWorkingDays: 0,\n monthWorkingDays: 0,\n nonBillableSharePercent: null,\n },\n entries: [],\n projects: [],\n recentTasks: [],\n}\n\nfunction formatClockRange(entry: MyWorkEntry, locale?: string): string | null {\n if (!entry.startedAt || !entry.endedAt) return null\n const format = (iso: string) => {\n const parsed = new Date(iso)\n if (Number.isNaN(parsed.getTime())) return ''\n try {\n return new Intl.DateTimeFormat(locale, { hour: '2-digit', minute: '2-digit', hour12: false }).format(parsed)\n } catch {\n return ''\n }\n }\n const start = format(entry.startedAt)\n const end = format(entry.endedAt)\n return start && end ? `${start} \u2013 ${end}` : null\n}\n\nexport default function TimeTrackingMyWorkPage() {\n const t = useT()\n const scopeVersion = useOrganizationScopeVersion()\n const { payload: chrome } = useBackendChrome()\n const canManageOwn = hasFeature(chrome?.grantedFeatures, MANAGE_OWN_FEATURE)\n\n const [data, setData] = React.useState<MyWorkPayload>(EMPTY_PAYLOAD)\n const [loadState, setLoadState] = React.useState<'loading' | 'ready' | 'error'>('loading')\n const [dialog, setDialog] = React.useState<{ open: boolean; entryId: string | null }>({\n open: false,\n entryId: null,\n })\n const [quickTargetKey, setQuickTargetKey] = React.useState('')\n const [quickMinutes, setQuickMinutes] = React.useState<number | null>(null)\n const [quickEpoch, setQuickEpoch] = React.useState(0)\n const [quickDetails, setQuickDetails] = React.useState<EntryDetailsValue>(() => emptyEntryDetails(''))\n const [tagOptions, setTagOptions] = React.useState<TagOption[]>([])\n\n // The date defaults to the day the page is showing, once that is known.\n React.useEffect(() => {\n if (!data.today) return\n setQuickDetails((current) => (current.date ? current : { ...current, date: data.today }))\n }, [data.today])\n\n React.useEffect(() => {\n let cancelled = false\n void (async () => {\n try {\n const res = await Promise.resolve(\n apiCall<{ items?: Array<Record<string, unknown>> }>('/api/staff/timesheets/tags?page=1&pageSize=100'),\n )\n if (cancelled || !res?.ok) return\n const items = Array.isArray(res.result?.items) ? res.result.items : []\n setTagOptions(\n items\n .map((row: Record<string, unknown>) => {\n const id = typeof row.id === 'string' ? row.id : null\n const label = typeof row.label === 'string' ? row.label : null\n return id && label\n ? { id, label, color: typeof row.color === 'string' ? row.color : null }\n : null\n })\n .filter((tag): tag is TagOption => tag !== null),\n )\n } catch {\n // A missing tag vocabulary leaves the picker empty rather than the page broken.\n }\n })()\n return () => { cancelled = true }\n }, [])\n\n const createTag = React.useCallback(\n async (label: string): Promise<TagOption | null> => {\n try {\n const created = await createCrud('/api/staff/timesheets/tags', {\n label,\n slug: slugifyProjectName(label).toLowerCase(),\n }, { errorMessage: t('staff.time_tracking.entryDialog.errors.createTag', 'Could not create the tag.') })\n const payload = created as unknown as Record<string, unknown> | null\n const raw = payload?.id ?? (payload?.result as Record<string, unknown> | undefined)?.id\n const id = typeof raw === 'string' ? raw : null\n if (!id) return null\n const tag: TagOption = { id, label, color: null }\n setTagOptions((current) => [...current, tag])\n return tag\n } catch (error) {\n logger.warn('staff.time_tracking.my-work tag create failed', { err: error })\n return null\n }\n },\n [t],\n )\n const [quickSaving, setQuickSaving] = React.useState(false)\n\n const loadError = t('staff.time_tracking.myWork.errors.load', 'Failed to load your work summary.')\n\n // A failed request leaves `data` at EMPTY_PAYLOAD, which reads as screen 2 \u2014 \"no\n // assignments\" \u2014 so the failure has to be a state of its own or the page reports a\n // server fault as an empty account.\n const load = React.useCallback(async () => {\n try {\n const result = await readApiResultOrThrow<MyWorkPayload>(MY_WORK_ENDPOINT, undefined, {\n errorMessage: loadError,\n fallback: EMPTY_PAYLOAD,\n })\n setData({ ...EMPTY_PAYLOAD, ...result })\n setLoadState('ready')\n } catch (error) {\n logger.error('staff.time_tracking.my-work load failed', { err: error })\n setLoadState('error')\n }\n }, [loadError])\n\n const retryLoad = React.useCallback(() => {\n setLoadState('loading')\n void load()\n }, [load])\n\n React.useEffect(() => {\n void load()\n }, [load, scopeVersion])\n\n const { runMutation, retryLastMutation } = useGuardedMutation<{\n formId: string\n resourceKind: string\n retryLastMutation: () => Promise<boolean>\n }>({\n contextId: MUTATION_CONTEXT_ID,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n\n const mutationContext = React.useMemo(\n () => ({\n formId: MUTATION_CONTEXT_ID,\n resourceKind: 'staff.timesheets.time_entry',\n retryLastMutation,\n }),\n [retryLastMutation],\n )\n\n const myWorkInjectionContext = React.useMemo(\n () => ({\n staffMemberId: data.staffMember?.id ?? null,\n today: data.today,\n projectIds: data.projects.map((project) => project.id),\n retryLastMutation,\n }),\n [data.projects, data.staffMember?.id, data.today, retryLastMutation],\n )\n\n const quickTargets = React.useMemo(\n () =>\n buildLogTargets(\n data.projects.map((project) => ({\n id: project.id,\n name: project.name,\n code: project.code,\n color: project.color,\n })),\n data.recentTasks.map((task) => ({\n id: task.id,\n title: task.title,\n timeProjectId: task.timeProjectId,\n })),\n ),\n [data.projects, data.recentTasks],\n )\n\n React.useEffect(() => {\n if (quickTargetKey || quickTargets.length === 0) return\n setQuickTargetKey(quickTargets[0].key)\n }, [quickTargetKey, quickTargets])\n\n const todayTotal = data.entries.reduce((sum, entry) => sum + entry.durationMinutes, 0)\n\n const todayEntriesColumns = React.useMemo<ColumnDef<MyWorkEntry>[]>(\n () => [\n {\n id: 'task',\n header: t('staff.time_tracking.myWork.today.columns.task', 'Task'),\n cell: ({ row }) => {\n const entry = row.original\n return (\n <div>\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"font-medium\">\n {entry.taskTitle ??\n entry.description ??\n t('staff.time_tracking.entries.table.noTask', 'No task')}\n </span>\n {entry.isBillable ? null : (\n <StatusBadge variant=\"neutral\">\n {t('staff.time_tracking.entries.badges.nonBillable', 'non-billable')}\n </StatusBadge>\n )}\n {entry.isLocked ? (\n <StatusBadge variant=\"neutral\">\n <Lock className=\"size-3\" aria-hidden=\"true\" />\n {t('staff.time_tracking.entries.badges.locked', 'locked')}\n </StatusBadge>\n ) : null}\n </div>\n <div className=\"text-xs text-muted-foreground\">\n {[entry.projectName, entry.taskTitle ? entry.description : null]\n .filter((part): part is string => Boolean(part))\n .join(' \u00B7 ')}\n </div>\n </div>\n )\n },\n },\n {\n id: 'clock',\n header: t('staff.time_tracking.myWork.today.columns.time', 'Time'),\n meta: { maxWidth: '8rem' },\n cell: ({ row }) => (\n <span className=\"text-xs text-muted-foreground\">{formatClockRange(row.original) ?? '\u2014'}</span>\n ),\n },\n {\n id: 'duration',\n header: () => (\n <span className=\"block text-right\">\n {t('staff.time_tracking.myWork.today.columns.duration', 'Duration')}\n </span>\n ),\n meta: { maxWidth: '6rem' },\n cell: ({ row }) => (\n <span className=\"block text-right font-mono font-semibold tabular-nums\">\n {formatDuration(row.original.durationMinutes, 'clock')}\n </span>\n ),\n },\n {\n id: 'actions',\n header: '',\n meta: { maxWidth: '3rem' },\n cell: ({ row }) => {\n const entry = row.original\n return canManageOwn && !entry.isLocked ? (\n <div className=\"flex justify-end\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"2xs\"\n aria-label={t('staff.time_tracking.timesheet.list.edit', 'Edit')}\n onClick={() => setDialog({ open: true, entryId: entry.id })}\n >\n <Pencil className=\"size-3.5\" aria-hidden=\"true\" />\n </Button>\n </div>\n ) : null\n },\n },\n ],\n [canManageOwn, t],\n )\n\n const submitQuickEntry = React.useCallback(async () => {\n if (!quickTargetKey || quickMinutes === null || quickMinutes <= 0 || quickSaving) return\n const target = parseTargetKey(quickTargetKey)\n const body = {\n staffMemberId: data.staffMember?.id,\n date: quickDetails.date || data.today,\n timeProjectId: target.timeProjectId,\n taskId: target.taskId,\n durationMinutes: quickMinutes,\n description: quickDetails.description.trim() || null,\n isBillable: quickDetails.isBillable,\n tagIds: quickDetails.tagIds.length > 0 ? quickDetails.tagIds : undefined,\n // Paired or omitted \u2014 a lone start is how the list identifies a running\n // timer, and a manual entry has no segment to stop.\n ...buildEntryClocks({\n date: quickDetails.date || data.today,\n startClock: quickDetails.startClock || null,\n endClock: quickDetails.endClock || null,\n minutes: quickMinutes,\n }),\n source: 'manual',\n }\n setQuickSaving(true)\n try {\n await runMutation({\n operation: () =>\n createCrud(ENTRIES_API_PATH, body, {\n errorMessage: t('staff.timesheets.my.errors.save', 'Failed to save timesheets.'),\n }),\n context: mutationContext,\n mutationPayload: body as unknown as Record<string, unknown>,\n })\n setQuickMinutes(null)\n setQuickEpoch((epoch) => epoch + 1)\n // The date and the billable flag survive; the rest clears. Logging three\n // lines against yesterday should not mean re-picking yesterday each time.\n setQuickDetails((current) => ({\n ...emptyEntryDetails(current.date, current.isBillable),\n }))\n await load()\n } catch (error) {\n logger.error('staff.time_tracking.my-work quick entry failed', { err: error })\n flash(t('staff.timesheets.my.errors.save', 'Failed to save timesheets.'), 'error')\n } finally {\n setQuickSaving(false)\n }\n }, [data.staffMember?.id, data.today, load, mutationContext, quickDetails, quickMinutes, quickSaving, quickTargetKey, runMutation, t])\n\n const copyYesterday = React.useCallback(async () => {\n const today = new Date(`${data.today}T00:00:00.000Z`)\n const yesterday = new Date(today)\n yesterday.setUTCDate(yesterday.getUTCDate() - 1)\n const body = {\n fromDate: yesterday.toISOString().slice(0, 10),\n toDate: data.today,\n }\n try {\n const result = await runMutation({\n operation: () =>\n createCrud<{ copied?: number; skipped?: unknown[] }>(`${ENTRIES_API_PATH}/copy-day`, body, {\n errorMessage: t('staff.time_tracking.entries.errors.copyDay', 'Could not copy the day.'),\n }),\n context: mutationContext,\n mutationPayload: body,\n })\n const copied = (result?.result as { copied?: number } | undefined)?.copied ?? 0\n flash(\n t('staff.time_tracking.myWork.copyDay.done', 'Copied {count} entries from yesterday.', {\n count: String(copied),\n }),\n 'success',\n )\n await load()\n } catch (error) {\n if (isEntryLockedError(error)) {\n flash(\n t(\n 'staff.time_tracking.entries.errors.entryLocked',\n 'This time entry is locked in a closed report and cannot be changed. Unlock the report first.',\n ),\n 'error',\n )\n return\n }\n logger.error('staff.time_tracking.my-work copy day failed', { err: error })\n flash(t('staff.time_tracking.entries.errors.copyDay', 'Could not copy the day.'), 'error')\n }\n }, [data.today, load, mutationContext, runMutation, t])\n\n if (loadState === 'loading') {\n return (\n <Page>\n <PageHeader title={t('staff.time_tracking.nav.my_work', 'My work')} />\n <PageBody>\n <LoadingMessage label={t('staff.timesheets.my.loading', 'Loading timesheets...')} />\n </PageBody>\n </Page>\n )\n }\n\n if (loadState === 'error') {\n return (\n <Page>\n <PageHeader title={t('staff.time_tracking.nav.my_work', 'My work')} />\n <PageBody>\n <ErrorMessage\n label={loadError}\n action={\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={retryLoad}>\n {t('staff.time_tracking.myWork.errors.retry', 'Try again')}\n </Button>\n }\n />\n </PageBody>\n </Page>\n )\n }\n\n const subtitle = [formatLongDayLabel(data.today), data.staffMember?.displayName]\n .filter((part): part is string => Boolean(part))\n .join(' \u00B7 ')\n\n const noAssignments = data.projects.length === 0 && data.entries.length === 0\n\n return (\n <Page>\n <PageHeader\n title={t('staff.time_tracking.nav.my_work', 'My work')}\n description={subtitle}\n actions={\n <div className=\"flex items-center gap-2\">\n <Button variant=\"outline\" asChild>\n <Link href={TIMESHEET_HREF}>\n <CalendarDays className=\"size-4\" aria-hidden=\"true\" />\n {t('staff.time_tracking.nav.timesheet', 'Timesheet')}\n </Link>\n </Button>\n {canManageOwn && !noAssignments ? (\n <Button type=\"button\" onClick={() => setDialog({ open: true, entryId: null })}>\n <Plus className=\"size-4\" aria-hidden=\"true\" />\n {t('staff.time_tracking.entries.actions.add', 'Add entry')}\n </Button>\n ) : null}\n </div>\n }\n />\n <PageBody>\n {noAssignments ? (\n <NoTimeProjectAssignments />\n ) : (\n <div className=\"flex flex-col gap-6\">\n <InjectionSpot\n spotId={extensionPoints.hosts.myWorkBeforeSections.spotId}\n context={myWorkInjectionContext}\n data={data}\n />\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4\">\n <KpiCard\n title={t('staff.time_tracking.myWork.kpi.today', 'Today')}\n value={data.kpis.todayMinutes}\n formatValue={(value) => formatDuration(value, 'clock')}\n comparisonLabel={\n data.kpis.dailyTargetMinutes !== null\n ? t('staff.time_tracking.myWork.kpi.targetGap', 'target {target} \u2014 {gap} to go', {\n target: formatDuration(data.kpis.dailyTargetMinutes, 'clock'),\n gap: formatDuration(\n Math.max(0, data.kpis.dailyTargetMinutes - data.kpis.todayMinutes),\n 'clock',\n ),\n })\n : undefined\n }\n />\n <KpiCard\n title={t('staff.time_tracking.myWork.kpi.week', 'This week')}\n value={data.kpis.weekMinutes}\n formatValue={(value) => formatDuration(value, 'clock')}\n comparisonLabel={\n data.kpis.weekTargetMinutes !== null\n ? t('staff.time_tracking.myWork.kpi.ofTarget', 'of {target}', {\n target: formatDuration(data.kpis.weekTargetMinutes, 'clock'),\n })\n : undefined\n }\n />\n <KpiCard\n title={t('staff.time_tracking.myWork.kpi.month', 'This month')}\n value={data.kpis.monthMinutes}\n formatValue={(value) => formatDuration(value, 'clock')}\n comparisonLabel={t('staff.time_tracking.myWork.kpi.workingDays', '{days} working days', {\n days: String(data.kpis.monthWorkingDays),\n })}\n />\n <KpiCard\n title={t('staff.time_tracking.myWork.kpi.nonBillable', 'Non-billable')}\n value={data.kpis.monthNonBillableMinutes}\n formatValue={(value) => formatDuration(value, 'clock')}\n comparisonLabel={\n data.kpis.nonBillableSharePercent !== null\n ? t('staff.time_tracking.myWork.kpi.shareOfMonth', '{percent}% of the month', {\n percent: String(data.kpis.nonBillableSharePercent),\n })\n : undefined\n }\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 xl:grid-cols-3\">\n <section className=\"rounded-lg border xl:col-span-2\">\n <div className=\"flex flex-wrap items-center justify-between gap-2 border-b p-4\">\n <h2 className=\"text-sm font-semibold\">\n {t('staff.time_tracking.myWork.today.title', 'Today\u2019s entries')}\n </h2>\n <div className=\"flex items-center gap-2\">\n {canManageOwn ? (\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => void copyYesterday()}>\n <Copy className=\"size-4\" aria-hidden=\"true\" />\n {t('staff.time_tracking.myWork.today.copyYesterday', 'Copy yesterday')}\n </Button>\n ) : null}\n <Button variant=\"outline\" size=\"sm\" asChild>\n <Link href={ENTRIES_HREF}>\n {t('staff.time_tracking.myWork.today.allEntries', 'All entries')}\n </Link>\n </Button>\n </div>\n </div>\n\n {data.entries.length === 0 ? (\n <p className=\"p-4 text-sm text-muted-foreground\">\n {t('staff.time_tracking.myWork.today.empty', 'Nothing logged today yet.')}\n </p>\n ) : (\n <>\n <DataTable<MyWorkEntry>\n embedded\n extensionTableId={extensionPoints.hosts.myWorkTodayEntriesTable.tableId}\n disableRowClick\n columns={todayEntriesColumns}\n data={data.entries}\n />\n {canManageOwn && quickTargets.length > 0 ? (\n <div\n className=\"flex flex-wrap items-start gap-2 border-t p-3\"\n onKeyDown={(event) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n void submitQuickEntry()\n }\n }}\n >\n <Select value={quickTargetKey} onValueChange={setQuickTargetKey}>\n <SelectTrigger\n className=\"w-72\"\n aria-label={t('staff.time_tracking.myWork.quickEntry.target', 'Project or task')}\n >\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {quickTargets.map((target) => (\n <SelectItem key={target.key} value={target.key}>\n {targetLabel(target)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <div className=\"w-32\">\n <DurationInput\n key={quickEpoch}\n variant=\"compact\"\n value={quickMinutes}\n onChange={(minutes) => setQuickMinutes(minutes)}\n ariaLabel={t('staff.time_tracking.myWork.quickEntry.duration', 'Duration')}\n />\n </div>\n <Button\n type=\"button\"\n size=\"sm\"\n disabled={quickSaving || quickMinutes === null || quickMinutes <= 0}\n onClick={() => void submitQuickEntry()}\n >\n {t('staff.time_tracking.myWork.quickEntry.save', 'Save')}\n </Button>\n <span className=\"self-center text-xs text-muted-foreground\">\n {t('staff.time_tracking.entryDialog.shortcutSave', '\u2318\u21B5 to save')}\n </span>\n {/*\n * The same block the task drawer offers, from the same\n * component: a row that can log an hour but not say what\n * it was for, or when, sends people to the full dialog\n * for the one field they were missing.\n */}\n <div className=\"w-full\">\n <EntryDetailsFields\n idPrefix=\"my-work-quick\"\n value={quickDetails}\n onChange={setQuickDetails}\n tagOptions={tagOptions}\n onCreateTag={createTag}\n disabled={quickSaving}\n />\n </div>\n </div>\n ) : null}\n <div className=\"flex items-center justify-between border-t bg-muted/40 p-3 text-sm font-medium\">\n <span>{t('staff.time_tracking.myWork.today.total', 'Total today')}</span>\n <span className=\"font-mono tabular-nums\">{formatDuration(todayTotal, 'clock')}</span>\n </div>\n </>\n )}\n </section>\n\n <div className=\"flex flex-col gap-6\">\n <section className=\"rounded-lg border\">\n <div className=\"flex items-center justify-between border-b p-4\">\n <h2 className=\"text-sm font-semibold\">\n {t('staff.time_tracking.myWork.projects.title', 'My projects')}\n </h2>\n <span className=\"text-xs text-muted-foreground\">{data.projects.length}</span>\n </div>\n <div className=\"flex flex-col gap-4 p-4\">\n {data.projects.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">\n {t('staff.time_tracking.myWork.projects.empty', 'No projects assigned yet.')}\n </p>\n ) : (\n data.projects.map((project) => (\n <div key={project.id} className=\"flex flex-col gap-1\">\n <div className=\"flex items-center justify-between gap-2\">\n <Link\n href={`/backend/staff/time-tracking/projects/${project.id}`}\n className=\"min-w-0 truncate font-medium hover:underline\"\n >\n {project.name}\n </Link>\n <span className=\"shrink-0 font-mono text-xs tabular-nums text-muted-foreground\">\n {formatDuration(project.myMinutes, 'clock')}\n </span>\n </div>\n <p className=\"text-xs text-muted-foreground\">\n {[\n project.customerName,\n project.hourlyRate != null && project.currencyCode\n ? t('staff.time_tracking.myWork.projects.rate', '{rate}/h', {\n rate: formatCurrency(project.hourlyRate, project.currencyCode) ?? '',\n })\n : null,\n ]\n .filter((part): part is string => Boolean(part))\n .join(' \u00B7 ')}\n </p>\n {project.budget ? (\n <>\n <Progress\n value={project.budget.barPercent}\n size=\"sm\"\n tone={\n project.budget.tone === 'destructive'\n ? 'destructive'\n : project.budget.tone === 'warning'\n ? 'warning'\n : 'accent'\n }\n />\n <p className=\"text-xs text-muted-foreground\">\n {t('staff.time_tracking.myWork.projects.budget', '{percent}% of budget ({value})', {\n percent: String(project.budget.percent),\n value:\n project.budget.kind === 'hours'\n ? t('staff.time_tracking.myWork.projects.budgetHours', '{hours} h', {\n hours: String(project.budget.budgetValue),\n })\n : formatCurrency(project.budget.budgetValue, project.currencyCode ?? undefined) ??\n String(project.budget.budgetValue),\n })}\n </p>\n </>\n ) : null}\n </div>\n ))\n )}\n </div>\n </section>\n\n <section className=\"rounded-lg border\">\n <div className=\"border-b p-4\">\n <h2 className=\"text-sm font-semibold\">\n {t('staff.time_tracking.myWork.recentTasks.title', 'Recent tasks')}\n </h2>\n </div>\n <div className=\"flex flex-col gap-2 p-4\">\n {data.recentTasks.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">\n {t('staff.time_tracking.myWork.recentTasks.empty', 'No tasks logged against yet.')}\n </p>\n ) : (\n data.recentTasks.map((task) => (\n <Link\n key={task.id}\n href={`/backend/staff/time-tracking/projects/${task.timeProjectId}/board?task=${task.id}`}\n className=\"flex items-center justify-between gap-2 truncate text-sm hover:underline\"\n >\n <span className=\"truncate\">{task.title}</span>\n <span className=\"shrink-0 text-xs text-muted-foreground\">{task.projectName}</span>\n </Link>\n ))\n )}\n </div>\n </section>\n </div>\n </div>\n <InjectionSpot\n spotId={extensionPoints.hosts.myWorkAfterSections.spotId}\n context={myWorkInjectionContext}\n data={data}\n />\n </div>\n )}\n </PageBody>\n\n <TimeEntryDialog\n open={dialog.open}\n onOpenChange={(open) => setDialog((current) => ({ ...current, open }))}\n entryId={dialog.entryId}\n defaults={{ date: data.today }}\n onSaved={() => {\n void load()\n }}\n />\n </Page>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { Fragment, 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
|
+
import { DataTable } from "@open-mercato/ui/backend/DataTable";
|
|
5
6
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
6
7
|
import { Badge } from "@open-mercato/ui/primitives/badge";
|
|
7
8
|
import { Spinner } from "@open-mercato/ui/primitives/spinner";
|
|
@@ -196,6 +197,34 @@ function TimesheetProjectDetailPage({ params }) {
|
|
|
196
197
|
);
|
|
197
198
|
const activeCount = employees.filter((emp) => emp.status === "active").length;
|
|
198
199
|
const inactiveCount = employees.length - activeCount;
|
|
200
|
+
const recentEntriesColumns = React.useMemo(
|
|
201
|
+
() => [
|
|
202
|
+
{
|
|
203
|
+
accessorKey: "date",
|
|
204
|
+
header: t("staff.timesheets.projects.detail.colDate", "Date"),
|
|
205
|
+
cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "whitespace-nowrap", children: row.original.date })
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
id: "person",
|
|
209
|
+
header: t("staff.timesheets.projects.detail.colWho", "Person"),
|
|
210
|
+
cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "whitespace-nowrap", children: employeeNameById.get(row.original.staffMemberId ?? "") ?? "\u2014" })
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
accessorKey: "description",
|
|
214
|
+
header: t("staff.timesheets.projects.detail.colDescription", "Description"),
|
|
215
|
+
cell: ({ row }) => /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
|
|
216
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: row.original.description ?? "\u2014" }),
|
|
217
|
+
row.original.isBillable ? null : /* @__PURE__ */ jsx(Badge, { variant: "neutral", children: t("staff.timesheets.projects.detail.nonBillableTag", "non-billable") })
|
|
218
|
+
] })
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
id: "hours",
|
|
222
|
+
header: () => /* @__PURE__ */ jsx("span", { className: "block text-right", children: t("staff.timesheets.projects.detail.colHours", "Hours") }),
|
|
223
|
+
cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "block whitespace-nowrap text-right font-mono tabular-nums", children: formatHours(row.original.durationMinutes) })
|
|
224
|
+
}
|
|
225
|
+
],
|
|
226
|
+
[employeeNameById, t]
|
|
227
|
+
);
|
|
199
228
|
React.useEffect(() => {
|
|
200
229
|
if (!projectId) return;
|
|
201
230
|
let cancelled = false;
|
|
@@ -779,23 +808,15 @@ function TimesheetProjectDetailPage({ params }) {
|
|
|
779
808
|
/* @__PURE__ */ jsx(TabsContent, { value: "time", children: recentEntriesLoading ? /* @__PURE__ */ jsx(LoadingMessage, { label: t("staff.timesheets.projects.detail.loadingTime", "Loading time entries\u2026") }) : recentEntries.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-dashed p-8 text-center", children: [
|
|
780
809
|
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: t("staff.timesheets.projects.detail.noTime", "No time logged yet") }),
|
|
781
810
|
/* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: t("staff.timesheets.projects.detail.noTimeHint", "Hours logged against this project appear here.") })
|
|
782
|
-
] }) : /* @__PURE__ */ jsx(
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
/* @__PURE__ */ jsx("td", { className: "whitespace-nowrap px-3 py-2", children: employeeNameById.get(entry.staffMemberId ?? "") ?? "\u2014" }),
|
|
792
|
-
/* @__PURE__ */ jsx("td", { className: "px-3 py-2", children: /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
|
|
793
|
-
/* @__PURE__ */ jsx("span", { className: "truncate", children: entry.description ?? "\u2014" }),
|
|
794
|
-
entry.isBillable ? null : /* @__PURE__ */ jsx(Badge, { variant: "neutral", children: t("staff.timesheets.projects.detail.nonBillableTag", "non-billable") })
|
|
795
|
-
] }) }),
|
|
796
|
-
/* @__PURE__ */ jsx("td", { className: "whitespace-nowrap px-3 py-2 text-right font-mono tabular-nums", children: formatHours(entry.durationMinutes) })
|
|
797
|
-
] }, entry.id)) })
|
|
798
|
-
] }) }) }),
|
|
811
|
+
] }) : /* @__PURE__ */ jsx(
|
|
812
|
+
DataTable,
|
|
813
|
+
{
|
|
814
|
+
extensionTableId: extensionPoints.hosts.projectDetailTimeEntriesTable.tableId,
|
|
815
|
+
disableRowClick: true,
|
|
816
|
+
columns: recentEntriesColumns,
|
|
817
|
+
data: recentEntries
|
|
818
|
+
}
|
|
819
|
+
) }),
|
|
799
820
|
/* @__PURE__ */ jsx(TabsContent, { value: "tasks", children: projectTasksLoading ? /* @__PURE__ */ jsx(LoadingMessage, { label: t("staff.timesheets.projects.detail.loadingTasks", "Loading tasks\u2026") }) : projectTasks.length === 0 ? /* @__PURE__ */ jsx("div", { className: "rounded-lg border border-dashed p-8 text-center", children: /* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: t("staff.timesheets.projects.detail.noTasks", "No tasks yet") }) }) : /* @__PURE__ */ jsx("div", { className: "divide-y rounded-lg border", children: projectTasks.map((task) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-3 py-2 text-sm", children: [
|
|
800
821
|
task.reference ? /* @__PURE__ */ jsx("span", { className: "shrink-0 rounded-sm border border-border bg-muted px-1.5 py-0.5 font-mono text-xs", children: task.reference }) : null,
|
|
801
822
|
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: task.title }),
|