@@ -490,6 +490,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 [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||
|
||||
// Load saved episode expand state when project changes
|
||||
@@ -501,6 +503,12 @@ export function ShotStatusClient({
|
||||
} catch {
|
||||
setExpandedEpisodes(new Set());
|
||||
}
|
||||
try {
|
||||
const savedGroup = localStorage.getItem(`shotStatus:${selectedProjectId}:groupByScene`);
|
||||
setGroupByScene(savedGroup === "1");
|
||||
} catch {
|
||||
setGroupByScene(false);
|
||||
}
|
||||
}, [selectedProjectId]);
|
||||
const [dueDateDialogOpen, setDueDateDialogOpen] = useState(false);
|
||||
|
||||
@@ -516,6 +524,21 @@ export function ShotStatusClient({
|
||||
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) => {
|
||||
localStorage.setItem("shotStatus:lastProjectId", id);
|
||||
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(
|
||||
episodeDueDates.map((e) => [e.episode, new Date(e.dueDate)])
|
||||
);
|
||||
@@ -605,6 +640,17 @@ 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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* No project selected */}
|
||||
@@ -727,6 +773,55 @@ export function ShotStatusClient({
|
||||
);
|
||||
})}
|
||||
</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">
|
||||
<ShotTable
|
||||
|
||||
Reference in New Issue
Block a user