diff --git a/inventory-server/src/routes/products.js b/inventory-server/src/routes/products.js index 893a2a5..5f70a57 100755 --- a/inventory-server/src/routes/products.js +++ b/inventory-server/src/routes/products.js @@ -6,11 +6,74 @@ const { importProductsFromCSV } = require('../utils/csvImporter'); // Configure multer for file uploads const upload = multer({ dest: 'uploads/' }); -// Get all products +// Get all products with pagination, filtering, and sorting router.get('/', async (req, res) => { const pool = req.app.locals.pool; try { - const [rows] = await pool.query(` + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 50; + const offset = (page - 1) * limit; + const search = req.query.search || ''; + const category = req.query.category || 'all'; + const vendor = req.query.vendor || 'all'; + const stockStatus = req.query.stockStatus || 'all'; + const minPrice = parseFloat(req.query.minPrice) || 0; + const maxPrice = req.query.maxPrice ? parseFloat(req.query.maxPrice) : null; + const sortColumn = req.query.sortColumn || 'title'; + const sortDirection = req.query.sortDirection === 'desc' ? 'DESC' : 'ASC'; + + // Build the WHERE clause + const conditions = ['visible = true']; + const params = []; + + if (search) { + conditions.push('(title LIKE ? OR SKU LIKE ?)'); + params.push(`%${search}%`, `%${search}%`); + } + + if (category !== 'all') { + conditions.push('categories = ?'); + params.push(category); + } + + if (vendor !== 'all') { + conditions.push('vendor = ?'); + params.push(vendor); + } + + if (stockStatus !== 'all') { + switch (stockStatus) { + case 'out_of_stock': + conditions.push('stock_quantity = 0'); + break; + case 'low_stock': + conditions.push('stock_quantity > 0 AND stock_quantity <= 5'); + break; + case 'in_stock': + conditions.push('stock_quantity > 5'); + break; + } + } + + if (minPrice > 0) { + conditions.push('price >= ?'); + params.push(minPrice); + } + + if (maxPrice) { + conditions.push('price <= ?'); + params.push(maxPrice); + } + + // Get total count for pagination + const [countResult] = await pool.query( + `SELECT COUNT(*) as total FROM products WHERE ${conditions.join(' AND ')}`, + params + ); + const total = countResult[0].total; + + // Get paginated results + const query = ` SELECT product_id, title, @@ -23,12 +86,37 @@ router.get('/', async (req, res) => { brand, categories, visible, - managing_stock + managing_stock, + image FROM products - WHERE visible = true - ORDER BY title ASC - `); - res.json(rows); + WHERE ${conditions.join(' AND ')} + ORDER BY ${sortColumn} ${sortDirection} + LIMIT ? OFFSET ? + `; + + const [rows] = await pool.query(query, [...params, limit, offset]); + + // Get unique categories and vendors for filters + const [categories] = await pool.query( + 'SELECT DISTINCT categories FROM products WHERE visible = true AND categories IS NOT NULL AND categories != "" ORDER BY categories' + ); + const [vendors] = await pool.query( + 'SELECT DISTINCT vendor FROM products WHERE visible = true AND vendor IS NOT NULL AND vendor != "" ORDER BY vendor' + ); + + res.json({ + products: rows, + pagination: { + total, + pages: Math.ceil(total / limit), + currentPage: page, + limit + }, + filters: { + categories: categories.map(c => c.categories), + vendors: vendors.map(v => v.vendor) + } + }); } catch (error) { console.error('Error fetching products:', error); res.status(500).json({ error: 'Failed to fetch products' }); @@ -71,4 +159,63 @@ router.post('/import', upload.single('file'), async (req, res) => { } }); +// Update a product +router.put('/:id', async (req, res) => { + const pool = req.app.locals.pool; + try { + const { + title, + sku, + stock_quantity, + price, + regular_price, + cost_price, + vendor, + brand, + categories, + visible, + managing_stock + } = req.body; + + const [result] = await pool.query( + `UPDATE products + SET title = ?, + sku = ?, + stock_quantity = ?, + price = ?, + regular_price = ?, + cost_price = ?, + vendor = ?, + brand = ?, + categories = ?, + visible = ?, + managing_stock = ? + WHERE product_id = ?`, + [ + title, + sku, + stock_quantity, + price, + regular_price, + cost_price, + vendor, + brand, + categories, + visible, + managing_stock, + req.params.id + ] + ); + + if (result.affectedRows === 0) { + return res.status(404).json({ error: 'Product not found' }); + } + + res.json({ message: 'Product updated successfully' }); + } catch (error) { + console.error('Error updating product:', error); + res.status(500).json({ error: 'Failed to update product' }); + } +}); + module.exports = router; \ No newline at end of file diff --git a/inventory/package-lock.json b/inventory/package-lock.json index b68e209..acbde06 100644 --- a/inventory/package-lock.json +++ b/inventory/package-lock.json @@ -12,13 +12,18 @@ "@radix-ui/react-collapsible": "^1.1.2", "@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-dropdown-menu": "^2.1.4", + "@radix-ui/react-label": "^2.1.1", "@radix-ui/react-progress": "^1.1.1", + "@radix-ui/react-select": "^2.1.4", "@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-slot": "^1.1.1", + "@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.6", "@shadcn/ui": "^0.0.4", "@tanstack/react-query": "^5.63.0", + "@tanstack/react-virtual": "^3.11.2", + "@tanstack/virtual-core": "^3.11.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.469.0", @@ -27,7 +32,8 @@ "react-router-dom": "^7.1.1", "recharts": "^2.15.0", "tailwind-merge": "^2.6.0", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "tanstack": "^1.0.0" }, "devDependencies": { "@eslint/js": "^9.17.0", @@ -1148,6 +1154,12 @@ "node": ">=14" } }, + "node_modules/@radix-ui/number": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz", + "integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==", + "license": "MIT" + }, "node_modules/@radix-ui/primitive": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", @@ -1454,6 +1466,29 @@ } } }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.1.tgz", + "integrity": "sha512-UUw5E4e/2+4kFMH7+YxORXGWggtY6sM8WIwh5RZchhLuUg2H1hc98Py+pr8HMz6rdaYrK2t296ZEjYLOCO5uUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-menu": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.4.tgz", @@ -1652,6 +1687,49 @@ } } }, + "node_modules/@radix-ui/react-select": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.4.tgz", + "integrity": "sha512-pOkb2u8KgO47j/h7AylCj7dJsm69BXcjkrvTqMptFqsE2i0p8lHkfgneXKjAgPzBMivnoMyt8o4KiV4wYzDdyQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.3", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.1", + "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.1", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "^2.6.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-separator": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.1.tgz", @@ -1693,6 +1771,35 @@ } } }, + "node_modules/@radix-ui/react-switch": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.1.2.tgz", + "integrity": "sha512-zGukiWHjEdBCRyXvKR6iXAQG6qXm2esuAD6kDOi9Cn+1X6ev3ASo4+CsYaD6Fov9r/AQFekqnD/7+V0Cs6/98g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-tabs": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.2.tgz", @@ -1823,6 +1930,21 @@ } } }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz", + "integrity": "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-rect": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", @@ -2220,6 +2342,33 @@ "react": "^18 || ^19" } }, + "node_modules/@tanstack/react-virtual": { + "version": "3.11.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.11.2.tgz", + "integrity": "sha512-OuFzMXPF4+xZgx8UzJha0AieuMihhhaWG0tCqpp6tDzlFwOmNBPYMuLOtMJ1Tr4pXLHmgjcWhG6RlknY2oNTdQ==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.11.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.11.2", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.11.2.tgz", + "integrity": "sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -5820,6 +5969,12 @@ "tailwindcss": ">=3.0.0 || insiders" } }, + "node_modules/tanstack": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tanstack/-/tanstack-1.0.0.tgz", + "integrity": "sha512-BUpDmwGlWHk2F183Uu1+k85biSLrpSh/zA9ephJwmZ9ze+XDEw3JOyN9vhcbFqrQFrf5yuWImt+0Kn4fUNgzTg==", + "license": "ISC" + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", diff --git a/inventory/package.json b/inventory/package.json index 85733a7..3733d7f 100644 --- a/inventory/package.json +++ b/inventory/package.json @@ -14,13 +14,18 @@ "@radix-ui/react-collapsible": "^1.1.2", "@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-dropdown-menu": "^2.1.4", + "@radix-ui/react-label": "^2.1.1", "@radix-ui/react-progress": "^1.1.1", + "@radix-ui/react-select": "^2.1.4", "@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-slot": "^1.1.1", + "@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.6", "@shadcn/ui": "^0.0.4", "@tanstack/react-query": "^5.63.0", + "@tanstack/react-virtual": "^3.11.2", + "@tanstack/virtual-core": "^3.11.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.469.0", @@ -29,7 +34,8 @@ "react-router-dom": "^7.1.1", "recharts": "^2.15.0", "tailwind-merge": "^2.6.0", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "tanstack": "^1.0.0" }, "devDependencies": { "@eslint/js": "^9.17.0", diff --git a/inventory/src/components/products/ProductEditDialog.tsx b/inventory/src/components/products/ProductEditDialog.tsx new file mode 100644 index 0000000..ded17b6 --- /dev/null +++ b/inventory/src/components/products/ProductEditDialog.tsx @@ -0,0 +1,185 @@ +import { useState } from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, + DialogDescription, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; + +interface Product { + product_id: string; + title: string; + sku: string; + stock_quantity: number; + price: number; + regular_price: number; + cost_price: number; + vendor: string; + brand: string; + categories: string; + visible: boolean; + managing_stock: boolean; +} + +interface ProductEditDialogProps { + product: Product | null; + open: boolean; + onOpenChange: (open: boolean) => void; + onSave: (product: Product) => void; +} + +export function ProductEditDialog({ + product, + open, + onOpenChange, + onSave, +}: ProductEditDialogProps) { + const [formData, setFormData] = useState>(product || {}); + + const handleChange = (field: keyof Product, value: any) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (product && formData) { + onSave({ ...product, ...formData }); + } + onOpenChange(false); + }; + + if (!product) return null; + + return ( + + + + Edit Product - {product.title} + + Make changes to the product details below. + + +
+
+
+ + handleChange("title", e.target.value)} + /> +
+
+ + handleChange("sku", e.target.value)} + /> +
+
+ +
+
+ + handleChange("price", parseFloat(e.target.value))} + /> +
+
+ + handleChange("regular_price", parseFloat(e.target.value))} + /> +
+
+ + handleChange("cost_price", parseFloat(e.target.value))} + /> +
+
+ +
+
+ + handleChange("vendor", e.target.value)} + /> +
+
+ + handleChange("brand", e.target.value)} + /> +
+
+ +
+ + handleChange("categories", e.target.value)} + /> +
+ +
+
+ + handleChange("stock_quantity", parseInt(e.target.value))} + /> +
+
+ handleChange("managing_stock", checked)} + /> + +
+
+ +
+ handleChange("visible", checked)} + /> + +
+ + + + +
+
+
+ ); +} \ No newline at end of file diff --git a/inventory/src/components/products/ProductFilters.tsx b/inventory/src/components/products/ProductFilters.tsx new file mode 100644 index 0000000..999074c --- /dev/null +++ b/inventory/src/components/products/ProductFilters.tsx @@ -0,0 +1,130 @@ +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Badge } from "@/components/ui/badge"; +import { X } from "lucide-react"; + +interface ProductFilters { + search: string; + category: string; + vendor: string; + stockStatus: string; + minPrice: string; + maxPrice: string; +} + +interface ProductFiltersProps { + filters: ProductFilters; + categories: string[]; + vendors: string[]; + onFilterChange: (filters: Partial) => void; + onClearFilters: () => void; +} + +export function ProductFilters({ + filters, + categories, + vendors, + onFilterChange, + onClearFilters +}: ProductFiltersProps) { + const activeFilterCount = Object.values(filters).filter(Boolean).length; + + return ( +
+
+

Filters

+ {activeFilterCount > 0 && ( + + )} +
+
+
+ onFilterChange({ search: e.target.value })} + className="h-8 w-full" + /> +
+
+ + +
+
+ +
+ onFilterChange({ minPrice: e.target.value })} + className="h-8 w-full" + /> + - + onFilterChange({ maxPrice: e.target.value })} + className="h-8 w-full" + /> +
+
+
+
+ ); +} \ No newline at end of file diff --git a/inventory/src/components/products/ProductTable.tsx b/inventory/src/components/products/ProductTable.tsx new file mode 100644 index 0000000..9325aa7 --- /dev/null +++ b/inventory/src/components/products/ProductTable.tsx @@ -0,0 +1,201 @@ +import { ArrowUpDown } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; + +interface Product { + product_id: string; + title: string; + sku: string; + stock_quantity: number; + price: number; + regular_price: number; + cost_price: number; + vendor: string; + brand: string; + categories: string; + visible: boolean; + managing_stock: boolean; + image?: string; +} + +interface ProductTableProps { + products: Product[]; + onSort: (column: keyof Product) => void; + sortColumn: keyof Product | null; + sortDirection: 'asc' | 'desc'; +} + +export function ProductTable({ + products, + onSort, + sortColumn, + sortDirection, +}: ProductTableProps) { + const getSortIcon = (column: keyof Product) => { + if (sortColumn !== column) return ; + return ( + + ); + }; + + const getStockStatus = (quantity: number) => { + if (quantity === 0) { + return Out of Stock; + } + if (quantity <= 5) { + return Low Stock; + } + return In Stock; + }; + + const getProfitMargin = (price: number, cost: number) => { + if (!price || !cost) return 0; + return ((price - cost) / price) * 100; + }; + + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Status + + + + {products.map((product) => ( + + +
+ + + {product.title.charAt(0).toUpperCase()} + + {product.title} +
+
+ {product.sku} + +
+ {product.stock_quantity} + {getStockStatus(product.stock_quantity)} +
+
+ ${product.price.toFixed(2)} + ${product.regular_price.toFixed(2)} + ${product.cost_price.toFixed(2)} + {product.vendor || '-'} + {product.brand || '-'} + {product.categories || '-'} + + {product.visible ? ( + Active + ) : ( + Hidden + )} + +
+ ))} + {!products.length && ( + + + No products found + + + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/inventory/src/components/products/ProductTableSkeleton.tsx b/inventory/src/components/products/ProductTableSkeleton.tsx new file mode 100644 index 0000000..96659ab --- /dev/null +++ b/inventory/src/components/products/ProductTableSkeleton.tsx @@ -0,0 +1,58 @@ +import { Skeleton } from "@/components/ui/skeleton"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; + +export function ProductTableSkeleton() { + return ( +
+ + + + Product + SKU + Stock + Price + Regular Price + Cost + Vendor + Brand + Categories + Status + + + + {Array.from({ length: 20 }).map((_, i) => ( + + +
+ + +
+
+ + +
+ + +
+
+ + + + + + + +
+ ))} +
+
+
+ ); +} \ No newline at end of file diff --git a/inventory/src/components/ui/badge.tsx b/inventory/src/components/ui/badge.tsx new file mode 100644 index 0000000..e87d62b --- /dev/null +++ b/inventory/src/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/inventory/src/components/ui/dialog.tsx b/inventory/src/components/ui/dialog.tsx new file mode 100644 index 0000000..9dbeaa0 --- /dev/null +++ b/inventory/src/components/ui/dialog.tsx @@ -0,0 +1,120 @@ +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/inventory/src/components/ui/label.tsx b/inventory/src/components/ui/label.tsx new file mode 100644 index 0000000..683faa7 --- /dev/null +++ b/inventory/src/components/ui/label.tsx @@ -0,0 +1,24 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +) + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)) +Label.displayName = LabelPrimitive.Root.displayName + +export { Label } diff --git a/inventory/src/components/ui/pagination.tsx b/inventory/src/components/ui/pagination.tsx new file mode 100644 index 0000000..d331105 --- /dev/null +++ b/inventory/src/components/ui/pagination.tsx @@ -0,0 +1,117 @@ +import * as React from "react" +import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "@/lib/utils" +import { ButtonProps, buttonVariants } from "@/components/ui/button" + +const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => ( +