@open-mercato/core 0.4.5-develop-0f0e676c72 → 0.4.5-develop-e694581d9f
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/dist/generated/entities/customer_deal/index.js +4 -0
- package/dist/generated/entities/customer_deal/index.js.map +2 -2
- package/dist/generated/entities/customer_pipeline/index.js +17 -0
- package/dist/generated/entities/customer_pipeline/index.js.map +7 -0
- package/dist/generated/entities/customer_pipeline_stage/index.js +19 -0
- package/dist/generated/entities/customer_pipeline_stage/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +2 -0
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +4 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/customers/acl.js +2 -0
- package/dist/modules/customers/acl.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/route.js +4 -0
- package/dist/modules/customers/api/deals/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/deals/route.js +12 -0
- package/dist/modules/customers/api/deals/route.js.map +2 -2
- package/dist/modules/customers/api/dictionaries/[kind]/route.js +20 -1
- package/dist/modules/customers/api/dictionaries/[kind]/route.js.map +2 -2
- package/dist/modules/customers/api/pipeline-stages/reorder/route.js +69 -0
- package/dist/modules/customers/api/pipeline-stages/reorder/route.js.map +7 -0
- package/dist/modules/customers/api/pipeline-stages/route.js +275 -0
- package/dist/modules/customers/api/pipeline-stages/route.js.map +7 -0
- package/dist/modules/customers/api/pipelines/route.js +245 -0
- package/dist/modules/customers/api/pipelines/route.js.map +7 -0
- package/dist/modules/customers/backend/config/customers/page.js +2 -0
- package/dist/modules/customers/backend/config/customers/page.js.map +2 -2
- package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js +439 -0
- package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js.map +7 -0
- package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js +17 -0
- package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/[id]/page.js +19 -1
- package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/page.js +35 -1
- package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js +102 -74
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
- package/dist/modules/customers/cli.js +28 -2
- package/dist/modules/customers/cli.js.map +2 -2
- package/dist/modules/customers/commands/deals.js +34 -2
- package/dist/modules/customers/commands/deals.js.map +2 -2
- package/dist/modules/customers/commands/index.js +2 -0
- package/dist/modules/customers/commands/index.js.map +2 -2
- package/dist/modules/customers/commands/pipeline-stages.js +126 -0
- package/dist/modules/customers/commands/pipeline-stages.js.map +7 -0
- package/dist/modules/customers/commands/pipelines.js +87 -0
- package/dist/modules/customers/commands/pipelines.js.map +7 -0
- package/dist/modules/customers/components/DictionarySettings.js +0 -5
- package/dist/modules/customers/components/DictionarySettings.js.map +2 -2
- package/dist/modules/customers/components/PipelineSettings.js +474 -0
- package/dist/modules/customers/components/PipelineSettings.js.map +7 -0
- package/dist/modules/customers/components/detail/DealForm.js +84 -12
- package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
- package/dist/modules/customers/data/entities.js +78 -0
- package/dist/modules/customers/data/entities.js.map +2 -2
- package/dist/modules/customers/data/validators.js +44 -0
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/customers/migrations/Migration20260218191730.js +77 -0
- package/dist/modules/customers/migrations/Migration20260218191730.js.map +7 -0
- package/dist/modules/customers/setup.js +7 -3
- package/dist/modules/customers/setup.js.map +2 -2
- package/dist/modules/translations/api/[entityType]/[entityId]/route.js +46 -44
- package/dist/modules/translations/api/[entityType]/[entityId]/route.js.map +2 -2
- package/dist/modules/translations/api/context.js +10 -1
- package/dist/modules/translations/api/context.js.map +2 -2
- package/dist/modules/translations/commands/index.js +2 -0
- package/dist/modules/translations/commands/index.js.map +7 -0
- package/dist/modules/translations/commands/translations.js +160 -0
- package/dist/modules/translations/commands/translations.js.map +7 -0
- package/dist/modules/translations/index.js +1 -0
- package/dist/modules/translations/index.js.map +2 -2
- package/dist/modules/workflows/migrations/Migration20260222205305.js +14 -0
- package/dist/modules/workflows/migrations/Migration20260222205305.js.map +7 -0
- package/generated/entities/customer_deal/index.ts +2 -0
- package/generated/entities/customer_pipeline/index.ts +7 -0
- package/generated/entities/customer_pipeline_stage/index.ts +8 -0
- package/generated/entities.ids.generated.ts +2 -0
- package/generated/entity-fields-registry.ts +4 -0
- package/package.json +2 -2
- package/src/modules/customers/acl.ts +2 -0
- package/src/modules/customers/api/deals/[id]/route.ts +4 -0
- package/src/modules/customers/api/deals/route.ts +12 -0
- package/src/modules/customers/api/dictionaries/[kind]/route.ts +21 -1
- package/src/modules/customers/api/pipeline-stages/reorder/route.ts +71 -0
- package/src/modules/customers/api/pipeline-stages/route.ts +296 -0
- package/src/modules/customers/api/pipelines/route.ts +261 -0
- package/src/modules/customers/backend/config/customers/page.tsx +2 -0
- package/src/modules/customers/backend/config/customers/pipeline-stages/page.meta.ts +13 -0
- package/src/modules/customers/backend/config/customers/pipeline-stages/page.tsx +512 -0
- package/src/modules/customers/backend/customers/deals/[id]/page.tsx +21 -1
- package/src/modules/customers/backend/customers/deals/page.tsx +33 -1
- package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +119 -79
- package/src/modules/customers/cli.ts +29 -1
- package/src/modules/customers/commands/deals.ts +44 -1
- package/src/modules/customers/commands/index.ts +2 -0
- package/src/modules/customers/commands/pipeline-stages.ts +156 -0
- package/src/modules/customers/commands/pipelines.ts +105 -0
- package/src/modules/customers/components/DictionarySettings.tsx +0 -5
- package/src/modules/customers/components/PipelineSettings.tsx +570 -0
- package/src/modules/customers/components/detail/DealForm.tsx +89 -11
- package/src/modules/customers/data/entities.ts +64 -0
- package/src/modules/customers/data/validators.ts +57 -0
- package/src/modules/customers/i18n/de.json +4 -0
- package/src/modules/customers/i18n/en.json +4 -0
- package/src/modules/customers/i18n/es.json +4 -0
- package/src/modules/customers/i18n/pl.json +5 -1
- package/src/modules/customers/migrations/Migration20260218191730.ts +84 -0
- package/src/modules/customers/setup.ts +5 -1
- package/src/modules/translations/api/[entityType]/[entityId]/route.ts +65 -60
- package/src/modules/translations/api/context.ts +12 -0
- package/src/modules/translations/commands/index.ts +1 -0
- package/src/modules/translations/commands/translations.ts +253 -0
- package/src/modules/translations/index.ts +1 -0
- package/src/modules/workflows/migrations/Migration20260222205305.ts +13 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/customers/components/DictionarySettings.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n} from '@open-mercato/ui/primitives/dialog'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport type { CustomerDictionaryKind } from '../lib/dictionaries'\nimport { ICON_SUGGESTIONS } from '@open-mercato/core/modules/dictionaries/components/dictionaryAppearance'\nimport {\n DictionaryForm,\n type DictionaryFormValues,\n} from '@open-mercato/core/modules/dictionaries/components/DictionaryForm'\nimport {\n DictionaryTable,\n type DictionaryTableEntry,\n} from '@open-mercato/core/modules/dictionaries/components/DictionaryTable'\n\ntype SectionDefinition = {\n kind: CustomerDictionaryKind\n title: string\n description: string\n}\n\ntype DialogState =\n | { mode: 'create' }\n | { mode: 'edit'; entry: DictionaryTableEntry }\n\nconst DEFAULT_FORM_VALUES: DictionaryFormValues = {\n value: '',\n label: '',\n color: null,\n icon: null,\n}\n\nexport default function DictionarySettings() {\n const t = useT()\n\n const sections = React.useMemo<SectionDefinition[]>(() => [\n {\n kind: 'statuses',\n title: t('customers.config.dictionaries.sections.statuses.title', 'Statuses'),\n description: t('customers.config.dictionaries.sections.statuses.description', 'Define the statuses available for customer records.'),\n },\n {\n kind: 'deal-statuses',\n title: t('customers.config.dictionaries.sections.dealStatuses.title', 'Deal statuses'),\n description: t('customers.config.dictionaries.sections.dealStatuses.description', 'Manage the statuses available for deals.'),\n },\n {\n kind: 'pipeline-stages',\n title: t('customers.config.dictionaries.sections.pipelineStages.title', 'Pipeline stages'),\n description: t('customers.config.dictionaries.sections.pipelineStages.description', 'Define the stages used in your deal pipeline.'),\n },\n {\n kind: 'job-titles',\n title: t('customers.config.dictionaries.sections.jobTitles.title', 'Job titles'),\n description: t('customers.config.dictionaries.sections.jobTitles.description', 'Configure job titles with their appearance.'),\n },\n {\n kind: 'sources',\n title: t('customers.config.dictionaries.sections.sources.title', 'Sources'),\n description: t('customers.config.dictionaries.sections.sources.description', 'Capture how customers were acquired.'),\n },\n {\n kind: 'industries',\n title: t('customers.config.dictionaries.sections.industries.title', 'Industries'),\n description: t('customers.config.dictionaries.sections.industries.description', 'Manage the industries used by companies.'),\n },\n {\n kind: 'lifecycle-stages',\n title: t('customers.config.dictionaries.sections.lifecycle.title', 'Lifecycle stages'),\n description: t('customers.config.dictionaries.sections.lifecycle.description', 'Configure lifecycle stages to track customer progress.'),\n },\n {\n kind: 'activity-types',\n title: t('customers.config.dictionaries.sections.activityTypes.title', 'Activity types'),\n description: t('customers.config.dictionaries.sections.activityTypes.description', 'Define the activity types used for customer interactions.'),\n },\n {\n kind: 'address-types',\n title: t('customers.config.dictionaries.sections.addressTypes.title', 'Address types'),\n description: t('customers.config.dictionaries.sections.addressTypes.description', 'Define the available address types.'),\n },\n ], [t])\n\n return (\n <div className=\"space-y-8\">\n <header className=\"space-y-2\">\n <h1 className=\"text-2xl font-semibold\">\n {t('customers.config.dictionaries.title', 'Customers dictionaries')}\n </h1>\n <p className=\"text-sm text-muted-foreground\">\n {t('customers.config.dictionaries.description', 'Manage the dictionaries used by the customers module.')}\n </p>\n </header>\n\n <div className=\"space-y-6\">\n {sections.map((section) => (\n <CustomerDictionarySection key={section.kind} {...section} />\n ))}\n </div>\n </div>\n )\n}\n\ntype CustomerDictionarySectionProps = SectionDefinition\n\nfunction CustomerDictionarySection({ kind, title, description }: CustomerDictionarySectionProps) {\n const t = useT()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const scopeVersion = useOrganizationScopeVersion()\n const [entries, setEntries] = React.useState<DictionaryTableEntry[]>([])\n const [loading, setLoading] = React.useState<boolean>(true)\n const [dialog, setDialog] = React.useState<DialogState | null>(null)\n const [submitting, setSubmitting] = React.useState(false)\n\n const inheritedActionBlocked = t('customers.config.dictionaries.inherited.blocked', 'Inherited entries can only be edited from the parent organization.')\n const inheritedTooltip = t('customers.config.dictionaries.inherited.tooltip', 'Managed in parent organization')\n const inheritedLabel = t('customers.config.dictionaries.inherited.label', 'Inherited')\n const errorLoad = t('customers.config.dictionaries.error.load', 'Failed to load dictionary entries.')\n const errorSave = t('customers.config.dictionaries.error.save', 'Failed to save dictionary entry.')\n const errorDelete = t('customers.config.dictionaries.error.delete', 'Failed to delete dictionary entry.')\n const successSave = t('customers.config.dictionaries.success.save', 'Dictionary entry saved.')\n const successDelete = t('customers.config.dictionaries.success.delete', 'Dictionary entry deleted.')\n const deleteConfirmTemplate = t('customers.config.dictionaries.deleteConfirm', 'Delete \"{{value}}\"?')\n const searchPlaceholder = t('customers.config.dictionaries.searchPlaceholder', 'Search entries\u2026')\n\n const loadEntries = React.useCallback(async () => {\n setLoading(true)\n try {\n const data = await readApiResultOrThrow<{ items?: unknown[] }>(\n `/api/customers/dictionaries/${kind}`,\n undefined,\n { errorMessage: errorLoad },\n )\n if (!Array.isArray(data?.items)) throw new Error(errorLoad)\n const mapped: DictionaryTableEntry[] = data.items.map((item: any) => ({\n id: String(item.id),\n value: String(item.value ?? ''),\n label: typeof item.label === 'string' ? item.label : '',\n color: typeof item.color === 'string' ? item.color : null,\n icon: typeof item.icon === 'string' ? item.icon : null,\n organizationId: typeof item.organizationId === 'string' ? item.organizationId : null,\n tenantId: typeof item.tenantId === 'string' ? item.tenantId : null,\n isInherited: item.isInherited === true,\n createdAt: typeof item.createdAt === 'string' ? item.createdAt : null,\n updatedAt: typeof item.updatedAt === 'string' ? item.updatedAt : null,\n }))\n setEntries(mapped)\n } catch (err) {\n console.error('customers.dictionaries.list failed', err)\n flash(errorLoad, 'error')\n } finally {\n setLoading(false)\n }\n }, [errorLoad, kind, scopeVersion])\n\n React.useEffect(() => {\n loadEntries().catch(() => {})\n }, [loadEntries])\n\n const closeDialog = React.useCallback(() => {\n setDialog(null)\n }, [])\n\n const handleCreate = React.useCallback(() => {\n setDialog({ mode: 'create' })\n }, [])\n\n const handleEdit = React.useCallback((entry: DictionaryTableEntry) => {\n if (entry.isInherited) {\n flash(inheritedActionBlocked, 'info')\n return\n }\n setDialog({ mode: 'edit', entry })\n }, [inheritedActionBlocked])\n\n const handleDelete = React.useCallback(async (entry: DictionaryTableEntry) => {\n if (entry.isInherited) {\n flash(inheritedActionBlocked, 'info')\n return\n }\n const message = deleteConfirmTemplate.replace('{{value}}', entry.label || entry.value)\n const confirmed = await confirm({\n title: message,\n variant: 'destructive',\n })\n if (!confirmed) return\n try {\n await apiCallOrThrow(\n `/api/customers/dictionaries/${kind}/${encodeURIComponent(entry.id)}`,\n { method: 'DELETE' },\n { errorMessage: errorDelete },\n )\n flash(successDelete, 'success')\n await loadEntries()\n } catch (err) {\n console.error('customers.dictionaries.delete failed', err)\n const messageValue = err instanceof Error ? err.message : errorDelete\n flash(messageValue, 'error')\n }\n }, [confirm, deleteConfirmTemplate, errorDelete, inheritedActionBlocked, kind, loadEntries, successDelete])\n\n const submitForm = React.useCallback(async (values: DictionaryFormValues) => {\n const payload = {\n value: values.value,\n label: values.label,\n color: values.color,\n icon: values.icon,\n }\n setSubmitting(true)\n try {\n if (!dialog || dialog.mode === 'create') {\n await apiCallOrThrow(\n `/api/customers/dictionaries/${kind}`,\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n },\n { errorMessage: errorSave },\n )\n flash(successSave, 'success')\n } else if (dialog.mode === 'edit') {\n const target = dialog.entry\n if (target.isInherited) {\n flash(inheritedActionBlocked, 'info')\n return\n }\n const body: Record<string, unknown> = {}\n if (values.value !== target.value) body.value = values.value\n if (values.label !== target.label) body.label = values.label\n const nextColor = values.color ?? null\n if (nextColor !== (target.color ?? null)) body.color = nextColor\n const nextIcon = values.icon ?? null\n if (nextIcon !== (target.icon ?? null)) body.icon = nextIcon\n if (Object.keys(body).length === 0) {\n closeDialog()\n return\n }\n await apiCallOrThrow(\n `/api/customers/dictionaries/${kind}/${encodeURIComponent(target.id)}`,\n {\n method: 'PATCH',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n },\n { errorMessage: errorSave },\n )\n flash(successSave, 'success')\n }\n closeDialog()\n await loadEntries()\n } catch (err) {\n console.error('customers.dictionaries.submit failed', err)\n throw err instanceof Error ? err : new Error(errorSave)\n } finally {\n setSubmitting(false)\n }\n }, [closeDialog, dialog, errorSave, inheritedActionBlocked, kind, loadEntries, successSave])\n\n const currentValues = React.useMemo<DictionaryFormValues>(() => {\n if (dialog && dialog.mode === 'edit') {\n return {\n value: dialog.entry.value,\n label: dialog.entry.label,\n color: dialog.entry.color,\n icon: dialog.entry.icon,\n }\n }\n return DEFAULT_FORM_VALUES\n }, [dialog])\n\n const tableTranslations = React.useMemo(() => ({\n title,\n valueColumn: t('customers.config.dictionaries.columns.value', 'Value'),\n labelColumn: t('customers.config.dictionaries.columns.label', 'Label'),\n appearanceColumn: t('customers.config.dictionaries.columns.appearance', 'Appearance'),\n addLabel: t('customers.config.dictionaries.actions.add', 'Add entry'),\n editLabel: t('customers.config.dictionaries.actions.edit', 'Edit'),\n deleteLabel: t('customers.config.dictionaries.actions.delete', 'Delete'),\n refreshLabel: t('customers.config.dictionaries.actions.refresh', 'Refresh'),\n inheritedLabel,\n inheritedTooltip,\n emptyLabel: t('customers.config.dictionaries.empty', 'No entries yet.'),\n searchPlaceholder,\n }), [inheritedLabel, inheritedTooltip, searchPlaceholder, t, title])\n\n const formTranslations = React.useMemo(() => ({\n title: dialog?.mode === 'edit'\n ? t('customers.config.dictionaries.dialog.editTitle', 'Edit entry')\n : t('customers.config.dictionaries.dialog.addTitle', 'Add entry'),\n valueLabel: t('customers.config.dictionaries.dialog.valueLabel', 'Value'),\n labelLabel: t('customers.config.dictionaries.dialog.labelLabel', 'Label'),\n saveLabel: t('customers.config.dictionaries.dialog.save', 'Save'),\n cancelLabel: t('customers.config.dictionaries.dialog.cancel', 'Cancel'),\n appearance: {\n colorLabel: t('customers.config.dictionaries.dialog.colorLabel', 'Color'),\n colorHelp: t('customers.config.dictionaries.dialog.colorHelp', 'Pick a highlight color for this entry.'),\n colorClearLabel: t('customers.config.dictionaries.dialog.colorClear', 'Remove color'),\n iconLabel: t('customers.config.dictionaries.dialog.iconLabel', 'Icon'),\n iconPlaceholder: t('customers.config.dictionaries.dialog.iconPlaceholder', 'Type an emoji or pick one of the suggestions.'),\n iconPickerTriggerLabel: t('customers.config.dictionaries.dialog.iconBrowse', 'Browse icons and emojis'),\n iconSearchPlaceholder: t('customers.config.dictionaries.dialog.iconSearchPlaceholder', 'Search icons or emojis\u2026'),\n iconSearchEmptyLabel: t('customers.config.dictionaries.dialog.iconSearchEmpty', 'No icons match your search.'),\n iconSuggestionsLabel: t('customers.config.dictionaries.dialog.iconSuggestions', 'Suggestions'),\n iconClearLabel: t('customers.config.dictionaries.dialog.iconClear', 'Remove icon'),\n previewEmptyLabel: t('customers.config.dictionaries.appearance.empty', 'None'),\n },\n }), [dialog, t])\n\n return (\n <section className=\"rounded border bg-card text-card-foreground shadow-sm\">\n <div className=\"border-b px-6 py-4 space-y-1\">\n <h2 className=\"text-lg font-medium\">{title}</h2>\n <p className=\"text-sm text-muted-foreground\">{description}</p>\n </div>\n <div className=\"px-2 py-4 sm:px-4\">\n <DictionaryTable\n entries={entries}\n loading={loading}\n canManage\n onCreate={handleCreate}\n onEdit={handleEdit}\n onDelete={handleDelete}\n onRefresh={loadEntries}\n translations={tableTranslations}\n />\n </div>\n <Dialog open={dialog !== null} onOpenChange={(open) => { if (!open) closeDialog() }}>\n <DialogContent className=\"max-w-lg\">\n <DialogHeader>\n <DialogTitle>{formTranslations.title}</DialogTitle>\n </DialogHeader>\n <DictionaryForm\n mode={dialog?.mode === 'edit' ? 'edit' : 'create'}\n initialValues={currentValues}\n onSubmit={submitForm}\n onCancel={closeDialog}\n submitting={submitting}\n translations={formTranslations}\n iconSuggestions={ICON_SUGGESTIONS}\n />\n </DialogContent>\n </Dialog>\n {ConfirmDialogElement}\n </section>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n} from '@open-mercato/ui/primitives/dialog'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport type { CustomerDictionaryKind } from '../lib/dictionaries'\nimport { ICON_SUGGESTIONS } from '@open-mercato/core/modules/dictionaries/components/dictionaryAppearance'\nimport {\n DictionaryForm,\n type DictionaryFormValues,\n} from '@open-mercato/core/modules/dictionaries/components/DictionaryForm'\nimport {\n DictionaryTable,\n type DictionaryTableEntry,\n} from '@open-mercato/core/modules/dictionaries/components/DictionaryTable'\n\ntype SectionDefinition = {\n kind: CustomerDictionaryKind\n title: string\n description: string\n}\n\ntype DialogState =\n | { mode: 'create' }\n | { mode: 'edit'; entry: DictionaryTableEntry }\n\nconst DEFAULT_FORM_VALUES: DictionaryFormValues = {\n value: '',\n label: '',\n color: null,\n icon: null,\n}\n\nexport default function DictionarySettings() {\n const t = useT()\n\n const sections = React.useMemo<SectionDefinition[]>(() => [\n {\n kind: 'statuses',\n title: t('customers.config.dictionaries.sections.statuses.title', 'Statuses'),\n description: t('customers.config.dictionaries.sections.statuses.description', 'Define the statuses available for customer records.'),\n },\n {\n kind: 'deal-statuses',\n title: t('customers.config.dictionaries.sections.dealStatuses.title', 'Deal statuses'),\n description: t('customers.config.dictionaries.sections.dealStatuses.description', 'Manage the statuses available for deals.'),\n },\n {\n kind: 'job-titles',\n title: t('customers.config.dictionaries.sections.jobTitles.title', 'Job titles'),\n description: t('customers.config.dictionaries.sections.jobTitles.description', 'Configure job titles with their appearance.'),\n },\n {\n kind: 'sources',\n title: t('customers.config.dictionaries.sections.sources.title', 'Sources'),\n description: t('customers.config.dictionaries.sections.sources.description', 'Capture how customers were acquired.'),\n },\n {\n kind: 'industries',\n title: t('customers.config.dictionaries.sections.industries.title', 'Industries'),\n description: t('customers.config.dictionaries.sections.industries.description', 'Manage the industries used by companies.'),\n },\n {\n kind: 'lifecycle-stages',\n title: t('customers.config.dictionaries.sections.lifecycle.title', 'Lifecycle stages'),\n description: t('customers.config.dictionaries.sections.lifecycle.description', 'Configure lifecycle stages to track customer progress.'),\n },\n {\n kind: 'activity-types',\n title: t('customers.config.dictionaries.sections.activityTypes.title', 'Activity types'),\n description: t('customers.config.dictionaries.sections.activityTypes.description', 'Define the activity types used for customer interactions.'),\n },\n {\n kind: 'address-types',\n title: t('customers.config.dictionaries.sections.addressTypes.title', 'Address types'),\n description: t('customers.config.dictionaries.sections.addressTypes.description', 'Define the available address types.'),\n },\n ], [t])\n\n return (\n <div className=\"space-y-8\">\n <header className=\"space-y-2\">\n <h1 className=\"text-2xl font-semibold\">\n {t('customers.config.dictionaries.title', 'Customers dictionaries')}\n </h1>\n <p className=\"text-sm text-muted-foreground\">\n {t('customers.config.dictionaries.description', 'Manage the dictionaries used by the customers module.')}\n </p>\n </header>\n\n <div className=\"space-y-6\">\n {sections.map((section) => (\n <CustomerDictionarySection key={section.kind} {...section} />\n ))}\n </div>\n </div>\n )\n}\n\ntype CustomerDictionarySectionProps = SectionDefinition\n\nfunction CustomerDictionarySection({ kind, title, description }: CustomerDictionarySectionProps) {\n const t = useT()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const scopeVersion = useOrganizationScopeVersion()\n const [entries, setEntries] = React.useState<DictionaryTableEntry[]>([])\n const [loading, setLoading] = React.useState<boolean>(true)\n const [dialog, setDialog] = React.useState<DialogState | null>(null)\n const [submitting, setSubmitting] = React.useState(false)\n\n const inheritedActionBlocked = t('customers.config.dictionaries.inherited.blocked', 'Inherited entries can only be edited from the parent organization.')\n const inheritedTooltip = t('customers.config.dictionaries.inherited.tooltip', 'Managed in parent organization')\n const inheritedLabel = t('customers.config.dictionaries.inherited.label', 'Inherited')\n const errorLoad = t('customers.config.dictionaries.error.load', 'Failed to load dictionary entries.')\n const errorSave = t('customers.config.dictionaries.error.save', 'Failed to save dictionary entry.')\n const errorDelete = t('customers.config.dictionaries.error.delete', 'Failed to delete dictionary entry.')\n const successSave = t('customers.config.dictionaries.success.save', 'Dictionary entry saved.')\n const successDelete = t('customers.config.dictionaries.success.delete', 'Dictionary entry deleted.')\n const deleteConfirmTemplate = t('customers.config.dictionaries.deleteConfirm', 'Delete \"{{value}}\"?')\n const searchPlaceholder = t('customers.config.dictionaries.searchPlaceholder', 'Search entries\u2026')\n\n const loadEntries = React.useCallback(async () => {\n setLoading(true)\n try {\n const data = await readApiResultOrThrow<{ items?: unknown[] }>(\n `/api/customers/dictionaries/${kind}`,\n undefined,\n { errorMessage: errorLoad },\n )\n if (!Array.isArray(data?.items)) throw new Error(errorLoad)\n const mapped: DictionaryTableEntry[] = data.items.map((item: any) => ({\n id: String(item.id),\n value: String(item.value ?? ''),\n label: typeof item.label === 'string' ? item.label : '',\n color: typeof item.color === 'string' ? item.color : null,\n icon: typeof item.icon === 'string' ? item.icon : null,\n organizationId: typeof item.organizationId === 'string' ? item.organizationId : null,\n tenantId: typeof item.tenantId === 'string' ? item.tenantId : null,\n isInherited: item.isInherited === true,\n createdAt: typeof item.createdAt === 'string' ? item.createdAt : null,\n updatedAt: typeof item.updatedAt === 'string' ? item.updatedAt : null,\n }))\n setEntries(mapped)\n } catch (err) {\n console.error('customers.dictionaries.list failed', err)\n flash(errorLoad, 'error')\n } finally {\n setLoading(false)\n }\n }, [errorLoad, kind, scopeVersion])\n\n React.useEffect(() => {\n loadEntries().catch(() => {})\n }, [loadEntries])\n\n const closeDialog = React.useCallback(() => {\n setDialog(null)\n }, [])\n\n const handleCreate = React.useCallback(() => {\n setDialog({ mode: 'create' })\n }, [])\n\n const handleEdit = React.useCallback((entry: DictionaryTableEntry) => {\n if (entry.isInherited) {\n flash(inheritedActionBlocked, 'info')\n return\n }\n setDialog({ mode: 'edit', entry })\n }, [inheritedActionBlocked])\n\n const handleDelete = React.useCallback(async (entry: DictionaryTableEntry) => {\n if (entry.isInherited) {\n flash(inheritedActionBlocked, 'info')\n return\n }\n const message = deleteConfirmTemplate.replace('{{value}}', entry.label || entry.value)\n const confirmed = await confirm({\n title: message,\n variant: 'destructive',\n })\n if (!confirmed) return\n try {\n await apiCallOrThrow(\n `/api/customers/dictionaries/${kind}/${encodeURIComponent(entry.id)}`,\n { method: 'DELETE' },\n { errorMessage: errorDelete },\n )\n flash(successDelete, 'success')\n await loadEntries()\n } catch (err) {\n console.error('customers.dictionaries.delete failed', err)\n const messageValue = err instanceof Error ? err.message : errorDelete\n flash(messageValue, 'error')\n }\n }, [confirm, deleteConfirmTemplate, errorDelete, inheritedActionBlocked, kind, loadEntries, successDelete])\n\n const submitForm = React.useCallback(async (values: DictionaryFormValues) => {\n const payload = {\n value: values.value,\n label: values.label,\n color: values.color,\n icon: values.icon,\n }\n setSubmitting(true)\n try {\n if (!dialog || dialog.mode === 'create') {\n await apiCallOrThrow(\n `/api/customers/dictionaries/${kind}`,\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n },\n { errorMessage: errorSave },\n )\n flash(successSave, 'success')\n } else if (dialog.mode === 'edit') {\n const target = dialog.entry\n if (target.isInherited) {\n flash(inheritedActionBlocked, 'info')\n return\n }\n const body: Record<string, unknown> = {}\n if (values.value !== target.value) body.value = values.value\n if (values.label !== target.label) body.label = values.label\n const nextColor = values.color ?? null\n if (nextColor !== (target.color ?? null)) body.color = nextColor\n const nextIcon = values.icon ?? null\n if (nextIcon !== (target.icon ?? null)) body.icon = nextIcon\n if (Object.keys(body).length === 0) {\n closeDialog()\n return\n }\n await apiCallOrThrow(\n `/api/customers/dictionaries/${kind}/${encodeURIComponent(target.id)}`,\n {\n method: 'PATCH',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n },\n { errorMessage: errorSave },\n )\n flash(successSave, 'success')\n }\n closeDialog()\n await loadEntries()\n } catch (err) {\n console.error('customers.dictionaries.submit failed', err)\n throw err instanceof Error ? err : new Error(errorSave)\n } finally {\n setSubmitting(false)\n }\n }, [closeDialog, dialog, errorSave, inheritedActionBlocked, kind, loadEntries, successSave])\n\n const currentValues = React.useMemo<DictionaryFormValues>(() => {\n if (dialog && dialog.mode === 'edit') {\n return {\n value: dialog.entry.value,\n label: dialog.entry.label,\n color: dialog.entry.color,\n icon: dialog.entry.icon,\n }\n }\n return DEFAULT_FORM_VALUES\n }, [dialog])\n\n const tableTranslations = React.useMemo(() => ({\n title,\n valueColumn: t('customers.config.dictionaries.columns.value', 'Value'),\n labelColumn: t('customers.config.dictionaries.columns.label', 'Label'),\n appearanceColumn: t('customers.config.dictionaries.columns.appearance', 'Appearance'),\n addLabel: t('customers.config.dictionaries.actions.add', 'Add entry'),\n editLabel: t('customers.config.dictionaries.actions.edit', 'Edit'),\n deleteLabel: t('customers.config.dictionaries.actions.delete', 'Delete'),\n refreshLabel: t('customers.config.dictionaries.actions.refresh', 'Refresh'),\n inheritedLabel,\n inheritedTooltip,\n emptyLabel: t('customers.config.dictionaries.empty', 'No entries yet.'),\n searchPlaceholder,\n }), [inheritedLabel, inheritedTooltip, searchPlaceholder, t, title])\n\n const formTranslations = React.useMemo(() => ({\n title: dialog?.mode === 'edit'\n ? t('customers.config.dictionaries.dialog.editTitle', 'Edit entry')\n : t('customers.config.dictionaries.dialog.addTitle', 'Add entry'),\n valueLabel: t('customers.config.dictionaries.dialog.valueLabel', 'Value'),\n labelLabel: t('customers.config.dictionaries.dialog.labelLabel', 'Label'),\n saveLabel: t('customers.config.dictionaries.dialog.save', 'Save'),\n cancelLabel: t('customers.config.dictionaries.dialog.cancel', 'Cancel'),\n appearance: {\n colorLabel: t('customers.config.dictionaries.dialog.colorLabel', 'Color'),\n colorHelp: t('customers.config.dictionaries.dialog.colorHelp', 'Pick a highlight color for this entry.'),\n colorClearLabel: t('customers.config.dictionaries.dialog.colorClear', 'Remove color'),\n iconLabel: t('customers.config.dictionaries.dialog.iconLabel', 'Icon'),\n iconPlaceholder: t('customers.config.dictionaries.dialog.iconPlaceholder', 'Type an emoji or pick one of the suggestions.'),\n iconPickerTriggerLabel: t('customers.config.dictionaries.dialog.iconBrowse', 'Browse icons and emojis'),\n iconSearchPlaceholder: t('customers.config.dictionaries.dialog.iconSearchPlaceholder', 'Search icons or emojis\u2026'),\n iconSearchEmptyLabel: t('customers.config.dictionaries.dialog.iconSearchEmpty', 'No icons match your search.'),\n iconSuggestionsLabel: t('customers.config.dictionaries.dialog.iconSuggestions', 'Suggestions'),\n iconClearLabel: t('customers.config.dictionaries.dialog.iconClear', 'Remove icon'),\n previewEmptyLabel: t('customers.config.dictionaries.appearance.empty', 'None'),\n },\n }), [dialog, t])\n\n return (\n <section className=\"rounded border bg-card text-card-foreground shadow-sm\">\n <div className=\"border-b px-6 py-4 space-y-1\">\n <h2 className=\"text-lg font-medium\">{title}</h2>\n <p className=\"text-sm text-muted-foreground\">{description}</p>\n </div>\n <div className=\"px-2 py-4 sm:px-4\">\n <DictionaryTable\n entries={entries}\n loading={loading}\n canManage\n onCreate={handleCreate}\n onEdit={handleEdit}\n onDelete={handleDelete}\n onRefresh={loadEntries}\n translations={tableTranslations}\n />\n </div>\n <Dialog open={dialog !== null} onOpenChange={(open) => { if (!open) closeDialog() }}>\n <DialogContent className=\"max-w-lg\">\n <DialogHeader>\n <DialogTitle>{formTranslations.title}</DialogTitle>\n </DialogHeader>\n <DictionaryForm\n mode={dialog?.mode === 'edit' ? 'edit' : 'create'}\n initialValues={currentValues}\n onSubmit={submitForm}\n onCancel={closeDialog}\n submitting={submitting}\n translations={formTranslations}\n iconSuggestions={ICON_SUGGESTIONS}\n />\n </DialogContent>\n </Dialog>\n {ConfirmDialogElement}\n </section>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Button } from "@open-mercato/ui/primitives/button";
|
|
5
|
+
import { Input } from "@open-mercato/ui/primitives/input";
|
|
6
|
+
import { Label } from "@open-mercato/ui/primitives/label";
|
|
7
|
+
import { Checkbox } from "@open-mercato/ui/primitives/checkbox";
|
|
8
|
+
import {
|
|
9
|
+
Dialog,
|
|
10
|
+
DialogContent,
|
|
11
|
+
DialogFooter,
|
|
12
|
+
DialogHeader,
|
|
13
|
+
DialogTitle
|
|
14
|
+
} from "@open-mercato/ui/primitives/dialog";
|
|
15
|
+
import { flash } from "@open-mercato/ui/backend/FlashMessages";
|
|
16
|
+
import { apiCall, readApiResultOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
|
|
17
|
+
import { raiseCrudError } from "@open-mercato/ui/backend/utils/serverErrors";
|
|
18
|
+
import { useOrganizationScopeVersion } from "@open-mercato/shared/lib/frontend/useOrganizationScope";
|
|
19
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
20
|
+
import { useConfirmDialog } from "@open-mercato/ui/backend/confirm-dialog";
|
|
21
|
+
import { Spinner } from "@open-mercato/ui/primitives/spinner";
|
|
22
|
+
import {
|
|
23
|
+
AppearanceSelector
|
|
24
|
+
} from "@open-mercato/core/modules/dictionaries/components/AppearanceSelector";
|
|
25
|
+
import {
|
|
26
|
+
renderDictionaryColor,
|
|
27
|
+
renderDictionaryIcon
|
|
28
|
+
} from "@open-mercato/core/modules/dictionaries/components/dictionaryAppearance";
|
|
29
|
+
function normalizePipeline(raw) {
|
|
30
|
+
return {
|
|
31
|
+
id: typeof raw.id === "string" ? raw.id : "",
|
|
32
|
+
name: typeof raw.name === "string" ? raw.name : "",
|
|
33
|
+
isDefault: raw.isDefault === true || raw.is_default === true,
|
|
34
|
+
organizationId: typeof raw.organizationId === "string" ? raw.organizationId : typeof raw.organization_id === "string" ? raw.organization_id : "",
|
|
35
|
+
tenantId: typeof raw.tenantId === "string" ? raw.tenantId : typeof raw.tenant_id === "string" ? raw.tenant_id : ""
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function normalizeStage(raw) {
|
|
39
|
+
return {
|
|
40
|
+
id: typeof raw.id === "string" ? raw.id : "",
|
|
41
|
+
pipelineId: typeof raw.pipelineId === "string" ? raw.pipelineId : typeof raw.pipeline_id === "string" ? raw.pipeline_id : "",
|
|
42
|
+
label: typeof raw.label === "string" ? raw.label : "",
|
|
43
|
+
order: typeof raw.order === "number" ? raw.order : 0,
|
|
44
|
+
color: typeof raw.color === "string" && raw.color.trim().length ? raw.color.trim() : null,
|
|
45
|
+
icon: typeof raw.icon === "string" && raw.icon.trim().length ? raw.icon.trim() : null
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function PipelineSettings() {
|
|
49
|
+
const t = useT();
|
|
50
|
+
const scopeVersion = useOrganizationScopeVersion();
|
|
51
|
+
const { confirm, ConfirmDialogElement } = useConfirmDialog();
|
|
52
|
+
const [pipelines, setPipelines] = React.useState([]);
|
|
53
|
+
const [loadingPipelines, setLoadingPipelines] = React.useState(false);
|
|
54
|
+
const [pipelineDialog, setPipelineDialog] = React.useState(null);
|
|
55
|
+
const [pipelineForm, setPipelineForm] = React.useState({ name: "", isDefault: false });
|
|
56
|
+
const [submittingPipeline, setSubmittingPipeline] = React.useState(false);
|
|
57
|
+
const [expandedPipelineId, setExpandedPipelineId] = React.useState(null);
|
|
58
|
+
const [stages, setStages] = React.useState({});
|
|
59
|
+
const [loadingStages, setLoadingStages] = React.useState({});
|
|
60
|
+
const [stageDialog, setStageDialog] = React.useState(null);
|
|
61
|
+
const [stageForm, setStageForm] = React.useState({ label: "", color: null, icon: null });
|
|
62
|
+
const [submittingStage, setSubmittingStage] = React.useState(false);
|
|
63
|
+
const loadPipelines = React.useCallback(async () => {
|
|
64
|
+
setLoadingPipelines(true);
|
|
65
|
+
try {
|
|
66
|
+
const data = await readApiResultOrThrow(
|
|
67
|
+
"/api/customers/pipelines",
|
|
68
|
+
void 0,
|
|
69
|
+
{ errorMessage: t("customers.pipelines.errors.loadFailed", "Failed to load pipelines"), fallback: { items: [] } }
|
|
70
|
+
);
|
|
71
|
+
const items = Array.isArray(data?.items) ? data.items : [];
|
|
72
|
+
setPipelines(items.map((item) => normalizePipeline(item)));
|
|
73
|
+
} finally {
|
|
74
|
+
setLoadingPipelines(false);
|
|
75
|
+
}
|
|
76
|
+
}, [t]);
|
|
77
|
+
const loadStages = React.useCallback(async (pipelineId) => {
|
|
78
|
+
setLoadingStages((prev) => ({ ...prev, [pipelineId]: true }));
|
|
79
|
+
try {
|
|
80
|
+
const data = await readApiResultOrThrow(
|
|
81
|
+
`/api/customers/pipeline-stages?pipelineId=${encodeURIComponent(pipelineId)}`,
|
|
82
|
+
void 0,
|
|
83
|
+
{ errorMessage: t("customers.pipelines.errors.stagesLoadFailed", "Failed to load stages"), fallback: { items: [] } }
|
|
84
|
+
);
|
|
85
|
+
const items = Array.isArray(data?.items) ? data.items : [];
|
|
86
|
+
setStages((prev) => ({
|
|
87
|
+
...prev,
|
|
88
|
+
[pipelineId]: items.map((item) => normalizeStage(item))
|
|
89
|
+
}));
|
|
90
|
+
} finally {
|
|
91
|
+
setLoadingStages((prev) => ({ ...prev, [pipelineId]: false }));
|
|
92
|
+
}
|
|
93
|
+
}, [t]);
|
|
94
|
+
React.useEffect(() => {
|
|
95
|
+
void loadPipelines();
|
|
96
|
+
}, [loadPipelines, scopeVersion]);
|
|
97
|
+
React.useEffect(() => {
|
|
98
|
+
if (expandedPipelineId) {
|
|
99
|
+
void loadStages(expandedPipelineId);
|
|
100
|
+
}
|
|
101
|
+
}, [expandedPipelineId, loadStages]);
|
|
102
|
+
const openCreatePipeline = React.useCallback(() => {
|
|
103
|
+
setPipelineForm({ name: "", isDefault: false });
|
|
104
|
+
setPipelineDialog({ mode: "create" });
|
|
105
|
+
}, []);
|
|
106
|
+
const openEditPipeline = React.useCallback((pipeline) => {
|
|
107
|
+
setPipelineForm({ name: pipeline.name, isDefault: pipeline.isDefault });
|
|
108
|
+
setPipelineDialog({ mode: "edit", entry: pipeline });
|
|
109
|
+
}, []);
|
|
110
|
+
const closePipelineDialog = React.useCallback(() => {
|
|
111
|
+
setPipelineDialog(null);
|
|
112
|
+
}, []);
|
|
113
|
+
const handlePipelineSubmit = React.useCallback(async () => {
|
|
114
|
+
if (!pipelineForm.name.trim()) return;
|
|
115
|
+
setSubmittingPipeline(true);
|
|
116
|
+
try {
|
|
117
|
+
if (pipelineDialog?.mode === "create") {
|
|
118
|
+
const res = await apiCall("/api/customers/pipelines", {
|
|
119
|
+
method: "POST",
|
|
120
|
+
headers: { "content-type": "application/json" },
|
|
121
|
+
body: JSON.stringify({ name: pipelineForm.name.trim(), isDefault: pipelineForm.isDefault })
|
|
122
|
+
});
|
|
123
|
+
if (!res.ok) {
|
|
124
|
+
await raiseCrudError(res.response, t("customers.pipelines.errors.createFailed", "Failed to create pipeline"));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
flash(t("customers.pipelines.flash.created", "Pipeline created"), "success");
|
|
128
|
+
} else if (pipelineDialog?.mode === "edit") {
|
|
129
|
+
const res = await apiCall("/api/customers/pipelines", {
|
|
130
|
+
method: "PUT",
|
|
131
|
+
headers: { "content-type": "application/json" },
|
|
132
|
+
body: JSON.stringify({ id: pipelineDialog.entry.id, name: pipelineForm.name.trim(), isDefault: pipelineForm.isDefault })
|
|
133
|
+
});
|
|
134
|
+
if (!res.ok) {
|
|
135
|
+
await raiseCrudError(res.response, t("customers.pipelines.errors.updateFailed", "Failed to update pipeline"));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
flash(t("customers.pipelines.flash.updated", "Pipeline updated"), "success");
|
|
139
|
+
}
|
|
140
|
+
setPipelineDialog(null);
|
|
141
|
+
await loadPipelines();
|
|
142
|
+
} finally {
|
|
143
|
+
setSubmittingPipeline(false);
|
|
144
|
+
}
|
|
145
|
+
}, [pipelineDialog, pipelineForm, loadPipelines, t]);
|
|
146
|
+
const handleDeletePipeline = React.useCallback(async (pipeline) => {
|
|
147
|
+
const confirmed = await confirm({
|
|
148
|
+
title: t("customers.pipelines.confirm.deleteTitle", "Delete pipeline"),
|
|
149
|
+
text: t("customers.pipelines.confirm.deleteDesc", "Are you sure you want to delete this pipeline? This cannot be undone."),
|
|
150
|
+
confirmText: t("customers.pipelines.confirm.deleteConfirm", "Delete"),
|
|
151
|
+
variant: "destructive"
|
|
152
|
+
});
|
|
153
|
+
if (!confirmed) return;
|
|
154
|
+
const res = await apiCall("/api/customers/pipelines", {
|
|
155
|
+
method: "DELETE",
|
|
156
|
+
headers: { "content-type": "application/json" },
|
|
157
|
+
body: JSON.stringify({ id: pipeline.id })
|
|
158
|
+
});
|
|
159
|
+
if (!res.ok) {
|
|
160
|
+
const body = res.result ?? {};
|
|
161
|
+
const msg = typeof body.error === "string" ? body.error : t("customers.pipelines.errors.deleteFailed", "Failed to delete pipeline");
|
|
162
|
+
flash(msg, "error");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
flash(t("customers.pipelines.flash.deleted", "Pipeline deleted"), "success");
|
|
166
|
+
if (expandedPipelineId === pipeline.id) setExpandedPipelineId(null);
|
|
167
|
+
await loadPipelines();
|
|
168
|
+
}, [confirm, expandedPipelineId, loadPipelines, t]);
|
|
169
|
+
const toggleExpand = React.useCallback((pipelineId) => {
|
|
170
|
+
setExpandedPipelineId((prev) => prev === pipelineId ? null : pipelineId);
|
|
171
|
+
}, []);
|
|
172
|
+
const openCreateStage = React.useCallback((pipelineId) => {
|
|
173
|
+
setStageForm({ label: "", color: null, icon: null });
|
|
174
|
+
setStageDialog({ mode: "create", pipelineId });
|
|
175
|
+
}, []);
|
|
176
|
+
const openEditStage = React.useCallback((stage) => {
|
|
177
|
+
setStageForm({ label: stage.label, color: stage.color, icon: stage.icon });
|
|
178
|
+
setStageDialog({ mode: "edit", entry: stage });
|
|
179
|
+
}, []);
|
|
180
|
+
const closeStageDialog = React.useCallback(() => {
|
|
181
|
+
setStageDialog(null);
|
|
182
|
+
}, []);
|
|
183
|
+
const handleStageSubmit = React.useCallback(async () => {
|
|
184
|
+
if (!stageForm.label.trim()) return;
|
|
185
|
+
setSubmittingStage(true);
|
|
186
|
+
try {
|
|
187
|
+
const appearance = {};
|
|
188
|
+
if (stageForm.color) appearance.color = stageForm.color;
|
|
189
|
+
if (stageForm.icon) appearance.icon = stageForm.icon;
|
|
190
|
+
if (stageDialog?.mode === "create") {
|
|
191
|
+
const res = await apiCall("/api/customers/pipeline-stages", {
|
|
192
|
+
method: "POST",
|
|
193
|
+
headers: { "content-type": "application/json" },
|
|
194
|
+
body: JSON.stringify({ pipelineId: stageDialog.pipelineId, label: stageForm.label.trim(), ...appearance })
|
|
195
|
+
});
|
|
196
|
+
if (!res.ok) {
|
|
197
|
+
await raiseCrudError(res.response, t("customers.pipelines.errors.stageCreateFailed", "Failed to create stage"));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
flash(t("customers.pipelines.flash.stageCreated", "Stage created"), "success");
|
|
201
|
+
await loadStages(stageDialog.pipelineId);
|
|
202
|
+
} else if (stageDialog?.mode === "edit") {
|
|
203
|
+
const res = await apiCall("/api/customers/pipeline-stages", {
|
|
204
|
+
method: "PUT",
|
|
205
|
+
headers: { "content-type": "application/json" },
|
|
206
|
+
body: JSON.stringify({ id: stageDialog.entry.id, label: stageForm.label.trim(), ...appearance })
|
|
207
|
+
});
|
|
208
|
+
if (!res.ok) {
|
|
209
|
+
await raiseCrudError(res.response, t("customers.pipelines.errors.stageUpdateFailed", "Failed to update stage"));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
flash(t("customers.pipelines.flash.stageUpdated", "Stage updated"), "success");
|
|
213
|
+
await loadStages(stageDialog.entry.pipelineId);
|
|
214
|
+
}
|
|
215
|
+
setStageDialog(null);
|
|
216
|
+
} finally {
|
|
217
|
+
setSubmittingStage(false);
|
|
218
|
+
}
|
|
219
|
+
}, [stageDialog, stageForm, loadStages, t]);
|
|
220
|
+
const handleDeleteStage = React.useCallback(async (stage) => {
|
|
221
|
+
const confirmed = await confirm({
|
|
222
|
+
title: t("customers.pipelines.confirm.stageDeleteTitle", "Delete stage"),
|
|
223
|
+
text: t("customers.pipelines.confirm.stageDeleteDesc", "Are you sure you want to delete this stage?"),
|
|
224
|
+
confirmText: t("customers.pipelines.confirm.stageDeleteConfirm", "Delete"),
|
|
225
|
+
variant: "destructive"
|
|
226
|
+
});
|
|
227
|
+
if (!confirmed) return;
|
|
228
|
+
const res = await apiCall("/api/customers/pipeline-stages", {
|
|
229
|
+
method: "DELETE",
|
|
230
|
+
headers: { "content-type": "application/json" },
|
|
231
|
+
body: JSON.stringify({ id: stage.id })
|
|
232
|
+
});
|
|
233
|
+
if (!res.ok) {
|
|
234
|
+
const body = res.result ?? {};
|
|
235
|
+
const msg = typeof body.error === "string" ? body.error : t("customers.pipelines.errors.stageDeleteFailed", "Failed to delete stage");
|
|
236
|
+
flash(msg, "error");
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
flash(t("customers.pipelines.flash.stageDeleted", "Stage deleted"), "success");
|
|
240
|
+
await loadStages(stage.pipelineId);
|
|
241
|
+
}, [confirm, loadStages, t]);
|
|
242
|
+
const handleMoveStage = React.useCallback(async (stage, direction) => {
|
|
243
|
+
const pipelineStages = stages[stage.pipelineId] ?? [];
|
|
244
|
+
const idx = pipelineStages.findIndex((s) => s.id === stage.id);
|
|
245
|
+
if (idx < 0) return;
|
|
246
|
+
const swapIdx = direction === "up" ? idx - 1 : idx + 1;
|
|
247
|
+
if (swapIdx < 0 || swapIdx >= pipelineStages.length) return;
|
|
248
|
+
const reordered = [...pipelineStages];
|
|
249
|
+
const temp = reordered[idx];
|
|
250
|
+
reordered[idx] = reordered[swapIdx];
|
|
251
|
+
reordered[swapIdx] = temp;
|
|
252
|
+
const orderedStages = reordered.map((s, i) => ({ id: s.id, order: i }));
|
|
253
|
+
const res = await apiCall("/api/customers/pipeline-stages/reorder", {
|
|
254
|
+
method: "POST",
|
|
255
|
+
headers: { "content-type": "application/json" },
|
|
256
|
+
body: JSON.stringify({ stages: orderedStages })
|
|
257
|
+
});
|
|
258
|
+
if (!res.ok) {
|
|
259
|
+
flash(t("customers.pipelines.errors.reorderFailed", "Failed to reorder stages"), "error");
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
await loadStages(stage.pipelineId);
|
|
263
|
+
}, [stages, loadStages, t]);
|
|
264
|
+
const handleKeyDown = React.useCallback(
|
|
265
|
+
(handler) => (e) => {
|
|
266
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
|
267
|
+
e.preventDefault();
|
|
268
|
+
handler();
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
[]
|
|
272
|
+
);
|
|
273
|
+
const appearanceLabels = React.useMemo(() => ({
|
|
274
|
+
colorLabel: t("customers.pipelines.stageForm.color", "Color"),
|
|
275
|
+
colorClearLabel: t("customers.pipelines.stageForm.colorClear", "Remove color"),
|
|
276
|
+
iconLabel: t("customers.pipelines.stageForm.icon", "Icon"),
|
|
277
|
+
iconPlaceholder: t("customers.pipelines.stageForm.iconPlaceholder", "e.g. lucide:star"),
|
|
278
|
+
iconPickerTriggerLabel: t("customers.pipelines.stageForm.iconPicker", "Pick icon"),
|
|
279
|
+
iconSearchPlaceholder: t("customers.pipelines.stageForm.iconSearch", "Search icons\u2026"),
|
|
280
|
+
iconSearchEmptyLabel: t("customers.pipelines.stageForm.iconSearchEmpty", "No icons found"),
|
|
281
|
+
iconSuggestionsLabel: t("customers.pipelines.stageForm.iconSuggestions", "Suggestions"),
|
|
282
|
+
iconClearLabel: t("customers.pipelines.stageForm.iconClear", "Remove icon"),
|
|
283
|
+
previewEmptyLabel: t("customers.pipelines.stageForm.previewEmpty", "No appearance set")
|
|
284
|
+
}), [t]);
|
|
285
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
286
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
287
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
288
|
+
/* @__PURE__ */ jsx("h3", { className: "text-base font-semibold", children: t("customers.pipelines.title", "Sales Pipelines") }),
|
|
289
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("customers.pipelines.description", "Manage sales pipelines and their stages.") })
|
|
290
|
+
] }),
|
|
291
|
+
/* @__PURE__ */ jsx(Button, { size: "sm", onClick: openCreatePipeline, children: t("customers.pipelines.actions.create", "Add pipeline") })
|
|
292
|
+
] }),
|
|
293
|
+
loadingPipelines ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [
|
|
294
|
+
/* @__PURE__ */ jsx(Spinner, { className: "h-4 w-4" }),
|
|
295
|
+
t("customers.pipelines.loading", "Loading pipelines\u2026")
|
|
296
|
+
] }) : pipelines.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("customers.pipelines.empty", "No pipelines yet. Create one to get started.") }) : /* @__PURE__ */ jsx("div", { className: "divide-y divide-border rounded-md border", children: pipelines.map((pipeline) => {
|
|
297
|
+
const isExpanded = expandedPipelineId === pipeline.id;
|
|
298
|
+
const pipelineStages = stages[pipeline.id] ?? [];
|
|
299
|
+
const isLoadingStages = loadingStages[pipeline.id] ?? false;
|
|
300
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
301
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 px-4 py-3", children: [
|
|
302
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
303
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: pipeline.name }),
|
|
304
|
+
pipeline.isDefault ? /* @__PURE__ */ jsx("span", { className: "rounded-full bg-primary/10 px-2 py-0.5 text-xs text-primary", children: t("customers.pipelines.defaultBadge", "Default") }) : null
|
|
305
|
+
] }),
|
|
306
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
307
|
+
/* @__PURE__ */ jsx(
|
|
308
|
+
Button,
|
|
309
|
+
{
|
|
310
|
+
variant: "ghost",
|
|
311
|
+
size: "sm",
|
|
312
|
+
onClick: () => toggleExpand(pipeline.id),
|
|
313
|
+
children: isExpanded ? t("customers.pipelines.actions.hideStages", "Hide stages") : t("customers.pipelines.actions.manageStages", "Manage stages")
|
|
314
|
+
}
|
|
315
|
+
),
|
|
316
|
+
/* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: () => openEditPipeline(pipeline), children: t("customers.pipelines.actions.edit", "Edit") }),
|
|
317
|
+
/* @__PURE__ */ jsx(
|
|
318
|
+
Button,
|
|
319
|
+
{
|
|
320
|
+
variant: "ghost",
|
|
321
|
+
size: "sm",
|
|
322
|
+
className: "text-destructive hover:text-destructive",
|
|
323
|
+
onClick: () => void handleDeletePipeline(pipeline),
|
|
324
|
+
children: t("customers.pipelines.actions.delete", "Delete")
|
|
325
|
+
}
|
|
326
|
+
)
|
|
327
|
+
] })
|
|
328
|
+
] }),
|
|
329
|
+
isExpanded ? /* @__PURE__ */ jsxs("div", { className: "border-t border-border bg-muted/30 px-4 py-3", children: [
|
|
330
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-3 flex items-center justify-between", children: [
|
|
331
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide", children: t("customers.pipelines.stages.title", "Stages") }),
|
|
332
|
+
/* @__PURE__ */ jsx(Button, { size: "sm", variant: "outline", onClick: () => openCreateStage(pipeline.id), children: t("customers.pipelines.stages.add", "Add stage") })
|
|
333
|
+
] }),
|
|
334
|
+
isLoadingStages ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [
|
|
335
|
+
/* @__PURE__ */ jsx(Spinner, { className: "h-3 w-3" }),
|
|
336
|
+
t("customers.pipelines.stages.loading", "Loading\u2026")
|
|
337
|
+
] }) : pipelineStages.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("customers.pipelines.stages.empty", "No stages yet.") }) : /* @__PURE__ */ jsx("div", { className: "divide-y divide-border rounded-md border bg-background", children: pipelineStages.map((stage, idx) => /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 px-3 py-2", children: [
|
|
338
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
339
|
+
/* @__PURE__ */ jsx("span", { className: "w-5 text-center text-xs text-muted-foreground", children: idx + 1 }),
|
|
340
|
+
stage.color ? renderDictionaryColor(stage.color, "h-3 w-3 rounded-full") : null,
|
|
341
|
+
stage.icon ? renderDictionaryIcon(stage.icon, "h-4 w-4") : null,
|
|
342
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm", children: stage.label })
|
|
343
|
+
] }),
|
|
344
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
345
|
+
/* @__PURE__ */ jsx(
|
|
346
|
+
Button,
|
|
347
|
+
{
|
|
348
|
+
variant: "ghost",
|
|
349
|
+
size: "icon",
|
|
350
|
+
className: "h-7 w-7",
|
|
351
|
+
disabled: idx === 0,
|
|
352
|
+
onClick: () => void handleMoveStage(stage, "up"),
|
|
353
|
+
title: t("customers.pipelines.stages.moveUp", "Move up"),
|
|
354
|
+
children: "\u2191"
|
|
355
|
+
}
|
|
356
|
+
),
|
|
357
|
+
/* @__PURE__ */ jsx(
|
|
358
|
+
Button,
|
|
359
|
+
{
|
|
360
|
+
variant: "ghost",
|
|
361
|
+
size: "icon",
|
|
362
|
+
className: "h-7 w-7",
|
|
363
|
+
disabled: idx === pipelineStages.length - 1,
|
|
364
|
+
onClick: () => void handleMoveStage(stage, "down"),
|
|
365
|
+
title: t("customers.pipelines.stages.moveDown", "Move down"),
|
|
366
|
+
children: "\u2193"
|
|
367
|
+
}
|
|
368
|
+
),
|
|
369
|
+
/* @__PURE__ */ jsx(
|
|
370
|
+
Button,
|
|
371
|
+
{
|
|
372
|
+
variant: "ghost",
|
|
373
|
+
size: "sm",
|
|
374
|
+
onClick: () => openEditStage(stage),
|
|
375
|
+
children: t("customers.pipelines.stages.edit", "Edit")
|
|
376
|
+
}
|
|
377
|
+
),
|
|
378
|
+
/* @__PURE__ */ jsx(
|
|
379
|
+
Button,
|
|
380
|
+
{
|
|
381
|
+
variant: "ghost",
|
|
382
|
+
size: "sm",
|
|
383
|
+
className: "text-destructive hover:text-destructive",
|
|
384
|
+
onClick: () => void handleDeleteStage(stage),
|
|
385
|
+
children: t("customers.pipelines.stages.delete", "Delete")
|
|
386
|
+
}
|
|
387
|
+
)
|
|
388
|
+
] })
|
|
389
|
+
] }, stage.id)) })
|
|
390
|
+
] }) : null
|
|
391
|
+
] }, pipeline.id);
|
|
392
|
+
}) }),
|
|
393
|
+
/* @__PURE__ */ jsx(Dialog, { open: pipelineDialog !== null, onOpenChange: (open) => {
|
|
394
|
+
if (!open) closePipelineDialog();
|
|
395
|
+
}, children: /* @__PURE__ */ jsxs(DialogContent, { onKeyDown: handleKeyDown(handlePipelineSubmit), children: [
|
|
396
|
+
/* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, { children: pipelineDialog?.mode === "create" ? t("customers.pipelines.dialog.createTitle", "Create pipeline") : t("customers.pipelines.dialog.editTitle", "Edit pipeline") }) }),
|
|
397
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-4 py-2", children: [
|
|
398
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
399
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: "pipeline-name", children: t("customers.pipelines.form.name", "Name") }),
|
|
400
|
+
/* @__PURE__ */ jsx(
|
|
401
|
+
Input,
|
|
402
|
+
{
|
|
403
|
+
id: "pipeline-name",
|
|
404
|
+
value: pipelineForm.name,
|
|
405
|
+
onChange: (e) => setPipelineForm((prev) => ({ ...prev, name: e.target.value })),
|
|
406
|
+
placeholder: t("customers.pipelines.form.namePlaceholder", "e.g. New Business"),
|
|
407
|
+
autoFocus: true
|
|
408
|
+
}
|
|
409
|
+
)
|
|
410
|
+
] }),
|
|
411
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
412
|
+
/* @__PURE__ */ jsx(
|
|
413
|
+
Checkbox,
|
|
414
|
+
{
|
|
415
|
+
id: "pipeline-default",
|
|
416
|
+
checked: pipelineForm.isDefault,
|
|
417
|
+
onCheckedChange: (checked) => setPipelineForm((prev) => ({ ...prev, isDefault: checked === true }))
|
|
418
|
+
}
|
|
419
|
+
),
|
|
420
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: "pipeline-default", className: "cursor-pointer", children: t("customers.pipelines.form.isDefault", "Set as default pipeline") })
|
|
421
|
+
] })
|
|
422
|
+
] }),
|
|
423
|
+
/* @__PURE__ */ jsxs(DialogFooter, { children: [
|
|
424
|
+
/* @__PURE__ */ jsx(Button, { variant: "outline", onClick: closePipelineDialog, disabled: submittingPipeline, children: t("customers.pipelines.dialog.cancel", "Cancel") }),
|
|
425
|
+
/* @__PURE__ */ jsxs(Button, { onClick: () => void handlePipelineSubmit(), disabled: submittingPipeline || !pipelineForm.name.trim(), children: [
|
|
426
|
+
submittingPipeline ? /* @__PURE__ */ jsx(Spinner, { className: "mr-2 h-4 w-4" }) : null,
|
|
427
|
+
t("customers.pipelines.dialog.save", "Save")
|
|
428
|
+
] })
|
|
429
|
+
] })
|
|
430
|
+
] }) }),
|
|
431
|
+
ConfirmDialogElement,
|
|
432
|
+
/* @__PURE__ */ jsx(Dialog, { open: stageDialog !== null, onOpenChange: (open) => {
|
|
433
|
+
if (!open) closeStageDialog();
|
|
434
|
+
}, children: /* @__PURE__ */ jsxs(DialogContent, { onKeyDown: handleKeyDown(handleStageSubmit), children: [
|
|
435
|
+
/* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, { children: stageDialog?.mode === "create" ? t("customers.pipelines.stageDialog.createTitle", "Add stage") : t("customers.pipelines.stageDialog.editTitle", "Edit stage") }) }),
|
|
436
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-4 py-2", children: [
|
|
437
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
438
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: "stage-label", children: t("customers.pipelines.stageForm.label", "Label") }),
|
|
439
|
+
/* @__PURE__ */ jsx(
|
|
440
|
+
Input,
|
|
441
|
+
{
|
|
442
|
+
id: "stage-label",
|
|
443
|
+
value: stageForm.label,
|
|
444
|
+
onChange: (e) => setStageForm((prev) => ({ ...prev, label: e.target.value })),
|
|
445
|
+
placeholder: t("customers.pipelines.stageForm.labelPlaceholder", "e.g. Discovery"),
|
|
446
|
+
autoFocus: true
|
|
447
|
+
}
|
|
448
|
+
)
|
|
449
|
+
] }),
|
|
450
|
+
/* @__PURE__ */ jsx(
|
|
451
|
+
AppearanceSelector,
|
|
452
|
+
{
|
|
453
|
+
color: stageForm.color,
|
|
454
|
+
icon: stageForm.icon,
|
|
455
|
+
onColorChange: (next) => setStageForm((prev) => ({ ...prev, color: next })),
|
|
456
|
+
onIconChange: (next) => setStageForm((prev) => ({ ...prev, icon: next })),
|
|
457
|
+
labels: appearanceLabels
|
|
458
|
+
}
|
|
459
|
+
)
|
|
460
|
+
] }),
|
|
461
|
+
/* @__PURE__ */ jsxs(DialogFooter, { children: [
|
|
462
|
+
/* @__PURE__ */ jsx(Button, { variant: "outline", onClick: closeStageDialog, disabled: submittingStage, children: t("customers.pipelines.stageDialog.cancel", "Cancel") }),
|
|
463
|
+
/* @__PURE__ */ jsxs(Button, { onClick: () => void handleStageSubmit(), disabled: submittingStage || !stageForm.label.trim(), children: [
|
|
464
|
+
submittingStage ? /* @__PURE__ */ jsx(Spinner, { className: "mr-2 h-4 w-4" }) : null,
|
|
465
|
+
t("customers.pipelines.stageDialog.save", "Save")
|
|
466
|
+
] })
|
|
467
|
+
] })
|
|
468
|
+
] }) })
|
|
469
|
+
] });
|
|
470
|
+
}
|
|
471
|
+
export {
|
|
472
|
+
PipelineSettings as default
|
|
473
|
+
};
|
|
474
|
+
//# sourceMappingURL=PipelineSettings.js.map
|