@@ -459,6 +459,26 @@ export async function removeFootagePlate(plateId: string) {
|
|||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function deleteShot(shotId: string) {
|
||||||
|
const session = await auth();
|
||||||
|
if (!session?.user) throw new Error("Unauthorized");
|
||||||
|
|
||||||
|
const role = session.user.role as string;
|
||||||
|
if (!["ADMIN", "PRODUCER", "SUPERVISOR"].includes(role)) throw new Error("Insufficient permissions");
|
||||||
|
|
||||||
|
const shot = await db.shot.findUnique({
|
||||||
|
where: { id: shotId },
|
||||||
|
select: { projectId: true },
|
||||||
|
});
|
||||||
|
if (!shot) throw new Error("Shot not found");
|
||||||
|
|
||||||
|
await db.shot.delete({ where: { id: shotId } });
|
||||||
|
|
||||||
|
revalidatePath(`/projects/${shot.projectId}`);
|
||||||
|
|
||||||
|
return { success: true, projectId: shot.projectId };
|
||||||
|
}
|
||||||
|
|
||||||
export async function renameFootagePlate(plateId: string, label: string) {
|
export async function renameFootagePlate(plateId: string, label: string) {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
if (!session?.user) throw new Error("Unauthorized");
|
if (!session?.user) throw new Error("Unauthorized");
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useRef } from "react";
|
import { useState, useRef } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
@@ -17,9 +18,9 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { updateShot } from "@/actions/shots";
|
import { updateShot, deleteShot } from "@/actions/shots";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
import { Upload, X, Film, ImageIcon } from "lucide-react";
|
import { Upload, X, Film, ImageIcon, Trash2 } from "lucide-react";
|
||||||
|
|
||||||
const settingsSchema = z.object({
|
const settingsSchema = z.object({
|
||||||
shotCode: z.string().min(1, "Required").max(120).regex(/^[A-Z0-9_]+$/i, "Alphanumeric and underscores only"),
|
shotCode: z.string().min(1, "Required").max(120).regex(/^[A-Z0-9_]+$/i, "Alphanumeric and underscores only"),
|
||||||
@@ -54,14 +55,18 @@ interface ShotSettingsTabProps {
|
|||||||
dueDate: Date | string | null;
|
dueDate: Date | string | null;
|
||||||
artistId: string | null;
|
artistId: string | null;
|
||||||
thumbnailUrl: string | null;
|
thumbnailUrl: string | null;
|
||||||
|
projectId: string;
|
||||||
};
|
};
|
||||||
artists: Artist[];
|
artists: Artist[];
|
||||||
onSaved?: () => void;
|
onSaved?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ShotSettingsTab({ shot, artists, onSaved }: ShotSettingsTabProps) {
|
export function ShotSettingsTab({ shot, artists, onSaved }: ShotSettingsTabProps) {
|
||||||
|
const router = useRouter();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
const [confirmDelete, setConfirmDelete] = useState(false);
|
||||||
const [thumbnailFile, setThumbnailFile] = useState<File | null>(null);
|
const [thumbnailFile, setThumbnailFile] = useState<File | null>(null);
|
||||||
const [thumbnailPreview, setThumbnailPreview] = useState<string | null>(shot.thumbnailUrl ?? null);
|
const [thumbnailPreview, setThumbnailPreview] = useState<string | null>(shot.thumbnailUrl ?? null);
|
||||||
const [clearThumbnail, setClearThumbnail] = useState(false);
|
const [clearThumbnail, setClearThumbnail] = useState(false);
|
||||||
@@ -142,6 +147,19 @@ export function ShotSettingsTab({ shot, artists, onSaved }: ShotSettingsTabProps
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
setIsDeleting(true);
|
||||||
|
try {
|
||||||
|
await deleteShot(shot.id);
|
||||||
|
toast({ title: "Shot deleted" });
|
||||||
|
router.push(`/projects/${shot.projectId}`);
|
||||||
|
} catch (e) {
|
||||||
|
toast({ title: "Failed to delete shot", description: e instanceof Error ? e.message : undefined, variant: "destructive" });
|
||||||
|
setIsDeleting(false);
|
||||||
|
setConfirmDelete(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-10 max-w-2xl">
|
<div className="space-y-10 max-w-2xl">
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-8">
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-8">
|
||||||
@@ -302,6 +320,49 @@ export function ShotSettingsTab({ shot, artists, onSaved }: ShotSettingsTabProps
|
|||||||
{isSaving ? "Saving…" : "Save Changes"}
|
{isSaving ? "Saving…" : "Save Changes"}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
{/* Danger Zone */}
|
||||||
|
<div className="space-y-3 pt-2">
|
||||||
|
<div className="flex items-center gap-2 text-sm font-semibold text-red-500">
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
Danger Zone
|
||||||
|
</div>
|
||||||
|
<Separator className="bg-red-900/30" />
|
||||||
|
{confirmDelete ? (
|
||||||
|
<div className="rounded-lg border border-red-900/50 bg-red-950/20 p-4 space-y-3">
|
||||||
|
<p className="text-sm text-zinc-300">
|
||||||
|
This will permanently delete <span className="font-mono font-semibold text-white">{shot.shotCode}</span> including all tasks, versions, footage, and annotations. This cannot be undone.
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
disabled={isDeleting}
|
||||||
|
onClick={handleDelete}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3.5 w-3.5 mr-1.5" />
|
||||||
|
{isDeleting ? "Deleting\u2026" : "Yes, delete shot"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
disabled={isDeleting}
|
||||||
|
onClick={() => setConfirmDelete(false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="border-red-900/50 text-red-400 hover:bg-red-950/30 hover:text-red-300 hover:border-red-800"
|
||||||
|
onClick={() => setConfirmDelete(true)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3.5 w-3.5 mr-1.5" />
|
||||||
|
Delete Shot
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div> </div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user