@fayz-ai/plugin-agenda 0.8.0 → 0.8.3
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-LFUELUIT.cjs → chunk-3KWNHV5A.cjs} +60 -2
- package/dist/chunk-3KWNHV5A.cjs.map +1 -0
- package/dist/{chunk-DRZF6CV2.js → chunk-YQO6B4G3.js} +60 -3
- package/dist/chunk-YQO6B4G3.js.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 +1071 -766
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +914 -609
- 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 +11 -0
- 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 +6 -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/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 +161 -0
- package/src/migrations/index.ts +1746 -1
- 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 +3 -0
- 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-YQO6B4G3.js';
|
|
2
|
+
export { createMockAgendaProvider } from './chunk-YQO6B4G3.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",
|
|
@@ -5010,7 +4711,7 @@ function createFinancialBridge(provider2) {
|
|
|
5010
4711
|
let typeNames = {};
|
|
5011
4712
|
if (typeIds.length > 0) {
|
|
5012
4713
|
const { data: types } = await supabase.from("plg_financial_payment_method_types").select("id, name").in("id", typeIds);
|
|
5013
|
-
typeNames = Object.fromEntries((types ?? []).map((
|
|
4714
|
+
typeNames = Object.fromEntries((types ?? []).map((t2) => [t2.id, t2.name]));
|
|
5014
4715
|
}
|
|
5015
4716
|
const movements = (movRes.data ?? []).map((m) => ({
|
|
5016
4717
|
id: String(m.id),
|
|
@@ -5111,7 +4812,7 @@ function createFinancialBridge(provider2) {
|
|
|
5111
4812
|
},
|
|
5112
4813
|
async getPaymentMethodTypes() {
|
|
5113
4814
|
const types = await provider2.getPaymentMethodTypes();
|
|
5114
|
-
return types.map((
|
|
4815
|
+
return types.map((t2) => ({ id: t2.id, name: t2.name, transactionType: t2.transactionType }));
|
|
5115
4816
|
},
|
|
5116
4817
|
async getBankAccounts() {
|
|
5117
4818
|
const accounts = await provider2.getBankAccounts();
|
|
@@ -5153,6 +4854,65 @@ function mapInvoiceStatus(status) {
|
|
|
5153
4854
|
return "pending";
|
|
5154
4855
|
}
|
|
5155
4856
|
}
|
|
4857
|
+
|
|
4858
|
+
// src/integrations/google-calendar/mapping.ts
|
|
4859
|
+
function bookingSummary(b) {
|
|
4860
|
+
const services = (b.services ?? []).map((s) => s.name).filter(Boolean).join(", ");
|
|
4861
|
+
const who = b.clientName ?? "";
|
|
4862
|
+
if (services && who) return `${services} \u2014 ${who}`;
|
|
4863
|
+
return services || who || "Appointment";
|
|
4864
|
+
}
|
|
4865
|
+
function bookingToEvent2(b, timeZone) {
|
|
4866
|
+
if (!b.startsAt || !b.endsAt) return null;
|
|
4867
|
+
const notes = [b.notes, b.professionalName ? `Pro: ${b.professionalName}` : "", b.locationName ?? ""].filter(Boolean).join("\n");
|
|
4868
|
+
return {
|
|
4869
|
+
summary: bookingSummary(b),
|
|
4870
|
+
description: notes || void 0,
|
|
4871
|
+
start: { dateTime: b.startsAt, timeZone },
|
|
4872
|
+
end: { dateTime: b.endsAt, timeZone }
|
|
4873
|
+
};
|
|
4874
|
+
}
|
|
4875
|
+
function eventToBookingPatch(ev) {
|
|
4876
|
+
if (ev.status === "cancelled") return { status: "cancelled" };
|
|
4877
|
+
const t2 = eventTimes(ev);
|
|
4878
|
+
return { startsAt: t2.startsAt, endsAt: t2.endsAt };
|
|
4879
|
+
}
|
|
4880
|
+
function eventTimes(ev) {
|
|
4881
|
+
const allDay = !ev.start?.dateTime && !!ev.start?.date;
|
|
4882
|
+
const startsAt = ev.start?.dateTime ?? ev.start?.date ?? "";
|
|
4883
|
+
const endsAt = ev.end?.dateTime ?? ev.end?.date ?? startsAt;
|
|
4884
|
+
return { startsAt, endsAt, allDay };
|
|
4885
|
+
}
|
|
4886
|
+
function channelRowToChannel(r) {
|
|
4887
|
+
return {
|
|
4888
|
+
id: r.id,
|
|
4889
|
+
integrationId: r.integration_id,
|
|
4890
|
+
tenantId: r.tenant_id,
|
|
4891
|
+
googleCalendarId: r.google_calendar_id,
|
|
4892
|
+
summary: r.summary ?? null,
|
|
4893
|
+
color: r.color ?? null,
|
|
4894
|
+
direction: r.direction,
|
|
4895
|
+
targetKind: r.target_kind ?? null,
|
|
4896
|
+
targetId: r.target_id ?? null,
|
|
4897
|
+
importMode: r.import_mode ?? "appointment",
|
|
4898
|
+
syncToken: r.sync_token ?? null,
|
|
4899
|
+
channelId: r.channel_id ?? null,
|
|
4900
|
+
resourceId: r.resource_id ?? null,
|
|
4901
|
+
channelExpiresAt: r.channel_expires_at ?? null,
|
|
4902
|
+
isActive: r.is_active ?? true,
|
|
4903
|
+
createdAt: r.created_at ?? "",
|
|
4904
|
+
updatedAt: r.updated_at ?? ""
|
|
4905
|
+
};
|
|
4906
|
+
}
|
|
4907
|
+
|
|
4908
|
+
// src/integrations/google-calendar/data/tables.ts
|
|
4909
|
+
var GCAL_TABLES = {
|
|
4910
|
+
integrations: "plg_calendar_integrations",
|
|
4911
|
+
channels: "plg_calendar_channels",
|
|
4912
|
+
syncLog: "plg_calendar_sync_log"
|
|
4913
|
+
};
|
|
4914
|
+
|
|
4915
|
+
// src/integrations/google-calendar/data/supabase.ts
|
|
5156
4916
|
var FUNCTION = "google-calendar-sync";
|
|
5157
4917
|
function sb() {
|
|
5158
4918
|
const supabase = getSupabaseClientOptional();
|
|
@@ -5162,7 +4922,7 @@ function sb() {
|
|
|
5162
4922
|
function createGoogleCalendarProvider() {
|
|
5163
4923
|
return {
|
|
5164
4924
|
async getIntegration() {
|
|
5165
|
-
const { data } = await sb().from(
|
|
4925
|
+
const { data } = await sb().from(GCAL_TABLES.integrations).select("*").eq("provider", "google").limit(1).maybeSingle();
|
|
5166
4926
|
if (!data) return null;
|
|
5167
4927
|
return {
|
|
5168
4928
|
id: data.id,
|
|
@@ -5175,24 +4935,24 @@ function createGoogleCalendarProvider() {
|
|
|
5175
4935
|
},
|
|
5176
4936
|
async getConnectUrl(redirectTo) {
|
|
5177
4937
|
const { data, error } = await sb().functions.invoke(FUNCTION, {
|
|
5178
|
-
body: { action: "oauth_start", redirectTo: redirectTo ?? window.location.href }
|
|
4938
|
+
body: { action: "oauth_start", tenantId: getActiveTenantId(), redirectTo: redirectTo ?? window.location.href }
|
|
5179
4939
|
});
|
|
5180
4940
|
if (error) throw new Error(error.message);
|
|
5181
4941
|
return data.url;
|
|
5182
4942
|
},
|
|
5183
4943
|
async setCalendar(calendarId) {
|
|
5184
|
-
await sb().from(
|
|
4944
|
+
await sb().from(GCAL_TABLES.integrations).update({ calendar_id: calendarId, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("provider", "google");
|
|
5185
4945
|
},
|
|
5186
4946
|
async disconnect() {
|
|
5187
|
-
await sb().from(
|
|
4947
|
+
await sb().from(GCAL_TABLES.integrations).update({ active: false, oauth_refresh_token: null, oauth_access_token: null }).eq("provider", "google");
|
|
5188
4948
|
},
|
|
5189
4949
|
async syncNow() {
|
|
5190
|
-
const { data, error } = await sb().functions.invoke(FUNCTION, { body: { action: "pull_events", trigger: "manual" } });
|
|
4950
|
+
const { data, error } = await sb().functions.invoke(FUNCTION, { body: { action: "pull_events", tenantId: getActiveTenantId(), trigger: "manual" } });
|
|
5191
4951
|
if (error) throw new Error(error.message);
|
|
5192
4952
|
return data;
|
|
5193
4953
|
},
|
|
5194
4954
|
async getSyncLog() {
|
|
5195
|
-
const { data } = await sb().from(
|
|
4955
|
+
const { data } = await sb().from(GCAL_TABLES.syncLog).select("*").order("created_at", { ascending: false }).limit(20);
|
|
5196
4956
|
return (data ?? []).map((r) => ({
|
|
5197
4957
|
id: r.id,
|
|
5198
4958
|
direction: r.direction,
|
|
@@ -5203,32 +4963,523 @@ function createGoogleCalendarProvider() {
|
|
|
5203
4963
|
error: r.error ?? void 0,
|
|
5204
4964
|
createdAt: r.created_at
|
|
5205
4965
|
}));
|
|
4966
|
+
},
|
|
4967
|
+
async listGoogleCalendars() {
|
|
4968
|
+
const { data, error } = await sb().functions.invoke(FUNCTION, { body: { action: "list_calendars", tenantId: getActiveTenantId() } });
|
|
4969
|
+
if (error) throw new Error(error.message);
|
|
4970
|
+
return data.calendars ?? [];
|
|
4971
|
+
},
|
|
4972
|
+
async listChannels() {
|
|
4973
|
+
const { data } = await sb().from(GCAL_TABLES.channels).select("*").order("created_at", { ascending: true });
|
|
4974
|
+
return (data ?? []).map(channelRowToChannel);
|
|
4975
|
+
},
|
|
4976
|
+
async saveChannel(input) {
|
|
4977
|
+
const tenantId = getActiveTenantId();
|
|
4978
|
+
let integrationId = input.integrationId;
|
|
4979
|
+
if (!integrationId) {
|
|
4980
|
+
const { data } = await sb().from(GCAL_TABLES.integrations).select("id").eq("provider", "google").limit(1).maybeSingle();
|
|
4981
|
+
integrationId = data?.id;
|
|
4982
|
+
}
|
|
4983
|
+
const row = {
|
|
4984
|
+
integration_id: integrationId,
|
|
4985
|
+
tenant_id: tenantId,
|
|
4986
|
+
google_calendar_id: input.googleCalendarId,
|
|
4987
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
4988
|
+
};
|
|
4989
|
+
if (input.id !== void 0) row.id = input.id;
|
|
4990
|
+
if (input.summary !== void 0) row.summary = input.summary;
|
|
4991
|
+
if (input.color !== void 0) row.color = input.color;
|
|
4992
|
+
if (input.direction !== void 0) row.direction = input.direction;
|
|
4993
|
+
if (input.targetKind !== void 0) row.target_kind = input.targetKind;
|
|
4994
|
+
if (input.targetId !== void 0) row.target_id = input.targetId;
|
|
4995
|
+
if (input.importMode !== void 0) row.import_mode = input.importMode;
|
|
4996
|
+
if (input.isActive !== void 0) row.is_active = input.isActive;
|
|
4997
|
+
const { error } = await sb().from(GCAL_TABLES.channels).upsert(row, { onConflict: "integration_id,google_calendar_id" });
|
|
4998
|
+
if (error) throw new Error(error.message);
|
|
4999
|
+
},
|
|
5000
|
+
async deleteChannel(id) {
|
|
5001
|
+
const { error } = await sb().from(GCAL_TABLES.channels).delete().eq("id", id);
|
|
5002
|
+
if (error) throw new Error(error.message);
|
|
5003
|
+
},
|
|
5004
|
+
async listExternalEvents(params) {
|
|
5005
|
+
const { data, error } = await sb().functions.invoke(FUNCTION, {
|
|
5006
|
+
body: { action: "list_external_events", tenantId: getActiveTenantId(), channelId: params?.channelId, timeMin: params?.timeMin }
|
|
5007
|
+
});
|
|
5008
|
+
if (error) throw new Error(error.message);
|
|
5009
|
+
return data.events ?? [];
|
|
5010
|
+
},
|
|
5011
|
+
async importEvents(items) {
|
|
5012
|
+
const { data, error } = await sb().functions.invoke(FUNCTION, {
|
|
5013
|
+
body: { action: "import_events", tenantId: getActiveTenantId(), items }
|
|
5014
|
+
});
|
|
5015
|
+
if (error) throw new Error(error.message);
|
|
5016
|
+
return data;
|
|
5206
5017
|
}
|
|
5207
5018
|
};
|
|
5208
5019
|
}
|
|
5020
|
+
function CalendarColorDot({
|
|
5021
|
+
color,
|
|
5022
|
+
className = ""
|
|
5023
|
+
}) {
|
|
5024
|
+
return /* @__PURE__ */ jsx(
|
|
5025
|
+
"span",
|
|
5026
|
+
{
|
|
5027
|
+
className: `inline-block h-2.5 w-2.5 shrink-0 rounded-full border border-black/10 ${className}`,
|
|
5028
|
+
style: { backgroundColor: color || "hsl(var(--muted-foreground))" },
|
|
5029
|
+
"aria-hidden": true
|
|
5030
|
+
}
|
|
5031
|
+
);
|
|
5032
|
+
}
|
|
5033
|
+
var EMPTY = { assignee: [], service: [], location: [] };
|
|
5034
|
+
function useAgendaLookups(enabled) {
|
|
5035
|
+
const [lookups, setLookups] = useState(EMPTY);
|
|
5036
|
+
const [loading, setLoading] = useState(false);
|
|
5037
|
+
useEffect(() => {
|
|
5038
|
+
let cancelled = false;
|
|
5039
|
+
setLoading(true);
|
|
5040
|
+
void (async () => {
|
|
5041
|
+
const agenda = createSupabaseAgendaProvider();
|
|
5042
|
+
const tenantId = getActiveTenantId();
|
|
5043
|
+
const publicProvider = tenantId ? createSupabasePublicBookingProvider({ tenantId }) : null;
|
|
5044
|
+
const [assignee, location, service] = await Promise.all([
|
|
5045
|
+
agenda.getProfessionals().then(
|
|
5046
|
+
(rows) => rows.map((p) => ({ id: p.id, name: p.name })),
|
|
5047
|
+
() => []
|
|
5048
|
+
),
|
|
5049
|
+
(agenda.getLocations?.() ?? Promise.resolve([])).then(
|
|
5050
|
+
(rows) => rows.map((l) => ({ id: l.id, name: l.name })),
|
|
5051
|
+
() => []
|
|
5052
|
+
),
|
|
5053
|
+
(publicProvider?.getServices() ?? Promise.resolve([])).then(
|
|
5054
|
+
(rows) => rows.map((s) => ({ id: s.id, name: s.name })),
|
|
5055
|
+
() => []
|
|
5056
|
+
)
|
|
5057
|
+
]);
|
|
5058
|
+
if (cancelled) return;
|
|
5059
|
+
setLookups({ assignee, service, location });
|
|
5060
|
+
setLoading(false);
|
|
5061
|
+
})();
|
|
5062
|
+
return () => {
|
|
5063
|
+
cancelled = true;
|
|
5064
|
+
};
|
|
5065
|
+
}, [enabled]);
|
|
5066
|
+
const hasAny = lookups.assignee.length > 0 || lookups.service.length > 0 || lookups.location.length > 0;
|
|
5067
|
+
return { lookups, loading, hasAny };
|
|
5068
|
+
}
|
|
5069
|
+
function optionsForKind(lookups, kind) {
|
|
5070
|
+
if (kind === "assignee") return lookups.assignee;
|
|
5071
|
+
if (kind === "service") return lookups.service;
|
|
5072
|
+
if (kind === "location") return lookups.location;
|
|
5073
|
+
return [];
|
|
5074
|
+
}
|
|
5075
|
+
var DIRECTION_LABELS = {
|
|
5076
|
+
bidirectional: "Two-way",
|
|
5077
|
+
inbound: "Import only",
|
|
5078
|
+
outbound: "Export only",
|
|
5079
|
+
off: "Off"
|
|
5080
|
+
};
|
|
5081
|
+
var IMPORT_MODE_LABELS = {
|
|
5082
|
+
block: "Block",
|
|
5083
|
+
appointment: "Appointment"
|
|
5084
|
+
};
|
|
5085
|
+
var TARGET_KIND_LABELS = {
|
|
5086
|
+
agenda: "Whole agenda",
|
|
5087
|
+
assignee: "Professional",
|
|
5088
|
+
service: "Service",
|
|
5089
|
+
location: "Location"
|
|
5090
|
+
};
|
|
5091
|
+
var NO_TARGET = "__none__";
|
|
5092
|
+
function defaultChannel() {
|
|
5093
|
+
return { direction: "bidirectional", targetKind: null, targetId: null, importMode: "block" };
|
|
5094
|
+
}
|
|
5095
|
+
function ChannelMappingList({
|
|
5096
|
+
provider: provider2,
|
|
5097
|
+
onChannelsChange
|
|
5098
|
+
}) {
|
|
5099
|
+
const [calendars, setCalendars] = useState([]);
|
|
5100
|
+
const [byGcalId, setByGcalId] = useState({});
|
|
5101
|
+
const [loading, setLoading] = useState(true);
|
|
5102
|
+
const [error, setError] = useState(null);
|
|
5103
|
+
const [savingId, setSavingId] = useState(null);
|
|
5104
|
+
const { lookups, hasAny: hasLookups } = useAgendaLookups(true);
|
|
5105
|
+
const load = useCallback(async () => {
|
|
5106
|
+
setLoading(true);
|
|
5107
|
+
setError(null);
|
|
5108
|
+
try {
|
|
5109
|
+
const [cals, channels] = await Promise.all([
|
|
5110
|
+
provider2.listGoogleCalendars(),
|
|
5111
|
+
provider2.listChannels()
|
|
5112
|
+
]);
|
|
5113
|
+
setCalendars(cals);
|
|
5114
|
+
const map = {};
|
|
5115
|
+
for (const ch of channels) map[ch.googleCalendarId] = ch;
|
|
5116
|
+
setByGcalId(map);
|
|
5117
|
+
onChannelsChange?.(channels);
|
|
5118
|
+
} catch (e) {
|
|
5119
|
+
setError(e?.message ?? "Could not load Google calendars");
|
|
5120
|
+
} finally {
|
|
5121
|
+
setLoading(false);
|
|
5122
|
+
}
|
|
5123
|
+
}, [provider2, onChannelsChange]);
|
|
5124
|
+
useEffect(() => {
|
|
5125
|
+
void load();
|
|
5126
|
+
}, [load]);
|
|
5127
|
+
const persist = useCallback(
|
|
5128
|
+
async (cal, patch) => {
|
|
5129
|
+
const existing = byGcalId[cal.id];
|
|
5130
|
+
const base = existing ?? { ...defaultChannel(), isActive: true };
|
|
5131
|
+
setSavingId(cal.id);
|
|
5132
|
+
try {
|
|
5133
|
+
await provider2.saveChannel({
|
|
5134
|
+
id: existing?.id,
|
|
5135
|
+
googleCalendarId: cal.id,
|
|
5136
|
+
summary: cal.summary,
|
|
5137
|
+
color: cal.backgroundColor ?? null,
|
|
5138
|
+
direction: patch.direction ?? base.direction,
|
|
5139
|
+
targetKind: patch.targetKind !== void 0 ? patch.targetKind : base.targetKind,
|
|
5140
|
+
targetId: patch.targetId !== void 0 ? patch.targetId : base.targetId ?? null,
|
|
5141
|
+
importMode: patch.importMode ?? base.importMode,
|
|
5142
|
+
isActive: patch.isActive !== void 0 ? patch.isActive : base.isActive
|
|
5143
|
+
});
|
|
5144
|
+
const channels = await provider2.listChannels();
|
|
5145
|
+
const map = {};
|
|
5146
|
+
for (const ch of channels) map[ch.googleCalendarId] = ch;
|
|
5147
|
+
setByGcalId(map);
|
|
5148
|
+
onChannelsChange?.(channels);
|
|
5149
|
+
} catch (e) {
|
|
5150
|
+
toast.error(e?.message ?? "Could not save calendar mapping");
|
|
5151
|
+
} finally {
|
|
5152
|
+
setSavingId(null);
|
|
5153
|
+
}
|
|
5154
|
+
},
|
|
5155
|
+
[byGcalId, provider2, onChannelsChange]
|
|
5156
|
+
);
|
|
5157
|
+
if (loading) {
|
|
5158
|
+
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" }) });
|
|
5159
|
+
}
|
|
5160
|
+
if (error) {
|
|
5161
|
+
return /* @__PURE__ */ jsxs("div", { className: "rounded-md border border-warning/40 bg-warning-soft/40 px-3 py-3 text-sm", children: [
|
|
5162
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-warning-soft-foreground", children: [
|
|
5163
|
+
/* @__PURE__ */ jsx(AlertCircle, { className: "h-4 w-4" }),
|
|
5164
|
+
" ",
|
|
5165
|
+
error
|
|
5166
|
+
] }),
|
|
5167
|
+
/* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", className: "mt-2", onClick: () => void load(), children: [
|
|
5168
|
+
/* @__PURE__ */ jsx(RefreshCw, { className: "h-3.5 w-3.5" }),
|
|
5169
|
+
" Retry"
|
|
5170
|
+
] })
|
|
5171
|
+
] });
|
|
5172
|
+
}
|
|
5173
|
+
if (calendars.length === 0) {
|
|
5174
|
+
return /* @__PURE__ */ jsx("p", { className: "py-6 text-center text-sm text-muted-foreground", children: "No Google calendars found on this account." });
|
|
5175
|
+
}
|
|
5176
|
+
return /* @__PURE__ */ jsxs("div", { className: "rounded-md border divide-y", children: [
|
|
5177
|
+
calendars.map((cal) => {
|
|
5178
|
+
const ch = byGcalId[cal.id];
|
|
5179
|
+
const active = !!ch?.isActive;
|
|
5180
|
+
const saving = savingId === cal.id;
|
|
5181
|
+
const direction = ch?.direction ?? "bidirectional";
|
|
5182
|
+
const targetKind = ch?.targetKind ?? null;
|
|
5183
|
+
const importMode = ch?.importMode ?? "block";
|
|
5184
|
+
const canImport = direction === "inbound" || direction === "bidirectional";
|
|
5185
|
+
const kindOptions = optionsForKind(lookups, targetKind);
|
|
5186
|
+
return /* @__PURE__ */ jsxs("div", { className: "px-3 py-3 space-y-3", children: [
|
|
5187
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5", children: [
|
|
5188
|
+
/* @__PURE__ */ jsx(
|
|
5189
|
+
Checkbox,
|
|
5190
|
+
{
|
|
5191
|
+
checked: active,
|
|
5192
|
+
disabled: saving,
|
|
5193
|
+
onChange: (next) => void persist(cal, { isActive: next })
|
|
5194
|
+
}
|
|
5195
|
+
),
|
|
5196
|
+
/* @__PURE__ */ jsx(CalendarColorDot, { color: cal.backgroundColor }),
|
|
5197
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium truncate", children: cal.summary }),
|
|
5198
|
+
cal.primary && /* @__PURE__ */ jsx(Badge, { variant: "outline", className: "shrink-0", children: "Primary" }),
|
|
5199
|
+
saving && /* @__PURE__ */ jsx(Loader2, { className: "ml-auto h-3.5 w-3.5 animate-spin text-muted-foreground" })
|
|
5200
|
+
] }),
|
|
5201
|
+
active && /* @__PURE__ */ jsxs("div", { className: "grid gap-2 pl-[26px] sm:grid-cols-2 lg:grid-cols-4", children: [
|
|
5202
|
+
/* @__PURE__ */ jsxs("label", { className: "block", children: [
|
|
5203
|
+
/* @__PURE__ */ jsx("span", { className: "text-[11px] font-medium text-muted-foreground", children: "Direction" }),
|
|
5204
|
+
/* @__PURE__ */ jsxs(
|
|
5205
|
+
Select,
|
|
5206
|
+
{
|
|
5207
|
+
value: direction,
|
|
5208
|
+
onValueChange: (v) => void persist(cal, { direction: v }),
|
|
5209
|
+
children: [
|
|
5210
|
+
/* @__PURE__ */ jsx(SelectTrigger, { className: "mt-1 h-8 text-sm", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
|
|
5211
|
+
/* @__PURE__ */ jsx(SelectContent, { children: Object.keys(DIRECTION_LABELS).map((d) => /* @__PURE__ */ jsx(SelectItem, { value: d, children: DIRECTION_LABELS[d] }, d)) })
|
|
5212
|
+
]
|
|
5213
|
+
}
|
|
5214
|
+
)
|
|
5215
|
+
] }),
|
|
5216
|
+
/* @__PURE__ */ jsxs("label", { className: "block", children: [
|
|
5217
|
+
/* @__PURE__ */ jsx("span", { className: "text-[11px] font-medium text-muted-foreground", children: "Target" }),
|
|
5218
|
+
/* @__PURE__ */ jsxs(
|
|
5219
|
+
Select,
|
|
5220
|
+
{
|
|
5221
|
+
value: targetKind ?? "agenda",
|
|
5222
|
+
onValueChange: (v) => void persist(cal, {
|
|
5223
|
+
targetKind: v === "agenda" ? null : v,
|
|
5224
|
+
// Reset the id whenever the kind changes.
|
|
5225
|
+
targetId: null
|
|
5226
|
+
}),
|
|
5227
|
+
children: [
|
|
5228
|
+
/* @__PURE__ */ jsx(SelectTrigger, { className: "mt-1 h-8 text-sm", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
|
|
5229
|
+
/* @__PURE__ */ jsx(SelectContent, { children: Object.keys(TARGET_KIND_LABELS).map(
|
|
5230
|
+
(k) => /* @__PURE__ */ jsx(SelectItem, { value: k, children: TARGET_KIND_LABELS[k] }, k)
|
|
5231
|
+
) })
|
|
5232
|
+
]
|
|
5233
|
+
}
|
|
5234
|
+
)
|
|
5235
|
+
] }),
|
|
5236
|
+
targetKind && /* @__PURE__ */ jsxs("label", { className: "block", children: [
|
|
5237
|
+
/* @__PURE__ */ jsx("span", { className: "text-[11px] font-medium text-muted-foreground", children: TARGET_KIND_LABELS[targetKind] }),
|
|
5238
|
+
kindOptions.length > 0 ? /* @__PURE__ */ jsxs(
|
|
5239
|
+
Select,
|
|
5240
|
+
{
|
|
5241
|
+
value: ch?.targetId ?? NO_TARGET,
|
|
5242
|
+
onValueChange: (v) => void persist(cal, { targetId: v === NO_TARGET ? null : v }),
|
|
5243
|
+
children: [
|
|
5244
|
+
/* @__PURE__ */ jsx(SelectTrigger, { className: "mt-1 h-8 text-sm", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "Select\u2026" }) }),
|
|
5245
|
+
/* @__PURE__ */ jsx(SelectContent, { children: kindOptions.map((o) => /* @__PURE__ */ jsx(SelectItem, { value: o.id, children: o.name }, o.id)) })
|
|
5246
|
+
]
|
|
5247
|
+
}
|
|
5248
|
+
) : /* @__PURE__ */ jsx(
|
|
5249
|
+
"input",
|
|
5250
|
+
{
|
|
5251
|
+
defaultValue: ch?.targetId ?? "",
|
|
5252
|
+
placeholder: "uuid",
|
|
5253
|
+
onBlur: (e) => void persist(cal, { targetId: e.target.value.trim() || null }),
|
|
5254
|
+
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"
|
|
5255
|
+
}
|
|
5256
|
+
)
|
|
5257
|
+
] }),
|
|
5258
|
+
canImport && /* @__PURE__ */ jsxs("label", { className: "block", children: [
|
|
5259
|
+
/* @__PURE__ */ jsx("span", { className: "text-[11px] font-medium text-muted-foreground", children: "Import as" }),
|
|
5260
|
+
/* @__PURE__ */ jsxs(
|
|
5261
|
+
Select,
|
|
5262
|
+
{
|
|
5263
|
+
value: importMode,
|
|
5264
|
+
onValueChange: (v) => void persist(cal, { importMode: v }),
|
|
5265
|
+
children: [
|
|
5266
|
+
/* @__PURE__ */ jsx(SelectTrigger, { className: "mt-1 h-8 text-sm", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
|
|
5267
|
+
/* @__PURE__ */ jsx(SelectContent, { children: Object.keys(IMPORT_MODE_LABELS).map((m) => /* @__PURE__ */ jsx(SelectItem, { value: m, children: IMPORT_MODE_LABELS[m] }, m)) })
|
|
5268
|
+
]
|
|
5269
|
+
}
|
|
5270
|
+
)
|
|
5271
|
+
] })
|
|
5272
|
+
] })
|
|
5273
|
+
] }, cal.id);
|
|
5274
|
+
}),
|
|
5275
|
+
!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." })
|
|
5276
|
+
] });
|
|
5277
|
+
}
|
|
5278
|
+
function formatWhen(ev) {
|
|
5279
|
+
const start = new Date(ev.startsAt);
|
|
5280
|
+
if (ev.allDay) {
|
|
5281
|
+
return start.toLocaleDateString("pt-BR", { day: "2-digit", month: "short", year: "numeric" });
|
|
5282
|
+
}
|
|
5283
|
+
return start.toLocaleString("pt-BR", {
|
|
5284
|
+
day: "2-digit",
|
|
5285
|
+
month: "short",
|
|
5286
|
+
hour: "2-digit",
|
|
5287
|
+
minute: "2-digit"
|
|
5288
|
+
});
|
|
5289
|
+
}
|
|
5290
|
+
function ImportEventsWizard({
|
|
5291
|
+
provider: provider2,
|
|
5292
|
+
channels,
|
|
5293
|
+
onClose,
|
|
5294
|
+
onImported
|
|
5295
|
+
}) {
|
|
5296
|
+
const [events, setEvents] = useState([]);
|
|
5297
|
+
const [selected, setSelected] = useState(/* @__PURE__ */ new Set());
|
|
5298
|
+
const [loading, setLoading] = useState(true);
|
|
5299
|
+
const [error, setError] = useState(null);
|
|
5300
|
+
const [importing, setImporting] = useState(false);
|
|
5301
|
+
const [result, setResult] = useState(null);
|
|
5302
|
+
const channelById = useMemo(() => {
|
|
5303
|
+
const m = {};
|
|
5304
|
+
for (const c of channels) m[c.id] = c;
|
|
5305
|
+
return m;
|
|
5306
|
+
}, [channels]);
|
|
5307
|
+
const load = useCallback(async () => {
|
|
5308
|
+
setLoading(true);
|
|
5309
|
+
setError(null);
|
|
5310
|
+
setResult(null);
|
|
5311
|
+
try {
|
|
5312
|
+
const rows = await provider2.listExternalEvents();
|
|
5313
|
+
setEvents(rows);
|
|
5314
|
+
setSelected(new Set(rows.map((r) => r.eventId)));
|
|
5315
|
+
} catch (e) {
|
|
5316
|
+
setError(e?.message ?? "Could not load upcoming events");
|
|
5317
|
+
} finally {
|
|
5318
|
+
setLoading(false);
|
|
5319
|
+
}
|
|
5320
|
+
}, [provider2]);
|
|
5321
|
+
useEffect(() => {
|
|
5322
|
+
void load();
|
|
5323
|
+
}, [load]);
|
|
5324
|
+
const toggle = (eventId) => setSelected((prev) => {
|
|
5325
|
+
const next = new Set(prev);
|
|
5326
|
+
if (next.has(eventId)) next.delete(eventId);
|
|
5327
|
+
else next.add(eventId);
|
|
5328
|
+
return next;
|
|
5329
|
+
});
|
|
5330
|
+
const allSelected = events.length > 0 && selected.size === events.length;
|
|
5331
|
+
const toggleAll = () => setSelected(allSelected ? /* @__PURE__ */ new Set() : new Set(events.map((e) => e.eventId)));
|
|
5332
|
+
async function handleImport() {
|
|
5333
|
+
const items = events.filter((e) => selected.has(e.eventId)).map((e) => ({ channelId: e.channelId, eventId: e.eventId }));
|
|
5334
|
+
if (items.length === 0) return;
|
|
5335
|
+
setImporting(true);
|
|
5336
|
+
try {
|
|
5337
|
+
const res = await provider2.importEvents(items);
|
|
5338
|
+
setResult(res);
|
|
5339
|
+
onImported?.(res);
|
|
5340
|
+
toast.success(`Imported ${res.imported} \xB7 skipped ${res.skipped}`);
|
|
5341
|
+
await load();
|
|
5342
|
+
} catch (e) {
|
|
5343
|
+
toast.error(e?.message ?? "Import failed");
|
|
5344
|
+
} finally {
|
|
5345
|
+
setImporting(false);
|
|
5346
|
+
}
|
|
5347
|
+
}
|
|
5348
|
+
return /* @__PURE__ */ jsxs("div", { className: "rounded-md border bg-card", children: [
|
|
5349
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-b px-3 py-2", children: [
|
|
5350
|
+
/* @__PURE__ */ jsx(CalendarPlus, { className: "h-4 w-4 text-muted-foreground" }),
|
|
5351
|
+
/* @__PURE__ */ jsx("h4", { className: "text-sm font-semibold", children: "Import future events" }),
|
|
5352
|
+
/* @__PURE__ */ jsx(
|
|
5353
|
+
"button",
|
|
5354
|
+
{
|
|
5355
|
+
onClick: onClose,
|
|
5356
|
+
className: "ml-auto rounded p-1 text-muted-foreground hover:bg-muted/50",
|
|
5357
|
+
"aria-label": "Close",
|
|
5358
|
+
children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5" })
|
|
5359
|
+
}
|
|
5360
|
+
)
|
|
5361
|
+
] }),
|
|
5362
|
+
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: [
|
|
5363
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm text-warning-soft-foreground", children: [
|
|
5364
|
+
/* @__PURE__ */ jsx(AlertCircle, { className: "h-4 w-4" }),
|
|
5365
|
+
" ",
|
|
5366
|
+
error
|
|
5367
|
+
] }),
|
|
5368
|
+
/* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", className: "mt-2", onClick: () => void load(), children: [
|
|
5369
|
+
/* @__PURE__ */ jsx(RefreshCw, { className: "h-3.5 w-3.5" }),
|
|
5370
|
+
" Retry"
|
|
5371
|
+
] })
|
|
5372
|
+
] }) : events.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "py-8 px-4 text-center space-y-1", children: [
|
|
5373
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: "You're all caught up" }),
|
|
5374
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "No upcoming Google events left to import on your active calendars." }),
|
|
5375
|
+
result && /* @__PURE__ */ jsxs("p", { className: "text-xs text-success", children: [
|
|
5376
|
+
"Imported ",
|
|
5377
|
+
result.imported,
|
|
5378
|
+
" \xB7 skipped ",
|
|
5379
|
+
result.skipped,
|
|
5380
|
+
result.errors.length > 0 ? ` \xB7 ${result.errors.length} errors` : ""
|
|
5381
|
+
] })
|
|
5382
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
5383
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5 border-b px-3 py-2", children: [
|
|
5384
|
+
/* @__PURE__ */ jsx(Checkbox, { checked: allSelected, onChange: toggleAll }),
|
|
5385
|
+
/* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
|
|
5386
|
+
selected.size,
|
|
5387
|
+
" of ",
|
|
5388
|
+
events.length,
|
|
5389
|
+
" selected"
|
|
5390
|
+
] })
|
|
5391
|
+
] }),
|
|
5392
|
+
/* @__PURE__ */ jsx("div", { className: "max-h-[320px] overflow-y-auto divide-y", children: events.map((ev) => {
|
|
5393
|
+
const ch = channelById[ev.channelId];
|
|
5394
|
+
return /* @__PURE__ */ jsxs(
|
|
5395
|
+
"label",
|
|
5396
|
+
{
|
|
5397
|
+
className: "flex items-start gap-2.5 px-3 py-2.5 cursor-pointer hover:bg-muted/30",
|
|
5398
|
+
children: [
|
|
5399
|
+
/* @__PURE__ */ jsx(
|
|
5400
|
+
Checkbox,
|
|
5401
|
+
{
|
|
5402
|
+
checked: selected.has(ev.eventId),
|
|
5403
|
+
onChange: () => toggle(ev.eventId)
|
|
5404
|
+
}
|
|
5405
|
+
),
|
|
5406
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1 space-y-1", children: [
|
|
5407
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
5408
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium truncate", children: ev.summary || "(no title)" }),
|
|
5409
|
+
ev.allDay && /* @__PURE__ */ jsx(Badge, { variant: "secondary", className: "shrink-0", children: "All-day" })
|
|
5410
|
+
] }),
|
|
5411
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
|
|
5412
|
+
/* @__PURE__ */ jsx("span", { children: formatWhen(ev) }),
|
|
5413
|
+
/* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1", children: [
|
|
5414
|
+
/* @__PURE__ */ jsx(CalendarColorDot, { color: ch?.color }),
|
|
5415
|
+
ch?.summary ?? ev.calendarId
|
|
5416
|
+
] })
|
|
5417
|
+
] })
|
|
5418
|
+
] })
|
|
5419
|
+
]
|
|
5420
|
+
},
|
|
5421
|
+
ev.eventId
|
|
5422
|
+
);
|
|
5423
|
+
}) }),
|
|
5424
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-t px-3 py-2", children: [
|
|
5425
|
+
result && /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
|
|
5426
|
+
"Last run: ",
|
|
5427
|
+
result.imported,
|
|
5428
|
+
" imported",
|
|
5429
|
+
result.errors.length > 0 ? `, ${result.errors.length} errors` : ""
|
|
5430
|
+
] }),
|
|
5431
|
+
/* @__PURE__ */ jsxs(
|
|
5432
|
+
Button,
|
|
5433
|
+
{
|
|
5434
|
+
size: "sm",
|
|
5435
|
+
className: "ml-auto",
|
|
5436
|
+
disabled: importing || selected.size === 0,
|
|
5437
|
+
onClick: handleImport,
|
|
5438
|
+
children: [
|
|
5439
|
+
importing ? /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsx(CheckCircle2, { className: "h-3.5 w-3.5" }),
|
|
5440
|
+
"Import ",
|
|
5441
|
+
selected.size > 0 ? selected.size : ""
|
|
5442
|
+
]
|
|
5443
|
+
}
|
|
5444
|
+
)
|
|
5445
|
+
] })
|
|
5446
|
+
] })
|
|
5447
|
+
] });
|
|
5448
|
+
}
|
|
5209
5449
|
var provider = createGoogleCalendarProvider();
|
|
5450
|
+
function inboundChannels(channels) {
|
|
5451
|
+
return channels.filter(
|
|
5452
|
+
(c) => c.isActive && (c.direction === "inbound" || c.direction === "bidirectional")
|
|
5453
|
+
);
|
|
5454
|
+
}
|
|
5210
5455
|
function GoogleCalendarExtraPanel() {
|
|
5211
5456
|
const [connected, setConnected] = useState(false);
|
|
5212
|
-
const [
|
|
5457
|
+
const [lastSyncAt, setLastSyncAt] = useState();
|
|
5213
5458
|
const [syncing, setSyncing] = useState(false);
|
|
5214
5459
|
const [log, setLog] = useState([]);
|
|
5215
5460
|
const [loaded, setLoaded] = useState(false);
|
|
5461
|
+
const [channels, setChannels] = useState([]);
|
|
5462
|
+
const [showMapping, setShowMapping] = useState(false);
|
|
5463
|
+
const [showWizard, setShowWizard] = useState(false);
|
|
5216
5464
|
async function load() {
|
|
5217
5465
|
const integ = await provider.getIntegration();
|
|
5218
5466
|
setConnected(!!integ?.connected);
|
|
5219
5467
|
if (integ) {
|
|
5220
|
-
|
|
5468
|
+
setLastSyncAt(integ.lastSyncAt);
|
|
5221
5469
|
void provider.getSyncLog().then(setLog);
|
|
5470
|
+
void provider.listChannels().then((ch) => {
|
|
5471
|
+
setChannels(ch);
|
|
5472
|
+
if (ch.length > 0) setShowMapping(true);
|
|
5473
|
+
});
|
|
5222
5474
|
}
|
|
5223
5475
|
setLoaded(true);
|
|
5224
5476
|
}
|
|
5225
5477
|
useEffect(() => {
|
|
5226
5478
|
void load();
|
|
5227
5479
|
}, []);
|
|
5228
|
-
if (!loaded
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
toast.success("Saved");
|
|
5480
|
+
if (!loaded) return null;
|
|
5481
|
+
if (!connected) {
|
|
5482
|
+
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
5483
|
}
|
|
5233
5484
|
async function handleSyncNow() {
|
|
5234
5485
|
setSyncing(true);
|
|
@@ -5236,48 +5487,81 @@ function GoogleCalendarExtraPanel() {
|
|
|
5236
5487
|
const r = await provider.syncNow();
|
|
5237
5488
|
toast.success(`Synced \u2014 ${r.written} updated`);
|
|
5238
5489
|
void provider.getSyncLog().then(setLog);
|
|
5490
|
+
void provider.getIntegration().then((i) => setLastSyncAt(i?.lastSyncAt));
|
|
5239
5491
|
} catch (e) {
|
|
5240
5492
|
toast.error(e?.message ?? "Sync failed");
|
|
5241
5493
|
} finally {
|
|
5242
5494
|
setSyncing(false);
|
|
5243
5495
|
}
|
|
5244
5496
|
}
|
|
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
|
-
)
|
|
5497
|
+
const canImport = inboundChannels(channels).length > 0;
|
|
5498
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-5 border-t pt-4", children: [
|
|
5499
|
+
/* @__PURE__ */ jsxs("section", { className: "space-y-2", children: [
|
|
5500
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
5501
|
+
/* @__PURE__ */ jsx(CalendarCog, { className: "h-4 w-4 text-muted-foreground" }),
|
|
5502
|
+
/* @__PURE__ */ jsx("h4", { className: "text-sm font-semibold", children: "Calendars" })
|
|
5258
5503
|
] }),
|
|
5259
|
-
/* @__PURE__ */ jsx(
|
|
5260
|
-
|
|
5261
|
-
/* @__PURE__ */
|
|
5262
|
-
|
|
5504
|
+
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: [
|
|
5505
|
+
/* @__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." }),
|
|
5506
|
+
/* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", onClick: () => setShowMapping(true), children: [
|
|
5507
|
+
/* @__PURE__ */ jsx(CalendarCog, { className: "h-3.5 w-3.5" }),
|
|
5508
|
+
" Configure calendars"
|
|
5509
|
+
] })
|
|
5263
5510
|
] })
|
|
5264
5511
|
] }),
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5512
|
+
showMapping && /* @__PURE__ */ jsx("section", { className: "space-y-2", children: showWizard ? /* @__PURE__ */ jsx(
|
|
5513
|
+
ImportEventsWizard,
|
|
5514
|
+
{
|
|
5515
|
+
provider,
|
|
5516
|
+
channels: inboundChannels(channels),
|
|
5517
|
+
onClose: () => setShowWizard(false),
|
|
5518
|
+
onImported: () => {
|
|
5519
|
+
void provider.getSyncLog().then(setLog);
|
|
5520
|
+
void provider.getIntegration().then((i) => setLastSyncAt(i?.lastSyncAt));
|
|
5521
|
+
}
|
|
5522
|
+
}
|
|
5523
|
+
) : /* @__PURE__ */ jsxs(
|
|
5524
|
+
Button,
|
|
5525
|
+
{
|
|
5526
|
+
variant: "outline",
|
|
5527
|
+
size: "sm",
|
|
5528
|
+
disabled: !canImport,
|
|
5529
|
+
title: canImport ? void 0 : "Enable an importing calendar first",
|
|
5530
|
+
onClick: () => setShowWizard(true),
|
|
5531
|
+
children: [
|
|
5532
|
+
/* @__PURE__ */ jsx(CalendarPlus, { className: "h-3.5 w-3.5" }),
|
|
5533
|
+
" Import future events"
|
|
5534
|
+
]
|
|
5535
|
+
}
|
|
5536
|
+
) }),
|
|
5537
|
+
/* @__PURE__ */ jsxs("section", { className: "space-y-2", children: [
|
|
5538
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [
|
|
5539
|
+
/* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", onClick: handleSyncNow, disabled: syncing, children: [
|
|
5540
|
+
/* @__PURE__ */ jsx(RefreshCw, { className: `h-3.5 w-3.5 ${syncing ? "animate-spin" : ""}` }),
|
|
5541
|
+
" Sync now"
|
|
5542
|
+
] }),
|
|
5543
|
+
lastSyncAt && /* @__PURE__ */ jsxs("span", { className: "text-[11px] text-muted-foreground", children: [
|
|
5544
|
+
"Last sync: ",
|
|
5545
|
+
new Date(lastSyncAt).toLocaleString()
|
|
5546
|
+
] })
|
|
5269
5547
|
] }),
|
|
5270
|
-
|
|
5271
|
-
/* @__PURE__ */
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
r.written,
|
|
5275
|
-
" written / ",
|
|
5276
|
-
r.fetched,
|
|
5277
|
-
" fetched"
|
|
5548
|
+
log.length > 0 && /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
5549
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
5550
|
+
/* @__PURE__ */ jsx(History, { className: "h-4 w-4 text-muted-foreground" }),
|
|
5551
|
+
/* @__PURE__ */ jsx("h4", { className: "text-sm font-semibold", children: "History" })
|
|
5278
5552
|
] }),
|
|
5279
|
-
|
|
5280
|
-
|
|
5553
|
+
/* @__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: [
|
|
5554
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-xs", children: new Date(r.createdAt).toLocaleString() }),
|
|
5555
|
+
/* @__PURE__ */ jsx("span", { className: "text-[10px] uppercase tracking-wide px-1 py-px rounded bg-muted text-muted-foreground", children: r.direction }),
|
|
5556
|
+
/* @__PURE__ */ jsxs("span", { className: "ml-auto text-xs", children: [
|
|
5557
|
+
r.written,
|
|
5558
|
+
" written / ",
|
|
5559
|
+
r.fetched,
|
|
5560
|
+
" fetched"
|
|
5561
|
+
] }),
|
|
5562
|
+
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" })
|
|
5563
|
+
] }, r.id)) })
|
|
5564
|
+
] })
|
|
5281
5565
|
] })
|
|
5282
5566
|
] });
|
|
5283
5567
|
}
|
|
@@ -5312,31 +5596,6 @@ var googleCalendarConnector = {
|
|
|
5312
5596
|
]
|
|
5313
5597
|
};
|
|
5314
5598
|
|
|
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
5599
|
// src/integrations/google-calendar/index.ts
|
|
5341
5600
|
function createGoogleCalendarPlugin() {
|
|
5342
5601
|
return {
|
|
@@ -5358,7 +5617,7 @@ function createAgendaPlugin(options) {
|
|
|
5358
5617
|
const config = resolveConfig(options);
|
|
5359
5618
|
registerTranslations(agendaLocales);
|
|
5360
5619
|
const provider2 = options?.dataProvider ?? createSafeDataProvider(
|
|
5361
|
-
() => createSupabaseAgendaProvider(),
|
|
5620
|
+
() => createSupabaseAgendaProvider({ professionalKind: config.professionalKind }),
|
|
5362
5621
|
() => createMockAgendaProvider()
|
|
5363
5622
|
);
|
|
5364
5623
|
const store = createAgendaStore(provider2, options?.financialBridge);
|
|
@@ -5378,7 +5637,7 @@ function createAgendaPlugin(options) {
|
|
|
5378
5637
|
return locations;
|
|
5379
5638
|
} : void 0
|
|
5380
5639
|
});
|
|
5381
|
-
const PageComponent = () =>
|
|
5640
|
+
const PageComponent = () => React13.createElement(AgendaPage, { config, provider: provider2, store });
|
|
5382
5641
|
return {
|
|
5383
5642
|
id: "agenda",
|
|
5384
5643
|
name: config.labels.pageTitle,
|
|
@@ -5395,6 +5654,48 @@ function createAgendaPlugin(options) {
|
|
|
5395
5654
|
{ id: "appointments", label: config.labels.pageTitle, group: config.labels.pageTitle },
|
|
5396
5655
|
{ id: "agenda.schedules", label: "Schedule Management", group: config.labels.pageTitle }
|
|
5397
5656
|
],
|
|
5657
|
+
// Recurring monthly quota — counts appointment rows created this month.
|
|
5658
|
+
// (Clients/patients are the app's CRUD entity, so their cap is declared by
|
|
5659
|
+
// the app's EntityDef.limitKey, not here.)
|
|
5660
|
+
declaredLimits: [
|
|
5661
|
+
{ key: "bookings_month", label: "Appointments this month", table: "appointments", period: "month" }
|
|
5662
|
+
],
|
|
5663
|
+
queryEntities: [
|
|
5664
|
+
{
|
|
5665
|
+
key: "agenda:appointments",
|
|
5666
|
+
entity: {
|
|
5667
|
+
name: "Appointment",
|
|
5668
|
+
namePlural: "Appointments",
|
|
5669
|
+
icon: "Calendar",
|
|
5670
|
+
permission: { feature: "appointments", action: "read" },
|
|
5671
|
+
// Keys are the PROVIDER's output shape (camelCase) — what filters/
|
|
5672
|
+
// dateField/groupBy match against, not the raw DB columns.
|
|
5673
|
+
fields: [
|
|
5674
|
+
{ key: "startsAt", label: "Starts at", type: "text" },
|
|
5675
|
+
{ key: "endsAt", label: "Ends at", type: "text" },
|
|
5676
|
+
{ key: "status", label: "Status", type: "text" },
|
|
5677
|
+
{ key: "clientName", label: "Client", type: "text", searchable: true },
|
|
5678
|
+
{ key: "professionalName", label: "Professional", type: "text", searchable: true },
|
|
5679
|
+
{ key: "orderTotal", label: "Order total", type: "number" },
|
|
5680
|
+
{ key: "totalDurationMinutes", label: "Duration (min)", type: "number" },
|
|
5681
|
+
{ key: "createdAt", label: "Created at", type: "text" }
|
|
5682
|
+
],
|
|
5683
|
+
data: {
|
|
5684
|
+
table: "v_appointments",
|
|
5685
|
+
tenantScoped: true,
|
|
5686
|
+
searchColumns: ["client_name", "professional_name"]
|
|
5687
|
+
}
|
|
5688
|
+
}
|
|
5689
|
+
}
|
|
5690
|
+
],
|
|
5691
|
+
declaredRpcs: [
|
|
5692
|
+
{
|
|
5693
|
+
name: "agent_agenda_create_appointment",
|
|
5694
|
+
kind: "write",
|
|
5695
|
+
description: "Guarded appointment create: agent_guard (role\u2192plan\u2192bookings_month cap), working hours, race-safe conflict check, audited.",
|
|
5696
|
+
audits: true
|
|
5697
|
+
}
|
|
5698
|
+
],
|
|
5398
5699
|
navigation: [
|
|
5399
5700
|
{
|
|
5400
5701
|
section: options?.navSection ?? "main",
|
|
@@ -5409,7 +5710,9 @@ function createAgendaPlugin(options) {
|
|
|
5409
5710
|
{
|
|
5410
5711
|
path: "/agenda",
|
|
5411
5712
|
component: PageComponent,
|
|
5412
|
-
permission: { feature: "appointments", action: "read" }
|
|
5713
|
+
permission: { feature: "appointments", action: "read" },
|
|
5714
|
+
// Matches the ref agent_agenda_create_appointment returns.
|
|
5715
|
+
entityRoutes: ["schedule:appointment"]
|
|
5413
5716
|
}
|
|
5414
5717
|
],
|
|
5415
5718
|
widgets: [],
|
|
@@ -5438,20 +5741,22 @@ function createAgendaPlugin(options) {
|
|
|
5438
5741
|
{
|
|
5439
5742
|
id: "agenda.create-appointment",
|
|
5440
5743
|
name: "createAppointment",
|
|
5441
|
-
description: "Creates a new appointment
|
|
5744
|
+
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
5745
|
icon: "CalendarPlus",
|
|
5443
5746
|
mode: "persist",
|
|
5747
|
+
limitKey: "bookings_month",
|
|
5748
|
+
execution: { plane: "server", kind: "rpc", rpc: "agent_agenda_create_appointment" },
|
|
5444
5749
|
category: "Scheduling",
|
|
5445
5750
|
parameters: {
|
|
5446
5751
|
type: "object",
|
|
5447
5752
|
properties: {
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5753
|
+
client_id: { type: "string", description: "Client id (from a search)" },
|
|
5754
|
+
service_id: { type: "string", description: "Service id (from a search)" },
|
|
5755
|
+
professional_id: { type: "string", description: "Professional id (optional \u2014 auto-assigned when omitted)" },
|
|
5756
|
+
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." },
|
|
5757
|
+
notes: { type: "string", description: "Optional notes" }
|
|
5453
5758
|
},
|
|
5454
|
-
required: ["
|
|
5759
|
+
required: ["client_id", "service_id", "starts_at"]
|
|
5455
5760
|
},
|
|
5456
5761
|
suggestions: [
|
|
5457
5762
|
{ label: "Book a haircut for Sarah tomorrow at 10am" }
|
|
@@ -5489,9 +5794,9 @@ function createAgendaPlugin(options) {
|
|
|
5489
5794
|
icon: "Calendar",
|
|
5490
5795
|
// Standard layout: General | Integrations (Google Calendar addon) | Properties.
|
|
5491
5796
|
component: (() => {
|
|
5492
|
-
const AgendaSettingsTab = () =>
|
|
5797
|
+
const AgendaSettingsTab = () => React13.createElement(PluginSettingsPanel, {
|
|
5493
5798
|
title: "Agenda",
|
|
5494
|
-
generalSettings:
|
|
5799
|
+
generalSettings: React13.createElement(AgendaGeneralSettings),
|
|
5495
5800
|
registries,
|
|
5496
5801
|
hostPluginId: "agenda",
|
|
5497
5802
|
routeBase: "/settings/agenda"
|