@fayz-ai/plugin-agenda 0.21.3 → 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/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 React21, { useState, useEffect, useRef, useMemo, useCallback, useLayoutEffect } from 'react';
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, PersonLink, PageActions, useRightRailStore, usePermissionOptional, ScheduleEditor, useLimitGuard, useLookupCreate, CrudFormPage, CrudDetailPage, ContactPicker, openEntity, PaymentStatusBadge as PaymentStatusBadge$1, RightRailPage, invalidateLimit } from '@fayz-ai/admin';
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, Play, UserRound, Package, Clock, Mic, MapPin, CalendarX, UserX, Search, CalendarOff, Pencil, Trash2, User, FileText, AlertTriangle, DollarSign, HandCoins, CheckSquare, Calendar, History, ExternalLink, DoorOpen, Briefcase, CircleDot, MessageCircle, Phone, AlarmClock, ArrowLeft, Link2, Receipt, ClipboardCheck, FileCheck, ArrowRightLeft, CreditCard, QrCode, Banknote, Sparkles } from 'lucide-react';
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 Progress", color: "#f59e0b", availableWhen: "today_only" },
227
- { value: "completed", label: "Completed", color: "#10b981", availableWhen: "today_or_past" },
228
- { value: "cancelled", label: "Cancelled", color: "#ef4444" },
229
- { value: "no_show", label: "No Show", color: "#6b7280", availableWhen: "today_or_past" }
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] = React21.useState(prefillProfessional ?? "");
765
- const [title, setTitle] = React21.useState("");
766
- const [description, setDescription] = React21.useState("");
767
- const [mode, setMode] = React21.useState("single");
768
- const [specificDate, setSpecificDate] = React21.useState(date ?? hoje());
769
- const [allDay, setAllDay] = React21.useState(false);
770
- const [startsAt, setStartsAt] = React21.useState("12:00");
771
- const [endsAt, setEndsAt] = React21.useState("13:00");
772
- const [days2, setDays] = React21.useState([]);
773
- const [validFrom, setValidFrom] = React21.useState(date ?? hoje());
774
- const [validTo, setValidTo] = React21.useState("");
775
- const [weekInterval, setWeekInterval] = React21.useState("0");
776
- const [saving, setSaving] = React21.useState(false);
777
- React21.useEffect(() => {
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] = React21.useState(null);
968
- const [falhou, setFalhou] = React21.useState(false);
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
- React21.useEffect(() => {
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] = React21.useState("");
1028
- const [selectedId, setSelectedId] = React21.useState(null);
1029
- React21.useEffect(() => {
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 = React21.useRef(null);
1034
- React21.useEffect(() => {
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] = React21.useState(null);
1699
- const [failed, setFailed] = React21.useState(false);
1700
- React21.useEffect(() => {
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] = React21.useState([]);
1753
- const [term, setTerm] = React21.useState("");
1754
- const [options, setOptions] = React21.useState([]);
1755
- const initializedFor = React21.useRef(null);
1756
- const onChangeRef = React21.useRef(onChange);
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
- React21.useEffect(() => {
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
- React21.useEffect(() => {
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 = React21.useMemo(() => {
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
- React21.useEffect(() => {
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] = React21.useState(null);
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()) && !conflict && !saving;
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("span", { className: "text-[11px] text-muted-foreground/60", children: endTime }),
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__ */ jsx("div", { className: "pl-7", children: /* @__PURE__ */ jsx(PersonFormsAlert, { personId: clientId, version: formsVersao, onFill: (t3) => setPreenchendoForm(t3) }) }),
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 fmtDate(iso) {
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
- fmtDate(booking.startsAt),
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
- function bookingToEvent(booking, statusColors, visibleResourceIds = []) {
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] = React21.useState(null);
4231
- const [schedulesFor, setSchedulesFor] = React21.useState(null);
4232
- const [editingBlock, setEditingBlock] = React21.useState(null);
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 hhmm = (iso) => new Date(iso).toTimeString().slice(0, 5);
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: hhmm(arg.event.startStr),
4817
- endsAt: hhmm(arg.event.endStr),
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
- return /* @__PURE__ */ jsxs("div", { className: "px-1.5 py-0.5 overflow-hidden leading-tight h-full", children: [
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
- React21.useEffect(() => {
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] = React21.useState(false);
5592
- const [preenchendo, setPreenchendo] = React21.useState(null);
5593
- const [preenchidos, setPreenchidos] = React21.useState(0);
5594
- const [pendencias, setPendencias] = React21.useState([]);
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 = React21.useMemo(() => ({
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 = React21.useMemo(
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] = React21.useState("");
5873
- const [busyId, setBusyId] = React21.useState(null);
5874
- const [myPersonId, setMyPersonId] = React21.useState(void 0);
5875
- const [startRefusal, setStartRefusal] = React21.useState(null);
5876
- const [checkouts, setCheckouts] = React21.useState([]);
5877
- const [conferindo, setConferindo] = React21.useState(null);
5878
- const [checkoutRefusal, setCheckoutRefusal] = React21.useState(null);
5879
- const [panel, setPanel] = React21.useState(null);
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
- React21.useEffect(() => {
6197
+ React23.useEffect(() => {
5889
6198
  void fetchBookings(todayRange(), [...CHECKIN_STATUSES]);
5890
6199
  }, [fetchBookings]);
5891
- const [shownStatuses, setShownStatuses] = React21.useState([]);
5892
- const [shownPros, setShownPros] = React21.useState([]);
5893
- const prosToday = React21.useMemo(() => {
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 = React21.useMemo(() => {
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] = React21.useState({ cancelled: 0, noShow: 0 });
5912
- React21.useEffect(() => {
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 = React21.useMemo(
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
- React21.useEffect(() => {
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 = React21.useCallback(async () => {
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
- React21.useEffect(() => {
6275
+ React23.useEffect(() => {
5957
6276
  void recarregarCheckouts();
5958
6277
  }, [recarregarCheckouts]);
5959
- React21.useEffect(() => {
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
- React21.useEffect(() => {
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] = React21.useState(void 0);
6356
- const [booking, setBooking] = React21.useState(null);
6357
- const [indo, setIndo] = React21.useState(false);
6358
- const [recusa, setRecusa] = React21.useState(null);
6359
- React21.useEffect(() => {
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.name]));
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] = React21.useState(0);
8821
- const [statusLocal, setStatusLocal] = React21.useState(booking.status);
8822
- React21.useEffect(() => {
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] = React21.useState(void 0);
8829
- React21.useEffect(() => {
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] = React21.useState(false);
8841
- const [recusaInicio, setRecusaInicio] = React21.useState(null);
8842
- const [rows, setRows] = React21.useState(null);
8843
- const [record, setRecord] = React21.useState(null);
8844
- const [erro, setErro] = React21.useState(null);
8845
- const [contratando, setContratando] = React21.useState(false);
8846
- const [servicos, setServicos] = React21.useState([]);
8847
- const [marcados, setMarcados] = React21.useState({});
8848
- const [salvando, setSalvando] = React21.useState(false);
8849
- const [confirmando, setConfirmando] = React21.useState(null);
8850
- const [encerrando, setEncerrando] = React21.useState(false);
8851
- const [avisoFinal, setAvisoFinal] = React21.useState(null);
8852
- const [preenchendo, setPreenchendo] = React21.useState(null);
8853
- const [avaliando, setAvaliando] = React21.useState(false);
8854
- const [lancando, setLancando] = React21.useState(null);
8855
- const [executando, setExecutando] = React21.useState(null);
8856
- const [vinculando, setVinculando] = React21.useState(null);
8857
- const [candidatos, setCandidatos] = React21.useState(null);
8858
- const [buscaInsumo, setBuscaInsumo] = React21.useState("");
8859
- const [insumos, setInsumos] = React21.useState([]);
8860
- const [usados, setUsados] = React21.useState([]);
8861
- const [busca, setBusca] = React21.useState("");
8862
- const [catalogo, setCatalogo] = React21.useState([]);
8863
- const [indicados, setIndicados] = React21.useState([]);
8864
- React21.useEffect(() => {
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
- const [r, reg] = await Promise.all([
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(r ?? []);
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
- await abrirExecucao();
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 = React21.useMemo(() => {
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 = React21.useMemo(() => {
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
- React21.useEffect(() => {
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
- React21.useEffect(() => {
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
- React21.useEffect(() => {
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
- onSaved: () => setVersao((v) => v + 1),
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) : void 0
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] = React21.useState(null);
9716
- const [aberto, setAberto] = React21.useState(null);
9717
- const [emCurso, setEmCurso] = React21.useState(null);
9718
- const recarregar = React21.useCallback(async () => {
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
- React21.useEffect(() => {
10245
+ React23.useEffect(() => {
9727
10246
  void recarregar();
9728
10247
  }, [recarregar]);
9729
- React21.useEffect(() => {
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] = React21.useState(null);
9828
- React21.useEffect(() => {
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] = React21.useState(null);
15338
- const [busy, setBusy] = React21.useState(false);
15339
- const refresh = React21.useCallback(async () => {
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
- React21.useEffect(() => {
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] = React21.useState([]);
15509
- const [snapshot, setSnapshot] = React21.useState(null);
15510
- const [loading, setLoading] = React21.useState(false);
15511
- const [error, setError] = React21.useState(null);
15512
- const [, tick] = React21.useState(0);
15513
- const load = React21.useCallback(async () => {
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
- React21.useEffect(() => {
16167
+ React23.useEffect(() => {
15529
16168
  void load();
15530
16169
  }, [load]);
15531
- React21.useEffect(() => {
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
- React21.useEffect(() => provider2.subscribe(() => {
16185
+ React23.useEffect(() => provider2.subscribe(() => {
15547
16186
  void load();
15548
16187
  }), [provider2, load]);
15549
- React21.useEffect(() => {
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 = React21.useMemo(() => [...snapshot?.entries ?? []].sort((a, b) => {
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 = React21.useMemo(() => {
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 = React21.useCallback((professionalId) => {
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] = React21.useState(null);
15676
- const [loading, setLoading] = React21.useState(true);
15677
- const [saving, setSaving] = React21.useState(false);
15678
- const [radius, setRadius] = React21.useState("80");
15679
- React21.useEffect(() => {
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 = (() => React21.createElement(TurnQueuePage, { provider: provider2, agendaProvider, title, subtitle }));
15785
- const HeaderControl = (() => React21.createElement(AvailabilityHeaderControl, { provider: provider2 }));
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 = () => React21.createElement(AgendaPage, { config, provider: provider2, store });
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
- React21.createElement(
19791
+ React23.createElement(
19153
19792
  AgendaContextProvider,
19154
19793
  { config, provider: provider2, store },
19155
- React21.createElement(PersonAttendancesWidget, { personId: props.item?.id })
19794
+ React23.createElement(PersonAttendancesWidget, { personId: props.item?.id })
19156
19795
  )
19157
19796
  );
19158
19797
  PersonAttendancesBound.displayName = "AgendaPersonAttendances";
19159
- const PersonAttendanceBlockBound = (props) => React21.createElement(
19798
+ const PersonAttendanceBlockBound = (props) => React23.createElement(
19160
19799
  AgendaContextProvider,
19161
19800
  { config, provider: provider2, store },
19162
- React21.createElement(PersonAttendanceBlock, { personId: props.item?.id })
19801
+ React23.createElement(PersonAttendanceBlock, { personId: props.item?.id })
19163
19802
  );
19164
19803
  PersonAttendanceBlockBound.displayName = "AgendaPersonAttendanceBlock";
19165
- const PersonServicesBound = (props) => React21.createElement(
19804
+ const PersonServicesBound = (props) => React23.createElement(
19166
19805
  AgendaContextProvider,
19167
19806
  { config, provider: provider2, store },
19168
- React21.createElement(PersonServicesWidget, { personId: props.item?.id })
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: (() => React21.createElement(
19969
+ component: (() => React23.createElement(
19325
19970
  AgendaPage,
19326
19971
  { config, provider: provider2, store },
19327
- React21.createElement(ConfirmationsView)
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: (() => React21.createElement(CheckinPage, {
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 = () => React21.createElement(PluginSettingsPanel, {
20144
+ const AgendaSettingsTab = () => React23.createElement(PluginSettingsPanel, {
19488
20145
  title: "Agenda",
19489
- generalSettings: React21.createElement(AgendaGeneralSettings),
20146
+ generalSettings: React23.createElement(AgendaGeneralSettings),
19490
20147
  registries,
19491
20148
  hostPluginId: "agenda",
19492
20149
  routeBase: "/settings/agenda"