@open-mercato/core 0.6.6-develop.6296.1.5e8bdfee17 → 0.6.6-develop.6300.1.08ff40c897

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 (36) hide show
  1. package/AGENTS.md +8 -0
  2. package/dist/generated/entities/module_config/index.js +4 -0
  3. package/dist/generated/entities/module_config/index.js.map +2 -2
  4. package/dist/generated/entity-fields-registry.js +2 -0
  5. package/dist/generated/entity-fields-registry.js.map +2 -2
  6. package/dist/modules/auth/lib/setup-app.js +1 -1
  7. package/dist/modules/auth/lib/setup-app.js.map +2 -2
  8. package/dist/modules/configs/data/entities.js +15 -1
  9. package/dist/modules/configs/data/entities.js.map +2 -2
  10. package/dist/modules/configs/lib/module-config-service.js +52 -19
  11. package/dist/modules/configs/lib/module-config-service.js.map +3 -3
  12. package/dist/modules/configs/migrations/Migration20260617150000.js +21 -0
  13. package/dist/modules/configs/migrations/Migration20260617150000.js.map +7 -0
  14. package/dist/modules/customers/api/activities/route.js.map +2 -2
  15. package/dist/modules/customers/backend/customers/deals/pipeline/page.js +26 -22
  16. package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
  17. package/dist/modules/customers/components/detail/CompanyKpiBar.js +8 -7
  18. package/dist/modules/customers/components/detail/CompanyKpiBar.js.map +2 -2
  19. package/dist/modules/customers/components/detail/CompanyPeopleSection.js +6 -5
  20. package/dist/modules/customers/components/detail/CompanyPeopleSection.js.map +2 -2
  21. package/generated/entities/module_config/index.ts +2 -0
  22. package/generated/entity-fields-registry.ts +2 -0
  23. package/package.json +7 -7
  24. package/src/modules/auth/lib/setup-app.ts +1 -1
  25. package/src/modules/configs/data/entities.ts +17 -1
  26. package/src/modules/configs/lib/module-config-service.ts +83 -24
  27. package/src/modules/configs/migrations/.snapshot-open-mercato.json +41 -4
  28. package/src/modules/configs/migrations/.snapshot-openmercato.json +41 -4
  29. package/src/modules/configs/migrations/Migration20260617150000.ts +21 -0
  30. package/src/modules/customers/api/activities/route.ts +6 -0
  31. package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +29 -22
  32. package/src/modules/customers/components/detail/CompanyKpiBar.tsx +8 -7
  33. package/src/modules/customers/components/detail/CompanyPeopleSection.tsx +6 -5
  34. package/dist/modules/customers/components/detail/CompanyDashboardTab.js +0 -133
  35. package/dist/modules/customers/components/detail/CompanyDashboardTab.js.map +0 -7
  36. package/src/modules/customers/components/detail/CompanyDashboardTab.tsx +0 -161
