@open-mercato/core 0.6.6-develop.6253.1.a8c2b49d90 → 0.6.6-develop.6256.1.9fc16aedc4

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.
Files changed (29) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/customers/components/calendar/TimeGrid.js +3 -2
  3. package/dist/modules/customers/components/calendar/TimeGrid.js.map +2 -2
  4. package/dist/modules/customers/lib/calendar/grid.js +5 -2
  5. package/dist/modules/customers/lib/calendar/grid.js.map +2 -2
  6. package/dist/modules/perspectives/api/[tableId]/[perspectiveId]/route.js +20 -9
  7. package/dist/modules/perspectives/api/[tableId]/[perspectiveId]/route.js.map +2 -2
  8. package/dist/modules/perspectives/api/[tableId]/roles/[roleId]/route.js +43 -6
  9. package/dist/modules/perspectives/api/[tableId]/roles/[roleId]/route.js.map +2 -2
  10. package/dist/modules/perspectives/api/[tableId]/route.js +21 -4
  11. package/dist/modules/perspectives/api/[tableId]/route.js.map +2 -2
  12. package/dist/modules/perspectives/api/openapi.js +1 -0
  13. package/dist/modules/perspectives/api/openapi.js.map +2 -2
  14. package/dist/modules/perspectives/data/validators.js +2 -0
  15. package/dist/modules/perspectives/data/validators.js.map +2 -2
  16. package/dist/modules/perspectives/services/perspectiveService.js +135 -6
  17. package/dist/modules/perspectives/services/perspectiveService.js.map +2 -2
  18. package/dist/modules/query_index/lib/engine.js +5 -1
  19. package/dist/modules/query_index/lib/engine.js.map +2 -2
  20. package/package.json +7 -7
  21. package/src/modules/customers/components/calendar/TimeGrid.tsx +3 -2
  22. package/src/modules/customers/lib/calendar/grid.ts +10 -2
  23. package/src/modules/perspectives/api/[tableId]/[perspectiveId]/route.ts +20 -9
  24. package/src/modules/perspectives/api/[tableId]/roles/[roleId]/route.ts +45 -6
  25. package/src/modules/perspectives/api/[tableId]/route.ts +23 -1
  26. package/src/modules/perspectives/api/openapi.ts +1 -1
  27. package/src/modules/perspectives/data/validators.ts +2 -0
  28. package/src/modules/perspectives/services/perspectiveService.ts +171 -7
  29. package/src/modules/query_index/lib/engine.ts +5 -1
@@ -1,4 +1,4 @@
1
- [build:core] found 3409 entry points
1
+ [build:core] found 3410 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 185 entry points
4
4
  [build:core:generated] built successfully
