İzin listesi,yetkilendirme vb

This commit is contained in:
2026-03-20 02:30:35 +03:00
parent b34623350e
commit 015caea52e
13 changed files with 1601 additions and 386 deletions
+31 -1
View File
@@ -6,6 +6,22 @@ import { revalidatePath } from 'next/cache'
export async function addCompany(formData: FormData) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return { error: 'Oturum bulunamadı.' }
// Check if user has admin/manager role somewhere
const { data: employeeRoles } = await supabase
.from('employees')
.select('roles(name)')
.eq('user_id', user.id)
const canManage = employeeRoles?.some((emp: any) => {
const roleName = Array.isArray(emp.roles) ? emp.roles[0]?.name : emp.roles?.name
return roleName === 'admin' || roleName === 'manager'
}) || false
if (!canManage) return { error: 'Şirket ekleme yetkiniz bulunmamaktadır.' }
const name = formData.get('name') as string
if (!name) return { error: 'Şirket adı zorunludur.' }
@@ -23,6 +39,20 @@ export async function addCompany(formData: FormData) {
export async function deleteCompany(id: string) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return { error: 'Oturum bulunamadı.' }
const { data: employeeRoles } = await supabase
.from('employees')
.select('roles(name)')
.eq('user_id', user.id)
const canManage = employeeRoles?.some((emp: any) => {
const roleName = Array.isArray(emp.roles) ? emp.roles[0]?.name : emp.roles?.name
return roleName === 'admin' || roleName === 'manager'
}) || false
if (!canManage) return { error: 'Şirket silme yetkiniz bulunmamaktadır.' }
const { error } = await supabase
.from('companies')
@@ -30,7 +60,7 @@ export async function deleteCompany(id: string) {
.eq('id', id)
if (error) {
return { error: 'Şirket silinirken bir hata oluştu: ' + error.message }
return { error: 'Şirket silünerken bir hata oluştu: ' + error.message }
}
revalidatePath('/companies')
+65 -47
View File
@@ -1,11 +1,30 @@
import { createClient } from '@/utils/supabase/server'
import { deleteCompany } from './actions'
import { BuildingOfficeIcon, TrashIcon } from '@heroicons/react/24/outline'
import CompanyForm from '@/components/companies/CompanyForm'
import { BuildingOfficeIcon, TrashIcon, PlusIcon } from '@heroicons/react/24/outline'
import { redirect } from 'next/navigation'
import AddCompanyModal from '@/components/companies/AddCompanyModal'
export default async function CompaniesPage() {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return redirect('/login')
// Authorization Check
const { data: employeeRoles } = await supabase
.from('employees')
.select('roles(name)')
.eq('user_id', user.id)
const isAdminOrManager = employeeRoles?.some((emp: any) => {
const roleName = Array.isArray(emp.roles) ? emp.roles[0]?.name : emp.roles?.name
return roleName === 'admin' || roleName === 'manager'
}) || false
if (!isAdminOrManager) {
return redirect('/')
}
// Fetch companies
const { data: companies } = await supabase
.from('companies')
@@ -13,72 +32,70 @@ export default async function CompaniesPage() {
.order('created_at', { ascending: false })
return (
<div className="p-4 sm:p-6 lg:p-8 max-w-7xl mx-auto">
<div className="sm:flex sm:items-center justify-between gap-4">
<div className="sm:flex-auto">
<div className="p-4 sm:p-6 lg:p-8 max-w-none mx-auto space-y-8">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-6">
<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-12 bg-[#CE0515] rounded-full" />
<div>
<h1 className="text-3xl font-bold text-slate-900 dark:text-white tracking-tight">Şirket Yönetimi</h1>
<h1 className="text-3xl font-black text-[#173363] dark:text-white tracking-tight">Şirket Yönetimi</h1>
<p className="text-sm font-medium text-slate-500 dark:text-slate-400 mt-1">
Sisteme kayıtlı şirketlerin listesi ve yeni şirket ekleme alanı.
Sisteme kayıtlı şirketlerin listesi ve yönetim alanı.
</p>
</div>
</div>
</div>
{/* Modal bazlı yeni şirket ekleme */}
<AddCompanyModal />
</div>
<div className="mt-8 grid grid-cols-1 md:grid-cols-3 gap-8">
{/* Add Company Form */}
<div className="md:col-span-1 border border-gray-200 dark:border-zinc-800 rounded-xl bg-white dark:bg-zinc-900 shadow-sm overflow-hidden h-fit">
<div className="px-4 py-5 sm:p-6">
<h3 className="text-base font-semibold leading-6 text-gray-900 dark:text-white">
Yeni Şirket Ekle
</h3>
<div className="mt-2 max-w-xl text-sm text-gray-500 dark:text-gray-400">
<p>Personelleri atayabilmek için öncelikle şirket kaydı açmalısınız.</p>
</div>
<CompanyForm />
</div>
</div>
{/* Company List */}
<div className="md:col-span-2">
<div className="bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-gray-200 dark:ring-zinc-800 sm:rounded-xl overflow-hidden">
<table className="min-w-full divide-y divide-slate-200 dark:divide-zinc-800">
<thead className="bg-slate-50 dark:bg-zinc-950/50">
<div className="bg-white dark:bg-zinc-900 shadow-sm border border-slate-100 dark:border-zinc-800 rounded-[2rem] overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-slate-100 dark:divide-zinc-800">
<thead className="bg-slate-50/50 dark:bg-zinc-950/50">
<tr>
<th scope="col" className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-slate-900 dark:text-white sm:pl-6">
Şirket Adı
<th scope="col" className="py-5 pl-6 pr-3 text-left">
<span className="text-xs font-black text-slate-400 uppercase tracking-widest">Şirket Adı</span>
</th>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-slate-900 dark:text-white">
Kayıt Tarihi
<th scope="col" className="px-3 py-5 text-left">
<span className="text-xs font-black text-slate-400 uppercase tracking-widest">Kayıt Tarihi</span>
</th>
<th scope="col" className="relative py-3.5 pl-3 pr-4 sm:pr-6">
<span className="sr-only">İşlemler</span>
<th scope="col" className="relative py-5 pl-3 pr-6 text-right">
<span className="text-xs font-black text-slate-400 uppercase tracking-widest">İşlemler</span>
</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-200 dark:divide-zinc-800 bg-white dark:bg-zinc-900">
<tbody className="divide-y divide-slate-50 dark:divide-zinc-800 bg-white dark:bg-zinc-900">
{companies?.map((company) => (
<tr key={company.id} className="hover:bg-slate-50 dark:hover:bg-zinc-800/50 transition-colors">
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-bold text-slate-900 dark:text-white sm:pl-6">
<tr key={company.id} className="hover:bg-slate-50/50 dark:hover:bg-zinc-800/50 transition-colors group">
<td className="whitespace-nowrap py-6 pl-6 pr-3">
<div className="flex items-center gap-3">
<div className="p-2 bg-slate-100 dark:bg-zinc-800 rounded-xl group-hover:bg-red-50 dark:group-hover:bg-red-500/10 transition-colors">
<BuildingOfficeIcon className="h-5 w-5 text-[#173363] group-hover:text-[#CE0515]" />
</div>
<span className="font-bold text-[#173363] dark:text-white group-hover:text-[#CE0515] transition-colors text-base">
{company.name}
</span>
</div>
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-slate-500 dark:text-slate-400 font-medium">
<td className="whitespace-nowrap px-3 py-6 text-sm">
<span className="font-bold text-slate-500 dark:text-slate-400">
{new Date(company.created_at).toLocaleDateString('tr-TR')}
</span>
</td>
<td className="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
<td className="relative whitespace-nowrap py-6 pl-3 pr-6 text-right font-medium">
<form action={async () => {
'use server';
const res = await deleteCompany(company.id);
if (res.error) {
alert(res.error);
// Note: In server component we can't easily alert, this needs client-side handling ideally
console.error(res.error);
}
}}>
<button type="submit" className="text-slate-400 hover:text-rose-600 dark:hover:text-rose-400 p-2 rounded-2xl hover:bg-rose-50 dark:hover:bg-rose-500/10 transition-all" title="Sil">
<button
type="submit"
className="bg-rose-50 text-rose-600 hover:bg-rose-600 hover:text-white p-2.5 rounded-xl transition-all active:scale-95"
title="Sil"
>
<TrashIcon className="h-5 w-5" />
</button>
</form>
@@ -87,8 +104,11 @@ export default async function CompaniesPage() {
))}
{(!companies || companies.length === 0) && (
<tr>
<td colSpan={3} className="py-12 text-center text-sm text-slate-500 dark:text-slate-400 font-medium">
Henüz kayıtlı şirket bulunmamaktadır.
<td colSpan={3} className="py-20 text-center">
<div className="flex flex-col items-center">
<BuildingOfficeIcon className="h-12 w-12 text-slate-200 mb-4" />
<span className="text-sm font-bold text-slate-400 uppercase tracking-widest">Henüz kayıtlı şirket bulunmamaktadır.</span>
</div>
</td>
</tr>
)}
@@ -96,8 +116,6 @@ export default async function CompaniesPage() {
</table>
</div>
</div>
</div>
</div>
)
}
+11 -1
View File
@@ -34,6 +34,7 @@ export default async function RootLayout({
// Fetch db user profile if logged in
let dbUser = null
let isAdminSomewhere = false
if (user) {
const { data } = await supabase
.from('users')
@@ -41,6 +42,15 @@ export default async function RootLayout({
.eq('id', user.id)
.single()
dbUser = data
const { data: employeeRoles } = await supabase
.from('employees')
.select('roles(name)')
.eq('user_id', user.id)
isAdminSomewhere = employeeRoles?.some((emp: any) =>
emp.roles?.name === 'admin' || emp.roles?.name === 'manager'
) || false
}
return (
@@ -50,7 +60,7 @@ export default async function RootLayout({
>
{user ? (
<div>
<Sidebar />
<Sidebar isAdmin={isAdminSomewhere} />
<div className="lg:pl-72 flex flex-col min-h-screen">
<Header user={dbUser || user} />
<main className="flex-1">
+161 -9
View File
@@ -12,28 +12,54 @@ export async function submitLeaveRequest(formData: FormData) {
const startDate = formData.get('start_date') as string
const endDate = formData.get('end_date') as string
const reason = formData.get('reason') as string
const companyId = formData.get('company_id') as string // which company they are requesting leave for
const leaveTypeId = formData.get('leave_type_id') as string
const companyId = formData.get('company_id') as string
const targetEmployeeId = formData.get('employee_id') as string // Optional: for admin use
if (!startDate || !endDate || !companyId || !reason) {
if (!startDate || !endDate || !companyId || !reason || !leaveTypeId) {
return { error: 'Tüm alanları doldurunuz.' }
}
// Find their employee record for this specific company
const { data: employeeData, error: empError } = await supabase
// Find user's admin/manager status for checks
const { data: currentUserEmployee } = await supabase
.from('employees')
.select('id')
.select('id, roles(name)')
.eq('user_id', user.id)
.eq('company_id', companyId)
.single()
.single();
if (empError || !employeeData) {
const isManager = (currentUserEmployee?.roles as any)?.name === 'admin' || (currentUserEmployee?.roles as any)?.name === 'manager';
let finalEmployeeId = targetEmployeeId;
if (!isManager || !targetEmployeeId) {
// If not manager or no target ID, use the current user's employee record for this company
if (!currentUserEmployee) {
return { error: 'Seçili şirket için personel kaydınız bulunamadı.' }
}
finalEmployeeId = currentUserEmployee.id;
}
// Check if this leave type is restricted to admins only
const { data: leaveType, error: typeError } = await supabase
.from('leave_types')
.select('only_admin_can_create')
.eq('id', leaveTypeId)
.single();
if (typeError) return { error: 'İzin türü doğrulanamadı.' };
if (leaveType.only_admin_can_create) {
if (!isManager) {
return { error: 'Bu izin türü sadece yöneticiler tarafından tanımlanabilir.' };
}
}
const { error } = await supabase
.from('leave_requests')
.insert([{
employee_id: employeeData.id,
employee_id: finalEmployeeId,
leave_type_id: leaveTypeId,
start_date: startDate,
end_date: endDate,
reason: reason,
@@ -50,10 +76,44 @@ export async function submitLeaveRequest(formData: FormData) {
export async function updateLeaveStatus(id: string, newStatus: 'approved' | 'rejected') {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return { error: 'Oturum bulunamadı.' }
// 1. Fetch leave request to know its company
const { data: leaveReq, error: fetchError } = await supabase
.from('leave_requests')
.select('employee_id, employees(company_id)')
.eq('id', id)
.single()
if (fetchError || !leaveReq) return { error: 'İzin talebi bulunamadı.' }
const companyId = (leaveReq.employees as any)?.company_id
// 2. Check if current user is admin/manager in that company
const { data: currentUserEmployee } = await supabase
.from('employees')
.select('roles(name)')
.eq('user_id', user.id)
.eq('company_id', companyId)
.single()
const isManager = (currentUserEmployee?.roles as any)?.name === 'admin' || (currentUserEmployee?.roles as any)?.name === 'manager';
if (!isManager) {
return { error: 'Bu izin talebini onaylamak için yetkiniz bulunmamaktadır.' }
}
const updateData: any = { status: newStatus }
if (newStatus === 'approved') {
updateData.approved_by = user.id
updateData.approval_date = new Date().toISOString()
}
const { error } = await supabase
.from('leave_requests')
.update({ status: newStatus })
.update(updateData)
.eq('id', id)
if (error) {
@@ -63,3 +123,95 @@ export async function updateLeaveStatus(id: string, newStatus: 'approved' | 'rej
revalidatePath('/leave-requests')
return { success: true }
}
export async function resetLeaveStatus(id: string) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return { error: 'Oturum bulunamadı.' }
const { data: leaveReq } = await supabase
.from('leave_requests')
.select('employees(company_id)')
.eq('id', id)
.single()
if (!leaveReq) return { error: 'Talebe ulaşılamadı.' }
const companyId = (leaveReq.employees as any).company_id
const { data: currentUserEmployee } = await supabase
.from('employees')
.select('roles(name)')
.eq('user_id', user.id)
.eq('company_id', companyId)
.single()
const isManager = (currentUserEmployee?.roles as any)?.name === 'admin' || (currentUserEmployee?.roles as any)?.name === 'manager'
if (!isManager) return { error: 'Bu işlem için yetkiniz bulunmamaktadır.' }
const { error } = await supabase
.from('leave_requests')
.update({
status: 'pending',
approved_by: null,
approval_date: null
})
.eq('id', id)
if (error) return { error: 'Durum sıfırlanırken hata: ' + error.message }
revalidatePath('/leave-requests')
return { success: true }
}
export async function updateLeaveRequest(formData: FormData) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return { error: 'Oturum bulunamadı.' }
const id = formData.get('id') as string
const startDate = formData.get('start_date') as string
const endDate = formData.get('end_date') as string
const reason = formData.get('reason') as string
const leaveTypeId = formData.get('leave_type_id') as string
// Fetch current request status and role
const { data: leaveReq } = await supabase
.from('leave_requests')
.select('status, employee_id, employees(company_id, user_id)')
.eq('id', id)
.single()
if (!leaveReq) return { error: 'Talep bulunamadı.' }
const companyId = (leaveReq.employees as any).company_id
const isOwner = (leaveReq.employees as any).user_id === user.id
const { data: currentUserEmployee } = await supabase
.from('employees')
.select('roles(name)')
.eq('user_id', user.id)
.eq('company_id', companyId)
.single()
const isManager = (currentUserEmployee?.roles as any)?.name === 'admin' || (currentUserEmployee?.roles as any)?.name === 'manager'
// Only owner (if pending) or manager can edit
if (!isManager && (!isOwner || leaveReq.status !== 'pending')) {
return { error: 'Bu talebi düzenleme yetkiniz bulunmamaktadır (Onaylanmış veya yetkisiz işlem).' }
}
const { error } = await supabase
.from('leave_requests')
.update({
start_date: startDate,
end_date: endDate,
reason: reason,
leave_type_id: leaveTypeId,
updated_at: new Date().toISOString()
})
.eq('id', id)
if (error) return { error: 'Güncellenirken hata: ' + error.message }
revalidatePath('/leave-requests')
return { success: true }
}
+270 -147
View File
@@ -1,239 +1,364 @@
import { createClient } from '@/utils/supabase/server'
import { updateLeaveStatus } from './actions'
import { CalendarDaysIcon, CheckIcon, XMarkIcon } from '@heroicons/react/24/outline'
import { updateLeaveStatus, resetLeaveStatus } from './actions'
import { CalendarDaysIcon, CheckIcon, XMarkIcon, ClockIcon, ArrowPathIcon, PencilIcon } from '@heroicons/react/24/outline'
import LeaveRequestForm from '@/components/LeaveRequestForm'
export default async function LeaveRequestsPage({
searchParams,
}: {
searchParams: Promise<{ status?: string; search?: string; type?: string }>
}) {
const params = await searchParams;
const statusFilter = params.status;
const searchFilter = params.search;
const typeFilter = params.type;
export default async function LeaveRequestsPage() {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return null
// Fetch user role
const { data: dbUser } = await supabase
.from('users')
.select('roles(name)')
.eq('id', user.id)
.single()
// Fetch user records with roles for each company
const { data: employeeRecords } = await supabase
.from('employees')
.select('id, company_id, roles(name), companies:company_id(id, name)')
.eq('user_id', user.id)
const roleData: any = dbUser?.roles
const roleName = Array.isArray(roleData) ? roleData[0]?.name : roleData?.name
const isAdminOrManager = roleName === 'admin' || roleName === 'manager'
const adminCompanyIds = employeeRecords
?.filter((emp: any) => {
const roleName = emp.roles?.name
return roleName === 'admin' || roleName === 'manager'
})
.map((emp: any) => emp.company_id) || []
// Fetch leave requests
// If admin/manager, fetch all. If employee, fetch only their own.
let query = supabase
const isAdminSomewhere = adminCompanyIds.length > 0
// Fetch all employees ONLY for companies where the user is admin/manager
let allEmployees: any[] = []
if (isAdminSomewhere) {
const { data: empData } = await supabase
.from('employees')
.select('id, company_id, users(first_name, last_name)')
.in('company_id', adminCompanyIds)
.eq('status', 'active')
allEmployees = empData || []
}
// Fetch leave types
const { data: leaveTypes } = await supabase
.from('leave_types')
.select('*')
.order('display_order')
// Fetch leave balances for the current user
const { data: balances } = await supabase
.from('leave_balances')
.select('*, leave_types!inner(name, display_order)')
.eq('year', new Date().getFullYear())
.eq('employee_id', employeeRecords?.[0]?.id) // Just a fallback, handled better in query
.order('display_order', { referencedTable: 'leave_types' })
// Fetch leave requests (Admins see all for their companies, users see their own)
let requestsQuery = supabase
.from('leave_requests')
.select(`
id,
start_date,
end_date,
total_days,
status,
reason,
created_at,
leave_type:leave_type_id ( name, color_code ),
employees!inner (
id,
user_id,
company_id,
companies ( name ),
users( first_name, last_name, email )
)
`)
.order('created_at', { ascending: false })
if (!isAdminOrManager) {
query = query.eq('employees.user_id', user.id)
if (!isAdminSomewhere) {
requestsQuery = requestsQuery.eq('employees.user_id', user.id)
} else {
// If admin somewhere, they see their own AND all from companies they manage
requestsQuery = requestsQuery.or(`user_id.eq.${user.id},company_id.in.(${adminCompanyIds.join(',')})`, { foreignTable: 'employees' })
}
const { data: requests } = await query
// Apply filters
if (statusFilter) {
requestsQuery = requestsQuery.eq('status', statusFilter)
}
if (typeFilter) {
requestsQuery = requestsQuery.eq('leave_type_id', typeFilter)
}
// Also fetch companies they belong to if they want to request leave
const { data: userCompanies } = await supabase
.from('employees')
.select('companies:company_id(id, name)')
.eq('user_id', user.id)
const { data: requests } = await requestsQuery
// Manual filtering for search (since it's a join with users)
let filteredRequests = requests || []
if (searchFilter) {
const term = searchFilter.toLowerCase()
filteredRequests = filteredRequests.filter(req => {
const employee = Array.isArray(req.employees) ? req.employees[0] : req.employees
const user = Array.isArray(employee?.users) ? employee?.users[0] : employee?.users
return (
`${user?.first_name} ${user?.last_name}`.toLowerCase().includes(term) ||
user?.email?.toLowerCase().includes(term)
)
})
}
return (
<div className="p-4 sm:p-6 lg:p-8 max-w-7xl mx-auto">
<div className="sm:flex sm:items-center justify-between gap-4">
<div className="sm:flex-auto">
<div className="p-4 sm:p-6 lg:p-8 max-w-none mx-auto space-y-8">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-6">
<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-12 bg-[#CE0515] rounded-full" />
<div>
<h1 className="text-3xl font-bold text-slate-900 dark:text-white tracking-tight">İzin Talepleri</h1>
<h1 className="text-3xl font-black text-[#173363] dark:text-white tracking-tight">İzin Yönetimi</h1>
<p className="text-sm font-medium text-slate-500 dark:text-slate-400 mt-1">
{isAdminOrManager
? 'Tüm personellerin izin taleplerini görüntüleyin ve onaylayın.'
: 'Kendi izin taleplerinizi oluşturun ve durumlarını takip edin.'}
{isAdminSomewhere
? 'Personel izin taleplerini yönetin.'
: 'İzin taleplerinizi oluşturun ve takip edin.'}
</p>
</div>
</div>
<div className="flex items-center gap-3">
<LeaveRequestForm
leaveTypes={leaveTypes || []}
companies={employeeRecords || []}
allEmployees={allEmployees || []}
/>
</div>
</div>
<div className="mt-8 grid grid-cols-1 md:grid-cols-4 gap-8">
{/* Modern Filtre Barı */}
<div className="bg-white dark:bg-zinc-900 rounded-[2.5rem] shadow-xl shadow-slate-200/50 dark:shadow-none border border-slate-100 dark:border-zinc-800 p-6">
<form className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="relative">
<input
type="text"
name="search"
defaultValue={searchFilter}
placeholder="Personel adı veya e-posta..."
className="w-full rounded-2xl border-none bg-slate-50 dark:bg-zinc-800 p-4 text-sm font-bold text-[#173363] dark:text-white ring-1 ring-slate-100 dark:ring-zinc-700 focus:ring-2 focus:ring-[#CE0515] transition-all outline-none"
/>
</div>
{/* Request form - only show if they are actually employed somewhere */}
<div className="md:col-span-1 border border-gray-200 dark:border-zinc-800 rounded-xl bg-white dark:bg-zinc-900 shadow-sm overflow-hidden h-fit">
<div className="px-4 py-5 sm:p-6">
<h3 className="text-base font-semibold leading-6 text-gray-900 dark:text-white">
Yeni İzin Talebi
</h3>
{(userCompanies && userCompanies.length > 0) ? (
<form action="/api/leave" method="POST" className="mt-5 space-y-4">
{/* We use a workaround API route or action for complex auth logic with FormData above */}
{/* For real production, we can inline the action. I'll mock the action call for now */}
<div>
<label htmlFor="company_id" className="block text-sm font-medium leading-6 text-gray-900 dark:text-gray-300">İlgili Şirket</label>
<select
id="company_id"
name="company_id"
required
className="mt-2 block w-full rounded-xl border-0 py-2.5 pl-3 pr-10 text-slate-900 ring-1 ring-inset ring-slate-200 dark:ring-zinc-700 dark:bg-zinc-800 dark:text-white focus:ring-2 focus:ring-indigo-500 sm:text-sm outline-none"
name="status"
defaultValue={statusFilter}
className="rounded-2xl border-none bg-slate-50 dark:bg-zinc-800 p-4 text-sm font-bold text-[#173363] dark:text-white ring-1 ring-slate-100 dark:ring-zinc-700 focus:ring-2 focus:ring-[#CE0515] transition-all outline-none"
>
{userCompanies?.filter((c: any) => c.companies).map((c: any) => (
<option key={c.companies.id} value={c.companies.id}>{c.companies.name}</option>
<option value="">Tüm Durumlar</option>
<option value="pending">Bekliyor</option>
<option value="approved">Onaylandı</option>
<option value="rejected">Reddedildi</option>
</select>
<select
name="type"
defaultValue={typeFilter}
className="rounded-2xl border-none bg-slate-50 dark:bg-zinc-800 p-4 text-sm font-bold text-[#173363] dark:text-white ring-1 ring-slate-100 dark:ring-zinc-700 focus:ring-2 focus:ring-[#CE0515] transition-all outline-none"
>
<option value="">Tüm İzin Türleri</option>
{leaveTypes?.map(t => (
<option key={t.id} value={t.id}>{t.name}</option>
))}
</select>
</div>
<div>
<label htmlFor="start_date" className="block text-sm font-medium leading-6 text-gray-900 dark:text-gray-300">Başlangıç Tarihi</label>
<input
type="date"
name="start_date"
id="start_date"
required
className="mt-2 block w-full rounded-md border-0 py-1.5 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-700 dark:bg-zinc-800 dark:text-white sm:text-sm sm:leading-6"
/>
</div>
<div>
<label htmlFor="end_date" className="block text-sm font-medium leading-6 text-gray-900 dark:text-gray-300">Bitiş Tarihi</label>
<input
type="date"
name="end_date"
id="end_date"
required
className="mt-2 block w-full rounded-md border-0 py-1.5 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-700 dark:bg-zinc-800 dark:text-white sm:text-sm sm:leading-6"
/>
</div>
<div>
<label htmlFor="reason" className="block text-sm font-medium leading-6 text-gray-900 dark:text-gray-300">Açıklama</label>
<textarea
name="reason"
id="reason"
rows={3}
required
className="mt-2 block w-full rounded-xl border-0 py-2.5 px-3 text-slate-900 shadow-sm ring-1 ring-inset ring-slate-200 dark:ring-zinc-700 dark:bg-zinc-800 dark:text-white placeholder:text-slate-400 focus:ring-2 focus:ring-inset focus:ring-indigo-500 sm:text-sm outline-none"
/>
</div>
<div className="flex gap-2">
<button
type="submit"
formAction={async (formData) => {
'use server';
const { submitLeaveRequest } = await import('./actions');
await submitLeaveRequest(formData);
}}
className="inline-flex w-full items-center justify-center rounded-2xl bg-indigo-600 px-3 py-3 text-sm font-bold text-white shadow-sm shadow-indigo-200 dark:shadow-none hover:bg-indigo-700 transition-all active:scale-95"
className="flex-1 rounded-2xl bg-[#173363] text-white px-6 py-4 text-sm font-black uppercase tracking-widest hover:bg-[#1e4585] transition-all"
>
Talep Oluştur
FİLTRELE
</button>
<a
href="/leave-requests"
className="flex items-center justify-center p-4 rounded-2xl bg-slate-100 text-slate-500 hover:bg-slate-200 transition-all"
title="Temizle"
>
<ArrowPathIcon className="h-5 w-5" />
</a>
</div>
</form>
) : (
<div className="mt-4 text-sm text-red-500">
Öncelikle bir şirkete atanmış olmanız gerekmektedir.
</div>
)}
</div>
</div>
{/* Requests List */}
<div className="md:col-span-3">
<div className="bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-gray-200 dark:ring-zinc-800 sm:rounded-xl overflow-hidden overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-zinc-800">
<thead className="bg-slate-50 dark:bg-zinc-950/50">
<tr>
<th scope="col" className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-slate-900 dark:text-white sm:pl-6">
Personel
</th>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-slate-900 dark:text-white">
Tarih Aralığı
</th>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-slate-900 dark:text-white">
Gerekçe
</th>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-slate-900 dark:text-white">
Durum
</th>
{isAdminOrManager && (
<th scope="col" className="relative py-3.5 pl-3 pr-4 sm:pr-6">
<span className="sr-only">İşlemler</span>
</th>
{/* Balance Summary Cards - Only for regular employees or managers viewing own balance */}
{!isAdminSomewhere && balances && balances.length > 0 && (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{balances.map((balance: any) => (
<div key={balance.id} className="bg-white dark:bg-zinc-900 p-6 rounded-[1.5rem] shadow-sm border border-slate-100 dark:border-zinc-800 group hover:shadow-xl transition-all duration-300">
<div className="flex items-center justify-between mb-4">
<span className="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-[0.2em]">
{balance.leave_types?.name}
</span>
<div className="p-2 bg-slate-50 dark:bg-zinc-800 rounded-lg group-hover:bg-red-50 dark:group-hover:bg-red-500/10 transition-colors">
<ClockIcon className="h-4 w-4 text-[#173363] group-hover:text-[#CE0515]" />
</div>
</div>
<div className="flex items-baseline gap-2">
<span className="text-4xl font-black text-[#173363] dark:text-white tracking-tighter">{balance.remaining_days}</span>
<span className="text-sm font-bold text-slate-400">gün kaldı</span>
</div>
<div className="mt-4 w-full bg-slate-100 dark:bg-zinc-800 rounded-full h-1.5 overflow-hidden">
<div
className="bg-[#CE0515] h-full rounded-full transition-all duration-1000"
style={{ width: `${Math.min(100, (balance.used_days / (balance.accrued_days || 1)) * 100)}%` }}
/>
</div>
<div className="mt-4 flex justify-between text-[10px] font-bold text-slate-400">
<span>{balance.used_days} KULLANILDI</span>
<span>{balance.accrued_days} TOPLAM</span>
</div>
</div>
))}
</div>
)}
{/* Requests List - Now Full Width */}
<div className="bg-white dark:bg-zinc-900 shadow-sm border border-slate-100 dark:border-zinc-800 rounded-[2rem] overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-slate-100 dark:divide-zinc-800">
<thead className="bg-slate-50/50 dark:bg-zinc-950/50">
<tr>
<th scope="col" className="py-5 pl-6 pr-3 text-left">
<span className="text-xs font-black text-slate-400 uppercase tracking-widest">Personel / Şirket</span>
</th>
<th scope="col" className="px-3 py-5 text-left">
<span className="text-xs font-black text-slate-400 uppercase tracking-widest">İzin Türü / Süre</span>
</th>
<th scope="col" className="px-3 py-5 text-left">
<span className="text-xs font-black text-slate-400 uppercase tracking-widest">Tarih Aralığı</span>
</th>
<th scope="col" className="px-3 py-5 text-left">
<span className="text-xs font-black text-slate-400 uppercase tracking-widest">Durum</span>
</th>
<th scope="col" className="relative py-5 pl-3 pr-6 text-right">
<span className="text-xs font-black text-slate-400 uppercase tracking-widest">İşlemler</span>
</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-200 dark:divide-zinc-800 bg-white dark:bg-zinc-900">
<tbody className="divide-y divide-slate-50 dark:divide-zinc-800 bg-white dark:bg-zinc-900">
{requests?.map((req: any) => {
const employeeData = Array.isArray(req.employees) ? req.employees[0] : req.employees;
const userData = Array.isArray(employeeData?.users) ? employeeData.users[0] : employeeData?.users;
const companyData = Array.isArray(employeeData?.companies) ? employeeData.companies[0] : employeeData?.companies;
const leaveType = (req.leave_type as any);
return (
<tr key={req.id} className="hover:bg-slate-50 dark:hover:bg-zinc-800/50 transition-colors">
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm sm:pl-6">
<div className="font-semibold text-slate-900 dark:text-white">
<tr key={req.id} className="hover:bg-slate-50/50 dark:hover:bg-zinc-800/50 transition-colors group">
<td className="whitespace-nowrap py-6 pl-6 pr-3">
<div className="flex flex-col">
<span className="font-bold text-[#173363] dark:text-white group-hover:text-[#CE0515] transition-colors">
{userData?.first_name} {userData?.last_name}
</span>
<span className="text-[10px] font-bold text-slate-400 mt-0.5">{companyData?.name}</span>
</div>
<div className="text-slate-500 dark:text-slate-400">{companyData?.name}</div>
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-slate-500 dark:text-slate-400">
<div>{new Date(req.start_date).toLocaleDateString('tr-TR')}</div>
<div>{new Date(req.end_date).toLocaleDateString('tr-TR')}</div>
<td className="whitespace-nowrap px-3 py-6">
<div className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full" style={{ backgroundColor: leaveType?.color_code || '#cbd5e1' }} />
<span className="font-bold text-slate-700 dark:text-slate-200">{leaveType?.name || 'Belirsiz'}</span>
</div>
<div className="text-[10px] text-slate-500 font-black mt-1 uppercase">
{req.total_days || Math.ceil((new Date(req.end_date).getTime() - new Date(req.start_date).getTime()) / (1000 * 60 * 60 * 24)) + 1} GÜN
</div>
</td>
<td className="px-3 py-4 text-sm text-slate-500 dark:text-slate-400 max-w-xs truncate font-medium">
{req.reason}
<td className="whitespace-nowrap px-3 py-6 text-sm">
<div className="font-bold text-[#173363] dark:text-slate-300">{new Date(req.start_date).toLocaleDateString('tr-TR')}</div>
<div className="text-[10px] text-slate-400 font-medium">Bitiş: {new Date(req.end_date).toLocaleDateString('tr-TR')}</div>
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm">
<span className={`inline-flex items-center rounded-lg px-2.5 py-1 text-xs font-bold ${
<td className="whitespace-nowrap px-3 py-6">
<span className={`inline-flex items-center rounded-xl px-3 py-1.5 text-[10px] font-black uppercase tracking-widest ${
req.status === 'approved' ? 'bg-emerald-50 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-400' :
req.status === 'rejected' ? 'bg-rose-50 text-rose-700 dark:bg-rose-500/10 dark:text-rose-400' :
'bg-slate-100 text-slate-700 dark:bg-zinc-800 dark:text-slate-400'
'bg-slate-100 text-slate-600 dark:bg-zinc-800 dark:text-slate-500'
}`}>
{req.status === 'approved' ? 'Onaylandı' : req.status === 'rejected' ? 'Reddedildi' : 'Bekliyor'}
</span>
</td>
{isAdminOrManager && (
<td className="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
{req.status === 'pending' && (
<form className="flex justify-end gap-2">
<td className="relative whitespace-nowrap py-6 pl-3 pr-6 text-right font-medium">
<div className="flex justify-end gap-2 flex-wrap max-w-xs ml-auto">
<LeaveRequestForm
leaveTypes={leaveTypes || []}
companies={employeeRecords || []}
allEmployees={allEmployees || []}
initialData={req}
readOnly={req.status !== 'pending'}
trigger={
<button className="flex items-center gap-2 px-4 py-2.5 rounded-xl bg-slate-50 dark:bg-zinc-800 text-slate-500 hover:text-[#173363] dark:hover:text-white border border-slate-100 dark:border-zinc-700 transition-all active:scale-95 text-[10px] font-black uppercase tracking-widest" title="Detay / Düzenle">
<PencilIcon className="h-4 w-4" />
{req.status === 'pending' ? 'DÜZENLE' : 'DETAY'}
</button>
}
/>
{isAdminSomewhere && req.status === 'pending' && (
<div className="flex gap-2">
<form>
<button
formAction={async () => {
'use server';
await updateLeaveStatus(req.id, 'approved')
}}
className="text-emerald-600 hover:text-emerald-900 dark:text-emerald-500 dark:hover:text-emerald-400 p-2 rounded-xl hover:bg-emerald-50 dark:hover:bg-emerald-500/10 transition-all" title="Onayla">
<CheckIcon className="h-5 w-5" />
className="flex items-center gap-2 bg-emerald-50 text-emerald-600 hover:bg-emerald-600 hover:text-white px-4 py-2.5 rounded-xl border border-emerald-100 transition-all active:scale-95 text-[10px] font-black uppercase tracking-widest" title="Onayla">
<CheckIcon className="h-4 w-4" />
ONAYLA
</button>
</form>
<form>
<button
formAction={async () => {
'use server';
await updateLeaveStatus(req.id, 'rejected')
}}
className="text-rose-600 hover:text-rose-900 dark:text-rose-500 dark:hover:text-rose-400 p-2 rounded-xl hover:bg-rose-50 dark:hover:bg-rose-500/10 transition-all" title="Reddet">
<XMarkIcon className="h-5 w-5" />
className="flex items-center gap-2 bg-rose-50 text-rose-600 hover:bg-rose-600 hover:text-white px-4 py-2.5 rounded-xl border border-rose-100 transition-all active:scale-95 text-[10px] font-black uppercase tracking-widest" title="Reddet">
<XMarkIcon className="h-4 w-4" />
REDDET
</button>
</form>
</div>
)}
{isAdminSomewhere && (req.status === 'approved' || req.status === 'rejected') && (
<form>
<button
formAction={async () => {
'use server';
await resetLeaveStatus(req.id)
}}
className="flex items-center gap-2 bg-amber-50 text-amber-600 hover:bg-amber-700 hover:text-white px-4 py-2.5 rounded-xl border border-amber-200 transition-all active:scale-95 text-[10px] font-black uppercase tracking-widest"
title={req.status === 'approved' ? 'Onayı Kaldır' : 'Reddi Kaldır'}
>
<ArrowPathIcon className="h-4 w-4" />
{req.status === 'approved' ? 'ONAYI KALDIR' : 'REDDİ KALDIR'}
</button>
</form>
)}
</td>
{!isAdminSomewhere && req.status !== 'pending' && (
<div className="flex items-center px-4 py-2.5">
<span className="text-[10px] font-black text-slate-300 uppercase tracking-widest italic flex items-center gap-2">
<ClockIcon className="h-4 w-4" /> TAMAMLANDI
</span>
</div>
)}
</div>
</td>
</tr>
)})}
{(!requests || requests.length === 0) && (
{(!filteredRequests || filteredRequests.length === 0) && (
<tr>
<td colSpan={isAdminOrManager ? 5 : 4} className="py-12 text-center text-sm text-slate-500 dark:text-slate-400">
Henüz bir izin talebi bulunmamaktadır.
<td colSpan={5} className="py-20 text-center">
<div className="flex flex-col items-center">
<CalendarDaysIcon className="h-12 w-12 text-slate-200 mb-4" />
<span className="text-sm font-bold text-slate-400 uppercase tracking-widest">Henüz bir izin talebi bulunmamaktadır.</span>
</div>
</td>
</tr>
)}
@@ -241,8 +366,6 @@ export default async function LeaveRequestsPage() {
</table>
</div>
</div>
</div>
</div>
)
}
+147 -67
View File
@@ -1,6 +1,7 @@
import { createClient } from '@/utils/supabase/server'
import { redirect } from 'next/navigation'
import { UsersIcon, CalendarDaysIcon, BuildingOfficeIcon } from '@heroicons/react/24/outline'
import Link from 'next/link'
import { UsersIcon, CalendarDaysIcon, BuildingOfficeIcon, ClockIcon } from '@heroicons/react/24/outline'
export default async function Home() {
const supabase = await createClient()
@@ -11,119 +12,198 @@ export default async function Home() {
redirect('/login')
}
// Deep debug: Fetch users and employees separately
const { data: dbUser, error: dbUserError } = await supabase
// Fetch users and employees
const { data: dbUser } = await supabase
.from('users')
.select('*')
.eq('id', user.id)
.single()
const { data: dbEmployees, error: dbEmpError } = await supabase
const { data: dbEmployees } = await supabase
.from('employees')
.select('*, roles(*)')
.eq('user_id', user.id)
const userData = dbUser
const role = dbEmployees?.[0]?.roles || null
const displayName = userData?.first_name
? `${userData.first_name}${userData.last_name ? ' ' + userData.last_name : ''}`
const isAdminOrManager = dbEmployees?.some((emp: any) => {
const roleName = Array.isArray(emp.roles) ? emp.roles[0]?.name : emp.roles?.name
return roleName === 'admin' || roleName === 'manager'
}) || false
const role = dbEmployees?.[0]?.roles || null // Display first role as default label
const displayName = dbUser?.first_name
? `${dbUser.first_name}${dbUser.last_name ? ' ' + dbUser.last_name : ''}`
: user.email?.split('@')[0]
// Data fetching for stats
const { count: employeeCount } = await supabase.from('employees').select('*', { count: 'exact', head: true })
const { count: companyCount } = await supabase.from('companies').select('*', { count: 'exact', head: true })
let pendingQuery = supabase.from('leave_requests').select('*', { count: 'exact', head: true }).eq('status', 'pending')
if (!isAdminOrManager) {
pendingQuery = pendingQuery.eq('employee_id', dbEmployees?.[0]?.id)
}
const { count: pendingLeaveCount } = await pendingQuery
// Fetch remaining annual leave for current user
let remainingAnnualLeave = 0
if (!isAdminOrManager && dbEmployees?.[0]) {
const { data: balanceData } = await supabase
.from('leave_balances')
.select('remaining_days, leave_types!inner(name)')
.eq('employee_id', dbEmployees[0].id)
.eq('year', new Date().getFullYear())
.eq('leave_types.name', 'Yıllık İzin')
.maybeSingle();
remainingAnnualLeave = balanceData?.remaining_days || 0
}
const stats = [
{ name: 'Toplam Personel', stat: '0', icon: UsersIcon, change: '12%', changeType: 'increase' },
{ name: 'Bekleyen İzinler', stat: '0', icon: CalendarDaysIcon, change: '3', changeType: 'increase' },
{ name: 'Aktif Şirketler', stat: '1', icon: BuildingOfficeIcon, change: '0%', changeType: 'none' },
{
name: isAdminOrManager ? 'Toplam Personel' : 'Şirket Personelleri',
stat: isAdminOrManager ? (employeeCount || 0).toString() : (employeeCount || 0).toString(),
icon: UsersIcon,
href: '/employees',
change: '',
changeType: 'none'
},
{
name: isAdminOrManager ? 'Bekleyen İzinler' : 'Bekleyen Taleplerim',
stat: (pendingLeaveCount || 0).toString(),
icon: CalendarDaysIcon,
href: '/leave-requests',
change: '',
changeType: 'none'
},
{
name: isAdminOrManager ? 'Aktif Şirketler' : 'Kalan Yıllık İzin',
stat: isAdminOrManager ? (companyCount || 0).toString() : `${remainingAnnualLeave} Gün`,
icon: isAdminOrManager ? BuildingOfficeIcon : ClockIcon,
href: isAdminOrManager ? '/companies' : '/leave-requests',
change: '',
changeType: 'none'
},
]
return (
<div className="p-4 sm:p-6 lg:p-8 space-y-8 max-w-7xl mx-auto">
{/* Welcome Section - Modern & Clean */}
<div className="relative overflow-hidden rounded-[2rem] bg-[#173363] p-10 shadow-xl shadow-blue-900/10">
{/* Welcome Section */}
<div className="relative overflow-hidden rounded-[2.5rem] bg-[#173363] p-10 shadow-2xl shadow-blue-900/20">
<div className="relative z-10 flex flex-col md:flex-row md:items-center justify-between gap-6">
<div>
<h1 className="text-4xl font-black tracking-tight text-white mb-3">
<div className="animate-in fade-in slide-in-from-left duration-700">
<h1 className="text-4xl md:text-5xl font-black tracking-tighter text-white mb-4">
Hoş Geldiniz, {displayName} 👋
</h1>
<p className="max-w-xl text-blue-100/80 font-medium leading-relaxed text-lg">
IK Yönetim Sistemine tekrar hoş geldiniz. Bugün sisteme
<span className="text-white font-black px-3 py-1 bg-[#CE0515] rounded-full mx-2 text-sm uppercase tracking-wider shadow-lg shadow-red-900/20">
{role?.name || 'Kullanıcı'}
<p className="max-w-xl text-blue-100/70 font-medium leading-relaxed text-lg">
HRMS Projesi kapsamında bugün sisteme
<span className="text-white font-black px-4 py-1.5 bg-[#CE0515] rounded-full mx-2 text-xs uppercase tracking-widest shadow-lg shadow-red-900/30">
{(role as any)?.name || 'Kullanıcı'}
</span>
yetkileriyle bağlısınız.
yetkileriyle erişim sağladınız.
</p>
</div>
<div className="hidden lg:flex -space-x-3">
{[1,2,3,4].map(i => (
<div key={i} className="w-14 h-14 rounded-full border-4 border-[#1e40af] bg-blue-800 shadow-inner" />
))}
<div className="w-14 h-14 rounded-full border-4 border-[#1e40af] bg-[#CE0515] flex items-center justify-center text-sm font-black text-white shadow-lg shadow-red-900/20 tracking-tighter">+12</div>
<div className="hidden lg:flex -space-x-4 items-center animate-in fade-in slide-in-from-right duration-1000">
<div className="px-6 py-3 bg-white/5 backdrop-blur-md rounded-2xl border border-white/10 text-white font-bold text-sm tracking-widest uppercase">
{new Date().toLocaleDateString('tr-TR', { weekday: 'long' })}
</div>
</div>
</div>
{/* Corporate decorative element */}
<div className="absolute top-0 right-0 w-64 h-64 bg-white/5 rounded-full -mr-32 -mt-32 blur-3xl" />
<div className="absolute bottom-0 left-0 w-48 h-48 bg-[#CE0515]/10 rounded-full -ml-24 -mb-24 blur-2xl" />
<div className="absolute top-0 right-0 w-96 h-96 bg-white/5 rounded-full -mr-48 -mt-48 blur-3xl opacity-50" />
<div className="absolute bottom-0 left-0 w-64 h-64 bg-[#CE0515]/10 rounded-full -ml-32 -mb-32 blur-2xl opacity-40" />
</div>
{/* Stats Grid - abisena.tr Style */}
{/* Stats Grid */}
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
{stats.map((item) => (
<div
{stats.map((item, idx) => (
<Link
key={item.name}
className="group relative flex flex-col justify-between overflow-hidden rounded-3xl bg-white border border-slate-100 p-8 shadow-sm hover:shadow-xl hover:-translate-y-1 transition-all duration-500"
href={item.href}
className="group relative flex flex-col justify-between overflow-hidden rounded-[2rem] bg-white border border-slate-100 p-8 shadow-sm hover:shadow-2xl hover:-translate-y-2 transition-all duration-500 animate-in fade-in slide-in-from-bottom"
style={{ animationDelay: `${idx * 150}ms` }}
>
<div>
<div className="flex items-center justify-between mb-6">
<div className="rounded-2xl bg-slate-50 p-4 text-[#173363] group-hover:bg-[#CE0515] group-hover:text-white transition-all duration-500 rotate-3 group-hover:rotate-0 shadow-inner">
<item.icon className="h-7 w-7" aria-hidden="true" />
</div>
<div className={`text-xs font-black leading-5 px-3 py-1.5 rounded-full tracking-wider uppercase ${
item.changeType === 'increase' ? 'bg-emerald-50 text-emerald-700' : 'bg-slate-50 text-slate-500'
}`}>
{item.changeType === 'increase' ? '↑' : ''} {item.change}
<div className="flex items-center justify-between mb-8">
<div className="rounded-2xl bg-slate-50 p-4 text-[#173363] group-hover:bg-[#CE0515] group-hover:text-white transition-all duration-500 group-hover:rotate-12 shadow-sm">
<item.icon className="h-8 w-8" aria-hidden="true" />
</div>
</div>
<p className="text-xs font-black text-slate-400 uppercase tracking-[0.2em] mb-1">{item.name}</p>
<p className="text-4xl font-black text-[#173363] tracking-tighter">{item.stat}</p>
</div>
{/* Subtle progress bar */}
<div className="mt-6 w-full h-1.5 bg-slate-50 rounded-full overflow-hidden">
<div className="h-full bg-[#173363] group-hover:bg-[#CE0515] transition-all duration-700 w-2/3" />
<p className="text-[10px] font-black text-slate-400 uppercase tracking-[0.25em] mb-2">{item.name}</p>
<p className="text-5xl font-black text-[#173363] tracking-tighter group-hover:text-[#CE0515] transition-colors duration-500">
{item.stat}
</p>
</div>
<div className="mt-8 w-full h-1.5 bg-slate-50 rounded-full overflow-hidden">
<div
className="h-full bg-[#173363] group-hover:bg-[#CE0515] transition-all duration-1000 ease-out fill-mode-both"
style={{ width: '40%' }} // Placeholder progress
/>
</div>
</Link>
))}
</div>
{/* Profile & Quick Actions section */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div className="bg-white border border-slate-100 rounded-[2rem] p-10 shadow-sm hover:shadow-md transition-shadow duration-500">
<h2 className="text-xl font-black mb-8 text-[#173363] flex items-center gap-3">
<div className="w-2 h-8 bg-[#CE0515] rounded-full" />
{/* Account Info Card */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className="lg:col-span-2 bg-white border border-slate-100 rounded-[2.5rem] p-10 shadow-sm hover:shadow-xl transition-all duration-500">
<div className="flex items-center justify-between mb-10">
<h2 className="text-2xl font-black text-[#173363] flex items-center gap-4">
<div className="w-2.5 h-10 bg-[#CE0515] rounded-full shadow-lg shadow-red-500/20" />
Sistem Bilgileri
</h2>
<ul className="space-y-6 text-sm">
<li className="flex items-center justify-between border-b border-slate-50 pb-6">
<span className="font-bold text-slate-400 uppercase tracking-widest text-[10px]">E-posta Adresi</span>
<span className="text-[#173363] font-black">{user.email}</span>
</li>
<li className="flex items-center justify-between border-b border-slate-50 pb-6">
<span className="font-bold text-slate-400 uppercase tracking-widest text-[10px]">Hesap Yetkisi</span>
<span className="inline-flex items-center gap-2 text-[#CE0515] font-black bg-rose-50 px-4 py-2 rounded-full text-xs shadow-sm shadow-rose-100 transition-transform hover:scale-105 cursor-default">
{role?.description || 'Standart Erişim'}
<div className="text-[10px] font-black text-slate-400 uppercase tracking-widest">
ID: {user.id.slice(0, 8)}...
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-12">
<div className="space-y-8">
<div className="group cursor-default">
<p className="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1.5 group-hover:text-[#CE0515] transition-colors">E-posta Adresi</p>
<p className="text-[#173363] font-black text-lg">{user.email}</p>
</div>
<div className="group cursor-default">
<p className="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1.5 group-hover:text-[#CE0515] transition-colors">Hesap Yetkisi</p>
<span className="inline-flex items-center gap-3 text-[#CE0515] font-black bg-rose-50 px-5 py-2.5 rounded-2xl text-xs shadow-sm shadow-rose-100 transition-all hover:bg-[#CE0515] hover:text-white hover:scale-105">
<div className="w-2 h-2 rounded-full bg-current animate-pulse" />
{(role as any)?.description || 'Sistem Erişimi'}
</span>
</li>
<li className="flex items-center justify-between pt-2">
<span className="font-bold text-slate-400 uppercase tracking-widest text-[10px]">Sistem Tarihi</span>
<span className="text-[#173363] font-black">{new Date().toLocaleDateString('tr-TR', { day: 'numeric', month: 'long', year: 'numeric' })}</span>
</li>
</ul>
</div>
</div>
<div className="space-y-8">
<div className="group cursor-default">
<p className="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1.5 group-hover:text-[#CE0515] transition-colors">Son Giriş</p>
<p className="text-[#173363] font-black text-lg">
{new Date(user.last_sign_in_at!).toLocaleDateString('tr-TR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' })}
</p>
</div>
<div className="group cursor-default">
<p className="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1.5 group-hover:text-[#CE0515] transition-colors">Sistem Tarihi</p>
<p className="text-[#173363] font-black text-lg">
{new Date().toLocaleDateString('tr-TR', { day: 'numeric', month: 'long', year: 'numeric' })}
</p>
</div>
</div>
</div>
</div>
{/* Quick Links Placeholder */}
<div className="bg-slate-50 border border-slate-100 rounded-[2.5rem] p-10 flex flex-col justify-center items-center text-center">
<div className="w-16 h-16 bg-white rounded-2xl shadow-sm text-[#173363] flex items-center justify-center mb-6">
<CalendarDaysIcon className="h-8 w-8" />
</div>
<h3 className="text-xl font-black text-[#173363] mb-2">Hızlı İşlemler</h3>
<p className="text-sm text-slate-500 font-medium mb-8">İzin talebi oluşturmak veya yönetmek için tıklayın.</p>
<a href="/leave-requests" className="w-full py-4 bg-[#173363] text-white rounded-2xl font-black text-sm uppercase tracking-widest hover:bg-[#CE0515] transition-all hover:shadow-xl hover:shadow-red-900/20 active:scale-95">
İzin Yönetimi
</a>
</div>
</div>
</div>
);
}
+332
View File
@@ -0,0 +1,332 @@
'use client'
import { useState, Fragment } from 'react'
import { CalendarDaysIcon, XMarkIcon, UserIcon, BuildingOfficeIcon, TagIcon, ChatBubbleBottomCenterTextIcon, PencilIcon } from '@heroicons/react/24/outline'
import { submitLeaveRequest, updateLeaveRequest } from '@/app/leave-requests/actions'
interface LeaveRequestFormProps {
leaveTypes: any[]
companies: any[]
allEmployees?: any[] // Only for admins
initialData?: any // For edit mode
readOnly?: boolean // For approved view mode
trigger?: React.ReactNode // Custom trigger button
}
export default function LeaveRequestForm({ leaveTypes, companies, allEmployees, initialData, readOnly = false, trigger }: LeaveRequestFormProps) {
const [isOpen, setIsOpen] = useState(false)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [selectedCompanyId, setSelectedCompanyId] = useState(initialData?.leave_types?.company_id || initialData?.employees?.company_id || companies[0]?.company_id || companies[0]?.companies?.id || '')
// Calculate if user is admin/manager for the currently selected company
const currentCompanyRole = companies.find(c => (c.company_id || c.companies?.id) === selectedCompanyId)?.roles?.name;
const isAdminOrManager = currentCompanyRole === 'admin' || currentCompanyRole === 'manager';
const [searchTerm, setSearchTerm] = useState(initialData ? `${initialData.employees?.users?.first_name} ${initialData.employees?.users?.last_name}` : '')
const [selectedEmployeeId, setSelectedEmployeeId] = useState(initialData?.employee_id || '')
const [selectedEmployeeName, setSelectedEmployeeName] = useState(initialData ? `${initialData.employees?.users?.first_name} ${initialData.employees?.users?.last_name}` : '')
const [showEmployeeResults, setShowEmployeeResults] = useState(false)
const [startDate, setStartDate] = useState(initialData?.start_date || '')
const [endDate, setEndDate] = useState(initialData?.end_date || '')
// Calculate business days (excluding weekends)
const calculateDays = (start: string, end: string) => {
if (!start || !end) return 0
const s = new Date(start)
const e = new Date(end)
if (e < s) return 0
let count = 0
let current = new Date(s)
while (current <= e) {
const dayOfWeek = current.getDay()
if (dayOfWeek !== 0 && dayOfWeek !== 6) count++
current.setDate(current.getDate() + 1)
}
return count
}
const totalDays = calculateDays(startDate, endDate)
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (readOnly) return;
setLoading(true)
setError(null)
const formData = new FormData(e.currentTarget)
if (initialData?.id) {
formData.append('id', initialData.id)
}
const result = initialData ? await updateLeaveRequest(formData) : await submitLeaveRequest(formData)
if (result?.error) {
setError(result.error)
setLoading(false)
} else {
if (!initialData) {
setSearchTerm('')
setSelectedEmployeeId('')
setSelectedEmployeeName('')
}
setIsOpen(false)
setLoading(false)
}
}
// Filter employees by selected company AND search term
const filteredEmployees = allEmployees?.filter(emp => {
const matchesCompany = emp.company_id === selectedCompanyId
const fullName = `${emp.users.first_name} ${emp.users.last_name}`.toLowerCase()
const matchesSearch = fullName.includes(searchTerm.toLowerCase())
return matchesCompany && matchesSearch
})
return (
<>
{trigger ? (
<div onClick={() => setIsOpen(true)}>{trigger}</div>
) : (
<button
onClick={() => setIsOpen(true)}
className="inline-flex items-center gap-2 rounded-2xl bg-[#CE0515] px-6 py-3 text-sm font-bold text-white shadow-lg shadow-red-100 hover:bg-[#b00412] transition-all active:scale-95 group"
>
<CalendarDaysIcon className="h-5 w-5 group-hover:rotate-12 transition-transform" />
Yeni İzin Talebi
</button>
)}
{isOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
{/* Overlay */}
<div
className="absolute inset-0 bg-slate-900/60 backdrop-blur-sm animate-in fade-in duration-300"
onClick={() => !loading && setIsOpen(false)}
/>
{/* Modal Content */}
<div className="relative w-full max-w-lg bg-white dark:bg-zinc-900 rounded-[2.5rem] shadow-2xl border border-slate-100 dark:border-zinc-800 p-8 animate-in zoom-in-95 slide-in-from-bottom-10 duration-500 overflow-hidden">
<div className="absolute top-0 right-0 p-6">
<button
onClick={() => setIsOpen(false)}
className="p-2 rounded-xl text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 hover:bg-slate-50 dark:hover:bg-zinc-800 transition-colors"
>
<XMarkIcon className="h-6 w-6" />
</button>
</div>
<div className="mb-8">
<h2 className="text-2xl font-black text-[#173363] dark:text-white tracking-tight flex items-center gap-3">
<div className="p-2.5 bg-red-50 dark:bg-red-500/10 rounded-2xl">
<CalendarDaysIcon className="h-6 w-6 text-[#CE0515]" />
</div>
{readOnly ? 'İzin Talebi Detayı' : (initialData ? 'İzin Talebini Düzenle' : 'İzin Talebi Oluştur')}
</h2>
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400 font-medium">
{readOnly ? 'Onaylanmış izinler üzerinde değişiklik yapılamaz.' : 'Lütfen izin detaylarını eksiksiz doldurunuz.'}
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
{error && (
<div className="p-4 rounded-2xl bg-red-50 dark:bg-red-500/10 text-sm text-[#CE0515] border border-red-100 dark:border-red-500/20 font-bold">
{error}
</div>
)}
<div className="grid grid-cols-1 gap-6">
{/* Şirket Seçimi */}
<div className="space-y-2">
<label htmlFor="company_id" className="text-xs font-black text-slate-400 uppercase tracking-widest flex items-center gap-2">
<BuildingOfficeIcon className="h-3.5 w-3.5" /> Şirket
</label>
<select
id="company_id"
name="company_id"
required
disabled={loading || readOnly || !!initialData}
value={selectedCompanyId}
onChange={(e) => setSelectedCompanyId(e.target.value)}
className="w-full rounded-2xl border-none bg-slate-50 dark:bg-zinc-800 p-4 text-sm font-bold text-[#173363] dark:text-white ring-1 ring-slate-100 dark:ring-zinc-700 focus:ring-2 focus:ring-[#CE0515] transition-all outline-none disabled:opacity-50"
>
{companies.map((emp: any) => (
<option key={emp.company_id || emp.companies?.id} value={emp.company_id || emp.companies?.id}>
{emp.companies?.name || emp.name}
</option>
))}
</select>
</div>
{/* Personel Seçimi (Sadece Yönetici) */}
{isAdminOrManager && allEmployees && (
<div className="space-y-2 relative">
<label className="text-xs font-black text-slate-400 uppercase tracking-widest flex items-center gap-2">
<UserIcon className="h-3.5 w-3.5" /> Personel Arama
</label>
<div className="relative">
<input
type="text"
placeholder="Personel adı veya soyadı ile arayın..."
autoComplete="off"
disabled={loading || readOnly || !!initialData}
value={searchTerm || selectedEmployeeName}
onFocus={() => {
if (loading || readOnly || initialData) return
setShowEmployeeResults(true)
setSelectedEmployeeName('')
}}
onChange={(e) => {
setSearchTerm(e.target.value)
setShowEmployeeResults(true)
}}
className="w-full rounded-2xl border-none bg-slate-50 dark:bg-zinc-800 p-4 text-sm font-bold text-[#173363] dark:text-white ring-1 ring-slate-100 dark:ring-zinc-700 focus:ring-2 focus:ring-[#CE0515] transition-all outline-none disabled:opacity-50"
/>
<input type="hidden" name="employee_id" value={selectedEmployeeId} />
{showEmployeeResults && !readOnly && !initialData && (searchTerm.length > 0 || filteredEmployees?.length) && (
<div className="absolute z-10 mt-2 w-full max-h-60 overflow-auto rounded-2xl bg-white dark:bg-zinc-900 shadow-2xl border border-slate-100 dark:border-zinc-800 animate-in fade-in slide-in-from-top-2 duration-200">
<div
className="p-3 hover:bg-slate-50 dark:hover:bg-zinc-800 cursor-pointer transition-colors border-b border-slate-50 dark:border-zinc-800"
onClick={() => {
setSelectedEmployeeId('')
setSelectedEmployeeName('Kendim İçin')
setSearchTerm('')
setShowEmployeeResults(false)
}}
>
<span className="text-sm font-bold text-slate-400 italic">Kendim İçin</span>
</div>
{filteredEmployees?.map((emp: any) => (
<div
key={emp.id}
className="p-3 hover:bg-slate-50 dark:hover:bg-zinc-800 cursor-pointer transition-colors"
onClick={() => {
setSelectedEmployeeId(emp.id)
setSelectedEmployeeName(`${emp.users.first_name} ${emp.users.last_name}`)
setSearchTerm(`${emp.users.first_name} ${emp.users.last_name}`)
setShowEmployeeResults(false)
}}
>
<div className="text-sm font-bold text-[#173363] dark:text-white">
{emp.users.first_name} {emp.users.last_name}
</div>
</div>
))}
{filteredEmployees?.length === 0 && searchTerm.length > 0 && (
<div className="p-4 text-center text-xs text-slate-400 font-bold uppercase tracking-widest">
Sonuç bulunamadı
</div>
)}
</div>
)}
</div>
</div>
)}
{/* İzin Türü */}
<div className="space-y-2">
<label htmlFor="leave_type_id" className="text-xs font-black text-slate-400 uppercase tracking-widest flex items-center gap-2">
<TagIcon className="h-3.5 w-3.5" /> İzin Türü
</label>
<select
id="leave_type_id"
name="leave_type_id"
required
disabled={loading || readOnly}
defaultValue={initialData?.leave_type_id}
className="w-full rounded-2xl border-none bg-slate-50 dark:bg-zinc-800 p-4 text-sm font-bold text-[#173363] dark:text-white ring-1 ring-slate-100 dark:ring-zinc-700 focus:ring-2 focus:ring-[#CE0515] transition-all outline-none disabled:opacity-50"
>
{leaveTypes.filter(t => !t.only_admin_can_create || isAdminOrManager).map((t: any) => (
<option key={t.id} value={t.id}>{t.name}</option>
))}
</select>
</div>
{/* Tarihler */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label htmlFor="start_date" className="text-xs font-black text-slate-400 uppercase tracking-widest flex items-center gap-2">
Başlangıç
</label>
<input
type="date"
name="start_date"
id="start_date"
required
disabled={loading || readOnly}
defaultValue={initialData?.start_date}
className="w-full rounded-2xl border-none bg-slate-50 dark:bg-zinc-800 p-4 text-sm font-bold text-[#173363] dark:text-white ring-1 ring-slate-100 dark:ring-zinc-700 focus:ring-2 focus:ring-[#CE0515] transition-all outline-none disabled:opacity-50"
onChange={(e) => setStartDate(e.target.value)}
/>
</div>
<div className="space-y-2">
<label htmlFor="end_date" className="text-xs font-black text-slate-400 uppercase tracking-widest flex items-center gap-2">
Bitiş
</label>
<input
type="date"
name="end_date"
id="end_date"
required
disabled={loading || readOnly}
defaultValue={initialData?.end_date}
className="w-full rounded-2xl border-none bg-slate-50 dark:bg-zinc-800 p-4 text-sm font-bold text-[#173363] dark:text-white ring-1 ring-slate-100 dark:ring-zinc-700 focus:ring-2 focus:ring-[#CE0515] transition-all outline-none disabled:opacity-50"
onChange={(e) => setEndDate(e.target.value)}
/>
</div>
</div>
{/* Gün Sayısı Göstergesi */}
{totalDays > 0 && (
<div className="flex items-center justify-between px-6 py-4 rounded-2xl bg-slate-50 dark:bg-zinc-800/50 border border-slate-100 dark:border-zinc-800 animate-in fade-in zoom-in-95 duration-300">
<span className="text-sm font-bold text-slate-500 dark:text-slate-400">Tahmini İzin Süresi:</span>
<div className="flex items-center gap-2">
<span className="text-lg font-black text-[#CE0515]">{totalDays}</span>
<span className="text-xs font-black text-[#173363] dark:text-slate-300 uppercase tracking-widest">İş Günü</span>
</div>
</div>
)}
{/* Açıklama */}
<div className="space-y-2">
<label htmlFor="reason" className="text-xs font-black text-slate-400 uppercase tracking-widest flex items-center gap-2">
<ChatBubbleBottomCenterTextIcon className="h-3.5 w-3.5" /> Açıklama
</label>
<textarea
name="reason"
id="reason"
rows={3}
required
disabled={loading || readOnly}
defaultValue={initialData?.reason}
placeholder="İzin nedeninizi belirtiniz..."
className="w-full rounded-2xl border-none bg-slate-50 dark:bg-zinc-800 p-4 text-sm font-bold text-[#173363] dark:text-white ring-1 ring-slate-100 dark:ring-zinc-700 focus:ring-2 focus:ring-[#CE0515] transition-all outline-none resize-none disabled:opacity-50"
/>
</div>
</div>
{!readOnly && (
<div className="pt-2">
<button
type="submit"
disabled={loading}
className="w-full inline-flex items-center justify-center rounded-[1.25rem] bg-[#173363] px-6 py-4 text-sm font-black text-white shadow-xl shadow-blue-900/10 hover:bg-[#1e4585] transition-all active:scale-95 disabled:opacity-50 disabled:active:scale-100"
>
{loading ? (
<div className="flex items-center gap-2">
<div className="w-4 h-4 border-2 border-white/20 border-t-white rounded-full animate-spin" />
Gönderiliyor...
</div>
) : (initialData ? 'Değişiklikleri Kaydet' : 'Talep Oluştur')}
</button>
</div>
)}
</form>
</div>
</div>
)}
</>
)
}
@@ -0,0 +1,55 @@
'use client'
import { useState } from 'react'
import { BuildingOfficeIcon, XMarkIcon, PlusIcon } from '@heroicons/react/24/outline'
import CompanyForm from './CompanyForm'
export default function AddCompanyModal() {
const [isOpen, setIsOpen] = useState(false)
return (
<>
<button
onClick={() => setIsOpen(true)}
className="inline-flex items-center gap-2 rounded-2xl bg-[#CE0515] px-6 py-3 text-sm font-bold text-white shadow-lg shadow-red-100 hover:bg-[#b00412] transition-all active:scale-95 group"
>
<PlusIcon className="h-5 w-5 group-hover:rotate-90 transition-transform duration-300" />
Yeni Şirket Ekle
</button>
{isOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
{/* Overlay */}
<div
className="absolute inset-0 bg-slate-900/60 backdrop-blur-sm animate-in fade-in duration-300"
onClick={() => setIsOpen(false)}
/>
{/* Modal Content */}
<div className="relative w-full max-w-md bg-white dark:bg-zinc-900 rounded-[2.5rem] shadow-2xl border border-slate-100 dark:border-zinc-800 p-8 animate-in zoom-in-95 slide-in-from-bottom-10 duration-500 overflow-hidden">
<div className="absolute top-0 right-0 p-6">
<button
onClick={() => setIsOpen(false)}
className="p-2 rounded-xl text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 hover:bg-slate-50 dark:hover:bg-zinc-800 transition-colors"
>
<XMarkIcon className="h-6 w-6" />
</button>
</div>
<div className="mb-8">
<h2 className="text-2xl font-black text-[#173363] dark:text-white tracking-tight flex items-center gap-3">
<div className="p-2.5 bg-red-50 dark:bg-red-500/10 rounded-2xl">
<BuildingOfficeIcon className="h-6 w-6 text-[#CE0515]" />
</div>
Şirket Kaydı
</h2>
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400 font-medium">Lütfen eklemek istediğiniz şirketin adını giriniz.</p>
</div>
<CompanyForm onSuccess={() => setIsOpen(false)} />
</div>
</div>
)}
</>
)
}
+32 -16
View File
@@ -1,48 +1,64 @@
'use client';
import { useActionState } from 'react';
import { useActionState, useEffect } from 'react';
import { addCompany } from '@/app/companies/actions';
export default function CompanyForm() {
interface CompanyFormProps {
onSuccess?: () => void;
}
export default function CompanyForm({ onSuccess }: CompanyFormProps) {
const [state, formAction, isPending] = useActionState(
async (prevState: any, formData: FormData) => {
return await addCompany(formData);
const result = await addCompany(formData);
return result;
},
null
);
useEffect(() => {
if (state?.success && onSuccess) {
onSuccess();
}
}, [state, onSuccess]);
return (
<form action={formAction} className="mt-5 space-y-4">
<form action={formAction} className="space-y-6">
{state?.error && (
<div className="p-4 text-xs font-black text-rose-600 bg-rose-50 border border-rose-100 rounded-2xl text-center uppercase tracking-widest leading-relaxed">
<div className="p-4 text-xs font-black text-[#CE0515] bg-red-50 dark:bg-red-500/10 border border-red-100 dark:border-red-500/20 rounded-2xl text-center uppercase tracking-widest leading-relaxed animate-in shake duration-300">
{state.error}
</div>
)}
{state?.success && (
<div className="p-4 text-xs font-black text-emerald-600 bg-emerald-50 border border-emerald-100 rounded-2xl text-center uppercase tracking-widest leading-relaxed">
Şirket başarıyla kaydedildi.
</div>
)}
<div>
<label htmlFor="name" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Şirket Adı</label>
<div className="space-y-2">
<label htmlFor="name" className="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-[0.2em] ml-1">
Şirket Adı
</label>
<input
type="text"
name="name"
id="name"
required
disabled={isPending}
className="block w-full rounded-2xl border-0 py-3 px-4 text-slate-900 shadow-sm ring-1 ring-inset ring-slate-200 dark:ring-zinc-700 dark:bg-zinc-800 dark:text-white placeholder:text-slate-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm outline-none transition-all disabled:opacity-50"
placeholder="Abisena Ltd. Şti."
className="block w-full rounded-2xl border-none bg-slate-50 dark:bg-zinc-800 py-4 px-5 text-[#173363] dark:text-white ring-1 ring-slate-100 dark:ring-zinc-700 focus:ring-2 focus:ring-[#CE0515] transition-all outline-none disabled:opacity-50 font-bold placeholder:font-medium"
placeholder="Şirket ismini buraya yazın..."
/>
</div>
<div className="pt-2">
<button
type="submit"
disabled={isPending}
className="inline-flex w-full items-center justify-center rounded-2xl bg-indigo-600 px-4 py-3 text-sm font-bold text-white shadow-sm shadow-indigo-100 dark:shadow-none hover:bg-indigo-700 transition-all active:scale-95 disabled:opacity-50"
className="w-full inline-flex items-center justify-center rounded-[1.25rem] bg-[#173363] px-6 py-4 text-sm font-black text-white shadow-xl shadow-blue-900/10 hover:bg-[#1e4585] transition-all active:scale-95 disabled:opacity-50 disabled:active:scale-100"
>
{isPending ? 'KAYDEDİLİYOR...' : 'Şirket Kaydet'}
{isPending ? (
<div className="flex items-center gap-2">
<div className="w-4 h-4 border-2 border-white/20 border-t-white rounded-full animate-spin" />
Kaydediliyor...
</div>
) : 'Şirketi Kaydet'}
</button>
</div>
</form>
);
}
+12 -3
View File
@@ -23,10 +23,19 @@ const navigation = [
{ name: 'Ayarlar', href: '/settings', icon: Cog6ToothIcon },
]
export function Sidebar() {
interface SidebarProps {
isAdmin?: boolean
}
export function Sidebar({ isAdmin = false }: SidebarProps) {
const pathname = usePathname()
const [sidebarOpen, setSidebarOpen] = useState(false)
const filteredNavigation = navigation.filter(item => {
if (item.href === '/companies') return isAdmin
return true
})
// Listen for mobile menu open event from Header
useEffect(() => {
const handleOpen = () => setSidebarOpen(true);
@@ -67,7 +76,7 @@ export function Sidebar() {
<ul role="list" className="flex flex-1 flex-col gap-y-7">
<li>
<ul role="list" className="-mx-2 space-y-2">
{navigation.map((item) => (
{filteredNavigation.map((item) => (
<li key={item.name}>
<Link
href={item.href}
@@ -127,7 +136,7 @@ export function Sidebar() {
<li>
<div className="text-[10px] font-bold tracking-widest leading-6 text-slate-400 mb-4 uppercase">YÖNETİM PANELİ</div>
<ul role="list" className="-mx-2 space-y-2">
{navigation.map((item) => (
{filteredNavigation.map((item) => (
<li key={item.name}>
<Link
href={item.href}
@@ -0,0 +1,262 @@
-- 1. Create Public Holidays Table
CREATE TABLE IF NOT EXISTS public.public_holidays (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
date DATE NOT NULL UNIQUE,
name TEXT NOT NULL,
is_recurring BOOLEAN DEFAULT false, -- If true, year is ignored (e.g., Oct 29)
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
);
-- Seed some recurring Turkish public holidays
INSERT INTO public.public_holidays (date, name, is_recurring) VALUES
('2024-01-01', 'Yılbaşı', true),
('2024-04-23', 'Ulusal Egemenlik ve Çocuk Bayramı', true),
('2024-05-01', 'Emek ve Dayanışma Günü', true),
('2024-05-19', 'Atatürk''ü Anma, Gençlik ve Spor Bayramı', true),
('2024-07-15', 'Demokrasi ve Milli Birlik Günü', true),
('2024-08-30', 'Zafer Bayramı', true),
('2024-10-29', 'Cumhuriyet Bayramı', true)
ON CONFLICT (date) DO NOTHING;
-- 2. Create Leave Types Table
CREATE TABLE IF NOT EXISTS public.leave_types (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
description TEXT,
is_paid BOOLEAN DEFAULT true,
requires_approval BOOLEAN DEFAULT true,
is_deductible BOOLEAN DEFAULT true, -- If true, reduces standard balance
only_admin_can_create BOOLEAN DEFAULT false, -- If true, only managers/admins can initiate
color_code TEXT,
display_order INTEGER DEFAULT 99, -- Sort order for UI
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
);
-- Add column if it doesn't exist (for cases where table already existed)
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='leave_types' AND column_name='display_order') THEN
ALTER TABLE public.leave_types ADD COLUMN display_order INTEGER DEFAULT 99;
END IF;
END $$;
-- Seed Leave Types with display_order
INSERT INTO public.leave_types (name, description, is_paid, requires_approval, is_deductible, only_admin_can_create, color_code, display_order) VALUES
('Yıllık İzin', 'Kıdeme bağlı hakedilen ücretli izin', true, true, true, false, '#4CAF50', 1),
('Mazeret İzni', 'Özel durumlar için kısa süreli izinler', true, true, true, false, '#FF9800', 2),
('Hastalık İzni', 'Raporlu olunan günler', true, true, false, false, '#F44336', 3),
('Evlilik İzni', 'Evlilik durumunda verilen 3 günlük yasal izin', true, true, false, false, '#E91E63', 4),
('Vefat İzni', 'Birinci derece yakın vefatı durumunda verilen 3 günlük yasal izin', true, true, false, false, '#607D8B', 5),
('İdari İzin', 'Yönetim tarafından verilen idari izin', true, true, false, true, '#2196F3', 6),
('İcap', 'Nöbet/İcap görevleri için verilen izin', true, true, false, true, '#9C27B0', 7)
ON CONFLICT (name) DO UPDATE SET display_order = EXCLUDED.display_order;
-- 3. Modify Leave Requests Table
-- First, add columns (handle case where they might already exist if re-run)
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='leave_requests' AND column_name='leave_type_id') THEN
ALTER TABLE public.leave_requests ADD COLUMN leave_type_id UUID REFERENCES public.leave_types(id);
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='leave_requests' AND column_name='total_days') THEN
ALTER TABLE public.leave_requests ADD COLUMN total_days DECIMAL(5,2);
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='leave_requests' AND column_name='approved_by') THEN
ALTER TABLE public.leave_requests ADD COLUMN approved_by UUID REFERENCES auth.users(id);
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='leave_requests' AND column_name='approval_date') THEN
ALTER TABLE public.leave_requests ADD COLUMN approval_date TIMESTAMP WITH TIME ZONE;
END IF;
END $$;
-- 4. Update Leave Balances Table
-- We need to drop the unique constraint on employee_id because now it's employee_id + leave_type_id + year
ALTER TABLE public.leave_balances DROP CONSTRAINT IF EXISTS leave_balances_employee_id_key;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='leave_balances' AND column_name='leave_type_id') THEN
ALTER TABLE public.leave_balances ADD COLUMN leave_type_id UUID REFERENCES public.leave_types(id);
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='leave_balances' AND column_name='year') THEN
ALTER TABLE public.leave_balances ADD COLUMN year INTEGER DEFAULT EXTRACT(YEAR FROM CURRENT_DATE);
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='leave_balances' AND column_name='accrued_days') THEN
ALTER TABLE public.leave_balances ADD COLUMN accrued_days DECIMAL(5,2) DEFAULT 0;
END IF;
END $$;
-- Update existing data: map annual leave type to existing balances
DO $$
DECLARE
annual_leave_id UUID;
BEGIN
SELECT id INTO annual_leave_id FROM public.leave_types WHERE name = 'Yıllık İzin' LIMIT 1;
UPDATE public.leave_balances SET leave_type_id = annual_leave_id WHERE leave_type_id IS NULL;
UPDATE public.leave_requests SET leave_type_id = annual_leave_id WHERE leave_type_id IS NULL;
END $$;
-- Now add NOT NULL and UNIQUE constraint
ALTER TABLE public.leave_balances ALTER COLUMN leave_type_id SET NOT NULL;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'leave_balances_employee_type_year_unique') THEN
ALTER TABLE public.leave_balances ADD CONSTRAINT leave_balances_employee_type_year_unique UNIQUE(employee_id, leave_type_id, year);
END IF;
END $$;
-- 5. Helper Function: Calculate Leave Days
CREATE OR REPLACE FUNCTION public.calculate_leave_days(p_start_date DATE, p_end_date DATE)
RETURNS DECIMAL AS $$
DECLARE
curr_date DATE;
total_days DECIMAL := 0;
is_holiday BOOLEAN;
BEGIN
curr_date := p_start_date;
WHILE curr_date <= p_end_date LOOP
-- Sunday (0) is never counts.
-- Saturday logic: Depends on company, but generally counts in law.
-- We'll exclude Sundays and Public Holidays.
IF EXTRACT(DOW FROM curr_date) != 0 THEN
SELECT EXISTS(
SELECT 1 FROM public.public_holidays
WHERE (is_recurring AND EXTRACT(MONTH FROM date) = EXTRACT(MONTH FROM curr_date) AND EXTRACT(DAY FROM date) = EXTRACT(DAY FROM curr_date))
OR (NOT is_recurring AND date = curr_date)
) INTO is_holiday;
IF NOT is_holiday THEN
total_days := total_days + 1;
END IF;
END IF;
curr_date := curr_date + 1;
END LOOP;
RETURN total_days;
END;
$$ LANGUAGE plpgsql;
-- 6. Trigger to auto-calculate total_days on leave_requests
CREATE OR REPLACE FUNCTION public.handle_leave_request_days()
RETURNS TRIGGER AS $$
BEGIN
NEW.total_days := public.calculate_leave_days(NEW.start_date, NEW.end_date);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS tr_calculate_leave_days ON public.leave_requests;
CREATE TRIGGER tr_calculate_leave_days
BEFORE INSERT OR UPDATE OF start_date, end_date ON public.leave_requests
FOR EACH ROW EXECUTE PROCEDURE public.handle_leave_request_days();
-- 7. Seniority based Accrual Function
CREATE OR REPLACE FUNCTION public.calculate_annual_leave_entitlement(p_employee_id UUID)
RETURNS INTEGER AS $$
DECLARE
v_hire_date DATE;
v_years_service INTEGER;
BEGIN
SELECT COALESCE(start_date, hire_date, created_at::date) INTO v_hire_date
FROM public.employees WHERE id = p_employee_id;
v_years_service := EXTRACT(YEAR FROM age(CURRENT_DATE, v_hire_date));
IF v_years_service < 1 THEN RETURN 0; -- No leave in first year
ELSIF v_years_service < 5 THEN RETURN 14;
ELSIF v_years_service < 15 THEN RETURN 20;
ELSE RETURN 26;
END IF;
END;
$$ LANGUAGE plpgsql;
-- 8. Updated Balance Trigger
-- We need to replace handle_new_employee_balance to handle new logic
CREATE OR REPLACE FUNCTION public.handle_new_employee_balance()
RETURNS TRIGGER AS $$
DECLARE
annual_leave_id UUID;
BEGIN
SELECT id INTO annual_leave_id FROM public.leave_types WHERE name = 'Yıllık İzin' LIMIT 1;
INSERT INTO public.leave_balances (employee_id, leave_type_id, year, accrued_days, total_days)
VALUES (NEW.id, annual_leave_id, EXTRACT(YEAR FROM CURRENT_DATE), 0, 0)
ON CONFLICT DO NOTHING;
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- 9. Function to update balance based on leave request status
CREATE OR REPLACE FUNCTION public.update_leave_balance_on_status_change()
RETURNS TRIGGER AS $$
DECLARE
v_is_deductible BOOLEAN;
v_annual_leave_id UUID;
v_year INTEGER;
BEGIN
SELECT is_deductible INTO v_is_deductible FROM public.leave_types WHERE id = NEW.leave_type_id;
IF v_is_deductible THEN
v_year := EXTRACT(YEAR FROM NEW.start_date);
-- Ensure balance record exists for this year/type
INSERT INTO public.leave_balances (employee_id, leave_type_id, year)
VALUES (NEW.employee_id, NEW.leave_type_id, v_year)
ON CONFLICT (employee_id, leave_type_id, year) DO NOTHING;
-- Update based on status
IF NEW.status = 'approved' AND (OLD.status IS NULL OR OLD.status != 'approved') THEN
UPDATE public.leave_balances
SET used_days = used_days + NEW.total_days,
pending_days = CASE WHEN OLD.status = 'pending' THEN pending_days - NEW.total_days ELSE pending_days END
WHERE employee_id = NEW.employee_id AND leave_type_id = NEW.leave_type_id AND year = v_year;
ELSIF NEW.status = 'pending' AND (OLD.status IS NULL OR OLD.status != 'pending') THEN
UPDATE public.leave_balances
SET pending_days = pending_days + NEW.total_days
WHERE employee_id = NEW.employee_id AND leave_type_id = NEW.leave_type_id AND year = v_year;
ELSIF (NEW.status = 'rejected' OR NEW.status = 'cancelled') AND OLD.status = 'pending' THEN
UPDATE public.leave_balances
SET pending_days = pending_days - NEW.total_days
WHERE employee_id = NEW.employee_id AND leave_type_id = NEW.leave_type_id AND year = v_year;
ELSIF (NEW.status = 'cancelled') AND OLD.status = 'approved' THEN
UPDATE public.leave_balances
SET used_days = used_days - NEW.total_days
WHERE employee_id = NEW.employee_id AND leave_type_id = NEW.leave_type_id AND year = v_year;
END IF;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- 10. Update RLS for leave_balances (ensure proper access)
ALTER TABLE public.leave_balances ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "Admins can do everything on leave_balances" ON public.leave_balances;
CREATE POLICY "Admins can do everything on leave_balances"
ON public.leave_balances
FOR ALL TO authenticated
USING (
EXISTS (
SELECT 1 FROM public.employees e
JOIN public.roles r ON e.role_id = r.id
WHERE e.user_id = auth.uid() AND r.name IN ('admin', 'manager')
)
);
DROP POLICY IF EXISTS "Users can view their own leave balance" ON public.leave_balances;
CREATE POLICY "Users can view their own leave balance"
ON public.leave_balances
FOR SELECT TO authenticated
USING (
employee_id IN (
SELECT id FROM public.employees WHERE user_id = auth.uid()
)
);
DROP TRIGGER IF EXISTS tr_update_leave_balance ON public.leave_requests;
CREATE TRIGGER tr_update_leave_balance
AFTER INSERT OR UPDATE OF status ON public.leave_requests
FOR EACH ROW EXECUTE PROCEDURE public.update_leave_balance_on_status_change();
@@ -0,0 +1,59 @@
-- Fix Leave Requests RLS to allow managers to approve/reject
-- Enable UPDATE for managers belonging to the same company as the request
DROP POLICY IF EXISTS "Managers can update leave requests in their company" ON public.leave_requests;
CREATE POLICY "Managers can update leave requests in their company"
ON public.leave_requests
FOR UPDATE TO authenticated
USING (
EXISTS (
SELECT 1 FROM public.employees manager_emp
JOIN public.roles r ON manager_emp.role_id = r.id
WHERE manager_emp.user_id = auth.uid()
AND r.name IN ('admin', 'manager')
AND manager_emp.company_id = (
SELECT company_id FROM public.employees target_emp
WHERE target_emp.id = public.leave_requests.employee_id
)
)
)
WITH CHECK (
EXISTS (
SELECT 1 FROM public.employees manager_emp
JOIN public.roles r ON manager_emp.role_id = r.id
WHERE manager_emp.user_id = auth.uid()
AND r.name IN ('admin', 'manager')
AND manager_emp.company_id = (
SELECT company_id FROM public.employees target_emp
WHERE target_emp.id = public.leave_requests.employee_id
)
)
);
-- Ensure managers can also view everything in their company
DROP POLICY IF EXISTS "Managers can view all leave requests in their company" ON public.leave_requests;
CREATE POLICY "Managers can view all leave requests in their company"
ON public.leave_requests
FOR SELECT TO authenticated
USING (
EXISTS (
SELECT 1 FROM public.employees manager_emp
JOIN public.roles r ON manager_emp.role_id = r.id
WHERE manager_emp.user_id = auth.uid()
AND r.name IN ('admin', 'manager')
AND manager_emp.company_id = (
SELECT company_id FROM public.employees target_emp
WHERE target_emp.id = public.leave_requests.employee_id
)
)
OR
employee_id IN (
SELECT id FROM public.employees WHERE user_id = auth.uid()
)
);
-- Update existing records with NULL total_days
UPDATE public.leave_requests
SET total_days = public.calculate_leave_days(start_date, end_date)
WHERE total_days IS NULL;
@@ -0,0 +1,69 @@
-- Robust Leave Balance Trigger to handle ALL status transitions and edits
-- This handles Reset to Pending, Date Changes, and Role Changes
CREATE OR REPLACE FUNCTION public.update_leave_balance_on_status_change()
RETURNS TRIGGER AS $$
DECLARE
v_is_deductible_old BOOLEAN;
v_is_deductible_new BOOLEAN;
v_year_old INTEGER;
v_year_new INTEGER;
BEGIN
-- 1. REVERT OLD STATE
IF OLD IS NOT NULL THEN
SELECT is_deductible INTO v_is_deductible_old FROM public.leave_types WHERE id = OLD.leave_type_id;
IF v_is_deductible_old THEN
v_year_old := EXTRACT(YEAR FROM OLD.start_date);
-- Revert based on OLD status
IF OLD.status = 'approved' THEN
UPDATE public.leave_balances
SET used_days = used_days - OLD.total_days
WHERE employee_id = OLD.employee_id AND leave_type_id = OLD.leave_type_id AND year = v_year_old;
ELSIF OLD.status = 'pending' THEN
UPDATE public.leave_balances
SET pending_days = pending_days - OLD.total_days
WHERE employee_id = OLD.employee_id AND leave_type_id = OLD.leave_type_id AND year = v_year_old;
END IF;
END IF;
END IF;
-- 2. APPLY NEW STATE
IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN
IF NEW.status != 'cancelled' AND NEW.status != 'rejected' THEN
SELECT is_deductible INTO v_is_deductible_new FROM public.leave_types WHERE id = NEW.leave_type_id;
IF v_is_deductible_new THEN
v_year_new := EXTRACT(YEAR FROM NEW.start_date);
-- Ensure balance record exists
INSERT INTO public.leave_balances (employee_id, leave_type_id, year)
VALUES (NEW.employee_id, NEW.leave_type_id, v_year_new)
ON CONFLICT (employee_id, leave_type_id, year) DO NOTHING;
-- Apply based on NEW status
IF NEW.status = 'approved' THEN
UPDATE public.leave_balances
SET used_days = used_days + NEW.total_days
WHERE employee_id = NEW.employee_id AND leave_type_id = NEW.leave_type_id AND year = v_year_new;
ELSIF NEW.status = 'pending' THEN
UPDATE public.leave_balances
SET pending_days = pending_days + NEW.total_days
WHERE employee_id = NEW.employee_id AND leave_type_id = NEW.leave_type_id AND year = v_year_new;
END IF;
END IF;
END IF;
END IF;
IF (TG_OP = 'DELETE') THEN
RETURN OLD;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
DROP TRIGGER IF EXISTS tr_update_leave_balance ON public.leave_requests;
CREATE TRIGGER tr_update_leave_balance
AFTER INSERT OR UPDATE OR DELETE ON public.leave_requests
FOR EACH ROW EXECUTE PROCEDURE public.update_leave_balance_on_status_change();