Commit 485c9da7 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Fixed 19 Tickets

parent 71387a23
......@@ -127,11 +127,11 @@ final class LedgerService
$params = [$dateFrom, $dateTo];
if ($costCenterId !== null) {
$extraWhere .= ' AND jel.cost_center_id = ?';
$extraWhere .= ' AND COALESCE(jel.cost_center_id, je.cost_center_id) = ?';
$params[] = $costCenterId;
}
if ($branchId !== null) {
$extraWhere .= ' AND jel.branch_id = ?';
$extraWhere .= ' AND COALESCE(jel.branch_id, je.branch_id) = ?';
$params[] = $branchId;
}
......@@ -144,12 +144,10 @@ final class LedgerService
COALESCE(SUM(jel.credit), 0) as total_credit
FROM chart_of_accounts coa
LEFT JOIN journal_entry_lines jel ON jel.account_id = coa.id
AND jel.journal_entry_id IN (
SELECT id FROM journal_entries
WHERE status = 'posted' AND entry_date >= ? AND entry_date <= ? AND is_archived = 0
)
LEFT JOIN journal_entries je ON je.id = jel.journal_entry_id
AND je.status = 'posted' AND je.entry_date >= ? AND je.entry_date <= ? AND je.is_archived = 0
{$extraWhere}
WHERE coa.is_archived = 0
WHERE coa.is_archived = 0 AND (jel.id IS NULL OR je.id IS NOT NULL)
GROUP BY coa.id, coa.account_code, coa.name_ar, coa.name_en,
coa.account_type, coa.account_nature, coa.level, coa.is_header,
coa.opening_balance, coa.current_balance
......
......@@ -176,6 +176,14 @@ class ActivitySubscriptionController extends Controller
public function generate(Request $request): Response
{
$month = trim((string) $request->post('month', ''));
if ($month !== '' && !preg_match('/^\d{4}-\d{2}$/', $month)) {
$parsed = date_create_from_format('F Y', $month) ?: date_create_from_format('M Y', $month);
if ($parsed) {
$month = $parsed->format('Y-m');
}
}
if ($month === '' || !preg_match('/^\d{4}-\d{2}$/', $month)) {
return $this->redirect('/activity-subscriptions')->withError('يجب تحديد شهر صحيح بصيغة YYYY-MM');
}
......
......@@ -57,7 +57,7 @@ final class PaymentRequestService
$id = $db->insert('payment_requests', [
'request_number' => $requestNumber,
'member_id' => $memberId,
'member_id' => $memberId > 0 ? $memberId : null,
'payment_type' => $paymentType,
'amount' => $amount,
'currency' => $currency,
......
......@@ -344,6 +344,64 @@ class FacilityController extends Controller
return $this->redirect('/facilities/' . $id)->withSuccess('تم حذف تاريخ الحظر');
}
public function createTimeSlot(Request $request, string $id): Response
{
$facility = Facility::find((int) $id);
if (!$facility) {
return $this->redirect('/facilities')->withError('المرفق غير موجود');
}
$db = App::getInstance()->db();
$units = $db->select("SELECT id, name_ar FROM facility_units WHERE facility_id = ? AND is_active = 1 ORDER BY name_ar", [(int) $id]);
return $this->view('Facilities.Views.timeslots.create', [
'facility' => $facility,
'units' => $units,
]);
}
public function storeTimeSlot(Request $request, string $id): Response
{
$facility = Facility::find((int) $id);
if (!$facility) {
return $this->redirect('/facilities')->withError('المرفق غير موجود');
}
$db = App::getInstance()->db();
$unitId = (int) $request->post('facility_unit_id', 0);
$dayOfWeek = (int) $request->post('day_of_week', 0);
$startTime = trim((string) $request->post('start_time', ''));
$endTime = trim((string) $request->post('end_time', ''));
$slotType = trim((string) $request->post('slot_type', 'blocked'));
$label = trim((string) $request->post('label', ''));
if ($startTime === '' || $endTime === '') {
return $this->redirect("/facilities/{$id}/timeslots/create")->withError('وقت البداية والنهاية مطلوبان');
}
$db->insert('facility_time_slots', [
'facility_id' => (int) $id,
'facility_unit_id' => $unitId > 0 ? $unitId : null,
'day_of_week' => $dayOfWeek,
'start_time' => $startTime,
'end_time' => $endTime,
'slot_type' => $slotType,
'label' => $label ?: null,
'is_active' => 1,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
return $this->redirect('/facilities/' . $id)->withSuccess('تم حظر الفترة الزمنية بنجاح');
}
public function deleteTimeSlot(Request $request, string $id, string $slotId): Response
{
$db = App::getInstance()->db();
$db->delete('facility_time_slots', 'id = ? AND facility_id = ?', [(int) $slotId, (int) $id]);
return $this->redirect('/facilities/' . $id)->withSuccess('تم حذف الفترة الزمنية');
}
/**
* Build the config array from the request.
*/
......
......@@ -11,4 +11,7 @@ return [
['POST', '/facilities/{id:\d+}/toggle', 'Facilities\Controllers\FacilityController@toggle', ['auth', 'csrf'], 'facility.manage'],
['POST', '/facilities/{id:\d+}/blackout', 'Facilities\Controllers\FacilityController@addBlackout', ['auth', 'csrf'], 'facility.manage'],
['POST', '/facilities/{id:\d+}/blackout/remove', 'Facilities\Controllers\FacilityController@removeBlackout', ['auth', 'csrf'], 'facility.manage'],
['GET', '/facilities/{id:\d+}/timeslots/create','Facilities\Controllers\FacilityController@createTimeSlot', ['auth'], 'facility.manage'],
['POST', '/facilities/{id:\d+}/timeslots', 'Facilities\Controllers\FacilityController@storeTimeSlot', ['auth', 'csrf'], 'facility.manage'],
['POST', '/facilities/{id:\d+}/timeslots/{slotId:\d+}/delete', 'Facilities\Controllers\FacilityController@deleteTimeSlot', ['auth', 'csrf'], 'facility.manage'],
];
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>حظر فترة زمنية — <?= e($facility->name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/facilities/<?= (int) $facility->id ?>" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للمرفق</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<form method="POST" action="/facilities/<?= (int) $facility->id ?>/timeslots">
<?= csrf_field() ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="clock" style="width:18px;height:18px;color:#DC2626;"></i>
<h3 style="margin:0;color:#DC2626;font-size:15px;">حظر فترة زمنية</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;">
<?php if (!empty($units)): ?>
<div class="form-group">
<label class="form-label">الوحدة</label>
<select name="facility_unit_id" class="form-select">
<option value="0">— المرفق بالكامل —</option>
<?php foreach ($units as $unit): ?>
<option value="<?= (int) $unit['id'] ?>"><?= e($unit['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<?php endif; ?>
<div class="form-group">
<label class="form-label">اليوم <span style="color:#DC2626;">*</span></label>
<select name="day_of_week" class="form-select" required>
<option value="0">الأحد</option>
<option value="1">الإثنين</option>
<option value="2">الثلاثاء</option>
<option value="3">الأربعاء</option>
<option value="4">الخميس</option>
<option value="5">الجمعة</option>
<option value="6">السبت</option>
</select>
</div>
<div class="form-group">
<label class="form-label">النوع</label>
<select name="slot_type" class="form-select">
<option value="blocked">محظور</option>
<option value="maintenance">صيانة</option>
<option value="reserved">محجوز</option>
</select>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;margin-top:15px;">
<div class="form-group">
<label class="form-label">وقت البداية <span style="color:#DC2626;">*</span></label>
<input type="time" name="start_time" class="form-input" required style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">وقت النهاية <span style="color:#DC2626;">*</span></label>
<input type="time" name="end_time" class="form-input" required style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">ملاحظة</label>
<input type="text" name="label" class="form-input" placeholder="سبب الحظر (اختياري)">
</div>
</div>
</div>
</div>
<div style="display:flex;gap:10px;justify-content:flex-start;">
<button type="submit" class="btn btn-primary" style="padding:12px 30px;font-size:15px;">
<i data-lucide="lock" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> حظر الفترة
</button>
<a href="/facilities/<?= (int) $facility->id ?>" class="btn btn-outline" style="padding:12px 30px;font-size:15px;">إلغاء</a>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
......@@ -48,11 +48,11 @@ class EmployeeProfileController extends Controller
// Get employees without HR profiles yet
$unlinked = $db->select(
"SELECT e.id, e.username, e.name
"SELECT e.id, e.username, e.full_name_ar
FROM employees e
LEFT JOIN hr_employee_profiles hp ON hp.employee_id = e.id AND hp.is_archived = 0
WHERE hp.id IS NULL AND e.is_archived = 0
ORDER BY e.name ASC"
ORDER BY e.full_name_ar ASC"
);
return $this->view('HR.Views.employees.form', [
......@@ -263,6 +263,20 @@ class EmployeeProfileController extends Controller
return $this->redirect('/hr/employees/' . $id . '/salary')->withError('لم يتم تحديد مكونات الراتب');
}
// Validate payment basis compatibility
if ($profile->salary_structure_id) {
$structure = $db->selectOne("SELECT payment_basis FROM hr_salary_structures WHERE id = ?", [(int) $profile->salary_structure_id]);
if ($structure && in_array($structure['payment_basis'], ['hourly', 'per_session'], true)) {
foreach ($componentAmounts as $componentId => $amount) {
$comp = $db->selectOne("SELECT component_code FROM hr_salary_components WHERE id = ?", [(int) $componentId]);
if ($comp && $comp['component_code'] === 'BASIC_SALARY') {
return $this->redirect('/hr/employees/' . $id . '/salary')
->withError('لا يمكن إضافة راتب شهري لموظف بنظام الأجر بالساعة/الحصة. يرجى استخدام مكونات الأجر المناسبة.');
}
}
}
}
$db->beginTransaction();
try {
foreach ($componentAmounts as $componentId => $amount) {
......
......@@ -18,7 +18,7 @@
<select name="employee_id" class="form-control" required>
<option value="">-- اختر --</option>
<?php foreach ($unlinked as $u): ?>
<option value="<?= (int) $u['id'] ?>" <?= old('employee_id') == $u['id'] ? 'selected' : '' ?>><?= e($u['name'] . ' (' . $u['username'] . ')') ?></option>
<option value="<?= (int) $u['id'] ?>" <?= old('employee_id') == $u['id'] ? 'selected' : '' ?>><?= e($u['full_name_ar'] . ' (' . $u['username'] . ')') ?></option>
<?php endforeach; ?>
</select>
<?php if (empty($unlinked)): ?>
......
......@@ -375,15 +375,10 @@ class GroupController extends Controller
$facilityUnitIds = [];
}
// Deactivate old schedules
$db->update('sa_group_schedule', [
'is_active' => 0,
'updated_at' => date('Y-m-d H:i:s'),
], 'group_id = ? AND is_active = 1', [(int) $id]);
// Insert new entries
// Insert or reactivate new schedule entries
$count = min(count($facilityUnitIds), count($daysOfWeek), count($startTimes), count($endTimes));
$inserted = 0;
$keepIds = [];
for ($i = 0; $i < $count; $i++) {
$unitId = (int) ($facilityUnitIds[$i] ?? 0);
......@@ -395,8 +390,21 @@ class GroupController extends Controller
continue;
}
try {
$db->insert('sa_group_schedule', [
$existing = $db->selectOne(
"SELECT id FROM sa_group_schedule WHERE group_id = ? AND facility_unit_id = ? AND day_of_week = ? AND start_time = ?",
[(int) $id, $unitId, $day, $start]
);
if ($existing) {
$db->update('sa_group_schedule', [
'end_time' => $end,
'is_active' => 1,
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int) $existing['id']]);
$keepIds[] = (int) $existing['id'];
$inserted++;
} else {
$newId = $db->insert('sa_group_schedule', [
'group_id' => (int) $id,
'facility_unit_id' => $unitId,
'day_of_week' => $day,
......@@ -406,15 +414,25 @@ class GroupController extends Controller
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
$keepIds[] = $newId;
$inserted++;
} catch (\PDOException $e) {
if (str_contains($e->getMessage(), 'Duplicate entry')) {
continue;
}
throw $e;
}
}
// Deactivate only schedules not in the new set
if (!empty($keepIds)) {
$placeholders = implode(',', array_fill(0, count($keepIds), '?'));
$db->query(
"UPDATE sa_group_schedule SET is_active = 0, updated_at = NOW() WHERE group_id = ? AND is_active = 1 AND id NOT IN ({$placeholders})",
array_merge([(int) $id], $keepIds)
);
} else {
$db->update('sa_group_schedule', [
'is_active' => 0,
'updated_at' => date('Y-m-d H:i:s'),
], 'group_id = ? AND is_active = 1', [(int) $id]);
}
return $this->redirect('/sa/groups/' . $id)->withSuccess("تم حفظ الجدول ({$inserted} حصة)");
}
......
......@@ -89,6 +89,13 @@ class SubscriptionController extends Controller
{
$yearMonth = trim((string) $request->post('year_month', ''));
if ($yearMonth !== '' && !preg_match('/^\d{4}-\d{2}$/', $yearMonth)) {
$parsed = date_create_from_format('F Y', $yearMonth) ?: date_create_from_format('M Y', $yearMonth);
if ($parsed) {
$yearMonth = $parsed->format('Y-m');
}
}
if ($yearMonth === '' || !preg_match('/^\d{4}-\d{2}$/', $yearMonth)) {
return $this->redirect('/sa/subscriptions')->withError('يرجى تحديد شهر صالح');
}
......
......@@ -20,7 +20,7 @@
<!-- Attendance Form -->
<div class="card">
<form method="POST" action="/sa/attendance/store/<?= (int) $booking['id'] ?>">
<form method="POST" action="/sa/attendance/record/<?= (int) $booking['id'] ?>">
<?= csrf_field() ?>
<div class="table-responsive">
......
<?php
declare(strict_types=1);
return [
'up' => "ALTER TABLE payment_requests MODIFY COLUMN member_id BIGINT UNSIGNED NULL DEFAULT NULL",
'down' => "ALTER TABLE payment_requests MODIFY COLUMN member_id BIGINT UNSIGNED NOT NULL",
];
<?php
declare(strict_types=1);
return [
'up' => "ALTER TABLE hr_salary_structures ADD COLUMN payment_basis VARCHAR(20) NOT NULL DEFAULT 'monthly' COMMENT 'monthly, hourly, per_session' AFTER description",
'down' => "ALTER TABLE hr_salary_structures DROP COLUMN payment_basis",
];
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