lots
This commit is contained in:
@@ -31,6 +31,7 @@
|
||||
<a href="/" class="px-3 py-2 rounded-md text-sm font-medium text-gray-300 hover:text-white hover:bg-gray-700">Orders</a>
|
||||
<a href="/manabox.html" class="px-3 py-2 rounded-md text-sm font-medium text-gray-300 hover:text-white hover:bg-gray-700">Manabox</a>
|
||||
<a href="/transactions.html" class="px-3 py-2 rounded-md text-sm font-medium text-gray-300 hover:text-white hover:bg-gray-700">Transactions</a>
|
||||
<a href="/inventory_labels.html" class="px-3 py-2 rounded-md text-sm font-medium text-gray-300 hover:text-white hover:bg-gray-700">Inventory Labels</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
118
app/static/inventory_labels.html
Normal file
118
app/static/inventory_labels.html
Normal file
@@ -0,0 +1,118 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Inventory Label Creator</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'sans-serif'],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-gray-900 min-h-screen text-gray-100">
|
||||
<!-- Navigation Menu -->
|
||||
<nav class="bg-gray-800 shadow-sm">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex items-center justify-between h-16">
|
||||
<div class="flex items-center">
|
||||
<a href="/" class="text-xl font-bold text-gray-100">TCGPlayer Manager</a>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<a href="/" class="px-3 py-2 rounded-md text-sm font-medium text-gray-300 hover:text-white hover:bg-gray-700">Orders</a>
|
||||
<a href="/manabox.html" class="px-3 py-2 rounded-md text-sm font-medium text-gray-300 hover:text-white hover:bg-gray-700">Manabox</a>
|
||||
<a href="/transactions.html" class="px-3 py-2 rounded-md text-sm font-medium text-gray-300 hover:text-white hover:bg-gray-700">Transactions</a>
|
||||
<a href="/inventory_labels.html" class="px-3 py-2 rounded-md text-sm font-medium text-white bg-blue-600 rounded-lg">Inventory Labels</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="bg-gray-800 rounded-xl shadow-sm p-6 mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-100 mb-2">Inventory Label Creator</h1>
|
||||
<p class="text-gray-400">Create QR code labels for inventory items with optional UPC codes and metadata</p>
|
||||
</div>
|
||||
|
||||
<!-- Create Label Form -->
|
||||
<div class="bg-gray-800 rounded-xl shadow-sm p-6 mb-8">
|
||||
<h2 class="text-xl font-semibold text-gray-100 mb-6">Create New Label</h2>
|
||||
|
||||
<form id="createLabelForm" class="space-y-6">
|
||||
<!-- UPC Code -->
|
||||
<div>
|
||||
<label for="upc" class="block text-sm font-medium text-gray-300 mb-2">UPC Code (Optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
id="upc"
|
||||
name="upc"
|
||||
placeholder="Enter UPC code..."
|
||||
class="w-full rounded-lg border-gray-600 bg-gray-700 text-gray-100 focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<p class="text-sm text-gray-400 mt-1">Enter a valid UPC-A, UPC-E, or EAN-13 code</p>
|
||||
</div>
|
||||
|
||||
<!-- Metadata Section -->
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<label class="block text-sm font-medium text-gray-300">Metadata (Optional)</label>
|
||||
<button
|
||||
type="button"
|
||||
onclick="addMetadataField()"
|
||||
class="px-3 py-1 bg-blue-600 text-white rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors text-sm"
|
||||
>
|
||||
Add Field
|
||||
</button>
|
||||
</div>
|
||||
<div id="metadataFields" class="space-y-3">
|
||||
<!-- Metadata fields will be added here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Print Option -->
|
||||
<div>
|
||||
<label class="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="printLabel"
|
||||
name="print"
|
||||
checked
|
||||
class="rounded border-gray-600 bg-gray-800 text-blue-600 focus:ring-blue-500"
|
||||
>
|
||||
<span class="text-gray-300">Print label immediately</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
class="px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 transition-colors font-medium"
|
||||
>
|
||||
Create Label
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Recent Labels Section -->
|
||||
<div class="bg-gray-800 rounded-xl shadow-sm p-6">
|
||||
<h2 class="text-xl font-semibold text-gray-100 mb-6">Recent Labels</h2>
|
||||
<div id="recentLabels" class="space-y-4">
|
||||
<!-- Recent labels will be displayed here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/inventory_labels.js"></script>
|
||||
</body>
|
||||
</html>
|
270
app/static/inventory_labels.js
Normal file
270
app/static/inventory_labels.js
Normal file
@@ -0,0 +1,270 @@
|
||||
// API base URL
|
||||
const API_BASE_URL = '/api';
|
||||
|
||||
// Show toast notification
|
||||
function showToast(message, type = 'success') {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `fixed bottom-4 right-4 px-6 py-3 rounded-lg shadow-lg text-white ${
|
||||
type === 'success' ? 'bg-green-600' : 'bg-red-600'
|
||||
} transform translate-y-0 opacity-100 transition-all duration-300 z-50`;
|
||||
toast.textContent = message;
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.style.transform = 'translateY(100%)';
|
||||
toast.style.opacity = '0';
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
function setLoading(isLoading) {
|
||||
const submitButton = document.querySelector('#createLabelForm button[type="submit"]');
|
||||
if (isLoading) {
|
||||
submitButton.disabled = true;
|
||||
submitButton.classList.add('opacity-50', 'cursor-not-allowed');
|
||||
submitButton.textContent = 'Creating...';
|
||||
} else {
|
||||
submitButton.disabled = false;
|
||||
submitButton.classList.remove('opacity-50', 'cursor-not-allowed');
|
||||
submitButton.textContent = 'Create Label';
|
||||
}
|
||||
}
|
||||
|
||||
// Add metadata field
|
||||
function addMetadataField() {
|
||||
const metadataFields = document.getElementById('metadataFields');
|
||||
const fieldId = Date.now(); // Simple unique ID
|
||||
|
||||
const fieldDiv = document.createElement('div');
|
||||
fieldDiv.className = 'flex space-x-3 items-end';
|
||||
fieldDiv.innerHTML = `
|
||||
<div class="flex-1">
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Key</label>
|
||||
<input
|
||||
type="text"
|
||||
name="metadata_key_${fieldId}"
|
||||
placeholder="e.g., product_name, condition, location..."
|
||||
class="w-full rounded-lg border-gray-600 bg-gray-700 text-gray-100 focus:ring-blue-500 focus:border-blue-500"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Value</label>
|
||||
<input
|
||||
type="text"
|
||||
name="metadata_value_${fieldId}"
|
||||
placeholder="e.g., Lightning Bolt, NM, Shelf A1..."
|
||||
class="w-full rounded-lg border-gray-600 bg-gray-700 text-gray-100 focus:ring-blue-500 focus:border-blue-500"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onclick="removeMetadataField(this)"
|
||||
class="px-3 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 transition-colors"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
`;
|
||||
|
||||
metadataFields.appendChild(fieldDiv);
|
||||
}
|
||||
|
||||
// Remove metadata field
|
||||
function removeMetadataField(button) {
|
||||
button.closest('div').remove();
|
||||
}
|
||||
|
||||
// Validate UPC code
|
||||
function validateUPC(upc) {
|
||||
if (!upc) return true; // Empty UPC is valid (optional field)
|
||||
|
||||
// Remove any non-digit characters
|
||||
const digitsOnly = upc.replace(/[^0-9]/g, '');
|
||||
|
||||
// Check for valid lengths
|
||||
if (digitsOnly.length === 12) {
|
||||
return validateUPCAChecksum(digitsOnly);
|
||||
} else if (digitsOnly.length === 8) {
|
||||
return validateUPCChecksum(digitsOnly);
|
||||
} else if (digitsOnly.length === 13) {
|
||||
return validateEAN13Checksum(digitsOnly);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate UPC-A checksum
|
||||
function validateUPCAChecksum(upc) {
|
||||
if (upc.length !== 12 || !/^\d+$/.test(upc)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let total = 0;
|
||||
for (let i = 0; i < 11; i++) {
|
||||
const digit = parseInt(upc[i]);
|
||||
if (i % 2 === 0) { // Odd positions (0-indexed)
|
||||
total += digit * 3;
|
||||
} else { // Even positions
|
||||
total += digit;
|
||||
}
|
||||
}
|
||||
|
||||
const checksum = (10 - (total % 10)) % 10;
|
||||
return checksum === parseInt(upc[11]);
|
||||
}
|
||||
|
||||
// Validate UPC-E checksum
|
||||
function validateUPCChecksum(upc) {
|
||||
if (upc.length !== 8 || !/^\d+$/.test(upc)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let total = 0;
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const digit = parseInt(upc[i]);
|
||||
if (i % 2 === 0) { // Odd positions (0-indexed)
|
||||
total += digit * 3;
|
||||
} else { // Even positions
|
||||
total += digit;
|
||||
}
|
||||
}
|
||||
|
||||
const checksum = (10 - (total % 10)) % 10;
|
||||
return checksum === parseInt(upc[7]);
|
||||
}
|
||||
|
||||
// Validate EAN-13 checksum
|
||||
function validateEAN13Checksum(ean) {
|
||||
if (ean.length !== 13 || !/^\d+$/.test(ean)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let total = 0;
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const digit = parseInt(ean[i]);
|
||||
if (i % 2 === 0) { // Even positions (0-indexed)
|
||||
total += digit;
|
||||
} else { // Odd positions
|
||||
total += digit * 3;
|
||||
}
|
||||
}
|
||||
|
||||
const checksum = (10 - (total % 10)) % 10;
|
||||
return checksum === parseInt(ean[12]);
|
||||
}
|
||||
|
||||
// Collect form data
|
||||
function collectFormData() {
|
||||
const upc = document.getElementById('upc').value.trim();
|
||||
const print = document.getElementById('printLabel').checked;
|
||||
|
||||
// Collect metadata
|
||||
const metadata = [];
|
||||
const metadataFields = document.querySelectorAll('#metadataFields input[type="text"]');
|
||||
|
||||
for (let i = 0; i < metadataFields.length; i += 2) {
|
||||
const keyInput = metadataFields[i];
|
||||
const valueInput = metadataFields[i + 1];
|
||||
|
||||
if (keyInput && valueInput && keyInput.value.trim() && valueInput.value.trim()) {
|
||||
metadata.push({
|
||||
key: keyInput.value.trim(),
|
||||
value: valueInput.value.trim()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
upc: upc || null,
|
||||
metadata: metadata.length > 0 ? metadata : null,
|
||||
print: print
|
||||
};
|
||||
}
|
||||
|
||||
// Create inventory label
|
||||
async function createInventoryLabel(formData) {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/inventory-labels/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.detail || 'Failed to create inventory label');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
showToast('Inventory label created successfully!', 'success');
|
||||
|
||||
// Reset form
|
||||
document.getElementById('createLabelForm').reset();
|
||||
document.getElementById('metadataFields').innerHTML = '';
|
||||
|
||||
// Optionally refresh recent labels
|
||||
// loadRecentLabels();
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
showToast('Error creating inventory label: ' + error.message, 'error');
|
||||
throw error;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle form submission
|
||||
async function handleFormSubmit(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = collectFormData();
|
||||
|
||||
// Validate UPC if provided
|
||||
if (formData.upc && !validateUPC(formData.upc)) {
|
||||
showToast('Invalid UPC code. Please enter a valid UPC-A, UPC-E, or EAN-13 code.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate metadata if provided
|
||||
if (formData.metadata) {
|
||||
for (const item of formData.metadata) {
|
||||
if (!item.key || !item.value) {
|
||||
showToast('All metadata fields must have both key and value.', 'error');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await createInventoryLabel(formData);
|
||||
} catch (error) {
|
||||
console.error('Error creating inventory label:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Load recent labels (placeholder for future implementation)
|
||||
async function loadRecentLabels() {
|
||||
// This could be implemented to show recently created labels
|
||||
// For now, it's a placeholder
|
||||
const recentLabelsDiv = document.getElementById('recentLabels');
|
||||
recentLabelsDiv.innerHTML = '<p class="text-gray-400 text-center">No recent labels to display</p>';
|
||||
}
|
||||
|
||||
// Initialize page
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Set up form submission handler
|
||||
document.getElementById('createLabelForm').addEventListener('submit', handleFormSubmit);
|
||||
|
||||
// Load recent labels
|
||||
loadRecentLabels();
|
||||
|
||||
// Add initial metadata field
|
||||
addMetadataField();
|
||||
});
|
@@ -673,7 +673,7 @@ async function showOpenEventResultingItems(inventoryItemId, openEventId) {
|
||||
<tr>
|
||||
<td class="px-6 py-4 text-sm text-gray-300">${item.product.name}</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-300">${item.product.group_name}</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-300">$${item.product.market_price.toFixed(2)}</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-300">${item.product.market_price !== null ? `$${item.product.market_price.toFixed(2)}` : 'N/A'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<button onclick="showInventoryItemDetails(${item.id})" class="px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors">
|
||||
View Details
|
||||
|
Reference in New Issue
Block a user