@open-mercato/core 0.5.1-develop.2802.9223828f7f → 0.5.1-develop.2851.2854b4507f
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/.turbo/turbo-build.log +1 -1
- package/dist/generated/entities/action_log/index.js +4 -0
- package/dist/generated/entities/action_log/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/audit_logs/data/entities.js +10 -1
- package/dist/modules/audit_logs/data/entities.js.map +2 -2
- package/dist/modules/audit_logs/data/validators.js +2 -0
- package/dist/modules/audit_logs/data/validators.js.map +2 -2
- package/dist/modules/audit_logs/migrations/Migration20260423202109.js +15 -0
- package/dist/modules/audit_logs/migrations/Migration20260423202109.js.map +7 -0
- package/dist/modules/audit_logs/services/accessLogService.js +3 -2
- package/dist/modules/audit_logs/services/accessLogService.js.map +3 -3
- package/dist/modules/audit_logs/services/actionLogService.js +13 -2
- package/dist/modules/audit_logs/services/actionLogService.js.map +3 -3
- package/dist/modules/customers/api/entity-roles-factory.js +3 -18
- package/dist/modules/customers/api/entity-roles-factory.js.map +2 -2
- package/dist/modules/customers/api/interactions/cancel/route.js +7 -2
- package/dist/modules/customers/api/interactions/cancel/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/complete/route.js +7 -2
- package/dist/modules/customers/api/interactions/complete/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/page.js +45 -44
- package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
- package/dist/modules/customers/commands/comments.js +6 -0
- package/dist/modules/customers/commands/comments.js.map +2 -2
- package/dist/modules/customers/components/detail/AssignRoleDialog.js +41 -13
- package/dist/modules/customers/components/detail/AssignRoleDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyDetailHeader.js +30 -0
- package/dist/modules/customers/components/detail/CompanyDetailHeader.js.map +2 -2
- package/dist/modules/customers/components/detail/DealDetailHeader.js +32 -0
- package/dist/modules/customers/components/detail/DealDetailHeader.js.map +2 -2
- package/dist/modules/customers/components/detail/DealWonPopup.js +2 -2
- package/dist/modules/customers/components/detail/DealWonPopup.js.map +2 -2
- package/dist/modules/customers/components/detail/InlineActivityComposer.js +62 -6
- package/dist/modules/customers/components/detail/InlineActivityComposer.js.map +2 -2
- package/dist/modules/customers/components/detail/ObjectHistoryButton.js +39 -0
- package/dist/modules/customers/components/detail/ObjectHistoryButton.js.map +7 -0
- package/dist/modules/customers/components/detail/PersonDetailHeader.js +30 -0
- package/dist/modules/customers/components/detail/PersonDetailHeader.js.map +2 -2
- package/dist/modules/customers/components/detail/RolesSection.js +14 -4
- package/dist/modules/customers/components/detail/RolesSection.js.map +3 -3
- package/dist/modules/customers/components/formConfig.js +16 -2
- package/dist/modules/customers/components/formConfig.js.map +2 -2
- package/dist/modules/customers/lib/displayName.js +15 -0
- package/dist/modules/customers/lib/displayName.js.map +7 -0
- package/dist/modules/customers/lib/interactionReadModel.js +1 -2
- package/dist/modules/customers/lib/interactionReadModel.js.map +2 -2
- package/dist/modules/customers/lib/operationMetadata.js +21 -0
- package/dist/modules/customers/lib/operationMetadata.js.map +7 -0
- package/generated/entities/action_log/index.ts +2 -0
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +3 -3
- package/src/modules/audit_logs/data/entities.ts +7 -0
- package/src/modules/audit_logs/data/validators.ts +2 -0
- package/src/modules/audit_logs/migrations/.snapshot-open-mercato.json +51 -5
- package/src/modules/audit_logs/migrations/Migration20260423202109.ts +15 -0
- package/src/modules/audit_logs/services/accessLogService.ts +1 -3
- package/src/modules/audit_logs/services/actionLogService.ts +11 -6
- package/src/modules/customers/api/entity-roles-factory.ts +3 -23
- package/src/modules/customers/api/interactions/cancel/route.ts +7 -2
- package/src/modules/customers/api/interactions/complete/route.ts +7 -2
- package/src/modules/customers/backend/customers/deals/page.tsx +48 -44
- package/src/modules/customers/commands/comments.ts +6 -0
- package/src/modules/customers/components/detail/AssignRoleDialog.tsx +37 -9
- package/src/modules/customers/components/detail/CompanyDetailHeader.tsx +25 -0
- package/src/modules/customers/components/detail/DealDetailHeader.tsx +29 -0
- package/src/modules/customers/components/detail/DealWonPopup.tsx +2 -2
- package/src/modules/customers/components/detail/InlineActivityComposer.tsx +65 -6
- package/src/modules/customers/components/detail/ObjectHistoryButton.tsx +47 -0
- package/src/modules/customers/components/detail/PersonDetailHeader.tsx +25 -0
- package/src/modules/customers/components/detail/RolesSection.tsx +20 -1
- package/src/modules/customers/components/formConfig.tsx +14 -2
- package/src/modules/customers/i18n/de.json +12 -0
- package/src/modules/customers/i18n/en.json +12 -0
- package/src/modules/customers/i18n/es.json +13 -1
- package/src/modules/customers/i18n/pl.json +13 -1
- package/src/modules/customers/lib/displayName.ts +16 -0
- package/src/modules/customers/lib/interactionReadModel.ts +1 -7
- package/src/modules/customers/lib/operationMetadata.ts +38 -0
|
@@ -51,9 +51,9 @@ function DealWonPopup({
|
|
|
51
51
|
const t = useT();
|
|
52
52
|
return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange: (nextOpen) => {
|
|
53
53
|
if (!nextOpen) onClose();
|
|
54
|
-
}, children: /* @__PURE__ */ jsxs(DialogContent, { className: "overflow-hidden p-0 sm:max-w-[420px]", children: [
|
|
54
|
+
}, children: /* @__PURE__ */ jsxs(DialogContent, { className: "max-h-[90vh] overflow-hidden p-0 sm:max-w-[420px]", children: [
|
|
55
55
|
/* @__PURE__ */ jsx(VisuallyHidden, { children: /* @__PURE__ */ jsx(DialogTitle, { children: t("customers.deals.detail.won.title", "Closed successfully") }) }),
|
|
56
|
-
/* @__PURE__ */ jsxs("div", { className: "overflow-
|
|
56
|
+
/* @__PURE__ */ jsxs("div", { className: "max-h-[90vh] overflow-y-auto rounded-2xl bg-card", children: [
|
|
57
57
|
/* @__PURE__ */ jsx("div", { className: "px-6 pb-5 pt-6", children: /* @__PURE__ */ jsx("div", { className: "flex h-[200px] items-center justify-center rounded-2xl bg-[linear-gradient(135deg,rgba(141,150,244,0.5),rgba(198,203,254,0.95))] text-foreground", children: /* @__PURE__ */ jsx(Trophy, { className: "size-24", strokeWidth: 1.5 }) }) }),
|
|
58
58
|
/* @__PURE__ */ jsxs("div", { className: "space-y-5 px-7 pb-7 text-center", children: [
|
|
59
59
|
/* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customers/components/detail/DealWonPopup.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { VisuallyHidden } from '@radix-ui/react-visually-hidden'\nimport { ChartColumnIncreasing, Clock3, Medal, Trophy } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Dialog, DialogContent, DialogTitle } from '@open-mercato/ui/primitives/dialog'\n\ntype DealStatsPayload = {\n dealValue: number | null\n dealCurrency: string | null\n closureOutcome: 'won' | 'lost'\n closedAt: string\n pipelineName: string | null\n dealsClosedThisPeriod: number\n salesCycleDays: number | null\n dealRankInQuarter: number | null\n lossReason: string | null\n}\n\ntype DealWonPopupProps = {\n open: boolean\n onClose: () => void\n dealTitle: string\n stats: DealStatsPayload | null\n onViewDashboard?: () => void\n onBackToPipeline?: () => void\n}\n\nfunction formatCurrency(value: number | null, currency: string | null): string {\n if (value === null || !Number.isFinite(value)) return '\u2014'\n if (!currency) return value.toLocaleString()\n try {\n return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(value)\n } catch {\n return `${value.toLocaleString()} ${currency}`\n }\n}\n\nfunction formatClosedDate(value: string, t: ReturnType<typeof useT>): string {\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return t('customers.deals.detail.won.closed', 'Closed')\n return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })\n}\n\nfunction formatSalesCycle(value: number | null): string {\n if (value === null || !Number.isFinite(value)) return '\u2014'\n if (value >= 30) {\n const months = Math.max(1, Math.round(value / 30))\n return `${months} mo`\n }\n return `${value}d`\n}\n\nfunction StatCard({\n icon,\n label,\n value,\n}: {\n icon: React.ReactNode\n label: string\n value: string\n}) {\n return (\n <div className=\"rounded-2xl border bg-background px-4 py-4\">\n <div className=\"mb-2 flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground\">\n {icon}\n {label}\n </div>\n <div className=\"text-xl font-semibold text-foreground\">{value}</div>\n </div>\n )\n}\n\nexport function DealWonPopup({\n open,\n onClose,\n dealTitle,\n stats,\n onViewDashboard,\n onBackToPipeline,\n}: DealWonPopupProps) {\n const t = useT()\n\n return (\n <Dialog open={open} onOpenChange={(nextOpen) => { if (!nextOpen) onClose() }}>\n <DialogContent className=\"overflow-hidden p-0 sm:max-w-[420px]\">\n <VisuallyHidden>\n <DialogTitle>{t('customers.deals.detail.won.title', 'Closed successfully')}</DialogTitle>\n </VisuallyHidden>\n <div className=\"overflow-
|
|
5
|
-
"mappings": ";AAkEM,SAIA,KAJA;AA/DN,SAAS,sBAAsB;AAC/B,SAAS,uBAAuB,QAAQ,OAAO,cAAc;AAC7D,SAAS,YAAY;AACrB,SAAS,cAAc;AACvB,SAAS,QAAQ,eAAe,mBAAmB;AAuBnD,SAAS,eAAe,OAAsB,UAAiC;AAC7E,MAAI,UAAU,QAAQ,CAAC,OAAO,SAAS,KAAK,EAAG,QAAO;AACtD,MAAI,CAAC,SAAU,QAAO,MAAM,eAAe;AAC3C,MAAI;AACF,WAAO,IAAI,KAAK,aAAa,QAAW,EAAE,OAAO,YAAY,SAAS,CAAC,EAAE,OAAO,KAAK;AAAA,EACvF,QAAQ;AACN,WAAO,GAAG,MAAM,eAAe,CAAC,IAAI,QAAQ;AAAA,EAC9C;AACF;AAEA,SAAS,iBAAiB,OAAe,GAAoC;AAC3E,QAAM,OAAO,IAAI,KAAK,KAAK;AAC3B,MAAI,OAAO,MAAM,KAAK,QAAQ,CAAC,EAAG,QAAO,EAAE,qCAAqC,QAAQ;AACxF,SAAO,KAAK,mBAAmB,QAAW,EAAE,OAAO,SAAS,KAAK,WAAW,MAAM,UAAU,CAAC;AAC/F;AAEA,SAAS,iBAAiB,OAA8B;AACtD,MAAI,UAAU,QAAQ,CAAC,OAAO,SAAS,KAAK,EAAG,QAAO;AACtD,MAAI,SAAS,IAAI;AACf,UAAM,SAAS,KAAK,IAAI,GAAG,KAAK,MAAM,QAAQ,EAAE,CAAC;AACjD,WAAO,GAAG,MAAM;AAAA,EAClB;AACA,SAAO,GAAG,KAAK;AACjB;AAEA,SAAS,SAAS;AAAA,EAChB;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,SACE,qBAAC,SAAI,WAAU,8CACb;AAAA,yBAAC,SAAI,WAAU,wGACZ;AAAA;AAAA,MACA;AAAA,OACH;AAAA,IACA,oBAAC,SAAI,WAAU,yCAAyC,iBAAM;AAAA,KAChE;AAEJ;AAEO,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAsB;AACpB,QAAM,IAAI,KAAK;AAEf,SACE,oBAAC,UAAO,MAAY,cAAc,CAAC,aAAa;AAAE,QAAI,CAAC,SAAU,SAAQ;AAAA,EAAE,GACzE,+BAAC,iBAAc,WAAU,
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { VisuallyHidden } from '@radix-ui/react-visually-hidden'\nimport { ChartColumnIncreasing, Clock3, Medal, Trophy } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Dialog, DialogContent, DialogTitle } from '@open-mercato/ui/primitives/dialog'\n\ntype DealStatsPayload = {\n dealValue: number | null\n dealCurrency: string | null\n closureOutcome: 'won' | 'lost'\n closedAt: string\n pipelineName: string | null\n dealsClosedThisPeriod: number\n salesCycleDays: number | null\n dealRankInQuarter: number | null\n lossReason: string | null\n}\n\ntype DealWonPopupProps = {\n open: boolean\n onClose: () => void\n dealTitle: string\n stats: DealStatsPayload | null\n onViewDashboard?: () => void\n onBackToPipeline?: () => void\n}\n\nfunction formatCurrency(value: number | null, currency: string | null): string {\n if (value === null || !Number.isFinite(value)) return '\u2014'\n if (!currency) return value.toLocaleString()\n try {\n return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(value)\n } catch {\n return `${value.toLocaleString()} ${currency}`\n }\n}\n\nfunction formatClosedDate(value: string, t: ReturnType<typeof useT>): string {\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return t('customers.deals.detail.won.closed', 'Closed')\n return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })\n}\n\nfunction formatSalesCycle(value: number | null): string {\n if (value === null || !Number.isFinite(value)) return '\u2014'\n if (value >= 30) {\n const months = Math.max(1, Math.round(value / 30))\n return `${months} mo`\n }\n return `${value}d`\n}\n\nfunction StatCard({\n icon,\n label,\n value,\n}: {\n icon: React.ReactNode\n label: string\n value: string\n}) {\n return (\n <div className=\"rounded-2xl border bg-background px-4 py-4\">\n <div className=\"mb-2 flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground\">\n {icon}\n {label}\n </div>\n <div className=\"text-xl font-semibold text-foreground\">{value}</div>\n </div>\n )\n}\n\nexport function DealWonPopup({\n open,\n onClose,\n dealTitle,\n stats,\n onViewDashboard,\n onBackToPipeline,\n}: DealWonPopupProps) {\n const t = useT()\n\n return (\n <Dialog open={open} onOpenChange={(nextOpen) => { if (!nextOpen) onClose() }}>\n <DialogContent className=\"max-h-[90vh] overflow-hidden p-0 sm:max-w-[420px]\">\n <VisuallyHidden>\n <DialogTitle>{t('customers.deals.detail.won.title', 'Closed successfully')}</DialogTitle>\n </VisuallyHidden>\n <div className=\"max-h-[90vh] overflow-y-auto rounded-2xl bg-card\">\n <div className=\"px-6 pb-5 pt-6\">\n {/* TODO(ds-review): decorative gradient \u2014 consider defining a named gradient token if reused */}\n <div className=\"flex h-[200px] items-center justify-center rounded-2xl bg-[linear-gradient(135deg,rgba(141,150,244,0.5),rgba(198,203,254,0.95))] text-foreground\">\n <Trophy className=\"size-24\" strokeWidth={1.5} />\n </div>\n </div>\n\n <div className=\"space-y-5 px-7 pb-7 text-center\">\n <div className=\"space-y-2\">\n <h2 className=\"text-2xl font-bold leading-tight text-foreground\">\n {t('customers.deals.detail.won.title', 'Closed successfully')}\n </h2>\n <p className=\"text-sm font-medium text-muted-foreground\">\n {t('customers.deals.detail.won.subtitle', 'You are a sales machine!')}\n </p>\n </div>\n\n <div className=\"rounded-xl border bg-muted/20 px-4 py-4\">\n <p className=\"text-overline font-bold uppercase tracking-[0.16em] text-muted-foreground\">\n {dealTitle}\n </p>\n <p className=\"mt-2 text-2xl font-bold text-primary\">\n {stats ? formatCurrency(stats.dealValue, stats.dealCurrency) : '\u2014'}\n </p>\n <p className=\"mt-1 text-xs text-muted-foreground\">\n {t('customers.deals.detail.won.closed', 'Won')} \u00B7 {stats ? formatClosedDate(stats.closedAt, t) : '\u2014'}\n </p>\n </div>\n\n <div className=\"grid grid-cols-3 gap-2\">\n <StatCard\n icon={<ChartColumnIncreasing className=\"size-4\" />}\n label={t('customers.deals.detail.won.dealsThisWeek', 'Deals this week')}\n value={stats ? String(stats.dealsClosedThisPeriod) : '\u2014'}\n />\n <StatCard\n icon={<Clock3 className=\"size-4\" />}\n label={t('customers.deals.detail.won.salesCycle', 'Sales cycle')}\n value={formatSalesCycle(stats?.salesCycleDays ?? null)}\n />\n <StatCard\n icon={<Medal className=\"size-4\" />}\n label={t('customers.deals.detail.won.rank', 'Quarter rank')}\n value={stats?.dealRankInQuarter !== null && stats?.dealRankInQuarter !== undefined ? `#${stats.dealRankInQuarter}` : '\u2014'}\n />\n </div>\n\n <div className=\"rounded-2xl border bg-muted/30 px-4 py-4\">\n <div className=\"text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground\">\n {t('customers.deals.detail.won.pipeline', 'Pipeline')}\n </div>\n <div className=\"mt-2 text-sm text-foreground\">\n {stats?.pipelineName ?? t('customers.deals.detail.won.pipelineFallback', 'Current pipeline')}\n </div>\n </div>\n\n <div className=\"space-y-2\">\n {onViewDashboard ? (\n <Button type=\"button\" className=\"w-full\" onClick={onViewDashboard}>\n {t('customers.deals.detail.won.primaryAction', 'View sales report')}\n </Button>\n ) : null}\n <Button type=\"button\" variant={onViewDashboard ? 'outline' : 'default'} className=\"w-full\" onClick={onBackToPipeline ?? onClose}>\n {t('customers.deals.detail.won.secondaryAction', 'Back to pipeline')}\n </Button>\n </div>\n </div>\n </div>\n </DialogContent>\n </Dialog>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import { SquarePen, Calendar, Check } from "lucide-react";
|
|
4
|
+
import { SquarePen, Calendar, Check, ChevronDown, ChevronUp } from "lucide-react";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
7
7
|
import { apiCallOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
|
|
8
8
|
import { flash } from "@open-mercato/ui/backend/FlashMessages";
|
|
9
9
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
10
|
+
import { usePersistedBooleanFlag } from "@open-mercato/ui/backend/crud/usePersistedBooleanFlag";
|
|
10
11
|
import { ActivityTypeSelector } from "./ActivityTypeSelector.js";
|
|
11
12
|
import { MiniWeekCalendar } from "./MiniWeekCalendar.js";
|
|
12
13
|
const composerSchema = z.object({
|
|
@@ -40,6 +41,21 @@ function InlineActivityComposer({
|
|
|
40
41
|
const [saving, setSaving] = React.useState(false);
|
|
41
42
|
const [errors, setErrors] = React.useState({});
|
|
42
43
|
const descriptionRef = React.useRef(null);
|
|
44
|
+
const weekPreviewStorageKey = `om:inline-composer:week-preview:${entityType}`;
|
|
45
|
+
const { value: weekPreviewHidden, toggle: toggleWeekPreview } = usePersistedBooleanFlag(
|
|
46
|
+
weekPreviewStorageKey,
|
|
47
|
+
false
|
|
48
|
+
);
|
|
49
|
+
const resizeDescription = React.useCallback(() => {
|
|
50
|
+
const el = descriptionRef.current;
|
|
51
|
+
if (!el) return;
|
|
52
|
+
el.style.height = "auto";
|
|
53
|
+
const maxHeight = 200;
|
|
54
|
+
el.style.height = `${Math.min(el.scrollHeight, maxHeight)}px`;
|
|
55
|
+
}, []);
|
|
56
|
+
React.useEffect(() => {
|
|
57
|
+
resizeDescription();
|
|
58
|
+
}, [description, resizeDescription]);
|
|
43
59
|
const handleTypeSelect = React.useCallback((type) => {
|
|
44
60
|
setSelectedType((previous) => previous === type ? null : type);
|
|
45
61
|
setErrors({});
|
|
@@ -149,20 +165,29 @@ function InlineActivityComposer({
|
|
|
149
165
|
/* @__PURE__ */ jsx(ActivityTypeSelector, { selectedType, onSelect: handleTypeSelect }),
|
|
150
166
|
/* @__PURE__ */ jsxs("div", { className: "mt-4 flex items-start gap-3", children: [
|
|
151
167
|
/* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
|
|
168
|
+
/* @__PURE__ */ jsx(
|
|
169
|
+
"label",
|
|
170
|
+
{
|
|
171
|
+
htmlFor: "inline-activity-composer-description",
|
|
172
|
+
className: "mb-1 block text-xs font-medium text-muted-foreground",
|
|
173
|
+
children: t("customers.activityComposer.descriptionLabel", "Description")
|
|
174
|
+
}
|
|
175
|
+
),
|
|
152
176
|
/* @__PURE__ */ jsx(
|
|
153
177
|
"textarea",
|
|
154
178
|
{
|
|
155
179
|
ref: descriptionRef,
|
|
180
|
+
id: "inline-activity-composer-description",
|
|
156
181
|
value: description,
|
|
157
182
|
onChange: (event) => setDescription(event.target.value),
|
|
158
183
|
placeholder: t("customers.activityComposer.descriptionPlaceholder", "What happened?"),
|
|
159
|
-
className: "min-h-[
|
|
160
|
-
rows:
|
|
184
|
+
className: "min-h-[72px] w-full resize-none rounded-lg border bg-background px-3 py-2.5 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring",
|
|
185
|
+
rows: 3
|
|
161
186
|
}
|
|
162
187
|
),
|
|
163
188
|
errors.description ? /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-destructive", children: errors.description }) : null
|
|
164
189
|
] }),
|
|
165
|
-
/* @__PURE__ */ jsxs("label", { className: "flex shrink-0 cursor-pointer items-center gap-2 rounded-lg border bg-background px-3 py-2.5 text-sm text-muted-foreground", children: [
|
|
190
|
+
/* @__PURE__ */ jsxs("label", { className: "mt-[22px] flex shrink-0 cursor-pointer items-center gap-2 rounded-lg border bg-background px-3 py-2.5 text-sm text-muted-foreground", children: [
|
|
166
191
|
/* @__PURE__ */ jsx(Calendar, { className: "size-4" }),
|
|
167
192
|
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-foreground", children: formatDateBadge(occurredAt, t) }),
|
|
168
193
|
/* @__PURE__ */ jsx(
|
|
@@ -176,7 +201,38 @@ function InlineActivityComposer({
|
|
|
176
201
|
)
|
|
177
202
|
] })
|
|
178
203
|
] }),
|
|
179
|
-
/* @__PURE__ */
|
|
204
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-4", children: [
|
|
205
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center justify-between", children: [
|
|
206
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: t("customers.activityComposer.weekPreviewTitle", "This week") }),
|
|
207
|
+
/* @__PURE__ */ jsx(
|
|
208
|
+
Button,
|
|
209
|
+
{
|
|
210
|
+
type: "button",
|
|
211
|
+
variant: "ghost",
|
|
212
|
+
size: "sm",
|
|
213
|
+
onClick: toggleWeekPreview,
|
|
214
|
+
"aria-expanded": !weekPreviewHidden,
|
|
215
|
+
"aria-controls": "inline-activity-composer-week-preview",
|
|
216
|
+
className: "h-auto px-2 py-1 text-xs text-muted-foreground hover:text-foreground",
|
|
217
|
+
children: weekPreviewHidden ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
218
|
+
/* @__PURE__ */ jsx(ChevronDown, { className: "mr-1 size-3" }),
|
|
219
|
+
t("customers.activityComposer.showWeekPreview", "Show week preview")
|
|
220
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
221
|
+
/* @__PURE__ */ jsx(ChevronUp, { className: "mr-1 size-3" }),
|
|
222
|
+
t("customers.activityComposer.hideWeekPreview", "Hide week preview")
|
|
223
|
+
] })
|
|
224
|
+
}
|
|
225
|
+
)
|
|
226
|
+
] }),
|
|
227
|
+
!weekPreviewHidden ? /* @__PURE__ */ jsx("div", { id: "inline-activity-composer-week-preview", children: /* @__PURE__ */ jsx(
|
|
228
|
+
MiniWeekCalendar,
|
|
229
|
+
{
|
|
230
|
+
entityId,
|
|
231
|
+
useCanonicalInteractions,
|
|
232
|
+
refreshRef: calendarRefreshRef
|
|
233
|
+
}
|
|
234
|
+
) }) : null
|
|
235
|
+
] }),
|
|
180
236
|
scheduledAt || false ? /* @__PURE__ */ jsxs("div", { className: "mt-3 flex items-center gap-2", children: [
|
|
181
237
|
/* @__PURE__ */ jsx("label", { className: "text-xs text-muted-foreground", children: t("customers.activityComposer.scheduledLabel", "Scheduled for") }),
|
|
182
238
|
/* @__PURE__ */ jsx(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customers/components/detail/InlineActivityComposer.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { SquarePen, Calendar, Check } from 'lucide-react'\nimport { z } from 'zod'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { ActivityTypeSelector, type ActivityType } from './ActivityTypeSelector'\nimport { MiniWeekCalendar } from './MiniWeekCalendar'\n\ntype GuardedMutationRunner = <T,>(\n operation: () => Promise<T>,\n mutationPayload?: Record<string, unknown>,\n) => Promise<T>\n\nconst composerSchema = z.object({\n description: z.string().trim().min(1, 'customers.activityComposer.validation.descriptionRequired'),\n occurredAt: z.string().min(1),\n})\n\ntype TranslateFn = (key: string, fallback?: string, params?: Record<string, string>) => string\n\nfunction formatDateBadge(isoLocal: string, t: TranslateFn): string {\n if (!isoLocal) return ''\n const now = new Date()\n const date = new Date(isoLocal.replace('T', ' '))\n const time = isoLocal.slice(11, 16)\n const isToday =\n date.getFullYear() === now.getFullYear() &&\n date.getMonth() === now.getMonth() &&\n date.getDate() === now.getDate()\n const dayLabel = isToday\n ? t('customers.activityComposer.today', 'Today')\n : date.toLocaleDateString(undefined, { day: 'numeric', month: 'short' })\n return `${dayLabel} \u00B7 ${time}`\n}\n\ninterface InlineActivityComposerProps {\n entityType: 'company' | 'person' | 'deal'\n entityId: string\n dealId?: string | null\n onActivityCreated?: () => void\n runGuardedMutation?: GuardedMutationRunner\n onScheduleRequested?: () => void\n useCanonicalInteractions?: boolean\n}\n\nexport function InlineActivityComposer({\n entityType,\n entityId,\n dealId = null,\n onActivityCreated,\n runGuardedMutation,\n onScheduleRequested,\n useCanonicalInteractions,\n}: InlineActivityComposerProps) {\n const t = useT()\n const calendarRefreshRef = React.useRef<(() => void) | null>(null)\n const [selectedType, setSelectedType] = React.useState<ActivityType | null>('call')\n const [description, setDescription] = React.useState('')\n const [occurredAt, setOccurredAt] = React.useState(() => new Date().toISOString().slice(0, 16))\n const [scheduledAt, setScheduledAt] = React.useState('')\n const [saving, setSaving] = React.useState(false)\n const [errors, setErrors] = React.useState<Record<string, string>>({})\n const descriptionRef = React.useRef<HTMLTextAreaElement>(null)\n\n const handleTypeSelect = React.useCallback((type: ActivityType) => {\n setSelectedType((previous) => (previous === type ? null : type))\n setErrors({})\n }, [])\n\n const handleReset = React.useCallback(() => {\n setDescription('')\n setOccurredAt(new Date().toISOString().slice(0, 16))\n setScheduledAt('')\n setErrors({})\n }, [])\n\n const handleSave = React.useCallback(async () => {\n if (!selectedType) {\n setErrors({ type: t('customers.activityComposer.validation.typeRequired', 'Select an activity type') })\n return\n }\n\n const result = composerSchema.safeParse({ description, occurredAt })\n if (!result.success) {\n const fieldErrors: Record<string, string> = {}\n for (const issue of result.error.issues) {\n const field = issue.path[0]\n if (field) fieldErrors[String(field)] = t(issue.message)\n }\n setErrors(fieldErrors)\n return\n }\n\n setSaving(true)\n setErrors({})\n\n try {\n const mutationPayload = {\n entityId,\n dealId,\n interactionType: selectedType,\n body: description.trim(),\n status: scheduledAt ? 'planned' : 'done',\n occurredAt: scheduledAt ? null : new Date(occurredAt).toISOString(),\n scheduledAt: scheduledAt ? new Date(scheduledAt).toISOString() : null,\n }\n const operation = () =>\n apiCallOrThrow('/api/customers/interactions', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(mutationPayload),\n })\n\n if (runGuardedMutation) {\n await runGuardedMutation(operation, mutationPayload)\n } else {\n await operation()\n }\n\n flash(\n t('customers.activityComposer.saved', {\n type: t(`customers.activityComposer.types.${selectedType}`),\n }),\n 'success',\n )\n handleReset()\n calendarRefreshRef.current?.()\n onActivityCreated?.()\n } catch (error) {\n console.error('customers.inlineActivityComposer.save failed', error)\n flash(t('customers.activityComposer.error', 'Failed to save activity'), 'error')\n } finally {\n setSaving(false)\n }\n }, [\n description,\n dealId,\n entityId,\n handleReset,\n occurredAt,\n onActivityCreated,\n runGuardedMutation,\n scheduledAt,\n selectedType,\n t,\n ])\n\n const handleKeyDown = React.useCallback((event: React.KeyboardEvent) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n handleSave()\n }\n }, [handleSave])\n\n return (\n <div className=\"rounded-2xl border border-border/70 bg-card p-5\" onKeyDown={handleKeyDown}>\n {/* Header: title + save button */}\n <div className=\"mb-4 flex items-center justify-between\">\n <h3 className=\"flex items-center gap-2 text-base font-semibold text-foreground\">\n <SquarePen className=\"size-4\" />\n {t('customers.activityComposer.title', 'Log activity')}\n </h3>\n <div className=\"flex items-center gap-2\">\n {onScheduleRequested ? (\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={onScheduleRequested} disabled={saving}>\n <Calendar className=\"size-4\" />\n {t('customers.activityComposer.schedule', 'Schedule')}\n </Button>\n ) : null}\n <Button\n type=\"button\"\n size=\"sm\"\n onClick={handleSave}\n disabled={saving || !selectedType}\n >\n <Check className=\"size-4\" />\n {saving\n ? t('customers.activityComposer.saving', 'Saving...')\n : t('customers.activityComposer.saveActivity', 'Save activity')}\n </Button>\n </div>\n </div>\n\n {/* Activity type selector \u2014 4 equal-width buttons */}\n <ActivityTypeSelector selectedType={selectedType} onSelect={handleTypeSelect} />\n\n {/* Description + date row */}\n <div className=\"mt-4 flex items-start gap-3\">\n <div className=\"flex-1\">\n <textarea\n ref={descriptionRef}\n value={description}\n onChange={(event) => setDescription(event.target.value)}\n placeholder={t('customers.activityComposer.descriptionPlaceholder', 'What happened?')}\n className=\"min-h-[44px] w-full resize-none rounded-lg border bg-background px-3 py-2.5 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring\"\n rows={1}\n />\n {errors.description ? (\n <p className=\"mt-1 text-xs text-destructive\">{errors.description}</p>\n ) : null}\n </div>\n <label className=\"flex shrink-0 cursor-pointer items-center gap-2 rounded-lg border bg-background px-3 py-2.5 text-sm text-muted-foreground\">\n <Calendar className=\"size-4\" />\n <span className=\"text-sm font-medium text-foreground\">{formatDateBadge(occurredAt, t)}</span>\n <input\n type=\"datetime-local\"\n value={occurredAt}\n onChange={(event) => setOccurredAt(event.target.value)}\n className=\"sr-only\"\n />\n </label>\n </div>\n\n {/* Mini calendar preview */}\n <div className=\"mt-4\">\n <MiniWeekCalendar entityId={entityId} useCanonicalInteractions={useCanonicalInteractions} refreshRef={calendarRefreshRef} />\n </div>\n\n {/* Scheduled for (optional) */}\n {scheduledAt || false ? (\n <div className=\"mt-3 flex items-center gap-2\">\n <label className=\"text-xs text-muted-foreground\">\n {t('customers.activityComposer.scheduledLabel', 'Scheduled for')}\n </label>\n <input\n type=\"datetime-local\"\n value={scheduledAt}\n onChange={(event) => setScheduledAt(event.target.value)}\n className=\"rounded-md border bg-background px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-ring\"\n />\n </div>\n ) : null}\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { SquarePen, Calendar, Check, ChevronDown, ChevronUp } from 'lucide-react'\nimport { z } from 'zod'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { usePersistedBooleanFlag } from '@open-mercato/ui/backend/crud/usePersistedBooleanFlag'\nimport { ActivityTypeSelector, type ActivityType } from './ActivityTypeSelector'\nimport { MiniWeekCalendar } from './MiniWeekCalendar'\n\ntype GuardedMutationRunner = <T,>(\n operation: () => Promise<T>,\n mutationPayload?: Record<string, unknown>,\n) => Promise<T>\n\nconst composerSchema = z.object({\n description: z.string().trim().min(1, 'customers.activityComposer.validation.descriptionRequired'),\n occurredAt: z.string().min(1),\n})\n\ntype TranslateFn = (key: string, fallback?: string, params?: Record<string, string>) => string\n\nfunction formatDateBadge(isoLocal: string, t: TranslateFn): string {\n if (!isoLocal) return ''\n const now = new Date()\n const date = new Date(isoLocal.replace('T', ' '))\n const time = isoLocal.slice(11, 16)\n const isToday =\n date.getFullYear() === now.getFullYear() &&\n date.getMonth() === now.getMonth() &&\n date.getDate() === now.getDate()\n const dayLabel = isToday\n ? t('customers.activityComposer.today', 'Today')\n : date.toLocaleDateString(undefined, { day: 'numeric', month: 'short' })\n return `${dayLabel} \u00B7 ${time}`\n}\n\ninterface InlineActivityComposerProps {\n entityType: 'company' | 'person' | 'deal'\n entityId: string\n dealId?: string | null\n onActivityCreated?: () => void\n runGuardedMutation?: GuardedMutationRunner\n onScheduleRequested?: () => void\n useCanonicalInteractions?: boolean\n}\n\nexport function InlineActivityComposer({\n entityType,\n entityId,\n dealId = null,\n onActivityCreated,\n runGuardedMutation,\n onScheduleRequested,\n useCanonicalInteractions,\n}: InlineActivityComposerProps) {\n const t = useT()\n const calendarRefreshRef = React.useRef<(() => void) | null>(null)\n const [selectedType, setSelectedType] = React.useState<ActivityType | null>('call')\n const [description, setDescription] = React.useState('')\n const [occurredAt, setOccurredAt] = React.useState(() => new Date().toISOString().slice(0, 16))\n const [scheduledAt, setScheduledAt] = React.useState('')\n const [saving, setSaving] = React.useState(false)\n const [errors, setErrors] = React.useState<Record<string, string>>({})\n const descriptionRef = React.useRef<HTMLTextAreaElement>(null)\n\n const weekPreviewStorageKey = `om:inline-composer:week-preview:${entityType}`\n const { value: weekPreviewHidden, toggle: toggleWeekPreview } = usePersistedBooleanFlag(\n weekPreviewStorageKey,\n false,\n )\n\n const resizeDescription = React.useCallback(() => {\n const el = descriptionRef.current\n if (!el) return\n el.style.height = 'auto'\n const maxHeight = 200\n el.style.height = `${Math.min(el.scrollHeight, maxHeight)}px`\n }, [])\n\n React.useEffect(() => {\n resizeDescription()\n }, [description, resizeDescription])\n\n const handleTypeSelect = React.useCallback((type: ActivityType) => {\n setSelectedType((previous) => (previous === type ? null : type))\n setErrors({})\n }, [])\n\n const handleReset = React.useCallback(() => {\n setDescription('')\n setOccurredAt(new Date().toISOString().slice(0, 16))\n setScheduledAt('')\n setErrors({})\n }, [])\n\n const handleSave = React.useCallback(async () => {\n if (!selectedType) {\n setErrors({ type: t('customers.activityComposer.validation.typeRequired', 'Select an activity type') })\n return\n }\n\n const result = composerSchema.safeParse({ description, occurredAt })\n if (!result.success) {\n const fieldErrors: Record<string, string> = {}\n for (const issue of result.error.issues) {\n const field = issue.path[0]\n if (field) fieldErrors[String(field)] = t(issue.message)\n }\n setErrors(fieldErrors)\n return\n }\n\n setSaving(true)\n setErrors({})\n\n try {\n const mutationPayload = {\n entityId,\n dealId,\n interactionType: selectedType,\n body: description.trim(),\n status: scheduledAt ? 'planned' : 'done',\n occurredAt: scheduledAt ? null : new Date(occurredAt).toISOString(),\n scheduledAt: scheduledAt ? new Date(scheduledAt).toISOString() : null,\n }\n const operation = () =>\n apiCallOrThrow('/api/customers/interactions', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(mutationPayload),\n })\n\n if (runGuardedMutation) {\n await runGuardedMutation(operation, mutationPayload)\n } else {\n await operation()\n }\n\n flash(\n t('customers.activityComposer.saved', {\n type: t(`customers.activityComposer.types.${selectedType}`),\n }),\n 'success',\n )\n handleReset()\n calendarRefreshRef.current?.()\n onActivityCreated?.()\n } catch (error) {\n console.error('customers.inlineActivityComposer.save failed', error)\n flash(t('customers.activityComposer.error', 'Failed to save activity'), 'error')\n } finally {\n setSaving(false)\n }\n }, [\n description,\n dealId,\n entityId,\n handleReset,\n occurredAt,\n onActivityCreated,\n runGuardedMutation,\n scheduledAt,\n selectedType,\n t,\n ])\n\n const handleKeyDown = React.useCallback((event: React.KeyboardEvent) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n handleSave()\n }\n }, [handleSave])\n\n return (\n <div className=\"rounded-2xl border border-border/70 bg-card p-5\" onKeyDown={handleKeyDown}>\n {/* Header: title + save button */}\n <div className=\"mb-4 flex items-center justify-between\">\n <h3 className=\"flex items-center gap-2 text-base font-semibold text-foreground\">\n <SquarePen className=\"size-4\" />\n {t('customers.activityComposer.title', 'Log activity')}\n </h3>\n <div className=\"flex items-center gap-2\">\n {onScheduleRequested ? (\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={onScheduleRequested} disabled={saving}>\n <Calendar className=\"size-4\" />\n {t('customers.activityComposer.schedule', 'Schedule')}\n </Button>\n ) : null}\n <Button\n type=\"button\"\n size=\"sm\"\n onClick={handleSave}\n disabled={saving || !selectedType}\n >\n <Check className=\"size-4\" />\n {saving\n ? t('customers.activityComposer.saving', 'Saving...')\n : t('customers.activityComposer.saveActivity', 'Save activity')}\n </Button>\n </div>\n </div>\n\n {/* Activity type selector \u2014 4 equal-width buttons */}\n <ActivityTypeSelector selectedType={selectedType} onSelect={handleTypeSelect} />\n\n {/* Description + date row */}\n <div className=\"mt-4 flex items-start gap-3\">\n <div className=\"flex-1\">\n <label\n htmlFor=\"inline-activity-composer-description\"\n className=\"mb-1 block text-xs font-medium text-muted-foreground\"\n >\n {t('customers.activityComposer.descriptionLabel', 'Description')}\n </label>\n <textarea\n ref={descriptionRef}\n id=\"inline-activity-composer-description\"\n value={description}\n onChange={(event) => setDescription(event.target.value)}\n placeholder={t('customers.activityComposer.descriptionPlaceholder', 'What happened?')}\n className=\"min-h-[72px] w-full resize-none rounded-lg border bg-background px-3 py-2.5 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring\"\n rows={3}\n />\n {errors.description ? (\n <p className=\"mt-1 text-xs text-destructive\">{errors.description}</p>\n ) : null}\n </div>\n <label className=\"mt-[22px] flex shrink-0 cursor-pointer items-center gap-2 rounded-lg border bg-background px-3 py-2.5 text-sm text-muted-foreground\">\n <Calendar className=\"size-4\" />\n <span className=\"text-sm font-medium text-foreground\">{formatDateBadge(occurredAt, t)}</span>\n <input\n type=\"datetime-local\"\n value={occurredAt}\n onChange={(event) => setOccurredAt(event.target.value)}\n className=\"sr-only\"\n />\n </label>\n </div>\n\n <div className=\"mt-4\">\n <div className=\"mb-2 flex items-center justify-between\">\n <span className=\"text-xs font-medium uppercase tracking-wide text-muted-foreground\">\n {t('customers.activityComposer.weekPreviewTitle', 'This week')}\n </span>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={toggleWeekPreview}\n aria-expanded={!weekPreviewHidden}\n aria-controls=\"inline-activity-composer-week-preview\"\n className=\"h-auto px-2 py-1 text-xs text-muted-foreground hover:text-foreground\"\n >\n {weekPreviewHidden ? (\n <>\n <ChevronDown className=\"mr-1 size-3\" />\n {t('customers.activityComposer.showWeekPreview', 'Show week preview')}\n </>\n ) : (\n <>\n <ChevronUp className=\"mr-1 size-3\" />\n {t('customers.activityComposer.hideWeekPreview', 'Hide week preview')}\n </>\n )}\n </Button>\n </div>\n {!weekPreviewHidden ? (\n <div id=\"inline-activity-composer-week-preview\">\n <MiniWeekCalendar\n entityId={entityId}\n useCanonicalInteractions={useCanonicalInteractions}\n refreshRef={calendarRefreshRef}\n />\n </div>\n ) : null}\n </div>\n\n {/* Scheduled for (optional) */}\n {scheduledAt || false ? (\n <div className=\"mt-3 flex items-center gap-2\">\n <label className=\"text-xs text-muted-foreground\">\n {t('customers.activityComposer.scheduledLabel', 'Scheduled for')}\n </label>\n <input\n type=\"datetime-local\"\n value={scheduledAt}\n onChange={(event) => setScheduledAt(event.target.value)}\n className=\"rounded-md border bg-background px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-ring\"\n />\n </div>\n ) : null}\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": ";AAqLQ,SA6EM,UA5EJ,KADF;AAnLR,YAAY,WAAW;AACvB,SAAS,WAAW,UAAU,OAAO,aAAa,iBAAiB;AACnE,SAAS,SAAS;AAClB,SAAS,YAAY;AACrB,SAAS,sBAAsB;AAC/B,SAAS,aAAa;AACtB,SAAS,cAAc;AACvB,SAAS,+BAA+B;AACxC,SAAS,4BAA+C;AACxD,SAAS,wBAAwB;AAOjC,MAAM,iBAAiB,EAAE,OAAO;AAAA,EAC9B,aAAa,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,2DAA2D;AAAA,EACjG,YAAY,EAAE,OAAO,EAAE,IAAI,CAAC;AAC9B,CAAC;AAID,SAAS,gBAAgB,UAAkB,GAAwB;AACjE,MAAI,CAAC,SAAU,QAAO;AACtB,QAAM,MAAM,oBAAI,KAAK;AACrB,QAAM,OAAO,IAAI,KAAK,SAAS,QAAQ,KAAK,GAAG,CAAC;AAChD,QAAM,OAAO,SAAS,MAAM,IAAI,EAAE;AAClC,QAAM,UACJ,KAAK,YAAY,MAAM,IAAI,YAAY,KACvC,KAAK,SAAS,MAAM,IAAI,SAAS,KACjC,KAAK,QAAQ,MAAM,IAAI,QAAQ;AACjC,QAAM,WAAW,UACb,EAAE,oCAAoC,OAAO,IAC7C,KAAK,mBAAmB,QAAW,EAAE,KAAK,WAAW,OAAO,QAAQ,CAAC;AACzE,SAAO,GAAG,QAAQ,SAAM,IAAI;AAC9B;AAYO,SAAS,uBAAuB;AAAA,EACrC;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAgC;AAC9B,QAAM,IAAI,KAAK;AACf,QAAM,qBAAqB,MAAM,OAA4B,IAAI;AACjE,QAAM,CAAC,cAAc,eAAe,IAAI,MAAM,SAA8B,MAAM;AAClF,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAAS,EAAE;AACvD,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAAS,OAAM,oBAAI,KAAK,GAAE,YAAY,EAAE,MAAM,GAAG,EAAE,CAAC;AAC9F,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAAS,EAAE;AACvD,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAAS,KAAK;AAChD,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAAiC,CAAC,CAAC;AACrE,QAAM,iBAAiB,MAAM,OAA4B,IAAI;AAE7D,QAAM,wBAAwB,mCAAmC,UAAU;AAC3E,QAAM,EAAE,OAAO,mBAAmB,QAAQ,kBAAkB,IAAI;AAAA,IAC9D;AAAA,IACA;AAAA,EACF;AAEA,QAAM,oBAAoB,MAAM,YAAY,MAAM;AAChD,UAAM,KAAK,eAAe;AAC1B,QAAI,CAAC,GAAI;AACT,OAAG,MAAM,SAAS;AAClB,UAAM,YAAY;AAClB,OAAG,MAAM,SAAS,GAAG,KAAK,IAAI,GAAG,cAAc,SAAS,CAAC;AAAA,EAC3D,GAAG,CAAC,CAAC;AAEL,QAAM,UAAU,MAAM;AACpB,sBAAkB;AAAA,EACpB,GAAG,CAAC,aAAa,iBAAiB,CAAC;AAEnC,QAAM,mBAAmB,MAAM,YAAY,CAAC,SAAuB;AACjE,oBAAgB,CAAC,aAAc,aAAa,OAAO,OAAO,IAAK;AAC/D,cAAU,CAAC,CAAC;AAAA,EACd,GAAG,CAAC,CAAC;AAEL,QAAM,cAAc,MAAM,YAAY,MAAM;AAC1C,mBAAe,EAAE;AACjB,mBAAc,oBAAI,KAAK,GAAE,YAAY,EAAE,MAAM,GAAG,EAAE,CAAC;AACnD,mBAAe,EAAE;AACjB,cAAU,CAAC,CAAC;AAAA,EACd,GAAG,CAAC,CAAC;AAEL,QAAM,aAAa,MAAM,YAAY,YAAY;AAC/C,QAAI,CAAC,cAAc;AACjB,gBAAU,EAAE,MAAM,EAAE,sDAAsD,yBAAyB,EAAE,CAAC;AACtG;AAAA,IACF;AAEA,UAAM,SAAS,eAAe,UAAU,EAAE,aAAa,WAAW,CAAC;AACnE,QAAI,CAAC,OAAO,SAAS;AACnB,YAAM,cAAsC,CAAC;AAC7C,iBAAW,SAAS,OAAO,MAAM,QAAQ;AACvC,cAAM,QAAQ,MAAM,KAAK,CAAC;AAC1B,YAAI,MAAO,aAAY,OAAO,KAAK,CAAC,IAAI,EAAE,MAAM,OAAO;AAAA,MACzD;AACA,gBAAU,WAAW;AACrB;AAAA,IACF;AAEA,cAAU,IAAI;AACd,cAAU,CAAC,CAAC;AAEZ,QAAI;AACF,YAAM,kBAAkB;AAAA,QACtB;AAAA,QACA;AAAA,QACA,iBAAiB;AAAA,QACjB,MAAM,YAAY,KAAK;AAAA,QACvB,QAAQ,cAAc,YAAY;AAAA,QAClC,YAAY,cAAc,OAAO,IAAI,KAAK,UAAU,EAAE,YAAY;AAAA,QAClE,aAAa,cAAc,IAAI,KAAK,WAAW,EAAE,YAAY,IAAI;AAAA,MACnE;AACA,YAAM,YAAY,MAChB,eAAe,+BAA+B;AAAA,QAC5C,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU,eAAe;AAAA,MACtC,CAAC;AAEH,UAAI,oBAAoB;AACtB,cAAM,mBAAmB,WAAW,eAAe;AAAA,MACrD,OAAO;AACL,cAAM,UAAU;AAAA,MAClB;AAEA;AAAA,QACE,EAAE,oCAAoC;AAAA,UACpC,MAAM,EAAE,oCAAoC,YAAY,EAAE;AAAA,QAC5D,CAAC;AAAA,QACD;AAAA,MACF;AACA,kBAAY;AACZ,yBAAmB,UAAU;AAC7B,0BAAoB;AAAA,IACtB,SAAS,OAAO;AACd,cAAQ,MAAM,gDAAgD,KAAK;AACnE,YAAM,EAAE,oCAAoC,yBAAyB,GAAG,OAAO;AAAA,IACjF,UAAE;AACA,gBAAU,KAAK;AAAA,IACjB;AAAA,EACF,GAAG;AAAA,IACD;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAED,QAAM,gBAAgB,MAAM,YAAY,CAAC,UAA+B;AACtE,SAAK,MAAM,WAAW,MAAM,YAAY,MAAM,QAAQ,SAAS;AAC7D,YAAM,eAAe;AACrB,iBAAW;AAAA,IACb;AAAA,EACF,GAAG,CAAC,UAAU,CAAC;AAEf,SACE,qBAAC,SAAI,WAAU,mDAAkD,WAAW,eAE1E;AAAA,yBAAC,SAAI,WAAU,0CACb;AAAA,2BAAC,QAAG,WAAU,mEACZ;AAAA,4BAAC,aAAU,WAAU,UAAS;AAAA,QAC7B,EAAE,oCAAoC,cAAc;AAAA,SACvD;AAAA,MACA,qBAAC,SAAI,WAAU,2BACZ;AAAA,8BACC,qBAAC,UAAO,MAAK,UAAS,SAAQ,WAAU,MAAK,MAAK,SAAS,qBAAqB,UAAU,QACxF;AAAA,8BAAC,YAAS,WAAU,UAAS;AAAA,UAC5B,EAAE,uCAAuC,UAAU;AAAA,WACtD,IACE;AAAA,QACJ;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,MAAK;AAAA,YACL,SAAS;AAAA,YACT,UAAU,UAAU,CAAC;AAAA,YAErB;AAAA,kCAAC,SAAM,WAAU,UAAS;AAAA,cACzB,SACG,EAAE,qCAAqC,WAAW,IAClD,EAAE,2CAA2C,eAAe;AAAA;AAAA;AAAA,QAClE;AAAA,SACF;AAAA,OACF;AAAA,IAGA,oBAAC,wBAAqB,cAA4B,UAAU,kBAAkB;AAAA,IAG9E,qBAAC,SAAI,WAAU,+BACb;AAAA,2BAAC,SAAI,WAAU,UACb;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,SAAQ;AAAA,YACR,WAAU;AAAA,YAET,YAAE,+CAA+C,aAAa;AAAA;AAAA,QACjE;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,KAAK;AAAA,YACL,IAAG;AAAA,YACH,OAAO;AAAA,YACP,UAAU,CAAC,UAAU,eAAe,MAAM,OAAO,KAAK;AAAA,YACtD,aAAa,EAAE,qDAAqD,gBAAgB;AAAA,YACpF,WAAU;AAAA,YACV,MAAM;AAAA;AAAA,QACR;AAAA,QACC,OAAO,cACN,oBAAC,OAAE,WAAU,iCAAiC,iBAAO,aAAY,IAC/D;AAAA,SACN;AAAA,MACA,qBAAC,WAAM,WAAU,uIACf;AAAA,4BAAC,YAAS,WAAU,UAAS;AAAA,QAC7B,oBAAC,UAAK,WAAU,uCAAuC,0BAAgB,YAAY,CAAC,GAAE;AAAA,QACtF;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,OAAO;AAAA,YACP,UAAU,CAAC,UAAU,cAAc,MAAM,OAAO,KAAK;AAAA,YACrD,WAAU;AAAA;AAAA,QACZ;AAAA,SACF;AAAA,OACF;AAAA,IAEA,qBAAC,SAAI,WAAU,QACb;AAAA,2BAAC,SAAI,WAAU,0CACb;AAAA,4BAAC,UAAK,WAAU,qEACb,YAAE,+CAA+C,WAAW,GAC/D;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,MAAK;AAAA,YACL,SAAS;AAAA,YACT,iBAAe,CAAC;AAAA,YAChB,iBAAc;AAAA,YACd,WAAU;AAAA,YAET,8BACC,iCACE;AAAA,kCAAC,eAAY,WAAU,eAAc;AAAA,cACpC,EAAE,8CAA8C,mBAAmB;AAAA,eACtE,IAEA,iCACE;AAAA,kCAAC,aAAU,WAAU,eAAc;AAAA,cAClC,EAAE,8CAA8C,mBAAmB;AAAA,eACtE;AAAA;AAAA,QAEJ;AAAA,SACF;AAAA,MACC,CAAC,oBACA,oBAAC,SAAI,IAAG,yCACN;AAAA,QAAC;AAAA;AAAA,UACC;AAAA,UACA;AAAA,UACA,YAAY;AAAA;AAAA,MACd,GACF,IACE;AAAA,OACN;AAAA,IAGC,eAAe,QACd,qBAAC,SAAI,WAAU,gCACb;AAAA,0BAAC,WAAM,WAAU,iCACd,YAAE,6CAA6C,eAAe,GACjE;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,OAAO;AAAA,UACP,UAAU,CAAC,UAAU,eAAe,MAAM,OAAO,KAAK;AAAA,UACtD,WAAU;AAAA;AAAA,MACZ;AAAA,OACF,IACE;AAAA,KACN;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { VersionHistoryAction } from "@open-mercato/ui/backend/version-history";
|
|
5
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
6
|
+
const OUTLINE_ICON_BUTTON_CLASSES = "size-8 rounded-md border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50";
|
|
7
|
+
function ObjectHistoryButton({
|
|
8
|
+
resourceKind,
|
|
9
|
+
resourceId,
|
|
10
|
+
resourceIdFallback,
|
|
11
|
+
organizationId,
|
|
12
|
+
includeRelated
|
|
13
|
+
}) {
|
|
14
|
+
const t = useT();
|
|
15
|
+
const config = React.useMemo(
|
|
16
|
+
() => ({
|
|
17
|
+
resourceKind,
|
|
18
|
+
resourceId,
|
|
19
|
+
resourceIdFallback,
|
|
20
|
+
organizationId,
|
|
21
|
+
includeRelated
|
|
22
|
+
}),
|
|
23
|
+
[resourceKind, resourceId, resourceIdFallback, organizationId, includeRelated]
|
|
24
|
+
);
|
|
25
|
+
return /* @__PURE__ */ jsx(
|
|
26
|
+
VersionHistoryAction,
|
|
27
|
+
{
|
|
28
|
+
config,
|
|
29
|
+
t,
|
|
30
|
+
buttonClassName: OUTLINE_ICON_BUTTON_CLASSES
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
var ObjectHistoryButton_default = ObjectHistoryButton;
|
|
35
|
+
export {
|
|
36
|
+
ObjectHistoryButton,
|
|
37
|
+
ObjectHistoryButton_default as default
|
|
38
|
+
};
|
|
39
|
+
//# sourceMappingURL=ObjectHistoryButton.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/modules/customers/components/detail/ObjectHistoryButton.tsx"],
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { VersionHistoryAction } from '@open-mercato/ui/backend/version-history'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { VersionHistoryConfig } from '@open-mercato/ui/backend/version-history'\n\nexport type ObjectHistoryButtonProps = {\n resourceKind: VersionHistoryConfig['resourceKind']\n resourceId: VersionHistoryConfig['resourceId']\n resourceIdFallback?: VersionHistoryConfig['resourceIdFallback']\n organizationId?: VersionHistoryConfig['organizationId']\n includeRelated?: VersionHistoryConfig['includeRelated']\n}\n\nconst OUTLINE_ICON_BUTTON_CLASSES =\n 'size-8 rounded-md border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50'\n\nexport function ObjectHistoryButton({\n resourceKind,\n resourceId,\n resourceIdFallback,\n organizationId,\n includeRelated,\n}: ObjectHistoryButtonProps) {\n const t = useT()\n const config = React.useMemo<VersionHistoryConfig>(\n () => ({\n resourceKind,\n resourceId,\n resourceIdFallback,\n organizationId,\n includeRelated,\n }),\n [resourceKind, resourceId, resourceIdFallback, organizationId, includeRelated],\n )\n\n return (\n <VersionHistoryAction\n config={config}\n t={t}\n buttonClassName={OUTLINE_ICON_BUTTON_CLASSES}\n />\n )\n}\n\nexport default ObjectHistoryButton\n"],
|
|
5
|
+
"mappings": ";AAsCI;AApCJ,YAAY,WAAW;AACvB,SAAS,4BAA4B;AACrC,SAAS,YAAY;AAWrB,MAAM,8BACJ;AAEK,SAAS,oBAAoB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA6B;AAC3B,QAAM,IAAI,KAAK;AACf,QAAM,SAAS,MAAM;AAAA,IACnB,OAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,CAAC,cAAc,YAAY,oBAAoB,gBAAgB,cAAc;AAAA,EAC/E;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA;AAAA,MACA,iBAAiB;AAAA;AAAA,EACnB;AAEJ;AAEA,IAAO,8BAAQ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -8,11 +8,14 @@ import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
|
8
8
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
9
9
|
import { IconButton } from "@open-mercato/ui/primitives/icon-button";
|
|
10
10
|
import { Badge } from "@open-mercato/ui/primitives/badge";
|
|
11
|
+
import { SendObjectMessageDialog } from "@open-mercato/ui/backend/messages";
|
|
11
12
|
import { useQueryClient } from "@tanstack/react-query";
|
|
13
|
+
import { ObjectHistoryButton } from "./ObjectHistoryButton.js";
|
|
12
14
|
import { PersonTagsDialog } from "./PersonTagsDialog.js";
|
|
13
15
|
import { useCustomerDictionary, invalidateCustomerDictionary } from "./hooks/useCustomerDictionary.js";
|
|
14
16
|
import { renderDictionaryIcon } from "../../../dictionaries/components/dictionaryAppearance.js";
|
|
15
17
|
import { getInitials, formatFallbackLabel } from "./utils.js";
|
|
18
|
+
const HEADER_ICON_BUTTON_CLASS = "size-8 rounded-md";
|
|
16
19
|
function DictionaryBadge({ value, map, categoryIcon, className }) {
|
|
17
20
|
const entry = map?.[value];
|
|
18
21
|
const color = entry?.color ?? null;
|
|
@@ -160,6 +163,33 @@ function PersonDetailHeader({
|
|
|
160
163
|
] })
|
|
161
164
|
] }),
|
|
162
165
|
/* @__PURE__ */ jsxs("div", { className: "flex w-full shrink-0 items-center justify-start gap-2 sm:w-auto sm:justify-end", children: [
|
|
166
|
+
/* @__PURE__ */ jsx(
|
|
167
|
+
SendObjectMessageDialog,
|
|
168
|
+
{
|
|
169
|
+
object: {
|
|
170
|
+
entityModule: "customers",
|
|
171
|
+
entityType: "person",
|
|
172
|
+
entityId: person.id,
|
|
173
|
+
previewData: {
|
|
174
|
+
title: displayName,
|
|
175
|
+
subtitle: person.primaryEmail ?? companyName ?? void 0
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
viewHref: `/backend/customers/people-v2/${person.id}`,
|
|
179
|
+
buttonVariant: "outline",
|
|
180
|
+
buttonSize: "icon",
|
|
181
|
+
buttonClassName: HEADER_ICON_BUTTON_CLASS,
|
|
182
|
+
buttonLabel: t("customers.people.detail.actions.sendMessage", "Send message")
|
|
183
|
+
}
|
|
184
|
+
),
|
|
185
|
+
/* @__PURE__ */ jsx(
|
|
186
|
+
ObjectHistoryButton,
|
|
187
|
+
{
|
|
188
|
+
resourceKind: "customers.person",
|
|
189
|
+
resourceId: person.id,
|
|
190
|
+
organizationId: person.organizationId ?? void 0
|
|
191
|
+
}
|
|
192
|
+
),
|
|
163
193
|
/* @__PURE__ */ jsx(
|
|
164
194
|
IconButton,
|
|
165
195
|
{
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customers/components/detail/PersonDetailHeader.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { Phone, Mail, Building2, Trash2, Pencil } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { useQueryClient } from '@tanstack/react-query'\nimport { PersonTagsDialog } from './PersonTagsDialog'\nimport { useCustomerDictionary, invalidateCustomerDictionary } from './hooks/useCustomerDictionary'\nimport { renderDictionaryIcon } from '../../../dictionaries/components/dictionaryAppearance'\nimport type { TagSummary } from './types'\nimport type { TagsSectionController } from '@open-mercato/ui/backend/detail'\nimport type { PersonOverview } from '../formConfig'\nimport type { CustomerDictionaryMap } from '@open-mercato/core/modules/customers/lib/dictionaries'\nimport { getInitials, formatFallbackLabel } from './utils'\n\ntype PersonDetailHeaderProps = {\n data: PersonOverview\n onTagsChange: (tags: TagSummary[]) => void\n tagsSectionControllerRef: React.RefObject<TagsSectionController | null>\n onSave: () => void\n onDelete: () => Promise<void>\n isDirty: boolean\n isSaving: boolean\n /** Callback to focus a specific field in the Zone 1 CrudForm by field name. */\n onFocusField?: (fieldName: string) => void\n /**\n * @deprecated Kept for backward compatibility. The \"+ Link company\" header CTA was removed;\n * company linking now happens exclusively through the Zone 2 Companies tab via\n * `PersonCompaniesSection`. This prop is a no-op and will be removed in a future major release.\n */\n onOpenCompaniesTab?: () => void\n /** Callback to reload person data after tags dialog save. */\n onDataReload?: () => void\n}\n\nfunction DictionaryBadge({ value, map, categoryIcon, className }: { value: string; map: CustomerDictionaryMap | undefined; categoryIcon?: React.ReactNode; className?: string }) {\n const entry = map?.[value]\n const color = entry?.color ?? null\n const icon = entry?.icon ?? null\n const label = entry?.label ?? formatFallbackLabel(value)\n const colorStyle: React.CSSProperties | undefined = color\n ? { color, borderColor: color, backgroundColor: `${color}1A` }\n : undefined\n return (\n <Badge\n variant=\"outline\"\n className={cn(\n 'rounded-sm gap-1.5 text-xs font-medium',\n className,\n )}\n style={colorStyle}\n >\n {icon ? renderDictionaryIcon(icon, 'size-2.5') : categoryIcon ?? null}\n {label}\n </Badge>\n )\n}\n\n/** Renders a tag badge with color-based text/border/background from TagSummary.color. */\nfunction TagBadge({ tag }: { tag: TagSummary }) {\n const colorStyle: React.CSSProperties | undefined = tag.color\n ? { color: tag.color, borderColor: tag.color, backgroundColor: `${tag.color}1A` }\n : undefined\n return (\n <Badge variant=\"outline\" className=\"rounded-sm gap-1.5 text-xs font-medium\" style={colorStyle}>\n {tag.label}\n </Badge>\n )\n}\n\nexport function PersonDetailHeader({\n data,\n onTagsChange,\n tagsSectionControllerRef,\n onSave,\n onDelete,\n isDirty,\n isSaving,\n onFocusField,\n onOpenCompaniesTab,\n onDataReload,\n}: PersonDetailHeaderProps) {\n const t = useT()\n const queryClient = useQueryClient()\n const [manageTagsOpen, setManageTagsOpen] = React.useState(false)\n const person = data.person\n const profile = data.profile\n const displayName = person.displayName || t('customers.people.detail.untitled', 'Untitled')\n\n const jobTitle = profile?.jobTitle ?? null\n const linkedCompanies = React.useMemo(() => {\n const items = Array.isArray(data.companies) && data.companies.length > 0\n ? data.companies\n : data.company\n ? [{ ...data.company, isPrimary: Boolean(data.isPrimary) }]\n : []\n return items\n }, [data.companies, data.company, data.isPrimary])\n const visibleCompanies = React.useMemo(() => linkedCompanies.slice(0, 3), [linkedCompanies])\n const hiddenCompaniesCount = Math.max(0, linkedCompanies.length - visibleCompanies.length)\n const visibleTags = React.useMemo(() => data.tags.slice(0, 6), [data.tags])\n const hiddenTagsCount = Math.max(0, data.tags.length - visibleTags.length)\n const primaryCompany = linkedCompanies.find((entry) => entry.isPrimary) ?? linkedCompanies[0] ?? null\n const companyName = primaryCompany?.displayName ?? null\n const companyId = primaryCompany?.id ?? profile?.companyEntityId ?? null\n\n // Fetch dictionary maps for colored badge rendering (scoped to person's organization)\n const personOrgId = person.organizationId ?? null\n const { data: statusDict } = useCustomerDictionary('statuses', 0, personOrgId)\n const { data: lifecycleDict } = useCustomerDictionary('lifecycle-stages', 0, personOrgId)\n const { data: sourceDict } = useCustomerDictionary('sources', 0, personOrgId)\n const { data: temperatureDict } = useCustomerDictionary('temperature', 0, personOrgId)\n const { data: renewalQuarterDict } = useCustomerDictionary('renewal-quarters', 0, personOrgId)\n\n return (\n <div className=\"rounded-lg border bg-card px-6 py-5\">\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:gap-5\">\n {/* Avatar */}\n <div className=\"flex size-18 shrink-0 items-center justify-center rounded-full bg-muted text-xl font-bold text-muted-foreground\">\n {getInitials(displayName)}\n </div>\n\n {/* Person info */}\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-2\">\n <h1 className=\"truncate text-2xl font-bold text-foreground\">{displayName}</h1>\n {data.isPrimary && (\n <span className=\"shrink-0 rounded-sm bg-status-warning-bg px-1.5 py-0.5 text-overline font-bold text-status-warning-text\">\n {t('customers.people.detail.header.primary', 'PRIMARY')}\n </span>\n )}\n </div>\n\n {/* Subtitle: job title + company link */}\n {(jobTitle || companyName) && (\n <p className=\"mt-0.5 text-sm text-muted-foreground\">\n {jobTitle}\n {jobTitle && companyName && ` ${t('customers.people.detail.header.at', 'at')} `}\n {companyName && companyId && (\n <Link href={`/backend/customers/companies-v2/${companyId}`} className=\"text-primary hover:underline\">\n {companyName}\n </Link>\n )}\n {companyName && !companyId && companyName}\n </p>\n )}\n\n {/* Contact row */}\n <div className=\"mt-1.5 flex flex-wrap items-center gap-x-5 gap-y-1 text-sm text-muted-foreground\">\n {person.primaryPhone && (\n <span className=\"inline-flex items-center gap-1.5\">\n <Phone className=\"size-3.5\" />\n <a href={`tel:${person.primaryPhone}`} className=\"hover:text-foreground\">{person.primaryPhone}</a>\n </span>\n )}\n {person.primaryEmail && (\n <span className=\"inline-flex items-center gap-1.5\">\n <Mail className=\"size-3.5\" />\n <a href={`mailto:${person.primaryEmail}`} className=\"hover:text-foreground\">{person.primaryEmail}</a>\n </span>\n )}\n </div>\n\n {/* Company chips (annotation 1a) */}\n {linkedCompanies.length > 0 && (\n <div className=\"mt-1.5 flex flex-wrap items-center gap-2 text-sm\">\n <span className=\"text-muted-foreground\">\n <Building2 className=\"mr-1 inline size-3.5\" />\n {t('customers.people.detail.header.companies', 'Companies')} ({linkedCompanies.length}):\n </span>\n {visibleCompanies.map((company) => (\n <Link\n key={company.id}\n href={`/backend/customers/companies-v2/${company.id}`}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-sm border px-2 py-0.5 text-xs font-semibold transition-colors hover:bg-status-info-bg',\n company.isPrimary\n ? 'border-status-info-border bg-status-info-bg text-status-info-text'\n : 'border-border bg-background text-foreground',\n )}\n >\n <Building2 className=\"size-3\" />\n {company.displayName}\n {company.isPrimary ? (\n <span className=\"rounded-sm bg-status-info-icon px-1 py-px text-overline font-bold text-white\">\n {t('customers.people.detail.header.primary', 'PRIMARY')}\n </span>\n ) : null}\n </Link>\n ))}\n {hiddenCompaniesCount > 0 ? (\n <Badge variant=\"outline\" className=\"rounded-sm text-xs font-semibold\">\n +{hiddenCompaniesCount} {t('customers.people.detail.header.more', 'more')}\n </Badge>\n ) : null}\n </div>\n )}\n\n {/* Status badges + inline tags + manage tags */}\n <div className=\"mt-2.5 flex flex-wrap items-center gap-2\">\n {person.status && (\n <DictionaryBadge value={person.status} map={statusDict?.map} />\n )}\n {person.lifecycleStage && (\n <DictionaryBadge value={person.lifecycleStage} map={lifecycleDict?.map} />\n )}\n {person.source && (\n <DictionaryBadge value={person.source} map={sourceDict?.map} />\n )}\n {person.temperature && (\n <DictionaryBadge value={person.temperature} map={temperatureDict?.map} />\n )}\n {person.renewalQuarter && (\n <DictionaryBadge value={person.renewalQuarter} map={renewalQuarterDict?.map} />\n )}\n {/* Inline tag pills */}\n {visibleTags.map((tag) => (\n <TagBadge key={tag.id ?? tag.label} tag={tag} />\n ))}\n {hiddenTagsCount > 0 ? (\n <Badge variant=\"outline\" className=\"rounded-sm gap-1.5 text-xs font-medium\">\n +{hiddenTagsCount} {t('customers.people.detail.header.more', 'more')}\n </Badge>\n ) : null}\n {/* Manage tags \u2014 opens dialog directly */}\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-auto rounded-sm px-2 py-1 text-xs font-medium text-muted-foreground hover:text-foreground\"\n onClick={() => setManageTagsOpen(true)}\n >\n <Pencil className=\"mr-1 size-3\" />\n {t('customers.people.detail.actions.manageTags', 'Edit tags')}\n </Button>\n </div>\n </div>\n\n {/* Right side: actions */}\n <div className=\"flex w-full shrink-0 items-center justify-start gap-2 sm:w-auto sm:justify-end\">\n <IconButton\n variant=\"outline\"\n size=\"sm\"\n type=\"button\"\n aria-label={t('customers.people.detail.actions.delete', 'Delete')}\n onClick={() => {\n void onDelete()\n }}\n >\n <Trash2 className=\"size-4\" />\n </IconButton>\n <Button\n type=\"button\"\n size=\"sm\"\n onClick={onSave}\n disabled={!isDirty || isSaving}\n >\n {t('customers.people.detail.actions.save', 'Save')}\n </Button>\n </div>\n </div>\n <PersonTagsDialog\n open={manageTagsOpen}\n onClose={() => setManageTagsOpen(false)}\n personId={person.id}\n personOrganizationId={person.organizationId ?? null}\n personData={{\n status: person.status,\n lifecycleStage: person.lifecycleStage,\n source: person.source,\n temperature: person.temperature,\n renewalQuarter: person.renewalQuarter,\n jobTitle: data.profile?.jobTitle ?? null,\n customFields: data.customFields,\n tags: data.tags,\n }}\n onSaved={() => {\n // Invalidate dictionary caches so header badges pick up fresh colors\n void invalidateCustomerDictionary(queryClient, 'statuses')\n void invalidateCustomerDictionary(queryClient, 'lifecycle-stages')\n void invalidateCustomerDictionary(queryClient, 'sources')\n void invalidateCustomerDictionary(queryClient, 'temperature')\n void invalidateCustomerDictionary(queryClient, 'renewal-quarters')\n onDataReload?.()\n }}\n />\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { Phone, Mail, Building2, Trash2, Pencil } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { SendObjectMessageDialog } from '@open-mercato/ui/backend/messages'\nimport { useQueryClient } from '@tanstack/react-query'\nimport { ObjectHistoryButton } from './ObjectHistoryButton'\nimport { PersonTagsDialog } from './PersonTagsDialog'\nimport { useCustomerDictionary, invalidateCustomerDictionary } from './hooks/useCustomerDictionary'\nimport { renderDictionaryIcon } from '../../../dictionaries/components/dictionaryAppearance'\nimport type { TagSummary } from './types'\nimport type { TagsSectionController } from '@open-mercato/ui/backend/detail'\nimport type { PersonOverview } from '../formConfig'\nimport type { CustomerDictionaryMap } from '@open-mercato/core/modules/customers/lib/dictionaries'\nimport { getInitials, formatFallbackLabel } from './utils'\n\nconst HEADER_ICON_BUTTON_CLASS = 'size-8 rounded-md'\n\ntype PersonDetailHeaderProps = {\n data: PersonOverview\n onTagsChange: (tags: TagSummary[]) => void\n tagsSectionControllerRef: React.RefObject<TagsSectionController | null>\n onSave: () => void\n onDelete: () => Promise<void>\n isDirty: boolean\n isSaving: boolean\n /** Callback to focus a specific field in the Zone 1 CrudForm by field name. */\n onFocusField?: (fieldName: string) => void\n /**\n * @deprecated Kept for backward compatibility. The \"+ Link company\" header CTA was removed;\n * company linking now happens exclusively through the Zone 2 Companies tab via\n * `PersonCompaniesSection`. This prop is a no-op and will be removed in a future major release.\n */\n onOpenCompaniesTab?: () => void\n /** Callback to reload person data after tags dialog save. */\n onDataReload?: () => void\n}\n\nfunction DictionaryBadge({ value, map, categoryIcon, className }: { value: string; map: CustomerDictionaryMap | undefined; categoryIcon?: React.ReactNode; className?: string }) {\n const entry = map?.[value]\n const color = entry?.color ?? null\n const icon = entry?.icon ?? null\n const label = entry?.label ?? formatFallbackLabel(value)\n const colorStyle: React.CSSProperties | undefined = color\n ? { color, borderColor: color, backgroundColor: `${color}1A` }\n : undefined\n return (\n <Badge\n variant=\"outline\"\n className={cn(\n 'rounded-sm gap-1.5 text-xs font-medium',\n className,\n )}\n style={colorStyle}\n >\n {icon ? renderDictionaryIcon(icon, 'size-2.5') : categoryIcon ?? null}\n {label}\n </Badge>\n )\n}\n\n/** Renders a tag badge with color-based text/border/background from TagSummary.color. */\nfunction TagBadge({ tag }: { tag: TagSummary }) {\n const colorStyle: React.CSSProperties | undefined = tag.color\n ? { color: tag.color, borderColor: tag.color, backgroundColor: `${tag.color}1A` }\n : undefined\n return (\n <Badge variant=\"outline\" className=\"rounded-sm gap-1.5 text-xs font-medium\" style={colorStyle}>\n {tag.label}\n </Badge>\n )\n}\n\nexport function PersonDetailHeader({\n data,\n onTagsChange,\n tagsSectionControllerRef,\n onSave,\n onDelete,\n isDirty,\n isSaving,\n onFocusField,\n onOpenCompaniesTab,\n onDataReload,\n}: PersonDetailHeaderProps) {\n const t = useT()\n const queryClient = useQueryClient()\n const [manageTagsOpen, setManageTagsOpen] = React.useState(false)\n const person = data.person\n const profile = data.profile\n const displayName = person.displayName || t('customers.people.detail.untitled', 'Untitled')\n\n const jobTitle = profile?.jobTitle ?? null\n const linkedCompanies = React.useMemo(() => {\n const items = Array.isArray(data.companies) && data.companies.length > 0\n ? data.companies\n : data.company\n ? [{ ...data.company, isPrimary: Boolean(data.isPrimary) }]\n : []\n return items\n }, [data.companies, data.company, data.isPrimary])\n const visibleCompanies = React.useMemo(() => linkedCompanies.slice(0, 3), [linkedCompanies])\n const hiddenCompaniesCount = Math.max(0, linkedCompanies.length - visibleCompanies.length)\n const visibleTags = React.useMemo(() => data.tags.slice(0, 6), [data.tags])\n const hiddenTagsCount = Math.max(0, data.tags.length - visibleTags.length)\n const primaryCompany = linkedCompanies.find((entry) => entry.isPrimary) ?? linkedCompanies[0] ?? null\n const companyName = primaryCompany?.displayName ?? null\n const companyId = primaryCompany?.id ?? profile?.companyEntityId ?? null\n\n // Fetch dictionary maps for colored badge rendering (scoped to person's organization)\n const personOrgId = person.organizationId ?? null\n const { data: statusDict } = useCustomerDictionary('statuses', 0, personOrgId)\n const { data: lifecycleDict } = useCustomerDictionary('lifecycle-stages', 0, personOrgId)\n const { data: sourceDict } = useCustomerDictionary('sources', 0, personOrgId)\n const { data: temperatureDict } = useCustomerDictionary('temperature', 0, personOrgId)\n const { data: renewalQuarterDict } = useCustomerDictionary('renewal-quarters', 0, personOrgId)\n\n return (\n <div className=\"rounded-lg border bg-card px-6 py-5\">\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:gap-5\">\n {/* Avatar */}\n <div className=\"flex size-18 shrink-0 items-center justify-center rounded-full bg-muted text-xl font-bold text-muted-foreground\">\n {getInitials(displayName)}\n </div>\n\n {/* Person info */}\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-2\">\n <h1 className=\"truncate text-2xl font-bold text-foreground\">{displayName}</h1>\n {data.isPrimary && (\n <span className=\"shrink-0 rounded-sm bg-status-warning-bg px-1.5 py-0.5 text-overline font-bold text-status-warning-text\">\n {t('customers.people.detail.header.primary', 'PRIMARY')}\n </span>\n )}\n </div>\n\n {/* Subtitle: job title + company link */}\n {(jobTitle || companyName) && (\n <p className=\"mt-0.5 text-sm text-muted-foreground\">\n {jobTitle}\n {jobTitle && companyName && ` ${t('customers.people.detail.header.at', 'at')} `}\n {companyName && companyId && (\n <Link href={`/backend/customers/companies-v2/${companyId}`} className=\"text-primary hover:underline\">\n {companyName}\n </Link>\n )}\n {companyName && !companyId && companyName}\n </p>\n )}\n\n {/* Contact row */}\n <div className=\"mt-1.5 flex flex-wrap items-center gap-x-5 gap-y-1 text-sm text-muted-foreground\">\n {person.primaryPhone && (\n <span className=\"inline-flex items-center gap-1.5\">\n <Phone className=\"size-3.5\" />\n <a href={`tel:${person.primaryPhone}`} className=\"hover:text-foreground\">{person.primaryPhone}</a>\n </span>\n )}\n {person.primaryEmail && (\n <span className=\"inline-flex items-center gap-1.5\">\n <Mail className=\"size-3.5\" />\n <a href={`mailto:${person.primaryEmail}`} className=\"hover:text-foreground\">{person.primaryEmail}</a>\n </span>\n )}\n </div>\n\n {/* Company chips (annotation 1a) */}\n {linkedCompanies.length > 0 && (\n <div className=\"mt-1.5 flex flex-wrap items-center gap-2 text-sm\">\n <span className=\"text-muted-foreground\">\n <Building2 className=\"mr-1 inline size-3.5\" />\n {t('customers.people.detail.header.companies', 'Companies')} ({linkedCompanies.length}):\n </span>\n {visibleCompanies.map((company) => (\n <Link\n key={company.id}\n href={`/backend/customers/companies-v2/${company.id}`}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-sm border px-2 py-0.5 text-xs font-semibold transition-colors hover:bg-status-info-bg',\n company.isPrimary\n ? 'border-status-info-border bg-status-info-bg text-status-info-text'\n : 'border-border bg-background text-foreground',\n )}\n >\n <Building2 className=\"size-3\" />\n {company.displayName}\n {company.isPrimary ? (\n <span className=\"rounded-sm bg-status-info-icon px-1 py-px text-overline font-bold text-white\">\n {t('customers.people.detail.header.primary', 'PRIMARY')}\n </span>\n ) : null}\n </Link>\n ))}\n {hiddenCompaniesCount > 0 ? (\n <Badge variant=\"outline\" className=\"rounded-sm text-xs font-semibold\">\n +{hiddenCompaniesCount} {t('customers.people.detail.header.more', 'more')}\n </Badge>\n ) : null}\n </div>\n )}\n\n {/* Status badges + inline tags + manage tags */}\n <div className=\"mt-2.5 flex flex-wrap items-center gap-2\">\n {person.status && (\n <DictionaryBadge value={person.status} map={statusDict?.map} />\n )}\n {person.lifecycleStage && (\n <DictionaryBadge value={person.lifecycleStage} map={lifecycleDict?.map} />\n )}\n {person.source && (\n <DictionaryBadge value={person.source} map={sourceDict?.map} />\n )}\n {person.temperature && (\n <DictionaryBadge value={person.temperature} map={temperatureDict?.map} />\n )}\n {person.renewalQuarter && (\n <DictionaryBadge value={person.renewalQuarter} map={renewalQuarterDict?.map} />\n )}\n {/* Inline tag pills */}\n {visibleTags.map((tag) => (\n <TagBadge key={tag.id ?? tag.label} tag={tag} />\n ))}\n {hiddenTagsCount > 0 ? (\n <Badge variant=\"outline\" className=\"rounded-sm gap-1.5 text-xs font-medium\">\n +{hiddenTagsCount} {t('customers.people.detail.header.more', 'more')}\n </Badge>\n ) : null}\n {/* Manage tags \u2014 opens dialog directly */}\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-auto rounded-sm px-2 py-1 text-xs font-medium text-muted-foreground hover:text-foreground\"\n onClick={() => setManageTagsOpen(true)}\n >\n <Pencil className=\"mr-1 size-3\" />\n {t('customers.people.detail.actions.manageTags', 'Edit tags')}\n </Button>\n </div>\n </div>\n\n {/* Right side: actions */}\n <div className=\"flex w-full shrink-0 items-center justify-start gap-2 sm:w-auto sm:justify-end\">\n <SendObjectMessageDialog\n object={{\n entityModule: 'customers',\n entityType: 'person',\n entityId: person.id,\n previewData: {\n title: displayName,\n subtitle: person.primaryEmail ?? companyName ?? undefined,\n },\n }}\n viewHref={`/backend/customers/people-v2/${person.id}`}\n buttonVariant=\"outline\"\n buttonSize=\"icon\"\n buttonClassName={HEADER_ICON_BUTTON_CLASS}\n buttonLabel={t('customers.people.detail.actions.sendMessage', 'Send message')}\n />\n <ObjectHistoryButton\n resourceKind=\"customers.person\"\n resourceId={person.id}\n organizationId={person.organizationId ?? undefined}\n />\n <IconButton\n variant=\"outline\"\n size=\"sm\"\n type=\"button\"\n aria-label={t('customers.people.detail.actions.delete', 'Delete')}\n onClick={() => {\n void onDelete()\n }}\n >\n <Trash2 className=\"size-4\" />\n </IconButton>\n <Button\n type=\"button\"\n size=\"sm\"\n onClick={onSave}\n disabled={!isDirty || isSaving}\n >\n {t('customers.people.detail.actions.save', 'Save')}\n </Button>\n </div>\n </div>\n <PersonTagsDialog\n open={manageTagsOpen}\n onClose={() => setManageTagsOpen(false)}\n personId={person.id}\n personOrganizationId={person.organizationId ?? null}\n personData={{\n status: person.status,\n lifecycleStage: person.lifecycleStage,\n source: person.source,\n temperature: person.temperature,\n renewalQuarter: person.renewalQuarter,\n jobTitle: data.profile?.jobTitle ?? null,\n customFields: data.customFields,\n tags: data.tags,\n }}\n onSaved={() => {\n // Invalidate dictionary caches so header badges pick up fresh colors\n void invalidateCustomerDictionary(queryClient, 'statuses')\n void invalidateCustomerDictionary(queryClient, 'lifecycle-stages')\n void invalidateCustomerDictionary(queryClient, 'sources')\n void invalidateCustomerDictionary(queryClient, 'temperature')\n void invalidateCustomerDictionary(queryClient, 'renewal-quarters')\n onDataReload?.()\n }}\n />\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -7,11 +7,18 @@ import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
|
7
7
|
import { apiCallOrThrow, readApiResultOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
|
|
8
8
|
import { flash } from "@open-mercato/ui/backend/FlashMessages";
|
|
9
9
|
import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuardedMutation";
|
|
10
|
+
import { useBackendChrome } from "@open-mercato/ui/backend/BackendChromeProvider";
|
|
10
11
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
12
|
+
import { hasFeature } from "@open-mercato/shared/security/features";
|
|
11
13
|
import { RoleAssignmentRow } from "./RoleAssignmentRow.js";
|
|
12
14
|
import { AssignRoleDialog } from "./AssignRoleDialog.js";
|
|
13
15
|
function RolesSection({ entityType, entityId, entityName }) {
|
|
14
16
|
const t = useT();
|
|
17
|
+
const { payload } = useBackendChrome();
|
|
18
|
+
const canManageRoleTypes = React.useMemo(
|
|
19
|
+
() => hasFeature(payload?.grantedFeatures ?? [], "customers.settings.manage"),
|
|
20
|
+
[payload?.grantedFeatures]
|
|
21
|
+
);
|
|
15
22
|
const [roles, setRoles] = React.useState([]);
|
|
16
23
|
const [roleTypes, setRoleTypes] = React.useState([]);
|
|
17
24
|
const [loading, setLoading] = React.useState(true);
|
|
@@ -68,9 +75,9 @@ function RolesSection({ entityType, entityId, entityName }) {
|
|
|
68
75
|
let active = true;
|
|
69
76
|
readApiResultOrThrow(
|
|
70
77
|
"/api/customers/dictionaries/person-company-roles"
|
|
71
|
-
).then((
|
|
78
|
+
).then((payload2) => {
|
|
72
79
|
if (!active) return;
|
|
73
|
-
const entries = (Array.isArray(
|
|
80
|
+
const entries = (Array.isArray(payload2?.items) ? payload2.items : []).map((item) => {
|
|
74
81
|
const id = typeof item.id === "string" ? item.id : null;
|
|
75
82
|
const value = typeof item.value === "string" ? item.value.trim() : "";
|
|
76
83
|
if (!id || value.length === 0) return null;
|
|
@@ -135,9 +142,11 @@ function RolesSection({ entityType, entityId, entityName }) {
|
|
|
135
142
|
if (loading) {
|
|
136
143
|
return /* @__PURE__ */ jsx("div", { className: "py-2 text-sm text-muted-foreground", children: t("customers.roles.loading", "Loading roles...") });
|
|
137
144
|
}
|
|
145
|
+
const resolvedEntityName = entityName && entityName.trim().length ? entityName.trim() : entityType === "company" ? t("customers.roles.defaultEntityName.company", "this company") : t("customers.roles.defaultEntityName.person", "this person");
|
|
146
|
+
const groupTitle = entityType === "company" ? t("customers.roles.groupTitle.company", "Roles at {{name}}", { name: resolvedEntityName }) : t("customers.roles.groupTitle.person", "My roles with {{name}}", { name: resolvedEntityName });
|
|
138
147
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
139
148
|
/* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
140
|
-
/* @__PURE__ */ jsx("div", { className: "text-sm font-semibold", children:
|
|
149
|
+
/* @__PURE__ */ jsx("div", { className: "text-sm font-semibold", children: groupTitle }),
|
|
141
150
|
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: entityType === "company" ? t("customers.roles.subtitle.company", "Who is responsible for this company on your side") : t("customers.roles.subtitle.person", "Who owns the relationship on your side") })
|
|
142
151
|
] }),
|
|
143
152
|
cards.length > 0 ? /* @__PURE__ */ jsx(
|
|
@@ -223,7 +232,8 @@ function RolesSection({ entityType, entityId, entityName }) {
|
|
|
223
232
|
entityName: entityName && entityName.trim().length ? entityName : entityType === "company" ? t("customers.roles.dialog.defaultEntity.company", "this company") : t("customers.roles.dialog.defaultEntity.person", "this person"),
|
|
224
233
|
existingRoleTypes: assignedRoleTypes,
|
|
225
234
|
existingAssignments: roles,
|
|
226
|
-
initialRoleType
|
|
235
|
+
initialRoleType,
|
|
236
|
+
canManageRoleTypes
|
|
227
237
|
}
|
|
228
238
|
)
|
|
229
239
|
] });
|