Initial commit: Wedding Hall Automation System with Next.js, Supabase, and Modern UI
This commit is contained in:
34
src/app/dashboard/calendar/page.tsx
Normal file
34
src/app/dashboard/calendar/page.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { CalendarView } from "@/components/calendar-view"
|
||||
import { createClient } from "@/lib/supabase/server"
|
||||
|
||||
export default async function CalendarPage() {
|
||||
const supabase = await createClient()
|
||||
|
||||
// Fetch reservations
|
||||
const { data: reservations } = await supabase
|
||||
.from('reservations')
|
||||
.select(`
|
||||
id,
|
||||
start_time,
|
||||
end_time,
|
||||
status,
|
||||
halls (name),
|
||||
customers (full_name)
|
||||
`)
|
||||
|
||||
// Transform data for calendar
|
||||
const events = reservations?.map(res => ({
|
||||
id: res.id,
|
||||
title: `${res.halls?.name || 'Salon'} - ${res.customers?.full_name || 'Müşteri'}`,
|
||||
start: new Date(res.start_time),
|
||||
end: new Date(res.end_time),
|
||||
resource: res,
|
||||
})) || []
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-3xl font-bold tracking-tight">Takvim</h2>
|
||||
<CalendarView events={events} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
21
src/app/dashboard/customers/new/actions.ts
Normal file
21
src/app/dashboard/customers/new/actions.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
'use server'
|
||||
|
||||
import { createClient } from "@/lib/supabase/server"
|
||||
import { revalidatePath } from "next/cache"
|
||||
|
||||
export async function createCustomer(data: { full_name: string; phone?: string; email?: string; notes?: string }) {
|
||||
const supabase = await createClient()
|
||||
|
||||
const { error } = await supabase.from('customers').insert({
|
||||
full_name: data.full_name,
|
||||
phone: data.phone,
|
||||
email: data.email || null,
|
||||
notes: data.notes,
|
||||
})
|
||||
|
||||
if (error) {
|
||||
throw new Error(error.message)
|
||||
}
|
||||
|
||||
revalidatePath('/dashboard/customers')
|
||||
}
|
||||
119
src/app/dashboard/customers/new/customer-form.tsx
Normal file
119
src/app/dashboard/customers/new/customer-form.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
'use client'
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import * as z from "zod"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { createCustomer } from "./actions"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useState } from "react"
|
||||
|
||||
const formSchema = z.object({
|
||||
full_name: z.string().min(2, {
|
||||
message: "İsim en az 2 karakter olmalıdır.",
|
||||
}),
|
||||
phone: z.string().optional(),
|
||||
email: z.string().email({ message: "Geçersiz e-posta adresi." }).optional().or(z.literal('')),
|
||||
notes: z.string().optional(),
|
||||
})
|
||||
|
||||
export function CustomerForm() {
|
||||
const router = useRouter()
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
full_name: "",
|
||||
phone: "",
|
||||
email: "",
|
||||
notes: "",
|
||||
},
|
||||
})
|
||||
|
||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
setLoading(true)
|
||||
try {
|
||||
await createCustomer(values)
|
||||
router.push('/dashboard/customers')
|
||||
router.refresh()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
alert("Bir hata oluştu")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="full_name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>İsim Soyisim</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Ahmet Yılmaz" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="phone"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Telefon</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="0555 555 55 55" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>E-posta</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="ahmet@ornek.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="notes"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Notlar</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea placeholder="Müşteri hakkında notlar..." {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? "Kaydediliyor..." : "Kaydet"}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
17
src/app/dashboard/customers/new/page.tsx
Normal file
17
src/app/dashboard/customers/new/page.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { CustomerForm } from "./customer-form"
|
||||
|
||||
export default function NewCustomerPage() {
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Yeni Müşteri Ekle</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CustomerForm />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
91
src/app/dashboard/customers/page.tsx
Normal file
91
src/app/dashboard/customers/page.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import { createClient } from "@/lib/supabase/server"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Plus, Search } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { Input } from "@/components/ui/input"
|
||||
|
||||
export default async function CustomersPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ q?: string }>
|
||||
}) {
|
||||
const query = (await searchParams).q || ''
|
||||
const supabase = await createClient()
|
||||
|
||||
let request = supabase.from('customers').select('*').order('created_at', { ascending: false })
|
||||
|
||||
if (query) {
|
||||
request = request.ilike('full_name', `%${query}%`)
|
||||
}
|
||||
|
||||
const { data: customers } = await request
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-3xl font-bold tracking-tight">Müşteriler</h2>
|
||||
<Link href="/dashboard/customers/new">
|
||||
<Button>
|
||||
<Plus className="mr-2 h-4 w-4" /> Yeni Müşteri Ekle
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="relative flex-1 max-w-sm">
|
||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="İsim ile ara..."
|
||||
className="pl-8"
|
||||
defaultValue={query}
|
||||
// Note: Real search implementation would need a client component or form submission
|
||||
// For now just UI
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>İsim Soyisim</TableHead>
|
||||
<TableHead>Telefon</TableHead>
|
||||
<TableHead>E-posta</TableHead>
|
||||
<TableHead>Notlar</TableHead>
|
||||
<TableHead className="text-right">İşlemler</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{customers?.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="text-center h-24 text-muted-foreground">
|
||||
Müşteri bulunamadı.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
customers?.map((customer) => (
|
||||
<TableRow key={customer.id}>
|
||||
<TableCell className="font-medium">{customer.full_name}</TableCell>
|
||||
<TableCell>{customer.phone}</TableCell>
|
||||
<TableCell>{customer.email}</TableCell>
|
||||
<TableCell className="max-w-xs truncate">{customer.notes}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button variant="ghost" size="sm">Düzenle</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
20
src/app/dashboard/halls/new/actions.ts
Normal file
20
src/app/dashboard/halls/new/actions.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
'use server'
|
||||
|
||||
import { createClient } from "@/lib/supabase/server"
|
||||
import { revalidatePath } from "next/cache"
|
||||
|
||||
export async function createHall(data: { name: string; capacity: number; description?: string }) {
|
||||
const supabase = await createClient()
|
||||
|
||||
const { error } = await supabase.from('halls').insert({
|
||||
name: data.name,
|
||||
capacity: data.capacity,
|
||||
description: data.description,
|
||||
})
|
||||
|
||||
if (error) {
|
||||
throw new Error(error.message)
|
||||
}
|
||||
|
||||
revalidatePath('/dashboard/halls')
|
||||
}
|
||||
108
src/app/dashboard/halls/new/hall-form.tsx
Normal file
108
src/app/dashboard/halls/new/hall-form.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
'use client'
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import * as z from "zod"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { createHall } from "./actions"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useState } from "react"
|
||||
import { useToast } from "@/hooks/use-toast" // Note: Need to check if toast hook exists or create it
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(2, {
|
||||
message: "Salon ismi en az 2 karakter olmalıdır.",
|
||||
}),
|
||||
capacity: z.coerce.number().min(1, {
|
||||
message: "Kapasite en az 1 olmalıdır.",
|
||||
}),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
|
||||
export function HallForm() {
|
||||
const router = useRouter()
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
capacity: 0,
|
||||
description: "",
|
||||
},
|
||||
})
|
||||
|
||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
setLoading(true)
|
||||
try {
|
||||
await createHall(values)
|
||||
router.push('/dashboard/halls')
|
||||
router.refresh()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
alert("Bir hata oluştu")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Salon İsmi</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Örn: Altın Salon" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="capacity"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Kapasite</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="number" placeholder="500" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Açıklama</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea placeholder="Salon özellikleri..." {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? "Kaydediliyor..." : "Kaydet"}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
17
src/app/dashboard/halls/new/page.tsx
Normal file
17
src/app/dashboard/halls/new/page.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { HallForm } from "./hall-form"
|
||||
|
||||
export default function NewHallPage() {
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Yeni Salon Ekle</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<HallForm />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
63
src/app/dashboard/halls/page.tsx
Normal file
63
src/app/dashboard/halls/page.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { createClient } from "@/lib/supabase/server"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Plus } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
|
||||
export default async function HallsPage() {
|
||||
const supabase = await createClient()
|
||||
const { data: halls } = await supabase.from('halls').select('*').order('created_at', { ascending: false })
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-3xl font-bold tracking-tight">Salonlar</h2>
|
||||
<Link href="/dashboard/halls/new">
|
||||
<Button>
|
||||
<Plus className="mr-2 h-4 w-4" /> Yeni Salon Ekle
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>İsim</TableHead>
|
||||
<TableHead>Kapasite</TableHead>
|
||||
<TableHead>Açıklama</TableHead>
|
||||
<TableHead className="text-right">İşlemler</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{halls?.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="text-center h-24 text-muted-foreground">
|
||||
Henüz salon eklenmemiş.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
halls?.map((hall) => (
|
||||
<TableRow key={hall.id}>
|
||||
<TableCell className="font-medium">{hall.name}</TableCell>
|
||||
<TableCell>{hall.capacity} Kişi</TableCell>
|
||||
<TableCell>{hall.description}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button variant="ghost" size="sm">Düzenle</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
40
src/app/dashboard/layout.tsx
Normal file
40
src/app/dashboard/layout.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { MainNav } from "@/components/main-nav"
|
||||
import { UserNav } from "@/components/user-nav"
|
||||
import { Building } from "lucide-react"
|
||||
|
||||
export default function DashboardLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div className="flex min-h-screen bg-gray-50/50 dark:bg-gray-900/50">
|
||||
{/* 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">
|
||||
<MainNav />
|
||||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 md:ml-72 flex flex-col min-h-screen">
|
||||
{/* Header */}
|
||||
<header className="sticky top-0 z-40 h-16 glass border-b px-6 flex items-center justify-between">
|
||||
<div className="md:hidden flex items-center font-bold text-xl">
|
||||
<Building className="mr-2 h-6 w-6 text-primary" />
|
||||
WeddingOS
|
||||
</div>
|
||||
|
||||
<div className="ml-auto flex items-center space-x-4">
|
||||
<UserNav />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Page Content */}
|
||||
<main className="flex-1 p-6 md:p-8 overflow-y-auto">
|
||||
<div className="max-w-7xl mx-auto animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
118
src/app/dashboard/page.tsx
Normal file
118
src/app/dashboard/page.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { CalendarDays, CreditCard, Users, DollarSign, TrendingUp, ArrowUpRight } from "lucide-react"
|
||||
|
||||
export default function DashboardPage() {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold tracking-tight gradient-text">Hoş Geldiniz, Admin</h2>
|
||||
<p className="text-muted-foreground mt-1">İşletmenizin durumu hakkında genel bakış.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
|
||||
<Card className="card-hover border-l-4 border-l-blue-500">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
Toplam Rezervasyon
|
||||
</CardTitle>
|
||||
<div className="h-8 w-8 rounded-full bg-blue-100 dark:bg-blue-900/20 flex items-center justify-center">
|
||||
<CalendarDays className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold mt-2">12</div>
|
||||
<p className="text-xs text-muted-foreground mt-1 flex items-center">
|
||||
<span className="text-green-600 flex items-center mr-1"><TrendingUp className="h-3 w-3 mr-1" /> +2</span> geçen aydan beri
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="card-hover border-l-4 border-l-purple-500">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
Aktif Müşteriler
|
||||
</CardTitle>
|
||||
<div className="h-8 w-8 rounded-full bg-purple-100 dark:bg-purple-900/20 flex items-center justify-center">
|
||||
<Users className="h-4 w-4 text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold mt-2">50</div>
|
||||
<p className="text-xs text-muted-foreground mt-1 flex items-center">
|
||||
<span className="text-green-600 flex items-center mr-1"><ArrowUpRight className="h-3 w-3 mr-1" /> +4</span> geçen aydan beri
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="card-hover border-l-4 border-l-orange-500">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
Bekleyen Ödemeler
|
||||
</CardTitle>
|
||||
<div className="h-8 w-8 rounded-full bg-orange-100 dark:bg-orange-900/20 flex items-center justify-center">
|
||||
<CreditCard className="h-4 w-4 text-orange-600 dark:text-orange-400" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold mt-2">₺12,000</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
3 rezervasyon için
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="card-hover border-l-4 border-l-green-500">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
Aylık Gelir
|
||||
</CardTitle>
|
||||
<div className="h-8 w-8 rounded-full bg-green-100 dark:bg-green-900/20 flex items-center justify-center">
|
||||
<DollarSign className="h-4 w-4 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold mt-2">₺45,000</div>
|
||||
<p className="text-xs text-muted-foreground mt-1 flex items-center">
|
||||
<span className="text-green-600 flex items-center mr-1"><TrendingUp className="h-3 w-3 mr-1" /> +10%</span> geçen aydan beri
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-7">
|
||||
<Card className="col-span-4 shadow-md border-none">
|
||||
<CardHeader>
|
||||
<CardTitle>Yaklaşan Etkinlikler</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-[300px] flex items-center justify-center bg-muted/20 rounded-lg border border-dashed">
|
||||
<p className="text-muted-foreground">Takvim önizlemesi buraya gelecek.</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="col-span-3 shadow-md border-none">
|
||||
<CardHeader>
|
||||
<CardTitle>Son Aktiviteler</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="flex items-center gap-4 p-3 rounded-lg hover:bg-muted/50 transition-colors">
|
||||
<div className="h-9 w-9 rounded-full bg-primary/10 flex items-center justify-center text-primary font-bold text-sm">
|
||||
AH
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium">Ahmet Hakan rezervasyon yaptı</p>
|
||||
<p className="text-xs text-muted-foreground">2 dakika önce</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
39
src/app/dashboard/reservations/[id]/actions.ts
Normal file
39
src/app/dashboard/reservations/[id]/actions.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
'use server'
|
||||
|
||||
import { createClient } from "@/lib/supabase/server"
|
||||
import { revalidatePath } from "next/cache"
|
||||
|
||||
export async function addPayment(reservationId: string, formData: FormData) {
|
||||
const supabase = await createClient()
|
||||
|
||||
const amount = formData.get('amount')
|
||||
const type = formData.get('type')
|
||||
const method = formData.get('method')
|
||||
|
||||
const { error } = await supabase.from('payments').insert({
|
||||
reservation_id: reservationId,
|
||||
amount: amount,
|
||||
payment_type: type,
|
||||
payment_method: method,
|
||||
status: 'paid', // Assuming immediate payment for now
|
||||
paid_at: new Date().toISOString(),
|
||||
})
|
||||
|
||||
if (error) throw new Error(error.message)
|
||||
|
||||
revalidatePath(`/dashboard/reservations/${reservationId}`)
|
||||
}
|
||||
|
||||
export async function updateStatus(id: string, status: string) {
|
||||
const supabase = await createClient()
|
||||
|
||||
const { error } = await supabase
|
||||
.from('reservations')
|
||||
.update({ status })
|
||||
.eq('id', id)
|
||||
|
||||
if (error) throw new Error(error.message)
|
||||
|
||||
revalidatePath(`/dashboard/reservations/${id}`)
|
||||
revalidatePath('/dashboard/reservations')
|
||||
}
|
||||
157
src/app/dashboard/reservations/[id]/page.tsx
Normal file
157
src/app/dashboard/reservations/[id]/page.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
import { createClient } from "@/lib/supabase/server"
|
||||
import { notFound } from "next/navigation"
|
||||
import { format } from "date-fns"
|
||||
import { tr } from "date-fns/locale"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { ArrowLeft, Calendar, Clock, MapPin, User, CreditCard } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { PaymentList } from "./payment-list"
|
||||
import { StatusActions } from "./status-actions"
|
||||
|
||||
export default async function ReservationDetailsPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>
|
||||
}) {
|
||||
const id = (await params).id
|
||||
const supabase = await createClient()
|
||||
|
||||
const { data: reservation } = await supabase
|
||||
.from('reservations')
|
||||
.select(`
|
||||
*,
|
||||
halls (name),
|
||||
customers (full_name, phone, email),
|
||||
packages (name, price)
|
||||
`)
|
||||
.eq('id', id)
|
||||
.single()
|
||||
|
||||
if (!reservation) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const { data: payments } = await supabase
|
||||
.from('payments')
|
||||
.select('*')
|
||||
.eq('reservation_id', id)
|
||||
.order('created_at', { ascending: false })
|
||||
|
||||
const totalPaid = payments?.reduce((sum, p) => sum + (p.status === 'paid' ? Number(p.amount) : 0), 0) || 0
|
||||
const packagePrice = reservation.packages?.price || 0
|
||||
const remaining = Math.max(0, packagePrice - totalPaid)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/dashboard/reservations">
|
||||
<Button variant="ghost" size="icon">
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
<h2 className="text-3xl font-bold tracking-tight">Rezervasyon Detayı</h2>
|
||||
<div className="ml-auto">
|
||||
<StatusActions id={reservation.id} currentStatus={reservation.status} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Calendar className="h-5 w-5" /> Etkinlik Bilgileri
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Tarih</p>
|
||||
<p className="text-lg">{format(new Date(reservation.start_time), 'd MMMM yyyy', { locale: tr })}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Saat</p>
|
||||
<p className="text-lg">
|
||||
{format(new Date(reservation.start_time), 'HH:mm')} - {format(new Date(reservation.end_time), 'HH:mm')}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Salon</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<MapPin className="h-4 w-4 text-muted-foreground" />
|
||||
<span>{reservation.halls?.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Durum</p>
|
||||
<Badge variant={reservation.status === 'cancelled' ? 'destructive' : 'default'}>
|
||||
{reservation.status === 'confirmed' ? 'Onaylandı' :
|
||||
reservation.status === 'pending' ? 'Bekliyor' :
|
||||
reservation.status === 'cancelled' ? 'İptal' : reservation.status}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground mb-2">Notlar</p>
|
||||
<p className="text-sm">{reservation.notes || "Not yok."}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<User className="h-5 w-5" /> Müşteri Bilgileri
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">İsim Soyisim</p>
|
||||
<p className="text-lg font-medium">{reservation.customers?.full_name}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Telefon</p>
|
||||
<p>{reservation.customers?.phone || "-"}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">E-posta</p>
|
||||
<p>{reservation.customers?.email || "-"}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="md:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<CreditCard className="h-5 w-5" /> Ödeme Bilgileri
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
||||
<div className="bg-muted/50 p-4 rounded-lg">
|
||||
<p className="text-sm font-medium text-muted-foreground">Toplam Tutar</p>
|
||||
<p className="text-2xl font-bold">₺{packagePrice}</p>
|
||||
<p className="text-xs text-muted-foreground">{reservation.packages?.name || "Paket Yok"}</p>
|
||||
</div>
|
||||
<div className="bg-green-500/10 p-4 rounded-lg">
|
||||
<p className="text-sm font-medium text-green-600">Ödenen</p>
|
||||
<p className="text-2xl font-bold text-green-700">₺{totalPaid}</p>
|
||||
</div>
|
||||
<div className="bg-red-500/10 p-4 rounded-lg">
|
||||
<p className="text-sm font-medium text-red-600">Kalan</p>
|
||||
<p className="text-2xl font-bold text-red-700">₺{remaining}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator className="my-6" />
|
||||
|
||||
<PaymentList reservationId={reservation.id} payments={payments || []} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
148
src/app/dashboard/reservations/[id]/payment-list.tsx
Normal file
148
src/app/dashboard/reservations/[id]/payment-list.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
'use client'
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { useState } from "react"
|
||||
import { addPayment } from "./actions"
|
||||
import { format } from "date-fns"
|
||||
import { tr } from "date-fns/locale"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
|
||||
interface PaymentListProps {
|
||||
reservationId: string
|
||||
payments: any[]
|
||||
}
|
||||
|
||||
export function PaymentList({ reservationId, payments }: PaymentListProps) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
const formData = new FormData(e.currentTarget)
|
||||
|
||||
try {
|
||||
await addPayment(reservationId, formData)
|
||||
setOpen(false)
|
||||
} catch (error) {
|
||||
alert("Ödeme eklenirken hata oluştu")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-medium">Ödeme Geçmişi</h3>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm">Ödeme Ekle</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Ödeme Ekle</DialogTitle>
|
||||
<DialogDescription>
|
||||
Bu rezervasyon için yeni bir ödeme kaydı oluşturun.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="amount">Tutar (₺)</Label>
|
||||
<Input id="amount" name="amount" type="number" required />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="type">Ödeme Türü</Label>
|
||||
<Select name="type" defaultValue="deposit">
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="deposit">Kapora</SelectItem>
|
||||
<SelectItem value="remaining">Ara Ödeme / Kalan</SelectItem>
|
||||
<SelectItem value="full">Tamamı</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="method">Ödeme Yöntemi</Label>
|
||||
<Select name="method" defaultValue="cash">
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="cash">Nakit</SelectItem>
|
||||
<SelectItem value="credit_card">Kredi Kartı</SelectItem>
|
||||
<SelectItem value="transfer">Havale / EFT</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? "Kaydediliyor..." : "Kaydet"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
<div className="border rounded-md">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/50">
|
||||
<th className="h-10 px-4 text-left font-medium">Tarih</th>
|
||||
<th className="h-10 px-4 text-left font-medium">Tutar</th>
|
||||
<th className="h-10 px-4 text-left font-medium">Tür</th>
|
||||
<th className="h-10 px-4 text-left font-medium">Yöntem</th>
|
||||
<th className="h-10 px-4 text-left font-medium">Durum</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{payments.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="h-24 text-center text-muted-foreground">
|
||||
Henüz ödeme kaydı yok.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
payments.map((payment) => (
|
||||
<tr key={payment.id} className="border-b last:border-0">
|
||||
<td className="p-4">
|
||||
{format(new Date(payment.created_at), 'd MMM yyyy HH:mm', { locale: tr })}
|
||||
</td>
|
||||
<td className="p-4 font-medium">₺{payment.amount}</td>
|
||||
<td className="p-4 capitalize">
|
||||
{payment.payment_type === 'deposit' ? 'Kapora' :
|
||||
payment.payment_type === 'full' ? 'Tamamı' : 'Ara Ödeme'}
|
||||
</td>
|
||||
<td className="p-4 capitalize">
|
||||
{payment.payment_method === 'cash' ? 'Nakit' :
|
||||
payment.payment_method === 'credit_card' ? 'Kredi Kartı' : 'Havale'}
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<Badge variant="outline" className="bg-green-50 text-green-700 border-green-200">
|
||||
Ödendi
|
||||
</Badge>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
50
src/app/dashboard/reservations/[id]/status-actions.tsx
Normal file
50
src/app/dashboard/reservations/[id]/status-actions.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
'use client'
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { updateStatus } from "./actions"
|
||||
import { useState } from "react"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
|
||||
export function StatusActions({ id, currentStatus }: { id: string, currentStatus: string }) {
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
async function handleStatusChange(status: string) {
|
||||
if (!confirm(`Rezervasyon durumunu '${status}' olarak değiştirmek istediğinize emin misiniz?`)) return
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
await updateStatus(id, status)
|
||||
} catch (error) {
|
||||
alert("Durum güncellenirken hata oluştu")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" disabled={loading}>
|
||||
İşlemler <ChevronDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => handleStatusChange('confirmed')}>
|
||||
Onayla
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleStatusChange('completed')}>
|
||||
Tamamlandı Olarak İşaretle
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleStatusChange('cancelled')} className="text-red-600">
|
||||
İptal Et
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
52
src/app/dashboard/reservations/new/actions.ts
Normal file
52
src/app/dashboard/reservations/new/actions.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
'use server'
|
||||
|
||||
import { createClient } from "@/lib/supabase/server"
|
||||
import { revalidatePath } from "next/cache"
|
||||
import { redirect } from "next/navigation"
|
||||
|
||||
export async function createReservation(data: {
|
||||
hall_id: string
|
||||
customer_id: string
|
||||
package_id?: string
|
||||
start_time: string
|
||||
end_time: string
|
||||
notes?: string
|
||||
}) {
|
||||
const supabase = await createClient()
|
||||
|
||||
// 1. Check for conflicts
|
||||
// We look for any reservation in the same hall that overlaps with the requested time range
|
||||
const { data: conflicts, error: conflictError } = await supabase
|
||||
.from('reservations')
|
||||
.select('id')
|
||||
.eq('hall_id', data.hall_id)
|
||||
.neq('status', 'cancelled') // Ignore cancelled bookings
|
||||
.or(`and(start_time.lte.${data.end_time},end_time.gte.${data.start_time})`)
|
||||
|
||||
if (conflictError) {
|
||||
throw new Error("Müsaitlik kontrolü yapılırken hata oluştu: " + conflictError.message)
|
||||
}
|
||||
|
||||
if (conflicts && conflicts.length > 0) {
|
||||
return { error: "Seçilen tarih ve saatte bu salon zaten dolu." }
|
||||
}
|
||||
|
||||
// 2. Create Reservation
|
||||
const { error } = await supabase.from('reservations').insert({
|
||||
hall_id: data.hall_id,
|
||||
customer_id: data.customer_id,
|
||||
package_id: data.package_id || null,
|
||||
start_time: data.start_time,
|
||||
end_time: data.end_time,
|
||||
status: 'confirmed', // Auto confirm for admin created
|
||||
notes: data.notes,
|
||||
})
|
||||
|
||||
if (error) {
|
||||
return { error: error.message }
|
||||
}
|
||||
|
||||
revalidatePath('/dashboard/reservations')
|
||||
revalidatePath('/dashboard/calendar')
|
||||
redirect('/dashboard/reservations')
|
||||
}
|
||||
29
src/app/dashboard/reservations/new/page.tsx
Normal file
29
src/app/dashboard/reservations/new/page.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { ReservationForm } from "./reservation-form"
|
||||
import { createClient } from "@/lib/supabase/server"
|
||||
|
||||
export default async function NewReservationPage() {
|
||||
const supabase = await createClient()
|
||||
|
||||
// Fetch necessary data for the form
|
||||
const { data: halls } = await supabase.from('halls').select('id, name')
|
||||
const { data: customers } = await supabase.from('customers').select('id, full_name').order('created_at', { ascending: false }).limit(50)
|
||||
const { data: packages } = await supabase.from('packages').select('id, name, price').eq('is_active', true)
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Yeni Rezervasyon Oluştur</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ReservationForm
|
||||
halls={halls || []}
|
||||
customers={customers || []}
|
||||
packages={packages || []}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
226
src/app/dashboard/reservations/new/reservation-form.tsx
Normal file
226
src/app/dashboard/reservations/new/reservation-form.tsx
Normal file
@@ -0,0 +1,226 @@
|
||||
'use client'
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import * as z from "zod"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { createReservation } from "./actions"
|
||||
import { useState } from "react"
|
||||
import { useToast } from "@/hooks/use-toast" // Assuming we have this or will use alert
|
||||
|
||||
const formSchema = z.object({
|
||||
hall_id: z.string().min(1, "Salon seçmelisiniz."),
|
||||
customer_id: z.string().min(1, "Müşteri seçmelisiniz."),
|
||||
package_id: z.string().optional(),
|
||||
date: z.string().min(1, "Tarih seçmelisiniz."),
|
||||
start_time: z.string().min(1, "Başlangıç saati seçmelisiniz."),
|
||||
end_time: z.string().min(1, "Bitiş saati seçmelisiniz."),
|
||||
notes: z.string().optional(),
|
||||
})
|
||||
|
||||
interface ReservationFormProps {
|
||||
halls: { id: string, name: string }[]
|
||||
customers: { id: string, full_name: string }[]
|
||||
packages: { id: string, name: string, price: number }[]
|
||||
}
|
||||
|
||||
export function ReservationForm({ halls, customers, packages }: ReservationFormProps) {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
notes: "",
|
||||
},
|
||||
})
|
||||
|
||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
// Combine date and time
|
||||
const startDateTime = new Date(`${values.date}T${values.start_time}`)
|
||||
const endDateTime = new Date(`${values.date}T${values.end_time}`)
|
||||
|
||||
if (endDateTime <= startDateTime) {
|
||||
setError("Bitiş saati başlangıç saatinden sonra olmalıdır.")
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await createReservation({
|
||||
hall_id: values.hall_id,
|
||||
customer_id: values.customer_id,
|
||||
package_id: values.package_id === "none" ? undefined : values.package_id,
|
||||
start_time: startDateTime.toISOString(),
|
||||
end_time: endDateTime.toISOString(),
|
||||
notes: values.notes,
|
||||
})
|
||||
|
||||
if (result && result.error) {
|
||||
setError(result.error)
|
||||
}
|
||||
} catch (e) {
|
||||
setError("Beklenmedik bir hata oluştu.")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
{error && (
|
||||
<div className="bg-destructive/15 text-destructive px-4 py-2 rounded-md text-sm font-medium">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="hall_id"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Salon</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Salon Seçin" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{halls.map(hall => (
|
||||
<SelectItem key={hall.id} value={hall.id}>{hall.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="customer_id"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Müşteri</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Müşteri Seçin" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{customers.map(c => (
|
||||
<SelectItem key={c.id} value={c.id}>{c.full_name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="date"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Tarih</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="date" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="start_time"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Başlangıç Saati</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="time" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="end_time"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Bitiş Saati</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="time" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="package_id"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Paket (Opsiyonel)</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Paket Seçin" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">Paket Yok</SelectItem>
|
||||
{packages.map(p => (
|
||||
<SelectItem key={p.id} value={p.id}>{p.name} - ₺{p.price}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="notes"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Notlar</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea placeholder="Özel istekler..." {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button type="submit" disabled={loading} className="w-full">
|
||||
{loading ? "Oluşturuluyor..." : "Rezervasyonu Oluştur"}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
108
src/app/dashboard/reservations/page.tsx
Normal file
108
src/app/dashboard/reservations/page.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import { createClient } from "@/lib/supabase/server"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Plus, Calendar as CalendarIcon } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { format } from "date-fns"
|
||||
import { tr } from "date-fns/locale"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
|
||||
export default async function ReservationsPage() {
|
||||
const supabase = await createClient()
|
||||
|
||||
const { data: reservations } = await supabase
|
||||
.from('reservations')
|
||||
.select(`
|
||||
id,
|
||||
start_time,
|
||||
end_time,
|
||||
status,
|
||||
halls (name),
|
||||
customers (full_name),
|
||||
packages (name, price)
|
||||
`)
|
||||
.order('start_time', { ascending: true })
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case 'confirmed': return <Badge className="bg-green-600">Onaylandı</Badge>
|
||||
case 'pending': return <Badge variant="secondary">Bekliyor</Badge>
|
||||
case 'cancelled': return <Badge variant="destructive">İptal</Badge>
|
||||
case 'completed': return <Badge variant="outline">Tamamlandı</Badge>
|
||||
default: return <Badge>{status}</Badge>
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-3xl font-bold tracking-tight">Rezervasyonlar</h2>
|
||||
<div className="space-x-2">
|
||||
<Link href="/dashboard/calendar">
|
||||
<Button variant="outline">
|
||||
<CalendarIcon className="mr-2 h-4 w-4" /> Takvim Görünümü
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/dashboard/reservations/new">
|
||||
<Button>
|
||||
<Plus className="mr-2 h-4 w-4" /> Yeni Rezervasyon
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Tarih</TableHead>
|
||||
<TableHead>Saat</TableHead>
|
||||
<TableHead>Salon</TableHead>
|
||||
<TableHead>Müşteri</TableHead>
|
||||
<TableHead>Paket</TableHead>
|
||||
<TableHead>Durum</TableHead>
|
||||
<TableHead className="text-right">İşlemler</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{reservations?.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} className="text-center h-24 text-muted-foreground">
|
||||
Henüz rezervasyon yok.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
reservations?.map((res) => (
|
||||
<TableRow key={res.id}>
|
||||
<TableCell className="font-medium">
|
||||
{format(new Date(res.start_time), 'd MMMM yyyy', { locale: tr })}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{format(new Date(res.start_time), 'HH:mm')} - {format(new Date(res.end_time), 'HH:mm')}
|
||||
</TableCell>
|
||||
<TableCell>{res.halls?.name}</TableCell>
|
||||
<TableCell>{res.customers?.full_name}</TableCell>
|
||||
<TableCell>
|
||||
{res.packages?.name}
|
||||
{res.packages?.price && <span className="text-xs text-muted-foreground block">₺{res.packages.price}</span>}
|
||||
</TableCell>
|
||||
<TableCell>{getStatusBadge(res.status || 'pending')}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button variant="ghost" size="sm">Detay</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
21
src/app/dashboard/settings/packages/new/actions.ts
Normal file
21
src/app/dashboard/settings/packages/new/actions.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
'use server'
|
||||
|
||||
import { createClient } from "@/lib/supabase/server"
|
||||
import { revalidatePath } from "next/cache"
|
||||
|
||||
export async function createPackage(data: { name: string; price: number; description?: string; is_active: boolean }) {
|
||||
const supabase = await createClient()
|
||||
|
||||
const { error } = await supabase.from('packages').insert({
|
||||
name: data.name,
|
||||
price: data.price,
|
||||
description: data.description,
|
||||
is_active: data.is_active,
|
||||
})
|
||||
|
||||
if (error) {
|
||||
throw new Error(error.message)
|
||||
}
|
||||
|
||||
revalidatePath('/dashboard/settings/packages')
|
||||
}
|
||||
127
src/app/dashboard/settings/packages/new/package-form.tsx
Normal file
127
src/app/dashboard/settings/packages/new/package-form.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
'use client'
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import * as z from "zod"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { createPackage } from "./actions"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useState } from "react"
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(2, "Paket ismi en az 2 karakter olmalıdır."),
|
||||
price: z.coerce.number().min(0, "Fiyat 0'dan küçük olamaz."),
|
||||
description: z.string().optional(),
|
||||
is_active: z.boolean().default(true),
|
||||
})
|
||||
|
||||
export function PackageForm() {
|
||||
const router = useRouter()
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
price: 0,
|
||||
description: "",
|
||||
is_active: true,
|
||||
},
|
||||
})
|
||||
|
||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
setLoading(true)
|
||||
try {
|
||||
await createPackage(values)
|
||||
router.push('/dashboard/settings/packages')
|
||||
router.refresh()
|
||||
} catch (error) {
|
||||
alert("Bir hata oluştu")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Paket İsmi</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Örn: Altın Menü" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="price"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Fiyat (₺)</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="number" placeholder="10000" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Açıklama / İçerik</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea placeholder="Paket içeriği..." {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="is_active"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel>
|
||||
Aktif
|
||||
</FormLabel>
|
||||
<FormDescription>
|
||||
Bu paket yeni rezervasyonlarda seçilebilir olsun.
|
||||
</FormDescription>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? "Kaydediliyor..." : "Kaydet"}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
17
src/app/dashboard/settings/packages/new/page.tsx
Normal file
17
src/app/dashboard/settings/packages/new/page.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { PackageForm } from "./package-form"
|
||||
|
||||
export default function NewPackagePage() {
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Yeni Paket Ekle</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<PackageForm />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
72
src/app/dashboard/settings/packages/page.tsx
Normal file
72
src/app/dashboard/settings/packages/page.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { createClient } from "@/lib/supabase/server"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Plus } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
|
||||
export default async function PackagesPage() {
|
||||
const supabase = await createClient()
|
||||
const { data: packages } = await supabase.from('packages').select('*').order('created_at', { ascending: false })
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-3xl font-bold tracking-tight">Fiyat Paketleri</h2>
|
||||
<Link href="/dashboard/settings/packages/new">
|
||||
<Button>
|
||||
<Plus className="mr-2 h-4 w-4" /> Yeni Paket Ekle
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Paket İsmi</TableHead>
|
||||
<TableHead>Fiyat</TableHead>
|
||||
<TableHead>Açıklama</TableHead>
|
||||
<TableHead>Durum</TableHead>
|
||||
<TableHead className="text-right">İşlemler</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{packages?.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="text-center h-24 text-muted-foreground">
|
||||
Henüz paket eklenmemiş.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
packages?.map((pkg) => (
|
||||
<TableRow key={pkg.id}>
|
||||
<TableCell className="font-medium">{pkg.name}</TableCell>
|
||||
<TableCell>₺{pkg.price}</TableCell>
|
||||
<TableCell>{pkg.description}</TableCell>
|
||||
<TableCell>
|
||||
{pkg.is_active ? (
|
||||
<Badge className="bg-green-600">Aktif</Badge>
|
||||
) : (
|
||||
<Badge variant="secondary">Pasif</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button variant="ghost" size="sm">Düzenle</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
39
src/app/dashboard/settings/page.tsx
Normal file
39
src/app/dashboard/settings/page.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import Link from "next/link"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Utensils, Users } from "lucide-react"
|
||||
|
||||
export default function SettingsPage() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-3xl font-bold tracking-tight">Ayarlar</h2>
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Link href="/dashboard/settings/packages">
|
||||
<Card className="hover:bg-muted/50 transition-colors cursor-pointer">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Utensils className="h-5 w-5" /> Fiyat Paketleri
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Menüler, hafta sonu tarifeleri ve paket fiyatlarını yönetin.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</Link>
|
||||
|
||||
<Link href="/dashboard/settings/users">
|
||||
<Card className="hover:bg-muted/50 transition-colors cursor-pointer">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Users className="h-5 w-5" /> Kullanıcı Yönetimi
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Personel hesapları ve yetkilendirmeleri yönetin.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user