feat: add storage hierarchy component to display nested objects
This commit is contained in:
parent
b5fe07d07e
commit
005b04e380
3 changed files with 318 additions and 2 deletions
193
web/src/components/StorageHierarchy.vue
Normal file
193
web/src/components/StorageHierarchy.vue
Normal 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>
|
|
@ -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
109
web/src/stores/storage.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
Loading…
Add table
Reference in a new issue