@fayz-ai/plugin-agenda 0.18.4 → 0.20.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.
Files changed (45) hide show
  1. package/dist/{chunk-RVO5ILXN.js → chunk-A77PUFY5.js} +85 -2
  2. package/dist/chunk-A77PUFY5.js.map +1 -0
  3. package/dist/components/AgendaFilterRail.d.ts +1 -1
  4. package/dist/components/AgendaGeneralSettings.d.ts.map +1 -1
  5. package/dist/components/AppointmentModal.d.ts.map +1 -1
  6. package/dist/components/AppointmentPopover.d.ts.map +1 -1
  7. package/dist/components/AvailabilityHeaderControl.d.ts +6 -0
  8. package/dist/components/AvailabilityHeaderControl.d.ts.map +1 -0
  9. package/dist/components/EventContextMenu.d.ts.map +1 -1
  10. package/dist/components/SeriesDeleteChoice.d.ts +20 -0
  11. package/dist/components/SeriesDeleteChoice.d.ts.map +1 -0
  12. package/dist/config.d.ts +5 -5
  13. package/dist/config.d.ts.map +1 -1
  14. package/dist/data/mock.d.ts.map +1 -1
  15. package/dist/data/supabase.d.ts.map +1 -1
  16. package/dist/data/turn-queue-types.d.ts +71 -0
  17. package/dist/data/turn-queue-types.d.ts.map +1 -0
  18. package/dist/data/turn-queue.d.ts +4 -0
  19. package/dist/data/turn-queue.d.ts.map +1 -0
  20. package/dist/data/types.d.ts +73 -0
  21. package/dist/data/types.d.ts.map +1 -1
  22. package/dist/index.d.ts +3 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +3072 -858
  25. package/dist/index.js.map +1 -1
  26. package/dist/lib/recurrence.d.ts +122 -0
  27. package/dist/lib/recurrence.d.ts.map +1 -0
  28. package/dist/lib/turn-queue-plugin.d.ts +4 -0
  29. package/dist/lib/turn-queue-plugin.d.ts.map +1 -0
  30. package/dist/lib/turn-queue-presence.d.ts +4 -0
  31. package/dist/lib/turn-queue-presence.d.ts.map +1 -0
  32. package/dist/locales/en.d.ts.map +1 -1
  33. package/dist/locales/pt-BR.d.ts.map +1 -1
  34. package/dist/migrations/index.d.ts +4 -0
  35. package/dist/migrations/index.d.ts.map +1 -1
  36. package/dist/public/index.js +2 -2
  37. package/dist/store.d.ts +7 -1
  38. package/dist/store.d.ts.map +1 -1
  39. package/dist/types.d.ts +9 -0
  40. package/dist/types.d.ts.map +1 -1
  41. package/dist/views/CalendarView.d.ts.map +1 -1
  42. package/dist/views/TurnQueuePage.d.ts +10 -0
  43. package/dist/views/TurnQueuePage.d.ts.map +1 -0
  44. package/package.json +6 -6
  45. package/dist/chunk-RVO5ILXN.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
- import { createMockAgendaProvider, createSupabasePublicBookingProvider } from './chunk-RVO5ILXN.js';
2
- export { createMockAgendaProvider } from './chunk-RVO5ILXN.js';
3
- import React20, { useState, useEffect, useRef, useMemo, useCallback, useLayoutEffect } from 'react';
4
- import { tl, recordRailFields, RECORD_GROUP, activeFacet, connectionStatus, createConnectionStore, todayLocal, getSupabaseClientOptional, getActiveTenantId, registerTranslations, registerMessageDefaults, createSafeDataProvider, connectorRuntimeToken, CONNECTOR_RUNTIME_TOKEN_HEADER, getActiveUnitId, assertWrote, emitDataChanged, globalCache, usePluginRuntimeOptional, useTranslation, emitDomainEvent, getCurrentLocale, useActiveUnitId, localDateKey, useDataChanged, formatCurrency, resolveDataProvider } from '@fayz-ai/core';
5
- import { createPluginContext, fayzConnectorOAuth, setScheduleBlockConfig, getScheduleBlockConfig, registerPersonActions, PluginSettingsPanel, createArchetypeLookup, dedup, usePluginPrefs, useTenantPluginSettings, SettingsGroup, ToggleRow, SelectRow, PersonLink, PageActions, useRightRailStore, usePermissionOptional, ScheduleEditor, useLimitGuard, useLookupCreate, CrudFormPage, CrudDetailPage, ContactPicker, openEntity, PaymentStatusBadge as PaymentStatusBadge$1, RightRailPage, invalidateLimit } from '@fayz-ai/admin';
1
+ import { createMockAgendaProvider, createSupabasePublicBookingProvider } from './chunk-A77PUFY5.js';
2
+ export { createMockAgendaProvider } from './chunk-A77PUFY5.js';
3
+ import React21, { useState, useEffect, useRef, useMemo, useCallback, useLayoutEffect } from 'react';
4
+ import { tl, recordRailFields, RECORD_GROUP, activeFacet, connectionStatus, createConnectionStore, getActiveTenantId, createSafeDataProvider, todayLocal, getSupabaseClientOptional, registerTranslations, registerMessageDefaults, getActiveUnitId, assertWrote, connectorRuntimeToken, CONNECTOR_RUNTIME_TOKEN_HEADER, emitDataChanged, globalCache, usePluginRuntimeOptional, useTranslation, useActiveUnitId, emitDomainEvent, getCurrentLocale, localDateKey, useDataChanged, formatCurrency, resolveDataProvider } from '@fayz-ai/core';
5
+ import { createPluginContext, fayzConnectorOAuth, setScheduleBlockConfig, getScheduleBlockConfig, registerPersonActions, PluginSettingsPanel, createArchetypeLookup, dedup, usePluginPrefs, useTenantPluginSettings, SettingsGroup, ToggleRow, SelectRow, PermissionGate, PersonLink, PageActions, useRightRailStore, usePermissionOptional, ScheduleEditor, useLimitGuard, useLookupCreate, CrudFormPage, CrudDetailPage, ContactPicker, openEntity, PaymentStatusBadge as PaymentStatusBadge$1, RightRailPage, invalidateLimit } from '@fayz-ai/admin';
6
6
  import FullCalendar from '@fullcalendar/react';
7
7
  import dayGridPlugin from '@fullcalendar/daygrid';
8
8
  import timeGridPlugin from '@fullcalendar/timegrid';
@@ -10,8 +10,8 @@ import resourceTimeGridPlugin from '@fullcalendar/resource-timegrid';
10
10
  import interactionPlugin from '@fullcalendar/interaction';
11
11
  import listPlugin from '@fullcalendar/list';
12
12
  import ptBrLocale from '@fullcalendar/core/locales/pt-br';
13
- import { CheckCircle, Stethoscope, RotateCcw, Check, CircleDashed, Layers, Loader2, AlertCircle, RefreshCw, CalendarPlus, X, CheckCircle2, Ban, Clock3, Plus, Settings, ChevronLeft, ChevronRight, ChevronDown, CircleDollarSign, LogIn, CalendarDays, Play, UserRound, Package, Clock, Mic, CalendarX, UserX, Search, CalendarOff, Pencil, Trash2, User, MapPin, FileText, AlertTriangle, DollarSign, HandCoins, CheckSquare, Calendar, History, ExternalLink, Users, DoorOpen, Briefcase, CircleDot, MessageCircle, Phone, AlarmClock, ArrowLeft, Link2, Receipt, ClipboardCheck, FileCheck, ArrowRightLeft, CreditCard, QrCode, Banknote, Sparkles } from 'lucide-react';
14
- import { toast, FilterRail, AdaptiveFilter, FilterCheckRow, TooltipProvider, Button, Checkbox, Badge, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, PersonAvatar, abbreviatePersonName, INLINE_FILTER_MAX, PersonMultiPicker, SegmentedControl, Switch, MiniCalendar, PageHeaderActions, ConfirmDialog, RailWorkspace, Tooltip, TooltipTrigger, TooltipContent, SubpageHeader, ModalTask, Skeleton, Modal, ModalContent, ModalHeader, ModalTitle, ModalBody, PersonPicker, Input, Tabs, TabsList, TabsTrigger, DatePicker, TimePicker, ModalFooter, ModalTaskHost, SearchCombobox, ModalTaskHeader, Dropdown, DropdownTrigger, DropdownContent, DropdownItem, cn, normalizePhone, DotBadge } from '@fayz-ai/ui';
13
+ import { CheckCircle, Stethoscope, RotateCcw, Check, CircleDashed, Layers, Users, Activity, RefreshCw, Clock3, CalendarPlus, Loader2, AlertCircle, X, CheckCircle2, Ban, Plus, Settings, ChevronLeft, ChevronRight, ChevronDown, CircleDollarSign, LogIn, CalendarDays, Play, UserRound, Package, Clock, Mic, MapPin, CalendarX, UserX, Search, CalendarOff, Pencil, Trash2, User, FileText, AlertTriangle, DollarSign, HandCoins, CheckSquare, Calendar, History, ExternalLink, DoorOpen, Briefcase, CircleDot, MessageCircle, Phone, AlarmClock, ArrowLeft, Link2, Receipt, ClipboardCheck, FileCheck, ArrowRightLeft, CreditCard, QrCode, Banknote, Sparkles } from 'lucide-react';
14
+ import { toast, FilterRail, AdaptiveFilter, FilterCheckRow, TooltipProvider, ModulePage, Card, CardContent, Button, Avatar, AvatarImage, AvatarFallback, Badge, Checkbox, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, PersonAvatar, abbreviatePersonName, INLINE_FILTER_MAX, PersonMultiPicker, SegmentedControl, Switch, MiniCalendar, PageHeaderActions, ConfirmDialog, RailWorkspace, Tooltip, TooltipTrigger, TooltipContent, SubpageHeader, ModalTask, Skeleton, Modal, ModalContent, ModalHeader, ModalTitle, ModalBody, PersonPicker, Input, Tabs, TabsList, TabsTrigger, DatePicker, TimePicker, ModalFooter, ModalTaskHost, SearchCombobox, ModalTaskHeader, Dropdown, DropdownTrigger, DropdownContent, DropdownItem, cn, normalizePhone, DotBadge } from '@fayz-ai/ui';
15
15
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
16
16
  import { createPortal } from 'react-dom';
17
17
  import { FormFillDialog, PersonFormsAlert } from '@fayz-ai/plugin-forms';
