diff --git a/app/Http/Controllers/Finance/VehicleLogController.php b/app/Http/Controllers/Finance/VehicleLogController.php new file mode 100644 index 00000000..e32970dc --- /dev/null +++ b/app/Http/Controllers/Finance/VehicleLogController.php @@ -0,0 +1,265 @@ +header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('finance.vehicle-logs')); + } + + $tenantId = session('tenant_id', 1); + $vehicles = CorporateVehicle::where('tenant_id', $tenantId) + ->where('status', 'active') + ->orderBy('plate_number') + ->get(); + + return view('finance.vehicle-logs', [ + 'vehicles' => $vehicles, + 'tripTypes' => VehicleLog::tripTypeLabels(), + 'locationTypes' => VehicleLog::locationTypeLabels(), + ]); + } + + public function list(Request $request): JsonResponse + { + $tenantId = session('tenant_id', 1); + + $request->validate([ + 'vehicle_id' => 'required|integer', + 'start_date' => 'required|date', + 'end_date' => 'required|date|after_or_equal:start_date', + ]); + + $vehicleId = $request->vehicle_id; + $startDate = $request->start_date; + $endDate = $request->end_date; + + // 차량 정보 + $vehicle = CorporateVehicle::where('tenant_id', $tenantId) + ->findOrFail($vehicleId); + + // 전체 운행기록 수 (해당 차량) + $totalCount = VehicleLog::where('tenant_id', $tenantId) + ->where('vehicle_id', $vehicleId) + ->count(); + + $logs = VehicleLog::where('tenant_id', $tenantId) + ->where('vehicle_id', $vehicleId) + ->whereBetween('log_date', [$startDate, $endDate]) + ->orderBy('log_date', 'desc') + ->orderBy('id', 'desc') + ->get(); + + // 월별 합계 + $totals = [ + 'business_km' => $logs->whereIn('trip_type', ['commute_to', 'commute_from', 'business', 'commute_round', 'business_round'])->sum('distance_km'), + 'personal_km' => $logs->whereIn('trip_type', ['personal', 'personal_round'])->sum('distance_km'), + 'total_km' => $logs->sum('distance_km'), + ]; + + return response()->json([ + 'success' => true, + 'data' => [ + 'vehicle' => $vehicle, + 'logs' => $logs, + 'totals' => $totals, + 'totalCount' => $totalCount, + ], + ]); + } + + public function store(Request $request): JsonResponse + { + $tenantId = session('tenant_id', 1); + + $request->validate([ + 'vehicle_id' => 'required|integer|exists:corporate_vehicles,id', + 'log_date' => 'required|date', + 'driver_name' => 'required|string|max:50', + 'trip_type' => 'required|in:commute_to,commute_from,business,personal,commute_round,business_round,personal_round', + 'distance_km' => 'required|integer|min:0', + ]); + + // 해당 차량이 현재 테넌트의 것인지 확인 + CorporateVehicle::where('tenant_id', $tenantId) + ->findOrFail($request->vehicle_id); + + $log = VehicleLog::create([ + 'tenant_id' => $tenantId, + 'vehicle_id' => $request->vehicle_id, + 'log_date' => $request->log_date, + 'department' => $request->department, + 'driver_name' => $request->driver_name, + 'trip_type' => $request->trip_type, + 'departure_type' => $request->departure_type, + 'departure_name' => $request->departure_name, + 'departure_address' => $request->departure_address, + 'arrival_type' => $request->arrival_type, + 'arrival_name' => $request->arrival_name, + 'arrival_address' => $request->arrival_address, + 'distance_km' => $request->distance_km, + 'note' => $request->note, + ]); + + return response()->json([ + 'success' => true, + 'message' => '운행기록이 등록되었습니다.', + 'data' => $log, + ]); + } + + public function update(Request $request, int $id): JsonResponse + { + $tenantId = session('tenant_id', 1); + + $log = VehicleLog::where('tenant_id', $tenantId)->findOrFail($id); + + $request->validate([ + 'log_date' => 'required|date', + 'driver_name' => 'required|string|max:50', + 'trip_type' => 'required|in:commute_to,commute_from,business,personal,commute_round,business_round,personal_round', + 'distance_km' => 'required|integer|min:0', + ]); + + $log->update([ + 'log_date' => $request->log_date, + 'department' => $request->department, + 'driver_name' => $request->driver_name, + 'trip_type' => $request->trip_type, + 'departure_type' => $request->departure_type, + 'departure_name' => $request->departure_name, + 'departure_address' => $request->departure_address, + 'arrival_type' => $request->arrival_type, + 'arrival_name' => $request->arrival_name, + 'arrival_address' => $request->arrival_address, + 'distance_km' => $request->distance_km, + 'note' => $request->note, + ]); + + return response()->json([ + 'success' => true, + 'message' => '운행기록이 수정되었습니다.', + 'data' => $log, + ]); + } + + public function destroy(int $id): JsonResponse + { + $tenantId = session('tenant_id', 1); + + $log = VehicleLog::where('tenant_id', $tenantId)->findOrFail($id); + $log->delete(); + + return response()->json([ + 'success' => true, + 'message' => '운행기록이 삭제되었습니다.', + ]); + } + + public function export(Request $request): StreamedResponse + { + $tenantId = session('tenant_id', 1); + + $request->validate([ + 'vehicle_id' => 'required|integer', + 'start_date' => 'required|date', + 'end_date' => 'required|date|after_or_equal:start_date', + ]); + + $vehicleId = $request->vehicle_id; + $startDate = $request->start_date; + $endDate = $request->end_date; + + $vehicle = CorporateVehicle::where('tenant_id', $tenantId) + ->findOrFail($vehicleId); + + $logs = VehicleLog::where('tenant_id', $tenantId) + ->where('vehicle_id', $vehicleId) + ->whereBetween('log_date', [$startDate, $endDate]) + ->orderBy('log_date') + ->orderBy('id') + ->get(); + + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setTitle('운행기록부'); + + // 기본 정보 + $sheet->setCellValue('A1', '업무용승용차 운행기록부'); + $sheet->setCellValue('A3', '차량번호'); + $sheet->setCellValue('B3', $vehicle->plate_number); + $sheet->setCellValue('C3', '차종'); + $sheet->setCellValue('D3', $vehicle->model); + $sheet->setCellValue('E3', '구분'); + $sheet->setCellValue('F3', $this->getOwnershipTypeLabel($vehicle->ownership_type)); + $sheet->setCellValue('A4', '조회기간'); + $sheet->setCellValue('B4', sprintf('%s ~ %s', $startDate, $endDate)); + + // 헤더 + $headers = ['일자', '부서', '성명', '구분', '출발지', '도착지', '주행km', '비고']; + $col = 'A'; + foreach ($headers as $header) { + $sheet->setCellValue($col . '6', $header); + $col++; + } + + // 데이터 + $row = 7; + $tripTypeLabels = VehicleLog::tripTypeLabels(); + + foreach ($logs as $log) { + $sheet->setCellValue('A' . $row, $log->log_date->format('Y-m-d')); + $sheet->setCellValue('B' . $row, $log->department ?? ''); + $sheet->setCellValue('C' . $row, $log->driver_name); + $sheet->setCellValue('D' . $row, $tripTypeLabels[$log->trip_type] ?? $log->trip_type); + $sheet->setCellValue('E' . $row, $log->departure_name ?? ''); + $sheet->setCellValue('F' . $row, $log->arrival_name ?? ''); + $sheet->setCellValue('G' . $row, $log->distance_km); + $sheet->setCellValue('H' . $row, $log->note ?? ''); + $row++; + } + + // 합계 + $businessKm = $logs->whereIn('trip_type', ['commute_to', 'commute_from', 'business', 'commute_round', 'business_round'])->sum('distance_km'); + $personalKm = $logs->whereIn('trip_type', ['personal', 'personal_round'])->sum('distance_km'); + $totalKm = $logs->sum('distance_km'); + + $sheet->setCellValue('A' . $row, '합계'); + $sheet->setCellValue('F' . $row, '업무용: ' . number_format($businessKm) . 'km'); + $sheet->setCellValue('G' . $row, number_format($totalKm)); + $sheet->setCellValue('H' . $row, '비업무: ' . number_format($personalKm) . 'km'); + + $filename = sprintf('운행기록부_%s_%s_%s.xlsx', $vehicle->plate_number, $startDate, $endDate); + + return response()->streamDownload(function () use ($spreadsheet) { + $writer = new Xlsx($spreadsheet); + $writer->save('php://output'); + }, $filename, [ + 'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + ]); + } + + private function getOwnershipTypeLabel(string $type): string + { + return match ($type) { + 'corporate' => '회사', + 'rent' => '렌트', + 'lease' => '리스', + default => $type, + }; + } +} diff --git a/app/Http/Requests/UpdateProfileRequest.php b/app/Http/Requests/UpdateProfileRequest.php index 87feec04..7895a229 100644 --- a/app/Http/Requests/UpdateProfileRequest.php +++ b/app/Http/Requests/UpdateProfileRequest.php @@ -21,8 +21,13 @@ public function authorize(): bool */ public function rules(): array { + // 최고관리자만 이름 수정 가능 + $nameRule = auth()->user()->isSuperAdmin() + ? 'required|string|max:100' + : 'nullable'; + return [ - 'name' => 'required|string|max:100', + 'name' => $nameRule, 'phone' => 'nullable|string|max:20', ]; } diff --git a/app/Models/VehicleLog.php b/app/Models/VehicleLog.php new file mode 100644 index 00000000..ded7a901 --- /dev/null +++ b/app/Models/VehicleLog.php @@ -0,0 +1,92 @@ + 'date:Y-m-d', + 'distance_km' => 'integer', + ]; + + // trip_type 상수 + public const TRIP_TYPE_COMMUTE_TO = 'commute_to'; + public const TRIP_TYPE_COMMUTE_FROM = 'commute_from'; + public const TRIP_TYPE_BUSINESS = 'business'; + public const TRIP_TYPE_PERSONAL = 'personal'; + public const TRIP_TYPE_COMMUTE_ROUND = 'commute_round'; + public const TRIP_TYPE_BUSINESS_ROUND = 'business_round'; + public const TRIP_TYPE_PERSONAL_ROUND = 'personal_round'; + + // location_type 상수 + public const LOCATION_TYPE_HOME = 'home'; + public const LOCATION_TYPE_OFFICE = 'office'; + public const LOCATION_TYPE_CLIENT = 'client'; + public const LOCATION_TYPE_OTHER = 'other'; + + public static function tripTypeLabels(): array + { + return [ + self::TRIP_TYPE_COMMUTE_TO => '출근용', + self::TRIP_TYPE_COMMUTE_FROM => '퇴근용', + self::TRIP_TYPE_BUSINESS => '업무용', + self::TRIP_TYPE_PERSONAL => '비업무용(개인)', + self::TRIP_TYPE_COMMUTE_ROUND => '출퇴근용(왕복)', + self::TRIP_TYPE_BUSINESS_ROUND => '업무용(왕복)', + self::TRIP_TYPE_PERSONAL_ROUND => '비업무용(왕복)', + ]; + } + + public static function locationTypeLabels(): array + { + return [ + self::LOCATION_TYPE_HOME => '자택', + self::LOCATION_TYPE_OFFICE => '회사', + self::LOCATION_TYPE_CLIENT => '거래처', + self::LOCATION_TYPE_OTHER => '기타', + ]; + } + + public function vehicle(): BelongsTo + { + return $this->belongsTo(CorporateVehicle::class, 'vehicle_id'); + } + + public function getTripTypeLabelAttribute(): string + { + return self::tripTypeLabels()[$this->trip_type] ?? $this->trip_type; + } + + public function getDepartureTypeLabelAttribute(): string + { + return self::locationTypeLabels()[$this->departure_type] ?? ($this->departure_type ?? ''); + } + + public function getArrivalTypeLabelAttribute(): string + { + return self::locationTypeLabels()[$this->arrival_type] ?? ($this->arrival_type ?? ''); + } +} diff --git a/app/Services/ProfileService.php b/app/Services/ProfileService.php index 27b046dc..ade5056f 100644 --- a/app/Services/ProfileService.php +++ b/app/Services/ProfileService.php @@ -9,10 +9,15 @@ class ProfileService { /** * 프로필 정보 수정 (이름, 전화번호) + * 이름은 최고관리자만 수정 가능 */ public function updateProfile(User $user, array $data): bool { - $user->name = $data['name']; + // 최고관리자만 이름 수정 가능 + if ($user->isSuperAdmin() && isset($data['name'])) { + $user->name = $data['name']; + } + $user->phone = $data['phone'] ?? null; $user->updated_by = $user->id; diff --git a/database/seeders/VehicleLogMenuSeeder.php b/database/seeders/VehicleLogMenuSeeder.php new file mode 100644 index 00000000..be81b80d --- /dev/null +++ b/database/seeders/VehicleLogMenuSeeder.php @@ -0,0 +1,110 @@ + 법인차량관리 하위에 차량일지 메뉴 추가 + */ +class VehicleLogMenuSeeder extends Seeder +{ + public function run(): void + { + $tenantId = 1; + + // 법인차량관리 메뉴 찾기 + $vehicleMenu = Menu::where('tenant_id', $tenantId) + ->where('name', '법인차량관리') + ->first(); + + if (!$vehicleMenu) { + $this->command->error('법인차량관리 메뉴를 찾을 수 없습니다.'); + return; + } + + // 차량일지 메뉴가 이미 있는지 확인 + $existingMenu = Menu::where('tenant_id', $tenantId) + ->where('name', '차량일지') + ->where('parent_id', $vehicleMenu->id) + ->first(); + + if ($existingMenu) { + $this->command->info('차량일지 메뉴가 이미 존재합니다.'); + return; + } + + // 법인차량관리가 그룹 메뉴인지 확인 + // 그룹 메뉴가 아니면 그룹으로 변경 + if ($vehicleMenu->url) { + // 기존 URL을 차량목록으로 변경 + $vehicleListMenu = Menu::create([ + 'tenant_id' => $tenantId, + 'parent_id' => $vehicleMenu->id, + 'name' => '차량목록', + 'url' => $vehicleMenu->url, + 'icon' => 'car', + 'sort_order' => 1, + 'is_active' => true, + ]); + $this->command->info("차량목록 하위 메뉴 생성: {$vehicleListMenu->url}"); + + // 법인차량관리를 그룹 메뉴로 변경 + $vehicleMenu->url = null; + $vehicleMenu->save(); + $this->command->info('법인차량관리를 그룹 메뉴로 변경'); + + // 차량일지 메뉴 생성 + $vehicleLogMenu = Menu::create([ + 'tenant_id' => $tenantId, + 'parent_id' => $vehicleMenu->id, + 'name' => '차량일지', + 'url' => '/finance/vehicle-logs', + 'icon' => 'file-text', + 'sort_order' => 2, + 'is_active' => true, + ]); + $this->command->info("차량일지 메뉴 생성: {$vehicleLogMenu->url}"); + + // 차량정비 메뉴가 있으면 순서 조정 + $maintenanceMenu = Menu::where('tenant_id', $tenantId) + ->where('name', '차량정비') + ->where('parent_id', $vehicleMenu->id) + ->first(); + + if ($maintenanceMenu) { + $maintenanceMenu->sort_order = 3; + $maintenanceMenu->save(); + } + } else { + // 이미 그룹 메뉴인 경우 차량일지만 추가 + // 기존 하위 메뉴 순서 확인 + $maxSortOrder = Menu::where('parent_id', $vehicleMenu->id) + ->max('sort_order') ?? 0; + + $vehicleLogMenu = Menu::create([ + 'tenant_id' => $tenantId, + 'parent_id' => $vehicleMenu->id, + 'name' => '차량일지', + 'url' => '/finance/vehicle-logs', + 'icon' => 'file-text', + 'sort_order' => $maxSortOrder + 1, + 'is_active' => true, + ]); + $this->command->info("차량일지 메뉴 생성: {$vehicleLogMenu->url}"); + } + + // 결과 출력 + $this->command->info(''); + $this->command->info('=== 법인차량관리 하위 메뉴 ==='); + $children = Menu::where('parent_id', $vehicleMenu->id) + ->orderBy('sort_order') + ->get(['name', 'url', 'sort_order']); + + foreach ($children as $child) { + $this->command->info("{$child->sort_order}. {$child->name} ({$child->url})"); + } + } +} diff --git a/resources/views/finance/vehicle-logs.blade.php b/resources/views/finance/vehicle-logs.blade.php new file mode 100644 index 00000000..2b4e617a --- /dev/null +++ b/resources/views/finance/vehicle-logs.blade.php @@ -0,0 +1,795 @@ +@extends('layouts.app') + +@section('title', '차량일지') + +@push('styles') + +@endpush + +@section('content') +
+@endsection + +@push('scripts') + + + + + +@verbatim + +@endverbatim +@endpush diff --git a/resources/views/profile/index.blade.php b/resources/views/profile/index.blade.php index 2d92b4b0..3f37d084 100644 --- a/resources/views/profile/index.blade.php +++ b/resources/views/profile/index.blade.php @@ -60,11 +60,16 @@ class="w-full px-4 py-2 border border-gray-200 rounded-lg bg-gray-50 text-gray-5