1077 lines
49 KiB
JavaScript
1077 lines
49 KiB
JavaScript
document.addEventListener('DOMContentLoaded', function() {
|
|
// Event Listeners
|
|
document.getElementById('createTransactionBtn').addEventListener('click', () => {
|
|
showTransactionModal();
|
|
});
|
|
|
|
document.getElementById('addVendorBtn').addEventListener('click', () => {
|
|
const vendorName = prompt('Enter vendor name:');
|
|
if (vendorName) {
|
|
createVendor(vendorName);
|
|
}
|
|
});
|
|
|
|
document.getElementById('addMarketplaceBtn').addEventListener('click', () => {
|
|
const marketplaceName = prompt('Enter marketplace name:');
|
|
if (marketplaceName) {
|
|
createMarketplace(marketplaceName);
|
|
}
|
|
});
|
|
|
|
document.getElementById('addItemBtn').addEventListener('click', addItem);
|
|
|
|
document.getElementById('transactionType').addEventListener('change', (e) => {
|
|
const marketplaceSection = document.getElementById('marketplaceSection');
|
|
if (e.target.value === 'sale') {
|
|
marketplaceSection.classList.remove('hidden');
|
|
} else {
|
|
marketplaceSection.classList.add('hidden');
|
|
}
|
|
});
|
|
|
|
document.getElementById('saveTransactionBtn').addEventListener('click', saveTransaction);
|
|
|
|
// Load initial data
|
|
loadVendors();
|
|
loadMarketplaces();
|
|
loadTransactions();
|
|
addItem(); // Add first item by default
|
|
});
|
|
|
|
// Modal Functions
|
|
function showTransactionModal() {
|
|
document.getElementById('createTransactionModal').classList.remove('hidden');
|
|
}
|
|
|
|
function closeTransactionModal() {
|
|
document.getElementById('createTransactionModal').classList.add('hidden');
|
|
}
|
|
|
|
// Item Management Functions
|
|
function addItem() {
|
|
const itemsContainer = document.getElementById('itemsContainer');
|
|
const itemIndex = itemsContainer.children.length;
|
|
|
|
const itemDiv = document.createElement('div');
|
|
itemDiv.className = 'item border border-gray-700 rounded-lg p-4';
|
|
itemDiv.innerHTML = `
|
|
<div class="flex items-center justify-between mb-2">
|
|
<h6 class="text-md font-medium text-gray-100">Item ${itemIndex + 1}</h6>
|
|
${itemIndex > 0 ? '<button class="remove-item-btn px-2 py-1 text-sm bg-red-600 text-white rounded hover:bg-red-700">Remove</button>' : ''}
|
|
</div>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div class="relative">
|
|
<label class="block text-sm font-medium text-gray-300 mb-2">Product</label>
|
|
<div class="flex items-center">
|
|
<input type="text" class="product-search w-full rounded-lg border-gray-600 bg-gray-700 text-gray-100 focus:ring-blue-500 focus:border-blue-500" required>
|
|
<button class="set-ev-btn ml-2 p-2 bg-red-600 text-white rounded-lg hover:bg-red-700 hidden">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<div class="product-suggestions absolute w-full bg-gray-700 rounded-lg shadow-lg hidden"></div>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-300 mb-2">Item Type</label>
|
|
<select class="item-type w-full rounded-lg border-gray-600 bg-gray-700 text-gray-100 focus:ring-blue-500 focus:border-blue-500" required>
|
|
<option value="box" selected>Box</option>
|
|
<option value="case">Case</option>
|
|
<option value="card">Card</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-300 mb-2">Unit Cost</label>
|
|
<input type="number" step="0.01" min="0" class="unit-cost w-full rounded-lg border-gray-600 bg-gray-700 text-gray-100 focus:ring-blue-500 focus:border-blue-500" required>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-300 mb-2">Quantity</label>
|
|
<input type="number" min="1" class="quantity w-full rounded-lg border-gray-600 bg-gray-700 text-gray-100 focus:ring-blue-500 focus:border-blue-500" required>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
itemsContainer.appendChild(itemDiv);
|
|
|
|
// Add event listeners for the new item
|
|
const productSearch = itemDiv.querySelector('.product-search');
|
|
const setEvBtn = itemDiv.querySelector('.set-ev-btn');
|
|
const suggestionsDiv = itemDiv.querySelector('.product-suggestions');
|
|
|
|
// Product search functionality
|
|
let searchTimeout;
|
|
productSearch.addEventListener('input', (e) => {
|
|
clearTimeout(searchTimeout);
|
|
const searchTerm = e.target.value;
|
|
|
|
if (searchTerm.length < 2) {
|
|
suggestionsDiv.classList.add('hidden');
|
|
return;
|
|
}
|
|
|
|
searchTimeout = setTimeout(async () => {
|
|
try {
|
|
const response = await fetch(`/api/inventory/products/search?q=${encodeURIComponent(searchTerm)}`);
|
|
const products = await response.json();
|
|
|
|
suggestionsDiv.innerHTML = products.map(product => `
|
|
<div class="p-2 hover:bg-gray-600 cursor-pointer" data-tcgplayer_product_id="${product.tcgplayer_product_id}">
|
|
${product.name}
|
|
</div>
|
|
`).join('');
|
|
|
|
suggestionsDiv.classList.remove('hidden');
|
|
} catch (error) {
|
|
console.error('Error searching products:', error);
|
|
}
|
|
}, 300);
|
|
});
|
|
|
|
// Handle suggestion selection
|
|
suggestionsDiv.addEventListener('click', async (e) => {
|
|
const selectedProduct = e.target.closest('[data-tcgplayer_product_id]');
|
|
if (selectedProduct) {
|
|
const productId = selectedProduct.dataset.tcgplayer_product_id;
|
|
productSearch.value = selectedProduct.textContent.trim();
|
|
productSearch.dataset.tcgplayer_product_id = productId;
|
|
suggestionsDiv.classList.add('hidden');
|
|
|
|
// Check expected value
|
|
try {
|
|
const response = await fetch(`/api/inventory/products/${productId}/expected-value`);
|
|
const expectedValue = await response.json();
|
|
|
|
if (expectedValue === null || expectedValue === undefined || isNaN(expectedValue)) {
|
|
setEvBtn.classList.remove('hidden');
|
|
} else {
|
|
setEvBtn.classList.add('hidden');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error checking expected value:', error);
|
|
setEvBtn.classList.remove('hidden');
|
|
}
|
|
}
|
|
});
|
|
|
|
// Handle expected value setting
|
|
setEvBtn.addEventListener('click', async () => {
|
|
const expectedValue = prompt('Enter expected value for this product:');
|
|
if (expectedValue && !isNaN(expectedValue)) {
|
|
try {
|
|
const response = await fetch('/api/inventory/products/expected-value', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
tcgplayer_product_id: parseInt(productSearch.dataset.tcgplayer_product_id),
|
|
expected_value: parseFloat(expectedValue)
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
setEvBtn.classList.add('hidden');
|
|
} else {
|
|
const error = await response.json();
|
|
alert(`Failed to set expected value: ${error.detail}`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error setting expected value:', error);
|
|
alert('Failed to set expected value. Please try again.');
|
|
}
|
|
}
|
|
});
|
|
|
|
// Handle item removal
|
|
const removeBtn = itemDiv.querySelector('.remove-item-btn');
|
|
if (removeBtn) {
|
|
removeBtn.addEventListener('click', () => {
|
|
itemDiv.remove();
|
|
});
|
|
}
|
|
}
|
|
|
|
// API Functions
|
|
async function loadVendors() {
|
|
try {
|
|
const response = await fetch('/api/inventory/vendors');
|
|
const vendors = await response.json();
|
|
const vendorSelect = document.getElementById('vendorSelect');
|
|
|
|
// Clear existing options except the first one
|
|
while (vendorSelect.options.length > 1) {
|
|
vendorSelect.remove(1);
|
|
}
|
|
|
|
// Add new options
|
|
vendors.forEach(vendor => {
|
|
const option = new Option(vendor.name, vendor.id);
|
|
vendorSelect.add(option);
|
|
});
|
|
} catch (error) {
|
|
console.error('Error loading vendors:', error);
|
|
}
|
|
}
|
|
|
|
async function loadMarketplaces() {
|
|
try {
|
|
const response = await fetch('/api/inventory/marketplaces');
|
|
const marketplaces = await response.json();
|
|
const marketplaceSelect = document.getElementById('marketplaceSelect');
|
|
|
|
// Clear existing options except the first one
|
|
while (marketplaceSelect.options.length > 1) {
|
|
marketplaceSelect.remove(1);
|
|
}
|
|
|
|
// Add new options
|
|
marketplaces.forEach(marketplace => {
|
|
const option = new Option(marketplace.name, marketplace.id);
|
|
marketplaceSelect.add(option);
|
|
});
|
|
} catch (error) {
|
|
console.error('Error loading marketplaces:', error);
|
|
}
|
|
}
|
|
|
|
async function createVendor(name) {
|
|
try {
|
|
const response = await fetch('/api/inventory/vendors', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ vendor_name: name })
|
|
});
|
|
|
|
if (response.ok) {
|
|
const newVendor = await response.json();
|
|
// Add new vendor to select and select it
|
|
const vendorSelect = document.getElementById('vendorSelect');
|
|
const option = new Option(newVendor.name, newVendor.id);
|
|
vendorSelect.add(option);
|
|
vendorSelect.value = newVendor.id;
|
|
} else {
|
|
throw new Error('Failed to create vendor');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error creating vendor:', error);
|
|
alert('Failed to create vendor. Please try again.');
|
|
}
|
|
}
|
|
|
|
async function createMarketplace(name) {
|
|
try {
|
|
const response = await fetch('/api/inventory/marketplaces', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ marketplace_name: name })
|
|
});
|
|
|
|
if (response.ok) {
|
|
const newMarketplace = await response.json();
|
|
// Add new marketplace to select and select it
|
|
const marketplaceSelect = document.getElementById('marketplaceSelect');
|
|
const option = new Option(newMarketplace.name, newMarketplace.id);
|
|
marketplaceSelect.add(option);
|
|
marketplaceSelect.value = newMarketplace.id;
|
|
} else {
|
|
throw new Error('Failed to create marketplace');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error creating marketplace:', error);
|
|
alert('Failed to create marketplace. Please try again.');
|
|
}
|
|
}
|
|
|
|
// Transaction Management Functions
|
|
let currentPage = 1;
|
|
let currentLimit = 25;
|
|
let totalTransactions = 0;
|
|
|
|
// Manabox Files Pagination
|
|
let currentManaboxPage = 1;
|
|
let currentManaboxLimit = 5;
|
|
let totalManaboxFiles = 0;
|
|
|
|
// DOM Elements
|
|
const transactionsBody = document.getElementById('transactionsBody');
|
|
const prevPageBtn = document.getElementById('prevPageBtn');
|
|
const nextPageBtn = document.getElementById('nextPageBtn');
|
|
const pageInfo = document.getElementById('pageInfo');
|
|
const limitSelect = document.getElementById('limitSelect');
|
|
const transactionDetailsModal = document.getElementById('transactionDetailsModal');
|
|
const transactionDetails = document.getElementById('transactionDetails');
|
|
|
|
// Event Listeners
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadTransactions();
|
|
|
|
limitSelect.addEventListener('change', (e) => {
|
|
currentLimit = parseInt(e.target.value);
|
|
currentPage = 1;
|
|
loadTransactions();
|
|
});
|
|
|
|
prevPageBtn.addEventListener('click', () => {
|
|
if (currentPage > 1) {
|
|
currentPage--;
|
|
loadTransactions();
|
|
}
|
|
});
|
|
|
|
nextPageBtn.addEventListener('click', () => {
|
|
if (currentPage * currentLimit < totalTransactions) {
|
|
currentPage++;
|
|
loadTransactions();
|
|
}
|
|
});
|
|
});
|
|
|
|
// Load transactions with pagination
|
|
async function loadTransactions() {
|
|
try {
|
|
const skip = (currentPage - 1) * currentLimit;
|
|
const response = await fetch(`/api/inventory/transactions?skip=${skip}&limit=${currentLimit}`);
|
|
const data = await response.json();
|
|
|
|
totalTransactions = data.total;
|
|
renderTransactions(data.transactions);
|
|
updatePaginationControls();
|
|
} catch (error) {
|
|
console.error('Error loading transactions:', error);
|
|
}
|
|
}
|
|
|
|
// Render transactions in the table
|
|
function renderTransactions(transactions) {
|
|
transactionsBody.innerHTML = '';
|
|
|
|
transactions.forEach(transaction => {
|
|
const row = document.createElement('tr');
|
|
row.className = 'hover:bg-gray-700 cursor-pointer';
|
|
row.onclick = () => showTransactionDetails(transaction);
|
|
|
|
row.innerHTML = `
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">${formatDate(transaction.transaction_date)}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">${transaction.transaction_type}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">${getPartyName(transaction)}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">$${transaction.transaction_total_amount.toFixed(2)}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">${transaction.transaction_notes || ''}</td>
|
|
`;
|
|
|
|
transactionsBody.appendChild(row);
|
|
});
|
|
}
|
|
|
|
// Update pagination controls
|
|
function updatePaginationControls() {
|
|
const totalPages = Math.ceil(totalTransactions / currentLimit);
|
|
|
|
prevPageBtn.disabled = currentPage === 1;
|
|
nextPageBtn.disabled = currentPage === totalPages;
|
|
|
|
pageInfo.textContent = `Page ${currentPage} of ${totalPages}`;
|
|
}
|
|
|
|
// Show transaction details modal
|
|
async function showTransactionDetails(transaction) {
|
|
try {
|
|
const response = await fetch(`/api/inventory/transactions/${transaction.id}`);
|
|
const transactionData = await response.json();
|
|
|
|
transactionDetails.innerHTML = `
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<h4 class="text-sm font-medium text-gray-400">Date</h4>
|
|
<p class="text-gray-100">${formatDate(transactionData.transaction_date)}</p>
|
|
</div>
|
|
<div>
|
|
<h4 class="text-sm font-medium text-gray-400">Type</h4>
|
|
<p class="text-gray-100">${transactionData.transaction_type}</p>
|
|
</div>
|
|
<div>
|
|
<h4 class="text-sm font-medium text-gray-400">${transactionData.transaction_type === 'purchase' ? 'Vendor' : 'Customer'}</h4>
|
|
<p class="text-gray-100">${getPartyName(transactionData)}</p>
|
|
</div>
|
|
<div>
|
|
<h4 class="text-sm font-medium text-gray-400">Total Amount</h4>
|
|
<p class="text-gray-100">$${transactionData.transaction_total_amount.toFixed(2)}</p>
|
|
</div>
|
|
<div class="col-span-2">
|
|
<h4 class="text-sm font-medium text-gray-400">Notes</h4>
|
|
<p class="text-gray-100">${transactionData.transaction_notes || 'No notes'}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-6">
|
|
<h4 class="text-lg font-medium text-gray-100 mb-4">Items</h4>
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full divide-y divide-gray-700">
|
|
<thead>
|
|
<tr>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Item ID</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Unit Price</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-gray-700">
|
|
${transactionData.transaction_items.map(item => `
|
|
<tr class="hover:bg-gray-700 cursor-pointer" onclick="showInventoryItemDetails(${item.inventory_item_id})">
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">${item.inventory_item_id}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">$${item.unit_price.toFixed(2)}</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
transactionDetailsModal.classList.remove('hidden');
|
|
} catch (error) {
|
|
console.error('Error loading transaction details:', error);
|
|
}
|
|
}
|
|
|
|
// Close transaction details modal
|
|
function closeTransactionDetailsModal() {
|
|
transactionDetailsModal.classList.add('hidden');
|
|
}
|
|
|
|
// Show inventory item details
|
|
async function showInventoryItemDetails(inventoryItemId) {
|
|
try {
|
|
const response = await fetch(`/api/inventory/items/${inventoryItemId}`);
|
|
const itemData = await response.json();
|
|
|
|
// Check for open events first if it's not a card
|
|
let hasOpenEvents = false;
|
|
if (itemData.item_type !== 'card') {
|
|
const openEventsResponse = await fetch(`/api/inventory/items/${inventoryItemId}/open-events`);
|
|
const openEventsData = await openEventsResponse.json();
|
|
hasOpenEvents = openEventsData.open_events.length > 0;
|
|
}
|
|
|
|
// Create a new modal for inventory item details
|
|
const modal = document.createElement('div');
|
|
modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center';
|
|
modal.innerHTML = `
|
|
<div class="bg-gray-800 rounded-lg p-6 max-w-2xl w-full mx-4">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h3 class="text-xl font-semibold text-gray-100">Inventory Item Details</h3>
|
|
<button onclick="this.closest('.fixed').remove()" class="text-gray-400 hover:text-white">
|
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<div class="space-y-6">
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<h4 class="text-sm font-medium text-gray-400">Item ID</h4>
|
|
<p class="text-gray-100">${itemData.id}</p>
|
|
</div>
|
|
<div>
|
|
<h4 class="text-sm font-medium text-gray-400">Physical Item ID</h4>
|
|
<p class="text-gray-100">${itemData.physical_item_id}</p>
|
|
</div>
|
|
<div>
|
|
<h4 class="text-sm font-medium text-gray-400">Cost Basis</h4>
|
|
<p class="text-gray-100">$${itemData.cost_basis.toFixed(2)}</p>
|
|
</div>
|
|
<div>
|
|
<h4 class="text-sm font-medium text-gray-400">Parent ID</h4>
|
|
<p class="text-gray-100">${itemData.parent_id || 'None'}</p>
|
|
</div>
|
|
<div>
|
|
<h4 class="text-sm font-medium text-gray-400">Created At</h4>
|
|
<p class="text-gray-100">${formatDate(itemData.created_at)}</p>
|
|
</div>
|
|
<div>
|
|
<h4 class="text-sm font-medium text-gray-400">Updated At</h4>
|
|
<p class="text-gray-100">${formatDate(itemData.updated_at)}</p>
|
|
</div>
|
|
${itemData.listed_price || itemData.recommended_price ? `
|
|
<div class="col-span-2">
|
|
<h4 class="text-sm font-medium text-gray-400">Pricing</h4>
|
|
<div class="mt-2 space-y-2">
|
|
${itemData.listed_price ? `
|
|
<div class="flex items-center">
|
|
<span class="text-gray-300 mr-2">Listed Price:</span>
|
|
<span class="text-gray-100">$${itemData.listed_price.toFixed(2)}</span>
|
|
</div>
|
|
` : ''}
|
|
${itemData.recommended_price ? `
|
|
<div class="flex items-center">
|
|
<span class="text-gray-300 mr-2">Recommended Price:</span>
|
|
<span class="text-gray-100">$${itemData.recommended_price.toFixed(2)}</span>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
|
|
<div class="border-t border-gray-700 pt-6">
|
|
<h4 class="text-lg font-medium text-gray-100 mb-4">Product Details</h4>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<h4 class="text-sm font-medium text-gray-400">Product ID</h4>
|
|
<p class="text-gray-100">${itemData.product.id}</p>
|
|
</div>
|
|
<div>
|
|
<h4 class="text-sm font-medium text-gray-400">TCGPlayer Product ID</h4>
|
|
<p class="text-gray-100">${itemData.product.tcgplayer_product_id}</p>
|
|
</div>
|
|
<div class="col-span-2">
|
|
<h4 class="text-sm font-medium text-gray-400">Name</h4>
|
|
<p class="text-gray-100">${itemData.product.name}</p>
|
|
</div>
|
|
<div class="col-span-2">
|
|
<h4 class="text-sm font-medium text-gray-400">Image</h4>
|
|
<img src="${itemData.product.image_url}" alt="${itemData.product.name}" class="mt-2 rounded-lg max-h-40">
|
|
</div>
|
|
<div>
|
|
<h4 class="text-sm font-medium text-gray-400">Category</h4>
|
|
<p class="text-gray-100">${itemData.product.category_name}</p>
|
|
</div>
|
|
<div>
|
|
<h4 class="text-sm font-medium text-gray-400">Group</h4>
|
|
<p class="text-gray-100">${itemData.product.group_name}</p>
|
|
</div>
|
|
<div>
|
|
<h4 class="text-sm font-medium text-gray-400">Market Price</h4>
|
|
<p class="text-gray-100">$${itemData.product.market_price.toFixed(2)}</p>
|
|
</div>
|
|
<div>
|
|
<h4 class="text-sm font-medium text-gray-400">TCGPlayer URL</h4>
|
|
<a href="${itemData.product.url}" target="_blank" class="text-blue-400 hover:text-blue-300">View on TCGPlayer</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
${itemData.product.category_name === 'Magic' && itemData.item_type !== 'card' && !hasOpenEvents ? `
|
|
<div class="border-t border-gray-700 pt-6">
|
|
<h4 class="text-lg font-medium text-gray-100 mb-4">Box Actions</h4>
|
|
<button onclick="showOpenBoxModal(${itemData.id}, ${itemData.physical_item_id})" class="px-4 py-2 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">
|
|
Open Box
|
|
</button>
|
|
</div>
|
|
` : ''}
|
|
|
|
<div class="border-t border-gray-700 pt-6 hidden" id="openEventsSection">
|
|
<h4 class="text-lg font-medium text-gray-100 mb-4">Open Events</h4>
|
|
<div id="openEventsTable" class="overflow-x-auto">
|
|
<!-- Open events will be loaded here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.body.appendChild(modal);
|
|
|
|
// Only load open events if not a card
|
|
if (itemData.item_type !== 'card') {
|
|
await loadOpenEvents(inventoryItemId);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading inventory item details:', error);
|
|
}
|
|
}
|
|
|
|
// Load open events for an inventory item
|
|
async function loadOpenEvents(inventoryItemId) {
|
|
try {
|
|
const response = await fetch(`/api/inventory/items/${inventoryItemId}/open-events`);
|
|
const data = await response.json();
|
|
|
|
const openEventsSection = document.getElementById('openEventsSection');
|
|
const openEventsTable = document.getElementById('openEventsTable');
|
|
if (!openEventsSection || !openEventsTable) return;
|
|
|
|
if (data.open_events.length === 0) {
|
|
openEventsSection.classList.add('hidden');
|
|
return;
|
|
}
|
|
|
|
openEventsSection.classList.remove('hidden');
|
|
openEventsTable.innerHTML = `
|
|
<table class="min-w-full divide-y divide-gray-700">
|
|
<thead>
|
|
<tr>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Date</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Source Item ID</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-gray-700">
|
|
${data.open_events.map(event => `
|
|
<tr class="hover:bg-gray-700 cursor-pointer" onclick="showOpenEventResultingItems(${inventoryItemId}, ${event.id})">
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">${formatDate(event.created_at)}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">${event.source_item_id}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<button 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 Items
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
`;
|
|
} catch (error) {
|
|
console.error('Error loading open events:', error);
|
|
}
|
|
}
|
|
|
|
// Show resulting items from an open event
|
|
async function showOpenEventResultingItems(inventoryItemId, openEventId) {
|
|
try {
|
|
const response = await fetch(`/api/inventory/items/${inventoryItemId}/open-events/${openEventId}/resulting-items`);
|
|
const resultingItems = await response.json();
|
|
|
|
const modal = document.createElement('div');
|
|
modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center';
|
|
modal.innerHTML = `
|
|
<div class="bg-gray-800 rounded-lg p-6 max-w-4xl w-full mx-4 max-h-[90vh] overflow-y-auto">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h3 class="text-xl font-semibold text-gray-100">Resulting Items</h3>
|
|
<div class="flex items-center space-x-4">
|
|
<button onclick="createListingsForOpenEvent(${inventoryItemId}, ${openEventId})"
|
|
class="px-4 py-2 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">
|
|
Create Listings
|
|
</button>
|
|
<button onclick="confirmListingsForOpenEvent(${inventoryItemId}, ${openEventId})"
|
|
class="px-4 py-2 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">
|
|
Confirm Listings
|
|
</button>
|
|
<button onclick="this.closest('.fixed').remove()" class="text-gray-400 hover:text-white">
|
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="space-y-4">
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full divide-y divide-gray-700">
|
|
<thead>
|
|
<tr>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Card Name</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Set</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Market Price</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-gray-700">
|
|
${resultingItems.map(item => `
|
|
<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 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
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.body.appendChild(modal);
|
|
} catch (error) {
|
|
console.error('Error loading resulting items:', error);
|
|
}
|
|
}
|
|
|
|
// Add new function to handle confirming listings
|
|
async function confirmListingsForOpenEvent(inventoryItemId, openEventId) {
|
|
try {
|
|
const response = await fetch(`/api/inventory/items/${inventoryItemId}/open-events/${openEventId}/confirm-listings`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.detail || 'Failed to confirm listings');
|
|
}
|
|
|
|
// Get the filename from the Content-Disposition header
|
|
const contentDisposition = response.headers.get('content-disposition');
|
|
let filename = 'tcgplayer_listings.csv';
|
|
if (contentDisposition) {
|
|
const filenameMatch = contentDisposition.match(/filename=(.+)/);
|
|
if (filenameMatch) {
|
|
filename = filenameMatch[1];
|
|
}
|
|
}
|
|
|
|
// Create a blob from the response and download it
|
|
const blob = await response.blob();
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = filename;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
window.URL.revokeObjectURL(url);
|
|
document.body.removeChild(a);
|
|
|
|
// Close the modal
|
|
const modal = document.querySelector('.fixed');
|
|
if (modal) {
|
|
modal.remove();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error confirming listings:', error);
|
|
alert(error.message || 'Failed to confirm listings. Please try again.');
|
|
}
|
|
}
|
|
|
|
// Show open box modal
|
|
async function showOpenBoxModal(inventoryItemId, physicalItemId) {
|
|
try {
|
|
// Fetch Manabox files with pagination
|
|
const skip = (currentManaboxPage - 1) * currentManaboxLimit;
|
|
const response = await fetch(`/api/manabox/manabox-file-uploads?skip=${skip}&limit=${currentManaboxLimit}`);
|
|
const files = await response.json();
|
|
|
|
// Create modal for selecting Manabox file
|
|
const modal = document.createElement('div');
|
|
modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center';
|
|
modal.innerHTML = `
|
|
<div class="bg-gray-800 rounded-lg p-6 max-w-2xl w-full mx-4">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h3 class="text-xl font-semibold text-gray-100">Select Manabox Files</h3>
|
|
<button onclick="this.closest('.fixed').remove()" class="text-gray-400 hover:text-white">
|
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<div class="space-y-4">
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full divide-y divide-gray-700">
|
|
<thead>
|
|
<tr>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Select</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Source</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Description</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Upload Date</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-gray-700">
|
|
${files.map(file => `
|
|
<tr>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<input type="checkbox" class="manabox-file-checkbox rounded border-gray-600 bg-gray-700 text-blue-600 focus:ring-blue-500" value="${file.id}">
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">${file.file_metadata.source}</td>
|
|
<td class="px-6 py-4 text-sm text-gray-300">${file.file_metadata.description}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">${formatDate(file.created_at)}</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<!-- Pagination Controls -->
|
|
<div class="flex justify-between items-center mt-4">
|
|
<button onclick="changeManaboxPage(${inventoryItemId}, ${physicalItemId}, ${currentManaboxPage - 1})"
|
|
class="px-4 py-2 bg-gray-700 text-white rounded-lg hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
${currentManaboxPage === 1 ? 'disabled' : ''}>
|
|
Previous
|
|
</button>
|
|
<span class="text-gray-300">Page ${currentManaboxPage}</span>
|
|
<button onclick="changeManaboxPage(${inventoryItemId}, ${physicalItemId}, ${currentManaboxPage + 1})"
|
|
class="px-4 py-2 bg-gray-700 text-white rounded-lg hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
${files.length < currentManaboxLimit ? 'disabled' : ''}>
|
|
Next
|
|
</button>
|
|
</div>
|
|
<div class="flex justify-end mt-4">
|
|
<button onclick="openBoxWithFiles(${inventoryItemId}, ${physicalItemId})"
|
|
class="px-4 py-2 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">
|
|
Open Box with Selected Files
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.body.appendChild(modal);
|
|
} catch (error) {
|
|
console.error('Error loading Manabox files:', error);
|
|
}
|
|
}
|
|
|
|
// Change Manabox page
|
|
async function changeManaboxPage(inventoryItemId, physicalItemId, newPage) {
|
|
if (newPage < 1) return;
|
|
currentManaboxPage = newPage;
|
|
const modal = document.querySelector('.fixed');
|
|
if (modal) {
|
|
modal.remove();
|
|
}
|
|
await showOpenBoxModal(inventoryItemId, physicalItemId);
|
|
}
|
|
|
|
// Open box with selected Manabox files
|
|
async function openBoxWithFiles(inventoryItemId, physicalItemId) {
|
|
try {
|
|
// Get selected file IDs
|
|
const selectedFiles = Array.from(document.querySelectorAll('.manabox-file-checkbox:checked'))
|
|
.map(checkbox => parseInt(checkbox.value));
|
|
|
|
if (selectedFiles.length === 0) {
|
|
alert('Please select at least one Manabox file');
|
|
return;
|
|
}
|
|
|
|
const response = await fetch(`/api/inventory/items/${inventoryItemId}/open`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
inventory_item_id: inventoryItemId,
|
|
manabox_file_upload_ids: selectedFiles
|
|
})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.detail || 'Failed to open box');
|
|
}
|
|
|
|
const openEvent = await response.json();
|
|
if (!openEvent) {
|
|
throw new Error('No data received from server');
|
|
}
|
|
|
|
showOpenEventDetails(openEvent);
|
|
|
|
// Close the modal
|
|
const modal = document.querySelector('.fixed');
|
|
if (modal) {
|
|
modal.remove();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error opening box:', error);
|
|
alert(error.message || 'Failed to open box. Please try again.');
|
|
}
|
|
}
|
|
|
|
// Show open event details
|
|
function showOpenEventDetails(openEvent) {
|
|
if (!openEvent) {
|
|
console.error('No open event data provided');
|
|
return;
|
|
}
|
|
|
|
const modal = document.createElement('div');
|
|
modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center';
|
|
modal.innerHTML = `
|
|
<div class="bg-gray-800 rounded-lg p-6 max-w-2xl w-full mx-4">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h3 class="text-xl font-semibold text-gray-100">Box Opening Results</h3>
|
|
<button onclick="this.closest('.fixed').remove()" class="text-gray-400 hover:text-white">
|
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<div class="space-y-4">
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<h4 class="text-sm font-medium text-gray-400">Open Event ID</h4>
|
|
<p class="text-gray-100">${openEvent.id || 'N/A'}</p>
|
|
</div>
|
|
<div>
|
|
<h4 class="text-sm font-medium text-gray-400">Date</h4>
|
|
<p class="text-gray-100">${openEvent.created_at ? formatDate(openEvent.created_at) : 'N/A'}</p>
|
|
</div>
|
|
</div>
|
|
<div class="border-t border-gray-700 pt-6">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h4 class="text-lg font-medium text-gray-100">Opened Items</h4>
|
|
${openEvent.source_item_id ? `
|
|
<button onclick="createListingsForOpenEvent(${openEvent.source_item_id}, ${openEvent.id})"
|
|
class="px-4 py-2 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">
|
|
Create Listings
|
|
</button>
|
|
` : ''}
|
|
</div>
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full divide-y divide-gray-700">
|
|
<thead>
|
|
<tr>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Card Name</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Set</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Condition</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-gray-700">
|
|
${openEvent.items && openEvent.items.length > 0 ?
|
|
openEvent.items.map(item => `
|
|
<tr>
|
|
<td class="px-6 py-4 text-sm text-gray-300">${item.card_name || 'N/A'}</td>
|
|
<td class="px-6 py-4 text-sm text-gray-300">${item.set_name || 'N/A'}</td>
|
|
<td class="px-6 py-4 text-sm text-gray-300">${item.condition || 'N/A'}</td>
|
|
</tr>
|
|
`).join('') :
|
|
`<tr>
|
|
<td colspan="3" class="px-6 py-4 text-sm text-gray-300 text-center">No items found</td>
|
|
</tr>`
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.body.appendChild(modal);
|
|
}
|
|
|
|
// Add new function to handle creating listings
|
|
async function createListingsForOpenEvent(inventoryItemId, openEventId) {
|
|
try {
|
|
const response = await fetch(`/api/inventory/items/${inventoryItemId}/open-events/${openEventId}/create-listings`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.detail || 'Failed to create listings');
|
|
}
|
|
|
|
const result = await response.json();
|
|
alert('Listings created successfully!');
|
|
|
|
// Close the modal
|
|
const modal = document.querySelector('.fixed');
|
|
if (modal) {
|
|
modal.remove();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error creating listings:', error);
|
|
alert(error.message || 'Failed to create listings. Please try again.');
|
|
}
|
|
}
|
|
|
|
// Helper functions
|
|
function formatDate(dateString) {
|
|
return new Date(dateString).toLocaleString();
|
|
}
|
|
|
|
function getPartyName(transaction) {
|
|
if (transaction.transaction_type === 'purchase') {
|
|
return transaction.vendor?.name || 'Unknown Vendor';
|
|
} else {
|
|
return transaction.customer?.name || 'Unknown Customer';
|
|
}
|
|
}
|
|
|
|
// Close modal when clicking outside
|
|
transactionDetailsModal.addEventListener('click', (e) => {
|
|
if (e.target === transactionDetailsModal) {
|
|
closeTransactionDetailsModal();
|
|
}
|
|
});
|
|
|
|
async function saveTransaction() {
|
|
const form = document.getElementById('transactionForm');
|
|
if (!form.checkValidity()) {
|
|
form.reportValidity();
|
|
return;
|
|
}
|
|
|
|
const transactionType = document.getElementById('transactionType').value;
|
|
|
|
// Collect all items with their data
|
|
const items = Array.from(document.querySelectorAll('.item')).map(item => {
|
|
const productId = parseInt(item.querySelector('.product-search').dataset.tcgplayer_product_id);
|
|
const unitPrice = parseFloat(item.querySelector('.unit-cost').value);
|
|
const quantity = parseInt(item.querySelector('.quantity').value);
|
|
const itemType = item.querySelector('.item-type').value;
|
|
|
|
if (isNaN(productId) || isNaN(unitPrice) || isNaN(quantity)) {
|
|
throw new Error('Invalid item data');
|
|
}
|
|
|
|
return {
|
|
product_id: productId,
|
|
unit_price: unitPrice,
|
|
quantity: quantity,
|
|
item_type: itemType
|
|
};
|
|
});
|
|
|
|
if (items.length === 0) {
|
|
alert('Please add at least one item to the transaction');
|
|
return;
|
|
}
|
|
|
|
const transactionData = {
|
|
transaction_date: document.getElementById('transactionDate').value,
|
|
transaction_notes: document.getElementById('transactionNotes').value,
|
|
items: items
|
|
};
|
|
|
|
if (transactionType === 'purchase') {
|
|
const vendorId = parseInt(document.getElementById('vendorSelect').value);
|
|
if (isNaN(vendorId)) {
|
|
alert('Please select a vendor');
|
|
return;
|
|
}
|
|
transactionData.vendor_id = vendorId;
|
|
} else {
|
|
const customerId = parseInt(document.getElementById('vendorSelect').value);
|
|
if (isNaN(customerId)) {
|
|
alert('Please select a customer');
|
|
return;
|
|
}
|
|
transactionData.customer_id = customerId;
|
|
|
|
const marketplaceId = document.getElementById('marketplaceSelect').value;
|
|
if (marketplaceId) {
|
|
transactionData.marketplace_id = parseInt(marketplaceId);
|
|
}
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/api/inventory/transactions/${transactionType}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(transactionData)
|
|
});
|
|
|
|
if (response.ok) {
|
|
closeTransactionModal();
|
|
loadTransactions();
|
|
} else {
|
|
const error = await response.json();
|
|
alert(`Failed to save transaction: ${error.detail}`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error saving transaction:', error);
|
|
alert('Failed to save transaction. Please try again.');
|
|
}
|
|
}
|