@open-mercato/core 0.6.6-develop.6519.1.bdc91fb755 → 0.6.6-develop.6520.1.55df6df792

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.
Files changed (70) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/customers/backend/calendar/page.js +9 -1
  3. package/dist/modules/customers/backend/calendar/page.js.map +2 -2
  4. package/dist/modules/customers/components/calendar/AgendaList.js +0 -11
  5. package/dist/modules/customers/components/calendar/AgendaList.js.map +2 -2
  6. package/dist/modules/customers/components/calendar/CalendarEventEditor.js +391 -235
  7. package/dist/modules/customers/components/calendar/CalendarEventEditor.js.map +2 -2
  8. package/dist/modules/customers/components/calendar/CalendarScreen.js +5 -5
  9. package/dist/modules/customers/components/calendar/CalendarScreen.js.map +2 -2
  10. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js +2 -2
  11. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js.map +2 -2
  12. package/dist/modules/customers/components/calendar/MonthGrid.js +1 -11
  13. package/dist/modules/customers/components/calendar/MonthGrid.js.map +2 -2
  14. package/dist/modules/customers/components/calendar/TimeGrid.js +162 -176
  15. package/dist/modules/customers/components/calendar/TimeGrid.js.map +2 -2
  16. package/dist/modules/customers/components/calendar/editor/LocationField.js +61 -14
  17. package/dist/modules/customers/components/calendar/editor/LocationField.js.map +2 -2
  18. package/dist/modules/customers/components/calendar/editor/PeopleField.js +10 -5
  19. package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +2 -2
  20. package/dist/modules/customers/components/calendar/editor/PriorityField.js +85 -0
  21. package/dist/modules/customers/components/calendar/editor/PriorityField.js.map +7 -0
  22. package/dist/modules/customers/components/calendar/editor/RelatedToField.js +8 -3
  23. package/dist/modules/customers/components/calendar/editor/RelatedToField.js.map +2 -2
  24. package/dist/modules/customers/components/calendar/editor/ResourcesField.js +225 -0
  25. package/dist/modules/customers/components/calendar/editor/ResourcesField.js.map +7 -0
  26. package/dist/modules/customers/components/calendar/editor/SegmentGroup.js +12 -7
  27. package/dist/modules/customers/components/calendar/editor/SegmentGroup.js.map +2 -2
  28. package/dist/modules/customers/components/calendar/editor/inputs.js +68 -57
  29. package/dist/modules/customers/components/calendar/editor/inputs.js.map +2 -2
  30. package/dist/modules/customers/components/calendar/editor/lookups.js +82 -2
  31. package/dist/modules/customers/components/calendar/editor/lookups.js.map +2 -2
  32. package/dist/modules/customers/components/calendar/useCalendarItems.js +6 -1
  33. package/dist/modules/customers/components/calendar/useCalendarItems.js.map +2 -2
  34. package/dist/modules/customers/data/validators.js +3 -1
  35. package/dist/modules/customers/data/validators.js.map +2 -2
  36. package/dist/modules/customers/lib/calendar/editorPayload.js +53 -22
  37. package/dist/modules/customers/lib/calendar/editorPayload.js.map +2 -2
  38. package/dist/modules/entities/backend/entities/user/[entityId]/page.js +1 -1
  39. package/dist/modules/entities/backend/entities/user/[entityId]/page.js.map +2 -2
  40. package/package.json +7 -7
  41. package/src/modules/customers/backend/calendar/page.tsx +8 -1
  42. package/src/modules/customers/components/calendar/AgendaList.tsx +0 -10
  43. package/src/modules/customers/components/calendar/CalendarEventEditor.tsx +444 -261
  44. package/src/modules/customers/components/calendar/CalendarScreen.tsx +12 -5
  45. package/src/modules/customers/components/calendar/CalendarSettingsModal.tsx +6 -6
  46. package/src/modules/customers/components/calendar/MonthGrid.tsx +0 -11
  47. package/src/modules/customers/components/calendar/TimeGrid.tsx +1 -24
  48. package/src/modules/customers/components/calendar/editor/LocationField.tsx +64 -2
  49. package/src/modules/customers/components/calendar/editor/PeopleField.tsx +11 -4
  50. package/src/modules/customers/components/calendar/editor/PriorityField.tsx +90 -0
  51. package/src/modules/customers/components/calendar/editor/RelatedToField.tsx +8 -3
  52. package/src/modules/customers/components/calendar/editor/ResourcesField.tsx +264 -0
  53. package/src/modules/customers/components/calendar/editor/SegmentGroup.tsx +14 -6
  54. package/src/modules/customers/components/calendar/editor/inputs.tsx +91 -50
  55. package/src/modules/customers/components/calendar/editor/lookups.ts +115 -2
  56. package/src/modules/customers/components/calendar/useCalendarItems.ts +8 -1
  57. package/src/modules/customers/data/validators.ts +3 -1
  58. package/src/modules/customers/i18n/de.json +12 -2
  59. package/src/modules/customers/i18n/en.json +12 -2
  60. package/src/modules/customers/i18n/es.json +12 -2
  61. package/src/modules/customers/i18n/pl.json +12 -2
  62. package/src/modules/customers/lib/calendar/editorPayload.ts +94 -37
  63. package/src/modules/entities/backend/entities/user/[entityId]/page.tsx +1 -1
  64. package/src/modules/entities/i18n/de.json +120 -0
  65. package/src/modules/entities/i18n/en.json +120 -0
  66. package/src/modules/entities/i18n/es.json +120 -0
  67. package/src/modules/entities/i18n/pl.json +120 -0
  68. package/dist/modules/customers/components/calendar/editor/CategoryField.js +0 -75
  69. package/dist/modules/customers/components/calendar/editor/CategoryField.js.map +0 -7
  70. package/src/modules/customers/components/calendar/editor/CategoryField.tsx +0 -79
