Per organized-ideas.md §8: the storage layer (DiagramRepository etc.) was already renamed in an earlier phase; this finishes it everywhere else. - domain/types.ts: Project -> Diagram. domain/project.ts -> domain/diagram.ts (createEmptyProject -> createEmptyDiagram, default name "Untitled Diagram"). - domain/compatibility.ts, domain/bom.ts: Project param/type -> Diagram. - data/exportImport.ts: ProjectImportError -> DiagramImportError, projectToJson/downloadProjectFile/readProjectFile/normalizeProject -> their Diagram equivalents. - state/projectStore.ts -> state/diagramStore.ts: useProjectStore -> useDiagramStore, the `project` field -> `diagram`, newProject/renameProject/ importProject/applyRestoredProject -> *Diagram, restoredProjectUpdatedAt -> restoredDiagramUpdatedAt. - Every component updated to match, compiler-guided (tsc -b enumerated each remaining call site after the core rename, the same approach used for the earlier catalog-parameter refactor). - README updated for the terminology, and to match the repository class names (which had already been renamed but the README hadn't caught up). No backend/schema changes: the JSON shape stored in diagrams.data never changed, only TypeScript-side identifiers, so existing diagrams are unaffected. One SQL comment fixed for accuracy (no migration needed). Verified: tsc -b and oxlint clean; grepped src/ for any remaining Project/project reference (none) after the sweep.
181 lines
7.6 KiB
TypeScript
181 lines
7.6 KiB
TypeScript
import { useMemo, useRef, useState } from 'react'
|
|
import { downloadDiagramFile, readDiagramFile, DiagramImportError } from '../../data/exportImport'
|
|
import { supabase } from '../../data/supabaseClient'
|
|
import { useAdminReviewStore } from '../../state/adminReviewStore'
|
|
import { useAuthStore } from '../../state/authStore'
|
|
import { useDiagramStore } from '../../state/diagramStore'
|
|
import { useSubmissionStore } from '../../state/submissionStore'
|
|
import AdminUsersModal from '../admin/AdminUsersModal'
|
|
import DiagramSharingModal from '../sharing/DiagramSharingModal'
|
|
import VersionHistoryModal from '../sharing/VersionHistoryModal'
|
|
import AdminReviewModal from '../submissions/AdminReviewModal'
|
|
import MySubmissionsModal from '../submissions/MySubmissionsModal'
|
|
import DiagramManagerModal from './DiagramManagerModal'
|
|
|
|
// When "My Submissions" was last opened — a per-user UI preference (like
|
|
// hiddenPublicDeviceTemplateIds in catalogStore), used to badge outcomes
|
|
// (approved/rejected) you haven't seen yet without a server-side
|
|
// notifications table.
|
|
const LAST_SEEN_SUBMISSIONS_KEY = 'av-planner:lastSeenSubmissionsAt'
|
|
function getLastSeenSubmissionsAt(): string {
|
|
try {
|
|
return window.localStorage.getItem(LAST_SEEN_SUBMISSIONS_KEY) ?? ''
|
|
} catch {
|
|
return ''
|
|
}
|
|
}
|
|
function setLastSeenSubmissionsAt(iso: string): void {
|
|
try {
|
|
window.localStorage.setItem(LAST_SEEN_SUBMISSIONS_KEY, iso)
|
|
} catch {
|
|
// Not essential — worst case an already-seen outcome stays badged.
|
|
}
|
|
}
|
|
|
|
export default function TopBar() {
|
|
const diagram = useDiagramStore((s) => s.diagram)
|
|
const access = useDiagramStore((s) => s.access)
|
|
const canEdit = access?.myPermission !== 'view'
|
|
const renameDiagram = useDiagramStore((s) => s.renameDiagram)
|
|
const importDiagram = useDiagramStore((s) => s.importDiagram)
|
|
const mySubmissions = useSubmissionStore((s) => s.mySubmissions)
|
|
const username = useAuthStore((s) => s.username)
|
|
const role = useAuthStore((s) => s.role)
|
|
const isAdmin = role === 'admin' || role === 'super_admin'
|
|
const isSuperAdmin = role === 'super_admin'
|
|
const adminPendingCount = useAdminReviewStore((s) => s.allSubmissions.filter((sub) => sub.status === 'pending').length)
|
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
const [diagramManagerOpen, setDiagramManagerOpen] = useState(false)
|
|
const [versionHistoryOpen, setVersionHistoryOpen] = useState(false)
|
|
const [sharingOpen, setSharingOpen] = useState(false)
|
|
const [submissionsOpen, setSubmissionsOpen] = useState(false)
|
|
const [adminReviewOpen, setAdminReviewOpen] = useState(false)
|
|
const [adminUsersOpen, setAdminUsersOpen] = useState(false)
|
|
|
|
const pendingSubmissionCount = useMemo(
|
|
() => mySubmissions.filter((s) => s.status === 'pending').length,
|
|
[mySubmissions],
|
|
)
|
|
const unseenOutcomeCount = useMemo(() => {
|
|
const lastSeen = getLastSeenSubmissionsAt()
|
|
return mySubmissions.filter((s) => s.status !== 'pending' && s.updatedAt > lastSeen).length
|
|
}, [mySubmissions])
|
|
|
|
const openMySubmissions = () => {
|
|
setSubmissionsOpen(true)
|
|
setLastSeenSubmissionsAt(new Date().toISOString())
|
|
}
|
|
|
|
const handleExport = () => downloadDiagramFile(diagram)
|
|
|
|
const handleImportClick = () => fileInputRef.current?.click()
|
|
|
|
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = event.target.files?.[0]
|
|
event.target.value = ''
|
|
if (!file) return
|
|
try {
|
|
const imported = await readDiagramFile(file)
|
|
await importDiagram(imported)
|
|
} catch (err) {
|
|
const message = err instanceof DiagramImportError ? err.message : 'Could not read that file.'
|
|
window.alert(`Import failed: ${message}`)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<header className="flex h-12 shrink-0 items-center justify-between border-b border-slate-200 bg-white px-3">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm font-semibold text-indigo-700">AV Planner</span>
|
|
<input
|
|
value={diagram.name}
|
|
onChange={(e) => renameDiagram(e.target.value)}
|
|
disabled={!canEdit}
|
|
className="rounded border border-transparent px-2 py-1 text-sm text-slate-700 hover:border-slate-200 focus:border-slate-300 focus:outline-none disabled:cursor-not-allowed disabled:opacity-60"
|
|
/>
|
|
</div>
|
|
<div className="flex items-center gap-1.5">
|
|
<button
|
|
onClick={() => setDiagramManagerOpen(true)}
|
|
className="rounded px-2.5 py-1.5 text-xs text-slate-600 hover:bg-slate-100"
|
|
>
|
|
Diagrams
|
|
</button>
|
|
<button
|
|
onClick={() => setVersionHistoryOpen(true)}
|
|
className="rounded px-2.5 py-1.5 text-xs text-slate-600 hover:bg-slate-100"
|
|
>
|
|
History
|
|
</button>
|
|
<button
|
|
onClick={() => setSharingOpen(true)}
|
|
className="rounded px-2.5 py-1.5 text-xs text-slate-600 hover:bg-slate-100"
|
|
>
|
|
Share
|
|
</button>
|
|
<button onClick={openMySubmissions} className="rounded px-2.5 py-1.5 text-xs text-slate-600 hover:bg-slate-100">
|
|
My Submissions
|
|
{unseenOutcomeCount > 0 ? (
|
|
<span
|
|
className="ml-1 rounded-full bg-indigo-100 px-1.5 py-0.5 text-[10px] font-semibold text-indigo-700"
|
|
title="Submission(s) reviewed since you last checked"
|
|
>
|
|
{unseenOutcomeCount}
|
|
</span>
|
|
) : (
|
|
pendingSubmissionCount > 0 && (
|
|
<span className="ml-1 rounded-full bg-amber-100 px-1.5 py-0.5 text-[10px] font-semibold text-amber-700">
|
|
{pendingSubmissionCount}
|
|
</span>
|
|
)
|
|
)}
|
|
</button>
|
|
{isAdmin && (
|
|
<button
|
|
onClick={() => setAdminReviewOpen(true)}
|
|
className="rounded px-2.5 py-1.5 text-xs text-slate-600 hover:bg-slate-100"
|
|
>
|
|
Review Queue
|
|
{adminPendingCount > 0 && (
|
|
<span className="ml-1 rounded-full bg-amber-100 px-1.5 py-0.5 text-[10px] font-semibold text-amber-700">
|
|
{adminPendingCount}
|
|
</span>
|
|
)}
|
|
</button>
|
|
)}
|
|
{isSuperAdmin && (
|
|
<button
|
|
onClick={() => setAdminUsersOpen(true)}
|
|
className="rounded px-2.5 py-1.5 text-xs text-slate-600 hover:bg-slate-100"
|
|
>
|
|
Manage Users
|
|
</button>
|
|
)}
|
|
<button onClick={handleImportClick} className="rounded px-2.5 py-1.5 text-xs text-slate-600 hover:bg-slate-100">
|
|
Import
|
|
</button>
|
|
<input ref={fileInputRef} type="file" accept="application/json,.json" className="hidden" onChange={handleFileChange} />
|
|
<button
|
|
onClick={handleExport}
|
|
className="rounded bg-indigo-600 px-2.5 py-1.5 text-xs font-medium text-white hover:bg-indigo-500"
|
|
>
|
|
Export
|
|
</button>
|
|
{username && <span className="ml-1 text-xs text-slate-400">{username}</span>}
|
|
<button
|
|
onClick={() => supabase.auth.signOut()}
|
|
className="rounded px-2.5 py-1.5 text-xs text-slate-500 hover:bg-slate-100"
|
|
>
|
|
Sign out
|
|
</button>
|
|
</div>
|
|
{diagramManagerOpen && <DiagramManagerModal onClose={() => setDiagramManagerOpen(false)} />}
|
|
{versionHistoryOpen && <VersionHistoryModal onClose={() => setVersionHistoryOpen(false)} />}
|
|
{sharingOpen && <DiagramSharingModal onClose={() => setSharingOpen(false)} />}
|
|
{submissionsOpen && <MySubmissionsModal onClose={() => setSubmissionsOpen(false)} />}
|
|
{adminReviewOpen && <AdminReviewModal onClose={() => setAdminReviewOpen(false)} />}
|
|
{adminUsersOpen && <AdminUsersModal onClose={() => setAdminUsersOpen(false)} />}
|
|
</header>
|
|
)
|
|
}
|