@@ -761,20 +761,20 @@ function BlockDialog({
761
761
  }) {
762
762
  const professionals = useAgendaStore((s) => s.agendaProfessionals);
763
763
  const provider2 = useAgendaProvider();
764
- const [assigneeId, setAssigneeId] = React20.useState(prefillProfessional ?? "");
765
- const [title, setTitle] = React20.useState("");
766
- const [description, setDescription] = React20.useState("");
767
- const [mode, setMode] = React20.useState("single");
768
- const [specificDate, setSpecificDate] = React20.useState(date ?? hoje());
769
- const [allDay, setAllDay] = React20.useState(false);
770
- const [startsAt, setStartsAt] = React20.useState("12:00");
771
- const [endsAt, setEndsAt] = React20.useState("13:00");
772
- const [days2, setDays] = React20.useState([]);
773
- const [validFrom, setValidFrom] = React20.useState(date ?? hoje());
774
- const [validTo, setValidTo] = React20.useState("");
775
- const [weekInterval, setWeekInterval] = React20.useState("0");
776
- const [saving, setSaving] = React20.useState(false);
777
- React20.useEffect(() => {
764
+ const [assigneeId, setAssigneeId] = React21.useState(prefillProfessional ?? "");
765
+ const [title, setTitle] = React21.useState("");
766
+ const [description, setDescription] = React21.useState("");
767
+ const [mode, setMode] = React21.useState("single");
768
+ const [specificDate, setSpecificDate] = React21.useState(date ?? hoje());
769
+ const [allDay, setAllDay] = React21.useState(false);
770
+ const [startsAt, setStartsAt] = React21.useState("12:00");
771
+ const [endsAt, setEndsAt] = React21.useState("13:00");
772
+ const [days2, setDays] = React21.useState([]);
773
+ const [validFrom, setValidFrom] = React21.useState(date ?? hoje());
774
+ const [validTo, setValidTo] = React21.useState("");
775
+ const [weekInterval, setWeekInterval] = React21.useState("0");
776
+ const [saving, setSaving] = React21.useState(false);
777
+ React21.useEffect(() => {
778
778
  if (!open) return;
779
779
  if (editing) {
780
780
  setAssigneeId(editing.assigneeId);
@@ -964,10 +964,10 @@ function BlockDialog({
964
964
  }
965
965
  function LostSlotsCard({ range, selectedStatuses, onPick }) {
966
966
  const provider2 = useAgendaProvider();
967
- const [counts, setCounts] = React20.useState(null);
968
- const [falhou, setFalhou] = React20.useState(false);
967
+ const [counts, setCounts] = React21.useState(null);
968
+ const [falhou, setFalhou] = React21.useState(false);
969
969
  const key = range ? `${range.start}|${range.end}` : null;
970
- React20.useEffect(() => {
970
+ React21.useEffect(() => {
971
971
  if (!range || !provider2.getLostCounts) {
972
972
  setCounts(null);
973
973
  return;
@@ -1024,14 +1024,14 @@ function ScheduleGridDialog({ open, onClose, professionalId }) {
1024
1024
  const t2 = useTranslation();
1025
1025
  const professionals = useAgendaStore((s) => s.professionals);
1026
1026
  const invalidateSchedules2 = useAgendaStore((s) => s.invalidateSchedules);
1027
- const [search, setSearch] = React20.useState("");
1028
- const [selectedId, setSelectedId] = React20.useState(null);
1029
- React20.useEffect(() => {
1027
+ const [search, setSearch] = React21.useState("");
1028
+ const [selectedId, setSelectedId] = React21.useState(null);
1029
+ React21.useEffect(() => {
1030
1030
  if (!open) return;
1031
1031
  setSelectedId((current) => professionalId ?? current ?? professionals[0]?.id ?? null);
1032
1032
  }, [open, professionalId, professionals]);
1033
- const selectedRef = React20.useRef(null);
1034
- React20.useEffect(() => {
1033
+ const selectedRef = React21.useRef(null);
1034
+ React21.useEffect(() => {
1035
1035
  if (!open || !selectedId) return;
1036
1036
  const id = window.setTimeout(() => {
1037
1037
  selectedRef.current?.scrollIntoView({ block: "nearest" });
@@ -1107,6 +1107,123 @@ function shouldClearProfessional(people, locationId, selectedId) {
1107
1107
  if (!escolhido || atendeEm(escolhido, locationId)) return null;
1108
1108
  return escolhido;
1109
1109
  }
1110
+
1111
+ // src/lib/recurrence.ts
1112
+ var RECURRENCE_MAX_OCCURRENCES = 60;
1113
+ var RECURRENCE_HORIZON_DAYS = 730;
1114
+ function parseDay(key) {
1115
+ const [y, m, d] = key.slice(0, 10).split("-").map(Number);
1116
+ return new Date(Date.UTC(y, (m ?? 1) - 1, d ?? 1));
1117
+ }
1118
+ function formatDay(date) {
1119
+ const y = date.getUTCFullYear();
1120
+ const m = String(date.getUTCMonth() + 1).padStart(2, "0");
1121
+ const d = String(date.getUTCDate()).padStart(2, "0");
1122
+ return `${y}-${m}-${d}`;
1123
+ }
1124
+ function addDays(date, days2) {
1125
+ return new Date(date.getTime() + days2 * 864e5);
1126
+ }
1127
+ function weekdayOrdinalInMonth(key) {
1128
+ return Math.floor((parseDay(key).getUTCDate() - 1) / 7) + 1;
1129
+ }
1130
+ function nthWeekdayOfMonth(year, month, weekday, nth) {
1131
+ const first = new Date(Date.UTC(year, month, 1));
1132
+ const offset = (weekday - first.getUTCDay() + 7) % 7;
1133
+ const candidate = new Date(Date.UTC(year, month, 1 + offset + (nth - 1) * 7));
1134
+ if (candidate.getUTCMonth() !== (month % 12 + 12) % 12) return null;
1135
+ return candidate;
1136
+ }
1137
+ function ruleForPreset(preset) {
1138
+ switch (preset) {
1139
+ case "daily":
1140
+ return { freq: "daily", interval: 1 };
1141
+ case "weekly":
1142
+ return { freq: "weekly", interval: 1 };
1143
+ case "biweekly":
1144
+ return { freq: "weekly", interval: 2 };
1145
+ case "monthly":
1146
+ return { freq: "monthly_weekday", interval: 1 };
1147
+ default:
1148
+ return null;
1149
+ }
1150
+ }
1151
+ function presetForRule(rule) {
1152
+ if (!rule) return "none";
1153
+ if (rule.freq === "daily" && rule.interval === 1) return "daily";
1154
+ if (rule.freq === "weekly" && rule.interval === 1) return "weekly";
1155
+ if (rule.freq === "weekly" && rule.interval === 2) return "biweekly";
1156
+ if (rule.freq === "monthly_weekday" && rule.interval === 1) return "monthly";
1157
+ return "none";
1158
+ }
1159
+ function expandRecurrence(startDate, rule) {
1160
+ const interval = Math.max(1, Math.floor(rule.interval || 1));
1161
+ const hardCount = rule.count != null ? Math.max(1, Math.floor(rule.count)) : null;
1162
+ const until = rule.until || null;
1163
+ const maxCount = Math.min(hardCount ?? RECURRENCE_MAX_OCCURRENCES, RECURRENCE_MAX_OCCURRENCES);
1164
+ const horizon = formatDay(addDays(parseDay(startDate), RECURRENCE_HORIZON_DAYS));
1165
+ const dates = [];
1166
+ let reachedEndCondition = false;
1167
+ const push = (key) => {
1168
+ if (until && key > until) {
1169
+ reachedEndCondition = true;
1170
+ return false;
1171
+ }
1172
+ if (key > horizon) return false;
1173
+ dates.push(key);
1174
+ if (hardCount != null && dates.length >= hardCount) {
1175
+ reachedEndCondition = true;
1176
+ return false;
1177
+ }
1178
+ return dates.length < maxCount;
1179
+ };
1180
+ dates.push(startDate);
1181
+ if (hardCount === 1) return { dates, truncated: false };
1182
+ if (until && startDate >= until) return { dates, truncated: false };
1183
+ if (rule.freq === "monthly_weekday") {
1184
+ const start = parseDay(startDate);
1185
+ const weekday = start.getUTCDay();
1186
+ const nth = weekdayOrdinalInMonth(startDate);
1187
+ let step = 1;
1188
+ while (true) {
1189
+ const target = new Date(Date.UTC(start.getUTCFullYear(), start.getUTCMonth() + step * interval, 1));
1190
+ const day = nthWeekdayOfMonth(target.getUTCFullYear(), target.getUTCMonth(), weekday, nth);
1191
+ step += 1;
1192
+ if (!day) {
1193
+ if (step > RECURRENCE_MAX_OCCURRENCES * 2) return { dates, truncated: true };
1194
+ const probe = formatDay(target);
1195
+ if (until && probe > until) return { dates, truncated: false };
1196
+ if (probe > horizon) return { dates, truncated: !reachedEndCondition };
1197
+ continue;
1198
+ }
1199
+ if (!push(formatDay(day))) return { dates, truncated: !reachedEndCondition };
1200
+ }
1201
+ }
1202
+ const stepDays = rule.freq === "daily" ? interval : interval * 7;
1203
+ let cursor = parseDay(startDate);
1204
+ while (true) {
1205
+ cursor = addDays(cursor, stepDays);
1206
+ if (!push(formatDay(cursor))) return { dates, truncated: !reachedEndCondition };
1207
+ }
1208
+ }
1209
+ function resolveSeriesTarget(booking, scope) {
1210
+ if (scope === "series" && booking.seriesId) {
1211
+ return { kind: "series", seriesId: booking.seriesId, bookingId: booking.id };
1212
+ }
1213
+ return { kind: "single", bookingId: booking.id };
1214
+ }
1215
+ function planSeriesEdit(args) {
1216
+ const target = resolveSeriesTarget(args.booking, args.scope);
1217
+ if (target.kind === "single") {
1218
+ return { single: args.patch, series: null, dateStaysLocal: false };
1219
+ }
1220
+ const { startsAt, endsAt, ...shared } = args.patch;
1221
+ return {
1222
+ single: args.dateChanged ? { startsAt, endsAt } : null,
1223
+ series: { seriesId: target.seriesId, patch: shared, timeOfDay: args.timeOfDay },
1224
+ dateStaysLocal: args.dateChanged
1225
+ };
1226
+ }
1110
1227
  var TYPE_ICONS = {
1111
1228
  cash: Banknote,
1112
1229
  pix: QrCode,
@@ -1578,9 +1695,9 @@ function ClientHistoryPanel({ clientId, currentBookingId, formatCurrency: format
1578
1695
  const t2 = useTranslation();
1579
1696
  const config = useAgendaConfig();
1580
1697
  const provider2 = useAgendaProvider();
1581
- const [bookings, setBookings] = React20.useState(null);
1582
- const [failed, setFailed] = React20.useState(false);
1583
- React20.useEffect(() => {
1698
+ const [bookings, setBookings] = React21.useState(null);
1699
+ const [failed, setFailed] = React21.useState(false);
1700
+ React21.useEffect(() => {
1584
1701
  let alive = true;
1585
1702
  if (!provider2.getClientHistory) {
1586
1703
  setBookings([]);
@@ -1632,13 +1749,13 @@ function ClientHistoryPanel({ clientId, currentBookingId, formatCurrency: format
1632
1749
  }
1633
1750
  function BookingInterestsPanel({ clientId, bookingId, value, onChange }) {
1634
1751
  const provider2 = useAgendaProvider();
1635
- const [crmInterests, setCrmInterests] = React20.useState([]);
1636
- const [term, setTerm] = React20.useState("");
1637
- const [options, setOptions] = React20.useState([]);
1638
- const initializedFor = React20.useRef(null);
1639
- const onChangeRef = React20.useRef(onChange);
1752
+ const [crmInterests, setCrmInterests] = React21.useState([]);
1753
+ const [term, setTerm] = React21.useState("");
1754
+ const [options, setOptions] = React21.useState([]);
1755
+ const initializedFor = React21.useRef(null);
1756
+ const onChangeRef = React21.useRef(onChange);
1640
1757
  onChangeRef.current = onChange;
1641
- React20.useEffect(() => {
1758
+ React21.useEffect(() => {
1642
1759
  if (!clientId) {
1643
1760
  setCrmInterests([]);
1644
1761
  initializedFor.current = null;
@@ -1671,7 +1788,7 @@ function BookingInterestsPanel({ clientId, bookingId, value, onChange }) {
1671
1788
  vivo = false;
1672
1789
  };
1673
1790
  }, [clientId, bookingId, provider2]);
1674
- React20.useEffect(() => {
1791
+ React21.useEffect(() => {
1675
1792
  if (!term.trim()) {
1676
1793
  setOptions([]);
1677
1794
  return;
@@ -1774,6 +1891,9 @@ function formatCompactDate(dateStr, locale) {
1774
1891
  day: "numeric"
1775
1892
  });
1776
1893
  }
1894
+ var ROW = "-mx-2 flex cursor-pointer gap-3 rounded-lg px-2 transition-colors hover:bg-muted/40 [&_input]:cursor-text [&_textarea]:cursor-text";
1895
+ var ROW_CENTER = `${ROW} items-center`;
1896
+ var ROW_TOP = `${ROW} items-start`;
1777
1897
  function InlineNumber({ value, onChange, min = 0, step = 1, className, disabled, tooltip }) {
1778
1898
  const [draft, setDraft] = useState(String(value));
1779
1899
  const [focused, setFocused] = useState(false);
@@ -1972,6 +2092,9 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
1972
2092
  const guardBookings = useLimitGuard("bookings_month");
1973
2093
  const updateBooking = useAgendaStore((s) => s.updateBooking);
1974
2094
  const deleteBooking = useAgendaStore((s) => s.deleteBooking);
2095
+ const createBookingSeries = useAgendaStore((s) => s.createBookingSeries);
2096
+ const updateBookingSeries = useAgendaStore((s) => s.updateBookingSeries);
2097
+ const deleteBookingSeries = useAgendaStore((s) => s.deleteBookingSeries);
1975
2098
  const updateBookingStatus = useAgendaStore((s) => s.updateBookingStatus);
1976
2099
  const fetchBookings = useAgendaStore((s) => s.fetchBookings);
1977
2100
  const agendaDateRange = useAgendaStore((s) => s.dateRange);
@@ -2006,16 +2129,27 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
2006
2129
  return "09:00";
2007
2130
  });
2008
2131
  const [status, setStatus] = useState("scheduled");
2132
+ const [recurrencePreset, setRecurrencePreset] = useState("none");
2133
+ const [recurrenceEndMode, setRecurrenceEndMode] = useState("count");
2134
+ const [recurrenceUntil, setRecurrenceUntil] = useState("");
2135
+ const [recurrenceCount, setRecurrenceCount] = useState(4);
2136
+ const [seriesId, setSeriesId] = useState(null);
2137
+ const [seriesSize, setSeriesSize] = useState(0);
2138
+ const [seriesConflicts, setSeriesConflicts] = useState(null);
2139
+ const [checkingSeries, setCheckingSeries] = useState(false);
2140
+ const [scopeAsk, setScopeAsk] = useState(null);
2141
+ const [scopeChoice, setScopeChoice] = useState("this");
2142
+ const initialDate = useRef("");
2009
2143
  const [markingWaiting, setMarkingWaiting] = useState(false);
2010
2144
  const [preenchendoForm, setPreenchendoForm] = useState(null);
2011
2145
  const [formsVersao, setFormsVersao] = useState(0);
2012
- const professionalOptions = React20.useMemo(() => {
2146
+ const professionalOptions = React21.useMemo(() => {
2013
2147
  const doLocal = professionalsForLocation(professionals, locationId, professionalId);
2014
2148
  if (!professionalId || doLocal.some((p) => p.id === professionalId)) return doLocal;
2015
2149
  const nome = loadedBookings.find((b) => b.id === bookingId)?.professionalName;
2016
2150
  return [...doLocal, { id: professionalId, name: nome ?? "\u2014", avatarUrl: null }];
2017
2151
  }, [professionals, professionalId, locationId, loadedBookings, bookingId]);
2018
- React20.useEffect(() => {
2152
+ React21.useEffect(() => {
2019
2153
  const fora = shouldClearProfessional(professionals, locationId, professionalId);
2020
2154
  if (!fora) return;
2021
2155
  setProfessionalId("");
@@ -2097,6 +2231,17 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
2097
2231
  setCancellationReasonId("");
2098
2232
  setCancellationNotes("");
2099
2233
  setCancellationReasonsLoading(false);
2234
+ setRecurrencePreset("none");
2235
+ setRecurrenceEndMode("count");
2236
+ setRecurrenceUntil("");
2237
+ setRecurrenceCount(4);
2238
+ setSeriesId(null);
2239
+ setSeriesSize(0);
2240
+ setSeriesConflicts(null);
2241
+ setCheckingSeries(false);
2242
+ setScopeAsk(null);
2243
+ setScopeChoice("this");
2244
+ initialDate.current = prefill?.startsAt ? prefill.startsAt.slice(0, 10) : todayLocal();
2100
2245
  setModalTab(initialTab || "appointment");
2101
2246
  if (!prefill?.professionalId && professionals.length === 1) setProfessionalId(professionals[0].id);
2102
2247
  setDate(() => {
@@ -2132,8 +2277,18 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
2132
2277
  setLocationId(b.locationId ?? "");
2133
2278
  setResourceId(b.resourceId ?? "");
2134
2279
  setDate(b.startsAt.slice(0, 10));
2280
+ initialDate.current = b.startsAt.slice(0, 10);
2135
2281
  const d = new Date(b.startsAt);
2136
2282
  setStartTime(`${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`);
2283
+ const serie = b.seriesId ?? null;
2284
+ setSeriesId(serie);
2285
+ if (serie) {
2286
+ Promise.resolve(provider2.countSeriesOccurrences?.(serie)).then((n) => setSeriesSize(n ?? 0)).catch(() => setSeriesSize(0));
2287
+ provider2.getRecurrence?.(serie).then((regra) => {
2288
+ if (regra) setRecurrencePreset(presetForRule(regra));
2289
+ }).catch(() => {
2290
+ });
2291
+ }
2137
2292
  setStatus(b.status);
2138
2293
  setNotes(b.notes ?? "");
2139
2294
  if (b.notes) setShowNotes(true);
@@ -2259,7 +2414,48 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
2259
2414
  }, 300);
2260
2415
  return () => clearTimeout(timer);
2261
2416
  }, [professionalId, janela, bookingId, totalDuration]);
2262
- const [feriado, setFeriado] = React20.useState(null);
2417
+ const recurrenceRule = useMemo(() => {
2418
+ const base = ruleForPreset(recurrencePreset);
2419
+ if (!base) return null;
2420
+ if (recurrenceEndMode === "until") {
2421
+ if (!recurrenceUntil) return null;
2422
+ return { ...base, until: recurrenceUntil, count: null };
2423
+ }
2424
+ return { ...base, until: null, count: Math.max(1, Math.min(RECURRENCE_MAX_OCCURRENCES, recurrenceCount)) };
2425
+ }, [recurrencePreset, recurrenceEndMode, recurrenceUntil, recurrenceCount]);
2426
+ const expansion = useMemo(
2427
+ () => recurrenceRule ? expandRecurrence(date, recurrenceRule) : null,
2428
+ [date, recurrenceRule]
2429
+ );
2430
+ const seriesDates = expansion && expansion.dates.length > 1 ? expansion.dates : null;
2431
+ useEffect(() => {
2432
+ if (!open || !seriesDates || !professionalId || totalDuration === 0 || !provider2.checkSeriesConflicts) {
2433
+ setSeriesConflicts(null);
2434
+ setCheckingSeries(false);
2435
+ return;
2436
+ }
2437
+ setCheckingSeries(true);
2438
+ const timer = setTimeout(() => {
2439
+ const windows = seriesDates.map((dia) => {
2440
+ const inicio = /* @__PURE__ */ new Date(`${dia}T${startTime}:00`);
2441
+ return {
2442
+ startsAt: inicio.toISOString(),
2443
+ endsAt: new Date(inicio.getTime() + totalDuration * 6e4).toISOString()
2444
+ };
2445
+ });
2446
+ provider2.checkSeriesConflicts({
2447
+ assigneeId: professionalId,
2448
+ windows,
2449
+ excludeBookingId: bookingId,
2450
+ unitId: activeUnitId
2451
+ }).then(setSeriesConflicts).catch(() => setSeriesConflicts(null)).finally(() => setCheckingSeries(false));
2452
+ }, 400);
2453
+ return () => {
2454
+ clearTimeout(timer);
2455
+ setCheckingSeries(false);
2456
+ };
2457
+ }, [open, seriesDates, professionalId, startTime, totalDuration, bookingId, activeUnitId, provider2]);
2458
+ const [feriado, setFeriado] = React21.useState(null);
2263
2459
  useEffect(() => {
2264
2460
  if (!open || !date || !provider2.getHolidays) {
2265
2461
  setFeriado(null);
@@ -2308,7 +2504,8 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
2308
2504
  },
2309
2505
  { variant: "inline" }
2310
2506
  );
2311
- const agendaPrefs = useTenantPluginSettings("agenda", { interestsOnBooking: true });
2507
+ const agendaPrefs = useTenantPluginSettings("agenda", { interestsOnBooking: true, recurringBookings: true });
2508
+ const recurrenceEnabled = agendaPrefs.get("recurringBookings");
2312
2509
  useEffect(() => {
2313
2510
  const ids = services.map((s) => s.serviceId).filter(Boolean);
2314
2511
  if (ids.length === 0 || !provider2.getServiceKinds) {
@@ -2335,43 +2532,54 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
2335
2532
  toast.error("Interesses n\xE3o vinculados a este agendamento");
2336
2533
  }
2337
2534
  }
2535
+ function bookingPayload2() {
2536
+ return {
2537
+ clientId,
2538
+ professionalId,
2539
+ locationId: locationId || void 0,
2540
+ resourceId: resourceId || null,
2541
+ notes: notes || void 0,
2542
+ title: activeType.fields.title ? title.trim() : void 0,
2543
+ services: services.map((s) => ({ serviceId: s.serviceId, name: s.name, durationMinutes: s.durationMinutes, price: s.price })),
2544
+ attendees: activeType.fields.attendees ? attendees.map((a) => ({ personId: a.personId })) : void 0
2545
+ };
2546
+ }
2338
2547
  async function handleSubmit(e) {
2339
2548
  e.preventDefault();
2340
2549
  if (!canSubmit) return;
2550
+ if (mode === "edit" && seriesId && seriesSize > 1) {
2551
+ setScopeAsk("save");
2552
+ return;
2553
+ }
2554
+ await commitSave("this");
2555
+ }
2556
+ async function commitSave(scope) {
2557
+ setScopeAsk(null);
2341
2558
  if (mode === "create" && await guardBookings() === "blocked") return;
2342
2559
  setSaving(true);
2343
2560
  try {
2344
2561
  const startsAt = (/* @__PURE__ */ new Date(`${date}T${startTime}:00`)).toISOString();
2345
2562
  if (mode === "create") {
2346
- const newBooking = await createBooking({
2347
- kind: bookingType,
2348
- clientId,
2349
- professionalId,
2350
- locationId: locationId || void 0,
2351
- resourceId: resourceId || null,
2352
- startsAt,
2353
- notes: notes || void 0,
2354
- title: activeType.fields.title ? title.trim() : void 0,
2355
- services: services.map((s) => ({ serviceId: s.serviceId, name: s.name, durationMinutes: s.durationMinutes, price: s.price })),
2356
- attendees: activeType.fields.attendees ? attendees.map((a) => ({ personId: a.personId })) : void 0
2357
- });
2563
+ const input = { kind: bookingType, startsAt, ...bookingPayload2() };
2564
+ const firstId = seriesDates && recurrenceRule ? (await createBookingSeries(input, recurrenceRule, seriesDates)).bookingIds[0] : (await createBooking(input)).id;
2358
2565
  invalidateLimit("bookings_month");
2359
- await persistInterests(newBooking.id);
2360
- onClose();
2361
- onCreated?.(newBooking.id, externalSource);
2566
+ if (firstId) {
2567
+ await persistInterests(firstId);
2568
+ onClose();
2569
+ onCreated?.(firstId, externalSource);
2570
+ } else {
2571
+ onClose();
2572
+ }
2362
2573
  } else if (bookingId) {
2363
- await updateBooking(bookingId, {
2364
- clientId,
2365
- professionalId,
2366
- locationId: locationId || void 0,
2367
- resourceId: resourceId || null,
2368
- startsAt,
2369
- notes: notes || void 0,
2370
- status,
2371
- title: activeType.fields.title ? title.trim() : void 0,
2372
- services: services.map((s) => ({ serviceId: s.serviceId, name: s.name, durationMinutes: s.durationMinutes, price: s.price })),
2373
- attendees: activeType.fields.attendees ? attendees.map((a) => ({ personId: a.personId })) : void 0
2574
+ const plano = planSeriesEdit({
2575
+ booking: { id: bookingId, seriesId },
2576
+ scope,
2577
+ patch: { startsAt, status, ...bookingPayload2() },
2578
+ timeOfDay: startTime,
2579
+ dateChanged: date !== initialDate.current
2374
2580
  });
2581
+ if (plano.series) await updateBookingSeries(plano.series.seriesId, plano.series.patch, plano.series.timeOfDay);
2582
+ if (plano.single) await updateBooking(bookingId, plano.single);
2375
2583
  await persistInterests(bookingId);
2376
2584
  if (status === "cancelled" && config.cancellationDetails && cancellationReasonId) {
2377
2585
  await config.cancellationDetails.saveCancellationDetails({
@@ -2381,6 +2589,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
2381
2589
  cancelledAt: (/* @__PURE__ */ new Date()).toISOString()
2382
2590
  });
2383
2591
  }
2592
+ if (plano.dateStaysLocal) toast.info(t2("agenda.recurrence.dateStaysLocal").replace("{{time}}", startTime));
2384
2593
  onClose();
2385
2594
  }
2386
2595
  } catch {
@@ -2389,9 +2598,19 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
2389
2598
  }
2390
2599
  async function handleDelete() {
2391
2600
  if (!bookingId) return;
2601
+ if (seriesId && seriesSize > 1) {
2602
+ setScopeAsk("delete");
2603
+ return;
2604
+ }
2605
+ await commitDelete("this");
2606
+ }
2607
+ async function commitDelete(scope) {
2608
+ if (!bookingId) return;
2609
+ setScopeAsk(null);
2392
2610
  setDeleting(true);
2393
2611
  try {
2394
- await deleteBooking(bookingId);
2612
+ if (scope === "series" && seriesId) await deleteBookingSeries(seriesId);
2613
+ else await deleteBooking(bookingId);
2395
2614
  onClose();
2396
2615
  } catch {
2397
2616
  setConfirmingDelete(false);
@@ -2426,633 +2645,724 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
2426
2645
  // barra de onde foi aberto e sai quase-branco sobre cartão branco.
2427
2646
  /* @__PURE__ */ jsx(Modal, { open, onOpenChange: (o) => {
2428
2647
  if (!o) onClose();
2429
- }, children: /* @__PURE__ */ jsx(ModalContent, { size: "3xl", noPadding: true, className: "gap-0", hideClose: true, children: /* @__PURE__ */ jsx(ModalBody, { className: "sm:max-h-[75vh] overflow-hidden p-0", children: /* @__PURE__ */ jsxs(
2430
- ModalTaskHost,
2431
- {
2432
- className: "flex min-h-0 flex-1 flex-col overflow-hidden",
2433
- header: (stack) => /* @__PURE__ */ jsx(ModalTaskHeader, { title: modalTitle, stack, onClose }),
2434
- children: [
2435
- preenchendoForm && clientId && /* @__PURE__ */ jsx(
2436
- ModalTask,
2437
- {
2438
- open: true,
2439
- title: "Formul\xE1rio do cliente",
2440
- hint: clientSearch || void 0,
2441
- onClose: () => setPreenchendoForm(null),
2442
- children: /* @__PURE__ */ jsx(
2443
- FormFillDialog,
2444
- {
2445
- frame: "inline",
2446
- templateId: preenchendoForm,
2447
- personId: clientId,
2448
- personName: clientSearch || void 0,
2449
- onSaved: () => setFormsVersao((n) => n + 1),
2450
- onClose: () => setPreenchendoForm(null)
2451
- }
2452
- )
2453
- }
2454
- ),
2455
- /* @__PURE__ */ jsxs(Fragment, { children: [
2456
- /* @__PURE__ */ jsxs("div", { className: "px-5 pt-3 pb-3 shrink-0 space-y-3", children: [
2457
- showTabs && mode === "create" ? /* @__PURE__ */ jsx("div", { className: "flex gap-1 border-b fayz-glass-divider", children: config.bookingTypes.map((bt) => {
2458
- const label = rotuloDoTipo(t2, bt.id, bt.label);
2459
- const isActive = bookingType === bt.id;
2460
- const BtIcon = BOOKING_TYPE_ICONS[bt.icon] ?? Calendar;
2461
- return /* @__PURE__ */ jsxs(
2462
- "button",
2463
- {
2464
- type: "button",
2465
- onClick: () => setBookingType(bt.id),
2466
- className: `relative flex items-center gap-1.5 pb-2 px-2 text-xs font-medium transition-colors ${isActive ? "text-foreground" : "text-muted-foreground hover:text-foreground"}`,
2467
- children: [
2468
- /* @__PURE__ */ jsx(BtIcon, { className: "h-3.5 w-3.5" }),
2469
- label,
2470
- isActive && /* @__PURE__ */ jsx("span", { className: "absolute bottom-0 left-0 right-0 h-0.5 rounded-full bg-foreground" })
2471
- ]
2472
- },
2473
- bt.id
2474
- );
2475
- }) }) : mode === "edit" && (config.clientEntityDef && clientId || clientId || config.modules.financial && editOrderId ? /* @__PURE__ */ jsx("div", { className: "flex gap-1 border-b fayz-glass-divider", children: [
2476
- { id: "appointment", label: rotuloDoTipo(t2, bookingType, activeType.label), icon: Calendar, show: true },
2477
- { id: "client", label: t2("agenda.appointment.clientTab"), icon: User, show: !!config.clientEntityDef && !!clientId },
2478
- // Entre a ficha e o dinheiro, como no V1: "quem é" → "o que
2479
- // já fez" → "o que deve". Só com o provider sabendo contar.
2480
- { id: "history", label: t2("agenda.history.tab"), icon: History, show: !!clientId && !!provider2.getClientHistory },
2481
- // "Financeiro", como no V1 — e é a CONTA DO CLIENTE inteira,
2482
- // não só a fatura deste agendamento; por isso basta o cliente.
2483
- { id: "financial", label: t2("agenda.appointment.financialTab"), icon: DollarSign, show: config.modules.financial && (!!clientId || !!editOrderId) }
2484
- ].filter((tab) => tab.show).map((tab) => {
2485
- const isActive = modalTab === tab.id;
2486
- return /* @__PURE__ */ jsxs(
2487
- "button",
2648
+ }, children: /* @__PURE__ */ jsx(ModalContent, { size: "3xl", noPadding: true, className: "gap-0", hideClose: true, children: /* @__PURE__ */ jsxs(ModalBody, { className: "sm:max-h-[75vh] overflow-hidden p-0", children: [
2649
+ /* @__PURE__ */ jsxs(
2650
+ ModalTaskHost,
2651
+ {
2652
+ className: "flex min-h-0 flex-1 flex-col overflow-hidden",
2653
+ header: (stack) => /* @__PURE__ */ jsx(ModalTaskHeader, { title: modalTitle, stack, onClose }),
2654
+ children: [
2655
+ preenchendoForm && clientId && /* @__PURE__ */ jsx(
2656
+ ModalTask,
2657
+ {
2658
+ open: true,
2659
+ title: "Formul\xE1rio do cliente",
2660
+ hint: clientSearch || void 0,
2661
+ onClose: () => setPreenchendoForm(null),
2662
+ children: /* @__PURE__ */ jsx(
2663
+ FormFillDialog,
2488
2664
  {
2489
- type: "button",
2490
- onClick: () => setModalTab(tab.id),
2491
- className: `relative flex items-center gap-1.5 pb-2 px-2 text-xs font-medium transition-colors ${isActive ? "text-foreground" : "text-muted-foreground hover:text-foreground"}`,
2492
- children: [
2493
- /* @__PURE__ */ jsx(tab.icon, { className: "h-3.5 w-3.5" }),
2494
- tab.label,
2495
- tab.id === "financial" && !paymentStatusLoading && (() => {
2496
- const negativo = status === "cancelled" || status === "no_show";
2497
- const hoje3 = date === todayLocal();
2498
- if (editPaymentStatus === "paid") {
2499
- return /* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5 text-success", "aria-label": "Quitado" });
2500
- }
2501
- if (editPaymentStatus !== "none" && editPaymentStatus !== "cancelled") {
2502
- return hoje3 && !negativo ? /* @__PURE__ */ jsx(AlertTriangle, { className: "h-3.5 w-3.5 text-destructive", "aria-label": "Pendente hoje" }) : /* @__PURE__ */ jsx(Clock, { className: "h-3.5 w-3.5 text-warning", "aria-label": "Em aberto" });
2503
- }
2504
- if (editPaymentStatus === "none" && hoje3 && !negativo && totalPrice > 0) {
2505
- return /* @__PURE__ */ jsx(AlertTriangle, { className: "h-3.5 w-3.5 text-destructive", "aria-label": "Sem cobran\xE7a levantada" });
2506
- }
2507
- return null;
2508
- })(),
2509
- isActive && /* @__PURE__ */ jsx("span", { className: "absolute bottom-0 left-0 right-0 h-0.5 rounded-full bg-foreground" })
2510
- ]
2511
- },
2512
- tab.id
2513
- );
2514
- }) }) : null),
2515
- modalTab === "appointment" && /* @__PURE__ */ jsxs(Fragment, { children: [
2516
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
2517
- /* @__PURE__ */ jsx(Clock, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
2518
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-1.5 gap-y-1 text-sm flex-1 min-w-0", children: [
2519
- editingDate ? /* @__PURE__ */ jsx(
2520
- "input",
2521
- {
2522
- type: "date",
2523
- value: date,
2524
- autoFocus: true,
2525
- onChange: (e) => {
2526
- setDate(e.target.value);
2527
- setEditingDate(false);
2528
- },
2529
- onBlur: () => setEditingDate(false),
2530
- className: "bg-transparent text-sm font-medium focus:outline-none border-b border-foreground w-auto shrink-0"
2531
- }
2532
- ) : /* @__PURE__ */ jsx(
2533
- "button",
2534
- {
2535
- type: "button",
2536
- onClick: () => setEditingDate(true),
2537
- className: "px-0.5 py-0.5 font-medium border-b border-transparent hover:border-foreground transition-colors shrink-0",
2538
- children: formatCompactDate(date, locale)
2539
- }
2540
- ),
2541
- editingTime ? /* @__PURE__ */ jsx(
2542
- "input",
2543
- {
2544
- type: "time",
2545
- value: startTime,
2546
- autoFocus: true,
2547
- onChange: (e) => {
2548
- if (e.target.value) {
2549
- setStartTime(e.target.value);
2550
- setEditingTime(false);
2665
+ frame: "inline",
2666
+ templateId: preenchendoForm,
2667
+ personId: clientId,
2668
+ personName: clientSearch || void 0,
2669
+ onSaved: () => setFormsVersao((n) => n + 1),
2670
+ onClose: () => setPreenchendoForm(null)
2671
+ }
2672
+ )
2673
+ }
2674
+ ),
2675
+ /* @__PURE__ */ jsxs(Fragment, { children: [
2676
+ /* @__PURE__ */ jsxs("div", { className: "px-5 pt-3 pb-3 shrink-0 space-y-3", children: [
2677
+ showTabs && mode === "create" ? /* @__PURE__ */ jsx("div", { className: "flex gap-1 border-b fayz-glass-divider", children: config.bookingTypes.map((bt) => {
2678
+ const label = rotuloDoTipo(t2, bt.id, bt.label);
2679
+ const isActive = bookingType === bt.id;
2680
+ const BtIcon = BOOKING_TYPE_ICONS[bt.icon] ?? Calendar;
2681
+ return /* @__PURE__ */ jsxs(
2682
+ "button",
2683
+ {
2684
+ type: "button",
2685
+ onClick: () => setBookingType(bt.id),
2686
+ className: `relative flex items-center gap-1.5 pb-2 px-2 text-xs font-medium transition-colors ${isActive ? "text-foreground" : "text-muted-foreground hover:text-foreground"}`,
2687
+ children: [
2688
+ /* @__PURE__ */ jsx(BtIcon, { className: "h-3.5 w-3.5" }),
2689
+ label,
2690
+ isActive && /* @__PURE__ */ jsx("span", { className: "absolute bottom-0 left-0 right-0 h-0.5 rounded-full bg-foreground" })
2691
+ ]
2692
+ },
2693
+ bt.id
2694
+ );
2695
+ }) }) : mode === "edit" && (config.clientEntityDef && clientId || clientId || config.modules.financial && editOrderId ? /* @__PURE__ */ jsx("div", { className: "flex gap-1 border-b fayz-glass-divider", children: [
2696
+ { id: "appointment", label: rotuloDoTipo(t2, bookingType, activeType.label), icon: Calendar, show: true },
2697
+ { id: "client", label: t2("agenda.appointment.clientTab"), icon: User, show: !!config.clientEntityDef && !!clientId },
2698
+ // Entre a ficha e o dinheiro, como no V1: "quem é" → "o que
2699
+ // já fez" → "o que deve". Só com o provider sabendo contar.
2700
+ { id: "history", label: t2("agenda.history.tab"), icon: History, show: !!clientId && !!provider2.getClientHistory },
2701
+ // "Financeiro", como no V1 — e é a CONTA DO CLIENTE inteira,
2702
+ // não só a fatura deste agendamento; por isso basta o cliente.
2703
+ { id: "financial", label: t2("agenda.appointment.financialTab"), icon: DollarSign, show: config.modules.financial && (!!clientId || !!editOrderId) }
2704
+ ].filter((tab) => tab.show).map((tab) => {
2705
+ const isActive = modalTab === tab.id;
2706
+ return /* @__PURE__ */ jsxs(
2707
+ "button",
2708
+ {
2709
+ type: "button",
2710
+ onClick: () => setModalTab(tab.id),
2711
+ className: `relative flex items-center gap-1.5 pb-2 px-2 text-xs font-medium transition-colors ${isActive ? "text-foreground" : "text-muted-foreground hover:text-foreground"}`,
2712
+ children: [
2713
+ /* @__PURE__ */ jsx(tab.icon, { className: "h-3.5 w-3.5" }),
2714
+ tab.label,
2715
+ tab.id === "financial" && !paymentStatusLoading && (() => {
2716
+ const negativo = status === "cancelled" || status === "no_show";
2717
+ const hoje3 = date === todayLocal();
2718
+ if (editPaymentStatus === "paid") {
2719
+ return /* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5 text-success", "aria-label": "Quitado" });
2551
2720
  }
2552
- },
2553
- onBlur: () => setEditingTime(false),
2554
- className: "bg-transparent text-xs text-muted-foreground focus:outline-none border-b border-foreground w-auto shrink-0"
2555
- }
2556
- ) : /* @__PURE__ */ jsx(
2557
- "button",
2558
- {
2559
- type: "button",
2560
- onClick: () => setEditingTime(true),
2561
- className: "px-0.5 py-0.5 text-xs text-muted-foreground border-b border-transparent hover:border-foreground transition-colors shrink-0",
2562
- children: startTime
2563
- }
2564
- ),
2565
- /* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground/60", children: "\u2014" }),
2566
- /* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground/60", children: endTime }),
2567
- activeType.fields.professional && /* @__PURE__ */ jsx("div", { className: "ml-auto shrink-0", children: /* @__PURE__ */ jsx(ProfessionalSelect, { value: professionalId, onChange: setProfessionalId, professionals: professionalOptions }) })
2721
+ if (editPaymentStatus !== "none" && editPaymentStatus !== "cancelled") {
2722
+ return hoje3 && !negativo ? /* @__PURE__ */ jsx(AlertTriangle, { className: "h-3.5 w-3.5 text-destructive", "aria-label": "Pendente hoje" }) : /* @__PURE__ */ jsx(Clock, { className: "h-3.5 w-3.5 text-warning", "aria-label": "Em aberto" });
2723
+ }
2724
+ if (editPaymentStatus === "none" && hoje3 && !negativo && totalPrice > 0) {
2725
+ return /* @__PURE__ */ jsx(AlertTriangle, { className: "h-3.5 w-3.5 text-destructive", "aria-label": "Sem cobran\xE7a levantada" });
2726
+ }
2727
+ return null;
2728
+ })(),
2729
+ isActive && /* @__PURE__ */ jsx("span", { className: "absolute bottom-0 left-0 right-0 h-0.5 rounded-full bg-foreground" })
2730
+ ]
2731
+ },
2732
+ tab.id
2733
+ );
2734
+ }) }) : null),
2735
+ modalTab === "appointment" && /* @__PURE__ */ jsxs(Fragment, { children: [
2736
+ /* @__PURE__ */ jsxs("div", { className: `${ROW_TOP} py-1`, children: [
2737
+ /* @__PURE__ */ jsx(Clock, { className: "mt-0.5 h-4 w-4 text-muted-foreground shrink-0" }),
2738
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
2739
+ /* @__PURE__ */ jsxs(
2740
+ "div",
2741
+ {
2742
+ className: "flex flex-wrap items-center gap-x-1.5 gap-y-1 text-sm min-w-0",
2743
+ onClick: (e) => {
2744
+ if (e.target.closest('button, input, select, [role="combobox"]')) return;
2745
+ setEditingDate(true);
2746
+ setEditingTime(true);
2747
+ },
2748
+ children: [
2749
+ editingDate ? /* @__PURE__ */ jsx(
2750
+ "input",
2751
+ {
2752
+ type: "date",
2753
+ value: date,
2754
+ autoFocus: true,
2755
+ onChange: (e) => setDate(e.target.value),
2756
+ onBlur: () => setEditingDate(false),
2757
+ className: "w-auto shrink-0 border-b border-foreground bg-transparent text-sm font-medium focus:outline-none"
2758
+ }
2759
+ ) : /* @__PURE__ */ jsx(
2760
+ "button",
2761
+ {
2762
+ type: "button",
2763
+ onClick: () => setEditingDate(true),
2764
+ className: "shrink-0 border-b border-transparent px-0.5 py-0.5 font-medium transition-colors hover:border-foreground",
2765
+ children: formatCompactDate(date, locale)
2766
+ }
2767
+ ),
2768
+ editingTime ? /* @__PURE__ */ jsx(
2769
+ "input",
2770
+ {
2771
+ type: "time",
2772
+ value: startTime,
2773
+ autoFocus: !editingDate,
2774
+ onChange: (e) => {
2775
+ if (e.target.value) setStartTime(e.target.value);
2776
+ },
2777
+ onBlur: () => setEditingTime(false),
2778
+ className: "w-auto shrink-0 border-b border-foreground bg-transparent text-xs text-muted-foreground focus:outline-none"
2779
+ }
2780
+ ) : /* @__PURE__ */ jsx(
2781
+ "button",
2782
+ {
2783
+ type: "button",
2784
+ onClick: () => setEditingTime(true),
2785
+ className: "shrink-0 border-b border-transparent px-0.5 py-0.5 text-xs text-muted-foreground transition-colors hover:border-foreground",
2786
+ children: startTime
2787
+ }
2788
+ ),
2789
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground/60", children: "\u2014" }),
2790
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground/60", children: endTime }),
2791
+ activeType.fields.professional && /* @__PURE__ */ jsx("div", { className: "ml-auto shrink-0", children: /* @__PURE__ */ jsx(ProfessionalSelect, { value: professionalId, onChange: setProfessionalId, professionals: professionalOptions }) })
2792
+ ]
2793
+ }
2794
+ ),
2795
+ provider2.createRecurringBookings && recurrenceEnabled && /* COLADA na linha de cima: as duas são a mesma frase, e um
2796
+ respiro entre elas as separa em dois assuntos de novo. */
2797
+ /* @__PURE__ */ jsx("div", { className: "-mt-0.5", children: mode === "edit" ? /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: seriesId && seriesSize > 1 ? t2("agenda.recurrence.partOfSeries").replace("{{count}}", String(seriesSize)) + (recurrencePreset !== "none" ? ` \xB7 ${t2(`agenda.recurrence.preset.${recurrencePreset}`)}` : "") : t2("agenda.recurrence.preset.none") }) : /* @__PURE__ */ jsxs(Fragment, { children: [
2798
+ /* @__PURE__ */ jsxs("div", { className: "mt-1 flex flex-wrap items-center gap-2", children: [
2799
+ /* @__PURE__ */ jsxs(Select, { value: recurrencePreset, onValueChange: (v) => setRecurrencePreset(v), children: [
2800
+ /* @__PURE__ */ jsx(SelectTrigger, { className: "h-auto w-auto gap-1 border-0 bg-transparent p-0 text-xs text-muted-foreground shadow-none transition-colors hover:text-foreground focus:ring-0", "aria-label": t2("agenda.recurrence.label"), children: /* @__PURE__ */ jsx(SelectValue, {}) }),
2801
+ /* @__PURE__ */ jsxs(SelectContent, { children: [
2802
+ /* @__PURE__ */ jsx(SelectItem, { value: "none", children: t2("agenda.recurrence.preset.none") }),
2803
+ /* @__PURE__ */ jsx(SelectItem, { value: "daily", children: t2("agenda.recurrence.preset.daily") }),
2804
+ /* @__PURE__ */ jsx(SelectItem, { value: "weekly", children: t2("agenda.recurrence.preset.weekly") }),
2805
+ /* @__PURE__ */ jsx(SelectItem, { value: "biweekly", children: t2("agenda.recurrence.preset.biweekly") }),
2806
+ /* @__PURE__ */ jsx(SelectItem, { value: "monthly", children: t2("agenda.recurrence.preset.monthly") })
2807
+ ] })
2808
+ ] }),
2809
+ recurrencePreset !== "none" && /* @__PURE__ */ jsxs(Fragment, { children: [
2810
+ /* @__PURE__ */ jsxs(Select, { value: recurrenceEndMode, onValueChange: (v) => setRecurrenceEndMode(v), children: [
2811
+ /* @__PURE__ */ jsx(SelectTrigger, { className: "h-auto w-auto gap-1 border-0 bg-transparent p-0 text-xs text-muted-foreground shadow-none transition-colors hover:text-foreground focus:ring-0", "aria-label": t2("agenda.recurrence.ends"), children: /* @__PURE__ */ jsx(SelectValue, {}) }),
2812
+ /* @__PURE__ */ jsxs(SelectContent, { children: [
2813
+ /* @__PURE__ */ jsx(SelectItem, { value: "count", children: t2("agenda.recurrence.endCountShort") }),
2814
+ /* @__PURE__ */ jsx(SelectItem, { value: "until", children: t2("agenda.recurrence.endUntilShort") })
2815
+ ] })
2816
+ ] }),
2817
+ recurrenceEndMode === "count" ? /* @__PURE__ */ jsxs(Fragment, { children: [
2818
+ /* @__PURE__ */ jsx(
2819
+ "input",
2820
+ {
2821
+ type: "number",
2822
+ min: 1,
2823
+ max: RECURRENCE_MAX_OCCURRENCES,
2824
+ value: recurrenceCount,
2825
+ onChange: (e) => setRecurrenceCount(Number(e.target.value) || 1),
2826
+ "aria-label": t2("agenda.recurrence.countLabel"),
2827
+ className: "w-10 shrink-0 border-b border-foreground bg-transparent text-center text-xs focus:outline-none"
2828
+ }
2829
+ ),
2830
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t2("agenda.recurrence.times") })
2831
+ ] }) : /* @__PURE__ */ jsx(DatePicker, { value: recurrenceUntil, onChange: setRecurrenceUntil })
2832
+ ] })
2833
+ ] }),
2834
+ recurrencePreset !== "none" && /* @__PURE__ */ jsxs("div", { className: "mt-1.5 space-y-1", children: [
2835
+ seriesDates && /* @__PURE__ */ jsx("p", { className: "text-[11px] text-muted-foreground", children: t2("agenda.recurrence.summary").replace("{{count}}", String(seriesDates.length)).replace("{{last}}", formatCompactDate(seriesDates[seriesDates.length - 1], locale)) }),
2836
+ expansion?.truncated && /* @__PURE__ */ jsx("p", { className: "text-[11px] text-warning", children: t2("agenda.recurrence.capped").replace("{{max}}", String(RECURRENCE_MAX_OCCURRENCES)) }),
2837
+ !professionalId && seriesDates && /* @__PURE__ */ jsx("p", { className: "text-[11px] text-muted-foreground", children: t2("agenda.recurrence.needsProfessional") }),
2838
+ checkingSeries && /* @__PURE__ */ jsx("p", { className: "text-[11px] text-muted-foreground", children: t2("agenda.recurrence.checking") }),
2839
+ !checkingSeries && seriesConflicts && seriesDates && (seriesConflicts.busyStarts.length > 0 || seriesConflicts.closedDates.length > 0) && /* @__PURE__ */ jsxs("div", { className: "space-y-0.5 rounded-md bg-warning/10 px-2 py-1.5", children: [
2840
+ seriesConflicts.busyStarts.length > 0 && /* @__PURE__ */ jsx("p", { className: "text-[11px] font-medium text-warning", children: t2("agenda.recurrence.conflicts").replace("{{busy}}", String(seriesConflicts.busyStarts.length)).replace("{{total}}", String(seriesDates.length)) }),
2841
+ seriesConflicts.closedDates.length > 0 && /* @__PURE__ */ jsx("p", { className: "text-[11px] font-medium text-warning", children: t2("agenda.recurrence.closedDays").replace("{{closed}}", String(seriesConflicts.closedDates.length)) }),
2842
+ /* @__PURE__ */ jsx("p", { className: "text-[10px] text-muted-foreground", children: t2("agenda.recurrence.createsAnyway") })
2843
+ ] })
2844
+ ] })
2845
+ ] }) })
2846
+ ] })
2847
+ ] }),
2848
+ conflict && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 rounded-lg bg-destructive/10 px-3 py-1.5 text-xs text-destructive", children: [
2849
+ /* @__PURE__ */ jsx(AlertTriangle, { className: "h-3.5 w-3.5 shrink-0" }),
2850
+ t2("agenda.appointment.conflict")
2851
+ ] }),
2852
+ feriado && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 rounded-lg bg-warning/10 px-3 py-1.5 text-xs text-warning-foreground", children: [
2853
+ /* @__PURE__ */ jsx(AlertTriangle, { className: "h-3.5 w-3.5 shrink-0" }),
2854
+ t2("agenda.appointment.holidayWarning"),
2855
+ " ",
2856
+ feriado
2857
+ ] }),
2858
+ roomConflict && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 rounded-lg bg-warning/10 px-3 py-1.5 text-xs text-warning-foreground", children: [
2859
+ /* @__PURE__ */ jsx(AlertTriangle, { className: "h-3.5 w-3.5 shrink-0" }),
2860
+ t2("agenda.appointment.roomConflict")
2568
2861
  ] })
2569
- ] }),
2570
- conflict && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 rounded-lg bg-destructive/10 px-3 py-1.5 text-xs text-destructive", children: [
2571
- /* @__PURE__ */ jsx(AlertTriangle, { className: "h-3.5 w-3.5 shrink-0" }),
2572
- t2("agenda.appointment.conflict")
2573
- ] }),
2574
- feriado && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 rounded-lg bg-warning/10 px-3 py-1.5 text-xs text-warning-foreground", children: [
2575
- /* @__PURE__ */ jsx(AlertTriangle, { className: "h-3.5 w-3.5 shrink-0" }),
2576
- t2("agenda.appointment.holidayWarning"),
2577
- " ",
2578
- feriado
2579
- ] }),
2580
- roomConflict && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 rounded-lg bg-warning/10 px-3 py-1.5 text-xs text-warning-foreground", children: [
2581
- /* @__PURE__ */ jsx(AlertTriangle, { className: "h-3.5 w-3.5 shrink-0" }),
2582
- t2("agenda.appointment.roomConflict")
2583
2862
  ] })
2584
- ] })
2585
- ] }),
2586
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col flex-1 min-h-0 overflow-hidden", children: [
2587
- modalTab === "client" && clientId && config.clientEntityDef && /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto px-5 py-4", children: clientDataLoading ? /* @__PURE__ */ jsx("div", { className: "space-y-3 py-2", children: [...Array(4)].map((_, i) => /* @__PURE__ */ jsx("div", { className: "h-8 rounded-lg bg-muted animate-pulse" }, i)) }) : clientData ? clientEditing ? /* @__PURE__ */ jsx(
2588
- CrudFormPage,
2589
- {
2590
- embedded: true,
2591
- entityDef: config.clientEntityDef,
2592
- mode: "edit",
2593
- initialData: clientData,
2594
- namePlural: "",
2595
- onCancel: () => setClientEditing(false),
2596
- onSubmit: async (data) => {
2597
- try {
2598
- if (!clientProvider) return;
2599
- await clientProvider.update(clientId, data);
2600
- setClientData({ ...clientData, ...data });
2601
- setClientEditing(false);
2602
- } catch {
2863
+ ] }),
2864
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col flex-1 min-h-0 overflow-hidden", children: [
2865
+ modalTab === "client" && clientId && config.clientEntityDef && /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto px-5 py-4", children: clientDataLoading ? /* @__PURE__ */ jsx("div", { className: "space-y-3 py-2", children: [...Array(4)].map((_, i) => /* @__PURE__ */ jsx("div", { className: "h-8 rounded-lg bg-muted animate-pulse" }, i)) }) : clientData ? clientEditing ? /* @__PURE__ */ jsx(
2866
+ CrudFormPage,
2867
+ {
2868
+ embedded: true,
2869
+ entityDef: config.clientEntityDef,
2870
+ mode: "edit",
2871
+ initialData: clientData,
2872
+ namePlural: "",
2873
+ onCancel: () => setClientEditing(false),
2874
+ onSubmit: async (data) => {
2875
+ try {
2876
+ if (!clientProvider) return;
2877
+ await clientProvider.update(clientId, data);
2878
+ setClientData({ ...clientData, ...data });
2879
+ setClientEditing(false);
2880
+ } catch {
2881
+ }
2603
2882
  }
2604
2883
  }
2605
- }
2606
- ) : /* @__PURE__ */ jsx(
2607
- CrudDetailPage,
2608
- {
2609
- embedded: true,
2610
- entityDef: config.clientEntityDef,
2611
- item: clientData,
2612
- namePlural: "",
2613
- basePath: "",
2614
- onBack: () => setModalTab("appointment"),
2615
- onEdit: () => setClientEditing(true),
2616
- onFieldSave: clientProvider ? saveClientField : void 0,
2617
- extraActions: config.clientRecordPath ? [{
2618
- id: "full-record",
2619
- label: t2("agenda.appointment.fullRecord"),
2620
- icon: ExternalLink,
2621
- // `utility` fica AO LADO do Editar (ícone + tooltip);
2622
- // secondary sumiria no caret.
2623
- intent: "utility",
2624
- onSelect: () => {
2625
- window.location.hash = `#${config.clientRecordPath}/${clientId}`;
2626
- onClose();
2627
- }
2628
- }] : void 0
2629
- }
2630
- ) : /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground py-4 text-center", children: t2("agenda.appointment.noClientData") }) }),
2631
- modalTab === "history" && clientId && /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto", children: /* @__PURE__ */ jsx(
2632
- ClientHistoryPanel,
2633
- {
2634
- clientId,
2635
- currentBookingId: bookingId,
2636
- formatCurrency: (v) => fmtCurrency(v, currency.locale, currency.code)
2637
- }
2638
- ) }),
2639
- modalTab === "financial" && clientId && /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto px-5 py-4", children: /* @__PURE__ */ jsx(
2640
- ClientAccountInline,
2641
- {
2642
- personId: clientId,
2643
- orderId: editOrderId,
2644
- clientName: clientSearch || void 0,
2645
- currency,
2646
- onGenerate: bookingId && provider2.chargeCheckout ? async () => {
2647
- const r = await provider2.chargeCheckout(bookingId);
2648
- if (!r.ok) {
2649
- toast.error({
2650
- not_found: "Este agendamento n\xE3o existe mais.",
2651
- sem_sessao: "Sess\xE3o expirada. Entre de novo para cobrar.",
2652
- nada_contratado: "N\xE3o h\xE1 servi\xE7o contratado para cobrar neste agendamento."
2653
- }[r.reason ?? ""] ?? "N\xE3o foi poss\xEDvel gerar o recebimento.");
2654
- return;
2655
- }
2656
- if (!r.total) {
2657
- toast.success("Atendimento sem valor a cobrar \u2014 nada foi levantado.");
2658
- }
2659
- let orderNow = editOrderId;
2660
- try {
2661
- const fresh = await provider2.getBookingById(bookingId);
2662
- if (fresh?.orderId) {
2663
- orderNow = fresh.orderId;
2664
- setEditOrderId(fresh.orderId);
2884
+ ) : /* @__PURE__ */ jsx(
2885
+ CrudDetailPage,
2886
+ {
2887
+ embedded: true,
2888
+ entityDef: config.clientEntityDef,
2889
+ item: clientData,
2890
+ namePlural: "",
2891
+ basePath: "",
2892
+ onBack: () => setModalTab("appointment"),
2893
+ onEdit: () => setClientEditing(true),
2894
+ onFieldSave: clientProvider ? saveClientField : void 0,
2895
+ extraActions: config.clientRecordPath ? [{
2896
+ id: "full-record",
2897
+ label: t2("agenda.appointment.fullRecord"),
2898
+ icon: ExternalLink,
2899
+ // `utility` fica AO LADO do Editar (ícone + tooltip);
2900
+ // secondary sumiria no caret.
2901
+ intent: "utility",
2902
+ onSelect: () => {
2903
+ window.location.hash = `#${config.clientRecordPath}/${clientId}`;
2904
+ onClose();
2665
2905
  }
2666
- } catch {
2667
- }
2668
- config.financialBridge?.getPaymentDetail(orderNow ?? "").then((d) => {
2669
- if (d) {
2670
- setEditPaymentStatus(d.status);
2671
- setEditOrderTotal(d.totalAmount);
2906
+ }] : void 0
2907
+ }
2908
+ ) : /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground py-4 text-center", children: t2("agenda.appointment.noClientData") }) }),
2909
+ modalTab === "history" && clientId && /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto", children: /* @__PURE__ */ jsx(
2910
+ ClientHistoryPanel,
2911
+ {
2912
+ clientId,
2913
+ currentBookingId: bookingId,
2914
+ formatCurrency: (v) => fmtCurrency(v, currency.locale, currency.code)
2915
+ }
2916
+ ) }),
2917
+ modalTab === "financial" && clientId && /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto px-5 py-4", children: /* @__PURE__ */ jsx(
2918
+ ClientAccountInline,
2919
+ {
2920
+ personId: clientId,
2921
+ orderId: editOrderId,
2922
+ clientName: clientSearch || void 0,
2923
+ currency,
2924
+ onGenerate: bookingId && provider2.chargeCheckout ? async () => {
2925
+ const r = await provider2.chargeCheckout(bookingId);
2926
+ if (!r.ok) {
2927
+ toast.error({
2928
+ not_found: "Este agendamento n\xE3o existe mais.",
2929
+ sem_sessao: "Sess\xE3o expirada. Entre de novo para cobrar.",
2930
+ nada_contratado: "N\xE3o h\xE1 servi\xE7o contratado para cobrar neste agendamento."
2931
+ }[r.reason ?? ""] ?? "N\xE3o foi poss\xEDvel gerar o recebimento.");
2932
+ return;
2672
2933
  }
2673
- }).catch(() => {
2674
- });
2675
- if (agendaDateRange.start) fetchBookings(agendaDateRange);
2676
- } : void 0
2677
- }
2678
- ) }),
2679
- modalTab === "financial" && !clientId && editOrderId && config.financialBridge && /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto", children: /* @__PURE__ */ jsx(
2680
- BookingPaymentPanel,
2681
- {
2682
- orderId: editOrderId,
2683
- orderTotal: editOrderTotal,
2684
- services: services.map((s) => ({ name: s.name, price: s.price, durationMinutes: s.durationMinutes })),
2685
- formatCurrency: (v) => fmtCurrency(v, currency.locale, currency.code),
2686
- inline: true,
2687
- onClose: () => setModalTab("appointment"),
2688
- onPaymentChange: () => {
2689
- config.financialBridge.getPaymentDetail(editOrderId).then((d) => {
2690
- if (d) {
2691
- setEditPaymentStatus(d.status);
2692
- setEditOrderTotal(d.totalAmount);
2934
+ if (!r.total) {
2935
+ toast.success("Atendimento sem valor a cobrar \u2014 nada foi levantado.");
2693
2936
  }
2694
- }).catch(() => {
2695
- });
2696
- if (agendaDateRange.start) fetchBookings(agendaDateRange);
2697
- }
2698
- }
2699
- ) }),
2700
- /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col flex-1 min-h-0 overflow-hidden", style: { display: modalTab === "appointment" ? void 0 : "none" }, children: [
2701
- /* @__PURE__ */ jsxs("div", { className: "border-t fayz-glass-divider px-5 py-3 space-y-1 flex-1 overflow-y-auto", children: [
2702
- loading && /* @__PURE__ */ jsx("div", { className: "space-y-2 py-2", children: [...Array(4)].map((_, i) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 py-1.5", children: [
2703
- /* @__PURE__ */ jsx("div", { className: "h-4 w-4 rounded bg-muted animate-pulse shrink-0" }),
2704
- /* @__PURE__ */ jsx("div", { className: "h-8 flex-1 rounded-lg bg-muted animate-pulse" })
2705
- ] }, i)) }),
2706
- !loading && /* @__PURE__ */ jsxs(Fragment, { children: [
2707
- activeType.fields.title && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 py-1", children: [
2708
- /* @__PURE__ */ jsx(Calendar, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
2709
- /* @__PURE__ */ jsx(
2710
- "input",
2711
- {
2712
- type: "text",
2713
- value: title,
2714
- onChange: (e) => setTitle(e.target.value),
2715
- placeholder: t2("agenda.appointment.addTitle"),
2716
- autoFocus: mode === "create",
2717
- "aria-required": true,
2718
- className: "w-full flex-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-2.5 py-1.5 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-ring"
2937
+ let orderNow = editOrderId;
2938
+ try {
2939
+ const fresh = await provider2.getBookingById(bookingId);
2940
+ if (fresh?.orderId) {
2941
+ orderNow = fresh.orderId;
2942
+ setEditOrderId(fresh.orderId);
2719
2943
  }
2720
- )
2721
- ] }),
2722
- activeType.fields.client && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 py-1", children: [
2723
- /* @__PURE__ */ jsx(User, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
2724
- /* @__PURE__ */ jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsx(
2725
- ContactPicker,
2726
- {
2727
- value: clientId || clientSearch ? { id: clientId || void 0, name: clientSearch, phone: clientPhone } : null,
2728
- onChange: (contact) => {
2729
- setClientId(contact?.id ?? "");
2730
- setClientSearch(contact?.name ?? "");
2731
- setClientPhone(contact?.phone ?? "");
2732
- },
2733
- kind: config.clientKind,
2734
- lookup: config.contactLookup,
2735
- createEntityDef: config.clientEntityDef,
2736
- clearable: !isPaidBooking,
2737
- variant: "inline",
2738
- placeholder: t2("agenda.appointment.searchClient"),
2739
- createLabel: t2("agenda.appointment.newClient"),
2740
- required: activeType.requiresClient
2944
+ } catch {
2945
+ }
2946
+ config.financialBridge?.getPaymentDetail(orderNow ?? "").then((d) => {
2947
+ if (d) {
2948
+ setEditPaymentStatus(d.status);
2949
+ setEditOrderTotal(d.totalAmount);
2741
2950
  }
2742
- ) })
2743
- ] }),
2744
- activeType.fields.client && clientId && /* @__PURE__ */ jsx("div", { className: "pl-7", children: /* @__PURE__ */ jsx(PersonFormsAlert, { personId: clientId, version: formsVersao, onFill: (t3) => setPreenchendoForm(t3) }) }),
2745
- activeType.fields.attendees && /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3 py-1", children: [
2746
- /* @__PURE__ */ jsx(Users, { className: "h-4 w-4 mt-1.5 text-muted-foreground shrink-0" }),
2747
- /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
2748
- attendees.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5 mb-1.5", children: attendees.map((a) => /* @__PURE__ */ jsxs(
2749
- "span",
2750
- {
2751
- className: "inline-flex items-center gap-1 rounded-full bg-muted/40 pl-2.5 pr-1 py-0.5 text-sm",
2752
- children: [
2753
- a.name,
2754
- /* @__PURE__ */ jsx(
2755
- "button",
2756
- {
2757
- type: "button",
2758
- "aria-label": t2("agenda.attendees.remove"),
2759
- onClick: () => setAttendees((p) => p.filter((x) => x.personId !== a.personId)),
2760
- className: "rounded-full p-0.5 hover:bg-muted transition-colors",
2761
- children: /* @__PURE__ */ jsx(X, { className: "h-3 w-3" })
2762
- }
2763
- )
2764
- ]
2765
- },
2766
- a.personId
2767
- )) }),
2951
+ }).catch(() => {
2952
+ });
2953
+ if (agendaDateRange.start) fetchBookings(agendaDateRange);
2954
+ } : void 0
2955
+ }
2956
+ ) }),
2957
+ modalTab === "financial" && !clientId && editOrderId && config.financialBridge && /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto", children: /* @__PURE__ */ jsx(
2958
+ BookingPaymentPanel,
2959
+ {
2960
+ orderId: editOrderId,
2961
+ orderTotal: editOrderTotal,
2962
+ services: services.map((s) => ({ name: s.name, price: s.price, durationMinutes: s.durationMinutes })),
2963
+ formatCurrency: (v) => fmtCurrency(v, currency.locale, currency.code),
2964
+ inline: true,
2965
+ onClose: () => setModalTab("appointment"),
2966
+ onPaymentChange: () => {
2967
+ config.financialBridge.getPaymentDetail(editOrderId).then((d) => {
2968
+ if (d) {
2969
+ setEditPaymentStatus(d.status);
2970
+ setEditOrderTotal(d.totalAmount);
2971
+ }
2972
+ }).catch(() => {
2973
+ });
2974
+ if (agendaDateRange.start) fetchBookings(agendaDateRange);
2975
+ }
2976
+ }
2977
+ ) }),
2978
+ /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col flex-1 min-h-0 overflow-hidden", style: { display: modalTab === "appointment" ? void 0 : "none" }, children: [
2979
+ /* @__PURE__ */ jsxs("div", { className: "border-t fayz-glass-divider px-5 py-3 space-y-1 flex-1 overflow-y-auto", children: [
2980
+ loading && /* @__PURE__ */ jsx("div", { className: "space-y-2 py-2", children: [...Array(4)].map((_, i) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 py-1.5", children: [
2981
+ /* @__PURE__ */ jsx("div", { className: "h-4 w-4 rounded bg-muted animate-pulse shrink-0" }),
2982
+ /* @__PURE__ */ jsx("div", { className: "h-8 flex-1 rounded-lg bg-muted animate-pulse" })
2983
+ ] }, i)) }),
2984
+ !loading && /* @__PURE__ */ jsxs(Fragment, { children: [
2985
+ activeType.fields.title && /* @__PURE__ */ jsxs("div", { className: `${ROW_CENTER} py-1`, children: [
2986
+ /* @__PURE__ */ jsx(Calendar, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
2768
2987
  /* @__PURE__ */ jsx(
2988
+ "input",
2989
+ {
2990
+ type: "text",
2991
+ value: title,
2992
+ onChange: (e) => setTitle(e.target.value),
2993
+ placeholder: t2("agenda.appointment.addTitle"),
2994
+ autoFocus: mode === "create",
2995
+ "aria-required": true,
2996
+ className: "w-full flex-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-2.5 py-1.5 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-ring"
2997
+ }
2998
+ )
2999
+ ] }),
3000
+ activeType.fields.client && /* @__PURE__ */ jsxs("div", { className: `${ROW_CENTER} py-1`, children: [
3001
+ /* @__PURE__ */ jsx(User, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
3002
+ /* @__PURE__ */ jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsx(
2769
3003
  ContactPicker,
2770
3004
  {
2771
- value: null,
3005
+ value: clientId || clientSearch ? { id: clientId || void 0, name: clientSearch, phone: clientPhone } : null,
2772
3006
  onChange: (contact) => {
2773
- if (!contact?.id) return;
2774
- setAttendees((p) => p.some((x) => x.personId === contact.id) ? p : [...p, { personId: contact.id, name: contact.name ?? "" }]);
2775
- setAttendeeSeq((n) => n + 1);
3007
+ setClientId(contact?.id ?? "");
3008
+ setClientSearch(contact?.name ?? "");
3009
+ setClientPhone(contact?.phone ?? "");
2776
3010
  },
2777
- lookup: config.attendeeLookup,
3011
+ kind: config.clientKind,
3012
+ lookup: config.contactLookup,
2778
3013
  createEntityDef: config.clientEntityDef,
3014
+ clearable: !isPaidBooking,
2779
3015
  variant: "inline",
2780
- placeholder: t2("agenda.attendees.add"),
2781
- createLabel: t2("agenda.appointment.newClient")
2782
- },
2783
- attendeeSeq
2784
- )
2785
- ] })
2786
- ] }),
2787
- activeType.fields.location && config.modules.locationSelection && (storeLocations.length > 0 || config.locations.length > 0) && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 py-1", children: [
2788
- /* @__PURE__ */ jsx(MapPin, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
2789
- /* @__PURE__ */ jsx(
2790
- LocationSelect,
2791
- {
2792
- value: locationId,
2793
- onChange: setLocationId,
2794
- locations: storeLocations.length > 0 ? storeLocations : config.locations
2795
- }
2796
- )
2797
- ] }),
2798
- storeResources.length > 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 py-1", children: [
2799
- /* @__PURE__ */ jsx(DoorOpen, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
2800
- /* @__PURE__ */ jsx(
2801
- LocationSelect,
2802
- {
2803
- value: resourceId,
2804
- onChange: setResourceId,
2805
- locations: storeResources,
2806
- placeholder: t2("agenda.appointment.selectRoom")
2807
- }
2808
- )
2809
- ] }),
2810
- activeType.fields.services && /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3 py-1", children: [
2811
- /* @__PURE__ */ jsx(Briefcase, { className: "h-4 w-4 mt-1.5 text-muted-foreground shrink-0" }),
2812
- /* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
2813
- services.length > 0 && /* @__PURE__ */ jsx("div", { className: "space-y-0.5 mb-1", children: services.map((svc, idx) => /* @__PURE__ */ jsxs("div", { className: "flex items-center rounded-md bg-muted/30 px-2.5 py-1.5 text-sm", children: [
2814
- /* @__PURE__ */ jsx("span", { className: "flex-1 truncate min-w-0", children: svc.name }),
2815
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5 shrink-0 ml-3", children: [
2816
- /* @__PURE__ */ jsx(
2817
- InlineNumber,
2818
- {
2819
- value: svc.durationMinutes,
2820
- onChange: (v) => setServices((p) => p.map((s, i) => i === idx ? { ...s, durationMinutes: Math.round(v) } : s)),
2821
- min: 5,
2822
- step: 5,
2823
- className: "w-10 shrink-0",
2824
- disabled: isPaidBooking,
2825
- tooltip: t2("agenda.appointment.duration")
2826
- }
2827
- ),
2828
- /* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground", children: "min" })
2829
- ] }),
2830
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5 shrink-0 ml-3", children: [
2831
- /* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground", children: currency.symbol }),
2832
- /* @__PURE__ */ jsx(
2833
- InlineNumber,
2834
- {
2835
- value: svc.price,
2836
- onChange: (v) => setServices((p) => p.map((s, i) => i === idx ? { ...s, price: v } : s)),
2837
- min: 0,
2838
- step: 0.01,
2839
- className: "w-14 shrink-0",
2840
- disabled: isPaidBooking,
2841
- tooltip: t2("agenda.appointment.price")
2842
- }
2843
- )
2844
- ] }),
2845
- !isPaidBooking && /* @__PURE__ */ jsx("button", { type: "button", onClick: () => removeService(idx), className: "p-0.5 ml-2 text-muted-foreground hover:text-destructive shrink-0", children: /* @__PURE__ */ jsx(X, { className: "h-3 w-3" }) })
2846
- ] }, idx)) }),
2847
- serviceCreate.creating && serviceCreate.form,
2848
- !isPaidBooking && !serviceCreate.creating && /* @__PURE__ */ jsx(
2849
- SearchCombobox,
2850
- {
2851
- value: serviceSearch,
2852
- onChange: setServiceSearch,
2853
- onSelect: addService,
2854
- options: serviceOptions,
2855
- placeholder: t2("agenda.appointment.searchService"),
2856
- onBlurEmpty: () => {
2857
- },
2858
- onFocus: () => {
2859
- if (!serviceSearch && serviceOptions.length === 0) setServiceOptions(initialServiceOptions);
3016
+ placeholder: t2("agenda.appointment.searchClient"),
3017
+ createLabel: t2("agenda.appointment.newClient"),
3018
+ required: activeType.requiresClient
3019
+ }
3020
+ ) })
3021
+ ] }),
3022
+ activeType.fields.client && clientId && /* @__PURE__ */ jsx("div", { className: "pl-7", children: /* @__PURE__ */ jsx(PersonFormsAlert, { personId: clientId, version: formsVersao, onFill: (t3) => setPreenchendoForm(t3) }) }),
3023
+ activeType.fields.attendees && /* @__PURE__ */ jsxs("div", { className: `${ROW_TOP} py-1`, children: [
3024
+ /* @__PURE__ */ jsx(Users, { className: "h-4 w-4 mt-1.5 text-muted-foreground shrink-0" }),
3025
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
3026
+ attendees.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5 mb-1.5", children: attendees.map((a) => /* @__PURE__ */ jsxs(
3027
+ "span",
3028
+ {
3029
+ className: "inline-flex items-center gap-1 rounded-full bg-muted/40 pl-2.5 pr-1 py-0.5 text-sm",
3030
+ children: [
3031
+ a.name,
3032
+ /* @__PURE__ */ jsx(
3033
+ "button",
3034
+ {
3035
+ type: "button",
3036
+ "aria-label": t2("agenda.attendees.remove"),
3037
+ onClick: () => setAttendees((p) => p.filter((x) => x.personId !== a.personId)),
3038
+ className: "rounded-full p-0.5 hover:bg-muted transition-colors",
3039
+ children: /* @__PURE__ */ jsx(X, { className: "h-3 w-3" })
3040
+ }
3041
+ )
3042
+ ]
2860
3043
  },
2861
- renderRight: (opt) => {
2862
- const dur = opt.data?.duration_minutes || 0;
2863
- const hasPrice = opt.price != null;
2864
- if (!hasPrice && !dur) return null;
2865
- return /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground text-xs whitespace-nowrap", children: [
2866
- hasPrice && fmtCurrency(opt.price, currency.locale, currency.code),
2867
- hasPrice && dur > 0 && " \xB7 ",
2868
- dur > 0 && `${dur}min`
2869
- ] });
3044
+ a.personId
3045
+ )) }),
3046
+ /* @__PURE__ */ jsx(
3047
+ ContactPicker,
3048
+ {
3049
+ value: null,
3050
+ onChange: (contact) => {
3051
+ if (!contact?.id) return;
3052
+ setAttendees((p) => p.some((x) => x.personId === contact.id) ? p : [...p, { personId: contact.id, name: contact.name ?? "" }]);
3053
+ setAttendeeSeq((n) => n + 1);
3054
+ },
3055
+ lookup: config.attendeeLookup,
3056
+ createEntityDef: config.clientEntityDef,
3057
+ variant: "inline",
3058
+ placeholder: t2("agenda.attendees.add"),
3059
+ createLabel: t2("agenda.appointment.newClient")
2870
3060
  },
2871
- minimal: true,
2872
- chevron: true,
2873
- required: activeType.requiresServices && services.length === 0,
2874
- allowCreate: !!serviceCreate.onCreate,
2875
- onCreateNew: serviceCreate.onCreate,
2876
- createLabel: t2("agenda.appointment.newService"),
2877
- inlineLabel: services.length > 0 ? t2("agenda.appointment.addService") : t2("agenda.appointment.addServiceFirst"),
2878
- inlineIcon: services.length > 0 ? "plus" : "search"
3061
+ attendeeSeq
3062
+ )
3063
+ ] })
3064
+ ] }),
3065
+ activeType.fields.location && config.modules.locationSelection && (storeLocations.length > 0 || config.locations.length > 0) && /* @__PURE__ */ jsxs("div", { className: `${ROW_CENTER} py-1`, children: [
3066
+ /* @__PURE__ */ jsx(MapPin, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
3067
+ /* @__PURE__ */ jsx(
3068
+ LocationSelect,
3069
+ {
3070
+ value: locationId,
3071
+ onChange: setLocationId,
3072
+ locations: storeLocations.length > 0 ? storeLocations : config.locations
2879
3073
  }
2880
- ),
2881
- showInterests && /* @__PURE__ */ jsx("div", { className: "mt-2", children: /* @__PURE__ */ jsx(
2882
- BookingInterestsPanel,
3074
+ )
3075
+ ] }),
3076
+ storeResources.length > 0 && /* @__PURE__ */ jsxs("div", { className: `${ROW_CENTER} py-1`, children: [
3077
+ /* @__PURE__ */ jsx(DoorOpen, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
3078
+ /* @__PURE__ */ jsx(
3079
+ LocationSelect,
2883
3080
  {
2884
- clientId: clientId || null,
2885
- bookingId: mode === "edit" ? bookingId : void 0,
2886
- value: interests,
2887
- onChange: setInterests
3081
+ value: resourceId,
3082
+ onChange: setResourceId,
3083
+ locations: storeResources,
3084
+ placeholder: t2("agenda.appointment.selectRoom")
2888
3085
  }
2889
- ) })
2890
- ] })
2891
- ] }),
2892
- activeType.fields.status && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 py-1", children: [
2893
- /* @__PURE__ */ jsx(CircleDot, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
2894
- /* @__PURE__ */ jsx(StatusSelect, { value: status, onChange: setStatus, statuses: config.statuses, bookingDate: date })
2895
- ] }),
2896
- needsCancellationReason && /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3 py-1", children: [
2897
- /* @__PURE__ */ jsx(Ban, { className: "h-4 w-4 mt-2 text-muted-foreground shrink-0" }),
2898
- /* @__PURE__ */ jsxs("div", { className: "flex-1 space-y-2", children: [
2899
- /* @__PURE__ */ jsxs(
2900
- "select",
3086
+ )
3087
+ ] }),
3088
+ activeType.fields.services && /* @__PURE__ */ jsxs("div", { className: `${ROW_TOP} py-1`, children: [
3089
+ /* @__PURE__ */ jsx(Briefcase, { className: "h-4 w-4 mt-1.5 text-muted-foreground shrink-0" }),
3090
+ /* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
3091
+ services.length > 0 && /* @__PURE__ */ jsx("div", { className: "space-y-0.5 mb-1", children: services.map((svc, idx) => /* @__PURE__ */ jsxs("div", { className: "flex items-center rounded-md bg-muted/30 px-2.5 py-1.5 text-sm", children: [
3092
+ /* @__PURE__ */ jsx("span", { className: "flex-1 truncate min-w-0", children: svc.name }),
3093
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5 shrink-0 ml-3", children: [
3094
+ /* @__PURE__ */ jsx(
3095
+ InlineNumber,
3096
+ {
3097
+ value: svc.durationMinutes,
3098
+ onChange: (v) => setServices((p) => p.map((s, i) => i === idx ? { ...s, durationMinutes: Math.round(v) } : s)),
3099
+ min: 5,
3100
+ step: 5,
3101
+ className: "w-10 shrink-0",
3102
+ disabled: isPaidBooking,
3103
+ tooltip: t2("agenda.appointment.duration")
3104
+ }
3105
+ ),
3106
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground", children: "min" })
3107
+ ] }),
3108
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5 shrink-0 ml-3", children: [
3109
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground", children: currency.symbol }),
3110
+ /* @__PURE__ */ jsx(
3111
+ InlineNumber,
3112
+ {
3113
+ value: svc.price,
3114
+ onChange: (v) => setServices((p) => p.map((s, i) => i === idx ? { ...s, price: v } : s)),
3115
+ min: 0,
3116
+ step: 0.01,
3117
+ className: "w-14 shrink-0",
3118
+ disabled: isPaidBooking,
3119
+ tooltip: t2("agenda.appointment.price")
3120
+ }
3121
+ )
3122
+ ] }),
3123
+ !isPaidBooking && /* @__PURE__ */ jsx("button", { type: "button", onClick: () => removeService(idx), className: "p-0.5 ml-2 text-muted-foreground hover:text-destructive shrink-0", children: /* @__PURE__ */ jsx(X, { className: "h-3 w-3" }) })
3124
+ ] }, idx)) }),
3125
+ serviceCreate.creating && serviceCreate.form,
3126
+ !isPaidBooking && !serviceCreate.creating && /* @__PURE__ */ jsx(
3127
+ SearchCombobox,
3128
+ {
3129
+ value: serviceSearch,
3130
+ onChange: setServiceSearch,
3131
+ onSelect: addService,
3132
+ options: serviceOptions,
3133
+ placeholder: t2("agenda.appointment.searchService"),
3134
+ onBlurEmpty: () => {
3135
+ },
3136
+ onFocus: () => {
3137
+ if (!serviceSearch && serviceOptions.length === 0) setServiceOptions(initialServiceOptions);
3138
+ },
3139
+ renderRight: (opt) => {
3140
+ const dur = opt.data?.duration_minutes || 0;
3141
+ const hasPrice = opt.price != null;
3142
+ if (!hasPrice && !dur) return null;
3143
+ return /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground text-xs whitespace-nowrap", children: [
3144
+ hasPrice && fmtCurrency(opt.price, currency.locale, currency.code),
3145
+ hasPrice && dur > 0 && " \xB7 ",
3146
+ dur > 0 && `${dur}min`
3147
+ ] });
3148
+ },
3149
+ minimal: true,
3150
+ chevron: true,
3151
+ required: activeType.requiresServices && services.length === 0,
3152
+ allowCreate: !!serviceCreate.onCreate,
3153
+ onCreateNew: serviceCreate.onCreate,
3154
+ createLabel: t2("agenda.appointment.newService"),
3155
+ inlineLabel: services.length > 0 ? t2("agenda.appointment.addService") : t2("agenda.appointment.addServiceFirst"),
3156
+ inlineIcon: services.length > 0 ? "plus" : "search"
3157
+ }
3158
+ ),
3159
+ showInterests && /* @__PURE__ */ jsx("div", { className: "mt-2", children: /* @__PURE__ */ jsx(
3160
+ BookingInterestsPanel,
3161
+ {
3162
+ clientId: clientId || null,
3163
+ bookingId: mode === "edit" ? bookingId : void 0,
3164
+ value: interests,
3165
+ onChange: setInterests
3166
+ }
3167
+ ) })
3168
+ ] })
3169
+ ] }),
3170
+ activeType.fields.status && /* @__PURE__ */ jsxs("div", { className: `${ROW_CENTER} py-1`, children: [
3171
+ /* @__PURE__ */ jsx(CircleDot, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
3172
+ /* @__PURE__ */ jsx(StatusSelect, { value: status, onChange: setStatus, statuses: config.statuses, bookingDate: date })
3173
+ ] }),
3174
+ needsCancellationReason && /* @__PURE__ */ jsxs("div", { className: `${ROW_TOP} py-1`, children: [
3175
+ /* @__PURE__ */ jsx(Ban, { className: "h-4 w-4 mt-2 text-muted-foreground shrink-0" }),
3176
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 space-y-2", children: [
3177
+ /* @__PURE__ */ jsxs(
3178
+ "select",
3179
+ {
3180
+ value: cancellationReasonId,
3181
+ onChange: (event) => setCancellationReasonId(event.target.value),
3182
+ disabled: cancellationReasonsLoading,
3183
+ "aria-required": true,
3184
+ className: "w-full rounded-input border border-input bg-card px-2.5 py-1.5 text-sm shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] focus:outline-none focus:ring-2 focus:ring-ring",
3185
+ children: [
3186
+ /* @__PURE__ */ jsx("option", { value: "", children: cancellationReasonsLoading ? t2("agenda.cancellation.loadingReasons") : t2("agenda.cancellation.selectReason") }),
3187
+ cancellationReasons.map((reason) => /* @__PURE__ */ jsxs("option", { value: reason.id, children: [
3188
+ reason.label,
3189
+ reason.requiresNotes ? " *" : ""
3190
+ ] }, reason.id))
3191
+ ]
3192
+ }
3193
+ ),
3194
+ /* @__PURE__ */ jsx(
3195
+ "textarea",
3196
+ {
3197
+ value: cancellationNotes,
3198
+ onChange: (event) => setCancellationNotes(event.target.value),
3199
+ placeholder: needsCancellationNotes ? t2("agenda.cancellation.notesRequired") : t2("agenda.cancellation.notesOptional"),
3200
+ rows: 2,
3201
+ "aria-required": needsCancellationNotes || void 0,
3202
+ className: "w-full rounded-input border border-input bg-card px-2.5 py-1.5 text-sm resize-none shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] focus:outline-none focus:ring-2 focus:ring-ring"
3203
+ }
3204
+ )
3205
+ ] })
3206
+ ] }),
3207
+ !showNotes ? /* @__PURE__ */ jsxs("div", { className: `${ROW_CENTER} py-1`, children: [
3208
+ /* @__PURE__ */ jsx(FileText, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
3209
+ /* @__PURE__ */ jsx(
3210
+ "button",
2901
3211
  {
2902
- value: cancellationReasonId,
2903
- onChange: (event) => setCancellationReasonId(event.target.value),
2904
- disabled: cancellationReasonsLoading,
2905
- "aria-required": true,
2906
- className: "w-full rounded-input border border-input bg-card px-2.5 py-1.5 text-sm shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] focus:outline-none focus:ring-2 focus:ring-ring",
2907
- children: [
2908
- /* @__PURE__ */ jsx("option", { value: "", children: cancellationReasonsLoading ? t2("agenda.cancellation.loadingReasons") : t2("agenda.cancellation.selectReason") }),
2909
- cancellationReasons.map((reason) => /* @__PURE__ */ jsxs("option", { value: reason.id, children: [
2910
- reason.label,
2911
- reason.requiresNotes ? " *" : ""
2912
- ] }, reason.id))
2913
- ]
3212
+ type: "button",
3213
+ onClick: () => setShowNotes(true),
3214
+ className: "text-sm text-muted-foreground hover:text-foreground transition-colors text-left py-1.5 pl-2",
3215
+ children: t2("agenda.appointment.addNotes")
2914
3216
  }
2915
- ),
3217
+ )
3218
+ ] }) : /* @__PURE__ */ jsxs("div", { className: `${ROW_TOP} py-1`, children: [
3219
+ /* @__PURE__ */ jsx(FileText, { className: "h-4 w-4 mt-2 text-muted-foreground shrink-0" }),
2916
3220
  /* @__PURE__ */ jsx(
2917
3221
  "textarea",
2918
3222
  {
2919
- value: cancellationNotes,
2920
- onChange: (event) => setCancellationNotes(event.target.value),
2921
- placeholder: needsCancellationNotes ? t2("agenda.cancellation.notesRequired") : t2("agenda.cancellation.notesOptional"),
3223
+ value: notes,
3224
+ onChange: (e) => setNotes(e.target.value),
3225
+ placeholder: t2("agenda.appointment.addDescription"),
2922
3226
  rows: 2,
2923
- "aria-required": needsCancellationNotes || void 0,
2924
- className: "w-full rounded-input border border-input bg-card px-2.5 py-1.5 text-sm resize-none shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] focus:outline-none focus:ring-2 focus:ring-ring"
3227
+ autoFocus: true,
3228
+ className: "w-full flex-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-2.5 py-1.5 text-sm resize-none focus:outline-none focus:ring-2 focus:ring-ring"
2925
3229
  }
2926
3230
  )
2927
3231
  ] })
