@open-mercato/core 0.6.5-develop.5382.1.f542de69af → 0.6.6-develop.5412.1.e2a52b14f0

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 (138) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/helpers/integration/crmFixtures.js +4 -0
  3. package/dist/helpers/integration/crmFixtures.js.map +2 -2
  4. package/dist/modules/attachments/api/route.js +2 -0
  5. package/dist/modules/attachments/api/route.js.map +2 -2
  6. package/dist/modules/attachments/lib/access.js +18 -0
  7. package/dist/modules/attachments/lib/access.js.map +2 -2
  8. package/dist/modules/auth/services/rbacService.js +3 -2
  9. package/dist/modules/auth/services/rbacService.js.map +2 -2
  10. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js +0 -3
  11. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js.map +2 -2
  12. package/dist/modules/customers/api/deals/route.js +43 -2
  13. package/dist/modules/customers/api/deals/route.js.map +2 -2
  14. package/dist/modules/customers/api/deals/summary/route.js +402 -0
  15. package/dist/modules/customers/api/deals/summary/route.js.map +7 -0
  16. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealActivities.js +16 -5
  17. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealActivities.js.map +2 -2
  18. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealData.js +22 -5
  19. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealData.js.map +2 -2
  20. package/dist/modules/customers/backend/customers/deals/[id]/page.js +12 -2
  21. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  22. package/dist/modules/customers/backend/customers/deals/page.js +221 -56
  23. package/dist/modules/customers/backend/customers/deals/page.js.map +3 -3
  24. package/dist/modules/customers/backend/customers/deals/pipeline/page.js +1 -1
  25. package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
  26. package/dist/modules/customers/cli.js +15 -9
  27. package/dist/modules/customers/cli.js.map +2 -2
  28. package/dist/modules/customers/components/DealsKpiStrip.js +282 -0
  29. package/dist/modules/customers/components/DealsKpiStrip.js.map +7 -0
  30. package/dist/modules/customers/components/detail/ConfirmDealLostDialog.js +0 -1
  31. package/dist/modules/customers/components/detail/ConfirmDealLostDialog.js.map +2 -2
  32. package/dist/modules/customers/components/detail/DealForm.js +100 -17
  33. package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
  34. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +1 -2
  35. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
  36. package/dist/modules/customers/components/kpi/PipelineStageBar.js +63 -0
  37. package/dist/modules/customers/components/kpi/PipelineStageBar.js.map +7 -0
  38. package/dist/modules/customers/lib/dealsMetrics.js +82 -0
  39. package/dist/modules/customers/lib/dealsMetrics.js.map +7 -0
  40. package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js +2 -1
  41. package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js.map +2 -2
  42. package/dist/modules/directory/utils/organizationScope.js +59 -27
  43. package/dist/modules/directory/utils/organizationScope.js.map +2 -2
  44. package/dist/modules/entities/api/entities.js +7 -0
  45. package/dist/modules/entities/api/entities.js.map +2 -2
  46. package/dist/modules/entities/api/records.js +26 -15
  47. package/dist/modules/entities/api/records.js.map +2 -2
  48. package/dist/modules/entities/backend/entities/user/[entityId]/records/[recordId]/page.js +14 -0
  49. package/dist/modules/entities/backend/entities/user/[entityId]/records/[recordId]/page.js.map +2 -2
  50. package/dist/modules/entities/backend/entities/user/[entityId]/records/create/page.js +14 -0
  51. package/dist/modules/entities/backend/entities/user/[entityId]/records/create/page.js.map +2 -2
  52. package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js +12 -0
  53. package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js.map +2 -2
  54. package/dist/modules/entities/components/useRecordsEntityGuard.js +30 -0
  55. package/dist/modules/entities/components/useRecordsEntityGuard.js.map +7 -0
  56. package/dist/modules/query_index/lib/engine.js +4 -2
  57. package/dist/modules/query_index/lib/engine.js.map +2 -2
  58. package/dist/modules/staff/api/team-members.js +9 -2
  59. package/dist/modules/staff/api/team-members.js.map +2 -2
  60. package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.js +24 -1
  61. package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.js.map +2 -2
  62. package/dist/modules/staff/backend/staff/team-members/[id]/page.js +11 -6
  63. package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
  64. package/dist/modules/staff/commands/team-members.js +1 -1
  65. package/dist/modules/staff/commands/team-members.js.map +2 -2
  66. package/dist/modules/staff/components/TeamMemberForm.js +1 -1
  67. package/dist/modules/staff/components/TeamMemberForm.js.map +2 -2
  68. package/dist/modules/staff/lib/scheduleSwitch.js +23 -0
  69. package/dist/modules/staff/lib/scheduleSwitch.js.map +7 -0
  70. package/dist/modules/workflows/backend/definitions/create/page.js +1 -2
  71. package/dist/modules/workflows/backend/definitions/create/page.js.map +2 -2
  72. package/dist/modules/workflows/backend/definitions/visual-editor/page.js +1 -2
  73. package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
  74. package/dist/modules/workflows/components/DefinitionTriggersEditor.js +1 -2
  75. package/dist/modules/workflows/components/DefinitionTriggersEditor.js.map +2 -2
  76. package/dist/modules/workflows/components/NodeEditDialog.js +4 -13
  77. package/dist/modules/workflows/components/NodeEditDialog.js.map +2 -2
  78. package/dist/modules/workflows/components/NodeEditDialogCrudForm.js +4 -13
  79. package/dist/modules/workflows/components/NodeEditDialogCrudForm.js.map +2 -2
  80. package/dist/modules/workflows/components/WorkflowGraphImpl.js +1 -4
  81. package/dist/modules/workflows/components/WorkflowGraphImpl.js.map +2 -2
  82. package/dist/modules/workflows/components/fields/FormFieldArrayEditor.js +2 -5
  83. package/dist/modules/workflows/components/fields/FormFieldArrayEditor.js.map +2 -2
  84. package/package.json +8 -8
  85. package/src/helpers/integration/crmFixtures.ts +21 -1
  86. package/src/modules/attachments/AGENTS.md +79 -0
  87. package/src/modules/attachments/api/route.ts +2 -0
  88. package/src/modules/attachments/lib/access.ts +36 -0
  89. package/src/modules/auth/services/rbacService.ts +11 -2
  90. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.tsx +0 -3
  91. package/src/modules/customers/api/deals/route.ts +51 -2
  92. package/src/modules/customers/api/deals/summary/route.ts +496 -0
  93. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealActivities.ts +28 -6
  94. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealData.ts +33 -6
  95. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +17 -2
  96. package/src/modules/customers/backend/customers/deals/page.tsx +254 -66
  97. package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +1 -2
  98. package/src/modules/customers/cli.ts +15 -15
  99. package/src/modules/customers/components/DealsKpiStrip.tsx +389 -0
  100. package/src/modules/customers/components/detail/ConfirmDealLostDialog.tsx +0 -1
  101. package/src/modules/customers/components/detail/DealForm.tsx +121 -19
  102. package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +1 -2
  103. package/src/modules/customers/components/kpi/PipelineStageBar.tsx +77 -0
  104. package/src/modules/customers/i18n/de.json +43 -0
  105. package/src/modules/customers/i18n/en.json +43 -0
  106. package/src/modules/customers/i18n/es.json +43 -0
  107. package/src/modules/customers/i18n/pl.json +43 -0
  108. package/src/modules/customers/lib/dealsMetrics.ts +159 -0
  109. package/src/modules/directory/subscribers/invalidateOrgScopeCache.ts +3 -1
  110. package/src/modules/directory/utils/organizationScope.ts +85 -30
  111. package/src/modules/entities/api/entities.ts +11 -0
  112. package/src/modules/entities/api/records.ts +46 -25
  113. package/src/modules/entities/backend/entities/user/[entityId]/records/[recordId]/page.tsx +15 -0
  114. package/src/modules/entities/backend/entities/user/[entityId]/records/create/page.tsx +15 -0
  115. package/src/modules/entities/backend/entities/user/[entityId]/records/page.tsx +23 -0
  116. package/src/modules/entities/components/useRecordsEntityGuard.ts +41 -0
  117. package/src/modules/entities/i18n/de.json +1 -0
  118. package/src/modules/entities/i18n/en.json +1 -0
  119. package/src/modules/entities/i18n/es.json +1 -0
  120. package/src/modules/entities/i18n/pl.json +1 -0
  121. package/src/modules/query_index/lib/engine.ts +11 -5
  122. package/src/modules/staff/api/team-members.ts +9 -2
  123. package/src/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.ts +31 -1
  124. package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +18 -8
  125. package/src/modules/staff/commands/team-members.ts +5 -2
  126. package/src/modules/staff/components/TeamMemberForm.tsx +4 -1
  127. package/src/modules/staff/i18n/de.json +1 -0
  128. package/src/modules/staff/i18n/en.json +1 -0
  129. package/src/modules/staff/i18n/es.json +1 -0
  130. package/src/modules/staff/i18n/pl.json +1 -0
  131. package/src/modules/staff/lib/scheduleSwitch.ts +46 -0
  132. package/src/modules/workflows/backend/definitions/create/page.tsx +1 -2
  133. package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +1 -2
  134. package/src/modules/workflows/components/DefinitionTriggersEditor.tsx +1 -2
  135. package/src/modules/workflows/components/NodeEditDialog.tsx +1 -4
  136. package/src/modules/workflows/components/NodeEditDialogCrudForm.tsx +4 -7
  137. package/src/modules/workflows/components/WorkflowGraphImpl.tsx +1 -2
  138. package/src/modules/workflows/components/fields/FormFieldArrayEditor.tsx +2 -3
