added vendors table
This commit is contained in:
225
components/tables/VendorsTable.tsx
Normal file
225
components/tables/VendorsTable.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
// VendorsTable.tsx
|
||||
'use client'
|
||||
|
||||
import { ColumnDef } from '@tanstack/react-table'
|
||||
import { DataTable } from './DataTable'
|
||||
import { Button } from '../ui/button'
|
||||
import DialogWrapper from '../dialogs/DialogWrapper'
|
||||
import CreateVendorForm from '../forms/CreateVendorForm'
|
||||
import { useState } from 'react'
|
||||
import { fetchVendorsClient } from '@/lib/helper/fetchVendors' // You'll need to create this
|
||||
import { Vendor, VendorType, VendorStatus } from '@prisma/client'
|
||||
import { Badge } from '../ui/badge' // You'll need Badge component
|
||||
|
||||
interface VendorRow {
|
||||
id: string
|
||||
name: string
|
||||
type: VendorType
|
||||
description?: string | null
|
||||
website?: string | null
|
||||
contactPerson?: string | null
|
||||
email?: string | null
|
||||
phone?: string | null
|
||||
status: VendorStatus
|
||||
isBooked: boolean
|
||||
bookedDate?: Date | null
|
||||
quotedPrice?: number | null
|
||||
finalCost?: number | null
|
||||
depositPaid?: number | null
|
||||
depositDueDate?: Date | null
|
||||
finalPaymentDue?: Date | null
|
||||
createdAt: Date
|
||||
}
|
||||
|
||||
interface Props {
|
||||
initialVendors: VendorRow[]
|
||||
}
|
||||
|
||||
// Format vendor type for display
|
||||
const formatVendorType = (type: VendorType): string => {
|
||||
return type.charAt(0) + type.slice(1).toLowerCase()
|
||||
}
|
||||
|
||||
// Format vendor status with colors
|
||||
const formatVendorStatus = (status: VendorStatus): { label: string, color: string } => {
|
||||
const colors: Record<VendorStatus, string> = {
|
||||
RESEARCHING: 'bg-gray-100 text-gray-800',
|
||||
CONTACTING: 'bg-blue-100 text-blue-800',
|
||||
RESPONDED: 'bg-yellow-100 text-yellow-800',
|
||||
PROPOSAL_RECEIVED: 'bg-purple-100 text-purple-800',
|
||||
NEGOTIATING: 'bg-orange-100 text-orange-800',
|
||||
CONTRACT_SENT: 'bg-indigo-100 text-indigo-800',
|
||||
CONTRACT_SIGNED: 'bg-green-100 text-green-800',
|
||||
DECLINED: 'bg-red-100 text-red-800',
|
||||
BACKUP: 'bg-slate-100 text-slate-800',
|
||||
}
|
||||
|
||||
const label = status.charAt(0) + status.slice(1).toLowerCase().replace('_', ' ')
|
||||
return { label, color: colors[status] }
|
||||
}
|
||||
|
||||
// Format currency
|
||||
const formatCurrency = (amount?: number | null): string => {
|
||||
if (!amount) return '—'
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
}).format(amount)
|
||||
}
|
||||
|
||||
// Format date
|
||||
const formatDate = (date?: Date | null): string => {
|
||||
if (!date) return '—'
|
||||
return new Date(date).toLocaleDateString()
|
||||
}
|
||||
|
||||
const columns: ColumnDef<VendorRow>[] = [
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: 'Name',
|
||||
cell: ({ row }) => (
|
||||
<div>
|
||||
<div className="font-medium">{row.original.name}</div>
|
||||
{row.original.contactPerson && (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{row.original.contactPerson}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'type',
|
||||
header: 'Type',
|
||||
cell: ({ row }) => (
|
||||
<Badge variant="outline" className="capitalize">
|
||||
{formatVendorType(row.original.type)}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'contact',
|
||||
header: 'Contact',
|
||||
cell: ({ row }) => (
|
||||
<div className="text-sm">
|
||||
{row.original.email && (
|
||||
<div className="truncate max-w-[180px]">{row.original.email}</div>
|
||||
)}
|
||||
{row.original.phone && (
|
||||
<div>{row.original.phone}</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: 'Status',
|
||||
cell: ({ row }) => {
|
||||
const status = formatVendorStatus(row.original.status)
|
||||
return (
|
||||
<Badge className={status.color}>
|
||||
{status.label}
|
||||
</Badge>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'isBooked',
|
||||
header: 'Booked',
|
||||
cell: ({ row }) => (
|
||||
<Badge variant={row.original.isBooked ? "default" : "outline"}>
|
||||
{row.original.isBooked ? 'Booked' : 'Not Booked'}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'cost',
|
||||
header: 'Cost',
|
||||
cell: ({ row }) => (
|
||||
<div className="text-sm">
|
||||
{row.original.finalCost ? (
|
||||
<div className="font-medium">{formatCurrency(row.original.finalCost)}</div>
|
||||
) : row.original.quotedPrice ? (
|
||||
<div className="font-medium">{formatCurrency(row.original.quotedPrice)}</div>
|
||||
) : (
|
||||
<div className="text-muted-foreground">—</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'deposit',
|
||||
header: 'Deposit',
|
||||
cell: ({ row }) => (
|
||||
<div className="text-sm">
|
||||
{row.original.depositPaid ? (
|
||||
<div>{formatCurrency(row.original.depositPaid)}</div>
|
||||
) : (
|
||||
<div className="text-muted-foreground">—</div>
|
||||
)}
|
||||
{row.original.depositDueDate && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Due: {formatDate(row.original.depositDueDate)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'createdAt',
|
||||
header: 'Added',
|
||||
cell: ({ row }) => formatDate(row.original.createdAt),
|
||||
},
|
||||
]
|
||||
|
||||
export default function VendorsTable({ initialVendors }: Props) {
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
||||
const [vendors, setVendors] = useState<VendorRow[]>(initialVendors)
|
||||
|
||||
async function refreshVendors() {
|
||||
try {
|
||||
const updated = await fetchVendorsClient()
|
||||
setVendors(updated)
|
||||
} catch (err) {
|
||||
console.error('Failed to refresh vendors:', err)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold tracking-tight">Vendors</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Manage your wedding vendors and suppliers
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => setIsDialogOpen(true)}
|
||||
>
|
||||
Add Vendor
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={vendors}
|
||||
/>
|
||||
|
||||
<DialogWrapper
|
||||
title="Add New Vendor"
|
||||
description="Enter the vendor information below"
|
||||
open={isDialogOpen}
|
||||
onOpenChange={setIsDialogOpen}
|
||||
form={
|
||||
<CreateVendorForm
|
||||
onSuccess={async () => {
|
||||
await refreshVendors()
|
||||
setIsDialogOpen(false)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user