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')
|
||||
|
||||
// Transform data for calendar
|
||||
const events = reservations?.map(res => ({
|
||||
id: res.id,
|
||||
title: `${res.customers?.full_name || 'Müşteri'} ${res.customers?.phone ? `(${res.customers.phone})` : ''}`,
|
||||
start: new Date(res.start_time),
|
||||
end: new Date(res.end_time),
|
||||
resource: res,
|
||||
})) || []
|
||||
const events = reservations?.map(res => {
|
||||
const customer = Array.isArray(res.customers) ? res.customers[0] : res.customers
|
||||
return {
|
||||
id: res.id,
|
||||
title: `${customer?.full_name || 'Müşteri'} ${customer?.phone ? `(${customer.phone})` : ''}`,
|
||||
start: new Date(res.start_time),
|
||||
end: new Date(res.end_time),
|
||||
resource: res,
|
||||
}
|
||||
}) || []
|
||||
|
||||
return (
|
||||
<div className="space-y-4 h-full">
|
||||
<div className="h-full -m-4 md:-m-8 p-0">
|
||||
<CalendarView events={events} halls={halls || []} />
|
||||
</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')
|
||||
.select('*', { count: 'exact', head: true })
|
||||
|
||||
// 3. Pending Payments (Sum of remaining balances)
|
||||
// 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.
|
||||
|
||||
// 3. Total Revenue (Paid)
|
||||
const { data: payments } = await supabase
|
||||
.from('payments')
|
||||
.select('amount')
|
||||
@@ -35,7 +28,16 @@ export default async function DashboardPage() {
|
||||
|
||||
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
|
||||
.from('reservations')
|
||||
.select(`
|
||||
@@ -50,7 +52,7 @@ export default async function DashboardPage() {
|
||||
.order('start_time', { ascending: true })
|
||||
.limit(5)
|
||||
|
||||
// 5. Recent Activities (Last 5 created reservations)
|
||||
// 6. Recent Activities (Last 5 created reservations)
|
||||
const { data: recentReservations } = await supabase
|
||||
.from('reservations')
|
||||
.select(`
|
||||
@@ -71,40 +73,6 @@ export default async function DashboardPage() {
|
||||
</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">{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">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
@@ -115,12 +83,63 @@ export default async function DashboardPage() {
|
||||
</div>
|
||||
</CardHeader>
|
||||
<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">
|
||||
Tahsil edilen ödemeler
|
||||
</p>
|
||||
</CardContent>
|
||||
</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 className="grid gap-6 md:grid-cols-2 lg:grid-cols-7">
|
||||
@@ -135,23 +154,27 @@ export default async function DashboardPage() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{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">
|
||||
<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" />
|
||||
{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 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>
|
||||
<p className="font-medium">{event.customers?.full_name}</p>
|
||||
<p className="text-sm text-muted-foreground">{event.halls?.name}</p>
|
||||
<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 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>
|
||||
)}
|
||||
</CardContent>
|
||||
@@ -162,19 +185,22 @@ export default async function DashboardPage() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{recentReservations?.map((res) => (
|
||||
<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">
|
||||
{res.customers?.full_name?.substring(0, 2).toUpperCase()}
|
||||
{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 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 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 && (
|
||||
<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}`)
|
||||
}
|
||||
|
||||
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) {
|
||||
const supabase = await createClient()
|
||||
|
||||
@@ -42,6 +58,15 @@ export async function updateStatus(id: string, status: string) {
|
||||
|
||||
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 })
|
||||
|
||||
revalidatePath(`/dashboard/reservations/${id}`)
|
||||
@@ -61,3 +86,20 @@ export async function deleteReservation(id: string) {
|
||||
await logAction('delete_reservation', 'reservation', id)
|
||||
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 { PaymentList } from "./payment-list"
|
||||
import { StatusActions } from "./status-actions"
|
||||
import { FinancialsEditor } from "./financials-editor"
|
||||
|
||||
export default async function ReservationDetailsPage({
|
||||
params,
|
||||
@@ -34,6 +35,12 @@ export default async function ReservationDetailsPage({
|
||||
notFound()
|
||||
}
|
||||
|
||||
const { data: packages } = await supabase
|
||||
.from('packages')
|
||||
.select('*')
|
||||
.eq('is_active', true)
|
||||
.order('price')
|
||||
|
||||
const { data: payments } = await supabase
|
||||
.from('payments')
|
||||
.select('*')
|
||||
@@ -41,8 +48,9 @@ export default async function ReservationDetailsPage({
|
||||
.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)
|
||||
// Use reservation.price if set (custom override), otherwise fallback to package price
|
||||
const effectivePrice = reservation.price ?? reservation.packages?.price ?? 0
|
||||
const remaining = Math.max(0, effectivePrice - totalPaid)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -132,9 +140,17 @@ export default async function ReservationDetailsPage({
|
||||
<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 className="flex items-center justify-between mb-1">
|
||||
<p className="text-sm font-medium text-muted-foreground">Toplam Tutar</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 className="bg-green-500/10 p-4 rounded-lg">
|
||||
<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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { useState } from "react"
|
||||
import { addPayment } from "./actions"
|
||||
import { addPayment, deletePayment, cancelPayment } from "./actions"
|
||||
import { format } from "date-fns"
|
||||
import { tr } from "date-fns/locale"
|
||||
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 {
|
||||
reservationId: string
|
||||
@@ -36,13 +44,34 @@ export function PaymentList({ reservationId, payments }: PaymentListProps) {
|
||||
try {
|
||||
await addPayment(reservationId, formData)
|
||||
setOpen(false)
|
||||
toast.success("Ödeme eklendi")
|
||||
} catch (error) {
|
||||
alert("Ödeme eklenirken hata oluştu")
|
||||
toast.error("Ödeme eklenirken hata oluştu")
|
||||
} finally {
|
||||
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 (
|
||||
<div className="space-y-4">
|
||||
<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">Yöntem</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>
|
||||
</thead>
|
||||
<tbody>
|
||||
{payments.length === 0 ? (
|
||||
<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.
|
||||
</td>
|
||||
</tr>
|
||||
@@ -133,10 +163,40 @@ export function PaymentList({ reservationId, payments }: PaymentListProps) {
|
||||
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 variant="outline" className={
|
||||
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>
|
||||
</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>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
--card-foreground: 224 71.4% 4.1%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 224 71.4% 4.1%;
|
||||
--primary: 262.1 83.3% 57.8%;
|
||||
--primary: 268 75% 50%;
|
||||
--primary-foreground: 210 20% 98%;
|
||||
--secondary: 220 14.3% 95.9%;
|
||||
--secondary-foreground: 220.9 39.3% 11%;
|
||||
@@ -66,7 +66,7 @@
|
||||
--destructive-foreground: 210 20% 98%;
|
||||
--border: 220 13% 91%;
|
||||
--input: 220 13% 91%;
|
||||
--ring: 262.1 83.3% 57.8%;
|
||||
--ring: 268 75% 50%;
|
||||
--radius: 0.75rem;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
@@ -82,7 +82,7 @@
|
||||
--card-foreground: 210 20% 98%;
|
||||
--popover: 224 71.4% 4.1%;
|
||||
--popover-foreground: 210 20% 98%;
|
||||
--primary: 263.4 70% 50.4%;
|
||||
--primary: 268 75% 60%;
|
||||
--primary-foreground: 210 20% 98%;
|
||||
--secondary: 215 27.9% 16.9%;
|
||||
--secondary-foreground: 210 20% 98%;
|
||||
@@ -94,7 +94,7 @@
|
||||
--destructive-foreground: 210 20% 98%;
|
||||
--border: 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-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
|
||||
@@ -55,8 +55,8 @@ export function CalendarView({ events = [], halls = [] }: CalendarViewProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="h-[calc(100vh-10rem)] min-h-[500px] flex flex-col">
|
||||
<CardHeader className="flex flex-col md:flex-row items-start md:items-center justify-between space-y-2 md:space-y-0 pb-4">
|
||||
<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 px-4 py-2">
|
||||
<CardTitle>Rezervasyon Takvimi</CardTitle>
|
||||
<div className="w-full md:w-[200px]">
|
||||
<Select value={selectedHallId} onValueChange={setSelectedHallId}>
|
||||
@@ -72,7 +72,7 @@ export function CalendarView({ events = [], halls = [] }: CalendarViewProps) {
|
||||
</Select>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 pb-4 overflow-hidden">
|
||||
<CardContent className="flex-1 p-0 overflow-hidden">
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger className="h-full w-full">
|
||||
<div className="h-full w-full overflow-x-auto">
|
||||
|
||||
@@ -4,7 +4,7 @@ import Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { cn } from "@/lib/utils"
|
||||
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> {
|
||||
onNavClick?: () => void
|
||||
@@ -48,6 +48,12 @@ export function MainNav({
|
||||
icon: Building2,
|
||||
active: pathname.startsWith("/dashboard/halls"),
|
||||
},
|
||||
{
|
||||
href: "/dashboard/expenses",
|
||||
label: "Giderler",
|
||||
icon: Receipt,
|
||||
active: pathname.startsWith("/dashboard/expenses"),
|
||||
},
|
||||
{
|
||||
href: "/dashboard/settings",
|
||||
label: "Ayarlar",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type NextRequest } from 'next/server'
|
||||
import { updateSession } from '@/lib/supabase/middleware'
|
||||
import { updateSession } from '@/lib/supabase/session'
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
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