diff --git a/frontend/lib/api/__test__/user/items.test.ts b/frontend/lib/api/__test__/user/items.test.ts index 7837e50..1a5f94e 100644 --- a/frontend/lib/api/__test__/user/items.test.ts +++ b/frontend/lib/api/__test__/user/items.test.ts @@ -33,6 +33,7 @@ describe("user should be able to create an item and add an attachment", () => { const [location, cleanup] = await useLocation(api); const { response, data: item } = await api.items.create({ + parentId: null, name: "test-item", labelIds: [], description: "test-description", @@ -43,7 +44,7 @@ describe("user should be able to create an item and add an attachment", () => { // Add attachment { const testFile = new Blob(["test"], { type: "text/plain" }); - const { response } = await api.items.addAttachment(item.id, testFile, "test.txt", AttachmentTypes.Attachment); + const { response } = await api.items.attachments.add(item.id, testFile, "test.txt", AttachmentTypes.Attachment); expect(response.status).toBe(201); } @@ -54,7 +55,7 @@ describe("user should be able to create an item and add an attachment", () => { expect(data.attachments).toHaveLength(1); expect(data.attachments[0].document.title).toBe("test.txt"); - const resp = await api.items.deleteAttachment(data.id, data.attachments[0].id); + const resp = await api.items.attachments.delete(data.id, data.attachments[0].id); expect(resp.response.status).toBe(204); api.items.delete(item.id); @@ -66,6 +67,7 @@ describe("user should be able to create an item and add an attachment", () => { const [location, cleanup] = await useLocation(api); const { response, data: item } = await api.items.create({ + parentId: null, name: faker.vehicle.model(), labelIds: [], description: faker.lorem.paragraph(1), @@ -82,6 +84,7 @@ describe("user should be able to create an item and add an attachment", () => { // Add fields const itemUpdate = { + parentId: null, ...item, locationId: item.location.id, labelIds: item.labels.map(l => l.id), @@ -113,4 +116,41 @@ describe("user should be able to create an item and add an attachment", () => { cleanup(); }); + + test("users should be able to create and few maintenance logs for an item", async () => { + const api = await sharedUserClient(); + const [location, cleanup] = await useLocation(api); + const { response, data: item } = await api.items.create({ + parentId: null, + name: faker.vehicle.model(), + labelIds: [], + description: faker.lorem.paragraph(1), + locationId: location.id, + }); + expect(response.status).toBe(201); + + const maintenanceEntries = []; + for (let i = 0; i < 5; i++) { + const { response, data } = await api.items.maintenance.create(item.id, { + name: faker.vehicle.model(), + description: faker.lorem.paragraph(1), + date: faker.date.past(1), + cost: faker.datatype.number(100).toString(), + }); + + expect(response.status).toBe(201); + maintenanceEntries.push(data); + } + + // Log + { + const { response, data } = await api.items.maintenance.getLog(item.id); + expect(response.status).toBe(200); + expect(data.entries).toHaveLength(maintenanceEntries.length); + expect(data.costAverage).toBeGreaterThan(0); + expect(data.costTotal).toBeGreaterThan(0); + } + + cleanup(); + }); }); diff --git a/frontend/lib/api/classes/items.ts b/frontend/lib/api/classes/items.ts index f4fb38d..8522852 100644 --- a/frontend/lib/api/classes/items.ts +++ b/frontend/lib/api/classes/items.ts @@ -1,7 +1,18 @@ import { BaseAPI, route } from "../base"; import { parseDate } from "../base/base-api"; -import { ItemAttachmentUpdate, ItemCreate, ItemOut, ItemSummary, ItemUpdate } from "../types/data-contracts"; +import { + ItemAttachmentUpdate, + ItemCreate, + ItemOut, + ItemSummary, + ItemUpdate, + MaintenanceEntry, + MaintenanceEntryCreate, + MaintenanceEntryUpdate, + MaintenanceLog, +} from "../types/data-contracts"; import { AttachmentTypes, PaginationResult } from "../types/non-generated"; +import { Requests } from "~~/lib/requests"; export type ItemsQuery = { includeArchived?: boolean; @@ -12,7 +23,65 @@ export type ItemsQuery = { q?: string; }; +export class AttachmentsAPI extends BaseAPI { + add(id: string, file: File | Blob, filename: string, type: AttachmentTypes) { + const formData = new FormData(); + formData.append("file", file); + formData.append("type", type); + formData.append("name", filename); + + return this.http.post({ + url: route(`/items/${id}/attachments`), + data: formData, + }); + } + + delete(id: string, attachmentId: string) { + return this.http.delete({ url: route(`/items/${id}/attachments/${attachmentId}`) }); + } + + update(id: string, attachmentId: string, data: ItemAttachmentUpdate) { + return this.http.put({ + url: route(`/items/${id}/attachments/${attachmentId}`), + body: data, + }); + } +} + +export class MaintenanceAPI extends BaseAPI { + getLog(itemId: string) { + return this.http.get({ url: route(`/items/${itemId}/maintenance`) }); + } + + create(itemId: string, data: MaintenanceEntryCreate) { + return this.http.post({ + url: route(`/items/${itemId}/maintenance`), + body: data, + }); + } + + delete(itemId: string, entryId: string) { + return this.http.delete({ url: route(`/items/${itemId}/maintenance/${entryId}`) }); + } + + update(itemId: string, entryId: string, data: MaintenanceEntryUpdate) { + return this.http.put({ + url: route(`/items/${itemId}/maintenance/${entryId}`), + body: data, + }); + } +} + export class ItemsApi extends BaseAPI { + attachments: AttachmentsAPI; + maintenance: MaintenanceAPI; + + constructor(http: Requests, token: string) { + super(http, token); + this.attachments = new AttachmentsAPI(http); + this.maintenance = new MaintenanceAPI(http); + } + getAll(q: ItemsQuery = {}) { return this.http.get>({ url: route("/items", q) }); } @@ -59,27 +128,4 @@ export class ItemsApi extends BaseAPI { data: formData, }); } - - addAttachment(id: string, file: File | Blob, filename: string, type: AttachmentTypes) { - const formData = new FormData(); - formData.append("file", file); - formData.append("type", type); - formData.append("name", filename); - - return this.http.post({ - url: route(`/items/${id}/attachments`), - data: formData, - }); - } - - async deleteAttachment(id: string, attachmentId: string) { - return await this.http.delete({ url: route(`/items/${id}/attachments/${attachmentId}`) }); - } - - async updateAttachment(id: string, attachmentId: string, data: ItemAttachmentUpdate) { - return await this.http.put({ - url: route(`/items/${id}/attachments/${attachmentId}`), - body: data, - }); - } } diff --git a/frontend/lib/api/types/data-contracts.ts b/frontend/lib/api/types/data-contracts.ts index 404aa5b..e313175 100644 --- a/frontend/lib/api/types/data-contracts.ts +++ b/frontend/lib/api/types/data-contracts.ts @@ -223,6 +223,38 @@ export interface LocationUpdate { parentId: string | null; } +export interface MaintenanceEntry { + /** @example "0" */ + cost: string; + date: Date; + description: string; + id: string; + name: string; +} + +export interface MaintenanceEntryCreate { + /** @example "0" */ + cost: string; + date: Date; + description: string; + name: string; +} + +export interface MaintenanceEntryUpdate { + /** @example "0" */ + cost: string; + date: Date; + description: string; + name: string; +} + +export interface MaintenanceLog { + costAverage: number; + costTotal: number; + entries: MaintenanceEntry[]; + itemId: string; +} + export interface PaginationResultRepoItemSummary { items: ItemSummary[]; page: number; @@ -260,7 +292,7 @@ export interface ValueOverTime { } export interface ValueOverTimeEntry { - date: string; + date: Date; name: string; value: number; } diff --git a/frontend/pages/item/[id]/edit.vue b/frontend/pages/item/[id]/edit.vue index 0ed53d3..98b9f2a 100644 --- a/frontend/pages/item/[id]/edit.vue +++ b/frontend/pages/item/[id]/edit.vue @@ -214,7 +214,7 @@ return; } - const { data, error } = await api.items.addAttachment(itemId.value, files[0], files[0].name, type); + const { data, error } = await api.items.attachments.add(itemId.value, files[0], files[0].name, type); if (error) { toast.error("Failed to upload attachment"); @@ -235,7 +235,7 @@ return; } - const { error } = await api.items.deleteAttachment(itemId.value, attachmentId); + const { error } = await api.items.attachments.delete(itemId.value, attachmentId); if (error) { toast.error("Failed to delete attachment"); @@ -273,7 +273,7 @@ async function updateAttachment() { editState.loading = true; - const { error, data } = await api.items.updateAttachment(itemId.value, editState.id, { + const { error, data } = await api.items.attachments.update(itemId.value, editState.id, { title: editState.title, type: editState.type, }); diff --git a/scripts/process-types.py b/scripts/process-types.py index 00896b7..1641987 100644 --- a/scripts/process-types.py +++ b/scripts/process-types.py @@ -34,6 +34,7 @@ regex_replace: dict[re.Pattern, str] = { "purchaseTime", "warrantyExpires", "expiresAt", + "date", ), }