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,12 +76,18 @@ 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', [
'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'),
......@@ -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
......
......@@ -38,7 +38,6 @@ class HrContract extends Model
{
return [
'draft' => 'مسودة',
'pending_approval' => 'في انتظار الاعتماد',
'active' => 'ساري',
'renewed' => 'تم التجديد',
'expired' => 'منتهي',
......@@ -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,12 +66,14 @@ 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' => 'عقد قارب على الانتهاء',
$this->db->insert('notification_queue', [
'type' => 'system',
'recipient_type' => 'employee',
'recipient_id' => 0,
'subject' => 'عقد قارب على الانتهاء',
'message' => "عقد الموظف {$employeeName} سينتهي خلال {$daysRemaining} يوم (تاريخ الانتهاء: {$c['end_date']})",
'link' => '/hr/contracts/' . (int) $c['id'],
'is_read' => 0,
'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,21 +59,23 @@ 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' => 'مستند قارب على الانتهاء',
$this->db->insert('notification_queue', [
'type' => 'system',
'recipient_type' => 'employee',
'recipient_id' => 0,
'subject' => 'مستند قارب على الانتهاء',
'message' => "{$docTypeName} للموظف {$employeeName} سينتهي خلال {$daysRemaining} يوم (تاريخ الانتهاء: {$doc['expiry_date']})",
'link' => '/hr/documents/employee/' . (int) $doc['employee_profile_id'],
'is_read' => 0,
'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,12 +48,14 @@ class HrLoanDeductionReminderJob
$totalAmount = bcadd($totalAmount, (string) $inst['amount'], 2);
}
$this->db->insert('notifications', [
'type' => 'hr_loan_deduction',
'title' => 'تذكير: أقساط السلف',
$this->db->insert('notification_queue', [
'type' => 'system',
'recipient_type' => 'employee',
'recipient_id' => 0,
'subject' => 'تذكير: أقساط السلف',
'message' => "يوجد " . count($pendingInstallments) . " قسط سلفة مستحق هذا الشهر بإجمالي " . number_format((float) $totalAmount, 2) . " ج.م. تأكد من خصمها في الرواتب.",
'link' => '/hr/loans',
'is_read' => 0,
'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,12 +75,14 @@ class HrLoanDeductionReminderJob
$overdue = count($overdueInstallments);
if ($overdue > 0) {
$this->db->insert('notifications', [
'type' => 'hr_loan_overdue',
'title' => 'تنبيه: أقساط سلف متأخرة',
$this->db->insert('notification_queue', [
'type' => 'system',
'recipient_type' => 'employee',
'recipient_id' => 0,
'subject' => 'تنبيه: أقساط سلف متأخرة',
'message' => "يوجد {$overdue} قسط سلفة متأخر عن موعد السداد. يرجى مراجعة حالة السلف.",
'link' => '/hr/loans',
'is_read' => 0,
'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,34 +38,40 @@ class HrPayrollReminderJob
if (!$period) {
// No payroll period created yet
$this->db->insert('notifications', [
'type' => 'hr_payroll_reminder',
'title' => 'تذكير: فترة الرواتب',
$this->db->insert('notification_queue', [
'type' => 'system',
'recipient_type' => 'employee',
'recipient_id' => 0,
'subject' => 'تذكير: فترة الرواتب',
'message' => "لم يتم إنشاء فترة رواتب شهر {$monthName} {$year} بعد. يرجى إنشاء الفترة وحساب الرواتب.",
'link' => '/hr/payroll',
'is_read' => 0,
'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' => 'تذكير: حساب الرواتب',
$this->db->insert('notification_queue', [
'type' => 'system',
'recipient_type' => 'employee',
'recipient_id' => 0,
'subject' => 'تذكير: حساب الرواتب',
'message' => "فترة رواتب شهر {$monthName} {$year} مفتوحة ولم يتم حساب الرواتب بعد.",
'link' => '/hr/payroll/' . (int) $period['id'],
'is_read' => 0,
'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' => 'تذكير: اعتماد الرواتب',
$this->db->insert('notification_queue', [
'type' => 'system',
'recipient_type' => 'employee',
'recipient_id' => 0,
'subject' => 'تذكير: اعتماد الرواتب',
'message' => "رواتب شهر {$monthName} {$year} تم حسابها وبانتظار الاعتماد.",
'link' => '/hr/payroll/' . (int) $period['id'],
'is_read' => 0,
'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,12 +43,14 @@ 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' => 'انتهاء فترة اختبار',
$this->db->insert('notification_queue', [
'type' => 'system',
'recipient_type' => 'employee',
'recipient_id' => 0,
'subject' => 'انتهاء فترة اختبار',
'message' => "فترة اختبار الموظف {$employeeName} ستنتهي خلال {$daysRemaining} يوم (تاريخ الانتهاء: {$emp['probation_end_date']})",
'link' => '/hr/employees/' . (int) $emp['id'],
'is_read' => 0,
'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++;
......
This diff is collapsed.
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