Skip to content

Commit b9323ae

Browse files
committed
feat: Implement invoice management features including view, edit, and create functionalities
- Added server-side logic for loading and displaying individual invoices. - Enhanced invoice view page with detailed information and improved styling. - Created edit functionality for invoices with form validation and data handling. - Developed new invoice creation page with dynamic line item management and validation. - Integrated account selection and status management in invoice forms.
1 parent 830c24b commit b9323ae

File tree

10 files changed

+814
-297
lines changed

10 files changed

+814
-297
lines changed

src/routes/(app)/app/cases/[caseId]/+page.svelte

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
<script>
1+
<script lang="ts">
22
import { Briefcase, Edit3, Trash2, Clock, User, Building, Calendar, AlertCircle, MessageCircle, Send, CheckCircle, RotateCcw, XCircle } from '@lucide/svelte';
33
44
export let data;
55
let comment = '';
66
let errorMsg = '';
77
8-
function getPriorityColor(priority) {
8+
function getPriorityColor(priority: string) {
99
switch (priority) {
1010
case 'High': return 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300 border-red-200 dark:border-red-800';
1111
case 'Medium': return 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-300 border-yellow-200 dark:border-yellow-800';
@@ -14,7 +14,7 @@
1414
}
1515
}
1616
17-
function getStatusColor(status) {
17+
function getStatusColor(status: string) {
1818
switch (status) {
1919
case 'OPEN': return 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 border-blue-200 dark:border-blue-800';
2020
case 'IN_PROGRESS': return 'bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300 border-orange-200 dark:border-orange-800';
@@ -23,7 +23,7 @@
2323
}
2424
}
2525
26-
function getStatusIcon(status) {
26+
function getStatusIcon(status: string) {
2727
switch (status) {
2828
case 'OPEN': return AlertCircle;
2929
case 'IN_PROGRESS': return RotateCcw;

src/routes/(app)/app/cases/[caseId]/edit/+page.svelte

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<script>
1+
<script lang="ts">
22
export let data;
33
import { enhance } from '$app/forms';
44
import { goto } from '$app/navigation';
@@ -9,10 +9,11 @@
99
let accountId = data.caseItem.accountId;
1010
let dueDate = '';
1111
if (data.caseItem.dueDate) {
12-
if (typeof data.caseItem.dueDate === 'string') {
13-
dueDate = data.caseItem.dueDate.split('T')[0];
14-
} else if (data.caseItem.dueDate instanceof Date) {
15-
dueDate = data.caseItem.dueDate.toISOString().split('T')[0];
12+
const dueDateValue: any = data.caseItem.dueDate;
13+
if (typeof dueDateValue === 'string') {
14+
dueDate = dueDateValue.split('T')[0];
15+
} else if (dueDateValue instanceof Date) {
16+
dueDate = dueDateValue.toISOString().split('T')[0];
1617
}
1718
}
1819
let assignedId = data.caseItem.ownerId;
@@ -28,7 +29,10 @@
2829
}
2930
3031
function confirmCloseCase() {
31-
document.getElementById('close-case-form').submit();
32+
const form = document.getElementById('close-case-form');
33+
if (form && 'submit' in form && typeof form.submit === 'function') {
34+
form.submit();
35+
}
3236
showCloseConfirmation = false;
3337
}
3438
@@ -41,7 +45,10 @@
4145
}
4246
4347
function confirmReopenCase() {
44-
document.getElementById('reopen-case-form').submit();
48+
const form = document.getElementById('reopen-case-form');
49+
if (form && 'submit' in form && typeof form.submit === 'function') {
50+
form.submit();
51+
}
4552
showReopenConfirmation = false;
4653
}
4754
@@ -80,7 +87,7 @@
8087
return async ({ result, update }) => {
8188
loading = false;
8289
if (result.type === 'failure') {
83-
errorMsg = result.data?.error || 'An error occurred';
90+
errorMsg = (result.data as any)?.error || 'An error occurred';
8491
} else if (result.type === 'success') {
8592
successMsg = 'Case updated successfully!';
8693
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { redirect } from '@sveltejs/kit';
2+
import prisma from '$lib/prisma';
3+
4+
/** @type {import('./$types').PageServerLoad} */
5+
export async function load({ locals }) {
6+
if (!locals.user || !locals.org) {
7+
throw redirect(302, '/login');
8+
}
9+
10+
// Get quotes that serve as invoices for this organization
11+
const invoices = await prisma.quote.findMany({
12+
where: {
13+
organizationId: locals.org.id
14+
},
15+
include: {
16+
account: {
17+
select: {
18+
id: true,
19+
name: true
20+
}
21+
},
22+
lineItems: {
23+
include: {
24+
product: {
25+
select: {
26+
name: true
27+
}
28+
}
29+
}
30+
}
31+
},
32+
orderBy: {
33+
createdAt: 'desc'
34+
}
35+
});
36+
37+
return {
38+
invoices
39+
};
40+
};
Lines changed: 98 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,108 +1,134 @@
1+
<script>
2+
/** @type {import('./$types').PageData} - for external reference */
3+
export let data;
4+
5+
/**
6+
* @param {string} status
7+
*/
8+
function getStatusClass(status) {
9+
/** @type {{ [key: string]: string }} */
10+
const classes = {
11+
'ACCEPTED': 'bg-green-100 text-green-700',
12+
'PRESENTED': 'bg-blue-100 text-blue-700',
13+
'DRAFT': 'bg-gray-100 text-gray-700',
14+
'APPROVED': 'bg-purple-100 text-purple-700',
15+
'REJECTED': 'bg-red-100 text-red-700'
16+
};
17+
return classes[status] || 'bg-gray-100 text-gray-700';
18+
}
19+
20+
/**
21+
* @param {number} amount
22+
*/
23+
function formatCurrency(amount) {
24+
return new Intl.NumberFormat('en-US', {
25+
style: 'currency',
26+
currency: 'USD'
27+
}).format(amount);
28+
}
29+
</script>
30+
131
<!-- Super Rich Invoice List Page - Uniform Blue-Purple Theme -->
232
<div class="min-h-screen bg-gradient-to-br from-blue-100 to-purple-100 p-8">
333
<div class="max-w-5xl mx-auto">
434
<div class="flex justify-between items-center mb-10">
535
<h1 class="text-4xl font-extrabold text-blue-900 tracking-tight">Invoices</h1>
6-
<a href="." class="inline-flex items-center px-6 py-3 bg-gradient-to-r from-blue-700 to-purple-700 text-white text-lg font-semibold rounded-xl shadow-lg hover:from-blue-800 hover:to-purple-800 transition">+ New Invoice</a>
36+
<a href="/app/invoices/new" class="inline-flex items-center px-6 py-3 bg-gradient-to-r from-blue-700 to-purple-700 text-white text-lg font-semibold rounded-xl shadow-lg hover:from-blue-800 hover:to-purple-800 transition">+ New Invoice</a>
737
</div>
38+
39+
<!-- Search and Filter Controls -->
840
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-8">
941
<!-- Search -->
1042
<div class="flex-1 flex items-center bg-white/80 backdrop-blur-md rounded-xl shadow px-4 py-2 border border-blue-200">
11-
<svg class="w-5 h-5 text-blue-400 mr-2" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
43+
<svg class="w-5 h-5 text-blue-400 mr-2" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
44+
<circle cx="11" cy="11" r="8"/>
45+
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
46+
</svg>
1247
<label for="invoice-search" class="sr-only">Search invoices</label>
13-
<input id="invoice-search" type="text" placeholder="Search invoices..." class="bg-transparent outline-none flex-1 text-blue-900 placeholder-blue-400" />
48+
<input
49+
id="invoice-search"
50+
type="text"
51+
placeholder="Search invoices..."
52+
class="bg-transparent outline-none flex-1 text-blue-900 placeholder-blue-400" />
1453
</div>
54+
1555
<!-- Status Filter -->
1656
<div class="flex items-center bg-white/80 backdrop-blur-md rounded-xl shadow px-4 py-2 border border-blue-200">
17-
<svg class="w-5 h-5 text-purple-400 mr-2" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><rect x="3" y="7" width="18" height="13" rx="2"/><path d="M16 3v4M8 3v4"/></svg>
57+
<svg class="w-5 h-5 text-purple-400 mr-2" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
58+
<rect x="3" y="7" width="18" height="13" rx="2"/>
59+
<path d="M16 3v4M8 3v4"/>
60+
</svg>
1861
<label for="invoice-status-filter" class="sr-only">Filter by status</label>
19-
<select id="invoice-status-filter" class="bg-transparent outline-none text-blue-900 font-semibold">
62+
<select
63+
id="invoice-status-filter"
64+
class="bg-transparent outline-none text-blue-900 font-semibold">
2065
<option>All Statuses</option>
2166
<option>Paid</option>
2267
<option>Unpaid</option>
2368
<option>Overdue</option>
2469
</select>
2570
</div>
71+
2672
<!-- Date Range -->
2773
<div class="flex items-center bg-white/80 backdrop-blur-md rounded-xl shadow px-4 py-2 border border-blue-200">
28-
<svg class="w-5 h-5 text-blue-400 mr-2" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>
74+
<svg class="w-5 h-5 text-blue-400 mr-2" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
75+
<rect x="3" y="4" width="18" height="18" rx="2"/>
76+
<path d="M16 2v4M8 2v4M3 10h18"/>
77+
</svg>
2978
<label for="invoice-date-range" class="sr-only">Date range filter</label>
30-
<input id="invoice-date-range" type="text" placeholder="Date range" class="bg-transparent outline-none text-blue-900 placeholder-blue-400 w-28" />
79+
<input
80+
id="invoice-date-range"
81+
type="text"
82+
placeholder="Date range"
83+
class="bg-transparent outline-none text-blue-900 placeholder-blue-400 w-28" />
3184
</div>
3285
</div>
86+
87+
<!-- Invoice Cards -->
3388
<div class="flex flex-col gap-5">
34-
<!-- Invoice Card 1 -->
35-
<div class="bg-white/80 backdrop-blur-md rounded-2xl shadow-xl p-5 border-t-8 border-blue-600 relative overflow-hidden flex flex-col md:flex-row md:items-center md:justify-between gap-4">
36-
<div class="flex-1 flex flex-col md:flex-row md:items-center gap-4">
37-
<div class="flex flex-col gap-1 min-w-[120px]">
38-
<span class="text-xs font-bold uppercase tracking-widest text-blue-700 bg-blue-100 px-2 py-0.5 rounded-full w-fit">Unpaid</span>
39-
<span class="text-blue-500 text-xs">Due: 15 Apr 2025</span>
40-
</div>
41-
<div class="flex-1">
42-
<h2 class="text-xl font-bold text-blue-900 mb-0.5">INV-001</h2>
43-
<p class="text-blue-500 mb-1 text-sm">Acme Corp</p>
44-
<div class="mb-1">
45-
<div class="flex justify-between text-blue-700 text-xs mb-0.5">
46-
<span>Service</span><span>$1,000</span>
47-
</div>
48-
<div class="flex justify-between text-blue-700 text-xs mb-0.5">
49-
<span>Hosting</span><span>$200</span>
89+
{#each data.invoices as invoice}
90+
<div class="bg-white/80 backdrop-blur-md rounded-2xl shadow-xl p-5 border-t-8 border-blue-600 relative overflow-hidden flex flex-col md:flex-row md:items-center md:justify-between gap-4">
91+
<div class="flex-1 flex flex-col md:flex-row md:items-center gap-4">
92+
<div class="flex flex-col gap-1 min-w-[120px]">
93+
<span class="text-xs font-bold uppercase tracking-widest px-2 py-0.5 rounded-full w-fit {getStatusClass(invoice.status)}">{invoice.status.toLowerCase()}</span>
94+
<span class="text-blue-500 text-xs">Due: {invoice.expirationDate ? new Date(invoice.expirationDate).toLocaleDateString() : 'N/A'}</span>
95+
</div>
96+
<div class="flex-1">
97+
<h2 class="text-xl font-bold text-blue-900 mb-0.5">{invoice.quoteNumber}</h2>
98+
<p class="text-blue-500 mb-1 text-sm">{invoice.account.name}</p>
99+
<div class="mb-1">
100+
{#each invoice.lineItems as item}
101+
<div class="flex justify-between text-blue-700 text-xs mb-0.5">
102+
<span>{item.description || item.product?.name}</span>
103+
<span>{formatCurrency(Number(item.totalPrice))}</span>
104+
</div>
105+
{/each}
50106
</div>
51107
</div>
52108
</div>
53-
</div>
54-
<div class="flex flex-col items-end gap-2 min-w-[120px]">
55-
<div class="flex items-center gap-1">
56-
<span class="font-bold text-blue-800 text-sm">Total</span>
57-
<span class="text-base font-extrabold text-purple-700">$1,200</span>
58-
</div>
59-
<div class="flex gap-2">
60-
<button class="px-4 py-1.5 bg-gradient-to-r from-blue-600 to-purple-600 text-white rounded-full shadow hover:from-blue-700 hover:to-purple-700 transition font-semibold text-sm">Edit</button>
61-
<button class="px-4 py-1.5 bg-blue-100 text-blue-700 rounded-full font-semibold hover:bg-blue-200 transition text-sm">Download</button>
62-
</div>
63-
</div>
64-
<div class="absolute right-4 top-1 opacity-10 text-[5rem] font-black text-blue-200 select-none pointer-events-none">💸</div>
65-
</div>
66-
<!-- Invoice Card 2 -->
67-
<div class="bg-white/80 backdrop-blur-md rounded-2xl shadow-xl p-5 border-t-8 border-green-500 relative overflow-hidden flex flex-col md:flex-row md:items-center md:justify-between gap-4">
68-
<div class="flex-1 flex flex-col md:flex-row md:items-center gap-4">
69-
<div class="flex flex-col gap-1 min-w-[120px]">
70-
<span class="text-xs font-bold uppercase tracking-widest text-green-700 bg-green-100 px-2 py-0.5 rounded-full w-fit">Paid</span>
71-
<span class="text-green-500 text-xs">Due: 10 Mar 2025</span>
72-
</div>
73-
<div class="flex-1">
74-
<h2 class="text-xl font-bold text-green-900 mb-0.5">INV-002</h2>
75-
<p class="text-green-500 mb-1 text-sm">Beta LLC</p>
76-
<div class="mb-1">
77-
<div class="flex justify-between text-green-700 text-xs mb-0.5">
78-
<span>Design</span><span>$800</span>
79-
</div>
109+
<div class="text-right flex-shrink-0">
110+
<div class="text-2xl font-extrabold text-purple-700 mb-1">{formatCurrency(Number(invoice.grandTotal))}</div>
111+
<div class="flex gap-2">
112+
<a href="/app/invoices/{invoice.id}" class="px-3 py-1 bg-blue-600 text-white rounded-full text-xs font-semibold hover:bg-blue-700 transition">View</a>
113+
<a href="/app/invoices/{invoice.id}/edit" class="px-3 py-1 bg-purple-600 text-white rounded-full text-xs font-semibold hover:bg-purple-700 transition">Edit</a>
80114
</div>
81115
</div>
116+
<!-- Decorative gradient -->
117+
<div class="absolute top-0 right-0 w-32 h-32 bg-gradient-to-bl from-purple-200/30 to-transparent rounded-full -translate-y-16 translate-x-16"></div>
82118
</div>
83-
<div class="flex flex-col items-end gap-2 min-w-[120px]">
84-
<div class="flex items-center gap-1">
85-
<span class="font-bold text-green-800 text-sm">Total</span>
86-
<span class="text-base font-extrabold text-green-700">$800</span>
87-
</div>
88-
<div class="flex gap-2">
89-
<button class="px-4 py-1.5 bg-gradient-to-r from-green-600 to-green-400 text-white rounded-full shadow hover:from-green-700 hover:to-green-500 transition font-semibold text-sm">View</button>
90-
</div>
119+
{/each}
120+
121+
<!-- Empty State -->
122+
{#if data.invoices.length === 0}
123+
<div class="bg-white/80 backdrop-blur-md rounded-2xl shadow-xl p-12 text-center">
124+
<div class="text-6xl mb-4">📄</div>
125+
<h3 class="text-2xl font-bold text-blue-900 mb-2">No invoices yet</h3>
126+
<p class="text-blue-600 mb-6">Create your first invoice to get started</p>
127+
<a href="/app/invoices/new" class="inline-flex items-center px-6 py-3 bg-gradient-to-r from-blue-700 to-purple-700 text-white font-semibold rounded-xl shadow-lg hover:from-blue-800 hover:to-purple-800 transition">
128+
Create Invoice
129+
</a>
91130
</div>
92-
<div class="absolute right-4 top-1 opacity-10 text-[5rem] font-black text-green-200 select-none pointer-events-none">✅</div>
93-
</div>
94-
</div>
95-
<!-- Pagination -->
96-
<div class="max-w-5xl mx-auto mt-12 flex justify-center">
97-
<nav class="inline-flex items-center space-x-2 bg-white/80 backdrop-blur-md rounded-xl shadow px-6 py-3 border border-blue-200">
98-
<button class="px-3 py-1 rounded-full text-blue-400 hover:bg-blue-100 transition" disabled>&laquo;</button>
99-
<button class="px-3 py-1 rounded-full bg-gradient-to-r from-blue-600 to-purple-600 text-white font-bold shadow">1</button>
100-
<button class="px-3 py-1 rounded-full text-blue-700 hover:bg-blue-100 transition">2</button>
101-
<button class="px-3 py-1 rounded-full text-blue-700 hover:bg-blue-100 transition">3</button>
102-
<span class="px-2 text-blue-400">...</span>
103-
<button class="px-3 py-1 rounded-full text-blue-700 hover:bg-blue-100 transition">10</button>
104-
<button class="px-3 py-1 rounded-full text-blue-700 hover:bg-blue-100 transition">&raquo;</button>
105-
</nav>
131+
{/if}
106132
</div>
107133
</div>
108134
</div>
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { error, redirect } from '@sveltejs/kit';
2+
import prisma from '$lib/prisma';
3+
4+
/** @type {import('./$types').PageServerLoad} */
5+
export async function load({ params, locals }) {
6+
if (!locals.user || !locals.org) {
7+
throw redirect(302, '/login');
8+
}
9+
10+
const invoice = await prisma.quote.findFirst({
11+
where: {
12+
id: params.invoiceId,
13+
organizationId: locals.org.id
14+
},
15+
include: {
16+
account: {
17+
select: {
18+
id: true,
19+
name: true,
20+
street: true,
21+
city: true,
22+
state: true,
23+
postalCode: true,
24+
country: true
25+
}
26+
},
27+
contact: {
28+
select: {
29+
firstName: true,
30+
lastName: true,
31+
email: true
32+
}
33+
},
34+
lineItems: {
35+
include: {
36+
product: {
37+
select: {
38+
name: true,
39+
code: true
40+
}
41+
}
42+
},
43+
orderBy: {
44+
id: 'asc'
45+
}
46+
},
47+
preparedBy: {
48+
select: {
49+
name: true,
50+
email: true
51+
}
52+
}
53+
}
54+
});
55+
56+
if (!invoice) {
57+
throw error(404, 'Invoice not found');
58+
}
59+
60+
return {
61+
invoice
62+
};
63+
};

0 commit comments

Comments
 (0)