diff --git a/backend/src/modules/purchase/service.ts b/backend/src/modules/purchase/service.ts index a14cb827..e4f81960 100644 --- a/backend/src/modules/purchase/service.ts +++ b/backend/src/modules/purchase/service.ts @@ -61,25 +61,28 @@ export class PurchaseService implements IPurchaseService { async (payload: GetCompanyPurchasesDTO): Promise => { const qbPurchases = await this.PurchaseTransaction.getPurchasesForCompany(payload); - return qbPurchases.map((qbPurchase) => ({ - companyId: qbPurchase.companyId, - dateCreated: qbPurchase.dateCreated.toUTCString(), - id: qbPurchase.id, - isRefund: qbPurchase.isRefund, - quickBooksId: qbPurchase.quickBooksId, - vendor: qbPurchase.vendor ? qbPurchase.vendor : undefined, - totalAmountCents: Math.round(qbPurchase.totalAmountCents), - quickbooksDateCreated: qbPurchase.quickbooksDateCreated?.toUTCString(), - lastUpdated: qbPurchase.lastUpdated.toUTCString(), - lineItems: qbPurchase.lineItems - ? qbPurchase.lineItems.map((item) => ({ - ...item, - dateCreated: item.dateCreated.toISOString(), - lastUpdated: item.lastUpdated.toISOString(), - quickbooksDateCreated: item.quickbooksDateCreated?.toISOString(), - })) - : [], - })); + return { + purchases: qbPurchases.purchases.map((qbPurchase) => ({ + companyId: qbPurchase.companyId, + dateCreated: qbPurchase.dateCreated.toUTCString(), + id: qbPurchase.id, + isRefund: qbPurchase.isRefund, + quickBooksId: qbPurchase.quickBooksId, + vendor: qbPurchase.vendor ? qbPurchase.vendor : undefined, + totalAmountCents: Math.round(qbPurchase.totalAmountCents), + quickbooksDateCreated: qbPurchase.quickbooksDateCreated?.toUTCString(), + lastUpdated: qbPurchase.lastUpdated.toUTCString(), + lineItems: qbPurchase.lineItems + ? qbPurchase.lineItems.map((item) => ({ + ...item, + dateCreated: item.dateCreated.toISOString(), + lastUpdated: item.lastUpdated.toISOString(), + quickbooksDateCreated: item.quickbooksDateCreated?.toISOString(), + })) + : [], + })), + numPurchases: qbPurchases.numPurchases, + }; } ); diff --git a/backend/src/modules/purchase/transaction.ts b/backend/src/modules/purchase/transaction.ts index 4ae33de6..7eaaf3c0 100644 --- a/backend/src/modules/purchase/transaction.ts +++ b/backend/src/modules/purchase/transaction.ts @@ -7,12 +7,13 @@ import { GetCompanyPurchasesByDateDTO, GetCompanyPurchasesDTO, GetCompanyPurchasesInMonthBinsResponse, + PurchasesWithCount, } from "./types"; export interface IPurchaseTransaction { createOrUpdatePurchase(payload: CreateOrChangePurchaseDTO): Promise; getPurchase(id: string): Promise; - getPurchasesForCompany(payload: GetCompanyPurchasesDTO): Promise; + getPurchasesForCompany(payload: GetCompanyPurchasesDTO): Promise; sumPurchasesByCompanyAndDateRange(payload: GetCompanyPurchasesByDateDTO): Promise; sumPurchasesByCompanyInMonthBins( payload: GetCompanyPurchasesByDateDTO @@ -59,7 +60,7 @@ export class PurchaseTransaction implements IPurchaseTransaction { return existingQBPurchase; } - async getPurchasesForCompany(payload: GetCompanyPurchasesDTO): Promise { + async getPurchasesForCompany(payload: GetCompanyPurchasesDTO): Promise { const { companyId, pageNumber, resultsPerPage, categories, type, dateFrom, dateTo, search, sortBy, sortOrder } = payload; @@ -128,10 +129,12 @@ export class PurchaseTransaction implements IPurchaseTransaction { queryBuilder.orderBy(sortColumnMap[sortBy], sortOrder); } + const totalPurchases = await queryBuilder.getCount(); + const idRows = await queryBuilder.skip(numToSkip).take(resultsPerPage).getMany(); const ids = idRows.map((row) => row.id); if (ids.length === 0) { - return []; + return { purchases: [], numPurchases: 0 }; } const queryBuilderForIds = this.db.manager @@ -144,7 +147,8 @@ export class PurchaseTransaction implements IPurchaseTransaction { } // to guarantee that line items do not move in the table rows queryBuilderForIds.addOrderBy("li.dateCreated", "ASC"); - return await queryBuilderForIds.getMany(); + const paginatedPurchases = await queryBuilderForIds.getMany(); + return { purchases: paginatedPurchases, numPurchases: totalPurchases }; } async sumPurchasesByCompanyAndDateRange(payload: GetCompanyPurchasesByDateDTO): Promise { diff --git a/backend/src/modules/purchase/types.ts b/backend/src/modules/purchase/types.ts index 57dbe1a1..bdacbfde 100644 --- a/backend/src/modules/purchase/types.ts +++ b/backend/src/modules/purchase/types.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { PurchaseLineItemType } from "../../entities/PurchaseLineItem"; import { GetPurchaseLineItemResponseSchema } from "../purchase-line-item/types"; +import { Purchase } from "../../entities/Purchase"; export const CreateOrChangePurchaseRequestSchema = z.object({ items: z @@ -95,7 +96,10 @@ export const GetCompanyPurchasesByDateDTOSchema = z.object({ endDate: z.iso.datetime(), }); -export const GetCompanyPurchasesResponseSchema = z.array(GetPurchaseWithLineItems); +export const GetCompanyPurchasesResponseSchema = z.object({ + purchases: z.array(GetPurchaseWithLineItems), + numPurchases: z.number().nonnegative(), +}); export const GetCompanyPurchasesSummationResponseSchema = z.object({ total: z.number().nonnegative(), @@ -124,3 +128,11 @@ export type CreateOrChangePurchaseDTO = z.infer; export type GetCompanyPurchasesByDateDTO = z.infer; export type GetPurchaseDTO = z.infer; + +/** + * Types for the transaction layer + */ +export type PurchasesWithCount = { + purchases: Purchase[]; + numPurchases: number; +}; diff --git a/backend/src/tests/purchases/getPurchaseForCompany.test.ts b/backend/src/tests/purchases/getPurchaseForCompany.test.ts index 4578058b..33123001 100644 --- a/backend/src/tests/purchases/getPurchaseForCompany.test.ts +++ b/backend/src/tests/purchases/getPurchaseForCompany.test.ts @@ -2,13 +2,18 @@ import { Hono } from "hono"; import { describe, test, expect, beforeAll, afterEach, beforeEach } from "bun:test"; import { startTestApp } from "../setup-tests"; import { IBackup } from "pg-mem"; -import { CreateOrChangePurchaseRequest, GetCompanyPurchasesResponse } from "../../modules/purchase/types"; +import { + CreateOrChangePurchaseRequest, + GetCompanyPurchasesResponse, + PurchasesWithCount, +} from "../../modules/purchase/types"; import { TESTING_PREFIX } from "../../utilities/constants"; import CompanySeeder, { seededCompanies } from "../../database/seeds/company.seed"; import { SeederFactoryManager } from "typeorm-extension"; import { DataSource } from "typeorm"; import { PurchaseSeeder, seededPurchases } from "../../database/seeds/purchase.seed"; import { PurchaseLineItemSeeder } from "../../database/seeds/purchaseLineItem.seed"; +import { Purchase } from "../../entities/Purchase"; describe("GET /purchase", () => { let app: Hono; @@ -63,10 +68,10 @@ describe("GET /purchase", () => { }); expect(response.status).toBe(200); - const body = await response.json(); - expect(Array.isArray(body)).toBe(true); - expect(body.length).toBeGreaterThan(0); - body.forEach((purchase: GetCompanyPurchasesResponse[number]) => { + const body: PurchasesWithCount = await response.json(); + expect(Array.isArray(body.purchases)).toBe(true); + expect(body.purchases.length).toBeGreaterThan(0); + body.purchases.forEach((purchase) => { expect(purchase.id).toBeDefined(); expect(purchase.companyId).toBeDefined(); expect(purchase.quickBooksId).toBeDefined(); @@ -100,7 +105,7 @@ describe("GET /purchase", () => { expect(response.status).toBe(200); const body = await response.json(); - expect(Array.isArray(body)).toBe(true); + expect(Array.isArray(body.purchases)).toBe(true); }); test("GET /purchase - With resultsPerPage", async () => { @@ -126,8 +131,8 @@ describe("GET /purchase", () => { expect(response.status).toBe(200); const body = await response.json(); - expect(Array.isArray(body)).toBe(true); - expect(body.length).toBeLessThanOrEqual(10); + expect(Array.isArray(body.purchases)).toBe(true); + expect(body.purchases.length).toBeLessThanOrEqual(10); }); test("GET /purchase - With pageNumber and resultsPerPage", async () => { @@ -153,8 +158,8 @@ describe("GET /purchase", () => { expect(response.status).toBe(200); const body = await response.json(); - expect(Array.isArray(body)).toBe(true); - expect(body.length).toBeLessThanOrEqual(5); + expect(Array.isArray(body.purchases)).toBe(true); + expect(body.purchases.length).toBeLessThanOrEqual(5); }); test("GET /purchase - Default Pagination Values", async () => { @@ -181,8 +186,8 @@ describe("GET /purchase", () => { expect(response.status).toBe(200); const body = await response.json(); - expect(Array.isArray(body)).toBe(true); - expect(body.length).toBeLessThanOrEqual(20); + expect(Array.isArray(body.purchases)).toBe(true); + expect(body.purchases.length).toBeLessThanOrEqual(20); }); test("GET /purchase - Non-Existent companyId", async () => { @@ -195,8 +200,8 @@ describe("GET /purchase", () => { expect(response.status).toBe(200); const body = await response.json(); - expect(Array.isArray(body)).toBe(true); - expect(body.length).toBe(0); + expect(Array.isArray(body.purchases)).toBe(true); + expect(body.purchases.length).toBe(0); }); test("GET /purchase - Missing companyId", async () => { @@ -299,8 +304,8 @@ describe("GET /purchase", () => { expect(response.status).toBe(200); const body = await response.json(); - expect(Array.isArray(body)).toBe(true); - expect(body.length).toBe(0); + expect(Array.isArray(body.purchases)).toBe(true); + expect(body.purchases.length).toBe(0); }); test("GET /purchase - Very Large resultsPerPage", async () => { @@ -326,7 +331,7 @@ describe("GET /purchase", () => { expect(response.status).toBe(200); const body = await response.json(); - expect(Array.isArray(body)).toBe(true); + expect(Array.isArray(body.purchases)).toBe(true); }); test("GET /purchase - Multiple Purchases Same Company", async () => { @@ -354,11 +359,11 @@ describe("GET /purchase", () => { expect(response.status).toBe(200); const body = await response.json(); - expect(Array.isArray(body)).toBe(true); - expect(body.length).toBe(purchaseCount); + expect(Array.isArray(body.purchases)).toBe(true); + expect(body.purchases.length).toBe(purchaseCount); // Verify all purchases belong to the same company - body.forEach((purchase: GetCompanyPurchasesResponse[number]) => { + body.purchases.forEach((purchase: Purchase) => { expect(purchase.companyId).toBe("ffc8243b-876e-4b6d-8b80-ffc73522a838"); }); }); @@ -404,10 +409,10 @@ describe("GET /purchase - Filtered and Sorted", () => { }); expect(response.status).toBe(200); - const body = (await response.json()) as GetCompanyPurchasesResponse; - expect(body.length).toBe(6); + const body = await response.json(); + expect(body.purchases.length).toBe(6); - const purchaseIds = body.map((p) => p.id); + const purchaseIds = body.purchases.map((p: Purchase) => p.id); expect(purchaseIds).toContain(seededPurchases[0].id); expect(purchaseIds).toContain(seededPurchases[1].id); expect(purchaseIds).toContain(seededPurchases[2].id); @@ -427,12 +432,12 @@ describe("GET /purchase - Filtered and Sorted", () => { expect(response.status).toBe(200); const body = (await response.json()) as GetCompanyPurchasesResponse; - expect(body.length).toBe(2); - expect(body[0].id).toBe(seededPurchases[0].id); - expect(body[1].id).toBe(seededPurchases[4].id); + expect(body.purchases.length).toBe(2); + expect(body.purchases[0].id).toBe(seededPurchases[0].id); + expect(body.purchases[1].id).toBe(seededPurchases[4].id); - expect(body[0].lineItems.some((li) => li.category === "Supplies")).toBe(true); - expect(body[1].lineItems.some((li) => li.category === "Supplies")).toBe(true); + expect(body.purchases[0].lineItems.some((li) => li.category === "Supplies")).toBe(true); + expect(body.purchases[1].lineItems.some((li) => li.category === "Supplies")).toBe(true); }); test("GET /purchase - Filter by Technology category ", async () => { @@ -446,12 +451,12 @@ describe("GET /purchase - Filtered and Sorted", () => { expect(response.status).toBe(200); const body = (await response.json()) as GetCompanyPurchasesResponse; - expect(body.length).toBe(2); - expect(body[0].id).toBe(seededPurchases[0].id); - expect(body[1].id).toBe(seededPurchases[5].id); + expect(body.purchases.length).toBe(2); + expect(body.purchases[0].id).toBe(seededPurchases[0].id); + expect(body.purchases[1].id).toBe(seededPurchases[5].id); - expect(body[0].lineItems.some((li) => li.category === "Technology")).toBe(true); - expect(body[1].lineItems.some((li) => li.category === "Technology")).toBe(true); + expect(body.purchases[0].lineItems.some((li) => li.category === "Technology")).toBe(true); + expect(body.purchases[1].lineItems.some((li) => li.category === "Technology")).toBe(true); }); test("GET /purchase - Filter by multiple categories ", async () => { @@ -465,9 +470,9 @@ describe("GET /purchase - Filtered and Sorted", () => { expect(response.status).toBe(200); const body = (await response.json()) as GetCompanyPurchasesResponse; - expect(body.length).toBe(3); + expect(body.purchases.length).toBe(3); - const returnedIds = body.map((p) => p.id).sort(); + const returnedIds = body.purchases.map((p) => p.id).sort(); expect(returnedIds).toEqual( [ seededPurchases[0].id, @@ -476,22 +481,22 @@ describe("GET /purchase - Filtered and Sorted", () => { ].sort() ); - body.forEach((purchase) => { + body.purchases.forEach((purchase) => { const hasMatchingCategory = purchase.lineItems.some( (li) => li.category === "Supplies" || li.category === "Technology" ); expect(hasMatchingCategory).toBe(true); }); - const purchase0 = body.find((p) => p.id === seededPurchases[0].id); + const purchase0 = body.purchases.find((p) => p.id === seededPurchases[0].id); expect(purchase0?.lineItems.some((li) => li.category === "Supplies")).toBe(true); expect(purchase0?.lineItems.some((li) => li.category === "Technology")).toBe(true); - const suppliesOnly = body.find((p) => p.id === "a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d"); + const suppliesOnly = body.purchases.find((p) => p.id === "a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d"); expect(suppliesOnly?.lineItems.some((li) => li.category === "Supplies")).toBe(true); expect(suppliesOnly?.lineItems.every((li) => li.category !== "Technology")).toBe(true); - const technologyOnly = body.find((p) => p.id === "b2c3d4e5-f6a7-4b5c-9d0e-1f2a3b4c5d6e"); + const technologyOnly = body.purchases.find((p) => p.id === "b2c3d4e5-f6a7-4b5c-9d0e-1f2a3b4c5d6e"); expect(technologyOnly?.lineItems.some((li) => li.category === "Technology")).toBe(true); expect(technologyOnly?.lineItems.every((li) => li.category !== "Supplies")).toBe(true); }); @@ -506,7 +511,7 @@ describe("GET /purchase - Filtered and Sorted", () => { expect(response.status).toBe(200); const body = (await response.json()) as GetCompanyPurchasesResponse; - expect(body.length).toBe(0); + expect(body.purchases.length).toBe(0); }); test("GET /purchase - Filter by type typical", async () => { @@ -520,8 +525,8 @@ describe("GET /purchase - Filtered and Sorted", () => { expect(response.status).toBe(200); const body = (await response.json()) as GetCompanyPurchasesResponse; - expect(body.length).toBe(5); - const returnedIds = body.map((p) => p.id).sort(); + expect(body.purchases.length).toBe(5); + const returnedIds = body.purchases.map((p) => p.id).sort(); expect(returnedIds).toEqual( [ seededPurchases[1].id, @@ -532,7 +537,7 @@ describe("GET /purchase - Filtered and Sorted", () => { ].sort() ); - body.forEach((purchase) => { + body.purchases.forEach((purchase) => { expect(purchase.lineItems.every((li) => li.type === "typical")).toBe(true); }); }); @@ -548,9 +553,9 @@ describe("GET /purchase - Filtered and Sorted", () => { expect(response.status).toBe(200); const body = (await response.json()) as GetCompanyPurchasesResponse; - expect(body.length).toBe(1); - expect(body[0].id).toBe(seededPurchases[0].id); - expect(body[0].lineItems.some((li) => li.type === "extraneous")).toBe(true); + expect(body.purchases.length).toBe(1); + expect(body.purchases[0].id).toBe(seededPurchases[0].id); + expect(body.purchases[0].lineItems.some((li) => li.type === "extraneous")).toBe(true); }); test("GET /purchase - Invalid type returns 400", async () => { @@ -578,8 +583,8 @@ describe("GET /purchase - Filtered and Sorted", () => { expect(response.status).toBe(200); const body = (await response.json()) as GetCompanyPurchasesResponse; // Should return all seeded purchases since dateFrom is missing - expect(body.length).toBe(6); - const returnedIds = body.map((p) => p.id).sort(); + expect(body.purchases.length).toBe(6); + const returnedIds = body.purchases.map((p) => p.id).sort(); expect(returnedIds).toEqual( [ seededPurchases[0].id, @@ -606,8 +611,8 @@ describe("GET /purchase - Filtered and Sorted", () => { expect(response.status).toBe(200); const body = (await response.json()) as GetCompanyPurchasesResponse; // Should return all seeded purchases since dateTo is missing - expect(body.length).toBe(6); - const returnedIds = body.map((p) => p.id).sort(); + expect(body.purchases.length).toBe(6); + const returnedIds = body.purchases.map((p) => p.id).sort(); expect(returnedIds).toEqual( [ seededPurchases[0].id, @@ -669,8 +674,8 @@ describe("GET /purchase - Filtered and Sorted", () => { expect(response.status).toBe(200); const body = (await response.json()) as GetCompanyPurchasesResponse; - expect(body.length).toBe(2); - expect(body.map((p) => p.id).sort()).toEqual([seededPurchases[1].id, seededPurchases[2].id].sort()); + expect(body.purchases.length).toBe(2); + expect(body.purchases.map((p) => p.id).sort()).toEqual([seededPurchases[1].id, seededPurchases[2].id].sort()); }); test("GET /purchase - dateFrom undefined should not filter", async () => { @@ -686,8 +691,8 @@ describe("GET /purchase - Filtered and Sorted", () => { expect(response.status).toBe(200); const body = (await response.json()) as GetCompanyPurchasesResponse; - expect(body.length).toBe(6); - const returnedIds = body.map((p) => p.id).sort(); + expect(body.purchases.length).toBe(6); + const returnedIds = body.purchases.map((p) => p.id).sort(); expect(returnedIds).toEqual( [ seededPurchases[0].id, @@ -713,8 +718,8 @@ describe("GET /purchase - Filtered and Sorted", () => { expect(response.status).toBe(200); const body = (await response.json()) as GetCompanyPurchasesResponse; - expect(body.length).toBe(6); - const returnedIds = body.map((p) => p.id).sort(); + expect(body.purchases.length).toBe(6); + const returnedIds = body.purchases.map((p) => p.id).sort(); expect(returnedIds).toEqual( [ seededPurchases[0].id, @@ -758,9 +763,9 @@ describe("GET /purchase - Filtered and Sorted", () => { expect(response.status).toBe(200); const body = (await response.json()) as GetCompanyPurchasesResponse; - expect(body.length).toBe(1); - expect(body[0].id).toBe(seededPurchases[0].id); - expect(body[0].lineItems.some((li) => li.description?.includes("Office"))).toBe(true); + expect(body.purchases.length).toBe(1); + expect(body.purchases[0].id).toBe(seededPurchases[0].id); + expect(body.purchases[0].lineItems.some((li) => li.description?.includes("Office"))).toBe(true); }); test("GET /purchase - Search by 'Software' in description", async () => { @@ -774,9 +779,9 @@ describe("GET /purchase - Filtered and Sorted", () => { expect(response.status).toBe(200); const body = (await response.json()) as GetCompanyPurchasesResponse; - expect(body.length).toBe(1); - expect(body[0].id).toBe(seededPurchases[0].id); - expect(body[0].lineItems.some((li) => li.description?.includes("Software"))).toBe(true); + expect(body.purchases.length).toBe(1); + expect(body.purchases[0].id).toBe(seededPurchases[0].id); + expect(body.purchases[0].lineItems.some((li) => li.description?.includes("Software"))).toBe(true); }); test("GET /purchase - Search with no results", async () => { @@ -789,7 +794,7 @@ describe("GET /purchase - Filtered and Sorted", () => { expect(response.status).toBe(200); const body = (await response.json()) as GetCompanyPurchasesResponse; - expect(body.length).toBe(0); + expect(body.purchases.length).toBe(0); }); test("GET /purchase - Sort by date DESC", async () => { @@ -803,16 +808,16 @@ describe("GET /purchase - Filtered and Sorted", () => { expect(response.status).toBe(200); const body = (await response.json()) as GetCompanyPurchasesResponse; - expect(body.length).toBe(6); - expect(body[0].id).toBe("b2c3d4e5-f6a7-4b5c-9d0e-1f2a3b4c5d6e"); // 2025-03-02 - expect(body[1].id).toBe("a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d"); // 2025-03-01 - expect(body[2].id).toBe(seededPurchases[0].id); // 2025-02-05 - expect(body[3].id).toBe(seededPurchases[1].id); // 2025-01-11 - expect(body[4].id).toBe(seededPurchases[2].id); // 2025-01-09 - expect(body[5].id).toBe(seededPurchases[3].id); // 2024-04-11 + expect(body.purchases.length).toBe(6); + expect(body.purchases[0].id).toBe("b2c3d4e5-f6a7-4b5c-9d0e-1f2a3b4c5d6e"); // 2025-03-02 + expect(body.purchases[1].id).toBe("a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d"); // 2025-03-01 + expect(body.purchases[2].id).toBe(seededPurchases[0].id); // 2025-02-05 + expect(body.purchases[3].id).toBe(seededPurchases[1].id); // 2025-01-11 + expect(body.purchases[4].id).toBe(seededPurchases[2].id); // 2025-01-09 + expect(body.purchases[5].id).toBe(seededPurchases[3].id); // 2024-04-11 - for (let i = 1; i < body.length; i++) { - expect(new Date(body[i - 1].dateCreated) >= new Date(body[i].dateCreated)).toBe(true); + for (let i = 1; i < body.purchases.length; i++) { + expect(new Date(body.purchases[i - 1].dateCreated) >= new Date(body.purchases[i].dateCreated)).toBe(true); } }); @@ -827,16 +832,16 @@ describe("GET /purchase - Filtered and Sorted", () => { expect(response.status).toBe(200); const body = (await response.json()) as GetCompanyPurchasesResponse; - expect(body.length).toBe(6); - expect(body[0].id).toBe(seededPurchases[3].id); // 2024-04-11 - expect(body[1].id).toBe(seededPurchases[2].id); // 2025-01-09 - expect(body[2].id).toBe(seededPurchases[1].id); // 2025-01-11 - expect(body[3].id).toBe(seededPurchases[0].id); // 2025-02-05 - expect(body[4].id).toBe("a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d"); // 2025-03-01 - expect(body[5].id).toBe("b2c3d4e5-f6a7-4b5c-9d0e-1f2a3b4c5d6e"); // 2025-03-02 + expect(body.purchases.length).toBe(6); + expect(body.purchases[0].id).toBe(seededPurchases[3].id); // 2024-04-11 + expect(body.purchases[1].id).toBe(seededPurchases[2].id); // 2025-01-09 + expect(body.purchases[2].id).toBe(seededPurchases[1].id); // 2025-01-11 + expect(body.purchases[3].id).toBe(seededPurchases[0].id); // 2025-02-05 + expect(body.purchases[4].id).toBe("a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d"); // 2025-03-01 + expect(body.purchases[5].id).toBe("b2c3d4e5-f6a7-4b5c-9d0e-1f2a3b4c5d6e"); // 2025-03-02 - for (let i = 1; i < body.length; i++) { - expect(new Date(body[i - 1].dateCreated) <= new Date(body[i].dateCreated)).toBe(true); + for (let i = 1; i < body.purchases.length; i++) { + expect(new Date(body.purchases[i - 1].dateCreated) <= new Date(body.purchases[i].dateCreated)).toBe(true); } }); @@ -851,22 +856,22 @@ describe("GET /purchase - Filtered and Sorted", () => { expect(response.status).toBe(200); const body = (await response.json()) as GetCompanyPurchasesResponse; - expect(body.length).toBe(6); - expect(body[0].id).toBe(seededPurchases[3].id); - expect(body[0].totalAmountCents).toBe(50); - expect(body[1].id).toBe(seededPurchases[2].id); - expect(body[1].totalAmountCents).toBe(456); - expect(body[2].id).toBe(seededPurchases[0].id); - expect(body[2].totalAmountCents).toBe(1234); - expect(body[3].id).toBe("a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d"); - expect(body[3].totalAmountCents).toBe(2000); - expect(body[4].id).toBe("b2c3d4e5-f6a7-4b5c-9d0e-1f2a3b4c5d6e"); - expect(body[4].totalAmountCents).toBe(3000); - expect(body[5].id).toBe(seededPurchases[1].id); - expect(body[5].totalAmountCents).toBe(5678); - - for (let i = 1; i < body.length; i++) { - expect(body[i - 1].totalAmountCents <= body[i].totalAmountCents).toBe(true); + expect(body.purchases.length).toBe(6); + expect(body.purchases[0].id).toBe(seededPurchases[3].id); + expect(body.purchases[0].totalAmountCents).toBe(50); + expect(body.purchases[1].id).toBe(seededPurchases[2].id); + expect(body.purchases[1].totalAmountCents).toBe(456); + expect(body.purchases[2].id).toBe(seededPurchases[0].id); + expect(body.purchases[2].totalAmountCents).toBe(1234); + expect(body.purchases[3].id).toBe("a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d"); + expect(body.purchases[3].totalAmountCents).toBe(2000); + expect(body.purchases[4].id).toBe("b2c3d4e5-f6a7-4b5c-9d0e-1f2a3b4c5d6e"); + expect(body.purchases[4].totalAmountCents).toBe(3000); + expect(body.purchases[5].id).toBe(seededPurchases[1].id); + expect(body.purchases[5].totalAmountCents).toBe(5678); + + for (let i = 1; i < body.purchases.length; i++) { + expect(body.purchases[i - 1].totalAmountCents <= body.purchases[i].totalAmountCents).toBe(true); } }); @@ -881,22 +886,22 @@ describe("GET /purchase - Filtered and Sorted", () => { expect(response.status).toBe(200); const body = (await response.json()) as GetCompanyPurchasesResponse; - expect(body.length).toBe(6); - expect(body[0].id).toBe(seededPurchases[1].id); - expect(body[0].totalAmountCents).toBe(5678); - expect(body[1].id).toBe("b2c3d4e5-f6a7-4b5c-9d0e-1f2a3b4c5d6e"); - expect(body[1].totalAmountCents).toBe(3000); - expect(body[2].id).toBe("a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d"); - expect(body[2].totalAmountCents).toBe(2000); - expect(body[3].id).toBe(seededPurchases[0].id); - expect(body[3].totalAmountCents).toBe(1234); - expect(body[4].id).toBe(seededPurchases[2].id); - expect(body[4].totalAmountCents).toBe(456); - expect(body[5].id).toBe(seededPurchases[3].id); - expect(body[5].totalAmountCents).toBe(50); - - for (let i = 1; i < body.length; i++) { - expect(body[i - 1].totalAmountCents >= body[i].totalAmountCents).toBe(true); + expect(body.purchases.length).toBe(6); + expect(body.purchases[0].id).toBe(seededPurchases[1].id); + expect(body.purchases[0].totalAmountCents).toBe(5678); + expect(body.purchases[1].id).toBe("b2c3d4e5-f6a7-4b5c-9d0e-1f2a3b4c5d6e"); + expect(body.purchases[1].totalAmountCents).toBe(3000); + expect(body.purchases[2].id).toBe("a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d"); + expect(body.purchases[2].totalAmountCents).toBe(2000); + expect(body.purchases[3].id).toBe(seededPurchases[0].id); + expect(body.purchases[3].totalAmountCents).toBe(1234); + expect(body.purchases[4].id).toBe(seededPurchases[2].id); + expect(body.purchases[4].totalAmountCents).toBe(456); + expect(body.purchases[5].id).toBe(seededPurchases[3].id); + expect(body.purchases[5].totalAmountCents).toBe(50); + + for (let i = 1; i < body.purchases.length; i++) { + expect(body.purchases[i - 1].totalAmountCents >= body.purchases[i].totalAmountCents).toBe(true); } }); @@ -936,9 +941,9 @@ describe("GET /purchase - Filtered and Sorted", () => { expect(response.status).toBe(200); const body = (await response.json()) as GetCompanyPurchasesResponse; - expect(body.length).toBe(2); - expect(body[0].id).toBe("b2c3d4e5-f6a7-4b5c-9d0e-1f2a3b4c5d6e"); - expect(body[1].id).toBe("a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d"); + expect(body.purchases.length).toBe(2); + expect(body.purchases[0].id).toBe("b2c3d4e5-f6a7-4b5c-9d0e-1f2a3b4c5d6e"); + expect(body.purchases[1].id).toBe("a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d"); }); test("GET /purchase - Pagination all pages, resultsPerPage = 2", async () => { @@ -954,10 +959,10 @@ describe("GET /purchase - Filtered and Sorted", () => { expect(responsePage0.status).toBe(200); const bodyPage0 = (await responsePage0.json()) as GetCompanyPurchasesResponse; - expect(Array.isArray(bodyPage0)).toBe(true); - expect(bodyPage0.length).toBe(2); - expect(bodyPage0[0].id).toBe(seededPurchases[5].id); - expect(bodyPage0[1].id).toBe(seededPurchases[4].id); + expect(Array.isArray(bodyPage0.purchases)).toBe(true); + expect(bodyPage0.purchases.length).toBe(2); + expect(bodyPage0.purchases[0].id).toBe(seededPurchases[5].id); + expect(bodyPage0.purchases[1].id).toBe(seededPurchases[4].id); // PAGE 1 const responsePage1 = await app.request( @@ -971,13 +976,13 @@ describe("GET /purchase - Filtered and Sorted", () => { expect(responsePage1.status).toBe(200); const bodyPage1 = (await responsePage1.json()) as GetCompanyPurchasesResponse; - expect(Array.isArray(bodyPage1)).toBe(true); - expect(bodyPage1.length).toBe(2); - expect(bodyPage1[0].id).toBe(seededPurchases[0].id); - expect(bodyPage1[1].id).toBe(seededPurchases[1].id); + expect(Array.isArray(bodyPage1.purchases)).toBe(true); + expect(bodyPage1.purchases.length).toBe(2); + expect(bodyPage1.purchases[0].id).toBe(seededPurchases[0].id); + expect(bodyPage1.purchases[1].id).toBe(seededPurchases[1].id); - const page0Ids = bodyPage0.map((p) => p.id); - const page1Ids = bodyPage1.map((p) => p.id); + const page0Ids = bodyPage0.purchases.map((p) => p.id); + const page1Ids = bodyPage1.purchases.map((p) => p.id); page1Ids.forEach((id) => expect(page0Ids).not.toContain(id)); // PAGE 2 @@ -992,10 +997,10 @@ describe("GET /purchase - Filtered and Sorted", () => { expect(responsePage2.status).toBe(200); const bodyPage2 = (await responsePage2.json()) as GetCompanyPurchasesResponse; - expect(Array.isArray(bodyPage2)).toBe(true); - expect(bodyPage2.length).toBe(2); - expect(bodyPage2[0].id).toBe(seededPurchases[2].id); - expect(bodyPage2[1].id).toBe(seededPurchases[3].id); + expect(Array.isArray(bodyPage2.purchases)).toBe(true); + expect(bodyPage2.purchases.length).toBe(2); + expect(bodyPage2.purchases[0].id).toBe(seededPurchases[2].id); + expect(bodyPage2.purchases[1].id).toBe(seededPurchases[3].id); // PAGE 3 (should be empty) const responsePage3 = await app.request( @@ -1009,8 +1014,8 @@ describe("GET /purchase - Filtered and Sorted", () => { expect(responsePage3.status).toBe(200); const bodyPage3 = (await responsePage3.json()) as GetCompanyPurchasesResponse; - expect(Array.isArray(bodyPage3)).toBe(true); - expect(bodyPage3.length).toBe(0); + expect(Array.isArray(bodyPage3.purchases)).toBe(true); + expect(bodyPage3.purchases.length).toBe(0); }); test("GET /purchase - Combined: category + type + dateRange + sort", async () => { @@ -1033,8 +1038,8 @@ describe("GET /purchase - Filtered and Sorted", () => { expect(response.status).toBe(200); const body = (await response.json()) as GetCompanyPurchasesResponse; - expect(body.length).toBe(1); - const returnedIds = body.map((p) => p.id).sort(); + expect(body.purchases.length).toBe(1); + const returnedIds = body.purchases.map((p) => p.id).sort(); expect(returnedIds).toEqual([seededPurchases[4].id].sort()); }); @@ -1058,7 +1063,7 @@ describe("GET /purchase - Filtered and Sorted", () => { expect(response.status).toBe(200); const body = (await response.json()) as GetCompanyPurchasesResponse; - expect(Array.isArray(body)).toBe(true); - expect(body.length).toBe(0); + expect(Array.isArray(body.purchases)).toBe(true); + expect(body.purchases.length).toBe(0); }); }); diff --git a/frontend/api/purchase.ts b/frontend/api/purchase.ts index 0b9f8d10..8195907a 100644 --- a/frontend/api/purchase.ts +++ b/frontend/api/purchase.ts @@ -1,42 +1,7 @@ "use server"; -import { - CreatePurchaseInput, - CreatePurchaseResponse, - FilteredPurchases, - PurchaseLineItemType, - Purchases, -} from "../types/purchase"; +import { CreatePurchaseInput, CreatePurchaseResponse, PurchaseLineItemType } from "../types/purchase"; import { authHeader, authWrapper, getClient } from "./client"; -export const getAllPurchasesForCompany = async (filters: FilteredPurchases): Promise => { - const req = async (token: string): Promise => { - const client = getClient(); - const { data, error, response } = await client.GET("/purchase", { - params: { - query: { - pageNumber: filters.pageNumber, - resultsPerPage: filters.resultsPerPage, - categories: filters.categories, - type: filters.type, - dateFrom: filters.dateFrom, - dateTo: filters.dateTo, - search: filters.search, - sortBy: filters.sortBy, - sortOrder: filters.sortOrder, - }, - }, - headers: authHeader(token), - }); - if (response.ok) { - return data!; - } else { - throw Error(error?.error); - } - }; - - return authWrapper()(req); -}; - export const sumPurchasesByCompanyAndDateRange = async (startDate: Date, endDate: Date): Promise<{ total: number }> => { const req = async (token: string): Promise<{ total: number }> => { const client = getClient(); diff --git a/frontend/app/expense-tracker/expense-table/PaginationControls.tsx b/frontend/app/expense-tracker/expense-table/PaginationControls.tsx index b2cb774e..7bca841b 100644 --- a/frontend/app/expense-tracker/expense-table/PaginationControls.tsx +++ b/frontend/app/expense-tracker/expense-table/PaginationControls.tsx @@ -1,35 +1,43 @@ -import { - Pagination, - PaginationContent, - PaginationItem, - PaginationNext, - PaginationPrevious, -} from "@/components/ui/pagination"; +import { ChevronLeft, ChevronRight } from "lucide-react"; -export default function PaginationControls({ - page, - onPageChange, - isLastPage, -}: { +type PaginationProps = { page: number; + resultsPerPage: number; onPageChange: (page: number) => void; - isLastPage: boolean; -}) { + totalNumPurchases?: number; +}; + +export default function PaginationControls({ page, resultsPerPage, onPageChange, totalNumPurchases }: PaginationProps) { + const isFirstPage = page === 0; + const total = totalNumPurchases ?? 0; + const isLastPage = (page + 1) * resultsPerPage >= total; + + const start = total === 0 ? 0 : page * resultsPerPage + 1; + const end = Math.min(total, (page + 1) * resultsPerPage); + return ( - - - - onPageChange(Math.max(0, page - 1))} /> - - - { - if (!isLastPage) onPageChange(page + 1); - }} - /> - - - +
+ + {start}-{end} of {total} + + + + + +
); } diff --git a/frontend/app/expense-tracker/expense-table/ResultsPerPageSelect.tsx b/frontend/app/expense-tracker/expense-table/ResultsPerPageSelect.tsx index 6e99888b..4cfe41ab 100644 --- a/frontend/app/expense-tracker/expense-table/ResultsPerPageSelect.tsx +++ b/frontend/app/expense-tracker/expense-table/ResultsPerPageSelect.tsx @@ -8,14 +8,19 @@ export default function ResultsPerPageSelect({ onValueChange: (value: number) => void; }) { const pageSizeOptions = [5, 10, 15, 20]; + return ( -
-

Results per page

+
+ Rows per page: { - e.stopPropagation(); - }} - /> - )} - {displayMerchant.length > 0 ? displayMerchant : ""} + {displayVendor}
); }, @@ -171,7 +143,6 @@ export default function TableContent({ }} lineItemIds={row.lineItemIds} editableTags={editableTags} - hasLineItems={row.lineItems.length > 0} /> ); }, @@ -207,5 +178,5 @@ export default function TableContent({ if (purchases.error) return
Error loading expenses
; - return ; + return
onRowClick?.(row.originalPurchase)} />; } diff --git a/frontend/app/expense-tracker/utility-functions.ts b/frontend/app/expense-tracker/utility-functions.ts index bcb1201d..0adb718d 100644 --- a/frontend/app/expense-tracker/utility-functions.ts +++ b/frontend/app/expense-tracker/utility-functions.ts @@ -16,7 +16,7 @@ export function getPurchaseTypeString(lineItems: { type?: string | null }[]): Di } else if (types.includes("extraneous")) { return "extraneous"; } else if (types.includes("typical")) { - return "extraneous"; + return "typical"; } else if (types.includes("suggested extraneous")) { return "suggested extraneous"; } else if (types.includes("suggested typical")) { diff --git a/frontend/components/table/index.tsx b/frontend/components/table/index.tsx index f6d01b48..30e2648c 100644 --- a/frontend/components/table/index.tsx +++ b/frontend/components/table/index.tsx @@ -1,7 +1,7 @@ import { flexRender, Table as ReactTable } from "@tanstack/react-table"; import { Table as CTable, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table"; -export function Table({ table }: { table: ReactTable }) { +export function Table({ table, onRowClick }: { table: ReactTable; onRowClick?: (row: T) => void }) { return ( @@ -19,7 +19,14 @@ export function Table({ table }: { table: ReactTable }) { {table.getRowModel().rows.map((row) => ( - + onRowClick?.(row.original)} + className={[ + onRowClick ? "cursor-pointer hover:bg-muted/50" : "", + row.depth > 0 ? "bg-muted/100" : "", + ].join(" ")} + > {row.getVisibleCells().map((cell) => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} diff --git a/frontend/components/ui/sheet.tsx b/frontend/components/ui/sheet.tsx new file mode 100644 index 00000000..6f23c5c7 --- /dev/null +++ b/frontend/components/ui/sheet.tsx @@ -0,0 +1,103 @@ +"use client"; + +import * as React from "react"; +import * as SheetPrimitive from "@radix-ui/react-dialog"; +import { ArrowLeft } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +function Sheet({ ...props }: React.ComponentProps) { + return ; +} + +function SheetTrigger({ ...props }: React.ComponentProps) { + return ; +} + +function SheetClose({ ...props }: React.ComponentProps) { + return ; +} + +function SheetPortal({ ...props }: React.ComponentProps) { + return ; +} + +function SheetOverlay({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +function SheetContent({ + className, + children, + side = "right", + ...props +}: React.ComponentProps & { + side?: "top" | "right" | "bottom" | "left"; +}) { + return ( + + + + {children} + + + Close + + + + ); +} + +function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { + return
; +} + +function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { + return
; +} + +function SheetTitle({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +function SheetDescription({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +export { Sheet, SheetTrigger, SheetClose, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription }; diff --git a/frontend/schema.d.ts b/frontend/schema.d.ts index 5128ada4..4272f0f0 100644 --- a/frontend/schema.d.ts +++ b/frontend/schema.d.ts @@ -304,8 +304,8 @@ export interface paths { id: string; name: string; businessOwnerFullName: string; - lastQuickBooksInvoiceImportTime?: string | unknown | unknown; - lastQuickBooksPurchaseImportTime?: string | unknown | unknown; + lastQuickBooksInvoiceImportTime?: string | null; + lastQuickBooksPurchaseImportTime?: string | null; externals?: { id: string; source: string; @@ -320,7 +320,7 @@ export interface paths { /** @enum {string} */ companyType: "LLC" | "Sole Proprietorship" | "Corporation" | "Partnership"; createdAt: string; - updatedAt?: string; + updatedAt: string; }; }; }; @@ -390,8 +390,8 @@ export interface paths { id: string; name: string; businessOwnerFullName: string; - lastQuickBooksInvoiceImportTime?: string | unknown | unknown; - lastQuickBooksPurchaseImportTime?: string | unknown | unknown; + lastQuickBooksInvoiceImportTime?: string | null; + lastQuickBooksPurchaseImportTime?: string | null; externals?: { id: string; source: string; @@ -406,7 +406,7 @@ export interface paths { /** @enum {string} */ companyType: "LLC" | "Sole Proprietorship" | "Corporation" | "Partnership"; createdAt: string; - updatedAt?: string; + updatedAt: string; }; }; }; @@ -478,8 +478,8 @@ export interface paths { id: string; name: string; businessOwnerFullName: string; - lastQuickBooksInvoiceImportTime?: string | unknown | unknown; - lastQuickBooksPurchaseImportTime?: string | unknown | unknown; + lastQuickBooksInvoiceImportTime?: string | null; + lastQuickBooksPurchaseImportTime?: string | null; externals?: { id: string; source: string; @@ -494,7 +494,7 @@ export interface paths { /** @enum {string} */ companyType: "LLC" | "Sole Proprietorship" | "Corporation" | "Partnership"; createdAt: string; - updatedAt?: string; + updatedAt: string; }; }; }; @@ -571,8 +571,8 @@ export interface paths { id: string; name: string; businessOwnerFullName: string; - lastQuickBooksInvoiceImportTime?: string | unknown | unknown; - lastQuickBooksPurchaseImportTime?: string | unknown | unknown; + lastQuickBooksInvoiceImportTime?: string | null; + lastQuickBooksPurchaseImportTime?: string | null; externals?: { id: string; source: string; @@ -587,7 +587,7 @@ export interface paths { /** @enum {string} */ companyType: "LLC" | "Sole Proprietorship" | "Corporation" | "Partnership"; createdAt: string; - updatedAt?: string; + updatedAt: string; }; }; }; @@ -664,8 +664,8 @@ export interface paths { id: string; name: string; businessOwnerFullName: string; - lastQuickBooksInvoiceImportTime?: string | unknown | unknown; - lastQuickBooksPurchaseImportTime?: string | unknown | unknown; + lastQuickBooksInvoiceImportTime?: string | null; + lastQuickBooksPurchaseImportTime?: string | null; externals?: { id: string; source: string; @@ -680,7 +680,7 @@ export interface paths { /** @enum {string} */ companyType: "LLC" | "Sole Proprietorship" | "Corporation" | "Partnership"; createdAt: string; - updatedAt?: string; + updatedAt: string; }; }; }; @@ -1619,8 +1619,8 @@ export interface paths { id: string; name: string; businessOwnerFullName: string; - lastQuickBooksInvoiceImportTime?: string | unknown | unknown; - lastQuickBooksPurchaseImportTime?: string | unknown | unknown; + lastQuickBooksInvoiceImportTime?: string | null; + lastQuickBooksPurchaseImportTime?: string | null; externals?: { id: string; source: string; @@ -1635,7 +1635,7 @@ export interface paths { /** @enum {string} */ companyType: "LLC" | "Sole Proprietorship" | "Corporation" | "Partnership"; createdAt: string; - updatedAt?: string; + updatedAt: string; }; }; }[]; @@ -2913,35 +2913,38 @@ export interface paths { }; content: { "application/json": { - id: string; - companyId: string; - quickBooksId?: number; - totalAmountCents: number; - quickbooksDateCreated?: string; - isRefund: boolean; - vendor?: string; - dateCreated: string; - lastUpdated: string; - lineItems: { + purchases: { id: string; - description?: string; + companyId: string; quickBooksId?: number; - purchaseId: string; - amountCents: number; - category?: string | null; - /** @enum {string} */ - type: - | "extraneous" - | "typical" - | "pending" - | "suggested extraneous" - | "suggested typical"; + totalAmountCents: number; + quickbooksDateCreated?: string; + isRefund: boolean; + vendor?: string; dateCreated: string; lastUpdated: string; - /** Format: date-time */ - quickbooksDateCreated?: string; + lineItems: { + id: string; + description?: string; + quickBooksId?: number; + purchaseId: string; + amountCents: number; + category?: string | null; + /** @enum {string} */ + type: + | "extraneous" + | "typical" + | "pending" + | "suggested extraneous" + | "suggested typical"; + dateCreated: string; + lastUpdated: string; + /** Format: date-time */ + quickbooksDateCreated?: string; + }[]; }[]; - }[]; + numPurchases: number; + }; }; }; /** @description Get company purchases error */ @@ -6019,8 +6022,8 @@ export interface paths { id: string; name: string; businessOwnerFullName: string; - lastQuickBooksInvoiceImportTime?: string | unknown | unknown; - lastQuickBooksPurchaseImportTime?: string | unknown | unknown; + lastQuickBooksInvoiceImportTime?: string | null; + lastQuickBooksPurchaseImportTime?: string | null; externals?: { id: string; source: string; @@ -6035,7 +6038,7 @@ export interface paths { /** @enum {string} */ companyType: "LLC" | "Sole Proprietorship" | "Corporation" | "Partnership"; createdAt: string; - updatedAt?: string; + updatedAt: string; }; claim?: { id: string; diff --git a/frontend/types/disaster.ts b/frontend/types/disaster.ts index aaed9ac8..b14b8960 100644 --- a/frontend/types/disaster.ts +++ b/frontend/types/disaster.ts @@ -1,3 +1,23 @@ import { paths } from "@/schema"; +import { DisasterType } from "./purchase"; export type FemaDisaster = paths["/disaster"]["post"]["responses"][201]["content"]["application/json"]; + +export const DISASTER_TYPE_LABELS = new Map([ + ["typical", "Non-Disaster"], + ["extraneous", "Disaster"], + ["suggested extraneous", "Suggested: Disaster"], + ["suggested typical", "Suggested: Typical"], + ["pending", "Pending"], +]); +export const DISASTER_TYPE_LABELS_TO_CHANGE = new Map([ + ["typical", "Non-Disaster"], + ["extraneous", "Disaster"], +]); +export const DISASTER_TYPE_COLORS = new Map([ + ["typical", "bg-teal-100 text-teal-800 border border-teal-200"], + ["extraneous", "bg-pink-100 text-pink-800 border border-pink-200"], + ["pending", "bg-grey-100 text-grey-800 border border-grey-200"], + ["suggested extraneous", "bg-yellow-100 text-yellow-800 border border-yellow-200"], + ["suggested typical", "bg-blue-100 text-blue-800 border border-blue-200"], +]); diff --git a/frontend/types/purchase.ts b/frontend/types/purchase.ts index c408b471..4035a3cc 100644 --- a/frontend/types/purchase.ts +++ b/frontend/types/purchase.ts @@ -2,8 +2,10 @@ import type { paths } from "../schema"; export type Purchase = paths["/purchase/{id}"]["get"]["responses"]["200"]["content"]["application/json"]; export type CreatePurchaseInput = paths["/purchase/bulk"]["post"]["requestBody"]["content"]["application/json"]; export type CreatePurchaseResponse = paths["/purchase/bulk"]["post"]["responses"]["200"]["content"]["application/json"]; -export type Purchases = paths["/purchase"]["get"]["responses"]["200"]["content"]["application/json"]; +export type PurchasesWithCount = paths["/purchase"]["get"]["responses"]["200"]["content"]["application/json"]; +export type Purchases = PurchasesWithCount["purchases"]; export type PurchaseLineItem = paths["/purchase/line/{id}"]["get"]["responses"]["200"]["content"]["application/json"]; +export type PurchaseWithLineItems = Purchases[number]; export enum PurchaseLineItemType { EXTRANEOUS = "extraneous", diff --git a/spec.json b/spec.json index 64cc0ae6..bff9ebde 100644 --- a/spec.json +++ b/spec.json @@ -463,38 +463,12 @@ "minLength": 1 }, "lastQuickBooksInvoiceImportTime": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "string", - "format": "date" - }, - { - "nullable": true - }, - { - "nullable": true - } - ] + "type": "string", + "nullable": true }, "lastQuickBooksPurchaseImportTime": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "string", - "format": "date" - }, - { - "nullable": true - }, - { - "nullable": true - } - ] + "type": "string", + "nullable": true }, "externals": { "type": "array", @@ -547,26 +521,10 @@ ] }, "createdAt": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "string", - "format": "date" - } - ] + "type": "string" }, "updatedAt": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "string", - "format": "date" - } - ] + "type": "string" } }, "required": [ @@ -574,7 +532,8 @@ "name", "businessOwnerFullName", "companyType", - "createdAt" + "createdAt", + "updatedAt" ] } } @@ -647,38 +606,12 @@ "minLength": 1 }, "lastQuickBooksInvoiceImportTime": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "string", - "format": "date" - }, - { - "nullable": true - }, - { - "nullable": true - } - ] + "type": "string", + "nullable": true }, "lastQuickBooksPurchaseImportTime": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "string", - "format": "date" - }, - { - "nullable": true - }, - { - "nullable": true - } - ] + "type": "string", + "nullable": true }, "externals": { "type": "array", @@ -731,26 +664,10 @@ ] }, "createdAt": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "string", - "format": "date" - } - ] + "type": "string" }, "updatedAt": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "string", - "format": "date" - } - ] + "type": "string" } }, "required": [ @@ -758,7 +675,8 @@ "name", "businessOwnerFullName", "companyType", - "createdAt" + "createdAt", + "updatedAt" ] } } @@ -860,38 +778,12 @@ "minLength": 1 }, "lastQuickBooksInvoiceImportTime": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "string", - "format": "date" - }, - { - "nullable": true - }, - { - "nullable": true - } - ] + "type": "string", + "nullable": true }, "lastQuickBooksPurchaseImportTime": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "string", - "format": "date" - }, - { - "nullable": true - }, - { - "nullable": true - } - ] + "type": "string", + "nullable": true }, "externals": { "type": "array", @@ -944,26 +836,10 @@ ] }, "createdAt": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "string", - "format": "date" - } - ] + "type": "string" }, "updatedAt": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "string", - "format": "date" - } - ] + "type": "string" } }, "required": [ @@ -971,7 +847,8 @@ "name", "businessOwnerFullName", "companyType", - "createdAt" + "createdAt", + "updatedAt" ] } } @@ -1072,38 +949,12 @@ "minLength": 1 }, "lastQuickBooksInvoiceImportTime": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "string", - "format": "date" - }, - { - "nullable": true - }, - { - "nullable": true - } - ] + "type": "string", + "nullable": true }, "lastQuickBooksPurchaseImportTime": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "string", - "format": "date" - }, - { - "nullable": true - }, - { - "nullable": true - } - ] + "type": "string", + "nullable": true }, "externals": { "type": "array", @@ -1156,26 +1007,10 @@ ] }, "createdAt": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "string", - "format": "date" - } - ] + "type": "string" }, "updatedAt": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "string", - "format": "date" - } - ] + "type": "string" } }, "required": [ @@ -1183,7 +1018,8 @@ "name", "businessOwnerFullName", "companyType", - "createdAt" + "createdAt", + "updatedAt" ] } } @@ -1284,38 +1120,12 @@ "minLength": 1 }, "lastQuickBooksInvoiceImportTime": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "string", - "format": "date" - }, - { - "nullable": true - }, - { - "nullable": true - } - ] + "type": "string", + "nullable": true }, "lastQuickBooksPurchaseImportTime": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "string", - "format": "date" - }, - { - "nullable": true - }, - { - "nullable": true - } - ] + "type": "string", + "nullable": true }, "externals": { "type": "array", @@ -1368,26 +1178,10 @@ ] }, "createdAt": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "string", - "format": "date" - } - ] + "type": "string" }, "updatedAt": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "string", - "format": "date" - } - ] + "type": "string" } }, "required": [ @@ -1395,7 +1189,8 @@ "name", "businessOwnerFullName", "companyType", - "createdAt" + "createdAt", + "updatedAt" ] } } @@ -3141,38 +2936,12 @@ "minLength": 1 }, "lastQuickBooksInvoiceImportTime": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "string", - "format": "date" - }, - { - "nullable": true - }, - { - "nullable": true - } - ] + "type": "string", + "nullable": true }, "lastQuickBooksPurchaseImportTime": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "string", - "format": "date" - }, - { - "nullable": true - }, - { - "nullable": true - } - ] + "type": "string", + "nullable": true }, "externals": { "type": "array", @@ -3225,26 +2994,10 @@ ] }, "createdAt": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "string", - "format": "date" - } - ] + "type": "string" }, "updatedAt": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "string", - "format": "date" - } - ] + "type": "string" } }, "required": [ @@ -3252,7 +3005,8 @@ "name", "businessOwnerFullName", "companyType", - "createdAt" + "createdAt", + "updatedAt" ] } }, @@ -5167,108 +4921,121 @@ "content": { "application/json": { "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "minLength": 1 - }, - "companyId": { - "type": "string", - "minLength": 1 - }, - "quickBooksId": { - "type": "number" - }, - "totalAmountCents": { - "type": "number", - "minimum": 0 - }, - "quickbooksDateCreated": { - "type": "string" - }, - "isRefund": { - "type": "boolean" - }, - "vendor": { - "type": "string" - }, - "dateCreated": { - "type": "string" - }, - "lastUpdated": { - "type": "string" - }, - "lineItems": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "minLength": 1 - }, - "description": { - "type": "string" - }, - "quickBooksId": { - "type": "number" - }, - "purchaseId": { - "type": "string" - }, - "amountCents": { - "type": "number" - }, - "category": { - "type": "string", - "nullable": true, - "minLength": 1 - }, - "type": { - "type": "string", - "enum": [ - "extraneous", - "typical", - "pending", - "suggested extraneous", - "suggested typical" + "type": "object", + "properties": { + "purchases": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "companyId": { + "type": "string", + "minLength": 1 + }, + "quickBooksId": { + "type": "number" + }, + "totalAmountCents": { + "type": "number", + "minimum": 0 + }, + "quickbooksDateCreated": { + "type": "string" + }, + "isRefund": { + "type": "boolean" + }, + "vendor": { + "type": "string" + }, + "dateCreated": { + "type": "string" + }, + "lastUpdated": { + "type": "string" + }, + "lineItems": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": "string" + }, + "quickBooksId": { + "type": "number" + }, + "purchaseId": { + "type": "string" + }, + "amountCents": { + "type": "number" + }, + "category": { + "type": "string", + "nullable": true, + "minLength": 1 + }, + "type": { + "type": "string", + "enum": [ + "extraneous", + "typical", + "pending", + "suggested extraneous", + "suggested typical" + ] + }, + "dateCreated": { + "type": "string" + }, + "lastUpdated": { + "type": "string" + }, + "quickbooksDateCreated": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "purchaseId", + "amountCents", + "type", + "dateCreated", + "lastUpdated" ] - }, - "dateCreated": { - "type": "string" - }, - "lastUpdated": { - "type": "string" - }, - "quickbooksDateCreated": { - "type": "string", - "format": "date-time" } - }, - "required": [ - "id", - "purchaseId", - "amountCents", - "type", - "dateCreated", - "lastUpdated" - ] - } + } + }, + "required": [ + "id", + "companyId", + "totalAmountCents", + "isRefund", + "dateCreated", + "lastUpdated", + "lineItems" + ] } }, - "required": [ - "id", - "companyId", - "totalAmountCents", - "isRefund", - "dateCreated", - "lastUpdated", - "lineItems" - ] - } + "numPurchases": { + "type": "number", + "minimum": 0 + } + }, + "required": [ + "purchases", + "numPurchases" + ] } } } @@ -10262,38 +10029,12 @@ "minLength": 1 }, "lastQuickBooksInvoiceImportTime": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "string", - "format": "date" - }, - { - "nullable": true - }, - { - "nullable": true - } - ] + "type": "string", + "nullable": true }, "lastQuickBooksPurchaseImportTime": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "string", - "format": "date" - }, - { - "nullable": true - }, - { - "nullable": true - } - ] + "type": "string", + "nullable": true }, "externals": { "type": "array", @@ -10346,26 +10087,10 @@ ] }, "createdAt": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "string", - "format": "date" - } - ] + "type": "string" }, "updatedAt": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "string", - "format": "date" - } - ] + "type": "string" } }, "required": [ @@ -10373,7 +10098,8 @@ "name", "businessOwnerFullName", "companyType", - "createdAt" + "createdAt", + "updatedAt" ] }, "claim": {