@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/customers/backend/calendar/page.js +9 -1
- package/dist/modules/customers/backend/calendar/page.js.map +2 -2
- package/dist/modules/customers/components/calendar/AgendaList.js +0 -11
- package/dist/modules/customers/components/calendar/AgendaList.js.map +2 -2
- package/dist/modules/customers/components/calendar/CalendarEventEditor.js +391 -235
- package/dist/modules/customers/components/calendar/CalendarEventEditor.js.map +2 -2
- package/dist/modules/customers/components/calendar/CalendarScreen.js +5 -5
- package/dist/modules/customers/components/calendar/CalendarScreen.js.map +2 -2
- package/dist/modules/customers/components/calendar/CalendarSettingsModal.js +2 -2
- package/dist/modules/customers/components/calendar/CalendarSettingsModal.js.map +2 -2
- package/dist/modules/customers/components/calendar/MonthGrid.js +1 -11
- package/dist/modules/customers/components/calendar/MonthGrid.js.map +2 -2
- package/dist/modules/customers/components/calendar/TimeGrid.js +162 -176
- package/dist/modules/customers/components/calendar/TimeGrid.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/LocationField.js +61 -14
- package/dist/modules/customers/components/calendar/editor/LocationField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/PeopleField.js +10 -5
- package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/PriorityField.js +85 -0
- package/dist/modules/customers/components/calendar/editor/PriorityField.js.map +7 -0
- package/dist/modules/customers/components/calendar/editor/RelatedToField.js +8 -3
- package/dist/modules/customers/components/calendar/editor/RelatedToField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/ResourcesField.js +225 -0
- package/dist/modules/customers/components/calendar/editor/ResourcesField.js.map +7 -0
- package/dist/modules/customers/components/calendar/editor/SegmentGroup.js +12 -7
- package/dist/modules/customers/components/calendar/editor/SegmentGroup.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/inputs.js +68 -57
- package/dist/modules/customers/components/calendar/editor/inputs.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/lookups.js +82 -2
- package/dist/modules/customers/components/calendar/editor/lookups.js.map +2 -2
- package/dist/modules/customers/components/calendar/useCalendarItems.js +6 -1
- package/dist/modules/customers/components/calendar/useCalendarItems.js.map +2 -2
- package/dist/modules/customers/data/validators.js +3 -1
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/customers/lib/calendar/editorPayload.js +53 -22
- package/dist/modules/customers/lib/calendar/editorPayload.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/page.js +1 -1
- package/dist/modules/entities/backend/entities/user/[entityId]/page.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/customers/backend/calendar/page.tsx +8 -1
- package/src/modules/customers/components/calendar/AgendaList.tsx +0 -10
- package/src/modules/customers/components/calendar/CalendarEventEditor.tsx +444 -261
- package/src/modules/customers/components/calendar/CalendarScreen.tsx +12 -5
- package/src/modules/customers/components/calendar/CalendarSettingsModal.tsx +6 -6
- package/src/modules/customers/components/calendar/MonthGrid.tsx +0 -11
- package/src/modules/customers/components/calendar/TimeGrid.tsx +1 -24
- package/src/modules/customers/components/calendar/editor/LocationField.tsx +64 -2
- package/src/modules/customers/components/calendar/editor/PeopleField.tsx +11 -4
- package/src/modules/customers/components/calendar/editor/PriorityField.tsx +90 -0
- package/src/modules/customers/components/calendar/editor/RelatedToField.tsx +8 -3
- package/src/modules/customers/components/calendar/editor/ResourcesField.tsx +264 -0
- package/src/modules/customers/components/calendar/editor/SegmentGroup.tsx +14 -6
- package/src/modules/customers/components/calendar/editor/inputs.tsx +91 -50
- package/src/modules/customers/components/calendar/editor/lookups.ts +115 -2
- package/src/modules/customers/components/calendar/useCalendarItems.ts +8 -1
- package/src/modules/customers/data/validators.ts +3 -1
- package/src/modules/customers/i18n/de.json +12 -2
- package/src/modules/customers/i18n/en.json +12 -2
- package/src/modules/customers/i18n/es.json +12 -2
- package/src/modules/customers/i18n/pl.json +12 -2
- package/src/modules/customers/lib/calendar/editorPayload.ts +94 -37
- package/src/modules/entities/backend/entities/user/[entityId]/page.tsx +1 -1
- package/src/modules/entities/i18n/de.json +120 -0
- package/src/modules/entities/i18n/en.json +120 -0
- package/src/modules/entities/i18n/es.json +120 -0
- package/src/modules/entities/i18n/pl.json +120 -0
- package/dist/modules/customers/components/calendar/editor/CategoryField.js +0 -75
- package/dist/modules/customers/components/calendar/editor/CategoryField.js.map +0 -7
- package/src/modules/customers/components/calendar/editor/CategoryField.tsx +0 -79
|
@@ -107,7 +107,14 @@ function asEditableItem(item: CalendarItem): CalendarItem {
|
|
|
107
107
|
return item.isRecurringOccurrence ? { ...item, id: item.raw.id } : item
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
export
|
|
110
|
+
export type CalendarScreenProps = {
|
|
111
|
+
/** True when the optional resources module is loaded (server-resolved). */
|
|
112
|
+
resourcesEnabled?: boolean
|
|
113
|
+
/** True when the optional staff module is loaded (server-resolved). */
|
|
114
|
+
staffEnabled?: boolean
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function CalendarScreen({ resourcesEnabled = false, staffEnabled = true }: CalendarScreenProps = {}) {
|
|
111
118
|
const t = useT()
|
|
112
119
|
const [view, setView] = React.useState<CalendarView>('week')
|
|
113
120
|
const [anchor, setAnchor] = React.useState<Date>(() => new Date())
|
|
@@ -136,7 +143,7 @@ export function CalendarScreen() {
|
|
|
136
143
|
() => getVisibleRange(view, anchor, agendaHorizonDays),
|
|
137
144
|
[view, anchor, agendaHorizonDays],
|
|
138
145
|
)
|
|
139
|
-
const { items, isLoading, error, truncated, typeLabels, typeColors, refetch } = useCalendarItems(range)
|
|
146
|
+
const { items, isLoading, error, truncated, typeLabels, typeColors, typeIcons, refetch } = useCalendarItems(range)
|
|
140
147
|
|
|
141
148
|
React.useEffect(() => {
|
|
142
149
|
if (!isLoading) setHasLoadedOnce(true)
|
|
@@ -577,11 +584,11 @@ export function CalendarScreen() {
|
|
|
577
584
|
defaultDate={anchor}
|
|
578
585
|
defaultRange={createRange}
|
|
579
586
|
typeLabels={typeLabels}
|
|
580
|
-
|
|
581
|
-
surfacedTypes={preferences.activityTypes}
|
|
582
|
-
eventCategories={preferences.eventCategories}
|
|
587
|
+
typeIcons={typeIcons}
|
|
583
588
|
conflictScope={preferences.conflictScope}
|
|
584
589
|
currentUserId={currentUserId}
|
|
590
|
+
resourcesEnabled={resourcesEnabled}
|
|
591
|
+
staffEnabled={staffEnabled}
|
|
585
592
|
onOpenChange={(open) => setEditor((current) => ({ ...current, open }))}
|
|
586
593
|
onSaved={refetch}
|
|
587
594
|
/>
|
|
@@ -30,10 +30,10 @@ export type CalendarSettingsModalProps = {
|
|
|
30
30
|
type ToggleKey = 'showCrmActivities' | 'aiSummaries' | 'conflictWarnings' | 'showWeekends'
|
|
31
31
|
|
|
32
32
|
// An empty Activity Types list is an intentional floor meaning "surface all
|
|
33
|
-
// dictionary types"
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
33
|
+
// dictionary types" rather than "surface none". When the stored list is empty
|
|
34
|
+
// the modal seeds the dictionary types for display. NOTE: since the editor's
|
|
35
|
+
// Category quick-pick was removed (owner feedback, #3552) these lists no longer
|
|
36
|
+
// affect the event editor — the type switcher always shows the full dictionary.
|
|
37
37
|
function buildDraft(preferences: CalendarPreferences, seedActivityTypes: string[]): CalendarPreferences {
|
|
38
38
|
return {
|
|
39
39
|
...preferences,
|
|
@@ -162,8 +162,8 @@ export function CalendarSettingsModal({
|
|
|
162
162
|
value={draft.conflictScope}
|
|
163
163
|
onChange={(conflictScope) => setDraft((current) => ({ ...current, conflictScope }))}
|
|
164
164
|
options={[
|
|
165
|
-
{ value: 'mine', label: t('customers.calendar.settings.conflictScopeMine', 'My meetings
|
|
166
|
-
{ value: 'all', label: t('customers.calendar.settings.conflictScopeAll', 'All
|
|
165
|
+
{ value: 'mine', label: t('customers.calendar.settings.conflictScopeMine', 'My meetings') },
|
|
166
|
+
{ value: 'all', label: t('customers.calendar.settings.conflictScopeAll', 'All meetings') },
|
|
167
167
|
]}
|
|
168
168
|
/>
|
|
169
169
|
</div>
|
|
@@ -5,11 +5,9 @@ import { addDays } from 'date-fns/addDays'
|
|
|
5
5
|
import { format } from 'date-fns/format'
|
|
6
6
|
import { isSameMonth } from 'date-fns/isSameMonth'
|
|
7
7
|
import { isToday } from 'date-fns/isToday'
|
|
8
|
-
import { CalendarDays } from 'lucide-react'
|
|
9
8
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
10
9
|
import { cn } from '@open-mercato/shared/lib/utils'
|
|
11
10
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
12
|
-
import { EmptyState } from '@open-mercato/ui/primitives/empty-state'
|
|
13
11
|
import { getVisibleRange } from '../../lib/calendar/range'
|
|
14
12
|
import { eventDisplayTitle } from '../../lib/calendar/labels'
|
|
15
13
|
import type { CalendarItem, MonthGridProps } from './types'
|
|
@@ -225,15 +223,6 @@ export function MonthGrid({ anchor, items, onItemClick, onDayOpen }: MonthGridPr
|
|
|
225
223
|
))}
|
|
226
224
|
</div>
|
|
227
225
|
))}
|
|
228
|
-
{items.length === 0 ? (
|
|
229
|
-
<div className="pointer-events-none absolute inset-0 z-10 flex items-center justify-center p-6">
|
|
230
|
-
<EmptyState
|
|
231
|
-
icon={<CalendarDays className="h-6 w-6" aria-hidden="true" />}
|
|
232
|
-
title={t('customers.calendar.empty.month', 'Nothing scheduled this month')}
|
|
233
|
-
className="bg-background/90"
|
|
234
|
-
/>
|
|
235
|
-
</div>
|
|
236
|
-
) : null}
|
|
237
226
|
</div>
|
|
238
227
|
)
|
|
239
228
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
-
import {
|
|
4
|
+
import { ChevronLeft, ChevronRight } from 'lucide-react'
|
|
5
5
|
import { addDays } from 'date-fns/addDays'
|
|
6
6
|
import { isSameDay } from 'date-fns/isSameDay'
|
|
7
7
|
import { startOfDay } from 'date-fns/startOfDay'
|
|
@@ -9,7 +9,6 @@ import { cn } from '@open-mercato/shared/lib/utils'
|
|
|
9
9
|
import { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'
|
|
10
10
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
11
11
|
import { IconButton } from '@open-mercato/ui/primitives/icon-button'
|
|
12
|
-
import { EmptyState } from '@open-mercato/ui/primitives/empty-state'
|
|
13
12
|
import { packOverlaps } from '../../lib/calendar/layout'
|
|
14
13
|
import { getVisibleRange } from '../../lib/calendar/range'
|
|
15
14
|
import {
|
|
@@ -165,7 +164,6 @@ export function TimeGrid({
|
|
|
165
164
|
onItemClick,
|
|
166
165
|
onJoin,
|
|
167
166
|
onNavigate,
|
|
168
|
-
onCreate,
|
|
169
167
|
onCreateRange,
|
|
170
168
|
}: TimeGridProps) {
|
|
171
169
|
const t = useT()
|
|
@@ -488,27 +486,6 @@ export function TimeGrid({
|
|
|
488
486
|
})}
|
|
489
487
|
</div>
|
|
490
488
|
</div>
|
|
491
|
-
{items.length === 0 ? (
|
|
492
|
-
<div className="pointer-events-none absolute inset-0 z-40 flex items-center justify-center p-6">
|
|
493
|
-
<EmptyState
|
|
494
|
-
className="pointer-events-auto bg-card/95 shadow-sm"
|
|
495
|
-
icon={<CalendarDays className="h-6 w-6" aria-hidden />}
|
|
496
|
-
title={
|
|
497
|
-
days === 7
|
|
498
|
-
? t('customers.calendar.empty.week', 'Nothing scheduled this week')
|
|
499
|
-
: t('customers.calendar.empty.day', 'Nothing scheduled this day')
|
|
500
|
-
}
|
|
501
|
-
description={t('customers.calendar.empty.description', 'Plan a meeting, event or task to fill your week.')}
|
|
502
|
-
actions={
|
|
503
|
-
onCreate ? (
|
|
504
|
-
<Button type="button" size="sm" onClick={onCreate}>
|
|
505
|
-
{t('customers.calendar.actions.newEvent', 'New event')}
|
|
506
|
-
</Button>
|
|
507
|
-
) : undefined
|
|
508
|
-
}
|
|
509
|
-
/>
|
|
510
|
-
</div>
|
|
511
|
-
) : null}
|
|
512
489
|
</div>
|
|
513
490
|
)
|
|
514
491
|
}
|
|
@@ -1,19 +1,76 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
-
import { MapPin } from 'lucide-react'
|
|
4
|
+
import { MapPin, Phone, Plus } from 'lucide-react'
|
|
5
5
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
6
6
|
import { Input } from '@open-mercato/ui/primitives/input'
|
|
7
7
|
import { Field } from './inputs'
|
|
8
|
+
import { fetchPeoplePhones, type ContactPhone } from './lookups'
|
|
9
|
+
|
|
10
|
+
// Below the Call "Phone / link" field: when a linked contact has a primary
|
|
11
|
+
// phone and the field is still empty, offer it as a one-click suggestion chip
|
|
12
|
+
// showing the actual number (#3552). Companies/staff have no phone and never
|
|
13
|
+
// appear. The chips vanish once the field is filled.
|
|
14
|
+
function PhoneSuggestions({
|
|
15
|
+
contactIds,
|
|
16
|
+
onPick,
|
|
17
|
+
}: {
|
|
18
|
+
contactIds: string[]
|
|
19
|
+
onPick(phone: string): void
|
|
20
|
+
}) {
|
|
21
|
+
const t = useT()
|
|
22
|
+
const [contacts, setContacts] = React.useState<ContactPhone[]>([])
|
|
23
|
+
const idsKey = contactIds.join(',')
|
|
24
|
+
|
|
25
|
+
React.useEffect(() => {
|
|
26
|
+
if (contactIds.length === 0) {
|
|
27
|
+
setContacts([])
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
const controller = new AbortController()
|
|
31
|
+
let cancelled = false
|
|
32
|
+
fetchPeoplePhones(contactIds, controller.signal).then((found) => {
|
|
33
|
+
if (!cancelled) setContacts(found)
|
|
34
|
+
})
|
|
35
|
+
return () => {
|
|
36
|
+
cancelled = true
|
|
37
|
+
controller.abort()
|
|
38
|
+
}
|
|
39
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- idsKey captures the ids
|
|
40
|
+
}, [idsKey])
|
|
41
|
+
|
|
42
|
+
if (contacts.length === 0) return null
|
|
43
|
+
return (
|
|
44
|
+
<div className="flex flex-wrap items-center gap-1.5 pt-1.5">
|
|
45
|
+
{contacts.map((contact) => (
|
|
46
|
+
<button
|
|
47
|
+
key={contact.id}
|
|
48
|
+
type="button"
|
|
49
|
+
onClick={() => onPick(contact.phone)}
|
|
50
|
+
title={t('customers.calendar.editor.phoneSuggestion.use', 'Use {name}’s number', { name: contact.name })}
|
|
51
|
+
className="inline-flex max-w-full items-center gap-1.5 rounded-md border border-border bg-background px-2 py-1 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
|
52
|
+
>
|
|
53
|
+
<Plus aria-hidden className="size-3 shrink-0" />
|
|
54
|
+
<span className="shrink-0">{t('customers.calendar.editor.phoneSuggestion.add', 'Add')}</span>
|
|
55
|
+
<span className="truncate">{contact.name}</span>
|
|
56
|
+
<span className="shrink-0 tabular-nums text-foreground">{contact.phone}</span>
|
|
57
|
+
</button>
|
|
58
|
+
))}
|
|
59
|
+
</div>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
8
62
|
|
|
9
63
|
export function LocationField({
|
|
10
64
|
variant,
|
|
11
65
|
value,
|
|
12
66
|
onChange,
|
|
67
|
+
phoneContactIds,
|
|
13
68
|
}: {
|
|
14
69
|
variant: 'location' | 'phoneLink'
|
|
15
70
|
value: string
|
|
16
71
|
onChange(next: string): void
|
|
72
|
+
/** Person ids whose phone can be suggested (Call variant only). */
|
|
73
|
+
phoneContactIds?: string[]
|
|
17
74
|
}) {
|
|
18
75
|
const t = useT()
|
|
19
76
|
const label = variant === 'phoneLink'
|
|
@@ -22,6 +79,8 @@ export function LocationField({
|
|
|
22
79
|
const placeholder = variant === 'phoneLink'
|
|
23
80
|
? t('customers.calendar.editor.phoneLinkPlaceholder', 'Add a phone number or link…')
|
|
24
81
|
: t('customers.calendar.editor.locationPlaceholder', 'Add a location or link…')
|
|
82
|
+
// Suggestions only help while the field is empty — once filled they'd be noise.
|
|
83
|
+
const showSuggestions = variant === 'phoneLink' && value.trim().length === 0
|
|
25
84
|
return (
|
|
26
85
|
<Field label={label}>
|
|
27
86
|
<Input
|
|
@@ -30,8 +89,11 @@ export function LocationField({
|
|
|
30
89
|
onChange={(event) => onChange(event.target.value)}
|
|
31
90
|
placeholder={placeholder}
|
|
32
91
|
aria-label={label}
|
|
33
|
-
leftIcon={<MapPin />}
|
|
92
|
+
leftIcon={variant === 'phoneLink' ? <Phone /> : <MapPin />}
|
|
34
93
|
/>
|
|
94
|
+
{showSuggestions ? (
|
|
95
|
+
<PhoneSuggestions contactIds={phoneContactIds ?? []} onPick={(phone) => onChange(phone)} />
|
|
96
|
+
) : null}
|
|
35
97
|
</Field>
|
|
36
98
|
)
|
|
37
99
|
}
|
|
@@ -9,7 +9,7 @@ import { Input } from '@open-mercato/ui/primitives/input'
|
|
|
9
9
|
import type { EditorParticipant } from '../../../lib/calendar/editorPayload'
|
|
10
10
|
import { composeAccessibleName } from '../../../lib/calendar/labels'
|
|
11
11
|
import { searchPeopleOptions, type PersonOption } from './lookups'
|
|
12
|
-
import { CONTROL_BORDER, DROPDOWN_PANEL_CLASS, PersonChip, UppercaseBadge } from './inputs'
|
|
12
|
+
import { CONTROL_BORDER, DROPDOWN_PANEL_CLASS, PersonChip, UppercaseBadge, useDropdownDismiss } from './inputs'
|
|
13
13
|
|
|
14
14
|
export function PeopleField({
|
|
15
15
|
mode,
|
|
@@ -18,6 +18,7 @@ export function PeopleField({
|
|
|
18
18
|
value,
|
|
19
19
|
onChange,
|
|
20
20
|
includeCustomers,
|
|
21
|
+
includeStaff = true,
|
|
21
22
|
}: {
|
|
22
23
|
mode: 'multi' | 'single'
|
|
23
24
|
placeholder: string
|
|
@@ -25,12 +26,16 @@ export function PeopleField({
|
|
|
25
26
|
value: EditorParticipant[]
|
|
26
27
|
onChange(next: EditorParticipant[]): void
|
|
27
28
|
includeCustomers: boolean
|
|
29
|
+
/** Pass false when the staff module is not loaded — customer-only options. */
|
|
30
|
+
includeStaff?: boolean
|
|
28
31
|
}) {
|
|
29
32
|
const t = useT()
|
|
30
33
|
const [query, setQuery] = React.useState('')
|
|
31
34
|
const [open, setOpen] = React.useState(false)
|
|
32
35
|
const [options, setOptions] = React.useState<PersonOption[]>([])
|
|
33
36
|
const [loading, setLoading] = React.useState(false)
|
|
37
|
+
const close = React.useCallback(() => setOpen(false), [])
|
|
38
|
+
const rootRef = useDropdownDismiss(open, close)
|
|
34
39
|
|
|
35
40
|
React.useEffect(() => {
|
|
36
41
|
if (!open) return
|
|
@@ -39,7 +44,7 @@ export function PeopleField({
|
|
|
39
44
|
const timer = window.setTimeout(async () => {
|
|
40
45
|
setLoading(true)
|
|
41
46
|
try {
|
|
42
|
-
const results = await searchPeopleOptions(query.trim(), { includeCustomers, signal: controller.signal })
|
|
47
|
+
const results = await searchPeopleOptions(query.trim(), { includeCustomers, includeStaff, signal: controller.signal })
|
|
43
48
|
if (cancelled) return
|
|
44
49
|
setOptions(results)
|
|
45
50
|
} catch {
|
|
@@ -53,7 +58,7 @@ export function PeopleField({
|
|
|
53
58
|
controller.abort()
|
|
54
59
|
window.clearTimeout(timer)
|
|
55
60
|
}
|
|
56
|
-
}, [open, query, includeCustomers])
|
|
61
|
+
}, [open, query, includeCustomers, includeStaff])
|
|
57
62
|
|
|
58
63
|
const selectedIds = new Set(value.map((participant) => participant.userId))
|
|
59
64
|
const visibleOptions = options.filter((option) => !selectedIds.has(option.userId))
|
|
@@ -66,6 +71,7 @@ export function PeopleField({
|
|
|
66
71
|
|
|
67
72
|
return (
|
|
68
73
|
<div
|
|
74
|
+
ref={rootRef}
|
|
69
75
|
className="relative w-full"
|
|
70
76
|
onBlur={(event) => {
|
|
71
77
|
if (!event.currentTarget.contains(event.relatedTarget as Node | null)) setOpen(false)
|
|
@@ -74,7 +80,7 @@ export function PeopleField({
|
|
|
74
80
|
<div
|
|
75
81
|
className={cn(
|
|
76
82
|
'flex w-full flex-wrap content-center items-center gap-2 rounded-md bg-background px-2.5 py-2',
|
|
77
|
-
|
|
83
|
+
'min-h-14',
|
|
78
84
|
CONTROL_BORDER,
|
|
79
85
|
)}
|
|
80
86
|
>
|
|
@@ -125,6 +131,7 @@ export function PeopleField({
|
|
|
125
131
|
option.email,
|
|
126
132
|
option.isCustomer ? t('customers.calendar.editor.customerBadge', 'Customer') : null,
|
|
127
133
|
])}
|
|
134
|
+
title={composeAccessibleName([option.name, option.email])}
|
|
128
135
|
onClick={() => {
|
|
129
136
|
const participant: EditorParticipant = {
|
|
130
137
|
userId: option.userId,
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { Check, ChevronDown, ChevronsDown, ChevronsUp, Equal } from 'lucide-react'
|
|
5
|
+
import { cn } from '@open-mercato/shared/lib/utils'
|
|
6
|
+
import { Button } from '@open-mercato/ui/primitives/button'
|
|
7
|
+
import type { EditorPriority } from '../../../lib/calendar/editorPayload'
|
|
8
|
+
import { CONTROL_BORDER, DROPDOWN_PANEL_CLASS, useDropdownDismiss } from './inputs'
|
|
9
|
+
|
|
10
|
+
// Jira/Linear-style priority glyphs: filled double chevrons for the extremes,
|
|
11
|
+
// an equals bar for the middle. Colours stay on DS status tokens (no amber):
|
|
12
|
+
// blue = low urgency, muted = medium, red = high.
|
|
13
|
+
const PRIORITY_META: Record<EditorPriority, { Icon: React.ComponentType<{ className?: string }>; color: string }> = {
|
|
14
|
+
low: { Icon: ChevronsDown, color: 'text-status-info-text' },
|
|
15
|
+
medium: { Icon: Equal, color: 'text-muted-foreground' },
|
|
16
|
+
high: { Icon: ChevronsUp, color: 'text-status-error-text' },
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const PRIORITY_ORDER: EditorPriority[] = ['high', 'medium', 'low']
|
|
20
|
+
|
|
21
|
+
export function PriorityField({
|
|
22
|
+
value,
|
|
23
|
+
labels,
|
|
24
|
+
ariaLabel,
|
|
25
|
+
onChange,
|
|
26
|
+
}: {
|
|
27
|
+
value: EditorPriority
|
|
28
|
+
labels: Record<EditorPriority, string>
|
|
29
|
+
ariaLabel: string
|
|
30
|
+
onChange(next: EditorPriority): void
|
|
31
|
+
}) {
|
|
32
|
+
const [open, setOpen] = React.useState(false)
|
|
33
|
+
const close = React.useCallback(() => setOpen(false), [])
|
|
34
|
+
const rootRef = useDropdownDismiss(open, close)
|
|
35
|
+
const selected = PRIORITY_META[value]
|
|
36
|
+
const SelectedIcon = selected.Icon
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div
|
|
40
|
+
ref={rootRef}
|
|
41
|
+
className="relative w-full"
|
|
42
|
+
onBlur={(event) => {
|
|
43
|
+
if (!event.currentTarget.contains(event.relatedTarget as Node | null)) setOpen(false)
|
|
44
|
+
}}
|
|
45
|
+
>
|
|
46
|
+
<Button
|
|
47
|
+
type="button"
|
|
48
|
+
variant="outline"
|
|
49
|
+
aria-haspopup="listbox"
|
|
50
|
+
aria-expanded={open}
|
|
51
|
+
aria-label={ariaLabel}
|
|
52
|
+
onClick={() => setOpen((previous) => !previous)}
|
|
53
|
+
className={cn('h-9 w-full justify-between bg-background px-3 font-normal shadow-none', CONTROL_BORDER)}
|
|
54
|
+
>
|
|
55
|
+
<span className="flex min-w-0 items-center gap-2">
|
|
56
|
+
<SelectedIcon aria-hidden className={cn('size-4 shrink-0', selected.color)} />
|
|
57
|
+
<span className="truncate text-sm text-foreground">{labels[value]}</span>
|
|
58
|
+
</span>
|
|
59
|
+
<ChevronDown aria-hidden className="size-4 shrink-0 opacity-60" />
|
|
60
|
+
</Button>
|
|
61
|
+
{open ? (
|
|
62
|
+
<div role="listbox" aria-label={ariaLabel} className={DROPDOWN_PANEL_CLASS}>
|
|
63
|
+
{PRIORITY_ORDER.map((priority) => {
|
|
64
|
+
const meta = PRIORITY_META[priority]
|
|
65
|
+
const Icon = meta.Icon
|
|
66
|
+
const active = priority === value
|
|
67
|
+
return (
|
|
68
|
+
<Button
|
|
69
|
+
key={priority}
|
|
70
|
+
type="button"
|
|
71
|
+
variant="ghost"
|
|
72
|
+
role="option"
|
|
73
|
+
aria-selected={active}
|
|
74
|
+
onClick={() => {
|
|
75
|
+
onChange(priority)
|
|
76
|
+
setOpen(false)
|
|
77
|
+
}}
|
|
78
|
+
className={cn('h-auto w-full justify-start gap-2 px-2 py-1.5 text-left text-sm font-normal', active && 'bg-muted')}
|
|
79
|
+
>
|
|
80
|
+
<Icon aria-hidden className={cn('size-4 shrink-0', meta.color)} />
|
|
81
|
+
<span className="min-w-0 flex-1 truncate text-foreground">{labels[priority]}</span>
|
|
82
|
+
<Check aria-hidden className={cn('size-4 shrink-0', active ? 'opacity-100' : 'opacity-0')} />
|
|
83
|
+
</Button>
|
|
84
|
+
)
|
|
85
|
+
})}
|
|
86
|
+
</div>
|
|
87
|
+
) : null}
|
|
88
|
+
</div>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
type DealOption,
|
|
16
16
|
type RelatedEntityOption,
|
|
17
17
|
} from './lookups'
|
|
18
|
-
import { CONTROL_BORDER, DROPDOWN_PANEL_CLASS, PersonChip, UppercaseBadge } from './inputs'
|
|
18
|
+
import { CONTROL_BORDER, DROPDOWN_PANEL_CLASS, PersonChip, UppercaseBadge, useDropdownDismiss } from './inputs'
|
|
19
19
|
|
|
20
20
|
const OPTION_ROW_CLASS =
|
|
21
21
|
'h-auto w-full justify-start gap-2 whitespace-normal px-2 py-1.5 text-left text-sm font-normal text-foreground'
|
|
@@ -37,6 +37,8 @@ export function RelatedToField({
|
|
|
37
37
|
}) {
|
|
38
38
|
const t = useT()
|
|
39
39
|
const [open, setOpen] = React.useState(false)
|
|
40
|
+
const closeDropdown = React.useCallback(() => setOpen(false), [])
|
|
41
|
+
const rootRef = useDropdownDismiss(open, closeDropdown)
|
|
40
42
|
const [query, setQuery] = React.useState('')
|
|
41
43
|
const [options, setOptions] = React.useState<RelatedEntityOption[]>([])
|
|
42
44
|
const [deals, setDeals] = React.useState<DealOption[]>([])
|
|
@@ -92,6 +94,7 @@ export function RelatedToField({
|
|
|
92
94
|
|
|
93
95
|
return (
|
|
94
96
|
<div
|
|
97
|
+
ref={rootRef}
|
|
95
98
|
className="relative w-full"
|
|
96
99
|
onBlur={(event) => {
|
|
97
100
|
if (!event.currentTarget.contains(event.relatedTarget as Node | null)) setOpen(false)
|
|
@@ -99,7 +102,7 @@ export function RelatedToField({
|
|
|
99
102
|
>
|
|
100
103
|
<div
|
|
101
104
|
className={cn(
|
|
102
|
-
'flex h-10 w-full items-center rounded-md bg-background pl-2.5 pr-3',
|
|
105
|
+
'flex h-10 w-full items-center rounded-md bg-background pl-2.5 pr-3 transition-colors hover:bg-accent/50',
|
|
103
106
|
error ? 'border border-status-error-border' : CONTROL_BORDER,
|
|
104
107
|
)}
|
|
105
108
|
>
|
|
@@ -110,7 +113,7 @@ export function RelatedToField({
|
|
|
110
113
|
aria-expanded={open}
|
|
111
114
|
aria-label={label}
|
|
112
115
|
onClick={() => setOpen((previous) => !previous)}
|
|
113
|
-
className="h-full min-w-0 flex-1 justify-between rounded-none bg-transparent p-0 text-left shadow-none hover:bg-transparent"
|
|
116
|
+
className="h-full min-w-0 flex-1 justify-between rounded-none bg-transparent p-0 text-left shadow-none hover:bg-transparent dark:hover:bg-transparent"
|
|
114
117
|
>
|
|
115
118
|
<span className="flex min-w-0 items-center gap-2">
|
|
116
119
|
{value ? (
|
|
@@ -180,6 +183,7 @@ export function RelatedToField({
|
|
|
180
183
|
role="option"
|
|
181
184
|
aria-selected={value?.id === option.id}
|
|
182
185
|
aria-label={composeAccessibleName([option.label, option.subtitle, kindLabel(option.kind)])}
|
|
186
|
+
title={composeAccessibleName([option.label, option.subtitle])}
|
|
183
187
|
onClick={() => {
|
|
184
188
|
onChange({ id: option.id, kind: option.kind, label: option.label })
|
|
185
189
|
if (value?.id !== option.id) onDealChange(null)
|
|
@@ -223,6 +227,7 @@ export function RelatedToField({
|
|
|
223
227
|
variant="ghost"
|
|
224
228
|
role="option"
|
|
225
229
|
aria-selected={deal?.id === option.id}
|
|
230
|
+
title={option.label}
|
|
226
231
|
onClick={() => {
|
|
227
232
|
onDealChange(option)
|
|
228
233
|
setOpen(false)
|