459 lines
17 KiB
TypeScript

'use client'
import { useState, useEffect } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { AppointmentCalendar } from '@/components/admin/appointment-calendar'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Badge } from '@/components/ui/badge'
import { CalendarIcon, Plus, Users, Clock, CheckCircle, XCircle } from 'lucide-react'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { toast } from 'sonner'
import moment from 'moment'
const appointmentSchema = z.object({
artistId: z.string().min(1, 'Artist is required'),
clientName: z.string().min(1, 'Client name is required'),
clientEmail: z.string().email('Valid email is required'),
title: z.string().min(1, 'Title is required'),
description: z.string().optional(),
startTime: z.string().min(1, 'Start time is required'),
endTime: z.string().min(1, 'End time is required'),
depositAmount: z.number().optional(),
totalAmount: z.number().optional(),
notes: z.string().optional(),
})
type AppointmentFormData = z.infer<typeof appointmentSchema>
export default function CalendarPage() {
const [isNewAppointmentOpen, setIsNewAppointmentOpen] = useState(false)
const [selectedSlot, setSelectedSlot] = useState<{ start: Date; end: Date } | null>(null)
const queryClient = useQueryClient()
const form = useForm<AppointmentFormData>({
resolver: zodResolver(appointmentSchema),
defaultValues: {
artistId: '',
clientName: '',
clientEmail: '',
title: '',
description: '',
startTime: '',
endTime: '',
depositAmount: undefined,
totalAmount: undefined,
notes: '',
},
})
// Fetch appointments
const { data: appointmentsData, isLoading: appointmentsLoading } = useQuery({
queryKey: ['appointments'],
queryFn: async () => {
const response = await fetch('/api/appointments')
if (!response.ok) throw new Error('Failed to fetch appointments')
return response.json()
},
})
// Fetch artists
const { data: artistsData, isLoading: artistsLoading } = useQuery({
queryKey: ['artists'],
queryFn: async () => {
const response = await fetch('/api/artists')
if (!response.ok) throw new Error('Failed to fetch artists')
return response.json()
},
})
// Create appointment mutation
const createAppointmentMutation = useMutation({
mutationFn: async (data: AppointmentFormData) => {
// First, create or find the client user
const clientResponse = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: data.clientName,
email: data.clientEmail,
role: 'CLIENT',
}),
})
let clientId
if (clientResponse.ok) {
const client = await clientResponse.json()
clientId = client.user.id
} else {
// If user already exists, try to find them
const existingUserResponse = await fetch(`/api/users?email=${encodeURIComponent(data.clientEmail)}`)
if (existingUserResponse.ok) {
const existingUser = await existingUserResponse.json()
clientId = existingUser.user.id
} else {
throw new Error('Failed to create or find client')
}
}
// Create the appointment
const appointmentResponse = await fetch('/api/appointments', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...data,
clientId,
startTime: new Date(data.startTime).toISOString(),
endTime: new Date(data.endTime).toISOString(),
}),
})
if (!appointmentResponse.ok) {
const error = await appointmentResponse.json()
throw new Error(error.error || 'Failed to create appointment')
}
return appointmentResponse.json()
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['appointments'] })
setIsNewAppointmentOpen(false)
form.reset()
toast.success('Appointment created successfully')
},
onError: (error: Error) => {
toast.error(error.message)
},
})
// Update appointment mutation
const updateAppointmentMutation = useMutation({
mutationFn: async ({ id, updates }: { id: string; updates: any }) => {
const response = await fetch('/api/appointments', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, ...updates }),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error || 'Failed to update appointment')
}
return response.json()
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['appointments'] })
toast.success('Appointment updated successfully')
},
onError: (error: Error) => {
toast.error(error.message)
},
})
// Handle slot selection for new appointment
const handleSlotSelect = (slotInfo: { start: Date; end: Date; slots: Date[] }) => {
setSelectedSlot({ start: slotInfo.start, end: slotInfo.end })
form.setValue('startTime', moment(slotInfo.start).format('YYYY-MM-DDTHH:mm'))
form.setValue('endTime', moment(slotInfo.end).format('YYYY-MM-DDTHH:mm'))
setIsNewAppointmentOpen(true)
}
// Handle event update
const handleEventUpdate = (eventId: string, updates: any) => {
updateAppointmentMutation.mutate({ id: eventId, updates })
}
const onSubmit = (data: AppointmentFormData) => {
createAppointmentMutation.mutate(data)
}
const appointments = appointmentsData?.appointments || []
const artists = artistsData?.artists || []
// Calculate stats
const stats = {
total: appointments.length,
pending: appointments.filter((apt: any) => apt.status === 'PENDING').length,
confirmed: appointments.filter((apt: any) => apt.status === 'CONFIRMED').length,
completed: appointments.filter((apt: any) => apt.status === 'COMPLETED').length,
}
if (appointmentsLoading || artistsLoading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto"></div>
<p className="mt-2 text-sm text-muted-foreground">Loading calendar...</p>
</div>
</div>
)
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
<div>
<h1 className="text-2xl font-bold">Appointment Calendar</h1>
<p className="text-muted-foreground">Manage studio appointments and scheduling</p>
</div>
<Dialog open={isNewAppointmentOpen} onOpenChange={setIsNewAppointmentOpen}>
<DialogTrigger asChild>
<Button>
<Plus className="h-4 w-4 mr-2" />
New Appointment
</Button>
</DialogTrigger>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Create New Appointment</DialogTitle>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="artistId"
render={({ field }) => (
<FormItem>
<FormLabel>Artist</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select an artist" />
</SelectTrigger>
</FormControl>
<SelectContent>
{artists.map((artist: any) => (
<SelectItem key={artist.id} value={artist.id}>
{artist.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="clientName"
render={({ field }) => (
<FormItem>
<FormLabel>Client Name</FormLabel>
<FormControl>
<Input placeholder="John Doe" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="clientEmail"
render={({ field }) => (
<FormItem>
<FormLabel>Client Email</FormLabel>
<FormControl>
<Input type="email" placeholder="john@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Appointment Title</FormLabel>
<FormControl>
<Input placeholder="Tattoo Session" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea placeholder="Appointment details..." {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="startTime"
render={({ field }) => (
<FormItem>
<FormLabel>Start Time</FormLabel>
<FormControl>
<Input type="datetime-local" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="endTime"
render={({ field }) => (
<FormItem>
<FormLabel>End Time</FormLabel>
<FormControl>
<Input type="datetime-local" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="depositAmount"
render={({ field }) => (
<FormItem>
<FormLabel>Deposit Amount</FormLabel>
<FormControl>
<Input
type="number"
step="0.01"
placeholder="0.00"
{...field}
onChange={(e) => field.onChange(e.target.value ? parseFloat(e.target.value) : undefined)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="totalAmount"
render={({ field }) => (
<FormItem>
<FormLabel>Total Amount</FormLabel>
<FormControl>
<Input
type="number"
step="0.01"
placeholder="0.00"
{...field}
onChange={(e) => field.onChange(e.target.value ? parseFloat(e.target.value) : undefined)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="notes"
render={({ field }) => (
<FormItem>
<FormLabel>Notes</FormLabel>
<FormControl>
<Textarea placeholder="Additional notes..." {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={() => setIsNewAppointmentOpen(false)}>
Cancel
</Button>
<Button type="submit" disabled={createAppointmentMutation.isPending}>
{createAppointmentMutation.isPending ? 'Creating...' : 'Create Appointment'}
</Button>
</div>
</form>
</Form>
</DialogContent>
</Dialog>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Appointments</CardTitle>
<CalendarIcon className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.total}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Pending</CardTitle>
<Clock className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-yellow-600">{stats.pending}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Confirmed</CardTitle>
<CheckCircle className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-blue-600">{stats.confirmed}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Completed</CardTitle>
<Users className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">{stats.completed}</div>
</CardContent>
</Card>
</div>
{/* Calendar */}
<AppointmentCalendar
appointments={appointments}
artists={artists}
onSlotSelect={handleSlotSelect}
onEventUpdate={handleEventUpdate}
/>
</div>
)
}