Spaces:
Sleeping
Sleeping
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.
|
|
|
|
| 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 [
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
{
|
| 413 |
<tr>
|
| 414 |
-
<td colSpan={
|
| 415 |
No recent reports yet.
|
| 416 |
</td>
|
| 417 |
</tr>
|
| 418 |
) : (
|
| 419 |
-
|
| 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 |
-
<
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =
|
| 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, [])
|