@open-mercato/core 0.10.1-develop.7983.1.fd91207efe → 0.10.1-develop.7990.1.82d0a183c5
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 +27 -23
- package/dist/modules/auth/commands/users.js.map +2 -2
- package/dist/modules/customers/commands/companies.js +10 -1
- package/dist/modules/customers/commands/companies.js.map +2 -2
- package/dist/modules/customers/docs/issue-8675-ownership-assessment.md +30 -0
- package/dist/modules/promotions/backend/promotions/[id]/page.js +3 -2
- package/dist/modules/promotions/backend/promotions/[id]/page.js.map +2 -2
- package/dist/modules/promotions/backend/promotions/page.js +3 -7
- package/dist/modules/promotions/backend/promotions/page.js.map +2 -2
- package/dist/modules/promotions/components/promotionDates.js +19 -0
- package/dist/modules/promotions/components/promotionDates.js.map +7 -0
- package/dist/modules/promotions/data/validators.js +8 -1
- package/dist/modules/promotions/data/validators.js.map +2 -2
- package/dist/modules/wms/components/backend/CompletePutawayTaskDialog.js +2 -2
- package/dist/modules/wms/components/backend/CompletePutawayTaskDialog.js.map +2 -2
- package/dist/modules/wms/components/backend/CreateAsnDialog.js +5 -5
- package/dist/modules/wms/components/backend/CreateAsnDialog.js.map +2 -2
- package/dist/modules/wms/components/backend/ReceiveAsnLineDialog.js +1 -1
- package/dist/modules/wms/components/backend/ReceiveAsnLineDialog.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/commands/users.ts +28 -25
- package/src/modules/customers/commands/companies.ts +11 -1
- package/src/modules/customers/docs/issue-8675-ownership-assessment.md +30 -0
- package/src/modules/promotions/backend/promotions/[id]/page.tsx +3 -2
- package/src/modules/promotions/backend/promotions/page.tsx +3 -8
- package/src/modules/promotions/components/promotionDates.ts +19 -0
- package/src/modules/promotions/data/validators.ts +9 -1
- package/src/modules/promotions/i18n/de.json +6 -6
- package/src/modules/promotions/i18n/es.json +6 -6
- package/src/modules/promotions/i18n/ko.json +6 -6
- package/src/modules/promotions/i18n/pl.json +6 -6
- package/src/modules/wms/components/backend/CompletePutawayTaskDialog.tsx +2 -2
- package/src/modules/wms/components/backend/CreateAsnDialog.tsx +5 -5
- package/src/modules/wms/components/backend/ReceiveAsnLineDialog.tsx +1 -1
- package/src/modules/wms/i18n/de.json +173 -165
- package/src/modules/wms/i18n/en.json +8 -0
- package/src/modules/wms/i18n/es.json +175 -167
- package/src/modules/wms/i18n/ko.json +175 -167
- package/src/modules/wms/i18n/pl.json +172 -164
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/promotions/backend/promotions/%5Bid%5D/page.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\n\n// Detail page (Step 1.7, SPEC-055 \u00A7UI/UX \"Promotion Form\"): the same three metadata\n// panels as the create page (Info, Settings, Exclusions) via `CrudForm`, plus the\n// fourth panel \u2014 \"Rules & Benefits\" \u2014 rendered by `RuleTreeBuilder` below it. The two\n// save surfaces are deliberately decoupled (CrudForm PUTs promotion metadata via\n// `promotions.promotion.update`; `RuleTreeBuilder` PUTs the tree via\n// `promotions.rule_tree.reconcile`, Step 1.4) \u2014 mirroring the pattern\n// `customers/backend/customers/people-v2/[id]/page.tsx` uses for an embedded `CrudForm`\n// alongside a fully custom section with its own independent save.\n\nimport * as React from 'react'\nimport { Page, PageBody, PageHeader } from '@open-mercato/ui/backend/Page'\nimport { CrudForm } from '@open-mercato/ui/backend/CrudForm'\nimport { updateCrud } from '@open-mercato/ui/backend/utils/crud'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { LoadingMessage, ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport {\n createPromotionFormFields,\n createPromotionFormGroups,\n createPromotionFormSchema,\n type PromotionFormValues,\n} from '../../../components/promotionFormFields'\nimport { RuleTreeBuilder } from '../../../components/RuleTreeBuilder'\n\ntype PromotionDetailResponse = {\n id: string\n name: string\n description: string | null\n active: boolean\n startsAt: string | null\n endsAt: string | null\n cumulative: boolean\n tags: string[]\n excludedTags: string[]\n excludeFlags: Record<string, boolean>\n eligibleCurrencies: string[]\n maxBudget: string | null\n budgetCurrency: string | null\n updatedAt: string\n}\n\nexport default function PromotionDetailPage({ params }: { params?: { id?: string } }) {\n const t = useT()\n // This app routes every backend page through a single catch-all\n // `/backend/[...slug]` route (`apps/mercato/src/app/(backend)/backend/[...slug]/page.tsx`),\n // which resolves the dynamic segment from its own manifest and passes it down\n // as a `params` prop \u2014 NOT as a Next.js router param. `useParams()` from\n // `next/navigation` only ever sees `{ slug: [...] }` here, never `{ id }`,\n // which is why using it made this page always render null (`people-v2/[id]`\n // is the house precedent for the `params` prop pattern).\n const promotionId = params?.id\n\n const [record, setRecord] = React.useState<PromotionDetailResponse | null>(null)\n const [isLoading, setIsLoading] = React.useState(true)\n const [loadError, setLoadError] = React.useState<string | null>(null)\n const [notFound, setNotFound] = React.useState(false)\n // Bumped after a successful CrudForm save so `record.updatedAt` (and therefore\n // `initialValues.updatedAt`) reflects the server's post-save version, same pattern as\n // `codes/[id]/page.tsx`'s own `reloadToken`. Fixes the stale-version 409 a second save\n // used to get, since `record` previously never refreshed after the first save.\n const [reloadToken, setReloadToken] = React.useState(0)\n // Bumped after `RuleTreeBuilder`'s own tree save, so the metadata `CrudForm` above also\n // picks up the new `updatedAt` the tree save just set on the promotion.\n const [treeVersionToken, setTreeVersionToken] = React.useState(0)\n\n const schema = React.useMemo(() => createPromotionFormSchema(t), [t])\n const fields = React.useMemo(() => createPromotionFormFields(t), [t])\n const groups = React.useMemo(() => createPromotionFormGroups(t), [t])\n\n React.useEffect(() => {\n if (!promotionId) return\n const id = promotionId\n let cancelled = false\n async function load() {\n setIsLoading(true)\n setLoadError(null)\n setNotFound(false)\n try {\n const call = await apiCall<PromotionDetailResponse>(`/api/promotions/${encodeURIComponent(id)}`)\n if (cancelled) return\n if (!call.ok) {\n if (call.status === 404) setNotFound(true)\n else setLoadError(t('promotions.detail.loadError', 'Failed to load the promotion'))\n return\n }\n setRecord(call.result ?? null)\n } catch {\n if (!cancelled) setLoadError(t('promotions.detail.loadError', 'Failed to load the promotion'))\n } finally {\n if (!cancelled) setIsLoading(false)\n }\n }\n load()\n return () => {\n cancelled = true\n }\n }, [promotionId, reloadToken, treeVersionToken, t])\n\n if (!promotionId) return null\n // Only block on the FIRST load. A reload triggered by `reloadToken`/`treeVersionToken`\n // after a save must not unmount the form \u2014 `record` is already populated, so it stays\n // rendered (with its pre-refresh values) while the refreshed version loads in the\n // background, same as `codes/[id]/page.tsx`'s own `isLoading && !record` guard.\n if (isLoading && !record) return <LoadingMessage label={t('promotions.detail.loading', 'Loading promotion...')} />\n if (notFound) {\n return (\n <RecordNotFoundState\n label={t('promotions.detail.notFound.title', 'Promotion not found')}\n description={t('promotions.detail.notFound.description', 'This promotion may have been deleted.')}\n backHref=\"/backend/promotions\"\n backLabel={t('promotions.detail.notFound.backLabel', 'Back to promotions')}\n />\n )\n }\n if (loadError || !record) return <ErrorMessage label={loadError ?? t('promotions.detail.loadError', 'Failed to load the promotion')} />\n\n const initialValues: Partial<PromotionFormValues> & { id: string; updatedAt: string } = {\n id: record.id,\n updatedAt: record.updatedAt,\n name: record.name,\n description: record.description,\n startsAt: record.startsAt,\n endsAt: record.endsAt,\n active: record.active,\n cumulative: record.cumulative,\n tags: record.tags,\n excludedTags: record.excludedTags,\n excludeFlags: { excludePromotionalPrices: record.excludeFlags?.excludePromotionalPrices ?? false },\n eligibleCurrencies: record.eligibleCurrencies ?? [],\n maxBudget: record.maxBudget ?? null,\n budgetCurrency: record.budgetCurrency ?? null,\n }\n\n return (\n <Page>\n <PageHeader\n title={record.name}\n description={t('promotions.detail.description', 'Edit promotion details and its rule tree.')}\n />\n <PageBody className=\"space-y-8\">\n <CrudForm<PromotionFormValues>\n fields={fields}\n groups={groups}\n initialValues={initialValues}\n schema={schema}\n submitLabel={t('promotions.form.submit', 'Save promotion')}\n cancelHref=\"/backend/promotions\"\n onSubmit={async (values) => {\n const payload = {\n id: record.id,\n name: values.name,\n description: values.description ?? null,\n startsAt: values.startsAt ?? null,\n endsAt: values.endsAt ?? null,\n active: values.active,\n cumulative: values.cumulative,\n tags: values.tags ?? [],\n excludedTags: values.excludedTags ?? [],\n excludeFlags: { excludePromotionalPrices: values.excludeFlags?.excludePromotionalPrices ?? false },\n eligibleCurrencies: values.eligibleCurrencies ?? [],\n maxBudget: values.maxBudget ?? null,\n budgetCurrency: values.budgetCurrency ?? null,\n }\n await updateCrud('promotions', payload)\n flash(t('promotions.form.success.update', 'Promotion saved'), 'success')\n setReloadToken((value) => value + 1)\n }}\n />\n\n <section className=\"space-y-3\">\n <h2 className=\"text-lg font-semibold text-foreground\">{t('promotions.tree.title', 'Rules & Benefits')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('promotions.tree.description', 'Define when this promotion activates and what it does.')}\n </p>\n <RuleTreeBuilder\n promotionId={record.id}\n refreshSignal={reloadToken}\n onSaved={() => setTreeVersionToken((value) => value + 1)}\n />\n </section>\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["'use client'\n\n// Detail page (Step 1.7, SPEC-055 \u00A7UI/UX \"Promotion Form\"): the same three metadata\n// panels as the create page (Info, Settings, Exclusions) via `CrudForm`, plus the\n// fourth panel \u2014 \"Rules & Benefits\" \u2014 rendered by `RuleTreeBuilder` below it. The two\n// save surfaces are deliberately decoupled (CrudForm PUTs promotion metadata via\n// `promotions.promotion.update`; `RuleTreeBuilder` PUTs the tree via\n// `promotions.rule_tree.reconcile`, Step 1.4) \u2014 mirroring the pattern\n// `customers/backend/customers/people-v2/[id]/page.tsx` uses for an embedded `CrudForm`\n// alongside a fully custom section with its own independent save.\n\nimport * as React from 'react'\nimport { Page, PageBody, PageHeader } from '@open-mercato/ui/backend/Page'\nimport { CrudForm } from '@open-mercato/ui/backend/CrudForm'\nimport { updateCrud } from '@open-mercato/ui/backend/utils/crud'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { LoadingMessage, ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport {\n createPromotionFormFields,\n createPromotionFormGroups,\n createPromotionFormSchema,\n type PromotionFormValues,\n} from '../../../components/promotionFormFields'\nimport { RuleTreeBuilder } from '../../../components/RuleTreeBuilder'\nimport { promotionCalendarDate } from '../../../components/promotionDates'\n\ntype PromotionDetailResponse = {\n id: string\n name: string\n description: string | null\n active: boolean\n startsAt: string | null\n endsAt: string | null\n cumulative: boolean\n tags: string[]\n excludedTags: string[]\n excludeFlags: Record<string, boolean>\n eligibleCurrencies: string[]\n maxBudget: string | null\n budgetCurrency: string | null\n updatedAt: string\n}\n\nexport default function PromotionDetailPage({ params }: { params?: { id?: string } }) {\n const t = useT()\n // This app routes every backend page through a single catch-all\n // `/backend/[...slug]` route (`apps/mercato/src/app/(backend)/backend/[...slug]/page.tsx`),\n // which resolves the dynamic segment from its own manifest and passes it down\n // as a `params` prop \u2014 NOT as a Next.js router param. `useParams()` from\n // `next/navigation` only ever sees `{ slug: [...] }` here, never `{ id }`,\n // which is why using it made this page always render null (`people-v2/[id]`\n // is the house precedent for the `params` prop pattern).\n const promotionId = params?.id\n\n const [record, setRecord] = React.useState<PromotionDetailResponse | null>(null)\n const [isLoading, setIsLoading] = React.useState(true)\n const [loadError, setLoadError] = React.useState<string | null>(null)\n const [notFound, setNotFound] = React.useState(false)\n // Bumped after a successful CrudForm save so `record.updatedAt` (and therefore\n // `initialValues.updatedAt`) reflects the server's post-save version, same pattern as\n // `codes/[id]/page.tsx`'s own `reloadToken`. Fixes the stale-version 409 a second save\n // used to get, since `record` previously never refreshed after the first save.\n const [reloadToken, setReloadToken] = React.useState(0)\n // Bumped after `RuleTreeBuilder`'s own tree save, so the metadata `CrudForm` above also\n // picks up the new `updatedAt` the tree save just set on the promotion.\n const [treeVersionToken, setTreeVersionToken] = React.useState(0)\n\n const schema = React.useMemo(() => createPromotionFormSchema(t), [t])\n const fields = React.useMemo(() => createPromotionFormFields(t), [t])\n const groups = React.useMemo(() => createPromotionFormGroups(t), [t])\n\n React.useEffect(() => {\n if (!promotionId) return\n const id = promotionId\n let cancelled = false\n async function load() {\n setIsLoading(true)\n setLoadError(null)\n setNotFound(false)\n try {\n const call = await apiCall<PromotionDetailResponse>(`/api/promotions/${encodeURIComponent(id)}`)\n if (cancelled) return\n if (!call.ok) {\n if (call.status === 404) setNotFound(true)\n else setLoadError(t('promotions.detail.loadError', 'Failed to load the promotion'))\n return\n }\n setRecord(call.result ?? null)\n } catch {\n if (!cancelled) setLoadError(t('promotions.detail.loadError', 'Failed to load the promotion'))\n } finally {\n if (!cancelled) setIsLoading(false)\n }\n }\n load()\n return () => {\n cancelled = true\n }\n }, [promotionId, reloadToken, treeVersionToken, t])\n\n if (!promotionId) return null\n // Only block on the FIRST load. A reload triggered by `reloadToken`/`treeVersionToken`\n // after a save must not unmount the form \u2014 `record` is already populated, so it stays\n // rendered (with its pre-refresh values) while the refreshed version loads in the\n // background, same as `codes/[id]/page.tsx`'s own `isLoading && !record` guard.\n if (isLoading && !record) return <LoadingMessage label={t('promotions.detail.loading', 'Loading promotion...')} />\n if (notFound) {\n return (\n <RecordNotFoundState\n label={t('promotions.detail.notFound.title', 'Promotion not found')}\n description={t('promotions.detail.notFound.description', 'This promotion may have been deleted.')}\n backHref=\"/backend/promotions\"\n backLabel={t('promotions.detail.notFound.backLabel', 'Back to promotions')}\n />\n )\n }\n if (loadError || !record) return <ErrorMessage label={loadError ?? t('promotions.detail.loadError', 'Failed to load the promotion')} />\n\n const initialValues: Partial<PromotionFormValues> & { id: string; updatedAt: string } = {\n id: record.id,\n updatedAt: record.updatedAt,\n name: record.name,\n description: record.description,\n startsAt: promotionCalendarDate(record.startsAt),\n endsAt: promotionCalendarDate(record.endsAt),\n active: record.active,\n cumulative: record.cumulative,\n tags: record.tags,\n excludedTags: record.excludedTags,\n excludeFlags: { excludePromotionalPrices: record.excludeFlags?.excludePromotionalPrices ?? false },\n eligibleCurrencies: record.eligibleCurrencies ?? [],\n maxBudget: record.maxBudget ?? null,\n budgetCurrency: record.budgetCurrency ?? null,\n }\n\n return (\n <Page>\n <PageHeader\n title={record.name}\n description={t('promotions.detail.description', 'Edit promotion details and its rule tree.')}\n />\n <PageBody className=\"space-y-8\">\n <CrudForm<PromotionFormValues>\n fields={fields}\n groups={groups}\n initialValues={initialValues}\n schema={schema}\n submitLabel={t('promotions.form.submit', 'Save promotion')}\n cancelHref=\"/backend/promotions\"\n onSubmit={async (values) => {\n const payload = {\n id: record.id,\n name: values.name,\n description: values.description ?? null,\n startsAt: values.startsAt ?? null,\n endsAt: values.endsAt ?? null,\n active: values.active,\n cumulative: values.cumulative,\n tags: values.tags ?? [],\n excludedTags: values.excludedTags ?? [],\n excludeFlags: { excludePromotionalPrices: values.excludeFlags?.excludePromotionalPrices ?? false },\n eligibleCurrencies: values.eligibleCurrencies ?? [],\n maxBudget: values.maxBudget ?? null,\n budgetCurrency: values.budgetCurrency ?? null,\n }\n await updateCrud('promotions', payload)\n flash(t('promotions.form.success.update', 'Promotion saved'), 'success')\n setReloadToken((value) => value + 1)\n }}\n />\n\n <section className=\"space-y-3\">\n <h2 className=\"text-lg font-semibold text-foreground\">{t('promotions.tree.title', 'Rules & Benefits')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('promotions.tree.description', 'Define when this promotion activates and what it does.')}\n </p>\n <RuleTreeBuilder\n promotionId={record.id}\n refreshSignal={reloadToken}\n onSaved={() => setTreeVersionToken((value) => value + 1)}\n />\n </section>\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
|
+
"mappings": ";AA2GmC,cAkE3B,YAlE2B;AAhGnC,YAAY,WAAW;AACvB,SAAS,MAAM,UAAU,kBAAkB;AAC3C,SAAS,gBAAgB;AACzB,SAAS,kBAAkB;AAC3B,SAAS,eAAe;AACxB,SAAS,gBAAgB,cAAc,2BAA2B;AAClE,SAAS,YAAY;AACrB,SAAS,aAAa;AACtB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,uBAAuB;AAChC,SAAS,6BAA6B;AAmBvB,SAAR,oBAAqC,EAAE,OAAO,GAAiC;AACpF,QAAM,IAAI,KAAK;AAQf,QAAM,cAAc,QAAQ;AAE5B,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAAyC,IAAI;AAC/E,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAS,IAAI;AACrD,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAwB,IAAI;AACpE,QAAM,CAAC,UAAU,WAAW,IAAI,MAAM,SAAS,KAAK;AAKpD,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAAS,CAAC;AAGtD,QAAM,CAAC,kBAAkB,mBAAmB,IAAI,MAAM,SAAS,CAAC;AAEhE,QAAM,SAAS,MAAM,QAAQ,MAAM,0BAA0B,CAAC,GAAG,CAAC,CAAC,CAAC;AACpE,QAAM,SAAS,MAAM,QAAQ,MAAM,0BAA0B,CAAC,GAAG,CAAC,CAAC,CAAC;AACpE,QAAM,SAAS,MAAM,QAAQ,MAAM,0BAA0B,CAAC,GAAG,CAAC,CAAC,CAAC;AAEpE,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,YAAa;AAClB,UAAM,KAAK;AACX,QAAI,YAAY;AAChB,mBAAe,OAAO;AACpB,mBAAa,IAAI;AACjB,mBAAa,IAAI;AACjB,kBAAY,KAAK;AACjB,UAAI;AACF,cAAM,OAAO,MAAM,QAAiC,mBAAmB,mBAAmB,EAAE,CAAC,EAAE;AAC/F,YAAI,UAAW;AACf,YAAI,CAAC,KAAK,IAAI;AACZ,cAAI,KAAK,WAAW,IAAK,aAAY,IAAI;AAAA,cACpC,cAAa,EAAE,+BAA+B,8BAA8B,CAAC;AAClF;AAAA,QACF;AACA,kBAAU,KAAK,UAAU,IAAI;AAAA,MAC/B,QAAQ;AACN,YAAI,CAAC,UAAW,cAAa,EAAE,+BAA+B,8BAA8B,CAAC;AAAA,MAC/F,UAAE;AACA,YAAI,CAAC,UAAW,cAAa,KAAK;AAAA,MACpC;AAAA,IACF;AACA,SAAK;AACL,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,aAAa,aAAa,kBAAkB,CAAC,CAAC;AAElD,MAAI,CAAC,YAAa,QAAO;AAKzB,MAAI,aAAa,CAAC,OAAQ,QAAO,oBAAC,kBAAe,OAAO,EAAE,6BAA6B,sBAAsB,GAAG;AAChH,MAAI,UAAU;AACZ,WACE;AAAA,MAAC;AAAA;AAAA,QACC,OAAO,EAAE,oCAAoC,qBAAqB;AAAA,QAClE,aAAa,EAAE,0CAA0C,uCAAuC;AAAA,QAChG,UAAS;AAAA,QACT,WAAW,EAAE,wCAAwC,oBAAoB;AAAA;AAAA,IAC3E;AAAA,EAEJ;AACA,MAAI,aAAa,CAAC,OAAQ,QAAO,oBAAC,gBAAa,OAAO,aAAa,EAAE,+BAA+B,8BAA8B,GAAG;AAErI,QAAM,gBAAkF;AAAA,IACtF,IAAI,OAAO;AAAA,IACX,WAAW,OAAO;AAAA,IAClB,MAAM,OAAO;AAAA,IACb,aAAa,OAAO;AAAA,IACpB,UAAU,sBAAsB,OAAO,QAAQ;AAAA,IAC/C,QAAQ,sBAAsB,OAAO,MAAM;AAAA,IAC3C,QAAQ,OAAO;AAAA,IACf,YAAY,OAAO;AAAA,IACnB,MAAM,OAAO;AAAA,IACb,cAAc,OAAO;AAAA,IACrB,cAAc,EAAE,0BAA0B,OAAO,cAAc,4BAA4B,MAAM;AAAA,IACjG,oBAAoB,OAAO,sBAAsB,CAAC;AAAA,IAClD,WAAW,OAAO,aAAa;AAAA,IAC/B,gBAAgB,OAAO,kBAAkB;AAAA,EAC3C;AAEA,SACE,qBAAC,QACC;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,OAAO,OAAO;AAAA,QACd,aAAa,EAAE,iCAAiC,2CAA2C;AAAA;AAAA,IAC7F;AAAA,IACA,qBAAC,YAAS,WAAU,aAClB;AAAA;AAAA,QAAC;AAAA;AAAA,UACC;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA,aAAa,EAAE,0BAA0B,gBAAgB;AAAA,UACzD,YAAW;AAAA,UACX,UAAU,OAAO,WAAW;AAC1B,kBAAM,UAAU;AAAA,cACd,IAAI,OAAO;AAAA,cACX,MAAM,OAAO;AAAA,cACb,aAAa,OAAO,eAAe;AAAA,cACnC,UAAU,OAAO,YAAY;AAAA,cAC7B,QAAQ,OAAO,UAAU;AAAA,cACzB,QAAQ,OAAO;AAAA,cACf,YAAY,OAAO;AAAA,cACnB,MAAM,OAAO,QAAQ,CAAC;AAAA,cACtB,cAAc,OAAO,gBAAgB,CAAC;AAAA,cACtC,cAAc,EAAE,0BAA0B,OAAO,cAAc,4BAA4B,MAAM;AAAA,cACjG,oBAAoB,OAAO,sBAAsB,CAAC;AAAA,cAClD,WAAW,OAAO,aAAa;AAAA,cAC/B,gBAAgB,OAAO,kBAAkB;AAAA,YAC3C;AACA,kBAAM,WAAW,cAAc,OAAO;AACtC,kBAAM,EAAE,kCAAkC,iBAAiB,GAAG,SAAS;AACvE,2BAAe,CAAC,UAAU,QAAQ,CAAC;AAAA,UACrC;AAAA;AAAA,MACF;AAAA,MAEA,qBAAC,aAAQ,WAAU,aACjB;AAAA,4BAAC,QAAG,WAAU,yCAAyC,YAAE,yBAAyB,kBAAkB,GAAE;AAAA,QACtG,oBAAC,OAAE,WAAU,iCACV,YAAE,+BAA+B,wDAAwD,GAC5F;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,aAAa,OAAO;AAAA,YACpB,eAAe;AAAA,YACf,SAAS,MAAM,oBAAoB,CAAC,UAAU,QAAQ,CAAC;AAAA;AAAA,QACzD;AAAA,SACF;AAAA,OACF;AAAA,KACF;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -39,17 +39,13 @@ import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuarde
|
|
|
39
39
|
import { flash } from "@open-mercato/ui/backend/FlashMessages";
|
|
40
40
|
import { useConfirmDialog } from "@open-mercato/ui/backend/confirm-dialog";
|
|
41
41
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
42
|
+
import { formatPromotionCalendarDate } from "../../components/promotionDates.js";
|
|
42
43
|
const PAGE_SIZE = 50;
|
|
43
44
|
const RESOURCE_KIND = "promotions.promotion";
|
|
44
45
|
function sameOrder(a, b) {
|
|
45
46
|
if (a.length !== b.length) return false;
|
|
46
47
|
return a.every((row, index) => row.id === b[index]?.id);
|
|
47
48
|
}
|
|
48
|
-
function formatDate(value) {
|
|
49
|
-
if (!value) return null;
|
|
50
|
-
const date = new Date(value);
|
|
51
|
-
return Number.isNaN(date.getTime()) ? null : date.toLocaleDateString();
|
|
52
|
-
}
|
|
53
49
|
function isFilterValueActive(value) {
|
|
54
50
|
if (value == null) return false;
|
|
55
51
|
if (Array.isArray(value)) return value.length > 0;
|
|
@@ -78,8 +74,8 @@ function PromotionCard({
|
|
|
78
74
|
transform: CSS.Transform.toString(transform),
|
|
79
75
|
transition
|
|
80
76
|
};
|
|
81
|
-
const startsLabel =
|
|
82
|
-
const endsLabel =
|
|
77
|
+
const startsLabel = formatPromotionCalendarDate(row.startsAt);
|
|
78
|
+
const endsLabel = formatPromotionCalendarDate(row.endsAt);
|
|
83
79
|
const windowLabel = startsLabel || endsLabel ? [
|
|
84
80
|
startsLabel ? t("promotions.list.card.starts", "Starts {date}", { date: startsLabel }) : null,
|
|
85
81
|
endsLabel ? t("promotions.list.card.ends", "Ends {date}", { date: endsLabel }) : null
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/promotions/backend/promotions/page.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport {\n DndContext,\n closestCenter,\n KeyboardSensor,\n PointerSensor,\n useSensor,\n useSensors,\n type DragEndEvent,\n} from '@dnd-kit/core'\nimport {\n SortableContext,\n sortableKeyboardCoordinates,\n verticalListSortingStrategy,\n useSortable,\n} from '@dnd-kit/sortable'\nimport { CSS } from '@dnd-kit/utilities'\nimport { ArrowDown, ArrowUp, GripVertical, Plus } from 'lucide-react'\nimport { Page, PageBody, PageHeader } from '@open-mercato/ui/backend/Page'\nimport { Card, CardContent } from '@open-mercato/ui/primitives/card'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Switch } from '@open-mercato/ui/primitives/switch'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { Alert } from '@open-mercato/ui/primitives/alert'\nimport { Pagination } from '@open-mercato/ui/primitives/pagination'\nimport { FilterBar } from '@open-mercato/ui/backend/FilterBar'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { ListEmptyState } from '@open-mercato/ui/backend/filters/ListEmptyState'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { useBackendChrome } from '@open-mercato/ui/backend/BackendChromeProvider'\nimport { hasFeature } from '@open-mercato/shared/security/features'\nimport { apiCall, apiCallOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader, extractOptimisticLockConflict } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\nconst PAGE_SIZE = 50\nconst RESOURCE_KIND = 'promotions.promotion'\n\ntype PromotionRow = {\n id: string\n name: string\n description: string | null\n order: number\n active: boolean\n startsAt: string | null\n endsAt: string | null\n cumulative: boolean\n tags: string[]\n excludedTags: string[]\n updatedAt: string | null\n}\n\ntype ListResponse = {\n items: PromotionRow[]\n total: number\n page: number\n totalPages: number\n totalIsCapped?: boolean\n}\n\ntype OrderPatchResponse = {\n updated: string[]\n conflicts: { id: string; currentUpdatedAt: string }[]\n}\n\n// Whether the current working copy's id sequence differs from the server's \u2014\n// the cheapest correct test for \"has the display order changed\" since both\n// arrays always carry the same ids (no row is added/removed locally).\nfunction sameOrder(a: PromotionRow[], b: PromotionRow[]): boolean {\n if (a.length !== b.length) return false\n return a.every((row, index) => row.id === b[index]?.id)\n}\n\nfunction formatDate(value: string | null): string | null {\n if (!value) return null\n const date = new Date(value)\n return Number.isNaN(date.getTime()) ? null : date.toLocaleDateString()\n}\n\nfunction isFilterValueActive(value: unknown): boolean {\n if (value == null) return false\n if (Array.isArray(value)) return value.length > 0\n if (typeof value === 'object') return Object.values(value).some((entry) => entry != null && entry !== '')\n return value !== ''\n}\n\ntype PromotionCardProps = {\n row: PromotionRow\n canDrag: boolean\n canEdit: boolean\n canDelete: boolean\n isFirst: boolean\n isLast: boolean\n onToggleActive: (checked: boolean) => void\n onToggleCumulative: (checked: boolean) => void\n onMoveUp: () => void\n onMoveDown: () => void\n onDelete: () => void\n}\n\n// One promotion card. `useSortable` is always called (dnd-kit requires a\n// stable hook per rendered id) but drag is inert via `disabled` when the\n// caller lacks `promotions.update` \u2014 the handle and up/down buttons are also\n// hidden in that case, matching the DataTable-row precedent in\n// `customer_groups/backend/customer-groups/page.tsx`.\nfunction PromotionCard({\n row,\n canDrag,\n canEdit,\n canDelete,\n isFirst,\n isLast,\n onToggleActive,\n onToggleCumulative,\n onMoveUp,\n onMoveDown,\n onDelete,\n}: PromotionCardProps) {\n const t = useT()\n const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({\n id: row.id,\n disabled: !canDrag,\n })\n const style: React.CSSProperties = {\n transform: CSS.Transform.toString(transform),\n transition,\n }\n const startsLabel = formatDate(row.startsAt)\n const endsLabel = formatDate(row.endsAt)\n const windowLabel = startsLabel || endsLabel\n ? [\n startsLabel ? t('promotions.list.card.starts', 'Starts {date}', { date: startsLabel }) : null,\n endsLabel ? t('promotions.list.card.ends', 'Ends {date}', { date: endsLabel }) : null,\n ]\n .filter(Boolean)\n .join(' \u00B7 ')\n : t('promotions.list.card.noWindow', 'No date restriction')\n\n return (\n <div ref={setNodeRef} style={style}>\n <Card className={isDragging ? 'border-primary shadow-lg' : undefined}>\n <CardContent className=\"space-y-3 p-4\">\n <div className=\"flex items-start gap-3\">\n {canDrag ? (\n <div className=\"flex shrink-0 flex-col items-center gap-1 pt-0.5\">\n <span\n className=\"inline-flex cursor-grab items-center text-muted-foreground active:cursor-grabbing\"\n aria-label={t('promotions.list.card.dragHandle', 'Reorder')}\n {...attributes}\n {...listeners}\n >\n <GripVertical className=\"size-4\" />\n </span>\n <div className=\"flex flex-col gap-0.5\">\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n aria-label={t('promotions.list.card.moveUp', 'Move up')}\n disabled={isFirst}\n onClick={onMoveUp}\n >\n <ArrowUp className=\"size-3.5\" />\n </IconButton>\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n aria-label={t('promotions.list.card.moveDown', 'Move down')}\n disabled={isLast}\n onClick={onMoveDown}\n >\n <ArrowDown className=\"size-3.5\" />\n </IconButton>\n </div>\n </div>\n ) : null}\n <div className=\"min-w-0 flex-1 space-y-1.5\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"truncate font-medium\">{row.name}</span>\n <span className=\"text-xs text-muted-foreground\">{windowLabel}</span>\n </div>\n {row.description ? (\n <p className=\"truncate text-xs text-muted-foreground\">{row.description}</p>\n ) : null}\n {row.tags.length > 0 ? (\n <div className=\"flex flex-wrap gap-1.5\">\n {row.tags.map((tag) => (\n <Tag key={tag} variant=\"neutral\">\n {tag}\n </Tag>\n ))}\n </div>\n ) : null}\n </div>\n </div>\n <div className=\"flex flex-wrap items-center justify-between gap-3 border-t border-border pt-3\">\n <div className=\"flex items-center gap-4\">\n <label className=\"flex items-center gap-2 text-sm\">\n <Switch checked={row.active} onCheckedChange={onToggleActive} disabled={!canEdit} />\n {t('promotions.list.card.active', 'Active')}\n </label>\n <label className=\"flex items-center gap-2 text-sm\">\n <Switch checked={row.cumulative} onCheckedChange={onToggleCumulative} disabled={!canEdit} />\n {t('promotions.list.card.cumulative', 'Cumulative')}\n </label>\n </div>\n <div className=\"flex items-center gap-2\">\n {canEdit ? (\n <Button asChild variant=\"outline\" size=\"sm\">\n <Link href={`/backend/promotions/${row.id}`}>{t('promotions.list.card.edit', 'Edit')}</Link>\n </Button>\n ) : null}\n {canDelete ? (\n <Button type=\"button\" variant=\"destructive\" size=\"sm\" onClick={onDelete}>\n {t('promotions.list.card.delete', 'Delete')}\n </Button>\n ) : null}\n </div>\n </div>\n </CardContent>\n </Card>\n </div>\n )\n}\n\nexport default function PromotionsListPage() {\n const t = useT()\n const { confirm: confirmDialog, ConfirmDialogElement } = useConfirmDialog()\n const { payload: chromePayload, isReady: chromeReady } = useBackendChrome()\n const canCreate = chromeReady && hasFeature(chromePayload?.grantedFeatures, 'promotions.create')\n const canUpdate = chromeReady && hasFeature(chromePayload?.grantedFeatures, 'promotions.update')\n const canDelete = chromeReady && hasFeature(chromePayload?.grantedFeatures, 'promotions.delete')\n\n const [rows, setRows] = React.useState<PromotionRow[]>([])\n const [serverRows, setServerRows] = React.useState<PromotionRow[]>([])\n const [page, setPage] = React.useState(1)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [totalIsCapped, setTotalIsCapped] = React.useState(false)\n const [search, setSearch] = React.useState('')\n const [filterValues, setFilterValues] = React.useState<FilterValues>({})\n const [isLoading, setIsLoading] = React.useState(true)\n const [loadError, setLoadError] = React.useState<string | null>(null)\n const [isSaving, setIsSaving] = React.useState(false)\n const [reloadToken, setReloadToken] = React.useState(0)\n\n const mutationContextId = 'promotions-list:mutation'\n const { runMutation, retryLastMutation } = useGuardedMutation<{\n formId: string\n resourceKind: string\n resourceId: string\n retryLastMutation: () => Promise<boolean>\n }>({\n contextId: mutationContextId,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setIsLoading(true)\n setLoadError(null)\n try {\n const params = new URLSearchParams()\n params.set('page', String(page))\n params.set('pageSize', String(PAGE_SIZE))\n // Drag-sort and the Up/Down buttons both rely on the displayed order\n // matching `Promotion.order` ascending.\n params.set('sortField', 'order')\n params.set('sortDir', 'asc')\n if (search) params.set('search', search)\n if (filterValues.active === 'true' || filterValues.active === 'false') {\n params.set('active', filterValues.active)\n }\n if (filterValues.cumulative === 'true' || filterValues.cumulative === 'false') {\n params.set('cumulative', filterValues.cumulative)\n }\n const dateRange = filterValues.startsWindow as { from?: string; to?: string } | undefined\n if (dateRange?.from) params.set('startsFrom', dateRange.from)\n if (dateRange?.to) params.set('startsTo', dateRange.to)\n const tagList = Array.isArray(filterValues.tags)\n ? filterValues.tags.filter((entry: unknown): entry is string => typeof entry === 'string')\n : []\n if (tagList.length) params.set('tags', tagList.join(','))\n\n const fallback: ListResponse = { items: [], total: 0, page: 1, totalPages: 1 }\n const call = await apiCall<ListResponse>(`/api/promotions?${params.toString()}`, undefined, { fallback })\n if (cancelled) return\n if (!call.ok) {\n setRows([])\n setServerRows([])\n setLoadError(t('promotions.list.errors.loadFailed', 'Failed to load promotions'))\n return\n }\n const payload = call.result ?? fallback\n const items = Array.isArray(payload.items) ? payload.items : []\n setRows(items)\n setServerRows(items)\n setTotal(typeof payload.total === 'number' ? payload.total : items.length)\n setTotalPages(typeof payload.totalPages === 'number' && payload.totalPages > 0 ? payload.totalPages : 1)\n setTotalIsCapped(payload.totalIsCapped === true)\n } catch {\n if (!cancelled) {\n setRows([])\n setServerRows([])\n setLoadError(t('promotions.list.errors.loadFailed', 'Failed to load promotions'))\n }\n } finally {\n if (!cancelled) setIsLoading(false)\n }\n }\n load()\n return () => {\n cancelled = true\n }\n }, [page, search, filterValues, reloadToken, t])\n\n const handleSearchChange = React.useCallback((next: string) => {\n setSearch(next)\n setPage(1)\n }, [])\n\n const handleFiltersApply = React.useCallback((next: FilterValues) => {\n setFilterValues(next)\n setPage(1)\n }, [])\n\n const isFiltered = Boolean(search) || Object.values(filterValues).some(isFilterValueActive)\n const orderDirty = !sameOrder(rows, serverRows)\n const toggleDirty = rows.some((row) => {\n const server = serverRows.find((candidate) => candidate.id === row.id)\n return server ? server.active !== row.active || server.cumulative !== row.cumulative : false\n })\n const isDirty = orderDirty || toggleDirty\n // The reorder command writes an absolute `order` per item (\u00A7API Contracts),\n // and the admin only ever sees the current filtered/paginated page, so\n // saving while filtered or across more than one page can only touch the\n // slots the visible rows already occupy \u2014 promotions outside this view keep\n // their own `order` but may end up interleaved differently. The banner\n // names that risk explicitly rather than silently pretending the reorder is\n // scoped to the full dataset.\n const showFilterWarning = isDirty && (isFiltered || totalPages > 1)\n\n const sensors = useSensors(\n useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),\n useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),\n )\n\n const moveRow = React.useCallback((id: string, direction: -1 | 1) => {\n setRows((current) => {\n const index = current.findIndex((row) => row.id === id)\n if (index === -1) return current\n const target = index + direction\n if (target < 0 || target >= current.length) return current\n const next = [...current]\n const [moved] = next.splice(index, 1)\n next.splice(target, 0, moved)\n return next\n })\n }, [])\n\n const handleDragEnd = React.useCallback((event: DragEndEvent) => {\n const { active, over } = event\n if (!over || active.id === over.id) return\n setRows((current) => {\n const oldIndex = current.findIndex((row) => row.id === active.id)\n const newIndex = current.findIndex((row) => row.id === over.id)\n if (oldIndex === -1 || newIndex === -1) return current\n const next = [...current]\n const [moved] = next.splice(oldIndex, 1)\n next.splice(newIndex, 0, moved)\n return next\n })\n }, [])\n\n const handleToggleActive = React.useCallback((id: string, checked: boolean) => {\n setRows((current) => current.map((row) => (row.id === id ? { ...row, active: checked } : row)))\n }, [])\n\n const handleToggleCumulative = React.useCallback((id: string, checked: boolean) => {\n setRows((current) => current.map((row) => (row.id === id ? { ...row, cumulative: checked } : row)))\n }, [])\n\n const handleDelete = React.useCallback(\n async (row: PromotionRow) => {\n // The list response (Step 1.5, `GET /api/promotions`) does not carry a\n // rule-count or \"tree is non-empty\" flag, so there is no honest way to\n // show the confirmation only for promotions with a populated tree as\n // the spec describes. Confirming unconditionally is the safe default \u2014\n // flagged in the Step 1.6 report as a possible Step 1.5 follow-up\n // (adding e.g. `ruleNodeCount` to the list projection).\n const confirmed = await confirmDialog({\n title: t('promotions.list.confirmDelete.title', 'Delete promotion \"{name}\"?', { name: row.name }),\n description: t(\n 'promotions.list.confirmDelete.description',\n 'This permanently removes the promotion and its rule tree. This cannot be undone.',\n ),\n variant: 'destructive',\n })\n if (!confirmed) return\n\n try {\n await runMutation({\n operation: async () => {\n const call = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(row.updatedAt),\n () => apiCall(`/api/promotions?id=${encodeURIComponent(row.id)}`, { method: 'DELETE' }),\n )\n if (!call.ok) {\n throw Object.assign(new Error('[internal] promotions.delete failed'), {\n status: call.status,\n ...((call.result as Record<string, unknown> | null) ?? {}),\n })\n }\n return call\n },\n context: {\n formId: mutationContextId,\n resourceKind: RESOURCE_KIND,\n resourceId: row.id,\n retryLastMutation,\n },\n mutationPayload: { id: row.id },\n })\n flash(t('promotions.list.flash.deleted', 'Promotion deleted'), 'success')\n setReloadToken((v) => v + 1)\n } catch (error) {\n if (surfaceRecordConflict(error, t, { onRefresh: () => setReloadToken((v) => v + 1) })) return\n const serverMessage = (error as { error?: unknown } | null)?.error\n flash(\n typeof serverMessage === 'string' && serverMessage.trim()\n ? serverMessage\n : t('promotions.list.flash.deleteError', 'Could not delete the promotion'),\n 'error',\n )\n }\n },\n [t, confirmDialog, mutationContextId, retryLastMutation, runMutation],\n )\n\n const handleSave = React.useCallback(async () => {\n if (!isDirty) return\n setIsSaving(true)\n const conflictIds = new Set<string>()\n try {\n await runMutation({\n operation: async () => {\n if (orderDirty) {\n // Redistribute the visible rows' own existing `order` slots across\n // their new positions \u2014 this keeps promotions outside the current\n // filtered/paginated view untouched (their `order` values are\n // never read here), at the cost of the interleaving risk the\n // filter/pagination warning banner names.\n const orderedSlots = rows.map((row) => row.order).sort((left, right) => left - right)\n const items = rows\n .map((row, index) => ({ id: row.id, order: orderedSlots[index], updatedAt: row.updatedAt }))\n .filter((item): item is { id: string; order: number; updatedAt: string } => typeof item.updatedAt === 'string')\n if (items.length) {\n const call = await apiCall<OrderPatchResponse>('/api/promotions/order', {\n method: 'PATCH',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ items }),\n })\n if (!call.ok) {\n throw Object.assign(new Error('[internal] promotions.order save failed'), { status: call.status })\n }\n for (const conflict of call.result?.conflicts ?? []) conflictIds.add(conflict.id)\n }\n }\n\n // Toggle PUTs run after the reorder PATCH and lock against the\n // pre-save `updatedAt`. A promotion that was BOTH dragged and\n // toggled in this same Save will therefore see its own just-written\n // reorder as a conflict here \u2014 surfaced through the same per-item\n // conflict path as a genuine concurrent edit, resolved by pressing\n // Save again (the reload below refreshes `updatedAt` first).\n const dirtyToggleRows = rows.filter((row) => {\n const server = serverRows.find((candidate) => candidate.id === row.id)\n return server ? server.active !== row.active || server.cumulative !== row.cumulative : false\n })\n for (const row of dirtyToggleRows) {\n const server = serverRows.find((candidate) => candidate.id === row.id)\n try {\n await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(server?.updatedAt ?? null),\n () =>\n apiCallOrThrow('/api/promotions', {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ id: row.id, active: row.active, cumulative: row.cumulative }),\n }),\n )\n } catch (err) {\n if (extractOptimisticLockConflict(err)) {\n conflictIds.add(row.id)\n continue\n }\n throw err\n }\n }\n },\n context: {\n formId: mutationContextId,\n resourceKind: RESOURCE_KIND,\n resourceId: 'save',\n retryLastMutation,\n },\n mutationPayload: { ids: rows.map((row) => row.id) },\n })\n\n if (conflictIds.size > 0) {\n flash(\n t(\n 'promotions.list.flash.saveConflicts',\n '{count} change(s) could not be saved because the promotion changed elsewhere. Showing current data.',\n { count: String(conflictIds.size) },\n ),\n 'error',\n )\n } else {\n flash(t('promotions.list.flash.saved', 'Promotions updated'), 'success')\n }\n } catch {\n flash(t('promotions.list.flash.saveError', 'Could not save promotion changes'), 'error')\n } finally {\n setIsSaving(false)\n setReloadToken((v) => v + 1)\n }\n }, [isDirty, orderDirty, rows, serverRows, runMutation, mutationContextId, retryLastMutation, t])\n\n const filterDefs = React.useMemo<FilterDef[]>(\n () => [\n {\n id: 'active',\n label: t('promotions.list.filters.status', 'Status'),\n type: 'select',\n options: [\n { value: '', label: t('promotions.list.filters.status.all', 'All') },\n { value: 'true', label: t('promotions.list.filters.status.active', 'Active') },\n { value: 'false', label: t('promotions.list.filters.status.inactive', 'Inactive') },\n ],\n },\n {\n id: 'cumulative',\n label: t('promotions.list.filters.cumulative', 'Cumulative'),\n type: 'select',\n options: [\n { value: '', label: t('promotions.list.filters.cumulative.all', 'All') },\n { value: 'true', label: t('promotions.list.filters.cumulative.yes', 'Cumulative') },\n { value: 'false', label: t('promotions.list.filters.cumulative.no', 'Non-cumulative') },\n ],\n },\n {\n id: 'startsWindow',\n label: t('promotions.list.filters.dateRange', 'Date range'),\n type: 'dateRange',\n },\n {\n id: 'tags',\n label: t('promotions.list.filters.tags', 'Tags'),\n type: 'tags',\n placeholder: t('promotions.list.filters.tagsPlaceholder', 'Add a tag...'),\n },\n ],\n [t],\n )\n\n const hasRows = rows.length > 0\n\n return (\n <Page>\n <PageHeader\n title={t('promotions.list.title', 'Promotions')}\n description={t('promotions.list.description', 'Manage promotion rules, benefits, and display order.')}\n actions={\n canCreate ? (\n <Button asChild>\n <Link href=\"/backend/promotions/create\">\n <Plus className=\"mr-2 h-4 w-4\" />\n {t('promotions.list.actions.create', 'Create promotion')}\n </Link>\n </Button>\n ) : undefined\n }\n />\n <PageBody>\n <FilterBar\n searchValue={search}\n onSearchChange={handleSearchChange}\n searchPlaceholder={t('promotions.list.searchPlaceholder', 'Search promotions...')}\n filters={filterDefs}\n values={filterValues}\n onApply={handleFiltersApply}\n onClear={() => handleFiltersApply({})}\n />\n\n {isLoading ? <LoadingMessage label={t('promotions.list.loading', 'Loading promotions...')} /> : null}\n {!isLoading && loadError ? <ErrorMessage label={loadError} /> : null}\n\n {!isLoading && !loadError && !hasRows ? (\n <ListEmptyState\n entityName={t('promotions.list.title', 'Promotions')}\n title={t('promotions.list.empty.title', 'No promotions yet')}\n description={t(\n 'promotions.list.empty.description',\n 'Create your first promotion to start configuring rules and benefits.',\n )}\n createHref={canCreate ? '/backend/promotions/create' : undefined}\n createLabel={t('promotions.list.empty.createLabel', 'Create your first promotion')}\n />\n ) : null}\n\n {!isLoading && !loadError && hasRows ? (\n <>\n {showFilterWarning ? (\n <Alert status=\"warning\" size=\"sm\" className=\"mb-4\">\n {t(\n 'promotions.list.save.filterWarning',\n 'Saving while filtered may reorder promotions outside the current page.',\n )}\n </Alert>\n ) : null}\n <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>\n <SortableContext items={rows.map((row) => row.id)} strategy={verticalListSortingStrategy}>\n <div className=\"space-y-3\">\n {rows.map((row, index) => (\n <PromotionCard\n key={row.id}\n row={row}\n canDrag={canUpdate}\n canEdit={canUpdate}\n canDelete={canDelete}\n isFirst={index === 0}\n isLast={index === rows.length - 1}\n onToggleActive={(checked) => handleToggleActive(row.id, checked)}\n onToggleCumulative={(checked) => handleToggleCumulative(row.id, checked)}\n onMoveUp={() => moveRow(row.id, -1)}\n onMoveDown={() => moveRow(row.id, 1)}\n onDelete={() => handleDelete(row)}\n />\n ))}\n </div>\n </SortableContext>\n </DndContext>\n\n <Pagination\n className=\"mt-4\"\n page={page}\n pageSize={PAGE_SIZE}\n total={total}\n totalIsCapped={totalIsCapped}\n onPageChange={setPage}\n showPageSize={false}\n />\n </>\n ) : null}\n\n {canUpdate ? (\n <div\n className={`sticky bottom-0 z-sticky mt-4 flex items-center justify-end gap-3 border-t bg-background/95 py-3 backdrop-blur ${\n isDirty ? 'border-accent' : 'border-border'\n }`}\n >\n <span className=\"text-xs text-muted-foreground\">\n {isDirty\n ? t('promotions.list.save.unsavedHint', 'Unsaved changes')\n : t('promotions.list.save.allSaved', 'All changes saved')}\n </span>\n <Button\n type=\"button\"\n size=\"sm\"\n className={isDirty ? 'border-2 border-accent' : undefined}\n disabled={!isDirty || isSaving}\n onClick={handleSave}\n >\n {isSaving ? <Spinner className=\"size-4\" /> : null}\n {t('promotions.list.save.button', 'Save')}\n </Button>\n </div>\n ) : null}\n </PageBody>\n {ConfirmDialogElement}\n </Page>\n )\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport {\n DndContext,\n closestCenter,\n KeyboardSensor,\n PointerSensor,\n useSensor,\n useSensors,\n type DragEndEvent,\n} from '@dnd-kit/core'\nimport {\n SortableContext,\n sortableKeyboardCoordinates,\n verticalListSortingStrategy,\n useSortable,\n} from '@dnd-kit/sortable'\nimport { CSS } from '@dnd-kit/utilities'\nimport { ArrowDown, ArrowUp, GripVertical, Plus } from 'lucide-react'\nimport { Page, PageBody, PageHeader } from '@open-mercato/ui/backend/Page'\nimport { Card, CardContent } from '@open-mercato/ui/primitives/card'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Switch } from '@open-mercato/ui/primitives/switch'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { Alert } from '@open-mercato/ui/primitives/alert'\nimport { Pagination } from '@open-mercato/ui/primitives/pagination'\nimport { FilterBar } from '@open-mercato/ui/backend/FilterBar'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { ListEmptyState } from '@open-mercato/ui/backend/filters/ListEmptyState'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { useBackendChrome } from '@open-mercato/ui/backend/BackendChromeProvider'\nimport { hasFeature } from '@open-mercato/shared/security/features'\nimport { apiCall, apiCallOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader, extractOptimisticLockConflict } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { formatPromotionCalendarDate } from '../../components/promotionDates'\n\nconst PAGE_SIZE = 50\nconst RESOURCE_KIND = 'promotions.promotion'\n\ntype PromotionRow = {\n id: string\n name: string\n description: string | null\n order: number\n active: boolean\n startsAt: string | null\n endsAt: string | null\n cumulative: boolean\n tags: string[]\n excludedTags: string[]\n updatedAt: string | null\n}\n\ntype ListResponse = {\n items: PromotionRow[]\n total: number\n page: number\n totalPages: number\n totalIsCapped?: boolean\n}\n\ntype OrderPatchResponse = {\n updated: string[]\n conflicts: { id: string; currentUpdatedAt: string }[]\n}\n\n// Whether the current working copy's id sequence differs from the server's \u2014\n// the cheapest correct test for \"has the display order changed\" since both\n// arrays always carry the same ids (no row is added/removed locally).\nfunction sameOrder(a: PromotionRow[], b: PromotionRow[]): boolean {\n if (a.length !== b.length) return false\n return a.every((row, index) => row.id === b[index]?.id)\n}\n\nfunction isFilterValueActive(value: unknown): boolean {\n if (value == null) return false\n if (Array.isArray(value)) return value.length > 0\n if (typeof value === 'object') return Object.values(value).some((entry) => entry != null && entry !== '')\n return value !== ''\n}\n\ntype PromotionCardProps = {\n row: PromotionRow\n canDrag: boolean\n canEdit: boolean\n canDelete: boolean\n isFirst: boolean\n isLast: boolean\n onToggleActive: (checked: boolean) => void\n onToggleCumulative: (checked: boolean) => void\n onMoveUp: () => void\n onMoveDown: () => void\n onDelete: () => void\n}\n\n// One promotion card. `useSortable` is always called (dnd-kit requires a\n// stable hook per rendered id) but drag is inert via `disabled` when the\n// caller lacks `promotions.update` \u2014 the handle and up/down buttons are also\n// hidden in that case, matching the DataTable-row precedent in\n// `customer_groups/backend/customer-groups/page.tsx`.\nfunction PromotionCard({\n row,\n canDrag,\n canEdit,\n canDelete,\n isFirst,\n isLast,\n onToggleActive,\n onToggleCumulative,\n onMoveUp,\n onMoveDown,\n onDelete,\n}: PromotionCardProps) {\n const t = useT()\n const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({\n id: row.id,\n disabled: !canDrag,\n })\n const style: React.CSSProperties = {\n transform: CSS.Transform.toString(transform),\n transition,\n }\n const startsLabel = formatPromotionCalendarDate(row.startsAt)\n const endsLabel = formatPromotionCalendarDate(row.endsAt)\n const windowLabel = startsLabel || endsLabel\n ? [\n startsLabel ? t('promotions.list.card.starts', 'Starts {date}', { date: startsLabel }) : null,\n endsLabel ? t('promotions.list.card.ends', 'Ends {date}', { date: endsLabel }) : null,\n ]\n .filter(Boolean)\n .join(' \u00B7 ')\n : t('promotions.list.card.noWindow', 'No date restriction')\n\n return (\n <div ref={setNodeRef} style={style}>\n <Card className={isDragging ? 'border-primary shadow-lg' : undefined}>\n <CardContent className=\"space-y-3 p-4\">\n <div className=\"flex items-start gap-3\">\n {canDrag ? (\n <div className=\"flex shrink-0 flex-col items-center gap-1 pt-0.5\">\n <span\n className=\"inline-flex cursor-grab items-center text-muted-foreground active:cursor-grabbing\"\n aria-label={t('promotions.list.card.dragHandle', 'Reorder')}\n {...attributes}\n {...listeners}\n >\n <GripVertical className=\"size-4\" />\n </span>\n <div className=\"flex flex-col gap-0.5\">\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n aria-label={t('promotions.list.card.moveUp', 'Move up')}\n disabled={isFirst}\n onClick={onMoveUp}\n >\n <ArrowUp className=\"size-3.5\" />\n </IconButton>\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n aria-label={t('promotions.list.card.moveDown', 'Move down')}\n disabled={isLast}\n onClick={onMoveDown}\n >\n <ArrowDown className=\"size-3.5\" />\n </IconButton>\n </div>\n </div>\n ) : null}\n <div className=\"min-w-0 flex-1 space-y-1.5\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"truncate font-medium\">{row.name}</span>\n <span className=\"text-xs text-muted-foreground\">{windowLabel}</span>\n </div>\n {row.description ? (\n <p className=\"truncate text-xs text-muted-foreground\">{row.description}</p>\n ) : null}\n {row.tags.length > 0 ? (\n <div className=\"flex flex-wrap gap-1.5\">\n {row.tags.map((tag) => (\n <Tag key={tag} variant=\"neutral\">\n {tag}\n </Tag>\n ))}\n </div>\n ) : null}\n </div>\n </div>\n <div className=\"flex flex-wrap items-center justify-between gap-3 border-t border-border pt-3\">\n <div className=\"flex items-center gap-4\">\n <label className=\"flex items-center gap-2 text-sm\">\n <Switch checked={row.active} onCheckedChange={onToggleActive} disabled={!canEdit} />\n {t('promotions.list.card.active', 'Active')}\n </label>\n <label className=\"flex items-center gap-2 text-sm\">\n <Switch checked={row.cumulative} onCheckedChange={onToggleCumulative} disabled={!canEdit} />\n {t('promotions.list.card.cumulative', 'Cumulative')}\n </label>\n </div>\n <div className=\"flex items-center gap-2\">\n {canEdit ? (\n <Button asChild variant=\"outline\" size=\"sm\">\n <Link href={`/backend/promotions/${row.id}`}>{t('promotions.list.card.edit', 'Edit')}</Link>\n </Button>\n ) : null}\n {canDelete ? (\n <Button type=\"button\" variant=\"destructive\" size=\"sm\" onClick={onDelete}>\n {t('promotions.list.card.delete', 'Delete')}\n </Button>\n ) : null}\n </div>\n </div>\n </CardContent>\n </Card>\n </div>\n )\n}\n\nexport default function PromotionsListPage() {\n const t = useT()\n const { confirm: confirmDialog, ConfirmDialogElement } = useConfirmDialog()\n const { payload: chromePayload, isReady: chromeReady } = useBackendChrome()\n const canCreate = chromeReady && hasFeature(chromePayload?.grantedFeatures, 'promotions.create')\n const canUpdate = chromeReady && hasFeature(chromePayload?.grantedFeatures, 'promotions.update')\n const canDelete = chromeReady && hasFeature(chromePayload?.grantedFeatures, 'promotions.delete')\n\n const [rows, setRows] = React.useState<PromotionRow[]>([])\n const [serverRows, setServerRows] = React.useState<PromotionRow[]>([])\n const [page, setPage] = React.useState(1)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [totalIsCapped, setTotalIsCapped] = React.useState(false)\n const [search, setSearch] = React.useState('')\n const [filterValues, setFilterValues] = React.useState<FilterValues>({})\n const [isLoading, setIsLoading] = React.useState(true)\n const [loadError, setLoadError] = React.useState<string | null>(null)\n const [isSaving, setIsSaving] = React.useState(false)\n const [reloadToken, setReloadToken] = React.useState(0)\n\n const mutationContextId = 'promotions-list:mutation'\n const { runMutation, retryLastMutation } = useGuardedMutation<{\n formId: string\n resourceKind: string\n resourceId: string\n retryLastMutation: () => Promise<boolean>\n }>({\n contextId: mutationContextId,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setIsLoading(true)\n setLoadError(null)\n try {\n const params = new URLSearchParams()\n params.set('page', String(page))\n params.set('pageSize', String(PAGE_SIZE))\n // Drag-sort and the Up/Down buttons both rely on the displayed order\n // matching `Promotion.order` ascending.\n params.set('sortField', 'order')\n params.set('sortDir', 'asc')\n if (search) params.set('search', search)\n if (filterValues.active === 'true' || filterValues.active === 'false') {\n params.set('active', filterValues.active)\n }\n if (filterValues.cumulative === 'true' || filterValues.cumulative === 'false') {\n params.set('cumulative', filterValues.cumulative)\n }\n const dateRange = filterValues.startsWindow as { from?: string; to?: string } | undefined\n if (dateRange?.from) params.set('startsFrom', dateRange.from)\n if (dateRange?.to) params.set('startsTo', dateRange.to)\n const tagList = Array.isArray(filterValues.tags)\n ? filterValues.tags.filter((entry: unknown): entry is string => typeof entry === 'string')\n : []\n if (tagList.length) params.set('tags', tagList.join(','))\n\n const fallback: ListResponse = { items: [], total: 0, page: 1, totalPages: 1 }\n const call = await apiCall<ListResponse>(`/api/promotions?${params.toString()}`, undefined, { fallback })\n if (cancelled) return\n if (!call.ok) {\n setRows([])\n setServerRows([])\n setLoadError(t('promotions.list.errors.loadFailed', 'Failed to load promotions'))\n return\n }\n const payload = call.result ?? fallback\n const items = Array.isArray(payload.items) ? payload.items : []\n setRows(items)\n setServerRows(items)\n setTotal(typeof payload.total === 'number' ? payload.total : items.length)\n setTotalPages(typeof payload.totalPages === 'number' && payload.totalPages > 0 ? payload.totalPages : 1)\n setTotalIsCapped(payload.totalIsCapped === true)\n } catch {\n if (!cancelled) {\n setRows([])\n setServerRows([])\n setLoadError(t('promotions.list.errors.loadFailed', 'Failed to load promotions'))\n }\n } finally {\n if (!cancelled) setIsLoading(false)\n }\n }\n load()\n return () => {\n cancelled = true\n }\n }, [page, search, filterValues, reloadToken, t])\n\n const handleSearchChange = React.useCallback((next: string) => {\n setSearch(next)\n setPage(1)\n }, [])\n\n const handleFiltersApply = React.useCallback((next: FilterValues) => {\n setFilterValues(next)\n setPage(1)\n }, [])\n\n const isFiltered = Boolean(search) || Object.values(filterValues).some(isFilterValueActive)\n const orderDirty = !sameOrder(rows, serverRows)\n const toggleDirty = rows.some((row) => {\n const server = serverRows.find((candidate) => candidate.id === row.id)\n return server ? server.active !== row.active || server.cumulative !== row.cumulative : false\n })\n const isDirty = orderDirty || toggleDirty\n // The reorder command writes an absolute `order` per item (\u00A7API Contracts),\n // and the admin only ever sees the current filtered/paginated page, so\n // saving while filtered or across more than one page can only touch the\n // slots the visible rows already occupy \u2014 promotions outside this view keep\n // their own `order` but may end up interleaved differently. The banner\n // names that risk explicitly rather than silently pretending the reorder is\n // scoped to the full dataset.\n const showFilterWarning = isDirty && (isFiltered || totalPages > 1)\n\n const sensors = useSensors(\n useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),\n useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),\n )\n\n const moveRow = React.useCallback((id: string, direction: -1 | 1) => {\n setRows((current) => {\n const index = current.findIndex((row) => row.id === id)\n if (index === -1) return current\n const target = index + direction\n if (target < 0 || target >= current.length) return current\n const next = [...current]\n const [moved] = next.splice(index, 1)\n next.splice(target, 0, moved)\n return next\n })\n }, [])\n\n const handleDragEnd = React.useCallback((event: DragEndEvent) => {\n const { active, over } = event\n if (!over || active.id === over.id) return\n setRows((current) => {\n const oldIndex = current.findIndex((row) => row.id === active.id)\n const newIndex = current.findIndex((row) => row.id === over.id)\n if (oldIndex === -1 || newIndex === -1) return current\n const next = [...current]\n const [moved] = next.splice(oldIndex, 1)\n next.splice(newIndex, 0, moved)\n return next\n })\n }, [])\n\n const handleToggleActive = React.useCallback((id: string, checked: boolean) => {\n setRows((current) => current.map((row) => (row.id === id ? { ...row, active: checked } : row)))\n }, [])\n\n const handleToggleCumulative = React.useCallback((id: string, checked: boolean) => {\n setRows((current) => current.map((row) => (row.id === id ? { ...row, cumulative: checked } : row)))\n }, [])\n\n const handleDelete = React.useCallback(\n async (row: PromotionRow) => {\n // The list response (Step 1.5, `GET /api/promotions`) does not carry a\n // rule-count or \"tree is non-empty\" flag, so there is no honest way to\n // show the confirmation only for promotions with a populated tree as\n // the spec describes. Confirming unconditionally is the safe default \u2014\n // flagged in the Step 1.6 report as a possible Step 1.5 follow-up\n // (adding e.g. `ruleNodeCount` to the list projection).\n const confirmed = await confirmDialog({\n title: t('promotions.list.confirmDelete.title', 'Delete promotion \"{name}\"?', { name: row.name }),\n description: t(\n 'promotions.list.confirmDelete.description',\n 'This permanently removes the promotion and its rule tree. This cannot be undone.',\n ),\n variant: 'destructive',\n })\n if (!confirmed) return\n\n try {\n await runMutation({\n operation: async () => {\n const call = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(row.updatedAt),\n () => apiCall(`/api/promotions?id=${encodeURIComponent(row.id)}`, { method: 'DELETE' }),\n )\n if (!call.ok) {\n throw Object.assign(new Error('[internal] promotions.delete failed'), {\n status: call.status,\n ...((call.result as Record<string, unknown> | null) ?? {}),\n })\n }\n return call\n },\n context: {\n formId: mutationContextId,\n resourceKind: RESOURCE_KIND,\n resourceId: row.id,\n retryLastMutation,\n },\n mutationPayload: { id: row.id },\n })\n flash(t('promotions.list.flash.deleted', 'Promotion deleted'), 'success')\n setReloadToken((v) => v + 1)\n } catch (error) {\n if (surfaceRecordConflict(error, t, { onRefresh: () => setReloadToken((v) => v + 1) })) return\n const serverMessage = (error as { error?: unknown } | null)?.error\n flash(\n typeof serverMessage === 'string' && serverMessage.trim()\n ? serverMessage\n : t('promotions.list.flash.deleteError', 'Could not delete the promotion'),\n 'error',\n )\n }\n },\n [t, confirmDialog, mutationContextId, retryLastMutation, runMutation],\n )\n\n const handleSave = React.useCallback(async () => {\n if (!isDirty) return\n setIsSaving(true)\n const conflictIds = new Set<string>()\n try {\n await runMutation({\n operation: async () => {\n if (orderDirty) {\n // Redistribute the visible rows' own existing `order` slots across\n // their new positions \u2014 this keeps promotions outside the current\n // filtered/paginated view untouched (their `order` values are\n // never read here), at the cost of the interleaving risk the\n // filter/pagination warning banner names.\n const orderedSlots = rows.map((row) => row.order).sort((left, right) => left - right)\n const items = rows\n .map((row, index) => ({ id: row.id, order: orderedSlots[index], updatedAt: row.updatedAt }))\n .filter((item): item is { id: string; order: number; updatedAt: string } => typeof item.updatedAt === 'string')\n if (items.length) {\n const call = await apiCall<OrderPatchResponse>('/api/promotions/order', {\n method: 'PATCH',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ items }),\n })\n if (!call.ok) {\n throw Object.assign(new Error('[internal] promotions.order save failed'), { status: call.status })\n }\n for (const conflict of call.result?.conflicts ?? []) conflictIds.add(conflict.id)\n }\n }\n\n // Toggle PUTs run after the reorder PATCH and lock against the\n // pre-save `updatedAt`. A promotion that was BOTH dragged and\n // toggled in this same Save will therefore see its own just-written\n // reorder as a conflict here \u2014 surfaced through the same per-item\n // conflict path as a genuine concurrent edit, resolved by pressing\n // Save again (the reload below refreshes `updatedAt` first).\n const dirtyToggleRows = rows.filter((row) => {\n const server = serverRows.find((candidate) => candidate.id === row.id)\n return server ? server.active !== row.active || server.cumulative !== row.cumulative : false\n })\n for (const row of dirtyToggleRows) {\n const server = serverRows.find((candidate) => candidate.id === row.id)\n try {\n await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(server?.updatedAt ?? null),\n () =>\n apiCallOrThrow('/api/promotions', {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ id: row.id, active: row.active, cumulative: row.cumulative }),\n }),\n )\n } catch (err) {\n if (extractOptimisticLockConflict(err)) {\n conflictIds.add(row.id)\n continue\n }\n throw err\n }\n }\n },\n context: {\n formId: mutationContextId,\n resourceKind: RESOURCE_KIND,\n resourceId: 'save',\n retryLastMutation,\n },\n mutationPayload: { ids: rows.map((row) => row.id) },\n })\n\n if (conflictIds.size > 0) {\n flash(\n t(\n 'promotions.list.flash.saveConflicts',\n '{count} change(s) could not be saved because the promotion changed elsewhere. Showing current data.',\n { count: String(conflictIds.size) },\n ),\n 'error',\n )\n } else {\n flash(t('promotions.list.flash.saved', 'Promotions updated'), 'success')\n }\n } catch {\n flash(t('promotions.list.flash.saveError', 'Could not save promotion changes'), 'error')\n } finally {\n setIsSaving(false)\n setReloadToken((v) => v + 1)\n }\n }, [isDirty, orderDirty, rows, serverRows, runMutation, mutationContextId, retryLastMutation, t])\n\n const filterDefs = React.useMemo<FilterDef[]>(\n () => [\n {\n id: 'active',\n label: t('promotions.list.filters.status', 'Status'),\n type: 'select',\n options: [\n { value: '', label: t('promotions.list.filters.status.all', 'All') },\n { value: 'true', label: t('promotions.list.filters.status.active', 'Active') },\n { value: 'false', label: t('promotions.list.filters.status.inactive', 'Inactive') },\n ],\n },\n {\n id: 'cumulative',\n label: t('promotions.list.filters.cumulative', 'Cumulative'),\n type: 'select',\n options: [\n { value: '', label: t('promotions.list.filters.cumulative.all', 'All') },\n { value: 'true', label: t('promotions.list.filters.cumulative.yes', 'Cumulative') },\n { value: 'false', label: t('promotions.list.filters.cumulative.no', 'Non-cumulative') },\n ],\n },\n {\n id: 'startsWindow',\n label: t('promotions.list.filters.dateRange', 'Date range'),\n type: 'dateRange',\n },\n {\n id: 'tags',\n label: t('promotions.list.filters.tags', 'Tags'),\n type: 'tags',\n placeholder: t('promotions.list.filters.tagsPlaceholder', 'Add a tag...'),\n },\n ],\n [t],\n )\n\n const hasRows = rows.length > 0\n\n return (\n <Page>\n <PageHeader\n title={t('promotions.list.title', 'Promotions')}\n description={t('promotions.list.description', 'Manage promotion rules, benefits, and display order.')}\n actions={\n canCreate ? (\n <Button asChild>\n <Link href=\"/backend/promotions/create\">\n <Plus className=\"mr-2 h-4 w-4\" />\n {t('promotions.list.actions.create', 'Create promotion')}\n </Link>\n </Button>\n ) : undefined\n }\n />\n <PageBody>\n <FilterBar\n searchValue={search}\n onSearchChange={handleSearchChange}\n searchPlaceholder={t('promotions.list.searchPlaceholder', 'Search promotions...')}\n filters={filterDefs}\n values={filterValues}\n onApply={handleFiltersApply}\n onClear={() => handleFiltersApply({})}\n />\n\n {isLoading ? <LoadingMessage label={t('promotions.list.loading', 'Loading promotions...')} /> : null}\n {!isLoading && loadError ? <ErrorMessage label={loadError} /> : null}\n\n {!isLoading && !loadError && !hasRows ? (\n <ListEmptyState\n entityName={t('promotions.list.title', 'Promotions')}\n title={t('promotions.list.empty.title', 'No promotions yet')}\n description={t(\n 'promotions.list.empty.description',\n 'Create your first promotion to start configuring rules and benefits.',\n )}\n createHref={canCreate ? '/backend/promotions/create' : undefined}\n createLabel={t('promotions.list.empty.createLabel', 'Create your first promotion')}\n />\n ) : null}\n\n {!isLoading && !loadError && hasRows ? (\n <>\n {showFilterWarning ? (\n <Alert status=\"warning\" size=\"sm\" className=\"mb-4\">\n {t(\n 'promotions.list.save.filterWarning',\n 'Saving while filtered may reorder promotions outside the current page.',\n )}\n </Alert>\n ) : null}\n <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>\n <SortableContext items={rows.map((row) => row.id)} strategy={verticalListSortingStrategy}>\n <div className=\"space-y-3\">\n {rows.map((row, index) => (\n <PromotionCard\n key={row.id}\n row={row}\n canDrag={canUpdate}\n canEdit={canUpdate}\n canDelete={canDelete}\n isFirst={index === 0}\n isLast={index === rows.length - 1}\n onToggleActive={(checked) => handleToggleActive(row.id, checked)}\n onToggleCumulative={(checked) => handleToggleCumulative(row.id, checked)}\n onMoveUp={() => moveRow(row.id, -1)}\n onMoveDown={() => moveRow(row.id, 1)}\n onDelete={() => handleDelete(row)}\n />\n ))}\n </div>\n </SortableContext>\n </DndContext>\n\n <Pagination\n className=\"mt-4\"\n page={page}\n pageSize={PAGE_SIZE}\n total={total}\n totalIsCapped={totalIsCapped}\n onPageChange={setPage}\n showPageSize={false}\n />\n </>\n ) : null}\n\n {canUpdate ? (\n <div\n className={`sticky bottom-0 z-sticky mt-4 flex items-center justify-end gap-3 border-t bg-background/95 py-3 backdrop-blur ${\n isDirty ? 'border-accent' : 'border-border'\n }`}\n >\n <span className=\"text-xs text-muted-foreground\">\n {isDirty\n ? t('promotions.list.save.unsavedHint', 'Unsaved changes')\n : t('promotions.list.save.allSaved', 'All changes saved')}\n </span>\n <Button\n type=\"button\"\n size=\"sm\"\n className={isDirty ? 'border-2 border-accent' : undefined}\n disabled={!isDirty || isSaving}\n onClick={handleSave}\n >\n {isSaving ? <Spinner className=\"size-4\" /> : null}\n {t('promotions.list.save.button', 'Save')}\n </Button>\n </div>\n ) : null}\n </PageBody>\n {ConfirmDialogElement}\n </Page>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const PROMOTION_DATE_PATTERN = /^(\d{4}-\d{2}-\d{2})(?:T.*)?$/;
|
|
2
|
+
function promotionCalendarDate(value) {
|
|
3
|
+
if (!value) return null;
|
|
4
|
+
const dateOnly = PROMOTION_DATE_PATTERN.exec(value)?.[1];
|
|
5
|
+
if (!dateOnly) return null;
|
|
6
|
+
const date = /* @__PURE__ */ new Date(`${dateOnly}T00:00:00.000Z`);
|
|
7
|
+
if (Number.isNaN(date.getTime()) || date.toISOString().slice(0, 10) !== dateOnly) return null;
|
|
8
|
+
return dateOnly;
|
|
9
|
+
}
|
|
10
|
+
function formatPromotionCalendarDate(value, locales) {
|
|
11
|
+
const dateOnly = promotionCalendarDate(value);
|
|
12
|
+
if (!dateOnly) return null;
|
|
13
|
+
return (/* @__PURE__ */ new Date(`${dateOnly}T00:00:00.000Z`)).toLocaleDateString(locales, { timeZone: "UTC" });
|
|
14
|
+
}
|
|
15
|
+
export {
|
|
16
|
+
formatPromotionCalendarDate,
|
|
17
|
+
promotionCalendarDate
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=promotionDates.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/promotions/components/promotionDates.ts"],
|
|
4
|
+
"sourcesContent": ["const PROMOTION_DATE_PATTERN = /^(\\d{4}-\\d{2}-\\d{2})(?:T.*)?$/\n\nexport function promotionCalendarDate(value: string | null): string | null {\n if (!value) return null\n const dateOnly = PROMOTION_DATE_PATTERN.exec(value)?.[1]\n if (!dateOnly) return null\n const date = new Date(`${dateOnly}T00:00:00.000Z`)\n if (Number.isNaN(date.getTime()) || date.toISOString().slice(0, 10) !== dateOnly) return null\n return dateOnly\n}\n\nexport function formatPromotionCalendarDate(\n value: string | null,\n locales?: string | string[],\n): string | null {\n const dateOnly = promotionCalendarDate(value)\n if (!dateOnly) return null\n return new Date(`${dateOnly}T00:00:00.000Z`).toLocaleDateString(locales, { timeZone: 'UTC' })\n}\n"],
|
|
5
|
+
"mappings": "AAAA,MAAM,yBAAyB;AAExB,SAAS,sBAAsB,OAAqC;AACzE,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,WAAW,uBAAuB,KAAK,KAAK,IAAI,CAAC;AACvD,MAAI,CAAC,SAAU,QAAO;AACtB,QAAM,OAAO,oBAAI,KAAK,GAAG,QAAQ,gBAAgB;AACjD,MAAI,OAAO,MAAM,KAAK,QAAQ,CAAC,KAAK,KAAK,YAAY,EAAE,MAAM,GAAG,EAAE,MAAM,SAAU,QAAO;AACzF,SAAO;AACT;AAEO,SAAS,4BACd,OACA,SACe;AACf,QAAM,WAAW,sBAAsB,KAAK;AAC5C,MAAI,CAAC,SAAU,QAAO;AACtB,UAAO,oBAAI,KAAK,GAAG,QAAQ,gBAAgB,GAAE,mBAAmB,SAAS,EAAE,UAAU,MAAM,CAAC;AAC9F;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -22,7 +22,14 @@ const clearableEndOfDayDateSchema = z.preprocess(
|
|
|
22
22
|
z.coerce.date().nullable().optional()
|
|
23
23
|
);
|
|
24
24
|
const clearableUuidSchema = z.preprocess(emptyStringToNull, uuid().nullable().optional());
|
|
25
|
-
const
|
|
25
|
+
const normalizeDecimalSeparator = (value) => {
|
|
26
|
+
if (typeof value !== "string" || !/^\d+,\d+$/.test(value)) return value;
|
|
27
|
+
return value.replace(",", ".");
|
|
28
|
+
};
|
|
29
|
+
const decimalStringSchema = z.preprocess(
|
|
30
|
+
normalizeDecimalSeparator,
|
|
31
|
+
z.string().regex(/^\d+(\.\d+)?$/, "Must be a non-negative decimal string")
|
|
32
|
+
);
|
|
26
33
|
const clearableDecimalStringSchema = z.preprocess(emptyStringToNull, decimalStringSchema.nullable().optional());
|
|
27
34
|
function rejectPercentageOver100(config, ctx) {
|
|
28
35
|
if (config.discountType === "percentage" && Number(config.value) > 100) {
|