Add expenses module, update dashboard financials, and enhance reservation payment management

This commit is contained in:
2025-12-04 16:31:49 +03:00
parent 6d777aa215
commit b89669f795
18 changed files with 801 additions and 98 deletions

View File

@@ -25,16 +25,19 @@ export default async function CalendarPage() {
.order('name') .order('name')
// Transform data for calendar // Transform data for calendar
const events = reservations?.map(res => ({ const events = reservations?.map(res => {
id: res.id, const customer = Array.isArray(res.customers) ? res.customers[0] : res.customers
title: `${res.customers?.full_name || 'Müşteri'} ${res.customers?.phone ? `(${res.customers.phone})` : ''}`, return {
start: new Date(res.start_time), id: res.id,
end: new Date(res.end_time), title: `${customer?.full_name || 'Müşteri'} ${customer?.phone ? `(${customer.phone})` : ''}`,
resource: res, start: new Date(res.start_time),
})) || [] end: new Date(res.end_time),
resource: res,
}
}) || []
return ( return (
<div className="space-y-4 h-full"> <div className="h-full -m-4 md:-m-8 p-0">
<CalendarView events={events} halls={halls || []} /> <CalendarView events={events} halls={halls || []} />
</div> </div>
) )

View File

@@ -0,0 +1,127 @@
'use client'
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { Trash2 } from "lucide-react"
import { createClient } from "@/lib/supabase/client"
import { toast } from "sonner"
import { useRouter } from "next/navigation"
export function CategoryManager({ initialCategories }: { initialCategories: any[] }) {
const [categories, setCategories] = useState(initialCategories)
const [newCategory, setNewCategory] = useState("")
const [loading, setLoading] = useState(false)
const router = useRouter()
const supabase = createClient()
const handleAdd = async (e: React.FormEvent) => {
e.preventDefault()
if (!newCategory.trim()) return
setLoading(true)
const { data, error } = await supabase
.from('expense_categories')
.insert({ name: newCategory })
.select()
.single()
if (error) {
toast.error("Kategori eklenirken hata oluştu")
console.error(error)
} else {
setCategories([data, ...categories])
setNewCategory("")
toast.success("Kategori eklendi")
router.refresh()
}
setLoading(false)
}
const handleDelete = async (id: string) => {
if (!confirm("Bu kategoriyi silmek istediğinize emin misiniz?")) return
const { error } = await supabase
.from('expense_categories')
.delete()
.eq('id', id)
if (error) {
toast.error("Silinirken hata oluştu. Bu kategoriye bağlı harcamalar olabilir.")
console.error(error)
} else {
setCategories(categories.filter(c => c.id !== id))
toast.success("Kategori silindi")
router.refresh()
}
}
return (
<div className="grid gap-6 md:grid-cols-2">
<div className="space-y-6">
<div className="rounded-lg border bg-card p-6">
<h3 className="text-lg font-medium mb-4">Yeni Kategori Ekle</h3>
<form onSubmit={handleAdd} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Kategori Adı</Label>
<Input
id="name"
placeholder="Örn: Mutfak, Personel, Elektrik"
value={newCategory}
onChange={(e) => setNewCategory(e.target.value)}
/>
</div>
<Button type="submit" disabled={loading || !newCategory.trim()}>
{loading ? "Ekleniyor..." : "Ekle"}
</Button>
</form>
</div>
</div>
<div className="rounded-lg border bg-card">
<Table>
<TableHeader>
<TableRow>
<TableHead>Kategori Adı</TableHead>
<TableHead className="w-[100px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{categories.length === 0 ? (
<TableRow>
<TableCell colSpan={2} className="text-center text-muted-foreground">
Kategori bulunamadı.
</TableCell>
</TableRow>
) : (
categories.map((category) => (
<TableRow key={category.id}>
<TableCell className="font-medium">{category.name}</TableCell>
<TableCell>
<Button
variant="ghost"
size="icon"
onClick={() => handleDelete(category.id)}
className="text-destructive hover:text-destructive/90"
>
<Trash2 className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</div>
)
}

View File

@@ -0,0 +1,31 @@
import { createClient } from "@/lib/supabase/server"
import { CategoryManager } from "./category-manager"
import { Button } from "@/components/ui/button"
import { ArrowLeft } from "lucide-react"
import Link from "next/link"
export default async function CategoriesPage() {
const supabase = await createClient()
const { data: categories } = await supabase
.from('expense_categories')
.select('*')
.order('created_at', { ascending: false })
return (
<div className="space-y-6">
<div className="flex items-center gap-4">
<Link href="/dashboard/expenses">
<Button variant="ghost" size="icon">
<ArrowLeft className="h-4 w-4" />
</Button>
</Link>
<div>
<h2 className="text-3xl font-bold tracking-tight">Gider Kategorileri</h2>
<p className="text-muted-foreground">Giderlerinizi gruplandırmak için kategoriler oluşturun.</p>
</div>
</div>
<CategoryManager initialCategories={categories || []} />
</div>
)
}

View File

@@ -0,0 +1,114 @@
'use client'
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { createClient } from "@/lib/supabase/client"
import { toast } from "sonner"
import { useRouter } from "next/navigation"
import { format } from "date-fns"
export function ExpenseForm({ categories }: { categories: any[] }) {
const [loading, setLoading] = useState(false)
const router = useRouter()
const supabase = createClient()
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
setLoading(true)
const formData = new FormData(e.currentTarget)
const amount = formData.get("amount")
const categoryId = formData.get("category_id")
const description = formData.get("description")
const date = formData.get("date")
const { error } = await supabase
.from('expenses')
.insert({
amount: parseFloat(amount as string),
category_id: categoryId === "general" || !categoryId ? null : categoryId,
description,
date: new Date(date as string).toISOString(),
})
if (error) {
toast.error("Gider eklenirken hata oluştu")
console.error(error)
} else {
toast.success("Gider kaydedildi")
router.push("/dashboard/expenses")
router.refresh()
}
setLoading(false)
}
return (
<form onSubmit={handleSubmit} className="space-y-6 max-w-lg">
<div className="space-y-2">
<Label htmlFor="amount">Tutar (TL)</Label>
<Input
id="amount"
name="amount"
type="number"
step="0.01"
placeholder="0.00"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="category_id">Kategori</Label>
<Select name="category_id">
<SelectTrigger>
<SelectValue placeholder="Kategori Seçin" />
</SelectTrigger>
<SelectContent>
<SelectItem value="general">Genel</SelectItem>
{categories.map((c) => (
<SelectItem key={c.id} value={c.id}>{c.name}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="date">Tarih</Label>
<Input
id="date"
name="date"
type="date"
defaultValue={format(new Date(), 'yyyy-MM-dd')}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="description">ıklama</Label>
<Textarea
id="description"
name="description"
placeholder="Harcama detayı..."
/>
</div>
<div className="flex gap-4">
<Button type="button" variant="outline" onClick={() => router.back()}>
İptal
</Button>
<Button type="submit" disabled={loading}>
{loading ? "Kaydediliyor..." : "Kaydet"}
</Button>
</div>
</form>
)
}

View File

@@ -0,0 +1,33 @@
import { createClient } from "@/lib/supabase/server"
import { ExpenseForm } from "./expense-form"
import { Button } from "@/components/ui/button"
import { ArrowLeft } from "lucide-react"
import Link from "next/link"
export default async function NewExpensePage() {
const supabase = await createClient()
const { data: categories } = await supabase
.from('expense_categories')
.select('*')
.order('name')
return (
<div className="space-y-6">
<div className="flex items-center gap-4">
<Link href="/dashboard/expenses">
<Button variant="ghost" size="icon">
<ArrowLeft className="h-4 w-4" />
</Button>
</Link>
<div>
<h2 className="text-3xl font-bold tracking-tight">Yeni Gider</h2>
<p className="text-muted-foreground">Yeni bir harcama kaydı oluşturun.</p>
</div>
</div>
<div className="border rounded-lg bg-card p-6">
<ExpenseForm categories={categories || []} />
</div>
</div>
)
}

View File

@@ -0,0 +1,90 @@
import { createClient } from "@/lib/supabase/server"
import { Button } from "@/components/ui/button"
import { Plus, Settings2 } from "lucide-react"
import Link from "next/link"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { format } from "date-fns"
import { tr } from "date-fns/locale"
export default async function ExpensesPage() {
const supabase = await createClient()
const { data: expenses } = await supabase
.from('expenses')
.select(`
*,
expense_categories (name)
`)
.order('date', { ascending: false })
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-3xl font-bold tracking-tight">Giderler</h2>
<p className="text-muted-foreground">İşletme giderlerini takip edin.</p>
</div>
<div className="flex gap-2">
<Link href="/dashboard/expenses/categories">
<Button variant="outline">
<Settings2 className="mr-2 h-4 w-4" />
Kategoriler
</Button>
</Link>
<Link href="/dashboard/expenses/new">
<Button>
<Plus className="mr-2 h-4 w-4" />
Yeni Gider
</Button>
</Link>
</div>
</div>
<div className="border rounded-lg bg-white dark:bg-card">
<Table>
<TableHeader>
<TableRow>
<TableHead>Tarih</TableHead>
<TableHead>Kategori</TableHead>
<TableHead>ıklama</TableHead>
<TableHead className="text-right">Tutar</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{!expenses || expenses.length === 0 ? (
<TableRow>
<TableCell colSpan={4} className="text-center h-24 text-muted-foreground">
Henüz gider kaydı bulunmuyor.
</TableCell>
</TableRow>
) : (
expenses.map((expense: any) => (
<TableRow key={expense.id}>
<TableCell>
{format(new Date(expense.date), 'd MMMM yyyy', { locale: tr })}
</TableCell>
<TableCell>
<span className="inline-flex items-center rounded-md bg-secondary px-2 py-1 text-xs font-medium text-secondary-foreground">
{expense.expense_categories?.name || 'Genel'}
</span>
</TableCell>
<TableCell>{expense.description}</TableCell>
<TableCell className="text-right font-medium text-red-600 dark:text-red-400">
- {new Intl.NumberFormat('tr-TR', { style: 'currency', currency: 'TRY' }).format(expense.amount)}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</div>
)
}

View File

@@ -20,14 +20,7 @@ export default async function DashboardPage() {
.from('customers') .from('customers')
.select('*', { count: 'exact', head: true }) .select('*', { count: 'exact', head: true })
// 3. Pending Payments (Sum of remaining balances) // 3. Total Revenue (Paid)
// This is complex to calculate in one query without a view or function,
// so we'll approximate or fetch pending payments directly if possible.
// For now, let's just count pending reservations as a proxy or fetch recent payments.
// Better: Sum of 'amount' from 'payments' where status = 'pending' (if we tracked pending payments that way)
// Or: Calculate total potential revenue vs paid revenue.
// Let's stick to "Total Revenue" (Paid) for now.
const { data: payments } = await supabase const { data: payments } = await supabase
.from('payments') .from('payments')
.select('amount') .select('amount')
@@ -35,7 +28,16 @@ export default async function DashboardPage() {
const totalRevenue = payments?.reduce((sum, p) => sum + Number(p.amount), 0) || 0 const totalRevenue = payments?.reduce((sum, p) => sum + Number(p.amount), 0) || 0
// 4. Upcoming Events (Next 5) // 4. Total Expenses
const { data: expensesData } = await supabase
.from('expenses')
.select('amount')
const totalExpenses = expensesData?.reduce((sum, e) => sum + Number(e.amount), 0) || 0
const netProfit = totalRevenue - totalExpenses
// 5. Upcoming Events (Next 5)
const { data: upcomingEvents } = await supabase const { data: upcomingEvents } = await supabase
.from('reservations') .from('reservations')
.select(` .select(`
@@ -50,7 +52,7 @@ export default async function DashboardPage() {
.order('start_time', { ascending: true }) .order('start_time', { ascending: true })
.limit(5) .limit(5)
// 5. Recent Activities (Last 5 created reservations) // 6. Recent Activities (Last 5 created reservations)
const { data: recentReservations } = await supabase const { data: recentReservations } = await supabase
.from('reservations') .from('reservations')
.select(` .select(`
@@ -71,40 +73,6 @@ export default async function DashboardPage() {
</div> </div>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4"> <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">{totalReservations || 0}</div>
<p className="text-xs text-muted-foreground mt-1 flex items-center">
Aktif rezervasyonlar
</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">
Toplam Müşteri
</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">{totalCustomers || 0}</div>
<p className="text-xs text-muted-foreground mt-1 flex items-center">
Kayıtlı müşteri sayısı
</p>
</CardContent>
</Card>
<Card className="card-hover border-l-4 border-l-green-500"> <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"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground"> <CardTitle className="text-sm font-medium text-muted-foreground">
@@ -115,12 +83,63 @@ export default async function DashboardPage() {
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-3xl font-bold mt-2">{totalRevenue.toLocaleString('tr-TR')}</div> <div className="text-2xl font-bold mt-2">{totalRevenue.toLocaleString('tr-TR')}</div>
<p className="text-xs text-muted-foreground mt-1 flex items-center"> <p className="text-xs text-muted-foreground mt-1 flex items-center">
Tahsil edilen ödemeler Tahsil edilen ödemeler
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
<Card className="card-hover border-l-4 border-l-red-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 Gider
</CardTitle>
<div className="h-8 w-8 rounded-full bg-red-100 dark:bg-red-900/20 flex items-center justify-center">
<TrendingUp className="h-4 w-4 text-red-600 dark:text-red-400 rotate-180" />
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold mt-2">{totalExpenses.toLocaleString('tr-TR')}</div>
<p className="text-xs text-muted-foreground mt-1 flex items-center">
İşletme giderleri
</p>
</CardContent>
</Card>
<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">
Net Kâr
</CardTitle>
<div className="h-8 w-8 rounded-full bg-blue-100 dark:bg-blue-900/20 flex items-center justify-center">
<TrendingUp className="h-4 w-4 text-blue-600 dark:text-blue-400" />
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold mt-2">{netProfit.toLocaleString('tr-TR')}</div>
<p className="text-xs text-muted-foreground mt-1 flex items-center">
Gelir - Gider
</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">
Toplam Rezervasyon
</CardTitle>
<div className="h-8 w-8 rounded-full bg-purple-100 dark:bg-purple-900/20 flex items-center justify-center">
<CalendarDays className="h-4 w-4 text-purple-600 dark:text-purple-400" />
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold mt-2">{totalReservations || 0}</div>
<p className="text-xs text-muted-foreground mt-1 flex items-center">
Aktif rezervasyonlar
</p>
</CardContent>
</Card>
</div> </div>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-7"> <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-7">
@@ -135,23 +154,27 @@ export default async function DashboardPage() {
</div> </div>
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4">
{upcomingEvents?.map((event) => ( {upcomingEvents?.map((event) => {
<div key={event.id} className="flex items-center justify-between p-4 border rounded-lg bg-card hover:bg-accent/50 transition-colors"> const customer = Array.isArray(event.customers) ? event.customers[0] : event.customers
<div className="flex items-center gap-4"> const hall = Array.isArray(event.halls) ? event.halls[0] : event.halls
<div className="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center text-primary"> return (
<CalendarIcon className="h-5 w-5" /> <div key={event.id} className="flex items-center justify-between p-4 border rounded-lg bg-card hover:bg-accent/50 transition-colors">
<div className="flex items-center gap-4">
<div className="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center text-primary">
<CalendarIcon className="h-5 w-5" />
</div>
<div>
<p className="font-medium">{customer?.full_name}</p>
<p className="text-sm text-muted-foreground">{hall?.name}</p>
</div>
</div> </div>
<div> <div className="text-right">
<p className="font-medium">{event.customers?.full_name}</p> <p className="font-medium">{format(new Date(event.start_time), 'd MMM yyyy', { locale: tr })}</p>
<p className="text-sm text-muted-foreground">{event.halls?.name}</p> <p className="text-sm text-muted-foreground">{format(new Date(event.start_time), 'HH:mm')}</p>
</div> </div>
</div> </div>
<div className="text-right"> )
<p className="font-medium">{format(new Date(event.start_time), 'd MMM yyyy', { locale: tr })}</p> })}
<p className="text-sm text-muted-foreground">{format(new Date(event.start_time), 'HH:mm')}</p>
</div>
</div>
))}
</div> </div>
)} )}
</CardContent> </CardContent>
@@ -162,19 +185,22 @@ export default async function DashboardPage() {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-4"> <div className="space-y-4">
{recentReservations?.map((res) => ( {recentReservations?.map((res) => {
<div key={res.id} className="flex items-center gap-4 p-3 rounded-lg hover:bg-muted/50 transition-colors"> const customer = Array.isArray(res.customers) ? res.customers[0] : res.customers
<div className="h-9 w-9 rounded-full bg-primary/10 flex items-center justify-center text-primary font-bold text-sm"> return (
{res.customers?.full_name?.substring(0, 2).toUpperCase()} <div key={res.id} 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">
{customer?.full_name?.substring(0, 2).toUpperCase()}
</div>
<div className="flex-1">
<p className="text-sm font-medium">{customer?.full_name} rezervasyon oluşturdu</p>
<p className="text-xs text-muted-foreground">
{format(new Date(res.created_at), 'd MMM HH:mm', { locale: tr })}
</p>
</div>
</div> </div>
<div className="flex-1"> )
<p className="text-sm font-medium">{res.customers?.full_name} rezervasyon oluşturdu</p> })}
<p className="text-xs text-muted-foreground">
{format(new Date(res.created_at), 'd MMM HH:mm', { locale: tr })}
</p>
</div>
</div>
))}
{recentReservations?.length === 0 && ( {recentReservations?.length === 0 && (
<p className="text-sm text-muted-foreground text-center py-4">Henüz işlem yok.</p> <p className="text-sm text-muted-foreground text-center py-4">Henüz işlem yok.</p>
)} )}

View File

@@ -32,6 +32,22 @@ export async function addPayment(reservationId: string, formData: FormData) {
revalidatePath(`/dashboard/reservations/${reservationId}`) revalidatePath(`/dashboard/reservations/${reservationId}`)
} }
export async function deletePayment(id: string, reservationId: string) {
const supabase = await createClient()
const { error } = await supabase.from('payments').delete().eq('id', id)
if (error) throw new Error(error.message)
await logAction('delete_payment', 'payment', id, { reservationId })
revalidatePath(`/dashboard/reservations/${reservationId}`)
}
export async function cancelPayment(id: string, reservationId: string) {
const supabase = await createClient()
const { error } = await supabase.from('payments').update({ status: 'refunded' }).eq('id', id)
if (error) throw new Error(error.message)
await logAction('cancel_payment', 'payment', id, { reservationId })
revalidatePath(`/dashboard/reservations/${reservationId}`)
}
export async function updateStatus(id: string, status: string) { export async function updateStatus(id: string, status: string) {
const supabase = await createClient() const supabase = await createClient()
@@ -42,6 +58,15 @@ export async function updateStatus(id: string, status: string) {
if (error) throw new Error(error.message) if (error) throw new Error(error.message)
if (status === 'cancelled') {
const { error: paymentError } = await supabase
.from('payments')
.update({ status: 'refunded' })
.eq('reservation_id', id)
if (paymentError) console.error("Error cancelling payments:", paymentError)
}
await logAction('update_reservation_status', 'reservation', id, { status }) await logAction('update_reservation_status', 'reservation', id, { status })
revalidatePath(`/dashboard/reservations/${id}`) revalidatePath(`/dashboard/reservations/${id}`)
@@ -61,3 +86,20 @@ export async function deleteReservation(id: string) {
await logAction('delete_reservation', 'reservation', id) await logAction('delete_reservation', 'reservation', id)
revalidatePath('/dashboard/reservations') revalidatePath('/dashboard/reservations')
} }
export async function updateReservationFinancials(id: string, packageId: string | null, price: number) {
const supabase = await createClient()
const { error } = await supabase
.from('reservations')
.update({
package_id: packageId,
price: price
})
.eq('id', id)
if (error) throw new Error(error.message)
await logAction('update_reservation_financials', 'reservation', id, { packageId, price })
revalidatePath(`/dashboard/reservations/${id}`)
}

View File

@@ -0,0 +1,122 @@
'use client'
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Edit2 } from "lucide-react"
import { updateReservationFinancials } from "./actions"
import { toast } from "sonner"
interface FinancialsEditorProps {
reservationId: string
currentPackageId: string | null
currentPrice: number
packages: any[]
}
export function FinancialsEditor({ reservationId, currentPackageId, currentPrice, packages }: FinancialsEditorProps) {
const [open, setOpen] = useState(false)
const [loading, setLoading] = useState(false)
const [packageId, setPackageId] = useState<string>(currentPackageId || "custom")
const [price, setPrice] = useState<number>(currentPrice)
const handlePackageChange = (value: string) => {
setPackageId(value)
if (value !== "custom") {
const selectedPkg = packages.find(p => p.id === value)
if (selectedPkg) {
setPrice(selectedPkg.price)
}
}
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
try {
await updateReservationFinancials(
reservationId,
packageId === "custom" ? null : packageId,
price
)
toast.success("Finansal bilgiler güncellendi")
setOpen(false)
} catch (error) {
toast.error("Güncellenirken hata oluştu")
console.error(error)
} finally {
setLoading(false)
}
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="ghost" size="icon" className="h-6 w-6 ml-2">
<Edit2 className="h-3 w-3" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Tutar ve Paket Düzenle</DialogTitle>
<DialogDescription>
Rezervasyonun paketini ve toplam tutarını güncelleyin.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label>Paket Seçimi</Label>
<Select value={packageId} onValueChange={handlePackageChange}>
<SelectTrigger>
<SelectValue placeholder="Paket Seçin" />
</SelectTrigger>
<SelectContent>
<SelectItem value="custom">Özel (Paketsiz)</SelectItem>
{packages.map(pkg => (
<SelectItem key={pkg.id} value={pkg.id}>
{pkg.name} - {pkg.price} TL
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Toplam Tutar ()</Label>
<Input
type="number"
value={price}
onChange={(e) => setPrice(Number(e.target.value))}
step="0.01"
min="0"
/>
</div>
<DialogFooter>
<Button type="submit" disabled={loading}>
{loading ? "Kaydediliyor..." : "Kaydet"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}

View File

@@ -10,6 +10,7 @@ import { ArrowLeft, Calendar, Clock, MapPin, User, CreditCard } from "lucide-rea
import Link from "next/link" import Link from "next/link"
import { PaymentList } from "./payment-list" import { PaymentList } from "./payment-list"
import { StatusActions } from "./status-actions" import { StatusActions } from "./status-actions"
import { FinancialsEditor } from "./financials-editor"
export default async function ReservationDetailsPage({ export default async function ReservationDetailsPage({
params, params,
@@ -34,6 +35,12 @@ export default async function ReservationDetailsPage({
notFound() notFound()
} }
const { data: packages } = await supabase
.from('packages')
.select('*')
.eq('is_active', true)
.order('price')
const { data: payments } = await supabase const { data: payments } = await supabase
.from('payments') .from('payments')
.select('*') .select('*')
@@ -41,8 +48,9 @@ export default async function ReservationDetailsPage({
.order('created_at', { ascending: false }) .order('created_at', { ascending: false })
const totalPaid = payments?.reduce((sum, p) => sum + (p.status === 'paid' ? Number(p.amount) : 0), 0) || 0 const totalPaid = payments?.reduce((sum, p) => sum + (p.status === 'paid' ? Number(p.amount) : 0), 0) || 0
const packagePrice = reservation.packages?.price || 0 // Use reservation.price if set (custom override), otherwise fallback to package price
const remaining = Math.max(0, packagePrice - totalPaid) const effectivePrice = reservation.price ?? reservation.packages?.price ?? 0
const remaining = Math.max(0, effectivePrice - totalPaid)
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@@ -132,9 +140,17 @@ export default async function ReservationDetailsPage({
<CardContent> <CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6"> <div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
<div className="bg-muted/50 p-4 rounded-lg"> <div className="bg-muted/50 p-4 rounded-lg">
<p className="text-sm font-medium text-muted-foreground">Toplam Tutar</p> <div className="flex items-center justify-between mb-1">
<p className="text-2xl font-bold">{packagePrice}</p> <p className="text-sm font-medium text-muted-foreground">Toplam Tutar</p>
<p className="text-xs text-muted-foreground">{reservation.packages?.name || "Paket Yok"}</p> <FinancialsEditor
reservationId={reservation.id}
currentPackageId={reservation.package_id}
currentPrice={effectivePrice}
packages={packages || []}
/>
</div>
<p className="text-2xl font-bold">{effectivePrice}</p>
<p className="text-xs text-muted-foreground">{reservation.packages?.name || "Özel Fiyat / Paket Yok"}</p>
</div> </div>
<div className="bg-green-500/10 p-4 rounded-lg"> <div className="bg-green-500/10 p-4 rounded-lg">
<p className="text-sm font-medium text-green-600">Ödenen</p> <p className="text-sm font-medium text-green-600">Ödenen</p>

View File

@@ -14,10 +14,18 @@ import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { useState } from "react" import { useState } from "react"
import { addPayment } from "./actions" import { addPayment, deletePayment, cancelPayment } from "./actions"
import { format } from "date-fns" import { format } from "date-fns"
import { tr } from "date-fns/locale" import { tr } from "date-fns/locale"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { MoreVertical, Trash2, Ban } from "lucide-react"
import { toast } from "sonner"
interface PaymentListProps { interface PaymentListProps {
reservationId: string reservationId: string
@@ -36,13 +44,34 @@ export function PaymentList({ reservationId, payments }: PaymentListProps) {
try { try {
await addPayment(reservationId, formData) await addPayment(reservationId, formData)
setOpen(false) setOpen(false)
toast.success("Ödeme eklendi")
} catch (error) { } catch (error) {
alert("Ödeme eklenirken hata oluştu") toast.error("Ödeme eklenirken hata oluştu")
} finally { } finally {
setLoading(false) setLoading(false)
} }
} }
const handleDelete = async (id: string) => {
if (!confirm("Bu ödemeyi silmek istediğinize emin misiniz?")) return
try {
await deletePayment(id, reservationId)
toast.success("Ödeme silindi")
} catch (error) {
toast.error("Silinirken hata oluştu")
}
}
const handleCancel = async (id: string) => {
if (!confirm("Bu ödemeyi iptal/iade etmek istediğinize emin misiniz?")) return
try {
await cancelPayment(id, reservationId)
toast.success("Ödeme iptal edildi")
} catch (error) {
toast.error("İptal edilirken hata oluştu")
}
}
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -108,12 +137,13 @@ export function PaymentList({ reservationId, payments }: PaymentListProps) {
<th className="h-10 px-4 text-left font-medium">Tür</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">Yöntem</th>
<th className="h-10 px-4 text-left font-medium">Durum</th> <th className="h-10 px-4 text-left font-medium">Durum</th>
<th className="h-10 px-4 text-right font-medium">İşlemler</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{payments.length === 0 ? ( {payments.length === 0 ? (
<tr> <tr>
<td colSpan={5} className="h-24 text-center text-muted-foreground"> <td colSpan={6} className="h-24 text-center text-muted-foreground">
Henüz ödeme kaydı yok. Henüz ödeme kaydı yok.
</td> </td>
</tr> </tr>
@@ -133,10 +163,40 @@ export function PaymentList({ reservationId, payments }: PaymentListProps) {
payment.payment_method === 'credit_card' ? 'Kredi Kartı' : 'Havale'} payment.payment_method === 'credit_card' ? 'Kredi Kartı' : 'Havale'}
</td> </td>
<td className="p-4"> <td className="p-4">
<Badge variant="outline" className="bg-green-50 text-green-700 border-green-200"> <Badge variant="outline" className={
Ödendi payment.status === 'refunded'
? "bg-red-50 text-red-700 border-red-200"
: "bg-green-50 text-green-700 border-green-200"
}>
{payment.status === 'refunded' ? 'İade/İptal' : 'Ödendi'}
</Badge> </Badge>
</td> </td>
<td className="p-4 text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
className="text-destructive focus:text-destructive"
onClick={() => handleCancel(payment.id)}
disabled={payment.status === 'refunded'}
>
<Ban className="mr-2 h-4 w-4" />
İptal / İade Et
</DropdownMenuItem>
<DropdownMenuItem
className="text-destructive focus:text-destructive"
onClick={() => handleDelete(payment.id)}
>
<Trash2 className="mr-2 h-4 w-4" />
Sil
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</td>
</tr> </tr>
)) ))
)} )}

View File

@@ -54,7 +54,7 @@
--card-foreground: 224 71.4% 4.1%; --card-foreground: 224 71.4% 4.1%;
--popover: 0 0% 100%; --popover: 0 0% 100%;
--popover-foreground: 224 71.4% 4.1%; --popover-foreground: 224 71.4% 4.1%;
--primary: 262.1 83.3% 57.8%; --primary: 268 75% 50%;
--primary-foreground: 210 20% 98%; --primary-foreground: 210 20% 98%;
--secondary: 220 14.3% 95.9%; --secondary: 220 14.3% 95.9%;
--secondary-foreground: 220.9 39.3% 11%; --secondary-foreground: 220.9 39.3% 11%;
@@ -66,7 +66,7 @@
--destructive-foreground: 210 20% 98%; --destructive-foreground: 210 20% 98%;
--border: 220 13% 91%; --border: 220 13% 91%;
--input: 220 13% 91%; --input: 220 13% 91%;
--ring: 262.1 83.3% 57.8%; --ring: 268 75% 50%;
--radius: 0.75rem; --radius: 0.75rem;
--chart-1: 12 76% 61%; --chart-1: 12 76% 61%;
--chart-2: 173 58% 39%; --chart-2: 173 58% 39%;
@@ -82,7 +82,7 @@
--card-foreground: 210 20% 98%; --card-foreground: 210 20% 98%;
--popover: 224 71.4% 4.1%; --popover: 224 71.4% 4.1%;
--popover-foreground: 210 20% 98%; --popover-foreground: 210 20% 98%;
--primary: 263.4 70% 50.4%; --primary: 268 75% 60%;
--primary-foreground: 210 20% 98%; --primary-foreground: 210 20% 98%;
--secondary: 215 27.9% 16.9%; --secondary: 215 27.9% 16.9%;
--secondary-foreground: 210 20% 98%; --secondary-foreground: 210 20% 98%;
@@ -94,7 +94,7 @@
--destructive-foreground: 210 20% 98%; --destructive-foreground: 210 20% 98%;
--border: 215 27.9% 16.9%; --border: 215 27.9% 16.9%;
--input: 215 27.9% 16.9%; --input: 215 27.9% 16.9%;
--ring: 263.4 70% 50.4%; --ring: 268 75% 60%;
--chart-1: 220 70% 50%; --chart-1: 220 70% 50%;
--chart-2: 160 60% 45%; --chart-2: 160 60% 45%;
--chart-3: 30 80% 55%; --chart-3: 30 80% 55%;

View File

@@ -55,8 +55,8 @@ export function CalendarView({ events = [], halls = [] }: CalendarViewProps) {
} }
return ( return (
<Card className="h-[calc(100vh-10rem)] min-h-[500px] flex flex-col"> <Card className="h-[calc(100vh-4rem)] flex flex-col border-0 shadow-none rounded-none">
<CardHeader className="flex flex-col md:flex-row items-start md:items-center justify-between space-y-2 md:space-y-0 pb-4"> <CardHeader className="flex flex-col md:flex-row items-start md:items-center justify-between space-y-2 md:space-y-0 px-4 py-2">
<CardTitle>Rezervasyon Takvimi</CardTitle> <CardTitle>Rezervasyon Takvimi</CardTitle>
<div className="w-full md:w-[200px]"> <div className="w-full md:w-[200px]">
<Select value={selectedHallId} onValueChange={setSelectedHallId}> <Select value={selectedHallId} onValueChange={setSelectedHallId}>
@@ -72,7 +72,7 @@ export function CalendarView({ events = [], halls = [] }: CalendarViewProps) {
</Select> </Select>
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="flex-1 pb-4 overflow-hidden"> <CardContent className="flex-1 p-0 overflow-hidden">
<ContextMenu> <ContextMenu>
<ContextMenuTrigger className="h-full w-full"> <ContextMenuTrigger className="h-full w-full">
<div className="h-full w-full overflow-x-auto"> <div className="h-full w-full overflow-x-auto">

View File

@@ -4,7 +4,7 @@ import Link from "next/link"
import { usePathname } from "next/navigation" import { usePathname } from "next/navigation"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { CalendarDays, Users, Home, Settings, Building2, CreditCard, LogOut } from "lucide-react" import { CalendarDays, Users, Home, Settings, Building2, CreditCard, LogOut, Receipt } from "lucide-react"
interface MainNavProps extends React.HTMLAttributes<HTMLElement> { interface MainNavProps extends React.HTMLAttributes<HTMLElement> {
onNavClick?: () => void onNavClick?: () => void
@@ -48,6 +48,12 @@ export function MainNav({
icon: Building2, icon: Building2,
active: pathname.startsWith("/dashboard/halls"), active: pathname.startsWith("/dashboard/halls"),
}, },
{
href: "/dashboard/expenses",
label: "Giderler",
icon: Receipt,
active: pathname.startsWith("/dashboard/expenses"),
},
{ {
href: "/dashboard/settings", href: "/dashboard/settings",
label: "Ayarlar", label: "Ayarlar",

View File

@@ -1,5 +1,5 @@
import { type NextRequest } from 'next/server' import { type NextRequest } from 'next/server'
import { updateSession } from '@/lib/supabase/middleware' import { updateSession } from '@/lib/supabase/session'
export async function middleware(request: NextRequest) { export async function middleware(request: NextRequest) {
return await updateSession(request) return await updateSession(request)

View File

@@ -0,0 +1,25 @@
-- Create Expense Categories Table
create table expense_categories (
id uuid default uuid_generate_v4() primary key,
name text not null,
description text,
created_at timestamp with time zone default timezone('utc'::text, now()) not null
);
-- Create Expenses Table
create table expenses (
id uuid default uuid_generate_v4() primary key,
category_id uuid references expense_categories(id) on delete set null,
amount decimal(10,2) not null,
description text,
date timestamp with time zone default timezone('utc'::text, now()) not null,
created_by uuid references auth.users(id),
created_at timestamp with time zone default timezone('utc'::text, now()) not null
);
-- RLS
alter table expense_categories enable row level security;
alter table expenses enable row level security;
create policy "Enable all access for authenticated users" on expense_categories for all using (auth.role() = 'authenticated');
create policy "Enable all access for authenticated users" on expenses for all using (auth.role() = 'authenticated');

View File

@@ -0,0 +1,8 @@
-- Add price column to reservations table
alter table reservations add column price decimal(10,2);
-- Optional: Update existing reservations to set price from their package (snapshotting the price)
update reservations
set price = packages.price
from packages
where reservations.package_id = packages.id;