@open-mercato/core 0.6.1-develop.3256.1.fe3dec2464 → 0.6.1-develop.3260.1.dc719cac84

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.
Files changed (120) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/helpers/integration/authFixtures.js.map +1 -1
  3. package/dist/helpers/integration/authUi.js +10 -2
  4. package/dist/helpers/integration/authUi.js.map +2 -2
  5. package/dist/helpers/integration/salesUi.js +3 -1
  6. package/dist/helpers/integration/salesUi.js.map +2 -2
  7. package/dist/modules/api_docs/frontend/docs/api/Explorer.js +10 -1
  8. package/dist/modules/api_docs/frontend/docs/api/Explorer.js.map +2 -2
  9. package/dist/modules/audit_logs/lib/display-helpers.js +28 -27
  10. package/dist/modules/audit_logs/lib/display-helpers.js.map +2 -2
  11. package/dist/modules/auth/api/users/route.js +21 -3
  12. package/dist/modules/auth/api/users/route.js.map +2 -2
  13. package/dist/modules/auth/backend/users/[id]/edit/page.js +6 -1
  14. package/dist/modules/auth/backend/users/[id]/edit/page.js.map +2 -2
  15. package/dist/modules/auth/backend/users/create/page.js +4 -1
  16. package/dist/modules/auth/backend/users/create/page.js.map +2 -2
  17. package/dist/modules/auth/backend/users/page.js +56 -2
  18. package/dist/modules/auth/backend/users/page.js.map +3 -3
  19. package/dist/modules/auth/commands/users.js +14 -0
  20. package/dist/modules/auth/commands/users.js.map +2 -2
  21. package/dist/modules/auth/data/validators.js +8 -0
  22. package/dist/modules/auth/data/validators.js.map +2 -2
  23. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +1 -1
  24. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
  25. package/dist/modules/customers/backend/customers/deals/[id]/page.js +15 -9
  26. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  27. package/dist/modules/customers/backend/customers/deals/pipeline/page.js +26 -3
  28. package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
  29. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +1 -1
  30. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
  31. package/dist/modules/customers/components/PipelineSettings.js +108 -99
  32. package/dist/modules/customers/components/PipelineSettings.js.map +2 -2
  33. package/dist/modules/customers/components/detail/AssignRoleDialog.js +13 -5
  34. package/dist/modules/customers/components/detail/AssignRoleDialog.js.map +2 -2
  35. package/dist/modules/customers/components/detail/EntityTagsDialog.js +10 -2
  36. package/dist/modules/customers/components/detail/EntityTagsDialog.js.map +2 -2
  37. package/dist/modules/customers/components/detail/PersonCompaniesSection.js +15 -8
  38. package/dist/modules/customers/components/detail/PersonCompaniesSection.js.map +2 -2
  39. package/dist/modules/customers/components/detail/RolesSection.js +35 -22
  40. package/dist/modules/customers/components/detail/RolesSection.js.map +2 -2
  41. package/dist/modules/customers/components/detail/TasksSection.js +2 -2
  42. package/dist/modules/customers/components/detail/TasksSection.js.map +2 -2
  43. package/dist/modules/customers/components/detail/schedule/DateTimeFields.js +73 -64
  44. package/dist/modules/customers/components/detail/schedule/DateTimeFields.js.map +2 -2
  45. package/dist/modules/customers/components/linking/LinkEntityDialog.js +11 -2
  46. package/dist/modules/customers/components/linking/LinkEntityDialog.js.map +2 -2
  47. package/dist/modules/data_sync/backend/data-sync/page.js +2 -9
  48. package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
  49. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js +14 -29
  50. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js.map +2 -2
  51. package/dist/modules/dictionaries/components/DictionariesManager.js +10 -2
  52. package/dist/modules/dictionaries/components/DictionariesManager.js.map +2 -2
  53. package/dist/modules/integrations/backend/integrations/[id]/page.js +79 -95
  54. package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
  55. package/dist/modules/payment_gateways/backend/payment-gateways/page.js +49 -70
  56. package/dist/modules/payment_gateways/backend/payment-gateways/page.js.map +2 -2
  57. package/dist/modules/planner/components/AvailabilityRuleSetForm.js +1 -1
  58. package/dist/modules/planner/components/AvailabilityRuleSetForm.js.map +1 -1
  59. package/dist/modules/planner/components/AvailabilityRulesEditor.js +19 -19
  60. package/dist/modules/planner/components/AvailabilityRulesEditor.js.map +2 -2
  61. package/dist/modules/resources/components/ResourceCrudForm.js +1 -1
  62. package/dist/modules/resources/components/ResourceCrudForm.js.map +1 -1
  63. package/dist/modules/resources/components/ResourceTypeCrudForm.js +1 -1
  64. package/dist/modules/resources/components/ResourceTypeCrudForm.js.map +2 -2
  65. package/dist/modules/sales/components/documents/ReturnDialog.js +11 -8
  66. package/dist/modules/sales/components/documents/ReturnDialog.js.map +2 -2
  67. package/dist/modules/staff/backend/staff/profile/create/page.js +1 -1
  68. package/dist/modules/staff/backend/staff/profile/create/page.js.map +2 -2
  69. package/dist/modules/staff/components/TeamForm.js +1 -1
  70. package/dist/modules/staff/components/TeamForm.js.map +2 -2
  71. package/dist/modules/staff/components/TeamMemberForm.js +1 -1
  72. package/dist/modules/staff/components/TeamMemberForm.js.map +1 -1
  73. package/dist/modules/staff/components/TeamRoleForm.js +1 -1
  74. package/dist/modules/staff/components/TeamRoleForm.js.map +2 -2
  75. package/package.json +5 -5
  76. package/src/helpers/integration/authFixtures.ts +1 -1
  77. package/src/helpers/integration/authUi.ts +11 -2
  78. package/src/helpers/integration/salesUi.ts +10 -1
  79. package/src/modules/api_docs/frontend/docs/api/Explorer.tsx +7 -3
  80. package/src/modules/audit_logs/lib/display-helpers.tsx +22 -14
  81. package/src/modules/auth/api/users/route.ts +22 -3
  82. package/src/modules/auth/backend/users/[id]/edit/page.tsx +9 -1
  83. package/src/modules/auth/backend/users/create/page.tsx +6 -2
  84. package/src/modules/auth/backend/users/page.tsx +60 -3
  85. package/src/modules/auth/commands/users.ts +15 -0
  86. package/src/modules/auth/data/validators.ts +8 -0
  87. package/src/modules/auth/i18n/de.json +4 -0
  88. package/src/modules/auth/i18n/en.json +4 -0
  89. package/src/modules/auth/i18n/es.json +4 -0
  90. package/src/modules/auth/i18n/pl.json +4 -0
  91. package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +1 -1
  92. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +12 -13
  93. package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +17 -13
  94. package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +1 -1
  95. package/src/modules/customers/components/PipelineSettings.tsx +95 -95
  96. package/src/modules/customers/components/detail/AssignRoleDialog.tsx +7 -4
  97. package/src/modules/customers/components/detail/EntityTagsDialog.tsx +13 -8
  98. package/src/modules/customers/components/detail/PersonCompaniesSection.tsx +16 -12
  99. package/src/modules/customers/components/detail/RolesSection.tsx +34 -31
  100. package/src/modules/customers/components/detail/TasksSection.tsx +16 -8
  101. package/src/modules/customers/components/detail/schedule/DateTimeFields.tsx +77 -51
  102. package/src/modules/customers/components/linking/LinkEntityDialog.tsx +7 -3
  103. package/src/modules/data_sync/backend/data-sync/page.tsx +4 -7
  104. package/src/modules/data_sync/backend/data-sync/runs/[id]/page.tsx +19 -54
  105. package/src/modules/dictionaries/components/DictionariesManager.tsx +7 -4
  106. package/src/modules/integrations/backend/integrations/[id]/page.tsx +101 -146
  107. package/src/modules/payment_gateways/backend/payment-gateways/page.tsx +60 -103
  108. package/src/modules/planner/components/AvailabilityRuleSetForm.tsx +1 -1
  109. package/src/modules/planner/components/AvailabilityRulesEditor.tsx +17 -17
  110. package/src/modules/resources/components/ResourceCrudForm.tsx +1 -1
  111. package/src/modules/resources/components/ResourceTypeCrudForm.tsx +1 -1
  112. package/src/modules/sales/components/documents/ReturnDialog.tsx +10 -7
  113. package/src/modules/sales/i18n/de.json +2 -0
  114. package/src/modules/sales/i18n/en.json +2 -0
  115. package/src/modules/sales/i18n/es.json +2 -0
  116. package/src/modules/sales/i18n/pl.json +2 -0
  117. package/src/modules/staff/backend/staff/profile/create/page.tsx +1 -1
  118. package/src/modules/staff/components/TeamForm.tsx +1 -1
  119. package/src/modules/staff/components/TeamMemberForm.tsx +1 -1
  120. package/src/modules/staff/components/TeamRoleForm.tsx +1 -1
@@ -172,6 +172,7 @@
172
172
  "auth.users.form.errors.notFound": "User not found",
173
173
  "auth.users.form.errors.widgetsAssign": "Failed to assign dashboard widgets to the new user",
174
174
  "auth.users.form.field.email": "Email",
175
+ "auth.users.form.field.name": "Display name",
175
176
  "auth.users.form.field.newPassword": "New Password",
176
177
  "auth.users.form.field.organization": "Organization",
177
178
  "auth.users.form.field.password": "Password",
@@ -190,8 +191,11 @@
190
191
  "auth.users.form.loading": "Loading user data...",
191
192
  "auth.users.form.title.create": "Create User",
192
193
  "auth.users.form.title.edit": "Edit User",
194
+ "auth.users.list.columns.name": "Display name",
193
195
  "auth.users.list.error.delete": "Failed to delete user",
194
196
  "auth.users.list.error.load": "Failed to load users",
197
+ "auth.users.list.filters.name": "Display name",
198
+ "auth.users.list.filters.namePlaceholder": "Filter by display name",
195
199
  "auth.users.widgets.errors.load": "Unable to load dashboard widgets. You can configure them later from the user page.",
196
200
  "auth.users.widgets.loading": "Loading widgets…",
197
201
  "auth.users.widgets.mode.hint": "New users inherit widgets from their assigned roles. Override to pick a custom set.",
