feat: [outbound] 배차차량 관리 API — CRUD + options JSON 정책
- VehicleDispatchService: index(검색/필터/페이지네이션), stats(선불/착불/합계), show, update - VehicleDispatchController + VehicleDispatchUpdateRequest - options JSON 컬럼 추가 (dispatch_no, status, freight_cost_type, supply_amount, vat, total_amount, writer) - ShipmentService.syncDispatches에 options 필드 지원 추가 - inventory.php에 vehicle-dispatches 라우트 4개 등록 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
74
app/Http/Controllers/Api/V1/VehicleDispatchController.php
Normal file
74
app/Http/Controllers/Api/V1/VehicleDispatchController.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\VehicleDispatch\VehicleDispatchUpdateRequest;
|
||||
use App\Services\VehicleDispatchService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class VehicleDispatchController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly VehicleDispatchService $service
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 배차차량 목록 조회
|
||||
*/
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$params = $request->only([
|
||||
'search',
|
||||
'status',
|
||||
'start_date',
|
||||
'end_date',
|
||||
'per_page',
|
||||
'page',
|
||||
]);
|
||||
|
||||
$dispatches = $this->service->index($params);
|
||||
|
||||
return ApiResponse::success($dispatches, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 배차차량 통계 조회
|
||||
*/
|
||||
public function stats(): JsonResponse
|
||||
{
|
||||
$stats = $this->service->stats();
|
||||
|
||||
return ApiResponse::success($stats, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 배차차량 상세 조회
|
||||
*/
|
||||
public function show(int $id): JsonResponse
|
||||
{
|
||||
try {
|
||||
$dispatch = $this->service->show($id);
|
||||
|
||||
return ApiResponse::success($dispatch, __('message.fetched'));
|
||||
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
|
||||
return ApiResponse::error(__('error.not_found'), 404);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 배차차량 수정
|
||||
*/
|
||||
public function update(VehicleDispatchUpdateRequest $request, int $id): JsonResponse
|
||||
{
|
||||
try {
|
||||
$dispatch = $this->service->update($id, $request->validated());
|
||||
|
||||
return ApiResponse::success($dispatch, __('message.updated'));
|
||||
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
|
||||
return ApiResponse::error(__('error.not_found'), 404);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\VehicleDispatch;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class VehicleDispatchUpdateRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'freight_cost_type' => 'nullable|in:prepaid,collect',
|
||||
'logistics_company' => 'nullable|string|max:100',
|
||||
'arrival_datetime' => 'nullable|date',
|
||||
'tonnage' => 'nullable|string|max:20',
|
||||
'vehicle_no' => 'nullable|string|max:20',
|
||||
'driver_contact' => 'nullable|string|max:50',
|
||||
'remarks' => 'nullable|string',
|
||||
'supply_amount' => 'nullable|numeric|min:0',
|
||||
'vat' => 'nullable|numeric|min:0',
|
||||
'total_amount' => 'nullable|numeric|min:0',
|
||||
'status' => 'nullable|in:draft,completed',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -22,12 +22,14 @@ class ShipmentVehicleDispatch extends Model
|
||||
'vehicle_no',
|
||||
'driver_contact',
|
||||
'remarks',
|
||||
'options',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'seq' => 'integer',
|
||||
'shipment_id' => 'integer',
|
||||
'arrival_datetime' => 'datetime',
|
||||
'options' => 'array',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -513,6 +513,7 @@ protected function syncDispatches(Shipment $shipment, array $dispatches, int $te
|
||||
'vehicle_no' => $dispatch['vehicle_no'] ?? null,
|
||||
'driver_contact' => $dispatch['driver_contact'] ?? null,
|
||||
'remarks' => $dispatch['remarks'] ?? null,
|
||||
'options' => $dispatch['options'] ?? null,
|
||||
]);
|
||||
$seq++;
|
||||
}
|
||||
|
||||
140
app/Services/VehicleDispatchService.php
Normal file
140
app/Services/VehicleDispatchService.php
Normal file
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Tenants\ShipmentVehicleDispatch;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
|
||||
class VehicleDispatchService extends Service
|
||||
{
|
||||
/**
|
||||
* 배차차량 목록 조회
|
||||
*/
|
||||
public function index(array $params): LengthAwarePaginator
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$query = ShipmentVehicleDispatch::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->with('shipment');
|
||||
|
||||
// 검색어 필터
|
||||
if (! empty($params['search'])) {
|
||||
$search = $params['search'];
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('vehicle_no', 'like', "%{$search}%")
|
||||
->orWhere('options->dispatch_no', 'like', "%{$search}%")
|
||||
->orWhereHas('shipment', function ($q3) use ($search) {
|
||||
$q3->where('lot_no', 'like', "%{$search}%")
|
||||
->orWhere('site_name', 'like', "%{$search}%")
|
||||
->orWhere('customer_name', 'like', "%{$search}%");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 상태 필터 (options JSON)
|
||||
if (! empty($params['status'])) {
|
||||
$query->where('options->status', $params['status']);
|
||||
}
|
||||
|
||||
// 날짜 범위 필터
|
||||
if (! empty($params['start_date'])) {
|
||||
$query->where('arrival_datetime', '>=', $params['start_date']);
|
||||
}
|
||||
if (! empty($params['end_date'])) {
|
||||
$query->where('arrival_datetime', '<=', $params['end_date'].' 23:59:59');
|
||||
}
|
||||
|
||||
// 정렬
|
||||
$query->orderBy('id', 'desc');
|
||||
|
||||
// 페이지네이션
|
||||
$perPage = $params['per_page'] ?? 20;
|
||||
|
||||
return $query->paginate($perPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 배차차량 통계
|
||||
*/
|
||||
public function stats(): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$all = ShipmentVehicleDispatch::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->get();
|
||||
|
||||
$prepaid = 0;
|
||||
$collect = 0;
|
||||
$total = 0;
|
||||
|
||||
foreach ($all as $dispatch) {
|
||||
$opts = $dispatch->options ?? [];
|
||||
$amount = (float) ($opts['total_amount'] ?? 0);
|
||||
$total += $amount;
|
||||
|
||||
if (($opts['freight_cost_type'] ?? '') === 'prepaid') {
|
||||
$prepaid += $amount;
|
||||
}
|
||||
if (($opts['freight_cost_type'] ?? '') === 'collect') {
|
||||
$collect += $amount;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'prepaid_amount' => $prepaid,
|
||||
'collect_amount' => $collect,
|
||||
'total_amount' => $total,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 배차차량 상세 조회
|
||||
*/
|
||||
public function show(int $id): ShipmentVehicleDispatch
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
return ShipmentVehicleDispatch::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->with('shipment')
|
||||
->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 배차차량 수정
|
||||
*/
|
||||
public function update(int $id, array $data): ShipmentVehicleDispatch
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$dispatch = ShipmentVehicleDispatch::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->findOrFail($id);
|
||||
|
||||
// options에 저장할 필드 분리
|
||||
$optionFields = ['freight_cost_type', 'supply_amount', 'vat', 'total_amount', 'status'];
|
||||
$directFields = ['logistics_company', 'arrival_datetime', 'tonnage', 'vehicle_no', 'driver_contact', 'remarks'];
|
||||
|
||||
// 기존 options 유지하면서 업데이트
|
||||
$options = $dispatch->options ?? [];
|
||||
foreach ($optionFields as $field) {
|
||||
if (array_key_exists($field, $data)) {
|
||||
$options[$field] = $data[$field];
|
||||
}
|
||||
}
|
||||
|
||||
// 직접 컬럼 업데이트
|
||||
$updateData = ['options' => $options];
|
||||
foreach ($directFields as $field) {
|
||||
if (array_key_exists($field, $data)) {
|
||||
$updateData[$field] = $data[$field];
|
||||
}
|
||||
}
|
||||
|
||||
$dispatch->update($updateData);
|
||||
|
||||
return $dispatch->load('shipment');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('shipment_vehicle_dispatches', function (Blueprint $table) {
|
||||
$table->json('options')->nullable()->after('remarks')
|
||||
->comment('추가 속성 (dispatch_no, status, freight_cost_type, supply_amount, vat, total_amount, writer)');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('shipment_vehicle_dispatches', function (Blueprint $table) {
|
||||
$table->dropColumn('options');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -19,6 +19,7 @@
|
||||
use App\Http\Controllers\Api\V1\ReceivingController;
|
||||
use App\Http\Controllers\Api\V1\ShipmentController;
|
||||
use App\Http\Controllers\Api\V1\StockController;
|
||||
use App\Http\Controllers\Api\V1\VehicleDispatchController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
// Items API (품목 관리)
|
||||
@@ -123,3 +124,11 @@
|
||||
Route::patch('/{id}/status', [ShipmentController::class, 'updateStatus'])->whereNumber('id')->name('v1.shipments.status');
|
||||
Route::delete('/{id}', [ShipmentController::class, 'destroy'])->whereNumber('id')->name('v1.shipments.destroy');
|
||||
});
|
||||
|
||||
// Vehicle Dispatch API (배차차량 관리)
|
||||
Route::prefix('vehicle-dispatches')->group(function () {
|
||||
Route::get('', [VehicleDispatchController::class, 'index'])->name('v1.vehicle-dispatches.index');
|
||||
Route::get('/stats', [VehicleDispatchController::class, 'stats'])->name('v1.vehicle-dispatches.stats');
|
||||
Route::get('/{id}', [VehicleDispatchController::class, 'show'])->whereNumber('id')->name('v1.vehicle-dispatches.show');
|
||||
Route::put('/{id}', [VehicleDispatchController::class, 'update'])->whereNumber('id')->name('v1.vehicle-dispatches.update');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user