@open-mercato/core 0.6.6-develop.6229.1.ebe6706732 → 0.6.6-develop.6245.1.2be3b151f8
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/commands/users.js +1 -0
- package/dist/modules/auth/commands/users.js.map +2 -2
- package/dist/modules/communication_channels/data/enrichers.js +9 -1
- package/dist/modules/communication_channels/data/enrichers.js.map +2 -2
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js +3 -5
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js.map +2 -2
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.js.map +2 -2
- package/dist/modules/customers/api/pipeline-stages/reorder/route.js +39 -1
- package/dist/modules/customers/api/pipeline-stages/reorder/route.js.map +2 -2
- package/dist/modules/customers/api/pipeline-stages/route.js +107 -12
- package/dist/modules/customers/api/pipeline-stages/route.js.map +2 -2
- package/dist/modules/customers/api/pipelines/route.js +107 -12
- package/dist/modules/customers/api/pipelines/route.js.map +2 -2
- package/dist/modules/customers/api/settings/address-format/route.js +33 -1
- package/dist/modules/customers/api/settings/address-format/route.js.map +2 -2
- package/dist/modules/customers/api/tags/assign/route.js +37 -0
- package/dist/modules/customers/api/tags/assign/route.js.map +2 -2
- package/dist/modules/customers/api/tags/unassign/route.js +37 -0
- package/dist/modules/customers/api/tags/unassign/route.js.map +2 -2
- package/dist/modules/customers/components/calendar/CalendarToolbar.js +4 -6
- package/dist/modules/customers/components/calendar/CalendarToolbar.js.map +2 -2
- package/dist/modules/customers/components/calendar/EventBlock.js +2 -6
- package/dist/modules/customers/components/calendar/EventBlock.js.map +2 -2
- package/dist/modules/customers/components/calendar/UpcomingCards.js +9 -7
- package/dist/modules/customers/components/calendar/UpcomingCards.js.map +2 -2
- package/dist/modules/customers/lib/calendar/format.js +27 -0
- package/dist/modules/customers/lib/calendar/format.js.map +7 -0
- package/dist/modules/entities/api/encryption.js +109 -38
- package/dist/modules/entities/api/encryption.js.map +2 -2
- package/dist/modules/entities/components/EncryptionManager.js +22 -9
- package/dist/modules/entities/components/EncryptionManager.js.map +2 -2
- package/dist/modules/integrations/api/[id]/credentials/route.js +23 -1
- package/dist/modules/integrations/api/[id]/credentials/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/route.js +10 -3
- package/dist/modules/integrations/api/[id]/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/state/route.js +20 -5
- package/dist/modules/integrations/api/[id]/state/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/version/route.js +17 -1
- package/dist/modules/integrations/api/[id]/version/route.js.map +2 -2
- package/dist/modules/integrations/api/route.js +2 -0
- package/dist/modules/integrations/api/route.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/[id]/page.js +36 -19
- package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js +13 -9
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/page.js +4 -3
- package/dist/modules/integrations/backend/integrations/page.js.map +2 -2
- package/dist/modules/integrations/data/entities.js +2 -2
- package/dist/modules/integrations/data/entities.js.map +2 -2
- package/dist/modules/integrations/lib/credentials-service.js +32 -0
- package/dist/modules/integrations/lib/credentials-service.js.map +2 -2
- package/dist/modules/integrations/lib/state-service.js +4 -2
- package/dist/modules/integrations/lib/state-service.js.map +2 -2
- package/dist/modules/notifications/lib/notificationService.js +42 -10
- package/dist/modules/notifications/lib/notificationService.js.map +2 -2
- package/dist/modules/payment_gateways/api/status/route.js +91 -6
- package/dist/modules/payment_gateways/api/status/route.js.map +2 -2
- package/dist/modules/payment_gateways/backend/payment-gateways/page.js +25 -11
- package/dist/modules/payment_gateways/backend/payment-gateways/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/offers/page.js +25 -7
- package/dist/modules/sales/backend/sales/channels/offers/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/page.js +25 -7
- package/dist/modules/sales/backend/sales/channels/page.js.map +2 -2
- package/dist/modules/sales/components/AdjustmentKindSettings.js +52 -24
- package/dist/modules/sales/components/AdjustmentKindSettings.js.map +2 -2
- package/dist/modules/sales/components/DocumentNumberSettings.js +26 -9
- package/dist/modules/sales/components/DocumentNumberSettings.js.map +2 -2
- package/dist/modules/sales/components/OrderEditingSettings.js +26 -9
- package/dist/modules/sales/components/OrderEditingSettings.js.map +2 -2
- package/dist/modules/sales/components/StatusSettings.js +66 -31
- package/dist/modules/sales/components/StatusSettings.js.map +2 -2
- package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js +25 -7
- package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js.map +2 -2
- package/dist/modules/shipping_carriers/api/cancel/route.js +38 -0
- package/dist/modules/shipping_carriers/api/cancel/route.js.map +2 -2
- package/dist/modules/shipping_carriers/api/shipments/route.js +12 -5
- package/dist/modules/shipping_carriers/api/shipments/route.js.map +2 -2
- package/dist/modules/staff/lib/timesheets-ui/TimerBar.js +46 -13
- package/dist/modules/staff/lib/timesheets-ui/TimerBar.js.map +2 -2
- package/dist/modules/staff/lib/timesheets-ui/timerErrors.js +12 -0
- package/dist/modules/staff/lib/timesheets-ui/timerErrors.js.map +7 -0
- package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js +43 -8
- package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/page.js +19 -10
- package/dist/modules/workflows/backend/definitions/page.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/commands/users.ts +1 -0
- package/src/modules/communication_channels/data/enrichers.ts +25 -0
- package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.tsx +7 -6
- package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.ts +5 -2
- package/src/modules/customers/api/pipeline-stages/reorder/route.ts +41 -1
- package/src/modules/customers/api/pipeline-stages/route.ts +112 -13
- package/src/modules/customers/api/pipelines/route.ts +112 -13
- package/src/modules/customers/api/settings/address-format/route.ts +36 -1
- package/src/modules/customers/api/tags/assign/route.ts +39 -0
- package/src/modules/customers/api/tags/unassign/route.ts +39 -0
- package/src/modules/customers/components/calendar/CalendarToolbar.tsx +4 -7
- package/src/modules/customers/components/calendar/EventBlock.tsx +2 -6
- package/src/modules/customers/components/calendar/UpcomingCards.tsx +9 -7
- 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/pl.json +1 -0
- package/src/modules/customers/lib/calendar/format.ts +25 -0
- package/src/modules/entities/api/encryption.ts +122 -39
- package/src/modules/entities/components/EncryptionManager.tsx +28 -9
- package/src/modules/integrations/api/[id]/credentials/route.ts +23 -0
- package/src/modules/integrations/api/[id]/route.ts +8 -1
- package/src/modules/integrations/api/[id]/state/route.ts +20 -4
- package/src/modules/integrations/api/[id]/version/route.ts +18 -1
- package/src/modules/integrations/api/route.ts +3 -0
- package/src/modules/integrations/backend/integrations/[id]/page.tsx +39 -20
- package/src/modules/integrations/backend/integrations/bundle/[id]/page.tsx +19 -11
- package/src/modules/integrations/backend/integrations/page.tsx +8 -5
- package/src/modules/integrations/data/entities.ts +2 -2
- package/src/modules/integrations/lib/credentials-service.ts +35 -0
- package/src/modules/integrations/lib/state-service.ts +3 -0
- package/src/modules/notifications/lib/notificationService.ts +74 -11
- package/src/modules/payment_gateways/api/status/route.ts +97 -5
- package/src/modules/payment_gateways/backend/payment-gateways/page.tsx +27 -11
- package/src/modules/sales/backend/sales/channels/offers/page.tsx +31 -7
- package/src/modules/sales/backend/sales/channels/page.tsx +31 -7
- package/src/modules/sales/components/AdjustmentKindSettings.tsx +60 -24
- package/src/modules/sales/components/DocumentNumberSettings.tsx +32 -10
- package/src/modules/sales/components/OrderEditingSettings.tsx +32 -10
- package/src/modules/sales/components/StatusSettings.tsx +74 -33
- package/src/modules/sales/components/channels/SalesChannelOffersPanel.tsx +30 -6
- package/src/modules/shipping_carriers/api/cancel/route.ts +46 -0
- package/src/modules/shipping_carriers/api/shipments/route.ts +21 -6
- package/src/modules/staff/i18n/de.json +5 -5
- package/src/modules/staff/i18n/es.json +5 -5
- package/src/modules/staff/i18n/pl.json +5 -5
- package/src/modules/staff/lib/timesheets-ui/TimerBar.tsx +57 -13
- package/src/modules/staff/lib/timesheets-ui/timerErrors.ts +23 -0
- package/src/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.tsx +57 -9
- package/src/modules/workflows/backend/definitions/page.tsx +19 -10
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { differenceInCalendarDays } from "date-fns/differenceInCalendarDays";
|
|
4
|
-
import { format } from "date-fns/format";
|
|
5
4
|
import { AlertTriangle, ChevronDown, Clock } from "lucide-react";
|
|
6
5
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
7
6
|
import { IconButton } from "@open-mercato/ui/primitives/icon-button";
|
|
@@ -12,10 +11,11 @@ import {
|
|
|
12
11
|
PopoverContent,
|
|
13
12
|
PopoverTrigger
|
|
14
13
|
} from "@open-mercato/ui/primitives/popover";
|
|
15
|
-
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
16
|
-
|
|
14
|
+
import { useLocale, useT } from "@open-mercato/shared/lib/i18n/context";
|
|
15
|
+
import { formatDateLabel, formatTimeRangeLabel } from "../../lib/calendar/format.js";
|
|
16
|
+
function formatTimeRange(locale, item, allDayLabel) {
|
|
17
17
|
if (item.allDay) return allDayLabel;
|
|
18
|
-
return
|
|
18
|
+
return formatTimeRangeLabel(locale, item.start, item.end);
|
|
19
19
|
}
|
|
20
20
|
function canJoin(item) {
|
|
21
21
|
return item.locationKind === "url" || item.locationKind === "platform";
|
|
@@ -26,6 +26,7 @@ function UpcomingCardStatus({
|
|
|
26
26
|
onSeeConflict
|
|
27
27
|
}) {
|
|
28
28
|
const t = useT();
|
|
29
|
+
const locale = useLocale();
|
|
29
30
|
const { item, kind, conflictCount } = card;
|
|
30
31
|
if (kind === "today") {
|
|
31
32
|
return /* @__PURE__ */ jsxs("div", { className: "flex w-full items-center gap-1.5 rounded-lg bg-status-success-bg py-2 pl-2 pr-3", children: [
|
|
@@ -67,14 +68,14 @@ function UpcomingCardStatus({
|
|
|
67
68
|
return /* @__PURE__ */ jsxs("div", { className: "flex w-full items-center gap-1.5 rounded-lg bg-status-error-bg py-2 pl-2 pr-3", children: [
|
|
68
69
|
/* @__PURE__ */ jsx(Clock, { className: "size-4 shrink-0 text-status-error-icon", "aria-hidden": "true" }),
|
|
69
70
|
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-xs font-medium text-foreground", children: t("customers.calendar.cards.cancelled", "Cancelled") }),
|
|
70
|
-
/* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs text-muted-foreground", children:
|
|
71
|
+
/* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs text-muted-foreground", children: formatDateLabel(locale, item.start) })
|
|
71
72
|
] });
|
|
72
73
|
}
|
|
73
74
|
const daysLater = Math.max(0, differenceInCalendarDays(item.start, /* @__PURE__ */ new Date()));
|
|
74
75
|
return /* @__PURE__ */ jsxs("div", { className: "flex w-full items-center gap-1.5 rounded-lg bg-muted py-2 pl-2 pr-3", children: [
|
|
75
76
|
/* @__PURE__ */ jsx(Clock, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": "true" }),
|
|
76
77
|
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-xs text-muted-foreground", children: daysLater === 1 ? t("customers.calendar.cards.dayLater", "1 day later") : t("customers.calendar.cards.daysLater", "{days} days later", { days: daysLater }) }),
|
|
77
|
-
/* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs text-muted-foreground", children:
|
|
78
|
+
/* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs text-muted-foreground", children: formatDateLabel(locale, item.start) })
|
|
78
79
|
] });
|
|
79
80
|
}
|
|
80
81
|
function UpcomingCardItem({
|
|
@@ -87,12 +88,13 @@ function UpcomingCardItem({
|
|
|
87
88
|
onCancel
|
|
88
89
|
}) {
|
|
89
90
|
const t = useT();
|
|
91
|
+
const locale = useLocale();
|
|
90
92
|
const { item } = card;
|
|
91
93
|
return /* @__PURE__ */ jsxs("article", { className: "flex w-[260px] shrink-0 snap-start flex-col gap-3 rounded-xl border bg-card px-2 pb-2 pt-4 shadow-xs sm:w-auto sm:shrink", children: [
|
|
92
94
|
/* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2 px-2", children: [
|
|
93
95
|
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1", children: [
|
|
94
96
|
/* @__PURE__ */ jsx("p", { className: "truncate text-sm font-medium text-foreground", children: item.title }),
|
|
95
|
-
/* @__PURE__ */ jsx("p", { className: "truncate text-xs text-muted-foreground", children: formatTimeRange(item, t("customers.calendar.cards.allDay", "All day")) })
|
|
97
|
+
/* @__PURE__ */ jsx("p", { className: "truncate text-xs text-muted-foreground", children: formatTimeRange(locale, item, t("customers.calendar.cards.allDay", "All day")) })
|
|
96
98
|
] }),
|
|
97
99
|
/* @__PURE__ */ jsxs(Popover, { children: [
|
|
98
100
|
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customers/components/calendar/UpcomingCards.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { differenceInCalendarDays } from 'date-fns/differenceInCalendarDays'\nimport {
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { differenceInCalendarDays } from 'date-fns/differenceInCalendarDays'\nimport { AlertTriangle, ChevronDown, Clock } from 'lucide-react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { LinkButton } from '@open-mercato/ui/primitives/link-button'\nimport {\n Popover,\n PopoverClose,\n PopoverContent,\n PopoverTrigger,\n} from '@open-mercato/ui/primitives/popover'\nimport { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'\nimport { formatDateLabel, formatTimeRangeLabel } from '../../lib/calendar/format'\nimport type { CalendarItem, UpcomingCard, UpcomingCardsProps } from './types'\n\nfunction formatTimeRange(locale: string, item: CalendarItem, allDayLabel: string): string {\n if (item.allDay) return allDayLabel\n return formatTimeRangeLabel(locale, item.start, item.end)\n}\n\nfunction canJoin(item: CalendarItem): boolean {\n return item.locationKind === 'url' || item.locationKind === 'platform'\n}\n\ntype CardCallbacks = Pick<UpcomingCardsProps, 'canManage' | 'onJoin' | 'onSeeConflict' | 'onOpen' | 'onEdit' | 'onCancel'>\n\nfunction UpcomingCardStatus({\n card,\n onJoin,\n onSeeConflict,\n}: {\n card: UpcomingCard\n onJoin: UpcomingCardsProps['onJoin']\n onSeeConflict: UpcomingCardsProps['onSeeConflict']\n}) {\n const t = useT()\n const locale = useLocale()\n const { item, kind, conflictCount } = card\n\n if (kind === 'today') {\n return (\n <div className=\"flex w-full items-center gap-1.5 rounded-lg bg-status-success-bg py-2 pl-2 pr-3\">\n <Clock className=\"size-4 shrink-0 text-status-success-icon\" aria-hidden=\"true\" />\n <span className=\"min-w-0 flex-1 truncate text-xs font-medium text-foreground\">\n {t('customers.calendar.cards.today', 'Today')}\n </span>\n {canJoin(item) ? (\n <LinkButton\n variant=\"gray\"\n size=\"sm\"\n underline=\"always\"\n onClick={() => onJoin(item)}\n className=\"shrink-0\"\n >\n {t('customers.calendar.cards.join', 'Join Meeting')}\n </LinkButton>\n ) : null}\n </div>\n )\n }\n\n if (kind === 'conflicted') {\n return (\n <div className=\"flex w-full items-center gap-1.5 rounded-lg bg-status-warning-bg py-2 pl-2 pr-3\">\n <AlertTriangle className=\"size-4 shrink-0 text-status-warning-icon\" aria-hidden=\"true\" />\n <span className=\"min-w-0 flex-1 truncate text-xs font-medium text-foreground\">\n {t('customers.calendar.cards.conflictedCount', '{count} Conflicted', {\n count: conflictCount,\n })}\n </span>\n <LinkButton\n variant=\"gray\"\n size=\"sm\"\n underline=\"always\"\n onClick={() => onSeeConflict(item)}\n className=\"shrink-0\"\n >\n {t('customers.calendar.cards.seeConflict', 'See Conflict')}\n </LinkButton>\n </div>\n )\n }\n\n if (kind === 'cancelled') {\n return (\n <div className=\"flex w-full items-center gap-1.5 rounded-lg bg-status-error-bg py-2 pl-2 pr-3\">\n <Clock className=\"size-4 shrink-0 text-status-error-icon\" aria-hidden=\"true\" />\n <span className=\"min-w-0 flex-1 truncate text-xs font-medium text-foreground\">\n {t('customers.calendar.cards.cancelled', 'Cancelled')}\n </span>\n <span className=\"shrink-0 text-xs text-muted-foreground\">\n {formatDateLabel(locale, item.start)}\n </span>\n </div>\n )\n }\n\n const daysLater = Math.max(0, differenceInCalendarDays(item.start, new Date()))\n return (\n <div className=\"flex w-full items-center gap-1.5 rounded-lg bg-muted py-2 pl-2 pr-3\">\n <Clock className=\"size-4 shrink-0 text-muted-foreground\" aria-hidden=\"true\" />\n <span className=\"min-w-0 flex-1 truncate text-xs text-muted-foreground\">\n {daysLater === 1\n ? t('customers.calendar.cards.dayLater', '1 day later')\n : t('customers.calendar.cards.daysLater', '{days} days later', { days: daysLater })}\n </span>\n <span className=\"shrink-0 text-xs text-muted-foreground\">\n {formatDateLabel(locale, item.start)}\n </span>\n </div>\n )\n}\n\nfunction UpcomingCardItem({\n card,\n canManage,\n onJoin,\n onSeeConflict,\n onOpen,\n onEdit,\n onCancel,\n}: { card: UpcomingCard } & CardCallbacks) {\n const t = useT()\n const locale = useLocale()\n const { item } = card\n\n return (\n <article className=\"flex w-[260px] shrink-0 snap-start flex-col gap-3 rounded-xl border bg-card px-2 pb-2 pt-4 shadow-xs sm:w-auto sm:shrink\">\n <div className=\"flex items-start gap-2 px-2\">\n <div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n <p className=\"truncate text-sm font-medium text-foreground\">{item.title}</p>\n <p className=\"truncate text-xs text-muted-foreground\">\n {formatTimeRange(locale, item, t('customers.calendar.cards.allDay', 'All day'))}\n </p>\n </div>\n <Popover>\n <PopoverTrigger asChild>\n <IconButton\n variant=\"outline\"\n size=\"xs\"\n fullRadius\n aria-label={t('customers.calendar.cards.menu.label', 'Event actions')}\n >\n <ChevronDown aria-hidden=\"true\" />\n </IconButton>\n </PopoverTrigger>\n <PopoverContent align=\"end\" className=\"w-40 min-w-0 p-1\">\n <PopoverClose asChild>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full justify-start\"\n onClick={() => onOpen(item)}\n >\n {t('customers.calendar.cards.menu.open', 'Open')}\n </Button>\n </PopoverClose>\n {canManage ? (\n <PopoverClose asChild>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full justify-start\"\n onClick={() => onEdit(item)}\n >\n {t('customers.calendar.cards.menu.edit', 'Edit')}\n </Button>\n </PopoverClose>\n ) : null}\n {canManage ? (\n <PopoverClose asChild>\n <Button\n type=\"button\"\n variant=\"destructive-ghost\"\n size=\"sm\"\n className=\"w-full justify-start\"\n onClick={() => onCancel(item)}\n >\n {t('customers.calendar.cards.menu.cancel', 'Cancel')}\n </Button>\n </PopoverClose>\n ) : null}\n </PopoverContent>\n </Popover>\n </div>\n <UpcomingCardStatus card={card} onJoin={onJoin} onSeeConflict={onSeeConflict} />\n </article>\n )\n}\n\nexport function UpcomingCards({\n cards,\n canManage = true,\n onJoin,\n onSeeConflict,\n onOpen,\n onEdit,\n onCancel,\n}: UpcomingCardsProps) {\n if (cards.length === 0) return null\n return (\n <div className=\"flex snap-x snap-mandatory gap-3 overflow-x-auto pb-1 sm:grid sm:snap-none sm:grid-cols-2 sm:gap-4 sm:overflow-visible sm:pb-0 xl:grid-cols-4\">\n {cards.map((card) => (\n <UpcomingCardItem\n key={card.item.id}\n card={card}\n canManage={canManage}\n onJoin={onJoin}\n onSeeConflict={onSeeConflict}\n onOpen={onOpen}\n onEdit={onEdit}\n onCancel={onCancel}\n />\n ))}\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const DATE_OPTIONS = { day: "numeric", month: "short", year: "numeric" };
|
|
2
|
+
const TIME_OPTIONS = { hour: "numeric", minute: "2-digit" };
|
|
3
|
+
function formatDateLabel(locale, date) {
|
|
4
|
+
return new Intl.DateTimeFormat(locale, DATE_OPTIONS).format(date);
|
|
5
|
+
}
|
|
6
|
+
function formatDateRangeLabel(locale, from, to) {
|
|
7
|
+
const formatter = new Intl.DateTimeFormat(locale, DATE_OPTIONS);
|
|
8
|
+
try {
|
|
9
|
+
return formatter.formatRange(from, to);
|
|
10
|
+
} catch {
|
|
11
|
+
return `${formatter.format(from)} \u2013 ${formatter.format(to)}`;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function formatTimeRangeLabel(locale, start, end) {
|
|
15
|
+
const formatter = new Intl.DateTimeFormat(locale, TIME_OPTIONS);
|
|
16
|
+
try {
|
|
17
|
+
return formatter.formatRange(start, end);
|
|
18
|
+
} catch {
|
|
19
|
+
return `${formatter.format(start)} \u2013 ${formatter.format(end)}`;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export {
|
|
23
|
+
formatDateLabel,
|
|
24
|
+
formatDateRangeLabel,
|
|
25
|
+
formatTimeRangeLabel
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=format.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/modules/customers/lib/calendar/format.ts"],
|
|
4
|
+
"sourcesContent": ["const DATE_OPTIONS: Intl.DateTimeFormatOptions = { day: 'numeric', month: 'short', year: 'numeric' }\n\nconst TIME_OPTIONS: Intl.DateTimeFormatOptions = { hour: 'numeric', minute: '2-digit' }\n\nexport function formatDateLabel(locale: string, date: Date): string {\n return new Intl.DateTimeFormat(locale, DATE_OPTIONS).format(date)\n}\n\nexport function formatDateRangeLabel(locale: string, from: Date, to: Date): string {\n const formatter = new Intl.DateTimeFormat(locale, DATE_OPTIONS)\n try {\n return formatter.formatRange(from, to)\n } catch {\n return `${formatter.format(from)} \u2013 ${formatter.format(to)}`\n }\n}\n\nexport function formatTimeRangeLabel(locale: string, start: Date, end: Date): string {\n const formatter = new Intl.DateTimeFormat(locale, TIME_OPTIONS)\n try {\n return formatter.formatRange(start, end)\n } catch {\n return `${formatter.format(start)} \u2013 ${formatter.format(end)}`\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,MAAM,eAA2C,EAAE,KAAK,WAAW,OAAO,SAAS,MAAM,UAAU;AAEnG,MAAM,eAA2C,EAAE,MAAM,WAAW,QAAQ,UAAU;AAE/E,SAAS,gBAAgB,QAAgB,MAAoB;AAClE,SAAO,IAAI,KAAK,eAAe,QAAQ,YAAY,EAAE,OAAO,IAAI;AAClE;AAEO,SAAS,qBAAqB,QAAgB,MAAY,IAAkB;AACjF,QAAM,YAAY,IAAI,KAAK,eAAe,QAAQ,YAAY;AAC9D,MAAI;AACF,WAAO,UAAU,YAAY,MAAM,EAAE;AAAA,EACvC,QAAQ;AACN,WAAO,GAAG,UAAU,OAAO,IAAI,CAAC,WAAM,UAAU,OAAO,EAAE,CAAC;AAAA,EAC5D;AACF;AAEO,SAAS,qBAAqB,QAAgB,OAAa,KAAmB;AACnF,QAAM,YAAY,IAAI,KAAK,eAAe,QAAQ,YAAY;AAC9D,MAAI;AACF,WAAO,UAAU,YAAY,OAAO,GAAG;AAAA,EACzC,QAAQ;AACN,WAAO,GAAG,UAAU,OAAO,KAAK,CAAC,WAAM,UAAU,OAAO,GAAG,CAAC;AAAA,EAC9D;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -4,6 +4,13 @@ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
|
4
4
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
5
5
|
import { EncryptionMap } from "@open-mercato/core/modules/entities/data/entities";
|
|
6
6
|
import { upsertEncryptionMapSchema } from "@open-mercato/core/modules/entities/data/validators";
|
|
7
|
+
import { isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
8
|
+
import { enforceCommandOptimisticLock } from "@open-mercato/shared/lib/crud/optimistic-lock-command";
|
|
9
|
+
import {
|
|
10
|
+
runCrudMutationGuardAfterSuccess,
|
|
11
|
+
validateCrudMutationGuard
|
|
12
|
+
} from "@open-mercato/shared/lib/crud/mutation-guard";
|
|
13
|
+
const ENCRYPTION_MAP_RESOURCE_KIND = "entities.encryption_map";
|
|
7
14
|
const metadata = {
|
|
8
15
|
GET: { requireAuth: true, requireFeatures: ["entities.definitions.manage"] },
|
|
9
16
|
POST: { requireAuth: true, requireFeatures: ["entities.definitions.manage"] }
|
|
@@ -14,6 +21,15 @@ function resolveScope(auth) {
|
|
|
14
21
|
organizationId: auth.orgId ?? null
|
|
15
22
|
};
|
|
16
23
|
}
|
|
24
|
+
function toIsoOrNull(value) {
|
|
25
|
+
if (value == null) return null;
|
|
26
|
+
if (value instanceof Date) {
|
|
27
|
+
const ms = value.getTime();
|
|
28
|
+
return Number.isFinite(ms) ? new Date(ms).toISOString() : null;
|
|
29
|
+
}
|
|
30
|
+
const trimmed = String(value).trim();
|
|
31
|
+
return trimmed.length ? trimmed : null;
|
|
32
|
+
}
|
|
17
33
|
async function GET(req) {
|
|
18
34
|
const url = new URL(req.url);
|
|
19
35
|
const entityId = url.searchParams.get("entityId") || "";
|
|
@@ -21,8 +37,8 @@ async function GET(req) {
|
|
|
21
37
|
const auth = await getAuthFromRequest(req);
|
|
22
38
|
if (!auth?.tenantId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
23
39
|
const { tenantId, organizationId } = resolveScope(auth);
|
|
24
|
-
const
|
|
25
|
-
const em = resolve("em");
|
|
40
|
+
const container = await createRequestContainer();
|
|
41
|
+
const em = container.resolve("em");
|
|
26
42
|
const repo = em.getRepository(EncryptionMap);
|
|
27
43
|
const candidates = [
|
|
28
44
|
{ entityId, tenantId, organizationId },
|
|
@@ -42,47 +58,99 @@ async function GET(req) {
|
|
|
42
58
|
tenantId,
|
|
43
59
|
organizationId,
|
|
44
60
|
fields: record?.fieldsJson ?? [],
|
|
45
|
-
isActive: record?.isActive ?? true
|
|
61
|
+
isActive: record?.isActive ?? true,
|
|
62
|
+
updatedAt: toIsoOrNull(record?.updatedAt)
|
|
46
63
|
});
|
|
47
64
|
}
|
|
48
65
|
async function POST(req) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
existing
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
try {
|
|
67
|
+
const body = await req.json().catch(() => ({}));
|
|
68
|
+
const parsed = upsertEncryptionMapSchema.safeParse(body);
|
|
69
|
+
if (!parsed.success) {
|
|
70
|
+
return NextResponse.json({ error: "Invalid payload", details: parsed.error.flatten() }, { status: 400 });
|
|
71
|
+
}
|
|
72
|
+
const auth = await getAuthFromRequest(req);
|
|
73
|
+
if (!auth?.tenantId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
74
|
+
const scope = resolveScope(auth);
|
|
75
|
+
const payload = parsed.data;
|
|
76
|
+
const tenantId = auth.tenantId;
|
|
77
|
+
const organizationId = scope.organizationId;
|
|
78
|
+
const container = await createRequestContainer();
|
|
79
|
+
const em = container.resolve("em");
|
|
80
|
+
const repo = em.getRepository(EncryptionMap);
|
|
81
|
+
const existing = await repo.findOne({ entityId: payload.entityId, tenantId, organizationId, deletedAt: null });
|
|
82
|
+
if (existing) {
|
|
83
|
+
enforceCommandOptimisticLock({
|
|
84
|
+
resourceKind: ENCRYPTION_MAP_RESOURCE_KIND,
|
|
85
|
+
resourceId: existing.id,
|
|
86
|
+
current: existing.updatedAt,
|
|
87
|
+
request: req
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
const guardResult = await validateCrudMutationGuard(container, {
|
|
72
91
|
tenantId,
|
|
73
92
|
organizationId,
|
|
74
|
-
|
|
75
|
-
|
|
93
|
+
userId: auth.sub,
|
|
94
|
+
resourceKind: ENCRYPTION_MAP_RESOURCE_KIND,
|
|
95
|
+
resourceId: existing?.id ?? payload.entityId,
|
|
96
|
+
operation: existing ? "update" : "create",
|
|
97
|
+
requestMethod: req.method,
|
|
98
|
+
requestHeaders: req.headers,
|
|
99
|
+
mutationPayload: payload
|
|
76
100
|
});
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
101
|
+
if (guardResult && !guardResult.ok) {
|
|
102
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status });
|
|
103
|
+
}
|
|
104
|
+
let saved;
|
|
105
|
+
if (existing) {
|
|
106
|
+
existing.fieldsJson = payload.fields;
|
|
107
|
+
existing.isActive = payload.isActive ?? true;
|
|
108
|
+
existing.updatedAt = /* @__PURE__ */ new Date();
|
|
109
|
+
await em.persist(existing).flush();
|
|
110
|
+
saved = existing;
|
|
111
|
+
} else {
|
|
112
|
+
const map = repo.create({
|
|
113
|
+
entityId: payload.entityId,
|
|
114
|
+
tenantId,
|
|
115
|
+
organizationId,
|
|
116
|
+
fieldsJson: payload.fields,
|
|
117
|
+
isActive: payload.isActive ?? true
|
|
118
|
+
});
|
|
119
|
+
await em.persist(map).flush();
|
|
120
|
+
saved = map;
|
|
121
|
+
}
|
|
122
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
123
|
+
await runCrudMutationGuardAfterSuccess(container, {
|
|
124
|
+
tenantId,
|
|
125
|
+
organizationId,
|
|
126
|
+
userId: auth.sub,
|
|
127
|
+
resourceKind: ENCRYPTION_MAP_RESOURCE_KIND,
|
|
128
|
+
resourceId: saved?.id ?? payload.entityId,
|
|
129
|
+
operation: existing ? "update" : "create",
|
|
130
|
+
requestMethod: req.method,
|
|
131
|
+
requestHeaders: req.headers,
|
|
132
|
+
metadata: guardResult.metadata ?? null
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
const svc = container.resolve("tenantEncryptionService");
|
|
137
|
+
await svc?.invalidateMap?.(payload.entityId, tenantId, organizationId);
|
|
138
|
+
} catch {
|
|
139
|
+
}
|
|
140
|
+
return NextResponse.json({ ok: true, updatedAt: toIsoOrNull(saved?.updatedAt) });
|
|
141
|
+
} catch (err) {
|
|
142
|
+
if (isCrudHttpError(err)) {
|
|
143
|
+
return NextResponse.json(err.body, { status: err.status });
|
|
144
|
+
}
|
|
145
|
+
throw err;
|
|
83
146
|
}
|
|
84
|
-
return NextResponse.json({ ok: true });
|
|
85
147
|
}
|
|
148
|
+
const conflictResponseSchema = z.object({
|
|
149
|
+
error: z.string(),
|
|
150
|
+
code: z.string(),
|
|
151
|
+
currentUpdatedAt: z.string(),
|
|
152
|
+
expectedUpdatedAt: z.string()
|
|
153
|
+
});
|
|
86
154
|
const openApi = {
|
|
87
155
|
tag: "Entities",
|
|
88
156
|
summary: "Manage encryption maps",
|
|
@@ -91,13 +159,16 @@ const openApi = {
|
|
|
91
159
|
summary: "Fetch encryption map",
|
|
92
160
|
description: "Returns the encrypted field map for the current tenant/organization scope.",
|
|
93
161
|
query: z.object({ entityId: z.string() }),
|
|
94
|
-
responses: [{ status: 200, description: "Map", schema: z.object({ entityId: z.string(), fields: z.array(z.object({ field: z.string(), hashField: z.string().nullable().optional() })), isActive: z.boolean().optional() }) }]
|
|
162
|
+
responses: [{ status: 200, description: "Map", schema: z.object({ entityId: z.string(), fields: z.array(z.object({ field: z.string(), hashField: z.string().nullable().optional() })), isActive: z.boolean().optional(), updatedAt: z.string().nullable().optional() }) }]
|
|
95
163
|
},
|
|
96
164
|
POST: {
|
|
97
165
|
summary: "Upsert encryption map",
|
|
98
|
-
description: "Creates or updates the encryption map for the current tenant/organization scope.",
|
|
166
|
+
description: "Creates or updates the encryption map for the current tenant/organization scope. Enforces optimistic locking when the caller sends the expected version header.",
|
|
99
167
|
requestBody: { contentType: "application/json", schema: upsertEncryptionMapSchema },
|
|
100
|
-
responses: [
|
|
168
|
+
responses: [
|
|
169
|
+
{ status: 200, description: "Saved", schema: z.object({ ok: z.boolean(), updatedAt: z.string().nullable().optional() }) },
|
|
170
|
+
{ status: 409, description: "Optimistic-lock conflict (stale write)", schema: conflictResponseSchema }
|
|
171
|
+
]
|
|
101
172
|
}
|
|
102
173
|
}
|
|
103
174
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/entities/api/encryption.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { EncryptionMap } from '@open-mercato/core/modules/entities/data/entities'\nimport { upsertEncryptionMapSchema } from '@open-mercato/core/modules/entities/data/validators'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['entities.definitions.manage'] },\n POST: { requireAuth: true, requireFeatures: ['entities.definitions.manage'] },\n}\n\nfunction resolveScope(auth: { tenantId?: string | null; orgId?: string | null }) {\n return {\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n }\n}\n\nexport async function GET(req: Request) {\n const url = new URL(req.url)\n const entityId = url.searchParams.get('entityId') || ''\n if (!entityId) return NextResponse.json({ error: 'entityId is required' }, { status: 400 })\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const { tenantId, organizationId } = resolveScope(auth)\n\n const
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,qBAAqB;AAC9B,SAAS,iCAAiC;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { EncryptionMap } from '@open-mercato/core/modules/entities/data/entities'\nimport { upsertEncryptionMapSchema } from '@open-mercato/core/modules/entities/data/validators'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\n\nconst ENCRYPTION_MAP_RESOURCE_KIND = 'entities.encryption_map'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['entities.definitions.manage'] },\n POST: { requireAuth: true, requireFeatures: ['entities.definitions.manage'] },\n}\n\nfunction resolveScope(auth: { tenantId?: string | null; orgId?: string | null }) {\n return {\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n }\n}\n\nfunction toIsoOrNull(value: Date | string | null | undefined): string | null {\n if (value == null) return null\n if (value instanceof Date) {\n const ms = value.getTime()\n return Number.isFinite(ms) ? new Date(ms).toISOString() : null\n }\n const trimmed = String(value).trim()\n return trimmed.length ? trimmed : null\n}\n\nexport async function GET(req: Request) {\n const url = new URL(req.url)\n const entityId = url.searchParams.get('entityId') || ''\n if (!entityId) return NextResponse.json({ error: 'entityId is required' }, { status: 400 })\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const { tenantId, organizationId } = resolveScope(auth)\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as any\n const repo = em.getRepository(EncryptionMap)\n // Prefer tenant+org, then tenant-global, then global\n const candidates = [\n { entityId, tenantId, organizationId },\n { entityId, tenantId, organizationId: null },\n { entityId, tenantId: null, organizationId: null },\n ]\n let record: any = null\n for (const where of candidates) {\n // eslint-disable-next-line no-await-in-loop\n const found = await repo.findOne({ ...where, deletedAt: null })\n if (found) {\n record = found\n break\n }\n }\n\n return NextResponse.json({\n entityId,\n tenantId,\n organizationId,\n fields: record?.fieldsJson ?? [],\n isActive: record?.isActive ?? true,\n updatedAt: toIsoOrNull(record?.updatedAt),\n })\n}\n\nexport async function POST(req: Request) {\n try {\n const body = await req.json().catch(() => ({}))\n const parsed = upsertEncryptionMapSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 400 })\n }\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const scope = resolveScope(auth)\n const payload = parsed.data\n const tenantId: string = auth.tenantId\n const organizationId = scope.organizationId\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as any\n const repo = em.getRepository(EncryptionMap)\n const existing = await repo.findOne({ entityId: payload.entityId, tenantId, organizationId, deletedAt: null })\n\n // Reject stale writes: a save started from an older tab must not silently\n // overwrite a newer encryption configuration. No-op when the client did not\n // send the expected-version header (strictly additive).\n if (existing) {\n enforceCommandOptimisticLock({\n resourceKind: ENCRYPTION_MAP_RESOURCE_KIND,\n resourceId: existing.id,\n current: existing.updatedAt,\n request: req,\n })\n }\n\n // Mutation-guard contract for custom write routes. The resource is the\n // encryption map for this entity scoped to the tenant/organization.\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId,\n organizationId,\n userId: auth.sub,\n resourceKind: ENCRYPTION_MAP_RESOURCE_KIND,\n resourceId: existing?.id ?? payload.entityId,\n operation: existing ? 'update' : 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: payload,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n let saved: any\n if (existing) {\n existing.fieldsJson = payload.fields\n existing.isActive = payload.isActive ?? true\n existing.updatedAt = new Date()\n await em.persist(existing).flush()\n saved = existing\n } else {\n const map = repo.create({\n entityId: payload.entityId,\n tenantId,\n organizationId,\n fieldsJson: payload.fields,\n isActive: payload.isActive ?? true,\n })\n await em.persist(map).flush()\n saved = map\n }\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId,\n organizationId,\n userId: auth.sub,\n resourceKind: ENCRYPTION_MAP_RESOURCE_KIND,\n resourceId: saved?.id ?? payload.entityId,\n operation: existing ? 'update' : 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n try {\n const svc = container.resolve('tenantEncryptionService') as { invalidateMap?: (e: string, t: string | null, o: string | null) => Promise<void> }\n await svc?.invalidateMap?.(payload.entityId, tenantId, organizationId)\n } catch {\n // best-effort cache bust\n }\n\n return NextResponse.json({ ok: true, updatedAt: toIsoOrNull(saved?.updatedAt) })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n throw err\n }\n}\n\nconst conflictResponseSchema = z.object({\n error: z.string(),\n code: z.string(),\n currentUpdatedAt: z.string(),\n expectedUpdatedAt: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Entities',\n summary: 'Manage encryption maps',\n methods: {\n GET: {\n summary: 'Fetch encryption map',\n description: 'Returns the encrypted field map for the current tenant/organization scope.',\n query: z.object({ entityId: z.string() }),\n responses: [{ status: 200, description: 'Map', schema: z.object({ entityId: z.string(), fields: z.array(z.object({ field: z.string(), hashField: z.string().nullable().optional() })), isActive: z.boolean().optional(), updatedAt: z.string().nullable().optional() }) }],\n },\n POST: {\n summary: 'Upsert encryption map',\n description: 'Creates or updates the encryption map for the current tenant/organization scope. Enforces optimistic locking when the caller sends the expected version header.',\n requestBody: { contentType: 'application/json', schema: upsertEncryptionMapSchema },\n responses: [\n { status: 200, description: 'Saved', schema: z.object({ ok: z.boolean(), updatedAt: z.string().nullable().optional() }) },\n { status: 409, description: 'Optimistic-lock conflict (stale write)', schema: conflictResponseSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -13,8 +13,10 @@ import {
|
|
|
13
13
|
} from "@open-mercato/ui/primitives/select";
|
|
14
14
|
import { LoadingMessage, ErrorMessage } from "@open-mercato/ui/backend/detail";
|
|
15
15
|
import { flash } from "@open-mercato/ui/backend/FlashMessages";
|
|
16
|
-
import { apiCall, readApiResultOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
|
|
16
|
+
import { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from "@open-mercato/ui/backend/utils/apiCall";
|
|
17
17
|
import { raiseCrudError } from "@open-mercato/ui/backend/utils/serverErrors";
|
|
18
|
+
import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuardedMutation";
|
|
19
|
+
import { buildOptimisticLockHeader } from "@open-mercato/ui/backend/utils/optimisticLock";
|
|
18
20
|
import { useCustomFieldDefs } from "@open-mercato/ui/backend/utils/customFieldDefs";
|
|
19
21
|
import { Plus, Save, Trash2 } from "lucide-react";
|
|
20
22
|
import { useOrganizationScopeVersion } from "@open-mercato/shared/lib/frontend/useOrganizationScope";
|
|
@@ -181,6 +183,7 @@ function EncryptionManager() {
|
|
|
181
183
|
setHasUserEdited(false);
|
|
182
184
|
lastMapSignatureRef.current = signature;
|
|
183
185
|
}, [mapSignature, map, canonicalOptions, hasUserEdited]);
|
|
186
|
+
const { runMutation } = useGuardedMutation({ contextId: "entities.encryption-map" });
|
|
184
187
|
const mutation = useMutation({
|
|
185
188
|
mutationFn: async () => {
|
|
186
189
|
const trimmed = fields.map((row) => ({
|
|
@@ -196,15 +199,25 @@ function EncryptionManager() {
|
|
|
196
199
|
if (!trimmed.length) {
|
|
197
200
|
throw new Error(t("entities.encryption.errors.noFields", "Add at least one field to encrypt"));
|
|
198
201
|
}
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
202
|
+
const payload = { entityId: selectedEntityId, fields: trimmed, isActive };
|
|
203
|
+
return runMutation({
|
|
204
|
+
operation: async () => {
|
|
205
|
+
const res = await withScopedApiRequestHeaders(
|
|
206
|
+
buildOptimisticLockHeader(map?.updatedAt ?? null),
|
|
207
|
+
() => apiCall("/api/entities/encryption", {
|
|
208
|
+
method: "POST",
|
|
209
|
+
headers: { "content-type": "application/json" },
|
|
210
|
+
body: JSON.stringify(payload)
|
|
211
|
+
})
|
|
212
|
+
);
|
|
213
|
+
if (!res.ok) {
|
|
214
|
+
await raiseCrudError(res.response, t("entities.encryption.errors.save", "Failed to save encryption map"));
|
|
215
|
+
}
|
|
216
|
+
return true;
|
|
217
|
+
},
|
|
218
|
+
context: {},
|
|
219
|
+
mutationPayload: payload
|
|
203
220
|
});
|
|
204
|
-
if (!res.ok) {
|
|
205
|
-
await raiseCrudError(res.response, t("entities.encryption.errors.save", "Failed to save encryption map"));
|
|
206
|
-
}
|
|
207
|
-
return true;
|
|
208
221
|
},
|
|
209
222
|
onSuccess: () => {
|
|
210
223
|
flash(t("entities.encryption.flash.saved", "Encryption map saved"), "success");
|