mirror of
https://github.com/hay-kot/homebox.git
synced 2025-07-05 10:08:35 +00:00
feat: items-editor (#5)
* format readme * update logo * format html * add logo to docs * repository for document and document tokens * add attachments type and repository * autogenerate types via scripts * use autogenerated types * attachment type updates * add insured and quantity fields for items * implement HasID interface for entities * implement label updates for items * implement service update method * WIP item update client side actions * check err on attachment * finish types for basic items editor * remove unused var * house keeping
This commit is contained in:
parent
fbc364dcd2
commit
95ab14b866
125 changed files with 15626 additions and 1791 deletions
8
frontend/.gitignore
vendored
8
frontend/.gitignore
vendored
|
@ -1,8 +0,0 @@
|
|||
node_modules
|
||||
*.log*
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
.output
|
||||
.env
|
||||
dist
|
|
@ -1,42 +0,0 @@
|
|||
# Nuxt 3 Minimal Starter
|
||||
|
||||
Look at the [nuxt 3 documentation](https://v3.nuxtjs.org) to learn more.
|
||||
|
||||
## Setup
|
||||
|
||||
Make sure to install the dependencies:
|
||||
|
||||
```bash
|
||||
# yarn
|
||||
yarn install
|
||||
|
||||
# npm
|
||||
npm install
|
||||
|
||||
# pnpm
|
||||
pnpm install --shamefully-hoist
|
||||
```
|
||||
|
||||
## Development Server
|
||||
|
||||
Start the development server on http://localhost:3000
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Production
|
||||
|
||||
Build the application for production:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Locally preview production build:
|
||||
|
||||
```bash
|
||||
npm run preview
|
||||
```
|
||||
|
||||
Checkout the [deployment documentation](https://v3.nuxtjs.org/guide/deploy/presets) for more information.
|
|
@ -70,7 +70,7 @@
|
|||
<BaseContainer>
|
||||
<h2 class="mt-1 text-4xl font-bold tracking-tight text-neutral-content sm:text-5xl lg:text-6xl flex">
|
||||
HomeB
|
||||
<AppLogo class="w-12 -mb-4" style="padding-left: 3px; padding-right: 2px" />
|
||||
<AppLogo class="w-12 -mb-4" />
|
||||
x
|
||||
</h2>
|
||||
<div class="ml-1 mt-2 text-lg text-neutral-content/75 space-x-2">
|
||||
|
|
|
@ -1,123 +1,47 @@
|
|||
<template>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 596.5055138004384 585.369487986598">
|
||||
<g
|
||||
stroke-linecap="round"
|
||||
transform="translate(437.568672588907 210.93877417794465) rotate(332.3235338946895 66.970006481548 27.559467997664797)"
|
||||
>
|
||||
<path
|
||||
d="M-0.3 -0.89 L131.27 -1.27 L131.05 52.32 L-2.89 53.3"
|
||||
stroke="none"
|
||||
stroke-width="0"
|
||||
fill="#15aabf"
|
||||
></path>
|
||||
<path
|
||||
d="M1.61 2.92 C34.39 0.43, 67.49 -3.76, 136.43 -0.16 M-1.81 1.81 C54.26 1.13, 105.28 -0.86, 133.28 -0.04 M132.66 3.06 C133.92 12.97, 132.16 31.97, 132.92 51.2 M134.28 1.16 C131.72 11.69, 133.56 23.47, 134.52 54.65 M133.93 53.09 C103.49 59.22, 80.28 58.51, -1.19 52.09 M133.03 54.88 C92.08 50.88, 53.71 52.46, -0.4 54.88 M-3.64 53.12 C-1.65 33.33, 3.49 15.58, -2.23 -1.93 M-1.08 54.38 C0.63 44.74, -0.42 33.82, 0.29 1.34"
|
||||
stroke="#000"
|
||||
stroke-width="2"
|
||||
fill="none"
|
||||
></path>
|
||||
</g>
|
||||
<g stroke-linecap="round">
|
||||
<g transform="translate(308.4481755172761 281.2115533662909) rotate(0 1.1385609918289674 145.9953857867422)">
|
||||
<path
|
||||
d="M-1.01 -2.17 C-1.07 46.71, 0.3 244.44, -0.46 294.16 M3.63 2.83 C3.44 50.71, -0.72 241.22, -1.36 289.41"
|
||||
stroke="#000000"
|
||||
stroke-width="2"
|
||||
fill="none"
|
||||
></path>
|
||||
</g>
|
||||
</g>
|
||||
<g stroke-linecap="round">
|
||||
<g transform="translate(308.16925883018916 284.66360015581995) rotate(0 135.1525798049602 -68.20042785962323)">
|
||||
<path
|
||||
d="M2.47 0.47 C46.8 -21.36, 220.33 -110.19, 264.96 -133.2 M0.37 -1.74 C45.62 -24.11, 225.01 -114.58, 269.93 -136.88"
|
||||
stroke="#000000"
|
||||
stroke-width="2"
|
||||
fill="none"
|
||||
></path>
|
||||
</g>
|
||||
</g>
|
||||
<g stroke-linecap="round">
|
||||
<g transform="translate(311.39372316987726 570.9674003164946) rotate(0 136.24116036890297 -67.43777376368234)">
|
||||
<path
|
||||
d="M-2.63 2.46 C20.4 -9.48, 94.34 -47.87, 140.63 -71.17 C186.92 -94.47, 252.17 -126.49, 275.11 -137.33 M1.14 1.33 C23.81 -10.35, 94.55 -46.04, 139.83 -68.58 C185.12 -91.11, 249.48 -121.76, 272.86 -133.89"
|
||||
stroke="#000000"
|
||||
stroke-width="2"
|
||||
fill="none"
|
||||
></path>
|
||||
</g>
|
||||
</g>
|
||||
<g stroke-linecap="round">
|
||||
<g transform="translate(580.6092831051336 150.72201134532952) rotate(0 1.5678417062894852 142.75141008423634)">
|
||||
<path
|
||||
d="M2.66 -1.91 C3.6 45.58, 2.41 239.01, 2.99 287.41 M0.67 3.23 C1.39 51.03, -0.51 235.96, 0.31 282.74"
|
||||
stroke="#000000"
|
||||
stroke-width="2"
|
||||
fill="none"
|
||||
></path>
|
||||
</g>
|
||||
</g>
|
||||
<g stroke-linecap="round">
|
||||
<g transform="translate(306.6976102947664 283.14653391715) rotate(0 -140.18354779216435 -59.60806644015338)">
|
||||
<path
|
||||
d="M-0.81 0.62 C-48.48 -18.36, -235.46 -96.23, -283.34 -115.75 M3.94 -1.52 C-44.13 -21.12, -236.56 -99.84, -284.31 -119.83"
|
||||
stroke="#000000"
|
||||
stroke-width="2"
|
||||
fill="none"
|
||||
></path>
|
||||
</g>
|
||||
</g>
|
||||
<g stroke-linecap="round">
|
||||
<g transform="translate(304.3414324224632 572.5226612839633) rotate(0 -144.27019052747903 -64.7761163684645)">
|
||||
<path
|
||||
d="M2.34 1.71 C-46 -19.52, -242.75 -105.04, -290.88 -126.78 M0.17 0.18 C-47.25 -21.98, -237.9 -110.2, -285.78 -131.27"
|
||||
stroke="#000000"
|
||||
stroke-width="2"
|
||||
fill="none"
|
||||
></path>
|
||||
</g>
|
||||
</g>
|
||||
<g stroke-linecap="round">
|
||||
<g
|
||||
transform="translate(15.275892138818847 448.50738095516135) rotate(0 -0.49579063445983707 -143.71703352554232)"
|
||||
>
|
||||
<path
|
||||
d="M-2.4 0.97 C-2.94 -47.38, -0.78 -240.15, -0.9 -288.41 M1.49 -0.97 C0.55 -49.09, -0.95 -237.81, -2.03 -285.33"
|
||||
stroke="#000000"
|
||||
stroke-width="2"
|
||||
fill="none"
|
||||
></path>
|
||||
</g>
|
||||
</g>
|
||||
<g stroke-linecap="round">
|
||||
<g transform="translate(10.301143858432795 164.72182108536072) rotate(0 142.35890827057267 -76.26873721417542)">
|
||||
<path
|
||||
d="M2.04 -1.02 C49.94 -26.43, 238.14 -126.5, 285.02 -151.52 M-0.3 -4.04 C47.43 -29.16, 234.44 -124.45, 282.68 -148.52"
|
||||
stroke="#000000"
|
||||
stroke-width="2"
|
||||
fill="none"
|
||||
></path>
|
||||
</g>
|
||||
</g>
|
||||
<g stroke-linecap="round">
|
||||
<g transform="translate(291.46813332015165 14.258444139957646) rotate(0 143.3244001532809 66.53622476241344)">
|
||||
<path
|
||||
d="M-0.18 -1.16 C46.98 21.36, 236.83 111.22, 284.98 134.14 M-3.72 -4.26 C44.15 18.77, 241.76 114.69, 290.37 137.33"
|
||||
stroke="#000000"
|
||||
stroke-width="2"
|
||||
fill="none"
|
||||
></path>
|
||||
</g>
|
||||
</g>
|
||||
<g stroke-linecap="round">
|
||||
<g transform="translate(175.60844139934756 81.23280016017816) rotate(0 131.7777041676277 66.73388742398038)">
|
||||
<path
|
||||
d="M-1.87 -0.8 C42.4 22.26, 220.78 113.99, 265.42 137.17 M2.32 -3.7 C46.4 18.63, 220.35 109.95, 264.21 132.92"
|
||||
stroke="#000000"
|
||||
stroke-width="2"
|
||||
fill="none"
|
||||
></path>
|
||||
</g>
|
||||
</g>
|
||||
<svg
|
||||
viewBox="0 0 10817 9730"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xml:space="preserve"
|
||||
style="
|
||||
fill-rule: evenodd;
|
||||
clip-rule: evenodd;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
stroke-miterlimit: 5.42683;
|
||||
"
|
||||
>
|
||||
<path
|
||||
d="M9310.16 2560.9c245.302 249.894 419.711 539.916 565.373 845.231 47.039 98.872 36.229 215.514-28.2 304.05-64.391 88.536-172.099 134.676-280.631 120.28 0 .053-.039.053-.039.053"
|
||||
style="fill: gray; stroke: #000; stroke-width: 206.41px"
|
||||
/>
|
||||
<path
|
||||
d="M5401.56 487.044c-127.958 6.227-254.855 40.77-370.992 103.628-765.271 414.225-2397.45 1297.68-3193.03 1728.32-137.966 74.669-250.327 183.605-328.791 313.046l3963.09 2122.43s-249.048 416.428-470.593 786.926c-189.24 316.445-592.833 429.831-919.198 258.219l-2699.36-1419.32v2215.59c0 226.273 128.751 435.33 337.755 548.466 764.649 413.885 2620.97 1418.66 3385.59 1832.51 209.018 113.137 466.496 113.137 675.514 0 764.623-413.857 2620.94-1418.63 3385.59-1832.51 208.989-113.136 337.743-322.193 337.743-548.466v-3513.48c0-318.684-174.59-611.722-454.853-763.409-795.543-430.632-2427.75-1314.09-3193.02-1728.32-141.693-76.684-299.364-111.227-455.442-103.628"
|
||||
style="fill: #dadada; stroke: #000; stroke-width: 206.42px"
|
||||
/>
|
||||
<path
|
||||
d="M5471.83 4754.46V504.71c-127.958 6.226-325.127 23.1-441.264 85.958-765.271 414.225-2397.45 1297.68-3193.03 1728.32-137.966 74.669-250.327 183.605-328.791 313.046l3963.09 2122.43Z"
|
||||
style="fill: gray; stroke: #000; stroke-width: 206.42px"
|
||||
/>
|
||||
<path
|
||||
d="m1459.34 2725.96-373.791 715.667c-177.166 339.292-46.417 758 292.375 936.167l4.75 2.5m0 0 2699.37 1419.29c326.374 171.625 729.916 58.25 919.165-258.208 221.542-370.5 470.583-786.917 470.583-786.917l-3963.04-2122.42-2.167 3.458-47.25 90.458"
|
||||
style="fill: #dadada; stroke: #000; stroke-width: 206.42px"
|
||||
/>
|
||||
<path d="M5443.74 520.879v4149.79" style="fill: none; stroke: #000; stroke-width: 153.5px" />
|
||||
<path
|
||||
d="M8951.41 4102.72c0-41.65-22.221-80.136-58.291-100.961-36.069-20.825-80.51-20.825-116.58 0l-2439.92 1408.69c-36.07 20.825-58.29 59.311-58.29 100.961V7058c0 41.65 22.22 80.136 58.29 100.961 36.07 20.825 80.51 20.825 116.58 0l2439.92-1408.69c36.07-20.825 58.291-59.312 58.291-100.962v-1546.59Z"
|
||||
style="fill: #567f67"
|
||||
/>
|
||||
<path
|
||||
d="M8951.41 4102.72c0-41.65-22.221-80.136-58.291-100.961-36.069-20.825-80.51-20.825-116.58 0l-2439.92 1408.69c-36.07 20.825-58.29 59.311-58.29 100.961V7058c0 41.65 22.22 80.136 58.29 100.961 36.07 20.825 80.51 20.825 116.58 0l2439.92-1408.69c36.07-20.825 58.291-59.312 58.291-100.962v-1546.59ZM6463.98 5551.29v1387.06l2301.77-1328.92V4222.37L6463.98 5551.29Z"
|
||||
/>
|
||||
<path
|
||||
d="M5443.76 9041.74v-4278.4"
|
||||
style="fill: none; stroke: #000; stroke-width: 206.44px; stroke-linejoin: miter"
|
||||
/>
|
||||
<path
|
||||
d="m5471.79 4773.86 3829.35-2188.22"
|
||||
style="fill: none; stroke: #000; stroke-width: 206.43px; stroke-linejoin: miter"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
{{ dKey }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
||||
<slot :name="dKey" v-bind="{ key: dKey, value: dValue }">
|
||||
<slot :name="rmSpace(dKey)" v-bind="{ key: dKey, value: dValue }">
|
||||
{{ dValue }}
|
||||
</slot>
|
||||
</dd>
|
||||
|
@ -28,8 +28,13 @@
|
|||
<script setup lang="ts">
|
||||
type StringLike = string | number | boolean;
|
||||
|
||||
function rmSpace(str: string) {
|
||||
return str.replace(" ", "");
|
||||
}
|
||||
|
||||
defineProps({
|
||||
details: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type: Object as () => Record<string, StringLike | any>,
|
||||
required: true,
|
||||
},
|
||||
|
|
35
frontend/components/Form/Checkbox.vue
Normal file
35
frontend/components/Form/Checkbox.vue
Normal file
|
@ -0,0 +1,35 @@
|
|||
<template>
|
||||
<div v-if="!inline" class="form-control w-full">
|
||||
<label class="label cursor-pointer">
|
||||
<span class="label-text"> {{ label }}</span>
|
||||
<input v-model="value" type="checkbox" class="checkbox" />
|
||||
</label>
|
||||
</div>
|
||||
<div v-else class="label cursor-pointer sm:grid sm:grid-cols-4 sm:items-start sm:gap-4">
|
||||
<label>
|
||||
<span class="label-text">
|
||||
{{ label }}
|
||||
</span>
|
||||
</label>
|
||||
<input v-model="value" type="checkbox" class="checkbox" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
inline: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
const value = useVModel(props, "modelValue");
|
||||
</script>
|
|
@ -52,9 +52,14 @@
|
|||
|
||||
const selected = useVModel(props, "modelValue", emit);
|
||||
const dateText = computed(() => {
|
||||
if (!validDate(selected.value)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (selected.value) {
|
||||
return selected.value.toLocaleDateString();
|
||||
}
|
||||
|
||||
return "";
|
||||
});
|
||||
|
||||
|
@ -91,9 +96,7 @@
|
|||
});
|
||||
|
||||
function select(e: MouseEvent, day: Date) {
|
||||
console.log(day);
|
||||
selected.value = day;
|
||||
console.log(selected.value);
|
||||
// @ts-ignore - this is a vue3 bug
|
||||
e.target.blur();
|
||||
resetTime();
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
v-for="(obj, idx) in items"
|
||||
:key="idx"
|
||||
:class="{
|
||||
bordered: selectedIndexes[idx],
|
||||
bordered: selected[idx],
|
||||
}"
|
||||
>
|
||||
<button type="button" @click="toggle(idx)">
|
||||
|
@ -37,10 +37,12 @@
|
|||
default: "",
|
||||
},
|
||||
modelValue: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type: Array as () => any[],
|
||||
default: null,
|
||||
},
|
||||
items: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type: Array as () => any[],
|
||||
required: true,
|
||||
},
|
||||
|
@ -54,28 +56,23 @@
|
|||
},
|
||||
});
|
||||
|
||||
const selectedIndexes = ref<Record<number, boolean>>({});
|
||||
const value = useVModel(props, "modelValue", emit);
|
||||
|
||||
const selected = computed<Record<number, boolean>>(() => {
|
||||
const obj: Record<number, boolean> = {};
|
||||
value.value.forEach(itm => {
|
||||
const idx = props.items.findIndex(item => item[props.name] === itm.name);
|
||||
obj[idx] = true;
|
||||
});
|
||||
return obj;
|
||||
});
|
||||
|
||||
function toggle(index: number) {
|
||||
selectedIndexes.value[index] = !selectedIndexes.value[index];
|
||||
|
||||
const item = props.items[index];
|
||||
|
||||
if (selectedIndexes.value[index]) {
|
||||
value.value = [...value.value, item];
|
||||
if (selected.value[index]) {
|
||||
value.value = value.value.filter(itm => itm.name !== item.name);
|
||||
} else {
|
||||
value.value = value.value.filter(itm => itm !== item);
|
||||
value.value = [...value.value, item];
|
||||
}
|
||||
}
|
||||
|
||||
watchOnce(
|
||||
() => props.items,
|
||||
() => {
|
||||
if (props.selectFirst && props.items.length > 0) {
|
||||
value.value = props.items[0];
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const value = useVModel(props, "modelValue", emit);
|
||||
</script>
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
<label class="label">
|
||||
<span class="label-text">{{ label }}</span>
|
||||
</label>
|
||||
<select v-model="value" class="select select-bordered">
|
||||
<select v-model="selectedIdx" class="select select-bordered">
|
||||
<option disabled selected>Pick one</option>
|
||||
<option v-for="obj in items" :key="name != '' ? obj[name] : obj" :value="obj">
|
||||
<option v-for="(obj, idx) in items" :key="name != '' ? obj[name] : obj" :value="idx">
|
||||
{{ name != "" ? obj[name] : obj }}
|
||||
</option>
|
||||
</select>
|
||||
|
@ -24,10 +24,12 @@
|
|||
default: "",
|
||||
},
|
||||
modelValue: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type: Object as any,
|
||||
default: null,
|
||||
},
|
||||
items: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type: Array as () => any[],
|
||||
required: true,
|
||||
},
|
||||
|
@ -45,10 +47,16 @@
|
|||
() => props.items,
|
||||
() => {
|
||||
if (props.selectFirst && props.items.length > 0) {
|
||||
value.value = props.items[0];
|
||||
selectedIdx.value = 0;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const value = useVModel(props, "modelValue", emit);
|
||||
const selectedIdx = ref(0);
|
||||
watch(
|
||||
() => selectedIdx.value,
|
||||
() => {
|
||||
emit("update:modelValue", props.items[selectedIdx.value]);
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<template>
|
||||
<div v-if="!inline" class="form-control">
|
||||
<div v-if="!inline" class="form-control w-full">
|
||||
<label class="label">
|
||||
<span class="label-text">{{ label }}</span>
|
||||
</label>
|
||||
<textarea v-model="value" class="textarea textarea-bordered h-24" :placeholder="placeholder" />
|
||||
<textarea ref="el" v-model="value" class="textarea w-full textarea-bordered h-28" :placeholder="placeholder" />
|
||||
<label v-if="limit" class="label">
|
||||
<span class="label-text-alt"></span>
|
||||
<span class="label-text-alt"> {{ valueLen }}/{{ limit }}</span>
|
||||
|
@ -14,10 +14,12 @@
|
|||
<span class="label-text">{{ label }}</span>
|
||||
</label>
|
||||
<textarea
|
||||
ref="el"
|
||||
v-model="value"
|
||||
class="textarea textarea-bordered col-span-3 mt-3 h-24"
|
||||
class="textarea textarea-bordered w-full col-span-3 mt-3 h-28"
|
||||
auto-grow
|
||||
:placeholder="placeholder"
|
||||
auto-height
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -51,6 +53,19 @@
|
|||
},
|
||||
});
|
||||
|
||||
const el = ref();
|
||||
function setHeight() {
|
||||
el.value.style.height = "auto";
|
||||
el.value.style.height = el.value.scrollHeight + 5 + "px";
|
||||
}
|
||||
|
||||
onUpdated(() => {
|
||||
console.log("updated");
|
||||
if (props.inline) {
|
||||
setHeight();
|
||||
}
|
||||
});
|
||||
|
||||
const value = useVModel(props, "modelValue", emit);
|
||||
const valueLen = computed(() => {
|
||||
return value.value ? value.value.length : 0;
|
||||
|
|
|
@ -22,11 +22,11 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Item } from "~~/lib/api/classes/items";
|
||||
import { ItemOut, ItemSummary } from "~~/lib/api/types/data-contracts";
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
type: Object as () => Item,
|
||||
type: Object as () => ItemOut | ItemSummary,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -26,7 +26,8 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { type Location } from "~~/lib/api/classes/locations";
|
||||
import { ItemCreate, LocationOut } from "~~/lib/api/types/data-contracts";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
|
@ -40,7 +41,7 @@
|
|||
const loading = ref(false);
|
||||
const focused = ref(false);
|
||||
const form = reactive({
|
||||
location: {} as Location,
|
||||
location: {} as LocationOut,
|
||||
name: "",
|
||||
description: "",
|
||||
color: "", // Future!
|
||||
|
@ -80,7 +81,7 @@
|
|||
return;
|
||||
}
|
||||
|
||||
const out = {
|
||||
const out: ItemCreate = {
|
||||
name: form.name,
|
||||
description: form.description,
|
||||
locationId: form.location.id as string,
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<script setup lang="ts">
|
||||
import { Label } from "~~/lib/api/classes/labels";
|
||||
import { LabelOut, LabelSummary } from "~~/lib/api/types/data-contracts";
|
||||
|
||||
export type sizes = "sm" | "md" | "lg";
|
||||
defineProps({
|
||||
label: {
|
||||
type: Object as () => Label,
|
||||
type: Object as () => LabelOut | LabelSummary,
|
||||
required: true,
|
||||
},
|
||||
size: {
|
||||
|
|
|
@ -26,11 +26,11 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Location } from "~~/lib/api/classes/locations";
|
||||
import { LocationCount } from "~~/lib/api/types/data-contracts";
|
||||
|
||||
defineProps({
|
||||
location: {
|
||||
type: Object as () => Location,
|
||||
type: Object as () => LocationCount,
|
||||
required: true,
|
||||
},
|
||||
dense: {
|
||||
|
|
52
frontend/components/global/DateTime.vue
Normal file
52
frontend/components/global/DateTime.vue
Normal file
|
@ -0,0 +1,52 @@
|
|||
<template>
|
||||
{{ value }}
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
enum DateTimeFormat {
|
||||
RELATIVE = "relative",
|
||||
LONG = "long",
|
||||
SHORT = "short",
|
||||
}
|
||||
|
||||
const value = computed(() => {
|
||||
if (!props.date) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const dt = typeof props.date === "string" ? new Date(props.date) : props.date;
|
||||
if (!dt) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (nullDate(dt)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
switch (props.format) {
|
||||
case DateTimeFormat.RELATIVE:
|
||||
return useTimeAgo(dt).value + useDateFormat(dt, " (MM-DD-YYYY)").value;
|
||||
case DateTimeFormat.LONG:
|
||||
return useDateFormat(dt, "YYYY-MM-DD (dddd)").value;
|
||||
case DateTimeFormat.SHORT:
|
||||
return useDateFormat(dt, "YYYY-MM-DD").value;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
function nullDate(dt: Date) {
|
||||
return dt.getFullYear() === 1;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
date: {
|
||||
type: [Date, String],
|
||||
required: true,
|
||||
},
|
||||
format: {
|
||||
type: String as () => DateTimeFormat,
|
||||
default: "relative",
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -1,6 +1,7 @@
|
|||
import { UseConfirmDialogReturn } from "@vueuse/core";
|
||||
import { Ref } from "vue";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type Store = UseConfirmDialogReturn<any, boolean, boolean> & {
|
||||
text: Ref<string>;
|
||||
setup: boolean;
|
||||
|
@ -21,6 +22,7 @@ const store: Partial<Store> = {
|
|||
export function useConfirm(): Store {
|
||||
if (!store.setup) {
|
||||
store.setup = true;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const { isRevealed, reveal, confirm, cancel } = useConfirmDialog<any, boolean, boolean>();
|
||||
store.isRevealed = isRevealed;
|
||||
store.reveal = reveal;
|
||||
|
|
|
@ -3,6 +3,7 @@ import { Ref } from "vue";
|
|||
export type LocationViewPreferences = {
|
||||
showDetails: boolean;
|
||||
showEmpty: boolean;
|
||||
editorSimpleView: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -15,6 +16,7 @@ export function useViewPreferences(): Ref<LocationViewPreferences> {
|
|||
{
|
||||
showDetails: true,
|
||||
showEmpty: true,
|
||||
editorSimpleView: true,
|
||||
},
|
||||
{ mergeDefaults: true }
|
||||
);
|
||||
|
|
35
frontend/composables/utils.ts
Normal file
35
frontend/composables/utils.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
export function validDate(dt: Date | string | null | undefined): boolean {
|
||||
if (!dt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If it's a string, try to parse it
|
||||
if (typeof dt === "string") {
|
||||
const parsed = new Date(dt);
|
||||
if (isNaN(parsed.getTime())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// If it's a date, check if it's valid
|
||||
if (dt instanceof Date) {
|
||||
if (dt.getFullYear() < 1000) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function fmtCurrency(value: number | string, currency = "USD", locale = "en-Us"): string {
|
||||
if (typeof value === "string") {
|
||||
value = parseFloat(value);
|
||||
}
|
||||
|
||||
const formatter = new Intl.NumberFormat(locale, {
|
||||
style: "currency",
|
||||
currency,
|
||||
minimumFractionDigits: 2,
|
||||
});
|
||||
return formatter.format(value);
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, test } from "vitest";
|
||||
import { Label } from "../../classes/labels";
|
||||
import { LabelOut } from "../../types/data-contracts";
|
||||
import { UserApi } from "../../user";
|
||||
import { sharedUserClient } from "../test-utils";
|
||||
|
||||
|
@ -10,7 +10,7 @@ describe("locations lifecycle (create, update, delete)", () => {
|
|||
* useLabel sets up a label resource for testing, and returns a function
|
||||
* that can be used to delete the label from the backend server.
|
||||
*/
|
||||
async function useLabel(api: UserApi): Promise<[Label, () => Promise<void>]> {
|
||||
async function useLabel(api: UserApi): Promise<[LabelOut, () => Promise<void>]> {
|
||||
const { response, data } = await api.labels.create({
|
||||
name: `__test__.label.name_${increment}`,
|
||||
description: `__test__.label.description_${increment}`,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, test } from "vitest";
|
||||
import { Location } from "../../classes/locations";
|
||||
import { LocationOut } from "../../types/data-contracts";
|
||||
import { UserApi } from "../../user";
|
||||
import { sharedUserClient } from "../test-utils";
|
||||
|
||||
|
@ -10,7 +10,7 @@ describe("locations lifecycle (create, update, delete)", () => {
|
|||
* useLocatio 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: UserApi): Promise<[Location, () => Promise<void>]> {
|
||||
async function useLocation(api: UserApi): Promise<[LocationOut, () => Promise<void>]> {
|
||||
const { response, data } = await api.locations.create({
|
||||
name: `__test__.location.name_${increment}`,
|
||||
description: `__test__.location.description_${increment}`,
|
||||
|
|
30
frontend/lib/api/base/base-api.test.ts
Normal file
30
frontend/lib/api/base/base-api.test.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { describe, expect, test } from "vitest";
|
||||
import { hasKey, parseDate } from "./base-api";
|
||||
|
||||
describe("hasKey works as expected", () => {
|
||||
test("hasKey returns true if the key exists", () => {
|
||||
const obj = { createdAt: "2021-01-01" };
|
||||
expect(hasKey(obj, "createdAt")).toBe(true);
|
||||
});
|
||||
|
||||
test("hasKey returns false if the key does not exist", () => {
|
||||
const obj = { createdAt: "2021-01-01" };
|
||||
expect(hasKey(obj, "updatedAt")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseDate should work as expected", () => {
|
||||
test("parseDate should set defaults", () => {
|
||||
const obj = { createdAt: "2021-01-01", updatedAt: "2021-01-01" };
|
||||
const result = parseDate(obj);
|
||||
expect(result.createdAt).toBeInstanceOf(Date);
|
||||
expect(result.updatedAt).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
test("parseDate should set passed in types", () => {
|
||||
const obj = { key1: "2021-01-01", key2: "2021-01-01" };
|
||||
const result = parseDate(obj, ["key1", "key2"]);
|
||||
expect(result.key1).toBeInstanceOf(Date);
|
||||
expect(result.key2).toBeInstanceOf(Date);
|
||||
});
|
||||
});
|
|
@ -8,10 +8,50 @@ import { Requests } from "../../requests";
|
|||
// TDeleteResult = void
|
||||
// >
|
||||
|
||||
type BaseApiType = {
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export function hasKey(obj: object, key: string): obj is Required<BaseApiType> {
|
||||
return typeof obj[key] === "string";
|
||||
}
|
||||
|
||||
export function parseDate<T>(obj: T, keys: Array<keyof T> = []): T {
|
||||
const result = { ...obj };
|
||||
[...keys, "createdAt", "updatedAt"].forEach(key => {
|
||||
// @ts-ignore - we are checking for the key above
|
||||
if (hasKey(result, key)) {
|
||||
// @ts-ignore - we are guarding against this above
|
||||
result[key] = new Date(result[key]);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export class BaseAPI {
|
||||
http: Requests;
|
||||
|
||||
constructor(requests: Requests) {
|
||||
this.http = requests;
|
||||
}
|
||||
|
||||
/**
|
||||
* dropFields will remove any fields that are specified in the fields array
|
||||
* additionally, it will remove the `createdAt` and `updatedAt` fields if they
|
||||
* are present. This is useful for when you want to send a subset of fields to
|
||||
* the server like when performing an update.
|
||||
*/
|
||||
dropFields<T>(obj: T, keys: Array<keyof T> = []): T {
|
||||
const result = { ...obj };
|
||||
[...keys, "createdAt", "updatedAt"].forEach(key => {
|
||||
// @ts-ignore - we are checking for the key above
|
||||
if (hasKey(result, key)) {
|
||||
// @ts-ignore - we are guarding against this above
|
||||
delete result[key];
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,60 +1,26 @@
|
|||
import { BaseAPI, route } from "../base";
|
||||
import { Label } from "./labels";
|
||||
import { Location } from "./locations";
|
||||
import { parseDate } from "../base/base-api";
|
||||
import { ItemCreate, ItemOut, ItemSummary, ItemUpdate } from "../types/data-contracts";
|
||||
import { Results } from "./types";
|
||||
|
||||
export interface ItemCreate {
|
||||
name: string;
|
||||
description: string;
|
||||
locationId: string;
|
||||
labelIds: string[];
|
||||
}
|
||||
|
||||
export interface Item {
|
||||
createdAt: string;
|
||||
description: string;
|
||||
id: string;
|
||||
labels: Label[];
|
||||
location: Location;
|
||||
manufacturer: string;
|
||||
modelNumber: string;
|
||||
name: string;
|
||||
notes: string;
|
||||
purchaseFrom: string;
|
||||
purchasePrice: number;
|
||||
purchaseTime: Date;
|
||||
serialNumber: string;
|
||||
soldNotes: string;
|
||||
soldPrice: number;
|
||||
soldTime: Date;
|
||||
soldTo: string;
|
||||
updatedAt: string;
|
||||
lifetimeWarranty: boolean;
|
||||
warrantyExpires: Date;
|
||||
warrantyDetails: string;
|
||||
}
|
||||
|
||||
export class ItemsApi extends BaseAPI {
|
||||
getAll() {
|
||||
return this.http.get<Results<Item>>({ url: route("/items") });
|
||||
return this.http.get<Results<ItemOut>>({ url: route("/items") });
|
||||
}
|
||||
|
||||
create(item: ItemCreate) {
|
||||
return this.http.post<ItemCreate, Item>({ url: route("/items"), body: item });
|
||||
return this.http.post<ItemCreate, ItemSummary>({ url: route("/items"), body: item });
|
||||
}
|
||||
|
||||
async get(id: string) {
|
||||
const payload = await this.http.get<Item>({ url: route(`/items/${id}`) });
|
||||
const payload = await this.http.get<ItemOut>({ url: route(`/items/${id}`) });
|
||||
|
||||
if (!payload.data) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
// Parse Date Types
|
||||
payload.data.purchaseTime = new Date(payload.data.purchaseTime);
|
||||
payload.data.soldTime = new Date(payload.data.soldTime);
|
||||
payload.data.warrantyExpires = new Date(payload.data.warrantyExpires);
|
||||
|
||||
payload.data = parseDate(payload.data, ["purchaseTime", "soldTime", "warrantyExpires"]);
|
||||
return payload;
|
||||
}
|
||||
|
||||
|
@ -62,8 +28,17 @@ export class ItemsApi extends BaseAPI {
|
|||
return this.http.delete<void>({ url: route(`/items/${id}`) });
|
||||
}
|
||||
|
||||
update(id: string, item: ItemCreate) {
|
||||
return this.http.put<ItemCreate, Item>({ url: route(`/items/${id}`), body: item });
|
||||
async update(id: string, item: ItemUpdate) {
|
||||
const payload = await this.http.put<ItemCreate, ItemOut>({
|
||||
url: route(`/items/${id}`),
|
||||
body: this.dropFields(item),
|
||||
});
|
||||
if (!payload.data) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
payload.data = parseDate(payload.data, ["purchaseTime", "soldTime", "warrantyExpires"]);
|
||||
return payload;
|
||||
}
|
||||
|
||||
import(file: File) {
|
||||
|
|
|
@ -1,37 +1,25 @@
|
|||
import { BaseAPI, route } from "../base";
|
||||
import { Item } from "./items";
|
||||
import { Details, OutType, Results } from "./types";
|
||||
|
||||
export type LabelCreate = Details & {
|
||||
color: string;
|
||||
};
|
||||
|
||||
export type LabelUpdate = LabelCreate;
|
||||
|
||||
export type Label = LabelCreate &
|
||||
OutType & {
|
||||
groupId: string;
|
||||
items: Item[];
|
||||
};
|
||||
import { LabelCreate, LabelOut } from "../types/data-contracts";
|
||||
import { Results } from "./types";
|
||||
|
||||
export class LabelsApi extends BaseAPI {
|
||||
getAll() {
|
||||
return this.http.get<Results<Label>>({ url: route("/labels") });
|
||||
return this.http.get<Results<LabelOut>>({ url: route("/labels") });
|
||||
}
|
||||
|
||||
create(body: LabelCreate) {
|
||||
return this.http.post<LabelCreate, Label>({ url: route("/labels"), body });
|
||||
return this.http.post<LabelCreate, LabelOut>({ url: route("/labels"), body });
|
||||
}
|
||||
|
||||
get(id: string) {
|
||||
return this.http.get<Label>({ url: route(`/labels/${id}`) });
|
||||
return this.http.get<LabelOut>({ url: route(`/labels/${id}`) });
|
||||
}
|
||||
|
||||
delete(id: string) {
|
||||
return this.http.delete<void>({ url: route(`/labels/${id}`) });
|
||||
}
|
||||
|
||||
update(id: string, body: LabelUpdate) {
|
||||
return this.http.put<LabelUpdate, Label>({ url: route(`/labels/${id}`), body });
|
||||
update(id: string, body: LabelCreate) {
|
||||
return this.http.put<LabelCreate, LabelOut>({ url: route(`/labels/${id}`), body });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,29 +1,20 @@
|
|||
import { BaseAPI, route } from "../base";
|
||||
import { Item } from "./items";
|
||||
import { Details, OutType, Results } from "./types";
|
||||
|
||||
export type LocationCreate = Details;
|
||||
|
||||
export type Location = LocationCreate &
|
||||
OutType & {
|
||||
groupId: string;
|
||||
items: Item[];
|
||||
itemCount: number;
|
||||
};
|
||||
import { LocationCount, LocationCreate, LocationOut } from "../types/data-contracts";
|
||||
import { Results } from "./types";
|
||||
|
||||
export type LocationUpdate = LocationCreate;
|
||||
|
||||
export class LocationsApi extends BaseAPI {
|
||||
getAll() {
|
||||
return this.http.get<Results<Location>>({ url: route("/locations") });
|
||||
return this.http.get<Results<LocationCount>>({ url: route("/locations") });
|
||||
}
|
||||
|
||||
create(body: LocationCreate) {
|
||||
return this.http.post<LocationCreate, Location>({ url: route("/locations"), body });
|
||||
return this.http.post<LocationCreate, LocationOut>({ url: route("/locations"), body });
|
||||
}
|
||||
|
||||
get(id: string) {
|
||||
return this.http.get<Location>({ url: route(`/locations/${id}`) });
|
||||
return this.http.get<LocationOut>({ url: route(`/locations/${id}`) });
|
||||
}
|
||||
|
||||
delete(id: string) {
|
||||
|
@ -31,6 +22,6 @@ export class LocationsApi extends BaseAPI {
|
|||
}
|
||||
|
||||
update(id: string, body: LocationUpdate) {
|
||||
return this.http.put<LocationUpdate, Location>({ url: route(`/locations/${id}`), body });
|
||||
return this.http.put<LocationUpdate, LocationOut>({ url: route(`/locations/${id}`), body });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,3 @@
|
|||
/**
|
||||
* OutType is the base type that is returned from the API.
|
||||
* In contains the common fields that are included with every
|
||||
* API response that isn't a bulk result
|
||||
*/
|
||||
export type OutType = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type Details = {
|
||||
name: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export type Results<T> = {
|
||||
items: T[];
|
||||
};
|
||||
|
|
262
frontend/lib/api/types/data-contracts.ts
Normal file
262
frontend/lib/api/types/data-contracts.ts
Normal file
|
@ -0,0 +1,262 @@
|
|||
/* post-processed by ./scripts/process-types.py */
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
/*
|
||||
* ---------------------------------------------------------------
|
||||
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
|
||||
* ## ##
|
||||
* ## AUTHOR: acacode ##
|
||||
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
|
||||
* ---------------------------------------------------------------
|
||||
*/
|
||||
|
||||
export interface ServerResult {
|
||||
details: any;
|
||||
error: boolean;
|
||||
item: any;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface ServerResults {
|
||||
items: any;
|
||||
}
|
||||
|
||||
export interface ApiSummary {
|
||||
health: boolean;
|
||||
message: string;
|
||||
title: string;
|
||||
versions: string[];
|
||||
}
|
||||
|
||||
export interface DocumentOut {
|
||||
id: string;
|
||||
path: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface ItemAttachment {
|
||||
createdAt: Date;
|
||||
document: DocumentOut;
|
||||
id: string;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface ItemCreate {
|
||||
description: string;
|
||||
labelIds: string[];
|
||||
|
||||
/** Edges */
|
||||
locationId: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ItemOut {
|
||||
attachments: ItemAttachment[];
|
||||
createdAt: Date;
|
||||
description: string;
|
||||
id: string;
|
||||
insured: boolean;
|
||||
labels: LabelSummary[];
|
||||
|
||||
/** Warranty */
|
||||
lifetimeWarranty: boolean;
|
||||
|
||||
/** Edges */
|
||||
location: LocationSummary;
|
||||
manufacturer: string;
|
||||
modelNumber: string;
|
||||
name: string;
|
||||
|
||||
/** Extras */
|
||||
notes: string;
|
||||
purchaseFrom: string;
|
||||
|
||||
/** @example 0 */
|
||||
purchasePrice: string;
|
||||
|
||||
/** Purchase */
|
||||
purchaseTime: Date;
|
||||
quantity: number;
|
||||
|
||||
/** Identifications */
|
||||
serialNumber: string;
|
||||
soldNotes: string;
|
||||
|
||||
/** @example 0 */
|
||||
soldPrice: string;
|
||||
|
||||
/** Sold */
|
||||
soldTime: Date;
|
||||
soldTo: string;
|
||||
updatedAt: Date;
|
||||
warrantyDetails: string;
|
||||
warrantyExpires: Date;
|
||||
}
|
||||
|
||||
export interface ItemSummary {
|
||||
createdAt: Date;
|
||||
description: string;
|
||||
id: string;
|
||||
insured: boolean;
|
||||
labels: LabelSummary[];
|
||||
|
||||
/** Warranty */
|
||||
lifetimeWarranty: boolean;
|
||||
|
||||
/** Edges */
|
||||
location: LocationSummary;
|
||||
manufacturer: string;
|
||||
modelNumber: string;
|
||||
name: string;
|
||||
|
||||
/** Extras */
|
||||
notes: string;
|
||||
purchaseFrom: string;
|
||||
|
||||
/** @example 0 */
|
||||
purchasePrice: string;
|
||||
|
||||
/** Purchase */
|
||||
purchaseTime: Date;
|
||||
quantity: number;
|
||||
|
||||
/** Identifications */
|
||||
serialNumber: string;
|
||||
soldNotes: string;
|
||||
|
||||
/** @example 0 */
|
||||
soldPrice: string;
|
||||
|
||||
/** Sold */
|
||||
soldTime: Date;
|
||||
soldTo: string;
|
||||
updatedAt: Date;
|
||||
warrantyDetails: string;
|
||||
warrantyExpires: Date;
|
||||
}
|
||||
|
||||
export interface ItemUpdate {
|
||||
description: string;
|
||||
id: string;
|
||||
insured: boolean;
|
||||
labelIds: string[];
|
||||
|
||||
/** Warranty */
|
||||
lifetimeWarranty: boolean;
|
||||
|
||||
/** Edges */
|
||||
locationId: string;
|
||||
manufacturer: string;
|
||||
modelNumber: string;
|
||||
name: string;
|
||||
|
||||
/** Extras */
|
||||
notes: string;
|
||||
purchaseFrom: string;
|
||||
|
||||
/** @example 0 */
|
||||
purchasePrice: string;
|
||||
|
||||
/** Purchase */
|
||||
purchaseTime: Date;
|
||||
quantity: number;
|
||||
|
||||
/** Identifications */
|
||||
serialNumber: string;
|
||||
soldNotes: string;
|
||||
|
||||
/** @example 0 */
|
||||
soldPrice: string;
|
||||
|
||||
/** Sold */
|
||||
soldTime: Date;
|
||||
soldTo: string;
|
||||
warrantyDetails: string;
|
||||
warrantyExpires: Date;
|
||||
}
|
||||
|
||||
export interface LabelCreate {
|
||||
color: string;
|
||||
description: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface LabelOut {
|
||||
createdAt: Date;
|
||||
description: string;
|
||||
groupId: string;
|
||||
id: string;
|
||||
items: ItemSummary[];
|
||||
name: string;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface LabelSummary {
|
||||
createdAt: Date;
|
||||
description: string;
|
||||
groupId: string;
|
||||
id: string;
|
||||
name: string;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface LocationCount {
|
||||
createdAt: Date;
|
||||
description: string;
|
||||
id: string;
|
||||
itemCount: number;
|
||||
name: string;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface LocationCreate {
|
||||
description: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface LocationOut {
|
||||
createdAt: Date;
|
||||
description: string;
|
||||
id: string;
|
||||
items: ItemSummary[];
|
||||
name: string;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface LocationSummary {
|
||||
createdAt: Date;
|
||||
description: string;
|
||||
id: string;
|
||||
name: string;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface TokenResponse {
|
||||
expiresAt: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface UserIn {
|
||||
email: string;
|
||||
name: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface UserOut {
|
||||
email: string;
|
||||
groupId: string;
|
||||
groupName: string;
|
||||
id: string;
|
||||
isSuperuser: boolean;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface UserRegistration {
|
||||
groupName: string;
|
||||
user: UserIn;
|
||||
}
|
||||
|
||||
export interface UserUpdate {
|
||||
email: string;
|
||||
name: string;
|
||||
}
|
|
@ -55,7 +55,6 @@
|
|||
|
||||
function setFile(e: Event & { target: HTMLInputElement }) {
|
||||
importCsv.value = e.target.files[0];
|
||||
console.log("importCsv.value", importCsv.value);
|
||||
}
|
||||
|
||||
const toast = useNotifier();
|
||||
|
|
|
@ -129,7 +129,7 @@
|
|||
<div>
|
||||
<h2 class="mt-1 text-4xl font-bold tracking-tight text-neutral-content sm:text-5xl lg:text-6xl flex">
|
||||
HomeB
|
||||
<AppLogo class="w-12 -mb-4" style="padding-left: 3px; padding-right: 2px" />
|
||||
<AppLogo class="w-12 -mb-4" />
|
||||
x
|
||||
</h2>
|
||||
<p class="ml-1 text-lg text-base-content/50">Track, Organize, and Manage your Shit.</p>
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { ItemUpdate } from "~~/lib/api/types/data-contracts";
|
||||
|
||||
definePageMeta({
|
||||
layout: "home",
|
||||
});
|
||||
|
@ -6,9 +8,20 @@
|
|||
const route = useRoute();
|
||||
const api = useUserApi();
|
||||
const toast = useNotifier();
|
||||
const preferences = useViewPreferences();
|
||||
|
||||
const itemId = computed<string>(() => route.params.id as string);
|
||||
|
||||
const { data: locations } = useAsyncData(async () => {
|
||||
const { data } = await api.locations.getAll();
|
||||
return data.items;
|
||||
});
|
||||
|
||||
const { data: labels } = useAsyncData(async () => {
|
||||
const { data } = await api.labels.getAll();
|
||||
return data.items;
|
||||
});
|
||||
|
||||
const { data: item } = useAsyncData(async () => {
|
||||
const { data, error } = await api.items.get(itemId.value);
|
||||
if (error) {
|
||||
|
@ -16,11 +29,30 @@
|
|||
navigateTo("/home");
|
||||
return;
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
|
||||
async function saveItem() {
|
||||
const payload: ItemUpdate = {
|
||||
...item.value,
|
||||
locationId: item.value.location?.id,
|
||||
labelIds: item.value.labels.map(l => l.id),
|
||||
};
|
||||
|
||||
const { error } = await api.items.update(itemId.value, payload);
|
||||
|
||||
if (error) {
|
||||
toast.error("Failed to save item");
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success("Item saved");
|
||||
navigateTo("/item/" + itemId.value);
|
||||
}
|
||||
|
||||
type FormField = {
|
||||
type: "text" | "textarea" | "select" | "date";
|
||||
type: "text" | "textarea" | "select" | "date" | "label" | "location" | "number" | "checkbox";
|
||||
label: string;
|
||||
ref: string;
|
||||
};
|
||||
|
@ -31,6 +63,11 @@
|
|||
label: "Name",
|
||||
ref: "name",
|
||||
},
|
||||
{
|
||||
type: "number",
|
||||
label: "Quantity",
|
||||
ref: "quantity",
|
||||
},
|
||||
{
|
||||
type: "textarea",
|
||||
label: "Description",
|
||||
|
@ -56,6 +93,11 @@
|
|||
label: "Notes",
|
||||
ref: "notes",
|
||||
},
|
||||
{
|
||||
type: "checkbox",
|
||||
label: "Insured",
|
||||
ref: "insured",
|
||||
},
|
||||
];
|
||||
|
||||
const purchaseFields: FormField[] = [
|
||||
|
@ -76,6 +118,24 @@
|
|||
},
|
||||
];
|
||||
|
||||
const warrantyFields: FormField[] = [
|
||||
{
|
||||
type: "checkbox",
|
||||
label: "Lifetime Warranty",
|
||||
ref: "lifetimeWarranty",
|
||||
},
|
||||
{
|
||||
type: "date",
|
||||
label: "Warranty Expires",
|
||||
ref: "warrantyExpires",
|
||||
},
|
||||
{
|
||||
type: "textarea",
|
||||
label: "Warranty Notes",
|
||||
ref: "warrantyDetails",
|
||||
},
|
||||
];
|
||||
|
||||
const soldFields = [
|
||||
{
|
||||
type: "text",
|
||||
|
@ -97,51 +157,193 @@
|
|||
|
||||
<template>
|
||||
<BaseContainer v-if="item" class="pb-8">
|
||||
<div class="space-y-4">
|
||||
<div class="overflow-hidden card bg-base-100 shadow-xl sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:px-6">
|
||||
<h3 class="text-lg font-medium leading-6">Item Details</h3>
|
||||
</div>
|
||||
<div class="border-t border-gray-300 sm:p-0">
|
||||
<div v-for="field in mainFields" :key="field.ref" class="sm:divide-y sm:divide-gray-300 grid grid-cols-1">
|
||||
<div class="pt-2 pb-4 sm:px-6 border-b border-gray-300">
|
||||
<FormTextArea v-if="field.type === 'textarea'" v-model="item[field.ref]" :label="field.label" inline />
|
||||
<FormTextField v-else-if="field.type === 'text'" v-model="item[field.ref]" :label="field.label" inline />
|
||||
<FormDatePicker v-else-if="field.type === 'date'" v-model="item[field.ref]" :label="field.label" inline />
|
||||
</div>
|
||||
<section class="px-3">
|
||||
<div class="space-y-4">
|
||||
<div class="overflow-hidden card bg-base-100 shadow-xl sm:rounded-lg">
|
||||
<BaseSectionHeader v-if="item" class="p-5">
|
||||
<Icon name="mdi-package-variant" class="-mt-1 mr-2 text-gray-600" />
|
||||
<span class="text-gray-600">
|
||||
{{ item.name }}
|
||||
</span>
|
||||
<p class="text-sm text-gray-600 font-bold pb-0 mb-0">Quantity {{ item.quantity }}</p>
|
||||
<template #after>
|
||||
<div class="modal-action mt-3">
|
||||
<div class="mr-auto tooltip" data-tip="Hide the cruft! ">
|
||||
<label class="label cursor-pointer mr-auto">
|
||||
<input v-model="preferences.editorSimpleView" type="checkbox" class="toggle toggle-primary" />
|
||||
<span class="label-text ml-4"> Simple View </span>
|
||||
</label>
|
||||
</div>
|
||||
<BaseButton size="sm" @click="saveItem">
|
||||
<template #icon>
|
||||
<Icon name="mdi-content-save-outline" />
|
||||
</template>
|
||||
Save
|
||||
</BaseButton>
|
||||
</div>
|
||||
</template>
|
||||
</BaseSectionHeader>
|
||||
<div class="px-5 mb-6 grid md:grid-cols-2 gap-4">
|
||||
<FormSelect v-model="item.location" label="Location" :items="locations ?? []" select-first />
|
||||
<FormMultiselect v-model="item.labels" label="Labels" :items="labels ?? []" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-visible card bg-base-100 shadow-xl sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:px-6">
|
||||
<h3 class="text-lg font-medium leading-6">Purchase Details</h3>
|
||||
</div>
|
||||
<div class="border-t border-gray-300 sm:p-0">
|
||||
<div v-for="field in purchaseFields" :key="field.ref" class="sm:divide-y sm:divide-gray-300 grid grid-cols-1">
|
||||
<div class="pt-2 pb-4 sm:px-6 border-b border-gray-300">
|
||||
<FormTextArea v-if="field.type === 'textarea'" v-model="item[field.ref]" :label="field.label" inline />
|
||||
<FormTextField v-else-if="field.type === 'text'" v-model="item[field.ref]" :label="field.label" inline />
|
||||
<FormDatePicker v-else-if="field.type === 'date'" v-model="item[field.ref]" :label="field.label" inline />
|
||||
<div class="border-t border-gray-300 sm:p-0">
|
||||
<div v-for="field in mainFields" :key="field.ref" class="sm:divide-y sm:divide-gray-300 grid grid-cols-1">
|
||||
<div class="pt-2 px-4 pb-4 sm:px-6 border-b border-gray-300">
|
||||
<FormTextArea v-if="field.type === 'textarea'" v-model="item[field.ref]" :label="field.label" inline />
|
||||
<FormTextField
|
||||
v-else-if="field.type === 'text'"
|
||||
v-model="item[field.ref]"
|
||||
:label="field.label"
|
||||
inline
|
||||
/>
|
||||
<FormTextField
|
||||
v-else-if="field.type === 'number'"
|
||||
v-model.number="item[field.ref]"
|
||||
type="number"
|
||||
:label="field.label"
|
||||
inline
|
||||
/>
|
||||
<FormDatePicker
|
||||
v-else-if="field.type === 'date'"
|
||||
v-model="item[field.ref]"
|
||||
:label="field.label"
|
||||
inline
|
||||
/>
|
||||
<FormCheckbox
|
||||
v-else-if="field.type === 'checkbox'"
|
||||
v-model="item[field.ref]"
|
||||
:label="field.label"
|
||||
inline
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-visible card bg-base-100 shadow-xl sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:px-6">
|
||||
<h3 class="text-lg font-medium leading-6">Sold Details</h3>
|
||||
<div v-if="!preferences.editorSimpleView" class="overflow-visible card bg-base-100 shadow-xl sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:px-6">
|
||||
<h3 class="text-lg font-medium leading-6">Purchase Details</h3>
|
||||
</div>
|
||||
<div class="border-t border-gray-300 sm:p-0">
|
||||
<div
|
||||
v-for="field in purchaseFields"
|
||||
:key="field.ref"
|
||||
class="sm:divide-y sm:divide-gray-300 grid grid-cols-1"
|
||||
>
|
||||
<div class="pt-2 px-4 pb-4 sm:px-6 border-b border-gray-300">
|
||||
<FormTextArea v-if="field.type === 'textarea'" v-model="item[field.ref]" :label="field.label" inline />
|
||||
<FormTextField
|
||||
v-else-if="field.type === 'text'"
|
||||
v-model="item[field.ref]"
|
||||
:label="field.label"
|
||||
inline
|
||||
/>
|
||||
<FormTextField
|
||||
v-else-if="field.type === 'number'"
|
||||
v-model.number="item[field.ref]"
|
||||
type="number"
|
||||
:label="field.label"
|
||||
inline
|
||||
/>
|
||||
<FormDatePicker
|
||||
v-else-if="field.type === 'date'"
|
||||
v-model="item[field.ref]"
|
||||
:label="field.label"
|
||||
inline
|
||||
/>
|
||||
<FormCheckbox
|
||||
v-else-if="field.type === 'checkbox'"
|
||||
v-model="item[field.ref]"
|
||||
:label="field.label"
|
||||
inline
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t border-gray-300 sm:p-0">
|
||||
<div v-for="field in soldFields" :key="field.ref" class="sm:divide-y sm:divide-gray-300 grid grid-cols-1">
|
||||
<div class="pt-2 pb-4 sm:px-6 border-b border-gray-300">
|
||||
<FormTextArea v-if="field.type === 'textarea'" v-model="item[field.ref]" :label="field.label" inline />
|
||||
<FormTextField v-else-if="field.type === 'text'" v-model="item[field.ref]" :label="field.label" inline />
|
||||
<FormDatePicker v-else-if="field.type === 'date'" v-model="item[field.ref]" :label="field.label" inline />
|
||||
|
||||
<div v-if="!preferences.editorSimpleView" class="overflow-visible card bg-base-100 shadow-xl sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:px-6">
|
||||
<h3 class="text-lg font-medium leading-6">Warranty Details</h3>
|
||||
</div>
|
||||
<div class="border-t border-gray-300 sm:p-0">
|
||||
<div
|
||||
v-for="field in warrantyFields"
|
||||
:key="field.ref"
|
||||
class="sm:divide-y sm:divide-gray-300 grid grid-cols-1"
|
||||
>
|
||||
<div class="pt-2 px-4 pb-4 sm:px-6 border-b border-gray-300">
|
||||
<FormTextArea v-if="field.type === 'textarea'" v-model="item[field.ref]" :label="field.label" inline />
|
||||
<FormTextField
|
||||
v-else-if="field.type === 'text'"
|
||||
v-model="item[field.ref]"
|
||||
:label="field.label"
|
||||
inline
|
||||
/>
|
||||
<FormTextField
|
||||
v-else-if="field.type === 'number'"
|
||||
v-model.number="item[field.ref]"
|
||||
type="number"
|
||||
:label="field.label"
|
||||
inline
|
||||
/>
|
||||
<FormDatePicker
|
||||
v-else-if="field.type === 'date'"
|
||||
v-model="item[field.ref]"
|
||||
:label="field.label"
|
||||
inline
|
||||
/>
|
||||
<FormCheckbox
|
||||
v-else-if="field.type === 'checkbox'"
|
||||
v-model="item[field.ref]"
|
||||
:label="field.label"
|
||||
inline
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!preferences.editorSimpleView" class="overflow-visible card bg-base-100 shadow-xl sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:px-6">
|
||||
<h3 class="text-lg font-medium leading-6">Sold Details</h3>
|
||||
</div>
|
||||
<div class="border-t border-gray-300 sm:p-0">
|
||||
<div v-for="field in soldFields" :key="field.ref" class="sm:divide-y sm:divide-gray-300 grid grid-cols-1">
|
||||
<div class="pt-2 pb-4 px-4 sm:px-6 border-b border-gray-300">
|
||||
<FormTextArea v-if="field.type === 'textarea'" v-model="item[field.ref]" :label="field.label" inline />
|
||||
<FormTextField
|
||||
v-else-if="field.type === 'text'"
|
||||
v-model="item[field.ref]"
|
||||
:label="field.label"
|
||||
inline
|
||||
/>
|
||||
<FormTextField
|
||||
v-else-if="field.type === 'number'"
|
||||
v-model.number="item[field.ref]"
|
||||
type="number"
|
||||
:label="field.label"
|
||||
inline
|
||||
/>
|
||||
<FormDatePicker
|
||||
v-else-if="field.type === 'date'"
|
||||
v-model="item[field.ref]"
|
||||
:label="field.label"
|
||||
inline
|
||||
/>
|
||||
<FormCheckbox
|
||||
v-else-if="field.type === 'checkbox'"
|
||||
v-model="item[field.ref]"
|
||||
:label="field.label"
|
||||
inline
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</BaseContainer>
|
||||
</template>
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
const itemId = computed<string>(() => route.params.id as string);
|
||||
const preferences = useViewPreferences();
|
||||
|
||||
const { data: item } = useAsyncData(async () => {
|
||||
const { data: item, refresh } = useAsyncData(itemId.value, async () => {
|
||||
const { data, error } = await api.items.get(itemId.value);
|
||||
if (error) {
|
||||
toast.error("Failed to load item");
|
||||
|
@ -20,6 +20,11 @@
|
|||
return data;
|
||||
});
|
||||
|
||||
// Trigger Refresh on navigate
|
||||
onMounted(() => {
|
||||
refresh();
|
||||
});
|
||||
|
||||
const itemSummary = computed(() => {
|
||||
return {
|
||||
Description: item.value?.description || "",
|
||||
|
@ -27,6 +32,7 @@
|
|||
"Model Number": item.value?.modelNumber || "",
|
||||
Manufacturer: item.value?.manufacturer || "",
|
||||
Notes: item.value?.notes || "",
|
||||
Insured: item.value?.insured ? "Yes" : "No",
|
||||
Attachments: "", // TODO: Attachments
|
||||
};
|
||||
});
|
||||
|
@ -35,15 +41,15 @@
|
|||
if (preferences.value.showEmpty) {
|
||||
return true;
|
||||
}
|
||||
return item.value?.warrantyExpires !== undefined;
|
||||
return validDate(item.value?.warrantyExpires);
|
||||
});
|
||||
|
||||
const warrantyDetails = computed(() => {
|
||||
const payload = {};
|
||||
const payload = {
|
||||
"Lifetime Warranty": item.value?.lifetimeWarranty ? "Yes" : "No",
|
||||
};
|
||||
|
||||
if (item.value.lifetimeWarranty) {
|
||||
payload["Lifetime Warranty"] = "Yes";
|
||||
} else {
|
||||
if (showWarranty.value) {
|
||||
payload["Warranty Expires"] = item.value?.warrantyExpires || "";
|
||||
}
|
||||
|
||||
|
@ -62,7 +68,7 @@
|
|||
const purchaseDetails = computed(() => {
|
||||
return {
|
||||
"Purchased From": item.value?.purchaseFrom || "",
|
||||
"Purchased Price": item.value?.purchasePrice || "",
|
||||
"Purchased Price": item.value?.purchasePrice ? fmtCurrency(item.value.purchasePrice) : "",
|
||||
"Purchased At": item.value?.purchaseTime || "",
|
||||
};
|
||||
});
|
||||
|
@ -78,7 +84,7 @@
|
|||
const soldDetails = computed(() => {
|
||||
return {
|
||||
"Sold To": item.value?.soldTo || "",
|
||||
"Sold Price": item.value?.soldPrice || "",
|
||||
"Sold Price": item.value?.soldPrice ? fmtCurrency(item.value.soldPrice) : "",
|
||||
"Sold At": item.value?.soldTime || "",
|
||||
};
|
||||
});
|
||||
|
@ -103,7 +109,7 @@
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<BaseContainer class="pb-8">
|
||||
<BaseContainer v-if="item" class="pb-8">
|
||||
<section class="px-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="form-control"></div>
|
||||
|
@ -116,11 +122,14 @@
|
|||
<span class="text-gray-600">
|
||||
{{ item.name }}
|
||||
</span>
|
||||
<p class="text-sm text-gray-600 font-bold pb-0 mb-0">
|
||||
{{ item.location.name }} - Quantity {{ item.quantity }}
|
||||
</p>
|
||||
<template #after>
|
||||
<div class="flex flex-wrap gap-3 mt-3">
|
||||
<div v-if="item.labels && item.labels.length > 0" class="flex flex-wrap gap-3 mt-3">
|
||||
<LabelChip v-for="label in item.labels" :key="label.id" class="badge-primary" :label="label" />
|
||||
</div>
|
||||
<div class="modal-action">
|
||||
<div class="modal-action mt-3">
|
||||
<label class="label cursor-pointer mr-auto">
|
||||
<input v-model="preferences.showEmpty" type="checkbox" class="toggle toggle-primary" />
|
||||
<span class="label-text ml-4"> Show Empty </span>
|
||||
|
@ -166,12 +175,21 @@
|
|||
</BaseDetails>
|
||||
<BaseDetails v-if="showPurchase" :details="purchaseDetails">
|
||||
<template #title> Purchase Details </template>
|
||||
<template #PurchasedAt>
|
||||
<DateTime :date="item.purchaseTime" />
|
||||
</template>
|
||||
</BaseDetails>
|
||||
<BaseDetails v-if="showWarranty" :details="warrantyDetails">
|
||||
<template #title> Warranty </template>
|
||||
<template #WarrantyExpires>
|
||||
<DateTime :date="item.warrantyExpires" />
|
||||
</template>
|
||||
</BaseDetails>
|
||||
<BaseDetails v-if="showSold" :details="soldDetails">
|
||||
<template #title> Sold </template>
|
||||
<template #SoldAt>
|
||||
<DateTime :date="item.soldTime" />
|
||||
</template>
|
||||
</BaseDetails>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -1 +1,11 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M2,10.96C1.5,10.68 1.35,10.07 1.63,9.59L3.13,7C3.24,6.8 3.41,6.66 3.6,6.58L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.66,6.72 20.82,6.88 20.91,7.08L22.36,9.6C22.64,10.08 22.47,10.69 22,10.96L21,11.54V16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V10.96C2.7,11.13 2.32,11.14 2,10.96M12,4.15V4.15L12,10.85V10.85L17.96,7.5L12,4.15M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V12.69L14,15.59C13.67,15.77 13.3,15.76 13,15.6V19.29L19,15.91M13.85,13.36L20.13,9.73L19.55,8.72L13.27,12.35L13.85,13.36Z" /></svg>
|
||||
<svg viewBox="0 0 10817 9730" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:5.42683">
|
||||
<path d="M9310.16 2560.9c245.302 249.894 419.711 539.916 565.373 845.231 47.039 98.872 36.229 215.514-28.2 304.05-64.391 88.536-172.099 134.676-280.631 120.28 0 .053-.039.053-.039.053" style="fill:gray;stroke:#000;stroke-width:206.41px"/>
|
||||
<path d="M5401.56 487.044c-127.958 6.227-254.855 40.77-370.992 103.628-765.271 414.225-2397.45 1297.68-3193.03 1728.32-137.966 74.669-250.327 183.605-328.791 313.046l3963.09 2122.43s-249.048 416.428-470.593 786.926c-189.24 316.445-592.833 429.831-919.198 258.219l-2699.36-1419.32v2215.59c0 226.273 128.751 435.33 337.755 548.466 764.649 413.885 2620.97 1418.66 3385.59 1832.51 209.018 113.137 466.496 113.137 675.514 0 764.623-413.857 2620.94-1418.63 3385.59-1832.51 208.989-113.136 337.743-322.193 337.743-548.466v-3513.48c0-318.684-174.59-611.722-454.853-763.409-795.543-430.632-2427.75-1314.09-3193.02-1728.32-141.693-76.684-299.364-111.227-455.442-103.628" style="fill:#dadada;stroke:#000;stroke-width:206.42px"/>
|
||||
<path d="M5471.83 4754.46V504.71c-127.958 6.226-325.127 23.1-441.264 85.958-765.271 414.225-2397.45 1297.68-3193.03 1728.32-137.966 74.669-250.327 183.605-328.791 313.046l3963.09 2122.43Z" style="fill:gray;stroke:#000;stroke-width:206.42px"/>
|
||||
<path d="m1459.34 2725.96-373.791 715.667c-177.166 339.292-46.417 758 292.375 936.167l4.75 2.5m0 0 2699.37 1419.29c326.374 171.625 729.916 58.25 919.165-258.208 221.542-370.5 470.583-786.917 470.583-786.917l-3963.04-2122.42-2.167 3.458-47.25 90.458" style="fill:#dadada;stroke:#000;stroke-width:206.42px"/>
|
||||
<path d="M5443.74 520.879v4149.79" style="fill:none;stroke:#000;stroke-width:153.5px"/>
|
||||
<path d="M8951.41 4102.72c0-41.65-22.221-80.136-58.291-100.961-36.069-20.825-80.51-20.825-116.58 0l-2439.92 1408.69c-36.07 20.825-58.29 59.311-58.29 100.961V7058c0 41.65 22.22 80.136 58.29 100.961 36.07 20.825 80.51 20.825 116.58 0l2439.92-1408.69c36.07-20.825 58.291-59.312 58.291-100.962v-1546.59Z" style="fill:#567f67"/>
|
||||
<path d="M8951.41 4102.72c0-41.65-22.221-80.136-58.291-100.961-36.069-20.825-80.51-20.825-116.58 0l-2439.92 1408.69c-36.07 20.825-58.29 59.311-58.29 100.961V7058c0 41.65 22.22 80.136 58.29 100.961 36.07 20.825 80.51 20.825 116.58 0l2439.92-1408.69c36.07-20.825 58.291-59.312 58.291-100.962v-1546.59ZM6463.98 5551.29v1387.06l2301.77-1328.92V4222.37L6463.98 5551.29Z"/>
|
||||
<path d="M5443.76 9041.74v-4278.4" style="fill:none;stroke:#000;stroke-width:206.44px;stroke-linejoin:miter"/>
|
||||
<path d="m5471.79 4773.86 3829.35-2188.22" style="fill:none;stroke:#000;stroke-width:206.43px;stroke-linejoin:miter"/>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 707 B After Width: | Height: | Size: 2.7 KiB |
Loading…
Add table
Add a link
Reference in a new issue