@@ -490,6 +490,8 @@ export function ShotStatusClient({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const [expandedEpisodes, setExpandedEpisodes] = useState<Set<string>>(new Set());
|
const [expandedEpisodes, setExpandedEpisodes] = useState<Set<string>>(new Set());
|
||||||
|
const [expandedScenes, setExpandedScenes] = useState<Set<string>>(new Set());
|
||||||
|
const [groupByScene, setGroupByScene] = useState(false);
|
||||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
// Load saved episode expand state when project changes
|
// Load saved episode expand state when project changes
|
||||||
@@ -501,6 +503,12 @@ export function ShotStatusClient({
|
|||||||
} catch {
|
} catch {
|
||||||
setExpandedEpisodes(new Set());
|
setExpandedEpisodes(new Set());
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
const savedGroup = localStorage.getItem(`shotStatus:${selectedProjectId}:groupByScene`);
|
||||||
|
setGroupByScene(savedGroup === "1");
|
||||||
|
} catch {
|
||||||
|
setGroupByScene(false);
|
||||||
|
}
|
||||||
}, [selectedProjectId]);
|
}, [selectedProjectId]);
|
||||||
const [dueDateDialogOpen, setDueDateDialogOpen] = useState(false);
|
const [dueDateDialogOpen, setDueDateDialogOpen] = useState(false);
|
||||||
|
|
||||||
@@ -516,6 +524,21 @@ export function ShotStatusClient({
|
|||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const toggleScene = (sc: string) =>
|
||||||
|
setExpandedScenes((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.has(sc) ? next.delete(sc) : next.add(sc);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleGroupByScene = (checked: boolean) => {
|
||||||
|
setGroupByScene(checked);
|
||||||
|
if (checked) setExpandedScenes(new Set(shots.map((s) => s.scene)));
|
||||||
|
if (selectedProjectId) {
|
||||||
|
try { localStorage.setItem(`shotStatus:${selectedProjectId}:groupByScene`, checked ? "1" : "0"); } catch {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleProjectChange = (id: string) => {
|
const handleProjectChange = (id: string) => {
|
||||||
localStorage.setItem("shotStatus:lastProjectId", id);
|
localStorage.setItem("shotStatus:lastProjectId", id);
|
||||||
setSelectedIds(new Set());
|
setSelectedIds(new Set());
|
||||||
@@ -567,6 +590,18 @@ export function ShotStatusClient({
|
|||||||
})()
|
})()
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
const sceneGroups: [string, ShotRow[]][] = !isEpisodic && groupByScene
|
||||||
|
? (() => {
|
||||||
|
const map = new Map<string, ShotRow[]>();
|
||||||
|
for (const shot of shots) {
|
||||||
|
const key = shot.scene || "(No Scene)";
|
||||||
|
if (!map.has(key)) map.set(key, []);
|
||||||
|
map.get(key)!.push(shot);
|
||||||
|
}
|
||||||
|
return Array.from(map.entries());
|
||||||
|
})()
|
||||||
|
: [];
|
||||||
|
|
||||||
const episodeDueDateMap = new Map(
|
const episodeDueDateMap = new Map(
|
||||||
episodeDueDates.map((e) => [e.episode, new Date(e.dueDate)])
|
episodeDueDates.map((e) => [e.episode, new Date(e.dueDate)])
|
||||||
);
|
);
|
||||||
@@ -605,6 +640,17 @@ export function ShotStatusClient({
|
|||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</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>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* No project selected */}
|
{/* No project selected */}
|
||||||
@@ -727,6 +773,55 @@ export function ShotStatusClient({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
) : groupByScene ? (
|
||||||
|
<div className="divide-y divide-zinc-800">
|
||||||
|
{sceneGroups.map(([scene, sceneShots]) => {
|
||||||
|
const collapsed = !expandedScenes.has(scene);
|
||||||
|
const sceneIds = sceneShots.map((s) => s.id);
|
||||||
|
const allScSelected = sceneIds.every((id) => selectedIds.has(id));
|
||||||
|
const someScSelected = sceneIds.some((id) => selectedIds.has(id));
|
||||||
|
return (
|
||||||
|
<div key={scene}>
|
||||||
|
<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={allScSelected}
|
||||||
|
indeterminate={someScSelected && !allScSelected}
|
||||||
|
onChange={(e) => toggleMany(sceneIds, e.target.checked)}
|
||||||
|
className="shrink-0"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={() => toggleScene(scene)}
|
||||||
|
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">Scene {scene}</span>
|
||||||
|
<span className="text-xs text-zinc-500 font-normal">
|
||||||
|
{sceneShots.length} shot{sceneShots.length !== 1 ? "s" : ""}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{!collapsed && (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<ShotTable
|
||||||
|
shots={sceneShots}
|
||||||
|
canManage={canManage}
|
||||||
|
projectId={selectedProjectId!}
|
||||||
|
selectedIds={selectedIds}
|
||||||
|
onToggle={toggleShot}
|
||||||
|
onToggleAll={toggleMany}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<ShotTable
|
<ShotTable
|
||||||
|
|||||||
Reference in New Issue
Block a user