added vendors table

This commit is contained in:
2026-01-27 12:23:59 -05:00
parent ecd7182153
commit 3aa9b6f325
20 changed files with 1058 additions and 5 deletions

View 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>
)
}