Initial commit
This commit is contained in:
218
app/lib/data.ts
Normal file
218
app/lib/data.ts
Normal file
@ -0,0 +1,218 @@
|
||||
import postgres from 'postgres';
|
||||
import {
|
||||
CustomerField,
|
||||
CustomersTableType,
|
||||
InvoiceForm,
|
||||
InvoicesTable,
|
||||
LatestInvoiceRaw,
|
||||
Revenue,
|
||||
} from './definitions';
|
||||
import { formatCurrency } from './utils';
|
||||
|
||||
const sql = postgres(process.env.POSTGRES_URL!, { ssl: 'require' });
|
||||
|
||||
export async function fetchRevenue() {
|
||||
try {
|
||||
// Artificially delay a response for demo purposes.
|
||||
// Don't do this in production :)
|
||||
|
||||
// console.log('Fetching revenue data...');
|
||||
// await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
|
||||
const data = await sql<Revenue[]>`SELECT * FROM revenue`;
|
||||
|
||||
// console.log('Data fetch completed after 3 seconds.');
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Database Error:', error);
|
||||
throw new Error('Failed to fetch revenue data.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchLatestInvoices() {
|
||||
try {
|
||||
const data = await sql<LatestInvoiceRaw[]>`
|
||||
SELECT invoices.amount, customers.name, customers.image_url, customers.email, invoices.id
|
||||
FROM invoices
|
||||
JOIN customers ON invoices.customer_id = customers.id
|
||||
ORDER BY invoices.date DESC
|
||||
LIMIT 5`;
|
||||
|
||||
const latestInvoices = data.map((invoice) => ({
|
||||
...invoice,
|
||||
amount: formatCurrency(invoice.amount),
|
||||
}));
|
||||
return latestInvoices;
|
||||
} catch (error) {
|
||||
console.error('Database Error:', error);
|
||||
throw new Error('Failed to fetch the latest invoices.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchCardData() {
|
||||
try {
|
||||
// You can probably combine these into a single SQL query
|
||||
// However, we are intentionally splitting them to demonstrate
|
||||
// how to initialize multiple queries in parallel with JS.
|
||||
const invoiceCountPromise = sql`SELECT COUNT(*) FROM invoices`;
|
||||
const customerCountPromise = sql`SELECT COUNT(*) FROM customers`;
|
||||
const invoiceStatusPromise = 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 data = await Promise.all([
|
||||
invoiceCountPromise,
|
||||
customerCountPromise,
|
||||
invoiceStatusPromise,
|
||||
]);
|
||||
|
||||
const numberOfInvoices = Number(data[0][0].count ?? '0');
|
||||
const numberOfCustomers = Number(data[1][0].count ?? '0');
|
||||
const totalPaidInvoices = formatCurrency(data[2][0].paid ?? '0');
|
||||
const totalPendingInvoices = formatCurrency(data[2][0].pending ?? '0');
|
||||
|
||||
return {
|
||||
numberOfCustomers,
|
||||
numberOfInvoices,
|
||||
totalPaidInvoices,
|
||||
totalPendingInvoices,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Database Error:', error);
|
||||
throw new Error('Failed to fetch card data.');
|
||||
}
|
||||
}
|
||||
|
||||
const ITEMS_PER_PAGE = 6;
|
||||
export async function fetchFilteredInvoices(
|
||||
query: string,
|
||||
currentPage: number,
|
||||
) {
|
||||
const offset = (currentPage - 1) * ITEMS_PER_PAGE;
|
||||
|
||||
try {
|
||||
const invoices = await sql<InvoicesTable[]>`
|
||||
SELECT
|
||||
invoices.id,
|
||||
invoices.amount,
|
||||
invoices.date,
|
||||
invoices.status,
|
||||
customers.name,
|
||||
customers.email,
|
||||
customers.image_url
|
||||
FROM invoices
|
||||
JOIN customers ON invoices.customer_id = customers.id
|
||||
WHERE
|
||||
customers.name ILIKE ${`%${query}%`} OR
|
||||
customers.email ILIKE ${`%${query}%`} OR
|
||||
invoices.amount::text ILIKE ${`%${query}%`} OR
|
||||
invoices.date::text ILIKE ${`%${query}%`} OR
|
||||
invoices.status ILIKE ${`%${query}%`}
|
||||
ORDER BY invoices.date DESC
|
||||
LIMIT ${ITEMS_PER_PAGE} OFFSET ${offset}
|
||||
`;
|
||||
|
||||
return invoices;
|
||||
} catch (error) {
|
||||
console.error('Database Error:', error);
|
||||
throw new Error('Failed to fetch invoices.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchInvoicesPages(query: string) {
|
||||
try {
|
||||
const data = await sql`SELECT COUNT(*)
|
||||
FROM invoices
|
||||
JOIN customers ON invoices.customer_id = customers.id
|
||||
WHERE
|
||||
customers.name ILIKE ${`%${query}%`} OR
|
||||
customers.email ILIKE ${`%${query}%`} OR
|
||||
invoices.amount::text ILIKE ${`%${query}%`} OR
|
||||
invoices.date::text ILIKE ${`%${query}%`} OR
|
||||
invoices.status ILIKE ${`%${query}%`}
|
||||
`;
|
||||
|
||||
const totalPages = Math.ceil(Number(data[0].count) / ITEMS_PER_PAGE);
|
||||
return totalPages;
|
||||
} catch (error) {
|
||||
console.error('Database Error:', error);
|
||||
throw new Error('Failed to fetch total number of invoices.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchInvoiceById(id: string) {
|
||||
try {
|
||||
const data = await sql<InvoiceForm[]>`
|
||||
SELECT
|
||||
invoices.id,
|
||||
invoices.customer_id,
|
||||
invoices.amount,
|
||||
invoices.status
|
||||
FROM invoices
|
||||
WHERE invoices.id = ${id};
|
||||
`;
|
||||
|
||||
const invoice = data.map((invoice) => ({
|
||||
...invoice,
|
||||
// Convert amount from cents to dollars
|
||||
amount: invoice.amount / 100,
|
||||
}));
|
||||
|
||||
return invoice[0];
|
||||
} catch (error) {
|
||||
console.error('Database Error:', error);
|
||||
throw new Error('Failed to fetch invoice.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchCustomers() {
|
||||
try {
|
||||
const customers = await sql<CustomerField[]>`
|
||||
SELECT
|
||||
id,
|
||||
name
|
||||
FROM customers
|
||||
ORDER BY name ASC
|
||||
`;
|
||||
|
||||
return customers;
|
||||
} catch (err) {
|
||||
console.error('Database Error:', err);
|
||||
throw new Error('Failed to fetch all customers.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchFilteredCustomers(query: string) {
|
||||
try {
|
||||
const data = await sql<CustomersTableType[]>`
|
||||
SELECT
|
||||
customers.id,
|
||||
customers.name,
|
||||
customers.email,
|
||||
customers.image_url,
|
||||
COUNT(invoices.id) AS total_invoices,
|
||||
SUM(CASE WHEN invoices.status = 'pending' THEN invoices.amount ELSE 0 END) AS total_pending,
|
||||
SUM(CASE WHEN invoices.status = 'paid' THEN invoices.amount ELSE 0 END) AS total_paid
|
||||
FROM customers
|
||||
LEFT JOIN invoices ON customers.id = invoices.customer_id
|
||||
WHERE
|
||||
customers.name ILIKE ${`%${query}%`} OR
|
||||
customers.email ILIKE ${`%${query}%`}
|
||||
GROUP BY customers.id, customers.name, customers.email, customers.image_url
|
||||
ORDER BY customers.name ASC
|
||||
`;
|
||||
|
||||
const customers = data.map((customer) => ({
|
||||
...customer,
|
||||
total_pending: formatCurrency(customer.total_pending),
|
||||
total_paid: formatCurrency(customer.total_paid),
|
||||
}));
|
||||
|
||||
return customers;
|
||||
} catch (err) {
|
||||
console.error('Database Error:', err);
|
||||
throw new Error('Failed to fetch customer table.');
|
||||
}
|
||||
}
|
||||
9
app/lib/definitions.ts
Normal file
9
app/lib/definitions.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export type Invoice = {
|
||||
id: string;
|
||||
customer_id: string;
|
||||
amount: number;
|
||||
date: string;
|
||||
// In TypeScript, this is called a string union type.
|
||||
// It means that the "status" property can only be one of the two strings: 'pending' or 'paid'.
|
||||
status: 'pending' | 'paid';
|
||||
};
|
||||
15
app/lib/placeholder-data.ts
Normal file
15
app/lib/placeholder-data.ts
Normal file
@ -0,0 +1,15 @@
|
||||
const invoices = [
|
||||
{
|
||||
customer_id: customers[0].id,
|
||||
amount: 15795,
|
||||
status: 'pending',
|
||||
date: '2022-12-06',
|
||||
},
|
||||
{
|
||||
customer_id: customers[1].id,
|
||||
amount: 20348,
|
||||
status: 'pending',
|
||||
date: '2022-11-14',
|
||||
},
|
||||
// ...
|
||||
];
|
||||
69
app/lib/utils.ts
Normal file
69
app/lib/utils.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { Revenue } from './definitions';
|
||||
|
||||
export const formatCurrency = (amount: number) => {
|
||||
return (amount / 100).toLocaleString('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
});
|
||||
};
|
||||
|
||||
export const formatDateToLocal = (
|
||||
dateStr: string,
|
||||
locale: string = 'en-US',
|
||||
) => {
|
||||
const date = new Date(dateStr);
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
};
|
||||
const formatter = new Intl.DateTimeFormat(locale, options);
|
||||
return formatter.format(date);
|
||||
};
|
||||
|
||||
export const generateYAxis = (revenue: Revenue[]) => {
|
||||
// Calculate what labels we need to display on the y-axis
|
||||
// based on highest record and in 1000s
|
||||
const yAxisLabels = [];
|
||||
const highestRecord = Math.max(...revenue.map((month) => month.revenue));
|
||||
const topLabel = Math.ceil(highestRecord / 1000) * 1000;
|
||||
|
||||
for (let i = topLabel; i >= 0; i -= 1000) {
|
||||
yAxisLabels.push(`$${i / 1000}K`);
|
||||
}
|
||||
|
||||
return { yAxisLabels, topLabel };
|
||||
};
|
||||
|
||||
export const generatePagination = (currentPage: number, totalPages: number) => {
|
||||
// If the total number of pages is 7 or less,
|
||||
// display all pages without any ellipsis.
|
||||
if (totalPages <= 7) {
|
||||
return Array.from({ length: totalPages }, (_, i) => i + 1);
|
||||
}
|
||||
|
||||
// If the current page is among the first 3 pages,
|
||||
// show the first 3, an ellipsis, and the last 2 pages.
|
||||
if (currentPage <= 3) {
|
||||
return [1, 2, 3, '...', totalPages - 1, totalPages];
|
||||
}
|
||||
|
||||
// If the current page is among the last 3 pages,
|
||||
// show the first 2, an ellipsis, and the last 3 pages.
|
||||
if (currentPage >= totalPages - 2) {
|
||||
return [1, 2, '...', totalPages - 2, totalPages - 1, totalPages];
|
||||
}
|
||||
|
||||
// If the current page is somewhere in the middle,
|
||||
// show the first page, an ellipsis, the current page and its neighbors,
|
||||
// another ellipsis, and the last page.
|
||||
return [
|
||||
1,
|
||||
'...',
|
||||
currentPage - 1,
|
||||
currentPage,
|
||||
currentPage + 1,
|
||||
'...',
|
||||
totalPages,
|
||||
];
|
||||
};
|
||||
Reference in New Issue
Block a user