@fayz-ai/plugin-agenda 0.8.1 → 0.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-DRZF6CV2.js → chunk-Q2PAWBPR.js} +62 -3
- package/dist/chunk-Q2PAWBPR.js.map +1 -0
- package/dist/{chunk-LFUELUIT.cjs → chunk-RX2HMWXS.cjs} +62 -2
- package/dist/chunk-RX2HMWXS.cjs.map +1 -0
- package/dist/components/AgendaGeneralSettings.d.ts.map +1 -1
- package/dist/components/AppointmentModal.d.ts.map +1 -1
- package/dist/components/CalendarToolbar.d.ts.map +1 -1
- package/dist/data/supabase.d.ts +5 -1
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/index.cjs +1078 -768
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +921 -611
- package/dist/index.js.map +1 -1
- package/dist/integrations/google-calendar/components/CalendarColorDot.d.ts +6 -0
- package/dist/integrations/google-calendar/components/CalendarColorDot.d.ts.map +1 -0
- package/dist/integrations/google-calendar/components/ChannelMappingList.d.ts +9 -0
- package/dist/integrations/google-calendar/components/ChannelMappingList.d.ts.map +1 -0
- package/dist/integrations/google-calendar/components/ImportEventsWizard.d.ts +12 -0
- package/dist/integrations/google-calendar/components/ImportEventsWizard.d.ts.map +1 -0
- package/dist/integrations/google-calendar/components/useAgendaLookups.d.ts +26 -0
- package/dist/integrations/google-calendar/components/useAgendaLookups.d.ts.map +1 -0
- package/dist/integrations/google-calendar/connectorDef.d.ts.map +1 -1
- package/dist/integrations/google-calendar/data/supabase.d.ts +33 -1
- package/dist/integrations/google-calendar/data/supabase.d.ts.map +1 -1
- package/dist/integrations/google-calendar/data/tables.d.ts +6 -0
- package/dist/integrations/google-calendar/data/tables.d.ts.map +1 -0
- package/dist/integrations/google-calendar/index.d.ts +1 -1
- package/dist/integrations/google-calendar/index.d.ts.map +1 -1
- package/dist/integrations/google-calendar/mapping.d.ts +56 -4
- package/dist/integrations/google-calendar/mapping.d.ts.map +1 -1
- package/dist/integrations/google-calendar/types.d.ts +101 -0
- package/dist/integrations/google-calendar/types.d.ts.map +1 -1
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/migrations/index.d.ts +12 -1
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/public/BookingWidget.d.ts.map +1 -1
- package/dist/public/context.d.ts +3 -1
- package/dist/public/context.d.ts.map +1 -1
- package/dist/public/createPublicBookingPlugin.d.ts +7 -1
- package/dist/public/createPublicBookingPlugin.d.ts.map +1 -1
- package/dist/public/data.d.ts +6 -0
- package/dist/public/data.d.ts.map +1 -1
- package/dist/public/data.supabase.d.ts.map +1 -1
- package/dist/public/format.d.ts +22 -1
- package/dist/public/format.d.ts.map +1 -1
- package/dist/public/hooks.d.ts.map +1 -1
- package/dist/public/index.cjs +31 -63
- package/dist/public/index.cjs.map +1 -1
- package/dist/public/index.d.ts +1 -1
- package/dist/public/index.d.ts.map +1 -1
- package/dist/public/index.js +27 -61
- package/dist/public/index.js.map +1 -1
- package/dist/public/steps/ContactStep.d.ts +4 -2
- package/dist/public/steps/ContactStep.d.ts.map +1 -1
- package/dist/public/types.d.ts +16 -0
- package/dist/public/types.d.ts.map +1 -1
- package/dist/views/CalendarView.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/components/AgendaGeneralSettings.tsx +38 -16
- package/src/components/AppointmentModal.tsx +50 -108
- package/src/components/CalendarToolbar.tsx +10 -7
- package/src/data/supabase.ts +67 -13
- package/src/index.ts +56 -8
- package/src/integrations/google-calendar/components/CalendarColorDot.tsx +23 -0
- package/src/integrations/google-calendar/components/ChannelMappingList.tsx +329 -0
- package/src/integrations/google-calendar/components/ImportEventsWizard.tsx +215 -0
- package/src/integrations/google-calendar/components/useAgendaLookups.ts +100 -0
- package/src/integrations/google-calendar/connectorDef.tsx +123 -40
- package/src/integrations/google-calendar/data/supabase.ts +109 -13
- package/src/integrations/google-calendar/data/tables.ts +12 -0
- package/src/integrations/google-calendar/index.ts +10 -1
- package/src/integrations/google-calendar/integration.test.ts +126 -1
- package/src/integrations/google-calendar/mapping.ts +149 -9
- package/src/integrations/google-calendar/migrations/001_google_calendar.sql +9 -6
- package/src/integrations/google-calendar/migrations/002_smart_sync.sql +517 -0
- package/src/integrations/google-calendar/migrations/003_plg_rename.sql +273 -0
- package/src/integrations/google-calendar/types.ts +111 -0
- package/src/locales/en.ts +1 -0
- package/src/locales/pt-BR.ts +1 -0
- package/src/migrations/001_public_booking.sql +7 -2
- package/src/migrations/003_public_client_kind.sql +175 -0
- package/src/migrations/004_public_booking_assignee.sql +176 -0
- package/src/migrations/005_agent_rpcs.sql +168 -0
- package/src/migrations/006_agent_rpc_localtime.sql +181 -0
- package/src/migrations/007_schedule_exception_precedence.sql +290 -0
- package/src/migrations/008_agent_rpc_exception_precedence.sql +172 -0
- package/src/migrations/009_public_booking_location.sql +162 -0
- package/src/migrations/010_public_service_curation.sql +31 -0
- package/src/migrations/011_default_assignee.sql +167 -0
- package/src/migrations/012_drop_legacy_booking_overload.sql +29 -0
- package/src/migrations/013_normalize_v_appointments.sql +199 -0
- package/src/migrations/index.ts +1791 -3
- package/src/public/BookingWidget.tsx +10 -1
- package/src/public/context.tsx +3 -0
- package/src/public/createPublicBookingPlugin.ts +14 -1
- package/src/public/data.supabase.ts +7 -4
- package/src/public/data.ts +6 -0
- package/src/public/format.ts +36 -1
- package/src/public/hooks.ts +7 -2
- package/src/public/index.ts +1 -0
- package/src/public/steps/ContactStep.tsx +10 -5
- package/src/public/types.ts +21 -0
- package/src/views/CalendarView.tsx +15 -1
- package/dist/chunk-3LJKZQK4.js +0 -12
- package/dist/chunk-3LJKZQK4.js.map +0 -1
- package/dist/chunk-DRZF6CV2.js.map +0 -1
- package/dist/chunk-LFUELUIT.cjs.map +0 -1
- package/dist/chunk-SYDBDI3Q.cjs +0 -15
- package/dist/chunk-SYDBDI3Q.cjs.map +0 -1
- package/dist/components/SearchCombobox.d.ts +0 -38
- package/dist/components/SearchCombobox.d.ts.map +0 -1
- package/dist/tenant-222EHYUC.cjs +0 -16
- package/dist/tenant-222EHYUC.cjs.map +0 -1
- package/dist/tenant-LAUWVUYB.js +0 -3
- package/dist/tenant-LAUWVUYB.js.map +0 -1
- package/src/components/SearchCombobox.tsx +0 -216
package/dist/index.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { createMockAgendaProvider } from './chunk-
|
|
2
|
-
export { createMockAgendaProvider } from './chunk-
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import { createPluginContext, setScheduleBlockConfig, getScheduleBlockConfig, PluginSettingsPanel, dedup, SettingsGroup, ToggleRow, SelectRow, usePluginPrefs, PersonLink, CrudFormPage, CrudDetailPage } from '@fayz-ai/saas';
|
|
1
|
+
import { createMockAgendaProvider, createSupabasePublicBookingProvider } from './chunk-Q2PAWBPR.js';
|
|
2
|
+
export { createMockAgendaProvider } from './chunk-Q2PAWBPR.js';
|
|
3
|
+
import React13, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
|
4
|
+
import { getSupabaseClientOptional, getActiveTenantId, registerTranslations, createSafeDataProvider, usePluginRuntimeOptional, useTranslation, getCurrentLocale, useDataChanged, formatCurrency } from '@fayz-ai/core';
|
|
5
|
+
import { createPluginContext, setScheduleBlockConfig, getScheduleBlockConfig, PluginSettingsPanel, dedup, useTenantPluginSettings, SettingsGroup, ToggleRow, SelectRow, usePluginPrefs, PersonLink, useLimitGuard, CrudFormPage, CrudDetailPage, ContactPicker, invalidateLimit } from '@fayz-ai/saas';
|
|
7
6
|
import FullCalendar from '@fullcalendar/react';
|
|
8
7
|
import dayGridPlugin from '@fullcalendar/daygrid';
|
|
9
8
|
import timeGridPlugin from '@fullcalendar/timegrid';
|
|
@@ -11,10 +10,10 @@ import resourceTimeGridPlugin from '@fullcalendar/resource-timegrid';
|
|
|
11
10
|
import interactionPlugin from '@fullcalendar/interaction';
|
|
12
11
|
import listPlugin from '@fullcalendar/list';
|
|
13
12
|
import ptBrLocale from '@fullcalendar/core/locales/pt-br';
|
|
14
|
-
import { RefreshCw, History, CheckCircle2, AlertCircle, Plus, ChevronDown, ChevronLeft, ChevronRight, Settings, Pencil, Trash2,
|
|
15
|
-
import {
|
|
13
|
+
import { CalendarCog, CalendarPlus, RefreshCw, History, CheckCircle2, AlertCircle, Loader2, X, Plus, ChevronDown, ChevronLeft, ChevronRight, Settings, Pencil, Trash2, Clock, User, MapPin, FileText, AlertTriangle, Check, DollarSign, HandCoins, Ban, CheckSquare, Calendar, Briefcase, CircleDot, FileCheck, ArrowRightLeft, CreditCard, QrCode, Banknote, ExternalLink } from 'lucide-react';
|
|
14
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
16
15
|
import { createPortal } from 'react-dom';
|
|
17
|
-
import { Button, toast, ConfirmDialog, Modal, ModalContent, ModalTitle, cn, TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '@fayz-ai/ui';
|
|
16
|
+
import { Button, toast, Checkbox, Badge, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, ConfirmDialog, Modal, ModalContent, ModalTitle, SearchCombobox, cn, TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '@fayz-ai/ui';
|
|
18
17
|
import { createStore } from 'zustand/vanilla';
|
|
19
18
|
|
|
20
19
|
// src/config.ts
|
|
@@ -446,7 +445,7 @@ function toDateLocale(locale) {
|
|
|
446
445
|
}
|
|
447
446
|
function MiniCalendar({ selectedDate, onDateSelect, onMonthChange }) {
|
|
448
447
|
const locale = getCurrentLocale();
|
|
449
|
-
const [viewMonth, setViewMonth] =
|
|
448
|
+
const [viewMonth, setViewMonth] = React13.useState(() => new Date(selectedDate.getFullYear(), selectedDate.getMonth(), 1));
|
|
450
449
|
const monthLabel = viewMonth.toLocaleDateString(toDateLocale(locale), { month: "long", year: "numeric" });
|
|
451
450
|
const dayLabels = DAY_LABELS[locale] ?? DAY_LABELS.en;
|
|
452
451
|
const weeks = useMemo(() => {
|
|
@@ -501,216 +500,6 @@ function MiniCalendar({ selectedDate, onDateSelect, onMonthChange }) {
|
|
|
501
500
|
}) }, wi))
|
|
502
501
|
] });
|
|
503
502
|
}
|
|
504
|
-
function SearchCombobox({
|
|
505
|
-
value,
|
|
506
|
-
onChange,
|
|
507
|
-
onSelect,
|
|
508
|
-
options,
|
|
509
|
-
placeholder,
|
|
510
|
-
autoFocus,
|
|
511
|
-
onBlurEmpty,
|
|
512
|
-
renderRight,
|
|
513
|
-
allowCreate,
|
|
514
|
-
onCreateNew,
|
|
515
|
-
createLabel = "Create",
|
|
516
|
-
className,
|
|
517
|
-
minimal,
|
|
518
|
-
inlineLabel,
|
|
519
|
-
inlineIcon = "search",
|
|
520
|
-
loading,
|
|
521
|
-
onFocus: onFocusProp
|
|
522
|
-
}) {
|
|
523
|
-
const [open, setOpen] = useState(false);
|
|
524
|
-
const [activeIndex, setActiveIndex] = useState(-1);
|
|
525
|
-
const [expanded, setExpanded] = useState(!inlineLabel);
|
|
526
|
-
const [dropRect, setDropRect] = useState(null);
|
|
527
|
-
const inputRef = useRef(null);
|
|
528
|
-
const wrapperRef = useRef(null);
|
|
529
|
-
const listRef = useRef(null);
|
|
530
|
-
useEffect(() => {
|
|
531
|
-
if (value && inlineLabel) setExpanded(true);
|
|
532
|
-
if (!value && inlineLabel && !open) setExpanded(false);
|
|
533
|
-
}, [value, open]);
|
|
534
|
-
function updateRect() {
|
|
535
|
-
if (wrapperRef.current) setDropRect(wrapperRef.current.getBoundingClientRect());
|
|
536
|
-
}
|
|
537
|
-
const showCreate = open && allowCreate && onCreateNew && value.trim().length > 0 && !options.some((o) => o.label.toLowerCase() === value.trim().toLowerCase());
|
|
538
|
-
const totalItems = options.length + (showCreate ? 1 : 0);
|
|
539
|
-
useEffect(() => {
|
|
540
|
-
setActiveIndex(-1);
|
|
541
|
-
}, [options]);
|
|
542
|
-
useEffect(() => {
|
|
543
|
-
if ((options.length > 0 || showCreate || loading) && document.activeElement === inputRef.current) setOpen(true);
|
|
544
|
-
}, [options, showCreate, loading]);
|
|
545
|
-
const handleKeyDown = useCallback((e) => {
|
|
546
|
-
if (!open || totalItems === 0) {
|
|
547
|
-
if (e.key === "ArrowDown" && totalItems > 0) {
|
|
548
|
-
e.preventDefault();
|
|
549
|
-
setOpen(true);
|
|
550
|
-
setActiveIndex(0);
|
|
551
|
-
}
|
|
552
|
-
return;
|
|
553
|
-
}
|
|
554
|
-
switch (e.key) {
|
|
555
|
-
case "ArrowDown":
|
|
556
|
-
e.preventDefault();
|
|
557
|
-
setActiveIndex((prev) => (prev + 1) % totalItems);
|
|
558
|
-
break;
|
|
559
|
-
case "ArrowUp":
|
|
560
|
-
e.preventDefault();
|
|
561
|
-
setActiveIndex((prev) => prev <= 0 ? totalItems - 1 : prev - 1);
|
|
562
|
-
break;
|
|
563
|
-
case "Enter":
|
|
564
|
-
e.preventDefault();
|
|
565
|
-
if (activeIndex >= 0 && activeIndex < options.length) {
|
|
566
|
-
onSelect(options[activeIndex]);
|
|
567
|
-
setOpen(false);
|
|
568
|
-
setActiveIndex(-1);
|
|
569
|
-
} else if (showCreate && activeIndex === options.length) {
|
|
570
|
-
onCreateNew?.(value.trim());
|
|
571
|
-
setOpen(false);
|
|
572
|
-
setActiveIndex(-1);
|
|
573
|
-
}
|
|
574
|
-
break;
|
|
575
|
-
case "Escape":
|
|
576
|
-
e.preventDefault();
|
|
577
|
-
setOpen(false);
|
|
578
|
-
setActiveIndex(-1);
|
|
579
|
-
break;
|
|
580
|
-
}
|
|
581
|
-
}, [open, options, totalItems, activeIndex, onSelect, showCreate, onCreateNew, value]);
|
|
582
|
-
useEffect(() => {
|
|
583
|
-
if (activeIndex < 0 || !listRef.current) return;
|
|
584
|
-
const items = listRef.current.children;
|
|
585
|
-
if (items[activeIndex]) items[activeIndex].scrollIntoView({ block: "nearest" });
|
|
586
|
-
}, [activeIndex]);
|
|
587
|
-
function handleBlur() {
|
|
588
|
-
setTimeout(() => {
|
|
589
|
-
setOpen(false);
|
|
590
|
-
setActiveIndex(-1);
|
|
591
|
-
if (!value && onBlurEmpty) onBlurEmpty();
|
|
592
|
-
if (!value && inlineLabel) setExpanded(false);
|
|
593
|
-
}, 200);
|
|
594
|
-
}
|
|
595
|
-
const hasDropdown = open && (options.length > 0 || showCreate || loading);
|
|
596
|
-
if (!expanded && inlineLabel) {
|
|
597
|
-
const isPlus = inlineIcon === "plus";
|
|
598
|
-
return /* @__PURE__ */ jsxs(
|
|
599
|
-
"button",
|
|
600
|
-
{
|
|
601
|
-
type: "button",
|
|
602
|
-
onClick: () => {
|
|
603
|
-
setExpanded(true);
|
|
604
|
-
setTimeout(() => inputRef.current?.focus(), 50);
|
|
605
|
-
},
|
|
606
|
-
className: `flex items-center gap-1.5 w-full text-left text-sm py-1.5 pl-2 transition-colors ${isPlus ? "text-primary hover:text-primary/80" : "text-muted-foreground hover:text-foreground"} ${className ?? ""}`,
|
|
607
|
-
children: [
|
|
608
|
-
isPlus && /* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5 shrink-0" }),
|
|
609
|
-
inlineLabel
|
|
610
|
-
]
|
|
611
|
-
}
|
|
612
|
-
);
|
|
613
|
-
}
|
|
614
|
-
return /* @__PURE__ */ jsxs("div", { ref: wrapperRef, className: `relative ${className ?? ""}`, children: [
|
|
615
|
-
!minimal && /* @__PURE__ */ jsx(Search, { className: "absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground pointer-events-none" }),
|
|
616
|
-
/* @__PURE__ */ jsx(
|
|
617
|
-
"input",
|
|
618
|
-
{
|
|
619
|
-
ref: inputRef,
|
|
620
|
-
type: "text",
|
|
621
|
-
value,
|
|
622
|
-
onChange: (e) => {
|
|
623
|
-
onChange(e.target.value);
|
|
624
|
-
updateRect();
|
|
625
|
-
setOpen(true);
|
|
626
|
-
},
|
|
627
|
-
onFocus: () => {
|
|
628
|
-
updateRect();
|
|
629
|
-
onFocusProp?.();
|
|
630
|
-
if (options.length > 0 || showCreate || loading) setOpen(true);
|
|
631
|
-
},
|
|
632
|
-
onBlur: handleBlur,
|
|
633
|
-
onKeyDown: handleKeyDown,
|
|
634
|
-
placeholder,
|
|
635
|
-
autoFocus,
|
|
636
|
-
className: minimal ? "w-full bg-transparent pl-2 pr-3 py-1.5 text-sm placeholder:text-muted-foreground focus:outline-none border-b border-transparent hover:border-border focus:border-foreground transition-colors" : "w-full rounded-lg border bg-background pl-8 pr-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring",
|
|
637
|
-
role: "combobox",
|
|
638
|
-
"aria-expanded": hasDropdown,
|
|
639
|
-
"aria-autocomplete": "list",
|
|
640
|
-
"aria-activedescendant": activeIndex >= 0 ? `combobox-opt-${activeIndex}` : void 0
|
|
641
|
-
}
|
|
642
|
-
),
|
|
643
|
-
hasDropdown && dropRect && createPortal(
|
|
644
|
-
/* @__PURE__ */ jsxs(
|
|
645
|
-
"div",
|
|
646
|
-
{
|
|
647
|
-
ref: listRef,
|
|
648
|
-
role: "listbox",
|
|
649
|
-
"data-modal-passthrough": true,
|
|
650
|
-
style: { position: "fixed", left: dropRect.left, top: dropRect.bottom + 4, width: dropRect.width, zIndex: 9999, pointerEvents: "auto" },
|
|
651
|
-
className: "fayz-glass-surface max-h-52 overflow-y-auto rounded-lg border bg-popover shadow-lg py-0.5 animate-in fade-in zoom-in-95 duration-100",
|
|
652
|
-
children: [
|
|
653
|
-
loading && options.length === 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-3 py-2 text-sm text-muted-foreground", children: [
|
|
654
|
-
/* @__PURE__ */ jsxs("svg", { className: "h-3.5 w-3.5 animate-spin shrink-0", viewBox: "0 0 16 16", fill: "none", children: [
|
|
655
|
-
/* @__PURE__ */ jsx("circle", { cx: "8", cy: "8", r: "6.5", stroke: "currentColor", strokeWidth: "1.5", opacity: "0.25" }),
|
|
656
|
-
/* @__PURE__ */ jsx("path", { d: "M14.5 8a6.5 6.5 0 00-6.5-6.5", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" })
|
|
657
|
-
] }),
|
|
658
|
-
/* @__PURE__ */ jsx("span", { children: "Buscando\u2026" })
|
|
659
|
-
] }),
|
|
660
|
-
options.map((opt, i) => /* @__PURE__ */ jsxs(
|
|
661
|
-
"div",
|
|
662
|
-
{
|
|
663
|
-
id: `combobox-opt-${i}`,
|
|
664
|
-
role: "option",
|
|
665
|
-
"aria-selected": i === activeIndex,
|
|
666
|
-
onMouseDown: (e) => {
|
|
667
|
-
e.preventDefault();
|
|
668
|
-
onSelect(opt);
|
|
669
|
-
setOpen(false);
|
|
670
|
-
},
|
|
671
|
-
onMouseEnter: () => setActiveIndex(i),
|
|
672
|
-
className: `flex w-full items-center gap-2 px-3 py-2 text-sm cursor-pointer transition-colors ${i === activeIndex ? "bg-primary/10 text-primary" : "hover:bg-muted/50"}`,
|
|
673
|
-
children: [
|
|
674
|
-
/* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
|
|
675
|
-
/* @__PURE__ */ jsx("p", { className: "truncate font-medium", children: opt.label }),
|
|
676
|
-
opt.subtitle && /* @__PURE__ */ jsx("p", { className: "truncate text-[11px] text-muted-foreground", children: opt.subtitle })
|
|
677
|
-
] }),
|
|
678
|
-
renderRight && /* @__PURE__ */ jsx("div", { className: "shrink-0", children: renderRight(opt) })
|
|
679
|
-
]
|
|
680
|
-
},
|
|
681
|
-
opt.id
|
|
682
|
-
)),
|
|
683
|
-
showCreate && /* @__PURE__ */ jsxs(
|
|
684
|
-
"div",
|
|
685
|
-
{
|
|
686
|
-
id: `combobox-opt-${options.length}`,
|
|
687
|
-
role: "option",
|
|
688
|
-
"aria-selected": activeIndex === options.length,
|
|
689
|
-
onMouseDown: (e) => {
|
|
690
|
-
e.preventDefault();
|
|
691
|
-
onCreateNew?.(value.trim());
|
|
692
|
-
setOpen(false);
|
|
693
|
-
},
|
|
694
|
-
onMouseEnter: () => setActiveIndex(options.length),
|
|
695
|
-
className: `flex w-full items-center gap-2 px-3 py-2 text-sm cursor-pointer transition-colors border-t ${activeIndex === options.length ? "bg-primary/10 text-primary" : "hover:bg-muted/50"}`,
|
|
696
|
-
children: [
|
|
697
|
-
/* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5 shrink-0" }),
|
|
698
|
-
/* @__PURE__ */ jsxs("span", { children: [
|
|
699
|
-
createLabel,
|
|
700
|
-
" \u201C",
|
|
701
|
-
/* @__PURE__ */ jsx("span", { className: "font-medium", children: value.trim() }),
|
|
702
|
-
"\u201D"
|
|
703
|
-
] })
|
|
704
|
-
]
|
|
705
|
-
}
|
|
706
|
-
)
|
|
707
|
-
]
|
|
708
|
-
}
|
|
709
|
-
),
|
|
710
|
-
document.body
|
|
711
|
-
)
|
|
712
|
-
] });
|
|
713
|
-
}
|
|
714
503
|
var TYPE_ICONS = {
|
|
715
504
|
cash: Banknote,
|
|
716
505
|
pix: QrCode,
|
|
@@ -736,7 +525,7 @@ var ICON_COLORS = {
|
|
|
736
525
|
check: "text-muted-foreground"
|
|
737
526
|
};
|
|
738
527
|
function BookingPaymentPanel({ orderId, orderTotal, services, onClose, onPaymentChange, formatCurrency: formatCurrency2, inline }) {
|
|
739
|
-
const
|
|
528
|
+
const t2 = useTranslation();
|
|
740
529
|
const config = useAgendaConfig();
|
|
741
530
|
const bridge = config.financialBridge;
|
|
742
531
|
const [visible, setVisible] = useState(false);
|
|
@@ -777,7 +566,7 @@ function BookingPaymentPanel({ orderId, orderTotal, services, onClose, onPayment
|
|
|
777
566
|
setVisible(false);
|
|
778
567
|
setTimeout(onClose, 200);
|
|
779
568
|
}
|
|
780
|
-
const selectedType = paymentTypes.find((
|
|
569
|
+
const selectedType = paymentTypes.find((t3) => t3.id === selectedTypeId);
|
|
781
570
|
const txType = selectedType?.transactionType ?? "";
|
|
782
571
|
const needsBank = ["bank_transfer", "pix", "check"].includes(txType);
|
|
783
572
|
const needsCard = txType === "credit_card" || txType === "debit_card";
|
|
@@ -816,7 +605,7 @@ function BookingPaymentPanel({ orderId, orderTotal, services, onClose, onPayment
|
|
|
816
605
|
loading && /* @__PURE__ */ jsx("div", { className: "space-y-3 py-2", children: [...Array(3)].map((_, i) => /* @__PURE__ */ jsx("div", { className: "h-8 rounded-lg bg-muted animate-pulse" }, i)) }),
|
|
817
606
|
!loading && isPaid && justPaid && /* @__PURE__ */ jsxs("div", { className: "text-center py-8 space-y-2 animate-in fade-in zoom-in-95 duration-300", children: [
|
|
818
607
|
/* @__PURE__ */ jsx("div", { className: "flex h-14 w-14 mx-auto items-center justify-center rounded-full bg-success/10 animate-bounce", style: { animationDuration: "600ms", animationIterationCount: "2" }, children: /* @__PURE__ */ jsx(Check, { className: "h-7 w-7 text-success" }) }),
|
|
819
|
-
/* @__PURE__ */ jsx("p", { className: "text-sm font-semibold text-success", children:
|
|
608
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-semibold text-success", children: t2("agenda.payment.checkoutComplete") }),
|
|
820
609
|
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: formatCurrency2(detail.totalAmount) })
|
|
821
610
|
] }),
|
|
822
611
|
!loading && detail && detail.status !== "none" && !justPaid && /* @__PURE__ */ jsxs("div", { className: "space-y-3 animate-in fade-in duration-300", children: [
|
|
@@ -838,14 +627,14 @@ function BookingPaymentPanel({ orderId, orderTotal, services, onClose, onPayment
|
|
|
838
627
|
] }),
|
|
839
628
|
/* @__PURE__ */ jsxs("p", { className: "text-[10px] text-muted-foreground", children: [
|
|
840
629
|
mov.paymentDate ?? mov.dueDate,
|
|
841
|
-
detail.movements.length > 1 && mov.installmentNumber ? ` \xB7 ${
|
|
630
|
+
detail.movements.length > 1 && mov.installmentNumber ? ` \xB7 ${t2("agenda.payment.installment")} ${mov.installmentNumber}` : ""
|
|
842
631
|
] })
|
|
843
632
|
] }),
|
|
844
633
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [
|
|
845
634
|
isMovPaid && /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-success/10 px-2 py-0.5 text-[10px] font-medium text-success", children: [
|
|
846
635
|
/* @__PURE__ */ jsx(Check, { className: "h-2.5 w-2.5" }),
|
|
847
636
|
" ",
|
|
848
|
-
|
|
637
|
+
t2("agenda.payment.paid")
|
|
849
638
|
] }),
|
|
850
639
|
isPartial && /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-warning/10 px-2 py-0.5 text-[10px] font-medium text-warning", children: [
|
|
851
640
|
"! ",
|
|
@@ -855,14 +644,14 @@ function BookingPaymentPanel({ orderId, orderTotal, services, onClose, onPayment
|
|
|
855
644
|
] }),
|
|
856
645
|
isOverdue && /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-destructive/10 px-2 py-0.5 text-[10px] font-medium text-destructive", children: [
|
|
857
646
|
"! ",
|
|
858
|
-
|
|
647
|
+
t2("agenda.payment.overdue")
|
|
859
648
|
] }),
|
|
860
|
-
mov.status === "pending" && /* @__PURE__ */ jsx("span", { className: "inline-flex items-center rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium text-muted-foreground", children:
|
|
649
|
+
mov.status === "pending" && /* @__PURE__ */ jsx("span", { className: "inline-flex items-center rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium text-muted-foreground", children: t2("agenda.payment.pending") }),
|
|
861
650
|
/* @__PURE__ */ jsx("span", { className: "text-sm font-semibold", children: formatCurrency2(mov.paidAmount || mov.amount) })
|
|
862
651
|
] })
|
|
863
652
|
] }, mov.id);
|
|
864
653
|
}) }) : /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-sm px-1", children: [
|
|
865
|
-
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children:
|
|
654
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: t2("agenda.payment.total") }),
|
|
866
655
|
/* @__PURE__ */ jsx("span", { className: "font-semibold", children: formatCurrency2(detail.totalAmount) })
|
|
867
656
|
] }),
|
|
868
657
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
@@ -876,7 +665,7 @@ function BookingPaymentPanel({ orderId, orderTotal, services, onClose, onPayment
|
|
|
876
665
|
href: `#/financial/receivables/detail/${detail.invoiceId}`,
|
|
877
666
|
className: "inline-flex items-center gap-1.5 text-xs font-medium text-primary hover:underline ml-auto",
|
|
878
667
|
children: [
|
|
879
|
-
|
|
668
|
+
t2("agenda.payment.viewInvoice"),
|
|
880
669
|
/* @__PURE__ */ jsx(ExternalLink, { className: "h-3 w-3" })
|
|
881
670
|
]
|
|
882
671
|
}
|
|
@@ -885,22 +674,22 @@ function BookingPaymentPanel({ orderId, orderTotal, services, onClose, onPayment
|
|
|
885
674
|
] }),
|
|
886
675
|
!loading && (!detail || detail.movements.length === 0) && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
887
676
|
services && services.length > 0 && /* @__PURE__ */ jsxs("div", { children: [
|
|
888
|
-
/* @__PURE__ */ jsx("p", { className: "text-[11px] font-medium text-muted-foreground mb-2", children:
|
|
677
|
+
/* @__PURE__ */ jsx("p", { className: "text-[11px] font-medium text-muted-foreground mb-2", children: t2("agenda.payment.serviceSummary") }),
|
|
889
678
|
/* @__PURE__ */ jsx("div", { className: "space-y-0.5", children: services.map((svc, i) => /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-sm py-1", children: [
|
|
890
679
|
/* @__PURE__ */ jsx("span", { className: "truncate", children: svc.name }),
|
|
891
680
|
/* @__PURE__ */ jsx("span", { className: "shrink-0 text-muted-foreground", children: formatCurrency2(svc.price) })
|
|
892
681
|
] }, i)) })
|
|
893
682
|
] }),
|
|
894
683
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-sm font-semibold pt-2 border-t", children: [
|
|
895
|
-
/* @__PURE__ */ jsx("span", { children:
|
|
684
|
+
/* @__PURE__ */ jsx("span", { children: t2("agenda.payment.total") }),
|
|
896
685
|
/* @__PURE__ */ jsx("span", { children: formatCurrency2(orderTotal) })
|
|
897
686
|
] }),
|
|
898
687
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
899
|
-
/* @__PURE__ */ jsx("p", { className: "text-[11px] font-medium text-muted-foreground mb-2", children:
|
|
688
|
+
/* @__PURE__ */ jsx("p", { className: "text-[11px] font-medium text-muted-foreground mb-2", children: t2("agenda.payment.paymentMethod") }),
|
|
900
689
|
paymentTypes.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-dashed border-input bg-muted/30 px-3 py-3 text-center", children: [
|
|
901
|
-
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children:
|
|
690
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t2("agenda.payment.noMethods") }),
|
|
902
691
|
/* @__PURE__ */ jsxs("a", { href: "#/financial/settings", className: "mt-1.5 inline-block text-xs font-medium text-primary hover:underline", children: [
|
|
903
|
-
|
|
692
|
+
t2("agenda.payment.configureMethods"),
|
|
904
693
|
" \u2192"
|
|
905
694
|
] })
|
|
906
695
|
] }) : /* @__PURE__ */ jsx("div", { className: "grid grid-cols-3 gap-1.5", children: paymentTypes.map((pt) => {
|
|
@@ -927,7 +716,7 @@ function BookingPaymentPanel({ orderId, orderTotal, services, onClose, onPayment
|
|
|
927
716
|
] }),
|
|
928
717
|
selectedTypeId && /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
929
718
|
filteredMethods.length > 0 && /* @__PURE__ */ jsxs("div", { children: [
|
|
930
|
-
/* @__PURE__ */ jsx("label", { className: "text-[11px] font-medium text-muted-foreground", children: needsCard ?
|
|
719
|
+
/* @__PURE__ */ jsx("label", { className: "text-[11px] font-medium text-muted-foreground", children: needsCard ? t2("agenda.payment.card") : t2("agenda.payment.method") }),
|
|
931
720
|
/* @__PURE__ */ jsxs(
|
|
932
721
|
"select",
|
|
933
722
|
{
|
|
@@ -935,7 +724,7 @@ function BookingPaymentPanel({ orderId, orderTotal, services, onClose, onPayment
|
|
|
935
724
|
onChange: (e) => setPaymentMethodId(e.target.value),
|
|
936
725
|
className: "w-full mt-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-2.5 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-ring",
|
|
937
726
|
children: [
|
|
938
|
-
/* @__PURE__ */ jsx("option", { value: "", children:
|
|
727
|
+
/* @__PURE__ */ jsx("option", { value: "", children: t2("agenda.payment.select") }),
|
|
939
728
|
filteredMethods.map((m) => /* @__PURE__ */ jsx("option", { value: m.id, children: m.name }, m.id))
|
|
940
729
|
]
|
|
941
730
|
}
|
|
@@ -943,7 +732,7 @@ function BookingPaymentPanel({ orderId, orderTotal, services, onClose, onPayment
|
|
|
943
732
|
] }),
|
|
944
733
|
needsCard && /* @__PURE__ */ jsxs("div", { className: `grid gap-2.5 ${needsCardInstallments ? "grid-cols-2" : "grid-cols-1"}`, children: [
|
|
945
734
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
946
|
-
/* @__PURE__ */ jsx("label", { className: "text-[11px] font-medium text-muted-foreground", children:
|
|
735
|
+
/* @__PURE__ */ jsx("label", { className: "text-[11px] font-medium text-muted-foreground", children: t2("agenda.payment.cardBrand") }),
|
|
947
736
|
/* @__PURE__ */ jsxs(
|
|
948
737
|
"select",
|
|
949
738
|
{
|
|
@@ -951,14 +740,14 @@ function BookingPaymentPanel({ orderId, orderTotal, services, onClose, onPayment
|
|
|
951
740
|
onChange: (e) => setCardBrand(e.target.value),
|
|
952
741
|
className: "w-full mt-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-2.5 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-ring",
|
|
953
742
|
children: [
|
|
954
|
-
/* @__PURE__ */ jsx("option", { value: "", children:
|
|
743
|
+
/* @__PURE__ */ jsx("option", { value: "", children: t2("agenda.payment.selectBrand") }),
|
|
955
744
|
["Visa", "Mastercard", "American Express", "Elo", "Hipercard", "Diners Club"].map((b) => /* @__PURE__ */ jsx("option", { value: b, children: b }, b))
|
|
956
745
|
]
|
|
957
746
|
}
|
|
958
747
|
)
|
|
959
748
|
] }),
|
|
960
749
|
needsCardInstallments && /* @__PURE__ */ jsxs("div", { children: [
|
|
961
|
-
/* @__PURE__ */ jsx("label", { className: "text-[11px] font-medium text-muted-foreground", children:
|
|
750
|
+
/* @__PURE__ */ jsx("label", { className: "text-[11px] font-medium text-muted-foreground", children: t2("agenda.payment.cardInstallments") }),
|
|
962
751
|
/* @__PURE__ */ jsx(
|
|
963
752
|
"select",
|
|
964
753
|
{
|
|
@@ -975,7 +764,7 @@ function BookingPaymentPanel({ orderId, orderTotal, services, onClose, onPayment
|
|
|
975
764
|
] })
|
|
976
765
|
] }),
|
|
977
766
|
needsBank && bankAccounts.length > 0 && /* @__PURE__ */ jsxs("div", { children: [
|
|
978
|
-
/* @__PURE__ */ jsx("label", { className: "text-[11px] font-medium text-muted-foreground", children:
|
|
767
|
+
/* @__PURE__ */ jsx("label", { className: "text-[11px] font-medium text-muted-foreground", children: t2("agenda.payment.bankAccount") }),
|
|
979
768
|
/* @__PURE__ */ jsxs(
|
|
980
769
|
"select",
|
|
981
770
|
{
|
|
@@ -983,7 +772,7 @@ function BookingPaymentPanel({ orderId, orderTotal, services, onClose, onPayment
|
|
|
983
772
|
onChange: (e) => setSelectedBankId(e.target.value),
|
|
984
773
|
className: "w-full mt-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-2.5 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-ring",
|
|
985
774
|
children: [
|
|
986
|
-
/* @__PURE__ */ jsx("option", { value: "", children:
|
|
775
|
+
/* @__PURE__ */ jsx("option", { value: "", children: t2("agenda.payment.selectAccount") }),
|
|
987
776
|
bankAccounts.map((a) => /* @__PURE__ */ jsx("option", { value: a.id, children: a.name }, a.id))
|
|
988
777
|
]
|
|
989
778
|
}
|
|
@@ -997,7 +786,7 @@ function BookingPaymentPanel({ orderId, orderTotal, services, onClose, onPayment
|
|
|
997
786
|
onClick: handleCheckout,
|
|
998
787
|
disabled: processing || !selectedTypeId,
|
|
999
788
|
className: "w-full rounded-lg bg-primary border border-primary py-2.5 text-sm font-medium text-primary-foreground hover:bg-primary/90 shadow-button-primary active:shadow-button-inset disabled:opacity-40 transition-colors",
|
|
1000
|
-
children: processing ?
|
|
789
|
+
children: processing ? t2("agenda.payment.checkingOut") : `${t2("agenda.payment.checkout")} ${formatCurrency2(orderTotal)}`
|
|
1001
790
|
}
|
|
1002
791
|
)
|
|
1003
792
|
] })
|
|
@@ -1018,7 +807,7 @@ function BookingPaymentPanel({ orderId, orderTotal, services, onClose, onPayment
|
|
|
1018
807
|
onClick: (e) => e.stopPropagation(),
|
|
1019
808
|
children: [
|
|
1020
809
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-5 py-4 border-b sticky top-0 bg-card z-10", children: [
|
|
1021
|
-
/* @__PURE__ */ jsx("h3", { className: "text-base font-semibold", children:
|
|
810
|
+
/* @__PURE__ */ jsx("h3", { className: "text-base font-semibold", children: t2("agenda.payment.title") }),
|
|
1022
811
|
/* @__PURE__ */ jsx("button", { onClick: handleClose, className: "p-1 text-muted-foreground hover:text-foreground transition-colors", children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" }) })
|
|
1023
812
|
] }),
|
|
1024
813
|
content
|
|
@@ -1179,7 +968,7 @@ function StatusSelect({ value, onChange, statuses, bookingDate }) {
|
|
|
1179
968
|
] });
|
|
1180
969
|
}
|
|
1181
970
|
function ProfessionalSelect({ value, onChange, professionals }) {
|
|
1182
|
-
const
|
|
971
|
+
const t2 = useTranslation();
|
|
1183
972
|
const [open, setOpen] = useState(false);
|
|
1184
973
|
const ref = useRef(null);
|
|
1185
974
|
const current = professionals.find((p) => p.id === value);
|
|
@@ -1199,7 +988,7 @@ function ProfessionalSelect({ value, onChange, professionals }) {
|
|
|
1199
988
|
onClick: () => setOpen((p) => !p),
|
|
1200
989
|
className: "flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-muted/30 transition-colors text-left",
|
|
1201
990
|
children: [
|
|
1202
|
-
current ? /* @__PURE__ */ jsx("span", { children: current.name }) : /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children:
|
|
991
|
+
current ? /* @__PURE__ */ jsx("span", { children: current.name }) : /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: t2("agenda.appointment.selectProfessional") }),
|
|
1203
992
|
/* @__PURE__ */ jsx("svg", { className: "ml-auto h-3 w-3 text-muted-foreground shrink-0", viewBox: "0 0 16 16", fill: "none", children: /* @__PURE__ */ jsx("path", { d: "M4 6l4 4 4-4", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) })
|
|
1204
993
|
]
|
|
1205
994
|
}
|
|
@@ -1220,7 +1009,7 @@ function ProfessionalSelect({ value, onChange, professionals }) {
|
|
|
1220
1009
|
] });
|
|
1221
1010
|
}
|
|
1222
1011
|
function LocationSelect({ value, onChange, locations }) {
|
|
1223
|
-
const
|
|
1012
|
+
const t2 = useTranslation();
|
|
1224
1013
|
const [open, setOpen] = useState(false);
|
|
1225
1014
|
const ref = useRef(null);
|
|
1226
1015
|
const current = locations.find((l) => l.id === value);
|
|
@@ -1240,7 +1029,7 @@ function LocationSelect({ value, onChange, locations }) {
|
|
|
1240
1029
|
onClick: () => setOpen((p) => !p),
|
|
1241
1030
|
className: "flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-muted/30 transition-colors text-left",
|
|
1242
1031
|
children: [
|
|
1243
|
-
current ? /* @__PURE__ */ jsx("span", { children: current.name }) : /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children:
|
|
1032
|
+
current ? /* @__PURE__ */ jsx("span", { children: current.name }) : /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: t2("agenda.appointment.selectLocation") }),
|
|
1244
1033
|
/* @__PURE__ */ jsx("svg", { className: "ml-auto h-3 w-3 text-muted-foreground shrink-0", viewBox: "0 0 16 16", fill: "none", children: /* @__PURE__ */ jsx("path", { d: "M4 6l4 4 4-4", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) })
|
|
1245
1034
|
]
|
|
1246
1035
|
}
|
|
@@ -1261,12 +1050,13 @@ function LocationSelect({ value, onChange, locations }) {
|
|
|
1261
1050
|
] });
|
|
1262
1051
|
}
|
|
1263
1052
|
function AppointmentModal({ open, mode, bookingId, prefill, initialTab, externalSource, onClose, onCreated }) {
|
|
1264
|
-
const
|
|
1053
|
+
const t2 = useTranslation();
|
|
1265
1054
|
const locale = getCurrentLocale();
|
|
1266
1055
|
const config = useAgendaConfig();
|
|
1267
1056
|
const provider2 = useAgendaProvider();
|
|
1268
1057
|
const initialSnapshot = useRef("");
|
|
1269
1058
|
const createBooking = useAgendaStore((s) => s.createBooking);
|
|
1059
|
+
const guardBookings = useLimitGuard("bookings_month");
|
|
1270
1060
|
const updateBooking = useAgendaStore((s) => s.updateBooking);
|
|
1271
1061
|
const deleteBooking = useAgendaStore((s) => s.deleteBooking);
|
|
1272
1062
|
const fetchBookings = useAgendaStore((s) => s.fetchBookings);
|
|
@@ -1300,13 +1090,6 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
1300
1090
|
const [showNotes, setShowNotes] = useState(false);
|
|
1301
1091
|
const [saving, setSaving] = useState(false);
|
|
1302
1092
|
const [conflict, setConflict] = useState(false);
|
|
1303
|
-
const [quickCreate, setQuickCreate] = useState(false);
|
|
1304
|
-
const [newClientName, setNewClientName] = useState("");
|
|
1305
|
-
const [newClientPhone, setNewClientPhone] = useState("");
|
|
1306
|
-
const [newClientEmail, setNewClientEmail] = useState("");
|
|
1307
|
-
const [creatingClient, setCreatingClient] = useState(false);
|
|
1308
|
-
const [clientOptions, setClientOptions] = useState([]);
|
|
1309
|
-
const [clientSearching, setClientSearching] = useState(false);
|
|
1310
1093
|
const [serviceOptions, setServiceOptions] = useState([]);
|
|
1311
1094
|
const [initialServiceOptions, setInitialServiceOptions] = useState([]);
|
|
1312
1095
|
const [editingDate, setEditingDate] = useState(false);
|
|
@@ -1340,11 +1123,6 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
1340
1123
|
setShowNotes(false);
|
|
1341
1124
|
setConflict(false);
|
|
1342
1125
|
setLoading(mode === "edit");
|
|
1343
|
-
setQuickCreate(false);
|
|
1344
|
-
setNewClientName("");
|
|
1345
|
-
setNewClientPhone("");
|
|
1346
|
-
setNewClientEmail("");
|
|
1347
|
-
setCreatingClient(false);
|
|
1348
1126
|
setBookingType(prefill?.kind ?? defaultBookingType);
|
|
1349
1127
|
setTitle("");
|
|
1350
1128
|
setEditingDate(false);
|
|
@@ -1454,24 +1232,6 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
1454
1232
|
setInitialServiceOptions(opts);
|
|
1455
1233
|
}).catch(() => setInitialServiceOptions([]));
|
|
1456
1234
|
}, [open]);
|
|
1457
|
-
useEffect(() => {
|
|
1458
|
-
if (!config.contactLookup || clientSearch.length < 1 || clientId) {
|
|
1459
|
-
setClientOptions([]);
|
|
1460
|
-
setClientSearching(false);
|
|
1461
|
-
return;
|
|
1462
|
-
}
|
|
1463
|
-
setClientSearching(true);
|
|
1464
|
-
const timer = setTimeout(async () => {
|
|
1465
|
-
try {
|
|
1466
|
-
const results = await config.contactLookup.search(clientSearch);
|
|
1467
|
-
setClientOptions(results.map((r) => ({ id: r.id, label: r.label, subtitle: r.subtitle, price: r.price, data: r.data })));
|
|
1468
|
-
} catch {
|
|
1469
|
-
setClientOptions([]);
|
|
1470
|
-
}
|
|
1471
|
-
setClientSearching(false);
|
|
1472
|
-
}, 250);
|
|
1473
|
-
return () => clearTimeout(timer);
|
|
1474
|
-
}, [clientSearch, clientId]);
|
|
1475
1235
|
useEffect(() => {
|
|
1476
1236
|
if (!config.serviceLookup || serviceSearch.length < 1) {
|
|
1477
1237
|
setServiceOptions([]);
|
|
@@ -1515,12 +1275,6 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
1515
1275
|
}, 300);
|
|
1516
1276
|
return () => clearTimeout(timer);
|
|
1517
1277
|
}, [professionalId, date, startTime, endTime, bookingId]);
|
|
1518
|
-
function selectClient(opt) {
|
|
1519
|
-
setClientId(opt.id);
|
|
1520
|
-
setClientSearch(opt.label);
|
|
1521
|
-
setClientOptions([]);
|
|
1522
|
-
setClientPhone(opt.data?.phone ?? "");
|
|
1523
|
-
}
|
|
1524
1278
|
function addService(opt) {
|
|
1525
1279
|
const duration = opt.data?.duration_minutes || 60;
|
|
1526
1280
|
setServices((p) => [...p, { serviceId: opt.id, name: opt.label, durationMinutes: duration, price: opt.price ?? 0 }]);
|
|
@@ -1533,6 +1287,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
1533
1287
|
async function handleSubmit(e) {
|
|
1534
1288
|
e.preventDefault();
|
|
1535
1289
|
if (!canSubmit) return;
|
|
1290
|
+
if (mode === "create" && await guardBookings() === "blocked") return;
|
|
1536
1291
|
setSaving(true);
|
|
1537
1292
|
try {
|
|
1538
1293
|
const startsAt = (/* @__PURE__ */ new Date(`${date}T${startTime}:00`)).toISOString();
|
|
@@ -1547,6 +1302,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
1547
1302
|
title: activeType.fields.title ? title.trim() : void 0,
|
|
1548
1303
|
services: services.map((s) => ({ serviceId: s.serviceId, name: s.name, durationMinutes: s.durationMinutes, price: s.price }))
|
|
1549
1304
|
});
|
|
1305
|
+
invalidateLimit("bookings_month");
|
|
1550
1306
|
onClose();
|
|
1551
1307
|
onCreated?.(newBooking.id, externalSource);
|
|
1552
1308
|
} else if (bookingId) {
|
|
@@ -1581,8 +1337,10 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
1581
1337
|
await deleteBooking(bookingId);
|
|
1582
1338
|
onClose();
|
|
1583
1339
|
} catch {
|
|
1340
|
+
setConfirmingDelete(false);
|
|
1341
|
+
} finally {
|
|
1342
|
+
setDeleting(false);
|
|
1584
1343
|
}
|
|
1585
|
-
setDeleting(false);
|
|
1586
1344
|
}
|
|
1587
1345
|
const isPaidBooking = editPaymentStatus === "paid";
|
|
1588
1346
|
const selectedCancellationReason = cancellationReasons.find((reason) => reason.id === cancellationReasonId);
|
|
@@ -1599,9 +1357,9 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
1599
1357
|
if (!v) onClose();
|
|
1600
1358
|
}, children: /* @__PURE__ */ jsxs(ModalContent, { size: "lg", noPadding: true, children: [
|
|
1601
1359
|
/* @__PURE__ */ jsxs("div", { className: "px-5 pt-4 pb-3 shrink-0 space-y-3", children: [
|
|
1602
|
-
/* @__PURE__ */ jsx(ModalTitle, { className: "sr-only", children: mode === "create" ?
|
|
1360
|
+
/* @__PURE__ */ jsx(ModalTitle, { className: "sr-only", children: mode === "create" ? t2("agenda.appointment.new") : t2("agenda.appointment.edit") }),
|
|
1603
1361
|
showTabs && mode === "create" ? /* @__PURE__ */ jsx("div", { className: "flex gap-1 border-b fayz-glass-divider", children: config.bookingTypes.map((bt) => {
|
|
1604
|
-
const label =
|
|
1362
|
+
const label = t2(`agenda.bookingType.${bt.id}`) || bt.label;
|
|
1605
1363
|
const isActive = bookingType === bt.id;
|
|
1606
1364
|
const BtIcon = BOOKING_TYPE_ICONS[bt.icon] ?? Calendar;
|
|
1607
1365
|
return /* @__PURE__ */ jsxs(
|
|
@@ -1619,9 +1377,9 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
1619
1377
|
bt.id
|
|
1620
1378
|
);
|
|
1621
1379
|
}) }) : mode === "edit" && (config.clientEntityDef && clientId || config.modules.financial && editOrderId ? /* @__PURE__ */ jsx("div", { className: "flex gap-1 border-b fayz-glass-divider", children: [
|
|
1622
|
-
{ id: "appointment", label:
|
|
1623
|
-
{ id: "client", label:
|
|
1624
|
-
{ id: "financial", label:
|
|
1380
|
+
{ id: "appointment", label: t2(`agenda.bookingType.${bookingType}`) || activeType.label, icon: Calendar, show: true },
|
|
1381
|
+
{ id: "client", label: t2("agenda.appointment.clientTab"), icon: User, show: !!config.clientEntityDef && !!clientId },
|
|
1382
|
+
{ id: "financial", label: t2("agenda.payment.title"), icon: DollarSign, show: config.modules.financial && !!editOrderId }
|
|
1625
1383
|
].filter((tab) => tab.show).map((tab) => {
|
|
1626
1384
|
const isActive = modalTab === tab.id;
|
|
1627
1385
|
return /* @__PURE__ */ jsxs(
|
|
@@ -1640,7 +1398,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
1640
1398
|
},
|
|
1641
1399
|
tab.id
|
|
1642
1400
|
);
|
|
1643
|
-
}) }) : /* @__PURE__ */ jsx("h2", { className: "text-sm font-semibold", children:
|
|
1401
|
+
}) }) : /* @__PURE__ */ jsx("h2", { className: "text-sm font-semibold", children: t2(`agenda.bookingType.${bookingType}`) || activeType.label })),
|
|
1644
1402
|
modalTab === "appointment" && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1645
1403
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
1646
1404
|
/* @__PURE__ */ jsx(Clock, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
|
|
@@ -1698,7 +1456,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
1698
1456
|
] }),
|
|
1699
1457
|
conflict && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 rounded-lg bg-destructive/10 px-3 py-1.5 text-xs text-destructive", children: [
|
|
1700
1458
|
/* @__PURE__ */ jsx(AlertTriangle, { className: "h-3.5 w-3.5 shrink-0" }),
|
|
1701
|
-
|
|
1459
|
+
t2("agenda.appointment.conflict")
|
|
1702
1460
|
] })
|
|
1703
1461
|
] })
|
|
1704
1462
|
] }),
|
|
@@ -1739,7 +1497,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
1739
1497
|
onBack: () => setModalTab("appointment"),
|
|
1740
1498
|
onEdit: () => setClientEditing(true)
|
|
1741
1499
|
}
|
|
1742
|
-
) : /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground py-4 text-center", children:
|
|
1500
|
+
) : /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground py-4 text-center", children: t2("agenda.appointment.noClientData") }) }),
|
|
1743
1501
|
modalTab === "financial" && editOrderId && config.financialBridge && /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto", children: /* @__PURE__ */ jsx(
|
|
1744
1502
|
BookingPaymentPanel,
|
|
1745
1503
|
{
|
|
@@ -1776,7 +1534,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
1776
1534
|
type: "text",
|
|
1777
1535
|
value: title,
|
|
1778
1536
|
onChange: (e) => setTitle(e.target.value),
|
|
1779
|
-
placeholder:
|
|
1537
|
+
placeholder: t2("agenda.appointment.addTitle"),
|
|
1780
1538
|
autoFocus: mode === "create",
|
|
1781
1539
|
className: "w-full flex-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-2.5 py-1.5 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-ring"
|
|
1782
1540
|
}
|
|
@@ -1784,133 +1542,25 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
1784
1542
|
] }),
|
|
1785
1543
|
activeType.fields.client && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 py-1", children: [
|
|
1786
1544
|
/* @__PURE__ */ jsx(User, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
|
|
1787
|
-
/* @__PURE__ */ jsx("div", { className: "flex-1", children:
|
|
1788
|
-
|
|
1789
|
-
clientPhone && /* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground shrink-0", children: clientPhone }),
|
|
1790
|
-
!isPaidBooking && /* @__PURE__ */ jsx(
|
|
1791
|
-
"button",
|
|
1792
|
-
{
|
|
1793
|
-
type: "button",
|
|
1794
|
-
onClick: () => {
|
|
1795
|
-
setClientId("");
|
|
1796
|
-
setClientSearch("");
|
|
1797
|
-
setClientPhone("");
|
|
1798
|
-
},
|
|
1799
|
-
className: "p-0.5 text-muted-foreground hover:text-destructive shrink-0",
|
|
1800
|
-
children: /* @__PURE__ */ jsx(X, { className: "h-3 w-3" })
|
|
1801
|
-
}
|
|
1802
|
-
)
|
|
1803
|
-
] }) : !quickCreate ? /* @__PURE__ */ jsx(
|
|
1804
|
-
SearchCombobox,
|
|
1545
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsx(
|
|
1546
|
+
ContactPicker,
|
|
1805
1547
|
{
|
|
1806
|
-
value: clientSearch,
|
|
1807
|
-
onChange: (
|
|
1808
|
-
|
|
1809
|
-
|
|
1548
|
+
value: clientId || clientSearch ? { id: clientId || void 0, name: clientSearch, phone: clientPhone } : null,
|
|
1549
|
+
onChange: (contact) => {
|
|
1550
|
+
setClientId(contact?.id ?? "");
|
|
1551
|
+
setClientSearch(contact?.name ?? "");
|
|
1552
|
+
setClientPhone(contact?.phone ?? "");
|
|
1810
1553
|
},
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
setNewClientName(name);
|
|
1820
|
-
setNewClientPhone("");
|
|
1821
|
-
setNewClientEmail("");
|
|
1822
|
-
},
|
|
1823
|
-
minimal: true,
|
|
1824
|
-
inlineLabel: t("agenda.appointment.addClient")
|
|
1554
|
+
kind: config.clientKind,
|
|
1555
|
+
extensionTable: "clients",
|
|
1556
|
+
lookup: config.contactLookup,
|
|
1557
|
+
clearable: !isPaidBooking,
|
|
1558
|
+
variant: "inline",
|
|
1559
|
+
placeholder: t2("agenda.appointment.searchClient"),
|
|
1560
|
+
createLabel: t2("agenda.appointment.newClient"),
|
|
1561
|
+
hint: /* @__PURE__ */ jsx("p", { className: "text-[11px] text-warning", children: t2("agenda.appointment.selectClientHint") })
|
|
1825
1562
|
}
|
|
1826
|
-
)
|
|
1827
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
1828
|
-
/* @__PURE__ */ jsx(
|
|
1829
|
-
"input",
|
|
1830
|
-
{
|
|
1831
|
-
type: "text",
|
|
1832
|
-
value: newClientName,
|
|
1833
|
-
onChange: (e) => setNewClientName(e.target.value),
|
|
1834
|
-
placeholder: t("agenda.appointment.name"),
|
|
1835
|
-
className: "flex-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-2.5 py-1.5 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-ring",
|
|
1836
|
-
autoFocus: true
|
|
1837
|
-
}
|
|
1838
|
-
),
|
|
1839
|
-
/* @__PURE__ */ jsx(
|
|
1840
|
-
"button",
|
|
1841
|
-
{
|
|
1842
|
-
type: "button",
|
|
1843
|
-
onClick: () => setQuickCreate(false),
|
|
1844
|
-
className: "p-1 text-muted-foreground hover:text-foreground shrink-0",
|
|
1845
|
-
children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5" })
|
|
1846
|
-
}
|
|
1847
|
-
)
|
|
1848
|
-
] }),
|
|
1849
|
-
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
|
|
1850
|
-
/* @__PURE__ */ jsx(
|
|
1851
|
-
"input",
|
|
1852
|
-
{
|
|
1853
|
-
type: "tel",
|
|
1854
|
-
value: newClientPhone,
|
|
1855
|
-
onChange: (e) => setNewClientPhone(e.target.value),
|
|
1856
|
-
placeholder: t("agenda.appointment.phone"),
|
|
1857
|
-
className: "rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-2.5 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
1858
|
-
}
|
|
1859
|
-
),
|
|
1860
|
-
/* @__PURE__ */ jsx(
|
|
1861
|
-
"input",
|
|
1862
|
-
{
|
|
1863
|
-
type: "email",
|
|
1864
|
-
value: newClientEmail,
|
|
1865
|
-
onChange: (e) => setNewClientEmail(e.target.value),
|
|
1866
|
-
placeholder: t("agenda.appointment.emailOptional"),
|
|
1867
|
-
className: "rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-2.5 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
1868
|
-
}
|
|
1869
|
-
)
|
|
1870
|
-
] }),
|
|
1871
|
-
/* @__PURE__ */ jsx(
|
|
1872
|
-
"button",
|
|
1873
|
-
{
|
|
1874
|
-
type: "button",
|
|
1875
|
-
disabled: creatingClient || !newClientName.trim(),
|
|
1876
|
-
onClick: async () => {
|
|
1877
|
-
setCreatingClient(true);
|
|
1878
|
-
try {
|
|
1879
|
-
const { getSupabaseClientOptional: getSupabaseClientOptional4 } = await import('@fayz-ai/core');
|
|
1880
|
-
const { getAgendaTenantId: getAgendaTenantId2 } = await import('./tenant-LAUWVUYB.js');
|
|
1881
|
-
const supabase = getSupabaseClientOptional4();
|
|
1882
|
-
const tenantId = getAgendaTenantId2();
|
|
1883
|
-
if (!supabase || !tenantId) throw new Error("Not initialized");
|
|
1884
|
-
const { data: personRow, error: personError } = await supabase.from("people").insert({
|
|
1885
|
-
tenant_id: tenantId,
|
|
1886
|
-
kind: config.clientKind,
|
|
1887
|
-
name: newClientName.trim(),
|
|
1888
|
-
phone: newClientPhone.trim() || null,
|
|
1889
|
-
email: newClientEmail.trim() || null
|
|
1890
|
-
}).select("id, name").single();
|
|
1891
|
-
if (personError) throw personError;
|
|
1892
|
-
const { error: clientError } = await supabase.from("clients").insert({
|
|
1893
|
-
person_id: personRow.id,
|
|
1894
|
-
tenant_id: tenantId
|
|
1895
|
-
});
|
|
1896
|
-
if (clientError) {
|
|
1897
|
-
await supabase.from("people").delete().eq("id", personRow.id);
|
|
1898
|
-
throw clientError;
|
|
1899
|
-
}
|
|
1900
|
-
setClientId(personRow.id);
|
|
1901
|
-
setClientSearch(personRow.name);
|
|
1902
|
-
setQuickCreate(false);
|
|
1903
|
-
} catch (err) {
|
|
1904
|
-
const { toast: toast3 } = await import('@fayz-ai/ui');
|
|
1905
|
-
toast3.error(t("agenda.appointment.createClientFailed"), { description: err?.message });
|
|
1906
|
-
}
|
|
1907
|
-
setCreatingClient(false);
|
|
1908
|
-
},
|
|
1909
|
-
className: "w-full rounded-md bg-primary border border-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90 shadow-button-primary active:shadow-button-inset disabled:opacity-40 transition-colors",
|
|
1910
|
-
children: creatingClient ? t("agenda.appointment.creating") : t("agenda.appointment.create")
|
|
1911
|
-
}
|
|
1912
|
-
)
|
|
1913
|
-
] }) })
|
|
1563
|
+
) })
|
|
1914
1564
|
] }),
|
|
1915
1565
|
activeType.fields.location && config.modules.locationSelection && (storeLocations.length > 0 || config.locations.length > 0) && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 py-1", children: [
|
|
1916
1566
|
/* @__PURE__ */ jsx(MapPin, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
|
|
@@ -1938,7 +1588,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
1938
1588
|
step: 5,
|
|
1939
1589
|
className: "w-10 shrink-0",
|
|
1940
1590
|
disabled: isPaidBooking,
|
|
1941
|
-
tooltip:
|
|
1591
|
+
tooltip: t2("agenda.appointment.duration")
|
|
1942
1592
|
}
|
|
1943
1593
|
),
|
|
1944
1594
|
/* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground", children: "min" })
|
|
@@ -1954,7 +1604,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
1954
1604
|
step: 0.01,
|
|
1955
1605
|
className: "w-14 shrink-0",
|
|
1956
1606
|
disabled: isPaidBooking,
|
|
1957
|
-
tooltip:
|
|
1607
|
+
tooltip: t2("agenda.appointment.price")
|
|
1958
1608
|
}
|
|
1959
1609
|
)
|
|
1960
1610
|
] }),
|
|
@@ -1967,7 +1617,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
1967
1617
|
onChange: setServiceSearch,
|
|
1968
1618
|
onSelect: addService,
|
|
1969
1619
|
options: serviceOptions,
|
|
1970
|
-
placeholder:
|
|
1620
|
+
placeholder: t2("agenda.appointment.searchService"),
|
|
1971
1621
|
onBlurEmpty: () => {
|
|
1972
1622
|
},
|
|
1973
1623
|
onFocus: () => {
|
|
@@ -1984,7 +1634,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
1984
1634
|
] });
|
|
1985
1635
|
},
|
|
1986
1636
|
minimal: true,
|
|
1987
|
-
inlineLabel: services.length > 0 ?
|
|
1637
|
+
inlineLabel: services.length > 0 ? t2("agenda.appointment.addService") : t2("agenda.appointment.addServiceFirst"),
|
|
1988
1638
|
inlineIcon: services.length > 0 ? "plus" : "search"
|
|
1989
1639
|
}
|
|
1990
1640
|
)
|
|
@@ -2005,7 +1655,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
2005
1655
|
disabled: cancellationReasonsLoading,
|
|
2006
1656
|
className: "w-full rounded-input border border-input bg-card px-2.5 py-1.5 text-sm shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] focus:outline-none focus:ring-2 focus:ring-ring",
|
|
2007
1657
|
children: [
|
|
2008
|
-
/* @__PURE__ */ jsx("option", { value: "", children: cancellationReasonsLoading ?
|
|
1658
|
+
/* @__PURE__ */ jsx("option", { value: "", children: cancellationReasonsLoading ? t2("agenda.cancellation.loadingReasons") : t2("agenda.cancellation.selectReason") }),
|
|
2009
1659
|
cancellationReasons.map((reason) => /* @__PURE__ */ jsxs("option", { value: reason.id, children: [
|
|
2010
1660
|
reason.label,
|
|
2011
1661
|
reason.requiresNotes ? " *" : ""
|
|
@@ -2018,7 +1668,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
2018
1668
|
{
|
|
2019
1669
|
value: cancellationNotes,
|
|
2020
1670
|
onChange: (event) => setCancellationNotes(event.target.value),
|
|
2021
|
-
placeholder: needsCancellationNotes ?
|
|
1671
|
+
placeholder: needsCancellationNotes ? t2("agenda.cancellation.notesRequired") : t2("agenda.cancellation.notesOptional"),
|
|
2022
1672
|
rows: 2,
|
|
2023
1673
|
className: "w-full rounded-input border border-input bg-card px-2.5 py-1.5 text-sm resize-none shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] focus:outline-none focus:ring-2 focus:ring-ring"
|
|
2024
1674
|
}
|
|
@@ -2033,7 +1683,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
2033
1683
|
type: "button",
|
|
2034
1684
|
onClick: () => setShowNotes(true),
|
|
2035
1685
|
className: "text-sm text-muted-foreground hover:text-foreground transition-colors text-left py-1.5 pl-2",
|
|
2036
|
-
children:
|
|
1686
|
+
children: t2("agenda.appointment.addNotes")
|
|
2037
1687
|
}
|
|
2038
1688
|
)
|
|
2039
1689
|
] }) : /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3 py-1", children: [
|
|
@@ -2043,7 +1693,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
2043
1693
|
{
|
|
2044
1694
|
value: notes,
|
|
2045
1695
|
onChange: (e) => setNotes(e.target.value),
|
|
2046
|
-
placeholder:
|
|
1696
|
+
placeholder: t2("agenda.appointment.addDescription"),
|
|
2047
1697
|
rows: 2,
|
|
2048
1698
|
autoFocus: true,
|
|
2049
1699
|
className: "w-full flex-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-2.5 py-1.5 text-sm resize-none focus:outline-none focus:ring-2 focus:ring-ring"
|
|
@@ -2053,14 +1703,14 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
2053
1703
|
] })
|
|
2054
1704
|
] }),
|
|
2055
1705
|
modalTab === "appointment" && !clientEditing && /* @__PURE__ */ jsx("div", { className: "px-5 py-2.5 border-t fayz-glass-divider bg-transparent shrink-0 sm:rounded-b-2xl", children: confirmingDelete ? /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
2056
|
-
/* @__PURE__ */ jsx("span", { className: "text-xs text-destructive font-medium", children:
|
|
1706
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-destructive font-medium", children: t2("agenda.appointment.deleteConfirm") }),
|
|
2057
1707
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
2058
|
-
/* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setConfirmingDelete(false), children:
|
|
2059
|
-
/* @__PURE__ */ jsx(Button, { type: "button", variant: "destructive", size: "sm", onClick: handleDelete, disabled: deleting, children: deleting ?
|
|
1708
|
+
/* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setConfirmingDelete(false), children: t2("agenda.appointment.cancel") }),
|
|
1709
|
+
/* @__PURE__ */ jsx(Button, { type: "button", variant: "destructive", size: "sm", onClick: handleDelete, disabled: deleting, children: deleting ? t2("agenda.appointment.deleting") : t2("agenda.appointment.confirmDelete") })
|
|
2060
1710
|
] })
|
|
2061
1711
|
] }) : /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
2062
1712
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
2063
|
-
mode === "edit" && /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setConfirmingDelete(true), className: "text-xs text-destructive font-medium hover:underline", children:
|
|
1713
|
+
mode === "edit" && /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setConfirmingDelete(true), className: "text-xs text-destructive font-medium hover:underline", children: t2("agenda.appointment.delete") }),
|
|
2064
1714
|
services.length > 0 && /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [
|
|
2065
1715
|
/* @__PURE__ */ jsx(Clock, { className: "h-3 w-3" }),
|
|
2066
1716
|
totalDuration,
|
|
@@ -2078,7 +1728,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
2078
1728
|
/* @__PURE__ */ jsx(Check, { className: "h-2.5 w-2.5" }),
|
|
2079
1729
|
fmtCurrency(totalPrice, currency.locale, currency.code),
|
|
2080
1730
|
" \xB7 ",
|
|
2081
|
-
|
|
1731
|
+
t2("agenda.payment.paid")
|
|
2082
1732
|
]
|
|
2083
1733
|
}
|
|
2084
1734
|
) : date <= (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) ? /* @__PURE__ */ jsxs(
|
|
@@ -2094,7 +1744,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
2094
1744
|
/* @__PURE__ */ jsx(HandCoins, { className: "h-3 w-3" }),
|
|
2095
1745
|
fmtCurrency(totalPrice, currency.locale, currency.code),
|
|
2096
1746
|
" \xB7 ",
|
|
2097
|
-
|
|
1747
|
+
t2("agenda.payment.collect")
|
|
2098
1748
|
]
|
|
2099
1749
|
}
|
|
2100
1750
|
) : /* @__PURE__ */ jsxs("span", { children: [
|
|
@@ -2107,8 +1757,8 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
2107
1757
|
] })
|
|
2108
1758
|
] }),
|
|
2109
1759
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
2110
|
-
/* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: onClose, className: "hidden sm:inline-flex", children:
|
|
2111
|
-
/* @__PURE__ */ jsx(Button, { type: "submit", variant: "default", size: "sm", disabled: !canSubmit || !isDirty, children: saving ?
|
|
1760
|
+
/* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: onClose, className: "hidden sm:inline-flex", children: t2("agenda.appointment.cancel") }),
|
|
1761
|
+
/* @__PURE__ */ jsx(Button, { type: "submit", variant: "default", size: "sm", disabled: !canSubmit || !isDirty, children: saving ? t2("agenda.appointment.saving") : mode === "create" ? t2("agenda.appointment.save") : t2("agenda.appointment.update") })
|
|
2112
1762
|
] })
|
|
2113
1763
|
] }) })
|
|
2114
1764
|
] }),
|
|
@@ -2123,7 +1773,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
2123
1773
|
className: "inline-flex items-center gap-1 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors",
|
|
2124
1774
|
children: [
|
|
2125
1775
|
/* @__PURE__ */ jsx("svg", { className: "h-3 w-3", viewBox: "0 0 16 16", fill: "none", children: /* @__PURE__ */ jsx("path", { d: "M10 12L6 8l4-4", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) }),
|
|
2126
|
-
|
|
1776
|
+
t2("agenda.payment.backToAppointment")
|
|
2127
1777
|
]
|
|
2128
1778
|
}
|
|
2129
1779
|
) }) })
|
|
@@ -2275,7 +1925,7 @@ function PopoverStatusSelect({ value, statuses, bookingStartsAt, onChange }) {
|
|
|
2275
1925
|
] });
|
|
2276
1926
|
}
|
|
2277
1927
|
function AppointmentPopover({ booking, position, onClose, onEdit }) {
|
|
2278
|
-
const
|
|
1928
|
+
const t2 = useTranslation();
|
|
2279
1929
|
const config = useAgendaConfig();
|
|
2280
1930
|
const deleteBooking = useAgendaStore((s) => s.deleteBooking);
|
|
2281
1931
|
const updateStatus = useAgendaStore((s) => s.updateBookingStatus);
|
|
@@ -2296,13 +1946,13 @@ function AppointmentPopover({ booking, position, onClose, onEdit }) {
|
|
|
2296
1946
|
}
|
|
2297
1947
|
return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx(FloatingPanel, { position, width: 320, height: 340, onClose, children: (panel) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2298
1948
|
/* @__PURE__ */ jsx("div", { className: "flex items-center justify-end gap-0.5 px-3 pt-3 pb-1", children: confirmingDelete ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2299
|
-
/* @__PURE__ */ jsx("span", { className: "text-xs text-destructive font-medium mr-auto pl-1", children:
|
|
1949
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-destructive font-medium mr-auto pl-1", children: t2("agenda.appointment.deleteConfirm") }),
|
|
2300
1950
|
/* @__PURE__ */ jsx(
|
|
2301
1951
|
"button",
|
|
2302
1952
|
{
|
|
2303
1953
|
onClick: () => setConfirmingDelete(false),
|
|
2304
1954
|
className: "rounded px-2 py-0.5 text-[11px] font-medium hover:bg-muted/50 transition-colors",
|
|
2305
|
-
children:
|
|
1955
|
+
children: t2("agenda.appointment.cancel")
|
|
2306
1956
|
}
|
|
2307
1957
|
),
|
|
2308
1958
|
/* @__PURE__ */ jsx(
|
|
@@ -2311,7 +1961,7 @@ function AppointmentPopover({ booking, position, onClose, onEdit }) {
|
|
|
2311
1961
|
onClick: handleDelete,
|
|
2312
1962
|
disabled: deleting,
|
|
2313
1963
|
className: "rounded bg-destructive border border-destructive px-2 py-0.5 text-[11px] font-medium text-destructive-foreground hover:bg-destructive/90 shadow-button-primary active:shadow-button-inset disabled:opacity-40 transition-colors",
|
|
2314
|
-
children: deleting ?
|
|
1964
|
+
children: deleting ? t2("agenda.appointment.deleting") : t2("agenda.appointment.confirmDelete")
|
|
2315
1965
|
}
|
|
2316
1966
|
)
|
|
2317
1967
|
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
@@ -2320,7 +1970,7 @@ function AppointmentPopover({ booking, position, onClose, onEdit }) {
|
|
|
2320
1970
|
{
|
|
2321
1971
|
onClick: () => panel.expandThen(() => onEdit(booking.id)),
|
|
2322
1972
|
className: "p-1.5 rounded-full hover:bg-muted transition-colors",
|
|
2323
|
-
title:
|
|
1973
|
+
title: t2("common.edit"),
|
|
2324
1974
|
children: /* @__PURE__ */ jsx(Pencil, { className: "h-4 w-4 text-muted-foreground" })
|
|
2325
1975
|
}
|
|
2326
1976
|
),
|
|
@@ -2329,7 +1979,7 @@ function AppointmentPopover({ booking, position, onClose, onEdit }) {
|
|
|
2329
1979
|
{
|
|
2330
1980
|
onClick: () => setConfirmingDelete(true),
|
|
2331
1981
|
className: "p-1.5 rounded-full hover:bg-destructive/10 transition-colors",
|
|
2332
|
-
title:
|
|
1982
|
+
title: t2("common.delete"),
|
|
2333
1983
|
children: /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4 text-muted-foreground hover:text-destructive" })
|
|
2334
1984
|
}
|
|
2335
1985
|
),
|
|
@@ -2339,7 +1989,7 @@ function AppointmentPopover({ booking, position, onClose, onEdit }) {
|
|
|
2339
1989
|
/* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
|
|
2340
1990
|
/* @__PURE__ */ jsx("span", { className: "mt-1.5 h-3 w-3 rounded shrink-0", style: { backgroundColor: color } }),
|
|
2341
1991
|
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
2342
|
-
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 ||
|
|
1992
|
+
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") }),
|
|
2343
1993
|
/* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground mt-0.5", children: [
|
|
2344
1994
|
fmtDate(booking.startsAt),
|
|
2345
1995
|
" \xB7 ",
|
|
@@ -2377,22 +2027,22 @@ function AppointmentPopover({ booking, position, onClose, onEdit }) {
|
|
|
2377
2027
|
const hasFinancial = config.modules.financial && booking.orderId;
|
|
2378
2028
|
if (!hasFinancial) {
|
|
2379
2029
|
return /* @__PURE__ */ jsxs("div", { className: "pt-2 border-t fayz-glass-divider flex items-center justify-between text-sm", children: [
|
|
2380
|
-
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children:
|
|
2030
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: t2("agenda.appointment.total") }),
|
|
2381
2031
|
/* @__PURE__ */ jsx("span", { className: "font-medium", children: total })
|
|
2382
2032
|
] });
|
|
2383
2033
|
}
|
|
2384
2034
|
const badgeConfig = {
|
|
2385
|
-
none: { icon: HandCoins, bg: "bg-warning/10 hover:bg-warning/15", text: "text-warning", label: `${total} \xB7 ${
|
|
2386
|
-
pending: { icon: Clock, bg: "bg-warning/10 hover:bg-warning/15", text: "text-warning", label: `${total} \xB7 ${
|
|
2387
|
-
partial: { icon: DollarSign, bg: "bg-warning/10 hover:bg-warning/15", text: "text-warning", label: `${total} \xB7 ${
|
|
2388
|
-
paid: { icon: Check, bg: "bg-success/10 hover:bg-success/15", text: "text-success", label: `${total} \xB7 ${
|
|
2389
|
-
overdue: { icon: AlertTriangle, bg: "bg-destructive/10 hover:bg-destructive/15", text: "text-destructive", label: `${total} \xB7 ${
|
|
2390
|
-
cancelled: { icon: X, bg: "bg-muted hover:bg-muted/80", text: "text-muted-foreground", label: `${total} \xB7 ${
|
|
2035
|
+
none: { icon: HandCoins, bg: "bg-warning/10 hover:bg-warning/15", text: "text-warning", label: `${total} \xB7 ${t2("agenda.payment.collect")}` },
|
|
2036
|
+
pending: { icon: Clock, bg: "bg-warning/10 hover:bg-warning/15", text: "text-warning", label: `${total} \xB7 ${t2("agenda.payment.pending")}` },
|
|
2037
|
+
partial: { icon: DollarSign, bg: "bg-warning/10 hover:bg-warning/15", text: "text-warning", label: `${total} \xB7 ${t2("agenda.payment.partial")}` },
|
|
2038
|
+
paid: { icon: Check, bg: "bg-success/10 hover:bg-success/15", text: "text-success", label: `${total} \xB7 ${t2("agenda.payment.paid")}` },
|
|
2039
|
+
overdue: { icon: AlertTriangle, bg: "bg-destructive/10 hover:bg-destructive/15", text: "text-destructive", label: `${total} \xB7 ${t2("agenda.payment.overdue")}` },
|
|
2040
|
+
cancelled: { icon: X, bg: "bg-muted hover:bg-muted/80", text: "text-muted-foreground", label: `${total} \xB7 ${t2("agenda.payment.cancelled")}` }
|
|
2391
2041
|
};
|
|
2392
2042
|
const cfg = badgeConfig[ps] ?? badgeConfig.none;
|
|
2393
2043
|
const StatusIcon = cfg.icon;
|
|
2394
2044
|
return /* @__PURE__ */ jsxs("div", { className: "pt-2 border-t fayz-glass-divider flex items-center justify-between", children: [
|
|
2395
|
-
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children:
|
|
2045
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t2("agenda.appointment.total") }),
|
|
2396
2046
|
/* @__PURE__ */ jsxs(
|
|
2397
2047
|
"button",
|
|
2398
2048
|
{
|
|
@@ -2464,7 +2114,7 @@ function StatusDot({ color, label, isActive, disabled, onClick }) {
|
|
|
2464
2114
|
] });
|
|
2465
2115
|
}
|
|
2466
2116
|
function EventContextMenu({ booking, position, onClose, onEdit }) {
|
|
2467
|
-
const
|
|
2117
|
+
const t2 = useTranslation();
|
|
2468
2118
|
const config = useAgendaConfig();
|
|
2469
2119
|
const updateStatus = useAgendaStore((s) => s.updateBookingStatus);
|
|
2470
2120
|
const deleteBooking = useAgendaStore((s) => s.deleteBooking);
|
|
@@ -2489,19 +2139,19 @@ function EventContextMenu({ booking, position, onClose, onEdit }) {
|
|
|
2489
2139
|
children: [
|
|
2490
2140
|
/* @__PURE__ */ jsx(Pencil, { className: "h-4 w-4 text-muted-foreground" }),
|
|
2491
2141
|
" ",
|
|
2492
|
-
|
|
2142
|
+
t2("agenda.contextMenu.edit")
|
|
2493
2143
|
]
|
|
2494
2144
|
}
|
|
2495
2145
|
),
|
|
2496
2146
|
confirming ? /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-4 py-2", children: [
|
|
2497
|
-
/* @__PURE__ */ jsx("span", { className: "text-xs text-destructive font-medium", children:
|
|
2147
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-destructive font-medium", children: t2("agenda.appointment.deleteConfirm") }),
|
|
2498
2148
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
2499
2149
|
/* @__PURE__ */ jsx(
|
|
2500
2150
|
"button",
|
|
2501
2151
|
{
|
|
2502
2152
|
onClick: () => setConfirming(false),
|
|
2503
2153
|
className: "rounded px-2 py-0.5 text-[11px] font-medium hover:bg-muted/50 transition-colors",
|
|
2504
|
-
children:
|
|
2154
|
+
children: t2("agenda.appointment.cancel")
|
|
2505
2155
|
}
|
|
2506
2156
|
),
|
|
2507
2157
|
/* @__PURE__ */ jsx(
|
|
@@ -2510,7 +2160,7 @@ function EventContextMenu({ booking, position, onClose, onEdit }) {
|
|
|
2510
2160
|
onClick: handleDelete,
|
|
2511
2161
|
disabled: deleting,
|
|
2512
2162
|
className: "rounded bg-destructive border border-destructive px-2 py-0.5 text-[11px] font-medium text-destructive-foreground hover:bg-destructive/90 shadow-button-primary active:shadow-button-inset disabled:opacity-40 transition-colors",
|
|
2513
|
-
children: deleting ?
|
|
2163
|
+
children: deleting ? t2("agenda.appointment.deleting") : t2("agenda.appointment.confirmDelete")
|
|
2514
2164
|
}
|
|
2515
2165
|
)
|
|
2516
2166
|
] })
|
|
@@ -2522,20 +2172,20 @@ function EventContextMenu({ booking, position, onClose, onEdit }) {
|
|
|
2522
2172
|
children: [
|
|
2523
2173
|
/* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4" }),
|
|
2524
2174
|
" ",
|
|
2525
|
-
|
|
2175
|
+
t2("agenda.contextMenu.delete")
|
|
2526
2176
|
]
|
|
2527
2177
|
}
|
|
2528
2178
|
)
|
|
2529
2179
|
] }),
|
|
2530
2180
|
config.capabilities.status && /* @__PURE__ */ jsxs("div", { className: "border-t px-4 py-3", children: [
|
|
2531
|
-
/* @__PURE__ */ jsx("p", { className: "text-[11px] font-medium text-muted-foreground mb-2", children:
|
|
2181
|
+
/* @__PURE__ */ jsx("p", { className: "text-[11px] font-medium text-muted-foreground mb-2", children: t2("agenda.contextMenu.changeStatus") }),
|
|
2532
2182
|
/* @__PURE__ */ jsx("div", { className: "flex items-center gap-1.5", children: config.statuses.map((s) => {
|
|
2533
2183
|
const available = isStatusAvailable(s, booking.startsAt);
|
|
2534
2184
|
return /* @__PURE__ */ jsx(
|
|
2535
2185
|
StatusDot,
|
|
2536
2186
|
{
|
|
2537
2187
|
color: s.color,
|
|
2538
|
-
label: available ? s.label : `${s.label} ${
|
|
2188
|
+
label: available ? s.label : `${s.label} ${t2("agenda.contextMenu.notAvailable")}`,
|
|
2539
2189
|
isActive: booking.status === s.value,
|
|
2540
2190
|
disabled: !available,
|
|
2541
2191
|
onClick: () => {
|
|
@@ -2561,7 +2211,7 @@ function timeLabel(iso) {
|
|
|
2561
2211
|
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
2562
2212
|
}
|
|
2563
2213
|
function AgendaMobileList({ bookings, statusColors, currencyCode, onSelect }) {
|
|
2564
|
-
const
|
|
2214
|
+
const t2 = useTranslation();
|
|
2565
2215
|
const locale = getCurrentLocale() === "pt-BR" ? "pt-BR" : "en";
|
|
2566
2216
|
const groups = useMemo(() => {
|
|
2567
2217
|
const todayStart = startOfDay(/* @__PURE__ */ new Date()).getTime();
|
|
@@ -2580,12 +2230,12 @@ function AgendaMobileList({ bookings, statusColors, currencyCode, onSelect }) {
|
|
|
2580
2230
|
function dayLabel(date) {
|
|
2581
2231
|
const today = startOfDay(/* @__PURE__ */ new Date());
|
|
2582
2232
|
const diff = Math.round((startOfDay(date).getTime() - today.getTime()) / 864e5);
|
|
2583
|
-
if (diff === 0) return
|
|
2584
|
-
if (diff === 1) return
|
|
2233
|
+
if (diff === 0) return t2("agenda.list.today");
|
|
2234
|
+
if (diff === 1) return t2("agenda.list.tomorrow");
|
|
2585
2235
|
return date.toLocaleDateString(locale, { weekday: "long", day: "numeric", month: "long" });
|
|
2586
2236
|
}
|
|
2587
2237
|
if (groups.length === 0) {
|
|
2588
|
-
return /* @__PURE__ */ jsx("div", { className: "flex-1 flex flex-col items-center justify-center px-6 py-16 text-center text-muted-foreground", children: /* @__PURE__ */ jsx("p", { className: "text-sm", children:
|
|
2238
|
+
return /* @__PURE__ */ jsx("div", { className: "flex-1 flex flex-col items-center justify-center px-6 py-16 text-center text-muted-foreground", children: /* @__PURE__ */ jsx("p", { className: "text-sm", children: t2("agenda.list.empty") }) });
|
|
2589
2239
|
}
|
|
2590
2240
|
return /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto px-3 py-3 space-y-5", children: groups.map((g) => /* @__PURE__ */ jsxs("div", { children: [
|
|
2591
2241
|
/* @__PURE__ */ jsx("h3", { className: "px-1 pb-1.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: dayLabel(g.date) }),
|
|
@@ -2641,8 +2291,8 @@ function generateAvailabilityEvents(schedules, professionals, dateRange, busines
|
|
|
2641
2291
|
list.push(s);
|
|
2642
2292
|
byAssignee.set(aid, list);
|
|
2643
2293
|
}
|
|
2644
|
-
function timeToMinutes(
|
|
2645
|
-
const [h, m] =
|
|
2294
|
+
function timeToMinutes(t2) {
|
|
2295
|
+
const [h, m] = t2.split(":").map(Number);
|
|
2646
2296
|
return h * 60 + m;
|
|
2647
2297
|
}
|
|
2648
2298
|
function minutesToTime(m) {
|
|
@@ -2767,7 +2417,7 @@ function useIsMobile() {
|
|
|
2767
2417
|
function CalendarView() {
|
|
2768
2418
|
injectCalendarStyles();
|
|
2769
2419
|
const config = useAgendaConfig();
|
|
2770
|
-
const
|
|
2420
|
+
const t2 = useTranslation();
|
|
2771
2421
|
const currentLocale = getCurrentLocale();
|
|
2772
2422
|
const fcLocale = currentLocale === "pt-BR" ? ptBrLocale : void 0;
|
|
2773
2423
|
const calendarRef = useRef(null);
|
|
@@ -2865,6 +2515,18 @@ function CalendarView() {
|
|
|
2865
2515
|
end.setDate(end.getDate() + 91);
|
|
2866
2516
|
fetchBookings({ start: start.toISOString(), end: end.toISOString() });
|
|
2867
2517
|
}, [isMobile, fetchBookings]);
|
|
2518
|
+
useDataChanged({}, () => {
|
|
2519
|
+
if (visibleRange) {
|
|
2520
|
+
fetchBookings(visibleRange);
|
|
2521
|
+
return;
|
|
2522
|
+
}
|
|
2523
|
+
const start = /* @__PURE__ */ new Date();
|
|
2524
|
+
start.setHours(0, 0, 0, 0);
|
|
2525
|
+
start.setDate(start.getDate() - 1);
|
|
2526
|
+
const end = new Date(start);
|
|
2527
|
+
end.setDate(end.getDate() + 91);
|
|
2528
|
+
fetchBookings({ start: start.toISOString(), end: end.toISOString() });
|
|
2529
|
+
}, [visibleRange, fetchBookings]);
|
|
2868
2530
|
const resources = useMemo(() => {
|
|
2869
2531
|
const filtered = selectedProfIds.length > 0 ? professionals.filter((p) => selectedProfIds.includes(p.id)) : professionals;
|
|
2870
2532
|
return filtered.map((p) => ({
|
|
@@ -3108,10 +2770,10 @@ function CalendarView() {
|
|
|
3108
2770
|
useEffect(() => {
|
|
3109
2771
|
applyResourceStriping();
|
|
3110
2772
|
const t1 = setTimeout(applyResourceStriping, 100);
|
|
3111
|
-
const
|
|
2773
|
+
const t22 = setTimeout(applyResourceStriping, 300);
|
|
3112
2774
|
return () => {
|
|
3113
2775
|
clearTimeout(t1);
|
|
3114
|
-
clearTimeout(
|
|
2776
|
+
clearTimeout(t22);
|
|
3115
2777
|
};
|
|
3116
2778
|
}, [applyResourceStriping, currentView, visibleRange, bookings]);
|
|
3117
2779
|
useEffect(() => {
|
|
@@ -3199,7 +2861,7 @@ function CalendarView() {
|
|
|
3199
2861
|
className: "flex items-center gap-2 rounded-button bg-primary text-primary-foreground border border-primary pl-3 pr-4 py-1.5 text-sm font-semibold hover:bg-primary/90 shadow-button-primary active:shadow-button-inset transition-all mb-6 w-fit",
|
|
3200
2862
|
children: [
|
|
3201
2863
|
/* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" }),
|
|
3202
|
-
|
|
2864
|
+
t2("agenda.sidebar.create")
|
|
3203
2865
|
]
|
|
3204
2866
|
}
|
|
3205
2867
|
),
|
|
@@ -3274,7 +2936,7 @@ function CalendarView() {
|
|
|
3274
2936
|
{
|
|
3275
2937
|
onClick: handleToday,
|
|
3276
2938
|
className: "rounded-md border px-3 py-1 text-sm font-medium hover:bg-muted bg-card shadow-button active:shadow-button-inset transition-colors",
|
|
3277
|
-
children:
|
|
2939
|
+
children: t2("agenda.toolbar.today")
|
|
3278
2940
|
}
|
|
3279
2941
|
),
|
|
3280
2942
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5", children: [
|
|
@@ -3282,7 +2944,7 @@ function CalendarView() {
|
|
|
3282
2944
|
/* @__PURE__ */ jsx("button", { onClick: handleNext, className: "flex h-7 w-7 items-center justify-center rounded-full hover:bg-muted transition-colors", children: /* @__PURE__ */ jsx(ChevronRight, { className: "h-4 w-4" }) })
|
|
3283
2945
|
] })
|
|
3284
2946
|
] }),
|
|
3285
|
-
/* @__PURE__ */ jsx("h1", { className: "text-base sm:text-lg font-normal truncate", children: isMobile && !mobileGridActive ?
|
|
2947
|
+
/* @__PURE__ */ jsx("h1", { className: "text-base sm:text-lg font-normal truncate", children: isMobile && !mobileGridActive ? t2("agenda.calendar.viewAgenda") : calendarTitle })
|
|
3286
2948
|
] }),
|
|
3287
2949
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 shrink-0", children: [
|
|
3288
2950
|
/* @__PURE__ */ jsx("div", { className: "hidden md:flex items-center gap-1", children: viewOptions.map((opt) => /* @__PURE__ */ jsx(
|
|
@@ -3290,7 +2952,7 @@ function CalendarView() {
|
|
|
3290
2952
|
{
|
|
3291
2953
|
onClick: () => handleViewChange(opt.key),
|
|
3292
2954
|
className: `rounded-md px-3 py-1 text-sm transition-colors ${currentView === opt.key ? "bg-primary/10 text-primary font-medium" : "text-muted-foreground hover:bg-muted/50"}`,
|
|
3293
|
-
children:
|
|
2955
|
+
children: t2(opt.labelKey)
|
|
3294
2956
|
},
|
|
3295
2957
|
opt.key
|
|
3296
2958
|
)) }),
|
|
@@ -3312,7 +2974,7 @@ function CalendarView() {
|
|
|
3312
2974
|
{
|
|
3313
2975
|
onClick: () => handleMobileViewChange(opt.key),
|
|
3314
2976
|
className: `shrink-0 rounded-md px-3 py-1 text-sm transition-colors ${mobileView === opt.key ? "bg-primary/10 text-primary font-medium" : "text-muted-foreground hover:bg-muted/50"}`,
|
|
3315
|
-
children:
|
|
2977
|
+
children: t2(opt.labelKey)
|
|
3316
2978
|
},
|
|
3317
2979
|
opt.key
|
|
3318
2980
|
)) })
|
|
@@ -3403,9 +3065,9 @@ function CalendarView() {
|
|
|
3403
3065
|
{
|
|
3404
3066
|
open: !!scheduleConfirm,
|
|
3405
3067
|
variant: "warning",
|
|
3406
|
-
title:
|
|
3407
|
-
confirmLabel:
|
|
3408
|
-
cancelLabel:
|
|
3068
|
+
title: t2("agenda.calendar.noScheduleConfirm"),
|
|
3069
|
+
confirmLabel: t2("common.confirm"),
|
|
3070
|
+
cancelLabel: t2("common.cancel"),
|
|
3409
3071
|
onConfirm: () => {
|
|
3410
3072
|
const cb = scheduleConfirm?.onConfirm;
|
|
3411
3073
|
setScheduleConfirm(null);
|
|
@@ -3452,10 +3114,19 @@ function CalendarView() {
|
|
|
3452
3114
|
)
|
|
3453
3115
|
] });
|
|
3454
3116
|
}
|
|
3117
|
+
|
|
3118
|
+
// src/lib/tenant.ts
|
|
3119
|
+
var t;
|
|
3120
|
+
function setAgendaTenantId(id) {
|
|
3121
|
+
t = id;
|
|
3122
|
+
}
|
|
3123
|
+
function getAgendaTenantId() {
|
|
3124
|
+
return t;
|
|
3125
|
+
}
|
|
3455
3126
|
function AgendaPage({ config, provider: provider2, store }) {
|
|
3456
3127
|
const runtime = usePluginRuntimeOptional();
|
|
3457
3128
|
const tenantId = runtime?.context.tenant?.id;
|
|
3458
|
-
|
|
3129
|
+
React13.useEffect(() => {
|
|
3459
3130
|
setAgendaTenantId(tenantId);
|
|
3460
3131
|
return () => setAgendaTenantId(void 0);
|
|
3461
3132
|
}, [tenantId]);
|
|
@@ -3570,8 +3241,10 @@ async function hydrateCanonicalBookings(core, bookings) {
|
|
|
3570
3241
|
locationId: booking.location_id,
|
|
3571
3242
|
metadata: booking.metadata ?? {},
|
|
3572
3243
|
clientId: booking.party_id,
|
|
3573
|
-
// Simple events (no party) fall back to the title stored in metadata
|
|
3574
|
-
|
|
3244
|
+
// Simple events (no party) fall back to the title stored in metadata;
|
|
3245
|
+
// partyless blocks/tasks (e.g. imported Google events) get their notes or
|
|
3246
|
+
// a kind label so the calendar never renders an unnamed chip.
|
|
3247
|
+
clientName: client?.name ?? booking.metadata?.title ?? (booking.kind === "appointment" ? null : booking.notes || (booking.kind === "task" ? "Tarefa" : "Bloqueio")),
|
|
3575
3248
|
clientPhone: client?.phone ?? null,
|
|
3576
3249
|
clientEmail: client?.email ?? null,
|
|
3577
3250
|
clientAvatarUrl: client?.avatar_url ?? null,
|
|
@@ -3611,7 +3284,8 @@ async function getCanonicalBookingById(core, id) {
|
|
|
3611
3284
|
const [booking] = await hydrateCanonicalBookings(core, [data]);
|
|
3612
3285
|
return booking ?? null;
|
|
3613
3286
|
}
|
|
3614
|
-
function createSupabaseAgendaProvider() {
|
|
3287
|
+
function createSupabaseAgendaProvider(providerOptions) {
|
|
3288
|
+
const professionalKind = providerOptions?.professionalKind ?? "staff";
|
|
3615
3289
|
return {
|
|
3616
3290
|
// v_appointments is in public schema (PostgREST default)
|
|
3617
3291
|
async getBookings(query) {
|
|
@@ -3632,7 +3306,26 @@ function createSupabaseAgendaProvider() {
|
|
|
3632
3306
|
const { core, pub } = getClients();
|
|
3633
3307
|
const { data, error } = await pub.from("v_appointments").select("*").eq("id", id).maybeSingle();
|
|
3634
3308
|
if (error) throw error;
|
|
3635
|
-
if (data)
|
|
3309
|
+
if (data) {
|
|
3310
|
+
const booking = mapBooking(data);
|
|
3311
|
+
if (booking.orderId) {
|
|
3312
|
+
const { data: items } = await core.from("order_items").select("id, service_id, name, duration_minutes, unit_price, assignee_id, sort_order").eq("order_id", booking.orderId).order("sort_order", { ascending: true });
|
|
3313
|
+
if (items && items.length) {
|
|
3314
|
+
const services = items.map((item) => ({
|
|
3315
|
+
id: item.id,
|
|
3316
|
+
serviceId: item.service_id,
|
|
3317
|
+
name: item.name,
|
|
3318
|
+
durationMinutes: item.duration_minutes,
|
|
3319
|
+
price: item.unit_price,
|
|
3320
|
+
assigneeId: item.assignee_id
|
|
3321
|
+
}));
|
|
3322
|
+
booking.services = services;
|
|
3323
|
+
const itemDuration = services.reduce((sum, s) => sum + (s.durationMinutes ?? 0), 0);
|
|
3324
|
+
if (itemDuration) booking.totalDurationMinutes = itemDuration;
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
return booking;
|
|
3328
|
+
}
|
|
3636
3329
|
return getCanonicalBookingById(core, id);
|
|
3637
3330
|
},
|
|
3638
3331
|
async createBooking(input) {
|
|
@@ -3655,7 +3348,7 @@ function createSupabaseAgendaProvider() {
|
|
|
3655
3348
|
kind: input.kind ?? "appointment",
|
|
3656
3349
|
status: "scheduled",
|
|
3657
3350
|
party_id: input.clientId || null,
|
|
3658
|
-
assignee_id: input.professionalId,
|
|
3351
|
+
assignee_id: input.professionalId || null,
|
|
3659
3352
|
location_id: input.locationId ?? null,
|
|
3660
3353
|
subtotal: totalPrice,
|
|
3661
3354
|
total: totalPrice,
|
|
@@ -3672,7 +3365,7 @@ function createSupabaseAgendaProvider() {
|
|
|
3672
3365
|
tenant_id: tenantId,
|
|
3673
3366
|
kind: input.kind ?? "appointment",
|
|
3674
3367
|
party_id: input.clientId || null,
|
|
3675
|
-
assignee_id: input.professionalId,
|
|
3368
|
+
assignee_id: input.professionalId || null,
|
|
3676
3369
|
location_id: input.locationId ?? null,
|
|
3677
3370
|
order_id: order.id,
|
|
3678
3371
|
starts_at: input.startsAt,
|
|
@@ -3697,7 +3390,7 @@ function createSupabaseAgendaProvider() {
|
|
|
3697
3390
|
total: s.price,
|
|
3698
3391
|
sort_order: i,
|
|
3699
3392
|
duration_minutes: s.durationMinutes,
|
|
3700
|
-
assignee_id: s.assigneeId
|
|
3393
|
+
assignee_id: s.assigneeId || input.professionalId || null
|
|
3701
3394
|
}));
|
|
3702
3395
|
const { error: oiErr } = await core.from("order_items").insert(orderItems);
|
|
3703
3396
|
if (oiErr) throw oiErr;
|
|
@@ -3777,9 +3470,12 @@ function createSupabaseAgendaProvider() {
|
|
|
3777
3470
|
const { data: booking, error: bookingErr } = await core.from("appointments").select("order_id").eq("id", id).single();
|
|
3778
3471
|
if (bookingErr) throw bookingErr;
|
|
3779
3472
|
const orderId = booking?.order_id;
|
|
3780
|
-
|
|
3781
|
-
if (
|
|
3782
|
-
|
|
3473
|
+
let movementCount = 0;
|
|
3474
|
+
if (orderId) {
|
|
3475
|
+
const { count, error: movementErr } = await pub.from("plg_financial_movements").select("id", { count: "exact", head: true }).eq("invoice_id", orderId);
|
|
3476
|
+
if (!movementErr) movementCount = count ?? 0;
|
|
3477
|
+
}
|
|
3478
|
+
if (orderId && movementCount > 0) {
|
|
3783
3479
|
await pub.from("plg_financial_movements").update({ status: "cancelled" }).eq("invoice_id", orderId);
|
|
3784
3480
|
await core.from("orders").update({ status: "cancelled" }).eq("id", orderId);
|
|
3785
3481
|
const { error } = await core.from("appointments").update({ status: "cancelled" }).eq("id", id);
|
|
@@ -3856,7 +3552,7 @@ function createSupabaseAgendaProvider() {
|
|
|
3856
3552
|
},
|
|
3857
3553
|
async getProfessionals() {
|
|
3858
3554
|
const { core } = getClients();
|
|
3859
|
-
const { data, error } = await core.from("people").select("id, name, avatar_url, is_active").eq("kind",
|
|
3555
|
+
const { data, error } = await core.from("people").select("id, name, avatar_url, is_active").eq("kind", professionalKind).eq("is_active", true).order("name");
|
|
3860
3556
|
if (error) throw error;
|
|
3861
3557
|
return (data ?? []).map((p) => ({
|
|
3862
3558
|
id: p.id,
|
|
@@ -4138,40 +3834,49 @@ var HOUR_OPTIONS = Array.from({ length: 18 }, (_, i) => {
|
|
|
4138
3834
|
const h = (5 + i).toString().padStart(2, "0");
|
|
4139
3835
|
return { value: `${h}:00`, label: `${h}:00` };
|
|
4140
3836
|
});
|
|
3837
|
+
var AGENDA_TOGGLE_DEFAULTS = {
|
|
3838
|
+
conflictDetection: true,
|
|
3839
|
+
dragDrop: true,
|
|
3840
|
+
autoCreateOrder: true,
|
|
3841
|
+
enableLocation: false,
|
|
3842
|
+
enableWorkingHours: true,
|
|
3843
|
+
blockOutside: false,
|
|
3844
|
+
enableConfirmations: true,
|
|
3845
|
+
autoConfirm: false,
|
|
3846
|
+
reminder: true,
|
|
3847
|
+
showCancelled: false,
|
|
3848
|
+
showNoShow: true,
|
|
3849
|
+
compact: false,
|
|
3850
|
+
newBookingNotif: true,
|
|
3851
|
+
cancelAlerts: true,
|
|
3852
|
+
noShowTracking: false
|
|
3853
|
+
};
|
|
4141
3854
|
function AgendaGeneralSettings() {
|
|
4142
|
-
const
|
|
3855
|
+
const t2 = useTranslation();
|
|
4143
3856
|
const { startTime, endTime, setStartTime, setEndTime } = useAgendaSettings();
|
|
3857
|
+
const s = useTenantPluginSettings("agenda", AGENDA_TOGGLE_DEFAULTS);
|
|
4144
3858
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
4145
|
-
/* @__PURE__ */ jsxs(SettingsGroup, { title:
|
|
4146
|
-
/* @__PURE__ */ jsx(ToggleRow, { label:
|
|
4147
|
-
|
|
4148
|
-
/* @__PURE__ */ jsx(ToggleRow, { label:
|
|
4149
|
-
} }),
|
|
4150
|
-
/* @__PURE__ */ jsx(ToggleRow, { label: t("agenda.settings.autoCreateOrder"), description: t("agenda.settings.autoCreateOrderDesc"), checked: true, onChange: () => {
|
|
4151
|
-
} })
|
|
3859
|
+
/* @__PURE__ */ jsxs(SettingsGroup, { title: t2("agenda.settings.scheduling"), description: t2("agenda.settings.schedulingDesc"), children: [
|
|
3860
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: t2("agenda.settings.conflictDetection"), description: t2("agenda.settings.conflictDetectionDesc"), checked: s.get("conflictDetection"), onChange: (v) => s.set("conflictDetection", v) }),
|
|
3861
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: t2("agenda.settings.dragDrop"), description: t2("agenda.settings.dragDropDesc"), checked: s.get("dragDrop"), onChange: (v) => s.set("dragDrop", v) }),
|
|
3862
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: t2("agenda.settings.autoCreateOrder"), description: t2("agenda.settings.autoCreateOrderDesc"), checked: s.get("autoCreateOrder"), onChange: (v) => s.set("autoCreateOrder", v) })
|
|
4152
3863
|
] }),
|
|
4153
|
-
/* @__PURE__ */ jsx(SettingsGroup, { title:
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
/* @__PURE__ */ jsx(ToggleRow, { label:
|
|
4157
|
-
} }),
|
|
4158
|
-
/* @__PURE__ */ jsx(ToggleRow, { label: t("agenda.settings.blockOutside"), description: t("agenda.settings.blockOutsideDesc"), checked: false, onChange: () => {
|
|
4159
|
-
} })
|
|
3864
|
+
/* @__PURE__ */ jsx(SettingsGroup, { title: t2("agenda.settings.locations"), description: t2("agenda.settings.locationsDesc"), children: /* @__PURE__ */ jsx(ToggleRow, { label: t2("agenda.settings.enableLocation"), description: t2("agenda.settings.enableLocationDesc"), checked: s.get("enableLocation"), onChange: (v) => s.set("enableLocation", v) }) }),
|
|
3865
|
+
/* @__PURE__ */ jsxs(SettingsGroup, { title: t2("agenda.settings.workingHours"), description: t2("agenda.settings.workingHoursDesc"), children: [
|
|
3866
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: t2("agenda.settings.enableWorkingHours"), description: t2("agenda.settings.enableWorkingHoursDesc"), checked: s.get("enableWorkingHours"), onChange: (v) => s.set("enableWorkingHours", v) }),
|
|
3867
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: t2("agenda.settings.blockOutside"), description: t2("agenda.settings.blockOutsideDesc"), checked: s.get("blockOutside"), onChange: (v) => s.set("blockOutside", v) })
|
|
4160
3868
|
] }),
|
|
4161
|
-
/* @__PURE__ */ jsxs(SettingsGroup, { title:
|
|
4162
|
-
/* @__PURE__ */ jsx(ToggleRow, { label:
|
|
4163
|
-
|
|
4164
|
-
/* @__PURE__ */ jsx(ToggleRow, { label:
|
|
4165
|
-
} }),
|
|
4166
|
-
/* @__PURE__ */ jsx(ToggleRow, { label: t("agenda.settings.reminder"), description: t("agenda.settings.reminderDesc"), checked: true, onChange: () => {
|
|
4167
|
-
} })
|
|
3869
|
+
/* @__PURE__ */ jsxs(SettingsGroup, { title: t2("agenda.settings.confirmations"), description: t2("agenda.settings.confirmationsDesc"), children: [
|
|
3870
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: t2("agenda.settings.enableConfirmations"), description: t2("agenda.settings.enableConfirmationsDesc"), checked: s.get("enableConfirmations"), onChange: (v) => s.set("enableConfirmations", v) }),
|
|
3871
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: t2("agenda.settings.autoConfirm"), description: t2("agenda.settings.autoConfirmDesc"), checked: s.get("autoConfirm"), onChange: (v) => s.set("autoConfirm", v) }),
|
|
3872
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: t2("agenda.settings.reminder"), description: t2("agenda.settings.reminderDesc"), checked: s.get("reminder"), onChange: (v) => s.set("reminder", v) })
|
|
4168
3873
|
] }),
|
|
4169
|
-
/* @__PURE__ */ jsxs(SettingsGroup, { title:
|
|
3874
|
+
/* @__PURE__ */ jsxs(SettingsGroup, { title: t2("agenda.settings.calendarDisplay"), description: t2("agenda.settings.calendarDisplayDesc"), children: [
|
|
4170
3875
|
/* @__PURE__ */ jsx(
|
|
4171
3876
|
SelectRow,
|
|
4172
3877
|
{
|
|
4173
|
-
label:
|
|
4174
|
-
description:
|
|
3878
|
+
label: t2("agenda.settings.dayStartTime"),
|
|
3879
|
+
description: t2("agenda.settings.dayStartTimeDesc"),
|
|
4175
3880
|
value: startTime,
|
|
4176
3881
|
options: HOUR_OPTIONS,
|
|
4177
3882
|
onChange: setStartTime
|
|
@@ -4180,27 +3885,21 @@ function AgendaGeneralSettings() {
|
|
|
4180
3885
|
/* @__PURE__ */ jsx(
|
|
4181
3886
|
SelectRow,
|
|
4182
3887
|
{
|
|
4183
|
-
label:
|
|
4184
|
-
description:
|
|
3888
|
+
label: t2("agenda.settings.dayEndTime"),
|
|
3889
|
+
description: t2("agenda.settings.dayEndTimeDesc"),
|
|
4185
3890
|
value: endTime,
|
|
4186
3891
|
options: HOUR_OPTIONS,
|
|
4187
3892
|
onChange: setEndTime
|
|
4188
3893
|
}
|
|
4189
3894
|
),
|
|
4190
|
-
/* @__PURE__ */ jsx(ToggleRow, { label:
|
|
4191
|
-
|
|
4192
|
-
/* @__PURE__ */ jsx(ToggleRow, { label:
|
|
4193
|
-
} }),
|
|
4194
|
-
/* @__PURE__ */ jsx(ToggleRow, { label: t("agenda.settings.compact"), description: t("agenda.settings.compactDesc"), checked: false, onChange: () => {
|
|
4195
|
-
} })
|
|
3895
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: t2("agenda.settings.showCancelled"), description: t2("agenda.settings.showCancelledDesc"), checked: s.get("showCancelled"), onChange: (v) => s.set("showCancelled", v) }),
|
|
3896
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: t2("agenda.settings.showNoShow"), description: t2("agenda.settings.showNoShowDesc"), checked: s.get("showNoShow"), onChange: (v) => s.set("showNoShow", v) }),
|
|
3897
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: t2("agenda.settings.compact"), description: t2("agenda.settings.compactDesc"), checked: s.get("compact"), onChange: (v) => s.set("compact", v) })
|
|
4196
3898
|
] }),
|
|
4197
|
-
/* @__PURE__ */ jsxs(SettingsGroup, { title:
|
|
4198
|
-
/* @__PURE__ */ jsx(ToggleRow, { label:
|
|
4199
|
-
|
|
4200
|
-
/* @__PURE__ */ jsx(ToggleRow, { label:
|
|
4201
|
-
} }),
|
|
4202
|
-
/* @__PURE__ */ jsx(ToggleRow, { label: t("agenda.settings.noShowTracking"), description: t("agenda.settings.noShowTrackingDesc"), checked: false, onChange: () => {
|
|
4203
|
-
} })
|
|
3899
|
+
/* @__PURE__ */ jsxs(SettingsGroup, { title: t2("agenda.settings.notifications"), description: t2("agenda.settings.notificationsDesc"), children: [
|
|
3900
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: t2("agenda.settings.newBookingNotif"), description: t2("agenda.settings.newBookingNotifDesc"), checked: s.get("newBookingNotif"), onChange: (v) => s.set("newBookingNotif", v) }),
|
|
3901
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: t2("agenda.settings.cancelAlerts"), description: t2("agenda.settings.cancelAlertsDesc"), checked: s.get("cancelAlerts"), onChange: (v) => s.set("cancelAlerts", v) }),
|
|
3902
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: t2("agenda.settings.noShowTracking"), description: t2("agenda.settings.noShowTrackingDesc"), checked: s.get("noShowTracking"), onChange: (v) => s.set("noShowTracking", v) })
|
|
4204
3903
|
] })
|
|
4205
3904
|
] });
|
|
4206
3905
|
}
|
|
@@ -4236,6 +3935,7 @@ var en = {
|
|
|
4236
3935
|
"agenda.appointment.searchService": "Search service to add...",
|
|
4237
3936
|
"agenda.appointment.selectLocation": "Select location...",
|
|
4238
3937
|
"agenda.appointment.selectProfessional": "Select professional...",
|
|
3938
|
+
"agenda.appointment.selectClientHint": "Select the client from the list or use New client to enable saving.",
|
|
4239
3939
|
"agenda.appointment.services": "Services",
|
|
4240
3940
|
"agenda.appointment.status": "Status",
|
|
4241
3941
|
"agenda.appointment.to": "to",
|
|
@@ -4456,6 +4156,7 @@ var ptBR = {
|
|
|
4456
4156
|
"agenda.appointment.searchService": "Buscar servi\xE7o para adicionar...",
|
|
4457
4157
|
"agenda.appointment.selectLocation": "Selecionar local...",
|
|
4458
4158
|
"agenda.appointment.selectProfessional": "Selecionar profissional...",
|
|
4159
|
+
"agenda.appointment.selectClientHint": "Selecione o cliente na lista ou use Novo cliente para habilitar o salvamento.",
|
|
4459
4160
|
"agenda.appointment.services": "Servi\xE7os",
|
|
4460
4161
|
"agenda.appointment.status": "Status",
|
|
4461
4162
|
"agenda.appointment.to": "at\xE9",
|
|
@@ -4698,7 +4399,10 @@ CREATE INDEX IF NOT EXISTS appointments_assignee_time
|
|
|
4698
4399
|
WHERE status NOT IN ('cancelled', 'no_show');
|
|
4699
4400
|
|
|
4700
4401
|
-- \xA72 \u2014 admin read model (security_invoker \u2192 underlying RLS applies; anon sees 0 rows)
|
|
4701
|
-
CREATE OR REPLACE
|
|
4402
|
+
-- DROP first: 013 widens this view and CREATE OR REPLACE cannot narrow a live
|
|
4403
|
+
-- view on replay ("cannot drop columns from view"). 013 replays right after.
|
|
4404
|
+
DROP VIEW IF EXISTS public.v_appointments;
|
|
4405
|
+
CREATE VIEW public.v_appointments
|
|
4702
4406
|
WITH (security_invoker = true) AS
|
|
4703
4407
|
SELECT
|
|
4704
4408
|
b.id, b.tenant_id, b.kind, b.starts_at, b.ends_at, b.status, b.notes,
|
|
@@ -4726,7 +4430,9 @@ GRANT SELECT ON public.v_appointments TO authenticated, service_role;
|
|
|
4726
4430
|
REVOKE SELECT ON public.v_appointments FROM anon;
|
|
4727
4431
|
|
|
4728
4432
|
-- \xA73 \u2014 anon-readable public catalog (owner-rights view = deliberate, whitelisted)
|
|
4729
|
-
|
|
4433
|
+
-- DROP first: 010 widens this view (curation columns) \u2014 same replay rule as \xA72.
|
|
4434
|
+
DROP VIEW IF EXISTS public.v_public_services;
|
|
4435
|
+
CREATE VIEW public.v_public_services AS
|
|
4730
4436
|
SELECT
|
|
4731
4437
|
s.id, s.tenant_id, s.name, s.description, s.price, s.currency,
|
|
4732
4438
|
COALESCE(s.duration_minutes, 30) AS duration_minutes,
|
|
@@ -5010,7 +4716,7 @@ function createFinancialBridge(provider2) {
|
|
|
5010
4716
|
let typeNames = {};
|
|
5011
4717
|
if (typeIds.length > 0) {
|
|
5012
4718
|
const { data: types } = await supabase.from("plg_financial_payment_method_types").select("id, name").in("id", typeIds);
|
|
5013
|
-
typeNames = Object.fromEntries((types ?? []).map((
|
|
4719
|
+
typeNames = Object.fromEntries((types ?? []).map((t2) => [t2.id, t2.name]));
|
|
5014
4720
|
}
|
|
5015
4721
|
const movements = (movRes.data ?? []).map((m) => ({
|
|
5016
4722
|
id: String(m.id),
|
|
@@ -5111,7 +4817,7 @@ function createFinancialBridge(provider2) {
|
|
|
5111
4817
|
},
|
|
5112
4818
|
async getPaymentMethodTypes() {
|
|
5113
4819
|
const types = await provider2.getPaymentMethodTypes();
|
|
5114
|
-
return types.map((
|
|
4820
|
+
return types.map((t2) => ({ id: t2.id, name: t2.name, transactionType: t2.transactionType }));
|
|
5115
4821
|
},
|
|
5116
4822
|
async getBankAccounts() {
|
|
5117
4823
|
const accounts = await provider2.getBankAccounts();
|
|
@@ -5153,6 +4859,65 @@ function mapInvoiceStatus(status) {
|
|
|
5153
4859
|
return "pending";
|
|
5154
4860
|
}
|
|
5155
4861
|
}
|
|
4862
|
+
|
|
4863
|
+
// src/integrations/google-calendar/mapping.ts
|
|
4864
|
+
function bookingSummary(b) {
|
|
4865
|
+
const services = (b.services ?? []).map((s) => s.name).filter(Boolean).join(", ");
|
|
4866
|
+
const who = b.clientName ?? "";
|
|
4867
|
+
if (services && who) return `${services} \u2014 ${who}`;
|
|
4868
|
+
return services || who || "Appointment";
|
|
4869
|
+
}
|
|
4870
|
+
function bookingToEvent2(b, timeZone) {
|
|
4871
|
+
if (!b.startsAt || !b.endsAt) return null;
|
|
4872
|
+
const notes = [b.notes, b.professionalName ? `Pro: ${b.professionalName}` : "", b.locationName ?? ""].filter(Boolean).join("\n");
|
|
4873
|
+
return {
|
|
4874
|
+
summary: bookingSummary(b),
|
|
4875
|
+
description: notes || void 0,
|
|
4876
|
+
start: { dateTime: b.startsAt, timeZone },
|
|
4877
|
+
end: { dateTime: b.endsAt, timeZone }
|
|
4878
|
+
};
|
|
4879
|
+
}
|
|
4880
|
+
function eventToBookingPatch(ev) {
|
|
4881
|
+
if (ev.status === "cancelled") return { status: "cancelled" };
|
|
4882
|
+
const t2 = eventTimes(ev);
|
|
4883
|
+
return { startsAt: t2.startsAt, endsAt: t2.endsAt };
|
|
4884
|
+
}
|
|
4885
|
+
function eventTimes(ev) {
|
|
4886
|
+
const allDay = !ev.start?.dateTime && !!ev.start?.date;
|
|
4887
|
+
const startsAt = ev.start?.dateTime ?? ev.start?.date ?? "";
|
|
4888
|
+
const endsAt = ev.end?.dateTime ?? ev.end?.date ?? startsAt;
|
|
4889
|
+
return { startsAt, endsAt, allDay };
|
|
4890
|
+
}
|
|
4891
|
+
function channelRowToChannel(r) {
|
|
4892
|
+
return {
|
|
4893
|
+
id: r.id,
|
|
4894
|
+
integrationId: r.integration_id,
|
|
4895
|
+
tenantId: r.tenant_id,
|
|
4896
|
+
googleCalendarId: r.google_calendar_id,
|
|
4897
|
+
summary: r.summary ?? null,
|
|
4898
|
+
color: r.color ?? null,
|
|
4899
|
+
direction: r.direction,
|
|
4900
|
+
targetKind: r.target_kind ?? null,
|
|
4901
|
+
targetId: r.target_id ?? null,
|
|
4902
|
+
importMode: r.import_mode ?? "appointment",
|
|
4903
|
+
syncToken: r.sync_token ?? null,
|
|
4904
|
+
channelId: r.channel_id ?? null,
|
|
4905
|
+
resourceId: r.resource_id ?? null,
|
|
4906
|
+
channelExpiresAt: r.channel_expires_at ?? null,
|
|
4907
|
+
isActive: r.is_active ?? true,
|
|
4908
|
+
createdAt: r.created_at ?? "",
|
|
4909
|
+
updatedAt: r.updated_at ?? ""
|
|
4910
|
+
};
|
|
4911
|
+
}
|
|
4912
|
+
|
|
4913
|
+
// src/integrations/google-calendar/data/tables.ts
|
|
4914
|
+
var GCAL_TABLES = {
|
|
4915
|
+
integrations: "plg_calendar_integrations",
|
|
4916
|
+
channels: "plg_calendar_channels",
|
|
4917
|
+
syncLog: "plg_calendar_sync_log"
|
|
4918
|
+
};
|
|
4919
|
+
|
|
4920
|
+
// src/integrations/google-calendar/data/supabase.ts
|
|
5156
4921
|
var FUNCTION = "google-calendar-sync";
|
|
5157
4922
|
function sb() {
|
|
5158
4923
|
const supabase = getSupabaseClientOptional();
|
|
@@ -5162,7 +4927,7 @@ function sb() {
|
|
|
5162
4927
|
function createGoogleCalendarProvider() {
|
|
5163
4928
|
return {
|
|
5164
4929
|
async getIntegration() {
|
|
5165
|
-
const { data } = await sb().from(
|
|
4930
|
+
const { data } = await sb().from(GCAL_TABLES.integrations).select("*").eq("provider", "google").limit(1).maybeSingle();
|
|
5166
4931
|
if (!data) return null;
|
|
5167
4932
|
return {
|
|
5168
4933
|
id: data.id,
|
|
@@ -5175,24 +4940,24 @@ function createGoogleCalendarProvider() {
|
|
|
5175
4940
|
},
|
|
5176
4941
|
async getConnectUrl(redirectTo) {
|
|
5177
4942
|
const { data, error } = await sb().functions.invoke(FUNCTION, {
|
|
5178
|
-
body: { action: "oauth_start", redirectTo: redirectTo ?? window.location.href }
|
|
4943
|
+
body: { action: "oauth_start", tenantId: getActiveTenantId(), redirectTo: redirectTo ?? window.location.href }
|
|
5179
4944
|
});
|
|
5180
4945
|
if (error) throw new Error(error.message);
|
|
5181
4946
|
return data.url;
|
|
5182
4947
|
},
|
|
5183
4948
|
async setCalendar(calendarId) {
|
|
5184
|
-
await sb().from(
|
|
4949
|
+
await sb().from(GCAL_TABLES.integrations).update({ calendar_id: calendarId, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("provider", "google");
|
|
5185
4950
|
},
|
|
5186
4951
|
async disconnect() {
|
|
5187
|
-
await sb().from(
|
|
4952
|
+
await sb().from(GCAL_TABLES.integrations).update({ active: false, oauth_refresh_token: null, oauth_access_token: null }).eq("provider", "google");
|
|
5188
4953
|
},
|
|
5189
4954
|
async syncNow() {
|
|
5190
|
-
const { data, error } = await sb().functions.invoke(FUNCTION, { body: { action: "pull_events", trigger: "manual" } });
|
|
4955
|
+
const { data, error } = await sb().functions.invoke(FUNCTION, { body: { action: "pull_events", tenantId: getActiveTenantId(), trigger: "manual" } });
|
|
5191
4956
|
if (error) throw new Error(error.message);
|
|
5192
4957
|
return data;
|
|
5193
4958
|
},
|
|
5194
4959
|
async getSyncLog() {
|
|
5195
|
-
const { data } = await sb().from(
|
|
4960
|
+
const { data } = await sb().from(GCAL_TABLES.syncLog).select("*").order("created_at", { ascending: false }).limit(20);
|
|
5196
4961
|
return (data ?? []).map((r) => ({
|
|
5197
4962
|
id: r.id,
|
|
5198
4963
|
direction: r.direction,
|
|
@@ -5203,32 +4968,523 @@ function createGoogleCalendarProvider() {
|
|
|
5203
4968
|
error: r.error ?? void 0,
|
|
5204
4969
|
createdAt: r.created_at
|
|
5205
4970
|
}));
|
|
4971
|
+
},
|
|
4972
|
+
async listGoogleCalendars() {
|
|
4973
|
+
const { data, error } = await sb().functions.invoke(FUNCTION, { body: { action: "list_calendars", tenantId: getActiveTenantId() } });
|
|
4974
|
+
if (error) throw new Error(error.message);
|
|
4975
|
+
return data.calendars ?? [];
|
|
4976
|
+
},
|
|
4977
|
+
async listChannels() {
|
|
4978
|
+
const { data } = await sb().from(GCAL_TABLES.channels).select("*").order("created_at", { ascending: true });
|
|
4979
|
+
return (data ?? []).map(channelRowToChannel);
|
|
4980
|
+
},
|
|
4981
|
+
async saveChannel(input) {
|
|
4982
|
+
const tenantId = getActiveTenantId();
|
|
4983
|
+
let integrationId = input.integrationId;
|
|
4984
|
+
if (!integrationId) {
|
|
4985
|
+
const { data } = await sb().from(GCAL_TABLES.integrations).select("id").eq("provider", "google").limit(1).maybeSingle();
|
|
4986
|
+
integrationId = data?.id;
|
|
4987
|
+
}
|
|
4988
|
+
const row = {
|
|
4989
|
+
integration_id: integrationId,
|
|
4990
|
+
tenant_id: tenantId,
|
|
4991
|
+
google_calendar_id: input.googleCalendarId,
|
|
4992
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
4993
|
+
};
|
|
4994
|
+
if (input.id !== void 0) row.id = input.id;
|
|
4995
|
+
if (input.summary !== void 0) row.summary = input.summary;
|
|
4996
|
+
if (input.color !== void 0) row.color = input.color;
|
|
4997
|
+
if (input.direction !== void 0) row.direction = input.direction;
|
|
4998
|
+
if (input.targetKind !== void 0) row.target_kind = input.targetKind;
|
|
4999
|
+
if (input.targetId !== void 0) row.target_id = input.targetId;
|
|
5000
|
+
if (input.importMode !== void 0) row.import_mode = input.importMode;
|
|
5001
|
+
if (input.isActive !== void 0) row.is_active = input.isActive;
|
|
5002
|
+
const { error } = await sb().from(GCAL_TABLES.channels).upsert(row, { onConflict: "integration_id,google_calendar_id" });
|
|
5003
|
+
if (error) throw new Error(error.message);
|
|
5004
|
+
},
|
|
5005
|
+
async deleteChannel(id) {
|
|
5006
|
+
const { error } = await sb().from(GCAL_TABLES.channels).delete().eq("id", id);
|
|
5007
|
+
if (error) throw new Error(error.message);
|
|
5008
|
+
},
|
|
5009
|
+
async listExternalEvents(params) {
|
|
5010
|
+
const { data, error } = await sb().functions.invoke(FUNCTION, {
|
|
5011
|
+
body: { action: "list_external_events", tenantId: getActiveTenantId(), channelId: params?.channelId, timeMin: params?.timeMin }
|
|
5012
|
+
});
|
|
5013
|
+
if (error) throw new Error(error.message);
|
|
5014
|
+
return data.events ?? [];
|
|
5015
|
+
},
|
|
5016
|
+
async importEvents(items) {
|
|
5017
|
+
const { data, error } = await sb().functions.invoke(FUNCTION, {
|
|
5018
|
+
body: { action: "import_events", tenantId: getActiveTenantId(), items }
|
|
5019
|
+
});
|
|
5020
|
+
if (error) throw new Error(error.message);
|
|
5021
|
+
return data;
|
|
5206
5022
|
}
|
|
5207
5023
|
};
|
|
5208
5024
|
}
|
|
5025
|
+
function CalendarColorDot({
|
|
5026
|
+
color,
|
|
5027
|
+
className = ""
|
|
5028
|
+
}) {
|
|
5029
|
+
return /* @__PURE__ */ jsx(
|
|
5030
|
+
"span",
|
|
5031
|
+
{
|
|
5032
|
+
className: `inline-block h-2.5 w-2.5 shrink-0 rounded-full border border-black/10 ${className}`,
|
|
5033
|
+
style: { backgroundColor: color || "hsl(var(--muted-foreground))" },
|
|
5034
|
+
"aria-hidden": true
|
|
5035
|
+
}
|
|
5036
|
+
);
|
|
5037
|
+
}
|
|
5038
|
+
var EMPTY = { assignee: [], service: [], location: [] };
|
|
5039
|
+
function useAgendaLookups(enabled) {
|
|
5040
|
+
const [lookups, setLookups] = useState(EMPTY);
|
|
5041
|
+
const [loading, setLoading] = useState(false);
|
|
5042
|
+
useEffect(() => {
|
|
5043
|
+
let cancelled = false;
|
|
5044
|
+
setLoading(true);
|
|
5045
|
+
void (async () => {
|
|
5046
|
+
const agenda = createSupabaseAgendaProvider();
|
|
5047
|
+
const tenantId = getActiveTenantId();
|
|
5048
|
+
const publicProvider = tenantId ? createSupabasePublicBookingProvider({ tenantId }) : null;
|
|
5049
|
+
const [assignee, location, service] = await Promise.all([
|
|
5050
|
+
agenda.getProfessionals().then(
|
|
5051
|
+
(rows) => rows.map((p) => ({ id: p.id, name: p.name })),
|
|
5052
|
+
() => []
|
|
5053
|
+
),
|
|
5054
|
+
(agenda.getLocations?.() ?? Promise.resolve([])).then(
|
|
5055
|
+
(rows) => rows.map((l) => ({ id: l.id, name: l.name })),
|
|
5056
|
+
() => []
|
|
5057
|
+
),
|
|
5058
|
+
(publicProvider?.getServices() ?? Promise.resolve([])).then(
|
|
5059
|
+
(rows) => rows.map((s) => ({ id: s.id, name: s.name })),
|
|
5060
|
+
() => []
|
|
5061
|
+
)
|
|
5062
|
+
]);
|
|
5063
|
+
if (cancelled) return;
|
|
5064
|
+
setLookups({ assignee, service, location });
|
|
5065
|
+
setLoading(false);
|
|
5066
|
+
})();
|
|
5067
|
+
return () => {
|
|
5068
|
+
cancelled = true;
|
|
5069
|
+
};
|
|
5070
|
+
}, [enabled]);
|
|
5071
|
+
const hasAny = lookups.assignee.length > 0 || lookups.service.length > 0 || lookups.location.length > 0;
|
|
5072
|
+
return { lookups, loading, hasAny };
|
|
5073
|
+
}
|
|
5074
|
+
function optionsForKind(lookups, kind) {
|
|
5075
|
+
if (kind === "assignee") return lookups.assignee;
|
|
5076
|
+
if (kind === "service") return lookups.service;
|
|
5077
|
+
if (kind === "location") return lookups.location;
|
|
5078
|
+
return [];
|
|
5079
|
+
}
|
|
5080
|
+
var DIRECTION_LABELS = {
|
|
5081
|
+
bidirectional: "Two-way",
|
|
5082
|
+
inbound: "Import only",
|
|
5083
|
+
outbound: "Export only",
|
|
5084
|
+
off: "Off"
|
|
5085
|
+
};
|
|
5086
|
+
var IMPORT_MODE_LABELS = {
|
|
5087
|
+
block: "Block",
|
|
5088
|
+
appointment: "Appointment"
|
|
5089
|
+
};
|
|
5090
|
+
var TARGET_KIND_LABELS = {
|
|
5091
|
+
agenda: "Whole agenda",
|
|
5092
|
+
assignee: "Professional",
|
|
5093
|
+
service: "Service",
|
|
5094
|
+
location: "Location"
|
|
5095
|
+
};
|
|
5096
|
+
var NO_TARGET = "__none__";
|
|
5097
|
+
function defaultChannel() {
|
|
5098
|
+
return { direction: "bidirectional", targetKind: null, targetId: null, importMode: "block" };
|
|
5099
|
+
}
|
|
5100
|
+
function ChannelMappingList({
|
|
5101
|
+
provider: provider2,
|
|
5102
|
+
onChannelsChange
|
|
5103
|
+
}) {
|
|
5104
|
+
const [calendars, setCalendars] = useState([]);
|
|
5105
|
+
const [byGcalId, setByGcalId] = useState({});
|
|
5106
|
+
const [loading, setLoading] = useState(true);
|
|
5107
|
+
const [error, setError] = useState(null);
|
|
5108
|
+
const [savingId, setSavingId] = useState(null);
|
|
5109
|
+
const { lookups, hasAny: hasLookups } = useAgendaLookups(true);
|
|
5110
|
+
const load = useCallback(async () => {
|
|
5111
|
+
setLoading(true);
|
|
5112
|
+
setError(null);
|
|
5113
|
+
try {
|
|
5114
|
+
const [cals, channels] = await Promise.all([
|
|
5115
|
+
provider2.listGoogleCalendars(),
|
|
5116
|
+
provider2.listChannels()
|
|
5117
|
+
]);
|
|
5118
|
+
setCalendars(cals);
|
|
5119
|
+
const map = {};
|
|
5120
|
+
for (const ch of channels) map[ch.googleCalendarId] = ch;
|
|
5121
|
+
setByGcalId(map);
|
|
5122
|
+
onChannelsChange?.(channels);
|
|
5123
|
+
} catch (e) {
|
|
5124
|
+
setError(e?.message ?? "Could not load Google calendars");
|
|
5125
|
+
} finally {
|
|
5126
|
+
setLoading(false);
|
|
5127
|
+
}
|
|
5128
|
+
}, [provider2, onChannelsChange]);
|
|
5129
|
+
useEffect(() => {
|
|
5130
|
+
void load();
|
|
5131
|
+
}, [load]);
|
|
5132
|
+
const persist = useCallback(
|
|
5133
|
+
async (cal, patch) => {
|
|
5134
|
+
const existing = byGcalId[cal.id];
|
|
5135
|
+
const base = existing ?? { ...defaultChannel(), isActive: true };
|
|
5136
|
+
setSavingId(cal.id);
|
|
5137
|
+
try {
|
|
5138
|
+
await provider2.saveChannel({
|
|
5139
|
+
id: existing?.id,
|
|
5140
|
+
googleCalendarId: cal.id,
|
|
5141
|
+
summary: cal.summary,
|
|
5142
|
+
color: cal.backgroundColor ?? null,
|
|
5143
|
+
direction: patch.direction ?? base.direction,
|
|
5144
|
+
targetKind: patch.targetKind !== void 0 ? patch.targetKind : base.targetKind,
|
|
5145
|
+
targetId: patch.targetId !== void 0 ? patch.targetId : base.targetId ?? null,
|
|
5146
|
+
importMode: patch.importMode ?? base.importMode,
|
|
5147
|
+
isActive: patch.isActive !== void 0 ? patch.isActive : base.isActive
|
|
5148
|
+
});
|
|
5149
|
+
const channels = await provider2.listChannels();
|
|
5150
|
+
const map = {};
|
|
5151
|
+
for (const ch of channels) map[ch.googleCalendarId] = ch;
|
|
5152
|
+
setByGcalId(map);
|
|
5153
|
+
onChannelsChange?.(channels);
|
|
5154
|
+
} catch (e) {
|
|
5155
|
+
toast.error(e?.message ?? "Could not save calendar mapping");
|
|
5156
|
+
} finally {
|
|
5157
|
+
setSavingId(null);
|
|
5158
|
+
}
|
|
5159
|
+
},
|
|
5160
|
+
[byGcalId, provider2, onChannelsChange]
|
|
5161
|
+
);
|
|
5162
|
+
if (loading) {
|
|
5163
|
+
return /* @__PURE__ */ jsx("div", { className: "py-6 text-center text-sm text-muted-foreground", children: /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin inline" }) });
|
|
5164
|
+
}
|
|
5165
|
+
if (error) {
|
|
5166
|
+
return /* @__PURE__ */ jsxs("div", { className: "rounded-md border border-warning/40 bg-warning-soft/40 px-3 py-3 text-sm", children: [
|
|
5167
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-warning-soft-foreground", children: [
|
|
5168
|
+
/* @__PURE__ */ jsx(AlertCircle, { className: "h-4 w-4" }),
|
|
5169
|
+
" ",
|
|
5170
|
+
error
|
|
5171
|
+
] }),
|
|
5172
|
+
/* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", className: "mt-2", onClick: () => void load(), children: [
|
|
5173
|
+
/* @__PURE__ */ jsx(RefreshCw, { className: "h-3.5 w-3.5" }),
|
|
5174
|
+
" Retry"
|
|
5175
|
+
] })
|
|
5176
|
+
] });
|
|
5177
|
+
}
|
|
5178
|
+
if (calendars.length === 0) {
|
|
5179
|
+
return /* @__PURE__ */ jsx("p", { className: "py-6 text-center text-sm text-muted-foreground", children: "No Google calendars found on this account." });
|
|
5180
|
+
}
|
|
5181
|
+
return /* @__PURE__ */ jsxs("div", { className: "rounded-md border divide-y", children: [
|
|
5182
|
+
calendars.map((cal) => {
|
|
5183
|
+
const ch = byGcalId[cal.id];
|
|
5184
|
+
const active = !!ch?.isActive;
|
|
5185
|
+
const saving = savingId === cal.id;
|
|
5186
|
+
const direction = ch?.direction ?? "bidirectional";
|
|
5187
|
+
const targetKind = ch?.targetKind ?? null;
|
|
5188
|
+
const importMode = ch?.importMode ?? "block";
|
|
5189
|
+
const canImport = direction === "inbound" || direction === "bidirectional";
|
|
5190
|
+
const kindOptions = optionsForKind(lookups, targetKind);
|
|
5191
|
+
return /* @__PURE__ */ jsxs("div", { className: "px-3 py-3 space-y-3", children: [
|
|
5192
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5", children: [
|
|
5193
|
+
/* @__PURE__ */ jsx(
|
|
5194
|
+
Checkbox,
|
|
5195
|
+
{
|
|
5196
|
+
checked: active,
|
|
5197
|
+
disabled: saving,
|
|
5198
|
+
onChange: (next) => void persist(cal, { isActive: next })
|
|
5199
|
+
}
|
|
5200
|
+
),
|
|
5201
|
+
/* @__PURE__ */ jsx(CalendarColorDot, { color: cal.backgroundColor }),
|
|
5202
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium truncate", children: cal.summary }),
|
|
5203
|
+
cal.primary && /* @__PURE__ */ jsx(Badge, { variant: "outline", className: "shrink-0", children: "Primary" }),
|
|
5204
|
+
saving && /* @__PURE__ */ jsx(Loader2, { className: "ml-auto h-3.5 w-3.5 animate-spin text-muted-foreground" })
|
|
5205
|
+
] }),
|
|
5206
|
+
active && /* @__PURE__ */ jsxs("div", { className: "grid gap-2 pl-[26px] sm:grid-cols-2 lg:grid-cols-4", children: [
|
|
5207
|
+
/* @__PURE__ */ jsxs("label", { className: "block", children: [
|
|
5208
|
+
/* @__PURE__ */ jsx("span", { className: "text-[11px] font-medium text-muted-foreground", children: "Direction" }),
|
|
5209
|
+
/* @__PURE__ */ jsxs(
|
|
5210
|
+
Select,
|
|
5211
|
+
{
|
|
5212
|
+
value: direction,
|
|
5213
|
+
onValueChange: (v) => void persist(cal, { direction: v }),
|
|
5214
|
+
children: [
|
|
5215
|
+
/* @__PURE__ */ jsx(SelectTrigger, { className: "mt-1 h-8 text-sm", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
|
|
5216
|
+
/* @__PURE__ */ jsx(SelectContent, { children: Object.keys(DIRECTION_LABELS).map((d) => /* @__PURE__ */ jsx(SelectItem, { value: d, children: DIRECTION_LABELS[d] }, d)) })
|
|
5217
|
+
]
|
|
5218
|
+
}
|
|
5219
|
+
)
|
|
5220
|
+
] }),
|
|
5221
|
+
/* @__PURE__ */ jsxs("label", { className: "block", children: [
|
|
5222
|
+
/* @__PURE__ */ jsx("span", { className: "text-[11px] font-medium text-muted-foreground", children: "Target" }),
|
|
5223
|
+
/* @__PURE__ */ jsxs(
|
|
5224
|
+
Select,
|
|
5225
|
+
{
|
|
5226
|
+
value: targetKind ?? "agenda",
|
|
5227
|
+
onValueChange: (v) => void persist(cal, {
|
|
5228
|
+
targetKind: v === "agenda" ? null : v,
|
|
5229
|
+
// Reset the id whenever the kind changes.
|
|
5230
|
+
targetId: null
|
|
5231
|
+
}),
|
|
5232
|
+
children: [
|
|
5233
|
+
/* @__PURE__ */ jsx(SelectTrigger, { className: "mt-1 h-8 text-sm", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
|
|
5234
|
+
/* @__PURE__ */ jsx(SelectContent, { children: Object.keys(TARGET_KIND_LABELS).map(
|
|
5235
|
+
(k) => /* @__PURE__ */ jsx(SelectItem, { value: k, children: TARGET_KIND_LABELS[k] }, k)
|
|
5236
|
+
) })
|
|
5237
|
+
]
|
|
5238
|
+
}
|
|
5239
|
+
)
|
|
5240
|
+
] }),
|
|
5241
|
+
targetKind && /* @__PURE__ */ jsxs("label", { className: "block", children: [
|
|
5242
|
+
/* @__PURE__ */ jsx("span", { className: "text-[11px] font-medium text-muted-foreground", children: TARGET_KIND_LABELS[targetKind] }),
|
|
5243
|
+
kindOptions.length > 0 ? /* @__PURE__ */ jsxs(
|
|
5244
|
+
Select,
|
|
5245
|
+
{
|
|
5246
|
+
value: ch?.targetId ?? NO_TARGET,
|
|
5247
|
+
onValueChange: (v) => void persist(cal, { targetId: v === NO_TARGET ? null : v }),
|
|
5248
|
+
children: [
|
|
5249
|
+
/* @__PURE__ */ jsx(SelectTrigger, { className: "mt-1 h-8 text-sm", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "Select\u2026" }) }),
|
|
5250
|
+
/* @__PURE__ */ jsx(SelectContent, { children: kindOptions.map((o) => /* @__PURE__ */ jsx(SelectItem, { value: o.id, children: o.name }, o.id)) })
|
|
5251
|
+
]
|
|
5252
|
+
}
|
|
5253
|
+
) : /* @__PURE__ */ jsx(
|
|
5254
|
+
"input",
|
|
5255
|
+
{
|
|
5256
|
+
defaultValue: ch?.targetId ?? "",
|
|
5257
|
+
placeholder: "uuid",
|
|
5258
|
+
onBlur: (e) => void persist(cal, { targetId: e.target.value.trim() || null }),
|
|
5259
|
+
className: "w-full mt-1 h-8 rounded-input border border-input bg-card px-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
5260
|
+
}
|
|
5261
|
+
)
|
|
5262
|
+
] }),
|
|
5263
|
+
canImport && /* @__PURE__ */ jsxs("label", { className: "block", children: [
|
|
5264
|
+
/* @__PURE__ */ jsx("span", { className: "text-[11px] font-medium text-muted-foreground", children: "Import as" }),
|
|
5265
|
+
/* @__PURE__ */ jsxs(
|
|
5266
|
+
Select,
|
|
5267
|
+
{
|
|
5268
|
+
value: importMode,
|
|
5269
|
+
onValueChange: (v) => void persist(cal, { importMode: v }),
|
|
5270
|
+
children: [
|
|
5271
|
+
/* @__PURE__ */ jsx(SelectTrigger, { className: "mt-1 h-8 text-sm", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
|
|
5272
|
+
/* @__PURE__ */ jsx(SelectContent, { children: Object.keys(IMPORT_MODE_LABELS).map((m) => /* @__PURE__ */ jsx(SelectItem, { value: m, children: IMPORT_MODE_LABELS[m] }, m)) })
|
|
5273
|
+
]
|
|
5274
|
+
}
|
|
5275
|
+
)
|
|
5276
|
+
] })
|
|
5277
|
+
] })
|
|
5278
|
+
] }, cal.id);
|
|
5279
|
+
}),
|
|
5280
|
+
!hasLookups && /* @__PURE__ */ jsx("p", { className: "px-3 py-2 text-[11px] text-muted-foreground", children: "No professional/service/location lookups available \u2014 target ids fall back to manual uuid entry." })
|
|
5281
|
+
] });
|
|
5282
|
+
}
|
|
5283
|
+
function formatWhen(ev) {
|
|
5284
|
+
const start = new Date(ev.startsAt);
|
|
5285
|
+
if (ev.allDay) {
|
|
5286
|
+
return start.toLocaleDateString("pt-BR", { day: "2-digit", month: "short", year: "numeric" });
|
|
5287
|
+
}
|
|
5288
|
+
return start.toLocaleString("pt-BR", {
|
|
5289
|
+
day: "2-digit",
|
|
5290
|
+
month: "short",
|
|
5291
|
+
hour: "2-digit",
|
|
5292
|
+
minute: "2-digit"
|
|
5293
|
+
});
|
|
5294
|
+
}
|
|
5295
|
+
function ImportEventsWizard({
|
|
5296
|
+
provider: provider2,
|
|
5297
|
+
channels,
|
|
5298
|
+
onClose,
|
|
5299
|
+
onImported
|
|
5300
|
+
}) {
|
|
5301
|
+
const [events, setEvents] = useState([]);
|
|
5302
|
+
const [selected, setSelected] = useState(/* @__PURE__ */ new Set());
|
|
5303
|
+
const [loading, setLoading] = useState(true);
|
|
5304
|
+
const [error, setError] = useState(null);
|
|
5305
|
+
const [importing, setImporting] = useState(false);
|
|
5306
|
+
const [result, setResult] = useState(null);
|
|
5307
|
+
const channelById = useMemo(() => {
|
|
5308
|
+
const m = {};
|
|
5309
|
+
for (const c of channels) m[c.id] = c;
|
|
5310
|
+
return m;
|
|
5311
|
+
}, [channels]);
|
|
5312
|
+
const load = useCallback(async () => {
|
|
5313
|
+
setLoading(true);
|
|
5314
|
+
setError(null);
|
|
5315
|
+
setResult(null);
|
|
5316
|
+
try {
|
|
5317
|
+
const rows = await provider2.listExternalEvents();
|
|
5318
|
+
setEvents(rows);
|
|
5319
|
+
setSelected(new Set(rows.map((r) => r.eventId)));
|
|
5320
|
+
} catch (e) {
|
|
5321
|
+
setError(e?.message ?? "Could not load upcoming events");
|
|
5322
|
+
} finally {
|
|
5323
|
+
setLoading(false);
|
|
5324
|
+
}
|
|
5325
|
+
}, [provider2]);
|
|
5326
|
+
useEffect(() => {
|
|
5327
|
+
void load();
|
|
5328
|
+
}, [load]);
|
|
5329
|
+
const toggle = (eventId) => setSelected((prev) => {
|
|
5330
|
+
const next = new Set(prev);
|
|
5331
|
+
if (next.has(eventId)) next.delete(eventId);
|
|
5332
|
+
else next.add(eventId);
|
|
5333
|
+
return next;
|
|
5334
|
+
});
|
|
5335
|
+
const allSelected = events.length > 0 && selected.size === events.length;
|
|
5336
|
+
const toggleAll = () => setSelected(allSelected ? /* @__PURE__ */ new Set() : new Set(events.map((e) => e.eventId)));
|
|
5337
|
+
async function handleImport() {
|
|
5338
|
+
const items = events.filter((e) => selected.has(e.eventId)).map((e) => ({ channelId: e.channelId, eventId: e.eventId }));
|
|
5339
|
+
if (items.length === 0) return;
|
|
5340
|
+
setImporting(true);
|
|
5341
|
+
try {
|
|
5342
|
+
const res = await provider2.importEvents(items);
|
|
5343
|
+
setResult(res);
|
|
5344
|
+
onImported?.(res);
|
|
5345
|
+
toast.success(`Imported ${res.imported} \xB7 skipped ${res.skipped}`);
|
|
5346
|
+
await load();
|
|
5347
|
+
} catch (e) {
|
|
5348
|
+
toast.error(e?.message ?? "Import failed");
|
|
5349
|
+
} finally {
|
|
5350
|
+
setImporting(false);
|
|
5351
|
+
}
|
|
5352
|
+
}
|
|
5353
|
+
return /* @__PURE__ */ jsxs("div", { className: "rounded-md border bg-card", children: [
|
|
5354
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-b px-3 py-2", children: [
|
|
5355
|
+
/* @__PURE__ */ jsx(CalendarPlus, { className: "h-4 w-4 text-muted-foreground" }),
|
|
5356
|
+
/* @__PURE__ */ jsx("h4", { className: "text-sm font-semibold", children: "Import future events" }),
|
|
5357
|
+
/* @__PURE__ */ jsx(
|
|
5358
|
+
"button",
|
|
5359
|
+
{
|
|
5360
|
+
onClick: onClose,
|
|
5361
|
+
className: "ml-auto rounded p-1 text-muted-foreground hover:bg-muted/50",
|
|
5362
|
+
"aria-label": "Close",
|
|
5363
|
+
children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5" })
|
|
5364
|
+
}
|
|
5365
|
+
)
|
|
5366
|
+
] }),
|
|
5367
|
+
loading ? /* @__PURE__ */ jsx("div", { className: "py-8 text-center text-sm text-muted-foreground", children: /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin inline" }) }) : error ? /* @__PURE__ */ jsxs("div", { className: "p-3", children: [
|
|
5368
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm text-warning-soft-foreground", children: [
|
|
5369
|
+
/* @__PURE__ */ jsx(AlertCircle, { className: "h-4 w-4" }),
|
|
5370
|
+
" ",
|
|
5371
|
+
error
|
|
5372
|
+
] }),
|
|
5373
|
+
/* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", className: "mt-2", onClick: () => void load(), children: [
|
|
5374
|
+
/* @__PURE__ */ jsx(RefreshCw, { className: "h-3.5 w-3.5" }),
|
|
5375
|
+
" Retry"
|
|
5376
|
+
] })
|
|
5377
|
+
] }) : events.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "py-8 px-4 text-center space-y-1", children: [
|
|
5378
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: "You're all caught up" }),
|
|
5379
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "No upcoming Google events left to import on your active calendars." }),
|
|
5380
|
+
result && /* @__PURE__ */ jsxs("p", { className: "text-xs text-success", children: [
|
|
5381
|
+
"Imported ",
|
|
5382
|
+
result.imported,
|
|
5383
|
+
" \xB7 skipped ",
|
|
5384
|
+
result.skipped,
|
|
5385
|
+
result.errors.length > 0 ? ` \xB7 ${result.errors.length} errors` : ""
|
|
5386
|
+
] })
|
|
5387
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
5388
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5 border-b px-3 py-2", children: [
|
|
5389
|
+
/* @__PURE__ */ jsx(Checkbox, { checked: allSelected, onChange: toggleAll }),
|
|
5390
|
+
/* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
|
|
5391
|
+
selected.size,
|
|
5392
|
+
" of ",
|
|
5393
|
+
events.length,
|
|
5394
|
+
" selected"
|
|
5395
|
+
] })
|
|
5396
|
+
] }),
|
|
5397
|
+
/* @__PURE__ */ jsx("div", { className: "max-h-[320px] overflow-y-auto divide-y", children: events.map((ev) => {
|
|
5398
|
+
const ch = channelById[ev.channelId];
|
|
5399
|
+
return /* @__PURE__ */ jsxs(
|
|
5400
|
+
"label",
|
|
5401
|
+
{
|
|
5402
|
+
className: "flex items-start gap-2.5 px-3 py-2.5 cursor-pointer hover:bg-muted/30",
|
|
5403
|
+
children: [
|
|
5404
|
+
/* @__PURE__ */ jsx(
|
|
5405
|
+
Checkbox,
|
|
5406
|
+
{
|
|
5407
|
+
checked: selected.has(ev.eventId),
|
|
5408
|
+
onChange: () => toggle(ev.eventId)
|
|
5409
|
+
}
|
|
5410
|
+
),
|
|
5411
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1 space-y-1", children: [
|
|
5412
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
5413
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium truncate", children: ev.summary || "(no title)" }),
|
|
5414
|
+
ev.allDay && /* @__PURE__ */ jsx(Badge, { variant: "secondary", className: "shrink-0", children: "All-day" })
|
|
5415
|
+
] }),
|
|
5416
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
|
|
5417
|
+
/* @__PURE__ */ jsx("span", { children: formatWhen(ev) }),
|
|
5418
|
+
/* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1", children: [
|
|
5419
|
+
/* @__PURE__ */ jsx(CalendarColorDot, { color: ch?.color }),
|
|
5420
|
+
ch?.summary ?? ev.calendarId
|
|
5421
|
+
] })
|
|
5422
|
+
] })
|
|
5423
|
+
] })
|
|
5424
|
+
]
|
|
5425
|
+
},
|
|
5426
|
+
ev.eventId
|
|
5427
|
+
);
|
|
5428
|
+
}) }),
|
|
5429
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-t px-3 py-2", children: [
|
|
5430
|
+
result && /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
|
|
5431
|
+
"Last run: ",
|
|
5432
|
+
result.imported,
|
|
5433
|
+
" imported",
|
|
5434
|
+
result.errors.length > 0 ? `, ${result.errors.length} errors` : ""
|
|
5435
|
+
] }),
|
|
5436
|
+
/* @__PURE__ */ jsxs(
|
|
5437
|
+
Button,
|
|
5438
|
+
{
|
|
5439
|
+
size: "sm",
|
|
5440
|
+
className: "ml-auto",
|
|
5441
|
+
disabled: importing || selected.size === 0,
|
|
5442
|
+
onClick: handleImport,
|
|
5443
|
+
children: [
|
|
5444
|
+
importing ? /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsx(CheckCircle2, { className: "h-3.5 w-3.5" }),
|
|
5445
|
+
"Import ",
|
|
5446
|
+
selected.size > 0 ? selected.size : ""
|
|
5447
|
+
]
|
|
5448
|
+
}
|
|
5449
|
+
)
|
|
5450
|
+
] })
|
|
5451
|
+
] })
|
|
5452
|
+
] });
|
|
5453
|
+
}
|
|
5209
5454
|
var provider = createGoogleCalendarProvider();
|
|
5455
|
+
function inboundChannels(channels) {
|
|
5456
|
+
return channels.filter(
|
|
5457
|
+
(c) => c.isActive && (c.direction === "inbound" || c.direction === "bidirectional")
|
|
5458
|
+
);
|
|
5459
|
+
}
|
|
5210
5460
|
function GoogleCalendarExtraPanel() {
|
|
5211
5461
|
const [connected, setConnected] = useState(false);
|
|
5212
|
-
const [
|
|
5462
|
+
const [lastSyncAt, setLastSyncAt] = useState();
|
|
5213
5463
|
const [syncing, setSyncing] = useState(false);
|
|
5214
5464
|
const [log, setLog] = useState([]);
|
|
5215
5465
|
const [loaded, setLoaded] = useState(false);
|
|
5466
|
+
const [channels, setChannels] = useState([]);
|
|
5467
|
+
const [showMapping, setShowMapping] = useState(false);
|
|
5468
|
+
const [showWizard, setShowWizard] = useState(false);
|
|
5216
5469
|
async function load() {
|
|
5217
5470
|
const integ = await provider.getIntegration();
|
|
5218
5471
|
setConnected(!!integ?.connected);
|
|
5219
5472
|
if (integ) {
|
|
5220
|
-
|
|
5473
|
+
setLastSyncAt(integ.lastSyncAt);
|
|
5221
5474
|
void provider.getSyncLog().then(setLog);
|
|
5475
|
+
void provider.listChannels().then((ch) => {
|
|
5476
|
+
setChannels(ch);
|
|
5477
|
+
if (ch.length > 0) setShowMapping(true);
|
|
5478
|
+
});
|
|
5222
5479
|
}
|
|
5223
5480
|
setLoaded(true);
|
|
5224
5481
|
}
|
|
5225
5482
|
useEffect(() => {
|
|
5226
5483
|
void load();
|
|
5227
5484
|
}, []);
|
|
5228
|
-
if (!loaded
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
toast.success("Saved");
|
|
5485
|
+
if (!loaded) return null;
|
|
5486
|
+
if (!connected) {
|
|
5487
|
+
return /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground border-t pt-4", children: "Connect a Google account above to map your calendars and import events." });
|
|
5232
5488
|
}
|
|
5233
5489
|
async function handleSyncNow() {
|
|
5234
5490
|
setSyncing(true);
|
|
@@ -5236,48 +5492,81 @@ function GoogleCalendarExtraPanel() {
|
|
|
5236
5492
|
const r = await provider.syncNow();
|
|
5237
5493
|
toast.success(`Synced \u2014 ${r.written} updated`);
|
|
5238
5494
|
void provider.getSyncLog().then(setLog);
|
|
5495
|
+
void provider.getIntegration().then((i) => setLastSyncAt(i?.lastSyncAt));
|
|
5239
5496
|
} catch (e) {
|
|
5240
5497
|
toast.error(e?.message ?? "Sync failed");
|
|
5241
5498
|
} finally {
|
|
5242
5499
|
setSyncing(false);
|
|
5243
5500
|
}
|
|
5244
5501
|
}
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
/* @__PURE__ */ jsx(
|
|
5250
|
-
|
|
5251
|
-
{
|
|
5252
|
-
value: calendarId,
|
|
5253
|
-
onChange: (e) => setCalendarId(e.target.value),
|
|
5254
|
-
placeholder: "primary",
|
|
5255
|
-
className: "w-full mt-1 rounded-input border border-input bg-card px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
5256
|
-
}
|
|
5257
|
-
)
|
|
5502
|
+
const canImport = inboundChannels(channels).length > 0;
|
|
5503
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-5 border-t pt-4", children: [
|
|
5504
|
+
/* @__PURE__ */ jsxs("section", { className: "space-y-2", children: [
|
|
5505
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
5506
|
+
/* @__PURE__ */ jsx(CalendarCog, { className: "h-4 w-4 text-muted-foreground" }),
|
|
5507
|
+
/* @__PURE__ */ jsx("h4", { className: "text-sm font-semibold", children: "Calendars" })
|
|
5258
5508
|
] }),
|
|
5259
|
-
/* @__PURE__ */ jsx(
|
|
5260
|
-
|
|
5261
|
-
/* @__PURE__ */
|
|
5262
|
-
|
|
5509
|
+
showMapping ? /* @__PURE__ */ jsx(ChannelMappingList, { provider, onChannelsChange: setChannels }) : /* @__PURE__ */ jsxs("div", { className: "rounded-md border border-dashed px-4 py-5 text-center space-y-2", children: [
|
|
5510
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Map each Google calendar to what it syncs \u2014 a professional, service, location, or your whole agenda." }),
|
|
5511
|
+
/* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", onClick: () => setShowMapping(true), children: [
|
|
5512
|
+
/* @__PURE__ */ jsx(CalendarCog, { className: "h-3.5 w-3.5" }),
|
|
5513
|
+
" Configure calendars"
|
|
5514
|
+
] })
|
|
5263
5515
|
] })
|
|
5264
5516
|
] }),
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5517
|
+
showMapping && /* @__PURE__ */ jsx("section", { className: "space-y-2", children: showWizard ? /* @__PURE__ */ jsx(
|
|
5518
|
+
ImportEventsWizard,
|
|
5519
|
+
{
|
|
5520
|
+
provider,
|
|
5521
|
+
channels: inboundChannels(channels),
|
|
5522
|
+
onClose: () => setShowWizard(false),
|
|
5523
|
+
onImported: () => {
|
|
5524
|
+
void provider.getSyncLog().then(setLog);
|
|
5525
|
+
void provider.getIntegration().then((i) => setLastSyncAt(i?.lastSyncAt));
|
|
5526
|
+
}
|
|
5527
|
+
}
|
|
5528
|
+
) : /* @__PURE__ */ jsxs(
|
|
5529
|
+
Button,
|
|
5530
|
+
{
|
|
5531
|
+
variant: "outline",
|
|
5532
|
+
size: "sm",
|
|
5533
|
+
disabled: !canImport,
|
|
5534
|
+
title: canImport ? void 0 : "Enable an importing calendar first",
|
|
5535
|
+
onClick: () => setShowWizard(true),
|
|
5536
|
+
children: [
|
|
5537
|
+
/* @__PURE__ */ jsx(CalendarPlus, { className: "h-3.5 w-3.5" }),
|
|
5538
|
+
" Import future events"
|
|
5539
|
+
]
|
|
5540
|
+
}
|
|
5541
|
+
) }),
|
|
5542
|
+
/* @__PURE__ */ jsxs("section", { className: "space-y-2", children: [
|
|
5543
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [
|
|
5544
|
+
/* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", onClick: handleSyncNow, disabled: syncing, children: [
|
|
5545
|
+
/* @__PURE__ */ jsx(RefreshCw, { className: `h-3.5 w-3.5 ${syncing ? "animate-spin" : ""}` }),
|
|
5546
|
+
" Sync now"
|
|
5547
|
+
] }),
|
|
5548
|
+
lastSyncAt && /* @__PURE__ */ jsxs("span", { className: "text-[11px] text-muted-foreground", children: [
|
|
5549
|
+
"Last sync: ",
|
|
5550
|
+
new Date(lastSyncAt).toLocaleString()
|
|
5551
|
+
] })
|
|
5269
5552
|
] }),
|
|
5270
|
-
|
|
5271
|
-
/* @__PURE__ */
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
r.written,
|
|
5275
|
-
" written / ",
|
|
5276
|
-
r.fetched,
|
|
5277
|
-
" fetched"
|
|
5553
|
+
log.length > 0 && /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
5554
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
5555
|
+
/* @__PURE__ */ jsx(History, { className: "h-4 w-4 text-muted-foreground" }),
|
|
5556
|
+
/* @__PURE__ */ jsx("h4", { className: "text-sm font-semibold", children: "History" })
|
|
5278
5557
|
] }),
|
|
5279
|
-
|
|
5280
|
-
|
|
5558
|
+
/* @__PURE__ */ jsx("div", { className: "rounded-md border divide-y text-sm", children: log.map((r) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-3 py-2", children: [
|
|
5559
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-xs", children: new Date(r.createdAt).toLocaleString() }),
|
|
5560
|
+
/* @__PURE__ */ jsx("span", { className: "text-[10px] uppercase tracking-wide px-1 py-px rounded bg-muted text-muted-foreground", children: r.direction }),
|
|
5561
|
+
/* @__PURE__ */ jsxs("span", { className: "ml-auto text-xs", children: [
|
|
5562
|
+
r.written,
|
|
5563
|
+
" written / ",
|
|
5564
|
+
r.fetched,
|
|
5565
|
+
" fetched"
|
|
5566
|
+
] }),
|
|
5567
|
+
r.status === "success" ? /* @__PURE__ */ jsx(CheckCircle2, { className: "h-3.5 w-3.5 text-success" }) : /* @__PURE__ */ jsx(AlertCircle, { className: "h-3.5 w-3.5 text-warning" })
|
|
5568
|
+
] }, r.id)) })
|
|
5569
|
+
] })
|
|
5281
5570
|
] })
|
|
5282
5571
|
] });
|
|
5283
5572
|
}
|
|
@@ -5312,31 +5601,6 @@ var googleCalendarConnector = {
|
|
|
5312
5601
|
]
|
|
5313
5602
|
};
|
|
5314
5603
|
|
|
5315
|
-
// src/integrations/google-calendar/mapping.ts
|
|
5316
|
-
function bookingSummary(b) {
|
|
5317
|
-
const services = (b.services ?? []).map((s) => s.name).filter(Boolean).join(", ");
|
|
5318
|
-
const who = b.clientName ?? "";
|
|
5319
|
-
if (services && who) return `${services} \u2014 ${who}`;
|
|
5320
|
-
return services || who || "Appointment";
|
|
5321
|
-
}
|
|
5322
|
-
function bookingToEvent2(b, timeZone) {
|
|
5323
|
-
if (!b.startsAt || !b.endsAt) return null;
|
|
5324
|
-
const notes = [b.notes, b.professionalName ? `Pro: ${b.professionalName}` : "", b.locationName ?? ""].filter(Boolean).join("\n");
|
|
5325
|
-
return {
|
|
5326
|
-
summary: bookingSummary(b),
|
|
5327
|
-
description: notes || void 0,
|
|
5328
|
-
start: { dateTime: b.startsAt, timeZone },
|
|
5329
|
-
end: { dateTime: b.endsAt, timeZone }
|
|
5330
|
-
};
|
|
5331
|
-
}
|
|
5332
|
-
function eventToBookingPatch(ev) {
|
|
5333
|
-
if (ev.status === "cancelled") return { status: "cancelled" };
|
|
5334
|
-
return {
|
|
5335
|
-
startsAt: ev.start?.dateTime,
|
|
5336
|
-
endsAt: ev.end?.dateTime
|
|
5337
|
-
};
|
|
5338
|
-
}
|
|
5339
|
-
|
|
5340
5604
|
// src/integrations/google-calendar/index.ts
|
|
5341
5605
|
function createGoogleCalendarPlugin() {
|
|
5342
5606
|
return {
|
|
@@ -5358,7 +5622,7 @@ function createAgendaPlugin(options) {
|
|
|
5358
5622
|
const config = resolveConfig(options);
|
|
5359
5623
|
registerTranslations(agendaLocales);
|
|
5360
5624
|
const provider2 = options?.dataProvider ?? createSafeDataProvider(
|
|
5361
|
-
() => createSupabaseAgendaProvider(),
|
|
5625
|
+
() => createSupabaseAgendaProvider({ professionalKind: config.professionalKind }),
|
|
5362
5626
|
() => createMockAgendaProvider()
|
|
5363
5627
|
);
|
|
5364
5628
|
const store = createAgendaStore(provider2, options?.financialBridge);
|
|
@@ -5378,7 +5642,7 @@ function createAgendaPlugin(options) {
|
|
|
5378
5642
|
return locations;
|
|
5379
5643
|
} : void 0
|
|
5380
5644
|
});
|
|
5381
|
-
const PageComponent = () =>
|
|
5645
|
+
const PageComponent = () => React13.createElement(AgendaPage, { config, provider: provider2, store });
|
|
5382
5646
|
return {
|
|
5383
5647
|
id: "agenda",
|
|
5384
5648
|
name: config.labels.pageTitle,
|
|
@@ -5395,6 +5659,48 @@ function createAgendaPlugin(options) {
|
|
|
5395
5659
|
{ id: "appointments", label: config.labels.pageTitle, group: config.labels.pageTitle },
|
|
5396
5660
|
{ id: "agenda.schedules", label: "Schedule Management", group: config.labels.pageTitle }
|
|
5397
5661
|
],
|
|
5662
|
+
// Recurring monthly quota — counts appointment rows created this month.
|
|
5663
|
+
// (Clients/patients are the app's CRUD entity, so their cap is declared by
|
|
5664
|
+
// the app's EntityDef.limitKey, not here.)
|
|
5665
|
+
declaredLimits: [
|
|
5666
|
+
{ key: "bookings_month", label: "Appointments this month", table: "appointments", period: "month" }
|
|
5667
|
+
],
|
|
5668
|
+
queryEntities: [
|
|
5669
|
+
{
|
|
5670
|
+
key: "agenda:appointments",
|
|
5671
|
+
entity: {
|
|
5672
|
+
name: "Appointment",
|
|
5673
|
+
namePlural: "Appointments",
|
|
5674
|
+
icon: "Calendar",
|
|
5675
|
+
permission: { feature: "appointments", action: "read" },
|
|
5676
|
+
// Keys are the PROVIDER's output shape (camelCase) — what filters/
|
|
5677
|
+
// dateField/groupBy match against, not the raw DB columns.
|
|
5678
|
+
fields: [
|
|
5679
|
+
{ key: "startsAt", label: "Starts at", type: "text" },
|
|
5680
|
+
{ key: "endsAt", label: "Ends at", type: "text" },
|
|
5681
|
+
{ key: "status", label: "Status", type: "text" },
|
|
5682
|
+
{ key: "clientName", label: "Client", type: "text", searchable: true },
|
|
5683
|
+
{ key: "professionalName", label: "Professional", type: "text", searchable: true },
|
|
5684
|
+
{ key: "orderTotal", label: "Order total", type: "number" },
|
|
5685
|
+
{ key: "totalDurationMinutes", label: "Duration (min)", type: "number" },
|
|
5686
|
+
{ key: "createdAt", label: "Created at", type: "text" }
|
|
5687
|
+
],
|
|
5688
|
+
data: {
|
|
5689
|
+
table: "v_appointments",
|
|
5690
|
+
tenantScoped: true,
|
|
5691
|
+
searchColumns: ["client_name", "professional_name"]
|
|
5692
|
+
}
|
|
5693
|
+
}
|
|
5694
|
+
}
|
|
5695
|
+
],
|
|
5696
|
+
declaredRpcs: [
|
|
5697
|
+
{
|
|
5698
|
+
name: "agent_agenda_create_appointment",
|
|
5699
|
+
kind: "write",
|
|
5700
|
+
description: "Guarded appointment create: agent_guard (role\u2192plan\u2192bookings_month cap), working hours, race-safe conflict check, audited.",
|
|
5701
|
+
audits: true
|
|
5702
|
+
}
|
|
5703
|
+
],
|
|
5398
5704
|
navigation: [
|
|
5399
5705
|
{
|
|
5400
5706
|
section: options?.navSection ?? "main",
|
|
@@ -5409,7 +5715,9 @@ function createAgendaPlugin(options) {
|
|
|
5409
5715
|
{
|
|
5410
5716
|
path: "/agenda",
|
|
5411
5717
|
component: PageComponent,
|
|
5412
|
-
permission: { feature: "appointments", action: "read" }
|
|
5718
|
+
permission: { feature: "appointments", action: "read" },
|
|
5719
|
+
// Matches the ref agent_agenda_create_appointment returns.
|
|
5720
|
+
entityRoutes: ["schedule:appointment"]
|
|
5413
5721
|
}
|
|
5414
5722
|
],
|
|
5415
5723
|
widgets: [],
|
|
@@ -5438,20 +5746,22 @@ function createAgendaPlugin(options) {
|
|
|
5438
5746
|
{
|
|
5439
5747
|
id: "agenda.create-appointment",
|
|
5440
5748
|
name: "createAppointment",
|
|
5441
|
-
description: "Creates a new appointment
|
|
5749
|
+
description: "Creates a new appointment. Takes IDS, not names: resolve the client/service/professional ids first via findAnything/searchRecords, then call this with an ISO start time in the tenant timezone. The user confirms before booking.",
|
|
5442
5750
|
icon: "CalendarPlus",
|
|
5443
5751
|
mode: "persist",
|
|
5752
|
+
limitKey: "bookings_month",
|
|
5753
|
+
execution: { plane: "server", kind: "rpc", rpc: "agent_agenda_create_appointment" },
|
|
5444
5754
|
category: "Scheduling",
|
|
5445
5755
|
parameters: {
|
|
5446
5756
|
type: "object",
|
|
5447
5757
|
properties: {
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5758
|
+
client_id: { type: "string", description: "Client id (from a search)" },
|
|
5759
|
+
service_id: { type: "string", description: "Service id (from a search)" },
|
|
5760
|
+
professional_id: { type: "string", description: "Professional id (optional \u2014 auto-assigned when omitted)" },
|
|
5761
|
+
starts_at: { type: "string", description: "LOCAL date-time of the business, NO timezone suffix \u2014 e.g. '2026-07-27T10:00' for 10h. The system applies the tenant timezone." },
|
|
5762
|
+
notes: { type: "string", description: "Optional notes" }
|
|
5453
5763
|
},
|
|
5454
|
-
required: ["
|
|
5764
|
+
required: ["client_id", "service_id", "starts_at"]
|
|
5455
5765
|
},
|
|
5456
5766
|
suggestions: [
|
|
5457
5767
|
{ label: "Book a haircut for Sarah tomorrow at 10am" }
|
|
@@ -5489,9 +5799,9 @@ function createAgendaPlugin(options) {
|
|
|
5489
5799
|
icon: "Calendar",
|
|
5490
5800
|
// Standard layout: General | Integrations (Google Calendar addon) | Properties.
|
|
5491
5801
|
component: (() => {
|
|
5492
|
-
const AgendaSettingsTab = () =>
|
|
5802
|
+
const AgendaSettingsTab = () => React13.createElement(PluginSettingsPanel, {
|
|
5493
5803
|
title: "Agenda",
|
|
5494
|
-
generalSettings:
|
|
5804
|
+
generalSettings: React13.createElement(AgendaGeneralSettings),
|
|
5495
5805
|
registries,
|
|
5496
5806
|
hostPluginId: "agenda",
|
|
5497
5807
|
routeBase: "/settings/agenda"
|