@open-mercato/core 0.8.1-develop.7458.1.3847638f67 → 0.9.1-develop.7459.1.99e3accd30
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/availability/api/policies/route.js +6 -6
- package/dist/modules/availability/api/policies/route.js.map +2 -2
- package/dist/modules/availability/backend/availability/check/page.js +4 -3
- package/dist/modules/availability/backend/availability/check/page.js.map +2 -2
- package/dist/modules/availability/backend/availability/policies/create/page.js +3 -0
- package/dist/modules/availability/backend/availability/policies/create/page.js.map +2 -2
- package/dist/modules/availability/lib/organizationScope.js +24 -1
- package/dist/modules/availability/lib/organizationScope.js.map +2 -2
- package/dist/modules/catalog/api/categories/bulk-create/route.js +16 -2
- package/dist/modules/catalog/api/categories/bulk-create/route.js.map +2 -2
- package/dist/modules/catalog/api/products/bulk-create/route.js +16 -2
- package/dist/modules/catalog/api/products/bulk-create/route.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js +1 -0
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
- package/dist/modules/catalog/components/prices/PriceScopeSelectors.js +2 -2
- package/dist/modules/catalog/components/prices/PriceScopeSelectors.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/users/[id]/sessions/[sessionId].js +78 -0
- package/dist/modules/customer_accounts/api/admin/users/[id]/sessions/[sessionId].js.map +7 -0
- package/dist/modules/customer_groups/api/customer-groups/price-kinds/route.js +90 -0
- package/dist/modules/customer_groups/api/customer-groups/price-kinds/route.js.map +7 -0
- package/dist/modules/customer_groups/backend/customer-groups/page.js +5 -1
- package/dist/modules/customer_groups/backend/customer-groups/page.js.map +2 -2
- package/dist/modules/customer_groups/components/CustomerGroupTermsPriceKindField.js +2 -2
- package/dist/modules/customer_groups/components/CustomerGroupTermsPriceKindField.js.map +2 -2
- package/dist/modules/customer_groups/components/CustomerGroupTermsSection.js +3 -1
- package/dist/modules/customer_groups/components/CustomerGroupTermsSection.js.map +2 -2
- package/dist/modules/customer_groups/lib/priceKindScope.js +35 -1
- package/dist/modules/customer_groups/lib/priceKindScope.js.map +2 -2
- package/dist/modules/customer_groups/widgets/injection/person-groups-explain-terms.js +1 -1
- package/dist/modules/customer_groups/widgets/injection/person-groups-explain-terms.js.map +2 -2
- package/dist/modules/customers/commands/companies.js +14 -1
- package/dist/modules/customers/commands/companies.js.map +2 -2
- package/dist/modules/dictionaries/components/DictionariesManager.js +98 -87
- package/dist/modules/dictionaries/components/DictionariesManager.js.map +2 -2
- package/dist/modules/forms/backend/forms/[id]/submissions/components/SubmissionDrawer.js +3 -3
- package/dist/modules/forms/backend/forms/[id]/submissions/components/SubmissionDrawer.js.map +2 -2
- package/dist/modules/forms/backend/forms/[id]/submissions/components/formSummary.js +9 -0
- package/dist/modules/forms/backend/forms/[id]/submissions/components/formSummary.js.map +7 -0
- package/dist/modules/forms/backend/forms/[id]/submissions/page.js +3 -6
- package/dist/modules/forms/backend/forms/[id]/submissions/page.js.map +2 -2
- package/dist/modules/forms/commands/distribution.js +7 -3
- package/dist/modules/forms/commands/distribution.js.map +2 -2
- package/dist/modules/forms/commands/form-version.js +9 -4
- package/dist/modules/forms/commands/form-version.js.map +2 -2
- package/dist/modules/forms/commands/form.js +9 -4
- package/dist/modules/forms/commands/form.js.map +2 -2
- package/dist/modules/forms/commands/invitation.js +7 -3
- package/dist/modules/forms/commands/invitation.js.map +2 -2
- package/dist/modules/forms/commands/submission.js +13 -6
- package/dist/modules/forms/commands/submission.js.map +2 -2
- package/package.json +8 -8
- package/src/modules/availability/api/policies/route.ts +10 -11
- package/src/modules/availability/backend/availability/check/page.tsx +38 -35
- package/src/modules/availability/backend/availability/policies/create/page.tsx +3 -0
- package/src/modules/availability/i18n/de.json +1 -0
- package/src/modules/availability/i18n/en.json +1 -0
- package/src/modules/availability/i18n/es.json +1 -0
- package/src/modules/availability/i18n/ko.json +1 -0
- package/src/modules/availability/i18n/pl.json +1 -0
- package/src/modules/availability/lib/organizationScope.ts +37 -0
- package/src/modules/catalog/api/categories/bulk-create/route.ts +16 -2
- package/src/modules/catalog/api/products/bulk-create/route.ts +16 -2
- package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +1 -0
- package/src/modules/catalog/components/prices/PriceScopeSelectors.tsx +2 -2
- package/src/modules/customer_accounts/AGENTS.md +1 -0
- package/src/modules/customer_accounts/api/admin/users/[id]/sessions/[sessionId].ts +89 -0
- package/src/modules/customer_groups/api/customer-groups/price-kinds/route.ts +99 -0
- package/src/modules/customer_groups/backend/customer-groups/page.tsx +7 -1
- package/src/modules/customer_groups/components/CustomerGroupTermsPriceKindField.tsx +5 -4
- package/src/modules/customer_groups/components/CustomerGroupTermsSection.tsx +8 -4
- package/src/modules/customer_groups/lib/priceKindScope.ts +43 -0
- package/src/modules/customer_groups/widgets/injection/person-groups-explain-terms.tsx +2 -2
- package/src/modules/customers/commands/companies.ts +16 -1
- package/src/modules/dictionaries/components/DictionariesManager.tsx +8 -1
- package/src/modules/eudr/i18n/de.json +1 -0
- package/src/modules/eudr/i18n/en.json +1 -0
- package/src/modules/eudr/i18n/es.json +1 -0
- package/src/modules/eudr/i18n/ko.json +1 -0
- package/src/modules/eudr/i18n/pl.json +1 -0
- package/src/modules/forms/backend/forms/[id]/submissions/components/SubmissionDrawer.tsx +6 -5
- package/src/modules/forms/backend/forms/[id]/submissions/components/formSummary.ts +19 -0
- package/src/modules/forms/backend/forms/[id]/submissions/page.tsx +4 -16
- package/src/modules/forms/commands/distribution.ts +7 -3
- package/src/modules/forms/commands/form-version.ts +9 -4
- package/src/modules/forms/commands/form.ts +9 -4
- package/src/modules/forms/commands/invitation.ts +7 -3
- package/src/modules/forms/commands/submission.ts +13 -6
- package/src/modules/forms/i18n/de.json +21 -0
- package/src/modules/forms/i18n/en.json +21 -0
- package/src/modules/forms/i18n/es.json +21 -0
- package/src/modules/forms/i18n/ko.json +21 -0
- package/src/modules/forms/i18n/pl.json +21 -0
- package/src/modules/notifications/AGENTS.md +1 -1
- package/src/modules/push_notifications/i18n/de.json +1 -0
- package/src/modules/push_notifications/i18n/en.json +1 -0
- package/src/modules/push_notifications/i18n/es.json +1 -0
- package/src/modules/push_notifications/i18n/ko.json +1 -0
- package/src/modules/push_notifications/i18n/pl.json +1 -0
- package/src/modules/warranty_claims/i18n/de.json +1 -0
- package/src/modules/warranty_claims/i18n/en.json +1 -0
- package/src/modules/warranty_claims/i18n/es.json +1 -0
- package/src/modules/warranty_claims/i18n/ko.json +1 -0
- package/src/modules/warranty_claims/i18n/pl.json +1 -0
- package/src/modules/wms/i18n/de.json +1 -0
- package/src/modules/wms/i18n/en.json +1 -0
- package/src/modules/wms/i18n/es.json +1 -0
- package/src/modules/wms/i18n/ko.json +1 -0
- package/src/modules/wms/i18n/pl.json +1 -0
|
@@ -30,7 +30,7 @@ async function loadOptions(path, params, mapItem) {
|
|
|
30
30
|
}
|
|
31
31
|
async function resolveOne(path, idParam, id, mapItem) {
|
|
32
32
|
const options = await loadOptions(path, { [idParam]: id, pageSize: "1" }, mapItem);
|
|
33
|
-
return options
|
|
33
|
+
return options.find((option) => option.value === id)?.label ?? id;
|
|
34
34
|
}
|
|
35
35
|
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
36
36
|
function buildLookupParams(query, pageSize) {
|
|
@@ -163,7 +163,7 @@ function PricePriceKindSelect({
|
|
|
163
163
|
allowCustomValues: false,
|
|
164
164
|
placeholder: t("catalog.prices.select.priceKind.placeholder", "Select a price kind\u2026"),
|
|
165
165
|
loadSuggestions: (query) => loadOptions("/api/catalog/price-kinds", buildLookupParams(query, "20"), mapItem),
|
|
166
|
-
resolveLabel: (id) => resolveOne("/api/catalog/price-kinds", "
|
|
166
|
+
resolveLabel: (id) => resolveOne("/api/catalog/price-kinds", "ids", id, mapItem)
|
|
167
167
|
}
|
|
168
168
|
);
|
|
169
169
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/catalog/components/prices/PriceScopeSelectors.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { ComboboxInput, type ComboboxOption } from '@open-mercato/ui/backend/inputs/ComboboxInput'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('catalog').child({ component: 'PriceScopeSelectors' })\n\ntype RemoteItem = Record<string, unknown>\n\nfunction pickString(item: RemoteItem, ...keys: string[]): string {\n for (const key of keys) {\n const value = item[key]\n if (typeof value === 'string' && value.length) return value\n }\n return ''\n}\n\nasync function loadOptions(\n path: string,\n params: Record<string, string>,\n mapItem: (item: RemoteItem) => ComboboxOption | null,\n): Promise<ComboboxOption[]> {\n try {\n const search = new URLSearchParams(params)\n const payload = await readApiResultOrThrow<{ items?: RemoteItem[] }>(\n `${path}?${search.toString()}`,\n undefined,\n { fallback: { items: [] } },\n )\n const items = Array.isArray(payload.items) ? payload.items : []\n return items.map(mapItem).filter((option): option is ComboboxOption => option !== null)\n } catch (err) {\n logger.error('catalog.prices.scopeSelect.load', { err, path })\n return []\n }\n}\n\nasync function resolveOne(\n path: string,\n idParam: string,\n id: string,\n mapItem: (item: RemoteItem) => ComboboxOption | null,\n): Promise<string> {\n const options = await loadOptions(path, { [idParam]: id, pageSize: '1' }, mapItem)\n return options
|
|
5
|
-
"mappings": ";AAkFI;AAhFJ,YAAY,WAAW;AACvB,SAAS,qBAA0C;AACnD,SAAS,4BAA4B;AACrC,SAAS,YAAY;AACrB,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,SAAS,EAAE,MAAM,EAAE,WAAW,sBAAsB,CAAC;AAIjF,SAAS,WAAW,SAAqB,MAAwB;AAC/D,aAAW,OAAO,MAAM;AACtB,UAAM,QAAQ,KAAK,GAAG;AACtB,QAAI,OAAO,UAAU,YAAY,MAAM,OAAQ,QAAO;AAAA,EACxD;AACA,SAAO;AACT;AAEA,eAAe,YACb,MACA,QACA,SAC2B;AAC3B,MAAI;AACF,UAAM,SAAS,IAAI,gBAAgB,MAAM;AACzC,UAAM,UAAU,MAAM;AAAA,MACpB,GAAG,IAAI,IAAI,OAAO,SAAS,CAAC;AAAA,MAC5B;AAAA,MACA,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE,EAAE;AAAA,IAC5B;AACA,UAAM,QAAQ,MAAM,QAAQ,QAAQ,KAAK,IAAI,QAAQ,QAAQ,CAAC;AAC9D,WAAO,MAAM,IAAI,OAAO,EAAE,OAAO,CAAC,WAAqC,WAAW,IAAI;AAAA,EACxF,SAAS,KAAK;AACZ,WAAO,MAAM,mCAAmC,EAAE,KAAK,KAAK,CAAC;AAC7D,WAAO,CAAC;AAAA,EACV;AACF;AAEA,eAAe,WACb,MACA,SACA,IACA,SACiB;AACjB,QAAM,UAAU,MAAM,YAAY,MAAM,EAAE,CAAC,OAAO,GAAG,IAAI,UAAU,IAAI,GAAG,OAAO;AACjF,SAAO,QAAQ,CAAC,GAAG,SAAS;
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { ComboboxInput, type ComboboxOption } from '@open-mercato/ui/backend/inputs/ComboboxInput'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('catalog').child({ component: 'PriceScopeSelectors' })\n\ntype RemoteItem = Record<string, unknown>\n\nfunction pickString(item: RemoteItem, ...keys: string[]): string {\n for (const key of keys) {\n const value = item[key]\n if (typeof value === 'string' && value.length) return value\n }\n return ''\n}\n\nasync function loadOptions(\n path: string,\n params: Record<string, string>,\n mapItem: (item: RemoteItem) => ComboboxOption | null,\n): Promise<ComboboxOption[]> {\n try {\n const search = new URLSearchParams(params)\n const payload = await readApiResultOrThrow<{ items?: RemoteItem[] }>(\n `${path}?${search.toString()}`,\n undefined,\n { fallback: { items: [] } },\n )\n const items = Array.isArray(payload.items) ? payload.items : []\n return items.map(mapItem).filter((option): option is ComboboxOption => option !== null)\n } catch (err) {\n logger.error('catalog.prices.scopeSelect.load', { err, path })\n return []\n }\n}\n\nasync function resolveOne(\n path: string,\n idParam: string,\n id: string,\n mapItem: (item: RemoteItem) => ComboboxOption | null,\n): Promise<string> {\n const options = await loadOptions(path, { [idParam]: id, pageSize: '1' }, mapItem)\n return options.find((option) => option.value === id)?.label ?? id\n}\n\nconst UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i\n\n/**\n * A typed query that is itself a record id (a pasted UUID) is searched via\n * the generic `ids=` narrowing instead of the entity's free-text `search=`\n * filter \u2014 free-text search matches title/name/sku columns, never the id\n * column, so pasting an exact id would otherwise return zero suggestions.\n */\nfunction buildLookupParams(query: string | undefined, pageSize: string): Record<string, string> {\n const trimmed = (query ?? '').trim()\n if (UUID_PATTERN.test(trimmed)) return { ids: trimmed, pageSize: '1' }\n return { search: trimmed, pageSize }\n}\n\nexport function PriceProductSelect({\n value,\n onChange,\n disabled,\n}: {\n value: string\n onChange: (next: string) => void\n disabled?: boolean\n}) {\n const t = useT()\n const mapItem = React.useCallback((item: RemoteItem): ComboboxOption | null => {\n const id = pickString(item, 'id')\n if (!id) return null\n const title = pickString(item, 'title', 'name') || id\n const sku = pickString(item, 'sku')\n return { value: id, label: sku ? `${title} (${sku})` : title }\n }, [])\n return (\n <ComboboxInput\n value={value}\n onChange={onChange}\n disabled={disabled}\n allowCustomValues={false}\n clearable\n placeholder={t('catalog.prices.select.product.placeholder', 'Search products\u2026')}\n loadSuggestions={(query) => loadOptions('/api/catalog/products', buildLookupParams(query, '10'), mapItem)}\n resolveLabel={(id) => resolveOne('/api/catalog/products', 'id', id, mapItem)}\n />\n )\n}\n\nexport function PriceVariantSelect({\n productId,\n value,\n onChange,\n}: {\n productId: string\n value: string\n onChange: (next: string) => void\n}) {\n const t = useT()\n const mapItem = React.useCallback((item: RemoteItem): ComboboxOption | null => {\n const id = pickString(item, 'id')\n if (!id) return null\n const name = pickString(item, 'name') || id\n const sku = pickString(item, 'sku')\n return { value: id, label: sku ? `${name} (${sku})` : name }\n }, [])\n const disabled = !productId\n return (\n <ComboboxInput\n value={value}\n onChange={onChange}\n disabled={disabled}\n allowCustomValues={false}\n clearable\n placeholder={\n disabled\n ? t('catalog.prices.form.field.product', 'Product')\n : t('catalog.prices.select.product.placeholder', 'Search products\u2026')\n }\n loadSuggestions={(query) =>\n disabled\n ? Promise.resolve([])\n : loadOptions('/api/catalog/variants', { ...buildLookupParams(query, '10'), productId }, mapItem)\n }\n resolveLabel={(id) => resolveOne('/api/catalog/variants', 'id', id, mapItem)}\n />\n )\n}\n\nexport function PriceCustomerSelect({\n value,\n onChange,\n}: {\n value: string\n onChange: (next: string) => void\n}) {\n const t = useT()\n const mapItem = React.useCallback((item: RemoteItem): ComboboxOption | null => {\n const id = pickString(item, 'id')\n if (!id) return null\n const name = pickString(item, 'displayName', 'display_name', 'name') || id\n const email = pickString(item, 'primaryEmail', 'primary_email')\n return { value: id, label: name, description: email || null }\n }, [])\n return (\n <ComboboxInput\n value={value}\n onChange={onChange}\n allowCustomValues={false}\n clearable\n placeholder={t('catalog.prices.select.customer.placeholder', 'Search customers\u2026')}\n loadSuggestions={(query) => loadOptions('/api/customers/people', buildLookupParams(query, '10'), mapItem)}\n resolveLabel={(id) => resolveOne('/api/customers/people', 'id', id, mapItem)}\n />\n )\n}\n\nexport function PriceChannelSelect({\n value,\n onChange,\n}: {\n value: string\n onChange: (next: string) => void\n}) {\n const t = useT()\n const mapItem = React.useCallback((item: RemoteItem): ComboboxOption | null => {\n const id = pickString(item, 'id')\n if (!id) return null\n const name = pickString(item, 'name') || id\n const code = pickString(item, 'code')\n return { value: id, label: code ? `${name} (${code})` : name }\n }, [])\n return (\n <ComboboxInput\n value={value}\n onChange={onChange}\n allowCustomValues={false}\n clearable\n placeholder={t('catalog.prices.select.channel.placeholder', 'Select a channel\u2026')}\n loadSuggestions={(query) => loadOptions('/api/sales/channels', buildLookupParams(query, '50'), mapItem)}\n resolveLabel={(id) => resolveOne('/api/sales/channels', 'id', id, mapItem)}\n />\n )\n}\n\nexport function PricePriceKindSelect({\n value,\n onChange,\n}: {\n value: string\n onChange: (next: string) => void\n}) {\n const t = useT()\n const mapItem = React.useCallback((item: RemoteItem): ComboboxOption | null => {\n const id = pickString(item, 'id')\n if (!id) return null\n const title = pickString(item, 'title') || id\n const code = pickString(item, 'code')\n return { value: id, label: code ? `${title} (${code})` : title }\n }, [])\n return (\n <ComboboxInput\n value={value}\n onChange={onChange}\n allowCustomValues={false}\n placeholder={t('catalog.prices.select.priceKind.placeholder', 'Select a price kind\u2026')}\n loadSuggestions={(query) => loadOptions('/api/catalog/price-kinds', buildLookupParams(query, '20'), mapItem)}\n resolveLabel={(id) => resolveOne('/api/catalog/price-kinds', 'ids', id, mapItem)}\n />\n )\n}\n\nexport function PriceCurrencySelect({\n value,\n onChange,\n}: {\n value: string\n onChange: (next: string) => void\n}) {\n const t = useT()\n const mapItem = React.useCallback((item: RemoteItem): ComboboxOption | null => {\n const code = pickString(item, 'value')\n if (!code) return null\n const label = pickString(item, 'label') || code\n return { value: code, label: label === code ? code : `${code} \u2014 ${label}` }\n }, [])\n return (\n <ComboboxInput\n value={value}\n onChange={onChange}\n allowCustomValues={false}\n placeholder={t('catalog.prices.select.currency.placeholder', 'Select a currency\u2026')}\n loadSuggestions={(query) => loadOptions('/api/currencies/currencies/options', { search: query ?? '', limit: '20' }, mapItem)}\n resolveLabel={(id) => resolveOne('/api/currencies/currencies/options', 'search', id, mapItem)}\n />\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
4
|
+
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
5
|
+
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
6
|
+
import { CustomerUser, CustomerUserSession } from "@open-mercato/core/modules/customer_accounts/data/entities";
|
|
7
|
+
const metadata = {};
|
|
8
|
+
const paramsSchema = z.object({
|
|
9
|
+
id: z.string().uuid(),
|
|
10
|
+
sessionId: z.string().uuid()
|
|
11
|
+
});
|
|
12
|
+
async function DELETE(req, { params }) {
|
|
13
|
+
const parsedParams = paramsSchema.safeParse(params);
|
|
14
|
+
if (!parsedParams.success) {
|
|
15
|
+
return NextResponse.json({ ok: false, error: "Invalid identifier" }, { status: 400 });
|
|
16
|
+
}
|
|
17
|
+
const { id: userId, sessionId } = parsedParams.data;
|
|
18
|
+
const auth = await getAuthFromRequest(req);
|
|
19
|
+
if (!auth) {
|
|
20
|
+
return NextResponse.json({ ok: false, error: "Authentication required" }, { status: 401 });
|
|
21
|
+
}
|
|
22
|
+
const container = await createRequestContainer();
|
|
23
|
+
const rbacService = container.resolve("rbacService");
|
|
24
|
+
const hasAccess = await rbacService.userHasAllFeatures(auth.sub, ["customer_accounts.manage"], { tenantId: auth.tenantId, organizationId: auth.orgId });
|
|
25
|
+
if (!hasAccess) {
|
|
26
|
+
return NextResponse.json({ ok: false, error: "Insufficient permissions" }, { status: 403 });
|
|
27
|
+
}
|
|
28
|
+
const em = container.resolve("em");
|
|
29
|
+
const scope = { tenantId: auth.tenantId, organizationId: auth.orgId };
|
|
30
|
+
const user = await findOneWithDecryption(
|
|
31
|
+
em,
|
|
32
|
+
CustomerUser,
|
|
33
|
+
{ id: userId, tenantId: auth.tenantId, organizationId: auth.orgId, deletedAt: null },
|
|
34
|
+
void 0,
|
|
35
|
+
scope
|
|
36
|
+
);
|
|
37
|
+
if (!user) {
|
|
38
|
+
return NextResponse.json({ ok: false, error: "User not found" }, { status: 404 });
|
|
39
|
+
}
|
|
40
|
+
const session = await findOneWithDecryption(
|
|
41
|
+
em,
|
|
42
|
+
CustomerUserSession,
|
|
43
|
+
{ id: sessionId, user: user.id, deletedAt: null },
|
|
44
|
+
void 0,
|
|
45
|
+
scope
|
|
46
|
+
);
|
|
47
|
+
if (!session) {
|
|
48
|
+
return NextResponse.json({ ok: false, error: "Session not found" }, { status: 404 });
|
|
49
|
+
}
|
|
50
|
+
const customerSessionService = container.resolve("customerSessionService");
|
|
51
|
+
await customerSessionService.revokeSession(session.id);
|
|
52
|
+
return NextResponse.json({ ok: true });
|
|
53
|
+
}
|
|
54
|
+
const successSchema = z.object({ ok: z.literal(true) });
|
|
55
|
+
const errorSchema = z.object({ ok: z.literal(false), error: z.string() });
|
|
56
|
+
const methodDoc = {
|
|
57
|
+
summary: "Revoke a customer user session (admin)",
|
|
58
|
+
description: "Allows staff to end one active portal session of a customer user. The customer is signed out of that session on the next request.",
|
|
59
|
+
tags: ["Customer Accounts Admin"],
|
|
60
|
+
responses: [{ status: 200, description: "Session revoked", schema: successSchema }],
|
|
61
|
+
errors: [
|
|
62
|
+
{ status: 400, description: "Invalid identifier", schema: errorSchema },
|
|
63
|
+
{ status: 401, description: "Not authenticated", schema: errorSchema },
|
|
64
|
+
{ status: 403, description: "Insufficient permissions", schema: errorSchema },
|
|
65
|
+
{ status: 404, description: "User or session not found", schema: errorSchema }
|
|
66
|
+
]
|
|
67
|
+
};
|
|
68
|
+
const openApi = {
|
|
69
|
+
summary: "Revoke customer user session (admin)",
|
|
70
|
+
pathParams: paramsSchema,
|
|
71
|
+
methods: { DELETE: methodDoc }
|
|
72
|
+
};
|
|
73
|
+
export {
|
|
74
|
+
DELETE,
|
|
75
|
+
metadata,
|
|
76
|
+
openApi
|
|
77
|
+
};
|
|
78
|
+
//# sourceMappingURL=%5BsessionId%5D.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../../../src/modules/customer_accounts/api/admin/users/%5Bid%5D/sessions/%5BsessionId%5D.ts"],
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { CustomerUser, CustomerUserSession } from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { CustomerSessionService } from '@open-mercato/core/modules/customer_accounts/services/customerSessionService'\n\nexport const metadata = {}\n\nconst paramsSchema = z.object({\n id: z.string().uuid(),\n sessionId: z.string().uuid(),\n})\n\nexport async function DELETE(req: Request, { params }: { params: { id: string; sessionId: string } }) {\n const parsedParams = paramsSchema.safeParse(params)\n if (!parsedParams.success) {\n return NextResponse.json({ ok: false, error: 'Invalid identifier' }, { status: 400 })\n }\n const { id: userId, sessionId } = parsedParams.data\n\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ ok: false, error: 'Authentication required' }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const rbacService = container.resolve('rbacService') as RbacService\n const hasAccess = await rbacService.userHasAllFeatures(auth.sub, ['customer_accounts.manage'], { tenantId: auth.tenantId, organizationId: auth.orgId })\n if (!hasAccess) {\n return NextResponse.json({ ok: false, error: 'Insufficient permissions' }, { status: 403 })\n }\n\n const em = container.resolve('em') as EntityManager\n const scope = { tenantId: auth.tenantId, organizationId: auth.orgId }\n\n const user = await findOneWithDecryption(\n em,\n CustomerUser,\n { id: userId, tenantId: auth.tenantId, organizationId: auth.orgId, deletedAt: null } as any,\n undefined,\n scope,\n )\n if (!user) {\n return NextResponse.json({ ok: false, error: 'User not found' }, { status: 404 })\n }\n\n const session = await findOneWithDecryption(\n em,\n CustomerUserSession,\n { id: sessionId, user: user.id as any, deletedAt: null } as any,\n undefined,\n scope,\n )\n if (!session) {\n return NextResponse.json({ ok: false, error: 'Session not found' }, { status: 404 })\n }\n\n const customerSessionService = container.resolve('customerSessionService') as CustomerSessionService\n await customerSessionService.revokeSession(session.id)\n\n return NextResponse.json({ ok: true })\n}\n\nconst successSchema = z.object({ ok: z.literal(true) })\nconst errorSchema = z.object({ ok: z.literal(false), error: z.string() })\n\nconst methodDoc: OpenApiMethodDoc = {\n summary: 'Revoke a customer user session (admin)',\n description: 'Allows staff to end one active portal session of a customer user. The customer is signed out of that session on the next request.',\n tags: ['Customer Accounts Admin'],\n responses: [{ status: 200, description: 'Session revoked', schema: successSchema }],\n errors: [\n { status: 400, description: 'Invalid identifier', schema: errorSchema },\n { status: 401, description: 'Not authenticated', schema: errorSchema },\n { status: 403, description: 'Insufficient permissions', schema: errorSchema },\n { status: 404, description: 'User or session not found', schema: errorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Revoke customer user session (admin)',\n pathParams: paramsSchema,\n methods: { DELETE: methodDoc },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
4
|
+
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
5
|
+
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
6
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
7
|
+
import { getTelemetryRuntime } from "@open-mercato/shared/lib/telemetry/runtime";
|
|
8
|
+
import { listTenantPriceKinds } from "../../../lib/priceKindScope.js";
|
|
9
|
+
const logger = createLogger("customer_groups");
|
|
10
|
+
const metadata = {
|
|
11
|
+
GET: { requireAuth: true, requireFeatures: ["customer_groups.terms.view"] }
|
|
12
|
+
};
|
|
13
|
+
const querySchema = z.object({
|
|
14
|
+
search: z.string().max(200).optional(),
|
|
15
|
+
ids: z.string().optional().transform((value) => value ? value.split(",").map((id) => id.trim()).filter(Boolean) : []).pipe(z.array(z.string().uuid()).max(100)),
|
|
16
|
+
pageSize: z.coerce.number().int().min(1).max(100).default(20)
|
|
17
|
+
});
|
|
18
|
+
async function GET(req) {
|
|
19
|
+
const { translate } = await resolveTranslations();
|
|
20
|
+
try {
|
|
21
|
+
const auth = await getAuthFromRequest(req);
|
|
22
|
+
if (!auth) {
|
|
23
|
+
return NextResponse.json(
|
|
24
|
+
{ error: translate("customer_groups.errors.unauthorized", "Unauthorized") },
|
|
25
|
+
{ status: 401 }
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
const tenantId = auth.tenantId ?? null;
|
|
29
|
+
if (!tenantId) {
|
|
30
|
+
return NextResponse.json(
|
|
31
|
+
{ error: translate("customer_groups.errors.tenant_required", "Tenant context is required") },
|
|
32
|
+
{ status: 400 }
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
const url = new URL(req.url);
|
|
36
|
+
const parsedQuery = querySchema.safeParse({
|
|
37
|
+
search: url.searchParams.get("search") ?? void 0,
|
|
38
|
+
ids: url.searchParams.get("ids") ?? void 0,
|
|
39
|
+
pageSize: url.searchParams.get("pageSize") ?? void 0
|
|
40
|
+
});
|
|
41
|
+
if (!parsedQuery.success) {
|
|
42
|
+
return NextResponse.json(
|
|
43
|
+
{ error: translate("customer_groups.errors.invalid_input", "Invalid input") },
|
|
44
|
+
{ status: 400 }
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
const { search, ids, pageSize } = parsedQuery.data;
|
|
48
|
+
const container = await createRequestContainer();
|
|
49
|
+
const em = container.resolve("em");
|
|
50
|
+
const items = await listTenantPriceKinds(em, tenantId, { search, ids, limit: pageSize });
|
|
51
|
+
return NextResponse.json({ items });
|
|
52
|
+
} catch (err) {
|
|
53
|
+
logger.error("customer_groups.terms.priceKinds failed", { err });
|
|
54
|
+
getTelemetryRuntime()?.reportError(err, { module: "customer_groups", code: "customer_groups.terms_price_kinds_failed" });
|
|
55
|
+
return NextResponse.json(
|
|
56
|
+
{ error: translate("customer_groups.errors.load_failed", "Failed to load commercial terms") },
|
|
57
|
+
{ status: 500 }
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const priceKindOptionSchema = z.object({ id: z.string().uuid(), code: z.string(), title: z.string() });
|
|
62
|
+
const priceKindsResponseSchema = z.object({ items: z.array(priceKindOptionSchema) });
|
|
63
|
+
const priceKindsErrorSchema = z.object({ error: z.string() });
|
|
64
|
+
const openApi = {
|
|
65
|
+
tag: "CustomerGroups",
|
|
66
|
+
summary: "List price kinds selectable in commercial terms",
|
|
67
|
+
methods: {
|
|
68
|
+
GET: {
|
|
69
|
+
summary: "List price kinds selectable in commercial terms",
|
|
70
|
+
description: "Returns the tenant\u2019s catalog price kinds (id, code, title) for the commercial terms price kind picker, readable with customer_groups.terms.view instead of catalog.settings.manage. Returns an empty list when the catalog module is not installed.",
|
|
71
|
+
query: z.object({
|
|
72
|
+
search: z.string().optional(),
|
|
73
|
+
ids: z.string().optional().describe("Comma-separated price kind ids"),
|
|
74
|
+
pageSize: z.coerce.number().int().min(1).max(100).optional()
|
|
75
|
+
}),
|
|
76
|
+
responses: [{ status: 200, description: "Selectable price kinds", schema: priceKindsResponseSchema }],
|
|
77
|
+
errors: [
|
|
78
|
+
{ status: 400, description: "Tenant context is required or the query is invalid", schema: priceKindsErrorSchema },
|
|
79
|
+
{ status: 401, description: "Unauthorized", schema: priceKindsErrorSchema },
|
|
80
|
+
{ status: 500, description: "Unexpected failure", schema: priceKindsErrorSchema }
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
export {
|
|
86
|
+
GET,
|
|
87
|
+
metadata,
|
|
88
|
+
openApi
|
|
89
|
+
};
|
|
90
|
+
//# sourceMappingURL=route.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../src/modules/customer_groups/api/customer-groups/price-kinds/route.ts"],
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport { listTenantPriceKinds } from '../../../lib/priceKindScope'\n\nconst logger = createLogger('customer_groups')\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['customer_groups.terms.view'] },\n}\n\nconst querySchema = z.object({\n search: z.string().max(200).optional(),\n ids: z\n .string()\n .optional()\n .transform((value) => (value ? value.split(',').map((id) => id.trim()).filter(Boolean) : []))\n .pipe(z.array(z.string().uuid()).max(100)),\n pageSize: z.coerce.number().int().min(1).max(100).default(20),\n})\n\nexport async function GET(req: Request) {\n const { translate } = await resolveTranslations()\n try {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json(\n { error: translate('customer_groups.errors.unauthorized', 'Unauthorized') },\n { status: 401 },\n )\n }\n const tenantId = auth.tenantId ?? null\n if (!tenantId) {\n return NextResponse.json(\n { error: translate('customer_groups.errors.tenant_required', 'Tenant context is required') },\n { status: 400 },\n )\n }\n\n const url = new URL(req.url)\n const parsedQuery = querySchema.safeParse({\n search: url.searchParams.get('search') ?? undefined,\n ids: url.searchParams.get('ids') ?? undefined,\n pageSize: url.searchParams.get('pageSize') ?? undefined,\n })\n if (!parsedQuery.success) {\n return NextResponse.json(\n { error: translate('customer_groups.errors.invalid_input', 'Invalid input') },\n { status: 400 },\n )\n }\n const { search, ids, pageSize } = parsedQuery.data\n\n const container = await createRequestContainer()\n const em = container.resolve<EntityManager>('em')\n const items = await listTenantPriceKinds(em, tenantId, { search, ids, limit: pageSize })\n return NextResponse.json({ items })\n } catch (err) {\n logger.error('customer_groups.terms.priceKinds failed', { err })\n getTelemetryRuntime()?.reportError(err, { module: 'customer_groups', code: 'customer_groups.terms_price_kinds_failed' })\n return NextResponse.json(\n { error: translate('customer_groups.errors.load_failed', 'Failed to load commercial terms') },\n { status: 500 },\n )\n }\n}\n\nconst priceKindOptionSchema = z.object({ id: z.string().uuid(), code: z.string(), title: z.string() })\nconst priceKindsResponseSchema = z.object({ items: z.array(priceKindOptionSchema) })\nconst priceKindsErrorSchema = z.object({ error: z.string() })\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'CustomerGroups',\n summary: 'List price kinds selectable in commercial terms',\n methods: {\n GET: {\n summary: 'List price kinds selectable in commercial terms',\n description:\n 'Returns the tenant\u2019s catalog price kinds (id, code, title) for the commercial terms price kind picker, readable with customer_groups.terms.view instead of catalog.settings.manage. Returns an empty list when the catalog module is not installed.',\n query: z.object({\n search: z.string().optional(),\n ids: z.string().optional().describe('Comma-separated price kind ids'),\n pageSize: z.coerce.number().int().min(1).max(100).optional(),\n }),\n responses: [{ status: 200, description: 'Selectable price kinds', schema: priceKindsResponseSchema }],\n errors: [\n { status: 400, description: 'Tenant context is required or the query is invalid', schema: priceKindsErrorSchema },\n { status: 401, description: 'Unauthorized', schema: priceKindsErrorSchema },\n { status: 500, description: 'Unexpected failure', schema: priceKindsErrorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -229,7 +229,11 @@ function CustomerGroupsPage() {
|
|
|
229
229
|
setReloadToken((token) => token + 1);
|
|
230
230
|
} catch (error) {
|
|
231
231
|
if (surfaceRecordConflict(error, t, { onRefresh: () => setReloadToken((token) => token + 1) })) return;
|
|
232
|
-
|
|
232
|
+
const serverMessage = error?.error;
|
|
233
|
+
flash(
|
|
234
|
+
typeof serverMessage === "string" && serverMessage.trim() ? serverMessage : t("customer_groups.groups.list.flash.deleteError", "Could not delete the customer group"),
|
|
235
|
+
"error"
|
|
236
|
+
);
|
|
233
237
|
}
|
|
234
238
|
},
|
|
235
239
|
[t, confirmDialog, mutationContextId, retryLastMutation, runMutation]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customer_groups/backend/customer-groups/page.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { ListEmptyState } from '@open-mercato/ui/backend/filters/ListEmptyState'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { StatusBadge, type StatusMap } from '@open-mercato/ui/primitives/status-badge'\nimport { Tag, type TagMap } from '@open-mercato/ui/primitives/tag'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Plus, GripVertical } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } 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 { useBackendChrome } from '@open-mercato/ui/backend/BackendChromeProvider'\nimport { ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { Alert } from '@open-mercato/ui/primitives/alert'\nimport { hasFeature } from '@open-mercato/shared/security/features'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\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 { OrphanBanner } from '../../components/OrphanBanner'\n\ntype CustomerGroupRow = {\n id: string\n organization_id: string | null\n tenant_id: string | null\n code: string\n name: string\n description: string | null\n kind: string\n parent_id: string | null\n priority: number\n is_default: boolean\n is_active: boolean\n created_at: string | null\n updated_at: string | null\n}\n\ntype ResponsePayload = {\n items: CustomerGroupRow[]\n total: number\n page: number\n totalPages: number\n totalIsCapped?: boolean\n}\n\nconst PAGE_SIZE = 100\nconst MANAGE_FEATURE = 'customer_groups.groups.manage'\n\nconst kindTagMap: TagMap<string> = {\n b2c: 'info',\n b2b: 'success',\n internal: 'neutral',\n partner: 'brand',\n}\n\nconst activeStatusMap: StatusMap<'active' | 'inactive'> = {\n active: 'success',\n inactive: 'neutral',\n}\n\n// Drag handle for one row's priority cell. Mirrors the `SortableColumnItem`\n// pattern in `packages/ui/src/backend/columns/ColumnChooserPanel.tsx` \u2014 the only\n// other place in this repo wiring `@dnd-kit/core` + `@dnd-kit/sortable`.\n//\n// Known limitation: `DataTable`'s `TableRow` (packages/ui/src/primitives/table.tsx)\n// is a plain function component that does not forward `ref`, so `useSortable`'s\n// `setNodeRef` cannot be attached to the actual `<tr>` \u2014 only to this handle. The\n// drag therefore animates the handle icon, not the whole row, while the\n// underlying reorder math (collision detection, `arrayMove`) is fully correct\n// because the handles stack in the same vertical order as the rows. A full\n// row-lift animation would require `TableRow` to forward `ref`, which is a\n// DataTable/Table primitive contract change gated behind \"Ask First\" in\n// packages/ui/AGENTS.md.\nfunction DragHandle({ id }: { id: string }) {\n const t = useT()\n const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id })\n const style: React.CSSProperties = {\n transform: CSS.Transform.toString(transform),\n transition,\n opacity: isDragging ? 0.5 : 1,\n }\n return (\n <span\n ref={setNodeRef}\n style={style}\n className=\"inline-flex cursor-grab items-center text-muted-foreground active:cursor-grabbing\"\n aria-label={t('customer_groups.groups.list.dragHandle', 'Reorder')}\n {...attributes}\n {...listeners}\n >\n <GripVertical className=\"size-4\" />\n </span>\n )\n}\n\nexport default function CustomerGroupsPage() {\n const t = useT()\n const { confirm: confirmDialog, ConfirmDialogElement } = useConfirmDialog()\n const { payload: backendChromePayload, isReady: backendChromeReady } = useBackendChrome()\n const canManage = backendChromeReady && hasFeature(backendChromePayload?.grantedFeatures, MANAGE_FEATURE)\n const [rows, setRows] = React.useState<CustomerGroupRow[]>([])\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 [filters, setFilters] = React.useState<FilterValues>({})\n const [isLoading, setIsLoading] = React.useState(true)\n const [loadError, setLoadError] = React.useState<string | null>(null)\n const [reloadToken, setReloadToken] = React.useState(0)\n const mutationContextId = 'customer-groups-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 // Always requested explicitly: drag-reorder relies on the displayed\n // order matching `CustomerGroup.priority` ascending.\n params.set('sortField', 'priority')\n params.set('sortDir', 'asc')\n if (search) params.set('search', search)\n if (filters.isActive === 'true') params.set('isActive', 'true')\n if (filters.isActive === 'false') params.set('isActive', 'false')\n\n const fallback: ResponsePayload = { items: [], total: 0, page: 1, totalPages: 1 }\n const call = await apiCall<ResponsePayload>(\n `/api/customer_groups/customer-groups?${params.toString()}`,\n undefined,\n { fallback },\n )\n if (cancelled) return\n if (!call.ok) {\n setRows([])\n setLoadError(t('customer_groups.groups.list.error.load', 'Failed to load customer groups'))\n return\n }\n const payload = call.result ?? fallback\n const items = Array.isArray(payload.items) ? payload.items : []\n setRows(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 setLoadError(t('customer_groups.groups.list.error.load', 'Failed to load customer groups'))\n }\n } finally {\n if (!cancelled) setIsLoading(false)\n }\n }\n load()\n return () => {\n cancelled = true\n }\n }, [page, search, filters, 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 setFilters(next)\n setPage(1)\n }, [])\n\n // Drag-reorder only makes sense over the full, priority-ordered set: a search\n // term or an is_active filter narrows the visible rows to a subset, and\n // rewriting priorities for just that subset (in gaps of 10, per the reorder\n // command) would scramble the relative order of rows currently hidden by the\n // filter. Reordering stays available only on the unfiltered view.\n // The same holds for a paginated view: the reorder command rewrites the posted\n // ids in gaps of 10, so posting one page of a multi-page set would collide with\n // (and scramble) the priorities of groups on other pages. Reordering therefore\n // also requires the whole set to be loaded, and the manage feature.\n const isUnfilteredView = !search && (!filters.isActive || filters.isActive === '')\n const allGroupsLoaded = page === 1 && !totalIsCapped && total <= rows.length\n const dragReorderEnabled = canManage && !loadError && isUnfilteredView && allGroupsLoaded\n const showReorderUnavailableHint = canManage && !loadError && !isLoading && isUnfilteredView && !allGroupsLoaded\n\n const sensors = useSensors(\n useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),\n useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),\n )\n\n const handleDragEnd = React.useCallback(\n async (event: DragEndEvent) => {\n const { active, over } = event\n if (!over || active.id === over.id) return\n const oldIndex = rows.findIndex((row) => row.id === active.id)\n const newIndex = rows.findIndex((row) => row.id === over.id)\n if (oldIndex === -1 || newIndex === -1) return\n\n const previousRows = rows\n const reordered = [...rows]\n const [moved] = reordered.splice(oldIndex, 1)\n reordered.splice(newIndex, 0, moved)\n // Optimistic reorder \u2014 visible immediately, no page reload.\n setRows(reordered)\n\n const ids = reordered.map((row) => row.id)\n try {\n await runMutation({\n operation: async () => {\n const call = await apiCall('/api/customer_groups/customer-groups/reorder', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ ids }),\n })\n if (!call.ok) {\n throw Object.assign(new Error('[internal] customer_groups.reorder failed'), { status: call.status })\n }\n return call\n },\n context: {\n formId: mutationContextId,\n resourceKind: 'customer_groups.group',\n resourceId: 'reorder',\n retryLastMutation,\n },\n mutationPayload: { ids },\n })\n flash(t('customer_groups.groups.list.flash.reordered', 'Group order updated'), 'success')\n setReloadToken((token) => token + 1)\n } catch {\n // Revert the optimistic reorder on failure.\n setRows(previousRows)\n flash(t('customer_groups.groups.list.flash.reorderError', 'Could not save the new order'), 'error')\n }\n },\n [mutationContextId, retryLastMutation, rows, runMutation, t],\n )\n\n const handleDelete = React.useCallback(\n async (row: CustomerGroupRow) => {\n const confirmed = await confirmDialog({\n title: t('customer_groups.groups.list.confirmDelete', 'Delete customer group \"{code}\"?', { code: row.code }),\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.updated_at),\n () => apiCall(`/api/customer_groups/customer-groups?id=${encodeURIComponent(row.id)}`, { method: 'DELETE' }),\n )\n if (!call.ok) {\n throw Object.assign(new Error('[internal] customer_groups.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: 'customer_groups.group',\n resourceId: row.id,\n retryLastMutation,\n },\n mutationPayload: { id: row.id },\n })\n flash(t('customer_groups.groups.list.flash.deleted', 'Customer group deleted'), 'success')\n setReloadToken((token) => token + 1)\n } catch (error) {\n if (surfaceRecordConflict(error, t, { onRefresh: () => setReloadToken((token) => token + 1) })) return\n flash(t('customer_groups.groups.list.flash.deleteError', 'Could not delete the customer group'), 'error')\n }\n },\n [t, confirmDialog, mutationContextId, retryLastMutation, runMutation],\n )\n\n const columns = React.useMemo<ColumnDef<CustomerGroupRow>[]>(\n () => [\n {\n accessorKey: 'priority',\n header: t('customer_groups.groups.list.columns.priority', 'Priority'),\n cell: ({ row }) => (\n <div className=\"flex items-center gap-2\">\n {dragReorderEnabled ? (\n <DragHandle id={row.original.id} />\n ) : (\n <GripVertical className=\"size-4 text-muted-foreground/40\" aria-hidden=\"true\" />\n )}\n <span className=\"tabular-nums text-muted-foreground\">{row.original.priority}</span>\n </div>\n ),\n },\n {\n accessorKey: 'code',\n header: t('customer_groups.groups.list.columns.code', 'Code'),\n cell: ({ row }) => <span className=\"font-mono font-medium\">{row.original.code}</span>,\n },\n {\n accessorKey: 'name',\n header: t('customer_groups.groups.list.columns.name', 'Name'),\n },\n {\n accessorKey: 'kind',\n header: t('customer_groups.groups.list.columns.kind', 'Kind'),\n cell: ({ row }) => (\n <Tag variant={kindTagMap[row.original.kind] ?? 'default'} dot>\n {t(`customer_groups.groups.kind.${row.original.kind}`, row.original.kind)}\n </Tag>\n ),\n },\n {\n accessorKey: 'is_default',\n header: t('customer_groups.groups.list.columns.isDefault', 'Default'),\n cell: ({ row }) =>\n row.original.is_default ? (\n <Tag variant=\"brand\" dot>\n {t('customer_groups.groups.list.defaultTag', 'Default')}\n </Tag>\n ) : (\n <span className=\"text-muted-foreground\">\u2014</span>\n ),\n },\n {\n accessorKey: 'is_active',\n header: t('customer_groups.groups.list.columns.isActive', 'Status'),\n cell: ({ row }) => (\n <StatusBadge variant={activeStatusMap[row.original.is_active ? 'active' : 'inactive']} dot>\n {row.original.is_active\n ? t('customer_groups.groups.list.status.active', 'Active')\n : t('customer_groups.groups.list.status.inactive', 'Inactive')}\n </StatusBadge>\n ),\n },\n {\n accessorKey: 'updated_at',\n header: t('customer_groups.groups.list.columns.updatedAt', 'Updated'),\n cell: ({ row }) => (row.original.updated_at ? new Date(row.original.updated_at).toLocaleString() : '\u2014'),\n },\n ],\n [t, dragReorderEnabled],\n )\n\n const filterDefs = React.useMemo<FilterDef[]>(\n () => [\n {\n id: 'isActive',\n label: t('customer_groups.groups.list.filters.status', 'Status'),\n type: 'select',\n options: [\n { label: t('customer_groups.groups.list.filters.all', 'All'), value: '' },\n { label: t('customer_groups.groups.list.filters.active', 'Active'), value: 'true' },\n { label: t('customer_groups.groups.list.filters.inactive', 'Inactive'), value: 'false' },\n ],\n },\n ],\n [t],\n )\n\n const table = (\n <DataTable\n title={t('customer_groups.groups.list.title', 'Customer groups')}\n titleHeadingLevel={1}\n columns={columns}\n data={rows}\n sortable={false}\n searchValue={search}\n onSearchChange={handleSearchChange}\n searchPlaceholder={t('customer_groups.groups.list.searchPlaceholder', 'Search groups...')}\n filters={filterDefs}\n filterValues={filters}\n onFiltersApply={handleFiltersApply}\n onFiltersClear={() => handleFiltersApply({})}\n actions={\n canManage ? (\n <Button asChild>\n <Link href=\"/backend/customer-groups/create\">\n <Plus className=\"mr-2 h-4 w-4\" />\n {t('customer_groups.groups.list.actions.create', 'Create group')}\n </Link>\n </Button>\n ) : undefined\n }\n rowActions={(row) => (\n <RowActions\n items={canManage ? [\n { id: 'edit', label: t('common.edit', 'Edit'), href: `/backend/customer-groups/${row.id}/edit` },\n { id: 'delete', label: t('common.delete', 'Delete'), destructive: true, onSelect: () => handleDelete(row) },\n ] : [\n { id: 'open', label: t('common.open', 'Open'), href: `/backend/customer-groups/${row.id}/edit` },\n ]}\n />\n )}\n emptyState={\n <ListEmptyState\n entityName={t('customer_groups.groups.list.title', 'Customer groups')}\n title={t('customer_groups.groups.list.empty.title', 'No customer groups yet')}\n description={t(\n 'customer_groups.groups.list.empty.description',\n 'Create your first group to start organizing customers.',\n )}\n createHref={canManage ? '/backend/customer-groups/create' : undefined}\n createLabel={t('customer_groups.groups.list.empty.createLabel', 'Create your first group')}\n />\n }\n error={loadError ? <ErrorMessage label={loadError} /> : null}\n pagination={{\n page,\n pageSize: PAGE_SIZE,\n total,\n totalPages,\n totalIsCapped,\n onPageChange: setPage,\n }}\n isLoading={isLoading}\n />\n )\n\n return (\n <Page>\n <PageBody>\n {canManage ? <OrphanBanner /> : null}\n {showReorderUnavailableHint ? (\n <Alert status=\"information\" size=\"sm\" className=\"mb-4\">\n {t(\n 'customer_groups.groups.list.reorderUnavailable',\n 'Drag-to-reorder is off because there are more groups than fit on one page. Edit a group to change its priority.',\n )}\n </Alert>\n ) : null}\n {dragReorderEnabled ? (\n <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>\n <SortableContext items={rows.map((row) => row.id)} strategy={verticalListSortingStrategy}>\n {table}\n </SortableContext>\n </DndContext>\n ) : (\n table\n )}\n </PageBody>\n {ConfirmDialogElement}\n </Page>\n )\n}\n"],
|
|
5
|
-
"mappings": ";AAgHM,
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { ListEmptyState } from '@open-mercato/ui/backend/filters/ListEmptyState'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { StatusBadge, type StatusMap } from '@open-mercato/ui/primitives/status-badge'\nimport { Tag, type TagMap } from '@open-mercato/ui/primitives/tag'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Plus, GripVertical } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } 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 { useBackendChrome } from '@open-mercato/ui/backend/BackendChromeProvider'\nimport { ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { Alert } from '@open-mercato/ui/primitives/alert'\nimport { hasFeature } from '@open-mercato/shared/security/features'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\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 { OrphanBanner } from '../../components/OrphanBanner'\n\ntype CustomerGroupRow = {\n id: string\n organization_id: string | null\n tenant_id: string | null\n code: string\n name: string\n description: string | null\n kind: string\n parent_id: string | null\n priority: number\n is_default: boolean\n is_active: boolean\n created_at: string | null\n updated_at: string | null\n}\n\ntype ResponsePayload = {\n items: CustomerGroupRow[]\n total: number\n page: number\n totalPages: number\n totalIsCapped?: boolean\n}\n\nconst PAGE_SIZE = 100\nconst MANAGE_FEATURE = 'customer_groups.groups.manage'\n\nconst kindTagMap: TagMap<string> = {\n b2c: 'info',\n b2b: 'success',\n internal: 'neutral',\n partner: 'brand',\n}\n\nconst activeStatusMap: StatusMap<'active' | 'inactive'> = {\n active: 'success',\n inactive: 'neutral',\n}\n\n// Drag handle for one row's priority cell. Mirrors the `SortableColumnItem`\n// pattern in `packages/ui/src/backend/columns/ColumnChooserPanel.tsx` \u2014 the only\n// other place in this repo wiring `@dnd-kit/core` + `@dnd-kit/sortable`.\n//\n// Known limitation: `DataTable`'s `TableRow` (packages/ui/src/primitives/table.tsx)\n// is a plain function component that does not forward `ref`, so `useSortable`'s\n// `setNodeRef` cannot be attached to the actual `<tr>` \u2014 only to this handle. The\n// drag therefore animates the handle icon, not the whole row, while the\n// underlying reorder math (collision detection, `arrayMove`) is fully correct\n// because the handles stack in the same vertical order as the rows. A full\n// row-lift animation would require `TableRow` to forward `ref`, which is a\n// DataTable/Table primitive contract change gated behind \"Ask First\" in\n// packages/ui/AGENTS.md.\nfunction DragHandle({ id }: { id: string }) {\n const t = useT()\n const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id })\n const style: React.CSSProperties = {\n transform: CSS.Transform.toString(transform),\n transition,\n opacity: isDragging ? 0.5 : 1,\n }\n return (\n <span\n ref={setNodeRef}\n style={style}\n className=\"inline-flex cursor-grab items-center text-muted-foreground active:cursor-grabbing\"\n aria-label={t('customer_groups.groups.list.dragHandle', 'Reorder')}\n {...attributes}\n {...listeners}\n >\n <GripVertical className=\"size-4\" />\n </span>\n )\n}\n\nexport default function CustomerGroupsPage() {\n const t = useT()\n const { confirm: confirmDialog, ConfirmDialogElement } = useConfirmDialog()\n const { payload: backendChromePayload, isReady: backendChromeReady } = useBackendChrome()\n const canManage = backendChromeReady && hasFeature(backendChromePayload?.grantedFeatures, MANAGE_FEATURE)\n const [rows, setRows] = React.useState<CustomerGroupRow[]>([])\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 [filters, setFilters] = React.useState<FilterValues>({})\n const [isLoading, setIsLoading] = React.useState(true)\n const [loadError, setLoadError] = React.useState<string | null>(null)\n const [reloadToken, setReloadToken] = React.useState(0)\n const mutationContextId = 'customer-groups-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 // Always requested explicitly: drag-reorder relies on the displayed\n // order matching `CustomerGroup.priority` ascending.\n params.set('sortField', 'priority')\n params.set('sortDir', 'asc')\n if (search) params.set('search', search)\n if (filters.isActive === 'true') params.set('isActive', 'true')\n if (filters.isActive === 'false') params.set('isActive', 'false')\n\n const fallback: ResponsePayload = { items: [], total: 0, page: 1, totalPages: 1 }\n const call = await apiCall<ResponsePayload>(\n `/api/customer_groups/customer-groups?${params.toString()}`,\n undefined,\n { fallback },\n )\n if (cancelled) return\n if (!call.ok) {\n setRows([])\n setLoadError(t('customer_groups.groups.list.error.load', 'Failed to load customer groups'))\n return\n }\n const payload = call.result ?? fallback\n const items = Array.isArray(payload.items) ? payload.items : []\n setRows(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 setLoadError(t('customer_groups.groups.list.error.load', 'Failed to load customer groups'))\n }\n } finally {\n if (!cancelled) setIsLoading(false)\n }\n }\n load()\n return () => {\n cancelled = true\n }\n }, [page, search, filters, 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 setFilters(next)\n setPage(1)\n }, [])\n\n // Drag-reorder only makes sense over the full, priority-ordered set: a search\n // term or an is_active filter narrows the visible rows to a subset, and\n // rewriting priorities for just that subset (in gaps of 10, per the reorder\n // command) would scramble the relative order of rows currently hidden by the\n // filter. Reordering stays available only on the unfiltered view.\n // The same holds for a paginated view: the reorder command rewrites the posted\n // ids in gaps of 10, so posting one page of a multi-page set would collide with\n // (and scramble) the priorities of groups on other pages. Reordering therefore\n // also requires the whole set to be loaded, and the manage feature.\n const isUnfilteredView = !search && (!filters.isActive || filters.isActive === '')\n const allGroupsLoaded = page === 1 && !totalIsCapped && total <= rows.length\n const dragReorderEnabled = canManage && !loadError && isUnfilteredView && allGroupsLoaded\n const showReorderUnavailableHint = canManage && !loadError && !isLoading && isUnfilteredView && !allGroupsLoaded\n\n const sensors = useSensors(\n useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),\n useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),\n )\n\n const handleDragEnd = React.useCallback(\n async (event: DragEndEvent) => {\n const { active, over } = event\n if (!over || active.id === over.id) return\n const oldIndex = rows.findIndex((row) => row.id === active.id)\n const newIndex = rows.findIndex((row) => row.id === over.id)\n if (oldIndex === -1 || newIndex === -1) return\n\n const previousRows = rows\n const reordered = [...rows]\n const [moved] = reordered.splice(oldIndex, 1)\n reordered.splice(newIndex, 0, moved)\n // Optimistic reorder \u2014 visible immediately, no page reload.\n setRows(reordered)\n\n const ids = reordered.map((row) => row.id)\n try {\n await runMutation({\n operation: async () => {\n const call = await apiCall('/api/customer_groups/customer-groups/reorder', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ ids }),\n })\n if (!call.ok) {\n throw Object.assign(new Error('[internal] customer_groups.reorder failed'), { status: call.status })\n }\n return call\n },\n context: {\n formId: mutationContextId,\n resourceKind: 'customer_groups.group',\n resourceId: 'reorder',\n retryLastMutation,\n },\n mutationPayload: { ids },\n })\n flash(t('customer_groups.groups.list.flash.reordered', 'Group order updated'), 'success')\n setReloadToken((token) => token + 1)\n } catch {\n // Revert the optimistic reorder on failure.\n setRows(previousRows)\n flash(t('customer_groups.groups.list.flash.reorderError', 'Could not save the new order'), 'error')\n }\n },\n [mutationContextId, retryLastMutation, rows, runMutation, t],\n )\n\n const handleDelete = React.useCallback(\n async (row: CustomerGroupRow) => {\n const confirmed = await confirmDialog({\n title: t('customer_groups.groups.list.confirmDelete', 'Delete customer group \"{code}\"?', { code: row.code }),\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.updated_at),\n () => apiCall(`/api/customer_groups/customer-groups?id=${encodeURIComponent(row.id)}`, { method: 'DELETE' }),\n )\n if (!call.ok) {\n throw Object.assign(new Error('[internal] customer_groups.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: 'customer_groups.group',\n resourceId: row.id,\n retryLastMutation,\n },\n mutationPayload: { id: row.id },\n })\n flash(t('customer_groups.groups.list.flash.deleted', 'Customer group deleted'), 'success')\n setReloadToken((token) => token + 1)\n } catch (error) {\n if (surfaceRecordConflict(error, t, { onRefresh: () => setReloadToken((token) => token + 1) })) return\n const serverMessage = (error as { error?: unknown } | null)?.error\n flash(\n typeof serverMessage === 'string' && serverMessage.trim()\n ? serverMessage\n : t('customer_groups.groups.list.flash.deleteError', 'Could not delete the customer group'),\n 'error',\n )\n }\n },\n [t, confirmDialog, mutationContextId, retryLastMutation, runMutation],\n )\n\n const columns = React.useMemo<ColumnDef<CustomerGroupRow>[]>(\n () => [\n {\n accessorKey: 'priority',\n header: t('customer_groups.groups.list.columns.priority', 'Priority'),\n cell: ({ row }) => (\n <div className=\"flex items-center gap-2\">\n {dragReorderEnabled ? (\n <DragHandle id={row.original.id} />\n ) : (\n <GripVertical className=\"size-4 text-muted-foreground/40\" aria-hidden=\"true\" />\n )}\n <span className=\"tabular-nums text-muted-foreground\">{row.original.priority}</span>\n </div>\n ),\n },\n {\n accessorKey: 'code',\n header: t('customer_groups.groups.list.columns.code', 'Code'),\n cell: ({ row }) => <span className=\"font-mono font-medium\">{row.original.code}</span>,\n },\n {\n accessorKey: 'name',\n header: t('customer_groups.groups.list.columns.name', 'Name'),\n },\n {\n accessorKey: 'kind',\n header: t('customer_groups.groups.list.columns.kind', 'Kind'),\n cell: ({ row }) => (\n <Tag variant={kindTagMap[row.original.kind] ?? 'default'} dot>\n {t(`customer_groups.groups.kind.${row.original.kind}`, row.original.kind)}\n </Tag>\n ),\n },\n {\n accessorKey: 'is_default',\n header: t('customer_groups.groups.list.columns.isDefault', 'Default'),\n cell: ({ row }) =>\n row.original.is_default ? (\n <Tag variant=\"brand\" dot>\n {t('customer_groups.groups.list.defaultTag', 'Default')}\n </Tag>\n ) : (\n <span className=\"text-muted-foreground\">\u2014</span>\n ),\n },\n {\n accessorKey: 'is_active',\n header: t('customer_groups.groups.list.columns.isActive', 'Status'),\n cell: ({ row }) => (\n <StatusBadge variant={activeStatusMap[row.original.is_active ? 'active' : 'inactive']} dot>\n {row.original.is_active\n ? t('customer_groups.groups.list.status.active', 'Active')\n : t('customer_groups.groups.list.status.inactive', 'Inactive')}\n </StatusBadge>\n ),\n },\n {\n accessorKey: 'updated_at',\n header: t('customer_groups.groups.list.columns.updatedAt', 'Updated'),\n cell: ({ row }) => (row.original.updated_at ? new Date(row.original.updated_at).toLocaleString() : '\u2014'),\n },\n ],\n [t, dragReorderEnabled],\n )\n\n const filterDefs = React.useMemo<FilterDef[]>(\n () => [\n {\n id: 'isActive',\n label: t('customer_groups.groups.list.filters.status', 'Status'),\n type: 'select',\n options: [\n { label: t('customer_groups.groups.list.filters.all', 'All'), value: '' },\n { label: t('customer_groups.groups.list.filters.active', 'Active'), value: 'true' },\n { label: t('customer_groups.groups.list.filters.inactive', 'Inactive'), value: 'false' },\n ],\n },\n ],\n [t],\n )\n\n const table = (\n <DataTable\n title={t('customer_groups.groups.list.title', 'Customer groups')}\n titleHeadingLevel={1}\n columns={columns}\n data={rows}\n sortable={false}\n searchValue={search}\n onSearchChange={handleSearchChange}\n searchPlaceholder={t('customer_groups.groups.list.searchPlaceholder', 'Search groups...')}\n filters={filterDefs}\n filterValues={filters}\n onFiltersApply={handleFiltersApply}\n onFiltersClear={() => handleFiltersApply({})}\n actions={\n canManage ? (\n <Button asChild>\n <Link href=\"/backend/customer-groups/create\">\n <Plus className=\"mr-2 h-4 w-4\" />\n {t('customer_groups.groups.list.actions.create', 'Create group')}\n </Link>\n </Button>\n ) : undefined\n }\n rowActions={(row) => (\n <RowActions\n items={canManage ? [\n { id: 'edit', label: t('common.edit', 'Edit'), href: `/backend/customer-groups/${row.id}/edit` },\n { id: 'delete', label: t('common.delete', 'Delete'), destructive: true, onSelect: () => handleDelete(row) },\n ] : [\n { id: 'open', label: t('common.open', 'Open'), href: `/backend/customer-groups/${row.id}/edit` },\n ]}\n />\n )}\n emptyState={\n <ListEmptyState\n entityName={t('customer_groups.groups.list.title', 'Customer groups')}\n title={t('customer_groups.groups.list.empty.title', 'No customer groups yet')}\n description={t(\n 'customer_groups.groups.list.empty.description',\n 'Create your first group to start organizing customers.',\n )}\n createHref={canManage ? '/backend/customer-groups/create' : undefined}\n createLabel={t('customer_groups.groups.list.empty.createLabel', 'Create your first group')}\n />\n }\n error={loadError ? <ErrorMessage label={loadError} /> : null}\n pagination={{\n page,\n pageSize: PAGE_SIZE,\n total,\n totalPages,\n totalIsCapped,\n onPageChange: setPage,\n }}\n isLoading={isLoading}\n />\n )\n\n return (\n <Page>\n <PageBody>\n {canManage ? <OrphanBanner /> : null}\n {showReorderUnavailableHint ? (\n <Alert status=\"information\" size=\"sm\" className=\"mb-4\">\n {t(\n 'customer_groups.groups.list.reorderUnavailable',\n 'Drag-to-reorder is off because there are more groups than fit on one page. Edit a group to change its priority.',\n )}\n </Alert>\n ) : null}\n {dragReorderEnabled ? (\n <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>\n <SortableContext items={rows.map((row) => row.id)} strategy={verticalListSortingStrategy}>\n {table}\n </SortableContext>\n </DndContext>\n ) : (\n table\n )}\n </PageBody>\n {ConfirmDialogElement}\n </Page>\n )\n}\n"],
|
|
5
|
+
"mappings": ";AAgHM,cAmNI,YAnNJ;AA9GN,YAAY,WAAW;AACvB,OAAO,UAAU;AACjB,SAAS,MAAM,gBAAgB;AAC/B,SAAS,iBAAiB;AAC1B,SAAS,sBAAsB;AAE/B,SAAS,kBAAkB;AAC3B,SAAS,mBAAmC;AAC5C,SAAS,WAAwB;AACjC,SAAS,cAAc;AACvB,SAAS,MAAM,oBAAoB;AACnC,SAAS,YAAY;AACrB,SAAS,SAAS,mCAAmC;AACrD,SAAS,iCAAiC;AAC1C,SAAS,6BAA6B;AACtC,SAAS,0BAA0B;AACnC,SAAS,aAAa;AACtB,SAAS,wBAAwB;AACjC,SAAS,wBAAwB;AACjC,SAAS,oBAAoB;AAC7B,SAAS,aAAa;AACtB,SAAS,kBAAkB;AAE3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,WAAW;AACpB,SAAS,oBAAoB;AA0B7B,MAAM,YAAY;AAClB,MAAM,iBAAiB;AAEvB,MAAM,aAA6B;AAAA,EACjC,KAAK;AAAA,EACL,KAAK;AAAA,EACL,UAAU;AAAA,EACV,SAAS;AACX;AAEA,MAAM,kBAAoD;AAAA,EACxD,QAAQ;AAAA,EACR,UAAU;AACZ;AAeA,SAAS,WAAW,EAAE,GAAG,GAAmB;AAC1C,QAAM,IAAI,KAAK;AACf,QAAM,EAAE,YAAY,WAAW,YAAY,WAAW,YAAY,WAAW,IAAI,YAAY,EAAE,GAAG,CAAC;AACnG,QAAM,QAA6B;AAAA,IACjC,WAAW,IAAI,UAAU,SAAS,SAAS;AAAA,IAC3C;AAAA,IACA,SAAS,aAAa,MAAM;AAAA,EAC9B;AACA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,KAAK;AAAA,MACL;AAAA,MACA,WAAU;AAAA,MACV,cAAY,EAAE,0CAA0C,SAAS;AAAA,MAChE,GAAG;AAAA,MACH,GAAG;AAAA,MAEJ,8BAAC,gBAAa,WAAU,UAAS;AAAA;AAAA,EACnC;AAEJ;AAEe,SAAR,qBAAsC;AAC3C,QAAM,IAAI,KAAK;AACf,QAAM,EAAE,SAAS,eAAe,qBAAqB,IAAI,iBAAiB;AAC1E,QAAM,EAAE,SAAS,sBAAsB,SAAS,mBAAmB,IAAI,iBAAiB;AACxF,QAAM,YAAY,sBAAsB,WAAW,sBAAsB,iBAAiB,cAAc;AACxG,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAA6B,CAAC,CAAC;AAC7D,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAAS,CAAC;AACxC,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAS,CAAC;AAC1C,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAAS,CAAC;AACpD,QAAM,CAAC,eAAe,gBAAgB,IAAI,MAAM,SAAS,KAAK;AAC9D,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAAS,EAAE;AAC7C,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAuB,CAAC,CAAC;AAC7D,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAS,IAAI;AACrD,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAwB,IAAI;AACpE,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAAS,CAAC;AACtD,QAAM,oBAAoB;AAC1B,QAAM,EAAE,aAAa,kBAAkB,IAAI,mBAKxC;AAAA,IACD,WAAW;AAAA,IACX,gBAAgB,EAAE,8BAA8B,4BAA4B;AAAA,EAC9E,CAAC;AAED,QAAM,UAAU,MAAM;AACpB,QAAI,YAAY;AAChB,mBAAe,OAAO;AACpB,mBAAa,IAAI;AACjB,mBAAa,IAAI;AACjB,UAAI;AACF,cAAM,SAAS,IAAI,gBAAgB;AACnC,eAAO,IAAI,QAAQ,OAAO,IAAI,CAAC;AAC/B,eAAO,IAAI,YAAY,OAAO,SAAS,CAAC;AAGxC,eAAO,IAAI,aAAa,UAAU;AAClC,eAAO,IAAI,WAAW,KAAK;AAC3B,YAAI,OAAQ,QAAO,IAAI,UAAU,MAAM;AACvC,YAAI,QAAQ,aAAa,OAAQ,QAAO,IAAI,YAAY,MAAM;AAC9D,YAAI,QAAQ,aAAa,QAAS,QAAO,IAAI,YAAY,OAAO;AAEhE,cAAM,WAA4B,EAAE,OAAO,CAAC,GAAG,OAAO,GAAG,MAAM,GAAG,YAAY,EAAE;AAChF,cAAM,OAAO,MAAM;AAAA,UACjB,wCAAwC,OAAO,SAAS,CAAC;AAAA,UACzD;AAAA,UACA,EAAE,SAAS;AAAA,QACb;AACA,YAAI,UAAW;AACf,YAAI,CAAC,KAAK,IAAI;AACZ,kBAAQ,CAAC,CAAC;AACV,uBAAa,EAAE,0CAA0C,gCAAgC,CAAC;AAC1F;AAAA,QACF;AACA,cAAM,UAAU,KAAK,UAAU;AAC/B,cAAM,QAAQ,MAAM,QAAQ,QAAQ,KAAK,IAAI,QAAQ,QAAQ,CAAC;AAC9D,gBAAQ,KAAK;AACb,iBAAS,OAAO,QAAQ,UAAU,WAAW,QAAQ,QAAQ,MAAM,MAAM;AACzE,sBAAc,OAAO,QAAQ,eAAe,YAAY,QAAQ,aAAa,IAAI,QAAQ,aAAa,CAAC;AACvG,yBAAiB,QAAQ,kBAAkB,IAAI;AAAA,MACjD,QAAQ;AACN,YAAI,CAAC,WAAW;AACd,kBAAQ,CAAC,CAAC;AACV,uBAAa,EAAE,0CAA0C,gCAAgC,CAAC;AAAA,QAC5F;AAAA,MACF,UAAE;AACA,YAAI,CAAC,UAAW,cAAa,KAAK;AAAA,MACpC;AAAA,IACF;AACA,SAAK;AACL,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,MAAM,QAAQ,SAAS,aAAa,CAAC,CAAC;AAE1C,QAAM,qBAAqB,MAAM,YAAY,CAAC,SAAiB;AAC7D,cAAU,IAAI;AACd,YAAQ,CAAC;AAAA,EACX,GAAG,CAAC,CAAC;AAEL,QAAM,qBAAqB,MAAM,YAAY,CAAC,SAAuB;AACnE,eAAW,IAAI;AACf,YAAQ,CAAC;AAAA,EACX,GAAG,CAAC,CAAC;AAWL,QAAM,mBAAmB,CAAC,WAAW,CAAC,QAAQ,YAAY,QAAQ,aAAa;AAC/E,QAAM,kBAAkB,SAAS,KAAK,CAAC,iBAAiB,SAAS,KAAK;AACtE,QAAM,qBAAqB,aAAa,CAAC,aAAa,oBAAoB;AAC1E,QAAM,6BAA6B,aAAa,CAAC,aAAa,CAAC,aAAa,oBAAoB,CAAC;AAEjG,QAAM,UAAU;AAAA,IACd,UAAU,eAAe,EAAE,sBAAsB,EAAE,UAAU,EAAE,EAAE,CAAC;AAAA,IAClE,UAAU,gBAAgB,EAAE,kBAAkB,4BAA4B,CAAC;AAAA,EAC7E;AAEA,QAAM,gBAAgB,MAAM;AAAA,IAC1B,OAAO,UAAwB;AAC7B,YAAM,EAAE,QAAQ,KAAK,IAAI;AACzB,UAAI,CAAC,QAAQ,OAAO,OAAO,KAAK,GAAI;AACpC,YAAM,WAAW,KAAK,UAAU,CAAC,QAAQ,IAAI,OAAO,OAAO,EAAE;AAC7D,YAAM,WAAW,KAAK,UAAU,CAAC,QAAQ,IAAI,OAAO,KAAK,EAAE;AAC3D,UAAI,aAAa,MAAM,aAAa,GAAI;AAExC,YAAM,eAAe;AACrB,YAAM,YAAY,CAAC,GAAG,IAAI;AAC1B,YAAM,CAAC,KAAK,IAAI,UAAU,OAAO,UAAU,CAAC;AAC5C,gBAAU,OAAO,UAAU,GAAG,KAAK;AAEnC,cAAQ,SAAS;AAEjB,YAAM,MAAM,UAAU,IAAI,CAAC,QAAQ,IAAI,EAAE;AACzC,UAAI;AACF,cAAM,YAAY;AAAA,UAChB,WAAW,YAAY;AACrB,kBAAM,OAAO,MAAM,QAAQ,gDAAgD;AAAA,cACzE,QAAQ;AAAA,cACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,cAC9C,MAAM,KAAK,UAAU,EAAE,IAAI,CAAC;AAAA,YAC9B,CAAC;AACD,gBAAI,CAAC,KAAK,IAAI;AACZ,oBAAM,OAAO,OAAO,IAAI,MAAM,2CAA2C,GAAG,EAAE,QAAQ,KAAK,OAAO,CAAC;AAAA,YACrG;AACA,mBAAO;AAAA,UACT;AAAA,UACA,SAAS;AAAA,YACP,QAAQ;AAAA,YACR,cAAc;AAAA,YACd,YAAY;AAAA,YACZ;AAAA,UACF;AAAA,UACA,iBAAiB,EAAE,IAAI;AAAA,QACzB,CAAC;AACD,cAAM,EAAE,+CAA+C,qBAAqB,GAAG,SAAS;AACxF,uBAAe,CAAC,UAAU,QAAQ,CAAC;AAAA,MACrC,QAAQ;AAEN,gBAAQ,YAAY;AACpB,cAAM,EAAE,kDAAkD,8BAA8B,GAAG,OAAO;AAAA,MACpG;AAAA,IACF;AAAA,IACA,CAAC,mBAAmB,mBAAmB,MAAM,aAAa,CAAC;AAAA,EAC7D;AAEA,QAAM,eAAe,MAAM;AAAA,IACzB,OAAO,QAA0B;AAC/B,YAAM,YAAY,MAAM,cAAc;AAAA,QACpC,OAAO,EAAE,6CAA6C,mCAAmC,EAAE,MAAM,IAAI,KAAK,CAAC;AAAA,QAC3G,SAAS;AAAA,MACX,CAAC;AACD,UAAI,CAAC,UAAW;AAEhB,UAAI;AACF,cAAM,YAAY;AAAA,UAChB,WAAW,YAAY;AACrB,kBAAM,OAAO,MAAM;AAAA,cACjB,0BAA0B,IAAI,UAAU;AAAA,cACxC,MAAM,QAAQ,2CAA2C,mBAAmB,IAAI,EAAE,CAAC,IAAI,EAAE,QAAQ,SAAS,CAAC;AAAA,YAC7G;AACA,gBAAI,CAAC,KAAK,IAAI;AACZ,oBAAM,OAAO,OAAO,IAAI,MAAM,0CAA0C,GAAG;AAAA,gBACzE,QAAQ,KAAK;AAAA,gBACb,GAAK,KAAK,UAA6C,CAAC;AAAA,cAC1D,CAAC;AAAA,YACH;AACA,mBAAO;AAAA,UACT;AAAA,UACA,SAAS;AAAA,YACP,QAAQ;AAAA,YACR,cAAc;AAAA,YACd,YAAY,IAAI;AAAA,YAChB;AAAA,UACF;AAAA,UACA,iBAAiB,EAAE,IAAI,IAAI,GAAG;AAAA,QAChC,CAAC;AACD,cAAM,EAAE,6CAA6C,wBAAwB,GAAG,SAAS;AACzF,uBAAe,CAAC,UAAU,QAAQ,CAAC;AAAA,MACrC,SAAS,OAAO;AACd,YAAI,sBAAsB,OAAO,GAAG,EAAE,WAAW,MAAM,eAAe,CAAC,UAAU,QAAQ,CAAC,EAAE,CAAC,EAAG;AAChG,cAAM,gBAAiB,OAAsC;AAC7D;AAAA,UACE,OAAO,kBAAkB,YAAY,cAAc,KAAK,IACpD,gBACA,EAAE,iDAAiD,qCAAqC;AAAA,UAC5F;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,IACA,CAAC,GAAG,eAAe,mBAAmB,mBAAmB,WAAW;AAAA,EACtE;AAEA,QAAM,UAAU,MAAM;AAAA,IACpB,MAAM;AAAA,MACJ;AAAA,QACE,aAAa;AAAA,QACb,QAAQ,EAAE,gDAAgD,UAAU;AAAA,QACpE,MAAM,CAAC,EAAE,IAAI,MACX,qBAAC,SAAI,WAAU,2BACZ;AAAA,+BACC,oBAAC,cAAW,IAAI,IAAI,SAAS,IAAI,IAEjC,oBAAC,gBAAa,WAAU,mCAAkC,eAAY,QAAO;AAAA,UAE/E,oBAAC,UAAK,WAAU,sCAAsC,cAAI,SAAS,UAAS;AAAA,WAC9E;AAAA,MAEJ;AAAA,MACA;AAAA,QACE,aAAa;AAAA,QACb,QAAQ,EAAE,4CAA4C,MAAM;AAAA,QAC5D,MAAM,CAAC,EAAE,IAAI,MAAM,oBAAC,UAAK,WAAU,yBAAyB,cAAI,SAAS,MAAK;AAAA,MAChF;AAAA,MACA;AAAA,QACE,aAAa;AAAA,QACb,QAAQ,EAAE,4CAA4C,MAAM;AAAA,MAC9D;AAAA,MACA;AAAA,QACE,aAAa;AAAA,QACb,QAAQ,EAAE,4CAA4C,MAAM;AAAA,QAC5D,MAAM,CAAC,EAAE,IAAI,MACX,oBAAC,OAAI,SAAS,WAAW,IAAI,SAAS,IAAI,KAAK,WAAW,KAAG,MAC1D,YAAE,+BAA+B,IAAI,SAAS,IAAI,IAAI,IAAI,SAAS,IAAI,GAC1E;AAAA,MAEJ;AAAA,MACA;AAAA,QACE,aAAa;AAAA,QACb,QAAQ,EAAE,iDAAiD,SAAS;AAAA,QACpE,MAAM,CAAC,EAAE,IAAI,MACX,IAAI,SAAS,aACX,oBAAC,OAAI,SAAQ,SAAQ,KAAG,MACrB,YAAE,0CAA0C,SAAS,GACxD,IAEA,oBAAC,UAAK,WAAU,yBAAwB,oBAAC;AAAA,MAE/C;AAAA,MACA;AAAA,QACE,aAAa;AAAA,QACb,QAAQ,EAAE,gDAAgD,QAAQ;AAAA,QAClE,MAAM,CAAC,EAAE,IAAI,MACX,oBAAC,eAAY,SAAS,gBAAgB,IAAI,SAAS,YAAY,WAAW,UAAU,GAAG,KAAG,MACvF,cAAI,SAAS,YACV,EAAE,6CAA6C,QAAQ,IACvD,EAAE,+CAA+C,UAAU,GACjE;AAAA,MAEJ;AAAA,MACA;AAAA,QACE,aAAa;AAAA,QACb,QAAQ,EAAE,iDAAiD,SAAS;AAAA,QACpE,MAAM,CAAC,EAAE,IAAI,MAAO,IAAI,SAAS,aAAa,IAAI,KAAK,IAAI,SAAS,UAAU,EAAE,eAAe,IAAI;AAAA,MACrG;AAAA,IACF;AAAA,IACA,CAAC,GAAG,kBAAkB;AAAA,EACxB;AAEA,QAAM,aAAa,MAAM;AAAA,IACvB,MAAM;AAAA,MACJ;AAAA,QACE,IAAI;AAAA,QACJ,OAAO,EAAE,8CAA8C,QAAQ;AAAA,QAC/D,MAAM;AAAA,QACN,SAAS;AAAA,UACP,EAAE,OAAO,EAAE,2CAA2C,KAAK,GAAG,OAAO,GAAG;AAAA,UACxE,EAAE,OAAO,EAAE,8CAA8C,QAAQ,GAAG,OAAO,OAAO;AAAA,UAClF,EAAE,OAAO,EAAE,gDAAgD,UAAU,GAAG,OAAO,QAAQ;AAAA,QACzF;AAAA,MACF;AAAA,IACF;AAAA,IACA,CAAC,CAAC;AAAA,EACJ;AAEA,QAAM,QACJ;AAAA,IAAC;AAAA;AAAA,MACC,OAAO,EAAE,qCAAqC,iBAAiB;AAAA,MAC/D,mBAAmB;AAAA,MACnB;AAAA,MACA,MAAM;AAAA,MACN,UAAU;AAAA,MACV,aAAa;AAAA,MACb,gBAAgB;AAAA,MAChB,mBAAmB,EAAE,iDAAiD,kBAAkB;AAAA,MACxF,SAAS;AAAA,MACT,cAAc;AAAA,MACd,gBAAgB;AAAA,MAChB,gBAAgB,MAAM,mBAAmB,CAAC,CAAC;AAAA,MAC3C,SACE,YACE,oBAAC,UAAO,SAAO,MACb,+BAAC,QAAK,MAAK,mCACT;AAAA,4BAAC,QAAK,WAAU,gBAAe;AAAA,QAC9B,EAAE,8CAA8C,cAAc;AAAA,SACjE,GACF,IACE;AAAA,MAEN,YAAY,CAAC,QACX;AAAA,QAAC;AAAA;AAAA,UACC,OAAO,YAAY;AAAA,YACjB,EAAE,IAAI,QAAQ,OAAO,EAAE,eAAe,MAAM,GAAG,MAAM,4BAA4B,IAAI,EAAE,QAAQ;AAAA,YAC/F,EAAE,IAAI,UAAU,OAAO,EAAE,iBAAiB,QAAQ,GAAG,aAAa,MAAM,UAAU,MAAM,aAAa,GAAG,EAAE;AAAA,UAC5G,IAAI;AAAA,YACF,EAAE,IAAI,QAAQ,OAAO,EAAE,eAAe,MAAM,GAAG,MAAM,4BAA4B,IAAI,EAAE,QAAQ;AAAA,UACjG;AAAA;AAAA,MACF;AAAA,MAEF,YACE;AAAA,QAAC;AAAA;AAAA,UACC,YAAY,EAAE,qCAAqC,iBAAiB;AAAA,UACpE,OAAO,EAAE,2CAA2C,wBAAwB;AAAA,UAC5E,aAAa;AAAA,YACX;AAAA,YACA;AAAA,UACF;AAAA,UACA,YAAY,YAAY,oCAAoC;AAAA,UAC5D,aAAa,EAAE,iDAAiD,yBAAyB;AAAA;AAAA,MAC3F;AAAA,MAEF,OAAO,YAAY,oBAAC,gBAAa,OAAO,WAAW,IAAK;AAAA,MACxD,YAAY;AAAA,QACV;AAAA,QACA,UAAU;AAAA,QACV;AAAA,QACA;AAAA,QACA;AAAA,QACA,cAAc;AAAA,MAChB;AAAA,MACA;AAAA;AAAA,EACF;AAGF,SACE,qBAAC,QACC;AAAA,yBAAC,YACE;AAAA,kBAAY,oBAAC,gBAAa,IAAK;AAAA,MAC/B,6BACC,oBAAC,SAAM,QAAO,eAAc,MAAK,MAAK,WAAU,QAC7C;AAAA,QACC;AAAA,QACA;AAAA,MACF,GACF,IACE;AAAA,MACH,qBACC,oBAAC,cAAW,SAAkB,oBAAoB,eAAe,WAAW,eAC1E,8BAAC,mBAAgB,OAAO,KAAK,IAAI,CAAC,QAAQ,IAAI,EAAE,GAAG,UAAU,6BAC1D,iBACH,GACF,IAEA;AAAA,OAEJ;AAAA,IACC;AAAA,KACH;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -24,7 +24,7 @@ async function loadPriceKindOptions(query) {
|
|
|
24
24
|
try {
|
|
25
25
|
const params = new URLSearchParams({ search: (query ?? "").trim(), pageSize: "20" });
|
|
26
26
|
const payload = await readApiResultOrThrow(
|
|
27
|
-
`/api/
|
|
27
|
+
`/api/customer_groups/customer-groups/price-kinds?${params.toString()}`,
|
|
28
28
|
void 0,
|
|
29
29
|
{ fallback: { items: [] } }
|
|
30
30
|
);
|
|
@@ -39,7 +39,7 @@ async function resolvePriceKindLabel(id) {
|
|
|
39
39
|
try {
|
|
40
40
|
const params = new URLSearchParams({ ids: id, pageSize: "1" });
|
|
41
41
|
const payload = await readApiResultOrThrow(
|
|
42
|
-
`/api/
|
|
42
|
+
`/api/customer_groups/customer-groups/price-kinds?${params.toString()}`,
|
|
43
43
|
void 0,
|
|
44
44
|
{ fallback: { items: [] } }
|
|
45
45
|
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/customer_groups/components/CustomerGroupTermsPriceKindField.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { ComboboxInput, type ComboboxOption } from '@open-mercato/ui/backend/inputs/ComboboxInput'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customer_groups').child({ component: 'CustomerGroupTermsPriceKindField' })\n\ntype RemotePriceKind = Record<string, unknown>\n\n// Mirrors `pickString`/`loadOptions`/`resolveOne` in\n// `catalog/components/prices/PriceScopeSelectors.tsx` (`PricePriceKindSelect`) \u2014 the\n// terms `priceKindId` field is an FK-id into `catalog`'s price kinds (spec: \"a picker\n// sourced from catalog price kinds, not free text\"), sourced via
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { ComboboxInput, type ComboboxOption } from '@open-mercato/ui/backend/inputs/ComboboxInput'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customer_groups').child({ component: 'CustomerGroupTermsPriceKindField' })\n\ntype RemotePriceKind = Record<string, unknown>\n\n// Mirrors `pickString`/`loadOptions`/`resolveOne` in\n// `catalog/components/prices/PriceScopeSelectors.tsx` (`PricePriceKindSelect`) \u2014 the\n// terms `priceKindId` field is an FK-id into `catalog`'s price kinds (spec: \"a picker\n// sourced from catalog price kinds, not free text\"), sourced via `customer_groups`'\n// own `/api/customer_groups/customer-groups/price-kinds` route (gated by `customer_groups.terms.view`,\n// not `catalog.settings.manage`) rather than an ORM relationship.\nfunction pickString(item: RemotePriceKind, ...keys: string[]): string {\n for (const key of keys) {\n const value = item[key]\n if (typeof value === 'string' && value.length) return value\n }\n return ''\n}\n\nfunction mapPriceKindItem(item: RemotePriceKind): ComboboxOption | null {\n const id = pickString(item, 'id')\n if (!id) return null\n const title = pickString(item, 'title') || id\n const code = pickString(item, 'code')\n return { value: id, label: code ? `${title} (${code})` : title }\n}\n\nasync function loadPriceKindOptions(query?: string): Promise<ComboboxOption[]> {\n try {\n const params = new URLSearchParams({ search: (query ?? '').trim(), pageSize: '20' })\n const payload = await readApiResultOrThrow<{ items?: RemotePriceKind[] }>(\n `/api/customer_groups/customer-groups/price-kinds?${params.toString()}`,\n undefined,\n { fallback: { items: [] } },\n )\n const items = Array.isArray(payload.items) ? payload.items : []\n return items.map(mapPriceKindItem).filter((option): option is ComboboxOption => option !== null)\n } catch (err) {\n logger.error('customer_groups.terms.priceKind.load', { err })\n return []\n }\n}\n\nasync function resolvePriceKindLabel(id: string): Promise<string> {\n try {\n const params = new URLSearchParams({ ids: id, pageSize: '1' })\n const payload = await readApiResultOrThrow<{ items?: RemotePriceKind[] }>(\n `/api/customer_groups/customer-groups/price-kinds?${params.toString()}`,\n undefined,\n { fallback: { items: [] } },\n )\n const items = Array.isArray(payload.items) ? payload.items : []\n const option = items.map(mapPriceKindItem).find((candidate): candidate is ComboboxOption => candidate !== null)\n return option?.label ?? id\n } catch {\n return id\n }\n}\n\nexport function CustomerGroupTermsPriceKindField({\n value,\n onChange,\n disabled,\n}: {\n value: string\n onChange: (next: string) => void\n disabled?: boolean\n}) {\n const t = useT()\n const currentId = typeof value === 'string' ? value.trim() : ''\n // Mirrors `GroupPickerField`'s resolve-in-flight guard: disable the combobox while a\n // pre-selected id's label is still resolving so the field never briefly shows the raw\n // uuid as interactive text.\n const [isLoading, setIsLoading] = React.useState(false)\n\n React.useEffect(() => {\n let cancelled = false\n if (!currentId) {\n setIsLoading(false)\n return\n }\n setIsLoading(true)\n resolvePriceKindLabel(currentId).then(() => {\n if (!cancelled) setIsLoading(false)\n })\n return () => {\n cancelled = true\n }\n }, [currentId])\n\n return (\n <ComboboxInput\n value={currentId}\n onChange={onChange}\n disabled={disabled || isLoading}\n allowCustomValues={false}\n clearable\n placeholder={t('customer_groups.groups.form.terms.field.priceKindPlaceholder', 'Search price kinds\u2026')}\n loadSuggestions={loadPriceKindOptions}\n resolveLabel={resolvePriceKindLabel}\n />\n )\n}\n\nexport default CustomerGroupTermsPriceKindField\n"],
|
|
5
|
+
"mappings": ";AAkGI;AAhGJ,YAAY,WAAW;AACvB,SAAS,qBAA0C;AACnD,SAAS,4BAA4B;AACrC,SAAS,YAAY;AACrB,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,iBAAiB,EAAE,MAAM,EAAE,WAAW,mCAAmC,CAAC;AAUtG,SAAS,WAAW,SAA0B,MAAwB;AACpE,aAAW,OAAO,MAAM;AACtB,UAAM,QAAQ,KAAK,GAAG;AACtB,QAAI,OAAO,UAAU,YAAY,MAAM,OAAQ,QAAO;AAAA,EACxD;AACA,SAAO;AACT;AAEA,SAAS,iBAAiB,MAA8C;AACtE,QAAM,KAAK,WAAW,MAAM,IAAI;AAChC,MAAI,CAAC,GAAI,QAAO;AAChB,QAAM,QAAQ,WAAW,MAAM,OAAO,KAAK;AAC3C,QAAM,OAAO,WAAW,MAAM,MAAM;AACpC,SAAO,EAAE,OAAO,IAAI,OAAO,OAAO,GAAG,KAAK,KAAK,IAAI,MAAM,MAAM;AACjE;AAEA,eAAe,qBAAqB,OAA2C;AAC7E,MAAI;AACF,UAAM,SAAS,IAAI,gBAAgB,EAAE,SAAS,SAAS,IAAI,KAAK,GAAG,UAAU,KAAK,CAAC;AACnF,UAAM,UAAU,MAAM;AAAA,MACpB,oDAAoD,OAAO,SAAS,CAAC;AAAA,MACrE;AAAA,MACA,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE,EAAE;AAAA,IAC5B;AACA,UAAM,QAAQ,MAAM,QAAQ,QAAQ,KAAK,IAAI,QAAQ,QAAQ,CAAC;AAC9D,WAAO,MAAM,IAAI,gBAAgB,EAAE,OAAO,CAAC,WAAqC,WAAW,IAAI;AAAA,EACjG,SAAS,KAAK;AACZ,WAAO,MAAM,wCAAwC,EAAE,IAAI,CAAC;AAC5D,WAAO,CAAC;AAAA,EACV;AACF;AAEA,eAAe,sBAAsB,IAA6B;AAChE,MAAI;AACF,UAAM,SAAS,IAAI,gBAAgB,EAAE,KAAK,IAAI,UAAU,IAAI,CAAC;AAC7D,UAAM,UAAU,MAAM;AAAA,MACpB,oDAAoD,OAAO,SAAS,CAAC;AAAA,MACrE;AAAA,MACA,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE,EAAE;AAAA,IAC5B;AACA,UAAM,QAAQ,MAAM,QAAQ,QAAQ,KAAK,IAAI,QAAQ,QAAQ,CAAC;AAC9D,UAAM,SAAS,MAAM,IAAI,gBAAgB,EAAE,KAAK,CAAC,cAA2C,cAAc,IAAI;AAC9G,WAAO,QAAQ,SAAS;AAAA,EAC1B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,iCAAiC;AAAA,EAC/C;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,QAAM,IAAI,KAAK;AACf,QAAM,YAAY,OAAO,UAAU,WAAW,MAAM,KAAK,IAAI;AAI7D,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAS,KAAK;AAEtD,QAAM,UAAU,MAAM;AACpB,QAAI,YAAY;AAChB,QAAI,CAAC,WAAW;AACd,mBAAa,KAAK;AAClB;AAAA,IACF;AACA,iBAAa,IAAI;AACjB,0BAAsB,SAAS,EAAE,KAAK,MAAM;AAC1C,UAAI,CAAC,UAAW,cAAa,KAAK;AAAA,IACpC,CAAC;AACD,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,SAAS,CAAC;AAEd,SACE;AAAA,IAAC;AAAA;AAAA,MACC,OAAO;AAAA,MACP;AAAA,MACA,UAAU,YAAY;AAAA,MACtB,mBAAmB;AAAA,MACnB,WAAS;AAAA,MACT,aAAa,EAAE,gEAAgE,0BAAqB;AAAA,MACpG,iBAAiB;AAAA,MACjB,cAAc;AAAA;AAAA,EAChB;AAEJ;AAEA,IAAO,2CAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -12,6 +12,7 @@ import { EmptyState } from "@open-mercato/ui/primitives/empty-state";
|
|
|
12
12
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
13
13
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
14
14
|
import { CustomerGroupTermsPriceKindField } from "./CustomerGroupTermsPriceKindField.js";
|
|
15
|
+
const TERMS_NOT_YET_CREATED_LOCK_TOKEN = (/* @__PURE__ */ new Date(0)).toISOString();
|
|
15
16
|
const PURCHASE_ON_ACCOUNT_VALUES = ["inherit", "allow", "deny"];
|
|
16
17
|
function toPurchaseOnAccountValue(value) {
|
|
17
18
|
if (value === true) return "allow";
|
|
@@ -176,7 +177,7 @@ function CustomerGroupTermsSection({
|
|
|
176
177
|
schema,
|
|
177
178
|
fields,
|
|
178
179
|
initialValues,
|
|
179
|
-
optimisticLockUpdatedAt: terms?.updatedAt ??
|
|
180
|
+
optimisticLockUpdatedAt: terms?.updatedAt ?? TERMS_NOT_YET_CREATED_LOCK_TOKEN,
|
|
180
181
|
readOnly: !canManage,
|
|
181
182
|
readOnlyOverlay: /* @__PURE__ */ jsx("div", { className: "rounded-xl border border-border/70 bg-background/95 px-4 py-3 text-sm text-muted-foreground shadow-sm", children: t(
|
|
182
183
|
"customer_groups.groups.form.terms.readOnly",
|
|
@@ -192,6 +193,7 @@ function CustomerGroupTermsSection({
|
|
|
192
193
|
var CustomerGroupTermsSection_default = CustomerGroupTermsSection;
|
|
193
194
|
export {
|
|
194
195
|
CustomerGroupTermsSection,
|
|
196
|
+
TERMS_NOT_YET_CREATED_LOCK_TOKEN,
|
|
195
197
|
CustomerGroupTermsSection_default as default
|
|
196
198
|
};
|
|
197
199
|
//# sourceMappingURL=CustomerGroupTermsSection.js.map
|