Commit 3fcec655 authored by Mahmoud Aglan's avatar Mahmoud Aglan

fixhe

parent f99774c0
......@@ -54,6 +54,7 @@ class ContractController extends Controller
'working_hours_per_day' => trim((string) $request->post('working_hours_per_day', '8')),
'notice_period_months' => (int) $request->post('notice_period_months', 2),
'basic_salary' => trim((string) $request->post('basic_salary', '0.00')),
'total_package' => trim((string) $request->post('total_package', '')) ?: null,
'terms_ar' => trim((string) $request->post('terms_ar', '')) ?: null,
'notes' => trim((string) $request->post('notes', '')) ?: null,
];
......@@ -133,6 +134,7 @@ class ContractController extends Controller
return $this->redirect('/hr/contracts/' . $id)->withError('لا يمكن تعديل عقد غير مسودة');
}
$termsAr = trim((string) $request->post('terms_ar', '')) ?: null;
$data = [
'contract_type' => trim((string) $request->post('contract_type', 'definite')),
'start_date' => trim((string) $request->post('start_date', '')),
......@@ -141,7 +143,8 @@ class ContractController extends Controller
'working_hours_per_day' => trim((string) $request->post('working_hours_per_day', '8')),
'notice_period_months' => (int) $request->post('notice_period_months', 2),
'basic_salary' => trim((string) $request->post('basic_salary', '0.00')),
'terms_ar' => trim((string) $request->post('terms_ar', '')) ?: null,
'total_package' => trim((string) $request->post('total_package', '0.00')),
'terms_json' => $termsAr ? json_encode($termsAr, JSON_UNESCAPED_UNICODE) : null,
'notes' => trim((string) $request->post('notes', '')) ?: null,
];
......
......@@ -356,7 +356,7 @@ class EmployeeProfileController extends Controller
'employment_status' => trim((string) $request->post('employment_status', 'active')),
'probation_end_date' => trim((string) $request->post('probation_end_date', '')) ?: null,
'basic_salary' => trim((string) $request->post('basic_salary', '0.00')),
'insurable_salary' => trim((string) $request->post('insurable_salary', '')) ?: null,
'insurable_salary' => trim((string) $request->post('insurable_salary', '0.00')) ?: '0.00',
'insurance_number' => trim((string) $request->post('insurance_number', '')) ?: null,
'bank_name' => trim((string) $request->post('bank_name', '')) ?: null,
'bank_account_number' => trim((string) $request->post('bank_account_number', '')) ?: null,
......
......@@ -11,6 +11,7 @@ use App\Modules\HR\Models\HrPerformanceCycle;
use App\Modules\HR\Models\HrPerformanceReview;
use App\Modules\HR\Models\HrEmployeeProfile;
use App\Modules\HR\Models\HrDepartment;
use App\Modules\HR\Services\HrNumberGenerator;
class PerformanceController extends Controller
{
......@@ -75,16 +76,22 @@ class PerformanceController extends Controller
$db = App::getInstance()->db();
$employee = $this->currentEmployee();
$reviewDeadline = trim((string) $request->post('review_deadline', '')) ?: $endDate;
$cycleCode = HrNumberGenerator::generateCycleCode();
$year = (int) date('Y', strtotime($startDate));
$cycleId = $db->insert('hr_performance_cycles', [
'name_ar' => $name,
'period_type' => $cycleType,
'start_date' => $startDate,
'end_date' => $endDate,
'status' => 'draft',
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
'created_by' => $employee ? (int) $employee->id : null,
'cycle_code' => $cycleCode,
'name_ar' => $name,
'year' => $year,
'period_type' => $cycleType,
'start_date' => $startDate,
'end_date' => $endDate,
'review_deadline' => $reviewDeadline,
'status' => 'draft',
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
'created_by' => $employee ? (int) $employee->id : null,
]);
// Auto-create review entries for all active employees
......@@ -92,10 +99,12 @@ class PerformanceController extends Controller
"SELECT id FROM hr_employee_profiles WHERE employment_status = 'active' AND is_archived = 0"
);
$reviewerId = $employee ? (int) $employee->id : 0;
foreach ($activeEmployees as $emp) {
$db->insert('hr_performance_reviews', [
'cycle_id' => $cycleId,
'employee_profile_id' => (int) $emp['id'],
'reviewer_id' => $reviewerId,
'status' => 'pending',
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
......@@ -119,7 +128,7 @@ class PerformanceController extends Controller
$db = App::getInstance()->db();
$stats = $db->selectOne(
"SELECT COUNT(*) as total,
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed,
SUM(CASE WHEN status = 'submitted' THEN 1 ELSE 0 END) as completed,
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending,
AVG(CASE WHEN overall_rating > 0 THEN overall_rating ELSE NULL END) as avg_rating
FROM hr_performance_reviews
......
......@@ -37,12 +37,11 @@ class HrContract extends Model
public static function getStatuses(): array
{
return [
'draft' => 'مسودة',
'pending_approval' => 'في انتظار الاعتماد',
'active' => 'ساري',
'renewed' => 'تم التجديد',
'expired' => 'منتهي',
'terminated' => 'تم إنهاؤه',
'draft' => 'مسودة',
'active' => 'ساري',
'renewed' => 'تم التجديد',
'expired' => 'منتهي',
'terminated' => 'تم إنهاؤه',
];
}
......@@ -90,9 +89,9 @@ class HrContract extends Model
$where .= ' AND c.status = ?';
$params[] = $filters['status'];
}
if (!empty($filters['contract_type'])) {
if (!empty($filters['type'])) {
$where .= ' AND c.contract_type = ?';
$params[] = $filters['contract_type'];
$params[] = $filters['type'];
}
$countRow = $db->selectOne(
......
......@@ -55,7 +55,7 @@ class HrEmployeeProfile extends Model
'full_time' => 'دوام كامل',
'part_time' => 'دوام جزئي',
'contract' => 'عقد مؤقت',
'seasonal' => 'موسمي',
'temporary' => 'مؤقت',
];
}
......
......@@ -191,9 +191,12 @@ final class AttendanceService
$daysInMonth = (int) date('t', strtotime($startDate));
$workingDays = 0;
$profile = HrEmployeeProfile::find($profileId);
$religion = $profile->religion ?? null;
for ($d = 1; $d <= $daysInMonth; $d++) {
$date = sprintf('%04d-%02d-%02d', $year, $month, $d);
if (!self::isRestDay($profileId, $date) && !HrHoliday::isHoliday($date)) {
if (!self::isRestDay($profileId, $date) && !HrHoliday::isHoliday($date, $religion)) {
$workingDays++;
}
}
......
......@@ -57,7 +57,8 @@ final class ContractService
'working_days_per_week' => $data['working_days_per_week'] ?? 6,
'notice_period_months' => $data['notice_period_months'] ?? 2,
'basic_salary' => $data['basic_salary'] ?? '0.00',
'total_package' => $data['total_package'] ?? '0.00',
'total_package' => $data['total_package'] ?? $data['basic_salary'] ?? '0.00',
'terms_json' => !empty($data['terms_ar']) ? json_encode($data['terms_ar'], JSON_UNESCAPED_UNICODE) : null,
'renewal_of_contract_id' => $data['renewal_of_contract_id'] ?? null,
'status' => 'draft',
'notes' => $data['notes'] ?? null,
......
......@@ -46,6 +46,26 @@ final class HrNumberGenerator
return sprintf('PAY-%04d-%02d', $year, $month);
}
public static function generateCycleCode(): string
{
$db = App::getInstance()->db();
$year = date('Y');
$prefix = 'PC-' . $year . '-';
$last = $db->selectOne(
"SELECT cycle_code FROM hr_performance_cycles WHERE cycle_code LIKE ? ORDER BY id DESC LIMIT 1",
[$prefix . '%']
);
if ($last) {
$parts = explode('-', $last['cycle_code']);
$seq = (int) end($parts) + 1;
} else {
$seq = 1;
}
return $prefix . str_pad((string) $seq, 3, '0', STR_PAD_LEFT);
}
public static function generateLoanReference(): string
{
$db = App::getInstance()->db();
......@@ -53,9 +73,15 @@ final class HrNumberGenerator
$prefix = 'LN-' . $year . '-';
$last = $db->selectOne(
"SELECT id FROM hr_employee_loans ORDER BY id DESC LIMIT 1"
"SELECT loan_number FROM hr_employee_loans WHERE loan_number LIKE ? ORDER BY id DESC LIMIT 1",
[$prefix . '%']
);
$seq = ($last ? (int) $last['id'] : 0) + 1;
if ($last) {
$parts = explode('-', $last['loan_number']);
$seq = (int) end($parts) + 1;
} else {
$seq = 1;
}
return $prefix . str_pad((string) $seq, 5, '0', STR_PAD_LEFT);
}
......
......@@ -310,10 +310,13 @@ final class LeaveService
$end = new \DateTime($endDate);
$days = 0;
$profile = HrEmployeeProfile::find($profileId);
$religion = $profile ? $profile->religion : null;
$current = clone $start;
while ($current <= $end) {
$dateStr = $current->format('Y-m-d');
if (!AttendanceService::isRestDay($profileId, $dateStr) && !HrHoliday::isHoliday($dateStr)) {
if (!AttendanceService::isRestDay($profileId, $dateStr) && !HrHoliday::isHoliday($dateStr, $religion)) {
$days++;
}
$current->modify('+1 day');
......
......@@ -8,6 +8,7 @@ use App\Core\EventBus;
use App\Core\Logger;
use App\Modules\HR\Models\HrEmployeeLoan;
use App\Modules\HR\Models\HrLoanInstallment;
use App\Modules\HR\Services\HrNumberGenerator;
/**
* Loan & Salary Advance Service
......@@ -43,10 +44,13 @@ final class LoanService
$totalScheduled = bcmul($installmentAmount, (string) $numInstallments, 2);
$lastInstallmentAdj = bcsub($loanAmount, bcmul($installmentAmount, (string) ($numInstallments - 1), 2), 2);
$loanNumber = HrNumberGenerator::generateLoanReference();
$db->beginTransaction();
try {
$loanId = $db->insert('hr_employee_loans', [
'employee_profile_id' => $profileId,
'loan_number' => $loanNumber,
'loan_type' => $data['loan_type'] ?? 'salary_advance',
'loan_amount' => $loanAmount,
'installment_amount' => $installmentAmount,
......
......@@ -319,9 +319,13 @@ final class PayrollCalculationService
'working_days' => $workingDays,
], JSON_UNESCAPED_UNICODE);
$db->beginTransaction();
try {
$runId = $db->insert('hr_payroll_runs', [
'period_id' => $periodId,
'employee_profile_id' => $profileId,
'employee_number' => $profile->employee_number ?? '',
'basic_salary' => $basicSalary,
'gross_earnings' => $grossEarnings,
'total_allowances' => $totalAllowances,
......@@ -401,6 +405,12 @@ final class PayrollCalculationService
LoanService::processInstallmentDeduction((int) $inst['loan_id'], $inst['amount'], $runId);
}
$db->commit();
} catch (\Throwable $e) {
$db->rollBack();
throw $e;
}
return [
'success' => true,
'run_id' => $runId,
......
......@@ -51,9 +51,14 @@
<label style="display:block;margin-bottom:4px;font-size:13px;">الراتب الأساسي</label>
<input type="text" name="basic_salary" value="<?= e(old('basic_salary') ?? ($isEdit ? $contract->basic_salary : ($profile ? $profile->basic_salary : '0.00'))) ?>" class="form-control">
</div>
<div>
<label style="display:block;margin-bottom:4px;font-size:13px;">إجمالي الحزمة</label>
<input type="text" name="total_package" value="<?= e(old('total_package') ?? ($isEdit ? $contract->total_package : ($profile ? $profile->basic_salary : '0.00'))) ?>" class="form-control">
<small style="color:#6B7280;">الراتب الأساسي + البدلات</small>
</div>
<div style="grid-column:1/-1;">
<label style="display:block;margin-bottom:4px;font-size:13px;">شروط العقد</label>
<textarea name="terms_ar" class="form-control" rows="4"><?= e(old('terms_ar') ?? ($isEdit ? $contract->terms_ar : '')) ?></textarea>
<textarea name="terms_ar" class="form-control" rows="4"><?= e(old('terms_ar') ?? ($isEdit ? json_decode($contract->terms_json ?? 'null', true) ?? '' : '')) ?></textarea>
</div>
<div style="grid-column:1/-1;">
<label style="display:block;margin-bottom:4px;font-size:13px;">ملاحظات</label>
......
......@@ -40,7 +40,7 @@
<?php $statusColors = ['draft' => '#E5E7EB;color:#374151', 'active' => '#DEF7EC;color:#03543F', 'expired' => '#FEF3C7;color:#92400E', 'terminated' => '#FDE8E8;color:#9B1C1C', 'renewed' => '#DBEAFE;color:#1E40AF']; ?>
<tr>
<td><a href="/hr/contracts/<?= (int) $c['id'] ?>"><?= e($c['contract_number']) ?></a></td>
<td><?= e(($c['first_name_ar'] ?? '') . ' ' . ($c['last_name_ar'] ?? '')) ?></td>
<td><?= e($c['full_name_ar'] ?? '') ?></td>
<td><?= $c['contract_type'] === 'definite' ? 'محدد المدة' : 'غير محدد' ?></td>
<td><?= e($c['start_date']) ?></td>
<td><?= e($c['end_date'] ?? '-') ?></td>
......
......@@ -29,6 +29,7 @@
<div><span style="color:#6B7280;font-size:13px;">ساعات العمل</span><div><?= e($contract->working_hours_per_day) ?> ساعة/يوم</div></div>
<div><span style="color:#6B7280;font-size:13px;">فترة الإنذار</span><div><?= (int) $contract->notice_period_months ?> أشهر</div></div>
<div><span style="color:#6B7280;font-size:13px;">الراتب الأساسي</span><div style="font-weight:600;"><?= number_format((float) $contract->basic_salary, 2) ?> ج.م</div></div>
<div><span style="color:#6B7280;font-size:13px;">إجمالي الحزمة</span><div style="font-weight:600;"><?= number_format((float) $contract->total_package, 2) ?> ج.م</div></div>
</div>
</div>
......
......@@ -25,6 +25,7 @@
</select>
</div>
<div><label style="display:block;margin-bottom:4px;font-size:13px;"><input type="checkbox" name="is_recurring" value="1" <?= (old('is_recurring') ?? ($isEdit ? $holiday->is_recurring : 0)) ? 'checked' : '' ?>> متكررة سنوياً</label></div>
<div><label style="display:block;margin-bottom:4px;font-size:13px;"><input type="checkbox" name="is_active" value="1" <?= (old('is_active') ?? ($isEdit ? $holiday->is_active : 1)) ? 'checked' : '' ?>> فعالة</label></div>
</div>
</div>
<div style="display:flex;gap:10px;justify-content:flex-end;">
......
......@@ -35,6 +35,7 @@
<label style="display:block;margin-bottom:4px;font-size:13px;">الحد الأقصى للراتب</label>
<input type="text" name="max_salary" value="<?= e(old('max_salary') ?? ($isEdit ? $jobTitle->max_salary : '')) ?>" class="form-control" placeholder="0.00">
</div>
<div><label style="display:block;margin-bottom:4px;font-size:13px;"><input type="checkbox" name="is_active" value="1" <?= (old('is_active') ?? ($isEdit ? $jobTitle->is_active : 1)) ? 'checked' : '' ?>> فعال</label></div>
<div style="grid-column:1/-1;">
<label style="display:block;margin-bottom:4px;font-size:13px;">الوصف</label>
<textarea name="description_ar" class="form-control" rows="3"><?= e(old('description_ar') ?? ($isEdit ? $jobTitle->description_ar : '')) ?></textarea>
......
......@@ -2,8 +2,8 @@
<?php $__template->section('title'); ?>دورات التقييم<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?><a href="/hr/performance" class="btn btn-secondary">رجوع</a><?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php $csl = ['draft'=>'مسودة','active'=>'نشطة','completed'=>'مكتملة','closed'=>'مغلقة']; ?>
<?php $csc = ['draft'=>'#FEF3C7;color:#92400E','active'=>'#D1FAE5;color:#065F46','completed'=>'#DBEAFE;color:#1E40AF','closed'=>'#E5E7EB;color:#374151']; ?>
<?php $csl = ['draft'=>'مسودة','active'=>'نشطة','review'=>'تحت المراجعة','closed'=>'مغلقة']; ?>
<?php $csc = ['draft'=>'#FEF3C7;color:#92400E','active'=>'#D1FAE5;color:#065F46','review'=>'#DBEAFE;color:#1E40AF','closed'=>'#E5E7EB;color:#374151']; ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:16px;border-bottom:1px solid #E5E7EB;"><h3 style="margin:0;font-size:16px;">إنشاء دورة تقييم جديدة</h3></div>
......@@ -12,10 +12,10 @@
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px;margin-bottom:12px;">
<div><label style="display:block;margin-bottom:4px;font-size:13px;">اسم الدورة <span style="color:#DC2626;">*</span></label><input type="text" name="name_ar" value="<?= e(old('name_ar') ?? '') ?>" class="form-control" placeholder="مثال: تقييم الأداء 2026" required></div>
<div><label style="display:block;margin-bottom:4px;font-size:13px;">النوع</label>
<select name="cycle_type" class="form-control">
<option value="annual" <?= old('cycle_type') === 'annual' ? 'selected' : '' ?>>سنوي</option>
<option value="semi_annual" <?= old('cycle_type') === 'semi_annual' ? 'selected' : '' ?>>نصف سنوي</option>
<option value="quarterly" <?= old('cycle_type') === 'quarterly' ? 'selected' : '' ?>>ربع سنوي</option>
<select name="period_type" class="form-control">
<option value="annual" <?= old('period_type') === 'annual' ? 'selected' : '' ?>>سنوي</option>
<option value="semi_annual" <?= old('period_type') === 'semi_annual' ? 'selected' : '' ?>>نصف سنوي</option>
<option value="quarterly" <?= old('period_type') === 'quarterly' ? 'selected' : '' ?>>ربع سنوي</option>
</select>
</div>
<div><label style="display:block;margin-bottom:4px;font-size:13px;">تاريخ البداية <span style="color:#DC2626;">*</span></label><input type="date" name="start_date" value="<?= e(old('start_date') ?? '') ?>" class="form-control" required></div>
......@@ -37,7 +37,7 @@
<?php foreach ($cycles as $c): ?>
<tr>
<td style="font-weight:600;"><?= e($c['name_ar']) ?></td>
<td><?= $c['cycle_type'] === 'annual' ? 'سنوي' : ($c['cycle_type'] === 'semi_annual' ? 'نصف سنوي' : 'ربع سنوي') ?></td>
<td><?= $c['period_type'] === 'annual' ? 'سنوي' : ($c['period_type'] === 'semi_annual' ? 'نصف سنوي' : 'ربع سنوي') ?></td>
<td><?= e($c['start_date']) ?> - <?= e($c['end_date']) ?></td>
<td><?= (int) ($c['review_count'] ?? 0) ?></td>
<td><?php if ($c['avg_rating']): ?><span style="font-weight:600;color:#D97706;"><?= number_format((float) $c['avg_rating'], 1) ?>/5</span><?php else: ?>-<?php endif; ?></td>
......
......@@ -2,15 +2,15 @@
<?php $__template->section('title'); ?>تقييم الأداء<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?><a href="/hr/performance/cycles" class="btn btn-secondary">جميع الدورات</a><?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php $csl = ['draft'=>'مسودة','active'=>'نشطة','completed'=>'مكتملة','closed'=>'مغلقة']; ?>
<?php $csc = ['draft'=>'#FEF3C7;color:#92400E','active'=>'#D1FAE5;color:#065F46','completed'=>'#DBEAFE;color:#1E40AF','closed'=>'#E5E7EB;color:#374151']; ?>
<?php $csl = ['draft'=>'مسودة','active'=>'نشطة','review'=>'تحت المراجعة','closed'=>'مغلقة']; ?>
<?php $csc = ['draft'=>'#FEF3C7;color:#92400E','active'=>'#D1FAE5;color:#065F46','review'=>'#DBEAFE;color:#1E40AF','closed'=>'#E5E7EB;color:#374151']; ?>
<?php if ($activeCycle): ?>
<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;">الدورة النشطة <span style="display:inline-block;padding:2px 8px;border-radius:9999px;font-size:12px;background:#D1FAE5;color:#065F46;">نشطة</span></h3></div>
<div style="padding:16px;display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:16px;">
<div><span style="color:#6B7280;font-size:13px;">اسم الدورة</span><div style="font-weight:600;"><?= e($activeCycle['name_ar']) ?></div></div>
<div><span style="color:#6B7280;font-size:13px;">النوع</span><div><?= $activeCycle['cycle_type'] === 'annual' ? 'سنوي' : ($activeCycle['cycle_type'] === 'semi_annual' ? 'نصف سنوي' : 'ربع سنوي') ?></div></div>
<div><span style="color:#6B7280;font-size:13px;">النوع</span><div><?= $activeCycle['period_type'] === 'annual' ? 'سنوي' : ($activeCycle['period_type'] === 'semi_annual' ? 'نصف سنوي' : 'ربع سنوي') ?></div></div>
<div><span style="color:#6B7280;font-size:13px;">الفترة</span><div><?= e($activeCycle['start_date']) ?> - <?= e($activeCycle['end_date']) ?></div></div>
</div>
<div style="padding:0 16px 16px;"><a href="/hr/performance/cycles/<?= (int) $activeCycle['id'] ?>" class="btn btn-primary">عرض التقييمات</a></div>
......@@ -32,7 +32,7 @@
<?php foreach ($recentCycles as $c): ?>
<tr>
<td style="font-weight:600;"><?= e($c['name_ar']) ?></td>
<td><?= $c['cycle_type'] === 'annual' ? 'سنوي' : ($c['cycle_type'] === 'semi_annual' ? 'نصف سنوي' : 'ربع سنوي') ?></td>
<td><?= $c['period_type'] === 'annual' ? 'سنوي' : ($c['period_type'] === 'semi_annual' ? 'نصف سنوي' : 'ربع سنوي') ?></td>
<td><?= e($c['start_date']) ?> - <?= e($c['end_date']) ?></td>
<td><span style="display:inline-block;padding:2px 8px;border-radius:9999px;font-size:12px;background:<?= $csc[$c['status']] ?? '#E5E7EB;color:#374151' ?>;"><?= e($csl[$c['status']] ?? $c['status']) ?></span></td>
<td><a href="/hr/performance/cycles/<?= (int) $c['id'] ?>" style="color:#2563EB;"><i data-lucide="eye" style="width:16px;height:16px;"></i></a></td>
......
......@@ -2,10 +2,10 @@
<?php $__template->section('title'); ?><?= e($cycle->name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?><a href="/hr/performance/cycles" class="btn btn-secondary">رجوع</a><?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php $csl = ['draft'=>'مسودة','active'=>'نشطة','completed'=>'مكتملة','closed'=>'مغلقة']; ?>
<?php $csc = ['draft'=>'#FEF3C7;color:#92400E','active'=>'#D1FAE5;color:#065F46','completed'=>'#DBEAFE;color:#1E40AF','closed'=>'#E5E7EB;color:#374151']; ?>
<?php $rsl = ['pending'=>'بانتظار التقييم','completed'=>'مكتمل','acknowledged'=>'تم الإقرار']; ?>
<?php $rsc = ['pending'=>'#FEF3C7;color:#92400E','completed'=>'#D1FAE5;color:#065F46','acknowledged'=>'#DBEAFE;color:#1E40AF']; ?>
<?php $csl = ['draft'=>'مسودة','active'=>'نشطة','review'=>'تحت المراجعة','closed'=>'مغلقة']; ?>
<?php $csc = ['draft'=>'#FEF3C7;color:#92400E','active'=>'#D1FAE5;color:#065F46','review'=>'#DBEAFE;color:#1E40AF','closed'=>'#E5E7EB;color:#374151']; ?>
<?php $rsl = ['pending'=>'بانتظار التقييم','in_progress'=>'قيد التقييم','submitted'=>'تم التقديم','acknowledged'=>'تم الإقرار']; ?>
<?php $rsc = ['pending'=>'#FEF3C7;color:#92400E','in_progress'=>'#FEF3C7;color:#92400E','submitted'=>'#D1FAE5;color:#065F46','acknowledged'=>'#DBEAFE;color:#1E40AF']; ?>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin-bottom:20px;">
<div class="card" style="padding:16px;text-align:center;"><div style="font-size:13px;color:#6B7280;">الحالة</div><div style="margin-top:4px;"><span style="display:inline-block;padding:2px 8px;border-radius:9999px;font-size:12px;background:<?= $csc[$cycle->status] ?? '#E5E7EB;color:#374151' ?>;"><?= e($csl[$cycle->status] ?? $cycle->status) ?></span></div></div>
......@@ -38,7 +38,7 @@ $pct = $total > 0 ? round($completed / $total * 100) : 0;
<?php else: ?>
<?php foreach ($reviews as $r): ?>
<tr>
<td><a href="/hr/employees/<?= (int) $r['employee_profile_id'] ?>"><?= e(($r['first_name_ar'] ?? '') . ' ' . ($r['last_name_ar'] ?? '')) ?></a></td>
<td><a href="/hr/employees/<?= (int) $r['employee_profile_id'] ?>"><?= e($r['full_name_ar'] ?? '') ?></a></td>
<td><?= e($r['department_name'] ?? '-') ?></td>
<td><?php if ((float) ($r['overall_rating'] ?? 0) > 0): ?>
<?php $rating = (float) $r['overall_rating']; $color = $rating >= 4 ? '#059669' : ($rating >= 3 ? '#D97706' : '#DC2626'); ?>
......
......@@ -12,6 +12,7 @@
<div><label style="display:block;margin-bottom:4px;font-size:13px;">الكود <span style="color:#DC2626;">*</span></label><input type="text" name="code" value="<?= e(old('code') ?? ($isEdit ? $structure->code : '')) ?>" class="form-control" required></div>
<div><label style="display:block;margin-bottom:4px;font-size:13px;">الوصف</label><input type="text" name="description" value="<?= e(old('description') ?? ($isEdit ? $structure->description : '')) ?>" class="form-control"></div>
<div><label style="display:block;margin-bottom:4px;font-size:13px;"><input type="checkbox" name="is_default" value="1" <?= (old('is_default') ?? ($isEdit ? $structure->is_default : 0)) ? 'checked' : '' ?>> هيكل افتراضي</label></div>
<div><label style="display:block;margin-bottom:4px;font-size:13px;"><input type="checkbox" name="is_active" value="1" <?= (old('is_active') ?? ($isEdit ? $structure->is_active : 1)) ? 'checked' : '' ?>> فعال</label></div>
</div>
</div>
<div style="display:flex;gap:10px;justify-content:flex-end;">
......
......@@ -66,13 +66,15 @@ class HrContractExpiryJob
$employeeName = $c['first_name_ar'] . ' ' . $c['last_name_ar'];
// Insert notification for HR managers
$this->db->insert('notifications', [
'type' => 'hr_contract_expiry',
'title' => 'عقد قارب على الانتهاء',
'message' => "عقد الموظف {$employeeName} سينتهي خلال {$daysRemaining} يوم (تاريخ الانتهاء: {$c['end_date']})",
'link' => '/hr/contracts/' . (int) $c['id'],
'is_read' => 0,
'created_at' => date('Y-m-d H:i:s'),
$this->db->insert('notification_queue', [
'type' => 'system',
'recipient_type' => 'employee',
'recipient_id' => 0,
'subject' => 'عقد قارب على الانتهاء',
'message' => "عقد الموظف {$employeeName} سينتهي خلال {$daysRemaining} يوم (تاريخ الانتهاء: {$c['end_date']})",
'data_json' => json_encode(['hr_type' => 'hr_contract_expiry', 'link' => '/hr/contracts/' . (int) $c['id']], JSON_UNESCAPED_UNICODE),
'status' => 'pending',
'created_at' => date('Y-m-d H:i:s'),
]);
$alerted++;
}
......
......@@ -59,22 +59,24 @@ class HrDocumentExpiryJob
$docTypeName = $docTypes[$doc['document_type']] ?? $doc['document_type'];
// Only alert once per week (check if similar notification exists in last 7 days)
$link = '/hr/documents/employee/' . (int) $doc['employee_profile_id'];
$recentAlert = $this->db->selectOne(
"SELECT id FROM notifications
WHERE type = 'hr_document_expiry'
AND link = ?
"SELECT id FROM notification_queue
WHERE data_json LIKE ?
AND created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)",
['/hr/documents/employee/' . (int) $doc['employee_profile_id']]
['%hr_document_expiry%' . $link . '%']
);
if (!$recentAlert) {
$this->db->insert('notifications', [
'type' => 'hr_document_expiry',
'title' => 'مستند قارب على الانتهاء',
'message' => "{$docTypeName} للموظف {$employeeName} سينتهي خلال {$daysRemaining} يوم (تاريخ الانتهاء: {$doc['expiry_date']})",
'link' => '/hr/documents/employee/' . (int) $doc['employee_profile_id'],
'is_read' => 0,
'created_at' => date('Y-m-d H:i:s'),
$this->db->insert('notification_queue', [
'type' => 'system',
'recipient_type' => 'employee',
'recipient_id' => 0,
'subject' => 'مستند قارب على الانتهاء',
'message' => "{$docTypeName} للموظف {$employeeName} سينتهي خلال {$daysRemaining} يوم (تاريخ الانتهاء: {$doc['expiry_date']})",
'data_json' => json_encode(['hr_type' => 'hr_document_expiry', 'link' => $link], JSON_UNESCAPED_UNICODE),
'status' => 'pending',
'created_at' => date('Y-m-d H:i:s'),
]);
$alerted++;
}
......
......@@ -48,13 +48,15 @@ class HrLoanDeductionReminderJob
$totalAmount = bcadd($totalAmount, (string) $inst['amount'], 2);
}
$this->db->insert('notifications', [
'type' => 'hr_loan_deduction',
'title' => 'تذكير: أقساط السلف',
'message' => "يوجد " . count($pendingInstallments) . " قسط سلفة مستحق هذا الشهر بإجمالي " . number_format((float) $totalAmount, 2) . " ج.م. تأكد من خصمها في الرواتب.",
'link' => '/hr/loans',
'is_read' => 0,
'created_at' => date('Y-m-d H:i:s'),
$this->db->insert('notification_queue', [
'type' => 'system',
'recipient_type' => 'employee',
'recipient_id' => 0,
'subject' => 'تذكير: أقساط السلف',
'message' => "يوجد " . count($pendingInstallments) . " قسط سلفة مستحق هذا الشهر بإجمالي " . number_format((float) $totalAmount, 2) . " ج.م. تأكد من خصمها في الرواتب.",
'data_json' => json_encode(['hr_type' => 'hr_loan_deduction', 'link' => '/hr/loans'], JSON_UNESCAPED_UNICODE),
'status' => 'pending',
'created_at' => date('Y-m-d H:i:s'),
]);
$reminded = count($pendingInstallments);
}
......@@ -73,13 +75,15 @@ class HrLoanDeductionReminderJob
$overdue = count($overdueInstallments);
if ($overdue > 0) {
$this->db->insert('notifications', [
'type' => 'hr_loan_overdue',
'title' => 'تنبيه: أقساط سلف متأخرة',
'message' => "يوجد {$overdue} قسط سلفة متأخر عن موعد السداد. يرجى مراجعة حالة السلف.",
'link' => '/hr/loans',
'is_read' => 0,
'created_at' => date('Y-m-d H:i:s'),
$this->db->insert('notification_queue', [
'type' => 'system',
'recipient_type' => 'employee',
'recipient_id' => 0,
'subject' => 'تنبيه: أقساط سلف متأخرة',
'message' => "يوجد {$overdue} قسط سلفة متأخر عن موعد السداد. يرجى مراجعة حالة السلف.",
'data_json' => json_encode(['hr_type' => 'hr_loan_overdue', 'link' => '/hr/loans'], JSON_UNESCAPED_UNICODE),
'status' => 'pending',
'created_at' => date('Y-m-d H:i:s'),
]);
}
......
......@@ -38,35 +38,41 @@ class HrPayrollReminderJob
if (!$period) {
// No payroll period created yet
$this->db->insert('notifications', [
'type' => 'hr_payroll_reminder',
'title' => 'تذكير: فترة الرواتب',
'message' => "لم يتم إنشاء فترة رواتب شهر {$monthName} {$year} بعد. يرجى إنشاء الفترة وحساب الرواتب.",
'link' => '/hr/payroll',
'is_read' => 0,
'created_at' => date('Y-m-d H:i:s'),
$this->db->insert('notification_queue', [
'type' => 'system',
'recipient_type' => 'employee',
'recipient_id' => 0,
'subject' => 'تذكير: فترة الرواتب',
'message' => "لم يتم إنشاء فترة رواتب شهر {$monthName} {$year} بعد. يرجى إنشاء الفترة وحساب الرواتب.",
'data_json' => json_encode(['hr_type' => 'hr_payroll_reminder', 'link' => '/hr/payroll'], JSON_UNESCAPED_UNICODE),
'status' => 'pending',
'created_at' => date('Y-m-d H:i:s'),
]);
$reminded++;
} elseif ($period['status'] === 'open') {
// Period exists but not yet calculated
$this->db->insert('notifications', [
'type' => 'hr_payroll_reminder',
'title' => 'تذكير: حساب الرواتب',
'message' => "فترة رواتب شهر {$monthName} {$year} مفتوحة ولم يتم حساب الرواتب بعد.",
'link' => '/hr/payroll/' . (int) $period['id'],
'is_read' => 0,
'created_at' => date('Y-m-d H:i:s'),
$this->db->insert('notification_queue', [
'type' => 'system',
'recipient_type' => 'employee',
'recipient_id' => 0,
'subject' => 'تذكير: حساب الرواتب',
'message' => "فترة رواتب شهر {$monthName} {$year} مفتوحة ولم يتم حساب الرواتب بعد.",
'data_json' => json_encode(['hr_type' => 'hr_payroll_reminder', 'link' => '/hr/payroll/' . (int) $period['id']], JSON_UNESCAPED_UNICODE),
'status' => 'pending',
'created_at' => date('Y-m-d H:i:s'),
]);
$reminded++;
} elseif ($period['status'] === 'calculated') {
// Calculated but not approved
$this->db->insert('notifications', [
'type' => 'hr_payroll_reminder',
'title' => 'تذكير: اعتماد الرواتب',
'message' => "رواتب شهر {$monthName} {$year} تم حسابها وبانتظار الاعتماد.",
'link' => '/hr/payroll/' . (int) $period['id'],
'is_read' => 0,
'created_at' => date('Y-m-d H:i:s'),
$this->db->insert('notification_queue', [
'type' => 'system',
'recipient_type' => 'employee',
'recipient_id' => 0,
'subject' => 'تذكير: اعتماد الرواتب',
'message' => "رواتب شهر {$monthName} {$year} تم حسابها وبانتظار الاعتماد.",
'data_json' => json_encode(['hr_type' => 'hr_payroll_reminder', 'link' => '/hr/payroll/' . (int) $period['id']], JSON_UNESCAPED_UNICODE),
'status' => 'pending',
'created_at' => date('Y-m-d H:i:s'),
]);
$reminded++;
}
......
......@@ -43,13 +43,15 @@ class HrProbationEndJob
$daysRemaining = (int) $emp['days_remaining'];
$employeeName = $emp['first_name_ar'] . ' ' . $emp['last_name_ar'];
$this->db->insert('notifications', [
'type' => 'hr_probation_end',
'title' => 'انتهاء فترة اختبار',
'message' => "فترة اختبار الموظف {$employeeName} ستنتهي خلال {$daysRemaining} يوم (تاريخ الانتهاء: {$emp['probation_end_date']})",
'link' => '/hr/employees/' . (int) $emp['id'],
'is_read' => 0,
'created_at' => date('Y-m-d H:i:s'),
$this->db->insert('notification_queue', [
'type' => 'system',
'recipient_type' => 'employee',
'recipient_id' => 0,
'subject' => 'انتهاء فترة اختبار',
'message' => "فترة اختبار الموظف {$employeeName} ستنتهي خلال {$daysRemaining} يوم (تاريخ الانتهاء: {$emp['probation_end_date']})",
'data_json' => json_encode(['hr_type' => 'hr_probation_end', 'link' => '/hr/employees/' . (int) $emp['id']], JSON_UNESCAPED_UNICODE),
'status' => 'pending',
'created_at' => date('Y-m-d H:i:s'),
]);
$alerted++;
}
......
# نظام الموارد البشرية — THE CLUB ERP
## نظرة عامة
نظام موارد بشرية متكامل مبني داخل منظومة THE CLUB ERP، مصمم وفقاً لقانون العمل المصري رقم 12 لسنة 2003 وقانون التأمينات الاجتماعية رقم 148 لسنة 2019. يغطي النظام دورة حياة الموظف بالكامل من التعيين حتى نهاية الخدمة.
**البنية التحتية:** 28 جدول قاعدة بيانات | 16 وحدة تحكم | 12 خدمة حسابية | 35 صلاحية | 80+ مسار
---
## 1. إدارة الموظفين
### 1.1 ملفات الموظفين
- ملف شامل لكل موظف يتضمن: الرقم القومي، تاريخ الميلاد، الجنس، الحالة الاجتماعية، الديانة، الحالة العسكرية
- ربط تلقائي مع حساب المستخدم في النظام (جدول employees) عبر مفتاح أجنبي 1:1
- تسجيل تاريخ التعيين وفترة الاختبار ونوع التوظيف (دوام كامل / جزئي / عقد / مؤقت)
- تتبع الحالة الوظيفية: نشط، في إجازة، موقوف، مُنهى خدمته، مستقيل
- بيانات بنكية كاملة للتحويلات: اسم البنك، رقم الحساب (IBAN)
- رقم التأمين الاجتماعي والراتب التأميني
- حقول الإعاقة والاحتياجات الخاصة
- بحث وفلترة متقدمة حسب القسم والحالة والمسمى الوظيفي
- أرشفة ناعمة (Soft Delete) — لا يُحذف أي سجل نهائياً
### 1.2 الأقسام والإدارات
- هيكل تنظيمي شجري (كل قسم يمكن أن يكون تابعاً لقسم أعلى)
- تعيين مدير لكل قسم
- ربط الأقسام بالفروع
- إضافة / تعديل / أرشفة الأقسام
### 1.3 المسميات الوظيفية
- تعريف المسميات مع الدرجة الوظيفية (Grade Level)
- تحديد الحد الأدنى والأقصى للراتب لكل مسمى
- إضافة / تعديل / أرشفة المسميات
---
## 2. العقود
### 2.1 إدارة العقود
- دعم أنواع العقود: محدد المدة، غير محدد المدة، تجريبي
- تسجيل تفاصيل العقد: تاريخ البدء والانتهاء، الراتب الأساسي، ساعات العمل، فترة الإنذار
- فترة اختبار (Probation) قابلة للتحديد — حد أقصى 3 شهور وفقاً للقانون
### 2.2 دورة حياة العقد
- **تجديد العقد**: إنشاء عقد جديد مرتبط بالعقد السابق مع تحديث التواريخ تلقائياً
- **إنهاء العقد**: تسجيل سبب وتاريخ الإنهاء مع تحديث حالة الموظف تلقائياً
- تنبيهات قبل انتهاء العقد بـ 30 يوم (عبر مهمة مجدولة)
- تنبيه عند انتهاء فترة الاختبار
---
## 3. الهيكل الراتبي
### 3.1 هياكل الرواتب
- تعريف هياكل رواتب متعددة (مثال: "الهيكل القياسي للنادي")
- كل هيكل يحتوي على مكونات مخصصة
### 3.2 مكونات الراتب
- 14 مكون افتراضي مُعرّف مسبقاً:
- **استحقاقات**: الراتب الأساسي، بدل سكن، بدل انتقال، بدل وجبات، بدل اجتماعي، بدل طبيعة عمل، أجر إضافي
- **استقطاعات**: حصة التأمينات، ضريبة كسب العمل، أقساط سلف، خصم جزاءات، اشتراك نقابة، خصم غياب
- كل مكون يحدد:
- النوع: ثابت / نسبة من الأساسي / معادلة
- خاضع للضريبة أم لا
- خاضع للتأمينات أم لا
- يؤثر على حساب الأجر الإضافي أم لا
- تعيين مكونات خاصة لكل موظف مع تاريخ سريان
---
## 4. الحضور والانصراف
### 4.1 تسجيل الحضور
- تسجيل يومي فردي أو جماعي (Bulk Entry) لكل القسم مرة واحدة
- دعم طرق التسجيل: يدوي، بصمة إصبع، كارت، تعرف على الوجه
- تسجيل وقت الدخول والخروج مع حساب الساعات الفعلية تلقائياً
### 4.2 المتابعة والتحليل
- حساب تلقائي لدقائق التأخير والانصراف المبكر
- حساب ساعات العمل الإضافي
- حالات الحضور: حاضر، غائب، متأخر، إجازة، عطلة رسمية، يوم راحة، نصف يوم
- عرض شهري لحضور كل موظف مع ملخص إحصائي
- اعتماد جماعي لسجلات الحضور
### 4.3 جداول العمل
- تعريف جداول عمل متعددة: 6 أيام، 5 أيام، وردية ليلية
- دعم ساعات رمضان (7 ساعات بدلاً من 8 للمسلمين — مادة 80)
- تعريف وردية ليلية (7 مساءً - 7 صباحاً) بـ 7 ساعات عمل
- تعيين جدول عمل لكل موظف مع تاريخ سريان
- كشف تلقائي لجدول رمضان حسب ديانة الموظف
### 4.4 العطلات الرسمية
- تعريف العطلات الوطنية والدينية
- دعم العطلات المتكررة سنوياً
- عطلات خاصة بديانة معينة (مثال: شم النسيم للمسيحيين)
- 7 عطلات مصرية مُعرّفة مسبقاً
---
## 5. إدارة الإجازات
### 5.1 أنواع الإجازات (وفقاً لقانون العمل المصري)
10 أنواع إجازات مُعرّفة مسبقاً وفقاً للقانون:
| النوع | المدة | النسبة المدفوعة | المرجع القانوني |
|-------|-------|-----------------|-----------------|
| سنوية (1-10 سنوات) | 21 يوم | 100% | مادة 47 |
| سنوية (10+ سنوات أو 50+ سنة) | 30 يوم | 100% | مادة 47 |
| سنوية (أعمال خطرة) | 45 يوم | 100% | مادة 47 |
| عارضة | 7 أيام/سنة (حد 2 متتالية) | 100% | مادة 51 |
| مرضية (أول 3 شهور) | 90 يوم | 75% | مادة 54 |
| مرضية (ثاني 3 شهور) | 90 يوم | 85% | مادة 54 |
| وضع (أمومة) | 90 يوم، حد 3 مرات بالمسيرة | 100% | مادة 91 |
| حج | 30 يوم، مرة واحدة بالمسيرة | بدون راتب | مادة 53 |
| زواج | 3 أيام | 100% | — |
| عزاء | 5 أيام | 100% | — |
### 5.2 إدارة الأرصدة
- رصيد تلقائي لكل موظف لكل نوع إجازة لكل سنة
- حساب الاستحقاق والمُستخدم والمُعلّق والمُرحّل والتعديلات
- ترحيل الرصيد السنوي (Carry Over) بحد أقصى 3 سنوات تراكم
- حد أدنى 6 أيام متتالية من الإجازة السنوية
- تعديل يدوي للأرصدة مع تسجيل السبب
### 5.3 طلبات الإجازة
- تقديم طلب إجازة مع تحديد النوع والتاريخ والمدة
- دعم نصف يوم إجازة (صباحي / مسائي)
- إرفاق مستندات (مثال: تقرير طبي للإجازة المرضية)
- سير عمل الاعتماد: معلق ← معتمد / مرفوض ← منفذ / ملغى
- إشعار SMS تلقائي عند اعتماد أو رفض الطلب
- خصم تلقائي من الرصيد عند الاعتماد
- إلغاء الطلب مع استرداد الرصيد تلقائياً
### 5.4 تقويم الإجازات
- عرض تقويم شهري لجميع الإجازات المعتمدة
- يساعد في التخطيط وتجنب تضارب الإجازات
---
## 6. كشوف الرواتب
### 6.1 محرك حساب الرواتب الآلي
محرك حسابي متكامل ينفذ 11 خطوة لكل موظف تلقائياً:
1. تحميل الهيكل الراتبي ومكوناته الفعالة
2. حساب أيام العمل والغياب والأجر الإضافي من سجل الحضور
3. حساب إجمالي الاستحقاقات: أساسي + بدلات + أجر إضافي
4. حساب حصة التأمينات الاجتماعية (موظف + صاحب عمل)
5. حساب ضريبة كسب العمل من الشرائح التصاعدية
6. حساب أقساط السلف المستحقة
7. حساب خصومات الجزاءات التأديبية
8. حساب خصومات الغياب
9. صافي الراتب = الإجمالي - جميع الاستقطاعات
10. تسجيل كشف الراتب + سجل المكونات + سجل التأمينات + سجل الضرائب
11. تحديث إجماليات الفترة
**جميع العمليات الحسابية تستخدم مكتبة bcmath بدقة رقمين عشريين — لا يُستخدم float أبداً**
### 6.2 فترات الرواتب
- فترة شهرية لكل شهر بدورة حياة كاملة:
- **مفتوحة****قيد المعالجة****محسوبة****معتمدة****مدفوعة****مغلقة**
- إعادة الحساب ممكنة قبل الاعتماد (يحذف السجلات السابقة ويعيد الحساب)
- بعد الإغلاق: السجلات مجمدة — أي تصحيحات تُرحّل للفترة التالية
- قفل الفترة لمنع التعديل
### 6.3 مفردات الراتب (Salary Slip)
- كشف راتب تفصيلي لكل موظف لكل فترة
- يعرض كل مكون (استحقاق / استقطاع) مع المبلغ
- إمكانية عرض مفردات الراتب الشخصي (للموظف نفسه)
- تسجيل JSON كامل لتفاصيل الحساب للمراجعة
### 6.4 الأجر الإضافي (Overtime)
- حساب تلقائي وفقاً للمادة 85:
- **نهاري**: 135% من أجر الساعة
- **ليلي**: 170% من أجر الساعة
- النسب قابلة للتعديل من إعدادات النظام
---
## 7. التأمينات الاجتماعية
### 7.1 الحساب الآلي (قانون 148 لسنة 2019)
| البند | حصة الموظف | حصة صاحب العمل |
|-------|------------|----------------|
| الأساسي | 9% | 12% |
| المتغير | 2% | 6.75% |
| **الإجمالي** | **11%** | **18.75%** |
- سقف تأميني قابل للتحديث سنوياً من الإعدادات
- حساب تلقائي مع كل كشف رواتب
- تسجيل تفصيلي لكل مكون (أساسي / متغير) لكل موظف لكل فترة
### 7.2 التقارير والنماذج
- **استمارة 1**: بيان اشتراك موظف جديد
- **استمارة 6**: كشف اشتراكات شهري لجميع الموظفين
- سجل تأمينات لكل فترة ولكل موظف
- تاريخ تأميني كامل لكل موظف
---
## 8. ضريبة كسب العمل
### 8.1 الحساب التصاعدي (شرائح 2024-2025)
| الشريحة | النسبة |
|---------|--------|
| حتى 40,000 ج.م | 0% |
| 40,001 — 55,000 | 10% |
| 55,001 — 70,000 | 15% |
| 70,001 — 200,000 | 20% |
| 200,001 — 400,000 | 22.5% |
| 400,001 — 1,200,000 | 25% |
| أكثر من 1,200,000 | 27.5% |
- إعفاء شخصي: 20,000 ج.م سنوياً
- إعفاء التأمينات: حصة الموظف مُعفاة من الضريبة
- **تتبع تراكمي (YTD)**: حساب الضريبة على أساس الدخل السنوي التراكمي لمنع الزيادة أو النقصان في الاستقطاع الشهري
- سجل ضريبي تفصيلي لكل موظف لكل شهر مع JSON لتفاصيل الشرائح
---
## 9. السلف والقروض
### 9.1 أنواع السلف
- **سلفة راتب**: استلاف من الراتب المقبل
- **قرض شخصي**: قرض بأقساط شهرية
- **قرض طوارئ**: لحالات الطوارئ
### 9.2 دورة حياة السلفة
- تقديم طلب ← اعتماد / رفض ← صرف ← سداد (أقساط) ← تسوية كاملة
- إشعار SMS عند اعتماد السلفة
- تحديد عدد الأقساط ومبلغ كل قسط وتاريخ بداية الخصم
### 9.3 جدول الأقساط
- إنشاء تلقائي لجدول الأقساط عند اعتماد القرض
- ربط كل قسط بكشف الراتب الشهري
- تتبع الأقساط: معلق، مخصوم، مُتخطى، ملغى
- خصم تلقائي من الراتب الشهري عبر محرك الرواتب
- تتبع المبلغ المتبقي والأقساط المسددة
---
## 10. الإجراءات التأديبية
### 10.1 الامتثال لقانون العمل (مواد 58-68)
- **تحقيق إلزامي** قبل أي عقوبة
- **حد أقصى لخصم الراتب**: 5 أيام شهرياً (مادة 58) — النظام يرفض تلقائياً ما يتجاوز الحد
- **حد أقصى للإيقاف**: 6 أيام شهرياً — النظام يرفض تلقائياً ما يتجاوز الحد
- **حق التظلم**: 7 أيام من تاريخ القرار — النظام يحسب المهلة تلقائياً
### 10.2 دورة الحياة الكاملة
تحت التحقيق ← في انتظار القرار ← تم تحديد العقوبة ← منفذ / متظلم ← تم مراجعة التظلم / ملغى
### 10.3 أنواع الجزاءات (تدرج تصاعدي)
1. إنذار
2. خصم من الراتب
3. تأجيل علاوة
4. إيقاف عن العمل
5. تنزيل درجة
6. فصل
### 10.4 التظلم
- تقديم تظلم مع كتابة السبب
- مراجعة التظلم من صاحب الصلاحية
- تسجيل نتيجة التظلم
### 10.5 الربط مع الرواتب
- خصومات الجزاءات التأديبية تُخصم تلقائياً من كشف الراتب الشهري
- حساب المبلغ المخصوم: (الراتب الأساسي ÷ 30) × عدد أيام الخصم
---
## 11. نهاية الخدمة
### 11.1 أنواع إنهاء الخدمة
- استقالة، فصل، انتهاء عقد، تقاعد، وفاة، عجز، اتفاق مشترك
### 11.2 حساب المستحقات الآلي
| البند | الصيغة |
|-------|--------|
| مكافأة أول 5 سنوات | نصف شهر × عدد السنوات |
| مكافأة بعد 5 سنوات | شهر كامل × عدد السنوات |
| تعويض فترة الإنذار | الراتب الشامل × عدد الأشهر (2 أو 3) |
| رصيد الإجازات | (الراتب ÷ 30) × الأيام المتبقية |
| خصم السلف | المبلغ المتبقي من جميع القروض النشطة |
| **الصافي** | **إجمالي المستحقات - إجمالي الخصومات** |
- فترة الإنذار: شهرين (أقل من 10 سنوات)، 3 شهور (10 سنوات فأكثر)
- **حماية الأمومة (مادة 91)**: النظام يرفض تلقائياً إنهاء خدمة موظفة أثناء إجازة الوضع أو خلال سنة من العودة
### 11.3 دورة الاعتماد
مسودة ← تم الحساب ← في انتظار الاعتماد ← معتمد ← تم الصرف
- عند الاعتماد: تتحدث حالة الموظف تلقائياً إلى "مُنهى خدمته"
- تسجيل تاريخ الصرف
---
## 12. تقييم الأداء
### 12.1 دورات التقييم
- دعم دورات: سنوية، نصف سنوية، ربع سنوية
- إنشاء مراجعات تلقائية لكل موظف عند فتح دورة تقييم
### 12.2 معايير التقييم
8 معايير مُعرّفة مسبقاً (قابلة للتخصيص):
1. جودة العمل
2. الإنتاجية
3. الانتظام والحضور
4. العمل الجماعي
5. المبادرة والإبداع
6. التواصل
7. القيادة
8. المهارات الفنية
- تقييم كل معيار من 1 (ضعيف) إلى 5 (ممتاز)
- تقييم عام إجمالي مع تصنيف تلقائي: ممتاز، جيد جداً، جيد، مقبول، ضعيف
### 12.3 سير العمل
بانتظار التقييم ← قيد التقييم ← مكتمل ← تم الإقرار
- تسجيل نقاط القوة ومجالات التحسين والأهداف للفترة القادمة
- ملاحظات المقيّم
- إقرار الموظف بالاطلاع على التقييم
---
## 13. مستندات الموظفين
### 13.1 أنواع المستندات
- بطاقة الرقم القومي، جواز السفر، شهادة الميلاد
- الشهادات العلمية
- شهادة تأدية/إعفاء الخدمة العسكرية
- استمارات التأمينات (1 و 6)
- التقارير الطبية
- صحيفة الحالة الجنائية
- شهادات الخبرة
- نسخ العقود
- صور شخصية
- مستندات أخرى
### 13.2 إدارة الملفات
- رفع ملفات بصيغ PDF, JPEG, PNG (حد أقصى 5 ميجابايت)
- تسجيل اسم الملف الأصلي وحجمه ونوعه
- تتبع تاريخ الانتهاء مع تنبيه قبل 30 يوم (عبر مهمة مجدولة)
- التحقق من صحة المستند من قبل المختص
- أرشفة المستندات المنتهية أو غير المطلوبة
---
## 14. التقارير
8 تقارير متخصصة:
| التقرير | الوصف |
|---------|-------|
| **القوى العاملة** (Headcount) | توزيع الموظفين حسب الأقسام والحالة والجنس |
| **ملخص الرواتب** | إجماليات الرواتب لكل فترة: استحقاقات، استقطاعات، صافي |
| **ملخص الحضور** | أيام الحضور والغياب والتأخير والإضافي لكل قسم |
| **ملخص الإجازات** | أرصدة الإجازات المُستخدمة والمتبقية لكل نوع |
| **ملخص التأمينات** | حصص الموظفين وأصحاب العمل لكل فترة |
| **ملخص الضرائب** | الضرائب المستقطعة شهرياً وتراكمياً |
| **ملخص السلف** | القروض النشطة والأقساط المسددة والمتبقية |
| **العقود المنتهية** | العقود القريبة من الانتهاء أو المنتهية فعلاً |
---
## 15. المهام المجدولة (Cron Jobs)
7 مهام تعمل تلقائياً:
| المهمة | التكرار | الوظيفة |
|--------|---------|---------|
| تنبيه انتهاء العقود | يومياً | إشعار قبل 30 يوم من انتهاء العقد |
| تنبيه انتهاء فترة الاختبار | يومياً | إشعار عند اقتراب انتهاء فترة التجربة |
| تجديد أرصدة الإجازات | 1 يناير سنوياً | حساب الترحيل وإنشاء أرصدة السنة الجديدة |
| تذكير كشف الرواتب | يوم 20 شهرياً | تنبيه إذا كانت فترة الرواتب مازالت مفتوحة |
| تذكير أقساط السلف | يوم 1 شهرياً | التحقق من الأقساط المعلقة للخصم |
| تنبيه انتهاء المستندات | يومياً | إشعار قبل 30 يوم من انتهاء صلاحية المستندات |
| إغلاق الحضور التلقائي | يومياً منتصف الليل | إغلاق سجلات الحضور المفتوحة (بدون تسجيل خروج) |
---
## 16. الإشعارات
7 قوالب SMS بالعربية:
1. اعتماد طلب إجازة
2. رفض طلب إجازة
3. إصدار مفردات الراتب
4. اقتراب انتهاء العقد
5. انتهاء فترة الاختبار
6. اعتماد السلفة
7. انتهاء صلاحية مستند
---
## 17. الأمان والصلاحيات
### 35 صلاحية مُفصّلة:
- **الأقسام**: عرض، إدارة
- **المسميات الوظيفية**: عرض، إدارة
- **الموظفون**: عرض، إدارة، عرض الرواتب، إدارة الرواتب
- **العقود**: عرض، إدارة
- **الحضور**: عرض، إدارة، اعتماد
- **الإجازات**: عرض، طلب، اعتماد، إدارة
- **الرواتب**: عرض، تشغيل، اعتماد + عرض مفردات الراتب الشخصي
- **التأمينات**: عرض، إدارة
- **الضرائب**: عرض
- **الإجراءات التأديبية**: عرض، إدارة
- **السلف**: عرض، إدارة، اعتماد
- **نهاية الخدمة**: عرض، إدارة، اعتماد
- **تقييم الأداء**: عرض، إدارة
- **المستندات**: عرض، إدارة
- **التقارير**: عرض
- **العطلات**: إدارة
- **جداول العمل**: إدارة
### حماية البيانات
- حماية CSRF على جميع النماذج
- تنظيف المدخلات (XSS Protection)
- أرشفة ناعمة (Soft Delete) — لا يُحذف أي سجل نهائياً
- تسجيل تلقائي لمن أنشأ ومن عدّل كل سجل (created_by, updated_by)
- سجل مراجعة (Audit Trail) للعمليات الحساسة: تغيير الراتب، اعتماد كشف الرواتب، إنهاء الخدمة
---
## 18. الإعدادات القابلة للتخصيص
~25 إعداد في `system_config` تحت مفتاح `hr.*`:
| الإعداد | القيمة الافتراضية | الوصف |
|---------|-------------------|-------|
| نسبة الأجر الإضافي نهاري | 1.35 (135%) | مادة 85 |
| نسبة الأجر الإضافي ليلي | 1.70 (170%) | مادة 85 |
| حد خصم الراتب الشهري | 5 أيام | مادة 58 |
| حد الإيقاف الشهري | 6 أيام | مادة 58 |
| مهلة التظلم | 7 أيام | مادة 68 |
| نسبة تأمين الموظف (أساسي) | 9% | قانون 148/2019 |
| نسبة تأمين الموظف (متغير) | 2% | قانون 148/2019 |
| نسبة تأمين صاحب العمل (أساسي) | 12% | قانون 148/2019 |
| نسبة تأمين صاحب العمل (متغير) | 6.75% | قانون 148/2019 |
| سقف الأجر التأميني (أساسي) | قابل للتحديث سنوياً | — |
| سقف الأجر التأميني (متغير) | قابل للتحديث سنوياً | — |
| الإعفاء الشخصي الضريبي | 20,000 ج.م سنوياً | — |
| شرائح الضريبة | 7 شرائح | — |
| مكافأة نهاية الخدمة (أول 5 سنوات) | 0.5 شهر/سنة | — |
| مكافأة نهاية الخدمة (بعد 5 سنوات) | 1.0 شهر/سنة | — |
| فترة إنذار (أقل من 10 سنوات) | شهرين | — |
| فترة إنذار (10 سنوات فأكثر) | 3 شهور | — |
**جميع النسب والحدود قابلة للتعديل من لوحة الإدارة بدون تعديل كود — يكفي تحديث القيمة في جدول الإعدادات.**
---
## 19. التكامل مع باقي الأنظمة
| النظام | نوع التكامل |
|--------|------------|
| **المستخدمون** | ربط ملف الموظف مع حساب الدخول (1:1) |
| **الفروع** | الأقسام مرتبطة بالفروع + فلترة الرواتب حسب الفرع |
| **الإشعارات / SMS** | 7 قوالب إشعار للإجازات والرواتب والعقود والسلف والمستندات |
| **سجل المراجعة** | تسجيل تلقائي لجميع العمليات الحساسة |
| **الأحداث** | نظام أحداث (EventBus) للربط مع أي نظام مستقبلي |
---
## ملخص تنفيذي
| البند | القيمة |
|-------|--------|
| جداول قاعدة البيانات | 28 |
| وحدات التحكم | 16 |
| الخدمات الحسابية | 12 |
| ملفات العرض | ~50 |
| المسارات (Routes) | 80+ |
| الصلاحيات | 35 |
| المهام المجدولة | 7 |
| قوالب SMS | 7 |
| أنواع الإجازات | 10 |
| مكونات الراتب الافتراضية | 14 |
| إعدادات قابلة للتخصيص | ~25 |
| المرجعية القانونية | قانون العمل 12/2003 + قانون التأمينات 148/2019 |
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