@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,207 @@
1
+ // agenda.* — merged plugin-own + saas-core central i18n
2
+ export const ptBR: Record<string, string> = {
3
+ "agenda.appointment.addDescription": "Adicionar descrição...",
4
+ "agenda.appointment.addService": "Adicionar serviço",
5
+ "agenda.appointment.cancel": "Cancelar",
6
+ "agenda.appointment.clientTab": "Cliente",
7
+ "agenda.appointment.noClientData": "Dados do cliente não disponíveis",
8
+ "agenda.appointment.client": "Cliente",
9
+ "agenda.appointment.conflict": "Conflito de horário — este profissional já está reservado neste horário.",
10
+ "agenda.appointment.create": "Criar",
11
+ "agenda.appointment.createClientFailed": "Falha ao criar cliente",
12
+ "agenda.appointment.creating": "Criando...",
13
+ "agenda.appointment.delete": "Excluir",
14
+ "agenda.appointment.deleteConfirm": "Excluir este agendamento?",
15
+ "agenda.appointment.confirmDelete": "Excluir",
16
+ "agenda.appointment.deleting": "Excluindo…",
17
+ "agenda.appointment.edit": "Editar Agendamento",
18
+ "agenda.appointment.emailOptional": "E-mail (opcional)",
19
+ "agenda.appointment.location": "Local",
20
+ "agenda.appointment.name": "Nome",
21
+ "agenda.appointment.new": "Novo Agendamento",
22
+ "agenda.appointment.newClient": "Novo cliente",
23
+ "agenda.appointment.notes": "Observações",
24
+ "agenda.appointment.phone": "Telefone",
25
+ "agenda.appointment.professional": "Profissional",
26
+ "agenda.appointment.save": "Salvar",
27
+ "agenda.appointment.saving": "Salvando...",
28
+ "agenda.appointment.searchClient": "Buscar cliente...",
29
+ "agenda.appointment.searchService": "Buscar serviço para adicionar...",
30
+ "agenda.appointment.selectLocation": "Selecionar local...",
31
+ "agenda.appointment.selectProfessional": "Selecionar profissional...",
32
+ "agenda.appointment.services": "Serviços",
33
+ "agenda.appointment.status": "Status",
34
+ "agenda.appointment.to": "até",
35
+ "agenda.appointment.total": "Total",
36
+ "agenda.appointment.update": "Atualizar",
37
+ "agenda.payment.title": "Recebimento",
38
+ "agenda.payment.generateInvoice": "Gerar recebimento",
39
+ "agenda.payment.generating": "Gerando…",
40
+ "agenda.payment.pending": "Pendente",
41
+ "agenda.payment.partial": "Parcial",
42
+ "agenda.payment.paid": "Pago",
43
+ "agenda.payment.collect": "Receber",
44
+ "agenda.payment.overdue": "Vencido",
45
+ "agenda.payment.cancelled": "Cancelado",
46
+ "agenda.payment.none": "Recebimento",
47
+ "agenda.payment.pay": "Pagar",
48
+ "agenda.payment.paying": "Pagando…",
49
+ "agenda.payment.remaining": "Restante",
50
+ "agenda.payment.installment": "Parcela",
51
+ "agenda.payment.total": "Total",
52
+ "agenda.payment.noInvoice": "Sem recebimento gerado",
53
+ "agenda.payment.selectAccount": "Selecionar conta",
54
+ "agenda.payment.registerPayment": "Registrar recebimento",
55
+ "agenda.payment.registerPaymentHint": "Registrar pagamento deste agendamento",
56
+ "agenda.payment.pendingHint": "Fatura gerada, aguardando pagamento",
57
+ "agenda.payment.partialHint": "Parcialmente pago, toque para completar",
58
+ "agenda.payment.paidHint": "Pagamento realizado",
59
+ "agenda.payment.overdueHint": "Pagamento vencido",
60
+ "agenda.payment.backToAppointment": "Voltar ao agendamento",
61
+ "agenda.payment.checkout": "Confirmar",
62
+ "agenda.payment.checkingOut": "Processando…",
63
+ "agenda.payment.serviceSummary": "Resumo do atendimento",
64
+ "agenda.payment.paymentMethod": "Forma de pagamento",
65
+ "agenda.payment.noMethods": "Nenhuma forma de pagamento configurada.",
66
+ "agenda.payment.configureMethods": "Configurar formas de pagamento",
67
+ "agenda.payment.checkoutComplete": "Pagamento confirmado",
68
+ "agenda.payment.card": "Cartão",
69
+ "agenda.payment.method": "Método",
70
+ "agenda.payment.select": "Selecionar…",
71
+ "agenda.payment.cardBrand": "Bandeira",
72
+ "agenda.payment.selectBrand": "Selecionar bandeira…",
73
+ "agenda.payment.cardInstallments": "Parcelas",
74
+ "agenda.payment.bankAccount": "Conta",
75
+ "agenda.payment.viewInvoice": "Ver detalhes da fatura",
76
+ "agenda.calendar.agendas": "Agendas",
77
+ "agenda.calendar.create": "Criar",
78
+ "agenda.calendar.next": "Próximo",
79
+ "agenda.calendar.previous": "Anterior",
80
+ "agenda.calendar.status": "Status",
81
+ "agenda.calendar.today": "Hoje",
82
+ "agenda.calendar.viewAgenda": "Agenda",
83
+ "agenda.calendar.viewDay": "Dia",
84
+ "agenda.calendar.viewMonth": "Mês",
85
+ "agenda.calendar.viewWeek": "Semana",
86
+ "agenda.confirmations.confirmed": "Confirmado:",
87
+ "agenda.confirmations.empty": "Nenhuma confirmação pendente",
88
+ "agenda.confirmations.emptySubtitle": "Todos os agendamentos futuros foram confirmados",
89
+ "agenda.confirmations.loading": "Carregando...",
90
+ "agenda.confirmations.subtitle": "Agendamentos futuros pendentes de confirmação",
91
+ "agenda.confirmations.unknownClient": "Cliente desconhecido",
92
+ "agenda.confirmations.sendWhatsApp": "Enviar WhatsApp",
93
+ "agenda.confirmations.call": "Ligar",
94
+ "agenda.confirmations.markConfirmed": "Marcar Confirmado",
95
+ "agenda.confirmations.markCancelled": "Marcar Cancelado",
96
+ "agenda.contextMenu.changeStatus": "Alterar status",
97
+ "agenda.contextMenu.delete": "Excluir",
98
+ "agenda.contextMenu.edit": "Editar",
99
+ "agenda.contextMenu.notAvailable": "(indisponível)",
100
+ "agenda.index.settingsSubtitle": "Horários de funcionamento, preferências de agendamento e confirmações",
101
+ "agenda.index.settingsTitle": "Configurações da Agenda",
102
+ "settings.plugin.agenda-working-hours": "Horário de Funcionamento",
103
+ "agenda.popover.unknown": "Desconhecido",
104
+ "agenda.settings.autoConfirm": "Confirmar Automaticamente",
105
+ "agenda.settings.autoConfirmDesc": "Confirmar automaticamente novos agendamentos",
106
+ "agenda.settings.autoCreateOrder": "Criar Pedido Automaticamente",
107
+ "agenda.settings.autoCreateOrderDesc": "Criar automaticamente um pedido financeiro para novos agendamentos",
108
+ "agenda.settings.blockOutside": "Bloquear Fora do Horário",
109
+ "agenda.settings.blockOutsideDesc": "Impedir agendamentos fora do horário de funcionamento",
110
+ "agenda.settings.calendarDisplay": "Exibição do Calendário",
111
+ "agenda.settings.calendarDisplayDesc": "Configurações visuais do calendário",
112
+ "agenda.settings.cancelAlerts": "Alertas de Cancelamento",
113
+ "agenda.settings.cancelAlertsDesc": "Notificar quando um agendamento é cancelado",
114
+ "agenda.settings.compact": "Modo Compacto",
115
+ "agenda.settings.compactDesc": "Usar blocos de eventos menores",
116
+ "agenda.settings.confirmations": "Confirmações",
117
+ "agenda.settings.confirmationsDesc": "Configurações de confirmação de agendamento",
118
+ "agenda.settings.conflictDetection": "Detecção de Conflitos",
119
+ "agenda.settings.conflictDetectionDesc": "Alertar quando agendamentos se sobrepõem",
120
+ "agenda.settings.dragDrop": "Arrastar e Soltar",
121
+ "agenda.settings.dragDropDesc": "Permitir reagendamento arrastando eventos",
122
+ "agenda.settings.enableConfirmations": "Habilitar Confirmações",
123
+ "agenda.settings.enableConfirmationsDesc": "Exigir confirmação para novos agendamentos",
124
+ "agenda.settings.enableLocation": "Habilitar Seleção de Local",
125
+ "agenda.settings.enableLocationDesc": "Mostrar seletor de local no modal de agendamento",
126
+ "agenda.settings.enableWorkingHours": "Habilitar Horário de Funcionamento",
127
+ "agenda.settings.enableWorkingHoursDesc": "Mostrar sobreposição de horário de funcionamento no calendário",
128
+ "agenda.settings.locations": "Locais",
129
+ "agenda.settings.locationsDesc": "Configurações de múltiplos locais",
130
+ "agenda.settings.newBookingNotif": "Alertas de Novo Agendamento",
131
+ "agenda.settings.newBookingNotifDesc": "Notificar quando um novo agendamento é criado",
132
+ "agenda.settings.noShowTracking": "Rastreamento de Ausências",
133
+ "agenda.settings.noShowTrackingDesc": "Rastrear e alertar sobre ausências",
134
+ "agenda.settings.notifications": "Notificações",
135
+ "agenda.settings.notificationsDesc": "Preferências de notificação",
136
+ "agenda.settings.reminder": "Lembretes",
137
+ "agenda.settings.reminderDesc": "Enviar lembretes antes dos compromissos",
138
+ "agenda.settings.scheduling": "Agendamento",
139
+ "agenda.settings.schedulingDesc": "Configurar comportamento do agendamento",
140
+ "agenda.settings.showCancelled": "Mostrar Cancelados",
141
+ "agenda.settings.showCancelledDesc": "Exibir agendamentos cancelados no calendário",
142
+ "agenda.settings.showNoShow": "Mostrar Ausências",
143
+ "agenda.settings.showNoShowDesc": "Exibir ausências no calendário",
144
+ "agenda.settings.workingHours": "Horário de Funcionamento",
145
+ "agenda.settings.workingHoursDesc": "Horários e disponibilidade",
146
+ "agenda.sidebar.agendas": "Agendas",
147
+ "agenda.sidebar.allLocations": "Todos os locais",
148
+ "agenda.sidebar.create": "Criar",
149
+ "agenda.sidebar.location": "Local",
150
+ "agenda.sidebar.status": "Status",
151
+ "agenda.toolbar.next": "Próximo",
152
+ "agenda.toolbar.previous": "Anterior",
153
+ "agenda.toolbar.today": "Hoje",
154
+ "agenda.workingHours.addBreak": "Adicionar intervalo",
155
+ "agenda.workingHours.copyToAll": "Copiar para todos",
156
+ "agenda.workingHours.dayOff": "Folga",
157
+ "agenda.workingHours.removePeriod": "Remover período",
158
+ "agenda.workingHours.professional": "Profissional",
159
+ "agenda.workingHours.save": "Salvar",
160
+ "agenda.workingHours.saveFailed": "Falha ao salvar",
161
+ "agenda.workingHours.saved": "Horários de trabalho salvos",
162
+ "agenda.workingHours.saving": "Salvando...",
163
+ "agenda.workingHours.subtitle": "Gerenciar horários de trabalho por profissional",
164
+ "agenda.workingHours.to": "até",
165
+ "agenda.title": "Agenda",
166
+ "agenda.calendar.title": "Calendário",
167
+ "agenda.calendar.noScheduleConfirm": "Este horário não possui agenda aberta para este profissional. Deseja agendar mesmo assim?",
168
+ "agenda.calendar.newAppointment": "Novo Agendamento",
169
+ "agenda.booking.create": "Novo Agendamento",
170
+ "agenda.booking.edit": "Editar Agendamento",
171
+ "agenda.booking.delete": "Excluir Agendamento",
172
+ "agenda.booking.deleteConfirm": "Tem certeza que deseja excluir este agendamento?",
173
+ "agenda.booking.client": "Cliente",
174
+ "agenda.booking.professional": "Profissional",
175
+ "agenda.booking.services": "Serviços",
176
+ "agenda.booking.date": "Data",
177
+ "agenda.booking.time": "Horário",
178
+ "agenda.booking.duration": "Duração",
179
+ "agenda.booking.notes": "Observações",
180
+ "agenda.booking.status": "Status",
181
+ "agenda.booking.save": "Salvar Agendamento",
182
+ "agenda.booking.saving": "Salvando...",
183
+ "agenda.confirmations.title": "Confirmações",
184
+ "agenda.confirmations.pending": "Confirmações Pendentes",
185
+ "agenda.confirmations.emptyDescription": "Todos os agendamentos futuros foram confirmados",
186
+ "agenda.confirmations.confirm": "Confirmar",
187
+ "agenda.confirmations.cancel": "Cancelar",
188
+ "agenda.workingHours.title": "Horário de Trabalho",
189
+ "agenda.workingHours.addPeriod": "Adicionar período",
190
+ "agenda.workingHours.loading": "Carregando...",
191
+ "agenda.appointment.addClient": "Adicionar cliente",
192
+ "agenda.appointment.addServiceFirst": "Adicionar serviço",
193
+ "agenda.appointment.addNotes": "Adicionar observações",
194
+ "agenda.appointment.duration": "Duração (minutos)",
195
+ "agenda.appointment.price": "Preço",
196
+ "agenda.bookingType.appointment": "Agendamento",
197
+ "agenda.bookingType.task": "Tarefa",
198
+ "agenda.bookingType.out_of_office": "Ausência",
199
+ "agenda.bookingType.block": "Bloqueio",
200
+ "agenda.settings.title": "Configurações da Agenda",
201
+ "agenda.settings.slotDuration": "Duração do Intervalo",
202
+ "agenda.settings.bookingKind": "Tipo de Agendamento",
203
+ "agenda.settings.dayStartTime": "Horário de Início",
204
+ "agenda.settings.dayStartTimeDesc": "Primeiro horário exibido no calendário",
205
+ "agenda.settings.dayEndTime": "Horário de Término",
206
+ "agenda.settings.dayEndTimeDesc": "Último horário exibido no calendário",
207
+ }
@@ -0,0 +1,36 @@
1
+ import type { PluginRegistryDef } from '@fayz-ai/core'
2
+ import type { EntityDef } from '@fayz-ai/core'
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Holidays (saas_core.holidays — tenant-scoped)
6
+ // ---------------------------------------------------------------------------
7
+
8
+ const holidayEntity: EntityDef = {
9
+ name: 'Holiday',
10
+ namePlural: 'Holidays',
11
+ icon: 'TreePalm',
12
+ displayField: 'name',
13
+ defaultSort: 'date',
14
+ defaultSortDir: 'asc',
15
+ fields: [
16
+ { key: 'name', label: 'Name', type: 'text', required: true, showInTable: true },
17
+ { key: 'date', label: 'Date', type: 'date', required: true, showInTable: true, sortable: true },
18
+ { key: 'recurring', label: 'Recurring Annually', type: 'boolean', showInTable: true, defaultValue: false },
19
+ { key: 'description', label: 'Description', type: 'text', showInTable: false },
20
+ { key: 'isActive', label: 'Active', type: 'boolean', showInTable: true, defaultValue: true },
21
+ ],
22
+ data: {
23
+ table: 'holidays',
24
+ schema: 'saas_core',
25
+ tenantScoped: true,
26
+ },
27
+ }
28
+
29
+ export const agendaRegistries: PluginRegistryDef[] = [
30
+ {
31
+ id: 'holidays',
32
+ entity: holidayEntity,
33
+ icon: 'TreePalm',
34
+ description: 'Non-working days and recurring holidays',
35
+ },
36
+ ]
package/src/store.ts ADDED
@@ -0,0 +1,304 @@
1
+ import { createStore, type StoreApi } from 'zustand/vanilla'
2
+ import { dedup } from '@fayz-ai/saas'
3
+ import { toast } from '@fayz-ai/ui'
4
+ import { getScheduleBlockConfig, setScheduleBlockConfig } from '@fayz-ai/saas'
5
+ import type { AgendaDataProvider } from './data/types'
6
+ import type { AgendaFinancialBridge } from './financial-bridge'
7
+ import type {
8
+ CalendarBooking, Professional, Schedule,
9
+ CreateBookingInput, UpdateBookingInput, BookingQuery,
10
+ } from './types'
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Store state
14
+ // ---------------------------------------------------------------------------
15
+
16
+ export interface AgendaUIState {
17
+ // Calendar data
18
+ bookings: CalendarBooking[]
19
+ bookingsLoading: boolean
20
+ dateRange: { start: string; end: string }
21
+ currentView: string
22
+ selectedDate: string
23
+
24
+ // Resources
25
+ professionals: Professional[]
26
+ professionalsLoading: boolean
27
+
28
+ // Filters
29
+ selectedProfessionalIds: string[]
30
+ selectedLocationId: string | null
31
+ selectedStatuses: string[]
32
+
33
+ // Locations
34
+ locations: Array<{ id: string; name: string }>
35
+
36
+ // Schedules
37
+ schedules: Schedule[]
38
+ schedulesLoading: boolean
39
+
40
+ // Confirmations
41
+ pendingConfirmations: CalendarBooking[]
42
+ confirmationsLoading: boolean
43
+
44
+ // Modal state
45
+ appointmentModal: {
46
+ open: boolean
47
+ mode: 'create' | 'edit'
48
+ bookingId?: string
49
+ prefill?: Partial<CreateBookingInput> & { endsAt?: string }
50
+ initialTab?: string
51
+ }
52
+
53
+ // Actions
54
+ fetchBookings(range: { start: string; end: string }): Promise<void>
55
+ fetchProfessionals(): Promise<void>
56
+ fetchLocations(): Promise<void>
57
+ fetchSchedules(professionalId?: string): Promise<void>
58
+ fetchConfirmations(): Promise<void>
59
+ createBooking(input: CreateBookingInput): Promise<CalendarBooking>
60
+ updateBooking(id: string, data: UpdateBookingInput): Promise<CalendarBooking>
61
+ deleteBooking(id: string): Promise<void>
62
+ updateBookingStatus(id: string, status: string): Promise<void>
63
+ rescheduleBooking(id: string, newStart: string, newEnd: string, newProfessionalId?: string): Promise<void>
64
+ setFilters(filters: Partial<{ professionalIds: string[]; locationId: string | null; statuses: string[] }>): void
65
+ setView(view: string): void
66
+ setSelectedDate(date: string): void
67
+ openAppointmentModal(mode: 'create' | 'edit', options?: { bookingId?: string; prefill?: Partial<CreateBookingInput> & { endsAt?: string }; initialTab?: string }): void
68
+ closeAppointmentModal(): void
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Store factory
73
+ // ---------------------------------------------------------------------------
74
+
75
+ function getCurrentBookingQuery(state: AgendaUIState, dateRange = state.dateRange): BookingQuery {
76
+ return {
77
+ dateRange,
78
+ professionalIds: state.selectedProfessionalIds.length ? state.selectedProfessionalIds : undefined,
79
+ locationId: state.selectedLocationId ?? undefined,
80
+ statuses: state.selectedStatuses.length ? state.selectedStatuses : undefined,
81
+ }
82
+ }
83
+
84
+ export function createAgendaStore(provider: AgendaDataProvider, financialBridge?: AgendaFinancialBridge): StoreApi<AgendaUIState> {
85
+ return createStore<AgendaUIState>((set, get) => ({
86
+ bookings: [],
87
+ bookingsLoading: false,
88
+ dateRange: { start: '', end: '' },
89
+ currentView: 'resourceTimeGridDay',
90
+ selectedDate: new Date().toISOString().slice(0, 10),
91
+
92
+ professionals: [],
93
+ professionalsLoading: false,
94
+
95
+ selectedProfessionalIds: [],
96
+ selectedLocationId: null,
97
+ selectedStatuses: ['scheduled', 'confirmed', 'in_progress', 'completed'],
98
+
99
+ locations: [],
100
+
101
+ schedules: [],
102
+ schedulesLoading: false,
103
+
104
+ pendingConfirmations: [],
105
+ confirmationsLoading: false,
106
+
107
+ appointmentModal: { open: false, mode: 'create' },
108
+
109
+ async fetchBookings(range) {
110
+ const key = `agenda:bookings:${range.start}:${range.end}`
111
+ return dedup(key, async () => {
112
+ set({ bookingsLoading: true, dateRange: range })
113
+ const state = get()
114
+ const query = getCurrentBookingQuery(state, range)
115
+ let bookings = await provider.getBookings(query)
116
+
117
+ // Enrich with payment status if financial bridge is available
118
+ if (financialBridge) {
119
+ const orderIds = bookings.map((b) => b.orderId).filter((id): id is string => !!id)
120
+ if (orderIds.length > 0) {
121
+ try {
122
+ const statuses = await financialBridge.resolvePaymentStatuses(orderIds)
123
+ bookings = bookings.map((b) => {
124
+ if (!b.orderId) return b
125
+ const summary = statuses.get(b.orderId)
126
+ return summary ? { ...b, paymentStatus: summary.status } : b
127
+ })
128
+ } catch { /* non-blocking */ }
129
+ }
130
+ }
131
+
132
+ set({ bookings, bookingsLoading: false })
133
+ })
134
+ },
135
+
136
+ async fetchProfessionals() {
137
+ return dedup('agenda:professionals', async () => {
138
+ set({ professionalsLoading: true })
139
+ const professionals = await provider.getProfessionals()
140
+ set({ professionals, professionalsLoading: false })
141
+ })
142
+ },
143
+
144
+ async fetchLocations() {
145
+ return dedup('agenda:locations', async () => {
146
+ if (!provider.getLocations) return
147
+ const locations = await provider.getLocations()
148
+ set({ locations })
149
+ // Update global schedule block config so ScheduleEditor picks up locations
150
+ const current = getScheduleBlockConfig()
151
+ if (current) {
152
+ setScheduleBlockConfig({ ...current, locations })
153
+ }
154
+ })
155
+ },
156
+
157
+ async fetchSchedules(professionalId) {
158
+ return dedup('agenda:schedules:' + (professionalId ?? 'all'), async () => {
159
+ set({ schedulesLoading: true })
160
+ const schedules = await provider.getSchedules(professionalId)
161
+ set({ schedules, schedulesLoading: false })
162
+ })
163
+ },
164
+
165
+ async fetchConfirmations() {
166
+ return dedup('agenda:confirmations', async () => {
167
+ set({ confirmationsLoading: true })
168
+ const pendingConfirmations = await provider.getConfirmationsPending()
169
+ set({ pendingConfirmations, confirmationsLoading: false })
170
+ })
171
+ },
172
+
173
+ async createBooking(input) {
174
+ try {
175
+ const booking = await provider.createBooking(input)
176
+ // Refresh bookings for current range
177
+ const state = get()
178
+ if (state.dateRange.start && state.dateRange.end) {
179
+ const bookings = await provider.getBookings(getCurrentBookingQuery(state))
180
+ set({ bookings })
181
+ }
182
+ toast.success('Appointment created')
183
+ return booking
184
+ } catch (err: any) {
185
+ toast.error('Failed to create appointment', { description: err?.message })
186
+ throw err
187
+ }
188
+ },
189
+
190
+ async updateBooking(id, data) {
191
+ try {
192
+ const booking = await provider.updateBooking(id, data)
193
+ const state = get()
194
+ if (state.dateRange.start && state.dateRange.end) {
195
+ const bookings = await provider.getBookings(getCurrentBookingQuery(state))
196
+ set({ bookings })
197
+ }
198
+ toast.success('Appointment updated')
199
+ return booking
200
+ } catch (err: any) {
201
+ toast.error('Failed to update appointment', { description: err?.message })
202
+ throw err
203
+ }
204
+ },
205
+
206
+ async deleteBooking(id) {
207
+ try {
208
+ await provider.deleteBooking(id)
209
+ const state = get()
210
+ if (state.dateRange.start && state.dateRange.end) {
211
+ const bookings = await provider.getBookings(getCurrentBookingQuery(state))
212
+ set({ bookings })
213
+ }
214
+ toast.success('Appointment deleted')
215
+ } catch (err: any) {
216
+ toast.error('Failed to delete appointment', { description: err?.message })
217
+ throw err
218
+ }
219
+ },
220
+
221
+ async updateBookingStatus(id, status) {
222
+ // Optimistic update — change color immediately
223
+ const prev = get().bookings
224
+ set({ bookings: prev.map((b) => b.id === id ? { ...b, status: status as any } : b) })
225
+ try {
226
+ await provider.updateBookingStatus(id, status)
227
+ // Refresh from server to get full data
228
+ const state = get()
229
+ if (state.dateRange.start && state.dateRange.end) {
230
+ const bookings = await provider.getBookings(getCurrentBookingQuery(state))
231
+ set({ bookings })
232
+ }
233
+ toast.success('Status updated')
234
+ } catch (err: any) {
235
+ // Rollback on failure
236
+ set({ bookings: prev })
237
+ toast.error('Failed to update status', { description: err?.message })
238
+ throw err
239
+ }
240
+ },
241
+
242
+ async rescheduleBooking(id, newStart, newEnd, newProfessionalId) {
243
+ try {
244
+ // Check for conflicts
245
+ const existing = get().bookings.find((b) => b.id === id)
246
+ const assigneeId = newProfessionalId ?? existing?.professionalId
247
+ if (assigneeId) {
248
+ const hasConflict = await provider.checkConflict({
249
+ assigneeId,
250
+ startsAt: newStart,
251
+ endsAt: newEnd,
252
+ excludeBookingId: id,
253
+ })
254
+ if (hasConflict) {
255
+ toast.error('Time conflict detected', { description: 'This professional already has an appointment at this time.' })
256
+ throw new Error('Time conflict')
257
+ }
258
+ }
259
+ await provider.updateBooking(id, {
260
+ startsAt: newStart,
261
+ endsAt: newEnd,
262
+ ...(newProfessionalId && { professionalId: newProfessionalId }),
263
+ })
264
+ const state = get()
265
+ if (state.dateRange.start && state.dateRange.end) {
266
+ const bookings = await provider.getBookings(getCurrentBookingQuery(state))
267
+ set({ bookings })
268
+ }
269
+ toast.success('Appointment rescheduled')
270
+ } catch (err: any) {
271
+ if (err?.message !== 'Time conflict') {
272
+ toast.error('Failed to reschedule', { description: err?.message })
273
+ }
274
+ throw err
275
+ }
276
+ },
277
+
278
+ setFilters(filters) {
279
+ set({
280
+ ...(filters.professionalIds !== undefined && { selectedProfessionalIds: filters.professionalIds }),
281
+ ...(filters.locationId !== undefined && { selectedLocationId: filters.locationId }),
282
+ ...(filters.statuses !== undefined && { selectedStatuses: filters.statuses }),
283
+ })
284
+ // Re-fetch with new filters
285
+ const { dateRange } = get()
286
+ if (dateRange.start && dateRange.end) {
287
+ get().fetchBookings(dateRange)
288
+ }
289
+ },
290
+
291
+ setView(view) { set({ currentView: view }) },
292
+ setSelectedDate(date) { set({ selectedDate: date }) },
293
+
294
+ openAppointmentModal(mode, options) {
295
+ set({ appointmentModal: { open: true, mode, bookingId: options?.bookingId, prefill: options?.prefill, initialTab: options?.initialTab } })
296
+ },
297
+
298
+ closeAppointmentModal() {
299
+ // Only toggle open — keep mode/bookingId/prefill intact so layout
300
+ // doesn't shift during the close animation
301
+ set((state) => ({ appointmentModal: { ...state.appointmentModal, open: false } }))
302
+ },
303
+ }))
304
+ }