This repository has been archived on 2026-05-01. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Nest/nest-intra/src/pages/intranet/IntranetBugs.tsx
2026-02-26 16:26:16 +01:00

252 lines
14 KiB
TypeScript

import { useState, useMemo, useCallback } from 'react';
import { MOCK_BUGS, MOCK_USERS } from '../../data/mockData';
import { useAuth } from '../../contexts/AuthContext';
import { formatDate, formatDateTime } from '../../utils/format';
import type { BugReport, BugSeverity, BugStatus, BugReportNote } from '../../types';
function StatusBadge({ status }: { status: BugStatus }) {
const map: Record<BugStatus, string> = { open: 'badge-open', in_progress: 'badge-progress', resolved: 'badge-resolved', closed: 'badge-closed' };
const labels: Record<BugStatus, string> = { open: 'Open', in_progress: 'In Progress', resolved: 'Resolved', closed: 'Closed' };
return <span className={`badge ${map[status]}`}>{labels[status]}</span>;
}
function SeverityBadge({ severity }: { severity: BugSeverity }) {
const map: Record<BugSeverity, string> = { low: 'badge-low', medium: 'badge-medium', high: 'badge-high', critical: 'badge-critical' };
return <span className={`badge ${map[severity]}`}>{severity}</span>;
}
const STATUSES: BugStatus[] = ['open', 'in_progress', 'resolved', 'closed'];
const STAFF_MEMBERS = MOCK_USERS.filter((u) => u.role === 'dev' || u.role === 'com');
export default function IntranetBugs() {
const { user } = useAuth();
const [bugs, setBugs] = useState<BugReport[]>(MOCK_BUGS);
const [selected, setSelected] = useState<BugReport | null>(null);
const [statusFilter, setStatusFilter] = useState<BugStatus | 'all'>('all');
const [severityFilter, setSeverityFilter] = useState<BugSeverity | 'all'>('all');
const [assignedFilter, setAssignedFilter] = useState<string | 'all'>('all');
const [noteText, setNoteText] = useState('');
const openCount = bugs.filter((b) => b.status === 'open').length;
const criticalCount = bugs.filter((b) => b.severity === 'critical').length;
const myCount = bugs.filter((b) => b.assignedToId === user?.id).length;
const filtered = useMemo(() => {
return bugs.filter((b) => {
if (statusFilter !== 'all' && b.status !== statusFilter) return false;
if (severityFilter !== 'all' && b.severity !== severityFilter) return false;
if (assignedFilter !== 'all') {
if (assignedFilter === 'unassigned' && b.assignedToId) return false;
if (assignedFilter !== 'unassigned' && b.assignedToId !== assignedFilter) return false;
}
return true;
});
}, [bugs, statusFilter, severityFilter, assignedFilter]);
const updateBug = useCallback((id: string, changes: Partial<BugReport>) => {
setBugs((prev) => prev.map((b) => b.id === id ? { ...b, ...changes, updatedAt: new Date().toISOString() } : b));
setSelected((prev) => prev?.id === id ? { ...prev, ...changes, updatedAt: new Date().toISOString() } : prev);
}, []);
const handleAssign = useCallback((bugId: string, staffId: string) => {
const staff = STAFF_MEMBERS.find((s) => s.id === staffId);
updateBug(bugId, { assignedToId: staffId || undefined, assignedToName: staff?.username });
}, [updateBug]);
const handleStatusChange = useCallback((bugId: string, status: BugStatus) => {
updateBug(bugId, { status });
}, [updateBug]);
const handleAddNote = useCallback((bugId: string) => {
if (!noteText.trim() || !user) return;
const note: BugReportNote = {
id: `n${Date.now()}`,
bugReportId: bugId,
authorId: user.id,
authorName: user.username,
content: noteText.trim(),
createdAt: new Date().toISOString(),
};
setBugs((prev) => prev.map((b) => b.id === bugId ? { ...b, notes: [...(b.notes ?? []), note] } : b));
setSelected((prev) => prev?.id === bugId ? { ...prev, notes: [...(prev.notes ?? []), note] } : prev);
setNoteText('');
}, [noteText, user]);
return (
<div style={{ display: 'grid', gridTemplateColumns: selected ? '1fr 400px' : '1fr', gap: '1.5rem', alignItems: 'start' }}>
{/* Left panel */}
<div>
<div style={{ marginBottom: '1.5rem' }}>
<div style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text-muted)', fontSize: '0.7rem', letterSpacing: '0.15em', marginBottom: '0.5rem' }}>
INTRANET / BUG REPORTS
</div>
<h1 style={{ fontFamily: 'var(--font-heading)', color: 'var(--color-text)', fontSize: '1.8rem', marginBottom: '1rem' }}>BUG DASHBOARD</h1>
{/* Stats */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '0.5rem', marginBottom: '1.25rem' }}>
{[
{ label: 'Open', value: openCount, color: 'var(--color-green)' },
{ label: 'Critical', value: criticalCount, color: 'var(--color-red)' },
{ label: 'Mine', value: myCount, color: 'var(--color-amber)' },
].map(({ label, value, color }) => (
<div key={label} style={{ background: 'var(--color-surface)', border: '1px solid var(--color-border)', padding: '0.75rem', textAlign: 'center' }}>
<div style={{ fontFamily: 'var(--font-heading)', color, fontSize: '1.8rem' }}>{value}</div>
<div style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text-muted)', fontSize: '0.65rem', letterSpacing: '0.1em' }}>{label}</div>
</div>
))}
</div>
{/* Filters */}
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
<select className="input-terminal" style={{ width: 'auto', fontSize: '0.75rem' }} value={statusFilter} onChange={(e) => setStatusFilter(e.target.value as BugStatus | 'all')}>
<option value="all">All Statuses</option>
{STATUSES.map((s) => <option key={s} value={s}>{s}</option>)}
</select>
<select className="input-terminal" style={{ width: 'auto', fontSize: '0.75rem' }} value={severityFilter} onChange={(e) => setSeverityFilter(e.target.value as BugSeverity | 'all')}>
<option value="all">All Severities</option>
{(['critical','high','medium','low'] as BugSeverity[]).map((s) => <option key={s} value={s}>{s}</option>)}
</select>
<select className="input-terminal" style={{ width: 'auto', fontSize: '0.75rem' }} value={assignedFilter} onChange={(e) => setAssignedFilter(e.target.value)}>
<option value="all">All Assigned</option>
<option value="unassigned">Unassigned</option>
{STAFF_MEMBERS.map((s) => <option key={s.id} value={s.id}>{s.username}</option>)}
</select>
</div>
</div>
{/* Bug list */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.4rem' }}>
{filtered.length === 0 ? (
<div style={{ background: 'var(--color-surface)', border: '1px solid var(--color-border)', padding: '2rem', textAlign: 'center', color: 'var(--color-text-muted)', fontFamily: 'var(--font-mono)', fontSize: '0.8rem' }}>
No reports match filters.
</div>
) : (
filtered.map((bug) => (
<div
key={bug.id}
onClick={() => setSelected(bug === selected ? null : bug)}
style={{
background: selected?.id === bug.id ? 'rgba(37,99,235,0.08)' : 'var(--color-surface)',
border: `1px solid ${selected?.id === bug.id ? 'var(--color-yellow)' : 'var(--color-border)'}`,
padding: '0.85rem 1.1rem',
cursor: 'pointer',
transition: 'all 0.15s',
}}
role="button"
tabIndex={0}
onKeyDown={(e) => e.key === 'Enter' && setSelected(bug === selected ? null : bug)}
aria-label={`Select bug report ${bug.uniqueCode}`}
>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '0.5rem', flexWrap: 'wrap', marginBottom: '0.3rem' }}>
<div style={{ display: 'flex', gap: '0.4rem', alignItems: 'center', flexWrap: 'wrap' }}>
<span style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text-muted)', fontSize: '0.68rem' }}>{bug.uniqueCode}</span>
<StatusBadge status={bug.status} />
<SeverityBadge severity={bug.severity} />
</div>
<span style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text-muted)', fontSize: '0.68rem', flexShrink: 0 }}>
{formatDate(bug.createdAt)}
</span>
</div>
<div style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text)', fontSize: '0.83rem', marginBottom: '0.2rem', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{bug.title}
</div>
<div style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text-muted)', fontSize: '0.68rem' }}>
{bug.submittedByName} &mdash; Assigned: {bug.assignedToName ?? 'None'}
</div>
</div>
))
)}
</div>
</div>
{/* Right panel — detail */}
{selected && (
<div style={{ background: 'var(--color-surface)', border: '1px solid var(--color-border)', padding: '1.5rem', position: 'sticky', top: '1rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '1rem' }}>
<div>
<div style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text-muted)', fontSize: '0.68rem', marginBottom: '0.25rem' }}>{selected.uniqueCode}</div>
<h2 style={{ fontFamily: 'var(--font-heading)', color: 'var(--color-text)', fontSize: '1.1rem' }}>{selected.title}</h2>
</div>
<button onClick={() => setSelected(null)} style={{ background: 'transparent', border: 'none', color: 'var(--color-text-muted)', cursor: 'pointer', fontSize: '1.1rem' }} aria-label="Close">&#x2715;</button>
</div>
{/* Controls */}
<div style={{ display: 'grid', gap: '0.75rem', marginBottom: '1.25rem' }}>
<div>
<label style={{ display: 'block', fontFamily: 'var(--font-mono)', color: 'var(--color-text-muted)', fontSize: '0.68rem', letterSpacing: '0.1em', marginBottom: '0.3rem' }}>STATUS</label>
<select
className="input-terminal"
style={{ fontSize: '0.75rem' }}
value={selected.status}
onChange={(e) => handleStatusChange(selected.id, e.target.value as BugStatus)}
>
{STATUSES.map((s) => <option key={s} value={s}>{s}</option>)}
</select>
</div>
<div>
<label style={{ display: 'block', fontFamily: 'var(--font-mono)', color: 'var(--color-text-muted)', fontSize: '0.68rem', letterSpacing: '0.1em', marginBottom: '0.3rem' }}>ASSIGN TO</label>
<select
className="input-terminal"
style={{ fontSize: '0.75rem' }}
value={selected.assignedToId ?? ''}
onChange={(e) => handleAssign(selected.id, e.target.value)}
>
<option value="">Unassigned</option>
{STAFF_MEMBERS.map((s) => <option key={s.id} value={s.id}>{s.username} ({s.role})</option>)}
</select>
</div>
</div>
{/* Description */}
<div style={{ marginBottom: '1rem' }}>
<div style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text-muted)', fontSize: '0.65rem', letterSpacing: '0.1em', marginBottom: '0.4rem' }}>DESCRIPTION</div>
<div style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text-dim)', fontSize: '0.78rem', lineHeight: 1.7, whiteSpace: 'pre-wrap', background: 'var(--color-bg-alt)', padding: '0.75rem', borderRadius: '4px' }}>
{selected.description}
</div>
</div>
<div style={{ marginBottom: '1rem' }}>
<div style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text-muted)', fontSize: '0.65rem', letterSpacing: '0.1em', marginBottom: '0.4rem' }}>STEPS TO REPRODUCE</div>
<div style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text-dim)', fontSize: '0.78rem', lineHeight: 1.7, whiteSpace: 'pre-wrap', background: 'var(--color-bg-alt)', padding: '0.75rem', borderRadius: '4px' }}>
{selected.stepsToReproduce}
</div>
</div>
{/* Internal notes */}
<div style={{ marginBottom: '1rem' }}>
<div style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-amber)', fontSize: '0.65rem', letterSpacing: '0.1em', marginBottom: '0.5rem' }}>INTERNAL NOTES (staff only)</div>
{(selected.notes ?? []).length === 0 ? (
<div style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text-muted)', fontSize: '0.75rem', padding: '0.5rem 0' }}>No notes yet.</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem', marginBottom: '0.75rem' }}>
{(selected.notes ?? []).map((note) => (
<div key={note.id} style={{ background: 'rgba(217,119,6,0.08)', border: '1px solid rgba(217,119,6,0.2)', padding: '0.6rem', borderRadius: '4px' }}>
<div style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-amber)', fontSize: '0.65rem', marginBottom: '0.2rem' }}>
{note.authorName} &mdash; {formatDateTime(note.createdAt)}
</div>
<div style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text-dim)', fontSize: '0.77rem', lineHeight: 1.6 }}>
{note.content}
</div>
</div>
))}
</div>
)}
<textarea
className="input-terminal"
rows={3}
placeholder="Add an internal note..."
value={noteText}
onChange={(e) => setNoteText(e.target.value)}
style={{ resize: 'vertical', fontSize: '0.8rem', marginBottom: '0.5rem' }}
/>
<button className="btn-terminal btn-amber" onClick={() => handleAddNote(selected.id)} style={{ padding: '0.35rem 0.9rem', fontSize: '0.75rem' }}>
Add Note
</button>
</div>
</div>
)}
</div>
);
}