diff --git a/app/Exports/PeriodeStatementExport.php b/app/Exports/PeriodeStatementExport.php new file mode 100644 index 0000000..29234fb --- /dev/null +++ b/app/Exports/PeriodeStatementExport.php @@ -0,0 +1,77 @@ +get(); + } + + /** + * @return array + */ + public function headings(): array + { + return [ + 'ID', + 'Periode', + 'Authorization Status', + 'Notes', + 'Processed At', + 'Created By', + 'Created At', + 'Updated By', + 'Updated At', + 'Authorized By', + 'Authorized At' + ]; + } + + /** + * @param mixed $row + * + * @return array + */ + public function map($row): array + { + return [ + $row->id, + $row->periode, + $row->authorized_status, + $row->notes, + $row->processed_at ? Carbon::parse($row->processed_at)->format('Y-m-d H:i:s') : null, + $row->created_by, + $row->created_at ? Carbon::parse($row->created_at)->format('Y-m-d H:i:s') : null, + $row->updated_by, + $row->updated_at ? Carbon::parse($row->updated_at)->format('Y-m-d H:i:s') : null, + $row->authorized_by ? ($row->authorizer ? $row->authorizer->name : $row->authorized_by) : null, + $row->authorized_at ? Carbon::parse($row->authorized_at)->format('Y-m-d H:i:s') : null, + ]; + } + + /** + * @param Worksheet $sheet + * + * @return array + */ + public function styles(Worksheet $sheet) + { + return [ + // Style the first row as bold text + 1 => ['font' => ['bold' => true]], + ]; + } + } diff --git a/app/Http/Controllers/PeriodeStatementController.php b/app/Http/Controllers/PeriodeStatementController.php new file mode 100644 index 0000000..c86650e --- /dev/null +++ b/app/Http/Controllers/PeriodeStatementController.php @@ -0,0 +1,362 @@ +validated(); + + if ($validate) { + try { + // Add created_by field and default values + $validate['created_by'] = auth()->id(); + $validate['status'] = 'pending'; + $validate['authorized_status'] = 'pending'; + + // Save to database + PeriodeStatement::create($validate); + + return redirect() + ->route('periode-statements.index') + ->with('success', 'Periode Statement created successfully'); + } catch (Exception $e) { + return redirect() + ->route('periode-statements.create') + ->with('error', 'Failed to create Periode Statement: ' . $e->getMessage()); + } + } + } + + /** + * Show the form for creating a new resource. + */ + public function create() + { + return view('webstatement::periode-statement.create'); + } + + /** + * Show the specified resource. + */ + public function show($id) + { + $periodeStatement = PeriodeStatement::find($id); + return view('webstatement::periode-statement.show', compact('periodeStatement')); + } + + /** + * Show the form for editing the specified resource. + */ + public function edit($id) + { + $periodeStatement = PeriodeStatement::find($id); + return view('webstatement::periode-statement.create', compact('periodeStatement')); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy($id) + { + try { + // Add deleted_by field + $periodeStatement = PeriodeStatement::find($id); + $periodeStatement->deleted_by = auth()->id(); + $periodeStatement->save(); + + // Delete from database + $periodeStatement->delete(); + + echo json_encode(['success' => true, 'message' => 'Periode Statement deleted successfully']); + } catch (Exception $e) { + echo json_encode([ + 'success' => false, + 'message' => 'Failed to delete Periode Statement: ' . $e->getMessage() + ]); + } + } + + /** + * Provide data for datatables. + */ + public function dataForDatatables(Request $request) + { + if (is_null($this->user) || !$this->user->can('periode-statements.view')) { + //abort(403, 'Sorry! You are not allowed to view periode statement.'); + } + + // Retrieve data from the database + $query = PeriodeStatement::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('periode', 'LIKE', "%$search%"); + $q->orWhere('status', 'LIKE', "%$search%"); + $q->orWhere('authorized_status', 'LIKE', "%$search%"); + $q->orWhere('notes', 'LIKE', "%$search%"); + }); + } + + // Apply sorting if provided + if ($request->has('sortOrder') && !empty($request->get('sortOrder'))) { + $order = $request->get('sortOrder'); + $column = $request->get('sortField'); + $query->orderBy($column, $order); + } + + // 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(); + + // Get the data for the current page + $data = $query->get(); + + // 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, + ]); + } + + /** + * Delete multiple records. + */ + public function deleteMultiple(Request $request) + { + $ids = $request->input('ids'); + + // Add deleted_by to each record + $periodeStatements = PeriodeStatement::whereIn('id', $ids)->get(); + foreach ($periodeStatements as $periodeStatement) { + $periodeStatement->deleted_by = auth()->id(); + $periodeStatement->save(); + } + + PeriodeStatement::whereIn('id', $ids)->delete(); + return response()->json(['message' => 'Periode Statements deleted successfully']); + } + + /** + * Export data to Excel. + */ + public function export() + { + return Excel::download(new PeriodeStatementExport, 'periode-statements.xlsx'); + } + + /** + * Process the specified periode statement. + */ + public function process($id) + { + try { + $periodeStatement = PeriodeStatement::find($id); + + // Update status to processing + $periodeStatement->update([ + 'status' => 'processing', + 'notes' => ($periodeStatement->notes ? $periodeStatement->notes . "\n" : '') . 'Processing started at ' . Carbon::now() + ->format('Y-m-d H:i:s'), + 'updated_by' => auth()->id(), + 'processed_at' => Carbon::now(), + ]); + + return redirect() + ->route('periode-statements.index') + ->with('success', 'Periode statement processing has been initiated.'); + } catch (Exception $e) { + return redirect() + ->route('periode-statements.index') + ->with('error', 'Failed to process Periode Statement: ' . $e->getMessage()); + } + } + + /** + * Update the specified resource in storage. + */ + public function update(PeriodeStatementRequest $request, $id) + { + $validate = $request->validated(); + + if ($validate) { + try { + // Add updated_by field + $validate['updated_by'] = auth()->id(); + if (request()->get('status') == 'approved') { + $validate['status'] = 'completed'; + $validate['authorized_status'] = 'approved'; + $validate['processed_at'] = Carbon::now(); + } else if (request()->get('status') == 'rejected') { + $validate['status'] = 'failed'; + $validate['authorized_status'] = 'rejected'; + } + + $periodeStatement = PeriodeStatement::find($id); + + $validate['notes'] = ($periodeStatement->notes ? $periodeStatement->notes . "\n" : '') . request()->get('notes'); + $validate['authorized_by'] = auth()->id(); + $validate['authorized_at'] = Carbon::now(); + + // Update in database + + $periodeStatement->update($validate); + + return redirect() + ->route('periode-statements.index') + ->with('success', 'Periode Statement updated successfully'); + } catch (Exception $e) { + return redirect() + ->route('periode-statements.edit', $id) + ->with('error', 'Failed to update Periode Statement: ' . $e->getMessage()); + } + } + } + + /** + * Show the form for authorizing the specified resource. + */ + public function showAuthorize($id) + { + $periodeStatement = PeriodeStatement::find($id); + return view('webstatement::periode-statement.authorize', compact('periodeStatement')); + } + + /** + * Authorize the specified resource in storage. + */ + public function authorize(Request $request, $id) + { + try { + $periodeStatement = PeriodeStatement::find($id); + + $notes = $periodeStatement->notes ?? ''; + if ($request->authorization_notes) { + $notes .= ($notes ? "\n" : '') . 'Authorization note: ' . $request->authorization_notes; + } + + $periodeStatement->update([ + 'authorized_status' => $request->authorized_status, + 'authorized_by' => auth()->id(), + 'authorized_at' => Carbon::now(), + 'notes' => $notes, + 'updated_by' => auth()->id(), + ]); + + return redirect() + ->route('periode-statements.index') + ->with('success', 'Periode statement has been ' . $request->authorized_status . '.'); + } catch (Exception $e) { + return redirect() + ->route('periode-statements.index') + ->with('error', 'Failed to authorize Periode Statement: ' . $e->getMessage()); + } + } + + /** + * Display a listing of the resources pending authorization. + */ + public function pendingAuthorization() + { + return view('webstatement::periode-statement.pending_authorization'); + } + + /** + * Complete the specified periode statement. + */ + public function complete($id) + { + try { + $periodeStatement = PeriodeStatement::find($id); + + // Update status to completed + $periodeStatement->update([ + 'status' => 'completed', + 'notes' => ($periodeStatement->notes ? $periodeStatement->notes . "\n" : '') . 'Completed at ' . Carbon::now() + ->format('Y-m-d H:i:s'), + 'updated_by' => auth()->id(), + ]); + + return redirect() + ->route('periode-statements.index') + ->with('success', 'Periode statement has been marked as completed.'); + } catch (Exception $e) { + return redirect() + ->route('periode-statements.index') + ->with('error', 'Failed to complete Periode Statement: ' . $e->getMessage()); + } + } + + /** + * Mark the specified periode statement as failed. + */ + public function fail(Request $request, $id) + { + try { + $periodeStatement = PeriodeStatement::find($id); + + // Update status to failed + $periodeStatement->update([ + 'status' => 'failed', + 'notes' => ($periodeStatement->notes ? $periodeStatement->notes . "\n" : '') . 'Failed at ' . Carbon::now() + ->format('Y-m-d H:i:s') . '. Reason: ' . $request->failure_reason, + 'updated_by' => auth()->id(), + ]); + + return redirect() + ->route('periode-statements.index') + ->with('success', 'Periode statement has been marked as failed.'); + } catch (Exception $e) { + return redirect() + ->route('periode-statements.index') + ->with('error', 'Failed to mark Periode Statement as failed: ' . $e->getMessage()); + } + } + } diff --git a/app/Http/Requests/PeriodeStatementRequest.php b/app/Http/Requests/PeriodeStatementRequest.php new file mode 100644 index 0000000..7a6008d --- /dev/null +++ b/app/Http/Requests/PeriodeStatementRequest.php @@ -0,0 +1,74 @@ + [ + 'required', + 'string', + 'max:255', + 'regex:/^\d{4}-\d{2}$/', // Format YYYY-MM + ], + 'notes' => [ + 'nullable', + 'string', + ], + ]; + + // If we're updating an existing record, add a unique constraint that ignores the current record + if ($this->isMethod('PUT') || $this->isMethod('PATCH')) { + $rules['periode'][] = Rule::unique('periode_statements', 'periode') + ->ignore($this->route('periode_statement')); + } else { + // For new records, just check uniqueness + $rules['periode'][] = 'unique:periode_statements,periode'; + } + + return $rules; + } + + /** + * Get custom attributes for validator errors. + */ + public function attributes() + : array + { + return [ + 'periode' => 'Periode', + 'notes' => 'Notes', + ]; + } + + /** + * Get the error messages for the defined validation rules. + */ + public function messages() + : array + { + return [ + 'periode.required' => 'The periode field is required.', + 'periode.unique' => 'This periode already exists.', + 'periode.regex' => 'The periode must be in the format YYYY-MM (e.g., 2023-01).', + ]; + } + } diff --git a/app/Models/PeriodeStatement.php b/app/Models/PeriodeStatement.php new file mode 100644 index 0000000..0834946 --- /dev/null +++ b/app/Models/PeriodeStatement.php @@ -0,0 +1,140 @@ + 'datetime', + 'authorized_at' => 'datetime', + ]; + + /** + * Get the user who authorized this record. + */ + public function authorizer() + { + return $this->belongsTo(User::class, 'authorized_by'); + } + + /** + * Get the formatted periode (YYYY-MM) + * + * @return string + */ + public function getFormattedPeriodeAttribute() + { + return $this->periode; + } + + /** + * Scope a query to only include pending statements. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopePending($query) + { + return $query->where('status', 'pending'); + } + + /** + * Scope a query to only include processing statements. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeProcessing($query) + { + return $query->where('status', 'processing'); + } + + /** + * Scope a query to only include completed statements. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeCompleted($query) + { + return $query->where('status', 'completed'); + } + + /** + * Scope a query to only include failed statements. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeFailed($query) + { + return $query->where('status', 'failed'); + } + + /** + * Scope a query to only include pending authorization statements. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopePendingAuthorization($query) + { + return $query->where('authorized_status', 'pending'); + } + + /** + * Scope a query to only include approved statements. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeApproved($query) + { + return $query->where('authorized_status', 'approved'); + } + + /** + * Scope a query to only include rejected statements. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeRejected($query) + { + return $query->where('authorized_status', 'rejected'); + } + } diff --git a/database/migrations/2025_05_11_145009_create_periode_statements_table.php b/database/migrations/2025_05_11_145009_create_periode_statements_table.php new file mode 100644 index 0000000..b3f4348 --- /dev/null +++ b/database/migrations/2025_05_11_145009_create_periode_statements_table.php @@ -0,0 +1,48 @@ +id(); + $table->string('periode'); // Format YYYY-MM + $table->enum('status', ['pending', 'processing', 'completed', 'failed'])->default('pending'); + $table->enum('authorized_status', ['pending', 'approved', 'rejected'])->default('pending'); + $table->text('notes')->nullable(); + $table->timestamp('processed_at')->nullable(); + + // 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(); + + // Add unique constraint to prevent duplicate periods + $table->unique('periode'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('periode_statements'); + } + } diff --git a/module.json b/module.json index 02fc16a..25ee3c1 100644 --- a/module.json +++ b/module.json @@ -11,10 +11,21 @@ "files": [], "menu": { "main": [ + { + "title": "Periode Statement", + "path": "periode-statements", + "icon": "ki-filled ki-calendar text-lg text-primary", + "classes": "", + "attributes": [], + "permission": "", + "roles": [ + "administrator" + ] + }, { "title": "Kartu ATM", "path": "kartu-atm", - "icon": "ki-filled ki-credit-cart text-lg", + "icon": "ki-filled ki-credit-cart text-lg text-primary", "classes": "", "attributes": [], "permission": "", diff --git a/resources/views/periode-statement/create.blade.php b/resources/views/periode-statement/create.blade.php new file mode 100644 index 0000000..c69363d --- /dev/null +++ b/resources/views/periode-statement/create.blade.php @@ -0,0 +1,82 @@ +@extends('layouts.main') + +@section('breadcrumbs') + @if(isset($periodeStatement->id)) + {{ Breadcrumbs::render(request()->route()->getName(), $periodeStatement) }} + @else + {{ Breadcrumbs::render(request()->route()->getName()) }} + @endif +@endsection + +@section('content') +
| + + | ++ Periode + + | ++ Authorization Status + + | ++ Notes + + | ++ Processed At + + | +Action | +
|---|