@devpablocristo/modules-scheduling 0.4.0
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/package.json +49 -0
- package/src/BlockedRangeModal.tsx +256 -0
- package/src/PublicSchedulingFlow.test.tsx +192 -0
- package/src/PublicSchedulingFlow.tsx +570 -0
- package/src/QueueOperatorBoard.test.tsx +169 -0
- package/src/QueueOperatorBoard.tsx +650 -0
- package/src/SchedulingBookingModal.test.tsx +241 -0
- package/src/SchedulingBookingModal.tsx +551 -0
- package/src/SchedulingCalendar.test.tsx +830 -0
- package/src/SchedulingCalendarBoard.tsx +1596 -0
- package/src/SchedulingDaySummary.test.tsx +136 -0
- package/src/SchedulingDaySummary.tsx +190 -0
- package/src/client.test.ts +105 -0
- package/src/client.ts +300 -0
- package/src/index.ts +66 -0
- package/src/locale.test.ts +36 -0
- package/src/locale.ts +91 -0
- package/src/next.ts +66 -0
- package/src/schedulingCalendarLogic.ts +373 -0
- package/src/styles.css +649 -0
- package/src/styles.next.css +1 -0
- package/src/types.ts +653 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
4
|
+
import { cleanup, render, screen, waitFor } from '@testing-library/react';
|
|
5
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import type { SchedulingClient } from './client';
|
|
7
|
+
import { SchedulingDaySummary } from './SchedulingDaySummary';
|
|
8
|
+
import type { DashboardStats, DayAgendaItem } from './types';
|
|
9
|
+
|
|
10
|
+
function createClient(overrides?: Partial<Record<keyof SchedulingClient, unknown>>): SchedulingClient {
|
|
11
|
+
const dashboard: DashboardStats = {
|
|
12
|
+
date: '2099-12-01',
|
|
13
|
+
timezone: 'America/Argentina/Tucuman',
|
|
14
|
+
bookings_today: 12,
|
|
15
|
+
confirmed_bookings_today: 7,
|
|
16
|
+
active_queues: 2,
|
|
17
|
+
waiting_tickets: 5,
|
|
18
|
+
tickets_in_service: 1,
|
|
19
|
+
};
|
|
20
|
+
const dayAgenda: DayAgendaItem[] = [
|
|
21
|
+
{
|
|
22
|
+
type: 'booking',
|
|
23
|
+
id: 'booking-1',
|
|
24
|
+
branch_id: 'branch-1',
|
|
25
|
+
service_id: 'service-1',
|
|
26
|
+
start_at: '2099-12-01T10:00:00Z',
|
|
27
|
+
end_at: '2099-12-01T10:30:00Z',
|
|
28
|
+
status: 'confirmed',
|
|
29
|
+
label: 'Consulta Ada',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
type: 'queue_ticket',
|
|
33
|
+
id: 'ticket-1',
|
|
34
|
+
branch_id: 'branch-1',
|
|
35
|
+
status: 'waiting',
|
|
36
|
+
label: 'A-17',
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
listBranches: vi.fn(),
|
|
42
|
+
listServices: vi.fn(),
|
|
43
|
+
listResources: vi.fn(),
|
|
44
|
+
listSlots: vi.fn(),
|
|
45
|
+
listBookings: vi.fn(),
|
|
46
|
+
getBooking: vi.fn(),
|
|
47
|
+
createBooking: vi.fn(),
|
|
48
|
+
confirmBooking: vi.fn(),
|
|
49
|
+
cancelBooking: vi.fn(),
|
|
50
|
+
checkInBooking: vi.fn(),
|
|
51
|
+
startService: vi.fn(),
|
|
52
|
+
completeBooking: vi.fn(),
|
|
53
|
+
markBookingNoShow: vi.fn(),
|
|
54
|
+
rescheduleBooking: vi.fn(),
|
|
55
|
+
listWaitlist: vi.fn(),
|
|
56
|
+
listQueues: vi.fn(),
|
|
57
|
+
createQueueTicket: vi.fn(),
|
|
58
|
+
getQueuePosition: vi.fn(),
|
|
59
|
+
pauseQueue: vi.fn(),
|
|
60
|
+
reopenQueue: vi.fn(),
|
|
61
|
+
closeQueue: vi.fn(),
|
|
62
|
+
callNext: vi.fn(),
|
|
63
|
+
serveTicket: vi.fn(),
|
|
64
|
+
completeTicket: vi.fn(),
|
|
65
|
+
markTicketNoShow: vi.fn(),
|
|
66
|
+
cancelTicket: vi.fn(),
|
|
67
|
+
returnTicketToWaiting: vi.fn(),
|
|
68
|
+
getDashboard: vi.fn(async () => dashboard),
|
|
69
|
+
getDayAgenda: vi.fn(async () => dayAgenda),
|
|
70
|
+
...(overrides ?? {}),
|
|
71
|
+
} as SchedulingClient;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function renderSummary(client: SchedulingClient) {
|
|
75
|
+
cleanup();
|
|
76
|
+
const queryClient = new QueryClient({
|
|
77
|
+
defaultOptions: {
|
|
78
|
+
queries: { retry: false },
|
|
79
|
+
mutations: { retry: false },
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return render(
|
|
84
|
+
<QueryClientProvider client={queryClient}>
|
|
85
|
+
<SchedulingDaySummary client={client} locale="es" initialDate="2099-12-01" />
|
|
86
|
+
</QueryClientProvider>,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function renderSummaryWithLocale(client: SchedulingClient, locale: string) {
|
|
91
|
+
cleanup();
|
|
92
|
+
const queryClient = new QueryClient({
|
|
93
|
+
defaultOptions: {
|
|
94
|
+
queries: { retry: false },
|
|
95
|
+
mutations: { retry: false },
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return render(
|
|
100
|
+
<QueryClientProvider client={queryClient}>
|
|
101
|
+
<SchedulingDaySummary client={client} locale={locale} initialDate="2099-12-01" />
|
|
102
|
+
</QueryClientProvider>,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
describe('SchedulingDaySummary', () => {
|
|
107
|
+
it('renders stats, next booking and queue flow from the shared client', async () => {
|
|
108
|
+
const client = createClient();
|
|
109
|
+
|
|
110
|
+
renderSummary(client);
|
|
111
|
+
|
|
112
|
+
await waitFor(() => {
|
|
113
|
+
expect(screen.getByText('Agenda de hoy')).toBeTruthy();
|
|
114
|
+
expect(screen.getByText('12')).toBeTruthy();
|
|
115
|
+
expect(screen.getByText('7')).toBeTruthy();
|
|
116
|
+
expect(screen.getByText('2')).toBeTruthy();
|
|
117
|
+
expect(screen.getByText('5')).toBeTruthy();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(screen.getByText('Consulta Ada')).toBeTruthy();
|
|
121
|
+
expect(screen.getByText('Confirmada')).toBeTruthy();
|
|
122
|
+
expect(screen.getByText('A-17')).toBeTruthy();
|
|
123
|
+
expect(screen.getAllByText('Esperando').length).toBeGreaterThan(0);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('supports regional locales while keeping the correct copy preset', async () => {
|
|
127
|
+
const client = createClient();
|
|
128
|
+
|
|
129
|
+
renderSummaryWithLocale(client, 'es-AR');
|
|
130
|
+
|
|
131
|
+
await waitFor(() => {
|
|
132
|
+
expect(screen.getByText('Agenda de hoy')).toBeTruthy();
|
|
133
|
+
expect(screen.getByText(/\d{1,2}\/\d{1,2}\/\d{4}/)).toBeTruthy();
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { useMemo, useState } from 'react';
|
|
2
|
+
import { useQuery } from '@tanstack/react-query';
|
|
3
|
+
import type { SchedulingClient } from './client';
|
|
4
|
+
import { formatSchedulingDateTime, resolveSchedulingCopyLocale } from './locale';
|
|
5
|
+
import type { DashboardStats, DayAgendaItem } from './types';
|
|
6
|
+
|
|
7
|
+
const summaryKeys = {
|
|
8
|
+
dashboard: (date: string) => ['scheduling-summary', 'dashboard', date] as const,
|
|
9
|
+
day: (date: string) => ['scheduling-summary', 'day', date] as const,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const schedulingDaySummaryCopyPresets = {
|
|
13
|
+
en: {
|
|
14
|
+
title: "Today's schedule",
|
|
15
|
+
description: 'Live bookings and queue activity for the current operating window.',
|
|
16
|
+
dateLabel: 'Date',
|
|
17
|
+
loading: 'Loading schedule snapshot…',
|
|
18
|
+
empty: 'No schedule activity for this date.',
|
|
19
|
+
bookings: 'Bookings',
|
|
20
|
+
confirmed: 'Confirmed',
|
|
21
|
+
activeQueues: 'Queues',
|
|
22
|
+
waiting: 'Waiting',
|
|
23
|
+
nextBooking: 'Next booking',
|
|
24
|
+
queueFlow: 'Queue flow',
|
|
25
|
+
noUpcoming: 'No upcoming bookings.',
|
|
26
|
+
noQueueFlow: 'No queue tickets in the agenda.',
|
|
27
|
+
statuses: {
|
|
28
|
+
confirmed: 'Confirmed',
|
|
29
|
+
waiting: 'Waiting',
|
|
30
|
+
called: 'Called',
|
|
31
|
+
serving: 'Serving',
|
|
32
|
+
completed: 'Completed',
|
|
33
|
+
cancelled: 'Cancelled',
|
|
34
|
+
no_show: 'No-show',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
es: {
|
|
38
|
+
title: 'Agenda de hoy',
|
|
39
|
+
description: 'Reservas activas y movimiento de cola en la ventana operativa actual.',
|
|
40
|
+
dateLabel: 'Fecha',
|
|
41
|
+
loading: 'Cargando resumen de agenda…',
|
|
42
|
+
empty: 'No hay actividad en la agenda para esta fecha.',
|
|
43
|
+
bookings: 'Reservas',
|
|
44
|
+
confirmed: 'Confirmadas',
|
|
45
|
+
activeQueues: 'Colas',
|
|
46
|
+
waiting: 'Esperando',
|
|
47
|
+
nextBooking: 'Próxima reserva',
|
|
48
|
+
queueFlow: 'Flujo de cola',
|
|
49
|
+
noUpcoming: 'No hay reservas próximas.',
|
|
50
|
+
noQueueFlow: 'No hay tickets de cola en la agenda.',
|
|
51
|
+
statuses: {
|
|
52
|
+
confirmed: 'Confirmada',
|
|
53
|
+
waiting: 'Esperando',
|
|
54
|
+
called: 'Llamado',
|
|
55
|
+
serving: 'Atendiendo',
|
|
56
|
+
completed: 'Completado',
|
|
57
|
+
cancelled: 'Cancelado',
|
|
58
|
+
no_show: 'No-show',
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
} as const;
|
|
62
|
+
|
|
63
|
+
export type SchedulingDaySummaryProps = {
|
|
64
|
+
client: SchedulingClient;
|
|
65
|
+
locale?: string;
|
|
66
|
+
className?: string;
|
|
67
|
+
initialDate?: string;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
function todayValue(): string {
|
|
71
|
+
return new Date().toISOString().slice(0, 10);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isUpcomingBooking(item: DayAgendaItem): boolean {
|
|
75
|
+
if (item.type !== 'booking' || !item.start_at) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
return new Date(item.start_at).getTime() >= Date.now();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isQueueItem(item: DayAgendaItem): boolean {
|
|
82
|
+
return item.type === 'queue_ticket';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function SchedulingDaySummary({
|
|
86
|
+
client,
|
|
87
|
+
locale = 'en',
|
|
88
|
+
className = '',
|
|
89
|
+
initialDate,
|
|
90
|
+
}: SchedulingDaySummaryProps) {
|
|
91
|
+
const copy = schedulingDaySummaryCopyPresets[resolveSchedulingCopyLocale(locale)];
|
|
92
|
+
const statusLabel = (status: string) => copy.statuses[status as keyof typeof copy.statuses] ?? status;
|
|
93
|
+
const [selectedDate, setSelectedDate] = useState(initialDate ?? todayValue());
|
|
94
|
+
|
|
95
|
+
const dashboardQuery = useQuery<DashboardStats>({
|
|
96
|
+
queryKey: summaryKeys.dashboard(selectedDate),
|
|
97
|
+
queryFn: () => client.getDashboard(undefined, selectedDate),
|
|
98
|
+
staleTime: 20_000,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const dayQuery = useQuery<DayAgendaItem[]>({
|
|
102
|
+
queryKey: summaryKeys.day(selectedDate),
|
|
103
|
+
queryFn: () => client.getDayAgenda(undefined, selectedDate),
|
|
104
|
+
staleTime: 20_000,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const nextBooking = useMemo(
|
|
108
|
+
() => (dayQuery.data ?? []).filter(isUpcomingBooking).sort((a, b) => (a.start_at ?? '').localeCompare(b.start_at ?? ''))[0],
|
|
109
|
+
[dayQuery.data],
|
|
110
|
+
);
|
|
111
|
+
const queueFlow = useMemo(() => (dayQuery.data ?? []).filter(isQueueItem).slice(0, 6), [dayQuery.data]);
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<div className={`card modules-scheduling__day-summary ${className}`.trim()}>
|
|
115
|
+
<div className="card-header">
|
|
116
|
+
<div>
|
|
117
|
+
<h2>{copy.title}</h2>
|
|
118
|
+
<p className="text-secondary">{copy.description}</p>
|
|
119
|
+
</div>
|
|
120
|
+
<div className="form-group">
|
|
121
|
+
<label htmlFor="scheduling-day-summary-date">{copy.dateLabel}</label>
|
|
122
|
+
<input
|
|
123
|
+
id="scheduling-day-summary-date"
|
|
124
|
+
type="date"
|
|
125
|
+
value={selectedDate}
|
|
126
|
+
onChange={(event) => setSelectedDate(event.target.value)}
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
{dashboardQuery.isLoading || dayQuery.isLoading ? (
|
|
132
|
+
<div className="modules-scheduling__empty">
|
|
133
|
+
<div className="spinner" />
|
|
134
|
+
<p>{copy.loading}</p>
|
|
135
|
+
</div>
|
|
136
|
+
) : !dashboardQuery.data ? (
|
|
137
|
+
<div className="modules-scheduling__empty">{copy.empty}</div>
|
|
138
|
+
) : (
|
|
139
|
+
<>
|
|
140
|
+
<div className="modules-scheduling__summary stats-grid">
|
|
141
|
+
<article className="stat-card">
|
|
142
|
+
<div className="stat-label">{copy.bookings}</div>
|
|
143
|
+
<div className="stat-value">{dashboardQuery.data.bookings_today}</div>
|
|
144
|
+
</article>
|
|
145
|
+
<article className="stat-card">
|
|
146
|
+
<div className="stat-label">{copy.confirmed}</div>
|
|
147
|
+
<div className="stat-value">{dashboardQuery.data.confirmed_bookings_today}</div>
|
|
148
|
+
</article>
|
|
149
|
+
<article className="stat-card">
|
|
150
|
+
<div className="stat-label">{copy.activeQueues}</div>
|
|
151
|
+
<div className="stat-value">{dashboardQuery.data.active_queues}</div>
|
|
152
|
+
</article>
|
|
153
|
+
<article className="stat-card">
|
|
154
|
+
<div className="stat-label">{copy.waiting}</div>
|
|
155
|
+
<div className="stat-value">{dashboardQuery.data.waiting_tickets}</div>
|
|
156
|
+
</article>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<div className="modules-scheduling__day-summary-grid">
|
|
160
|
+
<section className="modules-scheduling__day-summary-section">
|
|
161
|
+
<div className="modules-scheduling__day-summary-title">{copy.nextBooking}</div>
|
|
162
|
+
{nextBooking ? (
|
|
163
|
+
<div className="modules-scheduling__day-summary-item">
|
|
164
|
+
<strong>{nextBooking.label}</strong>
|
|
165
|
+
<span>{formatSchedulingDateTime(nextBooking.start_at, locale)}</span>
|
|
166
|
+
<span>{statusLabel(nextBooking.status)}</span>
|
|
167
|
+
</div>
|
|
168
|
+
) : (
|
|
169
|
+
<div className="modules-scheduling__queue-empty">{copy.noUpcoming}</div>
|
|
170
|
+
)}
|
|
171
|
+
</section>
|
|
172
|
+
<section className="modules-scheduling__day-summary-section">
|
|
173
|
+
<div className="modules-scheduling__day-summary-title">{copy.queueFlow}</div>
|
|
174
|
+
{queueFlow.length === 0 ? (
|
|
175
|
+
<div className="modules-scheduling__queue-empty">{copy.noQueueFlow}</div>
|
|
176
|
+
) : (
|
|
177
|
+
queueFlow.map((item) => (
|
|
178
|
+
<div key={item.id} className="modules-scheduling__day-summary-item">
|
|
179
|
+
<strong>{item.label}</strong>
|
|
180
|
+
<span>{statusLabel(item.status)}</span>
|
|
181
|
+
</div>
|
|
182
|
+
))
|
|
183
|
+
)}
|
|
184
|
+
</section>
|
|
185
|
+
</div>
|
|
186
|
+
</>
|
|
187
|
+
)}
|
|
188
|
+
</div>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { createPublicSchedulingClient, createSchedulingClient } from "./client";
|
|
5
|
+
|
|
6
|
+
describe("scheduling client compatibility", () => {
|
|
7
|
+
it("normalizes deprecated appointments_enabled into scheduling_enabled for public info", async () => {
|
|
8
|
+
const request = vi.fn().mockResolvedValue({
|
|
9
|
+
org_id: "org-demo",
|
|
10
|
+
slug: "demo-org",
|
|
11
|
+
name: "Demo Org",
|
|
12
|
+
business_name: "Demo Scheduling",
|
|
13
|
+
appointments_enabled: true,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const client = createPublicSchedulingClient(request);
|
|
17
|
+
const info = await client.getBusinessInfo("demo-org");
|
|
18
|
+
|
|
19
|
+
expect(request).toHaveBeenCalledWith("/v1/public/demo-org/info");
|
|
20
|
+
expect(info.scheduling_enabled).toBe(true);
|
|
21
|
+
expect(info.appointments_enabled).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("builds scheduling slot queries with branch, service and resource selectors", async () => {
|
|
25
|
+
const request = vi.fn().mockResolvedValue({ items: [] });
|
|
26
|
+
|
|
27
|
+
const client = createSchedulingClient(request);
|
|
28
|
+
await client.listSlots({
|
|
29
|
+
branchId: "branch-1",
|
|
30
|
+
serviceId: "service-1",
|
|
31
|
+
resourceId: "resource-1",
|
|
32
|
+
date: "2026-04-03",
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
expect(request).toHaveBeenCalledWith(
|
|
36
|
+
"/v1/scheduling/slots?branch_id=branch-1&service_id=service-1&resource_id=resource-1&date=2026-04-03",
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("lists blocked ranges with branch, resource and date filters", async () => {
|
|
41
|
+
const request = vi.fn().mockResolvedValue({ items: [] });
|
|
42
|
+
|
|
43
|
+
const client = createSchedulingClient(request);
|
|
44
|
+
await client.listBlockedRanges({ branchId: "branch-1", resourceId: "res-1", date: "2026-04-06" });
|
|
45
|
+
|
|
46
|
+
expect(request).toHaveBeenCalledWith(
|
|
47
|
+
"/v1/scheduling/blocked-ranges?branch_id=branch-1&resource_id=res-1&date=2026-04-06",
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("creates a blocked range with POST and the payload as body", async () => {
|
|
52
|
+
const request = vi.fn().mockResolvedValue({ id: "br-1" });
|
|
53
|
+
|
|
54
|
+
const client = createSchedulingClient(request);
|
|
55
|
+
await client.createBlockedRange({
|
|
56
|
+
branch_id: "branch-1",
|
|
57
|
+
kind: "manual",
|
|
58
|
+
reason: "Reunión con proveedor",
|
|
59
|
+
start_at: "2026-04-06T17:00:00Z",
|
|
60
|
+
end_at: "2026-04-06T18:00:00Z",
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(request).toHaveBeenCalledWith("/v1/scheduling/blocked-ranges", {
|
|
64
|
+
method: "POST",
|
|
65
|
+
body: {
|
|
66
|
+
branch_id: "branch-1",
|
|
67
|
+
kind: "manual",
|
|
68
|
+
reason: "Reunión con proveedor",
|
|
69
|
+
start_at: "2026-04-06T17:00:00Z",
|
|
70
|
+
end_at: "2026-04-06T18:00:00Z",
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("updates a blocked range with PATCH and the id in the path", async () => {
|
|
76
|
+
const request = vi.fn().mockResolvedValue({ id: "br-1" });
|
|
77
|
+
|
|
78
|
+
const client = createSchedulingClient(request);
|
|
79
|
+
await client.updateBlockedRange("br-1", {
|
|
80
|
+
branch_id: "branch-1",
|
|
81
|
+
kind: "manual",
|
|
82
|
+
start_at: "2026-04-06T17:00:00Z",
|
|
83
|
+
end_at: "2026-04-06T19:00:00Z",
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(request).toHaveBeenCalledWith("/v1/scheduling/blocked-ranges/br-1", {
|
|
87
|
+
method: "PATCH",
|
|
88
|
+
body: {
|
|
89
|
+
branch_id: "branch-1",
|
|
90
|
+
kind: "manual",
|
|
91
|
+
start_at: "2026-04-06T17:00:00Z",
|
|
92
|
+
end_at: "2026-04-06T19:00:00Z",
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("deletes a blocked range with DELETE", async () => {
|
|
98
|
+
const request = vi.fn().mockResolvedValue(undefined);
|
|
99
|
+
|
|
100
|
+
const client = createSchedulingClient(request);
|
|
101
|
+
await client.deleteBlockedRange("br-1");
|
|
102
|
+
|
|
103
|
+
expect(request).toHaveBeenCalledWith("/v1/scheduling/blocked-ranges/br-1", { method: "DELETE" });
|
|
104
|
+
});
|
|
105
|
+
});
|