@@ -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