134 lines
12 KiB
JavaScript
134 lines
12 KiB
JavaScript
import React, { useState } from 'react'
|
|
import { router, usePage } from '@inertiajs/react'
|
|
import StudioLayout from '../../Layouts/StudioLayout'
|
|
import { studioSurface, trackStudioEvent } from '../../utils/studioEvents'
|
|
|
|
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
|
|
}
|
|
|
|
function formatDate(value) {
|
|
if (!value) return 'Not scheduled'
|
|
const date = new Date(value)
|
|
if (Number.isNaN(date.getTime())) return 'Not scheduled'
|
|
return date.toLocaleString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' })
|
|
}
|
|
|
|
export default function StudioCalendar() {
|
|
const { props } = usePage()
|
|
const calendar = props.calendar || {}
|
|
const filters = calendar.filters || {}
|
|
const summary = calendar.summary || {}
|
|
const [busyKey, setBusyKey] = useState(null)
|
|
|
|
const updateFilters = (patch) => {
|
|
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 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)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<StudioLayout title={props.title} subtitle={props.description}>
|
|
<div className="space-y-6">
|
|
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
|
<div className="rounded-[24px] border border-white/10 bg-white/[0.03] p-5"><div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Scheduled</div><div className="mt-2 text-3xl font-semibold text-white">{Number(summary.scheduled_total || 0).toLocaleString()}</div></div>
|
|
<div className="rounded-[24px] border border-white/10 bg-white/[0.03] p-5"><div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Unscheduled</div><div className="mt-2 text-3xl font-semibold text-white">{Number(summary.unscheduled_total || 0).toLocaleString()}</div></div>
|
|
<div className="rounded-[24px] border border-white/10 bg-white/[0.03] p-5"><div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Overloaded days</div><div className="mt-2 text-3xl font-semibold text-white">{Number(summary.overloaded_days || 0).toLocaleString()}</div></div>
|
|
<div className="rounded-[24px] border border-white/10 bg-white/[0.03] p-5"><div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Next publish</div><div className="mt-2 text-base font-semibold text-white">{formatDate(summary.next_publish_at)}</div></div>
|
|
</section>
|
|
|
|
<section className="rounded-[30px] border border-white/10 bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.14),_transparent_35%),linear-gradient(135deg,_rgba(15,23,42,0.86),_rgba(2,6,23,0.96))] p-5 lg:p-6">
|
|
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-5">
|
|
<label className="space-y-2 text-sm text-slate-300 xl:col-span-2">
|
|
<span className="block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Search planning queue</span>
|
|
<input value={filters.q || ''} onChange={(event) => updateFilters({ q: event.target.value })} className="w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white" placeholder="Title or module" />
|
|
</label>
|
|
<label className="space-y-2 text-sm text-slate-300"><span className="block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">View</span><select value={filters.view || 'month'} onChange={(event) => updateFilters({ view: event.target.value })} className="w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white">{(calendar.view_options || []).map((option) => <option key={option.value} value={option.value} className="bg-slate-900">{option.label}</option>)}</select></label>
|
|
<label className="space-y-2 text-sm text-slate-300"><span className="block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Module</span><select value={filters.module || 'all'} onChange={(event) => updateFilters({ module: event.target.value })} className="w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white">{(calendar.module_options || []).map((option) => <option key={option.value} value={option.value} className="bg-slate-900">{option.label}</option>)}</select></label>
|
|
<label className="space-y-2 text-sm text-slate-300"><span className="block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Queue</span><select value={filters.status || 'scheduled'} onChange={(event) => updateFilters({ status: event.target.value })} className="w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white">{(calendar.status_options || []).map((option) => <option key={option.value} value={option.value} className="bg-slate-900">{option.label}</option>)}</select></label>
|
|
</div>
|
|
</section>
|
|
|
|
<div className="grid gap-6 xl:grid-cols-[minmax(0,1fr)_340px]">
|
|
<section className="rounded-[30px] border border-white/10 bg-white/[0.03] p-5">
|
|
{filters.view === 'week' ? (
|
|
<>
|
|
<h2 className="text-lg font-semibold text-white">{calendar.week?.label}</h2>
|
|
<div className="mt-4 grid gap-3 md:grid-cols-2 xl:grid-cols-7">
|
|
{(calendar.week?.days || []).map((day) => (
|
|
<div key={day.date} className="rounded-[22px] border border-white/10 bg-black/20 p-3">
|
|
<div className="text-sm font-semibold text-white">{day.label}</div>
|
|
<div className="mt-3 space-y-2">{day.items.length > 0 ? day.items.map((item) => <a key={item.id} href={item.edit_url || item.manage_url} className="block rounded-2xl border border-white/10 px-3 py-2 text-xs text-slate-200">{item.title}</a>) : <div className="text-xs text-slate-500">No scheduled items</div>}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</>
|
|
) : filters.view === 'agenda' ? (
|
|
<>
|
|
<h2 className="text-lg font-semibold text-white">Agenda</h2>
|
|
<div className="mt-4 space-y-4">{(calendar.agenda || []).map((group) => <div key={group.date} className="rounded-[22px] border border-white/10 bg-black/20 p-4"><div className="flex items-center justify-between gap-3"><div className="text-base font-semibold text-white">{group.label}</div><div className="text-xs uppercase tracking-[0.18em] text-slate-500">{group.count} items</div></div><div className="mt-3 space-y-2">{group.items.map((item) => <a key={item.id} href={item.edit_url || item.manage_url} className="flex items-center justify-between gap-3 rounded-2xl border border-white/10 px-3 py-2 text-sm text-slate-200"><span>{item.title}</span><span className="text-xs text-slate-500">{formatDate(item.scheduled_at)}</span></a>)}</div></div>)}</div>
|
|
</>
|
|
) : (
|
|
<>
|
|
<h2 className="text-lg font-semibold text-white">{calendar.month?.label}</h2>
|
|
<div className="mt-4 grid grid-cols-7 gap-2 text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">{['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map((label) => <div key={label} className="px-2 py-1">{label}</div>)}</div>
|
|
<div className="mt-2 grid grid-cols-7 gap-2">{(calendar.month?.days || []).map((day) => <div key={day.date} className={`min-h-[120px] rounded-[22px] border p-3 ${day.is_current_month ? 'border-white/10 bg-black/20' : 'border-white/5 bg-black/10'}`}><div className="flex items-center justify-between gap-2"><span className={`text-sm font-semibold ${day.is_current_month ? 'text-white' : 'text-slate-500'}`}>{day.day}</span><span className="text-[10px] uppercase tracking-[0.18em] text-slate-500">{day.count}</span></div><div className="mt-3 space-y-2">{day.items.map((item) => <a key={item.id} href={item.edit_url || item.manage_url} className="block rounded-xl border border-white/10 px-2 py-1.5 text-[11px] text-slate-200">{item.title}</a>)}</div></div>)}</div>
|
|
</>
|
|
)}
|
|
</section>
|
|
|
|
<aside className="space-y-6">
|
|
<section className="rounded-[30px] border border-white/10 bg-white/[0.03] p-5">
|
|
<div className="flex items-center justify-between"><h2 className="text-lg font-semibold text-white">Coverage gaps</h2><a href="/studio/drafts" className="text-sm font-medium text-sky-100">Open drafts</a></div>
|
|
<div className="mt-4 space-y-3">{(calendar.gaps || []).length > 0 ? (calendar.gaps || []).map((gap) => <div key={gap.date} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-slate-200">{gap.label}</div>) : <div className="rounded-2xl border border-dashed border-white/15 px-4 py-8 text-sm text-slate-500">No empty days in the next two weeks.</div>}</div>
|
|
</section>
|
|
|
|
<section className="rounded-[30px] border border-white/10 bg-white/[0.03] p-5">
|
|
<div className="flex items-center justify-between"><h2 className="text-lg font-semibold text-white">Unscheduled queue</h2><span className="text-xs uppercase tracking-[0.18em] text-slate-500">{(calendar.unscheduled_items || []).length}</span></div>
|
|
<div className="mt-4 space-y-3">{(calendar.unscheduled_items || []).map((item) => <a key={item.id} href={item.edit_url || item.manage_url} className="block rounded-2xl border border-white/10 bg-black/20 p-4"><div className="text-sm font-semibold text-white">{item.title}</div><div className="mt-1 text-xs text-slate-500">{item.module_label} · {item.workflow?.readiness?.label || 'Needs review'}</div></a>)}</div>
|
|
</section>
|
|
|
|
<section className="rounded-[30px] border border-white/10 bg-white/[0.03] p-5">
|
|
<div className="flex items-center justify-between"><h2 className="text-lg font-semibold text-white">Upcoming actions</h2><a href="/studio/scheduled" className="text-sm font-medium text-sky-100">Open list</a></div>
|
|
<div className="mt-4 space-y-3">{(calendar.scheduled_items || []).slice(0, 5).map((item) => <div key={item.id} className="rounded-2xl border border-white/10 bg-black/20 p-4"><div className="text-sm font-semibold text-white">{item.title}</div><div className="mt-1 text-xs text-slate-500">{formatDate(item.scheduled_at)}</div><div className="mt-3 flex flex-wrap gap-2"><button type="button" disabled={busyKey === `publish:${item.id}`} onClick={() => runAction(props.endpoints.publishNowPattern, item, 'publish')} className="rounded-full border border-sky-300/20 bg-sky-300/10 px-3 py-1.5 text-xs text-sky-100 disabled:opacity-50">Publish now</button><button type="button" disabled={busyKey === `unschedule:${item.id}`} onClick={() => runAction(props.endpoints.unschedulePattern, item, 'unschedule')} className="rounded-full border border-white/10 px-3 py-1.5 text-xs text-slate-200 disabled:opacity-50">Unschedule</button></div></div>)}</div>
|
|
</section>
|
|
</aside>
|
|
</div>
|
|
</div>
|
|
</StudioLayout>
|
|
)
|
|
} |