Ver Fonte

feat(schedule): add monthly calendar view with week/month tabs

Alexander Musikhin há 3 semanas atrás
pai
commit
bf8af1073d

+ 72 - 0
app/Http/Controllers/ScheduleController.php

@@ -20,6 +20,7 @@ use App\Models\Role;
 use App\Models\Schedule;
 use App\Models\User;
 use Illuminate\Http\Request;
+use Illuminate\Support\Carbon;
 
 class ScheduleController extends Controller
 {
@@ -35,6 +36,9 @@ class ScheduleController extends Controller
         $this->data['areas'] = Area::query()->get()->pluck('name', 'id');
         $this->data['brigadiers'] = User::query()->where('role', Role::BRIGADIER)->get()->pluck('name', 'id');
 
+        $tab = $request->get('tab', 'week');
+        $this->data['activeTab'] = in_array($tab, ['week', 'month'], true) ? $tab : 'week';
+
         $this->data['weekNumber'] = $request->get('week' ,date('W'));
         $weekDates = [
             'mon' => DateHelper::getDateOfWeek(year(), $this->data['weekNumber']),
@@ -54,12 +58,80 @@ class ScheduleController extends Controller
         }
         $result = Schedule::query()
             ->whereBetween('installation_date', [$weekDates['mon'], $weekDates['sun']])
+            ->with(['brigadier', 'district', 'area'])
             ->get();
         foreach ($result as $schedule) {
             $schedules[$schedule->installation_date][] = $schedule;
         }
 
         $this->data['schedules'] = $schedules;
+
+        $monthParam = $request->get('month');
+        $monthNumber = (int)date('m');
+        if (is_string($monthParam)) {
+            if (preg_match('/^(0[1-9]|1[0-2])$/', $monthParam)) {
+                $monthNumber = (int)$monthParam;
+            } elseif (preg_match('/^(\\d{4})-(0[1-9]|1[0-2])$/', $monthParam, $matches)) {
+                $monthNumber = (int)$matches[2];
+            }
+        }
+
+        $monthStart = Carbon::createFromDate(year(), $monthNumber, 1)->startOfMonth();
+        $monthEnd = $monthStart->copy()->endOfMonth();
+
+        $gridStart = $monthStart->copy()->startOfWeek(Carbon::MONDAY);
+        $gridEnd = $monthEnd->copy()->endOfWeek(Carbon::SUNDAY);
+        $cursor = $gridStart->copy();
+        $monthDays = [];
+        while ($cursor->lte($gridEnd)) {
+            $monthDays[] = [
+                'date' => $cursor->toDateString(),
+                'day' => (int)$cursor->format('j'),
+                'week' => (int)$cursor->isoWeek(),
+                'inMonth' => $cursor->month === $monthStart->month,
+                'isToday' => $cursor->isToday(),
+            ];
+            $cursor->addDay();
+        }
+
+        $monthSchedules = Schedule::query()
+            ->whereBetween('installation_date', [$monthStart->toDateString(), $monthEnd->toDateString()])
+            ->with(['brigadier'])
+            ->get();
+
+        $monthScheduleColors = [];
+        $monthBrigadierLegend = [];
+        foreach ($monthSchedules as $schedule) {
+            $date = $schedule->installation_date;
+            $brigadierId = $schedule->brigadier_id ?? 0;
+            if (!isset($monthScheduleColors[$date])) {
+                $monthScheduleColors[$date] = [];
+            }
+            if (!isset($monthScheduleColors[$date][$brigadierId])) {
+                $monthScheduleColors[$date][$brigadierId] = [
+                    'color' => $schedule->brigadier?->color ?? '#cccccc',
+                    'name' => $schedule->brigadier?->name ?? '',
+                ];
+            }
+            if (!isset($monthBrigadierLegend[$brigadierId])) {
+                $monthBrigadierLegend[$brigadierId] = [
+                    'color' => $schedule->brigadier?->color ?? '#cccccc',
+                    'name' => $schedule->brigadier?->name ?? '',
+                ];
+            }
+        }
+        foreach ($monthScheduleColors as $date => $colors) {
+            $monthScheduleColors[$date] = array_values($colors);
+        }
+
+        $this->data['monthGrid'] = array_chunk($monthDays, 7);
+        $this->data['monthLabel'] = $monthStart->isoFormat('MMMM YYYY');
+        $this->data['monthNumber'] = $monthNumber;
+        $this->data['monthPrev'] = $monthNumber > 1 ? str_pad((string)($monthNumber - 1), 2, '0', STR_PAD_LEFT) : null;
+        $this->data['monthNext'] = $monthNumber < 12 ? str_pad((string)($monthNumber + 1), 2, '0', STR_PAD_LEFT) : null;
+        $this->data['monthScheduleColors'] = $monthScheduleColors;
+        $this->data['monthBrigadierLegend'] = array_values($monthBrigadierLegend);
+
         return view('schedule.index', $this->data);
     }
 

+ 200 - 44
resources/views/schedule/index.blade.php

@@ -1,60 +1,79 @@
 @extends('layouts.app')
 
 @section('content')
-    <div class="p-0 m-0 table-responsive">
-        <div class="row mb-3">
+    <div class="p-0 m-0">
+        <div class="row mb-3 align-items-center">
             <div class="col-md-6">
                 <h3>График монтажей</h3>
             </div>
             <div class="col-md-6 text-end">
-                <div class="d-flex flex-row justify-content-end">
-                    <div class="p-2">
-                        <button @disabled($weekNumber == 1) class="btn btn-sm btn-primary"
-                                onclick="document.location = '{{ route('schedule.index', ['week' => $weekNumber - 1]) }}'">
-                            <i class="bi bi-arrow-left"></i>
-                        </button>
-                    </div>
-                    <label class="p-2 d-none d-mb-block small mt-1" for="fromDate">Неделя №</label>
-                    <div class="p-2">
-                        <input type="number" value="{{ $weekNumber }}"
-                               class="form-control form-control-sm week-number-input" name="weekNumber"
-                               onchange="document.location = '{{ route('schedule.index') }}?week='+this.value"
-                               min="1" max="53" title="№ недели">
-                    </div>
+                <ul class="nav nav-tabs justify-content-end">
+                    <li class="nav-item">
+                        <a class="nav-link @if($activeTab === 'week') active @endif"
+                           href="{{ route('schedule.index', ['week' => $weekNumber, 'tab' => 'week']) }}">Неделя</a>
+                    </li>
+                    <li class="nav-item">
+                        <a class="nav-link @if($activeTab === 'month') active @endif"
+                           href="{{ route('schedule.index', ['month' => str_pad((string)$monthNumber, 2, '0', STR_PAD_LEFT), 'tab' => 'month']) }}">Месяц</a>
+                    </li>
+                </ul>
+            </div>
+        </div>
 
-                    <div class="p-2">
-                        <input type="date" min="{{ year() . '-01-01' }}" max="{{ year() . '-12-31' }}"
-                               class="form-control form-control-sm" value="{{ $weekDates['mon'] }}"
-                               title="начало недели" name="monday" id="fromDate"
-                               onchange="document.location = '{{ route('schedule.index') }}?week=' + getWeekNumber(this.value)">
-                    </div>
-                    <div class="p-2 d-none d-md-block">
-                        <input type="date" disabled name="sunday"
-                               class="form-control form-control-sm" value="{{ $weekDates['sun'] }}"
-                               title="конец недели">
-                    </div>
+        @if($activeTab === 'week')
+            <div class="row mb-3">
+                <div class="col-md-12 text-end">
+                    <div class="d-flex flex-row justify-content-end">
+                        <div class="p-2">
+                            <button @disabled($weekNumber == 1) class="btn btn-sm btn-primary"
+                                    onclick="document.location = '{{ route('schedule.index', ['week' => $weekNumber - 1, 'tab' => 'week']) }}'">
+                                <i class="bi bi-arrow-left"></i>
+                            </button>
+                        </div>
+                        <label class="p-2 d-none d-mb-block small mt-1" for="fromDate">Неделя №</label>
+                        <div class="p-2">
+                            <input type="number" value="{{ $weekNumber }}"
+                                   class="form-control form-control-sm week-number-input" name="weekNumber"
+                                   onchange="document.location = '{{ route('schedule.index') }}?tab=week&week='+this.value"
+                                   min="1" max="53" title="№ недели">
+                        </div>
 
-                    <div class="p-2">
-                        <button @disabled($weekNumber > 52) class="btn btn-sm btn-primary"
-                                onclick="document.location = '{{ route('schedule.index', ['week' => $weekNumber + 1]) }}'">
-                            <i class="bi bi-arrow-right"></i>
-                        </button>
-                    </div>
-                    @if(hasRole('admin'))
-                    <div class="p-2 ms-3">
-                        <form action="{{ route('schedule.export') }}" method="post">
-                            @csrf
-                            <input type="hidden" name="start_date" value="{{ $weekDates['mon'] }}">
-                            <input type="hidden" name="end_date" value="{{ $weekDates['sun'] }}">
-                            <input type="hidden" name="week" value="{{ $weekNumber }}">
-                            <button type="submit" class="btn btn-sm btn-primary" id="exportScheduleButton">Экспорт</button>
-                        </form>
+                        <div class="p-2">
+                            <input type="date" min="{{ year() . '-01-01' }}" max="{{ year() . '-12-31' }}"
+                                   class="form-control form-control-sm" value="{{ $weekDates['mon'] }}"
+                                   title="начало недели" name="monday" id="fromDate"
+                                   onchange="document.location = '{{ route('schedule.index') }}?tab=week&week=' + getWeekNumber(this.value)">
+                        </div>
+                        <div class="p-2 d-none d-md-block">
+                            <input type="date" disabled name="sunday"
+                                   class="form-control form-control-sm" value="{{ $weekDates['sun'] }}"
+                                   title="конец недели">
+                        </div>
+
+                        <div class="p-2">
+                            <button @disabled($weekNumber > 52) class="btn btn-sm btn-primary"
+                                    onclick="document.location = '{{ route('schedule.index', ['week' => $weekNumber + 1, 'tab' => 'week']) }}'">
+                                <i class="bi bi-arrow-right"></i>
+                            </button>
+                        </div>
+                        @if(hasRole('admin'))
+                        <div class="p-2 ms-3">
+                            <form action="{{ route('schedule.export') }}" method="post">
+                                @csrf
+                                <input type="hidden" name="start_date" value="{{ $weekDates['mon'] }}">
+                                <input type="hidden" name="end_date" value="{{ $weekDates['sun'] }}">
+                                <input type="hidden" name="week" value="{{ $weekNumber }}">
+                                <button type="submit" class="btn btn-sm btn-primary" id="exportScheduleButton">Экспорт</button>
+                            </form>
+                        </div>
+                        @endif
                     </div>
-                    @endif
                 </div>
             </div>
-        </div>
+        @endif
 
+        @if($activeTab === 'week')
+        <div class="table-responsive">
         <table class="table">
             <thead>
             <tr>
@@ -141,7 +160,138 @@
 
             </tbody>
         </table>
+        </div>
+        @endif
+
+        @if($activeTab === 'month')
+            <div class="d-flex justify-content-between align-items-center mb-3">
+                <div class="d-flex align-items-center">
+                    <button class="btn btn-sm btn-primary me-2" @disabled(!$monthPrev)
+                            onclick="document.location = '{{ route('schedule.index', ['tab' => 'month', 'month' => $monthPrev ?? str_pad((string)$monthNumber, 2, '0', STR_PAD_LEFT)]) }}'">
+                        <i class="bi bi-arrow-left"></i>
+                    </button>
+                    <div class="h5 mb-0 text-capitalize">{{ $monthLabel }}</div>
+                    <button class="btn btn-sm btn-primary ms-2" @disabled(!$monthNext)
+                            onclick="document.location = '{{ route('schedule.index', ['tab' => 'month', 'month' => $monthNext ?? str_pad((string)$monthNumber, 2, '0', STR_PAD_LEFT)]) }}'">
+                        <i class="bi bi-arrow-right"></i>
+                    </button>
+                </div>
+                <div class="d-flex align-items-center">
+                    <label for="monthSelect" class="me-2 small">Месяц</label>
+                    <input id="monthSelect" type="month" class="form-control form-control-sm"
+                           min="{{ year() . '-01' }}" max="{{ year() . '-12' }}"
+                           value="{{ year() . '-' . str_pad((string)$monthNumber, 2, '0', STR_PAD_LEFT) }}"
+                           onchange="document.location='{{ route('schedule.index') }}?tab=month&month=' + this.value.substring(5,7)">
+                </div>
+            </div>
 
