@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../../src/modules/staff/backend/staff/time-tracking/reports/%5Bid%5D/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\n/**\n * Screens 14 and 15 \u2014 the report preview, its exports, and the lock/unlock pair.\n *\n * One page, two states, because they are the same document:\n *\n * - **Draft (screen 14).** The sheet is computed live, the header carries the\n * three export buttons and one primary action, \"Close & lock\". Exporting is\n * deliberately NOT the thing that locks (note 5) \u2014 a PDF download that froze\n * a team's timesheet would be a trap.\n * - **Closed (screen 15).** The sheet is what was frozen, an alert states that\n * the entries are read-only and that logging NEW time on the same tasks still\n * works (note 2), the frozen entries are listed, the history timeline shows\n * who closed, unlocked or exported and why, and the unlock button is\n * `destructive-outline` rather than full destructive (note 3): risky, but\n * reversible, since the report can simply be closed again.\n *\n * Unlocking demands a reason (note 1). The dialog shows the export history first\n * so \"this was already sent to the client on the 20th\" is on screen before\n * anybody restates a billed hour.\n */\n\nimport * as React from 'react'\nimport { useSearchParams } from 'next/navigation'\nimport { Download, Lock, LockOpen } from 'lucide-react'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { Alert, AlertDescription, AlertTitle } from '@open-mercato/ui/primitives/alert'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { Textarea } from '@open-mercato/ui/primitives/textarea'\nimport {\n Dialog,\n DialogContent,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '@open-mercato/ui/primitives/dialog'\nimport { FormHeader } from '@open-mercato/ui/backend/forms'\nimport { ErrorMessage, LoadingMessage } from '@open-mercato/ui/backend/detail'\nimport { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { useBackendChrome } from '@open-mercato/ui/backend/BackendChromeProvider'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { formatCurrency } from '@open-mercato/ui/utils/format'\nimport { hasFeature } from '@open-mercato/shared/security/features'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { ReportSheet } from '../../../../../lib/time-tracking-ui/ReportSheet'\nimport { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'\nimport { extensionPoints } from '@open-mercato/core/modules/staff/extension-points'\nimport {\n parseReportSheet,\n type ReportHistoryEvent,\n type ReportSheetPayload,\n} from '../../../../../lib/timesheets-reports/reportSheetData'\nimport { formatReportMinutes } from '../../../../../lib/timesheets-reports/reportTotals'\n\nconst logger = createLogger('staff').child({ component: 'time-tracking/reports/detail' })\n\nconst REPORTS_HREF = '/backend/staff/time-tracking/reports'\nconst REPORTS_API = '/api/staff/timesheets/reports'\nconst LOCK_FEATURE = 'staff.timesheets.lock'\nconst UNLOCK_FEATURE = 'staff.timesheets.reports.unlock'\nconst RATES_FEATURE = 'staff.timesheets.rates.view'\nconst MUTATION_CONTEXT_ID = 'staff.time_tracking.reportDetail'\nconst RESOURCE_KIND = 'staff.timesheets.time_report'\nconst EXPORT_FORMATS = ['pdf', 'csv', 'xlsx'] as const\n\ntype ExportFormat = (typeof EXPORT_FORMATS)[number]\n\ntype MutationContext = {\n formId: string\n resourceKind: string\n resourceId: string\n retryLastMutation: () => Promise<boolean>\n}\n\nconst REPORT_ENTITY_ID = 'staff:staff_time_report'\n\ntype ReportDetailInjectionContext = {\n entityId: string\n recordId: string\n reportId: string\n reference: string\n isClosed: boolean\n periodFrom: string | null\n periodTo: string | null\n retryLastMutation: () => Promise<boolean>\n}\n\nfunction readErrorMessage(payload: unknown, fallback: string): string {\n if (payload && typeof payload === 'object') {\n const error = (payload as { error?: unknown }).error\n if (typeof error === 'string' && error.trim().length > 0) return error\n }\n return fallback\n}\n\n/**\n * `params` arrives as a prop, not from `useParams()`.\n *\n * Backend pages render under the `/backend/[[...slug]]` catch-all, so\n * `useParams()` answers `{ slug: [...] }` \u2014 there is no `id` key in it, and\n * reading one yields `undefined`. The effect then short-circuits on the empty id\n * and the page sits on its loading state forever, having issued no request at\n * all. The catch-all passes the resolved segment as a prop; that is the contract\n * every other detail page in the module already uses.\n */\nexport default function TimeTrackingReportDetailPage({ params }: { params?: { id?: string } }) {\n const t = useT()\n const searchParams = useSearchParams()\n const reportId = typeof params?.id === 'string' ? params.id : ''\n const { payload: chrome } = useBackendChrome()\n const canClose = hasFeature(chrome?.grantedFeatures, LOCK_FEATURE)\n const canUnlock = hasFeature(chrome?.grantedFeatures, UNLOCK_FEATURE)\n const canSeeMoney = hasFeature(chrome?.grantedFeatures, RATES_FEATURE)\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n\n const [sheet, setSheet] = React.useState<ReportSheetPayload | null>(null)\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [unlockOpen, setUnlockOpen] = React.useState(false)\n const [unlockReason, setUnlockReason] = React.useState('')\n const [busy, setBusy] = React.useState(false)\n const [reloadToken, setReloadToken] = React.useState(0)\n\n const isClosed = sheet?.report.status === 'closed'\n // Screen 15 is `?unlock=1`, and it may only mean what the unlock button means:\n // the same closed-report state and the same feature. A draft report or a\n // caller without the feature gets the page, never a dialog it cannot use.\n const canUnlockNow = isClosed && canUnlock\n const unlockRequested = searchParams.get('unlock') === '1'\n\n const labels = React.useMemo(\n () => ({\n loadError: t('staff.time_tracking.reports.errors.loadFailed', 'Could not load this report.'),\n closeError: t('staff.time_tracking.reports.errors.closeFailed', 'Could not close this report.'),\n unlockError: t('staff.time_tracking.reports.errors.unlockFailed', 'Could not unlock this report.'),\n exportError: t('staff.time_tracking.reports.errors.exportFailed', 'Could not export this report.'),\n }),\n [t],\n )\n\n const { runMutation, retryLastMutation } = useGuardedMutation<MutationContext>({\n contextId: MUTATION_CONTEXT_ID,\n blockedMessage: labels.closeError,\n })\n\n const mutationContext = React.useCallback(\n (): MutationContext => ({\n formId: MUTATION_CONTEXT_ID,\n resourceKind: RESOURCE_KIND,\n resourceId: reportId,\n retryLastMutation,\n }),\n [reportId, retryLastMutation],\n )\n\n React.useEffect(() => {\n if (!canUnlockNow || !unlockRequested) return\n setUnlockOpen(true)\n }, [canUnlockNow, unlockRequested])\n\n React.useEffect(() => {\n if (!reportId) return\n let cancelled = false\n setLoading(true)\n setError(null)\n void readApiResultOrThrow<Record<string, unknown>>(\n `${REPORTS_API}/${reportId}/sheet`,\n undefined,\n { allowNullResult: true, errorMessage: labels.loadError },\n )\n .then((result) => {\n if (cancelled) return\n const parsed = parseReportSheet(result)\n if (!parsed) {\n setError(labels.loadError)\n setSheet(null)\n return\n }\n setSheet(parsed)\n })\n .catch((err) => {\n if (cancelled) return\n logger.error('staff.time_tracking.reports sheet load failed', { err })\n setError(err instanceof Error ? err.message : labels.loadError)\n })\n .finally(() => {\n if (!cancelled) setLoading(false)\n })\n return () => {\n cancelled = true\n }\n }, [labels.loadError, reloadToken, reportId])\n\n const money = React.useCallback(\n (amount: number | null | undefined) => {\n if (amount === null || amount === undefined) return '\u2014'\n return formatCurrency(amount, sheet?.report.currencyCode ?? undefined) ?? '\u2014'\n },\n [sheet?.report.currencyCode],\n )\n\n const roundingLabel = React.useMemo(() => {\n const unit = sheet?.report.roundingUnitMinutes ?? 0\n if (!unit) return t('staff.time_tracking.reports.rounding.off', 'no rounding')\n const direction =\n sheet?.report.roundingDirection === 'nearest'\n ? t('staff.time_tracking.reports.rounding.nearest', 'to the nearest')\n : t('staff.time_tracking.reports.rounding.up', 'always up')\n return t('staff.time_tracking.reports.rounding.rule', 'rounding {minutes} min, {direction}')\n .replace('{minutes}', String(unit))\n .replace('{direction}', direction)\n }, [sheet?.report.roundingDirection, sheet?.report.roundingUnitMinutes, t])\n\n const handleExport = React.useCallback(\n async (format: ExportFormat) => {\n if (!reportId) return\n try {\n const call = await apiCall<Blob>(\n `${REPORTS_API}/${reportId}/export?format=${format}`,\n undefined,\n { parse: (response) => response.blob() },\n )\n if (!call.ok || !call.result) {\n flash(labels.exportError, 'error')\n return\n }\n if (typeof window === 'undefined') return\n const href = URL.createObjectURL(call.result)\n const anchor = document.createElement('a')\n anchor.href = href\n anchor.download = `${sheet?.report.reference || 'report'}.${format}`\n document.body.appendChild(anchor)\n anchor.click()\n document.body.removeChild(anchor)\n URL.revokeObjectURL(href)\n // The export appended an `exported` event; refresh so the history shows\n // it before anybody reads the unlock dialog's \"already sent\" warning.\n setReloadToken((current) => current + 1)\n } catch (err) {\n logger.error('staff.time_tracking.reports export failed', { err })\n flash(labels.exportError, 'error')\n }\n },\n [labels.exportError, reportId, sheet?.report.reference],\n )\n\n const handleClose = React.useCallback(async () => {\n if (!reportId || !sheet) return\n const confirmed = await confirm({\n title: t('staff.time_tracking.reports.close.confirmTitle', 'Close and lock this report?'),\n description: t(\n 'staff.time_tracking.reports.close.confirmBody',\n 'The {count} entries it covers become read-only at their current values. New time can still be logged on the same tasks and will go into the next report.',\n ).replace('{count}', String(sheet.totals.entryCount)),\n confirmText: t('staff.time_tracking.reports.close.action', 'Close & lock'),\n })\n if (!confirmed) return\n\n setBusy(true)\n try {\n const call = await runMutation({\n operation: () => apiCall<Record<string, unknown>>(`${REPORTS_API}/${reportId}/close`, { method: 'POST' }),\n context: mutationContext(),\n mutationPayload: { id: reportId },\n })\n if (!call.ok) {\n flash(readErrorMessage(call.result, labels.closeError), 'error')\n return\n }\n flash(t('staff.time_tracking.reports.close.done', 'Report closed. Its entries are locked.'), 'success')\n setReloadToken((current) => current + 1)\n } catch (err) {\n logger.error('staff.time_tracking.reports close failed', { err })\n flash(err instanceof Error ? err.message : labels.closeError, 'error')\n } finally {\n setBusy(false)\n }\n }, [confirm, labels.closeError, mutationContext, reportId, runMutation, sheet, t])\n\n const handleUnlock = React.useCallback(async () => {\n if (!reportId || unlockReason.trim().length === 0) return\n setBusy(true)\n try {\n const call = await runMutation({\n operation: () =>\n apiCall<Record<string, unknown>>(`${REPORTS_API}/${reportId}/unlock`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ reason: unlockReason.trim() }),\n }),\n context: mutationContext(),\n mutationPayload: { id: reportId, reason: unlockReason.trim() },\n })\n if (!call.ok) {\n flash(readErrorMessage(call.result, labels.unlockError), 'error')\n return\n }\n flash(\n t('staff.time_tracking.reports.unlock.done', 'Report unlocked. Its entries can be edited again.'),\n 'success',\n )\n setUnlockOpen(false)\n setUnlockReason('')\n setReloadToken((current) => current + 1)\n } catch (err) {\n logger.error('staff.time_tracking.reports unlock failed', { err })\n flash(err instanceof Error ? err.message : labels.unlockError, 'error')\n } finally {\n setBusy(false)\n }\n }, [labels.unlockError, mutationContext, reportId, runMutation, t, unlockReason])\n\n if (loading) {\n return (\n <Page>\n <PageBody>\n <LoadingMessage label={t('staff.time_tracking.reports.detail.loading', 'Loading report\u2026')} />\n </PageBody>\n </Page>\n )\n }\n\n if (error || !sheet) {\n return (\n <Page>\n <PageBody>\n <ErrorMessage label={error ?? labels.loadError} />\n </PageBody>\n </Page>\n )\n }\n\n const lastExport = sheet.events.find((event) => event.eventType === 'exported') ?? null\n\n const reportInjectionContext: ReportDetailInjectionContext = {\n entityId: REPORT_ENTITY_ID,\n recordId: reportId,\n reportId,\n reference: sheet.report.reference,\n isClosed,\n periodFrom: sheet.report.periodFrom ?? null,\n periodTo: sheet.report.periodTo ?? null,\n retryLastMutation,\n }\n\n return (\n <Page>\n <PageBody>\n <FormHeader\n mode=\"detail\"\n backHref={REPORTS_HREF}\n title={sheet.report.title}\n subtitle={`${sheet.report.periodFrom ?? ''} \u2013 ${sheet.report.periodTo ?? ''}`}\n statusBadge={\n <>\n {isClosed ? (\n <Badge variant=\"success\">\n <Lock aria-hidden=\"true\" className=\"size-3\" />\n {t('staff.time_tracking.reports.status.closed', 'Closed')}\n </Badge>\n ) : (\n <Badge variant=\"warning\">{t('staff.time_tracking.reports.status.draft', 'Draft')}</Badge>\n )}\n <InjectionSpot\n spotId={extensionPoints.hosts.reportDetailStatusBadges.spotId}\n context={reportInjectionContext}\n data={sheet.report}\n />\n </>\n }\n actionsContent={\n <div className=\"flex flex-wrap items-center gap-2\">\n {EXPORT_FORMATS.map((format) => (\n <Button\n key={format}\n variant=\"outline\"\n onClick={() => void handleExport(format)}\n disabled={busy}\n >\n <Download aria-hidden=\"true\" />\n {format.toUpperCase()}\n </Button>\n ))}\n {isClosed ? (\n canUnlockNow ? (\n <Button\n variant=\"destructive-outline\"\n onClick={() => setUnlockOpen(true)}\n disabled={busy}\n >\n <LockOpen aria-hidden=\"true\" />\n {t('staff.time_tracking.reports.unlock.action', 'Unlock entries')}\n </Button>\n ) : null\n ) : canClose ? (\n <Button onClick={() => void handleClose()} disabled={busy}>\n <Lock aria-hidden=\"true\" />\n {t('staff.time_tracking.reports.close.action', 'Close & lock')}\n </Button>\n ) : null}\n <InjectionSpot\n spotId={extensionPoints.hosts.reportDetailHeader.spotId}\n context={reportInjectionContext}\n data={sheet.report}\n />\n </div>\n }\n />\n\n {isClosed ? (\n <Alert status=\"success\" icon={<Lock aria-hidden=\"true\" />} className=\"mb-4\">\n <AlertTitle>\n {t('staff.time_tracking.reports.locked.title', 'The entries in this report are read-only')}\n </AlertTitle>\n <AlertDescription>\n {t(\n 'staff.time_tracking.reports.locked.body',\n 'Their time, rate and billable flag cannot change without unlocking. New time can still be logged on the same tasks \u2014 it goes into the next report.',\n )}\n </AlertDescription>\n </Alert>\n ) : null}\n\n {sheet.alreadyReportedCount > 0 && !sheet.report.includeAlreadyReported ? (\n <Alert status=\"warning\" className=\"mb-4\">\n <AlertDescription>\n {t(\n 'staff.time_tracking.reports.detail.excludedHint',\n '{count} entries in this period are already frozen in another report and are excluded here ({hours}).',\n )\n .replace('{count}', String(sheet.alreadyReportedCount))\n .replace('{hours}', formatReportMinutes(sheet.alreadyReportedMinutes))}\n </AlertDescription>\n </Alert>\n ) : null}\n\n <ReportSheet\n reference={sheet.report.reference}\n customerName={sheet.report.customerName ?? sheet.report.title}\n periodLabel={`${sheet.report.periodFrom ?? ''} \u2013 ${sheet.report.periodTo ?? ''}`}\n issuedByLabel={null}\n issuedAtLabel={sheet.report.closedAt ? sheet.report.closedAt.slice(0, 10) : null}\n currencyCode={sheet.report.currencyCode}\n showRates={sheet.report.showRates && canSeeMoney}\n groups={sheet.groups}\n totals={{\n billableMinutes: sheet.totals.billableMinutes,\n nonbillableMinutes: sheet.totals.nonbillableMinutes,\n totalAmount: sheet.totals.totalAmount,\n }}\n roundingLabel={roundingLabel}\n reportId={reportId}\n />\n\n {isClosed ? (\n <section className=\"mt-6 rounded-lg border border-border\">\n <header className=\"flex flex-wrap items-center justify-between gap-2 border-b border-border p-4\">\n <h3 className=\"text-sm font-semibold\">\n {t('staff.time_tracking.reports.locked.tableTitle', 'Locked entries')}\n </h3>\n <span className=\"font-mono text-xs text-muted-foreground\">\n {t('staff.time_tracking.reports.locked.tableCount', '{count} entries \u00B7 {hours}')\n .replace('{count}', String(sheet.rowCount))\n .replace('{hours}', formatReportMinutes(sheet.totals.billableMinutes))}\n </span>\n </header>\n <div className=\"overflow-x-auto\">\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"border-b border-border text-xs uppercase tracking-wide text-muted-foreground\">\n <th className=\"px-4 py-2 text-left font-medium\">\n {t('staff.time_tracking.reports.export.date', 'Date')}\n </th>\n <th className=\"px-4 py-2 text-left font-medium\">\n {t('staff.time_tracking.reports.export.task', 'Task')}\n </th>\n <th className=\"px-4 py-2 text-left font-medium\">\n {t('staff.time_tracking.reports.export.person', 'Person')}\n </th>\n <th className=\"px-4 py-2 text-right font-medium\">\n {t('staff.time_tracking.reports.sheet.time', 'Time')}\n </th>\n <th className=\"px-4 py-2 text-right font-medium\">\n {t('staff.time_tracking.reports.sheet.amount', 'Amount')}\n </th>\n <th className=\"w-10 px-4 py-2\" />\n </tr>\n </thead>\n <tbody>\n {sheet.rows.map((row) => (\n <tr key={row.entryId} className=\"border-b border-border/60 last:border-b-0\">\n <td className=\"px-4 py-2 text-muted-foreground\">{row.date}</td>\n <td className=\"px-4 py-2\">{row.taskLabel}</td>\n <td className=\"px-4 py-2\">{row.personLabel}</td>\n <td className=\"px-4 py-2 text-right font-mono tabular-nums\">{row.hours}</td>\n <td className=\"px-4 py-2 text-right font-mono tabular-nums\">{money(row.amount)}</td>\n <td className=\"px-4 py-2 text-muted-foreground\">\n <Lock aria-hidden=\"true\" className=\"size-4\" />\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n {sheet.rowsTruncated ? (\n <p className=\"border-t border-border p-3 text-xs text-muted-foreground\">\n {t(\n 'staff.time_tracking.reports.locked.truncated',\n 'Showing the first {shown} of {total} entries \u2014 export the report for the full list.',\n )\n .replace('{shown}', String(sheet.rows.length))\n .replace('{total}', String(sheet.rowCount))}\n </p>\n ) : null}\n </section>\n ) : null}\n\n {sheet.events.length > 0 ? (\n <section className=\"mt-6\">\n <h3 className=\"mb-3 text-xs font-medium uppercase tracking-wide text-muted-foreground\">\n {t('staff.time_tracking.reports.history.title', 'History')}\n </h3>\n <ReportHistory events={sheet.events} />\n </section>\n ) : null}\n\n <Dialog open={unlockOpen} onOpenChange={(open) => setUnlockOpen(open)}>\n <DialogContent\n onKeyDown={(event) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n void handleUnlock()\n }\n }}\n >\n <DialogHeader>\n <DialogTitle>\n {t('staff.time_tracking.reports.unlock.dialogTitle', 'Unlock {count} entries?').replace(\n '{count}',\n String(sheet.rowCount),\n )}\n </DialogTitle>\n </DialogHeader>\n {lastExport ? (\n <Alert status=\"warning\">\n <AlertTitle>\n {t('staff.time_tracking.reports.unlock.sentTitle', 'This report has already been exported')}\n </AlertTitle>\n <AlertDescription>\n {t(\n 'staff.time_tracking.reports.unlock.sentBody',\n 'A file was downloaded on {date}. Changes to unlocked entries will diverge from the copy the client holds.',\n ).replace('{date}', lastExport.createdAt?.slice(0, 10) ?? '')}\n </AlertDescription>\n </Alert>\n ) : null}\n <div className=\"flex flex-col gap-1.5\">\n <Label htmlFor=\"unlock-reason\">\n {t('staff.time_tracking.reports.unlock.reason', 'Reason for unlocking')}\n </Label>\n <Textarea\n id=\"unlock-reason\"\n value={unlockReason}\n onChange={(event) => setUnlockReason(event.target.value)}\n placeholder={t(\n 'staff.time_tracking.reports.unlock.reasonPlaceholder',\n 'e.g. the client disputed 4h on the refactor \u2014 correction agreed on the 21st.',\n )}\n />\n <span className=\"text-xs text-muted-foreground\">\n {t(\n 'staff.time_tracking.reports.unlock.reasonHint',\n 'The reason is recorded in the report history with your name and the date.',\n )}\n </span>\n </div>\n <DialogFooter>\n <Button variant=\"ghost\" onClick={() => setUnlockOpen(false)}>\n {t('staff.time_tracking.reports.unlock.cancel', 'Cancel')}\n </Button>\n <Button\n variant=\"destructive\"\n onClick={() => void handleUnlock()}\n disabled={busy || unlockReason.trim().length === 0}\n >\n <LockOpen aria-hidden=\"true\" />\n {t('staff.time_tracking.reports.unlock.action', 'Unlock entries')}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n\n <InjectionSpot\n spotId={extensionPoints.hosts.reportDetailFooter.spotId}\n context={reportInjectionContext}\n data={sheet.report}\n />\n\n {ConfirmDialogElement}\n </PageBody>\n </Page>\n )\n}\n\nfunction ReportHistory({ events }: { events: readonly ReportHistoryEvent[] }) {\n const t = useT()\n return (\n <ol className=\"flex flex-col gap-3\">\n {events.map((event) => (\n <li key={event.id} className=\"rounded-md border border-border p-3\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"text-sm font-medium\">\n {event.eventType === 'closed'\n ? t('staff.time_tracking.reports.history.closed', 'Closed the report and locked its entries.')\n : event.eventType === 'unlocked'\n ? t('staff.time_tracking.reports.history.unlocked', 'Unlocked the report entries.')\n : t('staff.time_tracking.reports.history.exported', 'Exported the report.')}\n </span>\n <span className=\"text-xs text-muted-foreground\">{event.createdAt?.replace('T', ' ').slice(0, 16)}</span>\n </div>\n {event.reason ? <p className=\"mt-1 text-sm text-muted-foreground\">{event.reason}</p> : null}\n </li>\n ))}\n </ol>\n )\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\n/**\n * Screens 14 and 15 \u2014 the report preview, its exports, and the lock/unlock pair.\n *\n * One page, two states, because they are the same document:\n *\n * - **Draft (screen 14).** The sheet is computed live, the header carries the\n * three export buttons and one primary action, \"Close & lock\". Exporting is\n * deliberately NOT the thing that locks (note 5) \u2014 a PDF download that froze\n * a team's timesheet would be a trap.\n * - **Closed (screen 15).** The sheet is what was frozen, an alert states that\n * the entries are read-only and that logging NEW time on the same tasks still\n * works (note 2), the frozen entries are listed, the history timeline shows\n * who closed, unlocked or exported and why, and the unlock button is\n * `destructive-outline` rather than full destructive (note 3): risky, but\n * reversible, since the report can simply be closed again.\n *\n * Unlocking demands a reason (note 1). The dialog shows the export history first\n * so \"this was already sent to the client on the 20th\" is on screen before\n * anybody restates a billed hour.\n */\n\nimport * as React from 'react'\nimport { useSearchParams } from 'next/navigation'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { Download, Lock, LockOpen } from 'lucide-react'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { Alert, AlertDescription, AlertTitle } from '@open-mercato/ui/primitives/alert'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { Textarea } from '@open-mercato/ui/primitives/textarea'\nimport {\n Dialog,\n DialogContent,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '@open-mercato/ui/primitives/dialog'\nimport { FormHeader } from '@open-mercato/ui/backend/forms'\nimport { ErrorMessage, LoadingMessage } from '@open-mercato/ui/backend/detail'\nimport { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { useBackendChrome } from '@open-mercato/ui/backend/BackendChromeProvider'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { formatCurrency } from '@open-mercato/ui/utils/format'\nimport { hasFeature } from '@open-mercato/shared/security/features'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { ReportSheet } from '../../../../../lib/time-tracking-ui/ReportSheet'\nimport { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'\nimport { extensionPoints } from '@open-mercato/core/modules/staff/extension-points'\nimport {\n parseReportSheet,\n type ReportHistoryEvent,\n type ReportSheetPayload,\n type ReportSheetRow,\n} from '../../../../../lib/timesheets-reports/reportSheetData'\nimport { formatReportMinutes } from '../../../../../lib/timesheets-reports/reportTotals'\n\nconst logger = createLogger('staff').child({ component: 'time-tracking/reports/detail' })\n\nconst REPORTS_HREF = '/backend/staff/time-tracking/reports'\nconst REPORTS_API = '/api/staff/timesheets/reports'\nconst LOCK_FEATURE = 'staff.timesheets.lock'\nconst UNLOCK_FEATURE = 'staff.timesheets.reports.unlock'\nconst RATES_FEATURE = 'staff.timesheets.rates.view'\nconst MUTATION_CONTEXT_ID = 'staff.time_tracking.reportDetail'\nconst RESOURCE_KIND = 'staff.timesheets.time_report'\nconst EXPORT_FORMATS = ['pdf', 'csv', 'xlsx'] as const\n\ntype ExportFormat = (typeof EXPORT_FORMATS)[number]\n\ntype MutationContext = {\n formId: string\n resourceKind: string\n resourceId: string\n retryLastMutation: () => Promise<boolean>\n}\n\nconst REPORT_ENTITY_ID = 'staff:staff_time_report'\n\ntype ReportDetailInjectionContext = {\n entityId: string\n recordId: string\n reportId: string\n reference: string\n isClosed: boolean\n periodFrom: string | null\n periodTo: string | null\n retryLastMutation: () => Promise<boolean>\n}\n\nfunction readErrorMessage(payload: unknown, fallback: string): string {\n if (payload && typeof payload === 'object') {\n const error = (payload as { error?: unknown }).error\n if (typeof error === 'string' && error.trim().length > 0) return error\n }\n return fallback\n}\n\n/**\n * `params` arrives as a prop, not from `useParams()`.\n *\n * Backend pages render under the `/backend/[[...slug]]` catch-all, so\n * `useParams()` answers `{ slug: [...] }` \u2014 there is no `id` key in it, and\n * reading one yields `undefined`. The effect then short-circuits on the empty id\n * and the page sits on its loading state forever, having issued no request at\n * all. The catch-all passes the resolved segment as a prop; that is the contract\n * every other detail page in the module already uses.\n */\nexport default function TimeTrackingReportDetailPage({ params }: { params?: { id?: string } }) {\n const t = useT()\n const searchParams = useSearchParams()\n const reportId = typeof params?.id === 'string' ? params.id : ''\n const { payload: chrome } = useBackendChrome()\n const canClose = hasFeature(chrome?.grantedFeatures, LOCK_FEATURE)\n const canUnlock = hasFeature(chrome?.grantedFeatures, UNLOCK_FEATURE)\n const canSeeMoney = hasFeature(chrome?.grantedFeatures, RATES_FEATURE)\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n\n const [sheet, setSheet] = React.useState<ReportSheetPayload | null>(null)\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [unlockOpen, setUnlockOpen] = React.useState(false)\n const [unlockReason, setUnlockReason] = React.useState('')\n const [busy, setBusy] = React.useState(false)\n const [reloadToken, setReloadToken] = React.useState(0)\n\n const isClosed = sheet?.report.status === 'closed'\n // Screen 15 is `?unlock=1`, and it may only mean what the unlock button means:\n // the same closed-report state and the same feature. A draft report or a\n // caller without the feature gets the page, never a dialog it cannot use.\n const canUnlockNow = isClosed && canUnlock\n const unlockRequested = searchParams.get('unlock') === '1'\n\n const labels = React.useMemo(\n () => ({\n loadError: t('staff.time_tracking.reports.errors.loadFailed', 'Could not load this report.'),\n closeError: t('staff.time_tracking.reports.errors.closeFailed', 'Could not close this report.'),\n unlockError: t('staff.time_tracking.reports.errors.unlockFailed', 'Could not unlock this report.'),\n exportError: t('staff.time_tracking.reports.errors.exportFailed', 'Could not export this report.'),\n }),\n [t],\n )\n\n const { runMutation, retryLastMutation } = useGuardedMutation<MutationContext>({\n contextId: MUTATION_CONTEXT_ID,\n blockedMessage: labels.closeError,\n })\n\n const mutationContext = React.useCallback(\n (): MutationContext => ({\n formId: MUTATION_CONTEXT_ID,\n resourceKind: RESOURCE_KIND,\n resourceId: reportId,\n retryLastMutation,\n }),\n [reportId, retryLastMutation],\n )\n\n React.useEffect(() => {\n if (!canUnlockNow || !unlockRequested) return\n setUnlockOpen(true)\n }, [canUnlockNow, unlockRequested])\n\n React.useEffect(() => {\n if (!reportId) return\n let cancelled = false\n setLoading(true)\n setError(null)\n void readApiResultOrThrow<Record<string, unknown>>(\n `${REPORTS_API}/${reportId}/sheet`,\n undefined,\n { allowNullResult: true, errorMessage: labels.loadError },\n )\n .then((result) => {\n if (cancelled) return\n const parsed = parseReportSheet(result)\n if (!parsed) {\n setError(labels.loadError)\n setSheet(null)\n return\n }\n setSheet(parsed)\n })\n .catch((err) => {\n if (cancelled) return\n logger.error('staff.time_tracking.reports sheet load failed', { err })\n setError(err instanceof Error ? err.message : labels.loadError)\n })\n .finally(() => {\n if (!cancelled) setLoading(false)\n })\n return () => {\n cancelled = true\n }\n }, [labels.loadError, reloadToken, reportId])\n\n const money = React.useCallback(\n (amount: number | null | undefined) => {\n if (amount === null || amount === undefined) return '\u2014'\n return formatCurrency(amount, sheet?.report.currencyCode ?? undefined) ?? '\u2014'\n },\n [sheet?.report.currencyCode],\n )\n\n const lockedEntriesColumns = React.useMemo<ColumnDef<ReportSheetRow>[]>(\n () => [\n {\n accessorKey: 'date',\n header: t('staff.time_tracking.reports.export.date', 'Date'),\n cell: ({ row }) => <span className=\"text-muted-foreground\">{row.original.date}</span>,\n },\n {\n accessorKey: 'taskLabel',\n header: t('staff.time_tracking.reports.export.task', 'Task'),\n },\n {\n accessorKey: 'personLabel',\n header: t('staff.time_tracking.reports.export.person', 'Person'),\n },\n {\n id: 'hours',\n header: () => (\n <span className=\"block text-right\">\n {t('staff.time_tracking.reports.sheet.time', 'Time')}\n </span>\n ),\n cell: ({ row }) => (\n <span className=\"block font-mono tabular-nums text-right\">{row.original.hours}</span>\n ),\n },\n {\n id: 'amount',\n header: () => (\n <span className=\"block text-right\">\n {t('staff.time_tracking.reports.sheet.amount', 'Amount')}\n </span>\n ),\n cell: ({ row }) => (\n <span className=\"block font-mono tabular-nums text-right\">{money(row.original.amount)}</span>\n ),\n },\n {\n id: 'lock',\n header: '',\n meta: { maxWidth: '2.5rem' },\n cell: () => (\n <span className=\"text-muted-foreground\">\n <Lock aria-hidden=\"true\" className=\"size-4\" />\n </span>\n ),\n },\n ],\n [money, t],\n )\n\n const roundingLabel = React.useMemo(() => {\n const unit = sheet?.report.roundingUnitMinutes ?? 0\n if (!unit) return t('staff.time_tracking.reports.rounding.off', 'no rounding')\n const direction =\n sheet?.report.roundingDirection === 'nearest'\n ? t('staff.time_tracking.reports.rounding.nearest', 'to the nearest')\n : t('staff.time_tracking.reports.rounding.up', 'always up')\n return t('staff.time_tracking.reports.rounding.rule', 'rounding {minutes} min, {direction}')\n .replace('{minutes}', String(unit))\n .replace('{direction}', direction)\n }, [sheet?.report.roundingDirection, sheet?.report.roundingUnitMinutes, t])\n\n const handleExport = React.useCallback(\n async (format: ExportFormat) => {\n if (!reportId) return\n try {\n const call = await apiCall<Blob>(\n `${REPORTS_API}/${reportId}/export?format=${format}`,\n undefined,\n { parse: (response) => response.blob() },\n )\n if (!call.ok || !call.result) {\n flash(labels.exportError, 'error')\n return\n }\n if (typeof window === 'undefined') return\n const href = URL.createObjectURL(call.result)\n const anchor = document.createElement('a')\n anchor.href = href\n anchor.download = `${sheet?.report.reference || 'report'}.${format}`\n document.body.appendChild(anchor)\n anchor.click()\n document.body.removeChild(anchor)\n URL.revokeObjectURL(href)\n // The export appended an `exported` event; refresh so the history shows\n // it before anybody reads the unlock dialog's \"already sent\" warning.\n setReloadToken((current) => current + 1)\n } catch (err) {\n logger.error('staff.time_tracking.reports export failed', { err })\n flash(labels.exportError, 'error')\n }\n },\n [labels.exportError, reportId, sheet?.report.reference],\n )\n\n const handleClose = React.useCallback(async () => {\n if (!reportId || !sheet) return\n const confirmed = await confirm({\n title: t('staff.time_tracking.reports.close.confirmTitle', 'Close and lock this report?'),\n description: t(\n 'staff.time_tracking.reports.close.confirmBody',\n 'The {count} entries it covers become read-only at their current values. New time can still be logged on the same tasks and will go into the next report.',\n ).replace('{count}', String(sheet.totals.entryCount)),\n confirmText: t('staff.time_tracking.reports.close.action', 'Close & lock'),\n })\n if (!confirmed) return\n\n setBusy(true)\n try {\n const call = await runMutation({\n operation: () => apiCall<Record<string, unknown>>(`${REPORTS_API}/${reportId}/close`, { method: 'POST' }),\n context: mutationContext(),\n mutationPayload: { id: reportId },\n })\n if (!call.ok) {\n flash(readErrorMessage(call.result, labels.closeError), 'error')\n return\n }\n flash(t('staff.time_tracking.reports.close.done', 'Report closed. Its entries are locked.'), 'success')\n setReloadToken((current) => current + 1)\n } catch (err) {\n logger.error('staff.time_tracking.reports close failed', { err })\n flash(err instanceof Error ? err.message : labels.closeError, 'error')\n } finally {\n setBusy(false)\n }\n }, [confirm, labels.closeError, mutationContext, reportId, runMutation, sheet, t])\n\n const handleUnlock = React.useCallback(async () => {\n if (!reportId || unlockReason.trim().length === 0) return\n setBusy(true)\n try {\n const call = await runMutation({\n operation: () =>\n apiCall<Record<string, unknown>>(`${REPORTS_API}/${reportId}/unlock`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ reason: unlockReason.trim() }),\n }),\n context: mutationContext(),\n mutationPayload: { id: reportId, reason: unlockReason.trim() },\n })\n if (!call.ok) {\n flash(readErrorMessage(call.result, labels.unlockError), 'error')\n return\n }\n flash(\n t('staff.time_tracking.reports.unlock.done', 'Report unlocked. Its entries can be edited again.'),\n 'success',\n )\n setUnlockOpen(false)\n setUnlockReason('')\n setReloadToken((current) => current + 1)\n } catch (err) {\n logger.error('staff.time_tracking.reports unlock failed', { err })\n flash(err instanceof Error ? err.message : labels.unlockError, 'error')\n } finally {\n setBusy(false)\n }\n }, [labels.unlockError, mutationContext, reportId, runMutation, t, unlockReason])\n\n if (loading) {\n return (\n <Page>\n <PageBody>\n <LoadingMessage label={t('staff.time_tracking.reports.detail.loading', 'Loading report\u2026')} />\n </PageBody>\n </Page>\n )\n }\n\n if (error || !sheet) {\n return (\n <Page>\n <PageBody>\n <ErrorMessage label={error ?? labels.loadError} />\n </PageBody>\n </Page>\n )\n }\n\n const lastExport = sheet.events.find((event) => event.eventType === 'exported') ?? null\n\n const reportInjectionContext: ReportDetailInjectionContext = {\n entityId: REPORT_ENTITY_ID,\n recordId: reportId,\n reportId,\n reference: sheet.report.reference,\n isClosed,\n periodFrom: sheet.report.periodFrom ?? null,\n periodTo: sheet.report.periodTo ?? null,\n retryLastMutation,\n }\n\n return (\n <Page>\n <PageBody>\n <FormHeader\n mode=\"detail\"\n backHref={REPORTS_HREF}\n title={sheet.report.title}\n subtitle={`${sheet.report.periodFrom ?? ''} \u2013 ${sheet.report.periodTo ?? ''}`}\n statusBadge={\n <>\n {isClosed ? (\n <Badge variant=\"success\">\n <Lock aria-hidden=\"true\" className=\"size-3\" />\n {t('staff.time_tracking.reports.status.closed', 'Closed')}\n </Badge>\n ) : (\n <Badge variant=\"warning\">{t('staff.time_tracking.reports.status.draft', 'Draft')}</Badge>\n )}\n <InjectionSpot\n spotId={extensionPoints.hosts.reportDetailStatusBadges.spotId}\n context={reportInjectionContext}\n data={sheet.report}\n />\n </>\n }\n actionsContent={\n <div className=\"flex flex-wrap items-center gap-2\">\n {EXPORT_FORMATS.map((format) => (\n <Button\n key={format}\n variant=\"outline\"\n onClick={() => void handleExport(format)}\n disabled={busy}\n >\n <Download aria-hidden=\"true\" />\n {format.toUpperCase()}\n </Button>\n ))}\n {isClosed ? (\n canUnlockNow ? (\n <Button\n variant=\"destructive-outline\"\n onClick={() => setUnlockOpen(true)}\n disabled={busy}\n >\n <LockOpen aria-hidden=\"true\" />\n {t('staff.time_tracking.reports.unlock.action', 'Unlock entries')}\n </Button>\n ) : null\n ) : canClose ? (\n <Button onClick={() => void handleClose()} disabled={busy}>\n <Lock aria-hidden=\"true\" />\n {t('staff.time_tracking.reports.close.action', 'Close & lock')}\n </Button>\n ) : null}\n <InjectionSpot\n spotId={extensionPoints.hosts.reportDetailHeader.spotId}\n context={reportInjectionContext}\n data={sheet.report}\n />\n </div>\n }\n />\n\n {isClosed ? (\n <Alert status=\"success\" icon={<Lock aria-hidden=\"true\" />} className=\"mb-4\">\n <AlertTitle>\n {t('staff.time_tracking.reports.locked.title', 'The entries in this report are read-only')}\n </AlertTitle>\n <AlertDescription>\n {t(\n 'staff.time_tracking.reports.locked.body',\n 'Their time, rate and billable flag cannot change without unlocking. New time can still be logged on the same tasks \u2014 it goes into the next report.',\n )}\n </AlertDescription>\n </Alert>\n ) : null}\n\n {sheet.alreadyReportedCount > 0 && !sheet.report.includeAlreadyReported ? (\n <Alert status=\"warning\" className=\"mb-4\">\n <AlertDescription>\n {t(\n 'staff.time_tracking.reports.detail.excludedHint',\n '{count} entries in this period are already frozen in another report and are excluded here ({hours}).',\n )\n .replace('{count}', String(sheet.alreadyReportedCount))\n .replace('{hours}', formatReportMinutes(sheet.alreadyReportedMinutes))}\n </AlertDescription>\n </Alert>\n ) : null}\n\n <ReportSheet\n reference={sheet.report.reference}\n customerName={sheet.report.customerName ?? sheet.report.title}\n periodLabel={`${sheet.report.periodFrom ?? ''} \u2013 ${sheet.report.periodTo ?? ''}`}\n issuedByLabel={null}\n issuedAtLabel={sheet.report.closedAt ? sheet.report.closedAt.slice(0, 10) : null}\n currencyCode={sheet.report.currencyCode}\n showRates={sheet.report.showRates && canSeeMoney}\n groups={sheet.groups}\n totals={{\n billableMinutes: sheet.totals.billableMinutes,\n nonbillableMinutes: sheet.totals.nonbillableMinutes,\n totalAmount: sheet.totals.totalAmount,\n }}\n roundingLabel={roundingLabel}\n reportId={reportId}\n />\n\n {isClosed ? (\n <section className=\"mt-6 rounded-lg border border-border\">\n <header className=\"flex flex-wrap items-center justify-between gap-2 border-b border-border p-4\">\n <h3 className=\"text-sm font-semibold\">\n {t('staff.time_tracking.reports.locked.tableTitle', 'Locked entries')}\n </h3>\n <span className=\"font-mono text-xs text-muted-foreground\">\n {t('staff.time_tracking.reports.locked.tableCount', '{count} entries \u00B7 {hours}')\n .replace('{count}', String(sheet.rowCount))\n .replace('{hours}', formatReportMinutes(sheet.totals.billableMinutes))}\n </span>\n </header>\n <DataTable<ReportSheetRow>\n embedded\n extensionTableId={extensionPoints.hosts.reportDetailLockedEntriesTable.tableId}\n disableRowClick\n columns={lockedEntriesColumns}\n data={sheet.rows}\n />\n {sheet.rowsTruncated ? (\n <p className=\"border-t border-border p-3 text-xs text-muted-foreground\">\n {t(\n 'staff.time_tracking.reports.locked.truncated',\n 'Showing the first {shown} of {total} entries \u2014 export the report for the full list.',\n )\n .replace('{shown}', String(sheet.rows.length))\n .replace('{total}', String(sheet.rowCount))}\n </p>\n ) : null}\n </section>\n ) : null}\n\n {sheet.events.length > 0 ? (\n <section className=\"mt-6\">\n <h3 className=\"mb-3 text-xs font-medium uppercase tracking-wide text-muted-foreground\">\n {t('staff.time_tracking.reports.history.title', 'History')}\n </h3>\n <ReportHistory events={sheet.events} />\n </section>\n ) : null}\n\n <Dialog open={unlockOpen} onOpenChange={(open) => setUnlockOpen(open)}>\n <DialogContent\n onKeyDown={(event) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n void handleUnlock()\n }\n }}\n >\n <DialogHeader>\n <DialogTitle>\n {t('staff.time_tracking.reports.unlock.dialogTitle', 'Unlock {count} entries?').replace(\n '{count}',\n String(sheet.rowCount),\n )}\n </DialogTitle>\n </DialogHeader>\n {lastExport ? (\n <Alert status=\"warning\">\n <AlertTitle>\n {t('staff.time_tracking.reports.unlock.sentTitle', 'This report has already been exported')}\n </AlertTitle>\n <AlertDescription>\n {t(\n 'staff.time_tracking.reports.unlock.sentBody',\n 'A file was downloaded on {date}. Changes to unlocked entries will diverge from the copy the client holds.',\n ).replace('{date}', lastExport.createdAt?.slice(0, 10) ?? '')}\n </AlertDescription>\n </Alert>\n ) : null}\n <div className=\"flex flex-col gap-1.5\">\n <Label htmlFor=\"unlock-reason\">\n {t('staff.time_tracking.reports.unlock.reason', 'Reason for unlocking')}\n </Label>\n <Textarea\n id=\"unlock-reason\"\n value={unlockReason}\n onChange={(event) => setUnlockReason(event.target.value)}\n placeholder={t(\n 'staff.time_tracking.reports.unlock.reasonPlaceholder',\n 'e.g. the client disputed 4h on the refactor \u2014 correction agreed on the 21st.',\n )}\n />\n <span className=\"text-xs text-muted-foreground\">\n {t(\n 'staff.time_tracking.reports.unlock.reasonHint',\n 'The reason is recorded in the report history with your name and the date.',\n )}\n </span>\n </div>\n <DialogFooter>\n <Button variant=\"ghost\" onClick={() => setUnlockOpen(false)}>\n {t('staff.time_tracking.reports.unlock.cancel', 'Cancel')}\n </Button>\n <Button\n variant=\"destructive\"\n onClick={() => void handleUnlock()}\n disabled={busy || unlockReason.trim().length === 0}\n >\n <LockOpen aria-hidden=\"true\" />\n {t('staff.time_tracking.reports.unlock.action', 'Unlock entries')}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n\n <InjectionSpot\n spotId={extensionPoints.hosts.reportDetailFooter.spotId}\n context={reportInjectionContext}\n data={sheet.report}\n />\n\n {ConfirmDialogElement}\n </PageBody>\n </Page>\n )\n}\n\nfunction ReportHistory({ events }: { events: readonly ReportHistoryEvent[] }) {\n const t = useT()\n return (\n <ol className=\"flex flex-col gap-3\">\n {events.map((event) => (\n <li key={event.id} className=\"rounded-md border border-border p-3\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"text-sm font-medium\">\n {event.eventType === 'closed'\n ? t('staff.time_tracking.reports.history.closed', 'Closed the report and locked its entries.')\n : event.eventType === 'unlocked'\n ? t('staff.time_tracking.reports.history.unlocked', 'Unlocked the report entries.')\n : t('staff.time_tracking.reports.history.exported', 'Exported the report.')}\n </span>\n <span className=\"text-xs text-muted-foreground\">{event.createdAt?.replace('T', ' ').slice(0, 16)}</span>\n </div>\n {event.reason ? <p className=\"mt-1 text-sm text-muted-foreground\">{event.reason}</p> : null}\n </li>\n ))}\n </ol>\n )\n}\n"],
|
|
5
|
+
"mappings": ";AAuN2B,SAuMf,UAvMe,KAyMX,YAzMW;AAhM3B,YAAY,WAAW;AACvB,SAAS,uBAAuB;AAEhC,SAAS,UAAU,MAAM,gBAAgB;AACzC,SAAS,MAAM,gBAAgB;AAC/B,SAAS,iBAAiB;AAC1B,SAAS,OAAO,kBAAkB,kBAAkB;AACpD,SAAS,aAAa;AACtB,SAAS,cAAc;AACvB,SAAS,aAAa;AACtB,SAAS,gBAAgB;AACzB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,kBAAkB;AAC3B,SAAS,cAAc,sBAAsB;AAC7C,SAAS,SAAS,4BAA4B;AAC9C,SAAS,0BAA0B;AACnC,SAAS,wBAAwB;AACjC,SAAS,wBAAwB;AACjC,SAAS,aAAa;AACtB,SAAS,sBAAsB;AAC/B,SAAS,kBAAkB;AAC3B,SAAS,YAAY;AACrB,SAAS,oBAAoB;AAC7B,SAAS,mBAAmB;AAC5B,SAAS,qBAAqB;AAC9B,SAAS,uBAAuB;AAChC;AAAA,EACE;AAAA,OAIK;AACP,SAAS,2BAA2B;AAEpC,MAAM,SAAS,aAAa,OAAO,EAAE,MAAM,EAAE,WAAW,+BAA+B,CAAC;AAExF,MAAM,eAAe;AACrB,MAAM,cAAc;AACpB,MAAM,eAAe;AACrB,MAAM,iBAAiB;AACvB,MAAM,gBAAgB;AACtB,MAAM,sBAAsB;AAC5B,MAAM,gBAAgB;AACtB,MAAM,iBAAiB,CAAC,OAAO,OAAO,MAAM;AAW5C,MAAM,mBAAmB;AAazB,SAAS,iBAAiB,SAAkB,UAA0B;AACpE,MAAI,WAAW,OAAO,YAAY,UAAU;AAC1C,UAAM,QAAS,QAAgC;AAC/C,QAAI,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,SAAS,EAAG,QAAO;AAAA,EACnE;AACA,SAAO;AACT;AAYe,SAAR,6BAA8C,EAAE,OAAO,GAAiC;AAC7F,QAAM,IAAI,KAAK;AACf,QAAM,eAAe,gBAAgB;AACrC,QAAM,WAAW,OAAO,QAAQ,OAAO,WAAW,OAAO,KAAK;AAC9D,QAAM,EAAE,SAAS,OAAO,IAAI,iBAAiB;AAC7C,QAAM,WAAW,WAAW,QAAQ,iBAAiB,YAAY;AACjE,QAAM,YAAY,WAAW,QAAQ,iBAAiB,cAAc;AACpE,QAAM,cAAc,WAAW,QAAQ,iBAAiB,aAAa;AACrE,QAAM,EAAE,SAAS,qBAAqB,IAAI,iBAAiB;AAE3D,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAoC,IAAI;AACxE,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,IAAI;AACjD,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAwB,IAAI;AAC5D,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAAS,KAAK;AACxD,QAAM,CAAC,cAAc,eAAe,IAAI,MAAM,SAAS,EAAE;AACzD,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAAS,KAAK;AAC5C,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAAS,CAAC;AAEtD,QAAM,WAAW,OAAO,OAAO,WAAW;AAI1C,QAAM,eAAe,YAAY;AACjC,QAAM,kBAAkB,aAAa,IAAI,QAAQ,MAAM;AAEvD,QAAM,SAAS,MAAM;AAAA,IACnB,OAAO;AAAA,MACL,WAAW,EAAE,iDAAiD,6BAA6B;AAAA,MAC3F,YAAY,EAAE,kDAAkD,8BAA8B;AAAA,MAC9F,aAAa,EAAE,mDAAmD,+BAA+B;AAAA,MACjG,aAAa,EAAE,mDAAmD,+BAA+B;AAAA,IACnG;AAAA,IACA,CAAC,CAAC;AAAA,EACJ;AAEA,QAAM,EAAE,aAAa,kBAAkB,IAAI,mBAAoC;AAAA,IAC7E,WAAW;AAAA,IACX,gBAAgB,OAAO;AAAA,EACzB,CAAC;AAED,QAAM,kBAAkB,MAAM;AAAA,IAC5B,OAAwB;AAAA,MACtB,QAAQ;AAAA,MACR,cAAc;AAAA,MACd,YAAY;AAAA,MACZ;AAAA,IACF;AAAA,IACA,CAAC,UAAU,iBAAiB;AAAA,EAC9B;AAEA,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,gBAAgB,CAAC,gBAAiB;AACvC,kBAAc,IAAI;AAAA,EACpB,GAAG,CAAC,cAAc,eAAe,CAAC;AAElC,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,SAAU;AACf,QAAI,YAAY;AAChB,eAAW,IAAI;AACf,aAAS,IAAI;AACb,SAAK;AAAA,MACH,GAAG,WAAW,IAAI,QAAQ;AAAA,MAC1B;AAAA,MACA,EAAE,iBAAiB,MAAM,cAAc,OAAO,UAAU;AAAA,IAC1D,EACG,KAAK,CAAC,WAAW;AAChB,UAAI,UAAW;AACf,YAAM,SAAS,iBAAiB,MAAM;AACtC,UAAI,CAAC,QAAQ;AACX,iBAAS,OAAO,SAAS;AACzB,iBAAS,IAAI;AACb;AAAA,MACF;AACA,eAAS,MAAM;AAAA,IACjB,CAAC,EACA,MAAM,CAAC,QAAQ;AACd,UAAI,UAAW;AACf,aAAO,MAAM,iDAAiD,EAAE,IAAI,CAAC;AACrE,eAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,SAAS;AAAA,IAChE,CAAC,EACA,QAAQ,MAAM;AACb,UAAI,CAAC,UAAW,YAAW,KAAK;AAAA,IAClC,CAAC;AACH,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,OAAO,WAAW,aAAa,QAAQ,CAAC;AAE5C,QAAM,QAAQ,MAAM;AAAA,IAClB,CAAC,WAAsC;AACrC,UAAI,WAAW,QAAQ,WAAW,OAAW,QAAO;AACpD,aAAO,eAAe,QAAQ,OAAO,OAAO,gBAAgB,MAAS,KAAK;AAAA,IAC5E;AAAA,IACA,CAAC,OAAO,OAAO,YAAY;AAAA,EAC7B;AAEA,QAAM,uBAAuB,MAAM;AAAA,IACjC,MAAM;AAAA,MACJ;AAAA,QACE,aAAa;AAAA,QACb,QAAQ,EAAE,2CAA2C,MAAM;AAAA,QAC3D,MAAM,CAAC,EAAE,IAAI,MAAM,oBAAC,UAAK,WAAU,yBAAyB,cAAI,SAAS,MAAK;AAAA,MAChF;AAAA,MACA;AAAA,QACE,aAAa;AAAA,QACb,QAAQ,EAAE,2CAA2C,MAAM;AAAA,MAC7D;AAAA,MACA;AAAA,QACE,aAAa;AAAA,QACb,QAAQ,EAAE,6CAA6C,QAAQ;AAAA,MACjE;AAAA,MACA;AAAA,QACE,IAAI;AAAA,QACJ,QAAQ,MACN,oBAAC,UAAK,WAAU,oBACb,YAAE,0CAA0C,MAAM,GACrD;AAAA,QAEF,MAAM,CAAC,EAAE,IAAI,MACX,oBAAC,UAAK,WAAU,2CAA2C,cAAI,SAAS,OAAM;AAAA,MAElF;AAAA,MACA;AAAA,QACE,IAAI;AAAA,QACJ,QAAQ,MACN,oBAAC,UAAK,WAAU,oBACb,YAAE,4CAA4C,QAAQ,GACzD;AAAA,QAEF,MAAM,CAAC,EAAE,IAAI,MACX,oBAAC,UAAK,WAAU,2CAA2C,gBAAM,IAAI,SAAS,MAAM,GAAE;AAAA,MAE1F;AAAA,MACA;AAAA,QACE,IAAI;AAAA,QACJ,QAAQ;AAAA,QACR,MAAM,EAAE,UAAU,SAAS;AAAA,QAC3B,MAAM,MACJ,oBAAC,UAAK,WAAU,yBACd,8BAAC,QAAK,eAAY,QAAO,WAAU,UAAS,GAC9C;AAAA,MAEJ;AAAA,IACF;AAAA,IACA,CAAC,OAAO,CAAC;AAAA,EACX;AAEA,QAAM,gBAAgB,MAAM,QAAQ,MAAM;AACxC,UAAM,OAAO,OAAO,OAAO,uBAAuB;AAClD,QAAI,CAAC,KAAM,QAAO,EAAE,4CAA4C,aAAa;AAC7E,UAAM,YACJ,OAAO,OAAO,sBAAsB,YAChC,EAAE,gDAAgD,gBAAgB,IAClE,EAAE,2CAA2C,WAAW;AAC9D,WAAO,EAAE,6CAA6C,qCAAqC,EACxF,QAAQ,aAAa,OAAO,IAAI,CAAC,EACjC,QAAQ,eAAe,SAAS;AAAA,EACrC,GAAG,CAAC,OAAO,OAAO,mBAAmB,OAAO,OAAO,qBAAqB,CAAC,CAAC;AAE1E,QAAM,eAAe,MAAM;AAAA,IACzB,OAAO,WAAyB;AAC9B,UAAI,CAAC,SAAU;AACf,UAAI;AACF,cAAM,OAAO,MAAM;AAAA,UACjB,GAAG,WAAW,IAAI,QAAQ,kBAAkB,MAAM;AAAA,UAClD;AAAA,UACA,EAAE,OAAO,CAAC,aAAa,SAAS,KAAK,EAAE;AAAA,QACzC;AACA,YAAI,CAAC,KAAK,MAAM,CAAC,KAAK,QAAQ;AAC5B,gBAAM,OAAO,aAAa,OAAO;AACjC;AAAA,QACF;AACA,YAAI,OAAO,WAAW,YAAa;AACnC,cAAM,OAAO,IAAI,gBAAgB,KAAK,MAAM;AAC5C,cAAM,SAAS,SAAS,cAAc,GAAG;AACzC,eAAO,OAAO;AACd,eAAO,WAAW,GAAG,OAAO,OAAO,aAAa,QAAQ,IAAI,MAAM;AAClE,iBAAS,KAAK,YAAY,MAAM;AAChC,eAAO,MAAM;AACb,iBAAS,KAAK,YAAY,MAAM;AAChC,YAAI,gBAAgB,IAAI;AAGxB,uBAAe,CAAC,YAAY,UAAU,CAAC;AAAA,MACzC,SAAS,KAAK;AACZ,eAAO,MAAM,6CAA6C,EAAE,IAAI,CAAC;AACjE,cAAM,OAAO,aAAa,OAAO;AAAA,MACnC;AAAA,IACF;AAAA,IACA,CAAC,OAAO,aAAa,UAAU,OAAO,OAAO,SAAS;AAAA,EACxD;AAEA,QAAM,cAAc,MAAM,YAAY,YAAY;AAChD,QAAI,CAAC,YAAY,CAAC,MAAO;AACzB,UAAM,YAAY,MAAM,QAAQ;AAAA,MAC9B,OAAO,EAAE,kDAAkD,6BAA6B;AAAA,MACxF,aAAa;AAAA,QACX;AAAA,QACA;AAAA,MACF,EAAE,QAAQ,WAAW,OAAO,MAAM,OAAO,UAAU,CAAC;AAAA,MACpD,aAAa,EAAE,4CAA4C,cAAc;AAAA,IAC3E,CAAC;AACD,QAAI,CAAC,UAAW;AAEhB,YAAQ,IAAI;AACZ,QAAI;AACF,YAAM,OAAO,MAAM,YAAY;AAAA,QAC7B,WAAW,MAAM,QAAiC,GAAG,WAAW,IAAI,QAAQ,UAAU,EAAE,QAAQ,OAAO,CAAC;AAAA,QACxG,SAAS,gBAAgB;AAAA,QACzB,iBAAiB,EAAE,IAAI,SAAS;AAAA,MAClC,CAAC;AACD,UAAI,CAAC,KAAK,IAAI;AACZ,cAAM,iBAAiB,KAAK,QAAQ,OAAO,UAAU,GAAG,OAAO;AAC/D;AAAA,MACF;AACA,YAAM,EAAE,0CAA0C,wCAAwC,GAAG,SAAS;AACtG,qBAAe,CAAC,YAAY,UAAU,CAAC;AAAA,IACzC,SAAS,KAAK;AACZ,aAAO,MAAM,4CAA4C,EAAE,IAAI,CAAC;AAChE,YAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,YAAY,OAAO;AAAA,IACvE,UAAE;AACA,cAAQ,KAAK;AAAA,IACf;AAAA,EACF,GAAG,CAAC,SAAS,OAAO,YAAY,iBAAiB,UAAU,aAAa,OAAO,CAAC,CAAC;AAEjF,QAAM,eAAe,MAAM,YAAY,YAAY;AACjD,QAAI,CAAC,YAAY,aAAa,KAAK,EAAE,WAAW,EAAG;AACnD,YAAQ,IAAI;AACZ,QAAI;AACF,YAAM,OAAO,MAAM,YAAY;AAAA,QAC7B,WAAW,MACT,QAAiC,GAAG,WAAW,IAAI,QAAQ,WAAW;AAAA,UACpE,QAAQ;AAAA,UACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,UAC9C,MAAM,KAAK,UAAU,EAAE,QAAQ,aAAa,KAAK,EAAE,CAAC;AAAA,QACtD,CAAC;AAAA,QACH,SAAS,gBAAgB;AAAA,QACzB,iBAAiB,EAAE,IAAI,UAAU,QAAQ,aAAa,KAAK,EAAE;AAAA,MAC/D,CAAC;AACD,UAAI,CAAC,KAAK,IAAI;AACZ,cAAM,iBAAiB,KAAK,QAAQ,OAAO,WAAW,GAAG,OAAO;AAChE;AAAA,MACF;AACA;AAAA,QACE,EAAE,2CAA2C,mDAAmD;AAAA,QAChG;AAAA,MACF;AACA,oBAAc,KAAK;AACnB,sBAAgB,EAAE;AAClB,qBAAe,CAAC,YAAY,UAAU,CAAC;AAAA,IACzC,SAAS,KAAK;AACZ,aAAO,MAAM,6CAA6C,EAAE,IAAI,CAAC;AACjE,YAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,aAAa,OAAO;AAAA,IACxE,UAAE;AACA,cAAQ,KAAK;AAAA,IACf;AAAA,EACF,GAAG,CAAC,OAAO,aAAa,iBAAiB,UAAU,aAAa,GAAG,YAAY,CAAC;AAEhF,MAAI,SAAS;AACX,WACE,oBAAC,QACC,8BAAC,YACC,8BAAC,kBAAe,OAAO,EAAE,8CAA8C,sBAAiB,GAAG,GAC7F,GACF;AAAA,EAEJ;AAEA,MAAI,SAAS,CAAC,OAAO;AACnB,WACE,oBAAC,QACC,8BAAC,YACC,8BAAC,gBAAa,OAAO,SAAS,OAAO,WAAW,GAClD,GACF;AAAA,EAEJ;AAEA,QAAM,aAAa,MAAM,OAAO,KAAK,CAAC,UAAU,MAAM,cAAc,UAAU,KAAK;AAEnF,QAAM,yBAAuD;AAAA,IAC3D,UAAU;AAAA,IACV,UAAU;AAAA,IACV;AAAA,IACA,WAAW,MAAM,OAAO;AAAA,IACxB;AAAA,IACA,YAAY,MAAM,OAAO,cAAc;AAAA,IACvC,UAAU,MAAM,OAAO,YAAY;AAAA,IACnC;AAAA,EACF;AAEA,SACE,oBAAC,QACC,+BAAC,YACC;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,UAAU;AAAA,QACV,OAAO,MAAM,OAAO;AAAA,QACpB,UAAU,GAAG,MAAM,OAAO,cAAc,EAAE,WAAM,MAAM,OAAO,YAAY,EAAE;AAAA,QAC3E,aACE,iCACG;AAAA,qBACC,qBAAC,SAAM,SAAQ,WACb;AAAA,gCAAC,QAAK,eAAY,QAAO,WAAU,UAAS;AAAA,YAC3C,EAAE,6CAA6C,QAAQ;AAAA,aAC1D,IAEA,oBAAC,SAAM,SAAQ,WAAW,YAAE,4CAA4C,OAAO,GAAE;AAAA,UAEnF;AAAA,YAAC;AAAA;AAAA,cACC,QAAQ,gBAAgB,MAAM,yBAAyB;AAAA,cACvD,SAAS;AAAA,cACT,MAAM,MAAM;AAAA;AAAA,UACd;AAAA,WACF;AAAA,QAEF,gBACE,qBAAC,SAAI,WAAU,qCACZ;AAAA,yBAAe,IAAI,CAAC,WACnB;AAAA,YAAC;AAAA;AAAA,cAEC,SAAQ;AAAA,cACR,SAAS,MAAM,KAAK,aAAa,MAAM;AAAA,cACvC,UAAU;AAAA,cAEV;AAAA,oCAAC,YAAS,eAAY,QAAO;AAAA,gBAC5B,OAAO,YAAY;AAAA;AAAA;AAAA,YANf;AAAA,UAOP,CACD;AAAA,UACA,WACC,eACE;AAAA,YAAC;AAAA;AAAA,cACC,SAAQ;AAAA,cACR,SAAS,MAAM,cAAc,IAAI;AAAA,cACjC,UAAU;AAAA,cAEV;AAAA,oCAAC,YAAS,eAAY,QAAO;AAAA,gBAC5B,EAAE,6CAA6C,gBAAgB;AAAA;AAAA;AAAA,UAClE,IACE,OACF,WACF,qBAAC,UAAO,SAAS,MAAM,KAAK,YAAY,GAAG,UAAU,MACnD;AAAA,gCAAC,QAAK,eAAY,QAAO;AAAA,YACxB,EAAE,4CAA4C,cAAc;AAAA,aAC/D,IACE;AAAA,UACJ;AAAA,YAAC;AAAA;AAAA,cACC,QAAQ,gBAAgB,MAAM,mBAAmB;AAAA,cACjD,SAAS;AAAA,cACT,MAAM,MAAM;AAAA;AAAA,UACd;AAAA,WACF;AAAA;AAAA,IAEJ;AAAA,IAEC,WACC,qBAAC,SAAM,QAAO,WAAU,MAAM,oBAAC,QAAK,eAAY,QAAO,GAAI,WAAU,QACnE;AAAA,0BAAC,cACE,YAAE,4CAA4C,0CAA0C,GAC3F;AAAA,MACA,oBAAC,oBACE;AAAA,QACC;AAAA,QACA;AAAA,MACF,GACF;AAAA,OACF,IACE;AAAA,IAEH,MAAM,uBAAuB,KAAK,CAAC,MAAM,OAAO,yBAC/C,oBAAC,SAAM,QAAO,WAAU,WAAU,QAChC,8BAAC,oBACE;AAAA,MACC;AAAA,MACA;AAAA,IACF,EACG,QAAQ,WAAW,OAAO,MAAM,oBAAoB,CAAC,EACrD,QAAQ,WAAW,oBAAoB,MAAM,sBAAsB,CAAC,GACzE,GACF,IACE;AAAA,IAEJ;AAAA,MAAC;AAAA;AAAA,QACC,WAAW,MAAM,OAAO;AAAA,QACxB,cAAc,MAAM,OAAO,gBAAgB,MAAM,OAAO;AAAA,QACxD,aAAa,GAAG,MAAM,OAAO,cAAc,EAAE,WAAM,MAAM,OAAO,YAAY,EAAE;AAAA,QAC9E,eAAe;AAAA,QACf,eAAe,MAAM,OAAO,WAAW,MAAM,OAAO,SAAS,MAAM,GAAG,EAAE,IAAI;AAAA,QAC5E,cAAc,MAAM,OAAO;AAAA,QAC3B,WAAW,MAAM,OAAO,aAAa;AAAA,QACrC,QAAQ,MAAM;AAAA,QACd,QAAQ;AAAA,UACN,iBAAiB,MAAM,OAAO;AAAA,UAC9B,oBAAoB,MAAM,OAAO;AAAA,UACjC,aAAa,MAAM,OAAO;AAAA,QAC5B;AAAA,QACA;AAAA,QACA;AAAA;AAAA,IACF;AAAA,IAEC,WACC,qBAAC,aAAQ,WAAU,wCACjB;AAAA,2BAAC,YAAO,WAAU,gFAChB;AAAA,4BAAC,QAAG,WAAU,yBACX,YAAE,iDAAiD,gBAAgB,GACtE;AAAA,QACA,oBAAC,UAAK,WAAU,2CACb,YAAE,iDAAiD,8BAA2B,EAC5E,QAAQ,WAAW,OAAO,MAAM,QAAQ,CAAC,EACzC,QAAQ,WAAW,oBAAoB,MAAM,OAAO,eAAe,CAAC,GACzE;AAAA,SACF;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,UAAQ;AAAA,UACR,kBAAkB,gBAAgB,MAAM,+BAA+B;AAAA,UACvE,iBAAe;AAAA,UACf,SAAS;AAAA,UACT,MAAM,MAAM;AAAA;AAAA,MACd;AAAA,MACC,MAAM,gBACL,oBAAC,OAAE,WAAU,4DACV;AAAA,QACC;AAAA,QACA;AAAA,MACF,EACG,QAAQ,WAAW,OAAO,MAAM,KAAK,MAAM,CAAC,EAC5C,QAAQ,WAAW,OAAO,MAAM,QAAQ,CAAC,GAC9C,IACE;AAAA,OACN,IACE;AAAA,IAEH,MAAM,OAAO,SAAS,IACrB,qBAAC,aAAQ,WAAU,QACjB;AAAA,0BAAC,QAAG,WAAU,0EACX,YAAE,6CAA6C,SAAS,GAC3D;AAAA,MACA,oBAAC,iBAAc,QAAQ,MAAM,QAAQ;AAAA,OACvC,IACE;AAAA,IAEJ,oBAAC,UAAO,MAAM,YAAY,cAAc,CAAC,SAAS,cAAc,IAAI,GAClE;AAAA,MAAC;AAAA;AAAA,QACC,WAAW,CAAC,UAAU;AACpB,eAAK,MAAM,WAAW,MAAM,YAAY,MAAM,QAAQ,SAAS;AAC7D,kBAAM,eAAe;AACrB,iBAAK,aAAa;AAAA,UACpB;AAAA,QACF;AAAA,QAEA;AAAA,8BAAC,gBACC,8BAAC,eACE,YAAE,kDAAkD,yBAAyB,EAAE;AAAA,YAC9E;AAAA,YACA,OAAO,MAAM,QAAQ;AAAA,UACvB,GACF,GACF;AAAA,UACC,aACC,qBAAC,SAAM,QAAO,WACZ;AAAA,gCAAC,cACE,YAAE,gDAAgD,uCAAuC,GAC5F;AAAA,YACA,oBAAC,oBACE;AAAA,cACC;AAAA,cACA;AAAA,YACF,EAAE,QAAQ,UAAU,WAAW,WAAW,MAAM,GAAG,EAAE,KAAK,EAAE,GAC9D;AAAA,aACF,IACE;AAAA,UACJ,qBAAC,SAAI,WAAU,yBACb;AAAA,gCAAC,SAAM,SAAQ,iBACZ,YAAE,6CAA6C,sBAAsB,GACxE;AAAA,YACA;AAAA,cAAC;AAAA;AAAA,gBACC,IAAG;AAAA,gBACH,OAAO;AAAA,gBACP,UAAU,CAAC,UAAU,gBAAgB,MAAM,OAAO,KAAK;AAAA,gBACvD,aAAa;AAAA,kBACX;AAAA,kBACA;AAAA,gBACF;AAAA;AAAA,YACF;AAAA,YACA,oBAAC,UAAK,WAAU,iCACb;AAAA,cACC;AAAA,cACA;AAAA,YACF,GACF;AAAA,aACF;AAAA,UACA,qBAAC,gBACC;AAAA,gCAAC,UAAO,SAAQ,SAAQ,SAAS,MAAM,cAAc,KAAK,GACvD,YAAE,6CAA6C,QAAQ,GAC1D;AAAA,YACA;AAAA,cAAC;AAAA;AAAA,gBACC,SAAQ;AAAA,gBACR,SAAS,MAAM,KAAK,aAAa;AAAA,gBACjC,UAAU,QAAQ,aAAa,KAAK,EAAE,WAAW;AAAA,gBAEjD;AAAA,sCAAC,YAAS,eAAY,QAAO;AAAA,kBAC5B,EAAE,6CAA6C,gBAAgB;AAAA;AAAA;AAAA,YAClE;AAAA,aACF;AAAA;AAAA;AAAA,IACF,GACF;AAAA,IAEA;AAAA,MAAC;AAAA;AAAA,QACC,QAAQ,gBAAgB,MAAM,mBAAmB;AAAA,QACjD,SAAS;AAAA,QACT,MAAM,MAAM;AAAA;AAAA,IACd;AAAA,IAEC;AAAA,KACH,GACF;AAEJ;AAEA,SAAS,cAAc,EAAE,OAAO,GAA8C;AAC5E,QAAM,IAAI,KAAK;AACf,SACE,oBAAC,QAAG,WAAU,uBACX,iBAAO,IAAI,CAAC,UACX,qBAAC,QAAkB,WAAU,uCAC3B;AAAA,yBAAC,SAAI,WAAU,qCACb;AAAA,0BAAC,UAAK,WAAU,uBACb,gBAAM,cAAc,WACjB,EAAE,8CAA8C,2CAA2C,IAC3F,MAAM,cAAc,aAClB,EAAE,gDAAgD,8BAA8B,IAChF,EAAE,gDAAgD,sBAAsB,GAChF;AAAA,MACA,oBAAC,UAAK,WAAU,iCAAiC,gBAAM,WAAW,QAAQ,KAAK,GAAG,EAAE,MAAM,GAAG,EAAE,GAAE;AAAA,OACnG;AAAA,IACC,MAAM,SAAS,oBAAC,OAAE,WAAU,sCAAsC,gBAAM,QAAO,IAAO;AAAA,OAXhF,MAAM,EAYf,CACD,GACH;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -45,6 +45,22 @@ const extensionPoints = defineModuleExtensionPoints({
|
|
|
45
45
|
tableId: "staff.time_reports.list",
|
|
46
46
|
source: "backend/staff/time-tracking/reports/page.tsx"
|
|
47
47
|
}),
|
|
48
|
+
myWorkTodayEntriesTable: dataTableExtensionHost({
|
|
49
|
+
tableId: "staff.my_work.today_entries.list",
|
|
50
|
+
source: "backend/staff/time-tracking/page.tsx"
|
|
51
|
+
}),
|
|
52
|
+
projectDetailTimeEntriesTable: dataTableExtensionHost({
|
|
53
|
+
tableId: "staff.time_project_detail.entries.list",
|
|
54
|
+
source: "backend/staff/time-tracking/projects/[id]/page.tsx"
|
|
55
|
+
}),
|
|
56
|
+
reportDetailLockedEntriesTable: dataTableExtensionHost({
|
|
57
|
+
tableId: "staff.time_report_detail.locked_entries.list",
|
|
58
|
+
source: "backend/staff/time-tracking/reports/[id]/page.tsx"
|
|
59
|
+
}),
|
|
60
|
+
taskBoardListViewTable: dataTableExtensionHost({
|
|
61
|
+
tableId: "staff.time_task_board.list_view",
|
|
62
|
+
source: "lib/time-tracking-ui/TaskListView.tsx"
|
|
63
|
+
}),
|
|
48
64
|
projectForm: crudFormExtensionHost({
|
|
49
65
|
entityId: "staff.staff_time_project",
|
|
50
66
|
spotId: "crud-form:staff.staff_time_project",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/staff/extension-points.ts"],
|
|
4
|
-
"sourcesContent": ["import {\n componentExtensionHost,\n crudFormExtensionHost,\n dataTableExtensionHost,\n defineModuleExtensionPoints,\n injectionExtensionHost,\n} from '@open-mercato/shared/modules/widgets/extension-points'\n\n/**\n * Time-tracking host catalog (EP-01 of\n * `.ai/specs/2026-08-24-time-tracking-umes-extension-points.md`).\n *\n * Two id conventions meet here and neither is negotiable:\n *\n * - `crud-form:` hosts carry the **dot** form of the entity id\n * (`staff.staff_time_project`). `CrudForm` derives its own spot id from\n * `entityIds` by replacing every colon with a dot, so the colon form would\n * name a spot no widget is ever loaded for.\n * - `detail:` hosts carry the **colon** form (`detail:staff:staff_time_task:header`).\n * Nothing derives those ids; the host picks them, and these are the ids the\n * spec froze.\n *\n * Every id below is a FROZEN contract surface once shipped \u2014 see\n * `BACKWARD_COMPATIBILITY.md`.\n */\n\nconst detailHost = (spotId: string, source: string) => injectionExtensionHost({\n family: 'detail',\n spotId,\n supported: ['render-widget'],\n contextContract: 'staff.time_tracking.detail.v1',\n scopeContract: 'tenant+organization',\n source,\n})\n\nconst pageHost = (spotId: string, source: string) => injectionExtensionHost({\n family: 'generic',\n spotId,\n supported: ['render-widget'],\n contextContract: 'staff.time_tracking.page.v1',\n scopeContract: 'tenant+organization',\n source,\n})\n\n/**\n * EP-32\u2026EP-41. Each registry is a `specialized-registry` host: contributions\n * arrive through the module's own `register*` function rather than through the\n * widget registries, which is what `registry-contribution` names.\n *\n * `runtimeContract` is the id of the built-in strategy the registry ships with.\n * It is the guarantee that makes this whole group additive: with no contribution\n * the built-in is the only candidate, and it is the same code the module ran\n * before the registry existed.\n */\nconst registryHost = (\n spotId: string,\n source: string,\n contextContract: string,\n builtInId: string,\n) => injectionExtensionHost({\n family: 'specialized-registry',\n spotId,\n supported: ['registry-contribution'],\n contextContract,\n scopeContract: 'tenant+organization',\n runtimeContract: builtInId,\n source,\n})\n\nexport const extensionPoints = defineModuleExtensionPoints({\n moduleId: 'staff',\n hosts: {\n timeEntriesTable: dataTableExtensionHost({\n tableId: 'staff.time_entries.list',\n source: 'backend/staff/time-tracking/entries/page.tsx',\n }),\n timeProjectsTable: dataTableExtensionHost({\n tableId: 'staff.time_projects.list',\n source: 'backend/staff/time-tracking/projects/page.tsx',\n }),\n timeReportsTable: dataTableExtensionHost({\n tableId: 'staff.time_reports.list',\n source: 'backend/staff/time-tracking/reports/page.tsx',\n }),\n\n projectForm: crudFormExtensionHost({\n entityId: 'staff.staff_time_project',\n spotId: 'crud-form:staff.staff_time_project',\n source: 'backend/staff/time-tracking/projects/create/page.tsx',\n }),\n timeEntryForm: crudFormExtensionHost({\n entityId: 'staff.staff_time_entry',\n spotId: 'crud-form:staff.staff_time_entry',\n source: 'lib/time-tracking-ui/TimeEntryDialog.tsx',\n }),\n taskForm: crudFormExtensionHost({\n entityId: 'staff.staff_time_task',\n spotId: 'crud-form:staff.staff_time_task',\n source: 'lib/time-tracking-ui/NewTaskDialog.tsx',\n }),\n reportForm: crudFormExtensionHost({\n entityId: 'staff.staff_time_report',\n spotId: 'crud-form:staff.staff_time_report',\n source: 'backend/staff/time-tracking/reports/create/page.tsx',\n }),\n\n projectDetailHeader: detailHost('detail:staff:staff_time_project:header', 'backend/staff/time-tracking/projects/[id]/page.tsx'),\n projectDetailStatusBadges: detailHost('detail:staff:staff_time_project:status-badges', 'backend/staff/time-tracking/projects/[id]/page.tsx'),\n projectDetailTabs: detailHost('detail:staff:staff_time_project:tabs', 'backend/staff/time-tracking/projects/[id]/page.tsx'),\n projectDetailSidebar: detailHost('detail:staff:staff_time_project:sidebar', 'backend/staff/time-tracking/projects/[id]/page.tsx'),\n projectDetailFooter: detailHost('detail:staff:staff_time_project:footer', 'backend/staff/time-tracking/projects/[id]/page.tsx'),\n\n taskDetailHeader: detailHost('detail:staff:staff_time_task:header', 'lib/time-tracking-ui/TaskDrawer.tsx'),\n taskDetailStatusBadges: detailHost('detail:staff:staff_time_task:status-badges', 'lib/time-tracking-ui/TaskDrawer.tsx'),\n taskDetailTabs: detailHost('detail:staff:staff_time_task:tabs', 'lib/time-tracking-ui/TaskDrawer.tsx'),\n taskDetailSidebar: detailHost('detail:staff:staff_time_task:sidebar', 'lib/time-tracking-ui/TaskDrawer.tsx'),\n taskDetailFooter: detailHost('detail:staff:staff_time_task:footer', 'lib/time-tracking-ui/TaskDrawer.tsx'),\n\n reportDetailHeader: detailHost('detail:staff:staff_time_report:header', 'backend/staff/time-tracking/reports/[id]/page.tsx'),\n reportDetailStatusBadges: detailHost('detail:staff:staff_time_report:status-badges', 'backend/staff/time-tracking/reports/[id]/page.tsx'),\n reportDetailFooter: detailHost('detail:staff:staff_time_report:footer', 'backend/staff/time-tracking/reports/[id]/page.tsx'),\n\n reportSheetBeforeLines: pageHost('staff.time_report.sheet:before-lines', 'lib/time-tracking-ui/ReportSheet.tsx'),\n reportSheetAfterTotals: pageHost('staff.time_report.sheet:after-totals', 'lib/time-tracking-ui/ReportSheet.tsx'),\n\n timesheetToolbar: pageHost('staff.timesheet:toolbar', 'backend/staff/time-tracking/timesheet/page.tsx'),\n timesheetPeriodFooter: pageHost('staff.timesheet:period-footer', 'lib/time-tracking-ui/TimesheetPeriodFooter.tsx'),\n timesheetDayCellActions: pageHost('staff.timesheet:day-cell-actions', 'lib/time-tracking-ui/TimesheetCalendar.tsx'),\n\n taskBoardToolbar: pageHost('staff.time_task.board:toolbar', 'lib/time-tracking-ui/KanbanBoard.tsx'),\n taskBoardColumnHeader: pageHost('staff.time_task.board:column-header', 'lib/time-tracking-ui/KanbanColumn.tsx'),\n taskBoardCardBadges: pageHost('staff.time_task.board:card-badges', 'lib/time-tracking-ui/KanbanCard.tsx'),\n taskBoardCardFooter: pageHost('staff.time_task.board:card-footer', 'lib/time-tracking-ui/KanbanCard.tsx'),\n\n myWorkBeforeSections: pageHost('staff.my_work:before-sections', 'backend/staff/time-tracking/page.tsx'),\n myWorkAfterSections: pageHost('staff.my_work:after-sections', 'backend/staff/time-tracking/page.tsx'),\n\n timeTrackingSettingsSections: pageHost('staff.time_tracking.settings:sections', 'backend/staff/time-tracking/settings/page.tsx'),\n\n timerBarActions: pageHost('staff.timesheets.timer-bar:actions', 'lib/timesheets-ui/TimerBar.tsx'),\n\n /**\n * EP-50. The two customer-portal spots. `scopeContract` names the third\n * dimension the backoffice spots do not have: a portal context is scoped by\n * the signed-in **customer** as well as the tenant and organization, and the\n * host passes no money and no staff identity into either spot.\n */\n portalTimeReportBefore: injectionExtensionHost({\n family: 'portal-page',\n spotId: 'portal:staff.time_report:before',\n supported: ['render-widget'],\n contextContract: 'staff.time_tracking.portal.report.v1',\n scopeContract: 'tenant.organization.customer-session',\n source: 'frontend/[orgSlug]/portal/time-reports/[id]/page.tsx',\n }),\n portalTimeReportAfter: injectionExtensionHost({\n family: 'portal-page',\n spotId: 'portal:staff.time_report:after',\n supported: ['render-widget'],\n contextContract: 'staff.time_tracking.portal.report.v1',\n scopeContract: 'tenant.organization.customer-session',\n source: 'frontend/[orgSlug]/portal/time-reports/[id]/page.tsx',\n }),\n\n timeRoundingRegistry: registryHost(\n 'staff.time_tracking.rounding',\n 'lib/time-tracking/rounding.ts',\n 'staff.time_tracking.rounding.v1',\n 'staff.time_tracking.rounding.unit',\n ),\n timeRateRegistry: registryHost(\n 'staff.time_tracking.rate',\n 'lib/time-tracking/cost.ts',\n 'staff.time_tracking.rate.v1',\n 'staff.time_tracking.rate.entry_override_then_project',\n ),\n timeBillabilityRegistry: registryHost(\n 'staff.time_tracking.billability',\n 'lib/time-tracking/billability.ts',\n 'staff.time_tracking.billability.v1',\n 'staff.time_tracking.billability.project_then_tenant',\n ),\n reportExportFormatRegistry: registryHost(\n 'staff.time_tracking.report_export_format',\n 'lib/timesheets-reports/reportExportFormats.ts',\n 'staff.time_tracking.report_export_format.v1',\n 'pdf',\n ),\n reportGroupingRegistry: registryHost(\n 'staff.time_tracking.report_grouping',\n 'lib/timesheets-reports/reportGroupings.ts',\n 'staff.time_tracking.report_grouping.v1',\n 'project_task',\n ),\n timeEntrySourceRegistry: registryHost(\n 'staff.time_tracking.time_entry_source',\n 'lib/time-tracking/timeEntrySources.ts',\n 'staff.time_tracking.time_entry_source.v1',\n 'manual',\n ),\n overlapPolicyRegistry: registryHost(\n 'staff.time_tracking.overlap_policy',\n 'lib/time-tracking/overlap.ts',\n 'staff.time_tracking.overlap_policy.v1',\n 'staff.time_tracking.overlap.warn_when_enabled',\n ),\n projectCodeGeneratorRegistry: registryHost(\n 'staff.time_tracking.project_code_generator',\n 'lib/time-tracking/projectCode.ts',\n 'staff.time_tracking.project_code_generator.v1',\n 'staff.time_tracking.project_code.initials',\n ),\n capacityProviderRegistry: registryHost(\n 'staff.time_tracking.capacity_provider',\n 'lib/time-tracking/capacity.ts',\n 'staff.time_tracking.capacity_provider.v1',\n 'staff.time_tracking.capacity.flat_daily_hours',\n ),\n reportApprovalPolicyRegistry: registryHost(\n 'staff.time_tracking.report_approval_policy',\n 'lib/timesheets-reports/reportApprovalPolicies.ts',\n 'staff.time_tracking.report_approval_policy.v1',\n 'staff.time_tracking.report_approval.acl_only',\n ),\n /**\n * EP-51. The recalculation registry: a hook is a tenant-wide restatement of\n * derived time-tracking values, run by the existing queue worker under the\n * existing `ProgressJob`. The built-in is the retro-rounding pass the module\n * already shipped, so a deployment with no contribution runs exactly what it\n * ran before.\n */\n recalculationRegistry: registryHost(\n 'staff.time_tracking.recalculation',\n 'lib/time-tracking/recalculations.ts',\n 'staff.time_tracking.recalculation.v1',\n 'staff.time_tracking.recalculation.rounding',\n ),\n\n /**\n * EP-42. The one registry whose scope contract is `tenant` rather than\n * `tenant+organization`: time-tracking settings are tenant-global by spec \u00A710,\n * stored through `ModuleConfigService` with `organization_id` null, and a\n * contributed key inherits that scope rather than choosing its own.\n */\n settingKeyRegistry: injectionExtensionHost({\n family: 'specialized-registry',\n spotId: 'staff.time_tracking.setting_key',\n supported: ['registry-contribution'],\n contextContract: 'staff.time_tracking.setting_key.v1',\n scopeContract: 'tenant',\n runtimeContract: 'rounding.unitMinutes',\n source: 'lib/time-tracking/settingKeys.ts',\n }),\n\n timeEntryDialogComponent: componentExtensionHost({\n componentId: 'staff.time_entry_dialog',\n propsContract: 'staff.time_entry_dialog.props.v1',\n source: 'lib/time-tracking-ui/TimeEntryDialog.tsx',\n }),\n timerBarComponent: componentExtensionHost({\n componentId: 'staff.timer_bar',\n propsContract: 'staff.timer_bar.props.v1',\n source: 'lib/timesheets-ui/TimerBar.tsx',\n }),\n kanbanCardComponent: componentExtensionHost({\n componentId: 'staff.kanban_card',\n propsContract: 'staff.kanban_card.props.v1',\n source: 'lib/time-tracking-ui/KanbanCard.tsx',\n }),\n kanbanColumnComponent: componentExtensionHost({\n componentId: 'staff.kanban_column',\n propsContract: 'staff.kanban_column.props.v1',\n source: 'lib/time-tracking-ui/KanbanColumn.tsx',\n }),\n timesheetGridComponent: componentExtensionHost({\n componentId: 'staff.timesheet_grid',\n propsContract: 'staff.timesheet_grid.props.v1',\n source: 'backend/staff/time-tracking/timesheet/GridView.tsx',\n }),\n timesheetListComponent: componentExtensionHost({\n componentId: 'staff.timesheet_list',\n propsContract: 'staff.timesheet_list.props.v1',\n source: 'lib/timesheets-ui/ListView.tsx',\n }),\n timesheetCalendarComponent: componentExtensionHost({\n componentId: 'staff.timesheet_calendar',\n propsContract: 'staff.timesheet_calendar.props.v1',\n source: 'lib/time-tracking-ui/TimesheetCalendar.tsx',\n }),\n reportSheetComponent: componentExtensionHost({\n componentId: 'staff.report_sheet',\n propsContract: 'staff.report_sheet.props.v1',\n source: 'lib/time-tracking-ui/ReportSheet.tsx',\n }),\n projectCardComponent: componentExtensionHost({\n componentId: 'staff.project_card',\n propsContract: 'staff.project_card.props.v1',\n source: 'lib/timesheets-projects-ui/ProjectCard.tsx',\n }),\n entriesSummaryFooterComponent: componentExtensionHost({\n componentId: 'staff.entries_summary_footer',\n propsContract: 'staff.entries_summary_footer.props.v1',\n source: 'lib/time-tracking-ui/TimeEntriesSummaryFooter.tsx',\n }),\n },\n})\n\nexport default extensionPoints\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import {\n componentExtensionHost,\n crudFormExtensionHost,\n dataTableExtensionHost,\n defineModuleExtensionPoints,\n injectionExtensionHost,\n} from '@open-mercato/shared/modules/widgets/extension-points'\n\n/**\n * Time-tracking host catalog (EP-01 of\n * `.ai/specs/2026-08-24-time-tracking-umes-extension-points.md`).\n *\n * Two id conventions meet here and neither is negotiable:\n *\n * - `crud-form:` hosts carry the **dot** form of the entity id\n * (`staff.staff_time_project`). `CrudForm` derives its own spot id from\n * `entityIds` by replacing every colon with a dot, so the colon form would\n * name a spot no widget is ever loaded for.\n * - `detail:` hosts carry the **colon** form (`detail:staff:staff_time_task:header`).\n * Nothing derives those ids; the host picks them, and these are the ids the\n * spec froze.\n *\n * Every id below is a FROZEN contract surface once shipped \u2014 see\n * `BACKWARD_COMPATIBILITY.md`.\n */\n\nconst detailHost = (spotId: string, source: string) => injectionExtensionHost({\n family: 'detail',\n spotId,\n supported: ['render-widget'],\n contextContract: 'staff.time_tracking.detail.v1',\n scopeContract: 'tenant+organization',\n source,\n})\n\nconst pageHost = (spotId: string, source: string) => injectionExtensionHost({\n family: 'generic',\n spotId,\n supported: ['render-widget'],\n contextContract: 'staff.time_tracking.page.v1',\n scopeContract: 'tenant+organization',\n source,\n})\n\n/**\n * EP-32\u2026EP-41. Each registry is a `specialized-registry` host: contributions\n * arrive through the module's own `register*` function rather than through the\n * widget registries, which is what `registry-contribution` names.\n *\n * `runtimeContract` is the id of the built-in strategy the registry ships with.\n * It is the guarantee that makes this whole group additive: with no contribution\n * the built-in is the only candidate, and it is the same code the module ran\n * before the registry existed.\n */\nconst registryHost = (\n spotId: string,\n source: string,\n contextContract: string,\n builtInId: string,\n) => injectionExtensionHost({\n family: 'specialized-registry',\n spotId,\n supported: ['registry-contribution'],\n contextContract,\n scopeContract: 'tenant+organization',\n runtimeContract: builtInId,\n source,\n})\n\nexport const extensionPoints = defineModuleExtensionPoints({\n moduleId: 'staff',\n hosts: {\n timeEntriesTable: dataTableExtensionHost({\n tableId: 'staff.time_entries.list',\n source: 'backend/staff/time-tracking/entries/page.tsx',\n }),\n timeProjectsTable: dataTableExtensionHost({\n tableId: 'staff.time_projects.list',\n source: 'backend/staff/time-tracking/projects/page.tsx',\n }),\n timeReportsTable: dataTableExtensionHost({\n tableId: 'staff.time_reports.list',\n source: 'backend/staff/time-tracking/reports/page.tsx',\n }),\n myWorkTodayEntriesTable: dataTableExtensionHost({\n tableId: 'staff.my_work.today_entries.list',\n source: 'backend/staff/time-tracking/page.tsx',\n }),\n projectDetailTimeEntriesTable: dataTableExtensionHost({\n tableId: 'staff.time_project_detail.entries.list',\n source: 'backend/staff/time-tracking/projects/[id]/page.tsx',\n }),\n reportDetailLockedEntriesTable: dataTableExtensionHost({\n tableId: 'staff.time_report_detail.locked_entries.list',\n source: 'backend/staff/time-tracking/reports/[id]/page.tsx',\n }),\n taskBoardListViewTable: dataTableExtensionHost({\n tableId: 'staff.time_task_board.list_view',\n source: 'lib/time-tracking-ui/TaskListView.tsx',\n }),\n\n projectForm: crudFormExtensionHost({\n entityId: 'staff.staff_time_project',\n spotId: 'crud-form:staff.staff_time_project',\n source: 'backend/staff/time-tracking/projects/create/page.tsx',\n }),\n timeEntryForm: crudFormExtensionHost({\n entityId: 'staff.staff_time_entry',\n spotId: 'crud-form:staff.staff_time_entry',\n source: 'lib/time-tracking-ui/TimeEntryDialog.tsx',\n }),\n taskForm: crudFormExtensionHost({\n entityId: 'staff.staff_time_task',\n spotId: 'crud-form:staff.staff_time_task',\n source: 'lib/time-tracking-ui/NewTaskDialog.tsx',\n }),\n reportForm: crudFormExtensionHost({\n entityId: 'staff.staff_time_report',\n spotId: 'crud-form:staff.staff_time_report',\n source: 'backend/staff/time-tracking/reports/create/page.tsx',\n }),\n\n projectDetailHeader: detailHost('detail:staff:staff_time_project:header', 'backend/staff/time-tracking/projects/[id]/page.tsx'),\n projectDetailStatusBadges: detailHost('detail:staff:staff_time_project:status-badges', 'backend/staff/time-tracking/projects/[id]/page.tsx'),\n projectDetailTabs: detailHost('detail:staff:staff_time_project:tabs', 'backend/staff/time-tracking/projects/[id]/page.tsx'),\n projectDetailSidebar: detailHost('detail:staff:staff_time_project:sidebar', 'backend/staff/time-tracking/projects/[id]/page.tsx'),\n projectDetailFooter: detailHost('detail:staff:staff_time_project:footer', 'backend/staff/time-tracking/projects/[id]/page.tsx'),\n\n taskDetailHeader: detailHost('detail:staff:staff_time_task:header', 'lib/time-tracking-ui/TaskDrawer.tsx'),\n taskDetailStatusBadges: detailHost('detail:staff:staff_time_task:status-badges', 'lib/time-tracking-ui/TaskDrawer.tsx'),\n taskDetailTabs: detailHost('detail:staff:staff_time_task:tabs', 'lib/time-tracking-ui/TaskDrawer.tsx'),\n taskDetailSidebar: detailHost('detail:staff:staff_time_task:sidebar', 'lib/time-tracking-ui/TaskDrawer.tsx'),\n taskDetailFooter: detailHost('detail:staff:staff_time_task:footer', 'lib/time-tracking-ui/TaskDrawer.tsx'),\n\n reportDetailHeader: detailHost('detail:staff:staff_time_report:header', 'backend/staff/time-tracking/reports/[id]/page.tsx'),\n reportDetailStatusBadges: detailHost('detail:staff:staff_time_report:status-badges', 'backend/staff/time-tracking/reports/[id]/page.tsx'),\n reportDetailFooter: detailHost('detail:staff:staff_time_report:footer', 'backend/staff/time-tracking/reports/[id]/page.tsx'),\n\n reportSheetBeforeLines: pageHost('staff.time_report.sheet:before-lines', 'lib/time-tracking-ui/ReportSheet.tsx'),\n reportSheetAfterTotals: pageHost('staff.time_report.sheet:after-totals', 'lib/time-tracking-ui/ReportSheet.tsx'),\n\n timesheetToolbar: pageHost('staff.timesheet:toolbar', 'backend/staff/time-tracking/timesheet/page.tsx'),\n timesheetPeriodFooter: pageHost('staff.timesheet:period-footer', 'lib/time-tracking-ui/TimesheetPeriodFooter.tsx'),\n timesheetDayCellActions: pageHost('staff.timesheet:day-cell-actions', 'lib/time-tracking-ui/TimesheetCalendar.tsx'),\n\n taskBoardToolbar: pageHost('staff.time_task.board:toolbar', 'lib/time-tracking-ui/KanbanBoard.tsx'),\n taskBoardColumnHeader: pageHost('staff.time_task.board:column-header', 'lib/time-tracking-ui/KanbanColumn.tsx'),\n taskBoardCardBadges: pageHost('staff.time_task.board:card-badges', 'lib/time-tracking-ui/KanbanCard.tsx'),\n taskBoardCardFooter: pageHost('staff.time_task.board:card-footer', 'lib/time-tracking-ui/KanbanCard.tsx'),\n\n myWorkBeforeSections: pageHost('staff.my_work:before-sections', 'backend/staff/time-tracking/page.tsx'),\n myWorkAfterSections: pageHost('staff.my_work:after-sections', 'backend/staff/time-tracking/page.tsx'),\n\n timeTrackingSettingsSections: pageHost('staff.time_tracking.settings:sections', 'backend/staff/time-tracking/settings/page.tsx'),\n\n timerBarActions: pageHost('staff.timesheets.timer-bar:actions', 'lib/timesheets-ui/TimerBar.tsx'),\n\n /**\n * EP-50. The two customer-portal spots. `scopeContract` names the third\n * dimension the backoffice spots do not have: a portal context is scoped by\n * the signed-in **customer** as well as the tenant and organization, and the\n * host passes no money and no staff identity into either spot.\n */\n portalTimeReportBefore: injectionExtensionHost({\n family: 'portal-page',\n spotId: 'portal:staff.time_report:before',\n supported: ['render-widget'],\n contextContract: 'staff.time_tracking.portal.report.v1',\n scopeContract: 'tenant.organization.customer-session',\n source: 'frontend/[orgSlug]/portal/time-reports/[id]/page.tsx',\n }),\n portalTimeReportAfter: injectionExtensionHost({\n family: 'portal-page',\n spotId: 'portal:staff.time_report:after',\n supported: ['render-widget'],\n contextContract: 'staff.time_tracking.portal.report.v1',\n scopeContract: 'tenant.organization.customer-session',\n source: 'frontend/[orgSlug]/portal/time-reports/[id]/page.tsx',\n }),\n\n timeRoundingRegistry: registryHost(\n 'staff.time_tracking.rounding',\n 'lib/time-tracking/rounding.ts',\n 'staff.time_tracking.rounding.v1',\n 'staff.time_tracking.rounding.unit',\n ),\n timeRateRegistry: registryHost(\n 'staff.time_tracking.rate',\n 'lib/time-tracking/cost.ts',\n 'staff.time_tracking.rate.v1',\n 'staff.time_tracking.rate.entry_override_then_project',\n ),\n timeBillabilityRegistry: registryHost(\n 'staff.time_tracking.billability',\n 'lib/time-tracking/billability.ts',\n 'staff.time_tracking.billability.v1',\n 'staff.time_tracking.billability.project_then_tenant',\n ),\n reportExportFormatRegistry: registryHost(\n 'staff.time_tracking.report_export_format',\n 'lib/timesheets-reports/reportExportFormats.ts',\n 'staff.time_tracking.report_export_format.v1',\n 'pdf',\n ),\n reportGroupingRegistry: registryHost(\n 'staff.time_tracking.report_grouping',\n 'lib/timesheets-reports/reportGroupings.ts',\n 'staff.time_tracking.report_grouping.v1',\n 'project_task',\n ),\n timeEntrySourceRegistry: registryHost(\n 'staff.time_tracking.time_entry_source',\n 'lib/time-tracking/timeEntrySources.ts',\n 'staff.time_tracking.time_entry_source.v1',\n 'manual',\n ),\n overlapPolicyRegistry: registryHost(\n 'staff.time_tracking.overlap_policy',\n 'lib/time-tracking/overlap.ts',\n 'staff.time_tracking.overlap_policy.v1',\n 'staff.time_tracking.overlap.warn_when_enabled',\n ),\n projectCodeGeneratorRegistry: registryHost(\n 'staff.time_tracking.project_code_generator',\n 'lib/time-tracking/projectCode.ts',\n 'staff.time_tracking.project_code_generator.v1',\n 'staff.time_tracking.project_code.initials',\n ),\n capacityProviderRegistry: registryHost(\n 'staff.time_tracking.capacity_provider',\n 'lib/time-tracking/capacity.ts',\n 'staff.time_tracking.capacity_provider.v1',\n 'staff.time_tracking.capacity.flat_daily_hours',\n ),\n reportApprovalPolicyRegistry: registryHost(\n 'staff.time_tracking.report_approval_policy',\n 'lib/timesheets-reports/reportApprovalPolicies.ts',\n 'staff.time_tracking.report_approval_policy.v1',\n 'staff.time_tracking.report_approval.acl_only',\n ),\n /**\n * EP-51. The recalculation registry: a hook is a tenant-wide restatement of\n * derived time-tracking values, run by the existing queue worker under the\n * existing `ProgressJob`. The built-in is the retro-rounding pass the module\n * already shipped, so a deployment with no contribution runs exactly what it\n * ran before.\n */\n recalculationRegistry: registryHost(\n 'staff.time_tracking.recalculation',\n 'lib/time-tracking/recalculations.ts',\n 'staff.time_tracking.recalculation.v1',\n 'staff.time_tracking.recalculation.rounding',\n ),\n\n /**\n * EP-42. The one registry whose scope contract is `tenant` rather than\n * `tenant+organization`: time-tracking settings are tenant-global by spec \u00A710,\n * stored through `ModuleConfigService` with `organization_id` null, and a\n * contributed key inherits that scope rather than choosing its own.\n */\n settingKeyRegistry: injectionExtensionHost({\n family: 'specialized-registry',\n spotId: 'staff.time_tracking.setting_key',\n supported: ['registry-contribution'],\n contextContract: 'staff.time_tracking.setting_key.v1',\n scopeContract: 'tenant',\n runtimeContract: 'rounding.unitMinutes',\n source: 'lib/time-tracking/settingKeys.ts',\n }),\n\n timeEntryDialogComponent: componentExtensionHost({\n componentId: 'staff.time_entry_dialog',\n propsContract: 'staff.time_entry_dialog.props.v1',\n source: 'lib/time-tracking-ui/TimeEntryDialog.tsx',\n }),\n timerBarComponent: componentExtensionHost({\n componentId: 'staff.timer_bar',\n propsContract: 'staff.timer_bar.props.v1',\n source: 'lib/timesheets-ui/TimerBar.tsx',\n }),\n kanbanCardComponent: componentExtensionHost({\n componentId: 'staff.kanban_card',\n propsContract: 'staff.kanban_card.props.v1',\n source: 'lib/time-tracking-ui/KanbanCard.tsx',\n }),\n kanbanColumnComponent: componentExtensionHost({\n componentId: 'staff.kanban_column',\n propsContract: 'staff.kanban_column.props.v1',\n source: 'lib/time-tracking-ui/KanbanColumn.tsx',\n }),\n timesheetGridComponent: componentExtensionHost({\n componentId: 'staff.timesheet_grid',\n propsContract: 'staff.timesheet_grid.props.v1',\n source: 'backend/staff/time-tracking/timesheet/GridView.tsx',\n }),\n timesheetListComponent: componentExtensionHost({\n componentId: 'staff.timesheet_list',\n propsContract: 'staff.timesheet_list.props.v1',\n source: 'lib/timesheets-ui/ListView.tsx',\n }),\n timesheetCalendarComponent: componentExtensionHost({\n componentId: 'staff.timesheet_calendar',\n propsContract: 'staff.timesheet_calendar.props.v1',\n source: 'lib/time-tracking-ui/TimesheetCalendar.tsx',\n }),\n reportSheetComponent: componentExtensionHost({\n componentId: 'staff.report_sheet',\n propsContract: 'staff.report_sheet.props.v1',\n source: 'lib/time-tracking-ui/ReportSheet.tsx',\n }),\n projectCardComponent: componentExtensionHost({\n componentId: 'staff.project_card',\n propsContract: 'staff.project_card.props.v1',\n source: 'lib/timesheets-projects-ui/ProjectCard.tsx',\n }),\n entriesSummaryFooterComponent: componentExtensionHost({\n componentId: 'staff.entries_summary_footer',\n propsContract: 'staff.entries_summary_footer.props.v1',\n source: 'lib/time-tracking-ui/TimeEntriesSummaryFooter.tsx',\n }),\n },\n})\n\nexport default extensionPoints\n"],
|
|
5
|
+
"mappings": "AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAoBP,MAAM,aAAa,CAAC,QAAgB,WAAmB,uBAAuB;AAAA,EAC5E,QAAQ;AAAA,EACR;AAAA,EACA,WAAW,CAAC,eAAe;AAAA,EAC3B,iBAAiB;AAAA,EACjB,eAAe;AAAA,EACf;AACF,CAAC;AAED,MAAM,WAAW,CAAC,QAAgB,WAAmB,uBAAuB;AAAA,EAC1E,QAAQ;AAAA,EACR;AAAA,EACA,WAAW,CAAC,eAAe;AAAA,EAC3B,iBAAiB;AAAA,EACjB,eAAe;AAAA,EACf;AACF,CAAC;AAYD,MAAM,eAAe,CACnB,QACA,QACA,iBACA,cACG,uBAAuB;AAAA,EAC1B,QAAQ;AAAA,EACR;AAAA,EACA,WAAW,CAAC,uBAAuB;AAAA,EACnC;AAAA,EACA,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB;AACF,CAAC;AAEM,MAAM,kBAAkB,4BAA4B;AAAA,EACzD,UAAU;AAAA,EACV,OAAO;AAAA,IACL,kBAAkB,uBAAuB;AAAA,MACvC,SAAS;AAAA,MACT,QAAQ;AAAA,IACV,CAAC;AAAA,IACD,mBAAmB,uBAAuB;AAAA,MACxC,SAAS;AAAA,MACT,QAAQ;AAAA,IACV,CAAC;AAAA,IACD,kBAAkB,uBAAuB;AAAA,MACvC,SAAS;AAAA,MACT,QAAQ;AAAA,IACV,CAAC;AAAA,IACD,yBAAyB,uBAAuB;AAAA,MAC9C,SAAS;AAAA,MACT,QAAQ;AAAA,IACV,CAAC;AAAA,IACD,+BAA+B,uBAAuB;AAAA,MACpD,SAAS;AAAA,MACT,QAAQ;AAAA,IACV,CAAC;AAAA,IACD,gCAAgC,uBAAuB;AAAA,MACrD,SAAS;AAAA,MACT,QAAQ;AAAA,IACV,CAAC;AAAA,IACD,wBAAwB,uBAAuB;AAAA,MAC7C,SAAS;AAAA,MACT,QAAQ;AAAA,IACV,CAAC;AAAA,IAED,aAAa,sBAAsB;AAAA,MACjC,UAAU;AAAA,MACV,QAAQ;AAAA,MACR,QAAQ;AAAA,IACV,CAAC;AAAA,IACD,eAAe,sBAAsB;AAAA,MACnC,UAAU;AAAA,MACV,QAAQ;AAAA,MACR,QAAQ;AAAA,IACV,CAAC;AAAA,IACD,UAAU,sBAAsB;AAAA,MAC9B,UAAU;AAAA,MACV,QAAQ;AAAA,MACR,QAAQ;AAAA,IACV,CAAC;AAAA,IACD,YAAY,sBAAsB;AAAA,MAChC,UAAU;AAAA,MACV,QAAQ;AAAA,MACR,QAAQ;AAAA,IACV,CAAC;AAAA,IAED,qBAAqB,WAAW,0CAA0C,oDAAoD;AAAA,IAC9H,2BAA2B,WAAW,iDAAiD,oDAAoD;AAAA,IAC3I,mBAAmB,WAAW,wCAAwC,oDAAoD;AAAA,IAC1H,sBAAsB,WAAW,2CAA2C,oDAAoD;AAAA,IAChI,qBAAqB,WAAW,0CAA0C,oDAAoD;AAAA,IAE9H,kBAAkB,WAAW,uCAAuC,qCAAqC;AAAA,IACzG,wBAAwB,WAAW,8CAA8C,qCAAqC;AAAA,IACtH,gBAAgB,WAAW,qCAAqC,qCAAqC;AAAA,IACrG,mBAAmB,WAAW,wCAAwC,qCAAqC;AAAA,IAC3G,kBAAkB,WAAW,uCAAuC,qCAAqC;AAAA,IAEzG,oBAAoB,WAAW,yCAAyC,mDAAmD;AAAA,IAC3H,0BAA0B,WAAW,gDAAgD,mDAAmD;AAAA,IACxI,oBAAoB,WAAW,yCAAyC,mDAAmD;AAAA,IAE3H,wBAAwB,SAAS,wCAAwC,sCAAsC;AAAA,IAC/G,wBAAwB,SAAS,wCAAwC,sCAAsC;AAAA,IAE/G,kBAAkB,SAAS,2BAA2B,gDAAgD;AAAA,IACtG,uBAAuB,SAAS,iCAAiC,gDAAgD;AAAA,IACjH,yBAAyB,SAAS,oCAAoC,4CAA4C;AAAA,IAElH,kBAAkB,SAAS,iCAAiC,sCAAsC;AAAA,IAClG,uBAAuB,SAAS,uCAAuC,uCAAuC;AAAA,IAC9G,qBAAqB,SAAS,qCAAqC,qCAAqC;AAAA,IACxG,qBAAqB,SAAS,qCAAqC,qCAAqC;AAAA,IAExG,sBAAsB,SAAS,iCAAiC,sCAAsC;AAAA,IACtG,qBAAqB,SAAS,gCAAgC,sCAAsC;AAAA,IAEpG,8BAA8B,SAAS,yCAAyC,+CAA+C;AAAA,IAE/H,iBAAiB,SAAS,sCAAsC,gCAAgC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQhG,wBAAwB,uBAAuB;AAAA,MAC7C,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,WAAW,CAAC,eAAe;AAAA,MAC3B,iBAAiB;AAAA,MACjB,eAAe;AAAA,MACf,QAAQ;AAAA,IACV,CAAC;AAAA,IACD,uBAAuB,uBAAuB;AAAA,MAC5C,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,WAAW,CAAC,eAAe;AAAA,MAC3B,iBAAiB;AAAA,MACjB,eAAe;AAAA,MACf,QAAQ;AAAA,IACV,CAAC;AAAA,IAED,sBAAsB;AAAA,MACpB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,kBAAkB;AAAA,MAChB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,yBAAyB;AAAA,MACvB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,4BAA4B;AAAA,MAC1B;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,wBAAwB;AAAA,MACtB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,yBAAyB;AAAA,MACvB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,uBAAuB;AAAA,MACrB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,8BAA8B;AAAA,MAC5B;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,0BAA0B;AAAA,MACxB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,8BAA8B;AAAA,MAC5B;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQA,uBAAuB;AAAA,MACrB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQA,oBAAoB,uBAAuB;AAAA,MACzC,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,WAAW,CAAC,uBAAuB;AAAA,MACnC,iBAAiB;AAAA,MACjB,eAAe;AAAA,MACf,iBAAiB;AAAA,MACjB,QAAQ;AAAA,IACV,CAAC;AAAA,IAED,0BAA0B,uBAAuB;AAAA,MAC/C,aAAa;AAAA,MACb,eAAe;AAAA,MACf,QAAQ;AAAA,IACV,CAAC;AAAA,IACD,mBAAmB,uBAAuB;AAAA,MACxC,aAAa;AAAA,MACb,eAAe;AAAA,MACf,QAAQ;AAAA,IACV,CAAC;AAAA,IACD,qBAAqB,uBAAuB;AAAA,MAC1C,aAAa;AAAA,MACb,eAAe;AAAA,MACf,QAAQ;AAAA,IACV,CAAC;AAAA,IACD,uBAAuB,uBAAuB;AAAA,MAC5C,aAAa;AAAA,MACb,eAAe;AAAA,MACf,QAAQ;AAAA,IACV,CAAC;AAAA,IACD,wBAAwB,uBAAuB;AAAA,MAC7C,aAAa;AAAA,MACb,eAAe;AAAA,MACf,QAAQ;AAAA,IACV,CAAC;AAAA,IACD,wBAAwB,uBAAuB;AAAA,MAC7C,aAAa;AAAA,MACb,eAAe;AAAA,MACf,QAAQ;AAAA,IACV,CAAC;AAAA,IACD,4BAA4B,uBAAuB;AAAA,MACjD,aAAa;AAAA,MACb,eAAe;AAAA,MACf,QAAQ;AAAA,IACV,CAAC;AAAA,IACD,sBAAsB,uBAAuB;AAAA,MAC3C,aAAa;AAAA,MACb,eAAe;AAAA,MACf,QAAQ;AAAA,IACV,CAAC;AAAA,IACD,sBAAsB,uBAAuB;AAAA,MAC3C,aAAa;AAAA,MACb,eAAe;AAAA,MACf,QAAQ;AAAA,IACV,CAAC;AAAA,IACD,+BAA+B,uBAAuB;AAAA,MACpD,aAAa;AAAA,MACb,eAAe;AAAA,MACf,QAAQ;AAAA,IACV,CAAC;AAAA,EACH;AACF,CAAC;AAED,IAAO,2BAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import Link from "next/link";
|
|
5
|
-
import { useRouter } from "next/navigation";
|
|
6
5
|
import { ArrowLeft } from "lucide-react";
|
|
6
|
+
import { replaceWithPageReload } from "@open-mercato/shared/lib/navigation/pageReload";
|
|
7
7
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
8
8
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
9
9
|
import { Spinner } from "@open-mercato/ui/primitives/spinner";
|
|
@@ -19,7 +19,6 @@ import { extensionPoints } from "@open-mercato/core/modules/staff/extension-poin
|
|
|
19
19
|
import { formatDuration } from "@open-mercato/core/modules/staff/lib/time-tracking/duration";
|
|
20
20
|
function PortalTimeReportDetailPage({ params }) {
|
|
21
21
|
const t = useT();
|
|
22
|
-
const router = useRouter();
|
|
23
22
|
const { auth } = usePortalContext();
|
|
24
23
|
const { user, loading } = auth;
|
|
25
24
|
const [report, setReport] = React.useState(null);
|
|
@@ -27,8 +26,8 @@ function PortalTimeReportDetailPage({ params }) {
|
|
|
27
26
|
const [notFound, setNotFound] = React.useState(false);
|
|
28
27
|
const [error, setError] = React.useState(null);
|
|
29
28
|
React.useEffect(() => {
|
|
30
|
-
if (!loading && !user)
|
|
31
|
-
}, [loading, user,
|
|
29
|
+
if (!loading && !user) replaceWithPageReload(`/${params.orgSlug}/portal/login`);
|
|
30
|
+
}, [loading, user, params.orgSlug]);
|
|
32
31
|
const load = React.useCallback(async () => {
|
|
33
32
|
if (!user) return;
|
|
34
33
|
setIsLoading(true);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../../src/modules/staff/frontend/%5BorgSlug%5D/portal/time-reports/%5Bid%5D/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\n/**\n * EP-50. One closed time report, rendered for the customer it belongs to.\n *\n * The two injection spots are the module's portal extension surface. Their\n * context deliberately carries no money and no staff identity \u2014 a portal widget\n * that needs either must ask for it through its own gated endpoint, the same rule\n * the backoffice spots follow.\n */\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport {
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\n/**\n * EP-50. One closed time report, rendered for the customer it belongs to.\n *\n * The two injection spots are the module's portal extension surface. Their\n * context deliberately carries no money and no staff identity \u2014 a portal widget\n * that needs either must ask for it through its own gated endpoint, the same rule\n * the backoffice spots follow.\n */\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { ArrowLeft } from 'lucide-react'\nimport { replaceWithPageReload } from '@open-mercato/shared/lib/navigation/pageReload'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { EmptyState } from '@open-mercato/ui/primitives/empty-state'\nimport { ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { usePortalContext } from '@open-mercato/ui/portal/PortalContext'\nimport { PortalPageHeader } from '@open-mercato/ui/portal/components/PortalPageHeader'\nimport { PortalCard, PortalCardHeader } from '@open-mercato/ui/portal/components/PortalCard'\nimport { usePortalAppEvent } from '@open-mercato/ui/portal/hooks/usePortalAppEvent'\nimport { extensionPoints } from '@open-mercato/core/modules/staff/extension-points'\nimport { formatDuration } from '@open-mercato/core/modules/staff/lib/time-tracking/duration'\n\ntype Props = { params: { orgSlug: string; id: string } }\n\ntype PortalTimeReportProject = {\n timeProjectId: string | null\n projectName: string | null\n billableMinutes: number\n nonbillableMinutes: number\n}\n\ntype PortalTimeReportDetail = {\n id: string\n reference: string\n title: string\n periodFrom: string\n periodTo: string\n closedAt: string | null\n totalBillableMinutes: number\n totalNonbillableMinutes: number\n projects: PortalTimeReportProject[]\n}\n\nexport default function PortalTimeReportDetailPage({ params }: Props) {\n const t = useT()\n const { auth } = usePortalContext()\n const { user, loading } = auth\n\n const [report, setReport] = React.useState<PortalTimeReportDetail | null>(null)\n const [isLoading, setIsLoading] = React.useState(true)\n const [notFound, setNotFound] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n\n React.useEffect(() => {\n if (!loading && !user) replaceWithPageReload(`/${params.orgSlug}/portal/login`)\n }, [loading, user, params.orgSlug])\n\n const load = React.useCallback(async () => {\n if (!user) return\n setIsLoading(true)\n setError(null)\n setNotFound(false)\n try {\n const response = await apiCall<PortalTimeReportDetail>(\n `/api/staff/portal/time-reports/${encodeURIComponent(params.id)}`,\n )\n if (!response.ok) {\n if (response.status === 404) setNotFound(true)\n else setError(t('staff.portal.timeReports.loadFailed', 'Could not load your time reports.'))\n setReport(null)\n return\n }\n setReport(response.result ?? null)\n } catch {\n setError(t('staff.portal.timeReports.loadFailed', 'Could not load your time reports.'))\n setReport(null)\n } finally {\n setIsLoading(false)\n }\n }, [user, params.id, t])\n\n React.useEffect(() => {\n void load()\n }, [load])\n\n usePortalAppEvent('staff.timesheets.time_report.portal_published', () => {\n void load()\n }, [load])\n\n const injectionContext = React.useMemo(\n () => ({\n orgSlug: params.orgSlug,\n reportId: params.id,\n reference: report?.reference ?? null,\n periodFrom: report?.periodFrom ?? null,\n periodTo: report?.periodTo ?? null,\n resolvedFeatures: auth.resolvedFeatures,\n }),\n [params.orgSlug, params.id, report?.reference, report?.periodFrom, report?.periodTo, auth.resolvedFeatures],\n )\n\n if (loading) {\n return (\n <div className=\"flex items-center justify-center py-20\">\n <Spinner />\n </div>\n )\n }\n if (!user) return null\n\n return (\n <div className=\"space-y-6\">\n <div>\n <Button variant=\"ghost\" asChild>\n <Link href={`/${params.orgSlug}/portal/time-reports`}>\n <ArrowLeft className=\"mr-2 h-4 w-4\" />\n {t('staff.portal.timeReports.backToList', 'All time reports')}\n </Link>\n </Button>\n </div>\n\n <InjectionSpot spotId={extensionPoints.hosts.portalTimeReportBefore.spotId} context={injectionContext} />\n\n {error ? <ErrorMessage label={error} /> : null}\n\n {isLoading ? (\n <div className=\"flex items-center justify-center py-16\">\n <Spinner />\n </div>\n ) : notFound || !report ? (\n <EmptyState\n variant=\"subtle\"\n size=\"lg\"\n title={t('staff.portal.timeReports.notFoundTitle', 'Report not available')}\n description={t(\n 'staff.portal.timeReports.notFoundDescription',\n 'This report either does not exist or is not part of your account.',\n )}\n />\n ) : (\n <>\n <PortalPageHeader\n label={report.reference}\n title={report.title}\n description={`${report.periodFrom} \u2013 ${report.periodTo}`}\n />\n\n <PortalCard>\n <PortalCardHeader title={t('staff.portal.timeReports.totalsTitle', 'Totals')} />\n <dl className=\"grid gap-4 sm:grid-cols-2\">\n <div>\n <dt className=\"text-sm text-muted-foreground\">\n {t('staff.portal.timeReports.billableHours', 'Billable hours')}\n </dt>\n <dd className=\"text-lg font-medium text-foreground\">\n {formatDuration(report.totalBillableMinutes, 'hm')}\n </dd>\n </div>\n <div>\n <dt className=\"text-sm text-muted-foreground\">\n {t('staff.portal.timeReports.nonBillableHours', 'Non-billable hours')}\n </dt>\n <dd className=\"text-lg font-medium text-foreground\">\n {formatDuration(report.totalNonbillableMinutes, 'hm')}\n </dd>\n </div>\n </dl>\n </PortalCard>\n\n <PortalCard>\n <PortalCardHeader title={t('staff.portal.timeReports.projectsTitle', 'By project')} />\n {report.projects.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">\n {t('staff.portal.timeReports.projectsEmpty', 'No project breakdown is available for this period.')}\n </p>\n ) : (\n <div className=\"overflow-x-auto\">\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"border-b border-border text-left text-muted-foreground\">\n <th className=\"py-2 pr-4 font-medium\">\n {t('staff.portal.timeReports.projectColumn', 'Project')}\n </th>\n <th className=\"py-2 pr-4 text-right font-medium\">\n {t('staff.portal.timeReports.billableHours', 'Billable hours')}\n </th>\n <th className=\"py-2 text-right font-medium\">\n {t('staff.portal.timeReports.nonBillableHours', 'Non-billable hours')}\n </th>\n </tr>\n </thead>\n <tbody>\n {report.projects.map((line) => (\n <tr key={line.timeProjectId ?? line.projectName ?? 'unassigned'} className=\"border-b border-border/60\">\n <td className=\"py-2 pr-4 text-foreground\">\n {line.projectName ?? t('staff.portal.timeReports.projectUnassigned', 'Unassigned')}\n </td>\n <td className=\"py-2 pr-4 text-right tabular-nums\">\n {formatDuration(line.billableMinutes, 'hm')}\n </td>\n <td className=\"py-2 text-right tabular-nums\">\n {formatDuration(line.nonbillableMinutes, 'hm')}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )}\n </PortalCard>\n </>\n )}\n\n <InjectionSpot spotId={extensionPoints.hosts.portalTimeReportAfter.spotId} context={injectionContext} />\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import Link from "next/link";
|
|
5
|
-
import {
|
|
5
|
+
import { replaceWithPageReload } from "@open-mercato/shared/lib/navigation/pageReload";
|
|
6
6
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
7
7
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
8
8
|
import { Spinner } from "@open-mercato/ui/primitives/spinner";
|
|
@@ -17,7 +17,6 @@ import { formatDuration } from "@open-mercato/core/modules/staff/lib/time-tracki
|
|
|
17
17
|
const PAGE_SIZE = 20;
|
|
18
18
|
function PortalTimeReportsPage({ params }) {
|
|
19
19
|
const t = useT();
|
|
20
|
-
const router = useRouter();
|
|
21
20
|
const { auth } = usePortalContext();
|
|
22
21
|
const { user, loading } = auth;
|
|
23
22
|
const [reports, setReports] = React.useState([]);
|
|
@@ -26,8 +25,8 @@ function PortalTimeReportsPage({ params }) {
|
|
|
26
25
|
const [isLoading, setIsLoading] = React.useState(true);
|
|
27
26
|
const [error, setError] = React.useState(null);
|
|
28
27
|
React.useEffect(() => {
|
|
29
|
-
if (!loading && !user)
|
|
30
|
-
}, [loading, user,
|
|
28
|
+
if (!loading && !user) replaceWithPageReload(`/${params.orgSlug}/portal/login`);
|
|
29
|
+
}, [loading, user, params.orgSlug]);
|
|
31
30
|
const load = React.useCallback(async () => {
|
|
32
31
|
if (!user) return;
|
|
33
32
|
setIsLoading(true);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/staff/frontend/%5BorgSlug%5D/portal/time-reports/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\n/**\n * EP-50. The customer's own closed time reports.\n *\n * Everything the page shows comes from `/api/staff/portal/time-reports`, which\n * scopes every row to the signed-in portal user's own customer entity inside their\n * own tenant and organization. The page passes no customer id and no organization\n * id of its own \u2014 `orgSlug` is used for hrefs only, exactly as the other portal\n * pages use it.\n */\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport {
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\n/**\n * EP-50. The customer's own closed time reports.\n *\n * Everything the page shows comes from `/api/staff/portal/time-reports`, which\n * scopes every row to the signed-in portal user's own customer entity inside their\n * own tenant and organization. The page passes no customer id and no organization\n * id of its own \u2014 `orgSlug` is used for hrefs only, exactly as the other portal\n * pages use it.\n */\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { replaceWithPageReload } from '@open-mercato/shared/lib/navigation/pageReload'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { usePortalContext } from '@open-mercato/ui/portal/PortalContext'\nimport { PortalPageHeader } from '@open-mercato/ui/portal/components/PortalPageHeader'\nimport { PortalCard } from '@open-mercato/ui/portal/components/PortalCard'\nimport { EmptyState } from '@open-mercato/ui/primitives/empty-state'\nimport { usePortalAppEvent } from '@open-mercato/ui/portal/hooks/usePortalAppEvent'\nimport { formatDuration } from '@open-mercato/core/modules/staff/lib/time-tracking/duration'\n\ntype Props = { params: { orgSlug: string } }\n\ntype PortalTimeReport = {\n id: string\n reference: string\n title: string\n periodFrom: string\n periodTo: string\n closedAt: string | null\n totalBillableMinutes: number\n totalNonbillableMinutes: number\n}\n\ntype PortalTimeReportsResponse = {\n items?: PortalTimeReport[]\n total?: number\n page?: number\n pageSize?: number\n totalPages?: number\n}\n\nconst PAGE_SIZE = 20\n\nexport default function PortalTimeReportsPage({ params }: Props) {\n const t = useT()\n const { auth } = usePortalContext()\n const { user, loading } = auth\n\n const [reports, setReports] = React.useState<PortalTimeReport[]>([])\n const [page, setPage] = React.useState(1)\n const [totalPages, setTotalPages] = React.useState(1)\n const [isLoading, setIsLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n\n React.useEffect(() => {\n if (!loading && !user) replaceWithPageReload(`/${params.orgSlug}/portal/login`)\n }, [loading, user, params.orgSlug])\n\n const load = React.useCallback(async () => {\n if (!user) return\n setIsLoading(true)\n setError(null)\n try {\n const response = await apiCall<PortalTimeReportsResponse>(\n `/api/staff/portal/time-reports?page=${page}&pageSize=${PAGE_SIZE}`,\n )\n if (!response.ok) {\n setError(t('staff.portal.timeReports.loadFailed', 'Could not load your time reports.'))\n setReports([])\n return\n }\n setReports(response.result?.items ?? [])\n setTotalPages(Math.max(1, response.result?.totalPages ?? 1))\n } catch {\n setError(t('staff.portal.timeReports.loadFailed', 'Could not load your time reports.'))\n setReports([])\n } finally {\n setIsLoading(false)\n }\n }, [user, page, t])\n\n React.useEffect(() => {\n void load()\n }, [load])\n\n usePortalAppEvent('staff.timesheets.time_report.portal_published', () => {\n void load()\n }, [load])\n\n if (loading) {\n return (\n <div className=\"flex items-center justify-center py-20\">\n <Spinner />\n </div>\n )\n }\n if (!user) return null\n\n return (\n <div className=\"space-y-6\">\n <PortalPageHeader\n title={t('staff.portal.timeReports.listTitle', 'Time reports')}\n description={t(\n 'staff.portal.timeReports.listDescription',\n 'Hours delivered on your projects, as reported at the close of each period.',\n )}\n />\n\n {error ? <ErrorMessage label={error} /> : null}\n\n {isLoading ? (\n <div className=\"flex items-center justify-center py-16\">\n <Spinner />\n </div>\n ) : reports.length === 0 ? (\n <EmptyState\n variant=\"subtle\"\n size=\"lg\"\n title={t('staff.portal.timeReports.emptyTitle', 'No time reports yet')}\n description={t(\n 'staff.portal.timeReports.emptyDescription',\n 'A report appears here once the period it covers has been closed.',\n )}\n />\n ) : (\n <div className=\"space-y-3\">\n {reports.map((report) => (\n <PortalCard key={report.id}>\n <div className=\"flex flex-wrap items-start justify-between gap-4\">\n <div className=\"min-w-0 space-y-1\">\n <Link\n href={`/${params.orgSlug}/portal/time-reports/${report.id}`}\n className=\"text-base font-medium text-foreground hover:underline\"\n >\n {report.title}\n </Link>\n <p className=\"text-sm text-muted-foreground\">\n {report.reference} \u00B7 {report.periodFrom} \u2013 {report.periodTo}\n </p>\n </div>\n <div className=\"text-right\">\n <p className=\"text-base font-medium text-foreground\">\n {formatDuration(report.totalBillableMinutes, 'hm')}\n </p>\n <p className=\"text-sm text-muted-foreground\">\n {t('staff.portal.timeReports.billableHours', 'Billable hours')}\n </p>\n </div>\n </div>\n </PortalCard>\n ))}\n </div>\n )}\n\n {totalPages > 1 ? (\n <div className=\"flex items-center justify-between gap-3\">\n <Button variant=\"outline\" disabled={page <= 1} onClick={() => setPage((current) => Math.max(1, current - 1))}>\n {t('staff.portal.timeReports.previous', 'Previous')}\n </Button>\n <span className=\"text-sm text-muted-foreground\">\n {t('staff.portal.timeReports.pageLabel', 'Page')} {page} / {totalPages}\n </span>\n <Button\n variant=\"outline\"\n disabled={page >= totalPages}\n onClick={() => setPage((current) => Math.min(totalPages, current + 1))}\n >\n {t('staff.portal.timeReports.next', 'Next')}\n </Button>\n </div>\n ) : null}\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -4,6 +4,7 @@ import * as React from "react";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { ChevronDown, ChevronRight } from "lucide-react";
|
|
6
6
|
import { Badge } from "@open-mercato/ui/primitives/badge";
|
|
7
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@open-mercato/ui/primitives/table";
|
|
7
8
|
import { formatCurrency } from "@open-mercato/ui/utils/format";
|
|
8
9
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
9
10
|
import { InjectionSpot } from "@open-mercato/ui/backend/injection/InjectionSpot";
|
|
@@ -115,14 +116,14 @@ function ReportSheetGroup({ group, showRates, money }) {
|
|
|
115
116
|
)
|
|
116
117
|
] })
|
|
117
118
|
] }),
|
|
118
|
-
/* @__PURE__ */ jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxs(
|
|
119
|
-
/* @__PURE__ */ jsx(
|
|
120
|
-
/* @__PURE__ */ jsx(
|
|
121
|
-
/* @__PURE__ */ jsx(
|
|
122
|
-
showRates ? /* @__PURE__ */ jsx(
|
|
123
|
-
/* @__PURE__ */ jsx(
|
|
119
|
+
/* @__PURE__ */ jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxs(Table, { children: [
|
|
120
|
+
/* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { className: "text-xs uppercase tracking-wide text-muted-foreground", children: [
|
|
121
|
+
/* @__PURE__ */ jsx(TableHead, { className: "px-6 py-2 text-left", children: t("staff.time_tracking.reports.sheet.lineLabel", "Line") }),
|
|
122
|
+
/* @__PURE__ */ jsx(TableHead, { className: "px-3 py-2 text-right", children: t("staff.time_tracking.reports.sheet.time", "Time") }),
|
|
123
|
+
showRates ? /* @__PURE__ */ jsx(TableHead, { className: "px-3 py-2 text-right", children: t("staff.time_tracking.reports.sheet.rate", "Rate") }) : null,
|
|
124
|
+
/* @__PURE__ */ jsx(TableHead, { className: "px-6 py-2 text-right", children: t("staff.time_tracking.reports.sheet.amount", "Amount") })
|
|
124
125
|
] }) }),
|
|
125
|
-
/* @__PURE__ */ jsx(
|
|
126
|
+
/* @__PURE__ */ jsx(TableBody, { children: group.lines.map((line) => /* @__PURE__ */ jsx(
|
|
126
127
|
ReportSheetLine,
|
|
127
128
|
{
|
|
128
129
|
line,
|
|
@@ -142,8 +143,8 @@ function ReportSheetLine({ line, depth, isNonBillable, showRates, money }) {
|
|
|
142
143
|
const hasChildren = line.children.length > 0;
|
|
143
144
|
const columns = showRates ? 4 : 3;
|
|
144
145
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
145
|
-
/* @__PURE__ */ jsxs(
|
|
146
|
-
/* @__PURE__ */ jsx(
|
|
146
|
+
/* @__PURE__ */ jsxs(TableRow, { children: [
|
|
147
|
+
/* @__PURE__ */ jsx(TableCell, { className: "px-6 py-2", style: { paddingLeft: `${1.5 + depth * 1.25}rem` }, children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
147
148
|
hasChildren ? /* @__PURE__ */ jsx(
|
|
148
149
|
"button",
|
|
149
150
|
{
|
|
@@ -158,9 +159,9 @@ function ReportSheetLine({ line, depth, isNonBillable, showRates, money }) {
|
|
|
158
159
|
/* @__PURE__ */ jsx("span", { children: line.label }),
|
|
159
160
|
line.hasOverride ? /* @__PURE__ */ jsx(Badge, { variant: "secondary", children: t("staff.time_tracking.reports.sheet.overrideBadge", "agreed rate") }) : null
|
|
160
161
|
] }) }),
|
|
161
|
-
/* @__PURE__ */ jsx(
|
|
162
|
-
showRates ? /* @__PURE__ */ jsx(
|
|
163
|
-
/* @__PURE__ */ jsx(
|
|
162
|
+
/* @__PURE__ */ jsx(TableCell, { className: "px-3 py-2 text-right font-mono tabular-nums", children: formatReportMinutes(line.minutes) }),
|
|
163
|
+
showRates ? /* @__PURE__ */ jsx(TableCell, { className: "px-3 py-2 text-right font-mono tabular-nums", children: isNonBillable ? EM_DASH : money(line.rate) }) : null,
|
|
164
|
+
/* @__PURE__ */ jsx(TableCell, { className: "px-6 py-2 text-right font-mono tabular-nums", children: isNonBillable ? EM_DASH : money(line.amount) })
|
|
164
165
|
] }),
|
|
165
166
|
expanded ? line.children.map((child) => /* @__PURE__ */ jsx(
|
|
166
167
|
ReportSheetLine,
|
|
@@ -173,7 +174,7 @@ function ReportSheetLine({ line, depth, isNonBillable, showRates, money }) {
|
|
|
173
174
|
},
|
|
174
175
|
child.key
|
|
175
176
|
)) : null,
|
|
176
|
-
expanded && line.children.length === 0 ? /* @__PURE__ */ jsx(
|
|
177
|
+
expanded && line.children.length === 0 ? /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, { colSpan: columns, className: "px-6 py-2 text-xs text-muted-foreground", children: t("staff.time_tracking.reports.sheet.noSubtasks", "No subtasks") }) }) : null
|
|
177
178
|
] });
|
|
178
179
|
}
|
|
179
180
|
const reportSheetPropsSchema = z.object({
|