2928
- ] }),
2929
- !showNotes ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 py-1", children: [
2930
- /* @__PURE__ */ jsx(FileText, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
2931
- /* @__PURE__ */ jsx(
2932
- "button",
2933
- {
2934
- type: "button",
2935
- onClick: () => setShowNotes(true),
2936
- className: "text-sm text-muted-foreground hover:text-foreground transition-colors text-left py-1.5 pl-2",
2937
- children: t2("agenda.appointment.addNotes")
2938
- }
2939
- )
2940
- ] }) : /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3 py-1", children: [
2941
- /* @__PURE__ */ jsx(FileText, { className: "h-4 w-4 mt-2 text-muted-foreground shrink-0" }),
2942
- /* @__PURE__ */ jsx(
2943
- "textarea",
2944
- {
2945
- value: notes,
2946
- onChange: (e) => setNotes(e.target.value),
2947
- placeholder: t2("agenda.appointment.addDescription"),
2948
- rows: 2,
2949
- autoFocus: true,
2950
- className: "w-full flex-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-2.5 py-1.5 text-sm resize-none focus:outline-none focus:ring-2 focus:ring-ring"
2951
- }
2952
- )
2953
3232
  ] })
2954
- ] })
2955
- ] }),
2956
- modalTab === "appointment" && !clientEditing && /* @__PURE__ */ jsx("div", { className: "px-5 py-2.5 pb-[calc(0.625rem+env(safe-area-inset-bottom))] sm:pb-2.5 border-t fayz-glass-divider bg-transparent shrink-0", children: confirmingDelete ? /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
2957
- /* @__PURE__ */ jsx("span", { className: "text-xs text-destructive font-medium", children: t2("agenda.appointment.deleteConfirm") }),
2958
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2959
- /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setConfirmingDelete(false), children: t2("agenda.appointment.cancel") }),
2960
- /* @__PURE__ */ jsx(Button, { type: "button", variant: "destructive", size: "sm", onClick: handleDelete, disabled: deleting, children: deleting ? t2("agenda.appointment.deleting") : t2("agenda.appointment.confirmDelete") })
2961
- ] })
2962
- ] }) : /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
2963
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
2964
- mode === "edit" && /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setConfirmingDelete(true), className: "text-xs text-destructive font-medium hover:underline", children: t2("agenda.appointment.delete") }),
2965
- services.length > 0 && /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [
2966
- /* @__PURE__ */ jsx(Clock, { className: "h-3 w-3" }),
2967
- totalDuration,
2968
- "min",
2969
- config.modules.financial && totalPrice > 0 && mode === "edit" ? paymentStatusLoading ? /* @__PURE__ */ jsx("span", { className: "h-4 w-16 rounded-full bg-muted animate-pulse" }) : isPaidBooking ? /* @__PURE__ */ jsxs(
2970
- "button",
2971
- {
2972
- type: "button",
2973
- onClick: (e) => {
2974
- e.stopPropagation();
2975
- setModalTab("financial");
2976
- },
2977
- className: "inline-flex items-center gap-1 rounded-full bg-success/10 px-2 py-0.5 text-[10px] font-medium text-success hover:bg-success/20 transition-colors",
2978
- children: [
2979
- /* @__PURE__ */ jsx(Check, { className: "h-2.5 w-2.5" }),
2980
- fmtCurrency(totalPrice, currency.locale, currency.code),
2981
- " \xB7 ",
2982
- t2("agenda.payment.paid")
2983
- ]
2984
- }
2985
- ) : date <= todayLocal() ? /* @__PURE__ */ jsxs(
3233
+ ] }),
3234
+ modalTab === "appointment" && !clientEditing && /* @__PURE__ */ jsx("div", { className: "px-5 py-2.5 pb-[calc(0.625rem+env(safe-area-inset-bottom))] sm:pb-2.5 border-t fayz-glass-divider bg-transparent shrink-0", children: confirmingDelete ? /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
3235
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-destructive font-medium", children: t2("agenda.appointment.deleteConfirm") }),
3236
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
3237
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setConfirmingDelete(false), children: t2("agenda.appointment.cancel") }),
3238
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "destructive", size: "sm", onClick: handleDelete, disabled: deleting, children: deleting ? t2("agenda.appointment.deleting") : t2("agenda.appointment.confirmDelete") })
3239
+ ] })
3240
+ ] }) : /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
3241
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
3242
+ mode === "edit" && /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setConfirmingDelete(true), className: "text-xs text-destructive font-medium hover:underline", children: t2("agenda.appointment.delete") }),
3243
+ services.length > 0 && /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [
3244
+ /* @__PURE__ */ jsx(Clock, { className: "h-3 w-3" }),
3245
+ totalDuration,
3246
+ "min",
3247
+ config.modules.financial && totalPrice > 0 && mode === "edit" ? paymentStatusLoading ? /* @__PURE__ */ jsx("span", { className: "h-4 w-16 rounded-full bg-muted animate-pulse" }) : isPaidBooking ? /* @__PURE__ */ jsxs(
3248
+ "button",
3249
+ {
3250
+ type: "button",
3251
+ onClick: (e) => {
3252
+ e.stopPropagation();
3253
+ setModalTab("financial");
3254
+ },
3255
+ className: "inline-flex items-center gap-1 rounded-full bg-success/10 px-2 py-0.5 text-[10px] font-medium text-success hover:bg-success/20 transition-colors",
3256
+ children: [
3257
+ /* @__PURE__ */ jsx(Check, { className: "h-2.5 w-2.5" }),
3258
+ fmtCurrency(totalPrice, currency.locale, currency.code),
3259
+ " \xB7 ",
3260
+ t2("agenda.payment.paid")
3261
+ ]
3262
+ }
3263
+ ) : date <= todayLocal() ? /* @__PURE__ */ jsxs(
3264
+ "button",
3265
+ {
3266
+ type: "button",
3267
+ onClick: (e) => {
3268
+ e.stopPropagation();
3269
+ setModalTab("financial");
3270
+ },
3271
+ className: "inline-flex items-center gap-1 rounded-full bg-warning/10 px-2 py-0.5 text-[10px] font-medium text-warning hover:bg-warning/20 transition-colors",
3272
+ children: [
3273
+ /* @__PURE__ */ jsx(HandCoins, { className: "h-3 w-3" }),
3274
+ fmtCurrency(totalPrice, currency.locale, currency.code),
3275
+ " \xB7 ",
3276
+ t2("agenda.payment.collect")
3277
+ ]
3278
+ }
3279
+ ) : /* @__PURE__ */ jsxs("span", { children: [
3280
+ "\xB7 ",
3281
+ fmtCurrency(totalPrice, currency.locale, currency.code)
3282
+ ] }) : /* @__PURE__ */ jsxs("span", { children: [
3283
+ "\xB7 ",
3284
+ fmtCurrency(totalPrice, currency.locale, currency.code)
3285
+ ] })
3286
+ ] })
3287
+ ] }),
3288
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
3289
+ mode === "edit" && date === todayLocal() && (status === "scheduled" || status === "confirmed") && bookingId && /* @__PURE__ */ jsxs(
2986
3290
  "button",
2987
3291
  {
2988
3292
  type: "button",
2989
- onClick: (e) => {
2990
- e.stopPropagation();
2991
- setModalTab("financial");
3293
+ disabled: markingWaiting,
3294
+ onClick: async () => {
3295
+ setMarkingWaiting(true);
3296
+ try {
3297
+ await updateBookingStatus(bookingId, "waiting");
3298
+ setStatus("waiting");
3299
+ } finally {
3300
+ setMarkingWaiting(false);
3301
+ }
2992
3302
  },
2993
- className: "inline-flex items-center gap-1 rounded-full bg-warning/10 px-2 py-0.5 text-[10px] font-medium text-warning hover:bg-warning/20 transition-colors",
3303
+ className: "inline-flex items-center gap-1.5 rounded-button border border-input px-2.5 py-1 text-xs font-medium transition-colors hover:bg-muted disabled:opacity-60",
2994
3304
  children: [
2995
- /* @__PURE__ */ jsx(HandCoins, { className: "h-3 w-3" }),
2996
- fmtCurrency(totalPrice, currency.locale, currency.code),
2997
- " \xB7 ",
2998
- t2("agenda.payment.collect")
3305
+ /* @__PURE__ */ jsx(LogIn, { className: "h-3 w-3", "aria-hidden": true }),
3306
+ markingWaiting ? t2("agenda.appointment.saving") : "Cliente aguardando"
2999
3307
  ]
3000
3308
  }
3001
- ) : /* @__PURE__ */ jsxs("span", { children: [
3002
- "\xB7 ",
3003
- fmtCurrency(totalPrice, currency.locale, currency.code)
3004
- ] }) : /* @__PURE__ */ jsxs("span", { children: [
3005
- "\xB7 ",
3006
- fmtCurrency(totalPrice, currency.locale, currency.code)
3007
- ] })
3309
+ ),
3310
+ /* @__PURE__ */ jsx(Button, { type: "submit", variant: "default", size: "sm", disabled: !canSubmit || !isDirty, children: saving ? t2("agenda.appointment.saving") : mode === "create" ? t2("agenda.appointment.save") : t2("agenda.appointment.update") })
3008
3311
  ] })
3009
- ] }),
3010
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
3011
- mode === "edit" && date === todayLocal() && (status === "scheduled" || status === "confirmed") && bookingId && /* @__PURE__ */ jsxs(
3012
- "button",
3013
- {
3014
- type: "button",
3015
- disabled: markingWaiting,
3016
- onClick: async () => {
3017
- setMarkingWaiting(true);
3018
- try {
3019
- await updateBookingStatus(bookingId, "waiting");
3020
- setStatus("waiting");
3021
- } finally {
3022
- setMarkingWaiting(false);
3023
- }
3024
- },
3025
- className: "inline-flex items-center gap-1.5 rounded-button border border-input px-2.5 py-1 text-xs font-medium transition-colors hover:bg-muted disabled:opacity-60",
3026
- children: [
3027
- /* @__PURE__ */ jsx(LogIn, { className: "h-3 w-3", "aria-hidden": true }),
3028
- markingWaiting ? t2("agenda.appointment.saving") : "Cliente aguardando"
3029
- ]
3030
- }
3031
- ),
3032
- /* @__PURE__ */ jsx(Button, { type: "submit", variant: "default", size: "sm", disabled: !canSubmit || !isDirty, children: saving ? t2("agenda.appointment.saving") : mode === "create" ? t2("agenda.appointment.save") : t2("agenda.appointment.update") })
3033
- ] })
3034
- ] }) })
3035
- ] }),
3036
- modalTab !== "appointment" && !clientEditing && /* @__PURE__ */ jsx("div", { className: "px-5 py-2.5 pb-[calc(0.625rem+env(safe-area-inset-bottom))] sm:pb-2.5 border-t fayz-glass-divider bg-transparent shrink-0", children: /* @__PURE__ */ jsx("div", { className: "flex items-center", children: /* @__PURE__ */ jsxs(
3037
- "button",
3038
- {
3039
- type: "button",
3040
- onClick: () => {
3041
- setModalTab("appointment");
3042
- setClientEditing(false);
3043
- },
3044
- className: "inline-flex items-center gap-1 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors",
3045
- children: [
3046
- /* @__PURE__ */ jsx("svg", { className: "h-3 w-3", viewBox: "0 0 16 16", fill: "none", children: /* @__PURE__ */ jsx("path", { d: "M10 12L6 8l4-4", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) }),
3047
- t2("agenda.payment.backToAppointment")
3048
- ]
3049
- }
3050
- ) }) })
3312
+ ] }) })
3313
+ ] }),
3314
+ modalTab !== "appointment" && !clientEditing && /* @__PURE__ */ jsx("div", { className: "px-5 py-2.5 pb-[calc(0.625rem+env(safe-area-inset-bottom))] sm:pb-2.5 border-t fayz-glass-divider bg-transparent shrink-0", children: /* @__PURE__ */ jsx("div", { className: "flex items-center", children: /* @__PURE__ */ jsxs(
3315
+ "button",
3316
+ {
3317
+ type: "button",
3318
+ onClick: () => {
3319
+ setModalTab("appointment");
3320
+ setClientEditing(false);
3321
+ },
3322
+ className: "inline-flex items-center gap-1 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors",
3323
+ children: [
3324
+ /* @__PURE__ */ jsx("svg", { className: "h-3 w-3", viewBox: "0 0 16 16", fill: "none", children: /* @__PURE__ */ jsx("path", { d: "M10 12L6 8l4-4", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) }),
3325
+ t2("agenda.payment.backToAppointment")
3326
+ ]
3327
+ }
3328
+ ) }) })
3329
+ ] })
3051
3330
  ] })
