@open-mercato/core 0.6.6-develop.6317.1.b3be10ab84 → 0.6.6-develop.6330.1.a261878aa8
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/customer_accounts/backend/customer_accounts/roles/[id]/page.js +7 -12
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js +5 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +2 -1
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection/company-users/widget.client.js +2 -1
- package/dist/modules/customer_accounts/widgets/injection/company-users/widget.client.js.map +2 -2
- package/dist/modules/customers/components/detail/EntityTagsDialog.js +76 -67
- package/dist/modules/customers/components/detail/EntityTagsDialog.js.map +3 -3
- package/dist/modules/dashboards/components/WidgetVisibilityEditor.js +4 -3
- package/dist/modules/dashboards/components/WidgetVisibilityEditor.js.map +2 -2
- package/dist/modules/data_sync/api/runs.js +1 -0
- package/dist/modules/data_sync/api/runs.js.map +2 -2
- package/dist/modules/data_sync/backend/data-sync/page.js +3 -1
- package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
- package/dist/modules/data_sync/data/validators.js +1 -0
- package/dist/modules/data_sync/data/validators.js.map +2 -2
- package/dist/modules/data_sync/lib/sync-run-service.js +20 -0
- package/dist/modules/data_sync/lib/sync-run-service.js.map +2 -2
- package/dist/modules/entities/api/records.js +11 -1
- package/dist/modules/entities/api/records.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js +17 -10
- package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js.map +2 -2
- package/dist/modules/messages/api/unread-count/route.js +32 -0
- package/dist/modules/messages/api/unread-count/route.js.map +2 -2
- package/dist/modules/query_index/components/QueryIndexesTable.js +13 -5
- package/dist/modules/query_index/components/QueryIndexesTable.js.map +2 -2
- package/dist/modules/shipping_carriers/i18n/en.js +11 -1
- package/dist/modules/shipping_carriers/i18n/en.js.map +2 -2
- package/dist/modules/shipping_carriers/i18n/pl.js +11 -1
- package/dist/modules/shipping_carriers/i18n/pl.js.map +2 -2
- package/dist/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.client.js +25 -0
- package/dist/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.client.js.map +7 -0
- package/dist/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.js +3 -17
- package/dist/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.js.map +2 -2
- package/dist/modules/translations/widgets/injection/translation-manager/widget.client.js +19 -14
- package/dist/modules/translations/widgets/injection/translation-manager/widget.client.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.tsx +7 -10
- package/src/modules/customer_accounts/backend/customer_accounts/users/page.tsx +6 -7
- package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +3 -2
- package/src/modules/customer_accounts/widgets/injection/company-users/widget.client.tsx +3 -2
- package/src/modules/customers/components/detail/EntityTagsDialog.tsx +89 -71
- package/src/modules/dashboards/components/WidgetVisibilityEditor.tsx +6 -3
- package/src/modules/data_sync/api/runs.ts +1 -0
- package/src/modules/data_sync/backend/data-sync/page.tsx +3 -1
- package/src/modules/data_sync/data/validators.ts +1 -0
- package/src/modules/data_sync/i18n/de.json +1 -0
- package/src/modules/data_sync/i18n/en.json +1 -0
- package/src/modules/data_sync/i18n/es.json +1 -0
- package/src/modules/data_sync/i18n/pl.json +1 -0
- package/src/modules/data_sync/lib/sync-run-service.ts +23 -0
- package/src/modules/entities/api/records.ts +17 -1
- package/src/modules/entities/backend/entities/user/[entityId]/records/page.tsx +25 -12
- package/src/modules/messages/api/unread-count/route.ts +40 -0
- package/src/modules/query_index/components/QueryIndexesTable.tsx +17 -15
- package/src/modules/shipping_carriers/i18n/de.json +11 -1
- package/src/modules/shipping_carriers/i18n/en.json +11 -1
- package/src/modules/shipping_carriers/i18n/en.ts +10 -0
- package/src/modules/shipping_carriers/i18n/es.json +11 -1
- package/src/modules/shipping_carriers/i18n/pl.json +11 -1
- package/src/modules/shipping_carriers/i18n/pl.ts +10 -0
- package/src/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.client.tsx +35 -0
- package/src/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.tsx +3 -22
- package/src/modules/translations/widgets/injection/translation-manager/widget.client.tsx +30 -13
package/.turbo/turbo-build.log
CHANGED
|
@@ -6,6 +6,7 @@ import { useRouter } from "next/navigation";
|
|
|
6
6
|
import { Page, PageBody } from "@open-mercato/ui/backend/Page";
|
|
7
7
|
import { CrudForm } from "@open-mercato/ui/backend/CrudForm";
|
|
8
8
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
9
|
+
import { Checkbox } from "@open-mercato/ui/primitives/checkbox";
|
|
9
10
|
import { Spinner } from "@open-mercato/ui/primitives/spinner";
|
|
10
11
|
import { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from "@open-mercato/ui/backend/utils/apiCall";
|
|
11
12
|
import { buildOptimisticLockHeader } from "@open-mercato/ui/backend/utils/optimisticLock";
|
|
@@ -82,15 +83,10 @@ function PortalPermissionsEditor({ values, setValue }) {
|
|
|
82
83
|
/* @__PURE__ */ jsx("span", { className: "text-sm font-semibold", children: t(group.labelKey, group.fallback) }),
|
|
83
84
|
/* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
|
|
84
85
|
/* @__PURE__ */ jsx(
|
|
85
|
-
|
|
86
|
+
Checkbox,
|
|
86
87
|
{
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
ref: (el) => {
|
|
90
|
-
if (el) el.indeterminate = someSelected && !allSelected;
|
|
91
|
-
},
|
|
92
|
-
onChange: () => handleGroupToggle(groupFeatures),
|
|
93
|
-
className: "rounded border-border"
|
|
88
|
+
checked: allSelected ? true : someSelected ? "indeterminate" : false,
|
|
89
|
+
onCheckedChange: () => handleGroupToggle(groupFeatures)
|
|
94
90
|
}
|
|
95
91
|
),
|
|
96
92
|
t("customer_accounts.admin.roleDetail.selectAll", "Select all")
|
|
@@ -100,12 +96,11 @@ function PortalPermissionsEditor({ values, setValue }) {
|
|
|
100
96
|
const feature = PORTAL_FEATURES.find((portalFeature) => portalFeature.id === featureId);
|
|
101
97
|
return /* @__PURE__ */ jsxs("label", { className: "flex items-start gap-3 px-4 py-3 cursor-pointer hover:bg-muted/50 transition-colors", children: [
|
|
102
98
|
/* @__PURE__ */ jsx(
|
|
103
|
-
|
|
99
|
+
Checkbox,
|
|
104
100
|
{
|
|
105
|
-
type: "checkbox",
|
|
106
101
|
checked: features.includes(featureId),
|
|
107
|
-
|
|
108
|
-
className: "mt-0.5
|
|
102
|
+
onCheckedChange: () => handleFeatureToggle(featureId),
|
|
103
|
+
className: "mt-0.5"
|
|
109
104
|
}
|
|
110
105
|
),
|
|
111
106
|
/* @__PURE__ */ jsxs("div", { className: "space-y-0.5", children: [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/customer_accounts/backend/customer_accounts/roles/%5Bid%5D/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { CrudForm } from '@open-mercato/ui/backend/CrudForm'\nimport type { CrudField, CrudFormGroup, CrudFormGroupComponentProps } from '@open-mercato/ui/backend/CrudForm'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { apiCall, readApiResultOrThrow, 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 { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { RecordNotFoundState, ErrorMessage } from '@open-mercato/ui/backend/detail'\n\ntype RoleDetail = {\n id: string\n name: string\n slug: string\n description: string | null\n isDefault: boolean\n isSystem: boolean\n customerAssignable: boolean\n features: string[]\n updatedAt?: string | null\n updated_at?: string | null\n}\n\nconst PORTAL_FEATURES = [\n { id: 'portal.profile.view', labelKey: 'customer_accounts.admin.portalFeatures.profile.view', fallback: 'View profile', descriptionKey: 'customer_accounts.admin.portalFeatures.profile.view.description', descriptionFallback: 'Allows viewing own profile information and account details' },\n { id: 'portal.profile.edit', labelKey: 'customer_accounts.admin.portalFeatures.profile.edit', fallback: 'Edit profile', descriptionKey: 'customer_accounts.admin.portalFeatures.profile.edit.description', descriptionFallback: 'Allows editing display name and other profile settings' },\n { id: 'portal.orders.view', labelKey: 'customer_accounts.admin.portalFeatures.orders.view', fallback: 'View orders', descriptionKey: 'customer_accounts.admin.portalFeatures.orders.view.description', descriptionFallback: 'Allows viewing order history and order details' },\n { id: 'portal.orders.create', labelKey: 'customer_accounts.admin.portalFeatures.orders.create', fallback: 'Create orders', descriptionKey: 'customer_accounts.admin.portalFeatures.orders.create.description', descriptionFallback: 'Allows placing new orders through the portal' },\n { id: 'portal.invoices.view', labelKey: 'customer_accounts.admin.portalFeatures.invoices.view', fallback: 'View invoices', descriptionKey: 'customer_accounts.admin.portalFeatures.invoices.view.description', descriptionFallback: 'Allows viewing invoices and payment history' },\n { id: 'portal.quotes.view', labelKey: 'customer_accounts.admin.portalFeatures.quotes.view', fallback: 'View quotes', descriptionKey: 'customer_accounts.admin.portalFeatures.quotes.view.description', descriptionFallback: 'Allows viewing received quotes and their details' },\n { id: 'portal.quotes.request', labelKey: 'customer_accounts.admin.portalFeatures.quotes.request', fallback: 'Request quotes', descriptionKey: 'customer_accounts.admin.portalFeatures.quotes.request.description', descriptionFallback: 'Allows requesting new quotes from the company' },\n { id: 'portal.addresses.view', labelKey: 'customer_accounts.admin.portalFeatures.addresses.view', fallback: 'View addresses', descriptionKey: 'customer_accounts.admin.portalFeatures.addresses.view.description', descriptionFallback: 'Allows viewing saved shipping and billing addresses' },\n { id: 'portal.addresses.manage', labelKey: 'customer_accounts.admin.portalFeatures.addresses.manage', fallback: 'Manage addresses', descriptionKey: 'customer_accounts.admin.portalFeatures.addresses.manage.description', descriptionFallback: 'Allows adding, editing, and removing addresses' },\n { id: 'portal.users.view', labelKey: 'customer_accounts.admin.portalFeatures.users.view', fallback: 'View team members', descriptionKey: 'customer_accounts.admin.portalFeatures.users.view.description', descriptionFallback: 'Allows viewing other team members in the organization' },\n { id: 'portal.users.invite', labelKey: 'customer_accounts.admin.portalFeatures.users.invite', fallback: 'Invite team members', descriptionKey: 'customer_accounts.admin.portalFeatures.users.invite.description', descriptionFallback: 'Allows sending portal invitations to new team members' },\n { id: 'portal.users.manage', labelKey: 'customer_accounts.admin.portalFeatures.users.manage', fallback: 'Manage team members', descriptionKey: 'customer_accounts.admin.portalFeatures.users.manage.description', descriptionFallback: 'Allows editing roles and removing team members' },\n]\n\nconst FEATURE_GROUPS: Array<{ id: string; labelKey: string; fallback: string; features: string[] }> = (() => {\n const groups = new Map<string, string[]>()\n for (const feature of PORTAL_FEATURES) {\n const parts = feature.id.split('.')\n const groupKey = parts.length >= 2 ? `${parts[0]}.${parts[1]}` : parts[0]\n const existing = groups.get(groupKey)\n if (existing) {\n existing.push(feature.id)\n } else {\n groups.set(groupKey, [feature.id])\n }\n }\n return Array.from(groups.entries()).map(([groupId, features]) => {\n const scope = groupId.split('.').slice(1).join('')\n const fallback = scope.replace(/^\\w/, (ch) => ch.toUpperCase())\n return {\n id: groupId,\n labelKey: `customer_accounts.admin.portalFeatures.groups.${scope}`,\n fallback,\n features,\n }\n })\n})()\n\nfunction PortalPermissionsEditor({ values, setValue }: CrudFormGroupComponentProps) {\n const t = useT()\n const features = React.useMemo(\n () => Array.isArray(values.features) ? values.features as string[] : [],\n [values.features],\n )\n\n const handleFeatureToggle = React.useCallback((featureId: string) => {\n const next = features.includes(featureId)\n ? features.filter((existingId) => existingId !== featureId)\n : [...features, featureId]\n setValue('features', next)\n }, [features, setValue])\n\n const handleGroupToggle = React.useCallback((featureIds: string[]) => {\n const allSelected = featureIds.every((featureId) => features.includes(featureId))\n let next: string[]\n if (allSelected) {\n next = features.filter((featureId) => !featureIds.includes(featureId))\n } else {\n next = [...features]\n for (const featureId of featureIds) {\n if (!next.includes(featureId)) next.push(featureId)\n }\n }\n setValue('features', next)\n }, [features, setValue])\n\n return (\n <div className=\"grid gap-4 sm:grid-cols-2\">\n {FEATURE_GROUPS.map((group) => {\n const groupFeatures = group.features\n const allSelected = groupFeatures.every((featureId) => features.includes(featureId))\n const someSelected = groupFeatures.some((featureId) => features.includes(featureId))\n return (\n <div key={group.id} className=\"rounded-lg border\">\n <div className=\"flex items-center justify-between border-b px-4 py-3\">\n <span className=\"text-sm font-semibold\">{t(group.labelKey, group.fallback)}</span>\n <label className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n <input\n type=\"checkbox\"\n checked={allSelected}\n ref={(el) => { if (el) el.indeterminate = someSelected && !allSelected }}\n onChange={() => handleGroupToggle(groupFeatures)}\n className=\"rounded border-border\"\n />\n {t('customer_accounts.admin.roleDetail.selectAll', 'Select all')}\n </label>\n </div>\n <div className=\"divide-y\">\n {groupFeatures.map((featureId) => {\n const feature = PORTAL_FEATURES.find((portalFeature) => portalFeature.id === featureId)\n return (\n <label key={featureId} className=\"flex items-start gap-3 px-4 py-3 cursor-pointer hover:bg-muted/50 transition-colors\">\n <input\n type=\"checkbox\"\n checked={features.includes(featureId)}\n onChange={() => handleFeatureToggle(featureId)}\n className=\"mt-0.5 rounded border-border\"\n />\n <div className=\"space-y-0.5\">\n <div className=\"text-sm font-medium\">{feature ? t(feature.labelKey, feature.fallback) : featureId}</div>\n {feature && (\n <div className=\"text-xs text-muted-foreground\">{t(feature.descriptionKey, feature.descriptionFallback)}</div>\n )}\n </div>\n </label>\n )\n })}\n </div>\n </div>\n )\n })}\n </div>\n )\n}\n\nexport default function CustomerRoleDetailPage({ params }: { params?: { id?: string } }) {\n const id = params?.id\n const t = useT()\n const router = useRouter()\n const [data, setData] = React.useState<RoleDetail | null>(null)\n const [isLoading, setIsLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [isNotFound, setIsNotFound] = React.useState(false)\n\n React.useEffect(() => {\n if (!id) {\n setIsNotFound(true)\n setIsLoading(false)\n return\n }\n let cancelled = false\n async function load() {\n setIsLoading(true)\n setError(null)\n setIsNotFound(false)\n try {\n const payload = await readApiResultOrThrow<RoleDetail>(\n `/api/customer_accounts/admin/roles/${encodeURIComponent(id!)}`,\n undefined,\n { errorMessage: t('customer_accounts.admin.roleDetail.error.load', 'Failed to load role') },\n )\n if (cancelled) return\n setData(payload)\n } catch (err) {\n if (cancelled) return\n if ((err as { status?: number }).status === 404) {\n setIsNotFound(true)\n } else {\n const message = err instanceof Error ? err.message : t('customer_accounts.admin.roleDetail.error.load', 'Failed to load role')\n setError(message)\n }\n } finally {\n if (!cancelled) setIsLoading(false)\n }\n }\n load()\n return () => { cancelled = true }\n }, [id, t])\n\n const fields = React.useMemo<CrudField[]>(() => {\n if (!data) return []\n return [\n {\n id: 'name',\n type: 'text' as const,\n label: t('customer_accounts.admin.roleDetail.fields.name', 'Name'),\n required: true,\n disabled: data.isSystem,\n },\n {\n id: 'description',\n type: 'textarea' as const,\n label: t('customer_accounts.admin.roleDetail.fields.description', 'Description'),\n },\n {\n id: 'isDefault',\n type: 'checkbox' as const,\n label: t('customer_accounts.admin.roleDetail.fields.isDefault', 'Default role (auto-assigned to new users)'),\n },\n {\n id: 'customerAssignable',\n type: 'checkbox' as const,\n label: t('customer_accounts.admin.roleDetail.fields.customerAssignable', 'Customers can self-assign'),\n },\n ]\n }, [data, t])\n\n const groups = React.useMemo<CrudFormGroup[]>(() => [\n {\n id: 'details',\n title: t('customer_accounts.admin.roleDetail.sections.details', 'Role Details'),\n column: 1,\n fields: ['name', 'description'],\n },\n {\n id: 'options',\n title: t('customer_accounts.admin.roleDetail.sections.options', 'Options'),\n column: 1,\n fields: ['isDefault', 'customerAssignable'],\n },\n {\n id: 'permissions',\n title: t('customer_accounts.admin.roleDetail.sections.permissions', 'Portal Permissions'),\n column: 1,\n component: PortalPermissionsEditor,\n },\n ], [t])\n\n const initialValues = React.useMemo(() => {\n if (!data) return {}\n return {\n name: data.name,\n description: data.description || '',\n isDefault: data.isDefault,\n customerAssignable: data.customerAssignable,\n features: data.features,\n updatedAt: data.updatedAt ?? data.updated_at ?? null,\n }\n }, [data])\n\n const handleSubmit = React.useCallback(async (values: Record<string, unknown>) => {\n if (!id) return\n const headers = buildOptimisticLockHeader(data?.updatedAt ?? data?.updated_at ?? null)\n const roleCall = await withScopedApiRequestHeaders(headers, () => (\n apiCall(\n `/api/customer_accounts/admin/roles/${encodeURIComponent(id)}`,\n {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n name: (values.name as string)?.trim(),\n description: (values.description as string)?.trim() || null,\n isDefault: values.isDefault,\n customerAssignable: values.customerAssignable,\n }),\n },\n )\n ))\n if (!roleCall.ok) {\n if (surfaceRecordConflict({ status: roleCall.status, body: roleCall.result }, t)) return\n flash(t('customer_accounts.admin.roleDetail.error.save', 'Failed to save role'), 'error')\n return\n }\n const features = Array.isArray(values.features) ? values.features : []\n // The role PUT just bumped the aggregate's `updatedAt`; the ACL write guards\n // the SAME role aggregate, so send its NEW version (not the stale pre-edit one)\n // to avoid a false 409 while still blocking a concurrent overwrite (#3194).\n const roleResult = (roleCall.result ?? null) as { updatedAt?: string | null } | null\n const aclHeaders = buildOptimisticLockHeader(roleResult?.updatedAt ?? data?.updatedAt ?? data?.updated_at ?? null)\n const aclCall = await withScopedApiRequestHeaders(aclHeaders, () => (\n apiCall(\n `/api/customer_accounts/admin/roles/${encodeURIComponent(id)}/acl`,\n {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ features }),\n },\n )\n ))\n if (!aclCall.ok) {\n if (surfaceRecordConflict({ status: aclCall.status, body: aclCall.result }, t)) return\n flash(t('customer_accounts.admin.roleDetail.error.saveAcl', 'Failed to save permissions'), 'error')\n return\n }\n flash(t('customer_accounts.admin.roleDetail.flash.saved', 'Role updated'), 'success')\n router.push('/backend/customer_accounts/roles')\n }, [data?.updatedAt, data?.updated_at, id, router, t])\n\n const handleDelete = React.useCallback(async () => {\n if (!id) return\n const headers = buildOptimisticLockHeader(data?.updatedAt ?? data?.updated_at ?? null)\n const call = await withScopedApiRequestHeaders(headers, () => (\n apiCall(\n `/api/customer_accounts/admin/roles/${encodeURIComponent(id)}`,\n { method: 'DELETE' },\n )\n ))\n if (!call.ok) {\n if (surfaceRecordConflict({ status: call.status, body: call.result }, t)) return\n flash(t('customer_accounts.admin.roles.error.delete', 'Failed to delete role'), 'error')\n return\n }\n flash(t('customer_accounts.admin.roles.flash.deleted', 'Role deleted'), 'success')\n router.push('/backend/customer_accounts/roles')\n }, [data?.updatedAt, data?.updated_at, id, router, t])\n\n if (isLoading) {\n return (\n <Page>\n <PageBody>\n <div className=\"flex h-[50vh] flex-col items-center justify-center gap-2 text-muted-foreground\">\n <Spinner className=\"h-6 w-6\" />\n <span>{t('customer_accounts.admin.roleDetail.loading', 'Loading role...')}</span>\n </div>\n </PageBody>\n </Page>\n )\n }\n\n if (isNotFound) {\n return (\n <Page>\n <PageBody>\n <RecordNotFoundState\n label={t('customer_accounts.admin.roleDetail.error.notFound', 'Role not found')}\n backHref=\"/backend/customer_accounts/roles\"\n backLabel={t('customer_accounts.admin.roleDetail.actions.backToList', 'Back to roles')}\n />\n </PageBody>\n </Page>\n )\n }\n\n if (error || !data) {\n return (\n <Page>\n <PageBody>\n <ErrorMessage\n label={error ?? t('customer_accounts.admin.roleDetail.error.notFound', 'Role not found')}\n action={\n <Button asChild variant=\"outline\" size=\"sm\">\n <Link href=\"/backend/customer_accounts/roles\">\n {t('customer_accounts.admin.roleDetail.actions.backToList', 'Back to roles')}\n </Link>\n </Button>\n }\n />\n </PageBody>\n </Page>\n )\n }\n\n return (\n <Page>\n <PageBody>\n <CrudForm\n title={data.name}\n backHref=\"/backend/customer_accounts/roles\"\n fields={fields}\n groups={groups}\n initialValues={initialValues}\n optimisticLockUpdatedAt={data.updatedAt ?? data.updated_at ?? null}\n entityId=\"customer_accounts:customer_role\"\n onSubmit={handleSubmit}\n onDelete={!data.isSystem ? handleDelete : undefined}\n cancelHref=\"/backend/customer_accounts/roles\"\n />\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { CrudForm } from '@open-mercato/ui/backend/CrudForm'\nimport type { CrudField, CrudFormGroup, CrudFormGroupComponentProps } from '@open-mercato/ui/backend/CrudForm'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Checkbox } from '@open-mercato/ui/primitives/checkbox'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { apiCall, readApiResultOrThrow, 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 { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { RecordNotFoundState, ErrorMessage } from '@open-mercato/ui/backend/detail'\n\ntype RoleDetail = {\n id: string\n name: string\n slug: string\n description: string | null\n isDefault: boolean\n isSystem: boolean\n customerAssignable: boolean\n features: string[]\n updatedAt?: string | null\n updated_at?: string | null\n}\n\nconst PORTAL_FEATURES = [\n { id: 'portal.profile.view', labelKey: 'customer_accounts.admin.portalFeatures.profile.view', fallback: 'View profile', descriptionKey: 'customer_accounts.admin.portalFeatures.profile.view.description', descriptionFallback: 'Allows viewing own profile information and account details' },\n { id: 'portal.profile.edit', labelKey: 'customer_accounts.admin.portalFeatures.profile.edit', fallback: 'Edit profile', descriptionKey: 'customer_accounts.admin.portalFeatures.profile.edit.description', descriptionFallback: 'Allows editing display name and other profile settings' },\n { id: 'portal.orders.view', labelKey: 'customer_accounts.admin.portalFeatures.orders.view', fallback: 'View orders', descriptionKey: 'customer_accounts.admin.portalFeatures.orders.view.description', descriptionFallback: 'Allows viewing order history and order details' },\n { id: 'portal.orders.create', labelKey: 'customer_accounts.admin.portalFeatures.orders.create', fallback: 'Create orders', descriptionKey: 'customer_accounts.admin.portalFeatures.orders.create.description', descriptionFallback: 'Allows placing new orders through the portal' },\n { id: 'portal.invoices.view', labelKey: 'customer_accounts.admin.portalFeatures.invoices.view', fallback: 'View invoices', descriptionKey: 'customer_accounts.admin.portalFeatures.invoices.view.description', descriptionFallback: 'Allows viewing invoices and payment history' },\n { id: 'portal.quotes.view', labelKey: 'customer_accounts.admin.portalFeatures.quotes.view', fallback: 'View quotes', descriptionKey: 'customer_accounts.admin.portalFeatures.quotes.view.description', descriptionFallback: 'Allows viewing received quotes and their details' },\n { id: 'portal.quotes.request', labelKey: 'customer_accounts.admin.portalFeatures.quotes.request', fallback: 'Request quotes', descriptionKey: 'customer_accounts.admin.portalFeatures.quotes.request.description', descriptionFallback: 'Allows requesting new quotes from the company' },\n { id: 'portal.addresses.view', labelKey: 'customer_accounts.admin.portalFeatures.addresses.view', fallback: 'View addresses', descriptionKey: 'customer_accounts.admin.portalFeatures.addresses.view.description', descriptionFallback: 'Allows viewing saved shipping and billing addresses' },\n { id: 'portal.addresses.manage', labelKey: 'customer_accounts.admin.portalFeatures.addresses.manage', fallback: 'Manage addresses', descriptionKey: 'customer_accounts.admin.portalFeatures.addresses.manage.description', descriptionFallback: 'Allows adding, editing, and removing addresses' },\n { id: 'portal.users.view', labelKey: 'customer_accounts.admin.portalFeatures.users.view', fallback: 'View team members', descriptionKey: 'customer_accounts.admin.portalFeatures.users.view.description', descriptionFallback: 'Allows viewing other team members in the organization' },\n { id: 'portal.users.invite', labelKey: 'customer_accounts.admin.portalFeatures.users.invite', fallback: 'Invite team members', descriptionKey: 'customer_accounts.admin.portalFeatures.users.invite.description', descriptionFallback: 'Allows sending portal invitations to new team members' },\n { id: 'portal.users.manage', labelKey: 'customer_accounts.admin.portalFeatures.users.manage', fallback: 'Manage team members', descriptionKey: 'customer_accounts.admin.portalFeatures.users.manage.description', descriptionFallback: 'Allows editing roles and removing team members' },\n]\n\nconst FEATURE_GROUPS: Array<{ id: string; labelKey: string; fallback: string; features: string[] }> = (() => {\n const groups = new Map<string, string[]>()\n for (const feature of PORTAL_FEATURES) {\n const parts = feature.id.split('.')\n const groupKey = parts.length >= 2 ? `${parts[0]}.${parts[1]}` : parts[0]\n const existing = groups.get(groupKey)\n if (existing) {\n existing.push(feature.id)\n } else {\n groups.set(groupKey, [feature.id])\n }\n }\n return Array.from(groups.entries()).map(([groupId, features]) => {\n const scope = groupId.split('.').slice(1).join('')\n const fallback = scope.replace(/^\\w/, (ch) => ch.toUpperCase())\n return {\n id: groupId,\n labelKey: `customer_accounts.admin.portalFeatures.groups.${scope}`,\n fallback,\n features,\n }\n })\n})()\n\nfunction PortalPermissionsEditor({ values, setValue }: CrudFormGroupComponentProps) {\n const t = useT()\n const features = React.useMemo(\n () => Array.isArray(values.features) ? values.features as string[] : [],\n [values.features],\n )\n\n const handleFeatureToggle = React.useCallback((featureId: string) => {\n const next = features.includes(featureId)\n ? features.filter((existingId) => existingId !== featureId)\n : [...features, featureId]\n setValue('features', next)\n }, [features, setValue])\n\n const handleGroupToggle = React.useCallback((featureIds: string[]) => {\n const allSelected = featureIds.every((featureId) => features.includes(featureId))\n let next: string[]\n if (allSelected) {\n next = features.filter((featureId) => !featureIds.includes(featureId))\n } else {\n next = [...features]\n for (const featureId of featureIds) {\n if (!next.includes(featureId)) next.push(featureId)\n }\n }\n setValue('features', next)\n }, [features, setValue])\n\n return (\n <div className=\"grid gap-4 sm:grid-cols-2\">\n {FEATURE_GROUPS.map((group) => {\n const groupFeatures = group.features\n const allSelected = groupFeatures.every((featureId) => features.includes(featureId))\n const someSelected = groupFeatures.some((featureId) => features.includes(featureId))\n return (\n <div key={group.id} className=\"rounded-lg border\">\n <div className=\"flex items-center justify-between border-b px-4 py-3\">\n <span className=\"text-sm font-semibold\">{t(group.labelKey, group.fallback)}</span>\n <label className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n <Checkbox\n checked={allSelected ? true : (someSelected ? 'indeterminate' : false)}\n onCheckedChange={() => handleGroupToggle(groupFeatures)}\n />\n {t('customer_accounts.admin.roleDetail.selectAll', 'Select all')}\n </label>\n </div>\n <div className=\"divide-y\">\n {groupFeatures.map((featureId) => {\n const feature = PORTAL_FEATURES.find((portalFeature) => portalFeature.id === featureId)\n return (\n <label key={featureId} className=\"flex items-start gap-3 px-4 py-3 cursor-pointer hover:bg-muted/50 transition-colors\">\n <Checkbox\n checked={features.includes(featureId)}\n onCheckedChange={() => handleFeatureToggle(featureId)}\n className=\"mt-0.5\"\n />\n <div className=\"space-y-0.5\">\n <div className=\"text-sm font-medium\">{feature ? t(feature.labelKey, feature.fallback) : featureId}</div>\n {feature && (\n <div className=\"text-xs text-muted-foreground\">{t(feature.descriptionKey, feature.descriptionFallback)}</div>\n )}\n </div>\n </label>\n )\n })}\n </div>\n </div>\n )\n })}\n </div>\n )\n}\n\nexport default function CustomerRoleDetailPage({ params }: { params?: { id?: string } }) {\n const id = params?.id\n const t = useT()\n const router = useRouter()\n const [data, setData] = React.useState<RoleDetail | null>(null)\n const [isLoading, setIsLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [isNotFound, setIsNotFound] = React.useState(false)\n\n React.useEffect(() => {\n if (!id) {\n setIsNotFound(true)\n setIsLoading(false)\n return\n }\n let cancelled = false\n async function load() {\n setIsLoading(true)\n setError(null)\n setIsNotFound(false)\n try {\n const payload = await readApiResultOrThrow<RoleDetail>(\n `/api/customer_accounts/admin/roles/${encodeURIComponent(id!)}`,\n undefined,\n { errorMessage: t('customer_accounts.admin.roleDetail.error.load', 'Failed to load role') },\n )\n if (cancelled) return\n setData(payload)\n } catch (err) {\n if (cancelled) return\n if ((err as { status?: number }).status === 404) {\n setIsNotFound(true)\n } else {\n const message = err instanceof Error ? err.message : t('customer_accounts.admin.roleDetail.error.load', 'Failed to load role')\n setError(message)\n }\n } finally {\n if (!cancelled) setIsLoading(false)\n }\n }\n load()\n return () => { cancelled = true }\n }, [id, t])\n\n const fields = React.useMemo<CrudField[]>(() => {\n if (!data) return []\n return [\n {\n id: 'name',\n type: 'text' as const,\n label: t('customer_accounts.admin.roleDetail.fields.name', 'Name'),\n required: true,\n disabled: data.isSystem,\n },\n {\n id: 'description',\n type: 'textarea' as const,\n label: t('customer_accounts.admin.roleDetail.fields.description', 'Description'),\n },\n {\n id: 'isDefault',\n type: 'checkbox' as const,\n label: t('customer_accounts.admin.roleDetail.fields.isDefault', 'Default role (auto-assigned to new users)'),\n },\n {\n id: 'customerAssignable',\n type: 'checkbox' as const,\n label: t('customer_accounts.admin.roleDetail.fields.customerAssignable', 'Customers can self-assign'),\n },\n ]\n }, [data, t])\n\n const groups = React.useMemo<CrudFormGroup[]>(() => [\n {\n id: 'details',\n title: t('customer_accounts.admin.roleDetail.sections.details', 'Role Details'),\n column: 1,\n fields: ['name', 'description'],\n },\n {\n id: 'options',\n title: t('customer_accounts.admin.roleDetail.sections.options', 'Options'),\n column: 1,\n fields: ['isDefault', 'customerAssignable'],\n },\n {\n id: 'permissions',\n title: t('customer_accounts.admin.roleDetail.sections.permissions', 'Portal Permissions'),\n column: 1,\n component: PortalPermissionsEditor,\n },\n ], [t])\n\n const initialValues = React.useMemo(() => {\n if (!data) return {}\n return {\n name: data.name,\n description: data.description || '',\n isDefault: data.isDefault,\n customerAssignable: data.customerAssignable,\n features: data.features,\n updatedAt: data.updatedAt ?? data.updated_at ?? null,\n }\n }, [data])\n\n const handleSubmit = React.useCallback(async (values: Record<string, unknown>) => {\n if (!id) return\n const headers = buildOptimisticLockHeader(data?.updatedAt ?? data?.updated_at ?? null)\n const roleCall = await withScopedApiRequestHeaders(headers, () => (\n apiCall(\n `/api/customer_accounts/admin/roles/${encodeURIComponent(id)}`,\n {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n name: (values.name as string)?.trim(),\n description: (values.description as string)?.trim() || null,\n isDefault: values.isDefault,\n customerAssignable: values.customerAssignable,\n }),\n },\n )\n ))\n if (!roleCall.ok) {\n if (surfaceRecordConflict({ status: roleCall.status, body: roleCall.result }, t)) return\n flash(t('customer_accounts.admin.roleDetail.error.save', 'Failed to save role'), 'error')\n return\n }\n const features = Array.isArray(values.features) ? values.features : []\n // The role PUT just bumped the aggregate's `updatedAt`; the ACL write guards\n // the SAME role aggregate, so send its NEW version (not the stale pre-edit one)\n // to avoid a false 409 while still blocking a concurrent overwrite (#3194).\n const roleResult = (roleCall.result ?? null) as { updatedAt?: string | null } | null\n const aclHeaders = buildOptimisticLockHeader(roleResult?.updatedAt ?? data?.updatedAt ?? data?.updated_at ?? null)\n const aclCall = await withScopedApiRequestHeaders(aclHeaders, () => (\n apiCall(\n `/api/customer_accounts/admin/roles/${encodeURIComponent(id)}/acl`,\n {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ features }),\n },\n )\n ))\n if (!aclCall.ok) {\n if (surfaceRecordConflict({ status: aclCall.status, body: aclCall.result }, t)) return\n flash(t('customer_accounts.admin.roleDetail.error.saveAcl', 'Failed to save permissions'), 'error')\n return\n }\n flash(t('customer_accounts.admin.roleDetail.flash.saved', 'Role updated'), 'success')\n router.push('/backend/customer_accounts/roles')\n }, [data?.updatedAt, data?.updated_at, id, router, t])\n\n const handleDelete = React.useCallback(async () => {\n if (!id) return\n const headers = buildOptimisticLockHeader(data?.updatedAt ?? data?.updated_at ?? null)\n const call = await withScopedApiRequestHeaders(headers, () => (\n apiCall(\n `/api/customer_accounts/admin/roles/${encodeURIComponent(id)}`,\n { method: 'DELETE' },\n )\n ))\n if (!call.ok) {\n if (surfaceRecordConflict({ status: call.status, body: call.result }, t)) return\n flash(t('customer_accounts.admin.roles.error.delete', 'Failed to delete role'), 'error')\n return\n }\n flash(t('customer_accounts.admin.roles.flash.deleted', 'Role deleted'), 'success')\n router.push('/backend/customer_accounts/roles')\n }, [data?.updatedAt, data?.updated_at, id, router, t])\n\n if (isLoading) {\n return (\n <Page>\n <PageBody>\n <div className=\"flex h-[50vh] flex-col items-center justify-center gap-2 text-muted-foreground\">\n <Spinner className=\"h-6 w-6\" />\n <span>{t('customer_accounts.admin.roleDetail.loading', 'Loading role...')}</span>\n </div>\n </PageBody>\n </Page>\n )\n }\n\n if (isNotFound) {\n return (\n <Page>\n <PageBody>\n <RecordNotFoundState\n label={t('customer_accounts.admin.roleDetail.error.notFound', 'Role not found')}\n backHref=\"/backend/customer_accounts/roles\"\n backLabel={t('customer_accounts.admin.roleDetail.actions.backToList', 'Back to roles')}\n />\n </PageBody>\n </Page>\n )\n }\n\n if (error || !data) {\n return (\n <Page>\n <PageBody>\n <ErrorMessage\n label={error ?? t('customer_accounts.admin.roleDetail.error.notFound', 'Role not found')}\n action={\n <Button asChild variant=\"outline\" size=\"sm\">\n <Link href=\"/backend/customer_accounts/roles\">\n {t('customer_accounts.admin.roleDetail.actions.backToList', 'Back to roles')}\n </Link>\n </Button>\n }\n />\n </PageBody>\n </Page>\n )\n }\n\n return (\n <Page>\n <PageBody>\n <CrudForm\n title={data.name}\n backHref=\"/backend/customer_accounts/roles\"\n fields={fields}\n groups={groups}\n initialValues={initialValues}\n optimisticLockUpdatedAt={data.updatedAt ?? data.updated_at ?? null}\n entityId=\"customer_accounts:customer_role\"\n onSubmit={handleSubmit}\n onDelete={!data.isSystem ? handleDelete : undefined}\n cancelHref=\"/backend/customer_accounts/roles\"\n />\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -156,13 +156,16 @@ function CreateUserDialog({
|
|
|
156
156
|
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: roleOptions.map((role) => {
|
|
157
157
|
const isSelected = selectedRoleIds.includes(role.id);
|
|
158
158
|
return /* @__PURE__ */ jsx(
|
|
159
|
-
|
|
159
|
+
Button,
|
|
160
160
|
{
|
|
161
161
|
type: "button",
|
|
162
|
+
size: "2xs",
|
|
163
|
+
variant: isSelected ? "secondary" : "outline",
|
|
164
|
+
"aria-pressed": isSelected,
|
|
162
165
|
onClick: () => setSelectedRoleIds(
|
|
163
166
|
(prev) => prev.includes(role.id) ? prev.filter((rid) => rid !== role.id) : [...prev, role.id]
|
|
164
167
|
),
|
|
165
|
-
className:
|
|
168
|
+
className: "rounded-full",
|
|
166
169
|
children: role.label
|
|
167
170
|
},
|
|
168
171
|
role.id
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/customer_accounts/backend/customer_accounts/users/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport { Globe, Settings } from 'lucide-react'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { EmailInput } from '@open-mercato/ui/primitives/email-input'\nimport { PasswordInput } from '@open-mercato/ui/primitives/password-input'\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'\nimport { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { ListEmptyState } from '@open-mercato/ui/backend/filters/ListEmptyState'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\n\ntype UserRow = {\n id: string\n displayName: string\n email: string\n emailVerified: boolean\n isActive: boolean\n lastLoginAt: string | null\n roles: Array<{ id: string; name: string; slug: string }>\n createdAt: string\n updatedAt?: string | null\n personEntityId: string | null\n customerEntityId: string | null\n}\n\ntype UsersResponse = {\n items?: UserRow[]\n total?: number\n totalPages?: number\n}\n\nfunction formatDate(value: string | null | undefined, fallback: string): string {\n if (!value) return fallback\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return fallback\n return date.toLocaleDateString()\n}\n\nasync function fetchRoleFilterOptions(): Promise<Array<{ value: string; label: string; id: string }>> {\n try {\n const call = await apiCall<{ items?: Array<{ id: string; name: string }> }>(\n '/api/customer_accounts/admin/roles?pageSize=100',\n )\n if (!call.ok) return []\n const items = Array.isArray(call.result?.items) ? call.result!.items : []\n return items\n .filter((item) => typeof item?.id === 'string' && typeof item?.name === 'string')\n .map((item) => ({ value: item.id, label: item.name, id: item.id }))\n } catch {\n return []\n }\n}\n\nfunction CreateUserDialog({\n open,\n onOpenChange,\n roleOptions,\n onCreated,\n onRunMutation,\n}: {\n open: boolean\n onOpenChange: (next: boolean) => void\n roleOptions: Array<{ id: string; label: string }>\n onCreated: () => void\n onRunMutation: <T>(operation: () => Promise<T>) => Promise<T>\n}) {\n const t = useT()\n const [email, setEmail] = React.useState('')\n const [displayName, setDisplayName] = React.useState('')\n const [password, setPassword] = React.useState('')\n const [selectedRoleIds, setSelectedRoleIds] = React.useState<string[]>([])\n const [isSubmitting, setIsSubmitting] = React.useState(false)\n\n const resetForm = React.useCallback(() => {\n setEmail('')\n setDisplayName('')\n setPassword('')\n setSelectedRoleIds([])\n }, [])\n\n const handleSubmit = React.useCallback(async (event: React.FormEvent) => {\n event.preventDefault()\n if (!email.trim() || !displayName.trim() || !password.trim()) {\n flash(t('customer_accounts.admin.createUser.error.required', 'Email, name, and password are required'), 'error')\n return\n }\n setIsSubmitting(true)\n try {\n await onRunMutation(async () => {\n const call = await apiCall<{ ok: boolean; error?: string }>(\n '/api/customer_accounts/admin/users',\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n email: email.trim(),\n displayName: displayName.trim(),\n password,\n roleIds: selectedRoleIds.length > 0 ? selectedRoleIds : undefined,\n }),\n },\n )\n if (!call.ok) {\n flash(call.result?.error || t('customer_accounts.admin.createUser.error.save', 'Failed to create user'), 'error')\n return\n }\n flash(t('customer_accounts.admin.createUser.flash.created', 'User created'), 'success')\n resetForm()\n onOpenChange(false)\n onCreated()\n })\n } catch (err) {\n const message = err instanceof Error ? err.message : t('customer_accounts.admin.createUser.error.save', 'Failed to create user')\n flash(message, 'error')\n } finally {\n setIsSubmitting(false)\n }\n }, [displayName, email, onCreated, onOpenChange, onRunMutation, password, resetForm, selectedRoleIds, t])\n\n const handleKeyDown = React.useCallback((event: React.KeyboardEvent) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n const form = (event.target as HTMLElement).closest('form')\n if (form) form.requestSubmit()\n }\n }, [])\n\n return (\n <Dialog open={open} onOpenChange={(next) => { if (!next) resetForm(); onOpenChange(next) }}>\n <DialogContent className=\"sm:max-w-lg\">\n <DialogHeader>\n <DialogTitle>{t('customer_accounts.admin.createUser.title', 'Create Customer User')}</DialogTitle>\n </DialogHeader>\n <form onSubmit={(event) => { void handleSubmit(event) }} onKeyDown={handleKeyDown} className=\"space-y-4\">\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\" htmlFor=\"create-email\">\n {t('customer_accounts.admin.createUser.fields.email', 'Email')}\n </label>\n <EmailInput\n id=\"create-email\"\n required\n value={email}\n onChange={(event) => setEmail(event.target.value)}\n placeholder={t('customer_accounts.admin.createUser.fields.emailPlaceholder', 'user@example.com')}\n />\n </div>\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\" htmlFor=\"create-name\">\n {t('customer_accounts.admin.createUser.fields.displayName', 'Display Name')}\n </label>\n <Input\n id=\"create-name\"\n type=\"text\"\n required\n value={displayName}\n onChange={(event) => setDisplayName(event.target.value)}\n placeholder={t('customer_accounts.admin.createUser.fields.displayNamePlaceholder', 'John Doe')}\n />\n </div>\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\" htmlFor=\"create-password\">\n {t('customer_accounts.admin.createUser.fields.password', 'Password')}\n </label>\n <PasswordInput\n id=\"create-password\"\n required\n minLength={8}\n value={password}\n onChange={(event) => setPassword(event.target.value)}\n placeholder={t('customer_accounts.admin.createUser.fields.passwordPlaceholder', 'Min. 8 characters')}\n autoComplete=\"new-password\"\n />\n </div>\n {roleOptions.length > 0 && (\n <div className=\"space-y-2\">\n <p className=\"text-sm font-medium\">{t('customer_accounts.admin.createUser.fields.roles', 'Roles')}</p>\n <div className=\"flex flex-wrap gap-2\">\n {roleOptions.map((role) => {\n const isSelected = selectedRoleIds.includes(role.id)\n return (\n <button\n key={role.id}\n type=\"button\"\n onClick={() => setSelectedRoleIds((prev) =>\n prev.includes(role.id) ? prev.filter((rid) => rid !== role.id) : [...prev, role.id],\n )}\n className={`rounded-full border px-3 py-1 text-xs font-medium transition-colors ${\n isSelected\n ? 'border-primary bg-primary/10 text-primary'\n : 'border-border bg-background text-muted-foreground hover:bg-muted'\n }`}\n >\n {role.label}\n </button>\n )\n })}\n </div>\n </div>\n )}\n <div className=\"flex justify-end gap-2 pt-2\">\n <Button type=\"button\" variant=\"outline\" onClick={() => { resetForm(); onOpenChange(false) }}>\n {t('customer_accounts.admin.createUser.actions.cancel', 'Cancel')}\n </Button>\n <Button type=\"submit\" disabled={isSubmitting}>\n {isSubmitting\n ? t('customer_accounts.admin.createUser.actions.creating', 'Creating...')\n : t('customer_accounts.admin.createUser.actions.create', 'Create User')}\n </Button>\n </div>\n </form>\n </DialogContent>\n </Dialog>\n )\n}\n\nexport default function CustomerAccountsPage() {\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const t = useT()\n const router = useRouter()\n const [rows, setRows] = React.useState<UserRow[]>([])\n const [page, setPage] = React.useState(1)\n const [pageSize] = React.useState(50)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [search, setSearch] = React.useState('')\n const [filterValues, setFilterValues] = React.useState<FilterValues>({})\n const [isLoading, setIsLoading] = React.useState(true)\n const [reloadToken, setReloadToken] = React.useState(0)\n const [roleOptions, setRoleOptions] = React.useState<Array<{ value: string; label: string; id: string }>>([])\n const [createDialogOpen, setCreateDialogOpen] = React.useState(false)\n\n const { runMutation, retryLastMutation } = useGuardedMutation<{\n entityType: string\n entityId?: string\n }>({\n contextId: 'customer_accounts:users-list',\n })\n\n const runMutationWithContext = React.useCallback(\n async <T,>(operation: () => Promise<T>, mutationPayload?: Record<string, unknown>): Promise<T> => {\n return runMutation({\n operation,\n mutationPayload,\n context: { entityType: 'customer_accounts:user' },\n })\n },\n [runMutation],\n )\n\n React.useEffect(() => {\n let cancelled = false\n fetchRoleFilterOptions().then((opts) => {\n if (!cancelled) setRoleOptions(opts)\n })\n return () => { cancelled = true }\n }, [])\n\n const filters = React.useMemo<FilterDef[]>(() => [\n {\n id: 'status',\n label: t('customer_accounts.admin.filters.status', 'Status'),\n type: 'select',\n options: [\n { value: 'active', label: t('customer_accounts.admin.filters.active', 'Active') },\n { value: 'inactive', label: t('customer_accounts.admin.filters.inactive', 'Inactive') },\n ],\n },\n {\n id: 'roleId',\n label: t('customer_accounts.admin.filters.role', 'Role'),\n type: 'select',\n options: roleOptions,\n },\n ], [roleOptions, t])\n\n const queryParams = React.useMemo(() => {\n const params = new URLSearchParams()\n params.set('page', String(page))\n params.set('pageSize', String(pageSize))\n if (search.trim()) params.set('search', search.trim())\n const status = filterValues.status\n if (typeof status === 'string' && status.trim()) params.set('status', status)\n const roleId = filterValues.roleId\n if (typeof roleId === 'string' && roleId.trim()) params.set('roleId', roleId)\n return params.toString()\n }, [filterValues, page, pageSize, search])\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setIsLoading(true)\n try {\n const fallback: UsersResponse = { items: [], total: 0, totalPages: 1 }\n const payload = await readApiResultOrThrow<UsersResponse>(\n `/api/customer_accounts/admin/users?${queryParams}`,\n undefined,\n { errorMessage: t('customer_accounts.admin.error.loadUsers', 'Failed to load customer users'), fallback },\n )\n if (cancelled) return\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 : 1)\n } catch (err) {\n if (!cancelled) {\n const message = err instanceof Error ? err.message : t('customer_accounts.admin.error.loadUsers', 'Failed to load customer users')\n flash(message, 'error')\n }\n } finally {\n if (!cancelled) setIsLoading(false)\n }\n }\n load()\n return () => { cancelled = true }\n }, [queryParams, reloadToken, t])\n\n const handleToggleActive = React.useCallback(async (user: UserRow) => {\n const nextActive = !user.isActive\n const actionLabel = nextActive\n ? t('customer_accounts.admin.actions.activate', 'Activate')\n : t('customer_accounts.admin.actions.deactivate', 'Deactivate')\n const confirmed = await confirm({\n title: t('customer_accounts.admin.confirm.toggleActive', '{{action}} user \"{{name}}\"?', {\n action: actionLabel,\n name: user.displayName || user.email,\n }),\n variant: nextActive ? 'default' : 'destructive',\n })\n if (!confirmed) return\n try {\n await runMutationWithContext(async () => {\n const call = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(user.updatedAt),\n () => apiCall(\n `/api/customer_accounts/admin/users/${encodeURIComponent(user.id)}`,\n {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ isActive: nextActive }),\n },\n ),\n )\n if (!call.ok) {\n flash(t('customer_accounts.admin.error.toggleActive', 'Failed to update user status'), 'error')\n return\n }\n flash(\n nextActive\n ? t('customer_accounts.admin.flash.activated', 'User activated')\n : t('customer_accounts.admin.flash.deactivated', 'User deactivated'),\n 'success',\n )\n setReloadToken((token) => token + 1)\n }, { userId: user.id, isActive: nextActive })\n } catch (err) {\n const message = err instanceof Error ? err.message : t('customer_accounts.admin.error.toggleActive', 'Failed to update user status')\n flash(message, 'error')\n }\n }, [confirm, runMutationWithContext, t])\n\n const handleDelete = React.useCallback(async (user: UserRow) => {\n const confirmed = await confirm({\n title: t('customer_accounts.admin.confirm.delete', 'Delete user \"{{name}}\"?', {\n name: user.displayName || user.email,\n }),\n variant: 'destructive',\n })\n if (!confirmed) return\n try {\n await runMutationWithContext(async () => {\n const call = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(user.updatedAt),\n () => apiCall(\n `/api/customer_accounts/admin/users/${encodeURIComponent(user.id)}`,\n { method: 'DELETE' },\n ),\n )\n if (!call.ok) {\n flash(t('customer_accounts.admin.error.delete', 'Failed to delete user'), 'error')\n return\n }\n flash(t('customer_accounts.admin.flash.deleted', 'User deleted'), 'success')\n setReloadToken((token) => token + 1)\n }, { id: user.id })\n } catch (err) {\n const message = err instanceof Error ? err.message : t('customer_accounts.admin.error.delete', 'Failed to delete user')\n flash(message, 'error')\n }\n }, [confirm, runMutationWithContext, t])\n\n const handleFiltersApply = React.useCallback((values: FilterValues) => {\n setFilterValues(values)\n setPage(1)\n }, [])\n\n const handleFiltersClear = React.useCallback(() => {\n setFilterValues({})\n setPage(1)\n }, [])\n\n const columns = React.useMemo<ColumnDef<UserRow>[]>(() => {\n const noValue = <span className=\"text-muted-foreground text-sm\">-</span>\n return [\n {\n accessorKey: 'displayName',\n header: t('customer_accounts.admin.columns.displayName', 'Name'),\n cell: ({ row }) => (\n <Link\n href={`/backend/customer_accounts/users/${row.original.id}`}\n className=\"font-medium hover:underline\"\n >\n {row.original.displayName || row.original.email}\n </Link>\n ),\n },\n {\n accessorKey: 'email',\n header: t('customer_accounts.admin.columns.email', 'Email'),\n },\n {\n accessorKey: 'emailVerified',\n header: t('customer_accounts.admin.columns.emailVerified', 'Verified'),\n cell: ({ row }) => (\n <span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${\n row.original.emailVerified\n ? 'bg-status-success-bg text-status-success-text'\n : 'bg-status-warning-bg text-status-warning-text'\n }`}>\n {row.original.emailVerified\n ? t('customer_accounts.admin.verified', 'Yes')\n : t('customer_accounts.admin.unverified', 'No')}\n </span>\n ),\n },\n {\n accessorKey: 'isActive',\n header: t('customer_accounts.admin.columns.status', 'Status'),\n cell: ({ row }) => (\n <span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${\n row.original.isActive\n ? 'bg-status-success-bg text-status-success-text'\n : 'bg-status-error-bg text-status-error-text'\n }`}>\n {row.original.isActive\n ? t('customer_accounts.admin.active', 'Active')\n : t('customer_accounts.admin.inactive', 'Inactive')}\n </span>\n ),\n },\n {\n accessorKey: 'lastLoginAt',\n header: t('customer_accounts.admin.columns.lastLogin', 'Last Login'),\n cell: ({ row }) => formatDate(row.original.lastLoginAt, '-') || noValue,\n },\n {\n accessorKey: 'roles',\n header: t('customer_accounts.admin.columns.roles', 'Roles'),\n cell: ({ row }) => {\n const roles = row.original.roles\n if (!roles || !roles.length) return noValue\n return <span className=\"text-sm\">{roles.map((r) => r.name).join(', ')}</span>\n },\n },\n {\n accessorKey: 'createdAt',\n header: t('customer_accounts.admin.columns.createdAt', 'Created'),\n cell: ({ row }) => formatDate(row.original.createdAt, '-'),\n },\n ]\n }, [t])\n\n return (\n <Page>\n <PageBody className=\"space-y-4\">\n <div className=\"rounded-lg border border-status-info-border bg-status-info-bg p-4\">\n <div className=\"flex items-start justify-between gap-4\">\n <div>\n <h3 className=\"text-sm font-medium text-status-info-text\">\n {t('customer_accounts.admin.portalInfo.title', 'Customer Portal')}\n </h3>\n <p className=\"mt-1 text-sm text-status-info-text\">\n {t('customer_accounts.admin.portalInfo.description', 'Manage customer portal accounts. Customers can self-register, log in, and access orders, quotes, and invoices through the portal.')}\n </p>\n <p className=\"mt-1.5 text-xs text-status-info-text\">\n {t('customer_accounts.admin.portalInfo.url', 'Portal URL: {url}', {\n url: `${typeof window !== 'undefined' ? window.location.origin : ''}/[org-slug]/portal`,\n })}\n </p>\n <p className=\"mt-0.5 text-xs text-status-info-text\">\n {t('customer_accounts.admin.portalInfo.credentials', 'Demo credentials: alice.johnson@example.com / Password123!')}\n </p>\n </div>\n <div className=\"flex shrink-0 flex-col gap-2\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n asChild\n >\n <Link href=\"/backend/customer_accounts/settings\">\n <Settings className=\"size-4\" />\n {t('customer_accounts.admin.portalInfo.openConfiguration', 'Open Configuration')}\n </Link>\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n asChild\n >\n <a href={`${typeof window !== 'undefined' ? window.location.origin : ''}/portal`} target=\"_blank\" rel=\"noopener noreferrer\">\n <Globe className=\"size-4\" />\n {t('customer_accounts.admin.portalInfo.open', 'Open Portal')}\n </a>\n </Button>\n </div>\n </div>\n </div>\n <DataTable<UserRow>\n stickyActionsColumn\n title={t('customer_accounts.admin.title', 'Users')}\n actions={(\n <Button onClick={() => setCreateDialogOpen(true)}>\n {t('customer_accounts.admin.actions.createUser', 'Create User')}\n </Button>\n )}\n columns={columns}\n data={rows}\n searchValue={search}\n onSearchChange={(value) => { setSearch(value); setPage(1) }}\n searchPlaceholder={t('customer_accounts.admin.searchPlaceholder', 'Search by name or email...')}\n filters={filters}\n filterValues={filterValues}\n onFiltersApply={handleFiltersApply}\n onFiltersClear={handleFiltersClear}\n perspective={{ tableId: 'customer_accounts.admin.users' }}\n emptyState={(\n <ListEmptyState\n entityName={t('customer_accounts.admin.title', 'Users')}\n onCreate={() => setCreateDialogOpen(true)}\n createLabel={t('customer_accounts.admin.actions.createUser', 'Create User')}\n />\n )}\n onRowClick={(row) => router.push(`/backend/customer_accounts/users/${row.id}`)}\n rowActions={(row) => (\n <RowActions\n items={[\n {\n id: 'view',\n label: t('customer_accounts.admin.actions.view', 'View'),\n onSelect: () => { router.push(`/backend/customer_accounts/users/${row.id}`) },\n },\n {\n id: 'toggle-active',\n label: row.isActive\n ? t('customer_accounts.admin.actions.deactivate', 'Deactivate')\n : t('customer_accounts.admin.actions.activate', 'Activate'),\n onSelect: () => { void handleToggleActive(row) },\n },\n {\n id: 'delete',\n label: t('customer_accounts.admin.actions.delete', 'Delete'),\n destructive: true,\n onSelect: () => { void handleDelete(row) },\n },\n ]}\n />\n )}\n pagination={{ page, pageSize, total, totalPages, onPageChange: setPage }}\n isLoading={isLoading}\n />\n <CreateUserDialog\n open={createDialogOpen}\n onOpenChange={setCreateDialogOpen}\n roleOptions={roleOptions}\n onCreated={() => setReloadToken((token) => token + 1)}\n onRunMutation={runMutationWithContext}\n />\n </PageBody>\n {ConfirmDialogElement}\n </Page>\n )\n}\n"],
|
|
5
|
-
"mappings": ";AAgJU,cAGA,YAHA;AA9IV,YAAY,WAAW;AACvB,OAAO,UAAU;AACjB,SAAS,iBAAiB;AAC1B,SAAS,OAAO,gBAAgB;AAChC,SAAS,MAAM,gBAAgB;AAC/B,SAAS,iBAAiB;AAE1B,SAAS,kBAAkB;AAC3B,SAAS,cAAc;AACvB,SAAS,aAAa;AACtB,SAAS,kBAAkB;AAC3B,SAAS,qBAAqB;AAC9B,SAAS,QAAQ,eAAe,cAAc,mBAAmB;AACjE,SAAS,SAAS,sBAAsB,mCAAmC;AAC3E,SAAS,iCAAiC;AAC1C,SAAS,aAAa;AACtB,SAAS,YAAY;AACrB,SAAS,wBAAwB;AACjC,SAAS,0BAA0B;AACnC,SAAS,sBAAsB;AAuB/B,SAAS,WAAW,OAAkC,UAA0B;AAC9E,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,OAAO,IAAI,KAAK,KAAK;AAC3B,MAAI,OAAO,MAAM,KAAK,QAAQ,CAAC,EAAG,QAAO;AACzC,SAAO,KAAK,mBAAmB;AACjC;AAEA,eAAe,yBAAuF;AACpG,MAAI;AACF,UAAM,OAAO,MAAM;AAAA,MACjB;AAAA,IACF;AACA,QAAI,CAAC,KAAK,GAAI,QAAO,CAAC;AACtB,UAAM,QAAQ,MAAM,QAAQ,KAAK,QAAQ,KAAK,IAAI,KAAK,OAAQ,QAAQ,CAAC;AACxE,WAAO,MACJ,OAAO,CAAC,SAAS,OAAO,MAAM,OAAO,YAAY,OAAO,MAAM,SAAS,QAAQ,EAC/E,IAAI,CAAC,UAAU,EAAE,OAAO,KAAK,IAAI,OAAO,KAAK,MAAM,IAAI,KAAK,GAAG,EAAE;AAAA,EACtE,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAEA,SAAS,iBAAiB;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMG;AACD,QAAM,IAAI,KAAK;AACf,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAS,EAAE;AAC3C,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAAS,EAAE;AACvD,QAAM,CAAC,UAAU,WAAW,IAAI,MAAM,SAAS,EAAE;AACjD,QAAM,CAAC,iBAAiB,kBAAkB,IAAI,MAAM,SAAmB,CAAC,CAAC;AACzE,QAAM,CAAC,cAAc,eAAe,IAAI,MAAM,SAAS,KAAK;AAE5D,QAAM,YAAY,MAAM,YAAY,MAAM;AACxC,aAAS,EAAE;AACX,mBAAe,EAAE;AACjB,gBAAY,EAAE;AACd,uBAAmB,CAAC,CAAC;AAAA,EACvB,GAAG,CAAC,CAAC;AAEL,QAAM,eAAe,MAAM,YAAY,OAAO,UAA2B;AACvE,UAAM,eAAe;AACrB,QAAI,CAAC,MAAM,KAAK,KAAK,CAAC,YAAY,KAAK,KAAK,CAAC,SAAS,KAAK,GAAG;AAC5D,YAAM,EAAE,qDAAqD,wCAAwC,GAAG,OAAO;AAC/G;AAAA,IACF;AACA,oBAAgB,IAAI;AACpB,QAAI;AACF,YAAM,cAAc,YAAY;AAC9B,cAAM,OAAO,MAAM;AAAA,UACjB;AAAA,UACA;AAAA,YACE,QAAQ;AAAA,YACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,YAC9C,MAAM,KAAK,UAAU;AAAA,cACnB,OAAO,MAAM,KAAK;AAAA,cAClB,aAAa,YAAY,KAAK;AAAA,cAC9B;AAAA,cACA,SAAS,gBAAgB,SAAS,IAAI,kBAAkB;AAAA,YAC1D,CAAC;AAAA,UACH;AAAA,QACF;AACA,YAAI,CAAC,KAAK,IAAI;AACZ,gBAAM,KAAK,QAAQ,SAAS,EAAE,iDAAiD,uBAAuB,GAAG,OAAO;AAChH;AAAA,QACF;AACA,cAAM,EAAE,oDAAoD,cAAc,GAAG,SAAS;AACtF,kBAAU;AACV,qBAAa,KAAK;AAClB,kBAAU;AAAA,MACZ,CAAC;AAAA,IACH,SAAS,KAAK;AACZ,YAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,EAAE,iDAAiD,uBAAuB;AAC/H,YAAM,SAAS,OAAO;AAAA,IACxB,UAAE;AACA,sBAAgB,KAAK;AAAA,IACvB;AAAA,EACF,GAAG,CAAC,aAAa,OAAO,WAAW,cAAc,eAAe,UAAU,WAAW,iBAAiB,CAAC,CAAC;AAExG,QAAM,gBAAgB,MAAM,YAAY,CAAC,UAA+B;AACtE,SAAK,MAAM,WAAW,MAAM,YAAY,MAAM,QAAQ,SAAS;AAC7D,YAAM,eAAe;AACrB,YAAM,OAAQ,MAAM,OAAuB,QAAQ,MAAM;AACzD,UAAI,KAAM,MAAK,cAAc;AAAA,IAC/B;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,SACE,oBAAC,UAAO,MAAY,cAAc,CAAC,SAAS;AAAE,QAAI,CAAC,KAAM,WAAU;AAAG,iBAAa,IAAI;AAAA,EAAE,GACvF,+BAAC,iBAAc,WAAU,eACvB;AAAA,wBAAC,gBACC,8BAAC,eAAa,YAAE,4CAA4C,sBAAsB,GAAE,GACtF;AAAA,IACA,qBAAC,UAAK,UAAU,CAAC,UAAU;AAAE,WAAK,aAAa,KAAK;AAAA,IAAE,GAAG,WAAW,eAAe,WAAU,aAC3F;AAAA,2BAAC,SAAI,WAAU,aACb;AAAA,4BAAC,WAAM,WAAU,uBAAsB,SAAQ,gBAC5C,YAAE,mDAAmD,OAAO,GAC/D;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,IAAG;AAAA,YACH,UAAQ;AAAA,YACR,OAAO;AAAA,YACP,UAAU,CAAC,UAAU,SAAS,MAAM,OAAO,KAAK;AAAA,YAChD,aAAa,EAAE,8DAA8D,kBAAkB;AAAA;AAAA,QACjG;AAAA,SACF;AAAA,MACA,qBAAC,SAAI,WAAU,aACb;AAAA,4BAAC,WAAM,WAAU,uBAAsB,SAAQ,eAC5C,YAAE,yDAAyD,cAAc,GAC5E;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,IAAG;AAAA,YACH,MAAK;AAAA,YACL,UAAQ;AAAA,YACR,OAAO;AAAA,YACP,UAAU,CAAC,UAAU,eAAe,MAAM,OAAO,KAAK;AAAA,YACtD,aAAa,EAAE,oEAAoE,UAAU;AAAA;AAAA,QAC/F;AAAA,SACF;AAAA,MACA,qBAAC,SAAI,WAAU,aACb;AAAA,4BAAC,WAAM,WAAU,uBAAsB,SAAQ,mBAC5C,YAAE,sDAAsD,UAAU,GACrE;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,IAAG;AAAA,YACH,UAAQ;AAAA,YACR,WAAW;AAAA,YACX,OAAO;AAAA,YACP,UAAU,CAAC,UAAU,YAAY,MAAM,OAAO,KAAK;AAAA,YACnD,aAAa,EAAE,iEAAiE,mBAAmB;AAAA,YACnG,cAAa;AAAA;AAAA,QACf;AAAA,SACF;AAAA,MACC,YAAY,SAAS,KACpB,qBAAC,SAAI,WAAU,aACb;AAAA,4BAAC,OAAE,WAAU,uBAAuB,YAAE,mDAAmD,OAAO,GAAE;AAAA,QAClG,oBAAC,SAAI,WAAU,wBACZ,sBAAY,IAAI,CAAC,SAAS;AACzB,gBAAM,aAAa,gBAAgB,SAAS,KAAK,EAAE;AACnD,iBACE;AAAA,YAAC;AAAA;AAAA,cAEC,MAAK;AAAA,cACL,SAAS,MAAM;AAAA,gBAAmB,CAAC,SACjC,KAAK,SAAS,KAAK,EAAE,IAAI,KAAK,OAAO,CAAC,QAAQ,QAAQ,KAAK,EAAE,IAAI,CAAC,GAAG,MAAM,KAAK,EAAE;AAAA,cACpF;AAAA,cACA,WAAW,uEACT,aACI,8CACA,kEACN;AAAA,cAEC,eAAK;AAAA;AAAA,YAXD,KAAK;AAAA,UAYZ;AAAA,QAEJ,CAAC,GACH;AAAA,SACF;AAAA,MAEF,qBAAC,SAAI,WAAU,+BACb;AAAA,4BAAC,UAAO,MAAK,UAAS,SAAQ,WAAU,SAAS,MAAM;AAAE,oBAAU;AAAG,uBAAa,KAAK;AAAA,QAAE,GACvF,YAAE,qDAAqD,QAAQ,GAClE;AAAA,QACA,oBAAC,UAAO,MAAK,UAAS,UAAU,cAC7B,yBACG,EAAE,uDAAuD,aAAa,IACtE,EAAE,qDAAqD,aAAa,GAC1E;AAAA,SACF;AAAA,OACF;AAAA,KACF,GACF;AAEJ;AAEe,SAAR,uBAAwC;AAC7C,QAAM,EAAE,SAAS,qBAAqB,IAAI,iBAAiB;AAC3D,QAAM,IAAI,KAAK;AACf,QAAM,SAAS,UAAU;AACzB,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAAoB,CAAC,CAAC;AACpD,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAAS,CAAC;AACxC,QAAM,CAAC,QAAQ,IAAI,MAAM,SAAS,EAAE;AACpC,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAS,CAAC;AAC1C,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAAS,CAAC;AACpD,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAAS,EAAE;AAC7C,QAAM,CAAC,cAAc,eAAe,IAAI,MAAM,SAAuB,CAAC,CAAC;AACvE,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAS,IAAI;AACrD,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAAS,CAAC;AACtD,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAA8D,CAAC,CAAC;AAC5G,QAAM,CAAC,kBAAkB,mBAAmB,IAAI,MAAM,SAAS,KAAK;AAEpE,QAAM,EAAE,aAAa,kBAAkB,IAAI,mBAGxC;AAAA,IACD,WAAW;AAAA,EACb,CAAC;AAED,QAAM,yBAAyB,MAAM;AAAA,IACnC,OAAW,WAA6B,oBAA0D;AAChG,aAAO,YAAY;AAAA,QACjB;AAAA,QACA;AAAA,QACA,SAAS,EAAE,YAAY,yBAAyB;AAAA,MAClD,CAAC;AAAA,IACH;AAAA,IACA,CAAC,WAAW;AAAA,EACd;AAEA,QAAM,UAAU,MAAM;AACpB,QAAI,YAAY;AAChB,2BAAuB,EAAE,KAAK,CAAC,SAAS;AACtC,UAAI,CAAC,UAAW,gBAAe,IAAI;AAAA,IACrC,CAAC;AACD,WAAO,MAAM;AAAE,kBAAY;AAAA,IAAK;AAAA,EAClC,GAAG,CAAC,CAAC;AAEL,QAAM,UAAU,MAAM,QAAqB,MAAM;AAAA,IAC/C;AAAA,MACE,IAAI;AAAA,MACJ,OAAO,EAAE,0CAA0C,QAAQ;AAAA,MAC3D,MAAM;AAAA,MACN,SAAS;AAAA,QACP,EAAE,OAAO,UAAU,OAAO,EAAE,0CAA0C,QAAQ,EAAE;AAAA,QAChF,EAAE,OAAO,YAAY,OAAO,EAAE,4CAA4C,UAAU,EAAE;AAAA,MACxF;AAAA,IACF;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,OAAO,EAAE,wCAAwC,MAAM;AAAA,MACvD,MAAM;AAAA,MACN,SAAS;AAAA,IACX;AAAA,EACF,GAAG,CAAC,aAAa,CAAC,CAAC;AAEnB,QAAM,cAAc,MAAM,QAAQ,MAAM;AACtC,UAAM,SAAS,IAAI,gBAAgB;AACnC,WAAO,IAAI,QAAQ,OAAO,IAAI,CAAC;AAC/B,WAAO,IAAI,YAAY,OAAO,QAAQ,CAAC;AACvC,QAAI,OAAO,KAAK,EAAG,QAAO,IAAI,UAAU,OAAO,KAAK,CAAC;AACrD,UAAM,SAAS,aAAa;AAC5B,QAAI,OAAO,WAAW,YAAY,OAAO,KAAK,EAAG,QAAO,IAAI,UAAU,MAAM;AAC5E,UAAM,SAAS,aAAa;AAC5B,QAAI,OAAO,WAAW,YAAY,OAAO,KAAK,EAAG,QAAO,IAAI,UAAU,MAAM;AAC5E,WAAO,OAAO,SAAS;AAAA,EACzB,GAAG,CAAC,cAAc,MAAM,UAAU,MAAM,CAAC;AAEzC,QAAM,UAAU,MAAM;AACpB,QAAI,YAAY;AAChB,mBAAe,OAAO;AACpB,mBAAa,IAAI;AACjB,UAAI;AACF,cAAM,WAA0B,EAAE,OAAO,CAAC,GAAG,OAAO,GAAG,YAAY,EAAE;AACrE,cAAM,UAAU,MAAM;AAAA,UACpB,sCAAsC,WAAW;AAAA,UACjD;AAAA,UACA,EAAE,cAAc,EAAE,2CAA2C,+BAA+B,GAAG,SAAS;AAAA,QAC1G;AACA,YAAI,UAAW;AACf,cAAM,QAAQ,MAAM,QAAQ,SAAS,KAAK,IAAI,QAAQ,QAAQ,CAAC;AAC/D,gBAAQ,KAAK;AACb,iBAAS,OAAO,SAAS,UAAU,WAAW,QAAQ,QAAQ,MAAM,MAAM;AAC1E,sBAAc,OAAO,SAAS,eAAe,WAAW,QAAQ,aAAa,CAAC;AAAA,MAChF,SAAS,KAAK;AACZ,YAAI,CAAC,WAAW;AACd,gBAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,EAAE,2CAA2C,+BAA+B;AACjI,gBAAM,SAAS,OAAO;AAAA,QACxB;AAAA,MACF,UAAE;AACA,YAAI,CAAC,UAAW,cAAa,KAAK;AAAA,MACpC;AAAA,IACF;AACA,SAAK;AACL,WAAO,MAAM;AAAE,kBAAY;AAAA,IAAK;AAAA,EAClC,GAAG,CAAC,aAAa,aAAa,CAAC,CAAC;AAEhC,QAAM,qBAAqB,MAAM,YAAY,OAAO,SAAkB;AACpE,UAAM,aAAa,CAAC,KAAK;AACzB,UAAM,cAAc,aAChB,EAAE,4CAA4C,UAAU,IACxD,EAAE,8CAA8C,YAAY;AAChE,UAAM,YAAY,MAAM,QAAQ;AAAA,MAC9B,OAAO,EAAE,gDAAgD,+BAA+B;AAAA,QACtF,QAAQ;AAAA,QACR,MAAM,KAAK,eAAe,KAAK;AAAA,MACjC,CAAC;AAAA,MACD,SAAS,aAAa,YAAY;AAAA,IACpC,CAAC;AACD,QAAI,CAAC,UAAW;AAChB,QAAI;AACF,YAAM,uBAAuB,YAAY;AACvC,cAAM,OAAO,MAAM;AAAA,UACjB,0BAA0B,KAAK,SAAS;AAAA,UACxC,MAAM;AAAA,YACJ,sCAAsC,mBAAmB,KAAK,EAAE,CAAC;AAAA,YACjE;AAAA,cACE,QAAQ;AAAA,cACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,cAC9C,MAAM,KAAK,UAAU,EAAE,UAAU,WAAW,CAAC;AAAA,YAC/C;AAAA,UACF;AAAA,QACF;AACA,YAAI,CAAC,KAAK,IAAI;AACZ,gBAAM,EAAE,8CAA8C,8BAA8B,GAAG,OAAO;AAC9F;AAAA,QACF;AACA;AAAA,UACE,aACI,EAAE,2CAA2C,gBAAgB,IAC7D,EAAE,6CAA6C,kBAAkB;AAAA,UACrE;AAAA,QACF;AACA,uBAAe,CAAC,UAAU,QAAQ,CAAC;AAAA,MACrC,GAAG,EAAE,QAAQ,KAAK,IAAI,UAAU,WAAW,CAAC;AAAA,IAC9C,SAAS,KAAK;AACZ,YAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,EAAE,8CAA8C,8BAA8B;AACnI,YAAM,SAAS,OAAO;AAAA,IACxB;AAAA,EACF,GAAG,CAAC,SAAS,wBAAwB,CAAC,CAAC;AAEvC,QAAM,eAAe,MAAM,YAAY,OAAO,SAAkB;AAC9D,UAAM,YAAY,MAAM,QAAQ;AAAA,MAC9B,OAAO,EAAE,0CAA0C,2BAA2B;AAAA,QAC5E,MAAM,KAAK,eAAe,KAAK;AAAA,MACjC,CAAC;AAAA,MACD,SAAS;AAAA,IACX,CAAC;AACD,QAAI,CAAC,UAAW;AAChB,QAAI;AACF,YAAM,uBAAuB,YAAY;AACvC,cAAM,OAAO,MAAM;AAAA,UACjB,0BAA0B,KAAK,SAAS;AAAA,UACxC,MAAM;AAAA,YACJ,sCAAsC,mBAAmB,KAAK,EAAE,CAAC;AAAA,YACjE,EAAE,QAAQ,SAAS;AAAA,UACrB;AAAA,QACF;AACA,YAAI,CAAC,KAAK,IAAI;AACZ,gBAAM,EAAE,wCAAwC,uBAAuB,GAAG,OAAO;AACjF;AAAA,QACF;AACA,cAAM,EAAE,yCAAyC,cAAc,GAAG,SAAS;AAC3E,uBAAe,CAAC,UAAU,QAAQ,CAAC;AAAA,MACrC,GAAG,EAAE,IAAI,KAAK,GAAG,CAAC;AAAA,IACpB,SAAS,KAAK;AACZ,YAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,EAAE,wCAAwC,uBAAuB;AACtH,YAAM,SAAS,OAAO;AAAA,IACxB;AAAA,EACF,GAAG,CAAC,SAAS,wBAAwB,CAAC,CAAC;AAEvC,QAAM,qBAAqB,MAAM,YAAY,CAAC,WAAyB;AACrE,oBAAgB,MAAM;AACtB,YAAQ,CAAC;AAAA,EACX,GAAG,CAAC,CAAC;AAEL,QAAM,qBAAqB,MAAM,YAAY,MAAM;AACjD,oBAAgB,CAAC,CAAC;AAClB,YAAQ,CAAC;AAAA,EACX,GAAG,CAAC,CAAC;AAEL,QAAM,UAAU,MAAM,QAA8B,MAAM;AACxD,UAAM,UAAU,oBAAC,UAAK,WAAU,iCAAgC,eAAC;AACjE,WAAO;AAAA,MACL;AAAA,QACE,aAAa;AAAA,QACb,QAAQ,EAAE,+CAA+C,MAAM;AAAA,QAC/D,MAAM,CAAC,EAAE,IAAI,MACX;AAAA,UAAC;AAAA;AAAA,YACC,MAAM,oCAAoC,IAAI,SAAS,EAAE;AAAA,YACzD,WAAU;AAAA,YAET,cAAI,SAAS,eAAe,IAAI,SAAS;AAAA;AAAA,QAC5C;AAAA,MAEJ;AAAA,MACA;AAAA,QACE,aAAa;AAAA,QACb,QAAQ,EAAE,yCAAyC,OAAO;AAAA,MAC5D;AAAA,MACA;AAAA,QACE,aAAa;AAAA,QACb,QAAQ,EAAE,iDAAiD,UAAU;AAAA,QACrE,MAAM,CAAC,EAAE,IAAI,MACX,oBAAC,UAAK,WAAW,yEACf,IAAI,SAAS,gBACT,kDACA,+CACN,IACG,cAAI,SAAS,gBACV,EAAE,oCAAoC,KAAK,IAC3C,EAAE,sCAAsC,IAAI,GAClD;AAAA,MAEJ;AAAA,MACA;AAAA,QACE,aAAa;AAAA,QACb,QAAQ,EAAE,0CAA0C,QAAQ;AAAA,QAC5D,MAAM,CAAC,EAAE,IAAI,MACX,oBAAC,UAAK,WAAW,yEACf,IAAI,SAAS,WACT,kDACA,2CACN,IACG,cAAI,SAAS,WACV,EAAE,kCAAkC,QAAQ,IAC5C,EAAE,oCAAoC,UAAU,GACtD;AAAA,MAEJ;AAAA,MACA;AAAA,QACE,aAAa;AAAA,QACb,QAAQ,EAAE,6CAA6C,YAAY;AAAA,QACnE,MAAM,CAAC,EAAE,IAAI,MAAM,WAAW,IAAI,SAAS,aAAa,GAAG,KAAK;AAAA,MAClE;AAAA,MACA;AAAA,QACE,aAAa;AAAA,QACb,QAAQ,EAAE,yCAAyC,OAAO;AAAA,QAC1D,MAAM,CAAC,EAAE,IAAI,MAAM;AACjB,gBAAM,QAAQ,IAAI,SAAS;AAC3B,cAAI,CAAC,SAAS,CAAC,MAAM,OAAQ,QAAO;AACpC,iBAAO,oBAAC,UAAK,WAAU,WAAW,gBAAM,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,IAAI,GAAE;AAAA,QACxE;AAAA,MACF;AAAA,MACA;AAAA,QACE,aAAa;AAAA,QACb,QAAQ,EAAE,6CAA6C,SAAS;AAAA,QAChE,MAAM,CAAC,EAAE,IAAI,MAAM,WAAW,IAAI,SAAS,WAAW,GAAG;AAAA,MAC3D;AAAA,IACF;AAAA,EACF,GAAG,CAAC,CAAC,CAAC;AAEN,SACE,qBAAC,QACC;AAAA,yBAAC,YAAS,WAAU,aAClB;AAAA,0BAAC,SAAI,WAAU,qEACb,+BAAC,SAAI,WAAU,0CACb;AAAA,6BAAC,SACC;AAAA,8BAAC,QAAG,WAAU,6CACX,YAAE,4CAA4C,iBAAiB,GAClE;AAAA,UACA,oBAAC,OAAE,WAAU,sCACV,YAAE,kDAAkD,mIAAmI,GAC1L;AAAA,UACA,oBAAC,OAAE,WAAU,wCACV,YAAE,0CAA0C,qBAAqB;AAAA,YAChE,KAAK,GAAG,OAAO,WAAW,cAAc,OAAO,SAAS,SAAS,EAAE;AAAA,UACrE,CAAC,GACH;AAAA,UACA,oBAAC,OAAE,WAAU,wCACV,YAAE,kDAAkD,4DAA4D,GACnH;AAAA,WACF;AAAA,QACA,qBAAC,SAAI,WAAU,gCACb;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAQ;AAAA,cACR,MAAK;AAAA,cACL,SAAO;AAAA,cAEP,+BAAC,QAAK,MAAK,uCACT;AAAA,oCAAC,YAAS,WAAU,UAAS;AAAA,gBAC5B,EAAE,wDAAwD,oBAAoB;AAAA,iBACjF;AAAA;AAAA,UACF;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAQ;AAAA,cACR,MAAK;AAAA,cACL,SAAO;AAAA,cAEP,+BAAC,OAAE,MAAM,GAAG,OAAO,WAAW,cAAc,OAAO,SAAS,SAAS,EAAE,WAAW,QAAO,UAAS,KAAI,uBACpG;AAAA,oCAAC,SAAM,WAAU,UAAS;AAAA,gBACzB,EAAE,2CAA2C,aAAa;AAAA,iBAC7D;AAAA;AAAA,UACF;AAAA,WACF;AAAA,SACF,GACF;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,qBAAmB;AAAA,UACnB,OAAO,EAAE,iCAAiC,OAAO;AAAA,UACjD,SACE,oBAAC,UAAO,SAAS,MAAM,oBAAoB,IAAI,GAC5C,YAAE,8CAA8C,aAAa,GAChE;AAAA,UAEF;AAAA,UACA,MAAM;AAAA,UACN,aAAa;AAAA,UACb,gBAAgB,CAAC,UAAU;AAAE,sBAAU,KAAK;AAAG,oBAAQ,CAAC;AAAA,UAAE;AAAA,UAC1D,mBAAmB,EAAE,6CAA6C,4BAA4B;AAAA,UAC9F;AAAA,UACA;AAAA,UACA,gBAAgB;AAAA,UAChB,gBAAgB;AAAA,UAChB,aAAa,EAAE,SAAS,gCAAgC;AAAA,UACxD,YACE;AAAA,YAAC;AAAA;AAAA,cACC,YAAY,EAAE,iCAAiC,OAAO;AAAA,cACtD,UAAU,MAAM,oBAAoB,IAAI;AAAA,cACxC,aAAa,EAAE,8CAA8C,aAAa;AAAA;AAAA,UAC5E;AAAA,UAEF,YAAY,CAAC,QAAQ,OAAO,KAAK,oCAAoC,IAAI,EAAE,EAAE;AAAA,UAC7E,YAAY,CAAC,QACX;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,gBACL;AAAA,kBACE,IAAI;AAAA,kBACJ,OAAO,EAAE,wCAAwC,MAAM;AAAA,kBACvD,UAAU,MAAM;AAAE,2BAAO,KAAK,oCAAoC,IAAI,EAAE,EAAE;AAAA,kBAAE;AAAA,gBAC9E;AAAA,gBACA;AAAA,kBACE,IAAI;AAAA,kBACJ,OAAO,IAAI,WACP,EAAE,8CAA8C,YAAY,IAC5D,EAAE,4CAA4C,UAAU;AAAA,kBAC5D,UAAU,MAAM;AAAE,yBAAK,mBAAmB,GAAG;AAAA,kBAAE;AAAA,gBACjD;AAAA,gBACA;AAAA,kBACE,IAAI;AAAA,kBACJ,OAAO,EAAE,0CAA0C,QAAQ;AAAA,kBAC3D,aAAa;AAAA,kBACb,UAAU,MAAM;AAAE,yBAAK,aAAa,GAAG;AAAA,kBAAE;AAAA,gBAC3C;AAAA,cACF;AAAA;AAAA,UACF;AAAA,UAEF,YAAY,EAAE,MAAM,UAAU,OAAO,YAAY,cAAc,QAAQ;AAAA,UACvE;AAAA;AAAA,MACF;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,MAAM;AAAA,UACN,cAAc;AAAA,UACd;AAAA,UACA,WAAW,MAAM,eAAe,CAAC,UAAU,QAAQ,CAAC;AAAA,UACpD,eAAe;AAAA;AAAA,MACjB;AAAA,OACF;AAAA,IACC;AAAA,KACH;AAEJ;",
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport { Globe, Settings } from 'lucide-react'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { EmailInput } from '@open-mercato/ui/primitives/email-input'\nimport { PasswordInput } from '@open-mercato/ui/primitives/password-input'\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'\nimport { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { ListEmptyState } from '@open-mercato/ui/backend/filters/ListEmptyState'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\n\ntype UserRow = {\n id: string\n displayName: string\n email: string\n emailVerified: boolean\n isActive: boolean\n lastLoginAt: string | null\n roles: Array<{ id: string; name: string; slug: string }>\n createdAt: string\n updatedAt?: string | null\n personEntityId: string | null\n customerEntityId: string | null\n}\n\ntype UsersResponse = {\n items?: UserRow[]\n total?: number\n totalPages?: number\n}\n\nfunction formatDate(value: string | null | undefined, fallback: string): string {\n if (!value) return fallback\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return fallback\n return date.toLocaleDateString()\n}\n\nasync function fetchRoleFilterOptions(): Promise<Array<{ value: string; label: string; id: string }>> {\n try {\n const call = await apiCall<{ items?: Array<{ id: string; name: string }> }>(\n '/api/customer_accounts/admin/roles?pageSize=100',\n )\n if (!call.ok) return []\n const items = Array.isArray(call.result?.items) ? call.result!.items : []\n return items\n .filter((item) => typeof item?.id === 'string' && typeof item?.name === 'string')\n .map((item) => ({ value: item.id, label: item.name, id: item.id }))\n } catch {\n return []\n }\n}\n\nfunction CreateUserDialog({\n open,\n onOpenChange,\n roleOptions,\n onCreated,\n onRunMutation,\n}: {\n open: boolean\n onOpenChange: (next: boolean) => void\n roleOptions: Array<{ id: string; label: string }>\n onCreated: () => void\n onRunMutation: <T>(operation: () => Promise<T>) => Promise<T>\n}) {\n const t = useT()\n const [email, setEmail] = React.useState('')\n const [displayName, setDisplayName] = React.useState('')\n const [password, setPassword] = React.useState('')\n const [selectedRoleIds, setSelectedRoleIds] = React.useState<string[]>([])\n const [isSubmitting, setIsSubmitting] = React.useState(false)\n\n const resetForm = React.useCallback(() => {\n setEmail('')\n setDisplayName('')\n setPassword('')\n setSelectedRoleIds([])\n }, [])\n\n const handleSubmit = React.useCallback(async (event: React.FormEvent) => {\n event.preventDefault()\n if (!email.trim() || !displayName.trim() || !password.trim()) {\n flash(t('customer_accounts.admin.createUser.error.required', 'Email, name, and password are required'), 'error')\n return\n }\n setIsSubmitting(true)\n try {\n await onRunMutation(async () => {\n const call = await apiCall<{ ok: boolean; error?: string }>(\n '/api/customer_accounts/admin/users',\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n email: email.trim(),\n displayName: displayName.trim(),\n password,\n roleIds: selectedRoleIds.length > 0 ? selectedRoleIds : undefined,\n }),\n },\n )\n if (!call.ok) {\n flash(call.result?.error || t('customer_accounts.admin.createUser.error.save', 'Failed to create user'), 'error')\n return\n }\n flash(t('customer_accounts.admin.createUser.flash.created', 'User created'), 'success')\n resetForm()\n onOpenChange(false)\n onCreated()\n })\n } catch (err) {\n const message = err instanceof Error ? err.message : t('customer_accounts.admin.createUser.error.save', 'Failed to create user')\n flash(message, 'error')\n } finally {\n setIsSubmitting(false)\n }\n }, [displayName, email, onCreated, onOpenChange, onRunMutation, password, resetForm, selectedRoleIds, t])\n\n const handleKeyDown = React.useCallback((event: React.KeyboardEvent) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n const form = (event.target as HTMLElement).closest('form')\n if (form) form.requestSubmit()\n }\n }, [])\n\n return (\n <Dialog open={open} onOpenChange={(next) => { if (!next) resetForm(); onOpenChange(next) }}>\n <DialogContent className=\"sm:max-w-lg\">\n <DialogHeader>\n <DialogTitle>{t('customer_accounts.admin.createUser.title', 'Create Customer User')}</DialogTitle>\n </DialogHeader>\n <form onSubmit={(event) => { void handleSubmit(event) }} onKeyDown={handleKeyDown} className=\"space-y-4\">\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\" htmlFor=\"create-email\">\n {t('customer_accounts.admin.createUser.fields.email', 'Email')}\n </label>\n <EmailInput\n id=\"create-email\"\n required\n value={email}\n onChange={(event) => setEmail(event.target.value)}\n placeholder={t('customer_accounts.admin.createUser.fields.emailPlaceholder', 'user@example.com')}\n />\n </div>\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\" htmlFor=\"create-name\">\n {t('customer_accounts.admin.createUser.fields.displayName', 'Display Name')}\n </label>\n <Input\n id=\"create-name\"\n type=\"text\"\n required\n value={displayName}\n onChange={(event) => setDisplayName(event.target.value)}\n placeholder={t('customer_accounts.admin.createUser.fields.displayNamePlaceholder', 'John Doe')}\n />\n </div>\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium\" htmlFor=\"create-password\">\n {t('customer_accounts.admin.createUser.fields.password', 'Password')}\n </label>\n <PasswordInput\n id=\"create-password\"\n required\n minLength={8}\n value={password}\n onChange={(event) => setPassword(event.target.value)}\n placeholder={t('customer_accounts.admin.createUser.fields.passwordPlaceholder', 'Min. 8 characters')}\n autoComplete=\"new-password\"\n />\n </div>\n {roleOptions.length > 0 && (\n <div className=\"space-y-2\">\n <p className=\"text-sm font-medium\">{t('customer_accounts.admin.createUser.fields.roles', 'Roles')}</p>\n <div className=\"flex flex-wrap gap-2\">\n {roleOptions.map((role) => {\n const isSelected = selectedRoleIds.includes(role.id)\n return (\n <Button\n key={role.id}\n type=\"button\"\n size=\"2xs\"\n variant={isSelected ? 'secondary' : 'outline'}\n aria-pressed={isSelected}\n onClick={() => setSelectedRoleIds((prev) =>\n prev.includes(role.id) ? prev.filter((rid) => rid !== role.id) : [...prev, role.id],\n )}\n className=\"rounded-full\"\n >\n {role.label}\n </Button>\n )\n })}\n </div>\n </div>\n )}\n <div className=\"flex justify-end gap-2 pt-2\">\n <Button type=\"button\" variant=\"outline\" onClick={() => { resetForm(); onOpenChange(false) }}>\n {t('customer_accounts.admin.createUser.actions.cancel', 'Cancel')}\n </Button>\n <Button type=\"submit\" disabled={isSubmitting}>\n {isSubmitting\n ? t('customer_accounts.admin.createUser.actions.creating', 'Creating...')\n : t('customer_accounts.admin.createUser.actions.create', 'Create User')}\n </Button>\n </div>\n </form>\n </DialogContent>\n </Dialog>\n )\n}\n\nexport default function CustomerAccountsPage() {\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const t = useT()\n const router = useRouter()\n const [rows, setRows] = React.useState<UserRow[]>([])\n const [page, setPage] = React.useState(1)\n const [pageSize] = React.useState(50)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [search, setSearch] = React.useState('')\n const [filterValues, setFilterValues] = React.useState<FilterValues>({})\n const [isLoading, setIsLoading] = React.useState(true)\n const [reloadToken, setReloadToken] = React.useState(0)\n const [roleOptions, setRoleOptions] = React.useState<Array<{ value: string; label: string; id: string }>>([])\n const [createDialogOpen, setCreateDialogOpen] = React.useState(false)\n\n const { runMutation, retryLastMutation } = useGuardedMutation<{\n entityType: string\n entityId?: string\n }>({\n contextId: 'customer_accounts:users-list',\n })\n\n const runMutationWithContext = React.useCallback(\n async <T,>(operation: () => Promise<T>, mutationPayload?: Record<string, unknown>): Promise<T> => {\n return runMutation({\n operation,\n mutationPayload,\n context: { entityType: 'customer_accounts:user' },\n })\n },\n [runMutation],\n )\n\n React.useEffect(() => {\n let cancelled = false\n fetchRoleFilterOptions().then((opts) => {\n if (!cancelled) setRoleOptions(opts)\n })\n return () => { cancelled = true }\n }, [])\n\n const filters = React.useMemo<FilterDef[]>(() => [\n {\n id: 'status',\n label: t('customer_accounts.admin.filters.status', 'Status'),\n type: 'select',\n options: [\n { value: 'active', label: t('customer_accounts.admin.filters.active', 'Active') },\n { value: 'inactive', label: t('customer_accounts.admin.filters.inactive', 'Inactive') },\n ],\n },\n {\n id: 'roleId',\n label: t('customer_accounts.admin.filters.role', 'Role'),\n type: 'select',\n options: roleOptions,\n },\n ], [roleOptions, t])\n\n const queryParams = React.useMemo(() => {\n const params = new URLSearchParams()\n params.set('page', String(page))\n params.set('pageSize', String(pageSize))\n if (search.trim()) params.set('search', search.trim())\n const status = filterValues.status\n if (typeof status === 'string' && status.trim()) params.set('status', status)\n const roleId = filterValues.roleId\n if (typeof roleId === 'string' && roleId.trim()) params.set('roleId', roleId)\n return params.toString()\n }, [filterValues, page, pageSize, search])\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setIsLoading(true)\n try {\n const fallback: UsersResponse = { items: [], total: 0, totalPages: 1 }\n const payload = await readApiResultOrThrow<UsersResponse>(\n `/api/customer_accounts/admin/users?${queryParams}`,\n undefined,\n { errorMessage: t('customer_accounts.admin.error.loadUsers', 'Failed to load customer users'), fallback },\n )\n if (cancelled) return\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 : 1)\n } catch (err) {\n if (!cancelled) {\n const message = err instanceof Error ? err.message : t('customer_accounts.admin.error.loadUsers', 'Failed to load customer users')\n flash(message, 'error')\n }\n } finally {\n if (!cancelled) setIsLoading(false)\n }\n }\n load()\n return () => { cancelled = true }\n }, [queryParams, reloadToken, t])\n\n const handleToggleActive = React.useCallback(async (user: UserRow) => {\n const nextActive = !user.isActive\n const actionLabel = nextActive\n ? t('customer_accounts.admin.actions.activate', 'Activate')\n : t('customer_accounts.admin.actions.deactivate', 'Deactivate')\n const confirmed = await confirm({\n title: t('customer_accounts.admin.confirm.toggleActive', '{{action}} user \"{{name}}\"?', {\n action: actionLabel,\n name: user.displayName || user.email,\n }),\n variant: nextActive ? 'default' : 'destructive',\n })\n if (!confirmed) return\n try {\n await runMutationWithContext(async () => {\n const call = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(user.updatedAt),\n () => apiCall(\n `/api/customer_accounts/admin/users/${encodeURIComponent(user.id)}`,\n {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ isActive: nextActive }),\n },\n ),\n )\n if (!call.ok) {\n flash(t('customer_accounts.admin.error.toggleActive', 'Failed to update user status'), 'error')\n return\n }\n flash(\n nextActive\n ? t('customer_accounts.admin.flash.activated', 'User activated')\n : t('customer_accounts.admin.flash.deactivated', 'User deactivated'),\n 'success',\n )\n setReloadToken((token) => token + 1)\n }, { userId: user.id, isActive: nextActive })\n } catch (err) {\n const message = err instanceof Error ? err.message : t('customer_accounts.admin.error.toggleActive', 'Failed to update user status')\n flash(message, 'error')\n }\n }, [confirm, runMutationWithContext, t])\n\n const handleDelete = React.useCallback(async (user: UserRow) => {\n const confirmed = await confirm({\n title: t('customer_accounts.admin.confirm.delete', 'Delete user \"{{name}}\"?', {\n name: user.displayName || user.email,\n }),\n variant: 'destructive',\n })\n if (!confirmed) return\n try {\n await runMutationWithContext(async () => {\n const call = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(user.updatedAt),\n () => apiCall(\n `/api/customer_accounts/admin/users/${encodeURIComponent(user.id)}`,\n { method: 'DELETE' },\n ),\n )\n if (!call.ok) {\n flash(t('customer_accounts.admin.error.delete', 'Failed to delete user'), 'error')\n return\n }\n flash(t('customer_accounts.admin.flash.deleted', 'User deleted'), 'success')\n setReloadToken((token) => token + 1)\n }, { id: user.id })\n } catch (err) {\n const message = err instanceof Error ? err.message : t('customer_accounts.admin.error.delete', 'Failed to delete user')\n flash(message, 'error')\n }\n }, [confirm, runMutationWithContext, t])\n\n const handleFiltersApply = React.useCallback((values: FilterValues) => {\n setFilterValues(values)\n setPage(1)\n }, [])\n\n const handleFiltersClear = React.useCallback(() => {\n setFilterValues({})\n setPage(1)\n }, [])\n\n const columns = React.useMemo<ColumnDef<UserRow>[]>(() => {\n const noValue = <span className=\"text-muted-foreground text-sm\">-</span>\n return [\n {\n accessorKey: 'displayName',\n header: t('customer_accounts.admin.columns.displayName', 'Name'),\n cell: ({ row }) => (\n <Link\n href={`/backend/customer_accounts/users/${row.original.id}`}\n className=\"font-medium hover:underline\"\n >\n {row.original.displayName || row.original.email}\n </Link>\n ),\n },\n {\n accessorKey: 'email',\n header: t('customer_accounts.admin.columns.email', 'Email'),\n },\n {\n accessorKey: 'emailVerified',\n header: t('customer_accounts.admin.columns.emailVerified', 'Verified'),\n cell: ({ row }) => (\n <span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${\n row.original.emailVerified\n ? 'bg-status-success-bg text-status-success-text'\n : 'bg-status-warning-bg text-status-warning-text'\n }`}>\n {row.original.emailVerified\n ? t('customer_accounts.admin.verified', 'Yes')\n : t('customer_accounts.admin.unverified', 'No')}\n </span>\n ),\n },\n {\n accessorKey: 'isActive',\n header: t('customer_accounts.admin.columns.status', 'Status'),\n cell: ({ row }) => (\n <span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${\n row.original.isActive\n ? 'bg-status-success-bg text-status-success-text'\n : 'bg-status-error-bg text-status-error-text'\n }`}>\n {row.original.isActive\n ? t('customer_accounts.admin.active', 'Active')\n : t('customer_accounts.admin.inactive', 'Inactive')}\n </span>\n ),\n },\n {\n accessorKey: 'lastLoginAt',\n header: t('customer_accounts.admin.columns.lastLogin', 'Last Login'),\n cell: ({ row }) => formatDate(row.original.lastLoginAt, '-') || noValue,\n },\n {\n accessorKey: 'roles',\n header: t('customer_accounts.admin.columns.roles', 'Roles'),\n cell: ({ row }) => {\n const roles = row.original.roles\n if (!roles || !roles.length) return noValue\n return <span className=\"text-sm\">{roles.map((r) => r.name).join(', ')}</span>\n },\n },\n {\n accessorKey: 'createdAt',\n header: t('customer_accounts.admin.columns.createdAt', 'Created'),\n cell: ({ row }) => formatDate(row.original.createdAt, '-'),\n },\n ]\n }, [t])\n\n return (\n <Page>\n <PageBody className=\"space-y-4\">\n <div className=\"rounded-lg border border-status-info-border bg-status-info-bg p-4\">\n <div className=\"flex items-start justify-between gap-4\">\n <div>\n <h3 className=\"text-sm font-medium text-status-info-text\">\n {t('customer_accounts.admin.portalInfo.title', 'Customer Portal')}\n </h3>\n <p className=\"mt-1 text-sm text-status-info-text\">\n {t('customer_accounts.admin.portalInfo.description', 'Manage customer portal accounts. Customers can self-register, log in, and access orders, quotes, and invoices through the portal.')}\n </p>\n <p className=\"mt-1.5 text-xs text-status-info-text\">\n {t('customer_accounts.admin.portalInfo.url', 'Portal URL: {url}', {\n url: `${typeof window !== 'undefined' ? window.location.origin : ''}/[org-slug]/portal`,\n })}\n </p>\n <p className=\"mt-0.5 text-xs text-status-info-text\">\n {t('customer_accounts.admin.portalInfo.credentials', 'Demo credentials: alice.johnson@example.com / Password123!')}\n </p>\n </div>\n <div className=\"flex shrink-0 flex-col gap-2\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n asChild\n >\n <Link href=\"/backend/customer_accounts/settings\">\n <Settings className=\"size-4\" />\n {t('customer_accounts.admin.portalInfo.openConfiguration', 'Open Configuration')}\n </Link>\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n asChild\n >\n <a href={`${typeof window !== 'undefined' ? window.location.origin : ''}/portal`} target=\"_blank\" rel=\"noopener noreferrer\">\n <Globe className=\"size-4\" />\n {t('customer_accounts.admin.portalInfo.open', 'Open Portal')}\n </a>\n </Button>\n </div>\n </div>\n </div>\n <DataTable<UserRow>\n stickyActionsColumn\n title={t('customer_accounts.admin.title', 'Users')}\n actions={(\n <Button onClick={() => setCreateDialogOpen(true)}>\n {t('customer_accounts.admin.actions.createUser', 'Create User')}\n </Button>\n )}\n columns={columns}\n data={rows}\n searchValue={search}\n onSearchChange={(value) => { setSearch(value); setPage(1) }}\n searchPlaceholder={t('customer_accounts.admin.searchPlaceholder', 'Search by name or email...')}\n filters={filters}\n filterValues={filterValues}\n onFiltersApply={handleFiltersApply}\n onFiltersClear={handleFiltersClear}\n perspective={{ tableId: 'customer_accounts.admin.users' }}\n emptyState={(\n <ListEmptyState\n entityName={t('customer_accounts.admin.title', 'Users')}\n onCreate={() => setCreateDialogOpen(true)}\n createLabel={t('customer_accounts.admin.actions.createUser', 'Create User')}\n />\n )}\n onRowClick={(row) => router.push(`/backend/customer_accounts/users/${row.id}`)}\n rowActions={(row) => (\n <RowActions\n items={[\n {\n id: 'view',\n label: t('customer_accounts.admin.actions.view', 'View'),\n onSelect: () => { router.push(`/backend/customer_accounts/users/${row.id}`) },\n },\n {\n id: 'toggle-active',\n label: row.isActive\n ? t('customer_accounts.admin.actions.deactivate', 'Deactivate')\n : t('customer_accounts.admin.actions.activate', 'Activate'),\n onSelect: () => { void handleToggleActive(row) },\n },\n {\n id: 'delete',\n label: t('customer_accounts.admin.actions.delete', 'Delete'),\n destructive: true,\n onSelect: () => { void handleDelete(row) },\n },\n ]}\n />\n )}\n pagination={{ page, pageSize, total, totalPages, onPageChange: setPage }}\n isLoading={isLoading}\n />\n <CreateUserDialog\n open={createDialogOpen}\n onOpenChange={setCreateDialogOpen}\n roleOptions={roleOptions}\n onCreated={() => setReloadToken((token) => token + 1)}\n onRunMutation={runMutationWithContext}\n />\n </PageBody>\n {ConfirmDialogElement}\n </Page>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -8,6 +8,7 @@ import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
|
8
8
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
9
9
|
import { Input } from "@open-mercato/ui/primitives/input";
|
|
10
10
|
import { EmailInput } from "@open-mercato/ui/primitives/email-input";
|
|
11
|
+
import { StatusBadge } from "@open-mercato/ui/primitives/status-badge";
|
|
11
12
|
import { flash } from "@open-mercato/ui/backend/FlashMessages";
|
|
12
13
|
function InviteForm({ personEntityId, onSuccess }) {
|
|
13
14
|
const t = useT();
|
|
@@ -230,7 +231,7 @@ function AccountStatusWidget({ context }) {
|
|
|
230
231
|
/* @__PURE__ */ jsxs("div", { className: "space-y-1 text-sm", children: [
|
|
231
232
|
/* @__PURE__ */ jsxs("div", { className: "flex justify-between", children: [
|
|
232
233
|
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: t("common.status", "Status") }),
|
|
233
|
-
/* @__PURE__ */ jsx(
|
|
234
|
+
/* @__PURE__ */ jsx(StatusBadge, { variant: data.isActive ? "success" : "error", dot: true, children: data.isActive ? t("common.active", "Active") : t("common.inactive", "Inactive") })
|
|
234
235
|
] }),
|
|
235
236
|
/* @__PURE__ */ jsxs("div", { className: "flex justify-between", children: [
|
|
236
237
|
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: t("common.email", "Email") }),
|
package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport React from 'react'\nimport { useQuery, useQueryClient } from '@tanstack/react-query'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { EmailInput } from '@open-mercato/ui/primitives/email-input'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\n\ninterface AccountStatusData {\n id: string\n email: string\n isActive: boolean\n emailVerified: boolean\n lastLoginAt: string | null\n}\n\ninterface AccountStatusProps {\n context?: {\n entityId?: string\n recordId?: string\n }\n}\n\ninterface RoleOption {\n id: string\n name: string\n}\n\ninterface PersonData {\n person?: {\n primaryEmail?: string | null\n displayName?: string | null\n }\n profile?: {\n firstName?: string | null\n lastName?: string | null\n } | null\n}\n\nfunction InviteForm({ personEntityId, onSuccess }: { personEntityId: string; onSuccess: () => void }) {\n const t = useT()\n const [isLoadingPerson, setIsLoadingPerson] = React.useState(true)\n const [email, setEmail] = React.useState('')\n const [displayName, setDisplayName] = React.useState('')\n const [selectedRoleIds, setSelectedRoleIds] = React.useState<string[]>([])\n const [availableRoles, setAvailableRoles] = React.useState<RoleOption[]>([])\n const [isLoadingRoles, setIsLoadingRoles] = React.useState(true)\n const [isSubmitting, setIsSubmitting] = React.useState(false)\n\n const { runMutation } = useGuardedMutation<{ entityType: string }>({\n contextId: 'customer_accounts:account-status-invite',\n })\n\n React.useEffect(() => {\n let cancelled = false\n async function loadPerson() {\n try {\n const call = await apiCall<PersonData>(\n `/api/customers/people/${encodeURIComponent(personEntityId)}`,\n )\n if (cancelled) return\n if (call.ok && call.result) {\n const person = call.result.person\n const profile = call.result.profile\n if (person?.primaryEmail) {\n setEmail(person.primaryEmail)\n }\n const nameParts = [profile?.firstName, profile?.lastName].filter(Boolean)\n if (nameParts.length > 0) {\n setDisplayName(nameParts.join(' '))\n } else if (person?.displayName) {\n setDisplayName(person.displayName)\n }\n }\n } catch {\n /* ignore - fields will remain empty for manual entry */\n } finally {\n if (!cancelled) setIsLoadingPerson(false)\n }\n }\n loadPerson()\n return () => { cancelled = true }\n }, [personEntityId])\n\n React.useEffect(() => {\n let cancelled = false\n async function loadRoles() {\n try {\n const call = await apiCall<{ items?: RoleOption[] }>(\n '/api/customer_accounts/admin/roles?pageSize=100',\n )\n if (cancelled) return\n if (call.ok && call.result) {\n const items = Array.isArray(call.result.items) ? call.result.items : []\n setAvailableRoles(items.map((role) => ({ id: role.id, name: role.name })))\n }\n } catch {\n /* ignore */\n } finally {\n if (!cancelled) setIsLoadingRoles(false)\n }\n }\n loadRoles()\n return () => { cancelled = true }\n }, [])\n\n function toggleRole(roleId: string) {\n setSelectedRoleIds((prev) =>\n prev.includes(roleId) ? prev.filter((id) => id !== roleId) : [...prev, roleId],\n )\n }\n\n async function handleSubmit(event: React.FormEvent) {\n event.preventDefault()\n\n const trimmedEmail = email.trim()\n if (!trimmedEmail) {\n flash(t('customer_accounts.widgets.invite.error.emailRequired', 'Email is required'), 'error')\n return\n }\n if (selectedRoleIds.length === 0) {\n flash(t('customer_accounts.widgets.invite.error.roleRequired', 'At least one role must be selected'), 'error')\n return\n }\n\n setIsSubmitting(true)\n try {\n await runMutation({\n context: { entityType: 'customer_accounts:user' },\n mutationPayload: { customerEntityId: personEntityId, roleIds: selectedRoleIds },\n operation: async () => {\n // optimistic-lock-exempt: creates a new portal invitation, not a concurrent record edit\n const call = await apiCall<{ ok: boolean; error?: string }>(\n '/api/customer_accounts/admin/users-invite',\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n email: trimmedEmail,\n roleIds: selectedRoleIds,\n displayName: displayName.trim() || undefined,\n customerEntityId: personEntityId,\n }),\n },\n )\n\n if (!call.ok) {\n const errorMessage = (call.result as Record<string, unknown> | null)?.error as string | undefined\n flash(errorMessage || t('customer_accounts.widgets.invite.error.failed', 'Failed to send invitation'), 'error')\n return\n }\n\n flash(t('customer_accounts.widgets.invite.success', 'Invitation sent successfully'), 'success')\n onSuccess()\n },\n })\n } catch {\n flash(t('customer_accounts.widgets.invite.error.failed', 'Failed to send invitation'), 'error')\n } finally {\n setIsSubmitting(false)\n }\n }\n\n const isLoading = isLoadingPerson || isLoadingRoles\n\n if (isLoading) {\n return (\n <div className=\"text-sm text-muted-foreground py-2\">\n {t('common.loading', 'Loading...')}\n </div>\n )\n }\n\n return (\n <form onSubmit={handleSubmit} className=\"space-y-3 mt-2\">\n <div>\n <label htmlFor=\"invite-email\" className=\"block text-xs font-medium text-muted-foreground mb-1\">\n {t('common.email', 'Email')}\n </label>\n <EmailInput\n id=\"invite-email\"\n size=\"sm\"\n value={email}\n onChange={(event) => setEmail(event.target.value)}\n required\n disabled={isSubmitting}\n />\n </div>\n\n <div>\n <label htmlFor=\"invite-display-name\" className=\"block text-xs font-medium text-muted-foreground mb-1\">\n {t('customer_accounts.widgets.invite.displayName', 'Display Name')}\n </label>\n <Input\n id=\"invite-display-name\"\n type=\"text\"\n size=\"sm\"\n value={displayName}\n onChange={(event) => setDisplayName(event.target.value)}\n disabled={isSubmitting}\n />\n </div>\n\n <div>\n <div className=\"text-xs font-medium text-muted-foreground mb-1.5\">\n {t('customer_accounts.widgets.invite.roles', 'Roles')}\n </div>\n {availableRoles.length === 0 ? (\n <div className=\"text-xs text-muted-foreground\">\n {t('customer_accounts.widgets.invite.noRoles', 'No roles available')}\n </div>\n ) : (\n <div className=\"flex flex-wrap gap-1.5\">\n {availableRoles.map((role) => (\n <Button\n key={role.id}\n type=\"button\"\n variant={selectedRoleIds.includes(role.id) ? 'default' : 'outline'}\n size=\"sm\"\n onClick={() => toggleRole(role.id)}\n disabled={isSubmitting}\n className=\"text-xs h-7\"\n >\n {role.name}\n </Button>\n ))}\n </div>\n )}\n </div>\n\n <div className=\"flex justify-end gap-2 pt-1\">\n <Button\n type=\"submit\"\n size=\"sm\"\n disabled={isSubmitting || !email.trim() || selectedRoleIds.length === 0}\n >\n {isSubmitting\n ? t('common.loading', 'Loading...')\n : t('customer_accounts.widgets.invite.submit', 'Send Invitation')}\n </Button>\n </div>\n </form>\n )\n}\n\nexport default function AccountStatusWidget({ context }: AccountStatusProps) {\n const t = useT()\n const queryClient = useQueryClient()\n const personEntityId = context?.recordId\n const [showInviteForm, setShowInviteForm] = React.useState(false)\n\n const { data, isLoading } = useQuery({\n queryKey: ['customer-account-status', personEntityId],\n queryFn: async (): Promise<AccountStatusData | null> => {\n if (!personEntityId) return null\n const result = await apiCall(`/api/customer_accounts/admin/users?personEntityId=${personEntityId}&pageSize=1`)\n if (!result.ok) return null\n const json = result.result as Record<string, unknown> | null\n const items = json?.items as AccountStatusData[] | undefined\n return items?.[0] || null\n },\n enabled: !!personEntityId,\n })\n\n function handleInviteSuccess() {\n setShowInviteForm(false)\n queryClient.invalidateQueries({ queryKey: ['customer-account-status', personEntityId] })\n }\n\n if (isLoading) {\n return <div className=\"text-sm text-muted-foreground\">{t('common.loading', 'Loading...')}</div>\n }\n\n if (!data) {\n return (\n <div className=\"rounded-md border p-3\">\n <div className=\"text-sm font-medium mb-1\">{t('customer_accounts.widgets.accountStatus', 'Portal Account')}</div>\n <div className=\"text-sm text-muted-foreground\">{t('customer_accounts.widgets.noAccount', 'No portal account linked')}</div>\n {!showInviteForm && personEntityId && (\n <div className=\"mt-2\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setShowInviteForm(true)}\n >\n {t('customer_accounts.widgets.invite.button', 'Invite to Portal')}\n </Button>\n </div>\n )}\n {showInviteForm && personEntityId && (\n <InviteForm personEntityId={personEntityId} onSuccess={handleInviteSuccess} />\n )}\n </div>\n )\n }\n\n return (\n <div className=\"rounded-md border p-3\">\n <div className=\"text-sm font-medium mb-2\">{t('customer_accounts.widgets.accountStatus', 'Portal Account')}</div>\n <div className=\"space-y-1 text-sm\">\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">{t('common.status', 'Status')}</span>\n <span className={data.isActive ? 'text-green-600' : 'text-red-600'}>\n {data.isActive ? t('common.active', 'Active') : t('common.inactive', 'Inactive')}\n </span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">{t('common.email', 'Email')}</span>\n <span>{data.email}</span>\n </div>\n {data.emailVerified !== undefined && (\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">{t('customer_accounts.widgets.emailVerified', 'Email Verified')}</span>\n <span>{data.emailVerified ? '\u2713' : '\u2717'}</span>\n </div>\n )}\n {data.lastLoginAt && (\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">{t('customer_accounts.widgets.lastLogin', 'Last Login')}</span>\n <span>{new Date(data.lastLoginAt).toLocaleDateString()}</span>\n </div>\n )}\n </div>\n <div className=\"mt-2\">\n <a\n href={`/backend/customer_accounts/users/${data.id}`}\n className=\"text-xs text-primary hover:underline\"\n >\n {t('customer_accounts.widgets.viewAccount', 'View account details \u2192')}\n </a>\n </div>\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport React from 'react'\nimport { useQuery, useQueryClient } from '@tanstack/react-query'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { EmailInput } from '@open-mercato/ui/primitives/email-input'\nimport { StatusBadge } from '@open-mercato/ui/primitives/status-badge'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\n\ninterface AccountStatusData {\n id: string\n email: string\n isActive: boolean\n emailVerified: boolean\n lastLoginAt: string | null\n}\n\ninterface AccountStatusProps {\n context?: {\n entityId?: string\n recordId?: string\n }\n}\n\ninterface RoleOption {\n id: string\n name: string\n}\n\ninterface PersonData {\n person?: {\n primaryEmail?: string | null\n displayName?: string | null\n }\n profile?: {\n firstName?: string | null\n lastName?: string | null\n } | null\n}\n\nfunction InviteForm({ personEntityId, onSuccess }: { personEntityId: string; onSuccess: () => void }) {\n const t = useT()\n const [isLoadingPerson, setIsLoadingPerson] = React.useState(true)\n const [email, setEmail] = React.useState('')\n const [displayName, setDisplayName] = React.useState('')\n const [selectedRoleIds, setSelectedRoleIds] = React.useState<string[]>([])\n const [availableRoles, setAvailableRoles] = React.useState<RoleOption[]>([])\n const [isLoadingRoles, setIsLoadingRoles] = React.useState(true)\n const [isSubmitting, setIsSubmitting] = React.useState(false)\n\n const { runMutation } = useGuardedMutation<{ entityType: string }>({\n contextId: 'customer_accounts:account-status-invite',\n })\n\n React.useEffect(() => {\n let cancelled = false\n async function loadPerson() {\n try {\n const call = await apiCall<PersonData>(\n `/api/customers/people/${encodeURIComponent(personEntityId)}`,\n )\n if (cancelled) return\n if (call.ok && call.result) {\n const person = call.result.person\n const profile = call.result.profile\n if (person?.primaryEmail) {\n setEmail(person.primaryEmail)\n }\n const nameParts = [profile?.firstName, profile?.lastName].filter(Boolean)\n if (nameParts.length > 0) {\n setDisplayName(nameParts.join(' '))\n } else if (person?.displayName) {\n setDisplayName(person.displayName)\n }\n }\n } catch {\n /* ignore - fields will remain empty for manual entry */\n } finally {\n if (!cancelled) setIsLoadingPerson(false)\n }\n }\n loadPerson()\n return () => { cancelled = true }\n }, [personEntityId])\n\n React.useEffect(() => {\n let cancelled = false\n async function loadRoles() {\n try {\n const call = await apiCall<{ items?: RoleOption[] }>(\n '/api/customer_accounts/admin/roles?pageSize=100',\n )\n if (cancelled) return\n if (call.ok && call.result) {\n const items = Array.isArray(call.result.items) ? call.result.items : []\n setAvailableRoles(items.map((role) => ({ id: role.id, name: role.name })))\n }\n } catch {\n /* ignore */\n } finally {\n if (!cancelled) setIsLoadingRoles(false)\n }\n }\n loadRoles()\n return () => { cancelled = true }\n }, [])\n\n function toggleRole(roleId: string) {\n setSelectedRoleIds((prev) =>\n prev.includes(roleId) ? prev.filter((id) => id !== roleId) : [...prev, roleId],\n )\n }\n\n async function handleSubmit(event: React.FormEvent) {\n event.preventDefault()\n\n const trimmedEmail = email.trim()\n if (!trimmedEmail) {\n flash(t('customer_accounts.widgets.invite.error.emailRequired', 'Email is required'), 'error')\n return\n }\n if (selectedRoleIds.length === 0) {\n flash(t('customer_accounts.widgets.invite.error.roleRequired', 'At least one role must be selected'), 'error')\n return\n }\n\n setIsSubmitting(true)\n try {\n await runMutation({\n context: { entityType: 'customer_accounts:user' },\n mutationPayload: { customerEntityId: personEntityId, roleIds: selectedRoleIds },\n operation: async () => {\n // optimistic-lock-exempt: creates a new portal invitation, not a concurrent record edit\n const call = await apiCall<{ ok: boolean; error?: string }>(\n '/api/customer_accounts/admin/users-invite',\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n email: trimmedEmail,\n roleIds: selectedRoleIds,\n displayName: displayName.trim() || undefined,\n customerEntityId: personEntityId,\n }),\n },\n )\n\n if (!call.ok) {\n const errorMessage = (call.result as Record<string, unknown> | null)?.error as string | undefined\n flash(errorMessage || t('customer_accounts.widgets.invite.error.failed', 'Failed to send invitation'), 'error')\n return\n }\n\n flash(t('customer_accounts.widgets.invite.success', 'Invitation sent successfully'), 'success')\n onSuccess()\n },\n })\n } catch {\n flash(t('customer_accounts.widgets.invite.error.failed', 'Failed to send invitation'), 'error')\n } finally {\n setIsSubmitting(false)\n }\n }\n\n const isLoading = isLoadingPerson || isLoadingRoles\n\n if (isLoading) {\n return (\n <div className=\"text-sm text-muted-foreground py-2\">\n {t('common.loading', 'Loading...')}\n </div>\n )\n }\n\n return (\n <form onSubmit={handleSubmit} className=\"space-y-3 mt-2\">\n <div>\n <label htmlFor=\"invite-email\" className=\"block text-xs font-medium text-muted-foreground mb-1\">\n {t('common.email', 'Email')}\n </label>\n <EmailInput\n id=\"invite-email\"\n size=\"sm\"\n value={email}\n onChange={(event) => setEmail(event.target.value)}\n required\n disabled={isSubmitting}\n />\n </div>\n\n <div>\n <label htmlFor=\"invite-display-name\" className=\"block text-xs font-medium text-muted-foreground mb-1\">\n {t('customer_accounts.widgets.invite.displayName', 'Display Name')}\n </label>\n <Input\n id=\"invite-display-name\"\n type=\"text\"\n size=\"sm\"\n value={displayName}\n onChange={(event) => setDisplayName(event.target.value)}\n disabled={isSubmitting}\n />\n </div>\n\n <div>\n <div className=\"text-xs font-medium text-muted-foreground mb-1.5\">\n {t('customer_accounts.widgets.invite.roles', 'Roles')}\n </div>\n {availableRoles.length === 0 ? (\n <div className=\"text-xs text-muted-foreground\">\n {t('customer_accounts.widgets.invite.noRoles', 'No roles available')}\n </div>\n ) : (\n <div className=\"flex flex-wrap gap-1.5\">\n {availableRoles.map((role) => (\n <Button\n key={role.id}\n type=\"button\"\n variant={selectedRoleIds.includes(role.id) ? 'default' : 'outline'}\n size=\"sm\"\n onClick={() => toggleRole(role.id)}\n disabled={isSubmitting}\n className=\"text-xs h-7\"\n >\n {role.name}\n </Button>\n ))}\n </div>\n )}\n </div>\n\n <div className=\"flex justify-end gap-2 pt-1\">\n <Button\n type=\"submit\"\n size=\"sm\"\n disabled={isSubmitting || !email.trim() || selectedRoleIds.length === 0}\n >\n {isSubmitting\n ? t('common.loading', 'Loading...')\n : t('customer_accounts.widgets.invite.submit', 'Send Invitation')}\n </Button>\n </div>\n </form>\n )\n}\n\nexport default function AccountStatusWidget({ context }: AccountStatusProps) {\n const t = useT()\n const queryClient = useQueryClient()\n const personEntityId = context?.recordId\n const [showInviteForm, setShowInviteForm] = React.useState(false)\n\n const { data, isLoading } = useQuery({\n queryKey: ['customer-account-status', personEntityId],\n queryFn: async (): Promise<AccountStatusData | null> => {\n if (!personEntityId) return null\n const result = await apiCall(`/api/customer_accounts/admin/users?personEntityId=${personEntityId}&pageSize=1`)\n if (!result.ok) return null\n const json = result.result as Record<string, unknown> | null\n const items = json?.items as AccountStatusData[] | undefined\n return items?.[0] || null\n },\n enabled: !!personEntityId,\n })\n\n function handleInviteSuccess() {\n setShowInviteForm(false)\n queryClient.invalidateQueries({ queryKey: ['customer-account-status', personEntityId] })\n }\n\n if (isLoading) {\n return <div className=\"text-sm text-muted-foreground\">{t('common.loading', 'Loading...')}</div>\n }\n\n if (!data) {\n return (\n <div className=\"rounded-md border p-3\">\n <div className=\"text-sm font-medium mb-1\">{t('customer_accounts.widgets.accountStatus', 'Portal Account')}</div>\n <div className=\"text-sm text-muted-foreground\">{t('customer_accounts.widgets.noAccount', 'No portal account linked')}</div>\n {!showInviteForm && personEntityId && (\n <div className=\"mt-2\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setShowInviteForm(true)}\n >\n {t('customer_accounts.widgets.invite.button', 'Invite to Portal')}\n </Button>\n </div>\n )}\n {showInviteForm && personEntityId && (\n <InviteForm personEntityId={personEntityId} onSuccess={handleInviteSuccess} />\n )}\n </div>\n )\n }\n\n return (\n <div className=\"rounded-md border p-3\">\n <div className=\"text-sm font-medium mb-2\">{t('customer_accounts.widgets.accountStatus', 'Portal Account')}</div>\n <div className=\"space-y-1 text-sm\">\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">{t('common.status', 'Status')}</span>\n <StatusBadge variant={data.isActive ? 'success' : 'error'} dot>\n {data.isActive ? t('common.active', 'Active') : t('common.inactive', 'Inactive')}\n </StatusBadge>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">{t('common.email', 'Email')}</span>\n <span>{data.email}</span>\n </div>\n {data.emailVerified !== undefined && (\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">{t('customer_accounts.widgets.emailVerified', 'Email Verified')}</span>\n <span>{data.emailVerified ? '\u2713' : '\u2717'}</span>\n </div>\n )}\n {data.lastLoginAt && (\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">{t('customer_accounts.widgets.lastLogin', 'Last Login')}</span>\n <span>{new Date(data.lastLoginAt).toLocaleDateString()}</span>\n </div>\n )}\n </div>\n <div className=\"mt-2\">\n <a\n href={`/backend/customer_accounts/users/${data.id}`}\n className=\"text-xs text-primary hover:underline\"\n >\n {t('customer_accounts.widgets.viewAccount', 'View account details \u2192')}\n </a>\n </div>\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -3,6 +3,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useQuery } from "@tanstack/react-query";
|
|
4
4
|
import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
|
|
5
5
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
6
|
+
import { StatusBadge } from "@open-mercato/ui/primitives/status-badge";
|
|
6
7
|
function CompanyUsersWidget({ context }) {
|
|
7
8
|
const t = useT();
|
|
8
9
|
const customerEntityId = context?.recordId;
|
|
@@ -36,7 +37,7 @@ function CompanyUsersWidget({ context }) {
|
|
|
36
37
|
/* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground", children: user.email })
|
|
37
38
|
] }),
|
|
38
39
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
39
|
-
/* @__PURE__ */ jsx(
|
|
40
|
+
/* @__PURE__ */ jsx(StatusBadge, { variant: user.isActive ? "success" : "error", dot: true, children: user.isActive ? t("common.active", "Active") : t("common.inactive", "Inactive") }),
|
|
40
41
|
/* @__PURE__ */ jsx(
|
|
41
42
|
"a",
|
|
42
43
|
{
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/customer_accounts/widgets/injection/company-users/widget.client.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport { useQuery } from '@tanstack/react-query'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\ninterface CompanyUser {\n id: string\n displayName: string\n email: string\n isActive: boolean\n}\n\ninterface CompanyUsersProps {\n context?: {\n entityId?: string\n recordId?: string\n }\n}\n\nexport default function CompanyUsersWidget({ context }: CompanyUsersProps) {\n const t = useT()\n const customerEntityId = context?.recordId\n\n const { data, isLoading } = useQuery({\n queryKey: ['customer-company-users', customerEntityId],\n queryFn: async (): Promise<CompanyUser[]> => {\n if (!customerEntityId) return []\n const result = await apiCall(`/api/customer_accounts/admin/users?customerEntityId=${customerEntityId}&pageSize=50`)\n if (!result.ok) return []\n const json = result.result as Record<string, unknown> | null\n return (json?.items as CompanyUser[]) || []\n },\n enabled: !!customerEntityId,\n })\n\n if (isLoading) {\n return <div className=\"text-sm text-muted-foreground\">{t('common.loading', 'Loading...')}</div>\n }\n\n const users = data || []\n\n return (\n <div className=\"rounded-md border p-3\">\n <div className=\"text-sm font-medium mb-2\">\n {t('customer_accounts.widgets.portalUsers', 'Portal Users')}\n {users.length > 0 && <span className=\"ml-1 text-muted-foreground\">({users.length})</span>}\n </div>\n {users.length === 0 ? (\n <div className=\"text-sm text-muted-foreground\">\n {t('customer_accounts.widgets.noUsers', 'No portal users for this company')}\n </div>\n ) : (\n <div className=\"space-y-2\">\n {users.map((user) => (\n <div key={user.id} className=\"flex items-center justify-between text-sm\">\n <div>\n <div className=\"font-medium\">{user.displayName}</div>\n <div className=\"text-xs text-muted-foreground\">{user.email}</div>\n </div>\n <div className=\"flex items-center gap-2\">\n <
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport { useQuery } from '@tanstack/react-query'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { StatusBadge } from '@open-mercato/ui/primitives/status-badge'\n\ninterface CompanyUser {\n id: string\n displayName: string\n email: string\n isActive: boolean\n}\n\ninterface CompanyUsersProps {\n context?: {\n entityId?: string\n recordId?: string\n }\n}\n\nexport default function CompanyUsersWidget({ context }: CompanyUsersProps) {\n const t = useT()\n const customerEntityId = context?.recordId\n\n const { data, isLoading } = useQuery({\n queryKey: ['customer-company-users', customerEntityId],\n queryFn: async (): Promise<CompanyUser[]> => {\n if (!customerEntityId) return []\n const result = await apiCall(`/api/customer_accounts/admin/users?customerEntityId=${customerEntityId}&pageSize=50`)\n if (!result.ok) return []\n const json = result.result as Record<string, unknown> | null\n return (json?.items as CompanyUser[]) || []\n },\n enabled: !!customerEntityId,\n })\n\n if (isLoading) {\n return <div className=\"text-sm text-muted-foreground\">{t('common.loading', 'Loading...')}</div>\n }\n\n const users = data || []\n\n return (\n <div className=\"rounded-md border p-3\">\n <div className=\"text-sm font-medium mb-2\">\n {t('customer_accounts.widgets.portalUsers', 'Portal Users')}\n {users.length > 0 && <span className=\"ml-1 text-muted-foreground\">({users.length})</span>}\n </div>\n {users.length === 0 ? (\n <div className=\"text-sm text-muted-foreground\">\n {t('customer_accounts.widgets.noUsers', 'No portal users for this company')}\n </div>\n ) : (\n <div className=\"space-y-2\">\n {users.map((user) => (\n <div key={user.id} className=\"flex items-center justify-between text-sm\">\n <div>\n <div className=\"font-medium\">{user.displayName}</div>\n <div className=\"text-xs text-muted-foreground\">{user.email}</div>\n </div>\n <div className=\"flex items-center gap-2\">\n <StatusBadge variant={user.isActive ? 'success' : 'error'} dot>\n {user.isActive ? t('common.active', 'Active') : t('common.inactive', 'Inactive')}\n </StatusBadge>\n <a\n href={`/backend/customer_accounts/users/${user.id}`}\n className=\"text-xs text-primary hover:underline\"\n >\n {t('common.view', 'View')}\n </a>\n </div>\n </div>\n ))}\n </div>\n )}\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": ";AAsCW,cASkB,YATlB;AApCX,SAAS,gBAAgB;AACzB,SAAS,eAAe;AACxB,SAAS,YAAY;AACrB,SAAS,mBAAmB;AAgBb,SAAR,mBAAoC,EAAE,QAAQ,GAAsB;AACzE,QAAM,IAAI,KAAK;AACf,QAAM,mBAAmB,SAAS;AAElC,QAAM,EAAE,MAAM,UAAU,IAAI,SAAS;AAAA,IACnC,UAAU,CAAC,0BAA0B,gBAAgB;AAAA,IACrD,SAAS,YAAoC;AAC3C,UAAI,CAAC,iBAAkB,QAAO,CAAC;AAC/B,YAAM,SAAS,MAAM,QAAQ,uDAAuD,gBAAgB,cAAc;AAClH,UAAI,CAAC,OAAO,GAAI,QAAO,CAAC;AACxB,YAAM,OAAO,OAAO;AACpB,aAAQ,MAAM,SAA2B,CAAC;AAAA,IAC5C;AAAA,IACA,SAAS,CAAC,CAAC;AAAA,EACb,CAAC;AAED,MAAI,WAAW;AACb,WAAO,oBAAC,SAAI,WAAU,iCAAiC,YAAE,kBAAkB,YAAY,GAAE;AAAA,EAC3F;AAEA,QAAM,QAAQ,QAAQ,CAAC;AAEvB,SACE,qBAAC,SAAI,WAAU,yBACb;AAAA,yBAAC,SAAI,WAAU,4BACZ;AAAA,QAAE,yCAAyC,cAAc;AAAA,MACzD,MAAM,SAAS,KAAK,qBAAC,UAAK,WAAU,8BAA6B;AAAA;AAAA,QAAE,MAAM;AAAA,QAAO;AAAA,SAAC;AAAA,OACpF;AAAA,IACC,MAAM,WAAW,IAChB,oBAAC,SAAI,WAAU,iCACZ,YAAE,qCAAqC,kCAAkC,GAC5E,IAEA,oBAAC,SAAI,WAAU,aACZ,gBAAM,IAAI,CAAC,SACV,qBAAC,SAAkB,WAAU,6CAC3B;AAAA,2BAAC,SACC;AAAA,4BAAC,SAAI,WAAU,eAAe,eAAK,aAAY;AAAA,QAC/C,oBAAC,SAAI,WAAU,iCAAiC,eAAK,OAAM;AAAA,SAC7D;AAAA,MACA,qBAAC,SAAI,WAAU,2BACb;AAAA,4BAAC,eAAY,SAAS,KAAK,WAAW,YAAY,SAAS,KAAG,MAC3D,eAAK,WAAW,EAAE,iBAAiB,QAAQ,IAAI,EAAE,mBAAmB,UAAU,GACjF;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,MAAM,oCAAoC,KAAK,EAAE;AAAA,YACjD,WAAU;AAAA,YAET,YAAE,eAAe,MAAM;AAAA;AAAA,QAC1B;AAAA,SACF;AAAA,SAfQ,KAAK,EAgBf,CACD,GACH;AAAA,KAEJ;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|