feature/research-di #1
@ -15,6 +15,6 @@
|
|||||||
"utils": "@/bootstrap/helpers/lib/ui-utils",
|
"utils": "@/bootstrap/helpers/lib/ui-utils",
|
||||||
"ui": "@/app/components",
|
"ui": "@/app/components",
|
||||||
"lib": "@/bootstrap/helpers/lib",
|
"lib": "@/bootstrap/helpers/lib",
|
||||||
"hooks": "@/bootstrap/helpers/vm/hooks"
|
"hooks": "@/bootstrap/helpers/hooks"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -48,9 +48,9 @@ export default async function LatestInvoices() {
|
|||||||
<h2 className="mb-4 text-xl md:text-2xl">
|
<h2 className="mb-4 text-xl md:text-2xl">
|
||||||
Latest Invoices
|
Latest Invoices
|
||||||
</h2>
|
</h2>
|
||||||
<div className="flex grow flex-col justify-between rounded-xl bg-gray-50 p-4">
|
<div className="flex grow flex-col max-h-[66.5vh] justify-between rounded-xl bg-gray-50 p-4">
|
||||||
|
|
||||||
<div className="bg-white px-6">
|
<div className="bg-white px-6 h-full overflow-y-auto">
|
||||||
{invoices}
|
{invoices}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-end mt-auto pb-2 pt-6">
|
<div className="flex items-end mt-auto pb-2 pt-6">
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
import CreateRandomInvoiceButtonVM from "@/app/dashboard/vm/create-random-invoice-button-vm";
|
import CreateRandomInvoiceButtonVM from "@/app/dashboard/vm/create-random-invoice-button-vm";
|
||||||
import di from "@/bootstrap/di/init-di"
|
import di from "@/bootstrap/di/init-di"
|
||||||
|
import createInvoiceUsecase from "@/feature/core/invoice/domain/usecase/create-invoice-usecase";
|
||||||
|
|
||||||
export default function dashboardAppModule() {
|
export default function dashboardAppModule() {
|
||||||
const dashboardDi = di.createChildContainer()
|
const dashboardDi = di.createChildContainer()
|
||||||
|
|
||||||
|
dashboardDi.register(createInvoiceUsecase.name, {
|
||||||
|
useValue: createInvoiceUsecase
|
||||||
|
})
|
||||||
dashboardDi.register(CreateRandomInvoiceButtonVM, CreateRandomInvoiceButtonVM)
|
dashboardDi.register(CreateRandomInvoiceButtonVM, CreateRandomInvoiceButtonVM)
|
||||||
return dashboardDi
|
return dashboardDi
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,36 @@
|
|||||||
import ButtonVm from "@/app/components/button/button-vm";
|
import ButtonVm from "@/app/components/button/button-vm";
|
||||||
|
import useThrottle from "@/bootstrap/helpers/hooks/use-throttle";
|
||||||
import BaseVM from "@/bootstrap/helpers/vm/base-vm";
|
import BaseVM from "@/bootstrap/helpers/vm/base-vm";
|
||||||
|
import { InvoiceParam } from "@/feature/core/invoice/domain/param/invoice-param";
|
||||||
|
import createInvoiceUsecase from "@/feature/core/invoice/domain/usecase/create-invoice-usecase";
|
||||||
|
import { faker } from "@faker-js/faker";
|
||||||
|
|
||||||
export default class CreateRandomInvoiceButtonVM extends BaseVM<ButtonVm> {
|
export default class CreateRandomInvoiceButtonVM extends BaseVM<ButtonVm> {
|
||||||
|
private createInvoice: typeof createInvoiceUsecase
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
this.createInvoice = this.di.resolve(createInvoiceUsecase.name)
|
||||||
|
}
|
||||||
|
|
||||||
useVM(): ButtonVm {
|
useVM(): ButtonVm {
|
||||||
|
const throttledOnClick = useThrottle(this.onClickHandler.bind(this), 5000)
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
title: "Button Title"
|
title: "Create Random Invoice"
|
||||||
},
|
},
|
||||||
onClick: () => {
|
onClick: throttledOnClick
|
||||||
console.log('clicked');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onClickHandler() {
|
||||||
|
const fakedParams: InvoiceParam = {
|
||||||
|
amount: faker.number.int({
|
||||||
|
min: 1,
|
||||||
|
max: 10
|
||||||
|
}),
|
||||||
|
status: "paid"
|
||||||
|
}
|
||||||
|
this.createInvoice(fakedParams)
|
||||||
}
|
}
|
||||||
}
|
}
|
18
src/bootstrap/helpers/hooks/use-throttle.ts
Normal file
18
src/bootstrap/helpers/hooks/use-throttle.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useEffect, useRef } from "react"
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param callback
|
||||||
|
* @param time In miliseconds
|
||||||
|
*/
|
||||||
|
export default function useThrottle<T extends Function>(callback: T, time: number = 2000) {
|
||||||
|
const lastRun = useRef(Date.now())
|
||||||
|
|
||||||
|
return function() {
|
||||||
|
if (Date.now() - lastRun.current <= time) return;
|
||||||
|
lastRun.current = Date.now()
|
||||||
|
return callback()
|
||||||
|
}
|
||||||
|
}
|
@ -20,7 +20,7 @@ export default class CustomerInvoiceDbRepo implements CustomerInvoiceRepo {
|
|||||||
FROM invoices
|
FROM invoices
|
||||||
JOIN customers ON invoices.customer_id = customers.id
|
JOIN customers ON invoices.customer_id = customers.id
|
||||||
ORDER BY invoices.date DESC
|
ORDER BY invoices.date DESC
|
||||||
LIMIT 5` as postgres.RowList<customerInvoiceDbResponse[]>;
|
LIMIT 20 ` as postgres.RowList<customerInvoiceDbResponse[]>;
|
||||||
|
|
||||||
return this.customerInvoicesDto(data)
|
return this.customerInvoicesDto(data)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { sql } from "@/bootstrap/db/db";
|
import { sql } from "@/bootstrap/db/db";
|
||||||
import { formatCurrency } from "@/feature/common/feature-helpers";
|
import { formatCurrency } from "@/feature/common/feature-helpers";
|
||||||
import InvoiceRepo from "@/feature/core/invoice/domain/i-repo/invoice-repo";
|
import InvoiceRepo from "@/feature/core/invoice/domain/i-repo/invoice-repo";
|
||||||
|
import { InvoiceParam } from "@/feature/core/invoice/domain/param/invoice-param";
|
||||||
import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status";
|
import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status";
|
||||||
import postgres from "postgres";
|
import postgres from "postgres";
|
||||||
|
|
||||||
@ -12,6 +13,28 @@ export default class InvoiceDbRepo implements InvoiceRepo {
|
|||||||
return data.count ?? 0
|
return data.count ?? 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createInvoice(params: InvoiceParam): Promise<string> {
|
||||||
|
const firstCustomerIdDb = await sql`SELECT
|
||||||
|
id FROM customers
|
||||||
|
ORDER BY id ASC
|
||||||
|
LIMIT 1
|
||||||
|
`
|
||||||
|
const customerId = firstCustomerIdDb.at(0)?.id
|
||||||
|
if (!customerId) throw new Error("There is no customer")
|
||||||
|
|
||||||
|
const { amount, status } = params;
|
||||||
|
const amountInCents = amount * 100;
|
||||||
|
const date = new Date().toISOString().split('T')[0];
|
||||||
|
|
||||||
|
// Insert data into the database
|
||||||
|
const result = await sql`
|
||||||
|
INSERT INTO invoices (customer_id, amount, status, date)
|
||||||
|
VALUES (${customerId}, ${amountInCents}, ${status}, ${date})
|
||||||
|
RETURNING id
|
||||||
|
`;
|
||||||
|
return result.at(0)?.id ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
async fetchInvoicesStatusSummary(): Promise<InvoiceStatusSummary> {
|
async fetchInvoicesStatusSummary(): Promise<InvoiceStatusSummary> {
|
||||||
const invoiceStatusPromise = await sql`SELECT
|
const invoiceStatusPromise = await sql`SELECT
|
||||||
SUM(CASE WHEN status = 'paid' THEN amount ELSE 0 END) AS "paid",
|
SUM(CASE WHEN status = 'paid' THEN amount ELSE 0 END) AS "paid",
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
|
import { InvoiceParam } from "@/feature/core/invoice/domain/param/invoice-param"
|
||||||
import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status"
|
import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status"
|
||||||
|
|
||||||
export default interface InvoiceRepo {
|
export default interface InvoiceRepo {
|
||||||
fetchAllInvoicesAmount(): Promise<number>
|
fetchAllInvoicesAmount(): Promise<number>
|
||||||
fetchInvoicesStatusSummary(): Promise<InvoiceStatusSummary>
|
fetchInvoicesStatusSummary(): Promise<InvoiceStatusSummary>
|
||||||
|
createInvoice(params: InvoiceParam): Promise<string>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const invoiceRepoKey = "invoiceRepoKey"
|
export const invoiceRepoKey = "invoiceRepoKey"
|
10
src/feature/core/invoice/domain/param/invoice-param.ts
Normal file
10
src/feature/core/invoice/domain/param/invoice-param.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const invoiceSchema = z.object({
|
||||||
|
amount: z.coerce.number().gt(0, { message: 'Please enter an amount greater than $0.' }),
|
||||||
|
status: z.enum(['pending', 'paid'], {
|
||||||
|
invalid_type_error: 'Please select an invoice status.',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type InvoiceParam = z.infer<typeof invoiceSchema>
|
@ -0,0 +1,22 @@
|
|||||||
|
"use server"
|
||||||
|
import serverDi from "@/feature/common/server-di";
|
||||||
|
import InvoiceRepo, { invoiceRepoKey } from "@/feature/core/invoice/domain/i-repo/invoice-repo";
|
||||||
|
import { InvoiceParam, invoiceSchema } from "@/feature/core/invoice/domain/param/invoice-param";
|
||||||
|
import { invoiceModuleKey } from "@/feature/core/invoice/invoice-module-key";
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
export default async function createInvoiceUsecase(params: InvoiceParam): Promise<string | {errorMessage: string}> {
|
||||||
|
const isParamsValid = invoiceSchema.safeParse(params)
|
||||||
|
|
||||||
|
if (!isParamsValid) {
|
||||||
|
return {
|
||||||
|
errorMessage: "Please pass correct params"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const repo = serverDi(invoiceModuleKey).resolve<InvoiceRepo>(invoiceRepoKey)
|
||||||
|
|
||||||
|
revalidatePath("/dashboard")
|
||||||
|
return repo.createInvoice(params)
|
||||||
|
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user