@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.
- package/dist/{chunk-RVO5ILXN.js → chunk-A77PUFY5.js} +85 -2
- package/dist/chunk-A77PUFY5.js.map +1 -0
- package/dist/components/AgendaFilterRail.d.ts +1 -1
- package/dist/components/AgendaGeneralSettings.d.ts.map +1 -1
- package/dist/components/AppointmentModal.d.ts.map +1 -1
- package/dist/components/AppointmentPopover.d.ts.map +1 -1
- package/dist/components/AvailabilityHeaderControl.d.ts +6 -0
- package/dist/components/AvailabilityHeaderControl.d.ts.map +1 -0
- package/dist/components/EventContextMenu.d.ts.map +1 -1
- package/dist/components/SeriesDeleteChoice.d.ts +20 -0
- package/dist/components/SeriesDeleteChoice.d.ts.map +1 -0
- package/dist/config.d.ts +5 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/data/mock.d.ts.map +1 -1
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/data/turn-queue-types.d.ts +71 -0
- package/dist/data/turn-queue-types.d.ts.map +1 -0
- package/dist/data/turn-queue.d.ts +4 -0
- package/dist/data/turn-queue.d.ts.map +1 -0
- package/dist/data/types.d.ts +73 -0
- package/dist/data/types.d.ts.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3072 -858
- package/dist/index.js.map +1 -1
- package/dist/lib/recurrence.d.ts +122 -0
- package/dist/lib/recurrence.d.ts.map +1 -0
- package/dist/lib/turn-queue-plugin.d.ts +4 -0
- package/dist/lib/turn-queue-plugin.d.ts.map +1 -0
- package/dist/lib/turn-queue-presence.d.ts +4 -0
- package/dist/lib/turn-queue-presence.d.ts.map +1 -0
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/migrations/index.d.ts +4 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/public/index.js +2 -2
- package/dist/store.d.ts +7 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/views/CalendarView.d.ts.map +1 -1
- package/dist/views/TurnQueuePage.d.ts +10 -0
- package/dist/views/TurnQueuePage.d.ts.map +1 -0
- package/package.json +6 -6
- package/dist/chunk-RVO5ILXN.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { createMockAgendaProvider, createSupabasePublicBookingProvider } from './chunk-
|
|
2
|
-
export { createMockAgendaProvider } from './chunk-
|
|
3
|
-
import
|
|
4
|
-
import { tl, recordRailFields, RECORD_GROUP, activeFacet, connectionStatus, createConnectionStore, todayLocal, getSupabaseClientOptional,
|
|
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,
|
|
14
|
-
import { toast, FilterRail, AdaptiveFilter, FilterCheckRow, TooltipProvider, Button,
|
|
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] =
|
|
765
|
-
const [title, setTitle] =
|
|
766
|
-
const [description, setDescription] =
|
|
767
|
-
const [mode, setMode] =
|
|
768
|
-
const [specificDate, setSpecificDate] =
|
|
769
|
-
const [allDay, setAllDay] =
|
|
770
|
-
const [startsAt, setStartsAt] =
|
|
771
|
-
const [endsAt, setEndsAt] =
|
|
772
|
-
const [days2, setDays] =
|
|
773
|
-
const [validFrom, setValidFrom] =
|
|
774
|
-
const [validTo, setValidTo] =
|
|
775
|
-
const [weekInterval, setWeekInterval] =
|
|
776
|
-
const [saving, setSaving] =
|
|
777
|
-
|
|
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] =
|
|
968
|
-
const [falhou, setFalhou] =
|
|
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
|
-
|
|
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] =
|
|
1028
|
-
const [selectedId, setSelectedId] =
|
|
1029
|
-
|
|
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 =
|
|
1034
|
-
|
|
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] =
|
|
1582
|
-
const [failed, setFailed] =
|
|
1583
|
-
|
|
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] =
|
|
1636
|
-
const [term, setTerm] =
|
|
1637
|
-
const [options, setOptions] =
|
|
1638
|
-
const initializedFor =
|
|
1639
|
-
const onChangeRef =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
2347
|
-
|
|
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
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2566
|
+
if (firstId) {
|
|
2567
|
+
await persistInterests(firstId);
|
|
2568
|
+
onClose();
|
|
2569
|
+
onCreated?.(firstId, externalSource);
|
|
2570
|
+
} else {
|
|
2571
|
+
onClose();
|
|
2572
|
+
}
|
|
2362
2573
|
} else if (bookingId) {
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
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
|
|
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__ */
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
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
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
"button",
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
className: "
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
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
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
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
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
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
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
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
|
-
}
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
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
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
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
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
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
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
3007
|
+
setClientId(contact?.id ?? "");
|
|
3008
|
+
setClientSearch(contact?.name ?? "");
|
|
3009
|
+
setClientPhone(contact?.phone ?? "");
|
|
2776
3010
|
},
|
|
2777
|
-
|
|
3011
|
+
kind: config.clientKind,
|
|
3012
|
+
lookup: config.contactLookup,
|
|
2778
3013
|
createEntityDef: config.clientEntityDef,
|
|
3014
|
+
clearable: !isPaidBooking,
|
|
2779
3015
|
variant: "inline",
|
|
2780
|
-
placeholder: t2("agenda.
|
|
2781
|
-
createLabel: t2("agenda.appointment.newClient")
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
)
|
|
2785
|
-
] })
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
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
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
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
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
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
|
-
|
|
2882
|
-
|
|
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
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
3081
|
+
value: resourceId,
|
|
3082
|
+
onChange: setResourceId,
|
|
3083
|
+
locations: storeResources,
|
|
3084
|
+
placeholder: t2("agenda.appointment.selectRoom")
|
|
2888
3085
|
}
|
|
2889
|
-
)
|
|
2890
|
-
] })
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
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
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
"
|
|
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:
|
|
2920
|
-
onChange: (
|
|
2921
|
-
placeholder:
|
|
3223
|
+
value: notes,
|
|
3224
|
+
onChange: (e) => setNotes(e.target.value),
|
|
3225
|
+
placeholder: t2("agenda.appointment.addDescription"),
|
|
2922
3226
|
rows: 2,
|
|
2923
|
-
|
|
2924
|
-
className: "w-full rounded-input border border-input
|
|
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
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
] })
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
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
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
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-
|
|
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(
|
|
2996
|
-
|
|
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
|
-
)
|
|
3002
|
-
|
|
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
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
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 =
|
|
3123
|
-
let y =
|
|
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
|
|
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
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
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:
|
|
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
|
|
3398
|
-
|
|
3399
|
-
|
|
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__ */
|
|
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:
|
|
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] =
|
|
3844
|
-
const [schedulesFor, setSchedulesFor] =
|
|
3845
|
-
const [editingBlock, setEditingBlock] =
|
|
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__ */
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
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
|
-
|
|
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] =
|
|
5194
|
-
const [preenchendo, setPreenchendo] =
|
|
5195
|
-
const [preenchidos, setPreenchidos] =
|
|
5196
|
-
const [pendencias, setPendencias] =
|
|
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 =
|
|
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 =
|
|
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] =
|
|
5475
|
-
const [busyId, setBusyId] =
|
|
5476
|
-
const [myPersonId, setMyPersonId] =
|
|
5477
|
-
const [startRefusal, setStartRefusal] =
|
|
5478
|
-
const [checkouts, setCheckouts] =
|
|
5479
|
-
const [conferindo, setConferindo] =
|
|
5480
|
-
const [checkoutRefusal, setCheckoutRefusal] =
|
|
5481
|
-
const [panel, setPanel] =
|
|
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
|
-
|
|
5886
|
+
React21.useEffect(() => {
|
|
5491
5887
|
void fetchBookings(todayRange(), [...CHECKIN_STATUSES]);
|
|
5492
5888
|
}, [fetchBookings]);
|
|
5493
|
-
const [shownStatuses, setShownStatuses] =
|
|
5494
|
-
const [shownPros, setShownPros] =
|
|
5495
|
-
const prosToday =
|
|
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 =
|
|
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] =
|
|
5514
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
5954
|
+
React21.useEffect(() => {
|
|
5559
5955
|
void recarregarCheckouts();
|
|
5560
5956
|
}, [recarregarCheckouts]);
|
|
5561
|
-
|
|
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
|
-
|
|
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] =
|
|
5958
|
-
const [booking, setBooking] =
|
|
5959
|
-
const [indo, setIndo] =
|
|
5960
|
-
const [recusa, setRecusa] =
|
|
5961
|
-
|
|
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
|
|
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:
|
|
6602
|
-
clientPhone:
|
|
6603
|
-
clientEmail:
|
|
6604
|
-
clientAvatarUrl:
|
|
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] =
|
|
8160
|
-
const [statusLocal, setStatusLocal] =
|
|
8161
|
-
|
|
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] =
|
|
8168
|
-
|
|
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] =
|
|
8180
|
-
const [recusaInicio, setRecusaInicio] =
|
|
8181
|
-
const [rows, setRows] =
|
|
8182
|
-
const [record, setRecord] =
|
|
8183
|
-
const [erro, setErro] =
|
|
8184
|
-
const [contratando, setContratando] =
|
|
8185
|
-
const [servicos, setServicos] =
|
|
8186
|
-
const [marcados, setMarcados] =
|
|
8187
|
-
const [salvando, setSalvando] =
|
|
8188
|
-
const [confirmando, setConfirmando] =
|
|
8189
|
-
const [encerrando, setEncerrando] =
|
|
8190
|
-
const [avisoFinal, setAvisoFinal] =
|
|
8191
|
-
const [preenchendo, setPreenchendo] =
|
|
8192
|
-
const [avaliando, setAvaliando] =
|
|
8193
|
-
const [lancando, setLancando] =
|
|
8194
|
-
const [executando, setExecutando] =
|
|
8195
|
-
const [vinculando, setVinculando] =
|
|
8196
|
-
const [candidatos, setCandidatos] =
|
|
8197
|
-
const [buscaInsumo, setBuscaInsumo] =
|
|
8198
|
-
const [insumos, setInsumos] =
|
|
8199
|
-
const [usados, setUsados] =
|
|
8200
|
-
const [busca, setBusca] =
|
|
8201
|
-
const [catalogo, setCatalogo] =
|
|
8202
|
-
const [indicados, setIndicados] =
|
|
8203
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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] =
|
|
9055
|
-
const [aberto, setAberto] =
|
|
9056
|
-
const [emCurso, setEmCurso] =
|
|
9057
|
-
const recarregar =
|
|
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
|
-
|
|
9724
|
+
React21.useEffect(() => {
|
|
9066
9725
|
void recarregar();
|
|
9067
9726
|
}, [recarregar]);
|
|
9068
|
-
|
|
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] =
|
|
9167
|
-
|
|
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
|
-
-- \
|
|
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
|
-
--
|
|
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
|
-
--
|
|
13406
|
-
--
|
|
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
|
-
--
|
|
13409
|
-
--
|
|
13410
|
-
--
|
|
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
|
-
--
|
|
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
|
-
--
|
|
13415
|
-
--
|
|
13416
|
-
-- \
|
|
13417
|
-
-- para
|
|
13418
|
-
--
|
|
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
|
-
--
|
|
13422
|
-
|
|
13423
|
-
|
|
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
|
|
13426
|
-
|
|
13427
|
-
|
|
13428
|
-
|
|
13429
|
-
|
|
13430
|
-
|
|
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
|
-
|
|
13444
|
-
|
|
13445
|
-
|
|
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 = () =>
|
|
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
|
-
|
|
18983
|
+
React21.createElement(
|
|
16784
18984
|
AgendaContextProvider,
|
|
16785
18985
|
{ config, provider: provider2, store },
|
|
16786
|
-
|
|
18986
|
+
React21.createElement(PersonAttendancesWidget, { personId: props.item?.id })
|
|
16787
18987
|
)
|
|
16788
18988
|
);
|
|
16789
18989
|
PersonAttendancesBound.displayName = "AgendaPersonAttendances";
|
|
16790
|
-
const PersonAttendanceBlockBound = (props) =>
|
|
18990
|
+
const PersonAttendanceBlockBound = (props) => React21.createElement(
|
|
16791
18991
|
AgendaContextProvider,
|
|
16792
18992
|
{ config, provider: provider2, store },
|
|
16793
|
-
|
|
18993
|
+
React21.createElement(PersonAttendanceBlock, { personId: props.item?.id })
|
|
16794
18994
|
);
|
|
16795
18995
|
PersonAttendanceBlockBound.displayName = "AgendaPersonAttendanceBlock";
|
|
16796
|
-
const PersonServicesBound = (props) =>
|
|
18996
|
+
const PersonServicesBound = (props) => React21.createElement(
|
|
16797
18997
|
AgendaContextProvider,
|
|
16798
18998
|
{ config, provider: provider2, store },
|
|
16799
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
19153
|
+
{
|
|
16942
19154
|
path: "/agenda/confirmations",
|
|
16943
|
-
component: (() =>
|
|
19155
|
+
component: (() => React21.createElement(
|
|
16944
19156
|
AgendaPage,
|
|
16945
19157
|
{ config, provider: provider2, store },
|
|
16946
|
-
|
|
19158
|
+
React21.createElement(ConfirmationsView)
|
|
16947
19159
|
)),
|
|
16948
19160
|
fullBleed: true,
|
|
16949
|
-
permission: { feature: "agenda", action: "read" }
|
|
16950
|
-
|
|
16951
|
-
|
|
19161
|
+
permission: { feature: "agenda", action: "read" },
|
|
19162
|
+
requiresFacet: "confirmations"
|
|
19163
|
+
},
|
|
19164
|
+
{
|
|
16952
19165
|
path: "/checkin",
|
|
16953
|
-
component: (() =>
|
|
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 = () =>
|
|
19318
|
+
const AgendaSettingsTab = () => React21.createElement(PluginSettingsPanel, {
|
|
17105
19319
|
title: "Agenda",
|
|
17106
|
-
generalSettings:
|
|
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
|