Commit a901d48a authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat(hr): add Leave Type management CRUD (أنواع الإجازات)

Full admin UI to create/edit/view/archive leave types with all rule
fields: entitlement, limits, pay percentage, carry-over, accumulation,
gender restriction, service requirements, and legal references.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent fc229f20
<?php
declare(strict_types=1);
namespace App\Modules\HR\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\HR\Models\HrLeaveType;
class LeaveTypeController extends Controller
{
public function index(Request $request): Response
{
$this->authorize('hr.leave.manage');
$db = App::getInstance()->db();
$types = $db->select(
"SELECT * FROM hr_leave_types WHERE is_archived = 0 ORDER BY sort_order ASC, id ASC"
);
$categories = HrLeaveType::getCategories();
return $this->view('HR.Views.leave_types.index', [
'types' => $types,
'categories' => $categories,
]);
}
public function create(Request $request): Response
{
$this->authorize('hr.leave.manage');
return $this->view('HR.Views.leave_types.form', [
'leaveType' => null,
'categories' => HrLeaveType::getCategories(),
]);
}
public function store(Request $request): Response
{
$this->authorize('hr.leave.manage');
$data = $this->extractData($request);
$errors = $this->validateInput($data);
if (!empty($errors)) {
return $this->flashErrorsAndRedirect($errors, $request, '/hr/leave-types/create');
}
$db = App::getInstance()->db();
$existing = $db->selectOne(
"SELECT id FROM hr_leave_types WHERE code = ? AND is_archived = 0",
[$data['code']]
);
if ($existing) {
return $this->flashErrorsAndRedirect(
['كود النوع مستخدم بالفعل'],
$request,
'/hr/leave-types/create'
);
}
$leaveType = HrLeaveType::create($data);
return $this->redirect('/hr/leave-types/' . $leaveType->id)->withSuccess('تم إنشاء نوع الإجازة بنجاح');
}
public function show(Request $request, string $id): Response
{
$this->authorize('hr.leave.manage');
$leaveType = HrLeaveType::find((int) $id);
if (!$leaveType) {
return $this->redirect('/hr/leave-types')->withError('نوع الإجازة غير موجود');
}
$db = App::getInstance()->db();
$usageCount = $db->selectOne(
"SELECT COUNT(*) as cnt FROM hr_leave_requests WHERE leave_type_id = ? AND is_archived = 0",
[(int) $id]
);
return $this->view('HR.Views.leave_types.show', [
'leaveType' => $leaveType,
'usageCount' => (int) ($usageCount['cnt'] ?? 0),
'categories' => HrLeaveType::getCategories(),
]);
}
public function edit(Request $request, string $id): Response
{
$this->authorize('hr.leave.manage');
$leaveType = HrLeaveType::find((int) $id);
if (!$leaveType) {
return $this->redirect('/hr/leave-types')->withError('نوع الإجازة غير موجود');
}
return $this->view('HR.Views.leave_types.form', [
'leaveType' => $leaveType,
'categories' => HrLeaveType::getCategories(),
]);
}
public function update(Request $request, string $id): Response
{
$this->authorize('hr.leave.manage');
$leaveType = HrLeaveType::find((int) $id);
if (!$leaveType) {
return $this->redirect('/hr/leave-types')->withError('نوع الإجازة غير موجود');
}
$data = $this->extractData($request);
$errors = $this->validateInput($data, (int) $id);
if (!empty($errors)) {
return $this->flashErrorsAndRedirect($errors, $request, '/hr/leave-types/' . $id . '/edit');
}
$db = App::getInstance()->db();
$existing = $db->selectOne(
"SELECT id FROM hr_leave_types WHERE code = ? AND id != ? AND is_archived = 0",
[$data['code'], (int) $id]
);
if ($existing) {
return $this->flashErrorsAndRedirect(
['كود النوع مستخدم بالفعل بواسطة نوع إجازة آخر'],
$request,
'/hr/leave-types/' . $id . '/edit'
);
}
$leaveType->update($data);
return $this->redirect('/hr/leave-types/' . $id)->withSuccess('تم تحديث نوع الإجازة بنجاح');
}
public function archive(Request $request, string $id): Response
{
$this->authorize('hr.leave.manage');
$leaveType = HrLeaveType::find((int) $id);
if (!$leaveType) {
return $this->redirect('/hr/leave-types')->withError('نوع الإجازة غير موجود');
}
$db = App::getInstance()->db();
$activeRequests = $db->selectOne(
"SELECT COUNT(*) as cnt FROM hr_leave_requests WHERE leave_type_id = ? AND status IN ('pending','approved') AND is_archived = 0",
[(int) $id]
);
if ((int) ($activeRequests['cnt'] ?? 0) > 0) {
return $this->redirect('/hr/leave-types/' . $id)->withError('لا يمكن حذف نوع إجازة له طلبات نشطة (' . $activeRequests['cnt'] . ' طلب)');
}
$db->update('hr_leave_types', [
'is_archived' => 1,
'archived_at' => date('Y-m-d H:i:s'),
'archived_by' => App::getInstance()->session()->get('employee_id'),
], '`id` = ?', [(int) $id]);
return $this->redirect('/hr/leave-types')->withSuccess('تم حذف نوع الإجازة بنجاح');
}
private function extractData(Request $request): array
{
return [
'code' => strtolower(trim((string) $request->post('code', ''))),
'name_ar' => trim((string) $request->post('name_ar', '')),
'name_en' => trim((string) $request->post('name_en', '')) ?: null,
'category' => trim((string) $request->post('category', '')) ?: null,
'default_days_per_year' => $this->nullableDecimal($request->post('default_days_per_year')),
'max_days_per_year' => $this->nullableDecimal($request->post('max_days_per_year')),
'is_paid' => (int) ($request->post('is_paid', 1)),
'pay_percentage' => (float) ($request->post('pay_percentage', 100)),
'requires_approval' => (int) ($request->post('requires_approval', 1)),
'requires_attachment' => (int) ($request->post('requires_attachment', 0)),
'min_days_per_request' => $this->nullableDecimal($request->post('min_days_per_request')),
'max_days_per_request' => $this->nullableDecimal($request->post('max_days_per_request')),
'min_consecutive_annual' => $this->nullableDecimal($request->post('min_consecutive_annual')),
'max_consecutive_days' => $this->nullableInt($request->post('max_consecutive_days')),
'max_per_occurrence' => $this->nullableInt($request->post('max_per_occurrence')),
'max_times_in_career' => $this->nullableInt($request->post('max_times_in_career')),
'min_service_months' => $this->nullableInt($request->post('min_service_months')),
'advance_notice_days' => (int) ($request->post('advance_notice_days', 0)),
'gender_restriction' => trim((string) $request->post('gender_restriction', '')) ?: null,
'carry_over_allowed' => (int) ($request->post('carry_over_allowed', 0)),
'max_carry_over_days' => $this->nullableDecimal($request->post('max_carry_over_days')),
'carry_over_expiry_months' => $this->nullableInt($request->post('carry_over_expiry_months')),
'is_accumulative' => (int) ($request->post('is_accumulative', 0)),
'accumulation_max_years' => $this->nullableInt($request->post('accumulation_max_years')),
'career_max_times' => $this->nullableInt($request->post('career_max_times')),
'career_max_days' => $this->nullableDecimal($request->post('career_max_days')),
'deduct_from_salary' => (int) ($request->post('deduct_from_salary', 0)),
'legal_reference' => trim((string) $request->post('legal_reference', '')) ?: null,
'is_active' => (int) ($request->post('is_active', 1)),
'sort_order' => (int) ($request->post('sort_order', 0)),
];
}
private function validateInput(array $data, ?int $excludeId = null): array
{
$errors = [];
if ($data['code'] === '' || mb_strlen($data['code']) < 2) {
$errors[] = 'كود النوع مطلوب (حرفان على الأقل)';
}
if (!preg_match('/^[a-z][a-z0-9_]*$/', $data['code'])) {
$errors[] = 'كود النوع يجب أن يكون بالإنجليزية (حروف صغيرة وأرقام و _ فقط)';
}
if ($data['name_ar'] === '' || mb_strlen($data['name_ar']) < 2) {
$errors[] = 'اسم النوع بالعربي مطلوب (حرفان على الأقل)';
}
if ($data['pay_percentage'] < 0 || $data['pay_percentage'] > 100) {
$errors[] = 'نسبة الراتب يجب أن تكون بين 0 و 100';
}
return $errors;
}
private function nullableDecimal(mixed $value): ?float
{
$v = trim((string) ($value ?? ''));
return $v !== '' ? (float) $v : null;
}
private function nullableInt(mixed $value): ?int
{
$v = trim((string) ($value ?? ''));
return $v !== '' && $v !== '0' ? (int) $v : ($v === '0' ? 0 : null);
}
private function flashErrorsAndRedirect(array $errors, Request $request, string $url): Response
{
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $errors));
$session->flash('_old_input', $request->all());
return $this->redirect($url);
}
}
......@@ -18,10 +18,12 @@ class HrLeaveType extends Model
'default_days_per_year', 'max_days_per_year', 'pay_percentage',
'is_paid', 'requires_approval', 'requires_attachment',
'min_days_per_request', 'max_days_per_request', 'min_consecutive_annual',
'advance_notice_days', 'carry_over_allowed', 'max_carry_over_days',
'carry_over_expiry_months', 'accumulation_max_years',
'max_consecutive_days', 'max_per_occurrence', 'max_times_in_career',
'min_service_months', 'advance_notice_days',
'carry_over_allowed', 'max_carry_over_days',
'carry_over_expiry_months', 'is_accumulative', 'accumulation_max_years',
'gender_restriction', 'career_max_times', 'career_max_days',
'legal_reference', 'is_active', 'sort_order', 'rules_json',
'deduct_from_salary', 'legal_reference', 'is_active', 'sort_order', 'rules_json',
];
public static function allActive(): array
......
......@@ -65,6 +65,15 @@ return [
['POST', '/hr/attendance/approve', 'HR\Controllers\AttendanceController@approveBulk', ['auth', 'csrf'], 'hr.attendance.approve'],
['GET', '/hr/attendance/monthly/{employeeId:\d+}', 'HR\Controllers\AttendanceController@monthly', ['auth'], 'hr.attendance.view'],
// ── Leave Types (Admin) ──
['GET', '/hr/leave-types', 'HR\Controllers\LeaveTypeController@index', ['auth'], 'hr.leave.manage'],
['GET', '/hr/leave-types/create', 'HR\Controllers\LeaveTypeController@create', ['auth'], 'hr.leave.manage'],
['POST', '/hr/leave-types', 'HR\Controllers\LeaveTypeController@store', ['auth', 'csrf'], 'hr.leave.manage'],
['GET', '/hr/leave-types/{id:\d+}', 'HR\Controllers\LeaveTypeController@show', ['auth'], 'hr.leave.manage'],
['GET', '/hr/leave-types/{id:\d+}/edit', 'HR\Controllers\LeaveTypeController@edit', ['auth'], 'hr.leave.manage'],
['POST', '/hr/leave-types/{id:\d+}/update', 'HR\Controllers\LeaveTypeController@update', ['auth', 'csrf'], 'hr.leave.manage'],
['POST', '/hr/leave-types/{id:\d+}/archive', 'HR\Controllers\LeaveTypeController@archive', ['auth', 'csrf'], 'hr.leave.manage'],
// ── Leaves ──
['GET', '/hr/leaves', 'HR\Controllers\LeaveController@index', ['auth'], 'hr.leave.view'],
['GET', '/hr/leaves/request', 'HR\Controllers\LeaveController@requestForm', ['auth'], 'hr.leave.request'],
......
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?><?= $leaveType ? 'تعديل نوع الإجازة' : 'إضافة نوع إجازة' ?><?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$isEdit = $leaveType !== null;
$action = $isEdit ? '/hr/leave-types/' . $leaveType['id'] . '/update' : '/hr/leave-types';
$v = fn(string $key, $default = '') => e(old($key) ?? ($leaveType[$key] ?? $default));
$checked = fn(string $key, $default = 0) => ((int)(old($key) ?? ($leaveType[$key] ?? $default))) ? 'checked' : '';
?>
<form method="POST" action="<?= $action ?>" style="max-width:900px;">
<?= csrf_field() ?>
<!-- القسم الأساسي -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:16px;border-bottom:1px solid #E5E7EB;font-weight:600;">البيانات الأساسية</div>
<div style="padding:20px;display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px;">
<div>
<label style="display:block;margin-bottom:4px;font-size:13px;font-weight:500;">كود النوع <span style="color:#DC2626;">*</span></label>
<input type="text" name="code" value="<?= $v('code') ?>" class="form-control" placeholder="annual, sick, casual..." dir="ltr" style="text-align:left;" <?= $isEdit ? '' : '' ?>>
<small style="color:#6B7280;font-size:11px;">حروف إنجليزية صغيرة وأرقام و _ فقط</small>
</div>
<div>
<label style="display:block;margin-bottom:4px;font-size:13px;font-weight:500;">الاسم بالعربي <span style="color:#DC2626;">*</span></label>
<input type="text" name="name_ar" value="<?= $v('name_ar') ?>" class="form-control" placeholder="إجازة سنوية">
</div>
<div>
<label style="display:block;margin-bottom:4px;font-size:13px;font-weight:500;">الاسم بالإنجليزي</label>
<input type="text" name="name_en" value="<?= $v('name_en') ?>" class="form-control" placeholder="Annual Leave" dir="ltr" style="text-align:left;">
</div>
<div>
<label style="display:block;margin-bottom:4px;font-size:13px;font-weight:500;">التصنيف</label>
<select name="category" class="form-control">
<option value="">— بدون تصنيف —</option>
<?php foreach ($categories as $k => $label): ?>
<option value="<?= e($k) ?>" <?= (old('category') ?? ($leaveType['category'] ?? '')) === $k ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label style="display:block;margin-bottom:4px;font-size:13px;font-weight:500;">الترتيب</label>
<input type="number" name="sort_order" value="<?= $v('sort_order', '0') ?>" class="form-control" min="0">
</div>
<div>
<label style="display:block;margin-bottom:4px;font-size:13px;font-weight:500;">تقييد الجنس</label>
<select name="gender_restriction" class="form-control">
<option value="" <?= (old('gender_restriction') ?? ($leaveType['gender_restriction'] ?? '')) === '' ? 'selected' : '' ?>>بدون تقييد (الكل)</option>
<option value="male" <?= (old('gender_restriction') ?? ($leaveType['gender_restriction'] ?? '')) === 'male' ? 'selected' : '' ?>>ذكور فقط</option>
<option value="female" <?= (old('gender_restriction') ?? ($leaveType['gender_restriction'] ?? '')) === 'female' ? 'selected' : '' ?>>إناث فقط</option>
</select>
</div>
</div>
</div>
<!-- الاستحقاق والحدود -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:16px;border-bottom:1px solid #E5E7EB;font-weight:600;">الاستحقاق والحدود</div>
<div style="padding:20px;display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px;">
<div>
<label style="display:block;margin-bottom:4px;font-size:13px;font-weight:500;">الأيام الافتراضية/سنة</label>
<input type="number" name="default_days_per_year" value="<?= $v('default_days_per_year') ?>" class="form-control" step="0.5" min="0">
</div>
<div>
<label style="display:block;margin-bottom:4px;font-size:13px;font-weight:500;">الحد الأقصى أيام/سنة</label>
<input type="number" name="max_days_per_year" value="<?= $v('max_days_per_year') ?>" class="form-control" step="0.5" min="0">
</div>
<div>
<label style="display:block;margin-bottom:4px;font-size:13px;font-weight:500;">أقل أيام في الطلب</label>
<input type="number" name="min_days_per_request" value="<?= $v('min_days_per_request') ?>" class="form-control" step="0.5" min="0">
</div>
<div>
<label style="display:block;margin-bottom:4px;font-size:13px;font-weight:500;">أقصى أيام في الطلب</label>
<input type="number" name="max_days_per_request" value="<?= $v('max_days_per_request') ?>" class="form-control" step="0.5" min="0">
</div>
<div>
<label style="display:block;margin-bottom:4px;font-size:13px;font-weight:500;">أقل أيام متصلة (سنوية)</label>
<input type="number" name="min_consecutive_annual" value="<?= $v('min_consecutive_annual') ?>" class="form-control" step="0.5" min="0">
<small style="color:#6B7280;font-size:11px;">مادة 48 قانون العمل</small>
</div>
<div>
<label style="display:block;margin-bottom:4px;font-size:13px;font-weight:500;">أقصى أيام متصلة</label>
<input type="number" name="max_consecutive_days" value="<?= $v('max_consecutive_days') ?>" class="form-control" min="0">
</div>
<div>
<label style="display:block;margin-bottom:4px;font-size:13px;font-weight:500;">أقصى مرات لكل حدث</label>
<input type="number" name="max_per_occurrence" value="<?= $v('max_per_occurrence') ?>" class="form-control" min="0">
</div>
<div>
<label style="display:block;margin-bottom:4px;font-size:13px;font-weight:500;">أقصى مرات طوال الخدمة</label>
<input type="number" name="max_times_in_career" value="<?= $v('max_times_in_career') ?>" class="form-control" min="0">
</div>
<div>
<label style="display:block;margin-bottom:4px;font-size:13px;font-weight:500;">الحد الأقصى أيام طوال الخدمة</label>
<input type="number" name="career_max_days" value="<?= $v('career_max_days') ?>" class="form-control" step="0.5" min="0">
</div>
</div>
</div>
<!-- شروط الاستحقاق -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:16px;border-bottom:1px solid #E5E7EB;font-weight:600;">شروط الاستحقاق</div>
<div style="padding:20px;display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px;">
<div>
<label style="display:block;margin-bottom:4px;font-size:13px;font-weight:500;">الحد الأدنى لأشهر الخدمة</label>
<input type="number" name="min_service_months" value="<?= $v('min_service_months') ?>" class="form-control" min="0">
<small style="color:#6B7280;font-size:11px;">عدد أشهر الخدمة المطلوبة للاستحقاق</small>
</div>
<div>
<label style="display:block;margin-bottom:4px;font-size:13px;font-weight:500;">أيام الإخطار المسبق</label>
<input type="number" name="advance_notice_days" value="<?= $v('advance_notice_days', '0') ?>" class="form-control" min="0">
</div>
<div>
<label style="display:block;margin-bottom:4px;font-size:13px;font-weight:500;">المرجع القانوني</label>
<input type="text" name="legal_reference" value="<?= $v('legal_reference') ?>" class="form-control" placeholder="مادة 47 قانون العمل">
</div>
</div>
</div>
<!-- الراتب والخصومات -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:16px;border-bottom:1px solid #E5E7EB;font-weight:600;">الراتب والخصومات</div>
<div style="padding:20px;display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px;">
<div style="display:flex;align-items:center;gap:8px;">
<input type="hidden" name="is_paid" value="0">
<input type="checkbox" name="is_paid" value="1" id="is_paid" <?= $checked('is_paid', 1) ?> style="width:18px;height:18px;">
<label for="is_paid" style="font-size:13px;font-weight:500;cursor:pointer;">إجازة مدفوعة</label>
</div>
<div>
<label style="display:block;margin-bottom:4px;font-size:13px;font-weight:500;">نسبة الراتب %</label>
<input type="number" name="pay_percentage" value="<?= $v('pay_percentage', '100') ?>" class="form-control" min="0" max="100" step="0.01">
<small style="color:#6B7280;font-size:11px;">75% = ثلاثة أرباع الراتب</small>
</div>
<div style="display:flex;align-items:center;gap:8px;">
<input type="hidden" name="deduct_from_salary" value="0">
<input type="checkbox" name="deduct_from_salary" value="1" id="deduct_from_salary" <?= $checked('deduct_from_salary', 0) ?> style="width:18px;height:18px;">
<label for="deduct_from_salary" style="font-size:13px;font-weight:500;cursor:pointer;">خصم من الراتب</label>
</div>
</div>
</div>
<!-- الترحيل والتراكم -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:16px;border-bottom:1px solid #E5E7EB;font-weight:600;">الترحيل والتراكم</div>
<div style="padding:20px;display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px;">
<div style="display:flex;align-items:center;gap:8px;">
<input type="hidden" name="carry_over_allowed" value="0">
<input type="checkbox" name="carry_over_allowed" value="1" id="carry_over_allowed" <?= $checked('carry_over_allowed', 0) ?> style="width:18px;height:18px;">
<label for="carry_over_allowed" style="font-size:13px;font-weight:500;cursor:pointer;">يسمح بالترحيل</label>
</div>
<div>
<label style="display:block;margin-bottom:4px;font-size:13px;font-weight:500;">أقصى أيام ترحيل</label>
<input type="number" name="max_carry_over_days" value="<?= $v('max_carry_over_days') ?>" class="form-control" step="0.5" min="0">
</div>
<div>
<label style="display:block;margin-bottom:4px;font-size:13px;font-weight:500;">صلاحية الترحيل (أشهر)</label>
<input type="number" name="carry_over_expiry_months" value="<?= $v('carry_over_expiry_months') ?>" class="form-control" min="0">
</div>
<div style="display:flex;align-items:center;gap:8px;">
<input type="hidden" name="is_accumulative" value="0">
<input type="checkbox" name="is_accumulative" value="1" id="is_accumulative" <?= $checked('is_accumulative', 0) ?> style="width:18px;height:18px;">
<label for="is_accumulative" style="font-size:13px;font-weight:500;cursor:pointer;">تراكمية</label>
</div>
<div>
<label style="display:block;margin-bottom:4px;font-size:13px;font-weight:500;">أقصى سنوات تراكم</label>
<input type="number" name="accumulation_max_years" value="<?= $v('accumulation_max_years') ?>" class="form-control" min="0">
<small style="color:#6B7280;font-size:11px;">مادة 48 - لا تزيد عن 3 سنوات</small>
</div>
</div>
</div>
<!-- الإعدادات -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:16px;border-bottom:1px solid #E5E7EB;font-weight:600;">إعدادات النوع</div>
<div style="padding:20px;display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px;">
<div style="display:flex;align-items:center;gap:8px;">
<input type="hidden" name="requires_approval" value="0">
<input type="checkbox" name="requires_approval" value="1" id="requires_approval" <?= $checked('requires_approval', 1) ?> style="width:18px;height:18px;">
<label for="requires_approval" style="font-size:13px;font-weight:500;cursor:pointer;">تحتاج اعتماد</label>
</div>
<div style="display:flex;align-items:center;gap:8px;">
<input type="hidden" name="requires_attachment" value="0">
<input type="checkbox" name="requires_attachment" value="1" id="requires_attachment" <?= $checked('requires_attachment', 0) ?> style="width:18px;height:18px;">
<label for="requires_attachment" style="font-size:13px;font-weight:500;cursor:pointer;">تحتاج مرفق</label>
</div>
<div style="display:flex;align-items:center;gap:8px;">
<input type="hidden" name="is_active" value="0">
<input type="checkbox" name="is_active" value="1" id="is_active" <?= $checked('is_active', 1) ?> style="width:18px;height:18px;">
<label for="is_active" style="font-size:13px;font-weight:500;cursor:pointer;">مفعّل</label>
</div>
</div>
</div>
<div style="display:flex;gap:12px;justify-content:flex-start;">
<button type="submit" class="btn btn-primary"><?= $isEdit ? 'حفظ التعديلات' : 'إنشاء نوع الإجازة' ?></button>
<a href="/hr/leave-types" class="btn btn-secondary">إلغاء</a>
</div>
</form>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>أنواع الإجازات<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<?php if (can('hr.leave.manage')): ?>
<a href="/hr/leave-types/create" class="btn btn-primary" style="display:inline-flex;align-items:center;gap:6px;">
<i data-lucide="plus" style="width:16px;height:16px;"></i> إضافة نوع إجازة
</a>
<?php endif; ?>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card">
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th style="width:50px;">#</th>
<th>الكود</th>
<th>الاسم</th>
<th>التصنيف</th>
<th>الأيام/سنة</th>
<th>مدفوعة</th>
<th>نسبة الراتب</th>
<th>الجنس</th>
<th>ترحيل</th>
<th>الحالة</th>
<th>إجراءات</th>
</tr>
</thead>
<tbody>
<?php if (empty($types)): ?>
<tr><td colspan="11" style="text-align:center;padding:40px;color:#9CA3AF;">لا يوجد أنواع إجازات</td></tr>
<?php else: ?>
<?php foreach ($types as $type): ?>
<tr>
<td><?= (int) $type['sort_order'] ?></td>
<td><code style="font-size:12px;background:#F3F4F6;padding:2px 6px;border-radius:4px;"><?= e($type['code']) ?></code></td>
<td><a href="/hr/leave-types/<?= (int) $type['id'] ?>"><?= e($type['name_ar']) ?></a></td>
<td><?= e($categories[$type['category'] ?? ''] ?? '-') ?></td>
<td><?= $type['default_days_per_year'] !== null ? number_format((float)$type['default_days_per_year'], 1) : '-' ?></td>
<td>
<?php if ((int) $type['is_paid']): ?>
<span style="color:#059669;">✓ مدفوعة</span>
<?php else: ?>
<span style="color:#DC2626;">✗ بدون راتب</span>
<?php endif; ?>
</td>
<td><?= number_format((float)$type['pay_percentage'], 0) ?>%</td>
<td>
<?php
$genderMap = ['male' => 'ذكور فقط', 'female' => 'إناث فقط'];
echo e($genderMap[$type['gender_restriction'] ?? ''] ?? 'الكل');
?>
</td>
<td>
<?php if ((int) $type['carry_over_allowed']): ?>
<span style="color:#059669;"></span>
<?php else: ?>
<span style="color:#9CA3AF;"></span>
<?php endif; ?>
</td>
<td>
<?php if ((int) $type['is_active']): ?>
<span style="display:inline-block;padding:2px 8px;border-radius:9999px;font-size:12px;background:#DEF7EC;color:#03543F;">مفعّل</span>
<?php else: ?>
<span style="display:inline-block;padding:2px 8px;border-radius:9999px;font-size:12px;background:#FDE8E8;color:#9B1C1C;">معطّل</span>
<?php endif; ?>
</td>
<td style="white-space:nowrap;">
<a href="/hr/leave-types/<?= (int) $type['id'] ?>" title="عرض" style="color:#2563EB;margin-inline-end:8px;"><i data-lucide="eye" style="width:16px;height:16px;"></i></a>
<a href="/hr/leave-types/<?= (int) $type['id'] ?>/edit" title="تعديل" style="color:#059669;"><i data-lucide="edit" style="width:16px;height:16px;"></i></a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<script>if(typeof lucide!=='undefined')lucide.createIcons();</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>نوع الإجازة: <?= e($leaveType['name_ar']) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<?php if (can('hr.leave.manage')): ?>
<a href="/hr/leave-types/<?= (int) $leaveType['id'] ?>/edit" class="btn btn-primary" style="display:inline-flex;align-items:center;gap:6px;">
<i data-lucide="edit" style="width:16px;height:16px;"></i> تعديل
</a>
<?php endif; ?>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$genderMap = ['male' => 'ذكور فقط', 'female' => 'إناث فقط'];
?>
<div style="display:grid;grid-template-columns:2fr 1fr;gap:20px;">
<!-- البيانات الأساسية -->
<div>
<div class="card" style="margin-bottom:20px;">
<div style="padding:16px;border-bottom:1px solid #E5E7EB;font-weight:600;">البيانات الأساسية</div>
<div style="padding:20px;">
<table style="width:100%;border-collapse:collapse;">
<tr style="border-bottom:1px solid #F3F4F6;"><td style="padding:10px 0;color:#6B7280;width:180px;">الكود</td><td style="padding:10px 0;"><code style="background:#F3F4F6;padding:2px 8px;border-radius:4px;"><?= e($leaveType['code']) ?></code></td></tr>
<tr style="border-bottom:1px solid #F3F4F6;"><td style="padding:10px 0;color:#6B7280;">الاسم بالعربي</td><td style="padding:10px 0;font-weight:600;"><?= e($leaveType['name_ar']) ?></td></tr>
<tr style="border-bottom:1px solid #F3F4F6;"><td style="padding:10px 0;color:#6B7280;">الاسم بالإنجليزي</td><td style="padding:10px 0;"><?= e($leaveType['name_en'] ?? '-') ?></td></tr>
<tr style="border-bottom:1px solid #F3F4F6;"><td style="padding:10px 0;color:#6B7280;">التصنيف</td><td style="padding:10px 0;"><?= e($categories[$leaveType['category'] ?? ''] ?? '-') ?></td></tr>
<tr style="border-bottom:1px solid #F3F4F6;"><td style="padding:10px 0;color:#6B7280;">تقييد الجنس</td><td style="padding:10px 0;"><?= e($genderMap[$leaveType['gender_restriction'] ?? ''] ?? 'بدون تقييد') ?></td></tr>
<tr style="border-bottom:1px solid #F3F4F6;"><td style="padding:10px 0;color:#6B7280;">المرجع القانوني</td><td style="padding:10px 0;"><?= e($leaveType['legal_reference'] ?? '-') ?></td></tr>
<tr><td style="padding:10px 0;color:#6B7280;">عدد الطلبات المسجلة</td><td style="padding:10px 0;"><strong><?= $usageCount ?></strong> طلب</td></tr>
</table>
</div>
</div>
<!-- الاستحقاق والحدود -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:16px;border-bottom:1px solid #E5E7EB;font-weight:600;">الاستحقاق والحدود</div>
<div style="padding:20px;">
<table style="width:100%;border-collapse:collapse;">
<tr style="border-bottom:1px solid #F3F4F6;"><td style="padding:10px 0;color:#6B7280;width:220px;">الأيام الافتراضية/سنة</td><td style="padding:10px 0;"><?= $leaveType['default_days_per_year'] !== null ? number_format((float)$leaveType['default_days_per_year'], 1) . ' يوم' : '-' ?></td></tr>
<tr style="border-bottom:1px solid #F3F4F6;"><td style="padding:10px 0;color:#6B7280;">الحد الأقصى أيام/سنة</td><td style="padding:10px 0;"><?= $leaveType['max_days_per_year'] !== null ? number_format((float)$leaveType['max_days_per_year'], 1) . ' يوم' : '-' ?></td></tr>
<tr style="border-bottom:1px solid #F3F4F6;"><td style="padding:10px 0;color:#6B7280;">أقل أيام في الطلب</td><td style="padding:10px 0;"><?= $leaveType['min_days_per_request'] !== null ? number_format((float)$leaveType['min_days_per_request'], 1) : '-' ?></td></tr>
<tr style="border-bottom:1px solid #F3F4F6;"><td style="padding:10px 0;color:#6B7280;">أقصى أيام في الطلب</td><td style="padding:10px 0;"><?= $leaveType['max_days_per_request'] !== null ? number_format((float)$leaveType['max_days_per_request'], 1) : '-' ?></td></tr>
<tr style="border-bottom:1px solid #F3F4F6;"><td style="padding:10px 0;color:#6B7280;">أقل أيام متصلة (سنوية)</td><td style="padding:10px 0;"><?= $leaveType['min_consecutive_annual'] !== null ? number_format((float)$leaveType['min_consecutive_annual'], 1) : '-' ?></td></tr>
<tr style="border-bottom:1px solid #F3F4F6;"><td style="padding:10px 0;color:#6B7280;">أقصى أيام متصلة</td><td style="padding:10px 0;"><?= $leaveType['max_consecutive_days'] !== null ? $leaveType['max_consecutive_days'] . ' يوم' : '-' ?></td></tr>
<tr style="border-bottom:1px solid #F3F4F6;"><td style="padding:10px 0;color:#6B7280;">أقصى مرات لكل حدث</td><td style="padding:10px 0;"><?= $leaveType['max_per_occurrence'] ?? '-' ?></td></tr>
<tr style="border-bottom:1px solid #F3F4F6;"><td style="padding:10px 0;color:#6B7280;">أقصى مرات طوال الخدمة</td><td style="padding:10px 0;"><?= $leaveType['max_times_in_career'] ?? '-' ?></td></tr>
<tr><td style="padding:10px 0;color:#6B7280;">أقصى أيام طوال الخدمة</td><td style="padding:10px 0;"><?= $leaveType['career_max_days'] !== null ? number_format((float)$leaveType['career_max_days'], 1) : '-' ?></td></tr>
</table>
</div>
</div>
<!-- الترحيل والتراكم -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:16px;border-bottom:1px solid #E5E7EB;font-weight:600;">الترحيل والتراكم</div>
<div style="padding:20px;">
<table style="width:100%;border-collapse:collapse;">
<tr style="border-bottom:1px solid #F3F4F6;"><td style="padding:10px 0;color:#6B7280;width:220px;">يسمح بالترحيل</td><td style="padding:10px 0;"><?= (int)$leaveType['carry_over_allowed'] ? '<span style="color:#059669;">✓ نعم</span>' : '<span style="color:#DC2626;">✗ لا</span>' ?></td></tr>
<tr style="border-bottom:1px solid #F3F4F6;"><td style="padding:10px 0;color:#6B7280;">أقصى أيام ترحيل</td><td style="padding:10px 0;"><?= $leaveType['max_carry_over_days'] !== null ? number_format((float)$leaveType['max_carry_over_days'], 1) : '-' ?></td></tr>
<tr style="border-bottom:1px solid #F3F4F6;"><td style="padding:10px 0;color:#6B7280;">صلاحية الترحيل</td><td style="padding:10px 0;"><?= $leaveType['carry_over_expiry_months'] !== null ? $leaveType['carry_over_expiry_months'] . ' شهر' : '-' ?></td></tr>
<tr style="border-bottom:1px solid #F3F4F6;"><td style="padding:10px 0;color:#6B7280;">تراكمية</td><td style="padding:10px 0;"><?= (int)$leaveType['is_accumulative'] ? '<span style="color:#059669;">✓ نعم</span>' : '<span style="color:#DC2626;">✗ لا</span>' ?></td></tr>
<tr><td style="padding:10px 0;color:#6B7280;">أقصى سنوات تراكم</td><td style="padding:10px 0;"><?= $leaveType['accumulation_max_years'] ?? '-' ?></td></tr>
</table>
</div>
</div>
</div>
<!-- الجانب الأيمن -->
<div>
<!-- الحالة -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:20px;text-align:center;">
<?php if ((int)$leaveType['is_active']): ?>
<span style="display:inline-block;padding:6px 16px;border-radius:9999px;font-size:14px;background:#DEF7EC;color:#03543F;font-weight:600;">مفعّل</span>
<?php else: ?>
<span style="display:inline-block;padding:6px 16px;border-radius:9999px;font-size:14px;background:#FDE8E8;color:#9B1C1C;font-weight:600;">معطّل</span>
<?php endif; ?>
</div>
</div>
<!-- الراتب -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:16px;border-bottom:1px solid #E5E7EB;font-weight:600;">الراتب</div>
<div style="padding:20px;">
<table style="width:100%;border-collapse:collapse;">
<tr style="border-bottom:1px solid #F3F4F6;"><td style="padding:8px 0;color:#6B7280;">مدفوعة</td><td style="padding:8px 0;"><?= (int)$leaveType['is_paid'] ? '<span style="color:#059669;">✓</span>' : '<span style="color:#DC2626;">✗</span>' ?></td></tr>
<tr style="border-bottom:1px solid #F3F4F6;"><td style="padding:8px 0;color:#6B7280;">نسبة الراتب</td><td style="padding:8px 0;font-weight:600;"><?= number_format((float)$leaveType['pay_percentage'], 0) ?>%</td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">خصم من الراتب</td><td style="padding:8px 0;"><?= (int)$leaveType['deduct_from_salary'] ? '<span style="color:#DC2626;">✓ نعم</span>' : '<span style="color:#059669;">✗ لا</span>' ?></td></tr>
</table>
</div>
</div>
<!-- الشروط -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:16px;border-bottom:1px solid #E5E7EB;font-weight:600;">الشروط</div>
<div style="padding:20px;">
<table style="width:100%;border-collapse:collapse;">
<tr style="border-bottom:1px solid #F3F4F6;"><td style="padding:8px 0;color:#6B7280;">تحتاج اعتماد</td><td style="padding:8px 0;"><?= (int)$leaveType['requires_approval'] ? '<span style="color:#059669;">✓</span>' : '<span style="color:#DC2626;">✗</span>' ?></td></tr>
<tr style="border-bottom:1px solid #F3F4F6;"><td style="padding:8px 0;color:#6B7280;">تحتاج مرفق</td><td style="padding:8px 0;"><?= (int)$leaveType['requires_attachment'] ? '<span style="color:#059669;">✓</span>' : '<span style="color:#DC2626;">✗</span>' ?></td></tr>
<tr style="border-bottom:1px solid #F3F4F6;"><td style="padding:8px 0;color:#6B7280;">إخطار مسبق</td><td style="padding:8px 0;"><?= (int)$leaveType['advance_notice_days'] > 0 ? $leaveType['advance_notice_days'] . ' يوم' : 'غير مطلوب' ?></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">أشهر خدمة مطلوبة</td><td style="padding:8px 0;"><?= $leaveType['min_service_months'] !== null ? $leaveType['min_service_months'] . ' شهر' : 'غير مطلوب' ?></td></tr>
</table>
</div>
</div>
<!-- حذف -->
<?php if (can('hr.leave.manage')): ?>
<div class="card">
<div style="padding:20px;">
<form method="POST" action="/hr/leave-types/<?= (int) $leaveType['id'] ?>/archive" onsubmit="return confirm('هل أنت متأكد من حذف هذا النوع؟');">
<?= csrf_field() ?>
<button type="submit" class="btn btn-danger" style="width:100%;">
<i data-lucide="trash-2" style="width:16px;height:16px;"></i> حذف نوع الإجازة
</button>
</form>
</div>
</div>
<?php endif; ?>
</div>
</div>
<script>if(typeof lucide!=='undefined')lucide.createIcons();</script>
<?php $__template->endSection(); ?>
......@@ -124,6 +124,7 @@ MenuRegistry::register('hr', [
['label_ar' => 'الهيكل الراتبي', 'label_en' => 'Salary Structures', 'route' => '/hr/salary-structures', 'permission' => 'hr.employee.view_salary','order' => 5],
['label_ar' => 'الحضور والانصراف', 'label_en' => 'Attendance', 'route' => '/hr/attendance', 'permission' => 'hr.attendance.view', 'order' => 6],
['label_ar' => 'الإجازات', 'label_en' => 'Leaves', 'route' => '/hr/leaves', 'permission' => 'hr.leave.view', 'order' => 7],
['label_ar' => 'أنواع الإجازات', 'label_en' => 'Leave Types', 'route' => '/hr/leave-types', 'permission' => 'hr.leave.manage', 'order' => 8],
['label_ar' => 'كشوف الرواتب', 'label_en' => 'Payroll', 'route' => '/hr/payroll', 'permission' => 'hr.payroll.view', 'order' => 8],
['label_ar' => 'التأمينات الاجتماعية', 'label_en' => 'Social Insurance', 'route' => '/hr/insurance', 'permission' => 'hr.insurance.view', 'order' => 9],
['label_ar' => 'السلف والقروض', 'label_en' => 'Loans', 'route' => '/hr/loans', 'permission' => 'hr.loan.view', 'order' => 10],
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment