@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.
@@ -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 resolvedType = editData.interactionType ?? "meeting";
78
- setActivityType(resolvedType);
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": "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",
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": "AAAA,YAAY,WAAW;AAEvB,SAAS,gBAAgB,mCAAmC;AAsDrD,MAAM,qBAAqB;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAOA,MAAM,2BAAyD;AAAA,EAC7D,SAAS;AAAA,EACT,MAAM;AAAA,EACN,MAAM;AAAA,EACN,OAAO;AAAA,EACP,MAAM;AACR;AAMA,MAAM,uBAAuB;AAC7B,MAAM,oBAAoB;AAC1B,MAAM,sBAAsB;AAE5B,SAAS,YAAY,OAAuB;AAC1C,SAAO,OAAO,KAAK,EAAE,SAAS,GAAG,GAAG;AACtC;AAEA,SAAS,eAAe,MAAY,OAAsB;AACxD,SACE,KAAK,YAAY,MAAM,MAAM,YAAY,KACzC,KAAK,SAAS,MAAM,MAAM,SAAS,KACnC,KAAK,QAAQ,MAAM,MAAM,QAAQ;AAErC;AAEA,SAAS,cAAc,KAAiB;AACtC,QAAM,OAAO,IAAI,KAAK,GAAG;AACzB,OAAK,WAAW,GAAG,CAAC;AACpB,OAAK,WAAW,KAAK,WAAW,KAAK,uBAAwB,KAAK,WAAW,IAAI,qBAAsB;AACvG,MAAI,CAAC,eAAe,MAAM,GAAG,GAAG;AAC9B,SAAK,SAAS,qBAAqB,GAAG,GAAG,CAAC;AAAA,EAC5C;AACA,SAAO;AACT;AAMO,SAAS,4BAA4B,MAAoB,KAAiB;AAC/E,MAAI,SAAS,QAAQ;AACnB,UAAM,kBAAkB,IAAI,KAAK,GAAG;AACpC,oBAAgB,SAAS,mBAAmB,GAAG,GAAG,CAAC;AACnD,QAAI,gBAAgB,QAAQ,IAAI,IAAI,QAAQ,EAAG,QAAO;AAAA,EACxD;AACA,SAAO,cAAc,GAAG;AAC1B;AAEA,SAAS,qBAAqB,MAAoB;AAChD,SAAO,GAAG,KAAK,YAAY,CAAC,IAAI,YAAY,KAAK,SAAS,IAAI,CAAC,CAAC,IAAI,YAAY,KAAK,QAAQ,CAAC,CAAC;AACjG;AAEA,SAAS,qBAAqB,MAAoB;AAChD,SAAO,GAAG,YAAY,KAAK,SAAS,CAAC,CAAC,IAAI,YAAY,KAAK,WAAW,CAAC,CAAC;AAC1E;AAOO,SAAS,qBAAqB,EAAE,MAAM,SAAS,GAA+B;AAInF,QAAM,YAAY,QAAQ,UAAU,EAAE;AACtC,QAAM,CAAC,YAAY,IAAI,MAAM,SAAS,MAAM,4BAA4B,WAAW,oBAAI,KAAK,CAAC,CAAC;AAM9F,QAAM,CAAC,cAAc,eAAe,IAAI,MAAM,SAAiB,SAAS;AACxE,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAS,EAAE;AAC3C,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAAS,MAAM,qBAAqB,YAAY,CAAC;AAC/E,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAS,MAAM,qBAAqB,YAAY,CAAC;AACzF,QAAM,CAAC,UAAU,WAAW,IAAI,MAAM,SAAS,EAAE;AACjD,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAAS,KAAK;AAChD,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAAS,EAAE;AACvD,QAAM,CAAC,iBAAiB,kBAAkB,IAAI,MAAM,SAAS,IAAI;AACjE,QAAM,CAAC,UAAU,WAAW,IAAI,MAAM,SAAS,EAAE;AACjD,QAAM,CAAC,iBAAiB,kBAAkB,IAAI,MAAM,SAAS,EAAE;AAC/D,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAAS,MAAM;AACzD,QAAM,CAAC,cAAc,eAAe,IAAI,MAAM,SAAwB,CAAC,CAAC;AACxE,QAAM,CAAC,gBAAgB,iBAAiB,IAAI,MAAM,SAAyB,CAAC,CAAC;AAC7E,QAAM,CAAC,mBAAmB,oBAAoB,IAAI,MAAM,SAAS,KAAK;AACtE,QAAM,CAAC,gBAAgB,iBAAiB,IAAI,MAAM,SAAoB,CAAC,MAAM,OAAO,MAAM,OAAO,OAAO,OAAO,KAAK,CAAC;AACrH,QAAM,CAAC,mBAAmB,oBAAoB,IAAI,MAAM,SAAqC,OAAO;AACpG,QAAM,CAAC,iBAAiB,kBAAkB,IAAI,MAAM,SAAS,CAAC;AAC9D,QAAM,CAAC,mBAAmB,oBAAoB,IAAI,MAAM,SAAS,EAAE;AACnE,QAAM,CAAC,UAAU,WAAW,IAAI,MAAM,SAAwB,IAAI;AAClE,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAAS,KAAK;AAChD,QAAM,CAAC,kBAAkB,mBAAmB,IAAI,MAAM,SAAS,EAAE,iBAAiB,MAAM,WAAW,OAAO,YAAY,KAAK,CAAC;AAE5H,QAAM,UAAU,MAAM;AACpB,QAAI,MAAM;AACR,UAAI,UAAU;AAEZ,cAAM,UAAU,SAAS,mBAAmB;AAC5C,cAAM,eAAe,4BAA4B,OAAO;AACxD,wBAAgB,OAAO;AACvB,iBAAS,SAAS,SAAS,EAAE;AAU7B,cAAM,kBAAkB,SAAS,eAAe,SAAS,cAAc;AACvE,cAAM,WAAW,kBAAkB,IAAI,KAAK,eAAe,IAAI;AAC/D,cAAM,gBAAgB,aAAa,QAAQ,CAAC,OAAO,MAAM,SAAS,QAAQ,CAAC;AAM3E,YAAI,aAAa,CAAC,iBAAiB,CAAC,eAAe,YAAY,GAAG;AAChE,kBAAQ,EAAE;AACV,uBAAa,EAAE;AAAA,QACjB,OAAO;AACL,gBAAM,cAAc,gBAAiB,WAAoB,4BAA4B,cAAc,oBAAI,KAAK,CAAC;AAC7G,kBAAQ,qBAAqB,WAAW,CAAC;AACzC,uBAAa,qBAAqB,WAAW,CAAC;AAAA,QAChD;AACA,oBAAY,SAAS,mBAAmB,EAAE;AAC1C,kBAAU,SAAS,UAAU,KAAK;AAClC,uBAAe,SAAS,QAAQ,EAAE;AAClC,oBAAY,SAAS,YAAY,EAAE;AAGnC,2BAAmB,SAAS,mBAAmB,yBAAyB,YAAY,CAAC;AACrF,sBAAc,SAAS,cAAc,MAAM;AAC3C;AAAA,UACE,MAAM,QAAQ,SAAS,YAAY,IAC/B,SAAS,aAAa,IAAI,CAAC,GAAG,OAAO;AAAA,YACnC,QAAQ,EAAE;AAAA,YACV,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAU;AAAA,YACvC,OAAO,EAAE;AAAA,YACT,OAAO,mBAAmB,IAAI,mBAAmB,MAAM;AAAA,YACvD,QAAS,EAAE,UAAU;AAAA,UACvB,EAAE,IACF,CAAC;AAAA,QACP;AACA;AAAA,UACE,MAAM,QAAQ,SAAS,cAAc,IACjC,SAAS,eAAe,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,MAAM,EAAE,MAA8B,OAAO,EAAE,MAAM,EAAE,IACvG,CAAC;AAAA,QACP;AACA,YAAI,SAAS,gBAAgB;AAC3B,+BAAqB,IAAI;AAEzB,gBAAM,OAAO,SAAS;AACtB,gBAAM,aAAa,KAAK,MAAM,iBAAiB;AAC/C,cAAI,YAAY;AACd,kBAAM,WAAW,CAAC,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,IAAI;AAC1D,kBAAM,aAAa,WAAW,CAAC,EAAE,MAAM,GAAG;AAC1C,8BAAkB,SAAS,IAAI,CAAC,MAAM,WAAW,SAAS,CAAC,CAAC,CAAC;AAAA,UAC/D;AACA,gBAAM,aAAa,KAAK,MAAM,aAAa;AAC3C,gBAAM,aAAa,KAAK,MAAM,eAAe;AAC7C,cAAI,YAAY;AACd,iCAAqB,OAAO;AAC5B,+BAAmB,OAAO,WAAW,CAAC,CAAC,CAAC;AAAA,UAC1C,WAAW,YAAY;AACrB,iCAAqB,MAAM;AAC3B,kBAAM,MAAM,WAAW,CAAC;AACxB,iCAAqB,GAAG,IAAI,MAAM,GAAG,CAAC,CAAC,IAAI,IAAI,MAAM,GAAG,CAAC,CAAC,IAAI,IAAI,MAAM,GAAG,CAAC,CAAC,EAAE;AAAA,UACjF,OAAO;AACL,iCAAqB,OAAO;AAAA,UAC9B;AAAA,QACF,OAAO;AACL,+BAAqB,KAAK;AAAA,QAC5B;AACA,YAAI,SAAS,kBAAkB;AAC7B,8BAAoB;AAAA,YAClB,iBAAiB,SAAS,iBAAiB,mBAAmB;AAAA,YAC9D,WAAW,SAAS,iBAAiB,aAAa;AAAA,YAClD,YAAY,SAAS,iBAAiB,cAAc;AAAA,UACtD,CAAC;AAAA,QACH;AAAA,MACF,OAAO;AAEL,cAAM,eAAe,4BAA4B,WAAW,oBAAI,KAAK,CAAC;AACtE,wBAAgB,SAAS;AACzB,iBAAS,EAAE;AACX,gBAAQ,qBAAqB,YAAY,CAAC;AAC1C,qBAAa,qBAAqB,YAAY,CAAC;AAC/C,oBAAY,EAAE;AACd,kBAAU,KAAK;AACf,uBAAe,EAAE;AACjB,oBAAY,EAAE;AACd,2BAAmB,yBAAyB,OAAO;AACnD,sBAAc,MAAM;AACpB,wBAAgB,CAAC,CAAC;AAClB,0BAAkB,CAAC,CAAC;AACpB,6BAAqB,KAAK;AAAA,MAC5B;AACA,kBAAY,IAAI;AAAA,IAClB;AACA,WAAO,MAAM;AAEX,eAAS,KAAK,MAAM,eAAe,UAAU;AAC7C,eAAS,KAAK,MAAM,eAAe,gBAAgB;AAAA,IACrD;AAAA,EACF,GAAG,CAAC,MAAM,UAAU,SAAS,CAAC;AAK9B,QAAM,sBAAsB,MAAM,OAAe,SAAS;AAC1D,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,QAAQ,WAAW;AACtB,0BAAoB,UAAU;AAC9B;AAAA,IACF;AACA,QAAI,oBAAoB,YAAY,aAAc;AAClD,wBAAoB,UAAU;AAC9B,uBAAmB,yBAAyB,4BAA4B,YAAY,CAAC,CAAC;AAAA,EACxF,GAAG,CAAC,cAAc,WAAW,IAAI,CAAC;AAElC,QAAM,oBAAoB,MAAM,YAAY,CAAC,UAAkB;AAC7D,oBAAgB,CAAC,SAAS,KAAK,OAAO,CAAC,GAAG,MAAM,MAAM,KAAK,CAAC;AAAA,EAC9D,GAAG,CAAC,CAAC;AAEL,QAAM,sBAAsB,MAAM,YAAY,CAAC,UAAkB;AAC/D,sBAAkB,CAAC,SAAS;AAC1B,YAAM,OAAO,CAAC,GAAG,IAAI;AACrB,WAAK,KAAK,IAAI,CAAC,KAAK,KAAK;AACzB,aAAO;AAAA,IACT,CAAC;AAAA,EACH,GAAG,CAAC,CAAC;AAEL,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;",
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.7668.1.3aa8463bd8",
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.7668.1.3aa8463bd8",
269
- "@open-mercato/shared": "0.10.1-develop.7668.1.3aa8463bd8",
270
- "@open-mercato/ui": "0.10.1-develop.7668.1.3aa8463bd8",
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.7668.1.3aa8463bd8",
276
- "@open-mercato/shared": "0.10.1-develop.7668.1.3aa8463bd8",
277
- "@open-mercato/ui": "0.10.1-develop.7668.1.3aa8463bd8",
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 loadInteractionSnapshot(em: EntityManager, id: string): Promise<InteractionSnapshot | null> {
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
- const visibleFields = FIELD_VISIBILITY[state.activityType]
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[state.activityType]
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(state.activityType) && !trimmedDate
373
- const isTimeMissing = isTimeRequired(state.activityType) && !state.allDay && !trimmedStartTime
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 (state.activityType === 'call' && trimmedCallPhone) {
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 (state.activityType === 'call') {
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: state.activityType === 'call' ? phoneNumberForPayload : undefined,
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: state.activityType === 'task' ? buildTaskPriorityPayload(taskPriority, seededTaskPriority) : undefined,
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 (state.activityType === 'call' && phoneFieldError) {
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(state.activityType, 'title', t, 'customers.schedule.titleLabel', 'Title')}
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
- state.activityType === 'email'
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={state.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
- {state.activityType === 'call' && (
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
- {state.activityType === 'task' && (
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={state.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
- {state.activityType === 'call' ? (
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={state.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={state.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(state.activityType, 'description', t, 'customers.schedule.description', 'Description')}
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={state.activityType === 'email'}
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={state.activityType === 'email' ? 8 : undefined}
790
+ rows={presentationType === 'email' ? 8 : undefined}
784
791
  textareaClassName={
785
- state.activityType === 'email'
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={state.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',