Add descriptive error messages across all API routes and toast notifications on client pages
Build & Auto-Repair / build (push) Has been cancelled

- 86 catch blocks in 49 API route files now return the actual error message via error.message
- 14 campaign/lead/config catch blocks that lacked console.error now log errors
- 17 client pages/components now show toast.error notifications on API failures
- Silent .catch(() => {}) and finally-only try blocks now surface errors to users
This commit is contained in:
2026-07-01 15:20:30 +02:00
parent 727dcddf37
commit 3fe32d923e
76 changed files with 344 additions and 155 deletions
+5 -4
View File
@@ -12,6 +12,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import {
ArrowLeft, Plus, Trash2, Send, Play, Pause, Users, Mail, Clock,
} from "lucide-react"
import { toast } from "sonner"
export default function CampaignDetailPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = use(params)
@@ -29,7 +30,7 @@ export default function CampaignDetailPage({ params }: { params: Promise<{ id: s
try {
const res = await fetch(`/api/campaigns/${id}`)
if (res.ok) setCampaign(await res.json())
} catch { /* ignore */ }
} catch (e) { console.error("Failed to load campaign", e); toast.error("Failed to load campaign") }
setLoading(false)
}
@@ -53,7 +54,7 @@ export default function CampaignDetailPage({ params }: { params: Promise<{ id: s
})
setNewStep({ subject: "", bodyText: "", delayDays: 3 })
fetchCampaign()
} catch { /* ignore */ }
} catch (e) { console.error("Failed to add step", e); toast.error("Failed to add step") }
setAddingStep(false)
}
@@ -68,7 +69,7 @@ export default function CampaignDetailPage({ params }: { params: Promise<{ id: s
setLeadSearch("")
setSearchResults([])
fetchCampaign()
} catch { /* ignore */ }
} catch (e) { console.error("Failed to subscribe leads", e); toast.error("Failed to subscribe leads") }
setSubscribing(false)
}
@@ -78,7 +79,7 @@ export default function CampaignDetailPage({ params }: { params: Promise<{ id: s
const res = await fetch(`/api/campaigns/${id}/send`, { method: "POST" })
const data = await res.json()
if (data.sent > 0) fetchCampaign()
} catch { /* ignore */ }
} catch (e) { console.error("Failed to send campaign", e); toast.error("Failed to send campaign") }
setSending(false)
}
+2 -1
View File
@@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card } from "@/components/ui/card"
import { ArrowLeft } from "lucide-react"
import { toast } from "sonner"
export default function NewCampaignPage() {
const router = useRouter()
@@ -25,7 +26,7 @@ export default function NewCampaignPage() {
})
const data = await res.json()
if (data.id) router.push(`/campaigns/${data.id}`)
} catch { /* ignore */ }
} catch (e) { console.error("Campaign create error:", e); toast.error("Failed to create campaign") }
setSaving(false)
}
+2 -1
View File
@@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Plus, Mail, Users, Layers, Calendar } from "lucide-react"
import { toast } from "sonner"
const statusColors: Record<string, string> = {
draft: "bg-zinc-100 text-zinc-700 border-zinc-300",
@@ -24,7 +25,7 @@ export default function CampaignsPage() {
fetch("/api/campaigns")
.then(r => r.json())
.then(d => { if (Array.isArray(d)) setCampaigns(d); else setCampaigns([]) })
.catch(() => setCampaigns([]))
.catch((e) => { console.error("Failed to load campaigns", e); toast.error("Failed to load campaigns"); setCampaigns([]) })
.finally(() => setLoading(false))
}, [])
@@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Card } from "@/components/ui/card"
import { GripVertical, Plus, User, Building2, Mail, Phone, Hash } from "lucide-react"
import { toast } from "sonner"
interface KanbanLead {
id: string
@@ -57,6 +58,7 @@ export default function KanbanPage() {
const data = await res.json()
setColumns(data)
} catch (e) {
toast.error("Failed to load kanban data")
console.error("Failed to load kanban", e)
} finally {
setLoading(false)
@@ -98,6 +100,7 @@ export default function KanbanPage() {
body: JSON.stringify({ status: toStatus }),
})
} catch (e) {
toast.error("Failed to update lead status")
console.error("Failed to update lead status", e)
fetchKanban()
}
+3 -1
View File
@@ -29,6 +29,7 @@ import { ArrowLeft } from "lucide-react"
import { User } from "@/types"
import { useNotifications } from "@/providers/notification-provider"
import { LEAD_SOURCES, LEAD_STATUSES } from "@/lib/constants"
import { toast } from "sonner"
const leadFormSchema = z.object({
companyName: z.string().min(1, "Company name is required"),
@@ -53,7 +54,7 @@ export default function CreateLeadPage() {
fetch("/api/users")
.then((r) => r.json())
.then((data) => setUsers(data.users || []))
.catch(() => {})
.catch((e) => { console.error("Failed to load users", e); toast.error("Failed to load users") })
}, [])
const form = useForm<LeadFormValues>({
@@ -87,6 +88,7 @@ export default function CreateLeadPage() {
addNotification("lead_created", "New Lead Created", `${values.companyName}${values.contactName}`, `/leads/${data.id}`)
router.push("/leads")
} catch (e) {
toast.error("Failed to create lead")
console.error("Create lead error:", e)
} finally {
setSaving(false)
+2 -1
View File
@@ -7,6 +7,7 @@ import { LeadsTable } from "@/components/leads/leads-table"
import { LeadsTableToolbar } from "@/components/leads/leads-table-toolbar"
import { Button } from "@/components/ui/button"
import { Columns3 } from "lucide-react"
import { toast } from "sonner"
import { Lead } from "@/types"
export default function LeadsPage() {
@@ -30,7 +31,7 @@ export default function LeadsPage() {
setLeadsData(data)
setLoading(false)
})
.catch(() => setLoading(false))
.catch((e) => { console.error("Failed to load leads", e); toast.error("Failed to load leads"); setLoading(false) })
}, [search, statusFilter, periodFilter])
return (
+28 -12
View File
@@ -20,6 +20,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { toast } from "sonner"
interface Milestone {
id: string
@@ -82,6 +83,7 @@ export default function ProjectDetailPage() {
setMilestones(await mRes.json())
setStatuses(await sRes.json())
} catch (e) {
toast.error("Failed to load project")
console.error("Failed to load project", e)
} finally {
setLoading(false)
@@ -91,21 +93,31 @@ export default function ProjectDetailPage() {
useEffect(() => { fetchProject() }, [fetchProject])
const updateProjectStatus = async (statusId: string) => {
await fetch(`/api/projects/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ statusId }),
})
fetchProject()
try {
await fetch(`/api/projects/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ statusId }),
})
fetchProject()
} catch (e) {
console.error("Failed to update project status", e)
toast.error("Failed to update project status")
}
}
const updateMilestoneStatus = async (milestoneId: string, status: string) => {
await fetch(`/api/milestones/${milestoneId}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ status }),
})
fetchProject()
try {
await fetch(`/api/milestones/${milestoneId}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ status }),
})
fetchProject()
} catch (e) {
console.error("Failed to update milestone status", e)
toast.error("Failed to update milestone status")
}
}
const addMilestone = async () => {
@@ -119,6 +131,9 @@ export default function ProjectDetailPage() {
})
setNewMilestoneName("")
fetchProject()
} catch (e) {
console.error("Failed to add milestone", e)
toast.error("Failed to add milestone")
} finally {
setAddingMilestone(false)
}
@@ -140,6 +155,7 @@ export default function ProjectDetailPage() {
setNewTaskInputs((prev) => ({ ...prev, [milestoneId]: "" }))
fetchProject()
} catch (e) {
toast.error("Failed to add task")
console.error("Failed to add task", e)
}
}
+8 -3
View File
@@ -22,6 +22,7 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { toast } from "sonner"
interface Project {
id: string
@@ -65,6 +66,7 @@ export default function ProjectsPage() {
const data = res.ok ? await res.json() : []
setProjects(Array.isArray(data) ? data : [])
} catch (e) {
toast.error("Failed to load projects")
console.error("Failed to load projects", e)
setProjects([])
} finally {
@@ -75,7 +77,7 @@ export default function ProjectsPage() {
useEffect(() => { fetchProjects() }, [search, statusFilter])
useEffect(() => {
fetch("/api/project-statuses").then(r => r.json()).then((d) => setStatuses(Array.isArray(d) ? d : [])).catch(() => {})
fetch("/api/project-statuses").then(r => r.json()).then((d) => setStatuses(Array.isArray(d) ? d : [])).catch((e) => { console.error("Failed to load project statuses", e); toast.error("Failed to load project statuses") })
}, [])
const formatDate = (d: string) => d ? new Date(d).toLocaleDateString() : "—"
@@ -168,8 +170,8 @@ function NewProjectDialog({ statuses, onCreated }: { statuses: ProjectStatus[];
useEffect(() => {
if (!open) return
fetch("/api/customers?limit=100").then(r => r.json()).then(d => setCustomers(Array.isArray(d) ? d : [])).catch(() => {})
fetch("/api/users").then(r => r.json()).then(d => setUsers(d.users || [])).catch(() => {})
fetch("/api/customers?limit=100").then(r => r.json()).then(d => setCustomers(Array.isArray(d) ? d : [])).catch((e) => { console.error("Failed to load customers", e); toast.error("Failed to load customers") })
fetch("/api/users").then(r => r.json()).then(d => setUsers(d.users || [])).catch((e) => { console.error("Failed to load users", e); toast.error("Failed to load users") })
}, [open])
const handleSubmit = async () => {
@@ -194,6 +196,9 @@ function NewProjectDialog({ statuses, onCreated }: { statuses: ProjectStatus[];
setName(""); setDescription(""); setCustomerId(""); setOwnerId(""); setStartDate(""); setTargetEndDate(""); setBudget("")
onCreated()
}
} catch (e) {
console.error("Failed to create project", e)
toast.error("Failed to create project")
} finally {
setSaving(false)
}
+16 -6
View File
@@ -15,6 +15,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { toast } from "sonner"
export default function ProposalDetailPage() {
const { id } = useParams<{ id: string }>()
@@ -35,6 +36,7 @@ export default function ProposalDetailPage() {
setProposal(await pRes.json())
setStatuses(await sRes.json())
} catch (e) {
toast.error("Failed to load proposal")
console.error("Failed to load proposal", e)
} finally {
setLoading(false)
@@ -44,12 +46,17 @@ export default function ProposalDetailPage() {
useEffect(() => { fetchProposal() }, [fetchProposal])
const updateStatus = async (statusId: string) => {
await fetch(`/api/proposals/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ statusId }),
})
fetchProposal()
try {
await fetch(`/api/proposals/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ statusId }),
})
fetchProposal()
} catch (e) {
console.error("Failed to update proposal status", e)
toast.error("Failed to update proposal status")
}
}
const handleSign = async () => {
@@ -62,6 +69,9 @@ export default function ProposalDetailPage() {
body: JSON.stringify({ name: signName, email: signEmail }),
})
if (res.ok) fetchProposal()
} catch (e) {
console.error("Failed to sign proposal", e)
toast.error("Failed to sign proposal")
} finally {
setSigning(false)
}
+4 -2
View File
@@ -14,6 +14,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { toast } from "sonner"
interface LineItem {
id: string
@@ -39,8 +40,8 @@ export default function NewProposalPage() {
const [saving, setSaving] = useState(false)
useEffect(() => {
fetch("/api/leads?limit=100").then((r) => r.json()).then(setLeads).catch(() => {})
fetch("/api/customers?limit=100").then((r) => r.json()).then(setCustomers).catch(() => {})
fetch("/api/leads?limit=100").then((r) => r.json()).then(setLeads).catch((e) => { console.error("Failed to load leads", e); toast.error("Failed to load leads") })
fetch("/api/customers?limit=100").then((r) => r.json()).then(setCustomers).catch((e) => { console.error("Failed to load customers", e); toast.error("Failed to load customers") })
}, [])
const addItem = () => {
@@ -107,6 +108,7 @@ export default function NewProposalPage() {
router.push(`/proposals/${data.id}`)
} catch (e) {
toast.error("Failed to create proposal")
console.error("Failed to create proposal", e)
} finally {
setSaving(false)
+3 -2
View File
@@ -15,6 +15,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { toast } from "sonner"
export default function ProposalsPage() {
const router = useRouter()
@@ -31,12 +32,12 @@ export default function ProposalsPage() {
fetch(`/api/proposals?${params}`)
.then((r) => r.json())
.then(setProposals)
.catch(() => {})
.catch((e) => { console.error("Failed to load proposals", e); toast.error("Failed to load proposals") })
.finally(() => setLoading(false))
}, [search, statusFilter])
useEffect(() => {
fetch("/api/proposal-statuses").then((r) => r.json()).then(setStatuses).catch(() => {})
fetch("/api/proposal-statuses").then((r) => r.json()).then(setStatuses).catch((e) => { console.error("Failed to load proposal statuses", e); toast.error("Failed to load proposal statuses") })
}, [])
const formatCurrency = (n: number) => n ? `R${Number(n).toLocaleString()}` : "R0"
@@ -13,6 +13,7 @@ import {
SelectValue,
} from "@/components/ui/select"
import { Users, Clock, Briefcase, AlertTriangle, TrendingUp } from "lucide-react"
import { toast } from "sonner"
interface Member {
id: string
@@ -81,7 +82,7 @@ export default function ResourcePlanningPage() {
fetch(`/api/resource-planning?period=${period}`)
.then(r => r.json())
.then(d => { setMembers(d.members || []); setTotals(d.totals || null) })
.catch(() => {})
.catch((e) => { console.error("Failed to load resource data", e); toast.error("Failed to load resource data") })
.finally(() => setLoading(false))
}, [period])
+13 -2
View File
@@ -7,6 +7,7 @@ import { Input } from "@/components/ui/input"
import { Card } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Play, Square, Plus, Trash2, Clock, Briefcase, User, TrendingUp, DollarSign } from "lucide-react"
import { toast } from "sonner"
interface TimeEntry {
id: string
@@ -59,6 +60,7 @@ export default function TimeTrackingPage() {
setSummary(sData?.totals ? sData : null)
setProjects(Array.isArray(pData) ? pData : [])
} catch (e) {
toast.error("Failed to load time data")
console.error("Failed to load time data", e)
setEntries([])
setProjects([])
@@ -105,6 +107,7 @@ export default function TimeTrackingPage() {
setTimerSeconds(0)
fetchData()
} catch (e) {
toast.error("Failed to save time entry")
console.error("Failed to save time entry", e)
}
}
@@ -135,14 +138,22 @@ export default function TimeTrackingPage() {
setManualRate("")
setShowManual(false)
fetchData()
} catch (e) {
console.error("Failed to save time entry", e)
toast.error("Failed to save time entry")
} finally {
setSaving(false)
}
}
const deleteEntry = async (id: string) => {
await fetch(`/api/time-entries?id=${id}`, { method: "DELETE" })
fetchData()
try {
await fetch(`/api/time-entries?id=${id}`, { method: "DELETE" })
fetchData()
} catch (e) {
console.error("Failed to delete time entry", e)
toast.error("Failed to delete time entry")
}
}
const formatDuration = (mins: number) => {