@adatechnology/scheduling-ui 0.1.0-rc.1

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 ADDED
@@ -0,0 +1,430 @@
1
+ # @adatechnology/scheduling-ui
2
+
3
+ **Interface de agendamento modular** — tela com navegação por abas, dois hooks reutilizáveis e uma fábrica de componentes. Agenda, reservas, recursos, serviços e regras de disponibilidade em cinco áreas, cada uma com sua grade, filtros e ações.
4
+
5
+ - **Provider + hooks** para consumir a agenda em qualquer página do host
6
+ - **SchedulingWorkspace** — composição pronta de cinco áreas de trabalho
7
+ - **Sem fetch direto** — o host implementa `SchedulingApi`, separando a UI da lógica HTTP
8
+ - **Área controlada ou interna** — sincroniza com query string (sobrevive a refresh) ou gerencia estado localmente
9
+ - **TypeScript completo, React 18+**, com i18n português-brasileiro
10
+
11
+ ---
12
+
13
+ ## Instalação
14
+
15
+ ```bash
16
+ bun add @adatechnology/scheduling-ui
17
+ # ou: npm install / pnpm add
18
+ ```
19
+
20
+ Dependências de pares obrigatórias:
21
+
22
+ ```bash
23
+ bun add react react-dom @tanstack/react-query
24
+ ```
25
+
26
+ O pacote importa tipos de `@adatechnology/scheduling-contracts` automaticamente (workspace dependency).
27
+
28
+ ---
29
+
30
+ ## Uso básico
31
+
32
+ ```tsx
33
+ import { SchedulingProvider, SchedulingWorkspace, type SchedulingApi } from '@adatechnology/scheduling-ui'
34
+
35
+ // 1. Implementar SchedulingApi no seu cliente HTTP ou module
36
+ const schedulingApi: SchedulingApi = {
37
+ // Recursos
38
+ async listResources(params) {
39
+ const res = await fetch('/api/v1/scheduling/resources', { /* params */ })
40
+ return res.json()
41
+ },
42
+ async createResource(input) {
43
+ const res = await fetch('/api/v1/scheduling/resources', {
44
+ method: 'POST',
45
+ body: JSON.stringify(input),
46
+ })
47
+ return res.json()
48
+ },
49
+ async updateResource(id, input) {
50
+ const res = await fetch(`/api/v1/scheduling/resources/${id}`, {
51
+ method: 'PUT',
52
+ body: JSON.stringify(input),
53
+ })
54
+ return res.json()
55
+ },
56
+ async deleteResource(id) {
57
+ await fetch(`/api/v1/scheduling/resources/${id}`, { method: 'DELETE' })
58
+ },
59
+
60
+ // Serviços
61
+ async listServices(params) {
62
+ const res = await fetch('/api/v1/scheduling/services', { /* params */ })
63
+ return res.json()
64
+ },
65
+ async createService(input) {
66
+ const res = await fetch('/api/v1/scheduling/services', {
67
+ method: 'POST',
68
+ body: JSON.stringify(input),
69
+ })
70
+ return res.json()
71
+ },
72
+ async updateService(id, input) {
73
+ const res = await fetch(`/api/v1/scheduling/services/${id}`, {
74
+ method: 'PUT',
75
+ body: JSON.stringify(input),
76
+ })
77
+ return res.json()
78
+ },
79
+ async deleteService(id) {
80
+ await fetch(`/api/v1/scheduling/services/${id}`, { method: 'DELETE' })
81
+ },
82
+
83
+ // Regras de disponibilidade
84
+ async listAvailabilityRules(resourceId) {
85
+ const res = await fetch(`/api/v1/scheduling/resources/${resourceId}/availability-rules`)
86
+ return res.json()
87
+ },
88
+ async setAvailabilityRules(resourceId, rules) {
89
+ const res = await fetch(`/api/v1/scheduling/resources/${resourceId}/availability-rules`, {
90
+ method: 'PUT',
91
+ body: JSON.stringify(rules),
92
+ })
93
+ return res.json()
94
+ },
95
+
96
+ // Exceções de disponibilidade
97
+ async listAvailabilityExceptions(resourceId) {
98
+ const res = await fetch(`/api/v1/scheduling/resources/${resourceId}/availability-exceptions`)
99
+ return res.json()
100
+ },
101
+ async addAvailabilityException(input) {
102
+ const res = await fetch('/api/v1/scheduling/availability-exceptions', {
103
+ method: 'POST',
104
+ body: JSON.stringify(input),
105
+ })
106
+ return res.json()
107
+ },
108
+ async removeAvailabilityException(id) {
109
+ await fetch(`/api/v1/scheduling/availability-exceptions/${id}`, { method: 'DELETE' })
110
+ },
111
+
112
+ // Disponibilidade de horários
113
+ async getAvailableSlots(params) {
114
+ const res = await fetch('/api/v1/scheduling/available-slots', { /* params */ })
115
+ return res.json()
116
+ },
117
+
118
+ // Reservas
119
+ async listBookings(params) {
120
+ const res = await fetch('/api/v1/scheduling/bookings', { /* params */ })
121
+ return res.json()
122
+ },
123
+ async getBooking(id) {
124
+ const res = await fetch(`/api/v1/scheduling/bookings/${id}`)
125
+ return res.json()
126
+ },
127
+ async requestBooking(input, idempotencyKey) {
128
+ const res = await fetch('/api/v1/scheduling/bookings', {
129
+ method: 'POST',
130
+ headers: { 'Idempotency-Key': idempotencyKey },
131
+ body: JSON.stringify(input),
132
+ })
133
+ return res.json()
134
+ },
135
+ async confirmBooking(id) {
136
+ const res = await fetch(`/api/v1/scheduling/bookings/${id}/confirm`, { method: 'POST' })
137
+ return res.json()
138
+ },
139
+ async rescheduleBooking(id, input) {
140
+ const res = await fetch(`/api/v1/scheduling/bookings/${id}/reschedule`, {
141
+ method: 'POST',
142
+ body: JSON.stringify(input),
143
+ })
144
+ return res.json()
145
+ },
146
+ async cancelBooking(id, input) {
147
+ const res = await fetch(`/api/v1/scheduling/bookings/${id}/cancel`, {
148
+ method: 'POST',
149
+ body: JSON.stringify(input),
150
+ })
151
+ return res.json()
152
+ },
153
+ async completeBooking(id) {
154
+ const res = await fetch(`/api/v1/scheduling/bookings/${id}/complete`, { method: 'POST' })
155
+ return res.json()
156
+ },
157
+ async markNoShow(id) {
158
+ const res = await fetch(`/api/v1/scheduling/bookings/${id}/no-show`, { method: 'POST' })
159
+ return res.json()
160
+ },
161
+ }
162
+
163
+ // 2. Envolver o SchedulingWorkspace com o provider
164
+ export function SchedulingPage() {
165
+ return (
166
+ <SchedulingProvider api={schedulingApi}>
167
+ <SchedulingWorkspace />
168
+ </SchedulingProvider>
169
+ )
170
+ }
171
+ ```
172
+
173
+ ---
174
+
175
+ ## O provider e os hooks
176
+
177
+ `SchedulingProvider` injeta a API e a configuração no contexto. Componentes internos consomem via dois hooks:
178
+
179
+ ```tsx
180
+ import { useScheduling, useSchedulingConfig } from '@adatechnology/scheduling-ui'
181
+
182
+ // Dentro de <SchedulingProvider>
183
+ export function MyComponent() {
184
+ const api = useScheduling() // SchedulingApi
185
+ const config = useSchedulingConfig() // { locale, weekStartsOn }
186
+
187
+ // Chamar métodos da API
188
+ const bookings = await api.listBookings()
189
+
190
+ return <div>Locale: {config.locale}</div>
191
+ }
192
+ ```
193
+
194
+ Os hooks lançam erro se usados fora do provider:
195
+
196
+ ```
197
+ Error: useScheduling() must be used within a <SchedulingProvider>
198
+ ```
199
+
200
+ ---
201
+
202
+ ## SchedulingApi
203
+
204
+ Contrato que **o host implementa**. O pacote nunca faz `fetch` direto — chama os métodos que você forneceu via `SchedulingProvider`.
205
+
206
+ **Nenhum método recebe `companyId`** — o host resolve o tenant do contexto autenticado antes de implementar a API
207
+ (veja `security.md` §2, BOLA). Isso deixa o componente agnóstico sobre isolamento multi-empresa.
208
+
209
+ ### Recursos
210
+
211
+ ```ts
212
+ type SchedulingApi = {
213
+ // Listar com paginação e filtros opcionais
214
+ listResources(params?: Omit<ListResourcesParams, 'companyId'>)
215
+ : Promise<PaginatedResponse<Resource>>
216
+
217
+ // Criar novo recurso
218
+ createResource(input: CreateResourceInput): Promise<Resource>
219
+
220
+ // Atualizar
221
+ updateResource(id: ResourceId, input: UpdateResourceInput): Promise<Resource>
222
+
223
+ // Excluir
224
+ deleteResource(id: ResourceId): Promise<void>
225
+ }
226
+ ```
227
+
228
+ ### Serviços
229
+
230
+ ```ts
231
+ type SchedulingApi = {
232
+ listServices(params?: Omit<ListServicesParams, 'companyId'>)
233
+ : Promise<PaginatedResponse<Service>>
234
+
235
+ createService(input: CreateServiceInput): Promise<Service>
236
+
237
+ updateService(id: ServiceId, input: UpdateServiceInput): Promise<Service>
238
+
239
+ deleteService(id: ServiceId): Promise<void>
240
+ }
241
+ ```
242
+
243
+ ### Disponibilidade (Regras e Exceções)
244
+
245
+ ```ts
246
+ type SchedulingApi = {
247
+ // Regras de horário de funcionamento por recurso
248
+ listAvailabilityRules(resourceId: ResourceId): Promise<readonly AvailabilityRule[]>
249
+
250
+ setAvailabilityRules(
251
+ resourceId: ResourceId,
252
+ rules: readonly CreateAvailabilityRuleInput[],
253
+ ): Promise<readonly AvailabilityRule[]>
254
+
255
+ // Exceções (feriados, bloqueios, etc)
256
+ listAvailabilityExceptions(resourceId: ResourceId)
257
+ : Promise<readonly AvailabilityException[]>
258
+
259
+ addAvailabilityException(input: CreateAvailabilityExceptionInput)
260
+ : Promise<AvailabilityException>
261
+
262
+ removeAvailabilityException(id: AvailabilityExceptionId): Promise<void>
263
+ }
264
+ ```
265
+
266
+ ### Horários Disponíveis
267
+
268
+ ```ts
269
+ type SchedulingApi = {
270
+ // Buscar slots livres para um serviço/recurso/período
271
+ getAvailableSlots(params: Omit<GetAvailabilityParams, 'companyId'>)
272
+ : Promise<readonly AvailableSlot[]>
273
+ }
274
+ ```
275
+
276
+ ### Reservas
277
+
278
+ ```ts
279
+ type SchedulingApi = {
280
+ // Listar reservas do negócio
281
+ listBookings(params?: Omit<ListBookingsParams, 'companyId'>)
282
+ : Promise<PaginatedResponse<Booking>>
283
+
284
+ // Buscar uma reserva
285
+ getBooking(id: BookingId): Promise<Booking>
286
+
287
+ // Criar reserva. ÚNICO método com idempotencyKey como segundo parâmetro
288
+ requestBooking(input: RequestBookingInput, idempotencyKey: string): Promise<Booking>
289
+
290
+ // Confirmar após validação
291
+ confirmBooking(id: BookingId): Promise<Booking>
292
+
293
+ // Remarcar
294
+ rescheduleBooking(id: BookingId, input: RescheduleBookingInput): Promise<Booking>
295
+
296
+ // Cancelar
297
+ cancelBooking(id: BookingId, input: CancelBookingInput): Promise<Booking>
298
+
299
+ // Marcar como completa
300
+ completeBooking(id: BookingId): Promise<Booking>
301
+
302
+ // Marcar como não-comparecimento
303
+ markNoShow(id: BookingId): Promise<Booking>
304
+ }
305
+ ```
306
+
307
+ Todos os tipos (`Resource`, `Service`, `Booking`, etc) vêm de `@adatechnology/scheduling-contracts`.
308
+
309
+ ---
310
+
311
+ ## SchedulingWorkspace
312
+
313
+ Componente com navegação em cinco abas e a composição pronta. Cada aba é uma área:
314
+
315
+ - **Agenda** — visualizar e buscar reservas em grade
316
+ - **Reservas** — gerenciar reservas (confirmar, remarcar, cancelar)
317
+ - **Recursos** — criar, editar e excluir recursos
318
+ - **Serviços** — criar, editar e excluir serviços
319
+ - **Disponibilidade** — definir regras e exceções por recurso
320
+
321
+ ### Uso não-controlado (estado interno)
322
+
323
+ ```tsx
324
+ <SchedulingProvider api={schedulingApi}>
325
+ <SchedulingWorkspace />
326
+ </SchedulingProvider>
327
+ ```
328
+
329
+ A aba ativa é guardada no estado interno do componente. Refresh ou link colado volta para a aba padrão (Agenda).
330
+
331
+ ### Uso controlado (query string)
332
+
333
+ Para sincronizar a aba aberta com a URL (e sobreviver a refresh/link):
334
+
335
+ ```tsx
336
+ 'use client'
337
+
338
+ import { useSearchParams, useRouter } from 'next/navigation'
339
+ import { SchedulingProvider, SchedulingWorkspace, isSchedulingWorkspaceArea, SCHEDULING_WORKSPACE_AREA } from '@adatechnology/scheduling-ui'
340
+
341
+ export function SchedulingPage() {
342
+ const router = useRouter()
343
+ const searchParams = useSearchParams()
344
+
345
+ const areaFromUrl = searchParams.get('area')
346
+ const area = areaFromUrl && isSchedulingWorkspaceArea(areaFromUrl)
347
+ ? areaFromUrl
348
+ : SCHEDULING_WORKSPACE_AREA.AGENDA
349
+
350
+ function handleAreaChange(nextArea: string) {
351
+ const params = new URLSearchParams(searchParams)
352
+ params.set('area', nextArea)
353
+ router.push(`?${params.toString()}`)
354
+ }
355
+
356
+ return (
357
+ <SchedulingProvider api={schedulingApi}>
358
+ <SchedulingWorkspace area={area} onAreaChange={handleAreaChange} />
359
+ </SchedulingProvider>
360
+ )
361
+ }
362
+ ```
363
+
364
+ ### Props
365
+
366
+ ```ts
367
+ type SchedulingWorkspaceProps = {
368
+ // Sobrescrever rótulos de abas e títulos
369
+ readonly labels?: Partial<SchedulingWorkspaceLabels>
370
+
371
+ // Renderizar botões/ações no cabeçalho (ex: exportar agenda)
372
+ readonly renderHeaderActions?: () => ReactNode
373
+
374
+ // Aba aberta — torna o componente controlado
375
+ readonly area?: SchedulingWorkspaceArea
376
+
377
+ // Chamado quando o usuário clica em uma aba
378
+ readonly onAreaChange?: (area: SchedulingWorkspaceArea) => void
379
+ }
380
+ ```
381
+
382
+ Valores válidos para `area` (constante, não `enum` — projeto usa `as const` por convenção):
383
+
384
+ ```ts
385
+ import { SCHEDULING_WORKSPACE_AREA } from '@adatechnology/scheduling-ui'
386
+
387
+ const SCHEDULING_WORKSPACE_AREA = {
388
+ AGENDA: 'agenda',
389
+ BOOKINGS: 'bookings',
390
+ RESOURCES: 'resources',
391
+ SERVICES: 'services',
392
+ AVAILABILITY: 'availability',
393
+ } as const
394
+ ```
395
+
396
+ Use `isSchedulingWorkspaceArea(value)` para validar strings da URL:
397
+
398
+ ```ts
399
+ const area = searchParams.get('area')
400
+ if (isSchedulingWorkspaceArea(area)) {
401
+ // TypeScript agora sabe que `area` é válido
402
+ }
403
+ ```
404
+
405
+ ---
406
+
407
+ ## Configuração
408
+
409
+ `SchedulingProvider` aceita uma configuração parcial (`config`), que mescla com os padrões:
410
+
411
+ ```tsx
412
+ <SchedulingProvider
413
+ api={schedulingApi}
414
+ config={{
415
+ locale: 'pt-BR', // padrão
416
+ weekStartsOn: 1, // 0 = domingo, 1 = segunda-feira (padrão)
417
+ }}
418
+ >
419
+ <SchedulingWorkspace />
420
+ </SchedulingProvider>
421
+ ```
422
+
423
+ - **`locale`** — identifica o idioma; usado em i18n dentro do componente. Padrão: `'pt-BR'`.
424
+ - **`weekStartsOn`** — primeiro dia da semana na grade de agenda (0 = domingo, 1 = segunda). Padrão: `1`.
425
+
426
+ ---
427
+
428
+ ## Licença
429
+
430
+ MIT © Ada Technology