@@ -19,14 +19,19 @@ import { coalesceLastOperations } from "@open-mercato/ui/backend/operations/stor
19
19
  import { flash } from "@open-mercato/ui/backend/FlashMessages";
20
20
  import { RowActions } from "@open-mercato/ui/backend/RowActions";
21
21
  import { Button } from "@open-mercato/ui/primitives/button";
22
+ import { IconButton } from "@open-mercato/ui/primitives/icon-button";
23
+ import { StatusBadge } from "@open-mercato/ui/primitives/status-badge";
24
+ import { Avatar, AvatarStack } from "@open-mercato/ui/primitives/avatar";
25
+ import { Tag } from "@open-mercato/ui/primitives/tag";
26
+ import { SimpleTooltip } from "@open-mercato/ui/primitives/tooltip";
27
+ import { Briefcase, AlertTriangle, X } from "lucide-react";
28
+ import { formatRelativeTime } from "@open-mercato/shared/lib/time";
22
29
  import { ViewTabsRow } from "./pipeline/components/ViewTabsRow.js";
30
+ import { DealsKpiStrip } from "../../../components/DealsKpiStrip.js";
23
31
  import { E } from "../../../../../generated/entities.ids.generated.js";
24
32
  import { useOrganizationScopeVersion } from "@open-mercato/shared/lib/frontend/useOrganizationScope";