3052
- ] })
3053
- ]
3054
- }
3055
- ) }) }) })
3331
+ ]
3332
+ }
3333
+ ),
3334
+ scopeAsk && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 z-20 flex items-end justify-center bg-background/70 p-4 sm:items-center", children: /* @__PURE__ */ jsxs("div", { className: "w-full max-w-xs rounded-card border bg-card p-5 shadow-xl", children: [
3335
+ /* @__PURE__ */ jsx("p", { className: "text-base font-semibold", children: scopeAsk === "delete" ? t2("agenda.recurrence.scopeDeleteTitle") : t2("agenda.recurrence.scopeSaveTitle") }),
3336
+ /* @__PURE__ */ jsx("div", { className: "mt-4 space-y-3", children: ["this", "series"].map((op) => /* @__PURE__ */ jsxs("label", { className: "flex cursor-pointer items-center gap-3 text-sm", children: [
3337
+ /* @__PURE__ */ jsx(
3338
+ "input",
3339
+ {
3340
+ type: "radio",
3341
+ name: "series-scope",
3342
+ value: op,
3343
+ checked: scopeChoice === op,
3344
+ onChange: () => setScopeChoice(op),
3345
+ className: "h-4 w-4 accent-primary"
3346
+ }
3347
+ ),
3348
+ op === "this" ? t2("agenda.recurrence.scopeThis") : t2("agenda.recurrence.scopeSeries").replace("{{count}}", String(seriesSize))
3349
+ ] }, op)) }),
3350
+ /* @__PURE__ */ jsxs("div", { className: "mt-5 flex items-center justify-end gap-2", children: [
3351
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setScopeAsk(null), children: t2("agenda.appointment.cancel") }),
3352
+ /* @__PURE__ */ jsx(
3353
+ Button,
3354
+ {
3355
+ type: "button",
3356
+ size: "sm",
3357
+ onClick: () => {
3358
+ void (scopeAsk === "delete" ? commitDelete(scopeChoice) : commitSave(scopeChoice));
3359
+ },
3360
+ children: t2("common.confirm")
3361
+ }
3362
+ )
3363
+ ] })
3364
+ ] }) })
3365
+ ] }) }) })
3056
3366
  );
3057
3367
  }
3058
3368
  var STYLE_ID2 = "saas-floating-panel";
@@ -3068,7 +3378,7 @@ if (typeof document !== "undefined" && !document.getElementById(STYLE_ID2)) {
3068
3378
  document.head.appendChild(s);
3069
3379
  }
3070
3380
  function FloatingPanel({
3071
- position,
3381
+ position: position2,
3072
3382
  width,
3073
3383
  height,
3074
3384
  offset,
@@ -3119,8 +3429,8 @@ function FloatingPanel({
3119
3429
  }, [dismiss]);
3120
3430
  const ox = offset?.x ?? 0;
3121
3431
  const oy = offset?.y ?? 0;
3122
- let x = position.x + ox;
3123
- let y = position.y + oy;
3432
+ let x = position2.x + ox;
3433
+ let y = position2.y + oy;
3124
3434
  if (x + width > window.innerWidth - 12) x = x - width - ox * 2;
3125
3435
  if (y + height > window.innerHeight - 12) y = y - height - oy * 2;
3126
3436
  if (x < 12) x = 12;
@@ -3139,6 +3449,60 @@ function FloatingPanel({
3139
3449
  document.body
3140
3450
  );
3141
3451
  }
3452
+ function useSeriesDelete(booking, enabled, onDone) {
3453
+ const provider2 = useAgendaProvider();
3454
+ const deleteBooking = useAgendaStore((s) => s.deleteBooking);
3455
+ const deleteBookingSeries = useAgendaStore((s) => s.deleteBookingSeries);
3456
+ const [seriesSize, setSeriesSize] = useState(null);
3457
+ const [deleting, setDeleting] = useState(null);
3458
+ const seriesId = booking.seriesId ?? null;
3459
+ useEffect(() => {
3460
+ if (!enabled || !seriesId || !provider2.countSeriesOccurrences) return;
3461
+ let alive = true;
3462
+ provider2.countSeriesOccurrences(seriesId).then((n) => {
3463
+ if (alive) setSeriesSize(n);
3464
+ }).catch(() => {
3465
+ if (alive) setSeriesSize(0);
3466
+ });
3467
+ return () => {
3468
+ alive = false;
3469
+ };
3470
+ }, [enabled, seriesId, provider2]);
3471
+ const isSeries = Boolean(seriesId) && seriesSize !== 1 && seriesSize !== 0;
3472
+ async function remove(scope) {
3473
+ setDeleting(scope);
3474
+ try {
3475
+ if (scope === "series" && seriesId) await deleteBookingSeries(seriesId);
3476
+ else await deleteBooking(booking.id);
3477
+ onDone();
3478
+ } catch {
3479
+ setDeleting(null);
3480
+ }
3481
+ }
3482
+ return { isSeries, seriesSize, deleting, remove };
3483
+ }
3484
+ function SeriesDeleteChoice({ seriesSize, deleting, onPick, onCancel }) {
3485
+ const t2 = useTranslation();
3486
+ const allLabel = t2("agenda.recurrence.scopeSeries").replace("{{count}}", seriesSize ? String(seriesSize) : "\u2026");
3487
+ const btn = "w-full rounded px-2 py-1 text-left text-[11px] font-medium text-destructive hover:bg-destructive/10 disabled:opacity-40 transition-colors";
3488
+ return /* @__PURE__ */ jsxs("div", { className: "px-3 py-2 space-y-1", children: [
3489
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
3490
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] font-medium text-destructive", children: t2("agenda.recurrence.scopeDeleteTitle") }),
3491
+ /* @__PURE__ */ jsx(
3492
+ "button",
3493
+ {
3494
+ type: "button",
3495
+ onClick: onCancel,
3496
+ disabled: deleting !== null,
3497
+ className: "rounded px-1.5 py-0.5 text-[11px] font-medium text-muted-foreground hover:bg-muted/50 transition-colors",
3498
+ children: t2("agenda.appointment.cancel")
3499
+ }
3500
+ )
3501
+ ] }),
3502
+ /* @__PURE__ */ jsx("button", { type: "button", className: btn, disabled: deleting !== null, onClick: () => onPick("this"), children: deleting === "this" ? t2("agenda.appointment.deleting") : t2("agenda.recurrence.scopeThis") }),
3503
+ /* @__PURE__ */ jsx("button", { type: "button", className: btn, disabled: deleting !== null, onClick: () => onPick("series"), children: deleting === "series" ? t2("agenda.appointment.deleting") : allLabel })
3504
+ ] });
3505
+ }
3142
3506
  function fmtTime(iso) {
3143
3507
  return new Date(iso).toLocaleTimeString(void 0, { hour: "2-digit", minute: "2-digit" });
3144
3508
  }
@@ -3199,28 +3563,29 @@ function PopoverStatusSelect({ value, statuses, bookingStartsAt, onChange }) {
3199
3563
  }) })
3200
3564
  ] });
3201
3565
  }
