"use client"; import { useEffect, useState, useTransition } from "react"; import { toast } from "sonner"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; import { createFridaySideOp, updateFridaySideOp, uploadSideOpModlist, type FridaySideOpInput, } from "@/app/(frontend)/intelligence/missions/actions"; import { uploadUgcImage } from "./create"; import { SideOpNarrativeFields, type SideOpNarrative, } from "@/components/frontend/intelligence/SideOpNarrativeFields"; import type { LexicalJson } from "./LexicalRichTextEditor"; const MISSION_TYPES = ["Zeus", "PvE", "PvP", "PvPvE", "Training", "Special"] as const; const DEFAULT_BRIEFING_SECTIONS = ["Situation", "Mission", "Execution", "Signals"]; function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function hasLexicalText(value: unknown): boolean { if (!isRecord(value)) return false; if (typeof value.text === "string" && value.text.trim() !== "") return true; return Array.isArray(value.children) && value.children.some(hasLexicalText); } function isEmptyLexical(value: LexicalJson | null): boolean { return value === null || !hasLexicalText(value); } function defaultNarrative(): SideOpNarrative { return { briefing: DEFAULT_BRIEFING_SECTIONS.map((sectionName) => ({ sectionName, details: null })), missionObjectives: [], civilianPresence: "None", radioPlan: null, insertion: [], extraction: [], assetsAvailable: [], }; } export interface SideOpDialogOption { id: number; name: string; disabled?: boolean; } export interface SideOpDialogInitial { missionId: number; name: string; codeName?: string | null; summary: string; coverImageId?: number | null; coverImageUrl?: string | null; mapId: number | null; missionType: string; startDateTime: string; estimatedDuration: number; maxPlayers: number; campaignId: number | null; requiredModsId?: number | null; briefing?: { sectionName: string; details: LexicalJson | null }[]; missionObjectives?: { title: string; description: string; missionCritical: boolean; successOrFailConditions: string; redacted: boolean; }[]; civilianPresence?: string; radioPlan?: LexicalJson | null; insertion?: string[]; extraction?: string[]; assetsAvailable?: { assetType: string; assetCount: string; doesRespawn: boolean; assetCallsign: string; }[]; } interface CreateSideOpDialogProps { open: boolean; onOpenChange: (open: boolean) => void; maps: SideOpDialogOption[]; campaigns: SideOpDialogOption[]; modLists: SideOpDialogOption[]; defaultStartDateTime?: string; initial?: SideOpDialogInitial; onSaved?: () => void; onModlistUploaded?: () => void; } export function CreateSideOpDialog({ open, onOpenChange, maps, campaigns, modLists, defaultStartDateTime, initial, onSaved, onModlistUploaded, }: CreateSideOpDialogProps) { const isEdit = initial !== undefined; const [name, setName] = useState(initial?.name ?? ""); const [codeName, setCodeName] = useState(initial?.codeName ?? ""); const [summary, setSummary] = useState(initial?.summary ?? ""); const [mapId, setMapId] = useState(initial?.mapId ? String(initial.mapId) : ""); const [mapChoice, setMapChoice] = useState( initial?.mapId ? String(initial.mapId) : "existing", ); const [customMapName, setCustomMapName] = useState(""); const [missionType, setMissionType] = useState(initial?.missionType ?? "Zeus"); const [requiredModsId, setRequiredModsId] = useState( initial?.requiredModsId ? String(initial.requiredModsId) : "none", ); const [modlistName, setModlistName] = useState(""); const [modlistUploading, setModlistUploading] = useState(false); const [estimatedDuration, setEstimatedDuration] = useState( String(initial?.estimatedDuration ?? 240), ); const [maxPlayers, setMaxPlayers] = useState(String(initial?.maxPlayers ?? 32)); const [campaignChoice, setCampaignChoice] = useState( initial?.campaignId ? String(initial.campaignId) : "none", ); const [newCampaignName, setNewCampaignName] = useState(""); const [narrative, setNarrative] = useState(defaultNarrative); const [narrativeOpen, setNarrativeOpen] = useState(false); const [coverImage, setCoverImage] = useState<{ id: number; url: string } | null>(null); const [coverImageUploading, setCoverImageUploading] = useState(false); const [error, setError] = useState(null); const [isPending, startTransition] = useTransition(); useEffect(() => { if (open) { setError(null); setNarrativeOpen(false); if (initial) { setName(initial.name); setCodeName(initial.codeName ?? ""); setSummary(initial.summary); setCoverImage( initial.coverImageId != null && initial.coverImageUrl ? { id: initial.coverImageId, url: initial.coverImageUrl } : null, ); setMapId(initial.mapId ? String(initial.mapId) : ""); setMapChoice(initial.mapId ? String(initial.mapId) : "custom"); setCustomMapName(""); setMissionType(initial.missionType); setRequiredModsId(initial.requiredModsId ? String(initial.requiredModsId) : "none"); setModlistName(""); setEstimatedDuration(String(initial.estimatedDuration)); setMaxPlayers(String(initial.maxPlayers)); setCampaignChoice(initial.campaignId ? String(initial.campaignId) : "none"); setNarrative({ briefing: initial.briefing ?? [], missionObjectives: initial.missionObjectives ?? [], civilianPresence: initial.civilianPresence ?? "None", radioPlan: initial.radioPlan ?? null, insertion: initial.insertion ?? [], extraction: initial.extraction ?? [], assetsAvailable: initial.assetsAvailable ?? [], }); } else { setCoverImage(null); setNarrative(defaultNarrative()); } } }, [open, initial]); function narrativeInput() { if (!narrativeOpen) { return {}; } return { briefing: narrative.briefing .filter((e) => e.sectionName.trim() !== "" || !isEmptyLexical(e.details)) .map((e) => ({ sectionName: e.sectionName.trim() || "Untitled", details: e.details })), missionObjectives: narrative.missionObjectives .filter((o) => o.title.trim() !== "") .map((o) => ({ title: o.title.trim(), description: o.description || null, missionCritical: o.missionCritical, successOrFailConditions: o.successOrFailConditions || null, redacted: o.redacted, })), civilianPresence: narrative.civilianPresence as "None" | "Low" | "Medium" | "High", radioPlan: narrative.radioPlan, insertion: narrative.insertion as FridaySideOpInput["insertion"], extraction: narrative.extraction as FridaySideOpInput["extraction"], assetsAvailable: narrative.assetsAvailable.map((a) => ({ assetType: a.assetType.trim(), assetCount: a.assetCount || null, doesRespawn: a.doesRespawn, assetCallsign: a.assetCallsign || null, })), }; } function mapInput() { return mapChoice === "custom" && customMapName.trim() !== "" ? { customMapName } : mapId ? { mapId: Number(mapId), customMapName: null } : { customMapName: null }; } function modsInput() { return requiredModsId === "none" ? { requiredModsId: null } : { requiredModsId: Number(requiredModsId) }; } async function handleModlistUpload(file: File) { setModlistUploading(true); try { const formData = new FormData(); formData.append("file", file); formData.append("name", modlistName.trim() || file.name.replace(/\.html?$/i, "")); const result = await uploadSideOpModlist(formData); if (!result.success || !result.data) { toast.error(result.error ?? "Failed to upload the mod list."); return; } setRequiredModsId(String(result.data.modlistId)); toast.success(`Mod list "${result.data.modlistName}" uploaded.`); onModlistUploaded?.(); } finally { setModlistUploading(false); } } async function handleCoverImageUpload(file: File) { setCoverImageUploading(true); try { const formData = new FormData(); formData.append("file", file); const result = await uploadUgcImage(formData); if (!result.success || !result.data) { toast.error(result.error ?? "Failed to upload the image."); return; } setCoverImage({ id: result.data.id, url: result.data.url }); toast.success("Cover image uploaded."); } finally { setCoverImageUploading(false); } } function handleSubmit() { setError(null); if (!name.trim() || !summary.trim()) { setError("Name and summary are required."); return; } if (mapChoice === "custom" && !customMapName.trim()) { setError("Please enter a name for the custom map."); return; } if (mapChoice !== "custom" && !mapId) { setError("Please choose a map."); return; } if (!isEdit && !defaultStartDateTime) { setError("Missing start date: schedule from a Friday on the calendar."); return; } const startDateIso = defaultStartDateTime ?? ""; startTransition(async () => { if (isEdit) { const initialChoice = initial.campaignId ? String(initial.campaignId) : "none"; const campaignChanged = campaignChoice !== initialChoice; const result = await updateFridaySideOp({ missionId: initial.missionId, name, codeName: codeName.trim(), summary, ...(coverImage && coverImage.id !== (initial.coverImageId ?? null) ? { coverImageId: coverImage.id } : {}), ...mapInput(), missionType: missionType as FridaySideOpInput["missionType"], estimatedDuration: Number(estimatedDuration) || 240, maxPlayers: Number(maxPlayers) || 32, ...modsInput(), ...(campaignChanged ? campaignChoice === "none" ? { noCampaign: true } : campaignChoice === "new" ? { newCampaignName: newCampaignName || null } : { campaignId: Number(campaignChoice) } : {}), ...narrativeInput(), }); if (!result.success) { setError(result.error ?? "Failed to update the side operation."); return; } toast.success("Side operation updated."); onOpenChange(false); onSaved?.(); return; } const result = await createFridaySideOp({ name, codeName: codeName.trim() || undefined, summary, ...(coverImage ? { coverImageId: coverImage.id } : {}), ...mapInput(), missionType: missionType as FridaySideOpInput["missionType"], startDateTime: startDateIso, estimatedDuration: Number(estimatedDuration) || 240, maxPlayers: Number(maxPlayers) || 32, ...modsInput(), campaignId: campaignChoice === "new" || campaignChoice === "none" ? null : Number(campaignChoice), newCampaignName: campaignChoice === "new" ? newCampaignName || null : null, noCampaign: campaignChoice === "none", ...narrativeInput(), }); if (!result.success) { setError(result.error ?? "Failed to schedule the side operation."); return; } toast.success("Side operation scheduled."); onOpenChange(false); onSaved?.(); }); } return ( {isEdit ? "Edit Side Operation" : "Schedule Side Operation"} {isEdit ? "Update the details of this Friday side operation." : "Player-run Friday side operations include Discord roll-call and RSVP automatically."}
setName(e.target.value)} placeholder="Operation Broken Arrow" />
setCodeName(e.target.value)} placeholder="OP:BRKARW" />