Personel Sayfası ve Uygulama renk değişiklikleri
This commit is contained in:
BIN
public/favicon.png
Normal file
BIN
public/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
public/logo.png
Normal file
BIN
public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
@@ -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>
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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 |
@@ -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<{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
113
src/app/page.tsx
113
src/app/page.tsx
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
175
src/components/employees/EmployeeModal.tsx
Normal file
175
src/components/employees/EmployeeModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
211
src/components/employees/EmployeeTable.tsx
Normal file
211
src/components/employees/EmployeeTable.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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ü Aç</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ış
|
||||
|
||||
19
supabase/migrations/20240317000001_user_sync_trigger.sql
Normal file
19
supabase/migrations/20240317000001_user_sync_trigger.sql
Normal 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();
|
||||
40
supabase/migrations/20240317000002_leave_balances.sql
Normal file
40
supabase/migrations/20240317000002_leave_balances.sql
Normal 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();
|
||||
41
supabase/migrations/20240317000003_tighten_rls.sql
Normal file
41
supabase/migrations/20240317000003_tighten_rls.sql
Normal 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()
|
||||
)
|
||||
);
|
||||
76
supabase/migrations/20240317000004_fix_rls_recursion.sql
Normal file
76
supabase/migrations/20240317000004_fix_rls_recursion.sql
Normal 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.
|
||||
Reference in New Issue
Block a user