@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.
- package/README.md +51 -0
- package/dist/AgendaContext.d.ts +13 -0
- package/dist/AgendaContext.d.ts.map +1 -0
- package/dist/AgendaPage.d.ts +11 -0
- package/dist/AgendaPage.d.ts.map +1 -0
- package/dist/bridges/create-financial-bridge.d.ts +19 -0
- package/dist/bridges/create-financial-bridge.d.ts.map +1 -0
- package/dist/calendar-styles.d.ts +8 -0
- package/dist/calendar-styles.d.ts.map +1 -0
- package/dist/chunk-3LJKZQK4.js +12 -0
- package/dist/chunk-3LJKZQK4.js.map +1 -0
- package/dist/chunk-SYDBDI3Q.cjs +15 -0
- package/dist/chunk-SYDBDI3Q.cjs.map +1 -0
- package/dist/components/AgendaGeneralSettings.d.ts +3 -0
- package/dist/components/AgendaGeneralSettings.d.ts.map +1 -0
- package/dist/components/AppointmentModal.d.ts +16 -0
- package/dist/components/AppointmentModal.d.ts.map +1 -0
- package/dist/components/AppointmentPopover.d.ts +14 -0
- package/dist/components/AppointmentPopover.d.ts.map +1 -0
- package/dist/components/BookingPaymentPanel.d.ts +18 -0
- package/dist/components/BookingPaymentPanel.d.ts.map +1 -0
- package/dist/components/CalendarSidebar.d.ts +3 -0
- package/dist/components/CalendarSidebar.d.ts.map +1 -0
- package/dist/components/CalendarToolbar.d.ts +13 -0
- package/dist/components/CalendarToolbar.d.ts.map +1 -0
- package/dist/components/EventContextMenu.d.ts +14 -0
- package/dist/components/EventContextMenu.d.ts.map +1 -0
- package/dist/components/FloatingPanel.d.ts +27 -0
- package/dist/components/FloatingPanel.d.ts.map +1 -0
- package/dist/components/MiniCalendar.d.ts +9 -0
- package/dist/components/MiniCalendar.d.ts.map +1 -0
- package/dist/components/PaymentStatusBadge.d.ts +13 -0
- package/dist/components/PaymentStatusBadge.d.ts.map +1 -0
- package/dist/components/SearchCombobox.d.ts +38 -0
- package/dist/components/SearchCombobox.d.ts.map +1 -0
- package/dist/components/StatusBadge.d.ts +5 -0
- package/dist/components/StatusBadge.d.ts.map +1 -0
- package/dist/config.d.ts +151 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/data/index.d.ts +4 -0
- package/dist/data/index.d.ts.map +1 -0
- package/dist/data/mock.d.ts +3 -0
- package/dist/data/mock.d.ts.map +1 -0
- package/dist/data/supabase.d.ts +3 -0
- package/dist/data/supabase.d.ts.map +1 -0
- package/dist/data/types.d.ts +26 -0
- package/dist/data/types.d.ts.map +1 -0
- package/dist/financial-bridge.d.ts +92 -0
- package/dist/financial-bridge.d.ts.map +1 -0
- package/dist/hooks/useAgendaSettings.d.ts +7 -0
- package/dist/hooks/useAgendaSettings.d.ts.map +1 -0
- package/dist/index.cjs +5135 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5120 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/tenant.d.ts +3 -0
- package/dist/lib/tenant.d.ts.map +1 -0
- package/dist/locales/en.d.ts +2 -0
- package/dist/locales/en.d.ts.map +1 -0
- package/dist/locales/index.d.ts +2 -0
- package/dist/locales/index.d.ts.map +1 -0
- package/dist/locales/pt-BR.d.ts +2 -0
- package/dist/locales/pt-BR.d.ts.map +1 -0
- package/dist/registries.d.ts +3 -0
- package/dist/registries.d.ts.map +1 -0
- package/dist/store.d.ts +66 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/tenant-222EHYUC.cjs +16 -0
- package/dist/tenant-222EHYUC.cjs.map +1 -0
- package/dist/tenant-LAUWVUYB.js +3 -0
- package/dist/tenant-LAUWVUYB.js.map +1 -0
- package/dist/types.d.ts +202 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/views/CalendarView.d.ts +3 -0
- package/dist/views/CalendarView.d.ts.map +1 -0
- package/dist/views/ConfirmationsView.d.ts +3 -0
- package/dist/views/ConfirmationsView.d.ts.map +1 -0
- package/package.json +63 -0
- package/src/AgendaContext.tsx +11 -0
- package/src/AgendaPage.tsx +33 -0
- package/src/bridges/create-financial-bridge.ts +298 -0
- package/src/calendar-styles.css +245 -0
- package/src/calendar-styles.ts +285 -0
- package/src/components/AgendaGeneralSettings.tsx +64 -0
- package/src/components/AppointmentModal.tsx +1031 -0
- package/src/components/AppointmentPopover.tsx +221 -0
- package/src/components/BookingPaymentPanel.tsx +394 -0
- package/src/components/CalendarSidebar.tsx +119 -0
- package/src/components/CalendarToolbar.tsx +83 -0
- package/src/components/EventContextMenu.tsx +122 -0
- package/src/components/FloatingPanel.tsx +123 -0
- package/src/components/MiniCalendar.tsx +120 -0
- package/src/components/PaymentStatusBadge.tsx +62 -0
- package/src/components/SearchCombobox.tsx +216 -0
- package/src/components/StatusBadge.tsx +23 -0
- package/src/config.ts +269 -0
- package/src/data/index.ts +3 -0
- package/src/data/mock.ts +324 -0
- package/src/data/supabase.ts +570 -0
- package/src/data/types.ts +37 -0
- package/src/financial-bridge.ts +96 -0
- package/src/hooks/useAgendaSettings.ts +17 -0
- package/src/index.ts +180 -0
- package/src/lib/tenant.ts +3 -0
- package/src/locales/en.ts +207 -0
- package/src/locales/index.ts +7 -0
- package/src/locales/pt-BR.ts +207 -0
- package/src/registries.ts +36 -0
- package/src/store.ts +304 -0
- package/src/types.ts +265 -0
- package/src/views/CalendarView.tsx +904 -0
- 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} “<span className="font-medium">{value.trim()}</span>”</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
|
+
}
|