refactor: redesign table module

This commit is contained in:
2026-03-18 01:03:58 +05:30
parent caf527f584
commit 344f37719e
24 changed files with 991 additions and 815 deletions

View File

@@ -1,7 +1,7 @@
"use client";
import { useState, useEffect } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card";
import {
AlertCircle,
Activity,
@@ -11,6 +11,7 @@ import {
Zap,
PencilLine,
CheckCircle2,
Settings2,
} from "lucide-react";
import { StatsCard } from "@/components/dashboard/stats-card";
import { CompactProjectSelector } from "@/components/dashboard/compact-project-selector";
@@ -26,9 +27,9 @@ import {
PopoverTrigger,
} from "@/components/ui/popover";
import { Button } from "@/components/ui/button";
import { Filter } from "lucide-react";
import { DashboardSkeleton } from "@/components/dashboard/dashboard-skeleton"
import { PoweredBy } from "@/components/powered-by";
import { toast } from "sonner";
const API_URL = process.env.NEXT_PUBLIC_API_URL;
@@ -207,14 +208,12 @@ export default function DashboardPage() {
totalRoadDamage: 0,
locationData: [],
});
const [error, setError] = useState<string | null>(null);
// Load projects on mount
useEffect(() => {
const loadProjects = async () => {
try {
setIsLoading(true);
setError(null);
const projectsData = await fetchProjects();
setProjects(projectsData);
@@ -223,9 +222,9 @@ export default function DashboardPage() {
}
} catch (err) {
console.error("Failed to load projects:", err);
setError(
"Failed to load projects. Please check if the backend is running.",
);
toast.error("Failed to load projects", {
description: "Please check if the backend is running."
});
} finally {
setTimeout(() => {
setIsLoading(false);
@@ -274,7 +273,6 @@ export default function DashboardPage() {
const loadProjectSummary = async () => {
try {
setIsLoading(true);
setError(null);
const response = await fetch(
`${API_URL}/summary/projects/${selectedProjectId}`,
@@ -294,7 +292,7 @@ export default function DashboardPage() {
setProjectSummary(summary);
} catch (err) {
console.error("Failed to load project summary:", err);
setError("Failed to load project summary.");
toast.error("Failed to load project summary");
setProjectSummary(null);
} finally {
setTimeout(() => {
@@ -422,10 +420,10 @@ export default function DashboardPage() {
}, [projectSummary, selectedPackageId, selectedLocationId]);
return (
<div className="min-h-screen text-gray-900 dark:text-gray-100">
<>
{/* Main Content */}
<main className="min-h-screen">
<div className="p-4 px-8 max-w-340 mx-auto">
<main>
<div>
{/* Header */}
<div className="mb-8 flex items-center justify-between">
<PageHeader
@@ -437,14 +435,14 @@ export default function DashboardPage() {
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="gap-2 font-semibold">
<Filter className="h-4 w-4" />
<Settings2 className="h-4 w-4" />
<span>Filters</span>
</Button>
</PopoverTrigger>
<PopoverContent className="w-[320px] p-0 overflow-hidden" align="end">
<div className="p-4 border-b bg-muted/30">
<h3 className="font-bold text-sm flex items-center gap-2 text-foreground">
<Filter className="h-4 w-4 text-primary" />
<Settings2 className="h-4 w-4 text-primary" />
Filter Analysis
</h3>
<p className="text-xs mt-1 text-muted-foreground">Refine your view by project, package, or location</p>
@@ -467,14 +465,6 @@ export default function DashboardPage() {
</Popover>
</div>
{/* Error Display */}
{error && (
<div className="mb-4 flex items-center gap-2 p-3 rounded-xl bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800 text-red-600 dark:text-red-400 text-sm">
<AlertCircle className="h-4 w-4 shrink-0" />
<p>{error}</p>
</div>
)}
{isLoading && !projectSummary ? (
<DashboardSkeleton />
) : (
@@ -522,18 +512,12 @@ export default function DashboardPage() {
{/* Charts Row - Side by Side */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4">
{/* Left Chart - Detection Distribution */}
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-base font-bold flex items-center gap-2">
<div className="p-2 rounded-md bg-secondary text-secondary-foreground">
<BarChart3 className="h-5 w-5" />
</div>
<span className="text-xl">
Detection Distribution
</span>
</CardTitle>
<Card className="flex flex-col">
<CardHeader className="items-center pb-0">
<CardTitle>Detection Distribution</CardTitle>
<CardDescription>Breakdown of all detected road conditions</CardDescription>
</CardHeader>
<CardContent className="pt-4">
<CardContent className="flex-1 pb-0">
<DetectionDonutChart
defectedSignboard={stats.totalDefectedSignboard}
pothole={stats.totalPothole}
@@ -542,19 +526,20 @@ export default function DashboardPage() {
goodSignboard={stats.totalGoodSignboard}
/>
</CardContent>
<CardFooter className="flex-col gap-2 text-sm">
<div className="leading-none text-muted-foreground">
Total detections: {stats.totalRoadDamage + stats.totalGoodSignboard}
</div>
</CardFooter>
</Card>
{/* Right Chart - Location Bar Chart */}
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-base font-bold flex items-center gap-2">
<div className="p-2 rounded-md bg-secondary text-secondary-foreground">
<MapPinIcon className="h-5 w-5" />
</div>
<span className="text-xl">Detections by Location</span>
</CardTitle>
{/* Right Chart - Detections by Location */}
<Card className="flex flex-col">
<CardHeader className="items-center pb-0">
<CardTitle>Detections by Location</CardTitle>
<CardDescription>Frequency of road issues across different map segments</CardDescription>
</CardHeader>
<CardContent className="pt-4">
<CardContent className="flex-1 pb-0">
<LocationBarChart
data={stats.locationData.map((loc) => ({
name: loc.name,
@@ -562,11 +547,15 @@ export default function DashboardPage() {
pothole: loc.pothole,
road_crack: loc.road_crack,
damaged_road_marking: loc.damaged_road_marking,
good_sign_board: loc.good_sign_board,
total: loc.total,
}))}
/>
</CardContent>
<CardFooter className="flex-col gap-2 text-sm">
<div className="leading-none text-muted-foreground">
Analysis based on latest processed sequence
</div>
</CardFooter>
</Card>
</div>
@@ -585,6 +574,6 @@ export default function DashboardPage() {
<PoweredBy />
</div>
</main>
</div>
</>
);
}

View File

@@ -1,6 +1,8 @@
import { BreadcrumbBasic } from "@/components/app-breadcrumb";
import { AppSidebar } from "@/components/app-sidebar";
import { ModeToggle } from "@/components/mode-toogle";
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
import { Separator } from "@/components/ui/separator";
import React from "react";
const ModulesLayout = ({
@@ -11,15 +13,23 @@ const ModulesLayout = ({
return (
<SidebarProvider>
<AppSidebar />
<main className="flex flex-1 flex-col p-4 pt-0 w-full h-screen overflow-hidden">
<div className="flex gap-3 items-center mt-2 sticky top-0 bg-background z-50 py-2">
<SidebarTrigger />
<ModeToggle />
<main className="flex flex-1 flex-col w-full h-screen overflow-hidden">
<div className="flex-1 overflow-auto">
{/* Centering container wrapper */}
<div className="max-w-380 mx-auto w-full flex flex-col min-h-full">
<div className="flex gap-3 items-center sticky top-0 bg-background/50 backdrop-blur-md z-30 py-3 px-6 border-b border-border/10">
<SidebarTrigger />
<ModeToggle />
<Separator orientation="vertical" className="mx-2 h-4" />
<BreadcrumbBasic />
</div>
<div className="p-6 flex-1">{children}</div>
</div>
</div>
<div className="flex-1 py-2 overflow-auto">{children}</div>
</main>
</SidebarProvider>
);
};
export default ModulesLayout;

View File

@@ -27,6 +27,8 @@ import {
} from "@/lib/api"
import { ColumnDef } from "@tanstack/react-table"
import { toast } from "sonner"
import { Badge } from "@/components/ui/badge"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
export default function LocationPage() {
const [locations, setLocations] = useState<Location[]>([])
@@ -168,7 +170,9 @@ export default function LocationPage() {
end_lng: parseFloat(endLng),
}
await updateLocation(currentLocation.id, data)
toast.success("Location updated successfully!")
toast.success("Location Updated", {
description: `${segmentName} has been updated successfully at ${new Date().toLocaleTimeString()}`,
})
} else {
const data: LocationCreate = {
package_id: selectedPackageId,
@@ -181,7 +185,9 @@ export default function LocationPage() {
end_lng: parseFloat(endLng),
}
await createLocation(data)
toast.success("Location created successfully!")
toast.success("Location Created", {
description: `${segmentName} has been established successfully at ${new Date().toLocaleTimeString()}`,
})
}
// Refresh locations list
@@ -191,7 +197,11 @@ export default function LocationPage() {
setIsModalOpen(false)
resetForm()
} catch (err) {
setError(err instanceof Error ? err.message : `Failed to ${isEditing ? 'update' : 'create'} location`)
const message = err instanceof Error ? err.message : `Failed to ${isEditing ? 'update' : 'create'} location`
setError(message)
toast.error("Operation Failed", {
description: message,
})
} finally {
setIsSubmitting(false)
}
@@ -224,10 +234,15 @@ export default function LocationPage() {
try {
setIsLoading(true)
await deleteLocation(location.id)
toast.success("Location deleted successfully!")
toast.success("Location Deleted", {
description: `${location.segment_name} has been removed from the system.`,
})
await loadLocations()
} catch (err) {
setError("Failed to delete location")
toast.error("Deletion Failed", {
description: "The location could not be removed. Please try again.",
})
} finally {
setIsLoading(false)
}
@@ -262,6 +277,50 @@ export default function LocationPage() {
return project?.name || "—"
}
},
{
id: "project_state",
header: "Project State",
cell: ({ row }) => {
const location = row.original
const pkg = allPackages.find(p => p.id === location.package_id)
const project = projects.find(p => p.id === pkg?.project_id)
if (!project?.state) return <span className="text-gray-400"></span>
const states = project.state.split(',').map(s => s.trim()).filter(Boolean)
if (states.length === 0) return <span className="text-gray-400"></span>
const firstState = states[0]
const remainingStates = states.slice(1)
return (
<div className="flex items-center gap-1.5">
<Badge variant="secondary">
{firstState}
</Badge>
{remainingStates.length > 0 && (
<Popover>
<PopoverTrigger asChild>
<button className="flex items-center justify-center rounded-full bg-muted/50 hover:bg-muted px-1.5 py-0.5 text-[10px] font-bold text-muted-foreground transition-colors border border-border/40">
+{remainingStates.length}
</button>
</PopoverTrigger>
<PopoverContent className="w-auto p-2" align="start">
<div className="flex flex-col gap-1.5">
<p className="text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-0.5 px-1">Other States</p>
{remainingStates.map((item, idx) => (
<Badge key={idx} variant="secondary">
{item}
</Badge>
))}
</div>
</PopoverContent>
</Popover>
)}
</div>
)
}
},
{
accessorKey: "chainage",
header: "Chainage (km)",
@@ -269,9 +328,10 @@ export default function LocationPage() {
const location = row.original
if (location.chainage_start_km !== null && location.chainage_end_km !== null) {
return (
<span className="px-2 py-0.5 rounded-full bg-amber-50 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300 font-semibold border border-amber-100 dark:border-amber-800 whitespace-nowrap">
<Badge variant="outline" className="flex items-center gap-1.5 border-amber-500/50 text-amber-500 font-bold whitespace-nowrap">
<Milestone className="h-3 w-3" />
{location.chainage_start_km} - {location.chainage_end_km}
</span>
</Badge>
)
}
return "—"
@@ -281,44 +341,44 @@ export default function LocationPage() {
accessorKey: "start_gps",
header: "Start GPS",
cell: ({ row }) => (
<span className="px-2 py-0.5 rounded-full bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 border border-blue-100 dark:border-blue-800 whitespace-nowrap">
<Badge variant="outline" className="flex items-center gap-1.5 border-blue-500/50 text-blue-500 font-bold whitespace-nowrap">
<MapPin className="h-3 w-3" />
{row.original.start_lat.toFixed(4)}, {row.original.start_lng.toFixed(4)}
</span>
</Badge>
)
},
{
accessorKey: "end_gps",
header: "End GPS",
cell: ({ row }) => (
<span className="px-2 py-0.5 rounded-full bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 border border-blue-100 dark:border-blue-800 whitespace-nowrap">
<Badge variant="outline" className="flex items-center gap-1.5 border-blue-500/50 text-blue-500 font-bold whitespace-nowrap">
<MapPin className="h-3 w-3" />
{row.original.end_lat.toFixed(4)}, {row.original.end_lng.toFixed(4)}
</span>
</Badge>
)
},
]
return (
<div className="min-h-screen text-gray-900 dark:text-gray-100">
<main className="min-h-screen relative overflow-hidden">
<div className="mx-auto px-6 py-8 max-w-340 relative z-10">
<>
<main className="relative z-10">
{/* Refined Header */}
<div className="mb-8">
<PageHeader
title="Location Management"
description="Manage road segment locations"
icon={MapPin}
actions={
<Button
onClick={() => { setIsEditing(false); setIsModalOpen(true); }}
>
<MapPin className="mr-2 h-4 w-4" />
Add New Location
</Button>
}
/>
</div>
{/* Error Message */}
{error && !isModalOpen && (
<div className="mb-6 flex items-center gap-3 p-4 rounded-xl bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800">
<div className="w-5 h-5 rounded-full bg-red-100 dark:bg-red-900 flex items-center justify-center shrink-0">
<span className="text-xs font-bold text-red-500">!</span>
</div>
<p className="text-sm text-red-600 dark:text-red-400 break-all">{error}</p>
</div>
)}
{/* Data Table */}
<div>
@@ -326,10 +386,8 @@ export default function LocationPage() {
title="Locations"
data={locations}
columns={columns}
onAddNew={() => { setIsEditing(false); setIsModalOpen(true); }}
onEdit={handleEdit}
onDelete={handleDelete}
addButtonText="Add New Location"
isLoading={isLoading}
pagination={{
skip,
@@ -344,8 +402,7 @@ export default function LocationPage() {
</div>
<PoweredBy />
</div>
</main>
</main>
{/* Modal Dialog */}
<Dialog open={isModalOpen} onOpenChange={(open) => { if (!open) { setIsModalOpen(false); resetForm(); } else { setIsModalOpen(true); } }}>
@@ -365,15 +422,6 @@ export default function LocationPage() {
</DialogHeader>
{/* Error Message in Modal */}
{error && (
<div className="flex items-center gap-3 p-4 rounded-xl bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800">
<div className="w-5 h-5 rounded-full bg-red-100 dark:bg-red-900 flex items-center justify-center shrink-0">
<span className="text-xs font-bold text-red-500">!</span>
</div>
<p className="text-sm text-red-600 dark:text-red-400 break-all">{error}</p>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6">
{!isEditing && (
@@ -607,6 +655,6 @@ export default function LocationPage() {
</form>
</DialogContent>
</Dialog>
</div>
</>
)
}

View File

@@ -23,6 +23,8 @@ import {
} from "@/lib/api"
import { ColumnDef } from "@tanstack/react-table"
import { toast } from "sonner"
import { Badge } from "@/components/ui/badge"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { PoweredBy } from "@/components/powered-by"
export default function PackagePage() {
@@ -108,7 +110,9 @@ export default function PackagePage() {
region: region.trim() || null,
}
await updatePackage(currentPackage.id, data)
toast.success("Package updated successfully!")
toast.success("Package Updated", {
description: `${name} has been updated successfully at ${new Date().toLocaleTimeString()}`,
})
} else {
const data: PackageCreate = {
project_id: selectedProjectId,
@@ -116,7 +120,9 @@ export default function PackagePage() {
region: region.trim() || null,
}
await createPackage(data)
toast.success("Package created successfully!")
toast.success("Package Created", {
description: `${name} has been established successfully at ${new Date().toLocaleTimeString()}`,
})
}
// Refresh packages list
@@ -126,7 +132,11 @@ export default function PackagePage() {
setIsModalOpen(false)
resetForm()
} catch (err) {
setError(err instanceof Error ? err.message : `Failed to ${isEditing ? 'update' : 'create'} package`)
const message = err instanceof Error ? err.message : `Failed to ${isEditing ? 'update' : 'create'} package`
setError(message)
toast.error("Operation Failed", {
description: message,
})
} finally {
setIsSubmitting(false)
}
@@ -147,10 +157,15 @@ export default function PackagePage() {
try {
setIsLoading(true)
await deletePackage(pkg.id)
toast.success("Package deleted successfully!")
toast.success("Package Deleted", {
description: `${pkg.name} has been removed from the system.`,
})
await loadPackages()
} catch (err) {
setError("Failed to delete package")
toast.error("Deletion Failed", {
description: "The package could not be removed. Please try again.",
})
} finally {
setIsLoading(false)
}
@@ -180,16 +195,38 @@ export default function PackagePage() {
const pkg = row.original
const project = projects.find(p => p.id === pkg.project_id)
if (!project?.state) return <span className="text-gray-400"></span>
const states = project.state.split(',').map(s => s.trim()).filter(Boolean)
if (states.length === 0) return <span className="text-gray-400"></span>
const firstState = states[0]
const remainingStates = states.slice(1)
return (
<div className="flex flex-wrap gap-1">
{project.state.split(',').map((item, idx) => (
<span
key={idx}
className="px-2 py-0.5 rounded-full bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 font-semibold border border-blue-100 dark:border-blue-800"
>
{item.trim()}
</span>
))}
<div className="flex items-center gap-1.5">
<Badge variant="secondary">
{firstState}
</Badge>
{remainingStates.length > 0 && (
<Popover>
<PopoverTrigger asChild>
<button className="flex items-center justify-center rounded-full bg-muted/50 hover:bg-muted px-1.5 py-0.5 text-[10px] font-bold text-muted-foreground transition-colors border border-border/40">
+{remainingStates.length}
</button>
</PopoverTrigger>
<PopoverContent className="w-auto p-2" align="start">
<div className="flex flex-col gap-1.5">
<p className="text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-0.5 px-1">Other States</p>
{remainingStates.map((item, idx) => (
<Badge key={idx} variant="secondary">
{item}
</Badge>
))}
</div>
</PopoverContent>
</Popover>
)}
</div>
)
}
@@ -201,27 +238,26 @@ export default function PackagePage() {
]
return (
<div className="min-h-screen text-gray-900 dark:text-gray-100">
<main className="min-h-screen relative overflow-hidden">
<div className="mx-auto px-6 py-8 max-w-340 relative z-10">
<>
<main className="relative z-10">
{/* Refined Header */}
<div className="mb-8">
<PageHeader
title="Package Management"
description="Manage project packages"
icon={Package}
actions={
<Button
onClick={() => { setIsEditing(false); setIsModalOpen(true); }}
>
<Package className="mr-2 h-4 w-4" />
Add New Package
</Button>
}
/>
</div>
{/* Error Message */}
{error && !isModalOpen && (
<div className="mb-6 flex items-center gap-3 p-4 rounded-xl bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800">
<div className="w-5 h-5 rounded-full bg-red-100 dark:bg-red-900 flex items-center justify-center shrink-0">
<span className="text-xs font-bold text-red-500">!</span>
</div>
<p className="text-sm text-red-600 dark:text-red-400 break-all">{error}</p>
</div>
)}
{/* Data Table */}
<div>
@@ -229,10 +265,8 @@ export default function PackagePage() {
title="Packages"
data={packages}
columns={columns}
onAddNew={() => { setIsEditing(false); setIsModalOpen(true); }}
onEdit={handleEdit}
onDelete={handleDelete}
addButtonText="Add New Package"
isLoading={isLoading}
pagination={{
skip,
@@ -247,8 +281,7 @@ export default function PackagePage() {
</div>
<PoweredBy />
</div>
</main>
</main>
{/* Modal Dialog */}
<Dialog open={isModalOpen} onOpenChange={(open) => { if (!open) { setIsModalOpen(false); resetForm(); } else { setIsModalOpen(true); } }}>
@@ -269,15 +302,6 @@ export default function PackagePage() {
</DialogHeader>
{/* Error Message in Modal */}
{error && (
<div className="flex items-center gap-3 p-4 rounded-xl bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800">
<div className="w-5 h-5 rounded-full bg-red-100 dark:bg-red-900 flex items-center justify-center shrink-0">
<span className="text-xs font-bold text-red-500">!</span>
</div>
<p className="text-sm text-red-600 dark:text-red-400 break-all">{error}</p>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-8">
{/* Step 1: Select Project */}
@@ -371,7 +395,7 @@ export default function PackagePage() {
<Button
type="submit"
disabled={isSubmitting || !name.trim() || !selectedProjectId}
className="flex-1 h-12 font-bold uppercase tracking-wider text-xs shadow-lg shadow-primary/20"
className="flex-1 h-12 font-bold uppercase tracking-wider text-xs"
>
{isSubmitting ? (
<div className="flex items-center gap-2">
@@ -389,6 +413,6 @@ export default function PackagePage() {
</form>
</DialogContent>
</Dialog>
</div>
</>
)
}

View File

@@ -12,6 +12,8 @@ import { PageHeader } from "@/components/page-header"
import { createProject, fetchProjects, updateProject, deleteProject, type ProjectCreate, type Project, type ProjectUpdate } from "@/lib/api"
import { ColumnDef } from "@tanstack/react-table"
import { toast } from "sonner"
import { Badge } from "@/components/ui/badge"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { PoweredBy } from "@/components/powered-by"
export default function ProjectPage() {
@@ -91,7 +93,9 @@ export default function ProjectPage() {
end_lng: endLng ? parseFloat(endLng) : null,
}
await updateProject(currentProject.id, data)
toast.success("Project updated successfully!")
toast.success("Project Updated", {
description: `${name} has been updated successfully at ${new Date().toLocaleTimeString()}`,
})
} else {
const data: ProjectCreate = {
name: name.trim(),
@@ -103,7 +107,9 @@ export default function ProjectPage() {
end_lng: endLng ? parseFloat(endLng) : null,
}
await createProject(data)
toast.success("Project created successfully!")
toast.success("Project Created", {
description: `${name} has been established successfully at ${new Date().toLocaleTimeString()}`,
})
}
// Refresh projects list
@@ -113,7 +119,11 @@ export default function ProjectPage() {
setIsModalOpen(false)
resetForm()
} catch (err) {
setError(err instanceof Error ? err.message : `Failed to ${isEditing ? 'update' : 'create'} project`)
const message = err instanceof Error ? err.message : `Failed to ${isEditing ? 'update' : 'create'} project`
setError(message)
toast.error("Operation Failed", {
description: message,
})
} finally {
setIsSubmitting(false)
}
@@ -138,10 +148,15 @@ export default function ProjectPage() {
try {
setIsLoading(true)
await deleteProject(project.id)
toast.success("Project deleted successfully!")
toast.success("Project Deleted", {
description: `${project.name} has been removed from the system.`,
})
await loadProjects()
} catch (err) {
setError("Failed to delete project")
toast.error("Deletion Failed", {
description: "The project could not be removed. Please try again or check your permissions.",
})
} finally {
setIsLoading(false)
}
@@ -152,7 +167,7 @@ export default function ProjectPage() {
accessorKey: "name",
header: "Project Name",
cell: ({ row }) => (
<div className="font-semibold text-gray-900 dark:text-gray-100">{row.original.name}</div>
<div className="font-semibold">{row.original.name}</div>
)
},
{
@@ -160,17 +175,45 @@ export default function ProjectPage() {
header: "State",
cell: ({ row }) => {
const project = row.original
if (!project.state) return <span className="text-gray-400"></span>
if (!project.state) return <span></span>
const states = project.state.split(',').map(s => s.trim()).filter(Boolean)
if (states.length === 0) return <span></span>
const firstState = states[0]
const remainingStates = states.slice(1)
return (
<div className="flex flex-wrap gap-1.5">
{project.state.split(',').map((item, idx) => (
<span
key={idx}
className="px-2 py-0.5 rounded-full bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 font-semibold border border-blue-100 dark:border-blue-800"
>
{item.trim()}
</span>
))}
<div className="flex items-center gap-1.5">
<Badge
variant="secondary"
>
{firstState}
</Badge>
{remainingStates.length > 0 && (
<Popover>
<PopoverTrigger asChild>
<button className="flex items-center justify-center rounded-full bg-muted/50 hover:bg-muted px-1.5 py-0.5 text-[10px] font-bold text-muted-foreground transition-colors border border-border/40">
+{remainingStates.length}
</button>
</PopoverTrigger>
<PopoverContent className="w-auto p-2" align="start">
<div className="flex flex-col gap-1.5">
<p className="text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-0.5 px-1">Other States</p>
{remainingStates.map((item, idx) => (
<Badge
key={idx}
variant="secondary"
>
{item}
</Badge>
))}
</div>
</PopoverContent>
</Popover>
)}
</div>
)
}
@@ -182,27 +225,26 @@ export default function ProjectPage() {
]
return (
<div className="min-h-screen">
<main className="min-h-screen relative overflow-hidden">
<div className="mx-auto px-6 py-8 max-w-340 relative z-10">
<>
<main className="relative z-10">
{/* Refined Header */}
<div className="mb-8">
<PageHeader
title="Project Management"
description="Manage road infrastructure projects"
icon={FolderPlus}
actions={
<Button
onClick={() => { setIsEditing(false); setIsModalOpen(true); }}
>
<FolderPlus className="mr-2 h-5 w-5" />
Add New Project
</Button>
}
/>
</div>
{/* Error Message */}
{error && !isModalOpen && (
<div className="mb-6 flex items-center gap-3 p-4 rounded-xl bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800">
<div className="w-5 h-5 rounded-full bg-red-100 dark:bg-red-900 flex items-center justify-center shrink-0">
<span className="text-xs font-bold text-red-500">!</span>
</div>
<p className="text-sm text-red-600 dark:text-red-400 break-all">{error}</p>
</div>
)}
{/* Data Table */}
<div>
@@ -210,10 +252,8 @@ export default function ProjectPage() {
title="Projects"
data={projects}
columns={columns}
onAddNew={() => { setIsEditing(false); setIsModalOpen(true); }}
onEdit={handleEdit}
onDelete={handleDelete}
addButtonText="Add New Project"
isLoading={isLoading}
pagination={{
skip,
@@ -228,8 +268,8 @@ export default function ProjectPage() {
</div>
<PoweredBy />
</div>
</main>
</main>
{/* Modal Dialog */}
<Dialog open={isModalOpen} onOpenChange={(open) => { if (!open) { setIsModalOpen(false); resetForm(); } else { setIsModalOpen(true); } }}>
@@ -250,15 +290,6 @@ export default function ProjectPage() {
</DialogHeader>
{/* Error Message in Modal */}
{error && (
<div className="flex items-center gap-3 p-4 rounded-xl bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800">
<div className="w-5 h-5 rounded-full bg-red-100 dark:bg-red-900 flex items-center justify-center shrink-0">
<span className="text-xs font-bold text-red-500">!</span>
</div>
<p className="text-sm text-red-600 dark:text-red-400 break-all">{error}</p>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6">
{/* Project Name */}
@@ -409,6 +440,6 @@ export default function ProjectPage() {
</form>
</DialogContent>
</Dialog>
</div>
</>
)
}