Add descriptive error messages across all API routes and toast notifications on client pages
Build & Auto-Repair / build (push) Has been cancelled
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:
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user