ChristopherJKoen commited on
Commit
dd94ad9
·
1 Parent(s): 8cdcbe1

Update template sizing/box wrapping fixes

Browse files
frontend/src/lib/version.ts CHANGED
@@ -1 +1 @@
1
- export const APP_VERSION = "V0.1.5";
 
1
+ export const APP_VERSION = "V0.1.6";
frontend/src/pages/UploadPage.tsx CHANGED
@@ -9,12 +9,15 @@ import {
9
  FileText,
10
  FilePlus,
11
  Edit,
 
 
 
12
  } from "react-feather";
13
 
14
  import { postForm, request } from "../lib/api";
15
  import { formatBytes } from "../lib/format";
16
  import { formatDocNumber } from "../lib/report";
17
- import { setStoredSessionId } from "../lib/session";
18
  import { APP_VERSION } from "../lib/version";
19
  import { LoadingBar } from "../components/LoadingBar";
20
  import type { Session } from "../types/session";
@@ -29,7 +32,11 @@ export default function UploadPage() {
29
  const [inspectionDate, setInspectionDate] = useState("");
30
  const [uploadStatus, setUploadStatus] = useState("");
31
  const [statusTone, setStatusTone] = useState<StatusTone>("idle");
32
- const [recentSessions, setRecentSessions] = useState<Session[]>([]);
 
 
 
 
33
  const [dragActive, setDragActive] = useState(false);
34
  const [isSubmitting, setIsSubmitting] = useState(false);
35
  const [uploadProgress, setUploadProgress] = useState<number | null>(null);
@@ -68,7 +75,7 @@ export default function UploadPage() {
68
  try {
69
  const sessions = await request<Session[]>("/sessions");
70
  if (Array.isArray(sessions)) {
71
- setRecentSessions(sessions.slice(0, 5));
72
  }
73
  } catch {
74
  // ignore for now
@@ -139,6 +146,44 @@ export default function UploadPage() {
139
  }
140
  }
141
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  return (
143
  <main className="max-w-4xl mx-auto my-8 bg-white shadow-sm ring-1 ring-gray-200 rounded-xl p-6 md:p-8">
144
  <header className="mb-10 border-b border-gray-200 pb-4">
@@ -386,6 +431,37 @@ export default function UploadPage() {
386
  </h2>
387
 
388
  <div className="rounded-lg border border-gray-200 bg-white overflow-hidden">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
  <div className="overflow-x-auto">
390
  <table className="min-w-full divide-y divide-gray-200">
391
  <thead className="bg-gray-50">
@@ -402,6 +478,9 @@ export default function UploadPage() {
402
  <th className="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">
403
  Status
404
  </th>
 
 
 
405
  <th className="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">
406
  Actions
407
  </th>
@@ -409,14 +488,14 @@ export default function UploadPage() {
409
  </thead>
410
 
411
  <tbody className="bg-white divide-y divide-gray-200">
412
- {recentSessions.length === 0 ? (
413
  <tr>
414
- <td colSpan={5} className="px-6 py-6 text-sm text-gray-500 text-center">
415
  No recent reports yet.
416
  </td>
417
  </tr>
418
  ) : (
419
- recentSessions.map((session) => (
420
  <tr key={session.id}>
421
  <td className="px-6 py-4 whitespace-nowrap text-sm font-semibold text-gray-900">
422
  #{session.id.slice(0, 8)}
@@ -434,17 +513,33 @@ export default function UploadPage() {
434
  {session.status || "ready"}
435
  </span>
436
  </td>
 
 
 
437
  <td className="px-6 py-4 whitespace-nowrap text-sm">
438
- <button
439
- type="button"
440
- onClick={() =>
441
- navigate(`/report-viewer?session=${session.id}`)
442
- }
443
- className="inline-flex items-center text-blue-700 hover:text-blue-800"
444
- aria-label="View report"
445
- >
446
- <Edit className="h-4 w-4" />
447
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
448
  </td>
449
  </tr>
450
  ))
 
9
  FileText,
10
  FilePlus,
11
  Edit,
12
+ Trash2,
13
+ ChevronDown,
14
+ ChevronUp,
15
  } from "react-feather";
16
 
17
  import { postForm, request } from "../lib/api";
18
  import { formatBytes } from "../lib/format";
19
  import { formatDocNumber } from "../lib/report";
20
+ import { clearStoredSessionId, setStoredSessionId } from "../lib/session";
21
  import { APP_VERSION } from "../lib/version";
22
  import { LoadingBar } from "../components/LoadingBar";
23
  import type { Session } from "../types/session";
 
32
  const [inspectionDate, setInspectionDate] = useState("");
33
  const [uploadStatus, setUploadStatus] = useState("");
34
  const [statusTone, setStatusTone] = useState<StatusTone>("idle");
35
+ const [allSessions, setAllSessions] = useState<Session[]>([]);
36
+ const [showAllReports, setShowAllReports] = useState(false);
37
+ const [deletingSessionId, setDeletingSessionId] = useState<string | null>(
38
+ null,
39
+ );
40
  const [dragActive, setDragActive] = useState(false);
41
  const [isSubmitting, setIsSubmitting] = useState(false);
42
  const [uploadProgress, setUploadProgress] = useState<number | null>(null);
 
75
  try {
76
  const sessions = await request<Session[]>("/sessions");
77
  if (Array.isArray(sessions)) {
78
+ setAllSessions(sessions);
79
  }
80
  } catch {
81
  // ignore for now
 
146
  }
147
  }
148
 
149
+ async function handleDeleteSession(sessionId: string) {
150
+ const target = allSessions.find((session) => session.id === sessionId);
151
+ const targetLabel = target?.document_no || formatDocNumber(target || { id: sessionId } as Session);
152
+ const confirmDelete = window.confirm(
153
+ `Delete this report permanently?\n\n${targetLabel}\n#${sessionId.slice(0, 8)}`,
154
+ );
155
+ if (!confirmDelete) return;
156
+
157
+ setDeletingSessionId(sessionId);
158
+ try {
159
+ await request<null>(`/sessions/${sessionId}`, { method: "DELETE" });
160
+ setAllSessions((prev) => prev.filter((session) => session.id !== sessionId));
161
+ if (showAllReports && allSessions.length - 1 <= 5) {
162
+ setShowAllReports(false);
163
+ }
164
+ clearStoredSessionId();
165
+ } catch (err) {
166
+ const message =
167
+ err instanceof Error ? err.message : "Failed to delete report.";
168
+ setUploadStatus(message);
169
+ setStatusTone("error");
170
+ } finally {
171
+ setDeletingSessionId(null);
172
+ }
173
+ }
174
+
175
+ const visibleSessions = useMemo(
176
+ () => (showAllReports ? allSessions : allSessions.slice(0, 5)),
177
+ [allSessions, showAllReports],
178
+ );
179
+
180
+ function fileSummaryForSession(session: Session): string {
181
+ const photos = session.uploads?.photos?.length ?? 0;
182
+ const docs = session.uploads?.documents?.length ?? 0;
183
+ const data = session.uploads?.data_files?.length ?? 0;
184
+ return `${photos} photos - ${docs} docs - ${data} data`;
185
+ }
186
+
187
  return (
188
  <main className="max-w-4xl mx-auto my-8 bg-white shadow-sm ring-1 ring-gray-200 rounded-xl p-6 md:p-8">
189
  <header className="mb-10 border-b border-gray-200 pb-4">
 
431
  </h2>
432
 
433
  <div className="rounded-lg border border-gray-200 bg-white overflow-hidden">
434
+ <div className="flex items-center justify-between gap-3 border-b border-gray-200 bg-gray-50 px-4 py-3">
435
+ <div className="text-sm text-gray-700">
436
+ Showing{" "}
437
+ <span className="font-semibold text-gray-900">
438
+ {visibleSessions.length}
439
+ </span>{" "}
440
+ of{" "}
441
+ <span className="font-semibold text-gray-900">
442
+ {allSessions.length}
443
+ </span>{" "}
444
+ report{allSessions.length === 1 ? "" : "s"}
445
+ </div>
446
+ <button
447
+ type="button"
448
+ onClick={() => setShowAllReports((prev) => !prev)}
449
+ disabled={allSessions.length <= 5}
450
+ className="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-semibold text-gray-800 hover:bg-gray-100 transition disabled:opacity-50 disabled:cursor-not-allowed"
451
+ >
452
+ {showAllReports ? (
453
+ <>
454
+ <ChevronUp className="h-4 w-4" />
455
+ Collapse to recent
456
+ </>
457
+ ) : (
458
+ <>
459
+ <ChevronDown className="h-4 w-4" />
460
+ Expand all
461
+ </>
462
+ )}
463
+ </button>
464
+ </div>
465
  <div className="overflow-x-auto">
466
  <table className="min-w-full divide-y divide-gray-200">
467
  <thead className="bg-gray-50">
 
478
  <th className="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">
479
  Status
480
  </th>
481
+ <th className="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">
482
+ Files
483
+ </th>
484
  <th className="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">
485
  Actions
486
  </th>
 
488
  </thead>
489
 
490
  <tbody className="bg-white divide-y divide-gray-200">
491
+ {allSessions.length === 0 ? (
492
  <tr>
493
+ <td colSpan={6} className="px-6 py-6 text-sm text-gray-500 text-center">
494
  No recent reports yet.
495
  </td>
496
  </tr>
497
  ) : (
498
+ visibleSessions.map((session) => (
499
  <tr key={session.id}>
500
  <td className="px-6 py-4 whitespace-nowrap text-sm font-semibold text-gray-900">
501
  #{session.id.slice(0, 8)}
 
513
  {session.status || "ready"}
514
  </span>
515
  </td>
516
+ <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
517
+ {fileSummaryForSession(session)}
518
+ </td>
519
  <td className="px-6 py-4 whitespace-nowrap text-sm">
520
+ <div className="inline-flex items-center gap-3">
521
+ <button
522
+ type="button"
523
+ onClick={() =>
524
+ navigate(`/report-viewer?session=${session.id}`)
525
+ }
526
+ className="inline-flex items-center text-blue-700 hover:text-blue-800"
527
+ aria-label="View report"
528
+ title="Open report"
529
+ >
530
+ <Edit className="h-4 w-4" />
531
+ </button>
532
+ <button
533
+ type="button"
534
+ onClick={() => handleDeleteSession(session.id)}
535
+ disabled={deletingSessionId === session.id}
536
+ className="inline-flex items-center text-red-700 hover:text-red-800 disabled:opacity-50"
537
+ aria-label="Delete report"
538
+ title="Delete report"
539
+ >
540
+ <Trash2 className="h-4 w-4" />
541
+ </button>
542
+ </div>
543
  </td>
544
  </tr>
545
  ))
server/app/api/routes/sessions.py CHANGED
@@ -78,6 +78,16 @@ def list_sessions(store: SessionStore = Depends(get_session_store)) -> List[Sess
78
  return [_attach_urls(session) for session in sessions]
79
 
80
 
 
 
 
 
 
 
 
 
 
 
81
  @router.post("", response_model=SessionResponse, status_code=status.HTTP_201_CREATED)
82
  def create_session(
83
  document_no: str = Form(""),
 
78
  return [_attach_urls(session) for session in sessions]
79
 
80
 
81
+ @router.delete("/{session_id}", status_code=status.HTTP_204_NO_CONTENT)
82
+ def delete_session(
83
+ session_id: str, store: SessionStore = Depends(get_session_store)
84
+ ) -> None:
85
+ session_id = _normalize_session_id(session_id, store)
86
+ deleted = store.delete_session(session_id)
87
+ if not deleted:
88
+ raise HTTPException(status_code=404, detail="Session not found.")
89
+
90
+
91
  @router.post("", response_model=SessionResponse, status_code=status.HTTP_201_CREATED)
92
  def create_session(
93
  document_no: str = Form(""),
server/app/services/pdf_reportlab.py CHANGED
@@ -910,7 +910,7 @@ def render_report_pdf(
910
  pdf.setFillColor(gray_50)
911
  pdf.roundRect(x, y, cell_w, cell_h, 3 * mm, stroke=1, fill=1)
912
 
913
- caption_text = f"Fig {idx + 1}: {label}" if label else ""
914
  caption_font = "Helvetica"
915
  caption_size = 9
916
  caption_leading = 10
 
910
  pdf.setFillColor(gray_50)
911
  pdf.roundRect(x, y, cell_w, cell_h, 3 * mm, stroke=1, fill=1)
912
 
913
+ caption_text = label or ""
914
  caption_font = "Helvetica"
915
  caption_size = 9
916
  caption_leading = 10
server/app/services/session_store.py CHANGED
@@ -3,6 +3,7 @@ from __future__ import annotations
3
  import copy
4
  import json
5
  import re
 
6
  from dataclasses import dataclass
7
  from datetime import datetime, timezone
8
  from pathlib import Path
@@ -342,6 +343,14 @@ class SessionStore:
342
  session["updated_at"] = _now_iso()
343
  self._save_session(session)
344
 
 
 
 
 
 
 
 
 
345
  def add_uploads(self, session: dict, uploads: Iterable[StoredFile]) -> dict:
346
  for item in uploads:
347
  session["uploads"].setdefault(item.category, [])
 
3
  import copy
4
  import json
5
  import re
6
+ import shutil
7
  from dataclasses import dataclass
8
  from datetime import datetime, timezone
9
  from pathlib import Path
 
343
  session["updated_at"] = _now_iso()
344
  self._save_session(session)
345
 
346
+ def delete_session(self, session_id: str) -> bool:
347
+ session_dir = self._session_dir(session_id)
348
+ if not session_dir.exists():
349
+ return False
350
+ with self._lock:
351
+ shutil.rmtree(session_dir, ignore_errors=False)
352
+ return True
353
+
354
  def add_uploads(self, session: dict, uploads: Iterable[StoredFile]) -> dict:
355
  for item in uploads:
356
  session["uploads"].setdefault(item.category, [])