settings
This commit is contained in:
@@ -0,0 +1,47 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import { createClient } from '@/utils/supabase/server';
|
||||||
|
import { revalidatePath } from 'next/cache';
|
||||||
|
|
||||||
|
export async function upsertLookup(table: string, data: any) {
|
||||||
|
const supabase = await createClient();
|
||||||
|
|
||||||
|
const { id, ...rest } = data;
|
||||||
|
|
||||||
|
let result;
|
||||||
|
if (id) {
|
||||||
|
result = await supabase
|
||||||
|
.from(table)
|
||||||
|
.update(rest)
|
||||||
|
.eq('id', id);
|
||||||
|
} else {
|
||||||
|
result = await supabase
|
||||||
|
.from(table)
|
||||||
|
.insert([rest]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
return { error: result.error.message };
|
||||||
|
}
|
||||||
|
|
||||||
|
revalidatePath('/settings');
|
||||||
|
revalidatePath('/employees');
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteLookup(table: string, id: string) {
|
||||||
|
const supabase = await createClient();
|
||||||
|
|
||||||
|
const { error } = await supabase
|
||||||
|
.from(table)
|
||||||
|
.delete()
|
||||||
|
.eq('id', id);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return { error: error.message };
|
||||||
|
}
|
||||||
|
|
||||||
|
revalidatePath('/settings');
|
||||||
|
revalidatePath('/employees');
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { createClient } from '@/utils/supabase/server';
|
||||||
|
import LookupManager from '@/components/settings/LookupManager';
|
||||||
|
import SectionManager from '@/components/settings/SectionManager';
|
||||||
|
import { Cog6ToothIcon } from '@heroicons/react/24/outline';
|
||||||
|
|
||||||
|
export default async function SettingsPage() {
|
||||||
|
const supabase = await createClient();
|
||||||
|
|
||||||
|
// Fetch all lookup data
|
||||||
|
const [
|
||||||
|
{ data: departments },
|
||||||
|
{ data: sections },
|
||||||
|
{ data: employmentTypes },
|
||||||
|
{ data: jobTitles }
|
||||||
|
] = await Promise.all([
|
||||||
|
supabase.from('departments').select('*').order('name'),
|
||||||
|
supabase.from('sections').select('*').order('name'),
|
||||||
|
supabase.from('employment_types').select('*').order('name'),
|
||||||
|
supabase.from('job_titles').select('*').order('name')
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-10">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-1.5 h-12 bg-[#173363] rounded-full" />
|
||||||
|
<div>
|
||||||
|
<h1 className="text-4xl font-black text-slate-900 dark:text-white tracking-tight flex items-center gap-3">
|
||||||
|
<Cog6ToothIcon className="w-10 h-10 text-[#173363]" />
|
||||||
|
Sistem Ayarları
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm font-bold text-slate-400 mt-1 uppercase tracking-widest">
|
||||||
|
Personel kartı veri tanımlamaları ve konfigürasyon
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grid Layout for Managers */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 pb-20">
|
||||||
|
|
||||||
|
<LookupManager
|
||||||
|
title="Departmanlar"
|
||||||
|
table="departments"
|
||||||
|
items={departments || []}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SectionManager
|
||||||
|
sections={sections || []}
|
||||||
|
departments={departments || []}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LookupManager
|
||||||
|
title="Çalışma Şekilleri"
|
||||||
|
table="employment_types"
|
||||||
|
items={employmentTypes || []}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LookupManager
|
||||||
|
title="Görev Ünvanları"
|
||||||
|
table="job_titles"
|
||||||
|
items={jobTitles || []}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { PlusIcon, PencilSquareIcon, TrashIcon, XMarkIcon } from '@heroicons/react/24/outline';
|
||||||
|
import { upsertLookup, deleteLookup } from '@/app/settings/actions';
|
||||||
|
|
||||||
|
interface LookupManagerProps {
|
||||||
|
title: string;
|
||||||
|
table: string;
|
||||||
|
items: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LookupManager({ title, table, items }: LookupManagerProps) {
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [editingItem, setEditingItem] = useState<any>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const openModal = (item?: any) => {
|
||||||
|
setEditingItem(item || null);
|
||||||
|
setIsModalOpen(true);
|
||||||
|
setError(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const formData = new FormData(e.currentTarget);
|
||||||
|
const name = formData.get('name') as string;
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
id: editingItem?.id,
|
||||||
|
name
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await upsertLookup(table, data);
|
||||||
|
if (result.error) {
|
||||||
|
setError(result.error);
|
||||||
|
} else {
|
||||||
|
setIsModalOpen(false);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (id: string) => {
|
||||||
|
if (confirm('Bu öğeyi silmek istediğinize emin misiniz?')) {
|
||||||
|
const result = await deleteLookup(table, id);
|
||||||
|
if (result.error) {
|
||||||
|
alert(result.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white dark:bg-zinc-900 rounded-[2.5rem] border border-slate-100 dark:border-zinc-800 shadow-sm overflow-hidden">
|
||||||
|
<div className="p-8 border-b border-slate-50 dark:border-zinc-800 flex items-center justify-between">
|
||||||
|
<h3 className="text-lg font-black text-[#173363] dark:text-white uppercase tracking-wider">{title}</h3>
|
||||||
|
<button
|
||||||
|
onClick={() => openModal()}
|
||||||
|
className="flex items-center gap-2 bg-[#173363] hover:bg-[#CE0515] text-white px-6 py-2.5 rounded-full font-black text-[10px] uppercase tracking-widest transition-all active:scale-95"
|
||||||
|
>
|
||||||
|
<PlusIcon className="w-4 h-4" />
|
||||||
|
YENİ EKLE
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="divide-y divide-slate-50 dark:divide-zinc-800 max-h-[500px] overflow-y-auto">
|
||||||
|
{items.map((item) => (
|
||||||
|
<div key={item.id} className="p-6 flex items-center justify-between group hover:bg-slate-50/50 dark:hover:bg-zinc-800/50 transition-all">
|
||||||
|
<span className="text-sm font-bold text-slate-700 dark:text-slate-300">{item.name}</span>
|
||||||
|
<div className="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-all">
|
||||||
|
<button
|
||||||
|
onClick={() => openModal(item)}
|
||||||
|
className="p-2 text-slate-400 hover:text-white hover:bg-[#173363] rounded-xl transition-all"
|
||||||
|
>
|
||||||
|
<PencilSquareIcon className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDelete(item.id)}
|
||||||
|
className="p-2 text-slate-400 hover:text-white hover:bg-[#CE0515] rounded-xl transition-all"
|
||||||
|
>
|
||||||
|
<TrashIcon className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{items.length === 0 && (
|
||||||
|
<div className="p-10 text-center text-slate-400 text-sm font-bold">
|
||||||
|
Henüz kayıt bulunamadı.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isModalOpen && (
|
||||||
|
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-slate-900/60 backdrop-blur-md">
|
||||||
|
<div className="bg-white dark:bg-zinc-900 w-full max-w-sm rounded-[2.5rem] shadow-2xl border border-slate-100 dark:border-zinc-800 overflow-hidden">
|
||||||
|
<div className="p-8 border-b border-slate-50 dark:border-zinc-800 flex items-center justify-between">
|
||||||
|
<h4 className="text-sm font-black text-[#173363] dark:text-white uppercase tracking-widest">
|
||||||
|
{editingItem ? 'DÜZENLE' : 'YENİ EKLE'}
|
||||||
|
</h4>
|
||||||
|
<button onClick={() => setIsModalOpen(false)} className="p-2 text-slate-300 hover:text-[#CE0515] transition-all">
|
||||||
|
<XMarkIcon className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<form onSubmit={handleSave} className="p-8 space-y-6">
|
||||||
|
{error && (
|
||||||
|
<div className="p-3 bg-red-50 text-red-600 rounded-xl text-[10px] font-black uppercase tracking-widest text-center border border-red-100">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<label className="block text-[10px] font-black uppercase tracking-widest text-slate-400 mb-2 ml-1">İSİM / TANIM</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
defaultValue={editingItem?.name}
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-3 bg-slate-50 dark:bg-zinc-800 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-[#173363] transition-all outline-none"
|
||||||
|
placeholder="Örn: İnsan Kaynakları"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="w-full bg-[#173363] hover:bg-[#CE0515] text-white py-4 rounded-full font-black text-[10px] uppercase tracking-widest shadow-lg transition-all active:scale-95 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading ? 'KAYDEDİLİYOR...' : 'KAYDET'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { PlusIcon, PencilSquareIcon, TrashIcon, XMarkIcon, BuildingOfficeIcon } from '@heroicons/react/24/outline';
|
||||||
|
import { upsertLookup, deleteLookup } from '@/app/settings/actions';
|
||||||
|
|
||||||
|
interface SectionManagerProps {
|
||||||
|
sections: any[];
|
||||||
|
departments: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SectionManager({ sections, departments }: SectionManagerProps) {
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [editingItem, setEditingItem] = useState<any>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const openModal = (item?: any) => {
|
||||||
|
setEditingItem(item || null);
|
||||||
|
setIsModalOpen(true);
|
||||||
|
setError(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const formData = new FormData(e.currentTarget);
|
||||||
|
const name = formData.get('name') as string;
|
||||||
|
const department_id = formData.get('department_id') as string;
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
id: editingItem?.id,
|
||||||
|
name,
|
||||||
|
department_id
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await upsertLookup('sections', data);
|
||||||
|
if (result.error) {
|
||||||
|
setError(result.error);
|
||||||
|
} else {
|
||||||
|
setIsModalOpen(false);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (id: string) => {
|
||||||
|
if (confirm('Bu bölümü silmek istediğinize emin misiniz?')) {
|
||||||
|
const result = await deleteLookup('sections', id);
|
||||||
|
if (result.error) {
|
||||||
|
alert(result.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDeptName = (id: string) => {
|
||||||
|
return departments.find(d => d.id === id)?.name || 'Bilinmiyor';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white dark:bg-zinc-900 rounded-[2.5rem] border border-slate-100 dark:border-zinc-800 shadow-sm overflow-hidden h-full flex flex-col">
|
||||||
|
<div className="p-8 border-b border-slate-50 dark:border-zinc-800 flex items-center justify-between">
|
||||||
|
<h3 className="text-lg font-black text-[#173363] dark:text-white uppercase tracking-wider">Bölümler</h3>
|
||||||
|
<button
|
||||||
|
onClick={() => openModal()}
|
||||||
|
className="flex items-center gap-2 bg-[#173363] hover:bg-[#CE0515] text-white px-6 py-2.5 rounded-full font-black text-[10px] uppercase tracking-widest transition-all active:scale-95"
|
||||||
|
>
|
||||||
|
<PlusIcon className="w-4 h-4" />
|
||||||
|
YENİ EKLE
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="divide-y divide-slate-50 dark:divide-zinc-800 overflow-y-auto flex-1">
|
||||||
|
{sections.map((item) => (
|
||||||
|
<div key={item.id} className="p-6 flex items-center justify-between group hover:bg-slate-50/50 dark:hover:bg-zinc-800/50 transition-all">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-bold text-slate-700 dark:text-slate-300">{item.name}</p>
|
||||||
|
<div className="flex items-center gap-1.5 mt-1 text-slate-400">
|
||||||
|
<BuildingOfficeIcon className="w-3 h-3" />
|
||||||
|
<p className="text-[10px] font-black uppercase tracking-widest">{getDeptName(item.department_id)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-all">
|
||||||
|
<button
|
||||||
|
onClick={() => openModal(item)}
|
||||||
|
className="p-2 text-slate-400 hover:text-white hover:bg-[#173363] rounded-xl transition-all"
|
||||||
|
>
|
||||||
|
<PencilSquareIcon className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDelete(item.id)}
|
||||||
|
className="p-2 text-slate-400 hover:text-white hover:bg-[#CE0515] rounded-xl transition-all"
|
||||||
|
>
|
||||||
|
<TrashIcon className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{sections.length === 0 && (
|
||||||
|
<div className="p-10 text-center text-slate-400 text-sm font-bold">
|
||||||
|
Henüz kayıt bulunamadı.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isModalOpen && (
|
||||||
|
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-slate-900/60 backdrop-blur-md">
|
||||||
|
<div className="bg-white dark:bg-zinc-900 w-full max-w-sm rounded-[2.5rem] shadow-2xl border border-slate-100 dark:border-zinc-800 overflow-hidden">
|
||||||
|
<div className="p-8 border-b border-slate-50 dark:border-zinc-800 flex items-center justify-between">
|
||||||
|
<h4 className="text-sm font-black text-[#173363] dark:text-white uppercase tracking-widest">
|
||||||
|
{editingItem ? 'DÜZENLE' : 'YENİ EKLE'}
|
||||||
|
</h4>
|
||||||
|
<button onClick={() => setIsModalOpen(false)} className="p-2 text-slate-300 hover:text-[#CE0515] transition-all">
|
||||||
|
<XMarkIcon className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<form onSubmit={handleSave} className="p-8 space-y-6">
|
||||||
|
{error && (
|
||||||
|
<div className="p-3 bg-red-50 text-red-600 rounded-xl text-[10px] font-black uppercase tracking-widest text-center border border-red-100">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-[10px] font-black uppercase tracking-widest text-slate-400 mb-2 ml-1">DEPARTMAN</label>
|
||||||
|
<select
|
||||||
|
name="department_id"
|
||||||
|
required
|
||||||
|
defaultValue={editingItem?.department_id}
|
||||||
|
className="w-full px-4 py-3 bg-slate-50 dark:bg-zinc-800 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-[#173363] transition-all outline-none appearance-none"
|
||||||
|
>
|
||||||
|
<option value="">Seçiniz</option>
|
||||||
|
{departments.map(d => (
|
||||||
|
<option key={d.id} value={d.id}>{d.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-[10px] font-black uppercase tracking-widest text-slate-400 mb-2 ml-1">BÖLÜM İSMİ</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
defaultValue={editingItem?.name}
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-3 bg-slate-50 dark:bg-zinc-800 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-[#173363] transition-all outline-none"
|
||||||
|
placeholder="Örn: Frontend Ekibi"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="w-full bg-[#173363] hover:bg-[#CE0515] text-white py-4 rounded-full font-black text-[10px] uppercase tracking-widest shadow-lg transition-all active:scale-95 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading ? 'KAYDEDİLİYOR...' : 'KAYDET'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user