@@ -172,6 +172,7 @@
172
172
  "auth.users.form.errors.notFound": "Usuario no encontrado",
173
173
  "auth.users.form.errors.widgetsAssign": "No se pudieron asignar los widgets del panel al nuevo usuario",
174
174
  "auth.users.form.field.email": "Correo electrónico",
175
+ "auth.users.form.field.name": "Nombre visible",
175
176
  "auth.users.form.field.newPassword": "Nueva contraseña",
176
177
  "auth.users.form.field.organization": "Organización",
177
178
  "auth.users.form.field.password": "Contraseña",
@@ -190,8 +191,11 @@
190
191
  "auth.users.form.loading": "Cargando datos del usuario...",
191
192
  "auth.users.form.title.create": "Crear usuario",
192
193
  "auth.users.form.title.edit": "Editar usuario",
194
+ "auth.users.list.columns.name": "Nombre visible",
193
195
  "auth.users.list.error.delete": "No se pudo eliminar el usuario",
194
196
  "auth.users.list.error.load": "No se pudieron cargar los usuarios",
197
+ "auth.users.list.filters.name": "Nombre visible",
198
+ "auth.users.list.filters.namePlaceholder": "Filtrar por nombre visible",
195
199
  "auth.users.widgets.errors.load": "No se pudieron cargar los widgets del panel. Puedes configurarlos más tarde desde la página del usuario.",
196
200
  "auth.users.widgets.loading": "Cargando widgets…",
197
201
  "auth.users.widgets.mode.hint": "Los nuevos usuarios heredan los widgets de las funciones asignadas. Sobrescribe para elegir un conjunto personalizado.",
@@ -172,6 +172,7 @@
172
172
  "auth.users.form.errors.notFound": "Nie znaleziono użytkownika",
173
173
  "auth.users.form.errors.widgetsAssign": "Nie udało się przypisać widżetów pulpitu do nowego użytkownika",
174
174
  "auth.users.form.field.email": "Email",
175
+ "auth.users.form.field.name": "Nazwa wyświetlana",
175
176
  "auth.users.form.field.newPassword": "Nowe hasło",
176
177
  "auth.users.form.field.organization": "Organizacja",
177
178
  "auth.users.form.field.password": "Hasło",
@@ -190,8 +191,11 @@
190
191
  "auth.users.form.loading": "Ładowanie danych użytkownika...",
191
192
  "auth.users.form.title.create": "Utwórz użytkownika",
192
193
  "auth.users.form.title.edit": "Edytuj użytkownika",
194
+ "auth.users.list.columns.name": "Nazwa wyświetlana",
193
195
  "auth.users.list.error.delete": "Nie udało się usunąć użytkownika",
194
196
  "auth.users.list.error.load": "Nie udało się wczytać użytkowników",
197
+ "auth.users.list.filters.name": "Nazwa wyświetlana",
198
+ "auth.users.list.filters.namePlaceholder": "Filtruj po nazwie wyświetlanej",
195
199
  "auth.users.widgets.errors.load": "Nie udało się załadować widżetów pulpitu. Możesz skonfigurować je później na stronie użytkownika.",
196
200
  "auth.users.widgets.loading": "Ładowanie widżetów…",
197
201
  "auth.users.widgets.mode.hint": "Nowi użytkownicy dziedziczą widżety z przypisanych ról. Zastąp wybór, aby wskazać własny zestaw.",
@@ -438,7 +438,7 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
438
438
  onSubmit={handleFormSubmit}
439
439
  onDelete={handleDelete}
440
440
  hideFooterActions
441
- collapsibleGroups={{ pageType: 'company-v2', chevronPosition: 'left' }}
441
+ collapsibleGroups={{ pageType: 'company-v2', chevronPosition: 'right' }}
442
442
  sortableGroups={{ pageType: 'company-v2' }}
443
443
  onDirtyChange={setIsDirty}
444
444
  />
@@ -2,7 +2,8 @@
2
2
 
3
3
  import * as React from 'react'
4
4
  import Link from 'next/link'
5
- import { Building2, Users } from 'lucide-react'
5
+ import { Building2, UserSearch, Users } from 'lucide-react'
6
+ import { EmptyState } from '@open-mercato/ui/primitives/empty-state'
6
7
  import { useRouter, useSearchParams } from 'next/navigation'
7
8
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
8
9
  import { Button } from '@open-mercato/ui/primitives/button'
