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>
|
||||
<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>
|
||||
|
||||
<script lang="ts" setup>
|
||||
//
|
||||
import StorageHierarchy from '@/components/StorageHierarchy.vue';
|
||||
</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