Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add code for chapters 7-8 #164

Merged
merged 15 commits into from
Sep 19, 2023
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import InvoiceForm from '@/app/ui/invoices/form';
import { notFound } from 'next/navigation';
import { Invoice } from '@/app/lib/definitions';
import { fetchInvoiceById } from '@/app/lib/data-fetches';
import { fetchInvoiceById } from '@/app/lib/data';

export default async function Page({ params }: { params: { id: string } }) {
const id = params.id ? parseInt(params.id) : null;
Expand Down
5 changes: 5 additions & 0 deletions dashboard/15-final/app/dashboard/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import DashboardSkeleton from '@/app/ui/skeletons';

export default function Loading() {
return <DashboardSkeleton />;
}
37 changes: 34 additions & 3 deletions dashboard/15-final/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,40 @@
import DashboardOverview from '@/app/ui/dashboard/overview';
import Card from '@/app/ui/dashboard/card';
import RevenueChart from '@/app/ui/dashboard/revenue-chart';
import LatestInvoices from '@/app/ui/dashboard/latest-invoices';
import {
fetchLatestInvoices,
fetchCounts,
fetchTotalAmountByStatus,
} from '@/app/lib/data';
import { Suspense } from 'react';
import { RevenueChartSkeleton } from '@/app/ui/skeletons';

export const dynamic = 'force-dynamic';

export default async function Page() {
const latestInvoices = await fetchLatestInvoices();
const { numberOfInvoices, numberOfCustomers } = await fetchCounts();
const { totalPaidInvoices, totalPendingInvoices } =
await fetchTotalAmountByStatus();

export default function Page() {
return (
<main>
<DashboardOverview />
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
<Card title="Collected" value={totalPaidInvoices} type="collected" />
<Card title="Pending" value={totalPendingInvoices} type="pending" />
<Card title="Total Invoices" value={numberOfInvoices} type="invoices" />
<Card
title="Total Customers"
value={numberOfCustomers}
type="customers"
/>
</div>
<div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8">
<Suspense fallback={<RevenueChartSkeleton />}>
<RevenueChart />
</Suspense>
<LatestInvoices latestInvoices={latestInvoices} />
</div>
</main>
);
}
65 changes: 0 additions & 65 deletions dashboard/15-final/app/lib/data-fetches.ts

This file was deleted.

132 changes: 132 additions & 0 deletions dashboard/15-final/app/lib/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { sql } from '@vercel/postgres';
import { formatCurrency } from './utils';
import { Revenue, LatestInvoice } from './definitions';

export async function fetchRevenue(): Promise<Revenue[]> {
try {
const revenueData = await sql`SELECT * FROM revenue`;
return revenueData.rows as Revenue[];
} catch (error) {
console.error('Failed to fetch revenue data:', error);
throw new Error('Failed to fetch revenue data.');
}
}

export async function fetchRevenueDelayed(): Promise<Revenue[]> {
try {
// We artificially delay a reponse for demo purposes.
// Don't do this in real life :)
console.log('Fetching revenue data...');
await new Promise((resolve) => setTimeout(resolve, 3000));

const revenueData = await sql`SELECT * FROM revenue`;
console.log('Data fetch complete after 3 seconds.');

return revenueData.rows as Revenue[];
} catch (error) {
console.error('Failed to fetch revenue data:', error);
throw new Error('Failed to fetch revenue data.');
}
}

export async function fetchCounts() {
try {
const invoiceCount = await sql`SELECT COUNT(*) FROM invoices`;
const numberOfInvoices = parseInt(invoiceCount.rows[0].count, 10);

const customerCount = await sql`SELECT COUNT(*) FROM customers`;
const numberOfCustomers = parseInt(customerCount.rows[0].count, 10);

return { numberOfCustomers, numberOfInvoices };
} catch (error) {
console.error('Failed to fetch counts:', error);
throw new Error('Failed to fetch counts.');
}
}

export async function fetchTotalAmountByStatus() {
try {
const totalAmount = await sql`SELECT
SUM(CASE WHEN status = 'paid' THEN amount ELSE 0 END) AS "paid",
SUM(CASE WHEN status = 'pending' THEN amount ELSE 0 END) AS "pending"
FROM invoices`;
const totalPaidInvoices = formatCurrency(totalAmount.rows[0].paid);
const totalPendingInvoices = formatCurrency(totalAmount.rows[0].pending);

return { totalPaidInvoices, totalPendingInvoices };
} catch (error) {
console.error('Failed to fetch total amounts by status:', error);
throw new Error('Failed to fetch total amounts by status.');
}
}

export async function fetchLatestInvoices() {
try {
const data = await sql`
SELECT invoices.amount, customers.name, customers.image_url, customers.email
FROM invoices
JOIN customers ON invoices.customer_id = customers.id
ORDER BY invoices.date DESC
LIMIT 5`;
const latestInvoices = data.rows.map((invoice) => ({
...invoice,
amount: formatCurrency(invoice.amount),
}));
return latestInvoices as LatestInvoice[];
} catch (error) {
console.error('Failed to fetch the latest invoices:', error);
throw new Error('Failed to fetch the latest invoices.');
}
}

export async function fetchAllInvoices() {
const invoicesData = await sql`SELECT * FROM invoices`;
return invoicesData.rows;
}

export async function fetchAllCustomers() {
const customersData = await sql`SELECT * FROM customers`;
return customersData.rows;
}

export async function fetchFilteredInvoices(
searchTerm: string,
currentPage: number,
ITEMS_PER_PAGE: number,
) {
const invoicesData = await sql`
SELECT
invoices.*,
customers.name AS customer_name,
customers.email AS customer_email,
customers.image_url AS customer_image
FROM
invoices
JOIN
customers ON invoices.customer_id = customers.id
WHERE
invoices.id::text ILIKE ${`%${searchTerm}%`} OR
customers.name ILIKE ${`%${searchTerm}%`} OR
customers.email ILIKE ${`%${searchTerm}%`} OR
invoices.amount::text ILIKE ${`%${searchTerm}%`} OR
invoices.date::text ILIKE ${`%${searchTerm}%`} OR
invoices.status ILIKE ${`%${searchTerm}%`}
LIMIT ${ITEMS_PER_PAGE}
OFFSET ${(currentPage - 1) * ITEMS_PER_PAGE}
`;
return invoicesData.rows;
}

export async function fetchInvoiceCountBySearchTerm(searchTerm: string) {
const { rows: countRows } = await sql`
SELECT COUNT(*)
FROM invoices
LEFT JOIN customers ON invoices.customer_id = customers.id
WHERE (invoices.id::text ILIKE ${`%${searchTerm}%`} OR customers.name ILIKE ${`%${searchTerm}%`} OR customers.email ILIKE ${`%${searchTerm}%`})
`;
return countRows[0].count;
}

export async function fetchInvoiceById(id: number | null) {
return await sql`SELECT * from INVOICES where id=${id}`;
}
8 changes: 8 additions & 0 deletions dashboard/15-final/app/lib/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ export type Invoice = {
date: string;
};

export type LatestInvoice = {
id: number;
name: string;
image_url: string;
email: string;
amount: string;
};

export type Revenue = {
month: string;
revenue: number;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,43 @@
import { Invoice, Revenue } from './definitions';
import { fetchLatestInvoices } from './data-fetches';
import { Invoice, Revenue, Customer, LatestInvoice } from './definitions';

export const calculateAllInvoices = (
export const formatCurrency = (amount: number) => {
return (amount / 100).toLocaleString('en-US', {
style: 'currency',
currency: 'USD',
});
};

export function findLatestInvoices(invoices: Invoice[], customers: Customer[]) {
// Sort the invoices by date in descending order and take the top 5
const latestInvoices = [...invoices]
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
.slice(0, 5);

// Find corresponding customers for the latest 5 invoices
const latestInvoicesWithCustomerInfo = latestInvoices.map((invoice) => {
const customer = customers.find(
(customer) => customer.id === invoice.customer_id,
);

// Format the amount to USD
const formattedAmount = invoice.amount.toLocaleString('en-US', {
style: 'currency',
currency: 'USD',
});

return {
id: invoice.id,
name: customer?.name,
image_url: customer?.image_url,
email: customer?.email,
amount: formattedAmount,
};
});

return latestInvoicesWithCustomerInfo;
}

export const calculateInvoicesByStatus = (
invoices: Invoice[],
status: 'pending' | 'paid',
) => {
Expand Down Expand Up @@ -29,12 +65,6 @@ export const calculateCustomerInvoices = (
});
};

// Once a database is connected, we can use SQL to query the database directly
// This will be more efficient than querying all invoices and then filtering them
// E.g. "SELECT * FROM invoices
// ORDER BY date DESC
// LIMIT 5;"

export const countCustomerInvoices = (
invoices: Invoice[],
customerId: number,
Expand All @@ -43,10 +73,6 @@ export const countCustomerInvoices = (
.length;
};

export const findLatestInvoices = async () => {
return await fetchLatestInvoices();
};

export const generateYAxis = (revenue: Revenue[]) => {
// Calculate what labels we need to display on the y-axis
// based on highest record and in 1000s
Expand Down
4 changes: 2 additions & 2 deletions dashboard/15-final/app/ui/customers/table.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {
countCustomerInvoices,
calculateCustomerInvoices,
} from '@/app/lib/calculations';
} from '@/app/lib/utils';
import { Customer, Invoice } from '@/app/lib/definitions';
import { fetchAllCustomers, fetchAllInvoices } from '@/app/lib/data-fetches';
import { fetchAllCustomers, fetchAllInvoices } from '@/app/lib/data';
import Image from 'next/image';

export default async function CustomersTable() {
Expand Down
Loading
Loading