@open-mercato/core 0.6.6-develop.6515.1.01911d00e2 → 0.6.6-develop.6519.1.bdc91fb755
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/modules/configs/lib/upgrade-actions.js +31 -1
- package/dist/modules/configs/lib/upgrade-actions.js.map +2 -2
- package/dist/modules/customers/ai-tools/activities-tasks-pack.js +11 -2
- package/dist/modules/customers/ai-tools/activities-tasks-pack.js.map +2 -2
- package/dist/modules/customers/api/companies/[id]/route.js +3 -2
- package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/dictionaries/context.js +4 -2
- package/dist/modules/customers/api/dictionaries/context.js.map +2 -2
- package/dist/modules/customers/api/interactions/conflicts/route.js +2 -1
- package/dist/modules/customers/api/interactions/conflicts/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/counts/route.js +7 -2
- package/dist/modules/customers/api/interactions/counts/route.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/route.js +3 -2
- package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
- package/dist/modules/customers/cli.js +19 -0
- package/dist/modules/customers/cli.js.map +2 -2
- package/dist/modules/customers/commands/interactions.js +8 -3
- package/dist/modules/customers/commands/interactions.js.map +2 -2
- package/dist/modules/customers/commands/shared.js +2 -1
- package/dist/modules/customers/commands/shared.js.map +2 -2
- package/dist/modules/customers/components/DictionarySettings.js +5 -0
- package/dist/modules/customers/components/DictionarySettings.js.map +2 -2
- package/dist/modules/customers/components/DictionarySortSettings.js +5 -0
- package/dist/modules/customers/components/DictionarySortSettings.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivitiesCard.js +3 -2
- package/dist/modules/customers/components/detail/ActivitiesCard.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityCard.js +2 -1
- package/dist/modules/customers/components/detail/ActivityCard.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityTimeline.js +3 -2
- package/dist/modules/customers/components/detail/ActivityTimeline.js.map +2 -2
- package/dist/modules/customers/components/detail/TaskDialog.js +3 -2
- package/dist/modules/customers/components/detail/TaskDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/TaskForm.js +170 -69
- package/dist/modules/customers/components/detail/TaskForm.js.map +2 -2
- package/dist/modules/customers/components/detail/TasksSection.js +27 -4
- package/dist/modules/customers/components/detail/TasksSection.js.map +2 -2
- package/dist/modules/customers/components/detail/hooks/usePersonTasks.js.map +2 -2
- package/dist/modules/customers/data/enrichers.js +2 -2
- package/dist/modules/customers/data/enrichers.js.map +2 -2
- package/dist/modules/customers/data/validators.js +6 -2
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/customers/lib/dictionaries.js +2 -1
- package/dist/modules/customers/lib/dictionaries.js.map +2 -2
- package/dist/modules/customers/lib/interactionProjection.js +2 -1
- package/dist/modules/customers/lib/interactionProjection.js.map +2 -2
- package/dist/modules/customers/lib/interactionStatus.js +24 -0
- package/dist/modules/customers/lib/interactionStatus.js.map +7 -0
- package/dist/modules/customers/lib/link-channel-message-handler.js +4 -5
- package/dist/modules/customers/lib/link-channel-message-handler.js.map +2 -2
- package/dist/modules/dictionaries/components/AppearanceSelector.js +7 -3
- package/dist/modules/dictionaries/components/AppearanceSelector.js.map +2 -2
- package/dist/modules/dictionaries/components/DictionaryEntriesEditor.js +1 -0
- package/dist/modules/dictionaries/components/DictionaryEntriesEditor.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/configs/lib/upgrade-actions.ts +31 -1
- package/src/modules/customers/ai-tools/activities-tasks-pack.ts +12 -3
- package/src/modules/customers/api/companies/[id]/route.ts +3 -2
- package/src/modules/customers/api/dictionaries/context.ts +2 -0
- package/src/modules/customers/api/interactions/conflicts/route.ts +2 -1
- package/src/modules/customers/api/interactions/counts/route.ts +7 -2
- package/src/modules/customers/api/people/[id]/route.ts +3 -2
- package/src/modules/customers/cli.ts +19 -0
- package/src/modules/customers/commands/interactions.ts +8 -3
- package/src/modules/customers/commands/shared.ts +1 -0
- package/src/modules/customers/components/DictionarySettings.tsx +5 -0
- package/src/modules/customers/components/DictionarySortSettings.tsx +5 -0
- package/src/modules/customers/components/detail/ActivitiesCard.tsx +3 -2
- package/src/modules/customers/components/detail/ActivityCard.tsx +2 -1
- package/src/modules/customers/components/detail/ActivityTimeline.tsx +3 -2
- package/src/modules/customers/components/detail/TaskDialog.tsx +4 -1
- package/src/modules/customers/components/detail/TaskForm.tsx +218 -71
- package/src/modules/customers/components/detail/TasksSection.tsx +28 -2
- package/src/modules/customers/components/detail/hooks/usePersonTasks.ts +1 -0
- package/src/modules/customers/data/enrichers.ts +2 -7
- package/src/modules/customers/data/validators.ts +16 -2
- package/src/modules/customers/i18n/de.json +10 -0
- package/src/modules/customers/i18n/en.json +10 -0
- package/src/modules/customers/i18n/es.json +10 -0
- package/src/modules/customers/i18n/pl.json +10 -0
- package/src/modules/customers/lib/dictionaries.ts +1 -0
- package/src/modules/customers/lib/interactionProjection.ts +6 -2
- package/src/modules/customers/lib/interactionStatus.ts +30 -0
- package/src/modules/customers/lib/link-channel-message-handler.ts +4 -5
- package/src/modules/dictionaries/components/AppearanceSelector.tsx +10 -1
- package/src/modules/dictionaries/components/DictionaryEntriesEditor.tsx +1 -0
|
@@ -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 { 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, updatedAt?: string | null) => 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, updatedAt?: string | null) => 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
|
|
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'\nimport { isOpenInteractionStatus } from '../../lib/interactionStatus'\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, updatedAt?: string | null) => 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, updatedAt?: string | null) => 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 isOpen = isOpenInteractionStatus(activity.status)\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, activity.updatedAt)\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 {isOpen && 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
|
}
|
|
@@ -4,7 +4,7 @@ import * as React from "react";
|
|
|
4
4
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@open-mercato/ui/primitives/dialog";
|
|
5
5
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
6
6
|
import { TaskForm } from "./TaskForm.js";
|
|
7
|
-
function TaskDialog({ open, mode, onOpenChange, initialValues, onSubmit, isSubmitting, contextMessage }) {
|
|
7
|
+
function TaskDialog({ open, mode, onOpenChange, initialValues, onSubmit, isSubmitting, contextMessage, useCanonicalInteractions = false }) {
|
|
8
8
|
const t = useT();
|
|
9
9
|
const dialogTitle = mode === "edit" ? t("customers.people.detail.tasks.dialog.editTitle", "Edit task") : t("customers.people.detail.tasks.dialog.createTitle", "Add task");
|
|
10
10
|
const handleCancel = React.useCallback(() => {
|
|
@@ -29,7 +29,8 @@ function TaskDialog({ open, mode, onOpenChange, initialValues, onSubmit, isSubmi
|
|
|
29
29
|
initialValues,
|
|
30
30
|
onSubmit: handleSubmit,
|
|
31
31
|
onCancel: handleCancel,
|
|
32
|
-
isSubmitting
|
|
32
|
+
isSubmitting,
|
|
33
|
+
useCanonicalInteractions
|
|
33
34
|
}
|
|
34
35
|
)
|
|
35
36
|
] }) });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customers/components/detail/TaskDialog.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@open-mercato/ui/primitives/dialog'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { TaskFormPayload } from './hooks/usePersonTasks'\nimport { TaskForm } from './TaskForm'\n\nexport type TaskDialogProps = {\n open: boolean\n mode: 'create' | 'edit'\n onOpenChange: (next: boolean) => void\n initialValues?: Record<string, unknown>\n onSubmit: (payload: TaskFormPayload) => Promise<void>\n isSubmitting?: boolean\n contextMessage?: string\n}\n\nexport function TaskDialog({ open, mode, onOpenChange, initialValues, onSubmit, isSubmitting, contextMessage }: TaskDialogProps) {\n const t = useT()\n\n const dialogTitle =\n mode === 'edit'\n ? t('customers.people.detail.tasks.dialog.editTitle', 'Edit task')\n : t('customers.people.detail.tasks.dialog.createTitle', 'Add task')\n\n const handleCancel = React.useCallback(() => {\n onOpenChange(false)\n }, [onOpenChange])\n\n const handleSubmit = React.useCallback(\n async (payload: TaskFormPayload) => {\n await onSubmit(payload)\n onOpenChange(false)\n },\n [onOpenChange, onSubmit],\n )\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"sm:max-w-2xl\">\n <DialogHeader>\n <DialogTitle>{dialogTitle}</DialogTitle>\n {contextMessage ? <DialogDescription>{contextMessage}</DialogDescription> : null}\n </DialogHeader>\n <TaskForm\n mode={mode}\n initialValues={initialValues}\n onSubmit={handleSubmit}\n onCancel={handleCancel}\n isSubmitting={isSubmitting}\n />\n </DialogContent>\n </Dialog>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@open-mercato/ui/primitives/dialog'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { TaskFormPayload } from './hooks/usePersonTasks'\nimport { TaskForm } from './TaskForm'\n\nexport type TaskDialogProps = {\n open: boolean\n mode: 'create' | 'edit'\n onOpenChange: (next: boolean) => void\n initialValues?: Record<string, unknown>\n onSubmit: (payload: TaskFormPayload) => Promise<void>\n isSubmitting?: boolean\n contextMessage?: string\n /** Forwarded to TaskForm to gate the rich status picker to the canonical interactions path. */\n useCanonicalInteractions?: boolean\n}\n\nexport function TaskDialog({ open, mode, onOpenChange, initialValues, onSubmit, isSubmitting, contextMessage, useCanonicalInteractions = false }: TaskDialogProps) {\n const t = useT()\n\n const dialogTitle =\n mode === 'edit'\n ? t('customers.people.detail.tasks.dialog.editTitle', 'Edit task')\n : t('customers.people.detail.tasks.dialog.createTitle', 'Add task')\n\n const handleCancel = React.useCallback(() => {\n onOpenChange(false)\n }, [onOpenChange])\n\n const handleSubmit = React.useCallback(\n async (payload: TaskFormPayload) => {\n await onSubmit(payload)\n onOpenChange(false)\n },\n [onOpenChange, onSubmit],\n )\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"sm:max-w-2xl\">\n <DialogHeader>\n <DialogTitle>{dialogTitle}</DialogTitle>\n {contextMessage ? <DialogDescription>{contextMessage}</DialogDescription> : null}\n </DialogHeader>\n <TaskForm\n mode={mode}\n initialValues={initialValues}\n onSubmit={handleSubmit}\n onCancel={handleCancel}\n isSubmitting={isSubmitting}\n useCanonicalInteractions={useCanonicalInteractions}\n />\n </DialogContent>\n </Dialog>\n )\n}\n"],
|
|
5
|
+
"mappings": ";AA2CQ,SACE,KADF;AAzCR,YAAY,WAAW;AACvB,SAAS,QAAQ,eAAe,cAAc,aAAa,yBAAyB;AACpF,SAAS,YAAY;AAErB,SAAS,gBAAgB;AAclB,SAAS,WAAW,EAAE,MAAM,MAAM,cAAc,eAAe,UAAU,cAAc,gBAAgB,2BAA2B,MAAM,GAAoB;AACjK,QAAM,IAAI,KAAK;AAEf,QAAM,cACJ,SAAS,SACL,EAAE,kDAAkD,WAAW,IAC/D,EAAE,oDAAoD,UAAU;AAEtE,QAAM,eAAe,MAAM,YAAY,MAAM;AAC3C,iBAAa,KAAK;AAAA,EACpB,GAAG,CAAC,YAAY,CAAC;AAEjB,QAAM,eAAe,MAAM;AAAA,IACzB,OAAO,YAA6B;AAClC,YAAM,SAAS,OAAO;AACtB,mBAAa,KAAK;AAAA,IACpB;AAAA,IACA,CAAC,cAAc,QAAQ;AAAA,EACzB;AAEA,SACE,oBAAC,UAAO,MAAY,cAClB,+BAAC,iBAAc,WAAU,gBACvB;AAAA,yBAAC,gBACC;AAAA,0BAAC,eAAa,uBAAY;AAAA,MACzB,iBAAiB,oBAAC,qBAAmB,0BAAe,IAAuB;AAAA,OAC9E;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA,UAAU;AAAA,QACV,UAAU;AAAA,QACV;AAAA,QACA;AAAA;AAAA,IACF;AAAA,KACF,GACF;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,13 +1,30 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
CrudForm
|
|
6
|
+
} from "@open-mercato/ui/backend/CrudForm";
|
|
5
7
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
8
|
+
import {
|
|
9
|
+
Select,
|
|
10
|
+
SelectContent,
|
|
11
|
+
SelectItem,
|
|
12
|
+
SelectItemLeading,
|
|
13
|
+
SelectTrigger,
|
|
14
|
+
SelectTriggerLeading,
|
|
15
|
+
SelectValue
|
|
16
|
+
} from "@open-mercato/ui/primitives/select";
|
|
17
|
+
import {
|
|
18
|
+
renderDictionaryColor,
|
|
19
|
+
renderDictionaryIcon
|
|
20
|
+
} from "@open-mercato/core/modules/dictionaries/components/dictionaryAppearance";
|
|
6
21
|
import { collectCustomFieldValues } from "@open-mercato/ui/backend/utils/customFieldValues";
|
|
7
22
|
import { createCrudFormError } from "@open-mercato/ui/backend/utils/serverErrors";
|
|
8
23
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
24
|
+
import { useOrganizationScopeVersion } from "@open-mercato/shared/lib/frontend/useOrganizationScope";
|
|
9
25
|
import { CUSTOMER_INTERACTION_ENTITY_ID } from "../../lib/interactionCompatibility.js";
|
|
10
26
|
import { normalizeCustomFieldSubmitValue } from "./customFieldUtils.js";
|
|
27
|
+
import { useCustomerDictionary } from "./hooks/useCustomerDictionary.js";
|
|
11
28
|
function TaskForm({
|
|
12
29
|
mode,
|
|
13
30
|
initialValues,
|
|
@@ -16,76 +33,39 @@ function TaskForm({
|
|
|
16
33
|
submitLabel,
|
|
17
34
|
cancelLabel,
|
|
18
35
|
isSubmitting = false,
|
|
19
|
-
formEntityId = CUSTOMER_INTERACTION_ENTITY_ID
|
|
36
|
+
formEntityId = CUSTOMER_INTERACTION_ENTITY_ID,
|
|
37
|
+
useCanonicalInteractions = false
|
|
20
38
|
}) {
|
|
21
39
|
const t = useT();
|
|
22
40
|
const containerRef = React.useRef(null);
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
type: "datetime",
|
|
36
|
-
placeholder: t("customers.people.detail.tasks.fields.scheduledAtPlaceholder", "Choose a date and time")
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
id: "priority",
|
|
40
|
-
label: t("customers.people.detail.tasks.fields.priority", "Priority"),
|
|
41
|
-
type: "number",
|
|
42
|
-
placeholder: t("customers.people.detail.tasks.fields.priorityPlaceholder", "0-100")
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
id: "description",
|
|
46
|
-
label: t("customers.people.detail.tasks.fields.description", "Description"),
|
|
47
|
-
type: "textarea",
|
|
48
|
-
placeholder: t("customers.people.detail.tasks.fields.descriptionPlaceholder", "Add context, outcome, or follow-up notes"),
|
|
49
|
-
layout: "full"
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
id: "is_done",
|
|
53
|
-
label: t("customers.people.detail.tasks.fields.done", "Mark as done"),
|
|
54
|
-
type: "checkbox"
|
|
55
|
-
}
|
|
56
|
-
];
|
|
57
|
-
}, [t]);
|
|
58
|
-
const groups = React.useMemo(() => {
|
|
41
|
+
const scopeVersion = useOrganizationScopeVersion();
|
|
42
|
+
const statusDictionary = useCustomerDictionary("interaction-statuses", scopeVersion);
|
|
43
|
+
const statusOptions = React.useMemo(() => {
|
|
44
|
+
const entries = statusDictionary.data?.entries ?? [];
|
|
45
|
+
if (entries.length > 0) {
|
|
46
|
+
return entries.map((entry) => ({
|
|
47
|
+
value: entry.value,
|
|
48
|
+
label: entry.label,
|
|
49
|
+
color: entry.color ?? null,
|
|
50
|
+
icon: entry.icon ?? null
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
59
53
|
return [
|
|
60
|
-
{
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
id: "status",
|
|
68
|
-
title: t("customers.people.detail.tasks.form.status", "Status"),
|
|
69
|
-
column: 2,
|
|
70
|
-
fields: ["priority", "is_done"]
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
id: "attributes",
|
|
74
|
-
title: t("customers.people.detail.tasks.form.customFields", "Task attributes"),
|
|
75
|
-
column: 1,
|
|
76
|
-
kind: "customFields"
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
id: "tips",
|
|
80
|
-
title: t("customers.people.detail.tasks.form.tips", "Tips"),
|
|
81
|
-
column: 2,
|
|
82
|
-
component: () => /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: t(
|
|
83
|
-
"customers.people.detail.tasks.form.tipsBody",
|
|
84
|
-
'Tasks save independently from the main customer form. Use clear titles like "Follow up call" or "Send pricing deck".'
|
|
85
|
-
) })
|
|
86
|
-
}
|
|
54
|
+
{ value: "planned", label: t("customers.interactions.status.planned", "Planned") },
|
|
55
|
+
{ value: "in_progress", label: t("customers.interactions.status.in_progress", "In progress") },
|
|
56
|
+
{ value: "waiting", label: t("customers.interactions.status.waiting", "Waiting / blocked") },
|
|
57
|
+
{ value: "done", label: t("customers.interactions.status.done", "Done") },
|
|
58
|
+
{ value: "canceled", label: t("customers.interactions.status.canceled", "Canceled") }
|
|
87
59
|
];
|
|
88
|
-
}, [t]);
|
|
60
|
+
}, [statusDictionary.data, t]);
|
|
61
|
+
const fields = React.useMemo(
|
|
62
|
+
() => buildTaskFormFields({ useCanonicalInteractions, statusOptions, t }),
|
|
63
|
+
[statusOptions, t, useCanonicalInteractions]
|
|
64
|
+
);
|
|
65
|
+
const groups = React.useMemo(
|
|
66
|
+
() => buildTaskFormGroups({ useCanonicalInteractions, t }),
|
|
67
|
+
[t, useCanonicalInteractions]
|
|
68
|
+
);
|
|
89
69
|
const resolvedSubmitLabel = submitLabel ?? (mode === "edit" ? t("customers.people.detail.tasks.form.submitEdit", "Update task (\u2318/Ctrl + Enter)") : t("customers.people.detail.tasks.form.submitCreate", "Save task (\u2318/Ctrl + Enter)"));
|
|
90
70
|
const resolvedCancelLabel = cancelLabel ?? t("customers.people.detail.tasks.cancel", "Cancel");
|
|
91
71
|
const handleKeyDown = React.useCallback(
|
|
@@ -109,6 +89,11 @@ function TaskForm({
|
|
|
109
89
|
},
|
|
110
90
|
[onSubmit, t]
|
|
111
91
|
);
|
|
92
|
+
const resolvedInitialValues = React.useMemo(() => {
|
|
93
|
+
if (mode !== "create") return initialValues;
|
|
94
|
+
if (useCanonicalInteractions) return { status: "planned", ...initialValues ?? {} };
|
|
95
|
+
return initialValues;
|
|
96
|
+
}, [initialValues, mode, useCanonicalInteractions]);
|
|
112
97
|
return /* @__PURE__ */ jsx("div", { ref: containerRef, onKeyDown: handleKeyDown, children: /* @__PURE__ */ jsx(
|
|
113
98
|
CrudForm,
|
|
114
99
|
{
|
|
@@ -116,13 +101,122 @@ function TaskForm({
|
|
|
116
101
|
entityId: formEntityId,
|
|
117
102
|
fields,
|
|
118
103
|
groups,
|
|
119
|
-
initialValues,
|
|
104
|
+
initialValues: resolvedInitialValues,
|
|
120
105
|
submitLabel: resolvedSubmitLabel,
|
|
121
106
|
onSubmit: handleSubmit,
|
|
122
107
|
extraActions: /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", onClick: onCancel, disabled: isSubmitting, children: resolvedCancelLabel })
|
|
123
108
|
}
|
|
124
109
|
) });
|
|
125
110
|
}
|
|
111
|
+
function TaskStatusSelect({ id, value, setValue, disabled, options, placeholder }) {
|
|
112
|
+
const stringValue = typeof value === "string" ? value : "";
|
|
113
|
+
const activeOption = options.find((option) => option.value === stringValue) ?? null;
|
|
114
|
+
return /* @__PURE__ */ jsxs(
|
|
115
|
+
Select,
|
|
116
|
+
{
|
|
117
|
+
value: stringValue,
|
|
118
|
+
onValueChange: (next) => setValue(next || void 0),
|
|
119
|
+
disabled,
|
|
120
|
+
children: [
|
|
121
|
+
/* @__PURE__ */ jsxs(SelectTrigger, { id, "data-crud-focus-target": "", children: [
|
|
122
|
+
activeOption && (activeOption.icon || activeOption.color) ? /* @__PURE__ */ jsxs(SelectTriggerLeading, { children: [
|
|
123
|
+
renderDictionaryIcon(activeOption.icon, "h-4 w-4"),
|
|
124
|
+
renderDictionaryColor(activeOption.color, "h-3.5 w-3.5 rounded-full")
|
|
125
|
+
] }) : null,
|
|
126
|
+
/* @__PURE__ */ jsx(SelectValue, { placeholder, children: activeOption?.label })
|
|
127
|
+
] }),
|
|
128
|
+
/* @__PURE__ */ jsx(SelectContent, { children: options.map((option) => /* @__PURE__ */ jsxs(SelectItem, { value: option.value, children: [
|
|
129
|
+
option.icon || option.color ? /* @__PURE__ */ jsxs(SelectItemLeading, { children: [
|
|
130
|
+
renderDictionaryIcon(option.icon, "h-4 w-4"),
|
|
131
|
+
renderDictionaryColor(option.color, "h-3.5 w-3.5 rounded-full")
|
|
132
|
+
] }) : null,
|
|
133
|
+
option.label
|
|
134
|
+
] }, option.value)) })
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
function buildTaskFormFields(options) {
|
|
140
|
+
const { useCanonicalInteractions, statusOptions, t } = options;
|
|
141
|
+
const statusField = useCanonicalInteractions ? {
|
|
142
|
+
id: "status",
|
|
143
|
+
label: t("customers.people.detail.tasks.fields.status", "Status"),
|
|
144
|
+
type: "custom",
|
|
145
|
+
component: (props) => /* @__PURE__ */ jsx(
|
|
146
|
+
TaskStatusSelect,
|
|
147
|
+
{
|
|
148
|
+
...props,
|
|
149
|
+
options: statusOptions,
|
|
150
|
+
placeholder: t("customers.people.detail.tasks.fields.statusPlaceholder", "Select a status")
|
|
151
|
+
}
|
|
152
|
+
)
|
|
153
|
+
} : {
|
|
154
|
+
id: "is_done",
|
|
155
|
+
label: t("customers.people.detail.tasks.fields.done", "Mark as done"),
|
|
156
|
+
type: "checkbox"
|
|
157
|
+
};
|
|
158
|
+
return [
|
|
159
|
+
{
|
|
160
|
+
id: "title",
|
|
161
|
+
label: t("customers.people.detail.tasks.fields.title", "Title"),
|
|
162
|
+
type: "text",
|
|
163
|
+
required: true,
|
|
164
|
+
placeholder: t("customers.people.detail.tasks.fields.titlePlaceholder", "Task title")
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
id: "scheduledAt",
|
|
168
|
+
label: t("customers.people.detail.tasks.fields.scheduledAt", "Scheduled for"),
|
|
169
|
+
type: "datetime",
|
|
170
|
+
placeholder: t("customers.people.detail.tasks.fields.scheduledAtPlaceholder", "Choose a date and time")
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
id: "priority",
|
|
174
|
+
label: t("customers.people.detail.tasks.fields.priority", "Priority"),
|
|
175
|
+
type: "number",
|
|
176
|
+
placeholder: t("customers.people.detail.tasks.fields.priorityPlaceholder", "0-100")
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
id: "description",
|
|
180
|
+
label: t("customers.people.detail.tasks.fields.description", "Description"),
|
|
181
|
+
type: "textarea",
|
|
182
|
+
placeholder: t("customers.people.detail.tasks.fields.descriptionPlaceholder", "Add context, outcome, or follow-up notes"),
|
|
183
|
+
layout: "full"
|
|
184
|
+
},
|
|
185
|
+
statusField
|
|
186
|
+
];
|
|
187
|
+
}
|
|
188
|
+
function buildTaskFormGroups(options) {
|
|
189
|
+
const { useCanonicalInteractions, t } = options;
|
|
190
|
+
return [
|
|
191
|
+
{
|
|
192
|
+
id: "details",
|
|
193
|
+
title: t("customers.people.detail.tasks.form.details", "Task details"),
|
|
194
|
+
column: 1,
|
|
195
|
+
fields: ["title", "scheduledAt", "description"]
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
id: "status",
|
|
199
|
+
title: t("customers.people.detail.tasks.form.status", "Status"),
|
|
200
|
+
column: 2,
|
|
201
|
+
fields: ["priority", useCanonicalInteractions ? "status" : "is_done"]
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
id: "attributes",
|
|
205
|
+
title: t("customers.people.detail.tasks.form.customFields", "Task attributes"),
|
|
206
|
+
column: 1,
|
|
207
|
+
kind: "customFields"
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
id: "tips",
|
|
211
|
+
title: t("customers.people.detail.tasks.form.tips", "Tips"),
|
|
212
|
+
column: 2,
|
|
213
|
+
component: () => /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: t(
|
|
214
|
+
"customers.people.detail.tasks.form.tipsBody",
|
|
215
|
+
'Tasks save independently from the main customer form. Use clear titles like "Follow up call" or "Send pricing deck".'
|
|
216
|
+
) })
|
|
217
|
+
}
|
|
218
|
+
];
|
|
219
|
+
}
|
|
126
220
|
function buildTaskSubmitPayload(values, t) {
|
|
127
221
|
const rawTitle = typeof values.title === "string" ? values.title.trim() : "";
|
|
128
222
|
if (!rawTitle.length) {
|
|
@@ -141,8 +235,13 @@ function buildTaskSubmitPayload(values, t) {
|
|
|
141
235
|
}
|
|
142
236
|
const rawDescription = typeof values.description === "string" ? values.description.trim() : "";
|
|
143
237
|
const rawScheduledAt = typeof values.scheduledAt === "string" ? values.scheduledAt.trim() : "";
|
|
238
|
+
const rawStatus = typeof values.status === "string" ? values.status.trim() : "";
|
|
239
|
+
const status = rawStatus.length ? rawStatus : void 0;
|
|
144
240
|
const base = { title: rawTitle };
|
|
145
|
-
if (
|
|
241
|
+
if (status !== void 0) {
|
|
242
|
+
base.status = status;
|
|
243
|
+
base.is_done = status === "done";
|
|
244
|
+
} else if (typeof values.is_done === "boolean") {
|
|
146
245
|
base.is_done = values.is_done;
|
|
147
246
|
}
|
|
148
247
|
base.description = rawDescription || null;
|
|
@@ -155,6 +254,8 @@ function buildTaskSubmitPayload(values, t) {
|
|
|
155
254
|
}
|
|
156
255
|
export {
|
|
157
256
|
TaskForm,
|
|
257
|
+
buildTaskFormFields,
|
|
258
|
+
buildTaskFormGroups,
|
|
158
259
|
buildTaskSubmitPayload
|
|
159
260
|
};
|
|
160
261
|
//# sourceMappingURL=TaskForm.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customers/components/detail/TaskForm.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { CrudForm, type CrudField, type CrudFormGroup } from '@open-mercato/ui/backend/CrudForm'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { collectCustomFieldValues } from '@open-mercato/ui/backend/utils/customFieldValues'\nimport { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { CUSTOMER_INTERACTION_ENTITY_ID } from '../../lib/interactionCompatibility'\nimport type { TaskFormPayload } from './hooks/usePersonTasks'\nimport { normalizeCustomFieldSubmitValue } from './customFieldUtils'\n\nexport type TaskFormProps = {\n mode: 'create' | 'edit'\n initialValues?: Record<string, unknown>\n onSubmit: (payload: TaskFormPayload) => Promise<void>\n onCancel: () => void\n submitLabel?: string\n cancelLabel?: string\n isSubmitting?: boolean\n formEntityId?: string\n}\n\nexport function TaskForm({\n mode,\n initialValues,\n onSubmit,\n onCancel,\n submitLabel,\n cancelLabel,\n isSubmitting = false,\n formEntityId = CUSTOMER_INTERACTION_ENTITY_ID,\n}: TaskFormProps) {\n const t = useT()\n const containerRef = React.useRef<HTMLDivElement | null>(null)\n\n const fields = React.useMemo<CrudField[]>(() => {\n return [\n {\n id: 'title',\n label: t('customers.people.detail.tasks.fields.title', 'Title'),\n type: 'text',\n required: true,\n placeholder: t('customers.people.detail.tasks.fields.titlePlaceholder', 'Task title'),\n },\n {\n id: 'scheduledAt',\n label: t('customers.people.detail.tasks.fields.scheduledAt', 'Scheduled for'),\n type: 'datetime',\n placeholder: t('customers.people.detail.tasks.fields.scheduledAtPlaceholder', 'Choose a date and time'),\n },\n {\n id: 'priority',\n label: t('customers.people.detail.tasks.fields.priority', 'Priority'),\n type: 'number',\n placeholder: t('customers.people.detail.tasks.fields.priorityPlaceholder', '0-100'),\n },\n {\n id: 'description',\n label: t('customers.people.detail.tasks.fields.description', 'Description'),\n type: 'textarea',\n placeholder: t('customers.people.detail.tasks.fields.descriptionPlaceholder', 'Add context, outcome, or follow-up notes'),\n layout: 'full',\n },\n {\n id: 'is_done',\n label: t('customers.people.detail.tasks.fields.done', 'Mark as done'),\n type: 'checkbox',\n },\n ]\n }, [t])\n\n const groups = React.useMemo<CrudFormGroup[]>(() => {\n return [\n {\n id: 'details',\n title: t('customers.people.detail.tasks.form.details', 'Task details'),\n column: 1,\n fields: ['title', 'scheduledAt', 'description'],\n },\n {\n id: 'status',\n title: t('customers.people.detail.tasks.form.status', 'Status'),\n column: 2,\n fields: ['priority', 'is_done'],\n },\n {\n id: 'attributes',\n title: t('customers.people.detail.tasks.form.customFields', 'Task attributes'),\n column: 1,\n kind: 'customFields',\n },\n {\n id: 'tips',\n title: t('customers.people.detail.tasks.form.tips', 'Tips'),\n column: 2,\n component: () => (\n <div className=\"text-sm text-muted-foreground\">\n {t(\n 'customers.people.detail.tasks.form.tipsBody',\n 'Tasks save independently from the main customer form. Use clear titles like \"Follow up call\" or \"Send pricing deck\".',\n )}\n </div>\n ),\n },\n ]\n }, [t])\n\n const resolvedSubmitLabel =\n submitLabel ??\n (mode === 'edit'\n ? t('customers.people.detail.tasks.form.submitEdit', 'Update task (\u2318/Ctrl + Enter)')\n : t('customers.people.detail.tasks.form.submitCreate', 'Save task (\u2318/Ctrl + Enter)'))\n const resolvedCancelLabel = cancelLabel ?? t('customers.people.detail.tasks.cancel', 'Cancel')\n\n const handleKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLDivElement>) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n const form = containerRef.current?.querySelector('form')\n form?.requestSubmit()\n }\n if (event.key === 'Escape') {\n event.preventDefault()\n onCancel()\n }\n },\n [onCancel],\n )\n\n const handleSubmit = React.useCallback(\n async (values: Record<string, unknown>) => {\n const payload = buildTaskSubmitPayload(values, t)\n await onSubmit(payload)\n },\n [onSubmit, t],\n )\n\n return (\n <div ref={containerRef} onKeyDown={handleKeyDown}>\n <CrudForm\n embedded\n entityId={formEntityId}\n fields={fields}\n groups={groups}\n initialValues={initialValues}\n submitLabel={resolvedSubmitLabel}\n onSubmit={handleSubmit}\n extraActions={\n <Button type=\"button\" variant=\"outline\" onClick={onCancel} disabled={isSubmitting}>\n {resolvedCancelLabel}\n </Button>\n }\n />\n </div>\n )\n}\n\nexport function buildTaskSubmitPayload(values: Record<string, unknown>, t: (key: string, fallback?: string) => string): TaskFormPayload {\n const rawTitle = typeof values.title === 'string' ? values.title.trim() : ''\n if (!rawTitle.length) {\n const message = t('customers.people.detail.tasks.titleRequired', 'Task name is required.')\n throw createCrudFormError(message, { title: message })\n }\n\n const rawPriority = typeof values.priority === 'number' || typeof values.priority === 'string' ? values.priority : null\n let priority: number | null = null\n if (rawPriority !== null && rawPriority !== '') {\n const parsed = typeof rawPriority === 'number' ? rawPriority : Number(String(rawPriority).trim())\n if (!Number.isInteger(parsed) || parsed < 0 || parsed > 100) {\n const message = t('customers.people.detail.tasks.priorityInvalid', 'Enter a whole-number priority between 0 and 100.')\n throw createCrudFormError(message, { priority: message })\n }\n priority = parsed\n }\n\n const rawDescription = typeof values.description === 'string' ? values.description.trim() : ''\n const rawScheduledAt = typeof values.scheduledAt === 'string' ? values.scheduledAt.trim() : ''\n const base: TaskFormPayload['base'] = { title: rawTitle }\n if (typeof values.is_done === 'boolean') {\n base.is_done = values.is_done\n }\n base.description = rawDescription || null\n base.priority = priority\n base.scheduledAt = rawScheduledAt || null\n const custom = collectCustomFieldValues(values, {\n transform: (value) => normalizeCustomFieldSubmitValue(value),\n })\n return { base, custom }\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport {\n CrudForm,\n type CrudCustomFieldRenderProps,\n type CrudField,\n type CrudFormGroup,\n} from '@open-mercato/ui/backend/CrudForm'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectItemLeading,\n SelectTrigger,\n SelectTriggerLeading,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport {\n renderDictionaryColor,\n renderDictionaryIcon,\n} from '@open-mercato/core/modules/dictionaries/components/dictionaryAppearance'\nimport { collectCustomFieldValues } from '@open-mercato/ui/backend/utils/customFieldValues'\nimport { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { CUSTOMER_INTERACTION_ENTITY_ID } from '../../lib/interactionCompatibility'\nimport type { TaskFormPayload } from './hooks/usePersonTasks'\nimport { normalizeCustomFieldSubmitValue } from './customFieldUtils'\nimport { useCustomerDictionary } from './hooks/useCustomerDictionary'\n\nexport type TaskFormProps = {\n mode: 'create' | 'edit'\n initialValues?: Record<string, unknown>\n onSubmit: (payload: TaskFormPayload) => Promise<void>\n onCancel: () => void\n submitLabel?: string\n cancelLabel?: string\n isSubmitting?: boolean\n formEntityId?: string\n /**\n * Render the rich, dictionary-backed status picker only on the canonical interactions path.\n * On the legacy path the deprecated `/api/customers/todos` bridge round-trips a task through a\n * binary done/not-done shape, so non-binary statuses cannot persist \u2014 keep the done checkbox there.\n */\n useCanonicalInteractions?: boolean\n}\n\nexport function TaskForm({\n mode,\n initialValues,\n onSubmit,\n onCancel,\n submitLabel,\n cancelLabel,\n isSubmitting = false,\n formEntityId = CUSTOMER_INTERACTION_ENTITY_ID,\n useCanonicalInteractions = false,\n}: TaskFormProps) {\n const t = useT()\n const containerRef = React.useRef<HTMLDivElement | null>(null)\n const scopeVersion = useOrganizationScopeVersion()\n const statusDictionary = useCustomerDictionary('interaction-statuses', scopeVersion)\n\n const statusOptions = React.useMemo<TaskStatusOption[]>(() => {\n const entries = statusDictionary.data?.entries ?? []\n if (entries.length > 0) {\n return entries.map((entry) => ({\n value: entry.value,\n label: entry.label,\n color: entry.color ?? null,\n icon: entry.icon ?? null,\n }))\n }\n // Fallback to the canonical seeded set until the dictionary loads (or for existing\n // tenants whose interaction-statuses dictionary has not been backfilled yet).\n return [\n { value: 'planned', label: t('customers.interactions.status.planned', 'Planned') },\n { value: 'in_progress', label: t('customers.interactions.status.in_progress', 'In progress') },\n { value: 'waiting', label: t('customers.interactions.status.waiting', 'Waiting / blocked') },\n { value: 'done', label: t('customers.interactions.status.done', 'Done') },\n { value: 'canceled', label: t('customers.interactions.status.canceled', 'Canceled') },\n ]\n }, [statusDictionary.data, t])\n\n const fields = React.useMemo<CrudField[]>(\n () => buildTaskFormFields({ useCanonicalInteractions, statusOptions, t }),\n [statusOptions, t, useCanonicalInteractions],\n )\n\n const groups = React.useMemo<CrudFormGroup[]>(\n () => buildTaskFormGroups({ useCanonicalInteractions, t }),\n [t, useCanonicalInteractions],\n )\n\n const resolvedSubmitLabel =\n submitLabel ??\n (mode === 'edit'\n ? t('customers.people.detail.tasks.form.submitEdit', 'Update task (\u2318/Ctrl + Enter)')\n : t('customers.people.detail.tasks.form.submitCreate', 'Save task (\u2318/Ctrl + Enter)'))\n const resolvedCancelLabel = cancelLabel ?? t('customers.people.detail.tasks.cancel', 'Cancel')\n\n const handleKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLDivElement>) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n const form = containerRef.current?.querySelector('form')\n form?.requestSubmit()\n }\n if (event.key === 'Escape') {\n event.preventDefault()\n onCancel()\n }\n },\n [onCancel],\n )\n\n const handleSubmit = React.useCallback(\n async (values: Record<string, unknown>) => {\n const payload = buildTaskSubmitPayload(values, t)\n await onSubmit(payload)\n },\n [onSubmit, t],\n )\n\n // On create, default the canonical status select to `planned` so the dropdown is never empty.\n // The legacy path has no status field (just the done checkbox), so it needs no default.\n const resolvedInitialValues = React.useMemo<Record<string, unknown> | undefined>(() => {\n if (mode !== 'create') return initialValues\n if (useCanonicalInteractions) return { status: 'planned', ...(initialValues ?? {}) }\n return initialValues\n }, [initialValues, mode, useCanonicalInteractions])\n\n return (\n <div ref={containerRef} onKeyDown={handleKeyDown}>\n <CrudForm\n embedded\n entityId={formEntityId}\n fields={fields}\n groups={groups}\n initialValues={resolvedInitialValues}\n submitLabel={resolvedSubmitLabel}\n onSubmit={handleSubmit}\n extraActions={\n <Button type=\"button\" variant=\"outline\" onClick={onCancel} disabled={isSubmitting}>\n {resolvedCancelLabel}\n </Button>\n }\n />\n </div>\n )\n}\n\ntype TaskFormTranslator = (key: string, fallback?: string) => string\n\ntype TaskStatusOption = { value: string; label: string; color?: string | null; icon?: string | null }\n\ntype TaskStatusSelectProps = CrudCustomFieldRenderProps & {\n options: TaskStatusOption[]\n placeholder: string\n}\n\n// Dictionary-backed status picker that surfaces each status's icon + color (the dictionary\n// \"appearance\") next to its label, both in the dropdown rows and the trigger. CrudForm's generic\n// `select` renders label-only text, so the canonical status field uses this custom renderer instead.\nfunction TaskStatusSelect({ id, value, setValue, disabled, options, placeholder }: TaskStatusSelectProps) {\n const stringValue = typeof value === 'string' ? value : ''\n const activeOption = options.find((option) => option.value === stringValue) ?? null\n return (\n <Select\n value={stringValue}\n onValueChange={(next) => setValue(next || undefined)}\n disabled={disabled}\n >\n <SelectTrigger id={id} data-crud-focus-target=\"\">\n {activeOption && (activeOption.icon || activeOption.color) ? (\n <SelectTriggerLeading>\n {renderDictionaryIcon(activeOption.icon, 'h-4 w-4')}\n {renderDictionaryColor(activeOption.color, 'h-3.5 w-3.5 rounded-full')}\n </SelectTriggerLeading>\n ) : null}\n <SelectValue placeholder={placeholder}>{activeOption?.label}</SelectValue>\n </SelectTrigger>\n <SelectContent>\n {options.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.icon || option.color ? (\n <SelectItemLeading>\n {renderDictionaryIcon(option.icon, 'h-4 w-4')}\n {renderDictionaryColor(option.color, 'h-3.5 w-3.5 rounded-full')}\n </SelectItemLeading>\n ) : null}\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )\n}\n\nexport function buildTaskFormFields(options: {\n useCanonicalInteractions: boolean\n statusOptions: TaskStatusOption[]\n t: TaskFormTranslator\n}): CrudField[] {\n const { useCanonicalInteractions, statusOptions, t } = options\n // Canonical path: dictionary-backed rich status picker. Legacy path: the binary done checkbox,\n // because the deprecated todos bridge cannot persist non-binary statuses on read-back.\n const statusField: CrudField = useCanonicalInteractions\n ? {\n id: 'status',\n label: t('customers.people.detail.tasks.fields.status', 'Status'),\n type: 'custom',\n component: (props) => (\n <TaskStatusSelect\n {...props}\n options={statusOptions}\n placeholder={t('customers.people.detail.tasks.fields.statusPlaceholder', 'Select a status')}\n />\n ),\n }\n : {\n id: 'is_done',\n label: t('customers.people.detail.tasks.fields.done', 'Mark as done'),\n type: 'checkbox',\n }\n return [\n {\n id: 'title',\n label: t('customers.people.detail.tasks.fields.title', 'Title'),\n type: 'text',\n required: true,\n placeholder: t('customers.people.detail.tasks.fields.titlePlaceholder', 'Task title'),\n },\n {\n id: 'scheduledAt',\n label: t('customers.people.detail.tasks.fields.scheduledAt', 'Scheduled for'),\n type: 'datetime',\n placeholder: t('customers.people.detail.tasks.fields.scheduledAtPlaceholder', 'Choose a date and time'),\n },\n {\n id: 'priority',\n label: t('customers.people.detail.tasks.fields.priority', 'Priority'),\n type: 'number',\n placeholder: t('customers.people.detail.tasks.fields.priorityPlaceholder', '0-100'),\n },\n {\n id: 'description',\n label: t('customers.people.detail.tasks.fields.description', 'Description'),\n type: 'textarea',\n placeholder: t('customers.people.detail.tasks.fields.descriptionPlaceholder', 'Add context, outcome, or follow-up notes'),\n layout: 'full',\n },\n statusField,\n ]\n}\n\nexport function buildTaskFormGroups(options: {\n useCanonicalInteractions: boolean\n t: TaskFormTranslator\n}): CrudFormGroup[] {\n const { useCanonicalInteractions, t } = options\n return [\n {\n id: 'details',\n title: t('customers.people.detail.tasks.form.details', 'Task details'),\n column: 1,\n fields: ['title', 'scheduledAt', 'description'],\n },\n {\n id: 'status',\n title: t('customers.people.detail.tasks.form.status', 'Status'),\n column: 2,\n fields: ['priority', useCanonicalInteractions ? 'status' : 'is_done'],\n },\n {\n id: 'attributes',\n title: t('customers.people.detail.tasks.form.customFields', 'Task attributes'),\n column: 1,\n kind: 'customFields',\n },\n {\n id: 'tips',\n title: t('customers.people.detail.tasks.form.tips', 'Tips'),\n column: 2,\n component: () => (\n <div className=\"text-sm text-muted-foreground\">\n {t(\n 'customers.people.detail.tasks.form.tipsBody',\n 'Tasks save independently from the main customer form. Use clear titles like \"Follow up call\" or \"Send pricing deck\".',\n )}\n </div>\n ),\n },\n ]\n}\n\nexport function buildTaskSubmitPayload(values: Record<string, unknown>, t: (key: string, fallback?: string) => string): TaskFormPayload {\n const rawTitle = typeof values.title === 'string' ? values.title.trim() : ''\n if (!rawTitle.length) {\n const message = t('customers.people.detail.tasks.titleRequired', 'Task name is required.')\n throw createCrudFormError(message, { title: message })\n }\n\n const rawPriority = typeof values.priority === 'number' || typeof values.priority === 'string' ? values.priority : null\n let priority: number | null = null\n if (rawPriority !== null && rawPriority !== '') {\n const parsed = typeof rawPriority === 'number' ? rawPriority : Number(String(rawPriority).trim())\n if (!Number.isInteger(parsed) || parsed < 0 || parsed > 100) {\n const message = t('customers.people.detail.tasks.priorityInvalid', 'Enter a whole-number priority between 0 and 100.')\n throw createCrudFormError(message, { priority: message })\n }\n priority = parsed\n }\n\n const rawDescription = typeof values.description === 'string' ? values.description.trim() : ''\n const rawScheduledAt = typeof values.scheduledAt === 'string' ? values.scheduledAt.trim() : ''\n const rawStatus = typeof values.status === 'string' ? values.status.trim() : ''\n const status = rawStatus.length ? rawStatus : undefined\n const base: TaskFormPayload['base'] = { title: rawTitle }\n if (status !== undefined) {\n base.status = status\n // Keep is_done in sync so the deprecated todos bridge (which only carries a boolean)\n // stays correct; the canonical interactions path uses base.status directly.\n base.is_done = status === 'done'\n } else if (typeof values.is_done === 'boolean') {\n base.is_done = values.is_done\n }\n base.description = rawDescription || null\n base.priority = priority\n base.scheduledAt = rawScheduledAt || null\n const custom = collectCustomFieldValues(values, {\n transform: (value) => normalizeCustomFieldSubmitValue(value),\n })\n return { base, custom }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -5,6 +5,9 @@ import Link from "next/link";
|
|
|
5
5
|
import { Loader2, Pencil, Trash2 } from "lucide-react";
|
|
6
6
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
7
7
|
import { IconButton } from "@open-mercato/ui/primitives/icon-button";
|
|
8
|
+
import { StatusBadge } from "@open-mercato/ui/primitives/status-badge";
|
|
9
|
+
import { mapDictionaryColorToTone } from "@open-mercato/shared/lib/query/advanced-filter";
|
|
10
|
+
import { useOrganizationScopeVersion } from "@open-mercato/shared/lib/frontend/useOrganizationScope";
|
|
8
11
|
import { flash } from "@open-mercato/ui/backend/FlashMessages";
|
|
9
12
|
import { LoadingMessage, TabEmptyState } from "@open-mercato/ui/backend/detail";
|
|
10
13
|
import { cn } from "@open-mercato/shared/lib/utils";
|
|
@@ -15,6 +18,7 @@ import { formatDateTime } from "@open-mercato/shared/lib/time";
|
|
|
15
18
|
import { TimelineItemHeader } from "./TimelineItemHeader.js";
|
|
16
19
|
import { TaskDialog } from "./TaskDialog.js";
|
|
17
20
|
import { usePersonTasks } from "./hooks/usePersonTasks.js";
|
|
21
|
+
import { useCustomerDictionary } from "./hooks/useCustomerDictionary.js";
|
|
18
22
|
import { useInteractions } from "./hooks/useInteractions.js";
|
|
19
23
|
import { mapInteractionRecordToTodoSummary } from "../../lib/interactionCompatibility.js";
|
|
20
24
|
const RESERVED_TASK_CUSTOM_KEYS = /* @__PURE__ */ new Set(["priority", "description", "due_at", "dueAt"]);
|
|
@@ -38,11 +42,16 @@ function sortTaskSummaries(tasks) {
|
|
|
38
42
|
return left.id.localeCompare(right.id);
|
|
39
43
|
});
|
|
40
44
|
}
|
|
45
|
+
const STATUS_BADGE_VARIANTS = /* @__PURE__ */ new Set(["success", "warning", "error", "info", "neutral"]);
|
|
46
|
+
function coerceStatusBadgeVariant(tone) {
|
|
47
|
+
return tone && STATUS_BADGE_VARIANTS.has(tone) ? tone : "neutral";
|
|
48
|
+
}
|
|
41
49
|
function buildInitialFormValues(task) {
|
|
42
50
|
if (!task) return void 0;
|
|
43
51
|
const values = {
|
|
44
52
|
title: task.title ?? "",
|
|
45
53
|
is_done: task.isDone ?? false,
|
|
54
|
+
status: task.status ?? "planned",
|
|
46
55
|
description: task.description ?? "",
|
|
47
56
|
priority: task.priority ?? "",
|
|
48
57
|
scheduledAt: task.dueAt ?? ""
|
|
@@ -84,6 +93,9 @@ function TasksSection({
|
|
|
84
93
|
},
|
|
85
94
|
[runGuardedMutation]
|
|
86
95
|
);
|
|
96
|
+
const scopeVersion = useOrganizationScopeVersion();
|
|
97
|
+
const statusDictionary = useCustomerDictionary("interaction-statuses", scopeVersion);
|
|
98
|
+
const statusMap = statusDictionary.data?.map ?? null;
|
|
87
99
|
const legacyResult = usePersonTasks({ entityId, initialTasks });
|
|
88
100
|
const canonicalResult = useInteractions({
|
|
89
101
|
entityId: useCanonicalInteractions ? entityId : null,
|
|
@@ -100,7 +112,7 @@ function TasksSection({
|
|
|
100
112
|
entityId,
|
|
101
113
|
interactionType: "task",
|
|
102
114
|
title: payload.base.title,
|
|
103
|
-
status: payload.base.is_done ? "done" : "planned",
|
|
115
|
+
status: payload.base.status ?? (payload.base.is_done ? "done" : "planned"),
|
|
104
116
|
priority: payload.base.priority ?? null,
|
|
105
117
|
body: payload.base.description ?? null,
|
|
106
118
|
scheduledAt: payload.base.scheduledAt ?? null,
|
|
@@ -114,7 +126,7 @@ function TasksSection({
|
|
|
114
126
|
async (task, payload) => {
|
|
115
127
|
await canonicalResult.updateInteraction(task.todoId, {
|
|
116
128
|
title: payload.base.title,
|
|
117
|
-
status: payload.base.is_done ? "done" : "planned",
|
|
129
|
+
status: payload.base.status ?? (payload.base.is_done ? "done" : "planned"),
|
|
118
130
|
priority: payload.base.priority ?? null,
|
|
119
131
|
body: payload.base.description ?? null,
|
|
120
132
|
scheduledAt: payload.base.scheduledAt ?? null,
|
|
@@ -403,7 +415,17 @@ function TasksSection({
|
|
|
403
415
|
),
|
|
404
416
|
children: title
|
|
405
417
|
}
|
|
406
|
-
)
|
|
418
|
+
),
|
|
419
|
+
task.status ? /* @__PURE__ */ jsx(
|
|
420
|
+
StatusBadge,
|
|
421
|
+
{
|
|
422
|
+
variant: coerceStatusBadgeVariant(
|
|
423
|
+
mapDictionaryColorToTone(statusMap?.[task.status]?.color ?? null)
|
|
424
|
+
),
|
|
425
|
+
dot: true,
|
|
426
|
+
children: statusMap?.[task.status]?.label ?? task.status
|
|
427
|
+
}
|
|
428
|
+
) : null
|
|
407
429
|
] }),
|
|
408
430
|
timestamp: task.createdAt,
|
|
409
431
|
fallbackTimestampLabel: createdLabel
|
|
@@ -480,7 +502,8 @@ function TasksSection({
|
|
|
480
502
|
initialValues: buildInitialFormValues(editingTask),
|
|
481
503
|
onSubmit: handleDialogSubmit,
|
|
482
504
|
isSubmitting: isMutating,
|
|
483
|
-
contextMessage: dialogContextMessage
|
|
505
|
+
contextMessage: dialogContextMessage,
|
|
506
|
+
useCanonicalInteractions
|
|
484
507
|
}
|
|
485
508
|
)
|
|
486
509
|
] });
|