-
-
-
-
-
-
-
-
-
-
-
-
-
{{ month }} {{ year }}
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/frontend/components/Form/Multiselect.vue b/frontend/components/Form/Multiselect.vue
index 52cf2c5..e961f7f 100644
--- a/frontend/components/Form/Multiselect.vue
+++ b/frontend/components/Form/Multiselect.vue
@@ -38,12 +38,10 @@
default: "",
},
modelValue: {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
type: Array as () => any[],
default: null,
},
items: {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
type: Array as () => any[],
required: true,
},
diff --git a/frontend/components/Form/Password.vue b/frontend/components/Form/Password.vue
new file mode 100644
index 0000000..6303c0c
--- /dev/null
+++ b/frontend/components/Form/Password.vue
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
diff --git a/frontend/components/Form/Select.vue b/frontend/components/Form/Select.vue
index 09faed9..919720f 100644
--- a/frontend/components/Form/Select.vue
+++ b/frontend/components/Form/Select.vue
@@ -24,12 +24,10 @@
default: "",
},
modelValue: {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
type: [Object, String] as any,
default: null,
},
items: {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
type: Array as () => any[],
required: true,
},
@@ -86,7 +84,6 @@
{ immediate: true }
);
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
function compare(a: any, b: any): boolean {
if (a === b) {
return true;
diff --git a/frontend/components/Icon.vue b/frontend/components/Icon.vue
deleted file mode 100644
index db67841..0000000
--- a/frontend/components/Icon.vue
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
-
-
- {{ name }}
-
diff --git a/frontend/components/Item/AttachmentsList.vue b/frontend/components/Item/AttachmentsList.vue
index ddad600..69176ee 100644
--- a/frontend/components/Item/AttachmentsList.vue
+++ b/frontend/components/Item/AttachmentsList.vue
@@ -6,15 +6,15 @@
class="flex items-center justify-between py-3 pl-3 pr-4 text-sm"
>
-
+
{{ attachment.document.title }}
@@ -22,7 +22,10 @@
+
+
+
+
+ Items
+
+
+
+
+ -
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
No Items to Display
+
+
+
+
+
+
diff --git a/frontend/components/Item/View/Table.types.ts b/frontend/components/Item/View/Table.types.ts
new file mode 100644
index 0000000..b31ef88
--- /dev/null
+++ b/frontend/components/Item/View/Table.types.ts
@@ -0,0 +1,10 @@
+import type { ItemSummary } from "~~/lib/api/types/data-contracts";
+
+export type TableHeader = {
+ text: string;
+ value: keyof ItemSummary;
+ sortable?: boolean;
+ align?: "left" | "center" | "right";
+};
+
+export type TableData = Record
;
diff --git a/frontend/components/Item/View/Table.vue b/frontend/components/Item/View/Table.vue
new file mode 100644
index 0000000..ceed5d3
--- /dev/null
+++ b/frontend/components/Item/View/Table.vue
@@ -0,0 +1,192 @@
+
+
+
+
+
+
+
+ {{ h }}
+ {{ h.text }}
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+ {{ d.name }}
+
+
+
+
+
+
+
+
+
+
+ {{ extractValue(d, h.value) }}
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/components/Label/Chip.vue b/frontend/components/Label/Chip.vue
index 02d764d..3f7e801 100644
--- a/frontend/components/Label/Chip.vue
+++ b/frontend/components/Label/Chip.vue
@@ -1,5 +1,7 @@
diff --git a/frontend/components/Location/Card.vue b/frontend/components/Location/Card.vue
index d0b6543..c29bed8 100644
--- a/frontend/components/Location/Card.vue
+++ b/frontend/components/Location/Card.vue
@@ -13,18 +13,13 @@
>
{{ location.name }}
-
+
{{ count }}
@@ -33,7 +28,9 @@
diff --git a/frontend/components/Location/Selector.vue b/frontend/components/Location/Selector.vue
new file mode 100644
index 0000000..bcba455
--- /dev/null
+++ b/frontend/components/Location/Selector.vue
@@ -0,0 +1,57 @@
+
+
+
+
+
+ {{ cast(item.value).name }}
+
+
+
+
+
+ {{ cast(item.value).treeString }}
+
+
+
+
+
+
+
diff --git a/frontend/components/Location/Tree/Node.vue b/frontend/components/Location/Tree/Node.vue
new file mode 100644
index 0000000..8b72db6
--- /dev/null
+++ b/frontend/components/Location/Tree/Node.vue
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
{{ item.name }}
+
+
+
+
+
+
+
+
diff --git a/frontend/components/Location/Tree/Root.vue b/frontend/components/Location/Tree/Root.vue
new file mode 100644
index 0000000..c0f46d8
--- /dev/null
+++ b/frontend/components/Location/Tree/Root.vue
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
diff --git a/frontend/components/Location/Tree/tree-state.ts b/frontend/components/Location/Tree/tree-state.ts
new file mode 100644
index 0000000..f344394
--- /dev/null
+++ b/frontend/components/Location/Tree/tree-state.ts
@@ -0,0 +1,17 @@
+import type { Ref } from "vue";
+
+type TreeState = Record;
+
+const store: Record> = {};
+
+export function newTreeKey(): string {
+ return Math.random().toString(36).substring(2);
+}
+
+export function useTreeState(key: string): Ref {
+ if (!store[key]) {
+ store[key] = ref({});
+ }
+
+ return store[key];
+}
diff --git a/frontend/components/Search/Filter.vue b/frontend/components/Search/Filter.vue
new file mode 100644
index 0000000..328934a
--- /dev/null
+++ b/frontend/components/Search/Filter.vue
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/components/global/CopyText.vue b/frontend/components/global/CopyText.vue
index 953f098..fbaf0c4 100644
--- a/frontend/components/global/CopyText.vue
+++ b/frontend/components/global/CopyText.vue
@@ -6,17 +6,15 @@
'swap-active': copied,
}"
>
-
-
+ import MdiContentCopy from "~icons/mdi/content-copy";
+ import MdiClipboard from "~icons/mdi/clipboard";
+
const props = defineProps({
text: {
type: String as () => string,
@@ -51,5 +52,3 @@
}, 1000);
}
-
-
diff --git a/frontend/components/global/DateTime.vue b/frontend/components/global/DateTime.vue
index 9da127e..b23cac2 100644
--- a/frontend/components/global/DateTime.vue
+++ b/frontend/components/global/DateTime.vue
@@ -3,16 +3,18 @@
diff --git a/frontend/components/global/StatCard/StatCard.vue b/frontend/components/global/StatCard/StatCard.vue
index 37a386f..e4cf290 100644
--- a/frontend/components/global/StatCard/StatCard.vue
+++ b/frontend/components/global/StatCard/StatCard.vue
@@ -12,7 +12,7 @@
diff --git a/frontend/lib/api/__test__/factories/index.ts b/frontend/lib/api/__test__/factories/index.ts
index cebfb5f..0cba850 100644
--- a/frontend/lib/api/__test__/factories/index.ts
+++ b/frontend/lib/api/__test__/factories/index.ts
@@ -2,20 +2,21 @@ import { faker } from "@faker-js/faker";
import { expect } from "vitest";
import { overrideParts } from "../../base/urls";
import { PublicApi } from "../../public";
-import { ItemField, LabelCreate, LocationCreate, UserRegistration } from "../../types/data-contracts";
+import type { ItemField, LabelCreate, LocationCreate, UserRegistration } from "../../types/data-contracts";
import * as config from "../../../../test/config";
import { UserClient } from "../../user";
import { Requests } from "../../../requests";
function itemField(id = null): ItemField {
return {
+ // @ts-expect-error
id,
name: faker.lorem.word(),
type: "text",
textValue: faker.lorem.sentence(),
booleanValue: false,
- numberValue: faker.datatype.number(),
- timeValue: null,
+ numberValue: faker.number.int(),
+ timeValue: "",
};
}
@@ -27,14 +28,15 @@ function user(): UserRegistration {
return {
email: faker.internet.email(),
password: faker.internet.password(),
- name: faker.name.firstName(),
+ name: faker.person.firstName(),
token: "",
};
}
-function location(): LocationCreate {
+function location(parentId: string | null = null): LocationCreate {
return {
- name: faker.address.city(),
+ parentId,
+ name: faker.location.city(),
description: faker.lorem.sentence(),
};
}
@@ -56,7 +58,7 @@ function publicClient(): PublicApi {
function userClient(token: string): UserClient {
overrideParts(config.BASE_URL, "/api/v1");
const requests = new Requests("", token);
- return new UserClient(requests);
+ return new UserClient(requests, "");
}
type TestUser = {
@@ -75,7 +77,7 @@ async function userSingleUse(): Promise {
expect(result.status).toBe(200);
return {
- client: new UserClient(new Requests("", result.data.token)),
+ client: new UserClient(new Requests("", result.data.token), result.data.attachmentToken),
user: usr,
};
}
diff --git a/frontend/lib/api/__test__/test-utils.ts b/frontend/lib/api/__test__/test-utils.ts
index 41ef871..673773d 100644
--- a/frontend/lib/api/__test__/test-utils.ts
+++ b/frontend/lib/api/__test__/test-utils.ts
@@ -1,5 +1,6 @@
import { beforeAll, expect } from "vitest";
-import { UserClient } from "../user";
+import { faker } from "@faker-js/faker";
+import type { UserClient } from "../user";
import { factories } from "./factories";
const cache = {
@@ -15,9 +16,9 @@ export async function sharedUserClient(): Promise {
return factories.client.user(cache.token);
}
const testUser = {
- email: "__test__@__test__.com",
- name: "__test__",
- password: "__test__",
+ email: faker.internet.email(),
+ name: faker.person.fullName(),
+ password: faker.internet.password(),
token: "",
};
diff --git a/frontend/lib/api/__test__/user/group.test.ts b/frontend/lib/api/__test__/user/group.test.ts
index 181cfd5..4ad82b2 100644
--- a/frontend/lib/api/__test__/user/group.test.ts
+++ b/frontend/lib/api/__test__/user/group.test.ts
@@ -2,13 +2,12 @@ import { faker } from "@faker-js/faker";
import { describe, test, expect } from "vitest";
import { factories } from "../factories";
import { sharedUserClient } from "../test-utils";
-import { currencies } from "~~/lib/data/currency";
describe("first time user workflow (register, login, join group)", () => {
test("user should be able to update group", async () => {
const { client } = await factories.client.singleUse();
- const name = faker.name.firstName();
+ const name = faker.person.firstName();
const { response, data: group } = await client.group.update({
name,
@@ -29,20 +28,6 @@ describe("first time user workflow (register, login, join group)", () => {
expect(group.currency).toBe("USD");
});
- test("currencies should be in sync with backend", async () => {
- const { client } = await factories.client.singleUse();
-
- for (const currency of currencies) {
- const { response, data: group } = await client.group.update({
- name: faker.name.firstName(),
- currency: currency.code,
- });
-
- expect(response.status).toBe(200);
- expect(group.currency).toBe(currency.code);
- }
- });
-
test("user should be able to join create join token and have user signup", async () => {
const api = factories.client.public();
diff --git a/frontend/lib/api/__test__/user/items.test.ts b/frontend/lib/api/__test__/user/items.test.ts
index 1a5f94e..4f8973f 100644
--- a/frontend/lib/api/__test__/user/items.test.ts
+++ b/frontend/lib/api/__test__/user/items.test.ts
@@ -1,19 +1,20 @@
import { faker } from "@faker-js/faker";
import { describe, test, expect } from "vitest";
-import { ItemField, LocationOut } from "../../types/data-contracts";
+import type { ItemField, ItemUpdate, LocationOut } from "../../types/data-contracts";
import { AttachmentTypes } from "../../types/non-generated";
-import { UserClient } from "../../user";
+import type { UserClient } from "../../user";
import { factories } from "../factories";
import { sharedUserClient } from "../test-utils";
describe("user should be able to create an item and add an attachment", () => {
let increment = 0;
/**
- * useLocatio sets up a location resource for testing, and returns a function
+ * useLocation sets up a location resource for testing, and returns a function
* that can be used to delete the location from the backend server.
*/
async function useLocation(api: UserClient): Promise<[LocationOut, () => Promise]> {
const { response, data } = await api.locations.create({
+ parentId: null,
name: `__test__.location.name_${increment}`,
description: `__test__.location.description_${increment}`,
});
@@ -86,12 +87,12 @@ describe("user should be able to create an item and add an attachment", () => {
const itemUpdate = {
parentId: null,
...item,
- locationId: item.location.id,
+ locationId: item.location?.id || null,
labelIds: item.labels.map(l => l.id),
fields,
};
- const { response: updateResponse, data: item2 } = await api.items.update(item.id, itemUpdate);
+ const { response: updateResponse, data: item2 } = await api.items.update(item.id, itemUpdate as ItemUpdate);
expect(updateResponse.status).toBe(200);
expect(item2.fields).toHaveLength(fields.length);
@@ -104,7 +105,7 @@ describe("user should be able to create an item and add an attachment", () => {
itemUpdate.fields = [fields[0], fields[1]];
- const { response: updateResponse2, data: item3 } = await api.items.update(item.id, itemUpdate);
+ const { response: updateResponse2, data: item3 } = await api.items.update(item.id, itemUpdate as ItemUpdate);
expect(updateResponse2.status).toBe(200);
expect(item3.fields).toHaveLength(2);
@@ -134,8 +135,9 @@ describe("user should be able to create an item and add an attachment", () => {
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(),
+ completedDate: faker.date.past(),
+ scheduledDate: "null",
+ cost: faker.number.int(100).toString(),
});
expect(response.status).toBe(201);
@@ -153,4 +155,42 @@ describe("user should be able to create an item and add an attachment", () => {
cleanup();
});
+
+ test("full path of item should be retrievable", async () => {
+ const api = await sharedUserClient();
+ const [location, cleanup] = await useLocation(api);
+
+ const locations = [location.name, faker.animal.dog(), faker.animal.cat(), faker.animal.cow(), faker.animal.bear()];
+
+ let lastLocationId = location.id;
+ for (let i = 1; i < locations.length; i++) {
+ // Skip first one
+ const { response, data: loc } = await api.locations.create({
+ parentId: lastLocationId,
+ name: locations[i],
+ description: "",
+ });
+ expect(response.status).toBe(201);
+
+ lastLocationId = loc.id;
+ }
+
+ const { response, data: item } = await api.items.create({
+ name: faker.vehicle.model(),
+ labelIds: [],
+ description: faker.lorem.paragraph(1),
+ locationId: lastLocationId,
+ });
+ expect(response.status).toBe(201);
+
+ const { response: pathResponse, data: fullpath } = await api.items.fullpath(item.id);
+ expect(pathResponse.status).toBe(200);
+
+ const names = fullpath.map(p => p.name);
+
+ expect(names).toHaveLength(locations.length + 1);
+ expect(names).toEqual([...locations, item.name]);
+
+ cleanup();
+ });
});
diff --git a/frontend/lib/api/__test__/user/labels.test.ts b/frontend/lib/api/__test__/user/labels.test.ts
index 5564e6b..851f6b1 100644
--- a/frontend/lib/api/__test__/user/labels.test.ts
+++ b/frontend/lib/api/__test__/user/labels.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, test } from "vitest";
-import { LabelOut } from "../../types/data-contracts";
-import { UserClient } from "../../user";
+import type { LabelOut } from "../../types/data-contracts";
+import type { UserClient } from "../../user";
import { factories } from "../factories";
import { sharedUserClient } from "../test-utils";
diff --git a/frontend/lib/api/__test__/user/locations.test.ts b/frontend/lib/api/__test__/user/locations.test.ts
index eb7765e..834a567 100644
--- a/frontend/lib/api/__test__/user/locations.test.ts
+++ b/frontend/lib/api/__test__/user/locations.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, test } from "vitest";
-import { LocationOut } from "../../types/data-contracts";
-import { UserClient } from "../../user";
+import type { LocationOut } from "../../types/data-contracts";
+import type { UserClient } from "../../user";
import { factories } from "../factories";
import { sharedUserClient } from "../test-utils";
@@ -49,6 +49,8 @@ describe("locations lifecycle (create, update, delete)", () => {
const [location, cleanup] = await useLocation(api);
const updateData = {
+ id: location.id,
+ parentId: location.parent?.id,
name: "test-location-updated",
description: "test-description-updated",
};
diff --git a/frontend/lib/api/__test__/user/notifier.test.ts b/frontend/lib/api/__test__/user/notifier.test.ts
new file mode 100644
index 0000000..664b85e
--- /dev/null
+++ b/frontend/lib/api/__test__/user/notifier.test.ts
@@ -0,0 +1,59 @@
+import { faker } from "@faker-js/faker";
+import { describe, expect, test } from "vitest";
+import { factories } from "../factories";
+
+describe("basic notifier workflows", () => {
+ test("user should be able to create, update, and delete a notifier", async () => {
+ const { client } = await factories.client.singleUse();
+
+ // Create Notifier
+ const result = await client.notifiers.create({
+ name: faker.word.words(2),
+ url: "discord://" + faker.string.alphanumeric(10),
+ isActive: true,
+ });
+
+ expect(result.error).toBeFalsy();
+ expect(result.status).toBe(201);
+ expect(result.data).toBeTruthy();
+
+ const notifier = result.data;
+
+ // Update Notifier with new URL
+ {
+ const updateData = {
+ name: faker.word.words(2),
+ url: "discord://" + faker.string.alphanumeric(10),
+ isActive: true,
+ };
+
+ const updateResult = await client.notifiers.update(notifier.id, updateData);
+ expect(updateResult.error).toBeFalsy();
+ expect(updateResult.status).toBe(200);
+ expect(updateResult.data).toBeTruthy();
+ expect(updateResult.data.name).not.toBe(notifier.name);
+ }
+
+ // Update Notifier with empty URL
+ {
+ const updateData = {
+ name: faker.word.words(2),
+ url: null,
+ isActive: true,
+ };
+
+ const updateResult = await client.notifiers.update(notifier.id, updateData);
+ expect(updateResult.error).toBeFalsy();
+ expect(updateResult.status).toBe(200);
+ expect(updateResult.data).toBeTruthy();
+ expect(updateResult.data.name).not.toBe(notifier.name);
+ }
+
+ // Delete Notifier
+ {
+ const deleteResult = await client.notifiers.delete(notifier.id);
+ expect(deleteResult.error).toBeFalsy();
+ expect(deleteResult.status).toBe(204);
+ }
+ });
+});
diff --git a/frontend/lib/api/__test__/user/stats.test.ts b/frontend/lib/api/__test__/user/stats.test.ts
index 247b024..51f4e2e 100644
--- a/frontend/lib/api/__test__/user/stats.test.ts
+++ b/frontend/lib/api/__test__/user/stats.test.ts
@@ -1,30 +1,30 @@
import { faker } from "@faker-js/faker";
import { beforeAll, describe, expect, test } from "vitest";
-import { UserClient } from "../../user";
+import type { UserClient } from "../../user";
import { factories } from "../factories";
type ImportObj = {
- ImportRef: string;
- Location: string;
- Labels: string;
- Quantity: string;
- Name: string;
- Description: string;
- Insured: boolean;
- SerialNumber: string;
- ModelNumber: string;
- Manufacturer: string;
- Notes: string;
- PurchaseFrom: string;
- PurchasedPrice: number;
- PurchasedTime: string;
- LifetimeWarranty: boolean;
- WarrantyExpires: string;
- WarrantyDetails: string;
- SoldTo: string;
- SoldPrice: number;
- SoldTime: string;
- SoldNotes: string;
+ [`HB.import_ref`]: string;
+ [`HB.location`]: string;
+ [`HB.labels`]: string;
+ [`HB.quantity`]: number;
+ [`HB.name`]: string;
+ [`HB.description`]: string;
+ [`HB.insured`]: boolean;
+ [`HB.serial_number`]: string;
+ [`HB.model_number`]: string;
+ [`HB.manufacturer`]: string;
+ [`HB.notes`]: string;
+ [`HB.purchase_price`]: number;
+ [`HB.purchase_from`]: string;
+ [`HB.purchase_time`]: string;
+ [`HB.lifetime_warranty`]: boolean;
+ [`HB.warranty_expires`]: string;
+ [`HB.warranty_details`]: string;
+ [`HB.sold_to`]: string;
+ [`HB.sold_price`]: number;
+ [`HB.sold_time`]: string;
+ [`HB.sold_notes`]: string;
};
function toCsv(data: ImportObj[]): string {
@@ -36,66 +36,93 @@ function toCsv(data: ImportObj[]): string {
}
function importFileGenerator(entries: number): ImportObj[] {
- const imports: ImportObj[] = [];
+ const imports: Partial[] = [];
const pick = (arr: string[]) => arr[Math.floor(Math.random() * arr.length)];
- const labels = faker.random.words(5).split(" ").join(";");
- const locations = faker.random.words(3).split(" ");
+ const labels = faker.word.words(5).split(" ").join(";");
+ const locations = faker.word.words(3).split(" ");
const half = Math.floor(entries / 2);
+ // YYYY-MM-DD
+ const formatDate = (date: Date) => date.toISOString().split("T")[0];
+
for (let i = 0; i < entries; i++) {
imports.push({
- ImportRef: faker.database.mongodbObjectId(),
- Location: pick(locations),
- Labels: labels,
- Quantity: faker.random.numeric(1),
- Name: faker.random.words(3),
- Description: "",
- Insured: faker.datatype.boolean(),
- SerialNumber: faker.random.alphaNumeric(5),
- ModelNumber: faker.random.alphaNumeric(5),
- Manufacturer: faker.random.alphaNumeric(5),
- Notes: "",
- PurchaseFrom: faker.name.fullName(),
- PurchasedPrice: faker.datatype.number(100),
- PurchasedTime: faker.date.past().toDateString(),
- LifetimeWarranty: half > i,
- WarrantyExpires: faker.date.future().toDateString(),
- WarrantyDetails: "",
- SoldTo: faker.name.fullName(),
- SoldPrice: faker.datatype.number(100),
- SoldTime: faker.date.past().toDateString(),
- SoldNotes: "",
+ [`HB.import_ref`]: faker.database.mongodbObjectId(),
+ [`HB.location`]: pick(locations),
+ [`HB.labels`]: labels,
+ [`HB.quantity`]: Number(faker.number.int(2)),
+ [`HB.name`]: faker.word.words(3),
+ [`HB.description`]: "",
+ [`HB.insured`]: faker.datatype.boolean(),
+ [`HB.serial_number`]: faker.string.alphanumeric(5),
+ [`HB.model_number`]: faker.string.alphanumeric(5),
+ [`HB.manufacturer`]: faker.string.alphanumeric(5),
+ [`HB.notes`]: "",
+ [`HB.purchase_from`]: faker.person.fullName(),
+ [`HB.purchase_price`]: faker.number.int(100),
+ [`HB.purchase_time`]: faker.date.past().toDateString(),
+ [`HB.lifetime_warranty`]: half > i,
+ [`HB.warranty_details`]: "",
+ [`HB.sold_to`]: faker.person.fullName(),
+ [`HB.sold_price`]: faker.number.int(100),
+ [`HB.sold_time`]: formatDate(faker.date.past()),
+ [`HB.sold_notes`]: "",
});
}
- return imports;
+ return imports as ImportObj[];
}
describe("group related statistics tests", () => {
const TOTAL_ITEMS = 30;
+ const labelData: Record = {};
+ const locationData: Record = {};
- let api: UserClient | undefined;
+ let tAPI: UserClient | undefined;
const imports = importFileGenerator(TOTAL_ITEMS);
+ const api = (): UserClient => {
+ if (!tAPI) {
+ throw new Error("API not initialized");
+ }
+ return tAPI;
+ };
+
beforeAll(async () => {
// -- Setup --
const { client } = await factories.client.singleUse();
- api = client;
+ tAPI = client;
const csv = toCsv(imports);
const setupResp = await client.items.import(new Blob([csv], { type: "text/csv" }));
expect(setupResp.status).toBe(204);
+
+ for (const item of imports) {
+ const labels = item[`HB.labels`].split(";");
+ for (const label of labels) {
+ if (labelData[label]) {
+ labelData[label] += item[`HB.purchase_price`];
+ } else {
+ labelData[label] = item[`HB.purchase_price`];
+ }
+ }
+
+ const location = item[`HB.location`];
+ if (locationData[location]) {
+ locationData[location] += item[`HB.purchase_price`];
+ } else {
+ locationData[location] = item[`HB.purchase_price`];
+ }
+ }
});
- // Write to file system for debugging
- // fs.writeFileSync("test.csv", csv);
test("Validate Group Statistics", async () => {
- const { status, data } = await api.stats.group();
+ const { status, data } = await api().stats.group();
expect(status).toBe(200);
expect(data.totalItems).toEqual(TOTAL_ITEMS);
@@ -105,19 +132,8 @@ describe("group related statistics tests", () => {
expect(data.totalWithWarranty).toEqual(Math.floor(TOTAL_ITEMS / 2));
});
- const labelData: Record = {};
- const locationData: Record = {};
-
- for (const item of imports) {
- for (const label of item.Labels.split(";")) {
- labelData[label] = (labelData[label] || 0) + item.PurchasedPrice;
- }
-
- locationData[item.Location] = (locationData[item.Location] || 0) + item.PurchasedPrice;
- }
-
test("Validate Labels Statistics", async () => {
- const { status, data } = await api.stats.labels();
+ const { status, data } = await api().stats.labels();
expect(status).toBe(200);
for (const label of data) {
@@ -126,7 +142,7 @@ describe("group related statistics tests", () => {
});
test("Validate Locations Statistics", async () => {
- const { status, data } = await api.stats.locations();
+ const { status, data } = await api().stats.locations();
expect(status).toBe(200);
for (const location of data) {
@@ -135,7 +151,7 @@ describe("group related statistics tests", () => {
});
test("Validate Purchase Over Time", async () => {
- const { status, data } = await api.stats.totalPriceOverTime();
+ const { status, data } = await api().stats.totalPriceOverTime();
expect(status).toBe(200);
expect(data.entries.length).toEqual(TOTAL_ITEMS);
});
diff --git a/frontend/lib/api/base/base-api.ts b/frontend/lib/api/base/base-api.ts
index 0138b5b..b9d01ae 100644
--- a/frontend/lib/api/base/base-api.ts
+++ b/frontend/lib/api/base/base-api.ts
@@ -1,24 +1,27 @@
-import { Requests } from "../../requests";
+import type { Requests } from "../../requests";
+import { route } from ".";
const ZERO_DATE = "0001-01-01T00:00:00Z";
type BaseApiType = {
createdAt: string;
updatedAt: string;
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
+
[key: string]: any;
};
-export function hasKey(obj: object, key: string): obj is Required {
- return typeof obj[key] === "string";
+export function hasKey(obj: Record, key: string): obj is Required {
+ return key in obj ? typeof obj[key] === "string" : false;
}
export function parseDate(obj: T, keys: Array = []): T {
const result = { ...obj };
[...keys, "createdAt", "updatedAt"].forEach(key => {
- // @ts-ignore - TS doesn't know that we're checking for the key above
+ // @ts-expect-error - TS doesn't know that we're checking for the key above
if (hasKey(result, key)) {
- if (result[key] === ZERO_DATE) {
+ const value = result[key] as string;
+
+ if (value === undefined || value === "" || value.startsWith(ZERO_DATE)) {
const dt = new Date();
dt.setFullYear(1);
@@ -26,9 +29,33 @@ export function parseDate(obj: T, keys: Array = []): T {
return;
}
- // Ensure date like format YYYY/MM/DD - otherwise results will be 1 day off
- const dateStr: string = result[key].split("T")[0].replace(/-/g, "/");
- result[key] = new Date(dateStr);
+ // Possible Formats
+ // Date Only: YYYY-MM-DD
+ // Timestamp: 0001-01-01T00:00:00Z
+
+ // Parse timestamps with default date
+ if (value.includes("T")) {
+ result[key] = new Date(value);
+ return;
+ }
+
+ // Parse dates with default time
+ const split = value.split("-");
+
+ if (split.length !== 3) {
+ console.log(`Invalid date format: ${value}`);
+ throw new Error(`Invalid date format: ${value}`);
+ }
+
+ const [year, month, day] = split;
+
+ const dt = new Date();
+
+ dt.setFullYear(parseInt(year, 10));
+ dt.setMonth(parseInt(month, 10) - 1);
+ dt.setDate(parseInt(day, 10));
+
+ result[key] = dt;
}
});
@@ -44,12 +71,12 @@ export class BaseAPI {
this.attachmentToken = attachmentToken;
}
- // if a attachmentToken is present it will be added to URL as a query param
+ // if an attachmentToken is present, it will be added to URL as a query param
// this is done with a simple appending of the query param to the URL. If your
// URL already has a query param, this will not work.
authURL(url: string): string {
if (this.attachmentToken) {
- return `/api/v1${url}?access_token=${this.attachmentToken}`;
+ return route(url, { access_token: this.attachmentToken });
}
return url;
}
diff --git a/frontend/lib/api/base/urls.ts b/frontend/lib/api/base/urls.ts
index 31e263d..47a1c5b 100644
--- a/frontend/lib/api/base/urls.ts
+++ b/frontend/lib/api/base/urls.ts
@@ -11,13 +11,13 @@ export function overrideParts(host: string, prefix: string) {
export type QueryValue = string | string[] | number | number[] | boolean | null | undefined;
/**
- * route is a the main URL builder for the API. It will use a predefined host and prefix (global)
- * in the urls.ts file and then append the passed in path parameter uring the `URL` class from the
+ * route is the main URL builder for the API. It will use a predefined host and prefix (global)
+ * in the urls.ts file and then append the passed-in path parameter using the `URL` class from the
* browser. It will also append any query parameters passed in as the second parameter.
*
* The default host `http://localhost.com` is removed from the path if it is present. This allows us
* to bootstrap the API with different hosts as needed (like for testing) but still allows us to use
- * relative URLs in pruduction because the API and client bundle are served from the same server/host.
+ * relative URLs in production because the API and client bundle are served from the same server/host.
*/
export function route(rest: string, params: Record = {}): string {
const url = new URL(parts.prefix + rest, parts.host);
diff --git a/frontend/lib/api/classes/actions.ts b/frontend/lib/api/classes/actions.ts
index be892b3..3975a1d 100644
--- a/frontend/lib/api/classes/actions.ts
+++ b/frontend/lib/api/classes/actions.ts
@@ -1,10 +1,28 @@
import { BaseAPI, route } from "../base";
-import { EnsureAssetIDResult } from "../types/data-contracts";
+import type { ActionAmountResult } from "../types/data-contracts";
export class ActionsAPI extends BaseAPI {
ensureAssetIDs() {
- return this.http.post({
+ return this.http.post({
url: route("/actions/ensure-asset-ids"),
});
}
+
+ resetItemDateTimes() {
+ return this.http.post({
+ url: route("/actions/zero-item-time-fields"),
+ });
+ }
+
+ ensureImportRefs() {
+ return this.http.post({
+ url: route("/actions/ensure-import-refs"),
+ });
+ }
+
+ setPrimaryPhotos() {
+ return this.http.post({
+ url: route("/actions/set-primary-photos"),
+ });
+ }
}
diff --git a/frontend/lib/api/classes/assets.ts b/frontend/lib/api/classes/assets.ts
index 8820bb3..c22d01b 100644
--- a/frontend/lib/api/classes/assets.ts
+++ b/frontend/lib/api/classes/assets.ts
@@ -1,11 +1,11 @@
import { BaseAPI, route } from "../base";
-import { ItemSummary } from "../types/data-contracts";
-import { PaginationResult } from "../types/non-generated";
+import type { ItemSummary } from "../types/data-contracts";
+import type { PaginationResult } from "../types/non-generated";
export class AssetsApi extends BaseAPI {
async get(id: string, page = 1, pageSize = 50) {
return await this.http.get>({
- url: route(`/asset/${id}`, { page, pageSize }),
+ url: route(`/assets/${id}`, { page, pageSize }),
});
}
}
diff --git a/frontend/lib/api/classes/group.ts b/frontend/lib/api/classes/group.ts
index 7468f09..a33dbf9 100644
--- a/frontend/lib/api/classes/group.ts
+++ b/frontend/lib/api/classes/group.ts
@@ -1,5 +1,11 @@
import { BaseAPI, route } from "../base";
-import { Group, GroupInvitation, GroupInvitationCreate, GroupUpdate } from "../types/data-contracts";
+import type {
+ CurrenciesCurrency,
+ Group,
+ GroupInvitation,
+ GroupInvitationCreate,
+ GroupUpdate,
+} from "../types/data-contracts";
export class GroupApi extends BaseAPI {
createInvitation(data: GroupInvitationCreate) {
@@ -21,4 +27,10 @@ export class GroupApi extends BaseAPI {
url: route("/groups"),
});
}
+
+ currencies() {
+ return this.http.get({
+ url: route("/currencies"),
+ });
+ }
}
diff --git a/frontend/lib/api/classes/items.ts b/frontend/lib/api/classes/items.ts
index 8522852..a5d3f2e 100644
--- a/frontend/lib/api/classes/items.ts
+++ b/frontend/lib/api/classes/items.ts
@@ -1,9 +1,11 @@
import { BaseAPI, route } from "../base";
import { parseDate } from "../base/base-api";
-import {
+import type {
ItemAttachmentUpdate,
ItemCreate,
ItemOut,
+ ItemPatch,
+ ItemPath,
ItemSummary,
ItemUpdate,
MaintenanceEntry,
@@ -11,23 +13,28 @@ import {
MaintenanceEntryUpdate,
MaintenanceLog,
} from "../types/data-contracts";
-import { AttachmentTypes, PaginationResult } from "../types/non-generated";
-import { Requests } from "~~/lib/requests";
+import type { AttachmentTypes, PaginationResult } from "../types/non-generated";
+import type { Requests } from "~~/lib/requests";
export type ItemsQuery = {
+ orderBy?: string;
includeArchived?: boolean;
page?: number;
pageSize?: number;
locations?: string[];
labels?: string[];
+ parentIds?: string[];
q?: string;
+ fields?: string[];
};
export class AttachmentsAPI extends BaseAPI {
- add(id: string, file: File | Blob, filename: string, type: AttachmentTypes) {
+ add(id: string, file: File | Blob, filename: string, type: AttachmentTypes | null = null) {
const formData = new FormData();
formData.append("file", file);
- formData.append("type", type);
+ if (type) {
+ formData.append("type", type);
+ }
formData.append("name", filename);
return this.http.post({
@@ -48,9 +55,24 @@ export class AttachmentsAPI extends BaseAPI {
}
}
+export class FieldsAPI extends BaseAPI {
+ getAll() {
+ return this.http.get({ url: route("/items/fields") });
+ }
+
+ getAllValues(field: string) {
+ return this.http.get({ url: route(`/items/fields/values`, { field }) });
+ }
+}
+
+type MaintenanceEntryQuery = {
+ scheduled?: boolean;
+ completed?: boolean;
+};
+
export class MaintenanceAPI extends BaseAPI {
- getLog(itemId: string) {
- return this.http.get({ url: route(`/items/${itemId}/maintenance`) });
+ getLog(itemId: string, q: MaintenanceEntryQuery = {}) {
+ return this.http.get({ url: route(`/items/${itemId}/maintenance`, q) });
}
create(itemId: string, data: MaintenanceEntryCreate) {
@@ -75,13 +97,19 @@ export class MaintenanceAPI extends BaseAPI {
export class ItemsApi extends BaseAPI {
attachments: AttachmentsAPI;
maintenance: MaintenanceAPI;
+ fields: FieldsAPI;
constructor(http: Requests, token: string) {
super(http, token);
+ this.fields = new FieldsAPI(http);
this.attachments = new AttachmentsAPI(http);
this.maintenance = new MaintenanceAPI(http);
}
+ fullpath(id: string) {
+ return this.http.get({ url: route(`/items/${id}/path`) });
+ }
+
getAll(q: ItemsQuery = {}) {
return this.http.get>({ url: route("/items", q) });
}
@@ -119,6 +147,20 @@ export class ItemsApi extends BaseAPI {
return payload;
}
+ async patch(id: string, item: ItemPatch) {
+ const resp = await this.http.patch({
+ url: route(`/items/${id}`),
+ body: this.dropFields(item),
+ });
+
+ if (!resp.data) {
+ return resp;
+ }
+
+ resp.data = parseDate(resp.data, ["purchaseTime", "soldTime", "warrantyExpires"]);
+ return resp;
+ }
+
import(file: File | Blob) {
const formData = new FormData();
formData.append("csv", file);
@@ -128,4 +170,8 @@ export class ItemsApi extends BaseAPI {
data: formData,
});
}
+
+ exportURL() {
+ return route("/items/export");
+ }
}
diff --git a/frontend/lib/api/classes/labels.ts b/frontend/lib/api/classes/labels.ts
index 3a58bbe..6f7eaf0 100644
--- a/frontend/lib/api/classes/labels.ts
+++ b/frontend/lib/api/classes/labels.ts
@@ -1,10 +1,9 @@
import { BaseAPI, route } from "../base";
-import { LabelCreate, LabelOut } from "../types/data-contracts";
-import { Results } from "../types/non-generated";
+import type { LabelCreate, LabelOut } from "../types/data-contracts";
export class LabelsApi extends BaseAPI {
getAll() {
- return this.http.get>({ url: route("/labels") });
+ return this.http.get({ url: route("/labels") });
}
create(body: LabelCreate) {
diff --git a/frontend/lib/api/classes/locations.ts b/frontend/lib/api/classes/locations.ts
index 7559c62..0826611 100644
--- a/frontend/lib/api/classes/locations.ts
+++ b/frontend/lib/api/classes/locations.ts
@@ -1,14 +1,21 @@
import { BaseAPI, route } from "../base";
-import { LocationOutCount, LocationCreate, LocationOut, LocationUpdate } from "../types/data-contracts";
-import { Results } from "../types/non-generated";
+import type { LocationOutCount, LocationCreate, LocationOut, LocationUpdate, TreeItem } from "../types/data-contracts";
export type LocationsQuery = {
filterChildren: boolean;
};
+export type TreeQuery = {
+ withItems: boolean;
+};
+
export class LocationsApi extends BaseAPI {
getAll(q: LocationsQuery = { filterChildren: false }) {
- return this.http.get>({ url: route("/locations", q) });
+ return this.http.get({ url: route("/locations", q) });
+ }
+
+ getTree(tq = { withItems: false }) {
+ return this.http.get({ url: route("/locations/tree", tq) });
}
create(body: LocationCreate) {
diff --git a/frontend/lib/api/classes/notifiers.ts b/frontend/lib/api/classes/notifiers.ts
new file mode 100644
index 0000000..37044c0
--- /dev/null
+++ b/frontend/lib/api/classes/notifiers.ts
@@ -0,0 +1,28 @@
+import { BaseAPI, route } from "../base";
+import type { NotifierCreate, NotifierOut, NotifierUpdate } from "../types/data-contracts";
+
+export class NotifiersAPI extends BaseAPI {
+ getAll() {
+ return this.http.get({ url: route("/notifiers") });
+ }
+
+ create(body: NotifierCreate) {
+ return this.http.post({ url: route("/notifiers"), body });
+ }
+
+ update(id: string, body: NotifierUpdate) {
+ if (body.url === "") {
+ body.url = null;
+ }
+
+ return this.http.put({ url: route(`/notifiers/${id}`), body });
+ }
+
+ delete(id: string) {
+ return this.http.delete({ url: route(`/notifiers/${id}`) });
+ }
+
+ test(url: string) {
+ return this.http.post<{ url: string }, null>({ url: route(`/notifiers/test`), body: { url } });
+ }
+}
diff --git a/frontend/lib/api/classes/reports.ts b/frontend/lib/api/classes/reports.ts
new file mode 100644
index 0000000..eeae22b
--- /dev/null
+++ b/frontend/lib/api/classes/reports.ts
@@ -0,0 +1,7 @@
+import { BaseAPI, route } from "../base";
+
+export class ReportsAPI extends BaseAPI {
+ billOfMaterialsURL(): string {
+ return route("/reporting/bill-of-materials");
+ }
+}
diff --git a/frontend/lib/api/classes/stats.ts b/frontend/lib/api/classes/stats.ts
index b605270..f6e5cec 100644
--- a/frontend/lib/api/classes/stats.ts
+++ b/frontend/lib/api/classes/stats.ts
@@ -1,7 +1,7 @@
import { BaseAPI, route } from "../base";
-import { GroupStatistics, TotalsByOrganizer, ValueOverTime } from "../types/data-contracts";
+import type { GroupStatistics, TotalsByOrganizer, ValueOverTime } from "../types/data-contracts";
-function YYYY_DD_MM(date?: Date): string {
+function YYYY_MM_DD(date?: Date): string {
if (!date) {
return "";
}
@@ -14,7 +14,7 @@ function YYYY_DD_MM(date?: Date): string {
export class StatsAPI extends BaseAPI {
totalPriceOverTime(start?: Date, end?: Date) {
return this.http.get({
- url: route("/groups/statistics/purchase-price", { start: YYYY_DD_MM(start), end: YYYY_DD_MM(end) }),
+ url: route("/groups/statistics/purchase-price", { start: YYYY_MM_DD(start), end: YYYY_MM_DD(end) }),
});
}
diff --git a/frontend/lib/api/classes/users.ts b/frontend/lib/api/classes/users.ts
index 21006d0..292fa7e 100644
--- a/frontend/lib/api/classes/users.ts
+++ b/frontend/lib/api/classes/users.ts
@@ -1,6 +1,6 @@
import { BaseAPI, route } from "../base";
-import { ChangePassword, UserOut } from "../types/data-contracts";
-import { Result } from "../types/non-generated";
+import type { ChangePassword, UserOut } from "../types/data-contracts";
+import type { Result } from "../types/non-generated";
export class UserApi extends BaseAPI {
public self() {
diff --git a/frontend/lib/api/public.ts b/frontend/lib/api/public.ts
index 6fb309a..513e492 100644
--- a/frontend/lib/api/public.ts
+++ b/frontend/lib/api/public.ts
@@ -1,10 +1,5 @@
import { BaseAPI, route } from "./base";
-import { ApiSummary, TokenResponse, UserRegistration } from "./types/data-contracts";
-
-export type LoginPayload = {
- username: string;
- password: string;
-};
+import type { APISummary, LoginForm, TokenResponse, UserRegistration } from "./types/data-contracts";
export type StatusResult = {
health: boolean;
@@ -15,15 +10,16 @@ export type StatusResult = {
export class PublicApi extends BaseAPI {
public status() {
- return this.http.get({ url: route("/status") });
+ return this.http.get({ url: route("/status") });
}
- public login(username: string, password: string) {
- return this.http.post({
+ public login(username: string, password: string, stayLoggedIn = false) {
+ return this.http.post({
url: route("/users/login"),
body: {
username,
password,
+ stayLoggedIn,
},
});
}
diff --git a/frontend/lib/api/types/data-contracts.ts b/frontend/lib/api/types/data-contracts.ts
index 4846cef..384ffb6 100644
--- a/frontend/lib/api/types/data-contracts.ts
+++ b/frontend/lib/api/types/data-contracts.ts
@@ -10,6 +10,13 @@
* ---------------------------------------------------------------
*/
+export interface CurrenciesCurrency {
+ code: string;
+ local: string;
+ name: string;
+ symbol: string;
+}
+
export interface DocumentOut {
id: string;
path: string;
@@ -17,11 +24,11 @@ export interface DocumentOut {
}
export interface Group {
- createdAt: Date;
+ createdAt: Date | string;
currency: string;
id: string;
name: string;
- updatedAt: Date;
+ updatedAt: Date | string;
}
export interface GroupStatistics {
@@ -39,25 +46,32 @@ export interface GroupUpdate {
}
export interface ItemAttachment {
- createdAt: Date;
+ createdAt: Date | string;
document: DocumentOut;
id: string;
+ primary: boolean;
type: string;
- updatedAt: Date;
+ updatedAt: Date | string;
}
export interface ItemAttachmentUpdate {
+ primary: boolean;
title: string;
type: string;
}
export interface ItemCreate {
+ /** @maxLength 1000 */
description: string;
labelIds: string[];
/** Edges */
locationId: string;
+ /**
+ * @minLength 1
+ * @maxLength 255
+ */
name: string;
- parentId: string | null;
+ parentId?: string | null;
}
export interface ItemField {
@@ -66,7 +80,6 @@ export interface ItemField {
name: string;
numberValue: number;
textValue: string;
- timeValue: string;
type: string;
}
@@ -75,55 +88,72 @@ export interface ItemOut {
/** @example "0" */
assetId: string;
attachments: ItemAttachment[];
- children: ItemSummary[];
- createdAt: Date;
+ createdAt: Date | string;
description: string;
fields: ItemField[];
id: string;
+ imageId: string;
insured: boolean;
labels: LabelSummary[];
/** Warranty */
lifetimeWarranty: boolean;
/** Edges */
- location: LocationSummary | null;
+ location?: LocationSummary | null;
manufacturer: string;
modelNumber: string;
name: string;
/** Extras */
notes: string;
- parent: ItemSummary | null;
+ parent?: ItemSummary | null;
purchaseFrom: string;
/** @example "0" */
purchasePrice: string;
/** Purchase */
- purchaseTime: Date;
+ purchaseTime: Date | string;
quantity: number;
serialNumber: string;
soldNotes: string;
/** @example "0" */
soldPrice: string;
/** Sold */
- soldTime: Date;
+ soldTime: Date | string;
soldTo: string;
- updatedAt: Date;
+ updatedAt: Date | string;
warrantyDetails: string;
- warrantyExpires: Date;
+ warrantyExpires: Date | string;
+}
+
+export interface ItemPatch {
+ id: string;
+ quantity?: number | null;
+}
+
+export interface ItemPath {
+ id: string;
+ name: string;
+ type: ItemType;
}
export interface ItemSummary {
archived: boolean;
- createdAt: Date;
+ createdAt: Date | string;
description: string;
id: string;
+ imageId: string;
insured: boolean;
labels: LabelSummary[];
/** Edges */
- location: LocationSummary | null;
+ location?: LocationSummary | null;
name: string;
/** @example "0" */
purchasePrice: string;
quantity: number;
- updatedAt: Date;
+ updatedAt: Date | string;
+}
+
+export enum ItemType {
+ ItemTypeLocation = "location",
+ ItemTypeItem = "item",
}
export interface ItemUpdate {
@@ -143,12 +173,12 @@ export interface ItemUpdate {
name: string;
/** Extras */
notes: string;
- parentId: string | null;
+ parentId?: string | null;
purchaseFrom: string;
/** @example "0" */
purchasePrice: string;
/** Purchase */
- purchaseTime: Date;
+ purchaseTime: Date | string;
quantity: number;
/** Identifications */
serialNumber: string;
@@ -156,98 +186,105 @@ export interface ItemUpdate {
/** @example "0" */
soldPrice: string;
/** Sold */
- soldTime: Date;
+ soldTime: Date | string;
soldTo: string;
warrantyDetails: string;
- warrantyExpires: Date;
+ warrantyExpires: Date | string;
}
export interface LabelCreate {
color: string;
+ /** @maxLength 255 */
description: string;
+ /**
+ * @minLength 1
+ * @maxLength 255
+ */
name: string;
}
export interface LabelOut {
- createdAt: Date;
+ createdAt: Date | string;
description: string;
id: string;
- items: ItemSummary[];
name: string;
- updatedAt: Date;
+ updatedAt: Date | string;
}
export interface LabelSummary {
- createdAt: Date;
+ createdAt: Date | string;
description: string;
id: string;
name: string;
- updatedAt: Date;
+ updatedAt: Date | string;
}
export interface LocationCreate {
description: string;
name: string;
+ parentId?: string | null;
}
export interface LocationOut {
children: LocationSummary[];
- createdAt: Date;
+ createdAt: Date | string;
description: string;
id: string;
- items: ItemSummary[];
name: string;
parent: LocationSummary;
- updatedAt: Date;
+ updatedAt: Date | string;
}
export interface LocationOutCount {
- createdAt: Date;
+ createdAt: Date | string;
description: string;
id: string;
itemCount: number;
name: string;
- updatedAt: Date;
+ updatedAt: Date | string;
}
export interface LocationSummary {
- createdAt: Date;
+ createdAt: Date | string;
description: string;
id: string;
name: string;
- updatedAt: Date;
+ updatedAt: Date | string;
}
export interface LocationUpdate {
description: string;
id: string;
name: string;
- parentId: string | null;
+ parentId?: string | null;
}
export interface MaintenanceEntry {
+ completedDate: Date | string;
/** @example "0" */
cost: string;
- date: Date;
description: string;
id: string;
name: string;
+ scheduledDate: Date | string;
}
export interface MaintenanceEntryCreate {
+ completedDate: Date | string;
/** @example "0" */
cost: string;
- date: Date;
description: string;
name: string;
+ scheduledDate: Date | string;
}
export interface MaintenanceEntryUpdate {
+ completedDate: Date | string;
/** @example "0" */
cost: string;
- date: Date;
description: string;
name: string;
+ scheduledDate: Date | string;
}
export interface MaintenanceLog {
@@ -257,7 +294,37 @@ export interface MaintenanceLog {
itemId: string;
}
-export interface PaginationResultRepoItemSummary {
+export interface NotifierCreate {
+ isActive: boolean;
+ /**
+ * @minLength 1
+ * @maxLength 255
+ */
+ name: string;
+ url: string;
+}
+
+export interface NotifierOut {
+ createdAt: Date | string;
+ groupId: string;
+ id: string;
+ isActive: boolean;
+ name: string;
+ updatedAt: Date | string;
+ userId: string;
+}
+
+export interface NotifierUpdate {
+ isActive: boolean;
+ /**
+ * @minLength 1
+ * @maxLength 255
+ */
+ name: string;
+ url?: string | null;
+}
+
+export interface PaginationResultItemSummary {
items: ItemSummary[];
page: number;
pageSize: number;
@@ -270,6 +337,13 @@ export interface TotalsByOrganizer {
total: number;
}
+export interface TreeItem {
+ children: TreeItem[];
+ id: string;
+ name: string;
+ type: string;
+}
+
export interface UserOut {
email: string;
groupId: string;
@@ -294,27 +368,11 @@ export interface ValueOverTime {
}
export interface ValueOverTimeEntry {
- date: Date;
+ date: Date | string;
name: string;
value: number;
}
-export interface ServerErrorResponse {
- error: string;
- fields: Record;
-}
-
-export interface ServerResult {
- details: any;
- error: boolean;
- item: any;
- message: string;
-}
-
-export interface ServerResults {
- items: any;
-}
-
export interface UserRegistration {
email: string;
name: string;
@@ -322,7 +380,8 @@ export interface UserRegistration {
token: string;
}
-export interface ApiSummary {
+export interface APISummary {
+ allowRegistration: boolean;
build: Build;
demo: boolean;
health: boolean;
@@ -331,6 +390,10 @@ export interface ApiSummary {
versions: string[];
}
+export interface ActionAmountResult {
+ completed: number;
+}
+
export interface Build {
buildTime: string;
commit: string;
@@ -342,18 +405,18 @@ export interface ChangePassword {
new: string;
}
-export interface EnsureAssetIDResult {
- completed: number;
-}
-
export interface GroupInvitation {
- expiresAt: string;
+ expiresAt: Date | string;
token: string;
uses: number;
}
export interface GroupInvitationCreate {
- expiresAt: string;
+ expiresAt: Date | string;
+ /**
+ * @min 1
+ * @max 100
+ */
uses: number;
}
@@ -361,8 +424,23 @@ export interface ItemAttachmentToken {
token: string;
}
+export interface LoginForm {
+ password: string;
+ stayLoggedIn: boolean;
+ username: string;
+}
+
export interface TokenResponse {
attachmentToken: string;
- expiresAt: string;
+ expiresAt: Date | string;
token: string;
}
+
+export interface Wrapped {
+ item: any;
+}
+
+export interface ValidateErrorResponse {
+ error: string;
+ fields: string;
+}
diff --git a/frontend/lib/api/types/non-generated.ts b/frontend/lib/api/types/non-generated.ts
index acb7e01..bc920de 100644
--- a/frontend/lib/api/types/non-generated.ts
+++ b/frontend/lib/api/types/non-generated.ts
@@ -10,10 +10,6 @@ export type Result = {
item: T;
};
-export type Results = {
- items: T[];
-};
-
export interface PaginationResult {
items: T[];
page: number;
diff --git a/frontend/lib/api/user.ts b/frontend/lib/api/user.ts
index a0fd413..d477d48 100644
--- a/frontend/lib/api/user.ts
+++ b/frontend/lib/api/user.ts
@@ -7,7 +7,9 @@ import { UserApi } from "./classes/users";
import { ActionsAPI } from "./classes/actions";
import { StatsAPI } from "./classes/stats";
import { AssetsApi } from "./classes/assets";
-import { Requests } from "~~/lib/requests";
+import { ReportsAPI } from "./classes/reports";
+import { NotifiersAPI } from "./classes/notifiers";
+import type { Requests } from "~~/lib/requests";
export class UserClient extends BaseAPI {
locations: LocationsApi;
@@ -18,6 +20,8 @@ export class UserClient extends BaseAPI {
actions: ActionsAPI;
stats: StatsAPI;
assets: AssetsApi;
+ reports: ReportsAPI;
+ notifiers: NotifiersAPI;
constructor(requests: Requests, attachmentToken: string) {
super(requests, attachmentToken);
@@ -30,22 +34,9 @@ export class UserClient extends BaseAPI {
this.actions = new ActionsAPI(requests);
this.stats = new StatsAPI(requests);
this.assets = new AssetsApi(requests);
+ this.reports = new ReportsAPI(requests);
+ this.notifiers = new NotifiersAPI(requests);
Object.freeze(this);
}
-
- /** @deprecated use this.user.self() */
- public self() {
- return this.user.self();
- }
-
- /** @deprecated use this.user.logout() */
- public logout() {
- return this.user.logout();
- }
-
- /** @deprecated use this.user.delete() */
- public deleteAccount() {
- return this.user.delete();
- }
}
diff --git a/frontend/lib/data/currency.ts b/frontend/lib/data/currency.ts
deleted file mode 100644
index 54d2b2d..0000000
--- a/frontend/lib/data/currency.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-export type Codes = "USD" | "EUR" | "GBP" | "JPY" | "ZAR" | "AUD" | "NOK" | "SEK" | "DKK" | "INR" | "RMB";
-
-export type Currency = {
- code: Codes;
- local: string;
- symbol: string;
- name: string;
-};
-
-export const currencies: Currency[] = [
- {
- code: "AUD",
- local: "en-AU",
- symbol: "$",
- name: "Australian Dollar",
- },
- {
- code: "GBP",
- local: "en-GB",
- symbol: "£",
- name: "British Pound",
- },
- {
- code: "RMB",
- local: "zh-CN",
- symbol: "¥",
- name: "Chinese Yuan",
- },
- {
- code: "DKK",
- local: "da-DK",
- symbol: "kr",
- name: "Danish Krone",
- },
- {
- code: "EUR",
- local: "de-DE",
- symbol: "€",
- name: "Euro",
- },
- {
- code: "INR",
- local: "en-IN",
- symbol: "₹",
- name: "Indian Rupee",
- },
- {
- code: "JPY",
- local: "ja-JP",
- symbol: "¥",
- name: "Japanese Yen",
- },
- {
- code: "NOK",
- local: "nb-NO",
- symbol: "kr",
- name: "Norwegian Krone",
- },
- {
- code: "ZAR",
- local: "en-ZA",
- symbol: "R",
- name: "South African Rand",
- },
- {
- code: "SEK",
- local: "sv-SE",
- symbol: "kr",
- name: "Swedish Krona",
- },
- {
- code: "USD",
- local: "en-US",
- symbol: "$",
- name: "US Dollar",
- },
-];
diff --git a/frontend/lib/datelib/datelib.test.ts b/frontend/lib/datelib/datelib.test.ts
new file mode 100644
index 0000000..171d2cc
--- /dev/null
+++ b/frontend/lib/datelib/datelib.test.ts
@@ -0,0 +1,43 @@
+import { describe, test, expect } from "vitest";
+import { format, zeroTime, factorRange, parse } from "./datelib";
+
+describe("format", () => {
+ test("should format a date as a string", () => {
+ const date = new Date(2020, 1, 1);
+ expect(format(date)).toBe("2020-02-01");
+ });
+
+ test("should return the string if a string is passed in", () => {
+ expect(format("2020-02-01")).toBe("2020-02-01");
+ });
+});
+
+describe("zeroTime", () => {
+ test("should zero out the time", () => {
+ const date = new Date(2020, 1, 1, 12, 30, 30);
+ const zeroed = zeroTime(date);
+ expect(zeroed.getHours()).toBe(0);
+ expect(zeroed.getMinutes()).toBe(0);
+ expect(zeroed.getSeconds()).toBe(0);
+ });
+});
+
+describe("factorRange", () => {
+ test("should return a range of dates", () => {
+ const [start, end] = factorRange(10);
+ // Start should be today
+ expect(start).toBeInstanceOf(Date);
+ expect(start.getFullYear()).toBe(new Date().getFullYear());
+
+ // End should be 10 days from now
+ expect(end).toBeInstanceOf(Date);
+ expect(end.getFullYear()).toBe(new Date().getFullYear());
+ });
+});
+
+describe("parse", () => {
+ test("should parse a date string", () => {
+ const date = parse("2020-02-01");
+ expect(date).toBeInstanceOf(Date);
+ });
+});
diff --git a/frontend/lib/datelib/datelib.ts b/frontend/lib/datelib/datelib.ts
new file mode 100644
index 0000000..c70dbf9
--- /dev/null
+++ b/frontend/lib/datelib/datelib.ts
@@ -0,0 +1,34 @@
+import { addDays } from "date-fns";
+
+/*
+ * Formats a date as a string
+ * */
+export function format(date: Date | string): string {
+ if (typeof date === "string") {
+ return date;
+ }
+ return date.toISOString().split("T")[0];
+}
+
+export function zeroTime(date: Date): Date {
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate());
+}
+
+export function factorRange(offset: number = 7): [Date, Date] {
+ const date = zeroTime(new Date());
+
+ return [date, addDays(date, offset)];
+}
+
+export function factory(offset = 0): Date {
+ if (offset) {
+ return addDays(zeroTime(new Date()), offset);
+ }
+
+ return zeroTime(new Date());
+}
+
+export function parse(yyyyMMdd: string): Date {
+ const parts = yyyyMMdd.split("-");
+ return new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]));
+}
diff --git a/frontend/lib/requests/requests.ts b/frontend/lib/requests/requests.ts
index 9d62c36..8aecda1 100644
--- a/frontend/lib/requests/requests.ts
+++ b/frontend/lib/requests/requests.ts
@@ -3,10 +3,10 @@ export enum Method {
POST = "POST",
PUT = "PUT",
DELETE = "DELETE",
+ PATCH = "PATCH",
}
-export type RequestInterceptor = (r: Response) => void;
-export type ResponseInterceptor = (r: Response) => void;
+export type ResponseInterceptor = (r: Response, rq?: RequestInit) => void;
export interface TResponse {
status: number;
@@ -32,8 +32,8 @@ export class Requests {
this.responseInterceptors.push(interceptor);
}
- private callResponseInterceptors(response: Response) {
- this.responseInterceptors.forEach(i => i(response));
+ private callResponseInterceptors(response: Response, request?: RequestInit) {
+ this.responseInterceptors.forEach(i => i(response, request));
}
private url(rest: string): string {
@@ -58,12 +58,16 @@ export class Requests {
return this.do(Method.PUT, args);
}
+ public patch(args: RequestArgs): Promise> {
+ return this.do(Method.PATCH, args);
+ }
+
public delete(args: RequestArgs): Promise> {
return this.do(Method.DELETE, args);
}
private methodSupportsBody(method: Method): boolean {
- return method === Method.POST || method === Method.PUT;
+ return method === Method.POST || method === Method.PUT || method === Method.PATCH;
}
private async do(method: Method, rargs: RequestArgs): Promise> {
@@ -77,6 +81,7 @@ export class Requests {
const token = this.token();
if (token !== "" && payload.headers !== undefined) {
+ // @ts-expect-error - we know that the header is there
payload.headers["Authorization"] = token; // eslint-disable-line dot-notation
}
@@ -84,13 +89,14 @@ export class Requests {
if (rargs.data) {
payload.body = rargs.data;
} else {
+ // @ts-expect-error - we know that the header is there
payload.headers["Content-Type"] = "application/json";
payload.body = JSON.stringify(rargs.body);
}
}
const response = await fetch(this.url(rargs.url), payload);
- this.callResponseInterceptors(response);
+ this.callResponseInterceptors(response, payload);
const data: T = await (async () => {
if (response.status === 204) {
diff --git a/frontend/middleware/auth.ts b/frontend/middleware/auth.ts
index f67fa01..97a9920 100644
--- a/frontend/middleware/auth.ts
+++ b/frontend/middleware/auth.ts
@@ -1,15 +1,24 @@
-import { useAuthStore } from "~~/stores/auth";
-
export default defineNuxtRouteMiddleware(async () => {
- const auth = useAuthStore();
+ const ctx = useAuthContext();
const api = useUserApi();
- if (!auth.self) {
+ if (!ctx.isAuthorized()) {
+ if (window.location.pathname !== "/") {
+ console.debug("[middleware/auth] isAuthorized returned false, redirecting to /");
+ return navigateTo("/");
+ }
+ }
+
+ if (!ctx.user) {
+ console.log("Fetching user data");
const { data, error } = await api.user.self();
if (error) {
- navigateTo("/");
+ if (window.location.pathname !== "/") {
+ console.debug("[middleware/user] user is null and fetch failed, redirecting to /");
+ return navigateTo("/");
+ }
}
- auth.$patch({ self: data.item });
+ ctx.user = data.item;
}
});
diff --git a/frontend/nuxt.config.ts b/frontend/nuxt.config.ts
index 482fc05..dc45baa 100644
--- a/frontend/nuxt.config.ts
+++ b/frontend/nuxt.config.ts
@@ -3,14 +3,60 @@ import { defineNuxtConfig } from "nuxt/config";
// https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({
ssr: false,
- modules: ["@nuxtjs/tailwindcss", "@pinia/nuxt", "@vueuse/nuxt"],
+ modules: [
+ "@nuxtjs/tailwindcss",
+ "@pinia/nuxt",
+ "@vueuse/nuxt",
+ "@vite-pwa/nuxt",
+ "./nuxt.proxyoverride.ts",
+ "unplugin-icons/nuxt",
+ ],
nitro: {
devProxy: {
"/api": {
target: "http://localhost:7745/api",
+ ws: true,
changeOrigin: true,
},
},
},
css: ["@/assets/css/main.css"],
+ pwa: {
+ workbox: {
+ navigateFallbackDenylist: [/^\/api/],
+ },
+ injectRegister: "script",
+ injectManifest: {
+ swSrc: "sw.js",
+ },
+ devOptions: {
+ // Enable to troubleshoot during development
+ enabled: false,
+ },
+ manifest: {
+ name: "Homebox",
+ short_name: "Homebox",
+ description: "Home Inventory App",
+ theme_color: "#5b7f67",
+ start_url: "/home",
+ icons: [
+ {
+ src: "pwa-192x192.png",
+ sizes: "192x192",
+ type: "image/png",
+ },
+ {
+ src: "pwa-512x512.png",
+ sizes: "512x512",
+ type: "image/png",
+ },
+ {
+ src: "pwa-512x512.png",
+ sizes: "512x512",
+ type: "image/png",
+ purpose: "any maskable",
+ },
+ ],
+ },
+ },
});
diff --git a/frontend/nuxt.proxyoverride.ts b/frontend/nuxt.proxyoverride.ts
new file mode 100644
index 0000000..8650dd6
--- /dev/null
+++ b/frontend/nuxt.proxyoverride.ts
@@ -0,0 +1,52 @@
+// https://gist.github.com/ucw/67f7291c64777fb24341e8eae72bcd24
+import type { IncomingMessage } from "http";
+import type internal from "stream";
+import { defineNuxtModule, logger } from "@nuxt/kit";
+// Related To
+// - https://github.com/nuxt/nuxt/issues/15417
+// - https://github.com/nuxt/cli/issues/107
+//
+// fix from
+// - https://gist.github.com/ucw/67f7291c64777fb24341e8eae72bcd24
+// eslint-disable-next-line
+import { createProxyServer } from "http-proxy";
+
+export default defineNuxtModule({
+ defaults: {
+ target: "ws://localhost:7745",
+ path: "/api/v1/ws",
+ },
+ meta: {
+ configKey: "websocketProxy",
+ name: "Websocket proxy",
+ },
+ setup(resolvedOptions, nuxt) {
+ if (!nuxt.options.dev || !resolvedOptions.target) {
+ return;
+ }
+
+ nuxt.hook("listen", server => {
+ const proxy = createProxyServer({
+ ws: true,
+ secure: false,
+ changeOrigin: true,
+ target: resolvedOptions.target,
+ });
+
+ const proxyFn = (req: IncomingMessage, socket: internal.Duplex, head: Buffer) => {
+ if (req.url && req.url.startsWith(resolvedOptions.path)) {
+ proxy.ws(req, socket, head);
+ }
+ };
+
+ server.on("upgrade", proxyFn);
+
+ nuxt.hook("close", () => {
+ server.off("upgrade", proxyFn);
+ proxy.close();
+ });
+
+ logger.info(`Websocket dev proxy started on ${resolvedOptions.path}`);
+ });
+ },
+});
diff --git a/frontend/package.json b/frontend/package.json
index c8599e2..825f3e4 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -7,45 +7,57 @@
"postinstall": "nuxt prepare",
"lint": "eslint --ext \".ts,.js,.vue\" --ignore-path ../.gitignore .",
"lint:fix": "eslint --ext \".ts,.js,.vue\" --ignore-path ../.gitignore . --fix",
+ "lint:ci": "eslint --ext \".ts,.js,.vue\" --ignore-path ../.gitignore . --max-warnings 1",
+ "typecheck": "nuxi typecheck",
"test:ci": "TEST_SHUTDOWN_API_SERVER=true vitest --run --config ./test/vitest.config.ts",
"test:local": "TEST_SHUTDOWN_API_SERVER=false && vitest --run --config ./test/vitest.config.ts",
"test:watch": " TEST_SHUTDOWN_API_SERVER=false vitest --config ./test/vitest.config.ts"
},
"devDependencies": {
- "@faker-js/faker": "^7.5.0",
+ "@faker-js/faker": "^8.0.0",
+ "@iconify-json/mdi": "^1.1.64",
"@nuxtjs/eslint-config-typescript": "^12.0.0",
- "@typescript-eslint/eslint-plugin": "^5.36.2",
- "@typescript-eslint/parser": "^5.36.2",
+ "@types/dompurify": "^3.0.0",
+ "@types/markdown-it": "^13.0.0",
+ "@typescript-eslint/eslint-plugin": "^6.0.0",
+ "@typescript-eslint/parser": "^6.0.0",
+ "@vite-pwa/nuxt": "^0.5.0",
"eslint": "^8.23.0",
- "eslint-config-prettier": "^8.5.0",
- "eslint-plugin-prettier": "^4.2.1",
+ "eslint-config-prettier": "^9.0.0",
+ "eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-vue": "^9.4.0",
+ "h3": "^1.7.1",
"isomorphic-fetch": "^3.0.0",
- "nuxt": "3.0.0",
- "prettier": "^2.7.1",
- "typescript": "^4.8.3",
+ "nuxt": "3.6.5",
+ "prettier": "^3.2.5",
+ "typescript": "^5.0.0",
+ "unplugin-icons": "^0.18.5",
"vite-plugin-eslint": "^1.8.1",
- "vitest": "^0.27.0"
+ "vitest": "^1.0.0"
},
"dependencies": {
- "@iconify/vue": "^3.2.1",
+ "@headlessui/vue": "^1.7.9",
"@nuxtjs/tailwindcss": "^6.1.3",
- "@pinia/nuxt": "^0.4.1",
+ "@pinia/nuxt": "^0.5.0",
"@tailwindcss/aspect-ratio": "^0.4.0",
"@tailwindcss/forms": "^0.5.2",
"@tailwindcss/typography": "^0.5.4",
- "@vueuse/nuxt": "^9.1.1",
- "@vueuse/router": "^9.9.0",
+ "@types/lunr": "^2.3.7",
+ "@vuepic/vue-datepicker": "^8.1.1",
+ "@vueuse/nuxt": "^10.0.0",
+ "@vueuse/router": "^10.0.0",
"autoprefixer": "^10.4.8",
- "chart.js": "^4.0.1",
"daisyui": "^2.24.0",
- "dompurify": "^2.4.1",
- "markdown-it": "^13.0.1",
+ "date-fns": "^3.3.1",
+ "dompurify": "^3.0.0",
+ "h3": "^1.7.1",
+ "http-proxy": "^1.18.1",
+ "lunr": "^2.3.9",
+ "markdown-it": "^14.0.0",
"pinia": "^2.0.21",
"postcss": "^8.4.16",
"tailwindcss": "^3.1.8",
- "vue": "^3.2.38",
- "vue-chartjs": "^4.1.2",
+ "vue": "v3.4.8",
"vue-router": "4"
}
-}
\ No newline at end of file
+}
diff --git a/frontend/pages/home/charts.ts b/frontend/pages/home/charts.ts
deleted file mode 100644
index ff9c8c7..0000000
--- a/frontend/pages/home/charts.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import { TChartData } from "vue-chartjs/dist/types";
-import { UserClient } from "~~/lib/api/user";
-
-export function purchasePriceOverTimeChart(api: UserClient) {
- const { data: timeseries } = useAsyncData(async () => {
- const { data } = await api.stats.totalPriceOverTime();
- return data;
- });
-
- const primary = useCssVar("--p");
-
- return computed(() => {
- if (!timeseries.value) {
- return {
- labels: ["Purchase Price"],
- datasets: [
- {
- label: "Purchase Price",
- data: [],
- backgroundColor: primary.value,
- borderColor: primary.value,
- },
- ],
- } as TChartData<"line", number[], unknown>;
- }
-
- let start = timeseries.value?.valueAtStart;
-
- return {
- labels: timeseries?.value.entries.map(t => new Date(t.date).toDateString()) || [],
- datasets: [
- {
- label: "Purchase Price",
- data:
- timeseries.value?.entries.map(t => {
- start += t.value;
- return start;
- }) || [],
- backgroundColor: primary.value,
- borderColor: primary.value,
- },
- ],
- } as TChartData<"line", number[], unknown>;
- });
-}
-
-export function inventoryByLocationChart(api: UserClient) {
- const { data: donutSeries } = useAsyncData(async () => {
- const { data } = await api.stats.locations();
- return data;
- });
-
- const primary = useCssVar("--p");
- const secondary = useCssVar("--s");
- const neutral = useCssVar("--n");
-
- return computed(() => {
- return {
- labels: donutSeries.value?.map(l => l.name) || [],
- datasets: [
- {
- label: "Value",
- data: donutSeries.value?.map(l => l.total) || [],
- backgroundColor: [primary.value, secondary.value, neutral.value],
- borderColor: [primary.value, secondary.value, neutral.value],
- hoverOffset: 4,
- },
- ],
- };
- });
-}
diff --git a/frontend/pages/home/index.vue b/frontend/pages/home/index.vue
index 372c0a9..c114eee 100644
--- a/frontend/pages/home/index.vue
+++ b/frontend/pages/home/index.vue
@@ -22,55 +22,11 @@
const itemTable = itemsTable(api);
const stats = statCardData(api);
-
- // const purchasePriceOverTime = purchasePriceOverTimeChart(api);
-
- // const inventoryByLocation = inventoryByLocationChart(api);
-
- // const refDonutEl = ref();
-
- // const donutElWidth = computed(() => {
- // return refDonutEl.value?.clientWidth || 0;
- // });
-
-
Quick Statistics
@@ -82,20 +38,7 @@
Recently Added
-
-
-
-
-
-
-
-
-
-
- {{ item.location?.name }}
-
-
-
+
diff --git a/frontend/pages/home/statistics.ts b/frontend/pages/home/statistics.ts
index e1c7bf1..40ec1c2 100644
--- a/frontend/pages/home/statistics.ts
+++ b/frontend/pages/home/statistics.ts
@@ -1,4 +1,4 @@
-import { UserClient } from "~~/lib/api/user";
+import type { UserClient } from "~~/lib/api/user";
type StatCard = {
label: string;
diff --git a/frontend/pages/home/table.ts b/frontend/pages/home/table.ts
index e198bb4..eaff6da 100644
--- a/frontend/pages/home/table.ts
+++ b/frontend/pages/home/table.ts
@@ -1,41 +1,22 @@
-import { TableHeader } from "~~/components/global/Table.types";
-
-import { UserClient } from "~~/lib/api/user";
+import type { UserClient } from "~~/lib/api/user";
export function itemsTable(api: UserClient) {
- const { data: items } = useAsyncData(async () => {
+ const { data: items, refresh } = useAsyncData(async () => {
const { data } = await api.items.getAll({
page: 1,
pageSize: 5,
+ orderBy: "createdAt",
});
return data.items;
});
- const headers = [
- {
- text: "Name",
- sortable: true,
- value: "name",
- },
- {
- text: "Location",
- value: "location.name",
- },
- {
- text: "Warranty",
- value: "warranty",
- align: "center",
- },
- {
- text: "Price",
- value: "purchasePrice",
- align: "center",
- },
- ] as TableHeader[];
+ onServerEvent(ServerEvent.ItemMutation, () => {
+ console.log("item mutation");
+ refresh();
+ });
return computed(() => {
return {
- headers,
items: items.value || [],
};
});
diff --git a/frontend/pages/index.vue b/frontend/pages/index.vue
index 070fa05..2f761e2 100644
--- a/frontend/pages/index.vue
+++ b/frontend/pages/index.vue
@@ -1,13 +1,32 @@
-
+
x
-
Track, Organize, and Manage your Shit.
+
Track, Organize, and Manage your Things.
@@ -163,7 +171,7 @@
-
+
Register
@@ -174,7 +182,7 @@
Don't Want To Join a Group?
-
+
-
diff --git a/frontend/pages/item/[id]/index.vue b/frontend/pages/item/[id]/index.vue
index 72e6d14..c96f7ab 100644
--- a/frontend/pages/item/[id]/index.vue
+++ b/frontend/pages/item/[id]/index.vue
@@ -1,6 +1,12 @@
@@ -379,10 +445,10 @@
@@ -391,35 +457,45 @@
-
-
-
- {{ item ? item.name : "" }}
-
-
-
-
- -
- {{ item.parent.name }}
-
- - {{ item.name }}
-
-
-
-
- {{ item ? item.description : "" }}
-
-
-
-
- {{ item.location.name }}
-
-
-
-
+
+
+
+
+
+
+
+ {{ item ? item.name : "" }}
+
+
+
+ Created
+
+
+ -
+
+ Updated
+
+
+
+
-
-
+
+
+
+
+
+
+
-
+
Details
-
+
+
+ {{ detail.text }}
+
+
+
+
+
+
+
+
+
+
@@ -468,9 +554,9 @@
-
+
Attachments
-
+
+
-
+
Purchase Details
-
+
Warranty Details
-
+
Sold Details
@@ -520,11 +609,8 @@
-
- Child Items
-
-
-
+
diff --git a/frontend/pages/item/[id]/index/edit.vue b/frontend/pages/item/[id]/index/edit.vue
index e73cce0..93fda07 100644
--- a/frontend/pages/item/[id]/index/edit.vue
+++ b/frontend/pages/item/[id]/index/edit.vue
@@ -1,10 +1,13 @@
@@ -322,7 +418,6 @@
Attachment Edit
- {{ editState.type }}
+
+
+
+ Primary Photo
+ This options is only available for photos. Only one photo can be primary. If you select this option, the
+ current primary photo, if any will be unselected.
+
+
Update
-
-
-
-
- Edit
-
-
-
-
-
-
-
-
-
- Save
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ Save
+
+
+
+ Delete
+
+
+
-
+
Custom Fields
-
+
@@ -480,12 +579,12 @@
diff --git a/frontend/pages/item/[id]/index/log.vue b/frontend/pages/item/[id]/index/maintenance.vue
similarity index 65%
rename from frontend/pages/item/[id]/index/log.vue
rename to frontend/pages/item/[id]/index/maintenance.vue
index b10a503..9feb7bc 100644
--- a/frontend/pages/item/[id]/index/log.vue
+++ b/frontend/pages/item/[id]/index/maintenance.vue
@@ -1,7 +1,14 @@
@@ -144,16 +179,19 @@