@@ -124,14 +124,15 @@ function TimeGrid({
124
124
  const scrollRef = React.useRef(null);
125
125
  const nowMs = Date.now();
126
126
  const today = startOfDay(/* @__PURE__ */ new Date());
127
+ const todayMs = today.getTime();
127
128
  const anchorMs = anchor.getTime();
128
129
  const [selectedId, setSelectedId] = React.useState(null);
129
130
  const [drag, setDrag] = React.useState(null);
130
131
  const dayStarts = React.useMemo(() => {
131
132
  const rangeStart = getVisibleRange(days === 7 ? "week" : "day", new Date(anchorMs), 0).from;
132
133
  const all = Array.from({ length: days }, (_, index) => addDays(rangeStart, index));
133
- return days === 7 ? applyWeekendVisibility(all, showWeekends) : all;
134
- }, [days, anchorMs, showWeekends]);
134
+ return days === 7 ? applyWeekendVisibility(all, showWeekends, new Date(todayMs)) : all;
135
+ }, [days, anchorMs, showWeekends, todayMs]);
135
136
  const dayColumns = React.useMemo(
136
137
  () => dayStarts.map((dayStart) => buildDayColumn(dayStart, items)),
137
138
  [dayStarts, items]
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/customers/components/calendar/TimeGrid.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { CalendarDays, ChevronLeft, ChevronRight } from 'lucide-react'\nimport { addDays } from 'date-fns/addDays'\nimport { isSameDay } from 'date-fns/isSameDay'\nimport { startOfDay } from 'date-fns/startOfDay'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { EmptyState } from '@open-mercato/ui/primitives/empty-state'\nimport { packOverlaps } from '../../lib/calendar/layout'\nimport { getVisibleRange } from '../../lib/calendar/range'\nimport {\n applyWeekendVisibility,\n buildDragRange,\n DRAG_SNAP_MINUTES,\n isWeekendDay,\n offsetYToMinutes,\n} from '../../lib/calendar/grid'\nimport { EventBlock, formatTimeRange, resolveEventTone } from './EventBlock'\nimport { EventPeekPopover } from './EventPeekPopover'\nimport type { CalendarItem, TimeGridProps } from './types'\n\nconst HOUR_HEIGHT_PX = 120\nconst HOURS_PER_DAY = 24\nconst GRID_BODY_MAX_HEIGHT_PX = 654\nconst INITIAL_SCROLL_HOUR = 8\nconst BLOCK_VERTICAL_GAP_PX = 1\nconst MIN_BLOCK_HEIGHT_PX = 32\nconst PACKED_COLUMN_GAP_PX = 2\n\nconst NON_WORKING_HATCH_BACKGROUND =\n 'repeating-linear-gradient(45deg, transparent 0px, transparent 8px, var(--border) 8px, var(--border) 9px)'\n\ntype PositionedBlock = {\n item: CalendarItem\n top: number\n height: number\n insetInlineStart: string\n width: string\n}\n\ntype DayColumnData = {\n dayStart: Date\n allDayItems: CalendarItem[]\n blocks: PositionedBlock[]\n}\n\ntype DragState = { dayMs: number; startMin: number; endMin: number; moved: boolean }\n\nfunction buildDayColumn(dayStart: Date, items: CalendarItem[]): DayColumnData {\n const dayEnd = addDays(dayStart, 1)\n const dayStartMs = dayStart.getTime()\n const dayEndMs = dayEnd.getTime()\n const allDayItems: CalendarItem[] = []\n const segments: Array<{ original: CalendarItem; start: Date; end: Date }> = []\n\n for (const item of items) {\n const startMs = item.start.getTime()\n const endMs = item.end.getTime()\n if (startMs >= dayEndMs || endMs <= dayStartMs) continue\n if (item.allDay) {\n allDayItems.push(item)\n continue\n }\n segments.push({\n original: item,\n start: startMs < dayStartMs ? dayStart : item.start,\n end: endMs > dayEndMs ? dayEnd : item.end,\n })\n }\n\n allDayItems.sort((first, second) => first.start.getTime() - second.start.getTime())\n\n const clones = segments.map((segment) => ({ ...segment.original, start: segment.start, end: segment.end }))\n const originalByClone = new Map<CalendarItem, CalendarItem>()\n clones.forEach((clone, index) => originalByClone.set(clone, segments[index].original))\n\n const blocks: PositionedBlock[] = packOverlaps(clones).map(({ item: clone, column, columns }) => {\n const startMinutes = (clone.start.getTime() - dayStartMs) / 60000\n const durationMinutes = (clone.end.getTime() - clone.start.getTime()) / 60000\n const rawTop = (startMinutes / 60) * HOUR_HEIGHT_PX\n const rawHeight = (durationMinutes / 60) * HOUR_HEIGHT_PX\n const widthPct = 100 / columns\n return {\n item: originalByClone.get(clone) ?? clone,\n top: rawTop + BLOCK_VERTICAL_GAP_PX,\n height: Math.max(MIN_BLOCK_HEIGHT_PX, rawHeight - BLOCK_VERTICAL_GAP_PX * 2),\n insetInlineStart: `calc(${column * widthPct}% + ${column * PACKED_COLUMN_GAP_PX}px)`,\n width: `calc(${widthPct}% - ${((columns - 1) * PACKED_COLUMN_GAP_PX) / columns}px)`,\n }\n })\n\n return { dayStart, allDayItems, blocks }\n}\n\ntype ConflictBadgeProps = { count: number }\n\nfunction ConflictBadge({ count }: ConflictBadgeProps) {\n const t = useT()\n const label =\n count === 1\n ? t('customers.calendar.grid.conflictCount', '1 conflict')\n : t('customers.calendar.grid.conflictsCount', '{count} conflicts', { count })\n return (\n <span className=\"pointer-events-none absolute left-2 top-1 z-40 inline-flex items-center gap-1 rounded-full bg-status-error-bg px-2 py-0.5 text-overline font-medium uppercase tracking-wide text-status-error-text\">\n <span aria-hidden className=\"size-1.5 rounded-full bg-status-error-icon\" />\n {label}\n </span>\n )\n}\n\ntype AllDayChipProps = {\n item: CalendarItem\n conflicted: boolean\n highlighted: boolean\n selected: boolean\n nowMs: number\n} & Omit<React.ComponentProps<typeof Button>, 'style' | 'children'>\n\nconst AllDayChip = React.forwardRef<HTMLButtonElement, AllDayChipProps>(function AllDayChip(\n { item, conflicted, highlighted, selected, nowMs, className, ...buttonProps },\n ref,\n) {\n const t = useT()\n const tone = resolveEventTone(item, nowMs)\n const title = item.title || t('customers.calendar.grid.untitled', 'Untitled')\n return (\n <Button\n ref={ref}\n type=\"button\"\n variant=\"ghost\"\n aria-label={`${title}, ${t('customers.calendar.grid.allDay', 'All day')}`}\n className={cn(\n 'h-auto w-full min-w-0 justify-start rounded-sm px-2 py-0.5 text-start hover:bg-muted/70',\n tone.surfaceClassName,\n conflicted && 'ring-1 ring-status-warning-icon',\n selected && 'shadow-md ring-2 ring-foreground',\n highlighted && 'motion-safe:animate-pulse',\n 'focus-visible:ring-2 focus-visible:ring-ring',\n className,\n )}\n style={tone.style}\n {...buttonProps}\n >\n <span className={cn('truncate text-xs font-medium', tone.titleClassName)}>{title}</span>\n </Button>\n )\n})\n\nAllDayChip.displayName = 'AllDayChip'\n\nexport function TimeGrid({\n days,\n anchor,\n items,\n conflictIds,\n showWeekends,\n showConflicts,\n aiSummaries,\n highlightItemId,\n onItemClick,\n onJoin,\n onNavigate,\n onCreate,\n onCreateRange,\n}: TimeGridProps) {\n const t = useT()\n const locale = useLocale()\n const scrollRef = React.useRef<HTMLDivElement | null>(null)\n const nowMs = Date.now()\n const today = startOfDay(new Date())\n const anchorMs = anchor.getTime()\n const [selectedId, setSelectedId] = React.useState<string | null>(null)\n const [drag, setDrag] = React.useState<DragState | null>(null)\n\n const dayStarts = React.useMemo(() => {\n const rangeStart = getVisibleRange(days === 7 ? 'week' : 'day', new Date(anchorMs), 0).from\n const all = Array.from({ length: days }, (_, index) => addDays(rangeStart, index))\n return days === 7 ? applyWeekendVisibility(all, showWeekends) : all\n }, [days, anchorMs, showWeekends])\n\n const dayColumns = React.useMemo(\n () => dayStarts.map((dayStart) => buildDayColumn(dayStart, items)),\n [dayStarts, items],\n )\n\n const resolveJoinUrl = React.useCallback((location: string | null): string | null => {\n const trimmed = location?.trim() ?? ''\n if (!trimmed) return null\n if (/^https?:\\/\\//i.test(trimmed)) return trimmed\n if (/^www\\./i.test(trimmed)) return `https://${trimmed}`\n return null\n }, [])\n\n const hasAllDayLane = dayColumns.some((column) => column.allDayItems.length > 0)\n\n const formatters = React.useMemo(\n () => ({\n dayNumber: new Intl.DateTimeFormat(locale, { day: '2-digit' }),\n weekdayShort: new Intl.DateTimeFormat(locale, { weekday: 'short' }),\n weekdayLong: new Intl.DateTimeFormat(locale, { weekday: 'long' }),\n }),\n [locale],\n )\n\n const hourLabels = React.useMemo(() => {\n const formatter = new Intl.DateTimeFormat(locale, { hour: 'numeric' })\n return Array.from({ length: HOURS_PER_DAY }, (_, hour) => formatter.format(new Date(2024, 0, 1, hour)))\n }, [locale])\n\n React.useEffect(() => {\n const node = scrollRef.current\n if (!node) return\n node.scrollTop = INITIAL_SCROLL_HOUR * HOUR_HEIGHT_PX\n }, [])\n\n React.useEffect(() => {\n if (!highlightItemId) return\n const node = scrollRef.current\n if (!node) return\n const target = items.find((item) => item.id === highlightItemId && !item.allDay)\n if (!target) return\n const minutes = (target.start.getTime() - startOfDay(target.start).getTime()) / 60000\n const top = (minutes / 60) * HOUR_HEIGHT_PX\n const reduceMotion =\n typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches\n node.scrollTo({ top: Math.max(0, top - HOUR_HEIGHT_PX), behavior: reduceMotion ? 'auto' : 'smooth' })\n }, [highlightItemId, items])\n\n const nonWorkingLabel = t('customers.calendar.grid.nonWorking', 'Non-working day')\n const previousLabel =\n days === 7\n ? t('customers.calendar.previousWeek', 'Previous week')\n : t('customers.calendar.grid.previousDay', 'Previous day')\n const nextLabel =\n days === 7 ? t('customers.calendar.nextWeek', 'Next week') : t('customers.calendar.grid.nextDay', 'Next day')\n\n const headerLabelFor = (dayStart: Date): string => {\n const dayNumber = formatters.dayNumber.format(dayStart)\n if (days === 1) return `${formatters.weekdayLong.format(dayStart).toUpperCase()} \u00B7 ${dayNumber}`\n return `${dayNumber} ${formatters.weekdayShort.format(dayStart).toUpperCase()}`\n }\n\n const canCreateRange = Boolean(onCreateRange)\n\n const beginDrag = (event: React.PointerEvent<HTMLDivElement>, dayStart: Date) => {\n if (!canCreateRange || event.button !== 0) return\n const layer = event.currentTarget\n const rect = layer.getBoundingClientRect()\n const minute = offsetYToMinutes(event.clientY - rect.top, HOUR_HEIGHT_PX)\n try {\n layer.setPointerCapture(event.pointerId)\n } catch {\n // Pointer capture is best-effort; ignore environments that reject it.\n }\n setSelectedId(null)\n setDrag({ dayMs: dayStart.getTime(), startMin: minute, endMin: minute, moved: false })\n }\n\n const moveDrag = (event: React.PointerEvent<HTMLDivElement>) => {\n // Read the layer rect synchronously: `event.currentTarget` is only valid during\n // event dispatch, but the setDrag updater below runs later during re-render.\n const rect = event.currentTarget.getBoundingClientRect()\n const minute = offsetYToMinutes(event.clientY - rect.top, HOUR_HEIGHT_PX)\n setDrag((previous) => {\n if (!previous) return previous\n return {\n ...previous,\n endMin: minute,\n moved: previous.moved || Math.abs(minute - previous.startMin) >= DRAG_SNAP_MINUTES,\n }\n })\n }\n\n const endDrag = (dayStart: Date) => {\n setDrag((previous) => {\n if (previous && previous.moved && onCreateRange) {\n const range = buildDragRange(dayStart, previous.startMin, previous.endMin)\n onCreateRange(range.start, range.end)\n }\n return null\n })\n }\n\n return (\n <div className=\"relative flex flex-col overflow-hidden rounded-lg border border-border bg-card\">\n <div\n ref={scrollRef}\n className=\"overflow-auto overscroll-contain\"\n style={{ maxHeight: GRID_BODY_MAX_HEIGHT_PX }}\n >\n <div className=\"sticky top-0 z-30 min-w-full bg-card max-md:w-max\">\n <div className=\"flex border-b border-border\">\n <div className=\"sticky start-0 z-10 flex w-14 shrink-0 bg-card md:w-26\">\n <span className=\"flex flex-1 items-center justify-center border-e border-border\">\n <IconButton type=\"button\" variant=\"ghost\" size=\"xs\" aria-label={previousLabel} onClick={() => onNavigate(-days)}>\n <ChevronLeft aria-hidden />\n </IconButton>\n </span>\n <span className=\"flex flex-1 items-center justify-center border-e border-border\">\n <IconButton type=\"button\" variant=\"ghost\" size=\"xs\" aria-label={nextLabel} onClick={() => onNavigate(days)}>\n <ChevronRight aria-hidden />\n </IconButton>\n </span>\n </div>\n {dayColumns.map(({ dayStart }) => {\n const isToday = isSameDay(dayStart, today)\n return (\n <div\n key={dayStart.getTime()}\n className={cn(\n 'flex min-w-0 flex-1 items-center justify-center border-e border-border bg-muted px-1 py-2 last:border-e-0',\n days === 7 && 'min-w-[120px] max-md:max-w-[120px] md:min-w-0',\n )}\n >\n <span\n className={cn(\n 'truncate text-xs uppercase tracking-wide',\n isToday ? 'font-semibold text-foreground' : 'font-medium text-muted-foreground',\n )}\n >\n {headerLabelFor(dayStart)}\n </span>\n </div>\n )\n })}\n </div>\n {hasAllDayLane ? (\n <div className=\"flex border-b border-border\">\n <div className=\"sticky start-0 z-10 flex w-14 shrink-0 items-center justify-center border-e border-border bg-card py-1 md:w-26\">\n <span className=\"truncate text-overline uppercase tracking-wide text-muted-foreground\">\n {t('customers.calendar.grid.allDay', 'All day')}\n </span>\n </div>\n {dayColumns.map(({ dayStart, allDayItems }) => (\n <div\n key={dayStart.getTime()}\n className={cn(\n 'min-w-0 flex-1 space-y-1 border-e border-border p-1 last:border-e-0',\n days === 7 && 'min-w-[120px] max-md:max-w-[120px] md:min-w-0',\n )}\n >\n {allDayItems.map((item) => (\n <EventPeekPopover\n key={item.id}\n item={item}\n open={selectedId === item.id}\n joinUrl={resolveJoinUrl(item.location)}\n aiSummaries={aiSummaries}\n onOpenChange={(open) => setSelectedId(open ? item.id : null)}\n onJoin={onJoin}\n onEdit={onItemClick}\n >\n <AllDayChip\n item={item}\n conflicted={showConflicts && conflictIds.has(item.id)}\n highlighted={highlightItemId === item.id}\n selected={selectedId === item.id}\n nowMs={nowMs}\n />\n </EventPeekPopover>\n ))}\n </div>\n ))}\n </div>\n ) : null}\n </div>\n <div className=\"flex min-w-full max-md:w-max\" style={{ height: HOURS_PER_DAY * HOUR_HEIGHT_PX }}>\n <div className=\"sticky start-0 z-10 w-14 shrink-0 border-e border-border bg-card md:w-26\">\n <div className=\"relative h-full\">\n {hourLabels.map((label, hour) =>\n hour === 0 ? null : (\n <span\n key={hour}\n className=\"absolute w-full -translate-y-1/2 px-1 text-center text-xs font-medium text-muted-foreground md:px-3 md:text-sm\"\n style={{ top: hour * HOUR_HEIGHT_PX }}\n >\n {label}\n </span>\n ),\n )}\n </div>\n </div>\n {dayColumns.map(({ dayStart, blocks }) => {\n const nonWorking = isWeekendDay(dayStart)\n const conflictCount = showConflicts\n ? blocks.filter((block) => conflictIds.has(block.item.id)).length\n : 0\n const dragActive = drag && drag.moved && drag.dayMs === dayStart.getTime()\n const dragRange = dragActive ? buildDragRange(dayStart, drag.startMin, drag.endMin) : null\n const dragStartMinutes = dragRange\n ? (dragRange.start.getTime() - startOfDay(dragRange.start).getTime()) / 60000\n : 0\n const dragDurationMinutes = dragRange\n ? (dragRange.end.getTime() - dragRange.start.getTime()) / 60000\n : 0\n return (\n <div\n key={dayStart.getTime()}\n className={cn(\n 'relative min-w-0 flex-1 border-e border-border last:border-e-0',\n days === 7 && 'min-w-[120px] max-md:max-w-[120px] md:min-w-0',\n )}\n title={nonWorking ? nonWorkingLabel : undefined}\n >\n {nonWorking ? <span className=\"sr-only\">{nonWorkingLabel}</span> : null}\n <div aria-hidden className=\"absolute inset-0\">\n {Array.from({ length: HOURS_PER_DAY }, (_, hour) => (\n <div key={hour} className=\"relative h-30 border-b border-border\">\n <span className=\"absolute inset-x-0 top-1/2 h-px bg-border/50\" />\n </div>\n ))}\n </div>\n {nonWorking ? (\n <div\n aria-hidden\n className=\"pointer-events-none absolute inset-0\"\n style={{ backgroundImage: NON_WORKING_HATCH_BACKGROUND }}\n />\n ) : null}\n {canCreateRange ? (\n <div\n className=\"absolute inset-0 cursor-cell\"\n onPointerDown={(event) => beginDrag(event, dayStart)}\n onPointerMove={moveDrag}\n onPointerUp={() => endDrag(dayStart)}\n onPointerCancel={() => setDrag(null)}\n aria-hidden\n />\n ) : null}\n {dragRange ? (\n <div\n aria-hidden\n className=\"pointer-events-none absolute z-30 flex flex-col gap-0.5 overflow-hidden rounded-md border-2 border-dashed border-foreground bg-accent-indigo/10 px-2 pt-1.5\"\n style={{\n top: (dragStartMinutes / 60) * HOUR_HEIGHT_PX,\n height: Math.max(MIN_BLOCK_HEIGHT_PX, (dragDurationMinutes / 60) * HOUR_HEIGHT_PX),\n insetInlineStart: 8,\n insetInlineEnd: 8,\n }}\n >\n <span className=\"text-overline font-semibold text-foreground\">\n {t('customers.calendar.actions.newEvent', 'New event')}\n </span>\n <span className=\"text-overline text-muted-foreground\">\n {formatTimeRange(locale, dragRange.start, dragRange.end)}\n </span>\n </div>\n ) : null}\n {conflictCount > 0 ? <ConflictBadge count={conflictCount} /> : null}\n {/* Click-through container so empty space reaches the drag-to-create layer\n below; each block re-enables pointer events for its own click/peek. */}\n <div className=\"pointer-events-none absolute inset-y-0\" style={{ insetInlineStart: 8, insetInlineEnd: 8 }}>\n {blocks.map((block) => (\n <EventPeekPopover\n key={`${block.item.id}-${block.top}`}\n item={block.item}\n open={selectedId === block.item.id}\n joinUrl={resolveJoinUrl(block.item.location)}\n aiSummaries={aiSummaries}\n onOpenChange={(open) => setSelectedId(open ? block.item.id : null)}\n onJoin={onJoin}\n onEdit={onItemClick}\n >\n <EventBlock\n item={block.item}\n top={block.top}\n height={block.height}\n insetInlineStart={block.insetInlineStart}\n width={block.width}\n conflicted={showConflicts && conflictIds.has(block.item.id)}\n highlighted={highlightItemId === block.item.id}\n selected={selectedId === block.item.id}\n nowMs={nowMs}\n />\n </EventPeekPopover>\n ))}\n </div>\n </div>\n )\n })}\n </div>\n </div>\n {items.length === 0 ? (\n <div className=\"pointer-events-none absolute inset-0 z-40 flex items-center justify-center p-6\">\n <EmptyState\n className=\"pointer-events-auto bg-card/95 shadow-sm\"\n icon={<CalendarDays className=\"h-6 w-6\" aria-hidden />}\n title={\n days === 7\n ? t('customers.calendar.empty.week', 'Nothing scheduled this week')\n : t('customers.calendar.empty.day', 'Nothing scheduled this day')\n }\n description={t('customers.calendar.empty.description', 'Plan a meeting, event or task to fill your week.')}\n actions={\n onCreate ? (\n <Button type=\"button\" size=\"sm\" onClick={onCreate}>\n {t('customers.calendar.actions.newEvent', 'New event')}\n </Button>\n ) : undefined\n }\n />\n </div>\n ) : null}\n </div>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { CalendarDays, ChevronLeft, ChevronRight } from 'lucide-react'\nimport { addDays } from 'date-fns/addDays'\nimport { isSameDay } from 'date-fns/isSameDay'\nimport { startOfDay } from 'date-fns/startOfDay'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { EmptyState } from '@open-mercato/ui/primitives/empty-state'\nimport { packOverlaps } from '../../lib/calendar/layout'\nimport { getVisibleRange } from '../../lib/calendar/range'\nimport {\n applyWeekendVisibility,\n buildDragRange,\n DRAG_SNAP_MINUTES,\n isWeekendDay,\n offsetYToMinutes,\n} from '../../lib/calendar/grid'\nimport { EventBlock, formatTimeRange, resolveEventTone } from './EventBlock'\nimport { EventPeekPopover } from './EventPeekPopover'\nimport type { CalendarItem, TimeGridProps } from './types'\n\nconst HOUR_HEIGHT_PX = 120\nconst HOURS_PER_DAY = 24\nconst GRID_BODY_MAX_HEIGHT_PX = 654\nconst INITIAL_SCROLL_HOUR = 8\nconst BLOCK_VERTICAL_GAP_PX = 1\nconst MIN_BLOCK_HEIGHT_PX = 32\nconst PACKED_COLUMN_GAP_PX = 2\n\nconst NON_WORKING_HATCH_BACKGROUND =\n 'repeating-linear-gradient(45deg, transparent 0px, transparent 8px, var(--border) 8px, var(--border) 9px)'\n\ntype PositionedBlock = {\n item: CalendarItem\n top: number\n height: number\n insetInlineStart: string\n width: string\n}\n\ntype DayColumnData = {\n dayStart: Date\n allDayItems: CalendarItem[]\n blocks: PositionedBlock[]\n}\n\ntype DragState = { dayMs: number; startMin: number; endMin: number; moved: boolean }\n\nfunction buildDayColumn(dayStart: Date, items: CalendarItem[]): DayColumnData {\n const dayEnd = addDays(dayStart, 1)\n const dayStartMs = dayStart.getTime()\n const dayEndMs = dayEnd.getTime()\n const allDayItems: CalendarItem[] = []\n const segments: Array<{ original: CalendarItem; start: Date; end: Date }> = []\n\n for (const item of items) {\n const startMs = item.start.getTime()\n const endMs = item.end.getTime()\n if (startMs >= dayEndMs || endMs <= dayStartMs) continue\n if (item.allDay) {\n allDayItems.push(item)\n continue\n }\n segments.push({\n original: item,\n start: startMs < dayStartMs ? dayStart : item.start,\n end: endMs > dayEndMs ? dayEnd : item.end,\n })\n }\n\n allDayItems.sort((first, second) => first.start.getTime() - second.start.getTime())\n\n const clones = segments.map((segment) => ({ ...segment.original, start: segment.start, end: segment.end }))\n const originalByClone = new Map<CalendarItem, CalendarItem>()\n clones.forEach((clone, index) => originalByClone.set(clone, segments[index].original))\n\n const blocks: PositionedBlock[] = packOverlaps(clones).map(({ item: clone, column, columns }) => {\n const startMinutes = (clone.start.getTime() - dayStartMs) / 60000\n const durationMinutes = (clone.end.getTime() - clone.start.getTime()) / 60000\n const rawTop = (startMinutes / 60) * HOUR_HEIGHT_PX\n const rawHeight = (durationMinutes / 60) * HOUR_HEIGHT_PX\n const widthPct = 100 / columns\n return {\n item: originalByClone.get(clone) ?? clone,\n top: rawTop + BLOCK_VERTICAL_GAP_PX,\n height: Math.max(MIN_BLOCK_HEIGHT_PX, rawHeight - BLOCK_VERTICAL_GAP_PX * 2),\n insetInlineStart: `calc(${column * widthPct}% + ${column * PACKED_COLUMN_GAP_PX}px)`,\n width: `calc(${widthPct}% - ${((columns - 1) * PACKED_COLUMN_GAP_PX) / columns}px)`,\n }\n })\n\n return { dayStart, allDayItems, blocks }\n}\n\ntype ConflictBadgeProps = { count: number }\n\nfunction ConflictBadge({ count }: ConflictBadgeProps) {\n const t = useT()\n const label =\n count === 1\n ? t('customers.calendar.grid.conflictCount', '1 conflict')\n : t('customers.calendar.grid.conflictsCount', '{count} conflicts', { count })\n return (\n <span className=\"pointer-events-none absolute left-2 top-1 z-40 inline-flex items-center gap-1 rounded-full bg-status-error-bg px-2 py-0.5 text-overline font-medium uppercase tracking-wide text-status-error-text\">\n <span aria-hidden className=\"size-1.5 rounded-full bg-status-error-icon\" />\n {label}\n </span>\n )\n}\n\ntype AllDayChipProps = {\n item: CalendarItem\n conflicted: boolean\n highlighted: boolean\n selected: boolean\n nowMs: number\n} & Omit<React.ComponentProps<typeof Button>, 'style' | 'children'>\n\nconst AllDayChip = React.forwardRef<HTMLButtonElement, AllDayChipProps>(function AllDayChip(\n { item, conflicted, highlighted, selected, nowMs, className, ...buttonProps },\n ref,\n) {\n const t = useT()\n const tone = resolveEventTone(item, nowMs)\n const title = item.title || t('customers.calendar.grid.untitled', 'Untitled')\n return (\n <Button\n ref={ref}\n type=\"button\"\n variant=\"ghost\"\n aria-label={`${title}, ${t('customers.calendar.grid.allDay', 'All day')}`}\n className={cn(\n 'h-auto w-full min-w-0 justify-start rounded-sm px-2 py-0.5 text-start hover:bg-muted/70',\n tone.surfaceClassName,\n conflicted && 'ring-1 ring-status-warning-icon',\n selected && 'shadow-md ring-2 ring-foreground',\n highlighted && 'motion-safe:animate-pulse',\n 'focus-visible:ring-2 focus-visible:ring-ring',\n className,\n )}\n style={tone.style}\n {...buttonProps}\n >\n <span className={cn('truncate text-xs font-medium', tone.titleClassName)}>{title}</span>\n </Button>\n )\n})\n\nAllDayChip.displayName = 'AllDayChip'\n\nexport function TimeGrid({\n days,\n anchor,\n items,\n conflictIds,\n showWeekends,\n showConflicts,\n aiSummaries,\n highlightItemId,\n onItemClick,\n onJoin,\n onNavigate,\n onCreate,\n onCreateRange,\n}: TimeGridProps) {\n const t = useT()\n const locale = useLocale()\n const scrollRef = React.useRef<HTMLDivElement | null>(null)\n const nowMs = Date.now()\n const today = startOfDay(new Date())\n const todayMs = today.getTime()\n const anchorMs = anchor.getTime()\n const [selectedId, setSelectedId] = React.useState<string | null>(null)\n const [drag, setDrag] = React.useState<DragState | null>(null)\n\n const dayStarts = React.useMemo(() => {\n const rangeStart = getVisibleRange(days === 7 ? 'week' : 'day', new Date(anchorMs), 0).from\n const all = Array.from({ length: days }, (_, index) => addDays(rangeStart, index))\n return days === 7 ? applyWeekendVisibility(all, showWeekends, new Date(todayMs)) : all\n }, [days, anchorMs, showWeekends, todayMs])\n\n const dayColumns = React.useMemo(\n () => dayStarts.map((dayStart) => buildDayColumn(dayStart, items)),\n [dayStarts, items],\n )\n\n const resolveJoinUrl = React.useCallback((location: string | null): string | null => {\n const trimmed = location?.trim() ?? ''\n if (!trimmed) return null\n if (/^https?:\\/\\//i.test(trimmed)) return trimmed\n if (/^www\\./i.test(trimmed)) return `https://${trimmed}`\n return null\n }, [])\n\n const hasAllDayLane = dayColumns.some((column) => column.allDayItems.length > 0)\n\n const formatters = React.useMemo(\n () => ({\n dayNumber: new Intl.DateTimeFormat(locale, { day: '2-digit' }),\n weekdayShort: new Intl.DateTimeFormat(locale, { weekday: 'short' }),\n weekdayLong: new Intl.DateTimeFormat(locale, { weekday: 'long' }),\n }),\n [locale],\n )\n\n const hourLabels = React.useMemo(() => {\n const formatter = new Intl.DateTimeFormat(locale, { hour: 'numeric' })\n return Array.from({ length: HOURS_PER_DAY }, (_, hour) => formatter.format(new Date(2024, 0, 1, hour)))\n }, [locale])\n\n React.useEffect(() => {\n const node = scrollRef.current\n if (!node) return\n node.scrollTop = INITIAL_SCROLL_HOUR * HOUR_HEIGHT_PX\n }, [])\n\n React.useEffect(() => {\n if (!highlightItemId) return\n const node = scrollRef.current\n if (!node) return\n const target = items.find((item) => item.id === highlightItemId && !item.allDay)\n if (!target) return\n const minutes = (target.start.getTime() - startOfDay(target.start).getTime()) / 60000\n const top = (minutes / 60) * HOUR_HEIGHT_PX\n const reduceMotion =\n typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches\n node.scrollTo({ top: Math.max(0, top - HOUR_HEIGHT_PX), behavior: reduceMotion ? 'auto' : 'smooth' })\n }, [highlightItemId, items])\n\n const nonWorkingLabel = t('customers.calendar.grid.nonWorking', 'Non-working day')\n const previousLabel =\n days === 7\n ? t('customers.calendar.previousWeek', 'Previous week')\n : t('customers.calendar.grid.previousDay', 'Previous day')\n const nextLabel =\n days === 7 ? t('customers.calendar.nextWeek', 'Next week') : t('customers.calendar.grid.nextDay', 'Next day')\n\n const headerLabelFor = (dayStart: Date): string => {\n const dayNumber = formatters.dayNumber.format(dayStart)\n if (days === 1) return `${formatters.weekdayLong.format(dayStart).toUpperCase()} \u00B7 ${dayNumber}`\n return `${dayNumber} ${formatters.weekdayShort.format(dayStart).toUpperCase()}`\n }\n\n const canCreateRange = Boolean(onCreateRange)\n\n const beginDrag = (event: React.PointerEvent<HTMLDivElement>, dayStart: Date) => {\n if (!canCreateRange || event.button !== 0) return\n const layer = event.currentTarget\n const rect = layer.getBoundingClientRect()\n const minute = offsetYToMinutes(event.clientY - rect.top, HOUR_HEIGHT_PX)\n try {\n layer.setPointerCapture(event.pointerId)\n } catch {\n // Pointer capture is best-effort; ignore environments that reject it.\n }\n setSelectedId(null)\n setDrag({ dayMs: dayStart.getTime(), startMin: minute, endMin: minute, moved: false })\n }\n\n const moveDrag = (event: React.PointerEvent<HTMLDivElement>) => {\n // Read the layer rect synchronously: `event.currentTarget` is only valid during\n // event dispatch, but the setDrag updater below runs later during re-render.\n const rect = event.currentTarget.getBoundingClientRect()\n const minute = offsetYToMinutes(event.clientY - rect.top, HOUR_HEIGHT_PX)\n setDrag((previous) => {\n if (!previous) return previous\n return {\n ...previous,\n endMin: minute,\n moved: previous.moved || Math.abs(minute - previous.startMin) >= DRAG_SNAP_MINUTES,\n }\n })\n }\n\n const endDrag = (dayStart: Date) => {\n setDrag((previous) => {\n if (previous && previous.moved && onCreateRange) {\n const range = buildDragRange(dayStart, previous.startMin, previous.endMin)\n onCreateRange(range.start, range.end)\n }\n return null\n })\n }\n\n return (\n <div className=\"relative flex flex-col overflow-hidden rounded-lg border border-border bg-card\">\n <div\n ref={scrollRef}\n className=\"overflow-auto overscroll-contain\"\n style={{ maxHeight: GRID_BODY_MAX_HEIGHT_PX }}\n >\n <div className=\"sticky top-0 z-30 min-w-full bg-card max-md:w-max\">\n <div className=\"flex border-b border-border\">\n <div className=\"sticky start-0 z-10 flex w-14 shrink-0 bg-card md:w-26\">\n <span className=\"flex flex-1 items-center justify-center border-e border-border\">\n <IconButton type=\"button\" variant=\"ghost\" size=\"xs\" aria-label={previousLabel} onClick={() => onNavigate(-days)}>\n <ChevronLeft aria-hidden />\n </IconButton>\n </span>\n <span className=\"flex flex-1 items-center justify-center border-e border-border\">\n <IconButton type=\"button\" variant=\"ghost\" size=\"xs\" aria-label={nextLabel} onClick={() => onNavigate(days)}>\n <ChevronRight aria-hidden />\n </IconButton>\n </span>\n </div>\n {dayColumns.map(({ dayStart }) => {\n const isToday = isSameDay(dayStart, today)\n return (\n <div\n key={dayStart.getTime()}\n className={cn(\n 'flex min-w-0 flex-1 items-center justify-center border-e border-border bg-muted px-1 py-2 last:border-e-0',\n days === 7 && 'min-w-[120px] max-md:max-w-[120px] md:min-w-0',\n )}\n >\n <span\n className={cn(\n 'truncate text-xs uppercase tracking-wide',\n isToday ? 'font-semibold text-foreground' : 'font-medium text-muted-foreground',\n )}\n >\n {headerLabelFor(dayStart)}\n </span>\n </div>\n )\n })}\n </div>\n {hasAllDayLane ? (\n <div className=\"flex border-b border-border\">\n <div className=\"sticky start-0 z-10 flex w-14 shrink-0 items-center justify-center border-e border-border bg-card py-1 md:w-26\">\n <span className=\"truncate text-overline uppercase tracking-wide text-muted-foreground\">\n {t('customers.calendar.grid.allDay', 'All day')}\n </span>\n </div>\n {dayColumns.map(({ dayStart, allDayItems }) => (\n <div\n key={dayStart.getTime()}\n className={cn(\n 'min-w-0 flex-1 space-y-1 border-e border-border p-1 last:border-e-0',\n days === 7 && 'min-w-[120px] max-md:max-w-[120px] md:min-w-0',\n )}\n >\n {allDayItems.map((item) => (\n <EventPeekPopover\n key={item.id}\n item={item}\n open={selectedId === item.id}\n joinUrl={resolveJoinUrl(item.location)}\n aiSummaries={aiSummaries}\n onOpenChange={(open) => setSelectedId(open ? item.id : null)}\n onJoin={onJoin}\n onEdit={onItemClick}\n >\n <AllDayChip\n item={item}\n conflicted={showConflicts && conflictIds.has(item.id)}\n highlighted={highlightItemId === item.id}\n selected={selectedId === item.id}\n nowMs={nowMs}\n />\n </EventPeekPopover>\n ))}\n </div>\n ))}\n </div>\n ) : null}\n </div>\n <div className=\"flex min-w-full max-md:w-max\" style={{ height: HOURS_PER_DAY * HOUR_HEIGHT_PX }}>\n <div className=\"sticky start-0 z-10 w-14 shrink-0 border-e border-border bg-card md:w-26\">\n <div className=\"relative h-full\">\n {hourLabels.map((label, hour) =>\n hour === 0 ? null : (\n <span\n key={hour}\n className=\"absolute w-full -translate-y-1/2 px-1 text-center text-xs font-medium text-muted-foreground md:px-3 md:text-sm\"\n style={{ top: hour * HOUR_HEIGHT_PX }}\n >\n {label}\n </span>\n ),\n )}\n </div>\n </div>\n {dayColumns.map(({ dayStart, blocks }) => {\n const nonWorking = isWeekendDay(dayStart)\n const conflictCount = showConflicts\n ? blocks.filter((block) => conflictIds.has(block.item.id)).length\n : 0\n const dragActive = drag && drag.moved && drag.dayMs === dayStart.getTime()\n const dragRange = dragActive ? buildDragRange(dayStart, drag.startMin, drag.endMin) : null\n const dragStartMinutes = dragRange\n ? (dragRange.start.getTime() - startOfDay(dragRange.start).getTime()) / 60000\n : 0\n const dragDurationMinutes = dragRange\n ? (dragRange.end.getTime() - dragRange.start.getTime()) / 60000\n : 0\n return (\n <div\n key={dayStart.getTime()}\n className={cn(\n 'relative min-w-0 flex-1 border-e border-border last:border-e-0',\n days === 7 && 'min-w-[120px] max-md:max-w-[120px] md:min-w-0',\n )}\n title={nonWorking ? nonWorkingLabel : undefined}\n >\n {nonWorking ? <span className=\"sr-only\">{nonWorkingLabel}</span> : null}\n <div aria-hidden className=\"absolute inset-0\">\n {Array.from({ length: HOURS_PER_DAY }, (_, hour) => (\n <div key={hour} className=\"relative h-30 border-b border-border\">\n <span className=\"absolute inset-x-0 top-1/2 h-px bg-border/50\" />\n </div>\n ))}\n </div>\n {nonWorking ? (\n <div\n aria-hidden\n className=\"pointer-events-none absolute inset-0\"\n style={{ backgroundImage: NON_WORKING_HATCH_BACKGROUND }}\n />\n ) : null}\n {canCreateRange ? (\n <div\n className=\"absolute inset-0 cursor-cell\"\n onPointerDown={(event) => beginDrag(event, dayStart)}\n onPointerMove={moveDrag}\n onPointerUp={() => endDrag(dayStart)}\n onPointerCancel={() => setDrag(null)}\n aria-hidden\n />\n ) : null}\n {dragRange ? (\n <div\n aria-hidden\n className=\"pointer-events-none absolute z-30 flex flex-col gap-0.5 overflow-hidden rounded-md border-2 border-dashed border-foreground bg-accent-indigo/10 px-2 pt-1.5\"\n style={{\n top: (dragStartMinutes / 60) * HOUR_HEIGHT_PX,\n height: Math.max(MIN_BLOCK_HEIGHT_PX, (dragDurationMinutes / 60) * HOUR_HEIGHT_PX),\n insetInlineStart: 8,\n insetInlineEnd: 8,\n }}\n >\n <span className=\"text-overline font-semibold text-foreground\">\n {t('customers.calendar.actions.newEvent', 'New event')}\n </span>\n <span className=\"text-overline text-muted-foreground\">\n {formatTimeRange(locale, dragRange.start, dragRange.end)}\n </span>\n </div>\n ) : null}\n {conflictCount > 0 ? <ConflictBadge count={conflictCount} /> : null}\n {/* Click-through container so empty space reaches the drag-to-create layer\n below; each block re-enables pointer events for its own click/peek. */}\n <div className=\"pointer-events-none absolute inset-y-0\" style={{ insetInlineStart: 8, insetInlineEnd: 8 }}>\n {blocks.map((block) => (\n <EventPeekPopover\n key={`${block.item.id}-${block.top}`}\n item={block.item}\n open={selectedId === block.item.id}\n joinUrl={resolveJoinUrl(block.item.location)}\n aiSummaries={aiSummaries}\n onOpenChange={(open) => setSelectedId(open ? block.item.id : null)}\n onJoin={onJoin}\n onEdit={onItemClick}\n >\n <EventBlock\n item={block.item}\n top={block.top}\n height={block.height}\n insetInlineStart={block.insetInlineStart}\n width={block.width}\n conflicted={showConflicts && conflictIds.has(block.item.id)}\n highlighted={highlightItemId === block.item.id}\n selected={selectedId === block.item.id}\n nowMs={nowMs}\n />\n </EventPeekPopover>\n ))}\n </div>\n </div>\n )\n })}\n </div>\n </div>\n {items.length === 0 ? (\n <div className=\"pointer-events-none absolute inset-0 z-40 flex items-center justify-center p-6\">\n <EmptyState\n className=\"pointer-events-auto bg-card/95 shadow-sm\"\n icon={<CalendarDays className=\"h-6 w-6\" aria-hidden />}\n title={\n days === 7\n ? t('customers.calendar.empty.week', 'Nothing scheduled this week')\n : t('customers.calendar.empty.day', 'Nothing scheduled this day')\n }\n description={t('customers.calendar.empty.description', 'Plan a meeting, event or task to fill your week.')}\n actions={\n onCreate ? (\n <Button type=\"button\" size=\"sm\" onClick={onCreate}>\n {t('customers.calendar.actions.newEvent', 'New event')}\n </Button>\n ) : undefined\n }\n />\n </div>\n ) : null}\n </div>\n )\n}\n"],
5
+ "mappings": ";AA2GI,SACE,KADF;AAzGJ,YAAY,WAAW;AACvB,SAAS,cAAc,aAAa,oBAAoB;AACxD,SAAS,eAAe;AACxB,SAAS,iBAAiB;AAC1B,SAAS,kBAAkB;AAC3B,SAAS,UAAU;AACnB,SAAS,WAAW,YAAY;AAChC,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAC3B,SAAS,kBAAkB;AAC3B,SAAS,oBAAoB;AAC7B,SAAS,uBAAuB;AAChC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,YAAY,iBAAiB,wBAAwB;AAC9D,SAAS,wBAAwB;AAGjC,MAAM,iBAAiB;AACvB,MAAM,gBAAgB;AACtB,MAAM,0BAA0B;AAChC,MAAM,sBAAsB;AAC5B,MAAM,wBAAwB;AAC9B,MAAM,sBAAsB;AAC5B,MAAM,uBAAuB;AAE7B,MAAM,+BACJ;AAkBF,SAAS,eAAe,UAAgB,OAAsC;AAC5E,QAAM,SAAS,QAAQ,UAAU,CAAC;AAClC,QAAM,aAAa,SAAS,QAAQ;AACpC,QAAM,WAAW,OAAO,QAAQ;AAChC,QAAM,cAA8B,CAAC;AACrC,QAAM,WAAsE,CAAC;AAE7E,aAAW,QAAQ,OAAO;AACxB,UAAM,UAAU,KAAK,MAAM,QAAQ;AACnC,UAAM,QAAQ,KAAK,IAAI,QAAQ;AAC/B,QAAI,WAAW,YAAY,SAAS,WAAY;AAChD,QAAI,KAAK,QAAQ;AACf,kBAAY,KAAK,IAAI;AACrB;AAAA,IACF;AACA,aAAS,KAAK;AAAA,MACZ,UAAU;AAAA,MACV,OAAO,UAAU,aAAa,WAAW,KAAK;AAAA,MAC9C,KAAK,QAAQ,WAAW,SAAS,KAAK;AAAA,IACxC,CAAC;AAAA,EACH;AAEA,cAAY,KAAK,CAAC,OAAO,WAAW,MAAM,MAAM,QAAQ,IAAI,OAAO,MAAM,QAAQ,CAAC;AAElF,QAAM,SAAS,SAAS,IAAI,CAAC,aAAa,EAAE,GAAG,QAAQ,UAAU,OAAO,QAAQ,OAAO,KAAK,QAAQ,IAAI,EAAE;AAC1G,QAAM,kBAAkB,oBAAI,IAAgC;AAC5D,SAAO,QAAQ,CAAC,OAAO,UAAU,gBAAgB,IAAI,OAAO,SAAS,KAAK,EAAE,QAAQ,CAAC;AAErF,QAAM,SAA4B,aAAa,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,OAAO,QAAQ,QAAQ,MAAM;AAC/F,UAAM,gBAAgB,MAAM,MAAM,QAAQ,IAAI,cAAc;AAC5D,UAAM,mBAAmB,MAAM,IAAI,QAAQ,IAAI,MAAM,MAAM,QAAQ,KAAK;AACxE,UAAM,SAAU,eAAe,KAAM;AACrC,UAAM,YAAa,kBAAkB,KAAM;AAC3C,UAAM,WAAW,MAAM;AACvB,WAAO;AAAA,MACL,MAAM,gBAAgB,IAAI,KAAK,KAAK;AAAA,MACpC,KAAK,SAAS;AAAA,MACd,QAAQ,KAAK,IAAI,qBAAqB,YAAY,wBAAwB,CAAC;AAAA,MAC3E,kBAAkB,QAAQ,SAAS,QAAQ,OAAO,SAAS,oBAAoB;AAAA,MAC/E,OAAO,QAAQ,QAAQ,QAAS,UAAU,KAAK,uBAAwB,OAAO;AAAA,IAChF;AAAA,EACF,CAAC;AAED,SAAO,EAAE,UAAU,aAAa,OAAO;AACzC;AAIA,SAAS,cAAc,EAAE,MAAM,GAAuB;AACpD,QAAM,IAAI,KAAK;AACf,QAAM,QACJ,UAAU,IACN,EAAE,yCAAyC,YAAY,IACvD,EAAE,0CAA0C,qBAAqB,EAAE,MAAM,CAAC;AAChF,SACE,qBAAC,UAAK,WAAU,sMACd;AAAA,wBAAC,UAAK,eAAW,MAAC,WAAU,8CAA6C;AAAA,IACxE;AAAA,KACH;AAEJ;AAUA,MAAM,aAAa,MAAM,WAA+C,SAASA,YAC/E,EAAE,MAAM,YAAY,aAAa,UAAU,OAAO,WAAW,GAAG,YAAY,GAC5E,KACA;AACA,QAAM,IAAI,KAAK;AACf,QAAM,OAAO,iBAAiB,MAAM,KAAK;AACzC,QAAM,QAAQ,KAAK,SAAS,EAAE,oCAAoC,UAAU;AAC5E,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,MAAK;AAAA,MACL,SAAQ;AAAA,MACR,cAAY,GAAG,KAAK,KAAK,EAAE,kCAAkC,SAAS,CAAC;AAAA,MACvE,WAAW;AAAA,QACT;AAAA,QACA,KAAK;AAAA,QACL,cAAc;AAAA,QACd,YAAY;AAAA,QACZ,eAAe;AAAA,QACf;AAAA,QACA;AAAA,MACF;AAAA,MACA,OAAO,KAAK;AAAA,MACX,GAAG;AAAA,MAEJ,8BAAC,UAAK,WAAW,GAAG,gCAAgC,KAAK,cAAc,GAAI,iBAAM;AAAA;AAAA,EACnF;AAEJ,CAAC;AAED,WAAW,cAAc;AAElB,SAAS,SAAS;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAkB;AAChB,QAAM,IAAI,KAAK;AACf,QAAM,SAAS,UAAU;AACzB,QAAM,YAAY,MAAM,OAA8B,IAAI;AAC1D,QAAM,QAAQ,KAAK,IAAI;AACvB,QAAM,QAAQ,WAAW,oBAAI,KAAK,CAAC;AACnC,QAAM,UAAU,MAAM,QAAQ;AAC9B,QAAM,WAAW,OAAO,QAAQ;AAChC,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAAwB,IAAI;AACtE,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAA2B,IAAI;AAE7D,QAAM,YAAY,MAAM,QAAQ,MAAM;AACpC,UAAM,aAAa,gBAAgB,SAAS,IAAI,SAAS,OAAO,IAAI,KAAK,QAAQ,GAAG,CAAC,EAAE;AACvF,UAAM,MAAM,MAAM,KAAK,EAAE,QAAQ,KAAK,GAAG,CAAC,GAAG,UAAU,QAAQ,YAAY,KAAK,CAAC;AACjF,WAAO,SAAS,IAAI,uBAAuB,KAAK,cAAc,IAAI,KAAK,OAAO,CAAC,IAAI;AAAA,EACrF,GAAG,CAAC,MAAM,UAAU,cAAc,OAAO,CAAC;AAE1C,QAAM,aAAa,MAAM;AAAA,IACvB,MAAM,UAAU,IAAI,CAAC,aAAa,eAAe,UAAU,KAAK,CAAC;AAAA,IACjE,CAAC,WAAW,KAAK;AAAA,EACnB;AAEA,QAAM,iBAAiB,MAAM,YAAY,CAAC,aAA2C;AACnF,UAAM,UAAU,UAAU,KAAK,KAAK;AACpC,QAAI,CAAC,QAAS,QAAO;AACrB,QAAI,gBAAgB,KAAK,OAAO,EAAG,QAAO;AAC1C,QAAI,UAAU,KAAK,OAAO,EAAG,QAAO,WAAW,OAAO;AACtD,WAAO;AAAA,EACT,GAAG,CAAC,CAAC;AAEL,QAAM,gBAAgB,WAAW,KAAK,CAAC,WAAW,OAAO,YAAY,SAAS,CAAC;AAE/E,QAAM,aAAa,MAAM;AAAA,IACvB,OAAO;AAAA,MACL,WAAW,IAAI,KAAK,eAAe,QAAQ,EAAE,KAAK,UAAU,CAAC;AAAA,MAC7D,cAAc,IAAI,KAAK,eAAe,QAAQ,EAAE,SAAS,QAAQ,CAAC;AAAA,MAClE,aAAa,IAAI,KAAK,eAAe,QAAQ,EAAE,SAAS,OAAO,CAAC;AAAA,IAClE;AAAA,IACA,CAAC,MAAM;AAAA,EACT;AAEA,QAAM,aAAa,MAAM,QAAQ,MAAM;AACrC,UAAM,YAAY,IAAI,KAAK,eAAe,QAAQ,EAAE,MAAM,UAAU,CAAC;AACrE,WAAO,MAAM,KAAK,EAAE,QAAQ,cAAc,GAAG,CAAC,GAAG,SAAS,UAAU,OAAO,IAAI,KAAK,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC;AAAA,EACxG,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,UAAU,MAAM;AACpB,UAAM,OAAO,UAAU;AACvB,QAAI,CAAC,KAAM;AACX,SAAK,YAAY,sBAAsB;AAAA,EACzC,GAAG,CAAC,CAAC;AAEL,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,gBAAiB;AACtB,UAAM,OAAO,UAAU;AACvB,QAAI,CAAC,KAAM;AACX,UAAM,SAAS,MAAM,KAAK,CAAC,SAAS,KAAK,OAAO,mBAAmB,CAAC,KAAK,MAAM;AAC/E,QAAI,CAAC,OAAQ;AACb,UAAM,WAAW,OAAO,MAAM,QAAQ,IAAI,WAAW,OAAO,KAAK,EAAE,QAAQ,KAAK;AAChF,UAAM,MAAO,UAAU,KAAM;AAC7B,UAAM,eACJ,OAAO,WAAW,eAAe,OAAO,WAAW,kCAAkC,EAAE;AACzF,SAAK,SAAS,EAAE,KAAK,KAAK,IAAI,GAAG,MAAM,cAAc,GAAG,UAAU,eAAe,SAAS,SAAS,CAAC;AAAA,EACtG,GAAG,CAAC,iBAAiB,KAAK,CAAC;AAE3B,QAAM,kBAAkB,EAAE,sCAAsC,iBAAiB;AACjF,QAAM,gBACJ,SAAS,IACL,EAAE,mCAAmC,eAAe,IACpD,EAAE,uCAAuC,cAAc;AAC7D,QAAM,YACJ,SAAS,IAAI,EAAE,+BAA+B,WAAW,IAAI,EAAE,mCAAmC,UAAU;AAE9G,QAAM,iBAAiB,CAAC,aAA2B;AACjD,UAAM,YAAY,WAAW,UAAU,OAAO,QAAQ;AACtD,QAAI,SAAS,EAAG,QAAO,GAAG,WAAW,YAAY,OAAO,QAAQ,EAAE,YAAY,CAAC,SAAM,SAAS;AAC9F,WAAO,GAAG,SAAS,IAAI,WAAW,aAAa,OAAO,QAAQ,EAAE,YAAY,CAAC;AAAA,EAC/E;AAEA,QAAM,iBAAiB,QAAQ,aAAa;AAE5C,QAAM,YAAY,CAAC,OAA2C,aAAmB;AAC/E,QAAI,CAAC,kBAAkB,MAAM,WAAW,EAAG;AAC3C,UAAM,QAAQ,MAAM;AACpB,UAAM,OAAO,MAAM,sBAAsB;AACzC,UAAM,SAAS,iBAAiB,MAAM,UAAU,KAAK,KAAK,cAAc;AACxE,QAAI;AACF,YAAM,kBAAkB,MAAM,SAAS;AAAA,IACzC,QAAQ;AAAA,IAER;AACA,kBAAc,IAAI;AAClB,YAAQ,EAAE,OAAO,SAAS,QAAQ,GAAG,UAAU,QAAQ,QAAQ,QAAQ,OAAO,MAAM,CAAC;AAAA,EACvF;AAEA,QAAM,WAAW,CAAC,UAA8C;AAG9D,UAAM,OAAO,MAAM,cAAc,sBAAsB;AACvD,UAAM,SAAS,iBAAiB,MAAM,UAAU,KAAK,KAAK,cAAc;AACxE,YAAQ,CAAC,aAAa;AACpB,UAAI,CAAC,SAAU,QAAO;AACtB,aAAO;AAAA,QACL,GAAG;AAAA,QACH,QAAQ;AAAA,QACR,OAAO,SAAS,SAAS,KAAK,IAAI,SAAS,SAAS,QAAQ,KAAK;AAAA,MACnE;AAAA,IACF,CAAC;AAAA,EACH;AAEA,QAAM,UAAU,CAAC,aAAmB;AAClC,YAAQ,CAAC,aAAa;AACpB,UAAI,YAAY,SAAS,SAAS,eAAe;AAC/C,cAAM,QAAQ,eAAe,UAAU,SAAS,UAAU,SAAS,MAAM;AACzE,sBAAc,MAAM,OAAO,MAAM,GAAG;AAAA,MACtC;AACA,aAAO;AAAA,IACT,CAAC;AAAA,EACH;AAEA,SACE,qBAAC,SAAI,WAAU,kFACb;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACL,WAAU;AAAA,QACV,OAAO,EAAE,WAAW,wBAAwB;AAAA,QAE5C;AAAA,+BAAC,SAAI,WAAU,qDACb;AAAA,iCAAC,SAAI,WAAU,+BACb;AAAA,mCAAC,SAAI,WAAU,0DACb;AAAA,oCAAC,UAAK,WAAU,kEACd,8BAAC,cAAW,MAAK,UAAS,SAAQ,SAAQ,MAAK,MAAK,cAAY,eAAe,SAAS,MAAM,WAAW,CAAC,IAAI,GAC5G,8BAAC,eAAY,eAAW,MAAC,GAC3B,GACF;AAAA,gBACA,oBAAC,UAAK,WAAU,kEACd,8BAAC,cAAW,MAAK,UAAS,SAAQ,SAAQ,MAAK,MAAK,cAAY,WAAW,SAAS,MAAM,WAAW,IAAI,GACvG,8BAAC,gBAAa,eAAW,MAAC,GAC5B,GACF;AAAA,iBACF;AAAA,cACC,WAAW,IAAI,CAAC,EAAE,SAAS,MAAM;AAChC,sBAAM,UAAU,UAAU,UAAU,KAAK;AACzC,uBACE;AAAA,kBAAC;AAAA;AAAA,oBAEC,WAAW;AAAA,sBACT;AAAA,sBACA,SAAS,KAAK;AAAA,oBAChB;AAAA,oBAEA;AAAA,sBAAC;AAAA;AAAA,wBACC,WAAW;AAAA,0BACT;AAAA,0BACA,UAAU,kCAAkC;AAAA,wBAC9C;AAAA,wBAEC,yBAAe,QAAQ;AAAA;AAAA,oBAC1B;AAAA;AAAA,kBAbK,SAAS,QAAQ;AAAA,gBAcxB;AAAA,cAEJ,CAAC;AAAA,eACH;AAAA,YACC,gBACC,qBAAC,SAAI,WAAU,+BACb;AAAA,kCAAC,SAAI,WAAU,kHACb,8BAAC,UAAK,WAAU,wEACb,YAAE,kCAAkC,SAAS,GAChD,GACF;AAAA,cACC,WAAW,IAAI,CAAC,EAAE,UAAU,YAAY,MACvC;AAAA,gBAAC;AAAA;AAAA,kBAEC,WAAW;AAAA,oBACT;AAAA,oBACA,SAAS,KAAK;AAAA,kBAChB;AAAA,kBAEC,sBAAY,IAAI,CAAC,SAChB;AAAA,oBAAC;AAAA;AAAA,sBAEC;AAAA,sBACA,MAAM,eAAe,KAAK;AAAA,sBAC1B,SAAS,eAAe,KAAK,QAAQ;AAAA,sBACrC;AAAA,sBACA,cAAc,CAAC,SAAS,cAAc,OAAO,KAAK,KAAK,IAAI;AAAA,sBAC3D;AAAA,sBACA,QAAQ;AAAA,sBAER;AAAA,wBAAC;AAAA;AAAA,0BACC;AAAA,0BACA,YAAY,iBAAiB,YAAY,IAAI,KAAK,EAAE;AAAA,0BACpD,aAAa,oBAAoB,KAAK;AAAA,0BACtC,UAAU,eAAe,KAAK;AAAA,0BAC9B;AAAA;AAAA,sBACF;AAAA;AAAA,oBAfK,KAAK;AAAA,kBAgBZ,CACD;AAAA;AAAA,gBAzBI,SAAS,QAAQ;AAAA,cA0BxB,CACD;AAAA,eACH,IACE;AAAA,aACN;AAAA,UACA,qBAAC,SAAI,WAAU,gCAA+B,OAAO,EAAE,QAAQ,gBAAgB,eAAe,GAC5F;AAAA,gCAAC,SAAI,WAAU,4EACb,8BAAC,SAAI,WAAU,mBACZ,qBAAW;AAAA,cAAI,CAAC,OAAO,SACtB,SAAS,IAAI,OACX;AAAA,gBAAC;AAAA;AAAA,kBAEC,WAAU;AAAA,kBACV,OAAO,EAAE,KAAK,OAAO,eAAe;AAAA,kBAEnC;AAAA;AAAA,gBAJI;AAAA,cAKP;AAAA,YAEJ,GACF,GACF;AAAA,YACC,WAAW,IAAI,CAAC,EAAE,UAAU,OAAO,MAAM;AACxC,oBAAM,aAAa,aAAa,QAAQ;AACxC,oBAAM,gBAAgB,gBAClB,OAAO,OAAO,CAAC,UAAU,YAAY,IAAI,MAAM,KAAK,EAAE,CAAC,EAAE,SACzD;AACJ,oBAAM,aAAa,QAAQ,KAAK,SAAS,KAAK,UAAU,SAAS,QAAQ;AACzE,oBAAM,YAAY,aAAa,eAAe,UAAU,KAAK,UAAU,KAAK,MAAM,IAAI;AACtF,oBAAM,mBAAmB,aACpB,UAAU,MAAM,QAAQ,IAAI,WAAW,UAAU,KAAK,EAAE,QAAQ,KAAK,MACtE;AACJ,oBAAM,sBAAsB,aACvB,UAAU,IAAI,QAAQ,IAAI,UAAU,MAAM,QAAQ,KAAK,MACxD;AACJ,qBACE;AAAA,gBAAC;AAAA;AAAA,kBAEC,WAAW;AAAA,oBACT;AAAA,oBACA,SAAS,KAAK;AAAA,kBAChB;AAAA,kBACA,OAAO,aAAa,kBAAkB;AAAA,kBAErC;AAAA,iCAAa,oBAAC,UAAK,WAAU,WAAW,2BAAgB,IAAU;AAAA,oBACnE,oBAAC,SAAI,eAAW,MAAC,WAAU,oBACxB,gBAAM,KAAK,EAAE,QAAQ,cAAc,GAAG,CAAC,GAAG,SACzC,oBAAC,SAAe,WAAU,wCACxB,8BAAC,UAAK,WAAU,gDAA+C,KADvD,IAEV,CACD,GACH;AAAA,oBACC,aACC;AAAA,sBAAC;AAAA;AAAA,wBACC,eAAW;AAAA,wBACX,WAAU;AAAA,wBACV,OAAO,EAAE,iBAAiB,6BAA6B;AAAA;AAAA,oBACzD,IACE;AAAA,oBACH,iBACC;AAAA,sBAAC;AAAA;AAAA,wBACC,WAAU;AAAA,wBACV,eAAe,CAAC,UAAU,UAAU,OAAO,QAAQ;AAAA,wBACnD,eAAe;AAAA,wBACf,aAAa,MAAM,QAAQ,QAAQ;AAAA,wBACnC,iBAAiB,MAAM,QAAQ,IAAI;AAAA,wBACnC,eAAW;AAAA;AAAA,oBACb,IACE;AAAA,oBACH,YACC;AAAA,sBAAC;AAAA;AAAA,wBACC,eAAW;AAAA,wBACX,WAAU;AAAA,wBACV,OAAO;AAAA,0BACL,KAAM,mBAAmB,KAAM;AAAA,0BAC/B,QAAQ,KAAK,IAAI,qBAAsB,sBAAsB,KAAM,cAAc;AAAA,0BACjF,kBAAkB;AAAA,0BAClB,gBAAgB;AAAA,wBAClB;AAAA,wBAEA;AAAA,8CAAC,UAAK,WAAU,+CACb,YAAE,uCAAuC,WAAW,GACvD;AAAA,0BACA,oBAAC,UAAK,WAAU,uCACb,0BAAgB,QAAQ,UAAU,OAAO,UAAU,GAAG,GACzD;AAAA;AAAA;AAAA,oBACF,IACE;AAAA,oBACH,gBAAgB,IAAI,oBAAC,iBAAc,OAAO,eAAe,IAAK;AAAA,oBAG/D,oBAAC,SAAI,WAAU,0CAAyC,OAAO,EAAE,kBAAkB,GAAG,gBAAgB,EAAE,GACrG,iBAAO,IAAI,CAAC,UACX;AAAA,sBAAC;AAAA;AAAA,wBAEC,MAAM,MAAM;AAAA,wBACZ,MAAM,eAAe,MAAM,KAAK;AAAA,wBAChC,SAAS,eAAe,MAAM,KAAK,QAAQ;AAAA,wBAC3C;AAAA,wBACA,cAAc,CAAC,SAAS,cAAc,OAAO,MAAM,KAAK,KAAK,IAAI;AAAA,wBACjE;AAAA,wBACA,QAAQ;AAAA,wBAER;AAAA,0BAAC;AAAA;AAAA,4BACC,MAAM,MAAM;AAAA,4BACZ,KAAK,MAAM;AAAA,4BACX,QAAQ,MAAM;AAAA,4BACd,kBAAkB,MAAM;AAAA,4BACxB,OAAO,MAAM;AAAA,4BACb,YAAY,iBAAiB,YAAY,IAAI,MAAM,KAAK,EAAE;AAAA,4BAC1D,aAAa,oBAAoB,MAAM,KAAK;AAAA,4BAC5C,UAAU,eAAe,MAAM,KAAK;AAAA,4BACpC;AAAA;AAAA,wBACF;AAAA;AAAA,sBAnBK,GAAG,MAAM,KAAK,EAAE,IAAI,MAAM,GAAG;AAAA,oBAoBpC,CACD,GACH;AAAA;AAAA;AAAA,gBA/EK,SAAS,QAAQ;AAAA,cAgFxB;AAAA,YAEJ,CAAC;AAAA,aACH;AAAA;AAAA;AAAA,IACF;AAAA,IACC,MAAM,WAAW,IAChB,oBAAC,SAAI,WAAU,kFACb;AAAA,MAAC;AAAA;AAAA,QACC,WAAU;AAAA,QACV,MAAM,oBAAC,gBAAa,WAAU,WAAU,eAAW,MAAC;AAAA,QACpD,OACE,SAAS,IACL,EAAE,iCAAiC,6BAA6B,IAChE,EAAE,gCAAgC,4BAA4B;AAAA,QAEpE,aAAa,EAAE,wCAAwC,kDAAkD;AAAA,QACzG,SACE,WACE,oBAAC,UAAO,MAAK,UAAS,MAAK,MAAK,SAAS,UACtC,YAAE,uCAAuC,WAAW,GACvD,IACE;AAAA;AAAA,IAER,GACF,IACE;AAAA,KACN;AAEJ;",
6
6
  "names": ["AllDayChip"]
7
7
  }
@@ -1,3 +1,4 @@
1
+ import { isSameDay } from "date-fns/isSameDay";
1
2
  const DRAG_SNAP_MINUTES = 15;
2
3
  const MIN_DRAG_DURATION_MINUTES = 30;
3
4
  const MINUTES_PER_DAY = 24 * 60;
@@ -5,9 +6,11 @@ function isWeekendDay(date) {
5
6
  const weekday = date.getDay();
6
7
  return weekday === 0 || weekday === 6;
7
8
  }
8
- function applyWeekendVisibility(days, showWeekends) {
9
+ function applyWeekendVisibility(days, showWeekends, keepWeekendDate) {
9
10
  if (showWeekends) return days;
10
- const workingDays = days.filter((day) => !isWeekendDay(day));
11
+ const workingDays = days.filter(
12
+ (day) => !isWeekendDay(day) || keepWeekendDate != null && isSameDay(day, keepWeekendDate)
13
+ );
11
14
  return workingDays.length > 0 ? workingDays : days;
12
15
  }
13
16
  function clampMinutes(value) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/customers/lib/calendar/grid.ts"],
4
- "sourcesContent": ["export const DRAG_SNAP_MINUTES = 15\nexport const MIN_DRAG_DURATION_MINUTES = 30\nconst MINUTES_PER_DAY = 24 * 60\n\nexport function isWeekendDay(date: Date): boolean {\n const weekday = date.getDay()\n return weekday === 0 || weekday === 6\n}\n\nexport function applyWeekendVisibility(days: Date[], showWeekends: boolean): Date[] {\n if (showWeekends) return days\n const workingDays = days.filter((day) => !isWeekendDay(day))\n return workingDays.length > 0 ? workingDays : days\n}\n\nfunction clampMinutes(value: number): number {\n if (value < 0) return 0\n if (value > MINUTES_PER_DAY) return MINUTES_PER_DAY\n return value\n}\n\nexport function offsetYToMinutes(\n offsetY: number,\n hourHeightPx: number,\n snapMinutes: number = DRAG_SNAP_MINUTES,\n): number {\n if (hourHeightPx <= 0) return 0\n const rawMinutes = (offsetY / hourHeightPx) * 60\n const snapped = Math.round(rawMinutes / snapMinutes) * snapMinutes\n return clampMinutes(snapped)\n}\n\nexport type DragRange = { start: Date; end: Date }\n\nexport function buildDragRange(dayStart: Date, startMinutes: number, endMinutes: number): DragRange {\n let lower = clampMinutes(Math.min(startMinutes, endMinutes))\n let upper = clampMinutes(Math.max(startMinutes, endMinutes))\n if (upper - lower < MIN_DRAG_DURATION_MINUTES) upper = lower + MIN_DRAG_DURATION_MINUTES\n if (upper > MINUTES_PER_DAY) {\n upper = MINUTES_PER_DAY\n lower = Math.max(0, upper - MIN_DRAG_DURATION_MINUTES)\n }\n const start = new Date(dayStart)\n start.setHours(0, 0, 0, 0)\n start.setMinutes(lower)\n const end = new Date(dayStart)\n end.setHours(0, 0, 0, 0)\n end.setMinutes(upper)\n return { start, end }\n}\n"],
5
- "mappings": "AAAO,MAAM,oBAAoB;AAC1B,MAAM,4BAA4B;AACzC,MAAM,kBAAkB,KAAK;AAEtB,SAAS,aAAa,MAAqB;AAChD,QAAM,UAAU,KAAK,OAAO;AAC5B,SAAO,YAAY,KAAK,YAAY;AACtC;AAEO,SAAS,uBAAuB,MAAc,cAA+B;AAClF,MAAI,aAAc,QAAO;AACzB,QAAM,cAAc,KAAK,OAAO,CAAC,QAAQ,CAAC,aAAa,GAAG,CAAC;AAC3D,SAAO,YAAY,SAAS,IAAI,cAAc;AAChD;AAEA,SAAS,aAAa,OAAuB;AAC3C,MAAI,QAAQ,EAAG,QAAO;AACtB,MAAI,QAAQ,gBAAiB,QAAO;AACpC,SAAO;AACT;AAEO,SAAS,iBACd,SACA,cACA,cAAsB,mBACd;AACR,MAAI,gBAAgB,EAAG,QAAO;AAC9B,QAAM,aAAc,UAAU,eAAgB;AAC9C,QAAM,UAAU,KAAK,MAAM,aAAa,WAAW,IAAI;AACvD,SAAO,aAAa,OAAO;AAC7B;AAIO,SAAS,eAAe,UAAgB,cAAsB,YAA+B;AAClG,MAAI,QAAQ,aAAa,KAAK,IAAI,cAAc,UAAU,CAAC;AAC3D,MAAI,QAAQ,aAAa,KAAK,IAAI,cAAc,UAAU,CAAC;AAC3D,MAAI,QAAQ,QAAQ,0BAA2B,SAAQ,QAAQ;AAC/D,MAAI,QAAQ,iBAAiB;AAC3B,YAAQ;AACR,YAAQ,KAAK,IAAI,GAAG,QAAQ,yBAAyB;AAAA,EACvD;AACA,QAAM,QAAQ,IAAI,KAAK,QAAQ;AAC/B,QAAM,SAAS,GAAG,GAAG,GAAG,CAAC;AACzB,QAAM,WAAW,KAAK;AACtB,QAAM,MAAM,IAAI,KAAK,QAAQ;AAC7B,MAAI,SAAS,GAAG,GAAG,GAAG,CAAC;AACvB,MAAI,WAAW,KAAK;AACpB,SAAO,EAAE,OAAO,IAAI;AACtB;",
4
+ "sourcesContent": ["import { isSameDay } from 'date-fns/isSameDay'\n\nexport const DRAG_SNAP_MINUTES = 15\nexport const MIN_DRAG_DURATION_MINUTES = 30\nconst MINUTES_PER_DAY = 24 * 60\n\nexport function isWeekendDay(date: Date): boolean {\n const weekday = date.getDay()\n return weekday === 0 || weekday === 6\n}\n\nexport function applyWeekendVisibility(\n days: Date[],\n showWeekends: boolean,\n keepWeekendDate?: Date | null,\n): Date[] {\n if (showWeekends) return days\n const workingDays = days.filter(\n (day) => !isWeekendDay(day) || (keepWeekendDate != null && isSameDay(day, keepWeekendDate)),\n )\n return workingDays.length > 0 ? workingDays : days\n}\n\nfunction clampMinutes(value: number): number {\n if (value < 0) return 0\n if (value > MINUTES_PER_DAY) return MINUTES_PER_DAY\n return value\n}\n\nexport function offsetYToMinutes(\n offsetY: number,\n hourHeightPx: number,\n snapMinutes: number = DRAG_SNAP_MINUTES,\n): number {\n if (hourHeightPx <= 0) return 0\n const rawMinutes = (offsetY / hourHeightPx) * 60\n const snapped = Math.round(rawMinutes / snapMinutes) * snapMinutes\n return clampMinutes(snapped)\n}\n\nexport type DragRange = { start: Date; end: Date }\n\nexport function buildDragRange(dayStart: Date, startMinutes: number, endMinutes: number): DragRange {\n let lower = clampMinutes(Math.min(startMinutes, endMinutes))\n let upper = clampMinutes(Math.max(startMinutes, endMinutes))\n if (upper - lower < MIN_DRAG_DURATION_MINUTES) upper = lower + MIN_DRAG_DURATION_MINUTES\n if (upper > MINUTES_PER_DAY) {\n upper = MINUTES_PER_DAY\n lower = Math.max(0, upper - MIN_DRAG_DURATION_MINUTES)\n }\n const start = new Date(dayStart)\n start.setHours(0, 0, 0, 0)\n start.setMinutes(lower)\n const end = new Date(dayStart)\n end.setHours(0, 0, 0, 0)\n end.setMinutes(upper)\n return { start, end }\n}\n"],
5
+ "mappings": "AAAA,SAAS,iBAAiB;AAEnB,MAAM,oBAAoB;AAC1B,MAAM,4BAA4B;AACzC,MAAM,kBAAkB,KAAK;AAEtB,SAAS,aAAa,MAAqB;AAChD,QAAM,UAAU,KAAK,OAAO;AAC5B,SAAO,YAAY,KAAK,YAAY;AACtC;AAEO,SAAS,uBACd,MACA,cACA,iBACQ;AACR,MAAI,aAAc,QAAO;AACzB,QAAM,cAAc,KAAK;AAAA,IACvB,CAAC,QAAQ,CAAC,aAAa,GAAG,KAAM,mBAAmB,QAAQ,UAAU,KAAK,eAAe;AAAA,EAC3F;AACA,SAAO,YAAY,SAAS,IAAI,cAAc;AAChD;AAEA,SAAS,aAAa,OAAuB;AAC3C,MAAI,QAAQ,EAAG,QAAO;AACtB,MAAI,QAAQ,gBAAiB,QAAO;AACpC,SAAO;AACT;AAEO,SAAS,iBACd,SACA,cACA,cAAsB,mBACd;AACR,MAAI,gBAAgB,EAAG,QAAO;AAC9B,QAAM,aAAc,UAAU,eAAgB;AAC9C,QAAM,UAAU,KAAK,MAAM,aAAa,WAAW,IAAI;AACvD,SAAO,aAAa,OAAO;AAC7B;AAIO,SAAS,eAAe,UAAgB,cAAsB,YAA+B;AAClG,MAAI,QAAQ,aAAa,KAAK,IAAI,cAAc,UAAU,CAAC;AAC3D,MAAI,QAAQ,aAAa,KAAK,IAAI,cAAc,UAAU,CAAC;AAC3D,MAAI,QAAQ,QAAQ,0BAA2B,SAAQ,QAAQ;AAC/D,MAAI,QAAQ,iBAAiB;AAC3B,YAAQ;AACR,YAAQ,KAAK,IAAI,GAAG,QAAQ,yBAAyB;AAAA,EACvD;AACA,QAAM,QAAQ,IAAI,KAAK,QAAQ;AAC/B,QAAM,SAAS,GAAG,GAAG,GAAG,CAAC;AACzB,QAAM,WAAW,KAAK;AACtB,QAAM,MAAM,IAAI,KAAK,QAAQ;AAC7B,MAAI,SAAS,GAAG,GAAG,GAAG,CAAC;AACvB,MAAI,WAAW,KAAK;AACpB,SAAO,EAAE,OAAO,IAAI;AACtB;",
6
6
  "names": []
7
7
  }
@@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
2
2
  import { z } from "zod";
3
3
  import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
4
4
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
+ import { isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
5
6
  import {
6
7
  runCrudMutationGuardAfterSuccess,
7
8
  validateCrudMutationGuard
@@ -51,15 +52,24 @@ async function DELETE(req, ctx) {
51
52
  if (guardResult && !guardResult.ok) {
52
53
  return NextResponse.json(guardResult.body, { status: guardResult.status });
53
54
  }
54
- const deleted = await deleteUserPerspective(em, cache, {
55
- scope: {
56
- userId: auth.sub,
57
- tenantId: auth.tenantId ?? null,
58
- organizationId: auth.orgId ?? null
59
- },
60
- tableId,
61
- perspectiveId
62
- });
55
+ let deleted = false;
56
+ try {
57
+ deleted = await deleteUserPerspective(em, cache, {
58
+ scope: {
59
+ userId: auth.sub,
60
+ tenantId: auth.tenantId ?? null,
61
+ organizationId: auth.orgId ?? null
62
+ },
63
+ tableId,
64
+ perspectiveId,
65
+ request: req
66
+ });
67
+ } catch (err) {
68
+ if (isCrudHttpError(err)) {
69
+ return NextResponse.json(err.body, { status: err.status });
70
+ }
71
+ throw err;
72
+ }
63
73
  if (deleted && guardResult?.ok && guardResult.shouldRunAfterSuccess) {
64
74
  await runCrudMutationGuardAfterSuccess(container, {
65
75
  tenantId: auth.tenantId ?? "",
@@ -89,6 +99,7 @@ const perspectiveDeleteDoc = {
89
99
  errors: [
90
100
  { status: 400, description: "Invalid identifiers supplied", schema: perspectivesErrorSchema },
91
101
  { status: 401, description: "Authentication required", schema: perspectivesErrorSchema },
102
+ { status: 409, description: "Optimistic lock conflict", schema: perspectivesErrorSchema },
92
103
  { status: 404, description: "Perspective not found", schema: perspectivesErrorSchema }
93
104
  ]
94
105
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/perspectives/api/%5BtableId%5D/%5BperspectiveId%5D/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { deleteUserPerspective } from '@open-mercato/core/modules/perspectives/services/perspectiveService'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { perspectivesTag, perspectivesErrorSchema, perspectivesSuccessSchema } from '../../openapi'\n\nexport const metadata = {\n DELETE: { requireAuth: true, requireFeatures: ['perspectives.use'] },\n}\n\nconst decodeParam = (value: string | string[] | undefined): string => {\n if (!value) return ''\n const raw = Array.isArray(value) ? value[0] : value\n try {\n return decodeURIComponent(raw)\n } catch {\n return raw\n }\n}\n\nexport async function DELETE(req: Request, ctx: { params: { tableId: string; perspectiveId: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const tableId = decodeParam(ctx.params?.tableId).trim()\n const perspectiveId = decodeParam(ctx.params?.perspectiveId).trim()\n if (!tableId || !perspectiveId) {\n return NextResponse.json({ error: 'Invalid parameters' }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as import('@mikro-orm/postgresql').EntityManager\n const cache = ((): import('@open-mercato/cache').CacheStrategy | null => {\n try {\n return container.resolve('cache') as import('@open-mercato/cache').CacheStrategy\n } catch {\n return null\n }\n })()\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId ?? '',\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n resourceKind: 'perspectives.perspective',\n resourceId: perspectiveId,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: { tableId, perspectiveId },\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const deleted = await deleteUserPerspective(em, cache, {\n scope: {\n userId: auth.sub,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n },\n tableId,\n perspectiveId,\n })\n\n if (deleted && guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: auth.tenantId ?? '',\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n resourceKind: 'perspectives.perspective',\n resourceId: perspectiveId,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({ success: true })\n}\n\nconst perspectiveDeletePathParamsSchema = z.object({\n tableId: z.string().min(1),\n perspectiveId: z.string().uuid(),\n})\n\nconst perspectiveDeleteDoc: OpenApiMethodDoc = {\n summary: 'Delete a personal perspective',\n description: 'Removes a perspective owned by the current user for the given table.',\n tags: [perspectivesTag],\n responses: [\n { status: 200, description: 'Perspective removed.', schema: perspectivesSuccessSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid identifiers supplied', schema: perspectivesErrorSchema },\n { status: 401, description: 'Authentication required', schema: perspectivesErrorSchema },\n { status: 404, description: 'Perspective not found', schema: perspectivesErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: perspectivesTag,\n summary: 'Delete personal perspective',\n pathParams: perspectiveDeletePathParamsSchema,\n methods: {\n DELETE: perspectiveDeleteDoc,\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { deleteUserPerspective } from '@open-mercato/core/modules/perspectives/services/perspectiveService'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { perspectivesTag, perspectivesErrorSchema, perspectivesSuccessSchema } from '../../openapi'\n\nexport const metadata = {\n DELETE: { requireAuth: true, requireFeatures: ['perspectives.use'] },\n}\n\nconst decodeParam = (value: string | string[] | undefined): string => {\n if (!value) return ''\n const raw = Array.isArray(value) ? value[0] : value\n try {\n return decodeURIComponent(raw)\n } catch {\n return raw\n }\n}\n\nexport async function DELETE(req: Request, ctx: { params: { tableId: string; perspectiveId: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const tableId = decodeParam(ctx.params?.tableId).trim()\n const perspectiveId = decodeParam(ctx.params?.perspectiveId).trim()\n if (!tableId || !perspectiveId) {\n return NextResponse.json({ error: 'Invalid parameters' }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as import('@mikro-orm/postgresql').EntityManager\n const cache = ((): import('@open-mercato/cache').CacheStrategy | null => {\n try {\n return container.resolve('cache') as import('@open-mercato/cache').CacheStrategy\n } catch {\n return null\n }\n })()\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId ?? '',\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n resourceKind: 'perspectives.perspective',\n resourceId: perspectiveId,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: { tableId, perspectiveId },\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n let deleted = false\n try {\n deleted = await deleteUserPerspective(em, cache, {\n scope: {\n userId: auth.sub,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n },\n tableId,\n perspectiveId,\n request: req,\n })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n throw err\n }\n\n if (deleted && guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: auth.tenantId ?? '',\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n resourceKind: 'perspectives.perspective',\n resourceId: perspectiveId,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({ success: true })\n}\n\nconst perspectiveDeletePathParamsSchema = z.object({\n tableId: z.string().min(1),\n perspectiveId: z.string().uuid(),\n})\n\nconst perspectiveDeleteDoc: OpenApiMethodDoc = {\n summary: 'Delete a personal perspective',\n description: 'Removes a perspective owned by the current user for the given table.',\n tags: [perspectivesTag],\n responses: [\n { status: 200, description: 'Perspective removed.', schema: perspectivesSuccessSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid identifiers supplied', schema: perspectivesErrorSchema },\n { status: 401, description: 'Authentication required', schema: perspectivesErrorSchema },\n { status: 409, description: 'Optimistic lock conflict', schema: perspectivesErrorSchema },\n { status: 404, description: 'Perspective not found', schema: perspectivesErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: perspectivesTag,\n summary: 'Delete personal perspective',\n pathParams: perspectiveDeletePathParamsSchema,\n methods: {\n DELETE: perspectiveDeleteDoc,\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
2
2
  import { z } from "zod";
3
3
  import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
4
4
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
+ import { isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
5
6
  import {
6
7
  runCrudMutationGuardAfterSuccess,
7
8
  validateCrudMutationGuard
@@ -21,6 +22,10 @@ const decodeParam = (value) => {
21
22
  return raw;
22
23
  }
23
24
  };
25
+ const rolePerspectiveDeleteBodySchema = z.object({
26
+ roleExpectedUpdatedAtByRoleId: z.record(z.string().uuid(), z.string().min(1).nullable()).optional(),
27
+ roleExpectedUpdatedAtByPerspectiveId: z.record(z.string().uuid(), z.string().min(1).nullable()).optional()
28
+ }).optional();
24
29
  async function DELETE(req, ctx) {
25
30
  const auth = await getAuthFromRequest(req);
26
31
  if (!auth) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
@@ -29,6 +34,21 @@ async function DELETE(req, ctx) {
29
34
  if (!tableId || !roleId) {
30
35
  return NextResponse.json({ error: "Invalid parameters" }, { status: 400 });
31
36
  }
37
+ let parsedBody = void 0;
38
+ if (req.body && (req.headers.get("content-type") ?? "").includes("application/json")) {
39
+ try {
40
+ const rawBody = await req.text();
41
+ if (rawBody.trim().length > 0) {
42
+ const parsed = rolePerspectiveDeleteBodySchema.safeParse(JSON.parse(rawBody));
43
+ if (!parsed.success) {
44
+ return NextResponse.json({ error: "Invalid payload", details: parsed.error.flatten() }, { status: 400 });
45
+ }
46
+ parsedBody = parsed.data;
47
+ }
48
+ } catch {
49
+ return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
50
+ }
51
+ }
32
52
  const container = await createRequestContainer();
33
53
  const em = container.resolve("em");
34
54
  const cache = (() => {
@@ -55,12 +75,23 @@ async function DELETE(req, ctx) {
55
75
  if (guardResult && !guardResult.ok) {
56
76
  return NextResponse.json(guardResult.body, { status: guardResult.status });
57
77
  }
58
- const clearedCount = await clearRolePerspectives(em, cache, {
59
- tableId,
60
- tenantId: auth.tenantId ?? null,
61
- organizationId: auth.orgId ?? null,
62
- roleIds: [roleId]
63
- });
78
+ let clearedCount = 0;
79
+ try {
80
+ clearedCount = await clearRolePerspectives(em, cache, {
81
+ tableId,
82
+ tenantId: auth.tenantId ?? null,
83
+ organizationId: auth.orgId ?? null,
84
+ roleIds: [roleId],
85
+ expectedUpdatedAtByRoleId: parsedBody?.roleExpectedUpdatedAtByRoleId,
86
+ expectedUpdatedAtByPerspectiveId: parsedBody?.roleExpectedUpdatedAtByPerspectiveId,
87
+ request: req
88
+ });
89
+ } catch (err) {
90
+ if (isCrudHttpError(err)) {
91
+ return NextResponse.json(err.body, { status: err.status });
92
+ }
93
+ throw err;
94
+ }
64
95
  if (clearedCount > 0 && guardResult?.ok && guardResult.shouldRunAfterSuccess) {
65
96
  await runCrudMutationGuardAfterSuccess(container, {
66
97
  tenantId: auth.tenantId ?? "",
@@ -84,6 +115,11 @@ const rolePerspectiveDeleteDoc = {
84
115
  summary: "Clear role perspectives for a table",
85
116
  description: "Removes all role-level perspectives associated with the provided role identifier for the table.",
86
117
  tags: [perspectivesTag],
118
+ requestBody: {
119
+ contentType: "application/json",
120
+ schema: rolePerspectiveDeleteBodySchema,
121
+ description: "Optional per-role or per-perspective optimistic-lock tokens."
122
+ },
87
123
  responses: [
88
124
  { status: 200, description: "Role perspectives cleared.", schema: perspectivesSuccessSchema }
89
125
  ],
@@ -91,6 +127,7 @@ const rolePerspectiveDeleteDoc = {
91
127
  { status: 400, description: "Invalid identifiers supplied", schema: perspectivesErrorSchema },
92
128
  { status: 401, description: "Authentication required", schema: perspectivesErrorSchema },
93
129
  { status: 403, description: "Missing perspectives.role_defaults feature", schema: perspectivesErrorSchema },
130
+ { status: 409, description: "Optimistic lock conflict", schema: perspectivesErrorSchema },
94
131
  { status: 404, description: "Role not found in scope", schema: perspectivesErrorSchema }
95
132
  ]
96
133
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../src/modules/perspectives/api/%5BtableId%5D/roles/%5BroleId%5D/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { clearRolePerspectives } from '@open-mercato/core/modules/perspectives/services/perspectiveService'\nimport { Role } from '@open-mercato/core/modules/auth/data/entities'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { perspectivesTag, perspectivesErrorSchema, perspectivesSuccessSchema } from '../../../openapi'\n\nexport const metadata = {\n DELETE: { requireAuth: true, requireFeatures: ['perspectives.role_defaults'] },\n}\n\nconst decodeParam = (value: string | string[] | undefined): string => {\n if (!value) return ''\n const raw = Array.isArray(value) ? value[0] : value\n try {\n return decodeURIComponent(raw)\n } catch {\n return raw\n }\n}\n\nexport async function DELETE(req: Request, ctx: { params: { tableId: string; roleId: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const tableId = decodeParam(ctx.params?.tableId).trim()\n const roleId = decodeParam(ctx.params?.roleId).trim()\n if (!tableId || !roleId) {\n return NextResponse.json({ error: 'Invalid parameters' }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as import('@mikro-orm/postgresql').EntityManager\n const cache = ((): import('@open-mercato/cache').CacheStrategy | null => {\n try {\n return container.resolve('cache') as import('@open-mercato/cache').CacheStrategy\n } catch {\n return null\n }\n })()\n\n const scope = auth.tenantId\n ? { $or: [{ tenantId: auth.tenantId }, { tenantId: null }] }\n : { tenantId: null }\n\n const role = await em.findOne(Role, { id: roleId, deletedAt: null, ...(scope as any) } as any)\n if (!role) return NextResponse.json({ error: 'Role not found' }, { status: 404 })\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId ?? '',\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n resourceKind: 'perspectives.role_perspective',\n resourceId: roleId,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: { tableId, roleId },\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const clearedCount = await clearRolePerspectives(em, cache, {\n tableId,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n roleIds: [roleId],\n })\n\n if (clearedCount > 0 && guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: auth.tenantId ?? '',\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n resourceKind: 'perspectives.role_perspective',\n resourceId: roleId,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({ success: true })\n}\n\nconst rolePerspectiveDeleteParamsSchema = z.object({\n tableId: z.string().min(1),\n roleId: z.string().uuid(),\n})\n\nconst rolePerspectiveDeleteDoc: OpenApiMethodDoc = {\n summary: 'Clear role perspectives for a table',\n description: 'Removes all role-level perspectives associated with the provided role identifier for the table.',\n tags: [perspectivesTag],\n responses: [\n { status: 200, description: 'Role perspectives cleared.', schema: perspectivesSuccessSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid identifiers supplied', schema: perspectivesErrorSchema },\n { status: 401, description: 'Authentication required', schema: perspectivesErrorSchema },\n { status: 403, description: 'Missing perspectives.role_defaults feature', schema: perspectivesErrorSchema },\n { status: 404, description: 'Role not found in scope', schema: perspectivesErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: perspectivesTag,\n summary: 'Delete role-level perspectives',\n pathParams: rolePerspectiveDeleteParamsSchema,\n methods: {\n DELETE: rolePerspectiveDeleteDoc,\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { clearRolePerspectives } from '@open-mercato/core/modules/perspectives/services/perspectiveService'\nimport { Role } from '@open-mercato/core/modules/auth/data/entities'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { perspectivesTag, perspectivesErrorSchema, perspectivesSuccessSchema } from '../../../openapi'\n\nexport const metadata = {\n DELETE: { requireAuth: true, requireFeatures: ['perspectives.role_defaults'] },\n}\n\nconst decodeParam = (value: string | string[] | undefined): string => {\n if (!value) return ''\n const raw = Array.isArray(value) ? value[0] : value\n try {\n return decodeURIComponent(raw)\n } catch {\n return raw\n }\n}\n\nconst rolePerspectiveDeleteBodySchema = z.object({\n roleExpectedUpdatedAtByRoleId: z.record(z.string().uuid(), z.string().min(1).nullable()).optional(),\n roleExpectedUpdatedAtByPerspectiveId: z.record(z.string().uuid(), z.string().min(1).nullable()).optional(),\n}).optional()\n\nexport async function DELETE(req: Request, ctx: { params: { tableId: string; roleId: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const tableId = decodeParam(ctx.params?.tableId).trim()\n const roleId = decodeParam(ctx.params?.roleId).trim()\n if (!tableId || !roleId) {\n return NextResponse.json({ error: 'Invalid parameters' }, { status: 400 })\n }\n\n let parsedBody: z.infer<typeof rolePerspectiveDeleteBodySchema> = undefined\n if (req.body && (req.headers.get('content-type') ?? '').includes('application/json')) {\n try {\n const rawBody = await req.text()\n if (rawBody.trim().length > 0) {\n const parsed = rolePerspectiveDeleteBodySchema.safeParse(JSON.parse(rawBody))\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 400 })\n }\n parsedBody = parsed.data\n }\n } catch {\n return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 })\n }\n }\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as import('@mikro-orm/postgresql').EntityManager\n const cache = ((): import('@open-mercato/cache').CacheStrategy | null => {\n try {\n return container.resolve('cache') as import('@open-mercato/cache').CacheStrategy\n } catch {\n return null\n }\n })()\n\n const scope = auth.tenantId\n ? { $or: [{ tenantId: auth.tenantId }, { tenantId: null }] }\n : { tenantId: null }\n\n const role = await em.findOne(Role, { id: roleId, deletedAt: null, ...(scope as any) } as any)\n if (!role) return NextResponse.json({ error: 'Role not found' }, { status: 404 })\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId ?? '',\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n resourceKind: 'perspectives.role_perspective',\n resourceId: roleId,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: { tableId, roleId },\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n let clearedCount = 0\n try {\n clearedCount = await clearRolePerspectives(em, cache, {\n tableId,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n roleIds: [roleId],\n expectedUpdatedAtByRoleId: parsedBody?.roleExpectedUpdatedAtByRoleId,\n expectedUpdatedAtByPerspectiveId: parsedBody?.roleExpectedUpdatedAtByPerspectiveId,\n request: req,\n })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n throw err\n }\n\n if (clearedCount > 0 && guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: auth.tenantId ?? '',\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n resourceKind: 'perspectives.role_perspective',\n resourceId: roleId,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({ success: true })\n}\n\nconst rolePerspectiveDeleteParamsSchema = z.object({\n tableId: z.string().min(1),\n roleId: z.string().uuid(),\n})\n\nconst rolePerspectiveDeleteDoc: OpenApiMethodDoc = {\n summary: 'Clear role perspectives for a table',\n description: 'Removes all role-level perspectives associated with the provided role identifier for the table.',\n tags: [perspectivesTag],\n requestBody: {\n contentType: 'application/json',\n schema: rolePerspectiveDeleteBodySchema,\n description: 'Optional per-role or per-perspective optimistic-lock tokens.',\n },\n responses: [\n { status: 200, description: 'Role perspectives cleared.', schema: perspectivesSuccessSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid identifiers supplied', schema: perspectivesErrorSchema },\n { status: 401, description: 'Authentication required', schema: perspectivesErrorSchema },\n { status: 403, description: 'Missing perspectives.role_defaults feature', schema: perspectivesErrorSchema },\n { status: 409, description: 'Optimistic lock conflict', schema: perspectivesErrorSchema },\n { status: 404, description: 'Role not found in scope', schema: perspectivesErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: perspectivesTag,\n summary: 'Delete role-level perspectives',\n pathParams: rolePerspectiveDeleteParamsSchema,\n methods: {\n DELETE: rolePerspectiveDeleteDoc,\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -71,13 +71,19 @@ async function GET(_req, ctx) {
71
71
  ) ?? false;
72
72
  const roleScope = auth.tenantId ? { $or: [{ tenantId: auth.tenantId }, { tenantId: null }] } : { tenantId: null };
73
73
  const availableRoles = canApplyToRoles ? await em.find(Role, { ...roleScope, deletedAt: null }, { orderBy: { name: "asc" } }) : assignedRoles;
74
+ const manageableRoleIds = canApplyToRoles ? availableRoles.map((role) => role.id) : assignedRoleIds;
74
75
  const state = await loadPerspectivesState(em, cache, {
75
76
  scope: buildScope(auth),
76
77
  tableId,
77
78
  roleIds: assignedRoleIds
78
79
  });
80
+ const manageableState = manageableRoleIds.length === assignedRoleIds.length && manageableRoleIds.every((roleId) => assignedRoleIds.includes(roleId)) ? state : await loadPerspectivesState(em, cache, {
81
+ scope: buildScope(auth),
82
+ tableId,
83
+ roleIds: manageableRoleIds
84
+ });
79
85
  const rolePerspectiveByRole = /* @__PURE__ */ new Map();
80
- for (const item of state.rolePerspectives) {
86
+ for (const item of manageableState.rolePerspectives) {
81
87
  const entry = rolePerspectiveByRole.get(item.roleId) ?? { hasDefault: false, count: 0 };
82
88
  entry.count += 1;
83
89
  entry.hasDefault = entry.hasDefault || item.isDefault;
@@ -91,6 +97,10 @@ async function GET(_req, ctx) {
91
97
  ...rp,
92
98
  roleName: availableRoles.find((role) => role.id === rp.roleId)?.name ?? assignedRoles.find((role) => role.id === rp.roleId)?.name ?? null
93
99
  })),
100
+ manageableRolePerspectives: manageableState.rolePerspectives.map((rp) => ({
101
+ ...rp,
102
+ roleName: availableRoles.find((role) => role.id === rp.roleId)?.name ?? assignedRoles.find((role) => role.id === rp.roleId)?.name ?? null
103
+ })),
94
104
  roles: availableRoles.map((role) => {
95
105
  const stats = rolePerspectiveByRole.get(role.id);
96
106
  return {
@@ -217,7 +227,10 @@ async function POST(req, ctx) {
217
227
  name: parsed.data.name,
218
228
  settings: parsed.data.settings,
219
229
  setDefault: parsed.data.setRoleDefault ?? false
220
- }
230
+ },
231
+ expectedUpdatedAtByRoleId: parsed.data.roleExpectedUpdatedAtByRoleId,
232
+ expectedUpdatedAtByPerspectiveId: parsed.data.roleExpectedUpdatedAtByPerspectiveId,
233
+ request: req
221
234
  });
222
235
  }
223
236
  },
@@ -227,7 +240,10 @@ async function POST(req, ctx) {
227
240
  tableId,
228
241
  tenantId: auth.tenantId ?? null,
229
242
  organizationId: auth.orgId ?? null,
230
- roleIds: clearRoleIds
243
+ roleIds: clearRoleIds,
244
+ expectedUpdatedAtByRoleId: parsed.data.roleExpectedUpdatedAtByRoleId,
245
+ expectedUpdatedAtByPerspectiveId: parsed.data.roleExpectedUpdatedAtByPerspectiveId,
246
+ request: req
231
247
  });
232
248
  }
233
249
  }
@@ -301,7 +317,8 @@ const perspectivesPostDoc = {
301
317
  errors: [
302
318
  { status: 400, description: "Validation failed or invalid roles provided", schema: perspectivesErrorSchema },
303
319
  { status: 401, description: "Authentication required", schema: perspectivesErrorSchema },
304
- { status: 403, description: "Missing perspectives.role_defaults feature for role updates", schema: perspectivesErrorSchema }
320
+ { status: 403, description: "Missing perspectives.role_defaults feature for role updates", schema: perspectivesErrorSchema },
321
+ { status: 409, description: "Optimistic lock conflict", schema: perspectivesErrorSchema }
305
322
  ]
306
323
  };
307
324
  const openApi = {