diff --git a/app/Http/Controllers/PrintStatementController.php b/app/Http/Controllers/PrintStatementController.php new file mode 100644 index 0000000..3e82f55 --- /dev/null +++ b/app/Http/Controllers/PrintStatementController.php @@ -0,0 +1,281 @@ +get(); + + return view('webstatement::statements.index', compact('branches')); + } + + /** + * Show the form for creating a new statement request. + */ + public function create() + { + $branches = Branch::orderBy('name')->get(); + return view('webstatement::statements.create', compact('branches',)); + } + + /** + * Store a newly created statement request. + */ + public function store(PrintStatementRequest $request) + { + $validated = $request->validated(); + + // Add user tracking data + $validated['user_id'] = Auth::id(); + $validated['created_by'] = Auth::id(); + $validated['ip_address'] = $request->ip(); + $validated['user_agent'] = $request->userAgent(); + + // Create the statement log + $statement = PrintStatementLog::create($validated); + + // Process statement availability check (this would be implemented based on your system) + $this->checkStatementAvailability($statement); + + return redirect()->route('webstatement.statements.show', $statement->id) + ->with('success', 'Statement request has been created successfully.'); + } + + /** + * Display the specified statement. + */ + public function show(PrintStatementLog $statement) + { + $statement->load(['user', 'branch', 'creator', 'authorizer']); + return view('webstatement::statements.show', compact('statement')); + } + + /** + * Download the statement if available and authorized. + */ + public function download(PrintStatementLog $statement) + { + // Check if statement is available and authorized + if (!$statement->is_available) { + return back()->with('error', 'Statement is not available for download.'); + } + + if ($statement->authorization_status !== 'approved') { + return back()->with('error', 'Statement download requires authorization.'); + } + + // Update download status + $statement->update([ + 'is_downloaded' => true, + 'downloaded_at' => now(), + 'updated_by' => Auth::id() + ]); + + // Generate or fetch the statement file (implementation depends on your system) + $filePath = $this->generateStatementFile($statement); + + // Return file download response + return response()->download($filePath, $this->generateFileName($statement)); + } + + /** + * Authorize a statement request. + */ + public function authorize(Request $request, PrintStatementLog $statement) + { + $request->validate([ + 'authorization_status' => ['required', Rule::in(['approved', 'rejected'])], + 'remarks' => ['nullable', 'string', 'max:255'], + ]); + + // Update authorization status + $statement->update([ + 'authorization_status' => $request->authorization_status, + 'authorized_by' => Auth::id(), + 'authorized_at' => now(), + 'remarks' => $request->remarks, + 'updated_by' => Auth::id() + ]); + + $statusText = $request->authorization_status === 'approved' ? 'approved' : 'rejected'; + + return redirect()->route('webstatement.statements.show', $statement->id) + ->with('success', "Statement request has been {$statusText} successfully."); + } + + /** + * Check if the statement is available in the system. + * This is a placeholder method - implement according to your system. + */ + protected function checkStatementAvailability(PrintStatementLog $statement) + { + // This would be implemented based on your system's logic + // For example, checking an API or database for statement availability + + // Placeholder implementation - set to available for demo + $statement->update([ + 'is_available' => true, + 'updated_by' => Auth::id() + ]); + } + + /** + * Generate or fetch the statement file. + * This is a placeholder method - implement according to your system. + */ + protected function generateStatementFile(PrintStatementLog $statement) + { + // This would be implemented based on your system's logic + // For example, calling an API to generate a PDF or fetching from storage + + // Placeholder implementation - return a dummy path + $tempFile = tempnam(sys_get_temp_dir(), 'statement_'); + file_put_contents($tempFile, 'Statement content would go here'); + return $tempFile; + } + + /** + * Generate a filename for the statement download. + */ + protected function generateFileName(PrintStatementLog $statement) + { + $accountNumber = $statement->account_number; + + if ($statement->is_period_range) { + return "statement_{$accountNumber}_{$statement->period_from}_to_{$statement->period_to}.pdf"; + } + + return "statement_{$accountNumber}_{$statement->period_from}.pdf"; + } + + /** + * Provide data for datatables. + */ + public function dataForDatatables(Request $request) + { + // Check permissions if needed + // if (!auth()->user()->can('view_statements')) { + // abort(403, 'Sorry! You are not allowed to view statements.'); + // } + + // Retrieve data from the database + $query = PrintStatementLog::query(); + + // Apply search filter if provided + if ($request->has('search') && !empty($request->get('search'))) { + $search = $request->get('search'); + $query->where(function ($q) use ($search) { + $q->where('account_number', 'LIKE', "%$search%") + ->orWhere('branch_code', 'LIKE', "%$search%") + ->orWhere('period_from', 'LIKE', "%$search%") + ->orWhere('period_to', 'LIKE', "%$search%") + ->orWhere('authorization_status', 'LIKE', "%$search%"); + }); + } + + // Apply column filters if provided + if ($request->has('filters') && !empty($request->get('filters'))) { + $filters = json_decode($request->get('filters'), true); + + foreach ($filters as $filter) { + if (!empty($filter['value'])) { + if ($filter['column'] === 'branch_code') { + $query->where('branch_code', $filter['value']); + } elseif ($filter['column'] === 'authorization_status') { + $query->where('authorization_status', $filter['value']); + } elseif ($filter['column'] === 'is_downloaded') { + $query->where('is_downloaded', filter_var($filter['value'], FILTER_VALIDATE_BOOLEAN)); + } + } + } + } + + // Apply sorting if provided + if ($request->has('sortOrder') && !empty($request->get('sortOrder'))) { + $order = $request->get('sortOrder'); + $column = $request->get('sortField'); + + // Map frontend column names to database column names if needed + $columnMap = [ + 'branch' => 'branch_code', + 'account' => 'account_number', + 'period' => 'period_from', + 'status' => 'authorization_status', + // Add more mappings as needed + ]; + + $dbColumn = $columnMap[$column] ?? $column; + $query->orderBy($dbColumn, $order); + } else { + // Default sorting + $query->latest('created_at'); + } + + // Get the total count of records + $totalRecords = $query->count(); + + // Apply pagination if provided + if ($request->has('page') && $request->has('size')) { + $page = $request->get('page'); + $size = $request->get('size'); + $offset = ($page - 1) * $size; // Calculate the offset + + $query->skip($offset)->take($size); + } + + // Get the filtered count of records + $filteredRecords = $query->count(); + + // Eager load relationships to avoid N+1 query problems + $query->with(['user', 'branch', 'authorizer']); + + // Get the data for the current page + $data = $query->get()->map(function ($item) { + // Transform data for frontend if needed + return [ + 'id' => $item->id, + 'branch_code' => $item->branch_code, + 'branch_name' => $item->branch->name ?? 'N/A', + 'account_number' => $item->account_number, + 'period_from' => $item->period_from, + 'period_to' => $item->is_period_range ? $item->period_to : null, + 'authorization_status' => $item->authorization_status, + 'is_available' => $item->is_available, + 'is_downloaded' => $item->is_downloaded, + 'created_at' => $item->created_at->format('Y-m-d H:i:s'), + 'created_by' => $item->user->name ?? 'N/A', + 'authorized_by' => $item->authorizer ? $item->authorizer->name : null, + 'authorized_at' => $item->authorized_at ? $item->authorized_at->format('Y-m-d H:i:s') : null, + ]; + }); + + // Calculate the page count + $pageCount = ceil($filteredRecords / ($request->get('size') ?: 1)); + + // Calculate the current page number + $currentPage = $request->get('page') ?: 1; + + // Return the response data as a JSON object + return response()->json([ + 'draw' => $request->get('draw'), + 'recordsTotal' => $totalRecords, + 'recordsFiltered' => $filteredRecords, + 'pageCount' => $pageCount, + 'page' => $currentPage, + 'totalCount' => $totalRecords, + 'data' => $data, + ]); + } + } diff --git a/app/Http/Requests/PrintStatementRequest.php b/app/Http/Requests/PrintStatementRequest.php new file mode 100644 index 0000000..67701b9 --- /dev/null +++ b/app/Http/Requests/PrintStatementRequest.php @@ -0,0 +1,81 @@ +|string> + */ + public function rules() + : array + { + $rules = [ + 'branch_code' => ['required', 'string', 'exists:branches,code'], + 'account_number' => ['required', 'string'], + 'is_period_range' => ['sometimes', 'boolean'], + 'period_from' => ['required', 'string', 'regex:/^\d{6}$/'], // YYYYMM format + ]; + + // If it's a period range, require period_to + if ($this->input('is_period_range')) { + $rules['period_to'] = [ + 'required', + 'string', + 'regex:/^\d{6}$/', // YYYYMM format + 'gte:period_from' // period_to must be greater than or equal to period_from + ]; + } + + return $rules; + } + + /** + * Get custom messages for validator errors. + * + * @return array + */ + public function messages() + : array + { + return [ + 'branch_code.required' => 'Branch code is required', + 'branch_code.exists' => 'Selected branch does not exist', + 'account_number.required' => 'Account number is required', + 'period_from.required' => 'Period is required', + 'period_from.regex' => 'Period must be in YYYYMM format', + 'period_to.required' => 'End period is required for period range', + 'period_to.regex' => 'End period must be in YYYYMM format', + 'period_to.gte' => 'End period must be after or equal to start period', + ]; + } + + /** + * Prepare the data for validation. + * + * @return void + */ + protected function prepareForValidation() + : void + { + // Convert is_period_range to boolean if it exists + if ($this->has('is_period_range')) { + $this->merge([ + 'is_period_range' => filter_var($this->is_period_range, FILTER_VALIDATE_BOOLEAN), + ]); + } + } + } diff --git a/app/Models/PrintStatementLog.php b/app/Models/PrintStatementLog.php new file mode 100644 index 0000000..2206a08 --- /dev/null +++ b/app/Models/PrintStatementLog.php @@ -0,0 +1,183 @@ + 'boolean', + 'is_available' => 'boolean', + 'is_downloaded' => 'boolean', + 'downloaded_at' => 'datetime', + 'authorized_at' => 'datetime', + ]; + + /** + * Get the formatted period display + * + * @return string + */ + public function getPeriodDisplayAttribute() + { + if ($this->is_period_range) { + return $this->formatPeriod($this->period_from) . ' - ' . $this->formatPeriod($this->period_to); + } + + return $this->formatPeriod($this->period_from); + } + + /** + * Format period from YYYYMM to Month Year + * + * @param string $period + * + * @return string + */ + protected function formatPeriod($period) + { + if (strlen($period) !== 6) { + return $period; + } + + $year = substr($period, 0, 4); + $month = substr($period, 4, 2); + + return date('F Y', mktime(0, 0, 0, (int) $month, 1, (int) $year)); + } + + /** + * Get the user who requested the statement + */ + public function user() + { + return $this->belongsTo(User::class, 'user_id'); + } + + /** + * Get the user who created the record + */ + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + /** + * Get the user who updated the record + */ + public function updater() + { + return $this->belongsTo(User::class, 'updated_by'); + } + + /** + * Get the user who authorized the record + */ + public function authorizer() + { + return $this->belongsTo(User::class, 'authorized_by'); + } + + /** + * Scope a query to only include pending authorization records + */ + public function scopePending($query) + { + return $query->where('authorization_status', 'pending'); + } + + /** + * Scope a query to only include approved records + */ + public function scopeApproved($query) + { + return $query->where('authorization_status', 'approved'); + } + + /** + * Scope a query to only include rejected records + */ + public function scopeRejected($query) + { + return $query->where('authorization_status', 'rejected'); + } + + /** + * Scope a query to only include downloaded records + */ + public function scopeDownloaded($query) + { + return $query->where('is_downloaded', true); + } + + /** + * Scope a query to only include available records + */ + public function scopeAvailable($query) + { + return $query->where('is_available', true); + } + + /** + * Check if the statement is for a single period + */ + public function isSinglePeriod() + { + return !$this->is_period_range; + } + + /** + * Check if the statement is authorized + */ + public function isAuthorized() + { + return $this->authorization_status === 'approved'; + } + + /** + * Check if the statement is rejected + */ + public function isRejected() + { + return $this->authorization_status === 'rejected'; + } + + /** + * Check if the statement is pending authorization + */ + public function isPending() + { + return $this->authorization_status === 'pending'; + } + + public function branch(){ + return $this->belongsTo(Branch::class, 'branch_code','code'); + } + } diff --git a/database/migrations/2025_05_11_172635_create_print_statement_logs_table.php b/database/migrations/2025_05_11_172635_create_print_statement_logs_table.php new file mode 100644 index 0000000..a78ede6 --- /dev/null +++ b/database/migrations/2025_05_11_172635_create_print_statement_logs_table.php @@ -0,0 +1,54 @@ +id(); + $table->unsignedBigInteger('user_id')->nullable()->comment('User who requested the statement'); + $table->string('branch_code')->comment('Branch code'); + $table->string('account_number')->comment('Account number'); + $table->string('period_from')->comment('Statement period start (YYYYMM format)'); + $table->string('period_to')->nullable()->comment('Statement period end (YYYYMM format), null if single period'); + $table->boolean('is_period_range')->default(false)->comment('Whether the statement is for a period range'); + $table->boolean('is_available')->default(false)->comment('Whether the statement was found'); + $table->boolean('is_downloaded')->default(false)->comment('Whether the statement was downloaded'); + $table->string('ip_address')->nullable()->comment('IP address of requester'); + $table->string('user_agent')->nullable()->comment('User agent of requester'); + $table->timestamp('downloaded_at')->nullable()->comment('When the statement was downloaded'); + + $table->enum('authorization_status', ['pending', 'approved', 'rejected'])->default('pending'); + + // Add index for faster searching + $table->index(['branch_code', 'account_number', 'period_from', 'period_to']); + $table->index(['user_id', 'is_downloaded']); + $table->index('authorization_status'); + + // User tracking fields + $table->unsignedBigInteger('created_by')->nullable(); + $table->unsignedBigInteger('updated_by')->nullable(); + $table->unsignedBigInteger('deleted_by')->nullable(); + $table->unsignedBigInteger('authorized_by')->nullable(); + $table->timestamp('authorized_at')->nullable(); + + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('print_statement_logs'); + } + }; diff --git a/module.json b/module.json index 25ee3c1..664d81a 100644 --- a/module.json +++ b/module.json @@ -22,6 +22,17 @@ "administrator" ] }, + { + "title": "Print Statement", + "path": "statements", + "icon": "ki-filled ki-calendar text-lg text-primary", + "classes": "", + "attributes": [], + "permission": "", + "roles": [ + "administrator" + ] + }, { "title": "Kartu ATM", "path": "kartu-atm", diff --git a/resources/views/statements/create.blade.php b/resources/views/statements/create.blade.php new file mode 100644 index 0000000..f648007 --- /dev/null +++ b/resources/views/statements/create.blade.php @@ -0,0 +1,38 @@ +@extends('layouts.main') + +@section('breadcrumbs') + +@endsection + +@section('content') + +@endsection + +@push('scripts') + +@endpush diff --git a/resources/views/statements/index.blade.php b/resources/views/statements/index.blade.php new file mode 100644 index 0000000..6fd2358 --- /dev/null +++ b/resources/views/statements/index.blade.php @@ -0,0 +1,349 @@ +@extends('layouts.main') + +@section('breadcrumbs') + {{ Breadcrumbs::render(request()->route()->getName()) }} +@endsection + +@section('content') +
+
+
+

Request Print Stetement

+ +
+
+
+ @csrf + @if(isset($statement)) + @method('PUT') + @endif + +
+
+ + + @error('branch_code') +
{{ $message }}
+ @enderror +
+ +
+ + + @error('account_number') +
{{ $message }}
+ @enderror +
+ +
+ + + + @error('period_from') + {{ $message }} + @enderror +
+ +
+ + + @error('period_to') + {{ $message }} + @enderror +
+
+ +
+ + +
+
+
+
+
+ +
+
+
+

+ Daftar Statement Request +

+
+
+ +
+
+
+
+
+ + + + + + + + + + + + + + + +
+ + + ID + + + Branch + + + Account Number + + + Period + + + Status + + + Available + + + Downloaded + + + Created At + + Action
+
+ +
+
+
+@endsection + +@push('scripts') + + + + + +@endpush diff --git a/routes/breadcrumbs.php b/routes/breadcrumbs.php index 3d1cbe3..9d43203 100644 --- a/routes/breadcrumbs.php +++ b/routes/breadcrumbs.php @@ -109,3 +109,8 @@ $trail->parent('periode-statements.index'); $trail->push('View Periode Statement', route('periode-statements.show', $data)); }); + + Breadcrumbs::for('statements.index', function (BreadcrumbTrail $trail) { + $trail->parent('home'); + $trail->push('Print Stetement', route('statements.index')); + }); diff --git a/routes/web.php b/routes/web.php index fa01270..2906f34 100644 --- a/routes/web.php +++ b/routes/web.php @@ -2,6 +2,7 @@ use Illuminate\Support\Facades\Route; use Modules\Webstatement\Http\Controllers\PeriodeStatementController; + use Modules\Webstatement\Http\Controllers\PrintStatementController; use Modules\Webstatement\Http\Controllers\SyncLogsController; use Modules\Webstatement\Http\Controllers\JenisKartuController; use Modules\Webstatement\Http\Controllers\KartuAtmController; @@ -78,7 +79,13 @@ Route::middleware(['auth'])->group(function () { Route::resource('periode-statements', PeriodeStatementController::class); + Route::group(['prefix' => 'statements', 'as' => 'statements.', 'middleware' => ['auth']], function () { + Route::get('/datatables', [PrintStatementController::class, 'dataForDatatables'])->name('datatables'); + Route::get('/{statement}/download', [PrintStatementController::class, 'download'])->name('download'); + Route::post('/{statement}/authorize', [PrintStatementController::class, 'authorize'])->name('authorize'); + }); + Route::resource('statements', PrintStatementController::class); }); Route::get('migrasi', [MigrasiController::class, 'index'])->name('migrasi.index');