diff --git a/app/Http/Controllers/DashboardCalendarController.php b/app/Http/Controllers/DashboardCalendarController.php new file mode 100644 index 00000000..93ac94a9 --- /dev/null +++ b/app/Http/Controllers/DashboardCalendarController.php @@ -0,0 +1,171 @@ +input('year', now()->year); + $month = (int) $request->input('month', now()->month); + $tenantId = session('selected_tenant_id', 1); + + $firstDay = Carbon::create($year, $month, 1); + $lastDay = $firstDay->copy()->endOfMonth(); + $startOfWeek = $firstDay->copy()->startOfWeek(Carbon::SUNDAY); + $endOfWeek = $lastDay->copy()->endOfWeek(Carbon::SATURDAY); + + $calendarData = Schedule::forTenant($tenantId) + ->active() + ->betweenDates($startOfWeek->toDateString(), $endOfWeek->toDateString()) + ->orderBy('start_date') + ->orderBy('start_time') + ->get() + ->groupBy(fn ($s) => $s->start_date->format('Y-m-d')); + + $holidayMap = $this->getHolidayMap($tenantId, $year, $month); + + return view('dashboard.partials.calendar', compact( + 'year', 'month', 'calendarData', 'holidayMap' + )); + } + + /** + * 일정 등록 + */ + public function store(Request $request): JsonResponse + { + $validated = $request->validate([ + 'title' => 'required|string|max:255', + 'description' => 'nullable|string|max:1000', + 'start_date' => 'required|date', + 'end_date' => 'nullable|date|after_or_equal:start_date', + 'start_time' => 'nullable|date_format:H:i', + 'end_time' => 'nullable|date_format:H:i', + 'is_all_day' => 'boolean', + 'type' => 'required|in:event,meeting,notice,other', + 'color' => 'nullable|string|max:20', + ]); + + $tenantId = session('selected_tenant_id', 1); + + $validated['tenant_id'] = $tenantId; + $validated['created_by'] = auth()->id(); + $validated['is_all_day'] = $validated['is_all_day'] ?? true; + + if (empty($validated['end_date'])) { + $validated['end_date'] = $validated['start_date']; + } + + $schedule = Schedule::create($validated); + + return response()->json([ + 'success' => true, + 'message' => '일정이 등록되었습니다.', + 'data' => $schedule, + ]); + } + + /** + * 일정 상세 (JSON) + */ + public function show(int $id): JsonResponse + { + $tenantId = session('selected_tenant_id', 1); + + $schedule = Schedule::forTenant($tenantId)->findOrFail($id); + + return response()->json([ + 'success' => true, + 'data' => $schedule, + ]); + } + + /** + * 일정 수정 + */ + public function update(Request $request, int $id): JsonResponse + { + $validated = $request->validate([ + 'title' => 'required|string|max:255', + 'description' => 'nullable|string|max:1000', + 'start_date' => 'required|date', + 'end_date' => 'nullable|date|after_or_equal:start_date', + 'start_time' => 'nullable|date_format:H:i', + 'end_time' => 'nullable|date_format:H:i', + 'is_all_day' => 'boolean', + 'type' => 'required|in:event,meeting,notice,other', + 'color' => 'nullable|string|max:20', + ]); + + $tenantId = session('selected_tenant_id', 1); + $schedule = Schedule::forTenant($tenantId)->findOrFail($id); + + $validated['updated_by'] = auth()->id(); + $validated['is_all_day'] = $validated['is_all_day'] ?? true; + + if (empty($validated['end_date'])) { + $validated['end_date'] = $validated['start_date']; + } + + $schedule->update($validated); + + return response()->json([ + 'success' => true, + 'message' => '일정이 수정되었습니다.', + 'data' => $schedule->fresh(), + ]); + } + + /** + * 일정 삭제 + */ + public function destroy(int $id): JsonResponse + { + $tenantId = session('selected_tenant_id', 1); + $schedule = Schedule::forTenant($tenantId)->findOrFail($id); + + $schedule->update(['deleted_by' => auth()->id()]); + $schedule->delete(); + + return response()->json([ + 'success' => true, + 'message' => '일정이 삭제되었습니다.', + ]); + } + + /** + * 해당 월의 휴일 맵 생성 (날짜 => 휴일명) + */ + private function getHolidayMap(int $tenantId, int $year, int $month): array + { + $startOfMonth = Carbon::create($year, $month, 1)->startOfWeek(Carbon::SUNDAY); + $endOfMonth = Carbon::create($year, $month, 1)->endOfMonth()->endOfWeek(Carbon::SATURDAY); + + $holidays = Holiday::forTenant($tenantId) + ->where('start_date', '<=', $endOfMonth->toDateString()) + ->where('end_date', '>=', $startOfMonth->toDateString()) + ->get(); + + $map = []; + foreach ($holidays as $holiday) { + $start = $holiday->start_date->copy(); + $end = $holiday->end_date->copy(); + for ($d = $start; $d->lte($end); $d->addDay()) { + $map[$d->format('Y-m-d')] = $holiday->name; + } + } + + return $map; + } +} diff --git a/app/Models/System/Schedule.php b/app/Models/System/Schedule.php new file mode 100644 index 00000000..faa0588f --- /dev/null +++ b/app/Models/System/Schedule.php @@ -0,0 +1,101 @@ + 'date', + 'end_date' => 'date', + 'is_all_day' => 'boolean', + 'is_recurring' => 'boolean', + 'is_active' => 'boolean', + ]; + + protected $attributes = [ + 'is_all_day' => true, + 'type' => 'event', + 'is_recurring' => false, + 'is_active' => true, + ]; + + public const TYPE_EVENT = 'event'; + public const TYPE_MEETING = 'meeting'; + public const TYPE_NOTICE = 'notice'; + public const TYPE_OTHER = 'other'; + + public const TYPES = [ + self::TYPE_EVENT => '일정', + self::TYPE_MEETING => '회의', + self::TYPE_NOTICE => '공지', + self::TYPE_OTHER => '기타', + ]; + + public const TYPE_COLORS = [ + self::TYPE_EVENT => ['bg' => 'bg-emerald-50', 'text' => 'text-emerald-700', 'border' => 'border-emerald-200'], + self::TYPE_MEETING => ['bg' => 'bg-blue-50', 'text' => 'text-blue-700', 'border' => 'border-blue-200'], + self::TYPE_NOTICE => ['bg' => 'bg-amber-50', 'text' => 'text-amber-700', 'border' => 'border-amber-200'], + self::TYPE_OTHER => ['bg' => 'bg-gray-50', 'text' => 'text-gray-700', 'border' => 'border-gray-200'], + ]; + + public function scopeForTenant(Builder $query, int $tenantId): Builder + { + return $query->where(function ($q) use ($tenantId) { + $q->where('tenant_id', $tenantId) + ->orWhereNull('tenant_id'); + }); + } + + public function scopeActive(Builder $query): Builder + { + return $query->where('is_active', true); + } + + public function scopeBetweenDates(Builder $query, string $startDate, string $endDate): Builder + { + return $query->where(function ($q) use ($startDate, $endDate) { + $q->where('start_date', '<=', $endDate) + ->where(function ($inner) use ($startDate) { + $inner->where('end_date', '>=', $startDate) + ->orWhereNull('end_date'); + }); + }); + } + + public function getTypeLabelAttribute(): string + { + return self::TYPES[$this->type] ?? $this->type; + } + + public function getTypeColorsAttribute(): array + { + return self::TYPE_COLORS[$this->type] ?? self::TYPE_COLORS[self::TYPE_OTHER]; + } +} diff --git a/resources/views/dashboard/index.blade.php b/resources/views/dashboard/index.blade.php index 079eaa3b..8ec7eac9 100644 --- a/resources/views/dashboard/index.blade.php +++ b/resources/views/dashboard/index.blade.php @@ -30,7 +30,7 @@ - +
| 일 | +월 | +화 | +수 | +목 | +금 | +토 | +
|---|---|---|---|---|---|---|
|
+
+ {{-- 날짜 헤더 --}}
+
+
+
+
+ {{-- 일정 목록 --}}
+
+
+ {{ $currentDate->day }}
+
+ @if($isHoliday && $isCurrentMonth)
+ {{ $holidayName }}
+ @endif
+
+
+ @if($isCurrentMonth)
+
+ @endif
+
+ @foreach($daySchedules as $schedule)
+ @php
+ $colors = $schedule->type_colors;
+ @endphp
+
+ @endforeach
+
+ |
+
+ @php
+ $currentDate->addDay();
+ @endphp
+ @endfor
+