From e8acdd330300001b077d6f7b7cad2fb83e7585e2 Mon Sep 17 00:00:00 2001 From: Fiqh Pratama Date: Mon, 13 Oct 2025 16:42:05 +0700 Subject: [PATCH] update form others --- .../Controllers/Forms/FormOtherController.php | 365 +++++++++++--- .../pages/forms/other/create.blade.php | 336 +++++++++++-- .../backend/pages/forms/other/edit.blade.php | 463 +++++++++++++++++- .../backend/pages/forms/other/view.blade.php | 335 +++++++++---- routes/web.php | 1 + 5 files changed, 1275 insertions(+), 225 deletions(-) diff --git a/app/Http/Controllers/Forms/FormOtherController.php b/app/Http/Controllers/Forms/FormOtherController.php index 21b005f..416fdfb 100644 --- a/app/Http/Controllers/Forms/FormOtherController.php +++ b/app/Http/Controllers/Forms/FormOtherController.php @@ -18,7 +18,6 @@ use App\Mail\FinalApprove; use App\Mail\ExpenseCreated; use App\Helpers\WhatsappHelper; use App\Helpers\AuditTrailHelper; -use App\Rules\FileType; use App\Models\Admin; use App\Helpers\BudgetHelper; use App\Helpers\NotificationHelper; @@ -26,10 +25,30 @@ use App\Models\Cabang; use App\Helpers\OutstandingHelper; use Illuminate\Support\Facades\Log; use App\Services\BrevoService; +use App\Services\AttachmentService; +use App\Enums\AttachmentTableName; +use App\Models\AttachmentForm; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Validator; +use Illuminate\Validation\ValidationException; use Carbon\Carbon; class FormOtherController extends Controller { + protected AttachmentService $attachmentService; + + protected array $attachmentCategories = [ + 'bukti_total', + ]; + + protected array $blockedExtensions = ['exe', 'dll', 'sh', 'bat', 'cmd', 'msi']; + + public function __construct(AttachmentService $attachmentService) + { + $this->attachmentService = $attachmentService; + view()->share('otherAttachmentCategories', $this->attachmentCategories); + } + public function index() { $this->checkAuthorization(auth()->user(), ['forms.other.view']); @@ -117,22 +136,27 @@ class FormOtherController extends Controller { $this->checkAuthorization(auth()->user(), ['forms.other.view']); - $form = FormOthers::with(['user', 'kategori'])->findOrFail($id); + $form = FormOthers::with(['user', 'kategori', 'attachments'])->findOrFail($id); + $attachments = $this->formatAttachmentCollection($form); $form->bukti_total = $form->bukti_total ? NextCloudHelper::getFileUrl($form->bukti_total) : null; - return response()->json($form); + return response()->json(array_merge($form->toArray(), [ + 'attachments' => $attachments, + ])); } public function view($id) { $this->checkAuthorization(auth()->user(), ['forms.other.view']); - $form = FormOthers::with(['user', 'kategori'])->findOrFail($id); + $form = FormOthers::with(['user', 'kategori', 'attachments'])->findOrFail($id); + $attachments = $this->formatAttachmentCollection($form); $form->bukti_total = $form->bukti_total ? NextCloudHelper::getFileUrl($form->bukti_total) : null; return view('backend.pages.forms.other.view', [ 'form' => $form, - 'kategori' => Kategori::whereNotIn('name', ['Up Country', 'Vehicle Running Cost', 'Entertainment', 'Meeting / Seminar'])->get() + 'kategori' => Kategori::whereNotIn('name', ['Up Country', 'Vehicle Running Cost', 'Entertainment', 'Meeting / Seminar'])->get(), + 'attachments' => $attachments, ]); } @@ -141,21 +165,50 @@ class FormOtherController extends Controller $this->checkAuthorization(auth()->user(), ['forms.other.create']); return view('backend.pages.forms.other.create', [ - 'kategori' => Kategori::whereNotIn('name', ['Up Country', 'Vehicle Running Cost', 'Entertainment','Presentation','Sponsorship', 'Meeting / Seminar'])->get() + 'kategori' => Kategori::whereNotIn('name', ['Up Country', 'Vehicle Running Cost', 'Entertainment','Presentation','Sponsorship', 'Meeting / Seminar'])->get(), + 'attachmentCategories' => $this->attachmentCategories, ]); } public function store(Request $request) { $this->checkAuthorization(auth()->user(), ['forms.other.create']); - $request->validate([ + $validator = Validator::make($request->all(), [ 'kategori_id' => 'required', 'tanggal' => 'required|date', 'keterangan' => 'required', 'total' => 'required|numeric|min:1', - 'bukti_total' => ['nullable', 'file', 'max:51200'], + 'attachments' => 'nullable|array', + 'attachments.*.file_category' => 'required|string|in:' . implode(',', $this->attachmentCategories), + 'attachments.*.file_path' => [ + 'nullable', + 'file', + 'max:10240', + 'mimetypes:image/jpeg,image/png,image/webp,application/pdf,application/msword,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,text/plain,text/csv,application/zip,application/x-zip-compressed,application/x-7z-compressed,application/vnd.rar,application/octet-stream' + ], ]); + $validator->after(function ($validator) use ($request) { + $inputAttachments = $request->input('attachments', []); + $fileAttachments = $request->file('attachments', []); + + foreach ($inputAttachments as $index => $input) { + $category = $input['file_category'] ?? null; + $file = $fileAttachments[$index]['file_path'] ?? null; + + if ($category && !$file) { + $validator->errors()->add("attachments.{$index}.file_path", 'Attachment file is required.'); + continue; + } + + if ($file && $this->isBlockedExtension($file->getClientOriginalExtension())) { + $validator->errors()->add("attachments.{$index}.file_path", 'File type is not allowed.'); + } + } + }); + + $validator->validate(); + $tanggal = Carbon::parse($request->tanggal); $startDay = (int) env('STARTING_DATE', 11); $closingDay = (int) env('CLOSING_DATE', 10); @@ -204,39 +257,51 @@ class FormOtherController extends Controller $region = Region::where('id', $cabang->region_id)->first(); $kategori = Kategori::where('id', $request->kategori_id)->first(); - $filePaths = []; $folderPath = '/expense/' . $region->code . '/' . $cabang->code . '/other'; - - if ($request->hasFile('bukti_total')) { - $bukti_total = $request->file('bukti_total'); - $filename = Str::uuid() . '.' . $bukti_total->extension(); - - $filePaths['bukti_total'] = $folderPath . '/' . $filename; - - NextCloudHelper::uploadFile( - $folderPath, - $bukti_total->getContent(), - $filename - ); - } + $attachmentsPayload = $this->buildAttachmentPayload($request, $folderPath); + $primaryAttachmentPath = $attachmentsPayload[0]['file_path'] ?? null; + $primaryAttachmentPath = $attachmentsPayload[0]['file_path'] ?? null; // Generate sequence number and expense number $sequence_number = FormOthers::withTrashed()->where('expense_number', 'like', $region->code . '-' . $cabang->code . '-OTH-' . date('ym') . '%')->count() + 1; $formatted_sequence_number = str_pad($sequence_number, 4, '0', STR_PAD_LEFT); $expense_number = $region->code . '-' . $cabang->code . '-OTH-' . date('ym') . $formatted_sequence_number; - // Save the form data - $form = FormOthers::create([ - 'expense_number' => $expense_number, - 'user_id' => auth()->user()->id, - 'kategori_id' => $request->kategori_id, - 'tanggal' => $request->tanggal, - 'keterangan' => $request->keterangan, - 'total' => $request->total, - 'bukti_total' => $filePaths['bukti_total'] ?? null, - 'account_number' => $kategori->account_number, - 'status' => 'On Progress', - ]); + DB::beginTransaction(); + try { + $form = FormOthers::create([ + 'expense_number' => $expense_number, + 'user_id' => auth()->user()->id, + 'kategori_id' => $request->kategori_id, + 'tanggal' => $request->tanggal, + 'keterangan' => $request->keterangan, + 'total' => $request->total, + 'bukti_total' => $primaryAttachmentPath, + 'account_number' => $kategori->account_number, + 'status' => 'On Progress', + ]); + + if (!empty($attachmentsPayload)) { + $this->attachmentService->addMultipleAttachments( + $form->id, + AttachmentTableName::FORM_OTHERS, + $attachmentsPayload + ); + } + + DB::commit(); + } catch (\Throwable $th) { + DB::rollBack(); + Log::error('Failed to store Form Others', [ + 'message' => $th->getMessage(), + 'trace' => $th->getTraceAsString(), + ]); + + session()->flash('error', 'Terjadi kesalahan saat menyimpan data, silakan coba lagi.'); + return redirect()->back()->withInput(); + } + + $form->load('user'); $name = auth()->user()->name; $tanggal = $request->tanggal; @@ -323,7 +388,7 @@ class FormOtherController extends Controller $this->checkAuthorization(auth()->user(), ['forms.other.edit']); $role = auth()->user()->getRoleNames()[0]; - $form = FormOthers::findOrfail($id); + $form = FormOthers::with('attachments')->findOrfail($id); if($form->status != 'On Progress' && $role == 'Medical Representatif') { session()->flash('error', 'You cannot edit this form because it has been approved or rejected.'); return redirect()->back(); @@ -331,15 +396,17 @@ class FormOtherController extends Controller return view('backend.pages.forms.other.edit', [ 'form' => $form, - 'kategori' => Kategori::whereNotIn('name', ['Up Country', 'Vehicle Running Cost', 'Entertainment','Presentation','Sponsorship', 'Meeting / Seminar'])->get() - ]); + 'kategori' => Kategori::whereNotIn('name', ['Up Country', 'Vehicle Running Cost', 'Entertainment','Presentation','Sponsorship', 'Meeting / Seminar'])->get(), + 'attachments' => $this->formatAttachmentCollection($form), + 'attachmentCategories' => $this->attachmentCategories, + ]); } public function update(Request $request, $id) { $this->checkAuthorization(auth()->user(), ['forms.other.edit']); $role = auth()->user()->getRoleNames()[0]; - $form = FormOthers::findOrFail($id); + $form = FormOthers::with('attachments')->findOrFail($id); if ($form->status !== 'On Progress' && $role === 'Medical Representatif') { session()->flash('error', 'You cannot edit this form because it has been approved or rejected.'); @@ -368,48 +435,122 @@ class FormOtherController extends Controller // Ambil available budget $availableBudget = BudgetHelper::getAvailableBudget($cabang->id, $periodeMonth, $periodeYear); - // Validasi input - $request->validate([ + $validator = Validator::make($request->all(), [ 'kategori_id' => 'required', 'tanggal' => 'required|date', 'keterangan' => 'required', 'total' => 'required|numeric|min:1|max:' . $availableBudget, - 'bukti_total' => ['nullable', 'file', 'max:51200'], + 'attachments' => 'nullable|array', + 'attachments.*.file_category' => 'required|string|in:' . implode(',', $this->attachmentCategories), + 'attachments.*.file_path' => [ + 'nullable', + 'file', + 'max:10240', + 'mimetypes:image/jpeg,image/png,image/webp,application/pdf,application/msword,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,text/plain,text/csv,application/zip,application/x-zip-compressed,application/x-7z-compressed,application/vnd.rar,application/octet-stream' + ], ]); + $validator->after(function ($validator) use ($request) { + $inputAttachments = $request->input('attachments', []); + $fileAttachments = $request->file('attachments', []); + + foreach ($inputAttachments as $index => $input) { + $category = $input['file_category'] ?? null; + $file = $fileAttachments[$index]['file_path'] ?? null; + + if ($category && !$file) { + $validator->errors()->add("attachments.{$index}.file_path", 'Attachment file is required.'); + continue; + } + + if ($file && $this->isBlockedExtension($file->getClientOriginalExtension())) { + $validator->errors()->add("attachments.{$index}.file_path", 'File type is not allowed.'); + } + } + }); + + $validator->validate(); + $region = Region::find($cabang->region_id); $kategori = Kategori::find($request->kategori_id); - $filePaths = []; $folderPath = '/expense/' . $region->code . '/' . $cabang->code . '/other'; + $attachmentsPayload = $this->buildAttachmentPayload($request, $folderPath); - if ($request->hasFile('bukti_total')) { - $bukti_total = $request->file('bukti_total'); - $filename = Str::uuid() . '.' . $bukti_total->extension(); - $filePaths['bukti_total'] = $folderPath . '/' . $filename; + DB::beginTransaction(); + try { + $form->update([ + 'kategori_id' => $request->kategori_id, + 'tanggal' => $request->tanggal, + 'keterangan' => $request->keterangan, + 'total' => $request->total, + 'bukti_total' => $primaryAttachmentPath ?? $form->bukti_total, + 'account_number' => $kategori->account_number, + ]); - NextCloudHelper::uploadFile( - $folderPath, - $bukti_total->getContent(), - $filename - ); + if (!empty($attachmentsPayload)) { + $this->attachmentService->addMultipleAttachments( + $form->id, + AttachmentTableName::FORM_OTHERS, + $attachmentsPayload + ); + } + + DB::commit(); + } catch (\Throwable $th) { + DB::rollBack(); + Log::error('Failed to update Form Others', [ + 'form_id' => $form->id, + 'message' => $th->getMessage(), + 'trace' => $th->getTraceAsString(), + ]); + + session()->flash('error', 'Terjadi kesalahan saat memperbarui data, silakan coba lagi.'); + return redirect()->back()->withInput(); } - // Update form - $form->update([ - 'kategori_id' => $request->kategori_id, - 'tanggal' => $request->tanggal, - 'keterangan' => $request->keterangan, - 'total' => $request->total, - 'bukti_total' => $filePaths['bukti_total'] ?? $form->bukti_total, - 'account_number' => $kategori->account_number, - ]); - AuditTrailHelper::AddAuditTrail('Update', 'Update Record at Form Other (' . $form->expense_number . ')'); session()->flash('success', 'Form has been updated.'); return redirect()->back(); } + public function deleteAttachment(Request $request, $formId, $attachmentId) + { + $this->checkAuthorization(auth()->user(), ['forms.other.edit']); + + $form = FormOthers::findOrFail($formId); + $role = auth()->user()->getRoleNames()[0]; + + if (($form->status !== 'On Progress' && $form->status !== 'Rejected') && $role === 'Medical Representatif') { + return response()->json(['message' => 'You cannot modify attachments once the form is approved or closed.'], 403); + } + + $attachment = AttachmentForm::where('id', $attachmentId) + ->where('form_id', $form->id) + ->where('table_name', AttachmentTableName::FORM_OTHERS) + ->first(); + + if (!$attachment) { + return response()->json(['message' => 'Attachment not found.'], 404); + } + + $this->attachmentService->deleteAttachment($attachment->id); + + if ($form->bukti_total === $attachment->file_path) { + $replacement = AttachmentForm::where('form_id', $form->id) + ->where('table_name', AttachmentTableName::FORM_OTHERS) + ->whereNull('deleted_at') + ->orderBy('id') + ->first(); + + $form->update(['bukti_total' => $replacement->file_path ?? null]); + } + + AuditTrailHelper::AddAuditTrail('Delete Attachment', 'Delete Attachment at Form Other (' . $form->expense_number . ')'); + + return response()->json(['message' => 'Attachment deleted successfully.']); + } + public function approve($id) { $this->checkAuthorization(auth()->user(), ['approval.approve']); @@ -812,4 +953,106 @@ class FormOtherController extends Controller session()->flash('success', 'Form has been deleted.'); return redirect()->back(); } + + protected function formatAttachmentCollection(FormOthers $form): array + { + $attachments = $form->attachments->map(function ($attachment) { + return $this->formatAttachmentData( + $attachment->id, + $attachment->file_category, + $attachment->file_path + ); + }); + + if ($attachments->isEmpty() && $form->bukti_total) { + $attachments->push($this->formatAttachmentData(null, 'bukti_total', $form->bukti_total)); + } + + return $attachments->values()->all(); + } + + protected function formatAttachmentData(?int $id, ?string $category, string $filePath): array + { + $extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION)); + $previewType = $this->determinePreviewType($extension); + + return [ + 'id' => $id, + 'file_category' => $category, + 'file_path' => $filePath, + 'download_url' => NextCloudHelper::getFileUrl($filePath), + 'preview_url' => in_array($previewType, ['image', 'pdf'], true) ? NextCloudHelper::getPreviewUrl($filePath) : null, + 'preview_type' => $previewType, + 'filename' => basename($filePath), + 'extension' => $extension, + 'can_delete' => !is_null($id), + ]; + } + + protected function determinePreviewType(?string $extension): string + { + $extension = strtolower((string) $extension); + $imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']; + + if (in_array($extension, $imageExtensions, true)) { + return 'image'; + } + + if ($extension === 'pdf') { + return 'pdf'; + } + + return 'other'; + } + + protected function isBlockedExtension(?string $extension): bool + { + return in_array(strtolower((string) $extension), $this->blockedExtensions, true); + } + + protected function buildAttachmentPayload(Request $request, string $folderPath): array + { + $payload = []; + $inputs = $request->input('attachments', []); + $files = $request->file('attachments', []); + + foreach ($inputs as $index => $input) { + $category = $input['file_category'] ?? null; + $file = $files[$index]['file_path'] ?? null; + + if (!$category || !$file) { + continue; + } + + $extension = strtolower($file->getClientOriginalExtension()); + + if ($this->isBlockedExtension($extension)) { + throw ValidationException::withMessages([ + "attachments.{$index}.file_path" => 'File type is not allowed.', + ]); + } + + $filename = Str::uuid() . '.' . $extension; + $filePath = $folderPath . '/' . $filename; + + $uploadResponse = NextCloudHelper::uploadFile( + $folderPath, + $file->getContent(), + $filename + ); + + if (is_array($uploadResponse) && isset($uploadResponse['success']) && $uploadResponse['success'] === false) { + throw ValidationException::withMessages([ + "attachments.{$index}.file_path" => $uploadResponse['message'] ?? 'Failed to upload attachment.', + ]); + } + + $payload[] = [ + 'file_category' => $category, + 'file_path' => $filePath, + ]; + } + + return $payload; + } } diff --git a/resources/views/backend/pages/forms/other/create.blade.php b/resources/views/backend/pages/forms/other/create.blade.php index 438794c..684d585 100644 --- a/resources/views/backend/pages/forms/other/create.blade.php +++ b/resources/views/backend/pages/forms/other/create.blade.php @@ -65,50 +65,61 @@ - - - - - -
+
-
- - - -
-
- - -
- - -
- + +
+
+
+ + +
+

+ Maksimal 10 MB per file. Tidak diperbolehkan mengunggah file berformat .exe, .dll, .sh, .bat, .cmd, atau .msi. +

+
+ + + + + + + + + + + + + + +
KategoriLampiranPreviewAksi
Belum ada lampiran.
+
+
+
+ +
+ +
+ + @include('backend.components.attachment-preview-modal', [ + 'modalId' => 'otherAttachmentPreviewModal', + 'title' => 'Preview Lampiran', + ]) +
@@ -123,19 +134,264 @@ @endsection diff --git a/resources/views/backend/pages/forms/other/edit.blade.php b/resources/views/backend/pages/forms/other/edit.blade.php index 5d1fed8..5a49e79 100644 --- a/resources/views/backend/pages/forms/other/edit.blade.php +++ b/resources/views/backend/pages/forms/other/edit.blade.php @@ -66,16 +66,12 @@
-
- - -
- - + +
@@ -83,10 +79,108 @@
- +
+
+ +

Gunakan tombol preview untuk melihat lampiran yang sudah diunggah.

+
+ + + + + + + + + + + @forelse ($attachments as $attachment) + + + + + + + @empty + + + + @endforelse + +
KategoriNama FilePreviewAksi
{{ $attachment['file_category'] ? ucwords(str_replace('_', ' ', $attachment['file_category'])) : '-' }}{{ $attachment['filename'] ?? basename($attachment['file_path']) }} + + + + Download + + @if($attachment['can_delete']) + + @endif +
Belum ada lampiran.
+
+
+
+ +
+
+
+ + +
+

+ Maksimal 10 MB per file. File berformat .exe, .dll, .sh, .bat, .cmd, atau .msi tidak diperbolehkan. +

+
+ + + + + + + + + + + + + + +
KategoriLampiranPreviewAksi
Belum ada lampiran baru.
+
+
+
+ +
+ +
+ @include('backend.components.attachment-preview-modal', [ + 'modalId' => 'otherExistingAttachmentPreviewModal', + 'title' => 'Preview Lampiran', + ]) + + @include('backend.components.attachment-preview-modal', [ + 'modalId' => 'otherNewAttachmentPreviewModal', + 'title' => 'Preview Lampiran', + ]) +
@@ -99,19 +193,354 @@
+ @endsection diff --git a/resources/views/backend/pages/forms/other/view.blade.php b/resources/views/backend/pages/forms/other/view.blade.php index 7d0a5f1..cbfa2f1 100644 --- a/resources/views/backend/pages/forms/other/view.blade.php +++ b/resources/views/backend/pages/forms/other/view.blade.php @@ -56,16 +56,64 @@
-
-
- Lihat Bukti -
+
+
+ +

Gunakan tombol preview untuk melihat lampiran.

+
+ + + + + + + + + + + @forelse ($attachments as $attachment) + + + + + + + @empty + + + + @endforelse + +
KategoriNama FilePreviewDownload
{{ $attachment['file_category'] ? ucwords(str_replace('_', ' ', $attachment['file_category'])) : '-' }}{{ $attachment['filename'] ?? basename($attachment['file_path']) }} + + + + Download + +
Tidak ada lampiran.
+
+
+
+ + @include('backend.components.attachment-preview-modal', [ + 'modalId' => 'otherViewAttachmentPreviewModal', + 'title' => 'Preview Lampiran', + ])
@@ -261,15 +309,105 @@ @section('scripts') - @endsection diff --git a/routes/web.php b/routes/web.php index 5c3b2e5..64f2541 100644 --- a/routes/web.php +++ b/routes/web.php @@ -193,6 +193,7 @@ Route::middleware(['auth:admin', 'menu'])->group(function () { Route::post('/other/store', [FormOtherController::class, 'store'])->name('forms.other.store'); Route::get('/other/edit/{id}', [FormOtherController::class, 'edit'])->name('forms.other.edit'); Route::put('/other/update/{id}', [FormOtherController::class, 'update'])->name('forms.other.update'); + Route::delete('/other/{form}/attachments/{attachment}', [FormOtherController::class, 'deleteAttachment'])->name('forms.other.attachments.destroy'); Route::delete('/other/destroy/{id}', [FormOtherController::class, 'destroy'])->name('forms.other.destroy'); Route::put('/other/approve/{id}', [FormOtherController::class, 'approve'])->name('forms.other.approve'); Route::put('/other/approve2/{id}', [FormOtherController::class, 'approve2'])->name('forms.other.approve2');