stat2025 commited on
Commit
78be24b
·
verified ·
1 Parent(s): deaed9c

Create app.js

Browse files
Files changed (1) hide show
  1. app.js +364 -0
app.js ADDED
@@ -0,0 +1,364 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* =========
2
+ Storage
3
+ ========= */
4
+ const STORAGE_KEY = "eq_receipts_v1";
5
+
6
+ function loadRows() {
7
+ try {
8
+ const raw = localStorage.getItem(STORAGE_KEY);
9
+ return raw ? JSON.parse(raw) : [];
10
+ } catch {
11
+ return [];
12
+ }
13
+ }
14
+
15
+ function saveRows(rows) {
16
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(rows));
17
+ }
18
+
19
+ /* =========
20
+ Helpers
21
+ ========= */
22
+ function uid() {
23
+ return Math.random().toString(16).slice(2) + Date.now().toString(16);
24
+ }
25
+
26
+ function nowLabel() {
27
+ const d = new Date();
28
+ return d.toLocaleString("ar-SA");
29
+ }
30
+
31
+ function setDefaultDate() {
32
+ const d = new Date();
33
+ const yyyy = d.getFullYear();
34
+ const mm = String(d.getMonth() + 1).padStart(2, "0");
35
+ const dd = String(d.getDate()).padStart(2, "0");
36
+ document.getElementById("date").value = `${yyyy}-${mm}-${dd}`;
37
+ }
38
+
39
+ function sanitizePhone(v) {
40
+ return (v || "").replace(/[^\d+]/g, "").trim();
41
+ }
42
+
43
+ /* =========
44
+ Signature Canvas (draw)
45
+ ========= */
46
+ const canvas = document.getElementById("sigCanvas");
47
+ const ctx = canvas.getContext("2d");
48
+
49
+ let drawing = false;
50
+ let last = { x: 0, y: 0 };
51
+
52
+ function getCanvasPoint(e) {
53
+ const rect = canvas.getBoundingClientRect();
54
+ const clientX = e.touches ? e.touches[0].clientX : e.clientX;
55
+ const clientY = e.touches ? e.touches[0].clientY : e.clientY;
56
+ return {
57
+ x: (clientX - rect.left) * (canvas.width / rect.width),
58
+ y: (clientY - rect.top) * (canvas.height / rect.height),
59
+ };
60
+ }
61
+
62
+ function startDraw(e) {
63
+ drawing = true;
64
+ last = getCanvasPoint(e);
65
+ e.preventDefault();
66
+ }
67
+
68
+ function moveDraw(e) {
69
+ if (!drawing) return;
70
+ const p = getCanvasPoint(e);
71
+
72
+ ctx.lineWidth = 3;
73
+ ctx.lineCap = "round";
74
+ ctx.lineJoin = "round";
75
+ ctx.strokeStyle = "#0f172a";
76
+
77
+ ctx.beginPath();
78
+ ctx.moveTo(last.x, last.y);
79
+ ctx.lineTo(p.x, p.y);
80
+ ctx.stroke();
81
+
82
+ last = p;
83
+ e.preventDefault();
84
+ }
85
+
86
+ function endDraw() {
87
+ drawing = false;
88
+ }
89
+
90
+ function clearSignature() {
91
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
92
+ }
93
+
94
+ function signatureDataURL() {
95
+ // if empty => return ""
96
+ const blank = document.createElement("canvas");
97
+ blank.width = canvas.width;
98
+ blank.height = canvas.height;
99
+ if (canvas.toDataURL() === blank.toDataURL()) return "";
100
+ return canvas.toDataURL("image/png");
101
+ }
102
+
103
+ canvas.addEventListener("mousedown", startDraw);
104
+ canvas.addEventListener("mousemove", moveDraw);
105
+ window.addEventListener("mouseup", endDraw);
106
+
107
+ canvas.addEventListener("touchstart", startDraw, { passive: false });
108
+ canvas.addEventListener("touchmove", moveDraw, { passive: false });
109
+ canvas.addEventListener("touchend", endDraw);
110
+
111
+ document.getElementById("clearSigBtn").addEventListener("click", clearSignature);
112
+
113
+ /* =========
114
+ UI Elements
115
+ ========= */
116
+ const form = document.getElementById("entryForm");
117
+ const editIdEl = document.getElementById("editId");
118
+ const rowsCountEl = document.getElementById("rowsCount");
119
+ const lastSavedEl = document.getElementById("lastSaved");
120
+ const tableBody = document.getElementById("tableBody");
121
+ const searchEl = document.getElementById("search");
122
+
123
+ const inputs = {
124
+ companyName: document.getElementById("companyName"),
125
+ crNumber: document.getElementById("crNumber"),
126
+ receiverName: document.getElementById("receiverName"),
127
+ mobile: document.getElementById("mobile"),
128
+ email: document.getElementById("email"),
129
+ date: document.getElementById("date"),
130
+ signatureText: document.getElementById("signatureText"),
131
+ };
132
+
133
+ let rows = loadRows();
134
+
135
+ /* =========
136
+ Render
137
+ ========= */
138
+ function matchesSearch(row, q) {
139
+ if (!q) return true;
140
+ const hay = [
141
+ row.companyName,
142
+ row.crNumber,
143
+ row.receiverName,
144
+ row.mobile,
145
+ row.email,
146
+ row.date,
147
+ ].join(" ").toLowerCase();
148
+ return hay.includes(q.toLowerCase());
149
+ }
150
+
151
+ function render() {
152
+ const q = (searchEl.value || "").trim();
153
+ const filtered = rows.filter(r => matchesSearch(r, q));
154
+
155
+ tableBody.innerHTML = "";
156
+
157
+ filtered.forEach((r, idx) => {
158
+ const tr = document.createElement("tr");
159
+
160
+ const sigCell = (() => {
161
+ if (r.signatureText) return escapeHtml(r.signatureText);
162
+ if (r.signaturePng) return `<img src="${r.signaturePng}" alt="توقيع" style="height:36px; max-width:120px; object-fit:contain;"/>`;
163
+ return `<span style="color:#94a3b8">—</span>`;
164
+ })();
165
+
166
+ tr.innerHTML = `
167
+ <td class="col-n">${idx + 1}</td>
168
+ <td>${escapeHtml(r.companyName)}</td>
169
+ <td>${escapeHtml(r.crNumber)}</td>
170
+ <td>${escapeHtml(r.receiverName)}</td>
171
+ <td>${escapeHtml(r.mobile)}</td>
172
+ <td>${escapeHtml(r.email || "")}</td>
173
+ <td>${escapeHtml(r.date)}</td>
174
+ <td>${sigCell}</td>
175
+ <td class="col-actions">
176
+ <div class="row-actions">
177
+ <button class="btn btn-ghost" data-act="edit" data-id="${r.id}">تعديل</button>
178
+ <button class="btn btn-danger" data-act="delete" data-id="${r.id}">حذف</button>
179
+ </div>
180
+ </td>
181
+ `;
182
+ tableBody.appendChild(tr);
183
+ });
184
+
185
+ rowsCountEl.textContent = rows.length.toString();
186
+ }
187
+
188
+ function escapeHtml(str) {
189
+ return (str || "")
190
+ .replaceAll("&", "&amp;")
191
+ .replaceAll("<", "&lt;")
192
+ .replaceAll(">", "&gt;")
193
+ .replaceAll('"', "&quot;")
194
+ .replaceAll("'", "&#039;");
195
+ }
196
+
197
+ /* =========
198
+ Form Actions
199
+ ========= */
200
+ function resetForm() {
201
+ editIdEl.value = "";
202
+ inputs.companyName.value = "";
203
+ inputs.crNumber.value = "";
204
+ inputs.receiverName.value = "";
205
+ inputs.mobile.value = "";
206
+ inputs.email.value = "";
207
+ setDefaultDate();
208
+ inputs.signatureText.value = "";
209
+ clearSignature();
210
+ document.getElementById("saveBtn").textContent = "حفظ";
211
+ }
212
+
213
+ document.getElementById("resetBtn").addEventListener("click", resetForm);
214
+
215
+ form.addEventListener("submit", (e) => {
216
+ e.preventDefault();
217
+
218
+ const payload = {
219
+ companyName: inputs.companyName.value.trim(),
220
+ crNumber: inputs.crNumber.value.trim(),
221
+ receiverName: inputs.receiverName.value.trim(),
222
+ mobile: sanitizePhone(inputs.mobile.value),
223
+ email: inputs.email.value.trim(),
224
+ date: inputs.date.value,
225
+ signatureText: inputs.signatureText.value.trim(),
226
+ signaturePng: "",
227
+ };
228
+
229
+ // if no signatureText, use canvas signature if any
230
+ if (!payload.signatureText) {
231
+ payload.signaturePng = signatureDataURL();
232
+ }
233
+
234
+ // Basic validation
235
+ if (!payload.companyName || !payload.crNumber || !payload.receiverName || !payload.mobile || !payload.date) {
236
+ alert("فضلاً أكمل الحقول المطلوبة.");
237
+ return;
238
+ }
239
+
240
+ const editingId = editIdEl.value;
241
+ if (editingId) {
242
+ const i = rows.findIndex(r => r.id === editingId);
243
+ if (i >= 0) {
244
+ rows[i] = { ...rows[i], ...payload };
245
+ }
246
+ } else {
247
+ rows.unshift({ id: uid(), createdAt: Date.now(), ...payload });
248
+ }
249
+
250
+ saveRows(rows);
251
+ lastSavedEl.textContent = nowLabel();
252
+ render();
253
+ resetForm();
254
+ });
255
+
256
+ /* =========
257
+ Row Buttons
258
+ ========= */
259
+ tableBody.addEventListener("click", (e) => {
260
+ const btn = e.target.closest("button");
261
+ if (!btn) return;
262
+
263
+ const act = btn.getAttribute("data-act");
264
+ const id = btn.getAttribute("data-id");
265
+
266
+ if (act === "delete") {
267
+ const ok = confirm("هل تريد حذف هذا السجل؟");
268
+ if (!ok) return;
269
+ rows = rows.filter(r => r.id !== id);
270
+ saveRows(rows);
271
+ lastSavedEl.textContent = nowLabel();
272
+ render();
273
+ }
274
+
275
+ if (act === "edit") {
276
+ const r = rows.find(x => x.id === id);
277
+ if (!r) return;
278
+
279
+ editIdEl.value = r.id;
280
+ inputs.companyName.value = r.companyName || "";
281
+ inputs.crNumber.value = r.crNumber || "";
282
+ inputs.receiverName.value = r.receiverName || "";
283
+ inputs.mobile.value = r.mobile || "";
284
+ inputs.email.value = r.email || "";
285
+ inputs.date.value = r.date || "";
286
+ inputs.signatureText.value = r.signatureText || "";
287
+
288
+ clearSignature();
289
+ if (r.signaturePng && !r.signatureText) {
290
+ const img = new Image();
291
+ img.onload = () => ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
292
+ img.src = r.signaturePng;
293
+ }
294
+
295
+ document.getElementById("saveBtn").textContent = "تحديث";
296
+ window.scrollTo({ top: 0, behavior: "smooth" });
297
+ }
298
+ });
299
+
300
+ /* =========
301
+ Search
302
+ ========= */
303
+ searchEl.addEventListener("input", render);
304
+
305
+ /* =========
306
+ Delete All
307
+ ========= */
308
+ document.getElementById("deleteAllBtn").addEventListener("click", () => {
309
+ if (!rows.length) return alert("لا توجد سجلات للحذف.");
310
+ const ok = confirm("سيتم حذف جميع السجلات نهائيًا من هذا الجهاز. هل أنت متأكد؟");
311
+ if (!ok) return;
312
+ rows = [];
313
+ saveRows(rows);
314
+ lastSavedEl.textContent = nowLabel();
315
+ render();
316
+ resetForm();
317
+ });
318
+
319
+ /* =========
320
+ Export Excel
321
+ ========= */
322
+ document.getElementById("exportBtn").addEventListener("click", () => {
323
+ if (!rows.length) return alert("لا توجد بيانات لتصديرها.");
324
+
325
+ // Prepare Excel rows (match your statement columns)
326
+ const data = rows.slice().reverse().map((r, idx) => ({
327
+ "م": idx + 1,
328
+ "اسم الشركة": r.companyName,
329
+ "السجل التجاري": r.crNumber,
330
+ "اسم المستلم": r.receiverName,
331
+ "رقم الجوال": r.mobile,
332
+ "البريد الإلكتروني": r.email || "",
333
+ "التاريخ": r.date,
334
+ "التوقيع": r.signatureText ? r.signatureText : (r.signaturePng ? "توقيع مرسوم" : ""),
335
+ }));
336
+
337
+ const ws = XLSX.utils.json_to_sheet(data);
338
+ const wb = XLSX.utils.book_new();
339
+ XLSX.utils.book_append_sheet(wb, ws, "بيان الاستلام");
340
+
341
+ // Auto column widths
342
+ const colWidths = [
343
+ { wch: 4 }, { wch: 28 }, { wch: 18 }, { wch: 18 },
344
+ { wch: 14 }, { wch: 24 }, { wch: 12 }, { wch: 14 }
345
+ ];
346
+ ws["!cols"] = colWidths;
347
+
348
+ const d = new Date();
349
+ const yyyy = d.getFullYear();
350
+ const mm = String(d.getMonth() + 1).padStart(2, "0");
351
+ const dd = String(d.getDate()).padStart(2, "0");
352
+ const filename = `بيان_استلام_${yyyy}-${mm}-${dd}.xlsx`;
353
+
354
+ XLSX.writeFile(wb, filename);
355
+ });
356
+
357
+ /* =========
358
+ Init
359
+ ========= */
360
+ (function init() {
361
+ setDefaultDate();
362
+ lastSavedEl.textContent = "—";
363
+ render();
364
+ })();