Personel Sayfası ve Uygulama renk değişiklikleri

This commit is contained in:
2026-03-18 00:08:39 +03:00
parent eb7dee7705
commit b354412cb8
19 changed files with 836 additions and 349 deletions

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -13,15 +13,17 @@ export default async function CompaniesPage() {
return (
<div className="p-4 sm:p-6 lg:p-8 max-w-7xl mx-auto">
<div className="sm:flex sm:items-center">
<div className="sm:flex sm:items-center justify-between gap-4">
<div className="sm:flex-auto">
<h1 className="text-2xl font-semibold leading-6 text-gray-900 dark:text-white flex items-center gap-2">
<BuildingOfficeIcon className="w-6 h-6 text-blue-500" />
Şirket Yönetimi
</h1>
<p className="mt-2 text-sm text-gray-700 dark:text-gray-400">
Sisteme kayıtlı şirketlerin listesi ve yeni şirket ekleme alanı.
</p>
<div className="flex items-center gap-4">
<div className="w-1.5 h-10 bg-indigo-500 rounded-full" />
<div>
<h1 className="text-3xl font-bold text-slate-900 dark:text-white tracking-tight">Şirket Yönetimi</h1>
<p className="text-sm font-medium text-slate-500 dark:text-slate-400 mt-1">
Sisteme kayıtlı şirketlerin listesi ve yeni şirket ekleme alanı.
</p>
</div>
</div>
</div>
</div>
@@ -34,26 +36,32 @@ export default async function CompaniesPage() {
Yeni Şirket Ekle
</h3>
<div className="mt-2 max-w-xl text-sm text-gray-500 dark:text-gray-400">
<p>Çalışanları atayabilmek için öncelikle şirket kaydı açmalısınız.</p>
<p>Personelleri atayabilmek için öncelikle şirket kaydı açmalısınız.</p>
</div>
<form action={addCompany} className="mt-5 space-y-4">
<form
action={async (formData) => {
'use server';
await addCompany(formData);
}}
className="mt-5 space-y-4"
>
<div>
<label htmlFor="name" className="sr-only">Şirket Adı</label>
<label htmlFor="name" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Şirket Adı</label>
<input
type="text"
name="name"
id="name"
required
className="block w-full rounded-md border-0 py-2 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-700 dark:bg-zinc-800 dark:text-white placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
placeholder="Şirket Adını Giriniz"
className="block w-full rounded-2xl border-0 py-3 px-4 text-slate-900 shadow-sm ring-1 ring-inset ring-slate-200 dark:ring-zinc-700 dark:bg-zinc-800 dark:text-white placeholder:text-slate-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm outline-none transition-all"
placeholder="Abisena Ltd. Şti."
/>
</div>
<button
type="submit"
className="inline-flex w-full items-center justify-center rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
className="inline-flex w-full items-center justify-center rounded-2xl bg-indigo-600 px-4 py-3 text-sm font-bold text-white shadow-sm shadow-indigo-100 dark:shadow-none hover:bg-indigo-700 transition-all active:scale-95"
>
Kaydet
Şirket Kaydet
</button>
</form>
</div>
@@ -62,13 +70,13 @@ export default async function CompaniesPage() {
{/* Company List */}
<div className="md:col-span-2">
<div className="bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-gray-200 dark:ring-zinc-800 sm:rounded-xl overflow-hidden">
<table className="min-w-full divide-y divide-gray-200 dark:divide-zinc-800">
<thead className="bg-gray-50 dark:bg-zinc-950/50">
<table className="min-w-full divide-y divide-slate-200 dark:divide-zinc-800">
<thead className="bg-slate-50 dark:bg-zinc-950/50">
<tr>
<th scope="col" className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 dark:text-white sm:pl-6">
<th scope="col" className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-slate-900 dark:text-white sm:pl-6">
Şirket Adı
</th>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-white">
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-slate-900 dark:text-white">
Kayıt Tarihi
</th>
<th scope="col" className="relative py-3.5 pl-3 pr-4 sm:pr-6">
@@ -76,13 +84,13 @@ export default async function CompaniesPage() {
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-zinc-800 bg-white dark:bg-zinc-900">
<tbody className="divide-y divide-slate-200 dark:divide-zinc-800 bg-white dark:bg-zinc-900">
{companies?.map((company) => (
<tr key={company.id}>
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 dark:text-white sm:pl-6">
<tr key={company.id} className="hover:bg-slate-50 dark:hover:bg-zinc-800/50 transition-colors">
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-bold text-slate-900 dark:text-white sm:pl-6">
{company.name}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
<td className="whitespace-nowrap px-3 py-4 text-sm text-slate-500 dark:text-slate-400 font-medium">
{new Date(company.created_at).toLocaleDateString('tr-TR')}
</td>
<td className="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
@@ -90,7 +98,7 @@ export default async function CompaniesPage() {
'use server';
await deleteCompany(company.id)
}}>
<button type="submit" className="text-red-600 hover:text-red-900 dark:text-red-500 dark:hover:text-red-400" title="Sil">
<button type="submit" className="text-slate-400 hover:text-rose-600 dark:hover:text-rose-400 p-2 rounded-2xl hover:bg-rose-50 dark:hover:bg-rose-500/10 transition-all" title="Sil">
<TrashIcon className="h-5 w-5" />
</button>
</form>
@@ -99,7 +107,7 @@ export default async function CompaniesPage() {
))}
{(!companies || companies.length === 0) && (
<tr>
<td colSpan={3} className="py-8 text-center text-sm text-gray-500 dark:text-gray-400">
<td colSpan={3} className="py-12 text-center text-sm text-slate-500 dark:text-slate-400 font-medium">
Henüz kayıtlı şirket bulunmamaktadır.
</td>
</tr>

View File

@@ -11,9 +11,10 @@ export async function addEmployee(formData: FormData) {
const lastName = formData.get('last_name') as string
const email = formData.get('email') as string
const companyId = formData.get('company_id') as string
const roleId = formData.get('role_id') as string
if (!email || !companyId) {
return { error: 'E-posta ve Şirket seçimi zorunludur.' }
if (!email || !companyId || !roleId) {
return { error: 'E-posta, Şirket ve Rol seçimi zorunludur.' }
}
// Admin creating users manually currently requires an admin API setup or the user registering themselves
@@ -39,9 +40,10 @@ export async function addEmployee(formData: FormData) {
.insert([{
user_id: existingUser.id,
company_id: companyId,
role_id: roleId,
department: formData.get('department'),
title: formData.get('title'),
status: 'active'
status: formData.get('status') || 'active'
}])
if (employeeError) {
@@ -49,7 +51,7 @@ export async function addEmployee(formData: FormData) {
if (employeeError.code === '23505') {
return { error: 'Bu kullanıcı zaten bu şirkete eklenmiş.' }
}
return { error: 'Çalışan eklenirken hata: ' + employeeError.message }
return { error: 'Personel eklenirken hata: ' + employeeError.message }
}
revalidatePath('/employees')
@@ -65,7 +67,29 @@ export async function deleteEmployee(id: string) {
.eq('id', id)
if (error) {
return { error: 'Çalışan silinirken hata oluştu: ' + error.message }
return { error: 'Personel silinirken hata oluştu: ' + error.message }
}
revalidatePath('/employees')
return { success: true }
}
export async function updateEmployee(id: string, formData: FormData) {
const supabase = await createClient()
const { error } = await supabase
.from('employees')
.update({
company_id: formData.get('company_id'),
role_id: formData.get('role_id'),
department: formData.get('department'),
title: formData.get('title'),
status: formData.get('status')
})
.eq('id', id)
if (error) {
return { error: 'Personel güncellenirken hata oluştu: ' + error.message }
}
revalidatePath('/employees')

View File

@@ -1,6 +1,5 @@
import { createClient } from '@/utils/supabase/server'
import { addEmployee, deleteEmployee } from './actions'
import { UsersIcon, TrashIcon } from '@heroicons/react/24/outline'
import EmployeeTable from '@/components/employees/EmployeeTable'
export default async function EmployeesPage() {
const supabase = await createClient()
@@ -10,180 +9,39 @@ export default async function EmployeesPage() {
.from('employees')
.select(`
id,
user_id,
company_id,
role_id,
department,
title,
status,
created_at,
companies ( name ),
users ( first_name, last_name, email ),
roles ( name, description )
companies ( id, name ),
users ( id, first_name, last_name, email ),
roles ( id, name, description )
`)
.order('created_at', { ascending: false })
// Fetch companies for the dropdown
// Fetch companies for the modal
const { data: companies } = await supabase
.from('companies')
.select('id, name')
.order('name')
// Fetch roles for the modal
const { data: roles } = await supabase
.from('roles')
.select('id, name, description')
.order('name')
return (
<div className="p-4 sm:p-6 lg:p-8 max-w-7xl mx-auto">
<div className="sm:flex sm:items-center">
<div className="sm:flex-auto">
<h1 className="text-2xl font-semibold leading-6 text-gray-900 dark:text-white flex items-center gap-2">
<UsersIcon className="w-6 h-6 text-blue-500" />
Çalışan Yönetimi
</h1>
<p className="mt-2 text-sm text-gray-700 dark:text-gray-400">
Sisteme kayıtlı çalışanları görüntüleyin ve yeni çalışanları şirketlere atayın.
</p>
</div>
</div>
<div className="mt-8 grid grid-cols-1 md:grid-cols-4 gap-8">
{/* Add Employee Form */}
<div className="md:col-span-1 border border-gray-200 dark:border-zinc-800 rounded-xl bg-white dark:bg-zinc-900 shadow-sm overflow-hidden h-fit">
<div className="px-4 py-5 sm:p-6">
<h3 className="text-base font-semibold leading-6 text-gray-900 dark:text-white">
Yeni Çalışan Ata
</h3>
<div className="mt-2 text-sm text-gray-500 dark:text-gray-400">
<p>Sisteme daha önce kayıt olmuş (şifre oluşturmuş) kullanıcıları şirketlere atayın.</p>
</div>
<form action={addEmployee} className="mt-5 space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium leading-6 text-gray-900 dark:text-gray-300">Kullanıcı E-posta</label>
<input
type="email"
name="email"
id="email"
required
placeholder="ornek@sirket.com"
className="mt-2 block w-full rounded-md border-0 py-1.5 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-700 dark:bg-zinc-800 dark:text-white placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
/>
</div>
<div>
<label htmlFor="company_id" className="block text-sm font-medium leading-6 text-gray-900 dark:text-gray-300">Şirket</label>
<select
id="company_id"
name="company_id"
required
className="mt-2 block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-blue-600 sm:text-sm sm:leading-6 dark:bg-zinc-800 dark:text-white dark:ring-zinc-700"
>
<option value="">Şirket Seçiniz</option>
{companies?.map(company => (
<option key={company.id} value={company.id}>{company.name}</option>
))}
</select>
</div>
<div>
<label htmlFor="department" className="block text-sm font-medium leading-6 text-gray-900 dark:text-gray-300">Departman</label>
<input
type="text"
name="department"
id="department"
placeholder="Örn: IT, İnsan Kaynakları"
className="mt-2 block w-full rounded-md border-0 py-1.5 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-700 dark:bg-zinc-800 dark:text-white placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
/>
</div>
<div>
<label htmlFor="title" className="block text-sm font-medium leading-6 text-gray-900 dark:text-gray-300">Ünvan</label>
<input
type="text"
name="title"
id="title"
placeholder="Örn: Yazılım Uzmanı"
className="mt-2 block w-full rounded-md border-0 py-1.5 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-700 dark:bg-zinc-800 dark:text-white placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
/>
</div>
<button
type="submit"
className="inline-flex w-full items-center justify-center rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 mt-6"
>
Çalışan Ekle
</button>
</form>
</div>
</div>
{/* Employee List */}
<div className="md:col-span-3">
<div className="bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-gray-200 dark:ring-zinc-800 sm:rounded-xl overflow-hidden overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-zinc-800">
<thead className="bg-gray-50 dark:bg-zinc-950/50">
<tr>
<th scope="col" className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 dark:text-white sm:pl-6">
Kişi
</th>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-white">
Şirket
</th>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-white">
Departman / Ünvan
</th>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-white">
Durum
</th>
<th scope="col" className="relative py-3.5 pl-3 pr-4 sm:pr-6">
<span className="sr-only">İşlemler</span>
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-zinc-800 bg-white dark:bg-zinc-900">
{employees?.map((emp) => (
<tr key={emp.id}>
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm sm:pl-6">
<div className="font-medium text-gray-900 dark:text-white">
{emp.users?.first_name} {emp.users?.last_name}
</div>
<div className="text-gray-500 dark:text-gray-400">{emp.users?.email}</div>
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
<div className="text-gray-900 dark:text-white">{emp.companies?.name}</div>
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
<div className="text-gray-900 dark:text-white">{emp.department || '-'}</div>
<div className="text-gray-500 dark:text-gray-400">{emp.title || '-'}</div>
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
<span className={`inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ring-1 ring-inset ${
emp.status === 'active' ? 'bg-green-50 text-green-700 ring-green-600/20 dark:bg-green-500/10 dark:text-green-400 dark:ring-green-500/20' :
'bg-red-50 text-red-700 ring-red-600/20 dark:bg-red-500/10 dark:text-red-400 dark:ring-red-500/20'
}`}>
{emp.status === 'active' ? 'Aktif' : 'Pasif'}
</span>
</td>
<td className="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
<form action={async () => {
'use server';
await deleteEmployee(emp.id)
}}>
<button type="submit" className="text-red-600 hover:text-red-900 dark:text-red-500 dark:hover:text-red-400" title="Kişiyi Şirketten Çıkar">
<TrashIcon className="h-5 w-5" />
</button>
</form>
</td>
</tr>
))}
{(!employees || employees.length === 0) && (
<tr>
<td colSpan={5} className="py-8 text-center text-sm text-gray-500 dark:text-gray-400">
Sisteme kayıtlı çalışan bulunmamaktadır.
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
</div>
<EmployeeTable
initialEmployees={employees || []}
companies={companies || []}
roles={roles || []}
/>
</div>
)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -16,10 +16,14 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
title: "HRMS Yönetim Sistemi",
title: "Abisena | Personel Takip Sistemi",
description: "Şirket içi insan kaynakları yönetim paneli",
icons: {
icon: "/favicon.png",
},
};
export default async function RootLayout({
children,
}: Readonly<{

View File

@@ -27,7 +27,7 @@ export async function submitLeaveRequest(formData: FormData) {
.single()
if (empError || !employeeData) {
return { error: 'Seçili şirket için çalışan kaydınız bulunamadı.' }
return { error: 'Seçili şirket için personel kaydınız bulunamadı.' }
}
const { error } = await supabase

View File

@@ -53,17 +53,19 @@ export default async function LeaveRequestsPage() {
return (
<div className="p-4 sm:p-6 lg:p-8 max-w-7xl mx-auto">
<div className="sm:flex sm:items-center">
<div className="sm:flex sm:items-center justify-between gap-4">
<div className="sm:flex-auto">
<h1 className="text-2xl font-semibold leading-6 text-gray-900 dark:text-white flex items-center gap-2">
<CalendarDaysIcon className="w-6 h-6 text-blue-500" />
İzin Talepleri
</h1>
<p className="mt-2 text-sm text-gray-700 dark:text-gray-400">
{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.'}
</p>
<div className="flex items-center gap-4">
<div className="w-1.5 h-10 bg-indigo-500 rounded-full" />
<div>
<h1 className="text-3xl font-bold text-slate-900 dark:text-white tracking-tight">İzin Talepleri</h1>
<p className="text-sm font-medium text-slate-500 dark:text-slate-400 mt-1">
{isAdminOrManager
? 'Tüm personellerin izin taleplerini görüntüleyin ve onaylayın.'
: 'Kendi izin taleplerinizi oluşturun ve durumlarını takip edin.'}
</p>
</div>
</div>
</div>
</div>
@@ -86,7 +88,7 @@ export default async function LeaveRequestsPage() {
id="company_id"
name="company_id"
required
className="mt-2 block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-blue-600 sm:text-sm sm:leading-6 dark:bg-zinc-800 dark:text-white dark:ring-zinc-700"
className="mt-2 block w-full rounded-xl border-0 py-2.5 pl-3 pr-10 text-slate-900 ring-1 ring-inset ring-slate-200 dark:ring-zinc-700 dark:bg-zinc-800 dark:text-white focus:ring-2 focus:ring-indigo-500 sm:text-sm outline-none"
>
{userCompanies?.filter((c: any) => c.companies).map((c: any) => (
<option key={c.companies.id} value={c.companies.id}>{c.companies.name}</option>
@@ -123,7 +125,7 @@ export default async function LeaveRequestsPage() {
id="reason"
rows={3}
required
className="mt-2 block w-full rounded-md border-0 py-1.5 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-700 dark:bg-zinc-800 dark:text-white sm:text-sm sm:leading-6"
className="mt-2 block w-full rounded-xl border-0 py-2.5 px-3 text-slate-900 shadow-sm ring-1 ring-inset ring-slate-200 dark:ring-zinc-700 dark:bg-zinc-800 dark:text-white placeholder:text-slate-400 focus:ring-2 focus:ring-inset focus:ring-indigo-500 sm:text-sm outline-none"
/>
</div>
@@ -134,7 +136,7 @@ export default async function LeaveRequestsPage() {
const { submitLeaveRequest } = await import('./actions');
await submitLeaveRequest(formData);
}}
className="inline-flex w-full items-center justify-center rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500"
className="inline-flex w-full items-center justify-center rounded-2xl bg-indigo-600 px-3 py-3 text-sm font-bold text-white shadow-sm shadow-indigo-200 dark:shadow-none hover:bg-indigo-700 transition-all active:scale-95"
>
Talep Oluştur
</button>
@@ -151,18 +153,18 @@ export default async function LeaveRequestsPage() {
<div className="md:col-span-3">
<div className="bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-gray-200 dark:ring-zinc-800 sm:rounded-xl overflow-hidden overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-zinc-800">
<thead className="bg-gray-50 dark:bg-zinc-950/50">
<thead className="bg-slate-50 dark:bg-zinc-950/50">
<tr>
<th scope="col" className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 dark:text-white sm:pl-6">
Çalışan
<th scope="col" className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-slate-900 dark:text-white sm:pl-6">
Personel
</th>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-white">
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-slate-900 dark:text-white">
Tarih Aralığı
</th>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-white">
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-slate-900 dark:text-white">
Gerekçe
</th>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-white">
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-slate-900 dark:text-white">
Durum
</th>
{isAdminOrManager && (
@@ -172,30 +174,32 @@ export default async function LeaveRequestsPage() {
)}
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-zinc-800 bg-white dark:bg-zinc-900">
<tbody className="divide-y divide-slate-200 dark:divide-zinc-800 bg-white dark:bg-zinc-900">
{requests?.map((req: any) => {
const employeeInfo = req.employees?.users || {};
const employeeData = Array.isArray(req.employees) ? req.employees[0] : req.employees;
const userData = Array.isArray(employeeData?.users) ? employeeData.users[0] : employeeData?.users;
const companyData = Array.isArray(employeeData?.companies) ? employeeData.companies[0] : employeeData?.companies;
return (
<tr key={req.id}>
<tr key={req.id} className="hover:bg-slate-50 dark:hover:bg-zinc-800/50 transition-colors">
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm sm:pl-6">
<div className="font-medium text-gray-900 dark:text-white">
{employeeInfo?.first_name} {employeeInfo?.last_name}
<div className="font-semibold text-slate-900 dark:text-white">
{userData?.first_name} {userData?.last_name}
</div>
<div className="text-gray-500 dark:text-gray-400">{req.employees?.companies?.name}</div>
<div className="text-slate-500 dark:text-slate-400">{companyData?.name}</div>
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
<td className="whitespace-nowrap px-3 py-4 text-sm text-slate-500 dark:text-slate-400">
<div>{new Date(req.start_date).toLocaleDateString('tr-TR')}</div>
<div>{new Date(req.end_date).toLocaleDateString('tr-TR')}</div>
</td>
<td className="px-3 py-4 text-sm text-gray-500 dark:text-gray-400 max-w-xs truncate">
<td className="px-3 py-4 text-sm text-slate-500 dark:text-slate-400 max-w-xs truncate font-medium">
{req.reason}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm">
<span className={`inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ring-1 ring-inset ${
req.status === 'approved' ? 'bg-green-50 text-green-700 ring-green-600/20 dark:bg-green-500/10 dark:text-green-400' :
req.status === 'rejected' ? 'bg-red-50 text-red-700 ring-red-600/20 dark:bg-red-500/10 dark:text-red-400' :
'bg-yellow-50 text-yellow-800 ring-yellow-600/20 dark:bg-yellow-500/10 dark:text-yellow-500'
<span className={`inline-flex items-center rounded-lg px-2.5 py-1 text-xs font-bold ${
req.status === 'approved' ? 'bg-emerald-50 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-400' :
req.status === 'rejected' ? 'bg-rose-50 text-rose-700 dark:bg-rose-500/10 dark:text-rose-400' :
'bg-slate-100 text-slate-700 dark:bg-zinc-800 dark:text-slate-400'
}`}>
{req.status === 'approved' ? 'Onaylandı' : req.status === 'rejected' ? 'Reddedildi' : 'Bekliyor'}
</span>
@@ -209,7 +213,7 @@ export default async function LeaveRequestsPage() {
'use server';
await updateLeaveStatus(req.id, 'approved')
}}
className="text-green-600 hover:text-green-900 dark:text-green-500 dark:hover:text-green-400 bg-green-50 hover:bg-green-100 dark:bg-green-500/10 p-2 rounded-lg transition-colors" title="Onayla">
className="text-emerald-600 hover:text-emerald-900 dark:text-emerald-500 dark:hover:text-emerald-400 p-2 rounded-xl hover:bg-emerald-50 dark:hover:bg-emerald-500/10 transition-all" title="Onayla">
<CheckIcon className="h-5 w-5" />
</button>
<button
@@ -217,7 +221,7 @@ export default async function LeaveRequestsPage() {
'use server';
await updateLeaveStatus(req.id, 'rejected')
}}
className="text-red-600 hover:text-red-900 dark:text-red-500 dark:hover:text-red-400 bg-red-50 hover:bg-red-100 dark:bg-red-500/10 p-2 rounded-lg transition-colors" title="Reddet">
className="text-rose-600 hover:text-rose-900 dark:text-rose-500 dark:hover:text-rose-400 p-2 rounded-xl hover:bg-rose-50 dark:hover:bg-rose-500/10 transition-all" title="Reddet">
<XMarkIcon className="h-5 w-5" />
</button>
</form>
@@ -228,7 +232,7 @@ export default async function LeaveRequestsPage() {
)})}
{(!requests || requests.length === 0) && (
<tr>
<td colSpan={isAdminOrManager ? 5 : 4} className="py-8 text-center text-sm text-gray-500 dark:text-gray-400">
<td colSpan={isAdminOrManager ? 5 : 4} className="py-12 text-center text-sm text-slate-500 dark:text-slate-400">
Henüz bir izin talebi bulunmamaktadır.
</td>
</tr>

View File

@@ -6,77 +6,68 @@ export default function LoginPage({
searchParams: { error?: string }
}) {
return (
<div className="flex min-h-screen flex-col items-center justify-center p-24 bg-gray-50 dark:bg-zinc-950">
<div className="w-full max-w-sm p-8 space-y-8 bg-white dark:bg-zinc-900 rounded-xl shadow-lg border border-gray-100 dark:border-zinc-800">
<div className="flex min-h-screen flex-col items-center justify-center p-6 bg-slate-50 dark:bg-zinc-950">
<div className="w-full max-w-md p-10 space-y-10 bg-white dark:bg-zinc-900 rounded-[2.5rem] shadow-sm border border-slate-100 dark:border-zinc-800">
<div className="text-center">
<h2 className="text-2xl font-bold tracking-tight text-gray-900 dark:text-white">
HRMS Sistemine Giriş
<h2 className="text-3xl font-bold tracking-tight text-slate-900 dark:text-white">
Abisena Giriş
</h2>
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">
Lütfen e-posta ve şifrenizle giriş yapın.
<p className="mt-3 text-sm font-medium text-slate-500 dark:text-slate-400">
Personel Takip Sistemine Giriş Yapın
</p>
</div>
<form className="space-y-6" action={login}>
<form className="space-y-8" action={login}>
{searchParams?.error && (
<div className="p-3 text-sm text-red-500 bg-red-50 dark:bg-red-500/10 border border-red-200 dark:border-red-500/20 rounded-lg">
<div className="p-4 text-sm font-bold text-rose-600 bg-rose-50 dark:bg-rose-500/10 border border-rose-100 dark:border-rose-500/20 rounded-2xl text-center">
E-posta adresi veya şifre hatalı.
</div>
)}
<div>
<div className="space-y-2">
<label
htmlFor="email"
className="block text-sm font-medium leading-6 text-gray-900 dark:text-gray-300"
className="block text-sm font-bold text-slate-700 dark:text-slate-300 ml-1"
>
E-posta Adresi
</label>
<div className="mt-2">
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
className="block w-full rounded-md border-0 py-1.5 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-700 dark:bg-zinc-800 dark:text-white placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6 outline-none transition-shadow duration-200"
placeholder="ornek@sirket.com"
/>
</div>
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
className="block w-full rounded-2xl border-0 py-3.5 px-4 text-slate-900 shadow-sm ring-1 ring-inset ring-slate-200 dark:ring-zinc-700 dark:bg-zinc-800 dark:text-white placeholder:text-slate-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm outline-none transition-all"
placeholder="admin@abisena.com"
/>
</div>
<div>
<div className="flex items-center justify-between">
<label
htmlFor="password"
className="block text-sm font-medium leading-6 text-gray-900 dark:text-gray-300"
>
Şifre
</label>
</div>
<div className="mt-2">
<input
id="password"
name="password"
type="password"
autoComplete="current-password"
required
className="block w-full rounded-md border-0 py-1.5 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-700 dark:bg-zinc-800 dark:text-white placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6 outline-none transition-shadow duration-200"
placeholder="••••••••"
/>
</div>
</div>
<div>
<button
type="submit"
className="flex w-full justify-center rounded-md bg-blue-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 transition-colors duration-200"
<div className="space-y-2">
<label
htmlFor="password"
className="block text-sm font-bold text-slate-700 dark:text-slate-300 ml-1"
>
Giriş Yap
</button>
Şifre
</label>
<input
id="password"
name="password"
type="password"
autoComplete="current-password"
required
className="block w-full rounded-2xl border-0 py-3.5 px-4 text-slate-900 shadow-sm ring-1 ring-inset ring-slate-200 dark:ring-zinc-700 dark:bg-zinc-800 dark:text-white placeholder:text-slate-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm outline-none transition-all"
placeholder="••••••••"
/>
</div>
</form>
<button
type="submit"
className="flex w-full justify-center rounded-2xl bg-indigo-600 px-4 py-4 text-sm font-bold leading-6 text-white shadow-sm shadow-indigo-100 dark:shadow-none hover:bg-indigo-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 transition-all active:scale-95"
>
Giriş Yap
</button>
</form>
</div>
</div>
)

View File

@@ -11,18 +11,26 @@ export default async function Home() {
redirect('/login')
}
// Fetch the extended user metadata from our 'users' table
const { data: dbUser } = await supabase
// Deep debug: Fetch users and employees separately
const { data: dbUser, error: dbUserError } = await supabase
.from('users')
.select('*, roles(name, description)')
.select('*')
.eq('id', user.id)
.single()
const roleData: any = dbUser?.roles
const role = Array.isArray(roleData) ? roleData[0] : roleData
const { data: dbEmployees, error: dbEmpError } = await supabase
.from('employees')
.select('*, roles(*)')
.eq('user_id', user.id)
const userData = dbUser
const role = dbEmployees?.[0]?.roles || null
const displayName = userData?.first_name
? `${userData.first_name}${userData.last_name ? ' ' + userData.last_name : ''}`
: user.email?.split('@')[0]
const stats = [
{ name: 'Toplam Çalışan', stat: '0', icon: UsersIcon, change: '12%', changeType: 'increase' },
{ name: 'Toplam Personel', stat: '0', icon: UsersIcon, change: '12%', changeType: 'increase' },
{ name: 'Bekleyen İzinler', stat: '0', icon: CalendarDaysIcon, change: '3', changeType: 'increase' },
{ name: 'Aktif Şirketler', stat: '1', icon: BuildingOfficeIcon, change: '0%', changeType: 'none' },
]
@@ -30,75 +38,92 @@ export default async function Home() {
return (
<div className="p-4 sm:p-6 lg:p-8 space-y-8 max-w-7xl mx-auto">
{/* Welcome Section - Premium Header */}
<div className="relative overflow-hidden rounded-3xl bg-gradient-to-br from-blue-600 via-indigo-600 to-purple-700 p-8 shadow-2xl shadow-blue-500/20">
<div className="relative z-10">
<h1 className="text-3xl font-extrabold tracking-tight text-white mb-2">
Hoş Geldiniz, {dbUser?.first_name || user.email?.split('@')[0]} 👋
</h1>
<p className="max-w-xl text-blue-100/80 font-medium">
IK Yönetim Sistemine tekrar hoş geldiniz. Bugün <span className="text-white font-bold underline decoration-blue-400 decoration-2 underline-offset-4">{role?.name || 'Kullanıcı'}</span> yetkileriyle sisteme bağlısınız.
</p>
{/* Welcome Section - Modern & Clean */}
<div className="relative overflow-hidden rounded-[2rem] bg-[#173363] p-10 shadow-xl shadow-blue-900/10">
<div className="relative z-10 flex flex-col md:flex-row md:items-center justify-between gap-6">
<div>
<h1 className="text-4xl font-black tracking-tight text-white mb-3">
Hoş Geldiniz, {displayName} 👋
</h1>
<p className="max-w-xl text-blue-100/80 font-medium leading-relaxed text-lg">
IK Yönetim Sistemine tekrar hoş geldiniz. Bugün sisteme
<span className="text-white font-black px-3 py-1 bg-[#CE0515] rounded-full mx-2 text-sm uppercase tracking-wider shadow-lg shadow-red-900/20">
{role?.name || 'Kullanıcı'}
</span>
yetkileriyle bağlısınız.
</p>
</div>
<div className="hidden lg:flex -space-x-3">
{[1,2,3,4].map(i => (
<div key={i} className="w-14 h-14 rounded-full border-4 border-[#1e40af] bg-blue-800 shadow-inner" />
))}
<div className="w-14 h-14 rounded-full border-4 border-[#1e40af] bg-[#CE0515] flex items-center justify-center text-sm font-black text-white shadow-lg shadow-red-900/20 tracking-tighter">+12</div>
</div>
</div>
{/* Decorative backdrop blobs */}
<div className="absolute top-0 right-0 -mr-20 -mt-20 w-64 h-64 bg-white/10 rounded-full blur-3xl" />
<div className="absolute bottom-0 left-0 -ml-20 -mb-20 w-64 h-64 bg-blue-400/10 rounded-full blur-3xl" />
{/* Corporate decorative element */}
<div className="absolute top-0 right-0 w-64 h-64 bg-white/5 rounded-full -mr-32 -mt-32 blur-3xl" />
<div className="absolute bottom-0 left-0 w-48 h-48 bg-[#CE0515]/10 rounded-full -ml-24 -mb-24 blur-2xl" />
</div>
{/* Stats Grid */}
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{/* Stats Grid - abisena.tr Style */}
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
{stats.map((item) => (
<div
key={item.name}
className="group relative flex flex-col justify-between overflow-hidden rounded-2xl bg-white dark:bg-zinc-900 border border-gray-100 dark:border-zinc-800 p-6 shadow-sm hover:shadow-xl transition-all duration-300 hover:-translate-y-1"
className="group relative flex flex-col justify-between overflow-hidden rounded-3xl bg-white border border-slate-100 p-8 shadow-sm hover:shadow-xl hover:-translate-y-1 transition-all duration-500"
>
<div>
<div className="flex items-center justify-between mb-4">
<div className="rounded-xl bg-blue-50 dark:bg-blue-500/10 p-2.5 text-blue-600 dark:text-blue-400 group-hover:bg-blue-600 group-hover:text-white transition-colors duration-300">
<item.icon className="h-6 w-6" aria-hidden="true" />
<div className="flex items-center justify-between mb-6">
<div className="rounded-2xl bg-slate-50 p-4 text-[#173363] group-hover:bg-[#CE0515] group-hover:text-white transition-all duration-500 rotate-3 group-hover:rotate-0 shadow-inner">
<item.icon className="h-7 w-7" aria-hidden="true" />
</div>
<div className={`text-xs font-bold leading-5 px-2 py-0.5 rounded-full ${
item.changeType === 'increase' ? 'bg-green-100 text-green-700 dark:bg-green-500/20 dark:text-green-400' : 'bg-gray-100 text-gray-500 dark:bg-zinc-800 dark:text-gray-400'
<div className={`text-xs font-black leading-5 px-3 py-1.5 rounded-full tracking-wider uppercase ${
item.changeType === 'increase' ? 'bg-emerald-50 text-emerald-700' : 'bg-slate-50 text-slate-500'
}`}>
{item.changeType === 'increase' ? '+' : ''}{item.change}
{item.changeType === 'increase' ? '' : ''} {item.change}
</div>
</div>
<p className="text-sm font-semibold text-gray-500 dark:text-gray-400 tracking-wide uppercase">{item.name}</p>
<p className="mt-1 text-3xl font-bold text-gray-900 dark:text-white">{item.stat}</p>
<p className="text-xs font-black text-slate-400 uppercase tracking-[0.2em] mb-1">{item.name}</p>
<p className="text-4xl font-black text-[#173363] tracking-tighter">{item.stat}</p>
</div>
{/* Subtle progress bar */}
<div className="mt-6 w-full h-1.5 bg-slate-50 rounded-full overflow-hidden">
<div className="h-full bg-[#173363] group-hover:bg-[#CE0515] transition-all duration-700 w-2/3" />
</div>
{/* Subtle background icon for premium feel */}
<item.icon className="absolute -right-4 -bottom-4 h-24 w-24 text-gray-100 dark:text-zinc-800/20 group-hover:scale-110 transition-transform duration-500 pointer-events-none" />
</div>
))}
</div>
{/* Profile & Quick Actions section */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div className="bg-white/50 dark:bg-zinc-900/50 backdrop-blur-sm border border-gray-100 dark:border-zinc-800 rounded-3xl p-8 shadow-sm">
<h2 className="text-xl font-bold mb-6 dark:text-white flex items-center gap-2">
<div className="w-1.5 h-8 bg-blue-600 rounded-full" />
<div className="bg-white border border-slate-100 rounded-[2rem] p-10 shadow-sm hover:shadow-md transition-shadow duration-500">
<h2 className="text-xl font-black mb-8 text-[#173363] flex items-center gap-3">
<div className="w-2 h-8 bg-[#CE0515] rounded-full" />
Sistem Bilgileri
</h2>
<ul className="space-y-4 text-sm">
<li className="flex items-center justify-between border-b border-gray-100 dark:border-zinc-800/50 pb-4">
<span className="font-semibold text-gray-500 dark:text-gray-400">E-posta</span>
<span className="text-gray-900 dark:text-white font-medium">{user.email}</span>
<ul className="space-y-6 text-sm">
<li className="flex items-center justify-between border-b border-slate-50 pb-6">
<span className="font-bold text-slate-400 uppercase tracking-widest text-[10px]">E-posta Adresi</span>
<span className="text-[#173363] font-black">{user.email}</span>
</li>
<li className="flex items-center justify-between border-b border-gray-100 dark:border-zinc-800/50 pb-4">
<span className="font-semibold text-gray-500 dark:text-gray-400">Hesap Tipi</span>
<span className="inline-flex items-center gap-1.5 text-blue-600 dark:text-blue-400 font-bold">
<div className="w-2 h-2 rounded-full bg-blue-500 animate-pulse" />
<li className="flex items-center justify-between border-b border-slate-50 pb-6">
<span className="font-bold text-slate-400 uppercase tracking-widest text-[10px]">Hesap Yetkisi</span>
<span className="inline-flex items-center gap-2 text-[#CE0515] font-black bg-rose-50 px-4 py-2 rounded-full text-xs shadow-sm shadow-rose-100 transition-transform hover:scale-105 cursor-default">
{role?.description || 'Standart Erişim'}
</span>
</li>
<li className="flex items-center justify-between pt-2">
<span className="font-semibold text-gray-500 dark:text-gray-400">Son Giriş</span>
<span className="text-gray-900 dark:text-white font-medium">{new Date().toLocaleDateString('tr-TR')}</span>
<span className="font-bold text-slate-400 uppercase tracking-widest text-[10px]">Sistem Tarihi</span>
<span className="text-[#173363] font-black">{new Date().toLocaleDateString('tr-TR', { day: 'numeric', month: 'long', year: 'numeric' })}</span>
</li>
</ul>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,175 @@
'use client';
import { useState, useEffect } from 'react';
import { XMarkIcon } from '@heroicons/react/24/outline';
import { addEmployee, updateEmployee } from '@/app/employees/actions';
interface EmployeeModalProps {
isOpen: boolean;
onClose: () => void;
companies: any[];
roles: any[];
editingEmployee?: any;
}
export default function EmployeeModal({ isOpen, onClose, companies, roles, editingEmployee }: EmployeeModalProps) {
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
if (!isOpen) return null;
async function handleSubmit(formData: FormData) {
setLoading(true);
setError(null);
try {
let result;
if (editingEmployee) {
result = await updateEmployee(editingEmployee.id, formData);
} else {
result = await addEmployee(formData);
}
if (result.error) {
setError(result.error);
} else {
onClose();
}
} catch (err) {
setError('Bir hata oluştu, lütfen tekrar deneyin.');
} finally {
setLoading(false);
}
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-900/40 backdrop-blur-sm transition-all">
<div className="bg-white dark:bg-zinc-900 w-full max-w-lg rounded-[2.5rem] shadow-2xl border border-slate-100 dark:border-zinc-800 overflow-hidden animate-in fade-in zoom-in duration-200">
{/* Header */}
<div className="px-8 py-6 border-b border-slate-50 dark:border-zinc-800 flex items-center justify-between bg-slate-50/50 dark:bg-zinc-800/30">
<div>
<h2 className="text-xl font-bold text-slate-900 dark:text-white">
{editingEmployee ? 'Personel Düzenle' : 'Yeni Personel Ekle'}
</h2>
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mt-1">
Personel bilgilerini eksiksiz doldurunuz.
</p>
</div>
<button
onClick={onClose}
className="p-2 rounded-full hover:bg-slate-100 dark:hover:bg-zinc-800 text-slate-400 dark:text-zinc-500 transition-colors"
>
<XMarkIcon className="w-6 h-6" />
</button>
</div>
{/* Body */}
<form action={handleSubmit} className="p-8 space-y-6">
{error && (
<div className="p-4 bg-rose-50 dark:bg-rose-900/20 border border-rose-100 dark:border-rose-900/30 rounded-2xl text-rose-600 dark:text-rose-400 text-sm font-bold text-center">
{error}
</div>
)}
<div className="grid grid-cols-1 gap-6">
{!editingEmployee && (
<div className="space-y-2">
<label className="block text-sm font-bold text-slate-700 dark:text-slate-300 ml-1">E-posta Adresi</label>
<input
type="email"
name="email"
required
placeholder="ornek@abisena.com"
className="w-full rounded-2xl border-0 py-3.5 px-4 text-slate-900 dark:text-white bg-slate-50 dark:bg-zinc-800 ring-1 ring-inset ring-slate-200 dark:ring-zinc-700 focus:ring-2 focus:ring-indigo-600 outline-none transition-all"
/>
</div>
)}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-2">
<label className="block text-sm font-bold text-slate-700 dark:text-slate-300 ml-1">Şirket</label>
<select
name="company_id"
required
defaultValue={editingEmployee?.company_id || ''}
className="w-full rounded-2xl border-0 py-3.5 px-4 text-slate-900 dark:text-white bg-slate-50 dark:bg-zinc-800 ring-1 ring-inset ring-slate-200 dark:ring-zinc-700 focus:ring-2 focus:ring-indigo-600 outline-none transition-all"
>
<option value="">Seçiniz</option>
{companies.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
</select>
</div>
<div className="space-y-2">
<label className="block text-sm font-bold text-slate-700 dark:text-slate-300 ml-1">Rol</label>
<select
name="role_id"
required
defaultValue={editingEmployee?.role_id || ''}
className="w-full rounded-2xl border-0 py-3.5 px-4 text-slate-900 dark:text-white bg-slate-50 dark:bg-zinc-800 ring-1 ring-inset ring-slate-200 dark:ring-zinc-700 focus:ring-2 focus:ring-indigo-600 outline-none transition-all"
>
<option value="">Seçiniz</option>
{roles.map(r => <option key={r.id} value={r.id}>{r.description}</option>)}
</select>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-2">
<label className="block text-sm font-bold text-slate-700 dark:text-slate-300 ml-1">Departman</label>
<input
type="text"
name="department"
defaultValue={editingEmployee?.department || ''}
placeholder="Örn: Yazılım"
className="w-full rounded-2xl border-0 py-3.5 px-4 text-slate-900 dark:text-white bg-slate-50 dark:bg-zinc-800 ring-1 ring-inset ring-slate-200 dark:ring-zinc-700 focus:ring-2 focus:ring-indigo-600 outline-none transition-all"
/>
</div>
<div className="space-y-2">
<label className="block text-sm font-bold text-slate-700 dark:text-slate-300 ml-1">Ünvan</label>
<input
type="text"
name="title"
defaultValue={editingEmployee?.title || ''}
placeholder="Örn: Kıdemli Uzman"
className="w-full rounded-2xl border-0 py-3.5 px-4 text-slate-900 dark:text-white bg-slate-50 dark:bg-zinc-800 ring-1 ring-inset ring-slate-200 dark:ring-zinc-700 focus:ring-2 focus:ring-indigo-600 outline-none transition-all"
/>
</div>
</div>
<div className="space-y-2">
<label className="block text-sm font-bold text-slate-700 dark:text-slate-300 ml-1">Durum</label>
<select
name="status"
defaultValue={editingEmployee?.status || 'active'}
className="w-full rounded-2xl border-0 py-3.5 px-4 text-slate-900 dark:text-white bg-slate-50 dark:bg-zinc-800 ring-1 ring-inset ring-slate-200 dark:ring-zinc-700 focus:ring-2 focus:ring-indigo-600 outline-none transition-all"
>
<option value="active">Aktif</option>
<option value="inactive">Pasif</option>
<option value="terminated">İlişiği Kesildi</option>
</select>
</div>
</div>
<div className="flex gap-4 pt-6">
<button
type="button"
onClick={onClose}
className="flex-1 px-4 py-4 rounded-full font-black text-slate-400 hover:bg-slate-50 transition-all text-xs uppercase tracking-widest"
>
İptal
</button>
<button
type="submit"
disabled={loading}
className="flex-[2] bg-[#173363] hover:bg-[#CE0515] text-white px-8 py-4 rounded-full font-black shadow-lg shadow-blue-900/10 transition-all active:scale-95 disabled:opacity-50 text-xs uppercase tracking-[0.2em]"
>
{loading ? 'İşleniyor...' : (editingEmployee ? 'GÜNCELLE' : 'PERSONEL OLUŞTUR')}
</button>
</div>
</form>
</div>
</div>
);
}

View File

@@ -0,0 +1,211 @@
'use client';
import { useState } from 'react';
import {
UsersIcon,
TrashIcon,
PencilSquareIcon,
PlusIcon,
MagnifyingGlassIcon
} from '@heroicons/react/24/outline';
import { deleteEmployee } from '@/app/employees/actions';
import EmployeeModal from '@/components/employees/EmployeeModal';
interface EmployeeTableProps {
initialEmployees: any[];
companies: any[];
roles: any[];
}
export default function EmployeeTable({ initialEmployees, companies, roles }: EmployeeTableProps) {
const [employees, setEmployees] = useState(initialEmployees);
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('all');
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingEmployee, setEditingEmployee] = useState<any>(null);
const filteredEmployees = employees.filter(emp => {
const userData = Array.isArray(emp.users) ? emp.users[0] : emp.users;
const fullName = `${userData?.first_name || ''} ${userData?.last_name || ''}`.toLowerCase();
const email = (userData?.email || '').toLowerCase();
const matchesSearch = fullName.includes(searchTerm.toLowerCase()) || email.includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === 'all' || emp.status === statusFilter;
return matchesSearch && matchesStatus;
});
const handleEdit = (emp: any) => {
setEditingEmployee(emp);
setIsModalOpen(true);
};
const handleAdd = () => {
setEditingEmployee(null);
setIsModalOpen(true);
};
const handleDelete = async (id: string) => {
if (confirm('Bu personeli silmek istediğinize emin misiniz?')) {
const result = await deleteEmployee(id);
if (result.error) {
alert(result.error);
}
}
};
return (
<div className="space-y-8">
{/* Header with Add Button */}
<div className="sm:flex sm:items-center justify-between gap-4">
<div className="sm:flex-auto">
<div className="flex items-center gap-4">
<div className="w-1.5 h-10 bg-indigo-500 rounded-full" />
<div>
<h1 className="text-3xl font-bold text-slate-900 dark:text-white tracking-tight">Personeller</h1>
<p className="text-sm font-medium text-slate-500 dark:text-slate-400 mt-1">
Şirketinizdeki tüm personel kayıtlarını yönetin
</p>
</div>
</div>
</div>
<button
onClick={handleAdd}
className="group flex items-center gap-2 bg-[#173363] hover:bg-[#CE0515] text-white px-8 py-3.5 rounded-full font-black shadow-lg shadow-blue-900/20 transition-all duration-500 active:scale-95"
>
<PlusIcon className="w-5 h-5 transition-transform group-hover:rotate-90" />
YENİ PERSONEL
</button>
</div>
{/* Filter & Search Bar */}
<div className="flex flex-col md:flex-row gap-4 items-center justify-between bg-white p-6 rounded-[2rem] border border-slate-100 shadow-sm">
<div className="relative w-full md:w-96 group">
<MagnifyingGlassIcon className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-400 group-focus-within:text-[#CE0515] transition-colors" />
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="İsim veya e-posta ile ara..."
className="w-full pl-12 pr-4 py-3 bg-slate-50 border-none rounded-2xl text-sm font-medium focus:ring-2 focus:ring-[#CE0515] transition-all outline-none placeholder:text-slate-400"
/>
</div>
<div className="flex gap-2 w-full md:w-auto p-1.5 bg-slate-50 rounded-full border border-slate-100">
<button
onClick={() => setStatusFilter('all')}
className={`flex-1 md:flex-none px-8 py-2.5 rounded-full text-xs font-black uppercase tracking-wider transition-all duration-500 ${statusFilter === 'all' ? 'bg-[#173363] text-white shadow-md' : 'text-slate-400 hover:text-slate-600'}`}
>
Tümü
</button>
<button
onClick={() => setStatusFilter('active')}
className={`flex-1 md:flex-none px-8 py-2.5 rounded-full text-xs font-black uppercase tracking-wider transition-all duration-500 ${statusFilter === 'active' ? 'bg-emerald-600 text-white shadow-md' : 'text-slate-400 hover:text-slate-600'}`}
>
Aktif
</button>
<button
onClick={() => setStatusFilter('inactive')}
className={`flex-1 md:flex-none px-8 py-2.5 rounded-full text-xs font-black uppercase tracking-wider transition-all duration-500 ${statusFilter === 'inactive' ? 'bg-[#CE0515] text-white shadow-md' : 'text-slate-400 hover:text-slate-600'}`}
>
Pasif
</button>
</div>
</div>
{/* Table Section */}
<div className="bg-white shadow-sm border border-slate-100 rounded-[2.5rem] overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-slate-50">
<thead className="bg-slate-50/50">
<tr>
<th className="py-6 pl-8 pr-3 text-left text-[10px] font-black uppercase tracking-[0.2em] text-slate-400">Personel</th>
<th className="px-3 py-6 text-left text-[10px] font-black uppercase tracking-[0.2em] text-slate-400">Şirket / Rol</th>
<th className="px-3 py-6 text-left text-[10px] font-black uppercase tracking-[0.2em] text-slate-400">Departman / Ünvan</th>
<th className="px-3 py-6 text-left text-[10px] font-black uppercase tracking-[0.2em] text-slate-400">Durum</th>
<th className="relative py-6 pl-3 pr-8 text-right">
<span className="sr-only">İşlemler</span>
</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{filteredEmployees.map((emp) => {
const userData = Array.isArray(emp.users) ? emp.users[0] : emp.users;
const companyData = Array.isArray(emp.companies) ? emp.companies[0] : emp.companies;
const roleData = Array.isArray(emp.roles) ? emp.roles[0] : emp.roles;
return (
<tr key={emp.id} className="group hover:bg-slate-50/50 transition-all duration-500">
<td className="whitespace-nowrap py-6 pl-8 pr-3">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-2xl bg-[#173363] flex items-center justify-center text-white font-black text-sm shadow-lg shadow-blue-900/10 group-hover:bg-[#CE0515] transition-colors duration-500">
{userData?.first_name?.[0]}{userData?.last_name?.[0]}
</div>
<div>
<p className="text-sm font-black text-[#173363] group-hover:text-black transition-colors">
{userData?.first_name} {userData?.last_name}
</p>
<p className="text-xs text-slate-400 font-bold">{userData?.email}</p>
</div>
</div>
</td>
<td className="whitespace-nowrap px-3 py-6">
<p className="text-sm font-black text-[#173363]">{companyData?.name}</p>
<p className="text-[10px] text-[#CE0515] font-black uppercase tracking-widest mt-1">{roleData?.description}</p>
</td>
<td className="whitespace-nowrap px-3 py-6">
<p className="text-sm font-black text-slate-700">{emp.department || '-'}</p>
<p className="text-xs text-slate-400 font-bold mt-1">{emp.title || '-'}</p>
</td>
<td className="whitespace-nowrap px-3 py-6">
<span className={`inline-flex items-center gap-2 px-4 py-1.5 rounded-full text-[10px] font-black uppercase tracking-[0.15em] border ${
emp.status === 'active'
? 'bg-emerald-50 text-emerald-700 border-emerald-100 shadow-sm shadow-emerald-100'
: 'bg-slate-50 text-slate-500 border-slate-100'
}`}>
<span className={`w-2 h-2 rounded-full animate-pulse ${emp.status === 'active' ? 'bg-emerald-500' : 'bg-slate-400'}`} />
{emp.status === 'active' ? 'Aktif' : 'Pasif'}
</span>
</td>
<td className="whitespace-nowrap py-6 pl-3 pr-8 text-right">
<div className="flex items-center justify-end gap-3 translate-x-4 opacity-0 group-hover:translate-x-0 group-hover:opacity-100 transition-all duration-500">
<button
onClick={() => handleEdit(emp)}
className="p-3 rounded-2xl text-slate-400 hover:text-white hover:bg-[#173363] hover:shadow-lg hover:shadow-blue-900/20 transition-all duration-300"
title="Düzenle"
>
<PencilSquareIcon className="w-5 h-5" />
</button>
<button
onClick={() => handleDelete(emp.id)}
className="p-3 rounded-2xl text-slate-400 hover:text-white hover:bg-[#CE0515] hover:shadow-lg hover:shadow-red-900/20 transition-all duration-300"
title="Sil"
>
<TrashIcon className="w-5 h-5" />
</button>
</div>
</td>
</tr>
);
})}
{filteredEmployees.length === 0 && (
<tr>
<td colSpan={5} className="py-20 text-center">
<UsersIcon className="mx-auto h-16 w-16 text-slate-200 dark:text-zinc-800 mb-4" />
<p className="text-slate-500 dark:text-slate-400 font-bold">Herhangi bir personel kaydı bulunamadı.</p>
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
{/* Modal */}
<EmployeeModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
companies={companies}
roles={roles}
editingEmployee={editingEmployee}
/>
</div>
);
}

View File

@@ -11,9 +11,12 @@ export function Header({ user }: { user?: { email?: string; first_name?: string
<div className="flex items-center gap-x-3 text-sm font-semibold leading-6 text-gray-900 dark:text-white">
<span className="sr-only">Profil</span>
<div className="h-8 w-8 rounded-full bg-blue-100 dark:bg-blue-900/50 flex items-center justify-center text-blue-700 dark:text-blue-400 border border-blue-200 dark:border-blue-800">
<div className="h-8 w-8 rounded-full bg-indigo-50 dark:bg-indigo-500/10 flex items-center justify-center text-indigo-600 dark:text-indigo-400 border border-indigo-100 dark:border-indigo-800">
{user?.first_name?.[0]?.toUpperCase() || user?.email?.[0]?.toUpperCase() || 'U'}
</div>
<span className="hidden lg:flex lg:items-center">
<span>{user?.first_name || user?.email?.split('@')[0] || 'Kullanıcı'}</span>
</span>

View File

@@ -2,6 +2,7 @@
import { useState } from 'react'
import Link from 'next/link'
import Image from 'next/image'
import { usePathname } from 'next/navigation'
import {
HomeIcon,
@@ -16,7 +17,7 @@ import {
const navigation = [
{ name: 'Dashboard', href: '/', icon: HomeIcon },
{ name: 'Çalışanlar', href: '/employees', icon: UsersIcon },
{ name: 'Personeller', href: '/employees', icon: UsersIcon },
{ name: 'İzinler', href: '/leave-requests', icon: CalendarDaysIcon },
{ name: 'Şirketler', href: '/companies', icon: BuildingOfficeIcon },
{ name: 'Ayarlar', href: '/settings', icon: Cog6ToothIcon },
@@ -29,16 +30,18 @@ export function Sidebar() {
return (
<>
{/* Mobile sidebar trigger */}
<div className="lg:hidden fixed top-0 left-0 w-full h-16 bg-white dark:bg-zinc-900 border-b border-gray-200 dark:border-zinc-800 flex items-center px-4 z-40">
<div className="lg:hidden fixed top-0 left-0 w-full h-16 bg-white dark:bg-zinc-900 border-b border-slate-200 dark:border-zinc-800 flex items-center px-4 z-40">
<button
className="p-2 text-gray-500 hover:text-gray-900 dark:hover:text-white focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500 rounded-md"
className="p-2 text-slate-500 hover:text-slate-900 dark:hover:text-white focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 rounded-lg"
onClick={() => setSidebarOpen(true)}
>
<span className="sr-only">Menüyü </span>
<Bars3Icon className="h-6 w-6" aria-hidden="true" />
</button>
<div className="ml-4 font-bold text-xl dark:text-white text-gray-900 tracking-tight">
HRMS
<div className="ml-4 flex items-center gap-3 group">
<Image src="/logo.png" alt="Abisena Logo" width={120} height={40} className="h-8 w-auto object-contain" />
<div className="h-4 w-px bg-gray-300 dark:bg-zinc-700 mx-1" />
<span className="text-[10px] font-bold text-gray-500 dark:text-zinc-400 uppercase tracking-tighter leading-tight">Personel<br/>Sistemi</span>
</div>
</div>
@@ -59,11 +62,11 @@ export function Sidebar() {
</button>
</div>
{/* Mobile Sidebar Content */}
<div className="flex grow flex-col gap-y-5 overflow-y-auto bg-white dark:bg-zinc-950 px-6 pb-4">
<div className="flex h-16 shrink-0 items-center">
<div className="font-bold text-2xl dark:text-white text-gray-900 tracking-tight flex items-center gap-2">
<BuildingOfficeIcon className="w-8 h-8 text-blue-600" />
HRMS
<div className="flex grow flex-col gap-y-5 overflow-y-auto bg-[#173363] px-6 pb-4 shadow-xl">
<div className="flex h-20 shrink-0 items-center border-b border-gray-100 dark:border-zinc-900">
<div className="flex flex-col gap-1">
<Image src="/logo.png" alt="Abisena Logo" width={140} height={45} className="h-10 w-auto object-contain" />
<span className="text-[10px] font-bold text-gray-400 dark:text-zinc-500 uppercase tracking-[0.2em] ml-1">TAKİP SİSTEMİ</span>
</div>
</div>
<nav className="flex flex-1 flex-col">
@@ -78,13 +81,13 @@ export function Sidebar() {
className={`
group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold
${pathname === item.href
? 'bg-gray-50 dark:bg-zinc-900 text-blue-600 dark:text-blue-400'
: 'text-gray-700 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-gray-50 dark:hover:bg-zinc-900/50'
? 'bg-indigo-50 dark:bg-indigo-500/10 text-indigo-600 dark:text-indigo-400'
: 'text-slate-600 dark:text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 hover:bg-slate-50 dark:hover:bg-zinc-800/50'
}
`}
>
<item.icon
className={`h-6 w-6 shrink-0 ${pathname === item.href ? 'text-blue-600 dark:text-blue-400' : 'text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400'}`}
className={`h-6 w-6 shrink-0 ${pathname === item.href ? 'text-indigo-600 dark:text-indigo-400' : 'text-slate-400 group-hover:text-indigo-600 dark:group-hover:text-indigo-400'}`}
aria-hidden="true"
/>
{item.name}
@@ -103,19 +106,24 @@ export function Sidebar() {
{/* Desktop Sidebar */}
<div className="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-72 lg:flex-col">
<div className="flex grow flex-col gap-y-5 overflow-y-auto border-r border-gray-200/50 dark:border-zinc-800/50 bg-white/80 dark:bg-zinc-950/80 backdrop-blur-xl px-6 pb-4">
<div className="flex h-16 shrink-0 items-center mt-4">
<div className="font-bold text-2xl dark:text-white text-gray-900 tracking-tight flex items-center gap-2 group cursor-pointer">
<div className="p-2 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-lg shadow-lg shadow-blue-500/20 group-hover:scale-110 transition-transform duration-300">
<BuildingOfficeIcon className="w-6 h-6 text-white" />
<div className="flex grow flex-col gap-y-5 overflow-y-auto bg-[#173363] px-6 pb-4 shadow-xl">
<div className="flex h-24 shrink-0 items-center mt-6 mb-2 border-b border-gray-100/50 dark:border-zinc-900/50">
<div className="flex flex-col items-start gap-2 group cursor-pointer w-full">
<div className="p-2 transition-all duration-300 group-hover:brightness-110">
<Image src="/logo.png" alt="Abisena Logo" width={180} height={60} className="h-12 w-auto object-contain" priority />
</div>
<div className="px-2 w-full">
<div className="h-px w-full bg-gradient-to-r from-gray-200 via-gray-100 to-transparent dark:from-zinc-800 dark:via-zinc-900 dark:to-transparent mb-2" />
<span className="text-[10px] font-black text-gray-400 dark:text-zinc-500 tracking-[0.3em] uppercase ml-1 block">Personel Takip Sistemi</span>
</div>
<span className="bg-clip-text text-transparent bg-gradient-to-r from-gray-900 via-gray-700 to-gray-900 dark:from-white dark:via-gray-300 dark:to-white">HRMS</span>
</div>
</div>
<nav className="flex flex-1 flex-col">
<ul role="list" className="flex flex-1 flex-col gap-y-7">
<li>
<div className="text-[10px] font-bold tracking-widest leading-6 text-gray-400 dark:text-zinc-500 mb-4 uppercase">YÖNETİM PANELİ</div>
<div className="text-[10px] font-bold tracking-widest leading-6 text-slate-400 mb-4 uppercase">YÖNETİM PANELİ</div>
<ul role="list" className="-mx-2 space-y-2">
{navigation.map((item) => (
<li key={item.name}>
@@ -124,18 +132,18 @@ export function Sidebar() {
className={`
group flex items-center gap-x-3 rounded-xl p-3 text-sm leading-6 font-semibold transition-all duration-300 relative overflow-hidden
${pathname === item.href
? 'bg-blue-600 shadow-md shadow-blue-600/20 text-white translate-x-1'
: 'text-gray-600 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-500/5'
? 'bg-[#CE0515] text-white shadow-lg shadow-red-900/20'
: 'text-slate-300 hover:text-white hover:bg-white/5'
}
`}
>
<item.icon
className={`h-5 w-5 shrink-0 transition-colors duration-300 ${pathname === item.href ? 'text-white' : 'text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400'}`}
className={`h-5 w-5 shrink-0 transition-colors duration-200 ${pathname === item.href ? 'text-white' : 'text-slate-400 group-hover:text-white'}`}
aria-hidden="true"
/>
{item.name}
{pathname === item.href && (
<div className="absolute left-0 w-1 h-6 bg-white rounded-full opacity-50" />
<div className="ml-auto w-1.5 h-1.5 rounded-full bg-white animate-pulse" />
)}
</Link>
</li>
@@ -146,10 +154,10 @@ export function Sidebar() {
<form action="/auth/signout" method="post" className="-mx-2 pb-4">
<button
type="submit"
className="group flex w-full items-center gap-x-3 rounded-xl p-3 text-sm font-semibold leading-6 text-gray-500 hover:text-red-600 dark:text-zinc-400 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-red-500/5 transition-all duration-300"
className="group flex w-full items-center gap-x-3 rounded-2xl p-3 text-sm font-bold leading-6 text-slate-500 hover:text-rose-600 dark:text-zinc-400 dark:hover:text-rose-400 hover:bg-rose-50 dark:hover:bg-rose-500/5 transition-all duration-300 active:scale-95"
>
<ArrowRightOnRectangleIcon
className="h-5 w-5 shrink-0 text-gray-400 group-hover:text-red-500 transition-colors duration-300"
className="h-5 w-5 shrink-0 text-slate-400 group-hover:text-rose-500 transition-colors duration-300"
aria-hidden="true"
/>
Güvenli Çıkış

View File

@@ -0,0 +1,19 @@
-- Create a trigger function to handle new user registration
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO public.users (id, email, first_name, last_name)
VALUES (
NEW.id,
NEW.email,
NEW.raw_user_meta_data->>'first_name',
NEW.raw_user_meta_data->>'last_name'
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Trigger the function every time a user is created
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW EXECUTE PROCEDURE public.handle_new_user();

View File

@@ -0,0 +1,40 @@
-- Create leave_balances table
CREATE TABLE public.leave_balances (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
employee_id UUID REFERENCES public.employees(id) ON DELETE CASCADE UNIQUE NOT NULL,
total_days DECIMAL(5,2) DEFAULT 0 NOT NULL, -- Total accrued days
used_days DECIMAL(5,2) DEFAULT 0 NOT NULL, -- Successfully used days
pending_days DECIMAL(5,2) DEFAULT 0 NOT NULL, -- Days currently in pending status
remaining_days DECIMAL(5,2) GENERATED ALWAYS AS (total_days - used_days) STORED,
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
);
-- RLS for leave_balances
ALTER TABLE public.leave_balances ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view their own leave balance"
ON public.leave_balances
FOR SELECT TO authenticated
USING (
employee_id IN (
SELECT id FROM public.employees WHERE user_id = auth.uid()
)
);
-- Trigger to create a balance record when a new employee is added
CREATE OR REPLACE FUNCTION public.handle_new_employee_balance()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO public.leave_balances (employee_id, total_days)
VALUES (NEW.id, 14); -- Defaulting to 14 days per year
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER on_employee_created
AFTER INSERT ON public.employees
FOR EACH ROW EXECUTE PROCEDURE public.handle_new_employee_balance();
-- Trigger updated_at for leave_balances
CREATE TRIGGER update_leave_balances_modtime BEFORE UPDATE ON public.leave_balances FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column();

View File

@@ -0,0 +1,41 @@
-- Drop loose policies
DROP POLICY IF EXISTS "Allow authenticated full access to companies" ON public.companies;
DROP POLICY IF EXISTS "Allow authenticated full access to employees" ON public.employees;
-- Tighten Companies RLS (Only admins or users belonging to the company)
CREATE POLICY "Users can view their own company"
ON public.companies
FOR SELECT TO authenticated
USING (
id IN (
SELECT company_id FROM public.employees WHERE user_id = auth.uid()
)
);
CREATE POLICY "Personal employee record view"
ON public.employees
FOR SELECT TO authenticated
USING (user_id = auth.uid());
CREATE POLICY "Employees can view colleagues in their company"
ON public.employees
FOR SELECT TO authenticated
USING (
company_id IN (
SELECT company_id FROM public.employees WHERE user_id = auth.uid()
)
);
CREATE POLICY "Managers can manage employees in their company"
ON public.employees
FOR ALL TO authenticated
USING (
company_id IN (
SELECT company_id FROM public.employees WHERE user_id = auth.uid()
)
)
WITH CHECK (
company_id IN (
SELECT company_id FROM public.employees WHERE user_id = auth.uid()
)
);

View File

@@ -0,0 +1,76 @@
-- 1. Create SECURITY DEFINER functions to bypass RLS recursion
-- We need these functions to check roles and membership without recursing into the same table's RLS policy.
-- Function to check if a user is an admin
CREATE OR REPLACE FUNCTION public.is_admin()
RETURNS boolean AS $$
BEGIN
RETURN EXISTS (
SELECT 1 FROM public.employees e
INNER JOIN public.roles r ON e.role_id = r.id
WHERE e.user_id = auth.uid()
AND r.name = 'admin'
);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Function to get the current user's company IDs
CREATE OR REPLACE FUNCTION public.get_my_companies()
RETURNS SETOF uuid AS $$
BEGIN
RETURN QUERY SELECT company_id FROM public.employees WHERE user_id = auth.uid();
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- 2. Clean up old recursive policies
DROP POLICY IF EXISTS "Users can view their own company" ON public.companies;
DROP POLICY IF EXISTS "Personal employee record view" ON public.employees;
DROP POLICY IF EXISTS "Employees can view colleagues in their company" ON public.employees;
DROP POLICY IF EXISTS "Managers can manage employees in their company" ON public.employees;
-- 3. Create new non-recursive policies
-- Companies: Users can see companies they belong to
CREATE POLICY "View belonging companies"
ON public.companies
FOR SELECT TO authenticated
USING (
id IN (SELECT public.get_my_companies())
OR public.is_admin()
);
-- Employees: Everyone can see their own record
CREATE POLICY "View own employee record"
ON public.employees
FOR SELECT TO authenticated
USING (user_id = auth.uid());
-- Employees: Admins can see everyone in the companies they belong to
CREATE POLICY "Admins can see coworkers"
ON public.employees
FOR SELECT TO authenticated
USING (
company_id IN (SELECT public.get_my_companies())
AND public.is_admin()
);
-- Employees: Admins can manage coworkers
CREATE POLICY "Admins can manage coworkers"
ON public.employees
FOR ALL TO authenticated
USING (
company_id IN (SELECT public.get_my_companies())
AND public.is_admin()
)
WITH CHECK (
company_id IN (SELECT public.get_my_companies())
AND public.is_admin()
);
-- Users: Ensure everyone can see their own profile at least
CREATE POLICY "Users can view own profile"
ON public.users
FOR SELECT TO authenticated
USING (auth.uid() = id);
-- If "Users can view all users" already exists, it's fine.