feat: update reservation form and add schema migration
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
4
supabase_schema_add_regions.sql
Normal file
4
supabase_schema_add_regions.sql
Normal 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;
|
||||||
Reference in New Issue
Block a user