diff --git a/frontend/assets/css/theme.css b/frontend/assets/css/theme.css index 4aebea9..28aa3df 100644 --- a/frontend/assets/css/theme.css +++ b/frontend/assets/css/theme.css @@ -233,4 +233,53 @@ input::placeholder { padding: 3rem; text-align: center; color: var(--text-muted); +} + +.modal-split { + display: grid; + grid-template-columns: 1fr; + gap: 2rem; + margin-top: 1.5rem; +} + +@media (min-width: 768px) { + .modal-split { + grid-template-columns: 1fr 1fr; + } +} + +.modal-content.large { + max-width: 800px; +} + +.inner-table { + width: 100%; + margin-top: 1rem; + border: 1px solid var(--border); + border-radius: 8px; + overflow: hidden; +} + +.inner-table th, .inner-table td { + padding: 0.75rem 1rem; + font-size: 0.85rem; +} + +.inner-table th { + background: #111827; +} + +.badge { + background: rgba(255, 255, 255, 0.05); + padding: 0.25rem 0.6rem; + border-radius: 999px; + font-size: 0.8rem; + color: var(--text-muted); + border: 1px solid var(--border); +} + +.badge.success { + background: rgba(16, 185, 129, 0.1); + color: var(--success); + border-color: rgba(16, 185, 129, 0.2); } \ No newline at end of file diff --git a/frontend/assets/js/api.js b/frontend/assets/js/api.js index c60960b..289ae55 100644 --- a/frontend/assets/js/api.js +++ b/frontend/assets/js/api.js @@ -72,11 +72,12 @@ async function loadItems() { tbody.innerHTML += ` ${item.name} - ${item.category} - ${item.total_quantity} + ${item.category} + ${item.total_quantity || 0} + - + `; @@ -200,8 +201,9 @@ async function loadProjects() { ${proj.name} ${proj.description} + - + `; @@ -244,4 +246,142 @@ async function deleteProject(id) { await apiRequest(`/api/project?id=${id}`, 'DELETE'); loadProjects(); } +} + +// ---- STOCK ---- +async function openStockModal(item) { + document.getElementById('stock-modal-title').innerText = `Stock: ${item.name}`; + document.getElementById('stock-item-id').value = item.id; + document.getElementById('stock-qty').value = ''; + document.getElementById('stock-modal').classList.add('show'); + + await reloadStockTable(item.id); + + const locData = await apiRequest('/api/location'); + const locSelect = document.getElementById('stock-location'); + locSelect.innerHTML = ''; + if (locData.locations) { + locData.locations.forEach(loc => { + locSelect.innerHTML += ``; + }); + } +} + +async function reloadStockTable(itemId) { + const tbody = document.getElementById('stock-table-body'); + tbody.innerHTML = 'Loading...'; + + try { + const data = await apiRequest(`/api/stock?item_id=${itemId}`); + tbody.innerHTML = ''; + if (!data.stock || data.stock.length === 0) { + tbody.innerHTML = 'No stock entries.'; + return; + } + + data.stock.forEach(st => { + tbody.innerHTML += ` + + ${st.location_name || `Location #${st.location_id}`} + ${st.quantity} + + + + + `; + }); + } catch (e) { + tbody.innerHTML = 'Failed to load.'; + } +} + +async function saveStock(event) { + event.preventDefault(); + const itemId = document.getElementById('stock-item-id').value; + const payload = { + item_id: parseInt(itemId, 10), + location_id: parseInt(document.getElementById('stock-location').value, 10), + quantity: parseInt(document.getElementById('stock-qty').value, 10) + }; + + await apiRequest('/api/stock', 'POST', payload); + document.getElementById('stock-qty').value = ''; + await reloadStockTable(itemId); + loadItems(); +} + +async function deleteStock(stockId, itemId) { + if (confirm("Remove this stock entry?")) { + await apiRequest(`/api/stock?id=${stockId}`, 'DELETE'); + await reloadStockTable(itemId); + loadItems(); + } +} + +// ---- ASSOCIATIONS ---- +async function openAssociationModal(project) { + document.getElementById('association-modal-title').innerText = `Items for: ${project.name}`; + document.getElementById('assoc-project-id').value = project.id; + document.getElementById('assoc-qty').value = ''; + document.getElementById('association-modal').classList.add('show'); + + await reloadAssociationTable(project.id); + + const itemData = await apiRequest('/api/item'); + const itemSelect = document.getElementById('assoc-item'); + itemSelect.innerHTML = ''; + if (itemData.items) { + itemData.items.forEach(it => { + itemSelect.innerHTML += ``; + }); + } +} + +async function reloadAssociationTable(projectId) { + const tbody = document.getElementById('association-table-body'); + tbody.innerHTML = 'Loading...'; + + try { + const data = await apiRequest(`/api/association?project_id=${projectId}`); + tbody.innerHTML = ''; + if (!data.associations || data.associations.length === 0) { + tbody.innerHTML = 'No allocated items.'; + return; + } + + data.associations.forEach(asc => { + tbody.innerHTML += ` + + ${asc.item_name || `Item #${asc.item_id}`} + ${asc.quantity} + + + + + `; + }); + } catch (e) { + tbody.innerHTML = 'Failed to load.'; + } +} + +async function saveAssociation(event) { + event.preventDefault(); + const projectId = document.getElementById('assoc-project-id').value; + const payload = { + project_id: parseInt(projectId, 10), + item_id: parseInt(document.getElementById('assoc-item').value, 10), + quantity: parseInt(document.getElementById('assoc-qty').value, 10) + }; + + await apiRequest('/api/association', 'POST', payload); + document.getElementById('assoc-qty').value = ''; + await reloadAssociationTable(projectId); +} + +async function deleteAssociation(assocId, projectId) { + if (confirm("Remove this item from the project?")) { + await apiRequest(`/api/association?id=${assocId}`, 'DELETE'); + await reloadAssociationTable(projectId); + } } \ No newline at end of file diff --git a/frontend/htmx/contents/dash/inventory.html b/frontend/htmx/contents/dash/inventory.html index 577f4c0..76f8cc1 100644 --- a/frontend/htmx/contents/dash/inventory.html +++ b/frontend/htmx/contents/dash/inventory.html @@ -36,9 +36,6 @@
-
- -
@@ -46,4 +43,46 @@
+ + {{ end }} \ No newline at end of file diff --git a/frontend/htmx/contents/dash/projects.html b/frontend/htmx/contents/dash/projects.html index ffcdd96..f4c08be 100644 --- a/frontend/htmx/contents/dash/projects.html +++ b/frontend/htmx/contents/dash/projects.html @@ -39,4 +39,46 @@ + + {{ end }} \ No newline at end of file diff --git a/models/inventory.go b/models/inventory.go index cdc4a73..becb799 100644 --- a/models/inventory.go +++ b/models/inventory.go @@ -5,7 +5,8 @@ type Item struct { Name string `json:"name"` Category string `json:"category"` Description string `json:"description"` - TotalQuantity int `json:"total_quantity"` + TotalQuantity int `json:"total_quantity"` // Berechnet aus der Summe aller Stocks + FreeQuantity int `json:"free_quantity"` // TotalQuantity minus Summe aller Projekt-Zuweisungen } type Location struct { @@ -20,10 +21,19 @@ type Project struct { } type Stock struct { - ID int `json:"id"` - ItemID int `json:"item_id"` - LocationID int `json:"location_id"` - Quantity int `json:"quantity"` + ID int `json:"id"` + ItemID int `json:"item_id"` + LocationID int `json:"location_id"` + Quantity int `json:"quantity"` + LocationName string `json:"location_name"` // Used to display the location in the modal table +} + +type Association struct { + ID int `json:"id"` + ProjectID int `json:"project_id"` + ItemID int `json:"item_id"` + Quantity int `json:"quantity"` + ItemName string `json:"item_name"` // Used to display the item name in the modal table } type ProjectItem struct {