+            <div class="table-responsive">
+                <table class="table table-bordered schedule-month">
+                    <thead>
+                    <tr>
+                        <th class="text-center">Пн</th>
+                        <th class="text-center">Вт</th>
+                        <th class="text-center">Ср</th>
+                        <th class="text-center">Чт</th>
+                        <th class="text-center">Пт</th>
+                        <th class="text-center">Сб</th>
+                        <th class="text-center">Вс</th>
+                    </tr>
+                    </thead>
+                    <tbody>
+                    @foreach($monthGrid as $week)
+                        <tr>
+                            @foreach($week as $day)
+                                <td class="schedule-day @if(!$day['inMonth']) schedule-outside @endif @if($day['isToday']) schedule-today @endif"
+                                    data-week="{{ $day['week'] }}">
+                                    <a class="schedule-day-link"
+                                       href="{{ route('schedule.index', ['week' => $day['week'], 'tab' => 'week']) }}"
+                                       title="Открыть неделю №{{ $day['week'] }}">
+                                        {{ $day['day'] }}
+                                    </a>
+                                    @if(isset($monthScheduleColors[$day['date']]))
+                                        <div class="schedule-dots">
+                                            @foreach($monthScheduleColors[$day['date']] as $brigadier)
+                                                <span class="schedule-dot" style="background: {{ $brigadier['color'] }}"
+                                                      title="{{ $brigadier['name'] }}"></span>
+                                            @endforeach
+                                        </div>
+                                    @endif
+                                </td>
+                            @endforeach
+                        </tr>
+                    @endforeach
+                    </tbody>
+                </table>
+            </div>
+
+            @if(!empty($monthBrigadierLegend))
+                <div class="schedule-legend mt-3">
+                    <div class="small text-muted mb-2">Бригадиры</div>
+                    <div class="schedule-legend-items">
+                        @foreach($monthBrigadierLegend as $brigadier)
+                            <span class="schedule-legend-item">
+                                <span class="schedule-dot" style="background: {{ $brigadier['color'] }}"></span>
+                                <span class="schedule-legend-name">{{ $brigadier['name'] }}</span>
+                            </span>
+                        @endforeach
+                    </div>
+                </div>
+            @endif
+        @endif
+
+        <style>
+            .schedule-month {
+                table-layout: fixed;
+            }
+            .schedule-day {
+                height: 110px;
+                vertical-align: top;
+                position: relative;
+            }
+            .schedule-day-link {
+                display: inline-block;
+                font-weight: 600;
+                text-decoration: none;
+                color: #0d6efd;
+            }
+            .schedule-dots {
+                margin-top: 6px;
+                display: flex;
+                flex-wrap: wrap;
+                gap: 4px;
+            }
+            .schedule-dot {
+                width: 12px;
+                height: 12px;
+                border-radius: 50%;
+                display: inline-block;
+                border: 1px solid rgba(0, 0, 0, 0.1);
+            }
+            .schedule-outside {
+                background: #f8f9fa;
+                color: #9aa0a6;
+            }
+            .schedule-today {
+                outline: 2px solid rgba(13, 110, 253, 0.5);
+                outline-offset: -2px;
+            }
+            .schedule-legend-items {
+                display: flex;
+                flex-wrap: wrap;
+                gap: 10px 16px;
+                align-items: center;
+            }
+            .schedule-legend-item {
+                display: inline-flex;
+                align-items: center;
+                gap: 6px;
+                font-size: 0.9rem;
+            }
+            .schedule-legend-name {
+                white-space: nowrap;
+            }
+        </style>
 
         <!-- Модальное окно редактирования графика -->
         <div class="modal fade" id="copySchedule" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
@@ -184,6 +334,12 @@
 
 @push('scripts')
     <script type="module">
+        $('.schedule-day').on('dblclick', function () {
+            let week = $(this).attr('data-week');
+            if (!week) return;
+            document.location = '{{ route('schedule.index') }}?tab=week&week=' + week;
+        });
+
         $('.editSchedule').on('click', function () {
             let scheduleId = $(this).attr('data-schedule-id');
             let scheduleDate = $(this).attr('data-schedule-date');