@open-mercato/core 0.5.1-develop.3045.b4b3320cc2 → 0.6.0
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/AGENTS.md +21 -1
- package/dist/modules/api_keys/api/keys/route.js +9 -0
- package/dist/modules/api_keys/api/keys/route.js.map +2 -2
- package/dist/modules/audit_logs/services/accessLogService.js +13 -0
- package/dist/modules/audit_logs/services/accessLogService.js.map +3 -3
- package/dist/modules/audit_logs/services/actionLogService.js +6 -5
- package/dist/modules/audit_logs/services/actionLogService.js.map +2 -2
- package/dist/modules/auth/api/roles/acl/route.js +27 -37
- package/dist/modules/auth/api/roles/acl/route.js.map +2 -2
- package/dist/modules/auth/api/users/route.js +41 -28
- package/dist/modules/auth/api/users/route.js.map +3 -3
- package/dist/modules/auth/lib/grantChecks.js +160 -0
- package/dist/modules/auth/lib/grantChecks.js.map +7 -0
- package/dist/modules/configs/cli.js +11 -0
- package/dist/modules/configs/cli.js.map +2 -2
- package/dist/modules/configs/lib/touchGeneratedBarrels.js +46 -0
- package/dist/modules/configs/lib/touchGeneratedBarrels.js.map +7 -0
- package/dist/modules/customers/api/activities/route.js +1 -52
- package/dist/modules/customers/api/activities/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/counts/route.js +2 -1
- package/dist/modules/customers/api/interactions/counts/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/route.js +21 -1
- package/dist/modules/customers/api/interactions/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +7 -3
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js +5 -1
- package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +7 -3
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivitiesCard.js +62 -6
- package/dist/modules/customers/components/detail/ActivitiesCard.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivitiesDayStrip.js +21 -6
- package/dist/modules/customers/components/detail/ActivitiesDayStrip.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivitiesSection.js +37 -5
- package/dist/modules/customers/components/detail/ActivitiesSection.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityCard.js +69 -17
- package/dist/modules/customers/components/detail/ActivityCard.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityHistorySection.js +94 -34
- package/dist/modules/customers/components/detail/ActivityHistorySection.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityLogTab.js +3 -1
- package/dist/modules/customers/components/detail/ActivityLogTab.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityTimeline.js +41 -8
- package/dist/modules/customers/components/detail/ActivityTimeline.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityTimelineFilters.js +19 -6
- package/dist/modules/customers/components/detail/ActivityTimelineFilters.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityTypeSelector.js +4 -3
- package/dist/modules/customers/components/detail/ActivityTypeSelector.js.map +2 -2
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +80 -12
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/DateTimeFields.js +65 -10
- package/dist/modules/customers/components/detail/schedule/DateTimeFields.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js +10 -5
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js.map +2 -2
- package/dist/modules/customers/data/validators.js +74 -2
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/customers/lib/legacyActivityBridge.js +61 -0
- package/dist/modules/customers/lib/legacyActivityBridge.js.map +7 -0
- package/dist/modules/integrations/data/validators.js +2 -2
- package/dist/modules/integrations/data/validators.js.map +2 -2
- package/dist/modules/integrations/lib/credentials-service.js +12 -1
- package/dist/modules/integrations/lib/credentials-service.js.map +2 -2
- package/dist/modules/messages/commands/actions.js +29 -14
- package/dist/modules/messages/commands/actions.js.map +2 -2
- package/dist/modules/messages/lib/actions.js +24 -4
- package/dist/modules/messages/lib/actions.js.map +2 -2
- package/dist/modules/sales/api/documents/factory.js +49 -36
- package/dist/modules/sales/api/documents/factory.js.map +2 -2
- package/package.json +9 -10
- package/src/modules/api_keys/api/keys/route.ts +9 -0
- package/src/modules/audit_logs/services/accessLogService.ts +20 -0
- package/src/modules/audit_logs/services/actionLogService.ts +13 -5
- package/src/modules/auth/api/roles/acl/route.ts +32 -46
- package/src/modules/auth/api/users/route.ts +48 -33
- package/src/modules/auth/lib/grantChecks.ts +234 -0
- package/src/modules/configs/cli.ts +11 -0
- package/src/modules/configs/lib/touchGeneratedBarrels.ts +61 -0
- package/src/modules/customers/api/activities/route.ts +1 -76
- package/src/modules/customers/api/interactions/counts/route.ts +2 -1
- package/src/modules/customers/api/interactions/route.ts +28 -1
- package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +13 -3
- package/src/modules/customers/backend/customers/deals/[id]/page.tsx +14 -2
- package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +13 -3
- package/src/modules/customers/components/detail/ActivitiesCard.tsx +92 -5
- package/src/modules/customers/components/detail/ActivitiesDayStrip.tsx +38 -6
- package/src/modules/customers/components/detail/ActivitiesSection.tsx +37 -3
- package/src/modules/customers/components/detail/ActivityCard.tsx +79 -14
- package/src/modules/customers/components/detail/ActivityHistorySection.tsx +102 -33
- package/src/modules/customers/components/detail/ActivityLogTab.tsx +7 -1
- package/src/modules/customers/components/detail/ActivityTimeline.tsx +39 -5
- package/src/modules/customers/components/detail/ActivityTimelineFilters.tsx +29 -7
- package/src/modules/customers/components/detail/ActivityTypeSelector.tsx +3 -2
- package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +96 -13
- package/src/modules/customers/components/detail/schedule/DateTimeFields.tsx +50 -4
- package/src/modules/customers/components/detail/schedule/useScheduleFormState.ts +21 -5
- package/src/modules/customers/data/validators.ts +85 -2
- package/src/modules/customers/i18n/de.json +11 -0
- package/src/modules/customers/i18n/en.json +11 -0
- package/src/modules/customers/i18n/es.json +11 -0
- package/src/modules/customers/i18n/pl.json +11 -0
- package/src/modules/customers/lib/legacyActivityBridge.ts +106 -0
- package/src/modules/integrations/data/validators.ts +8 -6
- package/src/modules/integrations/lib/credentials-service.ts +15 -1
- package/src/modules/messages/commands/actions.ts +28 -13
- package/src/modules/messages/lib/actions.ts +34 -3
- package/src/modules/sales/api/documents/factory.ts +55 -38
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import { Phone, Mail, Users, StickyNote, User } from "lucide-react";
|
|
4
|
+
import { Check, ListTodo, Phone, Mail, Users, StickyNote, User } from "lucide-react";
|
|
5
5
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
6
|
+
import { Button } from "@open-mercato/ui/primitives/button";
|
|
6
7
|
import { AiActionChips } from "./AiActionChips.js";
|
|
7
8
|
const TYPE_ICONS = {
|
|
8
9
|
call: Phone,
|
|
9
10
|
email: Mail,
|
|
10
11
|
meeting: Users,
|
|
11
|
-
note: StickyNote
|
|
12
|
+
note: StickyNote,
|
|
13
|
+
task: ListTodo
|
|
12
14
|
};
|
|
13
|
-
function ActivityTimeline({ activities, onEdit }) {
|
|
15
|
+
function ActivityTimeline({ activities, onEdit, onMarkDone }) {
|
|
14
16
|
const t = useT();
|
|
15
17
|
if (activities.length === 0) {
|
|
16
18
|
return /* @__PURE__ */ jsx("div", { className: "py-6 text-center text-sm text-muted-foreground", children: t("customers.timeline.empty", "No activities match the current filters.") });
|
|
@@ -33,7 +35,8 @@ function ActivityTimeline({ activities, onEdit }) {
|
|
|
33
35
|
activity,
|
|
34
36
|
t,
|
|
35
37
|
withBorder: index < activities.length - 1,
|
|
36
|
-
onEdit
|
|
38
|
+
onEdit,
|
|
39
|
+
onMarkDone
|
|
37
40
|
}
|
|
38
41
|
)
|
|
39
42
|
] }, activity.id);
|
|
@@ -43,12 +46,25 @@ function TimelineEntry({
|
|
|
43
46
|
activity,
|
|
44
47
|
t,
|
|
45
48
|
withBorder,
|
|
46
|
-
onEdit
|
|
49
|
+
onEdit,
|
|
50
|
+
onMarkDone
|
|
47
51
|
}) {
|
|
48
52
|
const dateStr = activity.scheduledAt ?? activity.occurredAt ?? activity.createdAt;
|
|
49
53
|
const TypeIcon = TYPE_ICONS[activity.interactionType];
|
|
50
54
|
const title = activity.title ?? activity.body ?? activity.interactionType;
|
|
51
55
|
const duration = activity.duration ? ` (${activity.duration} min)` : "";
|
|
56
|
+
const isPlanned = activity.status === "planned";
|
|
57
|
+
const [markingDone, setMarkingDone] = React.useState(false);
|
|
58
|
+
const handleMarkDone = React.useCallback(async (event) => {
|
|
59
|
+
event.stopPropagation();
|
|
60
|
+
if (!onMarkDone || markingDone) return;
|
|
61
|
+
setMarkingDone(true);
|
|
62
|
+
try {
|
|
63
|
+
await onMarkDone(activity.id);
|
|
64
|
+
} finally {
|
|
65
|
+
setMarkingDone(false);
|
|
66
|
+
}
|
|
67
|
+
}, [activity.id, markingDone, onMarkDone]);
|
|
52
68
|
return /* @__PURE__ */ jsx(
|
|
53
69
|
"div",
|
|
54
70
|
{
|
|
@@ -66,9 +82,26 @@ function TimelineEntry({
|
|
|
66
82
|
] }),
|
|
67
83
|
/* @__PURE__ */ jsx("div", { className: "flex size-8 items-center justify-center rounded-md bg-muted shrink-0", children: TypeIcon ? /* @__PURE__ */ jsx(TypeIcon, { className: "size-3.5 text-muted-foreground" }) : null }),
|
|
68
84
|
/* @__PURE__ */ jsxs("div", { className: "min-w-0 space-y-1.5", children: [
|
|
69
|
-
/* @__PURE__ */ jsxs("
|
|
70
|
-
|
|
71
|
-
|
|
85
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-2", children: [
|
|
86
|
+
/* @__PURE__ */ jsxs("span", { className: "block text-[12px] font-semibold leading-tight text-foreground", children: [
|
|
87
|
+
title,
|
|
88
|
+
duration
|
|
89
|
+
] }),
|
|
90
|
+
isPlanned && onMarkDone ? /* @__PURE__ */ jsxs(
|
|
91
|
+
Button,
|
|
92
|
+
{
|
|
93
|
+
type: "button",
|
|
94
|
+
variant: "default",
|
|
95
|
+
size: "sm",
|
|
96
|
+
disabled: markingDone,
|
|
97
|
+
onClick: handleMarkDone,
|
|
98
|
+
className: "shrink-0",
|
|
99
|
+
children: [
|
|
100
|
+
/* @__PURE__ */ jsx(Check, { className: "size-3.5" }),
|
|
101
|
+
t("customers.activities.actions.markDone", "Mark done")
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
) : null
|
|
72
105
|
] }),
|
|
73
106
|
activity.body && activity.title && /* @__PURE__ */ jsx("p", { className: "text-[11px] leading-snug text-muted-foreground", children: activity.body }),
|
|
74
107
|
activity.authorName && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 text-[10px] text-muted-foreground", children: [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customers/components/detail/ActivityTimeline.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\nimport * as React from 'react'\nimport { Phone, Mail, Users, StickyNote, User } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { AiActionChips } from './AiActionChips'\nimport type { InteractionSummary } from './types'\n\nconst TYPE_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {\n call: Phone,\n email: Mail,\n meeting: Users,\n note: StickyNote,\n}\n\ninterface ActivityTimelineProps {\n activities: InteractionSummary[]\n onEdit?: (activity: InteractionSummary) => void\n}\n\nexport function ActivityTimeline({ activities, onEdit }: ActivityTimelineProps) {\n const t = useT()\n\n if (activities.length === 0) {\n return (\n <div className=\"py-6 text-center text-sm text-muted-foreground\">\n {t('customers.timeline.empty', 'No activities match the current filters.')}\n </div>\n )\n }\n\n return (\n <div>\n {activities.map((activity, index) => {\n const dateStr = activity.scheduledAt ?? activity.occurredAt ?? activity.createdAt\n const activityYear = dateStr ? new Date(dateStr).getFullYear() : null\n const prevDateStr = index > 0 ? (activities[index - 1].scheduledAt ?? activities[index - 1].occurredAt ?? activities[index - 1].createdAt) : null\n const prevYear = prevDateStr ? new Date(prevDateStr).getFullYear() : null\n const showYearSeparator = activityYear !== null && prevYear !== null && activityYear !== prevYear\n\n return (\n <React.Fragment key={activity.id}>\n {showYearSeparator && (\n <div className=\"flex items-center gap-3 py-3 px-5\">\n <div className=\"h-px flex-1 bg-border\" />\n <span className=\"text-xs font-semibold text-muted-foreground\">\n {t('customers.activities.yearSeparator', '{year}', { year: activityYear })}\n </span>\n <div className=\"h-px flex-1 bg-border\" />\n </div>\n )}\n <TimelineEntry\n activity={activity}\n t={t}\n withBorder={index < activities.length - 1}\n onEdit={onEdit}\n />\n </React.Fragment>\n )\n })}\n </div>\n )\n}\n\nfunction TimelineEntry({\n activity,\n t,\n withBorder,\n onEdit,\n}: {\n activity: InteractionSummary\n t: TranslateFn\n withBorder: boolean\n onEdit?: (activity: InteractionSummary) => void\n}) {\n const dateStr = activity.scheduledAt ?? activity.occurredAt ?? activity.createdAt\n const TypeIcon = TYPE_ICONS[activity.interactionType]\n const title = activity.title ?? activity.body ?? activity.interactionType\n const duration = activity.duration ? ` (${activity.duration} min)` : ''\n\n return (\n <div\n className={`py-2.5 ${withBorder ? 'border-b border-border/60' : ''} ${onEdit ? 'cursor-pointer hover:bg-accent/40 transition-colors' : ''}`}\n onClick={() => onEdit?.(activity)}\n role={onEdit ? 'button' : undefined}\n tabIndex={onEdit ? 0 : undefined}\n onKeyDown={onEdit ? (e) => { if (e.key === 'Enter') onEdit(activity) } : undefined}\n >\n <div className=\"grid items-start gap-3\" style={{ gridTemplateColumns: '75px 32px 1fr' }}>\n {/* Column 1: Date */}\n <div className=\"shrink-0\">\n <span className=\"block text-[11px] font-semibold leading-tight text-foreground\">\n {formatRelativeDate(dateStr, t)}\n </span>\n <span className=\"block text-[10px] leading-tight text-muted-foreground\">\n {formatTime(dateStr)}\n </span>\n </div>\n\n {/* Column 2: Type icon */}\n <div className=\"flex size-8 items-center justify-center rounded-md bg-muted shrink-0\">\n {TypeIcon ? <TypeIcon className=\"size-3.5 text-muted-foreground\" /> : null}\n </div>\n\n {/* Column 3: Content */}\n <div className=\"min-w-0 space-y-1.5\">\n <span className=\"block text-[12px] font-semibold leading-tight text-foreground\">\n
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["'use client'\nimport * as React from 'react'\nimport { Check, ListTodo, Phone, Mail, Users, StickyNote, User } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { AiActionChips } from './AiActionChips'\nimport type { InteractionSummary } from './types'\n\nconst TYPE_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {\n call: Phone,\n email: Mail,\n meeting: Users,\n note: StickyNote,\n task: ListTodo,\n}\n\ninterface ActivityTimelineProps {\n activities: InteractionSummary[]\n onEdit?: (activity: InteractionSummary) => void\n onMarkDone?: (activityId: string) => void | Promise<void>\n}\n\nexport function ActivityTimeline({ activities, onEdit, onMarkDone }: ActivityTimelineProps) {\n const t = useT()\n\n if (activities.length === 0) {\n return (\n <div className=\"py-6 text-center text-sm text-muted-foreground\">\n {t('customers.timeline.empty', 'No activities match the current filters.')}\n </div>\n )\n }\n\n return (\n <div>\n {activities.map((activity, index) => {\n const dateStr = activity.scheduledAt ?? activity.occurredAt ?? activity.createdAt\n const activityYear = dateStr ? new Date(dateStr).getFullYear() : null\n const prevDateStr = index > 0 ? (activities[index - 1].scheduledAt ?? activities[index - 1].occurredAt ?? activities[index - 1].createdAt) : null\n const prevYear = prevDateStr ? new Date(prevDateStr).getFullYear() : null\n const showYearSeparator = activityYear !== null && prevYear !== null && activityYear !== prevYear\n\n return (\n <React.Fragment key={activity.id}>\n {showYearSeparator && (\n <div className=\"flex items-center gap-3 py-3 px-5\">\n <div className=\"h-px flex-1 bg-border\" />\n <span className=\"text-xs font-semibold text-muted-foreground\">\n {t('customers.activities.yearSeparator', '{year}', { year: activityYear })}\n </span>\n <div className=\"h-px flex-1 bg-border\" />\n </div>\n )}\n <TimelineEntry\n activity={activity}\n t={t}\n withBorder={index < activities.length - 1}\n onEdit={onEdit}\n onMarkDone={onMarkDone}\n />\n </React.Fragment>\n )\n })}\n </div>\n )\n}\n\nfunction TimelineEntry({\n activity,\n t,\n withBorder,\n onEdit,\n onMarkDone,\n}: {\n activity: InteractionSummary\n t: TranslateFn\n withBorder: boolean\n onEdit?: (activity: InteractionSummary) => void\n onMarkDone?: (activityId: string) => void | Promise<void>\n}) {\n const dateStr = activity.scheduledAt ?? activity.occurredAt ?? activity.createdAt\n const TypeIcon = TYPE_ICONS[activity.interactionType]\n const title = activity.title ?? activity.body ?? activity.interactionType\n const duration = activity.duration ? ` (${activity.duration} min)` : ''\n const isPlanned = activity.status === 'planned'\n const [markingDone, setMarkingDone] = React.useState(false)\n\n const handleMarkDone = React.useCallback(async (event: React.MouseEvent | React.KeyboardEvent) => {\n event.stopPropagation()\n if (!onMarkDone || markingDone) return\n setMarkingDone(true)\n try {\n await onMarkDone(activity.id)\n } finally {\n setMarkingDone(false)\n }\n }, [activity.id, markingDone, onMarkDone])\n\n return (\n <div\n className={`py-2.5 ${withBorder ? 'border-b border-border/60' : ''} ${onEdit ? 'cursor-pointer hover:bg-accent/40 transition-colors' : ''}`}\n onClick={() => onEdit?.(activity)}\n role={onEdit ? 'button' : undefined}\n tabIndex={onEdit ? 0 : undefined}\n onKeyDown={onEdit ? (e) => { if (e.key === 'Enter') onEdit(activity) } : undefined}\n >\n <div className=\"grid items-start gap-3\" style={{ gridTemplateColumns: '75px 32px 1fr' }}>\n {/* Column 1: Date */}\n <div className=\"shrink-0\">\n <span className=\"block text-[11px] font-semibold leading-tight text-foreground\">\n {formatRelativeDate(dateStr, t)}\n </span>\n <span className=\"block text-[10px] leading-tight text-muted-foreground\">\n {formatTime(dateStr)}\n </span>\n </div>\n\n {/* Column 2: Type icon */}\n <div className=\"flex size-8 items-center justify-center rounded-md bg-muted shrink-0\">\n {TypeIcon ? <TypeIcon className=\"size-3.5 text-muted-foreground\" /> : null}\n </div>\n\n {/* Column 3: Content */}\n <div className=\"min-w-0 space-y-1.5\">\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"block text-[12px] font-semibold leading-tight text-foreground\">\n {title}{duration}\n </span>\n {isPlanned && onMarkDone ? (\n <Button\n type=\"button\"\n variant=\"default\"\n size=\"sm\"\n disabled={markingDone}\n onClick={handleMarkDone}\n className=\"shrink-0\"\n >\n <Check className=\"size-3.5\" />\n {t('customers.activities.actions.markDone', 'Mark done')}\n </Button>\n ) : null}\n </div>\n\n {activity.body && activity.title && (\n <p className=\"text-[11px] leading-snug text-muted-foreground\">\n {activity.body}\n </p>\n )}\n\n {activity.authorName && (\n <div className=\"flex items-center gap-1 text-[10px] text-muted-foreground\">\n <User className=\"size-2.5 shrink-0\" />\n <span>{t('customers.timeline.author', 'by {{name}}', { name: activity.authorName })}</span>\n </div>\n )}\n\n <AiActionChips activityType={activity.interactionType} />\n </div>\n </div>\n </div>\n )\n}\n\nfunction formatRelativeDate(isoString: string, t: TranslateFn): string {\n try {\n const date = new Date(isoString)\n const now = new Date()\n // Compare calendar dates (not time-based diff) to correctly handle same-day future times\n const dateDay = new Date(date.getFullYear(), date.getMonth(), date.getDate())\n const nowDay = new Date(now.getFullYear(), now.getMonth(), now.getDate())\n const diffDays = Math.round((nowDay.getTime() - dateDay.getTime()) / (1000 * 60 * 60 * 24))\n\n if (diffDays === 0) return t('customers.timeline.date.today', 'today')\n if (diffDays === 1) return t('customers.timeline.date.yesterday', 'yesterday')\n return date.toLocaleDateString(undefined, { day: 'numeric', month: 'short' })\n } catch {\n return ''\n }\n}\n\nfunction formatTime(isoString: string): string {\n try {\n return new Date(isoString).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })\n } catch {\n return ''\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import { Phone, Mail, Users, StickyNote, SlidersHorizontal } from "lucide-react";
|
|
4
|
+
import { Phone, Mail, Users, StickyNote, ListTodo, SlidersHorizontal } from "lucide-react";
|
|
5
5
|
import { cn } from "@open-mercato/shared/lib/utils";
|
|
6
6
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
7
7
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
@@ -12,7 +12,8 @@ const FILTER_TYPES = [
|
|
|
12
12
|
{ type: "note", icon: StickyNote },
|
|
13
13
|
{ type: "call", icon: Phone },
|
|
14
14
|
{ type: "meeting", icon: Users },
|
|
15
|
-
{ type: "email", icon: Mail }
|
|
15
|
+
{ type: "email", icon: Mail },
|
|
16
|
+
{ type: "task", icon: ListTodo }
|
|
16
17
|
];
|
|
17
18
|
const CHIP_BASE = "inline-flex h-7 items-center gap-1.5 rounded-lg px-2.5 text-sm font-medium transition-colors";
|
|
18
19
|
const CHIP_INACTIVE = "border border-border bg-card text-muted-foreground hover:bg-accent/40";
|
|
@@ -36,11 +37,19 @@ function ActivityTimelineFilters({
|
|
|
36
37
|
const controller = new AbortController();
|
|
37
38
|
void (async () => {
|
|
38
39
|
try {
|
|
39
|
-
const
|
|
40
|
+
const payload = await readApiResultOrThrow(
|
|
40
41
|
`/api/customers/interactions/counts?entityId=${encodeURIComponent(entityId)}`,
|
|
41
42
|
{ signal: controller.signal }
|
|
42
43
|
);
|
|
43
|
-
|
|
44
|
+
const source = payload.result ?? payload;
|
|
45
|
+
setCounts({
|
|
46
|
+
call: source.call ?? 0,
|
|
47
|
+
email: source.email ?? 0,
|
|
48
|
+
meeting: source.meeting ?? 0,
|
|
49
|
+
note: source.note ?? 0,
|
|
50
|
+
task: source.task ?? 0,
|
|
51
|
+
total: source.total ?? 0
|
|
52
|
+
});
|
|
44
53
|
} catch {
|
|
45
54
|
setCounts(null);
|
|
46
55
|
}
|
|
@@ -60,9 +69,11 @@ function ActivityTimelineFilters({
|
|
|
60
69
|
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 md:flex-row md:items-center md:justify-between", children: [
|
|
61
70
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2.5", children: [
|
|
62
71
|
/* @__PURE__ */ jsx(
|
|
63
|
-
|
|
72
|
+
Button,
|
|
64
73
|
{
|
|
65
74
|
type: "button",
|
|
75
|
+
variant: "ghost",
|
|
76
|
+
size: "sm",
|
|
66
77
|
onClick: handleSelectAll,
|
|
67
78
|
"aria-pressed": allActive,
|
|
68
79
|
className: cn(CHIP_BASE, allActive ? CHIP_ACTIVE : CHIP_INACTIVE),
|
|
@@ -74,9 +85,11 @@ function ActivityTimelineFilters({
|
|
|
74
85
|
const count = counts?.[type];
|
|
75
86
|
const hasCount = typeof count === "number" && count > 0;
|
|
76
87
|
return /* @__PURE__ */ jsxs(
|
|
77
|
-
|
|
88
|
+
Button,
|
|
78
89
|
{
|
|
79
90
|
type: "button",
|
|
91
|
+
variant: "ghost",
|
|
92
|
+
size: "sm",
|
|
80
93
|
onClick: () => handleTypeToggle(type),
|
|
81
94
|
"aria-pressed": isActive,
|
|
82
95
|
className: cn(CHIP_BASE, isActive ? CHIP_ACTIVE : CHIP_INACTIVE),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customers/components/detail/ActivityTimelineFilters.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\nimport * as React from 'react'\nimport { Phone, Mail, Users, StickyNote, SlidersHorizontal } 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 { Popover, PopoverContent, PopoverTrigger } from '@open-mercato/ui/primitives/popover'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\n\nconst FILTER_TYPES = [\n { type: 'note', icon: StickyNote },\n { type: 'call', icon: Phone },\n { type: 'meeting', icon: Users },\n { type: 'email', icon: Mail },\n] as const\n\ntype InteractionCounts = {\n call: number\n email: number\n meeting: number\n note: number\n total: number\n}\n\ninterface ActivityTimelineFiltersProps {\n entityId: string | null\n activeTypes: string[]\n dateFrom: string\n dateTo: string\n onTypesChange: (types: string[]) => void\n onDateFromChange: (value: string) => void\n onDateToChange: (value: string) => void\n onReset: () => void\n}\n\nconst CHIP_BASE = 'inline-flex h-7 items-center gap-1.5 rounded-lg px-2.5 text-sm font-medium transition-colors'\nconst CHIP_INACTIVE = 'border border-border bg-card text-muted-foreground hover:bg-accent/40'\nconst CHIP_ACTIVE = 'border border-status-info-border bg-status-info-bg text-status-info-text'\n\nexport function ActivityTimelineFilters({\n entityId,\n activeTypes,\n dateFrom,\n dateTo,\n onTypesChange,\n onDateFromChange,\n onDateToChange,\n onReset,\n}: ActivityTimelineFiltersProps) {\n const t = useT()\n const hasActiveFilters = activeTypes.length > 0 || dateFrom || dateTo\n const allActive = activeTypes.length === 0\n const [counts, setCounts] = React.useState<InteractionCounts | null>(null)\n\n React.useEffect(() => {\n if (!entityId) return\n const controller = new AbortController()\n void (async () => {\n try {\n const
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["'use client'\nimport * as React from 'react'\nimport { Phone, Mail, Users, StickyNote, ListTodo, SlidersHorizontal } 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 { Popover, PopoverContent, PopoverTrigger } from '@open-mercato/ui/primitives/popover'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\n\nconst FILTER_TYPES = [\n { type: 'note', icon: StickyNote },\n { type: 'call', icon: Phone },\n { type: 'meeting', icon: Users },\n { type: 'email', icon: Mail },\n { type: 'task', icon: ListTodo },\n] as const\n\ntype InteractionCounts = {\n call: number\n email: number\n meeting: number\n note: number\n task: number\n total: number\n}\n\ntype InteractionCountsResponse = {\n ok?: boolean\n result?: InteractionCounts\n} & Partial<InteractionCounts>\n\ninterface ActivityTimelineFiltersProps {\n entityId: string | null\n activeTypes: string[]\n dateFrom: string\n dateTo: string\n onTypesChange: (types: string[]) => void\n onDateFromChange: (value: string) => void\n onDateToChange: (value: string) => void\n onReset: () => void\n}\n\nconst CHIP_BASE = 'inline-flex h-7 items-center gap-1.5 rounded-lg px-2.5 text-sm font-medium transition-colors'\nconst CHIP_INACTIVE = 'border border-border bg-card text-muted-foreground hover:bg-accent/40'\nconst CHIP_ACTIVE = 'border border-status-info-border bg-status-info-bg text-status-info-text'\n\nexport function ActivityTimelineFilters({\n entityId,\n activeTypes,\n dateFrom,\n dateTo,\n onTypesChange,\n onDateFromChange,\n onDateToChange,\n onReset,\n}: ActivityTimelineFiltersProps) {\n const t = useT()\n const hasActiveFilters = activeTypes.length > 0 || dateFrom || dateTo\n const allActive = activeTypes.length === 0\n const [counts, setCounts] = React.useState<InteractionCounts | null>(null)\n\n React.useEffect(() => {\n if (!entityId) return\n const controller = new AbortController()\n void (async () => {\n try {\n const payload = await readApiResultOrThrow<InteractionCountsResponse>(\n `/api/customers/interactions/counts?entityId=${encodeURIComponent(entityId)}`,\n { signal: controller.signal },\n )\n // Endpoint envelope is `{ ok, result: {...counts} }`. Some legacy fixtures\n // return the counts at the top level \u2014 fall back to that shape so the chip\n // badges keep working in either case.\n const source = (payload.result ?? payload) as Partial<InteractionCounts>\n setCounts({\n call: source.call ?? 0,\n email: source.email ?? 0,\n meeting: source.meeting ?? 0,\n note: source.note ?? 0,\n task: source.task ?? 0,\n total: source.total ?? 0,\n })\n } catch {\n setCounts(null)\n }\n })()\n return () => controller.abort()\n }, [entityId])\n\n const handleTypeToggle = React.useCallback((type: string) => {\n if (activeTypes.includes(type)) {\n onTypesChange(activeTypes.filter((filterType) => filterType !== type))\n } else {\n onTypesChange([...activeTypes, type])\n }\n }, [activeTypes, onTypesChange])\n\n const handleSelectAll = React.useCallback(() => {\n onTypesChange([])\n }, [onTypesChange])\n\n return (\n <div className=\"flex flex-col gap-3 md:flex-row md:items-center md:justify-between\">\n <div className=\"flex flex-wrap items-center gap-2.5\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={handleSelectAll}\n aria-pressed={allActive}\n className={cn(CHIP_BASE, allActive ? CHIP_ACTIVE : CHIP_INACTIVE)}\n >\n <span>{t('customers.timeline.filter.all', 'All Activities')}</span>\n </Button>\n\n {FILTER_TYPES.map(({ type, icon: Icon }) => {\n const isActive = activeTypes.includes(type)\n const count = counts?.[type as keyof InteractionCounts]\n const hasCount = typeof count === 'number' && count > 0\n return (\n <Button\n key={type}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => handleTypeToggle(type)}\n aria-pressed={isActive}\n className={cn(CHIP_BASE, isActive ? CHIP_ACTIVE : CHIP_INACTIVE)}\n >\n <Icon className=\"size-[18px] shrink-0\" />\n <span>\n {t(`customers.timeline.filter.${type}`, type)}\n {hasCount ? ` ${count}` : ''}\n </span>\n </Button>\n )\n })}\n </div>\n\n <Popover>\n <PopoverTrigger asChild>\n <IconButton\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"size-7 rounded-md text-muted-foreground\"\n aria-label={t('customers.people.detail.activities.moreFilters', 'More filters')}\n >\n <SlidersHorizontal className=\"size-3.5\" />\n </IconButton>\n </PopoverTrigger>\n <PopoverContent align=\"end\" className=\"w-72 space-y-3\">\n <div className=\"space-y-1.5\">\n <label className=\"text-xs font-medium\">\n {t('customers.activities.filters.dateRange', 'Date range')}\n </label>\n <div className=\"flex items-center gap-2\">\n <input\n type=\"date\"\n value={dateFrom}\n onChange={(event) => onDateFromChange(event.target.value)}\n className=\"h-8 w-full rounded-md border bg-background px-2 text-xs focus:outline-none focus:ring-1 focus:ring-ring\"\n aria-label={t('customers.timeline.filter.from', 'From date')}\n />\n <span className=\"shrink-0 text-xs text-muted-foreground\">\u2014</span>\n <input\n type=\"date\"\n value={dateTo}\n onChange={(event) => onDateToChange(event.target.value)}\n className=\"h-8 w-full rounded-md border bg-background px-2 text-xs focus:outline-none focus:ring-1 focus:ring-ring\"\n aria-label={t('customers.timeline.filter.to', 'To date')}\n />\n </div>\n </div>\n\n {hasActiveFilters ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={onReset}\n className=\"h-7 w-full text-xs\"\n >\n {t('customers.activities.filters.clearAll', 'Clear filters')}\n </Button>\n ) : null}\n </PopoverContent>\n </Popover>\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": ";AAiHU,cAkBI,YAlBJ;AAhHV,YAAY,WAAW;AACvB,SAAS,OAAO,MAAM,OAAO,YAAY,UAAU,yBAAyB;AAC5E,SAAS,UAAU;AACnB,SAAS,YAAY;AACrB,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAC3B,SAAS,SAAS,gBAAgB,sBAAsB;AACxD,SAAS,4BAA4B;AAErC,MAAM,eAAe;AAAA,EACnB,EAAE,MAAM,QAAQ,MAAM,WAAW;AAAA,EACjC,EAAE,MAAM,QAAQ,MAAM,MAAM;AAAA,EAC5B,EAAE,MAAM,WAAW,MAAM,MAAM;AAAA,EAC/B,EAAE,MAAM,SAAS,MAAM,KAAK;AAAA,EAC5B,EAAE,MAAM,QAAQ,MAAM,SAAS;AACjC;AA2BA,MAAM,YAAY;AAClB,MAAM,gBAAgB;AACtB,MAAM,cAAc;AAEb,SAAS,wBAAwB;AAAA,EACtC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAiC;AAC/B,QAAM,IAAI,KAAK;AACf,QAAM,mBAAmB,YAAY,SAAS,KAAK,YAAY;AAC/D,QAAM,YAAY,YAAY,WAAW;AACzC,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAAmC,IAAI;AAEzE,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,SAAU;AACf,UAAM,aAAa,IAAI,gBAAgB;AACvC,UAAM,YAAY;AAChB,UAAI;AACF,cAAM,UAAU,MAAM;AAAA,UACpB,+CAA+C,mBAAmB,QAAQ,CAAC;AAAA,UAC3E,EAAE,QAAQ,WAAW,OAAO;AAAA,QAC9B;AAIA,cAAM,SAAU,QAAQ,UAAU;AAClC,kBAAU;AAAA,UACR,MAAM,OAAO,QAAQ;AAAA,UACrB,OAAO,OAAO,SAAS;AAAA,UACvB,SAAS,OAAO,WAAW;AAAA,UAC3B,MAAM,OAAO,QAAQ;AAAA,UACrB,MAAM,OAAO,QAAQ;AAAA,UACrB,OAAO,OAAO,SAAS;AAAA,QACzB,CAAC;AAAA,MACH,QAAQ;AACN,kBAAU,IAAI;AAAA,MAChB;AAAA,IACF,GAAG;AACH,WAAO,MAAM,WAAW,MAAM;AAAA,EAChC,GAAG,CAAC,QAAQ,CAAC;AAEb,QAAM,mBAAmB,MAAM,YAAY,CAAC,SAAiB;AAC3D,QAAI,YAAY,SAAS,IAAI,GAAG;AAC9B,oBAAc,YAAY,OAAO,CAAC,eAAe,eAAe,IAAI,CAAC;AAAA,IACvE,OAAO;AACL,oBAAc,CAAC,GAAG,aAAa,IAAI,CAAC;AAAA,IACtC;AAAA,EACF,GAAG,CAAC,aAAa,aAAa,CAAC;AAE/B,QAAM,kBAAkB,MAAM,YAAY,MAAM;AAC9C,kBAAc,CAAC,CAAC;AAAA,EAClB,GAAG,CAAC,aAAa,CAAC;AAElB,SACE,qBAAC,SAAI,WAAU,sEACb;AAAA,yBAAC,SAAI,WAAU,uCACb;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAQ;AAAA,UACR,MAAK;AAAA,UACL,SAAS;AAAA,UACT,gBAAc;AAAA,UACd,WAAW,GAAG,WAAW,YAAY,cAAc,aAAa;AAAA,UAEhE,8BAAC,UAAM,YAAE,iCAAiC,gBAAgB,GAAE;AAAA;AAAA,MAC9D;AAAA,MAEC,aAAa,IAAI,CAAC,EAAE,MAAM,MAAM,KAAK,MAAM;AAC1C,cAAM,WAAW,YAAY,SAAS,IAAI;AAC1C,cAAM,QAAQ,SAAS,IAA+B;AACtD,cAAM,WAAW,OAAO,UAAU,YAAY,QAAQ;AACtD,eACE;AAAA,UAAC;AAAA;AAAA,YAEC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,MAAK;AAAA,YACL,SAAS,MAAM,iBAAiB,IAAI;AAAA,YACpC,gBAAc;AAAA,YACd,WAAW,GAAG,WAAW,WAAW,cAAc,aAAa;AAAA,YAE/D;AAAA,kCAAC,QAAK,WAAU,wBAAuB;AAAA,cACvC,qBAAC,UACE;AAAA,kBAAE,6BAA6B,IAAI,IAAI,IAAI;AAAA,gBAC3C,WAAW,IAAI,KAAK,KAAK;AAAA,iBAC5B;AAAA;AAAA;AAAA,UAZK;AAAA,QAaP;AAAA,MAEJ,CAAC;AAAA,OACH;AAAA,IAEA,qBAAC,WACC;AAAA,0BAAC,kBAAe,SAAO,MACrB;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAQ;AAAA,UACR,MAAK;AAAA,UACL,WAAU;AAAA,UACV,cAAY,EAAE,kDAAkD,cAAc;AAAA,UAE9E,8BAAC,qBAAkB,WAAU,YAAW;AAAA;AAAA,MAC1C,GACF;AAAA,MACA,qBAAC,kBAAe,OAAM,OAAM,WAAU,kBACpC;AAAA,6BAAC,SAAI,WAAU,eACb;AAAA,8BAAC,WAAM,WAAU,uBACd,YAAE,0CAA0C,YAAY,GAC3D;AAAA,UACA,qBAAC,SAAI,WAAU,2BACb;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,OAAO;AAAA,gBACP,UAAU,CAAC,UAAU,iBAAiB,MAAM,OAAO,KAAK;AAAA,gBACxD,WAAU;AAAA,gBACV,cAAY,EAAE,kCAAkC,WAAW;AAAA;AAAA,YAC7D;AAAA,YACA,oBAAC,UAAK,WAAU,0CAAyC,oBAAC;AAAA,YAC1D;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,OAAO;AAAA,gBACP,UAAU,CAAC,UAAU,eAAe,MAAM,OAAO,KAAK;AAAA,gBACtD,WAAU;AAAA,gBACV,cAAY,EAAE,gCAAgC,SAAS;AAAA;AAAA,YACzD;AAAA,aACF;AAAA,WACF;AAAA,QAEC,mBACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,MAAK;AAAA,YACL,SAAS;AAAA,YACT,WAAU;AAAA,YAET,YAAE,yCAAyC,eAAe;AAAA;AAAA,QAC7D,IACE;AAAA,SACN;AAAA,OACF;AAAA,KACF;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
3
|
+
import { ListTodo, Mail, Phone, StickyNote, Users } from "lucide-react";
|
|
4
4
|
import { cn } from "@open-mercato/shared/lib/utils";
|
|
5
5
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
6
6
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
@@ -8,11 +8,12 @@ const ACTIVITY_TYPES = [
|
|
|
8
8
|
{ type: "call", icon: Phone, labelKey: "customers.activityComposer.types.call", fallback: "Call" },
|
|
9
9
|
{ type: "email", icon: Mail, labelKey: "customers.activityComposer.types.email", fallback: "Email" },
|
|
10
10
|
{ type: "meeting", icon: Users, labelKey: "customers.activityComposer.types.meeting", fallback: "Meeting" },
|
|
11
|
-
{ type: "note", icon: StickyNote, labelKey: "customers.activityComposer.types.note", fallback: "Note" }
|
|
11
|
+
{ type: "note", icon: StickyNote, labelKey: "customers.activityComposer.types.note", fallback: "Note" },
|
|
12
|
+
{ type: "task", icon: ListTodo, labelKey: "customers.activityComposer.types.task", fallback: "Task" }
|
|
12
13
|
];
|
|
13
14
|
function ActivityTypeSelector({ selectedType, onSelect }) {
|
|
14
15
|
const t = useT();
|
|
15
|
-
return /* @__PURE__ */ jsx("div", { className: "grid grid-cols-
|
|
16
|
+
return /* @__PURE__ */ jsx("div", { className: "grid grid-cols-5 gap-2", children: ACTIVITY_TYPES.map(({ type, icon: Icon, labelKey, fallback }) => {
|
|
16
17
|
const isSelected = selectedType === type;
|
|
17
18
|
return /* @__PURE__ */ jsxs(
|
|
18
19
|
Button,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customers/components/detail/ActivityTypeSelector.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\nimport * as React from 'react'\nimport {
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["'use client'\nimport * as React from 'react'\nimport { ListTodo, Mail, Phone, StickyNote, Users } 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'\n\nconst ACTIVITY_TYPES = [\n { type: 'call', icon: Phone, labelKey: 'customers.activityComposer.types.call', fallback: 'Call' },\n { type: 'email', icon: Mail, labelKey: 'customers.activityComposer.types.email', fallback: 'Email' },\n { type: 'meeting', icon: Users, labelKey: 'customers.activityComposer.types.meeting', fallback: 'Meeting' },\n { type: 'note', icon: StickyNote, labelKey: 'customers.activityComposer.types.note', fallback: 'Note' },\n { type: 'task', icon: ListTodo, labelKey: 'customers.activityComposer.types.task', fallback: 'Task' },\n] as const\n\nexport type ActivityType = (typeof ACTIVITY_TYPES)[number]['type']\n\ninterface ActivityTypeSelectorProps {\n selectedType: ActivityType | null\n onSelect: (type: ActivityType) => void\n}\n\nexport function ActivityTypeSelector({ selectedType, onSelect }: ActivityTypeSelectorProps) {\n const t = useT()\n\n return (\n <div className=\"grid grid-cols-5 gap-2\">\n {ACTIVITY_TYPES.map(({ type, icon: Icon, labelKey, fallback }) => {\n const isSelected = selectedType === type\n return (\n <Button\n key={type}\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => onSelect(type)}\n aria-pressed={isSelected}\n className={cn(\n 'h-10 gap-2 rounded-lg',\n isSelected\n ? 'border-foreground bg-background text-foreground shadow-sm'\n : 'border-border text-muted-foreground',\n )}\n >\n <Icon className=\"size-4\" />\n {t(labelKey, fallback)}\n </Button>\n )\n })}\n </div>\n )\n}\n\nexport { ACTIVITY_TYPES }\n"],
|
|
5
|
+
"mappings": ";AA8BU,SAcE,KAdF;AA5BV,SAAS,UAAU,MAAM,OAAO,YAAY,aAAa;AACzD,SAAS,UAAU;AACnB,SAAS,YAAY;AACrB,SAAS,cAAc;AAEvB,MAAM,iBAAiB;AAAA,EACrB,EAAE,MAAM,QAAQ,MAAM,OAAO,UAAU,yCAAyC,UAAU,OAAO;AAAA,EACjG,EAAE,MAAM,SAAS,MAAM,MAAM,UAAU,0CAA0C,UAAU,QAAQ;AAAA,EACnG,EAAE,MAAM,WAAW,MAAM,OAAO,UAAU,4CAA4C,UAAU,UAAU;AAAA,EAC1G,EAAE,MAAM,QAAQ,MAAM,YAAY,UAAU,yCAAyC,UAAU,OAAO;AAAA,EACtG,EAAE,MAAM,QAAQ,MAAM,UAAU,UAAU,yCAAyC,UAAU,OAAO;AACtG;AASO,SAAS,qBAAqB,EAAE,cAAc,SAAS,GAA8B;AAC1F,QAAM,IAAI,KAAK;AAEf,SACE,oBAAC,SAAI,WAAU,0BACZ,yBAAe,IAAI,CAAC,EAAE,MAAM,MAAM,MAAM,UAAU,SAAS,MAAM;AAChE,UAAM,aAAa,iBAAiB;AACpC,WACE;AAAA,MAAC;AAAA;AAAA,QAEC,MAAK;AAAA,QACL,SAAQ;AAAA,QACR,MAAK;AAAA,QACL,SAAS,MAAM,SAAS,IAAI;AAAA,QAC5B,gBAAc;AAAA,QACd,WAAW;AAAA,UACT;AAAA,UACA,aACI,8DACA;AAAA,QACN;AAAA,QAEA;AAAA,8BAAC,QAAK,WAAU,UAAS;AAAA,UACxB,EAAE,UAAU,QAAQ;AAAA;AAAA;AAAA,MAdhB;AAAA,IAeP;AAAA,EAEJ,CAAC,GACH;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -4,7 +4,9 @@ import * as React from "react";
|
|
|
4
4
|
import { Users, Phone, Check, Mail, Calendar, AlertTriangle, X } from "lucide-react";
|
|
5
5
|
import { cn } from "@open-mercato/shared/lib/utils";
|
|
6
6
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
7
|
+
import { validatePhoneNumber } from "@open-mercato/shared/lib/phone";
|
|
7
8
|
import { apiCallOrThrow, readApiResultOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
|
|
9
|
+
import { mapCrudServerErrorToFormErrors } from "@open-mercato/ui/backend/utils/serverErrors";
|
|
8
10
|
import { flash } from "@open-mercato/ui/backend/FlashMessages";
|
|
9
11
|
import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuardedMutation";
|
|
10
12
|
import { Alert, AlertDescription, AlertTitle } from "@open-mercato/ui/primitives/alert";
|
|
@@ -12,7 +14,7 @@ import { Button } from "@open-mercato/ui/primitives/button";
|
|
|
12
14
|
import { IconButton } from "@open-mercato/ui/primitives/icon-button";
|
|
13
15
|
import { Dialog, DialogContent, DialogTitle } from "@open-mercato/ui/primitives/dialog";
|
|
14
16
|
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
|
|
15
|
-
import { SwitchableMarkdownInput } from "@open-mercato/ui/backend/inputs";
|
|
17
|
+
import { PhoneNumberField, SwitchableMarkdownInput } from "@open-mercato/ui/backend/inputs";
|
|
16
18
|
import { useConfirmDialog } from "@open-mercato/ui/backend/confirm-dialog";
|
|
17
19
|
import {
|
|
18
20
|
useScheduleFormState,
|
|
@@ -106,14 +108,31 @@ function ScheduleActivityDialog({
|
|
|
106
108
|
const [callDirection, setCallDirection] = React.useState("outbound");
|
|
107
109
|
const [callOutcome, setCallOutcome] = React.useState(null);
|
|
108
110
|
const [callPhoneNumber, setCallPhoneNumber] = React.useState("");
|
|
111
|
+
const [callPhoneError, setCallPhoneError] = React.useState(null);
|
|
109
112
|
const [taskPriority, setTaskPriority] = React.useState("medium");
|
|
113
|
+
const callPhoneInvalidMessage = React.useMemo(
|
|
114
|
+
() => t(
|
|
115
|
+
"customers.activities.errors.phoneInvalid",
|
|
116
|
+
"Enter a valid phone number with country code (e.g. +1 212 555 1234)"
|
|
117
|
+
),
|
|
118
|
+
[t]
|
|
119
|
+
);
|
|
120
|
+
const translateErrorMessage = React.useCallback(
|
|
121
|
+
(message, fallback) => {
|
|
122
|
+
const key = typeof message === "string" ? message.trim() : "";
|
|
123
|
+
return key ? t(key, key) : fallback;
|
|
124
|
+
},
|
|
125
|
+
[t]
|
|
126
|
+
);
|
|
110
127
|
React.useEffect(() => {
|
|
111
128
|
if (!open) return;
|
|
112
129
|
const raw = editData;
|
|
113
130
|
const cv = raw?.customValues && typeof raw.customValues === "object" ? raw.customValues : null;
|
|
114
131
|
setCallDirection(typeof cv?.callDirection === "string" && cv.callDirection === "inbound" ? "inbound" : "outbound");
|
|
115
132
|
setCallOutcome(typeof cv?.callOutcome === "string" ? cv.callOutcome : null);
|
|
116
|
-
|
|
133
|
+
const seededPhone = typeof raw?.phoneNumber === "string" && raw.phoneNumber.trim().length > 0 ? raw.phoneNumber : typeof cv?.callPhoneNumber === "string" ? cv.callPhoneNumber : "";
|
|
134
|
+
setCallPhoneNumber(seededPhone);
|
|
135
|
+
setCallPhoneError(null);
|
|
117
136
|
setTaskPriority(typeof cv?.taskPriority === "string" ? cv.taskPriority : "medium");
|
|
118
137
|
}, [open, editData]);
|
|
119
138
|
React.useEffect(() => {
|
|
@@ -121,8 +140,13 @@ function ScheduleActivityDialog({
|
|
|
121
140
|
setCallDirection("outbound");
|
|
122
141
|
setCallOutcome(null);
|
|
123
142
|
setCallPhoneNumber("");
|
|
143
|
+
setCallPhoneError(null);
|
|
124
144
|
setTaskPriority("medium");
|
|
125
145
|
}, [state.activityType, open, isEditing]);
|
|
146
|
+
const handleCallPhoneChange = React.useCallback((next) => {
|
|
147
|
+
setCallPhoneNumber(next ?? "");
|
|
148
|
+
setCallPhoneError(null);
|
|
149
|
+
}, []);
|
|
126
150
|
const formSnapshot = React.useMemo(() => JSON.stringify({
|
|
127
151
|
activityType: state.activityType,
|
|
128
152
|
title: state.title,
|
|
@@ -264,8 +288,36 @@ function ScheduleActivityDialog({
|
|
|
264
288
|
}, 500);
|
|
265
289
|
return () => clearTimeout(timer);
|
|
266
290
|
}, [editData?.id, open, state.date, state.startTime, state.duration, state.allDay, t]);
|
|
291
|
+
const trimmedDate = state.date.trim();
|
|
292
|
+
const trimmedStartTime = state.startTime.trim();
|
|
293
|
+
const trimmedCallPhone = callPhoneNumber.trim();
|
|
294
|
+
const isDateMissing = !trimmedDate;
|
|
295
|
+
const isTimeMissing = !state.allDay && !trimmedStartTime;
|
|
296
|
+
const isSubmitDisabled = state.saving || !state.title.trim() || isDateMissing || isTimeMissing;
|
|
267
297
|
const handleSave = React.useCallback(async () => {
|
|
268
298
|
if (!state.title.trim()) return;
|
|
299
|
+
if (isDateMissing) {
|
|
300
|
+
flash(t("customers.activities.errors.dateRequired", "Date is required"), "error");
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (isTimeMissing) {
|
|
304
|
+
flash(t("customers.activities.errors.timeRequired", "Time is required"), "error");
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
let phoneNumberForPayload;
|
|
308
|
+
if (state.activityType === "call" && trimmedCallPhone) {
|
|
309
|
+
const phoneValidation = validatePhoneNumber(trimmedCallPhone);
|
|
310
|
+
if (!phoneValidation.valid) {
|
|
311
|
+
setCallPhoneError(callPhoneInvalidMessage);
|
|
312
|
+
flash(callPhoneInvalidMessage, "error");
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
phoneNumberForPayload = phoneValidation.normalized ?? trimmedCallPhone;
|
|
316
|
+
setCallPhoneError(null);
|
|
317
|
+
if (phoneNumberForPayload !== callPhoneNumber) {
|
|
318
|
+
setCallPhoneNumber(phoneNumberForPayload);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
269
321
|
state.setSaving(true);
|
|
270
322
|
try {
|
|
271
323
|
const scheduledAt = state.allDay ? (/* @__PURE__ */ new Date(`${state.date}T00:00:00`)).toISOString() : (/* @__PURE__ */ new Date(`${state.date}T${state.startTime}:00`)).toISOString();
|
|
@@ -275,7 +327,7 @@ function ScheduleActivityDialog({
|
|
|
275
327
|
if (state.activityType === "call") {
|
|
276
328
|
customValues.callDirection = callDirection;
|
|
277
329
|
if (callOutcome) customValues.callOutcome = callOutcome;
|
|
278
|
-
if (
|
|
330
|
+
if (phoneNumberForPayload) customValues.callPhoneNumber = phoneNumberForPayload;
|
|
279
331
|
}
|
|
280
332
|
if (state.activityType === "task") {
|
|
281
333
|
customValues.taskPriority = taskPriority;
|
|
@@ -288,6 +340,9 @@ function ScheduleActivityDialog({
|
|
|
288
340
|
title: state.title.trim(),
|
|
289
341
|
body: state.description.trim() || null,
|
|
290
342
|
status: "planned",
|
|
343
|
+
date: trimmedDate,
|
|
344
|
+
time: state.allDay ? "00:00" : trimmedStartTime,
|
|
345
|
+
phoneNumber: state.activityType === "call" ? phoneNumberForPayload : void 0,
|
|
291
346
|
scheduledAt,
|
|
292
347
|
durationMinutes: visibleFields.has("duration") && !state.allDay ? state.duration : null,
|
|
293
348
|
location: visibleFields.has("location") ? state.location.trim() || null : null,
|
|
@@ -318,12 +373,23 @@ function ScheduleActivityDialog({
|
|
|
318
373
|
requestAnimationFrame(() => {
|
|
319
374
|
onActivityCreated?.();
|
|
320
375
|
});
|
|
321
|
-
} catch {
|
|
322
|
-
|
|
376
|
+
} catch (err) {
|
|
377
|
+
const { message, fieldErrors } = mapCrudServerErrorToFormErrors(err);
|
|
378
|
+
const phoneFieldError = fieldErrors?.phoneNumber;
|
|
379
|
+
if (state.activityType === "call" && phoneFieldError) {
|
|
380
|
+
const translatedPhoneError = translateErrorMessage(phoneFieldError, callPhoneInvalidMessage);
|
|
381
|
+
setCallPhoneError(translatedPhoneError);
|
|
382
|
+
flash(translatedPhoneError, "error");
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
flash(
|
|
386
|
+
translateErrorMessage(message, t("customers.schedule.error", "Failed to schedule activity")),
|
|
387
|
+
"error"
|
|
388
|
+
);
|
|
323
389
|
} finally {
|
|
324
390
|
state.setSaving(false);
|
|
325
391
|
}
|
|
326
|
-
}, [state.activityType, state.allDay, state.date, state.description, dealId, state.duration, editData, entityId, state.guestPermissions, state.linkedEntities, state.location, onActivityCreated, onClose, state.participants, state.recurrenceCount, state.recurrenceDays, state.recurrenceEnabled, state.recurrenceEndDate, state.recurrenceEndType, state.reminderMinutes, runGuardedMutation, state.startTime, t, state.title, state.visibility, visibleFields]);
|
|
392
|
+
}, [callDirection, callOutcome, callPhoneInvalidMessage, callPhoneNumber, isDateMissing, isTimeMissing, state.activityType, state.allDay, state.date, state.description, dealId, state.duration, editData, entityId, state.guestPermissions, state.linkedEntities, state.location, onActivityCreated, onClose, state.participants, state.recurrenceCount, state.recurrenceDays, state.recurrenceEnabled, state.recurrenceEndDate, state.recurrenceEndType, state.reminderMinutes, runGuardedMutation, state.startTime, t, taskPriority, state.title, translateErrorMessage, trimmedCallPhone, trimmedDate, trimmedStartTime, state.visibility, visibleFields]);
|
|
327
393
|
const handleKeyDown = React.useCallback((e) => {
|
|
328
394
|
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
|
329
395
|
e.preventDefault();
|
|
@@ -498,15 +564,17 @@ function ScheduleActivityDialog({
|
|
|
498
564
|
}
|
|
499
565
|
),
|
|
500
566
|
state.activityType === "call" ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
|
|
501
|
-
/* @__PURE__ */ jsx("label", { className: "text-overline font-semibold uppercase text-muted-foreground tracking-wider", children: t("customers.schedule.call.phoneLabel", "Phone number") }),
|
|
567
|
+
/* @__PURE__ */ jsx("label", { htmlFor: "schedule-call-phone", className: "text-overline font-semibold uppercase text-muted-foreground tracking-wider", children: t("customers.schedule.call.phoneLabel", "Phone number") }),
|
|
502
568
|
/* @__PURE__ */ jsx(
|
|
503
|
-
|
|
569
|
+
PhoneNumberField,
|
|
504
570
|
{
|
|
505
|
-
|
|
571
|
+
id: "schedule-call-phone",
|
|
506
572
|
value: callPhoneNumber,
|
|
507
|
-
|
|
573
|
+
onValueChange: handleCallPhoneChange,
|
|
508
574
|
placeholder: t("customers.schedule.call.phonePlaceholder", "+1 555 000 0000"),
|
|
509
|
-
|
|
575
|
+
externalError: callPhoneError,
|
|
576
|
+
invalidLabel: callPhoneInvalidMessage,
|
|
577
|
+
minDigits: 7
|
|
510
578
|
}
|
|
511
579
|
)
|
|
512
580
|
] }) : /* @__PURE__ */ jsx(
|
|
@@ -556,7 +624,7 @@ function ScheduleActivityDialog({
|
|
|
556
624
|
/* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", onClick: () => {
|
|
557
625
|
void guardedClose();
|
|
558
626
|
}, className: "rounded-md border border-input bg-background px-5 py-3 text-sm font-semibold text-foreground", children: t("customers.schedule.cancel", "Cancel") }),
|
|
559
|
-
/* @__PURE__ */ jsxs(Button, { type: "button", onClick: handleSave, disabled:
|
|
627
|
+
/* @__PURE__ */ jsxs(Button, { type: "button", onClick: handleSave, disabled: isSubmitDisabled, className: "flex items-center gap-2 rounded-md bg-foreground px-5 py-3 text-sm font-semibold text-background hover:bg-foreground/90 disabled:opacity-50", children: [
|
|
560
628
|
/* @__PURE__ */ jsx(SaveIcon, { className: "size-3.5" }),
|
|
561
629
|
state.saving ? t("customers.schedule.saving", "Saving...") : isEditing ? t("customers.schedule.update", "Update activity") : t(chrome.saveKey, chrome.saveFallback)
|
|
562
630
|
] })
|