refactor: redesign table module
This commit is contained in:
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
BIN
src/app/favicon.ico
Normal file
BIN
src/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
@@ -149,6 +149,29 @@
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
|
||||
/* Modern Scrollbar Styles */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px; /* Slightly wider for better accessibility */
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: var(--muted-foreground);
|
||||
border-radius: 20px;
|
||||
border: 2px solid transparent;
|
||||
background-clip: content-box;
|
||||
opacity: 0.5; /* More visible default */
|
||||
}
|
||||
/* Firefox Support */
|
||||
* {
|
||||
/* scrollbar-width: ; */
|
||||
scrollbar-color: var(--muted-foreground) transparent;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import { Toaster } from "@/components/ui/sonner"
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
@@ -35,6 +36,7 @@ export default function RootLayout({
|
||||
disableTransitionOnChange
|
||||
>
|
||||
{children}
|
||||
<Toaster position="top-center" />
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user