25
33
  import { useT } from "@open-mercato/shared/lib/i18n/context";
26
34
  import { useConfirmDialog } from "@open-mercato/ui/backend/confirm-dialog";
27
- import {
28
- DictionaryValue
29
- } from "../../../lib/dictionaries.js";
30
35
  import {
31
36
  ensureCustomerDictionary,
32
37
  invalidateCustomerDictionary
@@ -106,25 +111,30 @@ function extractIdsFromParams(params, key) {
106
111
  const values = params.getAll(key);
107
112
  return normalizeIdCandidates(values);
108
113
  }
109
- function formatCurrency(amount, currency, fallback) {
110
- if (typeof amount !== "number" || Number.isNaN(amount)) return fallback;
111
- try {
112
- if (currency && currency.trim().length) {
113
- const formatter2 = new Intl.NumberFormat(void 0, { style: "currency", currency });
114
- return formatter2.format(amount);
115
- }
116
- const formatter = new Intl.NumberFormat(void 0, { style: "decimal", maximumFractionDigits: 2 });
117
- return formatter.format(amount);
118
- } catch {
119
- return currency ? `${amount} ${currency}` : String(amount);
120
- }
121
- }
122
114
  function formatDateValue(value, fallback) {
123
115
  if (!value) return fallback;
124
116
  const date = new Date(value);
125
117
  if (Number.isNaN(date.getTime())) return fallback;
126
118
  return date.toLocaleDateString();
127
119
  }
120
+ const STATUS_BADGE_VARIANTS = /* @__PURE__ */ new Set([
121
+ "success",
122
+ "warning",
123
+ "error",
124
+ "info",
125
+ "neutral"
126
+ ]);
127
+ function coerceStatusBadgeVariant(tone) {
128
+ if (tone && STATUS_BADGE_VARIANTS.has(tone)) {
129
+ return tone;
130
+ }
131
+ return "neutral";
132
+ }
133
+ const groupedAmountFormatter = new Intl.NumberFormat(void 0, { maximumFractionDigits: 0 });
134
+ function formatGroupedAmount(amount) {
135
+ if (typeof amount !== "number" || Number.isNaN(amount)) return null;
136
+ return groupedAmountFormatter.format(amount);
137
+ }
128
138
  function CustomersDealsPage() {
129
139
  const t = useT();
130
140
  const { confirm, ConfirmDialogElement } = useConfirmDialog();
@@ -146,6 +156,7 @@ function CustomersDealsPage() {
146
156
  const [isLoading, setIsLoading] = React.useState(false);
147
157
  const [reloadToken, setReloadToken] = React.useState(0);
148
158
  const [pendingDeleteId, setPendingDeleteId] = React.useState(null);
159
+ const [needsAttentionOnly, setNeedsAttentionOnly] = React.useState(() => searchParams?.get("needsAttention") === "true");
149
160
  const initialFilterTree = React.useMemo(() => {
150
161
  if (!searchParams) return createEmptyTree();
151
162
  const record = {};
@@ -260,12 +271,13 @@ function CustomersDealsPage() {
260
271
  if (search.trim().length) params.set("search", search.trim());
261
272
  if (selectedPersonIds.length) params.set("personId", selectedPersonIds.join(","));
262
273
  if (selectedCompanyIds.length) params.set("companyId", selectedCompanyIds.join(","));
274
+ if (needsAttentionOnly) params.set("needsAttention", "true");
263
275
  const advancedParams = serializeTree(advancedFilterState);
264
276
  for (const [key, val] of Object.entries(advancedParams)) {
265
277
  params.set(key, val);
266
278
  }
267
279
  return params.toString();
268
- }, [advancedFilterState, page, pageSize, search, selectedCompanyIds, selectedPersonIds, sorting]);
280
+ }, [advancedFilterState, needsAttentionOnly, page, pageSize, search, selectedCompanyIds, selectedPersonIds, sorting]);
269
281
  const currentParams = React.useMemo(
270
282
  () => Object.fromEntries(new URLSearchParams(queryParams)),
271
283
  [queryParams]
@@ -327,6 +339,7 @@ function CustomersDealsPage() {
327
339
  if (search.trim().length) params.set("search", search.trim());
328
340
  if (selectedPersonIds.length) selectedPersonIds.forEach((id) => params.append("personId", id));
329
341
  if (selectedCompanyIds.length) selectedCompanyIds.forEach((id) => params.append("companyId", id));
342
+ if (needsAttentionOnly) params.set("needsAttention", "true");
330
343
  if (page > 1) params.set("page", String(page));
331
344
  const advancedParams = serializeTree(advancedFilterState);
332
345
  for (const [key, val] of Object.entries(advancedParams)) {
@@ -336,7 +349,7 @@ function CustomersDealsPage() {
336
349
  if (queryRef.current === next) return;
337
350
  queryRef.current = next;
338
351
  router.replace(next ? `${pathname}?${next}` : pathname, { scroll: false });
339
- }, [pathname, router, page, search, selectedPersonIds, selectedCompanyIds, advancedFilterState]);
352
+ }, [pathname, router, page, search, selectedPersonIds, selectedCompanyIds, needsAttentionOnly, advancedFilterState]);
340
353
  const handleRefresh = React.useCallback(() => {
341
354
  void Promise.all([
342
355
  invalidateCustomerDictionary(queryClient, "deal-statuses"),
@@ -406,6 +419,14 @@ function CustomersDealsPage() {
406
419
  setPageSize(newSize);
407
420
  setPage(1);
408
421
  }, []);
422
+ const handleNeedsAttentionFilter = React.useCallback(() => {
423
+ setNeedsAttentionOnly(true);
424
+ setPage(1);
425
+ }, []);
426
+ const handleNeedsAttentionClear = React.useCallback(() => {
427
+ setNeedsAttentionOnly(false);
428
+ setPage(1);
429
+ }, []);
409
430
  const handleBulkDelete = React.useCallback(async (selectedRows) => {
410
431
  const confirmed = await confirm({
411
432
  title: t("customers.deals.list.bulkDelete.title", "Delete {count} deals?", { count: selectedRows.length }),
@@ -486,13 +507,22 @@ function CustomersDealsPage() {
486
507
  });
487
508
  const currentUserId = useCurrentUserId();
488
509
  const [ownerFilterOptions, setOwnerFilterOptions] = React.useState([]);
510
+ const [ownerNames, setOwnerNames] = React.useState({});
489
511
  React.useEffect(() => {
490
512
  const controller = new AbortController();
491
513
  let cancelled = false;
492
514
  void fetchAssignableStaffMembers("", { pageSize: 100, signal: controller.signal }).then((items) => {
493
- if (!cancelled) setOwnerFilterOptions(mapAssignableStaffToFilterOptions(items));
515
+ if (cancelled) return;
516
+ setOwnerFilterOptions(mapAssignableStaffToFilterOptions(items));
517
+ const names = {};
518
+ for (const item of items) {
519
+ if (item.userId) names[item.userId] = item.displayName;
520
+ }
521
+ setOwnerNames(names);
494
522
  }).catch(() => {
495
- if (!cancelled) setOwnerFilterOptions([]);
523
+ if (cancelled) return;
524
+ setOwnerFilterOptions([]);
525
+ setOwnerNames({});
496
526
  });
497
527
  return () => {
498
528
  cancelled = true;
@@ -511,28 +541,18 @@ function CustomersDealsPage() {
511
541
  const items = await fetchAssignableStaffMembers(query ?? "", { pageSize: 100 });
512
542
  return mapAssignableStaffToFilterOptions(items);
513
543
  }, []);
544
+ const startOfToday = React.useMemo(() => {
545
+ const today = /* @__PURE__ */ new Date();
546
+ today.setHours(0, 0, 0, 0);
547
+ return today;
548
+ }, []);
549
+ const isDealOverdue = React.useCallback(
550
+ (row) => !!row.expectedCloseAt && new Date(row.expectedCloseAt) < startOfToday && row.status === "open",
551
+ [startOfToday]
552
+ );
514
553
  const columns = React.useMemo(() => {
515
554
  const noValue = /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-sm", children: t("customers.deals.list.noValue") });
516
- const renderDictionaryCell = (kind, value) => /* @__PURE__ */ jsx(
517
- DictionaryValue,
518
- {
519
- value,
520
- map: dictionaryMaps[kind],
521
- fallback: value ? /* @__PURE__ */ jsx("span", { className: "text-sm", children: value }) : noValue,
522
- className: "text-sm",
523
- iconWrapperClassName: "inline-flex h-6 w-6 items-center justify-center rounded border border-border bg-card",
524
- iconClassName: "h-4 w-4",
525
- colorClassName: "h-3 w-3 rounded-full"
526
- }
527
- );
528
- const renderAssociationSummary = (items, fallbackLabel) => {
529
- if (!items.length) return noValue;
530
- const labels = normalizeCollectionLabels(
531
- items.map((entry) => entry.label && entry.label.trim().length ? entry.label : fallbackLabel)
532
- );
533
- if (!labels.length) return noValue;
534
- return /* @__PURE__ */ jsx(CollectionPreviewCell, { labels, maxVisible: 1 });
535
- };
555
+ const unknownOwner = t("customers.deals.list.unknownOwner");
536
556
  const customColumns = customFieldDefs.filter((def) => supportsCustomFieldColumn(def)).map((def) => ({
537
557
  accessorKey: `cf_${def.key}`,
538
558
  header: def.label || def.key,
@@ -577,7 +597,17 @@ function CustomersDealsPage() {
577
597
  filterGroup: "Deal",
578
598
  maxWidth: "280px"
579
599
  },
580
- cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "font-medium text-sm", children: row.original.title })
600
+ cell: ({ row }) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 min-w-0", children: [
601
+ /* @__PURE__ */ jsx("span", { className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground", children: /* @__PURE__ */ jsx(Briefcase, { className: "h-4 w-4" }) }),
602
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground truncate", children: row.original.title }),
603
+ isDealOverdue(row.original) ? /* @__PURE__ */ jsx(
604
+ AlertTriangle,
605
+ {
606
+ className: "h-4 w-4 shrink-0 text-status-warning-text",
607
+ "aria-label": t("customers.deals.list.close.overdue")
608
+ }
609
+ ) : null
610
+ ] })
581
611
  },
582
612
  {
583
613
  accessorKey: "status",
@@ -589,7 +619,14 @@ function CustomersDealsPage() {
589
619
  filterKey: "status",
590
620
  filterGroup: "Deal"
591
621
  },
592
- cell: ({ row }) => renderDictionaryCell("deal-statuses", row.original.status)
622
+ cell: ({ row }) => {
623
+ const status = row.original.status;
624
+ if (!status) return noValue;
625
+ const entry = dictionaryMaps["deal-statuses"]?.[status];
626
+ const label = entry?.label ?? status;
627
+ const variant = coerceStatusBadgeVariant(mapDictionaryColorToTone(entry?.color));
628
+ return /* @__PURE__ */ jsx(StatusBadge, { variant, dot: true, children: label });
629
+ }
593
630
  },
594
631
  {
595
632
  accessorKey: "pipelineStage",
@@ -601,7 +638,12 @@ function CustomersDealsPage() {
601
638
  filterKey: "pipeline_stage",
602
639
  filterGroup: "Deal"
603
640
  },
604
- cell: ({ row }) => renderDictionaryCell("pipeline-stages", row.original.pipelineStage)
641
+ cell: ({ row }) => {
642
+ const stage = row.original.pipelineStage;
643
+ if (!stage) return noValue;
644
+ const label = dictionaryMaps["pipeline-stages"]?.[stage]?.label ?? stage;
645
+ return /* @__PURE__ */ jsx("span", { className: "text-foreground", children: label });
646
+ }
605
647
  },
606
648
  {
607
649
  accessorKey: "pipelineId",
@@ -626,7 +668,15 @@ function CustomersDealsPage() {
626
668
  filterKey: "value_amount",
627
669
  filterGroup: "Deal"
628
670
  },
629
- cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: formatCurrency(row.original.valueAmount ?? null, row.original.valueCurrency ?? null, t("customers.deals.list.noValue")) })
671
+ cell: ({ row }) => {
672
+ const amount = formatGroupedAmount(row.original.valueAmount ?? null);
673
+ if (amount === null) return noValue;
674
+ const currency = row.original.valueCurrency;
675
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
676
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: amount }),
677
+ currency ? /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: currency }) : null
678
+ ] });
679
+ }
630
680
  },
631
681
  {
632
682
  accessorKey: "probability",
@@ -640,7 +690,7 @@ function CustomersDealsPage() {
640
690
  cell: ({ row }) => {
641
691
  const value = row.original.probability;
642
692
  if (typeof value === "number" && Number.isFinite(value)) {
643
- return /* @__PURE__ */ jsx("span", { className: "text-sm", children: `${Math.min(Math.max(value, 0), 100)}%` });
693
+ return /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: `${Math.min(Math.max(value, 0), 100)}%` });
644
694
  }
645
695
  return noValue;
646
696
  }
@@ -654,7 +704,27 @@ function CustomersDealsPage() {
654
704
  filterGroup: "Activity",
655
705
  filterIconName: "calendar"
656
706
  },
657
- cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "text-sm", children: formatDateValue(row.original.expectedCloseAt ?? null, t("customers.deals.list.noValue")) })
707
+ cell: ({ row }) => {
708
+ const expectedCloseAt = row.original.expectedCloseAt;
709
+ if (!expectedCloseAt) return noValue;
710
+ let subtitle = null;
711
+ if (isDealOverdue(row.original)) {
712
+ subtitle = /* @__PURE__ */ jsx("span", { className: "text-xs text-status-error-text", children: t("customers.deals.list.close.overdue") });
713
+ } else if (row.original.status === "win") {
714
+ subtitle = /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t("customers.deals.list.close.won") });
715
+ } else if (row.original.status === "loose") {
716
+ subtitle = /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t("customers.deals.list.close.lost") });
717
+ } else {
718
+ const relative = formatRelativeTime(expectedCloseAt, { translate: t });
719
+ if (relative) {
720
+ subtitle = /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: relative });
721
+ }
722
+ }
723
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
724
+ /* @__PURE__ */ jsx("span", { className: "text-foreground", children: formatDateValue(expectedCloseAt, t("customers.deals.list.noValue")) }),
725
+ subtitle
726
+ ] });
727
+ }
658
728
  },
