feat: update reservation form and add schema migration

This commit is contained in:
2025-12-06 23:53:05 +03:00
parent b89669f795
commit 2818e7ed1d
7 changed files with 161 additions and 18 deletions

View File

@@ -2,17 +2,26 @@ import { MainNav } from "@/components/main-nav"
import { UserNav } from "@/components/user-nav" import { UserNav } from "@/components/user-nav"
import { Building } from "lucide-react" import { Building } from "lucide-react"
import { MobileSidebar } from "@/components/mobile-sidebar" import { MobileSidebar } from "@/components/mobile-sidebar"
import { createClient } from "@/lib/supabase/server"
export default function DashboardLayout({ export default async function DashboardLayout({
children, children,
}: { }: {
children: React.ReactNode children: React.ReactNode
}) { }) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
const userData = user ? {
name: user.user_metadata?.full_name || user.email?.split('@')[0],
email: user.email || null
} : undefined
return ( return (
<div className="flex min-h-screen bg-gray-50/50 dark:bg-gray-900/50"> <div className="flex min-h-screen bg-gray-50/50 dark:bg-gray-900/50">
{/* Desktop Sidebar */} {/* Desktop Sidebar */}
<aside className="hidden md:flex w-72 flex-col fixed inset-y-0 z-50 bg-white dark:bg-gray-950 border-r shadow-sm"> <aside className="hidden md:flex w-72 flex-col fixed inset-y-0 z-50 bg-white dark:bg-gray-950 border-r shadow-sm">
<MainNav /> <MainNav user={userData} />
</aside> </aside>
{/* Main Content */} {/* Main Content */}
@@ -20,7 +29,7 @@ export default function DashboardLayout({
{/* Header */} {/* Header */}
<header className="sticky top-0 z-40 h-16 glass border-b px-4 md:px-6 flex items-center justify-between"> <header className="sticky top-0 z-40 h-16 glass border-b px-4 md:px-6 flex items-center justify-between">
<div className="flex items-center gap-2 md:hidden"> <div className="flex items-center gap-2 md:hidden">
<MobileSidebar /> <MobileSidebar user={userData} />
<div className="flex items-center font-bold text-lg"> <div className="flex items-center font-bold text-lg">
<Building className="mr-2 h-5 w-5 text-primary" /> <Building className="mr-2 h-5 w-5 text-primary" />
WeddingOS WeddingOS

View File

@@ -101,6 +101,20 @@ export default async function ReservationDetailsPage({
</Badge> </Badge>
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-4 mt-4">
{reservation.groom_region && (
<div>
<p className="text-sm font-medium text-muted-foreground">Damat Yöre</p>
<p className="text-base">{reservation.groom_region}</p>
</div>
)}
{reservation.bride_region && (
<div>
<p className="text-sm font-medium text-muted-foreground">Gelin Yöre</p>
<p className="text-base">{reservation.bride_region}</p>
</div>
)}
</div>
<Separator /> <Separator />
<div> <div>
<p className="text-sm font-medium text-muted-foreground mb-2">Notlar</p> <p className="text-sm font-medium text-muted-foreground mb-2">Notlar</p>

View File

@@ -8,10 +8,13 @@ import { logAction } from "@/lib/logger"
export async function createReservation(data: { export async function createReservation(data: {
hall_id: string hall_id: string
customer_id: string customer_id: string
package_id?: string package_id: string // Made required based on user request
start_time: string start_time: string
end_time: string end_time: string
notes?: string notes?: string
groom_region?: string
bride_region?: string
price: number
}) { }) {
const supabase = await createClient() const supabase = await createClient()
@@ -36,11 +39,14 @@ export async function createReservation(data: {
const { data: newReservation, error } = await supabase.from('reservations').insert({ const { data: newReservation, error } = await supabase.from('reservations').insert({
hall_id: data.hall_id, hall_id: data.hall_id,
customer_id: data.customer_id, customer_id: data.customer_id,
package_id: data.package_id || null, package_id: data.package_id,
start_time: data.start_time, start_time: data.start_time,
end_time: data.end_time, end_time: data.end_time,
status: 'pending', // Default to pending, admin must confirm status: 'pending', // Default to pending, admin must confirm
notes: data.notes, notes: data.notes,
groom_region: data.groom_region,
bride_region: data.bride_region,
price: data.price,
}).select().single() }).select().single()
if (error) { if (error) {

View File

@@ -16,8 +16,7 @@ import { Input } from "@/components/ui/input"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Textarea } from "@/components/ui/textarea" import { Textarea } from "@/components/ui/textarea"
import { createReservation } from "./actions" import { createReservation } from "./actions"
import { useState } from "react" import { useState, useEffect } from "react"
import { toast } from "sonner"
import { Check, ChevronsUpDown } from "lucide-react" import { Check, ChevronsUpDown } from "lucide-react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { import {
@@ -37,11 +36,15 @@ import {
const formSchema = z.object({ const formSchema = z.object({
hall_id: z.string().min(1, "Salon seçmelisiniz."), hall_id: z.string().min(1, "Salon seçmelisiniz."),
customer_id: z.string().min(1, "Müşteri seçmelisiniz."), customer_id: z.string().min(1, "Müşteri seçmelisiniz."),
package_id: z.string().optional(), package_id: z.string().min(1, "Paket seçmelisiniz."),
date: z.string().min(1, "Tarih seçmelisiniz."), date: z.string().min(1, "Tarih seçmelisiniz."),
start_time: z.string().min(1, "Başlangıç saati seçmelisiniz."), start_time: z.string().min(1, "Başlangıç saati seçmelisiniz."),
end_time: z.string().min(1, "Bitiş saati seçmelisiniz."), end_time: z.string().min(1, "Bitiş saati seçmelisiniz."),
notes: z.string().optional(), notes: z.string().optional(),
groom_region: z.string().optional(),
bride_region: z.string().optional(),
price: z.coerce.number().min(0, "Fiyat 0'dan küçük olamaz."),
discount_rate: z.coerce.number().min(0).max(100).optional(),
}) })
interface ReservationFormProps { interface ReservationFormProps {
@@ -56,7 +59,7 @@ export function ReservationForm({ halls, customers, packages }: ReservationFormP
const [openCustomer, setOpenCustomer] = useState(false) const [openCustomer, setOpenCustomer] = useState(false)
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema) as any,
defaultValues: { defaultValues: {
hall_id: "", hall_id: "",
customer_id: "", customer_id: "",
@@ -65,14 +68,40 @@ export function ReservationForm({ halls, customers, packages }: ReservationFormP
start_time: "", start_time: "",
end_time: "", end_time: "",
notes: "", notes: "",
groom_region: "",
bride_region: "",
price: 0,
discount_rate: 0,
}, },
}) })
// Watch for changes
const selectedPackageId = form.watch("package_id")
const discountRate = form.watch("discount_rate")
// Auto-calculate price logic
useEffect(() => {
if (selectedPackageId) {
const pkg = packages.find(p => p.id === selectedPackageId)
if (pkg) {
const basePrice = Number(pkg.price)
const discount = Number(discountRate) || 0
// Calculate new price
const discountedPrice = basePrice * (1 - discount / 100)
// Update price field if it differs significantly (avoid loops/floating point issues)
const currentPrice = form.getValues("price")
if (Math.abs(currentPrice - discountedPrice) > 0.1) {
form.setValue('price', Number(discountedPrice.toFixed(2)))
}
}
}
}, [selectedPackageId, discountRate, packages, form])
async function onSubmit(values: z.infer<typeof formSchema>) { async function onSubmit(values: z.infer<typeof formSchema>) {
setLoading(true) setLoading(true)
setError(null) setError(null)
// Combine date and time
const startDateTime = new Date(`${values.date}T${values.start_time}`) const startDateTime = new Date(`${values.date}T${values.start_time}`)
const endDateTime = new Date(`${values.date}T${values.end_time}`) const endDateTime = new Date(`${values.date}T${values.end_time}`)
@@ -86,10 +115,13 @@ export function ReservationForm({ halls, customers, packages }: ReservationFormP
const result = await createReservation({ const result = await createReservation({
hall_id: values.hall_id, hall_id: values.hall_id,
customer_id: values.customer_id, customer_id: values.customer_id,
package_id: values.package_id === "none" ? undefined : values.package_id, package_id: values.package_id,
start_time: startDateTime.toISOString(), start_time: startDateTime.toISOString(),
end_time: endDateTime.toISOString(), end_time: endDateTime.toISOString(),
notes: values.notes, notes: values.notes,
groom_region: values.groom_region,
bride_region: values.bride_region,
price: values.price,
}) })
if (result && result.error) { if (result && result.error) {
@@ -249,7 +281,7 @@ export function ReservationForm({ halls, customers, packages }: ReservationFormP
name="package_id" name="package_id"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Paket (Opsiyonel)</FormLabel> <FormLabel>Paket (Zorunlu)</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}> <Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl> <FormControl>
<SelectTrigger> <SelectTrigger>
@@ -257,7 +289,6 @@ export function ReservationForm({ halls, customers, packages }: ReservationFormP
</SelectTrigger> </SelectTrigger>
</FormControl> </FormControl>
<SelectContent> <SelectContent>
<SelectItem value="none">Paket Yok</SelectItem>
{packages.map(p => ( {packages.map(p => (
<SelectItem key={p.id} value={p.id}>{p.name} - {p.price}</SelectItem> <SelectItem key={p.id} value={p.id}>{p.name} - {p.price}</SelectItem>
))} ))}
@@ -268,6 +299,76 @@ export function ReservationForm({ halls, customers, packages }: ReservationFormP
)} )}
/> />
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="discount_rate"
render={({ field }) => (
<FormItem>
<FormLabel>İndirim Oranı (%)</FormLabel>
<FormControl>
<Input
type="number"
min="0"
max="100"
placeholder="0"
{...field}
onChange={e => field.onChange(e.target.valueAsNumber)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="price"
render={({ field }) => (
<FormItem>
<FormLabel>Toplam Tutar (TL)</FormLabel>
<FormControl>
<Input
type="number"
min="0"
{...field}
onChange={e => field.onChange(e.target.valueAsNumber)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="groom_region"
render={({ field }) => (
<FormItem>
<FormLabel>Damat Yöre</FormLabel>
<FormControl>
<Input placeholder="Örn: Ankara" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="bride_region"
render={({ field }) => (
<FormItem>
<FormLabel>Gelin Yöre</FormLabel>
<FormControl>
<Input placeholder="Örn: İzmir" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField <FormField
control={form.control} control={form.control}
name="notes" name="notes"

View File

@@ -8,11 +8,16 @@ import { CalendarDays, Users, Home, Settings, Building2, CreditCard, LogOut, Rec
interface MainNavProps extends React.HTMLAttributes<HTMLElement> { interface MainNavProps extends React.HTMLAttributes<HTMLElement> {
onNavClick?: () => void onNavClick?: () => void
user?: {
name: string | null
email: string | null
}
} }
export function MainNav({ export function MainNav({
className, className,
onNavClick, onNavClick,
user,
...props ...props
}: MainNavProps) { }: MainNavProps) {
const pathname = usePathname() const pathname = usePathname()
@@ -62,6 +67,10 @@ export function MainNav({
}, },
] ]
const displayName = user?.name || user?.email?.split('@')[0] || 'Kullanıcı'
const displayEmail = user?.email || ''
const initial = displayName.charAt(0).toUpperCase()
return ( return (
<nav <nav
className={cn("flex flex-col space-y-2 py-4", className)} className={cn("flex flex-col space-y-2 py-4", className)}
@@ -96,11 +105,11 @@ export function MainNav({
<div className="px-4 mt-auto pt-4 border-t"> <div className="px-4 mt-auto pt-4 border-t">
<div className="flex items-center gap-3 mb-4 p-2 rounded-lg bg-muted/50"> <div className="flex items-center gap-3 mb-4 p-2 rounded-lg bg-muted/50">
<div className="h-8 w-8 rounded-full bg-primary/20 flex items-center justify-center text-primary font-bold"> <div className="h-8 w-8 rounded-full bg-primary/20 flex items-center justify-center text-primary font-bold">
A {initial}
</div> </div>
<div className="flex-1 overflow-hidden"> <div className="flex-1 overflow-hidden">
<p className="text-sm font-medium truncate">Admin User</p> <p className="text-sm font-medium truncate">{displayName}</p>
<p className="text-xs text-muted-foreground truncate">admin@demo.com</p> <p className="text-xs text-muted-foreground truncate">{displayEmail}</p>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -6,7 +6,7 @@ import { Menu } from "lucide-react"
import { MainNav } from "@/components/main-nav" import { MainNav } from "@/components/main-nav"
import { useState, useEffect } from "react" import { useState, useEffect } from "react"
export function MobileSidebar() { export function MobileSidebar({ user }: { user?: { name: string | null; email: string | null } }) {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [isMounted, setIsMounted] = useState(false) const [isMounted, setIsMounted] = useState(false)
@@ -28,7 +28,7 @@ export function MobileSidebar() {
</SheetTrigger> </SheetTrigger>
<SheetContent side="left" className="p-0 w-72"> <SheetContent side="left" className="p-0 w-72">
<SheetTitle className="sr-only">Navigasyon Menüsü</SheetTitle> <SheetTitle className="sr-only">Navigasyon Menüsü</SheetTitle>
<MainNav onNavClick={() => setOpen(false)} /> <MainNav user={user} onNavClick={() => setOpen(false)} />
</SheetContent> </SheetContent>
</Sheet> </Sheet>
) )

View File

@@ -0,0 +1,4 @@
-- Add groom_region and bride_region to reservations table
ALTER TABLE reservations
ADD COLUMN IF NOT EXISTS groom_region text,
ADD COLUMN IF NOT EXISTS bride_region text;