diff --git a/PLANLAMA.md b/PLANLAMA.md new file mode 100644 index 0000000..3c80e55 --- /dev/null +++ b/PLANLAMA.md @@ -0,0 +1,23 @@ +# ParaKasa Geliştirme Planı (Revize Edildi) + +**Vizyon:** Proje, tek bir çatı altında iki farklı uygulama gibi çalışacaktır. + +## 1. Web Sitesi (Public) +- Herkese açık kurumsal web sitesi. +- Ürünler, kategoriler ve iletişim bilgileri sergilenecek. +- Veriler Yönetim Panelinden (CMS) çekilecek. + +## 2. Yönetim Paneli (Private) +- Sadece firma yetkililerinin giriş yapabileceği kapalı devre sistem. +- **Fonksiyonlar:** + - **CMS:** Web sitesinin içeriğini (ürünler, slider, metinler) yönetme. + - **ERP (İç Yönetim):** Stok takibi, gelir/gider hesapları, sipariş yönetimi. +- **Durum:** Bu bölümün tasarımı ve akışı tamamen baştan kurgulanacak. Şimdilik mevcut haliyle donduruldu. + +--- + +## Mevcut Durum (Tamamlananlar) +- [x] Kullanıcı Yönetimi (Admin Ekle/Sil). +- [x] Temel Site Ayarları (Başlık, İletişim). +- [x] Ürün Yönetimi (Temel CRUD). +- [x] Kategori Yönetimi (Arayüz hazır, veritabanı bekleniyor). diff --git a/app/(dashboard)/dashboard/categories/[id]/page.tsx b/app/(dashboard)/dashboard/categories/[id]/page.tsx new file mode 100644 index 0000000..99bd894 --- /dev/null +++ b/app/(dashboard)/dashboard/categories/[id]/page.tsx @@ -0,0 +1,22 @@ +import { createClient } from "@/lib/supabase-server" +import { CategoryForm } from "@/components/dashboard/category-form" +import { notFound } from "next/navigation" + +export default async function EditCategoryPage({ params }: { params: { id: string } }) { + const supabase = createClient() + const { data: category } = await supabase + .from('categories') + .select('*') + .eq('id', params.id) + .single() + + if (!category) { + notFound() + } + + return ( +
+ +
+ ) +} diff --git a/app/(dashboard)/dashboard/categories/actions.ts b/app/(dashboard)/dashboard/categories/actions.ts new file mode 100644 index 0000000..c4d9451 --- /dev/null +++ b/app/(dashboard)/dashboard/categories/actions.ts @@ -0,0 +1,107 @@ +"use server" + +import { createClient } from "@/lib/supabase-server" +import { createClient as createSupabaseClient } from "@supabase/supabase-js" +import { revalidatePath } from "next/cache" + +// Admin client for privileged operations +const supabaseAdmin = createSupabaseClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.SUPABASE_SERVICE_ROLE_KEY!, + { + auth: { + autoRefreshToken: false, + persistSession: false + } + } +) + +export async function createCategory(data: { name: string, description?: string, image_url?: string }) { + const supabase = createClient() + + // Check admin + const { data: { user } } = await supabase.auth.getUser() + if (!user) return { error: "Oturum açmanız gerekiyor." } + + // Check if current user is admin + const { data: profile } = await supabase.from('profiles').select('role').eq('id', user.id).single() + if (profile?.role !== 'admin') return { error: "Yetkisiz işlem." } + + // Generate slug from name + const slug = data.name.toLowerCase() + .replace(/ğ/g, 'g') + .replace(/ü/g, 'u') + .replace(/ş/g, 's') + .replace(/ı/g, 'i') + .replace(/ö/g, 'o') + .replace(/ç/g, 'c') + .replace(/[^a-z0-9]/g, '-') + .replace(/-+/g, '-') + .replace(/^-|-$/g, '') + + const { error } = await supabaseAdmin.from('categories').insert({ + name: data.name, + slug: slug, + description: data.description, + image_url: data.image_url + }) + + if (error) return { error: "Kategori oluşturulamadı: " + error.message } + + revalidatePath("/dashboard/categories") + return { success: true } +} + +export async function updateCategory(id: string, data: { name: string, description?: string, image_url?: string }) { + const supabase = createClient() + + // Check admin + const { data: { user } } = await supabase.auth.getUser() + if (!user) return { error: "Oturum açmanız gerekiyor." } + + // Check if current user is admin + const { data: profile } = await supabase.from('profiles').select('role').eq('id', user.id).single() + if (profile?.role !== 'admin') return { error: "Yetkisiz işlem." } + + const slug = data.name.toLowerCase() + .replace(/ğ/g, 'g') + .replace(/ü/g, 'u') + .replace(/ş/g, 's') + .replace(/ı/g, 'i') + .replace(/ö/g, 'o') + .replace(/ç/g, 'c') + .replace(/[^a-z0-9]/g, '-') + .replace(/-+/g, '-') + .replace(/^-|-$/g, '') + + const { error } = await supabaseAdmin.from('categories').update({ + name: data.name, + slug: slug, + description: data.description, + image_url: data.image_url + }).eq('id', id) + + if (error) return { error: "Kategori güncellenemedi: " + error.message } + + revalidatePath("/dashboard/categories") + return { success: true } +} + +export async function deleteCategory(id: string) { + const supabase = createClient() + + // Check admin + const { data: { user } } = await supabase.auth.getUser() + if (!user) return { error: "Oturum açmanız gerekiyor." } + + // Check if current user is admin + const { data: profile } = await supabase.from('profiles').select('role').eq('id', user.id).single() + if (profile?.role !== 'admin') return { error: "Yetkisiz işlem." } + + const { error } = await supabaseAdmin.from('categories').delete().eq('id', id) + + if (error) return { error: "Kategori silinemedi: " + error.message } + + revalidatePath("/dashboard/categories") + return { success: true } +} diff --git a/app/(dashboard)/dashboard/categories/new/page.tsx b/app/(dashboard)/dashboard/categories/new/page.tsx new file mode 100644 index 0000000..b484e34 --- /dev/null +++ b/app/(dashboard)/dashboard/categories/new/page.tsx @@ -0,0 +1,9 @@ +import { CategoryForm } from "@/components/dashboard/category-form" + +export default function NewCategoryPage() { + return ( +
+ +
+ ) +} diff --git a/app/(dashboard)/dashboard/categories/page.tsx b/app/(dashboard)/dashboard/categories/page.tsx new file mode 100644 index 0000000..3e7bfed --- /dev/null +++ b/app/(dashboard)/dashboard/categories/page.tsx @@ -0,0 +1,78 @@ +import { createClient } from "@/lib/supabase-server" +import { format } from "date-fns" +import { tr } from "date-fns/locale" +import Link from "next/link" +import { Plus } from "lucide-react" +import { Button } from "@/components/ui/button" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" + +export default async function CategoriesPage() { + const supabase = createClient() + const { data: categories } = await supabase + .from('categories') + .select('*') + .order('created_at', { ascending: false }) + + return ( +
+
+
+

Kategoriler ({categories?.length || 0})

+

+ Sitenizdeki ürün kategorilerini yönetin. +

+
+ + + +
+ +
+ + + + Ad + Slug + Oluşturulma Tarihi + İşlemler + + + + {categories?.map((category) => ( + + {category.name} + {category.slug} + + {format(new Date(category.created_at), "d MMMM yyyy", { locale: tr })} + + + + + + + + ))} + {(!categories || categories.length === 0) && ( + + + Kategori bulunamadı. + + + )} + +
+
+
+ ) +} diff --git a/app/(dashboard)/dashboard/page.tsx b/app/(dashboard)/dashboard/page.tsx index 83e7937..e4deb2a 100644 --- a/app/(dashboard)/dashboard/page.tsx +++ b/app/(dashboard)/dashboard/page.tsx @@ -1,126 +1,126 @@ - +import { createClient } from "@/lib/supabase-server" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" -import { DollarSign, ShoppingCart, Users, CreditCard } from "lucide-react" +import { DollarSign, ShoppingCart, Users, CreditCard, Package } from "lucide-react" +import Link from "next/link" +import { Button } from "@/components/ui/button" + +export default async function DashboardPage() { + const supabase = createClient() + + // Fetch real data + const { data: products } = await supabase + .from("products") + .select("*") + .order("created_at", { ascending: false }) + + const totalProducts = products?.length || 0 + const totalValue = products?.reduce((acc, product) => acc + (Number(product.price) || 0), 0) || 0 + const recentProducts = products?.slice(0, 5) || [] + + // Calculate unique categories + const categories = new Set(products?.map(p => p.category)).size -export default function DashboardPage() { return ( -
+

Genel Bakış

+
+ + + +
- {/* Stats Grid */}
- Toplam Gelir + Toplam Ürün Değeri -
₺45,231.89
-

+20.1% geçen aya göre

+
₺{totalValue.toLocaleString('tr-TR', { minimumFractionDigits: 2 })}
+

Stoktaki toplam varlık

- Abonelikler + Toplam Ürün + + + +
{totalProducts}
+

Kayıtlı ürün sayısı

+
+
+ + + Kategoriler + + + +
{categories}
+

Aktif kategori

+
+
+ + + Son Güncelleme -
+2350
-

+180.1% geçen aya göre

-
-
- - - Satışlar - - - -
+12,234
-

+19% geçen aya göre

-
-
- - - Aktif Şimdi - - - -
+573
-

+201 son bir saatte

+
Şimdi
+

Canlı veri akışı

- {/* Recent Sales / Activity */} - Son Hareketler + Son Eklenen Ürünler - Bu ay 265+ satış yaptınız. + En son eklenen {recentProducts.length} ürün. - {/* Mock List */}
-
-
- OM + {recentProducts.map((product) => ( +
+
+ {product.name.substring(0, 2).toUpperCase()} +
+
+

{product.name}

+

{product.category}

+
+
₺{Number(product.price).toLocaleString('tr-TR')}
-
-

Ozan Mehmet

-

ozan@email.com

-
-
+₺1,999.00
-
-
-
- -
-
-

Ayşe Özdemir

-

ayse@email.com

-
-
+₺39.00
-
-
-
- MK -
-
-

Mehmet Kaya

-

mehmet@email.com

-
-
+₺299.00
-
+ ))} + {recentProducts.length === 0 && ( +
Henüz ürün yok.
+ )}
- {/* Recent Products or Other Info */} + {/* Placeholder for future features or quick actions */} - Son Eklenen Ürünler + Hızlı İşlemler - Stoğa yeni giren ürünler. + Yönetim paneli kısayolları. -
-
- Çelik Kasa EV-100 - Stokta -
-
- Ofis Tipi XYZ - Azaldı -
-
- Otel Kasası H-20 - Stokta -
+
+ + + +
@@ -128,3 +128,23 @@ export default function DashboardPage() {
) } + +function PlusIcon(props: any) { + return ( + + + + + ) +} diff --git a/app/(dashboard)/dashboard/products/[productId]/page.tsx b/app/(dashboard)/dashboard/products/[productId]/page.tsx new file mode 100644 index 0000000..c92c6bb --- /dev/null +++ b/app/(dashboard)/dashboard/products/[productId]/page.tsx @@ -0,0 +1,33 @@ +import { createClient } from "@/lib/supabase-server" +import { ProductForm } from "@/components/dashboard/product-form" +import { notFound } from "next/navigation" + +interface ProductEditPageProps { + params: { + productId: string + } +} + +export default async function ProductEditPage({ params }: ProductEditPageProps) { + const supabase = createClient() + const { data: product } = await supabase + .from("products") + .select("*") + .eq("id", params.productId) + .single() + + if (!product) { + notFound() + } + + return ( +
+
+

Ürün Düzenle

+
+
+ +
+
+ ) +} diff --git a/app/(dashboard)/dashboard/products/actions.ts b/app/(dashboard)/dashboard/products/actions.ts new file mode 100644 index 0000000..71acb65 --- /dev/null +++ b/app/(dashboard)/dashboard/products/actions.ts @@ -0,0 +1,51 @@ +"use server" + +import { createClient } from "@/lib/supabase-server" +import { revalidatePath } from "next/cache" + +export async function createProduct(data: any) { + const supabase = createClient() + + // Validate data manually or use Zod schema here again securely + // For simplicity, we assume data is coming from the strongly typed Client Form + // In production, ALWAYS validate server-side strictly. + + try { + const { error } = await supabase.from("products").insert({ + name: data.name, + category: data.category, + description: data.description, + price: data.price, + image_url: data.image_url, + }) + + if (error) throw error + + revalidatePath("/dashboard/products") + return { success: true } + } catch (error: any) { + return { success: false, error: error.message } + } +} + +export async function updateProduct(id: number, data: any) { + const supabase = createClient() + + try { + const { error } = await supabase.from("products").update({ + name: data.name, + category: data.category, + description: data.description, + price: data.price, + image_url: data.image_url, + }).eq("id", id) + + if (error) throw error + + revalidatePath("/dashboard/products") + revalidatePath(`/dashboard/products/${id}`) + return { success: true } + } catch (error: any) { + return { success: false, error: error.message } + } +} diff --git a/app/(dashboard)/dashboard/products/new/page.tsx b/app/(dashboard)/dashboard/products/new/page.tsx new file mode 100644 index 0000000..b2d4312 --- /dev/null +++ b/app/(dashboard)/dashboard/products/new/page.tsx @@ -0,0 +1,14 @@ +import { ProductForm } from "@/components/dashboard/product-form" + +export default function NewProductPage() { + return ( +
+
+

Yeni Ürün Ekle

+
+
+ +
+
+ ) +} diff --git a/app/(dashboard)/dashboard/products/page.tsx b/app/(dashboard)/dashboard/products/page.tsx new file mode 100644 index 0000000..c786446 --- /dev/null +++ b/app/(dashboard)/dashboard/products/page.tsx @@ -0,0 +1,75 @@ +import { createClient } from "@/lib/supabase-server" +import { Button } from "@/components/ui/button" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { Plus } from "lucide-react" +import Link from "next/link" +import { Badge } from "@/components/ui/badge" + +export default async function ProductsPage() { + const supabase = createClient() + const { data: products } = await supabase + .from("products") + .select("*") + .order("created_at", { ascending: false }) + + return ( +
+
+

Ürünler

+
+ + + +
+
+ +
+ + + + Ad + Kategori + Fiyat + İşlemler + + + + {products?.length === 0 ? ( + + + Henüz ürün eklenmemiş. + + + ) : ( + products?.map((product) => ( + + {product.name} + + + {product.category} + + + ₺{product.price} + + + + + + + )) + )} + +
+
+
+ ) +} diff --git a/app/(dashboard)/dashboard/profile/page.tsx b/app/(dashboard)/dashboard/profile/page.tsx new file mode 100644 index 0000000..57526dd --- /dev/null +++ b/app/(dashboard)/dashboard/profile/page.tsx @@ -0,0 +1,50 @@ +import { createClient } from "@/lib/supabase-server" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" + +export default async function ProfilePage() { + const supabase = createClient() + const { data: { user } } = await supabase.auth.getUser() + + return ( +
+
+

