Skip to content

Commit 289d8f0

Browse files
committed
fix: page size
1 parent cecb6e1 commit 289d8f0

File tree

2 files changed

+175
-39
lines changed

2 files changed

+175
-39
lines changed

client/my-app/src/components/features/alerts/Details/Notes.tsx

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,19 @@ export default function Notes({ alrId }: { alrId: number }) {
330330
};
331331

332332
/* -------------------------------- Render -------------------------------- */
333+
const PAGE_SIZE = 10;
334+
const [page, setPage] = useState(1);
335+
336+
useEffect(() => {
337+
setPage(1);
338+
}, [sortedNotes.length]);
339+
340+
const total = sortedNotes.length;
341+
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
342+
const start = (page - 1) * PAGE_SIZE;
343+
const end = Math.min(start + PAGE_SIZE, total);
344+
const pagedNotes = sortedNotes.slice(start, end);
345+
333346
if (loading) return <div className="text-sm text-gray-500">Loading notes…</div>;
334347
if (err) return <div className="text-sm text-red-600">Error: {err}</div>;
335348

@@ -426,7 +439,7 @@ export default function Notes({ alrId }: { alrId: number }) {
426439
</TableCell>
427440
</TableRow>
428441
) : (
429-
sortedNotes.map((n) => {
442+
pagedNotes.map((n) => {
430443
const noteCode = `ANH${String(n.note_id).padStart(3, "0")}`;
431444
const userLabel = n.creator_username || n.creator_name || "-";
432445

@@ -516,6 +529,44 @@ export default function Notes({ alrId }: { alrId: number }) {
516529
cancelText="Cancel"
517530
onConfirm={() => !busyDelete && confirmDelete()}
518531
/>
532+
533+
{/* Pagination bar */}
534+
<div className="mt-3 flex items-center justify-between">
535+
<div className="text-xs text-gray-500">
536+
Showing <span className="font-medium">{total ? start + 1 : 0}</span>
537+
<span className="font-medium">{end}</span> of{" "}
538+
<span className="font-medium">{total}</span>
539+
</div>
540+
541+
<div className="flex items-center gap-2">
542+
<button
543+
onClick={() => setPage(p => Math.max(1, p - 1))}
544+
disabled={page <= 1}
545+
className={`px-3 py-1 rounded-md border text-sm ${page <= 1
546+
? "text-gray-400 border-gray-200"
547+
: "text-gray-700 border-gray-300 hover:bg-gray-50"
548+
}`}
549+
>
550+
Previous
551+
</button>
552+
553+
<div className="text-sm tabular-nums">
554+
{page} / {totalPages}
555+
</div>
556+
557+
<button
558+
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
559+
disabled={page >= totalPages}
560+
className={`px-3 py-1 rounded-md border text-sm ${page >= totalPages
561+
? "text-gray-400 border-gray-200"
562+
: "text-gray-700 border-gray-300 hover:bg-gray-50"
563+
}`}
564+
>
565+
Next
566+
</button>
567+
</div>
568+
</div>
569+
519570
</div>
520571
);
521572
}

client/my-app/src/components/features/cameras/Details/MaintenanceHistoryTable.tsx

Lines changed: 123 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -169,12 +169,12 @@ function AddMaintenanceModal({
169169
const body = { technician, type: toApiType(type), date, note };
170170
const res = await fetch(`/api/cameras/${camId}/maintenance`, {
171171
method: "POST",
172+
credentials: "include",
173+
cache: "no-store",
172174
headers: {
173-
Authorization: `Bearer ${process.env.NEXT_PUBLIC_TOKEN}`,
174175
"Content-Type": "application/json",
176+
Accept: "application/json",
175177
},
176-
cache: "no-store",
177-
credentials: "include",
178178
body: JSON.stringify(body),
179179
});
180180
const json = await res.json().catch(() => ({}));
@@ -325,12 +325,12 @@ function EditMaintenanceModal({
325325
const body = { technician, type: toApiType(type), date, note };
326326
const res = await fetch(`/api/cameras/maintenance/${row.id}`, {
327327
method: "PUT",
328+
credentials: "include",
329+
cache: "no-store",
328330
headers: {
329-
Authorization: `Bearer ${process.env.NEXT_PUBLIC_TOKEN}`,
330331
"Content-Type": "application/json",
332+
Accept: "application/json",
331333
},
332-
credentials: "include",
333-
cache: "no-store",
334334
body: JSON.stringify(body),
335335
});
336336
const json = await res.json().catch(() => ({}));
@@ -455,6 +455,10 @@ export default function CameraMaintenance({ camera }: { camera: Camera }) {
455455
const [deleteTarget, setDeleteTarget] = useState<Row | null>(null);
456456
const [busyDelete, setBusyDelete] = useState(false);
457457

458+
// 👉 Pagination state
459+
const [page, setPage] = useState(1);
460+
const PAGE_SIZE = 10;
461+
458462
useEffect(() => {
459463
if (!Number.isFinite(camId) || camId <= 0) {
460464
setErr("Invalid camera id.");
@@ -470,18 +474,14 @@ export default function CameraMaintenance({ camera }: { camera: Camera }) {
470474

471475
const res = await fetch(`/api/cameras/${camId}/maintenance`, {
472476
method: "GET",
473-
headers: {
474-
Authorization: `Bearer ${process.env.NEXT_PUBLIC_TOKEN}`,
475-
"Content-Type": "application/json",
476-
},
477-
cache: "no-store",
478477
credentials: "include",
478+
cache: "no-store",
479479
});
480480
const json = await res.json().catch(() => ({}));
481481
if (!res.ok) throw new Error(json?.message || `HTTP ${res.status}`);
482482

483483
const rows: ApiMaintenance[] = Array.isArray(json?.data) ? json.data : [];
484-
const mapped: Row[] = rows.map(r => ({
484+
const mapped: Row[] = rows.map((r) => ({
485485
id: r.maintenance_id,
486486
cameraId: r.camera_id,
487487
date: r.maintenance_date,
@@ -499,21 +499,27 @@ export default function CameraMaintenance({ camera }: { camera: Camera }) {
499499
}
500500
})();
501501

502-
return () => { mounted = false; };
502+
return () => {
503+
mounted = false;
504+
};
503505
}, [camId]);
504506

505507
const handleSort = (key: SortKey) => {
506508
if (sortKey === key) {
507-
setSortOrder(prev => (prev === "asc" ? "desc" : prev === "desc" ? null : "asc"));
509+
setSortOrder((prev) =>
510+
prev === "asc" ? "desc" : prev === "desc" ? null : "asc"
511+
);
508512
} else {
509513
setSortKey(key);
510514
setSortOrder("asc");
511515
}
512516
};
513517

514518
const renderSortIcon = (key: SortKey) => {
515-
if (sortKey !== key || !sortOrder) return <ArrowUpDown className="w-4 h-4 ml-1 inline-block" />;
516-
if (sortOrder === "asc") return <ArrowUp className="w-4 h-4 ml-1 inline-block" />;
519+
if (sortKey !== key || !sortOrder)
520+
return <ArrowUpDown className="w-4 h-4 ml-1 inline-block" />;
521+
if (sortOrder === "asc")
522+
return <ArrowUp className="w-4 h-4 ml-1 inline-block" />;
517523
return <ArrowDown className="w-4 h-4 ml-1 inline-block" />;
518524
};
519525

@@ -536,15 +542,29 @@ export default function CameraMaintenance({ camera }: { camera: Camera }) {
536542

537543
const aStr = String(aVal);
538544
const bStr = String(bVal);
539-
return sortOrder === "asc" ? aStr.localeCompare(bStr) : bStr.localeCompare(aStr);
545+
return sortOrder === "asc"
546+
? aStr.localeCompare(bStr)
547+
: bStr.localeCompare(aStr);
540548
});
541549
}, [records, sortKey, sortOrder]);
542550

551+
// เปลี่ยนหน้าให้กลับไปหน้า 1 เวลา sort เปลี่ยนหรือจำนวน record เปลี่ยน
552+
useEffect(() => {
553+
setPage(1);
554+
}, [sortKey, sortOrder, records.length]);
555+
556+
// 👉 Pagination calc
557+
const total = sortedRecords.length;
558+
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
559+
const start = (page - 1) * PAGE_SIZE;
560+
const end = Math.min(start + PAGE_SIZE, total);
561+
const pagedRecords = sortedRecords.slice(start, end);
562+
543563
function onAdded(row: Row) {
544-
setRecords(prev => [row, ...prev]);
564+
setRecords((prev) => [row, ...prev]);
545565
}
546566
function onUpdated(row: Row) {
547-
setRecords(prev => prev.map(r => (r.id === row.id ? row : r)));
567+
setRecords((prev) => prev.map((r) => (r.id === row.id ? row : r)));
548568
}
549569

550570
function askDelete(row: Row) {
@@ -558,17 +578,17 @@ export default function CameraMaintenance({ camera }: { camera: Camera }) {
558578
setBusyDelete(true);
559579
const res = await fetch(`/api/cameras/maintenance/${deleteTarget.id}`, {
560580
method: "PATCH",
581+
credentials: "include",
582+
cache: "no-store",
561583
headers: {
562-
Authorization: `Bearer ${process.env.NEXT_PUBLIC_TOKEN}`,
563584
"Content-Type": "application/json",
585+
Accept: "application/json",
564586
},
565-
credentials: "include",
566-
cache: "no-store",
567587
});
568588
const json = await res.json().catch(() => ({}));
569589
if (!res.ok) throw new Error(json?.message || `HTTP ${res.status}`);
570590

571-
setRecords(prev => prev.filter(r => r.id !== deleteTarget.id));
591+
setRecords((prev) => prev.filter((r) => r.id !== deleteTarget.id));
572592
setDeleteOpen(false);
573593
setDeleteTarget(null);
574594
} catch (e: any) {
@@ -582,36 +602,54 @@ export default function CameraMaintenance({ camera }: { camera: Camera }) {
582602
<div className="w-full">
583603
{/* Header + Add button */}
584604
<div className="flex items-center justify-between mb-3">
585-
<h3 className="font-semibold text-[var(--color-primary)]">Maintenance</h3>
605+
<h3 className="font-semibold text-[var(--color-primary)]">
606+
Maintenance
607+
</h3>
586608
<AddMaintenanceModal camId={camId} onAdded={onAdded} />
587609
</div>
588610

589-
{loading && <p className="text-sm text-slate-500 mb-2">Loading maintenance…</p>}
590-
{err && !loading && <p className="text-sm text-red-600 mb-2">{err}</p>}
611+
{loading && (
612+
<p className="text-sm text-slate-500 mb-2">Loading maintenance…</p>
613+
)}
614+
{err && !loading && (
615+
<p className="text-sm text-red-600 mb-2">{err}</p>
616+
)}
591617

592618
<div className="w-full max-h-[420px] overflow-y-auto">
593619
<Table className="w-full table-auto">
594620
<TableHeader>
595621
<TableRow>
596-
<TableHead onClick={() => handleSort("id")} className="cursor-pointer select-none text-[var(--color-primary)]">
622+
<TableHead
623+
onClick={() => handleSort("id")}
624+
className="cursor-pointer select-none text-[var(--color-primary)]"
625+
>
597626
<div className="flex items-center justify-between pr-3 border-r border-[var(--color-primary)] w-full">
598627
<span>ID</span>
599628
{renderSortIcon("id")}
600629
</div>
601630
</TableHead>
602-
<TableHead onClick={() => handleSort("date")} className="cursor-pointer select-none text-[var(--color-primary)]">
631+
<TableHead
632+
onClick={() => handleSort("date")}
633+
className="cursor-pointer select-none text-[var(--color-primary)]"
634+
>
603635
<div className="flex items-center justify-between pr-3 border-r border-[var(--color-primary)] w-full">
604636
<span>Date</span>
605637
{renderSortIcon("date")}
606638
</div>
607639
</TableHead>
608-
<TableHead onClick={() => handleSort("type")} className="cursor-pointer select-none text-[var(--color-primary)]">
640+
<TableHead
641+
onClick={() => handleSort("type")}
642+
className="cursor-pointer select-none text-[var(--color-primary)]"
643+
>
609644
<div className="flex items-center justify-between pr-3 border-r border-[var(--color-primary)] w-full">
610645
<span>Type</span>
611646
{renderSortIcon("type")}
612647
</div>
613648
</TableHead>
614-
<TableHead onClick={() => handleSort("technician")} className="cursor-pointer select-none text-[var(--color-primary)]">
649+
<TableHead
650+
onClick={() => handleSort("technician")}
651+
className="cursor-pointer select-none text-[var(--color-primary)]"
652+
>
615653
<div className="flex items-center justify-between pr-3 border-r border-[var(--color-primary)] w-full">
616654
<span>Technician</span>
617655
{renderSortIcon("technician")}
@@ -626,28 +664,38 @@ export default function CameraMaintenance({ camera }: { camera: Camera }) {
626664
{renderSortIcon("notes")}
627665
</div>
628666
</TableHead>
629-
<TableHead className="w-[96px] text-[var(--color-primary)] text-left font-medium">Actions</TableHead>
667+
<TableHead className="w-[96px] text-[var(--color-primary)] text-left font-medium">
668+
Actions
669+
</TableHead>
630670
</TableRow>
631671
</TableHeader>
632672

633673
<TableBody>
634-
{!loading && !err && sortedRecords.length === 0 && (
674+
{!loading && !err && total === 0 && (
635675
<TableRow>
636-
<TableCell colSpan={6} className="py-4 text-[12px] text-gray-500 text-center">
676+
<TableCell
677+
colSpan={6}
678+
className="py-4 text-[12px] text-gray-500 text-center"
679+
>
637680
No maintenance records.
638681
</TableCell>
639682
</TableRow>
640683
)}
641684

642-
{sortedRecords.map((rec) => {
685+
{pagedRecords.map((rec) => {
643686
const maintenanceCode = `MNT${String(rec.id).padStart(3, "0")}`;
644687
return (
645-
<TableRow key={rec.id} className="border-b border-gray-200 align-top text-[12px]">
688+
<TableRow
689+
key={rec.id}
690+
className="border-b border-gray-200 align-top text-[12px]"
691+
>
646692
<TableCell className="pl-0 py-3 align-top text-left font-medium">
647693
{maintenanceCode}
648694
</TableCell>
649695

650-
<TableCell className="px-2 py-3 align-top text-left font-medium">{rec.date}</TableCell>
696+
<TableCell className="px-2 py-3 align-top text-left font-medium">
697+
{rec.date}
698+
</TableCell>
651699
<TableCell className="px-2 py-3 align-top text-left font-medium">
652700
<MaintenanceTypeBadge type={rec.type} />
653701
</TableCell>
@@ -673,8 +721,8 @@ export default function CameraMaintenance({ camera }: { camera: Camera }) {
673721
disabled={busyDelete}
674722
onClick={() => {
675723
if (busyDelete) return;
676-
setDeleteTarget(rec); // ต้องตั้ง target ก่อน
677-
setDeleteOpen(true); // แล้วค่อยเปิด modal
724+
setDeleteTarget(rec);
725+
setDeleteOpen(true);
678726
}}
679727
>
680728
<Trash2 className="h-4 w-4" />
@@ -688,6 +736,43 @@ export default function CameraMaintenance({ camera }: { camera: Camera }) {
688736
</Table>
689737
</div>
690738

739+
{/* Pagination bar */}
740+
<div className="mt-3 flex items-center justify-between">
741+
<div className="text-xs text-gray-500">
742+
Showing <span className="font-medium">{total ? start + 1 : 0}</span>
743+
<span className="font-medium">{end}</span> of{" "}
744+
<span className="font-medium">{total}</span>
745+
</div>
746+
747+
<div className="flex items-center gap-2">
748+
<button
749+
onClick={() => setPage(p => Math.max(1, p - 1))}
750+
disabled={page <= 1}
751+
className={`px-3 py-1 rounded-md border text-sm ${page <= 1
752+
? "text-gray-400 border-gray-200"
753+
: "text-gray-700 border-gray-300 hover:bg-gray-50"
754+
}`}
755+
>
756+
Previous
757+
</button>
758+
759+
<div className="text-sm tabular-nums">
760+
{page} / {totalPages}
761+
</div>
762+
763+
<button
764+
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
765+
disabled={page >= totalPages}
766+
className={`px-3 py-1 rounded-md border text-sm ${page >= totalPages
767+
? "text-gray-400 border-gray-200"
768+
: "text-gray-700 border-gray-300 hover:bg-gray-50"
769+
}`}
770+
>
771+
Next
772+
</button>
773+
</div>
774+
</div>
775+
691776
{/* Modal ลบ */}
692777
<DeleteConfirmModal
693778
open={deleteOpen}

0 commit comments

Comments
 (0)