change to use the tannstack table

This commit is contained in:
THIS ONE IS A LITTLE BIT TRICKY KRUB 2025-03-31 21:07:23 +07:00
parent 7b69c68056
commit 2b694a1b44
4 changed files with 282 additions and 180 deletions

View File

@ -18,7 +18,13 @@ import {
CloudRain,
Wind,
} from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { Progress } from "@/components/ui/progress";
@ -26,7 +32,11 @@ import { Badge } from "@/components/ui/badge";
import { ScrollArea } from "@/components/ui/scroll-area";
import { ChatbotDialog } from "./chatbot-dialog";
import { AnalyticsDialog } from "./analytics-dialog";
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import type { Crop, CropAnalytics } from "@/types";
import GoogleMapWithDrawing from "@/components/google-map-with-drawing";
@ -37,7 +47,11 @@ interface CropDetailPageParams {
cropId: string;
}
export default function CropDetailPage({ params }: { params: Promise<CropDetailPageParams> }) {
export default function CropDetailPage({
params,
}: {
params: Promise<CropDetailPageParams>;
}) {
const router = useRouter();
const [crop, setCrop] = useState<Crop | null>(null);
const [analytics, setAnalytics] = useState<CropAnalytics | null>(null);
@ -57,7 +71,9 @@ export default function CropDetailPage({ params }: { params: Promise<CropDetailP
if (!crop || !analytics) {
return (
<div className="min-h-screen flex items-center justify-center bg-background text-foreground">Loading...</div>
<div className="min-h-screen flex items-center justify-center bg-background text-foreground">
Loading...
</div>
);
}
@ -87,7 +103,8 @@ export default function CropDetailPage({ params }: { params: Promise<CropDetailP
icon: ListCollapse,
description: "View detailed information",
onClick: () => console.log("Details clicked"),
color: "bg-purple-50 dark:bg-purple-900 text-purple-600 dark:text-purple-300",
color:
"bg-purple-50 dark:bg-purple-900 text-purple-600 dark:text-purple-300",
},
{
title: "Settings",
@ -107,7 +124,8 @@ export default function CropDetailPage({ params }: { params: Promise<CropDetailP
<Button
variant="ghost"
className="gap-2 text-green-700 dark:text-green-300 hover:text-green-800 dark:hover:text-green-200 hover:bg-green-100/50 dark:hover:bg-green-800/50"
onClick={() => router.back()}>
onClick={() => router.back()}
>
<ArrowLeft className="h-4 w-4" /> Back to Farm
</Button>
<HoverCard>
@ -126,7 +144,9 @@ export default function CropDetailPage({ params }: { params: Promise<CropDetailP
</Avatar>
<div className="space-y-1">
<h4 className="text-sm font-semibold">Growth Timeline</h4>
<p className="text-sm text-muted-foreground">Planted on {crop.plantedDate.toLocaleDateString()}</p>
<p className="text-sm text-muted-foreground">
Planted on {crop.plantedDate.toLocaleDateString()}
</p>
<div className="flex items-center pt-2">
<Separator className="w-full" />
<span className="mx-2 text-xs text-muted-foreground">
@ -150,19 +170,28 @@ export default function CropDetailPage({ params }: { params: Promise<CropDetailP
<div className="flex items-center gap-4">
<div className="flex flex-col items-end">
<div className="flex items-center gap-2">
<Badge variant="outline" className={`${healthColors[analytics.plantHealth]} border`}>
<Badge
variant="outline"
className={`${healthColors[analytics.plantHealth]} border`}
>
Health Score: {crop.healthScore}%
</Badge>
<Badge variant="outline" className="bg-blue-50 dark:bg-blue-900 text-blue-600 dark:text-blue-300">
<Badge
variant="outline"
className="bg-blue-50 dark:bg-blue-900 text-blue-600 dark:text-blue-300"
>
Growing
</Badge>
</div>
{crop.expectedHarvest ? (
<p className="text-sm text-muted-foreground mt-1">
Expected harvest: {crop.expectedHarvest.toLocaleDateString()}
Expected harvest:{" "}
{crop.expectedHarvest.toLocaleDateString()}
</p>
) : (
<p className="text-sm text-muted-foreground mt-1">Expected harvest date not available</p>
<p className="text-sm text-muted-foreground mt-1">
Expected harvest date not available
</p>
)}
</div>
</div>
@ -180,13 +209,18 @@ export default function CropDetailPage({ params }: { params: Promise<CropDetailP
key={action.title}
variant="outline"
className={`h-auto p-4 flex flex-col items-center gap-3 transition-all group ${action.color} hover:scale-105`}
onClick={action.onClick}>
<div className={`p-3 rounded-lg ${action.color} group-hover:scale-110 transition-transform`}>
onClick={action.onClick}
>
<div
className={`p-3 rounded-lg ${action.color} group-hover:scale-110 transition-transform`}
>
<action.icon className="h-5 w-5" />
</div>
<div className="text-center">
<div className="font-medium mb-1">{action.title}</div>
<p className="text-xs text-muted-foreground">{action.description}</p>
<p className="text-xs text-muted-foreground">
{action.description}
</p>
</div>
</Button>
))}
@ -196,7 +230,9 @@ export default function CropDetailPage({ params }: { params: Promise<CropDetailP
<Card className="border-green-100 dark:border-green-700">
<CardHeader>
<CardTitle>Environmental Conditions</CardTitle>
<CardDescription>Real-time monitoring of growing conditions</CardDescription>
<CardDescription>
Real-time monitoring of growing conditions
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-6">
@ -247,15 +283,22 @@ export default function CropDetailPage({ params }: { params: Promise<CropDetailP
].map((metric) => (
<Card
key={metric.label}
className="border-none shadow-none bg-gradient-to-br from-white to-gray-50/50 dark:from-slate-800 dark:to-slate-700/50">
className="border-none shadow-none bg-gradient-to-br from-white to-gray-50/50 dark:from-slate-800 dark:to-slate-700/50"
>
<CardContent className="p-4">
<div className="flex items-start gap-4">
<div className={`p-2 rounded-lg ${metric.bg}`}>
<metric.icon className={`h-4 w-4 ${metric.color}`} />
<metric.icon
className={`h-4 w-4 ${metric.color}`}
/>
</div>
<div>
<p className="text-sm font-medium text-muted-foreground">{metric.label}</p>
<p className="text-2xl font-semibold tracking-tight">{metric.value}</p>
<p className="text-sm font-medium text-muted-foreground">
{metric.label}
</p>
<p className="text-2xl font-semibold tracking-tight">
{metric.value}
</p>
</div>
</div>
</CardContent>
@ -269,9 +312,14 @@ export default function CropDetailPage({ params }: { params: Promise<CropDetailP
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="font-medium">Growth Progress</span>
<span className="text-muted-foreground">{analytics.growthProgress}%</span>
<span className="text-muted-foreground">
{analytics.growthProgress}%
</span>
</div>
<Progress value={analytics.growthProgress} className="h-2" />
<Progress
value={analytics.growthProgress}
className="h-2"
/>
</div>
{/* Next Action Card */}
@ -282,10 +330,15 @@ export default function CropDetailPage({ params }: { params: Promise<CropDetailP
<Timer className="h-4 w-4 text-green-600 dark:text-green-300" />
</div>
<div>
<p className="font-medium mb-1">Next Action Required</p>
<p className="text-sm text-muted-foreground">{analytics.nextAction}</p>
<p className="font-medium mb-1">
Next Action Required
</p>
<p className="text-sm text-muted-foreground">
{analytics.nextAction}
</p>
<p className="text-xs text-muted-foreground mt-1">
Due by {analytics.nextActionDue.toLocaleDateString()}
Due by{" "}
{analytics.nextActionDue.toLocaleDateString()}
</p>
</div>
</div>
@ -337,9 +390,14 @@ export default function CropDetailPage({ params }: { params: Promise<CropDetailP
<div key={nutrient.name} className="space-y-2">
<div className="flex justify-between text-sm">
<span className="font-medium">{nutrient.name}</span>
<span className="text-muted-foreground">{nutrient.value}%</span>
<span className="text-muted-foreground">
{nutrient.value}%
</span>
</div>
<Progress value={nutrient.value} className={`h-2 ${nutrient.color}`} />
<Progress
value={nutrient.value}
className={`h-2 ${nutrient.color}`}
/>
</div>
))}
</div>
@ -372,10 +430,14 @@ export default function CropDetailPage({ params }: { params: Promise<CropDetailP
][i]
}
</p>
<p className="text-xs text-muted-foreground">2 hours ago</p>
<p className="text-xs text-muted-foreground">
2 hours ago
</p>
</div>
</div>
{i < 4 && <Separator className="my-4 dark:bg-slate-700" />}
{i < 4 && (
<Separator className="my-4 dark:bg-slate-700" />
)}
</div>
))}
</ScrollArea>
@ -385,8 +447,17 @@ export default function CropDetailPage({ params }: { params: Promise<CropDetailP
</div>
{/* Dialogs */}
<ChatbotDialog open={isChatOpen} onOpenChange={setIsChatOpen} cropName={crop.name} />
<AnalyticsDialog open={isAnalyticsOpen} onOpenChange={setIsAnalyticsOpen} crop={crop} analytics={analytics} />
<ChatbotDialog
open={isChatOpen}
onOpenChange={setIsChatOpen}
cropName={crop.name}
/>
<AnalyticsDialog
open={isAnalyticsOpen}
onOpenChange={setIsAnalyticsOpen}
crop={crop}
analytics={analytics}
/>
</div>
</div>
);
@ -399,9 +470,15 @@ function Activity({ icon }: { icon: number }) {
const icons = [
<Droplets key="0" className="h-4 w-4 text-blue-500 dark:text-blue-300" />,
<Leaf key="1" className="h-4 w-4 text-green-500 dark:text-green-300" />,
<LineChart key="2" className="h-4 w-4 text-purple-500 dark:text-purple-300" />,
<LineChart
key="2"
className="h-4 w-4 text-purple-500 dark:text-purple-300"
/>,
<Sprout key="3" className="h-4 w-4 text-yellow-500 dark:text-yellow-300" />,
<ThermometerSun key="4" className="h-4 w-4 text-orange-500 dark:text-orange-300" />,
<ThermometerSun
key="4"
className="h-4 w-4 text-orange-500 dark:text-orange-300"
/>,
];
return icons[icon];
}

View File

@ -1,19 +1,25 @@
"use client";
import { useState } from "react";
import {
JSXElementConstructor,
ReactElement,
ReactNode,
ReactPortal,
useState,
} from "react";
import { useQuery } from "@tanstack/react-query";
import { Calendar, ChevronDown, Plus, Search } from "lucide-react";
import {
useReactTable,
getCoreRowModel,
getSortedRowModel,
getPaginationRowModel,
flexRender,
SortingState,
PaginationState,
} from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Table,
TableBody,
@ -26,14 +32,8 @@ import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
} from "@/components/ui/pagination";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Calendar as CalendarComponent } from "@/components/ui/calendar";
import { Badge } from "@/components/ui/badge";
import { fetchInventoryItems } from "@/api/inventory";
@ -44,11 +44,14 @@ import { DeleteInventoryItem } from "./delete-inventory-item";
export default function InventoryPage() {
const [date, setDate] = useState<Date>();
const [inventoryType, setInventoryType] = useState("all");
const [currentPage, setCurrentPage] = useState(1);
const [sorting, setSorting] = useState<SortingState>([]);
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
// Fetch inventory items using react-query.
const {
data: inventoryItems,
data: inventoryItems = [],
isLoading,
isError,
} = useQuery({
@ -57,155 +60,156 @@ export default function InventoryPage() {
staleTime: 60 * 1000,
});
if (isLoading) {
return (
<div className="flex min-h-screen bg-background items-center justify-center">
Loading...
</div>
);
}
if (isError || !inventoryItems) {
return (
<div className="flex min-h-screen bg-background items-center justify-center">
Error loading inventory data.
</div>
);
}
// Filter items based on selected type.
const filteredItems =
inventoryType === "all"
? inventoryItems
: inventoryItems.filter((item) =>
inventoryType === "plantation"
? item.type === "Plantation"
: item.type === "Fertilizer",
: inventoryItems.filter(
(item) => item.type.toLowerCase() === inventoryType
);
const columns = [
{ accessorKey: "name", header: "Name" },
{ accessorKey: "category", header: "Category" },
{ accessorKey: "type", header: "Type" },
{ accessorKey: "quantity", header: "Quantity" },
{ accessorKey: "lastUpdated", header: "Last Updated" },
{
accessorKey: "status",
header: "Status",
cell: (info: {
getValue: () =>
| string
| number
| bigint
| boolean
| ReactElement<unknown, string | JSXElementConstructor<any>>
| Iterable<ReactNode>
| ReactPortal
| Promise<
| string
| number
| bigint
| boolean
| ReactPortal
| ReactElement<unknown, string | JSXElementConstructor<any>>
| Iterable<ReactNode>
| null
| undefined
>
| null
| undefined;
}) => <Badge>{info.getValue()}</Badge>,
},
{ accessorKey: "edit", header: "Edit", cell: () => <EditInventoryItem /> },
{
accessorKey: "delete",
header: "Delete",
cell: () => <DeleteInventoryItem />,
},
];
const table = useReactTable({
data: filteredItems,
columns,
state: { sorting, pagination },
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onSortingChange: setSorting,
onPaginationChange: setPagination,
});
if (isLoading)
return (
<div className="flex min-h-screen items-center justify-center">
Loading...
</div>
);
if (isError)
return (
<div className="flex min-h-screen items-center justify-center">
Error loading inventory data.
</div>
);
return (
<div className="flex min-h-screen bg-background">
<div className="flex-1 flex flex-col">
<main className="flex-1 p-6">
<h1 className="text-2xl font-bold tracking-tight mb-6">Inventory</h1>
{/* Filters and search */}
<div className="flex flex-col sm:flex-row gap-4 mb-6">
<div className="flex gap-2">
<Button
variant={inventoryType === "all" ? "default" : "outline"}
onClick={() => setInventoryType("all")}
className="w-24"
>
All
</Button>
<Select value={inventoryType} onValueChange={setInventoryType}>
<SelectTrigger className="w-32">
<SelectValue placeholder="Crop" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="all">All</SelectItem>
<SelectItem value="plantation">Plantation</SelectItem>
<SelectItem value="fertilizer">Fertilizer</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
<div className="flex flex-1 gap-4">
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="flex-1 justify-between">
<div className="flex items-center">
<Calendar className="mr-2 h-4 w-4" />
{date ? date.toLocaleDateString() : "Time filter"}
</div>
<ChevronDown className="h-4 w-4 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<CalendarComponent
mode="single"
selected={date}
onSelect={setDate}
initialFocus
/>
</PopoverContent>
</Popover>
<div className="relative flex-1">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder="Search Farms"
className="pl-8"
/>
</div>
<AddInventoryItem />
</div>
<Button onClick={() => setInventoryType("all")} className="w-24">
All
</Button>
<Input type="search" placeholder="Search the name" />
<AddInventoryItem />
</div>
{/* Table */}
<div className="border rounded-md">
<h3 className="px-4 py-2 border-b font-medium">Table Fields</h3>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Category</TableHead>
<TableHead>Type</TableHead>
<TableHead className="text-right">Quantity</TableHead>
<TableHead>Last Updated</TableHead>
<TableHead>Status</TableHead>
<TableHead>Edit</TableHead>
<TableHead>Delete</TableHead>
</TableRow>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead
key={header.id}
onClick={header.column.getToggleSortingHandler()}
>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{filteredItems.length === 0 ? (
<TableRow>
<TableCell
colSpan={6}
className="text-center py-8 text-muted-foreground"
>
No inventory items found
</TableCell>
{table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
))}
</TableRow>
) : (
filteredItems.map((item) => (
<TableRow key={item.id}>
<TableCell className="font-medium">{item.name}</TableCell>
<TableCell>{item.category}</TableCell>
<TableCell>{item.type}</TableCell>
<TableCell className="text-right">
{item.quantity} {item.unit}
</TableCell>
<TableCell>{item.lastUpdated}</TableCell>
<TableCell>
<Badge
variant={
item.status === "Low Stock"
? "destructive"
: "default"
}
>
{item.status}
</Badge>
</TableCell>
<TableCell>
<EditInventoryItem />
</TableCell>
<TableCell>
<DeleteInventoryItem />
</TableCell>
</TableRow>
))
)}
))}
</TableBody>
</Table>
</div>
<Pagination className="mt-5">
<PaginationContent>
<PaginationItem>
<Button
className="flex w-24"
onClick={() =>
setPagination((prev) => ({
...prev,
pageIndex: Math.max(0, prev.pageIndex - 1),
}))
}
>
Previous
</Button>
</PaginationItem>
<PaginationItem>
<Button
className="flex w-24"
onClick={() =>
setPagination((prev) => ({
...prev,
pageIndex: prev.pageIndex + 1,
}))
}
>
Next
</Button>
</PaginationItem>
</PaginationContent>
</Pagination>
</main>
</div>
</div>

View File

@ -31,6 +31,7 @@
"@react-oauth/google": "^0.12.1",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/react-query": "^5.66.0",
"@tanstack/react-table": "^8.21.2",
"axios": "^1.7.9",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",

View File

@ -71,6 +71,9 @@ dependencies:
'@tanstack/react-query':
specifier: ^5.66.0
version: 5.67.3(react@19.0.0)
'@tanstack/react-table':
specifier: ^8.21.2
version: 8.21.2(react-dom@19.0.0)(react@19.0.0)
axios:
specifier: ^1.7.9
version: 1.8.3
@ -1600,6 +1603,23 @@ packages:
react: 19.0.0
dev: false
/@tanstack/react-table@8.21.2(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-11tNlEDTdIhMJba2RBH+ecJ9l1zgS2kjmexDPAraulc8jeNA4xocSNeyzextT0XJyASil4XsCYlJmf5jEWAtYg==}
engines: {node: '>=12'}
peerDependencies:
react: '>=16.8'
react-dom: '>=16.8'
dependencies:
'@tanstack/table-core': 8.21.2
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
dev: false
/@tanstack/table-core@8.21.2:
resolution: {integrity: sha512-uvXk/U4cBiFMxt+p9/G7yUWI/UbHYbyghLCjlpWZ3mLeIZiUBSKcUnw9UnKkdRz7Z/N4UBuFLWQdJCjUe7HjvA==}
engines: {node: '>=12'}
dev: false
/@types/d3-array@3.2.1:
resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==}
dev: false