@@ -204,6 +211,7 @@ export default function VendorsTable({ initialVendors }: Props) {
{
- await refreshVendors()
- setIsDialogOpen(false)
- }}
- />
+ {
+ await refreshVendors()
+ setIsDialogOpen(false)
+ }}
+ />
}
/>
diff --git a/components/vendor/ContactCard.tsx b/components/vendor/ContactCard.tsx
new file mode 100644
index 0000000..0c207cd
--- /dev/null
+++ b/components/vendor/ContactCard.tsx
@@ -0,0 +1,105 @@
+// components/vendors/ContactCard.tsx
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import { Separator } from '@/components/ui/separator'
+import { Globe, Mail, MapPin, Phone, User } from 'lucide-react'
+
+interface Address {
+ street: string
+ city: string
+ state: string
+ zip: number
+}
+
+interface ContactCardProps {
+ contactPerson?: string | null
+ email?: string | null
+ phone?: string | null
+ website?: string | null
+ address?: Address | null
+}
+
+export function ContactCard({ contactPerson, email, phone, website, address }: ContactCardProps) {
+ return (
+
+
+ Contact Information
+
+
+
+ {contactPerson && (
+
+
+
+
Contact Person
+
{contactPerson}
+
+
+ )}
+
+ {email && (
+
+ )}
+
+ {phone && (
+
+ )}
+
+ {website && (
+
+ )}
+
+
+ {address && (
+ <>
+
+
+
+
+
Address
+
+ {address.street}
+ {address.city}, {address.state} {address.zip}
+
+
+
+ >
+ )}
+
+
+ )
+}
\ No newline at end of file
diff --git a/components/vendor/EditVendorModal.tsx b/components/vendor/EditVendorModal.tsx
new file mode 100644
index 0000000..dc0e7e2
--- /dev/null
+++ b/components/vendor/EditVendorModal.tsx
@@ -0,0 +1,441 @@
+// components/vendors/EditVendorModal.tsx
+'use client'
+
+import { useState } from 'react'
+import { Vendor, VendorType, VendorStatus } from '@prisma/client'
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import { Label } from '@/components/ui/label'
+import { Textarea } from '@/components/ui/textarea'
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
+import { Checkbox } from '@/components/ui/checkbox'
+import { toast } from 'sonner'
+
+interface EditVendorModalProps {
+ vendor: {
+ id: string
+ name: string
+ type: VendorType
+ description?: string | null
+ website?: string | null
+ contactPerson?: string | null
+ email?: string | null
+ phone?: string | null
+ address?: {
+ street: string
+ city: string
+ state: string
+ zip: number
+ } | null
+ status: VendorStatus
+ isBooked: boolean
+ bookedDate?: Date | null
+ quotedPrice?: number | null
+ finalCost?: number | null
+ depositPaid?: number | null
+ depositDueDate?: Date | null
+ finalPaymentDue?: Date | null
+ paymentNotes?: string | null
+ contractUrl?: string | null
+ proposalUrl?: string | null
+ notes?: string | null
+ }
+ isOpen: boolean
+ onClose: () => void
+ onSave: (updatedVendor: any) => Promise
+}
+
+const VENDOR_TYPES = Object.values(VendorType)
+const VENDOR_STATUSES = Object.values(VendorStatus)
+
+export function EditVendorModal({ vendor, isOpen, onClose, onSave }: EditVendorModalProps) {
+ const [loading, setLoading] = useState(false)
+ const [formData, setFormData] = useState({
+ name: vendor.name,
+ type: vendor.type,
+ description: vendor.description || '',
+ website: vendor.website || '',
+ contactPerson: vendor.contactPerson || '',
+ email: vendor.email || '',
+ phone: vendor.phone || '',
+ street: vendor.address?.street || '',
+ city: vendor.address?.city || '',
+ state: vendor.address?.state || '',
+ postalCode: vendor.address?.zip?.toString() || '',
+ status: vendor.status,
+ isBooked: vendor.isBooked,
+ bookedDate: vendor.bookedDate ? new Date(vendor.bookedDate).toISOString().split('T')[0] : '',
+ quotedPrice: vendor.quotedPrice?.toString() || '',
+ finalCost: vendor.finalCost?.toString() || '',
+ depositPaid: vendor.depositPaid?.toString() || '',
+ depositDueDate: vendor.depositDueDate ? new Date(vendor.depositDueDate).toISOString().split('T')[0] : '',
+ finalPaymentDue: vendor.finalPaymentDue ? new Date(vendor.finalPaymentDue).toISOString().split('T')[0] : '',
+ paymentNotes: vendor.paymentNotes || '',
+ contractUrl: vendor.contractUrl || '',
+ proposalUrl: vendor.proposalUrl || '',
+ notes: vendor.notes || '',
+ })
+
+ const handleChange = (e: React.ChangeEvent) => {
+ const { name, value, type } = e.target
+ setFormData(prev => ({
+ ...prev,
+ [name]: type === 'checkbox' ? (e.target as HTMLInputElement).checked : value
+ }))
+ }
+
+ const handleSelectChange = (name: string, value: string) => {
+ setFormData(prev => ({ ...prev, [name]: value }))
+ }
+
+ const handleCheckboxChange = (name: string, checked: boolean) => {
+ setFormData(prev => ({ ...prev, [name]: checked }))
+ }
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setLoading(true)
+
+ try {
+ const response = await fetch(`/api/vendors/${vendor.id}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(formData),
+ })
+
+ if (!response.ok) throw new Error('Failed to update vendor')
+
+ const updatedVendor = await response.json()
+ await onSave(updatedVendor)
+ toast.success('Vendor updated successfully!')
+ onClose()
+ } catch (error) {
+ console.error('Error updating vendor:', error)
+ toast.error('Failed to update vendor')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+ )
+}
\ No newline at end of file
diff --git a/components/vendor/FinancialCard.tsx b/components/vendor/FinancialCard.tsx
new file mode 100644
index 0000000..0a76e74
--- /dev/null
+++ b/components/vendor/FinancialCard.tsx
@@ -0,0 +1,99 @@
+// components/vendors/FinancialCard.tsx
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import { Separator } from '@/components/ui/separator'
+import { Calendar } from 'lucide-react'
+
+interface FinancialCardProps {
+ quotedPrice?: number | null
+ finalCost?: number | null
+ depositPaid?: number | null
+ depositDueDate?: Date | null
+ finalPaymentDue?: Date | null
+ paymentNotes?: string | null
+}
+
+const formatCurrency = (amount?: number | null): string => {
+ if (!amount) return '—'
+ return new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ }).format(amount)
+}
+
+const formatDate = (date?: Date | null): string => {
+ if (!date) return '—'
+ return new Date(date).toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ })
+}
+
+export function FinancialCard({
+ quotedPrice,
+ finalCost,
+ depositPaid,
+ depositDueDate,
+ finalPaymentDue,
+ paymentNotes,
+}: FinancialCardProps) {
+ return (
+
+
+ Financial Information
+
+
+
+
+
Quoted Price
+
{formatCurrency(quotedPrice)}
+
+
+
Final Cost
+
{formatCurrency(finalCost)}
+
+
+
Deposit Paid
+
{formatCurrency(depositPaid)}
+
+
+
+ {(depositDueDate || finalPaymentDue) && (
+ <>
+
+
+ {depositDueDate && (
+
+
+
+
Deposit Due Date
+
{formatDate(depositDueDate)}
+
+
+ )}
+ {finalPaymentDue && (
+
+
+
+
Final Payment Due
+
{formatDate(finalPaymentDue)}
+
+
+ )}
+
+ >
+ )}
+
+ {paymentNotes && (
+ <>
+
+
+
Payment Notes
+
{paymentNotes}
+
+ >
+ )}
+
+
+ )
+}
\ No newline at end of file
diff --git a/components/vendor/NotesCard.tsx b/components/vendor/NotesCard.tsx
new file mode 100644
index 0000000..ad5125e
--- /dev/null
+++ b/components/vendor/NotesCard.tsx
@@ -0,0 +1,54 @@
+// components/vendors/NotesCard.tsx
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import { Button } from '@/components/ui/button'
+import { FileText } from 'lucide-react'
+
+interface NotesCardProps {
+ notes?: string | null
+ contractUrl?: string | null
+ proposalUrl?: string | null
+}
+
+export function NotesCard({ notes, contractUrl, proposalUrl }: NotesCardProps) {
+ if (!notes && !contractUrl && !proposalUrl) return null
+
+ return (
+
+
+ Notes & Documents
+
+
+ {notes && (
+
+ )}
+
+ {(contractUrl || proposalUrl) && (
+
+ )}
+
+
+ )
+}
\ No newline at end of file
diff --git a/components/vendor/SidebarCard.tsx b/components/vendor/SidebarCard.tsx
new file mode 100644
index 0000000..8b743a6
--- /dev/null
+++ b/components/vendor/SidebarCard.tsx
@@ -0,0 +1,145 @@
+// components/vendors/SidebarCards.tsx
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
+import { Button } from '@/components/ui/button'
+import Link from 'next/link'
+
+interface SidebarCardsProps {
+ description?: string | null
+ events: Array<{
+ id: string
+ name: string
+ date: Date | null
+ }>
+ timeline: {
+ createdAt: Date
+ bookedDate?: Date | null
+ depositDueDate?: Date | null
+ finalPaymentDue?: Date | null
+ }
+ vendorId: string
+}
+
+const formatDate = (date?: Date | null): string => {
+ if (!date) return '—'
+ return new Date(date).toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ })
+}
+
+export function SidebarCards({ description, events, timeline, vendorId }: SidebarCardsProps) {
+ return (
+
+ {/* Description */}
+ {description && (
+
+
+ Description
+
+
+ {description}
+
+
+ )}
+
+ {/* Associated Events */}
+ {events.length > 0 && (
+
+
+ Associated Events
+
+ {events.length} event{events.length !== 1 ? 's' : ''}
+
+
+
+
+ {events.map((event) => (
+
+
+
{event.name}
+ {event.date && (
+
+ {formatDate(event.date)}
+
+ )}
+
+
+ ))}
+
+
+
+ )}
+
+ {/* Status Timeline */}
+
+
+ Status Timeline
+
+
+
+
+ Created
+
+ {formatDate(timeline.createdAt)}
+
+
+ {timeline.bookedDate && (
+
+ Booked
+
+ {formatDate(timeline.bookedDate)}
+
+
+ )}
+ {timeline.depositDueDate && (
+
+ Deposit Due
+
+ {formatDate(timeline.depositDueDate)}
+
+
+ )}
+ {timeline.finalPaymentDue && (
+
+ Final Payment Due
+
+ {formatDate(timeline.finalPaymentDue)}
+
+
+ )}
+
+
+
+
+ {/* Quick Actions */}
+
+
+ Quick Actions
+
+
+
+
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/components/vendor/VendorDetailPage.tsx b/components/vendor/VendorDetailPage.tsx
new file mode 100644
index 0000000..305dd54
--- /dev/null
+++ b/components/vendor/VendorDetailPage.tsx
@@ -0,0 +1,97 @@
+// components/vendors/VendorDetailPage.tsx
+'use client'
+
+import { useState } from 'react'
+import { Vendor, VendorType, VendorStatus } from '@prisma/client'
+import { VendorHeader } from './VendorHeader'
+import { ContactCard } from './ContactCard'
+import { FinancialCard } from './FinancialCard'
+import { NotesCard } from './NotesCard'
+import { SidebarCards } from './SidebarCard'
+import { EditVendorModal } from './EditVendorModal'
+
+interface VendorWithEvents extends Vendor {
+ address: {
+ street: string
+ city: string
+ state: string
+ zip: number
+ } | null
+ events: Array<{
+ id: string
+ name: string
+ date: Date | null
+ }>
+}
+
+interface VendorDetailPageProps {
+ vendor: VendorWithEvents
+}
+
+export function VendorDetailPage({ vendor }: VendorDetailPageProps) {
+ const [isEditModalOpen, setIsEditModalOpen] = useState(false)
+ const [currentVendor, setCurrentVendor] = useState(vendor)
+
+ const handleSave = async (updatedVendor: any) => {
+ setCurrentVendor(updatedVendor)
+ }
+
+ return (
+ <>
+
+
setIsEditModalOpen(true)}
+ />
+
+
+ {/* Left Column - Main Info */}
+
+
+
+
+
+
+
+
+ {/* Right Column - Sidebar */}
+
+
+
+
+ setIsEditModalOpen(false)}
+ onSave={handleSave}
+ />
+ >
+ )
+}
\ No newline at end of file
diff --git a/components/vendor/VendorHeader.tsx b/components/vendor/VendorHeader.tsx
new file mode 100644
index 0000000..071cc61
--- /dev/null
+++ b/components/vendor/VendorHeader.tsx
@@ -0,0 +1,67 @@
+// components/vendors/VendorHeader.tsx
+import { Vendor, VendorType, VendorStatus } from '@prisma/client'
+import { Badge } from '@/components/ui/badge'
+import { Button } from '@/components/ui/button'
+import Link from 'next/link'
+
+interface VendorHeaderProps {
+ vendor: {
+ id: string
+ name: string
+ type: VendorType
+ status: VendorStatus
+ isBooked: boolean
+ createdAt: Date
+ }
+ onEditClick: () => void
+}
+
+const formatVendorType = (type: VendorType): string => {
+ return type.charAt(0) + type.slice(1).toLowerCase()
+}
+
+const formatStatus = (status: VendorStatus): { label: string, color: string } => {
+ const statusMap: Record = {
+ RESEARCHING: { label: 'Researching', color: 'bg-gray-100 text-gray-800' },
+ CONTACTING: { label: 'Contacting', color: 'bg-blue-100 text-blue-800' },
+ RESPONDED: { label: 'Responded', color: 'bg-yellow-100 text-yellow-800' },
+ PROPOSAL_RECEIVED: { label: 'Proposal Received', color: 'bg-purple-100 text-purple-800' },
+ NEGOTIATING: { label: 'Negotiating', color: 'bg-orange-100 text-orange-800' },
+ CONTRACT_SENT: { label: 'Contract Sent', color: 'bg-indigo-100 text-indigo-800' },
+ CONTRACT_SIGNED: { label: 'Contract Signed', color: 'bg-green-100 text-green-800' },
+ DECLINED: { label: 'Declined', color: 'bg-red-100 text-red-800' },
+ BACKUP: { label: 'Backup', color: 'bg-slate-100 text-slate-800' },
+ }
+ return statusMap[status]
+}
+
+export function VendorHeader({ vendor, onEditClick }: VendorHeaderProps) {
+ const status = formatStatus(vendor.status)
+
+ return (
+
+
+
+
{vendor.name}
+ {status.label}
+ {vendor.isBooked && (
+ Booked
+ )}
+
+
+ {formatVendorType(vendor.type)} • Added {new Date(vendor.createdAt).toLocaleDateString()}
+
+
+
+
+
+
+
+ )
+}
\ No newline at end of file