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 = `
Item ${itemIndex + 1}
${itemIndex > 0 ? '' : ''}
`;
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 => `
${product.name}
`).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 = `
${formatDate(transaction.transaction_date)} |
${transaction.transaction_type} |
${getPartyName(transaction)} |
$${transaction.transaction_total_amount.toFixed(2)} |
${transaction.transaction_notes || ''} |
`;
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 = `
Date
${formatDate(transactionData.transaction_date)}
Type
${transactionData.transaction_type}
${transactionData.transaction_type === 'purchase' ? 'Vendor' : 'Customer'}
${getPartyName(transactionData)}
Total Amount
$${transactionData.transaction_total_amount.toFixed(2)}
Notes
${transactionData.transaction_notes || 'No notes'}
Items
Item ID |
Unit Price |
${transactionData.transaction_items.map(item => `
${item.inventory_item_id} |
$${item.unit_price.toFixed(2)} |
`).join('')}
`;
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 = `
Physical Item ID
${itemData.physical_item_id}
Cost Basis
$${itemData.cost_basis.toFixed(2)}
Parent ID
${itemData.parent_id || 'None'}
Created At
${formatDate(itemData.created_at)}
Updated At
${formatDate(itemData.updated_at)}
${itemData.listed_price || itemData.recommended_price ? `
Pricing
${itemData.listed_price ? `
Listed Price:
$${itemData.listed_price.toFixed(2)}
` : ''}
${itemData.recommended_price ? `
Recommended Price:
$${itemData.recommended_price.toFixed(2)}
` : ''}
` : ''}
Product Details
Product ID
${itemData.product.id}
TCGPlayer Product ID
${itemData.product.tcgplayer_product_id}
Name
${itemData.product.name}
Image
Category
${itemData.product.category_name}
Group
${itemData.product.group_name}
Market Price
$${itemData.product.market_price.toFixed(2)}
${itemData.product.category_name === 'Magic' && itemData.item_type !== 'card' && !hasOpenEvents ? `
Box Actions
` : ''}
`;
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 = `
Date |
Source Item ID |
Action |
${data.open_events.map(event => `
${formatDate(event.created_at)} |
${event.source_item_id} |
|
`).join('')}
`;
} 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 = `
Resulting Items
Card Name |
Set |
Market Price |
Action |
${resultingItems.map(item => `
${item.product.name} |
${item.product.group_name} |
$${item.product.market_price.toFixed(2)} |
|
`).join('')}
`;
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 = `
Page ${currentManaboxPage}
`;
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 = `
Open Event ID
${openEvent.id || 'N/A'}
Date
${openEvent.created_at ? formatDate(openEvent.created_at) : 'N/A'}
Opened Items
${openEvent.source_item_id ? `
` : ''}
Card Name |
Set |
Condition |
${openEvent.items && openEvent.items.length > 0 ?
openEvent.items.map(item => `
${item.card_name || 'N/A'} |
${item.set_name || 'N/A'} |
${item.condition || 'N/A'} |
`).join('') :
`
No items found |
`
}
`;
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.');
}
}