-
-

- Edit Invoice -

-
- - {/* Customer selection */} -
- - -
- - {/* Invoice amount */} -
- -
-
- -
- -
-
- - {/* Invoice status */} -
- - -
- - {/* Submit button */} - -
-
+
+
); } diff --git a/dashboard/15-final/app/dashboard/invoices/create/page.tsx b/dashboard/15-final/app/dashboard/invoices/create/page.tsx index b614b18..b50595f 100644 --- a/dashboard/15-final/app/dashboard/invoices/create/page.tsx +++ b/dashboard/15-final/app/dashboard/invoices/create/page.tsx @@ -1,101 +1,12 @@ -import { createInvoice } from '@/app/lib/actions'; -import { fetchAllCustomers } from '@/app/lib/data'; +import { fetchCustomerNames } from '@/app/lib/data'; +import Form from '@/app/ui/invoices/create-form'; export default async function Page() { - const customers = await fetchAllCustomers(); + const customerNames = await fetchCustomerNames(); return ( -
-

- Create Invoice -

- - - {/* Customer */} -
- - -
- - {/* Amount */} -
- -
-
- -
- -
-
- - {/* Invoice Status */} -
- - -
- - {/* Submit Button */} - - -
+
+
+
); } diff --git a/dashboard/15-final/app/lib/actions.ts b/dashboard/15-final/app/lib/actions.ts index ac5c77f..06d67c7 100644 --- a/dashboard/15-final/app/lib/actions.ts +++ b/dashboard/15-final/app/lib/actions.ts @@ -5,48 +5,86 @@ import { sql } from '@vercel/postgres'; import { revalidatePath } from 'next/cache'; import { redirect } from 'next/navigation'; -const InvoiceSchema = z.object({ +const FormSchema = z.object({ id: z.string(), - customerId: z.string(), - amount: z.coerce.number(), + customerId: z.string({ + invalid_type_error: 'Please select a customer.', + }), + amount: z.coerce + .number() + .gt(0, { message: 'Please enter an amount greater than $0.' }), status: z.enum(['pending', 'paid']), date: z.string(), }); -const CreateInvoice = InvoiceSchema.omit({ id: true, date: true }); -const UpdateInvoice = InvoiceSchema.omit({ date: true }); -const DeleteInvoice = InvoiceSchema.pick({ id: true }); -export async function createInvoice(formData: FormData) { - const { customerId, amount, status } = CreateInvoice.parse({ +const CreateInvoice = FormSchema.omit({ id: true, date: true }); +const UpdateInvoice = FormSchema.omit({ date: true }); +const DeleteInvoice = FormSchema.pick({ id: true }); + +// This is temporary +export type State = { + errors?: { + customerId?: string[]; + amount?: string[]; + status?: string[]; + }; + message: string; +}; + +export async function createInvoice(prevState: State, formData: FormData) { + // Validate form fields using Zod + const validatedFields = CreateInvoice.safeParse({ customerId: formData.get('customerId'), amount: formData.get('amount'), status: formData.get('status'), }); + // If form validation fails, return errors early. Otherwise, continue. + if (!validatedFields.success) { + return { + errors: validatedFields.error.flatten().fieldErrors, + message: 'Missing Fields. Failed to Create Invoice.', + }; + } + + // Prepare data for insertion into the database + const { customerId, amount, status } = validatedFields.data; const amountInCents = amount * 100; const date = new Date().toISOString().split('T')[0]; + // Insert data into the database try { await sql` - INSERT INTO invoices (customer_id, amount, status, date) - VALUES (${customerId}, ${amountInCents}, ${status}, ${date}) - `; + INSERT INTO invoices (customer_id, amount, status, date) + VALUES (${customerId}, ${amountInCents}, ${status}, ${date}) + `; revalidatePath('/dashboard/invoices'); redirect('/dashboard/invoices'); } catch (error) { - throw new Error('Failed to Create Invoice'); + // If a database error occurs, return a more specific error. + return { + message: 'Database error: Failed to create invoice.', + }; } } -export async function updateInvoice(formData: FormData) { - const { id, customerId, amount, status } = UpdateInvoice.parse({ +export async function updateInvoice(prevState: State, formData: FormData) { + const validatedFields = UpdateInvoice.safeParse({ id: formData.get('id'), customerId: formData.get('customerId'), amount: formData.get('amount'), status: formData.get('status'), }); + if (!validatedFields.success) { + return { + errors: validatedFields.error.flatten().fieldErrors, + message: 'Missing Fields. Failed to Update Invoice.', + }; + } + + const { id, customerId, amount, status } = validatedFields.data; const amountInCents = amount * 100; try { @@ -59,7 +97,7 @@ export async function updateInvoice(formData: FormData) { revalidatePath('/dashboard/invoices'); redirect('/dashboard/invoices'); } catch (error) { - throw new Error('Failed to Update Invoice'); + return { message: 'Database error: Failed to update invoice.' }; } } @@ -73,6 +111,6 @@ export async function deleteInvoice(formData: FormData) { revalidatePath('/dashboard/invoices'); return { message: 'Deleted Invoice' }; } catch (error) { - throw new Error('Failed to Delete Invoice'); + return { message: 'Database error: Failed to delete invoice.' }; } } diff --git a/dashboard/15-final/app/lib/data.ts b/dashboard/15-final/app/lib/data.ts index 62e0811..3138221 100644 --- a/dashboard/15-final/app/lib/data.ts +++ b/dashboard/15-final/app/lib/data.ts @@ -2,9 +2,11 @@ import { sql } from '@vercel/postgres'; import { formatCurrency } from './utils'; import { Revenue, - LatestInvoice, InvoicesTable, CustomersTable, + InvoiceForm, + CustomerName, + LatestInvoiceRaw, } from './definitions'; export async function fetchRevenue() { @@ -14,11 +16,11 @@ export async function fetchRevenue() { // console.log('Fetching revenue data...'); // await new Promise((resolve) => setTimeout(resolve, 3000)); - const data = await sql`SELECT * FROM revenue`; + const data = await sql`SELECT * FROM revenue`; // console.log('Data fetch complete after 3 seconds.'); - return data.rows as Revenue[]; + return data.rows; } catch (error) { console.error('Database Error:', error); throw new Error('Failed to fetch revenue data.'); @@ -58,7 +60,7 @@ export async function fetchTotalAmountByStatus() { export async function fetchLatestInvoices() { try { - const data = await sql` + const data = await sql` SELECT invoices.amount, customers.name, customers.image_url, customers.email FROM invoices JOIN customers ON invoices.customer_id = customers.id @@ -69,7 +71,7 @@ export async function fetchLatestInvoices() { ...invoice, amount: formatCurrency(invoice.amount), })); - return latestInvoices as LatestInvoice[]; + return latestInvoices; } catch (error) { console.error('Database Error:', error); throw new Error('Failed to fetch the latest invoices.'); @@ -84,7 +86,7 @@ export async function fetchFilteredInvoices( const offset = (currentPage - 1) * itemsPerPage; try { - const data = await sql` + const data = await sql` SELECT invoices.id, invoices.amount, @@ -121,7 +123,7 @@ export async function fetchFilteredInvoices( const totalPages = Math.ceil(totalRecords / itemsPerPage); return { - invoices: data.rows as InvoicesTable[], + invoices: data.rows, totalPages, }; } catch (error) { @@ -132,7 +134,7 @@ export async function fetchFilteredInvoices( export async function fetchInvoiceById(id: string) { try { - const data = await sql` + const data = await sql` SELECT invoices.id, invoices.amount, @@ -149,23 +151,16 @@ export async function fetchInvoiceById(id: string) { amount: invoice.amount / 100, })); - return invoice[0] as - | { - id: string; - amount: number; - status: string; - name: string; - } - | undefined; + return invoice[0]; } catch (error) { console.error('Database Error:', error); throw new Error('Failed to fetch invoice.'); } } -export async function fetchAllCustomers() { +export async function fetchCustomerNames() { try { - const data = await sql` + const data = await sql` SELECT id, name @@ -174,7 +169,7 @@ export async function fetchAllCustomers() { `; const customers = data.rows; - return customers as { id: string; name: string }[]; + return customers; } catch (err) { console.error('Database Error:', err); throw new Error('Failed to fetch all customers.'); @@ -183,7 +178,7 @@ export async function fetchAllCustomers() { export async function fetchCustomersTable() { try { - const data = await sql` + const data = await sql` SELECT customers.id, customers.name, @@ -204,7 +199,7 @@ export async function fetchCustomersTable() { total_paid: formatCurrency(customer.total_paid), })); - return customers as CustomersTable[]; + return customers; } catch (err) { console.error('Database Error:', err); throw new Error('Failed to fetch customer table.'); diff --git a/dashboard/15-final/app/lib/definitions.ts b/dashboard/15-final/app/lib/definitions.ts index a9351b1..8586ef4 100644 --- a/dashboard/15-final/app/lib/definitions.ts +++ b/dashboard/15-final/app/lib/definitions.ts @@ -1,5 +1,7 @@ // This file contains type definitions for you data. -// These describe the shape of the data, and what data type each property should accept. +// It describes the shape of the data, and what data type each property should accept. +// For simplicity of teaching, we're manually defining these types. +// However, you're using an ORM such as Prisma, these types are generated automatically. export type User = { id: number; name: string; @@ -37,6 +39,11 @@ export type LatestInvoice = { amount: string; }; +// The database returns a number for amount, but we later format it to a string with the formatCurrency function +export type LatestInvoiceRaw = Omit & { + amount: number; +}; + export type InvoicesTable = { id: string; customer_id: string; @@ -54,6 +61,18 @@ export type CustomersTable = { email: string; image_url: string; total_invoices: number; - total_pending: string; - total_paid: string; + total_pending: number; + total_paid: number; +}; + +export type CustomerName = { + id: string; + name: string; +}; + +export type InvoiceForm = { + id: string; + name: string; + amount: number; + status: 'pending' | 'paid'; }; diff --git a/dashboard/15-final/app/page.tsx b/dashboard/15-final/app/page.tsx index 5af9f87..2a16413 100644 --- a/dashboard/15-final/app/page.tsx +++ b/dashboard/15-final/app/page.tsx @@ -14,14 +14,18 @@ export default function Page() { Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut vulputate dapibus consectetur. Duis quis eros euismod.

- - + + Log in
- Dashboard Hero Image + A collection of UI elements from the dashboard application.