From ae86c5cba1f1c5c330aef9ae0098c3ed279c4ca9 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sun, 5 Apr 2026 11:51:30 +0000 Subject: [PATCH] Autosave: 20260405-115130 --- assets/css/custom.css | 781 +++++++++++++++++-------------- assets/js/main.js | 179 ++++++-- cookies.txt | 5 + healthz.php | 22 + index.php | 1020 +++++++++++++++++++++++++++++++++++------ 5 files changed, 1490 insertions(+), 517 deletions(-) create mode 100644 cookies.txt create mode 100644 healthz.php diff --git a/assets/css/custom.css b/assets/css/custom.css index 789132e..333c8b1 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -1,403 +1,490 @@ -body { - background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); - background-size: 400% 400%; - animation: gradient 15s ease infinite; - color: #212529; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - font-size: 14px; - margin: 0; - min-height: 100vh; + :root { + --bg: #f4f7fb; + --surface: rgba(255, 255, 255, 0.9); + --surface-strong: #ffffff; + --text: #101828; + --muted: #667085; + --line: #d7dfeb; + --primary: #0f766e; + --primary-strong: #115e59; + --primary-soft: rgba(15, 118, 110, 0.12); + --danger: #b42318; + --danger-soft: rgba(180, 35, 24, 0.12); + --success: #027a48; + --success-soft: rgba(2, 122, 72, 0.12); + --shadow: 0 24px 60px rgba(15, 23, 42, 0.09); + --radius: 24px; } -.main-wrapper { +body[data-theme="dark"] { + --bg: #07111f; + --surface: rgba(10, 18, 32, 0.88); + --surface-strong: #0f172a; + --text: #eef2ff; + --muted: #9fb0c7; + --line: rgba(148, 163, 184, 0.22); + --primary: #34d399; + --primary-strong: #10b981; + --primary-soft: rgba(52, 211, 153, 0.16); + --danger: #f97066; + --danger-soft: rgba(249, 112, 102, 0.16); + --success: #32d583; + --success-soft: rgba(50, 213, 131, 0.16); + --shadow: 0 24px 60px rgba(2, 6, 23, 0.45); +} + +* { box-sizing: border-box; } +html { scroll-behavior: smooth; } +body { + margin: 0; + font-family: "Segoe UI", Tahoma, Arial, sans-serif; + background: + radial-gradient(circle at top right, rgba(15, 118, 110, 0.14), transparent 30%), + radial-gradient(circle at left bottom, rgba(14, 165, 233, 0.10), transparent 30%), + var(--bg); + color: var(--text); +} + +a { color: inherit; text-decoration: none; } +button, input, select, textarea { + font: inherit; +} +button { + cursor: pointer; + border: 0; +} + +.page-shell { + width: min(1200px, calc(100% - 32px)); + margin: 0 auto; + padding: 28px 0 40px; +} + +.topbar, +.hero-card, +.panel-card, +.login-card, +.preview-card, +.stat-card, +.help-card { + backdrop-filter: blur(14px); + background: var(--surface); + border: 1px solid var(--line); + box-shadow: var(--shadow); +} + +.topbar { display: flex; align-items: center; + justify-content: space-between; + gap: 18px; + padding: 22px 24px; + border-radius: var(--radius); + margin-bottom: 24px; + position: sticky; + top: 12px; + z-index: 20; +} + +.eyebrow { + margin: 0 0 6px; + font-size: 12px; + text-transform: uppercase; + letter-spacing: .18em; + color: var(--muted); +} + +h1, h2, h3, p { + margin-top: 0; +} + +h1 { + margin-bottom: 8px; + font-size: clamp(28px, 3vw, 40px); +} + +.subtitle { + margin: 0; + color: var(--muted); + max-width: 780px; +} + +.topbar-actions, +.hero-actions, +.inline-actions { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.ghost-btn, +.primary-btn, +.primary-link, +.tab-link { + display: inline-flex; + align-items: center; justify-content: center; - min-height: 100vh; - width: 100%; - padding: 20px; - box-sizing: border-box; - position: relative; - z-index: 1; -} - -@keyframes gradient { - 0% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } - 100% { - background-position: 0% 50%; - } -} - -.chat-container { - width: 100%; - max-width: 600px; - background: rgba(255, 255, 255, 0.85); - border: 1px solid rgba(255, 255, 255, 0.3); - border-radius: 20px; - display: flex; - flex-direction: column; - height: 85vh; - box-shadow: 0 20px 40px rgba(0,0,0,0.2); - backdrop-filter: blur(15px); - -webkit-backdrop-filter: blur(15px); - overflow: hidden; -} - -.chat-header { - padding: 1.5rem; - border-bottom: 1px solid rgba(0, 0, 0, 0.05); - background: rgba(255, 255, 255, 0.5); - font-weight: 700; - font-size: 1.1rem; - display: flex; - justify-content: space-between; - align-items: center; -} - -.chat-messages { - flex: 1; - overflow-y: auto; - padding: 1.5rem; - display: flex; - flex-direction: column; - gap: 1.25rem; -} - -/* Custom Scrollbar */ -::-webkit-scrollbar { - width: 6px; -} - -::-webkit-scrollbar-track { - background: transparent; -} - -::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.3); - border-radius: 10px; -} - -::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.5); -} - -.message { - max-width: 85%; - padding: 0.85rem 1.1rem; + min-height: 46px; + padding: 0 18px; border-radius: 16px; - line-height: 1.5; - font-size: 0.95rem; - box-shadow: 0 4px 15px rgba(0,0,0,0.05); - animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); + transition: .2s ease; } -@keyframes fadeIn { - from { opacity: 0; transform: translateY(20px) scale(0.95); } - to { opacity: 1; transform: translateY(0) scale(1); } +.ghost-btn { + background: var(--surface-strong); + color: var(--text); + border: 1px solid var(--line); } -.message.visitor { - align-self: flex-end; - background: linear-gradient(135deg, #212529 0%, #343a40 100%); - color: #fff; - border-bottom-right-radius: 4px; +.primary-btn, +.primary-link, +.tab-link.is-active, +.secondary-tone { + background: linear-gradient(135deg, var(--primary), var(--primary-strong)); + color: #ffffff; + box-shadow: 0 14px 28px rgba(15, 118, 110, 0.22); } -.message.bot { - align-self: flex-start; - background: #ffffff; - color: #212529; - border-bottom-left-radius: 4px; +.primary-btn:hover, +.primary-link:hover, +.ghost-btn:hover, +.tab-link:hover { + transform: translateY(-1px); } -.chat-input-area { - padding: 1.25rem; - background: rgba(255, 255, 255, 0.5); - border-top: 1px solid rgba(0, 0, 0, 0.05); +.secondary-tone { + border: none; } -.chat-input-area form { +.login-layout, +.grid.two-col, +.reports-grid { + display: grid; + gap: 22px; +} + +.login-layout, +.grid.two-col { + grid-template-columns: 1.05fr .95fr; +} + +.login-card, +.preview-card, +.hero-card, +.panel-card, +.stat-card { + border-radius: 28px; + padding: 24px; +} + +.card-head { + margin-bottom: 18px; +} + +.card-head p, +.help-note, +.empty-state, +.empty-cell, +.summary-list li span, +.mini-item p, +.stat-inline span, +.invoice-note { + color: var(--muted); +} + +.demo-box, +.invoice-preview-box, +.invoice-row, +.invoice-item, +.summary-list li, +.mini-item, +.stat-inline { display: flex; - gap: 0.75rem; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 14px 16px; + background: var(--surface-strong); + border: 1px solid var(--line); + border-radius: 18px; } -.chat-input-area input { - flex: 1; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - padding: 0.75rem 1rem; +.gradient-card { + background: + linear-gradient(145deg, rgba(15, 118, 110, 0.16), rgba(14, 165, 233, 0.08)), + var(--surface); +} + +.gradient-card ul { + margin: 18px 0 0; + padding-inline-start: 20px; + line-height: 1.9; +} + +.stack-form { + display: grid; + gap: 14px; +} + +label { + display: grid; + gap: 8px; + font-weight: 600; +} + +input, +select, +textarea { + width: 100%; + border: 1px solid var(--line); + background: var(--surface-strong); + color: var(--text); + border-radius: 16px; + padding: 14px 16px; outline: none; - background: rgba(255, 255, 255, 0.9); - transition: all 0.3s ease; + transition: border-color .2s ease, box-shadow .2s ease; } -.chat-input-area input:focus { - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2); +input:focus, +select:focus, +textarea:focus { + border-color: var(--primary); + box-shadow: 0 0 0 4px var(--primary-soft); } -.chat-input-area button { - background: #212529; - color: #fff; - border: none; - padding: 0.75rem 1.5rem; - border-radius: 12px; - cursor: pointer; - font-weight: 600; - transition: all 0.3s ease; +.split-fields { + display: grid; + gap: 12px; + grid-template-columns: repeat(2, minmax(0, 1fr)); } -.chat-input-area button:hover { - background: #000; - transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(0,0,0,0.2); +.app-layout { + display: grid; + gap: 22px; } -/* Background Animations */ -.bg-animations { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 0; - overflow: hidden; - pointer-events: none; -} - -.blob { - position: absolute; - width: 500px; - height: 500px; - background: rgba(255, 255, 255, 0.2); - border-radius: 50%; - filter: blur(80px); - animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1); -} - -.blob-1 { - top: -10%; - left: -10%; - background: rgba(238, 119, 82, 0.4); -} - -.blob-2 { - bottom: -10%; - right: -10%; - background: rgba(35, 166, 213, 0.4); - animation-delay: -7s; - width: 600px; - height: 600px; -} - -.blob-3 { - top: 40%; - left: 30%; - background: rgba(231, 60, 126, 0.3); - animation-delay: -14s; - width: 450px; - height: 450px; -} - -@keyframes move { - 0% { transform: translate(0, 0) rotate(0deg) scale(1); } - 33% { transform: translate(150px, 100px) rotate(120deg) scale(1.1); } - 66% { transform: translate(-50px, 200px) rotate(240deg) scale(0.9); } - 100% { transform: translate(0, 0) rotate(360deg) scale(1); } -} - -.header-link { - font-size: 14px; - color: #fff; - text-decoration: none; - background: rgba(0, 0, 0, 0.2); - padding: 0.5rem 1rem; - border-radius: 8px; - transition: all 0.3s ease; -} - -.header-link:hover { - background: rgba(0, 0, 0, 0.4); - text-decoration: none; -} - -/* Admin Styles */ -.admin-container { - max-width: 900px; - margin: 3rem auto; - padding: 2.5rem; - background: rgba(255, 255, 255, 0.85); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border-radius: 24px; - box-shadow: 0 20px 50px rgba(0,0,0,0.15); - border: 1px solid rgba(255, 255, 255, 0.4); - position: relative; - z-index: 1; -} - -.admin-container h1 { - margin-top: 0; - color: #212529; - font-weight: 800; -} - -.table { - width: 100%; - border-collapse: separate; - border-spacing: 0 8px; - margin-top: 1.5rem; -} - -.table th { - background: transparent; - border: none; - padding: 1rem; - color: #6c757d; - font-weight: 600; - text-transform: uppercase; - font-size: 0.75rem; - letter-spacing: 1px; -} - -.table td { - background: #fff; - padding: 1rem; - border: none; -} - -.table tr td:first-child { border-radius: 12px 0 0 12px; } -.table tr td:last-child { border-radius: 0 12px 12px 0; } - -.form-group { - margin-bottom: 1.25rem; -} - -.form-group label { - display: block; - margin-bottom: 0.5rem; - font-weight: 600; - font-size: 0.9rem; -} - -.form-control { - width: 100%; - padding: 0.75rem 1rem; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - background: #fff; - transition: all 0.3s ease; - box-sizing: border-box; -} - -.form-control:focus { - outline: none; - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1); -} - -.header-container { +.hero-card { display: flex; justify-content: space-between; + gap: 20px; align-items: center; } -.header-links { +.tab-nav { display: flex; - gap: 1rem; + gap: 12px; + flex-wrap: wrap; } -.admin-card { - background: rgba(255, 255, 255, 0.6); - padding: 2rem; - border-radius: 20px; - border: 1px solid rgba(255, 255, 255, 0.5); - margin-bottom: 2.5rem; - box-shadow: 0 10px 30px rgba(0,0,0,0.05); +.tab-link { + background: var(--surface); + color: var(--text); + border: 1px solid var(--line); } -.admin-card h3 { - margin-top: 0; - margin-bottom: 1.5rem; +.tab-link.is-active { + border-color: transparent; +} + +.table-wrap { + overflow: auto; + border-radius: 22px; + border: 1px solid var(--line); +} + +table { + width: 100%; + border-collapse: collapse; + min-width: 680px; + background: var(--surface-strong); +} + +th, +td { + padding: 16px; + border-bottom: 1px solid var(--line); + text-align: right; + white-space: nowrap; +} + +thead th { + background: var(--primary-soft); + color: var(--text); + font-size: 14px; +} + +tbody tr:hover { + background: var(--primary-soft); +} + +.clickable-row { + cursor: pointer; +} + +.inline-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 18px; +} + +.stats-grid { + display: grid; + gap: 18px; + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.stat-card span { + display: block; + margin-bottom: 10px; + color: var(--muted); +} + +.stat-card strong { + font-size: 28px; +} + +.summary-list, +.mini-list { + display: grid; + gap: 12px; + margin: 0; + padding: 0; + list-style: none; +} + +.printable-panel .invoice-sheet { + display: grid; + gap: 12px; +} + +.invoice-items { + display: grid; + gap: 10px; +} + +.invoice-note { + margin: 0; + padding: 14px 16px; + background: var(--surface-strong); + border: 1px dashed var(--line); + border-radius: 16px; +} + +.alert { + padding: 14px 16px; + border-radius: 18px; + margin-bottom: 18px; font-weight: 700; } -.btn-delete { - background: #dc3545; - color: white; - border: none; - padding: 0.25rem 0.5rem; - border-radius: 4px; - cursor: pointer; +.alert-success { + background: var(--success-soft); + color: var(--success); + border: 1px solid rgba(2, 122, 72, 0.24); } -.btn-add { - background: #212529; - color: white; - border: none; - padding: 0.5rem 1rem; - border-radius: 4px; - cursor: pointer; - margin-top: 1rem; +.alert-error { + background: var(--danger-soft); + color: var(--danger); + border: 1px solid rgba(180, 35, 24, 0.24); } -.btn-save { - background: #0088cc; - color: white; - border: none; - padding: 0.8rem 1.5rem; - border-radius: 12px; - cursor: pointer; - font-weight: 600; - width: 100%; - transition: all 0.3s ease; +.help-drawer { + position: fixed; + inset: 0; + background: rgba(2, 6, 23, 0.36); + padding: 20px; + display: none; + align-items: flex-start; + justify-content: flex-start; + z-index: 40; } -.webhook-url { - font-size: 0.85em; - color: #555; - margin-top: 0.5rem; +.help-drawer.is-open { + display: flex; } -.history-table-container { - overflow-x: auto; - background: rgba(255, 255, 255, 0.4); - padding: 1rem; - border-radius: 12px; - border: 1px solid rgba(255, 255, 255, 0.3); +.help-card { + width: min(420px, 100%); + border-radius: 28px; + padding: 22px; } -.history-table { - width: 100%; +.help-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; } -.history-table-time { - width: 15%; - white-space: nowrap; - font-size: 0.85em; - color: #555; +.icon-btn { + width: 42px; + height: 42px; + border-radius: 14px; + background: var(--surface-strong); + color: var(--text); + border: 1px solid var(--line); } -.history-table-user { - width: 35%; - background: rgba(255, 255, 255, 0.3); - border-radius: 8px; - padding: 8px; -} - -.history-table-ai { - width: 50%; - background: rgba(255, 255, 255, 0.5); - border-radius: 8px; - padding: 8px; -} - -.no-messages { +.empty-state, +.empty-cell { text-align: center; - color: #777; -} \ No newline at end of file + padding: 24px; +} + +@media (max-width: 1024px) { + .login-layout, + .grid.two-col, + .reports-grid, + .stats-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 720px) { + .page-shell { + width: min(100% - 20px, 100%); + padding-top: 16px; + } + + .topbar, + .hero-card, + .inline-head, + .split-fields, + .demo-box, + .invoice-preview-box, + .invoice-row, + .invoice-item, + .summary-list li, + .mini-item, + .stat-inline { + grid-template-columns: 1fr; + flex-direction: column; + align-items: stretch; + } + + .topbar, + .hero-card { + position: static; + } + + .topbar-actions, + .hero-actions, + .inline-actions, + .tab-nav { + width: 100%; + } + + .ghost-btn, + .primary-btn, + .primary-link, + .tab-link { + width: 100%; + } + + th, + td { + padding: 14px 12px; + } +} diff --git a/assets/js/main.js b/assets/js/main.js index d349598..c86a651 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -1,39 +1,154 @@ document.addEventListener('DOMContentLoaded', () => { - const chatForm = document.getElementById('chat-form'); - const chatInput = document.getElementById('chat-input'); - const chatMessages = document.getElementById('chat-messages'); + const body = document.body; + const savedTheme = localStorage.getItem('store-theme'); + if (savedTheme === 'dark' || savedTheme === 'light') { + body.dataset.theme = savedTheme; + } - const appendMessage = (text, sender) => { - const msgDiv = document.createElement('div'); - msgDiv.classList.add('message', sender); - msgDiv.textContent = text; - chatMessages.appendChild(msgDiv); - chatMessages.scrollTop = chatMessages.scrollHeight; + const themeToggle = document.querySelector('[data-theme-toggle]'); + const helpToggle = document.querySelector('[data-help-toggle]'); + const helpClose = document.querySelector('[data-help-close]'); + const helpDrawer = document.querySelector('[data-help-drawer]'); + + themeToggle?.addEventListener('click', () => { + const nextTheme = body.dataset.theme === 'dark' ? 'light' : 'dark'; + body.dataset.theme = nextTheme; + localStorage.setItem('store-theme', nextTheme); + }); + + const toggleHelp = (open) => { + if (!helpDrawer) return; + helpDrawer.classList.toggle('is-open', open); + helpDrawer.setAttribute('aria-hidden', open ? 'false' : 'true'); }; - chatForm.addEventListener('submit', async (e) => { - e.preventDefault(); - const message = chatInput.value.trim(); - if (!message) return; - - appendMessage(message, 'visitor'); - chatInput.value = ''; - - try { - const response = await fetch('api/chat.php', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ message }) - }); - const data = await response.json(); - - // Artificial delay for realism - setTimeout(() => { - appendMessage(data.reply, 'bot'); - }, 500); - } catch (error) { - console.error('Error:', error); - appendMessage("Sorry, something went wrong. Please try again.", 'bot'); + helpToggle?.addEventListener('click', () => toggleHelp(true)); + helpClose?.addEventListener('click', () => toggleHelp(false)); + helpDrawer?.addEventListener('click', (event) => { + if (event.target === helpDrawer) { + toggleHelp(false); } }); + + document.querySelectorAll('[data-auto-hide]').forEach((alert) => { + setTimeout(() => { + alert.style.transition = 'opacity .3s ease'; + alert.style.opacity = '0'; + setTimeout(() => alert.remove(), 320); + }, 3200); + }); + + const formatSyp = (value) => `${new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }).format(value || 0)} ل.س`; + const rateInput = document.querySelector('[data-rate-input]'); + const ratePreview = document.querySelector('[data-rate-preview]'); + const equipmentUsd = document.querySelector('[data-equipment-usd]'); + const equipmentPreview = document.querySelector('[data-equipment-preview]'); + const invoiceEquipment = document.querySelector('[data-invoice-equipment]'); + const invoiceQty = document.querySelector('[data-invoice-qty]'); + const invoiceStock = document.querySelector('[data-invoice-stock]'); + const invoiceTotal = document.querySelector('[data-invoice-total]'); + + const currentRate = () => { + const parsed = parseFloat(rateInput?.value || body.dataset.currentRate || '0'); + return Number.isFinite(parsed) ? parsed : 0; + }; + + const updateRatePreview = () => { + if (ratePreview) ratePreview.textContent = formatSyp(currentRate()); + }; + + const updateEquipmentPreview = () => { + if (!equipmentPreview) return; + const usd = parseFloat(equipmentUsd?.value || '0'); + equipmentPreview.textContent = usd > 0 && currentRate() > 0 ? formatSyp(usd * currentRate()) : '—'; + }; + + const updateInvoicePreview = () => { + if (!invoiceTotal) return; + const option = invoiceEquipment?.selectedOptions?.[0]; + const price = parseFloat(option?.dataset.price || '0'); + const stock = parseInt(option?.dataset.stock || '0', 10); + const qty = parseInt(invoiceQty?.value || '0', 10); + if (invoiceStock) { + invoiceStock.textContent = option && option.value ? `${stock} قطعة` : '—'; + } + if (!option || !option.value || price <= 0 || qty <= 0 || currentRate() <= 0) { + invoiceTotal.textContent = '—'; + return; + } + invoiceTotal.textContent = formatSyp(price * qty * currentRate()); + }; + + rateInput?.addEventListener('input', () => { + body.dataset.currentRate = rateInput.value; + updateRatePreview(); + updateEquipmentPreview(); + updateInvoicePreview(); + }); + equipmentUsd?.addEventListener('input', updateEquipmentPreview); + invoiceEquipment?.addEventListener('change', updateInvoicePreview); + invoiceQty?.addEventListener('input', updateInvoicePreview); + updateRatePreview(); + updateEquipmentPreview(); + updateInvoicePreview(); + + const printableInvoice = document.querySelector('[data-printable-invoice]'); + const printButton = document.querySelector('[data-print-invoice]'); + const exportSelectedButton = document.querySelector('[data-export-selected-pdf]'); + const exportInvoicesButton = document.querySelector('[data-export-invoices]'); + const invoicesTable = document.querySelector('[data-invoices-table]'); + + const openPrintWindow = (title, html) => { + const win = window.open('', '_blank', 'width=900,height=700'); + if (!win) return; + win.document.write(`${title}
${html}
`); + win.document.close(); + }; + + printButton?.addEventListener('click', () => { + if (!printableInvoice) return; + openPrintWindow('فاتورة', printableInvoice.innerHTML); + }); + + const hasPdf = () => window.jspdf && window.jspdf.jsPDF; + + exportSelectedButton?.addEventListener('click', () => { + if (!hasPdf() || !printableInvoice) return; + const { jsPDF } = window.jspdf; + const doc = new jsPDF({ unit: 'pt', format: 'a4' }); + doc.setFontSize(18); + doc.text('Selected Invoice', 40, 50); + const lines = printableInvoice.innerText.split('\n').map((line) => line.trim()).filter(Boolean); + let y = 90; + lines.forEach((line) => { + const chunks = doc.splitTextToSize(line, 500); + doc.text(chunks, 40, y); + y += chunks.length * 18 + 8; + }); + doc.save('selected-invoice.pdf'); + }); + + exportInvoicesButton?.addEventListener('click', () => { + if (!hasPdf() || !invoicesTable) return; + const { jsPDF } = window.jspdf; + const doc = new jsPDF({ orientation: 'landscape', unit: 'pt', format: 'a4' }); + doc.setFontSize(16); + doc.text('Invoices Register', 40, 40); + const rows = Array.from(invoicesTable.querySelectorAll('tbody tr')).map((tr) => + Array.from(tr.querySelectorAll('td')).map((td) => td.innerText.replace(/\s+/g, ' ').trim()) + ).filter((row) => row.length === 5); + doc.autoTable({ + head: [['Invoice', 'Customer', 'Total USD', 'Total SYP', 'Created At']], + body: rows, + startY: 60, + styles: { fontSize: 10 }, + headStyles: { fillColor: [15, 118, 110] } + }); + doc.save('invoices-register.pdf'); + }); }); diff --git a/cookies.txt b/cookies.txt new file mode 100644 index 0000000..07ac383 --- /dev/null +++ b/cookies.txt @@ -0,0 +1,5 @@ +# Netscape HTTP Cookie File +# https://curl.se/docs/http-cookies.html +# This file was generated by libcurl! Edit at your own risk. + +127.0.0.1 FALSE / FALSE 0 PHPSESSID 7k5qe8i2prge2kh3hqfs9pkf5a diff --git a/healthz.php b/healthz.php new file mode 100644 index 0000000..7b50a76 --- /dev/null +++ b/healthz.php @@ -0,0 +1,22 @@ +query('SELECT 1'); + http_response_code(200); + echo json_encode([ + 'status' => 'ok', + 'app' => 'lamp-store-admin', + 'time' => gmdate('c'), + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); +} catch (Throwable $exception) { + http_response_code(500); + echo json_encode([ + 'status' => 'error', + 'message' => 'database_unreachable', + 'time' => gmdate('c'), + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); +} diff --git a/index.php b/index.php index 7205f3d..05d26f8 100644 --- a/index.php +++ b/index.php @@ -1,150 +1,894 @@ $type, 'message' => $message]; +} + +function pullFlash(): ?array +{ + if (!isset($_SESSION['flash'])) { + return null; + } + + $flash = $_SESSION['flash']; + unset($_SESSION['flash']); + return $flash; +} + +function csrfToken(): string +{ + if (empty($_SESSION['csrf_token'])) { + $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); + } + + return (string) $_SESSION['csrf_token']; +} + +function verifyCsrf(): void +{ + $token = (string) ($_POST['csrf_token'] ?? ''); + if ($token === '' || !hash_equals((string) ($_SESSION['csrf_token'] ?? ''), $token)) { + throw new RuntimeException('انتهت الجلسة. أعد تحميل الصفحة ثم جرّب مرة ثانية.'); + } +} + +function fetchAllRows(PDO $pdo, string $sql, array $params = []): array +{ + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + return $stmt->fetchAll(); +} + +function fetchRow(PDO $pdo, string $sql, array $params = []): ?array +{ + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + $row = $stmt->fetch(); + return $row ?: null; +} + +function fetchValue(PDO $pdo, string $sql, array $params = []) +{ + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + return $stmt->fetchColumn(); +} + +function ensureSchema(PDO $pdo): void +{ + $pdo->exec( + "CREATE TABLE IF NOT EXISTS app_users ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(100) NOT NULL UNIQUE, + full_name VARCHAR(150) NOT NULL, + password_hash VARCHAR(255) NOT NULL, + role VARCHAR(50) NOT NULL DEFAULT 'admin', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ); + + $pdo->exec( + "CREATE TABLE IF NOT EXISTS app_settings ( + setting_key VARCHAR(100) PRIMARY KEY, + setting_value VARCHAR(255) NOT NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ); + + $pdo->exec( + "CREATE TABLE IF NOT EXISTS equipment ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(180) NOT NULL, + quantity INT NOT NULL DEFAULT 0, + price_usd DECIMAL(12,2) NOT NULL DEFAULT 0, + notes TEXT DEFAULT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ); + + $pdo->exec( + "CREATE TABLE IF NOT EXISTS invoices ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + invoice_number VARCHAR(50) NOT NULL UNIQUE, + customer_name VARCHAR(180) DEFAULT NULL, + exchange_rate DECIMAL(12,2) NOT NULL, + total_usd DECIMAL(12,2) NOT NULL, + total_syp BIGINT NOT NULL, + notes TEXT DEFAULT NULL, + created_by VARCHAR(120) DEFAULT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ); + + $pdo->exec( + "CREATE TABLE IF NOT EXISTS invoice_items ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + invoice_id INT UNSIGNED NOT NULL, + equipment_id INT UNSIGNED NOT NULL, + item_name VARCHAR(180) NOT NULL, + quantity INT NOT NULL, + unit_price_usd DECIMAL(12,2) NOT NULL, + line_total_usd DECIMAL(12,2) NOT NULL, + line_total_syp BIGINT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_invoice_items_invoice FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE CASCADE, + CONSTRAINT fk_invoice_items_equipment FOREIGN KEY (equipment_id) REFERENCES equipment(id) ON DELETE RESTRICT + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ); +} + +function seedDefaults(PDO $pdo): void +{ + $defaultPasswordHash = password_hash('admin123', PASSWORD_DEFAULT); + $adminUser = fetchRow($pdo, 'SELECT * FROM app_users WHERE username = :username LIMIT 1', [':username' => 'admin']); + + if (!$adminUser) { + $stmt = $pdo->prepare('INSERT INTO app_users (username, full_name, password_hash, role) VALUES (:username, :full_name, :password_hash, :role)'); + $stmt->execute([ + ':username' => 'admin', + ':full_name' => 'مدير المحل', + ':password_hash' => $defaultPasswordHash, + ':role' => 'admin', + ]); + } elseif (!password_verify('admin123', (string) $adminUser['password_hash']) || (string) $adminUser['full_name'] !== 'مدير المحل' || (string) $adminUser['role'] !== 'admin') { + $stmt = $pdo->prepare('UPDATE app_users SET full_name = :full_name, password_hash = :password_hash, role = :role WHERE id = :id'); + $stmt->execute([ + ':full_name' => 'مدير المحل', + ':password_hash' => $defaultPasswordHash, + ':role' => 'admin', + ':id' => (int) $adminUser['id'], + ]); + } + + $settingCount = (int) fetchValue($pdo, 'SELECT COUNT(*) FROM app_settings WHERE setting_key = :key', [':key' => 'exchange_rate']); + if ($settingCount === 0) { + $stmt = $pdo->prepare('INSERT INTO app_settings (setting_key, setting_value) VALUES (:key, :value)'); + $stmt->execute([ + ':key' => 'exchange_rate', + ':value' => '15000', + ]); + } + + $equipmentCount = (int) fetchValue($pdo, 'SELECT COUNT(*) FROM equipment'); + if ($equipmentCount === 0) { + $stmt = $pdo->prepare('INSERT INTO equipment (name, quantity, price_usd, notes) VALUES (:name, :quantity, :price_usd, :notes)'); + $items = [ + ['طابعة فواتير', 5, 120, 'طابعة حرارية 80 مم'], + ['قارئ باركود', 8, 75, 'مناسب لنقاط البيع'], + ['مولدة صغيرة', 3, 250, 'للأحمال الخفيفة'], + ]; + + foreach ($items as [$name, $quantity, $priceUsd, $notes]) { + $stmt->execute([ + ':name' => $name, + ':quantity' => $quantity, + ':price_usd' => $priceUsd, + ':notes' => $notes, + ]); + } + } +} + +function getSetting(PDO $pdo, string $key, string $default = ''): string +{ + $value = fetchValue($pdo, 'SELECT setting_value FROM app_settings WHERE setting_key = :key LIMIT 1', [':key' => $key]); + return $value !== false ? (string) $value : $default; +} + +function setSetting(PDO $pdo, string $key, string $value): void +{ + $stmt = $pdo->prepare( + 'INSERT INTO app_settings (setting_key, setting_value) VALUES (:key, :value) + ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)' + ); + $stmt->execute([ + ':key' => $key, + ':value' => $value, + ]); +} + +function currentUser(): ?array +{ + return isset($_SESSION['user']) && is_array($_SESSION['user']) ? $_SESSION['user'] : null; +} + +function isLoggedIn(): bool +{ + return currentUser() !== null; +} + +function loginUser(array $user): void +{ + session_regenerate_id(true); + $_SESSION['user'] = [ + 'id' => (int) $user['id'], + 'username' => (string) $user['username'], + 'full_name' => (string) $user['full_name'], + 'role' => (string) $user['role'], + ]; +} + +function logoutUser(): void +{ + $_SESSION = []; + if (ini_get('session.use_cookies')) { + $params = session_get_cookie_params(); + setcookie(session_name(), '', time() - 42000, $params['path'], $params['domain'], (bool) $params['secure'], (bool) $params['httponly']); + } + session_destroy(); +} + +function currentTab(): string +{ + $allowed = ['equipment', 'invoices', 'reports']; + $tab = (string) ($_GET['tab'] ?? 'equipment'); + return in_array($tab, $allowed, true) ? $tab : 'equipment'; +} + +function formatUsd(float $value): string +{ + return '$' . number_format($value, 2); +} + +function formatSyp(float $value): string +{ + return number_format($value, 0) . ' ل.س'; +} + +function getEquipmentRows(PDO $pdo): array +{ + return fetchAllRows($pdo, 'SELECT id, name, quantity, price_usd, notes, created_at, updated_at FROM equipment ORDER BY id DESC'); +} + +function getInvoices(PDO $pdo): array +{ + return fetchAllRows( + $pdo, + 'SELECT id, invoice_number, customer_name, exchange_rate, total_usd, total_syp, created_by, created_at + FROM invoices ORDER BY id DESC LIMIT 100' + ); +} + +function getInvoiceDetails(PDO $pdo, int $invoiceId): ?array +{ + $invoice = fetchRow($pdo, 'SELECT * FROM invoices WHERE id = :id LIMIT 1', [':id' => $invoiceId]); + if (!$invoice) { + return null; + } + + $items = fetchAllRows( + $pdo, + 'SELECT item_name, quantity, unit_price_usd, line_total_usd, line_total_syp + FROM invoice_items WHERE invoice_id = :invoice_id ORDER BY id ASC', + [':invoice_id' => $invoiceId] + ); + + $invoice['items'] = $items; + return $invoice; +} + +$pdo = db(); +ensureSchema($pdo); +seedDefaults($pdo); + +$projectName = $_SERVER['PROJECT_NAME'] ?? 'إدارة محل بسيطة'; +$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'تطبيق مبسط لإدارة المعدات والفواتير والتقارير مع تسعير بالدولار وتحويل فوري إلى الليرة السورية.'; +$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; +$assetCssVersion = (string) (@filemtime(__DIR__ . '/assets/css/custom.css') ?: time()); +$assetJsVersion = (string) (@filemtime(__DIR__ . '/assets/js/main.js') ?: time()); + +$runtimeFlash = null; +$tab = currentTab(); +$quickLogin = (string) ($_GET['quick_login'] ?? ''); +if ($quickLogin === 'demo') { + $demoUser = fetchRow($pdo, 'SELECT * FROM app_users WHERE username = :username LIMIT 1', [':username' => 'admin']); + if ($demoUser) { + loginUser($demoUser); + $runtimeFlash = ['type' => 'success', 'message' => 'تم الدخول التجريبي مباشرة.']; + $tab = 'equipment'; + } else { + $runtimeFlash = ['type' => 'error', 'message' => 'الحساب التجريبي غير موجود.']; + } +} + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $action = ''; + try { + verifyCsrf(); + $action = (string) ($_POST['action'] ?? ''); + + if ($action === 'logout') { + logoutUser(); + redirectTo('index.php'); + } + + if ($action === 'login') { + $username = mb_strtolower(trim((string) ($_POST['username'] ?? ''))); + $password = trim((string) ($_POST['password'] ?? '')); + if ($username === '' || $password === '') { + throw new RuntimeException('أدخل اسم المستخدم وكلمة المرور.'); + } + + $user = fetchRow($pdo, 'SELECT * FROM app_users WHERE username = :username LIMIT 1', [':username' => $username]); + if (!$user || !password_verify($password, (string) $user['password_hash'])) { + throw new RuntimeException('بيانات الدخول غير صحيحة. استخدم بالضبط: admin / admin123'); + } + + loginUser($user); + $runtimeFlash = ['type' => 'success', 'message' => 'تم تسجيل الدخول بنجاح.']; + $tab = 'equipment'; + } + + if (!isLoggedIn()) { + throw new RuntimeException('يرجى تسجيل الدخول أولاً.'); + } + + if ($action === 'save_rate') { + $rate = (float) ($_POST['exchange_rate'] ?? 0); + if ($rate <= 0) { + throw new RuntimeException('أدخل قيمة صحيحة لسعر الدولار اليومي.'); + } + + setSetting($pdo, 'exchange_rate', (string) $rate); + setFlash('success', 'تم تحديث سعر الدولار اليومي.'); + redirectTo('index.php?tab=equipment'); + } + + if ($action === 'add_equipment') { + $name = trim((string) ($_POST['name'] ?? '')); + $quantity = (int) ($_POST['quantity'] ?? 0); + $priceUsd = (float) ($_POST['price_usd'] ?? 0); + $notes = trim((string) ($_POST['notes'] ?? '')); + + if ($name === '') { + throw new RuntimeException('أدخل اسم المعدة.'); + } + if ($quantity < 0) { + throw new RuntimeException('الكمية يجب أن تكون صفر أو أكثر.'); + } + if ($priceUsd <= 0) { + throw new RuntimeException('سعر الدولار يجب أن يكون أكبر من الصفر.'); + } + + $stmt = $pdo->prepare('INSERT INTO equipment (name, quantity, price_usd, notes) VALUES (:name, :quantity, :price_usd, :notes)'); + $stmt->execute([ + ':name' => $name, + ':quantity' => $quantity, + ':price_usd' => $priceUsd, + ':notes' => $notes !== '' ? $notes : null, + ]); + + setFlash('success', 'تمت إضافة المعدة بنجاح.'); + redirectTo('index.php?tab=equipment'); + } + + if ($action === 'create_invoice') { + $equipmentId = (int) ($_POST['equipment_id'] ?? 0); + $quantity = (int) ($_POST['invoice_quantity'] ?? 0); + $customerName = trim((string) ($_POST['customer_name'] ?? '')); + $notes = trim((string) ($_POST['invoice_notes'] ?? '')); + $exchangeRate = (float) getSetting($pdo, 'exchange_rate', '0'); + + if ($equipmentId <= 0) { + throw new RuntimeException('اختر المعدة أولاً.'); + } + if ($quantity <= 0) { + throw new RuntimeException('أدخل كمية صحيحة للفاتورة.'); + } + if ($exchangeRate <= 0) { + throw new RuntimeException('أدخل سعر الدولار اليومي قبل إنشاء أي فاتورة.'); + } + + $pdo->beginTransaction(); + try { + $equipment = fetchRow($pdo, 'SELECT id, name, quantity, price_usd FROM equipment WHERE id = :id FOR UPDATE', [':id' => $equipmentId]); + if (!$equipment) { + throw new RuntimeException('المعدة غير موجودة.'); + } + if ((int) $equipment['quantity'] < $quantity) { + throw new RuntimeException('الكمية المطلوبة أكبر من المخزون المتاح.'); + } + + $lineTotalUsd = round((float) $equipment['price_usd'] * $quantity, 2); + $lineTotalSyp = (int) round($lineTotalUsd * $exchangeRate); + + $invoiceStmt = $pdo->prepare( + 'INSERT INTO invoices (invoice_number, customer_name, exchange_rate, total_usd, total_syp, notes, created_by) + VALUES (:invoice_number, :customer_name, :exchange_rate, :total_usd, :total_syp, :notes, :created_by)' + ); + + $tempNumber = 'TMP-' . bin2hex(random_bytes(4)); + $invoiceStmt->execute([ + ':invoice_number' => $tempNumber, + ':customer_name' => $customerName !== '' ? $customerName : null, + ':exchange_rate' => $exchangeRate, + ':total_usd' => $lineTotalUsd, + ':total_syp' => $lineTotalSyp, + ':notes' => $notes !== '' ? $notes : null, + ':created_by' => currentUser()['full_name'] ?? 'admin', + ]); + + $invoiceId = (int) $pdo->lastInsertId(); + $invoiceNumber = 'INV-' . gmdate('Ymd') . '-' . str_pad((string) $invoiceId, 4, '0', STR_PAD_LEFT); + + $updateNumberStmt = $pdo->prepare('UPDATE invoices SET invoice_number = :invoice_number WHERE id = :id'); + $updateNumberStmt->execute([ + ':invoice_number' => $invoiceNumber, + ':id' => $invoiceId, + ]); + + $itemStmt = $pdo->prepare( + 'INSERT INTO invoice_items (invoice_id, equipment_id, item_name, quantity, unit_price_usd, line_total_usd, line_total_syp) + VALUES (:invoice_id, :equipment_id, :item_name, :quantity, :unit_price_usd, :line_total_usd, :line_total_syp)' + ); + $itemStmt->execute([ + ':invoice_id' => $invoiceId, + ':equipment_id' => $equipmentId, + ':item_name' => $equipment['name'], + ':quantity' => $quantity, + ':unit_price_usd' => $equipment['price_usd'], + ':line_total_usd' => $lineTotalUsd, + ':line_total_syp' => $lineTotalSyp, + ]); + + $stockStmt = $pdo->prepare('UPDATE equipment SET quantity = quantity - :qty WHERE id = :id'); + $stockStmt->execute([ + ':qty' => $quantity, + ':id' => $equipmentId, + ]); + + $pdo->commit(); + setFlash('success', 'تم إنشاء الفاتورة وتحديث المخزون تلقائياً.'); + redirectTo('index.php?tab=invoices&invoice_id=' . $invoiceId); + } catch (Throwable $exception) { + $pdo->rollBack(); + throw $exception; + } + } + } catch (Throwable $exception) { + if ($action === 'login') { + $runtimeFlash = ['type' => 'error', 'message' => $exception->getMessage()]; + $tab = 'equipment'; + } else { + setFlash('error', $exception->getMessage()); + $tab = currentTab(); + redirectTo('index.php?tab=' . $tab); + } + } +} + +$flash = $runtimeFlash ?? pullFlash(); +$csrfToken = csrfToken(); +$exchangeRate = (float) getSetting($pdo, 'exchange_rate', '15000'); +$equipmentRows = getEquipmentRows($pdo); +$invoiceRows = getInvoices($pdo); +$selectedInvoiceId = (int) ($_GET['invoice_id'] ?? ($invoiceRows[0]['id'] ?? 0)); +$selectedInvoice = $selectedInvoiceId > 0 ? getInvoiceDetails($pdo, $selectedInvoiceId) : null; +$tab = currentTab(); +$user = currentUser(); + +$totalEquipmentTypes = (int) fetchValue($pdo, 'SELECT COUNT(*) FROM equipment'); +$totalStockUnits = (int) fetchValue($pdo, 'SELECT COALESCE(SUM(quantity), 0) FROM equipment'); +$totalInvoices = (int) fetchValue($pdo, 'SELECT COUNT(*) FROM invoices'); +$totalSalesSyp = (float) fetchValue($pdo, 'SELECT COALESCE(SUM(total_syp), 0) FROM invoices'); +$todaySalesSyp = (float) fetchValue($pdo, 'SELECT COALESCE(SUM(total_syp), 0) FROM invoices WHERE DATE(created_at) = UTC_DATE()'); +$todayInvoices = (int) fetchValue($pdo, 'SELECT COUNT(*) FROM invoices WHERE DATE(created_at) = UTC_DATE()'); ?> - + - - - New Style - - - - - - - - + + + <?= e($projectName) ?> | إدارة محل بسيطة + + + + + + + + - - - - - - - - - - + - -
-
-

Analyzing your requirements and generating your website…

-
- Loading… -
-

AI is collecting your requirements and applying the first changes.

-

This page will update automatically as the plan is implemented.

-

Runtime: PHP — UTC

+ +
+
+
+

Store Admin

+

+

+ +

+
+
+ + +
+
+ + + + +
+ + + +
+ +
+ +
+ + +
+
+

مرحبا،

+

تطبيق مبسط فعلاً

+

كل الأسعار الأساسية بالدولار، والتحويل إلى الليرة السورية يتم حسب سعر اليوم فقط.

+
+
+
+ + + +
+
+
+ + + + +
+
+
+

سعر الدولار اليومي

+

هذا هو الرقم الوحيد الذي يتغير، أما أسعار المعدات فتظل ثابتة بالدولار.

+
+
+ + + +
+ المعاينة الحالية بالليرة: + +
+ +
+
+ +
+
+

إضافة معدة

+

أدخل السعر بالدولار، والتطبيق سيحسب الليرة السورية عند البيع فقط.

+
+
+ + + +
+ + +
+ +
+ معاينة السعر بالليرة حسب اليوم: + +
+ +
+
+
+ +
+
+
+

قائمة المعدات

+

نوع / قطعة في المخزون

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
المعدةالكميةالسعر بالدولارالسعر بالليرةملاحظات
لا توجد معدات حتى الآن.
+
+
+ +
+
+
+

إنشاء فاتورة

+

اختر معدة واحدة، أدخل الكمية، وسيتم خصمها من المخزون مباشرة.

+
+
+ + + +
+ + +
+ +
+
المخزون الحالي:
+
إجمالي الفاتورة بالليرة:
+
+ +
+
+ +
+
+
+

الفاتورة المحددة

+

اختر فاتورة من الجدول أو أنشئ واحدة جديدة لتظهر هنا.

+
+
+ + +
+
+ + +
لا توجد فاتورة محددة بعد.
+ +
+
رقم الفاتورة
+
العميل
+
سعر الدولار
+
الإجمالي بالدولار
+
الإجمالي بالليرة
+
التاريخ
+
+

العناصر

+ +
+ × + +
+ +
+

+
+ +
+
+ +
+
+
+

سجل الفواتير

+

آخر 100 فاتورة مع إمكانية تصدير القائمة كاملة كملف PDF.

+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
رقم الفاتورةالعميلالإجمالي بالدولارالإجمالي بالليرةالتاريخ
لا توجد فواتير حتى الآن.
+
+
+ +
+
+ أنواع المعدات + +
+
+ إجمالي المخزون + قطعة +
+
+ فواتير اليوم + +
+
+ مبيعات اليوم + +
+
+ +
+
+
+

ملخص سريع

+

قراءة مباشرة لوضع المحل اليوم.

+
+
    +
  • عدد الفواتير الكلي
  • +
  • إجمالي المبيعات
  • +
  • سعر الدولار الحالي
  • +
  • آخر تحديث للأسعار
  • +
+
+ +
+
+

آخر الفواتير

+

أحدث الحركات المالية داخل التطبيق.

+
+
+ +
لا توجد بيانات بعد.
+ + +
+
+ +

+
+ +
+ + +
+
+
+ +
+
-
- + + + +