feature/research-di #1
@ -1,8 +1,5 @@
|
|||||||
import { sql } from '@/bootstrap/db/db';
|
import { sql } from '@/bootstrap/db/db';
|
||||||
import {
|
import {
|
||||||
CustomersTableType,
|
|
||||||
InvoiceForm,
|
|
||||||
InvoicesTable,
|
|
||||||
Revenue,
|
Revenue,
|
||||||
Invoice,
|
Invoice,
|
||||||
Customer,
|
Customer,
|
||||||
@ -97,127 +94,3 @@ export async function fetchCardData() {
|
|||||||
throw new Error('Failed to fetch card data.');
|
throw new Error('Failed to fetch card data.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ITEMS_PER_PAGE = 6;
|
|
||||||
export async function fetchFilteredInvoices(
|
|
||||||
query: string,
|
|
||||||
currentPage: number,
|
|
||||||
) {
|
|
||||||
// This is equivalent to in fetch(..., {cache: 'no-store'}).
|
|
||||||
connection()
|
|
||||||
|
|
||||||
const offset = (currentPage - 1) * ITEMS_PER_PAGE;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const invoices = await sql`
|
|
||||||
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}
|
|
||||||
` as postgres.RowList<InvoicesTable[]>;
|
|
||||||
|
|
||||||
return invoices;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Database Error:', error);
|
|
||||||
throw new Error('Failed to fetch invoices.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchInvoicesPages(query: string) {
|
|
||||||
// This is equivalent to in fetch(..., {cache: 'no-store'}).
|
|
||||||
connection()
|
|
||||||
try {
|
|
||||||
const count = 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(count.at(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) {
|
|
||||||
// This is equivalent to in fetch(..., {cache: 'no-store'}).
|
|
||||||
connection()
|
|
||||||
try {
|
|
||||||
const data = await sql`
|
|
||||||
SELECT
|
|
||||||
invoices.id,
|
|
||||||
invoices.customer_id,
|
|
||||||
invoices.amount,
|
|
||||||
invoices.status
|
|
||||||
FROM invoices
|
|
||||||
WHERE invoices.id = ${id};
|
|
||||||
` as postgres.RowList<InvoiceForm[]>;
|
|
||||||
|
|
||||||
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 fetchFilteredCustomers(query: string) {
|
|
||||||
// This is equivalent to in fetch(..., {cache: 'no-store'}).
|
|
||||||
connection()
|
|
||||||
try {
|
|
||||||
const data = await sql`
|
|
||||||
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
|
|
||||||
` as postgres.RowList<CustomersTableType[]>;
|
|
||||||
|
|
||||||
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.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -26,42 +26,6 @@ export type LatestInvoice = {
|
|||||||
amount: string;
|
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<LatestInvoice, 'amount'> & {
|
|
||||||
amount: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type InvoicesTable = {
|
|
||||||
id: string;
|
|
||||||
customer_id: string;
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
image_url: string;
|
|
||||||
date: string;
|
|
||||||
amount: number;
|
|
||||||
status: 'pending' | 'paid';
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CustomersTableType = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
image_url: string;
|
|
||||||
total_invoices: number;
|
|
||||||
total_pending: number;
|
|
||||||
total_paid: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type FormattedCustomersTable = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
image_url: string;
|
|
||||||
total_invoices: number;
|
|
||||||
total_pending: string;
|
|
||||||
total_paid: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type InvoiceForm = {
|
export type InvoiceForm = {
|
||||||
id: string;
|
id: string;
|
||||||
customer_id: string;
|
customer_id: string;
|
||||||
|
16
src/feature/customer-invoice/data/module/customer-di.ts
Normal file
16
src/feature/customer-invoice/data/module/customer-di.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import di from "@/bootstrap/di/init-di";
|
||||||
|
import CustomerDbRepo from "@/feature/customer/data/repo/customer-db-repo";
|
||||||
|
import { customerRepoKey } from "@/feature/customer/domain/i-repo/customer-repo";
|
||||||
|
import fetchCustomersUsecase from "@/feature/customer/domain/usecase/fetch-customers-usecase";
|
||||||
|
import { DependencyContainer } from "tsyringe";
|
||||||
|
|
||||||
|
export default function getCustomerDi(): DependencyContainer {
|
||||||
|
const customerDi = di.createChildContainer()
|
||||||
|
|
||||||
|
customerDi.register(fetchCustomersUsecase.name, {
|
||||||
|
useValue: fetchCustomersUsecase
|
||||||
|
})
|
||||||
|
|
||||||
|
customerDi.register(customerRepoKey, CustomerDbRepo)
|
||||||
|
return customerDi
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
import { formatCurrency } from "@/app/lib/utils";
|
||||||
|
import { sql } from "@/bootstrap/db/db";
|
||||||
|
import CustomerInvoice from "@/feature/customer-invoice/domain/entity/customer-invoice";
|
||||||
|
import CustomerInvoiceRepo from "@/feature/customer-invoice/domain/i-repo/customer-invoice-repo";
|
||||||
|
import { connection } from "next/server";
|
||||||
|
import postgres from "postgres";
|
||||||
|
|
||||||
|
type customerInvoiceDbResponse = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
image_url: string;
|
||||||
|
email: string;
|
||||||
|
amount: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class CustomerDbRepo implements CustomerInvoiceRepo {
|
||||||
|
async fetchList(): Promise<CustomerInvoice[]> {
|
||||||
|
// This is equivalent to in fetch(..., {cache: 'no-store'}).
|
||||||
|
connection()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await sql`
|
||||||
|
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` as postgres.RowList<customerInvoiceDbResponse[]>;
|
||||||
|
|
||||||
|
return this.customerInvoicesDto(data)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Database Error:', error);
|
||||||
|
throw new Error('Failed to fetch the latest invoices.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private customerInvoicesDto(dbCustomers: customerInvoiceDbResponse[]): CustomerInvoice[] {
|
||||||
|
return dbCustomers.map((customer) => this.customerInvoiceDto(customer));
|
||||||
|
}
|
||||||
|
|
||||||
|
private customerInvoiceDto(dbCustomer: customerInvoiceDbResponse): CustomerInvoice {
|
||||||
|
return new CustomerInvoice({
|
||||||
|
id: dbCustomer.id,
|
||||||
|
customerName: dbCustomer.name,
|
||||||
|
customerEmail: dbCustomer.email,
|
||||||
|
customerImageUrl: dbCustomer.image_url,
|
||||||
|
invoicesAmount: formatCurrency(+dbCustomer.amount),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
export default class CustomerInvoice {
|
||||||
|
id: string;
|
||||||
|
customerName: string;
|
||||||
|
customerImageUrl: string;
|
||||||
|
customerEmail: string;
|
||||||
|
invoicesAmount: string;
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
id,
|
||||||
|
customerEmail,
|
||||||
|
customerImageUrl,
|
||||||
|
customerName,
|
||||||
|
invoicesAmount
|
||||||
|
}: CustomerInvoice) {
|
||||||
|
this.id = id;
|
||||||
|
this.customerEmail = customerEmail
|
||||||
|
this.customerImageUrl = customerImageUrl
|
||||||
|
this.customerName = customerName
|
||||||
|
this.invoicesAmount = invoicesAmount
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
import CustomerInvoice from "@/feature/customer-invoice/domain/entity/customer-invoice"
|
||||||
|
|
||||||
|
export default interface CustomerInvoiceRepo {
|
||||||
|
fetchList(): Promise<CustomerInvoice[]>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const customerInvoiceRepoKey = "customerInvoiceRepoKey"
|
@ -0,0 +1,11 @@
|
|||||||
|
"use server"
|
||||||
|
import serverDi from "@/feature/common/server-di";
|
||||||
|
import CustomerInvoice from "@/feature/customer-invoice/domain/entity/customer-invoice";
|
||||||
|
import CustomerInvoiceRepo, { customerInvoiceRepoKey } from "@/feature/customer-invoice/domain/i-repo/customer-invoice-repo";
|
||||||
|
import { customerInvoiceModuleKey } from "@/feature/customer-invoice/invoice-module-key";
|
||||||
|
|
||||||
|
export default function fetchCustomerInvoicesUsecase(): Promise<CustomerInvoice[]> {
|
||||||
|
const repo = serverDi(customerInvoiceModuleKey).resolve<CustomerInvoiceRepo>(customerInvoiceRepoKey)
|
||||||
|
|
||||||
|
return repo.fetchList()
|
||||||
|
}
|
1
src/feature/customer-invoice/invoice-module-key.ts
Normal file
1
src/feature/customer-invoice/invoice-module-key.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const customerInvoiceModuleKey = "customerInvoiceModuleKey"
|
@ -1,4 +1,6 @@
|
|||||||
import di from "@/bootstrap/di/init-di";
|
import di from "@/bootstrap/di/init-di";
|
||||||
|
import CustomerDbRepo from "@/feature/customer/data/repo/customer-db-repo";
|
||||||
|
import { customerRepoKey } from "@/feature/customer/domain/i-repo/customer-repo";
|
||||||
import fetchCustomersUsecase from "@/feature/customer/domain/usecase/fetch-customers-usecase";
|
import fetchCustomersUsecase from "@/feature/customer/domain/usecase/fetch-customers-usecase";
|
||||||
import { DependencyContainer } from "tsyringe";
|
import { DependencyContainer } from "tsyringe";
|
||||||
|
|
||||||
@ -9,5 +11,6 @@ export default function getCustomerDi(): DependencyContainer {
|
|||||||
useValue: fetchCustomersUsecase
|
useValue: fetchCustomersUsecase
|
||||||
})
|
})
|
||||||
|
|
||||||
|
customerDi.register(customerRepoKey, CustomerDbRepo)
|
||||||
return customerDi
|
return customerDi
|
||||||
}
|
}
|
66
src/feature/customer/data/repo/customer-db-repo.ts
Normal file
66
src/feature/customer/data/repo/customer-db-repo.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { formatCurrency } from "@/app/lib/utils";
|
||||||
|
import { sql } from "@/bootstrap/db/db";
|
||||||
|
import Customer from "@/feature/customer/domain/entity/customer";
|
||||||
|
import CustomerRepo from "@/feature/customer/domain/i-repo/customer-repo";
|
||||||
|
import { connection } from "next/server";
|
||||||
|
import postgres from "postgres";
|
||||||
|
|
||||||
|
type customerDbResponse = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
image_url: string;
|
||||||
|
total_invoices: string;
|
||||||
|
total_pending: string;
|
||||||
|
total_paid: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class CustomerDbRepo implements CustomerRepo {
|
||||||
|
async fetchList(query: string): Promise<Customer[]> {
|
||||||
|
// This is equivalent to in fetch(..., {cache: 'no-store'}).
|
||||||
|
connection()
|
||||||
|
try {
|
||||||
|
const data = await sql`
|
||||||
|
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
|
||||||
|
` as postgres.RowList<customerDbResponse[]>;
|
||||||
|
|
||||||
|
|
||||||
|
return this.customersDto(data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Database Error:', err);
|
||||||
|
throw new Error('Failed to fetch customer table.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private customersDto(dbCustomers: customerDbResponse[]): Customer[] {
|
||||||
|
return dbCustomers.map((customer) => this.customerDto(customer));
|
||||||
|
}
|
||||||
|
|
||||||
|
private customerDto(dbCustomer: customerDbResponse): Customer {
|
||||||
|
return new Customer({
|
||||||
|
id: dbCustomer.id,
|
||||||
|
name: dbCustomer.name,
|
||||||
|
email: dbCustomer.email,
|
||||||
|
imageUrl: dbCustomer.image_url,
|
||||||
|
totalInvoices: dbCustomer.total_invoices,
|
||||||
|
totalPending: formatCurrency(Number(dbCustomer.total_pending)),
|
||||||
|
totalPaid: formatCurrency(Number(dbCustomer.total_paid)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,21 +1,11 @@
|
|||||||
export type CustomersTableType = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
image_url: string;
|
|
||||||
total_invoices: number;
|
|
||||||
total_pending: number;
|
|
||||||
total_paid: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default class Customer {
|
export default class Customer {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
imageUrl: string;
|
imageUrl: string;
|
||||||
totalInvoices: number;
|
totalInvoices: string;
|
||||||
totalPending: number;
|
totalPending: string;
|
||||||
totalPaid: number;
|
totalPaid: string;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
id,
|
id,
|
||||||
|
@ -8,9 +8,9 @@ export default class CustomerFakeFactory {
|
|||||||
name: faker.person.fullName(),
|
name: faker.person.fullName(),
|
||||||
email: faker.internet.email(),
|
email: faker.internet.email(),
|
||||||
imageUrl: faker.image.url(),
|
imageUrl: faker.image.url(),
|
||||||
totalInvoices: faker.number.int(),
|
totalInvoices: faker.number.int().toLocaleString(),
|
||||||
totalPaid: faker.number.int(),
|
totalPaid: faker.finance.amount(),
|
||||||
totalPending: faker.number.int(),
|
totalPending: faker.number.int().toLocaleString(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user