@@ -55,6 +55,7 @@ type ShotRow = {
|
||||
description: string | null;
|
||||
isKeyShot: boolean;
|
||||
artist: { id: string; name: string | null; image: string | null; email: string } | null;
|
||||
shotGroup: { id: string; name: string } | null;
|
||||
};
|
||||
|
||||
interface Project {
|
||||
@@ -491,7 +492,8 @@ export function ShotStatusClient({
|
||||
|
||||
const [expandedEpisodes, setExpandedEpisodes] = useState<Set<string>>(new Set());
|
||||
const [expandedScenes, setExpandedScenes] = useState<Set<string>>(new Set());
|
||||
const [groupByScene, setGroupByScene] = useState(false);
|
||||
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
|
||||
const [groupMode, setGroupMode] = useState<"flat" | "scene" | "group">("flat");
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||
|
||||
// Load saved episode expand state when project changes
|
||||
@@ -504,10 +506,10 @@ export function ShotStatusClient({
|
||||
setExpandedEpisodes(new Set());
|
||||
}
|
||||
try {
|
||||
const savedGroup = localStorage.getItem(`shotStatus:${selectedProjectId}:groupByScene`);
|
||||
setGroupByScene(savedGroup === "1");
|
||||
const savedGroup = localStorage.getItem(`shotStatus:${selectedProjectId}:groupMode`);
|
||||
setGroupMode((savedGroup as "flat" | "scene" | "group") ?? "flat");
|
||||
} catch {
|
||||
setGroupByScene(false);
|
||||
setGroupMode("flat");
|
||||
}
|
||||
}, [selectedProjectId]);
|
||||
const [dueDateDialogOpen, setDueDateDialogOpen] = useState(false);
|
||||
@@ -531,11 +533,19 @@ export function ShotStatusClient({
|
||||
return next;
|
||||
});
|
||||
|
||||
const handleGroupByScene = (checked: boolean) => {
|
||||
setGroupByScene(checked);
|
||||
if (checked) setExpandedScenes(new Set(shots.map((s) => s.scene)));
|
||||
const toggleGroup = (g: string) =>
|
||||
setExpandedGroups((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.has(g) ? next.delete(g) : next.add(g);
|
||||
return next;
|
||||
});
|
||||
|
||||
const handleGroupMode = (mode: "flat" | "scene" | "group") => {
|
||||
setGroupMode(mode);
|
||||
if (mode === "scene") setExpandedScenes(new Set(shots.map((s) => s.scene)));
|
||||
if (mode === "group") setExpandedGroups(new Set(shots.map((s) => s.shotGroup?.id ?? "")));
|
||||
if (selectedProjectId) {
|
||||
try { localStorage.setItem(`shotStatus:${selectedProjectId}:groupByScene`, checked ? "1" : "0"); } catch {}
|
||||
try { localStorage.setItem(`shotStatus:${selectedProjectId}:groupMode`, mode); } catch {}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -590,7 +600,7 @@ export function ShotStatusClient({
|
||||
})()
|
||||
: [];
|
||||
|
||||
const sceneGroups: [string, ShotRow[]][] = !isEpisodic && groupByScene
|
||||
const sceneGroups: [string, ShotRow[]][] = !isEpisodic && groupMode === "scene"
|
||||
? (() => {
|
||||
const map = new Map<string, ShotRow[]>();
|
||||
for (const shot of shots) {
|
||||
@@ -602,6 +612,19 @@ export function ShotStatusClient({
|
||||
})()
|
||||
: [];
|
||||
|
||||
const shotGroupGroups: [string, string, ShotRow[]][] = !isEpisodic && groupMode === "group"
|
||||
? (() => {
|
||||
const map = new Map<string, [string, ShotRow[]]>();
|
||||
for (const shot of shots) {
|
||||
const id = shot.shotGroup?.id ?? "";
|
||||
const name = shot.shotGroup?.name ?? "(No Group)";
|
||||
if (!map.has(id)) map.set(id, [name, []]);
|
||||
map.get(id)![1].push(shot);
|
||||
}
|
||||
return Array.from(map.entries()).map(([id, [name, s]]) => [id, name, s] as [string, string, ShotRow[]]);
|
||||
})()
|
||||
: [];
|
||||
|
||||
const episodeDueDateMap = new Map(
|
||||
episodeDueDates.map((e) => [e.episode, new Date(e.dueDate)])
|
||||
);
|
||||
@@ -641,15 +664,23 @@ export function ShotStatusClient({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{selectedProject && !isEpisodic && (
|
||||
<label className="flex items-center gap-2 cursor-pointer select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={groupByScene}
|
||||
onChange={(e) => handleGroupByScene(e.target.checked)}
|
||||
className="cursor-pointer accent-blue-500 w-3.5 h-3.5"
|
||||
/>
|
||||
<span className="text-sm text-zinc-400">Group by scene</span>
|
||||
</label>
|
||||
<>
|
||||
<span className="text-sm text-zinc-500">Group by:</span>
|
||||
{(["flat", "scene", "group"] as const).map((mode) => (
|
||||
<button
|
||||
key={mode}
|
||||
onClick={() => handleGroupMode(mode)}
|
||||
className={cn(
|
||||
"px-3 py-1 rounded text-xs font-medium transition-colors",
|
||||
groupMode === mode
|
||||
? "bg-zinc-700 text-zinc-100"
|
||||
: "text-zinc-500 hover:text-zinc-300"
|
||||
)}
|
||||
>
|
||||
{mode === "flat" ? "None" : mode === "scene" ? "Scene" : "Group"}
|
||||
</button>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -773,7 +804,7 @@ export function ShotStatusClient({
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : groupByScene ? (
|
||||
) : groupMode === "scene" ? (
|
||||
<div className="divide-y divide-zinc-800">
|
||||
{sceneGroups.map(([scene, sceneShots]) => {
|
||||
const collapsed = !expandedScenes.has(scene);
|
||||
@@ -822,6 +853,55 @@ export function ShotStatusClient({
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : groupMode === "group" ? (
|
||||
<div className="divide-y divide-zinc-800">
|
||||
{shotGroupGroups.map(([groupId, groupName, groupShots]) => {
|
||||
const collapsed = !expandedGroups.has(groupId);
|
||||
const groupIds = groupShots.map((s) => s.id);
|
||||
const allGrSelected = groupIds.every((id) => selectedIds.has(id));
|
||||
const someGrSelected = groupIds.some((id) => selectedIds.has(id));
|
||||
return (
|
||||
<div key={groupId}>
|
||||
<div className="flex items-center gap-2 px-4 py-3 bg-zinc-800/60 hover:bg-zinc-800/90 transition-colors">
|
||||
{canManage && (
|
||||
<IndeterminateCheckbox
|
||||
checked={allGrSelected}
|
||||
indeterminate={someGrSelected && !allGrSelected}
|
||||
onChange={(e) => toggleMany(groupIds, e.target.checked)}
|
||||
className="shrink-0"
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
onClick={() => toggleGroup(groupId)}
|
||||
className="flex items-center gap-2 flex-1 text-left"
|
||||
>
|
||||
{collapsed ? (
|
||||
<ChevronRight className="h-4 w-4 text-zinc-400 shrink-0" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4 text-zinc-400 shrink-0" />
|
||||
)}
|
||||
<span className="font-semibold text-sm text-white">{groupName}</span>
|
||||
<span className="text-xs text-zinc-500 font-normal">
|
||||
{groupShots.length} shot{groupShots.length !== 1 ? "s" : ""}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<div className="overflow-x-auto">
|
||||
<ShotTable
|
||||
shots={groupShots}
|
||||
canManage={canManage}
|
||||
projectId={selectedProjectId!}
|
||||
selectedIds={selectedIds}
|
||||
onToggle={toggleShot}
|
||||
onToggleAll={toggleMany}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<ShotTable
|
||||
|
||||
Reference in New Issue
Block a user