@@ -55,6 +55,7 @@ type ShotRow = {
|
|||||||
description: string | null;
|
description: string | null;
|
||||||
isKeyShot: boolean;
|
isKeyShot: boolean;
|
||||||
artist: { id: string; name: string | null; image: string | null; email: string } | null;
|
artist: { id: string; name: string | null; image: string | null; email: string } | null;
|
||||||
|
shotGroup: { id: string; name: string } | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Project {
|
interface Project {
|
||||||
@@ -491,7 +492,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 [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());
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
// Load saved episode expand state when project changes
|
// Load saved episode expand state when project changes
|
||||||
@@ -504,10 +506,10 @@ export function ShotStatusClient({
|
|||||||
setExpandedEpisodes(new Set());
|
setExpandedEpisodes(new Set());
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const savedGroup = localStorage.getItem(`shotStatus:${selectedProjectId}:groupByScene`);
|
const savedGroup = localStorage.getItem(`shotStatus:${selectedProjectId}:groupMode`);
|
||||||
setGroupByScene(savedGroup === "1");
|
setGroupMode((savedGroup as "flat" | "scene" | "group") ?? "flat");
|
||||||
} catch {
|
} catch {
|
||||||
setGroupByScene(false);
|
setGroupMode("flat");
|
||||||
}
|
}
|
||||||
}, [selectedProjectId]);
|
}, [selectedProjectId]);
|
||||||
const [dueDateDialogOpen, setDueDateDialogOpen] = useState(false);
|
const [dueDateDialogOpen, setDueDateDialogOpen] = useState(false);
|
||||||
@@ -531,11 +533,19 @@ export function ShotStatusClient({
|
|||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleGroupByScene = (checked: boolean) => {
|
const toggleGroup = (g: string) =>
|
||||||
setGroupByScene(checked);
|
setExpandedGroups((prev) => {
|
||||||
if (checked) setExpandedScenes(new Set(shots.map((s) => s.scene)));
|
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) {
|
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[]>();
|
const map = new Map<string, ShotRow[]>();
|
||||||
for (const shot of shots) {
|
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(
|
const episodeDueDateMap = new Map(
|
||||||
episodeDueDates.map((e) => [e.episode, new Date(e.dueDate)])
|
episodeDueDates.map((e) => [e.episode, new Date(e.dueDate)])
|
||||||
);
|
);
|
||||||
@@ -641,15 +664,23 @@ export function ShotStatusClient({
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
{selectedProject && !isEpisodic && (
|
{selectedProject && !isEpisodic && (
|
||||||
<label className="flex items-center gap-2 cursor-pointer select-none">
|
<>
|
||||||
<input
|
<span className="text-sm text-zinc-500">Group by:</span>
|
||||||
type="checkbox"
|
{(["flat", "scene", "group"] as const).map((mode) => (
|
||||||
checked={groupByScene}
|
<button
|
||||||
onChange={(e) => handleGroupByScene(e.target.checked)}
|
key={mode}
|
||||||
className="cursor-pointer accent-blue-500 w-3.5 h-3.5"
|
onClick={() => handleGroupMode(mode)}
|
||||||
/>
|
className={cn(
|
||||||
<span className="text-sm text-zinc-400">Group by scene</span>
|
"px-3 py-1 rounded text-xs font-medium transition-colors",
|
||||||
</label>
|
groupMode === mode
|
||||||
|
? "bg-zinc-700 text-zinc-100"
|
||||||
|
: "text-zinc-500 hover:text-zinc-300"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{mode === "flat" ? "None" : mode === "scene" ? "Scene" : "Group"}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -773,7 +804,7 @@ export function ShotStatusClient({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : groupByScene ? (
|
) : groupMode === "scene" ? (
|
||||||
<div className="divide-y divide-zinc-800">
|
<div className="divide-y divide-zinc-800">
|
||||||
{sceneGroups.map(([scene, sceneShots]) => {
|
{sceneGroups.map(([scene, sceneShots]) => {
|
||||||
const collapsed = !expandedScenes.has(scene);
|
const collapsed = !expandedScenes.has(scene);
|
||||||
@@ -822,6 +853,55 @@ export function ShotStatusClient({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</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">
|
<div className="overflow-x-auto">
|
||||||
<ShotTable
|
<ShotTable
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ async function getShotsForProject(projectId: string) {
|
|||||||
description: true,
|
description: true,
|
||||||
isKeyShot: true,
|
isKeyShot: true,
|
||||||
artist: { select: { id: true, name: true, image: true, email: true } },
|
artist: { select: { id: true, name: true, image: true, email: true } },
|
||||||
|
shotGroup: { select: { id: true, name: true } },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user