@@ -337,7 +338,7 @@ export default function DealDetailPage({ params }: { params?: { id?: string } })
337
338
  showVersionHistory={false}
338
339
  showCancelAction={false}
339
340
  onDirtyChange={setIsDirty}
340
- collapsibleGroups={{ pageType: 'deal-detail-v3', chevronPosition: 'left' }}
341
+ collapsibleGroups={{ pageType: 'deal-detail-v3', chevronPosition: 'right' }}
341
342
  sortableGroups={{ pageType: 'deal-detail-v3' }}
342
343
  initialValues={{
343
344
  ...data.deal,
@@ -424,17 +425,15 @@ export default function DealDetailPage({ params }: { params?: { id?: string } })
424
425
  onScheduleRequested={openSchedule}
425
426
  />
426
427
  ) : (
427
- <div className="rounded-[10px] border border-dashed border-border bg-muted/10 px-5 py-5">
428
- <div className="text-sm font-semibold text-foreground">
429
- {t('customers.deals.detail.activities.selectEntityRequiredTitle', 'Choose a person or company to continue')}
430
- </div>
431
- <div className="mt-1 text-sm text-muted-foreground">
432
- {t(
433
- 'customers.deals.detail.activities.selectEntityRequiredDescription',
434
- 'Select the customer record that should receive new deal activities before logging or scheduling anything.',
435
- )}
436
- </div>
437
- </div>
428
+ <EmptyState
429
+ size="sm"
430
+ icon={<UserSearch className="h-8 w-8" aria-hidden="true" />}
431
+ title={t('customers.deals.detail.activities.selectEntityRequiredTitle', 'Choose a person or company to continue')}
432
+ description={t(
433
+ 'customers.deals.detail.activities.selectEntityRequiredDescription',
434
+ 'Select the customer record that should receive new deal activities before logging or scheduling anything.',
435
+ )}
436
+ />
438
437
  )}
439
438
  <PlannedActivitiesSection
440
439
  activities={plannedActivities}
@@ -11,8 +11,10 @@ import {
11
11
  SelectTrigger,
12
12
  SelectValue,
13
13
  } from '@open-mercato/ui/primitives/select'
14
+ import { Layers, MoveRight, Workflow } from 'lucide-react'
14
15
  import { Spinner } from '@open-mercato/ui/primitives/spinner'
15
16
  import { ErrorNotice } from '@open-mercato/ui/primitives/ErrorNotice'
