From ee9896315bc976b5e93b028f64cdd4b4e90f3b7f Mon Sep 17 00:00:00 2001 From: Kenan KARAER Date: Tue, 17 Mar 2026 00:46:49 +0300 Subject: [PATCH] New Proje --- package-lock.json | 10 + package.json | 1 + src/app/auth/callback/route.ts | 31 +++ src/app/auth/signout/route.ts | 14 ++ src/app/companies/actions.ts | 38 +++ src/app/companies/page.tsx | 115 +++++++++ src/app/employees/actions.ts | 73 ++++++ src/app/employees/page.tsx | 189 +++++++++++++++ src/app/layout.tsx | 41 +++- src/app/leave-requests/actions.ts | 65 ++++++ src/app/leave-requests/page.tsx | 244 ++++++++++++++++++++ src/app/login/actions.ts | 25 ++ src/app/login/page.tsx | 83 +++++++ src/app/page.tsx | 149 +++++++----- src/components/layout/Header.tsx | 25 ++ src/components/layout/Sidebar.tsx | 165 +++++++++++++ middleware.ts => src/middleware.ts | 0 {utils => src/utils}/supabase/client.ts | 0 {utils => src/utils}/supabase/middleware.ts | 0 {utils => src/utils}/supabase/server.ts | 0 supabase/migrations/init_schema.sql | 140 +++++++++++ 21 files changed, 1347 insertions(+), 61 deletions(-) create mode 100644 src/app/auth/callback/route.ts create mode 100644 src/app/auth/signout/route.ts create mode 100644 src/app/companies/actions.ts create mode 100644 src/app/companies/page.tsx create mode 100644 src/app/employees/actions.ts create mode 100644 src/app/employees/page.tsx create mode 100644 src/app/leave-requests/actions.ts create mode 100644 src/app/leave-requests/page.tsx create mode 100644 src/app/login/actions.ts create mode 100644 src/app/login/page.tsx create mode 100644 src/components/layout/Header.tsx create mode 100644 src/components/layout/Sidebar.tsx rename middleware.ts => src/middleware.ts (100%) rename {utils => src/utils}/supabase/client.ts (100%) rename {utils => src/utils}/supabase/middleware.ts (100%) rename {utils => src/utils}/supabase/server.ts (100%) create mode 100644 supabase/migrations/init_schema.sql diff --git a/package-lock.json b/package-lock.json index 59c8df3..4733582 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "personel", "version": "0.1.0", "dependencies": { + "@heroicons/react": "^2.2.0", "@supabase/ssr": "^0.9.0", "@supabase/supabase-js": "^2.99.2", "next": "16.1.6", @@ -455,6 +456,15 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@heroicons/react": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", + "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==", + "license": "MIT", + "peerDependencies": { + "react": ">= 16 || ^19.0.0-rc" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", diff --git a/package.json b/package.json index b99c057..8c0148c 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint": "eslint" }, "dependencies": { + "@heroicons/react": "^2.2.0", "@supabase/ssr": "^0.9.0", "@supabase/supabase-js": "^2.99.2", "next": "16.1.6", diff --git a/src/app/auth/callback/route.ts b/src/app/auth/callback/route.ts new file mode 100644 index 0000000..0568e56 --- /dev/null +++ b/src/app/auth/callback/route.ts @@ -0,0 +1,31 @@ +import { NextResponse } from 'next/server' +import { createClient } from '@/utils/supabase/server' + +export async function GET(request: Request) { + const { searchParams, origin } = new URL(request.url) + const code = searchParams.get('code') + // if "next" is in param, use it as the redirect URL + const next = searchParams.get('next') ?? '/' + + if (code) { + const supabase = await createClient() + const { error } = await supabase.auth.exchangeCodeForSession(code) + + if (!error) { + const forwardedHost = request.headers.get('x-forwarded-host') // original origin before load balancer + const isLocalEnv = process.env.NODE_ENV === 'development' + + if (isLocalEnv) { + // we can be sure that there is no load balancer in between, so no need to watch for X-Forwarded-Host + return NextResponse.redirect(`${origin}${next}`) + } else if (forwardedHost) { + return NextResponse.redirect(`https://${forwardedHost}${next}`) + } else { + return NextResponse.redirect(`${origin}${next}`) + } + } + } + + // return the user to an error page with instructions + return NextResponse.redirect(`${origin}/auth/auth-code-error`) +} diff --git a/src/app/auth/signout/route.ts b/src/app/auth/signout/route.ts new file mode 100644 index 0000000..dbfb54a --- /dev/null +++ b/src/app/auth/signout/route.ts @@ -0,0 +1,14 @@ +import { createClient } from '@/utils/supabase/server' +import { redirect } from 'next/navigation' + +export async function POST(request: Request) { + const supabase = await createClient() + + const { error } = await supabase.auth.signOut() + + if (error) { + redirect('/error') + } + + redirect('/login') +} diff --git a/src/app/companies/actions.ts b/src/app/companies/actions.ts new file mode 100644 index 0000000..55f008a --- /dev/null +++ b/src/app/companies/actions.ts @@ -0,0 +1,38 @@ +'use server' + +import { createClient } from '@/utils/supabase/server' +import { revalidatePath } from 'next/cache' + +export async function addCompany(formData: FormData) { + const supabase = await createClient() + + const name = formData.get('name') as string + if (!name) return { error: 'Şirket adı zorunludur.' } + + const { error } = await supabase + .from('companies') + .insert([{ name }]) + + if (error) { + return { error: 'Şirket eklenirken bir hata oluştu: ' + error.message } + } + + revalidatePath('/companies') + return { success: true } +} + +export async function deleteCompany(id: string) { + const supabase = await createClient() + + const { error } = await supabase + .from('companies') + .delete() + .eq('id', id) + + if (error) { + return { error: 'Şirket silinirken bir hata oluştu: ' + error.message } + } + + revalidatePath('/companies') + return { success: true } +} diff --git a/src/app/companies/page.tsx b/src/app/companies/page.tsx new file mode 100644 index 0000000..d0a5d92 --- /dev/null +++ b/src/app/companies/page.tsx @@ -0,0 +1,115 @@ +import { createClient } from '@/utils/supabase/server' +import { addCompany, deleteCompany } from './actions' +import { BuildingOfficeIcon, TrashIcon } from '@heroicons/react/24/outline' + +export default async function CompaniesPage() { + const supabase = await createClient() + + // Fetch companies + const { data: companies } = await supabase + .from('companies') + .select('*') + .order('created_at', { ascending: false }) + + return ( +
+
+
+

+ + Şirket Yönetimi +

+

+ Sisteme kayıtlı şirketlerin listesi ve yeni şirket ekleme alanı. +

+
+
+ +
+ + {/* Add Company Form */} +
+
+

+ Yeni Şirket Ekle +

+
+

Çalışanları atayabilmek için öncelikle şirket kaydı açmalısınız.

+
+ +
+
+ + +
+ +
+
+
+ + {/* Company List */} +
+
+ + + + + + + + + + {companies?.map((company) => ( + + + + + + ))} + {(!companies || companies.length === 0) && ( + + + + )} + +
+ Şirket Adı + + Kayıt Tarihi + + İşlemler +
+ {company.name} + + {new Date(company.created_at).toLocaleDateString('tr-TR')} + +
{ + 'use server'; + await deleteCompany(company.id) + }}> + +
+
+ Henüz kayıtlı şirket bulunmamaktadır. +
+
+
+ +
+
+ ) +} diff --git a/src/app/employees/actions.ts b/src/app/employees/actions.ts new file mode 100644 index 0000000..90577d6 --- /dev/null +++ b/src/app/employees/actions.ts @@ -0,0 +1,73 @@ +'use server' + +import { createClient } from '@/utils/supabase/server' +import { revalidatePath } from 'next/cache' + +export async function addEmployee(formData: FormData) { + const supabase = await createClient() + + // 1. Get Form Data + const firstName = formData.get('first_name') as string + const lastName = formData.get('last_name') as string + const email = formData.get('email') as string + const companyId = formData.get('company_id') as string + + if (!email || !companyId) { + return { error: 'E-posta ve Şirket seçimi zorunludur.' } + } + + // Admin creating users manually currently requires an admin API setup or the user registering themselves + // In a robust HRMS, the Superadmin uses the Supabase Admin API to `createUser`. + // For the sake of this prototype, we'll assume the user must register first via valid credentials, + // or we can insert an unauthenticated ghost user into `public.users` (which breaks reference to auth.users if not careful). + // + // Let's implement the standard approach: we check if the user exists in `auth.users` via `public.users` + + let { data: existingUser } = await supabase + .from('users') + .select('id') + .eq('email', email) + .single() + + if (!existingUser) { + return { error: 'Bu e-posta adresine sahip bir kullanıcı bulunamadı. Kullanıcının önce sisteme kayıt olması gerekmektedir.' } + } + + // 2. Insert into Employees table + const { error: employeeError } = await supabase + .from('employees') + .insert([{ + user_id: existingUser.id, + company_id: companyId, + department: formData.get('department'), + title: formData.get('title'), + status: 'active' + }]) + + if (employeeError) { + // Catch unique constraint violation + if (employeeError.code === '23505') { + return { error: 'Bu kullanıcı zaten bu şirkete eklenmiş.' } + } + return { error: 'Çalışan eklenirken hata: ' + employeeError.message } + } + + revalidatePath('/employees') + return { success: true } +} + +export async function deleteEmployee(id: string) { + const supabase = await createClient() + + const { error } = await supabase + .from('employees') + .delete() + .eq('id', id) + + if (error) { + return { error: 'Çalışan silinirken hata oluştu: ' + error.message } + } + + revalidatePath('/employees') + return { success: true } +} diff --git a/src/app/employees/page.tsx b/src/app/employees/page.tsx new file mode 100644 index 0000000..4a8ba7d --- /dev/null +++ b/src/app/employees/page.tsx @@ -0,0 +1,189 @@ +import { createClient } from '@/utils/supabase/server' +import { addEmployee, deleteEmployee } from './actions' +import { UsersIcon, TrashIcon } from '@heroicons/react/24/outline' + +export default async function EmployeesPage() { + const supabase = await createClient() + + // Fetch employees with their joined user, company, and role data + const { data: employees } = await supabase + .from('employees') + .select(` + id, + department, + title, + status, + created_at, + companies ( name ), + users ( first_name, last_name, email ), + roles ( name, description ) + `) + .order('created_at', { ascending: false }) + + // Fetch companies for the dropdown + const { data: companies } = await supabase + .from('companies') + .select('id, name') + .order('name') + + return ( +
+
+
+

+ + Çalışan Yönetimi +

+

+ Sisteme kayıtlı çalışanları görüntüleyin ve yeni çalışanları şirketlere atayın. +

+
+
+ +
+ + {/* Add Employee Form */} +
+
+

+ Yeni Çalışan Ata +

+
+

Sisteme daha önce kayıt olmuş (şifre oluşturmuş) kullanıcıları şirketlere atayın.

+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+
+ + {/* Employee List */} +
+
+ + + + + + + + + + + + {employees?.map((emp) => ( + + + + + + + + ))} + {(!employees || employees.length === 0) && ( + + + + )} + +
+ Kişi + + Şirket + + Departman / Ünvan + + Durum + + İşlemler +
+
+ {emp.users?.first_name} {emp.users?.last_name} +
+
{emp.users?.email}
+
+
{emp.companies?.name}
+
+
{emp.department || '-'}
+
{emp.title || '-'}
+
+ + {emp.status === 'active' ? 'Aktif' : 'Pasif'} + + +
{ + 'use server'; + await deleteEmployee(emp.id) + }}> + +
+
+ Sisteme kayıtlı çalışan bulunmamaktadır. +
+
+
+ +
+
+ ) +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f7fa87e..939bbf6 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,8 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; +import { createClient } from '@/utils/supabase/server' +import { Sidebar } from "@/components/layout/Sidebar"; +import { Header } from "@/components/layout/Header"; import "./globals.css"; const geistSans = Geist({ @@ -13,21 +16,47 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "HRMS Yönetim Sistemi", + description: "Şirket içi insan kaynakları yönetim paneli", }; -export default function RootLayout({ +export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { + const supabase = await createClient() + const { data: { user } } = await supabase.auth.getUser() + + // Fetch db user profile if logged in + let dbUser = null + if (user) { + const { data } = await supabase + .from('users') + .select('first_name, last_name, email') + .eq('id', user.id) + .single() + dbUser = data + } + return ( - + - {children} + {user ? ( +
+ +
+
+
+ {children} +
+
+
+ ) : ( + children + )} ); diff --git a/src/app/leave-requests/actions.ts b/src/app/leave-requests/actions.ts new file mode 100644 index 0000000..eaf5cfb --- /dev/null +++ b/src/app/leave-requests/actions.ts @@ -0,0 +1,65 @@ +'use server' + +import { createClient } from '@/utils/supabase/server' +import { revalidatePath } from 'next/cache' + +export async function submitLeaveRequest(formData: FormData) { + const supabase = await createClient() + + const { data: { user } } = await supabase.auth.getUser() + if (!user) return { error: 'Oturum bulunamadı.' } + + const startDate = formData.get('start_date') as string + const endDate = formData.get('end_date') as string + const reason = formData.get('reason') as string + const companyId = formData.get('company_id') as string // which company they are requesting leave for + + if (!startDate || !endDate || !companyId || !reason) { + return { error: 'Tüm alanları doldurunuz.' } + } + + // Find their employee record for this specific company + const { data: employeeData, error: empError } = await supabase + .from('employees') + .select('id') + .eq('user_id', user.id) + .eq('company_id', companyId) + .single() + + if (empError || !employeeData) { + return { error: 'Seçili şirket için çalışan kaydınız bulunamadı.' } + } + + const { error } = await supabase + .from('leave_requests') + .insert([{ + employee_id: employeeData.id, + start_date: startDate, + end_date: endDate, + reason: reason, + status: 'pending' // default + }]) + + if (error) { + return { error: 'İzin talebi oluşturulurken hata: ' + error.message } + } + + revalidatePath('/leave-requests') + return { success: true } +} + +export async function updateLeaveStatus(id: string, newStatus: 'approved' | 'rejected') { + const supabase = await createClient() + + const { error } = await supabase + .from('leave_requests') + .update({ status: newStatus }) + .eq('id', id) + + if (error) { + return { error: 'Durum güncellenirken hata oluştu: ' + error.message } + } + + revalidatePath('/leave-requests') + return { success: true } +} diff --git a/src/app/leave-requests/page.tsx b/src/app/leave-requests/page.tsx new file mode 100644 index 0000000..6231d83 --- /dev/null +++ b/src/app/leave-requests/page.tsx @@ -0,0 +1,244 @@ +import { createClient } from '@/utils/supabase/server' +import { updateLeaveStatus } from './actions' +import { CalendarDaysIcon, CheckIcon, XMarkIcon } from '@heroicons/react/24/outline' + +export default async function LeaveRequestsPage() { + const supabase = await createClient() + + const { data: { user } } = await supabase.auth.getUser() + if (!user) return null + + // Fetch user role + const { data: dbUser } = await supabase + .from('users') + .select('roles(name)') + .eq('id', user.id) + .single() + + const roleData: any = dbUser?.roles + const roleName = Array.isArray(roleData) ? roleData[0]?.name : roleData?.name + const isAdminOrManager = roleName === 'admin' || roleName === 'manager' + + // Fetch leave requests + // If admin/manager, fetch all. If employee, fetch only their own. + let query = supabase + .from('leave_requests') + .select(` + id, + start_date, + end_date, + status, + reason, + created_at, + employees!inner ( + id, + user_id, + companies ( name ), + users( first_name, last_name, email ) + ) + `) + .order('created_at', { ascending: false }) + + if (!isAdminOrManager) { + query = query.eq('employees.user_id', user.id) + } + + const { data: requests } = await query + + // Also fetch companies they belong to if they want to request leave + const { data: userCompanies } = await supabase + .from('employees') + .select('companies:company_id(id, name)') + .eq('user_id', user.id) + + return ( +
+
+
+

+ + İzin Talepleri +

+

+ {isAdminOrManager + ? 'Tüm çalışanların izin taleplerini görüntüleyin ve onaylayın.' + : 'Kendi izin taleplerinizi oluşturun ve durumlarını takip edin.'} +

+
+
+ +
+ + {/* Request form - only show if they are actually employed somewhere */} +
+
+

+ Yeni İzin Talebi +

+ + {(userCompanies && userCompanies.length > 0) ? ( +
+ {/* We use a workaround API route or action for complex auth logic with FormData above */} + {/* For real production, we can inline the action. I'll mock the action call for now */} +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +