New Proje
This commit is contained in:
10
package-lock.json
generated
10
package-lock.json
generated
@@ -8,6 +8,7 @@
|
||||
"name": "personel",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@supabase/ssr": "^0.9.0",
|
||||
"@supabase/supabase-js": "^2.99.2",
|
||||
"next": "16.1.6",
|
||||
@@ -455,6 +456,15 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@heroicons/react": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz",
|
||||
"integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">= 16 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@supabase/ssr": "^0.9.0",
|
||||
"@supabase/supabase-js": "^2.99.2",
|
||||
"next": "16.1.6",
|
||||
|
||||
31
src/app/auth/callback/route.ts
Normal file
31
src/app/auth/callback/route.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { createClient } from '@/utils/supabase/server'
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams, origin } = new URL(request.url)
|
||||
const code = searchParams.get('code')
|
||||
// if "next" is in param, use it as the redirect URL
|
||||
const next = searchParams.get('next') ?? '/'
|
||||
|
||||
if (code) {
|
||||
const supabase = await createClient()
|
||||
const { error } = await supabase.auth.exchangeCodeForSession(code)
|
||||
|
||||
if (!error) {
|
||||
const forwardedHost = request.headers.get('x-forwarded-host') // original origin before load balancer
|
||||
const isLocalEnv = process.env.NODE_ENV === 'development'
|
||||
|
||||
if (isLocalEnv) {
|
||||
// we can be sure that there is no load balancer in between, so no need to watch for X-Forwarded-Host
|
||||
return NextResponse.redirect(`${origin}${next}`)
|
||||
} else if (forwardedHost) {
|
||||
return NextResponse.redirect(`https://${forwardedHost}${next}`)
|
||||
} else {
|
||||
return NextResponse.redirect(`${origin}${next}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// return the user to an error page with instructions
|
||||
return NextResponse.redirect(`${origin}/auth/auth-code-error`)
|
||||
}
|
||||
14
src/app/auth/signout/route.ts
Normal file
14
src/app/auth/signout/route.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { createClient } from '@/utils/supabase/server'
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const supabase = await createClient()
|
||||
|
||||
const { error } = await supabase.auth.signOut()
|
||||
|
||||
if (error) {
|
||||
redirect('/error')
|
||||
}
|
||||
|
||||
redirect('/login')
|
||||
}
|
||||
38
src/app/companies/actions.ts
Normal file
38
src/app/companies/actions.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
'use server'
|
||||
|
||||
import { createClient } from '@/utils/supabase/server'
|
||||
import { revalidatePath } from 'next/cache'
|
||||
|
||||
export async function addCompany(formData: FormData) {
|
||||
const supabase = await createClient()
|
||||
|
||||
const name = formData.get('name') as string
|
||||
if (!name) return { error: 'Şirket adı zorunludur.' }
|
||||
|
||||
const { error } = await supabase
|
||||
.from('companies')
|
||||
.insert([{ name }])
|
||||
|
||||
if (error) {
|
||||
return { error: 'Şirket eklenirken bir hata oluştu: ' + error.message }
|
||||
}
|
||||
|
||||
revalidatePath('/companies')
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export async function deleteCompany(id: string) {
|
||||
const supabase = await createClient()
|
||||
|
||||
const { error } = await supabase
|
||||
.from('companies')
|
||||
.delete()
|
||||
.eq('id', id)
|
||||
|
||||
if (error) {
|
||||
return { error: 'Şirket silinirken bir hata oluştu: ' + error.message }
|
||||
}
|
||||
|
||||
revalidatePath('/companies')
|
||||
return { success: true }
|
||||
}
|
||||
115
src/app/companies/page.tsx
Normal file
115
src/app/companies/page.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import { createClient } from '@/utils/supabase/server'
|
||||
import { addCompany, deleteCompany } from './actions'
|
||||
import { BuildingOfficeIcon, TrashIcon } from '@heroicons/react/24/outline'
|
||||
|
||||
export default async function CompaniesPage() {
|
||||
const supabase = await createClient()
|
||||
|
||||
// Fetch companies
|
||||
const { data: companies } = await supabase
|
||||
.from('companies')
|
||||
.select('*')
|
||||
.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">
|
||||
<div className="sm:flex-auto">
|
||||
<h1 className="text-2xl font-semibold leading-6 text-gray-900 dark:text-white flex items-center gap-2">
|
||||
<BuildingOfficeIcon className="w-6 h-6 text-blue-500" />
|
||||
Şirket Yönetimi
|
||||
</h1>
|
||||
<p className="mt-2 text-sm text-gray-700 dark:text-gray-400">
|
||||
Sisteme kayıtlı şirketlerin listesi ve yeni şirket ekleme alanı.
|
||||
</p>
|
||||
</div>
|
||||
</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>Çalışanları atayabilmek için öncelikle şirket kaydı açmalısınız.</p>
|
||||
</div>
|
||||
|
||||
<form action={addCompany} className="mt-5 space-y-4">
|
||||
<div>
|
||||
<label htmlFor="name" className="sr-only">Şirket Adı</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
id="name"
|
||||
required
|
||||
className="block w-full rounded-md border-0 py-2 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-700 dark:bg-zinc-800 dark:text-white placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
|
||||
placeholder="Şirket Adını Giriniz"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="inline-flex w-full items-center justify-center rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
||||
>
|
||||
Kaydet
|
||||
</button>
|
||||
</form>
|
||||
</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-gray-200 dark:divide-zinc-800">
|
||||
<thead className="bg-gray-50 dark:bg-zinc-950/50">
|
||||
<tr>
|
||||
<th scope="col" className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 dark:text-white sm:pl-6">
|
||||
Şirket Adı
|
||||
</th>
|
||||
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-white">
|
||||
Kayıt Tarihi
|
||||
</th>
|
||||
<th scope="col" className="relative py-3.5 pl-3 pr-4 sm:pr-6">
|
||||
<span className="sr-only">İşlemler</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 dark:divide-zinc-800 bg-white dark:bg-zinc-900">
|
||||
{companies?.map((company) => (
|
||||
<tr key={company.id}>
|
||||
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 dark:text-white sm:pl-6">
|
||||
{company.name}
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
{new Date(company.created_at).toLocaleDateString('tr-TR')}
|
||||
</td>
|
||||
<td className="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
|
||||
<form action={async () => {
|
||||
'use server';
|
||||
await deleteCompany(company.id)
|
||||
}}>
|
||||
<button type="submit" className="text-red-600 hover:text-red-900 dark:text-red-500 dark:hover:text-red-400" title="Sil">
|
||||
<TrashIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{(!companies || companies.length === 0) && (
|
||||
<tr>
|
||||
<td colSpan={3} className="py-8 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
Henüz kayıtlı şirket bulunmamaktadır.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
73
src/app/employees/actions.ts
Normal file
73
src/app/employees/actions.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
'use server'
|
||||
|
||||
import { createClient } from '@/utils/supabase/server'
|
||||
import { revalidatePath } from 'next/cache'
|
||||
|
||||
export async function addEmployee(formData: FormData) {
|
||||
const supabase = await createClient()
|
||||
|
||||
// 1. Get Form Data
|
||||
const firstName = formData.get('first_name') as string
|
||||
const lastName = formData.get('last_name') as string
|
||||
const email = formData.get('email') as string
|
||||
const companyId = formData.get('company_id') as string
|
||||
|
||||
if (!email || !companyId) {
|
||||
return { error: 'E-posta ve Şirket 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()
|
||||
|
||||
if (!existingUser) {
|
||||
return { error: 'Bu e-posta adresine sahip bir kullanıcı bulunamadı. Kullanıcının önce sisteme kayıt olması gerekmektedir.' }
|
||||
}
|
||||
|
||||
// 2. Insert into Employees table
|
||||
const { error: employeeError } = await supabase
|
||||
.from('employees')
|
||||
.insert([{
|
||||
user_id: existingUser.id,
|
||||
company_id: companyId,
|
||||
department: formData.get('department'),
|
||||
title: formData.get('title'),
|
||||
status: 'active'
|
||||
}])
|
||||
|
||||
if (employeeError) {
|
||||
// Catch unique constraint violation
|
||||
if (employeeError.code === '23505') {
|
||||
return { error: 'Bu kullanıcı zaten bu şirkete eklenmiş.' }
|
||||
}
|
||||
return { error: 'Çalışan eklenirken hata: ' + employeeError.message }
|
||||
}
|
||||
|
||||
revalidatePath('/employees')
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export async function deleteEmployee(id: string) {
|
||||
const supabase = await createClient()
|
||||
|
||||
const { error } = await supabase
|
||||
.from('employees')
|
||||
.delete()
|
||||
.eq('id', id)
|
||||
|
||||
if (error) {
|
||||
return { error: 'Çalışan silinirken hata oluştu: ' + error.message }
|
||||
}
|
||||
|
||||
revalidatePath('/employees')
|
||||
return { success: true }
|
||||
}
|
||||
189
src/app/employees/page.tsx
Normal file
189
src/app/employees/page.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
import { createClient } from '@/utils/supabase/server'
|
||||
import { addEmployee, deleteEmployee } from './actions'
|
||||
import { UsersIcon, TrashIcon } from '@heroicons/react/24/outline'
|
||||
|
||||
export default async function EmployeesPage() {
|
||||
const supabase = await createClient()
|
||||
|
||||
// Fetch employees with their joined user, company, and role data
|
||||
const { data: employees } = await supabase
|
||||
.from('employees')
|
||||
.select(`
|
||||
id,
|
||||
department,
|
||||
title,
|
||||
status,
|
||||
created_at,
|
||||
companies ( name ),
|
||||
users ( first_name, last_name, email ),
|
||||
roles ( name, description )
|
||||
`)
|
||||
.order('created_at', { ascending: false })
|
||||
|
||||
// Fetch companies for the dropdown
|
||||
const { data: companies } = await supabase
|
||||
.from('companies')
|
||||
.select('id, name')
|
||||
.order('name')
|
||||
|
||||
return (
|
||||
<div className="p-4 sm:p-6 lg:p-8 max-w-7xl mx-auto">
|
||||
<div className="sm:flex sm:items-center">
|
||||
<div className="sm:flex-auto">
|
||||
<h1 className="text-2xl font-semibold leading-6 text-gray-900 dark:text-white flex items-center gap-2">
|
||||
<UsersIcon className="w-6 h-6 text-blue-500" />
|
||||
Çalışan Yönetimi
|
||||
</h1>
|
||||
<p className="mt-2 text-sm text-gray-700 dark:text-gray-400">
|
||||
Sisteme kayıtlı çalışanları görüntüleyin ve yeni çalışanları şirketlere atayın.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||
|
||||
{/* Add Employee Form */}
|
||||
<div className="md:col-span-1 border border-gray-200 dark:border-zinc-800 rounded-xl bg-white dark:bg-zinc-900 shadow-sm overflow-hidden h-fit">
|
||||
<div className="px-4 py-5 sm:p-6">
|
||||
<h3 className="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||
Yeni Çalışan Ata
|
||||
</h3>
|
||||
<div className="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
<p>Sisteme daha önce kayıt olmuş (şifre oluşturmuş) kullanıcıları şirketlere atayın.</p>
|
||||
</div>
|
||||
|
||||
<form action={addEmployee} className="mt-5 space-y-4">
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium leading-6 text-gray-900 dark:text-gray-300">Kullanıcı E-posta</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
id="email"
|
||||
required
|
||||
placeholder="ornek@sirket.com"
|
||||
className="mt-2 block w-full rounded-md border-0 py-1.5 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-700 dark:bg-zinc-800 dark:text-white placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="company_id" className="block text-sm font-medium leading-6 text-gray-900 dark:text-gray-300">Şirket</label>
|
||||
<select
|
||||
id="company_id"
|
||||
name="company_id"
|
||||
required
|
||||
className="mt-2 block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-blue-600 sm:text-sm sm:leading-6 dark:bg-zinc-800 dark:text-white dark:ring-zinc-700"
|
||||
>
|
||||
<option value="">Şirket Seçiniz</option>
|
||||
{companies?.map(company => (
|
||||
<option key={company.id} value={company.id}>{company.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="department" className="block text-sm font-medium leading-6 text-gray-900 dark:text-gray-300">Departman</label>
|
||||
<input
|
||||
type="text"
|
||||
name="department"
|
||||
id="department"
|
||||
placeholder="Örn: IT, İnsan Kaynakları"
|
||||
className="mt-2 block w-full rounded-md border-0 py-1.5 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-700 dark:bg-zinc-800 dark:text-white placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="title" className="block text-sm font-medium leading-6 text-gray-900 dark:text-gray-300">Ünvan</label>
|
||||
<input
|
||||
type="text"
|
||||
name="title"
|
||||
id="title"
|
||||
placeholder="Örn: Yazılım Uzmanı"
|
||||
className="mt-2 block w-full rounded-md border-0 py-1.5 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-700 dark:bg-zinc-800 dark:text-white placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="inline-flex w-full items-center justify-center rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 mt-6"
|
||||
>
|
||||
Çalışan Ekle
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Employee List */}
|
||||
<div className="md:col-span-3">
|
||||
<div className="bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-gray-200 dark:ring-zinc-800 sm:rounded-xl overflow-hidden overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-zinc-800">
|
||||
<thead className="bg-gray-50 dark:bg-zinc-950/50">
|
||||
<tr>
|
||||
<th scope="col" className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 dark:text-white sm:pl-6">
|
||||
Kişi
|
||||
</th>
|
||||
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-white">
|
||||
Şirket
|
||||
</th>
|
||||
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-white">
|
||||
Departman / Ünvan
|
||||
</th>
|
||||
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-white">
|
||||
Durum
|
||||
</th>
|
||||
<th scope="col" className="relative py-3.5 pl-3 pr-4 sm:pr-6">
|
||||
<span className="sr-only">İşlemler</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 dark:divide-zinc-800 bg-white dark:bg-zinc-900">
|
||||
{employees?.map((emp) => (
|
||||
<tr key={emp.id}>
|
||||
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm sm:pl-6">
|
||||
<div className="font-medium text-gray-900 dark:text-white">
|
||||
{emp.users?.first_name} {emp.users?.last_name}
|
||||
</div>
|
||||
<div className="text-gray-500 dark:text-gray-400">{emp.users?.email}</div>
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
<div className="text-gray-900 dark:text-white">{emp.companies?.name}</div>
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
<div className="text-gray-900 dark:text-white">{emp.department || '-'}</div>
|
||||
<div className="text-gray-500 dark:text-gray-400">{emp.title || '-'}</div>
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
<span className={`inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ring-1 ring-inset ${
|
||||
emp.status === 'active' ? 'bg-green-50 text-green-700 ring-green-600/20 dark:bg-green-500/10 dark:text-green-400 dark:ring-green-500/20' :
|
||||
'bg-red-50 text-red-700 ring-red-600/20 dark:bg-red-500/10 dark:text-red-400 dark:ring-red-500/20'
|
||||
}`}>
|
||||
{emp.status === 'active' ? 'Aktif' : 'Pasif'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
|
||||
<form action={async () => {
|
||||
'use server';
|
||||
await deleteEmployee(emp.id)
|
||||
}}>
|
||||
<button type="submit" className="text-red-600 hover:text-red-900 dark:text-red-500 dark:hover:text-red-400" title="Kişiyi Şirketten Çıkar">
|
||||
<TrashIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{(!employees || employees.length === 0) && (
|
||||
<tr>
|
||||
<td colSpan={5} className="py-8 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
Sisteme kayıtlı çalışan bulunmamaktadır.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import { createClient } from '@/utils/supabase/server'
|
||||
import { Sidebar } from "@/components/layout/Sidebar";
|
||||
import { Header } from "@/components/layout/Header";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
@@ -13,21 +16,47 @@ const geistMono = Geist_Mono({
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
title: "HRMS Yönetim Sistemi",
|
||||
description: "Şirket içi insan kaynakları yönetim paneli",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const supabase = await createClient()
|
||||
const { data: { user } } = await supabase.auth.getUser()
|
||||
|
||||
// Fetch db user profile if logged in
|
||||
let dbUser = null
|
||||
if (user) {
|
||||
const { data } = await supabase
|
||||
.from('users')
|
||||
.select('first_name, last_name, email')
|
||||
.eq('id', user.id)
|
||||
.single()
|
||||
dbUser = data
|
||||
}
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang="tr" className="h-full bg-white dark:bg-zinc-950">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
||||
>
|
||||
{children}
|
||||
{user ? (
|
||||
<div>
|
||||
<Sidebar />
|
||||
<div className="lg:pl-72 flex flex-col min-h-screen">
|
||||
<Header user={dbUser || user} />
|
||||
<main className="flex-1">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
65
src/app/leave-requests/actions.ts
Normal file
65
src/app/leave-requests/actions.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
'use server'
|
||||
|
||||
import { createClient } from '@/utils/supabase/server'
|
||||
import { revalidatePath } from 'next/cache'
|
||||
|
||||
export async function submitLeaveRequest(formData: FormData) {
|
||||
const supabase = await createClient()
|
||||
|
||||
const { data: { user } } = await supabase.auth.getUser()
|
||||
if (!user) return { error: 'Oturum bulunamadı.' }
|
||||
|
||||
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
|
||||
|
||||
if (!startDate || !endDate || !companyId || !reason) {
|
||||
return { error: 'Tüm alanları doldurunuz.' }
|
||||
}
|
||||
|
||||
// Find their employee record for this specific company
|
||||
const { data: employeeData, error: empError } = await supabase
|
||||
.from('employees')
|
||||
.select('id')
|
||||
.eq('user_id', user.id)
|
||||
.eq('company_id', companyId)
|
||||
.single()
|
||||
|
||||
if (empError || !employeeData) {
|
||||
return { error: 'Seçili şirket için çalışan kaydınız bulunamadı.' }
|
||||
}
|
||||
|
||||
const { error } = await supabase
|
||||
.from('leave_requests')
|
||||
.insert([{
|
||||
employee_id: employeeData.id,
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
reason: reason,
|
||||
status: 'pending' // default
|
||||
}])
|
||||
|
||||
if (error) {
|
||||
return { error: 'İzin talebi oluşturulurken hata: ' + error.message }
|
||||
}
|
||||
|
||||
revalidatePath('/leave-requests')
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export async function updateLeaveStatus(id: string, newStatus: 'approved' | 'rejected') {
|
||||
const supabase = await createClient()
|
||||
|
||||
const { error } = await supabase
|
||||
.from('leave_requests')
|
||||
.update({ status: newStatus })
|
||||
.eq('id', id)
|
||||
|
||||
if (error) {
|
||||
return { error: 'Durum güncellenirken hata oluştu: ' + error.message }
|
||||
}
|
||||
|
||||
revalidatePath('/leave-requests')
|
||||
return { success: true }
|
||||
}
|
||||
244
src/app/leave-requests/page.tsx
Normal file
244
src/app/leave-requests/page.tsx
Normal file
@@ -0,0 +1,244 @@
|
||||
import { createClient } from '@/utils/supabase/server'
|
||||
import { updateLeaveStatus } from './actions'
|
||||
import { CalendarDaysIcon, CheckIcon, XMarkIcon } from '@heroicons/react/24/outline'
|
||||
|
||||
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()
|
||||
|
||||
const roleData: any = dbUser?.roles
|
||||
const roleName = Array.isArray(roleData) ? roleData[0]?.name : roleData?.name
|
||||
const isAdminOrManager = roleName === 'admin' || roleName === 'manager'
|
||||
|
||||
// Fetch leave requests
|
||||
// If admin/manager, fetch all. If employee, fetch only their own.
|
||||
let query = supabase
|
||||
.from('leave_requests')
|
||||
.select(`
|
||||
id,
|
||||
start_date,
|
||||
end_date,
|
||||
status,
|
||||
reason,
|
||||
created_at,
|
||||
employees!inner (
|
||||
id,
|
||||
user_id,
|
||||
companies ( name ),
|
||||
users( first_name, last_name, email )
|
||||
)
|
||||
`)
|
||||
.order('created_at', { ascending: false })
|
||||
|
||||
if (!isAdminOrManager) {
|
||||
query = query.eq('employees.user_id', user.id)
|
||||
}
|
||||
|
||||
const { data: requests } = await query
|
||||
|
||||
// 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)
|
||||
|
||||
return (
|
||||
<div className="p-4 sm:p-6 lg:p-8 max-w-7xl mx-auto">
|
||||
<div className="sm:flex sm:items-center">
|
||||
<div className="sm:flex-auto">
|
||||
<h1 className="text-2xl font-semibold leading-6 text-gray-900 dark:text-white flex items-center gap-2">
|
||||
<CalendarDaysIcon className="w-6 h-6 text-blue-500" />
|
||||
İzin Talepleri
|
||||
</h1>
|
||||
<p className="mt-2 text-sm text-gray-700 dark:text-gray-400">
|
||||
{isAdminOrManager
|
||||
? 'Tüm çalışanların izin taleplerini görüntüleyin ve onaylayın.'
|
||||
: 'Kendi izin taleplerinizi oluşturun ve durumlarını takip edin.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||
|
||||
{/* 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-md border-0 py-1.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-blue-600 sm:text-sm sm:leading-6 dark:bg-zinc-800 dark:text-white dark:ring-zinc-700"
|
||||
>
|
||||
{userCompanies?.filter((c: any) => c.companies).map((c: any) => (
|
||||
<option key={c.companies.id} value={c.companies.id}>{c.companies.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-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>
|
||||
|
||||
<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-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500"
|
||||
>
|
||||
Talep Oluştur
|
||||
</button>
|
||||
</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-gray-50 dark:bg-zinc-950/50">
|
||||
<tr>
|
||||
<th scope="col" className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 dark:text-white sm:pl-6">
|
||||
Çalışan
|
||||
</th>
|
||||
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-white">
|
||||
Tarih Aralığı
|
||||
</th>
|
||||
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-white">
|
||||
Gerekçe
|
||||
</th>
|
||||
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-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>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 dark:divide-zinc-800 bg-white dark:bg-zinc-900">
|
||||
{requests?.map((req: any) => {
|
||||
const employeeInfo = req.employees?.users || {};
|
||||
|
||||
return (
|
||||
<tr key={req.id}>
|
||||
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm sm:pl-6">
|
||||
<div className="font-medium text-gray-900 dark:text-white">
|
||||
{employeeInfo?.first_name} {employeeInfo?.last_name}
|
||||
</div>
|
||||
<div className="text-gray-500 dark:text-gray-400">{req.employees?.companies?.name}</div>
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
<div>{new Date(req.start_date).toLocaleDateString('tr-TR')}</div>
|
||||
<div>{new Date(req.end_date).toLocaleDateString('tr-TR')}</div>
|
||||
</td>
|
||||
<td className="px-3 py-4 text-sm text-gray-500 dark:text-gray-400 max-w-xs truncate">
|
||||
{req.reason}
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-3 py-4 text-sm">
|
||||
<span className={`inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ring-1 ring-inset ${
|
||||
req.status === 'approved' ? 'bg-green-50 text-green-700 ring-green-600/20 dark:bg-green-500/10 dark:text-green-400' :
|
||||
req.status === 'rejected' ? 'bg-red-50 text-red-700 ring-red-600/20 dark:bg-red-500/10 dark:text-red-400' :
|
||||
'bg-yellow-50 text-yellow-800 ring-yellow-600/20 dark:bg-yellow-500/10 dark:text-yellow-500'
|
||||
}`}>
|
||||
{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">
|
||||
<button
|
||||
formAction={async () => {
|
||||
'use server';
|
||||
await updateLeaveStatus(req.id, 'approved')
|
||||
}}
|
||||
className="text-green-600 hover:text-green-900 dark:text-green-500 dark:hover:text-green-400 bg-green-50 hover:bg-green-100 dark:bg-green-500/10 p-2 rounded-lg transition-colors" title="Onayla">
|
||||
<CheckIcon className="h-5 w-5" />
|
||||
</button>
|
||||
<button
|
||||
formAction={async () => {
|
||||
'use server';
|
||||
await updateLeaveStatus(req.id, 'rejected')
|
||||
}}
|
||||
className="text-red-600 hover:text-red-900 dark:text-red-500 dark:hover:text-red-400 bg-red-50 hover:bg-red-100 dark:bg-red-500/10 p-2 rounded-lg transition-colors" title="Reddet">
|
||||
<XMarkIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
)})}
|
||||
{(!requests || requests.length === 0) && (
|
||||
<tr>
|
||||
<td colSpan={isAdminOrManager ? 5 : 4} className="py-8 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
Henüz bir izin talebi bulunmamaktadır.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
25
src/app/login/actions.ts
Normal file
25
src/app/login/actions.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
'use server'
|
||||
|
||||
import { revalidatePath } from 'next/cache'
|
||||
import { redirect } from 'next/navigation'
|
||||
import { createClient } from '@/utils/supabase/server'
|
||||
|
||||
export async function login(formData: FormData) {
|
||||
const supabase = await createClient()
|
||||
|
||||
// type-casting here for convenience
|
||||
// in practice, you should validate your inputs
|
||||
const data = {
|
||||
email: formData.get('email') as string,
|
||||
password: formData.get('password') as string,
|
||||
}
|
||||
|
||||
const { error } = await supabase.auth.signInWithPassword(data)
|
||||
|
||||
if (error) {
|
||||
redirect('/login?error=true')
|
||||
}
|
||||
|
||||
revalidatePath('/', 'layout')
|
||||
redirect('/')
|
||||
}
|
||||
83
src/app/login/page.tsx
Normal file
83
src/app/login/page.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import { login } from './actions'
|
||||
|
||||
export default function LoginPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: { error?: string }
|
||||
}) {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center justify-center p-24 bg-gray-50 dark:bg-zinc-950">
|
||||
<div className="w-full max-w-sm p-8 space-y-8 bg-white dark:bg-zinc-900 rounded-xl shadow-lg border border-gray-100 dark:border-zinc-800">
|
||||
|
||||
<div className="text-center">
|
||||
<h2 className="text-2xl font-bold tracking-tight text-gray-900 dark:text-white">
|
||||
HRMS Sistemine Giriş
|
||||
</h2>
|
||||
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Lütfen e-posta ve şifrenizle giriş yapın.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form className="space-y-6" action={login}>
|
||||
{searchParams?.error && (
|
||||
<div className="p-3 text-sm text-red-500 bg-red-50 dark:bg-red-500/10 border border-red-200 dark:border-red-500/20 rounded-lg">
|
||||
E-posta adresi veya şifre hatalı.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="block text-sm font-medium leading-6 text-gray-900 dark:text-gray-300"
|
||||
>
|
||||
E-posta Adresi
|
||||
</label>
|
||||
<div className="mt-2">
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
className="block w-full rounded-md border-0 py-1.5 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-700 dark:bg-zinc-800 dark:text-white placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6 outline-none transition-shadow duration-200"
|
||||
placeholder="ornek@sirket.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block text-sm font-medium leading-6 text-gray-900 dark:text-gray-300"
|
||||
>
|
||||
Şifre
|
||||
</label>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
className="block w-full rounded-md border-0 py-1.5 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-700 dark:bg-zinc-800 dark:text-white placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6 outline-none transition-shadow duration-200"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
className="flex w-full justify-center rounded-md bg-blue-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 transition-colors duration-200"
|
||||
>
|
||||
Giriş Yap
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
149
src/app/page.tsx
149
src/app/page.tsx
@@ -1,65 +1,104 @@
|
||||
import Image from "next/image";
|
||||
import { createClient } from '@/utils/supabase/server'
|
||||
import { redirect } from 'next/navigation'
|
||||
import { UsersIcon, CalendarDaysIcon, BuildingOfficeIcon } from '@heroicons/react/24/outline'
|
||||
|
||||
export default async function Home() {
|
||||
const supabase = await createClient()
|
||||
|
||||
// Protect the route
|
||||
const { data: { user }, error } = await supabase.auth.getUser()
|
||||
if (error || !user) {
|
||||
redirect('/login')
|
||||
}
|
||||
|
||||
// Fetch the extended user metadata from our 'users' table
|
||||
const { data: dbUser } = await supabase
|
||||
.from('users')
|
||||
.select('*, roles(name, description)')
|
||||
.eq('id', user.id)
|
||||
.single()
|
||||
|
||||
const roleData: any = dbUser?.roles
|
||||
const role = Array.isArray(roleData) ? roleData[0] : roleData
|
||||
|
||||
const stats = [
|
||||
{ name: 'Toplam Çalışan', 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' },
|
||||
]
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
||||
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={100}
|
||||
height={20}
|
||||
priority
|
||||
/>
|
||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
||||
To get started, edit the page.tsx file.
|
||||
<div className="p-4 sm:p-6 lg:p-8 space-y-8 max-w-7xl mx-auto">
|
||||
|
||||
{/* Welcome Section - Premium Header */}
|
||||
<div className="relative overflow-hidden rounded-3xl bg-gradient-to-br from-blue-600 via-indigo-600 to-purple-700 p-8 shadow-2xl shadow-blue-500/20">
|
||||
<div className="relative z-10">
|
||||
<h1 className="text-3xl font-extrabold tracking-tight text-white mb-2">
|
||||
Hoş Geldiniz, {dbUser?.first_name || user.email?.split('@')[0]} 👋
|
||||
</h1>
|
||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
||||
Looking for a starting point or more instructions? Head over to{" "}
|
||||
<a
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
>
|
||||
Templates
|
||||
</a>{" "}
|
||||
or the{" "}
|
||||
<a
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
>
|
||||
Learning
|
||||
</a>{" "}
|
||||
center.
|
||||
<p className="max-w-xl text-blue-100/80 font-medium">
|
||||
IK Yönetim Sistemine tekrar hoş geldiniz. Bugün <span className="text-white font-bold underline decoration-blue-400 decoration-2 underline-offset-4">{role?.name || 'Kullanıcı'}</span> yetkileriyle sisteme bağlısınız.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
||||
<a
|
||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
{/* Decorative backdrop blobs */}
|
||||
<div className="absolute top-0 right-0 -mr-20 -mt-20 w-64 h-64 bg-white/10 rounded-full blur-3xl" />
|
||||
<div className="absolute bottom-0 left-0 -ml-20 -mb-20 w-64 h-64 bg-blue-400/10 rounded-full blur-3xl" />
|
||||
</div>
|
||||
|
||||
{/* Stats Grid */}
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{stats.map((item) => (
|
||||
<div
|
||||
key={item.name}
|
||||
className="group relative flex flex-col justify-between overflow-hidden rounded-2xl bg-white dark:bg-zinc-900 border border-gray-100 dark:border-zinc-800 p-6 shadow-sm hover:shadow-xl transition-all duration-300 hover:-translate-y-1"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Deploy Now
|
||||
</a>
|
||||
<a
|
||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Documentation
|
||||
</a>
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="rounded-xl bg-blue-50 dark:bg-blue-500/10 p-2.5 text-blue-600 dark:text-blue-400 group-hover:bg-blue-600 group-hover:text-white transition-colors duration-300">
|
||||
<item.icon className="h-6 w-6" aria-hidden="true" />
|
||||
</div>
|
||||
<div className={`text-xs font-bold leading-5 px-2 py-0.5 rounded-full ${
|
||||
item.changeType === 'increase' ? 'bg-green-100 text-green-700 dark:bg-green-500/20 dark:text-green-400' : 'bg-gray-100 text-gray-500 dark:bg-zinc-800 dark:text-gray-400'
|
||||
}`}>
|
||||
{item.changeType === 'increase' ? '+' : ''}{item.change}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm font-semibold text-gray-500 dark:text-gray-400 tracking-wide uppercase">{item.name}</p>
|
||||
<p className="mt-1 text-3xl font-bold text-gray-900 dark:text-white">{item.stat}</p>
|
||||
</div>
|
||||
{/* Subtle background icon for premium feel */}
|
||||
<item.icon className="absolute -right-4 -bottom-4 h-24 w-24 text-gray-100 dark:text-zinc-800/20 group-hover:scale-110 transition-transform duration-500 pointer-events-none" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Profile & Quick Actions section */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<div className="bg-white/50 dark:bg-zinc-900/50 backdrop-blur-sm border border-gray-100 dark:border-zinc-800 rounded-3xl p-8 shadow-sm">
|
||||
<h2 className="text-xl font-bold mb-6 dark:text-white flex items-center gap-2">
|
||||
<div className="w-1.5 h-8 bg-blue-600 rounded-full" />
|
||||
Sistem Bilgileri
|
||||
</h2>
|
||||
<ul className="space-y-4 text-sm">
|
||||
<li className="flex items-center justify-between border-b border-gray-100 dark:border-zinc-800/50 pb-4">
|
||||
<span className="font-semibold text-gray-500 dark:text-gray-400">E-posta</span>
|
||||
<span className="text-gray-900 dark:text-white font-medium">{user.email}</span>
|
||||
</li>
|
||||
<li className="flex items-center justify-between border-b border-gray-100 dark:border-zinc-800/50 pb-4">
|
||||
<span className="font-semibold text-gray-500 dark:text-gray-400">Hesap Tipi</span>
|
||||
<span className="inline-flex items-center gap-1.5 text-blue-600 dark:text-blue-400 font-bold">
|
||||
<div className="w-2 h-2 rounded-full bg-blue-500 animate-pulse" />
|
||||
{role?.description || 'Standart Erişim'}
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-center justify-between pt-2">
|
||||
<span className="font-semibold text-gray-500 dark:text-gray-400">Son Giriş</span>
|
||||
<span className="text-gray-900 dark:text-white font-medium">{new Date().toLocaleDateString('tr-TR')}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
25
src/components/layout/Header.tsx
Normal file
25
src/components/layout/Header.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
export function Header({ user }: { user?: { email?: string; first_name?: string } | null }) {
|
||||
return (
|
||||
<div className="sticky top-0 z-40 flex h-16 shrink-0 items-center justify-between gap-x-4 border-b border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-950 px-4 shadow-sm sm:gap-x-6 sm:px-6 lg:px-8">
|
||||
{/* Empty div for flex spacing since sidebar hamburger is handled by Sidebar component */}
|
||||
<div className="flex-1 lg:hidden"></div>
|
||||
|
||||
<div className="flex flex-1 justify-end items-center gap-x-4 lg:gap-x-6">
|
||||
{/* Profile dropdown */}
|
||||
<div className="flex items-center gap-x-4 lg:gap-x-6">
|
||||
<div className="hidden lg:block lg:h-6 lg:w-px lg:bg-gray-200 dark:bg-zinc-800" aria-hidden="true" />
|
||||
|
||||
<div className="flex items-center gap-x-3 text-sm font-semibold leading-6 text-gray-900 dark:text-white">
|
||||
<span className="sr-only">Profil</span>
|
||||
<div className="h-8 w-8 rounded-full bg-blue-100 dark:bg-blue-900/50 flex items-center justify-center text-blue-700 dark:text-blue-400 border border-blue-200 dark:border-blue-800">
|
||||
{user?.first_name?.[0]?.toUpperCase() || user?.email?.[0]?.toUpperCase() || 'U'}
|
||||
</div>
|
||||
<span className="hidden lg:flex lg:items-center">
|
||||
<span>{user?.first_name || user?.email?.split('@')[0] || 'Kullanıcı'}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
165
src/components/layout/Sidebar.tsx
Normal file
165
src/components/layout/Sidebar.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import {
|
||||
HomeIcon,
|
||||
UsersIcon,
|
||||
CalendarDaysIcon,
|
||||
BuildingOfficeIcon,
|
||||
Cog6ToothIcon,
|
||||
ArrowRightOnRectangleIcon,
|
||||
Bars3Icon,
|
||||
XMarkIcon,
|
||||
} from '@heroicons/react/24/outline'
|
||||
|
||||
const navigation = [
|
||||
{ name: 'Dashboard', href: '/', icon: HomeIcon },
|
||||
{ name: 'Çalışanlar', href: '/employees', icon: UsersIcon },
|
||||
{ name: 'İzinler', href: '/leave-requests', icon: CalendarDaysIcon },
|
||||
{ name: 'Şirketler', href: '/companies', icon: BuildingOfficeIcon },
|
||||
{ name: 'Ayarlar', href: '/settings', icon: Cog6ToothIcon },
|
||||
]
|
||||
|
||||
export function Sidebar() {
|
||||
const pathname = usePathname()
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Mobile sidebar trigger */}
|
||||
<div className="lg:hidden fixed top-0 left-0 w-full h-16 bg-white dark:bg-zinc-900 border-b border-gray-200 dark:border-zinc-800 flex items-center px-4 z-40">
|
||||
<button
|
||||
className="p-2 text-gray-500 hover:text-gray-900 dark:hover:text-white focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500 rounded-md"
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
>
|
||||
<span className="sr-only">Menüyü Aç</span>
|
||||
<Bars3Icon className="h-6 w-6" aria-hidden="true" />
|
||||
</button>
|
||||
<div className="ml-4 font-bold text-xl dark:text-white text-gray-900 tracking-tight">
|
||||
HRMS
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Sidebar Overlay */}
|
||||
{sidebarOpen && (
|
||||
<div className="relative z-50 lg:hidden" aria-modal="true">
|
||||
<div className="fixed inset-0 bg-gray-900/80 transition-opacity" onClick={() => setSidebarOpen(false)} />
|
||||
<div className="fixed inset-0 flex">
|
||||
<div className="relative mr-16 flex w-full max-w-xs flex-1 transform transition duration-300 ease-in-out">
|
||||
<div className="absolute left-full top-0 flex w-16 justify-center pt-5">
|
||||
<button
|
||||
type="button"
|
||||
className="-m-2.5 p-2.5"
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
>
|
||||
<span className="sr-only">Menüyü Kapat</span>
|
||||
<XMarkIcon className="h-6 w-6 text-white" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
{/* Mobile Sidebar Content */}
|
||||
<div className="flex grow flex-col gap-y-5 overflow-y-auto bg-white dark:bg-zinc-950 px-6 pb-4">
|
||||
<div className="flex h-16 shrink-0 items-center">
|
||||
<div className="font-bold text-2xl dark:text-white text-gray-900 tracking-tight flex items-center gap-2">
|
||||
<BuildingOfficeIcon className="w-8 h-8 text-blue-600" />
|
||||
HRMS
|
||||
</div>
|
||||
</div>
|
||||
<nav className="flex flex-1 flex-col">
|
||||
<ul role="list" className="flex flex-1 flex-col gap-y-7">
|
||||
<li>
|
||||
<ul role="list" className="-mx-2 space-y-1">
|
||||
{navigation.map((item) => (
|
||||
<li key={item.name}>
|
||||
<Link
|
||||
href={item.href}
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
className={`
|
||||
group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold
|
||||
${pathname === item.href
|
||||
? 'bg-gray-50 dark:bg-zinc-900 text-blue-600 dark:text-blue-400'
|
||||
: 'text-gray-700 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-gray-50 dark:hover:bg-zinc-900/50'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<item.icon
|
||||
className={`h-6 w-6 shrink-0 ${pathname === item.href ? 'text-blue-600 dark:text-blue-400' : 'text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400'}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{item.name}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Desktop Sidebar */}
|
||||
<div className="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-72 lg:flex-col">
|
||||
<div className="flex grow flex-col gap-y-5 overflow-y-auto border-r border-gray-200/50 dark:border-zinc-800/50 bg-white/80 dark:bg-zinc-950/80 backdrop-blur-xl px-6 pb-4">
|
||||
<div className="flex h-16 shrink-0 items-center mt-4">
|
||||
<div className="font-bold text-2xl dark:text-white text-gray-900 tracking-tight flex items-center gap-2 group cursor-pointer">
|
||||
<div className="p-2 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-lg shadow-lg shadow-blue-500/20 group-hover:scale-110 transition-transform duration-300">
|
||||
<BuildingOfficeIcon className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<span className="bg-clip-text text-transparent bg-gradient-to-r from-gray-900 via-gray-700 to-gray-900 dark:from-white dark:via-gray-300 dark:to-white">HRMS</span>
|
||||
</div>
|
||||
</div>
|
||||
<nav className="flex flex-1 flex-col">
|
||||
<ul role="list" className="flex flex-1 flex-col gap-y-7">
|
||||
<li>
|
||||
<div className="text-[10px] font-bold tracking-widest leading-6 text-gray-400 dark:text-zinc-500 mb-4 uppercase">YÖNETİM PANELİ</div>
|
||||
<ul role="list" className="-mx-2 space-y-2">
|
||||
{navigation.map((item) => (
|
||||
<li key={item.name}>
|
||||
<Link
|
||||
href={item.href}
|
||||
className={`
|
||||
group flex items-center gap-x-3 rounded-xl p-3 text-sm leading-6 font-semibold transition-all duration-300 relative overflow-hidden
|
||||
${pathname === item.href
|
||||
? 'bg-blue-600 shadow-md shadow-blue-600/20 text-white translate-x-1'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-500/5'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<item.icon
|
||||
className={`h-5 w-5 shrink-0 transition-colors duration-300 ${pathname === item.href ? 'text-white' : 'text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400'}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{item.name}
|
||||
{pathname === item.href && (
|
||||
<div className="absolute left-0 w-1 h-6 bg-white rounded-full opacity-50" />
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
<li className="mt-auto">
|
||||
<form action="/auth/signout" method="post" className="-mx-2 pb-4">
|
||||
<button
|
||||
type="submit"
|
||||
className="group flex w-full items-center gap-x-3 rounded-xl p-3 text-sm font-semibold leading-6 text-gray-500 hover:text-red-600 dark:text-zinc-400 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-red-500/5 transition-all duration-300"
|
||||
>
|
||||
<ArrowRightOnRectangleIcon
|
||||
className="h-5 w-5 shrink-0 text-gray-400 group-hover:text-red-500 transition-colors duration-300"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Güvenli Çıkış
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
140
supabase/migrations/init_schema.sql
Normal file
140
supabase/migrations/init_schema.sql
Normal file
@@ -0,0 +1,140 @@
|
||||
-- 1. Create Tables
|
||||
|
||||
-- Companies Table
|
||||
CREATE TABLE public.companies (
|
||||
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
|
||||
);
|
||||
|
||||
-- Roles Table
|
||||
CREATE TABLE public.roles (
|
||||
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
|
||||
);
|
||||
|
||||
-- Profiles/Users Table (Extending auth.users)
|
||||
CREATE TABLE public.users (
|
||||
id UUID REFERENCES auth.users(id) ON DELETE CASCADE PRIMARY KEY,
|
||||
first_name TEXT,
|
||||
last_name TEXT,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
phone TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
|
||||
);
|
||||
|
||||
-- Employees Table
|
||||
CREATE TABLE public.employees (
|
||||
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
user_id UUID REFERENCES public.users(id) ON DELETE CASCADE,
|
||||
company_id UUID REFERENCES public.companies(id) ON DELETE CASCADE,
|
||||
role_id UUID REFERENCES public.roles(id) ON DELETE RESTRICT,
|
||||
department TEXT,
|
||||
title TEXT,
|
||||
hire_date DATE,
|
||||
status TEXT DEFAULT 'active' CHECK (status IN ('active', 'inactive', 'terminated')),
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL,
|
||||
UNIQUE(user_id, company_id) -- An employee can only be tied to a specific company once
|
||||
);
|
||||
|
||||
-- Leave Requests Table
|
||||
CREATE TABLE public.leave_requests (
|
||||
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
employee_id UUID REFERENCES public.employees(id) ON DELETE CASCADE NOT NULL,
|
||||
start_date DATE NOT NULL,
|
||||
end_date DATE NOT NULL,
|
||||
status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'rejected', 'cancelled')),
|
||||
reason TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
|
||||
);
|
||||
|
||||
-- 2. Enable Row Level Security (RLS)
|
||||
ALTER TABLE public.companies ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.roles ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.users ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.employees ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.leave_requests ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- 3. Create Basic RLS Policies
|
||||
-- Temporarily, we want authenticated users to be able to read/write.
|
||||
-- In a real production setup, we'd add complex logic verifying auth.uid() against roles.
|
||||
|
||||
-- Companies Policies
|
||||
CREATE POLICY "Allow authenticated full access to companies"
|
||||
ON public.companies
|
||||
FOR ALL TO authenticated
|
||||
USING (true)
|
||||
WITH CHECK (true);
|
||||
|
||||
-- Roles Policies
|
||||
CREATE POLICY "Allow authenticated read access to roles"
|
||||
ON public.roles
|
||||
FOR SELECT TO authenticated
|
||||
USING (true);
|
||||
|
||||
-- Users Policies
|
||||
CREATE POLICY "Users can view all users"
|
||||
ON public.users
|
||||
FOR SELECT TO authenticated
|
||||
USING (true);
|
||||
|
||||
CREATE POLICY "Users can insert their own profile"
|
||||
ON public.users
|
||||
FOR INSERT TO authenticated
|
||||
WITH CHECK (auth.uid() = id);
|
||||
|
||||
CREATE POLICY "Users can update their own profile"
|
||||
ON public.users
|
||||
FOR UPDATE TO authenticated
|
||||
USING (auth.uid() = id);
|
||||
|
||||
-- Employees Policies
|
||||
CREATE POLICY "Allow authenticated full access to employees"
|
||||
ON public.employees
|
||||
FOR ALL TO authenticated
|
||||
USING (true)
|
||||
WITH CHECK (true);
|
||||
|
||||
-- Leave Requests Policies
|
||||
CREATE POLICY "Employees can manage their own leave requests"
|
||||
ON public.leave_requests
|
||||
FOR ALL TO authenticated
|
||||
USING (
|
||||
employee_id IN (
|
||||
SELECT id FROM public.employees WHERE user_id = auth.uid()
|
||||
)
|
||||
)
|
||||
WITH CHECK (
|
||||
employee_id IN (
|
||||
SELECT id FROM public.employees WHERE user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "Managers can view all leave requests"
|
||||
ON public.leave_requests
|
||||
FOR SELECT TO authenticated
|
||||
USING (true);
|
||||
|
||||
-- 4. Initial Seed Data
|
||||
INSERT INTO public.roles (name, description) VALUES
|
||||
('admin', 'Sistem Yöneticisi (Tüm yetkiler)'),
|
||||
('manager', 'Yönetici (Çalışan ve izin onay/red yetkisi)'),
|
||||
('employee', 'Standart Çalışan');
|
||||
|
||||
-- 5. Trigger for updated_at timestamps
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = now();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
CREATE TRIGGER update_users_modtime BEFORE UPDATE ON public.users FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column();
|
||||
CREATE TRIGGER update_employees_modtime BEFORE UPDATE ON public.employees FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column();
|
||||
CREATE TRIGGER update_leave_requests_modtime BEFORE UPDATE ON public.leave_requests FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column();
|
||||
Reference in New Issue
Block a user