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 { useState, useMemo, useCallback, useEffect } from 'react';
|
||||||
import { useAuth } from '../../contexts/AuthContext';
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
import { formatDate, formatDateTime } from '../../utils/format';
|
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';
|
import type { BugReport, BugSeverity, BugStatus, BugReportNote } from '../../types';
|
||||||
|
|
||||||
function StatusBadge({ status }: { status: BugStatus }) {
|
function StatusBadge({ status }: { status: BugStatus }) {
|
||||||
@@ -29,11 +29,39 @@ export default function IntranetBugs() {
|
|||||||
const [noteText, setNoteText] = useState('');
|
const [noteText, setNoteText] = useState('');
|
||||||
const [isEnabled, setIsEnabled] = useState(true);
|
const [isEnabled, setIsEnabled] = useState(true);
|
||||||
const [toggling, setToggling] = useState(false);
|
const [toggling, setToggling] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [loadError, setLoadError] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
settingsApi.get().then((s) => setIsEnabled(s.bugsEnabled)).catch(() => {});
|
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) => {
|
const handleToggle = useCallback((enabled: boolean) => {
|
||||||
setToggling(true);
|
setToggling(true);
|
||||||
settingsApi.update({ bugsEnabled: enabled })
|
settingsApi.update({ bugsEnabled: enabled })
|
||||||
@@ -59,33 +87,43 @@ export default function IntranetBugs() {
|
|||||||
}, [bugs, statusFilter, severityFilter, assignedFilter]);
|
}, [bugs, statusFilter, severityFilter, assignedFilter]);
|
||||||
|
|
||||||
const updateBug = useCallback((id: string, changes: Partial<BugReport>) => {
|
const updateBug = useCallback((id: string, changes: Partial<BugReport>) => {
|
||||||
setBugs((prev) => prev.map((b) => b.id === id ? { ...b, ...changes, updatedAt: new Date().toISOString() } : b));
|
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);
|
setSelected((prev) => (prev?.id === id ? { ...prev, ...changes, updatedAt: new Date().toISOString() } : prev));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleAssign = useCallback((bugId: string, staffId: string) => {
|
const handleAssign = useCallback((bugId: string, staffId: string) => {
|
||||||
const staff = STAFF_MEMBERS.find((s) => s.id === staffId);
|
const staff = STAFF_MEMBERS.find((s) => s.id === staffId);
|
||||||
updateBug(bugId, { assignedToId: staffId || undefined, assignedToName: staff?.username });
|
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) => {
|
const handleStatusChange = useCallback((bugId: string, status: BugStatus) => {
|
||||||
updateBug(bugId, { status });
|
updateBug(bugId, { status });
|
||||||
}, [updateBug]);
|
bugsApi.updateBug(bugId, { status }).catch(() => {
|
||||||
|
fetchBugs();
|
||||||
|
});
|
||||||
|
}, [fetchBugs, updateBug]);
|
||||||
|
|
||||||
const handleAddNote = useCallback((bugId: string) => {
|
const handleAddNote = useCallback((bugId: string) => {
|
||||||
if (!noteText.trim() || !user) return;
|
if (!noteText.trim() || !user) return;
|
||||||
|
const content = noteText.trim();
|
||||||
const note: BugReportNote = {
|
const note: BugReportNote = {
|
||||||
id: `n${Date.now()}`,
|
id: `n${Date.now()}`,
|
||||||
bugReportId: bugId,
|
bugReportId: bugId,
|
||||||
authorId: user.id,
|
authorId: user.id,
|
||||||
authorName: user.username,
|
authorName: user.username,
|
||||||
content: noteText.trim(),
|
content,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
setBugs((prev) => prev.map((b) => b.id === bugId ? { ...b, notes: [...(b.notes ?? []), note] } : b));
|
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);
|
setSelected((prev) => prev?.id === bugId ? { ...prev, notes: [...(prev.notes ?? []), note] } : prev);
|
||||||
setNoteText('');
|
setNoteText('');
|
||||||
}, [noteText, user]);
|
bugsApi.addNote(bugId, content).catch(() => {
|
||||||
|
fetchBugs();
|
||||||
|
});
|
||||||
|
}, [fetchBugs, noteText, user]);
|
||||||
|
|
||||||
if (!isEnabled) {
|
if (!isEnabled) {
|
||||||
return (
|
return (
|
||||||
@@ -182,7 +220,16 @@ export default function IntranetBugs() {
|
|||||||
|
|
||||||
{/* Bug list */}
|
{/* Bug list */}
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.4rem' }}>
|
<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' }}>
|
<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.
|
No reports match filters.
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useAuth } from '../../contexts/AuthContext';
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
|
import { bugsApi } from '../../utils/api';
|
||||||
|
import type { BugReport } from '../../types';
|
||||||
|
|
||||||
interface StatCardProps {
|
interface StatCardProps {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -79,10 +82,30 @@ function NavTile({ to, label, description, icon }: NavTileProps) {
|
|||||||
|
|
||||||
export default function IntranetDashboard() {
|
export default function IntranetDashboard() {
|
||||||
const { user } = useAuth();
|
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;
|
const totalUsers = 0;
|
||||||
|
|
||||||
return (
|
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' }}>
|
<div style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text-muted)', fontSize: '0.7rem', letterSpacing: '0.15em', marginBottom: '1rem' }}>
|
||||||
QUICK STATS
|
QUICK STATS
|
||||||
</div>
|
</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' }}>
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: '0.75rem' }}>
|
||||||
<StatCard label="Open Bugs" value={openBugs} accent="green" />
|
<StatCard label="Open Bugs" value={loadingBugs ? '...' : openBugs} accent="green" />
|
||||||
<StatCard label="Critical" value={criticalBugs} accent="red" />
|
<StatCard label="Critical" value={loadingBugs ? '...' : criticalBugs} accent="red" />
|
||||||
<StatCard label="Assigned to Me" value={assignedToMe} accent="amber" />
|
<StatCard label="Assigned to Me" value={loadingBugs ? '...' : assignedToMe} accent="amber" />
|
||||||
<StatCard label="Total Users" value={totalUsers} accent="green" />
|
<StatCard label="Total Users" value={totalUsers} accent="green" />
|
||||||
<StatCard label="Forum Threads" value={0} accent="green" />
|
<StatCard label="Forum Threads" value={0} accent="green" />
|
||||||
<StatCard label="Staff Posts Today" value={0} accent="amber" />
|
<StatCard label="Staff Posts Today" value={0} accent="amber" />
|
||||||
</div>
|
</div>
|
||||||
</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 */}
|
{/* Navigation tiles */}
|
||||||
<div style={{ marginBottom: '2rem' }}>
|
<div style={{ marginBottom: '2rem' }}>
|
||||||
<div style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text-muted)', fontSize: '0.7rem', letterSpacing: '0.15em', marginBottom: '1rem' }}>
|
<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 { 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';
|
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),
|
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