Commit 26eb4204 authored by Fares's avatar Fares

feat(sa): extend facility grid system to all facility types

The zone grid was previously restricted to pool-type facilities only.
Now any facility can have a grid configured (rows × cols, max 25 each)
via the create/edit forms. Facilities with grid dimensions get the
visual grid manager; those without keep the unit-based mirror view.

Changes:
- Remove facility_type='pool' filter from PoolGridService::getFacility
- MirrorController routes by grid dimensions instead of facility type
- Facility create/edit forms now include grid rows/cols fields
- FacilityController store/update save grid dimensions
- Facility model fillable updated with grid columns
- Grid size limit increased from 20 to 25
- Facility show page adds "الشبكة" button when grid is configured
- MirrorStateService and SlotAvailabilityService check grid dims
  instead of facility_type for zone booking cross-checks
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 858cd3f6
......@@ -147,7 +147,12 @@ class FacilityController extends Controller
'slot_minutes' => $slotMinutes,
], JSON_UNESCAPED_UNICODE);
Facility::create([
$gridRows = (int) $request->post('grid_rows', 0);
$gridCols = (int) $request->post('grid_cols', 0);
if ($gridRows > 25) $gridRows = 25;
if ($gridCols > 25) $gridCols = 25;
$data = [
'code' => $code,
'name_ar' => $nameAr,
'name_en' => $nameEn ?: null,
......@@ -157,7 +162,14 @@ class FacilityController extends Controller
'operating_hours_json' => $operatingHoursJson,
'is_active' => 1,
'branch_id' => null,
]);
];
if ($gridRows > 0 && $gridCols > 0) {
$data['pool_grid_rows'] = $gridRows;
$data['pool_grid_cols'] = $gridCols;
}
Facility::create($data);
return $this->redirect('/sa/facilities')->withSuccess('تم إضافة المنشأة بنجاح');
}
......@@ -298,6 +310,11 @@ class FacilityController extends Controller
'slot_minutes' => $slotMinutes,
], JSON_UNESCAPED_UNICODE);
$gridRows = (int) $request->post('grid_rows', 0);
$gridCols = (int) $request->post('grid_cols', 0);
if ($gridRows > 25) $gridRows = 25;
if ($gridCols > 25) $gridCols = 25;
$facility->update([
'code' => $code,
'name_ar' => $nameAr,
......@@ -306,6 +323,8 @@ class FacilityController extends Controller
'discipline_id' => $disciplineId !== '' ? (int) $disciplineId : null,
'location_description' => $location ?: null,
'operating_hours_json' => $operatingHoursJson,
'pool_grid_rows' => $gridRows > 0 ? $gridRows : null,
'pool_grid_cols' => $gridCols > 0 ? $gridCols : null,
]);
return $this->redirect("/sa/facilities/{$id}")->withSuccess('تم تحديث المنشأة بنجاح');
......
......@@ -104,11 +104,11 @@ class MirrorController extends Controller
{
$db = App::getInstance()->db();
$facility = $db->selectOne(
"SELECT facility_type FROM sa_facilities WHERE id = ? AND is_active = 1 AND is_archived = 0",
"SELECT facility_type, pool_grid_rows, pool_grid_cols FROM sa_facilities WHERE id = ? AND is_active = 1 AND is_archived = 0",
[$facilityId]
);
if ($facility && $facility['facility_type'] === 'pool') {
if ($facility && ((int) ($facility['pool_grid_rows'] ?? 0) > 0 && (int) ($facility['pool_grid_cols'] ?? 0) > 0)) {
return $this->redirect('/sa/mirror/pool/' . $facilityId . '?date=' . $date);
}
......@@ -150,8 +150,8 @@ class MirrorController extends Controller
[]
);
$allPools = $db->select(
"SELECT id, name_ar FROM sa_facilities WHERE is_active = 1 AND is_archived = 0 AND facility_type = 'pool' ORDER BY name_ar",
$allGridFacilities = $db->select(
"SELECT id, name_ar FROM sa_facilities WHERE is_active = 1 AND is_archived = 0 AND pool_grid_rows > 0 AND pool_grid_cols > 0 ORDER BY name_ar",
[]
);
......@@ -160,7 +160,7 @@ class MirrorController extends Controller
'slots' => $slots,
'groups' => $groups,
'date' => $date,
'allPools' => $allPools,
'allPools' => $allGridFacilities,
]);
}
}
......@@ -18,6 +18,7 @@ class Facility extends Model
protected static array $fillable = [
'code', 'name_ar', 'name_en', 'facility_type', 'discipline_id',
'location_description', 'operating_hours_json', 'is_active', 'branch_id',
'pool_grid_rows', 'pool_grid_cols',
];
public static function getTypeOptions(): array
......
......@@ -83,7 +83,7 @@ final class MirrorStateService
}
}
if ($facility['facility_type'] === 'pool') {
if ((int) ($facility['pool_grid_rows'] ?? 0) > 0 && (int) ($facility['pool_grid_cols'] ?? 0) > 0) {
$poolZoneBookings = $db->select(
"SELECT pzb.start_time, pzb.end_time, pzb.status as pzb_status, pzb.assignment_type,
pzb.zone_row, pzb.zone_col, g.name_ar as group_name, c.full_name_ar as coach_name
......
......@@ -11,7 +11,7 @@ final class PoolGridService
{
$db = App::getInstance()->db();
return $db->selectOne(
"SELECT * FROM sa_facilities WHERE id = ? AND facility_type = 'pool' AND is_active = 1 AND is_archived = 0",
"SELECT * FROM sa_facilities WHERE id = ? AND is_active = 1 AND is_archived = 0",
[$facilityId]
);
}
......@@ -299,8 +299,8 @@ final class PoolGridService
{
$db = App::getInstance()->db();
if ($rows < 1 || $rows > 20 || $cols < 1 || $cols > 20) {
return ['success' => false, 'error' => 'الحد المسموح من 1 إلى 20 صفوف/أعمدة'];
if ($rows < 1 || $rows > 25 || $cols < 1 || $cols > 25) {
return ['success' => false, 'error' => 'الحد المسموح من 1 إلى 25 صفوف/أعمدة'];
}
$db->update('sa_facilities', [
......
......@@ -40,12 +40,12 @@ final class SlotAvailabilityService
return ['available' => false, 'reason' => 'المرفق محجوب في هذا التوقيت', 'remaining' => 0];
}
// Cross-check pool zone bookings for pool-type facilities
// Cross-check zone bookings for facilities with grid configured
$facilityType = $db->selectOne(
"SELECT facility_type FROM sa_facilities WHERE id = ?",
"SELECT facility_type, pool_grid_rows, pool_grid_cols FROM sa_facilities WHERE id = ?",
[(int) $unit['fac_id']]
);
if ($facilityType && $facilityType['facility_type'] === 'pool') {
if ($facilityType && (int) ($facilityType['pool_grid_rows'] ?? 0) > 0 && (int) ($facilityType['pool_grid_cols'] ?? 0) > 0) {
$poolZoneBlocked = (int) ($db->selectOne(
"SELECT COUNT(*) as cnt FROM sa_pool_zone_bookings
WHERE facility_id = ? AND booking_date = ? AND status = 'active'
......
......@@ -71,6 +71,21 @@
</div>
</div>
<div style="background:#F9FAFB;border:1px solid #E5E7EB;border-radius:8px;padding:15px;margin-bottom:20px;">
<h4 style="margin:0 0 12px;color:#374151;font-size:14px;">إعدادات الشبكة (اختياري)</h4>
<p style="font-size:12px;color:#6B7280;margin:0 0 12px;">تفعيل الشبكة يتيح إدارة المناطق بصرياً على خريطة المنشأة. اتركها فارغة لعدم التفعيل.</p>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div>
<label class="form-label">عدد الصفوف</label>
<input type="number" name="grid_rows" value="<?= e(old('grid_rows', '')) ?>" class="form-input" min="1" max="25" placeholder="مثال: 10">
</div>
<div>
<label class="form-label">عدد الأعمدة</label>
<input type="number" name="grid_cols" value="<?= e(old('grid_cols', '')) ?>" class="form-input" min="1" max="25" placeholder="مثال: 20">
</div>
</div>
</div>
<div style="display:flex;gap:10px;">
<button type="submit" class="btn btn-primary">حفظ المنشأة</button>
<a href="/sa/facilities" class="btn btn-outline">إلغاء</a>
......
......@@ -75,6 +75,21 @@ $opHours = $facility->getOperatingHours();
</div>
</div>
<div style="background:#F9FAFB;border:1px solid #E5E7EB;border-radius:8px;padding:15px;margin-bottom:20px;">
<h4 style="margin:0 0 12px;color:#374151;font-size:14px;">إعدادات الشبكة</h4>
<p style="font-size:12px;color:#6B7280;margin:0 0 12px;">تفعيل الشبكة يتيح إدارة المناطق بصرياً على خريطة المنشأة. اتركها فارغة أو 0 لإيقاف الشبكة.</p>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div>
<label class="form-label">عدد الصفوف</label>
<input type="number" name="grid_rows" value="<?= e(old('grid_rows', (string) ($facility->pool_grid_rows ?? ''))) ?>" class="form-input" min="0" max="25" placeholder="مثال: 10">
</div>
<div>
<label class="form-label">عدد الأعمدة</label>
<input type="number" name="grid_cols" value="<?= e(old('grid_cols', (string) ($facility->pool_grid_cols ?? ''))) ?>" class="form-input" min="0" max="25" placeholder="مثال: 20">
</div>
</div>
</div>
<div style="display:flex;gap:10px;">
<button type="submit" class="btn btn-primary">حفظ التعديلات</button>
<a href="/sa/facilities/<?= (int) $facility->id ?>" class="btn btn-outline">إلغاء</a>
......
......@@ -2,6 +2,9 @@
<?php $__template->section('title'); ?>المنشأة: <?= e($facility->name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<div style="display:flex;gap:8px;">
<?php if ((int) ($facility->pool_grid_rows ?? 0) > 0 && (int) ($facility->pool_grid_cols ?? 0) > 0): ?>
<a href="/sa/mirror/pool/<?= (int) $facility->id ?>" class="btn btn-primary"><i data-lucide="grid-3x3" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> الشبكة</a>
<?php endif; ?>
<?php if (can('sa.facility.manage')): ?>
<a href="/sa/facilities/<?= (int) $facility->id ?>/edit" class="btn btn-outline">تعديل</a>
<?php endif; ?>
......
......@@ -173,8 +173,8 @@ $gridCols = (int) ($facility['pool_grid_cols'] ?? 0);
<div class="pg-setup <?= ($gridRows > 0 && $gridCols > 0) ? 'hidden neutral' : '' ?>" id="pgSetup">
<i data-lucide="grid-3x3" style="width:18px;height:18px;"></i>
<span class="pg-size-current"><?= ($gridRows > 0) ? "الحالي: {$gridRows}×{$gridCols}" : 'لم يتم التهيئة' ?></span>
<label>صفوف <input type="number" id="pgSetupRows" min="1" max="20" value="<?= $gridRows ?: 4 ?>"></label>
<label>أعمدة <input type="number" id="pgSetupCols" min="1" max="20" value="<?= $gridCols ?: 6 ?>"></label>
<label>صفوف <input type="number" id="pgSetupRows" min="1" max="25" value="<?= $gridRows ?: 4 ?>"></label>
<label>أعمدة <input type="number" id="pgSetupCols" min="1" max="25" value="<?= $gridCols ?: 6 ?>"></label>
<button id="pgSetupSave">حفظ</button>
</div>
......@@ -416,7 +416,7 @@ $gridCols = (int) ($facility['pool_grid_cols'] ?? 0);
document.getElementById('pgSetupSave').onclick = function(){
let r = parseInt(document.getElementById('pgSetupRows').value) || 0;
let c = parseInt(document.getElementById('pgSetupCols').value) || 0;
if(r < 1 || c < 1 || r > 20 || c > 20){ alert('الأبعاد يجب أن تكون بين 1 و 20'); return; }
if(r < 1 || c < 1 || r > 25 || c > 25){ alert('الأبعاد يجب أن تكون بين 1 و 25'); return; }
showLoading();
apiPost('/api/sa/pool-grid/' + FACILITY_ID + '/update-grid', {rows: r, cols: c})
.then(function(res){
......
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