feat: [HR/기타] 캘린더/배차/설비/재고 + DB 마이그레이션
- 캘린더 CRUD API, 배차차량 관리 API (CRUD + options) - 배차정보 다중 행 시스템 (shipment_vehicle_dispatches) - 설비 다중점검주기 + 부 담당자 스키마 추가 - TodayIssue 날짜 기반 조회, Stock/Client 날짜 필터 - i18n 메시지 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -51,4 +51,56 @@ public function summary(Request $request)
|
|||||||
);
|
);
|
||||||
}, __('message.fetched'));
|
}, __('message.fetched'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 일정 등록
|
||||||
|
*/
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'title' => 'required|string|max:200',
|
||||||
|
'description' => 'nullable|string|max:1000',
|
||||||
|
'start_date' => 'required|date_format:Y-m-d',
|
||||||
|
'end_date' => 'required|date_format:Y-m-d|after_or_equal:start_date',
|
||||||
|
'start_time' => 'nullable|date_format:H:i',
|
||||||
|
'end_time' => 'nullable|date_format:H:i',
|
||||||
|
'is_all_day' => 'boolean',
|
||||||
|
'color' => 'nullable|string|max:20',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return ApiResponse::handle(function () use ($validated) {
|
||||||
|
return $this->calendarService->createSchedule($validated);
|
||||||
|
}, __('message.created'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 일정 수정
|
||||||
|
*/
|
||||||
|
public function update(Request $request, int $id)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'title' => 'required|string|max:200',
|
||||||
|
'description' => 'nullable|string|max:1000',
|
||||||
|
'start_date' => 'required|date_format:Y-m-d',
|
||||||
|
'end_date' => 'required|date_format:Y-m-d|after_or_equal:start_date',
|
||||||
|
'start_time' => 'nullable|date_format:H:i',
|
||||||
|
'end_time' => 'nullable|date_format:H:i',
|
||||||
|
'is_all_day' => 'boolean',
|
||||||
|
'color' => 'nullable|string|max:20',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return ApiResponse::handle(function () use ($id, $validated) {
|
||||||
|
return $this->calendarService->updateSchedule($id, $validated);
|
||||||
|
}, __('message.updated'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 일정 삭제
|
||||||
|
*/
|
||||||
|
public function destroy(int $id)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($id) {
|
||||||
|
return $this->calendarService->deleteSchedule($id);
|
||||||
|
}, __('message.deleted'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ public function index(Request $request): JsonResponse
|
|||||||
'sort_dir',
|
'sort_dir',
|
||||||
'per_page',
|
'per_page',
|
||||||
'page',
|
'page',
|
||||||
|
'start_date',
|
||||||
|
'end_date',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$stocks = $this->service->index($params);
|
$stocks = $this->service->index($params);
|
||||||
|
|||||||
@@ -20,9 +20,10 @@ public function __construct(
|
|||||||
public function summary(Request $request): JsonResponse
|
public function summary(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
$limit = (int) $request->input('limit', 30);
|
$limit = (int) $request->input('limit', 30);
|
||||||
|
$date = $request->input('date'); // YYYY-MM-DD (이전 이슈 조회용)
|
||||||
|
|
||||||
return ApiResponse::handle(function () use ($limit) {
|
return ApiResponse::handle(function () use ($limit, $date) {
|
||||||
return $this->todayIssueService->summary($limit);
|
return $this->todayIssueService->summary($limit, null, $date);
|
||||||
}, __('message.fetched'));
|
}, __('message.fetched'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ public function rules(): array
|
|||||||
'scheduled_date' => 'required|date',
|
'scheduled_date' => 'required|date',
|
||||||
'status' => 'nullable|in:scheduled,ready,shipping,completed',
|
'status' => 'nullable|in:scheduled,ready,shipping,completed',
|
||||||
'priority' => 'nullable|in:urgent,normal,low',
|
'priority' => 'nullable|in:urgent,normal,low',
|
||||||
'delivery_method' => 'nullable|in:pickup,direct,logistics',
|
'delivery_method' => 'nullable|in:pickup,direct,logistics,direct_dispatch,loading,kyungdong_delivery,daesin_delivery,kyungdong_freight,daesin_freight,self_pickup',
|
||||||
|
|
||||||
// 발주처/배송 정보
|
// 발주처/배송 정보
|
||||||
'client_id' => 'nullable|integer|exists:clients,id',
|
'client_id' => 'nullable|integer|exists:clients,id',
|
||||||
@@ -55,6 +55,16 @@ public function rules(): array
|
|||||||
// 기타
|
// 기타
|
||||||
'remarks' => 'nullable|string',
|
'remarks' => 'nullable|string',
|
||||||
|
|
||||||
|
// 배차정보
|
||||||
|
'vehicle_dispatches' => 'nullable|array',
|
||||||
|
'vehicle_dispatches.*.seq' => 'nullable|integer|min:1',
|
||||||
|
'vehicle_dispatches.*.logistics_company' => 'nullable|string|max:100',
|
||||||
|
'vehicle_dispatches.*.arrival_datetime' => 'nullable|date',
|
||||||
|
'vehicle_dispatches.*.tonnage' => 'nullable|string|max:20',
|
||||||
|
'vehicle_dispatches.*.vehicle_no' => 'nullable|string|max:20',
|
||||||
|
'vehicle_dispatches.*.driver_contact' => 'nullable|string|max:50',
|
||||||
|
'vehicle_dispatches.*.remarks' => 'nullable|string',
|
||||||
|
|
||||||
// 출하 품목
|
// 출하 품목
|
||||||
'items' => 'nullable|array',
|
'items' => 'nullable|array',
|
||||||
'items.*.seq' => 'nullable|integer|min:1',
|
'items.*.seq' => 'nullable|integer|min:1',
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ public function rules(): array
|
|||||||
'order_id' => 'nullable|integer|exists:orders,id',
|
'order_id' => 'nullable|integer|exists:orders,id',
|
||||||
'scheduled_date' => 'nullable|date',
|
'scheduled_date' => 'nullable|date',
|
||||||
'priority' => 'nullable|in:urgent,normal,low',
|
'priority' => 'nullable|in:urgent,normal,low',
|
||||||
'delivery_method' => 'nullable|in:pickup,direct,logistics',
|
'delivery_method' => 'nullable|in:pickup,direct,logistics,direct_dispatch,loading,kyungdong_delivery,daesin_delivery,kyungdong_freight,daesin_freight,self_pickup',
|
||||||
|
|
||||||
// 발주처/배송 정보
|
// 발주처/배송 정보
|
||||||
'client_id' => 'nullable|integer|exists:clients,id',
|
'client_id' => 'nullable|integer|exists:clients,id',
|
||||||
@@ -53,6 +53,16 @@ public function rules(): array
|
|||||||
// 기타
|
// 기타
|
||||||
'remarks' => 'nullable|string',
|
'remarks' => 'nullable|string',
|
||||||
|
|
||||||
|
// 배차정보
|
||||||
|
'vehicle_dispatches' => 'nullable|array',
|
||||||
|
'vehicle_dispatches.*.seq' => 'nullable|integer|min:1',
|
||||||
|
'vehicle_dispatches.*.logistics_company' => 'nullable|string|max:100',
|
||||||
|
'vehicle_dispatches.*.arrival_datetime' => 'nullable|date',
|
||||||
|
'vehicle_dispatches.*.tonnage' => 'nullable|string|max:20',
|
||||||
|
'vehicle_dispatches.*.vehicle_no' => 'nullable|string|max:20',
|
||||||
|
'vehicle_dispatches.*.driver_contact' => 'nullable|string|max:50',
|
||||||
|
'vehicle_dispatches.*.remarks' => 'nullable|string',
|
||||||
|
|
||||||
// 출하 품목
|
// 출하 품목
|
||||||
'items' => 'nullable|array',
|
'items' => 'nullable|array',
|
||||||
'items.*.seq' => 'nullable|integer|min:1',
|
'items.*.seq' => 'nullable|integer|min:1',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -134,6 +134,14 @@ public function items(): HasMany
|
|||||||
return $this->hasMany(ShipmentItem::class)->orderBy('seq');
|
return $this->hasMany(ShipmentItem::class)->orderBy('seq');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 배차정보 관계
|
||||||
|
*/
|
||||||
|
public function vehicleDispatches(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(ShipmentVehicleDispatch::class)->orderBy('seq');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 거래처 관계
|
* 거래처 관계
|
||||||
*/
|
*/
|
||||||
|
|||||||
52
app/Models/Tenants/ShipmentVehicleDispatch.php
Normal file
52
app/Models/Tenants/ShipmentVehicleDispatch.php
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models\Tenants;
|
||||||
|
|
||||||
|
use App\Traits\Auditable;
|
||||||
|
use App\Traits\BelongsToTenant;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
|
class ShipmentVehicleDispatch extends Model
|
||||||
|
{
|
||||||
|
use Auditable, BelongsToTenant, SoftDeletes;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'tenant_id',
|
||||||
|
'shipment_id',
|
||||||
|
'seq',
|
||||||
|
'logistics_company',
|
||||||
|
'arrival_datetime',
|
||||||
|
'tonnage',
|
||||||
|
'vehicle_no',
|
||||||
|
'driver_contact',
|
||||||
|
'remarks',
|
||||||
|
'options',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'seq' => 'integer',
|
||||||
|
'shipment_id' => 'integer',
|
||||||
|
'arrival_datetime' => 'datetime',
|
||||||
|
'options' => 'array',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 출하 관계
|
||||||
|
*/
|
||||||
|
public function shipment(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Shipment::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 다음 순번 가져오기
|
||||||
|
*/
|
||||||
|
public static function getNextSeq(int $shipmentId): int
|
||||||
|
{
|
||||||
|
$maxSeq = static::where('shipment_id', $shipmentId)->max('seq');
|
||||||
|
|
||||||
|
return ($maxSeq ?? 0) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -226,6 +226,78 @@ private function getLeaveSchedules(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 일정 등록
|
||||||
|
*/
|
||||||
|
public function createSchedule(array $data): array
|
||||||
|
{
|
||||||
|
$schedule = Schedule::create([
|
||||||
|
'tenant_id' => $this->tenantId(),
|
||||||
|
'title' => $data['title'],
|
||||||
|
'description' => $data['description'] ?? null,
|
||||||
|
'start_date' => $data['start_date'],
|
||||||
|
'end_date' => $data['end_date'],
|
||||||
|
'start_time' => $data['start_time'] ?? null,
|
||||||
|
'end_time' => $data['end_time'] ?? null,
|
||||||
|
'is_all_day' => $data['is_all_day'] ?? true,
|
||||||
|
'type' => Schedule::TYPE_EVENT,
|
||||||
|
'color' => $data['color'] ?? null,
|
||||||
|
'is_active' => true,
|
||||||
|
'created_by' => $this->apiUserId(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $schedule->id,
|
||||||
|
'title' => $schedule->title,
|
||||||
|
'start_date' => $schedule->start_date?->format('Y-m-d'),
|
||||||
|
'end_date' => $schedule->end_date?->format('Y-m-d'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 일정 수정
|
||||||
|
*/
|
||||||
|
public function updateSchedule(int $id, array $data): array
|
||||||
|
{
|
||||||
|
$schedule = Schedule::where('tenant_id', $this->tenantId())
|
||||||
|
->findOrFail($id);
|
||||||
|
|
||||||
|
$schedule->update([
|
||||||
|
'title' => $data['title'],
|
||||||
|
'description' => $data['description'] ?? null,
|
||||||
|
'start_date' => $data['start_date'],
|
||||||
|
'end_date' => $data['end_date'],
|
||||||
|
'start_time' => $data['start_time'] ?? null,
|
||||||
|
'end_time' => $data['end_time'] ?? null,
|
||||||
|
'is_all_day' => $data['is_all_day'] ?? true,
|
||||||
|
'color' => $data['color'] ?? null,
|
||||||
|
'updated_by' => $this->apiUserId(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $schedule->id,
|
||||||
|
'title' => $schedule->title,
|
||||||
|
'start_date' => $schedule->start_date?->format('Y-m-d'),
|
||||||
|
'end_date' => $schedule->end_date?->format('Y-m-d'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 일정 삭제 (소프트 삭제)
|
||||||
|
*/
|
||||||
|
public function deleteSchedule(int $id): array
|
||||||
|
{
|
||||||
|
$schedule = Schedule::where('tenant_id', $this->tenantId())
|
||||||
|
->findOrFail($id);
|
||||||
|
|
||||||
|
$schedule->update(['deleted_by' => $this->apiUserId()]);
|
||||||
|
$schedule->delete();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $schedule->id,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 범용 일정 조회 (본사 공통 + 테넌트 일정)
|
* 범용 일정 조회 (본사 공통 + 테넌트 일정)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ public function index(array $params)
|
|||||||
$q = trim((string) ($params['q'] ?? ''));
|
$q = trim((string) ($params['q'] ?? ''));
|
||||||
$onlyActive = $params['only_active'] ?? null;
|
$onlyActive = $params['only_active'] ?? null;
|
||||||
$clientType = $params['client_type'] ?? null;
|
$clientType = $params['client_type'] ?? null;
|
||||||
|
$startDate = $params['start_date'] ?? null;
|
||||||
|
$endDate = $params['end_date'] ?? null;
|
||||||
|
|
||||||
$query = Client::query()->where('tenant_id', $tenantId);
|
$query = Client::query()->where('tenant_id', $tenantId);
|
||||||
|
|
||||||
@@ -43,6 +45,14 @@ public function index(array $params)
|
|||||||
$query->whereIn('client_type', $types);
|
$query->whereIn('client_type', $types);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 등록일 기간 필터
|
||||||
|
if ($startDate) {
|
||||||
|
$query->whereDate('created_at', '>=', $startDate);
|
||||||
|
}
|
||||||
|
if ($endDate) {
|
||||||
|
$query->whereDate('created_at', '<=', $endDate);
|
||||||
|
}
|
||||||
|
|
||||||
$query->orderBy('client_code')->orderBy('id');
|
$query->orderBy('client_code')->orderBy('id');
|
||||||
|
|
||||||
$paginator = $query->paginate($size, ['*'], 'page', $page);
|
$paginator = $query->paginate($size, ['*'], 'page', $page);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
use App\Models\Orders\Order;
|
use App\Models\Orders\Order;
|
||||||
use App\Models\Tenants\Shipment;
|
use App\Models\Tenants\Shipment;
|
||||||
use App\Models\Tenants\ShipmentItem;
|
use App\Models\Tenants\ShipmentItem;
|
||||||
|
use App\Models\Tenants\ShipmentVehicleDispatch;
|
||||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
@@ -19,7 +20,7 @@ public function index(array $params): LengthAwarePaginator
|
|||||||
|
|
||||||
$query = Shipment::query()
|
$query = Shipment::query()
|
||||||
->where('tenant_id', $tenantId)
|
->where('tenant_id', $tenantId)
|
||||||
->with(['items', 'order.client', 'order.writer', 'workOrder']);
|
->with(['items', 'vehicleDispatches', 'order.client', 'order.writer', 'workOrder']);
|
||||||
|
|
||||||
// 검색어 필터
|
// 검색어 필터
|
||||||
if (! empty($params['search'])) {
|
if (! empty($params['search'])) {
|
||||||
@@ -164,6 +165,7 @@ public function show(int $id): Shipment
|
|||||||
'items' => function ($query) {
|
'items' => function ($query) {
|
||||||
$query->orderBy('seq');
|
$query->orderBy('seq');
|
||||||
},
|
},
|
||||||
|
'vehicleDispatches',
|
||||||
'order.client',
|
'order.client',
|
||||||
'order.writer',
|
'order.writer',
|
||||||
'workOrder',
|
'workOrder',
|
||||||
@@ -228,7 +230,12 @@ public function store(array $data): Shipment
|
|||||||
$this->syncItems($shipment, $data['items'], $tenantId);
|
$this->syncItems($shipment, $data['items'], $tenantId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $shipment->load('items');
|
// 배차정보 추가
|
||||||
|
if (! empty($data['vehicle_dispatches'])) {
|
||||||
|
$this->syncDispatches($shipment, $data['vehicle_dispatches'], $tenantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $shipment->load(['items', 'vehicleDispatches']);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,7 +290,12 @@ public function update(int $id, array $data): Shipment
|
|||||||
$this->syncItems($shipment, $data['items'], $tenantId);
|
$this->syncItems($shipment, $data['items'], $tenantId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $shipment->load('items');
|
// 배차정보 동기화
|
||||||
|
if (isset($data['vehicle_dispatches'])) {
|
||||||
|
$this->syncDispatches($shipment, $data['vehicle_dispatches'], $tenantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $shipment->load(['items', 'vehicleDispatches']);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,7 +352,7 @@ public function updateStatus(int $id, string $status, ?array $additionalData = n
|
|||||||
// 연결된 수주(Order) 상태 동기화
|
// 연결된 수주(Order) 상태 동기화
|
||||||
$this->syncOrderStatus($shipment, $tenantId);
|
$this->syncOrderStatus($shipment, $tenantId);
|
||||||
|
|
||||||
return $shipment->load('items');
|
return $shipment->load(['items', 'vehicleDispatches']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -439,6 +451,9 @@ public function delete(int $id): bool
|
|||||||
// 품목 삭제
|
// 품목 삭제
|
||||||
$shipment->items()->delete();
|
$shipment->items()->delete();
|
||||||
|
|
||||||
|
// 배차정보 삭제
|
||||||
|
$shipment->vehicleDispatches()->delete();
|
||||||
|
|
||||||
// 출하 삭제
|
// 출하 삭제
|
||||||
$shipment->update(['deleted_by' => $userId]);
|
$shipment->update(['deleted_by' => $userId]);
|
||||||
$shipment->delete();
|
$shipment->delete();
|
||||||
@@ -477,6 +492,33 @@ protected function syncItems(Shipment $shipment, array $items, int $tenantId): v
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 배차정보 동기화
|
||||||
|
*/
|
||||||
|
protected function syncDispatches(Shipment $shipment, array $dispatches, int $tenantId): void
|
||||||
|
{
|
||||||
|
// 기존 배차정보 삭제
|
||||||
|
$shipment->vehicleDispatches()->forceDelete();
|
||||||
|
|
||||||
|
// 새 배차정보 생성
|
||||||
|
$seq = 1;
|
||||||
|
foreach ($dispatches as $dispatch) {
|
||||||
|
ShipmentVehicleDispatch::create([
|
||||||
|
'tenant_id' => $tenantId,
|
||||||
|
'shipment_id' => $shipment->id,
|
||||||
|
'seq' => $dispatch['seq'] ?? $seq,
|
||||||
|
'logistics_company' => $dispatch['logistics_company'] ?? null,
|
||||||
|
'arrival_datetime' => $dispatch['arrival_datetime'] ?? null,
|
||||||
|
'tonnage' => $dispatch['tonnage'] ?? null,
|
||||||
|
'vehicle_no' => $dispatch['vehicle_no'] ?? null,
|
||||||
|
'driver_contact' => $dispatch['driver_contact'] ?? null,
|
||||||
|
'remarks' => $dispatch['remarks'] ?? null,
|
||||||
|
'options' => $dispatch['options'] ?? null,
|
||||||
|
]);
|
||||||
|
$seq++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LOT 옵션 조회 (출고 가능한 LOT 목록)
|
* LOT 옵션 조회 (출고 가능한 LOT 목록)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ private function getBadDebtStatus(int $tenantId): array
|
|||||||
$count = BadDebt::query()
|
$count = BadDebt::query()
|
||||||
->where('tenant_id', $tenantId)
|
->where('tenant_id', $tenantId)
|
||||||
->where('status', BadDebt::STATUS_COLLECTING) // 추심 진행 중
|
->where('status', BadDebt::STATUS_COLLECTING) // 추심 진행 중
|
||||||
|
->where('is_active', true) // 활성 채권만 (목록 페이지와 일치)
|
||||||
->count();
|
->count();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -88,6 +88,20 @@ public function index(array $params): LengthAwarePaginator
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 날짜 범위 필터 (해당 기간에 입출고 이력이 있는 품목만)
|
||||||
|
if (! empty($params['start_date']) || ! empty($params['end_date'])) {
|
||||||
|
$query->whereHas('stock', function ($stockQuery) use ($params) {
|
||||||
|
$stockQuery->whereHas('transactions', function ($txQuery) use ($params) {
|
||||||
|
if (! empty($params['start_date'])) {
|
||||||
|
$txQuery->whereDate('created_at', '>=', $params['start_date']);
|
||||||
|
}
|
||||||
|
if (! empty($params['end_date'])) {
|
||||||
|
$txQuery->whereDate('created_at', '<=', $params['end_date']);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 정렬
|
// 정렬
|
||||||
$sortBy = $params['sort_by'] ?? 'code';
|
$sortBy = $params['sort_by'] ?? 'code';
|
||||||
$sortDir = $params['sort_dir'] ?? 'asc';
|
$sortDir = $params['sort_dir'] ?? 'asc';
|
||||||
|
|||||||
@@ -17,30 +17,42 @@ class TodayIssueService extends Service
|
|||||||
*
|
*
|
||||||
* @param int $limit 조회할 최대 항목 수 (기본 30)
|
* @param int $limit 조회할 최대 항목 수 (기본 30)
|
||||||
* @param string|null $badge 뱃지 필터 (null이면 전체)
|
* @param string|null $badge 뱃지 필터 (null이면 전체)
|
||||||
|
* @param string|null $date 조회 날짜 (YYYY-MM-DD, null이면 오늘)
|
||||||
*/
|
*/
|
||||||
public function summary(int $limit = 30, ?string $badge = null): array
|
public function summary(int $limit = 30, ?string $badge = null, ?string $date = null): array
|
||||||
{
|
{
|
||||||
$tenantId = $this->tenantId();
|
$tenantId = $this->tenantId();
|
||||||
$userId = $this->apiUserId();
|
$userId = $this->apiUserId();
|
||||||
|
|
||||||
|
// date 파라미터가 있으면 해당 날짜, 없으면 오늘
|
||||||
|
$targetDate = $date ? Carbon::parse($date) : today();
|
||||||
|
|
||||||
$query = TodayIssue::query()
|
$query = TodayIssue::query()
|
||||||
->where('tenant_id', $tenantId)
|
->where('tenant_id', $tenantId)
|
||||||
->forUser($userId) // 본인 대상 또는 전체 브로드캐스트
|
->forUser($userId) // 본인 대상 또는 전체 브로드캐스트
|
||||||
->active() // 만료되지 않은 이슈만
|
->whereDate('created_at', $targetDate)
|
||||||
->today() // 오늘 날짜 이슈만
|
|
||||||
->orderByDesc('created_at');
|
->orderByDesc('created_at');
|
||||||
|
|
||||||
|
// 이전 이슈 조회 시에는 만료 필터 무시 (과거 데이터도 조회 가능)
|
||||||
|
if (! $date) {
|
||||||
|
$query->active(); // 오늘 이슈만 만료 필터 적용
|
||||||
|
}
|
||||||
|
|
||||||
// 뱃지 필터
|
// 뱃지 필터
|
||||||
if ($badge !== null && $badge !== 'all') {
|
if ($badge !== null && $badge !== 'all') {
|
||||||
$query->byBadge($badge);
|
$query->byBadge($badge);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 전체 개수 (필터 적용 전, 오늘 날짜만)
|
// 전체 개수 (필터 적용 전)
|
||||||
$totalQuery = TodayIssue::query()
|
$totalQuery = TodayIssue::query()
|
||||||
->where('tenant_id', $tenantId)
|
->where('tenant_id', $tenantId)
|
||||||
->forUser($userId)
|
->forUser($userId)
|
||||||
->active()
|
->whereDate('created_at', $targetDate);
|
||||||
->today();
|
|
||||||
|
if (! $date) {
|
||||||
|
$totalQuery->active();
|
||||||
|
}
|
||||||
|
|
||||||
$totalCount = $totalQuery->count();
|
$totalCount = $totalQuery->count();
|
||||||
|
|
||||||
// 결과 조회
|
// 결과 조회
|
||||||
|
|||||||
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('equipments', function (Blueprint $table) {
|
||||||
|
$table->foreignId('sub_manager_id')->nullable()->after('manager_id')
|
||||||
|
->comment('부 담당자 ID (users.id)');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('equipments', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('sub_manager_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// 컬럼 추가 (이미 존재하면 건너뜀)
|
||||||
|
if (! Schema::hasColumn('equipment_inspection_templates', 'inspection_cycle')) {
|
||||||
|
Schema::table('equipment_inspection_templates', function (Blueprint $table) {
|
||||||
|
$table->string('inspection_cycle', 20)->default('daily')->after('equipment_id')
|
||||||
|
->comment('점검주기: daily/weekly/monthly/bimonthly/quarterly/semiannual');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// FK 삭제 → 유니크 변경 → FK 재생성 (개별 statement)
|
||||||
|
$this->dropFkIfExists('equipment_inspection_templates', 'equipment_inspection_templates_equipment_id_foreign');
|
||||||
|
$this->dropUniqueIfExists('equipment_inspection_templates', 'uq_equipment_item_no');
|
||||||
|
|
||||||
|
Schema::table('equipment_inspection_templates', function (Blueprint $table) {
|
||||||
|
$table->unique(['equipment_id', 'inspection_cycle', 'item_no'], 'uq_equipment_cycle_item_no');
|
||||||
|
$table->index('inspection_cycle', 'idx_insp_tmpl_cycle');
|
||||||
|
|
||||||
|
$table->foreign('equipment_id')
|
||||||
|
->references('id')
|
||||||
|
->on('equipments')
|
||||||
|
->onDelete('cascade');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$this->dropFkIfExists('equipment_inspection_templates', 'equipment_inspection_templates_equipment_id_foreign');
|
||||||
|
|
||||||
|
Schema::table('equipment_inspection_templates', function (Blueprint $table) {
|
||||||
|
$table->dropIndex('idx_insp_tmpl_cycle');
|
||||||
|
$table->dropUnique('uq_equipment_cycle_item_no');
|
||||||
|
$table->unique(['equipment_id', 'item_no'], 'uq_equipment_item_no');
|
||||||
|
$table->dropColumn('inspection_cycle');
|
||||||
|
|
||||||
|
$table->foreign('equipment_id')
|
||||||
|
->references('id')
|
||||||
|
->on('equipments')
|
||||||
|
->onDelete('cascade');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function dropFkIfExists(string $table, string $fkName): void
|
||||||
|
{
|
||||||
|
$fks = DB::select("SELECT CONSTRAINT_NAME FROM information_schema.TABLE_CONSTRAINTS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND CONSTRAINT_TYPE = 'FOREIGN KEY' AND CONSTRAINT_NAME = ?", [$table, $fkName]);
|
||||||
|
if (count($fks) > 0) {
|
||||||
|
DB::statement("ALTER TABLE `{$table}` DROP FOREIGN KEY `{$fkName}`");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function dropUniqueIfExists(string $table, string $indexName): void
|
||||||
|
{
|
||||||
|
$indexes = DB::select("SHOW INDEX FROM `{$table}` WHERE Key_name = ?", [$indexName]);
|
||||||
|
if (count($indexes) > 0) {
|
||||||
|
DB::statement("ALTER TABLE `{$table}` DROP INDEX `{$indexName}`");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('equipment_inspections', function (Blueprint $table) {
|
||||||
|
$table->string('inspection_cycle', 20)->default('daily')->after('equipment_id')
|
||||||
|
->comment('점검주기: daily/weekly/monthly/bimonthly/quarterly/semiannual');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 기존 레코드를 daily로 설정
|
||||||
|
DB::statement("UPDATE equipment_inspections SET inspection_cycle = 'daily' WHERE inspection_cycle = '' OR inspection_cycle IS NULL");
|
||||||
|
|
||||||
|
Schema::table('equipment_inspections', function (Blueprint $table) {
|
||||||
|
// 기존 유니크/인덱스 삭제
|
||||||
|
$table->dropUnique('uq_inspection_month');
|
||||||
|
$table->dropIndex('idx_inspection_ym');
|
||||||
|
|
||||||
|
// cycle 포함 유니크/인덱스 추가
|
||||||
|
$table->unique(['tenant_id', 'equipment_id', 'inspection_cycle', 'year_month'], 'uq_inspection_cycle_period');
|
||||||
|
$table->index(['tenant_id', 'inspection_cycle', 'year_month'], 'idx_inspection_cycle_period');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('equipment_inspections', function (Blueprint $table) {
|
||||||
|
$table->dropIndex('idx_inspection_cycle_period');
|
||||||
|
$table->dropUnique('uq_inspection_cycle_period');
|
||||||
|
|
||||||
|
$table->unique(['tenant_id', 'equipment_id', 'year_month'], 'uq_inspection_month');
|
||||||
|
$table->index(['tenant_id', 'year_month'], 'idx_inspection_ym');
|
||||||
|
|
||||||
|
$table->dropColumn('inspection_cycle');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('shipment_vehicle_dispatches', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('tenant_id')->comment('테넌트 ID');
|
||||||
|
$table->foreignId('shipment_id')->comment('출하 ID');
|
||||||
|
$table->integer('seq')->default(1)->comment('순번');
|
||||||
|
$table->string('logistics_company', 100)->nullable()->comment('물류사');
|
||||||
|
$table->datetime('arrival_datetime')->nullable()->comment('도착일시');
|
||||||
|
$table->string('tonnage', 20)->nullable()->comment('톤수');
|
||||||
|
$table->string('vehicle_no', 20)->nullable()->comment('차량번호');
|
||||||
|
$table->string('driver_contact', 50)->nullable()->comment('운전자 연락처');
|
||||||
|
$table->text('remarks')->nullable()->comment('비고');
|
||||||
|
$table->timestamps();
|
||||||
|
$table->softDeletes();
|
||||||
|
|
||||||
|
// 인덱스
|
||||||
|
$table->index(['shipment_id', 'seq']);
|
||||||
|
|
||||||
|
// 외래키
|
||||||
|
$table->foreign('shipment_id')->references('id')->on('shipments')->onDelete('cascade');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('shipment_vehicle_dispatches');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
* Phase 3.1: chandj.price_motor → items (SM) + prices 누락 품목
|
* Phase 3.1: chandj.price_motor → items (SM) + prices 누락 품목
|
||||||
* Phase 3.2: chandj.price_raw_materials → items (RM) + prices 누락 품목
|
* Phase 3.2: chandj.price_raw_materials → items (RM) + prices 누락 품목
|
||||||
*
|
*
|
||||||
* @see docs/plans/kd-items-migration-plan.md
|
* @see docs/dev_plans/kd-items-migration-plan.md
|
||||||
*/
|
*/
|
||||||
class KyungdongItemSeeder extends Seeder
|
class KyungdongItemSeeder extends Seeder
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -444,6 +444,15 @@
|
|||||||
'already_completed' => '이미 완료된 검사입니다.',
|
'already_completed' => '이미 완료된 검사입니다.',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// 품질관리서 관련
|
||||||
|
'quality' => [
|
||||||
|
'cannot_delete_completed' => '완료된 품질관리서는 삭제할 수 없습니다.',
|
||||||
|
'already_completed' => '이미 완료된 품질관리서입니다.',
|
||||||
|
'cannot_modify_completed' => '완료된 품질관리서는 수정할 수 없습니다.',
|
||||||
|
'pending_locations' => '미완료 개소가 :count건 있습니다.',
|
||||||
|
'confirm_failed' => '필수정보가 누락된 건이 있어 확정할 수 없습니다.',
|
||||||
|
],
|
||||||
|
|
||||||
// 입찰 관련
|
// 입찰 관련
|
||||||
'bidding' => [
|
'bidding' => [
|
||||||
'not_found' => '입찰을 찾을 수 없습니다.',
|
'not_found' => '입찰을 찾을 수 없습니다.',
|
||||||
@@ -481,4 +490,15 @@
|
|||||||
'cannot_delete' => '해당 계약은 삭제할 수 없습니다.',
|
'cannot_delete' => '해당 계약은 삭제할 수 없습니다.',
|
||||||
'invalid_status' => '유효하지 않은 계약 상태입니다.',
|
'invalid_status' => '유효하지 않은 계약 상태입니다.',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// 일반전표입력
|
||||||
|
'journal_entry' => [
|
||||||
|
'debit_credit_mismatch' => '차변 합계와 대변 합계가 일치해야 합니다.',
|
||||||
|
],
|
||||||
|
|
||||||
|
// 계정과목
|
||||||
|
'account_subject' => [
|
||||||
|
'duplicate_code' => '이미 존재하는 계정과목 코드입니다.',
|
||||||
|
'in_use' => '전표에서 사용 중인 계정과목은 삭제할 수 없습니다.',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -567,6 +567,22 @@
|
|||||||
'downloaded' => '문서가 다운로드되었습니다.',
|
'downloaded' => '문서가 다운로드되었습니다.',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// 일반전표입력
|
||||||
|
'journal_entry' => [
|
||||||
|
'fetched' => '전표 조회 성공',
|
||||||
|
'created' => '전표가 등록되었습니다.',
|
||||||
|
'updated' => '분개가 수정되었습니다.',
|
||||||
|
'deleted' => '분개가 삭제되었습니다.',
|
||||||
|
],
|
||||||
|
|
||||||
|
// 계정과목
|
||||||
|
'account_subject' => [
|
||||||
|
'fetched' => '계정과목 조회 성공',
|
||||||
|
'created' => '계정과목이 등록되었습니다.',
|
||||||
|
'toggled' => '계정과목 상태가 변경되었습니다.',
|
||||||
|
'deleted' => '계정과목이 삭제되었습니다.',
|
||||||
|
],
|
||||||
|
|
||||||
// CEO 대시보드 부가세 현황
|
// CEO 대시보드 부가세 현황
|
||||||
'vat' => [
|
'vat' => [
|
||||||
'sales_tax' => '매출세액',
|
'sales_tax' => '매출세액',
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
require __DIR__.'/api/v1/app.php';
|
require __DIR__.'/api/v1/app.php';
|
||||||
require __DIR__.'/api/v1/audit.php';
|
require __DIR__.'/api/v1/audit.php';
|
||||||
require __DIR__.'/api/v1/esign.php';
|
require __DIR__.'/api/v1/esign.php';
|
||||||
|
require __DIR__.'/api/v1/quality.php';
|
||||||
|
|
||||||
// 공유 링크 다운로드 (인증 불필요 - auth.apikey 그룹 밖)
|
// 공유 링크 다운로드 (인증 불필요 - auth.apikey 그룹 밖)
|
||||||
Route::get('/files/share/{token}', [FileStorageController::class, 'downloadShared'])->name('v1.files.share.download');
|
Route::get('/files/share/{token}', [FileStorageController::class, 'downloadShared'])->name('v1.files.share.download');
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
use App\Http\Controllers\Api\V1\CategoryTemplateController;
|
use App\Http\Controllers\Api\V1\CategoryTemplateController;
|
||||||
use App\Http\Controllers\Api\V1\ClassificationController;
|
use App\Http\Controllers\Api\V1\ClassificationController;
|
||||||
use App\Http\Controllers\Api\V1\CommonController;
|
use App\Http\Controllers\Api\V1\CommonController;
|
||||||
|
use App\Http\Controllers\Api\V1\DashboardCeoController;
|
||||||
use App\Http\Controllers\Api\V1\DashboardController;
|
use App\Http\Controllers\Api\V1\DashboardController;
|
||||||
use App\Http\Controllers\Api\V1\MenuController;
|
use App\Http\Controllers\Api\V1\MenuController;
|
||||||
use App\Http\Controllers\Api\V1\NotificationSettingController;
|
use App\Http\Controllers\Api\V1\NotificationSettingController;
|
||||||
@@ -225,4 +226,12 @@
|
|||||||
Route::get('/summary', [DashboardController::class, 'summary'])->name('v1.dashboard.summary');
|
Route::get('/summary', [DashboardController::class, 'summary'])->name('v1.dashboard.summary');
|
||||||
Route::get('/charts', [DashboardController::class, 'charts'])->name('v1.dashboard.charts');
|
Route::get('/charts', [DashboardController::class, 'charts'])->name('v1.dashboard.charts');
|
||||||
Route::get('/approvals', [DashboardController::class, 'approvals'])->name('v1.dashboard.approvals');
|
Route::get('/approvals', [DashboardController::class, 'approvals'])->name('v1.dashboard.approvals');
|
||||||
|
|
||||||
|
// CEO 대시보드 섹션별 API
|
||||||
|
Route::get('/sales/summary', [DashboardCeoController::class, 'salesSummary'])->name('v1.dashboard.ceo.sales');
|
||||||
|
Route::get('/purchases/summary', [DashboardCeoController::class, 'purchasesSummary'])->name('v1.dashboard.ceo.purchases');
|
||||||
|
Route::get('/production/summary', [DashboardCeoController::class, 'productionSummary'])->name('v1.dashboard.ceo.production');
|
||||||
|
Route::get('/unshipped/summary', [DashboardCeoController::class, 'unshippedSummary'])->name('v1.dashboard.ceo.unshipped');
|
||||||
|
Route::get('/construction/summary', [DashboardCeoController::class, 'constructionSummary'])->name('v1.dashboard.ceo.construction');
|
||||||
|
Route::get('/attendance/summary', [DashboardCeoController::class, 'attendanceSummary'])->name('v1.dashboard.ceo.attendance');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
use App\Http\Controllers\Api\V1\ReceivingController;
|
use App\Http\Controllers\Api\V1\ReceivingController;
|
||||||
use App\Http\Controllers\Api\V1\ShipmentController;
|
use App\Http\Controllers\Api\V1\ShipmentController;
|
||||||
use App\Http\Controllers\Api\V1\StockController;
|
use App\Http\Controllers\Api\V1\StockController;
|
||||||
|
use App\Http\Controllers\Api\V1\VehicleDispatchController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
// Items API (품목 관리)
|
// Items API (품목 관리)
|
||||||
@@ -123,3 +124,11 @@
|
|||||||
Route::patch('/{id}/status', [ShipmentController::class, 'updateStatus'])->whereNumber('id')->name('v1.shipments.status');
|
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');
|
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