Last active
August 28, 2025 12:43
-
-
Save algarih/4b64a0be8007f3d7daba28c79b5ebae3 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?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; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| (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'); | |
| } | |
| }); | |
| }); | |
| })(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?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()); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?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; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* 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