@open-mercato/core 0.6.8-develop.7088.1.3873d81f42 → 0.6.8-develop.7090.1.4dbca7b350
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/modules/customers/components/DealsKpiStrip.js +7 -6
- package/dist/modules/customers/components/DealsKpiStrip.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyKpiBar.js +5 -4
- package/dist/modules/customers/components/detail/CompanyKpiBar.js.map +2 -2
- package/dist/modules/customers/components/detail/DealsSection.js +6 -5
- package/dist/modules/customers/components/detail/DealsSection.js.map +2 -2
- package/dist/modules/customers/components/detail/utils.js +5 -5
- package/dist/modules/customers/components/detail/utils.js.map +2 -2
- package/dist/modules/sales/backend/sales/documents/[id]/page.js +10 -8
- package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
- package/dist/modules/sales/components/PriceWithCurrency.js +7 -5
- package/dist/modules/sales/components/PriceWithCurrency.js.map +2 -2
- package/dist/modules/sales/components/documents/AdjustmentsSection.js +8 -6
- package/dist/modules/sales/components/documents/AdjustmentsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/ItemsSection.js +14 -7
- package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/LineItemDialog.js +8 -7
- package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/PaymentsSection.js +9 -8
- package/dist/modules/sales/components/documents/PaymentsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/ReturnsSection.js +8 -6
- package/dist/modules/sales/components/documents/ReturnsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesOrderDraftLines.js +5 -4
- package/dist/modules/sales/components/documents/SalesOrderDraftLines.js.map +2 -2
- package/dist/modules/sales/components/documents/ShipmentDialog.js +5 -4
- package/dist/modules/sales/components/documents/ShipmentDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/ShipmentsSection.js +8 -6
- package/dist/modules/sales/components/documents/ShipmentsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/lineItemUtils.js +2 -2
- package/dist/modules/sales/components/documents/lineItemUtils.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/i18n/de.json +3 -0
- package/src/modules/auth/i18n/en.json +3 -0
- package/src/modules/auth/i18n/es.json +3 -0
- package/src/modules/auth/i18n/ko.json +3 -0
- package/src/modules/auth/i18n/pl.json +3 -0
- package/src/modules/customers/components/DealsKpiStrip.tsx +7 -7
- package/src/modules/customers/components/detail/CompanyKpiBar.tsx +5 -4
- package/src/modules/customers/components/detail/DealsSection.tsx +11 -5
- package/src/modules/customers/components/detail/utils.ts +15 -5
- package/src/modules/sales/backend/sales/documents/[id]/page.tsx +14 -8
- package/src/modules/sales/components/PriceWithCurrency.tsx +17 -5
- package/src/modules/sales/components/documents/AdjustmentsSection.tsx +6 -5
- package/src/modules/sales/components/documents/ItemsSection.tsx +8 -1
- package/src/modules/sales/components/documents/LineItemDialog.tsx +9 -8
- package/src/modules/sales/components/documents/PaymentsSection.tsx +9 -8
- package/src/modules/sales/components/documents/ReturnsSection.tsx +6 -5
- package/src/modules/sales/components/documents/SalesOrderDraftLines.tsx +5 -4
- package/src/modules/sales/components/documents/ShipmentDialog.tsx +5 -4
- package/src/modules/sales/components/documents/ShipmentsSection.tsx +6 -5
- package/src/modules/sales/components/documents/lineItemUtils.ts +7 -2
|
@@ -10,12 +10,12 @@ import { Avatar, AvatarStack } from "@open-mercato/ui/primitives/avatar";
|
|
|
10
10
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
11
11
|
import { Spinner } from "@open-mercato/ui/primitives/spinner";
|
|
12
12
|
import { PipelineStageBar } from "./kpi/PipelineStageBar.js";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return
|
|
13
|
+
function createCompactFormatter(locale) {
|
|
14
|
+
const formatter = new Intl.NumberFormat(locale, {
|
|
15
|
+
notation: "compact",
|
|
16
|
+
maximumFractionDigits: 1
|
|
17
|
+
});
|
|
18
|
+
return (value) => formatter.format(value);
|
|
19
19
|
}
|
|
20
20
|
function buildCurrencySuffix(code, convertedAll) {
|
|
21
21
|
if (!code) return convertedAll ? "" : "\u2248";
|
|
@@ -62,6 +62,7 @@ function DealsKpiStrip({
|
|
|
62
62
|
}) {
|
|
63
63
|
const t = useT();
|
|
64
64
|
const locale = useLocale();
|
|
65
|
+
const formatCompact = React.useMemo(() => createCompactFormatter(locale), [locale]);
|
|
65
66
|
const pluralCat = React.useCallback((count) => {
|
|
66
67
|
try {
|
|
67
68
|
return new Intl.PluralRules(locale).select(count);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/customers/components/DealsKpiStrip.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { CheckCircle } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT, useLocale } from '@open-mercato/shared/lib/i18n/context'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { KpiCard, DeltaBadge, Sparkline } from '@open-mercato/ui/backend/charts'\nimport { Avatar, AvatarStack } from '@open-mercato/ui/primitives/avatar'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport type { DictionaryMap } from '@open-mercato/core/modules/dictionaries/components/dictionaryAppearance'\nimport { PipelineStageBar } from './kpi/PipelineStageBar'\n\ntype DeltaDirection = 'up' | 'down' | 'unchanged'\n\ntype SummaryDelta = {\n value: number\n direction: DeltaDirection\n}\n\ntype DealsSummaryResponse = {\n baseCurrencyCode: string | null\n convertedAll: boolean\n missingRateCurrencies: string[]\n pipelineValue: {\n value: number\n delta: SummaryDelta\n stages: { stage: string | null; count: number; value: number }[]\n }\n activeDeals: {\n value: number\n delta: SummaryDelta\n ownersCount: number\n needAttention: number\n owners: { id: string; count: number }[]\n ownersOverflow: number\n }\n wonThisQuarter: {\n value: number\n delta: SummaryDelta\n dealsClosed: number\n avgDeal: number\n }\n winRate: {\n value: number\n deltaPp: number\n direction: DeltaDirection\n previousValue: number\n series: { period: string; rate: number }[]\n }\n}\n\nexport type DealsKpiStripProps = {\n ownerNames: Record<string, string>\n stageDictionary: DictionaryMap\n pipelineCount: number\n className?: string\n /** Bumped by the host when the active org scope changes \u2014 forces a KPI refetch so the cards never show another org's data. */\n scopeVersion?: number\n /** Bumped by the host on manual refresh / after mutations \u2014 forces a KPI refetch so totals stay in sync with the table. */\n reloadToken?: number\n onNeedsAttentionClick?: () => void\n}\n\nconst compactNumberFormatter = new Intl.NumberFormat(undefined, {\n notation: 'compact',\n maximumFractionDigits: 1,\n})\n\nfunction formatCompact(value: number): string {\n return compactNumberFormatter.format(value)\n}\n\nfunction buildCurrencySuffix(code: string | null, convertedAll: boolean): string {\n if (!code) return convertedAll ? '' : '\u2248'\n return convertedAll ? code : `\u2248 ${code}`\n}\n\nconst KPI_TITLE_CLASS = 'text-xs font-semibold uppercase tracking-wide text-muted-foreground'\n\nfunction DealKpiCard(props: React.ComponentProps<typeof KpiCard>) {\n return <KpiCard titleClassName={KPI_TITLE_CLASS} {...props} />\n}\n\nfunction KpiDeltaBadge({\n direction,\n value,\n unit,\n title,\n}: {\n direction: DeltaDirection\n value: number\n unit?: string\n title: string\n}) {\n if (direction === 'unchanged' && value === 0) {\n return (\n <span\n className=\"inline-flex items-center rounded-md bg-status-neutral-bg px-2 py-0.5 text-xs font-medium text-status-neutral-text\"\n title={title}\n >\n --\n </span>\n )\n }\n return <DeltaBadge direction={direction} value={value} unit={unit} title={title} />\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null\n}\n\n// Guard the summary payload before rendering: a non-conforming response (an unrelated\n// endpoint mock, an error body, or a future contract drift) must surface the error card,\n// never crash the whole deals page by dereferencing missing sections/arrays.\nfunction isDealsSummaryResponse(value: unknown): value is DealsSummaryResponse {\n if (!isObject(value)) return false\n const { pipelineValue, activeDeals, wonThisQuarter, winRate } = value\n return (\n isObject(pipelineValue) && Array.isArray(pipelineValue.stages) &&\n isObject(activeDeals) && Array.isArray(activeDeals.owners) &&\n isObject(wonThisQuarter) &&\n isObject(winRate) && Array.isArray(winRate.series)\n )\n}\n\nexport function DealsKpiStrip({\n ownerNames,\n stageDictionary,\n pipelineCount,\n className,\n scopeVersion,\n reloadToken,\n onNeedsAttentionClick,\n}: DealsKpiStripProps) {\n const t = useT()\n const locale = useLocale()\n const pluralCat = React.useCallback((count: number): string => {\n try {\n return new Intl.PluralRules(locale).select(count)\n } catch {\n return count === 1 ? 'one' : 'other'\n }\n }, [locale])\n const pf = React.useCallback((base: string, count: number): string => {\n const cat = pluralCat(count)\n const key = `${base}.${cat}`\n const out = t(key, { count })\n return out === key ? t(`${base}.other`, { count }) : out\n }, [t, pluralCat])\n const [data, setData] = React.useState<DealsSummaryResponse | null>(null)\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [retryToken, setRetryToken] = React.useState(0)\n const previousScopeVersionRef = React.useRef(scopeVersion)\n\n const retry = React.useCallback(() => {\n setRetryToken((token) => token + 1)\n }, [])\n\n React.useEffect(() => {\n let cancelled = false\n const scopeChanged = previousScopeVersionRef.current !== scopeVersion\n previousScopeVersionRef.current = scopeVersion\n if (scopeChanged) setData(null)\n setLoading(true)\n setError(null)\n apiCall<DealsSummaryResponse>('/api/customers/deals/summary')\n .then((call) => {\n if (cancelled) return\n if (!call.ok || !isDealsSummaryResponse(call.result)) {\n setError(t('customers.deals.list.kpi.error'))\n return\n }\n setData(call.result)\n })\n .catch(() => {\n if (cancelled) return\n setError(t('customers.deals.list.kpi.error'))\n })\n .finally(() => {\n if (!cancelled) setLoading(false)\n })\n return () => {\n cancelled = true\n }\n }, [t, scopeVersion, reloadToken, retryToken])\n\n const wrapperClassName = cn('space-y-2', className)\n const gridClassName = 'grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-4'\n\n if (loading && !data) {\n return (\n <div className={wrapperClassName}>\n <div className={gridClassName}>\n <DealKpiCard loading title={t('customers.deals.list.kpi.pipelineValue')} value={null} />\n <DealKpiCard loading title={t('customers.deals.list.kpi.activeDeals')} value={null} />\n <DealKpiCard loading title={t('customers.deals.list.kpi.wonThisQuarter')} value={null} />\n <DealKpiCard loading title={t('customers.deals.list.kpi.winRate')} value={null} />\n </div>\n </div>\n )\n }\n\n if (!data) {\n const errorMessage = error ?? t('customers.deals.list.kpi.error')\n return (\n <div className={wrapperClassName}>\n <div className=\"flex items-center justify-between gap-3 rounded-lg border border-destructive/30 bg-destructive/5 p-4\">\n <p className=\"text-sm text-destructive\">{errorMessage}</p>\n <Button type=\"button\" variant=\"destructive-outline\" size=\"sm\" onClick={retry}>\n {t('customers.deals.list.kpi.retry')}\n </Button>\n </div>\n </div>\n )\n }\n\n const currencySuffix = buildCurrencySuffix(data.baseCurrencyCode, data.convertedAll)\n const unassignedLabel = t('customers.deals.list.kpi.unassignedStage')\n const deltaTooltip = t('customers.deals.list.kpi.deltaTooltip')\n const deltaUnavailableTooltip = t('customers.deals.list.kpi.deltaUnavailable')\n const scopeLabel = t('customers.deals.list.kpi.scopeAllPipelinesThisQuarter')\n const unknownOwner = t('customers.deals.list.unknownOwner')\n const currencyHint = !data.convertedAll\n ? data.baseCurrencyCode\n ? t('customers.deals.list.kpi.currencyApproxMissing', {\n currencies: data.missingRateCurrencies.length ? data.missingRateCurrencies.join(', ') : currencySuffix,\n })\n : t('customers.deals.list.kpi.currencyApproxNoBase')\n : null\n const attentionLabel = pf('customers.deals.list.kpi.frag.needAttention', data.activeDeals.needAttention)\n\n return (\n <div className={wrapperClassName}>\n {error ? (\n <div className=\"flex items-center justify-between gap-3 rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2\">\n <p className=\"text-xs text-destructive\">{error}</p>\n <Button type=\"button\" variant=\"destructive-outline\" size=\"2xs\" onClick={retry}>\n {t('customers.deals.list.kpi.retry')}\n </Button>\n </div>\n ) : null}\n {loading ? (\n <div className=\"flex items-center justify-end gap-2 text-xs text-muted-foreground\">\n <Spinner className=\"h-3 w-3\" />\n <span>{t('customers.deals.list.kpi.updating')}</span>\n </div>\n ) : null}\n <div className={gridClassName}>\n <DealKpiCard\n title={t('customers.deals.list.kpi.pipelineValue')}\n value={data.pipelineValue.value}\n formatValue={formatCompact}\n suffix={currencySuffix}\n headerAction={\n <KpiDeltaBadge\n direction={data.pipelineValue.delta.direction}\n value={data.pipelineValue.delta.value}\n title={data.pipelineValue.delta.direction === 'unchanged' && data.pipelineValue.delta.value === 0 ? deltaUnavailableTooltip : deltaTooltip}\n />\n }\n footer={\n <div className=\"space-y-2\">\n <p className=\"text-xs text-muted-foreground\">{scopeLabel}</p>\n <p className=\"text-xs text-muted-foreground\">\n {t('customers.deals.list.kpi.activeAcrossPipelines', {\n deals: pf('customers.deals.list.kpi.frag.activeDeals', data.activeDeals.value),\n pipelines: pf('customers.deals.list.kpi.frag.pipelines', pipelineCount),\n })}\n </p>\n {currencyHint ? <p className=\"text-xs text-muted-foreground\">{currencyHint}</p> : null}\n <PipelineStageBar\n stages={data.pipelineValue.stages}\n stageDictionary={stageDictionary}\n unassignedLabel={unassignedLabel}\n />\n </div>\n }\n />\n\n <DealKpiCard\n title={t('customers.deals.list.kpi.activeDeals')}\n value={data.activeDeals.value}\n formatValue={formatCompact}\n headerAction={\n <KpiDeltaBadge\n direction={data.activeDeals.delta.direction}\n value={data.activeDeals.delta.value}\n title={data.activeDeals.delta.direction === 'unchanged' && data.activeDeals.delta.value === 0 ? deltaUnavailableTooltip : deltaTooltip}\n />\n }\n footer={\n <div className=\"space-y-2\">\n <p className=\"text-xs text-muted-foreground\">{scopeLabel}</p>\n <div className=\"flex flex-wrap items-center gap-x-1.5 gap-y-1 text-xs text-muted-foreground\">\n <span>{pf('customers.deals.list.kpi.frag.owners', data.activeDeals.ownersCount)}</span>\n <span aria-hidden=\"true\">\u00B7</span>\n {onNeedsAttentionClick && data.activeDeals.needAttention > 0 ? (\n <Button\n type=\"button\"\n variant=\"link\"\n size=\"2xs\"\n className=\"h-auto p-0 text-xs\"\n onClick={onNeedsAttentionClick}\n >\n {attentionLabel}\n </Button>\n ) : (\n <span>{attentionLabel}</span>\n )}\n </div>\n {data.activeDeals.owners.length > 0 ? (\n <AvatarStack max={4} size=\"sm\" overflowCount={data.activeDeals.ownersOverflow}>\n {data.activeDeals.owners.map((owner) => {\n const ownerLabel = ownerNames[owner.id]?.trim() || unknownOwner\n return <Avatar key={owner.id} label={ownerLabel} size=\"sm\" />\n })}\n </AvatarStack>\n ) : null}\n </div>\n }\n />\n\n <DealKpiCard\n title={t('customers.deals.list.kpi.wonThisQuarter')}\n value={data.wonThisQuarter.value}\n formatValue={formatCompact}\n suffix={currencySuffix}\n headerAction={\n <KpiDeltaBadge\n direction={data.wonThisQuarter.delta.direction}\n value={data.wonThisQuarter.delta.value}\n title={data.wonThisQuarter.delta.direction === 'unchanged' && data.wonThisQuarter.delta.value === 0 ? deltaUnavailableTooltip : deltaTooltip}\n />\n }\n footer={\n <div className=\"space-y-1\">\n <p className=\"text-xs text-muted-foreground\">{scopeLabel}</p>\n <div className=\"flex items-center gap-1.5 text-xs text-muted-foreground\">\n <CheckCircle className=\"h-4 w-4 text-status-success-text\" aria-hidden />\n <span>\n {pf('customers.deals.list.kpi.frag.dealsClosed', data.wonThisQuarter.dealsClosed)}\n </span>\n </div>\n <p className=\"text-xs text-muted-foreground\">\n {t('customers.deals.list.kpi.avgDeal', {\n value: `${formatCompact(data.wonThisQuarter.avgDeal)}${currencySuffix ? ` ${currencySuffix}` : ''}`,\n })}\n </p>\n {currencyHint ? <p className=\"text-xs text-muted-foreground\">{currencyHint}</p> : null}\n </div>\n }\n />\n\n <DealKpiCard\n title={t('customers.deals.list.kpi.winRate')}\n value={data.winRate.value}\n suffix=\"%\"\n headerAction={\n <KpiDeltaBadge\n direction={data.winRate.direction}\n value={Math.abs(data.winRate.deltaPp)}\n unit=\"pp\"\n title={data.winRate.previousValue === 0 ? deltaUnavailableTooltip : deltaTooltip}\n />\n }\n footer={\n <div className=\"space-y-2\">\n <p className=\"text-xs text-muted-foreground\">{scopeLabel}</p>\n <p className=\"text-xs text-muted-foreground\">\n {t('customers.deals.list.kpi.fromLastQuarter', { value: data.winRate.previousValue })}\n </p>\n <div className=\"text-primary\">\n <Sparkline\n values={data.winRate.series.map((point) => point.rate)}\n ariaLabel={t('customers.deals.list.kpi.winRate')}\n />\n </div>\n </div>\n }\n />\n </div>\n </div>\n )\n}\n\nexport default DealsKpiStrip\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { CheckCircle } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT, useLocale } from '@open-mercato/shared/lib/i18n/context'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { KpiCard, DeltaBadge, Sparkline } from '@open-mercato/ui/backend/charts'\nimport { Avatar, AvatarStack } from '@open-mercato/ui/primitives/avatar'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport type { DictionaryMap } from '@open-mercato/core/modules/dictionaries/components/dictionaryAppearance'\nimport { PipelineStageBar } from './kpi/PipelineStageBar'\n\ntype DeltaDirection = 'up' | 'down' | 'unchanged'\n\ntype SummaryDelta = {\n value: number\n direction: DeltaDirection\n}\n\ntype DealsSummaryResponse = {\n baseCurrencyCode: string | null\n convertedAll: boolean\n missingRateCurrencies: string[]\n pipelineValue: {\n value: number\n delta: SummaryDelta\n stages: { stage: string | null; count: number; value: number }[]\n }\n activeDeals: {\n value: number\n delta: SummaryDelta\n ownersCount: number\n needAttention: number\n owners: { id: string; count: number }[]\n ownersOverflow: number\n }\n wonThisQuarter: {\n value: number\n delta: SummaryDelta\n dealsClosed: number\n avgDeal: number\n }\n winRate: {\n value: number\n deltaPp: number\n direction: DeltaDirection\n previousValue: number\n series: { period: string; rate: number }[]\n }\n}\n\nexport type DealsKpiStripProps = {\n ownerNames: Record<string, string>\n stageDictionary: DictionaryMap\n pipelineCount: number\n className?: string\n /** Bumped by the host when the active org scope changes \u2014 forces a KPI refetch so the cards never show another org's data. */\n scopeVersion?: number\n /** Bumped by the host on manual refresh / after mutations \u2014 forces a KPI refetch so totals stay in sync with the table. */\n reloadToken?: number\n onNeedsAttentionClick?: () => void\n}\n\nfunction createCompactFormatter(locale: string): (value: number) => string {\n const formatter = new Intl.NumberFormat(locale, {\n notation: 'compact',\n maximumFractionDigits: 1,\n })\n return (value: number) => formatter.format(value)\n}\n\nfunction buildCurrencySuffix(code: string | null, convertedAll: boolean): string {\n if (!code) return convertedAll ? '' : '\u2248'\n return convertedAll ? code : `\u2248 ${code}`\n}\n\nconst KPI_TITLE_CLASS = 'text-xs font-semibold uppercase tracking-wide text-muted-foreground'\n\nfunction DealKpiCard(props: React.ComponentProps<typeof KpiCard>) {\n return <KpiCard titleClassName={KPI_TITLE_CLASS} {...props} />\n}\n\nfunction KpiDeltaBadge({\n direction,\n value,\n unit,\n title,\n}: {\n direction: DeltaDirection\n value: number\n unit?: string\n title: string\n}) {\n if (direction === 'unchanged' && value === 0) {\n return (\n <span\n className=\"inline-flex items-center rounded-md bg-status-neutral-bg px-2 py-0.5 text-xs font-medium text-status-neutral-text\"\n title={title}\n >\n --\n </span>\n )\n }\n return <DeltaBadge direction={direction} value={value} unit={unit} title={title} />\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null\n}\n\n// Guard the summary payload before rendering: a non-conforming response (an unrelated\n// endpoint mock, an error body, or a future contract drift) must surface the error card,\n// never crash the whole deals page by dereferencing missing sections/arrays.\nfunction isDealsSummaryResponse(value: unknown): value is DealsSummaryResponse {\n if (!isObject(value)) return false\n const { pipelineValue, activeDeals, wonThisQuarter, winRate } = value\n return (\n isObject(pipelineValue) && Array.isArray(pipelineValue.stages) &&\n isObject(activeDeals) && Array.isArray(activeDeals.owners) &&\n isObject(wonThisQuarter) &&\n isObject(winRate) && Array.isArray(winRate.series)\n )\n}\n\nexport function DealsKpiStrip({\n ownerNames,\n stageDictionary,\n pipelineCount,\n className,\n scopeVersion,\n reloadToken,\n onNeedsAttentionClick,\n}: DealsKpiStripProps) {\n const t = useT()\n const locale = useLocale()\n const formatCompact = React.useMemo(() => createCompactFormatter(locale), [locale])\n const pluralCat = React.useCallback((count: number): string => {\n try {\n return new Intl.PluralRules(locale).select(count)\n } catch {\n return count === 1 ? 'one' : 'other'\n }\n }, [locale])\n const pf = React.useCallback((base: string, count: number): string => {\n const cat = pluralCat(count)\n const key = `${base}.${cat}`\n const out = t(key, { count })\n return out === key ? t(`${base}.other`, { count }) : out\n }, [t, pluralCat])\n const [data, setData] = React.useState<DealsSummaryResponse | null>(null)\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [retryToken, setRetryToken] = React.useState(0)\n const previousScopeVersionRef = React.useRef(scopeVersion)\n\n const retry = React.useCallback(() => {\n setRetryToken((token) => token + 1)\n }, [])\n\n React.useEffect(() => {\n let cancelled = false\n const scopeChanged = previousScopeVersionRef.current !== scopeVersion\n previousScopeVersionRef.current = scopeVersion\n if (scopeChanged) setData(null)\n setLoading(true)\n setError(null)\n apiCall<DealsSummaryResponse>('/api/customers/deals/summary')\n .then((call) => {\n if (cancelled) return\n if (!call.ok || !isDealsSummaryResponse(call.result)) {\n setError(t('customers.deals.list.kpi.error'))\n return\n }\n setData(call.result)\n })\n .catch(() => {\n if (cancelled) return\n setError(t('customers.deals.list.kpi.error'))\n })\n .finally(() => {\n if (!cancelled) setLoading(false)\n })\n return () => {\n cancelled = true\n }\n }, [t, scopeVersion, reloadToken, retryToken])\n\n const wrapperClassName = cn('space-y-2', className)\n const gridClassName = 'grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-4'\n\n if (loading && !data) {\n return (\n <div className={wrapperClassName}>\n <div className={gridClassName}>\n <DealKpiCard loading title={t('customers.deals.list.kpi.pipelineValue')} value={null} />\n <DealKpiCard loading title={t('customers.deals.list.kpi.activeDeals')} value={null} />\n <DealKpiCard loading title={t('customers.deals.list.kpi.wonThisQuarter')} value={null} />\n <DealKpiCard loading title={t('customers.deals.list.kpi.winRate')} value={null} />\n </div>\n </div>\n )\n }\n\n if (!data) {\n const errorMessage = error ?? t('customers.deals.list.kpi.error')\n return (\n <div className={wrapperClassName}>\n <div className=\"flex items-center justify-between gap-3 rounded-lg border border-destructive/30 bg-destructive/5 p-4\">\n <p className=\"text-sm text-destructive\">{errorMessage}</p>\n <Button type=\"button\" variant=\"destructive-outline\" size=\"sm\" onClick={retry}>\n {t('customers.deals.list.kpi.retry')}\n </Button>\n </div>\n </div>\n )\n }\n\n const currencySuffix = buildCurrencySuffix(data.baseCurrencyCode, data.convertedAll)\n const unassignedLabel = t('customers.deals.list.kpi.unassignedStage')\n const deltaTooltip = t('customers.deals.list.kpi.deltaTooltip')\n const deltaUnavailableTooltip = t('customers.deals.list.kpi.deltaUnavailable')\n const scopeLabel = t('customers.deals.list.kpi.scopeAllPipelinesThisQuarter')\n const unknownOwner = t('customers.deals.list.unknownOwner')\n const currencyHint = !data.convertedAll\n ? data.baseCurrencyCode\n ? t('customers.deals.list.kpi.currencyApproxMissing', {\n currencies: data.missingRateCurrencies.length ? data.missingRateCurrencies.join(', ') : currencySuffix,\n })\n : t('customers.deals.list.kpi.currencyApproxNoBase')\n : null\n const attentionLabel = pf('customers.deals.list.kpi.frag.needAttention', data.activeDeals.needAttention)\n\n return (\n <div className={wrapperClassName}>\n {error ? (\n <div className=\"flex items-center justify-between gap-3 rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2\">\n <p className=\"text-xs text-destructive\">{error}</p>\n <Button type=\"button\" variant=\"destructive-outline\" size=\"2xs\" onClick={retry}>\n {t('customers.deals.list.kpi.retry')}\n </Button>\n </div>\n ) : null}\n {loading ? (\n <div className=\"flex items-center justify-end gap-2 text-xs text-muted-foreground\">\n <Spinner className=\"h-3 w-3\" />\n <span>{t('customers.deals.list.kpi.updating')}</span>\n </div>\n ) : null}\n <div className={gridClassName}>\n <DealKpiCard\n title={t('customers.deals.list.kpi.pipelineValue')}\n value={data.pipelineValue.value}\n formatValue={formatCompact}\n suffix={currencySuffix}\n headerAction={\n <KpiDeltaBadge\n direction={data.pipelineValue.delta.direction}\n value={data.pipelineValue.delta.value}\n title={data.pipelineValue.delta.direction === 'unchanged' && data.pipelineValue.delta.value === 0 ? deltaUnavailableTooltip : deltaTooltip}\n />\n }\n footer={\n <div className=\"space-y-2\">\n <p className=\"text-xs text-muted-foreground\">{scopeLabel}</p>\n <p className=\"text-xs text-muted-foreground\">\n {t('customers.deals.list.kpi.activeAcrossPipelines', {\n deals: pf('customers.deals.list.kpi.frag.activeDeals', data.activeDeals.value),\n pipelines: pf('customers.deals.list.kpi.frag.pipelines', pipelineCount),\n })}\n </p>\n {currencyHint ? <p className=\"text-xs text-muted-foreground\">{currencyHint}</p> : null}\n <PipelineStageBar\n stages={data.pipelineValue.stages}\n stageDictionary={stageDictionary}\n unassignedLabel={unassignedLabel}\n />\n </div>\n }\n />\n\n <DealKpiCard\n title={t('customers.deals.list.kpi.activeDeals')}\n value={data.activeDeals.value}\n formatValue={formatCompact}\n headerAction={\n <KpiDeltaBadge\n direction={data.activeDeals.delta.direction}\n value={data.activeDeals.delta.value}\n title={data.activeDeals.delta.direction === 'unchanged' && data.activeDeals.delta.value === 0 ? deltaUnavailableTooltip : deltaTooltip}\n />\n }\n footer={\n <div className=\"space-y-2\">\n <p className=\"text-xs text-muted-foreground\">{scopeLabel}</p>\n <div className=\"flex flex-wrap items-center gap-x-1.5 gap-y-1 text-xs text-muted-foreground\">\n <span>{pf('customers.deals.list.kpi.frag.owners', data.activeDeals.ownersCount)}</span>\n <span aria-hidden=\"true\">\u00B7</span>\n {onNeedsAttentionClick && data.activeDeals.needAttention > 0 ? (\n <Button\n type=\"button\"\n variant=\"link\"\n size=\"2xs\"\n className=\"h-auto p-0 text-xs\"\n onClick={onNeedsAttentionClick}\n >\n {attentionLabel}\n </Button>\n ) : (\n <span>{attentionLabel}</span>\n )}\n </div>\n {data.activeDeals.owners.length > 0 ? (\n <AvatarStack max={4} size=\"sm\" overflowCount={data.activeDeals.ownersOverflow}>\n {data.activeDeals.owners.map((owner) => {\n const ownerLabel = ownerNames[owner.id]?.trim() || unknownOwner\n return <Avatar key={owner.id} label={ownerLabel} size=\"sm\" />\n })}\n </AvatarStack>\n ) : null}\n </div>\n }\n />\n\n <DealKpiCard\n title={t('customers.deals.list.kpi.wonThisQuarter')}\n value={data.wonThisQuarter.value}\n formatValue={formatCompact}\n suffix={currencySuffix}\n headerAction={\n <KpiDeltaBadge\n direction={data.wonThisQuarter.delta.direction}\n value={data.wonThisQuarter.delta.value}\n title={data.wonThisQuarter.delta.direction === 'unchanged' && data.wonThisQuarter.delta.value === 0 ? deltaUnavailableTooltip : deltaTooltip}\n />\n }\n footer={\n <div className=\"space-y-1\">\n <p className=\"text-xs text-muted-foreground\">{scopeLabel}</p>\n <div className=\"flex items-center gap-1.5 text-xs text-muted-foreground\">\n <CheckCircle className=\"h-4 w-4 text-status-success-text\" aria-hidden />\n <span>\n {pf('customers.deals.list.kpi.frag.dealsClosed', data.wonThisQuarter.dealsClosed)}\n </span>\n </div>\n <p className=\"text-xs text-muted-foreground\">\n {t('customers.deals.list.kpi.avgDeal', {\n value: `${formatCompact(data.wonThisQuarter.avgDeal)}${currencySuffix ? ` ${currencySuffix}` : ''}`,\n })}\n </p>\n {currencyHint ? <p className=\"text-xs text-muted-foreground\">{currencyHint}</p> : null}\n </div>\n }\n />\n\n <DealKpiCard\n title={t('customers.deals.list.kpi.winRate')}\n value={data.winRate.value}\n suffix=\"%\"\n headerAction={\n <KpiDeltaBadge\n direction={data.winRate.direction}\n value={Math.abs(data.winRate.deltaPp)}\n unit=\"pp\"\n title={data.winRate.previousValue === 0 ? deltaUnavailableTooltip : deltaTooltip}\n />\n }\n footer={\n <div className=\"space-y-2\">\n <p className=\"text-xs text-muted-foreground\">{scopeLabel}</p>\n <p className=\"text-xs text-muted-foreground\">\n {t('customers.deals.list.kpi.fromLastQuarter', { value: data.winRate.previousValue })}\n </p>\n <div className=\"text-primary\">\n <Sparkline\n values={data.winRate.series.map((point) => point.rate)}\n ariaLabel={t('customers.deals.list.kpi.winRate')}\n />\n </div>\n </div>\n }\n />\n </div>\n </div>\n )\n}\n\nexport default DealsKpiStrip\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { EyeOff } from "lucide-react";
|
|
5
|
-
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
5
|
+
import { useT, useLocale } from "@open-mercato/shared/lib/i18n/context";
|
|
6
6
|
import { KpiCard } from "@open-mercato/ui/backend/charts/KpiCard";
|
|
7
7
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
8
8
|
import { IconButton } from "@open-mercato/ui/primitives/icon-button";
|
|
@@ -54,6 +54,7 @@ function computeDealTrend(deals) {
|
|
|
54
54
|
}
|
|
55
55
|
function CompanyKpiBar({ data }) {
|
|
56
56
|
const t = useT();
|
|
57
|
+
const locale = useLocale();
|
|
57
58
|
const activeDeals = React.useMemo(() => getActiveDeals(data.deals), [data.deals]);
|
|
58
59
|
const activeDealsValue = React.useMemo(
|
|
59
60
|
() => data.kpis?.activeDealsValue ?? sumActiveDeals(data.deals),
|
|
@@ -103,7 +104,7 @@ function CompanyKpiBar({ data }) {
|
|
|
103
104
|
title: t("customers.companies.dashboard.kpi.activeDeals", "ACTIVE DEALS"),
|
|
104
105
|
value: activeDealsValue,
|
|
105
106
|
trend: dealTrend,
|
|
106
|
-
formatValue: (v) => formatCurrency(v, dealCurrency),
|
|
107
|
+
formatValue: (v) => formatCurrency(v, dealCurrency, locale),
|
|
107
108
|
comparisonLabel: `${data.kpis?.activeDealsCount ?? activeDeals.length} ${(data.kpis?.activeDealsCount ?? activeDeals.length) === 1 ? "pipeline" : "pipelines"}`
|
|
108
109
|
},
|
|
109
110
|
{
|
|
@@ -117,7 +118,7 @@ function CompanyKpiBar({ data }) {
|
|
|
117
118
|
id: "ltv",
|
|
118
119
|
title: t("customers.companies.dashboard.kpi.ltv", "CUSTOMER VALUE (LTV)"),
|
|
119
120
|
value: ltvValue,
|
|
120
|
-
formatValue: ltvValue !== null ? (v) => formatCurrency(v, dealCurrency) : void 0,
|
|
121
|
+
formatValue: ltvValue !== null ? (v) => formatCurrency(v, dealCurrency, locale) : void 0,
|
|
121
122
|
comparisonLabel: ltvValue !== null ? t("customers.companies.dashboard.kpi.wonDeals", "won deals total") : t("customers.companies.dashboard.kpi.noWonDeals", "No won deals")
|
|
122
123
|
},
|
|
123
124
|
{
|
|
@@ -127,7 +128,7 @@ function CompanyKpiBar({ data }) {
|
|
|
127
128
|
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,
|
|
128
129
|
comparisonLabel: clientTenureYears !== null ? `${data.kpis?.completedDealsCount ?? data.deals.filter((d) => isWonDealStatus(d.status)).length} ${t("customers.companies.dashboard.kpi.completedDeals", "completed deals")}` : t("customers.companies.dashboard.kpi.noInteractions", "No interactions yet")
|
|
129
130
|
}
|
|
130
|
-
], [t, activeDealsValue, dealTrend, dealCurrency, activeDeals.length, activityTrend, ltvValue, clientTenureYears, data.deals, data.interactions.length, data.kpis]);
|
|
131
|
+
], [t, locale, activeDealsValue, dealTrend, dealCurrency, activeDeals.length, activityTrend, ltvValue, clientTenureYears, data.deals, data.interactions.length, data.kpis]);
|
|
131
132
|
const visibleTiles = kpiTiles.filter((tile) => !hiddenTiles.has(tile.id));
|
|
132
133
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
133
134
|
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4", children: visibleTiles.map((tile) => /* @__PURE__ */ jsxs("div", { className: "group relative", children: [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customers/components/detail/CompanyKpiBar.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 {\n readVersionedIdSet,\n writeVersionedIdSet,\n clearVersionedPreference,\n} from '@open-mercato/shared/lib/browser/versionedPreference'\nimport { isOpenDealStatus, isWonDealStatus } from '../../lib/dealStatus'\nimport type { CompanyOverview, DealSummary, InteractionSummary } from '../formConfig'\nimport { formatCurrency } from './utils'\n\nconst STORAGE_KEY = 'om:company-detail-kpi-hidden'\nconst STORAGE_VERSION = 1\n\nfunction sumActiveDeals(deals: DealSummary[]): number {\n return deals\n .filter((d) => isOpenDealStatus(d.status))\n .reduce((sum, d) => {\n const amount = typeof d.valueAmount === 'number' ? d.valueAmount : parseFloat(String(d.valueAmount ?? '0'))\n return sum + (Number.isFinite(amount) ? amount : 0)\n }, 0)\n}\n\nfunction getActiveDeals(deals: DealSummary[]): DealSummary[] {\n return deals.filter((d) => isOpenDealStatus(d.status))\n}\n\nfunction computeActivityTrend(interactions: InteractionSummary[]): KpiTrend | undefined {\n const now = Date.now()\n const weekMs = 7 * 86_400_000\n const thisWeek = interactions.filter((i) => {\n const d = i.occurredAt ?? i.scheduledAt\n return d && now - new Date(d).getTime() < weekMs\n }).length\n const lastWeek = interactions.filter((i) => {\n const d = i.occurredAt ?? i.scheduledAt\n if (!d) return false\n const diff = now - new Date(d).getTime()\n return diff >= weekMs && diff < weekMs * 2\n }).length\n if (lastWeek === 0 && thisWeek === 0) return undefined\n if (lastWeek === 0) return { value: 100, direction: 'up' }\n const pct = ((thisWeek - lastWeek) / lastWeek) * 100\n if (Math.abs(pct) < 0.5) return { value: 0, direction: 'unchanged' }\n return { value: Math.abs(pct), direction: pct > 0 ? 'up' : 'down' }\n}\n\nfunction computeDealTrend(deals: DealSummary[]): KpiTrend | undefined {\n const active = deals.filter((d) => isOpenDealStatus(d.status))\n if (active.length === 0) return undefined\n const now = Date.now()\n const monthMs = 30 * 86_400_000\n const recentDeals = active.filter((d) => d.createdAt && now - new Date(d.createdAt).getTime() < monthMs).length\n if (recentDeals > 0) return { value: recentDeals * 10, direction: 'up' }\n return { value: 0, direction: 'unchanged' }\n}\n\ntype CompanyKpiBarProps = {\n data: CompanyOverview\n}\n\nexport function CompanyKpiBar({ data }: CompanyKpiBarProps) {\n const t = useT()\n\n const activeDeals = React.useMemo(() => getActiveDeals(data.deals), [data.deals])\n const activeDealsValue = React.useMemo(\n () => data.kpis?.activeDealsValue ?? sumActiveDeals(data.deals),\n [data.deals, data.kpis?.activeDealsValue],\n )\n const dealCurrency = data.kpis?.dealCurrency ?? activeDeals[0]?.valueCurrency ?? data.deals[0]?.valueCurrency ?? 'PLN'\n const activityTrend = React.useMemo(\n () => data.kpis?.activityTrend ?? computeActivityTrend(data.interactions),\n [data.interactions, data.kpis?.activityTrend],\n )\n const dealTrend = React.useMemo(() => computeDealTrend(data.deals), [data.deals])\n\n const ltvValue = React.useMemo(() => {\n if (data.kpis?.ltvValue !== undefined) return data.kpis.ltvValue\n const wonDeals = data.deals.filter((d) => isWonDealStatus(d.status))\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, data.kpis?.ltvValue])\n\n const clientTenureYears = React.useMemo(() => {\n if (data.kpis?.clientTenureYears !== undefined) return data.kpis.clientTenureYears\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, data.kpis?.clientTenureYears])\n\n const [hiddenTiles, setHiddenTiles] = React.useState<Set<string>>(\n () => readVersionedIdSet(STORAGE_KEY, STORAGE_VERSION),\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 writeVersionedIdSet(STORAGE_KEY, STORAGE_VERSION, next)\n return next\n })\n }, [])\n\n const showAllTiles = React.useCallback(() => {\n setHiddenTiles(new Set())\n clearVersionedPreference(STORAGE_KEY)\n }, [])\n\n const kpiTiles = React.useMemo(() => [\n {\n id: 'activeDeals',\n title: t('customers.companies.dashboard.kpi.activeDeals', 'ACTIVE DEALS'),\n value: activeDealsValue,\n trend: dealTrend,\n formatValue: (v: number) => formatCurrency(v, dealCurrency),\n comparisonLabel: `${data.kpis?.activeDealsCount ?? activeDeals.length} ${(data.kpis?.activeDealsCount ?? activeDeals.length) === 1 ? 'pipeline' : 'pipelines'}`,\n },\n {\n id: 'activities',\n title: t('customers.companies.dashboard.kpi.activities', 'ACTIVITIES'),\n value: data.kpis?.activityCount ?? data.interactions.length,\n trend: activityTrend,\n comparisonLabel: t('customers.companies.dashboard.kpi.last12months', 'last 12 months'),\n },\n {\n id: 'ltv',\n title: t('customers.companies.dashboard.kpi.ltv', 'CUSTOMER VALUE (LTV)'),\n value: ltvValue,\n formatValue: ltvValue !== null ? (v: number) => formatCurrency(v, dealCurrency) : undefined,\n comparisonLabel: ltvValue !== null\n ? t('customers.companies.dashboard.kpi.wonDeals', 'won deals total')\n : t('customers.companies.dashboard.kpi.noWonDeals', 'No won deals'),\n },\n {\n id: 'clientSince',\n title: t('customers.companies.dashboard.kpi.clientSince', 'CLIENT SINCE'),\n value: clientTenureYears,\n formatValue: clientTenureYears !== null\n ? (v: number) => v < 1\n ? `< 1 ${t('customers.companies.dashboard.kpi.year', 'year')}`\n : `${v} ${v === 1 ? t('customers.companies.dashboard.kpi.year', 'year') : t('customers.companies.dashboard.kpi.years', 'years')}`\n : undefined,\n comparisonLabel: clientTenureYears !== null\n ? `${data.kpis?.completedDealsCount ?? data.deals.filter((d) => isWonDealStatus(d.status)).length} ${t('customers.companies.dashboard.kpi.completedDeals', 'completed deals')}`\n : t('customers.companies.dashboard.kpi.noInteractions', 'No interactions yet'),\n },\n ], [t, activeDealsValue, dealTrend, dealCurrency, activeDeals.length, activityTrend, ltvValue, clientTenureYears, data.deals, data.interactions.length, data.kpis])\n\n const visibleTiles = kpiTiles.filter((tile) => !hiddenTiles.has(tile.id))\n\n return (\n <div className=\"space-y-2\">\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4\">\n {visibleTiles.map((tile) => (\n <div key={tile.id} className=\"group relative\">\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=\"xs\"\n onClick={() => toggleTile(tile.id)}\n className=\"absolute right-2 top-2 opacity-0 transition-opacity group-hover:opacity-60\"\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\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-auto px-1.5 py-0.5 text-xs hover:bg-transparent\"\n onClick={showAllTiles}\n >\n {t('customers.companies.dashboard.showAll', 'Show all')}\n </Button>\n </div>\n )}\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { EyeOff } from 'lucide-react'\nimport { useT, useLocale } 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 {\n readVersionedIdSet,\n writeVersionedIdSet,\n clearVersionedPreference,\n} from '@open-mercato/shared/lib/browser/versionedPreference'\nimport { isOpenDealStatus, isWonDealStatus } from '../../lib/dealStatus'\nimport type { CompanyOverview, DealSummary, InteractionSummary } from '../formConfig'\nimport { formatCurrency } from './utils'\n\nconst STORAGE_KEY = 'om:company-detail-kpi-hidden'\nconst STORAGE_VERSION = 1\n\nfunction sumActiveDeals(deals: DealSummary[]): number {\n return deals\n .filter((d) => isOpenDealStatus(d.status))\n .reduce((sum, d) => {\n const amount = typeof d.valueAmount === 'number' ? d.valueAmount : parseFloat(String(d.valueAmount ?? '0'))\n return sum + (Number.isFinite(amount) ? amount : 0)\n }, 0)\n}\n\nfunction getActiveDeals(deals: DealSummary[]): DealSummary[] {\n return deals.filter((d) => isOpenDealStatus(d.status))\n}\n\nfunction computeActivityTrend(interactions: InteractionSummary[]): KpiTrend | undefined {\n const now = Date.now()\n const weekMs = 7 * 86_400_000\n const thisWeek = interactions.filter((i) => {\n const d = i.occurredAt ?? i.scheduledAt\n return d && now - new Date(d).getTime() < weekMs\n }).length\n const lastWeek = interactions.filter((i) => {\n const d = i.occurredAt ?? i.scheduledAt\n if (!d) return false\n const diff = now - new Date(d).getTime()\n return diff >= weekMs && diff < weekMs * 2\n }).length\n if (lastWeek === 0 && thisWeek === 0) return undefined\n if (lastWeek === 0) return { value: 100, direction: 'up' }\n const pct = ((thisWeek - lastWeek) / lastWeek) * 100\n if (Math.abs(pct) < 0.5) return { value: 0, direction: 'unchanged' }\n return { value: Math.abs(pct), direction: pct > 0 ? 'up' : 'down' }\n}\n\nfunction computeDealTrend(deals: DealSummary[]): KpiTrend | undefined {\n const active = deals.filter((d) => isOpenDealStatus(d.status))\n if (active.length === 0) return undefined\n const now = Date.now()\n const monthMs = 30 * 86_400_000\n const recentDeals = active.filter((d) => d.createdAt && now - new Date(d.createdAt).getTime() < monthMs).length\n if (recentDeals > 0) return { value: recentDeals * 10, direction: 'up' }\n return { value: 0, direction: 'unchanged' }\n}\n\ntype CompanyKpiBarProps = {\n data: CompanyOverview\n}\n\nexport function CompanyKpiBar({ data }: CompanyKpiBarProps) {\n const t = useT()\n const locale = useLocale()\n\n const activeDeals = React.useMemo(() => getActiveDeals(data.deals), [data.deals])\n const activeDealsValue = React.useMemo(\n () => data.kpis?.activeDealsValue ?? sumActiveDeals(data.deals),\n [data.deals, data.kpis?.activeDealsValue],\n )\n const dealCurrency = data.kpis?.dealCurrency ?? activeDeals[0]?.valueCurrency ?? data.deals[0]?.valueCurrency ?? 'PLN'\n const activityTrend = React.useMemo(\n () => data.kpis?.activityTrend ?? computeActivityTrend(data.interactions),\n [data.interactions, data.kpis?.activityTrend],\n )\n const dealTrend = React.useMemo(() => computeDealTrend(data.deals), [data.deals])\n\n const ltvValue = React.useMemo(() => {\n if (data.kpis?.ltvValue !== undefined) return data.kpis.ltvValue\n const wonDeals = data.deals.filter((d) => isWonDealStatus(d.status))\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, data.kpis?.ltvValue])\n\n const clientTenureYears = React.useMemo(() => {\n if (data.kpis?.clientTenureYears !== undefined) return data.kpis.clientTenureYears\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, data.kpis?.clientTenureYears])\n\n const [hiddenTiles, setHiddenTiles] = React.useState<Set<string>>(\n () => readVersionedIdSet(STORAGE_KEY, STORAGE_VERSION),\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 writeVersionedIdSet(STORAGE_KEY, STORAGE_VERSION, next)\n return next\n })\n }, [])\n\n const showAllTiles = React.useCallback(() => {\n setHiddenTiles(new Set())\n clearVersionedPreference(STORAGE_KEY)\n }, [])\n\n const kpiTiles = React.useMemo(() => [\n {\n id: 'activeDeals',\n title: t('customers.companies.dashboard.kpi.activeDeals', 'ACTIVE DEALS'),\n value: activeDealsValue,\n trend: dealTrend,\n formatValue: (v: number) => formatCurrency(v, dealCurrency, locale),\n comparisonLabel: `${data.kpis?.activeDealsCount ?? activeDeals.length} ${(data.kpis?.activeDealsCount ?? activeDeals.length) === 1 ? 'pipeline' : 'pipelines'}`,\n },\n {\n id: 'activities',\n title: t('customers.companies.dashboard.kpi.activities', 'ACTIVITIES'),\n value: data.kpis?.activityCount ?? data.interactions.length,\n trend: activityTrend,\n comparisonLabel: t('customers.companies.dashboard.kpi.last12months', 'last 12 months'),\n },\n {\n id: 'ltv',\n title: t('customers.companies.dashboard.kpi.ltv', 'CUSTOMER VALUE (LTV)'),\n value: ltvValue,\n formatValue: ltvValue !== null ? (v: number) => formatCurrency(v, dealCurrency, locale) : undefined,\n comparisonLabel: ltvValue !== null\n ? t('customers.companies.dashboard.kpi.wonDeals', 'won deals total')\n : t('customers.companies.dashboard.kpi.noWonDeals', 'No won deals'),\n },\n {\n id: 'clientSince',\n title: t('customers.companies.dashboard.kpi.clientSince', 'CLIENT SINCE'),\n value: clientTenureYears,\n formatValue: clientTenureYears !== null\n ? (v: number) => v < 1\n ? `< 1 ${t('customers.companies.dashboard.kpi.year', 'year')}`\n : `${v} ${v === 1 ? t('customers.companies.dashboard.kpi.year', 'year') : t('customers.companies.dashboard.kpi.years', 'years')}`\n : undefined,\n comparisonLabel: clientTenureYears !== null\n ? `${data.kpis?.completedDealsCount ?? data.deals.filter((d) => isWonDealStatus(d.status)).length} ${t('customers.companies.dashboard.kpi.completedDeals', 'completed deals')}`\n : t('customers.companies.dashboard.kpi.noInteractions', 'No interactions yet'),\n },\n ], [t, locale, activeDealsValue, dealTrend, dealCurrency, activeDeals.length, activityTrend, ltvValue, clientTenureYears, data.deals, data.interactions.length, data.kpis])\n\n const visibleTiles = kpiTiles.filter((tile) => !hiddenTiles.has(tile.id))\n\n return (\n <div className=\"space-y-2\">\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4\">\n {visibleTiles.map((tile) => (\n <div key={tile.id} className=\"group relative\">\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=\"xs\"\n onClick={() => toggleTile(tile.id)}\n className=\"absolute right-2 top-2 opacity-0 transition-opacity group-hover:opacity-60\"\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\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-auto px-1.5 py-0.5 text-xs hover:bg-transparent\"\n onClick={showAllTiles}\n >\n {t('customers.companies.dashboard.showAll', 'Show all')}\n </Button>\n </div>\n )}\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -12,7 +12,7 @@ import { LinkEntityDialog } from "../linking/LinkEntityDialog.js";
|
|
|
12
12
|
import { createDealLinkAdapter } from "../linking/adapters/dealAdapter.js";
|
|
13
13
|
import { LoadingMessage, TabEmptyState } from "@open-mercato/ui/backend/detail";
|
|
14
14
|
import { useOrganizationScopeVersion } from "@open-mercato/shared/lib/frontend/useOrganizationScope";
|
|
15
|
-
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
15
|
+
import { useT, useLocale } from "@open-mercato/shared/lib/i18n/context";
|
|
16
16
|
import { hasMoreFromPage } from "@open-mercato/shared/lib/pagination/load-more";
|
|
17
17
|
import { useConfirmDialog } from "@open-mercato/ui/backend/confirm-dialog";
|
|
18
18
|
import { E } from "../../../../generated/entities.ids.generated.js";
|
|
@@ -190,9 +190,9 @@ function normalizeDeal(deal) {
|
|
|
190
190
|
customFields
|
|
191
191
|
};
|
|
192
192
|
}
|
|
193
|
-
function formatValueLabel(amount, currency, emptyLabel) {
|
|
193
|
+
function formatValueLabel(amount, currency, emptyLabel, locale) {
|
|
194
194
|
if (typeof amount === "number") {
|
|
195
|
-
const formatter = new Intl.NumberFormat(
|
|
195
|
+
const formatter = new Intl.NumberFormat(locale, {
|
|
196
196
|
style: currency ? "currency" : "decimal",
|
|
197
197
|
currency: currency ?? void 0,
|
|
198
198
|
maximumFractionDigits: 2
|
|
@@ -218,6 +218,7 @@ function DealsSection({
|
|
|
218
218
|
runGuardedMutation
|
|
219
219
|
}) {
|
|
220
220
|
const tHook = useT();
|
|
221
|
+
const locale = useLocale();
|
|
221
222
|
const { confirm, ConfirmDialogElement } = useConfirmDialog();
|
|
222
223
|
const fallbackTranslator = React.useMemo(() => createTranslatorWithFallback(tHook), [tHook]);
|
|
223
224
|
const t = React.useMemo(() => translator ?? fallbackTranslator, [translator, fallbackTranslator]);
|
|
@@ -721,8 +722,8 @@ function DealsSection({
|
|
|
721
722
|
) : null,
|
|
722
723
|
/* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
723
724
|
sortedDeals.map((deal) => {
|
|
724
|
-
const valueLabel = formatValueLabel(deal.valueAmount, deal.valueCurrency ?? null, emptyLabel);
|
|
725
|
-
const expectedLabel = deal.expectedCloseAt ? formatDate(deal.expectedCloseAt) ?? emptyLabel : emptyLabel;
|
|
725
|
+
const valueLabel = formatValueLabel(deal.valueAmount, deal.valueCurrency ?? null, emptyLabel, locale);
|
|
726
|
+
const expectedLabel = deal.expectedCloseAt ? formatDate(deal.expectedCloseAt, locale) ?? emptyLabel : emptyLabel;
|
|
726
727
|
const probabilityLabel = typeof deal.probability === "number" ? `${deal.probability}%` : emptyLabel;
|
|
727
728
|
const isUnlinkPending = pendingAction?.kind === "remove" && pendingAction.id === deal.id;
|
|
728
729
|
const statusLabel = deal.status && statusDictionaryMap ? statusDictionaryMap[deal.status]?.label ?? deal.status : deal.status ?? emptyLabel;
|