feat: add storage hierarchy component to display nested objects

This commit is contained in:
garionion (aider) 2025-02-27 18:16:18 +01:00
parent b5fe07d07e
commit 005b04e380
3 changed files with 318 additions and 2 deletions

View file

@ -0,0 +1,193 @@
<template>
<v-container>
<v-card>
<v-card-title class="text-h5">
Storage Hierarchy
<v-spacer></v-spacer>
<v-select
v-model="selectedStorageId"
:items="storageSpaces"
item-title="location"
item-value="id"
label="Select Storage Space"
density="compact"
variant="outlined"
class="ml-2"
style="max-width: 300px"
></v-select>
</v-card-title>
<v-card-text>
<v-alert
v-if="error"
type="error"
variant="tonal"
closable
>
{{ error }}
</v-alert>
<div v-if="loading" class="d-flex justify-center align-center my-4">
<v-progress-circular indeterminate color="primary"></v-progress-circular>
</div>
<template v-else>
<v-expansion-panels v-if="selectedStorage">
<storage-box
:storage="selectedStorage"
:objects="objectsInStorage"
:nested-storages="nestedStorages"
/>
</v-expansion-panels>
<v-alert v-else type="info" variant="tonal">
Please select a storage space to view its contents
</v-alert>
</template>
</v-card-text>
</v-card>
</v-container>
</template>
<script lang="ts" setup>
import { ref, computed, watch, onMounted } from 'vue';
import { useStorageStore } from '@/stores/storage';
// Component for recursive display of storage boxes
const StorageBox = defineComponent({
name: 'StorageBox',
props: {
storage: Object,
objects: Array,
nestedStorages: Array
},
setup(props) {
const objectsInCurrentStorage = computed(() => {
return props.objects.filter(obj => obj.storagespaceId === props.storage.id);
});
const childStorages = computed(() => {
return props.nestedStorages.filter(storage =>
storage.parent && storage.parent === props.storage.id
);
});
return () => (
<v-expansion-panel>
<v-expansion-panel-title>
<div class="d-flex align-center">
<v-icon class="mr-2">mdi-package-variant-closed</v-icon>
<span>{ props.storage.location || `Storage #${props.storage.id}` }</span>
<v-chip class="ml-2" size="small" color="primary" variant="outlined">
{ objectsInCurrentStorage.value.length } objects
</v-chip>
</div>
</v-expansion-panel-title>
<v-expansion-panel-text>
<div class="mb-4">
<h3 class="text-subtitle-1 mb-2">Objects in this storage:</h3>
{objectsInCurrentStorage.value.length > 0 ? (
<v-list lines="two">
{objectsInCurrentStorage.value.map(obj => (
<v-list-item
:key="obj.id"
:title="obj.name"
:subtitle="obj.description || 'No description'"
>
<template v-slot:prepend>
<v-icon>mdi-cube-outline</v-icon>
</template>
<template v-slot:append>
<v-chip size="small" color="grey" variant="flat">
SN: { obj.serialnumber || 'N/A' }
</v-chip>
</template>
</v-list-item>
))}
</v-list>
) : (
<v-alert type="info" variant="tonal" density="compact">
No objects in this storage
</v-alert>
)}
</div>
{childStorages.value.length > 0 && (
<div class="mt-4">
<h3 class="text-subtitle-1 mb-2">Nested storage spaces:</h3>
<v-expansion-panels>
{childStorages.value.map(childStorage => (
<StorageBox
key={childStorage.id}
storage={childStorage}
objects={props.objects}
nestedStorages={props.nestedStorages}
/>
))}
</v-expansion-panels>
</div>
)}
</v-expansion-panel-text>
</v-expansion-panel>
);
}
});
// Main component logic
const storageStore = useStorageStore();
const loading = ref(false);
const error = ref(null);
const selectedStorageId = ref(null);
const storageSpaces = computed(() => storageStore.storageSpaces);
const objects = computed(() => storageStore.objects);
const selectedStorage = computed(() => {
if (!selectedStorageId.value) return null;
return storageSpaces.value.find(s => s.id === selectedStorageId.value);
});
const objectsInStorage = computed(() => {
if (!selectedStorageId.value) return [];
return objects.value;
});
const nestedStorages = computed(() => {
return storageSpaces.value;
});
watch(selectedStorageId, async (newId) => {
if (newId) {
await fetchStorageData(newId);
}
});
async function fetchStorageData(storageId) {
loading.value = true;
error.value = null;
try {
await storageStore.fetchStorageHierarchy(storageId);
} catch (err) {
error.value = err.message || 'Failed to load storage data';
} finally {
loading.value = false;
}
}
onMounted(async () => {
loading.value = true;
error.value = null;
try {
await storageStore.fetchStorageSpaces();
if (storageSpaces.value.length > 0) {
selectedStorageId.value = storageSpaces.value[0].id;
}
} catch (err) {
error.value = err.message || 'Failed to load storage spaces';
} finally {
loading.value = false;
}
});
</script>

View file

@ -1,7 +1,21 @@
<template> <template>
<HelloWorld /> <v-container>
<v-row>
<v-col cols="12">
<h1 class="text-h4 mb-4">Inventory Management System</h1>
<v-card class="mb-4">
<v-card-text>
<p>Welcome to the Inventory Management System. Use this dashboard to view and manage objects in storage spaces.</p>
</v-card-text>
</v-card>
<StorageHierarchy />
</v-col>
</v-row>
</v-container>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
// import StorageHierarchy from '@/components/StorageHierarchy.vue';
</script> </script>

109
web/src/stores/storage.ts Normal file
View file

@ -0,0 +1,109 @@
import { defineStore } from 'pinia';
export const useStorageStore = defineStore('storage', {
state: () => ({
storageSpaces: [] as any[],
objects: [] as any[],
loading: false,
error: null as string | null,
}),
getters: {
getStorageById: (state) => (id: number) => {
return state.storageSpaces.find(storage => storage.id === id);
},
getObjectsInStorage: (state) => (storageId: number) => {
return state.objects.filter(obj => obj.storagespaceId === storageId);
},
getChildStorages: (state) => (parentId: number) => {
return state.storageSpaces.filter(storage =>
storage.parent && storage.parent === parentId
);
}
},
actions: {
async fetchStorageSpaces() {
this.loading = true;
this.error = null;
try {
const response = await fetch('/api/v1/storageSpaces', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`,
},
});
if (!response.ok) {
throw new Error('Failed to fetch storage spaces');
}
const data = await response.json();
this.storageSpaces = data;
} catch (error: any) {
this.error = error.message;
throw error;
} finally {
this.loading = false;
}
},
async fetchObjectsInStorage(storageId: number) {
this.loading = true;
this.error = null;
try {
const response = await fetch(`/api/v1/storageSpaces/${storageId}/objects`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`,
},
});
if (!response.ok) {
throw new Error('Failed to fetch objects');
}
const data = await response.json();
this.objects = data;
} catch (error: any) {
this.error = error.message;
throw error;
} finally {
this.loading = false;
}
},
async fetchStorageHierarchy(rootStorageId: number) {
this.loading = true;
this.error = null;
try {
// First, ensure we have all storage spaces
if (this.storageSpaces.length === 0) {
await this.fetchStorageSpaces();
}
// Then fetch all objects in the hierarchy
const response = await fetch(`/api/v1/storageSpaces/${rootStorageId}/hierarchy/objects`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`,
},
});
if (!response.ok) {
throw new Error('Failed to fetch storage hierarchy');
}
const data = await response.json();
this.objects = data;
} catch (error: any) {
this.error = error.message;
throw error;
} finally {
this.loading = false;
}
}
}
});