@open-mercato/core 0.5.1-develop.3043.1a796c3920 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +21 -1
- package/dist/modules/api_keys/api/keys/route.js +9 -0
- package/dist/modules/api_keys/api/keys/route.js.map +2 -2
- package/dist/modules/audit_logs/services/accessLogService.js +13 -0
- package/dist/modules/audit_logs/services/accessLogService.js.map +3 -3
- package/dist/modules/audit_logs/services/actionLogService.js +6 -5
- package/dist/modules/audit_logs/services/actionLogService.js.map +2 -2
- package/dist/modules/auth/api/roles/acl/route.js +27 -37
- package/dist/modules/auth/api/roles/acl/route.js.map +2 -2
- package/dist/modules/auth/api/users/route.js +41 -28
- package/dist/modules/auth/api/users/route.js.map +3 -3
- package/dist/modules/auth/lib/grantChecks.js +160 -0
- package/dist/modules/auth/lib/grantChecks.js.map +7 -0
- package/dist/modules/configs/cli.js +11 -0
- package/dist/modules/configs/cli.js.map +2 -2
- package/dist/modules/configs/lib/touchGeneratedBarrels.js +46 -0
- package/dist/modules/configs/lib/touchGeneratedBarrels.js.map +7 -0
- package/dist/modules/customers/api/activities/route.js +1 -52
- package/dist/modules/customers/api/activities/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/counts/route.js +2 -1
- package/dist/modules/customers/api/interactions/counts/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/route.js +21 -1
- package/dist/modules/customers/api/interactions/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +7 -3
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js +5 -1
- package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +7 -3
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivitiesCard.js +62 -6
- package/dist/modules/customers/components/detail/ActivitiesCard.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivitiesDayStrip.js +21 -6
- package/dist/modules/customers/components/detail/ActivitiesDayStrip.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivitiesSection.js +37 -5
- package/dist/modules/customers/components/detail/ActivitiesSection.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityCard.js +69 -17
- package/dist/modules/customers/components/detail/ActivityCard.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityHistorySection.js +94 -34
- package/dist/modules/customers/components/detail/ActivityHistorySection.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityLogTab.js +3 -1
- package/dist/modules/customers/components/detail/ActivityLogTab.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityTimeline.js +41 -8
- package/dist/modules/customers/components/detail/ActivityTimeline.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityTimelineFilters.js +19 -6
- package/dist/modules/customers/components/detail/ActivityTimelineFilters.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityTypeSelector.js +4 -3
- package/dist/modules/customers/components/detail/ActivityTypeSelector.js.map +2 -2
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +80 -12
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/DateTimeFields.js +65 -10
- package/dist/modules/customers/components/detail/schedule/DateTimeFields.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js +10 -5
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js.map +2 -2
- package/dist/modules/customers/data/validators.js +74 -2
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/customers/lib/legacyActivityBridge.js +61 -0
- package/dist/modules/customers/lib/legacyActivityBridge.js.map +7 -0
- package/dist/modules/integrations/data/validators.js +2 -2
- package/dist/modules/integrations/data/validators.js.map +2 -2
- package/dist/modules/integrations/lib/credentials-service.js +12 -1
- package/dist/modules/integrations/lib/credentials-service.js.map +2 -2
- package/dist/modules/messages/commands/actions.js +29 -14
- package/dist/modules/messages/commands/actions.js.map +2 -2
- package/dist/modules/messages/lib/actions.js +24 -4
- package/dist/modules/messages/lib/actions.js.map +2 -2
- package/dist/modules/sales/api/documents/factory.js +49 -36
- package/dist/modules/sales/api/documents/factory.js.map +2 -2
- package/package.json +9 -10
- package/src/modules/api_keys/api/keys/route.ts +9 -0
- package/src/modules/audit_logs/services/accessLogService.ts +20 -0
- package/src/modules/audit_logs/services/actionLogService.ts +13 -5
- package/src/modules/auth/api/roles/acl/route.ts +32 -46
- package/src/modules/auth/api/users/route.ts +48 -33
- package/src/modules/auth/lib/grantChecks.ts +234 -0
- package/src/modules/configs/cli.ts +11 -0
- package/src/modules/configs/lib/touchGeneratedBarrels.ts +61 -0
- package/src/modules/customers/api/activities/route.ts +1 -76
- package/src/modules/customers/api/interactions/counts/route.ts +2 -1
- package/src/modules/customers/api/interactions/route.ts +28 -1
- package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +13 -3
- package/src/modules/customers/backend/customers/deals/[id]/page.tsx +14 -2
- package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +13 -3
- package/src/modules/customers/components/detail/ActivitiesCard.tsx +92 -5
- package/src/modules/customers/components/detail/ActivitiesDayStrip.tsx +38 -6
- package/src/modules/customers/components/detail/ActivitiesSection.tsx +37 -3
- package/src/modules/customers/components/detail/ActivityCard.tsx +79 -14
- package/src/modules/customers/components/detail/ActivityHistorySection.tsx +102 -33
- package/src/modules/customers/components/detail/ActivityLogTab.tsx +7 -1
- package/src/modules/customers/components/detail/ActivityTimeline.tsx +39 -5
- package/src/modules/customers/components/detail/ActivityTimelineFilters.tsx +29 -7
- package/src/modules/customers/components/detail/ActivityTypeSelector.tsx +3 -2
- package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +96 -13
- package/src/modules/customers/components/detail/schedule/DateTimeFields.tsx +50 -4
- package/src/modules/customers/components/detail/schedule/useScheduleFormState.ts +21 -5
- package/src/modules/customers/data/validators.ts +85 -2
- package/src/modules/customers/i18n/de.json +11 -0
- package/src/modules/customers/i18n/en.json +11 -0
- package/src/modules/customers/i18n/es.json +11 -0
- package/src/modules/customers/i18n/pl.json +11 -0
- package/src/modules/customers/lib/legacyActivityBridge.ts +106 -0
- package/src/modules/integrations/data/validators.ts +8 -6
- package/src/modules/integrations/lib/credentials-service.ts +15 -1
- package/src/modules/messages/commands/actions.ts +28 -13
- package/src/modules/messages/lib/actions.ts +34 -3
- package/src/modules/sales/api/documents/factory.ts +55 -38
|
@@ -4,7 +4,9 @@ import * as React from 'react'
|
|
|
4
4
|
import { Users, Phone, Check, Mail, Calendar, AlertTriangle, X } from 'lucide-react'
|
|
5
5
|
import { cn } from '@open-mercato/shared/lib/utils'
|
|
6
6
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
|
+
import { validatePhoneNumber } from '@open-mercato/shared/lib/phone'
|
|
7
8
|
import { apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
9
|
+
import { mapCrudServerErrorToFormErrors } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
8
10
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
9
11
|
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
10
12
|
import { Alert, AlertDescription, AlertTitle } from '@open-mercato/ui/primitives/alert'
|
|
@@ -12,7 +14,7 @@ import { Button } from '@open-mercato/ui/primitives/button'
|
|
|
12
14
|
import { IconButton } from '@open-mercato/ui/primitives/icon-button'
|
|
13
15
|
import { Dialog, DialogContent, DialogTitle } from '@open-mercato/ui/primitives/dialog'
|
|
14
16
|
import { VisuallyHidden } from '@radix-ui/react-visually-hidden'
|
|
15
|
-
import { SwitchableMarkdownInput } from '@open-mercato/ui/backend/inputs'
|
|
17
|
+
import { PhoneNumberField, SwitchableMarkdownInput } from '@open-mercato/ui/backend/inputs'
|
|
16
18
|
import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
|
|
17
19
|
import {
|
|
18
20
|
useScheduleFormState,
|
|
@@ -112,15 +114,41 @@ export function ScheduleActivityDialog({
|
|
|
112
114
|
const [callDirection, setCallDirection] = React.useState<'outbound' | 'inbound'>('outbound')
|
|
113
115
|
const [callOutcome, setCallOutcome] = React.useState<string | null>(null)
|
|
114
116
|
const [callPhoneNumber, setCallPhoneNumber] = React.useState('')
|
|
117
|
+
const [callPhoneError, setCallPhoneError] = React.useState<string | null>(null)
|
|
115
118
|
const [taskPriority, setTaskPriority] = React.useState<string>('medium')
|
|
119
|
+
const callPhoneInvalidMessage = React.useMemo(
|
|
120
|
+
() =>
|
|
121
|
+
t(
|
|
122
|
+
'customers.activities.errors.phoneInvalid',
|
|
123
|
+
'Enter a valid phone number with country code (e.g. +1 212 555 1234)',
|
|
124
|
+
),
|
|
125
|
+
[t],
|
|
126
|
+
)
|
|
127
|
+
const translateErrorMessage = React.useCallback(
|
|
128
|
+
(message: string | undefined, fallback: string) => {
|
|
129
|
+
const key = typeof message === 'string' ? message.trim() : ''
|
|
130
|
+
return key ? t(key, key) : fallback
|
|
131
|
+
},
|
|
132
|
+
[t],
|
|
133
|
+
)
|
|
116
134
|
|
|
117
135
|
React.useEffect(() => {
|
|
118
136
|
if (!open) return
|
|
119
|
-
const raw = editData as (Record<string, unknown> & { customValues?: unknown }) | null | undefined
|
|
137
|
+
const raw = editData as (Record<string, unknown> & { customValues?: unknown; phoneNumber?: unknown }) | null | undefined
|
|
120
138
|
const cv = (raw?.customValues && typeof raw.customValues === 'object' ? raw.customValues : null) as Record<string, unknown> | null
|
|
121
139
|
setCallDirection(typeof cv?.callDirection === 'string' && cv.callDirection === 'inbound' ? 'inbound' : 'outbound')
|
|
122
140
|
setCallOutcome(typeof cv?.callOutcome === 'string' ? cv.callOutcome : null)
|
|
123
|
-
|
|
141
|
+
// Seed phone number from either top-level `phoneNumber` (newer write path)
|
|
142
|
+
// or legacy `customValues.callPhoneNumber` so previously-saved calls still
|
|
143
|
+
// round-trip on edit (#1808).
|
|
144
|
+
const seededPhone =
|
|
145
|
+
typeof raw?.phoneNumber === 'string' && raw.phoneNumber.trim().length > 0
|
|
146
|
+
? raw.phoneNumber
|
|
147
|
+
: typeof cv?.callPhoneNumber === 'string'
|
|
148
|
+
? cv.callPhoneNumber
|
|
149
|
+
: ''
|
|
150
|
+
setCallPhoneNumber(seededPhone)
|
|
151
|
+
setCallPhoneError(null)
|
|
124
152
|
setTaskPriority(typeof cv?.taskPriority === 'string' ? cv.taskPriority : 'medium')
|
|
125
153
|
}, [open, editData])
|
|
126
154
|
|
|
@@ -131,9 +159,15 @@ export function ScheduleActivityDialog({
|
|
|
131
159
|
setCallDirection('outbound')
|
|
132
160
|
setCallOutcome(null)
|
|
133
161
|
setCallPhoneNumber('')
|
|
162
|
+
setCallPhoneError(null)
|
|
134
163
|
setTaskPriority('medium')
|
|
135
164
|
}, [state.activityType, open, isEditing])
|
|
136
165
|
|
|
166
|
+
const handleCallPhoneChange = React.useCallback((next: string | undefined) => {
|
|
167
|
+
setCallPhoneNumber(next ?? '')
|
|
168
|
+
setCallPhoneError(null)
|
|
169
|
+
}, [])
|
|
170
|
+
|
|
137
171
|
const formSnapshot = React.useMemo(() => JSON.stringify({
|
|
138
172
|
activityType: state.activityType,
|
|
139
173
|
title: state.title,
|
|
@@ -284,8 +318,41 @@ export function ScheduleActivityDialog({
|
|
|
284
318
|
return () => clearTimeout(timer)
|
|
285
319
|
}, [editData?.id, open, state.date, state.startTime, state.duration, state.allDay, t]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
286
320
|
|
|
321
|
+
const trimmedDate = state.date.trim()
|
|
322
|
+
const trimmedStartTime = state.startTime.trim()
|
|
323
|
+
const trimmedCallPhone = callPhoneNumber.trim()
|
|
324
|
+
const isDateMissing = !trimmedDate
|
|
325
|
+
const isTimeMissing = !state.allDay && !trimmedStartTime
|
|
326
|
+
const isSubmitDisabled =
|
|
327
|
+
state.saving ||
|
|
328
|
+
!state.title.trim() ||
|
|
329
|
+
isDateMissing ||
|
|
330
|
+
isTimeMissing
|
|
331
|
+
|
|
287
332
|
const handleSave = React.useCallback(async () => {
|
|
288
333
|
if (!state.title.trim()) return
|
|
334
|
+
if (isDateMissing) {
|
|
335
|
+
flash(t('customers.activities.errors.dateRequired', 'Date is required'), 'error')
|
|
336
|
+
return
|
|
337
|
+
}
|
|
338
|
+
if (isTimeMissing) {
|
|
339
|
+
flash(t('customers.activities.errors.timeRequired', 'Time is required'), 'error')
|
|
340
|
+
return
|
|
341
|
+
}
|
|
342
|
+
let phoneNumberForPayload: string | undefined
|
|
343
|
+
if (state.activityType === 'call' && trimmedCallPhone) {
|
|
344
|
+
const phoneValidation = validatePhoneNumber(trimmedCallPhone)
|
|
345
|
+
if (!phoneValidation.valid) {
|
|
346
|
+
setCallPhoneError(callPhoneInvalidMessage)
|
|
347
|
+
flash(callPhoneInvalidMessage, 'error')
|
|
348
|
+
return
|
|
349
|
+
}
|
|
350
|
+
phoneNumberForPayload = phoneValidation.normalized ?? trimmedCallPhone
|
|
351
|
+
setCallPhoneError(null)
|
|
352
|
+
if (phoneNumberForPayload !== callPhoneNumber) {
|
|
353
|
+
setCallPhoneNumber(phoneNumberForPayload)
|
|
354
|
+
}
|
|
355
|
+
}
|
|
289
356
|
state.setSaving(true)
|
|
290
357
|
try {
|
|
291
358
|
const scheduledAt = state.allDay
|
|
@@ -301,7 +368,7 @@ export function ScheduleActivityDialog({
|
|
|
301
368
|
if (state.activityType === 'call') {
|
|
302
369
|
customValues.callDirection = callDirection
|
|
303
370
|
if (callOutcome) customValues.callOutcome = callOutcome
|
|
304
|
-
if (
|
|
371
|
+
if (phoneNumberForPayload) customValues.callPhoneNumber = phoneNumberForPayload
|
|
305
372
|
}
|
|
306
373
|
if (state.activityType === 'task') {
|
|
307
374
|
customValues.taskPriority = taskPriority
|
|
@@ -314,6 +381,9 @@ export function ScheduleActivityDialog({
|
|
|
314
381
|
title: state.title.trim(),
|
|
315
382
|
body: state.description.trim() || null,
|
|
316
383
|
status: 'planned',
|
|
384
|
+
date: trimmedDate,
|
|
385
|
+
time: state.allDay ? '00:00' : trimmedStartTime,
|
|
386
|
+
phoneNumber: state.activityType === 'call' ? phoneNumberForPayload : undefined,
|
|
317
387
|
scheduledAt,
|
|
318
388
|
durationMinutes: visibleFields.has('duration') && !state.allDay ? state.duration : null,
|
|
319
389
|
location: visibleFields.has('location') ? (state.location.trim() || null) : null,
|
|
@@ -350,12 +420,23 @@ export function ScheduleActivityDialog({
|
|
|
350
420
|
onClose()
|
|
351
421
|
// Delay data reload so the dialog can unmount cleanly and Radix restores body scroll
|
|
352
422
|
requestAnimationFrame(() => { onActivityCreated?.() })
|
|
353
|
-
} catch {
|
|
354
|
-
|
|
423
|
+
} catch (err) {
|
|
424
|
+
const { message, fieldErrors } = mapCrudServerErrorToFormErrors(err)
|
|
425
|
+
const phoneFieldError = fieldErrors?.phoneNumber
|
|
426
|
+
if (state.activityType === 'call' && phoneFieldError) {
|
|
427
|
+
const translatedPhoneError = translateErrorMessage(phoneFieldError, callPhoneInvalidMessage)
|
|
428
|
+
setCallPhoneError(translatedPhoneError)
|
|
429
|
+
flash(translatedPhoneError, 'error')
|
|
430
|
+
return
|
|
431
|
+
}
|
|
432
|
+
flash(
|
|
433
|
+
translateErrorMessage(message, t('customers.schedule.error', 'Failed to schedule activity')),
|
|
434
|
+
'error',
|
|
435
|
+
)
|
|
355
436
|
} finally {
|
|
356
437
|
state.setSaving(false)
|
|
357
438
|
}
|
|
358
|
-
}, [state.activityType, state.allDay, state.date, state.description, dealId, state.duration, editData, entityId, state.guestPermissions, state.linkedEntities, state.location, onActivityCreated, onClose, state.participants, state.recurrenceCount, state.recurrenceDays, state.recurrenceEnabled, state.recurrenceEndDate, state.recurrenceEndType, state.reminderMinutes, runGuardedMutation, state.startTime, t, state.title, state.visibility, visibleFields]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
439
|
+
}, [callDirection, callOutcome, callPhoneInvalidMessage, callPhoneNumber, isDateMissing, isTimeMissing, state.activityType, state.allDay, state.date, state.description, dealId, state.duration, editData, entityId, state.guestPermissions, state.linkedEntities, state.location, onActivityCreated, onClose, state.participants, state.recurrenceCount, state.recurrenceDays, state.recurrenceEnabled, state.recurrenceEndDate, state.recurrenceEndType, state.reminderMinutes, runGuardedMutation, state.startTime, t, taskPriority, state.title, translateErrorMessage, trimmedCallPhone, trimmedDate, trimmedStartTime, state.visibility, visibleFields]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
359
440
|
|
|
360
441
|
const handleKeyDown = React.useCallback((e: React.KeyboardEvent) => {
|
|
361
442
|
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
|
@@ -580,15 +661,17 @@ export function ScheduleActivityDialog({
|
|
|
580
661
|
{/* Location (or phone number for calls) */}
|
|
581
662
|
{state.activityType === 'call' ? (
|
|
582
663
|
<div className="flex flex-col gap-1.5">
|
|
583
|
-
<label className="text-overline font-semibold uppercase text-muted-foreground tracking-wider">
|
|
664
|
+
<label htmlFor="schedule-call-phone" className="text-overline font-semibold uppercase text-muted-foreground tracking-wider">
|
|
584
665
|
{t('customers.schedule.call.phoneLabel', 'Phone number')}
|
|
585
666
|
</label>
|
|
586
|
-
<
|
|
587
|
-
|
|
667
|
+
<PhoneNumberField
|
|
668
|
+
id="schedule-call-phone"
|
|
588
669
|
value={callPhoneNumber}
|
|
589
|
-
|
|
670
|
+
onValueChange={handleCallPhoneChange}
|
|
590
671
|
placeholder={t('customers.schedule.call.phonePlaceholder', '+1 555 000 0000')}
|
|
591
|
-
|
|
672
|
+
externalError={callPhoneError}
|
|
673
|
+
invalidLabel={callPhoneInvalidMessage}
|
|
674
|
+
minDigits={7}
|
|
592
675
|
/>
|
|
593
676
|
</div>
|
|
594
677
|
) : (
|
|
@@ -642,7 +725,7 @@ export function ScheduleActivityDialog({
|
|
|
642
725
|
<Button type="button" variant="outline" onClick={() => { void guardedClose() }} className="rounded-md border border-input bg-background px-5 py-3 text-sm font-semibold text-foreground">
|
|
643
726
|
{t('customers.schedule.cancel', 'Cancel')}
|
|
644
727
|
</Button>
|
|
645
|
-
<Button type="button" onClick={handleSave} disabled={
|
|
728
|
+
<Button type="button" onClick={handleSave} disabled={isSubmitDisabled} className="flex items-center gap-2 rounded-md bg-foreground px-5 py-3 text-sm font-semibold text-background hover:bg-foreground/90 disabled:opacity-50">
|
|
646
729
|
<SaveIcon className="size-3.5" />
|
|
647
730
|
{state.saving
|
|
648
731
|
? t('customers.schedule.saving', 'Saving...')
|
|
@@ -65,6 +65,11 @@ export function DateTimeFields({
|
|
|
65
65
|
const showAllDay = isVisible(activityType, 'allDay')
|
|
66
66
|
const showRecurrence = isVisible(activityType, 'recurrence')
|
|
67
67
|
|
|
68
|
+
const dateMissing = !date.trim()
|
|
69
|
+
const timeMissing = showStartTime && !allDay && !startTime.trim()
|
|
70
|
+
const dateErrorId = 'schedule-date-error'
|
|
71
|
+
const timeErrorId = 'schedule-time-error'
|
|
72
|
+
|
|
68
73
|
return (
|
|
69
74
|
<>
|
|
70
75
|
{/* Date / Time / Duration */}
|
|
@@ -72,21 +77,62 @@ export function DateTimeFields({
|
|
|
72
77
|
<div className="flex flex-[2] flex-col gap-1.5">
|
|
73
78
|
<label className="text-overline font-semibold text-muted-foreground tracking-wider">
|
|
74
79
|
{getFieldLabel(activityType, 'date', t, 'customers.schedule.date', 'Date')}
|
|
80
|
+
<span aria-hidden="true" className="ml-1 text-status-error-foreground">*</span>
|
|
75
81
|
</label>
|
|
76
|
-
<div
|
|
82
|
+
<div
|
|
83
|
+
className={cn(
|
|
84
|
+
'flex items-center gap-2 rounded-md border bg-background px-3 py-2.5',
|
|
85
|
+
dateMissing ? 'border-status-error-border' : 'border-border',
|
|
86
|
+
)}
|
|
87
|
+
>
|
|
77
88
|
<Calendar className="size-3.5 text-muted-foreground" />
|
|
78
|
-
<input
|
|
89
|
+
<input
|
|
90
|
+
type="date"
|
|
91
|
+
value={date}
|
|
92
|
+
onChange={(e) => setDate(e.target.value)}
|
|
93
|
+
required
|
|
94
|
+
aria-required="true"
|
|
95
|
+
aria-invalid={dateMissing ? true : undefined}
|
|
96
|
+
aria-describedby={dateMissing ? dateErrorId : undefined}
|
|
97
|
+
className="flex-1 bg-transparent text-sm text-foreground focus:outline-none"
|
|
98
|
+
/>
|
|
79
99
|
</div>
|
|
100
|
+
{dateMissing ? (
|
|
101
|
+
<p id={dateErrorId} className="text-xs text-status-error-foreground">
|
|
102
|
+
{t('customers.activities.errors.dateRequired', 'Date is required')}
|
|
103
|
+
</p>
|
|
104
|
+
) : null}
|
|
80
105
|
</div>
|
|
81
106
|
{showStartTime && (
|
|
82
107
|
<div className="flex flex-1 flex-col gap-1.5">
|
|
83
108
|
<label className="text-overline font-semibold text-muted-foreground tracking-wider">
|
|
84
109
|
{getFieldLabel(activityType, 'startTime', t, 'customers.schedule.start', 'Start')}
|
|
110
|
+
<span aria-hidden="true" className="ml-1 text-status-error-foreground">*</span>
|
|
85
111
|
</label>
|
|
86
|
-
<div
|
|
112
|
+
<div
|
|
113
|
+
className={cn(
|
|
114
|
+
'flex items-center gap-2 rounded-md border bg-background px-3 py-2.5',
|
|
115
|
+
timeMissing ? 'border-status-error-border' : 'border-border',
|
|
116
|
+
)}
|
|
117
|
+
>
|
|
87
118
|
<Clock className="size-3.5 text-muted-foreground" />
|
|
88
|
-
<input
|
|
119
|
+
<input
|
|
120
|
+
type="time"
|
|
121
|
+
value={startTime}
|
|
122
|
+
onChange={(e) => setStartTime(e.target.value)}
|
|
123
|
+
disabled={allDay}
|
|
124
|
+
required={!allDay}
|
|
125
|
+
aria-required={!allDay}
|
|
126
|
+
aria-invalid={timeMissing ? true : undefined}
|
|
127
|
+
aria-describedby={timeMissing ? timeErrorId : undefined}
|
|
128
|
+
className="flex-1 bg-transparent text-sm text-foreground focus:outline-none disabled:opacity-50"
|
|
129
|
+
/>
|
|
89
130
|
</div>
|
|
131
|
+
{timeMissing ? (
|
|
132
|
+
<p id={timeErrorId} className="text-xs text-status-error-foreground">
|
|
133
|
+
{t('customers.activities.errors.timeRequired', 'Time is required')}
|
|
134
|
+
</p>
|
|
135
|
+
) : null}
|
|
90
136
|
</div>
|
|
91
137
|
)}
|
|
92
138
|
{showDuration && (
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
|
+
import { format } from 'date-fns'
|
|
2
3
|
import type { ActivityType } from './fieldConfig'
|
|
3
4
|
|
|
4
5
|
export type RsvpStatus = 'pending' | 'accepted' | 'declined' | 'tentative'
|
|
@@ -23,6 +24,12 @@ export type ScheduleActivityEditData = {
|
|
|
23
24
|
title?: string | null
|
|
24
25
|
body?: string | null
|
|
25
26
|
scheduledAt?: string | null
|
|
27
|
+
/**
|
|
28
|
+
* Historical timestamp for completed activities (status `done`). Required for
|
|
29
|
+
* the edit prefill to restore the original date/time instead of falling back
|
|
30
|
+
* to "today" (#1807).
|
|
31
|
+
*/
|
|
32
|
+
occurredAt?: string | null
|
|
26
33
|
durationMinutes?: number | null
|
|
27
34
|
location?: string | null
|
|
28
35
|
allDay?: boolean | null
|
|
@@ -64,7 +71,7 @@ interface UseScheduleFormStateParams {
|
|
|
64
71
|
export function useScheduleFormState({ open, editData }: UseScheduleFormStateParams) {
|
|
65
72
|
const [activityType, setActivityType] = React.useState<ActivityType>('meeting')
|
|
66
73
|
const [title, setTitle] = React.useState('')
|
|
67
|
-
const [date, setDate] = React.useState(() => new Date()
|
|
74
|
+
const [date, setDate] = React.useState(() => format(new Date(), 'yyyy-MM-dd'))
|
|
68
75
|
const [startTime, setStartTime] = React.useState('10:00')
|
|
69
76
|
const [duration, setDuration] = React.useState(30)
|
|
70
77
|
const [allDay, setAllDay] = React.useState(false)
|
|
@@ -91,9 +98,18 @@ export function useScheduleFormState({ open, editData }: UseScheduleFormStatePar
|
|
|
91
98
|
const resolvedType = (editData.interactionType as ActivityType) ?? 'meeting'
|
|
92
99
|
setActivityType(resolvedType)
|
|
93
100
|
setTitle(editData.title ?? '')
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
101
|
+
// For historical activities the canonical timestamp is `occurredAt`; for
|
|
102
|
+
// planned/future ones it's `scheduledAt`. Without this fallback editing a
|
|
103
|
+
// past activity prefilled to "today" instead of its actual moment (#1807).
|
|
104
|
+
// Use `date-fns` `format(...)` so the seed values are in the user's local
|
|
105
|
+
// timezone (matches the cluster-E local-day convention).
|
|
106
|
+
const sourceTimestamp = editData.occurredAt ?? editData.scheduledAt ?? null
|
|
107
|
+
const seedDate = sourceTimestamp ? new Date(sourceTimestamp) : new Date()
|
|
108
|
+
const seedDateValid = !Number.isNaN(seedDate.getTime())
|
|
109
|
+
const fallbackNow = new Date()
|
|
110
|
+
const dateForForm = seedDateValid ? seedDate : fallbackNow
|
|
111
|
+
setDate(format(dateForForm, 'yyyy-MM-dd'))
|
|
112
|
+
setStartTime(format(dateForForm, 'HH:mm'))
|
|
97
113
|
setDuration(editData.durationMinutes ?? 30)
|
|
98
114
|
setAllDay(editData.allDay ?? false)
|
|
99
115
|
setDescription(editData.body ?? '')
|
|
@@ -154,7 +170,7 @@ export function useScheduleFormState({ open, editData }: UseScheduleFormStatePar
|
|
|
154
170
|
// Create mode: reset all fields
|
|
155
171
|
setActivityType('meeting')
|
|
156
172
|
setTitle('')
|
|
157
|
-
setDate(new Date()
|
|
173
|
+
setDate(format(new Date(), 'yyyy-MM-dd'))
|
|
158
174
|
setStartTime('10:00')
|
|
159
175
|
setDuration(30)
|
|
160
176
|
setAllDay(false)
|
|
@@ -4,11 +4,17 @@ import { isValidPhoneNumber } from '@open-mercato/shared/lib/phone'
|
|
|
4
4
|
const uuid = () => z.string().uuid()
|
|
5
5
|
|
|
6
6
|
export const CUSTOMER_PHONE_INVALID_MESSAGE_KEY = 'customers.people.form.primaryPhone.invalid'
|
|
7
|
+
export const ACTIVITY_DATE_REQUIRED_MESSAGE_KEY = 'customers.activities.errors.dateRequired'
|
|
8
|
+
export const ACTIVITY_TIME_REQUIRED_MESSAGE_KEY = 'customers.activities.errors.timeRequired'
|
|
9
|
+
export const ACTIVITY_PHONE_REQUIRED_MESSAGE_KEY = 'customers.activities.errors.phoneRequired'
|
|
10
|
+
export const ACTIVITY_PHONE_INVALID_MESSAGE_KEY = 'customers.activities.errors.phoneInvalid'
|
|
7
11
|
|
|
8
12
|
const phoneSchema = z.string().trim().max(50).refine((val) => {
|
|
9
13
|
return isValidPhoneNumber(val)
|
|
10
14
|
}, { message: CUSTOMER_PHONE_INVALID_MESSAGE_KEY }).optional()
|
|
11
15
|
|
|
16
|
+
const interactionPhoneNumberSchema = z.string().trim().max(50).optional().nullable()
|
|
17
|
+
|
|
12
18
|
const scopedSchema = z.object({
|
|
13
19
|
organizationId: uuid(),
|
|
14
20
|
tenantId: uuid(),
|
|
@@ -140,6 +146,9 @@ export const activityCreateSchema = scopedSchema.extend({
|
|
|
140
146
|
activityType: z.string().min(1).max(100),
|
|
141
147
|
subject: z.string().max(200).optional(),
|
|
142
148
|
body: z.string().max(8000).optional(),
|
|
149
|
+
date: z.string().trim().min(1, ACTIVITY_DATE_REQUIRED_MESSAGE_KEY).optional(),
|
|
150
|
+
time: z.string().trim().min(1, ACTIVITY_TIME_REQUIRED_MESSAGE_KEY).optional(),
|
|
151
|
+
phoneNumber: interactionPhoneNumberSchema,
|
|
143
152
|
occurredAt: z.coerce.date().optional(),
|
|
144
153
|
dealId: uuid().optional(),
|
|
145
154
|
authorUserId: uuid().optional(),
|
|
@@ -351,13 +360,16 @@ const interactionExtendedFields = {
|
|
|
351
360
|
guestPermissions: interactionGuestPermissionsSchema.optional().nullable(),
|
|
352
361
|
} as const
|
|
353
362
|
|
|
354
|
-
|
|
363
|
+
const interactionCreateBaseSchema = scopedSchema.extend({
|
|
355
364
|
id: z.string().uuid().optional(),
|
|
356
365
|
entityId: z.string().uuid(),
|
|
357
366
|
interactionType: z.string().trim().min(1).max(100),
|
|
358
367
|
title: z.string().trim().max(500).optional().nullable(),
|
|
359
368
|
body: z.string().trim().max(10000).optional().nullable(),
|
|
360
369
|
status: z.enum(interactionStatusValues).optional().default('planned'),
|
|
370
|
+
date: z.string().trim().min(1, ACTIVITY_DATE_REQUIRED_MESSAGE_KEY).optional(),
|
|
371
|
+
time: z.string().trim().min(1, ACTIVITY_TIME_REQUIRED_MESSAGE_KEY).optional(),
|
|
372
|
+
phoneNumber: interactionPhoneNumberSchema,
|
|
361
373
|
scheduledAt: z.coerce.date().optional().nullable(),
|
|
362
374
|
occurredAt: z.coerce.date().optional().nullable(),
|
|
363
375
|
priority: z.number().int().min(0).max(100).optional().nullable(),
|
|
@@ -370,9 +382,48 @@ export const interactionCreateSchema = scopedSchema.extend({
|
|
|
370
382
|
...interactionExtendedFields,
|
|
371
383
|
})
|
|
372
384
|
|
|
385
|
+
function deriveScheduledAtFromDateTime(date?: string, time?: string): Date | null {
|
|
386
|
+
if (!date || typeof date !== 'string') return null
|
|
387
|
+
const trimmedDate = date.trim()
|
|
388
|
+
if (!trimmedDate) return null
|
|
389
|
+
const trimmedTime = typeof time === 'string' ? time.trim() : ''
|
|
390
|
+
const iso = trimmedTime ? `${trimmedDate}T${trimmedTime}:00` : `${trimmedDate}T00:00:00`
|
|
391
|
+
const parsed = new Date(iso)
|
|
392
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export const interactionCreateSchema = interactionCreateBaseSchema
|
|
396
|
+
.superRefine((value, ctx) => {
|
|
397
|
+
if (value.interactionType === 'call' && value.phoneNumber !== undefined && value.phoneNumber !== null) {
|
|
398
|
+
const phone = typeof value.phoneNumber === 'string' ? value.phoneNumber.trim() : ''
|
|
399
|
+
if (!phone) {
|
|
400
|
+
ctx.addIssue({
|
|
401
|
+
code: z.ZodIssueCode.custom,
|
|
402
|
+
path: ['phoneNumber'],
|
|
403
|
+
message: ACTIVITY_PHONE_REQUIRED_MESSAGE_KEY,
|
|
404
|
+
})
|
|
405
|
+
} else if (!isValidPhoneNumber(phone)) {
|
|
406
|
+
ctx.addIssue({
|
|
407
|
+
code: z.ZodIssueCode.custom,
|
|
408
|
+
path: ['phoneNumber'],
|
|
409
|
+
message: ACTIVITY_PHONE_INVALID_MESSAGE_KEY,
|
|
410
|
+
})
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
})
|
|
414
|
+
// Derive `scheduledAt` from `date+time` when only the latter are sent so
|
|
415
|
+
// external API consumers don't silently persist `scheduled_at: null` after
|
|
416
|
+
// the validator already enforced non-empty date/time. The form already
|
|
417
|
+
// computes `scheduledAt` itself, so this branch is a no-op for the form path.
|
|
418
|
+
.transform((value) => {
|
|
419
|
+
if (value.scheduledAt) return value
|
|
420
|
+
const derived = deriveScheduledAtFromDateTime(value.date, value.time)
|
|
421
|
+
return derived ? { ...value, scheduledAt: derived } : value
|
|
422
|
+
})
|
|
423
|
+
|
|
373
424
|
export type InteractionCreateInput = z.infer<typeof interactionCreateSchema>
|
|
374
425
|
|
|
375
|
-
|
|
426
|
+
const interactionUpdateBaseSchema = z
|
|
376
427
|
.object({
|
|
377
428
|
id: z.string().uuid(),
|
|
378
429
|
})
|
|
@@ -383,6 +434,9 @@ export const interactionUpdateSchema = z
|
|
|
383
434
|
title: z.string().trim().max(500).optional().nullable(),
|
|
384
435
|
body: z.string().trim().max(10000).optional().nullable(),
|
|
385
436
|
status: z.enum(interactionStatusValues).optional(),
|
|
437
|
+
date: z.string().trim().min(1, ACTIVITY_DATE_REQUIRED_MESSAGE_KEY).optional(),
|
|
438
|
+
time: z.string().trim().min(1, ACTIVITY_TIME_REQUIRED_MESSAGE_KEY).optional(),
|
|
439
|
+
phoneNumber: interactionPhoneNumberSchema,
|
|
386
440
|
scheduledAt: z.coerce.date().optional().nullable(),
|
|
387
441
|
occurredAt: z.coerce.date().optional().nullable(),
|
|
388
442
|
priority: z.number().int().min(0).max(100).optional().nullable(),
|
|
@@ -397,6 +451,35 @@ export const interactionUpdateSchema = z
|
|
|
397
451
|
.partial(),
|
|
398
452
|
)
|
|
399
453
|
|
|
454
|
+
export const interactionUpdateSchema = interactionUpdateBaseSchema
|
|
455
|
+
.superRefine((value, ctx) => {
|
|
456
|
+
if (value.interactionType === 'call' && value.phoneNumber !== undefined) {
|
|
457
|
+
const phone = typeof value.phoneNumber === 'string' ? value.phoneNumber.trim() : ''
|
|
458
|
+
if (!phone) {
|
|
459
|
+
ctx.addIssue({
|
|
460
|
+
code: z.ZodIssueCode.custom,
|
|
461
|
+
path: ['phoneNumber'],
|
|
462
|
+
message: ACTIVITY_PHONE_REQUIRED_MESSAGE_KEY,
|
|
463
|
+
})
|
|
464
|
+
} else if (!isValidPhoneNumber(phone)) {
|
|
465
|
+
ctx.addIssue({
|
|
466
|
+
code: z.ZodIssueCode.custom,
|
|
467
|
+
path: ['phoneNumber'],
|
|
468
|
+
message: ACTIVITY_PHONE_INVALID_MESSAGE_KEY,
|
|
469
|
+
})
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
})
|
|
473
|
+
// Mirror the create-schema derivation for partial updates: when an external
|
|
474
|
+
// caller supplies `date+time` without `scheduledAt`, derive the timestamp so
|
|
475
|
+
// the update doesn't silently leave `scheduled_at` stale.
|
|
476
|
+
.transform((value) => {
|
|
477
|
+
if (value.scheduledAt !== undefined) return value
|
|
478
|
+
if (!value.date && !value.time) return value
|
|
479
|
+
const derived = deriveScheduledAtFromDateTime(value.date, value.time)
|
|
480
|
+
return derived ? { ...value, scheduledAt: derived } : value
|
|
481
|
+
})
|
|
482
|
+
|
|
400
483
|
export type InteractionUpdateInput = z.infer<typeof interactionUpdateSchema>
|
|
401
484
|
|
|
402
485
|
export const interactionCompleteSchema = z.object({
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
"audit_logs.resource_kind.customers.comment": "Kommentar",
|
|
5
5
|
"audit_logs.resource_kind.customers.todoLink": "Aufgabe",
|
|
6
6
|
"backend.nav.configuration": "Konfiguration",
|
|
7
|
+
"customers.activities.actions.markDone": "Als erledigt markieren",
|
|
8
|
+
"customers.activities.actions.markDoneError": "Aktivität konnte nicht als erledigt markiert werden",
|
|
9
|
+
"customers.activities.actions.markDoneSuccess": "Aktivität als erledigt markiert",
|
|
7
10
|
"customers.activities.add.call": "Anruf protokollieren",
|
|
8
11
|
"customers.activities.add.email": "E-Mail verfassen",
|
|
9
12
|
"customers.activities.add.meeting": "Neues Meeting",
|
|
@@ -24,6 +27,10 @@
|
|
|
24
27
|
"customers.activities.card.empty": "Für diesen Tag ist nichts geplant.",
|
|
25
28
|
"customers.activities.card.overdue": "{count} überfällig",
|
|
26
29
|
"customers.activities.card.title": "Aktivitäten",
|
|
30
|
+
"customers.activities.errors.dateRequired": "Datum ist erforderlich",
|
|
31
|
+
"customers.activities.errors.phoneInvalid": "Geben Sie eine gültige Telefonnummer mit Ländervorwahl ein (z. B. +49 30 1234567)",
|
|
32
|
+
"customers.activities.errors.phoneRequired": "Telefonnummer ist für Anrufaktivitäten erforderlich",
|
|
33
|
+
"customers.activities.errors.timeRequired": "Uhrzeit ist erforderlich",
|
|
27
34
|
"customers.activities.filters.clearAll": "Clear filters",
|
|
28
35
|
"customers.activities.filters.dateRange": "Date range",
|
|
29
36
|
"customers.activities.loadFailed": "Aktivitäten konnten nicht geladen werden.",
|
|
@@ -52,6 +59,7 @@
|
|
|
52
59
|
"customers.activityComposer.types.email": "Email",
|
|
53
60
|
"customers.activityComposer.types.meeting": "Meeting",
|
|
54
61
|
"customers.activityComposer.types.note": "Note",
|
|
62
|
+
"customers.activityComposer.types.task": "Aufgabe",
|
|
55
63
|
"customers.activityComposer.validation.descriptionRequired": "Description is required",
|
|
56
64
|
"customers.activityComposer.validation.typeRequired": "Select an activity type",
|
|
57
65
|
"customers.activityComposer.weekPreviewTitle": "Diese Woche",
|
|
@@ -59,6 +67,8 @@
|
|
|
59
67
|
"customers.activityLog.direction.with": "with",
|
|
60
68
|
"customers.activityLog.emptyDescription": "Try broadening the date range or removing some filters.",
|
|
61
69
|
"customers.activityLog.error": "Failed to load activity history",
|
|
70
|
+
"customers.activityLog.filters.dateRangeLabel": "Nach Datumsbereich filtern",
|
|
71
|
+
"customers.activityLog.filters.sortLabel": "Aktivitäten sortieren",
|
|
62
72
|
"customers.activityLog.searchPlaceholder": "Search by title, note, or author",
|
|
63
73
|
"customers.activityLog.sort.recent": "Sort: newest",
|
|
64
74
|
"customers.activityLog.sort.titleAsc": "Sort: Name A-Z",
|
|
@@ -2022,6 +2032,7 @@
|
|
|
2022
2032
|
"customers.timeline.filter.from": "From date",
|
|
2023
2033
|
"customers.timeline.filter.meeting": "Meeting",
|
|
2024
2034
|
"customers.timeline.filter.note": "Note",
|
|
2035
|
+
"customers.timeline.filter.task": "Aufgabe",
|
|
2025
2036
|
"customers.timeline.filter.to": "To date",
|
|
2026
2037
|
"customers.timeline.history.filtered": "filtered: {{types}} · {{count}} results",
|
|
2027
2038
|
"customers.timeline.history.searchAriaLabel": "",
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
"audit_logs.resource_kind.customers.comment": "Comment",
|
|
5
5
|
"audit_logs.resource_kind.customers.todoLink": "Todo",
|
|
6
6
|
"backend.nav.configuration": "Configuration",
|
|
7
|
+
"customers.activities.actions.markDone": "Mark done",
|
|
8
|
+
"customers.activities.actions.markDoneError": "Could not mark activity as done",
|
|
9
|
+
"customers.activities.actions.markDoneSuccess": "Activity marked done",
|
|
7
10
|
"customers.activities.add.call": "Log call",
|
|
8
11
|
"customers.activities.add.email": "Compose email",
|
|
9
12
|
"customers.activities.add.meeting": "New meeting",
|
|
@@ -24,6 +27,10 @@
|
|
|
24
27
|
"customers.activities.card.empty": "Nothing scheduled for this day.",
|
|
25
28
|
"customers.activities.card.overdue": "{count} overdue",
|
|
26
29
|
"customers.activities.card.title": "Activities",
|
|
30
|
+
"customers.activities.errors.dateRequired": "Date is required",
|
|
31
|
+
"customers.activities.errors.phoneInvalid": "Enter a valid phone number with country code (e.g. +1 212 555 1234)",
|
|
32
|
+
"customers.activities.errors.phoneRequired": "Phone number is required for Call activities",
|
|
33
|
+
"customers.activities.errors.timeRequired": "Time is required",
|
|
27
34
|
"customers.activities.filters.clearAll": "Clear filters",
|
|
28
35
|
"customers.activities.filters.dateRange": "Date range",
|
|
29
36
|
"customers.activities.loadFailed": "Failed to load activities.",
|
|
@@ -52,6 +59,7 @@
|
|
|
52
59
|
"customers.activityComposer.types.email": "Email",
|
|
53
60
|
"customers.activityComposer.types.meeting": "Meeting",
|
|
54
61
|
"customers.activityComposer.types.note": "Note",
|
|
62
|
+
"customers.activityComposer.types.task": "Task",
|
|
55
63
|
"customers.activityComposer.validation.descriptionRequired": "Description is required",
|
|
56
64
|
"customers.activityComposer.validation.typeRequired": "Select an activity type",
|
|
57
65
|
"customers.activityComposer.weekPreviewTitle": "This week",
|
|
@@ -59,6 +67,8 @@
|
|
|
59
67
|
"customers.activityLog.direction.with": "with",
|
|
60
68
|
"customers.activityLog.emptyDescription": "Try broadening the date range or removing some filters.",
|
|
61
69
|
"customers.activityLog.error": "Failed to load activity history",
|
|
70
|
+
"customers.activityLog.filters.dateRangeLabel": "Date range",
|
|
71
|
+
"customers.activityLog.filters.sortLabel": "Sort order",
|
|
62
72
|
"customers.activityLog.searchPlaceholder": "Search by title, note, or author",
|
|
63
73
|
"customers.activityLog.sort.recent": "Sort: newest",
|
|
64
74
|
"customers.activityLog.sort.titleAsc": "Sort: Name A-Z",
|
|
@@ -2022,6 +2032,7 @@
|
|
|
2022
2032
|
"customers.timeline.filter.from": "From date",
|
|
2023
2033
|
"customers.timeline.filter.meeting": "Meeting",
|
|
2024
2034
|
"customers.timeline.filter.note": "Note",
|
|
2035
|
+
"customers.timeline.filter.task": "Task",
|
|
2025
2036
|
"customers.timeline.filter.to": "To date",
|
|
2026
2037
|
"customers.timeline.history.filtered": "filtered: {{types}} · {{count}} results",
|
|
2027
2038
|
"customers.timeline.history.searchAriaLabel": "Search interaction history",
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
"audit_logs.resource_kind.customers.comment": "Comentario",
|
|
5
5
|
"audit_logs.resource_kind.customers.todoLink": "Tarea",
|
|
6
6
|
"backend.nav.configuration": "Configuración",
|
|
7
|
+
"customers.activities.actions.markDone": "Marcar como hecho",
|
|
8
|
+
"customers.activities.actions.markDoneError": "No se pudo marcar la actividad como hecha",
|
|
9
|
+
"customers.activities.actions.markDoneSuccess": "Actividad marcada como hecha",
|
|
7
10
|
"customers.activities.add.call": "Registrar llamada",
|
|
8
11
|
"customers.activities.add.email": "Redactar correo",
|
|
9
12
|
"customers.activities.add.meeting": "Nueva reunión",
|
|
@@ -24,6 +27,10 @@
|
|
|
24
27
|
"customers.activities.card.empty": "Nada programado para este día.",
|
|
25
28
|
"customers.activities.card.overdue": "{count} vencidas",
|
|
26
29
|
"customers.activities.card.title": "Actividades",
|
|
30
|
+
"customers.activities.errors.dateRequired": "La fecha es obligatoria",
|
|
31
|
+
"customers.activities.errors.phoneInvalid": "Introduce un número de teléfono válido con prefijo internacional (p. ej., +34 912 345 678)",
|
|
32
|
+
"customers.activities.errors.phoneRequired": "El número de teléfono es obligatorio para actividades de llamada",
|
|
33
|
+
"customers.activities.errors.timeRequired": "La hora es obligatoria",
|
|
27
34
|
"customers.activities.filters.clearAll": "Clear filters",
|
|
28
35
|
"customers.activities.filters.dateRange": "Date range",
|
|
29
36
|
"customers.activities.loadFailed": "No se pudieron cargar las actividades.",
|
|
@@ -52,6 +59,7 @@
|
|
|
52
59
|
"customers.activityComposer.types.email": "Email",
|
|
53
60
|
"customers.activityComposer.types.meeting": "Meeting",
|
|
54
61
|
"customers.activityComposer.types.note": "Note",
|
|
62
|
+
"customers.activityComposer.types.task": "Tarea",
|
|
55
63
|
"customers.activityComposer.validation.descriptionRequired": "Description is required",
|
|
56
64
|
"customers.activityComposer.validation.typeRequired": "Select an activity type",
|
|
57
65
|
"customers.activityComposer.weekPreviewTitle": "Esta semana",
|
|
@@ -59,6 +67,8 @@
|
|
|
59
67
|
"customers.activityLog.direction.with": "with",
|
|
60
68
|
"customers.activityLog.emptyDescription": "Try broadening the date range or removing some filters.",
|
|
61
69
|
"customers.activityLog.error": "Failed to load activity history",
|
|
70
|
+
"customers.activityLog.filters.dateRangeLabel": "Filtrar por rango de fechas",
|
|
71
|
+
"customers.activityLog.filters.sortLabel": "Ordenar actividades",
|
|
62
72
|
"customers.activityLog.searchPlaceholder": "Search by title, note, or author",
|
|
63
73
|
"customers.activityLog.sort.recent": "Sort: newest",
|
|
64
74
|
"customers.activityLog.sort.titleAsc": "Sort: Name A-Z",
|
|
@@ -2022,6 +2032,7 @@
|
|
|
2022
2032
|
"customers.timeline.filter.from": "From date",
|
|
2023
2033
|
"customers.timeline.filter.meeting": "Meeting",
|
|
2024
2034
|
"customers.timeline.filter.note": "Note",
|
|
2035
|
+
"customers.timeline.filter.task": "Tarea",
|
|
2025
2036
|
"customers.timeline.filter.to": "To date",
|
|
2026
2037
|
"customers.timeline.history.filtered": "filtered: {{types}} · {{count}} results",
|
|
2027
2038
|
"customers.timeline.history.searchAriaLabel": "",
|