commit ef0106cb1d25d3fa4a5738e8405c1fe4e5a66792 Author: devrzian Date: Sat Apr 18 12:00:45 2026 +0300 SU-010 diff --git a/app/Domains/Maintenance/Notifications/MaintenanceReportReadyForInvoicingNotification.php b/app/Domains/Maintenance/Notifications/MaintenanceReportReadyForInvoicingNotification.php new file mode 100644 index 000000000..9d3151ff9 --- /dev/null +++ b/app/Domains/Maintenance/Notifications/MaintenanceReportReadyForInvoicingNotification.php @@ -0,0 +1,49 @@ +report->visit->visit_number ?? $this->report->id; + $projectName = $this->report->visit->project->title ?? 'N/A'; + + return (new MailMessage) + ->subject('Visit Report Ready for Invoicing: ' . $visitNumber) + ->line('The visit report for project "' . $projectName . '" is now ready for invoicing.') + ->line('Visit Number: ' . $visitNumber) + ->line('Total Cost Subject to Invoicing: ' . number_format($this->report->total_cost, 2)) + ->action('View Report', url('/maintenance/reports/' . $this->report->id)) + ->line('Please review and proceed with the invoicing process.'); + } + + public function toArray($notifiable): array + { + return [ + 'report_id' => $this->report->id, + 'visit_number' => $this->report->visit->visit_number ?? null, + 'project_name' => $this->report->visit->project->title ?? 'N/A', + 'total_cost' => $this->report->total_cost, + 'title' => 'تقرير زيارة جاهز للفوترة', + 'message' => "تقرير الزيارة رقم {$this->report->visit->visit_number} جاهز للفوترة للمشروع: {$this->report->visit->project->title}", + ]; + } +} diff --git a/app/Domains/Maintenance/Routes/api.php b/app/Domains/Maintenance/Routes/api.php index e4c9bfd48..dcb41fd41 100644 --- a/app/Domains/Maintenance/Routes/api.php +++ b/app/Domains/Maintenance/Routes/api.php @@ -470,6 +470,9 @@ Route::post('/{id}/supply-request', 'createSupplyRequest')->name('create_supply_request')->middleware('permission:maintenance.reports.create_supply_request'); Route::post('/invoice', 'createInvoice')->name('create_invoice')->middleware('permission:maintenance.reports.create_invoice'); Route::get('/{id}/with-attachments', 'withAttachments')->name('with_attachments')->middleware('permission:maintenance.visits.view'); + Route::get('/{id}/faults', 'getFaults')->name('get_faults')->middleware('permission:maintenance.visits.view'); + Route::post('/{id}/faults', 'addFault')->name('add_fault')->middleware('permission:maintenance.visits.edit'); + Route::post('/{id}/faults/{faultId}/attachments', 'addFaultAttachment')->name('faults.attachments.store'); Route::get('/{id}/pdf', 'generatePDF')->name('generate_pdf')->middleware('permission:maintenance.visits.view'); Route::get('/{id}/change-log', 'getChangeLog')->name('change_log')->middleware('permission:maintenance.visits.view'); Route::get('/{id}/emergency-timeline', 'getEmergencyTimeline')->name('emergency_timeline')->middleware('permission:maintenance.visits.view'); diff --git a/app/Domains/Maintenance/Services/ReportApprovalService.php b/app/Domains/Maintenance/Services/ReportApprovalService.php index a5ac5bb31..6fd02d1ca 100644 --- a/app/Domains/Maintenance/Services/ReportApprovalService.php +++ b/app/Domains/Maintenance/Services/ReportApprovalService.php @@ -4,6 +4,7 @@ use App\Domains\Maintenance\SubDomains\Visits\Models\MaintenanceVisitReport; use App\Domains\Maintenance\SubDomains\VisitReports\Enums\VisitReportStatus; +use App\Domains\Maintenance\Notifications\MaintenanceReportReadyForInvoicingNotification; use App\Domains\Maintenance\SubDomains\Visits\Enums\VisitType; use App\Domains\Maintenance\Events\HandoverReportApproved; use App\Domains\Maintenance\Models\MaintenanceProjectAsset; @@ -29,7 +30,7 @@ public function approve(MaintenanceVisitReport $report, ?string $notes = null): DB::transaction(function () use ($report, $visit, $notes) { $report->update([ - 'status' => VisitReportStatus::APPROVED, // Use APPROVED for Handover/Maintenance + 'status' => VisitReportStatus::AWAITING_INVOICING, 'approval_notes' => $notes, 'approved_at' => now(), 'approved_by' => Auth::id(), @@ -68,6 +69,9 @@ public function approve(MaintenanceVisitReport $report, ?string $notes = null): // Notify technician $this->notifyTechnician($report, 'Visit Report Approved', "Your visit report #{$report->visit?->visit_number} has been approved."); + // Notify Finance (AC18) + $this->notifyFinance($report); + return $report->fresh(); } @@ -276,4 +280,25 @@ public function getStatistics(): array 'needs_procurement' => MaintenanceVisitReport::where('requires_procurement', true)->count(), ]; } + /** + * Notify Finance users when a report is ready for invoicing + */ + protected function notifyFinance(MaintenanceVisitReport $report): void + { + try { + $financeUsers = \App\Domains\Core\User\Models\User::role('finance')->get(); + + if ($financeUsers->isNotEmpty()) { + \Illuminate\Support\Facades\Notification::send( + $financeUsers, + new MaintenanceReportReadyForInvoicingNotification($report) + ); + } + } catch (\Exception $e) { + \Illuminate\Support\Facades\Log::error('Failed to notify Finance department', [ + 'report_id' => $report->id, + 'error' => $e->getMessage() + ]); + } + } } diff --git a/app/Domains/Maintenance/SubDomains/VisitChecklists/Http/Controllers/V1/MaintenanceVisitChecklistController.php b/app/Domains/Maintenance/SubDomains/VisitChecklists/Http/Controllers/V1/MaintenanceVisitChecklistController.php index 1ae9d26bc..83042d96d 100644 --- a/app/Domains/Maintenance/SubDomains/VisitChecklists/Http/Controllers/V1/MaintenanceVisitChecklistController.php +++ b/app/Domains/Maintenance/SubDomains/VisitChecklists/Http/Controllers/V1/MaintenanceVisitChecklistController.php @@ -250,6 +250,8 @@ public function updateItem(Request $request, $itemId): JsonResponse try { $validated = $request->validate([ 'question_text' => 'sometimes|required|string|max:1000', + 'question_text_ar' => 'nullable|string|max:1000', + 'question_text_en' => 'nullable|string|max:1000', 'question_type' => 'sometimes|required|in:yes_no,text,number,rating,checkbox', 'is_required' => 'sometimes|required|boolean', 'order_position' => 'nullable|integer|min:0', diff --git a/app/Domains/Maintenance/SubDomains/VisitChecklists/Http/Requests/V1/StoreMaintenanceChecklistItemRequest.php b/app/Domains/Maintenance/SubDomains/VisitChecklists/Http/Requests/V1/StoreMaintenanceChecklistItemRequest.php index eb0bde78d..4eb3d783a 100644 --- a/app/Domains/Maintenance/SubDomains/VisitChecklists/Http/Requests/V1/StoreMaintenanceChecklistItemRequest.php +++ b/app/Domains/Maintenance/SubDomains/VisitChecklists/Http/Requests/V1/StoreMaintenanceChecklistItemRequest.php @@ -16,6 +16,8 @@ public function rules(): array return [ 'maintenance_visit_checklist_id' => 'required|integer|exists:mod_maintenance_visit_checklists,id', 'question_text' => 'required|string|max:1000', + 'question_text_ar' => 'nullable|string|max:1000', + 'question_text_en' => 'nullable|string|max:1000', 'question_type' => 'required|in:yes_no,text,number,rating,checkbox', 'is_required' => 'required|boolean', 'order_position' => 'nullable|integer|min:0', diff --git a/app/Domains/Maintenance/SubDomains/VisitChecklists/Http/Requests/V1/StoreMaintenanceVisitChecklistRequest.php b/app/Domains/Maintenance/SubDomains/VisitChecklists/Http/Requests/V1/StoreMaintenanceVisitChecklistRequest.php index 5a55f3cc5..ce11f363c 100644 --- a/app/Domains/Maintenance/SubDomains/VisitChecklists/Http/Requests/V1/StoreMaintenanceVisitChecklistRequest.php +++ b/app/Domains/Maintenance/SubDomains/VisitChecklists/Http/Requests/V1/StoreMaintenanceVisitChecklistRequest.php @@ -23,6 +23,8 @@ public function rules(): array // Items 'items' => 'nullable|array|min:0', 'items.*.question_text' => 'required|string|max:1000', + 'items.*.question_text_ar' => 'nullable|string|max:1000', + 'items.*.question_text_en' => 'nullable|string|max:1000', 'items.*.question_type' => 'required|in:yes_no,text,number,rating,checkbox', 'items.*.is_required' => 'required|boolean', 'items.*.order_position' => 'nullable|integer|min:0', diff --git a/app/Domains/Maintenance/SubDomains/VisitChecklists/Http/Requests/V1/UpdateMaintenanceVisitChecklistRequest.php b/app/Domains/Maintenance/SubDomains/VisitChecklists/Http/Requests/V1/UpdateMaintenanceVisitChecklistRequest.php index d42370156..df8ea42be 100644 --- a/app/Domains/Maintenance/SubDomains/VisitChecklists/Http/Requests/V1/UpdateMaintenanceVisitChecklistRequest.php +++ b/app/Domains/Maintenance/SubDomains/VisitChecklists/Http/Requests/V1/UpdateMaintenanceVisitChecklistRequest.php @@ -24,6 +24,8 @@ public function rules(): array 'items' => 'nullable|array|min:0', 'items.*.id' => 'nullable|integer|exists:mod_maintenance_checklist_items,id', 'items.*.question_text' => 'required|string|max:1000', + 'items.*.question_text_ar' => 'nullable|string|max:1000', + 'items.*.question_text_en' => 'nullable|string|max:1000', 'items.*.question_type' => 'required|in:yes_no,text,number,rating,checkbox', 'items.*.is_required' => 'required|boolean', 'items.*.order_position' => 'nullable|integer|min:0', diff --git a/app/Domains/Maintenance/SubDomains/VisitChecklists/Http/Resources/V1/MaintenanceChecklistItemResource.php b/app/Domains/Maintenance/SubDomains/VisitChecklists/Http/Resources/V1/MaintenanceChecklistItemResource.php index 574f2b7c6..c1d07129f 100644 --- a/app/Domains/Maintenance/SubDomains/VisitChecklists/Http/Resources/V1/MaintenanceChecklistItemResource.php +++ b/app/Domains/Maintenance/SubDomains/VisitChecklists/Http/Resources/V1/MaintenanceChecklistItemResource.php @@ -11,6 +11,8 @@ public function toArray($request) return [ 'id' => $this->id, 'question_text' => $this->question_text, + 'question_text_ar' => $this->question_text_ar, + 'question_text_en' => $this->question_text_en, 'question_type' => $this->question_type, 'is_required' => $this->is_required, 'order_position' => $this->order_position, diff --git a/app/Domains/Maintenance/SubDomains/VisitChecklists/Models/MaintenanceChecklistItem.php b/app/Domains/Maintenance/SubDomains/VisitChecklists/Models/MaintenanceChecklistItem.php index c86ad1d88..b249aca7b 100644 --- a/app/Domains/Maintenance/SubDomains/VisitChecklists/Models/MaintenanceChecklistItem.php +++ b/app/Domains/Maintenance/SubDomains/VisitChecklists/Models/MaintenanceChecklistItem.php @@ -16,6 +16,8 @@ class MaintenanceChecklistItem extends BaseModel protected $fillable = [ 'maintenance_visit_checklist_id', 'question_text', + 'question_text_ar', + 'question_text_en', 'question_type', 'is_required', 'order_position', diff --git a/app/Domains/Maintenance/SubDomains/VisitReports/Enums/VisitReportStatus.php b/app/Domains/Maintenance/SubDomains/VisitReports/Enums/VisitReportStatus.php index 67a558b78..d8f074c4a 100644 --- a/app/Domains/Maintenance/SubDomains/VisitReports/Enums/VisitReportStatus.php +++ b/app/Domains/Maintenance/SubDomains/VisitReports/Enums/VisitReportStatus.php @@ -11,6 +11,7 @@ enum VisitReportStatus: string case AWAITING_INVOICING = 'awaiting_invoicing'; case AWAITING_PAYMENT = 'awaiting_payment'; case COMPLETED = 'completed'; + case DRAFT = 'draft'; /** * Get all values for validation @@ -33,6 +34,7 @@ public function labelAr(): string self::AWAITING_INVOICING => 'بانتظار فوترة', self::AWAITING_PAYMENT => 'بانتظار الدفع', self::COMPLETED => 'مكتملة', + self::DRAFT => 'مسودة', }; } @@ -49,6 +51,7 @@ public function labelEn(): string self::AWAITING_INVOICING => 'Awaiting Invoicing', self::AWAITING_PAYMENT => 'Awaiting Payment', self::COMPLETED => 'Completed', + self::DRAFT => 'Draft', }; } @@ -65,6 +68,7 @@ public function color(): string self::AWAITING_INVOICING => '#20c997', self::AWAITING_PAYMENT => '#e83e8c', self::COMPLETED => '#28a745', + self::DRAFT => '#6c757d', }; } diff --git a/app/Domains/Maintenance/SubDomains/Visits/Http/Controllers/V1/MaintenanceVisitReportController.php b/app/Domains/Maintenance/SubDomains/Visits/Http/Controllers/V1/MaintenanceVisitReportController.php index c3162a25b..064139371 100644 --- a/app/Domains/Maintenance/SubDomains/Visits/Http/Controllers/V1/MaintenanceVisitReportController.php +++ b/app/Domains/Maintenance/SubDomains/Visits/Http/Controllers/V1/MaintenanceVisitReportController.php @@ -12,6 +12,10 @@ use App\Domains\Maintenance\SubDomains\Visits\Http\Requests\V1\UpdateMaintenanceVisitDynamicQuestionRequest; use App\Domains\Maintenance\SubDomains\Visits\Http\Resources\V1\MaintenanceVisitDynamicQuestionResource; use App\Domains\Maintenance\SubDomains\Visits\Http\Resources\V1\MaintenanceVisitReportResource; +use App\Domains\Maintenance\SubDomains\Visits\Http\Requests\V1\StoreMaintenanceVisitFaultRequest; +use App\Domains\Maintenance\SubDomains\Visits\Services\MaintenanceVisitFaultService; +use App\Domains\Maintenance\SubDomains\Visits\Models\MaintenanceVisitFault; +use App\Domains\Core\TemporaryFile\Services\TemporaryFileServices; use App\Domains\Maintenance\SubDomains\Visits\Services\MaintenanceVisitReportService; use App\Domains\Maintenance\Services\ReportApprovalService; use App\Domains\Maintenance\Services\AutomaticProcurementService; @@ -32,7 +36,9 @@ class MaintenanceVisitReportController public function __construct( private MaintenanceVisitReportService $service, private ReportApprovalService $approvalService, - private AutomaticProcurementService $procurementService + private AutomaticProcurementService $procurementService, + private MaintenanceVisitFaultService $faultService, + private TemporaryFileServices $temporaryFileServices ) {} public function index(): JsonResponse @@ -686,4 +692,59 @@ public function getEmergencyTimeline($id): JsonResponse return $this->sendFailedResponse($e->getMessage(), 500); } } + public function addFault(StoreMaintenanceVisitFaultRequest $request, $id): JsonResponse + { + try { + $fault = $this->faultService->createFault($request->validated(), $id); + + return $this->sendSuccessResponse( + $fault, + 'تم إضافة بلاغ العطل بنجاح', + 201 + ); + } catch (Exception $e) { + return $this->sendFailedResponse($e->getMessage(), 422); + } + } + + public function getFaults($id): JsonResponse + { + try { + $faults = $this->faultService->getFaultsForReport($id); + + return $this->sendSuccessResponse( + $faults, + 'بلاغات الأعطال', + 200 + ); + } catch (Exception $e) { + return $this->sendFailedResponse($e->getMessage(), 500); + } + } + public function addFaultAttachment(Request $request, $id, $faultId): JsonResponse + { + try { + $fault = MaintenanceVisitFault::findOrFail($faultId); + + $request->validate([ + 'files' => 'required|array', + 'files.*.temporary_folder' => 'required|string', + ]); + + $temporaryFolders = collect($request->input('files')) + ->pluck('temporary_folder') + ->toArray(); + + $this->temporaryFileServices->moveTemporaryFilesToMedia( + $temporaryFolders, + $fault, + 'maintenance_report_attachments', + 'fault_photos' + ); + + return $this->sendSuccessResponse($fault->load('media'), 'تم إضافة المرفقات بنجاح'); + } catch (Exception $e) { + return $this->sendFailedResponse($e->getMessage(), 500); + } + } } diff --git a/app/Domains/Maintenance/SubDomains/Visits/Http/Requests/V1/StoreMaintenanceVisitFaultRequest.php b/app/Domains/Maintenance/SubDomains/Visits/Http/Requests/V1/StoreMaintenanceVisitFaultRequest.php new file mode 100644 index 000000000..e391ca73f --- /dev/null +++ b/app/Domains/Maintenance/SubDomains/Visits/Http/Requests/V1/StoreMaintenanceVisitFaultRequest.php @@ -0,0 +1,30 @@ + 'required|exists:mod_maintenance_visit_checklist_responses,id', + 'description' => 'required|string', + ]; + } + + public function messages(): array + { + return [ + 'checklist_item_answer_id.required' => 'يجب ربط العطل بسؤال من قائمة التحقق.', + 'checklist_item_answer_id.exists' => 'الإجابة المحددة غير موجودة.', + 'description.required' => 'وصف العطل مطلوب.', + ]; + } +} diff --git a/app/Domains/Maintenance/SubDomains/Visits/Http/Requests/V1/StoreMaintenanceVisitReportRequest.php b/app/Domains/Maintenance/SubDomains/Visits/Http/Requests/V1/StoreMaintenanceVisitReportRequest.php index 62330b45f..1603b51cd 100644 --- a/app/Domains/Maintenance/SubDomains/Visits/Http/Requests/V1/StoreMaintenanceVisitReportRequest.php +++ b/app/Domains/Maintenance/SubDomains/Visits/Http/Requests/V1/StoreMaintenanceVisitReportRequest.php @@ -19,7 +19,7 @@ public function rules(): array return [ 'maintenance_visit_id' => 'required|exists:mod_maintenance_visits,id', 'status' => ['required', new Enum(VisitReportStatus::class)], - 'work_performed' => 'required|string', + 'work_performed' => $this->input('status') === VisitReportStatus::DRAFT->value ? 'nullable|string' : 'required|string', 'observations' => 'nullable|string', 'recommendations' => 'nullable|string', 'report_payload' => 'nullable|array', diff --git a/app/Domains/Maintenance/SubDomains/Visits/Http/Requests/V1/UpdateMaintenanceVisitReportRequest.php b/app/Domains/Maintenance/SubDomains/Visits/Http/Requests/V1/UpdateMaintenanceVisitReportRequest.php index cd793aaa2..df02200ab 100644 --- a/app/Domains/Maintenance/SubDomains/Visits/Http/Requests/V1/UpdateMaintenanceVisitReportRequest.php +++ b/app/Domains/Maintenance/SubDomains/Visits/Http/Requests/V1/UpdateMaintenanceVisitReportRequest.php @@ -2,9 +2,9 @@ namespace App\Domains\Maintenance\SubDomains\Visits\Http\Requests\V1; +use App\Domains\Maintenance\SubDomains\VisitReports\Enums\VisitReportStatus; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rules\Enum; -use App\Domains\Maintenance\SubDomains\Visits\Enums\ReportStatus; class UpdateMaintenanceVisitReportRequest extends FormRequest { @@ -17,8 +17,10 @@ public function rules(): array { return [ 'maintenance_visit_id' => 'sometimes|exists:mod_maintenance_visits,id', - 'status' => ['sometimes', new Enum(ReportStatus::class)], - 'report_content' => 'sometimes|string', + 'status' => ['sometimes', new Enum(VisitReportStatus::class)], + 'work_performed' => 'sometimes|string', + 'observations' => 'sometimes|string', + 'recommendations' => 'sometimes|string', 'submitted_by' => 'sometimes|exists:users,id', 'submitted_at' => 'nullable|date', 'visit_started_at' => 'nullable|date', diff --git a/app/Domains/Maintenance/SubDomains/Visits/Http/Resources/V1/MaintenanceVisitReportResource.php b/app/Domains/Maintenance/SubDomains/Visits/Http/Resources/V1/MaintenanceVisitReportResource.php index fa23ff7ab..451bb7868 100644 --- a/app/Domains/Maintenance/SubDomains/Visits/Http/Resources/V1/MaintenanceVisitReportResource.php +++ b/app/Domains/Maintenance/SubDomains/Visits/Http/Resources/V1/MaintenanceVisitReportResource.php @@ -18,7 +18,9 @@ public function toArray(Request $request): array 'work_performed' => $this->work_performed, 'observations' => $this->observations, 'recommendations' => $this->recommendations, + 'fault_description_deprecated' => $this->fault_description, // DEPRECATED: Use checklist_faults instead 'report_payload' => $this->report_payload, + 'checklist_faults' => $this->whenLoaded('faults'), 'submitted_at' => $this->submitted_at?->toIso8601String(), 'visit_started_at' => $this->visit_started_at?->toIso8601String(), 'visit_completed_at' => $this->visit_completed_at?->toIso8601String(), diff --git a/app/Domains/Maintenance/SubDomains/Visits/Http/Resources/V1/MaintenanceVisitSparePartResource.php b/app/Domains/Maintenance/SubDomains/Visits/Http/Resources/V1/MaintenanceVisitSparePartResource.php index 768049217..87f7a588b 100644 --- a/app/Domains/Maintenance/SubDomains/Visits/Http/Resources/V1/MaintenanceVisitSparePartResource.php +++ b/app/Domains/Maintenance/SubDomains/Visits/Http/Resources/V1/MaintenanceVisitSparePartResource.php @@ -24,8 +24,12 @@ public function toArray(Request $request): array 'maintenance_visit_id' => $this->maintenance_visit_id, 'maintenance_asset_id' => $this->maintenance_asset_id, 'spare_part_name' => $this->part_name, - 'part_number' => $partNumber, + 'part_number' => $this->part_number ?? $partNumber, + 'brand' => $this->brand, + 'make' => $this->make, + 'model' => $this->model, 'quantity' => (float) $this->quantity, + 'quantity_used' => (float) $this->quantity_used, 'unit_cost' => $this->estimated_unit_price ? (float) $this->estimated_unit_price : null, 'total_cost' => $this->estimated_total_price ? (float) $this->estimated_total_price : ($this->estimated_unit_price && $this->quantity ? (float) ($this->estimated_unit_price * $this->quantity) : null), 'unit_of_measure' => $this->unit_of_measure, diff --git a/app/Domains/Maintenance/SubDomains/Visits/Models/MaintenanceVisitFault.php b/app/Domains/Maintenance/SubDomains/Visits/Models/MaintenanceVisitFault.php new file mode 100644 index 000000000..16f8e8d67 --- /dev/null +++ b/app/Domains/Maintenance/SubDomains/Visits/Models/MaintenanceVisitFault.php @@ -0,0 +1,32 @@ +belongsTo(MaintenanceVisitReport::class, 'visit_report_id'); + } + + public function checklistResponse(): BelongsTo + { + return $this->belongsTo(MaintenanceVisitChecklistResponse::class, 'checklist_item_answer_id'); + } +} diff --git a/app/Domains/Maintenance/SubDomains/Visits/Models/MaintenanceVisitReport.php b/app/Domains/Maintenance/SubDomains/Visits/Models/MaintenanceVisitReport.php index cfee9ae6e..c9640d95c 100644 --- a/app/Domains/Maintenance/SubDomains/Visits/Models/MaintenanceVisitReport.php +++ b/app/Domains/Maintenance/SubDomains/Visits/Models/MaintenanceVisitReport.php @@ -100,6 +100,11 @@ public function supplyOrder() return $this->hasOne(SupplyOrder::class, 'maintenance_visit_report_id'); } + public function faults() + { + return $this->hasMany(MaintenanceVisitFault::class, 'visit_report_id'); + } + /** * Invoices Relationship - Invoices related to this report */ diff --git a/app/Domains/Maintenance/SubDomains/Visits/Models/MaintenanceVisitSparePart.php b/app/Domains/Maintenance/SubDomains/Visits/Models/MaintenanceVisitSparePart.php index b8a94dd26..f27c4e626 100644 --- a/app/Domains/Maintenance/SubDomains/Visits/Models/MaintenanceVisitSparePart.php +++ b/app/Domains/Maintenance/SubDomains/Visits/Models/MaintenanceVisitSparePart.php @@ -24,8 +24,11 @@ class MaintenanceVisitSparePart extends BaseModel 'part_name', 'part_number', 'brand', + 'make', + 'model', 'origin', 'quantity', + 'quantity_used', 'unit_of_measure', 'source', 'approval_status', @@ -39,6 +42,7 @@ class MaintenanceVisitSparePart extends BaseModel 'source' => SparePartSource::class, 'approval_status' => ApprovalStatus::class, 'quantity' => 'decimal:3', + 'quantity_used' => 'decimal:3', 'estimated_unit_price' => 'decimal:2', 'estimated_total_price' => 'decimal:2', 'is_for_next_visit' => 'boolean', diff --git a/app/Domains/Maintenance/SubDomains/Visits/Services/MaintenanceVisitFaultService.php b/app/Domains/Maintenance/SubDomains/Visits/Services/MaintenanceVisitFaultService.php new file mode 100644 index 000000000..d441cd558 --- /dev/null +++ b/app/Domains/Maintenance/SubDomains/Visits/Services/MaintenanceVisitFaultService.php @@ -0,0 +1,49 @@ +is_compliant === true) { + throw ValidationException::withMessages([ + 'checklist_item_answer_id' => ['يمكن إضافة بلاغ عطل فقط للإجابات السلبية.'], + ]); + } + + $data['visit_report_id'] = $visitReportId; + + return MaintenanceVisitFault::create($data); + } + + /** + * Get all faults for a visit report. + * + * @param int $visitReportId + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getFaultsForReport(int $visitReportId) + { + return MaintenanceVisitFault::with(['checklistResponse.item', 'media']) + ->where('visit_report_id', $visitReportId) + ->get(); + } +} diff --git a/app/Domains/Maintenance/SubDomains/Visits/Services/MaintenanceVisitReportService.php b/app/Domains/Maintenance/SubDomains/Visits/Services/MaintenanceVisitReportService.php index edc6187ec..848e826a1 100644 --- a/app/Domains/Maintenance/SubDomains/Visits/Services/MaintenanceVisitReportService.php +++ b/app/Domains/Maintenance/SubDomains/Visits/Services/MaintenanceVisitReportService.php @@ -5,6 +5,11 @@ use App\Domains\Maintenance\SubDomains\Visits\Contracts\MaintenanceVisitReportRepositoryContract; use Exception; use Illuminate\Support\Facades\DB; +use App\Domains\Maintenance\Notifications\MaintenanceReportReadyForInvoicingNotification; +use App\Domains\Maintenance\SubDomains\VisitReports\Enums\VisitReportStatus; +use App\Domains\Core\User\Models\User; +use Illuminate\Support\Facades\Notification; +use Illuminate\Support\Facades\Log; class MaintenanceVisitReportService { @@ -182,7 +187,7 @@ public function createReport(array $data) } // Validate required checklist items before creating report if status is awaiting_approval or completed - if (isset($data['status']) && in_array($data['status'], ['awaiting_approval', 'completed'])) { + if (isset($data['status']) && in_array($data['status'], ['awaiting_approval', 'completed', 'awaiting_invoicing'])) { $validation = $this->checklistService->validateRequiredResponses($data['maintenance_visit_id']); if (!$validation['valid']) { @@ -202,6 +207,10 @@ public function createReport(array $data) $report = $this->repository->create($data); + if (isset($data['status']) && $data['status'] === VisitReportStatus::AWAITING_INVOICING->value) { + $this->notifyFinance($report); + } + DB::commit(); return $report; } catch (Exception $e) { @@ -263,7 +272,7 @@ public function updateReport($id, array $data) } // Validate if changing to awaiting_approval or completed status - if (isset($data['status']) && in_array($data['status'], ['awaiting_approval', 'completed'])) { + if (isset($data['status']) && in_array($data['status'], ['awaiting_approval', 'completed', 'awaiting_invoicing']) && $report->status->value === 'draft') { $validation = $this->checklistService->validateRequiredResponses($report->maintenance_visit_id); if (!$validation['valid']) { @@ -283,6 +292,10 @@ public function updateReport($id, array $data) $updated = $this->repository->update($id, $data); + if (isset($data['status']) && $data['status'] === VisitReportStatus::AWAITING_INVOICING->value) { + $this->notifyFinance($updated); + } + // Log activity (AC14) activity() ->performedOn($updated) @@ -464,4 +477,23 @@ public function getDynamicQuestionsByVisit($visitId) ->orderBy('order_position') ->get(); } + + /** + * Notify Finance users when a report is ready for invoicing + */ + protected function notifyFinance($report): void + { + try { + $financeUsers = User::role('finance')->get(); + + if ($financeUsers->isNotEmpty()) { + Notification::send($financeUsers, new MaintenanceReportReadyForInvoicingNotification($report)); + } + } catch (Exception $e) { + Log::error('Failed to notify Finance department', [ + 'report_id' => $report->id, + 'error' => $e->getMessage() + ]); + } + } } diff --git a/database/migrations/2026_04_18_114814_add_bilingual_fields_to_maintenance_checklist_items_table.php b/database/migrations/2026_04_18_114814_add_bilingual_fields_to_maintenance_checklist_items_table.php new file mode 100644 index 000000000..09194b89e --- /dev/null +++ b/database/migrations/2026_04_18_114814_add_bilingual_fields_to_maintenance_checklist_items_table.php @@ -0,0 +1,29 @@ +string('question_text_ar')->nullable()->after('question_text'); + $blueprint->string('question_text_en')->nullable()->after('question_text_ar'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('mod_maintenance_checklist_items', function (Blueprint $blueprint) { + $blueprint->dropColumn(['question_text_ar', 'question_text_en']); + }); + } +}; diff --git a/database/migrations/2026_04_18_114815_create_mod_maintenance_visit_faults_table.php b/database/migrations/2026_04_18_114815_create_mod_maintenance_visit_faults_table.php new file mode 100644 index 000000000..b418f89e2 --- /dev/null +++ b/database/migrations/2026_04_18_114815_create_mod_maintenance_visit_faults_table.php @@ -0,0 +1,30 @@ +id(); + $table->foreignId('visit_report_id')->constrained('mod_maintenance_visit_reports')->onDelete('cascade'); + $table->foreignId('checklist_item_answer_id')->constrained('mod_maintenance_visit_checklist_responses')->onDelete('cascade'); + $table->text('description'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('mod_maintenance_visit_faults'); + } +}; diff --git a/database/migrations/2026_04_18_114816_add_attributes_to_maintenance_visit_spare_parts_table.php b/database/migrations/2026_04_18_114816_add_attributes_to_maintenance_visit_spare_parts_table.php new file mode 100644 index 000000000..71e99342b --- /dev/null +++ b/database/migrations/2026_04_18_114816_add_attributes_to_maintenance_visit_spare_parts_table.php @@ -0,0 +1,30 @@ +string('make')->nullable()->after('brand'); + $table->string('model')->nullable()->after('make'); + $table->decimal('quantity_used', 12, 3)->nullable()->after('quantity'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('mod_maintenance_visit_spare_parts', function (Blueprint $table) { + $table->dropColumn(['make', 'model', 'quantity_used']); + }); + } +};