Skip to content

Instantly share code, notes, and snippets.

@algarih
Last active August 28, 2025 12:43
Show Gist options
  • Select an option

  • Save algarih/4b64a0be8007f3d7daba28c79b5ebae3 to your computer and use it in GitHub Desktop.

Select an option

Save algarih/4b64a0be8007f3d7daba28c79b5ebae3 to your computer and use it in GitHub Desktop.
# Prevent directory listing
Options -Indexes
# Set default character set
AddDefaultCharset UTF-8
# Enable URL rewriting
RewriteEngine On
# Redirect to setup if database doesn't exist
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} !setup.php
RewriteRule ^(.*)$ setup.php [L]
<?php
require_once 'config.php';
require_once 'functions.php';
header('Content-Type: application/json');
// Get the action from the request
$action = isset($_POST['action']) ? $_POST['action'] : (isset($_GET['action']) ? $_GET['action'] : '');
// Get the page ID from the request
$pageId = isset($_POST['page_id']) ? $_POST['page_id'] : (isset($_GET['page_id']) ? $_GET['page_id'] : null);
// Process the action
switch ($action) {
// Page operations
case 'get_pages':
echo json_encode(getPages());
break;
case 'get_page':
if ($pageId) {
echo json_encode(getPage($pageId));
} else {
echo json_encode(['success' => false, 'message' => 'Page ID is required']);
}
break;
case 'create_page':
$title = isset($_POST['title']) ? $_POST['title'] : '';
$subtitle = isset($_POST['subtitle']) ? $_POST['subtitle'] : '';
$type = isset($_POST['type']) ? $_POST['type'] : 'invoice';
if ($title) {
$id = createPage($title, $subtitle, $type);
echo json_encode(['success' => true, 'id' => $id]);
} else {
echo json_encode(['success' => false, 'message' => 'Title is required']);
}
break;
case 'update_page':
if ($pageId) {
$title = isset($_POST['title']) ? $_POST['title'] : '';
$subtitle = isset($_POST['subtitle']) ? $_POST['subtitle'] : '';
if ($title) {
$rowCount = updatePage($pageId, $title, $subtitle);
echo json_encode(['success' => $rowCount > 0]);
} else {
echo json_encode(['success' => false, 'message' => 'Title is required']);
}
} else {
echo json_encode(['success' => false, 'message' => 'Page ID is required']);
}
break;
case 'delete_page':
if ($pageId) {
$rowCount = deletePage($pageId);
echo json_encode(['success' => $rowCount > 0]);
} else {
echo json_encode(['success' => false, 'message' => 'Page ID is required']);
}
break;
// Get data
case 'get_company_tags':
echo json_encode(getCompanyTags());
break;
case 'get_reference_tags':
echo json_encode(getReferenceTags());
break;
case 'get_hs_codes':
echo json_encode(getHsCodes());
break;
case 'get_products':
if ($pageId) {
echo json_encode(getProducts($pageId));
} else {
echo json_encode(['success' => false, 'message' => 'Page ID is required']);
}
break;
case 'get_shipment':
if ($pageId) {
echo json_encode(getShipment($pageId));
} else {
echo json_encode(['success' => false, 'message' => 'Page ID is required']);
}
break;
case 'get_document':
if ($pageId) {
echo json_encode(getDocument($pageId));
} else {
echo json_encode(['success' => false, 'message' => 'Page ID is required']);
}
break;
case 'get_notes':
if ($pageId) {
echo json_encode(getNotes($pageId));
} else {
echo json_encode(['success' => false, 'message' => 'Page ID is required']);
}
break;
case 'get_history':
if ($pageId) {
echo json_encode(getHistory($pageId));
} else {
echo json_encode(['success' => false, 'message' => 'Page ID is required']);
}
break;
// Add data
case 'add_company_tag':
$name = isset($_POST['name']) ? $_POST['name'] : '';
if ($name) {
$id = addCompanyTag($name);
echo json_encode(['success' => true, 'id' => $id]);
} else {
echo json_encode(['success' => false, 'message' => 'Name is required']);
}
break;
case 'add_reference_tag':
$data = [
'name' => isset($_POST['name']) ? $_POST['name'] : '',
'rma_ref' => isset($_POST['rma_ref']) ? $_POST['rma_ref'] : '',
'buyer_reference' => isset($_POST['buyer_reference']) ? $_POST['buyer_reference'] : '',
'packing_list_ref' => isset($_POST['packing_list_ref']) ? $_POST['packing_list_ref'] : '',
'export_invoice_number' => isset($_POST['export_invoice_number']) ? $_POST['export_invoice_number'] : '',
'export_invoice_date' => isset($_POST['export_invoice_date']) ? $_POST['export_invoice_date'] : '',
'method_of_delivery' => isset($_POST['method_of_delivery']) ? $_POST['method_of_delivery'] : '',
'delivery_term' => isset($_POST['delivery_term']) ? $_POST['delivery_term'] : '',
'terms' => isset($_POST['terms']) ? $_POST['terms'] : '',
'signatory_company' => isset($_POST['signatory_company']) ? $_POST['signatory_company'] : '',
'authorized_signatory' => isset($_POST['authorized_signatory']) ? $_POST['authorized_signatory'] : ''
];
if ($data['name']) {
$id = addReferenceTag($data);
echo json_encode(['success' => true, 'id' => $id]);
} else {
echo json_encode(['success' => false, 'message' => 'Name is required']);
}
break;
case 'add_hs_code':
$code = isset($_POST['code']) ? $_POST['code'] : '';
$title = isset($_POST['title']) ? $_POST['title'] : '';
if ($code && $title) {
$id = addHsCode($code, $title);
echo json_encode(['success' => true, 'id' => $id]);
} else {
echo json_encode(['success' => false, 'message' => 'Code and title are required']);
}
break;
case 'add_product':
if (!$pageId) {
echo json_encode(['success' => false, 'message' => 'Page ID is required']);
break;
}
$data = [
'page_id' => $pageId,
'product_code' => isset($_POST['product_code']) ? $_POST['product_code'] : '',
'tag_ref' => isset($_POST['tag_ref']) ? $_POST['tag_ref'] : '',
'hs_code' => isset($_POST['hs_code']) ? $_POST['hs_code'] : '',
'description' => isset($_POST['description']) ? $_POST['description'] : '',
'quantity' => isset($_POST['quantity']) ? $_POST['quantity'] : 0,
'unit_kind' => isset($_POST['unit_kind']) ? $_POST['unit_kind'] : 'Box',
'packages' => isset($_POST['packages']) ? $_POST['packages'] : 0,
'length' => isset($_POST['length']) ? $_POST['length'] : 0,
'width' => isset($_POST['width']) ? $_POST['width'] : 0,
'height' => isset($_POST['height']) ? $_POST['height'] : 0,
'net_weight' => isset($_POST['net_weight']) ? $_POST['net_weight'] : 0,
'gross_weight' => isset($_POST['gross_weight']) ? $_POST['gross_weight'] : 0,
'unit_price' => isset($_POST['unit_price']) ? $_POST['unit_price'] : 0,
'currency' => isset($_POST['currency']) ? $_POST['currency'] : 'USD',
'exclude_invoice' => isset($_POST['exclude_invoice']) ? 1 : 0,
'sort_order' => isset($_POST['sort_order']) ? $_POST['sort_order'] : 0
];
if ($data['product_code'] && $data['description']) {
$id = addProduct($data);
addHistory($pageId, 'add_product', ['id' => $id, 'data' => $data]);
echo json_encode(['success' => true, 'id' => $id]);
} else {
echo json_encode(['success' => false, 'message' => 'Product code and description are required']);
}
break;
// Update data
case 'update_shipment':
if (!$pageId) {
echo json_encode(['success' => false, 'message' => 'Page ID is required']);
break;
}
$data = [
'id' => isset($_POST['id']) ? $_POST['id'] : 0,
'shipper' => isset($_POST['shipper']) ? $_POST['shipper'] : '',
'dispatch_method' => isset($_POST['dispatch_method']) ? $_POST['dispatch_method'] : '',
'shipment_type' => isset($_POST['shipment_type']) ? $_POST['shipment_type'] : '',
'origin_country' => isset($_POST['origin_country']) ? $_POST['origin_country'] : '',
'destination_country' => isset($_POST['destination_country']) ? $_POST['destination_country'] : '',
'duty_exemption_no' => isset($_POST['duty_exemption_no']) ? $_POST['duty_exemption_no'] : '',
'factory_number' => isset($_POST['factory_number']) ? $_POST['factory_number'] : '',
'consignee' => isset($_POST['consignee']) ? $_POST['consignee'] : '',
'buyer' => isset($_POST['buyer']) ? $_POST['buyer'] : '',
'company_tag_id' => isset($_POST['company_tag_id']) ? $_POST['company_tag_id'] : null
];
$rowCount = updateShipment($data);
if ($rowCount > 0) {
addHistory($pageId, 'update_shipment', ['id' => $data['id'], 'data' => $data]);
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'message' => 'No changes made']);
}
break;
case 'update_document':
if (!$pageId) {
echo json_encode(['success' => false, 'message' => 'Page ID is required']);
break;
}
$data = [
'id' => isset($_POST['id']) ? $_POST['id'] : 0,
'rma_ref' => isset($_POST['rma_ref']) ? $_POST['rma_ref'] : '',
'buyer_reference' => isset($_POST['buyer_reference']) ? $_POST['buyer_reference'] : '',
'packing_list_ref' => isset($_POST['packing_list_ref']) ? $_POST['packing_list_ref'] : '',
'export_invoice_number' => isset($_POST['export_invoice_number']) ? $_POST['export_invoice_number'] : '',
'export_invoice_date' => isset($_POST['export_invoice_date']) ? $_POST['export_invoice_date'] : '',
'method_of_delivery' => isset($_POST['method_of_delivery']) ? $_POST['method_of_delivery'] : '',
'delivery_term' => isset($_POST['delivery_term']) ? $_POST['delivery_term'] : '',
'terms' => isset($_POST['terms']) ? $_POST['terms'] : '',
'signatory_company' => isset($_POST['signatory_company']) ? $_POST['signatory_company'] : '',
'authorized_signatory' => isset($_POST['authorized_signatory']) ? $_POST['authorized_signatory'] : '',
'reference_tag_id' => isset($_POST['reference_tag_id']) ? $_POST['reference_tag_id'] : null
];
$rowCount = updateDocument($data);
if ($rowCount > 0) {
addHistory($pageId, 'update_document', ['id' => $data['id'], 'data' => $data]);
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'message' => 'No changes made']);
}
break;
case 'update_notes':
if (!$pageId) {
echo json_encode(['success' => false, 'message' => 'Page ID is required']);
break;
}
$content = isset($_POST['content']) ? $_POST['content'] : '';
$id = isset($_POST['id']) ? $_POST['id'] : 0;
if ($id) {
$rowCount = updateNotes($content, $id);
if ($rowCount > 0) {
addHistory($pageId, 'update_notes', ['id' => $id, 'content' => $content]);
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'message' => 'No changes made']);
}
} else {
// Create new note if none exists
global $pdo;
$stmt = $pdo->prepare("INSERT INTO notes (page_id, content) VALUES (?, ?)");
$stmt->execute([$pageId, $content]);
$id = $pdo->lastInsertId();
addHistory($pageId, 'add_notes', ['id' => $id, 'content' => $content]);
echo json_encode(['success' => true, 'id' => $id]);
}
break;
case 'update_product':
if (!$pageId) {
echo json_encode(['success' => false, 'message' => 'Page ID is required']);
break;
}
$data = [
'id' => isset($_POST['id']) ? $_POST['id'] : 0,
'product_code' => isset($_POST['product_code']) ? $_POST['product_code'] : '',
'tag_ref' => isset($_POST['tag_ref']) ? $_POST['tag_ref'] : '',
'hs_code' => isset($_POST['hs_code']) ? $_POST['hs_code'] : '',
'description' => isset($_POST['description']) ? $_POST['description'] : '',
'quantity' => isset($_POST['quantity']) ? $_POST['quantity'] : 0,
'unit_kind' => isset($_POST['unit_kind']) ? $_POST['unit_kind'] : 'Box',
'packages' => isset($_POST['packages']) ? $_POST['packages'] : 0,
'length' => isset($_POST['length']) ? $_POST['length'] : 0,
'width' => isset($_POST['width']) ? $_POST['width'] : 0,
'height' => isset($_POST['height']) ? $_POST['height'] : 0,
'net_weight' => isset($_POST['net_weight']) ? $_POST['net_weight'] : 0,
'gross_weight' => isset($_POST['gross_weight']) ? $_POST['gross_weight'] : 0,
'unit_price' => isset($_POST['unit_price']) ? $_POST['unit_price'] : 0,
'currency' => isset($_POST['currency']) ? $_POST['currency'] : 'USD',
'exclude_invoice' => isset($_POST['exclude_invoice']) ? 1 : 0
];
// Get old data for history
global $pdo;
$stmt = $pdo->prepare("SELECT * FROM products WHERE id = ?");
$stmt->execute([$data['id']]);
$oldData = $stmt->fetch(PDO::FETCH_ASSOC);
$rowCount = updateProduct($data);
if ($rowCount > 0) {
addHistory($pageId, 'update_product', [
'id' => $data['id'],
'old_data' => $oldData,
'new_data' => $data
]);
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'message' => 'No changes made']);
}
break;
case 'update_product_sort_order':
if (!$pageId) {
echo json_encode(['success' => false, 'message' => 'Page ID is required']);
break;
}
$productId = isset($_POST['product_id']) ? $_POST['product_id'] : 0;
$sortOrder = isset($_POST['sort_order']) ? $_POST['sort_order'] : 0;
$rowCount = updateProductSortOrder($productId, $sortOrder);
if ($rowCount > 0) {
addHistory($pageId, 'update_product_sort_order', ['product_id' => $productId, 'sort_order' => $sortOrder]);
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'message' => 'No changes made']);
}
break;
// Delete data
case 'delete_company_tag':
$id = isset($_POST['id']) ? $_POST['id'] : 0;
if ($id) {
$rowCount = deleteCompanyTag($id);
echo json_encode(['success' => $rowCount > 0]);
} else {
echo json_encode(['success' => false, 'message' => 'ID is required']);
}
break;
case 'delete_reference_tag':
$id = isset($_POST['id']) ? $_POST['id'] : 0;
if ($id) {
$rowCount = deleteReferenceTag($id);
echo json_encode(['success' => $rowCount > 0]);
} else {
echo json_encode(['success' => false, 'message' => 'ID is required']);
}
break;
case 'delete_hs_code':
$id = isset($_POST['id']) ? $_POST['id'] : 0;
if ($id) {
$rowCount = deleteHsCode($id);
echo json_encode(['success' => $rowCount > 0]);
} else {
echo json_encode(['success' => false, 'message' => 'ID is required']);
}
break;
case 'delete_product':
if (!$pageId) {
echo json_encode(['success' => false, 'message' => 'Page ID is required']);
break;
}
$id = isset($_POST['id']) ? $_POST['id'] : 0;
if ($id) {
// Get product data for history
global $pdo;
$stmt = $pdo->prepare("SELECT * FROM products WHERE id = ?");
$stmt->execute([$id]);
$productData = $stmt->fetch(PDO::FETCH_ASSOC);
$rowCount = deleteProduct($id);
if ($rowCount > 0) {
addHistory($pageId, 'delete_product', $productData);
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'message' => 'Product not found']);
}
} else {
echo json_encode(['success' => false, 'message' => 'ID is required']);
}
break;
// Undo last action
case 'undo':
if (!$pageId) {
echo json_encode(['success' => false, 'message' => 'Page ID is required']);
break;
}
$success = undoLastAction($pageId);
echo json_encode(['success' => $success]);
break;
default:
echo json_encode(['success' => false, 'message' => 'Invalid action']);
break;
}
(function() {
// State and config
const state = { currentPageId: null, currentShipmentId: null, currentDocumentId: null,
currentNotesId: null, currentCompanyId: null, currentReferenceId: null, saveTimeout: null };
const config = {
defaults: { dispatchMethod: 'Road Freight', originCountry: 'BH', dutyExemption: '5019',
factoryNumber: '693/1', signatoryCompany: 'Mohamed Ramadhan', authorizedSignatory: 'RMA Middle East W.L.L' },
fields: {
shipment: ['shipper', 'dispatch-method', 'shipment-type', 'origin-country', 'destination-country',
'duty-exemption', 'factory-number', 'consignee', 'buyer'],
document: ['rma-ref', 'buyer-reference', 'packing-list-ref', 'export-invoice-number', 'export-invoice-date',
'method-of-delivery', 'delivery-term', 'terms', 'signatory-company', 'authorized-signatory'],
notes: ['notes']
},
references: ['rma-ref', 'buyer-reference', 'packing-list-ref', 'export-invoice-number', 'export-invoice-date',
'method-of-delivery', 'delivery-term', 'terms', 'signatory-company', 'authorized-signatory'],
rates: { EUR: 1.1, BHD: 2.65, GBP: 1.3 }
};
// API service
const api = {
async req(method, action, data = {}) {
try {
const isGet = method === 'get';
const options = isGet ?
{ params: { action, ...data } } :
data instanceof FormData ? data : new URLSearchParams({ action, ...data });
const response = await axios[method]('api.php', options);
return response.data;
} catch (error) {
Swal.fire('Error', `API Error: ${error.message}`, 'error');
throw error;
}
},
get(action, data) { return this.req('get', action, data); },
post(action, data) { return this.req('post', action, data); },
postForm(action, data) {
data.append('action', action);
return this.req('post', action, data);
}
};
// Utilities
const utils = {
formatPrice(p, c) { return (c === 'EUR' ? p.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, '.').replace('.', ',') :
p.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',')); },
notify(msg, type = 'success') {
Swal.mixin({ toast: true, position: 'top-end', showConfirmButton: false, timer: 3000, timerProgressBar: true })
.fire({ icon: { success: 'success', danger: 'error', warning: 'warning' }[type] || 'info', title: msg });
},
iconText(t) { return t ? (t.length >= 3 ? t[0] + t.slice(-2) : t.slice(0, Math.min(t.length, 3))) : ''; },
saving(show) { $('#live-saving-indicator')[show ? 'addClass' : 'removeClass']('show').text(show ? 'Saving...' : ''); },
createPageItem(page) {
return $(`<a class="nav-link d-flex align-items-center gap-3 nav-item page-item" data-id="${page.id}" href="#">
<div class="page-item-icon">${this.iconText(page.title)}</div>
<div class="nav-item-content">
<div>${page.title}</div>
<div class="text-muted small">${page.subtitle || ''}</div>
</div>
</a>`).on('click', e => {
e.preventDefault();
loadPage(page.id);
$('.page-item').removeClass('active');
$(e.currentTarget).addClass('active');
});
},
createTag(tag, attrs = {}) {
const $tag = $(`<span class="tag" data-id="${tag.id}" data-value="${tag.name}">${tag.name}</span>`);
Object.entries(attrs).forEach(([k, v]) => $tag.attr(k, v));
return $tag;
},
createHsCode(hs) {
return $(`<div class="hs-code-item" data-id="${hs.id}">
<div><strong>${hs.code}</strong> - ${hs.title}</div>
<button class="btn btn-sm btn-outline-danger delete-hs-code"><i class="bi bi-trash"></i></button>
</div>`);
},
// Fix for accessibility issue with modals
setupModalFocus(modalId) {
const modalElement = document.getElementById(modalId);
if (!modalElement) return;
// Store the element that opened the modal
let focusedElementBeforeModal;
// When modal is shown, store the currently focused element
modalElement.addEventListener('show.bs.modal', function() {
focusedElementBeforeModal = document.activeElement;
// Remove aria-hidden to prevent accessibility issues
this.removeAttribute('aria-hidden');
});
// When modal is hidden, restore focus to the element that opened it
modalElement.addEventListener('hidden.bs.modal', function() {
if (focusedElementBeforeModal) {
focusedElementBeforeModal.focus();
} else {
// If no stored element, try to find the button that triggered this modal
const triggerButton = document.querySelector(`[data-bs-target="#${modalId}"]`);
if (triggerButton) {
triggerButton.focus();
} else {
// If no trigger button found, focus on the main content area
document.getElementById('main')?.focus();
}
}
});
},
// Safely get or create a modal instance
getModalInstance(selector) {
const modalElement = document.querySelector(selector);
if (!modalElement) return null;
// Return existing instance if available
const existingInstance = bootstrap.Modal.getInstance(modalElement);
if (existingInstance) return existingInstance;
// Create new instance if none exists
return new bootstrap.Modal(modalElement);
},
// Fix modal stacking issues
setupModalStacking() {
// Handle modal stacking z-index
$(document).on('show.bs.modal', '.modal', function() {
const zIndex = 1040 + (10 * $('.modal:visible').length);
$(this).css('z-index', zIndex);
// Create a new backdrop for this modal
setTimeout(() => {
const backdrop = $('<div class="modal-backdrop modal-stack fade"></div>');
backdrop.css('z-index', zIndex - 1);
$('body').append(backdrop);
backdrop.addClass('show');
}, 0);
});
// Clean up when modal is hidden
$(document).on('hidden.bs.modal', '.modal', function() {
$('.modal-stack').last().remove();
});
}
};
// Data loader
const dataLoader = {
async load(endpoint, container, itemFn, emptyMsg) {
try {
const data = await api.get(endpoint);
const $container = $(container).empty();
data.length ?
data.forEach(item => $container.append(itemFn(item))) :
$container.html(emptyMsg);
return data;
} catch (error) {
utils.notify(`Error loading ${endpoint}`, 'danger');
}
},
loadPages() {
return this.load('get_pages', '#pages-container', page => utils.createPageItem(page), '')
.then(data => {
if (data.length && !state.currentPageId) {
loadPage(data[0].id);
$('.page-item').first().addClass('active');
}
});
},
loadCompanyTags() {
return this.load('get_company_tags', '#company-tags', tag => utils.createTag(tag),
'<span class="text-muted">No company tags available</span>');
},
loadReferenceTags() {
return this.load('get_reference_tags', '#reference-tags', tag => {
const attrs = { 'data-values': JSON.stringify(_.pick(tag, config.references)) };
return utils.createTag(tag, attrs);
}, '<span class="text-muted">No reference tags available</span>');
},
loadHsCodes() {
return this.load('get_hs_codes', '#hs-code-reference', hs => utils.createHsCode(hs),
'<div class="text-muted p-3">No HS codes available</div>');
},
loadProducts() {
if (!state.currentPageId) return;
return api.get('get_products', { page_id: state.currentPageId })
.then(data => {
const $tbody = $('#productTableBody').empty();
if (data.length) {
data.forEach(p => {
const vol = (p.length * p.width * p.height).toFixed(3);
const price = utils.formatPrice(parseFloat(p.unit_price), p.currency);
$tbody.append(`<tr data-id="${p.id}">
<td><i class="bi bi-grip-vertical drag-handle"></i></td>
<td><div class="product-code-container"><div>${p.product_code}</div><div class="product-tag">${p.tag_ref || ''}</div></div></td>
<td>${p.description}</td>
<td>${p.quantity}</td>
<td>${p.packages} ${p.unit_kind}</td>
<td><div class="weight-container"><div>Net: ${p.net_weight} KG</div><div>Gross: ${p.gross_weight} KG</div></div></td>
<td><div class="dimensions-container"><div>${p.length}x${p.width}x${p.height}</div><div class="dimensions-label">CBM = ${vol} m³</div></div></td>
<td>${p.currency} ${price}</td>
<td><div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary edit-product-btn" data-bs-toggle="modal" data-bs-target="#addProductModal"><i class="bi bi-pencil"></i></button>
<button class="btn btn-outline-secondary duplicate-product-btn"><i class="bi bi-copy"></i></button>
<button class="btn btn-outline-danger delete-product-btn"><i class="bi bi-trash"></i></button>
</div></td>
</tr>`);
});
$('#productTableFooter').show();
} else {
$tbody.html(`<tr id="empty-table-row">
<td colspan="9" class="empty-table-message">
<i class="bi bi-inbox fs-1 d-block mb-2"></i>
<p>No products added yet. Click "Add Product" to get started.</p>
</td>
</tr>`);
$('#productTableFooter').hide();
}
updateStats();
})
.catch(() => utils.notify('Error loading products', 'danger'));
},
loadShipment() {
if (!state.currentPageId) return;
return api.get('get_shipment', { page_id: state.currentPageId })
.then(data => {
if (data?.id) {
state.currentShipmentId = data.id;
$('#shipper').val(data.shipper || '');
$('#dispatch-method').val(data.dispatch_method || config.defaults.dispatchMethod);
$('#shipment-type').val(data.shipment_type || '');
$('#origin-country').val(data.origin_country || config.defaults.originCountry);
$('#destination-country').val(data.destination_country || '');
$('#duty-exemption').val(data.duty_exemption_no || config.defaults.dutyExemption);
$('#factory-number').val(data.factory_number || config.defaults.factoryNumber);
$('#consignee').val(data.consignee || '');
$('#buyer').val(data.buyer || '');
if (data.company_tag_id) {
state.currentCompanyId = data.company_tag_id;
$(`#company-tags .tag[data-id="${data.company_tag_id}"]`)
.addClass('active').siblings().removeClass('active');
}
} else {
$('#duty-exemption').val(config.defaults.dutyExemption);
$('#factory-number').val(config.defaults.factoryNumber);
}
})
.catch(() => utils.notify('Error loading shipment data', 'danger'));
},
loadDocument() {
if (!state.currentPageId) return;
return api.get('get_document', { page_id: state.currentPageId })
.then(data => {
if (data?.id) {
state.currentDocumentId = data.id;
$('#rma-ref').val(data.rma_ref || '');
$('#buyer-reference').val(data.buyer_reference || '');
$('#packing-list-ref').val(data.packing_list_ref || '');
$('#export-invoice-number').val(data.export_invoice_number || '');
$('#export-invoice-date').val(data.export_invoice_date || new Date().toISOString().split('T')[0]);
$('#method-of-delivery').val(data.method_of_delivery || '');
$('#delivery-term').val(data.delivery_term || '');
$('#terms').val(data.terms || '');
$('#signatory-company').val(data.signatory_company || config.defaults.signatoryCompany);
$('#authorized-signatory').val(data.authorized_signatory || config.defaults.authorizedSignatory);
if (data.reference_tag_id) {
state.currentReferenceId = data.reference_tag_id;
$(`#reference-tags .tag[data-id="${data.reference_tag_id}"]`)
.addClass('active').siblings().removeClass('active');
}
} else {
$('#export-invoice-date').val(new Date().toISOString().split('T')[0]);
$('#signatory-company').val(config.defaults.signatoryCompany);
$('#authorized-signatory').val(config.defaults.authorizedSignatory);
}
})
.catch(() => utils.notify('Error loading document data', 'danger'));
},
loadNotes() {
if (!state.currentPageId) return;
return api.get('get_notes', { page_id: state.currentPageId })
.then(data => {
if (data?.id) {
state.currentNotesId = data.id;
$('#notes').val(data.content || '');
}
})
.catch(() => utils.notify('Error loading notes data', 'danger'));
}
};
// Load page function
async function loadPage(pageId) {
state.currentPageId = pageId;
try {
const pageData = await api.get('get_page', { page_id: pageId });
if (pageData) {
$('#section-title').text(pageData.title);
$('#section-subtitle').text(pageData.subtitle || '');
}
await Promise.all([
dataLoader.loadCompanyTags(),
dataLoader.loadReferenceTags(),
dataLoader.loadHsCodes(),
dataLoader.loadProducts(),
dataLoader.loadShipment(),
dataLoader.loadDocument(),
dataLoader.loadNotes()
]);
} catch (error) {
console.error('Error loading page data:', error);
}
}
// Modal manager
const modalManager = {
init() {
// Set up focus management for all modals
const modalIds = [
'createPageModal', 'editModal', 'duplicateModal', 'deleteModal',
'saveTagModal', 'deleteCompanyModal', 'addProductModal',
'addHsCodeModal', 'confirmModal'
];
modalIds.forEach(id => utils.setupModalFocus(id));
// Set up modal stacking
utils.setupModalStacking();
this.setupModal('#createPageModal', () => {
let type = 'invoice';
$('.page-type-option').on('click', function() {
$('.page-type-option').removeClass('selected');
$(this).addClass('selected');
type = $(this).data('type');
});
$('.page-type-option[data-type="invoice"]').addClass('selected');
$('#confirm-create-page').on('click', async () => {
const title = $('#page-title').val().trim();
if (!title) return utils.notify('Title is required', 'warning');
try {
const data = await api.post('create_page', {
title: encodeURIComponent(title),
subtitle: encodeURIComponent($('#page-subtitle').val().trim()),
type
});
if (data.success) {
$('#create-page-form')[0].reset();
$('.page-type-option[data-type="invoice"]').addClass('selected');
type = 'invoice';
utils.getModalInstance('#createPageModal')?.hide();
dataLoader.loadPages();
utils.notify('Page created successfully!');
} else {
utils.notify(data.message || 'Error creating page', 'danger');
}
} catch (error) {
utils.notify('Error creating page', 'danger');
}
});
});
this.setupModal('#editModal', () => {
$('#editModal').on('show.bs.modal', () => {
$('#edit-title').val($('#section-title').text());
$('#edit-subtitle').val($('#section-subtitle').text());
});
$('#confirm-edit').on('click', async () => {
if (!state.currentPageId) return;
try {
const data = await api.post('update_page', {
page_id: state.currentPageId,
title: encodeURIComponent($('#edit-title').val()),
subtitle: encodeURIComponent($('#edit-subtitle').val())
});
if (data.success) {
$('#section-title').text($('#edit-title').val());
$('#section-subtitle').text($('#edit-subtitle').val());
dataLoader.loadPages();
utils.getModalInstance('#editModal')?.hide();
utils.notify('Page updated successfully!');
} else {
utils.notify(data.message || 'Error updating page', 'danger');
}
} catch (error) {
utils.notify('Error updating page', 'danger');
}
});
});
this.setupModal('#duplicateModal', () => {
$('#duplicateModal').on('show.bs.modal', () => {
$('#duplicate-title').val($('#section-title').text() + " (Copy)");
$('#duplicate-subtitle').val($('#section-subtitle').text());
});
$('#confirm-duplicate').on('click', async () => {
if (!state.currentPageId) return;
try {
const pageData = await api.get('get_page', { page_id: state.currentPageId });
if (pageData) {
const data = await api.post('create_page', {
title: encodeURIComponent($('#duplicate-title').val()),
subtitle: encodeURIComponent($('#duplicate-subtitle').val()),
type: pageData.type
});
if (data.success) {
utils.getModalInstance('#duplicateModal')?.hide();
utils.notify('Page duplicated successfully!');
dataLoader.loadPages();
} else {
utils.notify(data.message || 'Error duplicating page', 'danger');
}
}
} catch (error) {
utils.notify('Error duplicating page', 'danger');
}
});
});
this.setupModal('#deleteModal', () => {
$('#confirm-delete').on('click', async () => {
if (!state.currentPageId) return;
try {
const data = await api.post('delete_page', { page_id: state.currentPageId });
if (data.success) {
utils.getModalInstance('#deleteModal')?.hide();
utils.notify('Page deleted successfully!');
dataLoader.loadPages();
state.currentPageId = null;
} else {
utils.notify(data.message || 'Error deleting page', 'danger');
}
} catch (error) {
utils.notify('Error deleting page', 'danger');
}
});
});
this.setupModal('#saveTagModal', () => {
$('#confirm-save-tag').on('click', async () => {
const tagName = $('#tag-name').val().trim();
if (!tagName) return;
try {
const companyData = await api.post('add_company_tag', { name: encodeURIComponent(tagName) });
if (companyData.success) {
const formData = new FormData();
formData.append('name', tagName);
config.references.forEach(id => {
const input = document.getElementById(id);
if (input) formData.append(id, input.value);
});
const referenceData = await api.postForm('add_reference_tag', formData);
if (referenceData.success) {
dataLoader.loadCompanyTags();
dataLoader.loadReferenceTags();
$('#tag-name').val('');
utils.notify('Tag saved successfully!');
} else {
utils.notify(referenceData.message || 'Error saving reference tag', 'danger');
}
} else {
utils.notify(companyData.message || 'Error saving company tag', 'danger');
}
} catch (error) {
utils.notify('Error saving tag', 'danger');
}
utils.getModalInstance('#saveTagModal')?.hide();
});
});
this.setupModal('#deleteCompanyModal', () => {
const $select = $('#company-select');
api.get('get_company_tags')
.then(data => {
$select.html('<option value="">Select a company to delete</option>');
data.forEach(tag => $select.append(`<option value="${tag.id}">${tag.name}</option>`));
})
.catch(() => utils.notify('Error loading company tags', 'danger'));
$('#confirm-delete-company').on('click', async () => {
const id = $select.val();
if (!id) return;
try {
const data = await api.post('delete_company_tag', { id });
if (data.success) {
dataLoader.loadCompanyTags();
dataLoader.loadReferenceTags();
$select.val('');
utils.notify('Company deleted successfully!');
} else {
utils.notify(data.message || 'Error deleting company', 'danger');
}
} catch (error) {
utils.notify('Error deleting company', 'danger');
}
utils.getModalInstance('#deleteCompanyModal')?.hide();
});
});
this.setupModal('#addProductModal', () => {
$('#confirm-add-product').on('click', async () => {
if (!state.currentPageId) return utils.notify('No page selected', 'warning');
const productData = {
product_code: $('#product-code').val(),
tag_ref: $('#tag-ref').val(),
hs_code: $('#hs-code').val(),
description: $('#description').val(),
quantity: $('#quantity').val(),
unit_kind: $('#unit-kind').val(),
packages: $('#packages').val(),
length: $('#length').val(),
width: $('#width').val(),
height: $('#height').val(),
net_weight: $('#net-weight').val(),
gross_weight: $('#gross-weight').val(),
unit_price: $('#unit-price').val(),
currency: $('#unit-price').parent().find('select').val(),
exclude_invoice: $('#exclude-invoice').is(':checked') ? 1 : 0,
sort_order: $('#productTableBody tr:not(#empty-table-row)').length
};
if (!productData.product_code || !productData.hs_code || !productData.description) {
return utils.notify('Please fill in all required fields', 'warning');
}
try {
const formData = new FormData();
formData.append('page_id', state.currentPageId);
Object.entries(productData).forEach(([k, v]) => formData.append(k, v));
const data = await api.postForm('add_product', formData);
if (data.success) {
$('#add-product-form')[0].reset();
utils.getModalInstance('#addProductModal')?.hide();
dataLoader.loadProducts();
utils.notify('Product added successfully!');
} else {
utils.notify(data.message || 'Error adding product', 'danger');
}
} catch (error) {
utils.notify('Error adding product', 'danger');
}
});
});
this.setupModal('#addHsCodeModal', () => {
$('#confirm-add-hs-code').on('click', async () => {
const code = $('#new-hs-code').val();
const title = $('#new-hs-title').val();
if (code && title) {
try {
const data = await api.post('add_hs_code', {
code: encodeURIComponent(code),
title: encodeURIComponent(title)
});
if (data.success) {
$('#new-hs-code').val('');
$('#new-hs-title').val('');
const addHsModal = utils.getModalInstance('#addHsCodeModal');
if (addHsModal) {
addHsModal.hide();
// Wait for the modal to be fully hidden before showing the next one
addHsModal._element.addEventListener('hidden.bs.modal', function handler() {
addHsModal._element.removeEventListener('hidden.bs.modal', handler);
const addProductModal = utils.getModalInstance('#addProductModal');
if (addProductModal) {
addProductModal.show();
}
utils.notify('HS Code added successfully!');
});
} else {
utils.notify('HS Code added successfully!');
}
} else {
utils.notify(data.message || 'Error adding HS code', 'danger');
}
} catch (error) {
utils.notify('Error adding HS code', 'danger');
}
}
});
});
},
setupModal(selector, setupFn) {
const modal = utils.getModalInstance(selector);
setupFn();
return modal;
}
};
// Form handler
const formHandler = {
init() {
// Company tags
let activeTextarea = $('#consignee').addClass('highlighted');
$('#consignee').on('focus', function() {
activeTextarea = $(this).addClass('highlighted');
$('#buyer').removeClass('highlighted');
});
$('#buyer').on('focus', function() {
activeTextarea = $(this).addClass('highlighted');
$('#consignee').removeClass('highlighted');
});
$('#company-tags').on('click', '.tag', function() {
$('.tag').removeClass('active');
$(this).addClass('active');
if (activeTextarea) {
activeTextarea.val($(this).data('value'));
saveField(activeTextarea.attr('id'), activeTextarea.val());
}
state.currentCompanyId = $(this).data('id');
});
// Reference tags
config.references.forEach(id => {
$(`#${id}`).on('focus', function() {
config.references.forEach(i => $(`#${i}`).removeClass('highlighted'));
$(this).addClass('highlighted');
});
});
$('#reference-tags').on('click', '.tag', function() {
const tagData = $(this).data('values');
if (tagData) {
const values = JSON.parse(tagData);
const hasNonEmptyFields = config.references.some(id => $(`#${id}`).val().trim() !== '');
if (hasNonEmptyFields) {
Swal.fire({
title: 'Overwrite fields?',
text: 'Some fields are not empty. Do you want to overwrite them?',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Yes, overwrite'
}).then(result => {
if (result.isConfirmed) {
config.references.forEach(id => {
const $input = $(`#${id}`);
if ($input.length && values[id]) {
$input.val(values[id]);
saveField(id, values[id]);
}
});
}
});
} else {
config.references.forEach(id => {
const $input = $(`#${id}`);
if ($input.length && values[id]) {
$input.val(values[id]);
saveField(id, values[id]);
}
});
}
}
$('.tag').removeClass('active');
$(this).addClass('active');
state.currentReferenceId = $(this).data('id');
});
// HS code reference
$('#hs-code-reference').on('click', function(e) {
const $item = $(e.target).closest('.hs-code-item');
if ($item.length) {
const code = $item.find('strong').text();
$('#hs-code').val(code);
saveField('hs-code', code);
}
if ($(e.target).closest('.delete-hs-code').length) {
e.preventDefault();
const id = $item.data('id');
Swal.fire({
title: 'Delete HS Code?',
text: 'Are you sure you want to delete this HS code?',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Yes, delete it'
}).then(async result => {
if (result.isConfirmed) {
try {
const data = await api.post('delete_hs_code', { id });
if (data.success) {
dataLoader.loadHsCodes();
utils.notify('HS code deleted successfully!');
} else {
utils.notify(data.message || 'Error deleting HS code', 'danger');
}
} catch (error) {
utils.notify('Error deleting HS code', 'danger');
}
}
});
}
});
// Use the modal instance to show the addHsCodeModal
$('#add-hs-code').on('click', () => {
// Check if the product modal is open
const productModal = utils.getModalInstance('#addProductModal');
if (productModal && productModal._isShown) {
// Hide the product modal temporarily
productModal.hide();
// Store that we need to reopen it later
$('#addHsCodeModal').data('reopenProductModal', true);
}
const modal = utils.getModalInstance('#addHsCodeModal');
if (modal) {
modal.show();
} else {
// Fallback to creating a new instance if needed
new bootstrap.Modal('#addHsCodeModal').show();
}
});
// Clear section buttons
$('.clear-section-btn').on('click', function() {
const $section = $(this).closest('.content-section');
Swal.fire({
title: 'Clear section?',
text: 'Are you sure you want to clear this section? This will remove all data in this section.',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Yes, clear it'
}).then(result => {
if (result.isConfirmed) {
$section.find('input, textarea, select').each(function() {
if (this.type === 'date' && this.id === 'export-invoice-date') {
this.value = new Date().toISOString().split('T')[0];
} else if (this.type === 'checkbox') {
this.checked = false;
} else {
this.value = '';
}
saveField(this.id, this.value);
});
const $notes = $section.find('#notes');
if ($notes.length) {
$notes.val('');
saveField($notes.attr('id'), $notes.val());
}
utils.notify('Section cleared successfully!');
}
});
});
// Live saving
$('input, textarea, select').not('.modal *').on('input change', function() {
saveField(this.id, this.value);
});
}
};
// Product table
const productTable = {
init() {
const $tbody = $('#productTableBody');
if ($tbody.length) {
new Sortable($tbody[0], {
handle: '.drag-handle',
animation: 150,
ghostClass: 'sortable-ghost',
async onEnd() {
const $rows = $tbody.find('tr:not(#empty-table-row)');
$rows.each(async (i, row) => {
const id = $(row).data('id');
try {
const data = await api.post('update_product_sort_order', {
page_id: state.currentPageId,
product_id: id,
sort_order: i
});
if (!data.success) console.error('Error updating product sort order:', data.message);
} catch (error) {
console.error('Error updating product sort order:', error);
}
});
updateStats();
}
});
$tbody.on('click', this.handleActions.bind(this));
}
},
async handleActions(e) {
const $row = $(e.target).closest('tr');
const id = $row.data('id');
// Edit
if ($(e.target).closest('.edit-product-btn').length) {
try {
const data = await api.get('get_products', { page_id: state.currentPageId });
const product = data.find(p => p.id == id);
if (product) {
Object.entries({
'product-code': product.product_code,
'tag-ref': product.tag_ref,
'hs-code': product.hs_code,
'description': product.description,
'quantity': product.quantity,
'packages': product.packages,
'unit-kind': product.unit_kind,
'net-weight': product.net_weight,
'gross-weight': product.gross_weight,
'length': product.length,
'width': product.width,
'height': product.height,
'unit-price': product.unit_price
}).forEach(([selector, value]) => $(`#${selector}`).val(value));
$('#unit-price').parent().find('select').val(product.currency);
$('#exclude-invoice').prop('checked', product.exclude_invoice == 1);
$('#add-product-form').attr('data-product-id', id);
}
} catch (error) {
utils.notify('Error loading product data', 'danger');
}
}
// Duplicate
if ($(e.target).closest('.duplicate-product-btn').length) {
try {
const data = await api.get('get_products', { page_id: state.currentPageId });
const product = data.find(p => p.id == id);
if (product) {
const formData = new FormData();
formData.append('page_id', state.currentPageId);
formData.append('product_code', product.product_code + ' (Copy)');
Object.entries(product).forEach(([k, v]) => {
if (k !== 'id' && k !== 'product_code') formData.append(k, v);
});
formData.append('sort_order', $('#productTableBody tr:not(#empty-table-row)').length);
const result = await api.postForm('add_product', formData);
if (result.success) {
dataLoader.loadProducts();
utils.notify('Product duplicated successfully!');
} else {
utils.notify(result.message || 'Error duplicating product', 'danger');
}
}
} catch (error) {
utils.notify('Error duplicating product', 'danger');
}
}
// Delete
if ($(e.target).closest('.delete-product-btn').length) {
Swal.fire({
title: 'Delete product?',
text: 'Are you sure you want to delete this product? This action cannot be undone.',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Yes, delete it'
}).then(async result => {
if (result.isConfirmed) {
try {
const data = await api.post('delete_product', {
page_id: state.currentPageId,
id
});
if (data.success) {
dataLoader.loadProducts();
utils.notify('Product deleted successfully!');
} else {
utils.notify(data.message || 'Error deleting product', 'danger');
}
} catch (error) {
utils.notify('Error deleting product', 'danger');
}
}
});
}
}
};
// Action buttons
const actionButtons = {
init() {
// New page
$('#new-page-btn').on('click', () => {
const modal = utils.getModalInstance('#createPageModal');
if (modal) modal.show();
else new bootstrap.Modal('#createPageModal').show();
});
// Save
$('#save-btn').on('click', () => {
if (!state.currentPageId) return utils.notify('No page selected', 'warning');
$('input, textarea, select').not('.modal *').each(function() {
saveField(this.id, this.value);
});
utils.notify('All data saved successfully!');
});
// Print buttons
$('#print-invoice, #print-packing, #print-both').on('click', function(e) {
e.preventDefault();
if (!state.currentPageId) return utils.notify('No page selected', 'warning');
const messages = {
'print-invoice': 'Print Invoice action triggered!',
'print-packing': 'Print Packing List action triggered!',
'print-both': 'Print Invoice and Packing List action triggered!'
};
utils.notify(messages[$(this).attr('id')]);
});
// Clear all
$('#clear-all-btn').on('click', function() {
if (!state.currentPageId) return utils.notify('No page selected', 'warning');
Swal.fire({
title: 'Clear all data?',
text: 'Are you sure you want to clear all data? This action cannot be undone.',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Yes, clear all'
}).then(async result => {
if (result.isConfirmed) {
try {
const data = await api.get('get_products', { page_id: state.currentPageId });
// Delete all products
for (const product of data) {
await api.post('delete_product', {
page_id: state.currentPageId,
id: product.id
});
}
dataLoader.loadProducts();
// Reset form fields
$('#shipper, #shipment-type, #destination-country, #consignee, #buyer, #rma-ref, #buyer-reference, #packing-list-ref, #export-invoice-number, #method-of-delivery, #delivery-term, #terms').val('');
$('#dispatch-method').val(config.defaults.dispatchMethod);
$('#origin-country').val(config.defaults.originCountry);
$('#duty-exemption').val(config.defaults.dutyExemption);
$('#factory-number').val(config.defaults.factoryNumber);
$('#export-invoice-date').val(new Date().toISOString().split('T')[0]);
$('#signatory-company').val(config.defaults.signatoryCompany);
$('#authorized-signatory').val(config.defaults.authorizedSignatory);
// Reset IDs
state.currentShipmentId = null;
state.currentDocumentId = null;
state.currentCompanyId = null;
state.currentReferenceId = null;
utils.notify('All data cleared successfully!');
} catch (error) {
utils.notify('Error clearing data', 'danger');
}
}
});
});
// Undo
$('#undo-btn').on('click', async function() {
if (!state.currentPageId) return utils.notify('No page selected', 'warning');
try {
const data = await api.get('undo', { page_id: state.currentPageId });
if (data.success) {
await Promise.all([
dataLoader.loadCompanyTags(),
dataLoader.loadReferenceTags(),
dataLoader.loadHsCodes(),
dataLoader.loadProducts(),
dataLoader.loadShipment(),
dataLoader.loadDocument(),
dataLoader.loadNotes()
]);
utils.notify('Last action undone successfully!');
} else {
utils.notify('Nothing to undo!', 'warning');
}
} catch (error) {
utils.notify('Error undoing action', 'danger');
}
});
}
};
// UI components
const uiComponents = {
init() {
// Sidebar
const $body = $('body');
const $sidebar = $('#sidebar');
const $newPageBtn = $('#new-page-btn');
let collapseTimeout;
let sidebarState = localStorage.getItem('sidebar-state') || 'auto';
if (sidebarState === 'collapsed') $body.addClass('sidebar-collapsed');
$('#hamburger').on('click', () => {
if (window.innerWidth >= 992) {
const collapsed = $body.toggleClass('sidebar-collapsed').hasClass('sidebar-collapsed');
localStorage.setItem('sidebar-collapsed', collapsed ? '1' : '0');
sidebarState = collapsed ? 'collapsed' : 'expanded';
localStorage.setItem('sidebar-state', sidebarState);
} else {
new bootstrap.Offcanvas('#offcanvasSidebar').show();
}
});
if ($sidebar.length) {
$sidebar.on('mouseenter', () => {
clearTimeout(collapseTimeout);
if (window.innerWidth >= 992 && $body.hasClass('sidebar-collapsed') && sidebarState === 'auto') {
$body.removeClass('sidebar-collapsed');
}
});
$sidebar.on('mouseleave', () => {
if (window.innerWidth >= 992 && sidebarState === 'auto') {
collapseTimeout = setTimeout(() => $body.addClass('sidebar-collapsed'), 500);
}
});
}
if ($newPageBtn.length) {
const updateBtn = () => {
if ($body.hasClass('sidebar-collapsed')) {
$newPageBtn.html('<i class="bi bi-plus-circle"></i>').attr('title', 'New Page');
} else {
$newPageBtn.html('<i class="bi bi-plus-circle me-2"></i><span class="nav-label">New Page</span>').removeAttr('title');
}
};
updateBtn();
new MutationObserver(updateBtn).observe($body[0], { attributes: true, attributeFilter: ['class'] });
}
// Theme toggle
const $html = $('html');
const theme = localStorage.getItem('bs-theme') ||
(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
$html.attr('data-bs-theme', theme);
const updateIcons = t => {
const icon = t === 'dark' ? 'bi-sun' : 'bi-moon';
const inverse = t === 'dark' ? 'bi-moon' : 'bi-sun';
const text = t === 'dark' ? 'Light Mode' : 'Dark Mode';
$('#dark-mode-btn i').removeClass(inverse).addClass(icon);
$('#dark-mode-btn .nav-label').text(text);
$('#dark-mode-offcanvas i').removeClass(inverse).addClass(icon);
$('#dark-mode-offcanvas span').text(text);
};
updateIcons(theme);
$('#dark-mode-btn, #dark-mode-offcanvas').on('click', () => {
const newTheme = $html.attr('data-bs-theme') === 'dark' ? 'light' : 'dark';
$html.attr('data-bs-theme', newTheme);
localStorage.setItem('bs-theme', newTheme);
updateIcons(newTheme);
});
}
};
// Save field function
async function saveField(fieldId, value) {
if (!state.currentPageId) return;
if (state.saveTimeout) clearTimeout(state.saveTimeout);
utils.saving(true);
state.saveTimeout = setTimeout(async () => {
let action = '';
let data = {};
// Determine action and data based on field
if (config.fields.shipment.includes(fieldId)) {
action = 'update_shipment';
data = {
id: state.currentShipmentId || 0,
shipper: $('#shipper').val(),
dispatch_method: $('#dispatch-method').val(),
shipment_type: $('#shipment-type').val(),
origin_country: $('#origin-country').val(),
destination_country: $('#destination-country').val(),
duty_exemption_no: $('#duty-exemption').val(),
factory_number: $('#factory-number').val(),
consignee: $('#consignee').val(),
buyer: $('#buyer').val(),
company_tag_id: state.currentCompanyId
};
} else if (config.fields.document.includes(fieldId)) {
action = 'update_document';
data = {
id: state.currentDocumentId || 0,
rma_ref: $('#rma-ref').val(),
buyer_reference: $('#buyer-reference').val(),
packing_list_ref: $('#packing-list-ref').val(),
export_invoice_number: $('#export-invoice-number').val(),
export_invoice_date: $('#export-invoice-date').val(),
method_of_delivery: $('#method-of-delivery').val(),
delivery_term: $('#delivery-term').val(),
terms: $('#terms').val(),
signatory_company: $('#signatory-company').val(),
authorized_signatory: $('#authorized-signatory').val(),
reference_tag_id: state.currentReferenceId
};
} else if (fieldId === 'notes') {
action = 'update_notes';
data = { id: state.currentNotesId || 0, content: value };
}
if (action) {
try {
const formData = new FormData();
formData.append('page_id', state.currentPageId);
Object.entries(data).forEach(([k, v]) => formData.append(k, v));
const result = await api.postForm(action, formData);
if (result.success) {
if (action === 'update_shipment' && !state.currentShipmentId) dataLoader.loadShipment();
else if (action === 'update_document' && !state.currentDocumentId) dataLoader.loadDocument();
else if (action === 'update_notes' && !state.currentNotesId) dataLoader.loadNotes();
}
} catch (error) {
console.error('Error saving field:', error);
}
}
utils.saving(false);
}, 500);
}
// Update statistics
function updateStats() {
let totalPrice = 0, totalNetWeight = 0, totalGrossWeight = 0, totalVolume = 0, totalQuantity = 0, totalPackages = 0;
$('#productTableBody tr:not(#empty-table-row)').each(function() {
const $row = $(this), $cells = $row.find('td');
// Quantity
const quantity = parseFloat($cells.eq(3).text()) || 0;
totalQuantity += quantity;
// Packages
const packingText = $cells.eq(4).text();
const packages = parseFloat(packingText.split(' ')[0]) || 0;
totalPackages += packages;
// Weights
const $weightContainer = $row.find('.weight-container');
const netWeightText = $weightContainer.find('div').eq(0).text().replace('Net: ', '').replace(' KG', '');
const grossWeightText = $weightContainer.find('div').eq(1).text().replace('Gross: ', '').replace(' KG', '');
const netWeight = parseFloat(netWeightText) || 0;
const grossWeight = parseFloat(grossWeightText) || 0;
totalNetWeight += netWeight;
totalGrossWeight += grossWeight;
// Dimensions and volume
const $dimensionsContainer = $row.find('.dimensions-container');
const dimensionsText = $dimensionsContainer.find('div').eq(0).text();
const dimensions = dimensionsText.split('x');
if (dimensions.length === 3) {
const length = parseFloat(dimensions[0]) || 0;
const width = parseFloat(dimensions[1]) || 0;
const height = parseFloat(dimensions[2]) || 0;
totalVolume += length * width * height;
}
// Price
const priceText = $cells.eq(7).text();
const currency = priceText.split(' ')[0];
let price = parseFloat(priceText.split(' ')[1].replace(/,/g, '')) || 0;
if (config.rates[currency]) price *= config.rates[currency];
totalPrice += price;
});
// Update table footer
$('#total-quantity').text(totalQuantity.toFixed(2));
$('#total-packages').text(totalPackages);
$('#total-net-weight').text(totalNetWeight.toFixed(2));
$('#total-gross-weight').text(totalGrossWeight.toFixed(2));
$('#total-volume').text(totalVolume.toFixed(3));
$('#total-price').text('$' + totalPrice.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ','));
// Update statistics panel
$('#stats-total-price').text('$' + totalPrice.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ','));
$('#stats-total-weight').text(totalNetWeight.toFixed(2) + '/' + totalGrossWeight.toFixed(2) + ' kg');
$('#stats-total-volume').text(totalVolume.toFixed(3) + ' m³');
$('#stats-total-packages').text(totalPackages);
}
// Initialize app
$(document).ready(() => {
// Initialize Bootstrap components
const dropdownTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="dropdown"]'));
dropdownTriggerList.map(function (dropdownTriggerEl) {
return new bootstrap.Dropdown(dropdownTriggerEl);
});
// Initialize tooltips
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
// Initialize popovers
const popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
popoverTriggerList.map(function (popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl);
});
// Initialize offcanvas
const offcanvasTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="offcanvas"]'));
offcanvasTriggerList.map(function (offcanvasTriggerEl) {
return new bootstrap.Offcanvas(offcanvasTriggerEl);
});
// Initialize app components
dataLoader.loadPages();
uiComponents.init();
modalManager.init();
formHandler.init();
productTable.init();
actionButtons.init();
$('#export-invoice-date').val(new Date().toISOString().split('T')[0]);
// Set up event listener for reopening product modal after HS code modal closes
$('#addHsCodeModal').on('hidden.bs.modal', function() {
if ($(this).data('reopenProductModal')) {
const productModal = utils.getModalInstance('#addProductModal');
if (productModal) {
productModal.show();
}
$(this).removeData('reopenProductModal');
}
});
});
})();
<?php
// Start session
session_start();
// Database configuration
define('DB_HOST', 'localhost');
define('DB_NAME', 'product_management');
define('DB_USER', 'root');
define('DB_PASS', '');
// Create PDO connection
try {
// First, try to connect with the database
$pdo = new PDO("mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4", DB_USER, DB_PASS);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
// If database doesn't exist, redirect to setup
if ($e->getCode() == 1049) {
header('Location: setup.php');
exit;
} else {
die("ERROR: Could not connect. " . $e->getMessage());
}
}
<?php
require_once 'config.php';
// Function to get all pages
function getPages()
{
global $pdo;
$stmt = $pdo->query("SELECT * FROM pages ORDER BY updated_at DESC");
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// Function to get a specific page
function getPage($id)
{
global $pdo;
$stmt = $pdo->prepare("SELECT id, title, subtitle, defaults, created_at, updated_at FROM pages WHERE id = ?");
$stmt->execute([$id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row) {
$row['defaults'] = $row['defaults'] ? json_decode($row['defaults'], true) : new stdClass();
}
return $row;
}
// Function to create a new page
function createPage($title, $subtitle, $defaults = null)
{
global $pdo;
$defaults_json = $defaults ? $defaults : null; // expect string or null
$stmt = $pdo->prepare("INSERT INTO pages (title, subtitle, defaults, created_at) VALUES (?, ?, ?, NOW())");
$stmt->execute([$title, $subtitle, $defaults_json]);
return $pdo->lastInsertId();
}
// Function to update a page
function updatePage($pageId, $title, $subtitle)
{
global $pdo;
$stmt = $pdo->prepare("UPDATE pages SET title = ?, subtitle = ? WHERE id = ?");
$stmt->execute([$title, $subtitle, $pageId]);
return $stmt->rowCount();
}
// Function to delete a page
function deletePage($pageId)
{
global $pdo;
$stmt = $pdo->prepare("DELETE FROM pages WHERE id = ?");
$stmt->execute([$pageId]);
return $stmt->rowCount();
}
// Function to get company tags
function getCompanyTags()
{
global $pdo;
$stmt = $pdo->query("SELECT * FROM company_tags ORDER BY name");
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// Function to get reference tags
function getReferenceTags()
{
global $pdo;
$stmt = $pdo->query("SELECT * FROM reference_tags ORDER BY name");
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// Function to get HS codes
function getHsCodes()
{
global $pdo;
$stmt = $pdo->query("SELECT * FROM hs_codes ORDER BY code");
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// Function to get products for a specific page
function getProducts($pageId)
{
global $pdo;
$stmt = $pdo->prepare("SELECT * FROM products WHERE page_id = ? ORDER BY sort_order");
$stmt->execute([$pageId]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// Function to get shipment data for a specific page
function getShipment($pageId)
{
global $pdo;
$stmt = $pdo->prepare("SELECT * FROM shipments WHERE page_id = ? ORDER BY id DESC LIMIT 1");
$stmt->execute([$pageId]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
// Function to get document data for a specific page
function getDocument($pageId)
{
global $pdo;
$stmt = $pdo->prepare("SELECT * FROM documents WHERE page_id = ? ORDER BY id DESC LIMIT 1");
$stmt->execute([$pageId]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
// Function to get notes for a specific page
function getNotes($pageId)
{
global $pdo;
$stmt = $pdo->prepare("SELECT * FROM notes WHERE page_id = ? ORDER BY id DESC LIMIT 1");
$stmt->execute([$pageId]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
// Function to add company tag
function addCompanyTag($name)
{
global $pdo;
$stmt = $pdo->prepare("INSERT INTO company_tags (name) VALUES (?)");
$stmt->execute([$name]);
return $pdo->lastInsertId();
}
// Function to add reference tag
function addReferenceTag($data)
{
global $pdo;
$stmt = $pdo->prepare("INSERT INTO reference_tags (name, rma_ref, buyer_reference, packing_list_ref, export_invoice_number, export_invoice_date, method_of_delivery, delivery_term, terms, signatory_company, authorized_signatory) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([
$data['name'],
$data['rma_ref'],
$data['buyer_reference'],
$data['packing_list_ref'],
$data['export_invoice_number'],
$data['export_invoice_date'],
$data['method_of_delivery'],
$data['delivery_term'],
$data['terms'],
$data['signatory_company'],
$data['authorized_signatory']
]);
return $pdo->lastInsertId();
}
// Function to add HS code
function addHsCode($code, $title)
{
global $pdo;
$stmt = $pdo->prepare("INSERT INTO hs_codes (code, title) VALUES (?, ?)");
$stmt->execute([$code, $title]);
return $pdo->lastInsertId();
}
// Function to add product
function addProduct($data)
{
global $pdo;
$stmt = $pdo->prepare("INSERT INTO products (page_id, product_code, tag_ref, hs_code, description, quantity, unit_kind, packages, length, width, height, net_weight, gross_weight, unit_price, currency, exclude_invoice, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([
$data['page_id'],
$data['product_code'],
$data['tag_ref'],
$data['hs_code'],
$data['description'],
$data['quantity'],
$data['unit_kind'],
$data['packages'],
$data['length'],
$data['width'],
$data['height'],
$data['net_weight'],
$data['gross_weight'],
$data['unit_price'],
$data['currency'],
$data['exclude_invoice'],
$data['sort_order']
]);
return $pdo->lastInsertId();
}
// Function to update shipment
function updateShipment($data)
{
global $pdo;
$stmt = $pdo->prepare("UPDATE shipments SET shipper = ?, dispatch_method = ?, shipment_type = ?, origin_country = ?, destination_country = ?, duty_exemption_no = ?, factory_number = ?, consignee = ?, buyer = ?, company_tag_id = ? WHERE id = ?");
$stmt->execute([
$data['shipper'],
$data['dispatch_method'],
$data['shipment_type'],
$data['origin_country'],
$data['destination_country'],
$data['duty_exemption_no'],
$data['factory_number'],
$data['consignee'],
$data['buyer'],
$data['company_tag_id'],
$data['id']
]);
return $stmt->rowCount();
}
// Function to update document
function updateDocument($data)
{
global $pdo;
$stmt = $pdo->prepare("UPDATE documents SET rma_ref = ?, buyer_reference = ?, packing_list_ref = ?, export_invoice_number = ?, export_invoice_date = ?, method_of_delivery = ?, delivery_term = ?, terms = ?, signatory_company = ?, authorized_signatory = ?, reference_tag_id = ? WHERE id = ?");
$stmt->execute([
$data['rma_ref'],
$data['buyer_reference'],
$data['packing_list_ref'],
$data['export_invoice_number'],
$data['export_invoice_date'],
$data['method_of_delivery'],
$data['delivery_term'],
$data['terms'],
$data['signatory_company'],
$data['authorized_signatory'],
$data['reference_tag_id'],
$data['id']
]);
return $stmt->rowCount();
}
// Function to update notes
function updateNotes($content, $id)
{
global $pdo;
$stmt = $pdo->prepare("UPDATE notes SET content = ? WHERE id = ?");
$stmt->execute([$content, $id]);
return $stmt->rowCount();
}
// Function to delete company tag
function deleteCompanyTag($id)
{
global $pdo;
$stmt = $pdo->prepare("DELETE FROM company_tags WHERE id = ?");
$stmt->execute([$id]);
return $stmt->rowCount();
}
// Function to delete reference tag
function deleteReferenceTag($id)
{
global $pdo;
$stmt = $pdo->prepare("DELETE FROM reference_tags WHERE id = ?");
$stmt->execute([$id]);
return $stmt->rowCount();
}
// Function to delete HS code
function deleteHsCode($id)
{
global $pdo;
$stmt = $pdo->prepare("DELETE FROM hs_codes WHERE id = ?");
$stmt->execute([$id]);
return $stmt->rowCount();
}
// Function to delete product
function deleteProduct($id)
{
global $pdo;
$stmt = $pdo->prepare("DELETE FROM products WHERE id = ?");
$stmt->execute([$id]);
return $stmt->rowCount();
}
// Function to update product
function updateProduct($data)
{
global $pdo;
$stmt = $pdo->prepare("UPDATE products SET product_code = ?, tag_ref = ?, hs_code = ?, description = ?, quantity = ?, unit_kind = ?, packages = ?, length = ?, width = ?, height = ?, net_weight = ?, gross_weight = ?, unit_price = ?, currency = ?, exclude_invoice = ? WHERE id = ?");
$stmt->execute([
$data['product_code'],
$data['tag_ref'],
$data['hs_code'],
$data['description'],
$data['quantity'],
$data['unit_kind'],
$data['packages'],
$data['length'],
$data['width'],
$data['height'],
$data['net_weight'],
$data['gross_weight'],
$data['unit_price'],
$data['currency'],
$data['exclude_invoice'],
$data['id']
]);
return $stmt->rowCount();
}
// Function to update product sort order
function updateProductSortOrder($productId, $sortOrder)
{
global $pdo;
$stmt = $pdo->prepare("UPDATE products SET sort_order = ? WHERE id = ?");
$stmt->execute([$sortOrder, $productId]);
return $stmt->rowCount();
}
// Function to add history entry
function addHistory($pageId, $action, $data)
{
global $pdo;
$stmt = $pdo->prepare("INSERT INTO history (page_id, action, data) VALUES (?, ?, ?)");
$stmt->execute([$pageId, $action, json_encode($data)]);
return $pdo->lastInsertId();
}
// Function to get history for a specific page
function getHistory($pageId)
{
global $pdo;
$stmt = $pdo->prepare("SELECT * FROM history WHERE page_id = ? ORDER BY id DESC LIMIT 50");
$stmt->execute([$pageId]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// Function to undo last action for a specific page
function undoLastAction($pageId)
{
global $pdo;
$stmt = $pdo->query("SELECT * FROM history WHERE page_id = ? ORDER BY id DESC LIMIT 1");
$stmt->execute([$pageId]);
$lastAction = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$lastAction) {
return false;
}
$action = $lastAction['action'];
$data = json_decode($lastAction['data'], true);
switch ($action) {
case 'add_product':
deleteProduct($data['id']);
break;
case 'update_product':
// Restore previous product data
$stmt = $pdo->prepare("SELECT * FROM products WHERE id = ?");
$stmt->execute([$data['id']]);
$oldData = $stmt->fetch(PDO::FETCH_ASSOC);
$stmt = $pdo->prepare("UPDATE products SET product_code = ?, tag_ref = ?, hs_code = ?, description = ?, quantity = ?, unit_kind = ?, packages = ?, length = ?, width = ?, height = ?, net_weight = ?, gross_weight = ?, unit_price = ?, currency = ?, exclude_invoice = ? WHERE id = ?");
$stmt->execute([
$data['old_data']['product_code'],
$data['old_data']['tag_ref'],
$data['old_data']['hs_code'],
$data['old_data']['description'],
$data['old_data']['quantity'],
$data['old_data']['unit_kind'],
$data['old_data']['packages'],
$data['old_data']['length'],
$data['old_data']['width'],
$data['old_data']['height'],
$data['old_data']['net_weight'],
$data['old_data']['gross_weight'],
$data['old_data']['unit_price'],
$data['old_data']['currency'],
$data['old_data']['exclude_invoice'],
$data['id']
]);
break;
case 'delete_product':
// Restore deleted product
$stmt = $pdo->prepare("INSERT INTO products (product_code, tag_ref, hs_code, description, quantity, unit_kind, packages, length, width, height, net_weight, gross_weight, unit_price, currency, exclude_invoice, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([
$data['product_code'],
$data['tag_ref'],
$data['hs_code'],
$data['description'],
$data['quantity'],
$data['unit_kind'],
$data['packages'],
$data['length'],
$data['width'],
$data['height'],
$data['net_weight'],
$data['gross_weight'],
$data['unit_price'],
$data['currency'],
$data['exclude_invoice'],
$data['sort_order']
]);
break;
// Add more cases as needed
}
// Delete the history entry
$stmt = $pdo->prepare("DELETE FROM history WHERE id = ?");
$stmt->execute([$lastAction['id']]);
return true;
}
<?php
require_once 'config.php';
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Product Management System</title>
<!-- Inter font from Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<!-- Bootstrap CSS (stable 5.3.x CDN) -->
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css" rel="stylesheet">
<!-- Custom CSS -->
<link href="assets/css/styles.css" rel="stylesheet">
</head>
<body>
<!-- Mobile Offcanvas Sidebar -->
<div class="offcanvas offcanvas-start" tabindex="-1" id="offcanvasSidebar" aria-labelledby="offcanvasSidebarLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title d-lg-none" id="offcanvasSidebarLabel">Menu</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body p-0">
<nav class="nav flex-column">
<a class="nav-link d-flex align-items-center gap-3 active" href="#">
<i class="bi bi-house"></i>
<span>Dashboard</span>
</a>
<a class="nav-link d-flex align-items-center gap-3" href="#">
<i class="bi bi-gear"></i>
<span>Settings</span>
</a>
<a class="nav-link d-flex align-items-center gap-3" href="#">
<i class="bi bi-person"></i>
<span>Profile</span>
</a>
</nav>
<div class="p-3 mt-auto">
<button id="dark-mode-offcanvas" class="btn btn-theme-toggle w-100 d-flex align-items-center justify-content-center gap-2">
<i class="bi bi-moon"></i>
<span>Dark Mode</span>
</button>
</div>
</div>
</div>
<!-- Desktop Sidebar -->
<aside id="sidebar" class="bg-body-tertiary border-end d-none d-lg-flex flex-column" style="position:fixed; top:0; left:0; height:100vh; z-index:1030;">
<div class="d-flex align-items-center p-3 justify-content-end">
<!-- Space for logo/title if needed -->
</div>
<nav class="nav flex-column gap-1 px-3">
<!-- New page button -->
<button id="new-page-btn" class="new-page-btn" data-bs-toggle="modal" data-bs-target="#createPageModal">
<i class="bi bi-plus-circle me-2"></i>
<span class="nav-label">New Page</span>
</button>
<!-- Pages will be loaded here dynamically -->
<div id="pages-container"></div>
</nav>
<div class="mt-auto p-3">
<button id="dark-mode-btn" class="btn btn-theme-toggle w-100 d-flex align-items-center justify-content-center gap-2">
<i class="bi bi-moon"></i>
<span class="nav-label">Dark Mode</span>
</button>
</div>
</aside>
<!-- Main content area -->
<main id="main" class="min-vh-100">
<header id="main-header" class="bg-body-tertiary border-bottom d-flex align-items-center" style="gap:.75rem;">
<button id="hamburger" class="hamburger d-lg-inline-flex" aria-label="Toggle menu">
<i class="bi bi-list" style="font-size:1.15rem;"></i>
</button>
<h1 class="h5 mb-0 d-lg-none">Product Management</h1>
<div class="position-relative search-container flex-grow-1">
<input class="form-control" placeholder="Search..." />
<div class="search-dropdown bg-body-secondary p-3">
No results found.
</div>
</div>
<!-- Navbar Action Buttons -->
<div class="d-flex align-items-center gap-2">
<button class="navbar-action-btn" id="undo-btn" title="Undo">
<i class="bi bi-arrow-counterclockwise"></i>
</button>
<button class="navbar-action-btn" id="save-btn" title="Save">
<i class="bi bi-save"></i>
</button>
<div class="dropdown print-dropdown">
<button class="navbar-action-btn" id="print-btn" title="Print" data-bs-toggle="dropdown">
<i class="bi bi-printer"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="#" id="print-invoice">Print Invoice</a></li>
<li><a class="dropdown-item" href="#" id="print-packing">Print Packing List</a></li>
<li><a class="dropdown-item" href="#" id="print-both">Print Invoice and Packing List</a></li>
</ul>
</div>
<button class="navbar-action-btn clear-all-btn" id="clear-all-btn" title="Clear All Data">
<i class="bi bi-trash3"></i>
</button>
</div>
</header>
<div class="container-fluid">
<!-- Section with title, subtitle, and action buttons -->
<div class="p-4 mb-4">
<div class="section-header">
<div>
<h2 class="section-title" id="section-title">Product Details</h2>
<p class="section-subtitle" id="section-subtitle">Manage your product information</p>
</div>
<div class="section-actions">
<button class="btn btn-sm btn-outline-secondary" id="edit-btn" data-bs-toggle="modal" data-bs-target="#editModal">
<i class="bi bi-pencil"></i> Edit
</button>
<button class="btn btn-sm btn-outline-primary" id="duplicate-btn" data-bs-toggle="modal" data-bs-target="#duplicateModal">
<i class="bi bi-copy"></i> Duplicate
</button>
</div>
</div>
</div>
<!-- Two sections: 2/3 and 1/3 -->
<div class="row mb-4">
<div class="col-lg-8">
<div class="content-section section-2-3 p-4">
<button class="clear-section-btn" title="Clear this section">
<i class="bi bi-eraser"></i>
</button>
<h5>Shipment Information</h5>
<!-- Shipper and Dispatch Method -->
<div class="row mb-3-custom">
<div class="col-6">
<label for="shipper" class="form-label">Shipper</label>
<textarea class="form-control" id="shipper" rows="4"></textarea>
</div>
<div class="col-6">
<label for="dispatch-method" class="form-label">Dispatch Method</label>
<select class="form-select" id="dispatch-method">
<option>Road Freight</option>
<option>Air Freight</option>
<option>Sea Freight</option>
<option>Other</option>
</select>
<label for="shipment-type" class="form-label mt-3">Shipment Type</label>
<input type="text" class="form-control" id="shipment-type">
</div>
</div>
<!-- Origin Country, Destination Country, Duty Exemption No., Factory Number -->
<div class="row mb-3-custom">
<div class="col-6">
<label for="origin-country" class="form-label">Origin Country</label>
<select class="form-select" id="origin-country">
<option value="BH" selected>Bahrain</option>
<option value="KW">Kuwait</option>
<option value="OM">Oman</option>
<option value="QA">Qatar</option>
<option value="SA">Saudi Arabia</option>
<option value="AE">United Arab Emirates</option>
</select>
</div>
<div class="col-6">
<label for="destination-country" class="form-label">Destination Country</label>
<select class="form-select" id="destination-country">
<option value="">Select Country</option>
<option value="BH">Bahrain</option>
<option value="KW">Kuwait</option>
<option value="OM">Oman</option>
<option value="QA">Qatar</option>
<option value="SA">Saudi Arabia</option>
<option value="AE">United Arab Emirates</option>
</select>
</div>
</div>
<div class="row mb-3-custom">
<div class="col-6">
<label for="duty-exemption" class="form-label">Duty Exemption No.</label>
<input type="text" class="form-control" id="duty-exemption" value="5019">
</div>
<div class="col-6">
<label for="factory-number" class="form-label">Factory Number</label>
<input type="text" class="form-control" id="factory-number" value="693/1">
</div>
</div>
<!-- Consignee and Buyer -->
<div class="row mb-3-custom">
<div class="col-6">
<label for="consignee" class="form-label">Consignee</label>
<textarea class="form-control" id="consignee" rows="4"></textarea>
</div>
<div class="col-6">
<label for="buyer" class="form-label">Buyer</label>
<textarea class="form-control" id="buyer" rows="4"></textarea>
</div>
</div>
<!-- Tag System Section -->
<div class="tag-section">
<div class="d-flex justify-content-between align-items-center mb-2">
<label class="form-label mb-0">Company Tags</label>
<div class="dropdown">
<button class="three-dots" data-bs-toggle="dropdown">
<i class="bi bi-three-dots-vertical"></i>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#saveTagModal">Save Tag</a></li>
<li><a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#deleteCompanyModal">Delete Company</a></li>
</ul>
</div>
</div>
<div class="tag-container" id="company-tags">
<!-- Tags will be loaded via AJAX -->
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="content-section section-1-3 p-4">
<button class="clear-section-btn" title="Clear this section">
<i class="bi bi-eraser"></i>
</button>
<h5>Document References</h5>
<!-- Form fields in 1/2 layout -->
<div class="row mb-3">
<div class="col-6">
<label for="rma-ref" class="form-label">RMA Ref.</label>
<input type="text" class="form-control" id="rma-ref">
</div>
<div class="col-6">
<label for="buyer-reference" class="form-label">Buyer Reference</label>
<input type="text" class="form-control" id="buyer-reference">
</div>
</div>
<div class="row mb-3">
<div class="col-6">
<label for="packing-list-ref" class="form-label">Packing List Reference</label>
<input type="text" class="form-control" id="packing-list-ref">
</div>
<div class="col-6">
<label for="export-invoice-number" class="form-label">Export Invoice Number</label>
<input type="text" class="form-control" id="export-invoice-number">
</div>
</div>
<div class="row mb-3">
<div class="col-6">
<label for="export-invoice-date" class="form-label">Export Invoice Date</label>
<input type="date" class="form-control" id="export-invoice-date">
</div>
<div class="col-6">
<label for="method-of-delivery" class="form-label">Method Of Delivery</label>
<input type="text" class="form-control" id="method-of-delivery">
</div>
</div>
<div class="row mb-3">
<div class="col-6">
<label for="delivery-term" class="form-label">Delivery Term</label>
<input type="text" class="form-control" id="delivery-term">
</div>
<div class="col-6">
<label for="terms" class="form-label">Terms</label>
<input type="text" class="form-control" id="terms">
</div>
</div>
<div class="row mb-3">
<div class="col-6">
<label for="signatory-company" class="form-label">Signatory Company</label>
<input type="text" class="form-control" id="signatory-company" value="Mohamed Ramadhan">
</div>
<div class="col-6">
<label for="authorized-signatory" class="form-label">Authorized Signatory</label>
<input type="text" class="form-control" id="authorized-signatory" value="RMA Middle East W.L.L">
</div>
</div>
<!-- Tag System Section for 1/3 section -->
<div class="tag-section">
<div class="d-flex justify-content-between align-items-center mb-2">
<label class="form-label mb-0">Reference Tags</label>
<div class="dropdown">
<button class="three-dots" data-bs-toggle="dropdown">
<i class="bi bi-three-dots-vertical"></i>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#saveTagModal">Save Tag</a></li>
<li><a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#deleteCompanyModal">Delete Company</a></li>
</ul>
</div>
</div>
<div class="tag-container" id="reference-tags">
<!-- Tags will be loaded via AJAX -->
</div>
</div>
</div>
</div>
</div>
<!-- Product Table Section -->
<div class="row">
<div class="col-12">
<div class="content-section section-3-3 p-4">
<button class="clear-section-btn" title="Clear this section">
<i class="bi bi-eraser"></i>
</button>
<div class="d-flex justify-content-between align-items-center mb-3" style="margin-right: 40px;">
<h5 class="mb-0">Product Details</h5>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addProductModal">
<i class="bi bi-plus-circle"></i> Add Product
</button>
</div>
<div class="table-responsive">
<table class="table table-hover" id="productTable">
<thead>
<tr>
<th width="40"></th>
<th>Product Code</th>
<th>Description</th>
<th>Quantity</th>
<th>Packing</th>
<th>Weight</th>
<th>Dimensions</th>
<th>Price</th>
<th width="140">Actions</th>
</tr>
</thead>
<tbody id="productTableBody">
<!-- Products will be loaded via AJAX -->
</tbody>
<tfoot id="productTableFooter" style="display: none;">
<tr>
<td colspan="3" class="text-end"><strong>Totals:</strong></td>
<td id="total-quantity">0</td>
<td id="total-packages">0</td>
<td>
<div class="weight-container">
<div>Net: <span id="total-net-weight">0.00</span> KG</div>
<div>Gross: <span id="total-gross-weight">0.00</span> KG</div>
</div>
</td>
<td>
<div class="dimensions-container">
<div>CBM = <span id="total-volume">0.000</span> m³</div>
</div>
</td>
<td id="total-price">$0.00</td>
<td></td>
</tr>
</tfoot>
</table>
</div>
<!-- Statistics Section -->
<div class="row mt-4">
<div class="col-md-3">
<div class="stats-panel">
<h6 class="text-muted">Total Price</h6>
<h5 class="mb-0" id="stats-total-price">$0.00</h5>
</div>
</div>
<div class="col-md-3">
<div class="stats-panel">
<h6 class="text-muted">Total Weight (Net/Gross)</h6>
<h5 class="mb-0" id="stats-total-weight">0.00/0.00 kg</h5>
</div>
</div>
<div class="col-md-3">
<div class="stats-panel">
<h6 class="text-muted">Total Volume</h6>
<h5 class="mb-0" id="stats-total-volume">0.000 m³</h5>
</div>
</div>
<div class="col-md-3">
<div class="stats-panel">
<h6 class="text-muted">Total Packages</h6>
<h5 class="mb-0" id="stats-total-packages">0</h5>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Notes Section -->
<div class="row">
<div class="col-12">
<div class="content-section p-4">
<button class="clear-section-btn" title="Clear this section">
<i class="bi bi-eraser"></i>
</button>
<h5>Notes</h5>
<div class="notes-section">
<textarea class="form-control notes-textarea" id="notes" placeholder="Enter any additional notes here..."></textarea>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- Create Page Modal -->
<div class="modal fade" id="createPageModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Create New Page</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="create-page-form">
<div class="mb-3">
<label for="page-title" class="form-label">Title</label>
<input type="text" class="form-control" id="page-title" placeholder="Enter title" required>
</div>
<div class="mb-3">
<label for="page-subtitle" class="form-label">Subtitle</label>
<input type="text" class="form-control" id="page-subtitle" placeholder="Enter subtitle">
</div>
<div class="mb-3">
<label class="form-label">Page Type</label>
<div class="create-page-type">
<div class="page-type-option" data-type="invoice">
<div class="page-type-icon">
<i class="bi bi-file-earmark-text"></i>
</div>
<div>Invoice</div>
</div>
<div class="page-type-option" data-type="packing_list">
<div class="page-type-icon">
<i class="bi bi-box-seam"></i>
</div>
<div>Packing List</div>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="confirm-create-page">Create Page</button>
</div>
</div>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div class="modal fade" id="deleteModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirm Delete</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete this item? This action cannot be undone.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" id="confirm-delete">Delete</button>
</div>
</div>
</div>
</div>
<!-- Duplicate Modal -->
<div class="modal fade" id="duplicateModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Duplicate Item</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="duplicate-form">
<div class="mb-3">
<label for="duplicate-title" class="form-label">Title</label>
<input type="text" class="form-control" id="duplicate-title" placeholder="Enter title">
</div>
<div class="mb-3">
<label for="duplicate-subtitle" class="form-label">Subtitle</label>
<input type="text" class="form-control" id="duplicate-subtitle" placeholder="Enter subtitle">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="confirm-duplicate">Duplicate</button>
</div>
</div>
</div>
</div>
<!-- Edit Modal -->
<div class="modal fade" id="editModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Edit Item</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="edit-form">
<div class="mb-3">
<label for="edit-title" class="form-label">Title</label>
<input type="text" class="form-control" id="edit-title" placeholder="Enter title">
</div>
<div class="mb-3">
<label for="edit-subtitle" class="form-label">Subtitle</label>
<input type="text" class="form-control" id="edit-subtitle" placeholder="Enter subtitle">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="confirm-edit">Save Changes</button>
</div>
</div>
</div>
</div>
<!-- Save Tag Modal -->
<div class="modal fade" id="saveTagModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Save Tag</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="save-tag-form">
<div class="mb-3">
<label for="tag-name" class="form-label">Tag Name</label>
<input type="text" class="form-control" id="tag-name" placeholder="Enter tag name">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="confirm-save-tag">Save Tag</button>
</div>
</div>
</div>
</div>
<!-- Delete Company Modal -->
<div class="modal fade" id="deleteCompanyModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Delete Company</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="delete-company-form">
<div class="mb-3">
<label for="company-select" class="form-label">Select Company</label>
<select class="form-select" id="company-select">
<option value="">Select a company to delete</option>
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" id="confirm-delete-company">Delete Company</button>
</div>
</div>
</div>
</div>
<!-- Add Product Modal - Reworked layout -->
<div class="modal fade" id="addProductModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add Product</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-8">
<form id="add-product-form">
<div class="row mb-3">
<div class="col-md-6">
<label for="product-code" class="form-label">Product Code (SKU)</label>
<input type="text" class="form-control" id="product-code" placeholder="Enter product code">
</div>
<div class="col-md-6">
<label for="tag-ref" class="form-label">Tag / Internal Ref</label>
<input type="text" class="form-control" id="tag-ref" placeholder="Enter reference">
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label for="hs-code" class="form-label">HS Code</label>
<input type="text" class="form-control" id="hs-code" placeholder="Enter HS code">
</div>
<div class="col-md-6">
<label for="unit-price" class="form-label">Unit Price</label>
<div class="input-group">
<input type="number" step="0.01" class="form-control unit-price-input" id="unit-price" placeholder="Enter price">
<select class="form-select" style="max-width: 100px;">
<option>USD</option>
<option>BHD</option>
<option>EUR</option>
<option>GBP</option>
</select>
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-md-12">
<label for="description" class="form-label">Description</label>
<textarea class="form-control" id="description" rows="4" placeholder="Enter description"></textarea>
</div>
</div>
<div class="row mb-3">
<div class="col-md-4">
<label for="quantity" class="form-label">Quantity</label>
<input type="number" class="form-control" id="quantity" placeholder="Enter quantity">
</div>
<div class="col-md-4">
<label for="unit-kind" class="form-label">Unit Kind</label>
<select class="form-select" id="unit-kind">
<option>Box</option>
<option>Pkg</option>
<option>Pallet</option>
<option>Other</option>
</select>
</div>
<div class="col-md-4">
<label for="packages" class="form-label"># Packages</label>
<input type="number" class="form-control" id="packages" placeholder="Enter packages">
</div>
</div>
<div class="row mb-3">
<div class="col-md-4">
<label for="length" class="form-label">Length (m)</label>
<input type="number" step="0.01" class="form-control" id="length" placeholder="Enter length">
</div>
<div class="col-md-4">
<label for="width" class="form-label">Width (m)</label>
<input type="number" step="0.01" class="form-control" id="width" placeholder="Enter width">
</div>
<div class="col-md-4">
<label for="height" class="form-label">Height (m)</label>
<input type="number" step="0.01" class="form-control" id="height" placeholder="Enter height">
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label for="gross-weight" class="form-label">Gross Weight (KG)</label>
<input type="number" step="0.01" class="form-control" id="gross-weight" placeholder="Enter gross weight">
</div>
<div class="col-md-6">
<label for="net-weight" class="form-label">Net Weight (KG)</label>
<input type="number" step="0.01" class="form-control" id="net-weight" placeholder="Enter net weight">
</div>
</div>
<div class="row mb-3">
<div class="col-md-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="exclude-invoice">
<label class="form-check-label" for="exclude-invoice">
Exclude from Invoice
</label>
</div>
</div>
</div>
</form>
</div>
<div class="col-md-4">
<div class="mb-3 h-100 d-flex flex-column">
<div class="d-flex justify-content-between align-items-center mb-2">
<label class="form-label mb-0">HS Code Reference</label>
<button class="btn btn-sm btn-outline-primary" id="add-hs-code">
<i class="bi bi-plus"></i> Add
</button>
</div>
<div class="hs-code-reference flex-grow-1" id="hs-code-reference">
<!-- HS codes will be loaded via AJAX -->
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="confirm-add-product">Save Product</button>
</div>
</div>
</div>
</div>
<!-- Add HS Code Modal -->
<div class="modal fade" id="addHsCodeModal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static">
<div class="modal-dialog modal-dialog-centered add-hs-modal">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add HS Code</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="add-hs-code-form">
<div class="mb-3">
<label for="new-hs-code" class="form-label">HS Code</label>
<input type="text" class="form-control" id="new-hs-code" placeholder="Enter HS code">
</div>
<div class="mb-3">
<label for="new-hs-title" class="form-label">Title</label>
<input type="text" class="form-control" id="new-hs-title" placeholder="Enter title">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="confirm-add-hs-code">Add HS Code</button>
</div>
</div>
</div>
</div>
<!-- Confirmation Modal -->
<div class="modal fade" id="confirmModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirm Action</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="confirmModalBody">
Are you sure you want to proceed?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="confirm-action">Confirm</button>
</div>
</div>
</div>
</div>
<!-- Notification Container -->
<div class="notification-container"></div>
<!-- Live Saving Indicator -->
<div id="live-saving-indicator" class="live-saving-indicator">Saving...</div>
<!-- Scripts at the bottom for faster page loading -->
<!-- jQuery first, then Bootstrap JS bundle -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<!-- Then other libraries -->
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/Sortable.min.js"></script>
<!-- Finally, custom JavaScript -->
<script src="assets/js/app.js"></script>
</body>
</html>
<?php
// setup.php
// Start session
session_start();
// Database configuration
define('DB_HOST', 'localhost');
define('DB_NAME', 'product_management');
define('DB_USER', 'root');
define('DB_PASS', '');
// Create PDO connection
try {
// First, try to connect without the database
$pdo = new PDO("mysql:host=" . DB_HOST . ";charset=utf8mb4", DB_USER, DB_PASS);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
try {
// Drop existing database if it exists
$pdo->exec("DROP DATABASE IF EXISTS `" . DB_NAME . "`");
// Create database
$pdo->exec("CREATE DATABASE `" . DB_NAME . "` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
// Select the database
$pdo->exec("USE `" . DB_NAME . "`");
// Create tables
$queries = [
// Pages table
"CREATE TABLE IF NOT EXISTS pages (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
subtitle VARCHAR(255),
type ENUM('invoice', 'packing_list') NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)",
// Company Tags table
"CREATE TABLE IF NOT EXISTS company_tags (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)",
// Reference Tags table
"CREATE TABLE IF NOT EXISTS reference_tags (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE,
rma_ref VARCHAR(255),
buyer_reference VARCHAR(255),
packing_list_ref VARCHAR(255),
export_invoice_number VARCHAR(255),
export_invoice_date DATE,
method_of_delivery VARCHAR(255),
delivery_term VARCHAR(255),
terms VARCHAR(255),
signatory_company VARCHAR(255),
authorized_signatory VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)",
// HS Codes table
"CREATE TABLE IF NOT EXISTS hs_codes (
id INT AUTO_INCREMENT PRIMARY KEY,
code VARCHAR(50) NOT NULL UNIQUE,
title VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)",
// Products table
"CREATE TABLE IF NOT EXISTS products (
id INT AUTO_INCREMENT PRIMARY KEY,
page_id INT NOT NULL,
product_code VARCHAR(100) NOT NULL,
tag_ref VARCHAR(100),
hs_code VARCHAR(50),
description TEXT NOT NULL,
quantity DECIMAL(10,2) DEFAULT 0,
unit_kind ENUM('Box', 'Pkg', 'Pallet', 'Other') DEFAULT 'Box',
packages INT DEFAULT 0,
length DECIMAL(10,3) DEFAULT 0,
width DECIMAL(10,3) DEFAULT 0,
height DECIMAL(10,3) DEFAULT 0,
net_weight DECIMAL(10,2) DEFAULT 0,
gross_weight DECIMAL(10,2) DEFAULT 0,
unit_price DECIMAL(10,2) DEFAULT 0,
currency ENUM('USD', 'BHD', 'EUR', 'GBP') DEFAULT 'USD',
exclude_invoice TINYINT(1) DEFAULT 0,
sort_order INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (page_id) REFERENCES pages(id) ON DELETE CASCADE
)",
// Shipments table
"CREATE TABLE IF NOT EXISTS shipments (
id INT AUTO_INCREMENT PRIMARY KEY,
page_id INT NOT NULL,
shipper TEXT,
dispatch_method ENUM('Road Freight', 'Air Freight', 'Sea Freight', 'Other'),
shipment_type VARCHAR(255),
origin_country VARCHAR(10),
destination_country VARCHAR(10),
duty_exemption_no VARCHAR(100),
factory_number VARCHAR(100),
consignee TEXT,
buyer TEXT,
company_tag_id INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (page_id) REFERENCES pages(id) ON DELETE CASCADE,
FOREIGN KEY (company_tag_id) REFERENCES company_tags(id) ON DELETE SET NULL
)",
// Documents table
"CREATE TABLE IF NOT EXISTS documents (
id INT AUTO_INCREMENT PRIMARY KEY,
page_id INT NOT NULL,
rma_ref VARCHAR(100),
buyer_reference VARCHAR(100),
packing_list_ref VARCHAR(100),
export_invoice_number VARCHAR(100),
export_invoice_date DATE,
method_of_delivery VARCHAR(255),
delivery_term VARCHAR(255),
terms VARCHAR(255),
signatory_company VARCHAR(255),
authorized_signatory VARCHAR(255),
reference_tag_id INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (page_id) REFERENCES pages(id) ON DELETE CASCADE,
FOREIGN KEY (reference_tag_id) REFERENCES reference_tags(id) ON DELETE SET NULL
)",
// Notes table
"CREATE TABLE IF NOT EXISTS notes (
id INT AUTO_INCREMENT PRIMARY KEY,
page_id INT NOT NULL,
content TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (page_id) REFERENCES pages(id) ON DELETE CASCADE
)",
// History table
"CREATE TABLE IF NOT EXISTS history (
id INT AUTO_INCREMENT PRIMARY KEY,
page_id INT NOT NULL,
action VARCHAR(50) NOT NULL,
data JSON,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (page_id) REFERENCES pages(id) ON DELETE CASCADE
)"
];
// Execute all queries
foreach ($queries as $query) {
$pdo->exec($query);
}
// Insert default data
$defaultData = [
"INSERT INTO hs_codes (code, title) VALUES
('12245875', 'Test'),
('12245876', 'Another Test'),
('12245877', 'Sample Item')"
];
foreach ($defaultData as $data) {
$pdo->exec($data);
}
$message = "Database setup completed successfully!";
} catch (PDOException $e) {
$error = "ERROR: " . $e->getMessage();
}
} catch (PDOException $e) {
$error = "ERROR: Could not connect. " . $e->getMessage();
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Database Setup</title>
<!-- Inter font from Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<!-- Bootstrap CSS (stable 5.3.x CDN) -->
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css" rel="stylesheet">
<style>
body {
background-color: #f8f9fa;
font-family: 'Inter', sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
}
.setup-container {
max-width: 600px;
width: 100%;
padding: 2rem;
background-color: white;
border-radius: 0.5rem;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
.btn {
font-weight: 500;
border-radius: 0.375rem;
transition: all 0.2s ease;
}
.btn-primary {
background-color: #0d6efd;
border-color: #0d6efd;
}
.btn-primary:hover {
background-color: #0b5ed7;
border-color: #0a58ca;
}
</style>
</head>
<body>
<div class="setup-container">
<div class="text-center mb-4">
<i class="bi bi-database-fill-gear text-primary" style="font-size: 3rem;"></i>
<h1 class="mt-3">Database Setup</h1>
<p class="text-muted">Setting up your Product Management System database</p>
</div>
<?php if (isset($message)): ?>
<div class="alert alert-success d-flex align-items-center">
<i class="bi bi-check-circle-fill me-2"></i>
<div>
<?php echo $message; ?>
</div>
</div>
<div class="d-grid mt-4">
<a href="index.php" class="btn btn-primary btn-lg">
<i class="bi bi-box-arrow-in-right me-2"></i> Go to Application
</a>
</div>
<?php endif; ?>
<?php if (isset($error)): ?>
<div class="alert alert-danger d-flex align-items-center">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<div>
<?php echo $error; ?>
</div>
</div>
<div class="d-grid mt-4">
<a href="setup.php" class="btn btn-primary btn-lg">
<i class="bi bi-arrow-clockwise me-2"></i> Try Again
</a>
</div>
<?php endif; ?>
</div>
</body>
</html>
/* CSS variables for size management */
:root {
--w-expanded: 16rem;
--w-collapsed: 5rem;
}
html,
body {
height: 100%;
font-family: 'Inter', sans-serif;
}
/* Theme variables for backgrounds */
[data-bs-theme="light"] {
--main-bg: #f8f9fa;
--section-bg: #ffffff;
--tag-bg: var(--bs-primary-bg-subtle);
--tag-text: var(--bs-primary-text-emphasis);
--tag-hover-bg: var(--bs-primary);
--tag-hover-text: #ffffff;
--tag-active-bg: var(--bs-primary);
--tag-active-text: #ffffff;
--stats-bg: #f8f9fa;
}
[data-bs-theme="dark"] {
--main-bg: #1a1a1a;
--section-bg: #2d2d2d;
--tag-bg: #3a3a3a;
--tag-text: #e0e0e0;
--tag-hover-bg: var(--bs-primary);
--tag-hover-text: #ffffff;
--tag-active-bg: var(--bs-primary);
--tag-active-text: #ffffff;
--stats-bg: #3a3a3a;
}
body {
background-color: var(--main-bg);
}
/* Desktop sidebar sizing and transitions */
#sidebar {
width: var(--w-expanded);
transition: width .22s ease;
}
body.sidebar-collapsed #sidebar {
width: var(--w-collapsed);
}
/* Main content margin mirrors sidebar width */
#main {
transition: margin-left .22s ease;
}
@media (min-width: 992px) {
#main {
margin-left: var(--w-expanded);
}
body.sidebar-collapsed #main {
margin-left: var(--w-collapsed);
}
}
/* Hide navigation labels when sidebar is collapsed */
@media (min-width: 992px) {
body.sidebar-collapsed .nav-label {
display: none !important;
}
body.sidebar-collapsed .nav-item {
justify-content: center;
}
}
/* Hamburger button styling */
.hamburger {
width: 40px;
height: 40px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: .375rem;
background: transparent;
border: none;
}
.hamburger:hover {
background-color: var(--bs-secondary-bg);
color: var(--bs-emphasis-color);
}
/* Offcanvas & modal z-index */
.offcanvas {
z-index: 1040 !important;
}
.modal {
z-index: 1055 !important;
}
/* Sticky header */
#main-header {
position: sticky;
top: 0;
left: 0;
right: 0;
z-index: 1020;
}
/* Search dropdown */
.search-dropdown {
position: absolute;
width: 100%;
max-height: 200px;
overflow-y: auto;
border-radius: .375rem;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
margin-top: 0.5rem;
display: none;
z-index: 10;
}
.search-container:focus-within .search-dropdown {
display: block;
}
/* Navigation styling */
.nav-link {
color: var(--bs-body-color);
border-radius: 0.375rem;
padding: 0.75rem 1rem;
transition: all 0.2s ease;
font-weight: 500;
}
.nav-link:hover {
background-color: var(--bs-secondary-bg);
color: var(--bs-emphasis-color);
}
.nav-link.active {
background-color: var(--bs-primary-bg-subtle);
color: var(--bs-primary-text-emphasis);
font-weight: 600;
}
.nav-link i {
font-size: 1.25rem;
transition: transform 0.2s ease;
}
.nav-link:hover i {
transform: scale(1.1);
}
.nav-link.active i {
color: var(--bs-primary);
}
/* Button styling */
.btn {
font-weight: 500;
border-radius: 0.375rem;
transition: all 0.2s ease;
}
.btn-primary {
background-color: var(--bs-primary);
border-color: var(--bs-primary);
}
.btn-primary:hover {
background-color: var(--bs-primary-hover);
border-color: var(--bs-primary-hover);
}
/* Dark mode toggle button */
.btn-theme-toggle {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
padding: 0;
border: none;
background: transparent;
color: var(--bs-body-color);
transition: background-color 0.2s ease, color 0.2s ease;
}
.btn-theme-toggle:hover {
background-color: var(--bs-secondary-bg);
color: var(--bs-emphasis-color);
}
/* Form control styling */
.form-control,
.form-select {
border-radius: 0.375rem;
border-color: var(--bs-border-color);
transition: all 0.2s ease;
}
.form-control:focus,
.form-select:focus {
border-color: var(--bs-primary);
box-shadow: 0 0 0 0.25rem rgba(var(--bs-primary-rgb), 0.25);
}
/* Remove gaps around navbar */
#main-header {
margin: 0;
padding: 0.75rem 1rem;
}
#main {
padding: 0;
}
.container-fluid {
padding: 1rem;
}
/* Dark mode variables */
[data-bs-theme="dark"] {
--bs-primary-hover: rgb(73, 110, 243);
}
[data-bs-theme="light"] {
--bs-primary-hover: rgb(30, 64, 175);
}
/* Content section styling */
.content-section {
border-radius: 0.375rem;
margin-bottom: 1rem;
background-color: var(--section-bg);
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
position: relative;
}
/* Section header styling */
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.section-title {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
}
.section-subtitle {
margin: 0;
font-size: 0.875rem;
color: var(--bs-secondary-color);
}
.section-actions .btn {
margin-left: 0.5rem;
}
/* Clear section button styling */
.clear-section-btn {
position: absolute;
top: 10px;
right: 10px;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
font-size: 0.875rem;
z-index: 10;
background: transparent;
border: none;
color: var(--bs-secondary-color);
}
.clear-section-btn:hover {
color: var(--bs-danger);
}
/* Form styling */
.form-label {
font-weight: 500;
margin-bottom: 0.25rem;
font-size: 0.8rem;
}
.mb-3-custom {
margin-bottom: 1rem;
}
/* Tag system styling */
.tag-section {
margin-top: 1.5rem;
padding-top: 1rem;
border-top: 1px solid var(--bs-border-color);
}
.tag-container {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.tag {
display: inline-flex;
align-items: center;
padding: 0.25rem 0.75rem;
background-color: var(--tag-bg);
color: var(--tag-text);
border-radius: 1rem;
font-size: 0.75rem;
cursor: pointer;
transition: background-color 0.2s ease, color 0.2s ease;
}
.tag:hover {
background-color: var(--tag-hover-bg);
color: var(--tag-hover-text);
}
.tag.active {
background-color: var(--tag-active-bg);
color: var(--tag-active-text);
}
.tag-dropdown {
position: relative;
display: inline-block;
}
.tag-dropdown .dropdown-menu {
min-width: 200px;
}
/* Three dots button styling */
.three-dots {
width: 32px;
height: 32px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: transparent;
border: none;
color: var(--bs-body-color);
cursor: pointer;
transition: background-color 0.2s ease;
}
.three-dots:hover {
background-color: var(--bs-secondary-bg);
}
/* Highlighted textarea styling */
textarea.highlighted {
border-color: var(--bs-primary);
box-shadow: 0 0 0 0.25rem rgba(var(--bs-primary-rgb), 0.25);
}
/* Table styling */
.sortable-ghost {
opacity: 0.4;
}
.table th {
font-weight: 600;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.5px;
white-space: nowrap;
}
.table td {
vertical-align: middle;
font-size: 0.875rem;
}
/* Adjusted column widths */
.table th:nth-child(4),
/* Quantity */
.table th:nth-child(5)
/* Packing */
{
width: 80px;
}
.table th:nth-child(3)
/* Description */
{
min-width: 200px;
}
.drag-handle {
cursor: move;
color: var(--bs-secondary-color);
}
.drag-handle:hover {
color: var(--bs-primary);
}
/* Product code with tag styling */
.product-code-container {
display: flex;
flex-direction: column;
}
.product-tag {
font-size: 0.75rem;
color: var(--bs-secondary-color);
}
/* Weight and dimensions styling */
.weight-container,
.dimensions-container {
display: flex;
flex-direction: column;
line-height: 1.2;
}
.weight-label,
.dimensions-label {
font-size: 0.75rem;
color: var(--bs-secondary-color);
}
/* HS Code reference styling */
.hs-code-reference {
height: 100%;
overflow-y: auto;
border: 1px solid var(--bs-border-color);
border-radius: 0.375rem;
padding: 0.5rem;
}
.hs-code-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem;
border-bottom: 1px solid var(--bs-border-color);
}
.hs-code-item:last-child {
border-bottom: none;
}
.hs-code-item:hover {
background-color: var(--bs-secondary-bg);
}
/* Add HS Code modal styling */
.add-hs-modal {
max-width: 400px;
}
/* Statistics panels - fixed background for dark mode */
.stats-panel {
background-color: var(--stats-bg);
border-radius: 0.375rem;
padding: 1rem;
text-align: center;
height: 100%;
}
/* Navbar action buttons styling */
.navbar-action-btn {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
border-radius: 0.375rem;
border: none;
background: transparent;
color: var(--bs-body-color);
transition: background-color 0.2s ease, color 0.2s ease;
}
.navbar-action-btn:hover {
background-color: var(--bs-secondary-bg);
color: var(--bs-emphasis-color);
}
/* Notification styling */
.notification-container {
position: fixed;
bottom: 1rem;
right: 1rem;
z-index: 1100;
display: flex;
flex-direction: column;
gap: 0.5rem;
pointer-events: none;
}
.notification {
pointer-events: auto;
min-width: 250px;
}
/* Improved notification styling */
.toast {
background-color: rgba(var(--bs-success-rgb), 0.95);
color: white;
border: none;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
.toast-header {
background-color: rgba(255, 255, 255, 0.1);
color: white;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
}
.toast-body {
color: white;
}
.btn-close {
filter: invert(1);
}
/* Modal styling */
.modal-xl {
max-width: 90%;
}
/* Print dropdown styling */
.print-dropdown {
position: relative;
display: inline-block;
}
/* Clear all button styling */
.clear-all-btn {
margin-left: 0.5rem;
background-color: var(--bs-danger);
border-color: var(--bs-danger);
}
.clear-all-btn:hover {
background-color: var(--bs-danger-hover);
border-color: var(--bs-danger-hover);
}
/* Unit price input styling */
.unit-price-input {
flex-grow: 1;
}
/* Empty table styling */
.empty-table-message {
text-align: center;
padding: 2rem;
color: var(--bs-secondary-color);
}
/* Table footer styling */
.table tfoot td {
font-weight: 600;
background-color: var(--bs-secondary-bg);
}
/* Notes section styling */
.notes-section {
margin-top: 1rem;
}
.notes-textarea {
min-height: 100px;
}
/* Page item styling */
.page-item {
display: flex;
align-items: center;
padding: 0.75rem 1rem;
margin-bottom: 0.25rem;
border-radius: 0.375rem;
cursor: pointer;
transition: all 0.2s ease;
text-decoration: none;
color: var(--bs-body-color);
}
.page-item:hover {
background-color: var(--bs-secondary-bg);
color: var(--bs-emphasis-color);
}
.page-item.active {
background-color: var(--bs-primary-bg-subtle);
color: var(--bs-primary-text-emphasis);
font-weight: 600;
}
.page-item-icon {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 0.75rem;
border-radius: 0.25rem;
background-color: var(--bs-primary);
color: white;
font-size: 0.75rem;
font-weight: 600;
flex-shrink: 0;
}
.nav-item-content {
flex-grow: 1;
overflow: hidden;
}
.nav-item-content div:first-child {
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.nav-item-content div:last-child {
font-size: 0.75rem;
color: var(--bs-secondary-color);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* New page button styling */
.new-page-btn {
display: flex;
align-items: center;
padding: 0.75rem 1rem;
margin-bottom: 1rem;
border-radius: 0.375rem;
border: 1px dashed var(--bs-border-color);
background-color: transparent;
color: var(--bs-secondary-color);
transition: all 0.2s ease;
text-decoration: none;
}
.new-page-btn:hover {
background-color: var(--bs-secondary-bg);
color: var(--bs-emphasis-color);
border-color: var(--bs-primary);
}
/* Ensure text is hidden when sidebar is collapsed */
@media (min-width: 992px) {
body.sidebar-collapsed .nav-label,
body.sidebar-collapsed .nav-item-content div:last-child {
display: none;
}
body.sidebar-collapsed .new-page-btn {
justify-content: center;
padding: 0.75rem;
}
body.sidebar-collapsed .page-item {
justify-content: center;
padding: 0.75rem;
}
body.sidebar-collapsed .page-item-icon {
margin-right: 0;
}
}
/* Create page modal styling */
.create-page-type {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
}
.page-type-option {
flex: 1;
padding: 1rem;
border: 1px solid var(--bs-border-color);
border-radius: 0.375rem;
text-align: center;
cursor: pointer;
transition: all 0.2s ease;
}
.page-type-option:hover {
border-color: var(--bs-primary);
background-color: var(--bs-primary-bg-subtle);
}
.page-type-option.selected {
border-color: var(--bs-primary);
background-color: var(--bs-primary-bg-subtle);
color: var(--bs-primary-text-emphasis);
}
.page-type-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
color: var(--bs-primary);
}
/* Live saving indicator */
.live-saving-indicator {
position: fixed;
bottom: 1rem;
left: 1rem;
padding: 0.5rem 0.75rem;
background-color: rgba(var(--bs-success-rgb), 0.9);
color: white;
border-radius: 0.375rem;
font-size: 0.875rem;
display: none;
z-index: 1100;
}
.live-saving-indicator.show {
display: block;
}
/* Modal frost effect */
.modal-backdrop {
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
background-color: rgba(0, 0, 0, 0.4);
}
.modal-content {
background-color: rgba(var(--bs-body-bg-rgb), 0.85);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
}
/* Theme-specific modal adjustments */
[data-bs-theme="light"] .modal-content {
background-color: rgba(255, 255, 255, 0.85);
}
[data-bs-theme="dark"] .modal-content {
background-color: rgba(45, 45, 45, 0.85);
}
/* Ensure modal stacking works with frost effect */
.modal-backdrop.modal-stack {
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
background-color: rgba(0, 0, 0, 0.4);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment