@fayz-ai/plugin-agenda 0.1.3

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 (114) hide show
  1. package/README.md +51 -0
  2. package/dist/AgendaContext.d.ts +13 -0
  3. package/dist/AgendaContext.d.ts.map +1 -0
  4. package/dist/AgendaPage.d.ts +11 -0
  5. package/dist/AgendaPage.d.ts.map +1 -0
  6. package/dist/bridges/create-financial-bridge.d.ts +19 -0
  7. package/dist/bridges/create-financial-bridge.d.ts.map +1 -0
  8. package/dist/calendar-styles.d.ts +8 -0
  9. package/dist/calendar-styles.d.ts.map +1 -0
  10. package/dist/chunk-3LJKZQK4.js +12 -0
  11. package/dist/chunk-3LJKZQK4.js.map +1 -0
  12. package/dist/chunk-SYDBDI3Q.cjs +15 -0
  13. package/dist/chunk-SYDBDI3Q.cjs.map +1 -0
  14. package/dist/components/AgendaGeneralSettings.d.ts +3 -0
  15. package/dist/components/AgendaGeneralSettings.d.ts.map +1 -0
  16. package/dist/components/AppointmentModal.d.ts +16 -0
  17. package/dist/components/AppointmentModal.d.ts.map +1 -0
  18. package/dist/components/AppointmentPopover.d.ts +14 -0
  19. package/dist/components/AppointmentPopover.d.ts.map +1 -0
  20. package/dist/components/BookingPaymentPanel.d.ts +18 -0
  21. package/dist/components/BookingPaymentPanel.d.ts.map +1 -0
  22. package/dist/components/CalendarSidebar.d.ts +3 -0
  23. package/dist/components/CalendarSidebar.d.ts.map +1 -0
  24. package/dist/components/CalendarToolbar.d.ts +13 -0
  25. package/dist/components/CalendarToolbar.d.ts.map +1 -0
  26. package/dist/components/EventContextMenu.d.ts +14 -0
  27. package/dist/components/EventContextMenu.d.ts.map +1 -0
  28. package/dist/components/FloatingPanel.d.ts +27 -0
  29. package/dist/components/FloatingPanel.d.ts.map +1 -0
  30. package/dist/components/MiniCalendar.d.ts +9 -0
  31. package/dist/components/MiniCalendar.d.ts.map +1 -0
  32. package/dist/components/PaymentStatusBadge.d.ts +13 -0
  33. package/dist/components/PaymentStatusBadge.d.ts.map +1 -0
  34. package/dist/components/SearchCombobox.d.ts +38 -0
  35. package/dist/components/SearchCombobox.d.ts.map +1 -0
  36. package/dist/components/StatusBadge.d.ts +5 -0
  37. package/dist/components/StatusBadge.d.ts.map +1 -0
  38. package/dist/config.d.ts +151 -0
  39. package/dist/config.d.ts.map +1 -0
  40. package/dist/data/index.d.ts +4 -0
  41. package/dist/data/index.d.ts.map +1 -0
  42. package/dist/data/mock.d.ts +3 -0
  43. package/dist/data/mock.d.ts.map +1 -0
  44. package/dist/data/supabase.d.ts +3 -0
  45. package/dist/data/supabase.d.ts.map +1 -0
  46. package/dist/data/types.d.ts +26 -0
  47. package/dist/data/types.d.ts.map +1 -0
  48. package/dist/financial-bridge.d.ts +92 -0
  49. package/dist/financial-bridge.d.ts.map +1 -0
  50. package/dist/hooks/useAgendaSettings.d.ts +7 -0
  51. package/dist/hooks/useAgendaSettings.d.ts.map +1 -0
  52. package/dist/index.cjs +5135 -0
  53. package/dist/index.cjs.map +1 -0
  54. package/dist/index.d.ts +9 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.js +5120 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/lib/tenant.d.ts +3 -0
  59. package/dist/lib/tenant.d.ts.map +1 -0
  60. package/dist/locales/en.d.ts +2 -0
  61. package/dist/locales/en.d.ts.map +1 -0
  62. package/dist/locales/index.d.ts +2 -0
  63. package/dist/locales/index.d.ts.map +1 -0
  64. package/dist/locales/pt-BR.d.ts +2 -0
  65. package/dist/locales/pt-BR.d.ts.map +1 -0
  66. package/dist/registries.d.ts +3 -0
  67. package/dist/registries.d.ts.map +1 -0
  68. package/dist/store.d.ts +66 -0
  69. package/dist/store.d.ts.map +1 -0
  70. package/dist/tenant-222EHYUC.cjs +16 -0
  71. package/dist/tenant-222EHYUC.cjs.map +1 -0
  72. package/dist/tenant-LAUWVUYB.js +3 -0
  73. package/dist/tenant-LAUWVUYB.js.map +1 -0
  74. package/dist/types.d.ts +202 -0
  75. package/dist/types.d.ts.map +1 -0
  76. package/dist/views/CalendarView.d.ts +3 -0
  77. package/dist/views/CalendarView.d.ts.map +1 -0
  78. package/dist/views/ConfirmationsView.d.ts +3 -0
  79. package/dist/views/ConfirmationsView.d.ts.map +1 -0
  80. package/package.json +63 -0
  81. package/src/AgendaContext.tsx +11 -0
  82. package/src/AgendaPage.tsx +33 -0
  83. package/src/bridges/create-financial-bridge.ts +298 -0
  84. package/src/calendar-styles.css +245 -0
  85. package/src/calendar-styles.ts +285 -0
  86. package/src/components/AgendaGeneralSettings.tsx +64 -0
  87. package/src/components/AppointmentModal.tsx +1031 -0
  88. package/src/components/AppointmentPopover.tsx +221 -0
  89. package/src/components/BookingPaymentPanel.tsx +394 -0
  90. package/src/components/CalendarSidebar.tsx +119 -0
  91. package/src/components/CalendarToolbar.tsx +83 -0
  92. package/src/components/EventContextMenu.tsx +122 -0
  93. package/src/components/FloatingPanel.tsx +123 -0
  94. package/src/components/MiniCalendar.tsx +120 -0
  95. package/src/components/PaymentStatusBadge.tsx +62 -0
  96. package/src/components/SearchCombobox.tsx +216 -0
  97. package/src/components/StatusBadge.tsx +23 -0
  98. package/src/config.ts +269 -0
  99. package/src/data/index.ts +3 -0
  100. package/src/data/mock.ts +324 -0
  101. package/src/data/supabase.ts +570 -0
  102. package/src/data/types.ts +37 -0
  103. package/src/financial-bridge.ts +96 -0
  104. package/src/hooks/useAgendaSettings.ts +17 -0
  105. package/src/index.ts +180 -0
  106. package/src/lib/tenant.ts +3 -0
  107. package/src/locales/en.ts +207 -0
  108. package/src/locales/index.ts +7 -0
  109. package/src/locales/pt-BR.ts +207 -0
  110. package/src/registries.ts +36 -0
  111. package/src/store.ts +304 -0
  112. package/src/types.ts +265 -0
  113. package/src/views/CalendarView.tsx +904 -0
  114. package/src/views/ConfirmationsView.tsx +155 -0
