@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.
- package/AGENTS.md +8 -0
- package/dist/generated/entities/module_config/index.js +4 -0
- package/dist/generated/entities/module_config/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +2 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/auth/lib/setup-app.js +1 -1
- package/dist/modules/auth/lib/setup-app.js.map +2 -2
- package/dist/modules/configs/data/entities.js +15 -1
- package/dist/modules/configs/data/entities.js.map +2 -2
- package/dist/modules/configs/lib/module-config-service.js +52 -19
- package/dist/modules/configs/lib/module-config-service.js.map +3 -3
- package/dist/modules/configs/migrations/Migration20260617150000.js +21 -0
- package/dist/modules/configs/migrations/Migration20260617150000.js.map +7 -0
- package/dist/modules/customers/api/activities/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js +26 -22
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyKpiBar.js +8 -7
- package/dist/modules/customers/components/detail/CompanyKpiBar.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyPeopleSection.js +6 -5
- package/dist/modules/customers/components/detail/CompanyPeopleSection.js.map +2 -2
- package/generated/entities/module_config/index.ts +2 -0
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +7 -7
- package/src/modules/auth/lib/setup-app.ts +1 -1
- package/src/modules/configs/data/entities.ts +17 -1
- package/src/modules/configs/lib/module-config-service.ts +83 -24
- package/src/modules/configs/migrations/.snapshot-open-mercato.json +41 -4
- package/src/modules/configs/migrations/.snapshot-openmercato.json +41 -4
- package/src/modules/configs/migrations/Migration20260617150000.ts +21 -0
- package/src/modules/customers/api/activities/route.ts +6 -0
- package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +29 -22
- package/src/modules/customers/components/detail/CompanyKpiBar.tsx +8 -7
- package/src/modules/customers/components/detail/CompanyPeopleSection.tsx +6 -5
- package/dist/modules/customers/components/detail/CompanyDashboardTab.js +0 -133
- package/dist/modules/customers/components/detail/CompanyDashboardTab.js.map +0 -7
- 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": "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",
|
|
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
|
-
}
|