659
729
  {
660
730
  accessorKey: "ownerUserId",
@@ -666,10 +736,17 @@ function CustomersDealsPage() {
666
736
  filterLoadOptions: loadOwnerFilterOptions,
667
737
  filterGroup: "CRM",
668
738
  filterIconName: "user-round",
669
- filterKey: "owner_user_id",
670
- hidden: true
739
+ filterKey: "owner_user_id"
671
740
  },
672
- cell: ({ row }) => row.original.ownerUserId ?? null
741
+ cell: ({ row }) => {
742
+ const ownerUserId = row.original.ownerUserId;
743
+ if (!ownerUserId) return noValue;
744
+ const label = ownerNames[ownerUserId]?.trim() || unknownOwner;
745
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 min-w-0", children: [
746
+ /* @__PURE__ */ jsx(Avatar, { label, size: "sm" }),
747
+ /* @__PURE__ */ jsx("span", { className: "text-foreground truncate", children: label })
748
+ ] });
749
+ }
673
750
  },
674
751
  {
675
752
  accessorKey: "companies",
@@ -684,7 +761,17 @@ function CustomersDealsPage() {
684
761
  row.companies.map((entry) => entry.label && entry.label.trim().length ? entry.label : t("customers.deals.list.unnamedCompany"))
685
762
  ).join(", ")
686
763
  },
