diff --git a/app/Http/Controllers/Finance/VehicleLogController.php b/app/Http/Controllers/Finance/VehicleLogController.php new file mode 100644 index 00000000..8b240193 --- /dev/null +++ b/app/Http/Controllers/Finance/VehicleLogController.php @@ -0,0 +1,266 @@ +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', + 'year' => 'required|integer', + 'month' => 'required|integer|min:1|max:12', + ]); + + $vehicleId = $request->vehicle_id; + $year = $request->year; + $month = $request->month; + + // 차량 정보 + $vehicle = CorporateVehicle::where('tenant_id', $tenantId) + ->findOrFail($vehicleId); + + // 해당 월의 운행 기록 + $startDate = sprintf('%04d-%02d-01', $year, $month); + $endDate = date('Y-m-t', strtotime($startDate)); + + $logs = VehicleLog::where('tenant_id', $tenantId) + ->where('vehicle_id', $vehicleId) + ->whereBetween('log_date', [$startDate, $endDate]) + ->orderBy('log_date') + ->orderBy('id') + ->get(); + + // 월별 합계 + $totals = [ + 'business_km' => $logs->whereIn('trip_type', ['commute_to', 'commute_from', 'business'])->sum('distance_km'), + 'personal_km' => $logs->where('trip_type', 'personal')->sum('distance_km'), + 'total_km' => $logs->sum('distance_km'), + ]; + + return response()->json([ + 'success' => true, + 'data' => [ + 'vehicle' => $vehicle, + 'logs' => $logs, + 'totals' => $totals, + ], + ]); + } + + 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', + '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', + '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', + 'year' => 'required|integer', + 'month' => 'required|integer|min:1|max:12', + ]); + + $vehicleId = $request->vehicle_id; + $year = $request->year; + $month = $request->month; + + $vehicle = CorporateVehicle::where('tenant_id', $tenantId) + ->findOrFail($vehicleId); + + $startDate = sprintf('%04d-%02d-01', $year, $month); + $endDate = date('Y-m-t', strtotime($startDate)); + + $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('%d년 %d월', $year, $month)); + + // 헤더 + $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'])->sum('distance_km'); + $personalKm = $logs->where('trip_type', 'personal')->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_%d년%d월.xlsx', $vehicle->plate_number, $year, $month); + + 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/Models/VehicleLog.php b/app/Models/VehicleLog.php new file mode 100644 index 00000000..30c05dcc --- /dev/null +++ b/app/Models/VehicleLog.php @@ -0,0 +1,86 @@ + 'date', + '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'; + + // 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 => '비업무', + ]; + } + + 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/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..96aae406 --- /dev/null +++ b/resources/views/finance/vehicle-logs.blade.php @@ -0,0 +1,674 @@ +@extends('layouts.app') + +@section('title', '차량일지') + +@push('styles') + +@endpush + +@section('content') +
+@endsection + +@push('scripts') + + + + + +@verbatim + +@endverbatim +@endpush diff --git a/routes/web.php b/routes/web.php index 23c46bda..5de1cafd 100644 --- a/routes/web.php +++ b/routes/web.php @@ -815,6 +815,15 @@ Route::post('/corporate-vehicles', [\App\Http\Controllers\Finance\CorporateVehicleController::class, 'store'])->name('corporate-vehicles.store'); Route::put('/corporate-vehicles/{id}', [\App\Http\Controllers\Finance\CorporateVehicleController::class, 'update'])->name('corporate-vehicles.update'); Route::delete('/corporate-vehicles/{id}', [\App\Http\Controllers\Finance\CorporateVehicleController::class, 'destroy'])->name('corporate-vehicles.destroy'); + + // 차량일지 (운행기록부) + Route::get('/vehicle-logs', [\App\Http\Controllers\Finance\VehicleLogController::class, 'index'])->name('vehicle-logs'); + Route::get('/vehicle-logs/list', [\App\Http\Controllers\Finance\VehicleLogController::class, 'list'])->name('vehicle-logs.list'); + Route::post('/vehicle-logs', [\App\Http\Controllers\Finance\VehicleLogController::class, 'store'])->name('vehicle-logs.store'); + Route::put('/vehicle-logs/{id}', [\App\Http\Controllers\Finance\VehicleLogController::class, 'update'])->name('vehicle-logs.update'); + Route::delete('/vehicle-logs/{id}', [\App\Http\Controllers\Finance\VehicleLogController::class, 'destroy'])->name('vehicle-logs.destroy'); + Route::get('/vehicle-logs/export', [\App\Http\Controllers\Finance\VehicleLogController::class, 'export'])->name('vehicle-logs.export'); + Route::get('/vehicle-maintenance', function () { if (request()->header('HX-Request')) { return response('', 200)->header('HX-Redirect', route('finance.vehicle-maintenance'));