mirror of
https://github.com/hay-kot/homebox.git
synced 2025-08-05 09:10:26 +00:00
wip: frontend redesign
This commit is contained in:
parent
ca4ccc6053
commit
b66bc407fc
30 changed files with 1096 additions and 292 deletions
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
@ -20,5 +20,5 @@
|
|||
"[typescript]": {
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||
},
|
||||
|
||||
"eslint.format.enable": true,
|
||||
}
|
||||
|
|
|
@ -1665,6 +1665,10 @@ const docTemplate = `{
|
|||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"purchasePrice": {
|
||||
"type": "string",
|
||||
"example": "0"
|
||||
},
|
||||
"quantity": {
|
||||
"type": "integer"
|
||||
},
|
||||
|
|
|
@ -1657,6 +1657,10 @@
|
|||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"purchasePrice": {
|
||||
"type": "string",
|
||||
"example": "0"
|
||||
},
|
||||
"quantity": {
|
||||
"type": "integer"
|
||||
},
|
||||
|
|
|
@ -202,6 +202,9 @@ definitions:
|
|||
x-omitempty: true
|
||||
name:
|
||||
type: string
|
||||
purchasePrice:
|
||||
example: "0"
|
||||
type: string
|
||||
quantity:
|
||||
type: integer
|
||||
updatedAt:
|
||||
|
|
|
@ -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]),
|
||||
|
|
|
@ -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,
|
||||
|
|
1
frontend/.nuxtignore
Normal file
1
frontend/.nuxtignore
Normal file
|
@ -0,0 +1 @@
|
|||
pages/**/*.ts
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<NuxtLayout>
|
||||
<Html lang="en" :data-theme="theme" />
|
||||
<Html lang="en" :data-theme="theme || 'homebox'" />
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
|
|
3
frontend/assets/css/main.css
Normal file
3
frontend/assets/css/main.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
.text-no-transform {
|
||||
text-transform: none !important;
|
||||
}
|
337
frontend/components/App/HeaderDecor.vue
Normal file
337
frontend/components/App/HeaderDecor.vue
Normal file
|
@ -0,0 +1,337 @@
|
|||
<template>
|
||||
<svg viewBox="0 0 1440 237" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#a)" filter="url(#b)">
|
||||
<path fill="#fff" d="M0-103h1440v310H0z" />
|
||||
<path fill="#2C2E27" d="M0-103h1440v310H0z" />
|
||||
<g clip-path="url(#c)">
|
||||
<path
|
||||
d="M1344.93 230.569h-75.81v-45.486h30.32c5.98 0 11.89 1.177 17.41 3.463s10.53 5.636 14.76 9.86c8.53 8.53 13.32 20.099 13.32 32.163Z"
|
||||
fill="#507B5D"
|
||||
/>
|
||||
<path
|
||||
d="M1297.89 170.07c0-3.395-2.75-6.147-6.14-6.147-3.4 0-6.15 2.752-6.15 6.147 0 3.395 2.75 6.148 6.15 6.148 3.39 0 6.14-2.753 6.14-6.148ZM1328.45 170.07c0-3.395-2.75-6.147-6.15-6.147-3.39 0-6.15 2.752-6.15 6.147 0 3.395 2.76 6.148 6.15 6.148 3.4 0 6.15-2.753 6.15-6.148Z"
|
||||
fill="#F6FAFB"
|
||||
/>
|
||||
</g>
|
||||
<g clip-path="url(#d)">
|
||||
<path
|
||||
d="M1401.78 60c7.5 0 14.83 2.2231 21.06 6.388 6.24 4.165 11.09 10.0848 13.96 17.0109 2.87 6.9261 3.62 14.5474 2.16 21.9001-1.46 7.353-5.07 14.107-10.37 19.408-5.3 5.301-12.06 8.911-19.41 10.373-7.35 1.463-14.98.712-21.9-2.157-6.93-2.869-12.85-7.727-17.01-13.96-4.17-6.234-6.39-13.562-6.39-21.0587V60h37.9Z"
|
||||
fill="#F6FAFB"
|
||||
/>
|
||||
</g>
|
||||
<g clip-path="url(#e)">
|
||||
<path d="M1269.12 135.809h75.81V60h-75.81v75.809Z" fill="#FFDA56" />
|
||||
</g>
|
||||
<g clip-path="url(#f)">
|
||||
<path
|
||||
d="M1250.17 97.9043c0 7.4967-2.22 14.8247-6.39 21.0587-4.16 6.233-10.08 11.091-17.01 13.96-6.93 2.869-14.55 3.62-21.9 2.157-7.35-1.462-14.11-5.072-19.41-10.373-5.3-5.301-8.91-12.055-10.37-19.408-1.46-7.3527-.71-14.974 2.16-21.9001 2.86-6.9261 7.72-12.8459 13.96-17.0109 6.23-4.1649 13.56-6.388 21.05-6.388h37.91v37.9043Z"
|
||||
fill="#B4D9AE"
|
||||
/>
|
||||
</g>
|
||||
<g clip-path="url(#g)">
|
||||
<path d="M1079.6 135.809h75.81V60h-75.81v75.809Z" fill="#FFDA56" />
|
||||
</g>
|
||||
<g clip-path="url(#h)">
|
||||
<path
|
||||
d="M890.08 60h75.808v45.485h-30.323c-5.973 0-11.888-1.176-17.406-3.462-5.519-2.2861-10.533-5.6365-14.757-9.8602C894.872 83.6327 890.08 72.0634 890.08 60Z"
|
||||
fill="#507B5D"
|
||||
/>
|
||||
<path
|
||||
d="M937.114 120.498c0 3.395 2.752 6.148 6.147 6.148 3.395 0 6.147-2.753 6.147-6.148 0-3.395-2.752-6.147-6.147-6.147-3.395 0-6.147 2.752-6.147 6.147ZM906.56 120.498c0 3.395 2.752 6.148 6.147 6.148 3.395 0 6.148-2.753 6.148-6.148 0-3.395-2.753-6.147-6.148-6.147-3.395 0-6.147 2.752-6.147 6.147Z"
|
||||
fill="#F6FAFB"
|
||||
/>
|
||||
</g>
|
||||
<g clip-path="url(#i)">
|
||||
<path d="M795.32 154.76v75.809h75.808V154.76H795.32Z" fill="#FFDA56" />
|
||||
</g>
|
||||
<g clip-path="url(#j)">
|
||||
<path
|
||||
d="M776.368 154.76v75.809h-45.485v-30.324c0-5.973 1.177-11.888 3.463-17.406 2.286-5.519 5.636-10.533 9.86-14.757 8.53-8.53 20.099-13.322 32.162-13.322Z"
|
||||
fill="#507B5D"
|
||||
/>
|
||||
<path
|
||||
d="M715.87 201.794c-3.395 0-6.147 2.752-6.147 6.147 0 3.395 2.752 6.148 6.147 6.148 3.395 0 6.148-2.753 6.148-6.148 0-3.395-2.753-6.147-6.148-6.147ZM715.87 171.24c-3.395 0-6.147 2.752-6.147 6.147 0 3.395 2.752 6.148 6.147 6.148 3.395 0 6.148-2.753 6.148-6.148 0-3.395-2.753-6.147-6.148-6.147Z"
|
||||
fill="#F6FAFB"
|
||||
/>
|
||||
</g>
|
||||
<g clip-path="url(#k)">
|
||||
<path
|
||||
d="M871.128 97.9043c0 7.4967-2.223 14.8247-6.388 21.0587-4.165 6.233-10.084 11.091-17.01 13.96-6.927 2.869-14.548 3.62-21.901 2.157-7.352-1.462-14.106-5.072-19.407-10.373-5.301-5.301-8.911-12.055-10.374-19.408-1.462-7.3527-.712-14.974 2.157-21.9001 2.869-6.9261 7.727-12.8459 13.961-17.0109C818.399 62.2231 825.727 60 833.224 60h37.904v37.9043Z"
|
||||
fill="#F6FAFB"
|
||||
/>
|
||||
</g>
|
||||
<g clip-path="url(#l)">
|
||||
<path
|
||||
d="M679.334 184.193c-8.907 0-8.907 16.943-17.815 16.943-8.908 0-8.904-16.943-17.807-16.943-8.904 0-8.91 16.943-17.82 16.943s-8.908-16.943-17.818-16.943"
|
||||
stroke="#F6FAFB"
|
||||
stroke-width="4.54851"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<g clip-path="url(#m)">
|
||||
<path
|
||||
d="M605.8 97.9043c0-7.4968 2.223-14.8252 6.388-21.0585 4.165-6.2333 10.085-11.0916 17.011-13.9605 6.926-2.8689 14.547-3.6195 21.9-2.157 7.353 1.4626 14.107 5.0726 19.408 10.3736 5.301 5.301 8.911 12.0549 10.373 19.4076 1.463 7.3527.712 14.9735-2.157 21.9005-2.869 6.926-7.727 12.846-13.96 17.01-6.234 4.165-13.562 6.389-21.059 6.389H605.8V97.9043Z"
|
||||
fill="#F6FAFB"
|
||||
/>
|
||||
</g>
|
||||
<g clip-path="url(#n)">
|
||||
<path
|
||||
d="M511.04 135.809V60h45.485v30.3234c0 5.9732-1.176 11.8876-3.462 17.4066-2.286 5.518-5.637 10.533-9.86 14.756-8.53 8.53-20.1 13.323-32.163 13.323Z"
|
||||
fill="#507B5D"
|
||||
/>
|
||||
<path
|
||||
d="M571.538 88.7746c3.395 0 6.147-2.7522 6.147-6.1473 0-3.395-2.752-6.1473-6.147-6.1473-3.395 0-6.147 2.7523-6.147 6.1473 0 3.3951 2.752 6.1473 6.147 6.1473ZM571.538 119.328c3.395 0 6.147-2.752 6.147-6.147 0-3.395-2.752-6.147-6.147-6.147-3.395 0-6.147 2.752-6.147 6.147 0 3.395 2.752 6.147 6.147 6.147Z"
|
||||
fill="#F6FAFB"
|
||||
/>
|
||||
</g>
|
||||
<g clip-path="url(#o)">
|
||||
<path
|
||||
d="M489.814 184.193c-8.907 0-8.907 16.943-17.815 16.943-8.908 0-8.904-16.943-17.807-16.943-8.904 0-8.91 16.943-17.82 16.943s-8.908-16.943-17.818-16.943"
|
||||
stroke="#F6FAFB"
|
||||
stroke-width="4.54851"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<g clip-path="url(#p)">
|
||||
<path
|
||||
d="M454.184 135.809c-7.497 0-14.825-2.224-21.058-6.389-6.234-4.164-11.092-10.084-13.961-17.01-2.869-6.927-3.619-14.5478-2.157-21.9005 1.463-7.3527 5.073-14.1066 10.374-19.4076 5.301-5.301 12.055-8.911 19.407-10.3736 7.353-1.4625 14.974-.7119 21.9 2.157 6.927 2.8689 12.846 7.7272 17.011 13.9605 4.165 6.2333 6.388 13.5617 6.388 21.0585v37.9047h-37.904Z"
|
||||
fill="#B4D9AE"
|
||||
/>
|
||||
</g>
|
||||
<g clip-path="url(#q)">
|
||||
<path
|
||||
d="M132 230.569V154.76h45.485v30.323c0 5.974-1.176 11.888-3.462 17.407-2.286 5.518-5.636 10.533-9.86 14.756-8.53 8.53-20.1 13.323-32.163 13.323Z"
|
||||
fill="#507B5D"
|
||||
/>
|
||||
<path
|
||||
d="M192.498 183.535c3.395 0 6.148-2.753 6.148-6.148 0-3.395-2.753-6.147-6.148-6.147-3.395 0-6.147 2.752-6.147 6.147 0 3.395 2.752 6.148 6.147 6.148ZM192.498 214.089c3.395 0 6.148-2.753 6.148-6.148 0-3.395-2.753-6.147-6.148-6.147-3.395 0-6.147 2.752-6.147 6.147 0 3.395 2.752 6.148 6.147 6.148Z"
|
||||
fill="#F6FAFB"
|
||||
/>
|
||||
</g>
|
||||
<g clip-path="url(#r)">
|
||||
<path
|
||||
d="M302.569 135.809H226.76V90.3234h30.323c5.974 0 11.888 1.1765 17.407 3.4624 5.518 2.2858 10.533 5.6362 14.756 9.8602 8.53 8.53 13.323 20.099 13.323 32.163Z"
|
||||
fill="#507B5D"
|
||||
/>
|
||||
<path
|
||||
d="M255.535 75.3103c0-3.3951-2.753-6.1473-6.148-6.1473-3.395 0-6.147 2.7522-6.147 6.1473 0 3.3951 2.752 6.1473 6.147 6.1473 3.395 0 6.148-2.7522 6.148-6.1473ZM286.089 75.3103c0-3.3951-2.753-6.1473-6.148-6.1473-3.395 0-6.147 2.7522-6.147 6.1473 0 3.3951 2.752 6.1473 6.147 6.1473 3.395 0 6.148-2.7522 6.148-6.1473Z"
|
||||
fill="#F6FAFB"
|
||||
/>
|
||||
</g>
|
||||
<g clip-path="url(#s)">
|
||||
<path d="M207.809 60H132v75.809h75.809V60Z" fill="#FFDA56" />
|
||||
</g>
|
||||
<g clip-path="url(#t)">
|
||||
<path d="M502-31v75.8085h75.809V-31H502Z" fill="#FFDA56" />
|
||||
</g>
|
||||
<g clip-path="url(#u)">
|
||||
<path
|
||||
d="M65.9043-31.24c7.4967 0 14.8251 2.223 21.0584 6.388 6.2334 4.165 11.0916 10.0848 13.9603 17.01092 2.869 6.926094 3.62 14.54738 2.157 21.90008-1.462 7.3527-5.0724 14.1066-10.3734 19.4076-5.301 5.301-12.0549 8.911-19.4076 10.3736-7.3527 1.4625-14.974.7119-21.9001-2.157-6.9261-2.8689-12.8459-7.7272-17.0109-13.9605C30.2231 21.4894 28 14.161 28 6.66425V-31.24h37.9043Z"
|
||||
fill="#B4D9AE"
|
||||
/>
|
||||
</g>
|
||||
<g clip-path="url(#v)">
|
||||
<path
|
||||
d="M160.664 44.5685c-7.496 0-14.825-2.223-21.058-6.388-6.234-4.165-11.092-10.0848-13.961-17.0109-2.869-6.9261-3.619-14.54737-2.157-21.900078 1.463-7.352702 5.073-14.106622 10.374-19.407622s12.055-8.911 19.408-10.3736c7.352-1.4625 14.974-.7119 21.9 2.157 6.926 2.8689 12.846 7.7272 17.011 13.9605 4.164 6.23332 6.388 13.561722 6.388 21.05848V44.5685h-37.905Z"
|
||||
fill="#B4D9AE"
|
||||
/>
|
||||
</g>
|
||||
<g clip-path="url(#w)">
|
||||
<path
|
||||
d="M388.089 6.66425c0 7.49675-2.224 14.82515-6.388 21.05845-4.165 6.2333-10.085 11.0916-17.011 13.9605-6.926 2.8689-14.548 3.6195-21.9 2.157-7.353-1.4626-14.107-5.0726-19.408-10.3736-5.301-5.301-8.911-12.0549-10.374-19.4076-1.462-7.3527-.712-14.973986 2.157-21.90008 2.869-6.92612 7.728-12.84592 13.961-17.01092 6.233-4.165 13.562-6.388 21.058-6.388h37.905V6.66425Z"
|
||||
fill="#F6FAFB"
|
||||
/>
|
||||
</g>
|
||||
<g clip-path="url(#x)">
|
||||
<path
|
||||
d="M444.944-31.24c7.497 0 14.825 2.223 21.059 6.388 6.233 4.165 11.091 10.0848 13.96 17.01092 2.869 6.926094 3.62 14.54738 2.157 21.90008-1.462 7.3527-5.072 14.1066-10.373 19.4076-5.301 5.301-12.055 8.911-19.408 10.3736-7.353 1.4625-14.974.7119-21.9-2.157-6.926-2.8689-12.846-7.7272-17.011-13.9605-4.165-6.2333-6.388-13.5617-6.388-21.05845V-31.24h37.904Z"
|
||||
fill="#B4D9AE"
|
||||
/>
|
||||
</g>
|
||||
<g clip-path="url(#y)">
|
||||
<path
|
||||
d="M634.464 44.5685c-7.496 0-14.825-2.223-21.058-6.388-6.234-4.165-11.092-10.0848-13.961-17.0109-2.869-6.9261-3.619-14.54737-2.157-21.900078 1.463-7.352702 5.073-14.106622 10.374-19.407622s12.055-8.911 19.408-10.3736c7.352-1.4625 14.973-.7119 21.9 2.157 6.926 2.8689 12.846 7.7272 17.01 13.9605 4.165 6.23332 6.389 13.561722 6.389 21.05848V44.5685h-37.905Z"
|
||||
fill="#B4D9AE"
|
||||
/>
|
||||
</g>
|
||||
<g clip-path="url(#z)">
|
||||
<path
|
||||
d="M823.984-31.24c7.497 0 14.825 2.223 21.059 6.388 6.233 4.165 11.091 10.0848 13.96 17.01092 2.869 6.926094 3.62 14.54738 2.157 21.90008-1.462 7.3527-5.072 14.1066-10.373 19.4076-5.301 5.301-12.055 8.911-19.408 10.3736-7.353 1.4625-14.974.7119-21.9-2.157-6.926-2.8689-12.846-7.7272-17.011-13.9605-4.165-6.2333-6.388-13.5617-6.388-21.05845V-31.24h37.904Z"
|
||||
fill="#B4D9AE"
|
||||
/>
|
||||
</g>
|
||||
<g clip-path="url(#A)">
|
||||
<path
|
||||
d="M956.648-31.24v75.8085h-45.485V14.2451c0-5.97318 1.177-11.88788 3.463-17.40639 2.286-5.5185 5.636-10.53271 9.86-14.75641 8.53-8.5301 20.099-13.3223 32.162-13.3223Z"
|
||||
fill="#507B5D"
|
||||
/>
|
||||
<path
|
||||
d="M896.15 15.7939c-3.395 0-6.147 2.7522-6.147 6.1473 0 3.3951 2.752 6.1473 6.147 6.1473 3.395 0 6.148-2.7522 6.148-6.1473 0-3.3951-2.753-6.1473-6.148-6.1473Z"
|
||||
fill="#B4D9AE"
|
||||
/>
|
||||
</g>
|
||||
<g clip-path="url(#B)">
|
||||
<path
|
||||
d="M1013.5-31.24c7.5 0 14.83 2.223 21.06 6.388 6.24 4.165 11.09 10.0848 13.96 17.01092 2.87 6.926094 3.62 14.54738 2.16 21.90008-1.46 7.3527-5.07 14.1066-10.37 19.4076-5.3 5.301-12.06 8.911-19.41 10.3736-7.35 1.4625-14.97.7119-21.901-2.157-6.926-2.8689-12.846-7.7272-17.011-13.9605-4.165-6.2333-6.388-13.5617-6.388-21.05845V-31.24h37.9Z"
|
||||
fill="#B4D9AE"
|
||||
/>
|
||||
</g>
|
||||
<g clip-path="url(#C)">
|
||||
<path
|
||||
d="M1070.36 44.5685V-31.24h45.49V-.916588c0 5.973188-1.18 11.887888-3.47 17.406388-2.28 5.5185-5.63 10.5328-9.86 14.7564-8.53 8.5302-20.1 13.3223-32.16 13.3223Z"
|
||||
fill="#507B5D"
|
||||
/>
|
||||
<path
|
||||
d="M1130.86 28.0885c3.39 0 6.15-2.7522 6.15-6.1473 0-3.3951-2.76-6.1473-6.15-6.1473-3.4 0-6.15 2.7522-6.15 6.1473 0 3.3951 2.75 6.1473 6.15 6.1473Z"
|
||||
fill="#B4D9AE"
|
||||
/>
|
||||
</g>
|
||||
<g clip-path="url(#D)">
|
||||
<path
|
||||
d="M1203.02 44.5685c-7.49 0-14.82-2.223-21.05-6.388-6.24-4.165-11.1-10.0848-13.96-17.0109-2.87-6.9261-3.62-14.54737-2.16-21.900078 1.46-7.352702 5.07-14.106622 10.37-19.407622 5.3-5.301 12.06-8.911 19.41-10.3736 7.35-1.4625 14.97-.7119 21.9 2.157 6.93 2.8689 12.85 7.7272 17.01 13.9605 4.17 6.23332 6.39 13.561722 6.39 21.05848V44.5685h-37.91Z"
|
||||
fill="#F6FAFB"
|
||||
/>
|
||||
</g>
|
||||
<g clip-path="url(#E)">
|
||||
<path
|
||||
d="M1335.69-31.24v75.8085h-45.49V14.2451c0-5.97318 1.18-11.88788 3.47-17.40639 2.28-5.5185 5.63-10.53271 9.86-14.75641 8.53-8.5301 20.1-13.3223 32.16-13.3223Z"
|
||||
fill="#507B5D"
|
||||
/>
|
||||
<path
|
||||
d="M1275.19 15.7939c-3.39 0-6.15 2.7522-6.15 6.1473 0 3.3951 2.76 6.1473 6.15 6.1473 3.4 0 6.15-2.7522 6.15-6.1473 0-3.3951-2.75-6.1473-6.15-6.1473Z"
|
||||
fill="#B4D9AE"
|
||||
/>
|
||||
</g>
|
||||
<g clip-path="url(#F)">
|
||||
<path
|
||||
d="M292.808-31v75.8085h-45.485V14.4851c0-5.97316 1.177-11.88786 3.463-17.40636 2.286-5.51851 5.636-10.53274 9.86-14.75644C269.176-26.2078 280.745-31 292.808-31Z"
|
||||
fill="#507B5D"
|
||||
/>
|
||||
<path
|
||||
d="M232.31 16.0339c-3.395 0-6.147 2.7522-6.147 6.1473 0 3.3951 2.752 6.1473 6.147 6.1473 3.395 0 6.148-2.7522 6.148-6.1473 0-3.3951-2.753-6.1473-6.148-6.1473Z"
|
||||
fill="#B4D9AE"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M937.07 193v-40.557h9.802v16.278h15.982v-16.278h9.782V193h-9.782v-16.298h-15.982V193h-9.802Zm55.914.574c-3.195 0-5.941-.653-8.238-1.96-2.284-1.32-4.046-3.156-5.287-5.506-1.228-2.363-1.842-5.102-1.842-8.218 0-3.129.614-5.868 1.842-8.218 1.241-2.364 3.003-4.199 5.287-5.506 2.297-1.32 5.043-1.98 8.238-1.98 3.195 0 5.935.66 8.216 1.98 2.3 1.307 4.06 3.142 5.29 5.506 1.24 2.35 1.86 5.089 1.86 8.218 0 3.116-.62 5.855-1.86 8.218-1.23 2.35-2.99 4.186-5.29 5.506-2.281 1.307-5.021 1.96-8.216 1.96Zm.06-7.307c1.162 0 2.145-.357 2.95-1.07.806-.712 1.42-1.703 1.842-2.97.436-1.267.654-2.733.654-4.396 0-1.69-.218-3.169-.654-4.436-.422-1.268-1.036-2.258-1.842-2.971-.805-.713-1.788-1.069-2.95-1.069-1.202 0-2.218.356-3.05 1.069-.819.713-1.446 1.703-1.881 2.971-.423 1.267-.634 2.746-.634 4.436 0 1.663.211 3.129.634 4.396.435 1.267 1.062 2.258 1.881 2.97.832.713 1.848 1.07 3.05 1.07ZM1013.31 193v-30.418h9.21v5.585h.34c.63-1.849 1.7-3.308 3.21-4.377 1.5-1.069 3.3-1.604 5.38-1.604 2.11 0 3.92.541 5.43 1.624 1.5 1.082 2.46 2.535 2.87 4.357h.32c.56-1.809 1.67-3.255 3.32-4.337 1.65-1.096 3.6-1.644 5.85-1.644 2.87 0 5.21.924 7.01 2.772 1.79 1.836 2.69 4.357 2.69 7.565V193h-9.68v-18.259c0-1.518-.39-2.673-1.17-3.465-.78-.806-1.79-1.208-3.03-1.208-1.34 0-2.39.435-3.15 1.307-.75.858-1.13 2.013-1.13 3.465V193h-9.31v-18.358c0-1.412-.38-2.528-1.15-3.346-.76-.819-1.77-1.228-3.03-1.228-.84 0-1.59.204-2.23.614-.65.396-1.16.963-1.53 1.703-.36.739-.53 1.61-.53 2.614V193h-9.69Zm65.85.574c-3.18 0-5.93-.627-8.24-1.881-2.3-1.267-4.07-3.07-5.31-5.406-1.22-2.35-1.84-5.143-1.84-8.377 0-3.142.62-5.888 1.86-8.238 1.24-2.364 2.99-4.199 5.25-5.506 2.26-1.32 4.92-1.98 7.98-1.98 2.17 0 4.15.337 5.94 1.01 1.8.673 3.35 1.67 4.66 2.99 1.3 1.32 2.32 2.951 3.05 4.892.72 1.927 1.09 4.139 1.09 6.634v2.416h-26.44v-5.624h17.42c-.01-1.03-.25-1.948-.73-2.753-.47-.805-1.13-1.432-1.96-1.881-.82-.462-1.76-.693-2.83-.693-1.08 0-2.05.244-2.91.732-.86.476-1.54 1.129-2.04 1.961-.5.818-.77 1.749-.79 2.792v5.723c0 1.241.24 2.33.73 3.268.49.924 1.18 1.643 2.08 2.158.9.515 1.97.773 3.21.773.86 0 1.63-.119 2.33-.357.7-.237 1.3-.587 1.81-1.049.5-.462.87-1.03 1.12-1.703l8.9.257c-.37 1.994-1.19 3.73-2.44 5.208-1.24 1.466-2.87 2.608-4.89 3.426-2.02.806-4.36 1.208-7.01 1.208Zm19.38-.574v-40.557h16.91c3.04 0 5.58.429 7.63 1.287 2.06.858 3.6 2.059 4.63 3.604 1.04 1.545 1.57 3.334 1.57 5.367 0 1.545-.33 2.924-.97 4.139-.65 1.201-1.54 2.198-2.68 2.99-1.13.792-2.45 1.347-3.94 1.664v.396c1.64.079 3.15.521 4.54 1.326 1.4.806 2.52 1.928 3.36 3.367.85 1.426 1.27 3.116 1.27 5.07 0 2.178-.55 4.125-1.66 5.842-1.11 1.703-2.72 3.049-4.82 4.04-2.09.977-4.64 1.465-7.64 1.465h-18.2Zm9.8-7.902h6.06c2.13 0 3.69-.402 4.7-1.208 1.01-.805 1.52-1.927 1.52-3.366 0-1.043-.24-1.941-.73-2.693-.49-.766-1.18-1.354-2.08-1.763-.9-.422-1.97-.634-3.23-.634h-6.24v9.664Zm0-15.981h5.43c1.07 0 2.02-.178 2.85-.535.83-.356 1.48-.871 1.94-1.544.48-.674.72-1.485.72-2.436 0-1.36-.49-2.429-1.45-3.208-.96-.779-2.26-1.169-3.9-1.169h-5.59v8.892Zm71.41-6.535 5.02 10.021 5.18-10.021h9.77l-8.5 15.209 8.81 15.209h-9.68l-5.58-10.1-5.45 10.1h-9.82l8.83-15.209-8.4-15.209h9.82Z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="a">
|
||||
<path fill="#fff" d="M0-103h1440v310H0z" />
|
||||
</clipPath>
|
||||
<clipPath id="c">
|
||||
<path fill="#fff" d="M1269.12 230.569v-75.8085h75.8085v75.8085z" />
|
||||
</clipPath>
|
||||
<clipPath id="d">
|
||||
<path fill="#fff" d="M1439.69 135.809h-75.8085V60.0005h75.8085z" />
|
||||
</clipPath>
|
||||
<clipPath id="e">
|
||||
<path fill="#fff" d="M1344.93 135.809h-75.8085V60.0005h75.8085z" />
|
||||
</clipPath>
|
||||
<clipPath id="f">
|
||||
<path fill="#fff" d="M1174.36 135.809V60.0005h75.8085v75.8085z" />
|
||||
</clipPath>
|
||||
<clipPath id="g">
|
||||
<path fill="#fff" d="M1155.41 135.809h-75.8085V60.0005h75.8085z" />
|
||||
</clipPath>
|
||||
<clipPath id="h">
|
||||
<path fill="#fff" d="M965.888 60v75.8085h-75.8085V60z" />
|
||||
</clipPath>
|
||||
<clipPath id="i">
|
||||
<path fill="#fff" d="M795.32 230.569v-75.8085h75.8085v75.8085z" />
|
||||
</clipPath>
|
||||
<clipPath id="j">
|
||||
<path fill="#fff" d="M776.368 230.569h-75.8085v-75.8085h75.8085z" />
|
||||
</clipPath>
|
||||
<clipPath id="k">
|
||||
<path fill="#fff" d="M795.32 135.809V60.0005h75.8085v75.8085z" />
|
||||
</clipPath>
|
||||
<clipPath id="l">
|
||||
<path fill="#fff" d="M681.608 154.76v75.8085h-75.8085V154.76z" />
|
||||
</clipPath>
|
||||
<clipPath id="m">
|
||||
<path fill="#fff" d="M681.608 60v75.8085h-75.8085V60z" />
|
||||
</clipPath>
|
||||
<clipPath id="n">
|
||||
<path fill="#fff" d="M511.04 60h75.8085v75.8085H511.04z" />
|
||||
</clipPath>
|
||||
<clipPath id="o">
|
||||
<path fill="#fff" d="M492.088 154.76v75.8085h-75.8085V154.76z" />
|
||||
</clipPath>
|
||||
<clipPath id="p">
|
||||
<path fill="#fff" d="M416.28 60h75.8085v75.8085H416.28z" />
|
||||
</clipPath>
|
||||
<clipPath id="q">
|
||||
<path fill="#fff" d="M132 154.76h75.8085v75.8085H132z" />
|
||||
</clipPath>
|
||||
<clipPath id="r">
|
||||
<path fill="#fff" d="M226.76 135.809V60.0005h75.8085v75.8085z" />
|
||||
</clipPath>
|
||||
<clipPath id="s">
|
||||
<path fill="#fff" d="M132 60h75.8085v75.8085H132z" />
|
||||
</clipPath>
|
||||
<clipPath id="t">
|
||||
<path fill="#fff" d="M502 44.8085V-31h75.8085v75.8085z" />
|
||||
</clipPath>
|
||||
<clipPath id="u">
|
||||
<path fill="#fff" d="M103.809 44.5685H28.0005V-31.24h75.8085z" />
|
||||
</clipPath>
|
||||
<clipPath id="v">
|
||||
<path fill="#fff" d="M122.76-31.24h75.8085v75.8085H122.76z" />
|
||||
</clipPath>
|
||||
<clipPath id="w">
|
||||
<path fill="#fff" d="M312.28 44.5685V-31.24h75.8085v75.8085z" />
|
||||
</clipPath>
|
||||
<clipPath id="x">
|
||||
<path fill="#fff" d="M482.849 44.5685h-75.8085V-31.24h75.8085z" />
|
||||
</clipPath>
|
||||
<clipPath id="y">
|
||||
<path fill="#fff" d="M596.56-31.24h75.8085v75.8085H596.56z" />
|
||||
</clipPath>
|
||||
<clipPath id="z">
|
||||
<path fill="#fff" d="M861.889 44.5685h-75.8085V-31.24h75.8085z" />
|
||||
</clipPath>
|
||||
<clipPath id="A">
|
||||
<path fill="#fff" d="M956.648 44.5685h-75.8085V-31.24h75.8085z" />
|
||||
</clipPath>
|
||||
<clipPath id="B">
|
||||
<path fill="#fff" d="M1051.41 44.5685h-75.8085V-31.24h75.8085z" />
|
||||
</clipPath>
|
||||
<clipPath id="C">
|
||||
<path fill="#fff" d="M1070.36-31.24h75.8085v75.8085H1070.36z" />
|
||||
</clipPath>
|
||||
<clipPath id="D">
|
||||
<path fill="#fff" d="M1165.12-31.24h75.8085v75.8085H1165.12z" />
|
||||
</clipPath>
|
||||
<clipPath id="E">
|
||||
<path fill="#fff" d="M1335.69 44.5685h-75.8085V-31.24h75.8085z" />
|
||||
</clipPath>
|
||||
<clipPath id="F">
|
||||
<path fill="#fff" d="M292.808 44.8085h-75.8085V-31h75.8085z" />
|
||||
</clipPath>
|
||||
<filter
|
||||
id="b"
|
||||
x="-22"
|
||||
y="-117"
|
||||
width="1484"
|
||||
height="354"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feMorphology radius="4" in="SourceAlpha" result="effect1_dropShadow_5_1510" />
|
||||
<feOffset dy="8" />
|
||||
<feGaussianBlur stdDeviation="13" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix values="0 0 0 0 0.146667 0 0 0 0 0.183333 0 0 0 0 0.157752 0 0 0 0.25 0" />
|
||||
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_5_1510" />
|
||||
<feBlend in="SourceGraphic" in2="effect1_dropShadow_5_1510" result="shape" />
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
</template>
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="card bg-base-100 shadow-xl sm:rounded-lg">
|
||||
<div class="card bg-white shadow-xl sm:rounded-lg">
|
||||
<div v-if="$slots.title" class="px-4 py-5 sm:px-6">
|
||||
<h3 class="text-lg font-medium leading-6">
|
||||
<slot name="title"></slot>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { LabelOut, LabelSummary } from "~~/lib/api/types/data-contracts";
|
||||
|
||||
export type sizes = "sm" | "md" | "lg";
|
||||
export type sizes = "sm" | "md" | "lg" | "xl";
|
||||
defineProps({
|
||||
label: {
|
||||
type: Object as () => LabelOut | LabelSummary,
|
||||
|
@ -23,9 +23,10 @@
|
|||
<template>
|
||||
<NuxtLink
|
||||
ref="badge"
|
||||
class="badge"
|
||||
class="badge badge-secondary"
|
||||
:class="{
|
||||
'p-3': size !== 'sm',
|
||||
'badge-lg p-4': size === 'lg',
|
||||
'p-3': size !== 'sm' && size !== 'lg',
|
||||
'p-2 badge-sm': size === 'sm',
|
||||
}"
|
||||
:to="`/label/${label.id}`"
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<NuxtLink
|
||||
ref="card"
|
||||
:to="`/location/${location.id}`"
|
||||
class="card bg-primary text-primary-content transition duration-300"
|
||||
class="card bg-white text-neutral rounded-md transition duration-300 shadow-md"
|
||||
>
|
||||
<div
|
||||
class="card-body"
|
||||
|
@ -11,13 +11,15 @@
|
|||
'py-2 px-3': dense,
|
||||
}"
|
||||
>
|
||||
<h2 class="flex items-center gap-2">
|
||||
<h2 class="flex items-center justify-between gap-2">
|
||||
<label class="swap swap-rotate" :class="isActive ? 'swap-active' : ''">
|
||||
<Icon name="heroicons-arrow-right" class="swap-on" />
|
||||
<Icon name="heroicons-map-pin" class="swap-off" />
|
||||
<Icon name="heroicons-arrow-right" class="swap-on h-6 w-6" />
|
||||
<Icon name="heroicons-map-pin" class="swap-off h-6 w-6" />
|
||||
</label>
|
||||
{{ location.name }}
|
||||
<span v-if="hasCount" class="badge badge-secondary badge-lg ml-auto text-secondary-content"> {{ count }}</span>
|
||||
<span class="mx-auto">
|
||||
{{ location.name }}
|
||||
</span>
|
||||
<span v-if="hasCount" class="badge badge-primary h-6 w-6 badge-lg"> {{ count }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
|
|
|
@ -3,12 +3,11 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
amount: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
type Props = {
|
||||
amount: string | number;
|
||||
};
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const fmt = await useFormatCurrency();
|
||||
|
||||
|
|
27
frontend/components/global/StatCard.vue
Normal file
27
frontend/components/global/StatCard.vue
Normal file
|
@ -0,0 +1,27 @@
|
|||
<template>
|
||||
<div class="stats bg-neutral shadow rounded-md">
|
||||
<div class="stat text-neutral-content text-center space-y-1 p-3">
|
||||
<div class="stat-title">{{ title }}</div>
|
||||
<div class="stat-value text-2xl">
|
||||
<Currency v-if="type === 'currency'" :amount="value" />
|
||||
<template v-if="type === 'number'">{{ value }}</template>
|
||||
</div>
|
||||
<div v-if="subtitle" class="stat-desc">{{ subtitle }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
type format = "currency" | "number" | "percent";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
value: number;
|
||||
subtitle?: string;
|
||||
type?: format;
|
||||
};
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
type: "number",
|
||||
});
|
||||
</script>
|
5
frontend/components/global/Subtitle.vue
Normal file
5
frontend/components/global/Subtitle.vue
Normal file
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<h3 class="flex gap-2 items-center mb-3 pl-1 text-lg">
|
||||
<slot />
|
||||
</h3>
|
||||
</template>
|
8
frontend/components/global/Table.types.ts
Normal file
8
frontend/components/global/Table.types.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
export type TableHeader = {
|
||||
text: string;
|
||||
value: string;
|
||||
sortable?: boolean;
|
||||
align?: "left" | "center" | "right";
|
||||
};
|
||||
|
||||
export type TableData = Record<string, any>;
|
68
frontend/components/global/Table.vue
Normal file
68
frontend/components/global/Table.vue
Normal file
|
@ -0,0 +1,68 @@
|
|||
<template>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table w-full">
|
||||
<thead>
|
||||
<tr class="bg-primary">
|
||||
<th
|
||||
v-for="h in headers"
|
||||
:key="h.value"
|
||||
class="text-no-transform text-sm bg-neutral text-neutral-content"
|
||||
:class="{
|
||||
'text-center': h.align === 'center',
|
||||
'text-right': h.align === 'right',
|
||||
'text-left': h.align === 'left',
|
||||
}"
|
||||
>
|
||||
<template v-if="typeof h === 'string'">{{ h }}</template>
|
||||
<template v-else>{{ h.text }}</template>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- row 1 -->
|
||||
<tr v-for="(d, i) in data" :key="i">
|
||||
<td
|
||||
v-for="h in headers"
|
||||
:key="`${h.value}-${i}`"
|
||||
class="bg-white"
|
||||
:class="{
|
||||
'text-center': h.align === 'center',
|
||||
'text-right': h.align === 'right',
|
||||
'text-left': h.align === 'left',
|
||||
}"
|
||||
>
|
||||
<slot :name="cell(h)" v-bind="{ item: d }">
|
||||
{{ extractValue(d, h.value) }}
|
||||
</slot>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { TableData, TableHeader } from "./Table.types";
|
||||
|
||||
type Props = {
|
||||
headers: TableHeader[];
|
||||
data: TableData[];
|
||||
};
|
||||
|
||||
function extractValue(data: TableData, value: string) {
|
||||
const parts = value.split(".");
|
||||
let current = data;
|
||||
for (const part of parts) {
|
||||
current = current[part];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function cell(h: TableHeader) {
|
||||
return `cell-${h.value.replace(".", "_")}`;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -32,6 +32,13 @@ export function useConfirm(): Store {
|
|||
}
|
||||
|
||||
async function openDialog(msg: string): Promise<UseConfirmDialogRevealResult<boolean, boolean>> {
|
||||
if (!store.reveal) {
|
||||
throw new Error("reveal is not defined");
|
||||
}
|
||||
if (!store.text) {
|
||||
throw new Error("text is not defined");
|
||||
}
|
||||
|
||||
store.text.value = msg;
|
||||
return await store.reveal();
|
||||
}
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import { ComputedRef } from "vue";
|
||||
|
||||
type ColorType = "hsla";
|
||||
|
||||
export type VarOptions = {
|
||||
|
@ -8,15 +6,107 @@ export type VarOptions = {
|
|||
apply?: (value: string) => string;
|
||||
};
|
||||
|
||||
export function useCssVar(name: string, options?: VarOptions): ComputedRef<string> {
|
||||
export type Breakpoints = {
|
||||
sm: boolean;
|
||||
md: boolean;
|
||||
lg: boolean;
|
||||
xl: boolean;
|
||||
xxl: boolean;
|
||||
};
|
||||
|
||||
export function useBreakpoints(): Breakpoints {
|
||||
const breakpoints: Breakpoints = reactive({
|
||||
sm: false,
|
||||
md: false,
|
||||
lg: false,
|
||||
xl: false,
|
||||
xxl: false,
|
||||
});
|
||||
|
||||
const updateBreakpoints = () => {
|
||||
breakpoints.sm = window.innerWidth < 640;
|
||||
breakpoints.md = window.innerWidth >= 640;
|
||||
breakpoints.lg = window.innerWidth >= 768;
|
||||
breakpoints.xl = window.innerWidth >= 1024;
|
||||
breakpoints.xxl = window.innerWidth >= 1280;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
updateBreakpoints();
|
||||
window.addEventListener("resize", updateBreakpoints);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("resize", updateBreakpoints);
|
||||
});
|
||||
|
||||
return breakpoints;
|
||||
}
|
||||
|
||||
class ThemeObserver {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
private static instance?: ThemeObserver;
|
||||
private readonly observer: MutationObserver;
|
||||
|
||||
private fns: (() => void)[] = [];
|
||||
|
||||
private constructor() {
|
||||
this.observer = new MutationObserver(mutations => {
|
||||
mutations.forEach(mutation => {
|
||||
if (mutation.attributeName === "data-theme") {
|
||||
this.fire();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const html = document.querySelector("html");
|
||||
if (!html) {
|
||||
throw new Error("No html element found");
|
||||
}
|
||||
|
||||
this.observer.observe(html, { attributes: true });
|
||||
}
|
||||
|
||||
public static getInstance() {
|
||||
if (!ThemeObserver.instance) {
|
||||
ThemeObserver.instance = new ThemeObserver();
|
||||
}
|
||||
|
||||
return ThemeObserver.instance;
|
||||
}
|
||||
|
||||
private fire() {
|
||||
this.fns.forEach(fn => fn());
|
||||
}
|
||||
|
||||
public add(fn: () => void) {
|
||||
this.fns.push(fn);
|
||||
}
|
||||
|
||||
public remove(fn: () => void) {
|
||||
this.fns = this.fns.filter(f => f !== fn);
|
||||
}
|
||||
}
|
||||
|
||||
export function useCssVar(name: string, options?: VarOptions) {
|
||||
if (!options) {
|
||||
options = {
|
||||
type: "hsla",
|
||||
transparency: 1,
|
||||
apply: null,
|
||||
apply: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const cssVal = ref(getComputedStyle(document.documentElement).getPropertyValue(name).trim());
|
||||
const update = () => {
|
||||
cssVal.value = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
||||
};
|
||||
|
||||
ThemeObserver.getInstance().add(update);
|
||||
onUnmounted(() => {
|
||||
ThemeObserver.getInstance().remove(update);
|
||||
});
|
||||
|
||||
switch (options.type) {
|
||||
case "hsla": {
|
||||
return computed(() => {
|
||||
|
@ -24,13 +114,13 @@ export function useCssVar(name: string, options?: VarOptions): ComputedRef<strin
|
|||
return "";
|
||||
}
|
||||
|
||||
let val = getComputedStyle(document.documentElement).getPropertyValue(name);
|
||||
val = val.trim().split(" ").join(", ");
|
||||
|
||||
if (options.transparency) {
|
||||
let val = cssVal.value.trim().split(" ").join(", ");
|
||||
if (options?.transparency) {
|
||||
val += `, ${options.transparency}`;
|
||||
}
|
||||
|
||||
console.log(`hsla(${val})`);
|
||||
|
||||
return `hsla(${val})`;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,10 +1,62 @@
|
|||
<template>
|
||||
<div>
|
||||
<AppToast />
|
||||
<AppHeader />
|
||||
<main>
|
||||
<slot></slot>
|
||||
</main>
|
||||
<div class="drawer drawer-mobile">
|
||||
<input id="my-drawer-2" type="checkbox" class="drawer-toggle" />
|
||||
<div class="drawer-content justify-center">
|
||||
<AppHeaderDecor class="-mt-10" />
|
||||
<slot></slot>
|
||||
|
||||
<!-- Button -->
|
||||
<label for="my-drawer-2" class="btn btn-primary drawer-button lg:hidden">Open drawer</label>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="drawer-side shadow-lg w-60 flex flex-col justify-center py-10" style="background: white">
|
||||
<label for="my-drawer-2" class="drawer-overlay"></label>
|
||||
<!-- Top Section -->
|
||||
<div class="space-y-8">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<p>Kotelman House</p>
|
||||
<NuxtLink class="avatar placeholder" to="/home">
|
||||
<div class="bg-base-100 text-neutral-content rounded-full w-36">
|
||||
<span class="text-6xl text-base-content">HK</span>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="mx-auto w-40 mb-6">
|
||||
<BaseButton class="btn-block btn-primary text-xl">
|
||||
<template #icon>
|
||||
<Icon name="mdi-plus-circle" class="h-6 w-6" />
|
||||
</template>
|
||||
Create
|
||||
</BaseButton>
|
||||
</div>
|
||||
<ul class="flex flex-col mx-auto gap-y-8">
|
||||
<li v-for="n in nav" :key="n.id" class="text-xl">
|
||||
<NuxtLink v-if="n.to" :to="n.to">
|
||||
<span class="mr-4">
|
||||
<Icon :name="n.icon" class="h-5 w-5" />
|
||||
</span>
|
||||
{{ n.name }}
|
||||
</NuxtLink>
|
||||
<button v-else @click="n.action">
|
||||
<span class="mr-4">
|
||||
<Icon :name="n.icon" class="h-5 w-5" />
|
||||
</span>
|
||||
{{ n.name }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Bottom -->
|
||||
<button class="mt-auto mb-6">Sign Out</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -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", {
|
||||
|
|
|
@ -120,6 +120,8 @@ export interface ItemSummary {
|
|||
/** Edges */
|
||||
location: LocationSummary | null;
|
||||
name: string;
|
||||
/** @example "0" */
|
||||
purchasePrice: string;
|
||||
quantity: number;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -9,8 +9,9 @@ export default defineNuxtConfig({
|
|||
"/api": {
|
||||
target: "http://localhost:7745/api",
|
||||
changeOrigin: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
css: ["@/assets/css/main.css"],
|
||||
plugins: [],
|
||||
});
|
||||
|
|
|
@ -1,241 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import { useAuthStore } from "~~/stores/auth";
|
||||
import { useLabelStore } from "~~/stores/labels";
|
||||
import { useLocationStore } from "~~/stores/locations";
|
||||
|
||||
definePageMeta({
|
||||
middleware: ["auth"],
|
||||
});
|
||||
useHead({
|
||||
title: "Homebox | Home",
|
||||
});
|
||||
|
||||
const api = useUserApi();
|
||||
|
||||
const auth = useAuthStore();
|
||||
|
||||
const locationStore = useLocationStore();
|
||||
const locations = computed(() => locationStore.parentLocations);
|
||||
|
||||
const labelsStore = useLabelStore();
|
||||
const labels = computed(() => labelsStore.labels);
|
||||
|
||||
const { data: statistics } = useAsyncData(async () => {
|
||||
const { data } = await api.stats.group();
|
||||
return data;
|
||||
});
|
||||
|
||||
const stats = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: "Locations",
|
||||
value: statistics.value?.totalLocations || 0,
|
||||
},
|
||||
{
|
||||
label: "Items",
|
||||
value: statistics.value?.totalItems || 0,
|
||||
},
|
||||
{
|
||||
label: "Labels",
|
||||
value: statistics.value?.totalLabels || 0,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const importDialog = ref(false);
|
||||
const importCsv = ref(null);
|
||||
const importLoading = ref(false);
|
||||
const importRef = ref<HTMLInputElement>();
|
||||
whenever(
|
||||
() => !importDialog.value,
|
||||
() => {
|
||||
importCsv.value = null;
|
||||
}
|
||||
);
|
||||
|
||||
function setFile(e: Event & { target: HTMLInputElement }) {
|
||||
importCsv.value = e.target.files[0];
|
||||
}
|
||||
|
||||
const toast = useNotifier();
|
||||
|
||||
function openDialog() {
|
||||
importDialog.value = true;
|
||||
}
|
||||
|
||||
function uploadCsv() {
|
||||
importRef.value.click();
|
||||
}
|
||||
|
||||
const eventBus = useEventBus();
|
||||
|
||||
async function submitCsvFile() {
|
||||
importLoading.value = true;
|
||||
|
||||
const { error } = await api.items.import(importCsv.value);
|
||||
|
||||
if (error) {
|
||||
toast.error("Import failed. Please try again later.");
|
||||
}
|
||||
|
||||
// Reset
|
||||
importDialog.value = false;
|
||||
importLoading.value = false;
|
||||
importCsv.value = null;
|
||||
importRef.value.value = null;
|
||||
|
||||
eventBus.emit(EventTypes.ClearStores);
|
||||
}
|
||||
|
||||
const { data: timeseries } = useAsyncData(async () => {
|
||||
const { data } = await api.stats.totalPriceOverTime();
|
||||
return data;
|
||||
});
|
||||
|
||||
const primary = useCssVar("--p");
|
||||
const secondary = useCssVar("--s");
|
||||
const accent = useCssVar("--a");
|
||||
const neutral = useCssVar("--n");
|
||||
const base = useCssVar("--b");
|
||||
|
||||
const chartData = computed(() => {
|
||||
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,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const { data: donutSeries } = useAsyncData(async () => {
|
||||
const { data } = await api.stats.locations();
|
||||
return data;
|
||||
});
|
||||
|
||||
const donutData = 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,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const refDonutEl = ref<HTMLDivElement>(null);
|
||||
|
||||
const donutElWidth = computed(() => {
|
||||
return refDonutEl.value?.clientWidth || 0;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<BaseModal v-model="importDialog">
|
||||
<template #title> Import CSV File </template>
|
||||
<p>
|
||||
Import a CSV file containing your items, labels, and locations. See documentation for more information on the
|
||||
required format.
|
||||
</p>
|
||||
|
||||
<form @submit.prevent="submitCsvFile">
|
||||
<div class="flex flex-col gap-2 py-6">
|
||||
<input ref="importRef" type="file" class="hidden" accept=".csv,.tsv" @change="setFile" />
|
||||
|
||||
<BaseButton type="button" @click="uploadCsv">
|
||||
<Icon class="h-5 w-5 mr-2" name="mdi-upload" />
|
||||
Upload
|
||||
</BaseButton>
|
||||
<p class="text-center pt-4 -mb-5">
|
||||
{{ importCsv?.name }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="modal-action">
|
||||
<BaseButton type="submit" :disabled="!importCsv"> Submit </BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
<BaseContainer class="flex flex-col gap-16 pb-16">
|
||||
<section>
|
||||
<BaseCard>
|
||||
<template #title> Welcome Back, {{ auth.self ? auth.self.name : "Username" }} </template>
|
||||
<!-- <template #subtitle> {{ auth.self.isSuperuser ? "Admin" : "User" }} </template> -->
|
||||
<template #title-actions>
|
||||
<div class="flex justify-end gap-2">
|
||||
<div class="tooltip" data-tip="Import CSV File">
|
||||
<button class="btn btn-primary btn-sm" @click="openDialog">
|
||||
<Icon name="mdi-database" class="mr-2"></Icon>
|
||||
Import
|
||||
</button>
|
||||
</div>
|
||||
<BaseButton type="button" size="sm" to="/profile">
|
||||
<Icon class="h-5 w-5 mr-2" name="mdi-person" />
|
||||
Profile
|
||||
</BaseButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div
|
||||
class="grid grid-cols-1 divide-y divide-base-300 border-t border-base-300 sm:grid-cols-3 sm:divide-y-0 sm:divide-x"
|
||||
>
|
||||
<div v-for="stat in stats" :key="stat.label" class="px-6 py-5 text-center text-sm font-medium">
|
||||
<span class="text-base-900 font-bold">{{ stat.value }}</span>
|
||||
{{ " " }}
|
||||
<span class="text-base-600">{{ stat.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</BaseCard>
|
||||
</section>
|
||||
|
||||
<section v-if="timeseries" class="grid grid-cols-6 gap-6">
|
||||
<BaseCard class="col-span-4">
|
||||
<template #title>Total Asset Value {{ fmtCurrency(timeseries.valueAtEnd) }}</template>
|
||||
<div class="p-6 pt-0">
|
||||
<ClientOnly>
|
||||
<ChartLine chart-id="asd" :height="200" :chart-data="chartData" />
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</BaseCard>
|
||||
<BaseCard class="col-span-2">
|
||||
<template #title> Asset By Location {{ fmtCurrency(timeseries.valueAtEnd) }}</template>
|
||||
<div ref="refDonutEl" class="grid place-content-center h-full">
|
||||
<ClientOnly>
|
||||
<ChartDonut chart-id="donut" :width="donutElWidth - 50" :height="300" :chart-data="donutData" />
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</BaseCard>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<BaseSectionHeader class="mb-5"> Storage Locations </BaseSectionHeader>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 card md:grid-cols-3 gap-4">
|
||||
<LocationCard v-for="location in locations" :key="location.id" :location="location" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<BaseSectionHeader class="mb-5"> Labels </BaseSectionHeader>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<LabelChip v-for="label in labels" :key="label.id" size="lg" :label="label" />
|
||||
</div>
|
||||
</section>
|
||||
</BaseContainer>
|
||||
</div>
|
||||
</template>
|
71
frontend/pages/home/charts.ts
Normal file
71
frontend/pages/home/charts.ts
Normal file
|
@ -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,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
}
|
198
frontend/pages/home/index.vue
Normal file
198
frontend/pages/home/index.vue
Normal file
|
@ -0,0 +1,198 @@
|
|||
<script setup lang="ts">
|
||||
import { statCardData } from "./statistics";
|
||||
import { itemsTable } from "./table";
|
||||
import { inventoryByLocationChart, purchasePriceOverTimeChart } from "./charts";
|
||||
import { useLabelStore } from "~~/stores/labels";
|
||||
import { useLocationStore } from "~~/stores/locations";
|
||||
|
||||
definePageMeta({
|
||||
middleware: ["auth"],
|
||||
});
|
||||
useHead({
|
||||
title: "Homebox | Home",
|
||||
});
|
||||
|
||||
const api = useUserApi();
|
||||
const breakpoints = useBreakpoints();
|
||||
|
||||
const locationStore = useLocationStore();
|
||||
const locations = computed(() => locationStore.parentLocations);
|
||||
|
||||
const labelsStore = useLabelStore();
|
||||
const labels = computed(() => labelsStore.labels);
|
||||
|
||||
const itemTable = itemsTable(api);
|
||||
const stats = statCardData(api);
|
||||
|
||||
// const importDialog = ref(false);
|
||||
// const importCsv = ref(null);
|
||||
// const importLoading = ref(false);
|
||||
// const importRef = ref<HTMLInputElement>();
|
||||
// whenever(
|
||||
// () => !importDialog.value,
|
||||
// () => {
|
||||
// importCsv.value = null;
|
||||
// }
|
||||
// );
|
||||
|
||||
// function setFile(e: Event & { target: HTMLInputElement }) {
|
||||
// importCsv.value = e.target.files[0];
|
||||
// }
|
||||
|
||||
// function openDialog() {
|
||||
// importDialog.value = true;
|
||||
// }
|
||||
|
||||
// function uploadCsv() {
|
||||
// importRef.value.click();
|
||||
// }
|
||||
|
||||
// const eventBus = useEventBus();
|
||||
|
||||
// async function submitCsvFile() {
|
||||
// if (!importCsv.value) {
|
||||
// toast.error("Please select a file to import.");
|
||||
// return;
|
||||
// }
|
||||
|
||||
// importLoading.value = true;
|
||||
|
||||
// const { error } = await api.items.import(importCsv.value);
|
||||
|
||||
// if (error) {
|
||||
// toast.error("Import failed. Please try again later.");
|
||||
// }
|
||||
|
||||
// // Reset
|
||||
// importDialog.value = false;
|
||||
// importLoading.value = false;
|
||||
// importCsv.value = null;
|
||||
|
||||
// if (importRef.value) {
|
||||
// importRef.value.value = "";
|
||||
// }
|
||||
|
||||
// eventBus.emit(EventTypes.ClearStores);
|
||||
// }
|
||||
|
||||
const purchasePriceOverTime = purchasePriceOverTimeChart(api);
|
||||
|
||||
const inventoryByLocation = inventoryByLocationChart(api);
|
||||
|
||||
const refDonutEl = ref<HTMLDivElement>();
|
||||
|
||||
const donutElWidth = computed(() => {
|
||||
return refDonutEl.value?.clientWidth || 0;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- <BaseModal v-model="importDialog">
|
||||
<template #title> Import CSV File </template>
|
||||
<p>
|
||||
Import a CSV file containing your items, labels, and locations. See documentation for more information on the
|
||||
required format.
|
||||
</p>
|
||||
|
||||
<form @submit.prevent="submitCsvFile">
|
||||
<div class="flex flex-col gap-2 py-6">
|
||||
<input ref="importRef" type="file" class="hidden" accept=".csv,.tsv" @change="setFile" />
|
||||
|
||||
<BaseButton type="button" @click="uploadCsv">
|
||||
<Icon class="h-5 w-5 mr-2" name="mdi-upload" />
|
||||
Upload
|
||||
</BaseButton>
|
||||
<p class="text-center pt-4 -mb-5">
|
||||
{{ importCsv?.name }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="modal-action">
|
||||
<BaseButton type="submit" :disabled="!importCsv"> Submit </BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal> -->
|
||||
|
||||
<BaseContainer class="flex flex-col gap-12 pb-16">
|
||||
<section v-if="breakpoints.lg" class="grid grid-cols-6 gap-6">
|
||||
<article class="col-span-4">
|
||||
<Subtitle> Inventory Value Over Time </Subtitle>
|
||||
<BaseCard>
|
||||
<div class="p-6 pt-0">
|
||||
<ClientOnly>
|
||||
<ChartLine chart-id="asd" :height="140" :chart-data="purchasePriceOverTime" />
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</BaseCard>
|
||||
</article>
|
||||
<article class="col-span-2 max-h-[100px]">
|
||||
<Subtitle>
|
||||
Inventory By
|
||||
<span class="btn-group">
|
||||
<button class="btn btn-xs btn-active text-no-transform">Locations</button>
|
||||
<button class="btn btn-xs text-no-transform">Labels</button>
|
||||
</span>
|
||||
</Subtitle>
|
||||
<BaseCard>
|
||||
<div ref="refDonutEl" class="grid place-content-center h-full">
|
||||
<ClientOnly>
|
||||
<ChartDonut
|
||||
chart-id="donut"
|
||||
:width="donutElWidth - 50"
|
||||
:height="265"
|
||||
:chart-data="inventoryByLocation"
|
||||
/>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</BaseCard>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<Subtitle> Quick Statistics </Subtitle>
|
||||
<div class="grid grid-cols-2 gap-2 md:grid-cols-4 md:gap-6">
|
||||
<StatCard v-for="(stat, i) in stats" :key="i" :title="stat.label" :value="stat.value" :type="stat.type" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<Subtitle> Recently Added </Subtitle>
|
||||
|
||||
<BaseCard v-if="breakpoints.lg">
|
||||
<Table :headers="itemTable.headers" :data="itemTable.items">
|
||||
<template #cell-warranty="{ item }">
|
||||
<Icon v-if="item.warranty" name="mdi-check" class="text-green-500 h-5 w-5" />
|
||||
<Icon v-else name="mdi-close" class="text-red-500 h-5 w-5" />
|
||||
</template>
|
||||
<template #cell-purchasePrice="{ item }">
|
||||
<Currency :amount="item.purchasePrice" />
|
||||
</template>
|
||||
<template #cell-location_Name="{ item }">
|
||||
<NuxtLink class="badge badge-sm badge-primary p-3" :to="`/location/${item.location.id}`">
|
||||
{{ item.location?.name }}
|
||||
</NuxtLink>
|
||||
</template>
|
||||
</Table>
|
||||
</BaseCard>
|
||||
<div v-else class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<ItemCard v-for="item in itemTable.items" :key="item.id" :item="item" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<Subtitle> Storage Locations </Subtitle>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 card md:grid-cols-3 gap-4">
|
||||
<LocationCard v-for="location in locations" :key="location.id" :location="location" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<Subtitle> Labels </Subtitle>
|
||||
<div class="flex gap-4 flex-wrap">
|
||||
<LabelChip v-for="label in labels" :key="label.id" size="lg" :label="label" class="shadow-md" />
|
||||
</div>
|
||||
</section>
|
||||
</BaseContainer>
|
||||
</div>
|
||||
</template>
|
39
frontend/pages/home/statistics.ts
Normal file
39
frontend/pages/home/statistics.ts
Normal file
|
@ -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[];
|
||||
});
|
||||
}
|
42
frontend/pages/home/table.ts
Normal file
42
frontend/pages/home/table.ts
Normal file
|
@ -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 || [],
|
||||
};
|
||||
});
|
||||
}
|
|
@ -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 = [];
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue