Add expenses module, update dashboard financials, and enhance reservation payment management
This commit is contained in:
@@ -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 => {
|
||||||
|
const customer = Array.isArray(res.customers) ? res.customers[0] : res.customers
|
||||||
|
return {
|
||||||
id: res.id,
|
id: res.id,
|
||||||
title: `${res.customers?.full_name || 'Müşteri'} ${res.customers?.phone ? `(${res.customers.phone})` : ''}`,
|
title: `${customer?.full_name || 'Müşteri'} ${customer?.phone ? `(${customer.phone})` : ''}`,
|
||||||
start: new Date(res.start_time),
|
start: new Date(res.start_time),
|
||||||
end: new Date(res.end_time),
|
end: new Date(res.end_time),
|
||||||
resource: res,
|
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>
|
||||||
)
|
)
|
||||||
|
|||||||
127
src/app/dashboard/expenses/categories/category-manager.tsx
Normal file
127
src/app/dashboard/expenses/categories/category-manager.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
31
src/app/dashboard/expenses/categories/page.tsx
Normal file
31
src/app/dashboard/expenses/categories/page.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
114
src/app/dashboard/expenses/new/expense-form.tsx
Normal file
114
src/app/dashboard/expenses/new/expense-form.tsx
Normal 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">Açı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>
|
||||||
|
)
|
||||||
|
}
|
||||||
33
src/app/dashboard/expenses/new/page.tsx
Normal file
33
src/app/dashboard/expenses/new/page.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
90
src/app/dashboard/expenses/page.tsx
Normal file
90
src/app/dashboard/expenses/page.tsx
Normal 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>Açı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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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,15 +154,18 @@ export default async function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{upcomingEvents?.map((event) => (
|
{upcomingEvents?.map((event) => {
|
||||||
|
const customer = Array.isArray(event.customers) ? event.customers[0] : event.customers
|
||||||
|
const hall = Array.isArray(event.halls) ? event.halls[0] : event.halls
|
||||||
|
return (
|
||||||
<div key={event.id} className="flex items-center justify-between p-4 border rounded-lg bg-card hover:bg-accent/50 transition-colors">
|
<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="flex items-center gap-4">
|
||||||
<div className="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center text-primary">
|
<div className="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center text-primary">
|
||||||
<CalendarIcon className="h-5 w-5" />
|
<CalendarIcon className="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">{event.customers?.full_name}</p>
|
<p className="font-medium">{customer?.full_name}</p>
|
||||||
<p className="text-sm text-muted-foreground">{event.halls?.name}</p>
|
<p className="text-sm text-muted-foreground">{hall?.name}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
@@ -151,7 +173,8 @@ export default async function DashboardPage() {
|
|||||||
<p className="text-sm text-muted-foreground">{format(new Date(event.start_time), 'HH:mm')}</p>
|
<p className="text-sm text-muted-foreground">{format(new Date(event.start_time), 'HH:mm')}</p>
|
||||||
</div>
|
</div>
|
||||||
</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) => {
|
||||||
|
const customer = Array.isArray(res.customers) ? res.customers[0] : res.customers
|
||||||
|
return (
|
||||||
<div key={res.id} className="flex items-center gap-4 p-3 rounded-lg hover:bg-muted/50 transition-colors">
|
<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">
|
<div className="h-9 w-9 rounded-full bg-primary/10 flex items-center justify-center text-primary font-bold text-sm">
|
||||||
{res.customers?.full_name?.substring(0, 2).toUpperCase()}
|
{customer?.full_name?.substring(0, 2).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<p className="text-sm font-medium">{res.customers?.full_name} rezervasyon oluşturdu</p>
|
<p className="text-sm font-medium">{customer?.full_name} rezervasyon oluşturdu</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{format(new Date(res.created_at), 'd MMM HH:mm', { locale: tr })}
|
{format(new Date(res.created_at), 'd MMM HH:mm', { locale: tr })}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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}`)
|
||||||
|
}
|
||||||
|
|||||||
122
src/app/dashboard/reservations/[id]/financials-editor.tsx
Normal file
122
src/app/dashboard/reservations/[id]/financials-editor.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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">
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
<p className="text-sm font-medium text-muted-foreground">Toplam Tutar</p>
|
<p className="text-sm font-medium text-muted-foreground">Toplam Tutar</p>
|
||||||
<p className="text-2xl font-bold">₺{packagePrice}</p>
|
<FinancialsEditor
|
||||||
<p className="text-xs text-muted-foreground">{reservation.packages?.name || "Paket Yok"}</p>
|
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>
|
||||||
|
|||||||
@@ -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>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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%;
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
25
supabase_schema_expenses.sql
Normal file
25
supabase_schema_expenses.sql
Normal 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');
|
||||||
8
supabase_schema_update_price.sql
Normal file
8
supabase_schema_update_price.sql
Normal 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;
|
||||||
Reference in New Issue
Block a user