@fayz-ai/plugin-agenda 0.1.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/AgendaMobileList.d.ts +10 -0
- package/dist/components/AgendaMobileList.d.ts.map +1 -0
- package/dist/components/AppointmentModal.d.ts +4 -2
- package/dist/components/AppointmentModal.d.ts.map +1 -1
- package/dist/components/AppointmentPopover.d.ts.map +1 -1
- package/dist/components/EventContextMenu.d.ts.map +1 -1
- package/dist/config.d.ts +62 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/data/index.d.ts +1 -0
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/mock.d.ts +15 -1
- package/dist/data/mock.d.ts.map +1 -1
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/index.cjs +614 -308
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +452 -147
- package/dist/index.js.map +1 -1
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/store.d.ts +3 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/views/CalendarView.d.ts.map +1 -1
- package/package.json +5 -8
- package/src/components/AgendaMobileList.tsx +103 -0
- package/src/components/AppointmentModal.tsx +86 -9
- package/src/components/AppointmentPopover.tsx +20 -10
- package/src/components/EventContextMenu.tsx +3 -0
- package/src/config.ts +104 -5
- package/src/data/index.ts +1 -0
- package/src/data/mock.ts +22 -5
- package/src/data/supabase.ts +13 -2
- package/src/index.ts +8 -3
- package/src/locales/en.ts +12 -0
- package/src/locales/pt-BR.ts +12 -0
- package/src/store.ts +4 -2
- package/src/types.ts +8 -0
- package/src/views/CalendarView.tsx +220 -54
|
@@ -19,6 +19,7 @@ import { AppointmentModal } from '../components/AppointmentModal'
|
|
|
19
19
|
import { AppointmentPopover } from '../components/AppointmentPopover'
|
|
20
20
|
import { PersonLink } from '@fayz-ai/saas'
|
|
21
21
|
import { EventContextMenu } from '../components/EventContextMenu'
|
|
22
|
+
import { AgendaMobileList } from '../components/AgendaMobileList'
|
|
22
23
|
import { ConfirmDialog } from '@fayz-ai/ui'
|
|
23
24
|
import { Button } from '@fayz-ai/ui'
|
|
24
25
|
import type { CalendarBooking, Schedule } from '../types'
|
|
@@ -224,12 +225,55 @@ function CalendarSkeleton({ profCount = 2 }: { profCount?: number }) {
|
|
|
224
225
|
)
|
|
225
226
|
}
|
|
226
227
|
|
|
227
|
-
|
|
228
|
+
// Resource views — one column per professional. Used when the app has staff.
|
|
229
|
+
const RESOURCE_VIEW_OPTIONS = [
|
|
228
230
|
{ key: 'resourceTimeGridDay', labelKey: 'agenda.calendar.viewDay' },
|
|
229
231
|
{ key: 'resourceTimeGridWeek', labelKey: 'agenda.calendar.viewWeek' },
|
|
230
232
|
{ key: 'dayGridMonth', labelKey: 'agenda.calendar.viewMonth' },
|
|
231
233
|
] as const
|
|
232
234
|
|
|
235
|
+
// Non-resource variants — used when there are no professionals/resources (e.g. a
|
|
236
|
+
// personal-finance agenda). A resource timegrid renders an EMPTY grid with zero
|
|
237
|
+
// resource columns and drops events that have no resourceId, so those apps fall
|
|
238
|
+
// back to a plain month/day/week calendar.
|
|
239
|
+
const PLAIN_VIEW_OPTIONS = [
|
|
240
|
+
{ key: 'timeGridDay', labelKey: 'agenda.calendar.viewDay' },
|
|
241
|
+
{ key: 'timeGridWeek', labelKey: 'agenda.calendar.viewWeek' },
|
|
242
|
+
{ key: 'dayGridMonth', labelKey: 'agenda.calendar.viewMonth' },
|
|
243
|
+
{ key: 'listMonth', labelKey: 'agenda.calendar.viewList' },
|
|
244
|
+
] as const
|
|
245
|
+
|
|
246
|
+
// Sentinel view key for the thumb-friendly mobile agenda list (AgendaMobileList),
|
|
247
|
+
// which is NOT a FullCalendar view. Selected by default on <md so B2C opens on the
|
|
248
|
+
// list; picking any grid option (Dia/Semana/Mês) mounts the real calendar instead.
|
|
249
|
+
const MOBILE_LIST_VIEW = '__mobileList__'
|
|
250
|
+
|
|
251
|
+
/** Collapse a resource view to a plain month calendar when there are no resources. */
|
|
252
|
+
function viewForNoResources(view: string): string {
|
|
253
|
+
return view.startsWith('resource') ? 'dayGridMonth' : view
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Promote a plain timegrid to its resource variant when the app has resources. */
|
|
257
|
+
function viewForResources(view: string): string {
|
|
258
|
+
if (view === 'timeGridWeek') return 'resourceTimeGridWeek'
|
|
259
|
+
if (view === 'timeGridDay') return 'resourceTimeGridDay'
|
|
260
|
+
return view
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Small viewport (<md) → render the thumb-friendly agenda list instead of the grid. */
|
|
264
|
+
function useIsMobile(): boolean {
|
|
265
|
+
const [isMobile, setIsMobile] = useState(() =>
|
|
266
|
+
typeof window !== 'undefined' && window.matchMedia('(max-width: 767px)').matches)
|
|
267
|
+
useEffect(() => {
|
|
268
|
+
if (typeof window === 'undefined') return
|
|
269
|
+
const mq = window.matchMedia('(max-width: 767px)')
|
|
270
|
+
const onChange = () => setIsMobile(mq.matches)
|
|
271
|
+
mq.addEventListener('change', onChange)
|
|
272
|
+
return () => mq.removeEventListener('change', onChange)
|
|
273
|
+
}, [])
|
|
274
|
+
return isMobile
|
|
275
|
+
}
|
|
276
|
+
|
|
233
277
|
// ---------------------------------------------------------------------------
|
|
234
278
|
// Calendar View
|
|
235
279
|
// ---------------------------------------------------------------------------
|
|
@@ -268,6 +312,28 @@ export function CalendarView() {
|
|
|
268
312
|
const closeModal = useAgendaStore((s) => s.closeAppointmentModal)
|
|
269
313
|
const modalState = useAgendaStore((s) => s.appointmentModal)
|
|
270
314
|
|
|
315
|
+
// Resource-capable = the app has professionals/staff. When it doesn't (e.g. a
|
|
316
|
+
// personal-finance agenda), resource views render empty, so we force plain
|
|
317
|
+
// (non-resource) views and default to a month calendar.
|
|
318
|
+
const isMobile = useIsMobile()
|
|
319
|
+
const resourceCapable = professionals.length > 0
|
|
320
|
+
const viewOptions = resourceCapable ? RESOURCE_VIEW_OPTIONS : PLAIN_VIEW_OPTIONS
|
|
321
|
+
|
|
322
|
+
// Mobile (<md) has its own view selection, independent of the desktop pref: it
|
|
323
|
+
// defaults to the thumb-friendly list and lets the user switch to any grid view
|
|
324
|
+
// (Dia/Semana/Mês) without changing the desktop's stored calendarView. The list
|
|
325
|
+
// sentinel replaces FullCalendar's own listMonth so mobile always gets the rich
|
|
326
|
+
// AgendaMobileList, and a list option is offered even for resource apps.
|
|
327
|
+
const [mobileView, setMobileView] = useState<string>(MOBILE_LIST_VIEW)
|
|
328
|
+
const mobileViewOptions = useMemo(
|
|
329
|
+
() => [
|
|
330
|
+
...viewOptions.filter((o) => o.key !== 'listMonth'),
|
|
331
|
+
{ key: MOBILE_LIST_VIEW, labelKey: 'agenda.calendar.viewList' },
|
|
332
|
+
],
|
|
333
|
+
[viewOptions],
|
|
334
|
+
)
|
|
335
|
+
const mobileGridActive = isMobile && mobileView !== MOBILE_LIST_VIEW
|
|
336
|
+
|
|
271
337
|
const [selectedBooking, setSelectedBooking] = useState<CalendarBooking | null>(null)
|
|
272
338
|
const [popoverAnchor, setPopoverAnchor] = useState<{ x: number; y: number } | null>(null)
|
|
273
339
|
const [popoverVisible, _setPopoverVisible] = useState(false)
|
|
@@ -302,6 +368,30 @@ export function CalendarView() {
|
|
|
302
368
|
if (config.modules.locationSelection) fetchLocations()
|
|
303
369
|
}, [])
|
|
304
370
|
|
|
371
|
+
// Keep the view consistent with resource-capability, in case a stale/shared
|
|
372
|
+
// pref selected the wrong variant. No resources → a resource view is an empty
|
|
373
|
+
// grid that drops the seeded events; resources → a plain timegrid loses the
|
|
374
|
+
// per-professional columns.
|
|
375
|
+
useEffect(() => {
|
|
376
|
+
const api = calendarRef.current?.getApi()
|
|
377
|
+
if (!api) return
|
|
378
|
+
if (!resourceCapable && currentView.startsWith('resource')) {
|
|
379
|
+
api.changeView('dayGridMonth')
|
|
380
|
+
} else if (resourceCapable && (currentView === 'timeGridWeek' || currentView === 'timeGridDay')) {
|
|
381
|
+
api.changeView(viewForResources(currentView))
|
|
382
|
+
}
|
|
383
|
+
}, [resourceCapable, currentView])
|
|
384
|
+
|
|
385
|
+
// Mobile renders the agenda list (no FullCalendar), so the grid's datesSet —
|
|
386
|
+
// which normally drives fetchBookings — never fires. Fetch a wide upcoming
|
|
387
|
+
// window directly so the list has data.
|
|
388
|
+
useEffect(() => {
|
|
389
|
+
if (!isMobile) return
|
|
390
|
+
const start = new Date(); start.setHours(0, 0, 0, 0); start.setDate(start.getDate() - 1)
|
|
391
|
+
const end = new Date(start); end.setDate(end.getDate() + 91)
|
|
392
|
+
fetchBookings({ start: start.toISOString(), end: end.toISOString() })
|
|
393
|
+
}, [isMobile, fetchBookings])
|
|
394
|
+
|
|
305
395
|
const resources = useMemo(() => {
|
|
306
396
|
const filtered = selectedProfIds.length > 0
|
|
307
397
|
? professionals.filter((p) => selectedProfIds.includes(p.id))
|
|
@@ -335,6 +425,9 @@ export function CalendarView() {
|
|
|
335
425
|
const events = useMemo(() =>
|
|
336
426
|
[...bookingEvents, ...availabilityEvents], [bookingEvents, availabilityEvents])
|
|
337
427
|
|
|
428
|
+
const storedView = prefs.get('calendarView')
|
|
429
|
+
const effectiveInitialView = resourceCapable ? viewForResources(storedView) : viewForNoResources(storedView)
|
|
430
|
+
|
|
338
431
|
// ── Calendar handlers ──
|
|
339
432
|
const handleDatesSet = useCallback((arg: { startStr: string; endStr: string; view: { title: string; type: string } }) => {
|
|
340
433
|
setCalendarTitle(arg.view.title)
|
|
@@ -469,14 +562,16 @@ export function CalendarView() {
|
|
|
469
562
|
openModal('create', { prefill: { startsAt: arg.startStr, endsAt: arg.endStr, professionalId, locationId: locId || undefined } })
|
|
470
563
|
}
|
|
471
564
|
|
|
472
|
-
// Warn if no schedule covers this slot
|
|
473
|
-
|
|
565
|
+
// Warn if no schedule covers this slot — only when the working-hours module
|
|
566
|
+
// is on (off by default in eventMode:'simple', so simple/Google-Calendar
|
|
567
|
+
// events never surface the "no open schedule for this professional" dialog).
|
|
568
|
+
if (config.modules.workingHours && !schedule) {
|
|
474
569
|
setScheduleConfirm({ onConfirm: proceed })
|
|
475
570
|
return
|
|
476
571
|
}
|
|
477
572
|
|
|
478
573
|
proceed()
|
|
479
|
-
}, [openModal, findScheduleForSlot])
|
|
574
|
+
}, [openModal, findScheduleForSlot, config.modules.workingHours])
|
|
480
575
|
|
|
481
576
|
const handleEventDrop = useCallback(async (arg: EventDropArg) => {
|
|
482
577
|
const booking = arg.event.extendedProps.booking as CalendarBooking
|
|
@@ -488,6 +583,16 @@ export function CalendarView() {
|
|
|
488
583
|
calendarRef.current?.getApi().changeView(v)
|
|
489
584
|
prefs.set('calendarView', v)
|
|
490
585
|
}, [prefs])
|
|
586
|
+
// Mobile switcher: swap between the list sentinel and a real grid view. When a
|
|
587
|
+
// grid is picked we also sync the store's currentView so the resource-collapse
|
|
588
|
+
// effect doesn't fight the freshly-mounted mobile calendar; if it's already
|
|
589
|
+
// mounted (grid → grid), changeView animates in place.
|
|
590
|
+
const handleMobileViewChange = useCallback((v: string) => {
|
|
591
|
+
setMobileView(v)
|
|
592
|
+
if (v === MOBILE_LIST_VIEW) return
|
|
593
|
+
setView(v)
|
|
594
|
+
calendarRef.current?.getApi()?.changeView(v)
|
|
595
|
+
}, [setView])
|
|
491
596
|
const handlePrev = useCallback(() => calendarRef.current?.getApi().prev(), [])
|
|
492
597
|
const handleNext = useCallback(() => calendarRef.current?.getApi().next(), [])
|
|
493
598
|
const handleToday = useCallback(() => { calendarRef.current?.getApi().today(); setSelectedDate(new Date()) }, [])
|
|
@@ -587,8 +692,23 @@ export function CalendarView() {
|
|
|
587
692
|
// Listen for cross-plugin booking open requests (e.g., from financial invoice detail)
|
|
588
693
|
useEffect(() => {
|
|
589
694
|
function handleOpenBooking(e: Event) {
|
|
590
|
-
const
|
|
591
|
-
|
|
695
|
+
const detail = (e as CustomEvent).detail ?? {}
|
|
696
|
+
const externalSource = detail.source
|
|
697
|
+
? { type: String(detail.source), id: detail.waitlistId, waitlistId: detail.waitlistId }
|
|
698
|
+
: undefined
|
|
699
|
+
if (detail.bookingId) {
|
|
700
|
+
openModal('edit', { bookingId: detail.bookingId, externalSource })
|
|
701
|
+
return
|
|
702
|
+
}
|
|
703
|
+
openModal('create', {
|
|
704
|
+
prefill: detail.prefill ?? {
|
|
705
|
+
clientId: detail.clientId,
|
|
706
|
+
professionalId: detail.professionalId,
|
|
707
|
+
locationId: detail.locationId,
|
|
708
|
+
startsAt: detail.startsAt,
|
|
709
|
+
},
|
|
710
|
+
externalSource,
|
|
711
|
+
})
|
|
592
712
|
}
|
|
593
713
|
window.addEventListener('agenda:open-booking', handleOpenBooking)
|
|
594
714
|
return () => window.removeEventListener('agenda:open-booking', handleOpenBooking)
|
|
@@ -684,9 +804,11 @@ export function CalendarView() {
|
|
|
684
804
|
{/* Mini month calendar */}
|
|
685
805
|
<MiniCalendar selectedDate={selectedDate} onDateSelect={handleMiniDateSelect} />
|
|
686
806
|
|
|
807
|
+
{/* Agendas (professional filter) — hidden when no booking type uses a
|
|
808
|
+
professional (e.g. eventMode:'simple'). */}
|
|
809
|
+
{config.capabilities.professional && (<>
|
|
687
810
|
<div className="h-px bg-border my-4" />
|
|
688
811
|
|
|
689
|
-
{/* Agendas */}
|
|
690
812
|
<button onClick={() => setProfSectionOpen((p) => !p)}
|
|
691
813
|
className="flex items-center justify-between w-full text-xs font-medium text-muted-foreground hover:text-foreground py-1 transition-colors">
|
|
692
814
|
<span>Agendas</span>
|
|
@@ -710,10 +832,13 @@ export function CalendarView() {
|
|
|
710
832
|
})}
|
|
711
833
|
</div>
|
|
712
834
|
)}
|
|
835
|
+
</>)}
|
|
713
836
|
|
|
837
|
+
{/* Status filter — hidden when no booking type uses a status workflow
|
|
838
|
+
(e.g. eventMode:'simple'). */}
|
|
839
|
+
{config.capabilities.status && (<>
|
|
714
840
|
<div className="h-px bg-border my-4" />
|
|
715
841
|
|
|
716
|
-
{/* Status */}
|
|
717
842
|
<button onClick={() => setStatusSectionOpen((p) => !p)}
|
|
718
843
|
className="flex items-center justify-between w-full text-xs font-medium text-muted-foreground hover:text-foreground py-1 transition-colors">
|
|
719
844
|
<span>Status</span>
|
|
@@ -736,70 +861,105 @@ export function CalendarView() {
|
|
|
736
861
|
})}
|
|
737
862
|
</div>
|
|
738
863
|
)}
|
|
864
|
+
</>)}
|
|
739
865
|
</div>
|
|
740
866
|
|
|
741
867
|
{/* ═══ MAIN AREA ═══ */}
|
|
742
868
|
<div className="flex-1 flex flex-col min-w-0 overflow-hidden">
|
|
743
869
|
{/* Toolbar */}
|
|
744
|
-
<div className="
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
{t('agenda.toolbar.today')}
|
|
755
|
-
</button>
|
|
756
|
-
<div className="flex items-center gap-0.5">
|
|
757
|
-
<button onClick={handlePrev} className="flex h-7 w-7 items-center justify-center rounded-full hover:bg-muted transition-colors">
|
|
758
|
-
<ChevronLeft className="h-4 w-4" />
|
|
870
|
+
<div className="shrink-0 border-b">
|
|
871
|
+
{/* Row 1 — create + date nav + title (left), desktop switcher + settings
|
|
872
|
+
(right). Extra right padding on mobile clears the transparent header's
|
|
873
|
+
floating profile avatar, which otherwise overlaps the settings gear. */}
|
|
874
|
+
<div className="flex items-center justify-between pl-4 pr-14 md:px-4 h-12">
|
|
875
|
+
<div className="flex items-center gap-3 min-w-0">
|
|
876
|
+
{/* Mobile create button */}
|
|
877
|
+
<button onClick={() => openModal('create')}
|
|
878
|
+
className="md:hidden flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground">
|
|
879
|
+
<Plus className="h-4 w-4" />
|
|
759
880
|
</button>
|
|
760
|
-
<button onClick={handleNext} className="flex h-7 w-7 items-center justify-center rounded-full hover:bg-muted transition-colors">
|
|
761
|
-
<ChevronRight className="h-4 w-4" />
|
|
762
|
-
</button>
|
|
763
|
-
</div>
|
|
764
|
-
<h1 className="text-base sm:text-lg font-normal truncate">{calendarTitle}</h1>
|
|
765
|
-
</div>
|
|
766
881
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
: 'text-muted-foreground hover:bg-muted/50'
|
|
775
|
-
}`}>
|
|
776
|
-
{t(opt.labelKey)}
|
|
882
|
+
{/* Date navigation drives the FullCalendar api — shown on desktop and,
|
|
883
|
+
on mobile, only when a grid view is active (the list paginates
|
|
884
|
+
itself, so it has no grid to page through). */}
|
|
885
|
+
<div className={`${mobileGridActive ? 'flex' : 'hidden'} md:flex items-center gap-3`}>
|
|
886
|
+
<button onClick={handleToday}
|
|
887
|
+
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">
|
|
888
|
+
{t('agenda.toolbar.today')}
|
|
777
889
|
</button>
|
|
778
|
-
|
|
890
|
+
<div className="flex items-center gap-0.5">
|
|
891
|
+
<button onClick={handlePrev} className="flex h-7 w-7 items-center justify-center rounded-full hover:bg-muted transition-colors">
|
|
892
|
+
<ChevronLeft className="h-4 w-4" />
|
|
893
|
+
</button>
|
|
894
|
+
<button onClick={handleNext} className="flex h-7 w-7 items-center justify-center rounded-full hover:bg-muted transition-colors">
|
|
895
|
+
<ChevronRight className="h-4 w-4" />
|
|
896
|
+
</button>
|
|
897
|
+
</div>
|
|
898
|
+
</div>
|
|
899
|
+
<h1 className="text-base sm:text-lg font-normal truncate">{isMobile && !mobileGridActive ? t('agenda.calendar.viewAgenda') : calendarTitle}</h1>
|
|
900
|
+
</div>
|
|
901
|
+
|
|
902
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
903
|
+
<div className="hidden md:flex items-center gap-1">
|
|
904
|
+
{viewOptions.map((opt) => (
|
|
905
|
+
<button key={opt.key} onClick={() => handleViewChange(opt.key)}
|
|
906
|
+
className={`rounded-md px-3 py-1 text-sm transition-colors ${
|
|
907
|
+
currentView === opt.key
|
|
908
|
+
? 'bg-primary/10 text-primary font-medium'
|
|
909
|
+
: 'text-muted-foreground hover:bg-muted/50'
|
|
910
|
+
}`}>
|
|
911
|
+
{t(opt.labelKey)}
|
|
912
|
+
</button>
|
|
913
|
+
))}
|
|
914
|
+
</div>
|
|
915
|
+
<Button
|
|
916
|
+
variant="outline"
|
|
917
|
+
size="icon"
|
|
918
|
+
onClick={() => window.location.hash = '/settings/agenda'}
|
|
919
|
+
title="Agenda Settings"
|
|
920
|
+
className="ml-1"
|
|
921
|
+
>
|
|
922
|
+
<Settings className="h-4 w-4" />
|
|
923
|
+
</Button>
|
|
779
924
|
</div>
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
<
|
|
788
|
-
|
|
925
|
+
</div>
|
|
926
|
+
|
|
927
|
+
{/* Row 2 (mobile only) — compact segmented view switcher on its own row so
|
|
928
|
+
it never collides with the floating avatar. Dia/Semana/Mês mount the
|
|
929
|
+
calendar; Lista returns to the thumb-friendly agenda list. */}
|
|
930
|
+
<div className="flex md:hidden items-center gap-1 overflow-x-auto px-3 pb-2">
|
|
931
|
+
{mobileViewOptions.map((opt) => (
|
|
932
|
+
<button key={opt.key} onClick={() => handleMobileViewChange(opt.key)}
|
|
933
|
+
className={`shrink-0 rounded-md px-3 py-1 text-sm transition-colors ${
|
|
934
|
+
mobileView === opt.key
|
|
935
|
+
? 'bg-primary/10 text-primary font-medium'
|
|
936
|
+
: 'text-muted-foreground hover:bg-muted/50'
|
|
937
|
+
}`}>
|
|
938
|
+
{t(opt.labelKey)}
|
|
939
|
+
</button>
|
|
940
|
+
))}
|
|
789
941
|
</div>
|
|
790
942
|
</div>
|
|
791
943
|
|
|
792
|
-
{/*
|
|
793
|
-
|
|
944
|
+
{/* Mobile list view → thumb-friendly agenda list; otherwise (desktop, or a
|
|
945
|
+
mobile grid view the user switched to) → FullCalendar grid. */}
|
|
946
|
+
{isMobile && !mobileGridActive ? (
|
|
947
|
+
<AgendaMobileList
|
|
948
|
+
bookings={bookings}
|
|
949
|
+
statusColors={statusColors}
|
|
950
|
+
currencyCode={config.currency.code}
|
|
951
|
+
onSelect={(b) => openModal('edit', { bookingId: b.id })}
|
|
952
|
+
/>
|
|
953
|
+
) : professionalsLoading && professionals.length === 0 ? (
|
|
794
954
|
<CalendarSkeleton profCount={2} />
|
|
795
955
|
) : (
|
|
796
956
|
<div ref={wrapperRef} className="flex-1 overflow-hidden agenda-calendar">
|
|
797
957
|
<FullCalendar
|
|
798
958
|
ref={calendarRef}
|
|
799
959
|
plugins={[dayGridPlugin, timeGridPlugin, resourceTimeGridPlugin, interactionPlugin, listPlugin]}
|
|
800
|
-
initialView={
|
|
960
|
+
initialView={mobileGridActive ? mobileView : effectiveInitialView}
|
|
801
961
|
schedulerLicenseKey="CC-Attribution-NonCommercial-NoDerivatives"
|
|
802
|
-
resources={resources}
|
|
962
|
+
resources={resourceCapable ? resources : undefined}
|
|
803
963
|
events={events}
|
|
804
964
|
headerToolbar={false}
|
|
805
965
|
locale={fcLocale}
|
|
@@ -878,8 +1038,14 @@ export function CalendarView() {
|
|
|
878
1038
|
bookingId={modalState.bookingId}
|
|
879
1039
|
prefill={modalState.prefill}
|
|
880
1040
|
initialTab={modalState.initialTab}
|
|
1041
|
+
externalSource={modalState.externalSource}
|
|
881
1042
|
onClose={closeModal}
|
|
882
|
-
onCreated={(newId) => {
|
|
1043
|
+
onCreated={(newId, source) => {
|
|
1044
|
+
if (config.onBookingCreated) {
|
|
1045
|
+
void Promise.resolve(config.onBookingCreated({ bookingId: newId, source })).catch((error: unknown) => {
|
|
1046
|
+
console.error('Failed to run agenda booking-created bridge', error)
|
|
1047
|
+
})
|
|
1048
|
+
}
|
|
883
1049
|
// After a short delay (let calendar re-render), find the event and show popover
|
|
884
1050
|
setTimeout(() => {
|
|
885
1051
|
const booking = bookings.find((b: CalendarBooking) => b.id === newId)
|