687
- cell: ({ row }) => renderAssociationSummary(row.original.companies, t("customers.deals.list.unnamedCompany"))
764
+ cell: ({ row }) => {
765
+ const companies = row.original.companies;
766
+ if (!companies.length) return noValue;
767
+ const first = companies[0];
768
+ const firstLabel = first.label && first.label.trim().length ? first.label : t("customers.deals.list.unnamedCompany");
769
+ const overflow = companies.length - 1;
770
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 min-w-0", children: [
771
+ /* @__PURE__ */ jsx(Tag, { variant: "neutral", className: "max-w-36", children: /* @__PURE__ */ jsx("span", { className: "truncate", children: firstLabel }) }),
772
+ overflow > 0 ? /* @__PURE__ */ jsx(Tag, { variant: "neutral", children: `+${overflow}` }) : null
773
+ ] });
774
+ }
688
775
  },
689
776
  {
690
777
  accessorKey: "people",
@@ -699,7 +786,22 @@ function CustomersDealsPage() {
699
786
  row.people.map((entry) => entry.label && entry.label.trim().length ? entry.label : t("customers.deals.list.unnamedPerson"))
700
787
  ).join(", ")
701
788
  },
702
- cell: ({ row }) => renderAssociationSummary(row.original.people, t("customers.deals.list.unnamedPerson"))
789
+ cell: ({ row }) => {
790
+ const people = row.original.people;
791
+ if (!people.length) return noValue;
792
+ const labels = normalizeCollectionLabels(
793
+ people.map((person) => person.label && person.label.trim().length ? person.label : t("customers.deals.list.unnamedPerson"))
794
+ );
795
+ const tooltip = labels.join(", ");
796
+ return /* @__PURE__ */ jsx(SimpleTooltip, { content: tooltip, side: "top", children: /* @__PURE__ */ jsx("span", { className: "inline-flex", children: /* @__PURE__ */ jsx(AvatarStack, { max: 4, size: "sm", children: people.map((person) => /* @__PURE__ */ jsx(
797
+ Avatar,
798
+ {
799
+ label: person.label || t("customers.deals.list.unnamedPerson"),
800
+ size: "sm"
801
+ },
802
+ person.id
803
+ )) }) }) });
804
+ }
703
805
  },