@@ -4,11 +4,13 @@ import * as React from "react";
4
4
  import { Calendar, X } from "lucide-react";
5
5
  import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
6
6
  import { useLocale, useT } from "@open-mercato/shared/lib/i18n/context";
7
- import { apiCallOrThrow, withScopedApiRequestHeaders } from "@open-mercato/ui/backend/utils/apiCall";
8
- import { buildOptimisticLockHeader, extractOptimisticLockConflict } from "@open-mercato/ui/backend/utils/optimisticLock";
9
- import { mapCrudServerErrorToFormErrors } from "@open-mercato/ui/backend/utils/serverErrors";
7
+ import { apiCallOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
8
+ import { extractOptimisticLockConflict } from "@open-mercato/ui/backend/utils/optimisticLock";
9
+ import { surfaceRecordConflict } from "@open-mercato/ui/backend/conflicts";
10
+ import { createCrudFormError } from "@open-mercato/ui/backend/utils/serverErrors";
10
11
  import { flash } from "@open-mercato/ui/backend/FlashMessages";
11
- import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuardedMutation";
12
+ import { CrudForm } from "@open-mercato/ui/backend/CrudForm";
13
+ import { collectCustomFieldValues } from "@open-mercato/ui/backend/utils/customFieldValues";
12
14
  import { Alert, AlertDescription, AlertTitle } from "@open-mercato/ui/primitives/alert";
13
15
  import { Button } from "@open-mercato/ui/primitives/button";
14
16
  import { Dialog, DialogContent, DialogTitle } from "@open-mercato/ui/primitives/dialog";
@@ -16,133 +18,404 @@ import { IconButton } from "@open-mercato/ui/primitives/icon-button";
16
18
  import { Input } from "@open-mercato/ui/primitives/input";
17
19
  import { Textarea } from "@open-mercato/ui/primitives/textarea";
18
20
  import { useDialogKeyHandler } from "@open-mercato/ui/hooks/useDialogKeyHandler";
21
+ import { E } from "../../../../generated/entities.ids.generated.js";
19
22
  import {
20
- buildEditorCategoryOptions,
23
+ buildEditorTypeOptions,
21
24
  buildInteractionPayload,
22
25
  computeDurationMinutes,
23
26
  createDefaultFormState,
24
27
  defaultRepeatDaysForDateInput,
25
- EDITOR_KINDS,
28
+ editorKindOfInteractionType,
26
29
  KIND_CONFIG,
27
30
  parseItemToFormState,
28
31
  resolveSavedOwnerUserId
29
32
  } from "../../lib/calendar/editorPayload.js";
30
- import { Field } from "./editor/inputs.js";
33
+ import { renderDictionaryIcon } from "@open-mercato/core/modules/dictionaries/components/dictionaryAppearance";
34
+ import { normalizeCustomFieldSubmitValue } from "../detail/customFieldUtils.js";
35
+ import { EDITOR_SCROLL_EVENT, Field } from "./editor/inputs.js";
31
36
  import { SegmentGroup } from "./editor/SegmentGroup.js";
37
+ import { PriorityField } from "./editor/PriorityField.js";
32
38
  import { RelatedToField } from "./editor/RelatedToField.js";
33
- import { CategoryField } from "./editor/CategoryField.js";
34
39
  import { RepeatField } from "./editor/RepeatField.js";
35
40
  import { PeopleField } from "./editor/PeopleField.js";
41
+ import { ResourcesField } from "./editor/ResourcesField.js";
36
42
  import { ScheduleSection } from "./editor/ScheduleSection.js";
37
43
  import { LocationField } from "./editor/LocationField.js";
38
44
  import { useConflictProbe, useEditorLabelResolution } from "./editor/hooks.js";
45
+ const FORM_ID = "customers-calendar-event-editor";
46
+ const INTERACTION_ENTITY_IDS = [E.customers.customer_interaction];
39
47
  const PEOPLE_FIELD_TEXT = {
40
48
  attendees: { labelKey: "customers.calendar.editor.attendees", label: "Attendees", placeholderKey: "customers.calendar.editor.addPeoplePlaceholder", placeholder: "Add staff or customer\u2026" },
41
49
  participants: { labelKey: "customers.calendar.editor.participants", label: "Participants", placeholderKey: "customers.calendar.editor.addPeoplePlaceholder", placeholder: "Add staff or customer\u2026" },
42
50
  to: { labelKey: "customers.calendar.editor.to", label: "To", placeholderKey: "customers.calendar.editor.addRecipientPlaceholder", placeholder: "Add recipient\u2026" }
43
51
  };
44
- function CalendarEventEditor({ open, mode, item, defaultDate, defaultRange, typeLabels, typeColors, surfacedTypes, eventCategories, conflictScope, currentUserId, onOpenChange, onSaved }) {
52
+ function formStateOfValues(values) {
53
+ return values;
54
+ }
55
+ function customFieldInitialValues(item) {
56
+ const customValues = item.raw.customValues;
57
+ if (!customValues || typeof customValues !== "object" || Array.isArray(customValues)) return {};
58
+ return Object.fromEntries(
59
+ Object.entries(customValues).map(([key, value]) => [`cf_${key}`, value])
60
+ );
61
+ }
62
+ function EditorBody({
63
+ ctx,
64
+ open,
65
+ isEdit,
66
+ item,
67
+ typeLabels,
68
+ typeIcons,
69
+ conflictScope,
70
+ currentUserId,
71
+ resourcesEnabled,
72
+ staffEnabled
73
+ }) {
45
74
  const t = useT();
46
75
  const locale = useLocale();
47
- const [form, setForm] = React.useState(() => createDefaultFormState(defaultDate ?? null, void 0, defaultRange ?? null));
48
- const [fieldErrors, setFieldErrors] = React.useState({});
49
- const [saving, setSaving] = React.useState(false);
50
- const openKeyRef = React.useRef(null);
51
- const isEdit = mode === "edit" && Boolean(item?.id);
76
+ const { setValue, errors } = ctx;
77
+ const form = formStateOfValues(ctx.values);
52
78
  const config = KIND_CONFIG[form.kind];
53
- const update = React.useCallback((patch) => {
54
- setForm((previous) => ({ ...previous, ...patch }));
55
- }, []);
56
- React.useEffect(() => {
57
- if (!open) {
58
- openKeyRef.current = null;
59
- return;
60
- }
61
- const key = `${mode}:${item?.id ?? "new"}`;
62
- if (openKeyRef.current === key) return;
63
- openKeyRef.current = key;
64
- setForm(
65
- mode === "edit" && item ? parseItemToFormState(item) : createDefaultFormState(defaultDate ?? null, void 0, defaultRange ?? null)
66
- );
67
- setFieldErrors({});
68
- }, [open, mode, item, defaultDate, defaultRange]);
79
+ const update = React.useCallback(
80
+ (patch) => {
81
+ for (const [key, value] of Object.entries(patch)) setValue(key, value);
82
+ },
83
+ [setValue]
84
+ );
69
85
  useEditorLabelResolution(open, form, update);
70
86
  const draftOwnerUserId = resolveSavedOwnerUserId(config, form, isEdit, item?.ownerUserId ?? null);
71
- const conflict = useConflictProbe(open, form, config, isEdit && item ? item.raw.id : null, draftOwnerUserId, conflictScope ?? "all", currentUserId ?? null);
72
- const { runMutation, retryLastMutation } = useGuardedMutation({
73
- contextId: "customers-calendar-event-editor",
74
- blockedMessage: t("ui.forms.flash.saveBlocked", "Save blocked by validation")
75
- });
76
- const handleSave = React.useCallback(async () => {
77
- const errors = {};
78
- if (!form.title.trim()) errors.title = t("customers.calendar.editor.validation.titleRequired", "Title is required");
79
- if (!form.relatedTo) errors.relatedTo = t("customers.calendar.editor.validation.relatedToRequired", "Select a person or company to link this event");
80
- if (config.hasEnd && !form.allDay && computeDurationMinutes(form) === null) {
81
- errors.ends = t("customers.calendar.editor.validation.endsBeforeStarts", "End must be after start");
87
+ const conflict = useConflictProbe(open, form, config, isEdit && item ? item.raw.id : null, draftOwnerUserId, conflictScope, currentUserId);
88
+ const kindLabels = React.useMemo(
89
+ () => ({
90
+ meeting: t("customers.calendar.editor.types.meeting", "meeting"),
91
+ call: t("customers.calendar.editor.types.call", "call"),
92
+ email: t("customers.calendar.editor.types.email", "email"),
93
+ note: t("customers.calendar.editor.types.note", "note"),
94
+ event: t("customers.calendar.editor.types.event", "event"),
95
+ task: t("customers.calendar.editor.types.task", "task")
96
+ }),
97
+ [t]
98
+ );
99
+ const selectedType = form.category ?? form.kind;
100
+ const typeOptions = React.useMemo(
101
+ () => buildEditorTypeOptions({ typeLabels, typeIcons, selectedValue: selectedType, kindLabels }),
102
+ [typeLabels, typeIcons, selectedType, kindLabels]
103
+ );
104
+ const typeSwitcherOptions = React.useMemo(
105
+ () => typeOptions.map((option) => ({
106
+ value: option.value,
107
+ label: option.label,
108
+ icon: renderDictionaryIcon(option.icon, "h-4 w-4")
109
+ })),
110
+ [typeOptions]
111
+ );
112
+ const phoneContactIds = React.useMemo(() => {
113
+ const ids = [];
114
+ if (form.relatedTo && form.relatedTo.kind !== "company") ids.push(form.relatedTo.id);
115
+ for (const participant of form.participants) {
116
+ if (participant.isCustomer) ids.push(participant.userId);
82
117
  }
83
- setFieldErrors(errors);
84
- if (Object.keys(errors).length > 0) return;
85
- setSaving(true);
86
- try {
87
- const payload = buildInteractionPayload(form, { mode, id: item?.id });
88
- await runMutation({
89
- operation: () => {
90
- const call = () => apiCallOrThrow("/api/customers/interactions", {
91
- method: isEdit ? "PUT" : "POST",
92
- headers: { "content-type": "application/json" },
93
- body: JSON.stringify(payload)
94
- });
95
- return isEdit ? withScopedApiRequestHeaders(buildOptimisticLockHeader(item?.updatedAt), call) : call();
96
- },
97
- mutationPayload: {
98
- operation: isEdit ? "updateCalendarEvent" : "createCalendarEvent",
99
- interactionId: item?.id ?? null,
100
- interactionType: form.category ?? form.kind
101
- },
102
- context: {
103
- formId: "customers-calendar-event-editor",
104
- resourceKind: "customers.interaction",
105
- resourceId: item?.id ?? "new",
106
- retryLastMutation
118
+ return Array.from(new Set(ids));
119
+ }, [form.relatedTo, form.participants]);
120
+ const titleLabel = form.kind === "email" ? t("customers.calendar.editor.titleLabel.email", "Subject") : form.kind === "note" ? t("customers.calendar.editor.titleLabel.note", "Note") : t("customers.calendar.editor.titleLabel.generic", "Title");
121
+ return (
122
+ // Single column on phones (full-screen sheet). On lg+ the dialog widens and
123
+ // the fields group into two thematic columns: WHEN (schedule + repeat) on
124
+ // the left, CONTEXT (related record, category, location) on the right;
125
+ // people/resources and the task fields pair up below; title, description
126
+ // and the type switcher span both columns.
127
+ /* @__PURE__ */ jsxs("div", { className: "grid w-full grid-cols-1 items-start gap-4 lg:grid-cols-2 lg:gap-x-6", children: [
128
+ conflict ? /* @__PURE__ */ jsxs(Alert, { variant: "warning", className: "rounded-lg lg:col-span-2", children: [
129
+ /* @__PURE__ */ jsx(AlertTitle, { children: t("customers.calendar.editor.conflictTitle", "Calendar conflict") }),
130
+ /* @__PURE__ */ jsx(AlertDescription, { children: conflict })
131
+ ] }) : null,
132
+ /* @__PURE__ */ jsx("div", { className: "w-full lg:col-span-2", children: /* @__PURE__ */ jsx(
133
+ SegmentGroup,
134
+ {
135
+ ariaLabel: t("customers.calendar.editor.typeSwitcher", "Event type"),
136
+ value: selectedType,
137
+ onChange: (type) => update({ kind: editorKindOfInteractionType(type), category: type }),
138
+ options: typeSwitcherOptions
107
139
  }
108
- });
109
- flash(t("customers.calendar.editor.saved", "Event saved"), "success");
110
- onOpenChange(false);
111
- requestAnimationFrame(() => {
112
- onSaved();
113
- });
114
- } catch (err) {
115
- if (extractOptimisticLockConflict(err)) {
140
+ ) }),
141
+ /* @__PURE__ */ jsx(Field, { label: titleLabel, error: errors.title, className: "lg:col-span-2", children: /* @__PURE__ */ jsx(
142
+ Input,
143
+ {
144
+ type: "text",
145
+ value: form.title,
146
+ onChange: (event) => update({ title: event.target.value }),
147
+ placeholder: t("customers.calendar.editor.titlePlaceholder", "Add a title\u2026"),
148
+ "aria-label": titleLabel,
149
+ autoFocus: true,
150
+ size: "lg"
151
+ }
152
+ ) }),
153
+ /* @__PURE__ */ jsxs("div", { className: "flex w-full flex-col gap-4", children: [
154
+ /* @__PURE__ */ jsx(
155
+ ScheduleSection,
156
+ {
157
+ dateLabel: config.dateLabel,
158
+ hasAllDay: config.hasAllDay,
159
+ hasEnd: config.hasEnd,
160
+ allDay: form.allDay,
161
+ date: form.date,
162
+ startTime: form.startTime,
163
+ endDate: form.endDate,
164
+ endTime: form.endTime,
165
+ locale,
166
+ endsError: errors.ends,
167
+ onAllDayChange: (allDay) => update({ allDay }),
168
+ onDateChange: (date) => {
169
+ const untouchedDefault = JSON.stringify(form.repeatDays) === JSON.stringify(defaultRepeatDaysForDateInput(form.date));
170
+ update(
171
+ untouchedDefault ? { date, repeatDays: defaultRepeatDaysForDateInput(date) } : { date }
172
+ );
173
+ },
174
+ onStartTimeChange: (startTime) => update({ startTime }),
175
+ onEndDateChange: (endDate) => update({ endDate }),
176
+ onEndTimeChange: (endTime) => update({ endTime })
177
+ }
178
+ ),
179
+ config.hasRepeat ? /* @__PURE__ */ jsx(
180
+ RepeatField,
181
+ {
182
+ freq: form.repeatFreq,
183
+ days: form.repeatDays,
184
+ endType: form.repeatEndType,
185
+ count: form.repeatCount,
186
+ untilDate: form.repeatUntilDate,
187
+ locale,
188
+ onFreqChange: (repeatFreq) => update(
189
+ repeatFreq === "weekly" ? { repeatFreq, repeatDays: defaultRepeatDaysForDateInput(form.date) } : { repeatFreq }
190
+ ),
191
+ onToggleDay: (index) => update({ repeatDays: form.repeatDays.map((active, dayIndex) => dayIndex === index ? !active : active) }),
192
+ onEndTypeChange: (repeatEndType) => update({ repeatEndType }),
193
+ onCountChange: (repeatCount) => update({ repeatCount }),
194
+ onUntilDateChange: (repeatUntilDate) => update({ repeatUntilDate })
195
+ }
196
+ ) : null,
197
+ config.people && config.people !== "assignee" ? /* @__PURE__ */ jsx(Field, { label: t(PEOPLE_FIELD_TEXT[config.people].labelKey, PEOPLE_FIELD_TEXT[config.people].label), children: /* @__PURE__ */ jsx(
198
+ PeopleField,
199
+ {
200
+ mode: "multi",
201
+ includeCustomers: true,
202
+ includeStaff: staffEnabled,
203
+ placeholder: t(PEOPLE_FIELD_TEXT[config.people].placeholderKey, PEOPLE_FIELD_TEXT[config.people].placeholder),
204
+ ariaLabel: t(PEOPLE_FIELD_TEXT[config.people].labelKey, PEOPLE_FIELD_TEXT[config.people].label),
205
+ value: form.participants,
206
+ onChange: (participants) => update({ participants })
207
+ }
208
+ ) }) : null,
209
+ config.people === "assignee" && staffEnabled ? /* @__PURE__ */ jsx(Field, { label: t("customers.calendar.editor.assignee", "Assignee"), error: errors.assignee, children: /* @__PURE__ */ jsx(
210
+ PeopleField,
211
+ {
212
+ mode: "single",
213
+ includeCustomers: false,
214
+ includeStaff: true,
215
+ placeholder: t("customers.calendar.editor.assigneePlaceholder", "Assign to a team member\u2026"),
216
+ ariaLabel: t("customers.calendar.editor.assignee", "Assignee"),
217
+ value: form.assigneeUserId ? [{ userId: form.assigneeUserId, name: form.assigneeName ?? form.assigneeUserId, isCustomer: false }] : [],
218
+ onChange: (entries) => {
219
+ const next = entries[entries.length - 1] ?? null;
220
+ update({ assigneeUserId: next?.userId ?? null, assigneeName: next?.name ?? null });
221
+ }
222
+ }
223
+ ) }) : null
224
+ ] }),
225
+ /* @__PURE__ */ jsxs("div", { className: "flex w-full flex-col gap-4", children: [
226
+ /* @__PURE__ */ jsx(Field, { label: t("customers.calendar.editor.relatedTo", "Related to"), error: errors.relatedTo, children: /* @__PURE__ */ jsx(
227
+ RelatedToField,
228
+ {
229
+ label: t("customers.calendar.editor.relatedTo", "Related to"),
230
+ value: form.relatedTo,
231
+ deal: form.dealId && form.dealLabel ? { id: form.dealId, label: form.dealLabel } : null,
232
+ onChange: (relatedTo) => update({ relatedTo }),
233
+ onDealChange: (deal) => update({ dealId: deal?.id ?? null, dealLabel: deal?.label ?? null }),
234
+ error: errors.relatedTo
235
+ }
236
+ ) }),
237
+ config.location ? /* @__PURE__ */ jsx(
238
+ LocationField,
239
+ {
240
+ variant: config.location,
241
+ value: form.location,
242
+ onChange: (location) => update({ location }),
243
+ phoneContactIds
244
+ }
245
+ ) : null,
246
+ resourcesEnabled ? /* @__PURE__ */ jsx(Field, { label: t("customers.calendar.editor.resources", "Resources"), children: /* @__PURE__ */ jsx(
247
+ ResourcesField,
248
+ {
249
+ placeholder: t("customers.calendar.editor.resourcesPlaceholder", "Add a resource\u2026"),
250
+ ariaLabel: t("customers.calendar.editor.resources", "Resources"),
251
+ value: form.resources,
252
+ onChange: (resources) => update({ resources })
253
+ }
254
+ ) }) : null,
255
+ config.hasPriority ? /* @__PURE__ */ jsx(Field, { label: t("customers.calendar.editor.priority.label", "Priority"), children: /* @__PURE__ */ jsx(
256
+ PriorityField,
257
+ {
258
+ value: form.priority,
259
+ ariaLabel: t("customers.calendar.editor.priority.label", "Priority"),
260
+ labels: {
261
+ low: t("customers.calendar.editor.priority.low", "Low"),
262
+ medium: t("customers.calendar.editor.priority.medium", "Medium"),
263
+ high: t("customers.calendar.editor.priority.high", "High")
264
+ },
265
+ onChange: (priority) => update({ priority })
266
+ }
267
+ ) }) : null
268
+ ] }),
269
+ /* @__PURE__ */ jsx(Field, { label: t("customers.calendar.editor.description", "Description"), className: "lg:col-span-2", children: /* @__PURE__ */ jsx(
270
+ Textarea,
271
+ {
272
+ value: form.description,
273
+ onChange: (event) => update({ description: event.target.value }),
274
+ placeholder: t("customers.calendar.editor.descriptionPlaceholder", "Add details\u2026"),
275
+ "aria-label": t("customers.calendar.editor.description", "Description"),
276
+ className: "h-20 resize-none"
277
+ }
278
+ ) })
279
+ ] })
280
+ );
281
+ }
282
+ function CalendarEventEditor({
283
+ open,
284
+ mode,
285
+ item,
286
+ defaultDate,
287
+ defaultRange,
288
+ typeLabels,
289
+ typeIcons,
290
+ conflictScope,
291
+ currentUserId,
292
+ resourcesEnabled,
293
+ staffEnabled,
294
+ onOpenChange,
295
+ onSaved
296
+ }) {
297
+ const t = useT();
298
+ const [saving, setSaving] = React.useState(false);
299
+ const isEdit = mode === "edit" && Boolean(item?.id);
300
+ const [openSeq, setOpenSeq] = React.useState(0);
301
+ const wasOpenRef = React.useRef(false);
302
+ React.useEffect(() => {
303
+ if (open && !wasOpenRef.current) setOpenSeq((seq) => seq + 1);
304
+ wasOpenRef.current = open;
305
+ }, [open]);
306
+ const formKey = `${mode}:${item?.id ?? "new"}:${openSeq}`;
307
+ const initialValues = React.useMemo(() => {
308
+ if (isEdit && item) {
309
+ return {
310
+ ...parseItemToFormState(item),
311
+ ...customFieldInitialValues(item),
312
+ id: item.id,
313
+ updatedAt: item.updatedAt ?? void 0
314
+ };
315
+ }
316
+ return { ...createDefaultFormState(defaultDate ?? null, void 0, defaultRange ?? null) };
317
+ }, [isEdit, item, defaultDate, defaultRange, openSeq]);
318
+ const handleSubmit = React.useCallback(
319
+ async (values) => {
320
+ const form = formStateOfValues(values);
321
+ const config = KIND_CONFIG[form.kind];
322
+ const fieldErrors = {};
323
+ if (!form.title.trim()) {
324
+ fieldErrors.title = t("customers.calendar.editor.validation.titleRequired", "Title is required");
325
+ }
326
+ if (!form.relatedTo) {
327
+ fieldErrors.relatedTo = t("customers.calendar.editor.validation.relatedToRequired", "Select a person or company to link this event");
328
+ }
329
+ if (config.hasEnd && !form.allDay && computeDurationMinutes(form) === null) {
330
+ fieldErrors.ends = t("customers.calendar.editor.validation.endsBeforeStarts", "End must be after start");
331
+ }
332
+ if (config.people === "assignee" && staffEnabled !== false && !form.assigneeUserId) {
333
+ fieldErrors.assignee = t("customers.calendar.editor.validation.assigneeRequired", "Assign this task to a team member");
334
+ }
335
+ if (Object.keys(fieldErrors).length > 0) {
336
+ throw createCrudFormError(Object.values(fieldErrors)[0], fieldErrors);
337
+ }
338
+ setSaving(true);
339
+ try {
340
+ const payload = buildInteractionPayload(form, {
341
+ mode,
342
+ id: item?.id,
343
+ resourcesEnabled: resourcesEnabled === true,
344
+ staffEnabled: staffEnabled !== false
345
+ });
346
+ const custom = collectCustomFieldValues(values, {
347
+ transform: (value) => normalizeCustomFieldSubmitValue(value)
348
+ });
349
+ for (const [key, value] of Object.entries(custom)) payload[`cf_${key}`] = value;
350
+ await apiCallOrThrow("/api/customers/interactions", {
351
+ method: isEdit ? "PUT" : "POST",
352
+ headers: { "content-type": "application/json" },
353
+ body: JSON.stringify(payload)
354
+ });
355
+ flash(t("customers.calendar.editor.saved", "Event saved"), "success");
116
356
  onOpenChange(false);
117
- return;
357
+ requestAnimationFrame(() => {
358
+ onSaved();
359
+ });
360
+ } catch (err) {
361
+ if (extractOptimisticLockConflict(err)) {
362
+ surfaceRecordConflict(err, t);
363
+ onOpenChange(false);
364
+ return;
365
+ }
366
+ throw err;
367
+ } finally {
368
+ setSaving(false);
118
369
  }
119
- const { message } = mapCrudServerErrorToFormErrors(err);
120
- const key = typeof message === "string" ? message.trim() : "";
121
- flash(key ? t(key, key) : t("customers.calendar.editor.error", "Failed to save event"), "error");
122
- } finally {
123
- setSaving(false);
124
- }
125
- }, [config.hasEnd, form, isEdit, item?.id, mode, onOpenChange, onSaved, retryLastMutation, runMutation, t]);
126
- const handleKeyDown = useDialogKeyHandler({ onConfirm: handleSave, disabled: saving });
127
- const categoryOptions = React.useMemo(
128
- () => buildEditorCategoryOptions({
129
- typeLabels,
130
- surfacedTypes: surfacedTypes ?? [],
131
- eventCategories: eventCategories ?? [],
132
- selectedValue: form.category ?? form.kind,
133
- selectedFallbackLabel: t(`customers.calendar.editor.types.${form.kind}`, form.kind)
134
- }),
135
- [typeLabels, surfacedTypes, eventCategories, form.category, form.kind, t]
370
+ },
371
+ [isEdit, item?.id, mode, onOpenChange, onSaved, resourcesEnabled, staffEnabled, t]
136
372
  );
137
- const titleLabel = form.kind === "email" ? t("customers.calendar.editor.titleLabel.email", "Subject") : form.kind === "note" ? t("customers.calendar.editor.titleLabel.note", "Note") : t("customers.calendar.editor.titleLabel.generic", "Title");
373
+ const groups = React.useMemo(
374
+ () => [
375
+ {
376
+ id: "details",
377
+ bare: true,
378
+ component: (ctx) => /* @__PURE__ */ jsx(
379
+ EditorBody,
380
+ {
381
+ ctx,
382
+ open,
383
+ isEdit,
384
+ item,
385
+ typeLabels,
386
+ typeIcons: typeIcons ?? {},
387
+ conflictScope: conflictScope ?? "all",
388
+ currentUserId: currentUserId ?? null,
389
+ resourcesEnabled: resourcesEnabled === true,
390
+ staffEnabled: staffEnabled !== false
391
+ }
392
+ )
393
+ },
394
+ { id: "customFields", kind: "customFields" }
395
+ ],
396
+ [open, isEdit, item, typeLabels, typeIcons, conflictScope, currentUserId, resourcesEnabled, staffEnabled]
397
+ );
398
+ const handleKeyDown = useDialogKeyHandler({
399
+ onConfirm: () => {
400
+ const formElement = document.getElementById(FORM_ID);
401
+ if (formElement instanceof HTMLFormElement) formElement.requestSubmit();
402
+ },
403
+ disabled: saving
404
+ });
138
405
  const dialogTitle = isEdit ? t("customers.calendar.editor.title.edit", "Edit event") : t("customers.calendar.editor.title.create", "New event");
139
406
  return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs(
140
407
  DialogContent,
141
408
  {
142
409
  onKeyDown: handleKeyDown,
410
+ onInteractOutside: (event) => {
411
+ const target = event.detail.originalEvent.target;
412
+ if (target?.closest("[data-dialog-content]") || target?.closest("[data-radix-popper-content-wrapper]")) {
413
+ event.preventDefault();
414
+ }
415
+ },
143
416
  "aria-describedby": void 0,
144
417
  dismissible: false,
145
- className: "flex h-dvh max-h-dvh w-screen max-w-none flex-col gap-0 overflow-hidden rounded-none border-0 bg-background p-0 shadow-xl sm:h-auto sm:max-h-screen sm:w-full sm:max-w-md sm:rounded-2xl sm:border-0",
418
+ className: "flex h-dvh max-h-dvh w-screen max-w-none flex-col gap-0 overflow-hidden rounded-none border-0 bg-background p-0 shadow-xl sm:h-auto sm:max-h-[calc(100dvh-4rem)] sm:w-full sm:max-w-lg sm:rounded-2xl sm:border-0 lg:max-w-3xl",
146
419
  children: [
147
420
  /* @__PURE__ */ jsx(VisuallyHidden, { children: /* @__PURE__ */ jsx(DialogTitle, { children: dialogTitle }) }),
148
421
  /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-3 border-b border-border bg-background py-4 pl-5 pr-4", children: [
@@ -160,147 +433,30 @@ function CalendarEventEditor({ open, mode, item, defaultDate, defaultRange, type
160
433
  }
161
434
  )
162
435
  ] }),
163
- /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-start gap-5 px-4 py-5 sm:px-6 sm:py-6", children: [
164
- conflict ? /* @__PURE__ */ jsxs(Alert, { variant: "warning", className: "rounded-lg", children: [
165
- /* @__PURE__ */ jsx(AlertTitle, { children: t("customers.calendar.editor.conflictTitle", "Calendar conflict") }),
166
- /* @__PURE__ */ jsx(AlertDescription, { children: conflict })
167
- ] }) : null,
168
- /* @__PURE__ */ jsx(
169
- SegmentGroup,
170
- {
171
- ariaLabel: t("customers.calendar.editor.typeSwitcher", "Event type"),
172
- value: form.kind,
173
- onChange: (kind) => update({ kind, category: null }),
174
- options: EDITOR_KINDS.map((kind) => ({ value: kind, label: t(`customers.calendar.editor.types.${kind}`, kind) }))
175
- }
176
- ),
177
- /* @__PURE__ */ jsx(Field, { label: titleLabel, error: fieldErrors.title, children: /* @__PURE__ */ jsx(
178
- Input,
179
- {
180
- type: "text",
181
- value: form.title,
182
- onChange: (event) => update({ title: event.target.value }),
183
- placeholder: t("customers.calendar.editor.titlePlaceholder", "Add a title\u2026"),
184
- "aria-label": titleLabel,
185
- autoFocus: true,
186
- size: "lg"
187
- }
188
- ) }),
189
- /* @__PURE__ */ jsx(Field, { label: t("customers.calendar.editor.relatedTo", "Related to"), error: fieldErrors.relatedTo, children: /* @__PURE__ */ jsx(
190
- RelatedToField,
191
- {
192
- label: t("customers.calendar.editor.relatedTo", "Related to"),
193
- value: form.relatedTo,
194
- deal: form.dealId && form.dealLabel ? { id: form.dealId, label: form.dealLabel } : null,
195
- onChange: (relatedTo) => update({ relatedTo }),
196
- onDealChange: (deal) => update({ dealId: deal?.id ?? null, dealLabel: deal?.label ?? null }),
197
- error: fieldErrors.relatedTo
198
- }
199
- ) }),
200
- /* @__PURE__ */ jsx(
201
- ScheduleSection,
202
- {
203
- dateLabel: config.dateLabel,
204
- hasAllDay: config.hasAllDay,
205
- hasEnd: config.hasEnd,
206
- allDay: form.allDay,
207
- date: form.date,
208
- startTime: form.startTime,
209
- endDate: form.endDate,
210
- endTime: form.endTime,
211
- locale,
212
- endsError: fieldErrors.ends,
213
- onAllDayChange: (allDay) => update({ allDay }),
214
- onDateChange: (date) => {
215
- const untouchedDefault = JSON.stringify(form.repeatDays) === JSON.stringify(defaultRepeatDaysForDateInput(form.date));
216
- update(
217
- untouchedDefault ? { date, repeatDays: defaultRepeatDaysForDateInput(date) } : { date }
218
- );
436
+ /* @__PURE__ */ jsx(
437
+ "div",
438
+ {
439
+ className: "flex-1 overflow-y-auto",
440
+ onScroll: () => {
441
+ document.dispatchEvent(new CustomEvent(EDITOR_SCROLL_EVENT));
442
+ },
443
+ children: /* @__PURE__ */ jsx("div", { className: "px-4 py-4 sm:px-6 sm:py-5", children: /* @__PURE__ */ jsx(
444
+ CrudForm,
445
+ {
446
+ formId: FORM_ID,
447
+ embedded: true,
448
+ hideFooterActions: true,
449
+ customFieldsManageMode: "page",
450
+ fields: [],
451
+ groups,
452
+ initialValues,
453
+ entityIds: INTERACTION_ENTITY_IDS,
454
+ onSubmit: handleSubmit
219
455
  },
220
- onStartTimeChange: (startTime) => update({ startTime }),
221
- onEndDateChange: (endDate) => update({ endDate }),
222
- onEndTimeChange: (endTime) => update({ endTime })
223
- }
224
- ),
225
- config.hasRepeat ? /* @__PURE__ */ jsx(
226
- RepeatField,
227
- {
228
- freq: form.repeatFreq,
229
- days: form.repeatDays,
230
- endType: form.repeatEndType,
231
- count: form.repeatCount,
232
- untilDate: form.repeatUntilDate,
233
- locale,
234
- onFreqChange: (repeatFreq) => update(
235
- repeatFreq === "weekly" ? { repeatFreq, repeatDays: defaultRepeatDaysForDateInput(form.date) } : { repeatFreq }
236
- ),
237
- onToggleDay: (index) => update({ repeatDays: form.repeatDays.map((active, dayIndex) => dayIndex === index ? !active : active) }),
238
- onEndTypeChange: (repeatEndType) => update({ repeatEndType }),
239
- onCountChange: (repeatCount) => update({ repeatCount }),
240
- onUntilDateChange: (repeatUntilDate) => update({ repeatUntilDate })
241
- }
242
- ) : null,
243
- /* @__PURE__ */ jsx(Field, { label: t("customers.calendar.editor.category", "Category"), children: /* @__PURE__ */ jsx(
244
- CategoryField,
245
- {
246
- label: t("customers.calendar.editor.category", "Category"),
247
- value: form.category ?? form.kind,
248
- options: categoryOptions,
249
- colors: typeColors,
250
- onChange: (category) => update({ category })
251
- }
252
- ) }),
253
- config.location ? /* @__PURE__ */ jsx(LocationField, { variant: config.location, value: form.location, onChange: (location) => update({ location }) }) : null,
254
- config.people && config.people !== "assignee" ? /* @__PURE__ */ jsx(Field, { label: t(PEOPLE_FIELD_TEXT[config.people].labelKey, PEOPLE_FIELD_TEXT[config.people].label), children: /* @__PURE__ */ jsx(
255
- PeopleField,
256
- {
257
- mode: "multi",
258
- includeCustomers: true,
259
- placeholder: t(PEOPLE_FIELD_TEXT[config.people].placeholderKey, PEOPLE_FIELD_TEXT[config.people].placeholder),
260
- ariaLabel: t(PEOPLE_FIELD_TEXT[config.people].labelKey, PEOPLE_FIELD_TEXT[config.people].label),
261
- value: form.participants,
262
- onChange: (participants) => update({ participants })
263
- }
264
- ) }) : null,
265
- config.hasPriority ? /* @__PURE__ */ jsx(Field, { label: t("customers.calendar.editor.priority.label", "Priority"), children: /* @__PURE__ */ jsx(
266
- SegmentGroup,
267
- {
268
- size: "md",
269
- ariaLabel: t("customers.calendar.editor.priority.label", "Priority"),
270
- value: form.priority,
271
- onChange: (priority) => update({ priority }),
272
- options: [
273
- { value: "low", label: t("customers.calendar.editor.priority.low", "Low") },
274
- { value: "medium", label: t("customers.calendar.editor.priority.medium", "Medium") },
275
- { value: "high", label: t("customers.calendar.editor.priority.high", "High") }
276
- ]
277
- }
278
- ) }) : null,
279
- config.people === "assignee" ? /* @__PURE__ */ jsx(Field, { label: t("customers.calendar.editor.assignee", "Assignee"), children: /* @__PURE__ */ jsx(
280
- PeopleField,
281
- {
282
- mode: "single",
283
- includeCustomers: false,
284
- placeholder: t("customers.calendar.editor.assigneePlaceholder", "Assign to a team member\u2026"),
285
- ariaLabel: t("customers.calendar.editor.assignee", "Assignee"),
286
- value: form.assigneeUserId ? [{ userId: form.assigneeUserId, name: form.assigneeName ?? form.assigneeUserId, isCustomer: false }] : [],
287
- onChange: (entries) => {
288
- const next = entries[entries.length - 1] ?? null;
289
- update({ assigneeUserId: next?.userId ?? null, assigneeName: next?.name ?? null });
290
- }
291
- }
292
- ) }) : null,
293
- /* @__PURE__ */ jsx(Field, { label: t("customers.calendar.editor.description", "Description"), children: /* @__PURE__ */ jsx(
294
- Textarea,
295
- {
296
- value: form.description,
297
- onChange: (event) => update({ description: event.target.value }),
298
- placeholder: t("customers.calendar.editor.descriptionPlaceholder", "Add details\u2026"),
299
- "aria-label": t("customers.calendar.editor.description", "Description"),
300
- className: "h-24 resize-none"
301
- }
302
- ) })
303
- ] }) }),
456
+ formKey
457
+ ) })
458
+ }
459
+ ),
304
460
  /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center justify-end gap-3 border-t border-border bg-background px-5 py-4", children: [
305
461
  /* @__PURE__ */ jsx(
306
462
  Button,
@@ -314,8 +470,8 @@ function CalendarEventEditor({ open, mode, item, defaultDate, defaultRange, type
314
470
  /* @__PURE__ */ jsx(
315
471
  Button,
316
472
  {
317
- type: "button",
318
- onClick: handleSave,
473
+ type: "submit",
474
+ form: FORM_ID,
319
475
  disabled: saving,
320
476
  children: saving ? t("customers.calendar.editor.saving", "Saving\u2026") : t("customers.calendar.editor.save", "Save event")
321
477
  }