Fixed Projects/Proposals/Time

This commit is contained in:
JCBSComputer
2026-07-01 12:08:22 +02:00
parent b75351112e
commit cb1944a9ea
14 changed files with 50 additions and 45 deletions
+3
View File
@@ -85,5 +85,8 @@ BEGIN;
\echo '=== Running 022_custom_fields.sql (Custom Fields + Workflows) ==='
\i 022_custom_fields.sql
\echo '=== Running 023_client_portal.sql (Client Portal) ==='
\i ../../src/app/client-portal/database/020_client_portal.sql
\echo '=== Migration Complete ==='
COMMIT;
-3
View File
@@ -248,7 +248,6 @@
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"license": "MIT",
"dependencies": {
"@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2",
@@ -263,7 +262,6 @@
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
"license": "MIT",
"dependencies": {
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
@@ -277,7 +275,6 @@
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
+5 -4
View File
@@ -62,7 +62,8 @@ export default function ProjectsPage() {
if (search) params.set("search", search)
if (statusFilter !== "all") params.set("status", statusFilter)
const res = await fetch(`/api/projects?${params}`)
setProjects(await res.json())
const data = await res.json()
setProjects(Array.isArray(data) ? data : [])
} catch (e) {
console.error("Failed to load projects", e)
} finally {
@@ -73,7 +74,7 @@ export default function ProjectsPage() {
useEffect(() => { fetchProjects() }, [search, statusFilter])
useEffect(() => {
fetch("/api/project-statuses").then(r => r.json()).then(setStatuses).catch(() => {})
fetch("/api/project-statuses").then(r => r.json()).then((d) => setStatuses(Array.isArray(d) ? d : [])).catch(() => {})
}, [])
const formatDate = (d: string) => d ? new Date(d).toLocaleDateString() : "—"
@@ -166,8 +167,8 @@ function NewProjectDialog({ statuses, onCreated }: { statuses: ProjectStatus[];
useEffect(() => {
if (!open) return
fetch("/api/customers?limit=100").then(r => r.json()).then(setCustomers).catch(() => {})
fetch("/api/users").then(r => r.json()).then(setUsers).catch(() => {})
fetch("/api/customers?limit=100").then(r => r.json()).then((d) => setCustomers(Array.isArray(d) ? d : (d.customers || d.rows || []))).catch(() => {})
fetch("/api/users").then(r => r.json()).then((d) => setUsers(Array.isArray(d) ? d : (d.users || d.rows || []))).catch(() => {})
}, [open])
const handleSubmit = async () => {
+6 -3
View File
@@ -52,9 +52,12 @@ export default function TimeTrackingPage() {
fetch("/api/time-entries/summary"),
fetch("/api/projects"),
])
setEntries(await eRes.json())
setSummary(await sRes.json())
setProjects(await pRes.json())
const eData = await eRes.json()
const sData = await sRes.json()
const pData = await pRes.json()
setEntries(Array.isArray(eData) ? eData : [])
setSummary(sData?.totals ? sData : null)
setProjects(Array.isArray(pData) ? pData : [])
} catch (e) {
console.error("Failed to load time data", e)
} finally {
+1 -1
View File
@@ -71,7 +71,7 @@ export default function ClientActivity() {
<div className="relative">
<div className="absolute left-[17px] top-2 bottom-2 w-px bg-[#1a1a24]" />
<div className="space-y-0">
{events.map((e, i) => (
{(events || []).map((e, i) => (
<button
key={`${e.event_type}-${e.id}-${i}`}
onClick={() => handleClick(e)}
+3 -3
View File
@@ -42,9 +42,9 @@ export async function GET() {
)
const events = [
...milestones.rows.map((r: Record<string, unknown>) => ({ ...r, date: r.updated_at })),
...invoices.rows.map((r: Record<string, unknown>) => ({ ...r, date: r.updated_at })),
...tickets.rows.map((r: Record<string, unknown>) => ({ ...r, date: r.created_at })),
...(milestones.rows || []).map((r: Record<string, unknown>) => ({ ...r, date: r.updated_at })),
...(invoices.rows || []).map((r: Record<string, unknown>) => ({ ...r, date: r.updated_at })),
...(tickets.rows || []).map((r: Record<string, unknown>) => ({ ...r, date: r.created_at })),
].sort((a, b) => new Date(b.date as string).getTime() - new Date(a.date as string).getTime()).slice(0, 30)
return NextResponse.json({ events })
@@ -12,10 +12,11 @@ export async function GET(_request: Request, { params }: { params: Promise<{ id:
}
const project = await query(
`SELECT id, name, description, status, start_date, end_date, budget, currency,
created_at, updated_at
FROM projects
WHERE id = $1 AND customer_id = $2 AND deleted_at IS NULL`,
`SELECT p.id, p.name, p.description, p.start_date, p.target_end_date as end_date,
p.budget, p.created_at, p.updated_at, s.name as status_name, s.color as status_color
FROM projects p
LEFT JOIN project_statuses s ON s.id = p.status_id
WHERE p.id = $1 AND p.customer_id = $2 AND p.deleted_at IS NULL`,
[id, session.customerId],
)
+6 -5
View File
@@ -11,11 +11,12 @@ export async function GET() {
}
const result = await query(
`SELECT id, name, description, status, start_date, end_date, budget, currency,
created_at, updated_at
FROM projects
WHERE customer_id = $1 AND deleted_at IS NULL
ORDER BY created_at DESC`,
`SELECT p.id, p.name, p.description, p.start_date, p.target_end_date as end_date,
p.budget, p.created_at, p.updated_at, s.name as status_name, s.color as status_color
FROM projects p
LEFT JOIN project_statuses s ON s.id = p.status_id
WHERE p.customer_id = $1 AND p.deleted_at IS NULL
ORDER BY p.created_at DESC`,
[session.customerId],
)
+5 -3
View File
@@ -17,7 +17,9 @@ export default function InvoiceDetail() {
useEffect(() => {
fetch(`/client-portal/api/invoices/${id}`, { credentials: "include" })
.then((r) => r.json())
.then(setData)
.then((d) => {
if (d.invoice) setData({ ...d, items: d.items || [], receipts: d.receipts || [] })
})
.catch(() => {})
}, [id])
@@ -71,7 +73,7 @@ export default function InvoiceDetail() {
</tr>
</thead>
<tbody>
{items.map((item, i) => (
{(items || []).map((item, i) => (
<tr key={i} className="border-b border-[#1a1a24]/50">
<td className="text-sm text-white px-4 py-3">{item.description}</td>
<td className="text-sm text-white px-4 py-3 text-right">{item.quantity}</td>
@@ -116,7 +118,7 @@ export default function InvoiceDetail() {
<>
<h2 className="text-base font-semibold text-white mb-3">Receipts</h2>
<div className="space-y-2">
{receipts.map((r) => (
{(receipts || []).map((r) => (
<div key={r.id} className="bg-[#0d1117] border border-[#1a1a24] rounded-xl p-4 flex items-center justify-between">
<div className="flex items-center gap-3">
<FileText className="h-4 w-4 text-[#1BB0CE]" />
+1 -1
View File
@@ -38,7 +38,7 @@ export default function ClientInvoices() {
</div>
) : (
<div className="space-y-3">
{invoices.map((inv) => (
{(invoices || []).map((inv) => (
<button
key={inv.id}
onClick={() => router.push(`/client-portal/invoices/${inv.id}`)}
+1 -1
View File
@@ -155,7 +155,7 @@ export default function ClientProfile() {
{/* Contacts sidebar */}
<div className="space-y-3">
{contacts.map((c, i) => (
{(contacts || []).map((c, i) => (
<div key={i} className="bg-[#0d1117] border border-[#1a1a24] rounded-xl p-4">
<div className="flex items-center gap-3">
{c.type === "email" ? <Mail className="h-4 w-4 text-[#1BB0CE]" /> :
+7 -5
View File
@@ -42,7 +42,9 @@ export default function ProjectDetail() {
useEffect(() => {
fetch(`/client-portal/api/projects/${id}`, { credentials: "include" })
.then((r) => r.json())
.then(setData)
.then((d) => {
if (d.project) setData({ ...d, milestones: d.milestones || [], deliverables: d.deliverables || [] })
})
.catch(() => {})
}, [id])
@@ -60,10 +62,10 @@ export default function ProjectDetail() {
if (!prev) return prev
return {
...prev,
milestones: prev.milestones.map((m) =>
milestones: (prev.milestones || []).map((m) =>
m.id === milestoneId ? { ...m, ...updated.milestone } : m,
),
deliverables: prev.deliverables.map((d) =>
deliverables: (prev.deliverables || []).map((d) =>
d.milestone_id === milestoneId
? { ...d, status: action === "approve" ? "approved" : "rejected" }
: d,
@@ -120,7 +122,7 @@ export default function ProjectDetail() {
{milestones.length === 0 ? (
<p className="text-sm text-[#6a6a75]">No milestones yet</p>
) : (
milestones.map((m) => {
(milestones || []).map((m) => {
const si = statusIcon[m.status] || { icon: Clock, color: "#6a6a75" }
const Icon = si.icon
const milestoneDeliverables = deliverables.filter((d) => d.milestone_id === m.id)
@@ -163,7 +165,7 @@ export default function ProjectDetail() {
{milestoneDeliverables.length > 0 && (
<div className="space-y-2 mb-3">
<span className="text-[11px] text-[#6a6a75] uppercase tracking-wider">Deliverables</span>
{milestoneDeliverables.map((d) => (
{(milestoneDeliverables || []).map((d) => (
<div key={d.id} className="flex items-center justify-between bg-[#1a1a24] rounded-lg px-3 py-2">
<div className="flex items-center gap-2">
<FileText className="h-3.5 w-3.5 text-[#6a6a75]" />
+6 -11
View File
@@ -8,16 +8,11 @@ interface Project {
id: string
name: string
description: string
status: string
status_name: string
status_color: string
start_date: string
end_date: string
}
const statusColors: Record<string, string> = {
active: "bg-emerald-500/10 text-emerald-400",
on_hold: "bg-amber-500/10 text-amber-400",
completed: "bg-blue-500/10 text-blue-400",
cancelled: "bg-red-500/10 text-red-400",
budget: number
}
export default function ClientProjects() {
@@ -43,7 +38,7 @@ export default function ClientProjects() {
</div>
) : (
<div className="space-y-3">
{projects.map((p) => (
{(projects || []).map((p) => (
<button
key={p.id}
onClick={() => router.push(`/client-portal/projects/${p.id}`)}
@@ -53,8 +48,8 @@ export default function ClientProjects() {
<div className="flex-1">
<div className="flex items-center gap-3 mb-1">
<h3 className="text-white font-semibold">{p.name}</h3>
<span className={`text-[10px] font-medium px-2 py-0.5 rounded-full ${statusColors[p.status] || "bg-zinc-500/10 text-zinc-400"}`}>
{p.status.replace("_", " ")}
<span className="text-[10px] font-medium px-2 py-0.5 rounded-full" style={{ backgroundColor: `${p.status_color}15`, color: p.status_color }}>
{(p.status_name || "unknown").replace("_", " ")}
</span>
</div>
{p.description && (
+1 -1
View File
@@ -148,7 +148,7 @@ export default function ClientSupport() {
</div>
) : (
<div className="space-y-2">
{tickets.map((t) => (
{(tickets || []).map((t) => (
<div key={t.id} className="bg-[#0d1117] border border-[#1a1a24] rounded-xl p-4">
<div className="flex items-center justify-between mb-1">
<h3 className="text-white text-sm font-medium">{t.title}</h3>