inventory management add/del

This commit is contained in:
2025-08-18 11:28:55 +07:00
parent 664f58cefe
commit ca4532fa39
4 changed files with 948 additions and 13 deletions

View File

@@ -11,6 +11,19 @@
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="/">📚 Media Inventory</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link" href="/">🔍 Search</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/inventory">📦 Inventory</a>
</li>
</ul>
</div>
</div>
</nav>

281
templates/inventory.html Normal file
View File

@@ -0,0 +1,281 @@
{% extends "base.html" %}
{% block title %}Inventory Management - Media Inventory App{% endblock %}
{% block content %}
<div class="row">
<div class="col-12">
<h2>📦 Inventory Management</h2>
<p class="text-muted">Manage your InvenTree storage locations and inventory items</p>
{% if error %}
<div class="alert alert-danger" role="alert">
<h5>⚠️ Configuration Error</h5>
<p>{{ error }}</p>
<hr>
<p class="mb-0">
<strong>To configure InvenTree integration:</strong><br>
1. Set up your InvenTree instance<br>
2. Add your API credentials to the .env file<br>
3. Restart the application
</p>
</div>
{% else %}
<!-- Storage Locations Section -->
<div class="row mb-4">
<div class="col-md-8">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">📍 Storage Locations</h5>
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#createLocationModal">
Add Location
</button>
</div>
<div class="card-body">
{% if locations %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Path</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for location in locations %}
<tr id="location-row-{{ location.pk }}">
<td>{{ location.pk }}</td>
<td>{{ location.name }}</td>
<td><small class="text-muted">{{ location.pathstring }}</small></td>
<td>
<button class="btn btn-outline-danger btn-sm"
onclick="deleteLocation({{ location.pk }}, '{{ location.name }}')"
title="Delete empty location">
🗑️ Delete
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-4">
<p class="text-muted">No storage locations found</p>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createLocationModal">
Create Your First Location
</button>
</div>
{% endif %}
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0"> How to Use</h5>
</div>
<div class="card-body">
<ol class="small">
<li><strong>Create Locations:</strong> Set up storage locations for your media</li>
<li><strong>Search Media:</strong> Use the search function to find items</li>
<li><strong>Add to Inventory:</strong> Click "Add to Inventory" on search results</li>
<li><strong>Delete Empty Locations:</strong> Remove unused locations with the delete button</li>
<li><strong>Manage:</strong> Use InvenTree web interface for advanced management</li>
</ol>
<hr>
<p class="small text-muted mb-0">
<strong>Connected to:</strong><br>
InvenTree API ✅
</p>
</div>
</div>
</div>
</div>
{% endif %}
</div>
</div>
<!-- Create Location Modal -->
<div class="modal fade" id="createLocationModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"> Create Storage Location</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="createLocationForm">
<div class="mb-3">
<label for="locationName" class="form-label">Location Name *</label>
<input type="text" class="form-control" id="locationName" name="name" required
placeholder="e.g., Living Room Shelf, Bedroom Closet">
</div>
<div class="mb-3">
<label for="locationDescription" class="form-label">Description</label>
<textarea class="form-control" id="locationDescription" name="description" rows="3"
placeholder="Optional description of this storage location"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="createLocation()">Create Location</button>
</div>
</div>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div class="modal fade" id="deleteLocationModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">🗑️ Delete Storage Location</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="alert alert-warning">
<strong>⚠️ Warning:</strong> This action cannot be undone.
</div>
<p>Are you sure you want to delete the storage location <strong id="deleteLocationName"></strong>?</p>
<p class="text-muted small">
<strong>Note:</strong> You can only delete empty locations. If this location contains any stock items,
you'll need to move or remove them first.
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" id="confirmDeleteBtn">Delete Location</button>
</div>
</div>
</div>
</div>
<script>
async function createLocation() {
const form = document.getElementById('createLocationForm');
const formData = new FormData(form);
const data = Object.fromEntries(formData);
try {
const response = await fetch('/api/storage-locations', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
// Close modal and reload page
const modal = bootstrap.Modal.getInstance(document.getElementById('createLocationModal'));
modal.hide();
location.reload();
} else {
alert('Error creating location: ' + (result.error || 'Unknown error'));
}
} catch (error) {
alert('Error creating location: ' + error.message);
}
}
let locationToDelete = null;
function deleteLocation(locationId, locationName) {
// Store the location info for deletion
locationToDelete = { id: locationId, name: locationName };
// Update modal content
document.getElementById('deleteLocationName').textContent = locationName;
// Show the modal
const modal = new bootstrap.Modal(document.getElementById('deleteLocationModal'));
modal.show();
}
// Handle the actual deletion when confirmed
document.addEventListener('DOMContentLoaded', function() {
const confirmDeleteBtn = document.getElementById('confirmDeleteBtn');
if (confirmDeleteBtn) {
confirmDeleteBtn.addEventListener('click', async function() {
if (!locationToDelete) return;
// Disable button and show loading
confirmDeleteBtn.disabled = true;
confirmDeleteBtn.textContent = 'Deleting...';
try {
const response = await fetch(`/api/storage-locations/${locationToDelete.id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
}
});
const result = await response.json();
// Close modal
const modal = bootstrap.Modal.getInstance(document.getElementById('deleteLocationModal'));
modal.hide();
if (result.success) {
// Remove the row from the table
const row = document.getElementById(`location-row-${locationToDelete.id}`);
if (row) {
row.remove();
}
// Show success message
showAlert('success', result.message || 'Storage location deleted successfully');
} else {
// Show error message
showAlert('danger', result.message || result.error || 'Failed to delete location');
}
} catch (error) {
// Close modal
const modal = bootstrap.Modal.getInstance(document.getElementById('deleteLocationModal'));
modal.hide();
showAlert('danger', 'Error deleting location: ' + error.message);
} finally {
// Reset button
confirmDeleteBtn.disabled = false;
confirmDeleteBtn.textContent = 'Delete Location';
locationToDelete = null;
}
});
}
});
function showAlert(type, message) {
// Create alert element
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
// Insert at the top of the content
const container = document.querySelector('.container-fluid, .container');
if (container) {
container.insertBefore(alertDiv, container.firstChild);
// Auto-dismiss after 5 seconds
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.remove();
}
}, 5000);
}
}
</script>
{% endblock %}

View File

@@ -55,17 +55,25 @@
{% endif %}
<div class="mt-auto">
{% if media_type == 'book' and item.openlibrary_url %}
<a href="{{ item.openlibrary_url }}" target="_blank"
class="btn btn-primary btn-sm">
📚 View on OpenLibrary
</a>
{% elif media_type != 'book' and item.discogs_url %}
<a href="{{ item.discogs_url }}" target="_blank"
class="btn btn-primary btn-sm">
🎵 View on Discogs
</a>
{% endif %}
<div class="d-flex gap-2 flex-wrap">
{% if media_type == 'book' and item.openlibrary_url %}
<a href="{{ item.openlibrary_url }}" target="_blank"
class="btn btn-outline-primary btn-sm">
📚 View on OpenLibrary
</a>
{% elif media_type != 'book' and item.discogs_url %}
<a href="{{ item.discogs_url }}" target="_blank"
class="btn btn-outline-primary btn-sm">
🎵 View on Discogs
</a>
{% endif %}
<button class="btn btn-success btn-sm"
onclick="addToInventory({{ loop.index0 }})"
data-item="{{ item | tojson | e }}">
📦 Add to Inventory
</button>
</div>
</div>
</div>
</div>
@@ -81,4 +89,203 @@
{% endif %}
</div>
</div>
<!-- Add to Inventory Modal -->
<div class="modal fade" id="addToInventoryModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">📦 Add to Inventory</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div id="inventoryItemDetails" class="mb-3">
<!-- Item details will be populated here -->
</div>
<form id="addToInventoryForm">
<div class="mb-3">
<label for="storageLocation" class="form-label">Storage Location *</label>
<select class="form-select" id="storageLocation" name="storage_location_id" required>
<option value="">Loading locations...</option>
</select>
<div class="form-text">
<a href="/inventory" target="_blank">Manage storage locations</a>
</div>
</div>
<div class="mb-3">
<label for="quantity" class="form-label">Quantity</label>
<input type="number" class="form-control" id="quantity" name="quantity"
value="1" min="1" step="1">
</div>
<div class="mb-3">
<label for="notes" class="form-label">Notes</label>
<textarea class="form-control" id="notes" name="notes" rows="2"
placeholder="Optional notes about this item"></textarea>
</div>
</form>
<div id="inventoryError" class="alert alert-danger d-none"></div>
<div id="inventorySuccess" class="alert alert-success d-none"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-success" onclick="submitToInventory()" id="submitInventoryBtn">
📦 Add to Inventory
</button>
</div>
</div>
</div>
</div>
<script>
let currentMediaItem = null;
let storageLocations = [];
// Load storage locations when page loads
document.addEventListener('DOMContentLoaded', function() {
loadStorageLocations();
});
async function loadStorageLocations() {
try {
const response = await fetch('/api/storage-locations');
const data = await response.json();
if (data.error) {
console.error('Error loading storage locations:', data.error);
return;
}
storageLocations = data;
updateLocationDropdown();
} catch (error) {
console.error('Error loading storage locations:', error);
}
}
function updateLocationDropdown() {
const select = document.getElementById('storageLocation');
select.innerHTML = '';
if (storageLocations.length === 0) {
select.innerHTML = '<option value="">No storage locations found</option>';
return;
}
select.innerHTML = '<option value="">Select a storage location...</option>';
storageLocations.forEach(location => {
const option = document.createElement('option');
option.value = location.pk;
option.textContent = location.pathstring || location.name;
select.appendChild(option);
});
}
function addToInventory(itemIndex) {
// Get the item data from the button's data attribute
const buttons = document.querySelectorAll('[data-item]');
const button = buttons[itemIndex];
const itemData = JSON.parse(button.getAttribute('data-item'));
currentMediaItem = itemData;
// Populate item details
const detailsDiv = document.getElementById('inventoryItemDetails');
detailsDiv.innerHTML = `
<div class="card">
<div class="card-body">
<h6 class="card-title">${itemData.title}</h6>
<p class="card-text small">
${itemData.artist ? `<strong>Artist:</strong> ${itemData.artist}<br>` : ''}
${itemData.author ? `<strong>Author:</strong> ${itemData.author}<br>` : ''}
${itemData.year ? `<strong>Year:</strong> ${itemData.year}<br>` : ''}
${itemData.format ? `<strong>Format:</strong> ${itemData.format}` : ''}
</p>
</div>
</div>
`;
// Clear previous messages
document.getElementById('inventoryError').classList.add('d-none');
document.getElementById('inventorySuccess').classList.add('d-none');
// Reset form
document.getElementById('addToInventoryForm').reset();
document.getElementById('quantity').value = '1';
// Show modal
const modal = new bootstrap.Modal(document.getElementById('addToInventoryModal'));
modal.show();
}
async function submitToInventory() {
const form = document.getElementById('addToInventoryForm');
const formData = new FormData(form);
const submitData = {
media_item: currentMediaItem,
storage_location_id: parseInt(formData.get('storage_location_id')),
quantity: parseFloat(formData.get('quantity')),
notes: formData.get('notes') || null
};
// Validate required fields
if (!submitData.storage_location_id) {
showError('Please select a storage location');
return;
}
// Disable submit button
const submitBtn = document.getElementById('submitInventoryBtn');
const originalText = submitBtn.textContent;
submitBtn.disabled = true;
submitBtn.textContent = 'Adding...';
try {
const response = await fetch('/api/create-inventory-item', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(submitData)
});
const result = await response.json();
if (result.success) {
showSuccess(result.message);
// Close modal after 2 seconds
setTimeout(() => {
const modal = bootstrap.Modal.getInstance(document.getElementById('addToInventoryModal'));
modal.hide();
}, 2000);
} else {
showError(result.message || result.error || 'Failed to add item to inventory');
}
} catch (error) {
showError('Error adding item to inventory: ' + error.message);
} finally {
// Re-enable submit button
submitBtn.disabled = false;
submitBtn.textContent = originalText;
}
}
function showError(message) {
const errorDiv = document.getElementById('inventoryError');
errorDiv.textContent = message;
errorDiv.classList.remove('d-none');
document.getElementById('inventorySuccess').classList.add('d-none');
}
function showSuccess(message) {
const successDiv = document.getElementById('inventorySuccess');
successDiv.textContent = message;
successDiv.classList.remove('d-none');
document.getElementById('inventoryError').classList.add('d-none');
}
</script>
{% endblock %}