diff --git a/app/Http/Controllers/Finance/VehicleLogController.php b/app/Http/Controllers/Finance/VehicleLogController.php index e32970dc..9c568b5e 100644 --- a/app/Http/Controllers/Finance/VehicleLogController.php +++ b/app/Http/Controllers/Finance/VehicleLogController.php @@ -9,9 +9,6 @@ use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\View\View; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PhpOffice\PhpSpreadsheet\Writer\Xlsx; -use Symfony\Component\HttpFoundation\StreamedResponse; class VehicleLogController extends Controller { @@ -21,82 +18,112 @@ public function index(Request $request): View|Response return response('', 200)->header('HX-Redirect', route('finance.vehicle-logs')); } + return view('finance.vehicle-logs'); + } + + /** + * 차량 목록 조회 + */ + public function vehicles(Request $request): JsonResponse + { $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(), + return response()->json([ + 'success' => true, + 'data' => $vehicles, ]); } + /** + * 운행기록 목록 조회 + */ 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', - ]); + $query = VehicleLog::with('vehicle') + ->where('tenant_id', $tenantId); - $vehicleId = $request->vehicle_id; - $startDate = $request->start_date; - $endDate = $request->end_date; + // 차량 필터 + if ($request->filled('vehicle_id') && $request->vehicle_id !== 'all') { + $query->where('vehicle_id', $request->vehicle_id); + } - // 차량 정보 - $vehicle = CorporateVehicle::where('tenant_id', $tenantId) - ->findOrFail($vehicleId); + // 년/월 필터 + if ($request->filled('year')) { + $query->whereYear('log_date', $request->year); + } + if ($request->filled('month')) { + $query->whereMonth('log_date', $request->month); + } - // 전체 운행기록 수 (해당 차량) - $totalCount = VehicleLog::where('tenant_id', $tenantId) - ->where('vehicle_id', $vehicleId) - ->count(); + // 구분 필터 + if ($request->filled('trip_type') && $request->trip_type !== 'all') { + $query->where('trip_type', $request->trip_type); + } - $logs = VehicleLog::where('tenant_id', $tenantId) - ->where('vehicle_id', $vehicleId) - ->whereBetween('log_date', [$startDate, $endDate]) - ->orderBy('log_date', 'desc') + // 검색어 + if ($request->filled('search')) { + $search = $request->search; + $query->where(function ($q) use ($search) { + $q->where('driver_name', 'like', "%{$search}%") + ->orWhere('department', 'like', "%{$search}%") + ->orWhere('departure_name', 'like', "%{$search}%") + ->orWhere('arrival_name', 'like', "%{$search}%") + ->orWhere('note', 'like', "%{$search}%"); + }); + } + + $logs = $query->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'), - ]; + // 응답 포맷팅 + $data = $logs->map(function ($log) { + return [ + 'id' => $log->id, + 'logDate' => $log->log_date->format('Y-m-d'), + 'vehicleId' => $log->vehicle_id, + 'plateNumber' => $log->vehicle?->plate_number, + 'model' => $log->vehicle?->model, + 'department' => $log->department, + 'driverName' => $log->driver_name, + 'tripType' => $log->trip_type, + 'departureType' => $log->departure_type, + 'departureName' => $log->departure_name, + 'departureAddress' => $log->departure_address, + 'arrivalType' => $log->arrival_type, + 'arrivalName' => $log->arrival_name, + 'arrivalAddress' => $log->arrival_address, + 'distanceKm' => $log->distance_km, + 'note' => $log->note, + ]; + }); return response()->json([ 'success' => true, - 'data' => [ - 'vehicle' => $vehicle, - 'logs' => $logs, - 'totals' => $totals, - 'totalCount' => $totalCount, - ], + 'data' => $data, ]); } + /** + * 운행기록 등록 + */ public function store(Request $request): JsonResponse { - $tenantId = session('tenant_id', 1); - $request->validate([ - 'vehicle_id' => 'required|integer|exists:corporate_vehicles,id', + 'vehicle_id' => 'required|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', + '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); + $tenantId = session('tenant_id', 1); $log = VehicleLog::create([ 'tenant_id' => $tenantId, @@ -122,6 +149,9 @@ public function store(Request $request): JsonResponse ]); } + /** + * 운행기록 수정 + */ public function update(Request $request, int $id): JsonResponse { $tenantId = session('tenant_id', 1); @@ -129,13 +159,15 @@ public function update(Request $request, int $id): JsonResponse $log = VehicleLog::where('tenant_id', $tenantId)->findOrFail($id); $request->validate([ + 'vehicle_id' => 'required|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', + 'trip_type' => 'required|in:commute_to,commute_from,business,personal', 'distance_km' => 'required|integer|min:0', ]); $log->update([ + 'vehicle_id' => $request->vehicle_id, 'log_date' => $request->log_date, 'department' => $request->department, 'driver_name' => $request->driver_name, @@ -157,6 +189,9 @@ public function update(Request $request, int $id): JsonResponse ]); } + /** + * 운행기록 삭제 + */ public function destroy(int $id): JsonResponse { $tenantId = session('tenant_id', 1); @@ -170,96 +205,65 @@ public function destroy(int $id): JsonResponse ]); } - public function export(Request $request): StreamedResponse + /** + * 월간 통계 조회 + */ + public function summary(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', - ]); + $query = VehicleLog::where('tenant_id', $tenantId); - $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++; + // 차량 필터 + if ($request->filled('vehicle_id') && $request->vehicle_id !== 'all') { + $query->where('vehicle_id', $request->vehicle_id); } - // 데이터 - $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++; + // 년/월 필터 + if ($request->filled('year')) { + $query->whereYear('log_date', $request->year); + } + if ($request->filled('month')) { + $query->whereMonth('log_date', $request->month); } - // 합계 - $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'); + // 구분별 주행거리 합계 + $summary = $query->selectRaw(' + trip_type, + COUNT(*) as count, + SUM(distance_km) as total_distance + ') + ->groupBy('trip_type') + ->get() + ->keyBy('trip_type'); - $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'); + $tripTypes = VehicleLog::getTripTypes(); + $result = []; + $totalCount = 0; + $totalDistance = 0; - $filename = sprintf('운행기록부_%s_%s_%s.xlsx', $vehicle->plate_number, $startDate, $endDate); + foreach ($tripTypes as $type => $label) { + $data = $summary->get($type); + $count = $data ? $data->count : 0; + $distance = $data ? $data->total_distance : 0; + $result[$type] = [ + 'label' => $label, + 'count' => $count, + 'distance' => $distance, + ]; + $totalCount += $count; + $totalDistance += $distance; + } - return response()->streamDownload(function () use ($spreadsheet) { - $writer = new Xlsx($spreadsheet); - $writer->save('php://output'); - }, $filename, [ - 'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + return response()->json([ + 'success' => true, + 'data' => [ + 'byType' => $result, + 'total' => [ + 'count' => $totalCount, + 'distance' => $totalDistance, + ], + ], ]); } - - 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 index ded7a901..44d13968 100644 --- a/app/Models/VehicleLog.php +++ b/app/Models/VehicleLog.php @@ -2,13 +2,14 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Database\Eloquent\Relations\BelongsTo; class VehicleLog extends Model { - use SoftDeletes; + use HasFactory, SoftDeletes; protected $fillable = [ 'tenant_id', @@ -28,65 +29,63 @@ class VehicleLog extends Model ]; protected $casts = [ - 'log_date' => 'date:Y-m-d', + 'log_date' => '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'; - 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 + /** + * 테넌트 관계 + */ + public function tenant(): BelongsTo { - return self::tripTypeLabels()[$this->trip_type] ?? $this->trip_type; + return $this->belongsTo(Tenant::class); } - public function getDepartureTypeLabelAttribute(): string + /** + * 구분(trip_type) 목록 + */ + public static function getTripTypes(): array { - return self::locationTypeLabels()[$this->departure_type] ?? ($this->departure_type ?? ''); + return [ + 'commute_to' => '출근용', + 'commute_from' => '퇴근용', + 'business' => '업무용', + 'personal' => '비업무', + ]; } - public function getArrivalTypeLabelAttribute(): string + /** + * 분류(location_type) 목록 + */ + public static function getLocationTypes(): array { - return self::locationTypeLabels()[$this->arrival_type] ?? ($this->arrival_type ?? ''); + return [ + 'home' => '자택', + 'office' => '회사', + 'client' => '거래처', + 'other' => '기타', + ]; + } + + /** + * 비고 목록 + */ + public static function getNoteOptions(): array + { + return [ + '거래처방문', + '제조시설등', + '회의참석', + '판촉활동', + '교육등', + ]; } } diff --git a/resources/views/finance/vehicle-logs.blade.php b/resources/views/finance/vehicle-logs.blade.php index 2b4e617a..b2dfdaa0 100644 --- a/resources/views/finance/vehicle-logs.blade.php +++ b/resources/views/finance/vehicle-logs.blade.php @@ -17,13 +17,6 @@ - @verbatim @endverbatim @endpush diff --git a/routes/web.php b/routes/web.php index 26f0f329..c2659b6c 100644 --- a/routes/web.php +++ b/routes/web.php @@ -828,11 +828,12 @@ // 차량일지 (운행기록부) Route::get('/vehicle-logs', [\App\Http\Controllers\Finance\VehicleLogController::class, 'index'])->name('vehicle-logs'); + Route::get('/vehicle-logs/vehicles', [\App\Http\Controllers\Finance\VehicleLogController::class, 'vehicles'])->name('vehicle-logs.vehicles'); Route::get('/vehicle-logs/list', [\App\Http\Controllers\Finance\VehicleLogController::class, 'list'])->name('vehicle-logs.list'); + Route::get('/vehicle-logs/summary', [\App\Http\Controllers\Finance\VehicleLogController::class, 'summary'])->name('vehicle-logs.summary'); 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', [\App\Http\Controllers\Finance\VehicleMaintenanceController::class, 'index'])->name('vehicle-maintenance');