@open-mercato/core 0.8.1-develop.7275.1.f772b944d4 → 0.8.1-develop.7295.1.d0e0e33014
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 +155 -15
- 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 +238 -17
- 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/lib/time-tracking-ui/ReportSheet.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\n/**\n * Screen 14 \u2014 the sheet the client sees.\n *\n * It is deliberately a dumb renderer: every number arrives already computed by\n * `POST .../reports/preview` or `GET .../reports/[id]/sheet`, which own D-7. The\n * component adds nothing up, because a client-facing total computed in two\n * places is a client-facing total that will eventually disagree with itself.\n *\n * The four mockup notes it implements:\n *\n * 1. **Non-billable time gets its own group at zero** rather than being\n * omitted. The client sees the full effort and the total stays unambiguous\n * (US-G2). A dash, never `0,00`, in the rate and amount cells \u2014 a zero there\n * would read as free work rather than as work outside the invoice.\n * 2. **A line carrying a rate override is marked** and prints the rate actually\n * applied, not the project default (US-F2). Two lines with the same hours\n * and different amounts look like a bug otherwise.\n * 3. **Child tasks roll up into their parent line and expand underneath**\n * (D-2), so the sheet reads at the level the client contracted for while the\n * detail stays one click away.\n * 4. **The grand total spells out the rounding rule** it was produced under, so\n * an amount with an odd grosz ending is explained on the page rather than in\n * an email.\n */\n\nimport * as React from 'react'\nimport { z } from 'zod'\nimport { ChevronDown, ChevronRight } from 'lucide-react'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { formatCurrency } from '@open-mercato/ui/utils/format'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'\nimport { extensionPoints } from '@open-mercato/core/modules/staff/extension-points'\nimport { registerComponent } from '@open-mercato/shared/modules/widgets/component-registry'\nimport { useRegisteredComponent } from '@open-mercato/ui/backend/injection/useRegisteredComponent'\nimport { opaqueProp } from '../time-tracking/componentContracts'\nimport { formatReportMinutes } from '../timesheets-reports/reportTotals'\nimport type { PreviewGroup, PreviewLine } from '../timesheets-reports/reportConfigData'\n\nexport type ReportSheetProps = {\n /**\n * Identifies the rendered report to the sheet's injection spots. Optional so\n * the preview host, which renders a sheet no report row exists for yet, keeps\n * working unchanged.\n */\n reportId?: string | null\n reference: string\n customerName: string\n periodLabel: string\n issuedByLabel: string | null\n issuedAtLabel: string | null\n currencyCode: string | null\n showRates: boolean\n groups: readonly PreviewGroup[]\n totals: {\n billableMinutes: number\n nonbillableMinutes: number\n totalAmount: number | null\n }\n roundingLabel: string\n}\n\nconst EM_DASH = '\u2014'\n\nconst REPORT_ENTITY_ID = 'staff:staff_time_report'\n\nfunction useMoney(currencyCode: string | null) {\n return React.useCallback(\n (amount: number | null | undefined) => {\n if (amount === null || amount === undefined) return EM_DASH\n return formatCurrency(amount, currencyCode ?? undefined) ?? EM_DASH\n },\n [currencyCode],\n )\n}\n\nfunction DefaultReportSheet({\n reportId = null,\n reference,\n customerName,\n periodLabel,\n issuedByLabel,\n issuedAtLabel,\n currencyCode,\n showRates,\n groups,\n totals,\n roundingLabel,\n}: ReportSheetProps) {\n const t = useT()\n const money = useMoney(currencyCode)\n const sheetInjectionContext = React.useMemo(\n () => ({ entityId: REPORT_ENTITY_ID, recordId: reportId, reportId, reference, periodLabel, currencyCode }),\n [currencyCode, periodLabel, reference, reportId],\n )\n\n return (\n <section className=\"rounded-lg border border-border bg-card\">\n <header className=\"flex flex-wrap items-start justify-between gap-4 border-b border-border p-6\">\n <div className=\"flex flex-col gap-1\">\n <span className=\"text-xs font-medium uppercase tracking-wide text-muted-foreground\">\n {t('staff.time_tracking.reports.sheet.overline', 'Time and cost statement')}\n </span>\n <h2 className=\"text-lg font-semibold\">{customerName}</h2>\n <span className=\"text-sm text-muted-foreground\">{periodLabel}</span>\n </div>\n <div className=\"flex flex-col items-end gap-1 text-right\">\n {issuedByLabel ? (\n <>\n <span className=\"text-xs text-muted-foreground\">\n {t('staff.time_tracking.reports.sheet.issuedBy', 'Issued by')}\n </span>\n <span className=\"text-sm font-medium\">{issuedByLabel}</span>\n </>\n ) : null}\n {issuedAtLabel ? <span className=\"text-xs text-muted-foreground\">{issuedAtLabel}</span> : null}\n <span className=\"font-mono text-xs text-muted-foreground\">{reference}</span>\n </div>\n </header>\n\n <InjectionSpot\n spotId={extensionPoints.hosts.reportSheetBeforeLines.spotId}\n context={sheetInjectionContext}\n data={groups}\n />\n\n {groups.length === 0 ? (\n <p className=\"p-6 text-sm text-muted-foreground\">\n {t('staff.time_tracking.reports.sheet.empty', 'No time was logged in this period for the selected projects.')}\n </p>\n ) : (\n groups.map((group) => (\n <ReportSheetGroup\n key={group.key}\n group={group}\n showRates={showRates}\n money={money}\n />\n ))\n )}\n\n <footer className=\"flex flex-wrap items-center justify-between gap-4 border-t border-border p-6\">\n <div className=\"flex flex-col gap-1\">\n <span className=\"text-sm font-semibold\">\n {t('staff.time_tracking.reports.sheet.total', 'Total to invoice')}\n </span>\n <span className=\"text-xs text-muted-foreground\">\n {t(\n 'staff.time_tracking.reports.sheet.totalHint',\n '{billable} billable \u00B7 {nonbillable} non-billable \u00B7 {rounding}',\n )\n .replace('{billable}', formatReportMinutes(totals.billableMinutes))\n .replace('{nonbillable}', formatReportMinutes(totals.nonbillableMinutes))\n .replace('{rounding}', roundingLabel)}\n </span>\n </div>\n <span className=\"font-mono text-xl font-semibold tabular-nums\">{money(totals.totalAmount)}</span>\n </footer>\n\n <InjectionSpot\n spotId={extensionPoints.hosts.reportSheetAfterTotals.spotId}\n context={sheetInjectionContext}\n data={totals}\n />\n </section>\n )\n}\n\ntype GroupProps = {\n group: PreviewGroup\n showRates: boolean\n money: (amount: number | null | undefined) => string\n}\n\nfunction ReportSheetGroup({ group, showRates, money }: GroupProps) {\n const t = useT()\n const isNonBillable = group.kind === 'nonbillable'\n\n return (\n <div className=\"border-b border-border last:border-b-0\">\n <div className=\"flex flex-wrap items-center justify-between gap-3 bg-muted/40 px-6 py-3\">\n <div className=\"flex flex-col\">\n <span className=\"text-sm font-semibold\">{group.label}</span>\n <span className=\"text-xs text-muted-foreground\">\n {isNonBillable\n ? t('staff.time_tracking.reports.sheet.nonbillableHint', 'Shown for information, outside the invoice')\n : showRates && group.rate !== null\n ? `${money(group.rate)}/h`\n : ''}\n </span>\n </div>\n <div className=\"flex items-center gap-4\">\n <span className=\"font-mono text-sm tabular-nums\">{formatReportMinutes(group.minutes)}</span>\n <span\n className={`font-mono text-sm tabular-nums ${isNonBillable ? 'text-muted-foreground' : 'font-semibold'}`}\n >\n {isNonBillable ? money(0) : money(group.amount)}\n </span>\n </div>\n </div>\n\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-6 py-2 text-left font-medium\">\n {t('staff.time_tracking.reports.sheet.lineLabel', 'Line')}\n </th>\n <th className=\"px-3 py-2 text-right font-medium\">\n {t('staff.time_tracking.reports.sheet.time', 'Time')}\n </th>\n {showRates ? (\n <th className=\"px-3 py-2 text-right font-medium\">\n {t('staff.time_tracking.reports.sheet.rate', 'Rate')}\n </th>\n ) : null}\n <th className=\"px-6 py-2 text-right font-medium\">\n {t('staff.time_tracking.reports.sheet.amount', 'Amount')}\n </th>\n </tr>\n </thead>\n <tbody>\n {group.lines.map((line) => (\n <ReportSheetLine\n key={line.key}\n line={line}\n depth={0}\n isNonBillable={isNonBillable}\n showRates={showRates}\n money={money}\n />\n ))}\n </tbody>\n </table>\n </div>\n </div>\n )\n}\n\ntype LineProps = {\n line: PreviewLine\n depth: number\n isNonBillable: boolean\n showRates: boolean\n money: (amount: number | null | undefined) => string\n}\n\nfunction ReportSheetLine({ line, depth, isNonBillable, showRates, money }: LineProps) {\n const t = useT()\n const [expanded, setExpanded] = React.useState(false)\n const hasChildren = line.children.length > 0\n const columns = showRates ? 4 : 3\n\n return (\n <>\n <tr className=\"border-b border-border/60 last:border-b-0\">\n <td className=\"px-6 py-2\" style={{ paddingLeft: `${1.5 + depth * 1.25}rem` }}>\n <div className=\"flex items-center gap-2\">\n {hasChildren ? (\n <button\n type=\"button\"\n className=\"inline-flex items-center text-muted-foreground hover:text-foreground\"\n onClick={() => setExpanded((current) => !current)}\n aria-expanded={expanded}\n aria-label={\n expanded\n ? t('staff.time_tracking.reports.sheet.collapse', 'Hide subtasks')\n : t('staff.time_tracking.reports.sheet.expand', 'Show subtasks')\n }\n >\n {expanded ? (\n <ChevronDown className=\"size-4\" aria-hidden=\"true\" />\n ) : (\n <ChevronRight className=\"size-4\" aria-hidden=\"true\" />\n )}\n </button>\n ) : null}\n <span>{line.label}</span>\n {line.hasOverride ? (\n <Badge variant=\"secondary\">\n {t('staff.time_tracking.reports.sheet.overrideBadge', 'agreed rate')}\n </Badge>\n ) : null}\n </div>\n </td>\n <td className=\"px-3 py-2 text-right font-mono tabular-nums\">{formatReportMinutes(line.minutes)}</td>\n {showRates ? (\n <td className=\"px-3 py-2 text-right font-mono tabular-nums\">\n {isNonBillable ? EM_DASH : money(line.rate)}\n </td>\n ) : null}\n <td className=\"px-6 py-2 text-right font-mono tabular-nums\">\n {isNonBillable ? EM_DASH : money(line.amount)}\n </td>\n </tr>\n {expanded\n ? line.children.map((child) => (\n <ReportSheetLine\n key={child.key}\n line={child}\n depth={depth + 1}\n isNonBillable={isNonBillable}\n showRates={showRates}\n money={money}\n />\n ))\n : null}\n {expanded && line.children.length === 0 ? (\n <tr>\n <td colSpan={columns} className=\"px-6 py-2 text-xs text-muted-foreground\">\n {t('staff.time_tracking.reports.sheet.noSubtasks', 'No subtasks')}\n </td>\n </tr>\n ) : null}\n </>\n )\n}\n\nconst reportSheetPropsSchema: z.ZodType<ReportSheetProps> = z.object({\n reportId: z.string().nullable().optional(),\n reference: z.string(),\n customerName: z.string(),\n periodLabel: z.string(),\n issuedByLabel: z.string().nullable(),\n issuedAtLabel: z.string().nullable(),\n currencyCode: z.string().nullable(),\n showRates: z.boolean(),\n groups: z.array(opaqueProp<PreviewGroup>()),\n totals: z.object({\n billableMinutes: z.number(),\n nonbillableMinutes: z.number(),\n totalAmount: z.number().nullable(),\n }),\n roundingLabel: z.string(),\n})\n\nregisterComponent<ReportSheetProps>({\n id: extensionPoints.hosts.reportSheetComponent.componentId,\n component: DefaultReportSheet,\n metadata: {\n module: 'staff',\n description: 'Client-facing time and cost statement for one report.',\n propsSchema: reportSheetPropsSchema,\n },\n})\n\nexport function ReportSheet(props: ReportSheetProps) {\n const Resolved = useRegisteredComponent<ReportSheetProps>(\n extensionPoints.hosts.reportSheetComponent.componentId,\n DefaultReportSheet,\n )\n return <Resolved {...props} />\n}\n\nexport default ReportSheet\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\n/**\n * Screen 14 \u2014 the sheet the client sees.\n *\n * It is deliberately a dumb renderer: every number arrives already computed by\n * `POST .../reports/preview` or `GET .../reports/[id]/sheet`, which own D-7. The\n * component adds nothing up, because a client-facing total computed in two\n * places is a client-facing total that will eventually disagree with itself.\n *\n * The four mockup notes it implements:\n *\n * 1. **Non-billable time gets its own group at zero** rather than being\n * omitted. The client sees the full effort and the total stays unambiguous\n * (US-G2). A dash, never `0,00`, in the rate and amount cells \u2014 a zero there\n * would read as free work rather than as work outside the invoice.\n * 2. **A line carrying a rate override is marked** and prints the rate actually\n * applied, not the project default (US-F2). Two lines with the same hours\n * and different amounts look like a bug otherwise.\n * 3. **Child tasks roll up into their parent line and expand underneath**\n * (D-2), so the sheet reads at the level the client contracted for while the\n * detail stays one click away.\n * 4. **The grand total spells out the rounding rule** it was produced under, so\n * an amount with an odd grosz ending is explained on the page rather than in\n * an email.\n */\n\nimport * as React from 'react'\nimport { z } from 'zod'\nimport { ChevronDown, ChevronRight } from 'lucide-react'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@open-mercato/ui/primitives/table'\nimport { formatCurrency } from '@open-mercato/ui/utils/format'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'\nimport { extensionPoints } from '@open-mercato/core/modules/staff/extension-points'\nimport { registerComponent } from '@open-mercato/shared/modules/widgets/component-registry'\nimport { useRegisteredComponent } from '@open-mercato/ui/backend/injection/useRegisteredComponent'\nimport { opaqueProp } from '../time-tracking/componentContracts'\nimport { formatReportMinutes } from '../timesheets-reports/reportTotals'\nimport type { PreviewGroup, PreviewLine } from '../timesheets-reports/reportConfigData'\n\nexport type ReportSheetProps = {\n /**\n * Identifies the rendered report to the sheet's injection spots. Optional so\n * the preview host, which renders a sheet no report row exists for yet, keeps\n * working unchanged.\n */\n reportId?: string | null\n reference: string\n customerName: string\n periodLabel: string\n issuedByLabel: string | null\n issuedAtLabel: string | null\n currencyCode: string | null\n showRates: boolean\n groups: readonly PreviewGroup[]\n totals: {\n billableMinutes: number\n nonbillableMinutes: number\n totalAmount: number | null\n }\n roundingLabel: string\n}\n\nconst EM_DASH = '\u2014'\n\nconst REPORT_ENTITY_ID = 'staff:staff_time_report'\n\nfunction useMoney(currencyCode: string | null) {\n return React.useCallback(\n (amount: number | null | undefined) => {\n if (amount === null || amount === undefined) return EM_DASH\n return formatCurrency(amount, currencyCode ?? undefined) ?? EM_DASH\n },\n [currencyCode],\n )\n}\n\nfunction DefaultReportSheet({\n reportId = null,\n reference,\n customerName,\n periodLabel,\n issuedByLabel,\n issuedAtLabel,\n currencyCode,\n showRates,\n groups,\n totals,\n roundingLabel,\n}: ReportSheetProps) {\n const t = useT()\n const money = useMoney(currencyCode)\n const sheetInjectionContext = React.useMemo(\n () => ({ entityId: REPORT_ENTITY_ID, recordId: reportId, reportId, reference, periodLabel, currencyCode }),\n [currencyCode, periodLabel, reference, reportId],\n )\n\n return (\n <section className=\"rounded-lg border border-border bg-card\">\n <header className=\"flex flex-wrap items-start justify-between gap-4 border-b border-border p-6\">\n <div className=\"flex flex-col gap-1\">\n <span className=\"text-xs font-medium uppercase tracking-wide text-muted-foreground\">\n {t('staff.time_tracking.reports.sheet.overline', 'Time and cost statement')}\n </span>\n <h2 className=\"text-lg font-semibold\">{customerName}</h2>\n <span className=\"text-sm text-muted-foreground\">{periodLabel}</span>\n </div>\n <div className=\"flex flex-col items-end gap-1 text-right\">\n {issuedByLabel ? (\n <>\n <span className=\"text-xs text-muted-foreground\">\n {t('staff.time_tracking.reports.sheet.issuedBy', 'Issued by')}\n </span>\n <span className=\"text-sm font-medium\">{issuedByLabel}</span>\n </>\n ) : null}\n {issuedAtLabel ? <span className=\"text-xs text-muted-foreground\">{issuedAtLabel}</span> : null}\n <span className=\"font-mono text-xs text-muted-foreground\">{reference}</span>\n </div>\n </header>\n\n <InjectionSpot\n spotId={extensionPoints.hosts.reportSheetBeforeLines.spotId}\n context={sheetInjectionContext}\n data={groups}\n />\n\n {groups.length === 0 ? (\n <p className=\"p-6 text-sm text-muted-foreground\">\n {t('staff.time_tracking.reports.sheet.empty', 'No time was logged in this period for the selected projects.')}\n </p>\n ) : (\n groups.map((group) => (\n <ReportSheetGroup\n key={group.key}\n group={group}\n showRates={showRates}\n money={money}\n />\n ))\n )}\n\n <footer className=\"flex flex-wrap items-center justify-between gap-4 border-t border-border p-6\">\n <div className=\"flex flex-col gap-1\">\n <span className=\"text-sm font-semibold\">\n {t('staff.time_tracking.reports.sheet.total', 'Total to invoice')}\n </span>\n <span className=\"text-xs text-muted-foreground\">\n {t(\n 'staff.time_tracking.reports.sheet.totalHint',\n '{billable} billable \u00B7 {nonbillable} non-billable \u00B7 {rounding}',\n )\n .replace('{billable}', formatReportMinutes(totals.billableMinutes))\n .replace('{nonbillable}', formatReportMinutes(totals.nonbillableMinutes))\n .replace('{rounding}', roundingLabel)}\n </span>\n </div>\n <span className=\"font-mono text-xl font-semibold tabular-nums\">{money(totals.totalAmount)}</span>\n </footer>\n\n <InjectionSpot\n spotId={extensionPoints.hosts.reportSheetAfterTotals.spotId}\n context={sheetInjectionContext}\n data={totals}\n />\n </section>\n )\n}\n\ntype GroupProps = {\n group: PreviewGroup\n showRates: boolean\n money: (amount: number | null | undefined) => string\n}\n\nfunction ReportSheetGroup({ group, showRates, money }: GroupProps) {\n const t = useT()\n const isNonBillable = group.kind === 'nonbillable'\n\n return (\n <div className=\"border-b border-border last:border-b-0\">\n <div className=\"flex flex-wrap items-center justify-between gap-3 bg-muted/40 px-6 py-3\">\n <div className=\"flex flex-col\">\n <span className=\"text-sm font-semibold\">{group.label}</span>\n <span className=\"text-xs text-muted-foreground\">\n {isNonBillable\n ? t('staff.time_tracking.reports.sheet.nonbillableHint', 'Shown for information, outside the invoice')\n : showRates && group.rate !== null\n ? `${money(group.rate)}/h`\n : ''}\n </span>\n </div>\n <div className=\"flex items-center gap-4\">\n <span className=\"font-mono text-sm tabular-nums\">{formatReportMinutes(group.minutes)}</span>\n <span\n className={`font-mono text-sm tabular-nums ${isNonBillable ? 'text-muted-foreground' : 'font-semibold'}`}\n >\n {isNonBillable ? money(0) : money(group.amount)}\n </span>\n </div>\n </div>\n\n <div className=\"overflow-x-auto\">\n <Table>\n <TableHeader>\n <TableRow className=\"text-xs uppercase tracking-wide text-muted-foreground\">\n <TableHead className=\"px-6 py-2 text-left\">\n {t('staff.time_tracking.reports.sheet.lineLabel', 'Line')}\n </TableHead>\n <TableHead className=\"px-3 py-2 text-right\">\n {t('staff.time_tracking.reports.sheet.time', 'Time')}\n </TableHead>\n {showRates ? (\n <TableHead className=\"px-3 py-2 text-right\">\n {t('staff.time_tracking.reports.sheet.rate', 'Rate')}\n </TableHead>\n ) : null}\n <TableHead className=\"px-6 py-2 text-right\">\n {t('staff.time_tracking.reports.sheet.amount', 'Amount')}\n </TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {group.lines.map((line) => (\n <ReportSheetLine\n key={line.key}\n line={line}\n depth={0}\n isNonBillable={isNonBillable}\n showRates={showRates}\n money={money}\n />\n ))}\n </TableBody>\n </Table>\n </div>\n </div>\n )\n}\n\ntype LineProps = {\n line: PreviewLine\n depth: number\n isNonBillable: boolean\n showRates: boolean\n money: (amount: number | null | undefined) => string\n}\n\nfunction ReportSheetLine({ line, depth, isNonBillable, showRates, money }: LineProps) {\n const t = useT()\n const [expanded, setExpanded] = React.useState(false)\n const hasChildren = line.children.length > 0\n const columns = showRates ? 4 : 3\n\n return (\n <>\n <TableRow>\n <TableCell className=\"px-6 py-2\" style={{ paddingLeft: `${1.5 + depth * 1.25}rem` }}>\n <div className=\"flex items-center gap-2\">\n {hasChildren ? (\n <button\n type=\"button\"\n className=\"inline-flex items-center text-muted-foreground hover:text-foreground\"\n onClick={() => setExpanded((current) => !current)}\n aria-expanded={expanded}\n aria-label={\n expanded\n ? t('staff.time_tracking.reports.sheet.collapse', 'Hide subtasks')\n : t('staff.time_tracking.reports.sheet.expand', 'Show subtasks')\n }\n >\n {expanded ? (\n <ChevronDown className=\"size-4\" aria-hidden=\"true\" />\n ) : (\n <ChevronRight className=\"size-4\" aria-hidden=\"true\" />\n )}\n </button>\n ) : null}\n <span>{line.label}</span>\n {line.hasOverride ? (\n <Badge variant=\"secondary\">\n {t('staff.time_tracking.reports.sheet.overrideBadge', 'agreed rate')}\n </Badge>\n ) : null}\n </div>\n </TableCell>\n <TableCell className=\"px-3 py-2 text-right font-mono tabular-nums\">\n {formatReportMinutes(line.minutes)}\n </TableCell>\n {showRates ? (\n <TableCell className=\"px-3 py-2 text-right font-mono tabular-nums\">\n {isNonBillable ? EM_DASH : money(line.rate)}\n </TableCell>\n ) : null}\n <TableCell className=\"px-6 py-2 text-right font-mono tabular-nums\">\n {isNonBillable ? EM_DASH : money(line.amount)}\n </TableCell>\n </TableRow>\n {expanded\n ? line.children.map((child) => (\n <ReportSheetLine\n key={child.key}\n line={child}\n depth={depth + 1}\n isNonBillable={isNonBillable}\n showRates={showRates}\n money={money}\n />\n ))\n : null}\n {expanded && line.children.length === 0 ? (\n <TableRow>\n <TableCell colSpan={columns} className=\"px-6 py-2 text-xs text-muted-foreground\">\n {t('staff.time_tracking.reports.sheet.noSubtasks', 'No subtasks')}\n </TableCell>\n </TableRow>\n ) : null}\n </>\n )\n}\n\nconst reportSheetPropsSchema: z.ZodType<ReportSheetProps> = z.object({\n reportId: z.string().nullable().optional(),\n reference: z.string(),\n customerName: z.string(),\n periodLabel: z.string(),\n issuedByLabel: z.string().nullable(),\n issuedAtLabel: z.string().nullable(),\n currencyCode: z.string().nullable(),\n showRates: z.boolean(),\n groups: z.array(opaqueProp<PreviewGroup>()),\n totals: z.object({\n billableMinutes: z.number(),\n nonbillableMinutes: z.number(),\n totalAmount: z.number().nullable(),\n }),\n roundingLabel: z.string(),\n})\n\nregisterComponent<ReportSheetProps>({\n id: extensionPoints.hosts.reportSheetComponent.componentId,\n component: DefaultReportSheet,\n metadata: {\n module: 'staff',\n description: 'Client-facing time and cost statement for one report.',\n propsSchema: reportSheetPropsSchema,\n },\n})\n\nexport function ReportSheet(props: ReportSheetProps) {\n const Resolved = useRegisteredComponent<ReportSheetProps>(\n extensionPoints.hosts.reportSheetComponent.componentId,\n DefaultReportSheet,\n )\n return <Resolved {...props} />\n}\n\nexport default ReportSheet\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -6,9 +6,11 @@ import { Badge } from "@open-mercato/ui/primitives/badge";
|
|
|
6
6
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
7
7
|
import { EmptyState } from "@open-mercato/ui/primitives/empty-state";
|
|
8
8
|
import { Spinner } from "@open-mercato/ui/primitives/spinner";
|
|
9
|
+
import { DataTable } from "@open-mercato/ui/backend/DataTable";
|
|
9
10
|
import { ErrorMessage, LoadingMessage } from "@open-mercato/ui/backend/detail";
|
|
10
11
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
11
12
|
import { useOrganizationScopeVersion } from "@open-mercato/shared/lib/frontend/useOrganizationScope";
|
|
13
|
+
import { extensionPoints } from "@open-mercato/core/modules/staff/extension-points";
|
|
12
14
|
import { sumTaskLoggedMinutes } from "../timesheets-tasks/taskHoursTotals.js";
|
|
13
15
|
import {
|
|
14
16
|
BOARD_PAGE_SIZE,
|
|
@@ -85,6 +87,55 @@ function TaskListView({
|
|
|
85
87
|
React.useEffect(() => {
|
|
86
88
|
onSummaryChange?.(summary);
|
|
87
89
|
}, [onSummaryChange, summary]);
|
|
90
|
+
const columns = React.useMemo(
|
|
91
|
+
() => [
|
|
92
|
+
{
|
|
93
|
+
accessorKey: "reference",
|
|
94
|
+
header: t("staff.time_tracking.board.list.columns.reference", "Ref."),
|
|
95
|
+
cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "text-xs tabular-nums text-muted-foreground", children: row.original.reference ?? "\u2014" })
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
accessorKey: "title",
|
|
99
|
+
header: t("staff.time_tracking.board.list.columns.title", "Task"),
|
|
100
|
+
cell: ({ row }) => /* @__PURE__ */ jsx("span", { "data-task-row": row.original.id, children: /* @__PURE__ */ jsx(
|
|
101
|
+
Button,
|
|
102
|
+
{
|
|
103
|
+
type: "button",
|
|
104
|
+
variant: "link",
|
|
105
|
+
size: "2xs",
|
|
106
|
+
onClick: () => onOpenTask(row.original.id),
|
|
107
|
+
className: "h-auto px-0 text-left text-sm font-semibold text-foreground",
|
|
108
|
+
children: row.original.title
|
|
109
|
+
}
|
|
110
|
+
) })
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: "status",
|
|
114
|
+
header: t("staff.time_tracking.board.list.columns.status", "Status"),
|
|
115
|
+
cell: ({ row }) => row.original.taskStatusId ? statusNames.get(row.original.taskStatusId) ?? "\u2014" : "\u2014"
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: "assignee",
|
|
119
|
+
header: t("staff.time_tracking.board.list.columns.assignee", "Assignee"),
|
|
120
|
+
cell: ({ row }) => row.original.assigneeStaffMemberId ? assigneeNames.data?.get(row.original.assigneeStaffMemberId) ?? t("staff.time_tracking.board.card.unassigned", "Unassigned") : t("staff.time_tracking.board.card.unassigned", "Unassigned")
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: "tags",
|
|
124
|
+
header: t("staff.time_tracking.board.list.columns.tags", "Tags"),
|
|
125
|
+
cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "flex flex-wrap gap-1", children: row.original.tagIds.map((tagId) => {
|
|
126
|
+
const label = tagLabels.data?.get(tagId);
|
|
127
|
+
if (!label) return null;
|
|
128
|
+
return /* @__PURE__ */ jsx(Badge, { variant: "neutral", size: "sm", children: label }, tagId);
|
|
129
|
+
}) })
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: "hours",
|
|
133
|
+
header: () => /* @__PURE__ */ jsx("span", { className: "block text-right", children: t("staff.time_tracking.board.list.columns.hours", "Logged") }),
|
|
134
|
+
cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "block text-right font-semibold tabular-nums text-foreground", children: formatBoardMinutes(row.original.loggedMinutes) })
|
|
135
|
+
}
|
|
136
|
+
],
|
|
137
|
+
[assigneeNames.data, onOpenTask, statusNames, t, tagLabels.data]
|
|
138
|
+
);
|
|
88
139
|
if (tasksQuery.isLoading) {
|
|
89
140
|
return /* @__PURE__ */ jsx(LoadingMessage, { label: t("staff.time_tracking.board.loading", "Loading the board\u2026") });
|
|
90
141
|
}
|
|
@@ -112,47 +163,15 @@ function TaskListView({
|
|
|
112
163
|
const total = tasksQuery.data?.total ?? loadedTasks.length;
|
|
113
164
|
const remaining = Math.max(0, total - loadedTasks.length);
|
|
114
165
|
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
|
|
115
|
-
/* @__PURE__ */ jsx("div", {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
] }) }),
|
|
125
|
-
/* @__PURE__ */ jsx("tbody", { children: tasks.map((task) => /* @__PURE__ */ jsxs(
|
|
126
|
-
"tr",
|
|
127
|
-
{
|
|
128
|
-
"data-task-row": task.id,
|
|
129
|
-
className: "border-b border-border last:border-b-0 hover:bg-muted/40",
|
|
130
|
-
children: [
|
|
131
|
-
/* @__PURE__ */ jsx("td", { className: "px-3 py-2.5 text-xs tabular-nums text-muted-foreground", children: task.reference ?? "\u2014" }),
|
|
132
|
-
/* @__PURE__ */ jsx("td", { className: "px-3 py-2.5", children: /* @__PURE__ */ jsx(
|
|
133
|
-
Button,
|
|
134
|
-
{
|
|
135
|
-
type: "button",
|
|
136
|
-
variant: "link",
|
|
137
|
-
size: "2xs",
|
|
138
|
-
onClick: () => onOpenTask(task.id),
|
|
139
|
-
className: "h-auto px-0 text-left text-sm font-semibold text-foreground",
|
|
140
|
-
children: task.title
|
|
141
|
-
}
|
|
142
|
-
) }),
|
|
143
|
-
/* @__PURE__ */ jsx("td", { className: "px-3 py-2.5 text-muted-foreground", children: task.taskStatusId ? statusNames.get(task.taskStatusId) ?? "\u2014" : "\u2014" }),
|
|
144
|
-
/* @__PURE__ */ jsx("td", { className: "px-3 py-2.5 text-muted-foreground", children: task.assigneeStaffMemberId ? assigneeNames.data?.get(task.assigneeStaffMemberId) ?? t("staff.time_tracking.board.card.unassigned", "Unassigned") : t("staff.time_tracking.board.card.unassigned", "Unassigned") }),
|
|
145
|
-
/* @__PURE__ */ jsx("td", { className: "px-3 py-2.5", children: /* @__PURE__ */ jsx("span", { className: "flex flex-wrap gap-1", children: task.tagIds.map((tagId) => {
|
|
146
|
-
const label = tagLabels.data?.get(tagId);
|
|
147
|
-
if (!label) return null;
|
|
148
|
-
return /* @__PURE__ */ jsx(Badge, { variant: "neutral", size: "sm", children: label }, tagId);
|
|
149
|
-
}) }) }),
|
|
150
|
-
/* @__PURE__ */ jsx("td", { className: "px-3 py-2.5 text-right font-semibold tabular-nums text-foreground", children: formatBoardMinutes(task.loggedMinutes) })
|
|
151
|
-
]
|
|
152
|
-
},
|
|
153
|
-
task.id
|
|
154
|
-
)) })
|
|
155
|
-
] }) }),
|
|
166
|
+
/* @__PURE__ */ jsx("div", { "data-testid": "board-task-list", children: /* @__PURE__ */ jsx(
|
|
167
|
+
DataTable,
|
|
168
|
+
{
|
|
169
|
+
extensionTableId: extensionPoints.hosts.taskBoardListViewTable.tableId,
|
|
170
|
+
disableRowClick: true,
|
|
171
|
+
columns,
|
|
172
|
+
data: tasks
|
|
173
|
+
}
|
|
174
|
+
) }),
|
|
156
175
|
remaining > 0 ? /* @__PURE__ */ jsx(
|
|
157
176
|
Button,
|
|
158
177
|
{
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/staff/lib/time-tracking-ui/TaskListView.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useQuery } from '@tanstack/react-query'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { EmptyState } from '@open-mercato/ui/primitives/empty-state'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { ErrorMessage, LoadingMessage } from '@open-mercato/ui/backend/detail'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { sumTaskLoggedMinutes } from '../timesheets-tasks/taskHoursTotals'\nimport {\n BOARD_PAGE_SIZE,\n BOARD_QUERY_ROOT,\n fetchTaskPages,\n useAssigneeNames,\n useTagLabels,\n} from './boardDirectory'\nimport {\n boardServerFilterKey,\n boardServerFilterParams,\n type BoardFilters,\n} from './boardFilters'\nimport {\n formatBoardMinutes,\n sortByPosition,\n toBoardTask,\n type BoardStatus,\n type BoardTask,\n} from './kanbanBoardData'\nimport type { BoardSummary } from './boardSummary'\n\n/**\n * \"Lista\" \u2014 the flat half of the screen-6 view switch.\n *\n * It is a real view over the same tasks, not a placeholder: the filter set, the hours\n * arithmetic and the drawer are already built, so the only thing missing was a table to\n * put them in. Unlike the board it asks the server for the status filter (it has no\n * columns to select), which is the one place the two views build their request\n * differently \u2014 see `boardFilters.ts`.\n */\nexport function TaskListView({\n timeProjectId,\n filters,\n statuses,\n onOpenTask,\n onSummaryChange,\n}: {\n timeProjectId: string\n filters: BoardFilters\n statuses: readonly BoardStatus[]\n onOpenTask: (taskId: string) => void\n onSummaryChange?: (summary: BoardSummary) => void\n}): React.ReactElement {\n const t = useT()\n const scopeVersion = useOrganizationScopeVersion()\n const [pages, setPages] = React.useState(1)\n const filterKey = boardServerFilterKey(filters, { includeStatus: true })\n\n React.useEffect(() => {\n setPages(1)\n }, [filterKey, timeProjectId])\n\n const tasksQuery = useQuery({\n queryKey: [\n ...BOARD_QUERY_ROOT,\n 'list',\n `scope:${scopeVersion}`,\n `project:${timeProjectId}`,\n `pages:${pages}`,\n `filters:${filterKey}`,\n ],\n staleTime: 15_000,\n queryFn: () =>\n fetchTaskPages(\n (page) =>\n `timeProjectId=${encodeURIComponent(timeProjectId)}&topLevelOnly=true&page=${page}&pageSize=${BOARD_PAGE_SIZE}&sortField=position&sortDir=asc${boardServerFilterParams(\n filters,\n { includeStatus: true },\n )}`,\n pages,\n ),\n })\n\n const loadedTasks = React.useMemo(() => {\n const rows = (tasksQuery.data?.items ?? [])\n .map(toBoardTask)\n .filter((task): task is BoardTask => task !== null)\n return sortByPosition(rows)\n }, [tasksQuery.data])\n\n const tasks = loadedTasks\n\n const assigneeIds = React.useMemo(\n () =>\n tasks\n .map((task) => task.assigneeStaffMemberId)\n .filter((id): id is string => typeof id === 'string'),\n [tasks],\n )\n const tagIds = React.useMemo(() => tasks.flatMap((task) => task.tagIds), [tasks])\n const assigneeNames = useAssigneeNames(assigneeIds)\n const tagLabels = useTagLabels(tagIds)\n\n const statusNames = React.useMemo(() => {\n const map = new Map<string, string>()\n for (const status of statuses) map.set(status.id, status.name)\n return map\n }, [statuses])\n\n const summary = React.useMemo<BoardSummary>(\n () => ({\n taskCount: tasks.length,\n loggedMinutes: sumTaskLoggedMinutes(tasks),\n }),\n [tasks],\n )\n\n React.useEffect(() => {\n onSummaryChange?.(summary)\n }, [onSummaryChange, summary])\n\n
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useQuery } from '@tanstack/react-query'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { EmptyState } from '@open-mercato/ui/primitives/empty-state'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { ErrorMessage, LoadingMessage } from '@open-mercato/ui/backend/detail'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { extensionPoints } from '@open-mercato/core/modules/staff/extension-points'\nimport { sumTaskLoggedMinutes } from '../timesheets-tasks/taskHoursTotals'\nimport {\n BOARD_PAGE_SIZE,\n BOARD_QUERY_ROOT,\n fetchTaskPages,\n useAssigneeNames,\n useTagLabels,\n} from './boardDirectory'\nimport {\n boardServerFilterKey,\n boardServerFilterParams,\n type BoardFilters,\n} from './boardFilters'\nimport {\n formatBoardMinutes,\n sortByPosition,\n toBoardTask,\n type BoardStatus,\n type BoardTask,\n} from './kanbanBoardData'\nimport type { BoardSummary } from './boardSummary'\n\n/**\n * \"Lista\" \u2014 the flat half of the screen-6 view switch.\n *\n * It is a real view over the same tasks, not a placeholder: the filter set, the hours\n * arithmetic and the drawer are already built, so the only thing missing was a table to\n * put them in. Unlike the board it asks the server for the status filter (it has no\n * columns to select), which is the one place the two views build their request\n * differently \u2014 see `boardFilters.ts`.\n */\nexport function TaskListView({\n timeProjectId,\n filters,\n statuses,\n onOpenTask,\n onSummaryChange,\n}: {\n timeProjectId: string\n filters: BoardFilters\n statuses: readonly BoardStatus[]\n onOpenTask: (taskId: string) => void\n onSummaryChange?: (summary: BoardSummary) => void\n}): React.ReactElement {\n const t = useT()\n const scopeVersion = useOrganizationScopeVersion()\n const [pages, setPages] = React.useState(1)\n const filterKey = boardServerFilterKey(filters, { includeStatus: true })\n\n React.useEffect(() => {\n setPages(1)\n }, [filterKey, timeProjectId])\n\n const tasksQuery = useQuery({\n queryKey: [\n ...BOARD_QUERY_ROOT,\n 'list',\n `scope:${scopeVersion}`,\n `project:${timeProjectId}`,\n `pages:${pages}`,\n `filters:${filterKey}`,\n ],\n staleTime: 15_000,\n queryFn: () =>\n fetchTaskPages(\n (page) =>\n `timeProjectId=${encodeURIComponent(timeProjectId)}&topLevelOnly=true&page=${page}&pageSize=${BOARD_PAGE_SIZE}&sortField=position&sortDir=asc${boardServerFilterParams(\n filters,\n { includeStatus: true },\n )}`,\n pages,\n ),\n })\n\n const loadedTasks = React.useMemo(() => {\n const rows = (tasksQuery.data?.items ?? [])\n .map(toBoardTask)\n .filter((task): task is BoardTask => task !== null)\n return sortByPosition(rows)\n }, [tasksQuery.data])\n\n const tasks = loadedTasks\n\n const assigneeIds = React.useMemo(\n () =>\n tasks\n .map((task) => task.assigneeStaffMemberId)\n .filter((id): id is string => typeof id === 'string'),\n [tasks],\n )\n const tagIds = React.useMemo(() => tasks.flatMap((task) => task.tagIds), [tasks])\n const assigneeNames = useAssigneeNames(assigneeIds)\n const tagLabels = useTagLabels(tagIds)\n\n const statusNames = React.useMemo(() => {\n const map = new Map<string, string>()\n for (const status of statuses) map.set(status.id, status.name)\n return map\n }, [statuses])\n\n const summary = React.useMemo<BoardSummary>(\n () => ({\n taskCount: tasks.length,\n loggedMinutes: sumTaskLoggedMinutes(tasks),\n }),\n [tasks],\n )\n\n React.useEffect(() => {\n onSummaryChange?.(summary)\n }, [onSummaryChange, summary])\n\n const columns = React.useMemo<ColumnDef<BoardTask>[]>(\n () => [\n {\n accessorKey: 'reference',\n header: t('staff.time_tracking.board.list.columns.reference', 'Ref.'),\n cell: ({ row }) => (\n <span className=\"text-xs tabular-nums text-muted-foreground\">\n {row.original.reference ?? '\u2014'}\n </span>\n ),\n },\n {\n accessorKey: 'title',\n header: t('staff.time_tracking.board.list.columns.title', 'Task'),\n cell: ({ row }) => (\n <span data-task-row={row.original.id}>\n <Button\n type=\"button\"\n variant=\"link\"\n size=\"2xs\"\n onClick={() => onOpenTask(row.original.id)}\n className=\"h-auto px-0 text-left text-sm font-semibold text-foreground\"\n >\n {row.original.title}\n </Button>\n </span>\n ),\n },\n {\n id: 'status',\n header: t('staff.time_tracking.board.list.columns.status', 'Status'),\n cell: ({ row }) =>\n row.original.taskStatusId ? statusNames.get(row.original.taskStatusId) ?? '\u2014' : '\u2014',\n },\n {\n id: 'assignee',\n header: t('staff.time_tracking.board.list.columns.assignee', 'Assignee'),\n cell: ({ row }) =>\n row.original.assigneeStaffMemberId\n ? assigneeNames.data?.get(row.original.assigneeStaffMemberId) ??\n t('staff.time_tracking.board.card.unassigned', 'Unassigned')\n : t('staff.time_tracking.board.card.unassigned', 'Unassigned'),\n },\n {\n id: 'tags',\n header: t('staff.time_tracking.board.list.columns.tags', 'Tags'),\n cell: ({ row }) => (\n <span className=\"flex flex-wrap gap-1\">\n {row.original.tagIds.map((tagId) => {\n // Same rule as the board card: a tag whose label has not\n // arrived yet is skipped, never drawn as its raw id.\n const label = tagLabels.data?.get(tagId)\n if (!label) return null\n return (\n <Badge key={tagId} variant=\"neutral\" size=\"sm\">\n {label}\n </Badge>\n )\n })}\n </span>\n ),\n },\n {\n id: 'hours',\n header: () => (\n <span className=\"block text-right\">\n {t('staff.time_tracking.board.list.columns.hours', 'Logged')}\n </span>\n ),\n cell: ({ row }) => (\n <span className=\"block text-right font-semibold tabular-nums text-foreground\">\n {formatBoardMinutes(row.original.loggedMinutes)}\n </span>\n ),\n },\n ],\n [assigneeNames.data, onOpenTask, statusNames, t, tagLabels.data],\n )\n\n if (tasksQuery.isLoading) {\n return <LoadingMessage label={t('staff.time_tracking.board.loading', 'Loading the board\u2026')} />\n }\n\n if (tasksQuery.isError) {\n return (\n <ErrorMessage\n label={t('staff.time_tracking.board.loadError', 'Could not load the board.')}\n action={\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => void tasksQuery.refetch()}>\n {t('staff.time_tracking.board.reload', 'Try again')}\n </Button>\n }\n />\n )\n }\n\n if (tasks.length === 0) {\n return (\n <EmptyState\n title={t('staff.time_tracking.board.list.empty.title', 'No tasks match these filters')}\n description={t(\n 'staff.time_tracking.board.list.empty.description',\n 'Remove a filter chip to widen the view.',\n )}\n />\n )\n }\n\n const total = tasksQuery.data?.total ?? loadedTasks.length\n const remaining = Math.max(0, total - loadedTasks.length)\n\n return (\n <div className=\"flex flex-col gap-3\">\n <div data-testid=\"board-task-list\">\n <DataTable<BoardTask>\n extensionTableId={extensionPoints.hosts.taskBoardListViewTable.tableId}\n disableRowClick\n columns={columns}\n data={tasks}\n />\n </div>\n\n {remaining > 0 ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n disabled={tasksQuery.isFetching}\n onClick={() => setPages((previous) => previous + 1)}\n data-testid=\"board-task-list-load-more\"\n className=\"self-start rounded-lg border border-dashed border-muted-foreground/60 text-muted-foreground\"\n >\n {tasksQuery.isFetching ? (\n <Spinner className=\"size-4\" />\n ) : (\n t('staff.time_tracking.board.loadMore', 'Show more ({remaining})', { remaining })\n )}\n </Button>\n ) : null}\n </div>\n )\n}\n\nexport default TaskListView\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -3,7 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
4
4
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
5
5
|
import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
|
|
6
|
-
import { CrudHttpError, isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
6
|
+
import { CrudHttpError, isCrudHttpError, translateCrudErrorBody } from "@open-mercato/shared/lib/crud/errors";
|
|
7
7
|
import { getCommandInterceptorHttpRejection } from "@open-mercato/shared/lib/commands/errors";
|
|
8
8
|
import { readJsonSafe } from "@open-mercato/shared/lib/http/readJsonSafe";
|
|
9
9
|
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
@@ -268,7 +268,7 @@ async function POST(req) {
|
|
|
268
268
|
}
|
|
269
269
|
return NextResponse.json({ status: "ok", extraction });
|
|
270
270
|
} catch (err) {
|
|
271
|
-
if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status });
|
|
271
|
+
if (isCrudHttpError(err)) return NextResponse.json(translateCrudErrorBody(err.body, translate), { status: err.status });
|
|
272
272
|
const interceptorRejection = getCommandInterceptorHttpRejection(err);
|
|
273
273
|
if (interceptorRejection) {
|
|
274
274
|
return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/warranty_claims/api/ai/assess/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { AwilixContainer } from 'awilix'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest, type AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { runRouteMutationGuards, type RouteMutationGuardResult } from '@open-mercato/shared/lib/crud/route-mutation-guard'\nimport { withScopedPayload } from '@open-mercato/shared/lib/api/scoped'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { AiChatRequestContext } from '@open-mercato/ai-assistant/modules/ai_assistant/lib/attachment-bridge-types'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { AttachmentTargetAccessService } from '@open-mercato/core/modules/attachments/lib/target-access-service'\nimport { WarrantyClaimLine } from '../../../data/entities'\nimport {\n assessDamagePhoto,\n extractProofOfPurchase,\n isWarrantyAiNotConfiguredError,\n isWarrantyAiUnavailableError,\n} from '../../../lib/aiAssist'\nimport {\n WARRANTY_CLAIM_LINE_RESOURCE_KIND,\n requireScopedClaim,\n type WarrantyClaimScope,\n} from '../../../commands/shared'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('warranty_claims')\n\nconst uuid = z.string().uuid()\nconst optimisticLockTokenSchema = z.union([z.string().datetime(), z.date()]).nullable().optional()\nconst CLAIM_ATTACHMENT_ENTITY_ID = 'warranty_claims:warranty_claim'\nconst LINE_ATTACHMENT_ENTITY_ID = 'warranty_claims:warranty_claim_line'\n\nconst assessBodySchema = z\n .object({\n claimId: uuid,\n lineId: uuid.optional(),\n attachmentId: uuid,\n kind: z.enum(['damage', 'proof']),\n organizationId: uuid.optional(),\n tenantId: uuid.optional(),\n updatedAt: optimisticLockTokenSchema,\n })\n .strict()\n\ntype AssessBodyInput = z.infer<typeof assessBodySchema>\n\ntype AssessRouteContext = {\n ctx: CommandRuntimeContext\n tenantId: string\n organizationId: string\n container: AwilixContainer\n em: EntityManager\n translate: (key: string, fallback?: string) => string\n}\n\ntype SetAssessmentCommandInput = {\n id: string\n organizationId: string\n tenantId: string\n assessmentPayload: Record<string, unknown>\n updatedAt?: string | Date | null\n}\n\nconst damageAssessmentSchema = z.object({\n damageType: z.string(),\n severity: z.enum(['minor', 'moderate', 'severe', 'unknown']),\n probableCause: z.string(),\n misuseSuspected: z.boolean(),\n confidence: z.number(),\n summary: z.string(),\n})\n\nconst proofExtractionSchema = z.object({\n purchaseDate: z.string().nullable(),\n serialNumber: z.string().nullable(),\n amount: z.string().nullable(),\n currency: z.string().nullable(),\n merchant: z.string().nullable(),\n confidence: z.number(),\n})\n\nconst assessResponseSchema = z.union([\n z.object({\n status: z.literal('ok'),\n assessment: damageAssessmentSchema.optional(),\n extraction: proofExtractionSchema.optional(),\n }),\n z.object({ status: z.literal('notConfigured') }),\n z.object({ status: z.literal('aiUnavailable') }),\n])\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['warranty_claims.claim.manage'] },\n}\n\nfunction toRecord(value: unknown): Record<string, unknown> {\n return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {}\n}\n\nfunction isSuperAdmin(auth: CommandRuntimeContext['auth']): boolean {\n return Boolean(auth && toRecord(auth).isSuperAdmin === true)\n}\n\nfunction buildAuthContext(context: AssessRouteContext): AiChatRequestContext {\n const userId = context.ctx.auth?.sub\n if (!userId) {\n throw new CrudHttpError(401, { error: context.translate('warranty_claims.errors.unauthorized', 'Unauthorized') })\n }\n return {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId,\n features: [],\n isSuperAdmin: isSuperAdmin(context.ctx.auth),\n }\n}\n\nfunction buildAttachmentAuthContext(context: AssessRouteContext): Exclude<AuthContext, null> {\n const auth = context.ctx.auth\n if (!auth?.sub) {\n throw new CrudHttpError(401, { error: context.translate('warranty_claims.errors.unauthorized', 'Unauthorized') })\n }\n return {\n ...auth,\n sub: auth.sub,\n tenantId: context.tenantId,\n orgId: context.organizationId,\n }\n}\n\nasync function resolveAssessContext(\n req: Request,\n translate: AssessRouteContext['translate'],\n): Promise<AssessRouteContext> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, { error: translate('warranty_claims.errors.unauthorized', 'Unauthorized') })\n }\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationId = scope?.selectedId ?? auth.orgId ?? null\n if (!organizationId) {\n throw new CrudHttpError(400, { error: translate('warranty_claims.errors.organization_required', 'Organization context is required.') })\n }\n return {\n ctx: {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: organizationId,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n },\n tenantId: auth.tenantId,\n organizationId,\n container,\n em: container.resolve<EntityManager>('em').fork(),\n translate,\n }\n}\n\nfunction toAssessInput(payload: Record<string, unknown>, context: AssessRouteContext): AssessBodyInput {\n return assessBodySchema.parse(withScopedPayload(payload, context.ctx, context.translate))\n}\n\nasync function runGuard(\n req: Request,\n context: AssessRouteContext,\n input: AssessBodyInput & { lineId: string },\n): Promise<RouteMutationGuardResult> {\n const userId = context.ctx.auth?.sub\n if (!userId) {\n throw new CrudHttpError(401, { error: context.translate('warranty_claims.errors.unauthorized', 'Unauthorized') })\n }\n return runRouteMutationGuards({\n container: context.ctx.container,\n req,\n auth: {\n userId,\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n },\n input: {\n resourceKind: WARRANTY_CLAIM_LINE_RESOURCE_KIND,\n resourceId: input.lineId,\n operation: 'custom',\n mutationPayload: { ...input },\n },\n })\n}\n\nasync function loadScopedLine(\n em: EntityManager,\n scope: WarrantyClaimScope,\n claimId: string,\n lineId: string,\n translate: AssessRouteContext['translate'],\n): Promise<WarrantyClaimLine> {\n const line = await findOneWithDecryption(\n em,\n WarrantyClaimLine,\n {\n id: lineId,\n claim: claimId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n },\n {},\n scope,\n )\n if (!line) {\n throw new CrudHttpError(404, { error: translate('warranty_claims.errors.notFound', 'Claim not found.') })\n }\n return line\n}\n\nasync function verifyAttachmentLinkedToTarget(\n context: AssessRouteContext,\n input: { attachmentId: string; claimId: string; lineId?: string | null },\n): Promise<void> {\n let service: AttachmentTargetAccessService | null = null\n try {\n service = context.container.resolve<AttachmentTargetAccessService>('attachmentTargetAccessService')\n } catch {\n service = null\n }\n const linked = service ? await service.canAccessLinkedTarget({\n attachmentId: input.attachmentId,\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n auth: buildAttachmentAuthContext(context),\n targets: [\n { entityId: CLAIM_ATTACHMENT_ENTITY_ID, recordId: input.claimId },\n ...(input.lineId ? [{ entityId: LINE_ATTACHMENT_ENTITY_ID, recordId: input.lineId }] : []),\n ],\n }) : false\n if (!linked) {\n throw new CrudHttpError(400, { error: context.translate('warranty_claims.errors.attachmentNotLinked', 'The attachment is not linked to this claim.') })\n }\n}\n\nfunction mergeAssessmentPayload(\n current: Record<string, unknown> | null | undefined,\n key: 'damage' | 'proof',\n value: Record<string, unknown>,\n): Record<string, unknown> {\n return {\n ...(current ?? {}),\n [key]: value,\n generatedAt: new Date().toISOString(),\n }\n}\n\nasync function persistAssessmentPayload(\n context: AssessRouteContext,\n line: WarrantyClaimLine,\n key: 'damage' | 'proof',\n value: Record<string, unknown>,\n updatedAt: string | Date | null | undefined,\n): Promise<void> {\n const commandBus = context.ctx.container.resolve('commandBus') as CommandBus\n const assessmentPayload = mergeAssessmentPayload(line.assessmentPayload ?? null, key, value)\n await commandBus.execute<SetAssessmentCommandInput, { lineId: string; claimId: string }>(\n 'warranty_claims.claim_line.set_assessment',\n {\n input: {\n id: line.id,\n organizationId: context.organizationId,\n tenantId: context.tenantId,\n assessmentPayload,\n updatedAt,\n },\n ctx: context.ctx,\n },\n )\n}\n\nexport async function POST(req: Request) {\n const { translate } = await resolveTranslations()\n try {\n const context = await resolveAssessContext(req, translate)\n const payload = toRecord(await readJsonSafe(req, {}))\n const parsed = toAssessInput(payload, context)\n if (parsed.kind === 'damage' && !parsed.lineId) {\n throw new CrudHttpError(400, { error: translate('warranty_claims.errors.invalidInput', 'Invalid input') })\n }\n\n const guarded = parsed.lineId ? await runGuard(req, context, parsed as AssessBodyInput & { lineId: string }) : null\n if (guarded && !guarded.ok) {\n return guarded.response\n }\n const input = guarded?.modifiedPayload ? toAssessInput(guarded.modifiedPayload, context) : parsed\n const scope = { tenantId: context.tenantId, organizationId: context.organizationId }\n\n if (input.kind === 'damage') {\n if (!input.lineId) {\n throw new CrudHttpError(400, { error: translate('warranty_claims.errors.invalidInput', 'Invalid input') })\n }\n const line = await loadScopedLine(context.em, scope, input.claimId, input.lineId, translate)\n await verifyAttachmentLinkedToTarget(context, {\n attachmentId: input.attachmentId,\n claimId: input.claimId,\n lineId: input.lineId,\n })\n const assessment = await assessDamagePhoto({\n em: context.em,\n container: context.container,\n scope,\n claimId: input.claimId,\n lineId: input.lineId,\n attachmentId: input.attachmentId,\n authContext: buildAuthContext(context),\n })\n await persistAssessmentPayload(context, line, 'damage', assessment, input.updatedAt)\n await guarded?.runAfterSuccess()\n return NextResponse.json({ status: 'ok', assessment })\n }\n\n await requireScopedClaim(context.em, input.claimId, scope, {}, translate('warranty_claims.errors.notFound', 'Claim not found.'))\n const line = input.lineId ? await loadScopedLine(context.em, scope, input.claimId, input.lineId, translate) : null\n await verifyAttachmentLinkedToTarget(context, {\n attachmentId: input.attachmentId,\n claimId: input.claimId,\n lineId: input.lineId ?? null,\n })\n const extraction = await extractProofOfPurchase({\n em: context.em,\n container: context.container,\n scope,\n attachmentId: input.attachmentId,\n authContext: buildAuthContext(context),\n })\n if (line) {\n await persistAssessmentPayload(context, line, 'proof', extraction, input.updatedAt)\n await guarded?.runAfterSuccess()\n }\n return NextResponse.json({ status: 'ok', extraction })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n const interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n if (isWarrantyAiNotConfiguredError(err)) {\n return NextResponse.json({ status: 'notConfigured' })\n }\n if (isWarrantyAiUnavailableError(err)) {\n return NextResponse.json({ status: 'aiUnavailable' })\n }\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: translate('warranty_claims.errors.invalidInput', 'Invalid input') }, { status: 400 })\n }\n logger.error('warranty_claims.ai.assess.post failed', { err })\n return NextResponse.json({ error: translate('warranty_claims.errors.save_failed', 'Failed to save warranty claim.') }, { status: 500 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Warranty Claims',\n summary: 'Assess warranty claim attachment with AI',\n methods: {\n POST: {\n summary: 'Assess a damage photo or extract proof-of-purchase facts',\n requestBody: { contentType: 'application/json', schema: assessBodySchema },\n responses: [\n {\n status: 200,\n description: 'Assessment completed or AI gracefully degraded',\n schema: assessResponseSchema,\n },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAGlB,SAAS,8BAA8B;AACvC,SAAS,0BAA4C;AACrD,SAAS,0CAA0C;AAEnD,SAAS,eAAe,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { AwilixContainer } from 'awilix'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest, type AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { CrudHttpError, isCrudHttpError, translateCrudErrorBody } from '@open-mercato/shared/lib/crud/errors'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { runRouteMutationGuards, type RouteMutationGuardResult } from '@open-mercato/shared/lib/crud/route-mutation-guard'\nimport { withScopedPayload } from '@open-mercato/shared/lib/api/scoped'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { AiChatRequestContext } from '@open-mercato/ai-assistant/modules/ai_assistant/lib/attachment-bridge-types'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { AttachmentTargetAccessService } from '@open-mercato/core/modules/attachments/lib/target-access-service'\nimport { WarrantyClaimLine } from '../../../data/entities'\nimport {\n assessDamagePhoto,\n extractProofOfPurchase,\n isWarrantyAiNotConfiguredError,\n isWarrantyAiUnavailableError,\n} from '../../../lib/aiAssist'\nimport {\n WARRANTY_CLAIM_LINE_RESOURCE_KIND,\n requireScopedClaim,\n type WarrantyClaimScope,\n} from '../../../commands/shared'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('warranty_claims')\n\nconst uuid = z.string().uuid()\nconst optimisticLockTokenSchema = z.union([z.string().datetime(), z.date()]).nullable().optional()\nconst CLAIM_ATTACHMENT_ENTITY_ID = 'warranty_claims:warranty_claim'\nconst LINE_ATTACHMENT_ENTITY_ID = 'warranty_claims:warranty_claim_line'\n\nconst assessBodySchema = z\n .object({\n claimId: uuid,\n lineId: uuid.optional(),\n attachmentId: uuid,\n kind: z.enum(['damage', 'proof']),\n organizationId: uuid.optional(),\n tenantId: uuid.optional(),\n updatedAt: optimisticLockTokenSchema,\n })\n .strict()\n\ntype AssessBodyInput = z.infer<typeof assessBodySchema>\n\ntype AssessRouteContext = {\n ctx: CommandRuntimeContext\n tenantId: string\n organizationId: string\n container: AwilixContainer\n em: EntityManager\n translate: (key: string, fallback?: string) => string\n}\n\ntype SetAssessmentCommandInput = {\n id: string\n organizationId: string\n tenantId: string\n assessmentPayload: Record<string, unknown>\n updatedAt?: string | Date | null\n}\n\nconst damageAssessmentSchema = z.object({\n damageType: z.string(),\n severity: z.enum(['minor', 'moderate', 'severe', 'unknown']),\n probableCause: z.string(),\n misuseSuspected: z.boolean(),\n confidence: z.number(),\n summary: z.string(),\n})\n\nconst proofExtractionSchema = z.object({\n purchaseDate: z.string().nullable(),\n serialNumber: z.string().nullable(),\n amount: z.string().nullable(),\n currency: z.string().nullable(),\n merchant: z.string().nullable(),\n confidence: z.number(),\n})\n\nconst assessResponseSchema = z.union([\n z.object({\n status: z.literal('ok'),\n assessment: damageAssessmentSchema.optional(),\n extraction: proofExtractionSchema.optional(),\n }),\n z.object({ status: z.literal('notConfigured') }),\n z.object({ status: z.literal('aiUnavailable') }),\n])\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['warranty_claims.claim.manage'] },\n}\n\nfunction toRecord(value: unknown): Record<string, unknown> {\n return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {}\n}\n\nfunction isSuperAdmin(auth: CommandRuntimeContext['auth']): boolean {\n return Boolean(auth && toRecord(auth).isSuperAdmin === true)\n}\n\nfunction buildAuthContext(context: AssessRouteContext): AiChatRequestContext {\n const userId = context.ctx.auth?.sub\n if (!userId) {\n throw new CrudHttpError(401, { error: context.translate('warranty_claims.errors.unauthorized', 'Unauthorized') })\n }\n return {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId,\n features: [],\n isSuperAdmin: isSuperAdmin(context.ctx.auth),\n }\n}\n\nfunction buildAttachmentAuthContext(context: AssessRouteContext): Exclude<AuthContext, null> {\n const auth = context.ctx.auth\n if (!auth?.sub) {\n throw new CrudHttpError(401, { error: context.translate('warranty_claims.errors.unauthorized', 'Unauthorized') })\n }\n return {\n ...auth,\n sub: auth.sub,\n tenantId: context.tenantId,\n orgId: context.organizationId,\n }\n}\n\nasync function resolveAssessContext(\n req: Request,\n translate: AssessRouteContext['translate'],\n): Promise<AssessRouteContext> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, { error: translate('warranty_claims.errors.unauthorized', 'Unauthorized') })\n }\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationId = scope?.selectedId ?? auth.orgId ?? null\n if (!organizationId) {\n throw new CrudHttpError(400, { error: translate('warranty_claims.errors.organization_required', 'Organization context is required.') })\n }\n return {\n ctx: {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: organizationId,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n },\n tenantId: auth.tenantId,\n organizationId,\n container,\n em: container.resolve<EntityManager>('em').fork(),\n translate,\n }\n}\n\nfunction toAssessInput(payload: Record<string, unknown>, context: AssessRouteContext): AssessBodyInput {\n return assessBodySchema.parse(withScopedPayload(payload, context.ctx, context.translate))\n}\n\nasync function runGuard(\n req: Request,\n context: AssessRouteContext,\n input: AssessBodyInput & { lineId: string },\n): Promise<RouteMutationGuardResult> {\n const userId = context.ctx.auth?.sub\n if (!userId) {\n throw new CrudHttpError(401, { error: context.translate('warranty_claims.errors.unauthorized', 'Unauthorized') })\n }\n return runRouteMutationGuards({\n container: context.ctx.container,\n req,\n auth: {\n userId,\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n },\n input: {\n resourceKind: WARRANTY_CLAIM_LINE_RESOURCE_KIND,\n resourceId: input.lineId,\n operation: 'custom',\n mutationPayload: { ...input },\n },\n })\n}\n\nasync function loadScopedLine(\n em: EntityManager,\n scope: WarrantyClaimScope,\n claimId: string,\n lineId: string,\n translate: AssessRouteContext['translate'],\n): Promise<WarrantyClaimLine> {\n const line = await findOneWithDecryption(\n em,\n WarrantyClaimLine,\n {\n id: lineId,\n claim: claimId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n },\n {},\n scope,\n )\n if (!line) {\n throw new CrudHttpError(404, { error: translate('warranty_claims.errors.notFound', 'Claim not found.') })\n }\n return line\n}\n\nasync function verifyAttachmentLinkedToTarget(\n context: AssessRouteContext,\n input: { attachmentId: string; claimId: string; lineId?: string | null },\n): Promise<void> {\n let service: AttachmentTargetAccessService | null = null\n try {\n service = context.container.resolve<AttachmentTargetAccessService>('attachmentTargetAccessService')\n } catch {\n service = null\n }\n const linked = service ? await service.canAccessLinkedTarget({\n attachmentId: input.attachmentId,\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n auth: buildAttachmentAuthContext(context),\n targets: [\n { entityId: CLAIM_ATTACHMENT_ENTITY_ID, recordId: input.claimId },\n ...(input.lineId ? [{ entityId: LINE_ATTACHMENT_ENTITY_ID, recordId: input.lineId }] : []),\n ],\n }) : false\n if (!linked) {\n throw new CrudHttpError(400, { error: context.translate('warranty_claims.errors.attachmentNotLinked', 'The attachment is not linked to this claim.') })\n }\n}\n\nfunction mergeAssessmentPayload(\n current: Record<string, unknown> | null | undefined,\n key: 'damage' | 'proof',\n value: Record<string, unknown>,\n): Record<string, unknown> {\n return {\n ...(current ?? {}),\n [key]: value,\n generatedAt: new Date().toISOString(),\n }\n}\n\nasync function persistAssessmentPayload(\n context: AssessRouteContext,\n line: WarrantyClaimLine,\n key: 'damage' | 'proof',\n value: Record<string, unknown>,\n updatedAt: string | Date | null | undefined,\n): Promise<void> {\n const commandBus = context.ctx.container.resolve('commandBus') as CommandBus\n const assessmentPayload = mergeAssessmentPayload(line.assessmentPayload ?? null, key, value)\n await commandBus.execute<SetAssessmentCommandInput, { lineId: string; claimId: string }>(\n 'warranty_claims.claim_line.set_assessment',\n {\n input: {\n id: line.id,\n organizationId: context.organizationId,\n tenantId: context.tenantId,\n assessmentPayload,\n updatedAt,\n },\n ctx: context.ctx,\n },\n )\n}\n\nexport async function POST(req: Request) {\n const { translate } = await resolveTranslations()\n try {\n const context = await resolveAssessContext(req, translate)\n const payload = toRecord(await readJsonSafe(req, {}))\n const parsed = toAssessInput(payload, context)\n if (parsed.kind === 'damage' && !parsed.lineId) {\n throw new CrudHttpError(400, { error: translate('warranty_claims.errors.invalidInput', 'Invalid input') })\n }\n\n const guarded = parsed.lineId ? await runGuard(req, context, parsed as AssessBodyInput & { lineId: string }) : null\n if (guarded && !guarded.ok) {\n return guarded.response\n }\n const input = guarded?.modifiedPayload ? toAssessInput(guarded.modifiedPayload, context) : parsed\n const scope = { tenantId: context.tenantId, organizationId: context.organizationId }\n\n if (input.kind === 'damage') {\n if (!input.lineId) {\n throw new CrudHttpError(400, { error: translate('warranty_claims.errors.invalidInput', 'Invalid input') })\n }\n const line = await loadScopedLine(context.em, scope, input.claimId, input.lineId, translate)\n await verifyAttachmentLinkedToTarget(context, {\n attachmentId: input.attachmentId,\n claimId: input.claimId,\n lineId: input.lineId,\n })\n const assessment = await assessDamagePhoto({\n em: context.em,\n container: context.container,\n scope,\n claimId: input.claimId,\n lineId: input.lineId,\n attachmentId: input.attachmentId,\n authContext: buildAuthContext(context),\n })\n await persistAssessmentPayload(context, line, 'damage', assessment, input.updatedAt)\n await guarded?.runAfterSuccess()\n return NextResponse.json({ status: 'ok', assessment })\n }\n\n await requireScopedClaim(context.em, input.claimId, scope, {}, translate('warranty_claims.errors.notFound', 'Claim not found.'))\n const line = input.lineId ? await loadScopedLine(context.em, scope, input.claimId, input.lineId, translate) : null\n await verifyAttachmentLinkedToTarget(context, {\n attachmentId: input.attachmentId,\n claimId: input.claimId,\n lineId: input.lineId ?? null,\n })\n const extraction = await extractProofOfPurchase({\n em: context.em,\n container: context.container,\n scope,\n attachmentId: input.attachmentId,\n authContext: buildAuthContext(context),\n })\n if (line) {\n await persistAssessmentPayload(context, line, 'proof', extraction, input.updatedAt)\n await guarded?.runAfterSuccess()\n }\n return NextResponse.json({ status: 'ok', extraction })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(translateCrudErrorBody(err.body, translate), { status: err.status })\n const interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n if (isWarrantyAiNotConfiguredError(err)) {\n return NextResponse.json({ status: 'notConfigured' })\n }\n if (isWarrantyAiUnavailableError(err)) {\n return NextResponse.json({ status: 'aiUnavailable' })\n }\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: translate('warranty_claims.errors.invalidInput', 'Invalid input') }, { status: 400 })\n }\n logger.error('warranty_claims.ai.assess.post failed', { err })\n return NextResponse.json({ error: translate('warranty_claims.errors.save_failed', 'Failed to save warranty claim.') }, { status: 500 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Warranty Claims',\n summary: 'Assess warranty claim attachment with AI',\n methods: {\n POST: {\n summary: 'Assess a damage photo or extract proof-of-purchase facts',\n requestBody: { contentType: 'application/json', schema: assessBodySchema },\n responses: [\n {\n status: 200,\n description: 'Assessment completed or AI gracefully degraded',\n schema: assessResponseSchema,\n },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["line"]
|
|
7
7
|
}
|
|
@@ -4,7 +4,7 @@ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
|
4
4
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
5
5
|
import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
|
|
6
6
|
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
7
|
-
import { CrudHttpError, isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
7
|
+
import { CrudHttpError, isCrudHttpError, translateCrudErrorBody } from "@open-mercato/shared/lib/crud/errors";
|
|
8
8
|
import { readJsonSafe } from "@open-mercato/shared/lib/http/readJsonSafe";
|
|
9
9
|
import { withScopedPayload } from "@open-mercato/shared/lib/api/scoped";
|
|
10
10
|
import { buildClaimReplyDraft, isWarrantyAiNotConfiguredError, isWarrantyAiUnavailableError } from "../../../lib/aiAssist.js";
|
|
@@ -74,12 +74,11 @@ async function POST(req) {
|
|
|
74
74
|
});
|
|
75
75
|
return NextResponse.json({ ok: true, draft });
|
|
76
76
|
} catch (err) {
|
|
77
|
-
if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status });
|
|
78
77
|
const { translate } = await resolveTranslations();
|
|
78
|
+
if (isCrudHttpError(err)) return NextResponse.json(translateCrudErrorBody(err.body, translate), { status: err.status });
|
|
79
79
|
if (isWarrantyAiUnavailableError(err)) {
|
|
80
|
-
const { translate: translate2 } = await resolveTranslations();
|
|
81
80
|
return NextResponse.json(
|
|
82
|
-
{ ok: false, aiUnavailable: true, error:
|
|
81
|
+
{ ok: false, aiUnavailable: true, error: translate("warranty_claims.errors.aiUnavailable", "AI drafting is temporarily unavailable") },
|
|
83
82
|
{ status: 502 }
|
|
84
83
|
);
|
|
85
84
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/warranty_claims/api/ai/draft-reply/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { AwilixContainer } from 'awilix'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { withScopedPayload } from '@open-mercato/shared/lib/api/scoped'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { buildClaimReplyDraft, isWarrantyAiNotConfiguredError, isWarrantyAiUnavailableError } from '../../../lib/aiAssist'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('warranty_claims')\n\nconst draftReplySchema = z\n .object({\n claimId: z.string().uuid(),\n tone: z.enum(['formal', 'friendly', 'concise']).optional(),\n organizationId: z.string().uuid().optional(),\n tenantId: z.string().uuid().optional(),\n })\n .strict()\n\ntype DraftReplyInput = z.infer<typeof draftReplySchema>\n\ntype DraftReplyRouteContext = {\n ctx: CommandRuntimeContext\n tenantId: string\n organizationId: string\n container: AwilixContainer\n em: EntityManager\n translate: (key: string, fallback?: string) => string\n}\n\nconst successResponseSchema = z.object({\n ok: z.literal(true),\n draft: z.string(),\n})\n\nconst notConfiguredResponseSchema = z.object({\n ok: z.literal(false),\n notConfigured: z.literal(true),\n error: z.string(),\n})\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['warranty_claims.claim.manage'] },\n}\n\nfunction toRecord(value: unknown): Record<string, unknown> {\n return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {}\n}\n\nasync function resolveDraftReplyContext(req: Request): Promise<DraftReplyRouteContext> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, { error: translate('warranty_claims.errors.unauthorized', 'Unauthorized') })\n }\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationId = scope?.selectedId ?? auth.orgId ?? null\n if (!organizationId) {\n throw new CrudHttpError(400, { error: translate('warranty_claims.errors.organization_required', 'Organization context is required') })\n }\n\n const ctx: CommandRuntimeContext = {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: organizationId,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n }\n\n return {\n ctx,\n tenantId: auth.tenantId,\n organizationId,\n container,\n em: container.resolve<EntityManager>('em').fork(),\n translate,\n }\n}\n\nexport async function POST(req: Request) {\n try {\n const context = await resolveDraftReplyContext(req)\n const payload = toRecord(await readJsonSafe(req, {}))\n const input: DraftReplyInput = draftReplySchema.parse(withScopedPayload(payload, context.ctx, context.translate))\n const { draft } = await buildClaimReplyDraft({\n em: context.em,\n container: context.container,\n scope: { tenantId: context.tenantId, organizationId: context.organizationId },\n claimId: input.claimId,\n tone: input.tone,\n })\n return NextResponse.json({ ok: true, draft })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAGlB,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,0CAA0C;AAEnD,SAAS,2BAA2B;AACpC,SAAS,eAAe,
|
|
6
|
-
"names": [
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { AwilixContainer } from 'awilix'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError, isCrudHttpError, translateCrudErrorBody } from '@open-mercato/shared/lib/crud/errors'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { withScopedPayload } from '@open-mercato/shared/lib/api/scoped'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { buildClaimReplyDraft, isWarrantyAiNotConfiguredError, isWarrantyAiUnavailableError } from '../../../lib/aiAssist'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('warranty_claims')\n\nconst draftReplySchema = z\n .object({\n claimId: z.string().uuid(),\n tone: z.enum(['formal', 'friendly', 'concise']).optional(),\n organizationId: z.string().uuid().optional(),\n tenantId: z.string().uuid().optional(),\n })\n .strict()\n\ntype DraftReplyInput = z.infer<typeof draftReplySchema>\n\ntype DraftReplyRouteContext = {\n ctx: CommandRuntimeContext\n tenantId: string\n organizationId: string\n container: AwilixContainer\n em: EntityManager\n translate: (key: string, fallback?: string) => string\n}\n\nconst successResponseSchema = z.object({\n ok: z.literal(true),\n draft: z.string(),\n})\n\nconst notConfiguredResponseSchema = z.object({\n ok: z.literal(false),\n notConfigured: z.literal(true),\n error: z.string(),\n})\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['warranty_claims.claim.manage'] },\n}\n\nfunction toRecord(value: unknown): Record<string, unknown> {\n return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {}\n}\n\nasync function resolveDraftReplyContext(req: Request): Promise<DraftReplyRouteContext> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, { error: translate('warranty_claims.errors.unauthorized', 'Unauthorized') })\n }\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationId = scope?.selectedId ?? auth.orgId ?? null\n if (!organizationId) {\n throw new CrudHttpError(400, { error: translate('warranty_claims.errors.organization_required', 'Organization context is required') })\n }\n\n const ctx: CommandRuntimeContext = {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: organizationId,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n }\n\n return {\n ctx,\n tenantId: auth.tenantId,\n organizationId,\n container,\n em: container.resolve<EntityManager>('em').fork(),\n translate,\n }\n}\n\nexport async function POST(req: Request) {\n try {\n const context = await resolveDraftReplyContext(req)\n const payload = toRecord(await readJsonSafe(req, {}))\n const input: DraftReplyInput = draftReplySchema.parse(withScopedPayload(payload, context.ctx, context.translate))\n const { draft } = await buildClaimReplyDraft({\n em: context.em,\n container: context.container,\n scope: { tenantId: context.tenantId, organizationId: context.organizationId },\n claimId: input.claimId,\n tone: input.tone,\n })\n return NextResponse.json({ ok: true, draft })\n } catch (err) {\n const { translate } = await resolveTranslations()\n if (isCrudHttpError(err)) return NextResponse.json(translateCrudErrorBody(err.body, translate), { status: err.status })\n if (isWarrantyAiUnavailableError(err)) {\n return NextResponse.json(\n { ok: false, aiUnavailable: true, error: translate('warranty_claims.errors.aiUnavailable', 'AI drafting is temporarily unavailable') },\n { status: 502 },\n )\n }\n if (isWarrantyAiNotConfiguredError(err)) {\n return NextResponse.json({\n ok: false,\n notConfigured: true,\n error: translate('warranty_claims.errors.aiNotConfigured', 'AI drafting is not configured for this workspace'),\n }, { status: 422 })\n }\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: translate('warranty_claims.errors.invalidInput', 'Invalid input') }, { status: 400 })\n }\n logger.error('warranty_claims.ai.draft-reply.post failed', { err })\n return NextResponse.json({ error: translate('warranty_claims.errors.save_failed', 'Failed to save warranty claim') }, { status: 400 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Warranty Claims',\n summary: 'Draft warranty claim customer reply',\n methods: {\n POST: {\n summary: 'Generate an AI-assisted customer reply draft for operator review',\n requestBody: { contentType: 'application/json', schema: draftReplySchema },\n responses: [\n {\n status: 200,\n description: 'Customer reply draft generated',\n schema: successResponseSchema,\n },\n {\n status: 422,\n description: 'AI drafting is not configured',\n schema: notConfiguredResponseSchema,\n },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
7
|
}
|
|
@@ -4,7 +4,7 @@ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
|
4
4
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
5
5
|
import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
|
|
6
6
|
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
7
|
-
import { CrudHttpError, isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
7
|
+
import { CrudHttpError, isCrudHttpError, translateCrudErrorBody } from "@open-mercato/shared/lib/crud/errors";
|
|
8
8
|
import { readJsonSafe } from "@open-mercato/shared/lib/http/readJsonSafe";
|
|
9
9
|
import { buildWarrantyClaimTriageSuggestion } from "../../../lib/triage.js";
|
|
10
10
|
import {
|
|
@@ -116,8 +116,8 @@ async function GET(req) {
|
|
|
116
116
|
const input = suggestSchema.parse({ claimId: url.searchParams.get("claimId") ?? void 0 });
|
|
117
117
|
return buildResponse(context, input);
|
|
118
118
|
} catch (err) {
|
|
119
|
-
if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status });
|
|
120
119
|
const { translate } = await resolveTranslations();
|
|
120
|
+
if (isCrudHttpError(err)) return NextResponse.json(translateCrudErrorBody(err.body, translate), { status: err.status });
|
|
121
121
|
if (err instanceof z.ZodError) {
|
|
122
122
|
return NextResponse.json({ error: translate("warranty_claims.errors.invalidInput", "Invalid input") }, { status: 400 });
|
|
123
123
|
}
|
|
@@ -131,8 +131,8 @@ async function POST(req) {
|
|
|
131
131
|
const input = suggestSchema.parse(toRecord(await readJsonSafe(req, {})));
|
|
132
132
|
return buildResponse(context, input);
|
|
133
133
|
} catch (err) {
|
|
134
|
-
if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status });
|
|
135
134
|
const { translate } = await resolveTranslations();
|
|
135
|
+
if (isCrudHttpError(err)) return NextResponse.json(translateCrudErrorBody(err.body, translate), { status: err.status });
|
|
136
136
|
if (err instanceof z.ZodError) {
|
|
137
137
|
return NextResponse.json({ error: translate("warranty_claims.errors.invalidInput", "Invalid input") }, { status: 400 });
|
|
138
138
|
}
|