From e68c2c32ba514e2865317d7c632a3e5a2a0784d9 Mon Sep 17 00:00:00 2001 From: Thibault Pouch Date: Wed, 18 Mar 2026 11:13:35 +0100 Subject: [PATCH] fix: integrate bug reporting API and enhance dashboard with recent bug statistics --- .../src/pages/intranet/IntranetBugs.tsx | 63 +++++++++++-- .../src/pages/intranet/IntranetDashboard.tsx | 89 +++++++++++++++++-- nest-intra/src/utils/api.ts | 41 ++++++++- 3 files changed, 178 insertions(+), 15 deletions(-) diff --git a/nest-intra/src/pages/intranet/IntranetBugs.tsx b/nest-intra/src/pages/intranet/IntranetBugs.tsx index e2392dc..ff195e2 100644 --- a/nest-intra/src/pages/intranet/IntranetBugs.tsx +++ b/nest-intra/src/pages/intranet/IntranetBugs.tsx @@ -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) => { - 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 */}
- {filtered.length === 0 ? ( + {loadError && ( +
+ {loadError} +
+ )} + {loading ? ( +
+ Loading reports... +
+ ) : filtered.length === 0 ? (
No reports match filters.
diff --git a/nest-intra/src/pages/intranet/IntranetDashboard.tsx b/nest-intra/src/pages/intranet/IntranetDashboard.tsx index 4e1053b..22fe8ec 100644 --- a/nest-intra/src/pages/intranet/IntranetDashboard.tsx +++ b/nest-intra/src/pages/intranet/IntranetDashboard.tsx @@ -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([]); + 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() {
QUICK STATS
+ {bugError && ( +
+ {bugError} +
+ )}
- - - + + +
+ {/* Recent bug reports */} +
+
+
+ RECENT BUG REPORTS +
+ + View all + +
+
+ {loadingBugs ? ( +
+ Loading bug reports... +
+ ) : recentBugs.length === 0 ? ( +
+ No bug reports yet. +
+ ) : ( + recentBugs.map((bug) => ( + +
+
{bug.uniqueCode}
+
+ {bug.title} +
+
+
+ {bug.status} +
+ + )) + )} +
+
+ {/* Navigation tiles */}
diff --git a/nest-intra/src/utils/api.ts b/nest-intra/src/utils/api.ts index 6dcc516..4403742 100644 --- a/nest-intra/src/utils/api.ts +++ b/nest-intra/src/utils/api.ts @@ -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(`/bugs/${id}`, { + method: 'PATCH', + body: JSON.stringify(data), + }), + + addNote: (id: string, content: string) => + apiFetch(`/bugs/${id}/notes`, { + method: 'POST', + body: JSON.stringify({ content }), + }), +};