new özellik

This commit is contained in:
2026-03-18 12:38:50 +03:00
parent b354412cb8
commit 600dce8052
7 changed files with 875 additions and 218 deletions
+134 -28
View File
@@ -1,6 +1,7 @@
'use server' 'use server'
import { createClient } from '@/utils/supabase/server' import { createClient } from '@/utils/supabase/server'
import { createAdminClient } from '@/utils/supabase/admin'
import { revalidatePath } from 'next/cache' import { revalidatePath } from 'next/cache'
export async function addEmployee(formData: FormData) { export async function addEmployee(formData: FormData) {
@@ -12,45 +13,62 @@ export async function addEmployee(formData: FormData) {
const email = formData.get('email') as string const email = formData.get('email') as string
const companyId = formData.get('company_id') as string const companyId = formData.get('company_id') as string
const roleId = formData.get('role_id') as string const roleId = formData.get('role_id') as string
const photo = formData.get('photo') as File
if (!email || !companyId || !roleId) { if (!firstName || !lastName || !companyId || !roleId) {
return { error: 'E-posta, Şirket ve Rol seçimi zorunludur.' } return { error: 'Ad, Soyad, Şirket ve Rol seçimi zorunludur.' }
} }
// Admin creating users manually currently requires an admin API setup or the user registering themselves // 2. Handle Photo Upload
// In a robust HRMS, the Superadmin uses the Supabase Admin API to `createUser`. let photoUrl = null
// For the sake of this prototype, we'll assume the user must register first via valid credentials, if (photo && photo.size > 0) {
// or we can insert an unauthenticated ghost user into `public.users` (which breaks reference to auth.users if not careful). const fileExt = photo.name.split('.').pop()
// const fileName = `${Math.random()}.${fileExt}`
// Let's implement the standard approach: we check if the user exists in `auth.users` via `public.users` const filePath = `${fileName}`
let { data: existingUser } = await supabase
.from('users')
.select('id')
.eq('email', email)
.single()
if (!existingUser) { const { error: uploadError, data } = await supabase.storage
return { error: 'Bu e-posta adresine sahip bir kullanıcı bulunamadı. Kullanıcının önce sisteme kayıt olması gerekmektedir.' } .from('employee-photos')
.upload(filePath, photo)
if (uploadError) {
console.error('Photo upload error:', uploadError)
} else {
const { data: { publicUrl } } = supabase.storage
.from('employee-photos')
.getPublicUrl(filePath)
photoUrl = publicUrl
}
} }
// 2. Insert into Employees table // 3. Insert into Employees table
const { error: employeeError } = await supabase const { error: employeeError } = await supabase
.from('employees') .from('employees')
.insert([{ .insert([{
user_id: existingUser.id, first_name: firstName,
last_name: lastName,
email: email || null,
company_id: companyId, company_id: companyId,
role_id: roleId, role_id: roleId,
department: formData.get('department'), photo_url: photoUrl,
title: formData.get('title'), department_id: formData.get('department_id') || null,
status: formData.get('status') || 'active' section_id: formData.get('section_id') || null,
employment_type_id: formData.get('employment_type_id') || null,
job_title_id: formData.get('job_title_id') || null,
tc_no: formData.get('tc_no'),
birth_date: formData.get('birth_date') || null,
birth_place: formData.get('birth_place'),
gender: formData.get('gender'),
address: formData.get('address'),
phone1: formData.get('phone1'),
phone2: formData.get('phone2'),
has_driving_license: formData.get('has_driving_license') === 'on',
military_status: formData.get('military_status'),
start_date: formData.get('start_date') || null,
leave_date: formData.get('leave_date') || null,
status: 'active'
}]) }])
if (employeeError) { if (employeeError) {
// Catch unique constraint violation
if (employeeError.code === '23505') {
return { error: 'Bu kullanıcı zaten bu şirkete eklenmiş.' }
}
return { error: 'Personel eklenirken hata: ' + employeeError.message } return { error: 'Personel eklenirken hata: ' + employeeError.message }
} }
@@ -76,15 +94,52 @@ export async function deleteEmployee(id: string) {
export async function updateEmployee(id: string, formData: FormData) { export async function updateEmployee(id: string, formData: FormData) {
const supabase = await createClient() const supabase = await createClient()
const photo = formData.get('photo') as File
// 1. Handle Photo Upload if new photo provided
let photoUrl = formData.get('existing_photo_url') as string
if (photo && photo.size > 0) {
const fileExt = photo.name.split('.').pop()
const fileName = `${Math.random()}.${fileExt}`
const filePath = `${fileName}`
const { error: uploadError } = await supabase.storage
.from('employee-photos')
.upload(filePath, photo)
if (!uploadError) {
const { data: { publicUrl } } = supabase.storage
.from('employee-photos')
.getPublicUrl(filePath)
photoUrl = publicUrl
}
}
const { error } = await supabase const { error } = await supabase
.from('employees') .from('employees')
.update({ .update({
first_name: formData.get('first_name'),
last_name: formData.get('last_name'),
email: formData.get('email'),
company_id: formData.get('company_id'), company_id: formData.get('company_id'),
role_id: formData.get('role_id'), role_id: formData.get('role_id'),
department: formData.get('department'), photo_url: photoUrl,
title: formData.get('title'), department_id: formData.get('department_id') || null,
status: formData.get('status') section_id: formData.get('section_id') || null,
employment_type_id: formData.get('employment_type_id') || null,
job_title_id: formData.get('job_title_id') || null,
tc_no: formData.get('tc_no'),
birth_date: formData.get('birth_date') || null,
birth_place: formData.get('birth_place'),
gender: formData.get('gender'),
address: formData.get('address'),
phone1: formData.get('phone1'),
phone2: formData.get('phone2'),
has_driving_license: formData.get('has_driving_license') === 'on',
military_status: formData.get('military_status'),
start_date: formData.get('start_date') || null,
leave_date: formData.get('leave_date') || null,
status: formData.get('leave_date') ? 'inactive' : formData.get('status')
}) })
.eq('id', id) .eq('id', id)
@@ -95,3 +150,54 @@ export async function updateEmployee(id: string, formData: FormData) {
revalidatePath('/employees') revalidatePath('/employees')
return { success: true } return { success: true }
} }
export async function createUserForEmployee(formData: FormData) {
const supabaseAdmin = createAdminClient()
const supabase = await createClient()
const employeeId = formData.get('employee_id') as string
const password = formData.get('password') as string
if (!employeeId || !password) {
return { error: 'Personel ve şifre zorunludur.' }
}
// 1. Get employee email
const { data: employee, error: fetchError } = await supabase
.from('employees')
.select('email, first_name, last_name')
.eq('id', employeeId)
.single()
if (fetchError || !employee || !employee.email) {
return { error: 'Personel bilgisi veya e-posta adresi bulunamadı.' }
}
// 2. Create user in Auth
const { data: authData, error: authError } = await supabaseAdmin.auth.admin.createUser({
email: employee.email,
password: password,
email_confirm: true,
user_metadata: {
first_name: employee.first_name,
last_name: employee.last_name
}
})
if (authError) {
return { error: 'Kullanıcı oluşturulurken hata (Auth): ' + authError.message }
}
// 3. Link Auth User to Employee
const { error: updateError } = await supabase
.from('employees')
.update({ user_id: authData.user.id })
.eq('id', employeeId)
if (updateError) {
return { error: 'Kullanıcı personele bağlanırken hata: ' + updateError.message }
}
revalidatePath('/employees')
return { success: true }
}
+17 -20
View File
@@ -8,31 +8,24 @@ export default async function EmployeesPage() {
const { data: employees } = await supabase const { data: employees } = await supabase
.from('employees') .from('employees')
.select(` .select(`
id, *,
user_id,
company_id,
role_id,
department,
title,
status,
created_at,
companies ( id, name ), companies ( id, name ),
users ( id, first_name, last_name, email ), users ( id, first_name, last_name, email ),
roles ( id, name, description ) roles ( id, name, description ),
departments ( id, name ),
sections ( id, name ),
employment_types ( id, name ),
job_titles ( id, name )
`) `)
.order('created_at', { ascending: false }) .order('created_at', { ascending: false })
// Fetch companies for the modal // Fetch lookups
const { data: companies } = await supabase const { data: companies } = await supabase.from('companies').select('id, name').order('name')
.from('companies') const { data: roles } = await supabase.from('roles').select('id, name, description').order('name')
.select('id, name') const { data: departments } = await supabase.from('departments').select('id, name').order('name')
.order('name') const { data: sections } = await supabase.from('sections').select('id, name, department_id').order('name')
const { data: employmentTypes } = await supabase.from('employment_types').select('id, name').order('name')
// Fetch roles for the modal const { data: jobTitles } = await supabase.from('job_titles').select('id, name').order('name')
const { data: roles } = await supabase
.from('roles')
.select('id, name, description')
.order('name')
return ( return (
<div className="p-4 sm:p-6 lg:p-8 max-w-7xl mx-auto"> <div className="p-4 sm:p-6 lg:p-8 max-w-7xl mx-auto">
@@ -40,6 +33,10 @@ export default async function EmployeesPage() {
initialEmployees={employees || []} initialEmployees={employees || []}
companies={companies || []} companies={companies || []}
roles={roles || []} roles={roles || []}
departments={departments || []}
sections={sections || []}
employmentTypes={employmentTypes || []}
jobTitles={jobTitles || []}
/> />
</div> </div>
) )
+229 -82
View File
@@ -1,7 +1,7 @@
'use client'; 'use client';
import { useState, useEffect } from 'react'; import { useState, useEffect, useRef } from 'react';
import { XMarkIcon } from '@heroicons/react/24/outline'; import { XMarkIcon, PhotoIcon } from '@heroicons/react/24/outline';
import { addEmployee, updateEmployee } from '@/app/employees/actions'; import { addEmployee, updateEmployee } from '@/app/employees/actions';
interface EmployeeModalProps { interface EmployeeModalProps {
@@ -9,15 +9,54 @@ interface EmployeeModalProps {
onClose: () => void; onClose: () => void;
companies: any[]; companies: any[];
roles: any[]; roles: any[];
departments: any[];
sections: any[];
employmentTypes: any[];
jobTitles: any[];
editingEmployee?: any; editingEmployee?: any;
} }
export default function EmployeeModal({ isOpen, onClose, companies, roles, editingEmployee }: EmployeeModalProps) { export default function EmployeeModal({
isOpen,
onClose,
companies,
roles,
departments,
sections,
employmentTypes,
jobTitles,
editingEmployee
}: EmployeeModalProps) {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [activeTab, setActiveTab] = useState('personal');
const [selectedDept, setSelectedDept] = useState(editingEmployee?.department_id || '');
const [previewUrl, setPreviewUrl] = useState<string | null>(editingEmployee?.photo_url || null);
const fileInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (editingEmployee) {
setSelectedDept(editingEmployee.department_id || '');
setPreviewUrl(editingEmployee.photo_url || null);
} else {
setSelectedDept('');
setPreviewUrl(null);
}
setActiveTab('personal');
}, [editingEmployee, isOpen]);
if (!isOpen) return null; if (!isOpen) return null;
const filteredSections = sections.filter(s => s.department_id === selectedDept);
const handlePhotoChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const url = URL.createObjectURL(file);
setPreviewUrl(url);
}
};
async function handleSubmit(formData: FormData) { async function handleSubmit(formData: FormData) {
setLoading(true); setLoading(true);
setError(null); setError(null);
@@ -42,130 +81,238 @@ export default function EmployeeModal({ isOpen, onClose, companies, roles, editi
} }
} }
const inputClass = "w-full rounded-2xl border-0 py-3 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-[#173363] outline-none transition-all placeholder:text-slate-400 text-sm";
const labelClass = "block text-[10px] font-black uppercase tracking-widest text-slate-400 mb-1.5 ml-1";
return ( 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="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-900/60 backdrop-blur-md 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"> <div className="bg-white dark:bg-zinc-900 w-full max-w-4xl max-h-[90vh] rounded-[2.5rem] shadow-2xl border border-slate-100 dark:border-zinc-800 overflow-hidden flex flex-col animate-in fade-in zoom-in duration-300">
{/* Header */} {/* 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 className="px-10 py-8 border-b border-slate-50 dark:border-zinc-800 flex items-center justify-between bg-white dark:bg-zinc-900">
<div> <div>
<h2 className="text-xl font-bold text-slate-900 dark:text-white"> <h2 className="text-2xl font-black text-[#173363] dark:text-white tracking-tight">
{editingEmployee ? 'Personel Düzenle' : 'Yeni Personel Ekle'} {editingEmployee ? 'Personel Düzenle' : 'Yeni Personel Ekle'}
</h2> </h2>
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mt-1"> <p className="text-xs font-bold text-slate-400 mt-1">
Personel bilgilerini eksiksiz doldurunuz. Sistem üzerindeki personel bilgilerini detaylı olarak yönetin.
</p> </p>
</div> </div>
<button <button
onClick={onClose} 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" className="p-3 rounded-2xl hover:bg-slate-50 dark:hover:bg-zinc-800 text-slate-300 hover:text-[#CE0515] transition-all"
> >
<XMarkIcon className="w-6 h-6" /> <XMarkIcon className="w-6 h-6" />
</button> </button>
</div> </div>
{/* Tabs */}
<div className="px-10 flex gap-8 border-b border-slate-50 dark:border-zinc-800 bg-white dark:bg-zinc-900">
{[
{ id: 'personal', label: 'KİŞİSEL BİLGİLER' },
{ id: 'employment', label: 'ÇALIŞMA BİLGİLERİ' },
{ id: 'contact', label: 'İLETİŞİM / DİĞER' }
].map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`py-6 text-[11px] font-black tracking-[0.2em] transition-all border-b-2 ${
activeTab === tab.id
? 'border-[#CE0515] text-[#CE0515]'
: 'border-transparent text-slate-400 hover:text-slate-600'
}`}
>
{tab.label}
</button>
))}
</div>
{/* Body */} {/* Body */}
<form action={handleSubmit} className="p-8 space-y-6"> <form action={handleSubmit} className="flex-1 overflow-y-auto p-10 custom-scrollbar">
{error && ( {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"> <div className="mb-8 p-4 bg-rose-50 border border-rose-100 rounded-2xl text-rose-600 text-xs font-black text-center uppercase tracking-widest">
{error} {error}
</div> </div>
)} )}
<div className="grid grid-cols-1 gap-6"> {activeTab === 'personal' && (
{!editingEmployee && ( <div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="space-y-2"> <div className="flex flex-col md:flex-row gap-10">
<label className="block text-sm font-bold text-slate-700 dark:text-slate-300 ml-1">E-posta Adresi</label> {/* Photo Upload */}
<input <div className="flex-shrink-0 flex flex-col items-center gap-4">
type="email" <div
name="email" onClick={() => fileInputRef.current?.click()}
required className="w-40 h-40 rounded-[2.5rem] bg-slate-50 border-2 border-dashed border-slate-200 flex flex-col items-center justify-center cursor-pointer hover:border-[#173363] hover:bg-slate-100 transition-all overflow-hidden group"
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" {previewUrl ? (
/> <img src={previewUrl} alt="Preview" className="w-full h-full object-cover" />
</div> ) : (
)} <>
<PhotoIcon className="w-10 h-10 text-slate-300 group-hover:text-[#173363] transition-colors" />
<span className="text-[10px] font-black text-slate-400 mt-2 px-4 text-center">FOTOĞRAF YÜKLE</span>
</>
)}
</div>
<input
type="file"
name="photo"
ref={fileInputRef}
onChange={handlePhotoChange}
className="hidden"
accept="image/*"
/>
<input type="hidden" name="existing_photo_url" value={editingEmployee?.photo_url || ''} />
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="flex-1 grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2"> <div>
<label className="block text-sm font-bold text-slate-700 dark:text-slate-300 ml-1">Şirket</label> <label className={labelClass}>AD</label>
<select <input type="text" name="first_name" defaultValue={editingEmployee?.first_name} required className={inputClass} />
name="company_id" </div>
required <div>
defaultValue={editingEmployee?.company_id || ''} <label className={labelClass}>SOYAD</label>
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" <input type="text" name="last_name" defaultValue={editingEmployee?.last_name} required className={inputClass} />
> </div>
<div>
<label className={labelClass}>T.C. KİMLİK NO</label>
<input type="text" name="tc_no" defaultValue={editingEmployee?.tc_no} className={inputClass} maxLength={11} />
</div>
<div>
<label className={labelClass}>CİNSİYET</label>
<select name="gender" defaultValue={editingEmployee?.gender} className={inputClass}>
<option value="">Seçiniz</option>
<option value="Erkek">Erkek</option>
<option value="Kadın">Kadın</option>
</select>
</div>
<div>
<label className={labelClass}>DOĞUM TARİHİ</label>
<input type="date" name="birth_date" defaultValue={editingEmployee?.birth_date} className={inputClass} />
</div>
<div>
<label className={labelClass}>DOĞUM YERİ</label>
<input type="text" name="birth_place" defaultValue={editingEmployee?.birth_place} className={inputClass} />
</div>
</div>
</div>
</div>
)}
{activeTab === 'employment' && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div>
<label className={labelClass}>ŞİRKET</label>
<select name="company_id" defaultValue={editingEmployee?.company_id} required className={inputClass}>
<option value="">Seçiniz</option> <option value="">Seçiniz</option>
{companies.map(c => <option key={c.id} value={c.id}>{c.name}</option>)} {companies.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
</select> </select>
</div> </div>
<div>
<div className="space-y-2"> <label className={labelClass}>SİSTEM ROLÜ</label>
<label className="block text-sm font-bold text-slate-700 dark:text-slate-300 ml-1">Rol</label> <select name="role_id" defaultValue={editingEmployee?.role_id} required className={inputClass}>
<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> <option value="">Seçiniz</option>
{roles.map(r => <option key={r.id} value={r.id}>{r.description}</option>)} {roles.map(r => <option key={r.id} value={r.id}>{r.description}</option>)}
</select> </select>
</div> </div>
</div> <div>
<label className={labelClass}>DEPARTMAN</label>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <select
<div className="space-y-2"> name="department_id"
<label className="block text-sm font-bold text-slate-700 dark:text-slate-300 ml-1">Departman</label> value={selectedDept}
<input onChange={(e) => setSelectedDept(e.target.value)}
type="text" className={inputClass}
name="department" >
defaultValue={editingEmployee?.department || ''} <option value="">Seçiniz</option>
placeholder="Örn: Yazılım" {departments.map(d => <option key={d.id} value={d.id}>{d.name}</option>)}
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" </select>
/>
</div> </div>
<div>
<div className="space-y-2"> <label className={labelClass}>BÖLÜM</label>
<label className="block text-sm font-bold text-slate-700 dark:text-slate-300 ml-1">Ünvan</label> <select name="section_id" defaultValue={editingEmployee?.section_id} className={inputClass}>
<input <option value="">Seçiniz</option>
type="text" {filteredSections.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
name="title" </select>
defaultValue={editingEmployee?.title || ''} </div>
placeholder="Örn: Kıdemli Uzman" <div>
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" <label className={labelClass}>GÖREV / ÜNVAN</label>
/> <select name="job_title_id" defaultValue={editingEmployee?.job_title_id} className={inputClass}>
<option value="">Seçiniz</option>
{jobTitles.map(jt => <option key={jt.id} value={jt.id}>{jt.name}</option>)}
</select>
</div>
<div>
<label className={labelClass}>ÇALIŞMA ŞEKLİ</label>
<select name="employment_type_id" defaultValue={editingEmployee?.employment_type_id} className={inputClass}>
<option value="">Seçiniz</option>
{employmentTypes.map(et => <option key={et.id} value={et.id}>{et.name}</option>)}
</select>
</div>
<div>
<label className={labelClass}>İŞE BAŞLAMA TARİHİ</label>
<input type="date" name="start_date" defaultValue={editingEmployee?.start_date} className={inputClass} />
</div>
<div>
<label className={labelClass}>İŞTEN AYRILIŞ TARİHİ</label>
<input type="date" name="leave_date" defaultValue={editingEmployee?.leave_date} className={inputClass} />
</div> </div>
</div> </div>
)}
<div className="space-y-2"> {activeTab === 'contact' && (
<label className="block text-sm font-bold text-slate-700 dark:text-slate-300 ml-1">Durum</label> <div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
<select <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
name="status" <div>
defaultValue={editingEmployee?.status || 'active'} <label className={labelClass}>E-POSTA</label>
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" <input type="email" name="email" defaultValue={editingEmployee?.email} className={inputClass} placeholder="ornek@abisena.com" />
> </div>
<option value="active">Aktif</option> <div>
<option value="inactive">Pasif</option> <label className={labelClass}>TELEFON 1</label>
<option value="terminated">İlişiği Kesildi</option> <input type="tel" name="phone1" defaultValue={editingEmployee?.phone1} className={inputClass} placeholder="05XX XXX XX XX" />
</select> </div>
<div>
<label className={labelClass}>TELEFON 2</label>
<input type="tel" name="phone2" defaultValue={editingEmployee?.phone2} className={inputClass} placeholder="05XX XXX XX XX" />
</div>
<div>
<label className={labelClass}>ASKERLİK DURUMU</label>
<select name="military_status" defaultValue={editingEmployee?.military_status} className={inputClass}>
<option value="">Seçiniz</option>
<option value="Yapıldı">Yapıldı</option>
<option value="Muaf">Muaf</option>
<option value="Tecilli">Tecilli</option>
</select>
</div>
</div>
<div>
<label className={labelClass}>ADRES</label>
<textarea name="address" defaultValue={editingEmployee?.address} rows={3} className={inputClass + " resize-none"} />
</div>
<div className="flex items-center gap-4 bg-slate-50 p-6 rounded-2xl border border-slate-200">
<input
type="checkbox"
name="has_driving_license"
defaultChecked={editingEmployee?.has_driving_license}
className="w-5 h-5 rounded-lg border-slate-300 text-[#173363] focus:ring-[#173363]"
/>
<label className="text-sm font-bold text-[#173363]">SÜRÜCÜ EHLİYETİ VAR</label>
</div>
</div> </div>
</div> )}
<div className="flex gap-4 pt-6"> <div className="mt-12 flex gap-4">
<button <button
type="button" type="button"
onClick={onClose} 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" className="flex-1 px-8 py-5 rounded-full font-black text-slate-300 hover:bg-slate-50 transition-all text-[11px] uppercase tracking-[0.2em]"
> >
İptal Vazgeç
</button> </button>
<button <button
type="submit" type="submit"
disabled={loading} 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]" className="flex-[2] bg-[#173363] hover:bg-[#CE0515] text-white px-10 py-5 rounded-full font-black shadow-xl shadow-blue-900/10 transition-all active:scale-95 disabled:opacity-50 text-[11px] uppercase tracking-[0.2em] transform"
> >
{loading ? 'İşleniyor...' : (editingEmployee ? 'GÜNCELLE' : 'PERSONEL OLUŞTUR')} {loading ? 'YÜKLENİYOR...' : (editingEmployee ? 'BİLGİLERİ GÜNCELLE' : 'PERSONELİ KAYDET')}
</button> </button>
</div> </div>
</form> </form>
+206 -88
View File
@@ -1,39 +1,68 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { import {
UsersIcon, UsersIcon,
TrashIcon, TrashIcon,
PencilSquareIcon, PencilSquareIcon,
PlusIcon, PlusIcon,
MagnifyingGlassIcon MagnifyingGlassIcon,
UserPlusIcon,
KeyIcon,
PhoneIcon,
EnvelopeIcon,
CalendarDaysIcon,
IdentificationIcon,
BuildingOffice2Icon
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
import { deleteEmployee } from '@/app/employees/actions'; import { deleteEmployee } from '@/app/employees/actions';
import EmployeeModal from '@/components/employees/EmployeeModal'; import EmployeeModal from '@/components/employees/EmployeeModal';
import UserCreationModal from '@/components/employees/UserCreationModal';
interface EmployeeTableProps { interface EmployeeTableProps {
initialEmployees: any[]; initialEmployees: any[];
companies: any[]; companies: any[];
roles: any[]; roles: any[];
departments: any[];
sections: any[];
employmentTypes: any[];
jobTitles: any[];
} }
export default function EmployeeTable({ initialEmployees, companies, roles }: EmployeeTableProps) { export default function EmployeeTable({
initialEmployees,
companies,
roles,
departments,
sections,
employmentTypes,
jobTitles
}: EmployeeTableProps) {
const [employees, setEmployees] = useState(initialEmployees); const [employees, setEmployees] = useState(initialEmployees);
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('all'); const [statusFilter, setStatusFilter] = useState('all');
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [isUserModalOpen, setIsUserModalOpen] = useState(false);
const [editingEmployee, setEditingEmployee] = useState<any>(null); const [editingEmployee, setEditingEmployee] = useState<any>(null);
useEffect(() => {
setEmployees(initialEmployees);
}, [initialEmployees]);
const filteredEmployees = employees.filter(emp => { const filteredEmployees = employees.filter(emp => {
const userData = Array.isArray(emp.users) ? emp.users[0] : emp.users; const fullName = `${emp.first_name || ''} ${emp.last_name || ''}`.toLowerCase();
const fullName = `${userData?.first_name || ''} ${userData?.last_name || ''}`.toLowerCase(); const email = (emp.email || '').toLowerCase();
const email = (userData?.email || '').toLowerCase(); const tcNo = (emp.tc_no || '').toLowerCase();
const matchesSearch = fullName.includes(searchTerm.toLowerCase()) || email.includes(searchTerm.toLowerCase()); const matchesSearch = fullName.includes(searchTerm.toLowerCase()) ||
email.includes(searchTerm.toLowerCase()) ||
tcNo.includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === 'all' || emp.status === statusFilter; const matchesStatus = statusFilter === 'all' || emp.status === statusFilter;
return matchesSearch && matchesStatus; return matchesSearch && matchesStatus;
}); });
const handleEdit = (emp: any) => { const employeesWithoutUser = employees.filter(emp => !emp.user_id && emp.email);
const openEditModal = (emp: any) => {
setEditingEmployee(emp); setEditingEmployee(emp);
setIsModalOpen(true); setIsModalOpen(true);
}; };
@@ -52,13 +81,18 @@ export default function EmployeeTable({ initialEmployees, companies, roles }: Em
} }
}; };
const formatDate = (dateString: string | null) => {
if (!dateString) return '-';
return new Date(dateString).toLocaleDateString('tr-TR');
};
return ( return (
<div className="space-y-8"> <div className="space-y-8">
{/* Header with Add Button */} {/* Header with Add Button */}
<div className="sm:flex sm:items-center justify-between gap-4"> <div className="sm:flex sm:items-center justify-between gap-4">
<div className="sm:flex-auto"> <div className="sm:flex-auto">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="w-1.5 h-10 bg-indigo-500 rounded-full" /> <div className="w-1.5 h-10 bg-[#173363] rounded-full" />
<div> <div>
<h1 className="text-3xl font-bold text-slate-900 dark:text-white tracking-tight">Personeller</h1> <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"> <p className="text-sm font-medium text-slate-500 dark:text-slate-400 mt-1">
@@ -67,127 +101,201 @@ export default function EmployeeTable({ initialEmployees, companies, roles }: Em
</div> </div>
</div> </div>
</div> </div>
<button <div className="flex gap-4">
onClick={handleAdd} <button
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" onClick={() => setIsUserModalOpen(true)}
> className="group flex items-center gap-2 bg-slate-100 dark:bg-zinc-800 hover:bg-[#173363] text-[#173363] dark:text-slate-300 hover:text-white px-8 py-3.5 rounded-full font-black transition-all duration-500 active:scale-95 text-xs tracking-widest"
<PlusIcon className="w-5 h-5 transition-transform group-hover:rotate-90" /> >
YENİ PERSONEL <UserPlusIcon className="w-5 h-5 transition-transform group-hover:scale-110" />
</button> KULLANICI TANIMLA
</button>
<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 text-xs tracking-widest"
>
<PlusIcon className="w-5 h-5 transition-transform group-hover:rotate-90" />
YENİ PERSONEL
</button>
</div>
</div> </div>
{/* Filter & Search Bar */} {/* 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="flex flex-col md:flex-row gap-4 items-center justify-between bg-white dark:bg-zinc-900 p-6 rounded-[2rem] border border-slate-100 dark:border-zinc-800 shadow-sm">
<div className="relative w-full md:w-96 group"> <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" /> <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 <input
type="text" type="text"
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
placeholder="İsim veya e-posta ile ara..." placeholder="İsim, TC 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" className="w-full pl-12 pr-4 py-3 bg-slate-50 dark:bg-zinc-800 border-none rounded-2xl text-sm font-medium focus:ring-2 focus:ring-[#CE0515] transition-all outline-none placeholder:text-slate-400 dark:text-white"
/> />
</div> </div>
<div className="flex gap-2 w-full md:w-auto p-1.5 bg-slate-50 rounded-full border border-slate-100"> <div className="flex gap-2 w-full md:w-auto p-1.5 bg-slate-50 dark:bg-zinc-800 rounded-full border border-slate-100 dark:border-zinc-700">
<button <button
onClick={() => setStatusFilter('all')} 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'}`} 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 dark:hover:text-slate-300'}`}
> >
Tümü Tümü
</button> </button>
<button <button
onClick={() => setStatusFilter('active')} 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'}`} 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 dark:hover:text-slate-300'}`}
> >
Aktif Aktif
</button> </button>
<button <button
onClick={() => setStatusFilter('inactive')} 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'}`} 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 dark:hover:text-slate-300'}`}
> >
Pasif Ayrılan
</button> </button>
</div> </div>
</div> </div>
{/* Table Section */} {/* Table Section */}
<div className="bg-white shadow-sm border border-slate-100 rounded-[2.5rem] overflow-hidden"> <div className="bg-white dark:bg-zinc-900 shadow-sm border border-slate-100 dark:border-zinc-800 rounded-[2.5rem] overflow-hidden">
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="min-w-full divide-y divide-slate-50"> <table className="min-w-full divide-y divide-slate-50 dark:divide-zinc-800">
<thead className="bg-slate-50/50"> <thead className="bg-slate-50/50 dark:bg-zinc-800/50">
<tr> <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="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">Kurumsal</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">Bölüm / Görev</th>
<th className="px-3 py-6 text-left text-[10px] font-black uppercase tracking-[0.2em] text-slate-400">İletişim</th>
<th className="px-3 py-6 text-left text-[10px] font-black uppercase tracking-[0.2em] text-slate-400">Tarihler</th>
<th className="px-3 py-6 text-left text-[10px] font-black uppercase tracking-[0.2em] text-slate-400">Durum</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"> <th className="relative py-6 pl-3 pr-8 text-right">
<span className="sr-only">İşlemler</span> <span className="sr-only">İşlemler</span>
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-slate-50"> <tbody className="divide-y divide-slate-50 dark:divide-zinc-800">
{filteredEmployees.map((emp) => { {filteredEmployees.map((emp) => (
const userData = Array.isArray(emp.users) ? emp.users[0] : emp.users; <tr key={emp.id} className="group hover:bg-slate-50/50 dark:hover:bg-zinc-800/20 transition-all duration-500">
const companyData = Array.isArray(emp.companies) ? emp.companies[0] : emp.companies; {/* Personel */}
const roleData = Array.isArray(emp.roles) ? emp.roles[0] : emp.roles; <td className="whitespace-nowrap py-6 pl-8 pr-3">
<div className="flex items-center gap-4">
{emp.photo_url ? (
<img src={emp.photo_url} alt="" className="w-14 h-14 rounded-2xl object-cover shadow-lg shadow-blue-900/10 group-hover:scale-110 transition-transform duration-500" />
) : (
<div className="w-14 h-14 rounded-2xl bg-[#173363] flex items-center justify-center text-white font-black text-base shadow-lg shadow-blue-900/10 group-hover:bg-[#CE0515] transition-colors duration-500">
{emp.first_name?.[0]}{emp.last_name?.[0]}
</div>
)}
<div>
<p className="text-sm font-black text-[#173363] dark:text-white group-hover:text-black dark:group-hover:text-white transition-colors">
{emp.first_name} {emp.last_name}
</p>
<div className="flex items-center gap-1.5 mt-1 text-slate-400">
<IdentificationIcon className="w-3.5 h-3.5" />
<p className="text-[10px] font-black tracking-widest">{emp.tc_no || '--'}</p>
</div>
</div>
</div>
</td>
return ( {/* Kurumsal */}
<tr key={emp.id} className="group hover:bg-slate-50/50 transition-all duration-500"> <td className="whitespace-nowrap px-3 py-6">
<td className="whitespace-nowrap py-6 pl-8 pr-3"> <p className="text-sm font-black text-slate-700 dark:text-white">{emp.companies?.name}</p>
<div className="flex items-center gap-4"> <div className="flex flex-col gap-1 mt-1.5">
<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"> <span className="inline-flex items-center text-[10px] text-[#CE0515] font-black uppercase tracking-widest bg-red-50 dark:bg-red-900/20 px-2 py-0.5 rounded-md w-fit">
{userData?.first_name?.[0]}{userData?.last_name?.[0]} {emp.roles?.description}
</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> </span>
</td> <span className="text-[10px] text-slate-400 font-bold">
<td className="whitespace-nowrap py-6 pl-3 pr-8 text-right"> {emp.employment_types?.name || '-'}
<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"> </span>
<button </div>
onClick={() => handleEdit(emp)} </td>
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" {/* Bölüm / Görev */}
> <td className="whitespace-nowrap px-3 py-6">
<PencilSquareIcon className="w-5 h-5" /> <div className="flex items-start gap-2">
</button> <BuildingOffice2Icon className="w-4 h-4 text-slate-300 mt-0.5" />
<button <div>
onClick={() => handleDelete(emp.id)} <p className="text-sm font-black text-slate-700 dark:text-slate-300">{emp.departments?.name || '-'}</p>
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" <p className="text-[10px] text-slate-400 font-bold mt-1">
title="Sil" {emp.sections?.name ? `${emp.sections.name} / ` : ''}{emp.job_titles?.name || '-'}
> </p>
<TrashIcon className="w-5 h-5" /> </div>
</button> </div>
</td>
{/* İletişim */}
<td className="whitespace-nowrap px-3 py-6">
<div className="space-y-1.5">
<div className="flex items-center gap-2 text-slate-500 dark:text-slate-400">
<EnvelopeIcon className="w-3.5 h-3.5" />
<p className="text-[11px] font-bold">{emp.email || '-'}</p>
</div> </div>
</td> <div className="flex items-center gap-2 text-slate-500 dark:text-slate-400">
</tr> <PhoneIcon className="w-3.5 h-3.5" />
); <p className="text-[11px] font-bold">{emp.phone1 || '-'}</p>
})} </div>
</div>
</td>
{/* Tarihler */}
<td className="whitespace-nowrap px-3 py-6">
<div className="space-y-1.5">
<div className="flex items-center gap-2 text-slate-400">
<CalendarDaysIcon className="w-3.5 h-3.5 text-emerald-500" />
<p className="text-[10px] font-black">{formatDate(emp.start_date)}</p>
</div>
{emp.leave_date && (
<div className="flex items-center gap-2 text-slate-400">
<CalendarDaysIcon className="w-3.5 h-3.5 text-rose-500" />
<p className="text-[10px] font-black">{formatDate(emp.leave_date)}</p>
</div>
)}
</div>
</td>
{/* Durum */}
<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 dark:bg-emerald-900/20 text-emerald-700 dark:text-emerald-400 border-emerald-100 dark:border-emerald-900/30'
: 'bg-slate-50 dark:bg-zinc-800 text-slate-500 dark:text-slate-400 border-slate-100 dark:border-zinc-700'
}`}>
<span className={`w-2 h-2 rounded-full ${emp.status === 'active' ? 'bg-emerald-500 animate-pulse' : 'bg-slate-400'}`} />
{emp.status === 'active' ? 'Aktif' : 'Ayrılan'}
</span>
</td>
{/* İşlemler */}
<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">
{!emp.user_id && emp.email && (
<button
onClick={() => setIsUserModalOpen(true)}
className="p-3 rounded-2xl text-emerald-600 hover:text-white hover:bg-emerald-600 hover:shadow-lg hover:shadow-emerald-900/20 transition-all duration-300"
title="Sistem Girişi Tanımla"
>
<KeyIcon className="w-5 h-5" />
</button>
)}
<button
onClick={() => openEditModal(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 && ( {filteredEmployees.length === 0 && (
<tr> <tr>
<td colSpan={5} className="py-20 text-center"> <td colSpan={7} className="py-20 text-center">
<UsersIcon className="mx-auto h-16 w-16 text-slate-200 dark:text-zinc-800 mb-4" /> <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> <p className="text-slate-500 dark:text-slate-400 font-bold">Herhangi bir personel kaydı bulunamadı.</p>
</td> </td>
@@ -198,14 +306,24 @@ export default function EmployeeTable({ initialEmployees, companies, roles }: Em
</div> </div>
</div> </div>
{/* Modal */} {/* Modals */}
<EmployeeModal <EmployeeModal
isOpen={isModalOpen} isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)} onClose={() => setIsModalOpen(false)}
companies={companies} companies={companies}
roles={roles} roles={roles}
departments={departments}
sections={sections}
employmentTypes={employmentTypes}
jobTitles={jobTitles}
editingEmployee={editingEmployee} editingEmployee={editingEmployee}
/> />
<UserCreationModal
isOpen={isUserModalOpen}
onClose={() => setIsUserModalOpen(false)}
employeesWithoutUser={employeesWithoutUser}
/>
</div> </div>
); );
} }
@@ -0,0 +1,111 @@
'use client';
import { useState } from 'react';
import { XMarkIcon, KeyIcon } from '@heroicons/react/24/outline';
import { createUserForEmployee } from '@/app/employees/actions';
interface UserCreationModalProps {
isOpen: boolean;
onClose: () => void;
employeesWithoutUser: any[];
}
export default function UserCreationModal({ isOpen, onClose, employeesWithoutUser }: UserCreationModalProps) {
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 {
const result = await createUserForEmployee(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-[60] flex items-center justify-center p-4 bg-slate-900/60 backdrop-blur-md transition-all">
<div className="bg-white dark:bg-zinc-900 w-full max-w-md rounded-[2.5rem] shadow-2xl border border-slate-100 dark:border-zinc-800 overflow-hidden animate-in fade-in zoom-in duration-300">
{/* Header */}
<div className="px-10 py-8 border-b border-slate-50 dark:border-zinc-800 flex items-center justify-between">
<div>
<h2 className="text-xl font-black text-[#173363] dark:text-white tracking-tight">Kullanıcı Tanımla</h2>
<p className="text-xs font-bold text-slate-400 mt-1">Personel için sistem girişi oluşturun.</p>
</div>
<button onClick={onClose} className="p-2 rounded-xl hover:bg-slate-50 text-slate-300 hover:text-[#CE0515] transition-all">
<XMarkIcon className="w-6 h-6" />
</button>
</div>
{/* Body */}
<form action={handleSubmit} className="p-10 space-y-6">
{error && (
<div className="p-4 bg-rose-50 border border-rose-100 rounded-2xl text-rose-600 text-[10px] font-black text-center uppercase tracking-widest">
{error}
</div>
)}
<div className="space-y-4">
<div>
<label className="block text-[10px] font-black uppercase tracking-widest text-slate-400 mb-1.5 ml-1">PERSONEL SEÇİMİ</label>
<select
name="employee_id"
required
className="w-full rounded-2xl border-0 py-3.5 px-4 text-slate-900 bg-slate-50 ring-1 ring-inset ring-slate-200 focus:ring-2 focus:ring-[#173363] outline-none transition-all text-sm font-bold"
>
<option value="">Seçiniz</option>
{employeesWithoutUser.map(emp => (
<option key={emp.id} value={emp.id}>{emp.first_name} {emp.last_name} ({emp.email})</option>
))}
</select>
</div>
<div>
<label className="block text-[10px] font-black uppercase tracking-widest text-slate-400 mb-1.5 ml-1">GİRİŞ ŞİFRESİ</label>
<div className="relative group">
<KeyIcon className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-300 group-focus-within:text-[#173363] transition-colors" />
<input
type="password"
name="password"
required
placeholder="••••••••"
className="w-full pl-12 pr-4 py-3.5 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-[#173363] transition-all outline-none"
/>
</div>
</div>
</div>
<div className="pt-4 flex gap-4">
<button
type="button"
onClick={onClose}
className="flex-1 px-4 py-4 rounded-full font-black text-slate-300 hover:bg-slate-50 transition-all text-[10px] uppercase tracking-[0.2em]"
>
İ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-[10px] uppercase tracking-[0.2em]"
>
{loading ? 'İŞLENİYOR...' : 'HESAP OLUŞTUR'}
</button>
</div>
</form>
</div>
</div>
);
}
+14
View File
@@ -0,0 +1,14 @@
import { createClient } from '@supabase/supabase-js'
export function createAdminClient() {
return createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
{
auth: {
autoRefreshToken: false,
persistSession: false
}
}
)
}
@@ -0,0 +1,164 @@
-- 1. Create Lookup Tables (Idempotent)
CREATE TABLE IF NOT EXISTS public.departments (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
);
CREATE TABLE IF NOT EXISTS public.sections (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
department_id UUID REFERENCES public.departments(id) ON DELETE CASCADE,
name TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL,
UNIQUE(department_id, name)
);
CREATE TABLE IF NOT EXISTS public.employment_types (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
);
CREATE TABLE IF NOT EXISTS public.job_titles (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
);
-- 2. Update Employees Table (Idempotent)
DO $$
BEGIN
-- Add columns if they don't exist
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='employees' AND column_name='first_name') THEN
ALTER TABLE public.employees ADD COLUMN first_name TEXT;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='employees' AND column_name='last_name') THEN
ALTER TABLE public.employees ADD COLUMN last_name TEXT;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='employees' AND column_name='email') THEN
ALTER TABLE public.employees ADD COLUMN email TEXT UNIQUE;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='employees' AND column_name='photo_url') THEN
ALTER TABLE public.employees ADD COLUMN photo_url TEXT;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='employees' AND column_name='department_id') THEN
ALTER TABLE public.employees ADD COLUMN department_id UUID REFERENCES public.departments(id);
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='employees' AND column_name='section_id') THEN
ALTER TABLE public.employees ADD COLUMN section_id UUID REFERENCES public.sections(id);
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='employees' AND column_name='employment_type_id') THEN
ALTER TABLE public.employees ADD COLUMN employment_type_id UUID REFERENCES public.employment_types(id);
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='employees' AND column_name='job_title_id') THEN
ALTER TABLE public.employees ADD COLUMN job_title_id UUID REFERENCES public.job_titles(id);
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='employees' AND column_name='tc_no') THEN
ALTER TABLE public.employees ADD COLUMN tc_no TEXT;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='employees' AND column_name='birth_date') THEN
ALTER TABLE public.employees ADD COLUMN birth_date DATE;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='employees' AND column_name='birth_place') THEN
ALTER TABLE public.employees ADD COLUMN birth_place TEXT;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='employees' AND column_name='gender') THEN
ALTER TABLE public.employees ADD COLUMN gender TEXT;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='employees' AND column_name='address') THEN
ALTER TABLE public.employees ADD COLUMN address TEXT;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='employees' AND column_name='phone1') THEN
ALTER TABLE public.employees ADD COLUMN phone1 TEXT;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='employees' AND column_name='phone2') THEN
ALTER TABLE public.employees ADD COLUMN phone2 TEXT;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='employees' AND column_name='has_driving_license') THEN
ALTER TABLE public.employees ADD COLUMN has_driving_license BOOLEAN DEFAULT false;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='employees' AND column_name='military_status') THEN
ALTER TABLE public.employees ADD COLUMN military_status TEXT;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='employees' AND column_name='start_date') THEN
ALTER TABLE public.employees ADD COLUMN start_date DATE;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='employees' AND column_name='leave_date') THEN
ALTER TABLE public.employees ADD COLUMN leave_date DATE;
END IF;
END $$;
-- 2.5 Update Users Table (Idempotent)
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='users' AND column_name='status') THEN
ALTER TABLE public.users ADD COLUMN status TEXT DEFAULT 'active';
END IF;
END $$;
-- 3. Enable RLS for new tables
ALTER TABLE public.departments ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.sections ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.employment_types ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.job_titles ENABLE ROW LEVEL SECURITY;
-- Idempotent Policies
DO $$
BEGIN
DROP POLICY IF EXISTS "Allow authenticated full access to lookup tables" ON public.departments;
CREATE POLICY "Allow authenticated full access to lookup tables" ON public.departments FOR ALL TO authenticated USING (true) WITH CHECK (true);
DROP POLICY IF EXISTS "Allow authenticated full access to sections" ON public.sections;
CREATE POLICY "Allow authenticated full access to sections" ON public.sections FOR ALL TO authenticated USING (true) WITH CHECK (true);
DROP POLICY IF EXISTS "Allow authenticated full access to employment_types" ON public.employment_types;
CREATE POLICY "Allow authenticated full access to employment_types" ON public.employment_types FOR ALL TO authenticated USING (true) WITH CHECK (true);
DROP POLICY IF EXISTS "Allow authenticated full access to job_titles" ON public.job_titles;
CREATE POLICY "Allow authenticated full access to job_titles" ON public.job_titles FOR ALL TO authenticated USING (true) WITH CHECK (true);
END $$;
-- 4. Storage Bucket for Photos (Idempotent)
DO $$
BEGIN
INSERT INTO storage.buckets (id, name, public)
VALUES ('employee-photos', 'employee-photos', true)
ON CONFLICT (id) DO NOTHING;
END $$;
DO $$
BEGIN
DROP POLICY IF EXISTS "Public Access to Employee Photos" ON storage.objects;
CREATE POLICY "Public Access to Employee Photos" ON storage.objects FOR SELECT TO public USING (bucket_id = 'employee-photos');
DROP POLICY IF EXISTS "Authenticated users can upload employee photos" ON storage.objects;
CREATE POLICY "Authenticated users can upload employee photos" ON storage.objects FOR INSERT TO authenticated WITH CHECK (bucket_id = 'employee-photos');
END $$;
-- 5. Trigger for auto-deactivation
CREATE OR REPLACE FUNCTION public.handle_employee_leave()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.leave_date IS NOT NULL AND (OLD.leave_date IS NULL OR OLD.leave_date != NEW.leave_date) THEN
-- Mark employee as inactive
NEW.status := 'inactive';
-- Mark linked user as inactive if exists
IF NEW.user_id IS NOT NULL THEN
UPDATE public.users SET status = 'inactive' WHERE id = NEW.user_id;
END IF;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS on_employee_leave ON public.employees;
CREATE TRIGGER on_employee_leave
BEFORE UPDATE ON public.employees
FOR EACH ROW
EXECUTE FUNCTION public.handle_employee_leave();
-- 6. Seed some initial data (Idempotent)
INSERT INTO public.departments (name) VALUES ('Yönetim'), ('Bilgi İşlem'), ('İnsan Kaynakları'), ('Satış'), ('Üretim') ON CONFLICT (name) DO NOTHING;
INSERT INTO public.employment_types (name) VALUES ('Tam Zamanlı'), ('Yarı Zamanlı'), ('Stajyer'), ('Sözleşmeli') ON CONFLICT (name) DO NOTHING;
INSERT INTO public.job_titles (name) VALUES ('Müdür'), ('Yazılım Geliştirici'), ('IK Uzmanı'), ('Satış Temsilcisi'), ('Operatör') ON CONFLICT (name) DO NOTHING;