ai_giga_tcg/app/static/transactions.js
2025-05-30 17:31:59 -04:00

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.');
}
}