@@ -16,7 +16,8 @@ import {
1616} from "@tanstack/react-table" ;
1717import Link from "next/link" ;
1818import { Button } from "@/components/ui/button" ;
19- import { ChevronRight , AlertTriangle } from "lucide-react" ;
19+ import { ChevronRight , ThumbsUp , ThumbsDown } from "lucide-react" ;
20+ import { StackedProgress } from "@/components/ui/stacked-progress" ;
2021import { useGuardedEffect } from "@/hooks/use-guarded-effect" ;
2122import { useAtomValue } from "jotai" ;
2223import { externalIdentTypesAtom } from "@/atoms/base-data" ;
@@ -29,6 +30,11 @@ import {
2930} from "@/hooks/use-swr-with-auth-refresh" ;
3031// Per instruction, improve typing for ImportJobDataRow to make 'state' work
3132// in useDataTable. This is a first step, with 'any' types to be refined.
33+ const formatNumber = ( num : number | null | undefined ) : string => {
34+ if ( num === null || num === undefined ) return "0" ;
35+ return num . toLocaleString ( 'nb-NO' ) ;
36+ } ;
37+
3238type ImportJobDataRow = {
3339 row_id : number ;
3440 state : any ;
@@ -110,7 +116,11 @@ const fetcher = async (key: string): Promise<any> => {
110116 queryBuilder = queryBuilder . not ( key , 'is' , null ) . not ( key , 'eq' , '{}' ) ;
111117 }
112118 } else if ( [ 'operation' , 'state' , 'action' ] . includes ( key ) ) {
113- queryBuilder = queryBuilder . in ( key , values ) ;
119+ if ( values . length === 1 && values [ 0 ] === 'not_error' ) {
120+ queryBuilder = queryBuilder . neq ( key , 'error' ) ;
121+ } else {
122+ queryBuilder = queryBuilder . in ( key , values ) ;
123+ }
114124 } else {
115125 // Text search for name, external idents, etc.
116126 queryBuilder = queryBuilder . ilike ( key , `%${ values [ 0 ] } %` ) ;
@@ -479,7 +489,6 @@ export default function ImportJobDataPage() {
479489 { label : 'Has value' , value : 'not_null' } ,
480490 { label : 'Is empty' , value : 'is_null' } ,
481491 ] ,
482- isPrimary : true ,
483492 } ;
484493 }
485494
@@ -522,29 +531,71 @@ export default function ImportJobDataPage() {
522531
523532 const isLoading = isJobLoading || ( isTableDataLoading && ! tableData ) || awaitingAuthRefresh ;
524533
534+ const qualityFilterIds = [ 'state' , 'errors' , 'invalid_codes' ] ;
535+
536+ // Check if ok filter is active (quality-based: no errors, no warnings, not error state)
537+ const isOkFilterActive = React . useMemo ( ( ) => {
538+ const stateFilter = columnFilters . find ( f => f . id === 'state' ) ;
539+ const errorsFilter = columnFilters . find ( f => f . id === 'errors' ) ;
540+ const invalidCodesFilter = columnFilters . find ( f => f . id === 'invalid_codes' ) ;
541+ return Array . isArray ( stateFilter ?. value ) && stateFilter . value [ 0 ] === 'not_error'
542+ && Array . isArray ( errorsFilter ?. value ) && errorsFilter . value [ 0 ] === 'is_null'
543+ && Array . isArray ( invalidCodesFilter ?. value ) && invalidCodesFilter . value [ 0 ] === 'is_null' ;
544+ } , [ columnFilters ] ) ;
545+
525546 // Check if error filter is active
526547 const isErrorFilterActive = React . useMemo ( ( ) => {
527548 const stateFilter = columnFilters . find ( f => f . id === 'state' ) ;
528549 if ( ! stateFilter || ! Array . isArray ( stateFilter . value ) ) return false ;
529550 return stateFilter . value . includes ( 'error' ) && stateFilter . value . length === 1 ;
530551 } , [ columnFilters ] ) ;
531552
532- // Toggle error-only filter
553+ // Check if warning filter is active
554+ const isWarningFilterActive = React . useMemo ( ( ) => {
555+ const invalidCodesFilter = columnFilters . find ( f => f . id === 'invalid_codes' ) ;
556+ return Array . isArray ( invalidCodesFilter ?. value ) && invalidCodesFilter . value [ 0 ] === 'not_null' ;
557+ } , [ columnFilters ] ) ;
558+
559+ // Toggle ok-only filter (clears error and warning filters)
560+ const toggleOkFilter = React . useCallback ( ( ) => {
561+ setColumnFilters ( prev => {
562+ if ( isOkFilterActive ) {
563+ return prev . filter ( f => ! qualityFilterIds . includes ( f . id ) ) ;
564+ } else {
565+ const newFilters = prev . filter ( f => ! qualityFilterIds . includes ( f . id ) ) ;
566+ return [ ...newFilters ,
567+ { id : 'state' , value : [ 'not_error' ] } ,
568+ { id : 'errors' , value : [ 'is_null' ] } ,
569+ { id : 'invalid_codes' , value : [ 'is_null' ] } ,
570+ ] ;
571+ }
572+ } ) ;
573+ } , [ isOkFilterActive ] ) ;
574+
575+ // Toggle error-only filter (clears ok and warning filters)
533576 const toggleErrorFilter = React . useCallback ( ( ) => {
534577 setColumnFilters ( prev => {
535- const stateFilterIndex = prev . findIndex ( f => f . id === 'state' ) ;
536-
537578 if ( isErrorFilterActive ) {
538- // Remove the error filter
539- return prev . filter ( f => f . id !== 'state' ) ;
579+ return prev . filter ( f => ! qualityFilterIds . includes ( f . id ) ) ;
540580 } else {
541- // Add error filter (replace any existing state filter)
542- const newFilters = prev . filter ( f => f . id !== 'state' ) ;
543- return [ ...newFilters , { id : 'state' , value : [ 'error' ] } ] ;
581+ const newFilters = prev . filter ( f => ! qualityFilterIds . includes ( f . id ) ) ;
582+ return [ ...newFilters , { id : 'state' , value : [ 'error' ] } , { id : 'errors' , value : [ 'not_null' ] } ] ;
544583 }
545584 } ) ;
546585 } , [ isErrorFilterActive ] ) ;
547586
587+ // Toggle warning-only filter (clears ok and error filters)
588+ const toggleWarningFilter = React . useCallback ( ( ) => {
589+ setColumnFilters ( prev => {
590+ if ( isWarningFilterActive ) {
591+ return prev . filter ( f => ! qualityFilterIds . includes ( f . id ) ) ;
592+ } else {
593+ const newFilters = prev . filter ( f => ! qualityFilterIds . includes ( f . id ) ) ;
594+ return [ ...newFilters , { id : 'invalid_codes' , value : [ 'not_null' ] } ] ;
595+ }
596+ } ) ;
597+ } , [ isWarningFilterActive ] ) ;
598+
548599 // Show error (JWT errors are automatically suppressed while refreshing by the hook)
549600 if ( jobError ) {
550601 return (
@@ -606,7 +657,43 @@ export default function ImportJobDataPage() {
606657 </ div >
607658 < p className = "text-sm text-gray-500 mt-1" > Description: { job . description ?? 'N/A' } | Table: { job . data_table_name } </ p >
608659 </ div >
609-
660+
661+
662+
663+ { /* Approve/Reject bar for review workflow */ }
664+ { job . state === 'waiting_for_review' && (
665+ < div className = "flex items-center gap-3 p-3 bg-amber-50 border border-amber-200 rounded-lg" >
666+ < span className = "text-sm font-medium text-amber-800 flex-grow" > This job is waiting for your review.</ span >
667+ < Button
668+ variant = "outline"
669+ size = "sm"
670+ className = "border-red-300 text-red-600 hover:bg-red-50"
671+ onClick = { async ( ) => {
672+ const client = await getBrowserRestClient ( ) ;
673+ const { error } = await client . from ( "import_job" ) . update ( { state : 'rejected' as any } ) . eq ( "id" , job . id ) ;
674+ if ( error ) { console . error ( "Reject failed:" , error ) ; alert ( `Error: ${ error . message } ` ) ; }
675+ else { mutateJob ( ) ; }
676+ } }
677+ >
678+ < ThumbsDown className = "mr-1 h-4 w-4" />
679+ Reject
680+ </ Button >
681+ < Button
682+ size = "sm"
683+ className = "bg-green-600 hover:bg-green-700 text-white"
684+ onClick = { async ( ) => {
685+ const client = await getBrowserRestClient ( ) ;
686+ const { error } = await client . from ( "import_job" ) . update ( { state : 'approved' as any } ) . eq ( "id" , job . id ) ;
687+ if ( error ) { console . error ( "Approve failed:" , error ) ; alert ( `Error: ${ error . message } ` ) ; }
688+ else { mutateJob ( ) ; }
689+ } }
690+ >
691+ < ThumbsUp className = "mr-1 h-4 w-4" />
692+ Approve
693+ </ Button >
694+ </ div >
695+ ) }
696+
610697 { tableError && (
611698 < div className = "p-4 bg-red-50 border border-red-200 rounded-md text-red-700" >
612699 Failed to load table data: { tableError . message }
@@ -636,18 +723,48 @@ export default function ImportJobDataPage() {
636723 } }
637724 >
638725 < DataTableToolbar table = { table } >
639- < Button
640- variant = { isErrorFilterActive ? "default" : "outline" }
641- size = "sm"
642- className = { isErrorFilterActive
643- ? "h-8 bg-red-600 hover:bg-red-700 text-white"
644- : "h-8 border-dashed text-red-600 hover:bg-red-50 hover:text-red-700"
645- }
646- onClick = { toggleErrorFilter }
647- >
648- < AlertTriangle className = "mr-1 h-4 w-4" />
649- { isErrorFilterActive ? "Showing Errors" : "Show Errors Only" }
650- </ Button >
726+ { ( ( ) => {
727+ const okCount = ( job ?. total_rows ?? 0 ) - ( job ?. error_count ?? 0 ) - ( job ?. warning_count ?? 0 ) ;
728+ return okCount > 0 ? (
729+ < Button
730+ variant = { isOkFilterActive ? "default" : "outline" }
731+ size = "sm"
732+ className = { isOkFilterActive
733+ ? "h-8 bg-green-600 hover:bg-green-700 text-white"
734+ : "h-8 border-dashed text-green-700 hover:bg-green-50"
735+ }
736+ onClick = { toggleOkFilter }
737+ >
738+ < span className = "font-mono" > { formatNumber ( okCount ) } </ span > ok
739+ </ Button >
740+ ) : null ;
741+ } ) ( ) }
742+ { ( job ?. warning_count ?? 0 ) > 0 && (
743+ < Button
744+ variant = { isWarningFilterActive ? "default" : "outline" }
745+ size = "sm"
746+ className = { isWarningFilterActive
747+ ? "h-8 bg-amber-500 hover:bg-amber-600 text-white"
748+ : "h-8 border-dashed text-amber-600 hover:bg-amber-50"
749+ }
750+ onClick = { toggleWarningFilter }
751+ >
752+ < span className = "font-mono" > { formatNumber ( job ?. warning_count ) } </ span > warn
753+ </ Button >
754+ ) }
755+ { ( job ?. error_count ?? 0 ) > 0 && (
756+ < Button
757+ variant = { isErrorFilterActive ? "default" : "outline" }
758+ size = "sm"
759+ className = { isErrorFilterActive
760+ ? "h-8 bg-red-600 hover:bg-red-700 text-white"
761+ : "h-8 border-dashed text-red-600 hover:bg-red-50"
762+ }
763+ onClick = { toggleErrorFilter }
764+ >
765+ < span className = "font-mono" > { formatNumber ( job ?. error_count ) } </ span > err
766+ </ Button >
767+ ) }
651768 </ DataTableToolbar >
652769 </ DataTable >
653770 }
0 commit comments