Profil

+
+ +
+ + + Genel Bilgiler + + Kişisel profil bilgileriniz. + + + +
+ + + PK + + +
+ +
+ + +

E-posta adresi değiştirilemez.

+
+ +
+ + +
+
+
+
+
+ ) +} diff --git a/app/(dashboard)/dashboard/settings/actions.ts b/app/(dashboard)/dashboard/settings/actions.ts new file mode 100644 index 0000000..f26c8da --- /dev/null +++ b/app/(dashboard)/dashboard/settings/actions.ts @@ -0,0 +1,42 @@ +"use server" + +import { createClient } from "@/lib/supabase-server" +import { revalidatePath } from "next/cache" + +export async function updateSiteSettings(data: { + site_title: string + site_description: string + contact_email: string + contact_phone: string + currency: string +}) { + const supabase = createClient() + + // Check admin is already handled by RLS on database level, but we can double check here + const { data: { user } } = await supabase.auth.getUser() + if (!user) return { error: "Oturum açmanız gerekiyor." } + + // We update the single row where id is likely 1 or just the first row + // Since we initialized it with one row, we can just update match on something true or fetch id first. + // Easier: Update all rows (there should only be one) or fetch the specific ID first. + + // Let's first get the ID just to be precise + const { data: existing } = await supabase.from('site_settings').select('id').single() + + if (!existing) { + return { error: "Ayarlar bulunamadı." } + } + + const { error } = await supabase + .from('site_settings') + .update(data) + .eq('id', existing.id) + + if (error) { + return { error: "Ayarlar güncellenemedi: " + error.message } + } + + revalidatePath("/dashboard/settings") + revalidatePath("/") // Revalidate home as it might use these settings + return { success: true } +} diff --git a/app/(dashboard)/dashboard/settings/page.tsx b/app/(dashboard)/dashboard/settings/page.tsx new file mode 100644 index 0000000..5cced11 --- /dev/null +++ b/app/(dashboard)/dashboard/settings/page.tsx @@ -0,0 +1,45 @@ +import { createClient } from "@/lib/supabase-server" +import { SiteSettingsForm } from "@/components/dashboard/site-settings-form" +import { AppearanceForm } from "@/components/dashboard/appearance-form" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Label } from "@/components/ui/label" +import { Switch } from "@/components/ui/switch" +import { Button } from "@/components/ui/button" + +export default async function SettingsPage() { + const supabase = createClient() + + // Fetch site settings + const { data: settings } = await supabase + .from('site_settings') + .select('*') + .single() + + return ( +
+

Ayarlar

+ + {/* Site General Settings */} +
+ +
+ +
+ + + + + Hesap Güvenliği + + Şifre ve oturum yönetimi. + + + + + + + +
+
+ ) +} diff --git a/app/(dashboard)/dashboard/users/[userId]/page.tsx b/app/(dashboard)/dashboard/users/[userId]/page.tsx new file mode 100644 index 0000000..545a572 --- /dev/null +++ b/app/(dashboard)/dashboard/users/[userId]/page.tsx @@ -0,0 +1,69 @@ +import { createClient } from "@/lib/supabase-server" +import { UserForm } from "@/components/dashboard/user-form" +import { notFound } from "next/navigation" + +export default async function EditUserPage({ params }: { params: { userId: string } }) { + const supabase = createClient() + + // Fetch profile + const { data: profile } = await supabase + .from('profiles') + .select('*') + .eq('id', params.userId) + .single() + + if (!profile) { + notFound() + } + + // We also need the email, which is in auth.users, but we can't select from there easily with RLS/Client if not admin API + // However, our logged in user IS admin, but RLS on auth.users is usually strict. + // Let's see if we can get it via RPC or if the profile should store email (bad practice duplication, but helpful). + // Actually, `supabaseAdmin` in a server action can get it, but here we are in a Page (Server Component). + // We can use `supabaseAdmin` here too if we create a utility for it or just import createClient from supabase-js with admin key. + + // WORKAROUND: For now, let's assume we might need a server function to fetch full user details including email + // OR we just update the profile part. But the user wants to update email probably. + // Let's write a small server action/function to fetch this data securely to render the form. + + // Better: Helper function to get user details + const userDetails = await getUserDetails(params.userId) + + return ( +
+
+

Kullanıcı Düzenle

+
+ +
+ ) +} + +// Helper to get admin-level data for the form +import { createClient as createSupabaseClient } from "@supabase/supabase-js" + +async function getUserDetails(userId: string) { + const supabaseAdmin = createSupabaseClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.SUPABASE_SERVICE_ROLE_KEY!, + { auth: { autoRefreshToken: false, persistSession: false } } + ) + + const { data: { user }, error } = await supabaseAdmin.auth.admin.getUserById(userId) + const { data: profile } = await supabaseAdmin.from('profiles').select('*').eq('id', userId).single() + + if (error || !user || !profile) return undefined + + // Split full name + const parts = (profile.full_name || "").split(' ') + const firstName = parts[0] || "" + const lastName = parts.slice(1).join(' ') || "" + + return { + id: userId, + firstName, + lastName, + email: user.email || "", + role: profile.role as "admin" | "user" + } +} diff --git a/app/(dashboard)/dashboard/users/actions.ts b/app/(dashboard)/dashboard/users/actions.ts new file mode 100644 index 0000000..1d49868 --- /dev/null +++ b/app/(dashboard)/dashboard/users/actions.ts @@ -0,0 +1,133 @@ +"use server" + +import { createClient } from "@/lib/supabase-server" +import { createClient as createSupabaseClient } from "@supabase/supabase-js" +import { revalidatePath } from "next/cache" +import { redirect } from "next/navigation" + +// WARNING: specialized client for admin actions only +// This requires SUPABASE_SERVICE_ROLE_KEY to be set in .env.local +const supabaseAdmin = createSupabaseClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.SUPABASE_SERVICE_ROLE_KEY!, + { + auth: { + autoRefreshToken: false, + persistSession: false + } + } +) + +export async function createUser(firstName: string, lastName: string, email: string, password: string, role: 'admin' | 'user') { + const supabase = createClient() + + // 1. Check if current user is admin + const { data: { user: currentUser } } = await supabase.auth.getUser() + if (!currentUser) return { error: "Oturum açmanız gerekiyor." } + + const { data: profile } = await supabase + .from('profiles') + .select('role') + .eq('id', currentUser.id) + .single() + + if (!profile || profile.role !== 'admin') { + return { error: "Yetkisiz işlem. Sadece yöneticiler kullanıcı oluşturabilir." } + } + + // 2. Create user using Admin client + const { data: newUser, error: createError } = await supabaseAdmin.auth.admin.createUser({ + email, + password, + email_confirm: true, // Auto confirm + user_metadata: { + full_name: `${firstName} ${lastName}`.trim() + } + }) + + if (createError) { + return { error: createError.message } + } + + if (!newUser.user) { + return { error: "Kullanıcı oluşturulamadı." } + } + + // 3. Create profile entry (if not handled by trigger, but we'll do it manually to be safe/explicit about role) + const { error: profileError } = await supabaseAdmin + .from('profiles') + .insert({ + id: newUser.user.id, + full_name: `${firstName} ${lastName}`.trim(), + role: role + }) + + if (profileError) { + // Optional: delete auth user if profile creation fails? + // For now just return error + return { error: "Kullanıcı oluşturuldu ancak profil kaydedilemedi: " + profileError.message } + } + + revalidatePath("/dashboard/users") + return { success: true } +} + +export async function deleteUser(userId: string) { + const supabase = createClient() + + // Check admin + const { data: { user: currentUser } } = await supabase.auth.getUser() + if (!currentUser) return { error: "Oturum açmanız gerekiyor." } + + const { data: profile } = await supabase.from('profiles').select('role').eq('id', currentUser.id).single() + if (profile?.role !== 'admin') return { error: "Yetkisiz işlem." } + + // Delete user + const { error } = await supabaseAdmin.auth.admin.deleteUser(userId) + + if (error) return { error: error.message } + + revalidatePath("/dashboard/users") + return { success: true } +} + +export async function updateUser(userId: string, data: { firstName: string, lastName: string, email: string, password?: string, role: 'admin' | 'user' }) { + const supabase = createClient() + + // Check admin + const { data: { user: currentUser } } = await supabase.auth.getUser() + if (!currentUser) return { error: "Oturum açmanız gerekiyor." } + + // Check if current user is admin + const { data: profile } = await supabase.from('profiles').select('role').eq('id', currentUser.id).single() + if (profile?.role !== 'admin') return { error: "Yetkisiz işlem." } + + // 1. Update Profile (Role and Name) + const { error: profileError } = await supabaseAdmin + .from('profiles') + .update({ + full_name: `${data.firstName} ${data.lastName}`.trim(), + role: data.role + }) + .eq('id', userId) + + if (profileError) return { error: "Profil güncellenemedi: " + profileError.message } + + // 2. Update Auth (Email and Password) + const authUpdates: any = { + email: data.email, + user_metadata: { + full_name: `${data.firstName} ${data.lastName}`.trim() + } + } + if (data.password && data.password.length >= 6) { + authUpdates.password = data.password + } + + const { error: authError } = await supabaseAdmin.auth.admin.updateUserById(userId, authUpdates) + + if (authError) return { error: "Kullanıcı giriş bilgileri güncellenemedi: " + authError.message } + + revalidatePath("/dashboard/users") + return { success: true } +} diff --git a/app/(dashboard)/dashboard/users/new/page.tsx b/app/(dashboard)/dashboard/users/new/page.tsx new file mode 100644 index 0000000..3650b55 --- /dev/null +++ b/app/(dashboard)/dashboard/users/new/page.tsx @@ -0,0 +1,13 @@ + +import { UserForm } from "@/components/dashboard/user-form" + +export default function NewUserPage() { + return ( +
+
+

Yeni Kullanıcı Ekle

+
+ +
+ ) +} diff --git a/app/(dashboard)/dashboard/users/page.tsx b/app/(dashboard)/dashboard/users/page.tsx new file mode 100644 index 0000000..0290841 --- /dev/null +++ b/app/(dashboard)/dashboard/users/page.tsx @@ -0,0 +1,87 @@ +import { createClient } from "@/lib/supabase-server" +import { Button } from "@/components/ui/button" +import Link from "next/link" +import { Plus } from "lucide-react" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { Badge } from "@/components/ui/badge" + +export default async function UsersPage() { + const supabase = createClient() + const { data: { user } } = await supabase.auth.getUser() + + // Protected Route Check (Simple) + const { data: currentUserProfile } = await supabase + .from('profiles') + .select('role') + .eq('id', user?.id) + .single() + + // Only verify if we have profiles, if not (first run), maybe allow? + // But for safety, blocking non-admins. + if (currentUserProfile?.role !== 'admin') { + return ( +
+

Yetkisiz Erişim

+

Bu sayfayı görüntüleme yetkiniz yok.

+
+ ) + } + + const { data: profiles } = await supabase + .from("profiles") + .select("*") + .order("created_at", { ascending: false }) + + return ( +
+
+

Kullanıcı Yönetimi

+
+ + + +
+
+ +
+ + + + Ad Soyad + Rol + Kayıt Tarihi + İşlemler + + + + {profiles?.map((profile) => ( + + {profile.full_name} + + + {profile.role === 'admin' ? 'Yönetici' : 'Kullanıcı'} + + + {new Date(profile.created_at).toLocaleDateString('tr-TR')} + + + + + + + ))} + +
+
+
+ ) +} diff --git a/app/(public)/login/page.tsx b/app/(public)/login/page.tsx index 0d037ea..bc053b9 100644 --- a/app/(public)/login/page.tsx +++ b/app/(public)/login/page.tsx @@ -3,7 +3,7 @@ import { useState } from "react" import { useRouter } from "next/navigation" import Link from "next/link" -import { supabase } from "@/lib/supabase" +import { createClient } from "@/lib/supabase-browser" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" @@ -11,6 +11,7 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } import { AlertCircle, Loader2 } from "lucide-react" export default function LoginPage() { + const supabase = createClient() const router = useRouter() const [email, setEmail] = useState("") const [password, setPassword] = useState("") @@ -33,7 +34,7 @@ export default function LoginPage() { return } - router.push("/") + router.push("/dashboard") router.refresh() } catch (err: any) { setError("Bir hata oluştu. Lütfen tekrar deneyin.") diff --git a/app/layout.tsx b/app/layout.tsx index bc34f6f..2d099e5 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -6,10 +6,20 @@ import "./globals.css"; const inter = Inter({ subsets: ["latin"] }); const outfit = Outfit({ subsets: ["latin"], variable: "--font-outfit" }); -export const metadata: Metadata = { - title: "ParaKasa - Premium Çelik Kasalar", - description: "Eviniz ve iş yeriniz için en yüksek güvenlikli çelik kasa ve para sayma çözümleri.", -}; +import { getSiteSettings } from "@/lib/site-settings"; + +export async function generateMetadata() { + const settings = await getSiteSettings(); + + return { + title: settings?.site_title || "ParaKasa - Premium Çelik Kasalar", + description: settings?.site_description || "Eviniz ve iş yeriniz için en yüksek güvenlikli çelik kasa ve para sayma çözümleri.", + }; +} + +import { ThemeProvider } from "@/components/theme-provider" + +// ... imports export default function RootLayout({ children, @@ -17,12 +27,19 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - + - {children} - + + {children} + + ); diff --git a/components/dashboard/appearance-form.tsx b/components/dashboard/appearance-form.tsx new file mode 100644 index 0000000..5020a9f --- /dev/null +++ b/components/dashboard/appearance-form.tsx @@ -0,0 +1,57 @@ +"use client" + +import { useTheme } from "next-themes" +import { Card, CardContent, CardTitle, CardHeader } from "@/components/ui/card" +import { Label } from "@/components/ui/label" +import { Switch } from "@/components/ui/switch" +import { useEffect, useState } from "react" + +export function AppearanceForm() { + const { theme, setTheme } = useTheme() + const [mounted, setMounted] = useState(false) + + // Avoid hydration mismatch + useEffect(() => { + setMounted(true) + }, []) + + if (!mounted) { + return ( + + + Görünüm + + +
+ + +
+
+
+ ) + } + + return ( + + + Görünüm + + +
+ + setTheme(checked ? 'dark' : 'light')} + /> +
+
+
+ ) +} diff --git a/components/dashboard/category-form.tsx b/components/dashboard/category-form.tsx new file mode 100644 index 0000000..4bc5b76 --- /dev/null +++ b/components/dashboard/category-form.tsx @@ -0,0 +1,186 @@ +"use client" + +import { useState } from "react" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import * as z from "zod" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { toast } from "sonner" +import { useRouter } from "next/navigation" +import { createCategory, updateCategory, deleteCategory } from "@/app/(dashboard)/dashboard/categories/actions" +import { Trash } from "lucide-react" +import { AlertModal } from "@/components/modals/alert-modal" + +const formSchema = z.object({ + name: z.string().min(2, "Kategori adı en az 2 karakter olmalıdır."), + description: z.string().optional(), + image_url: z.string().optional(), +}) + +type CategoryFormValues = z.infer + +interface CategoryFormProps { + initialData?: { + id: string + name: string + description?: string + image_url?: string + } | null +} + +export function CategoryForm({ initialData }: CategoryFormProps) { + const router = useRouter() + const [open, setOpen] = useState(false) + const [loading, setLoading] = useState(false) + + const title = initialData ? "Kategoriyi Düzenle" : "Yeni Kategori" + const description = initialData ? "Kategori detaylarını düzenleyin." : "Yeni bir kategori ekleyin." + const toastMessage = initialData ? "Kategori güncellendi." : "Kategori oluşturuldu." + const action = initialData ? "Kaydet" : "Oluştur" + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: initialData || { + name: "", + description: "", + image_url: "", + }, + }) + + const onSubmit = async (data: CategoryFormValues) => { + setLoading(true) + try { + if (initialData) { + const result = await updateCategory(initialData.id, data) + if ((result as any).error) { + toast.error((result as any).error) + } else { + toast.success(toastMessage) + router.push(`/dashboard/categories`) + router.refresh() + } + } else { + const result = await createCategory(data) + if ((result as any).error) { + toast.error((result as any).error) + } else { + toast.success(toastMessage) + router.push(`/dashboard/categories`) + router.refresh() + } + } + } catch (error) { + toast.error("Bir hata oluştu.") + } finally { + setLoading(false) + } + } + + const onDelete = async () => { + setLoading(true) + try { + const result = await deleteCategory(initialData!.id) + if ((result as any).error) { + toast.error((result as any).error) + } else { + toast.success("Kategori silindi.") + router.push(`/dashboard/categories`) + router.refresh() + } + } catch (error) { + toast.error("Silme işlemi başarısız.") + } finally { + setLoading(false) + setOpen(false) + } + } + + return ( + <> + setOpen(false)} + onConfirm={onDelete} + loading={loading} + /> +
+
+

{title}

+

{description}

+
+ {initialData && ( + + )} +
+
+
+ +
+ ( + + Başlık + + + + + + )} + /> + ( + + Görsel URL (Opsiyonel) + + + + + + )} + /> +
+ ( + + Açıklama + +