704
806
  {
705
807
  accessorKey: "updatedAt",
@@ -714,7 +816,7 @@ function CustomersDealsPage() {
714
816
  },
715
817
  ...customColumns
716
818
  ];
717
- }, [customFieldDefs, dictionaryMaps, dictionaryOptions, loadOwnerFilterOptions, pipelineNames, resolvedOwnerFilterOptions, t]);
819
+ }, [customFieldDefs, dictionaryMaps, dictionaryOptions, isDealOverdue, loadOwnerFilterOptions, ownerNames, pipelineNames, resolvedOwnerFilterOptions, t]);
718
820
  const { advancedFilterFields } = useAutoDiscoveredFields({ columns, customFieldDefs });
719
821
  React.useEffect(() => {
720
822
  setPanelFields((prev) => {
@@ -779,11 +881,24 @@ function CustomersDealsPage() {
779
881
  return /* @__PURE__ */ jsxs(Page, { children: [
780
882
  /* @__PURE__ */ jsxs(PageBody, { children: [
781
883
  /* @__PURE__ */ jsx(ViewTabsRow, { active: "list", className: "mb-4" }),
884
+ /* @__PURE__ */ jsx(
885
+ DealsKpiStrip,
886
+ {
887
+ ownerNames,
888
+ stageDictionary: dictionaryMaps["pipeline-stages"] ?? {},
889
+ pipelineCount: Object.keys(pipelineNames).length,
890
+ scopeVersion,
891
+ reloadToken,
892
+ onNeedsAttentionClick: handleNeedsAttentionFilter,
893
+ className: "mb-4"
894
+ }
895
+ ),
782
896
  /* @__PURE__ */ jsx(
783
897
  DataTable,
784
898
  {
785
899
  stickyFirstColumn: true,
786
900
  stickyActionsColumn: true,
901
+ actionsColumnAlign: "center",
787
902
  title: t("customers.deals.list.title"),
788
903
  actions: /* @__PURE__ */ jsx(Button, { asChild: true, children: /* @__PURE__ */ jsx(Link, { href: "/backend/customers/deals/create", children: t("customers.deals.list.actions.new", "New deal") }) }),
789
904
  columns,
@@ -871,6 +986,35 @@ function CustomersDealsPage() {
871
986
  }
872
987
  },
873
988
  activeFilterChips: /* @__PURE__ */ jsxs(Fragment, { children: [
989
+ needsAttentionOnly ? /* @__PURE__ */ jsx(
990
+ "div",
991
+ {
992
+ className: "flex items-center gap-2 overflow-x-auto border-b border-border bg-background px-4 py-2",
993
+ "data-testid": "active-filter-chips",
994
+ children: /* @__PURE__ */ jsxs(
995
+ "div",
996
+ {
997
+ className: "inline-flex items-center gap-1",
998
+ "data-testid": "active-filter-chip",
999
+ "aria-label": t("customers.deals.list.filters.needsAttention"),
1000
+ children: [
1001
+ /* @__PURE__ */ jsx(Tag, { variant: "warning", dot: true, children: t("customers.deals.list.filters.needsAttention") }),
1002
+ /* @__PURE__ */ jsx(
1003
+ IconButton,
1004
+ {
1005
+ type: "button",
1006
+ variant: "ghost",
1007
+ size: "xs",
1008
+ "aria-label": t("customers.deals.list.filters.needsAttentionRemove"),
1009
+ onClick: handleNeedsAttentionClear,
1010
+ children: /* @__PURE__ */ jsx(X, { className: "size-3" })
1011
+ }
1012
+ )
1013
+ ]
1014
+ }
1015
+ )
1016
+ }
1017
+ ) : null,
874
1018
  /* @__PURE__ */ jsx(
875
1019
  ActiveFilterChips,
876
1020
  {
@@ -893,11 +1037,32 @@ function CustomersDealsPage() {
893
1037
  )
894
1038
  ] }),
895
1039
  filterAwareEmptyState: {
896
- active: advancedFilterState.root.children.length > 0,
1040
+ active: needsAttentionOnly || associationFilterTree.root.children.length > 0 || advancedFilterState.root.children.length > 0,
897
1041
  entityNamePlural: t("customers.deals.entityPlural", "deals"),
898
- canRemoveLast: filterPanel.tree.root.children.length > 0,
899
- onClearAll: handleAdvancedFilterClear,
900
- onRemoveLast: () => filterPanel.dispatch({ type: "removeLast" })
1042
+ canRemoveLast: needsAttentionOnly || associationFilterTree.root.children.length > 0 || filterPanel.tree.root.children.length > 0,
1043
+ onClearAll: () => {
1044
+ handleAdvancedFilterClear();
1045
+ setSelectedPersonIds([]);
1046
+ setSelectedCompanyIds([]);
1047
+ setNeedsAttentionOnly(false);
1048
+ },
1049
+ onRemoveLast: () => {
1050
+ if (needsAttentionOnly) {
1051
+ handleNeedsAttentionClear();
1052
+ return;
1053
+ }
1054
+ if (selectedCompanyIds.length > 0) {
1055
+ setSelectedCompanyIds([]);
1056
+ setPage(1);
1057
+ return;
1058
+ }
1059
+ if (selectedPersonIds.length > 0) {
1060
+ setSelectedPersonIds([]);
1061
+ setPage(1);
1062
+ return;
1063
+ }
1064
+ filterPanel.dispatch({ type: "removeLast" });
1065
+ }
901
1066
  },
902
1067
  emptyState: /* @__PURE__ */ jsx(
903
1068
  ListEmptyState,