From 422711ff3d2a6264e13928d78f34ef926dfa346c Mon Sep 17 00:00:00 2001 From: nafiuishaaq Date: Mon, 23 Feb 2026 13:52:42 +0100 Subject: [PATCH 1/5] implemented the components --- .../components/assets/condition-badge.tsx | 48 +++++++++++++++++++ frontend/components/assets/status-badge.tsx | 44 +++++++++++++++++ frontend/lib/api/assets.ts | 33 +++++++++++++ 3 files changed, 125 insertions(+) create mode 100644 frontend/components/assets/condition-badge.tsx create mode 100644 frontend/components/assets/status-badge.tsx diff --git a/frontend/components/assets/condition-badge.tsx b/frontend/components/assets/condition-badge.tsx new file mode 100644 index 0000000..a0a3f65 --- /dev/null +++ b/frontend/components/assets/condition-badge.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { AssetCondition } from "@/lib/query/types/asset"; + +const conditionConfig = { + [AssetCondition.NEW]: { + label: "New", + className: "bg-emerald-100 text-emerald-800 border-emerald-200", + }, + [AssetCondition.GOOD]: { + label: "Good", + className: "bg-green-100 text-green-800 border-green-200", + }, + [AssetCondition.FAIR]: { + label: "Fair", + className: "bg-blue-100 text-blue-800 border-blue-200", + }, + [AssetCondition.POOR]: { + label: "Poor", + className: "bg-orange-100 text-orange-800 border-orange-200", + }, + [AssetCondition.DAMAGED]: { + label: "Damaged", + className: "bg-red-100 text-red-800 border-red-200", + }, +}; + +interface ConditionBadgeProps { + condition: AssetCondition; +} + +export function ConditionBadge({ condition }: ConditionBadgeProps) { + const config = conditionConfig[condition]; + + if (!config) { + return ( + + {condition} + + ); + } + + return ( + + {config.label} + + ); +} diff --git a/frontend/components/assets/status-badge.tsx b/frontend/components/assets/status-badge.tsx new file mode 100644 index 0000000..e6c5508 --- /dev/null +++ b/frontend/components/assets/status-badge.tsx @@ -0,0 +1,44 @@ +"use client"; + +import { AssetStatus } from "@/lib/query/types/asset"; + +const statusConfig = { + [AssetStatus.ACTIVE]: { + label: "Active", + className: "bg-green-100 text-green-800 border-green-200", + }, + [AssetStatus.ASSIGNED]: { + label: "Assigned", + className: "bg-blue-100 text-blue-800 border-blue-200", + }, + [AssetStatus.MAINTENANCE]: { + label: "Maintenance", + className: "bg-yellow-100 text-yellow-800 border-yellow-200", + }, + [AssetStatus.RETIRED]: { + label: "Retired", + className: "bg-gray-100 text-gray-800 border-gray-200", + }, +}; + +interface StatusBadgeProps { + status: AssetStatus; +} + +export function StatusBadge({ status }: StatusBadgeProps) { + const config = statusConfig[status]; + + if (!config) { + return ( + + {status} + + ); + } + + return ( + + {config.label} + + ); +} diff --git a/frontend/lib/api/assets.ts b/frontend/lib/api/assets.ts index ec3a778..8eb8e9a 100644 --- a/frontend/lib/api/assets.ts +++ b/frontend/lib/api/assets.ts @@ -5,6 +5,7 @@ import { AssetHistoryEvent, AssetHistoryFilters, AssetNote, + AssetStatus, AssetUser, Category, CategoryWithCount, @@ -18,6 +19,38 @@ import { } from '@/lib/query/types/asset'; export const assetApiClient = { + getAssets(params?: { + page?: number; + limit?: number; + search?: string; + status?: AssetStatus; + sortBy?: string; + sortOrder?: 'asc' | 'desc'; + }): Promise<{ + assets: Asset[]; + total: number; + page: number; + limit: number; + totalPages: number; + }> { + const searchParams = new URLSearchParams(); + if (params) { + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + searchParams.append(key, String(value)); + } + }); + } + const qs = searchParams.toString(); + return apiClient.request<{ + assets: Asset[]; + total: number; + page: number; + limit: number; + totalPages: number; + }>(`/assets${qs ? `?${qs}` : ''}`); + }, + getAsset(id: string): Promise { return apiClient.request(`/assets/${id}`); }, From 64f87ddfb26590abd8d2b75ca50219f21181f075 Mon Sep 17 00:00:00 2001 From: nafiuishaaq Date: Mon, 23 Feb 2026 13:52:45 +0100 Subject: [PATCH 2/5] implemented the components --- frontend/lib/query/hooks/useAsset.ts | 31 ++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/frontend/lib/query/hooks/useAsset.ts b/frontend/lib/query/hooks/useAsset.ts index d2db574..7aa5579 100644 --- a/frontend/lib/query/hooks/useAsset.ts +++ b/frontend/lib/query/hooks/useAsset.ts @@ -11,6 +11,7 @@ import { Asset, AssetHistoryEvent, AssetDocument, + AssetStatus, MaintenanceRecord, AssetNote, UpdateAssetStatusInput, @@ -24,6 +25,36 @@ import { import { ApiError } from '../types'; // Queries +export function useAssets( + params?: { + page?: number; + limit?: number; + search?: string; + status?: AssetStatus; + sortBy?: string; + sortOrder?: 'asc' | 'desc'; + }, + options?: Omit, 'queryKey' | 'queryFn'> +) { + return useQuery<{ + assets: Asset[]; + total: number; + page: number; + limit: number; + totalPages: number; + }, ApiError>({ + queryKey: queryKeys.assets.list(params || {}), + queryFn: () => assetApiClient.getAssets(params), + ...options, + }); +} + export function useAsset( id: string, options?: Omit, 'queryKey' | 'queryFn'> From d6769a4d098429dd22b980e748aee3f77b27efdf Mon Sep 17 00:00:00 2001 From: nafiuishaaq Date: Mon, 23 Feb 2026 13:55:21 +0100 Subject: [PATCH 3/5] implemented the button components --- frontend/components/ui/button.tsx | 56 +++++++++++++++++++++++++++++++ frontend/lib/utils.ts | 6 ++++ 2 files changed, 62 insertions(+) create mode 100644 frontend/components/ui/button.tsx create mode 100644 frontend/lib/utils.ts diff --git a/frontend/components/ui/button.tsx b/frontend/components/ui/button.tsx new file mode 100644 index 0000000..0ba4277 --- /dev/null +++ b/frontend/components/ui/button.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/frontend/lib/utils.ts b/frontend/lib/utils.ts new file mode 100644 index 0000000..d084cca --- /dev/null +++ b/frontend/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} From 157612a665b05378746f41d71a2dcaa1f2847dee Mon Sep 17 00:00:00 2001 From: nafiuishaaq Date: Mon, 23 Feb 2026 13:55:27 +0100 Subject: [PATCH 4/5] implemented the button components --- frontend/components/ui/button-simple.tsx | 42 ++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 frontend/components/ui/button-simple.tsx diff --git a/frontend/components/ui/button-simple.tsx b/frontend/components/ui/button-simple.tsx new file mode 100644 index 0000000..68cbb3b --- /dev/null +++ b/frontend/components/ui/button-simple.tsx @@ -0,0 +1,42 @@ +import React from 'react'; + +export interface ButtonProps extends React.ButtonHTMLAttributes { + variant?: 'default' | 'outline' | 'destructive' | 'secondary' | 'ghost' | 'link'; + size?: 'default' | 'sm' | 'lg' | 'icon'; +} + +const Button = React.forwardRef( + ({ className = '', variant = 'default', size = 'default', ...props }, ref) => { + const baseClasses = 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50'; + + const variantClasses = { + default: 'bg-blue-600 text-white hover:bg-blue-700', + destructive: 'bg-red-600 text-white hover:bg-red-700', + outline: 'border border-gray-300 bg-white hover:bg-gray-50', + secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200', + ghost: 'hover:bg-gray-100', + link: 'text-blue-600 underline-offset-4 hover:underline', + }; + + const sizeClasses = { + default: 'h-10 px-4 py-2', + sm: 'h-9 rounded-md px-3', + lg: 'h-11 rounded-md px-8', + icon: 'h-10 w-10', + }; + + const classes = `${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${className}`; + + return ( + + + ); + } + + return ( +
+ {/* Header */} +
+
+
+

Assets

+

+ {data?.total || 0} total assets +

+
+ +
+
+ + {/* Filters */} +
+
+ {/* Search */} +
+
+ + setSearch(e.target.value)} + className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +
+
+ + {/* Status Filter */} +
+ +
+
+
+ + {/* Loading State */} + {isLoading && ( +
+
+
+

Loading assets...

+
+
+ )} + + {/* Table */} + {!isLoading && data && ( +
+ {data.assets.length === 0 ? ( +
+

+ {debouncedSearch || statusFilter + ? "No assets found matching your filters." + : "No assets registered yet."} +

+ {!debouncedSearch && !statusFilter && ( + + )} +
+ ) : ( + <> +
+ + + + + + + + + + + + + + {data.assets.map((asset) => ( + handleRowClick(asset.id)} + className="hover:bg-gray-50 cursor-pointer transition-colors" + > + + + + + + + + + ))} + +
+ + + + + + + + + + + + + +
+ {asset.assetId} + + {asset.name} + + {asset.category?.name || "—"} + + + + + + {asset.department?.name || "—"} + + {asset.assignedTo + ? `${asset.assignedTo.name}` + : "Unassigned"} +
+
+ + {/* Pagination */} + {data.totalPages > 1 && ( +
+
+
+ Showing {((currentPage - 1) * itemsPerPage) + 1} to{" "} + {Math.min(currentPage * itemsPerPage, data.total)} of{" "} + {data.total} results +
+
+ + + Page {currentPage} of {data.totalPages} + + +
+
+
+ )} + + )} +
+ )} +
+ ); +}