@open-mercato/core 0.10.1-develop.7668.1.3aa8463bd8 → 0.10.1-develop.7670.1.7b5ad74924
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/customers/commands/interactions.js +65 -21
- package/dist/modules/customers/commands/interactions.js.map +2 -2
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +26 -24
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/fieldConfig.js +10 -1
- package/dist/modules/customers/components/detail/schedule/fieldConfig.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/index.js +2 -1
- package/dist/modules/customers/components/detail/schedule/index.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js +5 -4
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/customers/commands/interactions.ts +87 -35
- package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +31 -24
- package/src/modules/customers/components/detail/schedule/fieldConfig.ts +16 -0
- package/src/modules/customers/components/detail/schedule/index.ts +1 -1
- package/src/modules/customers/components/detail/schedule/useScheduleFormState.ts +12 -6
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import { isDateRequired } from "./fieldConfig.js";
|
|
2
|
+
import { isDateRequired, resolveScheduleActivityType } from "./fieldConfig.js";
|
|
3
3
|
const PARTICIPANT_COLORS = [
|
|
4
4
|
"bg-chart-emerald",
|
|
5
5
|
"bg-chart-blue",
|
|
@@ -74,8 +74,9 @@ function useScheduleFormState({ open, editData }) {
|
|
|
74
74
|
React.useEffect(() => {
|
|
75
75
|
if (open) {
|
|
76
76
|
if (editData) {
|
|
77
|
-
const
|
|
78
|
-
|
|
77
|
+
const rawType = editData.interactionType ?? "meeting";
|
|
78
|
+
const resolvedType = resolveScheduleActivityType(rawType);
|
|
79
|
+
setActivityType(rawType);
|
|
79
80
|
setTitle(editData.title ?? "");
|
|
80
81
|
const sourceTimestamp = editData.scheduledAt ?? editData.occurredAt ?? null;
|
|
81
82
|
const seedDate = sourceTimestamp ? new Date(sourceTimestamp) : null;
|
|
@@ -168,7 +169,7 @@ function useScheduleFormState({ open, editData }) {
|
|
|
168
169
|
}
|
|
169
170
|
if (lastReminderTypeRef.current === activityType) return;
|
|
170
171
|
lastReminderTypeRef.current = activityType;
|
|
171
|
-
setReminderMinutes(DEFAULT_REMINDER_MINUTES[activityType]);
|
|
172
|
+
setReminderMinutes(DEFAULT_REMINDER_MINUTES[resolveScheduleActivityType(activityType)]);
|
|
172
173
|
}, [activityType, isEditing, open]);
|
|
173
174
|
const removeParticipant = React.useCallback((index) => {
|
|
174
175
|
setParticipants((prev) => prev.filter((_, i) => i !== index));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/customers/components/detail/schedule/useScheduleFormState.ts"],
|
|
4
|
-
"sourcesContent": ["import * as React from 'react'\nimport type { ActivityType } from './fieldConfig'\nimport { isDateRequired } from './fieldConfig'\n\nexport type RsvpStatus = 'pending' | 'accepted' | 'declined' | 'tentative'\n\nexport type Participant = {\n userId?: string\n name: string\n email?: string\n color?: string\n status?: RsvpStatus\n}\n\nexport type LinkedEntity = {\n id: string\n type: 'company' | 'deal' | 'offer'\n label: string\n}\n\nexport type ScheduleActivityEditData = {\n id: string\n /** Record version for the OSS optimistic-lock header on edit (#2055). */\n updatedAt?: string | null\n /**\n * The activity's own linked person/company id, captured at edit time. Callers\n * that show activities from more than one entity (e.g. the deal detail page's\n * multi-entity activity list) must set this so `ScheduleActivityDialog` keeps\n * the edit payload's `entityId` pinned to the activity's actual record instead\n * of whichever entity happens to be selected in the host page's picker (#6050).\n */\n entityId?: string | null\n interactionType?: string\n title?: string | null\n body?: string | null\n scheduledAt?: string | null\n /**\n * Historical timestamp for completed activities (status `done`). Required for\n * the edit prefill to restore the original date/time instead of falling back\n * to \"today\" (#1807).\n */\n occurredAt?: string | null\n durationMinutes?: number | null\n /** Interaction priority column (0-100, nullable) backing the task priority control (#5943). */\n priority?: number | null\n location?: string | null\n allDay?: boolean | null\n recurrenceRule?: string | null\n recurrenceEnd?: string | null\n participants?: Array<{ userId?: string; name?: string; email?: string; status?: string }> | null\n reminderMinutes?: number | null\n visibility?: string | null\n linkedEntities?: Array<{ id: string; type: string; label: string }> | null\n guestPermissions?: { canInviteOthers?: boolean; canModify?: boolean; canSeeList?: boolean } | null\n}\n\nexport const PARTICIPANT_COLORS = [\n 'bg-chart-emerald',\n 'bg-chart-blue',\n 'bg-chart-orange',\n 'bg-chart-violet',\n 'bg-chart-pink',\n 'bg-chart-teal',\n]\n\n// Per-Figma defaults for the Reminder dropdown when the user picks an activity\n// type. Meeting/email keep the standard 15 min; tasks default to 1 day (1440 min)\n// because they're plan-ahead artefacts; calls default to 5 min as a stand-in for\n// the Figma \"After call ends\" treatment (which would need a non-numeric sentinel\n// in the API contract \u2014 tracked as a follow-up).\nconst DEFAULT_REMINDER_MINUTES: Record<ActivityType, number> = {\n meeting: 15,\n call: 5,\n task: 1440,\n email: 15,\n note: 15,\n}\n\n// Create-mode date/time defaults. A fixed morning slot made every activity opened\n// later in the day start in the past, so the seed is always computed forward from\n// \"now\" (#5940): tasks are plan-ahead artefacts and default to the end of the\n// working day, everything else to the next half-hour slot.\nconst DEFAULT_SLOT_MINUTES = 30\nconst TASK_DEFAULT_HOUR = 17\nconst NEXT_DAY_START_HOUR = 9\n\nfunction padDatePart(value: number): string {\n return String(value).padStart(2, '0')\n}\n\nfunction isSameLocalDay(left: Date, right: Date): boolean {\n return (\n left.getFullYear() === right.getFullYear() &&\n left.getMonth() === right.getMonth() &&\n left.getDate() === right.getDate()\n )\n}\n\nfunction nextSlotAfter(now: Date): Date {\n const next = new Date(now)\n next.setSeconds(0, 0)\n next.setMinutes(next.getMinutes() + (DEFAULT_SLOT_MINUTES - (next.getMinutes() % DEFAULT_SLOT_MINUTES)))\n if (!isSameLocalDay(next, now)) {\n next.setHours(NEXT_DAY_START_HOUR, 0, 0, 0)\n }\n return next\n}\n\n/**\n * Default start moment for a newly created activity of `type`, always strictly\n * after `now` so a brand-new record is never born overdue (#5940).\n */\nexport function resolveDefaultActivityStart(type: ActivityType, now: Date): Date {\n if (type === 'task') {\n const endOfWorkingDay = new Date(now)\n endOfWorkingDay.setHours(TASK_DEFAULT_HOUR, 0, 0, 0)\n if (endOfWorkingDay.getTime() > now.getTime()) return endOfWorkingDay\n }\n return nextSlotAfter(now)\n}\n\nfunction formatLocalDateInput(date: Date): string {\n return `${date.getFullYear()}-${padDatePart(date.getMonth() + 1)}-${padDatePart(date.getDate())}`\n}\n\nfunction formatLocalTimeInput(date: Date): string {\n return `${padDatePart(date.getHours())}:${padDatePart(date.getMinutes())}`\n}\n\ninterface UseScheduleFormStateParams {\n open: boolean\n editData: ScheduleActivityEditData | null | undefined\n}\n\nexport function useScheduleFormState({ open, editData }: UseScheduleFormStateParams) {\n // An empty `id` is the menu-driven \"New X\" convention \u2014 a create with a preset\n // type, not an edit. The save path already reads it this way (`isSaveEdit`), so\n // the seeding below must agree or new records inherit edit-mode fallbacks (#5940).\n const isEditing = Boolean(editData?.id)\n const [initialStart] = React.useState(() => resolveDefaultActivityStart('meeting', new Date()))\n const [activityType, setActivityType] = React.useState<ActivityType>('meeting')\n const [title, setTitle] = React.useState('')\n const [date, setDate] = React.useState(() => formatLocalDateInput(initialStart))\n const [startTime, setStartTime] = React.useState(() => formatLocalTimeInput(initialStart))\n const [duration, setDuration] = React.useState(30)\n const [allDay, setAllDay] = React.useState(false)\n const [description, setDescription] = React.useState('')\n const [markdownEnabled, setMarkdownEnabled] = React.useState(true)\n const [location, setLocation] = React.useState('')\n const [reminderMinutes, setReminderMinutes] = React.useState(15)\n const [visibility, setVisibility] = React.useState('team')\n const [participants, setParticipants] = React.useState<Participant[]>([])\n const [linkedEntities, setLinkedEntities] = React.useState<LinkedEntity[]>([])\n const [recurrenceEnabled, setRecurrenceEnabled] = React.useState(false)\n const [recurrenceDays, setRecurrenceDays] = React.useState<boolean[]>([true, false, true, false, false, false, false])\n const [recurrenceEndType, setRecurrenceEndType] = React.useState<'never' | 'count' | 'date'>('never')\n const [recurrenceCount, setRecurrenceCount] = React.useState(8)\n const [recurrenceEndDate, setRecurrenceEndDate] = React.useState('')\n const [conflict, setConflict] = React.useState<string | null>(null)\n const [saving, setSaving] = React.useState(false)\n const [guestPermissions, setGuestPermissions] = React.useState({ canInviteOthers: true, canModify: false, canSeeList: true })\n\n React.useEffect(() => {\n if (open) {\n if (editData) {\n // Edit mode: populate from existing interaction\n const resolvedType = (editData.interactionType as ActivityType) ?? 'meeting'\n setActivityType(resolvedType)\n setTitle(editData.title ?? '')\n // `scheduledAt` is the canonical term of anything that was ever planned, so it\n // wins whenever it is set: the save path recomputes `scheduledAt` from these\n // seeded date/time fields, and seeding from `occurredAt` first silently\n // overwrote a completed task's due date on unrelated edits (#5939).\n // `occurredAt` stays the fallback for purely historical entries (a logged call\n // or note) that never had a `scheduledAt`, so editing those still restores\n // their actual moment instead of falling back to \"today\" (#1807).\n // Keep seed values in the user's local timezone, matching the cluster-E\n // local-day convention.\n const sourceTimestamp = editData.scheduledAt ?? editData.occurredAt ?? null\n const seedDate = sourceTimestamp ? new Date(sourceTimestamp) : null\n const seedDateValid = seedDate !== null && !Number.isNaN(seedDate.getTime())\n // A real edit of an undated task must stay undated: falling back to a\n // computed default here would silently give a backlog item a due date on\n // the next save (#5941). Menu-driven \"New Task\" (no id yet, so `isEditing`\n // is false) still gets the create-mode default so #5940's forward-seeded\n // default keeps working.\n if (isEditing && !seedDateValid && !isDateRequired(resolvedType)) {\n setDate('')\n setStartTime('')\n } else {\n const dateForForm = seedDateValid ? (seedDate as Date) : resolveDefaultActivityStart(resolvedType, new Date())\n setDate(formatLocalDateInput(dateForForm))\n setStartTime(formatLocalTimeInput(dateForForm))\n }\n setDuration(editData.durationMinutes ?? 30)\n setAllDay(editData.allDay ?? false)\n setDescription(editData.body ?? '')\n setLocation(editData.location ?? '')\n // Use per-type default when the editData omits an explicit reminder\n // (the menu-driven \"New X\" flow opens the dialog with `reminderMinutes: null`).\n setReminderMinutes(editData.reminderMinutes ?? DEFAULT_REMINDER_MINUTES[resolvedType])\n setVisibility(editData.visibility ?? 'team')\n setParticipants(\n Array.isArray(editData.participants)\n ? editData.participants.map((p, i) => ({\n userId: p.userId,\n name: p.name ?? p.email ?? p.userId ?? '',\n email: p.email,\n color: PARTICIPANT_COLORS[i % PARTICIPANT_COLORS.length],\n status: (p.status ?? 'pending') as RsvpStatus,\n }))\n : [],\n )\n setLinkedEntities(\n Array.isArray(editData.linkedEntities)\n ? editData.linkedEntities.map((e) => ({ id: e.id, type: e.type as LinkedEntity['type'], label: e.label }))\n : [],\n )\n if (editData.recurrenceRule) {\n setRecurrenceEnabled(true)\n // Parse RRULE to set days and end type\n const rule = editData.recurrenceRule\n const byDayMatch = rule.match(/BYDAY=([A-Z,]+)/)\n if (byDayMatch) {\n const dayNames = ['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU']\n const activeDays = byDayMatch[1].split(',')\n setRecurrenceDays(dayNames.map((d) => activeDays.includes(d)))\n }\n const countMatch = rule.match(/COUNT=(\\d+)/)\n const untilMatch = rule.match(/UNTIL=(\\d{8})/)\n if (countMatch) {\n setRecurrenceEndType('count')\n setRecurrenceCount(Number(countMatch[1]))\n } else if (untilMatch) {\n setRecurrenceEndType('date')\n const raw = untilMatch[1]\n setRecurrenceEndDate(`${raw.slice(0, 4)}-${raw.slice(4, 6)}-${raw.slice(6, 8)}`)\n } else {\n setRecurrenceEndType('never')\n }\n } else {\n setRecurrenceEnabled(false)\n }\n if (editData.guestPermissions) {\n setGuestPermissions({\n canInviteOthers: editData.guestPermissions.canInviteOthers ?? true,\n canModify: editData.guestPermissions.canModify ?? false,\n canSeeList: editData.guestPermissions.canSeeList ?? true,\n })\n }\n } else {\n // Create mode: reset all fields\n const defaultStart = resolveDefaultActivityStart('meeting', new Date())\n setActivityType('meeting')\n setTitle('')\n setDate(formatLocalDateInput(defaultStart))\n setStartTime(formatLocalTimeInput(defaultStart))\n setDuration(30)\n setAllDay(false)\n setDescription('')\n setLocation('')\n setReminderMinutes(DEFAULT_REMINDER_MINUTES.meeting)\n setVisibility('team')\n setParticipants([])\n setLinkedEntities([])\n setRecurrenceEnabled(false)\n }\n setConflict(null)\n }\n return () => {\n // Safety net: restore body scroll if Radix Dialog fails to clean up\n document.body.style.removeProperty('overflow')\n document.body.style.removeProperty('pointer-events')\n }\n }, [open, editData, isEditing])\n\n // Update the Reminder default when the activity type changes in create mode.\n // Skipped in edit mode (the persisted value wins), and gated by `open` to\n // avoid flipping the default in a closed-but-mounted dialog.\n const lastReminderTypeRef = React.useRef<ActivityType>('meeting')\n React.useEffect(() => {\n if (!open || isEditing) {\n lastReminderTypeRef.current = activityType\n return\n }\n if (lastReminderTypeRef.current === activityType) return\n lastReminderTypeRef.current = activityType\n setReminderMinutes(DEFAULT_REMINDER_MINUTES[activityType])\n }, [activityType, isEditing, open])\n\n const removeParticipant = React.useCallback((index: number) => {\n setParticipants((prev) => prev.filter((_, i) => i !== index))\n }, [])\n\n const toggleRecurrenceDay = React.useCallback((index: number) => {\n setRecurrenceDays((prev) => {\n const next = [...prev]\n next[index] = !next[index]\n return next\n })\n }, [])\n\n return {\n activityType,\n setActivityType,\n title,\n setTitle,\n date,\n setDate,\n startTime,\n setStartTime,\n duration,\n setDuration,\n allDay,\n setAllDay,\n description,\n setDescription,\n markdownEnabled,\n setMarkdownEnabled,\n location,\n setLocation,\n reminderMinutes,\n setReminderMinutes,\n visibility,\n setVisibility,\n participants,\n setParticipants,\n linkedEntities,\n setLinkedEntities,\n recurrenceEnabled,\n setRecurrenceEnabled,\n recurrenceDays,\n setRecurrenceDays,\n recurrenceEndType,\n setRecurrenceEndType,\n recurrenceCount,\n setRecurrenceCount,\n recurrenceEndDate,\n setRecurrenceEndDate,\n conflict,\n setConflict,\n saving,\n setSaving,\n guestPermissions,\n setGuestPermissions,\n removeParticipant,\n toggleRecurrenceDay,\n }\n}\n"],
|
|
5
|
-
"mappings": "AAAA,YAAY,WAAW;AAEvB,SAAS,
|
|
4
|
+
"sourcesContent": ["import * as React from 'react'\nimport type { ActivityType } from './fieldConfig'\nimport { isDateRequired, resolveScheduleActivityType } from './fieldConfig'\n\nexport type RsvpStatus = 'pending' | 'accepted' | 'declined' | 'tentative'\n\nexport type Participant = {\n userId?: string\n name: string\n email?: string\n color?: string\n status?: RsvpStatus\n}\n\nexport type LinkedEntity = {\n id: string\n type: 'company' | 'deal' | 'offer'\n label: string\n}\n\nexport type ScheduleActivityEditData = {\n id: string\n /** Record version for the OSS optimistic-lock header on edit (#2055). */\n updatedAt?: string | null\n /**\n * The activity's own linked person/company id, captured at edit time. Callers\n * that show activities from more than one entity (e.g. the deal detail page's\n * multi-entity activity list) must set this so `ScheduleActivityDialog` keeps\n * the edit payload's `entityId` pinned to the activity's actual record instead\n * of whichever entity happens to be selected in the host page's picker (#6050).\n */\n entityId?: string | null\n interactionType?: string\n title?: string | null\n body?: string | null\n scheduledAt?: string | null\n /**\n * Historical timestamp for completed activities (status `done`). Required for\n * the edit prefill to restore the original date/time instead of falling back\n * to \"today\" (#1807).\n */\n occurredAt?: string | null\n durationMinutes?: number | null\n /** Interaction priority column (0-100, nullable) backing the task priority control (#5943). */\n priority?: number | null\n location?: string | null\n allDay?: boolean | null\n recurrenceRule?: string | null\n recurrenceEnd?: string | null\n participants?: Array<{ userId?: string; name?: string; email?: string; status?: string }> | null\n reminderMinutes?: number | null\n visibility?: string | null\n linkedEntities?: Array<{ id: string; type: string; label: string }> | null\n guestPermissions?: { canInviteOthers?: boolean; canModify?: boolean; canSeeList?: boolean } | null\n}\n\nexport const PARTICIPANT_COLORS = [\n 'bg-chart-emerald',\n 'bg-chart-blue',\n 'bg-chart-orange',\n 'bg-chart-violet',\n 'bg-chart-pink',\n 'bg-chart-teal',\n]\n\n// Per-Figma defaults for the Reminder dropdown when the user picks an activity\n// type. Meeting/email keep the standard 15 min; tasks default to 1 day (1440 min)\n// because they're plan-ahead artefacts; calls default to 5 min as a stand-in for\n// the Figma \"After call ends\" treatment (which would need a non-numeric sentinel\n// in the API contract \u2014 tracked as a follow-up).\nconst DEFAULT_REMINDER_MINUTES: Record<ActivityType, number> = {\n meeting: 15,\n call: 5,\n task: 1440,\n email: 15,\n note: 15,\n}\n\n// Create-mode date/time defaults. A fixed morning slot made every activity opened\n// later in the day start in the past, so the seed is always computed forward from\n// \"now\" (#5940): tasks are plan-ahead artefacts and default to the end of the\n// working day, everything else to the next half-hour slot.\nconst DEFAULT_SLOT_MINUTES = 30\nconst TASK_DEFAULT_HOUR = 17\nconst NEXT_DAY_START_HOUR = 9\n\nfunction padDatePart(value: number): string {\n return String(value).padStart(2, '0')\n}\n\nfunction isSameLocalDay(left: Date, right: Date): boolean {\n return (\n left.getFullYear() === right.getFullYear() &&\n left.getMonth() === right.getMonth() &&\n left.getDate() === right.getDate()\n )\n}\n\nfunction nextSlotAfter(now: Date): Date {\n const next = new Date(now)\n next.setSeconds(0, 0)\n next.setMinutes(next.getMinutes() + (DEFAULT_SLOT_MINUTES - (next.getMinutes() % DEFAULT_SLOT_MINUTES)))\n if (!isSameLocalDay(next, now)) {\n next.setHours(NEXT_DAY_START_HOUR, 0, 0, 0)\n }\n return next\n}\n\n/**\n * Default start moment for a newly created activity of `type`, always strictly\n * after `now` so a brand-new record is never born overdue (#5940).\n */\nexport function resolveDefaultActivityStart(type: ActivityType, now: Date): Date {\n if (type === 'task') {\n const endOfWorkingDay = new Date(now)\n endOfWorkingDay.setHours(TASK_DEFAULT_HOUR, 0, 0, 0)\n if (endOfWorkingDay.getTime() > now.getTime()) return endOfWorkingDay\n }\n return nextSlotAfter(now)\n}\n\nfunction formatLocalDateInput(date: Date): string {\n return `${date.getFullYear()}-${padDatePart(date.getMonth() + 1)}-${padDatePart(date.getDate())}`\n}\n\nfunction formatLocalTimeInput(date: Date): string {\n return `${padDatePart(date.getHours())}:${padDatePart(date.getMinutes())}`\n}\n\ninterface UseScheduleFormStateParams {\n open: boolean\n editData: ScheduleActivityEditData | null | undefined\n}\n\nexport function useScheduleFormState({ open, editData }: UseScheduleFormStateParams) {\n // An empty `id` is the menu-driven \"New X\" convention \u2014 a create with a preset\n // type, not an edit. The save path already reads it this way (`isSaveEdit`), so\n // the seeding below must agree or new records inherit edit-mode fallbacks (#5940).\n const isEditing = Boolean(editData?.id)\n const [initialStart] = React.useState(() => resolveDefaultActivityStart('meeting', new Date()))\n // Holds the raw persisted/selected interaction type \u2014 may be a built-in Event, a\n // custom dictionary key, or a historical key the type picker no longer offers\n // (#7486). Lookups into the five-kind presentation maps (`FIELD_VISIBILITY`,\n // `TYPE_CHROME`, `DEFAULT_REMINDER_MINUTES`) must resolve through\n // `resolveScheduleActivityType` first; this state is saved back verbatim.\n const [activityType, setActivityType] = React.useState<string>('meeting')\n const [title, setTitle] = React.useState('')\n const [date, setDate] = React.useState(() => formatLocalDateInput(initialStart))\n const [startTime, setStartTime] = React.useState(() => formatLocalTimeInput(initialStart))\n const [duration, setDuration] = React.useState(30)\n const [allDay, setAllDay] = React.useState(false)\n const [description, setDescription] = React.useState('')\n const [markdownEnabled, setMarkdownEnabled] = React.useState(true)\n const [location, setLocation] = React.useState('')\n const [reminderMinutes, setReminderMinutes] = React.useState(15)\n const [visibility, setVisibility] = React.useState('team')\n const [participants, setParticipants] = React.useState<Participant[]>([])\n const [linkedEntities, setLinkedEntities] = React.useState<LinkedEntity[]>([])\n const [recurrenceEnabled, setRecurrenceEnabled] = React.useState(false)\n const [recurrenceDays, setRecurrenceDays] = React.useState<boolean[]>([true, false, true, false, false, false, false])\n const [recurrenceEndType, setRecurrenceEndType] = React.useState<'never' | 'count' | 'date'>('never')\n const [recurrenceCount, setRecurrenceCount] = React.useState(8)\n const [recurrenceEndDate, setRecurrenceEndDate] = React.useState('')\n const [conflict, setConflict] = React.useState<string | null>(null)\n const [saving, setSaving] = React.useState(false)\n const [guestPermissions, setGuestPermissions] = React.useState({ canInviteOthers: true, canModify: false, canSeeList: true })\n\n React.useEffect(() => {\n if (open) {\n if (editData) {\n // Edit mode: populate from existing interaction\n const rawType = editData.interactionType ?? 'meeting'\n const resolvedType = resolveScheduleActivityType(rawType)\n setActivityType(rawType)\n setTitle(editData.title ?? '')\n // `scheduledAt` is the canonical term of anything that was ever planned, so it\n // wins whenever it is set: the save path recomputes `scheduledAt` from these\n // seeded date/time fields, and seeding from `occurredAt` first silently\n // overwrote a completed task's due date on unrelated edits (#5939).\n // `occurredAt` stays the fallback for purely historical entries (a logged call\n // or note) that never had a `scheduledAt`, so editing those still restores\n // their actual moment instead of falling back to \"today\" (#1807).\n // Keep seed values in the user's local timezone, matching the cluster-E\n // local-day convention.\n const sourceTimestamp = editData.scheduledAt ?? editData.occurredAt ?? null\n const seedDate = sourceTimestamp ? new Date(sourceTimestamp) : null\n const seedDateValid = seedDate !== null && !Number.isNaN(seedDate.getTime())\n // A real edit of an undated task must stay undated: falling back to a\n // computed default here would silently give a backlog item a due date on\n // the next save (#5941). Menu-driven \"New Task\" (no id yet, so `isEditing`\n // is false) still gets the create-mode default so #5940's forward-seeded\n // default keeps working.\n if (isEditing && !seedDateValid && !isDateRequired(resolvedType)) {\n setDate('')\n setStartTime('')\n } else {\n const dateForForm = seedDateValid ? (seedDate as Date) : resolveDefaultActivityStart(resolvedType, new Date())\n setDate(formatLocalDateInput(dateForForm))\n setStartTime(formatLocalTimeInput(dateForForm))\n }\n setDuration(editData.durationMinutes ?? 30)\n setAllDay(editData.allDay ?? false)\n setDescription(editData.body ?? '')\n setLocation(editData.location ?? '')\n // Use per-type default when the editData omits an explicit reminder\n // (the menu-driven \"New X\" flow opens the dialog with `reminderMinutes: null`).\n setReminderMinutes(editData.reminderMinutes ?? DEFAULT_REMINDER_MINUTES[resolvedType])\n setVisibility(editData.visibility ?? 'team')\n setParticipants(\n Array.isArray(editData.participants)\n ? editData.participants.map((p, i) => ({\n userId: p.userId,\n name: p.name ?? p.email ?? p.userId ?? '',\n email: p.email,\n color: PARTICIPANT_COLORS[i % PARTICIPANT_COLORS.length],\n status: (p.status ?? 'pending') as RsvpStatus,\n }))\n : [],\n )\n setLinkedEntities(\n Array.isArray(editData.linkedEntities)\n ? editData.linkedEntities.map((e) => ({ id: e.id, type: e.type as LinkedEntity['type'], label: e.label }))\n : [],\n )\n if (editData.recurrenceRule) {\n setRecurrenceEnabled(true)\n // Parse RRULE to set days and end type\n const rule = editData.recurrenceRule\n const byDayMatch = rule.match(/BYDAY=([A-Z,]+)/)\n if (byDayMatch) {\n const dayNames = ['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU']\n const activeDays = byDayMatch[1].split(',')\n setRecurrenceDays(dayNames.map((d) => activeDays.includes(d)))\n }\n const countMatch = rule.match(/COUNT=(\\d+)/)\n const untilMatch = rule.match(/UNTIL=(\\d{8})/)\n if (countMatch) {\n setRecurrenceEndType('count')\n setRecurrenceCount(Number(countMatch[1]))\n } else if (untilMatch) {\n setRecurrenceEndType('date')\n const raw = untilMatch[1]\n setRecurrenceEndDate(`${raw.slice(0, 4)}-${raw.slice(4, 6)}-${raw.slice(6, 8)}`)\n } else {\n setRecurrenceEndType('never')\n }\n } else {\n setRecurrenceEnabled(false)\n }\n if (editData.guestPermissions) {\n setGuestPermissions({\n canInviteOthers: editData.guestPermissions.canInviteOthers ?? true,\n canModify: editData.guestPermissions.canModify ?? false,\n canSeeList: editData.guestPermissions.canSeeList ?? true,\n })\n }\n } else {\n // Create mode: reset all fields\n const defaultStart = resolveDefaultActivityStart('meeting', new Date())\n setActivityType('meeting')\n setTitle('')\n setDate(formatLocalDateInput(defaultStart))\n setStartTime(formatLocalTimeInput(defaultStart))\n setDuration(30)\n setAllDay(false)\n setDescription('')\n setLocation('')\n setReminderMinutes(DEFAULT_REMINDER_MINUTES.meeting)\n setVisibility('team')\n setParticipants([])\n setLinkedEntities([])\n setRecurrenceEnabled(false)\n }\n setConflict(null)\n }\n return () => {\n // Safety net: restore body scroll if Radix Dialog fails to clean up\n document.body.style.removeProperty('overflow')\n document.body.style.removeProperty('pointer-events')\n }\n }, [open, editData, isEditing])\n\n // Update the Reminder default when the activity type changes in create mode.\n // Skipped in edit mode (the persisted value wins), and gated by `open` to\n // avoid flipping the default in a closed-but-mounted dialog.\n const lastReminderTypeRef = React.useRef<string>('meeting')\n React.useEffect(() => {\n if (!open || isEditing) {\n lastReminderTypeRef.current = activityType\n return\n }\n if (lastReminderTypeRef.current === activityType) return\n lastReminderTypeRef.current = activityType\n setReminderMinutes(DEFAULT_REMINDER_MINUTES[resolveScheduleActivityType(activityType)])\n }, [activityType, isEditing, open])\n\n const removeParticipant = React.useCallback((index: number) => {\n setParticipants((prev) => prev.filter((_, i) => i !== index))\n }, [])\n\n const toggleRecurrenceDay = React.useCallback((index: number) => {\n setRecurrenceDays((prev) => {\n const next = [...prev]\n next[index] = !next[index]\n return next\n })\n }, [])\n\n return {\n activityType,\n setActivityType,\n title,\n setTitle,\n date,\n setDate,\n startTime,\n setStartTime,\n duration,\n setDuration,\n allDay,\n setAllDay,\n description,\n setDescription,\n markdownEnabled,\n setMarkdownEnabled,\n location,\n setLocation,\n reminderMinutes,\n setReminderMinutes,\n visibility,\n setVisibility,\n participants,\n setParticipants,\n linkedEntities,\n setLinkedEntities,\n recurrenceEnabled,\n setRecurrenceEnabled,\n recurrenceDays,\n setRecurrenceDays,\n recurrenceEndType,\n setRecurrenceEndType,\n recurrenceCount,\n setRecurrenceCount,\n recurrenceEndDate,\n setRecurrenceEndDate,\n conflict,\n setConflict,\n saving,\n setSaving,\n guestPermissions,\n setGuestPermissions,\n removeParticipant,\n toggleRecurrenceDay,\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.10.1-develop.
|
|
3
|
+
"version": "0.10.1-develop.7670.1.7b5ad74924",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -265,16 +265,16 @@
|
|
|
265
265
|
"zod": "^4.4.3"
|
|
266
266
|
},
|
|
267
267
|
"peerDependencies": {
|
|
268
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
269
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
270
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
268
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7670.1.7b5ad74924",
|
|
269
|
+
"@open-mercato/shared": "0.10.1-develop.7670.1.7b5ad74924",
|
|
270
|
+
"@open-mercato/ui": "0.10.1-develop.7670.1.7b5ad74924",
|
|
271
271
|
"react": "^19.0.0",
|
|
272
272
|
"react-dom": "^19.0.0"
|
|
273
273
|
},
|
|
274
274
|
"devDependencies": {
|
|
275
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
276
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
277
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
275
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7670.1.7b5ad74924",
|
|
276
|
+
"@open-mercato/shared": "0.10.1-develop.7670.1.7b5ad74924",
|
|
277
|
+
"@open-mercato/ui": "0.10.1-develop.7670.1.7b5ad74924",
|
|
278
278
|
"@testing-library/dom": "^10.4.1",
|
|
279
279
|
"@testing-library/jest-dom": "^7.0.1",
|
|
280
280
|
"@testing-library/react": "^16.3.3",
|
|
@@ -48,7 +48,8 @@ import {
|
|
|
48
48
|
INTERACTION_STATUS_COMPLETED,
|
|
49
49
|
INTERACTION_STATUS_PLANNED,
|
|
50
50
|
} from '../lib/interactionStatus'
|
|
51
|
-
import { canChangeEmailVisibility } from '../lib/visibilityFilter'
|
|
51
|
+
import { canChangeEmailVisibility, isEmailHiddenFrom } from '../lib/visibilityFilter'
|
|
52
|
+
import { listGrantsForViewerOnPerson, listSharedChannelIds } from '../lib/conversationShares'
|
|
52
53
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
53
54
|
import { loadCustomFieldDefinitionIndex } from '@open-mercato/shared/lib/crud/custom-fields'
|
|
54
55
|
import type { OrganizationHierarchyService } from '@open-mercato/shared/lib/auth/principal-service'
|
|
@@ -139,9 +140,77 @@ const INTERACTION_UNDO_DATE_OPTIONS = {
|
|
|
139
140
|
datePaths: ['before.interaction.scheduledAt', 'before.interaction.occurredAt', 'before.interaction.recurrenceEnd'],
|
|
140
141
|
}
|
|
141
142
|
|
|
142
|
-
async function
|
|
143
|
+
async function ensureInteractionMutationAccess(
|
|
144
|
+
ctx: CommandRuntimeContext,
|
|
145
|
+
em: EntityManager,
|
|
146
|
+
interaction: CustomerInteraction | (InteractionSnapshot['interaction'] & { channelId?: string | null }),
|
|
147
|
+
): Promise<void> {
|
|
148
|
+
ensureTenantScope(ctx, interaction.tenantId)
|
|
149
|
+
ensureOrganizationScope(ctx, interaction.organizationId)
|
|
150
|
+
const currentUserId = ctx.auth?.isApiKey ? null : ctx.auth?.sub ?? null
|
|
151
|
+
const personEntityId = 'entityId' in interaction
|
|
152
|
+
? interaction.entityId
|
|
153
|
+
: typeof interaction.entity === 'string' ? interaction.entity : interaction.entity.id
|
|
154
|
+
const visibility = {
|
|
155
|
+
interactionType: interaction.interactionType,
|
|
156
|
+
visibility: interaction.visibility,
|
|
157
|
+
authorUserId: interaction.authorUserId,
|
|
158
|
+
currentUserId,
|
|
159
|
+
personEntityId,
|
|
160
|
+
channelId: interaction.channelId,
|
|
161
|
+
}
|
|
162
|
+
if (!isEmailHiddenFrom(visibility)) return
|
|
163
|
+
const scope = { tenantId: interaction.tenantId, organizationId: interaction.organizationId }
|
|
164
|
+
const sharedConversations = await listGrantsForViewerOnPerson(em, scope, currentUserId, personEntityId)
|
|
165
|
+
const sharedChannelIds = await listSharedChannelIds(em, scope, currentUserId)
|
|
166
|
+
if (isEmailHiddenFrom({ ...visibility, sharedConversations, sharedChannelIds })) {
|
|
167
|
+
const { translate } = await resolveTranslations()
|
|
168
|
+
throw notFound(translate('customers.errors.entity_not_found', 'Entity not found'))
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function ensureInteractionPrivacyChangeAccess(
|
|
173
|
+
ctx: CommandRuntimeContext,
|
|
174
|
+
interaction: CustomerInteraction,
|
|
175
|
+
next: { interactionType?: string; authorUserId?: string | null; visibility?: string | null },
|
|
176
|
+
): Promise<void> {
|
|
177
|
+
if (interaction.interactionType !== 'email') return
|
|
178
|
+
const actorUserId = ctx.auth?.isApiKey ? null : ctx.auth?.sub ?? null
|
|
179
|
+
const changesOwnership = next.authorUserId !== undefined && (next.authorUserId ?? null) !== (interaction.authorUserId ?? null)
|
|
180
|
+
const changesType = next.interactionType !== undefined && next.interactionType !== interaction.interactionType
|
|
181
|
+
if (
|
|
182
|
+
((changesOwnership || changesType) && (!actorUserId || actorUserId !== interaction.authorUserId)) ||
|
|
183
|
+
(next.visibility !== undefined && !canChangeEmailVisibility({
|
|
184
|
+
interactionType: interaction.interactionType,
|
|
185
|
+
currentVisibility: interaction.visibility,
|
|
186
|
+
nextVisibility: next.visibility,
|
|
187
|
+
authorUserId: interaction.authorUserId,
|
|
188
|
+
actorUserId,
|
|
189
|
+
userFeatures: undefined,
|
|
190
|
+
}))
|
|
191
|
+
) {
|
|
192
|
+
const { translate } = await resolveTranslations()
|
|
193
|
+
throw notFound(translate('customers.errors.entity_not_found', 'Entity not found'))
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function ensureInteractionRestoreAccess(
|
|
198
|
+
ctx: CommandRuntimeContext,
|
|
199
|
+
em: EntityManager,
|
|
200
|
+
current: CustomerInteraction | null,
|
|
201
|
+
restored: InteractionSnapshot['interaction'],
|
|
202
|
+
): Promise<void> {
|
|
203
|
+
if (current) {
|
|
204
|
+
await ensureInteractionMutationAccess(ctx, em, current)
|
|
205
|
+
await ensureInteractionPrivacyChangeAccess(ctx, current, { ...restored, visibility: restored.visibility ?? null })
|
|
206
|
+
}
|
|
207
|
+
await ensureInteractionMutationAccess(ctx, em, { ...restored, channelId: current?.channelId })
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function loadInteractionSnapshot(em: EntityManager, id: string, ctx?: CommandRuntimeContext): Promise<InteractionSnapshot | null> {
|
|
143
211
|
const interaction = await findOneWithDecryption(em, CustomerInteraction, { id }, { populate: ['entity'] })
|
|
144
212
|
if (!interaction) return null
|
|
213
|
+
if (ctx) await ensureInteractionMutationAccess(ctx, em, interaction)
|
|
145
214
|
const custom = await loadCustomFieldSnapshot(em, {
|
|
146
215
|
entityId: INTERACTION_ENTITY_ID,
|
|
147
216
|
recordId: interaction.id,
|
|
@@ -619,6 +688,7 @@ const createInteractionCommand: CommandHandler<InteractionCreateInput, { interac
|
|
|
619
688
|
const result = await runInTransaction(em, async (trx) => {
|
|
620
689
|
const record = await findOneWithDecryption(trx, CustomerInteraction, { id: interactionId })
|
|
621
690
|
if (!record) return null
|
|
691
|
+
await ensureInteractionMutationAccess(ctx, trx, record)
|
|
622
692
|
const entityId = typeof record.entity === 'string' ? record.entity : record.entity.id
|
|
623
693
|
trx.remove(record)
|
|
624
694
|
await trx.flush()
|
|
@@ -648,6 +718,7 @@ const createInteractionCommand: CommandHandler<InteractionCreateInput, { interac
|
|
|
648
718
|
const { interaction, nextInteractionId } = await runInTransaction(em, async (trx) => {
|
|
649
719
|
const entity = await requireTimelineParentEntity(trx, after.interaction.entityId, { tenantId: after.interaction.tenantId, organizationId: after.interaction.organizationId })
|
|
650
720
|
let interaction = await findOneWithDecryption(trx, CustomerInteraction, { id: after.interaction.id })
|
|
721
|
+
await ensureInteractionRestoreAccess(ctx, trx, interaction, after.interaction)
|
|
651
722
|
if (!interaction) {
|
|
652
723
|
interaction = buildInteractionGraph(trx, {
|
|
653
724
|
id: after.interaction.id,
|
|
@@ -760,7 +831,7 @@ const createInteractionCommand: CommandHandler<InteractionCreateInput, { interac
|
|
|
760
831
|
async function prepareUpdateInteractionSnapshot(rawInput: InteractionUpdateInput, ctx: CommandRuntimeContext) {
|
|
761
832
|
const { parsed } = parseWithCustomFields(interactionUpdateSchema, rawInput)
|
|
762
833
|
const em = ctx.transactionalEm ?? (ctx.container.resolve('em') as EntityManager).fork()
|
|
763
|
-
const snapshot = await loadInteractionSnapshot(em, parsed.id)
|
|
834
|
+
const snapshot = await loadInteractionSnapshot(em, parsed.id, ctx)
|
|
764
835
|
return snapshot ? { before: snapshot } : {}
|
|
765
836
|
}
|
|
766
837
|
|
|
@@ -779,6 +850,7 @@ const updateInteractionCommand: CommandHandler<InteractionUpdateInput, { interac
|
|
|
779
850
|
}
|
|
780
851
|
ensureTenantScope(ctx, interaction.tenantId)
|
|
781
852
|
ensureOrganizationScope(ctx, interaction.organizationId)
|
|
853
|
+
await ensureInteractionMutationAccess(ctx, trx, interaction)
|
|
782
854
|
|
|
783
855
|
// Concurrent-edit guard for command-driven callers (e.g. the legacy
|
|
784
856
|
// /api/customers/todos route, which bypasses the makeCrudRoute lock guard):
|
|
@@ -792,6 +864,8 @@ const updateInteractionCommand: CommandHandler<InteractionUpdateInput, { interac
|
|
|
792
864
|
request: ctx.request ?? null,
|
|
793
865
|
})
|
|
794
866
|
|
|
867
|
+
await ensureInteractionPrivacyChangeAccess(ctx, interaction, parsed)
|
|
868
|
+
|
|
795
869
|
const nextType = parsed.interactionType ?? interaction.interactionType
|
|
796
870
|
const changingType = nextType !== interaction.interactionType
|
|
797
871
|
const enforceSelectableType = requestsSelectableTypeEnforcement(rawInput)
|
|
@@ -870,35 +944,6 @@ const updateInteractionCommand: CommandHandler<InteractionUpdateInput, { interac
|
|
|
870
944
|
: Object.fromEntries(Object.entries(custom).filter(([key]) =>
|
|
871
945
|
!customKeys.defined.has(key.toLowerCase()) || customKeys.allowed.has(key.toLowerCase())))
|
|
872
946
|
|
|
873
|
-
// Email visibility is an access-controlled field: only the interaction's
|
|
874
|
-
// author may change a
|
|
875
|
-
// private email's visibility (mirrors the dedicated PATCH .../visibility
|
|
876
|
-
// route). Enforce it here — the single persistence path — so the generic
|
|
877
|
-
// update route (PUT /api/interactions) cannot bypass the gate. Evaluated
|
|
878
|
-
// against the row's pre-mutation author/type. 404 (not 403) keeps the
|
|
879
|
-
// existence-masking consistent with the dedicated route.
|
|
880
|
-
if (
|
|
881
|
-
parsed.visibility !== undefined &&
|
|
882
|
-
interaction.interactionType === 'email' &&
|
|
883
|
-
(parsed.visibility ?? null) !== (interaction.visibility ?? null)
|
|
884
|
-
) {
|
|
885
|
-
const actorUserId = (ctx.auth as { sub?: string | null } | null)?.sub ?? null
|
|
886
|
-
if (
|
|
887
|
-
!canChangeEmailVisibility({
|
|
888
|
-
interactionType: interaction.interactionType,
|
|
889
|
-
currentVisibility: interaction.visibility,
|
|
890
|
-
nextVisibility: parsed.visibility,
|
|
891
|
-
authorUserId: interaction.authorUserId,
|
|
892
|
-
actorUserId,
|
|
893
|
-
// v1 strict owner-only: only the author may flip visibility; no admin
|
|
894
|
-
// bypass (canChangeEmailVisibility ignores caller features in v1).
|
|
895
|
-
userFeatures: undefined,
|
|
896
|
-
})
|
|
897
|
-
) {
|
|
898
|
-
throw notFound('Email not found')
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
|
|
902
947
|
// Re-link to a different person/company (#5938). Resolved before any scalar
|
|
903
948
|
// mutation below so the lookup never lands between a mutation and `trx.flush()`
|
|
904
949
|
// (packages/core/AGENTS.md → Entity Update Safety). `requireTimelineParentEntity`
|
|
@@ -1039,6 +1084,7 @@ const updateInteractionCommand: CommandHandler<InteractionUpdateInput, { interac
|
|
|
1039
1084
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
1040
1085
|
const { interaction, projections } = await runInTransaction(em, async (trx) => {
|
|
1041
1086
|
let interaction = await findOneWithDecryption(trx, CustomerInteraction, { id: before.interaction.id })
|
|
1087
|
+
await ensureInteractionRestoreAccess(ctx, trx, interaction, before.interaction)
|
|
1042
1088
|
const entity = await requireTimelineParentEntity(trx, before.interaction.entityId, { tenantId: before.interaction.tenantId, organizationId: before.interaction.organizationId })
|
|
1043
1089
|
|
|
1044
1090
|
if (!interaction) {
|
|
@@ -1166,7 +1212,7 @@ const completeInteractionCommand: CommandHandler<InteractionCompleteInput, { int
|
|
|
1166
1212
|
async prepare(rawInput, ctx) {
|
|
1167
1213
|
const parsed = interactionCompleteSchema.parse(rawInput)
|
|
1168
1214
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
1169
|
-
const snapshot = await loadInteractionSnapshot(em, parsed.id)
|
|
1215
|
+
const snapshot = await loadInteractionSnapshot(em, parsed.id, ctx)
|
|
1170
1216
|
return snapshot ? { before: snapshot } : {}
|
|
1171
1217
|
},
|
|
1172
1218
|
async execute(rawInput, ctx) {
|
|
@@ -1180,6 +1226,7 @@ const completeInteractionCommand: CommandHandler<InteractionCompleteInput, { int
|
|
|
1180
1226
|
}
|
|
1181
1227
|
ensureTenantScope(ctx, interaction.tenantId)
|
|
1182
1228
|
ensureOrganizationScope(ctx, interaction.organizationId)
|
|
1229
|
+
await ensureInteractionMutationAccess(ctx, trx, interaction)
|
|
1183
1230
|
|
|
1184
1231
|
await enforceCommandOptimisticLockWithGuards(ctx.container, {
|
|
1185
1232
|
resourceKind: 'customers.interaction',
|
|
@@ -1260,6 +1307,7 @@ const completeInteractionCommand: CommandHandler<InteractionCompleteInput, { int
|
|
|
1260
1307
|
const result = await runInTransaction(em, async (trx) => {
|
|
1261
1308
|
const interaction = await findOneWithDecryption(trx, CustomerInteraction, { id: before.interaction.id })
|
|
1262
1309
|
if (!interaction) return null
|
|
1310
|
+
await ensureInteractionMutationAccess(ctx, trx, interaction)
|
|
1263
1311
|
|
|
1264
1312
|
interaction.status = before.interaction.status
|
|
1265
1313
|
interaction.occurredAt = before.interaction.occurredAt
|
|
@@ -1306,7 +1354,7 @@ const cancelInteractionCommand: CommandHandler<InteractionCancelInput, { interac
|
|
|
1306
1354
|
async prepare(rawInput, ctx) {
|
|
1307
1355
|
const parsed = interactionCancelSchema.parse(rawInput)
|
|
1308
1356
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
1309
|
-
const snapshot = await loadInteractionSnapshot(em, parsed.id)
|
|
1357
|
+
const snapshot = await loadInteractionSnapshot(em, parsed.id, ctx)
|
|
1310
1358
|
return snapshot ? { before: snapshot } : {}
|
|
1311
1359
|
},
|
|
1312
1360
|
async execute(rawInput, ctx) {
|
|
@@ -1320,6 +1368,7 @@ const cancelInteractionCommand: CommandHandler<InteractionCancelInput, { interac
|
|
|
1320
1368
|
}
|
|
1321
1369
|
ensureTenantScope(ctx, interaction.tenantId)
|
|
1322
1370
|
ensureOrganizationScope(ctx, interaction.organizationId)
|
|
1371
|
+
await ensureInteractionMutationAccess(ctx, trx, interaction)
|
|
1323
1372
|
|
|
1324
1373
|
await enforceCommandOptimisticLockWithGuards(ctx.container, {
|
|
1325
1374
|
resourceKind: 'customers.interaction',
|
|
@@ -1398,6 +1447,7 @@ const cancelInteractionCommand: CommandHandler<InteractionCancelInput, { interac
|
|
|
1398
1447
|
const result = await runInTransaction(em, async (trx) => {
|
|
1399
1448
|
const interaction = await findOneWithDecryption(trx, CustomerInteraction, { id: before.interaction.id })
|
|
1400
1449
|
if (!interaction) return null
|
|
1450
|
+
await ensureInteractionMutationAccess(ctx, trx, interaction)
|
|
1401
1451
|
|
|
1402
1452
|
interaction.status = before.interaction.status
|
|
1403
1453
|
await trx.flush()
|
|
@@ -1444,7 +1494,7 @@ const deleteInteractionCommand: CommandHandler<{ body?: Record<string, unknown>;
|
|
|
1444
1494
|
async prepare(input, ctx) {
|
|
1445
1495
|
const id = requireId(input, 'Interaction id required')
|
|
1446
1496
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
1447
|
-
const snapshot = await loadInteractionSnapshot(em, id)
|
|
1497
|
+
const snapshot = await loadInteractionSnapshot(em, id, ctx)
|
|
1448
1498
|
return snapshot ? { before: snapshot } : {}
|
|
1449
1499
|
},
|
|
1450
1500
|
async execute(input, ctx) {
|
|
@@ -1458,6 +1508,7 @@ const deleteInteractionCommand: CommandHandler<{ body?: Record<string, unknown>;
|
|
|
1458
1508
|
}
|
|
1459
1509
|
ensureTenantScope(ctx, interaction.tenantId)
|
|
1460
1510
|
ensureOrganizationScope(ctx, interaction.organizationId)
|
|
1511
|
+
await ensureInteractionMutationAccess(ctx, trx, interaction)
|
|
1461
1512
|
|
|
1462
1513
|
await enforceCommandOptimisticLockWithGuards(ctx.container, {
|
|
1463
1514
|
resourceKind: 'customers.interaction',
|
|
@@ -1523,6 +1574,7 @@ const deleteInteractionCommand: CommandHandler<{ body?: Record<string, unknown>;
|
|
|
1523
1574
|
const { interaction, nextInteractionId } = await runInTransaction(em, async (trx) => {
|
|
1524
1575
|
const entity = await requireTimelineParentEntity(trx, before.interaction.entityId, { tenantId: before.interaction.tenantId, organizationId: before.interaction.organizationId })
|
|
1525
1576
|
let interaction = await findOneWithDecryption(trx, CustomerInteraction, { id: before.interaction.id })
|
|
1577
|
+
await ensureInteractionRestoreAccess(ctx, trx, interaction, before.interaction)
|
|
1526
1578
|
if (!interaction) {
|
|
1527
1579
|
interaction = trx.create(CustomerInteraction, {
|
|
1528
1580
|
id: before.interaction.id,
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
isDateRequired,
|
|
25
25
|
isTimeRequired,
|
|
26
26
|
getFieldLabel,
|
|
27
|
+
resolveScheduleActivityType,
|
|
27
28
|
DateTimeFields,
|
|
28
29
|
ParticipantsField,
|
|
29
30
|
LocationField,
|
|
@@ -141,10 +142,16 @@ export function ScheduleActivityDialog({
|
|
|
141
142
|
}: ScheduleActivityDialogProps) {
|
|
142
143
|
const t = useT()
|
|
143
144
|
const state = useScheduleFormState({ open, editData: editData ?? null })
|
|
144
|
-
|
|
145
|
+
// `state.activityType` is the raw persisted/selected key — may be a built-in
|
|
146
|
+
// Event, a custom dictionary key, or a historical key outside this dialog's
|
|
147
|
+
// five legacy kinds (#7486). `presentationType` resolves it to a safe key for
|
|
148
|
+
// every lookup into the five-kind maps below; the save payload keeps the raw
|
|
149
|
+
// value so an unsupported type is never silently rewritten.
|
|
150
|
+
const presentationType = resolveScheduleActivityType(state.activityType)
|
|
151
|
+
const visibleFields = FIELD_VISIBILITY[presentationType]
|
|
145
152
|
const { confirm, ConfirmDialogElement } = useConfirmDialog()
|
|
146
153
|
const isEditing = Boolean(editData?.id)
|
|
147
|
-
const chrome = TYPE_CHROME[
|
|
154
|
+
const chrome = TYPE_CHROME[presentationType]
|
|
148
155
|
const SaveIcon = chrome.saveIcon
|
|
149
156
|
const [callDirection, setCallDirection] = React.useState<'outbound' | 'inbound'>('outbound')
|
|
150
157
|
const [callOutcome, setCallOutcome] = React.useState<string | null>(null)
|
|
@@ -369,8 +376,8 @@ export function ScheduleActivityDialog({
|
|
|
369
376
|
const trimmedCallPhone = callPhoneNumber.trim()
|
|
370
377
|
// Only calendar-bound types must be scheduled; a task may stay an undated
|
|
371
378
|
// backlog item, so blocking its save on an empty date was wrong (#5941).
|
|
372
|
-
const isDateMissing = isDateRequired(
|
|
373
|
-
const isTimeMissing = isTimeRequired(
|
|
379
|
+
const isDateMissing = isDateRequired(presentationType) && !trimmedDate
|
|
380
|
+
const isTimeMissing = isTimeRequired(presentationType) && !state.allDay && !trimmedStartTime
|
|
374
381
|
const isSubmitDisabled =
|
|
375
382
|
state.saving ||
|
|
376
383
|
!state.title.trim() ||
|
|
@@ -388,7 +395,7 @@ export function ScheduleActivityDialog({
|
|
|
388
395
|
return
|
|
389
396
|
}
|
|
390
397
|
let phoneNumberForPayload: string | undefined
|
|
391
|
-
if (
|
|
398
|
+
if (presentationType === 'call' && trimmedCallPhone) {
|
|
392
399
|
const phoneValidation = validatePhoneNumber(trimmedCallPhone)
|
|
393
400
|
if (!phoneValidation.valid) {
|
|
394
401
|
setCallPhoneError(callPhoneInvalidMessage)
|
|
@@ -417,7 +424,7 @@ export function ScheduleActivityDialog({
|
|
|
417
424
|
|
|
418
425
|
const isSaveEdit = Boolean(editData?.id)
|
|
419
426
|
const customValues: Record<string, unknown> = {}
|
|
420
|
-
if (
|
|
427
|
+
if (presentationType === 'call') {
|
|
421
428
|
customValues.callDirection = callDirection
|
|
422
429
|
if (callOutcome) customValues.callOutcome = callOutcome
|
|
423
430
|
if (phoneNumberForPayload) customValues.callPhoneNumber = phoneNumberForPayload
|
|
@@ -440,10 +447,10 @@ export function ScheduleActivityDialog({
|
|
|
440
447
|
...(isSaveEdit ? {} : { status: 'planned' }),
|
|
441
448
|
date: trimmedDate || null,
|
|
442
449
|
time: trimmedDate ? timeForPayload || null : null,
|
|
443
|
-
phoneNumber:
|
|
450
|
+
phoneNumber: presentationType === 'call' ? phoneNumberForPayload : undefined,
|
|
444
451
|
// Only tasks expose the priority control, so other types leave the column
|
|
445
452
|
// untouched rather than clearing it on a type switch (#5943).
|
|
446
|
-
priority:
|
|
453
|
+
priority: presentationType === 'task' ? buildTaskPriorityPayload(taskPriority, seededTaskPriority) : undefined,
|
|
447
454
|
scheduledAt,
|
|
448
455
|
durationMinutes: visibleFields.has('duration') && !state.allDay ? state.duration : null,
|
|
449
456
|
location: visibleFields.has('location') ? (state.location.trim() || null) : null,
|
|
@@ -498,7 +505,7 @@ export function ScheduleActivityDialog({
|
|
|
498
505
|
}
|
|
499
506
|
const { message, fieldErrors } = mapCrudServerErrorToFormErrors(err)
|
|
500
507
|
const phoneFieldError = fieldErrors?.phoneNumber
|
|
501
|
-
if (
|
|
508
|
+
if (presentationType === 'call' && phoneFieldError) {
|
|
502
509
|
const translatedPhoneError = translateErrorMessage(phoneFieldError, callPhoneInvalidMessage)
|
|
503
510
|
setCallPhoneError(translatedPhoneError)
|
|
504
511
|
flash(translatedPhoneError, 'error')
|
|
@@ -511,7 +518,7 @@ export function ScheduleActivityDialog({
|
|
|
511
518
|
} finally {
|
|
512
519
|
state.setSaving(false)
|
|
513
520
|
}
|
|
514
|
-
}, [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, seededTaskPriority, state.startTime, t, taskPriority, state.title, translateErrorMessage, trimmedCallPhone, trimmedDate, trimmedStartTime, state.visibility, visibleFields]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
521
|
+
}, [callDirection, callOutcome, callPhoneInvalidMessage, callPhoneNumber, isDateMissing, isTimeMissing, presentationType, 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, seededTaskPriority, state.startTime, t, taskPriority, state.title, translateErrorMessage, trimmedCallPhone, trimmedDate, trimmedStartTime, state.visibility, visibleFields]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
515
522
|
|
|
516
523
|
const handleKeyDown = useDialogKeyHandler({ onConfirm: handleSave })
|
|
517
524
|
|
|
@@ -588,14 +595,14 @@ export function ScheduleActivityDialog({
|
|
|
588
595
|
{/* Title */}
|
|
589
596
|
<div className="flex flex-col gap-1.5">
|
|
590
597
|
<label className="text-overline font-semibold text-muted-foreground tracking-wider">
|
|
591
|
-
{getFieldLabel(
|
|
598
|
+
{getFieldLabel(presentationType, 'title', t, 'customers.schedule.titleLabel', 'Title')}
|
|
592
599
|
</label>
|
|
593
600
|
<input
|
|
594
601
|
type="text"
|
|
595
602
|
value={state.title}
|
|
596
603
|
onChange={(e) => state.setTitle(e.target.value)}
|
|
597
604
|
placeholder={
|
|
598
|
-
|
|
605
|
+
presentationType === 'email'
|
|
599
606
|
? t('customers.schedule.subjectPlaceholder', 'Subject...')
|
|
600
607
|
: t('customers.schedule.titlePlaceholder', 'Activity title...')
|
|
601
608
|
}
|
|
@@ -608,7 +615,7 @@ export function ScheduleActivityDialog({
|
|
|
608
615
|
workflows match Figma 829:50 / 790:280 (date row first, then status chips). */}
|
|
609
616
|
<DateTimeFields
|
|
610
617
|
visible={visibleFields}
|
|
611
|
-
activityType={
|
|
618
|
+
activityType={presentationType}
|
|
612
619
|
date={state.date}
|
|
613
620
|
setDate={state.setDate}
|
|
614
621
|
startTime={state.startTime}
|
|
@@ -630,7 +637,7 @@ export function ScheduleActivityDialog({
|
|
|
630
637
|
/>
|
|
631
638
|
|
|
632
639
|
{/* Call: Direction + Outcome chips */}
|
|
633
|
-
{
|
|
640
|
+
{presentationType === 'call' && (
|
|
634
641
|
<div className="flex flex-col gap-3">
|
|
635
642
|
<div>
|
|
636
643
|
<label className="text-overline font-semibold uppercase text-muted-foreground tracking-wider">
|
|
@@ -690,7 +697,7 @@ export function ScheduleActivityDialog({
|
|
|
690
697
|
)}
|
|
691
698
|
|
|
692
699
|
{/* Task: Priority chips */}
|
|
693
|
-
{
|
|
700
|
+
{presentationType === 'task' && (
|
|
694
701
|
<div>
|
|
695
702
|
<label className="text-overline font-semibold uppercase text-muted-foreground tracking-wider">
|
|
696
703
|
{t('customers.schedule.task.priorityLabel', 'Priority')}
|
|
@@ -723,7 +730,7 @@ export function ScheduleActivityDialog({
|
|
|
723
730
|
{/* Participants */}
|
|
724
731
|
<ParticipantsField
|
|
725
732
|
visible={visibleFields}
|
|
726
|
-
activityType={
|
|
733
|
+
activityType={presentationType}
|
|
727
734
|
participants={state.participants}
|
|
728
735
|
setParticipants={state.setParticipants}
|
|
729
736
|
removeParticipant={state.removeParticipant}
|
|
@@ -732,7 +739,7 @@ export function ScheduleActivityDialog({
|
|
|
732
739
|
/>
|
|
733
740
|
|
|
734
741
|
{/* Location (or phone number for calls) */}
|
|
735
|
-
{
|
|
742
|
+
{presentationType === 'call' ? (
|
|
736
743
|
<div className="flex flex-col gap-1.5">
|
|
737
744
|
<label htmlFor="schedule-call-phone" className="text-overline font-semibold uppercase text-muted-foreground tracking-wider">
|
|
738
745
|
{t('customers.schedule.call.phoneLabel', 'Phone number')}
|
|
@@ -751,7 +758,7 @@ export function ScheduleActivityDialog({
|
|
|
751
758
|
) : (
|
|
752
759
|
<LocationField
|
|
753
760
|
visible={visibleFields}
|
|
754
|
-
activityType={
|
|
761
|
+
activityType={presentationType}
|
|
755
762
|
location={state.location}
|
|
756
763
|
setLocation={state.setLocation}
|
|
757
764
|
/>
|
|
@@ -760,7 +767,7 @@ export function ScheduleActivityDialog({
|
|
|
760
767
|
{/* Linked Entities */}
|
|
761
768
|
<LinkedEntitiesField
|
|
762
769
|
visible={visibleFields}
|
|
763
|
-
activityType={
|
|
770
|
+
activityType={presentationType}
|
|
764
771
|
linkedEntities={state.linkedEntities}
|
|
765
772
|
setLinkedEntities={state.setLinkedEntities}
|
|
766
773
|
/>
|
|
@@ -768,7 +775,7 @@ export function ScheduleActivityDialog({
|
|
|
768
775
|
{/* Description */}
|
|
769
776
|
<div>
|
|
770
777
|
<label className="text-overline font-semibold uppercase text-muted-foreground tracking-wider">
|
|
771
|
-
{getFieldLabel(
|
|
778
|
+
{getFieldLabel(presentationType, 'description', t, 'customers.schedule.description', 'Description')}
|
|
772
779
|
</label>
|
|
773
780
|
<div className="mt-[8px]">
|
|
774
781
|
<SwitchableMarkdownInput
|
|
@@ -777,12 +784,12 @@ export function ScheduleActivityDialog({
|
|
|
777
784
|
isMarkdownEnabled={state.markdownEnabled}
|
|
778
785
|
// Email bodies are plain text from other mail clients, not Markdown; `<address>`
|
|
779
786
|
// or `<url>` tokens in them break the MDX editor (#5903).
|
|
780
|
-
disableMarkdown={
|
|
787
|
+
disableMarkdown={presentationType === 'email'}
|
|
781
788
|
// The plain textarea defaults to 3 hidden-overflow rows, which
|
|
782
789
|
// would cut off quoted replies; give email bodies room and a scrollbar.
|
|
783
|
-
rows={
|
|
790
|
+
rows={presentationType === 'email' ? 8 : undefined}
|
|
784
791
|
textareaClassName={
|
|
785
|
-
|
|
792
|
+
presentationType === 'email'
|
|
786
793
|
? 'w-full resize-y overflow-y-auto rounded-lg border border-muted-foreground/20 bg-background px-3 py-2 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring'
|
|
787
794
|
: undefined
|
|
788
795
|
}
|
|
@@ -795,7 +802,7 @@ export function ScheduleActivityDialog({
|
|
|
795
802
|
{/* Reminder + Visibility */}
|
|
796
803
|
<FooterFields
|
|
797
804
|
visible={visibleFields}
|
|
798
|
-
activityType={
|
|
805
|
+
activityType={presentationType}
|
|
799
806
|
reminderMinutes={state.reminderMinutes}
|
|
800
807
|
setReminderMinutes={state.setReminderMinutes}
|
|
801
808
|
visibility={state.visibility}
|
|
@@ -16,6 +16,22 @@ export type ScheduleFieldId =
|
|
|
16
16
|
| 'reminder'
|
|
17
17
|
| 'visibility'
|
|
18
18
|
|
|
19
|
+
const KNOWN_ACTIVITY_TYPES: ReadonlySet<string> = new Set(['meeting', 'call', 'task', 'email', 'note'])
|
|
20
|
+
|
|
21
|
+
export function isKnownActivityType(type: string): type is ActivityType {
|
|
22
|
+
return KNOWN_ACTIVITY_TYPES.has(type)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// The built-in Event type and any custom or historical dictionary key the backend
|
|
26
|
+
// accepts fall outside this dialog's five legacy presentation kinds (#7486) — this
|
|
27
|
+
// dialog predates the configurable calendar event type catalog. Resolve those to
|
|
28
|
+
// Meeting's chrome/field set, the closest and broadest safe default, while callers
|
|
29
|
+
// keep the exact persisted key (`editData.interactionType` / the save payload)
|
|
30
|
+
// untouched so the stored type is never silently rewritten.
|
|
31
|
+
export function resolveScheduleActivityType(type: string): ActivityType {
|
|
32
|
+
return isKnownActivityType(type) ? type : 'meeting'
|
|
33
|
+
}
|
|
34
|
+
|
|
19
35
|
export const FIELD_VISIBILITY: Record<ActivityType, Set<ScheduleFieldId>> = {
|
|
20
36
|
meeting: new Set([
|
|
21
37
|
'title', 'date', 'startTime', 'duration', 'allDay', 'timezone', 'recurrence',
|