@@ -0,0 +1,216 @@
1
+ import React, { useState, useEffect, useRef, useCallback } from 'react'
2
+ import { createPortal } from 'react-dom'
3
+ import { Search, Plus } from 'lucide-react'
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Reusable search combobox with keyboard navigation + optional create
7
+ // ---------------------------------------------------------------------------
8
+
9
+ export interface ComboboxOption {
10
+ id: string
11
+ label: string
12
+ subtitle?: string
13
+ price?: number
14
+ data?: Record<string, unknown>
15
+ }
16
+
17
+ interface SearchComboboxProps {
18
+ value: string
19
+ onChange: (value: string) => void
20
+ onSelect: (option: ComboboxOption) => void
21
+ options: ComboboxOption[]
22
+ placeholder?: string
23
+ autoFocus?: boolean
24
+ onBlurEmpty?: () => void
25
+ renderRight?: (option: ComboboxOption) => React.ReactNode
26
+ /** Show a "+ Create ..." option when search has no exact match */
27
+ allowCreate?: boolean
28
+ /** Called when user clicks the create option. Receives the current search text. */
29
+ onCreateNew?: (searchText: string) => void
30
+ /** Label for the create option (default: 'Create') */
31
+ createLabel?: string
32
+ className?: string
33
+ /** Minimal style — no border/bg, just underline on focus. For inline use. */
34
+ minimal?: boolean
35
+ /** Inline label shown when collapsed. Click reveals the search input. */
36
+ inlineLabel?: string
37
+ /** Icon for the inline label: 'search' (default) or 'plus' (for "add" actions) */
38
+ inlineIcon?: 'search' | 'plus'
39
+ /** Show a loading skeleton in the dropdown while results are being fetched */
40
+ loading?: boolean
41
+ /** Called when the input gains focus */
42
+ onFocus?: () => void
43
+ }
44
+
45
+ export function SearchCombobox({
46
+ value, onChange, onSelect, options, placeholder, autoFocus, onBlurEmpty,
47
+ renderRight, allowCreate, onCreateNew, createLabel = 'Create', className, minimal, inlineLabel, inlineIcon = 'search',
48
+ loading, onFocus: onFocusProp,
49
+ }: SearchComboboxProps) {
50
+ const [open, setOpen] = useState(false)
51
+ const [activeIndex, setActiveIndex] = useState(-1)
52
+ const [expanded, setExpanded] = useState(!inlineLabel)
53
+ const [dropRect, setDropRect] = useState<DOMRect | null>(null)
54
+ const inputRef = useRef<HTMLInputElement>(null)
55
+ const wrapperRef = useRef<HTMLDivElement>(null)
56
+ const listRef = useRef<HTMLDivElement>(null)
57
+
58
+ // When value is set externally (e.g. edit mode), auto-expand; collapse when cleared
59
+ useEffect(() => {
60
+ if (value && inlineLabel) setExpanded(true)
61
+ if (!value && inlineLabel && !open) setExpanded(false)
62
+ }, [value, open])
63
+
64
+ function updateRect() {
65
+ if (wrapperRef.current) setDropRect(wrapperRef.current.getBoundingClientRect())
66
+ }
67
+
68
+ // Show create option only when dropdown is open, has text, and no exact match
69
+ const showCreate = open && allowCreate && onCreateNew && value.trim().length > 0 &&
70
+ !options.some((o) => o.label.toLowerCase() === value.trim().toLowerCase())
71
+
72
+ const totalItems = options.length + (showCreate ? 1 : 0)
73
+
74
+ useEffect(() => { setActiveIndex(-1) }, [options])
75
+ useEffect(() => {
76
+ if ((options.length > 0 || showCreate || loading) && document.activeElement === inputRef.current) setOpen(true)
77
+ }, [options, showCreate, loading])
78
+
79
+ const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
80
+ if (!open || totalItems === 0) {
81
+ if (e.key === 'ArrowDown' && totalItems > 0) {
82
+ e.preventDefault()
83
+ setOpen(true)
84
+ setActiveIndex(0)
85
+ }
86
+ return
87
+ }
88
+
89
+ switch (e.key) {
90
+ case 'ArrowDown':
91
+ e.preventDefault()
92
+ setActiveIndex((prev) => (prev + 1) % totalItems)
93
+ break
94
+ case 'ArrowUp':
95
+ e.preventDefault()
96
+ setActiveIndex((prev) => (prev <= 0 ? totalItems - 1 : prev - 1))
97
+ break
98
+ case 'Enter':
99
+ e.preventDefault()
100
+ if (activeIndex >= 0 && activeIndex < options.length) {
101
+ onSelect(options[activeIndex])
102
+ setOpen(false)
103
+ setActiveIndex(-1)
104
+ } else if (showCreate && activeIndex === options.length) {
105
+ onCreateNew?.(value.trim())
106
+ setOpen(false)
107
+ setActiveIndex(-1)
108
+ }
109
+ break
110
+ case 'Escape':
111
+ e.preventDefault()
112
+ setOpen(false)
113
+ setActiveIndex(-1)
114
+ break
115
+ }
116
+ }, [open, options, totalItems, activeIndex, onSelect, showCreate, onCreateNew, value])
117
+
118
+ useEffect(() => {
119
+ if (activeIndex < 0 || !listRef.current) return
120
+ const items = listRef.current.children
121
+ if (items[activeIndex]) (items[activeIndex] as HTMLElement).scrollIntoView({ block: 'nearest' })
122
+ }, [activeIndex])
123
+
124
+ function handleBlur() {
125
+ setTimeout(() => {
126
+ setOpen(false)
127
+ setActiveIndex(-1)
128
+ if (!value && onBlurEmpty) onBlurEmpty()
129
+ if (!value && inlineLabel) setExpanded(false)
130
+ }, 200)
131
+ }
132
+
133
+ const hasDropdown = open && (options.length > 0 || showCreate || loading)
134
+
135
+ // Collapsed inline label — click to expand
136
+ if (!expanded && inlineLabel) {
137
+ const isPlus = inlineIcon === 'plus'
138
+ return (
139
+ <button type="button" onClick={() => { setExpanded(true); setTimeout(() => inputRef.current?.focus(), 50) }}
140
+ className={`flex items-center gap-1.5 w-full text-left text-sm py-1.5 pl-2 transition-colors ${
141
+ isPlus ? 'text-primary hover:text-primary/80' : 'text-muted-foreground hover:text-foreground'
142
+ } ${className ?? ''}`}>
143
+ {isPlus && <Plus className="h-3.5 w-3.5 shrink-0" />}
144
+ {inlineLabel}
145
+ </button>
146
+ )
147
+ }
148
+
149
+ return (
150
+ <div ref={wrapperRef} className={`relative ${className ?? ''}`}>
151
+ {!minimal && (
152
+ <Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground pointer-events-none" />
153
+ )}
154
+ <input
155
+ ref={inputRef}
156
+ type="text"
157
+ value={value}
158
+ onChange={(e) => { onChange(e.target.value); updateRect(); setOpen(true) }}
159
+ onFocus={() => { updateRect(); onFocusProp?.(); if (options.length > 0 || showCreate || loading) setOpen(true) }}
160
+ onBlur={handleBlur}
161
+ onKeyDown={handleKeyDown}
162
+ placeholder={placeholder}
163
+ autoFocus={autoFocus}
164
+ className={minimal
165
+ ? 'w-full bg-transparent pl-2 pr-3 py-1.5 text-sm placeholder:text-muted-foreground focus:outline-none border-b border-transparent hover:border-border focus:border-foreground transition-colors'
166
+ : 'w-full rounded-lg border bg-background pl-8 pr-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring'
167
+ }
168
+ role="combobox"
169
+ aria-expanded={hasDropdown}
170
+ aria-autocomplete="list"
171
+ aria-activedescendant={activeIndex >= 0 ? `combobox-opt-${activeIndex}` : undefined}
172
+ />
173
+ {hasDropdown && dropRect && createPortal(
174
+ <div ref={listRef} role="listbox" data-modal-passthrough
175
+ style={{ position: 'fixed', left: dropRect.left, top: dropRect.bottom + 4, width: dropRect.width, zIndex: 9999, pointerEvents: 'auto' }}
176
+ className="fayz-glass-surface max-h-52 overflow-y-auto rounded-lg border bg-popover shadow-lg py-0.5 animate-in fade-in zoom-in-95 duration-100">
177
+ {loading && options.length === 0 && (
178
+ <div className="flex items-center gap-2 px-3 py-2 text-sm text-muted-foreground">
179
+ <svg className="h-3.5 w-3.5 animate-spin shrink-0" viewBox="0 0 16 16" fill="none">
180
+ <circle cx="8" cy="8" r="6.5" stroke="currentColor" strokeWidth="1.5" opacity="0.25" />
181
+ <path d="M14.5 8a6.5 6.5 0 00-6.5-6.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
182
+ </svg>
183
+ <span>Buscando…</span>
184
+ </div>
185
+ )}
186
+ {options.map((opt, i) => (
187
+ <div key={opt.id} id={`combobox-opt-${i}`} role="option" aria-selected={i === activeIndex}
188
+ onMouseDown={(e) => { e.preventDefault(); onSelect(opt); setOpen(false) }}
189
+ onMouseEnter={() => setActiveIndex(i)}
190
+ className={`flex w-full items-center gap-2 px-3 py-2 text-sm cursor-pointer transition-colors ${
191
+ i === activeIndex ? 'bg-primary/10 text-primary' : 'hover:bg-muted/50'
192
+ }`}>
193
+ <div className="flex-1 min-w-0">
194
+ <p className="truncate font-medium">{opt.label}</p>
195
+ {opt.subtitle && <p className="truncate text-[11px] text-muted-foreground">{opt.subtitle}</p>}
196
+ </div>
197
+ {renderRight && <div className="shrink-0">{renderRight(opt)}</div>}
198
+ </div>
199
+ ))}
200
+ {showCreate && (
201
+ <div id={`combobox-opt-${options.length}`} role="option" aria-selected={activeIndex === options.length}
202
+ onMouseDown={(e) => { e.preventDefault(); onCreateNew?.(value.trim()); setOpen(false) }}
203
+ onMouseEnter={() => setActiveIndex(options.length)}
204
+ className={`flex w-full items-center gap-2 px-3 py-2 text-sm cursor-pointer transition-colors border-t ${
205
+ activeIndex === options.length ? 'bg-primary/10 text-primary' : 'hover:bg-muted/50'
206
+ }`}>
207
+ <Plus className="h-3.5 w-3.5 shrink-0" />
208
+ <span>{createLabel} &ldquo;<span className="font-medium">{value.trim()}</span>&rdquo;</span>
209
+ </div>
210
+ )}
211
+ </div>,
212
+ document.body,
213
+ )}
214
+ </div>
215
+ )
216
+ }
@@ -0,0 +1,23 @@
1
+ import React from 'react'
2
+ import { useAgendaConfig } from '../AgendaContext'
3
+
4
+ export function StatusBadge({ status }: { status: string }) {
5
+ const config = useAgendaConfig()
6
+ const statusConfig = config.statuses.find((s) => s.value === status)
7
+ const color = statusConfig?.color ?? '#6b7280'
8
+ const label = statusConfig?.label ?? status
9
+
10
+ return (
11
+ <span
12
+ className="inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-xs font-medium"
13
+ style={{
14
+ backgroundColor: `${color}15`,
15
+ color,
16
+ border: `1px solid ${color}30`,
17
+ }}
18
+ >
19
+ <span className="h-1.5 w-1.5 rounded-full" style={{ backgroundColor: color }} />
20
+ {label}
21
+ </span>
22
+ )
23
+ }
package/src/config.ts ADDED
@@ -0,0 +1,269 @@
1
+ import type { PluginScope, VerticalId } from '@fayz-ai/core'
2
+ import type { EntityLookup } from '@fayz-ai/saas'
3
+ import type { AgendaDataProvider } from './data/types'
4
+ import type { StatusConfig, BookingTypeConfig } from './types'
5
+ import type { AgendaFinancialBridge } from './financial-bridge'
6
+ import type { EntityDef } from '@fayz-ai/core'
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Public options interface
10
+ // ---------------------------------------------------------------------------
11
+
12
+ export interface AgendaPluginLabels {
13
+ pageTitle: string
14
+ pageSubtitle: string
15
+ calendar: string
16
+ dayView: string
17
+ weekView: string
18
+ monthView: string
19
+ listView: string
20
+ workingHours: string
21
+ confirmations: string
22
+ newAppointment: string
23
+ filters: string
24
+ }
25
+
26
+ export interface ConfirmationChannelOption {
27
+ id: string
28
+ label: string
29
+ icon: string
30
+ }
31
+
32
+ export interface LocationOption {
33
+ id: string
34
+ name: string
35
+ isHQ?: boolean
36
+ }
37
+
38
+ export interface AgendaPluginOptions {
39
+ modules?: {
40
+ workingHours?: boolean
41
+ confirmations?: boolean
42
+ conflictDetection?: boolean
43
+ dragAndDrop?: boolean
44
+ locationSelection?: boolean
45
+ /** Enable financial integration (auto-enabled when financialBridge is provided) */
46
+ financial?: boolean
47
+ }
48
+ labels?: Partial<AgendaPluginLabels>
49
+ currency?: { code?: string; locale?: string; symbol?: string }
50
+
51
+ /** Booking kind used in saas_core.bookings.kind (default: 'appointment') */
52
+ bookingKind?: string
53
+
54
+ /** Order kind for financial records (default: 'service_order') */
55
+ orderKind?: string
56
+
57
+ /** Schedule kind in saas_core.schedules.kind (default: 'working_hours') */
58
+ scheduleKind?: string
59
+
60
+ /** Booking type tabs shown in the appointment modal */
61
+ bookingTypes?: BookingTypeConfig[]
62
+
63
+ /** Booking status options */
64
+ statuses?: StatusConfig[]
65
+
66
+ /** Default calendar view (default: 'resourceTimeGridWeek'). Options: 'dayGridMonth', 'timeGridWeek', 'resourceTimeGridWeek', 'listWeek' */
67
+ defaultCalendarView?: string
68
+
69
+ /** Calendar visible time range (default: '07:00'-'21:00') */
70
+ businessHours?: { startTime: string; endTime: string }
71
+
72
+ /** Slot duration in minutes (default: 30) */
73
+ slotDuration?: number
74
+
75
+ /** Person kind for professionals (default: 'staff') */
76
+ professionalKind?: string
77
+
78
+ /** Person kind for clients (default: 'customer') */
79
+ clientKind?: string
80
+
81
+ /** Auto-create an order when creating a booking (default: true) */
82
+ autoCreateOrder?: boolean
83
+
84
+ /** Contact/client lookup */
85
+ contactLookup?: EntityLookup
86
+
87
+ /** Client entity definition for the Client tab in booking modal */
88
+ clientEntityDef?: EntityDef
89
+
90
+ /** Service lookup */
91
+ serviceLookup?: EntityLookup
92
+
93
+ /** Professional/staff lookup */
94
+ professionalLookup?: EntityLookup
95
+
96
+ /** Location lookup */
97
+ locationLookup?: EntityLookup
98
+
99
+ /** Called when user clicks a selected entity link (e.g. view client profile) */
100
+ onEntityClick?: (type: 'client' | 'service' | 'professional', id: string) => void
101
+
102
+ /** Business locations for unit filtering */
103
+ locations?: LocationOption[]
104
+
105
+ /** Confirmation channel options */
106
+ confirmationChannels?: ConfirmationChannelOption[]
107
+
108
+ /** Financial bridge for agenda × financial cross-plugin integration */
109
+ financialBridge?: AgendaFinancialBridge
110
+
111
+ /** Data provider — defaults to Supabase, falls back to mock */
112
+ dataProvider?: AgendaDataProvider
113
+
114
+ /** Default values for schedule block advanced settings */
115
+ scheduleBlockDefaults?: {
116
+ bufferMinutes?: number
117
+ maxConcurrent?: number
118
+ minAdvanceHours?: number
119
+ maxAdvanceDays?: number
120
+ }
121
+
122
+ navPosition?: number
123
+ navSection?: 'main' | 'secondary'
124
+ scope?: PluginScope
125
+ verticalId?: VerticalId
126
+ }
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // Defaults
130
+ // ---------------------------------------------------------------------------
131
+
132
+ const DEFAULT_LABELS: AgendaPluginLabels = {
133
+ pageTitle: 'Agenda',
134
+ pageSubtitle: 'Calendar and appointment management',
135
+ calendar: 'Calendar',
136
+ dayView: 'Day',
137
+ weekView: 'Week',
138
+ monthView: 'Month',
139
+ listView: 'Agenda',
140
+ workingHours: 'Working Hours',
141
+ confirmations: 'Confirmations',
142
+ newAppointment: 'New Appointment',
143
+ filters: 'Filters',
144
+ }
145
+
146
+ const DEFAULT_CURRENCY = { code: 'BRL', locale: 'pt-BR', symbol: 'R$' }
147
+
148
+ const DEFAULT_STATUSES: StatusConfig[] = [
149
+ { value: 'scheduled', label: 'Scheduled', color: '#6366f1' },
150
+ { value: 'confirmed', label: 'Confirmed', color: '#3b82f6' },
151
+ { value: 'in_progress', label: 'In Progress', color: '#f59e0b', availableWhen: 'today_only' },
152
+ { value: 'completed', label: 'Completed', color: '#10b981', availableWhen: 'today_or_past' },
153
+ { value: 'cancelled', label: 'Cancelled', color: '#ef4444' },
154
+ { value: 'no_show', label: 'No Show', color: '#6b7280', availableWhen: 'today_or_past' },
155
+ ]
156
+
157
+ const DEFAULT_BOOKING_TYPES: BookingTypeConfig[] = [
158
+ {
159
+ id: 'appointment', label: 'Appointment', icon: 'Calendar', color: '#6366f1',
160
+ fields: { client: true, professional: true, services: true, location: true, status: true },
161
+ requiresServices: true, requiresClient: true,
162
+ },
163
+ {
164
+ id: 'task', label: 'Task', icon: 'CheckSquare', color: '#f59e0b',
165
+ fields: { client: false, professional: true, services: false, location: false, status: true },
166
+ requiresServices: false, requiresClient: false,
167
+ },
168
+ {
169
+ id: 'block', label: 'Block', icon: 'Ban', color: '#6b7280',
170
+ fields: { client: false, professional: true, services: false, location: false, status: false },
171
+ requiresServices: false, requiresClient: false,
172
+ },
173
+ ]
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // Resolved config — fully merged, no optionals
177
+ // ---------------------------------------------------------------------------
178
+
179
+ export interface AgendaModules {
180
+ workingHours: boolean
181
+ confirmations: boolean
182
+ conflictDetection: boolean
183
+ dragAndDrop: boolean
184
+ locationSelection: boolean
185
+ financial: boolean
186
+ }
187
+
188
+ export interface AgendaCurrency {
189
+ code: string
190
+ locale: string
191
+ symbol: string
192
+ }
193
+
194
+ export interface ResolvedAgendaConfig {
195
+ modules: AgendaModules
196
+ labels: AgendaPluginLabels
197
+ currency: AgendaCurrency
198
+ bookingKind: string
199
+ orderKind: string
200
+ scheduleKind: string
201
+ statuses: StatusConfig[]
202
+ bookingTypes: BookingTypeConfig[]
203
+ businessHours: { startTime: string; endTime: string }
204
+ slotDuration: number
205
+ professionalKind: string
206
+ clientKind: string
207
+ autoCreateOrder: boolean
208
+ locations: LocationOption[]
209
+ confirmationChannels: ConfirmationChannelOption[]
210
+ contactLookup?: EntityLookup
211
+ clientEntityDef?: EntityDef
212
+ serviceLookup?: EntityLookup
213
+ professionalLookup?: EntityLookup
214
+ locationLookup?: EntityLookup
215
+ onEntityClick?: (type: 'client' | 'service' | 'professional', id: string) => void
216
+ financialBridge?: AgendaFinancialBridge
217
+ scheduleBlockDefaults: {
218
+ bufferMinutes: number
219
+ maxConcurrent: number
220
+ minAdvanceHours: number
221
+ maxAdvanceDays: number
222
+ }
223
+ defaultCalendarView: string
224
+ }
225
+
226
+ // ---------------------------------------------------------------------------
227
+ // Config resolver
228
+ // ---------------------------------------------------------------------------
229
+
230
+ export function resolveConfig(options?: AgendaPluginOptions): ResolvedAgendaConfig {
231
+ return {
232
+ modules: {
233
+ workingHours: options?.modules?.workingHours !== false,
234
+ confirmations: options?.modules?.confirmations !== false,
235
+ conflictDetection: options?.modules?.conflictDetection !== false,
236
+ dragAndDrop: options?.modules?.dragAndDrop !== false,
237
+ locationSelection: options?.modules?.locationSelection === true,
238
+ financial: options?.modules?.financial ?? !!options?.financialBridge,
239
+ },
240
+ labels: { ...DEFAULT_LABELS, ...options?.labels },
241
+ currency: { ...DEFAULT_CURRENCY, ...options?.currency },
242
+ bookingKind: options?.bookingKind ?? 'appointment',
243
+ orderKind: options?.orderKind ?? 'service_order',
244
+ scheduleKind: options?.scheduleKind ?? 'working_hours',
245
+ statuses: options?.statuses ?? DEFAULT_STATUSES,
246
+ bookingTypes: options?.bookingTypes ?? DEFAULT_BOOKING_TYPES,
247
+ businessHours: options?.businessHours ?? { startTime: '07:00', endTime: '21:00' },
248
+ slotDuration: options?.slotDuration ?? 30,
249
+ professionalKind: options?.professionalKind ?? 'staff',
250
+ clientKind: options?.clientKind ?? 'customer',
251
+ autoCreateOrder: options?.autoCreateOrder !== false,
252
+ locations: options?.locations ?? [],
253
+ confirmationChannels: options?.confirmationChannels ?? [],
254
+ contactLookup: options?.contactLookup,
255
+ clientEntityDef: options?.clientEntityDef,
256
+ serviceLookup: options?.serviceLookup,
257
+ professionalLookup: options?.professionalLookup,
258
+ locationLookup: options?.locationLookup,
259
+ onEntityClick: options?.onEntityClick,
260
+ financialBridge: options?.financialBridge,
261
+ scheduleBlockDefaults: {
262
+ bufferMinutes: options?.scheduleBlockDefaults?.bufferMinutes ?? 0,
263
+ maxConcurrent: options?.scheduleBlockDefaults?.maxConcurrent ?? 1,
264
+ minAdvanceHours: options?.scheduleBlockDefaults?.minAdvanceHours ?? 0,
265
+ maxAdvanceDays: options?.scheduleBlockDefaults?.maxAdvanceDays ?? 60,
266
+ },
267
+ defaultCalendarView: options?.defaultCalendarView ?? 'resourceTimeGridWeek',
268
+ }
269
+ }
@@ -0,0 +1,3 @@
1
+ export type { AgendaDataProvider } from './types'
2
+ export { createMockAgendaProvider } from './mock'
3
+ export { createSupabaseAgendaProvider } from './supabase'