@@ -1,133 +0,0 @@
1
- "use client";
2
- import { jsx, jsxs } from "react/jsx-runtime";
3
- import * as React from "react";
4
- import { EyeOff } from "lucide-react";
5
- import { useT } from "@open-mercato/shared/lib/i18n/context";
6
- import { KpiCard } from "@open-mercato/ui/backend/charts/KpiCard";
7
- import { Button } from "@open-mercato/ui/primitives/button";
8
- import { IconButton } from "@open-mercato/ui/primitives/icon-button";
9
- import { InlineActivityComposer } from "./InlineActivityComposer.js";
10
- import { formatCurrency } from "./utils.js";
11
- import { computeHealthScore } from "./healthScoreUtils.js";
12
- import {
13
- readJsonFromLocalStorage,
14
- writeJsonToLocalStorage,
15
- removeLocalStorageKey
16
- } from "@open-mercato/shared/lib/browser/safeLocalStorage";
17
- import {
18
- sumActiveDeals,
19
- getActiveDeals,
20
- getOpenTasks,
21
- getUpcomingMeetings,
22
- getRecentActivity,
23
- computeActivityTrend,
24
- computeDealTrend
25
- } from "./dashboard/helpers.js";
26
- import { UpcomingMeetingsWidget } from "./dashboard/UpcomingMeetingsWidget.js";
27
- import { RelationshipHealthWidget } from "./dashboard/RelationshipHealthWidget.js";
28
- import { ActiveDealWidget } from "./dashboard/ActiveDealWidget.js";
29
- import { OpenTasksWidget } from "./dashboard/OpenTasksWidget.js";
30
- import { RecentActivityWidget } from "./dashboard/RecentActivityWidget.js";
31
- function CompanyDashboardTab({ data, companyId, onTabChange, onActivityCreated, onScheduleRequested, runGuardedMutation, useCanonicalInteractions }) {
32
- const t = useT();
33
- const activeDeals = React.useMemo(() => getActiveDeals(data.deals), [data.deals]);
34
- const activeDealsValue = React.useMemo(() => sumActiveDeals(data.deals), [data.deals]);
35
- const openTasks = React.useMemo(() => getOpenTasks(data.todos), [data.todos]);
36
- const upcomingMeetings = React.useMemo(() => getUpcomingMeetings(data.interactions), [data.interactions]);
37
- const recentActivity = React.useMemo(() => getRecentActivity(data.interactions), [data.interactions]);
38
- const dealCurrency = activeDeals[0]?.valueCurrency ?? data.deals[0]?.valueCurrency ?? "PLN";
39
- const activityTrend = React.useMemo(() => computeActivityTrend(data.interactions), [data.interactions]);
40
- const dealTrend = React.useMemo(() => computeDealTrend(data.deals), [data.deals]);
41
- const healthScore = React.useMemo(() => computeHealthScore(data.interactions), [data.interactions]);
42
- const ltvValue = React.useMemo(() => {
43
- const wonDeals = data.deals.filter((d) => d.status === "won");
44
- if (wonDeals.length === 0) return null;
45
- return wonDeals.reduce((sum, d) => {
46
- const amt = typeof d.valueAmount === "number" ? d.valueAmount : parseFloat(String(d.valueAmount ?? "0"));
47
- return sum + (Number.isFinite(amt) ? amt : 0);
48
- }, 0);
49
- }, [data.deals]);
50
- const clientTenureYears = React.useMemo(() => {
51
- const allDates = data.interactions.map((i) => i.occurredAt ?? i.scheduledAt ?? i.createdAt).filter(Boolean).map((d) => new Date(d).getTime());
52
- if (allDates.length === 0) return null;
53
- const earliest = Math.min(...allDates);
54
- return Math.floor((Date.now() - earliest) / (365.25 * 864e5));
55
- }, [data.interactions]);
56
- const [hiddenTiles, setHiddenTiles] = React.useState(
57
- () => new Set(readJsonFromLocalStorage("om:dashboard-hidden-tiles", []))
58
- );
59
- const toggleTile = React.useCallback((tileId) => {
60
- setHiddenTiles((prev) => {
61
- const next = new Set(prev);
62
- if (next.has(tileId)) next.delete(tileId);
63
- else next.add(tileId);
64
- writeJsonToLocalStorage("om:dashboard-hidden-tiles", [...next]);
65
- return next;
66
- });
67
- }, []);
68
- const kpiTiles = [
69
- { id: "activeDeals", title: t("customers.companies.dashboard.kpi.activeDeals", "ACTIVE DEALS"), value: activeDealsValue, trend: dealTrend, formatValue: (v) => formatCurrency(v, dealCurrency), comparisonLabel: `${activeDeals.length} ${activeDeals.length === 1 ? "pipeline" : "pipelines"}` },
70
- { id: "activities", title: t("customers.companies.dashboard.kpi.activities", "ACTIVITIES"), value: data.interactions.length, trend: activityTrend, comparisonLabel: t("customers.companies.dashboard.kpi.last12months", "last 12 months") },
71
- { id: "ltv", title: t("customers.companies.dashboard.kpi.ltv", "CUSTOMER VALUE (LTV)"), value: ltvValue, formatValue: ltvValue !== null ? (v) => formatCurrency(v, dealCurrency) : void 0, comparisonLabel: ltvValue !== null ? t("customers.companies.dashboard.kpi.wonDeals", "won deals total") : t("customers.companies.dashboard.kpi.noWonDeals", "No won deals") },
72
- { id: "clientSince", title: t("customers.companies.dashboard.kpi.clientSince", "CLIENT SINCE"), value: clientTenureYears, formatValue: clientTenureYears !== null ? (v) => v < 1 ? `< 1 ${t("customers.companies.dashboard.kpi.year", "year")}` : `${v} ${v === 1 ? t("customers.companies.dashboard.kpi.year", "year") : t("customers.companies.dashboard.kpi.years", "years")}` : void 0, comparisonLabel: clientTenureYears !== null ? `${data.deals.filter((d) => d.status === "won").length} ${t("customers.companies.dashboard.kpi.completedDeals", "completed deals")}` : t("customers.companies.dashboard.kpi.noInteractions", "No interactions yet") }
73
- ];
74
- return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
75
- /* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-4 lg:grid-cols-4", children: kpiTiles.filter((tile) => !hiddenTiles.has(tile.id)).map((tile) => /* @__PURE__ */ jsxs("div", { className: "relative group", children: [
76
- /* @__PURE__ */ jsx(
77
- KpiCard,
78
- {
79
- title: tile.title,
80
- value: tile.value,
81
- trend: tile.trend,
82
- formatValue: tile.formatValue,
83
- comparisonLabel: tile.comparisonLabel
84
- }
85
- ),
86
- /* @__PURE__ */ jsx(
87
- IconButton,
88
- {
89
- type: "button",
90
- variant: "ghost",
91
- size: "sm",
92
- onClick: () => toggleTile(tile.id),
93
- className: "h-auto absolute top-2 right-2 opacity-0 group-hover:opacity-60 transition-opacity text-muted-foreground hover:text-foreground p-0",
94
- "aria-label": t("customers.companies.dashboard.hideTile", "Hide tile"),
95
- children: /* @__PURE__ */ jsx(EyeOff, { className: "size-3.5" })
96
- }
97
- )
98
- ] }, tile.id)) }),
99
- hiddenTiles.size > 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
100
- /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t("customers.companies.dashboard.hiddenTiles", "{{count}} tiles hidden", { count: hiddenTiles.size }) }),
101
- /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "h-auto text-xs px-1.5 py-0.5", onClick: () => {
102
- setHiddenTiles(/* @__PURE__ */ new Set());
103
- removeLocalStorageKey("om:dashboard-hidden-tiles");
104
- }, children: t("customers.companies.dashboard.showAll", "Show all") })
105
- ] }),
106
- /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 gap-6 lg:grid-cols-[1fr_380px]", children: [
107
- /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
108
- /* @__PURE__ */ jsx(UpcomingMeetingsWidget, { meetings: upcomingMeetings, t }),
109
- /* @__PURE__ */ jsx(OpenTasksWidget, { tasks: openTasks, t, onViewAll: () => onTabChange("activity-log"), currentUserId: null })
110
- ] }),
111
- /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
112
- /* @__PURE__ */ jsx(RelationshipHealthWidget, { health: healthScore, t }),
113
- /* @__PURE__ */ jsx(ActiveDealWidget, { deals: activeDeals, t })
114
- ] })
115
- ] }),
116
- /* @__PURE__ */ jsx(
117
- InlineActivityComposer,
118
- {
119
- entityType: "company",
120
- entityId: companyId,
121
- onActivityCreated,
122
- runGuardedMutation,
123
- onScheduleRequested,
124
- useCanonicalInteractions
125
- }
126
- ),
127
- /* @__PURE__ */ jsx(RecentActivityWidget, { interactions: recentActivity, t })
128
- ] });
129
- }
130
- export {
131
- CompanyDashboardTab
132
- };
133
- //# sourceMappingURL=CompanyDashboardTab.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../../../src/modules/customers/components/detail/CompanyDashboardTab.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { EyeOff } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { KpiCard, type KpiTrend } from '@open-mercato/ui/backend/charts/KpiCard'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { InlineActivityComposer } from './InlineActivityComposer'\nimport type { CompanyOverview } from '../formConfig'\nimport { formatCurrency } from './utils'\nimport { computeHealthScore } from './healthScoreUtils'\nimport {\n readJsonFromLocalStorage,\n writeJsonToLocalStorage,\n removeLocalStorageKey,\n} from '@open-mercato/shared/lib/browser/safeLocalStorage'\nimport {\n sumActiveDeals,\n getActiveDeals,\n getOpenTasks,\n getUpcomingMeetings,\n getRecentActivity,\n computeActivityTrend,\n computeDealTrend,\n} from './dashboard/helpers'\nimport { UpcomingMeetingsWidget } from './dashboard/UpcomingMeetingsWidget'\nimport { RelationshipHealthWidget } from './dashboard/RelationshipHealthWidget'\nimport { ActiveDealWidget } from './dashboard/ActiveDealWidget'\nimport { OpenTasksWidget } from './dashboard/OpenTasksWidget'\nimport { RecentActivityWidget } from './dashboard/RecentActivityWidget'\n\ntype GuardedMutationRunner = <T>(\n operation: () => Promise<T>,\n mutationPayload?: Record<string, unknown>,\n) => Promise<T>\n\ntype CompanyDashboardTabProps = {\n data: CompanyOverview\n companyId: string\n onTabChange: (tab: string) => void\n onActivityCreated?: () => void\n onScheduleRequested?: () => void\n runGuardedMutation?: GuardedMutationRunner\n useCanonicalInteractions?: boolean\n}\n\nexport function CompanyDashboardTab({ data, companyId, onTabChange, onActivityCreated, onScheduleRequested, runGuardedMutation, useCanonicalInteractions }: CompanyDashboardTabProps) {\n const t = useT()\n\n const activeDeals = React.useMemo(() => getActiveDeals(data.deals), [data.deals])\n const activeDealsValue = React.useMemo(() => sumActiveDeals(data.deals), [data.deals])\n const openTasks = React.useMemo(() => getOpenTasks(data.todos), [data.todos])\n const upcomingMeetings = React.useMemo(() => getUpcomingMeetings(data.interactions), [data.interactions])\n const recentActivity = React.useMemo(() => getRecentActivity(data.interactions), [data.interactions])\n\n const dealCurrency = activeDeals[0]?.valueCurrency ?? data.deals[0]?.valueCurrency ?? 'PLN'\n const activityTrend = React.useMemo(() => computeActivityTrend(data.interactions), [data.interactions])\n const dealTrend = React.useMemo(() => computeDealTrend(data.deals), [data.deals])\n const healthScore = React.useMemo(() => computeHealthScore(data.interactions), [data.interactions])\n\n const ltvValue = React.useMemo(() => {\n const wonDeals = data.deals.filter((d) => d.status === 'won')\n if (wonDeals.length === 0) return null\n return wonDeals.reduce((sum, d) => {\n const amt = typeof d.valueAmount === 'number' ? d.valueAmount : parseFloat(String(d.valueAmount ?? '0'))\n return sum + (Number.isFinite(amt) ? amt : 0)\n }, 0)\n }, [data.deals])\n\n const clientTenureYears = React.useMemo(() => {\n const allDates = data.interactions\n .map((i) => i.occurredAt ?? i.scheduledAt ?? i.createdAt)\n .filter(Boolean)\n .map((d) => new Date(d).getTime())\n if (allDates.length === 0) return null\n const earliest = Math.min(...allDates)\n return Math.floor((Date.now() - earliest) / (365.25 * 86_400_000))\n }, [data.interactions])\n\n const [hiddenTiles, setHiddenTiles] = React.useState<Set<string>>(\n () => new Set(readJsonFromLocalStorage<string[]>('om:dashboard-hidden-tiles', [])),\n )\n\n const toggleTile = React.useCallback((tileId: string) => {\n setHiddenTiles((prev) => {\n const next = new Set(prev)\n if (next.has(tileId)) next.delete(tileId)\n else next.add(tileId)\n writeJsonToLocalStorage('om:dashboard-hidden-tiles', [...next])\n return next\n })\n }, [])\n\n const kpiTiles: Array<{ id: string; title: string; value: number | null; trend?: KpiTrend; formatValue?: (v: number) => string; comparisonLabel: string }> = [\n { id: 'activeDeals', title: t('customers.companies.dashboard.kpi.activeDeals', 'ACTIVE DEALS'), value: activeDealsValue, trend: dealTrend, formatValue: (v: number) => formatCurrency(v, dealCurrency), comparisonLabel: `${activeDeals.length} ${activeDeals.length === 1 ? 'pipeline' : 'pipelines'}` },\n { id: 'activities', title: t('customers.companies.dashboard.kpi.activities', 'ACTIVITIES'), value: data.interactions.length, trend: activityTrend, comparisonLabel: t('customers.companies.dashboard.kpi.last12months', 'last 12 months') },\n { id: 'ltv', title: t('customers.companies.dashboard.kpi.ltv', 'CUSTOMER VALUE (LTV)'), value: ltvValue, formatValue: ltvValue !== null ? (v: number) => formatCurrency(v, dealCurrency) : undefined, comparisonLabel: ltvValue !== null ? t('customers.companies.dashboard.kpi.wonDeals', 'won deals total') : t('customers.companies.dashboard.kpi.noWonDeals', 'No won deals') },\n { id: 'clientSince', title: t('customers.companies.dashboard.kpi.clientSince', 'CLIENT SINCE'), value: clientTenureYears, formatValue: clientTenureYears !== null ? (v: number) => v < 1 ? `< 1 ${t('customers.companies.dashboard.kpi.year', 'year')}` : `${v} ${v === 1 ? t('customers.companies.dashboard.kpi.year', 'year') : t('customers.companies.dashboard.kpi.years', 'years')}` : undefined, comparisonLabel: clientTenureYears !== null ? `${data.deals.filter(d => d.status === 'won').length} ${t('customers.companies.dashboard.kpi.completedDeals', 'completed deals')}` : t('customers.companies.dashboard.kpi.noInteractions', 'No interactions yet') },\n ]\n\n return (\n <div className=\"space-y-6\">\n <div className=\"grid grid-cols-2 gap-4 lg:grid-cols-4\">\n {kpiTiles.filter((tile) => !hiddenTiles.has(tile.id)).map((tile) => (\n <div key={tile.id} className=\"relative group\">\n <KpiCard\n title={tile.title}\n value={tile.value}\n trend={tile.trend}\n formatValue={tile.formatValue}\n comparisonLabel={tile.comparisonLabel}\n />\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => toggleTile(tile.id)}\n className=\"h-auto absolute top-2 right-2 opacity-0 group-hover:opacity-60 transition-opacity text-muted-foreground hover:text-foreground p-0\"\n aria-label={t('customers.companies.dashboard.hideTile', 'Hide tile')}\n >\n <EyeOff className=\"size-3.5\" />\n </IconButton>\n </div>\n ))}\n </div>\n {hiddenTiles.size > 0 && (\n <div className=\"flex items-center gap-2\">\n <span className=\"text-xs text-muted-foreground\">\n {t('customers.companies.dashboard.hiddenTiles', '{{count}} tiles hidden', { count: hiddenTiles.size })}\n </span>\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" className=\"h-auto text-xs px-1.5 py-0.5\" onClick={() => { setHiddenTiles(new Set()); removeLocalStorageKey('om:dashboard-hidden-tiles') }}>\n {t('customers.companies.dashboard.showAll', 'Show all')}\n </Button>\n </div>\n )}\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-[1fr_380px]\">\n <div className=\"space-y-6\">\n <UpcomingMeetingsWidget meetings={upcomingMeetings} t={t} />\n <OpenTasksWidget tasks={openTasks} t={t} onViewAll={() => onTabChange('activity-log')} currentUserId={null} />\n </div>\n <div className=\"space-y-6\">\n <RelationshipHealthWidget health={healthScore} t={t} />\n <ActiveDealWidget deals={activeDeals} t={t} />\n </div>\n </div>\n\n <InlineActivityComposer\n entityType=\"company\"\n entityId={companyId}\n onActivityCreated={onActivityCreated}\n runGuardedMutation={runGuardedMutation}\n onScheduleRequested={onScheduleRequested}\n useCanonicalInteractions={useCanonicalInteractions}\n />\n\n <RecentActivityWidget interactions={recentActivity} t={t} />\n </div>\n )\n}\n"],
5
- "mappings": ";AAyGU,SACE,KADF;AAvGV,YAAY,WAAW;AACvB,SAAS,cAAc;AACvB,SAAS,YAAY;AACrB,SAAS,eAA8B;AACvC,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAC3B,SAAS,8BAA8B;AAEvC,SAAS,sBAAsB;AAC/B,SAAS,0BAA0B;AACnC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,8BAA8B;AACvC,SAAS,gCAAgC;AACzC,SAAS,wBAAwB;AACjC,SAAS,uBAAuB;AAChC,SAAS,4BAA4B;AAiB9B,SAAS,oBAAoB,EAAE,MAAM,WAAW,aAAa,mBAAmB,qBAAqB,oBAAoB,yBAAyB,GAA6B;AACpL,QAAM,IAAI,KAAK;AAEf,QAAM,cAAc,MAAM,QAAQ,MAAM,eAAe,KAAK,KAAK,GAAG,CAAC,KAAK,KAAK,CAAC;AAChF,QAAM,mBAAmB,MAAM,QAAQ,MAAM,eAAe,KAAK,KAAK,GAAG,CAAC,KAAK,KAAK,CAAC;AACrF,QAAM,YAAY,MAAM,QAAQ,MAAM,aAAa,KAAK,KAAK,GAAG,CAAC,KAAK,KAAK,CAAC;AAC5E,QAAM,mBAAmB,MAAM,QAAQ,MAAM,oBAAoB,KAAK,YAAY,GAAG,CAAC,KAAK,YAAY,CAAC;AACxG,QAAM,iBAAiB,MAAM,QAAQ,MAAM,kBAAkB,KAAK,YAAY,GAAG,CAAC,KAAK,YAAY,CAAC;AAEpG,QAAM,eAAe,YAAY,CAAC,GAAG,iBAAiB,KAAK,MAAM,CAAC,GAAG,iBAAiB;AACtF,QAAM,gBAAgB,MAAM,QAAQ,MAAM,qBAAqB,KAAK,YAAY,GAAG,CAAC,KAAK,YAAY,CAAC;AACtG,QAAM,YAAY,MAAM,QAAQ,MAAM,iBAAiB,KAAK,KAAK,GAAG,CAAC,KAAK,KAAK,CAAC;AAChF,QAAM,cAAc,MAAM,QAAQ,MAAM,mBAAmB,KAAK,YAAY,GAAG,CAAC,KAAK,YAAY,CAAC;AAElG,QAAM,WAAW,MAAM,QAAQ,MAAM;AACnC,UAAM,WAAW,KAAK,MAAM,OAAO,CAAC,MAAM,EAAE,WAAW,KAAK;AAC5D,QAAI,SAAS,WAAW,EAAG,QAAO;AAClC,WAAO,SAAS,OAAO,CAAC,KAAK,MAAM;AACjC,YAAM,MAAM,OAAO,EAAE,gBAAgB,WAAW,EAAE,cAAc,WAAW,OAAO,EAAE,eAAe,GAAG,CAAC;AACvG,aAAO,OAAO,OAAO,SAAS,GAAG,IAAI,MAAM;AAAA,IAC7C,GAAG,CAAC;AAAA,EACN,GAAG,CAAC,KAAK,KAAK,CAAC;AAEf,QAAM,oBAAoB,MAAM,QAAQ,MAAM;AAC5C,UAAM,WAAW,KAAK,aACnB,IAAI,CAAC,MAAM,EAAE,cAAc,EAAE,eAAe,EAAE,SAAS,EACvD,OAAO,OAAO,EACd,IAAI,CAAC,MAAM,IAAI,KAAK,CAAC,EAAE,QAAQ,CAAC;AACnC,QAAI,SAAS,WAAW,EAAG,QAAO;AAClC,UAAM,WAAW,KAAK,IAAI,GAAG,QAAQ;AACrC,WAAO,KAAK,OAAO,KAAK,IAAI,IAAI,aAAa,SAAS,MAAW;AAAA,EACnE,GAAG,CAAC,KAAK,YAAY,CAAC;AAEtB,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM;AAAA,IAC1C,MAAM,IAAI,IAAI,yBAAmC,6BAA6B,CAAC,CAAC,CAAC;AAAA,EACnF;AAEA,QAAM,aAAa,MAAM,YAAY,CAAC,WAAmB;AACvD,mBAAe,CAAC,SAAS;AACvB,YAAM,OAAO,IAAI,IAAI,IAAI;AACzB,UAAI,KAAK,IAAI,MAAM,EAAG,MAAK,OAAO,MAAM;AAAA,UACnC,MAAK,IAAI,MAAM;AACpB,8BAAwB,6BAA6B,CAAC,GAAG,IAAI,CAAC;AAC9D,aAAO;AAAA,IACT,CAAC;AAAA,EACH,GAAG,CAAC,CAAC;AAEL,QAAM,WAAuJ;AAAA,IAC3J,EAAE,IAAI,eAAe,OAAO,EAAE,iDAAiD,cAAc,GAAG,OAAO,kBAAkB,OAAO,WAAW,aAAa,CAAC,MAAc,eAAe,GAAG,YAAY,GAAG,iBAAiB,GAAG,YAAY,MAAM,IAAI,YAAY,WAAW,IAAI,aAAa,WAAW,GAAG;AAAA,IACxS,EAAE,IAAI,cAAc,OAAO,EAAE,gDAAgD,YAAY,GAAG,OAAO,KAAK,aAAa,QAAQ,OAAO,eAAe,iBAAiB,EAAE,kDAAkD,gBAAgB,EAAE;AAAA,IAC1O,EAAE,IAAI,OAAO,OAAO,EAAE,yCAAyC,sBAAsB,GAAG,OAAO,UAAU,aAAa,aAAa,OAAO,CAAC,MAAc,eAAe,GAAG,YAAY,IAAI,QAAW,iBAAiB,aAAa,OAAO,EAAE,8CAA8C,iBAAiB,IAAI,EAAE,gDAAgD,cAAc,EAAE;AAAA,IAClX,EAAE,IAAI,eAAe,OAAO,EAAE,iDAAiD,cAAc,GAAG,OAAO,mBAAmB,aAAa,sBAAsB,OAAO,CAAC,MAAc,IAAI,IAAI,OAAO,EAAE,0CAA0C,MAAM,CAAC,KAAK,GAAG,CAAC,IAAI,MAAM,IAAI,EAAE,0CAA0C,MAAM,IAAI,EAAE,2CAA2C,OAAO,CAAC,KAAK,QAAW,iBAAiB,sBAAsB,OAAO,GAAG,KAAK,MAAM,OAAO,OAAK,EAAE,WAAW,KAAK,EAAE,MAAM,IAAI,EAAE,oDAAoD,iBAAiB,CAAC,KAAK,EAAE,oDAAoD,qBAAqB,EAAE;AAAA,EACzoB;AAEA,SACE,qBAAC,SAAI,WAAU,aACb;AAAA,wBAAC,SAAI,WAAU,yCACZ,mBAAS,OAAO,CAAC,SAAS,CAAC,YAAY,IAAI,KAAK,EAAE,CAAC,EAAE,IAAI,CAAC,SACzD,qBAAC,SAAkB,WAAU,kBAC3B;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,OAAO,KAAK;AAAA,UACZ,OAAO,KAAK;AAAA,UACZ,OAAO,KAAK;AAAA,UACZ,aAAa,KAAK;AAAA,UAClB,iBAAiB,KAAK;AAAA;AAAA,MACxB;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAQ;AAAA,UACR,MAAK;AAAA,UACL,SAAS,MAAM,WAAW,KAAK,EAAE;AAAA,UACjC,WAAU;AAAA,UACV,cAAY,EAAE,0CAA0C,WAAW;AAAA,UAEnE,8BAAC,UAAO,WAAU,YAAW;AAAA;AAAA,MAC/B;AAAA,SAjBQ,KAAK,EAkBf,CACD,GACH;AAAA,IACC,YAAY,OAAO,KAClB,qBAAC,SAAI,WAAU,2BACb;AAAA,0BAAC,UAAK,WAAU,iCACb,YAAE,6CAA6C,0BAA0B,EAAE,OAAO,YAAY,KAAK,CAAC,GACvG;AAAA,MACA,oBAAC,UAAO,MAAK,UAAS,SAAQ,SAAQ,MAAK,MAAK,WAAU,gCAA+B,SAAS,MAAM;AAAE,uBAAe,oBAAI,IAAI,CAAC;AAAG,8BAAsB,2BAA2B;AAAA,MAAE,GACrL,YAAE,yCAAyC,UAAU,GACxD;AAAA,OACF;AAAA,IAGF,qBAAC,SAAI,WAAU,mDACb;AAAA,2BAAC,SAAI,WAAU,aACb;AAAA,4BAAC,0BAAuB,UAAU,kBAAkB,GAAM;AAAA,QAC1D,oBAAC,mBAAgB,OAAO,WAAW,GAAM,WAAW,MAAM,YAAY,cAAc,GAAG,eAAe,MAAM;AAAA,SAC9G;AAAA,MACA,qBAAC,SAAI,WAAU,aACb;AAAA,4BAAC,4BAAyB,QAAQ,aAAa,GAAM;AAAA,QACrD,oBAAC,oBAAiB,OAAO,aAAa,GAAM;AAAA,SAC9C;AAAA,OACF;AAAA,IAEA;AAAA,MAAC;AAAA;AAAA,QACC,YAAW;AAAA,QACX,UAAU;AAAA,QACV;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA;AAAA,IACF;AAAA,IAEA,oBAAC,wBAAqB,cAAc,gBAAgB,GAAM;AAAA,KAC5D;AAEJ;",
6
- "names": []
7
- }
@@ -1,161 +0,0 @@
1
- "use client"
2
-
3
- import * as React from 'react'
4
- import { EyeOff } from 'lucide-react'
5
- import { useT } from '@open-mercato/shared/lib/i18n/context'
6
- import { KpiCard, type KpiTrend } from '@open-mercato/ui/backend/charts/KpiCard'
7
- import { Button } from '@open-mercato/ui/primitives/button'
8
- import { IconButton } from '@open-mercato/ui/primitives/icon-button'
9
- import { InlineActivityComposer } from './InlineActivityComposer'
10
- import type { CompanyOverview } from '../formConfig'
11
- import { formatCurrency } from './utils'
12
- import { computeHealthScore } from './healthScoreUtils'
13
- import {
14
- readJsonFromLocalStorage,
15
- writeJsonToLocalStorage,
16
- removeLocalStorageKey,
17
- } from '@open-mercato/shared/lib/browser/safeLocalStorage'
18
- import {
19
- sumActiveDeals,
20
- getActiveDeals,
21
- getOpenTasks,
22
- getUpcomingMeetings,
23
- getRecentActivity,
24
- computeActivityTrend,
25
- computeDealTrend,
26
- } from './dashboard/helpers'
27
- import { UpcomingMeetingsWidget } from './dashboard/UpcomingMeetingsWidget'
28
- import { RelationshipHealthWidget } from './dashboard/RelationshipHealthWidget'
29
- import { ActiveDealWidget } from './dashboard/ActiveDealWidget'
30
- import { OpenTasksWidget } from './dashboard/OpenTasksWidget'
31
- import { RecentActivityWidget } from './dashboard/RecentActivityWidget'
32
-
33
- type GuardedMutationRunner = <T>(
34
- operation: () => Promise<T>,
35
- mutationPayload?: Record<string, unknown>,
36
- ) => Promise<T>
37
-
38
- type CompanyDashboardTabProps = {
39
- data: CompanyOverview
40
- companyId: string
41
- onTabChange: (tab: string) => void
42
- onActivityCreated?: () => void
43
- onScheduleRequested?: () => void
44
- runGuardedMutation?: GuardedMutationRunner
45
- useCanonicalInteractions?: boolean
46
- }
47
-
48
- export function CompanyDashboardTab({ data, companyId, onTabChange, onActivityCreated, onScheduleRequested, runGuardedMutation, useCanonicalInteractions }: CompanyDashboardTabProps) {
49
- const t = useT()
50
-
51
- const activeDeals = React.useMemo(() => getActiveDeals(data.deals), [data.deals])
52
- const activeDealsValue = React.useMemo(() => sumActiveDeals(data.deals), [data.deals])
53
- const openTasks = React.useMemo(() => getOpenTasks(data.todos), [data.todos])
54
- const upcomingMeetings = React.useMemo(() => getUpcomingMeetings(data.interactions), [data.interactions])
55
- const recentActivity = React.useMemo(() => getRecentActivity(data.interactions), [data.interactions])
56
-
57
- const dealCurrency = activeDeals[0]?.valueCurrency ?? data.deals[0]?.valueCurrency ?? 'PLN'
58
- const activityTrend = React.useMemo(() => computeActivityTrend(data.interactions), [data.interactions])
59
- const dealTrend = React.useMemo(() => computeDealTrend(data.deals), [data.deals])
60
- const healthScore = React.useMemo(() => computeHealthScore(data.interactions), [data.interactions])
61
-
62
- const ltvValue = React.useMemo(() => {
63
- const wonDeals = data.deals.filter((d) => d.status === 'won')
64
- if (wonDeals.length === 0) return null
65
- return wonDeals.reduce((sum, d) => {
66
- const amt = typeof d.valueAmount === 'number' ? d.valueAmount : parseFloat(String(d.valueAmount ?? '0'))
67
- return sum + (Number.isFinite(amt) ? amt : 0)
68
- }, 0)
69
- }, [data.deals])
70
-
71
- const clientTenureYears = React.useMemo(() => {
72
- const allDates = data.interactions
73
- .map((i) => i.occurredAt ?? i.scheduledAt ?? i.createdAt)
74
- .filter(Boolean)
75
- .map((d) => new Date(d).getTime())
76
- if (allDates.length === 0) return null
77
- const earliest = Math.min(...allDates)
78
- return Math.floor((Date.now() - earliest) / (365.25 * 86_400_000))
79
- }, [data.interactions])
80
-
81
- const [hiddenTiles, setHiddenTiles] = React.useState<Set<string>>(
82
- () => new Set(readJsonFromLocalStorage<string[]>('om:dashboard-hidden-tiles', [])),
83
- )
84
-
85
- const toggleTile = React.useCallback((tileId: string) => {
86
- setHiddenTiles((prev) => {
87
- const next = new Set(prev)
88
- if (next.has(tileId)) next.delete(tileId)
89
- else next.add(tileId)
90
- writeJsonToLocalStorage('om:dashboard-hidden-tiles', [...next])
91
- return next
92
- })
93
- }, [])
94
-
95
- const kpiTiles: Array<{ id: string; title: string; value: number | null; trend?: KpiTrend; formatValue?: (v: number) => string; comparisonLabel: string }> = [
96
- { id: 'activeDeals', title: t('customers.companies.dashboard.kpi.activeDeals', 'ACTIVE DEALS'), value: activeDealsValue, trend: dealTrend, formatValue: (v: number) => formatCurrency(v, dealCurrency), comparisonLabel: `${activeDeals.length} ${activeDeals.length === 1 ? 'pipeline' : 'pipelines'}` },
97
- { id: 'activities', title: t('customers.companies.dashboard.kpi.activities', 'ACTIVITIES'), value: data.interactions.length, trend: activityTrend, comparisonLabel: t('customers.companies.dashboard.kpi.last12months', 'last 12 months') },
98
- { id: 'ltv', title: t('customers.companies.dashboard.kpi.ltv', 'CUSTOMER VALUE (LTV)'), value: ltvValue, formatValue: ltvValue !== null ? (v: number) => formatCurrency(v, dealCurrency) : undefined, comparisonLabel: ltvValue !== null ? t('customers.companies.dashboard.kpi.wonDeals', 'won deals total') : t('customers.companies.dashboard.kpi.noWonDeals', 'No won deals') },
99
- { id: 'clientSince', title: t('customers.companies.dashboard.kpi.clientSince', 'CLIENT SINCE'), value: clientTenureYears, formatValue: clientTenureYears !== null ? (v: number) => v < 1 ? `< 1 ${t('customers.companies.dashboard.kpi.year', 'year')}` : `${v} ${v === 1 ? t('customers.companies.dashboard.kpi.year', 'year') : t('customers.companies.dashboard.kpi.years', 'years')}` : undefined, comparisonLabel: clientTenureYears !== null ? `${data.deals.filter(d => d.status === 'won').length} ${t('customers.companies.dashboard.kpi.completedDeals', 'completed deals')}` : t('customers.companies.dashboard.kpi.noInteractions', 'No interactions yet') },
100
- ]
101
-
102
- return (
103
- <div className="space-y-6">
104
- <div className="grid grid-cols-2 gap-4 lg:grid-cols-4">
105
- {kpiTiles.filter((tile) => !hiddenTiles.has(tile.id)).map((tile) => (
106
- <div key={tile.id} className="relative group">
107
- <KpiCard
108
- title={tile.title}
109
- value={tile.value}
110
- trend={tile.trend}
111
- formatValue={tile.formatValue}
112
- comparisonLabel={tile.comparisonLabel}
113
- />
114
- <IconButton
115
- type="button"
116
- variant="ghost"
117
- size="sm"
118
- onClick={() => toggleTile(tile.id)}
119
- className="h-auto absolute top-2 right-2 opacity-0 group-hover:opacity-60 transition-opacity text-muted-foreground hover:text-foreground p-0"
120
- aria-label={t('customers.companies.dashboard.hideTile', 'Hide tile')}
121
- >
122
- <EyeOff className="size-3.5" />
123
- </IconButton>
124
- </div>
125
- ))}
126
- </div>
127
- {hiddenTiles.size > 0 && (
128
- <div className="flex items-center gap-2">
129
- <span className="text-xs text-muted-foreground">
130
- {t('customers.companies.dashboard.hiddenTiles', '{{count}} tiles hidden', { count: hiddenTiles.size })}
131
- </span>
132
- <Button type="button" variant="ghost" size="sm" className="h-auto text-xs px-1.5 py-0.5" onClick={() => { setHiddenTiles(new Set()); removeLocalStorageKey('om:dashboard-hidden-tiles') }}>
133
- {t('customers.companies.dashboard.showAll', 'Show all')}
134
- </Button>
135
- </div>
136
- )}
137
-
138
- <div className="grid grid-cols-1 gap-6 lg:grid-cols-[1fr_380px]">
139
- <div className="space-y-6">
140
- <UpcomingMeetingsWidget meetings={upcomingMeetings} t={t} />
141
- <OpenTasksWidget tasks={openTasks} t={t} onViewAll={() => onTabChange('activity-log')} currentUserId={null} />
142
- </div>
143
- <div className="space-y-6">
144
- <RelationshipHealthWidget health={healthScore} t={t} />
145
- <ActiveDealWidget deals={activeDeals} t={t} />
146
- </div>
147
- </div>
148
-
149
- <InlineActivityComposer
150
- entityType="company"
151
- entityId={companyId}
152
- onActivityCreated={onActivityCreated}
153
- runGuardedMutation={runGuardedMutation}
154
- onScheduleRequested={onScheduleRequested}
155
- useCanonicalInteractions={useCanonicalInteractions}
156
- />
157
-
158
- <RecentActivityWidget interactions={recentActivity} t={t} />
159
- </div>
160
- )
161
- }