feat: Add name field to storage space and create dedicated storage page

This commit is contained in:
garionion (aider) 2025-02-28 22:03:18 +01:00
parent 699dca3328
commit 9d2f8e27c5
6 changed files with 255 additions and 115 deletions

View file

@ -43,6 +43,7 @@ func (h *StorageHandler) GetStorageSpaces(c echo.Context) error {
"id": space.ID, "id": space.ID,
"parent": space.Parent, "parent": space.Parent,
"location": space.Location, "location": space.Location,
"name": space.Name,
} }
} }
@ -191,6 +192,7 @@ func (h *StorageHandler) CreateStorageSpace(c echo.Context) error {
// Parse request body // Parse request body
type StorageSpaceRequest struct { type StorageSpaceRequest struct {
Name string `json:"name"`
Location string `json:"location"` Location string `json:"location"`
Parent *struct { Parent *struct {
Valid bool `json:"valid"` Valid bool `json:"valid"`
@ -234,10 +236,18 @@ func (h *StorageHandler) CreateStorageSpace(c echo.Context) error {
location.String = req.Location location.String = req.Location
} }
// Convert name to sql.NullString
var name sql.NullString
if req.Name != "" {
name.Valid = true
name.String = req.Name
}
// Create storage space // Create storage space
err := h.db.CreateStorageSpace(ctx, db.CreateStorageSpaceParams{ err := h.db.CreateStorageSpace(ctx, db.CreateStorageSpaceParams{
Parent: parent, Parent: parent,
Location: location, Location: location,
Name: name,
}) })
if err != nil { if err != nil {

View file

@ -56,17 +56,17 @@ INSERT INTO pictures (user_id, storagespace_id, object_id, event_id, check_in_id
VALUES (?, ?, ?, ?, ?, ?, ?, ?); VALUES (?, ?, ?, ?, ?, ?, ?, ?);
-- name: GetAllStorageSpaces :many -- name: GetAllStorageSpaces :many
SELECT id, parent, location FROM storagespace; SELECT id, parent, location, name FROM storagespace;
-- name: GetStorageSpaceByID :one -- name: GetStorageSpaceByID :one
SELECT id, parent, location FROM storagespace WHERE id = ?; SELECT id, parent, location, name FROM storagespace WHERE id = ?;
-- name: GetChildStorageSpaces :many -- name: GetChildStorageSpaces :many
SELECT id, parent, location FROM storagespace WHERE parent = ?; SELECT id, parent, location, name FROM storagespace WHERE parent = ?;
-- name: GetObjectsByStorageID :many -- name: GetObjectsByStorageID :many
SELECT id, storagespace_id, name, description, serialnumber, created SELECT id, storagespace_id, name, description, serialnumber, created
FROM objects WHERE storagespace_id = ?; FROM objects WHERE storagespace_id = ?;
-- name: CreateStorageSpace :exec -- name: CreateStorageSpace :exec
INSERT INTO storagespace (parent, location) VALUES (?, ?); INSERT INTO storagespace (parent, location, name) VALUES (?, ?, ?);

View file

@ -1,117 +1,20 @@
<template> <template>
<v-app-bar color="primary" app> <v-app-bar color="primary" app>
<v-app-bar-title>Inventory Manager</v-app-bar-title> <v-app-bar-title>Inventory Manager</v-app-bar-title>
<v-tabs>
<v-tab to="/">Home</v-tab>
<v-tab to="/storage">Storage</v-tab>
</v-tabs>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn
prepend-icon="mdi-plus" <v-btn icon>
@click="openAddStorageDialog" <v-icon>mdi-account</v-icon>
>
Add Storage Space
</v-btn> </v-btn>
<!-- Dialog for adding new storage space -->
<v-dialog v-model="showDialog" max-width="500px">
<v-card>
<v-card-title>Add New Storage Space</v-card-title>
<v-card-text>
<v-form @submit.prevent="addStorageSpace">
<v-text-field
v-model="newStorage.location"
label="Location"
required
:error-messages="locationError"
></v-text-field>
<v-select
v-model="newStorage.parentId"
:items="storageSpaces"
item-title="location"
item-value="id"
label="Parent Storage (optional)"
clearable
>
<template v-slot:item="{ item, props }">
<v-list-item v-bind="props" :title="item.raw.location || `Storage #${item.raw.id}`"></v-list-item>
</template>
</v-select>
<v-alert
v-if="error"
type="error"
variant="tonal"
class="mt-3"
>
{{ error }}
</v-alert>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey" variant="text" @click="showDialog = false">Cancel</v-btn>
<v-btn
color="primary"
variant="elevated"
@click="addStorageSpace"
:loading="loading"
>
Save
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-app-bar> </v-app-bar>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed } from 'vue'; // No additional logic needed
import { useStorageStore } from '@/stores/storage';
const storageStore = useStorageStore();
const showDialog = ref(false);
const loading = ref(false);
const error = ref<string | null>(null);
const locationError = ref<string>('');
const newStorage = ref({
location: '',
parentId: null as number | null
});
const storageSpaces = computed(() => storageStore.storageSpaces);
function openAddStorageDialog() {
// Reset form
newStorage.value = {
location: '',
parentId: null
};
error.value = null;
locationError.value = '';
showDialog.value = true;
}
async function addStorageSpace() {
// Validate
if (!newStorage.value.location.trim()) {
locationError.value = 'Location is required';
return;
}
locationError.value = '';
loading.value = true;
error.value = null;
try {
await storageStore.createStorageSpace({
location: newStorage.value.location,
parentId: newStorage.value.parentId
});
showDialog.value = false;
} catch (err: any) {
error.value = err.message || 'Failed to create storage space';
} finally {
loading.value = false;
}
}
</script> </script>

View file

@ -106,7 +106,7 @@ const StorageBox = defineComponent({
<v-expansion-panel-title> <v-expansion-panel-title>
<div class="d-flex align-center"> <div class="d-flex align-center">
<v-icon class="mr-2">mdi-package-variant-closed</v-icon> <v-icon class="mr-2">mdi-package-variant-closed</v-icon>
<span>{{ storage.location || \`Storage #\${storage.id}\` }}</span> <span>{{ storage.name || storage.location || \`Storage #\${storage.id}\` }}</span>
<v-chip class="ml-2" size="small" color="primary" variant="outlined"> <v-chip class="ml-2" size="small" color="primary" variant="outlined">
{{ objectsInCurrentStorage.length }} objects {{ objectsInCurrentStorage.length }} objects
</v-chip> </v-chip>

224
web/src/pages/storage.vue Normal file
View file

@ -0,0 +1,224 @@
<template>
<v-container>
<v-row>
<v-col cols="12">
<v-card>
<v-card-title class="text-h5">
Storage Spaces
<v-spacer></v-spacer>
<v-btn
color="primary"
prepend-icon="mdi-plus"
@click="openAddStorageDialog"
>
Add Storage Space
</v-btn>
</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>
<v-table v-else>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Location</th>
<th>Parent</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="space in storageSpaces" :key="space.id">
<td>{{ space.id }}</td>
<td>{{ space.name || '-' }}</td>
<td>{{ space.location || '-' }}</td>
<td>
<span v-if="space.parent && space.parent.valid">
{{ getParentName(space.parent.int64) }}
</span>
<span v-else>-</span>
</td>
<td>
<v-btn
icon
variant="text"
size="small"
:to="`/storage/${space.id}`"
title="View Details"
>
<v-icon>mdi-eye</v-icon>
</v-btn>
</td>
</tr>
</tbody>
</v-table>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- Dialog for adding new storage space -->
<v-dialog v-model="showDialog" max-width="500px">
<v-card>
<v-card-title>Add New Storage Space</v-card-title>
<v-card-text>
<v-form @submit.prevent="addStorageSpace">
<v-text-field
v-model="newStorage.name"
label="Name"
required
:error-messages="nameError"
></v-text-field>
<v-text-field
v-model="newStorage.location"
label="Location"
required
:error-messages="locationError"
></v-text-field>
<v-select
v-model="newStorage.parentId"
:items="storageSpaces"
item-title="displayName"
item-value="id"
label="Parent Storage (optional)"
clearable
>
<template v-slot:item="{ item, props }">
<v-list-item v-bind="props" :title="item.raw.displayName"></v-list-item>
</template>
</v-select>
<v-alert
v-if="error"
type="error"
variant="tonal"
class="mt-3"
>
{{ error }}
</v-alert>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey" variant="text" @click="showDialog = false">Cancel</v-btn>
<v-btn
color="primary"
variant="elevated"
@click="addStorageSpace"
:loading="loading"
>
Save
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-container>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue';
import { useStorageStore } from '@/stores/storage';
const storageStore = useStorageStore();
const showDialog = ref(false);
const loading = ref(false);
const error = ref<string | null>(null);
const nameError = ref<string>('');
const locationError = ref<string>('');
const newStorage = ref({
name: '',
location: '',
parentId: null as number | null
});
const storageSpaces = computed(() => {
return storageStore.storageSpaces.map(space => ({
...space,
displayName: space.name || space.location || `Storage #${space.id}`
}));
});
function getParentName(parentId: number): string {
const parent = storageSpaces.value.find(s => s.id === parentId);
return parent ? (parent.name || parent.location || `Storage #${parent.id}`) : `ID: ${parentId}`;
}
function openAddStorageDialog() {
// Reset form
newStorage.value = {
name: '',
location: '',
parentId: null
};
error.value = null;
nameError.value = '';
locationError.value = '';
showDialog.value = true;
}
async function addStorageSpace() {
// Validate
let isValid = true;
if (!newStorage.value.name.trim()) {
nameError.value = 'Name is required';
isValid = false;
} else {
nameError.value = '';
}
if (!newStorage.value.location.trim()) {
locationError.value = 'Location is required';
isValid = false;
} else {
locationError.value = '';
}
if (!isValid) return;
loading.value = true;
error.value = null;
try {
await storageStore.createStorageSpace({
name: newStorage.value.name,
location: newStorage.value.location,
parentId: newStorage.value.parentId
});
showDialog.value = false;
} catch (err: any) {
error.value = err.message || 'Failed to create storage space';
} finally {
loading.value = false;
}
}
onMounted(async () => {
loading.value = true;
error.value = null;
try {
await storageStore.fetchStorageSpaces();
} catch (err: any) {
error.value = err.message || 'Failed to load storage spaces';
} finally {
loading.value = false;
}
});
</script>

View file

@ -8,6 +8,7 @@ interface StorageSpace {
int64: number; int64: number;
} | null; } | null;
location: string | null; location: string | null;
name: string | null;
} }
interface StorageObject { interface StorageObject {
@ -43,7 +44,7 @@ export const useStorageStore = defineStore('storage', {
}, },
actions: { actions: {
async createStorageSpace(data: { location: string; parentId: number | null }): Promise<void> { async createStorageSpace(data: { name: string; location: string; parentId: number | null }): Promise<void> {
this.loading = true; this.loading = true;
this.error = null; this.error = null;
@ -57,6 +58,7 @@ export const useStorageStore = defineStore('storage', {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify({
name: data.name,
location: data.location, location: data.location,
parent: parentData parent: parentData
}), }),
@ -95,7 +97,8 @@ export const useStorageStore = defineStore('storage', {
this.storageSpaces = data.map((space: any) => ({ this.storageSpaces = data.map((space: any) => ({
id: space.id, id: space.id,
parent: space.parent, parent: space.parent,
location: space.location location: space.location,
name: space.name
})); }));
} catch (error: any) { } catch (error: any) {
this.error = error.message; this.error = error.message;