@fayz-ai/plugin-agenda 0.21.1 → 0.22.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/components/AgendaGeneralSettings.d.ts.map +1 -1
- package/dist/components/AppointmentModal.d.ts.map +1 -1
- package/dist/components/AttendanceRuler.d.ts.map +1 -1
- package/dist/components/BookingPrepaidPanel.d.ts +18 -0
- package/dist/components/BookingPrepaidPanel.d.ts.map +1 -0
- package/dist/components/BookingRevisionPanel.d.ts +16 -0
- package/dist/components/BookingRevisionPanel.d.ts.map +1 -0
- package/dist/components/CheckinDialog.d.ts +5 -0
- package/dist/components/CheckinDialog.d.ts.map +1 -1
- package/dist/components/MyClientsTodayWidget.d.ts +7 -0
- package/dist/components/MyClientsTodayWidget.d.ts.map +1 -0
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/data/types.d.ts +30 -0
- package/dist/data/types.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +857 -200
- package/dist/index.js.map +1 -1
- package/dist/lib/calendar-events.d.ts +15 -1
- package/dist/lib/calendar-events.d.ts.map +1 -1
- package/dist/lib/checkin-required.d.ts +14 -0
- package/dist/lib/checkin-required.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/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/views/CalendarView.d.ts.map +1 -1
- package/dist/views/CheckinView.d.ts.map +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createMockAgendaProvider, createSupabasePublicBookingProvider } from './chunk-A77PUFY5.js';
|
|
2
2
|
export { createMockAgendaProvider } from './chunk-A77PUFY5.js';
|
|
3
|
-
import
|
|
3
|
+
import React23, { useState, useEffect, useRef, useMemo, useCallback, useLayoutEffect } from 'react';
|
|
4
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, UNIT_DETAIL_ZONES, setScheduleBlockConfig, getScheduleBlockConfig, registerPersonActions, PluginSettingsPanel, adminNavigateTo, createArchetypeLookup, dedup, usePluginPrefs, useTenantPluginSettings, SettingsGroup, ToggleRow, SelectRow, PermissionGate,
|
|
5
|
+
import { createPluginContext, fayzConnectorOAuth, UNIT_DETAIL_ZONES, setScheduleBlockConfig, getScheduleBlockConfig, registerPersonActions, PluginSettingsPanel, adminNavigateTo, createArchetypeLookup, dedup, usePluginPrefs, PersonLink, useTenantPluginSettings, SettingsGroup, ToggleRow, SelectRow, PermissionGate, PageActions, useRightRailStore, useFieldRules, deriveEntityKey, usePermissionOptional, openEntity, ScheduleEditor, useLimitGuard, useLookupCreate, CrudFormPage, CrudDetailPage, ContactPicker, 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,11 +10,11 @@ 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, Users, Activity, RefreshCw, Clock3, CalendarPlus, Loader2, AlertCircle, X, CheckCircle2, Ban, Plus, Settings, ChevronLeft, ChevronRight, ChevronDown, CircleDollarSign, LogIn, CalendarDays,
|
|
13
|
+
import { CheckCircle, Stethoscope, RotateCcw, Check, CircleDashed, Layers, ArrowRight, Play, Users, Activity, RefreshCw, Clock3, CalendarPlus, Loader2, AlertCircle, X, CheckCircle2, Ban, Plus, Settings, ChevronLeft, ChevronRight, ChevronDown, CircleDollarSign, LogIn, CalendarDays, 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, PackageCheck, Sparkles } from 'lucide-react';
|
|
14
14
|
import { Card, CardHeader, CardTitle, CardDescription, CardContent, Skeleton, Switch, Input, Button, toast, FilterRail, AdaptiveFilter, FilterCheckRow, TooltipProvider, ModulePage, Avatar, AvatarImage, AvatarFallback, Badge, Checkbox, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, PersonAvatar, abbreviatePersonName, INLINE_FILTER_MAX, PersonMultiPicker, SegmentedControl, MiniCalendar, PageHeaderActions, ConfirmDialog, RailWorkspace, Tooltip, TooltipTrigger, TooltipContent, SubpageHeader, ModalTask, Modal, ModalContent, ModalHeader, ModalTitle, ModalBody, PersonPicker, 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
|
-
import { FormFillDialog, PersonFormsAlert } from '@fayz-ai/plugin-forms';
|
|
17
|
+
import { listAttendanceContracts, ensureAttendanceContracts, ContractSignPane, FormFillDialog, PersonFormsAlert } from '@fayz-ai/plugin-forms';
|
|
18
18
|
import { ClientAccountModal, ClientAccountInline } from '@fayz-ai/plugin-financial';
|
|
19
19
|
import { createStore } from 'zustand/vanilla';
|
|
20
20
|
|
|
@@ -220,20 +220,20 @@ var DEFAULT_LABELS = {
|
|
|
220
220
|
};
|
|
221
221
|
var DEFAULT_CURRENCY = { code: "BRL", locale: "pt-BR", symbol: "R$" };
|
|
222
222
|
var DEFAULT_STATUSES = [
|
|
223
|
-
{ value: "scheduled", label: "Scheduled", color: "#6366f1" },
|
|
224
|
-
{ value: "confirmed", label: "Confirmed", color: "#3b82f6" },
|
|
225
|
-
{ value: "waiting", label: "Waiting", color: "#f97316", availableWhen: "today_only" },
|
|
226
|
-
{ value: "in_progress", label: "In
|
|
227
|
-
{ value: "completed", label: "Completed", color: "#10b981", availableWhen: "today_or_past" },
|
|
228
|
-
{ value: "cancelled", label: "Cancelled", color: "#ef4444" },
|
|
229
|
-
{ value: "no_show", label: "No
|
|
223
|
+
{ value: "scheduled", label: tl("Scheduled", "Agendado"), color: "#6366f1" },
|
|
224
|
+
{ value: "confirmed", label: tl("Confirmed", "Confirmado"), color: "#3b82f6" },
|
|
225
|
+
{ value: "waiting", label: tl("Waiting", "Aguardando"), color: "#f97316", availableWhen: "today_only" },
|
|
226
|
+
{ value: "in_progress", label: tl("In progress", "Em atendimento"), color: "#f59e0b", availableWhen: "today_only" },
|
|
227
|
+
{ value: "completed", label: tl("Completed", "Atendido"), color: "#10b981", availableWhen: "today_or_past" },
|
|
228
|
+
{ value: "cancelled", label: tl("Cancelled", "Desmarcado"), color: "#ef4444" },
|
|
229
|
+
{ value: "no_show", label: tl("No show", "Faltou"), color: "#6b7280", availableWhen: "today_or_past" }
|
|
230
230
|
];
|
|
231
231
|
function withCheckinStatus(statuses, checkin) {
|
|
232
232
|
if (!checkin || statuses.some((s) => s.value === "waiting")) return statuses;
|
|
233
233
|
const at = statuses.findIndex((s) => s.value === "in_progress");
|
|
234
234
|
const waiting = {
|
|
235
235
|
value: "waiting",
|
|
236
|
-
label: "Waiting",
|
|
236
|
+
label: tl("Waiting", "Aguardando"),
|
|
237
237
|
color: "#f97316",
|
|
238
238
|
availableWhen: "today_only"
|
|
239
239
|
};
|
|
@@ -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] = React23.useState(prefillProfessional ?? "");
|
|
765
|
+
const [title, setTitle] = React23.useState("");
|
|
766
|
+
const [description, setDescription] = React23.useState("");
|
|
767
|
+
const [mode, setMode] = React23.useState("single");
|
|
768
|
+
const [specificDate, setSpecificDate] = React23.useState(date ?? hoje());
|
|
769
|
+
const [allDay, setAllDay] = React23.useState(false);
|
|
770
|
+
const [startsAt, setStartsAt] = React23.useState("12:00");
|
|
771
|
+
const [endsAt, setEndsAt] = React23.useState("13:00");
|
|
772
|
+
const [days2, setDays] = React23.useState([]);
|
|
773
|
+
const [validFrom, setValidFrom] = React23.useState(date ?? hoje());
|
|
774
|
+
const [validTo, setValidTo] = React23.useState("");
|
|
775
|
+
const [weekInterval, setWeekInterval] = React23.useState("0");
|
|
776
|
+
const [saving, setSaving] = React23.useState(false);
|
|
777
|
+
React23.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] = React23.useState(null);
|
|
968
|
+
const [falhou, setFalhou] = React23.useState(false);
|
|
969
969
|
const key = range ? `${range.start}|${range.end}` : null;
|
|
970
|
-
|
|
970
|
+
React23.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] = React23.useState("");
|
|
1028
|
+
const [selectedId, setSelectedId] = React23.useState(null);
|
|
1029
|
+
React23.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 = React23.useRef(null);
|
|
1034
|
+
React23.useEffect(() => {
|
|
1035
1035
|
if (!open || !selectedId) return;
|
|
1036
1036
|
const id = window.setTimeout(() => {
|
|
1037
1037
|
selectedRef.current?.scrollIntoView({ block: "nearest" });
|
|
@@ -1695,9 +1695,9 @@ function ClientHistoryPanel({ clientId, currentBookingId, formatCurrency: format
|
|
|
1695
1695
|
const t2 = useTranslation();
|
|
1696
1696
|
const config = useAgendaConfig();
|
|
1697
1697
|
const provider2 = useAgendaProvider();
|
|
1698
|
-
const [bookings, setBookings] =
|
|
1699
|
-
const [failed, setFailed] =
|
|
1700
|
-
|
|
1698
|
+
const [bookings, setBookings] = React23.useState(null);
|
|
1699
|
+
const [failed, setFailed] = React23.useState(false);
|
|
1700
|
+
React23.useEffect(() => {
|
|
1701
1701
|
let alive = true;
|
|
1702
1702
|
if (!provider2.getClientHistory) {
|
|
1703
1703
|
setBookings([]);
|
|
@@ -1749,13 +1749,13 @@ function ClientHistoryPanel({ clientId, currentBookingId, formatCurrency: format
|
|
|
1749
1749
|
}
|
|
1750
1750
|
function BookingInterestsPanel({ clientId, bookingId, value, onChange }) {
|
|
1751
1751
|
const provider2 = useAgendaProvider();
|
|
1752
|
-
const [crmInterests, setCrmInterests] =
|
|
1753
|
-
const [term, setTerm] =
|
|
1754
|
-
const [options, setOptions] =
|
|
1755
|
-
const initializedFor =
|
|
1756
|
-
const onChangeRef =
|
|
1752
|
+
const [crmInterests, setCrmInterests] = React23.useState([]);
|
|
1753
|
+
const [term, setTerm] = React23.useState("");
|
|
1754
|
+
const [options, setOptions] = React23.useState([]);
|
|
1755
|
+
const initializedFor = React23.useRef(null);
|
|
1756
|
+
const onChangeRef = React23.useRef(onChange);
|
|
1757
1757
|
onChangeRef.current = onChange;
|
|
1758
|
-
|
|
1758
|
+
React23.useEffect(() => {
|
|
1759
1759
|
if (!clientId) {
|
|
1760
1760
|
setCrmInterests([]);
|
|
1761
1761
|
initializedFor.current = null;
|
|
@@ -1788,7 +1788,7 @@ function BookingInterestsPanel({ clientId, bookingId, value, onChange }) {
|
|
|
1788
1788
|
vivo = false;
|
|
1789
1789
|
};
|
|
1790
1790
|
}, [clientId, bookingId, provider2]);
|
|
1791
|
-
|
|
1791
|
+
React23.useEffect(() => {
|
|
1792
1792
|
if (!term.trim()) {
|
|
1793
1793
|
setOptions([]);
|
|
1794
1794
|
return;
|
|
@@ -1866,6 +1866,139 @@ function BookingInterestsPanel({ clientId, bookingId, value, onChange }) {
|
|
|
1866
1866
|
)
|
|
1867
1867
|
] });
|
|
1868
1868
|
}
|
|
1869
|
+
function BookingPrepaidPanel({ clientId, addedServiceIds, onAdd }) {
|
|
1870
|
+
const provider2 = useAgendaProvider();
|
|
1871
|
+
const [data, setData] = React23.useState(null);
|
|
1872
|
+
React23.useEffect(() => {
|
|
1873
|
+
let alive = true;
|
|
1874
|
+
setData(null);
|
|
1875
|
+
provider2.getPrepaidItems?.(clientId).then((r) => {
|
|
1876
|
+
if (alive) setData(r);
|
|
1877
|
+
}).catch(() => {
|
|
1878
|
+
if (alive) setData({ open: [], scheduledCount: 0 });
|
|
1879
|
+
});
|
|
1880
|
+
return () => {
|
|
1881
|
+
alive = false;
|
|
1882
|
+
};
|
|
1883
|
+
}, [clientId, provider2]);
|
|
1884
|
+
if (!data || data.open.length === 0) return null;
|
|
1885
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1886
|
+
for (const it of data.open) {
|
|
1887
|
+
const g = grouped.get(it.serviceId);
|
|
1888
|
+
if (g) g.count += 1;
|
|
1889
|
+
else grouped.set(it.serviceId, { item: it, count: 1 });
|
|
1890
|
+
}
|
|
1891
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5 rounded-lg border border-primary/25 bg-primary/5 px-2.5 py-2", children: [
|
|
1892
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs font-medium text-primary", children: [
|
|
1893
|
+
/* @__PURE__ */ jsx(PackageCheck, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": true }),
|
|
1894
|
+
data.open.length,
|
|
1895
|
+
" servi\xE7o(s) pr\xE9-contratado(s)",
|
|
1896
|
+
data.scheduledCount > 0 && /* @__PURE__ */ jsxs("span", { className: "font-normal text-muted-foreground", children: [
|
|
1897
|
+
"\xB7 ",
|
|
1898
|
+
data.scheduledCount,
|
|
1899
|
+
" j\xE1 agendado(s)"
|
|
1900
|
+
] })
|
|
1901
|
+
] }),
|
|
1902
|
+
/* @__PURE__ */ jsx("p", { className: "text-[11px] text-muted-foreground", children: "J\xE1 contratados e n\xE3o executados \u2014 clique para adicionar a este atendimento." }),
|
|
1903
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5", children: [...grouped.values()].map(({ item, count }) => {
|
|
1904
|
+
const added = addedServiceIds.includes(item.serviceId);
|
|
1905
|
+
return /* @__PURE__ */ jsxs(
|
|
1906
|
+
"button",
|
|
1907
|
+
{
|
|
1908
|
+
type: "button",
|
|
1909
|
+
disabled: added,
|
|
1910
|
+
onClick: () => onAdd(item),
|
|
1911
|
+
className: `inline-flex items-center gap-1 rounded-full border px-2.5 py-0.5 text-xs transition-colors ${added ? "border-primary/40 bg-card text-muted-foreground" : "border-transparent bg-card hover:border-primary/40"}`,
|
|
1912
|
+
children: [
|
|
1913
|
+
added ? /* @__PURE__ */ jsx(Check, { className: "h-3 w-3 text-primary", "aria-hidden": true }) : /* @__PURE__ */ jsx(Plus, { className: "h-3 w-3", "aria-hidden": true }),
|
|
1914
|
+
item.serviceName,
|
|
1915
|
+
count > 1 ? ` \xD7${count}` : ""
|
|
1916
|
+
]
|
|
1917
|
+
},
|
|
1918
|
+
item.serviceId
|
|
1919
|
+
);
|
|
1920
|
+
}) })
|
|
1921
|
+
] });
|
|
1922
|
+
}
|
|
1923
|
+
var fmtDate = (iso) => iso ? new Date(iso).toLocaleDateString("pt-BR") : "";
|
|
1924
|
+
function BookingRevisionPanel({ clientId, value, onChange, onState }) {
|
|
1925
|
+
const provider2 = useAgendaProvider();
|
|
1926
|
+
const [rows, setRows] = React23.useState(null);
|
|
1927
|
+
const [state, setState] = React23.useState("loading");
|
|
1928
|
+
React23.useEffect(() => {
|
|
1929
|
+
let alive = true;
|
|
1930
|
+
setRows(null);
|
|
1931
|
+
setState("loading");
|
|
1932
|
+
if (!provider2.getRevisionCandidatesForPerson) {
|
|
1933
|
+
setState("unavailable");
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
provider2.getRevisionCandidatesForPerson(clientId).then((r) => {
|
|
1937
|
+
if (alive) {
|
|
1938
|
+
setRows(r);
|
|
1939
|
+
setState(r.length ? "ready" : "empty");
|
|
1940
|
+
}
|
|
1941
|
+
}).catch(() => {
|
|
1942
|
+
if (alive) {
|
|
1943
|
+
setRows([]);
|
|
1944
|
+
setState("unavailable");
|
|
1945
|
+
}
|
|
1946
|
+
});
|
|
1947
|
+
return () => {
|
|
1948
|
+
alive = false;
|
|
1949
|
+
};
|
|
1950
|
+
}, [clientId, provider2]);
|
|
1951
|
+
React23.useEffect(() => {
|
|
1952
|
+
onState?.(state);
|
|
1953
|
+
}, [state]);
|
|
1954
|
+
if (state === "unavailable") return null;
|
|
1955
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5 rounded-lg border border-primary/25 bg-primary/5 px-2.5 py-2", children: [
|
|
1956
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs font-medium text-primary", children: [
|
|
1957
|
+
/* @__PURE__ */ jsx(Link2, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": true }),
|
|
1958
|
+
"De qual servi\xE7o \xE9 esta revis\xE3o?"
|
|
1959
|
+
] }),
|
|
1960
|
+
state === "loading" && /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
1961
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-9 w-full" }),
|
|
1962
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-9 w-full" })
|
|
1963
|
+
] }),
|
|
1964
|
+
state === "empty" && /* @__PURE__ */ jsx("p", { className: "text-xs text-warning", children: "Este cliente n\xE3o tem direito a revis\xE3o agora \u2014 n\xE3o h\xE1 servi\xE7o executado que possa receber revis\xE3o. Para atender, agende uma nova avalia\xE7\xE3o." }),
|
|
1965
|
+
state === "ready" && rows && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1966
|
+
/* @__PURE__ */ jsx("p", { className: "text-[11px] text-muted-foreground", children: "A revis\xE3o s\xF3 entra apontando para um servi\xE7o j\xE1 executado. Sem esse v\xEDnculo o atendimento n\xE3o finaliza." }),
|
|
1967
|
+
/* @__PURE__ */ jsx("ul", { className: "space-y-1", role: "radiogroup", "aria-label": "Servi\xE7o revisado", children: rows.map((c) => {
|
|
1968
|
+
const on = value?.sessionId === c.sessionId;
|
|
1969
|
+
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
|
|
1970
|
+
"button",
|
|
1971
|
+
{
|
|
1972
|
+
type: "button",
|
|
1973
|
+
role: "radio",
|
|
1974
|
+
"aria-checked": on,
|
|
1975
|
+
onClick: () => onChange(on ? null : {
|
|
1976
|
+
sessionId: c.sessionId,
|
|
1977
|
+
revisionNumber: c.nextRevisionNumber,
|
|
1978
|
+
serviceName: c.serviceName
|
|
1979
|
+
}),
|
|
1980
|
+
className: `flex w-full items-center gap-2 rounded-md border px-2.5 py-1.5 text-left text-xs transition-colors ${on ? "border-primary bg-card" : "border-transparent bg-card/60 hover:bg-card"}`,
|
|
1981
|
+
children: [
|
|
1982
|
+
/* @__PURE__ */ jsx("span", { className: `flex h-4 w-4 shrink-0 items-center justify-center rounded-full border ${on ? "border-primary bg-primary text-primary-foreground" : "border-muted-foreground/40"}`, children: on && /* @__PURE__ */ jsx(Check, { className: "h-3 w-3", "aria-hidden": true }) }),
|
|
1983
|
+
/* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
|
|
1984
|
+
/* @__PURE__ */ jsx("span", { className: "block truncate font-medium text-foreground", children: c.serviceName ?? "Servi\xE7o" }),
|
|
1985
|
+
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
|
|
1986
|
+
"executado em ",
|
|
1987
|
+
fmtDate(c.executedOn),
|
|
1988
|
+
c.deadlineDays != null && (c.withinDeadline ? ` \xB7 no prazo (${c.deadlineDays} dias)` : ` \xB7 fora do prazo de ${c.deadlineDays} dias`)
|
|
1989
|
+
] })
|
|
1990
|
+
] }),
|
|
1991
|
+
/* @__PURE__ */ jsxs("span", { className: "shrink-0 rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium text-foreground", children: [
|
|
1992
|
+
c.nextRevisionNumber,
|
|
1993
|
+
"\xAA Revis\xE3o"
|
|
1994
|
+
] })
|
|
1995
|
+
]
|
|
1996
|
+
}
|
|
1997
|
+
) }, c.sessionId);
|
|
1998
|
+
}) })
|
|
1999
|
+
] })
|
|
2000
|
+
] });
|
|
2001
|
+
}
|
|
1869
2002
|
function isStatusAvailable(status, bookingStartsAt) {
|
|
1870
2003
|
const when = status.availableWhen ?? "always";
|
|
1871
2004
|
if (when === "always") return true;
|
|
@@ -2117,6 +2250,8 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
2117
2250
|
const [serviceSearch, setServiceSearch] = useState("");
|
|
2118
2251
|
const [interests, setInterests] = useState([]);
|
|
2119
2252
|
const [serviceKinds, setServiceKinds] = useState({});
|
|
2253
|
+
const [revisionChoice, setRevisionChoice] = useState(null);
|
|
2254
|
+
const [revisionState, setRevisionState] = useState("loading");
|
|
2120
2255
|
const [date, setDate] = useState(() => {
|
|
2121
2256
|
if (prefill?.startsAt) return prefill.startsAt.slice(0, 10);
|
|
2122
2257
|
return todayLocal();
|
|
@@ -2129,6 +2264,9 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
2129
2264
|
return "09:00";
|
|
2130
2265
|
});
|
|
2131
2266
|
const [status, setStatus] = useState("scheduled");
|
|
2267
|
+
const [durationOverride, setDurationOverride] = useState(null);
|
|
2268
|
+
const [editingEnd, setEditingEnd] = useState(false);
|
|
2269
|
+
const [endDraft, setEndDraft] = useState(null);
|
|
2132
2270
|
const [recurrencePreset, setRecurrencePreset] = useState("none");
|
|
2133
2271
|
const [recurrenceEndMode, setRecurrenceEndMode] = useState("count");
|
|
2134
2272
|
const [recurrenceUntil, setRecurrenceUntil] = useState("");
|
|
@@ -2143,13 +2281,13 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
2143
2281
|
const [markingWaiting, setMarkingWaiting] = useState(false);
|
|
2144
2282
|
const [preenchendoForm, setPreenchendoForm] = useState(null);
|
|
2145
2283
|
const [formsVersao, setFormsVersao] = useState(0);
|
|
2146
|
-
const professionalOptions =
|
|
2284
|
+
const professionalOptions = React23.useMemo(() => {
|
|
2147
2285
|
const doLocal = professionalsForLocation(professionals, locationId, professionalId);
|
|
2148
2286
|
if (!professionalId || doLocal.some((p) => p.id === professionalId)) return doLocal;
|
|
2149
2287
|
const nome = loadedBookings.find((b) => b.id === bookingId)?.professionalName;
|
|
2150
2288
|
return [...doLocal, { id: professionalId, name: nome ?? "\u2014", avatarUrl: null }];
|
|
2151
2289
|
}, [professionals, professionalId, locationId, loadedBookings, bookingId]);
|
|
2152
|
-
|
|
2290
|
+
React23.useEffect(() => {
|
|
2153
2291
|
const fora = shouldClearProfessional(professionals, locationId, professionalId);
|
|
2154
2292
|
if (!fora) return;
|
|
2155
2293
|
setProfessionalId("");
|
|
@@ -2171,6 +2309,9 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
2171
2309
|
const [editPaymentStatus, setEditPaymentStatus] = useState("none");
|
|
2172
2310
|
const [paymentStatusLoading, setPaymentStatusLoading] = useState(false);
|
|
2173
2311
|
const [modalTab, setModalTab] = useState("appointment");
|
|
2312
|
+
useEffect(() => {
|
|
2313
|
+
if (!clientId && modalTab !== "appointment") setModalTab("appointment");
|
|
2314
|
+
}, [clientId, modalTab]);
|
|
2174
2315
|
const [clientData, setClientData] = useState(null);
|
|
2175
2316
|
const [clientDataLoading, setClientDataLoading] = useState(false);
|
|
2176
2317
|
const [clientEditing, setClientEditing] = useState(false);
|
|
@@ -2209,6 +2350,8 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
2209
2350
|
setResourceId("");
|
|
2210
2351
|
setServices([]);
|
|
2211
2352
|
setAttendees([]);
|
|
2353
|
+
setDurationOverride(null);
|
|
2354
|
+
setRevisionChoice(null);
|
|
2212
2355
|
setServiceSearch("");
|
|
2213
2356
|
setNotes("");
|
|
2214
2357
|
setStatus("scheduled");
|
|
@@ -2299,6 +2442,10 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
2299
2442
|
price: s.price
|
|
2300
2443
|
})));
|
|
2301
2444
|
if (b.attendees) setAttendees(b.attendees.map((a) => ({ personId: a.personId, name: a.name })));
|
|
2445
|
+
const somaServicos = (b.services ?? []).reduce((acc, s) => acc + (s.durationMinutes ?? 0), 0);
|
|
2446
|
+
const gravada = b.endsAt ? Math.round((new Date(b.endsAt).getTime() - new Date(b.startsAt).getTime()) / 6e4) : 0;
|
|
2447
|
+
const override = somaServicos > 0 && gravada > 0 && gravada !== somaServicos ? gravada : null;
|
|
2448
|
+
setDurationOverride(override);
|
|
2302
2449
|
setEditOrderId(b.orderId ?? null);
|
|
2303
2450
|
setEditOrderTotal(b.orderTotal ?? 0);
|
|
2304
2451
|
setEditPaymentStatus(b.paymentStatus ?? "none");
|
|
@@ -2315,6 +2462,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
2315
2462
|
cancellationReasonId: "",
|
|
2316
2463
|
cancellationNotes: "",
|
|
2317
2464
|
services: svcSnap,
|
|
2465
|
+
dur: override,
|
|
2318
2466
|
// Sem isto, uma edição que SÓ acrescenta um convidado não conta como
|
|
2319
2467
|
// alteração e o botão "Atualizar" fica desabilitado para sempre — o
|
|
2320
2468
|
// mesmo defeito que os serviços tiveram e que fez a lista vazia travar
|
|
@@ -2388,7 +2536,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
2388
2536
|
if (!prefill?.startsAt || !prefill?.endsAt) return 0;
|
|
2389
2537
|
return Math.round((new Date(prefill.endsAt).getTime() - new Date(prefill.startsAt).getTime()) / 6e4);
|
|
2390
2538
|
}, [prefill?.startsAt, prefill?.endsAt]);
|
|
2391
|
-
const totalDuration = serviceDuration > 0 ? serviceDuration : selectionDuration;
|
|
2539
|
+
const totalDuration = durationOverride ?? (serviceDuration > 0 ? serviceDuration : selectionDuration);
|
|
2392
2540
|
const totalPrice = services.reduce((sum, s) => sum + s.price, 0);
|
|
2393
2541
|
const { currency } = config;
|
|
2394
2542
|
const endTime = useMemo(() => {
|
|
@@ -2455,7 +2603,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
2455
2603
|
setCheckingSeries(false);
|
|
2456
2604
|
};
|
|
2457
2605
|
}, [open, seriesDates, professionalId, startTime, totalDuration, bookingId, activeUnitId, provider2]);
|
|
2458
|
-
const [feriado, setFeriado] =
|
|
2606
|
+
const [feriado, setFeriado] = React23.useState(null);
|
|
2459
2607
|
useEffect(() => {
|
|
2460
2608
|
if (!open || !date || !provider2.getHolidays) {
|
|
2461
2609
|
setFeriado(null);
|
|
@@ -2483,11 +2631,21 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
2483
2631
|
function addService(opt) {
|
|
2484
2632
|
const duration = opt.data?.duration_minutes || 60;
|
|
2485
2633
|
setServices((p) => [...p, { serviceId: opt.id, name: opt.label, durationMinutes: duration, price: opt.price ?? 0 }]);
|
|
2634
|
+
setDurationOverride(null);
|
|
2486
2635
|
setServiceSearch("");
|
|
2487
2636
|
setServiceOptions([]);
|
|
2488
2637
|
}
|
|
2489
2638
|
function removeService(idx) {
|
|
2490
2639
|
setServices((p) => p.filter((_, i) => i !== idx));
|
|
2640
|
+
setDurationOverride(null);
|
|
2641
|
+
}
|
|
2642
|
+
function changeEndTime(value) {
|
|
2643
|
+
if (!value) return;
|
|
2644
|
+
const [sh, sm] = startTime.split(":").map(Number);
|
|
2645
|
+
const [eh, em] = value.split(":").map(Number);
|
|
2646
|
+
const minutes = eh * 60 + em - (sh * 60 + sm);
|
|
2647
|
+
if (minutes <= 0) return;
|
|
2648
|
+
setDurationOverride(minutes === (serviceDuration > 0 ? serviceDuration : selectionDuration) ? null : minutes);
|
|
2491
2649
|
}
|
|
2492
2650
|
const serviceCreate = useLookupCreate(
|
|
2493
2651
|
config.serviceLookup,
|
|
@@ -2522,6 +2680,10 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
2522
2680
|
vivo = false;
|
|
2523
2681
|
};
|
|
2524
2682
|
}, [services, provider2]);
|
|
2683
|
+
const showRevision = mode === "create" && !!clientId && services.some((s) => serviceKinds[s.serviceId] === "return");
|
|
2684
|
+
useEffect(() => {
|
|
2685
|
+
setRevisionChoice(null);
|
|
2686
|
+
}, [clientId]);
|
|
2525
2687
|
const showInterests = agendaPrefs.get("interestsOnBooking") && !!provider2.getClientInterests && !!provider2.setBookingInterests && !!clientId && services.some((s) => serviceKinds[s.serviceId] === "evaluation");
|
|
2526
2688
|
async function persistInterests(id) {
|
|
2527
2689
|
if (!showInterests) return;
|
|
@@ -2541,6 +2703,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
2541
2703
|
notes: notes || void 0,
|
|
2542
2704
|
title: activeType.fields.title ? title.trim() : void 0,
|
|
2543
2705
|
services: services.map((s) => ({ serviceId: s.serviceId, name: s.name, durationMinutes: s.durationMinutes, price: s.price })),
|
|
2706
|
+
...showRevision && revisionChoice ? { revisionOf: revisionChoice } : {},
|
|
2544
2707
|
attendees: activeType.fields.attendees ? attendees.map((a) => ({ personId: a.personId })) : void 0
|
|
2545
2708
|
};
|
|
2546
2709
|
}
|
|
@@ -2559,8 +2722,9 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
2559
2722
|
setSaving(true);
|
|
2560
2723
|
try {
|
|
2561
2724
|
const startsAt = (/* @__PURE__ */ new Date(`${date}T${startTime}:00`)).toISOString();
|
|
2725
|
+
const endsAt = durationOverride != null ? new Date(new Date(startsAt).getTime() + durationOverride * 6e4).toISOString() : void 0;
|
|
2562
2726
|
if (mode === "create") {
|
|
2563
|
-
const input = { kind: bookingType, startsAt, ...bookingPayload2() };
|
|
2727
|
+
const input = { kind: bookingType, startsAt, endsAt, ...bookingPayload2() };
|
|
2564
2728
|
const firstId = seriesDates && recurrenceRule ? (await createBookingSeries(input, recurrenceRule, seriesDates)).bookingIds[0] : (await createBooking(input)).id;
|
|
2565
2729
|
invalidateLimit("bookings_month");
|
|
2566
2730
|
if (firstId) {
|
|
@@ -2574,7 +2738,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
2574
2738
|
const plano = planSeriesEdit({
|
|
2575
2739
|
booking: { id: bookingId, seriesId },
|
|
2576
2740
|
scope,
|
|
2577
|
-
patch: { startsAt, status, ...bookingPayload2() },
|
|
2741
|
+
patch: { startsAt, ...endsAt ? { endsAt } : {}, status, ...bookingPayload2() },
|
|
2578
2742
|
timeOfDay: startTime,
|
|
2579
2743
|
dateChanged: date !== initialDate.current
|
|
2580
2744
|
});
|
|
@@ -2623,11 +2787,12 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
2623
2787
|
const needsCancellationReason = mode === "edit" && status === "cancelled" && !!config.cancellationDetails;
|
|
2624
2788
|
const needsCancellationNotes = needsCancellationReason && !!selectedCancellationReason?.requiresNotes;
|
|
2625
2789
|
const formSnapshot = useMemo(
|
|
2626
|
-
() => JSON.stringify({ title, clientId, professionalId, locationId, resourceId, date, startTime, status, notes, cancellationReasonId, cancellationNotes, services: services.map((s) => ({ id: s.serviceId, dur: s.durationMinutes, price: s.price })), attendees: attendees.map((a) => a.personId).sort() }),
|
|
2627
|
-
[title, clientId, professionalId, locationId, resourceId, date, startTime, status, notes, cancellationReasonId, cancellationNotes, services, attendees]
|
|
2790
|
+
() => JSON.stringify({ title, clientId, professionalId, locationId, resourceId, date, startTime, status, notes, cancellationReasonId, cancellationNotes, services: services.map((s) => ({ id: s.serviceId, dur: s.durationMinutes, price: s.price })), dur: durationOverride, attendees: attendees.map((a) => a.personId).sort() }),
|
|
2791
|
+
[title, clientId, professionalId, locationId, resourceId, date, startTime, status, notes, cancellationReasonId, cancellationNotes, services, durationOverride, attendees]
|
|
2628
2792
|
);
|
|
2629
2793
|
const isDirty = mode === "create" || formSnapshot !== initialSnapshot.current;
|
|
2630
|
-
const canSubmit = (!activeType.fields.title || title.trim()) && (!activeType.requiresClient || clientId) && (!activeType.fields.professional || professionalId) && (!activeType.requiresServices || services.length > 0) && (!needsCancellationReason || !!cancellationReasonId) && (!needsCancellationNotes || !!cancellationNotes.trim()) &&
|
|
2794
|
+
const canSubmit = (!activeType.fields.title || title.trim()) && (!activeType.requiresClient || clientId) && (!activeType.fields.professional || professionalId) && (!activeType.requiresServices || services.length > 0) && (!needsCancellationReason || !!cancellationReasonId) && (!needsCancellationNotes || !!cancellationNotes.trim()) && // V1: a Revisão only goes in pointing at an executed service.
|
|
2795
|
+
(!showRevision || revisionState === "unavailable" || !!revisionChoice) && !conflict && !saving;
|
|
2631
2796
|
const showTabs = config.bookingTypes.length > 1;
|
|
2632
2797
|
const modalTitle = mode === "create" ? t2("agenda.appointment.new") : title.trim() || clientSearch || t2("agenda.appointment.edit");
|
|
2633
2798
|
return (
|
|
@@ -2674,7 +2839,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
2674
2839
|
),
|
|
2675
2840
|
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2676
2841
|
/* @__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) => {
|
|
2842
|
+
showTabs && mode === "create" && !clientId ? /* @__PURE__ */ jsx("div", { className: "flex gap-1 border-b fayz-glass-divider", children: config.bookingTypes.map((bt) => {
|
|
2678
2843
|
const label = rotuloDoTipo(t2, bt.id, bt.label);
|
|
2679
2844
|
const isActive = bookingType === bt.id;
|
|
2680
2845
|
const BtIcon = BOOKING_TYPE_ICONS[bt.icon] ?? Calendar;
|
|
@@ -2692,7 +2857,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
2692
2857
|
},
|
|
2693
2858
|
bt.id
|
|
2694
2859
|
);
|
|
2695
|
-
}) }) : mode === "edit" && (config.clientEntityDef && clientId || clientId || config.modules.financial && editOrderId ? /* @__PURE__ */ jsx("div", { className: "flex gap-1 border-b fayz-glass-divider", children: [
|
|
2860
|
+
}) }) : (mode === "edit" || !!clientId) && (config.clientEntityDef && clientId || clientId || config.modules.financial && editOrderId ? /* @__PURE__ */ jsx("div", { className: "flex gap-1 border-b fayz-glass-divider", children: [
|
|
2696
2861
|
{ id: "appointment", label: rotuloDoTipo(t2, bookingType, activeType.label), icon: Calendar, show: true },
|
|
2697
2862
|
{ id: "client", label: t2("agenda.appointment.clientTab"), icon: User, show: !!config.clientEntityDef && !!clientId },
|
|
2698
2863
|
// Entre a ficha e o dinheiro, como no V1: "quem é" → "o que
|
|
@@ -2787,7 +2952,38 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
2787
2952
|
}
|
|
2788
2953
|
),
|
|
2789
2954
|
/* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground/60", children: "\u2014" }),
|
|
2790
|
-
/* @__PURE__ */ jsx(
|
|
2955
|
+
editingEnd ? /* @__PURE__ */ jsx(
|
|
2956
|
+
"input",
|
|
2957
|
+
{
|
|
2958
|
+
type: "time",
|
|
2959
|
+
value: endDraft ?? endTime,
|
|
2960
|
+
autoFocus: true,
|
|
2961
|
+
"aria-label": t2("agenda.appointment.endTime"),
|
|
2962
|
+
onChange: (e) => {
|
|
2963
|
+
setEndDraft(e.target.value);
|
|
2964
|
+
changeEndTime(e.target.value);
|
|
2965
|
+
},
|
|
2966
|
+
onBlur: () => {
|
|
2967
|
+
setEndDraft(null);
|
|
2968
|
+
setEditingEnd(false);
|
|
2969
|
+
},
|
|
2970
|
+
className: "w-auto shrink-0 border-b border-foreground bg-transparent text-xs text-muted-foreground focus:outline-none"
|
|
2971
|
+
}
|
|
2972
|
+
) : /* @__PURE__ */ jsx(
|
|
2973
|
+
"button",
|
|
2974
|
+
{
|
|
2975
|
+
type: "button",
|
|
2976
|
+
onClick: () => setEditingEnd(true),
|
|
2977
|
+
"aria-label": t2("agenda.appointment.endTime"),
|
|
2978
|
+
className: "shrink-0 border-b border-transparent px-0.5 py-0.5 text-xs text-muted-foreground transition-colors hover:border-foreground",
|
|
2979
|
+
children: endTime
|
|
2980
|
+
}
|
|
2981
|
+
),
|
|
2982
|
+
totalDuration > 0 && /* @__PURE__ */ jsxs("span", { className: "text-[11px] text-muted-foreground/60", children: [
|
|
2983
|
+
"\xB7 ",
|
|
2984
|
+
totalDuration,
|
|
2985
|
+
" min"
|
|
2986
|
+
] }),
|
|
2791
2987
|
activeType.fields.professional && /* @__PURE__ */ jsx("div", { className: "ml-auto shrink-0", children: /* @__PURE__ */ jsx(ProfessionalSelect, { value: professionalId, onChange: setProfessionalId, professionals: professionalOptions }) })
|
|
2792
2988
|
]
|
|
2793
2989
|
}
|
|
@@ -3019,7 +3215,20 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
3019
3215
|
}
|
|
3020
3216
|
) })
|
|
3021
3217
|
] }),
|
|
3022
|
-
activeType.fields.client && clientId && /* @__PURE__ */
|
|
3218
|
+
activeType.fields.client && clientId && /* @__PURE__ */ jsxs("div", { className: "pl-7", children: [
|
|
3219
|
+
/* @__PURE__ */ jsx(PersonFormsAlert, { personId: clientId, version: formsVersao, onFill: (t3) => setPreenchendoForm(t3), sendLink: "soon" }),
|
|
3220
|
+
activeType.fields.services && /* @__PURE__ */ jsx("div", { className: "mt-2", children: /* @__PURE__ */ jsx(
|
|
3221
|
+
BookingPrepaidPanel,
|
|
3222
|
+
{
|
|
3223
|
+
clientId,
|
|
3224
|
+
addedServiceIds: services.map((s) => s.serviceId),
|
|
3225
|
+
onAdd: (it) => {
|
|
3226
|
+
setServices((p) => [...p, { serviceId: it.serviceId, name: it.serviceName, durationMinutes: it.durationMinutes, price: 0 }]);
|
|
3227
|
+
setDurationOverride(null);
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
) })
|
|
3231
|
+
] }),
|
|
3023
3232
|
activeType.fields.attendees && /* @__PURE__ */ jsxs("div", { className: `${ROW_TOP} py-1`, children: [
|
|
3024
3233
|
/* @__PURE__ */ jsx(Users, { className: "h-4 w-4 mt-1.5 text-muted-foreground shrink-0" }),
|
|
3025
3234
|
/* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
|
|
@@ -3062,7 +3271,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
3062
3271
|
)
|
|
3063
3272
|
] })
|
|
3064
3273
|
] }),
|
|
3065
|
-
activeType.fields.location && config.modules.locationSelection && (storeLocations.length > 0 || config.locations.length > 0) && /* @__PURE__ */ jsxs("div", { className: `${ROW_CENTER} py-1`, children: [
|
|
3274
|
+
activeType.fields.location && config.modules.locationSelection && (storeLocations.length > 0 || config.locations.length > 0) && !(mode === "create" && prefill?.locationId && locationId === prefill.locationId) && /* @__PURE__ */ jsxs("div", { className: `${ROW_CENTER} py-1`, children: [
|
|
3066
3275
|
/* @__PURE__ */ jsx(MapPin, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
|
|
3067
3276
|
/* @__PURE__ */ jsx(
|
|
3068
3277
|
LocationSelect,
|
|
@@ -3156,6 +3365,15 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
3156
3365
|
inlineIcon: services.length > 0 ? "plus" : "search"
|
|
3157
3366
|
}
|
|
3158
3367
|
),
|
|
3368
|
+
showRevision && /* @__PURE__ */ jsx("div", { className: "mt-2", children: /* @__PURE__ */ jsx(
|
|
3369
|
+
BookingRevisionPanel,
|
|
3370
|
+
{
|
|
3371
|
+
clientId,
|
|
3372
|
+
value: revisionChoice,
|
|
3373
|
+
onChange: setRevisionChoice,
|
|
3374
|
+
onState: setRevisionState
|
|
3375
|
+
}
|
|
3376
|
+
) }),
|
|
3159
3377
|
showInterests && /* @__PURE__ */ jsx("div", { className: "mt-2", children: /* @__PURE__ */ jsx(
|
|
3160
3378
|
BookingInterestsPanel,
|
|
3161
3379
|
{
|
|
@@ -3508,7 +3726,7 @@ function SeriesDeleteChoice({ seriesSize, deleting, onPick, onCancel }) {
|
|
|
3508
3726
|
function fmtTime(iso) {
|
|
3509
3727
|
return new Date(iso).toLocaleTimeString(void 0, { hour: "2-digit", minute: "2-digit" });
|
|
3510
3728
|
}
|
|
3511
|
-
function
|
|
3729
|
+
function fmtDate2(iso) {
|
|
3512
3730
|
return new Date(iso).toLocaleDateString(void 0, { weekday: "long", month: "long", day: "numeric" });
|
|
3513
3731
|
}
|
|
3514
3732
|
function fmtCurrency2(amount, locale, code) {
|
|
@@ -3635,7 +3853,7 @@ function AppointmentPopover({ booking, position: position2, onClose, onEdit }) {
|
|
|
3635
3853
|
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
3636
3854
|
config.capabilities.client ? /* @__PURE__ */ jsx(PersonLink, { personId: booking.clientId, name: booking.clientName ?? "Unknown", className: "text-base" }) : /* @__PURE__ */ jsx("p", { className: "text-base font-medium truncate", children: booking.clientName || t2("agenda.appointment.untitled") }),
|
|
3637
3855
|
/* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground mt-0.5", children: [
|
|
3638
|
-
|
|
3856
|
+
fmtDate2(booking.startsAt),
|
|
3639
3857
|
" \xB7 ",
|
|
3640
3858
|
timeRange
|
|
3641
3859
|
] })
|
|
@@ -3974,8 +4192,25 @@ function opensOnDate(rows, iso) {
|
|
|
3974
4192
|
}
|
|
3975
4193
|
|
|
3976
4194
|
// src/lib/calendar-events.ts
|
|
3977
|
-
|
|
4195
|
+
var REVISION_CARD_COLOR = "#111827";
|
|
4196
|
+
var DEFAULT_SERVICE_CARD_COLOR = "#3b82f6";
|
|
4197
|
+
function bookingCardColor(booking, looks) {
|
|
4198
|
+
const own = (booking.services ?? []).map((s) => s.serviceId ? looks[s.serviceId] : void 0).filter(Boolean);
|
|
4199
|
+
if (own.length === 0) return null;
|
|
4200
|
+
if (own.some((l) => l.kind === "return")) return REVISION_CARD_COLOR;
|
|
4201
|
+
return own.find((l) => l.color)?.color ?? DEFAULT_SERVICE_CARD_COLOR;
|
|
4202
|
+
}
|
|
4203
|
+
function readableTextOn(hex) {
|
|
4204
|
+
let h = hex.replace("#", "");
|
|
4205
|
+
if (h.length === 3) h = h.split("").map((c) => c + c).join("");
|
|
4206
|
+
const [r, g, b] = [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16) / 255);
|
|
4207
|
+
const lin = (c) => c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
4208
|
+
const L = 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
|
|
4209
|
+
return (L + 0.05) / 0.05 > 1.05 / (L + 0.05) ? "#111827" : "#ffffff";
|
|
4210
|
+
}
|
|
4211
|
+
function bookingToEvent(booking, statusColors, visibleResourceIds = [], serviceLooks) {
|
|
3978
4212
|
const color = statusColors[booking.status] ?? "#6b7280";
|
|
4213
|
+
const kindColor = serviceLooks ? bookingCardColor(booking, serviceLooks) : null;
|
|
3979
4214
|
const serviceNames = booking.services?.map((service) => service.name).join(", ") ?? "";
|
|
3980
4215
|
return {
|
|
3981
4216
|
id: booking.id,
|
|
@@ -3986,10 +4221,10 @@ function bookingToEvent(booking, statusColors, visibleResourceIds = []) {
|
|
|
3986
4221
|
title: booking.clientName ?? "Unknown",
|
|
3987
4222
|
start: booking.startsAt,
|
|
3988
4223
|
end: booking.endsAt ?? void 0,
|
|
3989
|
-
backgroundColor: color + "30",
|
|
3990
|
-
borderColor: color,
|
|
3991
|
-
textColor: color,
|
|
3992
|
-
extendedProps: { booking, serviceNames }
|
|
4224
|
+
backgroundColor: kindColor ?? color + "30",
|
|
4225
|
+
borderColor: kindColor ?? color,
|
|
4226
|
+
textColor: kindColor ? readableTextOn(kindColor) : color,
|
|
4227
|
+
extendedProps: { booking, serviceNames, ...kindColor ? { statusStripe: color } : {} }
|
|
3993
4228
|
};
|
|
3994
4229
|
}
|
|
3995
4230
|
|
|
@@ -4227,9 +4462,9 @@ function CalendarView() {
|
|
|
4227
4462
|
const [serviceFilter, setServiceFilter] = useState([]);
|
|
4228
4463
|
const [hideEmptyGrids, setHideEmptyGrids] = useState(false);
|
|
4229
4464
|
const [schedulesOpen, setSchedulesOpen] = useState(false);
|
|
4230
|
-
const [blockFor, setBlockFor] =
|
|
4231
|
-
const [schedulesFor, setSchedulesFor] =
|
|
4232
|
-
const [editingBlock, setEditingBlock] =
|
|
4465
|
+
const [blockFor, setBlockFor] = React23.useState(null);
|
|
4466
|
+
const [schedulesFor, setSchedulesFor] = React23.useState(null);
|
|
4467
|
+
const [editingBlock, setEditingBlock] = React23.useState(null);
|
|
4233
4468
|
const isMobile = useIsMobile();
|
|
4234
4469
|
const resourceCapable = professionals.length > 0;
|
|
4235
4470
|
const viewOptions = resourceCapable ? RESOURCE_VIEW_OPTIONS : PLAIN_VIEW_OPTIONS;
|
|
@@ -4617,9 +4852,33 @@ function CalendarView() {
|
|
|
4617
4852
|
}
|
|
4618
4853
|
return out;
|
|
4619
4854
|
}, [blocks, visibleDates, visibleRange]);
|
|
4855
|
+
const cardPrefs = useTenantPluginSettings("agenda", { cardColorByServiceKind: false });
|
|
4856
|
+
const colorByKind = cardPrefs.get("cardColorByServiceKind");
|
|
4857
|
+
const serviceIdsKey = useMemo(() => {
|
|
4858
|
+
if (!colorByKind) return "";
|
|
4859
|
+
const ids = /* @__PURE__ */ new Set();
|
|
4860
|
+
for (const b of filteredBookings) for (const sv of b.services ?? []) if (sv.serviceId) ids.add(sv.serviceId);
|
|
4861
|
+
return [...ids].sort().join(",");
|
|
4862
|
+
}, [colorByKind, filteredBookings]);
|
|
4863
|
+
const [serviceLooks, setServiceLooks] = useState(void 0);
|
|
4864
|
+
useEffect(() => {
|
|
4865
|
+
if (!serviceIdsKey || !provider2.getServiceAppearance) {
|
|
4866
|
+
setServiceLooks(void 0);
|
|
4867
|
+
return;
|
|
4868
|
+
}
|
|
4869
|
+
let alive = true;
|
|
4870
|
+
provider2.getServiceAppearance(serviceIdsKey.split(",")).then((m) => {
|
|
4871
|
+
if (alive) setServiceLooks(m);
|
|
4872
|
+
}).catch(() => {
|
|
4873
|
+
if (alive) setServiceLooks(void 0);
|
|
4874
|
+
});
|
|
4875
|
+
return () => {
|
|
4876
|
+
alive = false;
|
|
4877
|
+
};
|
|
4878
|
+
}, [serviceIdsKey, provider2]);
|
|
4620
4879
|
const bookingEvents = useMemo(
|
|
4621
|
-
() => filteredBookings.map((b) => bookingToEvent(b, statusColors, visibleResourceIds)),
|
|
4622
|
-
[filteredBookings, statusColors, visibleResourceIds]
|
|
4880
|
+
() => filteredBookings.map((b) => bookingToEvent(b, statusColors, visibleResourceIds, colorByKind ? serviceLooks : void 0)),
|
|
4881
|
+
[filteredBookings, statusColors, visibleResourceIds, colorByKind, serviceLooks]
|
|
4623
4882
|
);
|
|
4624
4883
|
const allLocations = useMemo(() => {
|
|
4625
4884
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -4806,15 +5065,15 @@ function CalendarView() {
|
|
|
4806
5065
|
return;
|
|
4807
5066
|
}
|
|
4808
5067
|
const grupo = toEditingBlock(clicado, blocks);
|
|
4809
|
-
const
|
|
5068
|
+
const hhmm2 = (iso) => new Date(iso).toTimeString().slice(0, 5);
|
|
4810
5069
|
try {
|
|
4811
5070
|
await provider2.deleteBlock(grupo.ids);
|
|
4812
5071
|
await provider2.createBlock({
|
|
4813
5072
|
assigneeId: arg.newResource?.id ?? grupo.assigneeId,
|
|
4814
5073
|
title: grupo.title,
|
|
4815
5074
|
description: grupo.description || void 0,
|
|
4816
|
-
startsAt:
|
|
4817
|
-
endsAt:
|
|
5075
|
+
startsAt: hhmm2(arg.event.startStr),
|
|
5076
|
+
endsAt: hhmm2(arg.event.endStr),
|
|
4818
5077
|
...grupo.specificDate ? { specificDate: arg.event.startStr.slice(0, 10) } : {
|
|
4819
5078
|
daysOfWeek: grupo.daysOfWeek,
|
|
4820
5079
|
validFrom: grupo.validFrom ?? void 0,
|
|
@@ -5115,7 +5374,9 @@ function CalendarView() {
|
|
|
5115
5374
|
const end = arg.event.end;
|
|
5116
5375
|
const fmt = (d) => `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
5117
5376
|
const timeRange = start && end ? `${fmt(start)} \u2013 ${fmt(end)}` : start ? fmt(start) : "";
|
|
5118
|
-
|
|
5377
|
+
const stripe = arg.event.extendedProps?.statusStripe;
|
|
5378
|
+
return /* @__PURE__ */ jsxs("div", { className: `relative py-0.5 overflow-hidden leading-tight h-full ${stripe ? "pl-2.5 pr-1.5" : "px-1.5"}`, children: [
|
|
5379
|
+
stripe && /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "absolute inset-y-0 left-0 w-1.5", style: { backgroundColor: stripe } }),
|
|
5119
5380
|
/* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-1", children: [
|
|
5120
5381
|
/* @__PURE__ */ jsx("p", { className: "text-[11px] font-semibold truncate", children: arg.event.title }),
|
|
5121
5382
|
serviceNames && /* @__PURE__ */ jsx("p", { className: "text-[10px] opacity-60 truncate shrink-0 max-w-[45%] text-right", children: serviceNames })
|
|
@@ -5572,12 +5833,29 @@ function getAgendaTenantId() {
|
|
|
5572
5833
|
function AgendaPage({ config, provider: provider2, store, children }) {
|
|
5573
5834
|
const runtime = usePluginRuntimeOptional();
|
|
5574
5835
|
const tenantId = runtime?.context.tenant?.id;
|
|
5575
|
-
|
|
5836
|
+
React23.useEffect(() => {
|
|
5576
5837
|
setAgendaTenantId(tenantId);
|
|
5577
5838
|
return () => setAgendaTenantId(void 0);
|
|
5578
5839
|
}, [tenantId]);
|
|
5579
5840
|
return /* @__PURE__ */ jsx(AgendaContextProvider, { config, provider: provider2, store, children: /* @__PURE__ */ jsx("div", { className: "h-full min-h-0 space-y-3 overflow-y-auto p-2 md:space-y-4 md:px-3 md:pb-3 md:pt-2", children: children ?? /* @__PURE__ */ jsx(CalendarView, {}) }) });
|
|
5580
5841
|
}
|
|
5842
|
+
|
|
5843
|
+
// src/lib/checkin-required.ts
|
|
5844
|
+
function missingRequiredFields(fields, row, skip = ["name"]) {
|
|
5845
|
+
if (!row) return [];
|
|
5846
|
+
const out = [];
|
|
5847
|
+
for (const f of fields) {
|
|
5848
|
+
if (!f.required || skip.includes(f.key)) continue;
|
|
5849
|
+
const column = f.key.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
|
|
5850
|
+
const has = Object.prototype.hasOwnProperty.call(row, column) || Object.prototype.hasOwnProperty.call(row, f.key);
|
|
5851
|
+
if (!has) continue;
|
|
5852
|
+
const v = row[column] ?? row[f.key];
|
|
5853
|
+
if (v == null || typeof v === "string" && v.trim() === "") {
|
|
5854
|
+
out.push({ key: f.key, label: typeof f.label === "string" ? f.label : f.key });
|
|
5855
|
+
}
|
|
5856
|
+
}
|
|
5857
|
+
return out;
|
|
5858
|
+
}
|
|
5581
5859
|
var brl = (n) => n.toLocaleString("pt-BR", { style: "currency", currency: "BRL" });
|
|
5582
5860
|
function CheckinDialog({
|
|
5583
5861
|
booking,
|
|
@@ -5588,10 +5866,10 @@ function CheckinDialog({
|
|
|
5588
5866
|
onClose,
|
|
5589
5867
|
formatCurrency: formatCurrency2 = brl
|
|
5590
5868
|
}) {
|
|
5591
|
-
const [busy, setBusy] =
|
|
5592
|
-
const [preenchendo, setPreenchendo] =
|
|
5593
|
-
const [preenchidos, setPreenchidos] =
|
|
5594
|
-
const [pendencias, setPendencias] =
|
|
5869
|
+
const [busy, setBusy] = React23.useState(false);
|
|
5870
|
+
const [preenchendo, setPreenchendo] = React23.useState(null);
|
|
5871
|
+
const [preenchidos, setPreenchidos] = React23.useState(0);
|
|
5872
|
+
const [pendencias, setPendencias] = React23.useState([]);
|
|
5595
5873
|
const blocking = requirements.filter((r) => r.blocking);
|
|
5596
5874
|
const paid = booking.paymentStatus === "paid";
|
|
5597
5875
|
const hasBill = !!booking.paymentStatus && booking.paymentStatus !== "none";
|
|
@@ -5635,7 +5913,8 @@ function CheckinDialog({
|
|
|
5635
5913
|
id: `cadastro:${r.label}`,
|
|
5636
5914
|
label: r.label,
|
|
5637
5915
|
state: r.blocking ? "late" : "todo",
|
|
5638
|
-
detail: r.detail
|
|
5916
|
+
detail: r.detail,
|
|
5917
|
+
action: r.action
|
|
5639
5918
|
})),
|
|
5640
5919
|
onPending: setPendencias,
|
|
5641
5920
|
onFill: (templateId) => setPreenchendo(templateId)
|
|
@@ -5701,7 +5980,7 @@ function CheckinDialog({
|
|
|
5701
5980
|
}
|
|
5702
5981
|
function PaymentStatusBadge(props) {
|
|
5703
5982
|
const t2 = useTranslation();
|
|
5704
|
-
const labels =
|
|
5983
|
+
const labels = React23.useMemo(() => ({
|
|
5705
5984
|
none: t2("agenda.payment.title"),
|
|
5706
5985
|
pending: t2("agenda.payment.pending"),
|
|
5707
5986
|
partial: t2("agenda.payment.partial"),
|
|
@@ -5861,7 +6140,7 @@ function CheckinView({ onReceive, onOpenBooking, onOpenAttendance }) {
|
|
|
5861
6140
|
const config = useAgendaConfig();
|
|
5862
6141
|
const provider2 = useAgendaProvider();
|
|
5863
6142
|
const statusLabel = (v) => config.statuses.find((x) => x.value === v)?.label ?? v;
|
|
5864
|
-
const checkinStatuses =
|
|
6143
|
+
const checkinStatuses = React23.useMemo(
|
|
5865
6144
|
() => CHECKIN_STATUSES.map((v) => config.statuses.find((x) => x.value === v)).filter((x) => !!x),
|
|
5866
6145
|
[config.statuses]
|
|
5867
6146
|
);
|
|
@@ -5869,15 +6148,45 @@ function CheckinView({ onReceive, onOpenBooking, onOpenAttendance }) {
|
|
|
5869
6148
|
const loading = useAgendaStore((s) => s.bookingsLoading);
|
|
5870
6149
|
const fetchBookings = useAgendaStore((s) => s.fetchBookings);
|
|
5871
6150
|
const updateBookingStatus = useAgendaStore((s) => s.updateBookingStatus);
|
|
5872
|
-
const [term, setTerm] =
|
|
5873
|
-
const [busyId, setBusyId] =
|
|
5874
|
-
const [myPersonId, setMyPersonId] =
|
|
5875
|
-
const [startRefusal, setStartRefusal] =
|
|
5876
|
-
const [checkouts, setCheckouts] =
|
|
5877
|
-
const [conferindo, setConferindo] =
|
|
5878
|
-
const [checkoutRefusal, setCheckoutRefusal] =
|
|
5879
|
-
const [panel, setPanel] =
|
|
6151
|
+
const [term, setTerm] = React23.useState("");
|
|
6152
|
+
const [busyId, setBusyId] = React23.useState(null);
|
|
6153
|
+
const [myPersonId, setMyPersonId] = React23.useState(void 0);
|
|
6154
|
+
const [startRefusal, setStartRefusal] = React23.useState(null);
|
|
6155
|
+
const [checkouts, setCheckouts] = React23.useState([]);
|
|
6156
|
+
const [conferindo, setConferindo] = React23.useState(null);
|
|
6157
|
+
const [checkoutRefusal, setCheckoutRefusal] = React23.useState(null);
|
|
6158
|
+
const [panel, setPanel] = React23.useState(null);
|
|
5880
6159
|
const reviewing = panel?.kind === "review" ? panel.booking : null;
|
|
6160
|
+
const clientDef = config.clientEntityDef;
|
|
6161
|
+
const { applyRules } = useFieldRules(clientDef ? deriveEntityKey(clientDef) : "");
|
|
6162
|
+
const clientFields = React23.useMemo(() => clientDef ? applyRules(clientDef.fields) : [], [clientDef, applyRules]);
|
|
6163
|
+
const [clientRow, setClientRow] = React23.useState(null);
|
|
6164
|
+
const [clientRowVersion, setClientRowVersion] = React23.useState(0);
|
|
6165
|
+
React23.useEffect(() => {
|
|
6166
|
+
const id = reviewing?.clientId;
|
|
6167
|
+
if (!id || !config.contactLookup?.getById) {
|
|
6168
|
+
setClientRow(null);
|
|
6169
|
+
return;
|
|
6170
|
+
}
|
|
6171
|
+
let alive = true;
|
|
6172
|
+
config.contactLookup.getById(id).then((r) => {
|
|
6173
|
+
if (alive) setClientRow(r?.data ? { ...r.data, id } : null);
|
|
6174
|
+
}).catch(() => {
|
|
6175
|
+
if (alive) setClientRow(null);
|
|
6176
|
+
});
|
|
6177
|
+
return () => {
|
|
6178
|
+
alive = false;
|
|
6179
|
+
};
|
|
6180
|
+
}, [reviewing?.clientId, config.contactLookup, clientRowVersion]);
|
|
6181
|
+
React23.useEffect(() => {
|
|
6182
|
+
const again = () => setClientRowVersion((n) => n + 1);
|
|
6183
|
+
window.addEventListener("focus", again);
|
|
6184
|
+
window.addEventListener("hashchange", again);
|
|
6185
|
+
return () => {
|
|
6186
|
+
window.removeEventListener("focus", again);
|
|
6187
|
+
window.removeEventListener("hashchange", again);
|
|
6188
|
+
};
|
|
6189
|
+
}, []);
|
|
5881
6190
|
const editing = panel?.kind === "edit" ? panel.booking : null;
|
|
5882
6191
|
const charging = panel?.kind === "charge" ? panel.booking : null;
|
|
5883
6192
|
const setReviewing = (b) => setPanel(b ? { kind: "review", booking: b } : null);
|
|
@@ -5885,31 +6194,41 @@ function CheckinView({ onReceive, onOpenBooking, onOpenAttendance }) {
|
|
|
5885
6194
|
const setCharging = (b) => setPanel(b ? { kind: "charge", booking: b } : null);
|
|
5886
6195
|
const can = usePermissionOptional();
|
|
5887
6196
|
const canReceive = can("financial", "create") || can("financial.receivables", "create");
|
|
5888
|
-
|
|
6197
|
+
React23.useEffect(() => {
|
|
5889
6198
|
void fetchBookings(todayRange(), [...CHECKIN_STATUSES]);
|
|
5890
6199
|
}, [fetchBookings]);
|
|
5891
|
-
const [shownStatuses, setShownStatuses] =
|
|
5892
|
-
const [shownPros, setShownPros] =
|
|
5893
|
-
const prosToday =
|
|
6200
|
+
const [shownStatuses, setShownStatuses] = React23.useState([]);
|
|
6201
|
+
const [shownPros, setShownPros] = React23.useState([]);
|
|
6202
|
+
const prosToday = React23.useMemo(() => {
|
|
5894
6203
|
const seen = /* @__PURE__ */ new Map();
|
|
5895
6204
|
for (const b of bookings) if (b.professionalName) seen.set(b.professionalName, b.professionalName);
|
|
5896
6205
|
return [...seen.keys()].sort();
|
|
5897
6206
|
}, [bookings]);
|
|
5898
|
-
const cards =
|
|
6207
|
+
const cards = React23.useMemo(() => {
|
|
5899
6208
|
const q = term.trim().toLowerCase();
|
|
5900
6209
|
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));
|
|
5901
6210
|
}, [bookings, term, shownStatuses, shownPros]);
|
|
5902
6211
|
const waiting = cards.filter((b) => b.status === "waiting").length;
|
|
5903
6212
|
function requirementsFor(b) {
|
|
5904
6213
|
const out = [];
|
|
6214
|
+
if (b.clientId && b.clientId === clientRow?.id) {
|
|
6215
|
+
for (const m of missingRequiredFields(clientFields, clientRow)) {
|
|
6216
|
+
out.push({
|
|
6217
|
+
label: m.label,
|
|
6218
|
+
detail: "obrigat\xF3rio no cadastro",
|
|
6219
|
+
blocking: true,
|
|
6220
|
+
action: { label: "Completar cadastro", onClick: () => openEntity(b.clientId, "person", "customer") }
|
|
6221
|
+
});
|
|
6222
|
+
}
|
|
6223
|
+
}
|
|
5905
6224
|
if (!b.clientName) out.push({ label: "Cliente", detail: "o agendamento n\xE3o nomeia ningu\xE9m", blocking: true });
|
|
5906
6225
|
if (!b.professionalId) out.push({ label: "Profissional", detail: "ningu\xE9m para executar" });
|
|
5907
6226
|
if (!b.services?.length) out.push({ label: "Servi\xE7o", detail: "nada a executar nem a cobrar" });
|
|
5908
6227
|
return out;
|
|
5909
6228
|
}
|
|
5910
6229
|
const money = (n) => n.toLocaleString("pt-BR", { style: "currency", currency: "BRL" });
|
|
5911
|
-
const [lost, setLost] =
|
|
5912
|
-
|
|
6230
|
+
const [lost, setLost] = React23.useState({ cancelled: 0, noShow: 0 });
|
|
6231
|
+
React23.useEffect(() => {
|
|
5913
6232
|
if (!provider2.getLostCounts) return;
|
|
5914
6233
|
let cancelado = false;
|
|
5915
6234
|
provider2.getLostCounts(todayRange()).then((c) => {
|
|
@@ -5920,7 +6239,7 @@ function CheckinView({ onReceive, onOpenBooking, onOpenAttendance }) {
|
|
|
5920
6239
|
cancelado = true;
|
|
5921
6240
|
};
|
|
5922
6241
|
}, [provider2]);
|
|
5923
|
-
const openAmount =
|
|
6242
|
+
const openAmount = React23.useMemo(
|
|
5924
6243
|
() => bookings.filter((b) => b.orderTotal && b.paymentStatus && b.paymentStatus !== "paid").reduce((soma, b) => soma + (b.orderTotal ?? 0), 0),
|
|
5925
6244
|
[bookings]
|
|
5926
6245
|
);
|
|
@@ -5932,7 +6251,7 @@ function CheckinView({ onReceive, onOpenBooking, onOpenAttendance }) {
|
|
|
5932
6251
|
amount: m.paidAmount
|
|
5933
6252
|
}));
|
|
5934
6253
|
}
|
|
5935
|
-
|
|
6254
|
+
React23.useEffect(() => {
|
|
5936
6255
|
let vivo = true;
|
|
5937
6256
|
(async () => {
|
|
5938
6257
|
try {
|
|
@@ -5946,17 +6265,17 @@ function CheckinView({ onReceive, onOpenBooking, onOpenAttendance }) {
|
|
|
5946
6265
|
vivo = false;
|
|
5947
6266
|
};
|
|
5948
6267
|
}, []);
|
|
5949
|
-
const recarregarCheckouts =
|
|
6268
|
+
const recarregarCheckouts = React23.useCallback(async () => {
|
|
5950
6269
|
try {
|
|
5951
6270
|
setCheckouts(await provider2.getPendingCheckouts?.() ?? []);
|
|
5952
6271
|
} catch {
|
|
5953
6272
|
setCheckouts([]);
|
|
5954
6273
|
}
|
|
5955
6274
|
}, []);
|
|
5956
|
-
|
|
6275
|
+
React23.useEffect(() => {
|
|
5957
6276
|
void recarregarCheckouts();
|
|
5958
6277
|
}, [recarregarCheckouts]);
|
|
5959
|
-
|
|
6278
|
+
React23.useEffect(() => {
|
|
5960
6279
|
const parar = provider2.subscribeToBookingChanges?.(() => {
|
|
5961
6280
|
void recarregarCheckouts();
|
|
5962
6281
|
void fetchBookings(todayRange(), [...CHECKIN_STATUSES]);
|
|
@@ -6339,7 +6658,7 @@ function CheckinView({ onReceive, onOpenBooking, onOpenAttendance }) {
|
|
|
6339
6658
|
function CheckinPage({ config, provider: provider2, store, onReceive, onOpenBooking }) {
|
|
6340
6659
|
const runtime = usePluginRuntimeOptional();
|
|
6341
6660
|
const tenantId = runtime?.context.tenant?.id;
|
|
6342
|
-
|
|
6661
|
+
React23.useEffect(() => {
|
|
6343
6662
|
setAgendaTenantId(tenantId);
|
|
6344
6663
|
return () => setAgendaTenantId(void 0);
|
|
6345
6664
|
}, [tenantId]);
|
|
@@ -6352,11 +6671,11 @@ function hoje2() {
|
|
|
6352
6671
|
}
|
|
6353
6672
|
function PersonAttendanceBlock({ personId }) {
|
|
6354
6673
|
const provider2 = useAgendaProvider();
|
|
6355
|
-
const [pessoa, setPessoa] =
|
|
6356
|
-
const [booking, setBooking] =
|
|
6357
|
-
const [indo, setIndo] =
|
|
6358
|
-
const [recusa, setRecusa] =
|
|
6359
|
-
|
|
6674
|
+
const [pessoa, setPessoa] = React23.useState(void 0);
|
|
6675
|
+
const [booking, setBooking] = React23.useState(null);
|
|
6676
|
+
const [indo, setIndo] = React23.useState(false);
|
|
6677
|
+
const [recusa, setRecusa] = React23.useState(null);
|
|
6678
|
+
React23.useEffect(() => {
|
|
6360
6679
|
let vivo = true;
|
|
6361
6680
|
(async () => {
|
|
6362
6681
|
const eu = await provider2.getCurrentPersonId?.() ?? null;
|
|
@@ -7169,7 +7488,7 @@ function createSupabaseAgendaProvider(providerOptions) {
|
|
|
7169
7488
|
const hasServices = input.services.length > 0;
|
|
7170
7489
|
const totalDuration = input.services.reduce((sum, s) => sum + s.durationMinutes, 0);
|
|
7171
7490
|
input.services.reduce((sum, s) => sum + s.price, 0);
|
|
7172
|
-
const endsAt = addMinutes(input.startsAt, totalDuration || 30);
|
|
7491
|
+
const endsAt = input.endsAt ?? addMinutes(input.startsAt, totalDuration || 30);
|
|
7173
7492
|
const serviceNames = input.services.map((s) => s.name).join(", ");
|
|
7174
7493
|
let contactName;
|
|
7175
7494
|
if (input.clientId) {
|
|
@@ -7186,7 +7505,9 @@ function createSupabaseAgendaProvider(providerOptions) {
|
|
|
7186
7505
|
// quando existe: gravar `dealId: null` poria a chave em toda reserva
|
|
7187
7506
|
// de mesa e todo horário de salão, e um campo que está em tudo e vale
|
|
7188
7507
|
// para quase nada é o que faz ninguém confiar no metadata.
|
|
7189
|
-
...input.dealId ? { dealId: input.dealId } : {}
|
|
7508
|
+
...input.dealId ? { dealId: input.dealId } : {},
|
|
7509
|
+
// What this revision revises, chosen at booking; the ruler links the session when it exists.
|
|
7510
|
+
...input.revisionOf ? { revisionOf: input.revisionOf } : {}
|
|
7190
7511
|
};
|
|
7191
7512
|
const { data: created, error: rpcErr } = await core.rpc("agenda_booking_create", {
|
|
7192
7513
|
p_starts_at: input.startsAt,
|
|
@@ -7394,7 +7715,7 @@ function createSupabaseAgendaProvider(providerOptions) {
|
|
|
7394
7715
|
if (data.professionalId) updates.assignee_id = data.professionalId;
|
|
7395
7716
|
if (data.clientId) updates.party_id = data.clientId;
|
|
7396
7717
|
if (data.locationId) updates.location_id = data.locationId;
|
|
7397
|
-
if (data.services) {
|
|
7718
|
+
if (data.services && !data.endsAt) {
|
|
7398
7719
|
const totalDuration = data.services.reduce((s, svc) => s + svc.durationMinutes, 0);
|
|
7399
7720
|
data.services.reduce((s, svc) => s + svc.price, 0);
|
|
7400
7721
|
const startsAt = data.startsAt ?? existing?.starts_at;
|
|
@@ -7718,6 +8039,48 @@ function createSupabaseAgendaProvider(providerOptions) {
|
|
|
7718
8039
|
withinDeadline: r.within_deadline ?? null
|
|
7719
8040
|
}));
|
|
7720
8041
|
},
|
|
8042
|
+
async getPrepaidItems(personId) {
|
|
8043
|
+
const { core } = getClients();
|
|
8044
|
+
const tenantId = getTenantId();
|
|
8045
|
+
if (!tenantId) return { open: [], scheduledCount: 0 };
|
|
8046
|
+
const { data, error } = await core.from("plg_clinical_sessions").select("id, service_id, booking_id, status").eq("tenant_id", tenantId).eq("party_id", personId).in("status", ["contracted", "pending", "scheduled"]).not("service_id", "is", null).limit(200);
|
|
8047
|
+
if (error) throw error;
|
|
8048
|
+
const rows = data ?? [];
|
|
8049
|
+
const loose = rows.filter((r) => !r.booking_id && r.status !== "scheduled");
|
|
8050
|
+
const booked = rows.filter((r) => r.booking_id);
|
|
8051
|
+
let scheduledCount = 0;
|
|
8052
|
+
if (booked.length) {
|
|
8053
|
+
const { data: future } = await core.from("appointments").select("id").in("id", [...new Set(booked.map((r) => r.booking_id))]).gte("starts_at", (/* @__PURE__ */ new Date()).toISOString()).not("status", "in", "(cancelled,no_show,completed)");
|
|
8054
|
+
const ids = new Set((future ?? []).map((a) => a.id));
|
|
8055
|
+
scheduledCount = booked.filter((r) => ids.has(r.booking_id)).length;
|
|
8056
|
+
}
|
|
8057
|
+
const svcIds = [...new Set(loose.map((r) => r.service_id))];
|
|
8058
|
+
const { data: svcs } = svcIds.length ? await core.from("products").select("id, name, duration_minutes").in("id", svcIds) : { data: [] };
|
|
8059
|
+
const byId = new Map((svcs ?? []).map((p) => [p.id, p]));
|
|
8060
|
+
return {
|
|
8061
|
+
open: loose.map((r) => ({
|
|
8062
|
+
sessionId: r.id,
|
|
8063
|
+
serviceId: r.service_id,
|
|
8064
|
+
serviceName: byId.get(r.service_id)?.name ?? "Servi\xE7o",
|
|
8065
|
+
durationMinutes: Number(byId.get(r.service_id)?.duration_minutes) || 30
|
|
8066
|
+
})),
|
|
8067
|
+
scheduledCount
|
|
8068
|
+
};
|
|
8069
|
+
},
|
|
8070
|
+
async getRevisionCandidatesForPerson(personId) {
|
|
8071
|
+
const { core } = getClients();
|
|
8072
|
+
const { data, error } = await core.rpc("clinical_revision_candidates_for_person", { p_person: personId });
|
|
8073
|
+
if (error) throw error;
|
|
8074
|
+
return (data ?? []).map((r) => ({
|
|
8075
|
+
sessionId: r.session_id,
|
|
8076
|
+
serviceName: r.service_name ?? null,
|
|
8077
|
+
executedOn: r.executed_on ?? null,
|
|
8078
|
+
deadlineDays: r.deadline_days ?? null,
|
|
8079
|
+
daysSince: r.days_since ?? null,
|
|
8080
|
+
withinDeadline: r.within_deadline ?? null,
|
|
8081
|
+
nextRevisionNumber: Number(r.next_revision_number ?? 1)
|
|
8082
|
+
}));
|
|
8083
|
+
},
|
|
7721
8084
|
async linkRevision(sessionId, parentSessionId) {
|
|
7722
8085
|
const { core } = getClients();
|
|
7723
8086
|
const { data, error } = await core.rpc("clinical_link_revision", {
|
|
@@ -7744,12 +8107,13 @@ function createSupabaseAgendaProvider(providerOptions) {
|
|
|
7744
8107
|
if (error) throw error;
|
|
7745
8108
|
const linhas = data ?? [];
|
|
7746
8109
|
const ids = [...new Set(linhas.map((l) => l.service_id).filter(Boolean))];
|
|
7747
|
-
const nomes = ids.length ? await core.from("products").select("id, name").in("id", ids).then((r) => r.data ?? []) : [];
|
|
7748
|
-
const m = new Map(nomes.map((p) => [p.id, p
|
|
8110
|
+
const nomes = ids.length ? await core.from("products").select("id, name, service_kind").in("id", ids).then((r) => r.data ?? []) : [];
|
|
8111
|
+
const m = new Map(nomes.map((p) => [p.id, p]));
|
|
7749
8112
|
return linhas.map((l) => ({
|
|
7750
8113
|
sessionId: l.id,
|
|
7751
8114
|
serviceId: l.service_id ?? null,
|
|
7752
|
-
serviceName: m.get(l.service_id) ?? null,
|
|
8115
|
+
serviceName: m.get(l.service_id)?.name ?? null,
|
|
8116
|
+
serviceKind: m.get(l.service_id)?.service_kind ?? null,
|
|
7753
8117
|
status: l.status
|
|
7754
8118
|
}));
|
|
7755
8119
|
},
|
|
@@ -7847,6 +8211,16 @@ function createSupabaseAgendaProvider(providerOptions) {
|
|
|
7847
8211
|
if (error) throw error;
|
|
7848
8212
|
return Object.fromEntries((data ?? []).map((p) => [p.id, p.service_kind ?? "standard"]));
|
|
7849
8213
|
},
|
|
8214
|
+
async getServiceAppearance(serviceIds) {
|
|
8215
|
+
if (serviceIds.length === 0) return {};
|
|
8216
|
+
const { core } = getClients();
|
|
8217
|
+
const { data, error } = await core.from("products").select("id, service_kind, metadata").in("id", serviceIds);
|
|
8218
|
+
if (error) throw error;
|
|
8219
|
+
return Object.fromEntries((data ?? []).map((p) => {
|
|
8220
|
+
const c = p.metadata?.catalog?.color ?? p.metadata?.color;
|
|
8221
|
+
return [p.id, { kind: p.service_kind ?? "standard", color: typeof c === "string" && /^#[0-9a-f]{3,8}$/i.test(c) ? c : null }];
|
|
8222
|
+
}));
|
|
8223
|
+
},
|
|
7850
8224
|
async refreshAttendanceRuler(bookingId) {
|
|
7851
8225
|
const { core } = getClients();
|
|
7852
8226
|
const { error } = await core.rpc("clinical_requirements_refresh", { p_booking: bookingId });
|
|
@@ -8678,6 +9052,8 @@ var AGENDA_TOGGLE_DEFAULTS = {
|
|
|
8678
9052
|
// padrão desligado esconderia a função de quem não sabe que ela existe, que
|
|
8679
9053
|
// é justamente quem precisaria achá-la. Quem não a quer desliga aqui.
|
|
8680
9054
|
recurringBookings: true,
|
|
9055
|
+
// Off: most houses read the day by status. A clinic reads it by kind of visit (FAY-1613).
|
|
9056
|
+
cardColorByServiceKind: false,
|
|
8681
9057
|
enableLocation: false,
|
|
8682
9058
|
enableWorkingHours: true,
|
|
8683
9059
|
blockOutside: false,
|
|
@@ -8714,6 +9090,7 @@ function AgendaGeneralSettings() {
|
|
|
8714
9090
|
/* @__PURE__ */ jsx(ToggleRow, { label: t2("agenda.settings.reminder"), description: t2("agenda.settings.reminderDesc"), checked: s.get("reminder"), onChange: (v) => s.set("reminder", v) })
|
|
8715
9091
|
] }),
|
|
8716
9092
|
/* @__PURE__ */ jsxs(SettingsGroup, { title: t2("agenda.settings.calendarDisplay"), description: t2("agenda.settings.calendarDisplayDesc"), children: [
|
|
9093
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: t2("agenda.settings.cardColorByServiceKind"), description: t2("agenda.settings.cardColorByServiceKindDesc"), checked: s.get("cardColorByServiceKind"), onChange: (v) => s.set("cardColorByServiceKind", v) }),
|
|
8717
9094
|
/* @__PURE__ */ jsx(
|
|
8718
9095
|
SelectRow,
|
|
8719
9096
|
{
|
|
@@ -8765,8 +9142,25 @@ var KIND_ICON = {
|
|
|
8765
9142
|
};
|
|
8766
9143
|
function requirementLabel(r) {
|
|
8767
9144
|
if (r.kind === "form") return r.refName ?? KIND_LABEL.form;
|
|
9145
|
+
if ((r.kind === "contract" || r.kind === "session_term") && r.refName) return r.refName;
|
|
8768
9146
|
return KIND_LABEL[r.kind] ?? r.kind;
|
|
8769
9147
|
}
|
|
9148
|
+
function contratoComoBola(c) {
|
|
9149
|
+
return {
|
|
9150
|
+
id: `contract:${c.id}`,
|
|
9151
|
+
sessionId: null,
|
|
9152
|
+
serviceId: null,
|
|
9153
|
+
serviceName: null,
|
|
9154
|
+
kind: c.isSessionTerm ? "session_term" : "contract",
|
|
9155
|
+
scope: "attendance",
|
|
9156
|
+
status: c.status === "signed" ? "resolved" : "pending",
|
|
9157
|
+
refId: c.id,
|
|
9158
|
+
refName: c.templateName,
|
|
9159
|
+
blocksStart: false,
|
|
9160
|
+
blocksFinish: c.blocksFinish,
|
|
9161
|
+
validUntil: null
|
|
9162
|
+
};
|
|
9163
|
+
}
|
|
8770
9164
|
function Bola({ r, onClick }) {
|
|
8771
9165
|
const Icon = KIND_ICON[r.kind] ?? CircleDashed;
|
|
8772
9166
|
const done = r.status === "resolved";
|
|
@@ -8817,16 +9211,16 @@ function TarefaPane({ titulo, aoVoltar, children }) {
|
|
|
8817
9211
|
}
|
|
8818
9212
|
function AttendanceRuler({ booking, open, onClose, onFinished, frame = "modal" }) {
|
|
8819
9213
|
const provider2 = useAgendaProvider();
|
|
8820
|
-
const [versao, setVersao] =
|
|
8821
|
-
const [statusLocal, setStatusLocal] =
|
|
8822
|
-
|
|
9214
|
+
const [versao, setVersao] = React23.useState(0);
|
|
9215
|
+
const [statusLocal, setStatusLocal] = React23.useState(booking.status);
|
|
9216
|
+
React23.useEffect(() => {
|
|
8823
9217
|
setStatusLocal(booking.status);
|
|
8824
9218
|
}, [booking.id, booking.status]);
|
|
8825
9219
|
const emCurso = statusLocal === "in_progress";
|
|
8826
9220
|
const naoIniciado = statusLocal === "scheduled" || statusLocal === "confirmed" || statusLocal === "waiting";
|
|
8827
9221
|
const somenteLeitura = !emCurso;
|
|
8828
|
-
const [eu, setEu] =
|
|
8829
|
-
|
|
9222
|
+
const [eu, setEu] = React23.useState(void 0);
|
|
9223
|
+
React23.useEffect(() => {
|
|
8830
9224
|
if (!naoIniciado) return;
|
|
8831
9225
|
let vivo = true;
|
|
8832
9226
|
(async () => {
|
|
@@ -8837,31 +9231,34 @@ function AttendanceRuler({ booking, open, onClose, onFinished, frame = "modal" }
|
|
|
8837
9231
|
vivo = false;
|
|
8838
9232
|
};
|
|
8839
9233
|
}, [naoIniciado, provider2]);
|
|
8840
|
-
const [iniciando, setIniciando] =
|
|
8841
|
-
const [recusaInicio, setRecusaInicio] =
|
|
8842
|
-
const [rows, setRows] =
|
|
8843
|
-
const [record, setRecord] =
|
|
8844
|
-
const [erro, setErro] =
|
|
8845
|
-
const [contratando, setContratando] =
|
|
8846
|
-
const [servicos, setServicos] =
|
|
8847
|
-
const [marcados, setMarcados] =
|
|
8848
|
-
const [salvando, setSalvando] =
|
|
8849
|
-
const [confirmando, setConfirmando] =
|
|
8850
|
-
const [encerrando, setEncerrando] =
|
|
8851
|
-
const [avisoFinal, setAvisoFinal] =
|
|
8852
|
-
const [
|
|
8853
|
-
const [
|
|
8854
|
-
const
|
|
8855
|
-
const [
|
|
8856
|
-
const [
|
|
8857
|
-
const [
|
|
8858
|
-
const [
|
|
8859
|
-
const [
|
|
8860
|
-
const [
|
|
8861
|
-
const [
|
|
8862
|
-
const [
|
|
8863
|
-
const [
|
|
8864
|
-
|
|
9234
|
+
const [iniciando, setIniciando] = React23.useState(false);
|
|
9235
|
+
const [recusaInicio, setRecusaInicio] = React23.useState(null);
|
|
9236
|
+
const [rows, setRows] = React23.useState(null);
|
|
9237
|
+
const [record, setRecord] = React23.useState(null);
|
|
9238
|
+
const [erro, setErro] = React23.useState(null);
|
|
9239
|
+
const [contratando, setContratando] = React23.useState(false);
|
|
9240
|
+
const [servicos, setServicos] = React23.useState([]);
|
|
9241
|
+
const [marcados, setMarcados] = React23.useState({});
|
|
9242
|
+
const [salvando, setSalvando] = React23.useState(false);
|
|
9243
|
+
const [confirmando, setConfirmando] = React23.useState(null);
|
|
9244
|
+
const [encerrando, setEncerrando] = React23.useState(false);
|
|
9245
|
+
const [avisoFinal, setAvisoFinal] = React23.useState(null);
|
|
9246
|
+
const [contratos, setContratos] = React23.useState([]);
|
|
9247
|
+
const [assinando, setAssinando] = React23.useState(null);
|
|
9248
|
+
const filaAssinatura = React23.useRef({ ids: [], depois: null });
|
|
9249
|
+
const [preenchendo, setPreenchendo] = React23.useState(null);
|
|
9250
|
+
const [avaliando, setAvaliando] = React23.useState(false);
|
|
9251
|
+
const [lancando, setLancando] = React23.useState(null);
|
|
9252
|
+
const [executando, setExecutando] = React23.useState(null);
|
|
9253
|
+
const [vinculando, setVinculando] = React23.useState(null);
|
|
9254
|
+
const [candidatos, setCandidatos] = React23.useState(null);
|
|
9255
|
+
const [buscaInsumo, setBuscaInsumo] = React23.useState("");
|
|
9256
|
+
const [insumos, setInsumos] = React23.useState([]);
|
|
9257
|
+
const [usados, setUsados] = React23.useState([]);
|
|
9258
|
+
const [busca, setBusca] = React23.useState("");
|
|
9259
|
+
const [catalogo, setCatalogo] = React23.useState([]);
|
|
9260
|
+
const [indicados, setIndicados] = React23.useState([]);
|
|
9261
|
+
React23.useEffect(() => {
|
|
8865
9262
|
if (!open) return;
|
|
8866
9263
|
let vivo = true;
|
|
8867
9264
|
(async () => {
|
|
@@ -8872,12 +9269,23 @@ function AttendanceRuler({ booking, open, onClose, onFinished, frame = "modal" }
|
|
|
8872
9269
|
} catch {
|
|
8873
9270
|
}
|
|
8874
9271
|
}
|
|
8875
|
-
|
|
9272
|
+
if (emCurso && booking.clientId) {
|
|
9273
|
+
try {
|
|
9274
|
+
await garantirContratos([]);
|
|
9275
|
+
} catch {
|
|
9276
|
+
}
|
|
9277
|
+
}
|
|
9278
|
+
const [r, reg, cs] = await Promise.all([
|
|
8876
9279
|
provider2.getAttendanceRuler?.(booking.id),
|
|
8877
|
-
provider2.getAttendanceRecord?.(booking.id).catch(() => [])
|
|
9280
|
+
provider2.getAttendanceRecord?.(booking.id).catch(() => []),
|
|
9281
|
+
listAttendanceContracts(booking.id).catch(() => [])
|
|
8878
9282
|
]);
|
|
9283
|
+
if (vivo) setContratos(cs);
|
|
8879
9284
|
if (vivo) {
|
|
8880
|
-
setRows(
|
|
9285
|
+
setRows([
|
|
9286
|
+
...(r ?? []).filter((x) => !(x.kind === "form" && x.status === "not_applicable")),
|
|
9287
|
+
...cs.map(contratoComoBola)
|
|
9288
|
+
]);
|
|
8881
9289
|
setRecord(reg ?? []);
|
|
8882
9290
|
setErro(null);
|
|
8883
9291
|
}
|
|
@@ -8889,6 +9297,46 @@ function AttendanceRuler({ booking, open, onClose, onFinished, frame = "modal" }
|
|
|
8889
9297
|
vivo = false;
|
|
8890
9298
|
};
|
|
8891
9299
|
}, [open, booking.id, versao, emCurso]);
|
|
9300
|
+
const autoLinked = React23.useRef(false);
|
|
9301
|
+
React23.useEffect(() => {
|
|
9302
|
+
const escolhida = booking.metadata?.revisionOf?.sessionId;
|
|
9303
|
+
const pendente = rows?.find((r) => r.kind === "revision_link" && r.status === "pending" && r.sessionId);
|
|
9304
|
+
if (!emCurso || !escolhida || !pendente?.sessionId || autoLinked.current || !provider2.linkRevision) return;
|
|
9305
|
+
autoLinked.current = true;
|
|
9306
|
+
provider2.linkRevision(pendente.sessionId, escolhida).then((r) => {
|
|
9307
|
+
if (r.ok) setVersao((v) => v + 1);
|
|
9308
|
+
}).catch(() => {
|
|
9309
|
+
});
|
|
9310
|
+
}, [rows, emCurso, booking.metadata, provider2]);
|
|
9311
|
+
async function garantirContratos(contratados, indicados2 = []) {
|
|
9312
|
+
if (!booking.clientId) return [];
|
|
9313
|
+
const sess = await provider2.getAttendanceSessions?.(booking.id) ?? [];
|
|
9314
|
+
const daSessao = sess.filter((x) => x.serviceId && x.status !== "executed" && x.status !== "cancelled" && x.serviceKind !== "evaluation").map((x) => ({ serviceId: x.serviceId, name: x.serviceName ?? "Servi\xE7o", quantity: 1, unitPrice: 0 }));
|
|
9315
|
+
return ensureAttendanceContracts({
|
|
9316
|
+
bookingId: booking.id,
|
|
9317
|
+
personId: booking.clientId,
|
|
9318
|
+
unitId: booking.locationId ?? null,
|
|
9319
|
+
professionalId: booking.professionalId ?? null,
|
|
9320
|
+
contracted: contratados,
|
|
9321
|
+
indicated: indicados2,
|
|
9322
|
+
sessionServices: daSessao
|
|
9323
|
+
});
|
|
9324
|
+
}
|
|
9325
|
+
async function proximaAssinatura(lista) {
|
|
9326
|
+
const fila = filaAssinatura.current;
|
|
9327
|
+
while (fila.ids.length) {
|
|
9328
|
+
const c = lista.find((x) => x.id === fila.ids[0] && x.status === "pending");
|
|
9329
|
+
fila.ids.shift();
|
|
9330
|
+
if (c) {
|
|
9331
|
+
setAssinando(c);
|
|
9332
|
+
return;
|
|
9333
|
+
}
|
|
9334
|
+
}
|
|
9335
|
+
setAssinando(null);
|
|
9336
|
+
const depois = fila.depois;
|
|
9337
|
+
fila.depois = null;
|
|
9338
|
+
if (depois) await depois();
|
|
9339
|
+
}
|
|
8892
9340
|
async function abrirContratacao() {
|
|
8893
9341
|
const cands = await provider2.getContractCandidates?.(booking.id) ?? [];
|
|
8894
9342
|
const itens = cands.map((c) => ({ id: c.serviceId, ...c }));
|
|
@@ -8908,7 +9356,17 @@ function AttendanceRuler({ booking, open, onClose, onFinished, frame = "modal" }
|
|
|
8908
9356
|
}
|
|
8909
9357
|
setContratando(false);
|
|
8910
9358
|
setVersao((v) => v + 1);
|
|
8911
|
-
|
|
9359
|
+
const indicados2 = servicos.filter((sv) => sv.fromIndication && sv.serviceId).map((sv) => ({ serviceId: sv.serviceId, name: sv.name, quantity: sv.quantity, unitPrice: sv.price }));
|
|
9360
|
+
let lista = [];
|
|
9361
|
+
try {
|
|
9362
|
+
lista = await garantirContratos(linhas, indicados2);
|
|
9363
|
+
} catch (e) {
|
|
9364
|
+
setErro(e instanceof Error ? `Contratado, mas o contrato n\xE3o foi gerado: ${e.message}` : "Contratado, mas o contrato n\xE3o foi gerado.");
|
|
9365
|
+
}
|
|
9366
|
+
setContratos(lista);
|
|
9367
|
+
const pendentes = lista.filter((c) => c.status === "pending").sort((a, b) => Number(a.isSessionTerm) - Number(b.isSessionTerm));
|
|
9368
|
+
filaAssinatura.current = { ids: pendentes.map((c) => c.id), depois: abrirExecucao };
|
|
9369
|
+
await proximaAssinatura(lista);
|
|
8912
9370
|
} finally {
|
|
8913
9371
|
setSalvando(false);
|
|
8914
9372
|
}
|
|
@@ -8918,7 +9376,7 @@ function AttendanceRuler({ booking, open, onClose, onFinished, frame = "modal" }
|
|
|
8918
9376
|
const d = ORDEM.indexOf(a.kind) - ORDEM.indexOf(b.kind);
|
|
8919
9377
|
return d !== 0 ? d : a.id.localeCompare(b.id);
|
|
8920
9378
|
};
|
|
8921
|
-
const doAtendimento =
|
|
9379
|
+
const doAtendimento = React23.useMemo(() => {
|
|
8922
9380
|
const m = /* @__PURE__ */ new Map();
|
|
8923
9381
|
for (const r of (rows ?? []).filter((x) => x.scope === "attendance")) {
|
|
8924
9382
|
const k = `${r.kind}:${r.refId ?? ""}`;
|
|
@@ -8931,7 +9389,7 @@ function AttendanceRuler({ booking, open, onClose, onFinished, frame = "modal" }
|
|
|
8931
9389
|
}
|
|
8932
9390
|
return [...m.values()].sort(porOrdem);
|
|
8933
9391
|
}, [rows]);
|
|
8934
|
-
const porServico =
|
|
9392
|
+
const porServico = React23.useMemo(() => {
|
|
8935
9393
|
const m = /* @__PURE__ */ new Map();
|
|
8936
9394
|
for (const r of rows ?? []) {
|
|
8937
9395
|
if (r.scope === "attendance") continue;
|
|
@@ -8955,8 +9413,8 @@ function AttendanceRuler({ booking, open, onClose, onFinished, frame = "modal" }
|
|
|
8955
9413
|
return grupos;
|
|
8956
9414
|
}, [rows]);
|
|
8957
9415
|
const faltaParaTerminar = (rows ?? []).filter((r) => r.blocksFinish && r.status === "pending");
|
|
8958
|
-
const tarefaAberta = Boolean(vinculando || executando || lancando || avaliando || confirmando || contratando || preenchendo?.refId && booking.clientId);
|
|
8959
|
-
|
|
9416
|
+
const tarefaAberta = Boolean(assinando || vinculando || executando || lancando || avaliando || confirmando || contratando || preenchendo?.refId && booking.clientId);
|
|
9417
|
+
React23.useEffect(() => {
|
|
8960
9418
|
if (!avaliando) return;
|
|
8961
9419
|
let vivo = true;
|
|
8962
9420
|
const t2 = setTimeout(async () => {
|
|
@@ -8972,7 +9430,7 @@ function AttendanceRuler({ booking, open, onClose, onFinished, frame = "modal" }
|
|
|
8972
9430
|
clearTimeout(t2);
|
|
8973
9431
|
};
|
|
8974
9432
|
}, [avaliando, busca]);
|
|
8975
|
-
|
|
9433
|
+
React23.useEffect(() => {
|
|
8976
9434
|
if (!lancando) return;
|
|
8977
9435
|
let vivo = true;
|
|
8978
9436
|
const t2 = setTimeout(async () => {
|
|
@@ -8996,7 +9454,7 @@ function AttendanceRuler({ booking, open, onClose, onFinished, frame = "modal" }
|
|
|
8996
9454
|
return [...xs, ...kit.filter((k) => !tem.has(k.productId)).map((k) => ({ productId: k.productId, name: k.name, qtd: k.quantity, unitId: k.unitId }))];
|
|
8997
9455
|
});
|
|
8998
9456
|
}
|
|
8999
|
-
|
|
9457
|
+
React23.useEffect(() => {
|
|
9000
9458
|
if (!vinculando) return;
|
|
9001
9459
|
let vivo = true;
|
|
9002
9460
|
(async () => {
|
|
@@ -9069,6 +9527,39 @@ function AttendanceRuler({ booking, open, onClose, onFinished, frame = "modal" }
|
|
|
9069
9527
|
setSalvando(false);
|
|
9070
9528
|
}
|
|
9071
9529
|
}
|
|
9530
|
+
const [atalhosPrepago, setAtalhosPrepago] = React23.useState([]);
|
|
9531
|
+
React23.useEffect(() => {
|
|
9532
|
+
if (!preenchendo || !booking.clientId || !provider2.getPrepaidItems) {
|
|
9533
|
+
setAtalhosPrepago([]);
|
|
9534
|
+
return;
|
|
9535
|
+
}
|
|
9536
|
+
let vivo = true;
|
|
9537
|
+
provider2.getPrepaidItems(booking.clientId).then((r) => {
|
|
9538
|
+
if (!vivo) return;
|
|
9539
|
+
const vistos = /* @__PURE__ */ new Set();
|
|
9540
|
+
setAtalhosPrepago(r.open.filter((o) => !vistos.has(o.serviceId) && vistos.add(o.serviceId)).map((o) => ({ serviceId: o.serviceId, name: o.serviceName, unitPrice: 0, hint: "j\xE1 pago" })));
|
|
9541
|
+
}).catch(() => {
|
|
9542
|
+
if (vivo) setAtalhosPrepago([]);
|
|
9543
|
+
});
|
|
9544
|
+
return () => {
|
|
9545
|
+
vivo = false;
|
|
9546
|
+
};
|
|
9547
|
+
}, [preenchendo, booking.clientId, provider2]);
|
|
9548
|
+
async function salvouFormulario(r) {
|
|
9549
|
+
setVersao((v) => v + 1);
|
|
9550
|
+
if (!r.recommendations.length || !provider2.completeEvaluation) return;
|
|
9551
|
+
const res = await provider2.completeEvaluation(
|
|
9552
|
+
booking.id,
|
|
9553
|
+
r.recommendations.map((l) => ({ serviceId: l.serviceId, quantity: l.quantity }))
|
|
9554
|
+
);
|
|
9555
|
+
if (!res?.ok) {
|
|
9556
|
+
setErro(RECUSA_CONTRATO[res?.reason ?? ""] ?? "A avalia\xE7\xE3o foi salva, mas as indica\xE7\xF5es n\xE3o foram registradas.");
|
|
9557
|
+
return;
|
|
9558
|
+
}
|
|
9559
|
+
setPreenchendo(null);
|
|
9560
|
+
setVersao((v) => v + 1);
|
|
9561
|
+
await abrirContratacao();
|
|
9562
|
+
}
|
|
9072
9563
|
async function concluirAvaliacao() {
|
|
9073
9564
|
setSalvando(true);
|
|
9074
9565
|
try {
|
|
@@ -9362,6 +9853,27 @@ function AttendanceRuler({ booking, open, onClose, onFinished, frame = "modal" }
|
|
|
9362
9853
|
] })
|
|
9363
9854
|
] })
|
|
9364
9855
|
] }),
|
|
9856
|
+
assinando && /* @__PURE__ */ jsx(TarefaPane, { titulo: /* @__PURE__ */ jsx(Fragment, { children: assinando.templateName }), aoVoltar: () => {
|
|
9857
|
+
filaAssinatura.current = { ids: [], depois: null };
|
|
9858
|
+
setAssinando(null);
|
|
9859
|
+
}, children: /* @__PURE__ */ jsx(
|
|
9860
|
+
ContractSignPane,
|
|
9861
|
+
{
|
|
9862
|
+
contract: assinando,
|
|
9863
|
+
defaultSignerName: booking.clientName,
|
|
9864
|
+
onSigned: async () => {
|
|
9865
|
+
setVersao((v) => v + 1);
|
|
9866
|
+
const lista = await listAttendanceContracts(booking.id).catch(() => contratos);
|
|
9867
|
+
setContratos(lista);
|
|
9868
|
+
await proximaAssinatura(lista);
|
|
9869
|
+
},
|
|
9870
|
+
onClose: () => {
|
|
9871
|
+
filaAssinatura.current = { ids: [], depois: null };
|
|
9872
|
+
setAssinando(null);
|
|
9873
|
+
}
|
|
9874
|
+
},
|
|
9875
|
+
assinando.id
|
|
9876
|
+
) }),
|
|
9365
9877
|
preenchendo?.refId && booking.clientId && /* @__PURE__ */ jsx(
|
|
9366
9878
|
TarefaPane,
|
|
9367
9879
|
{
|
|
@@ -9374,7 +9886,11 @@ function AttendanceRuler({ booking, open, onClose, onFinished, frame = "modal" }
|
|
|
9374
9886
|
templateId: preenchendo.refId,
|
|
9375
9887
|
personId: booking.clientId,
|
|
9376
9888
|
personName: booking.clientName,
|
|
9377
|
-
|
|
9889
|
+
subject: { type: "appointment", id: booking.id },
|
|
9890
|
+
productId: preenchendo.scope === "service" ? preenchendo.serviceId : null,
|
|
9891
|
+
professionalId: booking.professionalId ?? null,
|
|
9892
|
+
recommendationShortcuts: atalhosPrepago,
|
|
9893
|
+
onSaved: salvouFormulario,
|
|
9378
9894
|
onClose: () => setPreenchendo(null)
|
|
9379
9895
|
}
|
|
9380
9896
|
)
|
|
@@ -9524,7 +10040,10 @@ function AttendanceRuler({ booking, open, onClose, onFinished, frame = "modal" }
|
|
|
9524
10040
|
Bola,
|
|
9525
10041
|
{
|
|
9526
10042
|
r,
|
|
9527
|
-
onClick: r.kind === "form" && r.refId && r.status === "pending" && booking.clientId ? () => setPreenchendo(r) :
|
|
10043
|
+
onClick: r.kind === "form" && r.refId && r.status === "pending" && booking.clientId ? () => setPreenchendo(r) : (r.kind === "contract" || r.kind === "session_term") && r.refId ? () => {
|
|
10044
|
+
filaAssinatura.current = { ids: [], depois: null };
|
|
10045
|
+
setAssinando(contratos.find((c) => c.id === r.refId) ?? null);
|
|
10046
|
+
} : void 0
|
|
9528
10047
|
},
|
|
9529
10048
|
r.id
|
|
9530
10049
|
)) })
|
|
@@ -9712,10 +10231,10 @@ var STATUS_LABEL = {
|
|
|
9712
10231
|
};
|
|
9713
10232
|
function PersonAttendancesWidget({ personId }) {
|
|
9714
10233
|
const provider2 = useAgendaProvider();
|
|
9715
|
-
const [linhas, setLinhas] =
|
|
9716
|
-
const [aberto, setAberto] =
|
|
9717
|
-
const [emCurso, setEmCurso] =
|
|
9718
|
-
const recarregar =
|
|
10234
|
+
const [linhas, setLinhas] = React23.useState(null);
|
|
10235
|
+
const [aberto, setAberto] = React23.useState(null);
|
|
10236
|
+
const [emCurso, setEmCurso] = React23.useState(null);
|
|
10237
|
+
const recarregar = React23.useCallback(async () => {
|
|
9719
10238
|
try {
|
|
9720
10239
|
const r = await provider2.getPersonAttendances?.(personId);
|
|
9721
10240
|
setLinhas(r ?? []);
|
|
@@ -9723,10 +10242,10 @@ function PersonAttendancesWidget({ personId }) {
|
|
|
9723
10242
|
setLinhas([]);
|
|
9724
10243
|
}
|
|
9725
10244
|
}, [personId, provider2]);
|
|
9726
|
-
|
|
10245
|
+
React23.useEffect(() => {
|
|
9727
10246
|
void recarregar();
|
|
9728
10247
|
}, [recarregar]);
|
|
9729
|
-
|
|
10248
|
+
React23.useEffect(() => {
|
|
9730
10249
|
let vivo = true;
|
|
9731
10250
|
(async () => {
|
|
9732
10251
|
const eu = await provider2.getCurrentPersonId?.() ?? null;
|
|
@@ -9822,10 +10341,124 @@ function PersonAttendancesWidget({ personId }) {
|
|
|
9822
10341
|
] })
|
|
9823
10342
|
] });
|
|
9824
10343
|
}
|
|
10344
|
+
var OPEN = ["scheduled", "confirmed", "waiting", "in_progress"];
|
|
10345
|
+
var REFUSAL = {
|
|
10346
|
+
sem_ficha_de_profissional: "Este login n\xE3o tem ficha de profissional. S\xF3 quem atende inicia o atendimento.",
|
|
10347
|
+
nao_e_seu_atendimento: "Este atendimento \xE9 de outro profissional.",
|
|
10348
|
+
nao_e_de_hoje: "S\xF3 d\xE1 para iniciar um atendimento de hoje.",
|
|
10349
|
+
ja_encerrado: "Este atendimento j\xE1 foi encerrado.",
|
|
10350
|
+
not_found: "Este agendamento n\xE3o existe mais."
|
|
10351
|
+
};
|
|
10352
|
+
function todayRange2() {
|
|
10353
|
+
const start = /* @__PURE__ */ new Date();
|
|
10354
|
+
start.setHours(0, 0, 0, 0);
|
|
10355
|
+
const end = /* @__PURE__ */ new Date();
|
|
10356
|
+
end.setHours(23, 59, 59, 999);
|
|
10357
|
+
return { start: start.toISOString(), end: end.toISOString() };
|
|
10358
|
+
}
|
|
10359
|
+
var hhmm = (iso) => new Date(iso).toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" });
|
|
10360
|
+
function MyClientsTodayWidget() {
|
|
10361
|
+
const provider2 = useAgendaProvider();
|
|
10362
|
+
const config = useAgendaConfig();
|
|
10363
|
+
const [me, setMe] = React23.useState(void 0);
|
|
10364
|
+
const [rows, setRows] = React23.useState(null);
|
|
10365
|
+
const [busy, setBusy] = React23.useState(null);
|
|
10366
|
+
const [refusal, setRefusal] = React23.useState(null);
|
|
10367
|
+
const load = React23.useCallback(async (personId) => {
|
|
10368
|
+
const r = await provider2.getBookings({ dateRange: todayRange2(), professionalIds: [personId], statuses: OPEN });
|
|
10369
|
+
setRows(r.filter((b) => b.clientId).sort((a, b) => a.startsAt.localeCompare(b.startsAt)));
|
|
10370
|
+
}, [provider2]);
|
|
10371
|
+
React23.useEffect(() => {
|
|
10372
|
+
let alive = true;
|
|
10373
|
+
Promise.resolve(provider2.getCurrentPersonId?.() ?? null).then((id) => {
|
|
10374
|
+
if (alive) setMe(id ?? null);
|
|
10375
|
+
}).catch(() => {
|
|
10376
|
+
if (alive) setMe(null);
|
|
10377
|
+
});
|
|
10378
|
+
return () => {
|
|
10379
|
+
alive = false;
|
|
10380
|
+
};
|
|
10381
|
+
}, [provider2]);
|
|
10382
|
+
const [isPro, setIsPro] = React23.useState(void 0);
|
|
10383
|
+
React23.useEffect(() => {
|
|
10384
|
+
if (!me) return;
|
|
10385
|
+
let alive = true;
|
|
10386
|
+
provider2.getProfessionals().then((ps) => {
|
|
10387
|
+
if (alive) setIsPro(ps.some((p) => p.id === me));
|
|
10388
|
+
}).catch(() => {
|
|
10389
|
+
if (alive) setIsPro(false);
|
|
10390
|
+
});
|
|
10391
|
+
return () => {
|
|
10392
|
+
alive = false;
|
|
10393
|
+
};
|
|
10394
|
+
}, [me, provider2]);
|
|
10395
|
+
React23.useEffect(() => {
|
|
10396
|
+
if (!me || !isPro) return;
|
|
10397
|
+
load(me).catch(() => setRows([]));
|
|
10398
|
+
}, [me, isPro, load]);
|
|
10399
|
+
if (me === null || isPro === false) return null;
|
|
10400
|
+
const statusLabel = (v) => config.statuses.find((s) => s.value === v)?.label ?? "";
|
|
10401
|
+
async function start(b) {
|
|
10402
|
+
setRefusal(null);
|
|
10403
|
+
if (b.status === "in_progress") {
|
|
10404
|
+
openEntity(b.clientId, "person", "customer", "attendance");
|
|
10405
|
+
return;
|
|
10406
|
+
}
|
|
10407
|
+
setBusy(b.id);
|
|
10408
|
+
try {
|
|
10409
|
+
const r = await provider2.startAttendance?.(b.id);
|
|
10410
|
+
if (!r?.ok) {
|
|
10411
|
+
setRefusal(REFUSAL[r?.reason ?? ""] ?? "N\xE3o foi poss\xEDvel iniciar o atendimento.");
|
|
10412
|
+
return;
|
|
10413
|
+
}
|
|
10414
|
+
openEntity(b.clientId, "person", "customer", "attendance");
|
|
10415
|
+
} finally {
|
|
10416
|
+
setBusy(null);
|
|
10417
|
+
}
|
|
10418
|
+
}
|
|
10419
|
+
return /* @__PURE__ */ jsxs(Card, { children: [
|
|
10420
|
+
/* @__PURE__ */ jsx(CardHeader, { className: "pb-2", children: /* @__PURE__ */ jsx(CardTitle, { className: "text-sm", children: "Seus clientes de hoje" }) }),
|
|
10421
|
+
/* @__PURE__ */ jsxs(CardContent, { className: "space-y-1.5", children: [
|
|
10422
|
+
(me === void 0 || isPro === void 0 || rows === null) && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
10423
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-11 w-full" }),
|
|
10424
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-11 w-full" })
|
|
10425
|
+
] }),
|
|
10426
|
+
rows && rows.length === 0 && /* @__PURE__ */ jsx("p", { className: "py-2 text-xs text-muted-foreground", children: "Nenhum cliente marcado com voc\xEA hoje." }),
|
|
10427
|
+
refusal && /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", children: refusal }),
|
|
10428
|
+
rows?.map((b) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 rounded-lg border px-3 py-2", children: [
|
|
10429
|
+
/* @__PURE__ */ jsx("span", { className: "w-11 shrink-0 text-sm font-medium tabular-nums", children: hhmm(b.startsAt) }),
|
|
10430
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
10431
|
+
/* @__PURE__ */ jsx(PersonLink, { personId: b.clientId, name: b.clientName ?? "", className: "block truncate text-sm font-medium" }),
|
|
10432
|
+
/* @__PURE__ */ jsxs("p", { className: "truncate text-xs text-muted-foreground", children: [
|
|
10433
|
+
(b.services ?? []).map((s) => s.name).join(", ") || "Servi\xE7o n\xE3o informado",
|
|
10434
|
+
statusLabel(b.status) ? ` \xB7 ${statusLabel(b.status)}` : ""
|
|
10435
|
+
] })
|
|
10436
|
+
] }),
|
|
10437
|
+
/* @__PURE__ */ jsx(
|
|
10438
|
+
"button",
|
|
10439
|
+
{
|
|
10440
|
+
type: "button",
|
|
10441
|
+
disabled: busy === b.id,
|
|
10442
|
+
onClick: () => start(b),
|
|
10443
|
+
className: "inline-flex shrink-0 items-center gap-1.5 rounded-button bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50",
|
|
10444
|
+
children: b.status === "in_progress" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
10445
|
+
/* @__PURE__ */ jsx(ArrowRight, { className: "h-3.5 w-3.5" }),
|
|
10446
|
+
" Continuar"
|
|
10447
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
10448
|
+
/* @__PURE__ */ jsx(Play, { className: "h-3.5 w-3.5" }),
|
|
10449
|
+
" ",
|
|
10450
|
+
busy === b.id ? "Iniciando\u2026" : "Iniciar"
|
|
10451
|
+
] })
|
|
10452
|
+
}
|
|
10453
|
+
)
|
|
10454
|
+
] }, b.id))
|
|
10455
|
+
] })
|
|
10456
|
+
] });
|
|
10457
|
+
}
|
|
9825
10458
|
function PersonServicesWidget({ personId }) {
|
|
9826
10459
|
const provider2 = useAgendaProvider();
|
|
9827
|
-
const [rows, setRows] =
|
|
9828
|
-
|
|
10460
|
+
const [rows, setRows] = React23.useState(null);
|
|
10461
|
+
React23.useEffect(() => {
|
|
9829
10462
|
let alive = true;
|
|
9830
10463
|
(async () => {
|
|
9831
10464
|
try {
|
|
@@ -10148,6 +10781,9 @@ var en = {
|
|
|
10148
10781
|
"agenda.appointment.addClient": "Add client",
|
|
10149
10782
|
"agenda.appointment.addServiceFirst": "Add service",
|
|
10150
10783
|
"agenda.appointment.addNotes": "Add notes",
|
|
10784
|
+
"agenda.appointment.endTime": "End",
|
|
10785
|
+
"agenda.settings.cardColorByServiceKind": "Card color by service",
|
|
10786
|
+
"agenda.settings.cardColorByServiceKindDesc": "The card takes the colour set on the service; a revision is black and a service without colour blue. The status becomes the stripe on the left.",
|
|
10151
10787
|
"agenda.appointment.duration": "Duration (minutes)",
|
|
10152
10788
|
"agenda.appointment.price": "Price",
|
|
10153
10789
|
"agenda.appointment.title": "Title",
|
|
@@ -10457,6 +11093,9 @@ var ptBR = {
|
|
|
10457
11093
|
"agenda.appointment.addClient": "Adicionar cliente",
|
|
10458
11094
|
"agenda.appointment.addServiceFirst": "Adicionar servi\xE7o",
|
|
10459
11095
|
"agenda.appointment.addNotes": "Adicionar observa\xE7\xF5es",
|
|
11096
|
+
"agenda.appointment.endTime": "T\xE9rmino",
|
|
11097
|
+
"agenda.settings.cardColorByServiceKind": "Cor do card pelo servi\xE7o",
|
|
11098
|
+
"agenda.settings.cardColorByServiceKindDesc": "O card sai na cor cadastrada no servi\xE7o; revis\xE3o sai preta e servi\xE7o sem cor, azul. O status vira a faixa \xE0 esquerda.",
|
|
10460
11099
|
"agenda.appointment.duration": "Dura\xE7\xE3o (minutos)",
|
|
10461
11100
|
"agenda.appointment.price": "Pre\xE7o",
|
|
10462
11101
|
"agenda.appointment.title": "T\xEDtulo",
|
|
@@ -15334,9 +15973,9 @@ function failureMessage(error) {
|
|
|
15334
15973
|
}
|
|
15335
15974
|
function AvailabilityHeaderControl({ provider: provider2 }) {
|
|
15336
15975
|
const unitId = useActiveUnitId();
|
|
15337
|
-
const [status, setStatus] =
|
|
15338
|
-
const [busy, setBusy] =
|
|
15339
|
-
const refresh =
|
|
15976
|
+
const [status, setStatus] = React23.useState(null);
|
|
15977
|
+
const [busy, setBusy] = React23.useState(false);
|
|
15978
|
+
const refresh = React23.useCallback(async () => {
|
|
15340
15979
|
if (!unitId) {
|
|
15341
15980
|
setStatus(null);
|
|
15342
15981
|
return;
|
|
@@ -15351,7 +15990,7 @@ function AvailabilityHeaderControl({ provider: provider2 }) {
|
|
|
15351
15990
|
setStatus(null);
|
|
15352
15991
|
}
|
|
15353
15992
|
}, [provider2, unitId]);
|
|
15354
|
-
|
|
15993
|
+
React23.useEffect(() => {
|
|
15355
15994
|
void refresh();
|
|
15356
15995
|
return provider2.subscribe(() => {
|
|
15357
15996
|
void refresh();
|
|
@@ -15505,12 +16144,12 @@ function EmptyUnit() {
|
|
|
15505
16144
|
}
|
|
15506
16145
|
function TurnQueuePage({ provider: provider2, agendaProvider, title, subtitle }) {
|
|
15507
16146
|
const unitId = useActiveUnitId();
|
|
15508
|
-
const [agendaProfessionals, setAgendaProfessionals] =
|
|
15509
|
-
const [snapshot, setSnapshot] =
|
|
15510
|
-
const [loading, setLoading] =
|
|
15511
|
-
const [error, setError] =
|
|
15512
|
-
const [, tick] =
|
|
15513
|
-
const load =
|
|
16147
|
+
const [agendaProfessionals, setAgendaProfessionals] = React23.useState([]);
|
|
16148
|
+
const [snapshot, setSnapshot] = React23.useState(null);
|
|
16149
|
+
const [loading, setLoading] = React23.useState(false);
|
|
16150
|
+
const [error, setError] = React23.useState(null);
|
|
16151
|
+
const [, tick] = React23.useState(0);
|
|
16152
|
+
const load = React23.useCallback(async () => {
|
|
15514
16153
|
if (!unitId) {
|
|
15515
16154
|
setSnapshot(null);
|
|
15516
16155
|
return;
|
|
@@ -15525,10 +16164,10 @@ function TurnQueuePage({ provider: provider2, agendaProvider, title, subtitle })
|
|
|
15525
16164
|
setLoading(false);
|
|
15526
16165
|
}
|
|
15527
16166
|
}, [provider2, unitId]);
|
|
15528
|
-
|
|
16167
|
+
React23.useEffect(() => {
|
|
15529
16168
|
void load();
|
|
15530
16169
|
}, [load]);
|
|
15531
|
-
|
|
16170
|
+
React23.useEffect(() => {
|
|
15532
16171
|
if (!unitId) {
|
|
15533
16172
|
setAgendaProfessionals([]);
|
|
15534
16173
|
return;
|
|
@@ -15543,19 +16182,19 @@ function TurnQueuePage({ provider: provider2, agendaProvider, title, subtitle })
|
|
|
15543
16182
|
active = false;
|
|
15544
16183
|
};
|
|
15545
16184
|
}, [agendaProvider, unitId]);
|
|
15546
|
-
|
|
16185
|
+
React23.useEffect(() => provider2.subscribe(() => {
|
|
15547
16186
|
void load();
|
|
15548
16187
|
}), [provider2, load]);
|
|
15549
|
-
|
|
16188
|
+
React23.useEffect(() => {
|
|
15550
16189
|
const timer = window.setInterval(() => tick((value) => value + 1), 6e4);
|
|
15551
16190
|
return () => window.clearInterval(timer);
|
|
15552
16191
|
}, []);
|
|
15553
|
-
const rows =
|
|
16192
|
+
const rows = React23.useMemo(() => [...snapshot?.entries ?? []].sort((a, b) => {
|
|
15554
16193
|
const rank = { available: 0, serving: 1, unavailable: 2 };
|
|
15555
16194
|
return rank[a.status] - rank[b.status] || a.position - b.position;
|
|
15556
16195
|
}), [snapshot]);
|
|
15557
16196
|
const queueRows = rows.filter((row) => row.status !== "unavailable");
|
|
15558
|
-
const professionals =
|
|
16197
|
+
const professionals = React23.useMemo(() => {
|
|
15559
16198
|
const byId = /* @__PURE__ */ new Map();
|
|
15560
16199
|
for (const professional of agendaProfessionals) {
|
|
15561
16200
|
byId.set(professional.id, {
|
|
@@ -15578,7 +16217,7 @@ function TurnQueuePage({ provider: provider2, agendaProvider, title, subtitle })
|
|
|
15578
16217
|
}, [agendaProfessionals, snapshot]);
|
|
15579
16218
|
const available = professionals.filter((row) => row.status === "available").length;
|
|
15580
16219
|
const serving = professionals.filter((row) => row.status === "serving").length;
|
|
15581
|
-
const addWalkIn =
|
|
16220
|
+
const addWalkIn = React23.useCallback((professionalId) => {
|
|
15582
16221
|
if (!unitId) return;
|
|
15583
16222
|
window.location.hash = "/agenda";
|
|
15584
16223
|
window.setTimeout(() => {
|
|
@@ -15672,11 +16311,11 @@ function TurnQueuePage({ provider: provider2, agendaProvider, title, subtitle })
|
|
|
15672
16311
|
}
|
|
15673
16312
|
function UnitTurnQueueWidget({ item }) {
|
|
15674
16313
|
const unitId = item?.id;
|
|
15675
|
-
const [row, setRow] =
|
|
15676
|
-
const [loading, setLoading] =
|
|
15677
|
-
const [saving, setSaving] =
|
|
15678
|
-
const [radius, setRadius] =
|
|
15679
|
-
|
|
16314
|
+
const [row, setRow] = React23.useState(null);
|
|
16315
|
+
const [loading, setLoading] = React23.useState(true);
|
|
16316
|
+
const [saving, setSaving] = React23.useState(false);
|
|
16317
|
+
const [radius, setRadius] = React23.useState("80");
|
|
16318
|
+
React23.useEffect(() => {
|
|
15680
16319
|
if (!unitId) return;
|
|
15681
16320
|
let alive = true;
|
|
15682
16321
|
const supabase = getSupabaseClientOptional();
|
|
@@ -15781,8 +16420,8 @@ function createAgendaTurnQueuePlugin(options = {}) {
|
|
|
15781
16420
|
const menu = options.labels?.menu ?? "Pr\xF3ximo da vez";
|
|
15782
16421
|
const title = options.labels?.title ?? menu;
|
|
15783
16422
|
const subtitle = options.labels?.subtitle ?? "Ordem ao vivo dos profissionais dispon\xEDveis nesta unidade";
|
|
15784
|
-
const Page = (() =>
|
|
15785
|
-
const HeaderControl = (() =>
|
|
16423
|
+
const Page = (() => React23.createElement(TurnQueuePage, { provider: provider2, agendaProvider, title, subtitle }));
|
|
16424
|
+
const HeaderControl = (() => React23.createElement(AvailabilityHeaderControl, { provider: provider2 }));
|
|
15786
16425
|
return {
|
|
15787
16426
|
id: "agenda-turn-queue",
|
|
15788
16427
|
name: menu,
|
|
@@ -19144,30 +19783,36 @@ function createAgendaPlugin(options) {
|
|
|
19144
19783
|
}
|
|
19145
19784
|
}
|
|
19146
19785
|
]);
|
|
19147
|
-
const PageComponent = () =>
|
|
19786
|
+
const PageComponent = () => React23.createElement(AgendaPage, { config, provider: provider2, store });
|
|
19148
19787
|
const PersonAttendancesBound = (props) => (
|
|
19149
19788
|
// O provider do PLUGIN, sem a moldura da página: dentro de uma aba a
|
|
19150
19789
|
// `AgendaPage` traria a altura cheia e a rolagem própria do calendário, e a
|
|
19151
19790
|
// lista passaria a rolar dentro de uma caixa que já rola.
|
|
19152
|
-
|
|
19791
|
+
React23.createElement(
|
|
19153
19792
|
AgendaContextProvider,
|
|
19154
19793
|
{ config, provider: provider2, store },
|
|
19155
|
-
|
|
19794
|
+
React23.createElement(PersonAttendancesWidget, { personId: props.item?.id })
|
|
19156
19795
|
)
|
|
19157
19796
|
);
|
|
19158
19797
|
PersonAttendancesBound.displayName = "AgendaPersonAttendances";
|
|
19159
|
-
const PersonAttendanceBlockBound = (props) =>
|
|
19798
|
+
const PersonAttendanceBlockBound = (props) => React23.createElement(
|
|
19160
19799
|
AgendaContextProvider,
|
|
19161
19800
|
{ config, provider: provider2, store },
|
|
19162
|
-
|
|
19801
|
+
React23.createElement(PersonAttendanceBlock, { personId: props.item?.id })
|
|
19163
19802
|
);
|
|
19164
19803
|
PersonAttendanceBlockBound.displayName = "AgendaPersonAttendanceBlock";
|
|
19165
|
-
const PersonServicesBound = (props) =>
|
|
19804
|
+
const PersonServicesBound = (props) => React23.createElement(
|
|
19166
19805
|
AgendaContextProvider,
|
|
19167
19806
|
{ config, provider: provider2, store },
|
|
19168
|
-
|
|
19807
|
+
React23.createElement(PersonServicesWidget, { personId: props.item?.id })
|
|
19169
19808
|
);
|
|
19170
19809
|
PersonServicesBound.displayName = "AgendaPersonServices";
|
|
19810
|
+
const MyClientsTodayBound = () => React23.createElement(
|
|
19811
|
+
AgendaContextProvider,
|
|
19812
|
+
{ config, provider: provider2, store },
|
|
19813
|
+
React23.createElement(MyClientsTodayWidget)
|
|
19814
|
+
);
|
|
19815
|
+
MyClientsTodayBound.displayName = "AgendaMyClientsToday";
|
|
19171
19816
|
return {
|
|
19172
19817
|
id: "agenda",
|
|
19173
19818
|
defaultAgentRole: "frontdesk",
|
|
@@ -19321,10 +19966,10 @@ function createAgendaPlugin(options) {
|
|
|
19321
19966
|
},
|
|
19322
19967
|
{
|
|
19323
19968
|
path: "/agenda/confirmations",
|
|
19324
|
-
component: (() =>
|
|
19969
|
+
component: (() => React23.createElement(
|
|
19325
19970
|
AgendaPage,
|
|
19326
19971
|
{ config, provider: provider2, store },
|
|
19327
|
-
|
|
19972
|
+
React23.createElement(ConfirmationsView)
|
|
19328
19973
|
)),
|
|
19329
19974
|
fullBleed: true,
|
|
19330
19975
|
permission: { feature: "agenda", action: "read" },
|
|
@@ -19332,7 +19977,7 @@ function createAgendaPlugin(options) {
|
|
|
19332
19977
|
},
|
|
19333
19978
|
{
|
|
19334
19979
|
path: "/checkin",
|
|
19335
|
-
component: (() =>
|
|
19980
|
+
component: (() => React23.createElement(CheckinPage, {
|
|
19336
19981
|
config,
|
|
19337
19982
|
provider: provider2,
|
|
19338
19983
|
store,
|
|
@@ -19347,6 +19992,18 @@ function createAgendaPlugin(options) {
|
|
|
19347
19992
|
requiresFacet: "checkin"
|
|
19348
19993
|
}
|
|
19349
19994
|
],
|
|
19995
|
+
dashboardWidgets: [
|
|
19996
|
+
{
|
|
19997
|
+
id: "agenda.my-clients-today",
|
|
19998
|
+
title: "Seus clientes de hoje",
|
|
19999
|
+
kind: "custom",
|
|
20000
|
+
span: 2,
|
|
20001
|
+
surfaces: ["home"],
|
|
20002
|
+
defaultOrder: 0,
|
|
20003
|
+
component: MyClientsTodayBound,
|
|
20004
|
+
permission: { feature: "agenda", action: "read" }
|
|
20005
|
+
}
|
|
20006
|
+
],
|
|
19350
20007
|
widgets: [
|
|
19351
20008
|
{
|
|
19352
20009
|
// O HISTÓRICO DE ATENDIMENTO NA FICHA DA PESSOA.
|
|
@@ -19484,9 +20141,9 @@ function createAgendaPlugin(options) {
|
|
|
19484
20141
|
icon: "Calendar",
|
|
19485
20142
|
// Standard layout: General | Integrations (Google Calendar addon) | Properties.
|
|
19486
20143
|
component: (() => {
|
|
19487
|
-
const AgendaSettingsTab = () =>
|
|
20144
|
+
const AgendaSettingsTab = () => React23.createElement(PluginSettingsPanel, {
|
|
19488
20145
|
title: "Agenda",
|
|
19489
|
-
generalSettings:
|
|
20146
|
+
generalSettings: React23.createElement(AgendaGeneralSettings),
|
|
19490
20147
|
registries,
|
|
19491
20148
|
hostPluginId: "agenda",
|
|
19492
20149
|
routeBase: "/settings/agenda"
|