From b66bc407fc4c2c8d6054136e15b05fe7e010c1b4 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Sat, 10 Dec 2022 16:55:23 -0900 Subject: [PATCH] wip: frontend redesign --- .vscode/settings.json | 2 +- backend/app/api/static/docs/docs.go | 4 + backend/app/api/static/docs/swagger.json | 4 + backend/app/api/static/docs/swagger.yaml | 3 + .../core/services/service_items_csv.go | 12 +- backend/internal/data/repo/repo_items.go | 27 +- frontend/.nuxtignore | 1 + frontend/app.vue | 2 +- frontend/assets/css/main.css | 3 + frontend/components/App/HeaderDecor.vue | 337 ++++++++++++++++++ frontend/components/Base/Card.vue | 2 +- frontend/components/Label/Chip.vue | 7 +- frontend/components/Location/Card.vue | 14 +- frontend/components/global/Currency.vue | 11 +- frontend/components/global/StatCard.vue | 27 ++ frontend/components/global/Subtitle.vue | 5 + frontend/components/global/Table.types.ts | 8 + frontend/components/global/Table.vue | 68 ++++ frontend/composables/use-confirm.ts | 7 + frontend/composables/use-css-var.ts | 106 +++++- frontend/layouts/default.vue | 87 ++++- frontend/lib/api/types/data-contracts.ts | 2 + frontend/lib/data/themes.ts | 5 + frontend/nuxt.config.ts | 5 +- frontend/pages/home.vue | 241 ------------- frontend/pages/home/charts.ts | 71 ++++ frontend/pages/home/index.vue | 198 ++++++++++ frontend/pages/home/statistics.ts | 39 ++ frontend/pages/home/table.ts | 42 +++ frontend/tailwind.config.js | 48 +++ 30 files changed, 1096 insertions(+), 292 deletions(-) create mode 100644 frontend/.nuxtignore create mode 100644 frontend/assets/css/main.css create mode 100644 frontend/components/App/HeaderDecor.vue create mode 100644 frontend/components/global/StatCard.vue create mode 100644 frontend/components/global/Subtitle.vue create mode 100644 frontend/components/global/Table.types.ts create mode 100644 frontend/components/global/Table.vue delete mode 100644 frontend/pages/home.vue create mode 100644 frontend/pages/home/charts.ts create mode 100644 frontend/pages/home/index.vue create mode 100644 frontend/pages/home/statistics.ts create mode 100644 frontend/pages/home/table.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index f05ebc8..3c39b0e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -20,5 +20,5 @@ "[typescript]": { "editor.defaultFormatter": "dbaeumer.vscode-eslint" }, - + "eslint.format.enable": true, } diff --git a/backend/app/api/static/docs/docs.go b/backend/app/api/static/docs/docs.go index 06cb95c..7863b0d 100644 --- a/backend/app/api/static/docs/docs.go +++ b/backend/app/api/static/docs/docs.go @@ -1665,6 +1665,10 @@ const docTemplate = `{ "name": { "type": "string" }, + "purchasePrice": { + "type": "string", + "example": "0" + }, "quantity": { "type": "integer" }, diff --git a/backend/app/api/static/docs/swagger.json b/backend/app/api/static/docs/swagger.json index 7e5ec85..cccc5f0 100644 --- a/backend/app/api/static/docs/swagger.json +++ b/backend/app/api/static/docs/swagger.json @@ -1657,6 +1657,10 @@ "name": { "type": "string" }, + "purchasePrice": { + "type": "string", + "example": "0" + }, "quantity": { "type": "integer" }, diff --git a/backend/app/api/static/docs/swagger.yaml b/backend/app/api/static/docs/swagger.yaml index 3d2fe2a..2efdf42 100644 --- a/backend/app/api/static/docs/swagger.yaml +++ b/backend/app/api/static/docs/swagger.yaml @@ -202,6 +202,9 @@ definitions: x-omitempty: true name: type: string + purchasePrice: + example: "0" + type: string quantity: type: integer updatedAt: diff --git a/backend/internal/core/services/service_items_csv.go b/backend/internal/core/services/service_items_csv.go index fb5e36a..147da67 100644 --- a/backend/internal/core/services/service_items_csv.go +++ b/backend/internal/core/services/service_items_csv.go @@ -97,18 +97,18 @@ func newCsvRow(row []string) csvRow { LabelStr: row[2], Item: repo.ItemOut{ ItemSummary: repo.ItemSummary{ - ImportRef: row[0], - Quantity: parseInt(row[3]), - Name: row[4], - Description: row[5], - Insured: parseBool(row[6]), + ImportRef: row[0], + Quantity: parseInt(row[3]), + Name: row[4], + Description: row[5], + Insured: parseBool(row[6]), + PurchasePrice: parseFloat(row[12]), }, SerialNumber: row[7], ModelNumber: row[8], Manufacturer: row[9], Notes: row[10], PurchaseFrom: row[11], - PurchasePrice: parseFloat(row[12]), PurchaseTime: parseDate(row[13]), LifetimeWarranty: parseBool(row[14]), WarrantyExpires: parseDate(row[15]), diff --git a/backend/internal/data/repo/repo_items.go b/backend/internal/data/repo/repo_items.go index 882f2d2..d8a3904 100644 --- a/backend/internal/data/repo/repo_items.go +++ b/backend/internal/data/repo/repo_items.go @@ -101,6 +101,8 @@ type ( CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` + PurchasePrice float64 `json:"purchasePrice,string"` + // Edges Location *LocationSummary `json:"location,omitempty" extensions:"x-nullable,x-omitempty"` Labels []LabelSummary `json:"labels"` @@ -121,9 +123,8 @@ type ( WarrantyDetails string `json:"warrantyDetails"` // Purchase - PurchaseTime time.Time `json:"purchaseTime"` - PurchaseFrom string `json:"purchaseFrom"` - PurchasePrice float64 `json:"purchasePrice,string"` + PurchaseTime time.Time `json:"purchaseTime"` + PurchaseFrom string `json:"purchaseFrom"` // Sold SoldTime time.Time `json:"soldTime"` @@ -157,13 +158,14 @@ func mapItemSummary(item *ent.Item) ItemSummary { } return ItemSummary{ - ID: item.ID, - Name: item.Name, - Description: item.Description, - Quantity: item.Quantity, - CreatedAt: item.CreatedAt, - UpdatedAt: item.UpdatedAt, - Archived: item.Archived, + ID: item.ID, + Name: item.Name, + Description: item.Description, + Quantity: item.Quantity, + CreatedAt: item.CreatedAt, + UpdatedAt: item.UpdatedAt, + Archived: item.Archived, + PurchasePrice: item.PurchasePrice, // Edges Location: location, @@ -230,9 +232,8 @@ func mapItemOut(item *ent.Item) ItemOut { Manufacturer: item.Manufacturer, // Purchase - PurchaseTime: item.PurchaseTime, - PurchaseFrom: item.PurchaseFrom, - PurchasePrice: item.PurchasePrice, + PurchaseTime: item.PurchaseTime, + PurchaseFrom: item.PurchaseFrom, // Sold SoldTime: item.SoldTime, diff --git a/frontend/.nuxtignore b/frontend/.nuxtignore new file mode 100644 index 0000000..5e5ef76 --- /dev/null +++ b/frontend/.nuxtignore @@ -0,0 +1 @@ +pages/**/*.ts \ No newline at end of file diff --git a/frontend/app.vue b/frontend/app.vue index ca390af..43c7263 100644 --- a/frontend/app.vue +++ b/frontend/app.vue @@ -1,6 +1,6 @@ diff --git a/frontend/assets/css/main.css b/frontend/assets/css/main.css new file mode 100644 index 0000000..a3c199c --- /dev/null +++ b/frontend/assets/css/main.css @@ -0,0 +1,3 @@ +.text-no-transform { + text-transform: none !important; +} \ No newline at end of file diff --git a/frontend/components/App/HeaderDecor.vue b/frontend/components/App/HeaderDecor.vue new file mode 100644 index 0000000..864bf50 --- /dev/null +++ b/frontend/components/App/HeaderDecor.vue @@ -0,0 +1,337 @@ + diff --git a/frontend/components/Base/Card.vue b/frontend/components/Base/Card.vue index 464066f..1c70cab 100644 --- a/frontend/components/Base/Card.vue +++ b/frontend/components/Base/Card.vue @@ -1,5 +1,5 @@ @@ -16,6 +68,33 @@ * Store Provider Initialization */ + const nav = [ + { + icon: "mdi-account", + id: 1, + name: "Profile", + to: "/profile", + }, + { + icon: "mdi-document", + id: 3, + name: "Items", + to: "/items", + }, + { + icon: "mdi-database", + id: 2, + name: "Import", + action: () => {}, + }, + { + icon: "mdi-database-export", + id: 5, + name: "Export", + action: () => {}, + }, + ]; + const labelStore = useLabelStore(); const reLabel = /\/api\/v1\/labels\/.*/gm; const rmLabelStoreObserver = defineObserver("labelStore", { diff --git a/frontend/lib/api/types/data-contracts.ts b/frontend/lib/api/types/data-contracts.ts index e313175..120a825 100644 --- a/frontend/lib/api/types/data-contracts.ts +++ b/frontend/lib/api/types/data-contracts.ts @@ -120,6 +120,8 @@ export interface ItemSummary { /** Edges */ location: LocationSummary | null; name: string; + /** @example "0" */ + purchasePrice: string; quantity: number; updatedAt: Date; } diff --git a/frontend/lib/data/themes.ts b/frontend/lib/data/themes.ts index 55f0f55..44f1c47 100644 --- a/frontend/lib/data/themes.ts +++ b/frontend/lib/data/themes.ts @@ -1,4 +1,5 @@ export type DaisyTheme = + | "homebox" | "light" | "dark" | "cupcake" @@ -35,6 +36,10 @@ export type ThemeOption = { }; export const themes: ThemeOption[] = [ + { + label: "Homebox", + value: "homebox", + }, { label: "Garden", value: "garden", diff --git a/frontend/nuxt.config.ts b/frontend/nuxt.config.ts index 6f4638b..cba18a2 100644 --- a/frontend/nuxt.config.ts +++ b/frontend/nuxt.config.ts @@ -9,8 +9,9 @@ export default defineNuxtConfig({ "/api": { target: "http://localhost:7745/api", changeOrigin: true, - } + }, }, }, - plugins: [], + css: ["@/assets/css/main.css"], + plugins: [], }); diff --git a/frontend/pages/home.vue b/frontend/pages/home.vue deleted file mode 100644 index ea93771..0000000 --- a/frontend/pages/home.vue +++ /dev/null @@ -1,241 +0,0 @@ - - - diff --git a/frontend/pages/home/charts.ts b/frontend/pages/home/charts.ts new file mode 100644 index 0000000..ff9c8c7 --- /dev/null +++ b/frontend/pages/home/charts.ts @@ -0,0 +1,71 @@ +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 new file mode 100644 index 0000000..2c8f657 --- /dev/null +++ b/frontend/pages/home/index.vue @@ -0,0 +1,198 @@ + + + diff --git a/frontend/pages/home/statistics.ts b/frontend/pages/home/statistics.ts new file mode 100644 index 0000000..e1c7bf1 --- /dev/null +++ b/frontend/pages/home/statistics.ts @@ -0,0 +1,39 @@ +import { UserClient } from "~~/lib/api/user"; + +type StatCard = { + label: string; + value: number; + type: "currency" | "number"; +}; + +export function statCardData(api: UserClient) { + const { data: statistics } = useAsyncData(async () => { + const { data } = await api.stats.group(); + return data; + }); + + return computed(() => { + return [ + { + label: "Total Value", + value: statistics.value?.totalItemPrice || 0, + type: "currency", + }, + { + label: "Total Items", + value: statistics.value?.totalItems || 0, + type: "number", + }, + { + label: "Total Locations", + value: statistics.value?.totalLocations || 0, + type: "number", + }, + { + label: "Total Labels", + value: statistics.value?.totalLabels || 0, + type: "number", + }, + ] as StatCard[]; + }); +} diff --git a/frontend/pages/home/table.ts b/frontend/pages/home/table.ts new file mode 100644 index 0000000..e198bb4 --- /dev/null +++ b/frontend/pages/home/table.ts @@ -0,0 +1,42 @@ +import { TableHeader } from "~~/components/global/Table.types"; + +import { UserClient } from "~~/lib/api/user"; + +export function itemsTable(api: UserClient) { + const { data: items } = useAsyncData(async () => { + const { data } = await api.items.getAll({ + page: 1, + pageSize: 5, + }); + 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[]; + + return computed(() => { + return { + headers, + items: items.value || [], + }; + }); +} diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index fc78c6c..a3680d2 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -4,8 +4,56 @@ module.exports = { theme: { extend: {}, }, + daisyui: { + themes: [ + { + homebox: { + primary: "#5C7F67", + secondary: "#ECF4E7", + accent: "#FFDA56", + neutral: "#2C2E27", + "base-100": "#F6FAFB", + info: "#3ABFF8", + success: "#36D399", + warning: "#FBBD23", + error: "#F87272", + }, + }, + "light", + "dark", + "cupcake", + "bumblebee", + "emerald", + "corporate", + "synthwave", + "retro", + "cyberpunk", + "valentine", + "halloween", + "garden", + "forest", + "aqua", + "lofi", + "pastel", + "fantasy", + "wireframe", + "black", + "luxury", + "dracula", + "cmyk", + "autumn", + "business", + "acid", + "lemonade", + "night", + "coffee", + "winter", + ], + }, variants: { extend: {}, }, plugins: [require("@tailwindcss/aspect-ratio"), require("@tailwindcss/typography"), require("daisyui")], }; + +const defaults = [];