17
+ import { EmptyState } from '@open-mercato/ui/primitives/empty-state'
16
18
  import { apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
17
19
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
18
20
  import { useT } from '@open-mercato/shared/lib/i18n/context'
@@ -528,11 +530,11 @@ export default function SalesPipelinePage(): React.ReactElement {
528
530
  </div>
529
531
 
530
532
  {!selectedPipelineId ? (
531
- <div className="flex h-[50vh] items-center justify-center">
532
- <span className="text-sm text-muted-foreground">
533
- {translate('customers.deals.pipeline.noPipeline', 'No pipeline selected. Create a pipeline in settings.')}
534
- </span>
535
- </div>
533
+ <EmptyState
534
+ icon={<Workflow className="h-8 w-8" aria-hidden="true" />}
535
+ title={translate('customers.deals.pipeline.noPipeline', 'No pipeline selected. Create a pipeline in settings.')}
536
+ className="h-[50vh] w-full"
537
+ />
536
538
  ) : dealsQuery.isLoading ? (
537
539
  <div className="flex h-[50vh] items-center justify-center">
538
540
  <Spinner />
@@ -561,11 +563,11 @@ export default function SalesPipelinePage(): React.ReactElement {
561
563
 
562
564
  <div className="flex flex-col gap-4 pb-6 md:flex-row md:overflow-x-auto">
563
565
  {stages.length === 0 ? (
564
- <div className="flex h-[50vh] w-full items-center justify-center rounded-lg border border-dashed border-border bg-muted/30">
565
- <span className="text-sm text-muted-foreground">
566
- {translate('customers.deals.pipeline.noStages', 'Define pipeline stages to start tracking deals.')}
567
- </span>
568
- </div>
566
+ <EmptyState
567
+ icon={<Layers className="h-8 w-8" aria-hidden="true" />}
568
+ title={translate('customers.deals.pipeline.noStages', 'Define pipeline stages to start tracking deals.')}
569
+ className="h-[50vh] w-full"
570
+ />
569
571
  ) : (
570
572
  stages.map((stage) => {
571
573
  const stageKey = stage.value ?? null
@@ -584,9 +586,11 @@ export default function SalesPipelinePage(): React.ReactElement {
584
586
  {renderLaneHeader(stage, laneDeals.length)}
585
587
  <div className="flex flex-1 flex-col gap-3 overflow-y-auto px-4 py-3">
586
588
  {sortedLaneDeals.length === 0 ? (
587
- <div className="rounded-md border border-dashed border-border bg-muted/30 p-4 text-center text-xs text-muted-foreground">
588
- {translate('customers.deals.pipeline.emptyLane', 'No deals in this stage yet.')}
589
- </div>
589
+ <EmptyState
590
+ size="sm"
591
+ icon={<MoveRight className="h-6 w-6" aria-hidden="true" />}
592
+ title={translate('customers.deals.pipeline.emptyLane', 'No deals in this stage yet.')}
593
+ />
590
594
  ) : (
591
595
  sortedLaneDeals.map((deal) => {
592
596
  const isDragging = draggingId === deal.id
@@ -446,7 +446,7 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
446
446
  onSubmit={handleFormSubmit}
447
447
  onDelete={handleFormDelete}
448
448
  hideFooterActions
449
- collapsibleGroups={{ pageType: 'person-v2', chevronPosition: 'left' }}
449
+ collapsibleGroups={{ pageType: 'person-v2', chevronPosition: 'right' }}
450
450
  sortableGroups={{ pageType: 'person-v2' }}
451
451
  onDirtyChange={setIsDirty}
452
452
  />
@@ -19,6 +19,12 @@ import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/u
19
19
  import { useT } from '@open-mercato/shared/lib/i18n/context'
20
20
  import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
21
21
  import { Spinner } from '@open-mercato/ui/primitives/spinner'
22
+ import {
23
+ Accordion,
24
+ AccordionContent,
25
+ AccordionItem,
26
+ AccordionTrigger,
27
+ } from '@open-mercato/ui/primitives/accordion'
22
28
  import {
23
29
  AppearanceSelector,
24
30
  type AppearanceSelectorLabels,
@@ -207,10 +213,6 @@ export default function PipelineSettings(): React.ReactElement {
207
213
  await loadPipelines()
208
214
  }, [confirm, expandedPipelineId, loadPipelines, t])
209
215
 
210
- const toggleExpand = React.useCallback((pipelineId: string) => {
211
- setExpandedPipelineId((prev) => (prev === pipelineId ? null : pipelineId))
212
- }, [])
213
-
214
216
  const openCreateStage = React.useCallback((pipelineId: string) => {
215
217
  setStageForm({ label: '', color: null, icon: null })
216
218
  setStageDialog({ mode: 'create', pipelineId })
@@ -359,35 +361,107 @@ export default function PipelineSettings(): React.ReactElement {
359
361
  {t('customers.pipelines.empty', 'No pipelines yet. Create one to get started.')}
360
362
  </p>
361
363
  ) : (
362
- <div className="divide-y divide-border rounded-md border">
364
+ <Accordion
365
+ type="single"
366
+ collapsible
367
+ value={expandedPipelineId ?? ''}
368
+ onValueChange={(next) => setExpandedPipelineId(next || null)}
369
+ className="space-y-2"
370
+ >
363
371
  {pipelines.map((pipeline) => {
364
- const isExpanded = expandedPipelineId === pipeline.id
365
372
  const pipelineStages = stages[pipeline.id] ?? []
366
373
  const isLoadingStages = loadingStages[pipeline.id] ?? false
367
374
 
368
375
  return (
369
- <div key={pipeline.id}>
370
- <div className="flex items-center justify-between gap-3 px-4 py-3">
371
- <div className="flex items-center gap-2">
376
+ <AccordionItem key={pipeline.id} value={pipeline.id}>
377
+ <AccordionTrigger triggerIcon="chevron">
378
+ <span className="flex items-center gap-2">
372
379
  <span className="text-sm font-medium">{pipeline.name}</span>
373
380
  {pipeline.isDefault ? (
374
381
  <span className="rounded-full bg-primary/10 px-2 py-0.5 text-xs text-primary">
375
382
  {t('customers.pipelines.defaultBadge', 'Default')}
376
383
  </span>
377
384
  ) : null}
385
+ </span>
386
+ </AccordionTrigger>
387
+ <AccordionContent>
388
+ <div className="mb-3 flex items-center justify-between">
389
+ <span className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
390
+ {t('customers.pipelines.stages.title', 'Stages')}
391
+ </span>
392
+ <Button size="sm" variant="outline" onClick={() => openCreateStage(pipeline.id)}>
393
+ {t('customers.pipelines.stages.add', 'Add stage')}
394
+ </Button>
378
395
  </div>
379
- <div className="flex items-center gap-2">
396
+
397
+ {isLoadingStages ? (
398
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
399
+ <Spinner className="h-3 w-3" />
400
+ {t('customers.pipelines.stages.loading', 'Loading…')}
401
+ </div>
402
+ ) : pipelineStages.length === 0 ? (
403
+ <p className="text-sm text-muted-foreground">
404
+ {t('customers.pipelines.stages.empty', 'No stages yet.')}
405
+ </p>
406
+ ) : (
407
+ <div className="divide-y divide-border rounded-md border bg-background">
408
+ {pipelineStages.map((stage, idx) => (
409
+ <div key={stage.id} className="flex items-center justify-between gap-3 px-3 py-2">
410
+ <div className="flex items-center gap-2">
411
+ <span className="w-5 text-center text-xs text-muted-foreground">{idx + 1}</span>
412
+ {stage.color ? renderDictionaryColor(stage.color, 'h-3 w-3 rounded-full') : null}
413
+ {stage.icon ? renderDictionaryIcon(stage.icon, 'h-4 w-4') : null}
414
+ <span className="text-sm">{stage.label}</span>
415
+ </div>
416
+ <div className="flex items-center gap-1">
417
+ <Button
418
+ variant="ghost"
419
+ size="icon"
420
+ className="h-7 w-7"
421
+ disabled={idx === 0}
422
+ onClick={() => void handleMoveStage(stage, 'up')}
423
+ title={t('customers.pipelines.stages.moveUp', 'Move up')}
424
+ >
425
+ ↑
426
+ </Button>
427
+ <Button
428
+ variant="ghost"
429
+ size="icon"
430
+ className="h-7 w-7"
431
+ disabled={idx === pipelineStages.length - 1}
432
+ onClick={() => void handleMoveStage(stage, 'down')}
433
+ title={t('customers.pipelines.stages.moveDown', 'Move down')}
434
+ >
435
+ ↓
436
+ </Button>
437
+ <Button
438
+ variant="ghost"
439
+ size="sm"
440
+ onClick={() => openEditStage(stage)}
441
+ >
442
+ {t('customers.pipelines.stages.edit', 'Edit')}
443
+ </Button>
444
+ <Button
445
+ variant="ghost"
446
+ size="sm"
447
+ className="text-destructive hover:text-destructive"
448
+ onClick={() => void handleDeleteStage(stage)}
449
+ >
450
+ {t('customers.pipelines.stages.delete', 'Delete')}
451
+ </Button>
452
+ </div>
453
+ </div>
454
+ ))}
455
+ </div>
456
+ )}
457
+
458
+ <div className="mt-4 flex items-center justify-end gap-2 border-t pt-3">
380
459
  <Button
381
460
  variant="ghost"
382
461
  size="sm"
383
- onClick={() => toggleExpand(pipeline.id)}
462
+ onClick={() => openEditPipeline(pipeline)}
384
463
  >
385
- {isExpanded
386
- ? t('customers.pipelines.actions.hideStages', 'Hide stages')
387
- : t('customers.pipelines.actions.manageStages', 'Manage stages')}
388
- </Button>
389
- <Button variant="ghost" size="sm" onClick={() => openEditPipeline(pipeline)}>
390
- {t('customers.pipelines.actions.edit', 'Edit')}
464
+ {t('customers.pipelines.actions.edit', 'Edit pipeline')}
391
465
  </Button>
392
466
  <Button
393
467
  variant="ghost"
@@ -395,88 +469,14 @@ export default function PipelineSettings(): React.ReactElement {
395
469
  className="text-destructive hover:text-destructive"
396
470
  onClick={() => void handleDeletePipeline(pipeline)}
397
471
  >
398
- {t('customers.pipelines.actions.delete', 'Delete')}
472
+ {t('customers.pipelines.actions.delete', 'Delete pipeline')}
399
473
  </Button>
400
474
  </div>
401
- </div>
402
-
403
- {isExpanded ? (
404
- <div className="border-t border-border bg-muted/30 px-4 py-3">
405
- <div className="mb-3 flex items-center justify-between">
406
- <span className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
407
- {t('customers.pipelines.stages.title', 'Stages')}
408
- </span>
409
- <Button size="sm" variant="outline" onClick={() => openCreateStage(pipeline.id)}>
410
- {t('customers.pipelines.stages.add', 'Add stage')}
411
- </Button>
412
- </div>
413
-
414
- {isLoadingStages ? (
415
- <div className="flex items-center gap-2 text-sm text-muted-foreground">
416
- <Spinner className="h-3 w-3" />
417
- {t('customers.pipelines.stages.loading', 'Loading…')}
418
- </div>
419
- ) : pipelineStages.length === 0 ? (
420
- <p className="text-sm text-muted-foreground">
421
- {t('customers.pipelines.stages.empty', 'No stages yet.')}
422
- </p>
423
- ) : (
424
- <div className="divide-y divide-border rounded-md border bg-background">
425
- {pipelineStages.map((stage, idx) => (
426
- <div key={stage.id} className="flex items-center justify-between gap-3 px-3 py-2">
427
- <div className="flex items-center gap-2">
428
- <span className="w-5 text-center text-xs text-muted-foreground">{idx + 1}</span>
429
- {stage.color ? renderDictionaryColor(stage.color, 'h-3 w-3 rounded-full') : null}
430
- {stage.icon ? renderDictionaryIcon(stage.icon, 'h-4 w-4') : null}
431
- <span className="text-sm">{stage.label}</span>
432
- </div>
433
- <div className="flex items-center gap-1">
434
- <Button
435
- variant="ghost"
436
- size="icon"
437
- className="h-7 w-7"
438
- disabled={idx === 0}
439
- onClick={() => void handleMoveStage(stage, 'up')}
440
- title={t('customers.pipelines.stages.moveUp', 'Move up')}
441
- >
442
- ↑
443
- </Button>
444
- <Button
445
- variant="ghost"
446
- size="icon"
447
- className="h-7 w-7"
448
- disabled={idx === pipelineStages.length - 1}
449
- onClick={() => void handleMoveStage(stage, 'down')}
450
- title={t('customers.pipelines.stages.moveDown', 'Move down')}
451
- >
452
- ↓
453
- </Button>
454
- <Button
455
- variant="ghost"
456
- size="sm"
457
- onClick={() => openEditStage(stage)}
458
- >
459
- {t('customers.pipelines.stages.edit', 'Edit')}
460
- </Button>
461
- <Button
462
- variant="ghost"
463
- size="sm"
464
- className="text-destructive hover:text-destructive"
465
- onClick={() => void handleDeleteStage(stage)}
466
- >
467
- {t('customers.pipelines.stages.delete', 'Delete')}
468
- </Button>
469
- </div>
470
- </div>
471
- ))}
472
- </div>
473
- )}
474
- </div>
475
- ) : null}
476
- </div>
475
+ </AccordionContent>
476
+ </AccordionItem>
477
477
  )
478
478
  })}
479
- </div>
479
+ </Accordion>
480
480
  )}
481
481
 
482
482
  {/* Pipeline Dialog */}
@@ -2,7 +2,8 @@
2
2
 
3
3
  import * as React from 'react'
4
4
  import Link from 'next/link'
5
- import { Search, Check, Settings2 } from 'lucide-react'
5
+ import { Search, Check, CheckCheck, Settings2 } from 'lucide-react'
6
+ import { EmptyState } from '@open-mercato/ui/primitives/empty-state'
6
7
  import { useT } from '@open-mercato/shared/lib/i18n/context'
7
8
  import { Button } from '@open-mercato/ui/primitives/button'
8
9
  import { Badge } from '@open-mercato/ui/primitives/badge'
@@ -419,12 +420,14 @@ export function AssignRoleDialog({
419
420
  </div>
420
421
 
421
422
  {!availableRoleTypes.length ? (
422
- <div className="rounded-lg border border-dashed border-border/80 px-4 py-6 text-sm text-muted-foreground">
423
- {t(
423
+ <EmptyState
424
+ size="sm"
425
+ icon={<CheckCheck className="h-8 w-8" aria-hidden="true" />}
426
+ title={t(
424
427
  'customers.roles.dialog.noAvailableRoles',
425
428
  'All available role types are already assigned.',
426
429
  )}
427
- </div>
430
+ />
428
431
  ) : null}
429
432
  </div>
430
433
  ) : null}
@@ -1,7 +1,8 @@
1
1
  'use client'
2
2
 
3
3
  import * as React from 'react'
4
- import { Check, Plus, Search, SlidersHorizontal, Tag, X } from 'lucide-react'
4
+ import { Check, Plus, Search, SearchX, SlidersHorizontal, Tag, X } from 'lucide-react'
5
+ import { EmptyState } from '@open-mercato/ui/primitives/empty-state'
5
6
  import { VisuallyHidden } from '@radix-ui/react-visually-hidden'
6
7
  import { useT } from '@open-mercato/shared/lib/i18n/context'
7
8
  import { cn, slugifyTagLabel } from '@open-mercato/shared/lib/utils'
@@ -1238,13 +1239,17 @@ export function EntityTagsDialog({
1238
1239
  {t('customers.personTags.loading', 'Loading...')}
1239
1240
  </div>
1240
1241
  ) : (
1241
- <div className="rounded-lg border border-dashed border-border bg-background px-4 py-6 text-center text-sm text-muted-foreground">
1242
- {searchValue.trim().length > 0
1243
- ? t('customers.personTags.emptySearchResults', 'No options match the current search.')
1244
- : activeCategory.source === 'dictionary'
1245
- ? t('customers.personTags.emptyDictionaryCategory', 'No options are configured for this category yet.')
1246
- : t('customers.personTags.emptyCategory', 'No items have been added for this category yet.')}
1247
- </div>
1242
+ <EmptyState
1243
+ size="sm"
1244
+ icon={<SearchX className="h-8 w-8" aria-hidden="true" />}
1245
+ title={
1246
+ searchValue.trim().length > 0
1247
+ ? t('customers.personTags.emptySearchResults', 'No options match the current search.')
1248
+ : activeCategory.source === 'dictionary'
1249
+ ? t('customers.personTags.emptyDictionaryCategory', 'No options are configured for this category yet.')
1250
+ : t('customers.personTags.emptyCategory', 'No items have been added for this category yet.')
1251
+ }
1252
+ />
1248
1253
  )}
1249
1254
 
1250
1255
  {activeCategory.supportsCreate ? (
@@ -1,7 +1,8 @@
1
1
  'use client'
2
2
 
3
3
  import * as React from 'react'
4
- import { ArrowLeft, ArrowRight, Link2 } from 'lucide-react'
4
+ import { ArrowLeft, ArrowRight, Building2, Link2 } from 'lucide-react'
5
+ import { EmptyState } from '@open-mercato/ui/primitives/empty-state'
5
6
  import { useT } from '@open-mercato/shared/lib/i18n/context'
6
7
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
7
8
  import { apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
@@ -437,17 +438,20 @@ export function PersonCompaniesSection({
437
438
  ))}
438
439
  </div>
439
440
  ) : items.length === 0 ? (
440
- <div className="rounded-2xl border border-dashed border-border/60 px-6 py-12 text-center text-sm text-muted-foreground">
441
- {search.trim().length
442
- ? t(
443
- 'customers.people.detail.companies.noSearchResults',
444
- 'No linked companies match your search.',
445
- )
446
- : t(
447
- 'customers.people.detail.empty.companies',
448
- 'No company linked to this person.',
449
- )}
450
- </div>
441
+ <EmptyState
442
+ icon={<Building2 className="h-8 w-8" aria-hidden="true" />}
443
+ title={
444
+ search.trim().length
445
+ ? t(
446
+ 'customers.people.detail.companies.noSearchResults',
447
+ 'No linked companies match your search.',
448
+ )
449
+ : t(
450
+ 'customers.people.detail.empty.companies',
451
+ 'No company linked to this person.',
452
+ )
453
+ }
454
+ />
451
455
  ) : (
452
456
  <>
453
457
  <div className="space-y-4">
@@ -2,7 +2,8 @@
2
2
 
3
3
  import * as React from 'react'
4
4
  import Link from 'next/link'
5
- import { Plus } from 'lucide-react'
5
+ import { Plus, Settings, UserCheck } from 'lucide-react'
6
+ import { EmptyState } from '@open-mercato/ui/primitives/empty-state'
6
7
  import { useT } from '@open-mercato/shared/lib/i18n/context'
7
8
  import { apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
8
9
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
@@ -254,37 +255,39 @@ export function RolesSection({ entityType, entityId, entityName }: RolesSectionP
254
255
  )}
255
256
  </div>
256
257
  ) : hasConfiguredRoleTypes ? (
257
- <div className="rounded-lg border border-dashed p-4 text-center">
258
- <p className="text-xs text-muted-foreground">
259
- {t('customers.roles.emptyState', 'No roles assigned yet. Click below to assign a person.')}
260
- </p>
261
- <Button
262
- type="button"
263
- variant="outline"
264
- size="sm"
265
- className="mt-2"
266
- onClick={() => openDialog(null)}
267
- >
268
- {t('customers.roles.choosePerson', 'Choose person')}
269
- </Button>
270
- </div>
258
+ <EmptyState
259
+ size="sm"
260
+ icon={<UserCheck className="h-8 w-8" aria-hidden="true" />}
261
+ title={t('customers.roles.emptyState', 'No roles assigned yet. Click below to assign a person.')}
262
+ actions={
263
+ <Button
264
+ type="button"
265
+ variant="outline"
266
+ size="sm"
267
+ onClick={() => openDialog(null)}
268
+ >
269
+ <Plus className="h-4 w-4" aria-hidden="true" />
270
+ {t('customers.roles.choosePerson', 'Choose person')}
271
+ </Button>
272
+ }
273
+ />
271
274
  ) : (
272
- <div className="rounded-lg border border-dashed p-4 text-center">
273
- <div className="text-sm font-medium text-foreground">
274
- {t('customers.roles.noRoleTypesTitle', 'No role types configured')}
275
- </div>
276
- <p className="mt-2 text-xs text-muted-foreground">
277
- {t(
278
- 'customers.roles.noRoleTypesDescription',
279
- 'Create role types in Customers config before assigning owners here.',
280
- )}
281
- </p>
282
- <Button asChild type="button" variant="outline" size="sm" className="mt-3">
283
- <Link href="/backend/config/customers">
284
- {t('customers.roles.configureRoleTypes', 'Configure role types')}
285
- </Link>
286
- </Button>
287
- </div>
275
+ <EmptyState
276
+ size="sm"
277
+ icon={<Settings className="h-8 w-8" aria-hidden="true" />}
278
+ title={t('customers.roles.noRoleTypesTitle', 'No role types configured')}
279
+ description={t(
280
+ 'customers.roles.noRoleTypesDescription',
281
+ 'Create role types in Customers config before assigning owners here.',
282
+ )}
283
+ actions={
284
+ <Button asChild type="button" variant="outline" size="sm">
285
+ <Link href="/backend/config/customers">
286
+ {t('customers.roles.configureRoleTypes', 'Configure role types')}
287
+ </Link>
288
+ </Button>
289
+ }
290
+ />
288
291
  )}
289
292
 
290
293
  {hasConfiguredRoleTypes ? (