-
-
-
diff --git a/frontend/components/Chart/Line.vue b/frontend/components/Chart/Line.vue
deleted file mode 100644
index c36ef93..0000000
--- a/frontend/components/Chart/Line.vue
+++ /dev/null
@@ -1,113 +0,0 @@
-
-
- {{ styles }}
-
-
-
-
-
-
-
diff --git a/frontend/components/Form/Autocomplete2.vue b/frontend/components/Form/Autocomplete2.vue
index b71c30e..d9fe64a 100644
--- a/frontend/components/Form/Autocomplete2.vue
+++ b/frontend/components/Form/Autocomplete2.vue
@@ -10,8 +10,16 @@
class="w-full input input-bordered"
@change="search = $event.target.value"
/>
+
+
+
-
+
-
+
@@ -53,6 +61,7 @@
diff --git a/frontend/components/Form/Password.vue b/frontend/components/Form/Password.vue
index 91b59c8..6303c0c 100644
--- a/frontend/components/Form/Password.vue
+++ b/frontend/components/Form/Password.vue
@@ -1,21 +1,23 @@
-
+
-
+
-
-
-
-
- {{ 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 @@
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
index 82b7954..bcba455 100644
--- a/frontend/components/Location/Selector.vue
+++ b/frontend/components/Location/Selector.vue
@@ -8,7 +8,7 @@
v-if="selected"
:class="['absolute inset-y-0 right-0 flex items-center pr-4', active ? 'text-white' : 'text-primary']"
>
-
+
@@ -20,8 +20,10 @@
-
-
diff --git a/frontend/components/global/DateTime.vue b/frontend/components/global/DateTime.vue
index 0388a67..b23cac2 100644
--- a/frontend/components/global/DateTime.vue
+++ b/frontend/components/global/DateTime.vue
@@ -3,7 +3,7 @@
diff --git a/frontend/components/global/DetailsSection/DetailsSection.vue b/frontend/components/global/DetailsSection/DetailsSection.vue
index 112970f..dd4faf8 100644
--- a/frontend/components/global/DetailsSection/DetailsSection.vue
+++ b/frontend/components/global/DetailsSection/DetailsSection.vue
@@ -16,7 +16,7 @@
@@ -33,7 +33,12 @@
v-if="detail.copyable"
class="opacity-0 group-hover:opacity-100 ml-4 my-0 duration-75 transition-opacity"
>
-
+
@@ -46,6 +51,7 @@
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 ff821f9..0cba850 100644
--- a/frontend/lib/api/__test__/factories/index.ts
+++ b/frontend/lib/api/__test__/factories/index.ts
@@ -2,7 +2,7 @@ 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";
@@ -15,7 +15,7 @@ function itemField(id = null): ItemField {
type: "text",
textValue: faker.lorem.sentence(),
booleanValue: false,
- numberValue: faker.datatype.number(),
+ numberValue: faker.number.int(),
timeValue: "",
};
}
@@ -28,7 +28,7 @@ function user(): UserRegistration {
return {
email: faker.internet.email(),
password: faker.internet.password(),
- name: faker.name.firstName(),
+ name: faker.person.firstName(),
token: "",
};
}
@@ -36,7 +36,7 @@ function user(): UserRegistration {
function location(parentId: string | null = null): LocationCreate {
return {
parentId,
- name: faker.address.city(),
+ name: faker.location.city(),
description: faker.lorem.sentence(),
};
}
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 304a54e..4f8973f 100644
--- a/frontend/lib/api/__test__/user/items.test.ts
+++ b/frontend/lib/api/__test__/user/items.test.ts
@@ -1,15 +1,15 @@
import { faker } from "@faker-js/faker";
import { describe, test, expect } from "vitest";
-import { ItemField, ItemUpdate, 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]> {
@@ -135,9 +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),
- completedDate: faker.date.past(1),
+ completedDate: faker.date.past(),
scheduledDate: "null",
- cost: faker.datatype.number(100).toString(),
+ cost: faker.number.int(100).toString(),
});
expect(response.status).toBe(201);
@@ -155,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 3012faf..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";
diff --git a/frontend/lib/api/__test__/user/notifier.test.ts b/frontend/lib/api/__test__/user/notifier.test.ts
index ed8e2ca..664b85e 100644
--- a/frontend/lib/api/__test__/user/notifier.test.ts
+++ b/frontend/lib/api/__test__/user/notifier.test.ts
@@ -8,8 +8,8 @@ describe("basic notifier workflows", () => {
// Create Notifier
const result = await client.notifiers.create({
- name: faker.name.firstName(),
- url: "discord://" + faker.random.alphaNumeric(10),
+ name: faker.word.words(2),
+ url: "discord://" + faker.string.alphanumeric(10),
isActive: true,
});
@@ -22,8 +22,8 @@ describe("basic notifier workflows", () => {
// Update Notifier with new URL
{
const updateData = {
- name: faker.name.firstName(),
- url: "discord://" + faker.random.alphaNumeric(10),
+ name: faker.word.words(2),
+ url: "discord://" + faker.string.alphanumeric(10),
isActive: true,
};
@@ -37,7 +37,7 @@ describe("basic notifier workflows", () => {
// Update Notifier with empty URL
{
const updateData = {
- name: faker.name.firstName(),
+ name: faker.word.words(2),
url: null,
isActive: true,
};
diff --git a/frontend/lib/api/__test__/user/stats.test.ts b/frontend/lib/api/__test__/user/stats.test.ts
index a13a03d..51f4e2e 100644
--- a/frontend/lib/api/__test__/user/stats.test.ts
+++ b/frontend/lib/api/__test__/user/stats.test.ts
@@ -1,6 +1,6 @@
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 = {
@@ -40,8 +40,8 @@ function importFileGenerator(entries: number): ImportObj[] {
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);
@@ -53,21 +53,21 @@ function importFileGenerator(entries: number): ImportObj[] {
[`HB.import_ref`]: faker.database.mongodbObjectId(),
[`HB.location`]: pick(locations),
[`HB.labels`]: labels,
- [`HB.quantity`]: Number(faker.random.numeric(2)),
- [`HB.name`]: faker.random.words(3),
+ [`HB.quantity`]: Number(faker.number.int(2)),
+ [`HB.name`]: faker.word.words(3),
[`HB.description`]: "",
[`HB.insured`]: faker.datatype.boolean(),
- [`HB.serial_number`]: faker.random.alphaNumeric(5),
- [`HB.model_number`]: faker.random.alphaNumeric(5),
- [`HB.manufacturer`]: faker.random.alphaNumeric(5),
+ [`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.name.fullName(),
- [`HB.purchase_price`]: faker.datatype.number(100),
+ [`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.name.fullName(),
- [`HB.sold_price`]: faker.datatype.number(100),
+ [`HB.sold_to`]: faker.person.fullName(),
+ [`HB.sold_price`]: faker.number.int(100),
[`HB.sold_time`]: formatDate(faker.date.past()),
[`HB.sold_notes`]: "",
});
diff --git a/frontend/lib/api/base/base-api.ts b/frontend/lib/api/base/base-api.ts
index 3847a4b..b9d01ae 100644
--- a/frontend/lib/api/base/base-api.ts
+++ b/frontend/lib/api/base/base-api.ts
@@ -1,4 +1,5 @@
-import { Requests } from "../../requests";
+import type { Requests } from "../../requests";
+import { route } from ".";
const ZERO_DATE = "0001-01-01T00:00:00Z";
@@ -70,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 f30e332..3975a1d 100644
--- a/frontend/lib/api/classes/actions.ts
+++ b/frontend/lib/api/classes/actions.ts
@@ -1,5 +1,5 @@
import { BaseAPI, route } from "../base";
-import { ActionAmountResult } from "../types/data-contracts";
+import type { ActionAmountResult } from "../types/data-contracts";
export class ActionsAPI extends BaseAPI {
ensureAssetIDs() {
@@ -19,4 +19,10 @@ export class ActionsAPI extends BaseAPI {
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 29403a2..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,24 +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({
@@ -100,6 +106,10 @@ export class ItemsApi extends BaseAPI {
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) });
}
@@ -137,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);
diff --git a/frontend/lib/api/classes/labels.ts b/frontend/lib/api/classes/labels.ts
index 61bcab7..6f7eaf0 100644
--- a/frontend/lib/api/classes/labels.ts
+++ b/frontend/lib/api/classes/labels.ts
@@ -1,5 +1,5 @@
import { BaseAPI, route } from "../base";
-import { LabelCreate, LabelOut } from "../types/data-contracts";
+import type { LabelCreate, LabelOut } from "../types/data-contracts";
export class LabelsApi extends BaseAPI {
getAll() {
diff --git a/frontend/lib/api/classes/locations.ts b/frontend/lib/api/classes/locations.ts
index acbbcdb..0826611 100644
--- a/frontend/lib/api/classes/locations.ts
+++ b/frontend/lib/api/classes/locations.ts
@@ -1,5 +1,5 @@
import { BaseAPI, route } from "../base";
-import { LocationOutCount, LocationCreate, LocationOut, LocationUpdate, TreeItem } from "../types/data-contracts";
+import type { LocationOutCount, LocationCreate, LocationOut, LocationUpdate, TreeItem } from "../types/data-contracts";
export type LocationsQuery = {
filterChildren: boolean;
diff --git a/frontend/lib/api/classes/notifiers.ts b/frontend/lib/api/classes/notifiers.ts
index c9e4a25..37044c0 100644
--- a/frontend/lib/api/classes/notifiers.ts
+++ b/frontend/lib/api/classes/notifiers.ts
@@ -1,5 +1,5 @@
import { BaseAPI, route } from "../base";
-import { NotifierCreate, NotifierOut, NotifierUpdate } from "../types/data-contracts";
+import type { NotifierCreate, NotifierOut, NotifierUpdate } from "../types/data-contracts";
export class NotifiersAPI extends BaseAPI {
getAll() {
diff --git a/frontend/lib/api/classes/stats.ts b/frontend/lib/api/classes/stats.ts
index 7959269..f6e5cec 100644
--- a/frontend/lib/api/classes/stats.ts
+++ b/frontend/lib/api/classes/stats.ts
@@ -1,5 +1,5 @@
import { BaseAPI, route } from "../base";
-import { GroupStatistics, TotalsByOrganizer, ValueOverTime } from "../types/data-contracts";
+import type { GroupStatistics, TotalsByOrganizer, ValueOverTime } from "../types/data-contracts";
function YYYY_MM_DD(date?: Date): string {
if (!date) {
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 ae8735a..513e492 100644
--- a/frontend/lib/api/public.ts
+++ b/frontend/lib/api/public.ts
@@ -1,5 +1,5 @@
import { BaseAPI, route } from "./base";
-import { ApiSummary, LoginForm, TokenResponse, UserRegistration } from "./types/data-contracts";
+import type { APISummary, LoginForm, TokenResponse, UserRegistration } from "./types/data-contracts";
export type StatusResult = {
health: boolean;
@@ -10,7 +10,7 @@ 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, stayLoggedIn = false) {
diff --git a/frontend/lib/api/types/data-contracts.ts b/frontend/lib/api/types/data-contracts.ts
index 98fad70..384ffb6 100644
--- a/frontend/lib/api/types/data-contracts.ts
+++ b/frontend/lib/api/types/data-contracts.ts
@@ -10,9 +10,11 @@
* ---------------------------------------------------------------
*/
-export interface MidErrorResponse {
- error: string;
- fields: Record;
+export interface CurrenciesCurrency {
+ code: string;
+ local: string;
+ name: string;
+ symbol: string;
}
export interface DocumentOut {
@@ -47,11 +49,13 @@ export interface ItemAttachment {
createdAt: Date | string;
document: DocumentOut;
id: string;
+ primary: boolean;
type: string;
updatedAt: Date | string;
}
export interface ItemAttachmentUpdate {
+ primary: boolean;
title: string;
type: string;
}
@@ -67,7 +71,7 @@ export interface ItemCreate {
* @maxLength 255
*/
name: string;
- parentId: string | null;
+ parentId?: string | null;
}
export interface ItemField {
@@ -84,23 +88,23 @@ export interface ItemOut {
/** @example "0" */
assetId: string;
attachments: ItemAttachment[];
- children: ItemSummary[];
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;
@@ -119,15 +123,27 @@ export interface ItemOut {
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 | string;
description: string;
id: string;
+ imageId: string;
insured: boolean;
labels: LabelSummary[];
/** Edges */
- location: LocationSummary | null;
+ location?: LocationSummary | null;
name: string;
/** @example "0" */
purchasePrice: string;
@@ -135,6 +151,11 @@ export interface ItemSummary {
updatedAt: Date | string;
}
+export enum ItemType {
+ ItemTypeLocation = "location",
+ ItemTypeItem = "item",
+}
+
export interface ItemUpdate {
archived: boolean;
assetId: string;
@@ -152,7 +173,7 @@ export interface ItemUpdate {
name: string;
/** Extras */
notes: string;
- parentId: string | null;
+ parentId?: string | null;
purchaseFrom: string;
/** @example "0" */
purchasePrice: string;
@@ -168,7 +189,6 @@ export interface ItemUpdate {
soldTime: Date | string;
soldTo: string;
warrantyDetails: string;
- /** Sold */
warrantyExpires: Date | string;
}
@@ -187,7 +207,6 @@ export interface LabelOut {
createdAt: Date | string;
description: string;
id: string;
- items: ItemSummary[];
name: string;
updatedAt: Date | string;
}
@@ -203,7 +222,7 @@ export interface LabelSummary {
export interface LocationCreate {
description: string;
name: string;
- parentId: string | null;
+ parentId?: string | null;
}
export interface LocationOut {
@@ -211,7 +230,6 @@ export interface LocationOut {
createdAt: Date | string;
description: string;
id: string;
- items: ItemSummary[];
name: string;
parent: LocationSummary;
updatedAt: Date | string;
@@ -238,40 +256,34 @@ export interface LocationUpdate {
description: string;
id: string;
name: string;
- parentId: string | null;
+ parentId?: string | null;
}
export interface MaintenanceEntry {
- /** Sold */
completedDate: Date | string;
/** @example "0" */
cost: string;
description: string;
id: string;
name: string;
- /** Sold */
scheduledDate: Date | string;
}
export interface MaintenanceEntryCreate {
- /** Sold */
completedDate: Date | string;
/** @example "0" */
cost: string;
description: string;
name: string;
- /** Sold */
scheduledDate: Date | string;
}
export interface MaintenanceEntryUpdate {
- /** Sold */
completedDate: Date | string;
/** @example "0" */
cost: string;
description: string;
name: string;
- /** Sold */
scheduledDate: Date | string;
}
@@ -309,7 +321,7 @@ export interface NotifierUpdate {
* @maxLength 255
*/
name: string;
- url: string | null;
+ url?: string | null;
}
export interface PaginationResultItemSummary {
@@ -368,11 +380,7 @@ export interface UserRegistration {
token: string;
}
-export interface ActionAmountResult {
- completed: number;
-}
-
-export interface ApiSummary {
+export interface APISummary {
allowRegistration: boolean;
build: Build;
demo: boolean;
@@ -382,6 +390,10 @@ export interface ApiSummary {
versions: string[];
}
+export interface ActionAmountResult {
+ completed: number;
+}
+
export interface Build {
buildTime: string;
commit: string;
@@ -427,3 +439,8 @@ export interface TokenResponse {
export interface Wrapped {
item: any;
}
+
+export interface ValidateErrorResponse {
+ error: string;
+ fields: string;
+}
diff --git a/frontend/lib/api/user.ts b/frontend/lib/api/user.ts
index 1107acd..d477d48 100644
--- a/frontend/lib/api/user.ts
+++ b/frontend/lib/api/user.ts
@@ -9,7 +9,7 @@ import { StatsAPI } from "./classes/stats";
import { AssetsApi } from "./classes/assets";
import { ReportsAPI } from "./classes/reports";
import { NotifiersAPI } from "./classes/notifiers";
-import { Requests } from "~~/lib/requests";
+import type { Requests } from "~~/lib/requests";
export class UserClient extends BaseAPI {
locations: LocationsApi;
diff --git a/frontend/lib/data/currency.ts b/frontend/lib/data/currency.ts
deleted file mode 100644
index 90b56fb..0000000
--- a/frontend/lib/data/currency.ts
+++ /dev/null
@@ -1,137 +0,0 @@
-export type Codes =
- | "AUD"
- | "BGN"
- | "CHF"
- | "CZK"
- | "DKK"
- | "EUR"
- | "GBP"
- | "INR"
- | "JPY"
- | "NOK"
- | "NZD"
- | "PLN"
- | "RMB"
- | "RON"
- | "SEK"
- | "TRY"
- | "USD"
- | "ZAR";
-
-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: "NZD",
- local: "en-NZ",
- symbol: "NZ$",
- name: "New Zealand Dollar",
- },
- {
- code: "PLN",
- local: "pl-PL",
- symbol: "zł",
- name: "Polish Zloty",
- },
- {
- code: "RON",
- local: "ro-RO",
- symbol: "lei",
- name: "Romanian Leu",
- },
- {
- code: "ZAR",
- local: "en-ZA",
- symbol: "R",
- name: "South African Rand",
- },
- {
- code: "SEK",
- local: "sv-SE",
- symbol: "kr",
- name: "Swedish Krona",
- },
- {
- code: "TRY",
- local: "tr-TR",
- symbol: "₺",
- name: "Turkish Lira",
- },
- {
- code: "USD",
- local: "en-US",
- symbol: "$",
- name: "US Dollar",
- },
- {
- code: "BGN",
- local: "bg-BG",
- symbol: "lv",
- name: "Bulgarian lev",
- },
- {
- code: "CHF",
- local: "de-CH",
- symbol: "chf",
- name: "Swiss Francs",
- },
- {
- code: "CZK",
- local: "cs-CZ",
- symbol: "Kč",
- name: "Czech Koruna",
- },
-];
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 32b79bc..8aecda1 100644
--- a/frontend/lib/requests/requests.ts
+++ b/frontend/lib/requests/requests.ts
@@ -3,6 +3,7 @@ export enum Method {
POST = "POST",
PUT = "PUT",
DELETE = "DELETE",
+ PATCH = "PATCH",
}
export type ResponseInterceptor = (r: Response, rq?: RequestInit) => void;
@@ -57,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> {
diff --git a/frontend/middleware/auth.ts b/frontend/middleware/auth.ts
index dd41635..97a9920 100644
--- a/frontend/middleware/auth.ts
+++ b/frontend/middleware/auth.ts
@@ -3,13 +3,20 @@ export default defineNuxtRouteMiddleware(async () => {
const api = useUserApi();
if (!ctx.isAuthorized()) {
- return navigateTo("/");
+ 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) {
- return navigateTo("/");
+ if (window.location.pathname !== "/") {
+ console.debug("[middleware/user] user is null and fetch failed, redirecting to /");
+ return navigateTo("/");
+ }
}
ctx.user = data.item;
diff --git a/frontend/nuxt.config.ts b/frontend/nuxt.config.ts
index 479d1f4..dc45baa 100644
--- a/frontend/nuxt.config.ts
+++ b/frontend/nuxt.config.ts
@@ -3,17 +3,28 @@ 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", "@vite-pwa/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",
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 2bda309..f1a05ed 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -14,44 +14,50 @@
"test:watch": " TEST_SHUTDOWN_API_SERVER=false vitest --config ./test/vitest.config.ts"
},
"devDependencies": {
- "@faker-js/faker": "^7.5.0",
- "@nuxtjs/eslint-config-typescript": "^12.0.0",
- "@types/dompurify": "^3.0.0",
- "@types/markdown-it": "^12.2.3",
- "@typescript-eslint/eslint-plugin": "^5.36.2",
- "@typescript-eslint/parser": "^5.36.2",
- "@vite-pwa/nuxt": "^0.0.7",
- "eslint": "^8.23.0",
- "eslint-config-prettier": "^8.5.0",
- "eslint-plugin-prettier": "^4.2.1",
- "eslint-plugin-vue": "^9.4.0",
+ "@faker-js/faker": "^8.4.1",
+ "@iconify-json/mdi": "^1.2.3",
+ "@nuxtjs/eslint-config-typescript": "^12.1.0",
+ "@types/dompurify": "^3.2.0",
+ "@types/markdown-it": "^13.0.9",
+ "@typescript-eslint/eslint-plugin": "^6.21.0",
+ "@typescript-eslint/parser": "^6.21.0",
+ "@vite-pwa/nuxt": "^0.5.0",
+ "eslint": "^8.57.1",
+ "eslint-config-prettier": "^9.1.0",
+ "eslint-plugin-prettier": "^5.2.6",
+ "eslint-plugin-vue": "^9.33.0",
+ "h3": "^1.7.1",
"isomorphic-fetch": "^3.0.0",
- "nuxt": "3.2.3",
- "prettier": "^2.7.1",
- "typescript": "^5.0.0",
+ "nuxt": "3.6.5",
+ "prettier": "^3.5.3",
+ "typescript": "^5.8.3",
+ "unplugin-icons": "^0.18.5",
"vite-plugin-eslint": "^1.8.1",
- "vitest": "^0.29.0"
+ "vitest": "^1.6.1"
},
"dependencies": {
- "@headlessui/vue": "^1.7.9",
- "@iconify/vue": "^3.2.1",
- "@nuxtjs/tailwindcss": "^6.1.3",
- "@pinia/nuxt": "^0.4.1",
- "@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",
- "autoprefixer": "^10.4.8",
- "chart.js": "^4.0.1",
- "daisyui": "^2.24.0",
- "dompurify": "^3.0.0",
- "markdown-it": "^13.0.1",
- "pinia": "^2.0.21",
- "postcss": "^8.4.16",
- "tailwindcss": "^3.1.8",
- "vue": "^3.2.45",
- "vue-chartjs": "^4.1.2",
- "vue-router": "4"
+ "@headlessui/vue": "^1.7.23",
+ "@nuxtjs/tailwindcss": "^6.13.2",
+ "@pinia/nuxt": "^0.5.5",
+ "@tailwindcss/aspect-ratio": "^0.4.2",
+ "@tailwindcss/forms": "^0.5.10",
+ "@tailwindcss/typography": "^0.5.16",
+ "@types/lunr": "^2.3.7",
+ "@vuepic/vue-datepicker": "^8.8.1",
+ "@vueuse/nuxt": "^10.11.1",
+ "@vueuse/router": "^10.11.1",
+ "autoprefixer": "^10.4.21",
+ "daisyui": "^2.52.0",
+ "date-fns": "^3.6.0",
+ "dompurify": "^3.2.5",
+ "h3": "^1.15.1",
+ "http-proxy": "^1.18.1",
+ "lunr": "^2.3.9",
+ "markdown-it": "^14.1.0",
+ "pinia": "^2.3.1",
+ "postcss": "^8.5.3",
+ "tailwindcss": "^3.4.17",
+ "vue": "3.4.8",
+ "vue-router": "^4.5.0"
}
-}
\ 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 5ce3f0d..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
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 127ecbb..eaff6da 100644
--- a/frontend/pages/home/table.ts
+++ b/frontend/pages/home/table.ts
@@ -1,14 +1,20 @@
-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;
});
+ onServerEvent(ServerEvent.ItemMutation, () => {
+ console.log("item mutation");
+ refresh();
+ });
+
return computed(() => {
return {
items: items.value || [],
diff --git a/frontend/pages/index.vue b/frontend/pages/index.vue
index 2258c4c..2f761e2 100644
--- a/frontend/pages/index.vue
+++ b/frontend/pages/index.vue
@@ -1,16 +1,31 @@
@@ -404,10 +445,10 @@
@@ -416,33 +457,45 @@
-
-
-
- {{ item ? item.name : "" }}
-
-
-
-
-
- {{ item.parent.name }}
-
- {{ item.name }}
-
-
-
-
-
-
-
- {{ item.location.name }}
-
-
-
-
+
+
+
+
+
+
+
+ {{ item ? item.name : "" }}
+
+
+
+ Created
+
+
+ -
+
+ Updated
+
+
+
+
-
-
+
+
+
+
+
+
+
@@ -475,7 +524,21 @@
-
+
+
+ {{ detail.text }}
+
+
+
+
+
+
+
+
+
+
@@ -546,8 +609,8 @@
-
diff --git a/frontend/pages/item/[id]/index/edit.vue b/frontend/pages/item/[id]/index/edit.vue
index f40ce91..93fda07 100644
--- a/frontend/pages/item/[id]/index/edit.vue
+++ b/frontend/pages/item/[id]/index/edit.vue
@@ -1,10 +1,13 @@
@@ -365,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
-
-
+
+
+
+
+
+ Advanced
+
+
+
+
+
+
+ Save
+
+
+
+ Delete
+
+
+
Edit Details
-
-
-
-
- Advanced
-
-
-
-
-
-
- Save
-
-
+
@@ -514,12 +579,12 @@
diff --git a/frontend/pages/item/[id]/index/maintenance.vue b/frontend/pages/item/[id]/index/maintenance.vue
index cf6055d..9feb7bc 100644
--- a/frontend/pages/item/[id]/index/maintenance.vue
+++ b/frontend/pages/item/[id]/index/maintenance.vue
@@ -1,7 +1,14 @@
@@ -163,7 +182,7 @@
{{ entry.id ? "Edit Entry" : "New Entry" }}
-