import React, { useEffect, useState } from 'react' import { router, usePage } from '@inertiajs/react' import StudioLayout from '../../Layouts/StudioLayout' import { studioSurface, trackStudioEvent } from '../../utils/studioEvents' import { formatReleaseCountdown, formatScheduledDate } from '../../utils/scheduleCountdown' import NovaSelect from '../../components/ui/NovaSelect' function parseFocusDate(value) { if (!value) return new Date() const parsed = new Date(`${value}T12:00:00`) return Number.isNaN(parsed.getTime()) ? new Date() : parsed } function toFocusDateValue(date) { const year = date.getFullYear() const month = String(date.getMonth() + 1).padStart(2, '0') const day = String(date.getDate()).padStart(2, '0') return `${year}-${month}-${day}` } function shiftFocusDate(value, view, direction) { const next = new Date(parseFocusDate(value)) if (view === 'week') { next.setDate(next.getDate() + (direction * 7)) return toFocusDateValue(next) } const originalDay = next.getDate() next.setDate(1) next.setMonth(next.getMonth() + direction) const lastDayOfMonth = new Date(next.getFullYear(), next.getMonth() + 1, 0).getDate() next.setDate(Math.min(originalDay, lastDayOfMonth)) return toFocusDateValue(next) } function itemHref(item) { return item.edit_url || item.manage_url || item.preview_url || item.view_url || '#' } function CalendarThumb({ item, className = 'h-full w-full', showTime = false }) { return ( {item.image_url ? ( {item.title ) : (
{item.module_label || 'Item'}
)}
{item.title}
{item.module_label} {showTime && item.scheduled_at ? {formatScheduledDate(item.scheduled_at)} : null}
) } function CalendarInlineItem({ item, showTime = true }) { return (
{item.image_url ? ( {item.title ) : (
{String(item.module_label || 'Item').slice(0, 3)}
)}
{item.title}
{item.module_label} {showTime && item.scheduled_at ? {formatScheduledDate(item.scheduled_at)} : null}
) } function CalendarDayModal({ day, busyKey, endpoints, onAction, onClose, nowMs }) { if (!day) return null return (
{(day.detail_items || []).map((item) => (
{item.image_url ? ( {item.title ) : (
{item.module_label || 'Item'}
)}
{item.title}
{item.module_label} {item.scheduled_at ? {formatScheduledDate(item.scheduled_at)} : null}
{formatReleaseCountdown(item.scheduled_at, nowMs)}
{item.subtitle || item.workflow?.readiness?.label || 'Scheduled item'}
Open
))}
) } function CalendarMonthDay({ day, onOpenDetail }) { const items = day.items || [] const overflowCount = Number(day.overflow_count || Math.max(0, Number(day.count || 0) - items.length)) const hasItems = items.length > 0 return (
{day.day} {hasItems ? ( ) : ( {day.count} )}
{hasItems ? (
{items.length === 1 ? ( ) : (
{items.map((item) => ( ))}
)} {overflowCount > 0 ? ( ) : null}
) : (
Quiet day
)}
) } async function requestJson(url, method = 'POST') { const response = await fetch(url, { method, credentials: 'same-origin', headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '', 'X-Requested-With': 'XMLHttpRequest', }, }) const payload = await response.json().catch(() => ({})) if (!response.ok) throw new Error(payload?.message || 'Request failed') return payload } export default function StudioCalendar() { const { props } = usePage() const calendar = props.calendar || {} const filters = calendar.filters || {} const summary = calendar.summary || {} const currentView = filters.view || 'month' const [busyKey, setBusyKey] = useState(null) const [nowMs, setNowMs] = useState(() => Date.now()) const [selectedDay, setSelectedDay] = useState(null) const updateFilters = (patch) => { setSelectedDay(null) const next = { ...filters, ...patch } trackStudioEvent('studio_scheduled_opened', { surface: studioSurface(), module: next.module, meta: patch, }) router.get(window.location.pathname, next, { preserveScroll: true, preserveState: true, replace: true, }) } const shiftCalendar = (direction) => { updateFilters({ focus_date: shiftFocusDate(filters.focus_date, currentView, direction) }) } const resetCalendarFocus = () => { updateFilters({ focus_date: toFocusDateValue(new Date()) }) } const runAction = async (pattern, item, key) => { const url = String(pattern || '').replace('__MODULE__', item.module).replace('__ID__', String(item.numeric_id)) setBusyKey(`${key}:${item.id}`) try { await requestJson(url) router.reload({ preserveScroll: true, preserveState: true }) } catch (error) { window.alert(error?.message || 'Unable to update schedule.') } finally { setBusyKey(null) } } useEffect(() => { const hasTimedEntries = Boolean(summary.next_publish_at) || (calendar.scheduled_items || []).some((item) => Boolean(item.scheduled_at)) if (!hasTimedEntries) return undefined const timer = window.setInterval(() => { setNowMs(Date.now()) }, 1000) return () => window.clearInterval(timer) }, [calendar.scheduled_items, summary.next_publish_at]) useEffect(() => { if (!selectedDay?.date) return const nextSelectedDay = (calendar.month?.days || []).find((day) => day.date === selectedDay.date) || null setSelectedDay(nextSelectedDay) }, [calendar.month?.days, selectedDay?.date]) useEffect(() => { if (!selectedDay) return undefined const handleKeyDown = (event) => { if (event.key === 'Escape') { setSelectedDay(null) } } window.addEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) }, [selectedDay]) return (
Scheduled
{Number(summary.scheduled_total || 0).toLocaleString()}
Unscheduled
{Number(summary.unscheduled_total || 0).toLocaleString()}
Overloaded days
{Number(summary.overloaded_days || 0).toLocaleString()}
Next publish
{formatReleaseCountdown(summary.next_publish_at, nowMs)}
{summary.next_publish_at &&
{formatScheduledDate(summary.next_publish_at)}
}
View updateFilters({ view: val })} options={calendar.view_options || []} searchable={false} />
Module updateFilters({ module: val })} options={calendar.module_options || []} searchable={false} />
Queue updateFilters({ status: val })} options={calendar.status_options || []} searchable={false} />
{filters.view === 'week' ? ( <>

{calendar.week?.label}

Week planning
{(calendar.week?.days || []).map((day) => (
{day.label}
{day.items.length > 0 ? day.items.map((item) => ) :
No scheduled items
}
))}
) : filters.view === 'agenda' ? ( <>

Agenda

{(calendar.agenda || []).map((group) =>
{group.label}
{group.count} items
{group.items.map((item) => )}
)}
) : ( <>

{calendar.month?.label}

Month planning
{['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map((label) =>
{label}
)}
{(calendar.month?.days || []).map((day) => )}
)}
setSelectedDay(null)} nowMs={nowMs} />
) }