Commit fc229f20 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat(hr): implement إدارة→قسم→موظف two-level department hierarchy

Adds department_type column (idara/qism) to hr_departments. Idaras are
top-level administrations; aqsam (sections) belong to an idara via
parent_id. Employees are assigned to a qism.

Cascading changes across 14 files:
- Migration adds department_type with CHECK constraint
- Model: allIdaras(), allAqsam(), getAqsamByIdara() helpers
- Controller: type-aware CRUD, validation (qism requires parent)
- Views: cascading إدارة→قسم dropdowns in employee form, type badges
  in department list, grouped optgroups in employee filters
- Reports: headcount groups by idara
- Routes: new /hr/departments/{id}/aqsam-json endpoint
- Bootstrap: updated Arabic labels
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 982aa243
......@@ -17,8 +17,10 @@ class DepartmentController extends Controller
public function index(Request $request): Response
{
$filters = [
'q' => trim((string) $request->get('q', '')),
'branch_id' => trim((string) $request->get('branch_id', '')),
'q' => trim((string) $request->get('q', '')),
'branch_id' => trim((string) $request->get('branch_id', '')),
'department_type' => trim((string) $request->get('department_type', '')),
'parent_id' => trim((string) $request->get('parent_id', '')),
];
$page = max(1, (int) $request->get('page', 1));
......@@ -26,25 +28,27 @@ class DepartmentController extends Controller
$db = App::getInstance()->db();
$branches = $db->select("SELECT id, name_ar FROM branches WHERE is_active = 1 ORDER BY name_ar");
$idaras = HrDepartment::allIdaras();
return $this->view('HR.Views.departments.index', [
'departments' => $result['data'],
'pagination' => $result['pagination'],
'filters' => $filters,
'branches' => $branches,
'idaras' => $idaras,
]);
}
public function create(Request $request): Response
{
$db = App::getInstance()->db();
$departments = HrDepartment::allActive();
$idaras = HrDepartment::allIdaras();
$branches = $db->select("SELECT id, name_ar FROM branches WHERE is_active = 1 ORDER BY name_ar");
$employees = HrEmployeeProfile::getActiveIds();
return $this->view('HR.Views.departments.form', [
'department' => null,
'departments' => $departments,
'idaras' => $idaras,
'branches' => $branches,
'employees' => $employees,
]);
......@@ -114,13 +118,13 @@ class DepartmentController extends Controller
}
$db = App::getInstance()->db();
$departments = HrDepartment::allActive();
$idaras = HrDepartment::allIdaras();
$branches = $db->select("SELECT id, name_ar FROM branches WHERE is_active = 1 ORDER BY name_ar");
$employees = HrEmployeeProfile::getActiveIds();
return $this->view('HR.Views.departments.form', [
'department' => $department,
'departments' => $departments,
'idaras' => $idaras,
'branches' => $branches,
'employees' => $employees,
]);
......@@ -265,13 +269,25 @@ class DepartmentController extends Controller
return $this->redirect('/hr/departments/' . $id . '/positions')->withSuccess('تم حذف الوظيفة المعتمدة بنجاح');
}
public function aqsamJson(Request $request, string $id): Response
{
$aqsam = HrDepartment::getAqsamByIdara((int) $id);
return $this->json(['results' => $aqsam]);
}
private function extractData(Request $request): array
{
$type = trim((string) $request->post('department_type', 'qism'));
if (!in_array($type, ['idara', 'qism'], true)) {
$type = 'qism';
}
return [
'name_ar' => trim((string) $request->post('name_ar', '')),
'name_en' => trim((string) $request->post('name_en', '')) ?: null,
'department_code' => strtoupper(trim((string) $request->post('department_code', ''))),
'parent_id' => ((int) $request->post('parent_id', 0)) ?: null,
'department_type' => $type,
'parent_id' => $type === 'qism' ? (((int) $request->post('parent_id', 0)) ?: null) : null,
'manager_employee_id' => ((int) $request->post('manager_employee_id', 0)) ?: null,
'branch_id' => ((int) $request->post('branch_id', 0)) ?: null,
'staffing_capacity' => ((int) $request->post('staffing_capacity', 0)) ?: null,
......@@ -282,11 +298,15 @@ class DepartmentController extends Controller
private function validateInput(array $data): array
{
$errors = [];
$typeLabel = $data['department_type'] === 'idara' ? 'الإدارة' : 'القسم';
if ($data['name_ar'] === '' || mb_strlen($data['name_ar']) < 2) {
$errors[] = 'اسم القسم بالعربي مطلوب (حرفان على الأقل)';
$errors[] = "اسم {$typeLabel} بالعربي مطلوب (حرفان على الأقل)";
}
if ($data['department_code'] === '') {
$errors[] = 'كود القسم مطلوب';
$errors[] = "كود {$typeLabel} مطلوب";
}
if ($data['department_type'] === 'qism' && empty($data['parent_id'])) {
$errors[] = 'يجب اختيار الإدارة التابع لها القسم';
}
return $errors;
}
......
......@@ -60,6 +60,7 @@ class EmployeeProfileController extends Controller
return $this->view('HR.Views.employees.form', [
'profile' => null,
'unlinked' => $unlinked,
'idaras' => HrDepartment::allIdaras(),
'departments' => HrDepartment::allActive(),
'jobTitles' => HrJobTitle::allActive(),
'structures' => HrSalaryStructure::allActive(),
......@@ -118,6 +119,10 @@ class EmployeeProfileController extends Controller
? HrDepartment::find((int) $profile->department_id)
: null;
$idara = ($department && $department->parent_id)
? HrDepartment::find((int) $department->parent_id)
: null;
$jobTitle = $profile->job_title_id
? HrJobTitle::find((int) $profile->job_title_id)
: null;
......@@ -154,6 +159,7 @@ class EmployeeProfileController extends Controller
return $this->view('HR.Views.employees.show', [
'profile' => $profile,
'department' => $department,
'idara' => $idara,
'jobTitle' => $jobTitle,
'structure' => $structure,
'activeContract' => $activeContract,
......@@ -182,6 +188,7 @@ class EmployeeProfileController extends Controller
return $this->view('HR.Views.employees.form', [
'profile' => $profile,
'unlinked' => [],
'idaras' => HrDepartment::allIdaras(),
'departments' => HrDepartment::allActive(),
'jobTitles' => HrJobTitle::allActive(),
'structures' => HrSalaryStructure::allActive(),
......
......@@ -21,7 +21,7 @@ class HrReportController extends Controller
$db = App::getInstance()->db();
$byDepartment = $db->select(
"SELECT d.name_ar as department_name,
"SELECT d.name_ar as department_name, COALESCE(ida.name_ar, d.name_ar) as idara_name,
COUNT(hp.id) as total,
SUM(CASE WHEN hp.gender = 'male' THEN 1 ELSE 0 END) as male_count,
SUM(CASE WHEN hp.gender = 'female' THEN 1 ELSE 0 END) as female_count,
......@@ -29,9 +29,10 @@ class HrReportController extends Controller
SUM(CASE WHEN hp.employment_type = 'part_time' THEN 1 ELSE 0 END) as part_time
FROM hr_employee_profiles hp
LEFT JOIN hr_departments d ON d.id = hp.department_id
LEFT JOIN hr_departments ida ON ida.id = d.parent_id AND ida.department_type = 'idara'
WHERE hp.employment_status = 'active' AND hp.is_archived = 0
GROUP BY hp.department_id, d.name_ar
ORDER BY total DESC"
GROUP BY hp.department_id, d.name_ar, ida.name_ar
ORDER BY idara_name ASC, total DESC"
);
$totals = $db->selectOne(
......
......@@ -16,7 +16,7 @@ class HrDepartment extends Model
protected static bool $autoTrackAuthor = true;
protected static array $fillable = [
'department_code', 'name_ar', 'name_en', 'parent_id', 'manager_employee_id',
'department_code', 'department_type', 'name_ar', 'name_en', 'parent_id', 'manager_employee_id',
'branch_id', 'description_ar', 'is_active', 'sort_order', 'staffing_capacity',
];
......@@ -28,6 +28,34 @@ class HrDepartment extends Model
->get();
}
public static function allIdaras(): array
{
return static::query()
->where('is_active', '=', 1)
->where('department_type', '=', 'idara')
->orderBy('name_ar', 'ASC')
->get();
}
public static function allAqsam(): array
{
return static::query()
->where('is_active', '=', 1)
->where('department_type', '=', 'qism')
->orderBy('name_ar', 'ASC')
->get();
}
public static function getAqsamByIdara(int $idaraId): array
{
return static::query()
->where('is_active', '=', 1)
->where('department_type', '=', 'qism')
->where('parent_id', '=', $idaraId)
->orderBy('name_ar', 'ASC')
->get();
}
public static function getTree(): array
{
$all = static::allActive();
......@@ -65,6 +93,14 @@ class HrDepartment extends Model
$where .= ' AND d.branch_id = ?';
$params[] = (int) $filters['branch_id'];
}
if (!empty($filters['department_type'])) {
$where .= ' AND d.department_type = ?';
$params[] = $filters['department_type'];
}
if (!empty($filters['parent_id'])) {
$where .= ' AND d.parent_id = ?';
$params[] = (int) $filters['parent_id'];
}
if (isset($filters['is_active']) && $filters['is_active'] !== '') {
$where .= ' AND d.is_active = ?';
$params[] = (int) $filters['is_active'];
......@@ -80,7 +116,7 @@ class HrDepartment extends Model
LEFT JOIN hr_departments p ON p.id = d.parent_id
LEFT JOIN branches b ON b.id = d.branch_id
WHERE {$where}
ORDER BY d.name_ar ASC
ORDER BY d.department_type ASC, d.name_ar ASC
LIMIT {$perPage} OFFSET {$offset}",
$params
);
......
......@@ -163,9 +163,10 @@ class HrEmployeeProfile extends Model
$offset = ($page - 1) * $perPage;
$rows = $db->select(
"SELECT p.*, d.name_ar as department_name, j.name_ar as job_title_name
"SELECT p.*, d.name_ar as department_name, d.department_type, ida.name_ar as idara_name, j.name_ar as job_title_name
FROM hr_employee_profiles p
LEFT JOIN hr_departments d ON d.id = p.department_id
LEFT JOIN hr_departments ida ON ida.id = d.parent_id AND ida.department_type = 'idara'
LEFT JOIN hr_job_titles j ON j.id = p.job_title_id
WHERE {$where}
ORDER BY p.first_name_ar ASC
......
......@@ -14,6 +14,8 @@ return [
['POST', '/hr/departments/{id:\d+}/positions', 'HR\Controllers\DepartmentController@storePosition', ['auth', 'csrf'], 'hr.department.manage'],
['POST', '/hr/departments/{id:\d+}/positions/{posId:\d+}/delete', 'HR\Controllers\DepartmentController@deletePosition', ['auth', 'csrf'], 'hr.department.manage'],
['GET', '/hr/departments/{id:\d+}/aqsam-json', 'HR\Controllers\DepartmentController@aqsamJson', ['auth'], 'hr.department.view'],
// ── Job Titles ──
['GET', '/hr/job-titles', 'HR\Controllers\JobTitleController@index', ['auth'], 'hr.job_title.view'],
['GET', '/hr/job-titles/create', 'HR\Controllers\JobTitleController@create', ['auth'], 'hr.job_title.manage'],
......
<?php $__template->layout('Layout.main'); ?>
<?php $isEdit = $department !== null; ?>
<?php $__template->section('title'); ?><?= $isEdit ? 'تعديل القسم' : 'إضافة قسم جديد' ?><?php $__template->endSection(); ?>
<?php $currentType = old('department_type') ?? ($isEdit ? ($department->department_type ?? 'qism') : 'qism'); ?>
<?php $__template->section('title'); ?><?= $isEdit ? ($currentType === 'idara' ? 'تعديل الإدارة' : 'تعديل القسم') : 'إضافة إدارة / قسم' ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/hr/departments" class="btn btn-secondary">رجوع</a>
......@@ -11,26 +12,46 @@
<?= csrf_field() ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:16px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;font-size:16px;">بيانات القسم</h3>
<h3 style="margin:0;font-size:16px;">نوع الوحدة التنظيمية</h3>
</div>
<div style="padding:16px;">
<div style="display:flex;gap:20px;">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;padding:12px 20px;border:2px solid #E5E7EB;border-radius:8px;" id="label_idara">
<input type="radio" name="department_type" value="idara" <?= $currentType === 'idara' ? 'checked' : '' ?> onchange="toggleType()">
<span style="font-weight:600;font-size:15px;">إدارة</span>
<span style="color:#6B7280;font-size:13px;">(المستوى الأعلى)</span>
</label>
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;padding:12px 20px;border:2px solid #E5E7EB;border-radius:8px;" id="label_qism">
<input type="radio" name="department_type" value="qism" <?= $currentType === 'qism' ? 'checked' : '' ?> onchange="toggleType()">
<span style="font-weight:600;font-size:15px;">قسم</span>
<span style="color:#6B7280;font-size:13px;">(تابع لإدارة)</span>
</label>
</div>
</div>
</div>
<div class="card" style="margin-bottom:20px;">
<div style="padding:16px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;font-size:16px;" id="formTitle"><?= $currentType === 'idara' ? 'بيانات الإدارة' : 'بيانات القسم' ?></h3>
</div>
<div style="padding:16px;display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:16px;">
<div>
<label style="display:block;margin-bottom:4px;font-size:13px;">اسم القسم بالعربي <span style="color:#DC2626;">*</span></label>
<label style="display:block;margin-bottom:4px;font-size:13px;" id="nameLabel"><?= $currentType === 'idara' ? 'اسم الإدارة بالعربي' : 'اسم القسم بالعربي' ?> <span style="color:#DC2626;">*</span></label>
<input type="text" name="name_ar" value="<?= e(old('name_ar') ?? ($isEdit ? $department->name_ar : '')) ?>" class="form-control" required>
</div>
<div>
<label style="display:block;margin-bottom:4px;font-size:13px;">اسم القسم بالإنجليزي</label>
<label style="display:block;margin-bottom:4px;font-size:13px;" id="nameEnLabel"><?= $currentType === 'idara' ? 'اسم الإدارة بالإنجليزي' : 'اسم القسم بالإنجليزي' ?></label>
<input type="text" name="name_en" value="<?= e(old('name_en') ?? ($isEdit ? $department->name_en : '')) ?>" class="form-control">
</div>
<div>
<label style="display:block;margin-bottom:4px;font-size:13px;">الكود <span style="color:#DC2626;">*</span></label>
<input type="text" name="department_code" value="<?= e(old('department_code') ?? ($isEdit ? $department->department_code : '')) ?>" class="form-control" required>
</div>
<div>
<label style="display:block;margin-bottom:4px;font-size:13px;">القسم الأب</label>
<select name="parent_id" class="form-control">
<option value="">-- بدون --</option>
<?php foreach ($departments as $d): ?>
<div id="parentField" style="<?= $currentType === 'idara' ? 'display:none;' : '' ?>">
<label style="display:block;margin-bottom:4px;font-size:13px;">الإدارة التابع لها <span style="color:#DC2626;">*</span></label>
<select name="parent_id" class="form-control" id="parentSelect">
<option value="">-- اختر الإدارة --</option>
<?php foreach ($idaras as $d): ?>
<?php if ($isEdit && (int) $d['id'] === (int) $department->id) continue; ?>
<option value="<?= (int) $d['id'] ?>" <?= (old('parent_id') ?? ($isEdit ? $department->parent_id : '')) == $d['id'] ? 'selected' : '' ?>><?= e($d['name_ar']) ?></option>
<?php endforeach; ?>
......@@ -72,5 +93,18 @@
<button type="submit" class="btn btn-primary"><?= $isEdit ? 'تحديث' : 'حفظ' ?></button>
</div>
</form>
<script>if(typeof lucide!=='undefined')lucide.createIcons();</script>
<script>
function toggleType() {
var isIdara = document.querySelector('input[name="department_type"]:checked').value === 'idara';
document.getElementById('parentField').style.display = isIdara ? 'none' : '';
document.getElementById('formTitle').textContent = isIdara ? 'بيانات الإدارة' : 'بيانات القسم';
document.getElementById('nameLabel').innerHTML = (isIdara ? 'اسم الإدارة بالعربي' : 'اسم القسم بالعربي') + ' <span style="color:#DC2626;">*</span>';
document.getElementById('nameEnLabel').textContent = isIdara ? 'اسم الإدارة بالإنجليزي' : 'اسم القسم بالإنجليزي';
document.getElementById('label_idara').style.borderColor = isIdara ? '#2563EB' : '#E5E7EB';
document.getElementById('label_qism').style.borderColor = isIdara ? '#E5E7EB' : '#2563EB';
if (isIdara) document.getElementById('parentSelect').value = '';
}
toggleType();
if(typeof lucide!=='undefined')lucide.createIcons();
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>الأقسام<?php $__template->endSection(); ?>
<?php $__template->section('title'); ?>الإدارات والأقسام<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<?php if (can('hr.department.manage')): ?>
<a href="/hr/departments/create" class="btn btn-primary" style="display:inline-flex;align-items:center;gap:6px;">
<i data-lucide="plus" style="width:16px;height:16px;"></i> إضافة قسم
<i data-lucide="plus" style="width:16px;height:16px;"></i> إضافة إدارة / قسم
</a>
<?php endif; ?>
<?php $__template->endSection(); ?>
......@@ -14,14 +14,31 @@
<form method="GET" action="/hr/departments" style="display:flex;gap:10px;flex-wrap:wrap;align-items:end;padding:16px;">
<div style="flex:1;min-width:200px;">
<label style="display:block;margin-bottom:4px;font-size:13px;color:#6B7280;">بحث</label>
<input type="text" name="q" value="<?= e($filters['q']) ?>" placeholder="اسم أو كود القسم..." class="form-control">
<input type="text" name="q" value="<?= e($filters['q']) ?>" placeholder="اسم أو كود..." class="form-control">
</div>
<div style="min-width:140px;">
<label style="display:block;margin-bottom:4px;font-size:13px;color:#6B7280;">النوع</label>
<select name="department_type" class="form-control">
<option value="">الكل</option>
<option value="idara" <?= ($filters['department_type'] ?? '') === 'idara' ? 'selected' : '' ?>>إدارة</option>
<option value="qism" <?= ($filters['department_type'] ?? '') === 'qism' ? 'selected' : '' ?>>قسم</option>
</select>
</div>
<div style="min-width:160px;">
<label style="display:block;margin-bottom:4px;font-size:13px;color:#6B7280;">الإدارة</label>
<select name="parent_id" class="form-control">
<option value="">الكل</option>
<?php foreach ($idaras as $ida): ?>
<option value="<?= (int) $ida['id'] ?>" <?= ($filters['parent_id'] ?? '') == $ida['id'] ? 'selected' : '' ?>><?= e($ida['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:160px;">
<label style="display:block;margin-bottom:4px;font-size:13px;color:#6B7280;">الفرع</label>
<select name="branch_id" class="form-control">
<option value="">الكل</option>
<?php foreach ($branches as $b): ?>
<option value="<?= (int) $b['id'] ?>" <?= $filters['branch_id'] == $b['id'] ? 'selected' : '' ?>><?= e($b['name_ar']) ?></option>
<option value="<?= (int) $b['id'] ?>" <?= ($filters['branch_id'] ?? '') == $b['id'] ? 'selected' : '' ?>><?= e($b['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
......@@ -35,8 +52,9 @@
<thead>
<tr>
<th>الكود</th>
<th>اسم القسم</th>
<th>القسم الأب</th>
<th>النوع</th>
<th>الاسم</th>
<th>الإدارة التابع لها</th>
<th>الفرع</th>
<th>الحالة</th>
<th>إجراءات</th>
......@@ -44,11 +62,18 @@
</thead>
<tbody>
<?php if (empty($departments)): ?>
<tr><td colspan="6" style="text-align:center;padding:40px;color:#9CA3AF;">لا توجد أقسام</td></tr>
<tr><td colspan="7" style="text-align:center;padding:40px;color:#9CA3AF;">لا توجد إدارات أو أقسام</td></tr>
<?php else: ?>
<?php foreach ($departments as $dept): ?>
<tr>
<td><code><?= e($dept['department_code']) ?></code></td>
<td>
<?php if (($dept['department_type'] ?? 'qism') === 'idara'): ?>
<span style="display:inline-block;padding:2px 8px;border-radius:9999px;font-size:12px;background:#DBEAFE;color:#1E40AF;">إدارة</span>
<?php else: ?>
<span style="display:inline-block;padding:2px 8px;border-radius:9999px;font-size:12px;background:#F3E8FF;color:#6B21A8;">قسم</span>
<?php endif; ?>
</td>
<td><a href="/hr/departments/<?= (int) $dept['id'] ?>"><?= e($dept['name_ar']) ?></a></td>
<td><?= e($dept['parent_name'] ?? '-') ?></td>
<td><?= e($dept['branch_name'] ?? '-') ?></td>
......
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>القسم: <?= e($department->name_ar) ?><?php $__template->endSection(); ?>
<?php $isIdara = ($department->department_type ?? 'qism') === 'idara'; ?>
<?php $__template->section('title'); ?><?= $isIdara ? 'الإدارة' : 'القسم' ?>: <?= e($department->name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<?php if (can('hr.department.manage')): ?>
......@@ -14,14 +15,23 @@
<div class="card" style="margin-bottom:20px;">
<div style="padding:16px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;font-size:16px;display:flex;align-items:center;gap:8px;">
<i data-lucide="building-2" style="width:18px;height:18px;color:#6B7280;"></i> بيانات القسم
<i data-lucide="building-2" style="width:18px;height:18px;color:#6B7280;"></i> بيانات <?= $isIdara ? 'الإدارة' : 'القسم' ?>
</h3>
</div>
<div style="padding:16px;display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px;">
<div><span style="color:#6B7280;font-size:13px;">الكود</span><div style="font-weight:600;"><?= e($department->department_code) ?></div></div>
<div><span style="color:#6B7280;font-size:13px;">النوع</span><div>
<?php if ($isIdara): ?>
<span style="display:inline-block;padding:2px 8px;border-radius:9999px;font-size:12px;background:#DBEAFE;color:#1E40AF;">إدارة</span>
<?php else: ?>
<span style="display:inline-block;padding:2px 8px;border-radius:9999px;font-size:12px;background:#F3E8FF;color:#6B21A8;">قسم</span>
<?php endif; ?>
</div></div>
<div><span style="color:#6B7280;font-size:13px;">الاسم بالعربي</span><div style="font-weight:600;"><?= e($department->name_ar) ?></div></div>
<div><span style="color:#6B7280;font-size:13px;">الاسم بالإنجليزي</span><div><?= e($department->name_en ?? '-') ?></div></div>
<div><span style="color:#6B7280;font-size:13px;">القسم الأب</span><div><?= $parent ? '<a href="/hr/departments/' . (int) $parent->id . '">' . e($parent->name_ar) . '</a>' : '-' ?></div></div>
<?php if (!$isIdara): ?>
<div><span style="color:#6B7280;font-size:13px;">الإدارة التابع لها</span><div><?= $parent ? '<a href="/hr/departments/' . (int) $parent->id . '">' . e($parent->name_ar) . '</a>' : '-' ?></div></div>
<?php endif; ?>
<div><span style="color:#6B7280;font-size:13px;">المدير</span><div><?= $manager ? e($manager['first_name_ar'] . ' ' . $manager['last_name_ar']) : '-' ?></div></div>
<div><span style="color:#6B7280;font-size:13px;">الفرع</span><div><?= $branch ? e($branch['name_ar']) : '-' ?></div></div>
<div><span style="color:#6B7280;font-size:13px;">عدد الموظفين</span><div style="font-weight:600;"><?= $employeeCount ?></div></div>
......@@ -116,7 +126,7 @@
<?php if (!empty($children)): ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:16px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;font-size:16px;">الأقسام الفرعية (<?= count($children) ?>)</h3>
<h3 style="margin:0;font-size:16px;"><?= $isIdara ? 'الأقسام التابعة' : 'الأقسام الفرعية' ?> (<?= count($children) ?>)</h3>
</div>
<div class="table-responsive">
<table class="data-table">
......@@ -136,9 +146,9 @@
<?php if (can('hr.department.manage')): ?>
<div style="display:flex;gap:10px;justify-content:flex-end;">
<form method="POST" action="/hr/departments/<?= (int) $department->id ?>/archive" onsubmit="return confirm('هل أنت متأكد من حذف هذا القسم؟');">
<form method="POST" action="/hr/departments/<?= (int) $department->id ?>/archive" onsubmit="return confirm('هل أنت متأكد من حذف <?= $isIdara ? 'هذه الإدارة' : 'هذا القسم' ?>؟');">
<?= csrf_field() ?>
<button type="submit" class="btn btn-danger">حذف القسم</button>
<button type="submit" class="btn btn-danger">حذف <?= $isIdara ? 'الإدارة' : 'القسم' ?></button>
</form>
</div>
<?php endif; ?>
......
......@@ -142,12 +142,23 @@
<label style="display:block;margin-bottom:4px;font-size:13px;">كود البصمة</label>
<input type="text" name="fingerprint_code" value="<?= e(old('fingerprint_code') ?? ($isEdit ? $profile->fingerprint_code : '')) ?>" class="form-control" placeholder="رقم الموظف في جهاز البصمة">
</div>
<div>
<label style="display:block;margin-bottom:4px;font-size:13px;">الإدارة</label>
<select id="idara_select" class="form-control" onchange="loadAqsam()">
<option value="">-- اختر الإدارة --</option>
<?php foreach ($idaras as $ida): ?>
<option value="<?= (int) $ida['id'] ?>" <?= (old('idara_id') ?? ($isEdit ? ($profile->idara_id ?? '') : '')) == $ida['id'] ? 'selected' : '' ?>><?= e($ida['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label style="display:block;margin-bottom:4px;font-size:13px;">القسم</label>
<select name="department_id" class="form-control">
<option value="">-- اختر --</option>
<select name="department_id" id="qism_select" class="form-control">
<option value="">-- اختر القسم --</option>
<?php foreach ($departments as $d): ?>
<option value="<?= (int) $d['id'] ?>" <?= (old('department_id') ?? ($isEdit ? $profile->department_id : '')) == $d['id'] ? 'selected' : '' ?>><?= e($d['name_ar']) ?></option>
<?php if (($d['department_type'] ?? '') === 'qism'): ?>
<option value="<?= (int) $d['id'] ?>" data-idara="<?= (int) ($d['parent_id'] ?? 0) ?>" <?= (old('department_id') ?? ($isEdit ? $profile->department_id : '')) == $d['id'] ? 'selected' : '' ?>><?= e($d['name_ar']) ?></option>
<?php endif; ?>
<?php endforeach; ?>
</select>
</div>
......@@ -275,6 +286,31 @@
</div>
</form>
<script>
function loadAqsam() {
var idaraId = document.getElementById('idara_select').value;
var qismSelect = document.getElementById('qism_select');
var options = qismSelect.querySelectorAll('option[data-idara]');
var currentVal = qismSelect.value;
options.forEach(function(opt) {
if (!idaraId) { opt.style.display = ''; return; }
opt.style.display = opt.getAttribute('data-idara') === idaraId ? '' : 'none';
if (opt.style.display === 'none' && opt.value === currentVal) qismSelect.value = '';
});
}
document.addEventListener('DOMContentLoaded', function() {
var qismSelect = document.getElementById('qism_select');
var idaraSelect = document.getElementById('idara_select');
var selectedQism = qismSelect.value;
if (selectedQism) {
var opt = qismSelect.querySelector('option[value="' + selectedQism + '"]');
if (opt && opt.getAttribute('data-idara')) {
idaraSelect.value = opt.getAttribute('data-idara');
}
}
loadAqsam();
});
</script>
<script>
document.addEventListener('DOMContentLoaded', function() {
var nidInput = document.getElementById('nidInput');
......
......@@ -21,7 +21,15 @@
<select name="department_id" class="form-control">
<option value="">الكل</option>
<?php foreach ($departments as $d): ?>
<option value="<?= (int) $d['id'] ?>" <?= $filters['department_id'] == $d['id'] ? 'selected' : '' ?>><?= e($d['name_ar']) ?></option>
<?php if (($d['department_type'] ?? '') === 'idara'): ?>
<optgroup label="<?= e($d['name_ar']) ?>">
<?php foreach ($departments as $sub): ?>
<?php if (($sub['department_type'] ?? '') === 'qism' && (int)($sub['parent_id'] ?? 0) === (int)$d['id']): ?>
<option value="<?= (int) $sub['id'] ?>" <?= $filters['department_id'] == $sub['id'] ? 'selected' : '' ?>><?= e($sub['name_ar']) ?></option>
<?php endif; ?>
<?php endforeach; ?>
</optgroup>
<?php endif; ?>
<?php endforeach; ?>
</select>
</div>
......@@ -51,17 +59,18 @@
<div class="table-responsive">
<table class="data-table">
<thead>
<tr><th>رقم وظيفي</th><th>الاسم</th><th>القسم</th><th>المسمى الوظيفي</th><th>تاريخ التعيين</th><th>الحالة</th><th>إجراءات</th></tr>
<tr><th>رقم وظيفي</th><th>الاسم</th><th>الإدارة</th><th>القسم</th><th>المسمى الوظيفي</th><th>تاريخ التعيين</th><th>الحالة</th><th>إجراءات</th></tr>
</thead>
<tbody>
<?php if (empty($employees)): ?>
<tr><td colspan="7" style="text-align:center;padding:40px;color:#9CA3AF;">لا يوجد موظفين</td></tr>
<tr><td colspan="8" style="text-align:center;padding:40px;color:#9CA3AF;">لا يوجد موظفين</td></tr>
<?php else: ?>
<?php foreach ($employees as $emp): ?>
<tr>
<td><code><?= e($emp['employee_number']) ?></code></td>
<td><a href="/hr/employees/<?= (int) $emp['id'] ?>"><?= e($emp['first_name_ar'] . ' ' . $emp['last_name_ar']) ?></a></td>
<td><?= e($emp['department_name'] ?? '-') ?></td>
<td><?= e($emp['idara_name'] ?? '-') ?></td>
<td><?= e(($emp['department_type'] ?? '') === 'qism' ? ($emp['department_name'] ?? '-') : '-') ?></td>
<td><?= e($emp['job_title_name'] ?? '-') ?></td>
<td><?= e($emp['hire_date'] ?? '-') ?></td>
<td>
......
......@@ -47,7 +47,8 @@ $sc = $statusColors[$profile->employment_status] ?? '#E5E7EB;color:#374151';
</h3>
</div>
<div style="padding:16px;display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px;">
<div><span style="color:#6B7280;font-size:13px;">القسم</span><div><?= $department ? '<a href="/hr/departments/' . (int) $department->id . '">' . e($department->name_ar) . '</a>' : '-' ?></div></div>
<div><span style="color:#6B7280;font-size:13px;">الإدارة</span><div><?= ($department && $department->parent_id) ? '<a href="/hr/departments/' . (int) $department->parent_id . '">' . e($idara->name_ar ?? '-') . '</a>' : ($department && ($department->department_type ?? '') === 'idara' ? '<a href="/hr/departments/' . (int) $department->id . '">' . e($department->name_ar) . '</a>' : '-') ?></div></div>
<div><span style="color:#6B7280;font-size:13px;">القسم</span><div><?= ($department && ($department->department_type ?? '') === 'qism') ? '<a href="/hr/departments/' . (int) $department->id . '">' . e($department->name_ar) . '</a>' : '-' ?></div></div>
<div><span style="color:#6B7280;font-size:13px;">المسمى الوظيفي</span><div><?= $jobTitle ? e($jobTitle->name_ar) : '-' ?></div></div>
<div><span style="color:#6B7280;font-size:13px;">تاريخ التعيين</span><div><?= e($profile->hire_date) ?></div></div>
<div><span style="color:#6B7280;font-size:13px;">سنوات الخدمة</span><div style="font-weight:600;"><?= number_format($yearsOfService, 1) ?> سنة</div></div>
......
......@@ -12,8 +12,8 @@ use App\Modules\HR\Services\AttendanceViolationService;
PermissionRegistry::register('hr', [
// Department
'hr.department.view' => ['ar' => 'عرض الأقسام والإدارات', 'en' => 'View Departments'],
'hr.department.manage' => ['ar' => 'إدارة الأقسام والإدارات', 'en' => 'Manage Departments'],
'hr.department.view' => ['ar' => 'عرض الإدارات والأقسام', 'en' => 'View Departments'],
'hr.department.manage' => ['ar' => 'إدارة الإدارات والأقسام', 'en' => 'Manage Departments'],
// Job Title
'hr.job_title.view' => ['ar' => 'عرض المسميات الوظيفية', 'en' => 'View Job Titles'],
......@@ -118,7 +118,7 @@ MenuRegistry::register('hr', [
'order' => 500,
'children' => [
['label_ar' => 'الموظفون', 'label_en' => 'Employees', 'route' => '/hr/employees', 'permission' => 'hr.employee.view', 'order' => 1],
['label_ar' => 'الأقسام والإدارات', 'label_en' => 'Departments', 'route' => '/hr/departments', 'permission' => 'hr.department.view', 'order' => 2],
['label_ar' => 'الإدارات والأقسام', 'label_en' => 'Departments', 'route' => '/hr/departments', 'permission' => 'hr.department.view', 'order' => 2],
['label_ar' => 'المسميات الوظيفية', 'label_en' => 'Job Titles', 'route' => '/hr/job-titles', 'permission' => 'hr.job_title.view', 'order' => 3],
['label_ar' => 'العقود', 'label_en' => 'Contracts', 'route' => '/hr/contracts', 'permission' => 'hr.contract.view', 'order' => 4],
['label_ar' => 'الهيكل الراتبي', 'label_en' => 'Salary Structures', 'route' => '/hr/salary-structures', 'permission' => 'hr.employee.view_salary','order' => 5],
......
<?php
declare(strict_types=1);
return [
'up' => "
ALTER TABLE hr_departments ADD COLUMN department_type VARCHAR(10) NOT NULL DEFAULT 'qism' COMMENT 'idara=إدارة (top-level), qism=قسم (under idara)' AFTER department_code;
ALTER TABLE hr_departments ADD CONSTRAINT chk_hr_dept_type CHECK (department_type IN ('idara', 'qism'))
",
'down' => "
ALTER TABLE hr_departments DROP CONSTRAINT chk_hr_dept_type;
ALTER TABLE hr_departments DROP COLUMN department_type
",
];
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