diff --git a/PLANLAMA.md b/PLANLAMA.md index 3c80e55..332adec 100644 --- a/PLANLAMA.md +++ b/PLANLAMA.md @@ -17,7 +17,7 @@ --- ## Mevcut Durum (Tamamlananlar) -- [x] Kullanıcı Yönetimi (Admin Ekle/Sil). +- [x] Kullanıcı Yönetimi (Admin Ekle/Sil/Düzenle + Telefon). - [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/profile/page.tsx b/app/(dashboard)/dashboard/profile/page.tsx index 57526dd..1d4b60a 100644 --- a/app/(dashboard)/dashboard/profile/page.tsx +++ b/app/(dashboard)/dashboard/profile/page.tsx @@ -1,14 +1,40 @@ 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" +import { UserForm } from "@/components/dashboard/user-form" +import { notFound } from "next/navigation" +import { getProfile } from "@/lib/data" export default async function ProfilePage() { const supabase = createClient() const { data: { user } } = await supabase.auth.getUser() + if (!user) { + // Should be protected by middleware but just in case + return
Lütfen giriş yapın.
+ } + + // Fetch profile data + const profile = await getProfile(user.id) + + if (!profile) { + // Fallback for user without profile row? + // Or create one on the fly? + return
Profil verisi bulunamadı.
+ } + + const parts = (profile.full_name || "").split(' ') + const firstName = parts[0] || "" + const lastName = parts.slice(1).join(' ') || "" + + const initialData = { + firstName, + lastName, + phone: profile.phone || "", + email: user.email || "", + role: profile.role || "user" + } + return (
@@ -20,28 +46,18 @@ export default async function ProfilePage() { Genel Bilgiler - Kişisel profil bilgileriniz. + Kişisel profil bilgilerinizi buradan güncelleyebilirsiniz. -
+
PK -
-
- - -

E-posta adresi değiştirilemez.

-
- -
- - -
+
diff --git a/app/(dashboard)/dashboard/profile/password/page.tsx b/app/(dashboard)/dashboard/profile/password/page.tsx new file mode 100644 index 0000000..bf4a08f --- /dev/null +++ b/app/(dashboard)/dashboard/profile/password/page.tsx @@ -0,0 +1,15 @@ + +import { PasswordForm } from "../../../../../components/dashboard/password-form" + +export default function ChangePasswordPage() { + return ( +
+
+

Şifre Değiştir

+
+
+ +
+
+ ) +} diff --git a/app/(dashboard)/dashboard/users/[userId]/page.tsx b/app/(dashboard)/dashboard/users/[userId]/page.tsx index 545a572..251e056 100644 --- a/app/(dashboard)/dashboard/users/[userId]/page.tsx +++ b/app/(dashboard)/dashboard/users/[userId]/page.tsx @@ -64,6 +64,7 @@ async function getUserDetails(userId: string) { firstName, lastName, email: user.email || "", - role: profile.role as "admin" | "user" + role: profile.role as "admin" | "user", + phone: profile.phone || "" } } diff --git a/app/(dashboard)/dashboard/users/actions.ts b/app/(dashboard)/dashboard/users/actions.ts index 1d49868..3655aef 100644 --- a/app/(dashboard)/dashboard/users/actions.ts +++ b/app/(dashboard)/dashboard/users/actions.ts @@ -18,7 +18,7 @@ const supabaseAdmin = createSupabaseClient( } ) -export async function createUser(firstName: string, lastName: string, email: string, password: string, role: 'admin' | 'user') { +export async function createUser(firstName: string, lastName: string, email: string, password: string, role: 'admin' | 'user', phone?: string) { const supabase = createClient() // 1. Check if current user is admin @@ -59,7 +59,8 @@ export async function createUser(firstName: string, lastName: string, email: str .insert({ id: newUser.user.id, full_name: `${firstName} ${lastName}`.trim(), - role: role + role: role, + phone: phone }) if (profileError) { @@ -91,7 +92,7 @@ export async function deleteUser(userId: string) { return { success: true } } -export async function updateUser(userId: string, data: { firstName: string, lastName: string, email: string, password?: string, role: 'admin' | 'user' }) { +export async function updateUser(userId: string, data: { firstName: string, lastName: string, email: string, password?: string, role: 'admin' | 'user', phone?: string }) { const supabase = createClient() // Check admin @@ -107,7 +108,8 @@ export async function updateUser(userId: string, data: { firstName: string, last .from('profiles') .update({ full_name: `${data.firstName} ${data.lastName}`.trim(), - role: data.role + role: data.role, + phone: data.phone }) .eq('id', userId) @@ -131,3 +133,32 @@ export async function updateUser(userId: string, data: { firstName: string, last revalidatePath("/dashboard/users") return { success: true } } + +export async function updateProfile(data: { firstName: string, lastName: string, phone?: string }) { + const supabase = createClient() + const { data: { user } } = await supabase.auth.getUser() + + if (!user) return { error: "Oturum açmanız gerekiyor." } + + const { error } = await supabase + .from('profiles') + .update({ + full_name: `${data.firstName} ${data.lastName}`.trim(), + phone: data.phone + }) + .eq('id', user.id) + + if (error) return { error: "Profil güncellenemedi: " + error.message } + + // Update Auth Metadata as well + if (data.firstName || data.lastName) { + await supabase.auth.updateUser({ + data: { + full_name: `${data.firstName} ${data.lastName}`.trim() + } + }) + } + + revalidatePath("/dashboard/profile") + return { success: true } +} diff --git a/app/(dashboard)/layout.tsx b/app/(dashboard)/layout.tsx index 080a16e..53d555d 100644 --- a/app/(dashboard)/layout.tsx +++ b/app/(dashboard)/layout.tsx @@ -1,3 +1,4 @@ +import { getProfile } from "@/lib/data" import { createClient } from "@/lib/supabase-server" import { redirect } from "next/navigation" import { Sidebar } from "@/components/dashboard/sidebar" @@ -15,6 +16,8 @@ export default async function DashboardLayout({ redirect("/login") } + const profile = await getProfile(user.id) + return (
- +
{children}
diff --git a/app/globals.css b/app/globals.css index 7e11b97..a494c3f 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,3 +1,4 @@ +@import 'react-phone-number-input/style.css'; @tailwind base; @tailwind components; @tailwind utilities; diff --git a/components/dashboard/header.tsx b/components/dashboard/header.tsx index b38ed43..7ddc169 100644 --- a/components/dashboard/header.tsx +++ b/components/dashboard/header.tsx @@ -6,7 +6,12 @@ import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet" import { Sidebar } from "@/components/dashboard/sidebar" import { UserNav } from "@/components/dashboard/user-nav" -export function DashboardHeader() { +interface DashboardHeaderProps { + user: { email?: string | null } | null + profile: { full_name?: string | null, role?: string | null } | null +} + +export function DashboardHeader({ user, profile }: DashboardHeaderProps) { return (
@@ -28,7 +33,7 @@ export function DashboardHeader() {
{/* Breadcrumb or Search could go here */}
- +
) } diff --git a/components/dashboard/password-form.tsx b/components/dashboard/password-form.tsx new file mode 100644 index 0000000..09048d4 --- /dev/null +++ b/components/dashboard/password-form.tsx @@ -0,0 +1,113 @@ +"use client" + +import { useState } from "react" +import { useRouter } from "next/navigation" +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 { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { toast } from "sonner" +import { Loader2 } from "lucide-react" +import { supabase } from "@/lib/supabase" + +const passwordSchema = z.object({ + password: z.string().min(6, "Şifre en az 6 karakter olmalıdır."), + confirmPassword: z.string().min(6, "Şifre tekrarı en az 6 karakter olmalıdır."), +}).refine((data) => data.password === data.confirmPassword, { + message: "Şifreler eşleşmiyor.", + path: ["confirmPassword"], +}) + +type PasswordFormValues = z.infer + +export function PasswordForm() { + const router = useRouter() + const [loading, setLoading] = useState(false) + + const form = useForm({ + resolver: zodResolver(passwordSchema), + defaultValues: { + password: "", + confirmPassword: "", + }, + }) + + const onSubmit = async (data: PasswordFormValues) => { + setLoading(true) + try { + const { error } = await supabase.auth.updateUser({ + password: data.password + }) + + if (error) { + toast.error("Şifre güncellenemedi: " + error.message) + return + } + + toast.success("Şifreniz başarıyla güncellendi.") + form.reset() + router.refresh() + } catch (error) { + toast.error("Bir sorun oluştu.") + } finally { + setLoading(false) + } + } + + return ( + + + Yeni Şifre Belirle + + Hesabınız için yeni bir şifre belirleyin. + + + +
+ + ( + + Yeni Şifre + + + + + + )} + /> + ( + + Şifre Tekrar + + + + + + )} + /> + + + +
+
+ ) +} diff --git a/components/dashboard/user-form.tsx b/components/dashboard/user-form.tsx index 30f36a1..2d6ca02 100644 --- a/components/dashboard/user-form.tsx +++ b/components/dashboard/user-form.tsx @@ -7,6 +7,7 @@ import { zodResolver } from "@hookform/resolvers/zod" import * as z from "zod" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" +import { PhoneInput } from "@/components/ui/phone-input" import { Form, FormControl, @@ -25,34 +26,45 @@ import { import { Card, CardContent } from "@/components/ui/card" import { toast } from "sonner" import { Loader2 } from "lucide-react" -import { createUser, updateUser } from "@/app/(dashboard)/dashboard/users/actions" +import { createUser, updateUser, updateProfile } from "@/app/(dashboard)/dashboard/users/actions" const userSchema = z.object({ firstName: z.string().min(2, "Ad en az 2 karakter olmalıdır."), lastName: z.string().min(2, "Soyad en az 2 karakter olmalıdır."), email: z.string().email("Geçerli bir e-posta adresi giriniz."), password: z.string().optional(), // Password is optional on edit + confirmPassword: z.string().optional(), role: z.enum(["admin", "user"]), + phone: z.string().optional(), }).refine((data) => { - // If we are creating a NEW user (no ID passed in props effectively, but schema doesn't know props), - // we generally want password required. But here we'll handle it in the component logic or strictly separate schemas. - // For simplicity, we make password optional in Zod but check it in onSubmit if it's a create action. + // 1. Password match check + if (data.password && data.password !== data.confirmPassword) { + return false; + } + // 2. New user password requirement check (simplified here, but strict would check id) + // We handle the "required" part in onSubmit manually for now as per previous logic, + // but the matching check is now here. return true +}, { + message: "Şifreler eşleşmiyor.", + path: ["confirmPassword"], }) type UserFormValues = z.infer interface UserFormProps { initialData?: { - id: string + id?: string firstName: string lastName: string email: string role: "admin" | "user" + phone?: string } + mode?: "admin" | "profile" } -export function UserForm({ initialData }: UserFormProps) { +export function UserForm({ initialData, mode = "admin" }: UserFormProps) { const router = useRouter() const [loading, setLoading] = useState(false) @@ -63,13 +75,17 @@ export function UserForm({ initialData }: UserFormProps) { lastName: initialData.lastName, email: initialData.email, password: "", // Empty password means no change + confirmPassword: "", role: initialData.role, + phone: initialData.phone, } : { firstName: "", lastName: "", email: "", password: "", + confirmPassword: "", role: "user", + phone: "", }, }) @@ -77,17 +93,32 @@ export function UserForm({ initialData }: UserFormProps) { setLoading(true) try { let result; - if (initialData) { - // Update - result = await updateUser(initialData.id, data) + + if (mode === "profile") { + // Profile update mode (self-service) + result = await updateProfile({ + firstName: data.firstName, + lastName: data.lastName, + phone: data.phone + }) + } else if (initialData?.id) { + // Admin update mode + result = await updateUser(initialData.id, { + firstName: data.firstName, + lastName: data.lastName, + email: data.email, + password: data.password, + role: data.role, + phone: data.phone + }) } else { - // Create + // Admin create mode if (!data.password || data.password.length < 6) { toast.error("Yeni kullanıcı için şifre gereklidir (min 6 karakter).") setLoading(false) return } - result = await createUser(data.firstName, data.lastName, data.email, data.password, data.role) + result = await createUser(data.firstName, data.lastName, data.email, data.password, data.role, data.phone) } if (result.error) { @@ -95,9 +126,16 @@ export function UserForm({ initialData }: UserFormProps) { return } - toast.success(initialData ? "Kullanıcı güncellendi." : "Kullanıcı oluşturuldu.") - router.push("/dashboard/users") + toast.success( + mode === "profile" + ? "Profil bilgileriniz güncellendi." + : initialData ? "Kullanıcı güncellendi." : "Kullanıcı oluşturuldu." + ) + router.refresh() + if (mode === "admin") { + router.push("/dashboard/users") + } } catch (error) { toast.error("Bir sorun oluştu.") } finally { @@ -139,6 +177,20 @@ export function UserForm({ initialData }: UserFormProps) { />
+ ( + + Telefon + + + + + + )} + /> + E-posta - + + {mode === "profile" &&

E-posta adresi değiştirilemez.

} )} /> - ( - - Şifre - - - - - - )} - /> + {mode === "admin" && ( + <> + ( + + Şifre + + + + + + )} + /> - ( - - Rol - - - - )} - /> + ( + + Şifre Tekrar + + + + + + )} + /> + + ( + + Rol + + + + )} + /> + + )} + + {mode === "profile" && ( +
+ Rol + +
+ )} diff --git a/components/dashboard/user-nav.tsx b/components/dashboard/user-nav.tsx index 55b738c..fad8f90 100644 --- a/components/dashboard/user-nav.tsx +++ b/components/dashboard/user-nav.tsx @@ -14,13 +14,30 @@ import { DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, - DropdownMenuShortcut, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { supabase } from "@/lib/supabase" import { useRouter } from "next/navigation" +import { useEffect, useState } from "react" +import { User } from "@supabase/supabase-js" -export function UserNav() { +interface UserProfile { + full_name: string | null + email: string | null + role: string | null +} + +interface UserNavProps { + user: { + email?: string | null + } | null + profile: { + full_name?: string | null + role?: string | null + } | null +} + +export function UserNav({ user, profile }: UserNavProps) { const router = useRouter() const handleSignOut = async () => { @@ -29,22 +46,31 @@ export function UserNav() { router.refresh() } + const getInitials = (name: string) => { + return name + .split(' ') + .map((n) => n[0]) + .join('') + .toUpperCase() + .substring(0, 2) + } + return (
-

Admin

+

{profile?.full_name || 'Kullanıcı'}

- admin@parakasa.com + {user?.email}

@@ -52,23 +78,18 @@ export function UserNav() { - Profil + Profil Bilgileri - + - Kullanıcılar - - - - - Ayarlar + Şifre Değiştir - - Çıkış Yap + + Çıkış
diff --git a/components/ui/phone-input.tsx b/components/ui/phone-input.tsx new file mode 100644 index 0000000..cb3f7f7 --- /dev/null +++ b/components/ui/phone-input.tsx @@ -0,0 +1,23 @@ +import * as React from "react" +import PhoneInput from "react-phone-number-input" +import { cn } from "@/lib/utils" + +export interface PhoneInputProps extends React.ComponentProps { + className?: string +} + +const PhoneInputComponent = React.forwardRef(({ className, ...props }, ref) => { + return ( + + ) +}) +PhoneInputComponent.displayName = "PhoneInput" + +export { PhoneInputComponent as PhoneInput } diff --git a/lib/data.ts b/lib/data.ts new file mode 100644 index 0000000..f2ce046 --- /dev/null +++ b/lib/data.ts @@ -0,0 +1,13 @@ +import { cache } from 'react' +import { createClient } from '@/lib/supabase-server' + +export const getProfile = cache(async (userId: string) => { + const supabase = createClient() + const { data } = await supabase + .from('profiles') + .select('*') + .eq('id', userId) + .single() + + return data +}) diff --git a/package-lock.json b/package-lock.json index b1a5904..e084744 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "react": "^18", "react-dom": "^18", "react-hook-form": "^7.70.0", + "react-phone-number-input": "^3.4.14", "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "zod": "^4.3.5" @@ -2922,6 +2923,12 @@ "url": "https://polar.sh/cva" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -2987,6 +2994,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/country-flag-icons": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/country-flag-icons/-/country-flag-icons-1.6.4.tgz", + "integrity": "sha512-Z3Zi419FI889tlElMsVhCIS5eRkiLDWixr576J5DPiTe5RGxpbRi+enMpHdYVp5iK5WFjr8P/RgyIFAGhFsiFg==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4504,6 +4517,27 @@ "dev": true, "license": "ISC" }, + "node_modules/input-format": { + "version": "0.3.14", + "resolved": "https://registry.npmjs.org/input-format/-/input-format-0.3.14.tgz", + "integrity": "sha512-gHMrgrbCgmT4uK5Um5eVDUohuV9lcs95ZUUN9Px2Y0VIfjTzT2wF8Q3Z4fwLFm7c5Z2OXCm53FHoovj6SlOKdg==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">=18.1.0", + "react-dom": ">=18.1.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -5128,6 +5162,12 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.12.33", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.33.tgz", + "integrity": "sha512-r9kw4OA6oDO4dPXkOrXTkArQAafIKAU71hChInV4FxZ69dxCfbwQGDPzqR5/vea94wU705/3AZroEbSoeVWrQw==", + "license": "MIT" + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -5436,7 +5476,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5936,7 +5975,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -6020,9 +6058,25 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, "license": "MIT" }, + "node_modules/react-phone-number-input": { + "version": "3.4.14", + "resolved": "https://registry.npmjs.org/react-phone-number-input/-/react-phone-number-input-3.4.14.tgz", + "integrity": "sha512-T9MziNuvthzv6+JAhKD71ab/jVXW5U20nQZRBJd6+q+ujmkC+/ISOf2GYo8pIi4VGjdIYRIHDftMAYn3WKZT3w==", + "license": "MIT", + "dependencies": { + "classnames": "^2.5.1", + "country-flag-icons": "^1.5.17", + "input-format": "^0.3.14", + "libphonenumber-js": "^1.12.27", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-remove-scroll": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", diff --git a/package.json b/package.json index 50c1bdd..ea6cd5c 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "react": "^18", "react-dom": "^18", "react-hook-form": "^7.70.0", + "react-phone-number-input": "^3.4.14", "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "zod": "^4.3.5" diff --git a/public/avatars/01.png b/public/avatars/01.png new file mode 100644 index 0000000..6443ce4 Binary files /dev/null and b/public/avatars/01.png differ diff --git a/supabase_migration_add_phone.sql b/supabase_migration_add_phone.sql new file mode 100644 index 0000000..6b171bc --- /dev/null +++ b/supabase_migration_add_phone.sql @@ -0,0 +1,3 @@ + +-- Add phone column to profiles table +ALTER TABLE profiles ADD COLUMN IF NOT EXISTS phone TEXT;