3202
- function AppointmentPopover({ booking, position, onClose, onEdit }) {
3566
+ function AppointmentPopover({ booking, position: position2, onClose, onEdit }) {
3203
3567
  const t2 = useTranslation();
3204
3568
  const config = useAgendaConfig();
3205
- const deleteBooking = useAgendaStore((s) => s.deleteBooking);
3206
3569
  const updateStatus = useAgendaStore((s) => s.updateBookingStatus);
3207
3570
  const openModal = useAgendaStore((s) => s.openAppointmentModal);
3208
3571
  const [confirmingDelete, setConfirmingDelete] = useState(false);
3209
- const [deleting, setDeleting] = useState(false);
3572
+ const series = useSeriesDelete(booking, confirmingDelete, onClose);
3573
+ const deleting = series.deleting !== null;
3210
3574
  const statusConfig = config.statuses.find((s) => s.value === booking.status);
3211
3575
  const color = statusConfig?.color ?? "#6b7280";
3212
3576
  const timeRange = `${fmtTime(booking.startsAt)}${booking.endsAt ? " \u2013 " + fmtTime(booking.endsAt) : ""}`;
3213
- async function handleDelete() {
3214
- setDeleting(true);
3215
- try {
3216
- await deleteBooking(booking.id);
3217
- onClose();
3218
- } catch {
3219
- setDeleting(false);
3220
- }
3221
- }
3222
- return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx(FloatingPanel, { position, width: 320, height: 340, onClose, children: (panel) => /* @__PURE__ */ jsxs(Fragment, { children: [
3223
- /* @__PURE__ */ jsx("div", { className: "flex items-center justify-end gap-0.5 px-3 pt-3 pb-1", children: confirmingDelete ? /* @__PURE__ */ jsxs(Fragment, { children: [
3577
+ return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx(FloatingPanel, { position: position2, width: 320, height: 340, onClose, children: (panel) => /* @__PURE__ */ jsxs(Fragment, { children: [
3578
+ /* @__PURE__ */ jsx("div", { className: "flex items-center justify-end gap-0.5 px-3 pt-3 pb-1", children: confirmingDelete && series.isSeries ? /* @__PURE__ */ jsx("div", { className: "w-full", children: /* @__PURE__ */ jsx(
3579
+ SeriesDeleteChoice,
3580
+ {
3581
+ seriesSize: series.seriesSize,
3582
+ deleting: series.deleting,
3583
+ onPick: (scope) => {
3584
+ void series.remove(scope);
3585
+ },
3586
+ onCancel: () => setConfirmingDelete(false)
3587
+ }
3588
+ ) }) : confirmingDelete ? /* @__PURE__ */ jsxs(Fragment, { children: [
3224
3589
  /* @__PURE__ */ jsx("span", { className: "text-xs text-destructive font-medium mr-auto pl-1", children: t2("agenda.appointment.deleteConfirm") }),
3225
3590
  /* @__PURE__ */ jsx(
3226
3591
  "button",
@@ -3233,7 +3598,9 @@ function AppointmentPopover({ booking, position, onClose, onEdit }) {
3233
3598
  /* @__PURE__ */ jsx(
3234
3599
  "button",
3235
3600
  {
3236
- onClick: handleDelete,
3601
+ onClick: () => {
3602
+ void series.remove("this");
3603
+ },
3237
3604
  disabled: deleting,
3238
3605
  className: "rounded bg-destructive border border-destructive px-2 py-0.5 text-[11px] font-medium text-destructive-foreground hover:bg-destructive/90 shadow-button-primary active:shadow-button-inset disabled:opacity-40 transition-colors",
3239
3606
  children: deleting ? t2("agenda.appointment.deleting") : t2("agenda.appointment.confirmDelete")
@@ -3388,23 +3755,14 @@ function StatusDot({ color, label, isActive, disabled, onClick }) {
3388
3755
  hover && rect && /* @__PURE__ */ jsx(Tooltip2, { anchor: rect, label })
3389
3756
  ] });
3390
3757
  }
3391
- function EventContextMenu({ booking, position, onClose, onEdit }) {
3758
+ function EventContextMenu({ booking, position: position2, onClose, onEdit }) {
3392
3759
  const t2 = useTranslation();
3393
3760
  const config = useAgendaConfig();
3394
3761
  const updateStatus = useAgendaStore((s) => s.updateBookingStatus);
3395
- const deleteBooking = useAgendaStore((s) => s.deleteBooking);
3396
3762
  const [confirming, setConfirming] = useState(false);
3397
- const [deleting, setDeleting] = useState(false);
3398
- async function handleDelete() {
3399
- setDeleting(true);
3400
- try {
3401
- await deleteBooking(booking.id);
3402
- onClose();
3403
- } catch {
3404
- setDeleting(false);
3405
- }
3406
- }
3407
- return /* @__PURE__ */ jsx(FloatingPanel, { position, width: 230, height: 180, offset: { x: 4, y: 4 }, onClose, zIndex: 110, children: (panel) => /* @__PURE__ */ jsxs(Fragment, { children: [
3763
+ const series = useSeriesDelete(booking, confirming, onClose);
3764
+ const deleting = series.deleting !== null;
3765
+ return /* @__PURE__ */ jsx(FloatingPanel, { position: position2, width: 230, height: 180, offset: { x: 4, y: 4 }, onClose, zIndex: 110, children: (panel) => /* @__PURE__ */ jsxs(Fragment, { children: [
3408
3766
  /* @__PURE__ */ jsxs("div", { className: "py-1", children: [
3409
3767
  /* @__PURE__ */ jsxs(
3410
3768
  "button",
@@ -3418,7 +3776,17 @@ function EventContextMenu({ booking, position, onClose, onEdit }) {
3418
3776
  ]
3419
3777
  }
3420
3778
  ),
3421
- confirming ? /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-4 py-2", children: [
3779
+ confirming && series.isSeries ? /* @__PURE__ */ jsx(
3780
+ SeriesDeleteChoice,
3781
+ {
3782
+ seriesSize: series.seriesSize,
3783
+ deleting: series.deleting,
3784
+ onPick: (scope) => {
3785
+ void series.remove(scope);
3786
+ },
3787
+ onCancel: () => setConfirming(false)
3788
+ }
3789
+ ) : confirming ? /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-4 py-2", children: [
3422
3790
  /* @__PURE__ */ jsx("span", { className: "text-xs text-destructive font-medium", children: t2("agenda.appointment.deleteConfirm") }),
3423
3791
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
3424
3792
  /* @__PURE__ */ jsx(
@@ -3432,7 +3800,9 @@ function EventContextMenu({ booking, position, onClose, onEdit }) {
3432
3800
  /* @__PURE__ */ jsx(
3433
3801
  "button",
3434
3802
  {
3435
- onClick: handleDelete,
3803
+ onClick: () => {
3804
+ void series.remove("this");
3805
+ },
3436
3806
  disabled: deleting,
3437
3807
  className: "rounded bg-destructive border border-destructive px-2 py-0.5 text-[11px] font-medium text-destructive-foreground hover:bg-destructive/90 shadow-button-primary active:shadow-button-inset disabled:opacity-40 transition-colors",
3438
3808
  children: deleting ? t2("agenda.appointment.deleting") : t2("agenda.appointment.confirmDelete")
@@ -3620,6 +3990,21 @@ function bookingToEvent(booking, statusColors, visibleResourceIds = []) {
3620
3990
  extendedProps: { booking, serviceNames }
3621
3991
  };
3622
3992
  }
3993
+
3994
+ // src/lib/turn-queue-presence.ts
3995
+ var leaders = /* @__PURE__ */ new Map();
3996
+ var listeners = /* @__PURE__ */ new Set();
3997
+ function publishTurnQueueLeader(unitId, professionalId) {
3998
+ leaders.set(unitId, professionalId);
3999
+ for (const listener of listeners) listener(unitId, professionalId);
4000
+ }
4001
+ function getTurnQueueLeader(unitId) {
4002
+ return unitId ? leaders.get(unitId) ?? null : null;
4003
+ }
4004
+ function subscribeTurnQueueLeader(listener) {
4005
+ listeners.add(listener);
4006
+ return () => listeners.delete(listener);
4007
+ }
3623
4008
  function localDateTimeString(d) {
3624
4009
  const pad = (n) => String(n).padStart(2, "0");
3625
4010
  return `${localDateKey(d)}T${pad(d.getHours())}:${pad(d.getMinutes())}:00`;
@@ -3840,9 +4225,9 @@ function CalendarView() {
3840
4225
  const [serviceFilter, setServiceFilter] = useState([]);
3841
4226
  const [hideEmptyGrids, setHideEmptyGrids] = useState(false);
3842
4227
  const [schedulesOpen, setSchedulesOpen] = useState(false);
3843
- const [blockFor, setBlockFor] = React20.useState(null);
3844
- const [schedulesFor, setSchedulesFor] = React20.useState(null);
3845
- const [editingBlock, setEditingBlock] = React20.useState(null);
4228
+ const [blockFor, setBlockFor] = React21.useState(null);
4229
+ const [schedulesFor, setSchedulesFor] = React21.useState(null);
4230
+ const [editingBlock, setEditingBlock] = React21.useState(null);
3846
4231
  const isMobile = useIsMobile();
3847
4232
  const resourceCapable = professionals.length > 0;
3848
4233
  const viewOptions = resourceCapable ? RESOURCE_VIEW_OPTIONS : PLAIN_VIEW_OPTIONS;
@@ -3909,6 +4294,13 @@ function CalendarView() {
3909
4294
  return map;
3910
4295
  }, [config.statuses]);
3911
4296
  const activeUnitId = useActiveUnitId();
4297
+ const [nextProfessionalId, setNextProfessionalId] = useState(() => getTurnQueueLeader(activeUnitId));
4298
+ useEffect(() => {
4299
+ setNextProfessionalId(getTurnQueueLeader(activeUnitId));
4300
+ return subscribeTurnQueueLeader((unitId, professionalId) => {
4301
+ if (unitId === activeUnitId) setNextProfessionalId(professionalId);
4302
+ });
4303
+ }, [activeUnitId]);
3912
4304
  useEffect(() => {
3913
4305
  fetchProfessionals();
3914
4306
  if (config.modules.locationSelection) fetchLocations();
@@ -4696,18 +5088,22 @@ function CalendarView() {
4696
5088
  const { id, title, extendedProps } = arg.resource;
4697
5089
  return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 px-1.5 py-1.5 min-w-0", children: [
4698
5090
  /* @__PURE__ */ jsx(PersonAvatar, { name: title, src: extendedProps?.avatarUrl, size: "xs" }),
4699
- /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1 overflow-hidden", children: /* @__PURE__ */ jsx(
4700
- PersonLink,
4701
- {
4702
- personId: id,
4703
- name: abbreviatePersonName(title),
4704
- size: "sm",
4705
- tab: "schedule",
4706
- profileHref: `#/registry/staff/${id}/schedule`
4707
- }
4708
- ) })
5091
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-1 overflow-hidden", children: [
5092
+ /* @__PURE__ */ jsx(
5093
+ PersonLink,
5094
+ {
5095
+ personId: id,
5096
+ name: abbreviatePersonName(title),
5097
+ avatarUrl: extendedProps?.avatarUrl,
5098
+ size: "sm",
5099
+ tab: "schedule",
5100
+ profileHref: `#/registry/staff/${id}/schedule`
5101
+ }
5102
+ ),
5103
+ id === nextProfessionalId ? /* @__PURE__ */ jsx(Badge, { className: "shrink-0 border-foreground/10 bg-foreground px-1.5 py-0 text-[9px] leading-4 text-background", children: "Pr\xF3ximo" }) : null
5104
+ ] })
4709
5105
  ] });
4710
- }, []);
5106
+ }, [nextProfessionalId]);
4711
5107
  const eventContent = useCallback((arg) => {
4712
5108
  if (arg.event.extendedProps?.isLocationLabel) {
4713
5109
  return /* @__PURE__ */ jsx("div", { className: "px-1.5 py-0.5 overflow-hidden", children: /* @__PURE__ */ jsx("p", { className: "text-[10px] font-medium truncate", children: arg.event.title }) });
@@ -5174,7 +5570,7 @@ function getAgendaTenantId() {
5174
5570
  function AgendaPage({ config, provider: provider2, store, children }) {
5175
5571
  const runtime = usePluginRuntimeOptional();
5176
5572
  const tenantId = runtime?.context.tenant?.id;
5177
- React20.useEffect(() => {
5573
+ React21.useEffect(() => {
5178
5574
  setAgendaTenantId(tenantId);
5179
5575
  return () => setAgendaTenantId(void 0);
5180
5576
  }, [tenantId]);
@@ -5190,10 +5586,10 @@ function CheckinDialog({
5190
5586
  onClose,
5191
5587
  formatCurrency: formatCurrency2 = brl
5192
5588
  }) {
5193
- const [busy, setBusy] = React20.useState(false);
5194
- const [preenchendo, setPreenchendo] = React20.useState(null);
5195
- const [preenchidos, setPreenchidos] = React20.useState(0);
5196
- const [pendencias, setPendencias] = React20.useState([]);
5589
+ const [busy, setBusy] = React21.useState(false);
5590
+ const [preenchendo, setPreenchendo] = React21.useState(null);
5591
+ const [preenchidos, setPreenchidos] = React21.useState(0);
5592
+ const [pendencias, setPendencias] = React21.useState([]);
5197
5593
  const blocking = requirements.filter((r) => r.blocking);
5198
5594
  const paid = booking.paymentStatus === "paid";
5199
5595
  const hasBill = !!booking.paymentStatus && booking.paymentStatus !== "none";
@@ -5303,7 +5699,7 @@ function CheckinDialog({
5303
5699
  }
5304
5700
  function PaymentStatusBadge(props) {
5305
5701
  const t2 = useTranslation();
5306
- const labels = React20.useMemo(() => ({
5702
+ const labels = React21.useMemo(() => ({
5307
5703
  none: t2("agenda.payment.title"),
5308
5704
  pending: t2("agenda.payment.pending"),
5309
5705
  partial: t2("agenda.payment.partial"),
@@ -5463,7 +5859,7 @@ function CheckinView({ onReceive, onOpenBooking, onOpenAttendance }) {
5463
5859
  const config = useAgendaConfig();
5464
5860
  const provider2 = useAgendaProvider();
5465
5861
  const statusLabel = (v) => config.statuses.find((x) => x.value === v)?.label ?? v;
5466
- const checkinStatuses = React20.useMemo(
5862
+ const checkinStatuses = React21.useMemo(
5467
5863
  () => CHECKIN_STATUSES.map((v) => config.statuses.find((x) => x.value === v)).filter((x) => !!x),
5468
5864
  [config.statuses]
5469
5865
  );
@@ -5471,14 +5867,14 @@ function CheckinView({ onReceive, onOpenBooking, onOpenAttendance }) {
5471
5867
  const loading = useAgendaStore((s) => s.bookingsLoading);
5472
5868
  const fetchBookings = useAgendaStore((s) => s.fetchBookings);
5473
5869
  const updateBookingStatus = useAgendaStore((s) => s.updateBookingStatus);
5474
- const [term, setTerm] = React20.useState("");
5475
- const [busyId, setBusyId] = React20.useState(null);
5476
- const [myPersonId, setMyPersonId] = React20.useState(void 0);
5477
- const [startRefusal, setStartRefusal] = React20.useState(null);
5478
- const [checkouts, setCheckouts] = React20.useState([]);
5479
- const [conferindo, setConferindo] = React20.useState(null);
5480
- const [checkoutRefusal, setCheckoutRefusal] = React20.useState(null);
5481
- const [panel, setPanel] = React20.useState(null);
5870
+ const [term, setTerm] = React21.useState("");
5871
+ const [busyId, setBusyId] = React21.useState(null);
5872
+ const [myPersonId, setMyPersonId] = React21.useState(void 0);
5873
+ const [startRefusal, setStartRefusal] = React21.useState(null);
5874
+ const [checkouts, setCheckouts] = React21.useState([]);
5875
+ const [conferindo, setConferindo] = React21.useState(null);
5876
+ const [checkoutRefusal, setCheckoutRefusal] = React21.useState(null);
5877
+ const [panel, setPanel] = React21.useState(null);
5482
5878
  const reviewing = panel?.kind === "review" ? panel.booking : null;
5483
5879
  const editing = panel?.kind === "edit" ? panel.booking : null;
5484
5880
  const charging = panel?.kind === "charge" ? panel.booking : null;
@@ -5487,17 +5883,17 @@ function CheckinView({ onReceive, onOpenBooking, onOpenAttendance }) {
5487
5883
  const setCharging = (b) => setPanel(b ? { kind: "charge", booking: b } : null);
5488
5884
  const can = usePermissionOptional();
5489
5885
  const canReceive = can("financial", "create") || can("financial.receivables", "create");
5490
- React20.useEffect(() => {
5886
+ React21.useEffect(() => {
5491
5887
  void fetchBookings(todayRange(), [...CHECKIN_STATUSES]);
5492
5888
  }, [fetchBookings]);
5493
- const [shownStatuses, setShownStatuses] = React20.useState([]);
5494
- const [shownPros, setShownPros] = React20.useState([]);
5495
- const prosToday = React20.useMemo(() => {
5889
+ const [shownStatuses, setShownStatuses] = React21.useState([]);
5890
+ const [shownPros, setShownPros] = React21.useState([]);
5891
+ const prosToday = React21.useMemo(() => {
5496
5892
  const seen = /* @__PURE__ */ new Map();
5497
5893
  for (const b of bookings) if (b.professionalName) seen.set(b.professionalName, b.professionalName);
5498
5894
  return [...seen.keys()].sort();
5499
5895
  }, [bookings]);
5500
- const cards = React20.useMemo(() => {
5896
+ const cards = React21.useMemo(() => {
5501
5897
  const q = term.trim().toLowerCase();
5502
5898
  return bookings.filter((b) => !shownStatuses.length || shownStatuses.includes(b.status)).filter((b) => !shownPros.length || b.professionalName && shownPros.includes(b.professionalName)).filter((b) => !q || (b.clientName ?? "").toLowerCase().includes(q) || (b.professionalName ?? "").toLowerCase().includes(q)).sort((a, b) => a.startsAt.localeCompare(b.startsAt));
5503
5899
  }, [bookings, term, shownStatuses, shownPros]);
@@ -5510,8 +5906,8 @@ function CheckinView({ onReceive, onOpenBooking, onOpenAttendance }) {
5510
5906
  return out;
5511
5907
  }
5512
5908
  const money = (n) => n.toLocaleString("pt-BR", { style: "currency", currency: "BRL" });
5513
- const [lost, setLost] = React20.useState({ cancelled: 0, noShow: 0 });
5514
- React20.useEffect(() => {
5909
+ const [lost, setLost] = React21.useState({ cancelled: 0, noShow: 0 });
5910
+ React21.useEffect(() => {
5515
5911
  if (!provider2.getLostCounts) return;
5516
5912
  let cancelado = false;
5517
5913
  provider2.getLostCounts(todayRange()).then((c) => {
@@ -5522,7 +5918,7 @@ function CheckinView({ onReceive, onOpenBooking, onOpenAttendance }) {
5522
5918
  cancelado = true;
5523
5919
  };
5524
5920
  }, [provider2]);
5525
- const openAmount = React20.useMemo(
5921
+ const openAmount = React21.useMemo(
5526
5922
  () => bookings.filter((b) => b.orderTotal && b.paymentStatus && b.paymentStatus !== "paid").reduce((soma, b) => soma + (b.orderTotal ?? 0), 0),
5527
5923
  [bookings]
5528
5924
  );
@@ -5534,7 +5930,7 @@ function CheckinView({ onReceive, onOpenBooking, onOpenAttendance }) {
5534
5930
  amount: m.paidAmount
5535
5931
  }));
5536
5932
  }
5537
- React20.useEffect(() => {
5933
+ React21.useEffect(() => {
5538
5934
  let vivo = true;
5539
5935
  (async () => {
5540
5936
  try {
@@ -5548,17 +5944,17 @@ function CheckinView({ onReceive, onOpenBooking, onOpenAttendance }) {
5548
5944
  vivo = false;
5549
5945
  };
5550
5946
  }, []);
5551
- const recarregarCheckouts = React20.useCallback(async () => {
5947
+ const recarregarCheckouts = React21.useCallback(async () => {
5552
5948
  try {
5553
5949
  setCheckouts(await provider2.getPendingCheckouts?.() ?? []);
5554
5950
  } catch {
5555
5951
  setCheckouts([]);
5556
5952
  }
5557
5953
  }, []);
5558
- React20.useEffect(() => {
5954
+ React21.useEffect(() => {
5559
5955
  void recarregarCheckouts();
5560
5956
  }, [recarregarCheckouts]);
5561
- React20.useEffect(() => {
5957
+ React21.useEffect(() => {
5562
5958
  const parar = provider2.subscribeToBookingChanges?.(() => {
5563
5959
  void recarregarCheckouts();
5564
5960
  void fetchBookings(todayRange(), [...CHECKIN_STATUSES]);
@@ -5941,7 +6337,7 @@ function CheckinView({ onReceive, onOpenBooking, onOpenAttendance }) {
5941
6337
  function CheckinPage({ config, provider: provider2, store, onReceive, onOpenBooking }) {
5942
6338
  const runtime = usePluginRuntimeOptional();
5943
6339
  const tenantId = runtime?.context.tenant?.id;
5944
- React20.useEffect(() => {
6340
+ React21.useEffect(() => {
5945
6341
  setAgendaTenantId(tenantId);
5946
6342
  return () => setAgendaTenantId(void 0);
5947
6343
  }, [tenantId]);
@@ -5954,11 +6350,11 @@ function hoje2() {
5954
6350
  }
5955
6351
  function PersonAttendanceBlock({ personId }) {
5956
6352
  const provider2 = useAgendaProvider();
5957
- const [pessoa, setPessoa] = React20.useState(void 0);
5958
- const [booking, setBooking] = React20.useState(null);
5959
- const [indo, setIndo] = React20.useState(false);
5960
- const [recusa, setRecusa] = React20.useState(null);
5961
- React20.useEffect(() => {
6353
+ const [pessoa, setPessoa] = React21.useState(void 0);
6354
+ const [booking, setBooking] = React21.useState(null);
6355
+ const [indo, setIndo] = React21.useState(false);
6356
+ const [recusa, setRecusa] = React21.useState(null);
6357
+ React21.useEffect(() => {
5962
6358
  let vivo = true;
5963
6359
  (async () => {
5964
6360
  const eu = await provider2.getCurrentPersonId?.() ?? null;
@@ -6465,6 +6861,14 @@ function addMinutes(iso, minutes) {
6465
6861
  d.setMinutes(d.getMinutes() + minutes);
6466
6862
  return d.toISOString();
6467
6863
  }
6864
+ function localTimeOf(iso) {
6865
+ const d = new Date(iso);
6866
+ return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
6867
+ }
6868
+ function localDayOf(iso) {
6869
+ const d = new Date(iso);
6870
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
6871
+ }
6468
6872
  function unique(values) {
6469
6873
  return Array.from(new Set(values.filter((value) => !!value)));
6470
6874
  }
@@ -6561,7 +6965,7 @@ async function hydrateCanonicalBookings(core, bookings) {
6561
6965
  const itemsByOrderId = groupBy(items ?? [], (item) => item.order_id);
6562
6966
  const itemsByBookingId = groupBy(bookingItemRows ?? [], (item) => item.booking_id);
6563
6967
  return bookings.map((booking) => {
6564
- const client = booking.party_id ? personsById.get(booking.party_id) : null;
6968
+ const client2 = booking.party_id ? personsById.get(booking.party_id) : null;
6565
6969
  const professional = booking.assignee_id ? personsById.get(booking.assignee_id) : null;
6566
6970
  const location = booking.location_id ? locationsById.get(booking.location_id) : null;
6567
6971
  const room = booking.resource_id ? roomsById.get(booking.resource_id) : null;
@@ -6598,16 +7002,17 @@ async function hydrateCanonicalBookings(core, bookings) {
6598
7002
  // Simple events (no party) fall back to the title stored in metadata;
6599
7003
  // partyless blocks/tasks (e.g. imported Google events) get their notes or
6600
7004
  // a kind label so the calendar never renders an unnamed chip.
6601
- clientName: client?.name ?? booking.metadata?.title ?? (booking.kind === "appointment" ? null : booking.notes || (booking.kind === "task" ? "Tarefa" : "Bloqueio")),
6602
- clientPhone: client?.phone ?? null,
6603
- clientEmail: client?.email ?? null,
6604
- clientAvatarUrl: client?.avatar_url ?? null,
7005
+ clientName: client2?.name ?? booking.metadata?.title ?? (booking.kind === "appointment" ? null : booking.notes || (booking.kind === "task" ? "Tarefa" : "Bloqueio")),
7006
+ clientPhone: client2?.phone ?? null,
7007
+ clientEmail: client2?.email ?? null,
7008
+ clientAvatarUrl: client2?.avatar_url ?? null,
6605
7009
  professionalId: booking.assignee_id,
6606
7010
  professionalName: professional?.name ?? null,
6607
7011
  professionalAvatarUrl: professional?.avatar_url ?? null,
6608
7012
  locationName: location?.name ?? null,
6609
7013
  resourceId: booking.resource_id ?? null,
6610
7014
  resourceName: room?.name ?? null,
7015
+ seriesId: booking.series_id ?? null,
6611
7016
  orderTotal: order?.total ?? null,
6612
7017
  orderStatus: order?.status ?? null,
6613
7018
  services: bookingItems.length ? bookingItems : null,
@@ -6619,7 +7024,7 @@ async function hydrateCanonicalBookings(core, bookings) {
6619
7024
  });
6620
7025
  }
6621
7026
  async function getCanonicalBookings(core, query) {
6622
- let q = core.from("appointments").select("id, tenant_id, kind, starts_at, ends_at, status, notes, order_id, location_id, resource_id, metadata, party_id, assignee_id, checked_in_at, created_at, updated_at").gte("starts_at", query.dateRange.start).lte("starts_at", query.dateRange.end);
7027
+ let q = core.from("appointments").select("id, tenant_id, kind, starts_at, ends_at, status, notes, order_id, location_id, resource_id, series_id, metadata, party_id, assignee_id, checked_in_at, created_at, updated_at").gte("starts_at", query.dateRange.start).lte("starts_at", query.dateRange.end);
6623
7028
  if (query.kind) q = q.eq("kind", query.kind);
6624
7029
  if (query.statuses?.length) q = q.in("status", query.statuses);
6625
7030
  if (query.professionalIds?.length) q = q.in("assignee_id", query.professionalIds);
@@ -6629,7 +7034,7 @@ async function getCanonicalBookings(core, query) {
6629
7034
  return hydrateCanonicalBookings(core, data ?? []);
6630
7035
  }
6631
7036
  async function getCanonicalBookingById(core, id) {
6632
- const columns = "id, tenant_id, kind, starts_at, ends_at, status, notes, order_id, location_id, resource_id, metadata, party_id, assignee_id, checked_in_at, created_at, updated_at";
7037
+ const columns = "id, tenant_id, kind, starts_at, ends_at, status, notes, order_id, location_id, resource_id, series_id, metadata, party_id, assignee_id, checked_in_at, created_at, updated_at";
6633
7038
  let { data, error } = await core.from("appointments").select(columns).eq("id", id).maybeSingle();
6634
7039
  if (error) throw error;
6635
7040
  if (!data) {
@@ -6669,6 +7074,16 @@ async function sincronizarConvidados(core, bookingId, tenantId, attendees) {
6669
7074
  );
6670
7075
  if (error) throw error;
6671
7076
  }
7077
+ async function emParalelo(itens, k, fn) {
7078
+ let i = 0;
7079
+ const trilhos = Array.from({ length: Math.min(k, itens.length) }, async () => {
7080
+ while (i < itens.length) {
7081
+ const item = itens[i++];
7082
+ await fn(item);
7083
+ }
7084
+ });
7085
+ await Promise.all(trilhos);
7086
+ }
6672
7087
  function createSupabaseAgendaProvider(providerOptions) {
6673
7088
  const professionalKind = providerOptions?.professionalKind ?? "staff";
6674
7089
  return {
@@ -6805,6 +7220,160 @@ function createSupabaseAgendaProvider(providerOptions) {
6805
7220
  if (!createdBooking) throw new Error("Created booking did not resolve from agenda read model");
6806
7221
  return createdBooking;
6807
7222
  },
7223
+ // -----------------------------------------------------------------------
7224
+ // RECORRÊNCIA — "Repetir"
7225
+ //
7226
+ // A série é MATERIALIZADA: a regra é gravada e cada ocorrência nasce pela
7227
+ // MESMA porta de sempre (`createBooking` → `agenda_booking_create`), uma de
7228
+ // cada vez. Nada aqui insere em `appointments` por fora: o pedido, as
7229
+ // linhas e o compromisso continuam saindo de uma transação só, e uma
7230
+ // segunda porta de criação seria uma segunda regra de negócio a manter.
7231
+ //
7232
+ // O preço é N chamadas para N ocorrências. Aceito, e limitado: a expansão
7233
+ // tem teto (`RECURRENCE_MAX_OCCURRENCES`), e o laço RELATA o que falhou em
7234
+ // vez de parar no meio e deixar a pessoa sem saber quantas nasceram.
7235
+ // -----------------------------------------------------------------------
7236
+ async createRecurringBookings(input, rule, dates) {
7237
+ const { core } = getClients();
7238
+ const tenantId = getTenantId();
7239
+ if (!tenantId) throw new Error("No active tenant");
7240
+ if (dates.length === 0) throw new Error("A s\xE9rie n\xE3o tem nenhuma data");
7241
+ const timeOfDay = localTimeOf(input.startsAt);
7242
+ const { data, error } = await core.rpc("agenda_series_create", {
7243
+ p_days: dates,
7244
+ p_time_of_day: timeOfDay,
7245
+ p_rule: { freq: rule.freq, interval: rule.interval, until: rule.until ?? null, count: rule.count ?? null },
7246
+ // A MESMA forma de linha de `createBooking` — é o mesmo
7247
+ // `agenda_booking_create` que vai lê-la, e duas formas para o mesmo
7248
+ // parâmetro é como uma série nasce sem duração por linha.
7249
+ p_lines: (input.services ?? []).map((sv, i) => ({
7250
+ service_id: sv.serviceId,
7251
+ name: sv.name,
7252
+ quantity: 1,
7253
+ unit_price: sv.price,
7254
+ sort_order: i,
7255
+ duration_minutes: sv.durationMinutes,
7256
+ assignee_id: sv.assigneeId || input.professionalId || null
7257
+ })),
7258
+ p_party_id: input.clientId ?? null,
7259
+ p_assignee_id: input.professionalId ?? null,
7260
+ p_unit_id: getActiveUnitId() ?? null,
7261
+ p_location_id: input.locationId ?? null,
7262
+ p_resource_id: input.resourceId ?? null,
7263
+ p_kind: input.kind ?? "appointment",
7264
+ p_notes: input.notes ?? null,
7265
+ p_metadata: {
7266
+ source: "agenda",
7267
+ ...input.services?.length ? { serviceNames: input.services.map((sv) => sv.name).join(", ") } : {},
7268
+ ...input.title?.trim() ? { title: input.title.trim() } : {},
7269
+ ...input.dealId ? { dealId: input.dealId } : {}
7270
+ },
7271
+ p_attendee_ids: (input.attendees ?? []).map((a) => a.personId)
7272
+ });
7273
+ if (error) throw new Error(error.message);
7274
+ const r = data ?? {};
7275
+ return {
7276
+ seriesId: r.series_id,
7277
+ bookingIds: r.booking_ids ?? [],
7278
+ failed: r.failed ?? []
7279
+ };
7280
+ },
7281
+ async getRecurrence(seriesId) {
7282
+ const { core } = getClients();
7283
+ const { data, error } = await core.from("plg_agenda_recurrences").select("id, freq, interval_count, starts_on, time_of_day, until_date, occurrence_count").eq("id", seriesId).maybeSingle();
7284
+ if (error) throw error;
7285
+ if (!data) return null;
7286
+ const row = data;
7287
+ return {
7288
+ id: row.id,
7289
+ freq: row.freq,
7290
+ interval: row.interval_count ?? 1,
7291
+ until: row.until_date ?? null,
7292
+ count: row.occurrence_count ?? null,
7293
+ startsOn: row.starts_on,
7294
+ timeOfDay: row.time_of_day ? String(row.time_of_day).slice(0, 5) : null
7295
+ };
7296
+ },
7297
+ async countSeriesOccurrences(seriesId) {
7298
+ const { core } = getClients();
7299
+ const { count, error } = await core.from("appointments").select("id", { count: "exact", head: true }).eq("series_id", seriesId);
7300
+ if (error) throw error;
7301
+ return count ?? 0;
7302
+ },
7303
+ async updateBookingSeries(seriesId, data, timeOfDay) {
7304
+ const { core } = getClients();
7305
+ const touch = [];
7306
+ if (data.professionalId !== void 0) touch.push("assigneeId");
7307
+ if (data.locationId !== void 0) touch.push("locationId");
7308
+ if (data.resourceId !== void 0) touch.push("resourceId");
7309
+ if (data.notes !== void 0) touch.push("notes");
7310
+ if (data.status !== void 0) touch.push("status");
7311
+ const { data: res, error } = await core.rpc("agenda_series_update", {
7312
+ p_series_id: seriesId,
7313
+ p_time_of_day: timeOfDay,
7314
+ p_assignee_id: data.professionalId ?? null,
7315
+ p_location_id: data.locationId ?? null,
7316
+ p_resource_id: data.resourceId ?? null,
7317
+ p_notes: data.notes ?? null,
7318
+ p_status: data.status ?? null,
7319
+ p_touch: touch
7320
+ });
7321
+ if (error) throw new Error(error.message);
7322
+ if (data.services !== void 0) {
7323
+ const { data: rows } = await core.from("appointments").select("id").eq("series_id", seriesId);
7324
+ const ids = (rows ?? []).map((r) => r.id);
7325
+ await emParalelo(ids, 6, async (id) => {
7326
+ try {
7327
+ await this.updateBooking(id, { services: data.services });
7328
+ } catch (err) {
7329
+ console.error("[agenda] servi\xE7os da ocorr\xEAncia n\xE3o acompanharam a s\xE9rie", id, err);
7330
+ }
7331
+ });
7332
+ }
7333
+ return { updated: res?.updated ?? 0 };
7334
+ },
7335
+ async deleteBookingSeries(seriesId) {
7336
+ const { core } = getClients();
7337
+ const { data: rows, error } = await core.from("appointments").select("id").eq("series_id", seriesId);
7338
+ if (error) throw error;
7339
+ const ids = (rows ?? []).map((r) => r.id);
7340
+ let deleted = 0;
7341
+ await emParalelo(ids, 6, async (id) => {
7342
+ try {
7343
+ await this.deleteBooking(id);
7344
+ deleted += 1;
7345
+ } catch (err) {
7346
+ console.error("[agenda] ocorr\xEAncia da s\xE9rie n\xE3o foi exclu\xEDda", id, err);
7347
+ }
7348
+ });
7349
+ const { error: ruleErr } = await core.from("plg_agenda_recurrences").delete().eq("id", seriesId);
7350
+ if (ruleErr) console.error("[agenda] regra da s\xE9rie n\xE3o foi apagada", seriesId, ruleErr);
7351
+ return { deleted };
7352
+ },
7353
+ async checkSeriesConflicts(params) {
7354
+ const { windows } = params;
7355
+ if (windows.length === 0) return { busyStarts: [], closedDates: [] };
7356
+ const { core } = getClients();
7357
+ const from = windows.reduce((min, w) => w.startsAt < min ? w.startsAt : min, windows[0].startsAt);
7358
+ const to = windows.reduce((max, w) => w.endsAt > max ? w.endsAt : max, windows[0].endsAt);
7359
+ let q = core.from("appointments").select("id, starts_at, ends_at").eq("assignee_id", params.assigneeId).eq("kind", "appointment").not("status", "in", '("cancelled","no_show")').not("starts_at", "is", null).lt("starts_at", to).gt("ends_at", from);
7360
+ if (params.excludeBookingId) q = q.neq("id", params.excludeBookingId);
7361
+ const { data: busy, error } = await q;
7362
+ if (error) throw error;
7363
+ const rows = busy ?? [];
7364
+ const busyStarts = windows.filter((w) => rows.some((r) => r.ends_at && w.startsAt < r.ends_at && w.endsAt > r.starts_at)).map((w) => w.startsAt);
7365
+ let closedDates = [];
7366
+ try {
7367
+ const dias = new Set(windows.map((w) => localDayOf(w.startsAt)));
7368
+ const holidays = await this.getHolidays?.(
7369
+ { from: localDayOf(from), to: localDayOf(to) },
7370
+ params.unitId ?? null
7371
+ );
7372
+ closedDates = (holidays ?? []).filter((h) => h.closes && dias.has(h.date)).map((h) => h.date);
7373
+ } catch {
7374
+ }
7375
+ return { busyStarts, closedDates: Array.from(new Set(closedDates)) };
7376
+ },
6808
7377
  async updateBooking(id, data) {
6809
7378
  const { core } = getClients();
6810
7379
  const { data: existing, error: existingErr } = await core.from("appointments").select("id, order_id, starts_at, assignee_id, unit_id, metadata").eq("id", id).single();
@@ -7868,6 +8437,90 @@ function createAgendaStore(provider2, financialBridge) {
7868
8437
  throw err;
7869
8438
  }
7870
8439
  },
8440
+ // --- "Repetir" ------------------------------------------------------
8441
+ //
8442
+ // Três ações e não uma: criar uma série, editar a série, apagar a série.
8443
+ // Passam pelo store pelo mesmo motivo que as de uma reserva só — é ele que
8444
+ // derruba o cache de todas as faixas e redesenha a grade. Uma série de dez
8445
+ // ocorrências espalha linhas por dez semanas; recarregar só a visível
8446
+ // deixaria as outras nove servindo uma grade sem elas.
8447
+ async createBookingSeries(input, rule, dates) {
8448
+ if (!provider2.createRecurringBookings) {
8449
+ throw new Error("This agenda provider does not support recurring bookings");
8450
+ }
8451
+ try {
8452
+ const result = await provider2.createRecurringBookings(input, rule, dates);
8453
+ for (const id of result.bookingIds) {
8454
+ const booking = await provider2.getBookingById(id).catch(() => null);
8455
+ if (booking) emitBookingEvent("agenda.booking_created", booking);
8456
+ }
8457
+ invalidateBookings();
8458
+ const state = get();
8459
+ if (state.dateRange.start && state.dateRange.end) {
8460
+ const bookings = await provider2.getBookings(getCurrentBookingQuery(state));
8461
+ set({ bookings });
8462
+ }
8463
+ emitDataChanged({ table: "appointments", op: "create", id: result.seriesId, source: "ui" });
8464
+ if (result.failed.length > 0) {
8465
+ toast.warning(
8466
+ tl(
8467
+ `${result.bookingIds.length} of ${dates.length} appointments created`,
8468
+ `${result.bookingIds.length} de ${dates.length} agendamentos criados`
8469
+ ),
8470
+ { description: result.failed.map((f) => f.date).join(", ") }
8471
+ );
8472
+ } else {
8473
+ toast.success(tl(
8474
+ `${result.bookingIds.length} appointments created`,
8475
+ `${result.bookingIds.length} agendamentos criados`
8476
+ ));
8477
+ }
8478
+ return result;
8479
+ } catch (err) {
8480
+ toast.error(tl("Failed to create the series", "N\xE3o foi poss\xEDvel criar a s\xE9rie"), { description: err?.message });
8481
+ throw err;
8482
+ }
8483
+ },
8484
+ async updateBookingSeries(seriesId, data, timeOfDay) {
8485
+ if (!provider2.updateBookingSeries) {
8486
+ throw new Error("This agenda provider does not support recurring bookings");
8487
+ }
8488
+ try {
8489
+ const { updated } = await provider2.updateBookingSeries(seriesId, data, timeOfDay);
8490
+ invalidateBookings();
8491
+ const state = get();
8492
+ if (state.dateRange.start && state.dateRange.end) {
8493
+ const bookings = await provider2.getBookings(getCurrentBookingQuery(state));
8494
+ set({ bookings });
8495
+ }
8496
+ emitDataChanged({ table: "appointments", op: "update", id: seriesId, source: "ui" });
8497
+ toast.success(tl(`${updated} appointments updated`, `${updated} agendamentos atualizados`));
8498
+ return updated;
8499
+ } catch (err) {
8500
+ toast.error(tl("Failed to update the series", "N\xE3o foi poss\xEDvel salvar a s\xE9rie"), { description: err?.message });
8501
+ throw err;
8502
+ }
8503
+ },
8504
+ async deleteBookingSeries(seriesId) {
8505
+ if (!provider2.deleteBookingSeries) {
8506
+ throw new Error("This agenda provider does not support recurring bookings");
8507
+ }
8508
+ try {
8509
+ const { deleted } = await provider2.deleteBookingSeries(seriesId);
8510
+ invalidateBookings();
8511
+ const state = get();
8512
+ if (state.dateRange.start && state.dateRange.end) {
8513
+ const bookings = await provider2.getBookings(getCurrentBookingQuery(state));
8514
+ set({ bookings });
8515
+ }
8516
+ emitDataChanged({ table: "appointments", op: "delete", id: seriesId, source: "ui" });
8517
+ toast.success(tl(`${deleted} appointments deleted`, `${deleted} agendamentos exclu\xEDdos`));
8518
+ return deleted;
8519
+ } catch (err) {
8520
+ toast.error(tl("Failed to delete the series", "N\xE3o foi poss\xEDvel excluir a s\xE9rie"), { description: err?.message });
8521
+ throw err;
8522
+ }
8523
+ },
7871
8524
  async updateBookingStatus(id, status) {
7872
8525
  const prev = get().bookings;
7873
8526
  const prevConfirmations = get().pendingConfirmations;
@@ -8018,6 +8671,11 @@ var AGENDA_TOGGLE_DEFAULTS = {
8018
8671
  // services. On by default — it only surfaces when an evaluation-kind
8019
8672
  // service is selected, so tenants without evaluations never see it.
8020
8673
  interestsOnBooking: true,
8674
+ // "Repetir" no agendamento. LIGADO: fechada, a linha é uma frase de 11px sob
8675
+ // o horário — quem nunca marca retorno fixo lê "Não se repete" e segue. Um
8676
+ // padrão desligado esconderia a função de quem não sabe que ela existe, que
8677
+ // é justamente quem precisaria achá-la. Quem não a quer desliga aqui.
8678
+ recurringBookings: true,
8021
8679
  enableLocation: false,
8022
8680
  enableWorkingHours: true,
8023
8681
  blockOutside: false,
@@ -8040,7 +8698,8 @@ function AgendaGeneralSettings() {
8040
8698
  /* @__PURE__ */ jsx(ToggleRow, { label: t2("agenda.settings.conflictDetection"), description: t2("agenda.settings.conflictDetectionDesc"), checked: s.get("conflictDetection"), onChange: (v) => s.set("conflictDetection", v) }),
8041
8699
  /* @__PURE__ */ jsx(ToggleRow, { label: t2("agenda.settings.dragDrop"), description: t2("agenda.settings.dragDropDesc"), checked: s.get("dragDrop"), onChange: (v) => s.set("dragDrop", v) }),
8042
8700
  /* @__PURE__ */ jsx(ToggleRow, { label: t2("agenda.settings.autoCreateOrder"), description: t2("agenda.settings.autoCreateOrderDesc"), checked: s.get("autoCreateOrder"), onChange: (v) => s.set("autoCreateOrder", v) }),
8043
- /* @__PURE__ */ jsx(ToggleRow, { label: t2("agenda.settings.interestsOnBooking"), description: t2("agenda.settings.interestsOnBookingDesc"), checked: s.get("interestsOnBooking"), onChange: (v) => s.set("interestsOnBooking", v) })
8701
+ /* @__PURE__ */ jsx(ToggleRow, { label: t2("agenda.settings.interestsOnBooking"), description: t2("agenda.settings.interestsOnBookingDesc"), checked: s.get("interestsOnBooking"), onChange: (v) => s.set("interestsOnBooking", v) }),
8702
+ /* @__PURE__ */ jsx(ToggleRow, { label: t2("agenda.settings.recurringBookings"), description: t2("agenda.settings.recurringBookingsDesc"), checked: s.get("recurringBookings"), onChange: (v) => s.set("recurringBookings", v) })
8044
8703
  ] }),
8045
8704
  /* @__PURE__ */ jsx(SettingsGroup, { title: t2("agenda.settings.locations"), description: t2("agenda.settings.locationsDesc"), children: /* @__PURE__ */ jsx(ToggleRow, { label: t2("agenda.settings.enableLocation"), description: t2("agenda.settings.enableLocationDesc"), checked: s.get("enableLocation"), onChange: (v) => s.set("enableLocation", v) }) }),
8046
8705
  /* @__PURE__ */ jsxs(SettingsGroup, { title: t2("agenda.settings.workingHours"), description: t2("agenda.settings.workingHoursDesc"), children: [
@@ -8156,16 +8815,16 @@ function TarefaPane({ titulo, aoVoltar, children }) {
8156
8815
  }
8157
8816
  function AttendanceRuler({ booking, open, onClose, onFinished, frame = "modal" }) {
8158
8817
  const provider2 = useAgendaProvider();
8159
- const [versao, setVersao] = React20.useState(0);
8160
- const [statusLocal, setStatusLocal] = React20.useState(booking.status);
8161
- React20.useEffect(() => {
8818
+ const [versao, setVersao] = React21.useState(0);
8819
+ const [statusLocal, setStatusLocal] = React21.useState(booking.status);
8820
+ React21.useEffect(() => {
8162
8821
  setStatusLocal(booking.status);
8163
8822
  }, [booking.id, booking.status]);
8164
8823
  const emCurso = statusLocal === "in_progress";
8165
8824
  const naoIniciado = statusLocal === "scheduled" || statusLocal === "confirmed" || statusLocal === "waiting";
8166
8825
  const somenteLeitura = !emCurso;
8167
- const [eu, setEu] = React20.useState(void 0);
8168
- React20.useEffect(() => {
8826
+ const [eu, setEu] = React21.useState(void 0);
8827
+ React21.useEffect(() => {
8169
8828
  if (!naoIniciado) return;
8170
8829
  let vivo = true;
8171
8830
  (async () => {
@@ -8176,31 +8835,31 @@ function AttendanceRuler({ booking, open, onClose, onFinished, frame = "modal" }
8176
8835
  vivo = false;
8177
8836
  };
8178
8837
  }, [naoIniciado, provider2]);
8179
- const [iniciando, setIniciando] = React20.useState(false);
8180
- const [recusaInicio, setRecusaInicio] = React20.useState(null);
8181
- const [rows, setRows] = React20.useState(null);
8182
- const [record, setRecord] = React20.useState(null);
8183
- const [erro, setErro] = React20.useState(null);
8184
- const [contratando, setContratando] = React20.useState(false);
8185
- const [servicos, setServicos] = React20.useState([]);
8186
- const [marcados, setMarcados] = React20.useState({});
8187
- const [salvando, setSalvando] = React20.useState(false);
8188
- const [confirmando, setConfirmando] = React20.useState(null);
8189
- const [encerrando, setEncerrando] = React20.useState(false);
8190
- const [avisoFinal, setAvisoFinal] = React20.useState(null);
8191
- const [preenchendo, setPreenchendo] = React20.useState(null);
8192
- const [avaliando, setAvaliando] = React20.useState(false);
8193
- const [lancando, setLancando] = React20.useState(null);
8194
- const [executando, setExecutando] = React20.useState(null);
8195
- const [vinculando, setVinculando] = React20.useState(null);
8196
- const [candidatos, setCandidatos] = React20.useState(null);
8197
- const [buscaInsumo, setBuscaInsumo] = React20.useState("");
8198
- const [insumos, setInsumos] = React20.useState([]);
8199
- const [usados, setUsados] = React20.useState([]);
8200
- const [busca, setBusca] = React20.useState("");
8201
- const [catalogo, setCatalogo] = React20.useState([]);
8202
- const [indicados, setIndicados] = React20.useState([]);
8203
- React20.useEffect(() => {
8838
+ const [iniciando, setIniciando] = React21.useState(false);
8839
+ const [recusaInicio, setRecusaInicio] = React21.useState(null);
8840
+ const [rows, setRows] = React21.useState(null);
8841
+ const [record, setRecord] = React21.useState(null);
8842
+ const [erro, setErro] = React21.useState(null);
8843
+ const [contratando, setContratando] = React21.useState(false);
8844
+ const [servicos, setServicos] = React21.useState([]);
8845
+ const [marcados, setMarcados] = React21.useState({});
8846
+ const [salvando, setSalvando] = React21.useState(false);
8847
+ const [confirmando, setConfirmando] = React21.useState(null);
8848
+ const [encerrando, setEncerrando] = React21.useState(false);
8849
+ const [avisoFinal, setAvisoFinal] = React21.useState(null);
8850
+ const [preenchendo, setPreenchendo] = React21.useState(null);
8851
+ const [avaliando, setAvaliando] = React21.useState(false);
8852
+ const [lancando, setLancando] = React21.useState(null);
8853
+ const [executando, setExecutando] = React21.useState(null);
8854
+ const [vinculando, setVinculando] = React21.useState(null);
8855
+ const [candidatos, setCandidatos] = React21.useState(null);
8856
+ const [buscaInsumo, setBuscaInsumo] = React21.useState("");
8857
+ const [insumos, setInsumos] = React21.useState([]);
8858
+ const [usados, setUsados] = React21.useState([]);
8859
+ const [busca, setBusca] = React21.useState("");
8860
+ const [catalogo, setCatalogo] = React21.useState([]);
8861
+ const [indicados, setIndicados] = React21.useState([]);
8862
+ React21.useEffect(() => {
8204
8863
  if (!open) return;
8205
8864
  let vivo = true;
8206
8865
  (async () => {
@@ -8257,7 +8916,7 @@ function AttendanceRuler({ booking, open, onClose, onFinished, frame = "modal" }
8257
8916
  const d = ORDEM.indexOf(a.kind) - ORDEM.indexOf(b.kind);
8258
8917
  return d !== 0 ? d : a.id.localeCompare(b.id);
8259
8918
  };
8260
- const doAtendimento = React20.useMemo(() => {
8919
+ const doAtendimento = React21.useMemo(() => {
8261
8920
  const m = /* @__PURE__ */ new Map();
8262
8921
  for (const r of (rows ?? []).filter((x) => x.scope === "attendance")) {
8263
8922
  const k = `${r.kind}:${r.refId ?? ""}`;
@@ -8270,7 +8929,7 @@ function AttendanceRuler({ booking, open, onClose, onFinished, frame = "modal" }
8270
8929
  }
8271
8930
  return [...m.values()].sort(porOrdem);
8272
8931
  }, [rows]);
8273
- const porServico = React20.useMemo(() => {
8932
+ const porServico = React21.useMemo(() => {
8274
8933
  const m = /* @__PURE__ */ new Map();
8275
8934
  for (const r of rows ?? []) {
8276
8935
  if (r.scope === "attendance") continue;
@@ -8295,7 +8954,7 @@ function AttendanceRuler({ booking, open, onClose, onFinished, frame = "modal" }
8295
8954
  }, [rows]);
8296
8955
  const faltaParaTerminar = (rows ?? []).filter((r) => r.blocksFinish && r.status === "pending");
8297
8956
  const tarefaAberta = Boolean(vinculando || executando || lancando || avaliando || confirmando || contratando || preenchendo?.refId && booking.clientId);
8298
- React20.useEffect(() => {
8957
+ React21.useEffect(() => {
8299
8958
  if (!avaliando) return;
8300
8959
  let vivo = true;
8301
8960
  const t2 = setTimeout(async () => {
@@ -8311,7 +8970,7 @@ function AttendanceRuler({ booking, open, onClose, onFinished, frame = "modal" }
8311
8970
  clearTimeout(t2);
8312
8971
  };
8313
8972
  }, [avaliando, busca]);
8314
- React20.useEffect(() => {
8973
+ React21.useEffect(() => {
8315
8974
  if (!lancando) return;
8316
8975
  let vivo = true;
8317
8976
  const t2 = setTimeout(async () => {
@@ -8335,7 +8994,7 @@ function AttendanceRuler({ booking, open, onClose, onFinished, frame = "modal" }
8335
8994
  return [...xs, ...kit.filter((k) => !tem.has(k.productId)).map((k) => ({ productId: k.productId, name: k.name, qtd: k.quantity, unitId: k.unitId }))];
8336
8995
  });
8337
8996
  }
8338
- React20.useEffect(() => {
8997
+ React21.useEffect(() => {
8339
8998
  if (!vinculando) return;
8340
8999
  let vivo = true;
8341
9000
  (async () => {
@@ -9051,10 +9710,10 @@ var STATUS_LABEL = {
9051
9710
  };
9052
9711
  function PersonAttendancesWidget({ personId }) {
9053
9712
  const provider2 = useAgendaProvider();
9054
- const [linhas, setLinhas] = React20.useState(null);
9055
- const [aberto, setAberto] = React20.useState(null);
9056
- const [emCurso, setEmCurso] = React20.useState(null);
9057
- const recarregar = React20.useCallback(async () => {
9713
+ const [linhas, setLinhas] = React21.useState(null);
9714
+ const [aberto, setAberto] = React21.useState(null);
9715
+ const [emCurso, setEmCurso] = React21.useState(null);
9716
+ const recarregar = React21.useCallback(async () => {
9058
9717
  try {
9059
9718
  const r = await provider2.getPersonAttendances?.(personId);
9060
9719
  setLinhas(r ?? []);
@@ -9062,10 +9721,10 @@ function PersonAttendancesWidget({ personId }) {
9062
9721
  setLinhas([]);
9063
9722
  }
9064
9723
  }, [personId, provider2]);
9065
- React20.useEffect(() => {
9724
+ React21.useEffect(() => {
9066
9725
  void recarregar();
9067
9726
  }, [recarregar]);
9068
- React20.useEffect(() => {
9727
+ React21.useEffect(() => {
9069
9728
  let vivo = true;
9070
9729
  (async () => {
9071
9730
  const eu = await provider2.getCurrentPersonId?.() ?? null;
@@ -9163,8 +9822,8 @@ function PersonAttendancesWidget({ personId }) {
9163
9822
  }
9164
9823
  function PersonServicesWidget({ personId }) {
9165
9824
  const provider2 = useAgendaProvider();
9166
- const [rows, setRows] = React20.useState(null);
9167
- React20.useEffect(() => {
9825
+ const [rows, setRows] = React21.useState(null);
9826
+ React21.useEffect(() => {
9168
9827
  let alive = true;
9169
9828
  (async () => {
9170
9829
  try {
@@ -9380,6 +10039,8 @@ var en = {
9380
10039
  "agenda.settings.autoCreateOrderDesc": "Automatically create a financial order for new bookings",
9381
10040
  "agenda.settings.interestsOnBooking": "Interests on booking",
9382
10041
  "agenda.settings.interestsOnBookingDesc": "When scheduling an evaluation, show the client's open interests and let the front desk add new ones \u2014 they feed the CRM funnel, not the bill",
10042
+ "agenda.settings.recurringBookings": "Repeating appointments",
10043
+ "agenda.settings.recurringBookingsDesc": "Lets an appointment become a series \u2014 weekly, every other week, monthly. Each occurrence is born a real appointment, with its own conflict check, check-in and order",
9383
10044
  "agenda.settings.blockOutside": "Block Outside Hours",
9384
10045
  "agenda.settings.blockOutsideDesc": "Prevent bookings outside working hours",
9385
10046
  "agenda.settings.calendarDisplay": "Calendar Display",
@@ -9507,7 +10168,35 @@ var en = {
9507
10168
  "agenda.settings.dayStartTime": "Day Start Time",
9508
10169
  "agenda.settings.dayStartTimeDesc": "First hour shown on the calendar",
9509
10170
  "agenda.settings.dayEndTime": "Day End Time",
9510
- "agenda.settings.dayEndTimeDesc": "Last hour shown on the calendar"
10171
+ "agenda.settings.dayEndTimeDesc": "Last hour shown on the calendar",
10172
+ "agenda.recurrence.label": "Repeat",
10173
+ "agenda.recurrence.preset.none": "Does not repeat",
10174
+ "agenda.recurrence.preset.daily": "Daily",
10175
+ "agenda.recurrence.preset.weekly": "Weekly",
10176
+ "agenda.recurrence.preset.biweekly": "Every 2 weeks",
10177
+ "agenda.recurrence.preset.monthly": "Monthly, on the same weekday",
10178
+ "agenda.recurrence.preset.custom": "Custom",
10179
+ "agenda.recurrence.ends": "Ends",
10180
+ "agenda.recurrence.endUntil": "On a date",
10181
+ "agenda.recurrence.endCountShort": "After",
10182
+ "agenda.recurrence.endUntilShort": "Until",
10183
+ "agenda.recurrence.times": "times",
10184
+ "agenda.recurrence.endCount": "After N times",
10185
+ "agenda.recurrence.untilLabel": "Until",
10186
+ "agenda.recurrence.countLabel": "Occurrences",
10187
+ "agenda.recurrence.checking": "Checking the dates\u2026",
10188
+ "agenda.recurrence.summary": "{{count}} appointments, through {{last}}",
10189
+ "agenda.recurrence.capped": "The series was capped at {{max}} occurrences.",
10190
+ "agenda.recurrence.conflicts": "{{busy}} of {{total}} dates conflict",
10191
+ "agenda.recurrence.closedDays": "{{closed}} fall on a day the business is closed",
10192
+ "agenda.recurrence.createsAnyway": "Conflicting dates are created anyway \u2014 no occurrence is dropped silently.",
10193
+ "agenda.recurrence.needsProfessional": "Pick the professional to check the dates.",
10194
+ "agenda.recurrence.partOfSeries": "Part of a series of {{count}}",
10195
+ "agenda.recurrence.scopeSaveTitle": "Edit recurring appointment",
10196
+ "agenda.recurrence.scopeDeleteTitle": "Delete recurring appointment",
10197
+ "agenda.recurrence.scopeThis": "This appointment",
10198
+ "agenda.recurrence.scopeSeries": "All in the series ({{count}})",
10199
+ "agenda.recurrence.dateStaysLocal": "The new date applies to this appointment only: every occurrence keeps its own day, and only the time becomes {{time}}."
9511
10200
  };
9512
10201
 
9513
10202
  // src/locales/pt-BR.ts
@@ -9657,6 +10346,8 @@ var ptBR = {
9657
10346
  "agenda.settings.autoCreateOrderDesc": "Criar automaticamente um pedido financeiro para novos agendamentos",
9658
10347
  "agenda.settings.interestsOnBooking": "Interesses no agendamento",
9659
10348
  "agenda.settings.interestsOnBookingDesc": "Ao marcar uma avalia\xE7\xE3o, mostra os interesses abertos do cliente e deixa a recep\xE7\xE3o declarar novos \u2014 eles alimentam o funil do CRM, n\xE3o a comanda",
10349
+ "agenda.settings.recurringBookings": "Repetir agendamento",
10350
+ "agenda.settings.recurringBookingsDesc": "Deixa um agendamento virar s\xE9rie \u2014 toda semana, a cada 2 semanas, mensal. Cada ocorr\xEAncia nasce como agendamento de verdade, com conflito, check-in e comanda pr\xF3prios",
9660
10351
  "agenda.settings.blockOutside": "Bloquear Fora do Hor\xE1rio",
9661
10352
  "agenda.settings.blockOutsideDesc": "Impedir agendamentos fora do hor\xE1rio de funcionamento",
9662
10353
  "agenda.settings.calendarDisplay": "Exibi\xE7\xE3o do Calend\xE1rio",
@@ -9784,7 +10475,35 @@ var ptBR = {
9784
10475
  "agenda.settings.dayStartTime": "Hor\xE1rio de In\xEDcio",
9785
10476
  "agenda.settings.dayStartTimeDesc": "Primeiro hor\xE1rio exibido no calend\xE1rio",
9786
10477
  "agenda.settings.dayEndTime": "Hor\xE1rio de T\xE9rmino",
9787
- "agenda.settings.dayEndTimeDesc": "\xDAltimo hor\xE1rio exibido no calend\xE1rio"
10478
+ "agenda.settings.dayEndTimeDesc": "\xDAltimo hor\xE1rio exibido no calend\xE1rio",
10479
+ "agenda.recurrence.label": "Repetir",
10480
+ "agenda.recurrence.preset.none": "N\xE3o se repete",
10481
+ "agenda.recurrence.preset.daily": "Diariamente",
10482
+ "agenda.recurrence.preset.weekly": "Toda semana",
10483
+ "agenda.recurrence.preset.biweekly": "A cada 2 semanas",
10484
+ "agenda.recurrence.preset.monthly": "Mensal, no mesmo dia da semana",
10485
+ "agenda.recurrence.preset.custom": "Personalizado",
10486
+ "agenda.recurrence.ends": "Termina",
10487
+ "agenda.recurrence.endUntil": "Em uma data",
10488
+ "agenda.recurrence.endCountShort": "Depois de",
10489
+ "agenda.recurrence.endUntilShort": "At\xE9",
10490
+ "agenda.recurrence.times": "vezes",
10491
+ "agenda.recurrence.endCount": "Depois de N vezes",
10492
+ "agenda.recurrence.untilLabel": "At\xE9",
10493
+ "agenda.recurrence.countLabel": "Ocorr\xEAncias",
10494
+ "agenda.recurrence.checking": "Conferindo as datas\u2026",
10495
+ "agenda.recurrence.summary": "{{count}} agendamentos, at\xE9 {{last}}",
10496
+ "agenda.recurrence.capped": "A s\xE9rie foi limitada a {{max}} ocorr\xEAncias.",
10497
+ "agenda.recurrence.conflicts": "{{busy}} de {{total}} datas em conflito",
10498
+ "agenda.recurrence.closedDays": "{{closed}} caem em dia que a casa fecha",
10499
+ "agenda.recurrence.createsAnyway": "As datas em conflito s\xE3o criadas do mesmo jeito \u2014 nenhuma ocorr\xEAncia \xE9 descartada em sil\xEAncio.",
10500
+ "agenda.recurrence.needsProfessional": "Escolha o profissional para conferir as datas.",
10501
+ "agenda.recurrence.partOfSeries": "Parte de uma s\xE9rie de {{count}}",
10502
+ "agenda.recurrence.scopeSaveTitle": "Editar agendamento recorrente",
10503
+ "agenda.recurrence.scopeDeleteTitle": "Excluir agendamento recorrente",
10504
+ "agenda.recurrence.scopeThis": "Este agendamento",
10505
+ "agenda.recurrence.scopeSeries": "Todos da s\xE9rie ({{count}})",
10506
+ "agenda.recurrence.dateStaysLocal": "A data nova vale s\xF3 para este agendamento: cada ocorr\xEAncia da s\xE9rie mant\xE9m o dia dela, e s\xF3 o hor\xE1rio passa a ser {{time}}."
9788
10507
  };
9789
10508
 
9790
10509
  // src/locales/index.ts
@@ -13389,60 +14108,1000 @@ var MIGRATION_014_A_CONFIGURACAO_NAO_E_UMA_TABELA = `-- A configura\xE7\xE3o n\x
13389
14108
  -- \`adapter.updateOrg(orgId, { settings })\`. \xC9 por a\xED que passam as telas de
13390
14109
  -- /settings de agenda, crm, inventory, financial, tasks e courses.
13391
14110
  --
13392
- -- E a agenda J\xC1 usa: \`AgendaGeneralSettings.tsx\` importa
13393
- -- \`useTenantPluginSettings\` e declara \`AGENDA_TOGGLE_DEFAULTS\`. Criar
13394
- -- \`plg_agenda_booking_configs\` abriria um SEGUNDO lugar para a configura\xE7\xE3o da
13395
- -- agenda \u2014 a tela lendo de um, o porte escrevendo no outro.
14111
+ -- E a agenda J\xC1 usa: \`AgendaGeneralSettings.tsx\` importa
14112
+ -- \`useTenantPluginSettings\` e declara \`AGENDA_TOGGLE_DEFAULTS\`. Criar
14113
+ -- \`plg_agenda_booking_configs\` abriria um SEGUNDO lugar para a configura\xE7\xE3o da
14114
+ -- agenda \u2014 a tela lendo de um, o porte escrevendo no outro.
14115
+ --
14116
+ -- \u2500\u2500 Mas existem plg_*_settings no cluster \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
14117
+ --
14118
+ -- Existem, e n\xE3o contradizem: \`plg_shop_settings\`, \`plg_tables_settings\`,
14119
+ -- \`plg_reservations_settings\`, \`plg_crm_engagement_settings\`,
14120
+ -- \`plg_inventory_product_settings\`. Todas t\xEAm \`unit_id\` e existem porque a
14121
+ -- configura\xE7\xE3o delas varia POR UNIDADE e \xE9 lida por RPC no caminho quente
14122
+ -- (fechar comanda, calcular frete) \u2014 n\xE3o por uma tela de toggles.
14123
+ --
14124
+ -- \`online_booking_configs\` n\xE3o \xE9 nenhum dos dois casos: UMA linha no V1
14125
+ -- inteiro, tenant 1, e ningu\xE9m a l\xEA num caminho quente. \xC9 toggle.
14126
+ --
14127
+ -- A troco de registro, se um dia ela precisar variar por unidade, o lugar certo
14128
+ -- \xE9 \`plg_agenda_settings\` com \`unit_id\`, no formato das outras cinco \u2014 e n\xE3o
14129
+ -- \`_configs\`, que n\xE3o \xE9 nome que este SDK usa.
14130
+ --
14131
+ -- \u2500\u2500 O que se perde, e \xE9 de prop\xF3sito \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
14132
+ --
14133
+ -- \`allowed_units\`, \`allowed_services\` e \`allowed_professionals\` viram arrays de
14134
+ -- uuid DENTRO do jsonb, sem chave estrangeira. Apagar uma unidade deixa o id
14135
+ -- \xF3rf\xE3o na lista. \xC9 a mesma troca que \`tenants.settings.branding\` j\xE1 faz, e
14136
+ -- para uma linha ela paga: integridade referencial num array de configura\xE7\xE3o
14137
+ -- custaria uma tabela de liga\xE7\xE3o para resolver um problema que o V1 n\xE3o tem em
14138
+ -- nenhum tenant.
14139
+ --
14140
+ -- Idempotente. A tabela nunca recebeu linha: o porte dela ainda n\xE3o rodou.
14141
+
14142
+ DO $$
14143
+ BEGIN
14144
+ IF to_regclass('public.plg_agenda_booking_configs') IS NOT NULL THEN
14145
+ IF EXISTS (SELECT 1 FROM public.plg_agenda_booking_configs) THEN
14146
+ RAISE EXCEPTION
14147
+ 'plg_agenda_booking_configs tem linha \u2014 mover para tenants.settings.plugins.agenda antes de derrubar';
14148
+ END IF;
14149
+ DROP TABLE public.plg_agenda_booking_configs;
14150
+ END IF;
14151
+ END $$;
14152
+ `;
14153
+ var MIGRATION_015_O_FERIADO_TAMBEM_TEM_OBSERVACAO_E_ETIQUETA = `-- ---------------------------------------------------------------------------
14154
+ -- O feriado tamb\xE9m tem observa\xE7\xE3o e etiqueta.
14155
+ --
14156
+ -- Irm\xE3 da 150 do n\xFAcleo e da 103 do financeiro, para o \xFAltimo cadastro que
14157
+ -- ficou de fora. Mesmo argumento, e aqui a etiqueta tem um uso imediato:
14158
+ -- \xABbanc\xE1rio\xBB, \xABmunicipal\xBB, \xABponto facultativo\xBB s\xE3o recortes que mudam quem
14159
+ -- fecha e quem n\xE3o fecha, e que nenhum campo do cadastro responde.
14160
+ -- ---------------------------------------------------------------------------
14161
+
14162
+ ALTER TABLE public.holidays
14163
+ ADD COLUMN IF NOT EXISTS notes text,
14164
+ ADD COLUMN IF NOT EXISTS tags text[] DEFAULT '{}'::text[];
14165
+ `;
14166
+ var MIGRATION_016_A_VEZ_E_UM_FATO = `-- A lista da vez is an opt-in agenda addon. Its state is per unit, and every
14167
+ -- transition produces an immutable event so reception never becomes the
14168
+ -- unaudited explanation for why somebody was skipped.
14169
+
14170
+ CREATE TABLE IF NOT EXISTS public.plg_agenda_turn_queues (
14171
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
14172
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
14173
+ unit_id uuid NOT NULL REFERENCES app.units(id) ON DELETE CASCADE,
14174
+ queue_key text NOT NULL DEFAULT 'walk_in',
14175
+ name text NOT NULL DEFAULT 'Pr\xF3ximo da vez',
14176
+ is_active boolean NOT NULL DEFAULT true,
14177
+ geofence_required boolean NOT NULL DEFAULT true,
14178
+ geofence_radius_meters integer NOT NULL DEFAULT 80 CHECK (geofence_radius_meters BETWEEN 10 AND 1000),
14179
+ upcoming_booking_block_minutes integer NOT NULL DEFAULT 30 CHECK (upcoming_booking_block_minutes BETWEEN 0 AND 240),
14180
+ round_direction smallint NOT NULL DEFAULT 1 CHECK (round_direction IN (-1, 1)),
14181
+ created_at timestamptz NOT NULL DEFAULT now(),
14182
+ updated_at timestamptz NOT NULL DEFAULT now(),
14183
+ UNIQUE (tenant_id, unit_id, queue_key)
14184
+ );
14185
+
14186
+ CREATE TABLE IF NOT EXISTS public.plg_agenda_turn_entries (
14187
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
14188
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
14189
+ queue_id uuid NOT NULL REFERENCES public.plg_agenda_turn_queues(id) ON DELETE CASCADE,
14190
+ unit_id uuid NOT NULL REFERENCES app.units(id) ON DELETE CASCADE,
14191
+ professional_id uuid NOT NULL REFERENCES public.people(id) ON DELETE CASCADE,
14192
+ status text NOT NULL DEFAULT 'unavailable' CHECK (status IN ('available', 'serving', 'unavailable')),
14193
+ position bigint NOT NULL DEFAULT 0,
14194
+ previous_position bigint,
14195
+ available_at timestamptz,
14196
+ state_changed_at timestamptz NOT NULL DEFAULT now(),
14197
+ appointment_id uuid REFERENCES public.appointments(id) ON DELETE SET NULL,
14198
+ updated_at timestamptz NOT NULL DEFAULT now(),
14199
+ UNIQUE (queue_id, professional_id)
14200
+ );
14201
+
14202
+ CREATE INDEX IF NOT EXISTS plg_agenda_turn_entries_order_idx
14203
+ ON public.plg_agenda_turn_entries (queue_id, status, position);
14204
+
14205
+ CREATE TABLE IF NOT EXISTS public.plg_agenda_turn_events (
14206
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
14207
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
14208
+ queue_id uuid NOT NULL REFERENCES public.plg_agenda_turn_queues(id) ON DELETE CASCADE,
14209
+ unit_id uuid NOT NULL REFERENCES app.units(id) ON DELETE CASCADE,
14210
+ professional_id uuid NOT NULL REFERENCES public.people(id) ON DELETE CASCADE,
14211
+ event_type text NOT NULL,
14212
+ origin text NOT NULL CHECK (origin IN ('app', 'frontdesk', 'automatic')),
14213
+ actor_user_id uuid,
14214
+ appointment_id uuid REFERENCES public.appointments(id) ON DELETE SET NULL,
14215
+ reason text,
14216
+ inside_geofence boolean,
14217
+ location_accuracy_meters numeric,
14218
+ payload jsonb NOT NULL DEFAULT '{}'::jsonb,
14219
+ occurred_at timestamptz NOT NULL DEFAULT now()
14220
+ );
14221
+
14222
+ CREATE INDEX IF NOT EXISTS plg_agenda_turn_events_timeline_idx
14223
+ ON public.plg_agenda_turn_events (queue_id, occurred_at DESC);
14224
+
14225
+ ALTER TABLE public.plg_agenda_turn_queues ENABLE ROW LEVEL SECURITY;
14226
+ ALTER TABLE public.plg_agenda_turn_queues FORCE ROW LEVEL SECURITY;
14227
+ ALTER TABLE public.plg_agenda_turn_entries ENABLE ROW LEVEL SECURITY;
14228
+ ALTER TABLE public.plg_agenda_turn_entries FORCE ROW LEVEL SECURITY;
14229
+ ALTER TABLE public.plg_agenda_turn_events ENABLE ROW LEVEL SECURITY;
14230
+ ALTER TABLE public.plg_agenda_turn_events FORCE ROW LEVEL SECURITY;
14231
+
14232
+ DO $$ BEGIN
14233
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'plg_agenda_turn_queues' AND policyname = 'plg_agenda_turn_queues_read') THEN
14234
+ CREATE POLICY plg_agenda_turn_queues_read ON public.plg_agenda_turn_queues
14235
+ FOR SELECT TO authenticated
14236
+ USING (tenant_id = app.current_tenant_id() AND app.has_unit(unit_id)
14237
+ AND app.has_permission('agenda.turn_queue.read', unit_id));
14238
+ END IF;
14239
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'plg_agenda_turn_entries' AND policyname = 'plg_agenda_turn_entries_read') THEN
14240
+ CREATE POLICY plg_agenda_turn_entries_read ON public.plg_agenda_turn_entries
14241
+ FOR SELECT TO authenticated
14242
+ USING (tenant_id = app.current_tenant_id() AND app.has_unit(unit_id)
14243
+ AND app.has_permission('agenda.turn_queue.read', unit_id));
14244
+ END IF;
14245
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'plg_agenda_turn_events' AND policyname = 'plg_agenda_turn_events_read') THEN
14246
+ CREATE POLICY plg_agenda_turn_events_read ON public.plg_agenda_turn_events
14247
+ FOR SELECT TO authenticated
14248
+ USING (tenant_id = app.current_tenant_id() AND app.has_unit(unit_id)
14249
+ AND app.has_permission('agenda.turn_queue.read', unit_id));
14250
+ END IF;
14251
+ END $$;
14252
+
14253
+ CREATE OR REPLACE FUNCTION public.agenda_turn_queue_set_availability(
14254
+ p_available boolean,
14255
+ p_unit_id uuid,
14256
+ p_latitude numeric DEFAULT NULL,
14257
+ p_longitude numeric DEFAULT NULL,
14258
+ p_accuracy_meters numeric DEFAULT NULL,
14259
+ p_origin text DEFAULT 'app'
14260
+ ) RETURNS jsonb
14261
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
14262
+ DECLARE
14263
+ v_tenant uuid := app.current_tenant_id();
14264
+ v_actor uuid := auth.uid();
14265
+ v_person uuid;
14266
+ v_queue public.plg_agenda_turn_queues%ROWTYPE;
14267
+ v_lat numeric;
14268
+ v_lng numeric;
14269
+ v_distance numeric;
14270
+ v_position bigint;
14271
+ v_entry uuid;
14272
+ BEGIN
14273
+ IF v_tenant IS NULL THEN
14274
+ RAISE EXCEPTION 'no_active_tenant' USING ERRCODE = '42501';
14275
+ END IF;
14276
+ IF p_unit_id IS NULL THEN
14277
+ RAISE EXCEPTION 'unit_required' USING ERRCODE = '22023';
14278
+ END IF;
14279
+ IF p_origin <> 'app' THEN
14280
+ RAISE EXCEPTION 'self_availability_origin_must_be_app' USING ERRCODE = '22023';
14281
+ END IF;
14282
+ IF NOT app.has_unit(p_unit_id) OR NOT app.has_permission('agenda.turn_queue.edit', p_unit_id) THEN
14283
+ RAISE EXCEPTION 'agenda.turn_queue.edit is required' USING ERRCODE = '42501';
14284
+ END IF;
14285
+
14286
+ v_person := app.person_for_login(v_tenant, v_actor);
14287
+ IF v_person IS NULL THEN
14288
+ RAISE EXCEPTION 'sem_ficha_de_profissional' USING ERRCODE = '42501';
14289
+ END IF;
14290
+
14291
+ INSERT INTO public.plg_agenda_turn_queues (tenant_id, unit_id)
14292
+ VALUES (v_tenant, p_unit_id)
14293
+ ON CONFLICT (tenant_id, unit_id, queue_key) DO NOTHING;
14294
+
14295
+ SELECT * INTO v_queue
14296
+ FROM public.plg_agenda_turn_queues q
14297
+ WHERE q.tenant_id = v_tenant AND q.unit_id = p_unit_id
14298
+ AND q.queue_key = 'walk_in' AND q.is_active
14299
+ FOR UPDATE;
14300
+ IF v_queue.id IS NULL THEN
14301
+ RAISE EXCEPTION 'turn_queue_disabled' USING ERRCODE = '42501';
14302
+ END IF;
14303
+
14304
+ IF p_available AND v_queue.geofence_required THEN
14305
+ SELECT u.latitude, u.longitude INTO v_lat, v_lng
14306
+ FROM public.v_units u
14307
+ WHERE u.id = p_unit_id AND u.tenant_id = v_tenant;
14308
+ IF v_lat IS NULL OR v_lng IS NULL THEN
14309
+ RAISE EXCEPTION 'unit_has_no_coordinates' USING ERRCODE = '22023';
14310
+ END IF;
14311
+ IF p_latitude IS NULL OR p_longitude IS NULL THEN
14312
+ RAISE EXCEPTION 'location_required' USING ERRCODE = '22023';
14313
+ END IF;
14314
+ v_distance := 6371000 * acos(least(1, greatest(-1,
14315
+ cos(radians(v_lat::double precision)) * cos(radians(p_latitude::double precision))
14316
+ * cos(radians(p_longitude::double precision) - radians(v_lng::double precision))
14317
+ + sin(radians(v_lat::double precision)) * sin(radians(p_latitude::double precision))
14318
+ )));
14319
+ IF v_distance > v_queue.geofence_radius_meters THEN
14320
+ RAISE EXCEPTION 'outside_geofence' USING ERRCODE = '42501';
14321
+ END IF;
14322
+ END IF;
14323
+
14324
+ SELECT coalesce(max(e.position), 0) + 1 INTO v_position
14325
+ FROM public.plg_agenda_turn_entries e WHERE e.queue_id = v_queue.id;
14326
+
14327
+ INSERT INTO public.plg_agenda_turn_entries
14328
+ (tenant_id, queue_id, unit_id, professional_id, status, position, available_at, state_changed_at, appointment_id, updated_at)
14329
+ VALUES
14330
+ (v_tenant, v_queue.id, p_unit_id, v_person,
14331
+ CASE WHEN p_available THEN 'available' ELSE 'unavailable' END,
14332
+ CASE WHEN p_available THEN v_position ELSE 0 END,
14333
+ CASE WHEN p_available THEN now() ELSE NULL END,
14334
+ now(), NULL, now())
14335
+ ON CONFLICT (queue_id, professional_id) DO UPDATE SET
14336
+ status = EXCLUDED.status,
14337
+ position = CASE WHEN p_available THEN v_position ELSE public.plg_agenda_turn_entries.position END,
14338
+ available_at = EXCLUDED.available_at,
14339
+ state_changed_at = now(),
14340
+ appointment_id = NULL,
14341
+ updated_at = now()
14342
+ RETURNING id INTO v_entry;
14343
+
14344
+ INSERT INTO public.plg_agenda_turn_events
14345
+ (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id,
14346
+ inside_geofence, location_accuracy_meters, payload)
14347
+ VALUES
14348
+ (v_tenant, v_queue.id, p_unit_id, v_person,
14349
+ CASE WHEN p_available THEN 'available' ELSE 'left' END,
14350
+ 'app', v_actor, CASE WHEN p_available THEN true ELSE NULL END,
14351
+ CASE WHEN p_available THEN p_accuracy_meters ELSE NULL END,
14352
+ jsonb_build_object('entry_id', v_entry));
14353
+
14354
+ RETURN jsonb_build_object('ok', true, 'status', CASE WHEN p_available THEN 'available' ELSE 'unavailable' END);
14355
+ END $$;
14356
+
14357
+ CREATE OR REPLACE FUNCTION public.agenda_turn_queue_snapshot(p_unit_id uuid)
14358
+ RETURNS jsonb
14359
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' STABLE AS $$
14360
+ DECLARE
14361
+ v_tenant uuid := app.current_tenant_id();
14362
+ v_queue public.plg_agenda_turn_queues%ROWTYPE;
14363
+ v_person uuid;
14364
+ v_entries jsonb;
14365
+ v_events jsonb;
14366
+ BEGIN
14367
+ IF p_unit_id IS NULL THEN
14368
+ RAISE EXCEPTION 'unit_required' USING ERRCODE = '22023';
14369
+ END IF;
14370
+ IF NOT app.has_unit(p_unit_id) OR NOT app.has_permission('agenda.turn_queue.read', p_unit_id) THEN
14371
+ RAISE EXCEPTION 'agenda.turn_queue.read is required' USING ERRCODE = '42501';
14372
+ END IF;
14373
+ v_person := app.person_for_login(v_tenant, auth.uid());
14374
+ SELECT * INTO v_queue FROM public.plg_agenda_turn_queues q
14375
+ WHERE q.tenant_id = v_tenant AND q.unit_id = p_unit_id AND q.queue_key = 'walk_in' AND q.is_active;
14376
+ IF v_queue.id IS NULL THEN
14377
+ RETURN jsonb_build_object('queue_id', NULL, 'unit_id', p_unit_id, 'queue_name', 'Pr\xF3ximo da vez',
14378
+ 'current_professional_id', v_person, 'entries', '[]'::jsonb, 'events', '[]'::jsonb);
14379
+ END IF;
14380
+
14381
+ SELECT coalesce(jsonb_agg(jsonb_build_object(
14382
+ 'id', e.id, 'queue_id', e.queue_id, 'unit_id', e.unit_id,
14383
+ 'professional_id', e.professional_id, 'professional_name', p.name,
14384
+ 'professional_avatar_url', p.avatar_url, 'status', e.status, 'position', e.position,
14385
+ 'state_changed_at', e.state_changed_at, 'available_at', e.available_at,
14386
+ 'appointment_id', e.appointment_id
14387
+ ) ORDER BY CASE e.status WHEN 'available' THEN 0 WHEN 'serving' THEN 1 ELSE 2 END, e.position), '[]'::jsonb)
14388
+ INTO v_entries
14389
+ FROM public.plg_agenda_turn_entries e
14390
+ JOIN public.people p ON p.id = e.professional_id AND p.tenant_id = e.tenant_id
14391
+ WHERE e.queue_id = v_queue.id;
14392
+
14393
+ SELECT coalesce(jsonb_agg(jsonb_build_object(
14394
+ 'id', ev.id, 'professional_id', ev.professional_id, 'professional_name', p.name,
14395
+ 'event_type', ev.event_type, 'origin', ev.origin, 'reason', ev.reason,
14396
+ 'occurred_at', ev.occurred_at
14397
+ ) ORDER BY ev.occurred_at DESC), '[]'::jsonb)
14398
+ INTO v_events
14399
+ FROM (
14400
+ SELECT * FROM public.plg_agenda_turn_events
14401
+ WHERE queue_id = v_queue.id AND occurred_at >= date_trunc('day', now())
14402
+ ORDER BY occurred_at DESC LIMIT 50
14403
+ ) ev
14404
+ JOIN public.people p ON p.id = ev.professional_id AND p.tenant_id = ev.tenant_id;
14405
+
14406
+ RETURN jsonb_build_object('queue_id', v_queue.id, 'unit_id', p_unit_id, 'queue_name', v_queue.name,
14407
+ 'current_professional_id', v_person, 'entries', v_entries, 'events', v_events);
14408
+ END $$;
14409
+
14410
+ CREATE OR REPLACE FUNCTION public.agenda_turn_queue_override(
14411
+ p_professional_id uuid,
14412
+ p_unit_id uuid,
14413
+ p_available boolean,
14414
+ p_reason text
14415
+ ) RETURNS jsonb
14416
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
14417
+ DECLARE
14418
+ v_tenant uuid := app.current_tenant_id();
14419
+ v_queue public.plg_agenda_turn_queues%ROWTYPE;
14420
+ v_position bigint;
14421
+ BEGIN
14422
+ IF NOT app.has_unit(p_unit_id) OR NOT app.has_permission('agenda.turn_queue.manage', p_unit_id) THEN
14423
+ RAISE EXCEPTION 'agenda.turn_queue.manage is required' USING ERRCODE = '42501';
14424
+ END IF;
14425
+ IF nullif(trim(p_reason), '') IS NULL THEN
14426
+ RAISE EXCEPTION 'override_reason_required' USING ERRCODE = '22023';
14427
+ END IF;
14428
+ SELECT * INTO v_queue FROM public.plg_agenda_turn_queues q
14429
+ WHERE q.tenant_id = v_tenant AND q.unit_id = p_unit_id AND q.queue_key = 'walk_in' AND q.is_active
14430
+ FOR UPDATE;
14431
+ IF v_queue.id IS NULL THEN RAISE EXCEPTION 'turn_queue_disabled' USING ERRCODE = '42501'; END IF;
14432
+ SELECT coalesce(max(position), 0) + 1 INTO v_position FROM public.plg_agenda_turn_entries WHERE queue_id = v_queue.id;
14433
+ INSERT INTO public.plg_agenda_turn_entries
14434
+ (tenant_id, queue_id, unit_id, professional_id, status, position, available_at, state_changed_at, updated_at)
14435
+ VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id,
14436
+ CASE WHEN p_available THEN 'available' ELSE 'unavailable' END,
14437
+ CASE WHEN p_available THEN v_position ELSE 0 END,
14438
+ CASE WHEN p_available THEN now() ELSE NULL END, now(), now())
14439
+ ON CONFLICT (queue_id, professional_id) DO UPDATE SET
14440
+ status = EXCLUDED.status, position = CASE WHEN p_available THEN v_position ELSE public.plg_agenda_turn_entries.position END,
14441
+ available_at = EXCLUDED.available_at, state_changed_at = now(), appointment_id = NULL, updated_at = now();
14442
+ INSERT INTO public.plg_agenda_turn_events
14443
+ (tenant_id, queue_id, unit_id, professional_id, event_type, origin, actor_user_id, reason)
14444
+ VALUES (v_tenant, v_queue.id, p_unit_id, p_professional_id,
14445
+ CASE WHEN p_available THEN 'override_available' ELSE 'override_left' END,
14446
+ 'frontdesk', auth.uid(), trim(p_reason));
14447
+ RETURN jsonb_build_object('ok', true);
14448
+ END $$;
14449
+
14450
+ CREATE OR REPLACE FUNCTION public.agenda_turn_queue_on_appointment_status()
14451
+ RETURNS trigger
14452
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
14453
+ DECLARE
14454
+ v_entry public.plg_agenda_turn_entries%ROWTYPE;
14455
+ v_position bigint;
14456
+ v_event text;
14457
+ BEGIN
14458
+ IF NEW.assignee_id IS NULL OR NEW.unit_id IS NULL OR NEW.status IS NOT DISTINCT FROM OLD.status THEN RETURN NEW; END IF;
14459
+ SELECT * INTO v_entry FROM public.plg_agenda_turn_entries e
14460
+ WHERE e.tenant_id = NEW.tenant_id AND e.unit_id = NEW.unit_id AND e.professional_id = NEW.assignee_id
14461
+ FOR UPDATE;
14462
+ IF v_entry.id IS NULL THEN RETURN NEW; END IF;
14463
+
14464
+ IF NEW.status = 'in_progress' AND OLD.status <> 'in_progress' AND v_entry.status = 'available' THEN
14465
+ SELECT coalesce(max(position), 0) + 1 INTO v_position FROM public.plg_agenda_turn_entries WHERE queue_id = v_entry.queue_id;
14466
+ UPDATE public.plg_agenda_turn_entries SET status = 'serving', previous_position = position,
14467
+ position = v_position, appointment_id = NEW.id, state_changed_at = now(), updated_at = now()
14468
+ WHERE id = v_entry.id;
14469
+ v_event := 'advanced_to_service';
14470
+ ELSIF OLD.status = 'in_progress' AND NEW.status = 'cancelled' AND v_entry.appointment_id = NEW.id THEN
14471
+ UPDATE public.plg_agenda_turn_entries SET status = 'available',
14472
+ position = coalesce(previous_position, position), appointment_id = NULL,
14473
+ state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;
14474
+ v_event := 'service_cancelled_restored';
14475
+ ELSIF OLD.status = 'in_progress' AND NEW.status = 'completed' AND v_entry.appointment_id = NEW.id THEN
14476
+ UPDATE public.plg_agenda_turn_entries SET status = 'unavailable', appointment_id = NULL,
14477
+ state_changed_at = now(), updated_at = now() WHERE id = v_entry.id;
14478
+ v_event := 'service_completed';
14479
+ ELSE
14480
+ RETURN NEW;
14481
+ END IF;
14482
+
14483
+ INSERT INTO public.plg_agenda_turn_events
14484
+ (tenant_id, queue_id, unit_id, professional_id, event_type, origin, appointment_id)
14485
+ VALUES (NEW.tenant_id, v_entry.queue_id, NEW.unit_id, NEW.assignee_id, v_event, 'automatic', NEW.id);
14486
+ RETURN NEW;
14487
+ END $$;
14488
+
14489
+ DROP TRIGGER IF EXISTS appointments_turn_queue_status ON public.appointments;
14490
+ CREATE TRIGGER appointments_turn_queue_status
14491
+ AFTER UPDATE OF status ON public.appointments
14492
+ FOR EACH ROW EXECUTE FUNCTION public.agenda_turn_queue_on_appointment_status();
14493
+
14494
+ REVOKE ALL ON public.plg_agenda_turn_queues, public.plg_agenda_turn_entries, public.plg_agenda_turn_events FROM PUBLIC, anon, authenticated;
14495
+ GRANT SELECT ON public.plg_agenda_turn_queues, public.plg_agenda_turn_entries, public.plg_agenda_turn_events TO authenticated;
14496
+ REVOKE ALL ON FUNCTION public.agenda_turn_queue_set_availability(boolean, uuid, numeric, numeric, numeric, text) FROM PUBLIC;
14497
+ REVOKE ALL ON FUNCTION public.agenda_turn_queue_snapshot(uuid) FROM PUBLIC;
14498
+ REVOKE ALL ON FUNCTION public.agenda_turn_queue_override(uuid, uuid, boolean, text) FROM PUBLIC;
14499
+ GRANT EXECUTE ON FUNCTION public.agenda_turn_queue_set_availability(boolean, uuid, numeric, numeric, numeric, text) TO authenticated;
14500
+ GRANT EXECUTE ON FUNCTION public.agenda_turn_queue_snapshot(uuid) TO authenticated;
14501
+ GRANT EXECUTE ON FUNCTION public.agenda_turn_queue_override(uuid, uuid, boolean, text) TO authenticated;
14502
+
14503
+ COMMENT ON TABLE public.plg_agenda_turn_events IS
14504
+ 'Immutable audit trail for the walk-in turn queue. Location stores only the inside/outside verdict and accuracy, never coordinates.';
14505
+ `;
14506
+ var MIGRATION_017_A_EQUIPE_DO_DIA_APARECE = `-- The board must show the whole team expected at this unit today, including
14507
+ -- professionals who have not explicitly joined the walk-in queue yet.
14508
+
14509
+ CREATE OR REPLACE FUNCTION public.agenda_turn_queue_snapshot(p_unit_id uuid)
14510
+ RETURNS jsonb
14511
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' STABLE AS $$
14512
+ DECLARE
14513
+ v_tenant uuid := app.current_tenant_id();
14514
+ v_queue public.plg_agenda_turn_queues%ROWTYPE;
14515
+ v_person uuid;
14516
+ v_timezone text;
14517
+ v_day date;
14518
+ v_entries jsonb;
14519
+ v_events jsonb;
14520
+ v_professionals jsonb;
14521
+ BEGIN
14522
+ IF p_unit_id IS NULL THEN
14523
+ RAISE EXCEPTION 'unit_required' USING ERRCODE = '22023';
14524
+ END IF;
14525
+ IF NOT app.has_unit(p_unit_id) OR NOT app.has_permission('agenda.turn_queue.read', p_unit_id) THEN
14526
+ RAISE EXCEPTION 'agenda.turn_queue.read is required' USING ERRCODE = '42501';
14527
+ END IF;
14528
+
14529
+ SELECT coalesce(t.settings->>'timezone', 'America/Sao_Paulo')
14530
+ INTO v_timezone
14531
+ FROM public.tenants t
14532
+ WHERE t.id = v_tenant;
14533
+ v_day := (now() AT TIME ZONE v_timezone)::date;
14534
+ v_person := app.person_for_login(v_tenant, auth.uid());
14535
+
14536
+ SELECT * INTO v_queue
14537
+ FROM public.plg_agenda_turn_queues q
14538
+ WHERE q.tenant_id = v_tenant
14539
+ AND q.unit_id = p_unit_id
14540
+ AND q.queue_key = 'walk_in'
14541
+ AND q.is_active;
14542
+
14543
+ SELECT coalesce(jsonb_agg(jsonb_build_object(
14544
+ 'id', e.id, 'queue_id', e.queue_id, 'unit_id', e.unit_id,
14545
+ 'professional_id', e.professional_id, 'professional_name', p.name,
14546
+ 'professional_avatar_url', p.avatar_url, 'status', e.status, 'position', e.position,
14547
+ 'state_changed_at', e.state_changed_at, 'available_at', e.available_at,
14548
+ 'appointment_id', e.appointment_id
14549
+ ) ORDER BY CASE e.status WHEN 'available' THEN 0 WHEN 'serving' THEN 1 ELSE 2 END, e.position), '[]'::jsonb)
14550
+ INTO v_entries
14551
+ FROM public.plg_agenda_turn_entries e
14552
+ JOIN public.people p ON p.id = e.professional_id AND p.tenant_id = e.tenant_id
14553
+ WHERE e.queue_id = v_queue.id;
14554
+
14555
+ SELECT coalesce(jsonb_agg(jsonb_build_object(
14556
+ 'id', ev.id, 'professional_id', ev.professional_id, 'professional_name', p.name,
14557
+ 'event_type', ev.event_type, 'origin', ev.origin, 'reason', ev.reason,
14558
+ 'occurred_at', ev.occurred_at
14559
+ ) ORDER BY ev.occurred_at DESC), '[]'::jsonb)
14560
+ INTO v_events
14561
+ FROM (
14562
+ SELECT * FROM public.plg_agenda_turn_events
14563
+ WHERE queue_id = v_queue.id
14564
+ AND (occurred_at AT TIME ZONE v_timezone)::date = v_day
14565
+ ORDER BY occurred_at DESC LIMIT 50
14566
+ ) ev
14567
+ JOIN public.people p ON p.id = ev.professional_id AND p.tenant_id = ev.tenant_id;
14568
+
14569
+ WITH scheduled AS (
14570
+ SELECT DISTINCT s.assignee_id AS professional_id
14571
+ FROM public.schedules s
14572
+ WHERE s.tenant_id = v_tenant
14573
+ AND s.kind = 'working_hours'
14574
+ AND s.assignee_id IS NOT NULL
14575
+ AND coalesce(s.unit_id, s.location_id) = p_unit_id
14576
+ AND (s.valid_from IS NULL OR s.valid_from <= v_day)
14577
+ AND (s.valid_to IS NULL OR v_day <= s.valid_to)
14578
+ AND (
14579
+ (s.specific_date = v_day AND s.is_active)
14580
+ OR (
14581
+ s.specific_date IS NULL
14582
+ AND s.is_active
14583
+ AND s.day_of_week = extract(dow FROM v_day)::smallint
14584
+ AND public.agenda_week_parity_active(s.valid_from, v_day, s.week_interval)
14585
+ AND NOT EXISTS (
14586
+ SELECT 1 FROM public.schedules day_override
14587
+ WHERE day_override.tenant_id = s.tenant_id
14588
+ AND day_override.assignee_id = s.assignee_id
14589
+ AND coalesce(day_override.unit_id, day_override.location_id) = p_unit_id
14590
+ AND day_override.specific_date = v_day
14591
+ )
14592
+ )
14593
+ )
14594
+ ), booked AS (
14595
+ SELECT a.assignee_id AS professional_id,
14596
+ count(*)::integer AS appointment_count,
14597
+ min(a.starts_at) FILTER (WHERE a.starts_at >= now()) AS next_appointment_at
14598
+ FROM public.appointments a
14599
+ WHERE a.tenant_id = v_tenant
14600
+ AND a.assignee_id IS NOT NULL
14601
+ AND coalesce(a.unit_id, a.location_id) = p_unit_id
14602
+ AND (a.starts_at AT TIME ZONE v_timezone)::date = v_day
14603
+ AND coalesce(a.status, '') NOT IN ('cancelled', 'canceled')
14604
+ GROUP BY a.assignee_id
14605
+ ), candidates AS (
14606
+ SELECT professional_id FROM scheduled
14607
+ UNION
14608
+ SELECT professional_id FROM booked
14609
+ UNION
14610
+ SELECT e.professional_id FROM public.plg_agenda_turn_entries e WHERE e.queue_id = v_queue.id
14611
+ )
14612
+ SELECT coalesce(jsonb_agg(jsonb_build_object(
14613
+ 'professional_id', p.id,
14614
+ 'professional_name', p.name,
14615
+ 'professional_avatar_url', p.avatar_url,
14616
+ 'status', coalesce(e.status, 'unavailable'),
14617
+ 'position', coalesce(e.position, 0),
14618
+ 'last_checkin_at', last_checkin.occurred_at,
14619
+ 'is_scheduled', scheduled.professional_id IS NOT NULL,
14620
+ 'appointment_count', coalesce(booked.appointment_count, 0),
14621
+ 'next_appointment_at', booked.next_appointment_at
14622
+ ) ORDER BY
14623
+ CASE coalesce(e.status, 'unavailable') WHEN 'available' THEN 0 WHEN 'serving' THEN 1 ELSE 2 END,
14624
+ CASE WHEN booked.next_appointment_at IS NOT NULL THEN 0 ELSE 1 END,
14625
+ e.position NULLS LAST,
14626
+ p.name), '[]'::jsonb)
14627
+ INTO v_professionals
14628
+ FROM candidates c
14629
+ JOIN public.people p ON p.id = c.professional_id AND p.tenant_id = v_tenant AND p.is_active
14630
+ LEFT JOIN scheduled ON scheduled.professional_id = p.id
14631
+ LEFT JOIN booked ON booked.professional_id = p.id
14632
+ LEFT JOIN public.plg_agenda_turn_entries e ON e.queue_id = v_queue.id AND e.professional_id = p.id
14633
+ LEFT JOIN LATERAL (
14634
+ SELECT ev.occurred_at
14635
+ FROM public.plg_agenda_turn_events ev
14636
+ WHERE ev.queue_id = v_queue.id
14637
+ AND ev.professional_id = p.id
14638
+ AND ev.event_type IN ('available', 'override_available')
14639
+ AND (ev.occurred_at AT TIME ZONE v_timezone)::date = v_day
14640
+ ORDER BY ev.occurred_at DESC
14641
+ LIMIT 1
14642
+ ) last_checkin ON true;
14643
+
14644
+ RETURN jsonb_build_object(
14645
+ 'queue_id', v_queue.id,
14646
+ 'unit_id', p_unit_id,
14647
+ 'queue_name', coalesce(v_queue.name, 'Pr\xF3ximo da vez'),
14648
+ 'current_professional_id', v_person,
14649
+ 'date', v_day,
14650
+ 'professionals', v_professionals,
14651
+ 'entries', v_entries,
14652
+ 'events', v_events
14653
+ );
14654
+ END $$;
14655
+
14656
+ REVOKE ALL ON FUNCTION public.agenda_turn_queue_snapshot(uuid) FROM PUBLIC;
14657
+ GRANT EXECUTE ON FUNCTION public.agenda_turn_queue_snapshot(uuid) TO authenticated;
14658
+ `;
14659
+ var MIGRATION_018_O_AGENDAMENTO_QUE_SE_REPETE = `-- O agendamento que se repete.
14660
+ --
14661
+ -- "Repetir" no agendamento \u2014 o que o Google Calendar chama de s\xE9rie. At\xE9 aqui
14662
+ -- s\xF3 o BLOQUEIO se repetia (\`schedules\` kind='block'); o compromisso de cliente
14663
+ -- era sempre avulso, e marcar o retorno de quinze em quinze dias por seis meses
14664
+ -- era doze aberturas do mesmo formul\xE1rio.
14665
+ --
14666
+ -- \u2500\u2500 Por que aqui a ocorr\xEAncia \xE9 LINHA, e no bloqueio n\xE3o \xE9 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
14667
+ --
14668
+ -- A 010 escolheu o contr\xE1rio, e escolheu certo para o que ela trata: o bloqueio
14669
+ -- recorrente \xE9 UMA linha por dia da semana com vig\xEAncia e paridade, expandida
14670
+ -- na LEITURA (\`agenda_busy_blocks\` no banco, \`lib/schedule-engine.ts\` na tela).
14671
+ -- O motivo est\xE1 escrito l\xE1: um almo\xE7o de segunda a sexta at\xE9 o fim do ano s\xE3o
14672
+ -- 1.300 ocorr\xEAncias de uma coisa que n\xE3o tem identidade pr\xF3pria \u2014 e a grade
14673
+ -- ETERNA (248 das 482 do V1 t\xEAm \`valid_to\` nulo) n\xE3o se expande de jeito nenhum.
14674
+ --
14675
+ -- Um compromisso de cliente \xE9 o caso oposto, e por tr\xEAs raz\xF5es concretas que
14676
+ -- vivem neste schema:
14677
+ --
14678
+ -- 1. CADA OCORR\xCANCIA \xC9 UM FATO SEPARADO. Ela tem status pr\xF3prio (a de 12/11
14679
+ -- foi falta, a de 26/11 foi atendida), check-in pr\xF3prio (\`checked_in_at\`),
14680
+ -- e pode ter PEDIDO pr\xF3prio \u2014 \`order_id\`, com fatura e recebimento. Nada
14681
+ -- disso cabe numa regra: a regra n\xE3o pode estar "paga".
14682
+ -- 2. TODO LEITOR J\xC1 L\xCA LINHA. A grade da semana, \`checkConflict\`, a fila da
14683
+ -- vez (\`plg_agenda_turn_entries.appointment_id\`), as confirma\xE7\xF5es, o
14684
+ -- hist\xF3rico da ficha. S\xE9rie virtual obrigaria cada um deles a aprender a
14685
+ -- expandir uma regra \u2014 seis lugares, a mesma regra, divergindo em seis.
14686
+ -- 3. A JANELA \xC9 FINITA POR NATUREZA. Ningu\xE9m marca "retorno toda semana para
14687
+ -- sempre": marca at\xE9 uma data ou um n\xFAmero de vezes. O que n\xE3o se expande
14688
+ -- \xE9 a grade eterna, e esta n\xE3o \xE9 uma.
14689
+ --
14690
+ -- Por isso: ocorr\xEAncia = linha em \`public.appointments\`, com \`series_id\` REAL.
14691
+ -- N\xE3o por casamento de campos \u2014 a identidade do bloqueio \xE9 reconstru\xEDda
14692
+ -- comparando t\xEDtulo + hor\xE1rio + profissional (\`views/CalendarView.tsx\`), e \xE9
14693
+ -- justamente a parte que n\xE3o se deve copiar: dois almo\xE7os de t\xEDtulos iguais em
14694
+ -- hor\xE1rios iguais s\xE3o o mesmo bloqueio para aquela heur\xEDstica.
14695
+ --
14696
+ -- \u2500\u2500 A regra tamb\xE9m fica guardada \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
14697
+ --
14698
+ -- Materializar n\xE3o dispensa a regra. Sem ela a tela n\xE3o sabe dizer "a cada 2
14699
+ -- semanas, 10 vezes" ao reabrir uma ocorr\xEAncia, e estender a s\xE9rie depois seria
14700
+ -- adivinhar o intervalo pela dist\xE2ncia entre duas linhas. O precedente \xE9 o
14701
+ -- Financeiro: \`RecurringBill\` guarda a regra E materializa as parcelas.
14702
+ --
14703
+ -- \u2500\u2500 O que N\xC3O est\xE1 aqui \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
14704
+ --
14705
+ -- RRULE livre (v\xE1rios dias da semana, "primeira ter\xE7a do m\xEAs" escolhida \xE0 m\xE3o).
14706
+ -- \`freq\` \xE9 uma lista fechada de tr\xEAs; crescer \xE9 acrescentar valor ao CHECK e um
14707
+ -- caso no expansor de \`lib/recurrence.ts\`, n\xE3o mudar o modelo.
14708
+ --
14709
+ -- "Este e os seguintes" tamb\xE9m n\xE3o: ele exige PARTIR a regra em duas (a antiga
14710
+ -- ganha \`until_date\`, a nova nasce do dia editado). Esta migration n\xE3o impede \u2014
14711
+ -- \xE9 um INSERT a mais no mesmo formato.
14712
+ --
14713
+ -- \`v_appointments\` N\xC3O \xE9 alargada. O provider j\xE1 l\xEA \`public.appointments\`
14714
+ -- direto e funde com a view (\`data/supabase.ts\`), justamente porque a view n\xE3o
14715
+ -- exp\xF5e \`resource_id\` nem \`unit_id\`; \`series_id\` entra pelo mesmo caminho e n\xE3o
14716
+ -- custa uma redefini\xE7\xE3o de view que a 007 teve de reescrever inteira.
14717
+ --
14718
+ -- Idempotente.
14719
+
14720
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
14721
+ -- 1. A REGRA
14722
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
14723
+
14724
+ CREATE TABLE IF NOT EXISTS public.plg_agenda_recurrences (
14725
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
14726
+ tenant_id uuid NOT NULL,
14727
+ -- A cerca da franquia, herdada da primeira ocorr\xEAncia. Nula \xE9 leg\xEDtima: a
14728
+ -- conta de uma unidade s\xF3 n\xE3o escolhe unidade, e a 004 estampa \`unit_id\` no
14729
+ -- agendamento por trigger \u2014 a regra nasce antes de a trigger ter rodado.
14730
+ unit_id uuid,
14731
+ -- \`daily\` | \`weekly\` | \`monthly_weekday\`. Mensal \xE9 "mesmo dia da semana"
14732
+ -- (a 2\xAA ter\xE7a), n\xE3o "mesmo dia do m\xEAs": \xE9 o que o retorno de sal\xE3o e de
14733
+ -- cl\xEDnica significa, e \xE9 o que o Google oferece como preset.
14734
+ freq text NOT NULL,
14735
+ -- A cada N per\xEDodos. 2 + weekly = "a cada 2 semanas".
14736
+ interval_count integer DEFAULT 1 NOT NULL,
14737
+ -- O dia da PRIMEIRA ocorr\xEAncia, que \xE9 a \xE2ncora de tudo: o weekday da
14738
+ -- semanal e a ordinal do m\xEAs da mensal saem dele.
14739
+ starts_on date NOT NULL,
14740
+ -- \`HH:MM\` da s\xE9rie. Guardado para a edi\xE7\xE3o "todos" saber que hor\xE1rio
14741
+ -- aplicar sem reler uma ocorr\xEAncia qualquer e torcer para ser a certa.
14742
+ time_of_day time without time zone,
14743
+ -- O fim, por UMA ponta s\xF3. Ver o CHECK abaixo.
14744
+ until_date date,
14745
+ occurrence_count integer,
14746
+ metadata jsonb DEFAULT '{}'::jsonb NOT NULL,
14747
+ created_at timestamp with time zone DEFAULT now() NOT NULL,
14748
+ updated_at timestamp with time zone DEFAULT now() NOT NULL,
14749
+ CONSTRAINT plg_agenda_recurrences_pkey PRIMARY KEY (id),
14750
+ CONSTRAINT plg_agenda_recurrences_freq_chk
14751
+ CHECK (freq IN ('daily', 'weekly', 'monthly_weekday')),
14752
+ CONSTRAINT plg_agenda_recurrences_interval_chk
14753
+ CHECK (interval_count BETWEEN 1 AND 12),
14754
+ -- UMA ponta, n\xE3o duas. Data E contagem juntas dariam duas respostas para
14755
+ -- "quando acaba", e quem lesse a regra depois n\xE3o saberia qual valeu.
14756
+ CONSTRAINT plg_agenda_recurrences_end_chk CHECK (
14757
+ (until_date IS NOT NULL AND occurrence_count IS NULL)
14758
+ OR (until_date IS NULL AND occurrence_count IS NOT NULL)
14759
+ ),
14760
+ -- O MESMO TETO DA TELA, no banco. \`RECURRENCE_MAX_OCCURRENCES\` em
14761
+ -- \`lib/recurrence.ts\` \xE9 60; um teto que s\xF3 existe no navegador n\xE3o \xE9 teto.
14762
+ CONSTRAINT plg_agenda_recurrences_count_chk
14763
+ CHECK (occurrence_count IS NULL OR occurrence_count BETWEEN 1 AND 60)
14764
+ );
14765
+
14766
+ DO $$
14767
+ BEGIN
14768
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_agenda_recurrences_tenant_fk') THEN
14769
+ ALTER TABLE public.plg_agenda_recurrences
14770
+ ADD CONSTRAINT plg_agenda_recurrences_tenant_fk
14771
+ FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;
14772
+ END IF;
14773
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_agenda_recurrences_unit_fk') THEN
14774
+ ALTER TABLE public.plg_agenda_recurrences
14775
+ ADD CONSTRAINT plg_agenda_recurrences_unit_fk
14776
+ FOREIGN KEY (unit_id) REFERENCES app.units(id) ON DELETE SET NULL;
14777
+ END IF;
14778
+ END $$;
14779
+
14780
+ CREATE INDEX IF NOT EXISTS plg_agenda_recurrences_tenant_idx
14781
+ ON public.plg_agenda_recurrences (tenant_id, starts_on DESC);
14782
+
14783
+ COMMENT ON TABLE public.plg_agenda_recurrences IS
14784
+ 'A regra de uma s\xE9rie de agendamentos ("Repetir"). As ocorr\xEAncias s\xE3o linhas reais em public.appointments, ligadas por appointments.series_id \u2014 ver o cabe\xE7alho da migration 018 para por que aqui se materializa e no bloqueio n\xE3o.';
14785
+
14786
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
14787
+ -- 2. A OCORR\xCANCIA APONTA PARA A REGRA
14788
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
14789
+
14790
+ ALTER TABLE public.appointments ADD COLUMN IF NOT EXISTS series_id uuid;
14791
+
14792
+ COMMENT ON COLUMN public.appointments.series_id IS
14793
+ 'A s\xE9rie de "Repetir" a que esta ocorr\xEAncia pertence. NULL = agendamento avulso, que \xE9 a esmagadora maioria. ON DELETE SET NULL: apagar a regra solta as ocorr\xEAncias, nunca as apaga.';
14794
+
14795
+ DO $$
14796
+ BEGIN
14797
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'appointments_series_fk') THEN
14798
+ -- SET NULL, e n\xE3o CASCADE. A 007 j\xE1 custou uma agenda inteira a um CASCADE
14799
+ -- ("o cascade levou a agenda junto"): a regra \xE9 METADADO da s\xE9rie, e perder
14800
+ -- o metadado n\xE3o pode apagar atendimentos que j\xE1 aconteceram e j\xE1 foram
14801
+ -- cobrados. Soltar a ocorr\xEAncia \xE9 a perda honesta.
14802
+ ALTER TABLE public.appointments
14803
+ ADD CONSTRAINT appointments_series_fk
14804
+ FOREIGN KEY (series_id) REFERENCES public.plg_agenda_recurrences(id) ON DELETE SET NULL;
14805
+ END IF;
14806
+ END $$;
14807
+
14808
+ -- A pergunta da tela: quais s\xE3o as outras ocorr\xEAncias desta s\xE9rie, em ordem.
14809
+ CREATE INDEX IF NOT EXISTS idx_appointments_series
14810
+ ON public.appointments USING btree (tenant_id, series_id, starts_at)
14811
+ WHERE series_id IS NOT NULL;
14812
+
14813
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
14814
+ -- 3. updated_at
14815
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
14816
+
14817
+ DROP TRIGGER IF EXISTS plg_agenda_recurrences_updated_at ON public.plg_agenda_recurrences;
14818
+ CREATE TRIGGER plg_agenda_recurrences_updated_at
14819
+ BEFORE UPDATE ON public.plg_agenda_recurrences
14820
+ FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
14821
+
14822
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
14823
+ -- 4. Quem enxerga
14824
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
14825
+ --
14826
+ -- A mesma cerca das ocorr\xEAncias: quem pode ler a agenda da unidade pode ler a
14827
+ -- regra das s\xE9ries dela. Regra sem unidade (conta de uma unidade s\xF3) fica no
14828
+ -- alcance de tenant \u2014 ela n\xE3o guarda cliente nem valor, s\xF3 freq e datas.
14829
+
14830
+ ALTER TABLE public.plg_agenda_recurrences ENABLE ROW LEVEL SECURITY;
14831
+ ALTER TABLE public.plg_agenda_recurrences FORCE ROW LEVEL SECURITY;
14832
+ GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE public.plg_agenda_recurrences TO authenticated;
14833
+ GRANT ALL ON TABLE public.plg_agenda_recurrences TO service_role;
14834
+
14835
+ DROP POLICY IF EXISTS plg_agenda_recurrences_authz_select ON public.plg_agenda_recurrences;
14836
+ CREATE POLICY plg_agenda_recurrences_authz_select ON public.plg_agenda_recurrences
14837
+ FOR SELECT TO authenticated USING (
14838
+ tenant_id = (SELECT app.current_tenant_id())
14839
+ AND (plg_agenda_recurrences.unit_id IS NULL OR (SELECT app.has_unit(plg_agenda_recurrences.unit_id)))
14840
+ AND (SELECT app.has_permission('agenda.read', plg_agenda_recurrences.unit_id)));
14841
+
14842
+ DROP POLICY IF EXISTS plg_agenda_recurrences_authz_insert ON public.plg_agenda_recurrences;
14843
+ CREATE POLICY plg_agenda_recurrences_authz_insert ON public.plg_agenda_recurrences
14844
+ FOR INSERT TO authenticated WITH CHECK (
14845
+ tenant_id = (SELECT app.current_tenant_id())
14846
+ AND (plg_agenda_recurrences.unit_id IS NULL OR (SELECT app.has_unit(plg_agenda_recurrences.unit_id)))
14847
+ AND (SELECT app.has_permission('agenda.create', plg_agenda_recurrences.unit_id)));
14848
+
14849
+ DROP POLICY IF EXISTS plg_agenda_recurrences_authz_update ON public.plg_agenda_recurrences;
14850
+ CREATE POLICY plg_agenda_recurrences_authz_update ON public.plg_agenda_recurrences
14851
+ FOR UPDATE TO authenticated USING (
14852
+ tenant_id = (SELECT app.current_tenant_id())
14853
+ AND (plg_agenda_recurrences.unit_id IS NULL OR (SELECT app.has_unit(plg_agenda_recurrences.unit_id)))
14854
+ AND (SELECT app.has_permission('agenda.edit', plg_agenda_recurrences.unit_id)))
14855
+ WITH CHECK (
14856
+ tenant_id = (SELECT app.current_tenant_id())
14857
+ AND (plg_agenda_recurrences.unit_id IS NULL OR (SELECT app.has_unit(plg_agenda_recurrences.unit_id)))
14858
+ AND (SELECT app.has_permission('agenda.edit', plg_agenda_recurrences.unit_id)));
14859
+
14860
+ DROP POLICY IF EXISTS plg_agenda_recurrences_authz_delete ON public.plg_agenda_recurrences;
14861
+ CREATE POLICY plg_agenda_recurrences_authz_delete ON public.plg_agenda_recurrences
14862
+ FOR DELETE TO authenticated USING (
14863
+ tenant_id = (SELECT app.current_tenant_id())
14864
+ AND (plg_agenda_recurrences.unit_id IS NULL OR (SELECT app.has_unit(plg_agenda_recurrences.unit_id)))
14865
+ AND (SELECT app.has_permission('agenda.delete', plg_agenda_recurrences.unit_id)));
14866
+
14867
+ -- N\xC3O entra em \`scoped_resources\`, pela mesma raz\xE3o da 013: aquele registro
14868
+ -- pede \`owner_id\` porque responde "de quem \xE9 esta linha". Uma regra de
14869
+ -- recorr\xEAncia n\xE3o tem dono \u2014 ela descreve as ocorr\xEAncias, e a cerca das
14870
+ -- ocorr\xEAncias j\xE1 est\xE1 em \`appointments\`.
14871
+ `;
14872
+ var MIGRATION_019_A_SERIE_NASCE_NUMA_VIAGEM_SO = `-- 019_a_serie_nasce_numa_viagem_so.sql \u2014 a s\xE9rie inteira em uma ida ao banco.
14873
+ --
14874
+ -- A 018 materializou a s\xE9rie, e o cliente a escrevia no la\xE7o: uma chamada por
14875
+ -- ocorr\xEAncia, em s\xE9rie, e cada ocorr\xEAncia custava QUATRO requisi\xE7\xF5es \u2014 o RPC de
14876
+ -- cria\xE7\xE3o, o update da sala, a sincroniza\xE7\xE3o de convidados e o carimbo do
14877
+ -- \`series_id\`. Uma s\xE9rie de doze ter\xE7as era ~48 viagens, uma depois da outra,
14878
+ -- com a aba aberta o tempo todo. Sessenta (o teto) seriam 240.
13396
14879
  --
13397
- -- \u2500\u2500 Mas existem plg_*_settings no cluster \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
14880
+ -- O custo n\xE3o \xE9 s\xF3 espera. Entre a primeira e a \xFAltima n\xE3o h\xE1 transa\xE7\xE3o: fechar
14881
+ -- a aba no meio deixa metade da s\xE9rie criada, e metade de uma s\xE9rie \xE9 pior que
14882
+ -- nenhuma \u2014 o cliente recebeu doze datas por telefone.
13398
14883
  --
13399
- -- Existem, e n\xE3o contradizem: \`plg_shop_settings\`, \`plg_tables_settings\`,
13400
- -- \`plg_reservations_settings\`, \`plg_crm_engagement_settings\`,
13401
- -- \`plg_inventory_product_settings\`. Todas t\xEAm \`unit_id\` e existem porque a
13402
- -- configura\xE7\xE3o delas varia POR UNIDADE e \xE9 lida por RPC no caminho quente
13403
- -- (fechar comanda, calcular frete) \u2014 n\xE3o por uma tela de toggles.
14884
+ -- Ent\xE3o o la\xE7o desce para o banco. Uma chamada, uma transa\xE7\xE3o, tudo ou nada.
13404
14885
  --
13405
- -- \`online_booking_configs\` n\xE3o \xE9 nenhum dos dois casos: UMA linha no V1
13406
- -- inteiro, tenant 1, e ningu\xE9m a l\xEA num caminho quente. \xC9 toggle.
14886
+ -- N\xC3O \xC9 UM SEGUNDO CAMINHO DE ESCRITA. Esta fun\xE7\xE3o n\xE3o insere em
14887
+ -- \`appointments\`: ela chama \`agenda_booking_create\`, que \xE9 a mesma porta que o
14888
+ -- cliente usava, dentro do la\xE7o. Pedido, linhas, dura\xE7\xE3o e idempot\xEAncia
14889
+ -- continuam decididos num lugar s\xF3 \u2014 o que muda \xE9 de onde o la\xE7o roda. A sala
14890
+ -- e os convidados, que o cliente estampava depois porque a assinatura do RPC
14891
+ -- est\xE1 congelada, entram aqui pelo mesmo motivo e na mesma transa\xE7\xE3o.
14892
+
14893
+ SET check_function_bodies = false;
14894
+
14895
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
14896
+ -- A s\xE9rie, de uma vez
14897
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
13407
14898
  --
13408
- -- A troco de registro, se um dia ela precisar variar por unidade, o lugar certo
13409
- -- \xE9 \`plg_agenda_settings\` com \`unit_id\`, no formato das outras cinco \u2014 e n\xE3o
13410
- -- \`_configs\`, que n\xE3o \xE9 nome que este SDK usa.
14899
+ -- \`p_days\` s\xE3o DIAS locais (\`YYYY-MM-DD\`) j\xE1 expandidos por
14900
+ -- \`lib/recurrence.expandRecurrence\`. A expans\xE3o fica no TypeScript de
14901
+ -- prop\xF3sito: \xE9 a parte que erra se algo errar, e teste de aritm\xE9tica de datas
14902
+ -- n\xE3o deve precisar de banco. O que o banco faz \xE9 escrever.
13411
14903
  --
13412
- -- \u2500\u2500 O que se perde, e \xE9 de prop\xF3sito \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
14904
+ -- \`p_time_of_day\` chega como texto \`HH:MM\` e o instante \xE9 montado AQUI, dia a
14905
+ -- dia, no fuso da sess\xE3o. Mandar 12 timestamps prontos daria no mesmo at\xE9 a
14906
+ -- s\xE9rie cruzar um hor\xE1rio de ver\xE3o \u2014 e a\xED a oitava ocorr\xEAncia cairia uma hora
14907
+ -- fora sem ningu\xE9m ter mexido nela.
14908
+ CREATE OR REPLACE FUNCTION public.agenda_series_create(
14909
+ p_days date[],
14910
+ p_time_of_day text,
14911
+ p_rule jsonb,
14912
+ p_lines jsonb DEFAULT '[]'::jsonb,
14913
+ p_party_id uuid DEFAULT NULL,
14914
+ p_assignee_id uuid DEFAULT NULL,
14915
+ p_unit_id uuid DEFAULT NULL,
14916
+ p_location_id uuid DEFAULT NULL,
14917
+ p_resource_id uuid DEFAULT NULL,
14918
+ p_kind text DEFAULT 'appointment',
14919
+ p_notes text DEFAULT NULL,
14920
+ p_metadata jsonb DEFAULT '{}'::jsonb,
14921
+ p_attendee_ids uuid[] DEFAULT NULL
14922
+ ) RETURNS jsonb
14923
+ LANGUAGE plpgsql SECURITY DEFINER
14924
+ SET search_path TO ''
14925
+ AS $$
14926
+ DECLARE
14927
+ v_tenant uuid := app.current_tenant_id();
14928
+ v_series uuid;
14929
+ v_day date;
14930
+ v_starts timestamptz;
14931
+ v_res jsonb;
14932
+ v_booking uuid;
14933
+ v_ids uuid[] := '{}';
14934
+ v_failed jsonb := '[]'::jsonb;
14935
+ BEGIN
14936
+ IF v_tenant IS NULL THEN
14937
+ RAISE EXCEPTION 'agenda_series_create: no active tenant' USING ERRCODE = '42501';
14938
+ END IF;
14939
+ -- A MESMA permiss\xE3o de uma ocorr\xEAncia. Criar doze n\xE3o \xE9 um poder novo; \xE9 o
14940
+ -- mesmo gesto doze vezes, e \`agenda_booking_create\` vai conferir de novo em
14941
+ -- cada uma. Conferir aqui \xE9 para a s\xE9rie falhar ANTES de escrever a regra.
14942
+ IF NOT (select app.has_permission('agenda.create', p_unit_id)) THEN
14943
+ RAISE EXCEPTION 'agenda_series_create: agenda.create is required' USING ERRCODE = '42501';
14944
+ END IF;
14945
+ IF p_days IS NULL OR array_length(p_days, 1) IS NULL THEN
14946
+ RAISE EXCEPTION 'agenda_series_create: a s\xE9rie n\xE3o tem nenhuma data' USING ERRCODE = '22023';
14947
+ END IF;
14948
+ -- O MESMO teto do expansor (\`RECURRENCE_MAX_OCCURRENCES\`). Aqui porque o
14949
+ -- cliente n\xE3o \xE9 a \xFAnica porta: um agente ou um script chamam este RPC direto,
14950
+ -- e um \`p_days\` de mil datas \xE9 uma transa\xE7\xE3o que segura a tabela.
14951
+ IF array_length(p_days, 1) > 60 THEN
14952
+ RAISE EXCEPTION 'agenda_series_create: uma s\xE9rie tem no m\xE1ximo 60 ocorr\xEAncias' USING ERRCODE = '22023';
14953
+ END IF;
14954
+
14955
+ INSERT INTO public.plg_agenda_recurrences (
14956
+ tenant_id, unit_id, freq, interval_count, starts_on, time_of_day, until_date, occurrence_count
14957
+ ) VALUES (
14958
+ v_tenant, p_unit_id,
14959
+ p_rule ->> 'freq',
14960
+ coalesce((p_rule ->> 'interval')::integer, 1),
14961
+ p_days[1],
14962
+ -- \`time_of_day\` \xE9 \`time\`; o par\xE2metro chega como \`HH:MM\` de texto porque \xE9
14963
+ -- assim que a tela o tem. O cast \xE9 expl\xEDcito para o erro ser aqui, e n\xE3o
14964
+ -- uma s\xE9rie recusada inteira por causa de dois pontos.
14965
+ p_time_of_day::time,
14966
+ nullif(p_rule ->> 'until', '')::date,
14967
+ CASE WHEN nullif(p_rule ->> 'until', '') IS NULL
14968
+ THEN coalesce((p_rule ->> 'count')::integer, array_length(p_days, 1))
14969
+ ELSE NULL END
14970
+ )
14971
+ RETURNING id INTO v_series;
14972
+
14973
+ FOREACH v_day IN ARRAY p_days LOOP
14974
+ v_starts := (v_day::text || ' ' || p_time_of_day)::timestamp AT TIME ZONE current_setting('TimeZone');
14975
+
14976
+ BEGIN
14977
+ v_res := public.agenda_booking_create(
14978
+ p_starts_at => v_starts,
14979
+ p_ends_at => NULL,
14980
+ p_lines => p_lines,
14981
+ p_party_id => p_party_id,
14982
+ p_assignee_id => p_assignee_id,
14983
+ p_unit_id => p_unit_id,
14984
+ p_location_id => p_location_id,
14985
+ p_kind => p_kind,
14986
+ p_notes => p_notes,
14987
+ p_metadata => p_metadata);
14988
+
14989
+ v_booking := (v_res ->> 'appointment_id')::uuid;
14990
+
14991
+ -- S\xE9rie e sala no mesmo UPDATE: as duas s\xE3o colunas que a porta
14992
+ -- congelada de \`agenda_booking_create\` n\xE3o escreve, e separ\xE1-las seria
14993
+ -- escrever a mesma linha duas vezes.
14994
+ UPDATE public.appointments
14995
+ SET series_id = v_series,
14996
+ resource_id = coalesce(p_resource_id, resource_id)
14997
+ WHERE id = v_booking;
14998
+
14999
+ IF p_attendee_ids IS NOT NULL AND array_length(p_attendee_ids, 1) > 0 THEN
15000
+ INSERT INTO public.appointment_attendees (tenant_id, booking_id, person_id, role)
15001
+ SELECT v_tenant, v_booking, a, 'required'
15002
+ FROM unnest(p_attendee_ids) a
15003
+ ON CONFLICT DO NOTHING;
15004
+ END IF;
15005
+
15006
+ v_ids := v_ids || v_booking;
15007
+ EXCEPTION WHEN OTHERS THEN
15008
+ -- UMA DATA QUE N\xC3O ENTROU N\xC3O DERRUBA AS OUTRAS, e tamb\xE9m n\xE3o some: o
15009
+ -- bloco devolve o motivo e a tela conta quantas nasceram. \xC9 a mesma
15010
+ -- regra do conflito \u2014 avisa, n\xE3o impede \u2014, e o \`EXCEPTION\` aninhado
15011
+ -- desfaz s\xF3 esta itera\xE7\xE3o, n\xE3o a s\xE9rie.
15012
+ v_failed := v_failed || jsonb_build_object('date', v_day::text, 'message', SQLERRM);
15013
+ END;
15014
+ END LOOP;
15015
+
15016
+ RETURN jsonb_build_object(
15017
+ 'series_id', v_series,
15018
+ 'booking_ids', to_jsonb(v_ids),
15019
+ 'failed', v_failed);
15020
+ END $$;
15021
+
15022
+ REVOKE ALL ON FUNCTION public.agenda_series_create(date[], text, jsonb, jsonb, uuid, uuid, uuid, uuid, uuid, text, text, jsonb, uuid[]) FROM PUBLIC;
15023
+ GRANT EXECUTE ON FUNCTION public.agenda_series_create(date[], text, jsonb, jsonb, uuid, uuid, uuid, uuid, uuid, text, text, jsonb, uuid[]) TO authenticated, service_role;
15024
+
15025
+ COMMENT ON FUNCTION public.agenda_series_create(date[], text, jsonb, jsonb, uuid, uuid, uuid, uuid, uuid, text, text, jsonb, uuid[]) IS
15026
+ 'Cria a regra de recorr\xEAncia e TODAS as ocorr\xEAncias numa transa\xE7\xE3o, chamando agenda_booking_create por dia. Substitui o la\xE7o de ~4 requisi\xE7\xF5es por ocorr\xEAncia que o cliente fazia.';
15027
+
15028
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
15029
+ -- Mudar a s\xE9rie inteira
15030
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
13413
15031
  --
13414
- -- \`allowed_units\`, \`allowed_services\` e \`allowed_professionals\` viram arrays de
13415
- -- uuid DENTRO do jsonb, sem chave estrangeira. Apagar uma unidade deixa o id
13416
- -- \xF3rf\xE3o na lista. \xC9 a mesma troca que \`tenants.settings.branding\` j\xE1 faz, e
13417
- -- para uma linha ela paga: integridade referencial num array de configura\xE7\xE3o
13418
- -- custaria uma tabela de liga\xE7\xE3o para resolver um problema que o V1 n\xE3o tem em
13419
- -- nenhum tenant.
15032
+ -- S\xF3 o que \xE9 DA S\xC9RIE: o hor\xE1rio, o profissional, a sala, o local, a nota e o
15033
+ -- status. O DIA de cada ocorr\xEAncia \xE9 dela \u2014 mandar um \`starts_at\` \xFAnico para
15034
+ -- todas empilharia a s\xE9rie num dia s\xF3, que \xE9 o defeito que \`planSeriesEdit\`
15035
+ -- existe para n\xE3o deixar acontecer no cliente. Aqui \xE9 a mesma regra, do lado
15036
+ -- do banco, para quem chamar direto.
13420
15037
  --
13421
- -- Idempotente. A tabela nunca recebeu linha: o porte dela ainda n\xE3o rodou.
13422
-
13423
- DO $$
15038
+ -- AS LINHAS (servi\xE7os) N\xC3O ENTRAM. Reescrev\xEA-las pede saber se o agendamento
15039
+ -- tem pedido ou n\xE3o \u2014 a regra que \`updateBooking\` carrega no TypeScript \u2014 e
15040
+ -- duplic\xE1-la aqui seria o segundo caminho de escrita que esta migration
15041
+ -- recusa. Quem muda servi\xE7o numa s\xE9rie inteira continua indo pelo cliente.
15042
+ CREATE OR REPLACE FUNCTION public.agenda_series_update(
15043
+ p_series_id uuid,
15044
+ p_time_of_day text,
15045
+ p_assignee_id uuid DEFAULT NULL,
15046
+ p_location_id uuid DEFAULT NULL,
15047
+ p_resource_id uuid DEFAULT NULL,
15048
+ p_notes text DEFAULT NULL,
15049
+ p_status text DEFAULT NULL,
15050
+ p_touch text[] DEFAULT '{}'
15051
+ ) RETURNS jsonb
15052
+ LANGUAGE plpgsql SECURITY DEFINER
15053
+ SET search_path TO ''
15054
+ AS $$
15055
+ DECLARE
15056
+ v_tenant uuid := app.current_tenant_id();
15057
+ v_unit uuid;
15058
+ v_count integer;
13424
15059
  BEGIN
13425
- IF to_regclass('public.plg_agenda_booking_configs') IS NOT NULL THEN
13426
- IF EXISTS (SELECT 1 FROM public.plg_agenda_booking_configs) THEN
13427
- RAISE EXCEPTION
13428
- 'plg_agenda_booking_configs tem linha \u2014 mover para tenants.settings.plugins.agenda antes de derrubar';
13429
- END IF;
13430
- DROP TABLE public.plg_agenda_booking_configs;
15060
+ IF v_tenant IS NULL THEN
15061
+ RAISE EXCEPTION 'agenda_series_update: no active tenant' USING ERRCODE = '42501';
15062
+ END IF;
15063
+
15064
+ SELECT r.unit_id INTO v_unit FROM public.plg_agenda_recurrences r
15065
+ WHERE r.id = p_series_id AND r.tenant_id = v_tenant;
15066
+ IF NOT FOUND THEN
15067
+ RAISE EXCEPTION 'agenda_series_update: s\xE9rie % n\xE3o encontrada', p_series_id USING ERRCODE = 'P0002';
15068
+ END IF;
15069
+ IF NOT (select app.has_permission('agenda.edit', v_unit)) THEN
15070
+ RAISE EXCEPTION 'agenda_series_update: agenda.edit is required' USING ERRCODE = '42501';
13431
15071
  END IF;
15072
+
15073
+ -- \`p_touch\` diz QUAIS campos a tela mexeu. Sem ele, um NULL seria amb\xEDguo
15074
+ -- entre "n\xE3o mexi" e "esvaziei" \u2014 e desatribuir o profissional de uma s\xE9rie
15075
+ -- \xE9 uma decis\xE3o t\xE3o leg\xEDtima quanto troc\xE1-lo.
15076
+ UPDATE public.appointments a
15077
+ SET starts_at = ((a.starts_at AT TIME ZONE current_setting('TimeZone'))::date::text
15078
+ || ' ' || p_time_of_day)::timestamp AT TIME ZONE current_setting('TimeZone'),
15079
+ ends_at = CASE WHEN a.ends_at IS NULL THEN NULL ELSE
15080
+ ((a.starts_at AT TIME ZONE current_setting('TimeZone'))::date::text
15081
+ || ' ' || p_time_of_day)::timestamp AT TIME ZONE current_setting('TimeZone')
15082
+ + (a.ends_at - a.starts_at) END,
15083
+ assignee_id = CASE WHEN 'assigneeId' = ANY(p_touch) THEN p_assignee_id ELSE a.assignee_id END,
15084
+ location_id = CASE WHEN 'locationId' = ANY(p_touch) THEN p_location_id ELSE a.location_id END,
15085
+ resource_id = CASE WHEN 'resourceId' = ANY(p_touch) THEN p_resource_id ELSE a.resource_id END,
15086
+ notes = CASE WHEN 'notes' = ANY(p_touch) THEN p_notes ELSE a.notes END,
15087
+ status = CASE WHEN 'status' = ANY(p_touch) THEN p_status ELSE a.status END,
15088
+ updated_at = now()
15089
+ WHERE a.series_id = p_series_id
15090
+ AND a.tenant_id = v_tenant;
15091
+ GET DIAGNOSTICS v_count = ROW_COUNT;
15092
+
15093
+ UPDATE public.plg_agenda_recurrences
15094
+ SET time_of_day = p_time_of_day::time, updated_at = now()
15095
+ WHERE id = p_series_id;
15096
+
15097
+ RETURN jsonb_build_object('updated', v_count);
13432
15098
  END $$;
13433
- `;
13434
- var MIGRATION_015_O_FERIADO_TAMBEM_TEM_OBSERVACAO_E_ETIQUETA = `-- ---------------------------------------------------------------------------
13435
- -- O feriado tamb\xE9m tem observa\xE7\xE3o e etiqueta.
13436
- --
13437
- -- Irm\xE3 da 150 do n\xFAcleo e da 103 do financeiro, para o \xFAltimo cadastro que
13438
- -- ficou de fora. Mesmo argumento, e aqui a etiqueta tem um uso imediato:
13439
- -- \xABbanc\xE1rio\xBB, \xABmunicipal\xBB, \xABponto facultativo\xBB s\xE3o recortes que mudam quem
13440
- -- fecha e quem n\xE3o fecha, e que nenhum campo do cadastro responde.
13441
- -- ---------------------------------------------------------------------------
13442
15099
 
13443
- ALTER TABLE public.holidays
13444
- ADD COLUMN IF NOT EXISTS notes text,
13445
- ADD COLUMN IF NOT EXISTS tags text[] DEFAULT '{}'::text[];
15100
+ REVOKE ALL ON FUNCTION public.agenda_series_update(uuid, text, uuid, uuid, uuid, text, text, text[]) FROM PUBLIC;
15101
+ GRANT EXECUTE ON FUNCTION public.agenda_series_update(uuid, text, uuid, uuid, uuid, text, text, text[]) TO authenticated, service_role;
15102
+
15103
+ COMMENT ON FUNCTION public.agenda_series_update(uuid, text, uuid, uuid, uuid, text, text, text[]) IS
15104
+ 'Aplica a mesma mudan\xE7a a todas as ocorr\xEAncias da s\xE9rie num UPDATE s\xF3, preservando o DIA de cada uma. N\xE3o toca nas linhas de servi\xE7o \u2014 essa regra vive no cliente.';
13446
15105
  `;
13447
15106
  var MIGRATIONS = [
13448
15107
  { id: "000_baseline", sql: MIGRATION_000_BASELINE },
@@ -13460,8 +15119,549 @@ var MIGRATIONS = [
13460
15119
  { id: "012_a_sala_sem_nome_tambem_e_uma_sala", sql: MIGRATION_012_A_SALA_SEM_NOME_TAMBEM_E_UMA_SALA },
13461
15120
  { id: "013_o_selo_do_atendimento_tem_onde_morar", sql: MIGRATION_013_O_SELO_DO_ATENDIMENTO_TEM_ONDE_MORAR },
13462
15121
  { id: "014_a_configuracao_nao_e_uma_tabela", sql: MIGRATION_014_A_CONFIGURACAO_NAO_E_UMA_TABELA },
13463
- { id: "015_o_feriado_tambem_tem_observacao_e_etiqueta", sql: MIGRATION_015_O_FERIADO_TAMBEM_TEM_OBSERVACAO_E_ETIQUETA }
15122
+ { id: "015_o_feriado_tambem_tem_observacao_e_etiqueta", sql: MIGRATION_015_O_FERIADO_TAMBEM_TEM_OBSERVACAO_E_ETIQUETA },
15123
+ { id: "016_a_vez_e_um_fato", sql: MIGRATION_016_A_VEZ_E_UM_FATO },
15124
+ { id: "017_a_equipe_do_dia_aparece", sql: MIGRATION_017_A_EQUIPE_DO_DIA_APARECE },
15125
+ { id: "018_o_agendamento_que_se_repete", sql: MIGRATION_018_O_AGENDAMENTO_QUE_SE_REPETE },
15126
+ { id: "019_a_serie_nasce_numa_viagem_so", sql: MIGRATION_019_A_SERIE_NASCE_NUMA_VIAGEM_SO }
13464
15127
  ];
15128
+ var channelSequence = 0;
15129
+ function client() {
15130
+ const value = getSupabaseClientOptional();
15131
+ if (!value) throw new Error("Supabase not initialized");
15132
+ return value;
15133
+ }
15134
+ function createSupabaseTurnQueueProvider() {
15135
+ return {
15136
+ async getSnapshot(unitId) {
15137
+ const { data, error } = await client().rpc("agenda_turn_queue_snapshot", { p_unit_id: unitId });
15138
+ if (error) throw error;
15139
+ const snapshot = data ?? {};
15140
+ return {
15141
+ queueId: snapshot.queue_id ?? null,
15142
+ unitId: snapshot.unit_id ?? unitId,
15143
+ queueName: snapshot.queue_name ?? "Pr\xF3ximo da vez",
15144
+ currentProfessionalId: snapshot.current_professional_id ?? null,
15145
+ professionals: (snapshot.professionals ?? []).map((row) => ({
15146
+ professionalId: row.professional_id,
15147
+ professionalName: row.professional_name ?? "Profissional",
15148
+ professionalAvatarUrl: row.professional_avatar_url ?? null,
15149
+ status: row.status ?? "unavailable",
15150
+ position: Number(row.position ?? 0),
15151
+ lastCheckinAt: row.last_checkin_at ?? null,
15152
+ isScheduled: Boolean(row.is_scheduled),
15153
+ appointmentCount: Number(row.appointment_count ?? 0),
15154
+ nextAppointmentAt: row.next_appointment_at ?? null
15155
+ })),
15156
+ entries: (snapshot.entries ?? []).map((row) => ({
15157
+ id: row.id,
15158
+ queueId: row.queue_id,
15159
+ unitId: row.unit_id,
15160
+ professionalId: row.professional_id,
15161
+ professionalName: row.professional_name ?? "Profissional",
15162
+ professionalAvatarUrl: row.professional_avatar_url ?? null,
15163
+ status: row.status,
15164
+ position: Number(row.position ?? 0),
15165
+ stateChangedAt: row.state_changed_at,
15166
+ availableAt: row.available_at ?? null,
15167
+ appointmentId: row.appointment_id ?? null
15168
+ })),
15169
+ events: (snapshot.events ?? []).map((row) => ({
15170
+ id: row.id,
15171
+ professionalId: row.professional_id,
15172
+ professionalName: row.professional_name ?? "Profissional",
15173
+ eventType: row.event_type,
15174
+ origin: row.origin,
15175
+ reason: row.reason ?? null,
15176
+ occurredAt: row.occurred_at
15177
+ }))
15178
+ };
15179
+ },
15180
+ async setAvailability(input) {
15181
+ const { data, error } = await client().rpc("agenda_turn_queue_set_availability", {
15182
+ p_available: input.available,
15183
+ p_unit_id: input.unitId,
15184
+ p_latitude: input.latitude ?? null,
15185
+ p_longitude: input.longitude ?? null,
15186
+ p_accuracy_meters: input.accuracyMeters ?? null,
15187
+ p_origin: "app"
15188
+ });
15189
+ if (error) throw error;
15190
+ return data ?? { ok: false };
15191
+ },
15192
+ subscribe(onChange) {
15193
+ const db = client();
15194
+ const channelName = `agenda:turn-queue:${getActiveTenantId() ?? "active"}:${++channelSequence}`;
15195
+ const channel = db.channel(channelName).on("postgres_changes", { event: "*", schema: "public", table: "plg_agenda_turn_entries" }, onChange).on("postgres_changes", { event: "INSERT", schema: "public", table: "plg_agenda_turn_events" }, onChange).subscribe();
15196
+ return () => {
15197
+ void db.removeChannel(channel);
15198
+ };
15199
+ }
15200
+ };
15201
+ }
15202
+ function createMockTurnQueueProvider() {
15203
+ const now = () => (/* @__PURE__ */ new Date()).toISOString();
15204
+ const entries = [
15205
+ { id: "turn-1", queueId: "queue-1", unitId: "loc-1", professionalId: "prof-1", professionalName: "Ana Silva", professionalAvatarUrl: null, status: "available", position: 1, stateChangedAt: now(), availableAt: now(), appointmentId: null },
15206
+ { id: "turn-2", queueId: "queue-1", unitId: "loc-1", professionalId: "prof-2", professionalName: "Carlos Santos", professionalAvatarUrl: null, status: "available", position: 2, stateChangedAt: now(), availableAt: now(), appointmentId: null }
15207
+ ];
15208
+ const events = [];
15209
+ const professionals = [
15210
+ { professionalId: "prof-1", professionalName: "Ana Silva", professionalAvatarUrl: null, status: "available", position: 1, lastCheckinAt: now(), isScheduled: true, appointmentCount: 2, nextAppointmentAt: null },
15211
+ { professionalId: "prof-2", professionalName: "Carlos Santos", professionalAvatarUrl: null, status: "available", position: 2, lastCheckinAt: now(), isScheduled: true, appointmentCount: 1, nextAppointmentAt: null },
15212
+ { professionalId: "prof-3", professionalName: "Bia Costa", professionalAvatarUrl: null, status: "unavailable", position: 0, lastCheckinAt: null, isScheduled: false, appointmentCount: 1, nextAppointmentAt: now() }
15213
+ ];
15214
+ const listeners2 = /* @__PURE__ */ new Set();
15215
+ return {
15216
+ async getSnapshot(unitId) {
15217
+ return { queueId: "queue-1", unitId, queueName: "Pr\xF3ximo da vez", entries: [...entries], events: [...events], professionals: [...professionals], currentProfessionalId: "prof-1" };
15218
+ },
15219
+ async setAvailability(input) {
15220
+ const row = entries.find((entry) => entry.professionalId === "prof-1");
15221
+ if (!row) return { ok: false, reason: "sem_ficha_de_profissional" };
15222
+ row.status = input.available ? "available" : "unavailable";
15223
+ row.stateChangedAt = now();
15224
+ if (input.available) {
15225
+ row.position = Math.max(0, ...entries.map((entry) => entry.position)) + 1;
15226
+ row.availableAt = row.stateChangedAt;
15227
+ }
15228
+ events.unshift({ id: `event-${events.length + 1}`, professionalId: row.professionalId, professionalName: row.professionalName, eventType: input.available ? "available" : "left", origin: "app", reason: null, occurredAt: row.stateChangedAt });
15229
+ const professional = professionals.find((candidate) => candidate.professionalId === row.professionalId);
15230
+ if (professional) {
15231
+ professional.status = row.status;
15232
+ professional.position = row.position;
15233
+ if (input.available) professional.lastCheckinAt = row.stateChangedAt;
15234
+ }
15235
+ listeners2.forEach((listener) => listener());
15236
+ return { ok: true, status: row.status };
15237
+ },
15238
+ subscribe(onChange) {
15239
+ listeners2.add(onChange);
15240
+ return () => listeners2.delete(onChange);
15241
+ }
15242
+ };
15243
+ }
15244
+ function position() {
15245
+ return new Promise((resolve, reject) => {
15246
+ if (!navigator.geolocation) {
15247
+ reject(new Error("geolocation_unavailable"));
15248
+ return;
15249
+ }
15250
+ navigator.geolocation.getCurrentPosition(resolve, reject, {
15251
+ enableHighAccuracy: true,
15252
+ timeout: 12e3,
15253
+ maximumAge: 3e4
15254
+ });
15255
+ });
15256
+ }
15257
+ function failureMessage(error) {
15258
+ const message = String(error?.message ?? error ?? "");
15259
+ if (message.includes("outside_geofence")) {
15260
+ return "Sua localiza\xE7\xE3o n\xE3o permite dizer que est\xE1 dispon\xEDvel ainda. Quando chegar na loja, aperte novamente.";
15261
+ }
15262
+ if (message.includes("unit_has_no_coordinates")) {
15263
+ return "A unidade ainda n\xE3o tem localiza\xE7\xE3o configurada para validar sua disponibilidade.";
15264
+ }
15265
+ if (message.includes("sem_ficha_de_profissional")) {
15266
+ return "Seu acesso ainda n\xE3o est\xE1 vinculado a uma ficha de profissional.";
15267
+ }
15268
+ if (message.includes("unit_required")) return "Escolha a unidade antes de marcar disponibilidade.";
15269
+ return "N\xE3o foi poss\xEDvel validar sua disponibilidade agora.";
15270
+ }
15271
+ function AvailabilityHeaderControl({ provider: provider2 }) {
15272
+ const unitId = useActiveUnitId();
15273
+ const [status, setStatus] = React21.useState(null);
15274
+ const [busy, setBusy] = React21.useState(false);
15275
+ const refresh = React21.useCallback(async () => {
15276
+ if (!unitId) {
15277
+ setStatus(null);
15278
+ return;
15279
+ }
15280
+ try {
15281
+ const snapshot = await provider2.getSnapshot(unitId);
15282
+ const own = snapshot.entries.find((entry) => entry.professionalId === snapshot.currentProfessionalId);
15283
+ const leader = [...snapshot.entries].filter((entry) => entry.status === "available").sort((a, b) => a.position - b.position)[0];
15284
+ publishTurnQueueLeader(unitId, leader?.professionalId ?? null);
15285
+ setStatus(own?.status ?? "unavailable");
15286
+ } catch {
15287
+ setStatus(null);
15288
+ }
15289
+ }, [provider2, unitId]);
15290
+ React21.useEffect(() => {
15291
+ void refresh();
15292
+ return provider2.subscribe(() => {
15293
+ void refresh();
15294
+ });
15295
+ }, [provider2, refresh]);
15296
+ async function toggle() {
15297
+ if (!unitId) {
15298
+ toast.error("Escolha a unidade antes de marcar disponibilidade.");
15299
+ return;
15300
+ }
15301
+ setBusy(true);
15302
+ try {
15303
+ if (status === "available") {
15304
+ const result = await provider2.setAvailability({ available: false, unitId });
15305
+ if (!result.ok) throw new Error(result.reason);
15306
+ setStatus("unavailable");
15307
+ toast.success("Voc\xEA saiu da vez.");
15308
+ } else {
15309
+ const geo = await position();
15310
+ const result = await provider2.setAvailability({
15311
+ available: true,
15312
+ unitId,
15313
+ latitude: geo.coords.latitude,
15314
+ longitude: geo.coords.longitude,
15315
+ accuracyMeters: geo.coords.accuracy
15316
+ });
15317
+ if (!result.ok) throw new Error(result.reason);
15318
+ setStatus("available");
15319
+ toast.success("Voc\xEA est\xE1 dispon\xEDvel e entrou no fim da fila.");
15320
+ }
15321
+ } catch (error) {
15322
+ toast.error(failureMessage(error));
15323
+ } finally {
15324
+ setBusy(false);
15325
+ }
15326
+ }
15327
+ const available = status === "available";
15328
+ return /* @__PURE__ */ jsx(PermissionGate, { feature: "agenda", action: "edit", children: /* @__PURE__ */ jsxs(
15329
+ "button",
15330
+ {
15331
+ type: "button",
15332
+ role: "switch",
15333
+ "aria-checked": available,
15334
+ className: "inline-flex min-h-11 items-center gap-2 rounded-full px-1.5 text-xs font-medium text-foreground transition-opacity disabled:cursor-not-allowed disabled:opacity-60 md:hidden",
15335
+ disabled: busy,
15336
+ onClick: () => {
15337
+ void toggle();
15338
+ },
15339
+ "aria-label": available ? "Sair da vez" : "Estou dispon\xEDvel",
15340
+ children: [
15341
+ /* @__PURE__ */ jsx("span", { className: "hidden min-[390px]:inline", children: "Dispon\xEDvel" }),
15342
+ /* @__PURE__ */ jsx(
15343
+ "span",
15344
+ {
15345
+ "aria-hidden": true,
15346
+ className: `relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors ${available ? "bg-success" : "bg-muted-foreground/35"}`,
15347
+ children: /* @__PURE__ */ jsx("span", { className: `inline-flex h-5 w-5 items-center justify-center rounded-full bg-white shadow-sm transition-transform ${available ? "translate-x-[1.375rem]" : "translate-x-0.5"}`, children: busy && /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin text-muted-foreground" }) })
15348
+ }
15349
+ )
15350
+ ]
15351
+ }
15352
+ ) });
15353
+ }
15354
+ function initials(name) {
15355
+ return name.split(/\s+/).slice(0, 2).map((part) => part[0]).join("").toUpperCase();
15356
+ }
15357
+ function elapsed(iso) {
15358
+ const minutes = Math.max(0, Math.floor((Date.now() - new Date(iso).getTime()) / 6e4));
15359
+ if (minutes < 1) return "agora";
15360
+ if (minutes < 60) return `h\xE1 ${minutes} min`;
15361
+ const hours = Math.floor(minutes / 60);
15362
+ return `h\xE1 ${hours}h${minutes % 60 ? ` ${minutes % 60}min` : ""}`;
15363
+ }
15364
+ var STATUS = {
15365
+ available: { label: "Dispon\xEDvel", className: "bg-success/15 text-success" },
15366
+ serving: { label: "Em atendimento", className: "bg-primary/10 text-primary" },
15367
+ unavailable: { label: "Fora", className: "bg-muted text-muted-foreground" }
15368
+ };
15369
+ function time(iso) {
15370
+ return new Date(iso).toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" });
15371
+ }
15372
+ function localNow() {
15373
+ const now = /* @__PURE__ */ new Date();
15374
+ const pad = (value) => String(value).padStart(2, "0");
15375
+ return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}:00`;
15376
+ }
15377
+ function ProfessionalRoster({ professionals }) {
15378
+ return /* @__PURE__ */ jsx(Card, { className: "overflow-hidden lg:sticky lg:top-4", children: /* @__PURE__ */ jsxs(CardContent, { className: "p-0", children: [
15379
+ /* @__PURE__ */ jsx("div", { className: "border-b border-border/70 p-4", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3", children: [
15380
+ /* @__PURE__ */ jsxs("div", { children: [
15381
+ /* @__PURE__ */ jsx("h2", { className: "font-semibold", children: "Profissionais hoje" }),
15382
+ /* @__PURE__ */ jsxs("p", { className: "mt-0.5 text-xs text-muted-foreground", children: [
15383
+ "Nesta unidade \xB7 ",
15384
+ professionals.length,
15385
+ " na agenda"
15386
+ ] })
15387
+ ] }),
15388
+ /* @__PURE__ */ jsx("div", { className: "flex -space-x-2", children: professionals.slice(0, 4).map((professional) => /* @__PURE__ */ jsxs(Avatar, { className: "h-7 w-7 border-2 border-card", children: [
15389
+ /* @__PURE__ */ jsx(AvatarImage, { src: professional.professionalAvatarUrl ?? void 0 }),
15390
+ /* @__PURE__ */ jsx(AvatarFallback, { className: "text-[9px]", children: initials(professional.professionalName) })
15391
+ ] }, professional.professionalId)) })
15392
+ ] }) }),
15393
+ professionals.length === 0 ? /* @__PURE__ */ jsx("div", { className: "p-5 text-center text-sm text-muted-foreground", children: "Nenhum profissional na agenda desta unidade hoje." }) : /* @__PURE__ */ jsx("div", { className: "max-h-[calc(100vh-16rem)] divide-y divide-border/60 overflow-y-auto", children: professionals.map((professional) => {
15394
+ const available = professional.status === "available";
15395
+ const serving = professional.status === "serving";
15396
+ return /* @__PURE__ */ jsxs("div", { className: "flex gap-3 p-3.5", children: [
15397
+ /* @__PURE__ */ jsxs("div", { className: "relative shrink-0", children: [
15398
+ /* @__PURE__ */ jsxs(Avatar, { className: "h-11 w-11", children: [
15399
+ /* @__PURE__ */ jsx(AvatarImage, { src: professional.professionalAvatarUrl ?? void 0 }),
15400
+ /* @__PURE__ */ jsx(AvatarFallback, { children: initials(professional.professionalName) })
15401
+ ] }),
15402
+ /* @__PURE__ */ jsx("span", { className: `absolute bottom-0 right-0 h-3 w-3 rounded-full border-2 border-card ${available ? "bg-success" : serving ? "bg-primary" : "bg-muted-foreground/50"}` })
15403
+ ] }),
15404
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
15405
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-2", children: [
15406
+ /* @__PURE__ */ jsx("p", { className: "truncate text-sm font-medium", children: professional.professionalName }),
15407
+ available && professional.position > 0 ? /* @__PURE__ */ jsxs("span", { className: "text-xs font-semibold tabular-nums text-primary", children: [
15408
+ "#",
15409
+ professional.position
15410
+ ] }) : null
15411
+ ] }),
15412
+ /* @__PURE__ */ jsx("p", { className: `mt-0.5 text-xs font-medium ${available ? "text-success" : serving ? "text-primary" : "text-muted-foreground"}`, children: available ? "Dispon\xEDvel" : serving ? "Em atendimento" : "Indispon\xEDvel" }),
15413
+ /* @__PURE__ */ jsxs("div", { className: "mt-1.5 flex flex-wrap gap-x-3 gap-y-1 text-[11px] text-muted-foreground", children: [
15414
+ /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1", children: [
15415
+ /* @__PURE__ */ jsx(Clock3, { className: "h-3 w-3" }),
15416
+ professional.lastCheckinAt ? `Check-in ${time(professional.lastCheckinAt)}` : "Sem check-in hoje"
15417
+ ] }),
15418
+ professional.appointmentCount > 0 ? /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1", children: [
15419
+ /* @__PURE__ */ jsx(CalendarDays, { className: "h-3 w-3" }),
15420
+ professional.appointmentCount,
15421
+ " ",
15422
+ professional.appointmentCount === 1 ? "hor\xE1rio" : "hor\xE1rios"
15423
+ ] }) : null
15424
+ ] })
15425
+ ] })
15426
+ ] }, professional.professionalId);
15427
+ }) })
15428
+ ] }) });
15429
+ }
15430
+ function QueueSkeleton() {
15431
+ return /* @__PURE__ */ jsx("div", { className: "grid gap-3", children: [0, 1, 2].map((i) => /* @__PURE__ */ jsx(Skeleton, { className: "h-20 rounded-xl" }, i)) });
15432
+ }
15433
+ function EmptyUnit() {
15434
+ return /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { className: "flex min-h-64 flex-col items-center justify-center gap-3 text-center", children: [
15435
+ /* @__PURE__ */ jsx(MapPin, { className: "h-9 w-9 text-muted-foreground" }),
15436
+ /* @__PURE__ */ jsxs("div", { children: [
15437
+ /* @__PURE__ */ jsx("p", { className: "font-medium", children: "Escolha uma unidade" }),
15438
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: "A lista da vez sempre pertence a uma unidade." })
15439
+ ] })
15440
+ ] }) });
15441
+ }
15442
+ function TurnQueuePage({ provider: provider2, agendaProvider, title, subtitle }) {
15443
+ const unitId = useActiveUnitId();
15444
+ const [agendaProfessionals, setAgendaProfessionals] = React21.useState([]);
15445
+ const [snapshot, setSnapshot] = React21.useState(null);
15446
+ const [loading, setLoading] = React21.useState(false);
15447
+ const [error, setError] = React21.useState(null);
15448
+ const [, tick] = React21.useState(0);
15449
+ const load = React21.useCallback(async () => {
15450
+ if (!unitId) {
15451
+ setSnapshot(null);
15452
+ return;
15453
+ }
15454
+ setLoading(true);
15455
+ try {
15456
+ setSnapshot(await provider2.getSnapshot(unitId));
15457
+ setError(null);
15458
+ } catch (cause) {
15459
+ setError(String(cause?.message ?? "N\xE3o foi poss\xEDvel carregar a fila."));
15460
+ } finally {
15461
+ setLoading(false);
15462
+ }
15463
+ }, [provider2, unitId]);
15464
+ React21.useEffect(() => {
15465
+ void load();
15466
+ }, [load]);
15467
+ React21.useEffect(() => {
15468
+ if (!unitId) {
15469
+ setAgendaProfessionals([]);
15470
+ return;
15471
+ }
15472
+ let active = true;
15473
+ void agendaProvider.getProfessionals({ unitId }).then((rows2) => {
15474
+ if (active) setAgendaProfessionals(rows2);
15475
+ }).catch(() => {
15476
+ if (active) setAgendaProfessionals([]);
15477
+ });
15478
+ return () => {
15479
+ active = false;
15480
+ };
15481
+ }, [agendaProvider, unitId]);
15482
+ React21.useEffect(() => provider2.subscribe(() => {
15483
+ void load();
15484
+ }), [provider2, load]);
15485
+ React21.useEffect(() => {
15486
+ const timer = window.setInterval(() => tick((value) => value + 1), 6e4);
15487
+ return () => window.clearInterval(timer);
15488
+ }, []);
15489
+ const rows = React21.useMemo(() => [...snapshot?.entries ?? []].sort((a, b) => {
15490
+ const rank = { available: 0, serving: 1, unavailable: 2 };
15491
+ return rank[a.status] - rank[b.status] || a.position - b.position;
15492
+ }), [snapshot]);
15493
+ const queueRows = rows.filter((row) => row.status !== "unavailable");
15494
+ const professionals = React21.useMemo(() => {
15495
+ const byId = /* @__PURE__ */ new Map();
15496
+ for (const professional of agendaProfessionals) {
15497
+ byId.set(professional.id, {
15498
+ professionalId: professional.id,
15499
+ professionalName: professional.name,
15500
+ professionalAvatarUrl: professional.avatarUrl,
15501
+ status: "unavailable",
15502
+ position: 0,
15503
+ lastCheckinAt: null,
15504
+ isScheduled: true,
15505
+ appointmentCount: 0,
15506
+ nextAppointmentAt: null
15507
+ });
15508
+ }
15509
+ for (const professional of snapshot?.professionals ?? []) {
15510
+ byId.set(professional.professionalId, professional);
15511
+ }
15512
+ const rank = { available: 0, serving: 1, unavailable: 2 };
15513
+ return [...byId.values()].sort((a, b) => rank[a.status] - rank[b.status] || a.position - b.position || a.professionalName.localeCompare(b.professionalName));
15514
+ }, [agendaProfessionals, snapshot]);
15515
+ const available = professionals.filter((row) => row.status === "available").length;
15516
+ const serving = professionals.filter((row) => row.status === "serving").length;
15517
+ const addWalkIn = React21.useCallback((professionalId) => {
15518
+ if (!unitId) return;
15519
+ window.location.hash = "/agenda";
15520
+ window.setTimeout(() => {
15521
+ window.dispatchEvent(new CustomEvent("agenda:open-booking", {
15522
+ detail: {
15523
+ prefill: {
15524
+ professionalId,
15525
+ locationId: unitId,
15526
+ startsAt: localNow()
15527
+ }
15528
+ }
15529
+ }));
15530
+ }, 150);
15531
+ }, [unitId]);
15532
+ return /* @__PURE__ */ jsx(ModulePage, { title, subtitle, nav: [], navVariant: "sidebar", children: !unitId ? /* @__PURE__ */ jsx(EmptyUnit, {}) : /* @__PURE__ */ jsxs("div", { className: `mx-auto grid w-full gap-4 lg:items-start ${professionals.length > 0 ? "max-w-7xl lg:grid-cols-[19rem_minmax(0,1fr)]" : "max-w-5xl"}`, children: [
15533
+ professionals.length > 0 ? /* @__PURE__ */ jsx("aside", { className: "min-w-0", children: /* @__PURE__ */ jsx(ProfessionalRoster, { professionals }) }) : null,
15534
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 space-y-4", children: [
15535
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-3 sm:grid-cols-3", children: [
15536
+ /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { className: "p-4", children: [
15537
+ /* @__PURE__ */ jsx(Users, { className: "mb-2 h-4 w-4 text-muted-foreground" }),
15538
+ /* @__PURE__ */ jsx("p", { className: "text-2xl font-semibold tabular-nums", children: available }),
15539
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "dispon\xEDveis" })
15540
+ ] }) }),
15541
+ /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { className: "p-4", children: [
15542
+ /* @__PURE__ */ jsx(Activity, { className: "mb-2 h-4 w-4 text-muted-foreground" }),
15543
+ /* @__PURE__ */ jsx("p", { className: "text-2xl font-semibold tabular-nums", children: serving }),
15544
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "em atendimento" })
15545
+ ] }) }),
15546
+ /* @__PURE__ */ jsx(Card, { className: "col-span-2 sm:col-span-1", children: /* @__PURE__ */ jsxs(CardContent, { className: "flex h-full items-center justify-between p-4", children: [
15547
+ /* @__PURE__ */ jsxs("div", { children: [
15548
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: "Atualiza\xE7\xE3o ao vivo" }),
15549
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "Todos veem a mesma ordem" })
15550
+ ] }),
15551
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", onClick: () => {
15552
+ void load();
15553
+ }, "aria-label": "Atualizar", children: /* @__PURE__ */ jsx(RefreshCw, { className: "h-4 w-4" }) })
15554
+ ] }) })
15555
+ ] }),
15556
+ loading && !snapshot ? /* @__PURE__ */ jsx(QueueSkeleton, {}) : error ? /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx(CardContent, { className: "p-6 text-sm text-destructive", children: error }) }) : queueRows.length === 0 ? /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { className: "flex min-h-52 flex-col items-center justify-center text-center", children: [
15557
+ /* @__PURE__ */ jsx(Users, { className: "mb-3 h-9 w-9 text-muted-foreground" }),
15558
+ /* @__PURE__ */ jsx("p", { className: "font-medium", children: "Ningu\xE9m dispon\xEDvel na vez" }),
15559
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: professionals.length > 0 ? "A equipe prevista para hoje continua vis\xEDvel ao lado, mesmo sem check-in." : "Nenhum profissional est\xE1 previsto para esta unidade hoje." })
15560
+ ] }) }) : /* @__PURE__ */ jsx("div", { className: "grid gap-2", children: queueRows.map((row) => {
15561
+ const state = STATUS[row.status];
15562
+ const queuePosition = row.status === "available" ? queueRows.filter((candidate) => candidate.status === "available").findIndex((candidate) => candidate.id === row.id) + 1 : null;
15563
+ const isNext = row.status === "available" && queuePosition === 1;
15564
+ return /* @__PURE__ */ jsx(Card, { className: isNext ? "overflow-hidden border-black bg-zinc-950 text-white shadow-lg ring-1 ring-black/15" : "", children: /* @__PURE__ */ jsxs(CardContent, { className: "flex items-center gap-3 p-3 sm:p-4", children: [
15565
+ /* @__PURE__ */ jsx("div", { className: `flex h-9 w-9 shrink-0 items-center justify-center rounded-full text-sm font-semibold tabular-nums ${isNext ? "border border-white/50 bg-black text-white shadow-sm" : "bg-muted"}`, children: queuePosition ?? "\u2014" }),
15566
+ /* @__PURE__ */ jsxs(Avatar, { className: `h-10 w-10 ${isNext ? "border border-white/50 bg-black shadow-sm" : ""}`, children: [
15567
+ /* @__PURE__ */ jsx(AvatarImage, { src: row.professionalAvatarUrl ?? void 0 }),
15568
+ /* @__PURE__ */ jsx(AvatarFallback, { className: isNext ? "bg-black text-white" : void 0, children: initials(row.professionalName) })
15569
+ ] }),
15570
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
15571
+ /* @__PURE__ */ jsx("p", { className: "truncate font-medium", children: row.professionalName }),
15572
+ /* @__PURE__ */ jsxs("p", { className: `mt-0.5 flex items-center gap-1 text-xs ${isNext ? "text-background/70" : "text-muted-foreground"}`, children: [
15573
+ /* @__PURE__ */ jsx(Clock3, { className: "h-3 w-3" }),
15574
+ isNext ? `Da vez \xB7 ${elapsed(row.stateChangedAt)}` : elapsed(row.stateChangedAt)
15575
+ ] })
15576
+ ] }),
15577
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [
15578
+ /* @__PURE__ */ jsx(Badge, { className: isNext ? "border border-white/25 bg-white/10 text-white hover:bg-white/10" : state.className, children: isNext ? "Pr\xF3ximo" : state.label }),
15579
+ isNext ? /* @__PURE__ */ jsx(PermissionGate, { feature: "agenda", action: "create", children: /* @__PURE__ */ jsxs(
15580
+ Button,
15581
+ {
15582
+ type: "button",
15583
+ size: "sm",
15584
+ variant: "outline",
15585
+ className: "h-8 border-white/35 bg-white/10 text-white shadow-none hover:bg-white/20 hover:text-white",
15586
+ onClick: () => addWalkIn(row.professionalId),
15587
+ children: [
15588
+ /* @__PURE__ */ jsx(CalendarPlus, { className: "mr-1.5 h-3.5 w-3.5" }),
15589
+ "Adicionar encaixe"
15590
+ ]
15591
+ }
15592
+ ) }) : null
15593
+ ] })
15594
+ ] }) }, row.id);
15595
+ }) }),
15596
+ (snapshot?.events.length ?? 0) > 0 && /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { className: "p-4", children: [
15597
+ /* @__PURE__ */ jsx("h2", { className: "mb-3 text-sm font-semibold", children: "Movimentos de hoje" }),
15598
+ /* @__PURE__ */ jsx("div", { className: "grid gap-2", children: snapshot.events.slice(0, 12).map((event) => /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-4 border-b border-border/60 py-2 text-sm last:border-0", children: [
15599
+ /* @__PURE__ */ jsxs("div", { children: [
15600
+ /* @__PURE__ */ jsx("p", { className: "font-medium", children: event.professionalName }),
15601
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: event.reason ?? event.eventType.replace(/_/g, " ") })
15602
+ ] }),
15603
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs text-muted-foreground", children: new Date(event.occurredAt).toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" }) })
15604
+ ] }, event.id)) })
15605
+ ] }) })
15606
+ ] })
15607
+ ] }) });
15608
+ }
15609
+
15610
+ // src/lib/turn-queue-plugin.ts
15611
+ function createAgendaTurnQueuePlugin(options = {}) {
15612
+ const provider2 = options.dataProvider ?? createSafeDataProvider(
15613
+ () => createSupabaseTurnQueueProvider(),
15614
+ () => createMockTurnQueueProvider()
15615
+ );
15616
+ const agendaProvider = createSafeDataProvider(
15617
+ () => createSupabaseAgendaProvider(),
15618
+ () => createMockAgendaProvider()
15619
+ );
15620
+ const menu = options.labels?.menu ?? "Pr\xF3ximo da vez";
15621
+ const title = options.labels?.title ?? menu;
15622
+ const subtitle = options.labels?.subtitle ?? "Ordem ao vivo dos profissionais dispon\xEDveis nesta unidade";
15623
+ const Page = (() => React21.createElement(TurnQueuePage, { provider: provider2, agendaProvider, title, subtitle }));
15624
+ const HeaderControl = (() => React21.createElement(AvailabilityHeaderControl, { provider: provider2 }));
15625
+ return {
15626
+ id: "agenda-turn-queue",
15627
+ name: menu,
15628
+ icon: "ListOrdered",
15629
+ version: "1.0.0",
15630
+ scope: "addon",
15631
+ verticalId: "beauty",
15632
+ scaffolds: ["saas"],
15633
+ defaultEnabled: false,
15634
+ dependencies: ["agenda"],
15635
+ hostPluginId: "agenda",
15636
+ facets: [{ id: "turn_queue", label: menu, description: "Distribui clientes de passagem pela ordem de disponibilidade.", includedWithPlugin: true }],
15637
+ declaredFeatures: [{
15638
+ id: "agenda.turn_queue",
15639
+ label: menu,
15640
+ group: "Agenda",
15641
+ actions: ["read", "create", "edit", "manage"]
15642
+ }],
15643
+ navigation: [{
15644
+ section: options.navSection ?? "main",
15645
+ position: options.navPosition ?? 2.3,
15646
+ label: menu,
15647
+ route: "/agenda/turn-queue",
15648
+ icon: "ListOrdered",
15649
+ permission: { feature: "agenda", action: "read" }
15650
+ }],
15651
+ routes: [{
15652
+ path: "/agenda/turn-queue",
15653
+ component: Page,
15654
+ permission: { feature: "agenda", action: "read" },
15655
+ title
15656
+ }],
15657
+ headerControls: [{
15658
+ id: "agenda-turn-queue.availability",
15659
+ component: HeaderControl,
15660
+ order: 70,
15661
+ permission: { feature: "agenda", action: "read" }
15662
+ }]
15663
+ };
15664
+ }
13465
15665
  function computeCommissionAmount(total, ratePercent) {
13466
15666
  const base = Number(total) || 0;
13467
15667
  const rate = Number(ratePercent) || 0;
@@ -16775,28 +18975,28 @@ function createAgendaPlugin(options) {
16775
18975
  }
16776
18976
  }
16777
18977
  ]);
16778
- const PageComponent = () => React20.createElement(AgendaPage, { config, provider: provider2, store });
18978
+ const PageComponent = () => React21.createElement(AgendaPage, { config, provider: provider2, store });
16779
18979
  const PersonAttendancesBound = (props) => (
16780
18980
  // O provider do PLUGIN, sem a moldura da página: dentro de uma aba a
16781
18981
  // `AgendaPage` traria a altura cheia e a rolagem própria do calendário, e a
16782
18982
  // lista passaria a rolar dentro de uma caixa que já rola.
16783
- React20.createElement(
18983
+ React21.createElement(
16784
18984
  AgendaContextProvider,
16785
18985
  { config, provider: provider2, store },
16786
- React20.createElement(PersonAttendancesWidget, { personId: props.item?.id })
18986
+ React21.createElement(PersonAttendancesWidget, { personId: props.item?.id })
16787
18987
  )
16788
18988
  );
16789
18989
  PersonAttendancesBound.displayName = "AgendaPersonAttendances";
16790
- const PersonAttendanceBlockBound = (props) => React20.createElement(
18990
+ const PersonAttendanceBlockBound = (props) => React21.createElement(
16791
18991
  AgendaContextProvider,
16792
18992
  { config, provider: provider2, store },
16793
- React20.createElement(PersonAttendanceBlock, { personId: props.item?.id })
18993
+ React21.createElement(PersonAttendanceBlock, { personId: props.item?.id })
16794
18994
  );
16795
18995
  PersonAttendanceBlockBound.displayName = "AgendaPersonAttendanceBlock";
16796
- const PersonServicesBound = (props) => React20.createElement(
18996
+ const PersonServicesBound = (props) => React21.createElement(
16797
18997
  AgendaContextProvider,
16798
18998
  { config, provider: provider2, store },
16799
- React20.createElement(PersonServicesWidget, { personId: props.item?.id })
18999
+ React21.createElement(PersonServicesWidget, { personId: props.item?.id })
16800
19000
  );
16801
19001
  PersonServicesBound.displayName = "AgendaPersonServices";
16802
19002
  return {
@@ -16818,7 +19018,14 @@ function createAgendaPlugin(options) {
16818
19018
  facets: [
16819
19019
  { id: "scheduling", label: "Agenda", description: "Hor\xE1rios, profissionais e reservas.", includedWithPlugin: true },
16820
19020
  { id: "public_booking", label: "Agendamento online", description: "A p\xE1gina p\xFAblica onde o cliente marca sozinho." },
16821
- { id: "smart_fit", label: "Encaixe inteligente", description: "Sugere o melhor hor\xE1rio lendo a agenda inteira." }
19021
+ { id: "smart_fit", label: "Encaixe inteligente", description: "Sugere o melhor hor\xE1rio lendo a agenda inteira." },
19022
+ // Check-in e Confirmações eram opção de BUILD (`options.checkin`,
19023
+ // `options.modules.confirmations`) — o aplicativo decidia, a conta nunca
19024
+ // podia desligar. Como facet, a mesma conta liga/desliga pela tela de
19025
+ // Módulos, e é a facet resolvida (`config.facets`, ver `resolvePluginRuntime`)
19026
+ // que decide se a rota e a linha de menu existem — não mais a opção.
19027
+ { id: "checkin", label: "Check-in", description: "Fila de recep\xE7\xE3o, ao lado da Agenda." },
19028
+ { id: "confirmations", label: config.labels.confirmations, description: "Fila de quem falta confirmar." }
16822
19029
  ],
16823
19030
  dependencies: [],
16824
19031
  // Os FATOS da agenda — só expostos enquanto o módulo está ATIVO.
@@ -16899,14 +19106,18 @@ function createAgendaPlugin(options) {
16899
19106
  // propósito — a casca agrupa num menu suspenso tudo que compartilha o
16900
19107
  // prefixo da rota, e sob `/agenda` o check-in virava um item escondido
16901
19108
  // atrás de um clique na Agenda, que é o oposto de entrada dedicada.
16902
- ...options?.checkin ? [{
19109
+ //
19110
+ // Sempre declarada — `requiresFacet` é quem decide se ela existe para
19111
+ // ESTA conta (ver `resolvePluginRuntime`), não mais `options.checkin`.
19112
+ {
16903
19113
  section: options?.navSection ?? "main",
16904
19114
  position: (options?.navPosition ?? 2) + 0.5,
16905
19115
  label: "Check-in",
16906
19116
  route: "/checkin",
16907
19117
  icon: "LogIn",
16908
- permission: { feature: "agenda", action: "read" }
16909
- }] : [],
19118
+ permission: { feature: "agenda", action: "read" },
19119
+ requiresFacet: "checkin"
19120
+ },
16910
19121
  // A FILA DE CONFIRMAÇÃO TINHA TELA E NÃO TINHA PORTA.
16911
19122
  //
16912
19123
  // `ConfirmationsView` existe aqui desde sempre e nenhuma rota a montava:
@@ -16914,14 +19125,15 @@ function createAgendaPlugin(options) {
16914
19125
  // menu, ao lado de Agendamentos, e é o que a recepção abre de manhã para
16915
19126
  // saber quem ligar. Sob `/agenda/confirmations` de propósito — é assim
16916
19127
  // que a rubrica do rail a recolhe junto com o calendário.
16917
- ...config.modules.confirmations ? [{
19128
+ {
16918
19129
  section: options?.navSection ?? "main",
16919
19130
  position: (options?.navPosition ?? 2) + 0.4,
16920
19131
  label: config.labels.confirmations,
16921
19132
  route: "/agenda/confirmations",
16922
19133
  icon: "CalendarCheck2",
16923
- permission: { feature: "agenda", action: "read" }
16924
- }] : []
19134
+ permission: { feature: "agenda", action: "read" },
19135
+ requiresFacet: "confirmations"
19136
+ }
16925
19137
  ],
16926
19138
  routes: [
16927
19139
  {
@@ -16938,19 +19150,20 @@ function createAgendaPlugin(options) {
16938
19150
  // Matches the ref agent_agenda_create_appointment returns.
16939
19151
  entityRoutes: ["schedule:appointment"]
16940
19152
  },
16941
- ...config.modules.confirmations ? [{
19153
+ {
16942
19154
  path: "/agenda/confirmations",
16943
- component: (() => React20.createElement(
19155
+ component: (() => React21.createElement(
16944
19156
  AgendaPage,
16945
19157
  { config, provider: provider2, store },
16946
- React20.createElement(ConfirmationsView)
19158
+ React21.createElement(ConfirmationsView)
16947
19159
  )),
16948
19160
  fullBleed: true,
16949
- permission: { feature: "agenda", action: "read" }
16950
- }] : [],
16951
- ...options?.checkin ? [{
19161
+ permission: { feature: "agenda", action: "read" },
19162
+ requiresFacet: "confirmations"
19163
+ },
19164
+ {
16952
19165
  path: "/checkin",
16953
- component: (() => React20.createElement(CheckinPage, {
19166
+ component: (() => React21.createElement(CheckinPage, {
16954
19167
  config,
16955
19168
  provider: provider2,
16956
19169
  store,
@@ -16961,8 +19174,9 @@ function createAgendaPlugin(options) {
16961
19174
  // volta ficava por conta dela. O host ainda pode passar o seu
16962
19175
  // quando quiser outro destino.
16963
19176
  })),
16964
- permission: { feature: "agenda", action: "read" }
16965
- }] : []
19177
+ permission: { feature: "agenda", action: "read" },
19178
+ requiresFacet: "checkin"
19179
+ }
16966
19180
  ],
16967
19181
  widgets: [
16968
19182
  {
@@ -17101,9 +19315,9 @@ function createAgendaPlugin(options) {
17101
19315
  icon: "Calendar",
17102
19316
  // Standard layout: General | Integrations (Google Calendar addon) | Properties.
17103
19317
  component: (() => {
17104
- const AgendaSettingsTab = () => React20.createElement(PluginSettingsPanel, {
19318
+ const AgendaSettingsTab = () => React21.createElement(PluginSettingsPanel, {
17105
19319
  title: "Agenda",
17106
- generalSettings: React20.createElement(AgendaGeneralSettings),
19320
+ generalSettings: React21.createElement(AgendaGeneralSettings),
17107
19321
  registries,
17108
19322
  hostPluginId: "agenda",
17109
19323
  routeBase: "/settings/agenda"
@@ -17132,6 +19346,6 @@ function createAgendaPlugin(options) {
17132
19346
  };
17133
19347
  }
17134
19348
 
17135
- export { bookingToEvent2 as bookingToEvent, buildAgendaDomainEvents, buildAgendaMessageDefaults, computeCommissionAmount, createAgendaPlugin, createFinancialBridge, createGoogleCalendarPlugin, createGoogleCalendarProvider, eventToBookingPatch, googleCalendarConnector };
19349
+ export { bookingToEvent2 as bookingToEvent, buildAgendaDomainEvents, buildAgendaMessageDefaults, computeCommissionAmount, createAgendaPlugin, createAgendaTurnQueuePlugin, createFinancialBridge, createGoogleCalendarPlugin, createGoogleCalendarProvider, createMockTurnQueueProvider, createSupabaseTurnQueueProvider, eventToBookingPatch, googleCalendarConnector };
17136
19350
  //# sourceMappingURL=index.js.map
17137
19351
  //# sourceMappingURL=index.js.map