From 8321df6d2bd6b5d2076dbe391bae2b5fdda28859 Mon Sep 17 00:00:00 2001 From: Fiqh Pratama Date: Mon, 13 Oct 2025 17:28:34 +0700 Subject: [PATCH] final upload form --- ...ormEntertainmentPresentationController.php | 479 +++++++++++++---- .../Forms/FormMeetingSeminarController.php | 482 ++++++++++++++---- .../forms/entertainment/create.blade.php | 41 +- .../pages/forms/entertainment/edit.blade.php | 99 +++- .../partials/attachment-modal.blade.php | 24 + .../partials/attachment-scripts.blade.php | 394 ++++++++++++++ .../pages/forms/entertainment/view.blade.php | 53 +- .../pages/forms/meeting/create.blade.php | 49 +- .../pages/forms/meeting/edit.blade.php | 107 +++- .../partials/attachment-modal.blade.php | 24 + .../partials/attachment-scripts.blade.php | 395 ++++++++++++++ .../pages/forms/meeting/view.blade.php | 61 ++- routes/web.php | 2 + 13 files changed, 1966 insertions(+), 244 deletions(-) create mode 100644 resources/views/backend/pages/forms/entertainment/partials/attachment-modal.blade.php create mode 100644 resources/views/backend/pages/forms/entertainment/partials/attachment-scripts.blade.php create mode 100644 resources/views/backend/pages/forms/meeting/partials/attachment-modal.blade.php create mode 100644 resources/views/backend/pages/forms/meeting/partials/attachment-scripts.blade.php diff --git a/app/Http/Controllers/Forms/FormEntertainmentPresentationController.php b/app/Http/Controllers/Forms/FormEntertainmentPresentationController.php index deb3173..2df717b 100644 --- a/app/Http/Controllers/Forms/FormEntertainmentPresentationController.php +++ b/app/Http/Controllers/Forms/FormEntertainmentPresentationController.php @@ -7,7 +7,6 @@ use App\Http\Controllers\Controller; use App\Models\FormEntertaimentPresentation; use App\Models\Rayon; use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Storage; use App\Models\Region; use App\Models\Cabang; use App\Models\UserHasCabang; @@ -23,16 +22,54 @@ use App\Mail\ExpenseCreated; use App\Helpers\WhatsappHelper; use App\Helpers\AuditTrailHelper; use App\Models\Admin; -use App\Rules\FileType; use App\Helpers\BudgetHelper; use App\Helpers\NotificationHelper; 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\Validator; +use Illuminate\Validation\ValidationException; use Carbon\Carbon; class FormEntertainmentPresentationController extends Controller { + protected AttachmentService $attachmentService; + + /** + * Attachment categories for Entertainment & Presentation. + * + * @var array + */ + protected array $attachmentCategories = [ + 'bukti_total' => 'Bukti Total', + ]; + + /** + * Blocked extensions to avoid dangerous uploads. + * + * @var array + */ + protected array $blockedExtensions = ['exe', 'dll', 'sh', 'bat', 'cmd', 'msi']; + + /** + * Supported image extensions for inline preview. + * + * @var array + */ + protected array $imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']; + + public function __construct(AttachmentService $attachmentService) + { + $this->attachmentService = $attachmentService; + + view()->share('entertainmentAttachmentCategories', $this->attachmentCategories); + view()->share('entertainmentAttachmentCategoryKeys', array_keys($this->attachmentCategories)); + view()->share('entertainmentBlockedExtensions', $this->blockedExtensions); + } + public function index() { $this->checkAuthorization(auth()->user(), ['forms.entertainment.view']); @@ -60,7 +97,7 @@ class FormEntertainmentPresentationController extends Controller $forms = FormEntertaimentPresentation::whereBetween('tanggal', [$dataRetrievalStartDate, $currentPeriodClosingDate]) ->orderBy('tanggal', 'desc') - ->with('user') + ->with(['user', 'attachments']) ->get(); $availableBudget = null; @@ -105,6 +142,11 @@ class FormEntertainmentPresentationController extends Controller ); session()->put('redirect_url', route('forms.entertainment')); + + $forms->each(function (FormEntertaimentPresentation $form) { + $form->setAttribute('formatted_attachments', $this->formatAttachmentCollection($form)); + }); + return view('backend.pages.forms.entertainment.index', [ 'forms' => $forms, 'availableBudget' => $availableBudget, @@ -118,8 +160,10 @@ class FormEntertainmentPresentationController extends Controller { $this->checkAuthorization(auth()->user(), ['forms.entertainment.view']); - $form = FormEntertaimentPresentation::with('user')->findOrFail($id); - $form->bukti_total = $form->bukti_total ? NextCloudHelper::getFileUrl($form->bukti_total) : null; + $form = FormEntertaimentPresentation::with(['user', 'attachments'])->findOrFail($id); + $attachments = $this->formatAttachmentCollection($form); + $form->setAttribute('formatted_attachments', $attachments); + $form->bukti_total = $this->getAttachmentDownloadUrlByCategory($attachments, 'bukti_total'); return response()->json($form); } @@ -128,11 +172,13 @@ class FormEntertainmentPresentationController extends Controller { $this->checkAuthorization(auth()->user(), ['forms.entertainment.view']); - $form = FormEntertaimentPresentation::with('user')->findOrFail($id); - $form->bukti_total = $form->bukti_total ? NextCloudHelper::getFileUrl($form->bukti_total) : null; + $form = FormEntertaimentPresentation::with(['user', 'attachments'])->findOrFail($id); + $attachments = $this->formatAttachmentCollection($form); + $form->setAttribute('formatted_attachments', $attachments); return view('backend.pages.forms.entertainment.view', [ 'form' => $form, + 'attachments' => $attachments, ]); } @@ -147,16 +193,20 @@ class FormEntertainmentPresentationController extends Controller { $this->checkAuthorization(auth()->user(), ['forms.entertainment.create']); - $request->validate([ - 'tanggal' => 'required', + $validator = Validator::make($request->all(), [ + 'tanggal' => 'required|date', 'jenis' => 'required|in:entertainment,presentation,sponsorship', - 'keterangan' => 'required', + 'keterangan' => 'required|string', 'total' => 'required|numeric|min:1', - 'name' => 'required', - 'alamat' => 'required', - 'nik_or_npwp' => 'required', - 'bukti_total' => ['nullable', 'file', 'max:51200'], + 'name' => 'required|string', + 'alamat' => 'required|string', + 'nik_or_npwp' => 'required|string', + 'attachments' => 'nullable|array', + 'attachments.*.file_category' => 'nullable|string|in:' . implode(',', array_keys($this->attachmentCategories)), + 'attachments.*.file_path' => 'nullable|file|max:10240', ]); + $this->addAttachmentValidationRules($validator, $request); + $validator->validate(); $tanggal = Carbon::parse($request->tanggal); $startDay = (int) env('STARTING_DATE', 11); @@ -177,7 +227,7 @@ class FormEntertainmentPresentationController extends Controller if ($tanggal->lt($startDate) || $tanggal->gt($closingDate)) { session()->flash('error', "Gagal, tanggal tidak berada dalam periode input: {$startDate->format('d M')} - {$closingDate->format('d M')}"); - return redirect()->back(); + return redirect()->back()->withInput(); } // Hitung periode budget dari tanggal form @@ -187,9 +237,9 @@ class FormEntertainmentPresentationController extends Controller $cabang = UserHasCabang::with('cabang')->where('user_id', auth()->user()->id)->first()->cabang; $availableBudget = BudgetHelper::getAvailableBudget($cabang->id, $periodeMonth, $periodeYear); - if($request->total > $availableBudget) { + if ($request->total > $availableBudget) { session()->flash('error', 'Alokasi budget bulanan cabang tidak mencukupi,silahkan ajukan pada periode expense berikutnya'); - return redirect()->back(); + return redirect()->back()->withInput(); } $region = Region::where('id', $cabang->region_id)->first(); @@ -197,48 +247,68 @@ class FormEntertainmentPresentationController extends Controller $kategori = ""; if (strtolower($request->jenis) == 'entertainment' || $request->jenis == 'entertainment') { $kategori = Kategori::where('name', 'Entertainment')->first(); - }elseif (strtolower($request->jenis) == 'presentation' || $request->jenis == 'presentation') { + } elseif (strtolower($request->jenis) == 'presentation' || $request->jenis == 'presentation') { $kategori = Kategori::where('name', 'Presentation')->first(); - }elseif (strtolower($request->jenis) == 'sponsorship' || $request->jenis == 'sponsorship') { + } elseif (strtolower($request->jenis) == 'sponsorship' || $request->jenis == 'sponsorship') { $kategori = Kategori::where('name', 'Sponsorship')->first(); } - $filePaths = []; $folderPath = '/expense/' . $region->code . '/' . $cabang->code . '/entertainment'; - 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 - ); - } - // Generate sequence number and expense number $sequence_number = FormEntertaimentPresentation::withTrashed()->where('expense_number', 'like', $region->code . '-' . $cabang->code . '-ETY-' . date('ym') . '%')->count() + 1; $formatted_sequence_number = str_pad($sequence_number, 4, '0', STR_PAD_LEFT); $expense_number = $region->code . '-' . $cabang->code . '-ETY-' . date('ym') . $formatted_sequence_number; - // Save the form data - $form = FormEntertaimentPresentation::create([ - 'expense_number' => $expense_number, - 'user_id' => auth()->user()->id, - 'tanggal' => $request->tanggal, - 'jenis' => $request->jenis, - 'keterangan' => $request->keterangan, - 'total' => $request->total, - 'name' => $request->name, - 'alamat' => $request->alamat, - 'nik_or_npwp' => $request->nik_or_npwp, - 'bukti_total' => $filePaths['bukti_total'] ?? null, - 'account_number' => $kategori->account_number, - 'status' => 'On Progress', - ]); + DB::beginTransaction(); + try { + $attachmentsPayload = $this->buildAttachmentPayload($request, $folderPath); + + if (empty($attachmentsPayload)) { + throw ValidationException::withMessages([ + 'attachments.0.file_path' => 'Minimal satu lampiran diperlukan.', + ]); + } + + $primaryAttachmentPath = $attachmentsPayload[0]['file_path'] ?? null; + + $form = FormEntertaimentPresentation::create([ + 'expense_number' => $expense_number, + 'user_id' => auth()->user()->id, + 'tanggal' => $request->tanggal, + 'jenis' => $request->jenis, + 'keterangan' => $request->keterangan, + 'total' => $request->total, + 'name' => $request->name, + 'alamat' => $request->alamat, + 'nik_or_npwp' => $request->nik_or_npwp, + 'bukti_total' => $primaryAttachmentPath, + 'account_number' => $kategori->account_number, + 'status' => 'On Progress', + ]); + + $this->attachmentService->addMultipleAttachments( + $form->id, + AttachmentTableName::FORM_ENTERTAINMENT_PRESENTATION, + $attachmentsPayload + ); + + DB::commit(); + } catch (ValidationException $exception) { + DB::rollBack(); + throw $exception; + } catch (\Throwable $th) { + DB::rollBack(); + + Log::error('Failed to create Entertainment Presentation form', [ + 'user_id' => auth()->id(), + 'message' => $th->getMessage(), + 'trace' => $th->getTraceAsString(), + ]); + + session()->flash('error', 'Terjadi kesalahan saat menyimpan data, silakan coba lagi.'); + return redirect()->back()->withInput(); + } $name = auth()->user()->name; $tanggal = $form->created_at; @@ -262,7 +332,7 @@ class FormEntertainmentPresentationController extends Controller if ($role === 'Marketing Information System') { return true; // Tidak ada filter cabang untuk peran ini } - + if (!$user->cabang || !$user->cabang->cabang) { return false; // Pastikan user memiliki cabang } @@ -325,14 +395,17 @@ class FormEntertainmentPresentationController extends Controller $this->checkAuthorization(auth()->user(), ['forms.entertainment.edit']); $role = auth()->user()->getRoleNames()[0]; - $form = FormEntertaimentPresentation::findOrfail($id); + $form = FormEntertaimentPresentation::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(); } - + + $attachments = $this->formatAttachmentCollection($form); + return view('backend.pages.forms.entertainment.edit', [ - 'form' => $form + 'form' => $form, + 'attachments' => $attachments, ]); } @@ -340,7 +413,7 @@ class FormEntertainmentPresentationController extends Controller { $this->checkAuthorization(auth()->user(), ['forms.entertainment.edit']); $role = auth()->user()->getRoleNames()[0]; - $form = FormEntertaimentPresentation::findOrFail($id); + $form = FormEntertaimentPresentation::with('attachments')->findOrFail($id); $cabang_id = UserHasCabang::where('user_id', $form->user_id)->first()?->cabang_id; if (!$cabang_id) { @@ -364,18 +437,22 @@ class FormEntertainmentPresentationController extends Controller $availableBudget = BudgetHelper::getAvailableBudget($cabang_id, $periodeMonth, $periodeYear); - $request->validate([ - 'tanggal' => 'required', + $validator = Validator::make($request->all(), [ + 'tanggal' => 'required|date', 'jenis' => 'required|in:entertainment,presentation,sponsorship', - 'keterangan' => 'required', + 'keterangan' => 'required|string', 'total' => 'required|numeric|min:1|max:' . $availableBudget, - 'name' => 'required', - 'alamat' => 'required', - 'nik_or_npwp' => 'required', - 'bukti_total' => ['nullable', 'file', 'max:51200'], + 'name' => 'required|string', + 'alamat' => 'required|string', + 'nik_or_npwp' => 'required|string', + 'attachments' => 'nullable|array', + 'attachments.*.file_category' => 'nullable|string|in:' . implode(',', array_keys($this->attachmentCategories)), + 'attachments.*.file_path' => 'nullable|file|max:10240', ]); + $this->addAttachmentValidationRules($validator, $request); + $validator->validate(); - if($form->status != 'On Progress' && $role == 'Medical Representatif') { + 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(); } @@ -386,46 +463,90 @@ class FormEntertainmentPresentationController extends Controller $kategori = ""; if (strtolower($request->jenis) == 'entertainment' || $request->jenis == 'entertainment') { $kategori = Kategori::where('name', 'Entertainment')->first(); - }elseif (strtolower($request->jenis) == 'presentation' || $request->jenis == 'presentation') { + } elseif (strtolower($request->jenis) == 'presentation' || $request->jenis == 'presentation') { $kategori = Kategori::where('name', 'Presentation')->first(); - }elseif (strtolower($request->jenis) == 'sponsorship' || $request->jenis == 'sponsorship') { + } elseif (strtolower($request->jenis) == 'sponsorship' || $request->jenis == 'sponsorship') { $kategori = Kategori::where('name', 'Sponsorship')->first(); } - $filePaths = []; $folderPath = '/expense/' . $region->code . '/' . $cabang->code . '/entertainment'; - if ($request->hasFile('bukti_total')) { - $bukti_total = $request->file('bukti_total'); - $filename = Str::uuid() . '.' . $bukti_total->extension(); + DB::beginTransaction(); + try { + $attachmentsPayload = $this->buildAttachmentPayload($request, $folderPath); + $primaryAttachmentPath = !empty($attachmentsPayload) + ? $attachmentsPayload[0]['file_path'] + : $form->bukti_total; - $filePaths['bukti_total'] = $folderPath . '/' . $filename; + $form->update([ + 'tanggal' => $request->tanggal, + 'jenis' => $request->jenis, + 'keterangan' => $request->keterangan, + 'total' => $request->total, + 'name' => $request->name, + 'alamat' => $request->alamat, + 'nik_or_npwp' => $request->nik_or_npwp, + 'bukti_total' => $primaryAttachmentPath, + 'account_number' => $kategori->account_number, + ]); - NextCloudHelper::uploadFile( - $folderPath, - $bukti_total->getContent(), - $filename - ); + if (!empty($attachmentsPayload)) { + $this->attachmentService->addMultipleAttachments( + $form->id, + AttachmentTableName::FORM_ENTERTAINMENT_PRESENTATION, + $attachmentsPayload + ); + } + + DB::commit(); + } catch (ValidationException $exception) { + DB::rollBack(); + throw $exception; + } catch (\Throwable $th) { + DB::rollBack(); + + Log::error('Failed to update Entertainment Presentation form', [ + '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(); } - // Save the form data - $form->update([ - 'tanggal' => $request->tanggal, - 'jenis' => $request->jenis, - 'keterangan' => $request->keterangan, - 'total' => $request->total, - 'name' => $request->name, - 'alamat' => $request->alamat, - 'nik_or_npwp' => $request->nik_or_npwp, - 'bukti_total' => $filePaths['bukti_total'] ?? $form->bukti_total, - 'account_number' => $kategori->account_number, - ]); - AuditTrailHelper::AddAuditTrail('Update', 'Update Expense at Form Entertainment Presentation (' . $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.entertainment.edit']); + + $form = FormEntertaimentPresentation::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_ENTERTAINMENT_PRESENTATION) + ->first(); + + if (!$attachment) { + return response()->json(['message' => 'Attachment not found.'], 404); + } + + $this->attachmentService->deleteAttachment($attachment->id); + + AuditTrailHelper::AddAuditTrail('Delete Attachment', 'Delete Attachment at Form Entertainment Presentation (' . $form->expense_number . ')'); + + return response()->json(['message' => 'Attachment deleted successfully.']); + } + public function approve($id) { $this->checkAuthorization(auth()->user(), ['approval.approve']); @@ -830,4 +951,190 @@ class FormEntertainmentPresentationController extends Controller session()->flash('success', 'Form has been deleted.'); return redirect()->back(); } + + protected function addAttachmentValidationRules(\Illuminate\Validation\Validator $validator, Request $request): void + { + $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 ($file && !$category) { + $validator->errors()->add("attachments.{$index}.file_category", 'Attachment category is required.'); + } + + if ($category && !$file) { + $validator->errors()->add("attachments.{$index}.file_path", 'Attachment file is required.'); + continue; + } + + if (!$file) { + continue; + } + + $extension = strtolower((string) $file->getClientOriginalExtension()); + if ($this->isBlockedExtension($extension)) { + $validator->errors()->add("attachments.{$index}.file_path", 'File type is not allowed.'); + continue; + } + + $mimeType = (string) $file->getMimeType(); + if (in_array($extension, $this->imageExtensions, true) && strpos($mimeType, 'image/') !== 0) { + $validator->errors()->add("attachments.{$index}.file_path", 'Invalid image file.'); + } + + if ($extension === 'pdf' && $mimeType !== 'application/pdf') { + $validator->errors()->add("attachments.{$index}.file_path", 'Invalid PDF file.'); + } + } + + foreach ($fileAttachments as $index => $files) { + $file = $files['file_path'] ?? null; + if ($file && !isset($inputAttachments[$index])) { + $validator->errors()->add("attachments.{$index}.file_category", 'Attachment category is required.'); + } + } + }); + } + + 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((string) $file->getClientOriginalExtension()); + + if ($this->isBlockedExtension($extension)) { + throw ValidationException::withMessages([ + "attachments.{$index}.file_path" => 'File type is not allowed.', + ]); + } + + $filename = Str::uuid() . '.' . $extension; + $filePath = rtrim($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; + } + + protected function formatAttachmentCollection(FormEntertaimentPresentation $form): array + { + if (!$form->relationLoaded('attachments')) { + $form->load('attachments'); + } + + $attachments = $form->attachments->map(function (AttachmentForm $attachment) { + if (!$attachment->file_path) { + return null; + } + + return $this->formatAttachmentData( + $attachment->id, + $attachment->file_category, + $attachment->file_path + ); + })->filter()->values()->all(); + + $hasLegacyPath = collect($attachments)->contains(function ($attachment) use ($form) { + return isset($attachment['file_path']) && $attachment['file_path'] === $form->bukti_total; + }); + + if ($form->bukti_total && !$hasLegacyPath) { + $attachments[] = $this->formatAttachmentData(null, 'bukti_total', $form->bukti_total, false); + } + + return $attachments; + } + + protected function formatAttachmentData(?int $id, ?string $category, string $filePath, bool $canDelete = true): array + { + $extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION)); + $previewType = $this->determinePreviewType($extension); + + return [ + 'id' => $id, + 'file_category' => $category, + 'category_label' => $this->getCategoryLabel($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' => $canDelete && !is_null($id), + ]; + } + + protected function determinePreviewType(?string $extension): string + { + $extension = strtolower((string) $extension); + + if (in_array($extension, $this->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 getAttachmentDownloadUrlByCategory(array $attachments, string $category): ?string + { + foreach ($attachments as $attachment) { + if (($attachment['file_category'] ?? null) === $category) { + return $attachment['download_url'] ?? null; + } + } + + return null; + } + + protected function getCategoryLabel(?string $category): string + { + if (!$category) { + return 'Lampiran'; + } + + if (isset($this->attachmentCategories[$category])) { + return $this->attachmentCategories[$category]; + } + + return ucwords(str_replace('_', ' ', $category)); + } } diff --git a/app/Http/Controllers/Forms/FormMeetingSeminarController.php b/app/Http/Controllers/Forms/FormMeetingSeminarController.php index 54a37e1..d687a00 100644 --- a/app/Http/Controllers/Forms/FormMeetingSeminarController.php +++ b/app/Http/Controllers/Forms/FormMeetingSeminarController.php @@ -7,7 +7,6 @@ use App\Http\Controllers\Controller; use App\Models\FormMeetingSeminar; use App\Models\Rayon; use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Storage; use App\Models\Region; use App\Models\Cabang; use App\Models\UserHasCabang; @@ -22,17 +21,54 @@ use App\Mail\ExpenseCreated; use App\Mail\FinalApprove; use App\Helpers\WhatsappHelper; use App\Helpers\AuditTrailHelper; -use App\Rules\FileType; use App\Models\Admin; use App\Helpers\BudgetHelper; use App\Helpers\NotificationHelper; 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\Validator; +use Illuminate\Validation\ValidationException; use Carbon\Carbon; class FormMeetingSeminarController extends Controller { + protected AttachmentService $attachmentService; + + /** + * @var array + */ + protected array $attachmentCategories = [ + 'bukti_allowance' => 'Bukti Allowance', + 'bukti_transport_ankot' => 'Bukti Transport Antar Kota', + 'bukti_hotel' => 'Bukti Hotel', + ]; + + /** + * Blocked file extensions (case insensitive). + * + * @var array + */ + protected array $blockedExtensions = ['exe', 'dll', 'sh', 'bat', 'cmd', 'msi']; + + /** + * Image extensions that can be previewed inline. + * + * @var array + */ + protected array $imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']; + + public function __construct(AttachmentService $attachmentService) + { + $this->attachmentService = $attachmentService; + view()->share('meetingAttachmentCategories', $this->attachmentCategories); + view()->share('meetingAttachmentCategoryKeys', array_keys($this->attachmentCategories)); + view()->share('meetingBlockedExtensions', $this->blockedExtensions); + } + public function index() { $this->checkAuthorization(auth()->user(), ['forms.meeting.view']); @@ -120,10 +156,13 @@ class FormMeetingSeminarController extends Controller { $this->checkAuthorization(auth()->user(), ['forms.meeting.view']); - $form = FormMeetingSeminar::with('user')->findOrFail($id); - $form->bukti_allowance = $form->bukti_allowance ? NextCloudHelper::getFileUrl($form->bukti_allowance) : null; - $form->bukti_transport_ankot = $form->bukti_transport_ankot ? NextCloudHelper::getFileUrl($form->bukti_transport_ankot) : null; - $form->bukti_hotel = $form->bukti_hotel ? NextCloudHelper::getFileUrl($form->bukti_hotel) : null; + $form = FormMeetingSeminar::with(['user', 'attachments'])->findOrFail($id); + $attachments = $this->formatAttachmentCollection($form); + + $form->bukti_allowance = $this->getAttachmentDownloadUrlByCategory($attachments, 'bukti_allowance'); + $form->bukti_transport_ankot = $this->getAttachmentDownloadUrlByCategory($attachments, 'bukti_transport_ankot'); + $form->bukti_hotel = $this->getAttachmentDownloadUrlByCategory($attachments, 'bukti_hotel'); + $form->setAttribute('formatted_attachments', $attachments); return response()->json($form); } @@ -132,13 +171,12 @@ class FormMeetingSeminarController extends Controller { $this->checkAuthorization(auth()->user(), ['forms.meeting.view']); - $form = FormMeetingSeminar::with('user')->findOrFail($id); - $form->bukti_allowance = $form->bukti_allowance ? NextCloudHelper::getFileUrl($form->bukti_allowance) : null; - $form->bukti_transport_ankot = $form->bukti_transport_ankot ? NextCloudHelper::getFileUrl($form->bukti_transport_ankot) : null; - $form->bukti_hotel = $form->bukti_hotel ? NextCloudHelper::getFileUrl($form->bukti_hotel) : null; + $form = FormMeetingSeminar::with(['user', 'attachments'])->findOrFail($id); + $attachments = $this->formatAttachmentCollection($form); return view('backend.pages.forms.meeting.view', [ 'form' => $form, + 'attachments' => $attachments, ]); } @@ -153,15 +191,17 @@ class FormMeetingSeminarController extends Controller { $this->checkAuthorization(auth()->user(), ['forms.meeting.create']); - $request->validate([ + $validator = Validator::make($request->all(), [ 'tanggal' => 'required|date', - 'allowance' => 'nullable|numeric', - 'transport_ankot' => 'nullable|numeric', - 'hotel' => 'nullable|numeric', - 'bukti_allowance' => ['nullable', 'file', 'max:51200'], - 'bukti_transport_ankot' => ['nullable', 'file', 'max:51200'], - 'bukti_hotel' => ['nullable', 'file', 'max:51200'], + 'allowance' => 'nullable|numeric|min:0', + 'transport_ankot' => 'nullable|numeric|min:0', + 'hotel' => 'nullable|numeric|min:0', + 'attachments' => 'nullable|array', + 'attachments.*.file_category' => 'nullable|string|in:' . implode(',', array_keys($this->attachmentCategories)), + 'attachments.*.file_path' => 'nullable|file', // 10 MB ]); + $this->addAttachmentValidationRules($validator, $request); + $validator->validate(); $tanggal = Carbon::parse($request->tanggal); $startDay = (int) env('STARTING_DATE', 11); @@ -202,31 +242,8 @@ class FormMeetingSeminarController extends Controller $periodeMonth = strtolower($periodeDate->format('F')); $periodeYear = (int) $periodeDate->format('Y'); - // Upload bukti file ke Nextcloud - $filePaths = []; $folderPath = '/expense/' . $region->code . '/' . $cabang->code . '/meeting'; - if ($request->hasFile('bukti_allowance')) { - $bukti_allowance = $request->file('bukti_allowance'); - $filename = Str::uuid() . '.' . $bukti_allowance->extension(); - $filePaths['bukti_allowance'] = $folderPath . '/' . $filename; - NextCloudHelper::uploadFile($folderPath, $bukti_allowance->getContent(), $filename); - } - - if ($request->hasFile('bukti_transport_ankot')) { - $bukti_transport_ankot = $request->file('bukti_transport_ankot'); - $filename = Str::uuid() . '.' . $bukti_transport_ankot->extension(); - $filePaths['bukti_transport_ankot'] = $folderPath . '/' . $filename; - NextCloudHelper::uploadFile($folderPath, $bukti_transport_ankot->getContent(), $filename); - } - - if ($request->hasFile('bukti_hotel')) { - $bukti_hotel = $request->file('bukti_hotel'); - $filename = Str::uuid() . '.' . $bukti_hotel->extension(); - $filePaths['bukti_hotel'] = $folderPath . '/' . $filename; - NextCloudHelper::uploadFile($folderPath, $bukti_hotel->getContent(), $filename); - } - // Generate nomor expense $sequence_number = FormMeetingSeminar::withTrashed() ->where('expense_number', 'like', $region->code . '-' . $cabang->code . '-MTG-' . date('ym') . '%') @@ -237,35 +254,64 @@ class FormMeetingSeminarController extends Controller // Hitung nominal $data_nominal = [ - 'allowance' => $request->allowance ?? 0, - 'transport_ankot' => $request->transport_ankot ?? 0, - 'hotel' => $request->hotel ?? 0, + 'allowance' => (float) ($request->allowance ?? 0), + 'transport_ankot' => (float) ($request->transport_ankot ?? 0), + 'hotel' => (float) ($request->hotel ?? 0), ]; + $totalNominal = array_sum($data_nominal); // Validasi budget $availableBudget = BudgetHelper::getAvailableBudget($cabang->id, $periodeMonth, $periodeYear); // Check if the total nominal exceeds the available budget - if(array_sum($data_nominal) > $availableBudget) { + if ($totalNominal > $availableBudget) { session()->flash('error', 'Alokasi budget bulanan cabang tidak mencukupi,silahkan ajukan pada periode expense berikutnya'); return redirect()->back(); } - // Save the form data - $form = FormMeetingSeminar::create([ - 'expense_number' => $expense_number, - 'user_id' => auth()->user()->id, - 'tanggal' => $request->tanggal, - 'allowance' => $data_nominal['allowance'], - 'transport_ankot' => $data_nominal['transport_ankot'], - 'hotel' => $data_nominal['hotel'], - 'total' => array_sum($data_nominal), - 'bukti_allowance' => $filePaths['bukti_allowance'] ?? null, - 'bukti_transport_ankot' => $filePaths['bukti_transport_ankot'] ?? null, - 'bukti_hotel' => $filePaths['bukti_hotel'] ?? null, - 'account_number' => $kategori->account_number, - 'status' => 'On Progress', - ]); + DB::beginTransaction(); + try { + $attachmentsPayload = $this->buildAttachmentPayload($request, $folderPath); + + $form = FormMeetingSeminar::create([ + 'expense_number' => $expense_number, + 'user_id' => auth()->user()->id, + 'tanggal' => $request->tanggal, + 'allowance' => $data_nominal['allowance'], + 'transport_ankot' => $data_nominal['transport_ankot'], + 'hotel' => $data_nominal['hotel'], + 'total' => $totalNominal, + 'bukti_allowance' => null, + 'bukti_transport_ankot' => null, + 'bukti_hotel' => null, + 'account_number' => $kategori?->account_number, + 'status' => 'On Progress', + ]); + + if (!empty($attachmentsPayload)) { + $this->attachmentService->addMultipleAttachments( + $form->id, + AttachmentTableName::FORM_MEETING_SEMINAR, + $attachmentsPayload + ); + } + + DB::commit(); + } catch (ValidationException $exception) { + DB::rollBack(); + throw $exception; + } catch (\Throwable $th) { + DB::rollBack(); + + Log::error('Failed to create Meeting Seminar form', [ + 'user_id' => auth()->user()->id, + 'message' => $th->getMessage(), + 'trace' => $th->getTraceAsString(), + ]); + + session()->flash('error', 'Terjadi kesalahan saat menyimpan data, silakan coba lagi.'); + return redirect()->back()->withInput(); + } $name = auth()->user()->name; $tanggal = $form->tanggal; @@ -352,14 +398,17 @@ class FormMeetingSeminarController extends Controller $this->checkAuthorization(auth()->user(), ['forms.meeting.edit']); $role = auth()->user()->getRoleNames()[0]; - $form = FormMeetingSeminar::findOrfail($id); + $form = FormMeetingSeminar::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(); } - + + $attachments = $this->formatAttachmentCollection($form); + return view('backend.pages.forms.meeting.edit', [ - 'form' => $form + 'form' => $form, + 'attachments' => $attachments, ]); } @@ -367,17 +416,19 @@ class FormMeetingSeminarController extends Controller { $this->checkAuthorization(auth()->user(), ['forms.meeting.edit']); $role = auth()->user()->getRoleNames()[0]; - $form = FormMeetingSeminar::findOrFail($id); + $form = FormMeetingSeminar::with('attachments')->findOrFail($id); - $request->validate([ + $validator = Validator::make($request->all(), [ 'tanggal' => 'required|date', - 'allowance' => 'nullable|numeric', - 'transport_ankot' => 'nullable|numeric', - 'hotel' => 'nullable|numeric', - 'bukti_allowance' => ['nullable', 'file', 'max:51200'], - 'bukti_transport_ankot' => ['nullable', 'file', 'max:51200'], - 'bukti_hotel' => ['nullable', 'file', 'max:51200'], + 'allowance' => 'nullable|numeric|min:0', + 'transport_ankot' => 'nullable|numeric|min:0', + 'hotel' => 'nullable|numeric|min:0', + 'attachments' => 'nullable|array', + 'attachments.*.file_category' => 'nullable|string|in:' . implode(',', array_keys($this->attachmentCategories)), + 'attachments.*.file_path' => 'nullable|file', // 10 MB ]); + $this->addAttachmentValidationRules($validator, $request); + $validator->validate(); if ($form->status != 'On Progress' && $role == 'Medical Representatif') { session()->flash('error', 'You cannot edit this form because it has been approved or rejected.'); @@ -393,35 +444,15 @@ class FormMeetingSeminarController extends Controller $region = Region::find($cabang->region_id); $kategori = Kategori::where('name', 'Meeting / Seminar')->first(); - $filePaths = []; $folderPath = '/expense/' . $region->code . '/' . $cabang->code . '/meeting'; - if ($request->hasFile('bukti_allowance')) { - $bukti_allowance = $request->file('bukti_allowance'); - $filename = Str::uuid() . '.' . $bukti_allowance->extension(); - $filePaths['bukti_allowance'] = $folderPath . '/' . $filename; - NextCloudHelper::uploadFile($folderPath, $bukti_allowance->getContent(), $filename); - } - - if ($request->hasFile('bukti_transport_ankot')) { - $bukti_transport_ankot = $request->file('bukti_transport_ankot'); - $filename = Str::uuid() . '.' . $bukti_transport_ankot->extension(); - $filePaths['bukti_transport_ankot'] = $folderPath . '/' . $filename; - NextCloudHelper::uploadFile($folderPath, $bukti_transport_ankot->getContent(), $filename); - } - - if ($request->hasFile('bukti_hotel')) { - $bukti_hotel = $request->file('bukti_hotel'); - $filename = Str::uuid() . '.' . $bukti_hotel->extension(); - $filePaths['bukti_hotel'] = $folderPath . '/' . $filename; - NextCloudHelper::uploadFile($folderPath, $bukti_hotel->getContent(), $filename); - } + $attachmentsPayload = $this->buildAttachmentPayload($request, $folderPath); // Hitung nominal total $data_nominal = [ - 'allowance' => $request->allowance ?? 0, - 'transport_ankot' => $request->transport_ankot ?? 0, - 'hotel' => $request->hotel ?? 0, + 'allowance' => (float) ($request->allowance ?? 0), + 'transport_ankot' => (float) ($request->transport_ankot ?? 0), + 'hotel' => (float) ($request->hotel ?? 0), ]; $totalNominal = array_sum($data_nominal); @@ -440,29 +471,76 @@ class FormMeetingSeminarController extends Controller // Ambil budget tersedia $availableBudget = BudgetHelper::getAvailableBudget($cabang->id, $periodeMonth, $periodeYear); - if(array_sum($data_nominal) > $availableBudget) { + if ($totalNominal > $availableBudget) { session()->flash('error', 'The total nominal exceeds the available budget.'); return redirect()->back(); } - // Save the form data - $form->update([ - 'tanggal' => $request->tanggal, - 'allowance' => $data_nominal['allowance'], - 'transport_ankot' => $data_nominal['transport_ankot'], - 'hotel' => $data_nominal['hotel'], - 'total' => $request->allowance + $request->transport_ankot + $request->hotel, - 'bukti_allowance' => $filePaths['bukti_allowance'] ?? $form->bukti_allowance, - 'bukti_transport_ankot' => $filePaths['bukti_transport_ankot'] ?? $form->bukti_transport_ankot, - 'bukti_hotel' => $filePaths['bukti_hotel'] ?? $form->bukti_hotel, - 'account_number' => $kategori->account_number, - ]); + DB::beginTransaction(); + try { + $form->update([ + 'tanggal' => $request->tanggal, + 'allowance' => $data_nominal['allowance'], + 'transport_ankot' => $data_nominal['transport_ankot'], + 'hotel' => $data_nominal['hotel'], + 'total' => $totalNominal, + 'account_number' => $kategori?->account_number, + ]); + + if (!empty($attachmentsPayload)) { + $this->attachmentService->addMultipleAttachments( + $form->id, + AttachmentTableName::FORM_MEETING_SEMINAR, + $attachmentsPayload + ); + } + + DB::commit(); + } catch (\Throwable $th) { + DB::rollBack(); + + Log::error('Failed to update Meeting Seminar form', [ + '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(); + } AuditTrailHelper::AddAuditTrail('Update', 'Update Expense at Form Meeting Seminar (' . $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.meeting.edit']); + + $form = FormMeetingSeminar::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_MEETING_SEMINAR) + ->first(); + + if (!$attachment) { + return response()->json(['message' => 'Attachment not found.'], 404); + } + + $this->attachmentService->deleteAttachment($attachment->id); + + AuditTrailHelper::AddAuditTrail('Delete Attachment', 'Delete Attachment at Form Meeting Seminar (' . $form->expense_number . ')'); + + return response()->json(['message' => 'Attachment deleted successfully.']); + } + public function approve(Request $request, $id) { $this->checkAuthorization(auth()->user(), ['approval.approve']); @@ -869,4 +947,194 @@ class FormMeetingSeminarController extends Controller session()->flash('success', 'Form has been deleted.'); return redirect()->back(); } + + protected function addAttachmentValidationRules(\Illuminate\Validation\Validator $validator, Request $request): void + { + $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 ($file && !$category) { + $validator->errors()->add("attachments.{$index}.file_category", 'Attachment category is required.'); + } + + if ($category && !$file) { + $validator->errors()->add("attachments.{$index}.file_path", 'Attachment file is required.'); + continue; + } + + if (!$file) { + continue; + } + + $extension = strtolower((string) $file->getClientOriginalExtension()); + if ($this->isBlockedExtension($extension)) { + $validator->errors()->add("attachments.{$index}.file_path", 'File type is not allowed.'); + continue; + } + + $mimeType = (string) $file->getMimeType(); + if (in_array($extension, $this->imageExtensions, true) && strpos($mimeType, 'image/') !== 0) { + $validator->errors()->add("attachments.{$index}.file_path", 'Invalid image file.'); + } + + if ($extension === 'pdf' && $mimeType !== 'application/pdf') { + $validator->errors()->add("attachments.{$index}.file_path", 'Invalid PDF file.'); + } + } + + foreach ($fileAttachments as $index => $files) { + $file = $files['file_path'] ?? null; + if ($file && !isset($inputAttachments[$index])) { + $validator->errors()->add("attachments.{$index}.file_category", 'Attachment category is required.'); + } + } + }); + } + + 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((string) $file->getClientOriginalExtension()); + + if ($this->isBlockedExtension($extension)) { + throw ValidationException::withMessages([ + "attachments.{$index}.file_path" => 'File type is not allowed.', + ]); + } + + $filename = Str::uuid() . '.' . $extension; + $filePath = rtrim($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; + } + + protected function formatAttachmentCollection(FormMeetingSeminar $form): array + { + if (!$form->relationLoaded('attachments')) { + $form->load('attachments'); + } + + $attachments = $form->attachments->map(function (AttachmentForm $attachment) { + if (!$attachment->file_path) { + return null; + } + + return $this->formatAttachmentData( + $attachment->id, + $attachment->file_category, + $attachment->file_path + ); + })->filter()->values()->all(); + + $legacySources = [ + 'bukti_allowance' => $form->bukti_allowance, + 'bukti_transport_ankot' => $form->bukti_transport_ankot, + 'bukti_hotel' => $form->bukti_hotel, + ]; + + foreach ($legacySources as $category => $path) { + if ($path) { + $attachments[] = $this->formatAttachmentData(null, $category, $path, false); + } + } + + return $attachments; + } + + protected function formatAttachmentData(?int $id, ?string $category, string $filePath, bool $canDelete = true): array + { + $extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION)); + $previewType = $this->determinePreviewType($extension); + + return [ + 'id' => $id, + 'file_category' => $category, + 'category_label' => $this->getCategoryLabel($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' => $canDelete && !is_null($id), + ]; + } + + protected function determinePreviewType(?string $extension): string + { + $extension = strtolower((string) $extension); + + if (in_array($extension, $this->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 getAttachmentDownloadUrlByCategory(array $attachments, string $category): ?string + { + foreach ($attachments as $attachment) { + if (($attachment['file_category'] ?? null) === $category) { + return $attachment['download_url'] ?? null; + } + } + + return null; + } + + protected function getCategoryLabel(?string $category): string + { + if (!$category) { + return 'Lampiran'; + } + + if (isset($this->attachmentCategories[$category])) { + return $this->attachmentCategories[$category]; + } + + return ucwords(str_replace('_', ' ', $category)); + } } diff --git a/resources/views/backend/pages/forms/entertainment/create.blade.php b/resources/views/backend/pages/forms/entertainment/create.blade.php index 7ebcf59..41c0724 100644 --- a/resources/views/backend/pages/forms/entertainment/create.blade.php +++ b/resources/views/backend/pages/forms/entertainment/create.blade.php @@ -82,17 +82,45 @@ -
- - -
- +
+
+
+
Lampiran
+ +
+
+ + + + + + + + + + + + + + +
KategoriFilePreviewAksi
Belum ada attachment.
+
+ + Maksimum 10 MB per file. Tidak diperbolehkan: {{ implode(', ', $entertainmentBlockedExtensions ?? []) }}. + +
+ +
+ +
@@ -123,4 +151,7 @@ spinnerOverlay.classList.add('d-flex'); // Use flexbox for centering }); + + @include('backend.pages.forms.entertainment.partials.attachment-modal') + @include('backend.pages.forms.entertainment.partials.attachment-scripts') @endsection diff --git a/resources/views/backend/pages/forms/entertainment/edit.blade.php b/resources/views/backend/pages/forms/entertainment/edit.blade.php index 5049be1..0db168f 100644 --- a/resources/views/backend/pages/forms/entertainment/edit.blade.php +++ b/resources/views/backend/pages/forms/entertainment/edit.blade.php @@ -83,17 +83,103 @@ -
- - -
- +
+
+
Lampiran Saat Ini
+
+ + + + + + + + + + + + @forelse ($attachments as $attachment) + + + + + + + + @empty + + + + @endforelse + +
KategoriNama FilePreviewDownloadDelete
{{ $attachment['category_label'] ?? '-' }}{{ $attachment['filename'] }} + + + @if (!empty($attachment['download_url'])) + Download + @else + Tidak tersedia + @endif + + @if (!empty($attachment['can_delete'])) + + @else + Tidak tersedia + @endif +
Belum ada attachment.
+
+
+ +
+
+
+
Tambah Lampiran Baru
+ +
+
+ + + + + + + + + + + + + + +
KategoriFilePreviewAksi
Belum ada attachment.
+
+ + Maksimum 10 MB per file. Tidak diperbolehkan: {{ implode(', ', $entertainmentBlockedExtensions ?? []) }}. + +
+ +
+ +
@@ -124,4 +210,7 @@ spinnerOverlay.classList.add('d-flex'); // Use flexbox for centering }); + + @include('backend.pages.forms.entertainment.partials.attachment-modal') + @include('backend.pages.forms.entertainment.partials.attachment-scripts') @endsection diff --git a/resources/views/backend/pages/forms/entertainment/partials/attachment-modal.blade.php b/resources/views/backend/pages/forms/entertainment/partials/attachment-modal.blade.php new file mode 100644 index 0000000..8c0f636 --- /dev/null +++ b/resources/views/backend/pages/forms/entertainment/partials/attachment-modal.blade.php @@ -0,0 +1,24 @@ + diff --git a/resources/views/backend/pages/forms/entertainment/partials/attachment-scripts.blade.php b/resources/views/backend/pages/forms/entertainment/partials/attachment-scripts.blade.php new file mode 100644 index 0000000..8927d77 --- /dev/null +++ b/resources/views/backend/pages/forms/entertainment/partials/attachment-scripts.blade.php @@ -0,0 +1,394 @@ +@php + $entertainmentAttachmentScriptConfig = [ + 'blockedExtensions' => array_map('strtolower', $entertainmentBlockedExtensions ?? []), + 'categoryOptions' => $entertainmentAttachmentCategories ?? [], + ]; +@endphp + diff --git a/resources/views/backend/pages/forms/entertainment/view.blade.php b/resources/views/backend/pages/forms/entertainment/view.blade.php index b5f7ca1..9ee6b03 100644 --- a/resources/views/backend/pages/forms/entertainment/view.blade.php +++ b/resources/views/backend/pages/forms/entertainment/view.blade.php @@ -66,16 +66,58 @@ -
-
- Lihat Bukti -
+
+
+
Lampiran
+
+ + + + + + + + + + + @forelse ($attachments ?? [] as $attachment) + + + + + + + @empty + + + + @endforelse + +
KategoriNama FilePreviewDownload
{{ $attachment['category_label'] ?? '-' }}{{ $attachment['filename'] }} + + + @if (!empty($attachment['download_url'])) + Download + @else + Tidak tersedia + @endif +
Belum ada attachment.
+
+
+
@@ -433,4 +475,7 @@ $('#total').val('0'); // Reset total to 0 }); + + @include('backend.pages.forms.entertainment.partials.attachment-modal') + @include('backend.pages.forms.entertainment.partials.attachment-scripts') @endsection diff --git a/resources/views/backend/pages/forms/meeting/create.blade.php b/resources/views/backend/pages/forms/meeting/create.blade.php index e1a364f..9ddba4d 100644 --- a/resources/views/backend/pages/forms/meeting/create.blade.php +++ b/resources/views/backend/pages/forms/meeting/create.blade.php @@ -58,10 +58,6 @@
-
- - -
@@ -69,21 +65,45 @@
-
- - -
-
- - -
- +
+
+
+
Lampiran
+ +
+
+ + + + + + + + + + + + + + +
KategoriFilePreviewAksi
Belum ada attachment.
+
+ + Maksimum 10 MB per file. Tidak diperbolehkan: {{ implode(', ', $meetingBlockedExtensions ?? []) }}. + +
+ +
+ +
@@ -130,4 +150,7 @@ spinnerOverlay.classList.add('d-flex'); // Use flexbox for centering }); + + @include('backend.pages.forms.meeting.partials.attachment-modal') + @include('backend.pages.forms.meeting.partials.attachment-scripts') @endsection diff --git a/resources/views/backend/pages/forms/meeting/edit.blade.php b/resources/views/backend/pages/forms/meeting/edit.blade.php index 54cb062..37e2e0b 100644 --- a/resources/views/backend/pages/forms/meeting/edit.blade.php +++ b/resources/views/backend/pages/forms/meeting/edit.blade.php @@ -59,10 +59,6 @@ -
- - -
@@ -70,21 +66,105 @@
-
- - -
-
- - +
+ +
+
+
Lampiran Saat Ini
+
+ + + + + + + + + + + + @forelse ($attachments as $attachment) + + + + + + + + @empty + + + + @endforelse + +
KategoriNama FilePreviewDownloadDelete
{{ $attachment['category_label'] ?? '-' }}{{ $attachment['filename'] }} + + + @if (!empty($attachment['download_url'])) + + Download + + @else + Tidak tersedia + @endif + + @if (!empty($attachment['can_delete'])) + + @else + Tidak tersedia + @endif +
Belum ada attachment.
- +
+
+
+
Tambah Lampiran Baru
+ +
+
+ + + + + + + + + + + + + + +
KategoriFilePreviewAksi
Belum ada attachment.
+
+ + Maksimum 10 MB per file. Tidak diperbolehkan: {{ implode(', ', $meetingBlockedExtensions ?? []) }}. + +
+ +
+ +
@@ -131,4 +211,7 @@ spinnerOverlay.classList.add('d-flex'); // Use flexbox for centering }); + + @include('backend.pages.forms.meeting.partials.attachment-modal') + @include('backend.pages.forms.meeting.partials.attachment-scripts') @endsection diff --git a/resources/views/backend/pages/forms/meeting/partials/attachment-modal.blade.php b/resources/views/backend/pages/forms/meeting/partials/attachment-modal.blade.php new file mode 100644 index 0000000..e9d57c1 --- /dev/null +++ b/resources/views/backend/pages/forms/meeting/partials/attachment-modal.blade.php @@ -0,0 +1,24 @@ + diff --git a/resources/views/backend/pages/forms/meeting/partials/attachment-scripts.blade.php b/resources/views/backend/pages/forms/meeting/partials/attachment-scripts.blade.php new file mode 100644 index 0000000..fed70b4 --- /dev/null +++ b/resources/views/backend/pages/forms/meeting/partials/attachment-scripts.blade.php @@ -0,0 +1,395 @@ +@php + $meetingAttachmentScriptConfig = [ + 'blockedExtensions' => array_map('strtolower', $meetingBlockedExtensions ?? []), + 'categoryOptions' => $meetingAttachmentCategories ?? [], + ]; +@endphp + diff --git a/resources/views/backend/pages/forms/meeting/view.blade.php b/resources/views/backend/pages/forms/meeting/view.blade.php index 22c783a..c6dc246 100644 --- a/resources/views/backend/pages/forms/meeting/view.blade.php +++ b/resources/views/backend/pages/forms/meeting/view.blade.php @@ -42,10 +42,6 @@ -
-
- Lihat Bukti -
@@ -53,17 +49,9 @@
-
-
- Lihat Bukti -
-
-
-
- Lihat Bukti
@@ -71,6 +59,52 @@
+
+
+
Lampiran
+
+ + + + + + + + + + + @forelse ($attachments as $attachment) + + + + + + + @empty + + + + @endforelse + +
KategoriNama FilePreviewDownload
{{ $attachment['category_label'] ?? '-' }}{{ $attachment['filename'] }} + + + @if (!empty($attachment['download_url'])) + Download + @else + Tidak tersedia + @endif +
Belum ada attachment.
+
+
+
@@ -475,4 +509,7 @@ $('#total2').val('0'); // Reset total to 0 }); + + @include('backend.pages.forms.meeting.partials.attachment-modal') + @include('backend.pages.forms.meeting.partials.attachment-scripts') @endsection diff --git a/routes/web.php b/routes/web.php index 64f2541..ab1b4a3 100644 --- a/routes/web.php +++ b/routes/web.php @@ -163,6 +163,7 @@ Route::middleware(['auth:admin', 'menu'])->group(function () { Route::post('/entertainment-presentation/store', [FormEntertainmentPresentationController::class, 'store'])->name('forms.entertainment.store'); Route::get('/entertainment-presentation/edit/{id}', [FormEntertainmentPresentationController::class, 'edit'])->name('forms.entertainment.edit'); Route::put('/entertainment-presentation/update/{id}', [FormEntertainmentPresentationController::class, 'update'])->name('forms.entertainment.update'); + Route::delete('/entertainment-presentation/{form}/attachments/{attachment}', [FormEntertainmentPresentationController::class, 'deleteAttachment'])->name('forms.entertainment.attachments.destroy'); Route::delete('/entertainment-presentation/destroy/{id}', [FormEntertainmentPresentationController::class, 'destroy'])->name('forms.entertainment.destroy'); Route::put('/entertainment-presentation/approve/{id}', [FormEntertainmentPresentationController::class, 'approve'])->name('forms.entertainment.approve'); Route::put('/entertainment-presentation/approve2/{id}', [FormEntertainmentPresentationController::class, 'approve2'])->name('forms.entertainment.approve2'); @@ -178,6 +179,7 @@ Route::middleware(['auth:admin', 'menu'])->group(function () { Route::post('/meeting-seminar/store', [FormMeetingSeminarController::class, 'store'])->name('forms.meeting.store'); Route::get('/meeting-seminar/edit/{id}', [FormMeetingSeminarController::class, 'edit'])->name('forms.meeting.edit'); Route::put('/meeting-seminar/update/{id}', [FormMeetingSeminarController::class, 'update'])->name('forms.meeting.update'); + Route::delete('/meeting-seminar/{form}/attachments/{attachment}', [FormMeetingSeminarController::class, 'deleteAttachment'])->name('forms.meeting.attachments.destroy'); Route::delete('/meeting-seminar/destroy/{id}', [FormMeetingSeminarController::class, 'destroy'])->name('forms.meeting.destroy'); Route::put('/meeting-seminar/approve/{id}', [FormMeetingSeminarController::class, 'approve'])->name('forms.meeting.approve'); Route::put('/meeting-seminar/approve2/{id}', [FormMeetingSeminarController::class, 'approve2'])->name('forms.meeting.approve2');