AhmadYarAI commited on
Commit
5f1152c
Β·
1 Parent(s): e31dd88

fix: serve frontend from root

Browse files
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] : "β€”") + ' &nbsp;Β·&nbsp; ' + (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
- });