@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 +430 -0
- package/dist/index.d.ts +316 -0
- package/dist/index.js +1917 -0
- package/package.json +47 -0
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
|