import React from 'react' import { Head, usePage } from '@inertiajs/react' import CollectionCard from '../../components/profile/collections/CollectionCard' function getCsrfToken() { if (typeof document === 'undefined') return '' return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '' } async function requestJson(url, { method = 'POST', body } = {}) { const response = await fetch(url, { method, credentials: 'same-origin', headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'X-CSRF-TOKEN': getCsrfToken(), 'X-Requested-With': 'XMLHttpRequest', }, body: body ? JSON.stringify(body) : undefined, }) const payload = await response.json().catch(() => ({})) if (!response.ok) { throw new Error(payload?.message || 'Request failed.') } return payload } function isoToLocalInput(value) { if (!value) return '' const date = new Date(value) if (Number.isNaN(date.getTime())) return '' const local = new Date(date.getTime() - (date.getTimezoneOffset() * 60000)) return local.toISOString().slice(0, 16) } function rulesJsonToText(rulesJson) { if (!rulesJson) return '{\n "campaign_key": "",\n "owner_username": "",\n "presentation_style": "hero_grid",\n "min_quality_score": 80\n}' try { return JSON.stringify(rulesJson, null, 2) } catch { return '{\n "campaign_key": "",\n "owner_username": "",\n "presentation_style": "hero_grid",\n "min_quality_score": 80\n}' } } function Field({ label, help, children }) { return ( ) } export default function CollectionStaffSurfaces() { const { props } = usePage() const collectionOptions = Array.isArray(props.collectionOptions) ? props.collectionOptions : [] const [definitions, setDefinitions] = React.useState(Array.isArray(props.definitions) ? props.definitions : []) const [placements, setPlacements] = React.useState(Array.isArray(props.placements) ? props.placements : []) const [conflicts, setConflicts] = React.useState(Array.isArray(props.conflicts) ? props.conflicts : []) const [definitionForm, setDefinitionForm] = React.useState({ id: null, surface_key: '', title: '', description: '', mode: 'manual', ranking_mode: 'ranking_score', max_items: 12, is_active: true, starts_at: '', ends_at: '', fallback_surface_key: '', rules_json: '{\n "campaign_key": "",\n "owner_username": "",\n "presentation_style": "hero_grid",\n "min_quality_score": 80\n}', }) const [placementForm, setPlacementForm] = React.useState({ id: null, surface_key: props.surfaceKeyOptions?.[0] || '', collection_id: collectionOptions[0]?.id || '', placement_type: 'manual', priority: 0, starts_at: '', ends_at: '', is_active: true, campaign_key: '', notes: '', }) const [batchForm, setBatchForm] = React.useState({ collection_ids: [], campaign_key: '', campaign_label: '', event_label: '', season_key: '', editorial_notes: '', surface_key: props.surfaceKeyOptions?.[0] || '', placement_type: 'campaign', priority: 0, starts_at: '', ends_at: '', is_active: true, notes: '', }) const [batchResult, setBatchResult] = React.useState(null) const [notice, setNotice] = React.useState('') const [busy, setBusy] = React.useState('') const seo = props.seo || {} const surfaceKeyOptions = React.useMemo(() => { const keys = definitions.map((definition) => definition.surface_key).filter(Boolean) return Array.from(new Set(keys)).sort((left, right) => String(left).localeCompare(String(right))) }, [definitions]) const conflictPlacementIds = React.useMemo(() => { return new Set(conflicts.flatMap((conflict) => Array.isArray(conflict.placement_ids) ? conflict.placement_ids : [])) }, [conflicts]) React.useEffect(() => { setPlacementForm((current) => { if (current.surface_key && surfaceKeyOptions.includes(current.surface_key)) { return current } return { ...current, surface_key: surfaceKeyOptions[0] || '', } }) setBatchForm((current) => { if (!current.surface_key || surfaceKeyOptions.includes(current.surface_key)) { return current } return { ...current, surface_key: surfaceKeyOptions[0] || '', } }) }, [surfaceKeyOptions]) function resetDefinitionForm() { setDefinitionForm({ id: null, surface_key: '', title: '', description: '', mode: 'manual', ranking_mode: 'ranking_score', max_items: 12, is_active: true, starts_at: '', ends_at: '', fallback_surface_key: '', rules_json: '{\n "campaign_key": "",\n "owner_username": "",\n "presentation_style": "hero_grid",\n "min_quality_score": 80\n}', }) } function resetPlacementForm() { setPlacementForm({ id: null, surface_key: surfaceKeyOptions[0] || '', collection_id: collectionOptions[0]?.id || '', placement_type: 'manual', priority: 0, starts_at: '', ends_at: '', is_active: true, campaign_key: '', notes: '', }) } function toggleBatchCollection(collectionId) { setBatchForm((current) => { const currentIds = Array.isArray(current.collection_ids) ? current.collection_ids : [] const nextIds = currentIds.includes(collectionId) ? currentIds.filter((id) => id !== collectionId) : [...currentIds, collectionId] return { ...current, collection_ids: nextIds, } }) } async function handleDefinitionSubmit(event) { event.preventDefault() setBusy('definition') setNotice('') try { const rulesJson = definitionForm.rules_json.trim() ? JSON.parse(definitionForm.rules_json) : null const url = definitionForm.id ? props.endpoints?.definitionsUpdatePattern?.replace('__DEFINITION__', String(definitionForm.id)) : props.endpoints?.definitionsStore const payload = await requestJson(url, { method: definitionForm.id ? 'PATCH' : 'POST', body: { ...definitionForm, max_items: Number(definitionForm.max_items || 12), starts_at: definitionForm.starts_at ? new Date(definitionForm.starts_at).toISOString() : null, ends_at: definitionForm.ends_at ? new Date(definitionForm.ends_at).toISOString() : null, fallback_surface_key: definitionForm.fallback_surface_key || null, rules_json: rulesJson, }, }) setDefinitions((current) => { const next = current.filter((definition) => definition.id !== payload.definition.id) return [...next, payload.definition].sort((left, right) => String(left.surface_key).localeCompare(String(right.surface_key))) }) setNotice(definitionForm.id ? 'Surface definition updated.' : 'Surface definition saved.') resetDefinitionForm() } catch (error) { setNotice(error.message || 'Failed to save definition.') } finally { setBusy('') } } async function handlePlacementSubmit(event) { event.preventDefault() setBusy('placement') setNotice('') try { const url = placementForm.id ? props.endpoints?.placementsUpdatePattern?.replace('__PLACEMENT__', String(placementForm.id)) : props.endpoints?.placementsStore const payload = await requestJson(url, { method: placementForm.id ? 'PATCH' : 'POST', body: { ...placementForm, collection_id: Number(placementForm.collection_id), priority: Number(placementForm.priority || 0), starts_at: placementForm.starts_at ? new Date(placementForm.starts_at).toISOString() : null, ends_at: placementForm.ends_at ? new Date(placementForm.ends_at).toISOString() : null, }, }) setPlacements((current) => { const next = current.filter((placement) => placement.id !== payload.placement.id) return [...next, payload.placement].sort((left, right) => { if (left.surface_key === right.surface_key) return (right.priority || 0) - (left.priority || 0) return String(left.surface_key).localeCompare(String(right.surface_key)) }) }) setConflicts(Array.isArray(payload.conflicts) ? payload.conflicts : []) setNotice(placementForm.id ? 'Surface placement updated.' : 'Surface placement saved.') resetPlacementForm() } catch (error) { setNotice(error.message || 'Failed to save placement.') } finally { setBusy('') } } async function handleBatchEditorial(mode) { setBusy(`batch-${mode}`) setNotice('') try { const payload = await requestJson(props.endpoints?.batchEditorial, { method: 'POST', body: { ...batchForm, starts_at: batchForm.starts_at ? new Date(batchForm.starts_at).toISOString() : null, ends_at: batchForm.ends_at ? new Date(batchForm.ends_at).toISOString() : null, collection_ids: (batchForm.collection_ids || []).map((id) => Number(id)), priority: Number(batchForm.priority || 0), surface_key: batchForm.surface_key || null, apply: mode === 'apply', }, }) setBatchResult(payload.plan || null) if (mode === 'apply') { setPlacements(Array.isArray(payload.placements) ? payload.placements : []) setConflicts(Array.isArray(payload.conflicts) ? payload.conflicts : []) setNotice('Batch editorial changes applied.') } else { setNotice('Batch editorial preview generated.') } } catch (error) { setNotice(error.message || 'Batch editorial tools failed.') } finally { setBusy('') } } function hydrateDefinition(definition) { setDefinitionForm({ id: definition.id, surface_key: definition.surface_key || '', title: definition.title || '', description: definition.description || '', mode: definition.mode || 'manual', ranking_mode: definition.ranking_mode || 'ranking_score', max_items: definition.max_items || 12, is_active: definition.is_active !== false, starts_at: isoToLocalInput(definition.starts_at), ends_at: isoToLocalInput(definition.ends_at), fallback_surface_key: definition.fallback_surface_key || '', rules_json: rulesJsonToText(definition.rules_json), }) } function hydratePlacement(placement) { setPlacementForm({ id: placement.id, surface_key: placement.surface_key || '', collection_id: placement.collection?.id || '', placement_type: placement.placement_type || 'manual', priority: placement.priority || 0, starts_at: isoToLocalInput(placement.starts_at), ends_at: isoToLocalInput(placement.ends_at), is_active: placement.is_active !== false, campaign_key: placement.campaign_key || '', notes: placement.notes || '', }) } async function handleDeleteDefinition(definition) { if (!window.confirm(`Delete surface definition "${definition.surface_key}"?`)) return setBusy(`delete-definition-${definition.id}`) setNotice('') try { const url = props.endpoints?.definitionsDeletePattern?.replace('__DEFINITION__', String(definition.id)) await requestJson(url, { method: 'DELETE' }) setDefinitions((current) => current.filter((item) => item.id !== definition.id)) if (definitionForm.id === definition.id) { resetDefinitionForm() } setNotice('Surface definition deleted.') } catch (error) { setNotice(error.message || 'Failed to delete definition.') } finally { setBusy('') } } async function handleDeletePlacement(placement) { if (!window.confirm(`Delete placement for "${placement.collection?.title || 'this collection'}" on ${placement.surface_key}?`)) return setBusy(`delete-placement-${placement.id}`) setNotice('') try { const url = props.endpoints?.placementsDeletePattern?.replace('__PLACEMENT__', String(placement.id)) const payload = await requestJson(url, { method: 'DELETE' }) setPlacements((current) => current.filter((item) => item.id !== placement.id)) setConflicts(Array.isArray(payload.conflicts) ? payload.conflicts : []) if (placementForm.id === placement.id) { resetPlacementForm() } setNotice('Surface placement deleted.') } catch (error) { setNotice(error.message || 'Failed to delete placement.') } finally { setBusy('') } } return ( <> {seo.title || 'Collection Surfaces — Skinbase Nova'} {seo.canonical ? : null}