Build inventory frontend and stock management features
- Full inventory UI: product list with search/filter, product detail with tabs (details, units, suppliers, stock receipts, price history) - Product filters: category, type (serialized/rental/repair), low stock, active/inactive — all server-side with URL-synced state - Product-supplier junction: link products to multiple suppliers with preferred flag, joined supplier details in UI - Stock receipts: record incoming stock with supplier, qty, cost per unit, invoice number; auto-increments qty_on_hand for non-serialized products - Price history tab on product detail page - categories/all endpoint to avoid pagination limit on dropdown fetches - categoryId filter on product list endpoint - Repair parts and additional inventory items in music store seed data - isDualUseRepair corrected: instruments set to false, strings/parts true - Product-supplier links and stock receipts in seed data - Price history seed data simulating cost increases over past year - 37 API tests covering categories, suppliers, products, units, product-suppliers, and stock receipts - alert-dialog and checkbox UI components - sync-and-deploy.sh script for rsync + remote deploy
This commit is contained in:
91
packages/admin/src/components/inventory/category-form.tsx
Normal file
91
packages/admin/src/components/inventory/category-form.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { categoryAllOptions } from '@/api/inventory'
|
||||
import type { Category } from '@/types/inventory'
|
||||
|
||||
interface Props {
|
||||
defaultValues?: Partial<Category>
|
||||
onSubmit: (data: Record<string, unknown>) => void
|
||||
onDelete?: () => void
|
||||
loading?: boolean
|
||||
deleteLoading?: boolean
|
||||
}
|
||||
|
||||
export function CategoryForm({ defaultValues, onSubmit, onDelete, loading, deleteLoading }: Props) {
|
||||
const { data: allCats } = useQuery(categoryAllOptions())
|
||||
const categories = (allCats?.data ?? []).filter((c) => c.id !== defaultValues?.id && c.isActive)
|
||||
|
||||
const { register, handleSubmit, setValue, watch } = useForm({
|
||||
defaultValues: {
|
||||
name: defaultValues?.name ?? '',
|
||||
parentId: defaultValues?.parentId ?? '',
|
||||
sortOrder: defaultValues?.sortOrder ?? 0,
|
||||
isActive: defaultValues?.isActive ?? true,
|
||||
},
|
||||
})
|
||||
|
||||
const parentId = watch('parentId')
|
||||
const isActive = watch('isActive')
|
||||
|
||||
function handleFormSubmit(data: { name: string; parentId: string; sortOrder: number; isActive: boolean }) {
|
||||
onSubmit({
|
||||
name: data.name,
|
||||
parentId: data.parentId || undefined,
|
||||
sortOrder: Number(data.sortOrder),
|
||||
isActive: data.isActive,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="cat-name">Name *</Label>
|
||||
<Input id="cat-name" {...register('name')} placeholder="e.g. Guitars, Accessories" required />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Parent Category</Label>
|
||||
<Select value={parentId || 'none'} onValueChange={(v) => setValue('parentId', v === 'none' ? '' : v)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="None (Top Level)" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">None (Top Level)</SelectItem>
|
||||
{categories.map((c) => (
|
||||
<SelectItem key={c.id} value={c.id}>{c.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="cat-order">Sort Order</Label>
|
||||
<Input id="cat-order" type="number" min={0} {...register('sortOrder')} />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 pt-7">
|
||||
<input
|
||||
id="cat-active"
|
||||
type="checkbox"
|
||||
checked={isActive}
|
||||
onChange={(e) => setValue('isActive', e.target.checked)}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<Label htmlFor="cat-active">Active</Label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Button type="submit" disabled={loading} className="flex-1">
|
||||
{loading ? 'Saving...' : defaultValues?.id ? 'Save Changes' : 'Create Category'}
|
||||
</Button>
|
||||
{onDelete && (
|
||||
<Button type="button" variant="destructive" disabled={deleteLoading} onClick={onDelete}>
|
||||
{deleteLoading ? 'Deleting...' : 'Delete'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
118
packages/admin/src/components/inventory/inventory-unit-form.tsx
Normal file
118
packages/admin/src/components/inventory/inventory-unit-form.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import type { InventoryUnit, UnitCondition, UnitStatus } from '@/types/inventory'
|
||||
|
||||
const CONDITIONS: { value: UnitCondition; label: string }[] = [
|
||||
{ value: 'new', label: 'New' },
|
||||
{ value: 'excellent', label: 'Excellent' },
|
||||
{ value: 'good', label: 'Good' },
|
||||
{ value: 'fair', label: 'Fair' },
|
||||
{ value: 'poor', label: 'Poor' },
|
||||
]
|
||||
|
||||
const STATUSES: { value: UnitStatus; label: string }[] = [
|
||||
{ value: 'available', label: 'Available' },
|
||||
{ value: 'sold', label: 'Sold' },
|
||||
{ value: 'rented', label: 'Rented' },
|
||||
{ value: 'on_trial', label: 'On Trial' },
|
||||
{ value: 'in_repair', label: 'In Repair' },
|
||||
{ value: 'layaway', label: 'Layaway' },
|
||||
{ value: 'lost', label: 'Lost' },
|
||||
{ value: 'retired', label: 'Retired' },
|
||||
]
|
||||
|
||||
interface Props {
|
||||
defaultValues?: Partial<InventoryUnit>
|
||||
onSubmit: (data: Record<string, unknown>) => void
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
export function InventoryUnitForm({ defaultValues, onSubmit, loading }: Props) {
|
||||
const { register, handleSubmit, setValue, watch } = useForm({
|
||||
defaultValues: {
|
||||
serialNumber: defaultValues?.serialNumber ?? '',
|
||||
condition: (defaultValues?.condition ?? 'new') as UnitCondition,
|
||||
status: (defaultValues?.status ?? 'available') as UnitStatus,
|
||||
purchaseDate: defaultValues?.purchaseDate ?? '',
|
||||
purchaseCost: defaultValues?.purchaseCost ?? '',
|
||||
notes: defaultValues?.notes ?? '',
|
||||
},
|
||||
})
|
||||
|
||||
const condition = watch('condition')
|
||||
const status = watch('status')
|
||||
|
||||
function handleFormSubmit(data: Record<string, unknown>) {
|
||||
onSubmit({
|
||||
serialNumber: (data.serialNumber as string) || undefined,
|
||||
condition: data.condition,
|
||||
status: data.status,
|
||||
purchaseDate: (data.purchaseDate as string) || undefined,
|
||||
purchaseCost: (data.purchaseCost as string) || undefined,
|
||||
notes: (data.notes as string) || undefined,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="unit-serial">Serial Number</Label>
|
||||
<Input id="unit-serial" {...register('serialNumber')} placeholder="e.g. US22041234" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Condition</Label>
|
||||
<Select value={condition} onValueChange={(v) => setValue('condition', v as UnitCondition)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{CONDITIONS.map((c) => (
|
||||
<SelectItem key={c.value} value={c.value}>{c.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Status</Label>
|
||||
<Select value={status} onValueChange={(v) => setValue('status', v as UnitStatus)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{STATUSES.map((s) => (
|
||||
<SelectItem key={s.value} value={s.value}>{s.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="unit-date">Purchase Date</Label>
|
||||
<Input id="unit-date" type="date" {...register('purchaseDate')} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="unit-cost">Purchase Cost</Label>
|
||||
<Input id="unit-cost" type="number" step="0.01" min="0" {...register('purchaseCost')} placeholder="0.00" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="unit-notes">Notes</Label>
|
||||
<textarea
|
||||
id="unit-notes"
|
||||
{...register('notes')}
|
||||
rows={2}
|
||||
placeholder="Any notes about this unit..."
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring resize-none"
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" disabled={loading} className="w-full">
|
||||
{loading ? 'Saving...' : defaultValues?.id ? 'Save Changes' : 'Add Unit'}
|
||||
</Button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
172
packages/admin/src/components/inventory/product-form.tsx
Normal file
172
packages/admin/src/components/inventory/product-form.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { categoryAllOptions } from '@/api/inventory'
|
||||
import type { Product } from '@/types/inventory'
|
||||
|
||||
interface Props {
|
||||
defaultValues?: Partial<Product>
|
||||
onSubmit: (data: Record<string, unknown>) => void
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
export function ProductForm({ defaultValues, onSubmit, loading }: Props) {
|
||||
const { data: allCats } = useQuery(categoryAllOptions())
|
||||
const categories = (allCats?.data ?? []).filter((c) => c.isActive)
|
||||
|
||||
const { register, handleSubmit, setValue, watch } = useForm({
|
||||
defaultValues: {
|
||||
name: defaultValues?.name ?? '',
|
||||
sku: defaultValues?.sku ?? '',
|
||||
upc: defaultValues?.upc ?? '',
|
||||
brand: defaultValues?.brand ?? '',
|
||||
model: defaultValues?.model ?? '',
|
||||
description: defaultValues?.description ?? '',
|
||||
categoryId: defaultValues?.categoryId ?? '',
|
||||
price: defaultValues?.price ?? '',
|
||||
minPrice: defaultValues?.minPrice ?? '',
|
||||
rentalRateMonthly: defaultValues?.rentalRateMonthly ?? '',
|
||||
qtyOnHand: defaultValues?.qtyOnHand ?? 0,
|
||||
qtyReorderPoint: defaultValues?.qtyReorderPoint ?? '',
|
||||
isSerialized: defaultValues?.isSerialized ?? false,
|
||||
isRental: defaultValues?.isRental ?? false,
|
||||
isDualUseRepair: defaultValues?.isDualUseRepair ?? false,
|
||||
isActive: defaultValues?.isActive ?? true,
|
||||
},
|
||||
})
|
||||
|
||||
const categoryId = watch('categoryId')
|
||||
const isRental = watch('isRental')
|
||||
const isSerialized = watch('isSerialized')
|
||||
const isDualUseRepair = watch('isDualUseRepair')
|
||||
const isActive = watch('isActive')
|
||||
|
||||
function handleFormSubmit(data: Record<string, unknown>) {
|
||||
onSubmit({
|
||||
name: data.name,
|
||||
sku: (data.sku as string) || undefined,
|
||||
upc: (data.upc as string) || undefined,
|
||||
brand: (data.brand as string) || undefined,
|
||||
model: (data.model as string) || undefined,
|
||||
description: (data.description as string) || undefined,
|
||||
categoryId: (data.categoryId as string) || undefined,
|
||||
price: (data.price as string) ? Number(data.price) : undefined,
|
||||
minPrice: (data.minPrice as string) ? Number(data.minPrice) : undefined,
|
||||
rentalRateMonthly: isRental && (data.rentalRateMonthly as string) ? Number(data.rentalRateMonthly) : undefined,
|
||||
qtyOnHand: Number(data.qtyOnHand),
|
||||
qtyReorderPoint: (data.qtyReorderPoint as string) ? Number(data.qtyReorderPoint) : undefined,
|
||||
isSerialized: data.isSerialized,
|
||||
isRental: data.isRental,
|
||||
isDualUseRepair: data.isDualUseRepair,
|
||||
isActive: data.isActive,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="p-name">Name *</Label>
|
||||
<Input id="p-name" {...register('name')} placeholder="e.g. Fender Player Stratocaster" required />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="p-sku">SKU</Label>
|
||||
<Input id="p-sku" {...register('sku')} placeholder="STR-001" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="p-upc">UPC / Barcode</Label>
|
||||
<Input id="p-upc" {...register('upc')} placeholder="0123456789" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="p-brand">Brand</Label>
|
||||
<Input id="p-brand" {...register('brand')} placeholder="Fender" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="p-model">Model</Label>
|
||||
<Input id="p-model" {...register('model')} placeholder="Player Stratocaster" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Category</Label>
|
||||
<Select value={categoryId || 'none'} onValueChange={(v) => setValue('categoryId', v === 'none' ? '' : v)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select category..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">No Category</SelectItem>
|
||||
{categories.map((c) => (
|
||||
<SelectItem key={c.id} value={c.id}>{c.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="p-price">Price</Label>
|
||||
<Input id="p-price" type="number" step="0.01" min="0" {...register('price')} placeholder="0.00" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="p-min-price">Min Price</Label>
|
||||
<Input id="p-min-price" type="number" step="0.01" min="0" {...register('minPrice')} placeholder="0.00" />
|
||||
</div>
|
||||
{isRental && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="p-rental-rate">Rental / Month</Label>
|
||||
<Input id="p-rental-rate" type="number" step="0.01" min="0" {...register('rentalRateMonthly')} placeholder="0.00" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isSerialized && (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="p-qty">Qty On Hand</Label>
|
||||
<Input id="p-qty" type="number" min="0" {...register('qtyOnHand')} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="p-reorder">Reorder Point</Label>
|
||||
<Input id="p-reorder" type="number" min="0" {...register('qtyReorderPoint')} placeholder="—" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="p-desc">Description</Label>
|
||||
<textarea
|
||||
id="p-desc"
|
||||
{...register('description')}
|
||||
rows={3}
|
||||
placeholder="Product description..."
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring resize-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium">Options</Label>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<input type="checkbox" checked={isSerialized} onChange={(e) => setValue('isSerialized', e.target.checked)} className="h-4 w-4" />
|
||||
Serialized (track individual units)
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<input type="checkbox" checked={isRental} onChange={(e) => setValue('isRental', e.target.checked)} className="h-4 w-4" />
|
||||
Available for Rental
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<input type="checkbox" checked={isDualUseRepair} onChange={(e) => setValue('isDualUseRepair', e.target.checked)} className="h-4 w-4" />
|
||||
Available as Repair Line Item
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<input type="checkbox" checked={isActive} onChange={(e) => setValue('isActive', e.target.checked)} className="h-4 w-4" />
|
||||
Active
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="submit" disabled={loading} className="w-full">
|
||||
{loading ? 'Saving...' : defaultValues?.id ? 'Save Changes' : 'Create Product'}
|
||||
</Button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
88
packages/admin/src/components/inventory/supplier-form.tsx
Normal file
88
packages/admin/src/components/inventory/supplier-form.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import type { Supplier } from '@/types/inventory'
|
||||
|
||||
interface Props {
|
||||
defaultValues?: Partial<Supplier>
|
||||
onSubmit: (data: Record<string, unknown>) => void
|
||||
onDelete?: () => void
|
||||
loading?: boolean
|
||||
deleteLoading?: boolean
|
||||
}
|
||||
|
||||
export function SupplierForm({ defaultValues, onSubmit, onDelete, loading, deleteLoading }: Props) {
|
||||
const { register, handleSubmit } = useForm({
|
||||
defaultValues: {
|
||||
name: defaultValues?.name ?? '',
|
||||
contactName: defaultValues?.contactName ?? '',
|
||||
email: defaultValues?.email ?? '',
|
||||
phone: defaultValues?.phone ?? '',
|
||||
website: defaultValues?.website ?? '',
|
||||
accountNumber: defaultValues?.accountNumber ?? '',
|
||||
paymentTerms: defaultValues?.paymentTerms ?? '',
|
||||
},
|
||||
})
|
||||
|
||||
function handleFormSubmit(data: Record<string, string>) {
|
||||
onSubmit({
|
||||
name: data.name,
|
||||
contactName: data.contactName || undefined,
|
||||
email: data.email || undefined,
|
||||
phone: data.phone || undefined,
|
||||
website: data.website || undefined,
|
||||
accountNumber: data.accountNumber || undefined,
|
||||
paymentTerms: data.paymentTerms || undefined,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="sup-name">Name *</Label>
|
||||
<Input id="sup-name" {...register('name')} placeholder="e.g. Fender Musical Instruments" required />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="sup-contact">Contact Name</Label>
|
||||
<Input id="sup-contact" {...register('contactName')} placeholder="Jane Smith" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="sup-email">Email</Label>
|
||||
<Input id="sup-email" type="email" {...register('email')} placeholder="orders@supplier.com" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="sup-phone">Phone</Label>
|
||||
<Input id="sup-phone" {...register('phone')} placeholder="555-0100" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="sup-website">Website</Label>
|
||||
<Input id="sup-website" {...register('website')} placeholder="https://supplier.com" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="sup-acct">Account Number</Label>
|
||||
<Input id="sup-acct" {...register('accountNumber')} placeholder="ACC-12345" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="sup-terms">Payment Terms</Label>
|
||||
<Input id="sup-terms" {...register('paymentTerms')} placeholder="Net 30" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Button type="submit" disabled={loading} className="flex-1">
|
||||
{loading ? 'Saving...' : defaultValues?.id ? 'Save Changes' : 'Create Supplier'}
|
||||
</Button>
|
||||
{onDelete && (
|
||||
<Button type="button" variant="destructive" disabled={deleteLoading} onClick={onDelete}>
|
||||
{deleteLoading ? 'Deleting...' : 'Delete'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
142
packages/admin/src/components/ui/alert-dialog.tsx
Normal file
142
packages/admin/src/components/ui/alert-dialog.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import * as React from "react"
|
||||
import { AlertDialog as AlertDialogPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
|
||||
function AlertDialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
||||
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
|
||||
}
|
||||
|
||||
function AlertDialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
||||
return <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
||||
}
|
||||
|
||||
function AlertDialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
||||
return <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
||||
}
|
||||
|
||||
function AlertDialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
data-slot="alert-dialog-overlay"
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
|
||||
return (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
data-slot="alert-dialog-content"
|
||||
className={cn(
|
||||
"fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-footer"
|
||||
className={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Title
|
||||
data-slot="alert-dialog-title"
|
||||
className={cn("text-lg font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Description
|
||||
data-slot="alert-dialog-description"
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogAction({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Action
|
||||
className={cn(buttonVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogCancel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
className={cn(buttonVariants({ variant: "outline" }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogPortal,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogFooter,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
}
|
||||
30
packages/admin/src/components/ui/checkbox.tsx
Normal file
30
packages/admin/src/components/ui/checkbox.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import * as React from "react"
|
||||
import { Checkbox as CheckboxPrimitive } from "radix-ui"
|
||||
import { CheckIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Checkbox({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"peer size-4 shrink-0 rounded-[4px] border border-input shadow-xs transition-shadow outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary dark:bg-input/30 dark:data-[state=checked]:bg-primary",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className="flex items-center justify-center text-current transition-none"
|
||||
>
|
||||
<CheckIcon className="size-3.5" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Checkbox }
|
||||
Reference in New Issue
Block a user