new özellik
This commit is contained in:
+134
-28
@@ -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
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
Reference in New Issue
Block a user