From 600dce8052dee7074a0ffa7adaf50e2c1eac5778 Mon Sep 17 00:00:00 2001 From: Kenan KARAER Date: Wed, 18 Mar 2026 12:38:50 +0300 Subject: [PATCH] =?UTF-8?q?new=20=C3=B6zellik?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/employees/actions.ts | 162 +++++++-- src/app/employees/page.tsx | 37 +-- src/components/employees/EmployeeModal.tsx | 311 +++++++++++++----- src/components/employees/EmployeeTable.tsx | 294 ++++++++++++----- .../employees/UserCreationModal.tsx | 111 +++++++ src/utils/supabase/admin.ts | 14 + .../20240318000001_extended_personnel.sql | 164 +++++++++ 7 files changed, 875 insertions(+), 218 deletions(-) create mode 100644 src/components/employees/UserCreationModal.tsx create mode 100644 src/utils/supabase/admin.ts create mode 100644 supabase/migrations/20240318000001_extended_personnel.sql diff --git a/src/app/employees/actions.ts b/src/app/employees/actions.ts index 75ac06d..75ac1ff 100644 --- a/src/app/employees/actions.ts +++ b/src/app/employees/actions.ts @@ -1,6 +1,7 @@ 'use server' import { createClient } from '@/utils/supabase/server' +import { createAdminClient } from '@/utils/supabase/admin' import { revalidatePath } from 'next/cache' export async function addEmployee(formData: FormData) { @@ -12,45 +13,62 @@ export async function addEmployee(formData: FormData) { const email = formData.get('email') as string const companyId = formData.get('company_id') as string const roleId = formData.get('role_id') as string + const photo = formData.get('photo') as File - if (!email || !companyId || !roleId) { - return { error: 'E-posta, Şirket ve Rol seçimi zorunludur.' } + if (!firstName || !lastName || !companyId || !roleId) { + 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 - // In a robust HRMS, the Superadmin uses the Supabase Admin API to `createUser`. - // For the sake of this prototype, we'll assume the user must register first via valid credentials, - // or we can insert an unauthenticated ghost user into `public.users` (which breaks reference to auth.users if not careful). - // - // Let's implement the standard approach: we check if the user exists in `auth.users` via `public.users` - - let { data: existingUser } = await supabase - .from('users') - .select('id') - .eq('email', email) - .single() + // 2. Handle Photo Upload + let photoUrl = null + if (photo && photo.size > 0) { + const fileExt = photo.name.split('.').pop() + const fileName = `${Math.random()}.${fileExt}` + const filePath = `${fileName}` - if (!existingUser) { - return { error: 'Bu e-posta adresine sahip bir kullanıcı bulunamadı. Kullanıcının önce sisteme kayıt olması gerekmektedir.' } + const { error: uploadError, data } = await supabase.storage + .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 .from('employees') .insert([{ - user_id: existingUser.id, + first_name: firstName, + last_name: lastName, + email: email || null, company_id: companyId, role_id: roleId, - department: formData.get('department'), - title: formData.get('title'), - status: formData.get('status') || 'active' + photo_url: photoUrl, + department_id: formData.get('department_id') || null, + 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) { - // Catch unique constraint violation - if (employeeError.code === '23505') { - return { error: 'Bu kullanıcı zaten bu şirkete eklenmiş.' } - } 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) { 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 .from('employees') .update({ + first_name: formData.get('first_name'), + last_name: formData.get('last_name'), + email: formData.get('email'), company_id: formData.get('company_id'), role_id: formData.get('role_id'), - department: formData.get('department'), - title: formData.get('title'), - status: formData.get('status') + photo_url: photoUrl, + department_id: formData.get('department_id') || null, + 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) @@ -95,3 +150,54 @@ export async function updateEmployee(id: string, formData: FormData) { revalidatePath('/employees') 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 } +} diff --git a/src/app/employees/page.tsx b/src/app/employees/page.tsx index d749c55..a594955 100644 --- a/src/app/employees/page.tsx +++ b/src/app/employees/page.tsx @@ -8,31 +8,24 @@ export default async function EmployeesPage() { const { data: employees } = await supabase .from('employees') .select(` - id, - user_id, - company_id, - role_id, - department, - title, - status, - created_at, + *, companies ( id, name ), 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 }) - // Fetch companies for the modal - const { data: companies } = await supabase - .from('companies') - .select('id, name') - .order('name') - - // Fetch roles for the modal - const { data: roles } = await supabase - .from('roles') - .select('id, name, description') - .order('name') + // Fetch lookups + const { data: companies } = await supabase.from('companies').select('id, name').order('name') + const { data: roles } = await supabase.from('roles').select('id, name, description').order('name') + const { data: departments } = await supabase.from('departments').select('id, 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') + const { data: jobTitles } = await supabase.from('job_titles').select('id, name').order('name') return (
@@ -40,6 +33,10 @@ export default async function EmployeesPage() { initialEmployees={employees || []} companies={companies || []} roles={roles || []} + departments={departments || []} + sections={sections || []} + employmentTypes={employmentTypes || []} + jobTitles={jobTitles || []} />
) diff --git a/src/components/employees/EmployeeModal.tsx b/src/components/employees/EmployeeModal.tsx index 909659b..41e7433 100644 --- a/src/components/employees/EmployeeModal.tsx +++ b/src/components/employees/EmployeeModal.tsx @@ -1,7 +1,7 @@ 'use client'; -import { useState, useEffect } from 'react'; -import { XMarkIcon } from '@heroicons/react/24/outline'; +import { useState, useEffect, useRef } from 'react'; +import { XMarkIcon, PhotoIcon } from '@heroicons/react/24/outline'; import { addEmployee, updateEmployee } from '@/app/employees/actions'; interface EmployeeModalProps { @@ -9,15 +9,54 @@ interface EmployeeModalProps { onClose: () => void; companies: any[]; roles: any[]; + departments: any[]; + sections: any[]; + employmentTypes: any[]; + jobTitles: 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(null); const [loading, setLoading] = useState(false); + const [activeTab, setActiveTab] = useState('personal'); + const [selectedDept, setSelectedDept] = useState(editingEmployee?.department_id || ''); + const [previewUrl, setPreviewUrl] = useState(editingEmployee?.photo_url || null); + const fileInputRef = useRef(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; + const filteredSections = sections.filter(s => s.department_id === selectedDept); + + const handlePhotoChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + const url = URL.createObjectURL(file); + setPreviewUrl(url); + } + }; + async function handleSubmit(formData: FormData) { setLoading(true); 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 ( -
-
+
+
{/* Header */} -
+
-

+

{editingEmployee ? 'Personel Düzenle' : 'Yeni Personel Ekle'}

-

- Personel bilgilerini eksiksiz doldurunuz. +

+ Sistem üzerindeki personel bilgilerini detaylı olarak yönetin.

+ {/* Tabs */} +
+ {[ + { 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 => ( + + ))} +
+ {/* Body */} -
+ {error && ( -
+
{error}
)} -
- {!editingEmployee && ( -
- - -
- )} + {activeTab === 'personal' && ( +
+
+ {/* Photo Upload */} +
+
fileInputRef.current?.click()} + 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" + > + {previewUrl ? ( + Preview + ) : ( + <> + + FOTOĞRAF YÜKLE + + )} +
+ + +
-
-
- - +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ )} + + {activeTab === 'employment' && ( +
+
+ +
- -
- - {roles.map(r => )}
-
- -
-
- - +
+ +
- -
- - +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ )} -
- - + {activeTab === 'contact' && ( +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +