fix: integrate bug reporting API and enhance dashboard with recent bug statistics
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { formatDate, formatDateTime } from '../../utils/format';
|
||||
import { settingsApi } from '../../utils/api';
|
||||
import { bugsApi, settingsApi } from '../../utils/api';
|
||||
import type { BugReport, BugSeverity, BugStatus, BugReportNote } from '../../types';
|
||||
|
||||
function StatusBadge({ status }: { status: BugStatus }) {
|
||||
@@ -29,11 +29,39 @@ export default function IntranetBugs() {
|
||||
const [noteText, setNoteText] = useState('');
|
||||
const [isEnabled, setIsEnabled] = useState(true);
|
||||
const [toggling, setToggling] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loadError, setLoadError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
settingsApi.get().then((s) => setIsEnabled(s.bugsEnabled)).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const fetchBugs = useCallback(() => {
|
||||
setLoading(true);
|
||||
setLoadError('');
|
||||
bugsApi
|
||||
.getBugs({
|
||||
status: statusFilter,
|
||||
severity: severityFilter,
|
||||
assignedTo: assignedFilter,
|
||||
limit: 100,
|
||||
})
|
||||
.then((res) => {
|
||||
const next = Array.isArray(res?.data) ? res.data : [];
|
||||
setBugs(next);
|
||||
setSelected((prev) => (prev ? next.find((b) => b.id === prev.id) ?? null : null));
|
||||
})
|
||||
.catch((err) => {
|
||||
setBugs([]);
|
||||
setLoadError(err instanceof Error ? err.message : 'Failed to load bug reports.');
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}, [statusFilter, severityFilter, assignedFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchBugs();
|
||||
}, [fetchBugs]);
|
||||
|
||||
const handleToggle = useCallback((enabled: boolean) => {
|
||||
setToggling(true);
|
||||
settingsApi.update({ bugsEnabled: enabled })
|
||||
@@ -59,33 +87,43 @@ export default function IntranetBugs() {
|
||||
}, [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);
|
||||
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]);
|
||||
bugsApi.updateBug(bugId, { assignedToId: staffId || null }).catch(() => {
|
||||
fetchBugs();
|
||||
});
|
||||
}, [fetchBugs, updateBug]);
|
||||
|
||||
const handleStatusChange = useCallback((bugId: string, status: BugStatus) => {
|
||||
updateBug(bugId, { status });
|
||||
}, [updateBug]);
|
||||
bugsApi.updateBug(bugId, { status }).catch(() => {
|
||||
fetchBugs();
|
||||
});
|
||||
}, [fetchBugs, updateBug]);
|
||||
|
||||
const handleAddNote = useCallback((bugId: string) => {
|
||||
if (!noteText.trim() || !user) return;
|
||||
const content = noteText.trim();
|
||||
const note: BugReportNote = {
|
||||
id: `n${Date.now()}`,
|
||||
bugReportId: bugId,
|
||||
authorId: user.id,
|
||||
authorName: user.username,
|
||||
content: noteText.trim(),
|
||||
content,
|
||||
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]);
|
||||
bugsApi.addNote(bugId, content).catch(() => {
|
||||
fetchBugs();
|
||||
});
|
||||
}, [fetchBugs, noteText, user]);
|
||||
|
||||
if (!isEnabled) {
|
||||
return (
|
||||
@@ -182,7 +220,16 @@ export default function IntranetBugs() {
|
||||
|
||||
{/* Bug list */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.4rem' }}>
|
||||
{filtered.length === 0 ? (
|
||||
{loadError && (
|
||||
<div style={{ background: 'rgba(220,38,38,0.08)', border: '1px solid rgba(220,38,38,0.35)', padding: '0.75rem 0.9rem', color: 'var(--color-red)', fontFamily: 'var(--font-mono)', fontSize: '0.72rem' }}>
|
||||
{loadError}
|
||||
</div>
|
||||
)}
|
||||
{loading ? (
|
||||
<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' }}>
|
||||
Loading reports...
|
||||
</div>
|
||||
) : 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>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { bugsApi } from '../../utils/api';
|
||||
import type { BugReport } from '../../types';
|
||||
|
||||
interface StatCardProps {
|
||||
label: string;
|
||||
@@ -79,10 +82,30 @@ function NavTile({ to, label, description, icon }: NavTileProps) {
|
||||
|
||||
export default function IntranetDashboard() {
|
||||
const { user } = useAuth();
|
||||
const [bugs, setBugs] = useState<BugReport[]>([]);
|
||||
const [loadingBugs, setLoadingBugs] = useState(true);
|
||||
const [bugError, setBugError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setLoadingBugs(true);
|
||||
setBugError('');
|
||||
bugsApi
|
||||
.getBugs({ limit: 100 })
|
||||
.then((res) => setBugs(Array.isArray(res?.data) ? res.data : []))
|
||||
.catch((err) => {
|
||||
setBugs([]);
|
||||
setBugError(err instanceof Error ? err.message : 'Failed to load bug reports.');
|
||||
})
|
||||
.finally(() => setLoadingBugs(false));
|
||||
}, []);
|
||||
|
||||
const { openBugs, criticalBugs, assignedToMe, recentBugs } = useMemo(() => {
|
||||
const open = bugs.filter((b) => b.status === 'open').length;
|
||||
const critical = bugs.filter((b) => b.severity === 'critical').length;
|
||||
const mine = bugs.filter((b) => b.assignedToId === user?.id).length;
|
||||
return { openBugs: open, criticalBugs: critical, assignedToMe: mine, recentBugs: bugs.slice(0, 5) };
|
||||
}, [bugs, user?.id]);
|
||||
|
||||
const openBugs = 0;
|
||||
const criticalBugs = 0;
|
||||
const assignedToMe = 0;
|
||||
const totalUsers = 0;
|
||||
|
||||
return (
|
||||
@@ -105,16 +128,70 @@ export default function IntranetDashboard() {
|
||||
<div style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text-muted)', fontSize: '0.7rem', letterSpacing: '0.15em', marginBottom: '1rem' }}>
|
||||
QUICK STATS
|
||||
</div>
|
||||
{bugError && (
|
||||
<div style={{ marginBottom: '0.75rem', background: 'rgba(220,38,38,0.08)', border: '1px solid rgba(220,38,38,0.35)', padding: '0.6rem 0.8rem', color: 'var(--color-red)', fontFamily: 'var(--font-mono)', fontSize: '0.72rem' }}>
|
||||
{bugError}
|
||||
</div>
|
||||
)}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: '0.75rem' }}>
|
||||
<StatCard label="Open Bugs" value={openBugs} accent="green" />
|
||||
<StatCard label="Critical" value={criticalBugs} accent="red" />
|
||||
<StatCard label="Assigned to Me" value={assignedToMe} accent="amber" />
|
||||
<StatCard label="Open Bugs" value={loadingBugs ? '...' : openBugs} accent="green" />
|
||||
<StatCard label="Critical" value={loadingBugs ? '...' : criticalBugs} accent="red" />
|
||||
<StatCard label="Assigned to Me" value={loadingBugs ? '...' : assignedToMe} accent="amber" />
|
||||
<StatCard label="Total Users" value={totalUsers} accent="green" />
|
||||
<StatCard label="Forum Threads" value={0} accent="green" />
|
||||
<StatCard label="Staff Posts Today" value={0} accent="amber" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent bug reports */}
|
||||
<div style={{ marginBottom: '2rem' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text-muted)', fontSize: '0.7rem', letterSpacing: '0.15em' }}>
|
||||
RECENT BUG REPORTS
|
||||
</div>
|
||||
<Link to="/intranet/bugs" style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-yellow)', fontSize: '0.72rem', textDecoration: 'none' }}>
|
||||
View all
|
||||
</Link>
|
||||
</div>
|
||||
<div style={{ background: 'var(--color-surface)', border: '1px solid var(--color-border)' }}>
|
||||
{loadingBugs ? (
|
||||
<div style={{ padding: '1rem', color: 'var(--color-text-muted)', fontFamily: 'var(--font-mono)', fontSize: '0.78rem' }}>
|
||||
Loading bug reports...
|
||||
</div>
|
||||
) : recentBugs.length === 0 ? (
|
||||
<div style={{ padding: '1rem', color: 'var(--color-text-muted)', fontFamily: 'var(--font-mono)', fontSize: '0.78rem' }}>
|
||||
No bug reports yet.
|
||||
</div>
|
||||
) : (
|
||||
recentBugs.map((bug) => (
|
||||
<Link
|
||||
key={bug.id}
|
||||
to="/intranet/bugs"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: '1rem',
|
||||
padding: '0.75rem 1rem',
|
||||
textDecoration: 'none',
|
||||
borderTop: '1px solid var(--color-border)',
|
||||
}}
|
||||
>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text-muted)', fontSize: '0.66rem' }}>{bug.uniqueCode}</div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text)', fontSize: '0.78rem', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{bug.title}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text-muted)', fontSize: '0.68rem', flexShrink: 0 }}>
|
||||
{bug.status}
|
||||
</div>
|
||||
</Link>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation tiles */}
|
||||
<div style={{ marginBottom: '2rem' }}>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text-muted)', fontSize: '0.7rem', letterSpacing: '0.15em', marginBottom: '1rem' }}>
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { getToken } from '../contexts/AuthContext';
|
||||
import type { ForumCategory, ForumReply, ForumThread } from '../types';
|
||||
import type {
|
||||
BugReport,
|
||||
BugReportNote,
|
||||
BugSeverity,
|
||||
BugStatus,
|
||||
ForumCategory,
|
||||
ForumReply,
|
||||
ForumThread,
|
||||
} from '../types';
|
||||
|
||||
export const API_BASE = (import.meta.env.VITE_API_URL as string | undefined) ?? '/api';
|
||||
|
||||
@@ -112,3 +120,34 @@ export const settingsApi = {
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
};
|
||||
|
||||
export const bugsApi = {
|
||||
getBugs: (params?: {
|
||||
status?: BugStatus | 'all';
|
||||
severity?: BugSeverity | 'all';
|
||||
assignedTo?: string | 'all';
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}) => {
|
||||
const q = new URLSearchParams();
|
||||
if (params?.status && params.status !== 'all') q.set('status', params.status);
|
||||
if (params?.severity && params.severity !== 'all') q.set('severity', params.severity);
|
||||
if (params?.assignedTo && params.assignedTo !== 'all') q.set('assignedTo', params.assignedTo);
|
||||
q.set('page', String(params?.page ?? 1));
|
||||
q.set('limit', String(params?.limit ?? 100));
|
||||
|
||||
return apiFetch<{ data: BugReport[]; total: number; page: number; pages: number }>(`/bugs?${q.toString()}`);
|
||||
},
|
||||
|
||||
updateBug: (id: string, data: { status?: BugStatus; assignedToId?: string | null }) =>
|
||||
apiFetch<BugReport>(`/bugs/${id}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
|
||||
addNote: (id: string, content: string) =>
|
||||
apiFetch<BugReportNote>(`/bugs/${id}/notes`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ content }),
|
||||
}),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user