AhmadYarAI commited on
Commit
b54d01b
Β·
1 Parent(s): 750b124

feat: add invoice creation logic with SQLAlchemy

Browse files
Dockerfile CHANGED
@@ -1,32 +1,21 @@
1
  FROM python:3.10-slim
2
 
3
  USER root
4
- # Install system dependencies for Postgres and building extensions
5
  RUN apt-get update && apt-get install -y libpq-dev gcc && rm -rf /var/lib/apt/lists/*
6
 
7
- # Setup user
8
  RUN useradd -m -u 1000 user
9
  USER user
10
  ENV HOME=/home/user \
11
  PATH=/home/user/.local/bin:$PATH
12
 
13
- # We set the app root
14
  WORKDIR $HOME/app
15
 
16
- # 1. Copy requirements from the backend folder
17
  COPY --chown=user:user requirements.txt .
18
  RUN pip install --no-cache-dir --upgrade -r requirements.txt
19
 
20
- # 2. Copy everything from the backend folder into the 'backend' subfolder
21
- COPY --chown=user:user . ./backend
22
 
23
- # 3. IMPORTANT: Hugging Face builds from the root of your repo.
24
- # If 'frontend' is next to 'backend', we ensure both are accessible.
25
- # If you are pushing from the root, use:
26
- COPY --chown=user:user ../frontend ./frontend
27
-
28
- # 4. Set the working directory to where main.py lives
29
- WORKDIR $HOME/app/backend
30
-
31
- # 5. Run the app
32
  CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
 
1
  FROM python:3.10-slim
2
 
3
  USER root
 
4
  RUN apt-get update && apt-get install -y libpq-dev gcc && rm -rf /var/lib/apt/lists/*
5
 
 
6
  RUN useradd -m -u 1000 user
7
  USER user
8
  ENV HOME=/home/user \
9
  PATH=/home/user/.local/bin:$PATH
10
 
 
11
  WORKDIR $HOME/app
12
 
13
+ # 1. Copy requirements and install
14
  COPY --chown=user:user requirements.txt .
15
  RUN pip install --no-cache-dir --upgrade -r requirements.txt
16
 
17
+ # 2. Copy everything (including the moved frontend folder)
18
+ COPY --chown=user:user . .
19
 
20
+ # 3. Run the app
 
 
 
 
 
 
 
 
21
  CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
frontend/css/style.css ADDED
@@ -0,0 +1,262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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-name { font-family: 'Lora', serif; font-size: 1.12rem; font-weight: 700; color: #fff; letter-spacing: 0.01em; }
59
+ .topbar-sub { font-size: 0.7rem; color: rgba(255,255,255,0.65); letter-spacing: 0.1em; text-transform: uppercase; margin-top: 1px; }
60
+ .topbar-tabs { display: flex; gap: 4px; }
61
+ .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; }
62
+ .topbar-tab:hover { background: rgba(255,255,255,0.12); color: #fff; }
63
+ .topbar-tab.active { background: rgba(255,255,255,0.22); color: #fff; }
64
+ .topbar-info { display: flex; align-items: center; gap: 8px; font-size: 0.78rem; color: rgba(255,255,255,0.65); }
65
+ .topbar-dot { width: 4px; height: 4px; background: rgba(255,255,255,0.35); border-radius: 50%; }
66
+
67
+ /* ─── LAYOUT ─── */
68
+ .app-body { padding: 2rem; max-width: 1080px; margin: 0 auto; }
69
+
70
+ /* ─── INVOICE CARD ─── */
71
+ .inv-card {
72
+ background: var(--white);
73
+ border-radius: var(--radius-lg);
74
+ box-shadow: var(--shadow-lg);
75
+ overflow: hidden;
76
+ animation: fadeUp 0.35s ease;
77
+ }
78
+ @keyframes fadeUp {
79
+ from { opacity: 0; transform: translateY(14px); }
80
+ to { opacity: 1; transform: translateY(0); }
81
+ }
82
+
83
+ /* ─── INVOICE HEADER BAND ─── */
84
+ .inv-header-band {
85
+ background: linear-gradient(135deg, var(--blue-deep) 0%, #1e4d8c 60%, var(--teal) 100%);
86
+ padding: 2rem 2.5rem;
87
+ display: grid;
88
+ grid-template-columns: 1fr auto;
89
+ gap: 2rem;
90
+ align-items: center;
91
+ }
92
+ .lab-title { font-family: 'Lora', serif; font-size: 1.7rem; font-weight: 700; color: #fff; letter-spacing: 0.01em; line-height: 1.2; }
93
+ .lab-meta { display: flex; align-items: center; gap: 16px; margin-top: 6px; flex-wrap: wrap; }
94
+ .lab-meta-item { display: flex; align-items: center; gap: 5px; font-size: 0.78rem; color: rgba(255,255,255,0.8); }
95
+ .inv-badge-block { text-align: right; }
96
+ .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; }
97
+ .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; }
98
+
99
+ /* ─── STATUS BADGES ─── */
100
+ .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; }
101
+ .status-pending { background: var(--amber-light); color: #92400E; }
102
+ .status-paid { background: var(--green-light); color: #065F46; }
103
+ .status-partial { background: var(--teal-light); color: #155E75; }
104
+ .status-cancelled{ background: var(--red-light); color: #991B1B; }
105
+
106
+ /* ─── SECTIONS ─── */
107
+ .inv-section { padding: 1.4rem 2.5rem; border-bottom: 1px solid var(--gray-200); }
108
+ .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; }
109
+ .section-title::after { content: ''; flex: 1; height: 1.5px; background: linear-gradient(to right, var(--blue-light), transparent); }
110
+
111
+ /* ─── FORM ─── */
112
+ .form-grid { display: grid; gap: 12px; }
113
+ .form-grid-3 { grid-template-columns: repeat(3, 1fr); }
114
+ .form-grid-2 { grid-template-columns: repeat(2, 1fr); }
115
+ .form-grid-4 { grid-template-columns: repeat(4, 1fr); }
116
+ .form-group { display: flex; flex-direction: column; gap: 4px; }
117
+ .form-group label { font-size: 0.71rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--gray-500); }
118
+ .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%; }
119
+ .form-control:focus { border-color: var(--blue-accent); box-shadow: 0 0 0 3px rgba(59,130,246,0.1); }
120
+ .form-control::placeholder { color: var(--gray-400); }
121
+
122
+ /* ─── SERVICES TABLE ─── */
123
+ .table-wrap { overflow-x: auto; }
124
+ table.svc-table { width: 100%; border-collapse: collapse; font-size: 0.84rem; }
125
+ .svc-table thead tr { background: var(--blue-deep); }
126
+ .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; }
127
+ .svc-table thead th.center { text-align: center; }
128
+ .svc-table thead th.right { text-align: right; }
129
+ .svc-table tbody tr { border-bottom: 1px solid var(--gray-100); transition: background 0.12s; }
130
+ .svc-table tbody tr:nth-child(even) { background: var(--gray-50); }
131
+ .svc-table tbody tr:hover { background: var(--blue-light); }
132
+ .svc-table td { padding: 6px 6px; vertical-align: middle; }
133
+ .svc-table td.sno { text-align: center; font-weight: 700; color: var(--gray-400); font-size: 0.8rem; width: 42px; }
134
+ .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; }
135
+ .svc-table td.action-cell { text-align: center; width: 40px; }
136
+
137
+ .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; }
138
+ .tbl-input:focus { border-color: var(--blue-accent); background: var(--white); }
139
+ .tbl-input.right { text-align: right; }
140
+ .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; }
141
+ .tbl-select:focus { border-color: var(--blue-accent); background: var(--white); }
142
+
143
+ .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; }
144
+ .btn-del:hover { background: var(--red); color: #fff; transform: scale(1.1); }
145
+
146
+ .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; }
147
+ .add-row-btn:hover { background: var(--blue-light); border-color: var(--blue-deep); color: var(--blue-deep); }
148
+
149
+ /* ─── SUMMARY ─── */
150
+ .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; }
151
+ .summary-row:last-child { border-bottom: none; }
152
+ .summary-row .label { color: var(--gray-500); font-weight: 600; }
153
+ .summary-row .value { font-weight: 700; color: var(--gray-900); font-size: 0.95rem; }
154
+ .summary-row.net { background: var(--blue-deep); margin: 8px -2.5rem -1.4rem; padding: 16px 2.5rem; border-bottom: none; }
155
+ .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; }
156
+ .summary-row.net .value { color: #fff; font-family: 'Lora', serif; font-size: 1.5rem; }
157
+ .summary-row.received .value { color: var(--green); }
158
+ .summary-row.remaining .value { color: var(--red); }
159
+
160
+ .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; }
161
+ .received-input:focus { border-color: var(--green); box-shadow: 0 0 0 3px rgba(16,185,129,0.1); }
162
+
163
+ /* ─── FOOTER / SIGNATURE ─── */
164
+ .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); }
165
+ .sig-block { text-align: right; }
166
+ .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); }
167
+
168
+ /* ─── ACTION BAR ─── */
169
+ .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; }
170
+ .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; }
171
+ .btn-save { background: var(--blue-deep); color: #fff; }
172
+ .btn-save:hover { background: #162f5a; transform: translateY(-1px); box-shadow: var(--shadow); }
173
+ .btn-pdf { background: var(--teal); color: #fff; }
174
+ .btn-pdf:hover { background: #0e7490; transform: translateY(-1px); box-shadow: var(--shadow); }
175
+ .btn-print { background: var(--gray-700); color: #fff; }
176
+ .btn-print:hover { background: var(--gray-900); transform: translateY(-1px); box-shadow: var(--shadow); }
177
+ .btn-excel { background: var(--green); color: #fff; }
178
+ .btn-excel:hover { background: #059669; transform: translateY(-1px); box-shadow: var(--shadow); }
179
+ .btn-clear { background: var(--amber-light); color: #92400E; border: 1.5px solid #FDE68A; }
180
+ .btn-clear:hover { background: #FDE68A; }
181
+ .btn-new { background: var(--blue-light); color: var(--blue-deep); border: 1.5px solid #BFDBFE; }
182
+ .btn-new:hover { background: #BFDBFE; }
183
+ .btn-spacer { flex: 1; }
184
+
185
+ /* ─── HISTORY PAGE ─── */
186
+ .page-heading { display: flex; align-items: baseline; gap: 12px; margin-bottom: 1.5rem; }
187
+ .page-heading h2 { font-family: 'Lora', serif; font-size: 1.5rem; font-weight: 700; color: var(--blue-deep); }
188
+ .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; }
189
+ .search-wrap { margin-bottom: 1.2rem; position: relative; }
190
+ .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; }
191
+ .search-wrap input:focus { border-color: var(--blue-accent); }
192
+ .search-icon { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--gray-400); font-size: 16px; }
193
+
194
+ .hist-list { display: flex; flex-direction: column; gap: 10px; }
195
+ .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; }
196
+ .hist-item:hover { box-shadow: var(--shadow); border-left-color: var(--blue-accent); }
197
+ .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; }
198
+ .hist-client-name { font-weight: 700; font-size: 0.95rem; }
199
+ .hist-date { font-size: 0.78rem; color: var(--gray-400); }
200
+ .hist-detail { font-size: 0.82rem; color: var(--gray-500); }
201
+ .hist-detail span { font-weight: 600; color: var(--gray-700); }
202
+ .hist-amount { font-family: 'Lora', serif; font-size: 1.05rem; font-weight: 700; color: var(--blue-deep); text-align: right; }
203
+ .hist-actions { display: flex; gap: 6px; }
204
+ .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; }
205
+ .btn-xs-blue { background: var(--blue-deep); color: #fff; }
206
+ .btn-xs-blue:hover { background: #162f5a; }
207
+ .btn-xs-red { background: var(--red-light); color: var(--red); }
208
+ .btn-xs-red:hover { background: var(--red); color: #fff; }
209
+
210
+ .empty-state { text-align: center; padding: 5rem 2rem; color: var(--gray-400); }
211
+ .empty-state .ico { font-size: 3.5rem; margin-bottom: 1rem; }
212
+ .empty-state h3 { font-family: 'Lora', serif; color: var(--gray-700); font-size: 1.3rem; margin-bottom: 6px; }
213
+
214
+ /* ─── TOAST ─── */
215
+ #toast {
216
+ position: fixed;
217
+ bottom: 2rem; right: 2rem;
218
+ background: var(--blue-deep);
219
+ color: #fff;
220
+ padding: 12px 20px;
221
+ border-radius: 10px;
222
+ font-size: 0.87rem;
223
+ font-weight: 600;
224
+ box-shadow: var(--shadow-lg);
225
+ display: flex;
226
+ align-items: center;
227
+ gap: 8px;
228
+ opacity: 0;
229
+ transform: translateY(16px);
230
+ transition: all 0.3s cubic-bezier(0.34,1.56,0.64,1);
231
+ z-index: 9999;
232
+ pointer-events: none;
233
+ }
234
+ #toast.show { opacity: 1; transform: translateY(0); }
235
+ #toast.success { border-left: 4px solid var(--green); }
236
+ #toast.error { border-left: 4px solid var(--red); }
237
+ #toast.info { border-left: 4px solid var(--teal); }
238
+
239
+ /* ─── PRINT ─── */
240
+ @media print {
241
+ body { background: #fff !important; }
242
+ .topbar, .action-bar, .btn-del, .add-row-btn, #toast, #page-history { display: none !important; }
243
+ #page-invoice { display: block !important; }
244
+ .app-body { padding: 0 !important; max-width: none !important; }
245
+ .inv-card { box-shadow: none !important; border: 1px solid #ddd; }
246
+ .inv-header-band, .svc-table thead tr, .summary-row.net { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
247
+ .tbl-input, .tbl-select, .form-control, .received-input { border: none !important; background: transparent !important; }
248
+ }
249
+
250
+ /* ─── RESPONSIVE ─── */
251
+ @media (max-width: 768px) {
252
+ .app-body { padding: 1rem; }
253
+ .inv-header-band { grid-template-columns: 1fr; }
254
+ .inv-badge-block { text-align: left; }
255
+ .form-grid-3, .form-grid-4 { grid-template-columns: 1fr 1fr; }
256
+ .form-grid-2 { grid-template-columns: 1fr; }
257
+ .topbar-info { display: none; }
258
+ .hist-item { grid-template-columns: 1fr 1fr; grid-template-rows: auto auto; }
259
+ .inv-section { padding: 1.2rem 1.2rem; }
260
+ .inv-header-band { padding: 1.5rem 1.2rem; }
261
+ .action-bar { padding: 1rem 1.2rem; }
262
+ }
frontend/index.html ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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@300;400;500;600;700;800&family=Lora:wght@400;500;600;700&display=swap" rel="stylesheet"/>
8
+ <link rel="stylesheet" href="css/style.css"/>
9
+ </head>
10
+ <body>
11
+
12
+ <!-- TOP NAV BAR -->
13
+ <div class="topbar">
14
+ <div class="topbar-brand">
15
+ <div class="topbar-icon">🦷</div>
16
+ <div>
17
+ <div class="topbar-name">SmiloCAD Dental Laboratory</div>
18
+ <div class="topbar-sub">Invoice Management System</div>
19
+ </div>
20
+ </div>
21
+ <div class="topbar-tabs">
22
+ <button class="topbar-tab active" id="tab-invoice" onclick="App.showPage('invoice')">βž• New Invoice</button>
23
+ <button class="topbar-tab" id="tab-history" onclick="App.showPage('history')">πŸ“‹ History</button>
24
+ </div>
25
+ <div class="topbar-info">
26
+ <span>πŸ“ž 0328-9577771</span>
27
+ <span class="topbar-dot"></span>
28
+ <span>Al Anayat Plaza, G11 Markaz Islamabad</span>
29
+ </div>
30
+ </div>
31
+
32
+ <!-- ═══ INVOICE PAGE ═══ -->
33
+ <div class="app-body" id="page-invoice">
34
+ <div class="inv-card">
35
+
36
+ <!-- Header Band -->
37
+ <div class="inv-header-band">
38
+ <div>
39
+ <div class="lab-title">SmiloCAD Dental Laboratory</div>
40
+ <div class="lab-meta">
41
+ <div class="lab-meta-item">πŸ“ž 0328-9577771</div>
42
+ <div class="lab-meta-item">πŸ“ Al Anayat Plaza, G11 Markaz Islamabad</div>
43
+ </div>
44
+ </div>
45
+ <div class="inv-badge-block">
46
+ <div class="inv-badge-label">Invoice</div>
47
+ <div class="inv-number-pill" id="display-inv-number">#INV-0001</div>
48
+ <div><span class="status-badge status-pending" id="display-status">Pending</span></div>
49
+ </div>
50
+ </div>
51
+
52
+ <!-- Invoice Details -->
53
+ <div class="inv-section">
54
+ <div class="section-title">Invoice Details</div>
55
+ <div class="form-grid form-grid-3">
56
+ <div class="form-group">
57
+ <label>Invoice No.</label>
58
+ <input class="form-control" id="inv-number" placeholder="INV-0001"/>
59
+ </div>
60
+ <div class="form-group">
61
+ <label>Date</label>
62
+ <input type="date" class="form-control" id="inv-date"/>
63
+ </div>
64
+ <div class="form-group">
65
+ <label>Status</label>
66
+ <select class="form-control" id="inv-status">
67
+ <option>Pending</option>
68
+ <option>Paid</option>
69
+ <option>Partial</option>
70
+ <option>Cancelled</option>
71
+ </select>
72
+ </div>
73
+ </div>
74
+ </div>
75
+
76
+ <!-- Doctor & Patient -->
77
+ <div class="inv-section">
78
+ <div class="section-title">Doctor &amp; Patient Information</div>
79
+ <div class="form-grid form-grid-2" style="margin-bottom:12px">
80
+ <div class="form-group">
81
+ <label>Doctor Name</label>
82
+ <input class="form-control" id="doctor-name" placeholder="Enter doctor name"/>
83
+ </div>
84
+ <div class="form-group">
85
+ <label>Clinic Name</label>
86
+ <input class="form-control" id="clinic" placeholder="e.g. Fit Dental Care"/>
87
+ </div>
88
+ </div>
89
+ <div class="form-grid form-grid-2">
90
+ <div class="form-group">
91
+ <label>Patient Name</label>
92
+ <input class="form-control" id="patient" placeholder="Patient full name"/>
93
+ </div>
94
+ <div class="form-group">
95
+ <label>Shade</label>
96
+ <input class="form-control" id="shade" placeholder="Enter shade"/>
97
+ </div>
98
+ </div>
99
+ </div>
100
+
101
+ <!-- Services Table -->
102
+ <div class="inv-section">
103
+ <div class="section-title">Services / Work Items</div>
104
+ <div class="table-wrap">
105
+ <table class="svc-table">
106
+ <thead>
107
+ <tr>
108
+ <th class="center">#</th>
109
+ <th style="min-width:220px">Description</th>
110
+ <th style="min-width:90px" class="center">Quantity</th>
111
+ <th style="min-width:110px" class="right">Price/Unit (PKR)</th>
112
+ <th style="min-width:120px" class="right">Total (PKR)</th>
113
+ <th class="center">βœ•</th>
114
+ </tr>
115
+ </thead>
116
+ <tbody id="rows-body">
117
+ <!-- Rows injected by JS -->
118
+ </tbody>
119
+ </table>
120
+ </div>
121
+ <button class="add-row-btn" onclick="Rows.add()">+ Add Row</button>
122
+ </div>
123
+
124
+ <!-- Summary -->
125
+ <div class="inv-section">
126
+ <div class="section-title">Summary</div>
127
+ <div style="max-width:380px;margin-left:auto">
128
+ <div class="summary-row">
129
+ <span class="label">Subtotal</span>
130
+ <span class="value" id="summary-subtotal">PKR 0</span>
131
+ </div>
132
+ <div class="summary-row received">
133
+ <span class="label">Received Amount</span>
134
+ <input class="received-input" type="number" min="0" placeholder="0" id="received-input"/>
135
+ </div>
136
+ <div class="summary-row remaining">
137
+ <span class="label">Remaining Balance</span>
138
+ <span class="value" id="summary-remaining">PKR 0</span>
139
+ </div>
140
+ <div class="summary-row net">
141
+ <span class="label">Total Amount</span>
142
+ <span class="value" id="summary-total">PKR 0</span>
143
+ </div>
144
+ </div>
145
+ </div>
146
+
147
+ <!-- Notes -->
148
+ <div class="inv-section" style="border-bottom:none">
149
+ <div class="section-title">Notes / Terms</div>
150
+ <textarea class="form-control" id="notes" rows="2"
151
+ style="resize:vertical;min-height:60px"
152
+ placeholder="Payment terms, delivery schedule, special instructions…"></textarea>
153
+ </div>
154
+
155
+ <!-- Footer -->
156
+ <div class="inv-footer">
157
+ <div>Thank you for your trust in <strong>SmiloCAD Dental Laboratory</strong></div>
158
+ <div class="sig-block">
159
+ <div class="sig-line">Lab Technician: Dt. Sajid</div>
160
+ </div>
161
+ </div>
162
+
163
+ <!-- Action Bar -->
164
+ <div class="action-bar">
165
+ <button class="btn btn-save" onclick="App.save()">πŸ’Ύ Save</button>
166
+ <button class="btn btn-pdf" onclick="App.downloadPDF()">⬇️ Download PDF</button>
167
+ <button class="btn btn-print" onclick="App.print()">πŸ–¨οΈ Print</button>
168
+ <button class="btn btn-excel" onclick="App.exportExcel()">πŸ“Š Export Excel</button>
169
+ <div class="btn-spacer"></div>
170
+ <button class="btn btn-new" onclick="App.newInvoice()">πŸ“„ New Invoice</button>
171
+ <button class="btn btn-clear" onclick="App.clear()">πŸ—‘οΈ Clear</button>
172
+ </div>
173
+
174
+ </div>
175
+ </div>
176
+
177
+ <!-- ═══ HISTORY PAGE ═══ -->
178
+ <div class="app-body" id="page-history" style="display:none">
179
+ <div class="page-heading">
180
+ <h2>Invoice History</h2>
181
+ <span class="count-pill" id="history-count">0 invoices</span>
182
+ </div>
183
+ <div class="search-wrap">
184
+ <span class="search-icon">πŸ”</span>
185
+ <input id="search-input" placeholder="Search by doctor, clinic, patient, invoice no., or status…"
186
+ oninput="History.filter()"/>
187
+ </div>
188
+ <div class="hist-list" id="hist-list">
189
+ <!-- Injected by JS -->
190
+ </div>
191
+ </div>
192
+
193
+ <!-- Toast -->
194
+ <div id="toast"></div>
195
+
196
+ <!-- JS Files (load in order) -->
197
+ <script src="js/data.js"></script>
198
+ <script src="js/db.js"></script>
199
+ <script src="js/ui.js"></script>
200
+ <script src="js/rows.js"></script>
201
+ <script src="js/history.js"></script>
202
+ <script src="js/app.js"></script>
203
+ </body>
204
+ </html>
frontend/js/app.js ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ─────────────────────────────────────────
2
+ js/app.js β€” Main application controller
3
+ ───────────────────────────────────────── */
4
+
5
+ var App = (function() {
6
+
7
+ var _currentId = null; // id of the invoice being edited (null = new)
8
+
9
+ /* ── Collect all form values into one plain object ── */
10
+ function _collect() {
11
+ var subtotal = Rows.subtotal();
12
+ var received = parseFloat(document.getElementById("received-input").value) || 0;
13
+
14
+ return {
15
+ invNumber: document.getElementById("inv-number").value,
16
+ invDate: document.getElementById("inv-date").value,
17
+ doctor: document.getElementById("doctor-name").value,
18
+ clinic: document.getElementById("clinic").value,
19
+ patient: document.getElementById("patient").value,
20
+ shade: document.getElementById("shade").value,
21
+ status: document.getElementById("inv-status").value,
22
+ rows: Rows.collect(),
23
+ subtotal: subtotal,
24
+ received: received,
25
+ remaining: subtotal - received,
26
+ notes: document.getElementById("notes").value,
27
+ savedAt: new Date().toISOString(),
28
+ };
29
+ }
30
+
31
+ /* ── Populate all form fields from a saved invoice object ── */
32
+ function _populate(inv) {
33
+ document.getElementById("inv-number").value = inv.invNumber || "";
34
+ document.getElementById("inv-date").value = inv.invDate || "";
35
+ document.getElementById("inv-status").value = inv.status || "Pending";
36
+ document.getElementById("doctor-name").value = inv.doctor || "";
37
+ document.getElementById("clinic").value = inv.clinic || "";
38
+ document.getElementById("patient").value = inv.patient || "";
39
+ document.getElementById("shade").value = inv.shade || "";
40
+ document.getElementById("notes").value = inv.notes || "";
41
+ document.getElementById("received-input").value = inv.received != null ? inv.received : "";
42
+
43
+ Rows.load(inv.rows || []);
44
+ updateHeaderBadge();
45
+ }
46
+
47
+ /* ── Generate the next invoice number from DB count ── */
48
+ async function _nextInvNumber() {
49
+ var all = await dbAll();
50
+ var next = String(all.length + 1).padStart(4, "0");
51
+ return "INV-" + next;
52
+ }
53
+
54
+ /* ── Reset form to a blank new invoice ── */
55
+ async function _resetForm() {
56
+ _currentId = null;
57
+ var num = await _nextInvNumber();
58
+ document.getElementById("inv-number").value = num;
59
+ document.getElementById("inv-date").value = todayStr();
60
+ document.getElementById("inv-status").value = "Pending";
61
+ document.getElementById("doctor-name").value = "";
62
+ document.getElementById("clinic").value = "";
63
+ document.getElementById("patient").value = "";
64
+ document.getElementById("shade").value = "";
65
+ document.getElementById("notes").value = "";
66
+ document.getElementById("received-input").value = "";
67
+ Rows.reset();
68
+ updateHeaderBadge();
69
+ }
70
+
71
+ /* ────────────────────────────────────────
72
+ PUBLIC METHODS
73
+ ──────────────────────────────────────── */
74
+
75
+ /* Switch visible page */
76
+ function showPage(page) {
77
+ switchPage(page);
78
+ if (page === "history") History.load();
79
+ }
80
+
81
+ /* Save current invoice */
82
+ async function save() {
83
+ var data = _collect();
84
+ if (_currentId) data.id = _currentId;
85
+ var id = await dbSave(data);
86
+ _currentId = data.id || id;
87
+ showToast("βœ… Invoice saved successfully!", "success");
88
+ }
89
+
90
+ /* New blank invoice */
91
+ async function newInvoice() {
92
+ await _resetForm();
93
+ showPage("invoice");
94
+ showToast("πŸ“„ New invoice ready", "info");
95
+ }
96
+
97
+ /* Clear the current form fields (keep invoice number & date) */
98
+ function clear() {
99
+ if (!confirm("Clear the current form?")) return;
100
+ document.getElementById("doctor-name").value = "";
101
+ document.getElementById("clinic").value = "";
102
+ document.getElementById("patient").value = "";
103
+ document.getElementById("shade").value = "";
104
+ document.getElementById("inv-status").value = "Pending";
105
+ document.getElementById("notes").value = "";
106
+ document.getElementById("received-input").value = "";
107
+ Rows.reset();
108
+ updateHeaderBadge();
109
+ showToast("πŸ—‘οΈ Form cleared", "info");
110
+ }
111
+
112
+ /* Print the invoice */
113
+ function print() {
114
+ showPage("invoice");
115
+ setTimeout(function() { window.print(); }, 200);
116
+ }
117
+
118
+ /* Download as PDF (uses browser print dialog) */
119
+ function downloadPDF() {
120
+ showToast("πŸ“„ Choose 'Save as PDF' in the print dialog", "info");
121
+ setTimeout(function() { window.print(); }, 400);
122
+ }
123
+
124
+ /* Export as CSV (opens in Excel) */
125
+ function exportExcel() {
126
+ var data = _collect();
127
+ var lines = [
128
+ ["SmiloCAD Dental Laboratory"],
129
+ [LAB.address],
130
+ ["Phone:", LAB.phone],
131
+ [],
132
+ ["Invoice No.", data.invNumber, "Date:", data.invDate],
133
+ ["Doctor:", data.doctor, "Clinic:", data.clinic],
134
+ ["Patient:", data.patient, "Shade:", data.shade],
135
+ ["Status:", data.status],
136
+ [],
137
+ ["S.No", "Description", "Quantity", "Price/Unit", "Total"],
138
+ ];
139
+
140
+ data.rows.filter(function(r) { return r.desc || r.qty || r.price; })
141
+ .forEach(function(r, i) {
142
+ lines.push([i + 1, r.desc, r.qty, r.price, r.total]);
143
+ });
144
+
145
+ lines = lines.concat([
146
+ [],
147
+ ["", "", "", "Total:", data.subtotal],
148
+ ["", "", "", "Received:", data.received],
149
+ ["", "", "", "Remaining:", data.remaining],
150
+ [],
151
+ ["Notes:", data.notes],
152
+ ["Lab Technician:", LAB.tech],
153
+ ]);
154
+
155
+ var csv = lines.map(function(row) {
156
+ return row.map(function(cell) {
157
+ return '"' + String(cell != null ? cell : "").replace(/"/g, '""') + '"';
158
+ }).join(",");
159
+ }).join("\n");
160
+
161
+ var a = document.createElement("a");
162
+ a.href = URL.createObjectURL(new Blob([csv], { type: "text/csv" }));
163
+ a.download = data.invNumber + ".csv";
164
+ a.click();
165
+ showToast("πŸ“Š Exported as CSV (open in Excel)", "success");
166
+ }
167
+
168
+ /* Load a saved invoice into the form for editing */
169
+ async function loadEdit(id) {
170
+ /* Try the History cache first, then fall back to a fresh DB read */
171
+ var inv = History.getById(id);
172
+ if (!inv) {
173
+ var all = await dbAll();
174
+ inv = all.find(function(r) { return r.id === id; });
175
+ }
176
+ if (!inv) { showToast("❌ Invoice not found", "error"); return; }
177
+
178
+ _currentId = inv.id;
179
+ _populate(inv);
180
+ showPage("invoice");
181
+ showToast("✏️ Invoice loaded for editing", "info");
182
+ }
183
+
184
+ /* ── Boot: open DB, set defaults, render two blank rows ── */
185
+ document.addEventListener("DOMContentLoaded", async function() {
186
+ await dbOpen();
187
+ var num = await _nextInvNumber();
188
+ document.getElementById("inv-number").value = num;
189
+ document.getElementById("inv-date").value = todayStr();
190
+ updateHeaderBadge();
191
+ Rows.reset();
192
+ });
193
+
194
+ /* Public API */
195
+ return {
196
+ showPage: showPage,
197
+ save: save,
198
+ newInvoice: newInvoice,
199
+ clear: clear,
200
+ print: print,
201
+ downloadPDF: downloadPDF,
202
+ exportExcel: exportExcel,
203
+ loadEdit: loadEdit,
204
+ };
205
+
206
+ })();
frontend/js/data.js ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ "Zirconium Crown",
14
+ "Veneers",
15
+ "PFM Crown",
16
+ "PFM Design",
17
+ "PFM 3D",
18
+ "Zirconium Bridge + I Bar",
19
+ "Zirconium Bridge",
20
+ "Full Zirconium",
21
+ "E-Max Crown",
22
+ "E-Max Veneer",
23
+ "Metal Crown",
24
+ "Acrylic Crown",
25
+ "Implant Crown",
26
+ "Implant Abutment",
27
+ "Complete Denture",
28
+ "Partial Denture (RPD)",
29
+ "Immediate Denture",
30
+ "Night Guard",
31
+ "Retainer",
32
+ "Maryland Bridge",
33
+ "Inlay",
34
+ "Onlay",
35
+ "Post & Core",
36
+ "Study Model",
37
+ "Occlusal Splint",
38
+ "Other",
39
+ ];
40
+
41
+ const DOCTORS = [
42
+ "Dr. Haroon Shah",
43
+ "Dr. Ahmad Raza",
44
+ "Dr. Sara Khan",
45
+ "Dr. Imran Ali",
46
+ "Dr. Fatima Malik",
47
+ "Other",
48
+ ];
49
+
50
+ const SHADES = [
51
+ "β€”","A1","A2","A3","A3.5","A4",
52
+ "B1","B2","B3","B4",
53
+ "C1","C2","C3","C4",
54
+ "D2","D3","D4",
55
+ "BL1","BL2","BL3","BL4","Custom",
56
+ ];
frontend/js/db.js ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ─────────────────────────────────────────
2
+ js/db.js β€” IndexedDB database helpers
3
+ ───────────────────────────────────────── */
4
+
5
+ const DB_NAME = "SmiloCAD_DB";
6
+ const DB_STORE = "invoices";
7
+ const DB_VER = 1;
8
+
9
+ let _db = null; // holds the open IDBDatabase instance
10
+
11
+ /* Open (or create) the database */
12
+ function dbOpen() {
13
+ return new Promise(function(resolve, reject) {
14
+ const request = indexedDB.open(DB_NAME, DB_VER);
15
+
16
+ request.onupgradeneeded = function(e) {
17
+ const db = e.target.result;
18
+ if (!db.objectStoreNames.contains(DB_STORE)) {
19
+ db.createObjectStore(DB_STORE, { keyPath: "id", autoIncrement: true });
20
+ }
21
+ };
22
+
23
+ request.onsuccess = function(e) {
24
+ _db = e.target.result;
25
+ resolve(_db);
26
+ };
27
+
28
+ request.onerror = function(e) {
29
+ reject(e);
30
+ };
31
+ });
32
+ }
33
+
34
+ /* Save (insert or update) one invoice record */
35
+ function dbSave(data) {
36
+ return new Promise(function(resolve, reject) {
37
+ const tx = _db.transaction(DB_STORE, "readwrite");
38
+ const store = tx.objectStore(DB_STORE);
39
+ const request = data.id ? store.put(data) : store.add(data);
40
+
41
+ request.onsuccess = function(e) { resolve(e.target.result); };
42
+ request.onerror = reject;
43
+ });
44
+ }
45
+
46
+ /* Load all invoice records */
47
+ function dbAll() {
48
+ return new Promise(function(resolve, reject) {
49
+ const request = _db.transaction(DB_STORE, "readonly")
50
+ .objectStore(DB_STORE)
51
+ .getAll();
52
+
53
+ request.onsuccess = function(e) { resolve(e.target.result); };
54
+ request.onerror = reject;
55
+ });
56
+ }
57
+
58
+ /* Delete one invoice by id */
59
+ function dbDelete(id) {
60
+ return new Promise(function(resolve, reject) {
61
+ const request = _db.transaction(DB_STORE, "readwrite")
62
+ .objectStore(DB_STORE)
63
+ .delete(id);
64
+
65
+ request.onsuccess = resolve;
66
+ request.onerror = reject;
67
+ });
68
+ }
frontend/js/history.js ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ─────────────────────────────────────────
2
+ js/history.js β€” Invoice history page
3
+ ───────────────────────────────────────── */
4
+
5
+ var History = (function() {
6
+
7
+ var _allInvoices = []; // cache of loaded invoices
8
+
9
+ /* Format PKR */
10
+ function _fmt(n) { return fmt(n); }
11
+
12
+ /* Build HTML for a single history card */
13
+ function _buildCard(inv) {
14
+ var itemCount = (inv.rows || []).filter(function(r) { return r.desc; }).length;
15
+ return [
16
+ '<div class="hist-item">',
17
+ '<div class="hist-inv-no">' + (inv.invNumber || "β€”") + '</div>',
18
+ '<div>',
19
+ '<div class="hist-client-name">' + (inv.doctor || "Unknown Doctor") + '</div>',
20
+ '<div class="hist-date">πŸ“… ' + (inv.invDate || "β€”") + ' &nbsp;Β·&nbsp; ' + (inv.clinic || "β€”") + '</div>',
21
+ '</div>',
22
+ '<div>',
23
+ '<div class="hist-detail">Patient: <span>' + (inv.patient || "β€”") + '</span></div>',
24
+ '<div class="hist-detail">Items: <span>' + itemCount + '</span> &nbsp;Β·&nbsp; Status: <span>' + (inv.status || "β€”") + '</span></div>',
25
+ '</div>',
26
+ '<div>',
27
+ '<div class="hist-amount">' + _fmt(inv.subtotal || 0) + '</div>',
28
+ '<div style="font-size:0.75rem;color:var(--green);text-align:right;margin-top:2px">Rcvd: ' + _fmt(inv.received || 0) + '</div>',
29
+ '</div>',
30
+ '<div class="hist-actions">',
31
+ '<button class="btn-xs btn-xs-blue" onclick="App.loadEdit(' + inv.id + ')">✏️ Load</button>',
32
+ '<button class="btn-xs btn-xs-red" onclick="History.deleteInvoice(' + inv.id + ')">πŸ—‘οΈ</button>',
33
+ '</div>',
34
+ '</div>',
35
+ ].join("");
36
+ }
37
+
38
+ /* Render the list β€” optionally filtered by a search term */
39
+ function _render(list) {
40
+ var container = document.getElementById("hist-list");
41
+ var count = document.getElementById("history-count");
42
+
43
+ count.textContent = list.length + " invoice" + (list.length !== 1 ? "s" : "");
44
+
45
+ if (list.length === 0) {
46
+ var q = document.getElementById("search-input").value;
47
+ container.innerHTML = [
48
+ '<div class="empty-state">',
49
+ '<div class="ico">πŸ“‹</div>',
50
+ '<h3>' + (q ? "No matching invoices" : "No invoices yet") + '</h3>',
51
+ '<p>' + (q ? "Try a different search term." : "Create your first invoice to see it here.") + '</p>',
52
+ '</div>',
53
+ ].join("");
54
+ } else {
55
+ container.innerHTML = list.map(_buildCard).join("");
56
+ }
57
+ }
58
+
59
+ /* Load all invoices from DB and render */
60
+ function load() {
61
+ dbAll().then(function(all) {
62
+ _allInvoices = all.reverse(); // newest first
63
+ filter();
64
+ });
65
+ }
66
+
67
+ /* Filter the cached list by search term and re-render */
68
+ function filter() {
69
+ var q = (document.getElementById("search-input").value || "").toLowerCase();
70
+ var filtered = _allInvoices.filter(function(inv) {
71
+ if (!q) return true;
72
+ return (
73
+ (inv.invNumber || "").toLowerCase().includes(q) ||
74
+ (inv.doctor || "").toLowerCase().includes(q) ||
75
+ (inv.clinic || "").toLowerCase().includes(q) ||
76
+ (inv.patient || "").toLowerCase().includes(q) ||
77
+ (inv.status || "").toLowerCase().includes(q)
78
+ );
79
+ });
80
+ _render(filtered);
81
+ }
82
+
83
+ /* Delete an invoice after confirmation */
84
+ function deleteInvoice(id) {
85
+ if (!confirm("Delete this invoice permanently?")) return;
86
+ dbDelete(id).then(function() {
87
+ showToast("πŸ—‘οΈ Invoice deleted", "error");
88
+ load(); // refresh the list
89
+ });
90
+ }
91
+
92
+ /* Get a single invoice by id from cache */
93
+ function getById(id) {
94
+ return _allInvoices.find(function(inv) { return inv.id === id; }) || null;
95
+ }
96
+
97
+ /* Public API */
98
+ return { load: load, filter: filter, deleteInvoice: deleteInvoice, getById: getById };
99
+
100
+ })();
frontend/js/rows.js ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ─────────────────────────────────────────
2
+ js/rows.js β€” Service row management
3
+ ───────────────────────────────────────── */
4
+
5
+ var Rows = (function() {
6
+
7
+ var _rowCounter = 0; // unique key for each row
8
+
9
+ /* Build the <select> options HTML for the services list */
10
+ function _serviceOptions(selected) {
11
+ var html = '<option value="">β€” Select Service β€”</option>';
12
+ SERVICES.forEach(function(s) {
13
+ html += '<option' + (s === selected ? ' selected' : '') + '>' + s + '</option>';
14
+ });
15
+ return html;
16
+ }
17
+
18
+ /* Create one table row of HTML */
19
+ function _buildRow(id, desc, qty, price, total) {
20
+ desc = desc || "";
21
+ qty = qty !== undefined ? qty : 1;
22
+ price = price !== undefined ? price : "";
23
+ total = total || 0;
24
+
25
+ return [
26
+ '<tr id="row-' + id + '">',
27
+ '<td class="sno row-index"></td>',
28
+ '<td>',
29
+ '<select class="tbl-select" onchange="Rows.update(' + id + ')">' + _serviceOptions(desc) + '</select>',
30
+ '</td>',
31
+ '<td>',
32
+ '<input type="number" class="tbl-input" min="1" value="' + qty + '"',
33
+ ' style="text-align:center" oninput="Rows.update(' + id + ')"/>',
34
+ '</td>',
35
+ '<td>',
36
+ '<input type="number" class="tbl-input right" min="0" value="' + price + '" placeholder="0"',
37
+ ' oninput="Rows.update(' + id + ')"/>',
38
+ '</td>',
39
+ '<td class="total-cell row-total">' + fmtNum(total) + '</td>',
40
+ '<td class="action-cell">',
41
+ '<button class="btn-del" onclick="Rows.remove(' + id + ')">Γ—</button>',
42
+ '</td>',
43
+ '</tr>',
44
+ ].join("");
45
+ }
46
+
47
+ /* Renumber the # column sequentially */
48
+ function _renumber() {
49
+ var cells = document.querySelectorAll("#rows-body .row-index");
50
+ cells.forEach(function(cell, i) { cell.textContent = i + 1; });
51
+ }
52
+
53
+ /* Recalculate a single row's total and update the summary */
54
+ function update(id) {
55
+ var row = document.getElementById("row-" + id);
56
+ if (!row) return;
57
+
58
+ var inputs = row.querySelectorAll("input[type=number]");
59
+ var qty = parseFloat(inputs[0].value) || 0;
60
+ var price = parseFloat(inputs[1].value) || 0;
61
+ var total = qty * price;
62
+
63
+ row.querySelector(".row-total").textContent = fmtNum(total);
64
+ _recalcSummary();
65
+ }
66
+
67
+ /* Recalculate subtotal / remaining and refresh the summary panel */
68
+ function _recalcSummary() {
69
+ var subtotal = 0;
70
+ document.querySelectorAll("#rows-body tr").forEach(function(row) {
71
+ var inputs = row.querySelectorAll("input[type=number]");
72
+ var qty = parseFloat(inputs[0] ? inputs[0].value : 0) || 0;
73
+ var price = parseFloat(inputs[1] ? inputs[1].value : 0) || 0;
74
+ subtotal += qty * price;
75
+ });
76
+
77
+ var received = parseFloat(document.getElementById("received-input").value) || 0;
78
+ var remaining = subtotal - received;
79
+
80
+ document.getElementById("summary-subtotal").textContent = fmt(subtotal);
81
+ document.getElementById("summary-total").textContent = fmt(subtotal);
82
+ document.getElementById("summary-remaining").textContent = fmt(remaining);
83
+ document.getElementById("summary-remaining").style.color =
84
+ remaining > 0 ? "var(--red)" : "var(--green)";
85
+ }
86
+
87
+ /* Add a blank row */
88
+ function add() {
89
+ var id = ++_rowCounter;
90
+ var tbody = document.getElementById("rows-body");
91
+ tbody.insertAdjacentHTML("beforeend", _buildRow(id));
92
+ _renumber();
93
+ }
94
+
95
+ /* Remove a row by id */
96
+ function remove(id) {
97
+ var row = document.getElementById("row-" + id);
98
+ if (row) { row.remove(); _renumber(); _recalcSummary(); }
99
+ }
100
+
101
+ /* Clear all rows and add two blank ones */
102
+ function reset() {
103
+ document.getElementById("rows-body").innerHTML = "";
104
+ add();
105
+ add();
106
+ }
107
+
108
+ /* Load rows from saved data (array of row objects) */
109
+ function load(savedRows) {
110
+ document.getElementById("rows-body").innerHTML = "";
111
+ (savedRows || []).forEach(function(r) {
112
+ var id = ++_rowCounter;
113
+ document.getElementById("rows-body")
114
+ .insertAdjacentHTML("beforeend", _buildRow(id, r.desc, r.qty, r.price, r.total));
115
+ });
116
+ _renumber();
117
+ _recalcSummary();
118
+ }
119
+
120
+ /* Collect current row data into an array of plain objects */
121
+ function collect() {
122
+ var result = [];
123
+ document.querySelectorAll("#rows-body tr").forEach(function(row) {
124
+ var sel = row.querySelector("select");
125
+ var inputs = row.querySelectorAll("input[type=number]");
126
+ var qty = parseFloat(inputs[0] ? inputs[0].value : 1) || 1;
127
+ var price = parseFloat(inputs[1] ? inputs[1].value : 0) || 0;
128
+ result.push({
129
+ desc: sel ? sel.value : "",
130
+ qty: qty,
131
+ price: price,
132
+ total: qty * price,
133
+ });
134
+ });
135
+ return result;
136
+ }
137
+
138
+ /* Get the current subtotal */
139
+ function subtotal() {
140
+ return collect().reduce(function(sum, r) { return sum + r.total; }, 0);
141
+ }
142
+
143
+ /* Wire up the received-input to trigger recalc */
144
+ document.addEventListener("DOMContentLoaded", function() {
145
+ document.getElementById("received-input").addEventListener("input", _recalcSummary);
146
+ });
147
+
148
+ /* Public API */
149
+ return { add: add, remove: remove, reset: reset, load: load, collect: collect, subtotal: subtotal, update: update };
150
+
151
+ })();
frontend/js/ui.js ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ─────────────────────────────────────────
2
+ js/ui.js β€” UI helpers & shared utils
3
+ ───────────────────────────────────────── */
4
+
5
+ /* Show a toast notification
6
+ type: "success" | "error" | "info" */
7
+ function showToast(msg, type) {
8
+ type = type || "success";
9
+ var el = document.getElementById("toast");
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 (no currency prefix) */
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 the two main pages and set the active tab */
32
+ function switchPage(page) {
33
+ document.getElementById("page-invoice").style.display = (page === "invoice") ? "" : "none";
34
+ document.getElementById("page-history").style.display = (page === "history") ? "" : "none";
35
+
36
+ document.getElementById("tab-invoice").classList.toggle("active", page === "invoice");
37
+ document.getElementById("tab-history").classList.toggle("active", page === "history");
38
+ }
39
+
40
+ /* Update the live invoice number & status badge in the header band */
41
+ function updateHeaderBadge() {
42
+ var num = document.getElementById("inv-number").value || "INV-0001";
43
+ var status = document.getElementById("inv-status").value || "Pending";
44
+
45
+ document.getElementById("display-inv-number").textContent = "#" + num;
46
+
47
+ var badge = document.getElementById("display-status");
48
+ badge.textContent = status;
49
+ badge.className = "status-badge status-" + status.toLowerCase();
50
+ }
51
+
52
+ /* Wire up live badge updates once DOM is ready */
53
+ document.addEventListener("DOMContentLoaded", function() {
54
+ document.getElementById("inv-number").addEventListener("input", updateHeaderBadge);
55
+ document.getElementById("inv-status").addEventListener("change", updateHeaderBadge);
56
+ });
main.py CHANGED
@@ -9,8 +9,8 @@ from api.invoices import router as invoice_router
9
  Base.metadata.create_all(bind=engine)
10
 
11
  app = FastAPI(title="SmiloCAD Invoice API")
12
- app.mount("/css", StaticFiles(directory="../frontend/css"), name="css")
13
- app.mount("/js", StaticFiles(directory="../frontend/js"), name="js")
14
 
15
  app.add_middleware(
16
  CORSMiddleware,
@@ -22,8 +22,11 @@ app.add_middleware(
22
  # 1. Route to serve the index.html
23
  @app.get("/")
24
  async def serve_index():
25
- return FileResponse("../frontend/index.html")
26
- # 2. Mount the CSS and JS folders so the HTML can find them
 
 
 
27
  # This matches your folder names: frontend/css and frontend/js
28
 
29
  # Include the routes from the api folder
 
9
  Base.metadata.create_all(bind=engine)
10
 
11
  app = FastAPI(title="SmiloCAD Invoice API")
12
+ app.mount("/css", StaticFiles(directory="frontend/css"), name="css")
13
+ app.mount("/js", StaticFiles(directory="frontend/js"), name="js")
14
 
15
  app.add_middleware(
16
  CORSMiddleware,
 
22
  # 1. Route to serve the index.html
23
  @app.get("/")
24
  async def serve_index():
25
+ # Return index.html from the new local frontend folder
26
+ return FileResponse("frontend/index.html")
27
+
28
+
29
+ # 2. Mount the CSS and JS folders so the HTML can find them
30
  # This matches your folder names: frontend/css and frontend/js
31
 
32
  # Include the routes from the api folder