Spaces:
Sleeping
Sleeping
Commit Β·
5f1152c
1
Parent(s): e31dd88
fix: serve frontend from root
Browse files- frontend/_pdf_ref/sajid_invoice_p1.png +0 -0
- frontend/css/style.css +0 -432
- frontend/img/logo.jpeg +0 -0
- frontend/index.html +0 -224
- frontend/js/app.js +0 -218
- frontend/js/data.js +0 -45
- frontend/js/db.js +0 -82
- frontend/js/history.js +0 -129
- frontend/js/rows.js +0 -153
- frontend/js/ui.js +0 -68
frontend/_pdf_ref/sajid_invoice_p1.png
DELETED
|
Binary file (93.4 kB)
|
|
|
frontend/css/style.css
DELETED
|
@@ -1,432 +0,0 @@
|
|
| 1 |
-
/* βββββββββββββββββββββββββββββββββββββββββ
|
| 2 |
-
css/style.css β SmiloCAD Invoice System
|
| 3 |
-
βββββββββββββββββββββββββββββββββββββββββ */
|
| 4 |
-
|
| 5 |
-
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
| 6 |
-
|
| 7 |
-
:root {
|
| 8 |
-
--blue-deep: #1B3A6B;
|
| 9 |
-
--blue-mid: #2563B0;
|
| 10 |
-
--blue-light: #EBF3FF;
|
| 11 |
-
--blue-accent: #3B82F6;
|
| 12 |
-
--teal: #0891B2;
|
| 13 |
-
--teal-light: #E0F7FA;
|
| 14 |
-
--gray-50: #F8FAFC;
|
| 15 |
-
--gray-100: #F1F5F9;
|
| 16 |
-
--gray-200: #E2E8F0;
|
| 17 |
-
--gray-300: #CBD5E1;
|
| 18 |
-
--gray-400: #94A3B8;
|
| 19 |
-
--gray-500: #64748B;
|
| 20 |
-
--gray-700: #334155;
|
| 21 |
-
--gray-900: #0F172A;
|
| 22 |
-
--white: #FFFFFF;
|
| 23 |
-
--green: #10B981;
|
| 24 |
-
--green-light: #D1FAE5;
|
| 25 |
-
--red: #EF4444;
|
| 26 |
-
--red-light: #FEE2E2;
|
| 27 |
-
--amber: #F59E0B;
|
| 28 |
-
--amber-light: #FEF3C7;
|
| 29 |
-
--shadow-sm: 0 1px 3px rgba(0,0,0,0.07), 0 1px 2px rgba(0,0,0,0.05);
|
| 30 |
-
--shadow: 0 4px 16px rgba(27,58,107,0.10);
|
| 31 |
-
--shadow-lg: 0 8px 40px rgba(27,58,107,0.14);
|
| 32 |
-
--radius: 10px;
|
| 33 |
-
--radius-lg: 16px;
|
| 34 |
-
}
|
| 35 |
-
|
| 36 |
-
body {
|
| 37 |
-
font-family: 'Nunito', sans-serif;
|
| 38 |
-
background: var(--gray-100);
|
| 39 |
-
color: var(--gray-900);
|
| 40 |
-
min-height: 100vh;
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
/* βββ TOP BAR βββ */
|
| 44 |
-
.topbar {
|
| 45 |
-
background: linear-gradient(135deg, var(--blue-deep) 0%, #1e4d8c 60%, var(--teal) 100%);
|
| 46 |
-
padding: 0 2rem;
|
| 47 |
-
height: 62px;
|
| 48 |
-
display: flex;
|
| 49 |
-
align-items: center;
|
| 50 |
-
justify-content: space-between;
|
| 51 |
-
box-shadow: 0 2px 20px rgba(27,58,107,0.35);
|
| 52 |
-
position: sticky;
|
| 53 |
-
top: 0;
|
| 54 |
-
z-index: 200;
|
| 55 |
-
}
|
| 56 |
-
.topbar-brand { display: flex; align-items: center; gap: 12px; }
|
| 57 |
-
.topbar-icon { width: 38px; height: 38px; background: rgba(255,255,255,0.15); border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 20px; border: 1px solid rgba(255,255,255,0.25); }
|
| 58 |
-
.topbar-logo { height: 38px; width: auto; max-width: 160px; background: #fff; padding: 4px 6px; border-radius: 8px; object-fit: contain; box-shadow: 0 2px 6px rgba(0,0,0,0.15); }
|
| 59 |
-
.topbar-name { font-family: 'Lora', serif; font-size: 1.12rem; font-weight: 700; color: #fff; letter-spacing: 0.01em; }
|
| 60 |
-
.topbar-sub { font-size: 0.7rem; color: rgba(255,255,255,0.65); letter-spacing: 0.1em; text-transform: uppercase; margin-top: 1px; }
|
| 61 |
-
.topbar-tabs { display: flex; gap: 4px; }
|
| 62 |
-
.topbar-tab { padding: 7px 18px; border-radius: 7px; border: none; background: transparent; color: rgba(255,255,255,0.7); font-family: 'Nunito', sans-serif; font-size: 0.85rem; font-weight: 600; cursor: pointer; transition: all 0.18s; letter-spacing: 0.02em; }
|
| 63 |
-
.topbar-tab:hover { background: rgba(255,255,255,0.12); color: #fff; }
|
| 64 |
-
.topbar-tab.active { background: rgba(255,255,255,0.22); color: #fff; }
|
| 65 |
-
.topbar-info { display: flex; align-items: center; gap: 8px; font-size: 0.78rem; color: rgba(255,255,255,0.65); }
|
| 66 |
-
.topbar-dot { width: 4px; height: 4px; background: rgba(255,255,255,0.35); border-radius: 50%; }
|
| 67 |
-
|
| 68 |
-
/* βββ LAYOUT βββ */
|
| 69 |
-
.app-body { padding: 2rem; max-width: 1080px; margin: 0 auto; }
|
| 70 |
-
|
| 71 |
-
/* βββ INVOICE CARD βββ */
|
| 72 |
-
.inv-card {
|
| 73 |
-
background: var(--white);
|
| 74 |
-
border-radius: var(--radius-lg);
|
| 75 |
-
box-shadow: var(--shadow-lg);
|
| 76 |
-
overflow: hidden;
|
| 77 |
-
animation: fadeUp 0.35s ease;
|
| 78 |
-
}
|
| 79 |
-
@keyframes fadeUp {
|
| 80 |
-
from { opacity: 0; transform: translateY(14px); }
|
| 81 |
-
to { opacity: 1; transform: translateY(0); }
|
| 82 |
-
}
|
| 83 |
-
|
| 84 |
-
/* βββ INVOICE HEADER BAND βββ */
|
| 85 |
-
.inv-header-band {
|
| 86 |
-
background: linear-gradient(135deg, var(--blue-deep) 0%, #1e4d8c 60%, var(--teal) 100%);
|
| 87 |
-
padding: 2rem 2.5rem;
|
| 88 |
-
display: grid;
|
| 89 |
-
grid-template-columns: 1fr auto;
|
| 90 |
-
gap: 2rem;
|
| 91 |
-
align-items: center;
|
| 92 |
-
}
|
| 93 |
-
.lab-title { font-family: 'Lora', serif; font-size: 1.7rem; font-weight: 700; color: #fff; letter-spacing: 0.01em; line-height: 1.2; }
|
| 94 |
-
.lab-meta { display: flex; align-items: center; gap: 16px; margin-top: 6px; flex-wrap: wrap; color: #fff; }
|
| 95 |
-
.lab-meta-item { display: flex; align-items: center; gap: 5px; font-size: 0.78rem; color: rgba(255,255,255,0.8); }
|
| 96 |
-
.inv-badge-block { text-align: right; }
|
| 97 |
-
.inv-badge-label { font-family: 'Lora', serif; font-size: 2.2rem; font-weight: 700; color: rgba(255,255,255,0.95); text-transform: uppercase; letter-spacing: 0.1em; line-height: 1; }
|
| 98 |
-
.inv-number-pill { display: inline-flex; align-items: center; gap: 6px; background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.3); border-radius: 20px; padding: 4px 12px; font-size: 0.82rem; font-weight: 700; color: rgba(255,255,255,0.9); margin-top: 6px; font-family: monospace; letter-spacing: 0.05em; }
|
| 99 |
-
|
| 100 |
-
/* βββ STATUS BADGES βββ */
|
| 101 |
-
.status-badge { display: inline-block; padding: 3px 10px; border-radius: 20px; font-size: 0.7rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.08em; margin-top: 6px; }
|
| 102 |
-
.status-pending { background: var(--amber-light); color: #92400E; }
|
| 103 |
-
.status-paid { background: var(--green-light); color: #065F46; }
|
| 104 |
-
.status-partial { background: var(--teal-light); color: #155E75; }
|
| 105 |
-
.status-cancelled{ background: var(--red-light); color: #991B1B; }
|
| 106 |
-
|
| 107 |
-
/* βββ SECTIONS βββ */
|
| 108 |
-
.inv-section { padding: 1.4rem 2.5rem; border-bottom: 1px solid var(--gray-200); }
|
| 109 |
-
.section-title { font-size: 0.68rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.14em; color: var(--blue-mid); margin-bottom: 1rem; display: flex; align-items: center; gap: 8px; }
|
| 110 |
-
.section-title::after { content: ''; flex: 1; height: 1.5px; background: linear-gradient(to right, var(--blue-light), transparent); }
|
| 111 |
-
|
| 112 |
-
/* βββ FORM βββ */
|
| 113 |
-
.form-grid { display: grid; gap: 12px; }
|
| 114 |
-
.form-grid-3 { grid-template-columns: repeat(3, 1fr); }
|
| 115 |
-
.form-grid-2 { grid-template-columns: repeat(2, 1fr); }
|
| 116 |
-
.form-grid-4 { grid-template-columns: repeat(4, 1fr); }
|
| 117 |
-
.form-group { display: flex; flex-direction: column; gap: 4px; }
|
| 118 |
-
.form-group label { font-size: 0.71rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--gray-500); }
|
| 119 |
-
.form-control { padding: 9px 12px; border: 1.5px solid var(--gray-200); border-radius: 8px; font-family: 'Nunito', sans-serif; font-size: 0.88rem; color: var(--gray-900); background: var(--white); outline: none; transition: border-color 0.18s, box-shadow 0.18s; width: 100%; }
|
| 120 |
-
.form-control:focus { border-color: var(--blue-accent); box-shadow: 0 0 0 3px rgba(59,130,246,0.1); }
|
| 121 |
-
.form-control::placeholder { color: var(--gray-400); }
|
| 122 |
-
|
| 123 |
-
/* βββ SERVICES TABLE βββ */
|
| 124 |
-
.table-wrap { overflow-x: auto; }
|
| 125 |
-
table.svc-table { width: 100%; border-collapse: collapse; font-size: 0.84rem; }
|
| 126 |
-
.svc-table thead tr { background: var(--blue-deep); }
|
| 127 |
-
.svc-table thead th { padding: 11px 10px; text-align: left; font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.09em; color: rgba(255,255,255,0.85); white-space: nowrap; }
|
| 128 |
-
.svc-table thead th.center { text-align: center; }
|
| 129 |
-
.svc-table thead th.right { text-align: right; }
|
| 130 |
-
.svc-table tbody tr { border-bottom: 1px solid var(--gray-100); transition: background 0.12s; }
|
| 131 |
-
.svc-table tbody tr:nth-child(even) { background: var(--gray-50); }
|
| 132 |
-
.svc-table tbody tr:hover { background: var(--blue-light); }
|
| 133 |
-
.svc-table td { padding: 6px 6px; vertical-align: middle; }
|
| 134 |
-
.svc-table td.sno { text-align: center; font-weight: 700; color: var(--gray-400); font-size: 0.8rem; width: 42px; }
|
| 135 |
-
.svc-table td.total-cell { text-align: right; font-weight: 700; color: var(--blue-deep); font-size: 0.9rem; padding-right: 10px; white-space: nowrap; }
|
| 136 |
-
.svc-table td.action-cell { text-align: center; width: 40px; }
|
| 137 |
-
|
| 138 |
-
.tbl-input { width: 100%; border: 1.5px solid transparent; border-radius: 6px; padding: 7px 9px; font-family: 'Nunito', sans-serif; font-size: 0.84rem; background: transparent; color: var(--gray-900); outline: none; transition: border-color 0.15s, background 0.15s; }
|
| 139 |
-
.tbl-input:focus { border-color: var(--blue-accent); background: var(--white); }
|
| 140 |
-
.tbl-input.right { text-align: right; }
|
| 141 |
-
.tbl-select { width: 100%; border: 1.5px solid transparent; border-radius: 6px; padding: 7px 9px; font-family: 'Nunito', sans-serif; font-size: 0.84rem; background: transparent; color: var(--gray-900); outline: none; cursor: pointer; transition: border-color 0.15s, background 0.15s; }
|
| 142 |
-
.tbl-select:focus { border-color: var(--blue-accent); background: var(--white); }
|
| 143 |
-
|
| 144 |
-
.btn-del { width: 28px; height: 28px; border: none; border-radius: 7px; background: var(--red-light); color: var(--red); font-size: 15px; cursor: pointer; display: flex; align-items: center; justify-content: center; margin: auto; transition: all 0.15s; font-weight: 700; }
|
| 145 |
-
.btn-del:hover { background: var(--red); color: #fff; transform: scale(1.1); }
|
| 146 |
-
|
| 147 |
-
.add-row-btn { margin-top: 10px; padding: 9px 22px; border: 2px dashed var(--blue-accent); border-radius: 8px; background: transparent; color: var(--blue-mid); font-family: 'Nunito', sans-serif; font-size: 0.85rem; font-weight: 700; cursor: pointer; transition: all 0.18s; letter-spacing: 0.03em; }
|
| 148 |
-
.add-row-btn:hover { background: var(--blue-light); border-color: var(--blue-deep); color: var(--blue-deep); }
|
| 149 |
-
|
| 150 |
-
/* βββ SUMMARY βββ */
|
| 151 |
-
.summary-row { display: flex; justify-content: space-between; align-items: center; padding: 9px 0; border-bottom: 1px solid var(--gray-100); font-size: 0.9rem; }
|
| 152 |
-
.summary-row:last-child { border-bottom: none; }
|
| 153 |
-
.summary-row .label { color: var(--gray-500); font-weight: 600; }
|
| 154 |
-
.summary-row .value { font-weight: 700; color: var(--gray-900); font-size: 0.95rem; }
|
| 155 |
-
.summary-row.net { background: var(--blue-deep); margin: 8px -2.5rem -1.4rem; padding: 16px 2.5rem; border-bottom: none; }
|
| 156 |
-
.summary-row.net .label { color: rgba(255,255,255,0.8); font-size: 0.85rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; }
|
| 157 |
-
.summary-row.net .value { color: #fff; font-family: 'Lora', serif; font-size: 1.5rem; }
|
| 158 |
-
.summary-row.received .value { color: var(--green); }
|
| 159 |
-
.summary-row.remaining .value { color: var(--red); }
|
| 160 |
-
|
| 161 |
-
.received-input { width: 160px; text-align: right; padding: 7px 10px; border: 1.5px solid var(--gray-200); border-radius: 7px; font-family: 'Nunito', sans-serif; font-size: 0.9rem; font-weight: 700; color: var(--green); outline: none; transition: border-color 0.18s; }
|
| 162 |
-
.received-input:focus { border-color: var(--green); box-shadow: 0 0 0 3px rgba(16,185,129,0.1); }
|
| 163 |
-
|
| 164 |
-
/* βββ FOOTER / SIGNATURE βββ */
|
| 165 |
-
.inv-footer { padding: 1.2rem 2.5rem; background: var(--gray-50); border-top: 1px solid var(--gray-200); display: flex; align-items: center; justify-content: space-between; font-size: 0.82rem; color: var(--gray-500); }
|
| 166 |
-
.sig-block { text-align: right; }
|
| 167 |
-
.sig-line { border-top: 1.5px solid var(--gray-400); width: 180px; margin-top: 28px; padding-top: 6px; font-weight: 700; color: var(--gray-700); }
|
| 168 |
-
|
| 169 |
-
/* βββ ACTION BAR βββ */
|
| 170 |
-
.action-bar { padding: 1.2rem 2.5rem; background: var(--white); border-top: 1px solid var(--gray-200); display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
|
| 171 |
-
.btn { padding: 10px 22px; border-radius: 9px; border: none; font-family: 'Nunito', sans-serif; font-size: 0.86rem; font-weight: 700; cursor: pointer; transition: all 0.18s; display: flex; align-items: center; gap: 7px; letter-spacing: 0.02em; white-space: nowrap; }
|
| 172 |
-
.btn-save { background: var(--blue-deep); color: #fff; }
|
| 173 |
-
.btn-save:hover { background: #162f5a; transform: translateY(-1px); box-shadow: var(--shadow); }
|
| 174 |
-
.btn-pdf { background: var(--teal); color: #fff; }
|
| 175 |
-
.btn-pdf:hover { background: #0e7490; transform: translateY(-1px); box-shadow: var(--shadow); }
|
| 176 |
-
.btn-print { background: var(--gray-700); color: #fff; }
|
| 177 |
-
.btn-print:hover { background: var(--gray-900); transform: translateY(-1px); box-shadow: var(--shadow); }
|
| 178 |
-
.btn-excel { background: var(--green); color: #fff; }
|
| 179 |
-
.btn-excel:hover { background: #059669; transform: translateY(-1px); box-shadow: var(--shadow); }
|
| 180 |
-
.btn-clear { background: var(--amber-light); color: #92400E; border: 1.5px solid #FDE68A; }
|
| 181 |
-
.btn-clear:hover { background: #FDE68A; }
|
| 182 |
-
.btn-new { background: var(--blue-light); color: var(--blue-deep); border: 1.5px solid #BFDBFE; }
|
| 183 |
-
.btn-new:hover { background: #BFDBFE; }
|
| 184 |
-
.btn-spacer { flex: 1; }
|
| 185 |
-
|
| 186 |
-
/* βββ HISTORY PAGE βββ */
|
| 187 |
-
.page-heading { display: flex; align-items: baseline; gap: 12px; margin-bottom: 1.5rem; }
|
| 188 |
-
.page-heading h2 { font-family: 'Lora', serif; font-size: 1.5rem; font-weight: 700; color: var(--blue-deep); }
|
| 189 |
-
.count-pill { background: var(--blue-light); color: var(--blue-mid); padding: 3px 12px; border-radius: 20px; font-size: 0.78rem; font-weight: 700; border: 1px solid #BFDBFE; }
|
| 190 |
-
.search-wrap { margin-bottom: 1.2rem; position: relative; }
|
| 191 |
-
.search-wrap input { width: 100%; padding: 10px 14px 10px 40px; border: 1.5px solid var(--gray-200); border-radius: 9px; font-family: 'Nunito', sans-serif; font-size: 0.9rem; outline: none; background: var(--white); transition: border-color 0.18s; }
|
| 192 |
-
.search-wrap input:focus { border-color: var(--blue-accent); }
|
| 193 |
-
.search-icon { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--gray-400); font-size: 16px; }
|
| 194 |
-
|
| 195 |
-
.hist-list { display: flex; flex-direction: column; gap: 10px; }
|
| 196 |
-
.hist-item { background: var(--white); border-radius: var(--radius); padding: 1rem 1.4rem; box-shadow: var(--shadow-sm); display: grid; grid-template-columns: auto 1fr 1fr auto auto; gap: 1rem; align-items: center; border-left: 4px solid transparent; transition: box-shadow 0.18s, border-color 0.18s; }
|
| 197 |
-
.hist-item:hover { box-shadow: var(--shadow); border-left-color: var(--blue-accent); }
|
| 198 |
-
.hist-inv-no { font-size: 0.78rem; font-weight: 800; font-family: monospace; color: var(--blue-mid); background: var(--blue-light); padding: 4px 10px; border-radius: 6px; border: 1px solid #BFDBFE; white-space: nowrap; }
|
| 199 |
-
.hist-client-name { font-weight: 700; font-size: 0.95rem; }
|
| 200 |
-
.hist-date { font-size: 0.78rem; color: var(--gray-400); }
|
| 201 |
-
.hist-detail { font-size: 0.82rem; color: var(--gray-500); }
|
| 202 |
-
.hist-detail span { font-weight: 600; color: var(--gray-700); }
|
| 203 |
-
.hist-amount { font-family: 'Lora', serif; font-size: 1.05rem; font-weight: 700; color: var(--blue-deep); text-align: right; }
|
| 204 |
-
.hist-actions { display: flex; gap: 6px; }
|
| 205 |
-
.btn-xs { padding: 6px 14px; border-radius: 6px; border: none; font-family: 'Nunito', sans-serif; font-size: 0.78rem; font-weight: 700; cursor: pointer; transition: all 0.15s; }
|
| 206 |
-
.btn-xs-blue { background: var(--blue-deep); color: #fff; }
|
| 207 |
-
.btn-xs-blue:hover { background: #162f5a; }
|
| 208 |
-
.btn-xs-red { background: var(--red-light); color: var(--red); }
|
| 209 |
-
.btn-xs-red:hover { background: var(--red); color: #fff; }
|
| 210 |
-
|
| 211 |
-
.empty-state { text-align: center; padding: 5rem 2rem; color: var(--gray-400); }
|
| 212 |
-
.empty-state .ico { font-size: 3.5rem; margin-bottom: 1rem; }
|
| 213 |
-
.empty-state h3 { font-family: 'Lora', serif; color: var(--gray-700); font-size: 1.3rem; margin-bottom: 6px; }
|
| 214 |
-
|
| 215 |
-
/* βββ TOAST βββ */
|
| 216 |
-
#toast {
|
| 217 |
-
position: fixed;
|
| 218 |
-
bottom: 2rem; right: 2rem;
|
| 219 |
-
background: var(--blue-deep);
|
| 220 |
-
color: #fff;
|
| 221 |
-
padding: 12px 20px;
|
| 222 |
-
border-radius: 10px;
|
| 223 |
-
font-size: 0.87rem;
|
| 224 |
-
font-weight: 600;
|
| 225 |
-
box-shadow: var(--shadow-lg);
|
| 226 |
-
display: flex;
|
| 227 |
-
align-items: center;
|
| 228 |
-
gap: 8px;
|
| 229 |
-
opacity: 0;
|
| 230 |
-
transform: translateY(16px);
|
| 231 |
-
transition: all 0.3s cubic-bezier(0.34,1.56,0.64,1);
|
| 232 |
-
z-index: 9999;
|
| 233 |
-
pointer-events: none;
|
| 234 |
-
}
|
| 235 |
-
#toast.show { opacity: 1; transform: translateY(0); }
|
| 236 |
-
#toast.success { border-left: 4px solid var(--green); }
|
| 237 |
-
#toast.error { border-left: 4px solid var(--red); }
|
| 238 |
-
#toast.info { border-left: 4px solid var(--teal); }
|
| 239 |
-
|
| 240 |
-
/* βββ PRINT βββ */
|
| 241 |
-
.print-sheet { display: none; max-width: 820px; margin: 0 auto; font-family: 'Nunito', sans-serif; color: #222; }
|
| 242 |
-
.print-sheet, .print-sheet * { box-sizing: border-box; }
|
| 243 |
-
.print-header {
|
| 244 |
-
background: #1e5c78;
|
| 245 |
-
color: #fff;
|
| 246 |
-
padding: 22px 26px;
|
| 247 |
-
display: flex;
|
| 248 |
-
align-items: center;
|
| 249 |
-
justify-content: space-between;
|
| 250 |
-
}
|
| 251 |
-
.print-title {
|
| 252 |
-
font-size: 2.1rem;
|
| 253 |
-
font-weight: 800;
|
| 254 |
-
letter-spacing: 0.08em;
|
| 255 |
-
}
|
| 256 |
-
.print-brand { text-align: right; font-size: 0.75rem; line-height: 1.45; }
|
| 257 |
-
.print-brand-name { font-weight: 700; font-size: 0.8rem; margin-bottom: 2px; }
|
| 258 |
-
.print-info {
|
| 259 |
-
padding: 18px 26px 8px;
|
| 260 |
-
display: grid;
|
| 261 |
-
grid-template-columns: 1fr 1fr;
|
| 262 |
-
gap: 24px;
|
| 263 |
-
font-size: 0.76rem;
|
| 264 |
-
}
|
| 265 |
-
.print-info-title { font-weight: 700; margin-bottom: 6px; }
|
| 266 |
-
.print-info-row { display: flex; gap: 8px; margin: 2px 0; }
|
| 267 |
-
.print-info-row .k { width: 120px; font-weight: 600; color: #333; }
|
| 268 |
-
.print-info-row .v { color: #333; }
|
| 269 |
-
|
| 270 |
-
.print-table {
|
| 271 |
-
width: 100%;
|
| 272 |
-
border-collapse: collapse;
|
| 273 |
-
font-size: 0.75rem;
|
| 274 |
-
margin: 6px 0 0;
|
| 275 |
-
}
|
| 276 |
-
.print-table thead th {
|
| 277 |
-
background: #1e5c78;
|
| 278 |
-
color: #fff;
|
| 279 |
-
padding: 6px 8px;
|
| 280 |
-
text-align: left;
|
| 281 |
-
font-weight: 700;
|
| 282 |
-
}
|
| 283 |
-
.print-table thead th.c { text-align: center; }
|
| 284 |
-
.print-table thead th.r { text-align: right; }
|
| 285 |
-
.print-table tbody td {
|
| 286 |
-
padding: 6px 8px;
|
| 287 |
-
border-bottom: 1px solid #d7e1e8;
|
| 288 |
-
}
|
| 289 |
-
.print-table tbody td.c { text-align: center; }
|
| 290 |
-
.print-table tbody td.r { text-align: right; }
|
| 291 |
-
|
| 292 |
-
.print-summary {
|
| 293 |
-
padding: 10px 26px 0;
|
| 294 |
-
display: grid;
|
| 295 |
-
grid-template-columns: 1fr 280px;
|
| 296 |
-
gap: 24px;
|
| 297 |
-
font-size: 0.75rem;
|
| 298 |
-
}
|
| 299 |
-
.print-terms { color: #333; }
|
| 300 |
-
.print-totals { display: grid; gap: 6px; }
|
| 301 |
-
.tot-row { display: flex; justify-content: space-between; }
|
| 302 |
-
.tot-row.total { border-top: 2px solid #7a2a2a; padding-top: 6px; font-weight: 700; }
|
| 303 |
-
|
| 304 |
-
.print-footer {
|
| 305 |
-
margin: 22px 26px 0;
|
| 306 |
-
background: #1e5c78;
|
| 307 |
-
color: #fff;
|
| 308 |
-
text-align: center;
|
| 309 |
-
padding: 10px 12px;
|
| 310 |
-
font-size: 0.75rem;
|
| 311 |
-
font-weight: 600;
|
| 312 |
-
}
|
| 313 |
-
|
| 314 |
-
@media print {
|
| 315 |
-
body { background: #fff !important; }
|
| 316 |
-
.topbar, .action-bar, .btn-del, .add-row-btn, #toast, #page-history, #page-invoice { display: none !important; }
|
| 317 |
-
.app-body { padding: 0 !important; max-width: none !important; }
|
| 318 |
-
.print-sheet { display: block; }
|
| 319 |
-
.print-header, .print-table thead th, .print-footer {
|
| 320 |
-
-webkit-print-color-adjust: exact;
|
| 321 |
-
print-color-adjust: exact;
|
| 322 |
-
}
|
| 323 |
-
}
|
| 324 |
-
|
| 325 |
-
/* βββ RESPONSIVE βββ */
|
| 326 |
-
@media (max-width: 768px) {
|
| 327 |
-
.app-body { padding: 1rem; }
|
| 328 |
-
.inv-header-band { grid-template-columns: 1fr; }
|
| 329 |
-
.inv-badge-block { text-align: left; }
|
| 330 |
-
.form-grid-3, .form-grid-4 { grid-template-columns: 1fr 1fr; }
|
| 331 |
-
.form-grid-2 { grid-template-columns: 1fr; }
|
| 332 |
-
.topbar-info { display: none; }
|
| 333 |
-
.hist-item { grid-template-columns: 1fr 1fr; grid-template-rows: auto auto; }
|
| 334 |
-
.inv-section { padding: 1.2rem 1.2rem; }
|
| 335 |
-
.inv-header-band { padding: 1.5rem 1.2rem; }
|
| 336 |
-
.action-bar { padding: 1rem 1.2rem; }
|
| 337 |
-
}
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
/* βββ SUMMARY SECTION (NEW DESIGN) βββ */
|
| 341 |
-
.inv-section.summary {
|
| 342 |
-
padding: 1.4rem 2.5rem;
|
| 343 |
-
border-bottom: none;
|
| 344 |
-
}
|
| 345 |
-
|
| 346 |
-
.summary-container {
|
| 347 |
-
max-width: 380px;
|
| 348 |
-
margin-left: auto;
|
| 349 |
-
margin-bottom: 20px;
|
| 350 |
-
}
|
| 351 |
-
|
| 352 |
-
.summary-row {
|
| 353 |
-
display: flex;
|
| 354 |
-
justify-content: space-between;
|
| 355 |
-
align-items: center;
|
| 356 |
-
padding: 10px 0;
|
| 357 |
-
font-size: 0.95rem;
|
| 358 |
-
border-bottom: 1px solid var(--gray-100);
|
| 359 |
-
}
|
| 360 |
-
|
| 361 |
-
.summary-row .label {
|
| 362 |
-
color: var(--gray-700);
|
| 363 |
-
font-weight: 700;
|
| 364 |
-
font-size: 0.85rem;
|
| 365 |
-
}
|
| 366 |
-
|
| 367 |
-
.summary-row .value {
|
| 368 |
-
font-weight: 800;
|
| 369 |
-
color: var(--gray-900);
|
| 370 |
-
font-size: 1rem;
|
| 371 |
-
}
|
| 372 |
-
|
| 373 |
-
/* Green styling for Remaining Balance as per screenshot */
|
| 374 |
-
.summary-row.remaining .value {
|
| 375 |
-
color: var(--green);
|
| 376 |
-
}
|
| 377 |
-
|
| 378 |
-
.received-input {
|
| 379 |
-
width: 140px;
|
| 380 |
-
text-align: right;
|
| 381 |
-
padding: 8px 12px;
|
| 382 |
-
border: 1.5px solid var(--gray-200);
|
| 383 |
-
border-radius: 8px;
|
| 384 |
-
font-family: 'Nunito', sans-serif;
|
| 385 |
-
font-size: 1rem;
|
| 386 |
-
font-weight: 700;
|
| 387 |
-
color: var(--gray-900);
|
| 388 |
-
outline: none;
|
| 389 |
-
transition: all 0.2s;
|
| 390 |
-
}
|
| 391 |
-
|
| 392 |
-
.received-input:focus {
|
| 393 |
-
border-color: var(--blue-accent);
|
| 394 |
-
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
| 395 |
-
}
|
| 396 |
-
|
| 397 |
-
/* The dark navy Total Amount block */
|
| 398 |
-
.total-block {
|
| 399 |
-
background: var(--blue-deep);
|
| 400 |
-
margin: 10px -2.5rem -1.4rem auto; /* Aligns to right and sticks to bottom/right edges */
|
| 401 |
-
padding: 20px 2.5rem;
|
| 402 |
-
width: calc(100% + 5rem); /* Stretches to full card width if desired, or keep as container */
|
| 403 |
-
max-width: 450px;
|
| 404 |
-
display: flex;
|
| 405 |
-
justify-content: space-between;
|
| 406 |
-
align-items: center;
|
| 407 |
-
border-top-left-radius: var(--radius);
|
| 408 |
-
}
|
| 409 |
-
|
| 410 |
-
.total-block .label {
|
| 411 |
-
color: rgba(255, 255, 255, 0.9);
|
| 412 |
-
font-size: 0.9rem;
|
| 413 |
-
font-weight: 800;
|
| 414 |
-
text-transform: uppercase;
|
| 415 |
-
letter-spacing: 0.08em;
|
| 416 |
-
}
|
| 417 |
-
|
| 418 |
-
.total-block .value {
|
| 419 |
-
color: #fff;
|
| 420 |
-
font-family: 'Lora', serif;
|
| 421 |
-
font-size: 1.8rem;
|
| 422 |
-
font-weight: 700;
|
| 423 |
-
}
|
| 424 |
-
|
| 425 |
-
@media (max-width: 768px) {
|
| 426 |
-
.total-block {
|
| 427 |
-
margin-right: -1.2rem;
|
| 428 |
-
width: calc(100% + 2.4rem);
|
| 429 |
-
max-width: none;
|
| 430 |
-
border-radius: 0;
|
| 431 |
-
}
|
| 432 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/img/logo.jpeg
DELETED
|
Binary file (50.3 kB)
|
|
|
frontend/index.html
DELETED
|
@@ -1,224 +0,0 @@
|
|
| 1 |
-
<!DOCTYPE html>
|
| 2 |
-
<html lang="en">
|
| 3 |
-
<head>
|
| 4 |
-
<meta charset="UTF-8"/>
|
| 5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
| 6 |
-
<title>SmiloCAD Dental Laboratory β Invoice System</title>
|
| 7 |
-
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;700&display=swap" rel="stylesheet"/>
|
| 8 |
-
<link rel="stylesheet" href="/css/style.css?v=20260413"/>
|
| 9 |
-
</head>
|
| 10 |
-
<body>
|
| 11 |
-
|
| 12 |
-
<div class="topbar">
|
| 13 |
-
<div class="topbar-brand">
|
| 14 |
-
<img class="topbar-logo" src="/img/logo.jpeg" alt="SmiloCAD Dental Lab logo" width="140" height="38">
|
| 15 |
-
<div>
|
| 16 |
-
<div class="topbar-name">SmiloCAD Dental Laboratory</div>
|
| 17 |
-
<div class="topbar-sub">Invoice Management System</div>
|
| 18 |
-
</div>
|
| 19 |
-
</div>
|
| 20 |
-
<div class="topbar-tabs">
|
| 21 |
-
<button class="topbar-tab active" id="tab-invoice" onclick="App.showPage('invoice')">β New Invoice</button>
|
| 22 |
-
<button class="topbar-tab" id="tab-history" onclick="App.showPage('history')">π History</button>
|
| 23 |
-
</div>
|
| 24 |
-
</div>
|
| 25 |
-
|
| 26 |
-
<div class="app-body" id="page-invoice">
|
| 27 |
-
<div class="inv-card">
|
| 28 |
-
|
| 29 |
-
<div class="inv-header-band">
|
| 30 |
-
<div>
|
| 31 |
-
<div class="lab-title">SmiloCAD Dental Laboratory</div>
|
| 32 |
-
<div class="lab-meta">π Al Anayat Plaza, G11 Markaz Islamabad</div>
|
| 33 |
-
</div>
|
| 34 |
-
<div class="inv-badge-block">
|
| 35 |
-
<div class="inv-badge-label">Invoice</div>
|
| 36 |
-
<div class="inv-number-pill" id="display-inv-number">#NEW</div>
|
| 37 |
-
</div>
|
| 38 |
-
</div>
|
| 39 |
-
|
| 40 |
-
<div class="inv-section">
|
| 41 |
-
<div class="section-title">Details</div>
|
| 42 |
-
<div class="form-grid form-grid-2">
|
| 43 |
-
<div class="form-group">
|
| 44 |
-
<label>Invoice No.</label>
|
| 45 |
-
<input class="form-control" id="inv-number" value="Auto-Generated" readonly>
|
| 46 |
-
</div>
|
| 47 |
-
<div class="form-group">
|
| 48 |
-
<label>Date</label>
|
| 49 |
-
<input type="date" class="form-control" id="inv-date">
|
| 50 |
-
</div>
|
| 51 |
-
</div>
|
| 52 |
-
|
| 53 |
-
<div class="form-grid form-grid-3" style="margin-top:15px">
|
| 54 |
-
<div class="form-group">
|
| 55 |
-
<label>Doctor Name</label>
|
| 56 |
-
<input class="form-control" id="doctor-name" placeholder="Dr. Name">
|
| 57 |
-
</div>
|
| 58 |
-
<div class="form-group">
|
| 59 |
-
<label>Clinic Name</label>
|
| 60 |
-
<input class="form-control" id="clinic" placeholder="Clinic Name">
|
| 61 |
-
</div>
|
| 62 |
-
<div class="form-group">
|
| 63 |
-
<label>Patient Name</label>
|
| 64 |
-
<input class="form-control" id="patient" placeholder="Patient Name">
|
| 65 |
-
</div>
|
| 66 |
-
</div>
|
| 67 |
-
|
| 68 |
-
<div class="form-grid form-grid-3" style="margin-top:15px">
|
| 69 |
-
<div class="form-group">
|
| 70 |
-
<label>Shade</label>
|
| 71 |
-
<select class="form-control" id="shade">
|
| 72 |
-
<option value="">Select shade</option>
|
| 73 |
-
<option value="A1">A1</option>
|
| 74 |
-
<option value="A2">A2</option>
|
| 75 |
-
<option value="A3">A3</option>
|
| 76 |
-
<option value="A3.5">A3.5</option>
|
| 77 |
-
<option value="A4">A4</option>
|
| 78 |
-
<option value="B1">B1</option>
|
| 79 |
-
<option value="B2">B2</option>
|
| 80 |
-
<option value="B3">B3</option>
|
| 81 |
-
<option value="B4">B4</option>
|
| 82 |
-
<option value="C1">C1</option>
|
| 83 |
-
<option value="C2">C2</option>
|
| 84 |
-
<option value="C3">C3</option>
|
| 85 |
-
<option value="C4">C4</option>
|
| 86 |
-
<option value="D2">D2</option>
|
| 87 |
-
<option value="D3">D3</option>
|
| 88 |
-
<option value="D4">D4</option>
|
| 89 |
-
</select>
|
| 90 |
-
</div>
|
| 91 |
-
</div>
|
| 92 |
-
</div>
|
| 93 |
-
|
| 94 |
-
<div class="inv-section">
|
| 95 |
-
<div class="section-title">Services / Work Items</div>
|
| 96 |
-
<table class="svc-table">
|
| 97 |
-
<thead>
|
| 98 |
-
<tr>
|
| 99 |
-
<th>#</th>
|
| 100 |
-
<th>Description</th>
|
| 101 |
-
<th>Qty</th>
|
| 102 |
-
<th>Price (PKR)</th>
|
| 103 |
-
<th>Total</th>
|
| 104 |
-
<th class="center">β</th>
|
| 105 |
-
</tr>
|
| 106 |
-
</thead>
|
| 107 |
-
<tbody id="rows-body"></tbody>
|
| 108 |
-
</table>
|
| 109 |
-
<button class="add-row-btn" onclick="Rows.add()">+ Add Row</button>
|
| 110 |
-
</div>
|
| 111 |
-
|
| 112 |
-
<div class="inv-section summary">
|
| 113 |
-
<div class="section-title">Summary</div>
|
| 114 |
-
<div class="summary-container">
|
| 115 |
-
<div class="summary-row">
|
| 116 |
-
<span class="label">Subtotal</span>
|
| 117 |
-
<span class="value" id="summary-subtotal">PKR 0</span>
|
| 118 |
-
</div>
|
| 119 |
-
<div class="summary-row received">
|
| 120 |
-
<span class="label">Received Amount</span>
|
| 121 |
-
<input type="number" class="received-input" id="received-input" placeholder="0" min="0">
|
| 122 |
-
</div>
|
| 123 |
-
<div class="summary-row remaining">
|
| 124 |
-
<span class="label">Remaining Balance</span>
|
| 125 |
-
<span class="value" id="summary-remaining">PKR 0</span>
|
| 126 |
-
</div>
|
| 127 |
-
</div>
|
| 128 |
-
<div class="total-block">
|
| 129 |
-
<span class="label">Total Amount</span>
|
| 130 |
-
<span class="value" id="summary-total">PKR 0</span>
|
| 131 |
-
</div>
|
| 132 |
-
</div>
|
| 133 |
-
|
| 134 |
-
<div class="inv-section" style="border-bottom:none">
|
| 135 |
-
<label style="font-size:0.85rem; font-weight:700; color:#555">Notes</label>
|
| 136 |
-
<textarea class="form-control" id="notes" rows="2" placeholder="Instructions..."></textarea>
|
| 137 |
-
</div>
|
| 138 |
-
|
| 139 |
-
<div class="action-bar">
|
| 140 |
-
<button class="btn btn-save" onclick="App.save()">πΎ Save</button>
|
| 141 |
-
<button class="btn btn-pdf" onclick="App.downloadPDF()">β¬οΈ PDF</button>
|
| 142 |
-
<button class="btn btn-print" onclick="App.print()">π¨οΈ Print</button>
|
| 143 |
-
<button class="btn btn-excel" onclick="App.exportExcel()">π Excel</button>
|
| 144 |
-
<div class="btn-spacer"></div>
|
| 145 |
-
<button class="btn btn-new" onclick="App.newInvoice()">π New</button>
|
| 146 |
-
<button class="btn btn-clear" onclick="App.clear()">ποΈ Clear</button>
|
| 147 |
-
</div>
|
| 148 |
-
|
| 149 |
-
</div>
|
| 150 |
-
</div>
|
| 151 |
-
|
| 152 |
-
<div class="app-body" id="page-history" style="display:none">
|
| 153 |
-
<div class="page-heading"><h2>Invoice History</h2><span class="count-pill" id="history-count">0</span></div>
|
| 154 |
-
<div class="search-wrap"><input id="search-input" placeholder="Search..." oninput="History.filter()"></div>
|
| 155 |
-
<div class="hist-list" id="hist-list"></div>
|
| 156 |
-
</div>
|
| 157 |
-
|
| 158 |
-
<div id="toast"></div>
|
| 159 |
-
|
| 160 |
-
<!-- Print-only invoice layout (matches example PDF style) -->
|
| 161 |
-
<div id="print-sheet" class="print-sheet">
|
| 162 |
-
<div class="print-header">
|
| 163 |
-
<div class="print-title">INVOICE</div>
|
| 164 |
-
<div class="print-brand">
|
| 165 |
-
<div class="print-brand-name">SmiloCAD Dental Lab</div>
|
| 166 |
-
<div class="print-brand-line">Al Anayat Plaza, G11 Markaz Islamabad</div>
|
| 167 |
-
<div class="print-brand-line">Phone: </div>
|
| 168 |
-
<div class="print-brand-line">Email: </div>
|
| 169 |
-
</div>
|
| 170 |
-
</div>
|
| 171 |
-
|
| 172 |
-
<div class="print-info">
|
| 173 |
-
<div class="print-info-left">
|
| 174 |
-
<div class="print-info-row"><span class="k">Invoice No.</span> <span class="v" id="print-inv-no">INV-0000</span></div>
|
| 175 |
-
<div class="print-info-row"><span class="k">Date of Issue</span> <span class="v" id="print-inv-date">Enter Date Here</span></div>
|
| 176 |
-
<div class="print-info-row"><span class="k">Due Date</span> <span class="v" id="print-due-date">Enter Due Date Here</span></div>
|
| 177 |
-
</div>
|
| 178 |
-
<div class="print-info-right">
|
| 179 |
-
<div class="print-info-title">Bill To</div>
|
| 180 |
-
<div class="print-info-row"><span class="k">Doctor Name</span> <span class="v" id="print-client-name">Client Name</span></div>
|
| 181 |
-
<div class="print-info-row"><span class="k">Clinic Name</span> <span class="v" id="print-company-name">Company Name</span></div>
|
| 182 |
-
<div class="print-info-row"><span class="k">Address</span> <span class="v" id="print-address">Address</span></div>
|
| 183 |
-
<div class="print-info-row"><span class="k">Phone</span> <span class="v" id="print-phone">Phone</span></div>
|
| 184 |
-
<div class="print-info-row"><span class="k">Email</span> <span class="v" id="print-email">Email</span></div>
|
| 185 |
-
</div>
|
| 186 |
-
</div>
|
| 187 |
-
|
| 188 |
-
<table class="print-table">
|
| 189 |
-
<thead>
|
| 190 |
-
<tr>
|
| 191 |
-
<th class="c">Item</th>
|
| 192 |
-
<th>Patient Name</th>
|
| 193 |
-
<th>Shield</th>
|
| 194 |
-
<th>Description</th>
|
| 195 |
-
<th class="c">Unit</th>
|
| 196 |
-
<th class="r">Rate</th>
|
| 197 |
-
<th class="r">Amount</th>
|
| 198 |
-
</tr>
|
| 199 |
-
</thead>
|
| 200 |
-
<tbody id="print-rows"></tbody>
|
| 201 |
-
</table>
|
| 202 |
-
|
| 203 |
-
<div class="print-summary">
|
| 204 |
-
<div class="print-terms">Terms</div>
|
| 205 |
-
<div class="print-totals">
|
| 206 |
-
<div class="tot-row"><span>Subtotal</span><span id="print-subtotal">$0.00</span></div>
|
| 207 |
-
<div class="tot-row"><span>Discount</span><span>$0.00</span></div>
|
| 208 |
-
<div class="tot-row"><span>Tax Rate</span><span>0%</span></div>
|
| 209 |
-
<div class="tot-row"><span>Tax</span><span>$0.00</span></div>
|
| 210 |
-
<div class="tot-row total"><span>Total</span><span id="print-total">$0.00</span></div>
|
| 211 |
-
</div>
|
| 212 |
-
</div>
|
| 213 |
-
|
| 214 |
-
<div class="print-footer">Thank you for your business!</div>
|
| 215 |
-
</div>
|
| 216 |
-
|
| 217 |
-
<script src="/js/data.js?v=20260413"></script>
|
| 218 |
-
<script src="/js/db.js?v=20260413"></script>
|
| 219 |
-
<script src="/js/ui.js?v=20260413"></script>
|
| 220 |
-
<script src="/js/rows.js?v=20260413"></script>
|
| 221 |
-
<script src="/js/history.js?v=20260413"></script>
|
| 222 |
-
<script src="/js/app.js?v=20260413"></script>
|
| 223 |
-
</body>
|
| 224 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/app.js
DELETED
|
@@ -1,218 +0,0 @@
|
|
| 1 |
-
/* βββββββββββββββββββββββββββββββββββββββββ
|
| 2 |
-
js/app.js β Main application controller
|
| 3 |
-
βββββββββββββββββββββββββββββββββββββββββ */
|
| 4 |
-
|
| 5 |
-
var App = (function() {
|
| 6 |
-
var _currentId = null;
|
| 7 |
-
|
| 8 |
-
// ββ Internal Helper: Collect form data ββ
|
| 9 |
-
function _collect() {
|
| 10 |
-
// Safe check for Rows object
|
| 11 |
-
var rowData = (typeof Rows !== 'undefined') ? Rows.collect() : [];
|
| 12 |
-
|
| 13 |
-
var items = rowData.map(row => ({
|
| 14 |
-
description: row.description || row.desc || "Service",
|
| 15 |
-
quantity: parseInt(row.quantity || row.qty) || 1,
|
| 16 |
-
price_per_unit: parseFloat(row.price_per_unit || row.price) || 0
|
| 17 |
-
}));
|
| 18 |
-
|
| 19 |
-
return {
|
| 20 |
-
date: document.getElementById("inv-date")?.value ? document.getElementById("inv-date").value + "T00:00:00" : new Date().toISOString(),
|
| 21 |
-
doctor_name: document.getElementById("doctor-name")?.value || "",
|
| 22 |
-
clinic_name: document.getElementById("clinic")?.value || "",
|
| 23 |
-
patient_name: document.getElementById("patient")?.value || "",
|
| 24 |
-
shade: document.getElementById("shade")?.value || "",
|
| 25 |
-
received_amount: parseFloat(document.getElementById("received-input")?.value) || 0,
|
| 26 |
-
items: items,
|
| 27 |
-
notes: document.getElementById("notes")?.value || ""
|
| 28 |
-
};
|
| 29 |
-
}
|
| 30 |
-
|
| 31 |
-
// ββ Public: Save to Neon ββ
|
| 32 |
-
async function save() {
|
| 33 |
-
console.log("Save initiated...");
|
| 34 |
-
try {
|
| 35 |
-
var data = _collect();
|
| 36 |
-
if (_currentId) data.id = _currentId;
|
| 37 |
-
|
| 38 |
-
// Ensure dbSave exists from db.js
|
| 39 |
-
if (typeof dbSave !== 'function') throw new Error("dbSave function not found. Check db.js");
|
| 40 |
-
|
| 41 |
-
var result = await dbSave(data);
|
| 42 |
-
|
| 43 |
-
if (document.getElementById("inv-number")) {
|
| 44 |
-
document.getElementById("inv-number").value = result.invoice_no;
|
| 45 |
-
}
|
| 46 |
-
_currentId = result.id;
|
| 47 |
-
|
| 48 |
-
if (typeof showToast === 'function') showToast(`β
Saved! ${result.invoice_no}`, "success");
|
| 49 |
-
if (typeof updateHeaderBadge === 'function') updateHeaderBadge();
|
| 50 |
-
|
| 51 |
-
} catch (err) {
|
| 52 |
-
console.error("Save Error:", err);
|
| 53 |
-
if (typeof showToast === 'function') showToast("β Save failed. Check console.", "error");
|
| 54 |
-
}
|
| 55 |
-
}
|
| 56 |
-
|
| 57 |
-
// ββ Public: New/Reset ββ
|
| 58 |
-
async function _resetForm() {
|
| 59 |
-
_currentId = null;
|
| 60 |
-
if (document.getElementById("inv-number")) document.getElementById("inv-number").value = "Auto-Generated";
|
| 61 |
-
if (document.getElementById("inv-date")) document.getElementById("inv-date").value = (typeof todayStr !== 'undefined') ? todayStr() : "";
|
| 62 |
-
|
| 63 |
-
const fields = ["doctor-name", "clinic", "patient", "shade", "notes", "received-input"];
|
| 64 |
-
fields.forEach(id => {
|
| 65 |
-
const el = document.getElementById(id);
|
| 66 |
-
if (el) el.value = "";
|
| 67 |
-
});
|
| 68 |
-
|
| 69 |
-
if (typeof Rows !== 'undefined') Rows.reset();
|
| 70 |
-
if (typeof updateHeaderBadge === 'function') updateHeaderBadge();
|
| 71 |
-
}
|
| 72 |
-
|
| 73 |
-
function showPage(page) {
|
| 74 |
-
if (typeof switchPage === 'function') switchPage(page);
|
| 75 |
-
if (page === "history" && typeof History !== 'undefined') History.load();
|
| 76 |
-
}
|
| 77 |
-
|
| 78 |
-
async function newInvoice() {
|
| 79 |
-
await _resetForm();
|
| 80 |
-
showPage("invoice");
|
| 81 |
-
}
|
| 82 |
-
|
| 83 |
-
function clear() {
|
| 84 |
-
if (confirm("Clear form?")) _resetForm();
|
| 85 |
-
}
|
| 86 |
-
|
| 87 |
-
function _fmtMoney(n) {
|
| 88 |
-
return "PKR " + (Number(n) || 0).toLocaleString("en-PK", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
| 89 |
-
}
|
| 90 |
-
|
| 91 |
-
function _preparePrint() {
|
| 92 |
-
var invNo = document.getElementById("inv-number")?.value || "INV-0000";
|
| 93 |
-
var invDate = document.getElementById("inv-date")?.value || "";
|
| 94 |
-
var doctor = document.getElementById("doctor-name")?.value || "";
|
| 95 |
-
var clinic = document.getElementById("clinic")?.value || "";
|
| 96 |
-
var patient = document.getElementById("patient")?.value || "";
|
| 97 |
-
var shade = document.getElementById("shade")?.value || "";
|
| 98 |
-
|
| 99 |
-
var invNoEl = document.getElementById("print-inv-no");
|
| 100 |
-
var invDateEl = document.getElementById("print-inv-date");
|
| 101 |
-
if (invNoEl) invNoEl.textContent = invNo === "Auto-Generated" ? "INV-0000" : invNo;
|
| 102 |
-
if (invDateEl) invDateEl.textContent = invDate || "Enter Date Here";
|
| 103 |
-
|
| 104 |
-
var clientEl = document.getElementById("print-client-name");
|
| 105 |
-
var companyEl = document.getElementById("print-company-name");
|
| 106 |
-
if (clientEl) clientEl.textContent = doctor || "Doctor Name";
|
| 107 |
-
if (companyEl) companyEl.textContent = clinic || "Clinic Name";
|
| 108 |
-
|
| 109 |
-
var rowsEl = document.getElementById("print-rows");
|
| 110 |
-
if (rowsEl) rowsEl.innerHTML = "";
|
| 111 |
-
|
| 112 |
-
var items = (typeof Rows !== 'undefined') ? Rows.collect() : [];
|
| 113 |
-
|
| 114 |
-
for (var i = 0; i < items.length; i++) {
|
| 115 |
-
var item = items[i];
|
| 116 |
-
var desc = item ? item.description : "";
|
| 117 |
-
var qty = item ? item.quantity : "";
|
| 118 |
-
var price = item ? item.price_per_unit : "";
|
| 119 |
-
var total = item ? (item.quantity * item.price_per_unit) : "";
|
| 120 |
-
var rowPatient = item ? (patient || "") : "";
|
| 121 |
-
var rowShade = item ? (shade || "") : "";
|
| 122 |
-
var rowHtml = [
|
| 123 |
-
'<tr>',
|
| 124 |
-
'<td class="c">', (i + 1), '</td>',
|
| 125 |
-
'<td>', rowPatient, '</td>',
|
| 126 |
-
'<td>', rowShade, '</td>',
|
| 127 |
-
'<td>', desc || '', '</td>',
|
| 128 |
-
'<td class="c">', (qty !== "" ? qty : ''), '</td>',
|
| 129 |
-
'<td class="r">', (price !== "" ? _fmtMoney(price) : ''), '</td>',
|
| 130 |
-
'<td class="r">', (total !== "" ? _fmtMoney(total) : ''), '</td>',
|
| 131 |
-
'</tr>'
|
| 132 |
-
].join("");
|
| 133 |
-
if (rowsEl) rowsEl.insertAdjacentHTML("beforeend", rowHtml);
|
| 134 |
-
}
|
| 135 |
-
|
| 136 |
-
var subtotal = (typeof Rows !== 'undefined') ? Rows.subtotal() : 0;
|
| 137 |
-
var subEl = document.getElementById("print-subtotal");
|
| 138 |
-
var totalEl = document.getElementById("print-total");
|
| 139 |
-
if (subEl) subEl.textContent = _fmtMoney(subtotal);
|
| 140 |
-
if (totalEl) totalEl.textContent = _fmtMoney(subtotal);
|
| 141 |
-
}
|
| 142 |
-
|
| 143 |
-
// Empty stubs to prevent "undefined" errors if UI calls them
|
| 144 |
-
function print() { _preparePrint(); window.print(); }
|
| 145 |
-
function downloadPDF() { _preparePrint(); window.print(); }
|
| 146 |
-
function exportExcel() { console.log("Exporting..."); }
|
| 147 |
-
async function loadEdit(id) {
|
| 148 |
-
try {
|
| 149 |
-
if (!id || isNaN(id)) {
|
| 150 |
-
if (typeof showToast === 'function') showToast("β Invalid invoice id", "error");
|
| 151 |
-
return;
|
| 152 |
-
}
|
| 153 |
-
// Give immediate feedback and switch view
|
| 154 |
-
if (typeof showPage === "function") showPage("invoice");
|
| 155 |
-
if (typeof showToast === 'function') showToast("Loading invoice...", "info");
|
| 156 |
-
if (typeof dbGet !== 'function') throw new Error("dbGet function not found. Check db.js");
|
| 157 |
-
const inv = await dbGet(id);
|
| 158 |
-
|
| 159 |
-
_currentId = inv.id || id;
|
| 160 |
-
if (document.getElementById("inv-number")) {
|
| 161 |
-
document.getElementById("inv-number").value = inv.invoice_no || "Auto-Generated";
|
| 162 |
-
}
|
| 163 |
-
if (document.getElementById("inv-date")) {
|
| 164 |
-
const dateStr = inv.date ? String(inv.date).split("T")[0] : "";
|
| 165 |
-
document.getElementById("inv-date").value = dateStr;
|
| 166 |
-
}
|
| 167 |
-
|
| 168 |
-
const doctorEl = document.getElementById("doctor-name");
|
| 169 |
-
const clinicEl = document.getElementById("clinic");
|
| 170 |
-
const patientEl = document.getElementById("patient");
|
| 171 |
-
const shadeEl = document.getElementById("shade");
|
| 172 |
-
const receivedEl = document.getElementById("received-input");
|
| 173 |
-
|
| 174 |
-
if (doctorEl) doctorEl.value = inv.doctor_name || "";
|
| 175 |
-
if (clinicEl) clinicEl.value = inv.clinic_name || "";
|
| 176 |
-
if (patientEl) patientEl.value = inv.patient_name || "";
|
| 177 |
-
if (shadeEl) shadeEl.value = inv.shade || "";
|
| 178 |
-
if (receivedEl) receivedEl.value = (inv.received_amount || 0);
|
| 179 |
-
|
| 180 |
-
if (typeof Rows !== 'undefined' && typeof Rows.load === 'function') {
|
| 181 |
-
Rows.load(inv.items || []);
|
| 182 |
-
}
|
| 183 |
-
|
| 184 |
-
if (typeof updateHeaderBadge === 'function') updateHeaderBadge();
|
| 185 |
-
showPage("invoice");
|
| 186 |
-
} catch (err) {
|
| 187 |
-
console.error("Load Error:", err);
|
| 188 |
-
if (typeof showToast === 'function') showToast("β Load failed. Check console.", "error");
|
| 189 |
-
}
|
| 190 |
-
}
|
| 191 |
-
|
| 192 |
-
// ββ Boot ββ
|
| 193 |
-
document.addEventListener("DOMContentLoaded", async function() {
|
| 194 |
-
console.log("App Booting...");
|
| 195 |
-
try {
|
| 196 |
-
if (typeof dbOpen === 'function') await dbOpen();
|
| 197 |
-
if (document.getElementById("inv-number")) document.getElementById("inv-number").value = "Auto-Generated";
|
| 198 |
-
if (document.getElementById("inv-date") && typeof todayStr !== 'undefined') {
|
| 199 |
-
document.getElementById("inv-date").value = todayStr();
|
| 200 |
-
}
|
| 201 |
-
if (typeof updateHeaderBadge === 'function') updateHeaderBadge();
|
| 202 |
-
if (typeof Rows !== 'undefined') Rows.reset();
|
| 203 |
-
} catch(e) {
|
| 204 |
-
console.error("Boot failure:", e);
|
| 205 |
-
}
|
| 206 |
-
});
|
| 207 |
-
|
| 208 |
-
return {
|
| 209 |
-
showPage: showPage,
|
| 210 |
-
save: save,
|
| 211 |
-
newInvoice: newInvoice,
|
| 212 |
-
clear: clear,
|
| 213 |
-
print: print,
|
| 214 |
-
downloadPDF: downloadPDF,
|
| 215 |
-
exportExcel: exportExcel,
|
| 216 |
-
loadEdit: loadEdit
|
| 217 |
-
};
|
| 218 |
-
})();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/data.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
| 1 |
-
/* βββββββββββββββββββββββββββββββββββββββββ
|
| 2 |
-
js/data.js β All static data constants
|
| 3 |
-
βββββββββββββββββββββββββββββββββββββββββ */
|
| 4 |
-
|
| 5 |
-
const LAB = {
|
| 6 |
-
name: "SmiloCAD Dental Laboratory",
|
| 7 |
-
phone: "0328-9577771",
|
| 8 |
-
address: "Al Anayat Plaza, G11 Markaz Islamabad",
|
| 9 |
-
tech: "Dt. Sajid",
|
| 10 |
-
};
|
| 11 |
-
|
| 12 |
-
const SERVICES = [
|
| 13 |
-
"Veneer",
|
| 14 |
-
"Zirconium Crown",
|
| 15 |
-
"Zirconium Implant",
|
| 16 |
-
"PFM Crown",
|
| 17 |
-
"PFM Implant",
|
| 18 |
-
"D-sign Porcelain",
|
| 19 |
-
"Night Guard",
|
| 20 |
-
"Retainer",
|
| 21 |
-
"Bleaching Tary",
|
| 22 |
-
"Soft Denture",
|
| 23 |
-
"EMAX Veneer",
|
| 24 |
-
"EMAX Crown",
|
| 25 |
-
"Zirconium bridge + I bar"
|
| 26 |
-
];
|
| 27 |
-
// Expose globally so rows.js can always access it
|
| 28 |
-
window.SERVICES = SERVICES;
|
| 29 |
-
|
| 30 |
-
const DOCTORS = [
|
| 31 |
-
"Dr. Haroon Shah",
|
| 32 |
-
"Dr. Ahmad Raza",
|
| 33 |
-
"Dr. Sara Khan",
|
| 34 |
-
"Dr. Imran Ali",
|
| 35 |
-
"Dr. Fatima Malik",
|
| 36 |
-
"Other",
|
| 37 |
-
];
|
| 38 |
-
|
| 39 |
-
const SHADES = [
|
| 40 |
-
"β","A1","A2","A3","A3.5","A4",
|
| 41 |
-
"B1","B2","B3","B4",
|
| 42 |
-
"C1","C2","C3","C4",
|
| 43 |
-
"D2","D3","D4",
|
| 44 |
-
"BL1","BL2","BL3","BL4","Custom",
|
| 45 |
-
];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/db.js
DELETED
|
@@ -1,82 +0,0 @@
|
|
| 1 |
-
/* βββββββββββββββββββββββββββββββββββββββββ
|
| 2 |
-
js/db.js β Live Neon DB Helpers
|
| 3 |
-
βββββββββββββββββββββββββββββββββββββββββ */
|
| 4 |
-
|
| 5 |
-
const API_URL = "/api/invoices"; // Relative path since frontend/backend are on same host
|
| 6 |
-
|
| 7 |
-
/* No need to "Open" a local DB anymore, but we'll keep the function
|
| 8 |
-
so app.js doesn't break. We'll just return true. */
|
| 9 |
-
async function dbOpen() {
|
| 10 |
-
console.log("Connected to Live API Mode");
|
| 11 |
-
return true;
|
| 12 |
-
}
|
| 13 |
-
|
| 14 |
-
/* Save one invoice record to Neon */
|
| 15 |
-
async function dbSave(data) {
|
| 16 |
-
try {
|
| 17 |
-
const response = await fetch(`${API_URL}`, {
|
| 18 |
-
method: 'POST',
|
| 19 |
-
headers: {
|
| 20 |
-
'Content-Type': 'application/json',
|
| 21 |
-
},
|
| 22 |
-
body: JSON.stringify(data)
|
| 23 |
-
});
|
| 24 |
-
|
| 25 |
-
if (!response.ok) {
|
| 26 |
-
const error = await response.json();
|
| 27 |
-
throw new Error(error.detail || "Failed to save to Neon");
|
| 28 |
-
}
|
| 29 |
-
|
| 30 |
-
const result = await response.json();
|
| 31 |
-
// result will contain { "id": X, "invoice_no": "INV-XXXX" }
|
| 32 |
-
return result;
|
| 33 |
-
} catch (error) {
|
| 34 |
-
console.error("Save Error:", error);
|
| 35 |
-
throw error;
|
| 36 |
-
}
|
| 37 |
-
}
|
| 38 |
-
|
| 39 |
-
/* Load all invoice records from Neon */
|
| 40 |
-
async function dbAll() {
|
| 41 |
-
try {
|
| 42 |
-
const response = await fetch(`${API_URL}/`);
|
| 43 |
-
if (!response.ok) throw new Error("Could not fetch history");
|
| 44 |
-
return await response.json();
|
| 45 |
-
} catch (error) {
|
| 46 |
-
console.error("Fetch Error:", error);
|
| 47 |
-
return []; // Return empty list on error to prevent UI crash
|
| 48 |
-
}
|
| 49 |
-
}
|
| 50 |
-
|
| 51 |
-
/* Load one invoice by id from Neon */
|
| 52 |
-
async function dbGet(id) {
|
| 53 |
-
try {
|
| 54 |
-
const response = await fetch(`${API_URL}/${id}`);
|
| 55 |
-
if (!response.ok) {
|
| 56 |
-
let detail = "";
|
| 57 |
-
try {
|
| 58 |
-
const err = await response.json();
|
| 59 |
-
detail = err.detail ? ` (${err.detail})` : "";
|
| 60 |
-
} catch (e) {}
|
| 61 |
-
throw new Error(`Could not fetch invoice${detail}`);
|
| 62 |
-
}
|
| 63 |
-
return await response.json();
|
| 64 |
-
} catch (error) {
|
| 65 |
-
console.error("Fetch One Error:", error);
|
| 66 |
-
throw error;
|
| 67 |
-
}
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
/* Delete one invoice by id from Neon */
|
| 71 |
-
async function dbDelete(id) {
|
| 72 |
-
try {
|
| 73 |
-
const response = await fetch(`${API_URL}/${id}`, {
|
| 74 |
-
method: 'DELETE'
|
| 75 |
-
});
|
| 76 |
-
if (!response.ok) throw new Error("Could not delete invoice");
|
| 77 |
-
return true;
|
| 78 |
-
} catch (error) {
|
| 79 |
-
console.error("Delete Error:", error);
|
| 80 |
-
throw error;
|
| 81 |
-
}
|
| 82 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/history.js
DELETED
|
@@ -1,129 +0,0 @@
|
|
| 1 |
-
/* βββββββββββββββββββββββββββββββββββββββββ
|
| 2 |
-
js/history.js β Live Neon History Page
|
| 3 |
-
βββββββββββββββββββββββββββββββββββββββββ */
|
| 4 |
-
|
| 5 |
-
var History = (function() {
|
| 6 |
-
|
| 7 |
-
var _allInvoices = []; // cache of loaded invoices
|
| 8 |
-
var _boundClicks = false;
|
| 9 |
-
|
| 10 |
-
/* Format PKR */
|
| 11 |
-
function _fmt(n) { return fmt(n); }
|
| 12 |
-
|
| 13 |
-
/* Build HTML for a single history card (Updated for Neon fields) */
|
| 14 |
-
/* Build HTML for a single history card (Updated for Neon fields + Notes) */
|
| 15 |
-
function _buildCard(inv) {
|
| 16 |
-
// Check if there is a note, and create a small snippet for the UI
|
| 17 |
-
var notePreview = inv.notes
|
| 18 |
-
? '<div class="hist-detail" style="color:var(--amber); font-style:italic;">π Note: ' + inv.notes.substring(0, 30) + (inv.notes.length > 30 ? '...' : '') + '</div>'
|
| 19 |
-
: '';
|
| 20 |
-
|
| 21 |
-
return [
|
| 22 |
-
'<div class="hist-item">',
|
| 23 |
-
'<div class="hist-inv-no">' + (inv.invoice_no || "β") + '</div>',
|
| 24 |
-
'<div>',
|
| 25 |
-
'<div class="hist-client-name">' + (inv.doctor_name || "Unknown Doctor") + '</div>',
|
| 26 |
-
'<div class="hist-date">π
' + (inv.date ? inv.date.split('T')[0] : "β") + ' Β· ' + (inv.clinic_name || "β") + '</div>',
|
| 27 |
-
'</div>',
|
| 28 |
-
'<div>',
|
| 29 |
-
'<div class="hist-detail">Patient: <span>' + (inv.patient_name || "β") + '</span></div>',
|
| 30 |
-
// Show the note preview here
|
| 31 |
-
notePreview,
|
| 32 |
-
'</div>',
|
| 33 |
-
'<div>',
|
| 34 |
-
'<div class="hist-amount">' + _fmt(inv.total_amount || 0) + '</div>',
|
| 35 |
-
'<div style="font-size:0.75rem;color:var(--green);text-align:right;margin-top:2px">Rcvd: ' + _fmt(inv.received_amount || 0) + '</div>',
|
| 36 |
-
'</div>',
|
| 37 |
-
'<div class="hist-actions">',
|
| 38 |
-
'<button class="btn-xs btn-xs-blue" type="button" data-action="load" data-id="' + inv.id + '">βοΈ Load</button>',
|
| 39 |
-
'<button class="btn-xs btn-xs-red" type="button" data-action="delete" data-id="' + inv.id + '">ποΈ</button>',
|
| 40 |
-
'</div>',
|
| 41 |
-
'</div>',
|
| 42 |
-
].join("");
|
| 43 |
-
}
|
| 44 |
-
|
| 45 |
-
/* Load all invoices from DB and render */
|
| 46 |
-
function load() {
|
| 47 |
-
// dbAll() now calls fetch('/invoices/')
|
| 48 |
-
dbAll().then(function(all) {
|
| 49 |
-
// API already returns newest first if you used .order_by(Invoice.id.desc())
|
| 50 |
-
_allInvoices = all;
|
| 51 |
-
filter();
|
| 52 |
-
}).catch(err => {
|
| 53 |
-
console.error("Failed to load history:", err);
|
| 54 |
-
showToast("β Could not load history", "error");
|
| 55 |
-
});
|
| 56 |
-
}
|
| 57 |
-
|
| 58 |
-
/* Filter the cached list by search term and re-render */
|
| 59 |
-
function filter() {
|
| 60 |
-
var q = (document.getElementById("search-input").value || "").toLowerCase();
|
| 61 |
-
var filtered = _allInvoices.filter(function(inv) {
|
| 62 |
-
if (!q) return true;
|
| 63 |
-
return (
|
| 64 |
-
(inv.invoice_no || "").toLowerCase().includes(q) ||
|
| 65 |
-
(inv.doctor_name || "").toLowerCase().includes(q) ||
|
| 66 |
-
(inv.clinic_name || "").toLowerCase().includes(q) ||
|
| 67 |
-
(inv.patient_name|| "").toLowerCase().includes(q)
|
| 68 |
-
);
|
| 69 |
-
});
|
| 70 |
-
_render(filtered);
|
| 71 |
-
}
|
| 72 |
-
|
| 73 |
-
/* Delete an invoice after confirmation */
|
| 74 |
-
function deleteInvoice(id) {
|
| 75 |
-
if (!confirm("Delete this invoice permanently from Neon?")) return;
|
| 76 |
-
dbDelete(id).then(function() {
|
| 77 |
-
showToast("ποΈ Invoice deleted", "success");
|
| 78 |
-
load(); // refresh the list
|
| 79 |
-
}).catch(err => {
|
| 80 |
-
showToast("β Delete failed", "error");
|
| 81 |
-
});
|
| 82 |
-
}
|
| 83 |
-
|
| 84 |
-
/* Get a single invoice by id from cache */
|
| 85 |
-
function getById(id) {
|
| 86 |
-
return _allInvoices.find(function(inv) { return inv.id === id; }) || null;
|
| 87 |
-
}
|
| 88 |
-
|
| 89 |
-
/* Internal render function (No changes needed) */
|
| 90 |
-
function _render(list) {
|
| 91 |
-
var container = document.getElementById("hist-list");
|
| 92 |
-
var count = document.getElementById("history-count");
|
| 93 |
-
if (!container || !count) return;
|
| 94 |
-
|
| 95 |
-
_bindClicks();
|
| 96 |
-
count.textContent = list.length + " invoice" + (list.length !== 1 ? "s" : "");
|
| 97 |
-
|
| 98 |
-
if (list.length === 0) {
|
| 99 |
-
var q = document.getElementById("search-input").value;
|
| 100 |
-
container.innerHTML = '<div class="empty-state"><h3>No invoices found</h3></div>';
|
| 101 |
-
} else {
|
| 102 |
-
container.innerHTML = list.map(_buildCard).join("");
|
| 103 |
-
}
|
| 104 |
-
}
|
| 105 |
-
|
| 106 |
-
function _bindClicks() {
|
| 107 |
-
if (_boundClicks) return;
|
| 108 |
-
document.addEventListener("click", function(e) {
|
| 109 |
-
var btn = e.target.closest("button[data-action]");
|
| 110 |
-
if (!btn) return;
|
| 111 |
-
var id = parseInt(btn.getAttribute("data-id"), 10);
|
| 112 |
-
var action = btn.getAttribute("data-action");
|
| 113 |
-
if (action === "load") {
|
| 114 |
-
if (typeof showToast === "function") showToast("Loading invoice...", "info");
|
| 115 |
-
if (typeof App !== "undefined" && typeof App.loadEdit === "function") {
|
| 116 |
-
App.loadEdit(id);
|
| 117 |
-
} else if (typeof showToast === "function") {
|
| 118 |
-
showToast("Load handler missing", "error");
|
| 119 |
-
}
|
| 120 |
-
} else if (action === "delete") {
|
| 121 |
-
deleteInvoice(id);
|
| 122 |
-
}
|
| 123 |
-
});
|
| 124 |
-
_boundClicks = true;
|
| 125 |
-
}
|
| 126 |
-
|
| 127 |
-
return { load: load, filter: filter, deleteInvoice: deleteInvoice, getById: getById };
|
| 128 |
-
|
| 129 |
-
})();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/rows.js
DELETED
|
@@ -1,153 +0,0 @@
|
|
| 1 |
-
/* βββββββββββββββββββββββββββββββββββββββββ
|
| 2 |
-
js/rows.js β Service row management (Live Version)
|
| 3 |
-
βββββββββββββββββββββββββββββββββββββββββ */
|
| 4 |
-
|
| 5 |
-
var Rows = (function() {
|
| 6 |
-
|
| 7 |
-
var _rowCounter = 0;
|
| 8 |
-
|
| 9 |
-
/* Build the <select> options HTML */
|
| 10 |
-
function _serviceOptions(selected) {
|
| 11 |
-
var html = '<option value="">β Select Service β</option>';
|
| 12 |
-
var services = (typeof SERVICES !== "undefined" && SERVICES) ? SERVICES : [];
|
| 13 |
-
services.forEach(function(s) {
|
| 14 |
-
html += '<option' + (s === selected ? ' selected' : '') + '>' + s + '</option>';
|
| 15 |
-
});
|
| 16 |
-
return html;
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
-
/* Create one table row of HTML */
|
| 20 |
-
function _buildRow(id, desc, qty, price, total) {
|
| 21 |
-
desc = desc || "";
|
| 22 |
-
qty = qty !== undefined ? qty : 1;
|
| 23 |
-
price = price !== undefined ? price : "";
|
| 24 |
-
total = total || 0;
|
| 25 |
-
|
| 26 |
-
return [
|
| 27 |
-
'<tr id="row-' + id + '">',
|
| 28 |
-
'<td class="sno row-index"></td>',
|
| 29 |
-
'<td>',
|
| 30 |
-
'<select class="tbl-select" onchange="Rows.update(' + id + ')">' + _serviceOptions(desc) + '</select>',
|
| 31 |
-
'</td>',
|
| 32 |
-
'<td>',
|
| 33 |
-
'<input type="number" class="tbl-input" min="1" value="' + qty + '"',
|
| 34 |
-
' style="text-align:center" oninput="Rows.update(' + id + ')"/>',
|
| 35 |
-
'</td>',
|
| 36 |
-
'<td>',
|
| 37 |
-
'<input type="number" class="tbl-input right" min="0" value="' + price + '" placeholder="0"',
|
| 38 |
-
' oninput="Rows.update(' + id + ')"/>',
|
| 39 |
-
'</td>',
|
| 40 |
-
'<td class="total-cell row-total">' + fmtNum(total) + '</td>',
|
| 41 |
-
'<td class="action-cell">',
|
| 42 |
-
'<button class="btn-del" onclick="Rows.remove(' + id + ')">Γ</button>',
|
| 43 |
-
'</td>',
|
| 44 |
-
'</tr>',
|
| 45 |
-
].join("");
|
| 46 |
-
}
|
| 47 |
-
|
| 48 |
-
/* Renumber, update, and summary logic (No changes needed here) */
|
| 49 |
-
function _renumber() {
|
| 50 |
-
var cells = document.querySelectorAll("#rows-body .row-index");
|
| 51 |
-
cells.forEach(function(cell, i) { cell.textContent = i + 1; });
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
function update(id) {
|
| 55 |
-
var row = document.getElementById("row-" + id);
|
| 56 |
-
if (!row) return;
|
| 57 |
-
var inputs = row.querySelectorAll("input[type=number]");
|
| 58 |
-
var qty = parseFloat(inputs[0].value) || 0;
|
| 59 |
-
var price = parseFloat(inputs[1].value) || 0;
|
| 60 |
-
var total = qty * price;
|
| 61 |
-
row.querySelector(".row-total").textContent = fmtNum(total);
|
| 62 |
-
_recalcSummary();
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
function _recalcSummary() {
|
| 66 |
-
var subtotal = 0;
|
| 67 |
-
document.querySelectorAll("#rows-body tr").forEach(function(row) {
|
| 68 |
-
var inputs = row.querySelectorAll("input[type=number]");
|
| 69 |
-
var qty = parseFloat(inputs[0] ? inputs[0].value : 0) || 0;
|
| 70 |
-
var price = parseFloat(inputs[1] ? inputs[1].value : 0) || 0;
|
| 71 |
-
subtotal += qty * price;
|
| 72 |
-
});
|
| 73 |
-
|
| 74 |
-
var received = parseFloat(document.getElementById("received-input").value) || 0;
|
| 75 |
-
var remaining = subtotal - received;
|
| 76 |
-
|
| 77 |
-
document.getElementById("summary-subtotal").textContent = fmt(subtotal);
|
| 78 |
-
document.getElementById("summary-total").textContent = fmt(subtotal);
|
| 79 |
-
document.getElementById("summary-remaining").textContent = fmt(remaining);
|
| 80 |
-
document.getElementById("summary-remaining").style.color = remaining > 0 ? "var(--red)" : "var(--green)";
|
| 81 |
-
}
|
| 82 |
-
|
| 83 |
-
/* ββ Collect current row data (UPDATED FOR FASTAPI) ββ */
|
| 84 |
-
function collect() {
|
| 85 |
-
var result = [];
|
| 86 |
-
document.querySelectorAll("#rows-body tr").forEach(function(row) {
|
| 87 |
-
var sel = row.querySelector("select");
|
| 88 |
-
var inputs = row.querySelectorAll("input[type=number]");
|
| 89 |
-
var qty = parseInt(inputs[0] ? inputs[0].value : 1) || 1;
|
| 90 |
-
var price = parseFloat(inputs[1] ? inputs[1].value : 0) || 0;
|
| 91 |
-
|
| 92 |
-
var serviceName = sel ? sel.value : "";
|
| 93 |
-
|
| 94 |
-
// We only collect rows that actually have a service selected
|
| 95 |
-
if (serviceName) {
|
| 96 |
-
result.push({
|
| 97 |
-
description: serviceName, // Matches Pydantic
|
| 98 |
-
quantity: qty, // Matches Pydantic
|
| 99 |
-
price_per_unit: price, // Matches Pydantic
|
| 100 |
-
total_price: qty * price // Optional, backend recalculates anyway
|
| 101 |
-
});
|
| 102 |
-
}
|
| 103 |
-
});
|
| 104 |
-
return result;
|
| 105 |
-
}
|
| 106 |
-
|
| 107 |
-
/* Helper methods */
|
| 108 |
-
function add() {
|
| 109 |
-
var id = ++_rowCounter;
|
| 110 |
-
document.getElementById("rows-body").insertAdjacentHTML("beforeend", _buildRow(id));
|
| 111 |
-
_renumber();
|
| 112 |
-
}
|
| 113 |
-
|
| 114 |
-
function remove(id) {
|
| 115 |
-
var row = document.getElementById("row-" + id);
|
| 116 |
-
if (row) { row.remove(); _renumber(); _recalcSummary(); }
|
| 117 |
-
}
|
| 118 |
-
|
| 119 |
-
function reset() {
|
| 120 |
-
document.getElementById("rows-body").innerHTML = "";
|
| 121 |
-
add(); add();
|
| 122 |
-
}
|
| 123 |
-
|
| 124 |
-
/* Loading from DB (Updated keys) */
|
| 125 |
-
function load(savedItems) {
|
| 126 |
-
document.getElementById("rows-body").innerHTML = "";
|
| 127 |
-
(savedItems || []).forEach(function(item) {
|
| 128 |
-
var id = ++_rowCounter;
|
| 129 |
-
// Note the mapping: description -> desc, etc.
|
| 130 |
-
document.getElementById("rows-body")
|
| 131 |
-
.insertAdjacentHTML("beforeend", _buildRow(
|
| 132 |
-
id,
|
| 133 |
-
item.description,
|
| 134 |
-
item.quantity,
|
| 135 |
-
item.price_per_unit,
|
| 136 |
-
(item.quantity * item.price_per_unit)
|
| 137 |
-
));
|
| 138 |
-
});
|
| 139 |
-
_renumber();
|
| 140 |
-
_recalcSummary();
|
| 141 |
-
}
|
| 142 |
-
|
| 143 |
-
function subtotal() {
|
| 144 |
-
return collect().reduce(function(sum, r) { return sum + (r.quantity * r.price_per_unit); }, 0);
|
| 145 |
-
}
|
| 146 |
-
|
| 147 |
-
document.addEventListener("DOMContentLoaded", function() {
|
| 148 |
-
document.getElementById("received-input").addEventListener("input", _recalcSummary);
|
| 149 |
-
});
|
| 150 |
-
|
| 151 |
-
return { add: add, remove: remove, reset: reset, load: load, collect: collect, subtotal: subtotal, update: update };
|
| 152 |
-
|
| 153 |
-
})();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/ui.js
DELETED
|
@@ -1,68 +0,0 @@
|
|
| 1 |
-
/* βββββββββββββββββββββββββββββββββββββββββ
|
| 2 |
-
js/ui.js β UI helpers & shared utils
|
| 3 |
-
βββββββββββββββββββββββββββββββββββββββββ */
|
| 4 |
-
|
| 5 |
-
/* Show a toast notification */
|
| 6 |
-
function showToast(msg, type) {
|
| 7 |
-
type = type || "success";
|
| 8 |
-
var el = document.getElementById("toast");
|
| 9 |
-
if (!el) return;
|
| 10 |
-
el.textContent = msg;
|
| 11 |
-
el.className = "show " + type;
|
| 12 |
-
clearTimeout(el._timer);
|
| 13 |
-
el._timer = setTimeout(function() { el.className = ""; }, 3000);
|
| 14 |
-
}
|
| 15 |
-
|
| 16 |
-
/* Format a number as PKR currency */
|
| 17 |
-
function fmt(n) {
|
| 18 |
-
return "PKR " + (Number(n) || 0).toLocaleString("en-PK", { minimumFractionDigits: 0 });
|
| 19 |
-
}
|
| 20 |
-
|
| 21 |
-
/* Format a number with commas */
|
| 22 |
-
function fmtNum(n) {
|
| 23 |
-
return (Number(n) || 0).toLocaleString("en-PK");
|
| 24 |
-
}
|
| 25 |
-
|
| 26 |
-
/* Return today's date as YYYY-MM-DD */
|
| 27 |
-
function todayStr() {
|
| 28 |
-
return new Date().toISOString().split("T")[0];
|
| 29 |
-
}
|
| 30 |
-
|
| 31 |
-
/* Show/hide pages */
|
| 32 |
-
function switchPage(page) {
|
| 33 |
-
const invPage = document.getElementById("page-invoice");
|
| 34 |
-
const histPage = document.getElementById("page-history");
|
| 35 |
-
const invTab = document.getElementById("tab-invoice");
|
| 36 |
-
const histTab = document.getElementById("tab-history");
|
| 37 |
-
|
| 38 |
-
if (invPage) invPage.style.display = (page === "invoice") ? "" : "none";
|
| 39 |
-
if (histPage) histPage.style.display = (page === "history") ? "" : "none";
|
| 40 |
-
|
| 41 |
-
if (invTab) invTab.classList.toggle("active", page === "invoice");
|
| 42 |
-
if (histTab) histTab.classList.toggle("active", page === "history");
|
| 43 |
-
}
|
| 44 |
-
|
| 45 |
-
/* Update the live invoice number badge in the header */
|
| 46 |
-
function updateHeaderBadge() {
|
| 47 |
-
const numInput = document.getElementById("inv-number");
|
| 48 |
-
const displayPill = document.getElementById("display-inv-number");
|
| 49 |
-
|
| 50 |
-
if (numInput && displayPill) {
|
| 51 |
-
const val = numInput.value;
|
| 52 |
-
// Show "NEW" if it hasn't been saved to Neon yet
|
| 53 |
-
const displayNum = (val === "Auto-Generated" || !val) ? "NEW" : val;
|
| 54 |
-
displayPill.textContent = "#" + displayNum;
|
| 55 |
-
}
|
| 56 |
-
}
|
| 57 |
-
|
| 58 |
-
/* Wire up listeners */
|
| 59 |
-
document.addEventListener("DOMContentLoaded", function() {
|
| 60 |
-
const invNumEl = document.getElementById("inv-number");
|
| 61 |
-
|
| 62 |
-
// Update badge whenever the invoice number changes (e.g., after saving)
|
| 63 |
-
if (invNumEl) {
|
| 64 |
-
invNumEl.addEventListener("change", updateHeaderBadge);
|
| 65 |
-
// Initial call to set badge to #NEW
|
| 66 |
-
updateHeaderBadge();
|
| 67 |
-
}
|
| 68 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|