Initial commit - expense
Deploy to VPS (PAT over HTTPS) / deploy (push) Has been cancelled

This commit is contained in:
root
2026-06-01 15:01:03 +07:00
parent 9e9fab4a3f
commit 1c1418f3e0
123 changed files with 18622 additions and 10376 deletions
@@ -0,0 +1,80 @@
= Illuminate\Database\Query\Builder {#6493
+connection: Illuminate\Database\MySqlConnection {#6495},
+grammar: Illuminate\Database\Query\Grammars\MySqlGrammar {#6477},
+processor: Illuminate\Database\Query\Processors\MySqlProcessor {#6476},
+bindings: [
"select" => [],
"from" => [],
"join" => [],
"where" => [],
"groupBy" => [],
"having" => [],
"order" => [],
"union" => [],
"unionOrder" => [],
],
+aggregate: null,
+columns: null,
+distinct: false,
+from: "budget_control",
+indexHint: null,
+joins: null,
+wheres: [],
+groups: null,
+havings: null,
+orders: null,
+limit: null,
+groupLimit: null,
+offset: null,
+unions: null,
+unionLimit: null,
+unionOffset: null,
+unionOrders: null,
+lock: null,
+beforeQueryCallbacks: [],
+operators: [
"=",
"<",
">",
"<=",
">=",
"<>",
"!=",
"<=>",
"like",
"like binary",
"not like",
"ilike",
"&",
"|",
"^",
"<<",
">>",
"&~",
"is",
"is not",
"rlike",
"not rlike",
"regexp",
"not regexp",
"~",
"~*",
"!~",
"!~*",
"similar to",
"not similar to",
"not ilike",
"~~*",
"!~~*",
],
+bitwiseOperators: [
"&",
"|",
"^",
"<<",
">>",
"&~",
],
+useWritePdo: false,
}
+7 -14
View File
@@ -1,15 +1,8 @@
/node_modules
/public/hot
/public/storage
/storage/*.key
/vendor
.env.backup
.phpunit.result.cache
docker-compose.override.yml
Homestead.json
Homestead.yaml
npm-debug.log
yarn-error.log
/.idea
/.vscode
.env
/node_modules
/.env
/storage/framework
/storage/logs
/storage/*.key
/public/storage
*.log
View File
@@ -0,0 +1,50 @@
= Illuminate\Support\Collection {#6508
all: [
1 => 3,
7 => 1,
8 => 5,
9 => 4,
11 => 1,
12 => 1,
13 => 3,
14 => 3,
15 => 4,
16 => 4,
17 => 5,
18 => 5,
19 => 4,
20 => 4,
21 => 4,
22 => 4,
23 => 4,
24 => 4,
25 => 4,
26 => 4,
27 => 5,
28 => 5,
29 => 5,
30 => 5,
31 => 5,
32 => 5,
33 => 5,
34 => 5,
35 => 5,
36 => 1,
37 => 1,
38 => 1,
39 => 1,
40 => 1,
41 => 1,
42 => 1,
43 => 1,
44 => 1,
45 => 3,
46 => 3,
47 => 3,
48 => 3,
49 => 3,
50 => 3,
51 => 3,
],
}
+34
View File
@@ -0,0 +1,34 @@
= Illuminate\Database\Eloquent\Collection {#8545
all: [
App\Models\FormUpCountry {#8380
id: 17,
expense_number: "RG-4-BGR-UPC-25040001",
tanggal: "2025-04-29",
status: "Rejected",
},
App\Models\FormUpCountry {#8372
id: 18,
expense_number: "RG-3-MDO-UPC-25050001",
tanggal: "2025-05-15",
status: "Rejected",
},
App\Models\FormUpCountry {#8381
id: 19,
expense_number: "RG-3-MDO-UPC-25050002",
tanggal: "2025-05-16",
status: "Rejected",
},
App\Models\FormUpCountry {#8382
id: 20,
expense_number: "RG-3-MDO-UPC-25050003",
tanggal: "2025-05-16",
status: "Rejected",
},
App\Models\FormUpCountry {#8383
id: 21,
expense_number: "RG-3-MDO-UPC-25050004",
tanggal: "2025-05-22",
status: "Rejected",
},
],
}
@@ -0,0 +1,97 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use App\Models\FormUpCountry;
use App\Models\FormVehicleRunningCost;
use App\Models\FormEntertaimentPresentation;
use App\Models\FormMeetingSeminar;
use App\Models\FormOthers;
use App\Helpers\AuditTrailHelper;
class AutoRejectExpense extends Command
{
/**
* Nama perintah yang akan dijalankan di terminal/cron
*/
protected $signature = 'expense:auto-reject';
/**
* Deskripsi perintah
*/
protected $description = 'Otomatis reject expense yang melewati batas waktu final approve (Tgl 13)';
public function handle()
{
$now = Carbon::now();
$closingDay = (int) env('CLOSING_DATE', 13);
$startDay = (int) env('STARTING_DATE', 11);
// Hanya jalankan eksekusi reject pada H+1 dari Closing Day (Misal: Tanggal 14 jam 00:01)
if ($now->day !== ($closingDay + 1)) {
$this->info("Hari ini bukan jadwal auto-reject. Jadwal berikutnya: Tanggal " . ($closingDay + 1));
return;
}
// Batas maksimal tanggal expense yang harus direject adalah H-1 dari Start Day (Misal: Tanggal 10 bulan ini)
// Karena tanggal 11 sudah masuk ke periode bulan depannya.
$batasTanggalPeriodeLalu = Carbon::create($now->year, $now->month, $startDay)->subDay()->endOfDay();
$models = [
FormUpCountry::class,
FormVehicleRunningCost::class,
FormEntertaimentPresentation::class,
FormMeetingSeminar::class,
FormOthers::class
];
// Daftar status yang dianggap menggantung / belum selesai
$pendingStatuses = ['On Progress', 'Approved 1', 'Approved 2', 'Waiting Approval', 'Validated'];
$totalRejected = 0;
DB::beginTransaction();
try {
foreach ($models as $model) {
// Tentukan kolom tanggal yang digunakan
$dateColumn = ($model === FormMeetingSeminar::class) ? 'created_at' : 'tanggal';
// Ambil semua data yang masih menggantung dari periode lalu
$pendingExpenses = $model::whereIn('status', $pendingStatuses)
->where($dateColumn, '<=', $batasTanggalPeriodeLalu)
->get();
foreach ($pendingExpenses as $expense) {
// Update status dan berikan alasan penolakan sesuai instruksi core business
$expense->update([
'status' => 'Rejected',
'remarks' => 'auto reject by system - lewat periode cut off',
'rejected_at' => Carbon::now(),
'rejected_by' => 0 // Diisi 0 sebagai flag penanda sistem otomatis
]);
// Tambahkan ke Audit Trail
$noExpense = $expense->expense_number ?? 'Unknown';
AuditTrailHelper::AddAuditTrail('System Update', "Auto Reject by System for Expense ({$noExpense})");
$totalRejected++;
}
}
DB::commit();
$msg = "Auto Reject berhasil dijalankan. Total direject: {$totalRejected} dokumen.";
$this->info($msg);
Log::info($msg);
} catch (\Exception $e) {
DB::rollBack();
$this->error("Gagal menjalankan Auto Reject: " . $e->getMessage());
Log::error("Cron Auto Reject Failed: " . $e->getMessage());
}
}
}
@@ -0,0 +1,111 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class MigrateOldExpenseData extends Command
{
// Perintah yang akan dieksekusi di terminal
protected $signature = 'expense:migrate-old';
protected $description = 'Migrasi data transaksional paling update dari expense-old ke database expense (v2.0) secara aman';
public function handle()
{
$this->info('Memulai Proses Sinkronisasi: Memindahkan data update dari [expense-old] ke [expense]...');
// Daftar tabel yang akan disinkronkan (Nama tabel diasumsikan sama pada kedua versi)
$tablesToMigrate = [
'form_up_country' => 'form_up_country',
'form_vehicle_running_cost' => 'form_vehicle_running_cost',
'form_entertainment_presentation' => 'form_entertainment_presentation',
'form_meeting_seminar' => 'form_meeting_seminar',
'form_others' => 'form_others',
];
foreach ($tablesToMigrate as $oldTable => $newTable) {
$this->comment("--------------------------------------------------");
$this->comment("Memproses Tabel Target: {$newTable}");
// 1. Validasi eksistensi tabel di database tujuan (expense)
if (!Schema::hasTable($newTable)) {
$this->error("Tabel [{$newTable}] tidak ditemukan di database 'expense'. Melewati tabel ini.");
continue;
}
// 2. Ambil list kolom lengkap yang ada di database tujuan (expense)
$destinationColumns = Schema::getColumnListing($newTable);
// 3. Ambil data transaksional paling update dari database lama (expense-old)
$oldData = DB::connection('mysql_lama')->table($oldTable)->get();
$insertedCount = 0;
$skippedCount = 0;
foreach ($oldData as $row) {
// 4. PROTEKSI ANTI-OVERWRITE: Cek apakah data sudah ada di database 'expense'
$isExists = DB::table($newTable)
->where('expense_number', $row->expense_number)
->exists();
if (!$isExists) {
$insertData = [];
// 5. MAPPING DATA SECARA DINAMIS
foreach ($destinationColumns as $column) {
// KUNCI PENYEMPURNAAN: Jangan bawa ID lama agar tidak tabrakan dengan Auto-Increment di DB baru
if ($column === 'id') {
continue;
}
// Jika kolom dari database baru tersedia di data 'expense-old', ambil nilainya
if (property_exists($row, $column) && !is_null($row->$column)) {
$insertData[$column] = $row->$column;
} else {
// Jika kolom baru versi 2.0 tidak ada di database lama / bernilai NULL, isi dengan nilai default aman
$insertData[$column] = $this->getDefaultValueForColumn($newTable, $column);
}
}
// 6. Jalankan amunisi Insert ke database utama (expense)
DB::table($newTable)->insert($insertData);
$insertedCount++;
} else {
$skippedCount++;
}
}
$this->info("Tabel [{$newTable}] Selesai: Masuk {$insertedCount} data baru. Diabaikan (Sudah ada) {$skippedCount} data.");
}
$this->info('==================================================');
$this->info('Sukses! Seluruh data dari [expense-old] telah bermigrasi ke [expense] dengan aman.');
}
/**
* Fallback Nilai Default untuk Kolom Baru Struktur Aplikasi 2.0
*/
private function getDefaultValueForColumn($tableName, $columnName)
{
// POINT 1: Solusi otomatisasi penanganan cabang_id yang NULL pada tabel entertainment
if ($tableName === 'form_entertainment_presentation' && $columnName === 'cabang_id') {
return 1; // Ganti angka 1 dengan ID Cabang default existing Anda
}
// Pengondisian khusus untuk kolom-kolom baru krusial di versi 2.0 Anda
if ($columnName === 'account_number') {
return '-';
}
if ($columnName === 'approved_total') {
return 0;
}
if ($columnName === 'approved_at' || $columnName === 'approved2_at' || $columnName === 'final_approved_at') {
return null;
}
// Nilai bawaan jika tidak masuk kategori spesifik di atas
return null;
}
}
+106
View File
@@ -0,0 +1,106 @@
<?php
namespace App\Exports;
use App\Models\AuditTrail;
use Maatwebsite\Excel\Concerns\FromQuery;
use Maatwebsite\Excel\Concerns\WithMapping;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithColumnWidths;
use Maatwebsite\Excel\Concerns\WithStyles;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class AuditTrailExport implements FromQuery, WithMapping, WithHeadings, WithColumnWidths, WithStyles
{
protected $filters;
public function __construct($filters)
{
$this->filters = $filters;
}
/**
* Menggunakan FromQuery untuk efisiensi memori pada data besar
*/
public function query()
{
$f = $this->filters;
$query = AuditTrail::with(['user.cabang.cabang.region']);
// Filter Region & Cabang
if (!empty($f['region']) || !empty($f['cabang'])) {
$query->whereHas('user.cabang.cabang', function($q) use ($f) {
if (!empty($f['cabang'])) {
$q->where('id', $f['cabang']);
}
if (!empty($f['region'])) {
$q->whereHas('region', function($rq) use ($f) {
// PERBAIKAN: Gunakan 'code' sesuai image_63aac6.png
$rq->where('code', $f['region']);
});
}
});
}
// Filter User & Tanggal...
if (!empty($f['admin_id'])) {
$query->where('user_id', $f['admin_id']);
}
return $query->latest();
}
/**
* Header Tabel Excel
*/
public function headings(): array
{
return [
'Waktu',
'User',
'IP Address',
'Aksi',
'Aktivitas',
'Browser'
];
}
/**
* Transformasi Data per baris
*/
public function map($audit): array
{
return [
$audit->created_at->format('d-m-Y H:i:s'),
$audit->user->name ?? 'System',
$audit->ip_address,
$audit->action,
$audit->activity,
$audit->browser,
];
}
/**
* Atur Lebar Kolom agar rapi secara otomatis
*/
public function columnWidths(): array
{
return [
'A' => 20,
'B' => 20,
'C' => 15,
'D' => 15,
'E' => 50,
'F' => 20,
];
}
/**
* Styling Header agar lebih profesional
*/
public function styles(Worksheet $sheet)
{
return [
1 => ['font' => ['bold' => true]],
];
}
}
+53
View File
@@ -0,0 +1,53 @@
<?php
namespace App\Exports;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithMapping;
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
class PendingDocumentExport implements FromCollection, WithHeadings, WithMapping, ShouldAutoSize
{
protected $cabangs;
public function __construct($cabangs)
{
$this->cabangs = $cabangs;
}
public function collection()
{
return $this->cabangs;
}
public function headings(): array
{
return [
'Region',
'Nama Cabang',
'Up Country',
'Vehicle',
'Entertainment',
'Meeting',
'Others',
'Total Pending',
'Budget Aktif'
];
}
public function map($cabang): array
{
return [
$cabang->region->name ?? '-',
$cabang->name,
$cabang->pending_details['up_country'] ?? 0,
$cabang->pending_details['vehicle'] ?? 0,
$cabang->pending_details['entertainment'] ?? 0,
$cabang->pending_details['meeting'] ?? 0,
$cabang->pending_details['others'] ?? 0,
$cabang->total_pending,
$cabang->available_budget
];
}
}
+37 -7
View File
@@ -4,29 +4,59 @@ namespace App\Helpers;
use Illuminate\Support\Facades\Auth;
use App\Models\AuditTrail;
use Illuminate\Support\Facades\Request;
class AuditTrailHelper
{
/**
* Mencatat aktivitas umum (Insert, Update, Delete, Approve, Reject)
*/
public static function AddAuditTrail($action, $activity)
{
// Menggunakan guard 'admin' sesuai spesifikasi login aplikasi Anda
$user = Auth::guard('admin')->user();
$audit = new AuditTrail();
$audit->user_id = Auth::user()->id;
$audit->ip_address = request()->ip();
$audit->user_id = $user ? $user->id : null;
$audit->ip_address = Request::ip();
$audit->activity = $activity;
$audit->action = $action;
$audit->user_agent = request()->userAgent();
$audit->browser = substr($audit->user_agent, 0, strpos($audit->user_agent, '/'));
$audit->user_agent = Request::userAgent();
$audit->browser = self::getBrowserName(Request::userAgent());
$audit->save();
}
/**
* Mencatat percobaan otentikasi (Login/Logout)
*/
public static function AuthAttempt($action, $activity)
{
$audit = new AuditTrail();
$audit->ip_address = request()->ip();
$audit->user_id = Auth::guard('admin')->id(); // Tetap coba ambil ID jika ada (misal saat logout)
$audit->ip_address = Request::ip();
$audit->activity = $activity;
$audit->action = $action;
$audit->user_agent = request()->userAgent();
$audit->browser = substr($audit->user_agent, 0, strpos($audit->user_agent, '/'));
$audit->user_agent = Request::userAgent();
$audit->browser = self::getBrowserName(Request::userAgent());
$audit->save();
}
/**
* Helper internal untuk deteksi browser yang lebih bersih
*/
private static function getBrowserName($userAgent)
{
if (!$userAgent) return 'Unknown';
if (strpos($userAgent, 'Opera') || strpos($userAgent, 'OPR')) return 'Opera';
if (strpos($userAgent, 'Edge')) return 'Edge';
if (strpos($userAgent, 'Chrome')) return 'Chrome';
if (strpos($userAgent, 'Safari')) return 'Safari';
if (strpos($userAgent, 'Firefox')) return 'Firefox';
if (strpos($userAgent, 'MSIE') || strpos($userAgent, 'Trident/7')) return 'Internet Explorer';
return explode('/', $userAgent)[0] ?? 'Unknown';
}
}
+300 -53
View File
@@ -2,97 +2,344 @@
namespace App\Helpers;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Support\Str;
use App\Models\Kategori;
use App\Models\BudgetControl;
use App\Models\FormUpCountry;
use App\Models\FormMeetingSeminar;
use App\Models\FormOthers;
use App\Models\FormEntertaimentPresentation;
use App\Models\FormVehicleRunningCost;
use App\Helpers\FormHelper;
use App\Models\UserHasCabang;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class BudgetHelper
{
/**
* Cache memori sementara untuk fungsi jurnal agar loading halaman riwayat tidak berat.
*/
private static $jurnalCache = [];
/**
* Daftar status yang akan memotong (menge-hold) sisa budget secara real-time.
*/
const HOLD_STATUSES = [
'Approved 1',
'Approved 2',
'Waiting Approval',
'Validated',
'Approved',
'Closed',
'Completed'
];
public static function getBulkAvailableBudget($budgetControls)
{
if ($budgetControls->isEmpty()) return [];
$cabangIds = $budgetControls->pluck('cabang_id')->unique()->toArray();
// 1. Mapping User per Cabang
$branchUsers = UserHasCabang::whereIn('cabang_id', $cabangIds)
->get()
->groupBy('cabang_id')
->map(fn($item) => $item->pluck('user_id')->toArray());
$allUserIds = $branchUsers->flatten()->unique()->toArray();
$resultsMap = [];
if (empty($allUserIds)) return [];
$formModels = [
'tanggal' => [
FormUpCountry::class,
FormVehicleRunningCost::class,
FormEntertaimentPresentation::class,
FormOthers::class
],
'created_at' => [
FormMeetingSeminar::class
]
];
// 2. Tarik data pengeluaran sekaligus dari semua tabel form
foreach ($formModels as $dateColumn => $models) {
foreach ($models as $model) {
$query = $model::whereIn('user_id', $allUserIds)
->whereIn('status', self::HOLD_STATUSES)
->select(
'user_id',
DB::raw("MONTH($dateColumn) as month"),
DB::raw("YEAR($dateColumn) as year"),
DB::raw("SUM(approved_total) as total")
)
->groupBy('user_id', 'month', 'year')
->get();
// 3. Masukkan ke Map berdasarkan Cabang_Bulan_Tahun
foreach ($query as $row) {
foreach ($branchUsers as $cabangId => $userIds) {
if (in_array($row->user_id, $userIds)) {
$monthName = strtolower(date('F', mktime(0, 0, 0, $row->month, 10)));
$key = "{$cabangId}_{$monthName}_{$row->year}";
$resultsMap[$key] = ($resultsMap[$key] ?? 0) + $row->total;
}
}
}
}
}
return $resultsMap;
}
/**
* Menghitung sisa budget (Pagu - Total Hold/Terpakai).
* Tetap menggunakan kolom tanggal/created_at karena pengajuan yang baru
* dibuat (Approved 1) belum memiliki final_approved_at.
*/
public static function getAvailableBudget($cabang_id, $periode_month, $periode_year)
{
// Validasi & normalisasi
$periode_month = strtolower($periode_month);
$periode_year = (int) $periode_year;
// Ambil budget sesuai cabang dan periode
$budget = BudgetControl::where('cabang_id', $cabang_id)
$totalPagu = BudgetControl::where('cabang_id', $cabang_id)
->where('periode_month', $periode_month)
->where('periode_year', $periode_year)
->with(['user.cabang'])
->get();
->sum('amount');
if ($totalPagu <= 0) return 0;
$userIds = UserHasCabang::where('cabang_id', $cabang_id)->pluck('user_id')->toArray();
if (empty($userIds)) return $totalPagu;
$usedBudget = 0;
$monthNumeric = date('m', strtotime($periode_month));
$users = UserHasCabang::where('cabang_id', $cabang_id)->get();
$userIds = $users->pluck('user_id')->toArray();
$formModels = [
FormUpCountry::class,
FormVehicleRunningCost::class,
FormEntertaimentPresentation::class,
FormMeetingSeminar::class,
FormOthers::class
];
$forms = FormHelper::getTotalAmount($userIds, $periode_month, $periode_year)
->sortByDesc('tanggal')
->sortByDesc('created_at');
foreach ($formModels as $model) {
$query = $model::whereIn('user_id', $userIds)
->whereIn('status', self::HOLD_STATUSES);
$usedBudget += $forms->sum('approved_total');
$availableBudget = $budget->sum('amount') - $usedBudget;
return $availableBudget;
if ($model === FormMeetingSeminar::class) {
$query->whereMonth('created_at', $monthNumeric)
->whereYear('created_at', $periode_year);
} else {
$query->whereMonth('tanggal', $monthNumeric)
->whereYear('tanggal', $periode_year);
}
$usedBudget += $query->sum('approved_total');
}
return $totalPagu - $usedBudget;
}
/**
* Memvalidasi apakah tanggal expense yang diinput user berada dalam
* rentang periode aktif (Tanggal 11 s/d 10).
*/
public static function checkExpenseInputWindow($tanggal_expense)
{
$inputDate = \Carbon\Carbon::parse($tanggal_expense)->startOfDay();
$today = \Carbon\Carbon::now();
$startDay = 11;
$endDay = 10;
// Menentukan jendela periode yang sedang aktif hari ini
if ($today->day >= $startDay) {
// Jika hari ini tgl 11 atau lebih: Jendela aktif adalah Tgl 11 bulan ini s/d Tgl 10 bulan depan
$periodStart = \Carbon\Carbon::create($today->year, $today->month, $startDay)->startOfDay();
$periodEnd = \Carbon\Carbon::create($today->year, $today->month, $endDay)->addMonth()->endOfDay();
} else {
// Jika hari ini tgl 1-10: Jendela aktif adalah Tgl 11 bulan lalu s/d Tgl 10 bulan ini
$periodStart = \Carbon\Carbon::create($today->year, $today->month, $startDay)->subMonth()->startOfDay();
$periodEnd = \Carbon\Carbon::create($today->year, $today->month, $endDay)->endOfDay();
}
// Jika tanggal form yang diinput user berada di luar jendela aktif
if ($inputDate->lt($periodStart) || $inputDate->gt($periodEnd)) {
return [
'is_valid' => false,
'message' => "Gagal! Batas input expense untuk periode aktif saat ini adalah {$periodStart->format('d M Y')} s/d {$periodEnd->format('d M Y')}. Tanggal {$inputDate->format('d M Y')} sudah ditutup atau belum dibuka."
];
}
return ['is_valid' => true];
}
/**
* Mengambil nominal per tipe form.
*/
public static function getNominalByFormType($userIds, $type, $type_id, $month, $year)
{
$monthNumeric = date('m', strtotime($month));
$used = 0;
if ($type == 'Up Country') {
$form = FormUpCountry::select('approved_total')
->whereIn('user_id', $userIds)
->whereMonth('tanggal', '=', date('m', strtotime($month)))
->whereYear('tanggal', '=', $year)
->whereIn('status', ['Approved', 'Closed'])
->get();
$used = FormUpCountry::whereIn('user_id', $userIds)
->whereMonth('tanggal', $monthNumeric)->whereYear('tanggal', $year)
->whereIn('status', self::HOLD_STATUSES)->sum('approved_total');
} elseif ($type == 'Vehicle Running Cost') {
$form = FormVehicleRunningCost::select('approved_total')
->whereIn('user_id', $userIds)
->whereMonth('tanggal', '=', date('m', strtotime($month)))
->whereYear('tanggal', '=', $year)
->whereIn('status', ['Approved', 'Closed'])
->get();
$used = FormVehicleRunningCost::whereIn('user_id', $userIds)
->whereMonth('tanggal', $monthNumeric)->whereYear('tanggal', $year)
->whereIn('status', self::HOLD_STATUSES)->sum('approved_total');
} elseif ($type == 'Entertainment') {
$form = FormEntertaimentPresentation::select('approved_total')
->whereIn('user_id', $userIds)
->whereMonth('tanggal', '=', date('m', strtotime($month)))
->whereYear('tanggal', '=', $year)
->whereIn('status', ['Approved', 'Closed'])
->get();
$used = FormEntertaimentPresentation::whereIn('user_id', $userIds)
->whereMonth('tanggal', $monthNumeric)->whereYear('tanggal', $year)
->whereIn('status', self::HOLD_STATUSES)->sum('approved_total');
} elseif ($type == 'Meeting / Seminar') {
$form = FormMeetingSeminar::select('approved_total')
->whereIn('user_id', $userIds)
->whereMonth('created_at', '=', date('m', strtotime($month)))
->whereYear('created_at', '=', $year)
->whereIn('status', ['Approved', 'Closed'])
->get();
$used = FormMeetingSeminar::whereIn('user_id', $userIds)
->whereMonth('created_at', $monthNumeric)->whereYear('created_at', $year)
->whereIn('status', self::HOLD_STATUSES)->sum('approved_total');
} else {
$form = FormOthers::select('approved_total')
->whereIn('user_id', $userIds)
->whereMonth('tanggal', '=', date('m', strtotime($month)))
->whereYear('tanggal', '=', $year)
$used = FormOthers::whereIn('user_id', $userIds)
->where('kategori_id', $type_id)
->whereIn('status', ['Approved', 'Closed'])
->get();
->whereMonth('tanggal', $monthNumeric)->whereYear('tanggal', $year)
->whereIn('status', self::HOLD_STATUSES)->sum('approved_total');
}
return $form->sum('approved_total');
return $used;
}
/**
* Mengambil tanggal closing.
*/
public static function getClosingDate($cabang_id)
{
$budget = BudgetControl::where('cabang_id', $cabang_id)->orderBy('created_at', 'desc')->first();
return $budget->closing_at;
return BudgetControl::where('cabang_id', $cabang_id)
->orderBy('created_at', 'desc')
->value('closing_at');
}
/**
* KALKULASI JURNAL AKUNTANSI
* Berlaku seragam menggunakan kolom final_approved_at untuk semua form
* Cut-off Date: Tgl 11 s/d Tgl 13 Bulan Berikutnya
*/
/**
* KALKULASI JURNAL AKUNTANSI (Log & Report)
* Sinkron dengan 12 Kategori Utama & Akun 800xxx
*/
// public static function getKalkulasiJurnal($cabang_id, $periode_month, $periode_year)
// {
// $periode_month = strtolower($periode_month);
// $periode_year = (int) $periode_year;
// $cacheKey = "{$cabang_id}_{$periode_month}_{$periode_year}";
// if (isset(self::$jurnalCache[$cacheKey])) return self::$jurnalCache[$cacheKey];
// $userIds = UserHasCabang::where('cabang_id', $cabang_id)->pluck('user_id')->toArray();
// $totalDebet = 0;
// if (!empty($userIds)) {
// // Ambil Range Tanggal (Contoh: 11 Maret - 13 April)
// $startDay = env('STARTING_DATE', 11);
// $closeDay = env('CLOSING_DATE', 13);
// $monthNumeric = date('m', strtotime($periode_month));
// $startDate = Carbon::create($periode_year, $monthNumeric, $startDay)->startOfDay();
// $endDate = Carbon::create($periode_year, $monthNumeric, $closeDay)->addMonth()->endOfDay();
// // Mapping Model dengan pengamanan Typo
// $formModels = [
// 'Up Country' => FormUpCountry::class,
// 'Vehicle Running Cost' => FormVehicleRunningCost::class,
// 'Entertainment' => FormEntertaimentPresentation::class,
// 'Entertaiment' => FormEntertaimentPresentation::class,
// 'Presentation' => FormEntertaimentPresentation::class,
// 'Meeting / Seminar' => FormMeetingSeminar::class,
// ];
// // Ambil kategori yang punya account_number dan tidak dikecualikan
// $kategoris = \App\Models\Kategori::whereNotIn('name', ['Cash in Bank BCA', 'Donasi/Gift', 'PPn Masukan', 'Sponsorship'])
// ->whereNotNull('account_number')
// ->where('account_number', '!=', '')
// ->get();
// foreach ($kategoris as $kat) {
// $katName = trim($kat->name);
// if (isset($formModels[$katName])) {
// $model = $formModels[$katName];
// $query = $model::whereIn('user_id', $userIds)
// ->whereIn('status', ['Closed', 'Final Approved'])
// ->whereBetween('final_approved_at', [$startDate, $endDate]);
// // PENTING: Filter berdasarkan kategori_id agar data tidak tertukar di tabel yang sama
// if (\Illuminate\Support\Facades\Schema::hasColumn((new $model)->getTable(), 'kategori_id')) {
// $query->where('kategori_id', $kat->id);
// }
// $totalDebet += $query->sum('approved_total');
// } else {
// // Masuk ke Form Others
// $totalDebet += FormOthers::whereIn('user_id', $userIds)
// ->where('kategori_id', $kat->id)
// ->whereIn('status', ['Closed', 'Final Approved'])
// ->whereBetween('final_approved_at', [$startDate, $endDate])
// ->sum('approved_total');
// }
// }
// }
// self::$jurnalCache[$cacheKey] = $totalDebet;
// return $totalDebet;
// }
public static function getBulkKalkulasiJurnal(array $lookups)
{
if (empty($lookups)) return [];
$resultsMap = [];
$startDay = 11; // Batas awal periode (Contoh: tgl 11)
$closeDay = 13; // Batas akhir approval (Contoh: tgl 13 bulan depan)
foreach ($lookups as $item) {
$cabId = $item['cabang_id'];
$monthNumeric = date('m', strtotime($item['month']));
$year = (int) $item['year'];
$key = $cabId . '_' . strtolower($item['month']) . '_' . $year;
// Penentuan Rentang Tanggal Cut-off Akuntansi
$startDate = \Illuminate\Support\Carbon::create($year, $monthNumeric, $startDay)->startOfDay();
$endDate = $startDate->copy()->addMonth()->day($closeDay)->endOfDay();
$userIds = \App\Models\UserHasCabang::where('cabang_id', $cabId)->pluck('user_id')->toArray();
if (empty($userIds)) {
$resultsMap[$key] = 0;
continue;
}
// Menghitung Total Pengeluaran Riil (Status: Closed / Final Approved)
$tables = ['form_up_country', 'form_vehicle_running_cost', 'form_entertainment_presentation', 'form_meeting_seminar', 'form_others'];
$totalExpense = 0;
foreach ($tables as $table) {
$totalExpense += \Illuminate\Support\Facades\DB::table($table)
->whereIn('user_id', $userIds)
->whereIn('status', ['Closed', 'Final Approved'])
->whereBetween('final_approved_at', [$startDate, $endDate])
->sum('approved_total');
}
$resultsMap[$key] = $totalExpense;
}
return $resultsMap;
}
}
+366
View File
@@ -0,0 +1,366 @@
<?php
namespace App\Helpers;
use App\Models\Admin;
use App\Models\FormEntertaimentPresentation;
use App\Models\FormMeetingSeminar;
use App\Models\FormOthers;
use App\Models\FormUpCountry;
use App\Models\FormVehicleRunningCost;
use Illuminate\Support\Facades\DB;
use App\Models\UserHasCabang;
class FormHelper
{
public static function getFormsByUserIds($userIds, $month, $year) {
$monthNumber = date('m', strtotime($month . ' 1'));
$startDate = "$year-$monthNumber-" . env('STARTING_DATE');
$nextMonthTimestamp = strtotime("first day of next month", strtotime("$year-$monthNumber-01"));
$closingDateNextMonth = date('Y-m-', $nextMonthTimestamp) . env('CLOSING_DATE');
$users = Admin::with(['region', 'cabang'])
->whereIn('id', $userIds)
->get();
$formUpCountryQuery = FormUpCountry::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
DB::raw("'Up Country' as type")
)->whereIn('user_id', $userIds)
->whereBetween('tanggal', [$startDate, $closingDateNextMonth]);
$formVehicleRunningCostQuery = FormVehicleRunningCost::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
DB::raw("'Vehicle Running Cost' as type")
)->whereIn('user_id', $userIds)
->whereBetween('tanggal', [$startDate, $closingDateNextMonth]);
$formEntertaimentPresentationQuery = FormEntertaimentPresentation::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
DB::raw("'Entertainment Presentation' as type")
)->whereIn('user_id', $userIds)
->whereBetween('tanggal', [$startDate, $closingDateNextMonth]);
$formMeetingSeminarQuery = FormMeetingSeminar::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
DB::raw("'Meeting Seminar' as type")
)->whereIn('user_id', $userIds)
->whereBetween('created_at', [$startDate, $closingDateNextMonth]);
$formOthersQuery = FormOthers::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
DB::raw("'Others' as type")
)->whereIn('user_id', $userIds)
->whereBetween('tanggal', [$startDate, $closingDateNextMonth]);
// Use union to combine queries
$forms = $formUpCountryQuery
->union($formVehicleRunningCostQuery)
->union($formEntertaimentPresentationQuery)
->union($formMeetingSeminarQuery)
->union($formOthersQuery)
->get();
// Return forms with eager loaded user details
return $forms->map(function ($form) use ($users) {
$user = $users->firstWhere('id', $form->user_id);
$form->user = $user;
return $form;
});
}
public static function getAllForms($month, $year) {
$monthNumber = date('m', strtotime($month . ' 1'));
$startDate = "$year-$monthNumber-" . env('STARTING_DATE');
$nextMonthTimestamp = strtotime("first day of next month", strtotime("$year-$monthNumber-01"));
$closingDateNextMonth = date('Y-m-', $nextMonthTimestamp) . env('CLOSING_DATE');
$users = Admin::with(['region', 'cabang'])
->get();
$formUpCountryQuery = FormUpCountry::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
DB::raw("'Up Country' as type")
)->whereBetween('tanggal', [$startDate, $closingDateNextMonth]);
$formVehicleRunningCostQuery = FormVehicleRunningCost::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
DB::raw("'Vehicle Running Cost' as type")
)->whereBetween('tanggal', [$startDate, $closingDateNextMonth]);
$formEntertaimentPresentationQuery = FormEntertaimentPresentation::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
DB::raw("'Entertainment Presentation' as type")
)->whereBetween('tanggal', [$startDate, $closingDateNextMonth]);
$formMeetingSeminarQuery = FormMeetingSeminar::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
DB::raw("'Meeting Seminar' as type")
)->whereBetween('created_at', [$startDate, $closingDateNextMonth]);
$formOthersQuery = FormOthers::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
DB::raw("'Others' as type")
)->whereBetween('tanggal', [$startDate, $closingDateNextMonth]);
// Combine queries using union
$forms = $formUpCountryQuery
->union($formVehicleRunningCostQuery)
->union($formEntertaimentPresentationQuery)
->union($formMeetingSeminarQuery)
->union($formOthersQuery)
->get();
// Map users to the forms with eager loaded data
return $forms->map(function ($form) use ($users) {
$user = $users->firstWhere('id', $form->user_id);
$form->user = $user;
return $form;
});
}
public static function getAllForms2() {
// Get all users with eager loading for 'region' and 'cabang'
$users = Admin::with(['region', 'cabang'])
->get();
$formUpCountryQuery = FormUpCountry::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
DB::raw("'Up Country' as type")
);
$formVehicleRunningCostQuery = FormVehicleRunningCost::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
DB::raw("'Vehicle Running Cost' as type")
);
$formEntertaimentPresentationQuery = FormEntertaimentPresentation::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
DB::raw("'Entertainment Presentation' as type")
);
$formMeetingSeminarQuery = FormMeetingSeminar::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
DB::raw("'Meeting Seminar' as type")
);
$formOthersQuery = FormOthers::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
DB::raw("'Others' as type")
);
// Combine queries using union
$forms = $formUpCountryQuery
->union($formVehicleRunningCostQuery)
->union($formEntertaimentPresentationQuery)
->union($formMeetingSeminarQuery)
->union($formOthersQuery)
->get();
// Map users to the forms with eager loaded data
return $forms->map(function ($form) use ($users) {
$user = $users->firstWhere('id', $form->user_id);
$form->user = $user;
return $form;
});
}
public static function getNominalByFormType($cabang_id, $type, $type_id, $month, $year) {
$users = UserHasCabang::where('cabang_id', $cabang_id)->get();
$userIds = $users->pluck('user_id')->toArray();
if($type == 'Up Country') {
$form = FormUpCountry::select('approved_total')->whereIn('user_id', $userIds)->whereMonth('tanggal', '=', date('m', strtotime($month)))->whereYear('tanggal', '=', $year)->where('status', ['Approved', 'Closed'])->get();
} else if($type == 'Vehicle Running Cost') {
$form = FormVehicleRunningCost::select('approved_total')->whereIn('user_id', $userIds)->whereMonth('tanggal', '=', date('m', strtotime($month)))->whereYear('tanggal', '=', $year)->where('status', ['Approved', 'Closed'])->get();
} else if($type == 'Entertainment') {
$form = FormEntertaimentPresentation::select('approved_total')->whereIn('user_id', $userIds)->whereMonth('tanggal', '=', date('m', strtotime($month)))->whereYear('tanggal', '=', $year)->where('status', ['Approved', 'Closed'])->get();
} else if($type == 'Meeting / Seminar') {
$form = FormMeetingSeminar::select('approved_total')->whereIn('user_id', $userIds)->whereMonth('created_at', '=', date('m', strtotime($month)))->whereYear('created_at', '=', $year)->where('status', ['Approved', 'Closed'])->get();
} else {
$form = FormOthers::select('approved_total')->whereIn('user_id', $userIds)->whereMonth('tanggal', '=', date('m', strtotime($month)))->whereYear('tanggal', '=', $year)->where('kategori_id', $type_id)->where('status', ['Approved', 'Closed'])->get();
}
$nominal = $form->sum('approved_total');
return $nominal;
}
public static function getTotalAmount($userIds, $month, $year) {
$monthNumber = date('m', strtotime($month . ' 1'));
$startDate = "$year-$monthNumber-" . env('STARTING_DATE');
$nextMonthTimestamp = strtotime("first day of next month", strtotime("$year-$monthNumber-01"));
$closingDateNextMonth = date('Y-m-', $nextMonthTimestamp) . env('CLOSING_DATE');
$users = Admin::with(['region', 'cabang'])
->whereIn('id', $userIds)
->get();
$formUpCountryQuery = FormUpCountry::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
DB::raw("'Up Country' as type")
)->whereIn('user_id', $userIds)
->whereBetween('final_approved_at', [$startDate, $closingDateNextMonth]);
$formVehicleRunningCostQuery = FormVehicleRunningCost::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
DB::raw("'Vehicle Running Cost' as type")
)->whereIn('user_id', $userIds)
->whereBetween('final_approved_at', [$startDate, $closingDateNextMonth]);
$formEntertaimentPresentationQuery = FormEntertaimentPresentation::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
DB::raw("'Entertainment Presentation' as type")
)->whereIn('user_id', $userIds)
->whereBetween('final_approved_at', [$startDate, $closingDateNextMonth]);
$formMeetingSeminarQuery = FormMeetingSeminar::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
DB::raw("'Meeting Seminar' as type")
)->whereIn('user_id', $userIds)
->whereBetween('final_approved_at', [$startDate, $closingDateNextMonth]);
$formOthersQuery = FormOthers::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
DB::raw("'Others' as type")
)->whereIn('user_id', $userIds)
->whereBetween('final_approved_at', [$startDate, $closingDateNextMonth]);
// Use union to combine queries
$forms = $formUpCountryQuery
->union($formVehicleRunningCostQuery)
->union($formEntertaimentPresentationQuery)
->union($formMeetingSeminarQuery)
->union($formOthersQuery)
->get();
// Return forms with eager loaded user details
return $forms->map(function ($form) use ($users) {
$user = $users->firstWhere('id', $form->user_id);
$form->user = $user;
return $form;
});
}
}
+130 -284
View File
@@ -10,81 +10,81 @@ use App\Models\FormUpCountry;
use App\Models\FormVehicleRunningCost;
use Illuminate\Support\Facades\DB;
use App\Models\UserHasCabang;
use Illuminate\Support\Carbon;
class FormHelper
{
public static function getFormsByUserIds($userIds, $month, $year) {
$monthNumber = date('m', strtotime($month . ' 1'));
$startDate = "$year-$monthNumber-" . env('STARTING_DATE');
/**
* Helper internal untuk menghitung rentang siklus tanggal
* Menghasilkan: Start (11), Max Input (10 bln depan), Closing (13 bln depan)
*/
private static function getCycleRange($month, $year)
{
// Pastikan di-cast ke (int) agar Carbon tidak error
$startDay = (int) env('STARTING_DATE', 11);
$maxInputDay = (int) env('INPUT_MAX_DATE', 10);
$closingDay = (int) env('CLOSING_DATE', 13);
$nextMonthTimestamp = strtotime("first day of next month", strtotime("$year-$monthNumber-01"));
$closingDateNextMonth = date('Y-m-', $nextMonthTimestamp) . env('CLOSING_DATE');
// Mengambil nomor bulan sebagai integer
$monthNumber = (int) date('m', strtotime($month . ' 1'));
// Pastikan $year juga integer
$year = (int) $year;
// Carbon::createFromDate membutuhkan integer
$startDate = \Illuminate\Support\Carbon::createFromDate($year, $monthNumber, $startDay)->startOfDay();
// Input maksimal dan Closing menggunakan method ->day() yang butuh integer
$maxInputDate = $startDate->copy()->addMonth()->day($maxInputDay)->endOfDay();
$closingDate = $startDate->copy()->addMonth()->day($closingDay)->endOfDay();
return [
'start' => $startDate->toDateTimeString(),
'input_max' => $maxInputDate->toDateTimeString(),
'closing' => $closingDate->toDateTimeString(),
'display_label' => $startDate->format('d M') . ' - ' . $maxInputDate->format('d M Y')
];
}
public static function getFormsByUserIds($userIds, $month, $year) {
$dates = self::getCycleRange($month, $year);
$startDate = $dates['start'];
$closingDate = $dates['closing']; // Mengacu ke Closing Date (13)
$users = Admin::with(['region', 'cabang'])
->whereIn('id', $userIds)
->get();
$formUpCountryQuery = FormUpCountry::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
'expense_number', 'user_id', 'total', 'approved_total', 'status', 'account_number', 'created_at',
DB::raw("'Up Country' as type")
)->whereIn('user_id', $userIds)
->whereBetween('tanggal', [$startDate, $closingDateNextMonth]);
->whereBetween('tanggal', [$startDate, $closingDate]);
$formVehicleRunningCostQuery = FormVehicleRunningCost::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
'expense_number', 'user_id', 'total', 'approved_total', 'status', 'account_number', 'created_at',
DB::raw("'Vehicle Running Cost' as type")
)->whereIn('user_id', $userIds)
->whereBetween('tanggal', [$startDate, $closingDateNextMonth]);
->whereBetween('tanggal', [$startDate, $closingDate]);
$formEntertaimentPresentationQuery = FormEntertaimentPresentation::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
'expense_number', 'user_id', 'total', 'approved_total', 'status', 'account_number', 'created_at',
DB::raw("'Entertainment Presentation' as type")
)->whereIn('user_id', $userIds)
->whereBetween('tanggal', [$startDate, $closingDateNextMonth]);
->whereBetween('tanggal', [$startDate, $closingDate]);
$formMeetingSeminarQuery = FormMeetingSeminar::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
'expense_number', 'user_id', 'total', 'approved_total', 'status', 'account_number', 'created_at',
DB::raw("'Meeting Seminar' as type")
)->whereIn('user_id', $userIds)
->whereBetween('created_at', [$startDate, $closingDateNextMonth]);
->whereBetween('created_at', [$startDate, $closingDate]);
$formOthersQuery = FormOthers::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
'expense_number', 'user_id', 'total', 'approved_total', 'status', 'account_number', 'created_at',
DB::raw("'Others' as type")
)->whereIn('user_id', $userIds)
->whereBetween('tanggal', [$startDate, $closingDateNextMonth]);
->whereBetween('tanggal', [$startDate, $closingDate]);
// Use union to combine queries
$forms = $formUpCountryQuery
->union($formVehicleRunningCostQuery)
->union($formEntertaimentPresentationQuery)
@@ -92,7 +92,6 @@ class FormHelper
->union($formOthersQuery)
->get();
// Return forms with eager loaded user details
return $forms->map(function ($form) use ($users) {
$user = $users->firstWhere('id', $form->user_id);
$form->user = $user;
@@ -101,79 +100,90 @@ class FormHelper
}
public static function getAllForms($month, $year) {
$monthNumber = date('m', strtotime($month . ' 1'));
$startDate = "$year-$monthNumber-" . env('STARTING_DATE');
$dates = self::getCycleRange($month, $year);
$startDate = $dates['start'];
$closingDate = $dates['closing'];
$nextMonthTimestamp = strtotime("first day of next month", strtotime("$year-$monthNumber-01"));
$closingDateNextMonth = date('Y-m-', $nextMonthTimestamp) . env('CLOSING_DATE');
$users = Admin::with(['region', 'cabang'])->get();
$users = Admin::with(['region', 'cabang'])
->get();
$commonSelect = ['expense_number', 'user_id', 'total', 'approved_total', 'status', 'account_number', 'created_at'];
$formUpCountryQuery = FormUpCountry::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
DB::raw("'Up Country' as type")
)->whereBetween('tanggal', [$startDate, $closingDateNextMonth]);
$queries = [
FormUpCountry::select(array_merge($commonSelect, [DB::raw("'Up Country' as type")]))->whereBetween('tanggal', [$startDate, $closingDate]),
FormVehicleRunningCost::select(array_merge($commonSelect, [DB::raw("'Vehicle Running Cost' as type")]))->whereBetween('tanggal', [$startDate, $closingDate]),
FormEntertaimentPresentation::select(array_merge($commonSelect, [DB::raw("'Entertainment Presentation' as type")]))->whereBetween('tanggal', [$startDate, $closingDate]),
FormMeetingSeminar::select(array_merge($commonSelect, [DB::raw("'Meeting Seminar' as type")]))->whereBetween('created_at', [$startDate, $closingDate]),
FormOthers::select(array_merge($commonSelect, [DB::raw("'Others' as type")]))->whereBetween('tanggal', [$startDate, $closingDate]),
];
$formVehicleRunningCostQuery = FormVehicleRunningCost::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
DB::raw("'Vehicle Running Cost' as type")
)->whereBetween('tanggal', [$startDate, $closingDateNextMonth]);
$mainQuery = array_shift($queries);
foreach ($queries as $q) { $mainQuery->union($q); }
$formEntertaimentPresentationQuery = FormEntertaimentPresentation::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
DB::raw("'Entertainment Presentation' as type")
)->whereBetween('tanggal', [$startDate, $closingDateNextMonth]);
$forms = $mainQuery->get();
$formMeetingSeminarQuery = FormMeetingSeminar::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
DB::raw("'Meeting Seminar' as type")
)->whereBetween('created_at', [$startDate, $closingDateNextMonth]);
return $forms->map(function ($form) use ($users) {
$user = $users->firstWhere('id', $form->user_id);
$form->user = $user;
return $form;
});
}
$formOthersQuery = FormOthers::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
DB::raw("'Others' as type")
)->whereBetween('tanggal', [$startDate, $closingDateNextMonth]);
/**
* Dashboard Info: Menghasilkan label periode input (11-10)
*/
public static function getDashboardPeriodLabel($month, $year) {
return self::getCycleRange($month, $year)['display_label'];
}
// Combine queries using union
$forms = $formUpCountryQuery
->union($formVehicleRunningCostQuery)
->union($formEntertaimentPresentationQuery)
->union($formMeetingSeminarQuery)
->union($formOthersQuery)
->get();
public static function getNominalByFormType($cabang_id, $type, $type_id, $month, $year) {
$users = UserHasCabang::where('cabang_id', $cabang_id)->get();
$userIds = $users->pluck('user_id')->toArray();
// Menggunakan logika cycle agar nominal yang muncul sinkron dengan cutoff
$dates = self::getCycleRange($month, $year);
$start = $dates['start'];
$end = $dates['closing'];
$query = null;
if($type == 'Up Country') {
$query = FormUpCountry::whereIn('user_id', $userIds)->whereBetween('tanggal', [$start, $end]);
} else if($type == 'Vehicle Running Cost') {
$query = FormVehicleRunningCost::whereIn('user_id', $userIds)->whereBetween('tanggal', [$start, $end]);
} else if($type == 'Entertainment') {
$query = FormEntertaimentPresentation::whereIn('user_id', $userIds)->whereBetween('tanggal', [$start, $end]);
} else if($type == 'Meeting / Seminar') {
$query = FormMeetingSeminar::whereIn('user_id', $userIds)->whereBetween('created_at', [$start, $end]);
} else {
$query = FormOthers::whereIn('user_id', $userIds)->whereBetween('tanggal', [$start, $end])->where('kategori_id', $type_id);
}
return $query->whereIn('status', ['Approved', 'Closed'])->sum('approved_total');
}
public static function getTotalAmount($userIds, $month, $year) {
$dates = self::getCycleRange($month, $year);
$startDate = $dates['start'];
$closingDate = $dates['closing'];
$users = Admin::with(['region', 'cabang'])->whereIn('id', $userIds)->get();
$formUpCountryQuery = FormUpCountry::select('expense_number', 'user_id', 'total', 'approved_total', 'status', 'account_number', 'created_at', DB::raw("'Up Country' as type"))
->whereIn('user_id', $userIds)->whereBetween('final_approved_at', [$startDate, $closingDate]);
$formVehicleRunningCostQuery = FormVehicleRunningCost::select('expense_number', 'user_id', 'total', 'approved_total', 'status', 'account_number', 'created_at', DB::raw("'Vehicle Running Cost' as type"))
->whereIn('user_id', $userIds)->whereBetween('final_approved_at', [$startDate, $closingDate]);
$formEntertaimentPresentationQuery = FormEntertaimentPresentation::select('expense_number', 'user_id', 'total', 'approved_total', 'status', 'account_number', 'created_at', DB::raw("'Entertainment Presentation' as type"))
->whereIn('user_id', $userIds)->whereBetween('final_approved_at', [$startDate, $closingDate]);
$formMeetingSeminarQuery = FormMeetingSeminar::select('expense_number', 'user_id', 'total', 'approved_total', 'status', 'account_number', 'created_at', DB::raw("'Meeting Seminar' as type"))
->whereIn('user_id', $userIds)->whereBetween('final_approved_at', [$startDate, $closingDate]);
$formOthersQuery = FormOthers::select('expense_number', 'user_id', 'total', 'approved_total', 'status', 'account_number', 'created_at', DB::raw("'Others' as type"))
->whereIn('user_id', $userIds)->whereBetween('final_approved_at', [$startDate, $closingDate]);
$forms = $formUpCountryQuery->union($formVehicleRunningCostQuery)->union($formEntertaimentPresentationQuery)->union($formMeetingSeminarQuery)->union($formOthersQuery)->get();
// Map users to the forms with eager loaded data
return $forms->map(function ($form) use ($users) {
$user = $users->firstWhere('id', $form->user_id);
$form->user = $user;
@@ -182,181 +192,17 @@ class FormHelper
}
public static function getAllForms2() {
// Get all users with eager loading for 'region' and 'cabang'
$users = Admin::with(['region', 'cabang'])
// Fungsi ini tetap dipertahankan sesuai request (tanpa filter tanggal)
$users = Admin::with(['region', 'cabang'])->get();
$commonSelect = ['expense_number', 'user_id', 'total', 'approved_total', 'status', 'account_number', 'created_at'];
$forms = FormUpCountry::select(array_merge($commonSelect, [DB::raw("'Up Country' as type")]))
->union(FormVehicleRunningCost::select(array_merge($commonSelect, [DB::raw("'Vehicle Running Cost' as type")])))
->union(FormEntertaimentPresentation::select(array_merge($commonSelect, [DB::raw("'Entertainment Presentation' as type")])))
->union(FormMeetingSeminar::select(array_merge($commonSelect, [DB::raw("'Meeting Seminar' as type")])))
->union(FormOthers::select(array_merge($commonSelect, [DB::raw("'Others' as type")])))
->get();
$formUpCountryQuery = FormUpCountry::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
DB::raw("'Up Country' as type")
);
$formVehicleRunningCostQuery = FormVehicleRunningCost::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
DB::raw("'Vehicle Running Cost' as type")
);
$formEntertaimentPresentationQuery = FormEntertaimentPresentation::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
DB::raw("'Entertainment Presentation' as type")
);
$formMeetingSeminarQuery = FormMeetingSeminar::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
DB::raw("'Meeting Seminar' as type")
);
$formOthersQuery = FormOthers::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
DB::raw("'Others' as type")
);
// Combine queries using union
$forms = $formUpCountryQuery
->union($formVehicleRunningCostQuery)
->union($formEntertaimentPresentationQuery)
->union($formMeetingSeminarQuery)
->union($formOthersQuery)
->get();
// Map users to the forms with eager loaded data
return $forms->map(function ($form) use ($users) {
$user = $users->firstWhere('id', $form->user_id);
$form->user = $user;
return $form;
});
}
public static function getNominalByFormType($cabang_id, $type, $type_id, $month, $year) {
$users = UserHasCabang::where('cabang_id', $cabang_id)->get();
$userIds = $users->pluck('user_id')->toArray();
if($type == 'Up Country') {
$form = FormUpCountry::select('approved_total')->whereIn('user_id', $userIds)->whereMonth('tanggal', '=', date('m', strtotime($month)))->whereYear('tanggal', '=', $year)->where('status', ['Approved', 'Closed'])->get();
} else if($type == 'Vehicle Running Cost') {
$form = FormVehicleRunningCost::select('approved_total')->whereIn('user_id', $userIds)->whereMonth('tanggal', '=', date('m', strtotime($month)))->whereYear('tanggal', '=', $year)->where('status', ['Approved', 'Closed'])->get();
} else if($type == 'Entertainment') {
$form = FormEntertaimentPresentation::select('approved_total')->whereIn('user_id', $userIds)->whereMonth('tanggal', '=', date('m', strtotime($month)))->whereYear('tanggal', '=', $year)->where('status', ['Approved', 'Closed'])->get();
} else if($type == 'Meeting / Seminar') {
$form = FormMeetingSeminar::select('approved_total')->whereIn('user_id', $userIds)->whereMonth('created_at', '=', date('m', strtotime($month)))->whereYear('created_at', '=', $year)->where('status', ['Approved', 'Closed'])->get();
} else {
$form = FormOthers::select('approved_total')->whereIn('user_id', $userIds)->whereMonth('tanggal', '=', date('m', strtotime($month)))->whereYear('tanggal', '=', $year)->where('kategori_id', $type_id)->where('status', ['Approved', 'Closed'])->get();
}
$nominal = $form->sum('approved_total');
return $nominal;
}
public static function getTotalAmount($userIds, $month, $year) {
$monthNumber = date('m', strtotime($month . ' 1'));
$startDate = "$year-$monthNumber-" . env('STARTING_DATE');
$nextMonthTimestamp = strtotime("first day of next month", strtotime("$year-$monthNumber-01"));
$closingDateNextMonth = date('Y-m-', $nextMonthTimestamp) . env('CLOSING_DATE');
$users = Admin::with(['region', 'cabang'])
->whereIn('id', $userIds)
->get();
$formUpCountryQuery = FormUpCountry::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
DB::raw("'Up Country' as type")
)->whereIn('user_id', $userIds)
->whereBetween('final_approved_at', [$startDate, $closingDateNextMonth]);
$formVehicleRunningCostQuery = FormVehicleRunningCost::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
DB::raw("'Vehicle Running Cost' as type")
)->whereIn('user_id', $userIds)
->whereBetween('final_approved_at', [$startDate, $closingDateNextMonth]);
$formEntertaimentPresentationQuery = FormEntertaimentPresentation::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
DB::raw("'Entertainment Presentation' as type")
)->whereIn('user_id', $userIds)
->whereBetween('final_approved_at', [$startDate, $closingDateNextMonth]);
$formMeetingSeminarQuery = FormMeetingSeminar::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
DB::raw("'Meeting Seminar' as type")
)->whereIn('user_id', $userIds)
->whereBetween('final_approved_at', [$startDate, $closingDateNextMonth]);
$formOthersQuery = FormOthers::select(
'expense_number',
'user_id',
'total',
'approved_total',
'status',
'account_number',
'created_at',
DB::raw("'Others' as type")
)->whereIn('user_id', $userIds)
->whereBetween('final_approved_at', [$startDate, $closingDateNextMonth]);
// Use union to combine queries
$forms = $formUpCountryQuery
->union($formVehicleRunningCostQuery)
->union($formEntertaimentPresentationQuery)
->union($formMeetingSeminarQuery)
->union($formOthersQuery)
->get();
// Return forms with eager loaded user details
return $forms->map(function ($form) use ($users) {
$user = $users->firstWhere('id', $form->user_id);
$form->user = $user;
+11 -7
View File
@@ -1,19 +1,23 @@
<?php
namespace App\Helpers;
use Illuminate\Support\Carbon;
class PeriodeHelper
{
public static function getPeriodeRange($month, $year)
/**
* Mendapatkan rentang tanggal fisik pengakuan untuk sebuah periode akuntansi (Bulan & Tahun)
* Contoh: Periode "april 2026" -> Start: 11 April 2026, End: 13 Mei 2026 (Closing Date)
*/
public static function getPeriodeRange(string $month, int $year): array
{
$month = (int) date('m', strtotime($month));
$year = (int) $year;
// 1. Tentukan tanggal 11 di bulan dan tahun yang dipilih
$startDate = Carbon::parse("11 $month $year")->startOfDay()->toDateTimeString();
// Jika Januari, periode dimulai dari 11 Januari hingga 10 Februari
$start = Carbon::create($year, $month, 11)->startOfDay();
$end = $start->copy()->addMonth()->subDay()->endOfDay();
// 2. Tentukan tanggal 13 di bulan berikutnya (Bulan berjalan + 1)
$endDate = Carbon::parse("13 $month $year")->addMonth()->endOfDay()->toDateTimeString();
return [$start, $end];
return [$startDate, $endDate];
}
}
@@ -6,26 +6,108 @@ namespace App\Http\Controllers\Backend;
use App\Http\Controllers\Controller;
use App\Models\AuditTrail;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
use App\Models\Admin; // Pastikan Model Admin diimport
use App\Models\Region; // Pastikan Model Region diimport
use App\Models\Cabang; // Pastikan Model Cabang diimport
use App\Helpers\AuditTrailHelper;
use Illuminate\Http\Request; // BARIS INI YANG WAJIB ADA
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use App\Exports\AuditTrailExport;
use Maatwebsite\Excel\Facades\Excel;
class AuditTrailController extends Controller
{
public function index()
{
public function index(Request $request)
{
$pageInfo = [
'title' => 'Audit Trails',
'sub_title' => 'List of Audit Trails System',
'path' => url()->current()
];
return view(
'backend.pages.audit_trail.index',
[
// sort by newest
'data' => AuditTrail::with('user')->orderBy('created_at', 'desc')->get(),
$query = AuditTrail::with(['user.cabang.cabang.region']);
// Filter: User
if ($request->filled('admin_id')) {
$query->where('user_id', $request->admin_id);
}
// Filter: Region & Cabang (Sesuai relasi hasOne UserHasCabang)
if ($request->filled('region') || $request->filled('cabang')) {
$query->whereHas('user.cabang.cabang', function($q) use ($request) {
if ($request->filled('cabang')) {
$q->where('id', $request->cabang);
}
if ($request->filled('region')) {
$q->whereHas('region', function($rq) use ($request) {
// PERBAIKAN: Gunakan 'code' sesuai image_63aac6.png
$rq->where('code', $request->region);
});
}
});
}
// Filter: Rentang Waktu
if ($request->filled('start_date') && $request->filled('end_date')) {
$query->whereBetween('created_at', [
$request->start_date . ' 00:00:00',
$request->end_date . ' 23:59:59'
]);
}
$data = $query->latest()->paginate(50)->appends($request->all());
return view('backend.pages.audit_trail.index', [
'data' => $data,
'pageInfo' => $pageInfo,
]
);
'admins' => \App\Models\Admin::select('id', 'name')->orderBy('name')->get(),
// PERBAIKAN: Select 'code' sesuai image_63aac6.png
'regions' => \App\Models\Region::select('code', 'name')->get(),
'cabangs' => \App\Models\Cabang::select('id', 'name')->orderBy('name')->get(),
]);
}
public function exportExcel(Request $request)
{
AuditTrailHelper::AddAuditTrail('Export', 'Melakukan export data Audit Trail dengan filter tertentu');
// Mengirimkan parameter filter ke class Export
return Excel::download(new AuditTrailExport($request->all()), 'Audit_Trail_' . date('Y-m-d') . '.xlsx');
}
/**
* Membersihkan semua data audit trail (Truncate)
*/
public function clearAll()
{
try {
// Catat siapa yang menghapus log sebelum data dihapus
AuditTrailHelper::AddAuditTrail('Delete', 'Membersihkan (Truncate) seluruh data Audit Trail');
// Menggunakan truncate untuk performa maksimal pada tabel besar
DB::table('audit_trail')->truncate();
return redirect()->back()->with('success', 'All audit logs have been cleared.');
} catch (\Exception $e) {
return redirect()->back()->with('error', 'Failed to clear logs: ' . $e->getMessage());
}
}
/**
* Fungsi untuk membersihkan cache sistem
*/
public function clearCache()
{
try {
Artisan::call('optimize:clear');
AuditTrailHelper::AddAuditTrail('System', 'Membersihkan cache sistem (Optimize Clear)');
return redirect()->back()->with('success', 'System cache cleared successfully!');
} catch (\Exception $e) {
return redirect()->back()->with('error', 'Failed to clear cache: ' . $e->getMessage());
}
}
}
@@ -11,164 +11,617 @@ use App\Models\BudgetControlLog;
use App\Models\Admin;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Maatwebsite\Excel\Facades\Excel;
use Maatwebsite\Excel\Concerns\FromArray;
use Maatwebsite\Excel\Concerns\WithHeadings;
use App\Helpers\PeriodeHelper;
class BudgetControlController extends Controller
{
public function index()
{
/**
* Halaman Utama Budget Control (Termasuk Data Grafik)
*/
public function index(Request $request)
{
$this->checkAuthorization(auth()->user(), ['budget_control.view']);
session()->put('redirect_url', route('budget_control'));
$role = auth()->user()->getRoleNames()[0];
$budget = null;
$user = auth()->user();
$role = $user->getRoleNames()->first();
if ($role == 'Admin Region' || $role == 'Marketing Operational Manager Region') {
$region_id = auth()->user()->getMyCabangAndRegionId()['region'];
// --- 1. IDENTIFIKASI WILAYAH USER (Sama seperti Dashboard) ---
// Mengambil semua cabang yang di-assign ke user ini
$userCabangIds = \App\Models\UserHasCabang::where('user_id', $user->id)
->pluck('cabang_id')
->toArray();
if ($region_id) {
$budget = BudgetControl::whereHas('cabang', function ($query) use ($region_id) {
$query->where('region_id', $region_id);
})->with(['receiver', 'sender', 'cabang'])->get();
}
} elseif ($role == 'Area Manager Cabang') {
$cabang_id = auth()->user()->getMyCabangAndRegionId()['cabang'];
// Mengambil semua ID Region dari cabang-cabang tersebut
$userRegionIds = \App\Models\Cabang::whereIn('id', $userCabangIds)
->pluck('region_id')
->unique()
->toArray();
// --- 2. KOLEKSI DROPDOWN FILTER BERDASARKAN ROLE ---
if (in_array($role, ['Admin Region', 'Marketing Operational Manager Region'])) {
// Dropdown Region HANYA muncul region miliknya (Menghilangkan Blank)
$regions = \App\Models\Region::whereIn('id', $userRegionIds)->get();
// Dropdown Cabang HANYA muncul cabang-cabang di bawah region tersebut
$cabangOptions = \App\Models\Cabang::whereIn('region_id', $userRegionIds)->get();
} elseif (in_array($role, ['Area Manager Cabang', 'Medical Representatif'])) {
$regions = \App\Models\Region::whereIn('id', $userRegionIds)->get();
$cabangOptions = \App\Models\Cabang::whereIn('id', $userCabangIds)->get();
if ($cabang_id) {
$budget = BudgetControl::where('cabang_id', $cabang_id)
->with(['receiver', 'sender', 'cabang'])
->get();
}
} else {
$budget = BudgetControl::with(['receiver', 'sender', 'cabang'])->get();
// Superadmin & Accounting: Muncul Semua
$regions = \App\Models\Region::all();
$cabangOptions = \App\Models\Cabang::when($request->region_id, function($q) use ($request) {
return $q->where('region_id', $request->region_id);
})->get();
}
// Tentukan periode berdasarkan start dan closing date
$startDay = (int) env('STARTING_DATE', 11);
$closingDay = (int) env('CLOSING_DATE', 10);
$now = Carbon::now();
// --- 3. QUERY UTAMA DENGAN PROTEKSI DATA ---
$query = \App\Models\BudgetControl::with(['receiver', 'sender', 'cabang.region']);
if ($now->day >= $startDay) {
$startDate = Carbon::create($now->year, $now->month, $startDay)->startOfDay();
} else {
$startDate = Carbon::create($now->copy()->subMonth()->year, $now->copy()->subMonth()->month, $startDay)->startOfDay();
// Jika bukan Superadmin/Accounting, kunci kueri ke wilayahnya saja
if (!in_array($role, ['Superadmin', 'Super Admin', 'Accounting', 'accounting'])) {
$query->whereHas('cabang', function($q) use ($userRegionIds) {
$q->whereIn('region_id', $userRegionIds);
});
}
// Hitung available budget berdasarkan periode
$budget->map(function ($b) {
$b->availableBudget = BudgetHelper::getAvailableBudget($b->cabang_id, $b->periode_month, $b->periode_year);
// --- 4. FILTER DARI INPUT USER ---
if ($request->filled('region_id')) {
$query->whereHas('cabang', fn($q) => $q->where('region_id', $request->region_id));
}
if ($request->filled('cabang_id')) {
$query->where('cabang_id', $request->cabang_id);
}
if ($request->filled('month')) {
$query->where('periode_month', strtolower($request->month));
}
if ($request->filled('year')) {
$query->where('periode_year', $request->year);
}
$data = $query->latest()->paginate(50)->withQueryString();
// --- 5. OPTIMASI PERHITUNGAN REALISASI ---
$expenseMap = \App\Helpers\BudgetHelper::getBulkAvailableBudget($data);
$data->getCollection()->transform(function ($b) use ($expenseMap) {
$key = $b->cabang_id . '_' . strtolower($b->periode_month) . '_' . $b->periode_year;
$b->availableBudget = isset($expenseMap[$key]) ? ($b->amount - $expenseMap[$key]) : $b->amount;
return $b;
});
return view('backend.pages.budget_control.index', [
'data' => $budget,
]);
}
public function log()
{
return view('backend.pages.budget_control.index', compact('data', 'regions', 'cabangOptions'));
}
/**
* Menampilkan Log Riwayat Budget (Disempurnakan dengan Total, Realisasi, DR, CR)
*/
/**
* Menampilkan Log Riwayat Budget (Menampilkan Budget & Realisasi Cash BCA)
*/
/**
* Menampilkan Log Riwayat Budget dengan Integrasi Tabel Transaksi
*/
public function log(Request $request)
{
$this->checkAuthorization(auth()->user(), ['budget_control.view']);
session()->put('redirect_url', route('budget_control'));
return view(
'backend.pages.budget_control.log',
[
'data' => BudgetControlLog::orderBy('created_at', 'desc')->with(['receiver', 'sender', 'cabang'])->get(),
]
);
// Data untuk Dropdown Filter
$regions = \App\Models\Region::all();
$cabangOptions = \App\Models\Cabang::when($request->region_id, function($q) use ($request) {
return $q->where('region_id', $request->region_id);
})->get();
// Query Utama dengan Filter Dinamis
$query = \App\Models\BudgetControlLog::with(['receiver', 'sender', 'cabang.region']);
if ($request->filled('region_id')) {
$query->whereHas('cabang', fn($q) => $q->where('region_id', $request->region_id));
}
if ($request->filled('cabang_id')) {
$query->where('cabang_id', $request->cabang_id);
}
if ($request->filled('month')) {
$query->where('periode_month', strtolower($request->month));
}
if ($request->filled('year')) {
$query->where('periode_year', $request->year);
}
$data = $query->orderBy('created_at', 'desc')->paginate(50)->withQueryString();
// Sinkronisasi Kalkulasi Realisasi
$lookups = $data->map(fn($l) => [
'cabang_id' => $l->cabang_id,
'month' => $l->periode_month,
'year' => $l->periode_year
])->unique()->toArray();
$debetMap = \App\Helpers\BudgetHelper::getBulkKalkulasiJurnal($lookups);
$data->getCollection()->transform(function ($log) use ($debetMap) {
$key = $log->cabang_id . '_' . strtolower($log->periode_month) . '_' . $log->periode_year;
$log->total_budget = (float) $log->amount;
$log->total_credit = (float) $log->amount; // Pagu/Transfer
$log->total_debet = (float) ($debetMap[$key] ?? 0); // Pengeluaran riil
$log->realisasi = $log->total_credit - $log->total_debet; // Sisa Saldo
return $log;
});
// Menyiapkan Data Grafik (Top 10 hasil filter)
$chartData = [
'labels' => $data->getCollection()->take(10)->map(fn($l) => $l->cabang->name)->toArray(),
'budget' => $data->getCollection()->take(10)->pluck('total_budget')->toArray(),
'realisasi' => $data->getCollection()->take(10)->pluck('realisasi')->toArray(),
];
return view('backend.pages.budget_control.log', compact('data', 'regions', 'cabangOptions', 'chartData'));
}
private function prepareLogChartData($collection)
{
// Mengelompokkan data berdasarkan periode untuk grafik
$grouped = $collection->groupBy(function($item) {
return strtoupper($item->periode_month) . ' ' . $item->periode_year;
})->take(6); // Ambil 6 periode terbaru yang ada di list
return [
'labels' => $grouped->keys()->toArray(),
'budget' => $grouped->map(fn($group) => $group->sum('total_budget'))->values()->toArray(),
'realisasi' => $grouped->map(fn($group) => $group->sum('realisasi'))->values()->toArray(),
];
}
/**
* Download Template untuk Bulk Upload
*/
public function downloadTemplate()
{
$fileName = 'Template_Bulk_Upload_Budget.csv';
$headers = [
"Content-type" => "text/csv",
"Content-Disposition" => "attachment; filename=$fileName",
"Pragma" => "no-cache",
"Cache-Control" => "must-revalidate, post-check=0, pre-check=0",
"Expires" => "0"
];
$columns = ['Kode Cabang', 'Bulan', 'Nominal Budget', 'Keterangan'];
$callback = function() use($columns) {
$file = fopen('php://output', 'w');
// Baris 1: Header
fputcsv($file, $columns);
// Baris 2 & 3: Contoh data agar user paham
fputcsv($file, ['JKT-01', 'January', '15000000', 'Budget Ops JKT-01']);
fputcsv($file, ['BDG-01', 'January', '12500000', 'Budget Ops BDG-01']);
fclose($file);
};
return response()->stream($callback, 200, $headers);
}
public static function generateRefNumber($cabang)
{
// Mengambil 3 huruf kode cabang (Pastikan Anda menggunakan kolom yang tepat, misalnya $cabang->code atau $cabang->name)
$kodeCabang = strtoupper(substr($cabang->code ?? $cabang->name, 0, 3));
// Format ym (Contoh: 2605 untuk Mei 2026)
$ym = date('ym');
$prefix = "JRN-{$kodeCabang}-{$ym}";
// Menghitung urutan transaksi untuk cabang dan bulan ini
$count = \Illuminate\Support\Facades\DB::table('transactions')
->where('cabang_id', $cabang->id)
->whereRaw("DATE_FORMAT(created_at, '%y%m') = ?", [$ym])
->count();
$sequence = str_pad((string)($count + 1), 3, '0', STR_PAD_LEFT);
// Hasil akhir: JRN-PDG-2605-001
return "{$prefix}-{$sequence}";
}
/**
* Form Create Budget
*/
public function create()
{
$this->checkAuthorization(auth()->user(), ['budget_control.create']);
$role = auth()->user()->getRoleNames()[0];
$user = auth()->user();
$role = $user->getRoleNames()[0];
if ($role == 'Admin Region' || $role == 'Marketing Operational Manager Region') {
$region_id = auth()->user()->getMyCabangAndRegionId()['region'];
if ($region_id) {
// only show users from the same region
$users = Admin::where('id', '!=', auth()->user()->id)
->whereHas('cabang', function ($query) use ($region_id) {
$query->where('region_id', $region_id);
})
->get();
} else {
$users = Admin::getUserByRoleName('Area Manager Cabang');
}
if (in_array($role, ['Admin Region', 'Marketing Operational Manager Region'])) {
$region_id = $user->getMyCabangAndRegionId()['region'];
$users = $region_id
? Admin::where('id', '!=', $user->id)->whereHas('cabang', fn($q) => $q->where('region_id', $region_id))->get()
: Admin::getUserByRoleName('Area Manager Cabang');
} elseif ($role == 'Area Manager Cabang') {
$cabang_id = auth()->user()->getMyCabangAndRegionId()['cabang'];
if ($cabang_id) {
// only show users from the same cabang
$users = Admin::where('id', '!=', auth()->user()->id)
->whereHas('cabang', function ($query) use ($cabang_id) {
$query->where('id', $cabang_id);
})
->get();
}
$cabang_id = $user->getMyCabangAndRegionId()['cabang'];
$users = $cabang_id
? Admin::where('id', '!=', $user->id)->whereHas('cabang', fn($q) => $q->where('id', $cabang_id))->get()
: collect();
} else {
$users = Admin::getUserByRoleName('Area Manager Cabang');
}
return view('backend.pages.budget_control.create', [
'users' => $users,
return view('backend.pages.budget_control.create', ['users' => $users]);
}
/**
* Menampilkan Halaman Filter Generate Report Jurnal
*/
public function jurnal()
{
$user = auth()->user();
$role = $user->getRoleNames()[0] ?? null;
$userData = $user->getMyCabangAndRegionId();
$regions = Region::all();
$cabangs = Cabang::all();
// Filter data berdasarkan hak akses (Role)
if (in_array($role, ['Admin Region', 'Marketing Operational Manager Region'])) {
$regions = Region::where('id', $userData['region'])->get();
$cabangs = Cabang::where('region_id', $userData['region'])->get();
} elseif ($role == 'Area Manager Cabang') {
$cabangs = Cabang::where('id', $userData['cabang'])->get();
$regions = Region::where('id', $cabangs->first()->region_id ?? 0)->get();
}
return view('backend.pages.report.jurnal', [
'pageInfo' => ['title' => 'Generate Reports For Jurnal'],
'cabang' => $cabangs,
'regions' => $regions
]);
}
/**
* Proses Menghitung Data Jurnal (Debet, Kredit, & Realisasi)
*/
public function generateJurnal()
{
$params = request()->all();
$accounting_period = $params['accounting_period'] ?? '-';
// 1. Ambil kategori yang valid (Wajib punya Account Number & tidak di-exclude)
$excluded = ['Cash in Bank BCA', 'Donasi/Gift', 'PPn Masukan', 'Vehicle Maintenance', 'Bahan Promosi', 'Sponsorship'];
$kategoris = Kategori::whereNotIn('name', $excluded)
->whereNotNull('account_number')
->where('account_number', '!=', '')
->where('account_number', '!=', '0')
->get();
$kategori_bca = Kategori::where('name', 'Cash in Bank BCA')->first();
$cabang = Cabang::where('code', $params['cabang'])->firstOrFail();
$year = $params['year'];
$month = $params['month'];
// 2. Ambil Range Tanggal Cut-off (Misal: 11 Maret - 13 April)
[$startDate, $endDate] = PeriodeHelper::getPeriodeRange($month, $year);
// 3. KREDIT: Total Petty Cash yang diterima cabang (Pagu)
$pettyCashAmount = BudgetControl::where('cabang_id', $cabang->id)
->where('periode_year', $year)
->where('periode_month', $month)
->sum('amount');
// Log budget untuk display di view
$budget = BudgetControlLog::where('cabang_id', $cabang->id)
->where('periode_year', $year)
->where('periode_month', $month)
->orderByDesc('created_at')->first();
$userIds = UserHasCabang::where('cabang_id', $cabang->id)->pluck('user_id');
// Mapping Model & Alias
$formModels = [
'Up Country' => FormUpCountry::class,
'Vehicle Running Cost' => FormVehicleRunningCost::class,
'Entertainment' => FormEntertaimentPresentation::class,
'Entertaiment' => FormEntertaimentPresentation::class,
'Presentation' => FormEntertaimentPresentation::class,
'Meeting / Seminar' => FormMeetingSeminar::class,
];
$nominals = [];
foreach ($kategoris as $kategori) {
$sum = 0;
$katName = trim($kategori->name);
if (isset($formModels[$katName])) {
$model = $formModels[$katName];
$sum = $model::whereIn('user_id', $userIds)
->where('expense_number', 'LIKE', "%-{$cabang->code}-%") // Filter Cabang
->where('kategori_id', $kategori->id) // Filter Kategori WAJIB
->whereIn('status', ['Closed', 'Final Approved'])
->whereBetween('final_approved_at', [$startDate, $endDate])
->sum('approved_total');
} else {
$sum = FormOthers::whereIn('user_id', $userIds)
->where('expense_number', 'LIKE', "%-{$cabang->code}-%") // Filter Cabang
->where('kategori_id', $kategori->id) // Filter Kategori WAJIB
->whereIn('status', ['Closed', 'Final Approved'])
->whereBetween('final_approved_at', [$startDate, $endDate])
->sum('approved_total');
}
$nominals[$kategori->id] = $sum;
}
$totalDebet = array_sum($nominals); // Total Pengeluaran riil
return view('backend.pages.report.generate_jurnal', [
'pageInfo' => ['title' => 'Generate Reports For Jurnal'],
'kategori' => $kategoris,
'cabang' => $cabang,
'year' => $year,
'month' => $month,
'kategori_bca' => $kategori_bca,
'nominals' => $nominals,
'budget' => $budget,
'totalKategori' => $totalDebet, // Total Debet Jurnal
'pettyCashAmount' => $pettyCashAmount, // Total Kredit Jurnal
'cashInBca' => $pettyCashAmount - $totalDebet, // Sisa Uang BCA
'accounting_period' => $accounting_period,
]);
}
/**
* Simpan Budget Baru & Log
*/
public function store(Request $request)
{
$this->checkAuthorization(auth()->user(), ['budget_control.create']);
$validated = $request->validate([
'receiver_id' => 'required|exists:admins,id',
'periode_month' => 'required|string|in:january,february,march,april,may,june,july,august,september,october,november,december',
'periode_month' => 'required|string',
'periode_year' => 'required|numeric',
'amount' => 'required|numeric',
'remarks' => 'nullable|string',
]);
$cabang_id = Admin::find($validated['receiver_id'])->getMyCabangAndRegionId()['cabang'];
$periode = [
'periode_month' => $validated['periode_month'],
'periode_year' => $validated['periode_year'],
return DB::transaction(function () use ($validated) {
$receiver = Admin::find($validated['receiver_id']);
$cabang_id = $receiver->getMyCabangAndRegionId()['cabang'];
$existing = BudgetControl::where('cabang_id', $cabang_id)
->where('periode_month', $validated['periode_month'])
->where('periode_year', $validated['periode_year'])
->first();
if ($existing && strtolower($existing->status) == 'assigned') {
return back()->with('error', 'Gagal: Budget periode ini sudah Assigned.');
}
$budgetData = [
'sender_id' => auth()->user()->id,
'receiver_id' => $validated['receiver_id'],
'amount' => $validated['amount'],
'remarks' => $validated['remarks'],
'status' => 'Assigned',
];
BudgetControl::updateOrCreate(
array_merge(['cabang_id' => $cabang_id], $periode),
array_merge($periode, [
'sender_id' => auth()->user()->id,
'receiver_id' => $validated['receiver_id'],
'amount' => $validated['amount'],
'remarks' => $validated['remarks'],
'closing_at' => null,
'status' => 'Assigned',
])
['cabang_id' => $cabang_id, 'periode_month' => $validated['periode_month'], 'periode_year' => $validated['periode_year']],
$budgetData
);
BudgetControlLog::create([
'cabang_id' => $cabang_id,
'sender_id' => auth()->user()->id,
'receiver_id' => $validated['receiver_id'],
'periode_month' => $validated['periode_month'],
'periode_year' => $validated['periode_year'],
'amount' => $validated['amount'],
'remarks' => $validated['remarks'],
'closing_at' => null,
'status' => 'Assigned',
]);
BudgetControlLog::create(array_merge(['cabang_id' => $cabang_id], $validated, $budgetData));
session()->flash('success', 'Budget has been added or updated successfully');
return redirect()->route('budget_control');
return redirect()->route('budget_control')->with('success', 'Budget Assigned successfully');
});
}
/**
* Proses Upload Bukti Transfer (Disempurnakan untuk dukung PDF & Gambar)
*/
public function uploadProof(Request $request)
{
// PERBAIKAN: Ganti 'image' menjadi 'file', karena aturan 'image' akan menolak PDF
$request->validate([
'budget_id' => 'required|exists:budget_control,id',
'transfer_proof' => 'required|file|mimes:jpeg,png,jpg,pdf|max:5120', // Maks 5MB
], [
'transfer_proof.mimes' => 'Format file harus berupa JPG, JPEG, PNG, atau PDF.',
'transfer_proof.max' => 'Ukuran file maksimal 5MB.'
]);
$budget = BudgetControl::find($request->budget_id);
if ($request->hasFile('transfer_proof')) {
$file = $request->file('transfer_proof');
$filename = time() . '_' . str_replace(' ', '_', $file->getClientOriginalName());
// Simpan file
$file->move(public_path('uploads/transfer_proofs'), $filename);
$budget->update([
'transfer_proof' => 'uploads/transfer_proofs/' . $filename,
'transfer_status' => 'Waiting Approval'
]);
return back()->with('success', 'Bukti transfer berhasil diunggah.');
}
return back()->with('error', 'File gagal diunggah.');
}
/**
* Konfirmasi Accounting (Validated)
*/
public function approveTransfer($id)
{
BudgetControl::where('id', $id)->update([
'transfer_status' => 'Validated',
'updated_at' => now()
]);
return back()->with('success', 'Transfer divalidasi.');
}
/**
* Penolakan Accounting (Rejected)
*/
public function rejectTransfer($id)
{
BudgetControl::where('id', $id)->update([
'transfer_status' => 'Rejected',
'updated_at' => now()
]);
return back()->with('error', 'Transfer ditolak.');
}
/**
* Proses Bulk Upload Budget via Excel/CSV
*/
public function bulkUpload(Request $request)
{
$this->checkAuthorization(auth()->user(), ['budget_control.create']);
// Tambahkan 'csv' pada mimes
$request->validate([
'upload_type' => 'required|in:region,cabang',
'periode_year' => 'required|numeric',
'file_excel' => 'required|file|mimes:xlsx,xls,csv|max:5120',
], [
'file_excel.mimes' => 'Format file wajib berupa Microsoft Excel (.xls / .xlsx) atau CSV.'
]);
// 1. Tentukan Cabang ID mana saja yang diizinkan untuk di-upload pada proses ini
$cabangIds = [];
if ($request->upload_type === 'region') {
$request->validate(['region_id' => 'required|exists:region,id']);
$cabangIds = \App\Models\Cabang::where('region_id', $request->region_id)->pluck('id')->toArray();
} else {
$request->validate(['cabang_id' => 'required|array', 'cabang_id.*' => 'exists:cabang,id']);
$cabangIds = $request->cabang_id;
}
try {
// 2. Baca file Excel menggunakan Maatwebsite (otomatis jadi Array)
// Parameter pertama new \stdClass() digunakan sebagai dummy import class
$data = Excel::toArray(new \stdClass(), $request->file('file_excel'));
$rows = $data[0] ?? [];
DB::beginTransaction();
$count = 0;
// 3. Looping data Excel
foreach ($rows as $index => $row) {
// Skip baris pertama (Asumsi baris 1 adalah Header)
if ($index === 0) continue;
$cabangCode = $row[0] ?? null;
$month = $row[1] ?? null;
$amount = (float) ($row[2] ?? 0);
$remarks = $row[3] ?? null;
if (!$cabangCode || !$month || $amount <= 0) continue;
// Cari cabang berdasarkan Kode Cabang di Excel
$cabang = \App\Models\Cabang::where('code', $cabangCode)->first();
// Validasi Keamanan: Pastikan cabang ditemukan DAN termasuk dalam scope filter modal
if (!$cabang || !in_array($cabang->id, $cabangIds)) continue;
$budgetData = [
'sender_id' => auth()->user()->id,
'receiver_id' => $cabang->head_id ?? auth()->user()->id, // Assign ke Head Cabang
'amount' => $amount,
'remarks' => $remarks,
'status' => 'Assigned',
];
// Simpan atau Update Budget
BudgetControl::updateOrCreate(
[
'cabang_id' => $cabang->id,
'periode_month' => $month,
'periode_year' => $request->periode_year
],
$budgetData
);
// Catat di Log
BudgetControlLog::create(array_merge([
'cabang_id' => $cabang->id,
'periode_month' => $month,
'periode_year' => $request->periode_year
], $budgetData));
$count++;
}
DB::commit();
return back()->with('success', "Bulk upload berhasil! Sebanyak {$count} data budget telah diproses.");
} catch (\Exception $e) {
DB::rollBack();
\Illuminate\Support\Facades\Log::error("Bulk Upload Error: " . $e->getMessage());
return back()->with('error', 'Gagal memproses file Excel. Pastikan format tabel sudah sesuai.');
}
}
/**
* Hapus Data Budget
*/
public function destroy($id)
{
$this->checkAuthorization(auth()->user(), ['budget_control.delete']);
BudgetControl::findOrFail($id)->delete();
return redirect()->route('budget_control')->with('success', 'Deleted successfully');
}
$budgetRequest = BudgetControl::findOrfail($id);
$budgetRequest->delete();
/**
* PRIVATE METHOD: Logika Grafik Trend Petty Cash vs Realisasi
*/
private function getTrendChartData(array $cabangIds): array
{
$currentYear = \Carbon\Carbon::now()->year;
$months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
return redirect()->route('budget_control');
$pettyCashData = [];
$realisasiData = [];
foreach ($months as $month) {
$pettyCash = \App\Models\BudgetControl::whereIn('cabang_id', $cabangIds)
->where('periode_month', strtolower($month))
->where('periode_year', $currentYear)
->sum('amount');
// Realisasi Sementara Di-set 0
$realisasi = 0;
$pettyCashData[] = (float) $pettyCash;
$realisasiData[] = (float) $realisasi;
}
return [
'labels' => $months,
'datasets' => [
[
'label' => 'Petty Cash (Budget)',
'data' => $pettyCashData,
'backgroundColor' => 'rgba(54, 162, 235, 0.6)',
'borderColor' => 'rgba(54, 162, 235, 1)',
],
[
'label' => 'Realisasi',
'data' => $realisasiData,
'backgroundColor' => 'rgba(255, 99, 132, 0.6)',
'borderColor' => 'rgba(255, 99, 132, 1)',
]
]
];
}
}
@@ -5,120 +5,243 @@ declare(strict_types=1);
namespace App\Http\Controllers\Backend;
use App\Http\Controllers\Controller;
use App\Models\Admin;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
use App\Models\FormUpCountry;
use App\Models\FormEntertaimentPresentation;
use App\Models\FormVehicleRunningCost;
use App\Models\FormMeetingSeminar;
use App\Models\FormOthers;
use App\Models\UserHasCabang;
use App\Models\Notifications;
use App\Models\Region;
use App\Models\Cabang;
use App\Models\Kategori;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
class DashboardController extends Controller
{
public function index()
{
$role = auth()->user()->getRoleNames()[0];
$startDay = (int) env('STARTING_DATE', 11);
$closingDay = (int) env('CLOSING_DATE', 10);
private array $tables = [
'up_country' => 'form_up_country',
'vehicle' => 'form_vehicle_running_cost',
'entertainment' => 'form_entertainment_presentation',
'meeting' => 'form_meeting_seminar',
'others' => 'form_others'
];
// Referensi waktu sekarang
public function index(Request $request)
{
$user = auth()->user();
$roleName = trim($user->getRoleNames()->first() ?? '');
$now = Carbon::now();
// Tentukan periode
if ($now->day >= $startDay) {
// Mulai dari tanggal START bulan ini
$startDate = Carbon::create($now->year, $now->month, $startDay)->startOfDay();
// Sampai tanggal CLOSING bulan depan
$closingDate = Carbon::create($now->copy()->addMonth()->year, $now->copy()->addMonth()->month, $closingDay)->endOfDay();
// 1. FILTER WAKTU (Default ke bulan berjalan)
$chartMonth = $request->chart_month ?? $now->format('m');
$chartYear = $request->chart_year ?? $now->format('Y');
$monthName = Carbon::createFromFormat('m', $chartMonth)->translatedFormat('F');
// --- KALKULASI PERIODE PENILAIAN (11 di bulan terpilih s/d 13 di bulan berikutnya) ---
$displayStart = Carbon::create((int)$chartYear, (int)$chartMonth, 11)->startOfDay();
$displayEnd = $displayStart->copy()->addMonth()->day(13)->endOfDay(); // Dikunci aman ke tanggal 13 penutupan closing laporan
$periodeLabelFormatted = $displayStart->translatedFormat('d F') . ' - ' . $displayEnd->translatedFormat('d F Y');
// 2. FILTER OTORISASI & DATA
$allowedCabangIds = $this->getScopedCabangIds($user, $roleName);
if (empty($allowedCabangIds)) {
return view('backend.pages.dashboard.index', $this->emptyData());
}
$availableRegions = $this->getAvailableRegions($user, $roleName, $allowedCabangIds);
$cabangOptions = Cabang::whereIn('id', $allowedCabangIds)
->when($request->region_id, fn($q) => $q->where('region_id', $request->region_id))
->get();
// 3. AMBIL DATA PENDING & PERFORMA (Passing Variable Waktu Siklus ke Chart Method)
$cabangDetails = $this->getPendingExpenseTable($allowedCabangIds, $request);
$performance = $this->getPerformanceCharts($allowedCabangIds, $displayStart, $displayEnd);
$totalKategori = Kategori::whereNotNull('account_number')->where('account_number', '!=', '')->count();
$data = [
'periodeLabel' => $periodeLabelFormatted,
'totalRegions' => Region::count(),
'totalCabangs' => count($allowedCabangIds),
'totalKategori' => $totalKategori,
'regions' => $availableRegions,
'cabangOptions' => $cabangOptions,
'cabangDetails' => $cabangDetails,
'pendingCount' => $this->getTotalPendingGlobal($allowedCabangIds),
];
return view('backend.pages.dashboard.index', array_merge($data, $performance));
}
// ========================================================================
// PRIVATE METHODS - SEPARATION OF CONCERNS
// ========================================================================
/**
* MENGAMBIL DATA UNTUK TABEL MONITORING PENDING
*/
private function getPendingExpenseTable($allowedCabangIds, Request $request)
{
$query = Cabang::whereIn('id', $allowedCabangIds)->with('region');
if ($request->region_id) $query->where('region_id', $request->region_id);
if ($request->cabang_id) $query->where('id', $request->cabang_id);
if ($request->filled('search')) $query->where('name', 'like', "%{$request->search}%");
$sortField = $request->get('sort', 'name');
$sortOrder = $request->get('direction', 'asc');
if ($sortField == 'region') {
$query->leftJoin('region', 'cabang.region_id', '=', 'region.id')
->select('cabang.*')->orderBy('region.name', $sortOrder);
} else {
// Mulai dari tanggal START bulan lalu
$startDate = Carbon::create($now->copy()->subMonth()->year, $now->copy()->subMonth()->month, $startDay)->startOfDay();
// Sampai tanggal CLOSING bulan ini
$closingDate = Carbon::create($now->year, $now->month, $closingDay)->endOfDay();
$query->orderBy('cabang.' . ($sortField == 'total_pending' ? 'name' : $sortField), $sortOrder);
}
$formQueries = [
'vehicle' => FormVehicleRunningCost::query()
->whereMonth('tanggal', date('m'))
->whereBetween('tanggal', [$startDate, $closingDate]),
'upcountry' => FormUpCountry::query()
->whereMonth('tanggal', date('m'))
->whereBetween('tanggal', [$startDate, $closingDate]),
'entertainment' => FormEntertaimentPresentation::query()
->whereMonth('tanggal', date('m'))
->whereBetween('tanggal', [$startDate, $closingDate]),
'meeting' => FormMeetingSeminar::query()
->whereMonth('created_at', date('m'))
->whereBetween('created_at', [$startDate, $closingDate]),
'others' => FormOthers::query()
->whereMonth('tanggal', date('m'))
->whereBetween('tanggal', [$startDate, $closingDate]),
];
$cabangDetails = $query->paginate(100)->withQueryString()->through(function($cabang) {
$statuses = ['Pending', 'On Progress', 'Approved 1', 'Approved 2', 'Waiting Approval'];
$details = []; $total = 0;
$approvedStatuses = [
// 'Approved',
// 'Approved 1',
// 'Approved 2',
// 'Final Approved',
// 'Final Approve',
'Closed',
];
$forms = [];
foreach ($formQueries as $key => $query) {
$forms[$key] = $query
->whereIn('status', $approvedStatuses)
->whereNotNull('final_approved_at')
->sum('approved_total');
foreach ($this->tables as $key => $table) {
$count = DB::table($table)->where('cabang_id', $cabang->id)->whereIn('status', $statuses)->count();
$details[$key] = $count;
$total += $count;
}
return view(
'backend.pages.dashboard.index',
[
'forms' => $forms,
$cabang->pending_details = $details;
$cabang->total_pending = $total;
return $cabang;
});
if ($sortField == 'total_pending') {
$sortedCollection = $cabangDetails->getCollection()->sortBy('total_pending', SORT_REGULAR, $sortOrder == 'desc')->values();
$cabangDetails->setCollection($sortedCollection);
}
return $cabangDetails;
}
/**
* MENGHITUNG PERFORMA KECEPATAN APPROVE CABANG (Fokus Akurat Jendela Siklus Transaksi Tanggal 11 - 13)
*/
private function getPerformanceCharts($cabangIds, Carbon $startDate, Carbon $endDate): array
{
$queryUnion = null;
foreach ($this->tables as $table) {
$q = DB::table($table)
->select('cabang_id', 'created_at', 'final_approved_at', 'updated_at')
->where('status', 'Closed')
->whereIn('cabang_id', $cabangIds)
// KUNCI UTAMA: Hanya membaca pengajuan yang dibuat murni di dalam rentang tanggal siklus 11 s/d 13 berjalan
->whereBetween('tanggal', [$startDate->format('Y-m-d H:i:s'), $endDate->format('Y-m-d H:i:s')]);
$queryUnion = is_null($queryUnion) ? $q : $queryUnion->unionAll($q);
}
if ($queryUnion && DB::table(DB::raw("({$queryUnion->toSql()}) as t"))->mergeBindings($queryUnion)->exists()) {
// Kalkulasi selisih waktu rata-rata penyelesaian berkas menggunakan fondasi TIMESTAMPDIFF detik secara objektif
$res = DB::table(DB::raw("({$queryUnion->toSql()}) as combined"))
->mergeBindings($queryUnion)
->select('cabang_id', DB::raw('AVG(TIMESTAMPDIFF(SECOND, created_at, COALESCE(final_approved_at, updated_at))) as avg_sec'))
->groupBy('cabang_id')
->get();
$cabangNames = Cabang::whereIn('id', $res->pluck('cabang_id'))->pluck('name', 'id');
$fastest = $res->sortBy('avg_sec')->take(5);
$slowest = $res->sortByDesc('avg_sec')->take(5);
return [
'chartFastest' => [
'labels' => $fastest->map(fn($i) => $cabangNames[$i->cabang_id] ?? 'N/A')->values(),
'data' => $fastest->map(fn($i) => round($i->avg_sec / 3600, 2))->values(),
'raw' => $fastest->map(fn($i) => $this->formatDuration((int)$i->avg_sec))->values()
],
'chartSlowest' => [
'labels' => $slowest->map(fn($i) => $cabangNames[$i->cabang_id] ?? 'N/A')->values(),
'data' => $slowest->map(fn($i) => round($i->avg_sec / 3600, 2))->values(),
'raw' => $slowest->map(fn($i) => $this->formatDuration((int)$i->avg_sec))->values()
]
);
];
}
public function notifications()
{
$this->checkAuthorization(auth()->user(), ['notification.view']);
// Mulai dengan query dasar
$notifications = Notifications::orderBy('created_at', 'desc')
->where('receiver_id', auth()->user()->id)
->where('is_read', 0);
// Ambil data dan kembalikan sebagai JSON
return response()->json([
'count' => $notifications->count(),
'data' => $notifications->take(7)->get(),
]);
return [
'chartFastest' => ['labels'=>[], 'data'=>[], 'raw'=>[]],
'chartSlowest' => ['labels'=>[], 'data'=>[], 'raw'=>[]]
];
}
public function mark_read($id)
/**
* GET AVAILABLE REGIONS BERDASARKAN ROLE
*/
private function getAvailableRegions($user, $roleName, $allowedCabangIds)
{
$this->checkAuthorization(auth()->user(), ['notification.delete']);
$receiver_id = auth()->user()->id;
Notifications::where('receiver_id', $receiver_id)->where('id', $id)->update(['is_read' => 1]);
return response()->json(['message' => 'Notification marked as read.']);
$regionsQuery = Region::query();
if (in_array($roleName, ['Admin Region', 'Marketing Operational Manager Region'])) {
$regionsQuery->where('id', $user->region_id);
} elseif (in_array($roleName, ['Area Manager Cabang', 'Medical Representatif'])) {
$myRegionIds = Cabang::whereIn('id', $allowedCabangIds)->pluck('region_id')->unique();
$regionsQuery->whereIn('id', $myRegionIds);
}
return $regionsQuery->get();
}
public function mark_all_read()
private function getScopedCabangIds($user, $roleName): array
{
$this->checkAuthorization(auth()->user(), ['notification.delete']);
$receiver_id = auth()->user()->id;
$central = ['Superadmin', 'Head of Sales Marketing', 'Marketing Information System', 'Accounting'];
if (in_array($roleName, $central)) return Cabang::pluck('id')->toArray();
Notifications::where('receiver_id', $receiver_id)->update(['is_read' => 1]);
if (in_array($roleName, ['Admin Region', 'Marketing Operational Manager Region'])) {
return $user->region_id ? Cabang::where('region_id', $user->region_id)->pluck('id')->toArray() : [];
}
session()->flash('success', 'All notifications marked as read.');
return redirect()->back();
if (in_array($roleName, ['Area Manager Cabang', 'Medical Representatif'])) {
$cabangId = $user->cabang->cabang_id ?? \App\Models\Rayon::where('id', $user->rayon_id)->value('cabang_id');
return $cabangId ? [(int)$cabangId] : [];
}
return [];
}
private function getTotalPendingGlobal($cabangIds): int
{
$total = 0;
foreach ($this->tables as $table) {
$total += DB::table($table)->whereIn('cabang_id', $cabangIds)->whereIn('status', ['Pending', 'On Progress', 'Approved 1', 'Approved 2', 'Waiting Approval'])->count();
}
return $total;
}
private function formatDuration(int $sec): string
{
if ($sec <= 0) return "0 Jam";
$d = floor($sec / 86400); $h = floor(($sec % 86400) / 3600);
return $d > 0 ? "{$d} Hari {$h} Jam" : "{$h} Jam";
}
private function emptyData(): array
{
return [
'periodeLabel' => '-', 'totalRegions' => 0, 'totalCabangs' => 0, 'totalKategori' => 0, 'pendingCount' => 0,
'cabangDetails' => collect(), 'chartFastest' => ['labels'=>[], 'data'=>[], 'raw'=>[]],
'chartSlowest' => ['labels'=>[], 'data'=>[], 'raw'=>[]], 'regions' => collect(), 'cabangOptions' => collect()
];
}
public function notifications() { return response()->json(['status' => 'success', 'data' => []]); }
public function show($id)
{
try {
$user = auth()->user();
$allowedIds = $this->getScopedCabangIds($user, $user->getRoleNames()->first());
if (!in_array((int)$id, $allowedIds)) abort(403);
$cabang = Cabang::with('region')->findOrFail($id);
$statuses = ['Pending', 'On Progress', 'Approved 1', 'Approved 2', 'Waiting Approval'];
$data = [];
foreach (['Up Country' => 'form_up_country', 'Vehicle' => 'form_vehicle_running_cost', 'Entertainment' => 'form_entertainment_presentation', 'Meeting' => 'form_meeting_seminar', 'Others' => 'form_others'] as $label => $table) {
$data[$label] = DB::table($table)->where('cabang_id', $id)->whereIn('status', $statuses)->get();
}
return view('backend.pages.dashboard.show', compact('cabang', 'data'));
} catch (\Exception $e) {
return redirect()->route('admin.dashboard')->with('error', 'Cabang tidak ditemukan.');
}
}
}
File diff suppressed because it is too large Load Diff
@@ -76,7 +76,7 @@ class FormEntertainmentPresentationController extends Controller
$role = auth()->user()->getRoleNames()[0];
$startDay = (int) env('STARTING_DATE', 11);
$closingDay = (int) env('CLOSING_DATE', 10);
$closingDay = (int) env('CLOSING_DATE', 13);
$userRegionData = auth()->user()->getMyCabangAndRegionId();
$regionContextId = $userRegionData['region'] !== '-' ? $userRegionData['region'] : null;
$cabangContextId = $userRegionData['cabang'] !== '-' ? $userRegionData['cabang'] : null;
@@ -201,6 +201,9 @@ class FormEntertainmentPresentationController extends Controller
'name' => 'required|string',
'alamat' => 'required|string',
'nik_or_npwp' => 'required|string',
'jabatan' => 'required|string',
'nama_perusahaan' => 'required|string',
'jenis_usaha' => 'required|string',
'attachments' => 'nullable|array',
'attachments.*.file_category' => 'nullable|string|in:' . implode(',', array_keys($this->attachmentCategories)),
'attachments.*.file_path' => 'nullable|file|max:10240',
@@ -209,24 +212,26 @@ class FormEntertainmentPresentationController extends Controller
$validator->validate();
$tanggal = Carbon::parse($request->tanggal);
// PEMBATASAN INPUT MENGGUNAKAN STARTING_DATE & INPUT_MAX_DATE
$startDay = (int) env('STARTING_DATE', 11);
$closingDay = (int) env('CLOSING_DATE', 10);
$inputMaxDay = (int) env('INPUT_MAX_DATE', 10);
// Ambil tanggal sekarang untuk tentukan periode berjalan
$now = Carbon::now();
if ($now->day >= $startDay) {
// Periode berjalan dari bulan ini sampai bulan depan
$startDate = Carbon::create($now->year, $now->month, $startDay);
$closingDate = Carbon::create($now->copy()->addMonth()->year, $now->copy()->addMonth()->month, $closingDay);
// Jendela Aktif: Tgl 11 Bulan Ini s/d Tgl 10 Bulan Depan
$startDate = Carbon::create($now->year, $now->month, $startDay)->startOfDay();
$maxInputDate = Carbon::create($now->copy()->addMonth()->year, $now->copy()->addMonth()->month, $inputMaxDay)->endOfDay();
} else {
// Periode berjalan dari bulan lalu sampai bulan ini
$startDate = Carbon::create($now->copy()->subMonth()->year, $now->copy()->subMonth()->month, $startDay);
$closingDate = Carbon::create($now->year, $now->month, $closingDay);
// Jendela Aktif: Tgl 11 Bulan Lalu s/d Tgl 10 Bulan Ini
$startDate = Carbon::create($now->copy()->subMonth()->year, $now->copy()->subMonth()->month, $startDay)->startOfDay();
$maxInputDate = Carbon::create($now->year, $now->month, $inputMaxDay)->endOfDay();
}
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')}");
if ($tanggal->lt($startDate) || $tanggal->gt($maxInputDate)) {
session()->flash('error', "Gagal, tanggal expense tidak berada dalam batas waktu input yang diizinkan: {$startDate->format('d M Y')} - {$maxInputDate->format('d M Y')}");
return redirect()->back()->withInput();
}
@@ -244,13 +249,13 @@ class FormEntertainmentPresentationController extends Controller
$region = Region::where('id', $cabang->region_id)->first();
$kategori = "";
$kategori = null;
if (strtolower($request->jenis) == 'entertainment' || $request->jenis == 'entertainment') {
$kategori = Kategori::where('name', 'Entertainment')->first();
$kategori = Kategori::where('name', 'LIKE', '%Entertainment%')->first();
} elseif (strtolower($request->jenis) == 'presentation' || $request->jenis == 'presentation') {
$kategori = Kategori::where('name', 'Presentation')->first();
$kategori = Kategori::where('name', 'LIKE', '%Presentation%')->first();
} elseif (strtolower($request->jenis) == 'sponsorship' || $request->jenis == 'sponsorship') {
$kategori = Kategori::where('name', 'Sponsorship')->first();
$kategori = Kategori::where('name', 'LIKE', '%Sponsorship%')->first();
}
$folderPath = '/expense/' . $region->code . '/' . $cabang->code . '/entertainment';
@@ -275,6 +280,7 @@ class FormEntertainmentPresentationController extends Controller
$form = FormEntertaimentPresentation::create([
'expense_number' => $expense_number,
'user_id' => auth()->user()->id,
'cabang_id' => $cabang->id, // <--- REPARASI SUNTIK CABANG_ID BARU
'tanggal' => $request->tanggal,
'jenis' => $request->jenis,
'keterangan' => $request->keterangan,
@@ -282,8 +288,12 @@ class FormEntertainmentPresentationController extends Controller
'name' => $request->name,
'alamat' => $request->alamat,
'nik_or_npwp' => $request->nik_or_npwp,
'jabatan' => $request->jabatan,
'nama_perusahaan' => $request->nama_perusahaan,
'jenis_usaha' => $request->jenis_usaha,
'bukti_total' => $primaryAttachmentPath,
'account_number' => $kategori->account_number,
'account_number' => $kategori->account_number ?? null,
'kategori_id' => $kategori->id ?? null,
'status' => 'On Progress',
]);
@@ -414,28 +424,38 @@ class FormEntertainmentPresentationController extends Controller
$this->checkAuthorization(auth()->user(), ['forms.entertainment.edit']);
$role = auth()->user()->getRoleNames()[0];
$form = FormEntertaimentPresentation::with('attachments')->findOrFail($id);
$cabang_id = UserHasCabang::where('user_id', $form->user_id)->first()?->cabang_id;
$cabang = UserHasCabang::with('cabang')->where('user_id', $form->user_id)->first()?->cabang;
if (!$cabang_id) {
if (!$cabang) {
return redirect()->back()->with('error', 'Cabang tidak ditemukan.');
}
// Hitung periode berdasarkan tanggal form
$tanggal = Carbon::parse($request->tanggal);
$startDay = (int) env('STARTING_DATE', 11);
$closingDay = (int) env('CLOSING_DATE', 10);
// Periode aktif berdasarkan tanggal input
if ($tanggal->day >= $startDay) {
$periodeDate = Carbon::create($tanggal->year, $tanggal->month, $startDay);
// PEMBATASAN INPUT MENGGUNAKAN STARTING_DATE & INPUT_MAX_DATE (Konsisten dengan fungsi Store)
$startDay = (int) env('STARTING_DATE', 11);
$inputMaxDay = (int) env('INPUT_MAX_DATE', 10);
$now = Carbon::now();
if ($now->day >= $startDay) {
$startDate = Carbon::create($now->year, $now->month, $startDay)->startOfDay();
$maxInputDate = Carbon::create($now->copy()->addMonth()->year, $now->copy()->addMonth()->month, $inputMaxDay)->endOfDay();
} else {
$periodeDate = Carbon::create($tanggal->copy()->subMonth()->year, $tanggal->copy()->subMonth()->month, $startDay);
$startDate = Carbon::create($now->copy()->subMonth()->year, $now->copy()->subMonth()->month, $startDay)->startOfDay();
$maxInputDate = Carbon::create($now->year, $now->month, $inputMaxDay)->endOfDay();
}
$periodeMonth = strtolower($periodeDate->format('F'));
$periodeYear = (int) $periodeDate->format('Y');
if ($tanggal->lt($startDate) || $tanggal->gt($maxInputDate)) {
session()->flash('error', "Gagal, tanggal expense tidak berada dalam batas waktu input yang diizinkan: {$startDate->format('d M Y')} - {$maxInputDate->format('d M Y')}");
return redirect()->back()->withInput();
}
$availableBudget = BudgetHelper::getAvailableBudget($cabang_id, $periodeMonth, $periodeYear);
// Hitung periode berdasarkan tanggal form untuk validasi budget
$periodeMonth = strtolower($startDate->format('F'));
$periodeYear = (int) $startDate->format('Y');
$availableBudget = BudgetHelper::getAvailableBudget($cabang->id, $periodeMonth, $periodeYear);
$validator = Validator::make($request->all(), [
'tanggal' => 'required|date',
@@ -445,6 +465,9 @@ class FormEntertainmentPresentationController extends Controller
'name' => 'required|string',
'alamat' => 'required|string',
'nik_or_npwp' => 'required|string',
'jabatan' => 'required|string',
'nama_perusahaan' => 'required|string',
'jenis_usaha' => 'required|string',
'attachments' => 'nullable|array',
'attachments.*.file_category' => 'nullable|string|in:' . implode(',', array_keys($this->attachmentCategories)),
'attachments.*.file_path' => 'nullable|file|max:10240',
@@ -457,16 +480,15 @@ class FormEntertainmentPresentationController extends Controller
return redirect()->back();
}
$cabang = UserHasCabang::with('cabang')->where('user_id', $form->user_id)->first()->cabang;
$region = Region::where('id', $cabang->region_id)->first();
$kategori = "";
$kategori = null;
if (strtolower($request->jenis) == 'entertainment' || $request->jenis == 'entertainment') {
$kategori = Kategori::where('name', 'Entertainment')->first();
$kategori = Kategori::where('name', 'LIKE', '%Entertainment%')->first();
} elseif (strtolower($request->jenis) == 'presentation' || $request->jenis == 'presentation') {
$kategori = Kategori::where('name', 'Presentation')->first();
$kategori = Kategori::where('name', 'LIKE', '%Presentation%')->first();
} elseif (strtolower($request->jenis) == 'sponsorship' || $request->jenis == 'sponsorship') {
$kategori = Kategori::where('name', 'Sponsorship')->first();
$kategori = Kategori::where('name', 'LIKE', '%Sponsorship%')->first();
}
$folderPath = '/expense/' . $region->code . '/' . $cabang->code . '/entertainment';
@@ -479,6 +501,7 @@ class FormEntertainmentPresentationController extends Controller
: $form->bukti_total;
$form->update([
'cabang_id' => $cabang->id, // <--- REPARASI SUNTIK CABANG_ID TERBARU
'tanggal' => $request->tanggal,
'jenis' => $request->jenis,
'keterangan' => $request->keterangan,
@@ -486,8 +509,12 @@ class FormEntertainmentPresentationController extends Controller
'name' => $request->name,
'alamat' => $request->alamat,
'nik_or_npwp' => $request->nik_or_npwp,
'jabatan' => $request->jabatan,
'nama_perusahaan' => $request->nama_perusahaan,
'jenis_usaha' => $request->jenis_usaha,
'bukti_total' => $primaryAttachmentPath,
'account_number' => $kategori->account_number,
'account_number' => $kategori->account_number ?? $form->account_number,
'kategori_id' => $kategori->id ?? null,
]);
if (!empty($attachmentsPayload)) {
@@ -731,7 +758,8 @@ class FormEntertainmentPresentationController extends Controller
return redirect()->back();
}
public function finalApprove($id)
// <--- REPARASI SUNTIKAN REQUEST DARI JAVASCRIPT MODAL AGAR DATA BISA DIAMBIL
public function finalApprove(Request $request, $id)
{
$this->checkAuthorization(auth()->user(), ['final_approval.approve']);
$form = FormEntertaimentPresentation::findOrFail($id);
@@ -748,10 +776,9 @@ class FormEntertainmentPresentationController extends Controller
return redirect()->back();
}
// Hitung periode dari tanggal form
// Hitung periode berdasarkan tanggal form
$tanggal = Carbon::parse($form->tanggal);
$startDay = (int) env('STARTING_DATE', 11);
$closingDay = (int) env('CLOSING_DATE', 10);
if ($tanggal->day >= $startDay) {
$periodeDate = Carbon::create($tanggal->year, $tanggal->month, $startDay);
@@ -762,13 +789,17 @@ class FormEntertainmentPresentationController extends Controller
$periodeMonth = strtolower($periodeDate->format('F'));
$periodeYear = (int) $periodeDate->format('Y');
// <--- REPARASI: Penangkapan nilai payload dari checkbox JavaScript
$approvedTotal = $request->input('approved_total', $form->total);
$availableBudget = BudgetHelper::getAvailableBudget($cabang_id, $periodeMonth, $periodeYear);
if($form->approved_total > $availableBudget) {
if($approvedTotal > $availableBudget) { // <--- DISINKRONKAN DENGAN NILAI BARU HASIL CHECKLIST
session()->flash('error', 'Budget cabang untuk periode ini tidak mencukupi, silahkan ajukan pada periode expense berikutnya');
return redirect()->back();
}
$form->update([
'approved_total' => $approvedTotal, // <--- SUNTIK DATA UPDATE HASIL CHECKLIST KE DATABASE
'final_approved_at' => now(),
'final_approved_by' => auth()->user()->id,
'status' => 'Closed',
@@ -777,7 +808,7 @@ class FormEntertainmentPresentationController extends Controller
$name = $form->user->name;
$expense_number = $form->expense_number;
$tanggal = $form->created_at;
$total = $form->approved_total;
$total = $form->approved_total; // <--- TOTAL BARU YANG DIKIRIMKAN KE EMAIL / WHATSAPP
$roles = ['Head of Sales Marketing', 'Marketing Operational Manager Region'];
$recipients = [$form->user->email, auth()->user()->email];
@@ -850,25 +881,31 @@ class FormEntertainmentPresentationController extends Controller
public function reject(Request $request, $id)
{
$this->checkAuthorization(auth()->user(), ['approval.reject']);
// Otorisasi khusus MOM Region dan Atasan
if (!auth()->user()->hasAnyPermission(['approval.reject', 'approval2.reject', 'final_approval.approve'])) {
abort(403, 'Akses Ditolak: Anda tidak memiliki izin untuk melakukan Reject.');
}
$request->validate([
'remarks' => 'required|string|max:500',
]);
// Menggunakan Model Entertainment & Presentation
$form = FormEntertaimentPresentation::findOrfail($id);
$form->update([
'status' => 'Rejected',
'remarks' => $request->remarks,
'rejected_by' => auth()->id(), // Kolom baru
'rejected_at' => now(), // Kolom baru
]);
$heads = $form->user->getCabangAndRegionHead();
$name = $form->user->name;
$expense_number = $form->expense_number;
$tanggal = $form->created_at;
$tanggal = $form->tanggal;
$total = $form->total;
// Collect all recipients
$recipients = [$form->user->email, auth()->user()->email];
$phoneNumbers = [$form->user->phone, auth()->user()->phone];
@@ -882,41 +919,37 @@ class FormEntertainmentPresentationController extends Controller
$phoneNumbers[] = $heads['region_head']['phone'];
}
// Remove duplicate email addresses, if any
$recipients = array_unique($recipients);
$phoneNumbers = array_unique($phoneNumbers);
// send whatsapp message
// Route khusus Entertainment
$url = route('forms.entertainment.view', $form->id);
WhatsappHelper::rejectExpense($phoneNumbers, $expense_number, $url, $tanggal, $total, $name, $request->remarks);
// Send bulk email
// Notifikasi WhatsApp
$this->rejectExpenseWithNotification($phoneNumbers, $expense_number, $url, $tanggal, $total, $name, $request->remarks, $recipients);
// Audit Trail Khusus Form Entertainment
AuditTrailHelper::AddAuditTrail('Reject', 'Reject Expense at Form Entertainment Presentation (' . $form->expense_number . ') oleh ' . auth()->user()->name);
session()->flash('success', 'Form has been rejected.');
return redirect()->back();
}
private function rejectExpenseWithNotification($phoneNumbers, $expense_number, $url, $tanggal, $total, $name, $remarks, $recipients)
{
WhatsappHelper::rejectExpense($phoneNumbers, $expense_number, $url, $tanggal, $total, $name, $remarks);
// Notifikasi Email (Brevo)
$brevoService = app(BrevoService::class);
foreach ($recipients as $recipient) {
try {
$mail = new ExpenseRejected(
$name,
$expense_number,
$tanggal,
$total,
$url,
$request->remarks
);
$response = $brevoService->expenseRejected($recipient, $name, $mail);
if (isset($response['success']) && $response['success'] === false) {
Log::warning("Email to {$recipient} failed: " . ($response['message'] ?? 'Unknown error'));
}
$mail = new ExpenseRejected($name, $expense_number, $tanggal, $total, $url, $remarks);
$brevoService->expenseRejected($recipient, $name, $mail);
} catch (\Exception $e) {
Log::error("Failed to send email to {$recipient}: " . $e->getMessage());
continue;
}
}
AuditTrailHelper::AddAuditTrail('Reject', 'Reject Expense at Form Entertainment (' . $form->expense_number . ')');
session()->flash('success', 'Form has been rejected.');
return redirect()->back();
}
public function open($id)
@@ -75,7 +75,7 @@ class FormMeetingSeminarController extends Controller
$role = auth()->user()->getRoleNames()[0];
$startDay = (int) env('STARTING_DATE', 11);
$closingDay = (int) env('CLOSING_DATE', 10);
$closingDay = (int) env('CLOSING_DATE', 13);
$userRegionData = auth()->user()->getMyCabangAndRegionId();
$regionContextId = $userRegionData['region'] !== '-' ? $userRegionData['region'] : null;
$cabangContextId = $userRegionData['cabang'] !== '-' ? $userRegionData['cabang'] : null;
@@ -204,23 +204,28 @@ class FormMeetingSeminarController extends Controller
$validator->validate();
$tanggal = Carbon::parse($request->tanggal);
// PEMBATASAN INPUT MENGGUNAKAN STARTING_DATE & INPUT_MAX_DATE
$startDay = (int) env('STARTING_DATE', 11);
$closingDay = (int) env('CLOSING_DATE', 10);
$inputMaxDay = (int) env('INPUT_MAX_DATE', 10);
$now = Carbon::now();
// Tentukan periode aktif berdasarkan tanggal saat ini
if ($now->day >= $startDay) {
$startDate = Carbon::create($now->year, $now->month, $startDay);
$closingDate = Carbon::create($now->copy()->addMonth()->year, $now->copy()->addMonth()->month, $closingDay);
// Jendela Aktif: Tgl 11 Bulan Ini s/d Tgl 10 Bulan Depan
$startDate = Carbon::create($now->year, $now->month, $startDay)->startOfDay();
$maxInputDate = Carbon::create($now->copy()->addMonth()->year, $now->copy()->addMonth()->month, $inputMaxDay)->endOfDay();
} else {
$startDate = Carbon::create($now->copy()->subMonth()->year, $now->copy()->subMonth()->month, $startDay);
$closingDate = Carbon::create($now->year, $now->month, $closingDay);
// Jendela Aktif: Tgl 11 Bulan Lalu s/d Tgl 10 Bulan Ini
$startDate = Carbon::create($now->copy()->subMonth()->year, $now->copy()->subMonth()->month, $startDay)->startOfDay();
$maxInputDate = Carbon::create($now->year, $now->month, $inputMaxDay)->endOfDay();
}
// Validasi apakah tanggal pengajuan masuk ke periode saat ini
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();
// Validasi apakah tanggal pengajuan masuk ke periode input saat ini
if ($tanggal->lt($startDate) || $tanggal->gt($maxInputDate)) {
session()->flash('error', "Gagal, tanggal expense tidak berada dalam batas waktu input yang diizinkan: {$startDate->format('d M Y')} - {$maxInputDate->format('d M Y')}");
return redirect()->back()->withInput();
}
$cabang = UserHasCabang::with('cabang')->where('user_id', auth()->user()->id)->first()?->cabang;
@@ -230,7 +235,9 @@ class FormMeetingSeminarController extends Controller
}
$region = Region::find($cabang->region_id);
$kategori = Kategori::where('name', 'Meeting / Seminar')->first();
// Optimasi pencarian kategori menggunakan LIKE fleksibel untuk menjamin ID ditemukan
$kategori = Kategori::where('name', 'LIKE', '%Meeting / Seminar%')->first();
// Hitung periode dari tanggal input (bukan dari today)
if ($tanggal->day >= $startDay) {
@@ -266,7 +273,7 @@ class FormMeetingSeminarController extends Controller
// Check if the total nominal exceeds the available budget
if ($totalNominal > $availableBudget) {
session()->flash('error', 'Alokasi budget bulanan cabang tidak mencukupi,silahkan ajukan pada periode expense berikutnya');
return redirect()->back();
return redirect()->back()->withInput();
}
DB::beginTransaction();
@@ -276,6 +283,7 @@ class FormMeetingSeminarController extends Controller
$form = FormMeetingSeminar::create([
'expense_number' => $expense_number,
'user_id' => auth()->user()->id,
'cabang_id' => $cabang->id, // <--- REPARASI SUNTIK CABANG_ID BARU
'tanggal' => $request->tanggal,
'allowance' => $data_nominal['allowance'],
'transport_ankot' => $data_nominal['transport_ankot'],
@@ -285,6 +293,7 @@ class FormMeetingSeminarController extends Controller
'bukti_transport_ankot' => null,
'bukti_hotel' => null,
'account_number' => $kategori?->account_number,
'kategori_id' => $kategori->id ?? null,
'status' => 'On Progress',
]);
@@ -435,6 +444,28 @@ class FormMeetingSeminarController extends Controller
return redirect()->back();
}
$tanggal = Carbon::parse($request->tanggal);
// PEMBATASAN INPUT MENGGUNAKAN STARTING_DATE & INPUT_MAX_DATE
$startDay = (int) env('STARTING_DATE', 11);
$inputMaxDay = (int) env('INPUT_MAX_DATE', 10);
$now = Carbon::now();
if ($now->day >= $startDay) {
$startDate = Carbon::create($now->year, $now->month, $startDay)->startOfDay();
$maxInputDate = Carbon::create($now->copy()->addMonth()->year, $now->copy()->addMonth()->month, $inputMaxDay)->endOfDay();
} else {
$startDate = Carbon::create($now->copy()->subMonth()->year, $now->copy()->subMonth()->month, $startDay)->startOfDay();
$maxInputDate = Carbon::create($now->year, $now->month, $inputMaxDay)->endOfDay();
}
// Validasi apakah tanggal pengajuan masuk ke periode input saat ini
if ($tanggal->lt($startDate) || $tanggal->gt($maxInputDate)) {
session()->flash('error', "Gagal, tanggal expense tidak berada dalam batas waktu input yang diizinkan: {$startDate->format('d M Y')} - {$maxInputDate->format('d M Y')}");
return redirect()->back()->withInput();
}
$cabang = UserHasCabang::with('cabang')->where('user_id', $form->user_id)->first()?->cabang;
if (!$cabang) {
session()->flash('error', 'Cabang tidak ditemukan.');
@@ -442,7 +473,7 @@ class FormMeetingSeminarController extends Controller
}
$region = Region::find($cabang->region_id);
$kategori = Kategori::where('name', 'Meeting / Seminar')->first();
$kategori = Kategori::where('name', 'LIKE', '%Meeting / Seminar%')->first();
$folderPath = '/expense/' . $region->code . '/' . $cabang->code . '/meeting';
@@ -458,8 +489,6 @@ class FormMeetingSeminarController extends Controller
$totalNominal = array_sum($data_nominal);
// Hitung periode dari tanggal form yang diinput
$tanggal = Carbon::parse($request->tanggal);
$startDay = (int) env('STARTING_DATE', 11);
if ($tanggal->day >= $startDay) {
$periodeDate = Carbon::create($tanggal->year, $tanggal->month, $startDay);
} else {
@@ -473,18 +502,20 @@ class FormMeetingSeminarController extends Controller
$availableBudget = BudgetHelper::getAvailableBudget($cabang->id, $periodeMonth, $periodeYear);
if ($totalNominal > $availableBudget) {
session()->flash('error', 'The total nominal exceeds the available budget.');
return redirect()->back();
return redirect()->back()->withInput();
}
DB::beginTransaction();
try {
$form->update([
'cabang_id' => $cabang->id, // <--- REPARASI SUNTIK CABANG_ID TERBARU SAAT PROSES 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,
'kategori_id' => $kategori->id ?? null,
]);
if (!empty($attachmentsPayload)) {
@@ -735,7 +766,8 @@ class FormMeetingSeminarController extends Controller
return redirect()->back();
}
public function finalApprove($id)
// <--- REPARASI SUNTIKAN OBJECT REQUEST HASIL CHECKLIST JAVASCRIPT MODAL DILUAR
public function finalApprove(Request $request, $id)
{
$this->checkAuthorization(auth()->user(), ['final_approval.approve']);
$form = FormMeetingSeminar::findOrFail($id);
@@ -764,13 +796,18 @@ class FormMeetingSeminarController extends Controller
$periodeMonth = strtolower($periodeDate->format('F'));
$periodeYear = (int) $periodeDate->format('Y');
// <--- REPARASI: Penangkapan nilai payload dari checkbox JavaScript
$approvedTotal = $request->input('approved_total', $form->total);
// Ambil sisa budget bulanan dan sinkronkan dengan nominal dinamis terbaru
$availableBudget = BudgetHelper::getAvailableBudget($cabang_id, $periodeMonth, $periodeYear);
if($form->approved_total > $availableBudget) {
if($approvedTotal > $availableBudget) {
session()->flash('error', 'Budget cabang untuk periode ini tidak mencukupi, silahkan ajukan pada periode expense berikutnya');
return redirect()->back();
}
$form->update([
'approved_total' => $approvedTotal, // <--- SUNTIK NOMINAL BARU HASIL CHECKLIST DARI JAVASCRIPT KE DB
'final_approved_at' => now(),
'final_approved_by' => auth()->user()->id,
'status' => 'Closed',
@@ -779,7 +816,7 @@ class FormMeetingSeminarController extends Controller
$name = $form->user->name;
$expense_number = $form->expense_number;
$tanggal = $form->tanggal;
$total = $form->approved_total;
$total = $form->approved_total; // <--- TOTAL BARU YANG DIKIRIMKAN KE EMAIL / WHATSAPP
$roles = ['Head of Sales Marketing', 'Marketing Operational Manager Region'];
$recipients = [$form->user->email, auth()->user()->email];
@@ -822,16 +859,18 @@ class FormMeetingSeminarController extends Controller
$url = route('forms.meeting.view', $form->id);
WhatsappHelper::finalApprove($phoneNumbers, $expense_number, $url, $tanggal, $total, $name);
// Send bulk email
// Send bulk email (Optimasi menggunakan BrevoService)
$brevoService = app(BrevoService::class);
foreach ($recipients as $recipient) {
try {
Mail::to($recipient)->send(new FinalApprove(
$mail = new FinalApprove(
$name,
$expense_number,
$tanggal,
$total,
$url
));
);
$brevoService->expenseFinalApproved($recipient, $name, $mail);
} catch (\Exception $e) {
Log::error("Failed to send email to {$recipient}: " . $e->getMessage());
continue;
@@ -845,16 +884,22 @@ class FormMeetingSeminarController extends Controller
public function reject(Request $request, $id)
{
$this->checkAuthorization(auth()->user(), ['approval.reject']);
if (!auth()->user()->hasAnyPermission(['approval.reject', 'approval2.reject', 'final_approval.approve'])) {
abort(403, 'Akses Ditolak: Anda tidak memiliki izin untuk melakukan Reject.');
}
$request->validate([
'remarks' => 'required|string|max:500',
]);
$form = FormMeetingSeminar::findOrfail($id);
// Update Metadata Rejeksi
$form->update([
'status' => 'Rejected',
'remarks' => $request->remarks,
'rejected_by' => auth()->id(), // Kolom baru
'rejected_at' => now(), // Kolom baru
]);
$heads = $form->user->getCabangAndRegionHead();
@@ -863,7 +908,6 @@ class FormMeetingSeminarController extends Controller
$tanggal = $form->tanggal;
$total = $form->total;
// Collect all recipients
$recipients = [$form->user->email, auth()->user()->email];
$phoneNumbers = [$form->user->phone, auth()->user()->phone];
@@ -877,39 +921,24 @@ class FormMeetingSeminarController extends Controller
$phoneNumbers[] = $heads['region_head']['phone'];
}
// Remove duplicate email addresses, if any
$recipients = array_unique($recipients);
$phoneNumbers = array_unique($phoneNumbers);
// send whatsapp message
$url = route('forms.meeting.view', $form->id);
WhatsappHelper::rejectExpense($phoneNumbers, $expense_number, $url, $tanggal, $total, $name, $request->remarks);
// Send bulk email
$brevoService = app(BrevoService::class);
foreach ($recipients as $recipient) {
try {
$mail = new ExpenseRejected(
$name,
$expense_number,
$tanggal,
$total,
$url,
$request->remarks
);
$response = $brevoService->expenseRejected($recipient, $name, $mail);
if (isset($response['success']) && $response['success'] === false) {
Log::warning("Email to {$recipient} failed: " . ($response['message'] ?? 'Unknown error'));
}
$mail = new ExpenseRejected($name, $expense_number, $tanggal, $total, $url, $request->remarks);
$brevoService->expenseRejected($recipient, $name, $mail);
} catch (\Exception $e) {
Log::error("Failed to send email to {$recipient}: " . $e->getMessage());
continue;
}
}
AuditTrailHelper::AddAuditTrail('Reject', 'Reject Expense at Form Meeting Seminar (' . $form->expense_number . ')');
AuditTrailHelper::AddAuditTrail('Reject', 'Reject Expense at Form Meeting Seminar (' . $form->expense_number . ') oleh ' . auth()->user()->name);
session()->flash('success', 'Form has been rejected.');
return redirect()->back();
}
@@ -74,7 +74,7 @@ class FormOtherController extends Controller
$role = auth()->user()->getRoleNames()[0];
$startDay = (int) env('STARTING_DATE', 11);
$closingDay = (int) env('CLOSING_DATE', 10);
$closingDay = (int) env('CLOSING_DATE', 13);
$userRegionData = auth()->user()->getMyCabangAndRegionId();
$regionContextId = $userRegionData['region'] !== '-' ? $userRegionData['region'] : null;
$cabangContextId = $userRegionData['cabang'] !== '-' ? $userRegionData['cabang'] : null;
@@ -203,7 +203,7 @@ class FormOtherController extends Controller
'nullable',
'file',
'max:10240',
'mimetypes:image/jpeg,image/png,image/webp,application/pdf,application/msword,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,text/plain,text/csv,application/zip,application/x-zip-compressed,application/x-7z-compressed,application/vnd.rar,application/octet-stream'
'mimetypes:image/jpeg,image/png,image/webp,application/pdf,application/msword,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,text/plain,text/csv,application/zip,application/x-zip-compressed,application/x-7z-compressed,application/vnd.rar,application/octet-stream'
],
]);
@@ -229,23 +229,27 @@ class FormOtherController extends Controller
$validator->validate();
$tanggal = Carbon::parse($request->tanggal);
// PEMBATASAN INPUT MENGGUNAKAN STARTING_DATE & INPUT_MAX_DATE
$startDay = (int) env('STARTING_DATE', 11);
$closingDay = (int) env('CLOSING_DATE', 10);
$inputMaxDay = (int) env('INPUT_MAX_DATE', 10);
$now = Carbon::now();
// Tentukan periode input berdasarkan waktu saat ini
// Tentukan periode aktif berdasarkan tanggal saat ini
if ($now->day >= $startDay) {
$startDate = Carbon::create($now->year, $now->month, $startDay);
$closingDate = Carbon::create($now->copy()->addMonth()->year, $now->copy()->addMonth()->month, $closingDay);
// Jendela Aktif: Tgl 11 Bulan Ini s/d Tgl 10 Bulan Depan
$startDate = Carbon::create($now->year, $now->month, $startDay)->startOfDay();
$maxInputDate = Carbon::create($now->copy()->addMonth()->year, $now->copy()->addMonth()->month, $inputMaxDay)->endOfDay();
} else {
$startDate = Carbon::create($now->copy()->subMonth()->year, $now->copy()->subMonth()->month, $startDay);
$closingDate = Carbon::create($now->year, $now->month, $closingDay);
// Jendela Aktif: Tgl 11 Bulan Lalu s/d Tgl 10 Bulan Ini
$startDate = Carbon::create($now->copy()->subMonth()->year, $now->copy()->subMonth()->month, $startDay)->startOfDay();
$maxInputDate = Carbon::create($now->year, $now->month, $inputMaxDay)->endOfDay();
}
// Validasi apakah tanggal berada dalam periode aktif input
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();
// Validasi apakah tanggal pengajuan masuk ke periode input saat ini
if ($tanggal->lt($startDate) || $tanggal->gt($maxInputDate)) {
session()->flash('error', "Gagal, tanggal expense tidak berada dalam batas waktu input yang diizinkan: {$startDate->format('d M Y')} - {$maxInputDate->format('d M Y')}");
return redirect()->back()->withInput();
}
// Ambil cabang user
@@ -256,7 +260,7 @@ class FormOtherController extends Controller
return redirect()->back();
}
// Tentukan periode budget berdasarkan tanggal input (bukan now)
// Tentukan periode budget berdasarkan tanggal input
if ($tanggal->day >= $startDay) {
$periodeDate = Carbon::create($tanggal->year, $tanggal->month, $startDay);
} else {
@@ -270,7 +274,7 @@ class FormOtherController extends Controller
$availableBudget = BudgetHelper::getAvailableBudget($cabang->id, $periodeMonth, $periodeYear);
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();
@@ -279,7 +283,6 @@ class FormOtherController extends Controller
$folderPath = '/expense/' . $region->code . '/' . $cabang->code . '/other';
$attachmentsPayload = $this->buildAttachmentPayload($request, $folderPath);
$primaryAttachmentPath = $attachmentsPayload[0]['file_path'] ?? null;
$primaryAttachmentPath = $attachmentsPayload[0]['file_path'] ?? null;
// Generate sequence number and expense number
$sequence_number = FormOthers::withTrashed()->where('expense_number', 'like', $region->code . '-' . $cabang->code . '-OTH-' . date('ym') . '%')->count() + 1;
@@ -291,6 +294,7 @@ class FormOtherController extends Controller
$form = FormOthers::create([
'expense_number' => $expense_number,
'user_id' => auth()->user()->id,
'cabang_id' => $cabang->id, // <--- REPARASI SUNTIK CABANG_ID BARU
'kategori_id' => $request->kategori_id,
'tanggal' => $request->tanggal,
'keterangan' => $request->keterangan,
@@ -438,10 +442,28 @@ class FormOtherController extends Controller
return redirect()->back();
}
// Hitung periode dari tanggal input
$tanggal = Carbon::parse($request->tanggal);
$startDay = (int) env('STARTING_DATE', 11);
// PEMBATASAN INPUT MENGGUNAKAN STARTING_DATE & INPUT_MAX_DATE
$startDay = (int) env('STARTING_DATE', 11);
$inputMaxDay = (int) env('INPUT_MAX_DATE', 10);
$now = Carbon::now();
if ($now->day >= $startDay) {
$startDate = Carbon::create($now->year, $now->month, $startDay)->startOfDay();
$maxInputDate = Carbon::create($now->copy()->addMonth()->year, $now->copy()->addMonth()->month, $inputMaxDay)->endOfDay();
} else {
$startDate = Carbon::create($now->copy()->subMonth()->year, $now->copy()->subMonth()->month, $startDay)->startOfDay();
$maxInputDate = Carbon::create($now->year, $now->month, $inputMaxDay)->endOfDay();
}
// Validasi apakah tanggal pengajuan masuk ke periode input saat ini
if ($tanggal->lt($startDate) || $tanggal->gt($maxInputDate)) {
session()->flash('error', "Gagal, tanggal expense tidak berada dalam batas waktu input yang diizinkan: {$startDate->format('d M Y')} - {$maxInputDate->format('d M Y')}");
return redirect()->back()->withInput();
}
// Hitung periode budget dari tanggal input
if ($tanggal->day >= $startDay) {
$periodeDate = Carbon::create($tanggal->year, $tanggal->month, $startDay);
} else {
@@ -465,7 +487,7 @@ class FormOtherController extends Controller
'nullable',
'file',
'max:10240',
'mimetypes:image/jpeg,image/png,image/webp,application/pdf,application/msword,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,text/plain,text/csv,application/zip,application/x-zip-compressed,application/x-7z-compressed,application/vnd.rar,application/octet-stream'
'mimetypes:image/jpeg,image/png,image/webp,application/pdf,application/msword,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,text/plain,text/csv,application/zip,application/x-zip-compressed,application/x-7z-compressed,application/vnd.rar,application/octet-stream'
],
]);
@@ -495,10 +517,12 @@ class FormOtherController extends Controller
$folderPath = '/expense/' . $region->code . '/' . $cabang->code . '/other';
$attachmentsPayload = $this->buildAttachmentPayload($request, $folderPath);
$primaryAttachmentPath = !empty($attachmentsPayload) ? $attachmentsPayload[0]['file_path'] : null;
DB::beginTransaction();
try {
$form->update([
'cabang_id' => $cabang->id, // <--- REPARASI SUNTIK CABANG_ID TERBARU
'kategori_id' => $request->kategori_id,
'tanggal' => $request->tanggal,
'keterangan' => $request->keterangan,
@@ -752,7 +776,8 @@ class FormOtherController extends Controller
return redirect()->back();
}
public function finalApprove($id)
// <--- REPARASI SUNTIKAN OBJECT REQUEST HASIL CHECKLIST JAVASCRIPT MODAL DILUAR
public function finalApprove(Request $request, $id)
{
$this->checkAuthorization(auth()->user(), ['final_approval.approve']);
$form = FormOthers::findOrFail($id);
@@ -781,14 +806,18 @@ class FormOtherController extends Controller
$periodeMonth = strtolower($periodeDate->format('F'));
$periodeYear = (int) $periodeDate->format('Y');
// Ambil sisa budget
// <--- REPARASI: Penangkapan nilai payload dari checkbox JavaScript
$approvedTotal = $request->input('approved_total', $form->total);
// Ambil sisa budget dan sinkronkan validasinya dengan nominal dinamis terbaru
$availableBudget = BudgetHelper::getAvailableBudget($cabang_id, $periodeMonth, $periodeYear);
if($form->approved_total > $availableBudget) {
if($approvedTotal > $availableBudget) {
session()->flash('error', 'Budget cabang untuk periode ini tidak mencukupi, silahkan ajukan pada periode expense berikutnya');
return redirect()->back();
}
$form->update([
'approved_total' => $approvedTotal, // <--- SUNTIK NOMINAL BARU HASIL CHECKLIST DARI JAVASCRIPT KE DB
'final_approved_at' => now(),
'final_approved_by' => auth()->user()->id,
'status' => 'Closed',
@@ -797,7 +826,7 @@ class FormOtherController extends Controller
$name = $form->user->name;
$expense_number = $form->expense_number;
$tanggal = $form->created_at;
$total = $form->approved_total;
$total = $form->approved_total; // <--- SEKARANG MENGGUNAKAN DATA UPDATE TERBARU
$roles = ['Head of Sales Marketing', 'Marketing Operational Manager Region'];
$recipients = [$form->user->email, auth()->user()->email];
@@ -870,25 +899,29 @@ class FormOtherController extends Controller
public function reject(Request $request, $id)
{
$this->checkAuthorization(auth()->user(), ['approval.reject']);
if (!auth()->user()->hasAnyPermission(['approval.reject', 'approval2.reject', 'final_approval.approve'])) {
abort(403, 'Akses Ditolak: Anda tidak memiliki izin untuk melakukan Reject.');
}
$request->validate([
'remarks' => 'required|string|max:500',
]);
$form = FormOthers::findOrfail($id);
$form->update([
'status' => 'Rejected',
'remarks' => $request->remarks,
'rejected_by' => auth()->id(), // Kolom baru
'rejected_at' => now(), // Kolom baru
]);
$heads = $form->user->getCabangAndRegionHead();
$name = $form->user->name;
$expense_number = $form->expense_number;
$tanggal = $form->created_at;
$tanggal = $form->tanggal;
$total = $form->total;
// Collect all recipients
$recipients = [$form->user->email, auth()->user()->email];
$phoneNumbers = [$form->user->phone, auth()->user()->phone];
@@ -902,39 +935,24 @@ class FormOtherController extends Controller
$phoneNumbers[] = $heads['region_head']['phone'];
}
// Remove duplicate email addresses, if any
$recipients = array_unique($recipients);
$phoneNumbers = array_unique($phoneNumbers);
// send whatsapp message
$url = route('forms.other.view', $form->id);
$url = route('forms.others.view', $form->id);
WhatsappHelper::rejectExpense($phoneNumbers, $expense_number, $url, $tanggal, $total, $name, $request->remarks);
// Send bulk email
$brevoService = app(BrevoService::class);
foreach ($recipients as $recipient) {
try {
$mail = new ExpenseRejected(
$name,
$expense_number,
$tanggal,
$total,
$url,
$request->remarks
);
$response = $brevoService->expenseRejected($recipient, $name, $mail);
if (isset($response['success']) && $response['success'] === false) {
Log::warning("Email to {$recipient} failed: " . ($response['message'] ?? 'Unknown error'));
}
$mail = new ExpenseRejected($name, $expense_number, $tanggal, $total, $url, $request->remarks);
$brevoService->expenseRejected($recipient, $name, $mail);
} catch (\Exception $e) {
Log::error("Failed to send email to {$recipient}: " . $e->getMessage());
continue;
}
}
AuditTrailHelper::AddAuditTrail('Reject', 'Reject Expense at Form Other (' . $form->expense_number . ')');
AuditTrailHelper::AddAuditTrail('Reject', 'Reject Expense at Form Others (' . $form->expense_number . ') oleh ' . auth()->user()->name);
session()->flash('success', 'Form has been rejected.');
return redirect()->back();
}
@@ -66,7 +66,7 @@ class FormUpCountryController extends Controller
$role = auth()->user()->getRoleNames()[0];
$startDay = (int) env('STARTING_DATE', 11);
$closingDay = (int) env('CLOSING_DATE', 10);
$closingDay = (int) env('CLOSING_DATE', 13);
// Current time reference
$now = Carbon::now();
@@ -203,7 +203,7 @@ class FormUpCountryController extends Controller
}
public function store(Request $request)
{
{
$this->checkAuthorization(auth()->user(), ['forms.country.create']);
$request->validate([
'rayon_id' => 'required|exists:rayon,id',
@@ -221,23 +221,24 @@ class FormUpCountryController extends Controller
]);
$tanggal = Carbon::parse($request->tanggal);
// SINKRONISASI JENDELA INPUT ATURAN BISNIS: 11 s/d 10 Bulan Berikutnya
$startDay = (int) env('STARTING_DATE', 11);
$closingDay = (int) env('CLOSING_DATE', 10);
$inputMaxDay = (int) env('INPUT_MAX_DATE', 10);
$now = Carbon::now();
// Tentukan periode aktif saat ini
if ($now->day >= $startDay) {
$startDate = Carbon::create($now->year, $now->month, $startDay);
$closingDate = Carbon::create($now->copy()->addMonth()->year, $now->copy()->addMonth()->month, $closingDay);
$startDate = Carbon::create($now->year, $now->month, $startDay)->startOfDay();
$maxInputDate = Carbon::create($now->copy()->addMonth()->year, $now->copy()->addMonth()->month, $inputMaxDay)->endOfDay();
} else {
$startDate = Carbon::create($now->copy()->subMonth()->year, $now->copy()->subMonth()->month, $startDay);
$closingDate = Carbon::create($now->year, $now->month, $closingDay);
$startDate = Carbon::create($now->copy()->subMonth()->year, $now->copy()->subMonth()->month, $startDay)->startOfDay();
$maxInputDate = Carbon::create($now->year, $now->month, $inputMaxDay)->endOfDay();
}
// Validasi apakah tanggal input berada di periode aktif
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();
// Blokade Sistem: Tolak input jika berada di luar siklus pengisian aktif
if ($now->lt($startDate) || $now->gt($maxInputDate)) {
session()->flash('error', "Gagal, pengajuan saat ini sudah ditutup. Periode pengisian aktif hanya diizinkan dari tanggal {$startDay} sampai tanggal {$inputMaxDay} berikutnya.");
return redirect()->back()->withInput();
}
$cabang = UserHasCabang::with('cabang')->where('user_id', auth()->user()->id)->first()?->cabang;
@@ -247,7 +248,9 @@ class FormUpCountryController extends Controller
}
$region = Region::find($cabang->region_id);
$kategori = Kategori::where('name', 'Up Country')->first();
// Optimasi pencarian kategori menggunakan LIKE fleksibel untuk menjamin ID ditemukan
$kategori = Kategori::where('name', 'LIKE', '%Up Country%')->first();
// Hitung periode budget dari tanggal input
if ($tanggal->day >= $startDay) {
@@ -301,6 +304,8 @@ class FormUpCountryController extends Controller
$form = FormUpCountry::create([
'expense_number' => $expense_number,
'user_id' => auth()->user()->id,
'cabang_id' => $cabang->id, // <--- REPARASI: Memasukkan Cabang ID milik user pengaju secara instan
'kategori_id' => $kategori->id ?? null, // SUNTIK KATEGORI_ID OTOMATIS BERHASIL
'rayon_id' => $request->rayon_id,
'tanggal' => $request->tanggal,
'tujuan' => $request->tujuan,
@@ -315,14 +320,14 @@ class FormUpCountryController extends Controller
'bukti_transport_ankot' => null,
'bukti_hotel' => null,
'uc_plan' => $ucPlanPath,
'account_number' => $kategori->account_number,
'account_number' => $kategori->account_number ?? null,
'status' => 'On Progress',
]);
if (!empty($attachmentsPayload)) {
$this->attachmentService->addMultipleAttachments(
$form->id,
AttachmentTableName::FORM_UP_COUNTRY,
$dynamicTable = AttachmentTableName::FORM_UP_COUNTRY,
$attachmentsPayload
);
}
@@ -359,11 +364,11 @@ class FormUpCountryController extends Controller
foreach ($roles as $role) {
$users = Admin::getUserByRoleName($role)->filter(function ($user) use ($role, $cabang_id, $region_id) {
if ($role === 'Marketing Information System') {
return true; // Tidak ada filter cabang untuk peran ini
return true;
}
if (!$user->cabang || !$user->cabang->cabang) {
return false; // Pastikan user memiliki cabang
return false;
}
if ($role === 'Admin Region' || $role === 'Marketing Operational Manager Region') {
@@ -417,7 +422,7 @@ class FormUpCountryController extends Controller
AuditTrailHelper::AddAuditTrail('Insert', 'Insert New Expense at Form Up Country (' . $expense_number . ')');
session()->flash('success', 'Form has been created.');
return redirect()->back();
}
}
public function edit($id)
{
@@ -494,7 +499,7 @@ class FormUpCountryController extends Controller
}
public function update(Request $request, $id)
{
{
$this->checkAuthorization(auth()->user(), ['forms.country.edit']);
$role = auth()->user()->getRoleNames()[0];
@@ -525,12 +530,31 @@ class FormUpCountryController extends Controller
return redirect()->back();
}
// PEMBATASAN INPUT MENGGUNAKAN STARTING_DATE & INPUT_MAX_DATE
$startDay = (int) env('STARTING_DATE', 11);
$inputMaxDay = (int) env('INPUT_MAX_DATE', 10);
$now = Carbon::now();
if ($now->day >= $startDay) {
$startDate = Carbon::create($now->year, $now->month, $startDay)->startOfDay();
$maxInputDate = Carbon::create($now->copy()->addMonth()->year, $now->copy()->addMonth()->month, $inputMaxDay)->endOfDay();
} else {
$startDate = Carbon::create($now->copy()->subMonth()->year, $now->copy()->subMonth()->month, $startDay)->startOfDay();
$maxInputDate = Carbon::create($now->year, $now->month, $inputMaxDay)->endOfDay();
}
if ($now->lt($startDate) || $now->gt($maxInputDate)) {
session()->flash('error', "Gagal, pengajuan saat ini sudah ditutup. Periode pengisian aktif hanya diizinkan dari tanggal {$startDay} sampai tanggal {$inputMaxDay} berikutnya.");
return redirect()->back()->withInput();
}
$region = Region::find($cabang->region_id);
$kategori = Kategori::where('name', 'Up Country')->first();
// Optimasi pencarian kategori menggunakan LIKE fleksibel untuk menjamin ID ditemukan
$kategori = Kategori::where('name', 'LIKE', '%Up Country%')->first();
// Hitung periode dari tanggal input
$tanggal = Carbon::parse($request->tanggal);
$startDay = (int) env('STARTING_DATE', 11);
if ($tanggal->day >= $startDay) {
$periodeDate = Carbon::create($tanggal->year, $tanggal->month, $startDay);
} else {
@@ -564,7 +588,7 @@ class FormUpCountryController extends Controller
// Validasi budget dengan periode
$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();
}
@@ -574,6 +598,8 @@ class FormUpCountryController extends Controller
try {
$form->update([
'rayon_id' => $request->rayon_id,
'cabang_id' => $cabang->id, // <--- REPARASI: Memastikan Cabang ID tersimpan akurat saat update data
'kategori_id' => $kategori->id ?? null, // RE-SUNTIK KATEGORI_ID SAAT UPDATE
'tanggal' => $request->tanggal,
'tujuan' => $request->tujuan,
'jarak' => $request->jarak,
@@ -583,7 +609,7 @@ class FormUpCountryController extends Controller
'hotel' => $data_nominal['hotel'],
'total' => $totalNominal,
'uc_plan' => $ucPlanPath ?? $form->uc_plan,
'account_number' => $kategori->account_number,
'account_number' => $kategori->account_number ?? $form->account_number,
]);
if (!empty($attachmentsPayload)) {
@@ -609,7 +635,7 @@ class FormUpCountryController extends Controller
AuditTrailHelper::AddAuditTrail('Update', 'Update Record at Form Up Country (' . $form->expense_number . ')');
session()->flash('success', 'Form has been updated.');
return redirect()->back();
}
}
public function deleteAttachment(Request $request, $formId, $attachmentId)
{
@@ -717,11 +743,11 @@ class FormUpCountryController extends Controller
foreach ($roles as $role) {
$users = Admin::getUserByRoleName($role)->filter(function ($user) use ($role, $cabang_id, $region_id) {
if ($role === 'Marketing Information System') {
return true; // Tidak ada filter cabang untuk peran ini
return true;
}
if (!$user->cabang || !$user->cabang->cabang) {
return false; // Pastikan user memiliki cabang
return false;
}
if ($role === 'Admin Region' || $role === 'Marketing Operational Manager Region') {
@@ -808,11 +834,11 @@ class FormUpCountryController extends Controller
foreach ($roles as $role) {
$users = Admin::getUserByRoleName($role)->filter(function ($user) use ($role, $cabang_id, $region_id) {
if ($role === 'Marketing Information System' || $role === 'Head of Sales Marketing') {
return true; // Tidak ada filter cabang untuk peran ini
return true;
}
if (!$user->cabang || !$user->cabang->cabang) {
return false; // Pastikan user memiliki cabang
return false;
}
if ($role === 'Admin Region' || $role === 'Marketing Operational Manager Region') {
@@ -867,7 +893,7 @@ class FormUpCountryController extends Controller
return redirect()->back();
}
public function finalApprove($id)
public function finalApprove(Request $request, $id) // <--- MENAMBAHKAN OBJECT REQUEST
{
$this->checkAuthorization(auth()->user(), ['final_approval.approve']);
$form = FormUpCountry::findOrFail($id);
@@ -877,33 +903,52 @@ class FormUpCountryController extends Controller
return redirect()->back();
}
// Ambil cabang_id dari user yang mengisi form
// ATURAN BISNIS: Batas Akhir Approval adalah tanggal CLOSING_DATE (Bulan Depan tgl 13)
$closingDay = (int) env('CLOSING_DATE', 13);
$startDay = (int) env('STARTING_DATE', 11);
$now = Carbon::now();
$tanggalForm = Carbon::parse($form->tanggal);
// Hitung batas maksimal closing sesuai siklus transaksi form tersebut
if ($tanggalForm->day >= $startDay) {
$maxClosingDate = Carbon::create($tanggalForm->year, $tanggalForm->month, $closingDay)->addMonth()->endOfDay();
} else {
$maxClosingDate = Carbon::create($tanggalForm->year, $tanggalForm->month, $closingDay)->endOfDay();
}
if ($now->gt($maxClosingDate)) {
session()->flash('error', "Gagal, Batas waktu maksimal approval (Closing Date) untuk pengajuan ini telah berakhir pada {$maxClosingDate->format('d M Y H:i')} WIB.");
return redirect()->back();
}
$cabang_id = UserHasCabang::where('user_id', $form->user_id)->first()?->cabang_id;
if (!$cabang_id) {
session()->flash('error', 'Data cabang tidak ditemukan.');
return redirect()->back();
}
// Hitung periode berdasarkan tanggal form
$tanggal = Carbon::parse($form->tanggal);
$startDay = (int) env('STARTING_DATE', 11);
if ($tanggal->day >= $startDay) {
$periodeDate = Carbon::create($tanggal->year, $tanggal->month, $startDay);
if ($tanggalForm->day >= $startDay) {
$periodeDate = Carbon::create($tanggalForm->year, $tanggalForm->month, $startDay);
} else {
$periodeDate = Carbon::create($tanggal->copy()->subMonth()->year, $tanggal->copy()->subMonth()->month, $startDay);
$periodeDate = Carbon::create($tanggalForm->copy()->subMonth()->year, $tanggalForm->copy()->subMonth()->month, $startDay);
}
$periodeMonth = strtolower($periodeDate->format('F'));
$periodeYear = (int) $periodeDate->format('Y');
// MENGAMBIL NILAI AKUMULASI CHECKLIST DARI JAVASCRIPT JS (Fallback ke total awal jika kosong)
$approvedTotal = $request->input('approved_total', $form->total);
// Ambil sisa budget sesuai periode
$availableBudget = BudgetHelper::getAvailableBudget($cabang_id, $periodeMonth, $periodeYear);
if($form->approved_total > $availableBudget) {
if($approvedTotal > $availableBudget) { // <--- DISINKRONKAN DENGAN NILAI BARU HASIL CHECKLIST
session()->flash('error', 'Budget cabang untuk periode ini tidak mencukupi, silahkan ajukan pada periode expense berikutnya');
return redirect()->back();
}
// UPDATE DATABASE SECARA AKURAT BERDASARKAN HASIL PILIHAN CHECKLIST FINANCE
$form->update([
'approved_total' => $approvedTotal, // <--- SUNTIK NOMINAL BARU HASIL CHECKLIST KE DATABASE
'final_approved_at' => now(),
'final_approved_by' => auth()->user()->id,
'status' => 'Closed',
@@ -912,7 +957,7 @@ class FormUpCountryController extends Controller
$name = $form->user->name;
$expense_number = $form->expense_number;
$tanggal = $form->tanggal;
$total = $form->approved_total;
$total = $form->approved_total; // <--- OTOMATIS MEMAKAI DATA UPDATE TERBARU
$roles = ['Head of Sales Marketing', 'Marketing Operational Manager Region'];
$recipients = [$form->user->email, auth()->user()->email];
@@ -927,11 +972,11 @@ class FormUpCountryController extends Controller
foreach ($roles as $role) {
$users = Admin::getUserByRoleName($role)->filter(function ($user) use ($role, $cabang_id, $region_id) {
if ($role === 'Marketing Information System' || $role === 'Head of Sales Marketing') {
return true; // Tidak ada filter cabang untuk peran ini
return true;
}
if (!$user->cabang || !$user->cabang->cabang) {
return false; // Pastikan user memiliki cabang
return false;
}
if ($role === 'Admin Region' || $role === 'Marketing Operational Manager Region') {
@@ -985,16 +1030,21 @@ class FormUpCountryController extends Controller
public function reject(Request $request, $id)
{
$this->checkAuthorization(auth()->user(), ['approval.reject']);
if (!auth()->user()->hasAnyPermission(['approval.reject', 'approval2.reject', 'final_approval.approve'])) {
abort(403, 'Akses Ditolak: Anda tidak memiliki izin untuk melakukan Reject.');
}
$request->validate([
'remarks' => 'required|string|max:500',
]);
$form = FormUpCountry::findOrfail($id);
$form->update([
'status' => 'Rejected',
'remarks' => $request->remarks,
'rejected_by' => auth()->id(), // SUNTIK DATA AUDIT TRAIL
'rejected_at' => now(), // SUNTIK DATA AUDIT TRAIL
]);
$heads = $form->user->getCabangAndRegionHead();
@@ -1003,7 +1053,6 @@ class FormUpCountryController extends Controller
$tanggal = $form->tanggal;
$total = $form->total;
// Collect all recipients
$recipients = [$form->user->email, auth()->user()->email];
$phoneNumbers = [$form->user->phone, auth()->user()->phone];
@@ -1017,39 +1066,24 @@ class FormUpCountryController extends Controller
$phoneNumbers[] = $heads['region_head']['phone'];
}
// Remove duplicate email addresses, if any
$recipients = array_unique($recipients);
$phoneNumbers = array_unique($phoneNumbers);
// send whatsapp message
$url = route('forms.up-country.view', $form->id);
WhatsappHelper::rejectExpense($phoneNumbers, $expense_number, $url, $tanggal, $total, $name, $request->remarks);
// Send bulk email
$brevoService = app(BrevoService::class);
foreach ($recipients as $recipient) {
try {
$mail = new ExpenseRejected(
$name,
$expense_number,
$tanggal,
$total,
$url,
$request->remarks
);
$response = $brevoService->expenseRejected($recipient, $name, $mail);
if (isset($response['success']) && $response['success'] === false) {
Log::warning("Email to {$recipient} failed: " . ($response['message'] ?? 'Unknown error'));
}
$mail = new ExpenseRejected($name, $expense_number, $tanggal, $total, $url, $request->remarks);
$brevoService->expenseRejected($recipient, $name, $mail);
} catch (\Exception $e) {
Log::error("Failed to send email to {$recipient}: " . $e->getMessage());
continue;
}
}
AuditTrailHelper::AddAuditTrail('Reject', 'Reject Expense at Form Up Country (' . $form->expense_number . ')');
AuditTrailHelper::AddAuditTrail('Reject', 'Reject Expense at Form Up Country (' . $form->expense_number . ') oleh ' . auth()->user()->name);
session()->flash('success', 'Form has been rejected.');
return redirect()->back();
}
@@ -68,7 +68,7 @@ class FormVehicleController extends Controller
$role = auth()->user()->getRoleNames()[0];
$startDay = (int) env('STARTING_DATE', 11);
$closingDay = (int) env('CLOSING_DATE', 10);
$closingDay = (int) env('CLOSING_DATE', 13);
// Referensi waktu sekarang
$now = Carbon::now();
@@ -226,23 +226,27 @@ class FormVehicleController extends Controller
$validator->validate();
$tanggal = Carbon::parse($request->tanggal);
// PEMBATASAN INPUT MENGGUNAKAN STARTING_DATE & INPUT_MAX_DATE
$startDay = (int) env('STARTING_DATE', 11);
$closingDay = (int) env('CLOSING_DATE', 10);
$inputMaxDay = (int) env('INPUT_MAX_DATE', 10);
$now = Carbon::now();
// Tentukan periode input berdasarkan waktu saat ini
// Tentukan periode aktif berdasarkan tanggal saat ini
if ($now->day >= $startDay) {
$startDate = Carbon::create($now->year, $now->month, $startDay);
$closingDate = Carbon::create($now->copy()->addMonth()->year, $now->copy()->addMonth()->month, $closingDay);
// Jendela Aktif: Tgl 11 Bulan Ini s/d Tgl 10 Bulan Depan
$startDate = Carbon::create($now->year, $now->month, $startDay)->startOfDay();
$maxInputDate = Carbon::create($now->copy()->addMonth()->year, $now->copy()->addMonth()->month, $inputMaxDay)->endOfDay();
} else {
$startDate = Carbon::create($now->copy()->subMonth()->year, $now->copy()->subMonth()->month, $startDay);
$closingDate = Carbon::create($now->year, $now->month, $closingDay);
// Jendela Aktif: Tgl 11 Bulan Lalu s/d Tgl 10 Bulan Ini
$startDate = Carbon::create($now->copy()->subMonth()->year, $now->copy()->subMonth()->month, $startDay)->startOfDay();
$maxInputDate = Carbon::create($now->year, $now->month, $inputMaxDay)->endOfDay();
}
// Validasi apakah tanggal form berada dalam periode input
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();
// Validasi apakah tanggal pengajuan masuk ke periode input saat ini
if ($tanggal->lt($startDate) || $tanggal->gt($maxInputDate)) {
session()->flash('error', "Gagal, tanggal expense tidak berada dalam batas waktu input yang diizinkan: {$startDate->format('d M Y')} - {$maxInputDate->format('d M Y')}");
return redirect()->back()->withInput();
}
// Ambil cabang user
@@ -252,7 +256,7 @@ class FormVehicleController extends Controller
return redirect()->back();
}
// Tentukan periode budget berdasarkan tanggal input (bukan now)
// Tentukan periode budget dari tanggal input
if ($tanggal->day >= $startDay) {
$periodeDate = Carbon::create($tanggal->year, $tanggal->month, $startDay);
} else {
@@ -266,11 +270,12 @@ class FormVehicleController extends Controller
$availableBudget = BudgetHelper::getAvailableBudget($cabang->id, $periodeMonth, $periodeYear);
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();
$kategori = Kategori::where('name', 'Vehicle Running Cost')->first();
// REPARASI SUNTIK KATEGORI: Penggunaan LIKE untuk toleransi kecocokan spasi
$kategori = Kategori::where('name', 'LIKE', '%Vehicle Running Cost%')->first();
$folderPath = '/expense/' . $region->code . '/' . $cabang->code . '/vehicle';
@@ -286,6 +291,8 @@ class FormVehicleController extends Controller
$form = FormVehicleRunningCost::create([
'expense_number' => $expense_number,
'user_id' => auth()->user()->id,
'cabang_id' => $cabang->id, // <--- REPARASI SUNTIK CABANG ID
'kategori_id' => $kategori->id ?? null, // <--- REPARASI SUNTIK KATEGORI ID
'tanggal' => $request->tanggal,
'type' => $request->type,
'liter' => $request->liter ?? 0,
@@ -295,7 +302,7 @@ class FormVehicleController extends Controller
'nopol' => $request->nopol ?? '-',
'keterangan' => $request->keterangan,
'bukti_total' => null,
'account_number' => $kategori->account_number,
'account_number' => $kategori->account_number ?? null,
'status' => 'On Progress',
]);
@@ -426,16 +433,34 @@ class FormVehicleController extends Controller
$form = FormVehicleRunningCost::with('attachments')->findOrFail($id);
// Ambil cabang ID dari user yang mengisi form
$cabang_id = UserHasCabang::where('user_id', $form->user_id)->first()?->cabang_id;
if (!$cabang_id) {
$cabang = UserHasCabang::with('cabang')->where('user_id', $form->user_id)->first()?->cabang;
if (!$cabang) {
session()->flash('error', 'Data cabang tidak ditemukan.');
return redirect()->back();
}
// Hitung periode berdasarkan tanggal input
$tanggal = Carbon::parse($request->tanggal);
$startDay = (int) env('STARTING_DATE', 11);
// PEMBATASAN INPUT MENGGUNAKAN STARTING_DATE & INPUT_MAX_DATE
$startDay = (int) env('STARTING_DATE', 11);
$inputMaxDay = (int) env('INPUT_MAX_DATE', 10);
$now = Carbon::now();
if ($now->day >= $startDay) {
$startDate = Carbon::create($now->year, $now->month, $startDay)->startOfDay();
$maxInputDate = Carbon::create($now->copy()->addMonth()->year, $now->copy()->addMonth()->month, $inputMaxDay)->endOfDay();
} else {
$startDate = Carbon::create($now->copy()->subMonth()->year, $now->copy()->subMonth()->month, $startDay)->startOfDay();
$maxInputDate = Carbon::create($now->year, $now->month, $inputMaxDay)->endOfDay();
}
// Validasi apakah tanggal pengajuan masuk ke periode input saat ini
if ($tanggal->lt($startDate) || $tanggal->gt($maxInputDate)) {
session()->flash('error', "Gagal, tanggal expense tidak berada dalam batas waktu input yang diizinkan: {$startDate->format('d M Y')} - {$maxInputDate->format('d M Y')}");
return redirect()->back()->withInput();
}
// Hitung periode dari tanggal form yang diinput
if ($tanggal->day >= $startDay) {
$periodeDate = Carbon::create($tanggal->year, $tanggal->month, $startDay);
} else {
@@ -446,7 +471,7 @@ class FormVehicleController extends Controller
$periodeYear = (int) $periodeDate->format('Y');
// Hitung sisa budget
$availableBudget = BudgetHelper::getAvailableBudget($cabang_id, $periodeMonth, $periodeYear);
$availableBudget = BudgetHelper::getAvailableBudget($cabang->id, $periodeMonth, $periodeYear);
// Validasi input
$validator = Validator::make($request->all(), [
@@ -494,9 +519,9 @@ class FormVehicleController extends Controller
return redirect()->back();
}
$cabang = UserHasCabang::with('cabang')->where('user_id', $form->user_id)->first()->cabang;
$region = Region::where('id', $cabang->region_id)->first();
$kategori = Kategori::where('name', 'Vehicle Running Cost')->first();
// REPARASI SUNTIK KATEGORI_ID SAAT UPDATE
$kategori = Kategori::where('name', 'LIKE', '%Vehicle Running Cost%')->first();
$folderPath = '/expense/' . $region->code . '/' . $cabang->code . '/vehicle';
@@ -505,6 +530,8 @@ class FormVehicleController extends Controller
DB::beginTransaction();
try {
$form->update([
'cabang_id' => $cabang->id, // <--- REPARASI SUNTIK CABANG_ID
'kategori_id' => $kategori->id ?? null, // <--- REPARASI SUNTIK KATEGORI_ID
'tanggal' => $request->tanggal,
'type' => $request->type,
'liter' => $request->liter ?? 0,
@@ -514,7 +541,7 @@ class FormVehicleController extends Controller
'nopol' => $request->nopol ?? '-',
'keterangan' => $request->keterangan,
'bukti_total' => $form->bukti_total,
'account_number' => $kategori->account_number,
'account_number' => $kategori->account_number ?? $form->account_number,
'status' => 'On Progress',
]);
@@ -755,7 +782,8 @@ class FormVehicleController extends Controller
return redirect()->back();
}
public function finalApprove($id)
// <--- REPARASI SUNTIKAN REQUEST DARI JAVASCRIPT MODAL AGAR DATA BISA DIAMBIL
public function finalApprove(Request $request, $id)
{
$this->checkAuthorization(auth()->user(), ['final_approval.approve']);
$form = FormVehicleRunningCost::findOrFail($id);
@@ -785,14 +813,18 @@ class FormVehicleController extends Controller
$periodeMonth = strtolower($periodeDate->format('F'));
$periodeYear = (int) $periodeDate->format('Y');
// Hitung sisa budget cabang di periode tersebut
// <--- REPARASI: Penangkapan nilai payload dari checkbox JavaScript
$approvedTotal = $request->input('approved_total', $form->total);
// Hitung sisa budget cabang di periode tersebut disinkronisasi dengan variabel kalkulasi terbaru
$availableBudget = BudgetHelper::getAvailableBudget($cabang_id, $periodeMonth, $periodeYear);
if($form->approved_total > $availableBudget) {
if($approvedTotal > $availableBudget) {
session()->flash('error', 'Budget cabang untuk periode ini tidak mencukupi, silahkan ajukan pada periode expense berikutnya');
return redirect()->back();
}
$form->update([
'approved_total' => $approvedTotal, // <--- SUNTIK DATA UPDATE HASIL CHECKLIST
'final_approved_at' => now(),
'final_approved_by' => auth()->user()->id,
'status' => 'Closed',
@@ -801,7 +833,7 @@ class FormVehicleController extends Controller
$name = $form->user->name;
$expense_number = $form->expense_number;
$tanggal = $form->created_at;
$total = $form->approved_total;
$total = $form->approved_total; // <--- TOTAL BARU YANG DIKIRIMKAN KE EMAIL / WHATSAPP
$roles = ['Head of Sales Marketing', 'Marketing Operational Manager Region'];
$recipients = [$form->user->email, auth()->user()->email];
@@ -874,26 +906,30 @@ class FormVehicleController extends Controller
}
public function reject(Request $request, $id)
{
$this->checkAuthorization(auth()->user(), ['approval.reject']);
{
if (!auth()->user()->hasAnyPermission(['approval.reject', 'approval2.reject', 'final_approval.approve'])) {
abort(403, 'Akses Ditolak: Anda tidak memiliki izin untuk melakukan Reject.');
}
$request->validate([
'remarks' => 'required|string|max:500',
]);
$form = FormVehicleRunningCost::findOrfail($id);
$form->update([
'status' => 'Rejected',
'remarks' => $request->remarks,
]);
'rejected_by' => auth()->id(), // Kolom baru
'rejected_at' => now(), // Kolom baru
]);
$heads = $form->user->getCabangAndRegionHead();
$name = $form->user->name;
$expense_number = $form->expense_number;
$tanggal = $form->created_at;
$tanggal = $form->tanggal;
$total = $form->total;
// Collect all recipients
$recipients = [$form->user->email, auth()->user()->email];
$phoneNumbers = [$form->user->phone, auth()->user()->phone];
@@ -907,42 +943,27 @@ class FormVehicleController extends Controller
$phoneNumbers[] = $heads['region_head']['phone'];
}
// Remove duplicate email addresses, if any
$recipients = array_unique($recipients);
$phoneNumbers = array_unique($phoneNumbers);
// send whatsapp message
$url = route('forms.vehicle.view', $form->id);
WhatsappHelper::rejectExpense($phoneNumbers, $expense_number, $url, $tanggal, $total, $name, $request->remarks);
// Send bulk email
$brevoService = app(BrevoService::class);
foreach ($recipients as $recipient) {
try {
$mail = new ExpenseRejected(
$name,
$expense_number,
$tanggal,
$total,
$url,
$request->remarks
);
$response = $brevoService->expenseRejected($recipient, $name, $mail);
if (isset($response['success']) && $response['success'] === false) {
Log::warning("Email to {$recipient} failed: " . ($response['message'] ?? 'Unknown error'));
}
$mail = new ExpenseRejected($name, $expense_number, $tanggal, $total, $url, $request->remarks);
$brevoService->expenseRejected($recipient, $name, $mail);
} catch (\Exception $e) {
Log::error("Failed to send email to {$recipient}: " . $e->getMessage());
continue;
}
}
AuditTrailHelper::AddAuditTrail('Reject', 'Reject Expense at Form Vehicle Running Cost (' . $form->expense_number . ')');
AuditTrailHelper::AddAuditTrail('Reject', 'Reject Expense at Form Vehicle Running Cost (' . $form->expense_number . ') oleh ' . auth()->user()->name);
session()->flash('success', 'Form has been rejected.');
return redirect()->back();
}
}
public function open($id)
{
+2
View File
@@ -21,6 +21,8 @@ class BudgetControl extends Model
'transferred_at',
'closing_at',
'status',
'transfer_proof',
'transfer_status',
];
public function user()
+29 -6
View File
@@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class FormEntertaimentPresentation extends Model
{
use SoftDeletes;
use SoftDeletes, \App\Traits\HasAttachments;
protected $table = 'form_entertainment_presentation';
protected $fillable = [
@@ -31,17 +31,40 @@ class FormEntertaimentPresentation extends Model
'approved2_by',
'final_approved_at',
'final_approved_by',
// Kolom baru
'jabatan',
'nama_perusahaan',
'jenis_usaha',
];
protected $casts = [
'created_at' => 'datetime',
'approved_at' => 'datetime',
'approved2_at' => 'datetime',
'final_approved_at' => 'datetime',
];
public function user()
{
return $this->belongsTo('App\Models\Admin');
}
public function attachments()
{
return $this->hasMany(\App\Models\AttachmentForm::class, 'form_id')
->where('table_name', \App\Enums\AttachmentTableName::FORM_ENTERTAINMENT_PRESENTATION);
}
// Relasi ke kategori untuk mengambil account_number
public function kategori()
{
return $this->belongsTo(Kategori::class, 'kategori_id');
}
public function rejector() {
return $this->belongsTo(User::class, 'rejected_by');
}
// public function attachments()
// {
// // form_id adalah kolom di tabel attachment_forms
// // table_name digunakan jika satu tabel attachment dipakai banyak form
// return $this->hasMany(\App\Models\AttachmentForm::class, 'form_id')
// ->where('table_name', 'form_entertainment_presentation');
// }
}
+32 -25
View File
@@ -4,46 +4,53 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class FormMeetingSeminar extends Model
{
use SoftDeletes;
protected $table = 'form_meeting_seminar';
protected $fillable = [
'expense_number',
'user_id',
'tanggal',
'allowance',
'transport_ankot',
'hotel',
'total',
'approved_allowance',
'approved_transport_ankot',
'approved_hotel',
'approved_total',
'bukti_allowance',
'bukti_transport_ankot',
'bukti_hotel',
'account_number',
'status',
'remarks',
'approved_at',
'approved2_at',
'approved_by',
'approved2_by',
'final_approved_at',
'expense_number', 'user_id', 'tanggal', 'allowance', 'transport_ankot',
'hotel', 'total', 'approved_allowance', 'approved_transport_ankot',
'approved_hotel', 'approved_total', 'bukti_allowance', 'bukti_transport_ankot',
'bukti_hotel', 'account_number', 'status', 'remarks', 'approved_at',
'approved2_at', 'approved_by', 'approved2_by', 'final_approved_at',
'final_approved_by',
];
public function user()
/**
* Casts untuk otomatisasi format Carbon pada tanggal agar konsisten di Blade.
*/
protected $casts = [
'tanggal' => 'date',
'final_approved_at' => 'datetime',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* Relasi ke Admin (Pengaju).
*/
public function user(): BelongsTo
{
return $this->belongsTo(Admin::class, 'user_id');
}
public function attachments()
/**
* Relasi ke Attachments (Dokumen Digital) sesuai pola Entertainment.
*/
public function attachments(): HasMany
{
return $this->hasMany(\App\Models\AttachmentForm::class, 'form_id')
return $this->hasMany(AttachmentForm::class, 'form_id')
->where('table_name', \App\Enums\AttachmentTableName::FORM_MEETING_SEMINAR);
}
public function rejector() {
return $this->belongsTo(User::class, 'rejected_by');
}
}
+35 -22
View File
@@ -4,6 +4,8 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class FormOthers extends Model
{
@@ -11,38 +13,49 @@ class FormOthers extends Model
protected $table = 'form_others';
protected $fillable = [
'expense_number',
'user_id',
'kategori_id',
'tanggal',
'keterangan',
'total',
'approved_total',
'bukti_total',
'account_number',
'status',
'remarks',
'approved_at',
'approved2_at',
'approved_by',
'approved2_by',
'final_approved_at',
'final_approved_by',
'expense_number', 'user_id', 'kategori_id', 'tanggal', 'keterangan',
'total', 'approved_total', 'bukti_total', 'account_number', 'status',
'remarks', 'approved_at', 'approved2_at', 'approved_by', 'approved2_by',
'final_approved_at', 'final_approved_by',
];
public function user()
/**
* Casts untuk otomatisasi format Carbon pada tanggal agar konsisten di Blade.
*/
protected $casts = [
'tanggal' => 'date',
'final_approved_at' => 'datetime',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* Relasi ke Admin (Pengaju).
*/
public function user(): BelongsTo
{
return $this->belongsTo(Admin::class);
return $this->belongsTo(Admin::class, 'user_id');
}
public function kategori()
/**
* Relasi ke Kategori.
*/
public function kategori(): BelongsTo
{
return $this->belongsTo(Kategori::class);
return $this->belongsTo(Kategori::class, 'kategori_id');
}
public function attachments()
/**
* Relasi ke Attachments (Dokumen Digital) sesuai pola Entertainment.
*/
public function attachments(): HasMany
{
return $this->hasMany(\App\Models\AttachmentForm::class, 'form_id')
->where('table_name', \App\Enums\AttachmentTableName::FORM_OTHERS);
}
public function rejector() {
return $this->belongsTo(User::class, 'rejected_by');
}
}
+39 -36
View File
@@ -4,58 +4,61 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class FormUpCountry extends Model
{
use SoftDeletes;
protected $table = 'form_up_country';
protected $fillable = [
'expense_number',
'user_id',
'rayon_id',
'tanggal',
'tujuan',
'jarak',
'allowance',
'transport_dalkot',
'transport_ankot',
'hotel',
'total',
'approved_allowance',
'approved_transport_dalkot',
'approved_transport_ankot',
'approved_hotel',
'approved_total',
'bukti_allowance',
'bukti_transport_dalkot',
'bukti_transport_ankot',
'bukti_hotel',
'uc_plan',
'account_number',
'status',
'remarks',
'approved_at',
'approved2_at',
'approved_by',
'approved2_by',
'final_approved_at',
'final_approved_by',
'expense_number', 'user_id', 'rayon_id', 'tanggal', 'tujuan', 'jarak',
'allowance', 'transport_dalkot', 'transport_ankot', 'hotel', 'total',
'approved_allowance', 'approved_transport_dalkot', 'approved_transport_ankot',
'approved_hotel', 'approved_total', 'bukti_allowance', 'bukti_transport_dalkot',
'bukti_transport_ankot', 'bukti_hotel', 'uc_plan', 'account_number', 'status',
'remarks', 'approved_at', 'approved2_at', 'approved_by', 'approved2_by',
'final_approved_at', 'final_approved_by','cabang_id','kategori_id',
];
public function user()
/**
* Casts untuk otomatisasi format Carbon pada tanggal.
*/
protected $casts = [
'tanggal' => 'date',
'final_approved_at' => 'datetime',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* Relasi ke Admin (Pengaju).
*/
public function user(): BelongsTo
{
return $this->belongsTo('App\Models\Admin');
return $this->belongsTo(Admin::class, 'user_id');
}
public function rayon()
/**
* Relasi ke Rayon.
*/
public function rayon(): BelongsTo
{
return $this->belongsTo('App\Models\Rayon');
return $this->belongsTo(Rayon::class, 'rayon_id');
}
public function attachments()
/**
* Relasi ke Attachments (Dokumen Digital) sesuai pola Entertainment.
*/
public function attachments(): HasMany
{
return $this->hasMany(\App\Models\AttachmentForm::class, 'form_id')
return $this->hasMany(AttachmentForm::class, 'form_id')
->where('table_name', \App\Enums\AttachmentTableName::FORM_UP_COUNTRY);
}
public function rejector() {
return $this->belongsTo(User::class, 'rejected_by');
}
}
+32 -25
View File
@@ -4,44 +4,51 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class FormVehicleRunningCost extends Model
{
use SoftDeletes;
protected $table = 'form_vehicle_running_cost';
protected $fillable = [
'expense_number',
'user_id',
'tanggal',
'liter',
'total',
'approved_total',
'jarak',
'tipe_bensin',
'nopol',
'keterangan',
'bukti_total',
'account_number',
'type',
'status',
'remarks',
'approved_at',
'approved2_at',
'approved_by',
'approved2_by',
'final_approved_at',
'final_approved_by',
'expense_number', 'user_id', 'tanggal', 'liter', 'total', 'approved_total',
'jarak', 'tipe_bensin', 'nopol', 'keterangan', 'bukti_total', 'account_number',
'type', 'status', 'remarks', 'approved_at', 'approved2_at', 'approved_by',
'approved2_by', 'final_approved_at', 'final_approved_by',
];
public function user()
/**
* Casts untuk otomatisasi format Carbon pada tanggal.
*/
protected $casts = [
'tanggal' => 'date',
'final_approved_at' => 'datetime',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* Relasi ke Admin (Pengaju)
*/
public function user(): BelongsTo
{
return $this->belongsTo('App\Models\Admin', 'user_id');
return $this->belongsTo(Admin::class, 'user_id');
}
public function attachments()
/**
* Relasi ke Attachments (Dokumen Digital)
*/
public function attachments(): HasMany
{
return $this->hasMany(\App\Models\AttachmentForm::class, 'form_id')
return $this->hasMany(AttachmentForm::class, 'form_id')
->where('table_name', \App\Enums\AttachmentTableName::FORM_VEHICLE_RUNNING_COST);
}
public function rejector() {
return $this->belongsTo(User::class, 'rejected_by');
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Transaction extends Model
{
use SoftDeletes;
protected $fillable = [
'cabang_id', 'budget_control_id', 'transaction_date', 'reference_number',
'description', 'amount', 'type', 'category', 'periode_month',
'periode_year', 'created_by'
];
protected $casts = [
'transaction_date' => 'date',
'amount' => 'decimal:2',
];
public function cabang() {
return $this->belongsTo(Cabang::class);
}
}
+40
View File
@@ -25,6 +25,8 @@ class User extends Authenticatable
'username',
'password',
'phone',
'region_id',
'cabang_id'
];
/**
@@ -49,4 +51,42 @@ class User extends Authenticatable
'password' => 'hashed',
];
}
/**
* Helper untuk mendapatkan ID Cabang & Region milik user
*/
public function getMyCabangAndRegionId(): array
{
// Jika User punya cabang_id (Level AM / MR)
if ($this->cabang_id) {
$cabang = \DB::table('cabang')->where('id', $this->cabang_id)->first();
return [
'cabang' => (int) $this->cabang_id,
'region' => $cabang ? (int) $cabang->region_id : null
];
}
// Jika User level Region (Admin Region / MOM Region)
// Pastikan kolom 'region_id' ada di tabel users Anda
return [
'cabang' => null,
'region' => $this->region_id ? (int) $this->region_id : null
];
}
public function cabangs()
{
// Relasi Many-to-Many melalui tabel pivot user_has_cabang
return $this->belongsToMany(Cabang::class, 'user_has_cabang', 'user_id', 'cabang_id');
}
/**
* Mendapatkan ID Region berdasarkan penugasan cabang pertama
*/
public function getAssignedRegionId()
{
$firstAssignment = $this->cabangs()->first();
return $firstAssignment ? $firstAssignment->region_id : null;
}
}
+3
View File
@@ -4,6 +4,7 @@ namespace App\Providers;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;
use Illuminate\Pagination\Paginator;
class AppServiceProvider extends ServiceProvider
@@ -29,6 +30,8 @@ class AppServiceProvider extends ServiceProvider
URL::forceScheme('https');
}
Paginator::useBootstrap();
}
}
+284
View File
@@ -0,0 +1,284 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
class DashboardService
{
protected $startDate;
protected $endDate;
protected $allowedCabangIds = [];
protected $user;
public function __construct($user)
{
$this->user = $user;
$this->setPeriode();
$this->setUserVisibility();
}
/**
* Set Periode Cut-off (11 - 10)
*/
protected function setPeriode(): void
{
$startDay = (int) env('STARTING_DATE', 11);
$closingDay = (int) env('CLOSING_DATE', 10);
$now = Carbon::now();
if ($now->day >= $startDay) {
$this->startDate = $now->copy()->day($startDay)->startOfDay();
$this->endDate = $now->copy()->addMonth()->day($closingDay)->endOfDay();
} else {
$this->startDate = $now->copy()->subMonth()->day($startDay)->startOfDay();
$this->endDate = $now->copy()->day($closingDay)->endOfDay();
}
}
/**
* Atur Visibilitas Berdasarkan Hierarki Role
*/
protected function setUserVisibility(): void
{
// 1. MIS / HEAD / SUPERADMIN: Akses Nasional
if ($this->user->hasRole(['Marketing Information System', 'Head of Sales Management', 'Super Admin'])) {
$this->allowedCabangIds = [];
return;
}
$myCabangs = DB::table('user_has_cabang')
->where('user_id', $this->user->id)
->whereNull('deleted_at')
->pluck('cabang_id')
->toArray();
// 2. MOM / ADMIN REGION: Akses Regional (Semua cabang di dalam regionnya)
if ($this->user->hasRole(['Marketing Operational Manager Region', 'Admin Region'])) {
$regionIds = DB::table('cabang')
->whereIn('id', $myCabangs)
->pluck('region_id')
->toArray();
$this->allowedCabangIds = DB::table('cabang')
->whereIn('region_id', $regionIds)
->pluck('id')
->toArray();
}
// 3. AM / MR: Akses Cabang Spesifik
else {
$this->allowedCabangIds = $myCabangs;
}
}
/**
* Helper Filter: Digunakan di semua query analitik
*/
protected function applyFilters($query)
{
if (!empty($this->allowedCabangIds)) {
$query->whereIn('combined_forms.cabang_id', $this->allowedCabangIds);
}
return $query;
}
/**
* Query Utama: Menggabungkan 5 tabel kategori expense
*/
protected function getCombinedFormsQuery()
{
$tables = [
'form_up_country',
'form_vehicle_running_cost',
'form_entertainment_presentation',
'form_meeting_seminar',
'form_others'
];
$query = null;
foreach ($tables as $table) {
$sub = DB::table($table)
->select(
'id',
'cabang_id',
'total',
'status',
'created_at',
'final_approved_at',
DB::raw("'$table' as source_table")
)
->whereNull('deleted_at');
$query = ($query === null) ? $sub : $query->unionAll($sub);
}
return DB::table(DB::raw("({$query->toSql()}) as combined_forms"))
->mergeBindings($query)
->join('cabang', 'combined_forms.cabang_id', '=', 'cabang.id')
->join('region', 'cabang.region_id', '=', 'region.id');
}
/**
* Data untuk 4 Kartu Statistik Utama
*/
public function getApprovalStats(): array
{
// Ambil semua data tanpa filter tanggal dulu untuk kartu "Waiting"
$allData = $this->getCombinedFormsQuery()
->select('combined_forms.status', 'combined_forms.total', 'combined_forms.created_at')
->get();
$map = [
// 1. TOTAL PENDING: Khusus periode aktif (11 Maret - 10 April) & Status On Progress
'total_pending' => [
'label' => 'Total Pending',
'keys' => ['on progress'],
'color' => 'warning',
'icon' => 'fas fa-hourglass-half',
'use_period' => true
],
// 2. WAITING APPR 1: Semua yang statusnya On Progress
'appr1' => [
'label' => 'Waiting Appr 1',
'keys' => ['on progress'],
'color' => 'info',
'icon' => 'fas fa-user-check',
'use_period' => false
],
// 3. WAITING APPR 2: Semua yang statusnya Approved 1
'appr2' => [
'label' => 'Waiting Appr 2',
'keys' => ['approved 1'],
'color' => 'primary',
'icon' => 'fas fa-file-invoice-dollar',
'use_period' => false
],
// 4. WAITING FINAL APPROVED: Semua yang statusnya Approved 2
'waiting_final' => [
'label' => 'Waiting Final Approved',
'keys' => ['approved 2'],
'color' => 'secondary',
'icon' => 'fas fa-tasks',
'use_period' => false
],
// 5. FINAL APPROVED (CLOSED): Semua yang sudah Closed
'closed' => [
'label' => 'Final Approved',
'keys' => ['closed', 'approved'],
'color' => 'success',
'icon' => 'fas fa-check-double',
'use_period' => false
],
];
$results = [];
foreach ($map as $config) {
$filtered = $allData->filter(function ($item) use ($config) {
$statusMatch = in_array(strtolower(trim((string)$item->status)), $config['keys']);
// Jika kartu membutuhkan filter periode (Maret 11 - April 10)
if ($config['use_period']) {
$dateMatch = Carbon::parse($item->created_at)->between($this->startDate, $this->endDate);
return $statusMatch && $dateMatch;
}
return $statusMatch;
});
$results[] = [
'label' => $config['label'],
'count' => $filtered->count(),
'amount' => (float) $filtered->sum('total'),
'color' => $config['color'],
'icon' => $config['icon']
];
}
return $results;
}
/**
* Mengambil daftar pengajuan dengan status 'Approved 2'
* Untuk antrian persetujuan akhir (HOSM/MIS)
*/
public function getPendingFinalApproval()
{
$query = $this->getCombinedFormsQuery()
->select(
'combined_forms.*',
'cabang.name as cabang_name',
'region.name as region_name'
)
->where('combined_forms.status', 'Approved 2');
$this->applyFilters($query);
return $query->orderBy('combined_forms.created_at', 'desc')->paginate(10);
}
/**
* Summary per Kategori untuk Chart
*/
public function getTotals(): array
{
$query = $this->getCombinedFormsQuery()
->whereBetween('combined_forms.created_at', [$this->startDate, $this->endDate])
->whereIn('combined_forms.status', ['Approved', 'Closed']);
$this->applyFilters($query);
$results = $query->select('combined_forms.source_table', DB::raw('SUM(combined_forms.total) as total_per_table'))
->groupBy('combined_forms.source_table')
->get();
return [
'forms' => [
'upcountry' => $results->where('source_table', 'form_up_country')->first()->total_per_table ?? 0,
'vehicle' => $results->where('source_table', 'form_vehicle_running_cost')->first()->total_per_table ?? 0,
'entertainment' => $results->where('source_table', 'form_entertainment_presentation')->first()->total_per_table ?? 0,
'meeting' => $results->where('source_table', 'form_meeting_seminar')->first()->total_per_table ?? 0,
'others' => $results->where('source_table', 'form_others')->first()->total_per_table ?? 0,
],
'grand_total' => $results->sum('total_per_table')
];
}
/**
* Top 5 Pengeluaran Terbanyak per Cabang
*/
/**
* Top 10 Pengeluaran Terbanyak (Volume Giants)
*/
public function getTopExpenses()
{
$query = $this->getCombinedFormsQuery()
->whereBetween('combined_forms.created_at', [$this->startDate, $this->endDate]);
$this->applyFilters($query);
return $query->select('cabang.name', 'region.name as region_name', DB::raw('SUM(combined_forms.total) as total_amount'))
->groupBy('cabang.id', 'cabang.name', 'region.name')
->orderBy('total_amount', 'desc')
->take(10) // Update ke 10
->get();
}
/**
* Top 10 Efisiensi Approval (Speed Kings)
*/
public function getFastestClosing()
{
$query = $this->getCombinedFormsQuery()
->select('cabang.name', DB::raw('AVG(TIMESTAMPDIFF(HOUR, combined_forms.created_at, combined_forms.final_approved_at)) as avg_hours'))
->whereIn('combined_forms.status', ['Approved', 'Closed'])
->whereNotNull('combined_forms.final_approved_at')
->whereBetween('combined_forms.final_approved_at', [$this->startDate, $this->endDate]);
$this->applyFilters($query);
return $query->groupBy('cabang.id', 'cabang.name')
->orderBy('avg_hours', 'asc')
->take(10) // Update ke 10
->get();
}
}
+15
View File
@@ -0,0 +1,15 @@
<?php
namespace App\Traits;
use App\Models\AttachmentForm;
trait HasAttachments
{
public function attachments()
{
// Relasi ini akan otomatis menyesuaikan table_name berdasarkan property $table di model
return $this->hasMany(AttachmentForm::class, 'form_id')
->where('table_name', $this->table);
}
}
+484
View File
@@ -0,0 +1,484 @@
SSUUMMMMAARRYY OOFF LLEESSSS CCOOMMMMAANNDDSS
Commands marked with * may be preceded by a number, _N.
Notes in parentheses indicate the behavior if _N is given.
A key preceded by a caret indicates the Ctrl key; thus ^K is ctrl-K.
h H Display this help.
q :q Q :Q ZZ Exit.
---------------------------------------------------------------------------
MMOOVVIINNGG
e ^E j ^N CR * Forward one line (or _N lines).
y ^Y k ^K ^P * Backward one line (or _N lines).
f ^F ^V SPACE * Forward one window (or _N lines).
b ^B ESC-v * Backward one window (or _N lines).
z * Forward one window (and set window to _N).
w * Backward one window (and set window to _N).
ESC-SPACE * Forward one window, but don't stop at end-of-file.
d ^D * Forward one half-window (and set half-window to _N).
u ^U * Backward one half-window (and set half-window to _N).
ESC-) RightArrow * Right one half screen width (or _N positions).
ESC-( LeftArrow * Left one half screen width (or _N positions).
ESC-} ^RightArrow Right to last column displayed.
ESC-{ ^LeftArrow Left to first column.
F Forward forever; like "tail -f".
ESC-F Like F but stop when search pattern is found.
r ^R ^L Repaint screen.
R Repaint screen, discarding buffered input.
---------------------------------------------------
Default "window" is the screen height.
Default "half-window" is half of the screen height.
---------------------------------------------------------------------------
SSEEAARRCCHHIINNGG
/_p_a_t_t_e_r_n * Search forward for (_N-th) matching line.
?_p_a_t_t_e_r_n * Search backward for (_N-th) matching line.
n * Repeat previous search (for _N-th occurrence).
N * Repeat previous search in reverse direction.
ESC-n * Repeat previous search, spanning files.
ESC-N * Repeat previous search, reverse dir. & spanning files.
ESC-u Undo (toggle) search highlighting.
&_p_a_t_t_e_r_n * Display only matching lines
---------------------------------------------------
A search pattern may be preceded by one or more of:
^N or ! Search for NON-matching lines.
^E or * Search multiple files (pass thru END OF FILE).
^F or @ Start search at FIRST file (for /) or last file (for ?).
^K Highlight matches, but don't move (KEEP position).
^R Don't use REGULAR EXPRESSIONS.
---------------------------------------------------------------------------
JJUUMMPPIINNGG
g < ESC-< * Go to first line in file (or line _N).
G > ESC-> * Go to last line in file (or line _N).
p % * Go to beginning of file (or _N percent into file).
t * Go to the (_N-th) next tag.
T * Go to the (_N-th) previous tag.
{ ( [ * Find close bracket } ) ].
} ) ] * Find open bracket { ( [.
ESC-^F _<_c_1_> _<_c_2_> * Find close bracket _<_c_2_>.
ESC-^B _<_c_1_> _<_c_2_> * Find open bracket _<_c_1_>
---------------------------------------------------
Each "find close bracket" command goes forward to the close bracket
matching the (_N-th) open bracket in the top line.
Each "find open bracket" command goes backward to the open bracket
matching the (_N-th) close bracket in the bottom line.
m_<_l_e_t_t_e_r_> Mark the current top line with <letter>.
M_<_l_e_t_t_e_r_> Mark the current bottom line with <letter>.
'_<_l_e_t_t_e_r_> Go to a previously marked position.
'' Go to the previous position.
^X^X Same as '.
ESC-M_<_l_e_t_t_e_r_> Clear a mark.
---------------------------------------------------
A mark is any upper-case or lower-case letter.
Certain marks are predefined:
^ means beginning of the file
$ means end of the file
---------------------------------------------------------------------------
CCHHAANNGGIINNGG FFIILLEESS
:e [_f_i_l_e] Examine a new file.
^X^V Same as :e.
:n * Examine the (_N-th) next file from the command line.
:p * Examine the (_N-th) previous file from the command line.
:x * Examine the first (or _N-th) file from the command line.
:d Delete the current file from the command line list.
= ^G :f Print current file name.
---------------------------------------------------------------------------
MMIISSCCEELLLLAANNEEOOUUSS CCOOMMMMAANNDDSS
-_<_f_l_a_g_> Toggle a command line option [see OPTIONS below].
--_<_n_a_m_e_> Toggle a command line option, by name.
__<_f_l_a_g_> Display the setting of a command line option.
___<_n_a_m_e_> Display the setting of an option, by name.
+_c_m_d Execute the less cmd each time a new file is examined.
!_c_o_m_m_a_n_d Execute the shell command with $SHELL.
|XX_c_o_m_m_a_n_d Pipe file between current pos & mark XX to shell command.
s _f_i_l_e Save input to a file.
v Edit the current file with $VISUAL or $EDITOR.
V Print version number of "less".
---------------------------------------------------------------------------
OOPPTTIIOONNSS
Most options may be changed either on the command line,
or from within less by using the - or -- command.
Options may be given in one of two forms: either a single
character preceded by a -, or a name preceded by --.
-? ........ --help
Display help (from command line).
-a ........ --search-skip-screen
Search skips current screen.
-A ........ --SEARCH-SKIP-SCREEN
Search starts just after target line.
-b [_N] .... --buffers=[_N]
Number of buffers.
-B ........ --auto-buffers
Don't automatically allocate buffers for pipes.
-c -C .... --clear-screen --CLEAR-SCREEN
Repaint by clearing rather than scrolling.
-d ........ --dumb
Dumb terminal.
-D [_x_n_._n] . --color=_x_n_._n
Set screen colors. (MS-DOS only)
-e -E .... --quit-at-eof --QUIT-AT-EOF
Quit at end of file.
-f ........ --force
Force open non-regular files.
-F ........ --quit-if-one-screen
Quit if entire file fits on first screen.
-g ........ --hilite-search
Highlight only last match for searches.
-G ........ --HILITE-SEARCH
Don't highlight any matches for searches.
--old-bot
Revert to the old bottom of screen behavior.
-h [_N] .... --max-back-scroll=[_N]
Backward scroll limit.
-i ........ --ignore-case
Ignore case in searches that do not contain uppercase.
-I ........ --IGNORE-CASE
Ignore case in all searches.
-j [_N] .... --jump-target=[_N]
Screen position of target lines.
-J ........ --status-column
Display a status column at left edge of screen.
-k [_f_i_l_e] . --lesskey-file=[_f_i_l_e]
Use a lesskey file.
-K --quit-on-intr
Exit less in response to ctrl-C.
-L ........ --no-lessopen
Ignore the LESSOPEN environment variable.
-m -M .... --long-prompt --LONG-PROMPT
Set prompt style.
-n ........ --line-numbers
Don't use line numbers.
-N ........ --LINE-NUMBERS
Use line numbers.
-o [_f_i_l_e] . --log-file=[_f_i_l_e]
Copy to log file (standard input only).
-O [_f_i_l_e] . --LOG-FILE=[_f_i_l_e]
Copy to log file (unconditionally overwrite).
-p [_p_a_t_t_e_r_n] --pattern=[_p_a_t_t_e_r_n]
Start at pattern (from command line).
-P [_p_r_o_m_p_t] --prompt=[_p_r_o_m_p_t]
Define new prompt.
-q -Q .... --quiet --QUIET --silent --SILENT
Quiet the terminal bell.
-r -R .... --raw-control-chars --RAW-CONTROL-CHARS
Output "raw" control characters.
-s ........ --squeeze-blank-lines
Squeeze multiple blank lines.
-S ........ --chop-long-lines
Chop (truncate) long lines rather than wrapping.
-t [_t_a_g] .. --tag=[_t_a_g]
Find a tag.
-T [_t_a_g_s_f_i_l_e] --tag-file=[_t_a_g_s_f_i_l_e]
Use an alternate tags file.
-u -U .... --underline-special --UNDERLINE-SPECIAL
Change handling of backspaces.
-V ........ --version
Display the version number of "less".
-w ........ --hilite-unread
Highlight first new line after forward-screen.
-W ........ --HILITE-UNREAD
Highlight first new line after any forward movement.
-x [_N[,...]] --tabs=[_N[,...]]
Set tab stops.
-X ........ --no-init
Don't use termcap init/deinit strings.
-y [_N] .... --max-forw-scroll=[_N]
Forward scroll limit.
-z [_N] .... --window=[_N]
Set size of window.
-" [_c[_c]] . --quotes=[_c[_c]]
Set shell quote characters.
-~ ........ --tilde
Don't display tildes after end of file.
-# [_N] .... --shift=[_N]
Horizontal scroll amount (0 = one half screen width)
........ --no-keypad
Don't send termcap keypad init/deinit strings.
........ --follow-name
The F command changes files if the input file is renamed.
........ --use-backslash
Subsequent options use backslash as escape char.
---------------------------------------------------------------------------
LLIINNEE EEDDIITTIINNGG
These keys can be used to edit text being entered
on the "command line" at the bottom of the screen.
RightArrow ..................... ESC-l ... Move cursor right one character.
LeftArrow ...................... ESC-h ... Move cursor left one character.
ctrl-RightArrow ESC-RightArrow ESC-w ... Move cursor right one word.
ctrl-LeftArrow ESC-LeftArrow ESC-b ... Move cursor left one word.
HOME ........................... ESC-0 ... Move cursor to start of line.
END ............................ ESC-$ ... Move cursor to end of line.
BACKSPACE ................................ Delete char to left of cursor.
DELETE ......................... ESC-x ... Delete char under cursor.
ctrl-BACKSPACE ESC-BACKSPACE ........... Delete word to left of cursor.
ctrl-DELETE .... ESC-DELETE .... ESC-X ... Delete word under cursor.
ctrl-U ......... ESC (MS-DOS only) ....... Delete entire line.
UpArrow ........................ ESC-k ... Retrieve previous command line.
DownArrow ...................... ESC-j ... Retrieve next command line.
TAB ...................................... Complete filename & cycle.
SHIFT-TAB ...................... ESC-TAB Complete filename & reverse cycle.
ctrl-L ................................... Complete filename, list all.
SSUUMMMMAARRYY OOFF LLEESSSS CCOOMMMMAANNDDSS
Commands marked with * may be preceded by a number, _N.
Notes in parentheses indicate the behavior if _N is given.
A key preceded by a caret indicates the Ctrl key; thus ^K is ctrl-K.
h H Display this help.
q :q Q :Q ZZ Exit.
---------------------------------------------------------------------------
MMOOVVIINNGG
e ^E j ^N CR * Forward one line (or _N lines).
y ^Y k ^K ^P * Backward one line (or _N lines).
f ^F ^V SPACE * Forward one window (or _N lines).
b ^B ESC-v * Backward one window (or _N lines).
z * Forward one window (and set window to _N).
w * Backward one window (and set window to _N).
ESC-SPACE * Forward one window, but don't stop at end-of-file.
d ^D * Forward one half-window (and set half-window to _N).
u ^U * Backward one half-window (and set half-window to _N).
ESC-) RightArrow * Right one half screen width (or _N positions).
ESC-( LeftArrow * Left one half screen width (or _N positions).
ESC-} ^RightArrow Right to last column displayed.
ESC-{ ^LeftArrow Left to first column.
F Forward forever; like "tail -f".
ESC-F Like F but stop when search pattern is found.
r ^R ^L Repaint screen.
R Repaint screen, discarding buffered input.
---------------------------------------------------
Default "window" is the screen height.
Default "half-window" is half of the screen height.
---------------------------------------------------------------------------
SSEEAARRCCHHIINNGG
/_p_a_t_t_e_r_n * Search forward for (_N-th) matching line.
?_p_a_t_t_e_r_n * Search backward for (_N-th) matching line.
n * Repeat previous search (for _N-th occurrence).
N * Repeat previous search in reverse direction.
ESC-n * Repeat previous search, spanning files.
ESC-N * Repeat previous search, reverse dir. & spanning files.
ESC-u Undo (toggle) search highlighting.
&_p_a_t_t_e_r_n * Display only matching lines
---------------------------------------------------
A search pattern may be preceded by one or more of:
^N or ! Search for NON-matching lines.
^E or * Search multiple files (pass thru END OF FILE).
^F or @ Start search at FIRST file (for /) or last file (for ?).
^K Highlight matches, but don't move (KEEP position).
^R Don't use REGULAR EXPRESSIONS.
---------------------------------------------------------------------------
JJUUMMPPIINNGG
g < ESC-< * Go to first line in file (or line _N).
G > ESC-> * Go to last line in file (or line _N).
p % * Go to beginning of file (or _N percent into file).
t * Go to the (_N-th) next tag.
T * Go to the (_N-th) previous tag.
{ ( [ * Find close bracket } ) ].
} ) ] * Find open bracket { ( [.
ESC-^F _<_c_1_> _<_c_2_> * Find close bracket _<_c_2_>.
ESC-^B _<_c_1_> _<_c_2_> * Find open bracket _<_c_1_>
---------------------------------------------------
Each "find close bracket" command goes forward to the close bracket
matching the (_N-th) open bracket in the top line.
Each "find open bracket" command goes backward to the open bracket
matching the (_N-th) close bracket in the bottom line.
m_<_l_e_t_t_e_r_> Mark the current top line with <letter>.
M_<_l_e_t_t_e_r_> Mark the current bottom line with <letter>.
'_<_l_e_t_t_e_r_> Go to a previously marked position.
'' Go to the previous position.
^X^X Same as '.
ESC-M_<_l_e_t_t_e_r_> Clear a mark.
---------------------------------------------------
A mark is any upper-case or lower-case letter.
Certain marks are predefined:
^ means beginning of the file
$ means end of the file
---------------------------------------------------------------------------
CCHHAANNGGIINNGG FFIILLEESS
:e [_f_i_l_e] Examine a new file.
^X^V Same as :e.
:n * Examine the (_N-th) next file from the command line.
:p * Examine the (_N-th) previous file from the command line.
:x * Examine the first (or _N-th) file from the command line.
:d Delete the current file from the command line list.
= ^G :f Print current file name.
---------------------------------------------------------------------------
MMIISSCCEELLLLAANNEEOOUUSS CCOOMMMMAANNDDSS
-_<_f_l_a_g_> Toggle a command line option [see OPTIONS below].
--_<_n_a_m_e_> Toggle a command line option, by name.
__<_f_l_a_g_> Display the setting of a command line option.
___<_n_a_m_e_> Display the setting of an option, by name.
+_c_m_d Execute the less cmd each time a new file is examined.
!_c_o_m_m_a_n_d Execute the shell command with $SHELL.
|XX_c_o_m_m_a_n_d Pipe file between current pos & mark XX to shell command.
s _f_i_l_e Save input to a file.
v Edit the current file with $VISUAL or $EDITOR.
V Print version number of "less".
---------------------------------------------------------------------------
OOPPTTIIOONNSS
Most options may be changed either on the command line,
or from within less by using the - or -- command.
Options may be given in one of two forms: either a single
character preceded by a -, or a name preceded by --.
-? ........ --help
Display help (from command line).
-a ........ --search-skip-screen
Search skips current screen.
-A ........ --SEARCH-SKIP-SCREEN
Search starts just after target line.
-b [_N] .... --buffers=[_N]
Number of buffers.
-B ........ --auto-buffers
Don't automatically allocate buffers for pipes.
-c -C .... --clear-screen --CLEAR-SCREEN
Repaint by clearing rather than scrolling.
-d ........ --dumb
Dumb terminal.
-D [_x_n_._n] . --color=_x_n_._n
Set screen colors. (MS-DOS only)
-e -E .... --quit-at-eof --QUIT-AT-EOF
Quit at end of file.
-f ........ --force
Force open non-regular files.
-F ........ --quit-if-one-screen
Quit if entire file fits on first screen.
-g ........ --hilite-search
Highlight only last match for searches.
-G ........ --HILITE-SEARCH
Don't highlight any matches for searches.
--old-bot
Revert to the old bottom of screen behavior.
-h [_N] .... --max-back-scroll=[_N]
Backward scroll limit.
-i ........ --ignore-case
Ignore case in searches that do not contain uppercase.
-I ........ --IGNORE-CASE
Ignore case in all searches.
-j [_N] .... --jump-target=[_N]
Screen position of target lines.
-J ........ --status-column
Display a status column at left edge of screen.
-k [_f_i_l_e] . --lesskey-file=[_f_i_l_e]
Use a lesskey file.
-K --quit-on-intr
Exit less in response to ctrl-C.
-L ........ --no-lessopen
Ignore the LESSOPEN environment variable.
-m -M .... --long-prompt --LONG-PROMPT
Set prompt style.
-n ........ --line-numbers
Don't use line numbers.
-N ........ --LINE-NUMBERS
Use line numbers.
-o [_f_i_l_e] . --log-file=[_f_i_l_e]
Copy to log file (standard input only).
-O [_f_i_l_e] . --LOG-FILE=[_f_i_l_e]
Copy to log file (unconditionally overwrite).
-p [_p_a_t_t_e_r_n] --pattern=[_p_a_t_t_e_r_n]
Start at pattern (from command line).
-P [_p_r_o_m_p_t] --prompt=[_p_r_o_m_p_t]
Define new prompt.
-q -Q .... --quiet --QUIET --silent --SILENT
Quiet the terminal bell.
-r -R .... --raw-control-chars --RAW-CONTROL-CHARS
Output "raw" control characters.
-s ........ --squeeze-blank-lines
Squeeze multiple blank lines.
-S ........ --chop-long-lines
Chop (truncate) long lines rather than wrapping.
-t [_t_a_g] .. --tag=[_t_a_g]
Find a tag.
-T [_t_a_g_s_f_i_l_e] --tag-file=[_t_a_g_s_f_i_l_e]
Use an alternate tags file.
-u -U .... --underline-special --UNDERLINE-SPECIAL
Change handling of backspaces.
-V ........ --version
Display the version number of "less".
-w ........ --hilite-unread
Highlight first new line after forward-screen.
-W ........ --HILITE-UNREAD
Highlight first new line after any forward movement.
-x [_N[,...]] --tabs=[_N[,...]]
Set tab stops.
-X ........ --no-init
Don't use termcap init/deinit strings.
-y [_N] .... --max-forw-scroll=[_N]
Forward scroll limit.
-z [_N] .... --window=[_N]
Set size of window.
-" [_c[_c]] . --quotes=[_c[_c]]
Set shell quote characters.
-~ ........ --tilde
Don't display tildes after end of file.
-# [_N] .... --shift=[_N]
Horizontal scroll amount (0 = one half screen width)
........ --no-keypad
Don't send termcap keypad init/deinit strings.
........ --follow-name
The F command changes files if the input file is renamed.
........ --use-backslash
Subsequent options use backslash as escape char.
---------------------------------------------------------------------------
LLIINNEE EEDDIITTIINNGG
These keys can be used to edit text being entered
on the "command line" at the bottom of the screen.
RightArrow ..................... ESC-l ... Move cursor right one character.
LeftArrow ...................... ESC-h ... Move cursor left one character.
ctrl-RightArrow ESC-RightArrow ESC-w ... Move cursor right one word.
ctrl-LeftArrow ESC-LeftArrow ESC-b ... Move cursor left one word.
HOME ........................... ESC-0 ... Move cursor to start of line.
END ............................ ESC-$ ... Move cursor to end of line.
BACKSPACE ................................ Delete char to left of cursor.
DELETE ......................... ESC-x ... Delete char under cursor.
ctrl-BACKSPACE ESC-BACKSPACE ........... Delete word to left of cursor.
ctrl-DELETE .... ESC-DELETE .... ESC-X ... Delete word under cursor.
ctrl-U ......... ESC (MS-DOS only) ....... Delete entire line.
UpArrow ........................ ESC-k ... Retrieve previous command line.
DownArrow ...................... ESC-j ... Retrieve next command line.
TAB ...................................... Complete filename & cycle.
SHIFT-TAB ...................... ESC-TAB Complete filename & reverse cycle.
ctrl-L ................................... Complete filename, list all.
Regular → Executable
View File
+5 -2
View File
@@ -3,7 +3,10 @@
"name": "laravel/laravel",
"type": "project",
"description": "The skeleton application for the Laravel framework.",
"keywords": ["laravel", "framework"],
"keywords": [
"laravel",
"framework"
],
"license": "MIT",
"require": {
"php": "^8.2",
@@ -16,8 +19,8 @@
"laravel/ui": "^4.6",
"laravolt/avatar": "^6.0",
"league/flysystem-webdav": "^3.29",
"maatwebsite/excel": "^3.1",
"nino/laravel-nextcloud-fs": "^2.0",
"phpoffice/phpspreadsheet": "^3.5",
"singlequote/laravel-webdav": "^1.1",
"spatie/image": "^3.7",
"spatie/laravel-html": "^3.11",
Generated
+1862 -1169
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -47,7 +47,7 @@ return [
|
*/
'timezone' => env('APP_TIMEZONE', 'UTC'),
'timezone' => env('APP_TIMEZONE', 'Asia/Jakarta'),
/*
|--------------------------------------------------------------------------
+16 -2
View File
@@ -62,6 +62,20 @@ return [
]) : [],
],
'mysql_lama' => [
'driver' => 'mysql',
'host' => env('DB_OLD_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_OLD_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'strict' => true,
'engine' => null,
],
'mariadb' => [
'driver' => 'mariadb',
'url' => env('DB_URL'),
@@ -78,8 +92,8 @@ return [
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
\Pdo\Mysql::ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
'pgsql' => [
@@ -6,8 +6,13 @@ use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// Pengecekan agar tidak error jika tabel sudah ada di database
if (!Schema::hasTable('attachment_form')) {
Schema::create('attachment_form', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('form_id');
@@ -17,10 +22,15 @@ return new class extends Migration
$table->timestamps();
$table->softDeletes();
// Menambahkan index untuk performa query
$table->index(['form_id', 'table_name'], 'idx_form_table');
});
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('attachment_form');
@@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('budget_control', function (Blueprint $table) {
// CEK: Hanya tambah kolom jika belum ada di database
if (!Schema::hasColumn('budget_control', 'region_id')) {
$table->unsignedBigInteger('region_id')->nullable()->after('cabang_id');
}
});
}
public function down(): void
{
Schema::table('budget_control', function (Blueprint $table) {
if (Schema::hasColumn('budget_control', 'region_id')) {
$table->dropColumn('region_id');
}
});
}
};
@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('budget_control', function (Blueprint $table) {
// Menambahkan kolom bukti transfer (berisi path file)
$table->string('transfer_proof')->after('remarks')->nullable();
// Menambahkan status transfer untuk approval Accounting
$table->enum('transfer_status', ['Pending', 'Approved', 'Rejected'])
->default('Pending')
->after('transfer_proof');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('budget_control', function (Blueprint $table) {
// Menghapus kolom jika migration di-rollback
$table->dropColumn(['transfer_proof', 'transfer_status']);
});
}
};
@@ -0,0 +1,46 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up(): void
{
// TAHAP 1: Ubah kolom menjadi string (VARCHAR) agar bisa menerima teks apapun
Schema::table('budget_control', function (Blueprint $table) {
$table->string('transfer_status')->change();
});
// TAHAP 2: Update data lama ke status baru
// Kita tangani semua kemungkinan data agar tidak ada yang 'nyangkut'
DB::table('budget_control')
->where('transfer_status', 'Approved')
->update(['transfer_status' => 'Validated']);
// Opsional: Jika ada data kosong/null, amankan ke 'Pending'
DB::table('budget_control')
->whereNull('transfer_status')
->orWhere('transfer_status', '')
->update(['transfer_status' => 'Pending']);
// TAHAP 3: Kembalikan kolom ke ENUM dengan definisi baru
Schema::table('budget_control', function (Blueprint $table) {
$table->enum('transfer_status', ['Pending', 'Waiting Approval', 'Validated', 'Rejected'])
->default('Pending')
->change();
});
}
public function down(): void
{
// Rollback: Kembalikan ke definisi awal jika diperlukan
Schema::table('budget_control', function (Blueprint $table) {
$table->enum('transfer_status', ['Pending', 'Approved', 'Rejected'])
->default('Pending')
->change();
});
}
};
@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
// FK ke tabel region dan cabang
$table->foreignId('region_id')->after('password')->nullable()->constrained('region');
$table->foreignId('cabang_id')->after('region_id')->nullable()->constrained('cabang');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
//
});
}
};
@@ -0,0 +1,93 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
// 1. Jika tabel belum ada sama sekali, buat baru dengan struktur lengkap
if (!Schema::hasTable('transactions')) {
Schema::create('transactions', function (Blueprint $table) {
$table->id();
$table->foreignId('cabang_id')->constrained('cabang')->onDelete('cascade');
$table->foreignId('budget_control_id')->nullable()->constrained('budget_control')->onDelete('set null');
$table->date('transaction_date');
$table->string('reference_number')->unique();
$table->string('description');
$table->decimal('amount', 15, 2);
$table->enum('type', ['DEBIT', 'CREDIT']);
$table->string('category')->default('General');
$table->string('periode_month');
$table->year('periode_year');
$table->foreignId('created_by')->constrained('admins');
$table->timestamps();
$table->softDeletes();
});
return;
}
// 2. JIKA TABEL SUDAH EXIST: Tambahkan kolom baru tanpa merusak / menimpa kolom cabang_id yang sudah ada
Schema::table('transactions', function (Blueprint $table) {
// Pastikan cabang_id tidak dibuat ulang, kita hanya memastikan tipenya aman jika belum ada
if (!Schema::hasColumn('transactions', 'cabang_id')) {
$table->foreignId('cabang_id')->constrained('cabang')->onDelete('cascade');
}
// Tambahkan kolom pendukung lainnya secara aman jika belum eksis di database
if (!Schema::hasColumn('transactions', 'budget_control_id')) {
$table->foreignId('budget_control_id')->nullable()->after('cabang_id')->constrained('budget_control')->onDelete('set null');
}
if (!Schema::hasColumn('transactions', 'transaction_date')) {
$table->date('transaction_date')->nullable()->after('budget_control_id');
}
if (!Schema::hasColumn('transactions', 'reference_number')) {
$table->string('reference_number')->nullable()->after('transaction_date');
}
if (!Schema::hasColumn('transactions', 'description')) {
$table->string('description')->nullable()->after('reference_number');
}
if (!Schema::hasColumn('transactions', 'amount')) {
$table->decimal('amount', 15, 2)->default(0)->after('description');
}
if (!Schema::hasColumn('transactions', 'type')) {
$table->enum('type', ['DEBIT', 'CREDIT'])->default('CREDIT')->after('amount');
}
if (!Schema::hasColumn('transactions', 'category')) {
$table->string('category')->default('General')->after('type');
}
if (!Schema::hasColumn('transactions', 'periode_month')) {
$table->string('periode_month')->nullable()->after('category');
}
if (!Schema::hasColumn('transactions', 'periode_year')) {
$table->year('periode_year')->nullable()->after('periode_month');
}
if (!Schema::hasColumn('transactions', 'created_by')) {
// Buat nullable dulu agar baris data lama yang sudah exist tidak error saat migrasi berjalan
$table->foreignId('created_by')->nullable()->after('periode_year')->constrained('admins');
}
if (!Schema::hasColumn('transactions', 'deleted_at')) {
$table->softDeletes();
}
});
}
public function down(): void
{
// Opsional: Hanya drop jika Anda benar-benar sedang di fase development murni
// Schema::dropIfExists('transactions');
}
};
@@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('form_entertainment_presentation', function (Blueprint $table) {
$table->string('jabatan')->nullable()->after('status');
$table->string('nama_perusahaan')->nullable()->after('jabatan');
$table->string('jenis_usaha')->nullable()->after('nama_perusahaan');
});
}
public function down(): void
{
Schema::table('form_entertainment_presentation', function (Blueprint $table) {
$table->dropColumn(['jabatan', 'nama_perusahaan', 'jenis_usaha']);
});
}
};
@@ -0,0 +1,43 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
protected $tables = [
'form_up_country',
'form_vehicle_running_cost',
'form_entertainment_presentation',
'form_meeting_seminar',
'form_others'
];
public function up(): void
{
foreach ($this->tables as $table) {
if (Schema::hasTable($table) && !Schema::hasColumn($table, 'rejected_by')) {
Schema::table($table, function (Blueprint $table) {
// Menambahkan kolom rejected_by sebagai foreign key ke tabel admins
$table->unsignedBigInteger('rejected_by')->nullable()->after('status');
$table->timestamp('rejected_at')->nullable()->after('rejected_by');
$table->foreign('rejected_by')->references('id')->on('admins')->onDelete('set null');
});
}
}
}
public function down(): void
{
foreach ($this->tables as $table) {
if (Schema::hasColumn($table, 'rejected_by')) {
Schema::table($table, function (Blueprint $table) {
$table->dropForeign([$table . '_rejected_by_foreign']);
$table->dropColumn(['rejected_by', 'rejected_at']);
});
}
}
}
};
@@ -0,0 +1,43 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
protected $tables = [
'form_up_country',
'form_vehicle_running_cost',
'form_entertainment_presentation',
'form_meeting_seminar'
];
public function up(): void
{
foreach ($this->tables as $table) {
if (Schema::hasTable($table) && !Schema::hasColumn($table, 'kategori_id')) {
Schema::table($table, function (Blueprint $bp) {
// Kita panggil nama tabel asli Anda 'kategori' di dalam fungsi constrained()
$bp->foreignId('kategori_id')
->nullable()
->after('user_id')
->constrained('kategori') // <-- PERBAIKAN: Menembak tabel 'kategori'
->onDelete('set null');
});
}
}
}
public function down(): void
{
foreach ($this->tables as $table) {
if (Schema::hasColumn($table, 'kategori_id')) {
Schema::table($table, function (Blueprint $bp) use ($table) {
$bp->dropForeign($table . '_kategori_id_foreign');
$bp->dropColumn('kategori_id');
});
}
}
}
};
+258
View File
@@ -0,0 +1,258 @@
SSUUMMMMAARRYY OOFF LLEESSSS CCOOMMMMAANNDDSS
Commands marked with * may be preceded by a number, _N.
Notes in parentheses indicate the behavior if _N is given.
A key preceded by a caret indicates the Ctrl key; thus ^K is ctrl-K.
h H Display this help.
q :q Q :Q ZZ Exit.
---------------------------------------------------------------------------
MMOOVVIINNGG
e ^E j ^N CR * Forward one line (or _N lines).
y ^Y k ^K ^P * Backward one line (or _N lines).
f ^F ^V SPACE * Forward one window (or _N lines).
b ^B ESC-v * Backward one window (or _N lines).
z * Forward one window (and set window to _N).
w * Backward one window (and set window to _N).
ESC-SPACE * Forward one window, but don't stop at end-of-file.
d ^D * Forward one half-window (and set half-window to _N).
u ^U * Backward one half-window (and set half-window to _N).
ESC-) RightArrow * Right one half screen width (or _N positions).
ESC-( LeftArrow * Left one half screen width (or _N positions).
ESC-} ^RightArrow Right to last column displayed.
ESC-{ ^LeftArrow Left to first column.
F Forward forever; like "tail -f".
ESC-F Like F but stop when search pattern is found.
r ^R ^L Repaint screen.
R Repaint screen, discarding buffered input.
---------------------------------------------------
Default "window" is the screen height.
Default "half-window" is half of the screen height.
---------------------------------------------------------------------------
SSEEAARRCCHHIINNGG
/_p_a_t_t_e_r_n * Search forward for (_N-th) matching line.
?_p_a_t_t_e_r_n * Search backward for (_N-th) matching line.
n * Repeat previous search (for _N-th occurrence).
N * Repeat previous search in reverse direction.
ESC-n * Repeat previous search, spanning files.
ESC-N * Repeat previous search, reverse dir. & spanning files.
ESC-u Undo (toggle) search highlighting.
ESC-U Clear search highlighting.
&_p_a_t_t_e_r_n * Display only matching lines.
---------------------------------------------------
A search pattern may begin with one or more of:
^N or ! Search for NON-matching lines.
^E or * Search multiple files (pass thru END OF FILE).
^F or @ Start search at FIRST file (for /) or last file (for ?).
^K Highlight matches, but don't move (KEEP position).
^R Don't use REGULAR EXPRESSIONS.
^W WRAP search if no match found.
---------------------------------------------------------------------------
JJUUMMPPIINNGG
g < ESC-< * Go to first line in file (or line _N).
G > ESC-> * Go to last line in file (or line _N).
p % * Go to beginning of file (or _N percent into file).
t * Go to the (_N-th) next tag.
T * Go to the (_N-th) previous tag.
{ ( [ * Find close bracket } ) ].
} ) ] * Find open bracket { ( [.
ESC-^F _<_c_1_> _<_c_2_> * Find close bracket _<_c_2_>.
ESC-^B _<_c_1_> _<_c_2_> * Find open bracket _<_c_1_>.
---------------------------------------------------
Each "find close bracket" command goes forward to the close bracket
matching the (_N-th) open bracket in the top line.
Each "find open bracket" command goes backward to the open bracket
matching the (_N-th) close bracket in the bottom line.
m_<_l_e_t_t_e_r_> Mark the current top line with <letter>.
M_<_l_e_t_t_e_r_> Mark the current bottom line with <letter>.
'_<_l_e_t_t_e_r_> Go to a previously marked position.
'' Go to the previous position.
^X^X Same as '.
ESC-M_<_l_e_t_t_e_r_> Clear a mark.
---------------------------------------------------
A mark is any upper-case or lower-case letter.
Certain marks are predefined:
^ means beginning of the file
$ means end of the file
---------------------------------------------------------------------------
CCHHAANNGGIINNGG FFIILLEESS
:e [_f_i_l_e] Examine a new file.
^X^V Same as :e.
:n * Examine the (_N-th) next file from the command line.
:p * Examine the (_N-th) previous file from the command line.
:x * Examine the first (or _N-th) file from the command line.
:d Delete the current file from the command line list.
= ^G :f Print current file name.
---------------------------------------------------------------------------
MMIISSCCEELLLLAANNEEOOUUSS CCOOMMMMAANNDDSS
-_<_f_l_a_g_> Toggle a command line option [see OPTIONS below].
--_<_n_a_m_e_> Toggle a command line option, by name.
__<_f_l_a_g_> Display the setting of a command line option.
___<_n_a_m_e_> Display the setting of an option, by name.
+_c_m_d Execute the less cmd each time a new file is examined.
!_c_o_m_m_a_n_d Execute the shell command with $SHELL.
|XX_c_o_m_m_a_n_d Pipe file between current pos & mark XX to shell command.
s _f_i_l_e Save input to a file.
v Edit the current file with $VISUAL or $EDITOR.
V Print version number of "less".
---------------------------------------------------------------------------
OOPPTTIIOONNSS
Most options may be changed either on the command line,
or from within less by using the - or -- command.
Options may be given in one of two forms: either a single
character preceded by a -, or a name preceded by --.
-? ........ --help
Display help (from command line).
-a ........ --search-skip-screen
Search skips current screen.
-A ........ --SEARCH-SKIP-SCREEN
Search starts just after target line.
-b [_N] .... --buffers=[_N]
Number of buffers.
-B ........ --auto-buffers
Don't automatically allocate buffers for pipes.
-c ........ --clear-screen
Repaint by clearing rather than scrolling.
-d ........ --dumb
Dumb terminal.
-D xx_c_o_l_o_r . --color=xx_c_o_l_o_r
Set screen colors.
-e -E .... --quit-at-eof --QUIT-AT-EOF
Quit at end of file.
-f ........ --force
Force open non-regular files.
-F ........ --quit-if-one-screen
Quit if entire file fits on first screen.
-g ........ --hilite-search
Highlight only last match for searches.
-G ........ --HILITE-SEARCH
Don't highlight any matches for searches.
-h [_N] .... --max-back-scroll=[_N]
Backward scroll limit.
-i ........ --ignore-case
Ignore case in searches that do not contain uppercase.
-I ........ --IGNORE-CASE
Ignore case in all searches.
-j [_N] .... --jump-target=[_N]
Screen position of target lines.
-J ........ --status-column
Display a status column at left edge of screen.
-k [_f_i_l_e] . --lesskey-file=[_f_i_l_e]
Use a lesskey file.
-K ........ --quit-on-intr
Exit less in response to ctrl-C.
-L ........ --no-lessopen
Ignore the LESSOPEN environment variable.
-m -M .... --long-prompt --LONG-PROMPT
Set prompt style.
-n -N .... --line-numbers --LINE-NUMBERS
Don't use line numbers.
-o [_f_i_l_e] . --log-file=[_f_i_l_e]
Copy to log file (standard input only).
-O [_f_i_l_e] . --LOG-FILE=[_f_i_l_e]
Copy to log file (unconditionally overwrite).
-p [_p_a_t_t_e_r_n] --pattern=[_p_a_t_t_e_r_n]
Start at pattern (from command line).
-P [_p_r_o_m_p_t] --prompt=[_p_r_o_m_p_t]
Define new prompt.
-q -Q .... --quiet --QUIET --silent --SILENT
Quiet the terminal bell.
-r -R .... --raw-control-chars --RAW-CONTROL-CHARS
Output "raw" control characters.
-s ........ --squeeze-blank-lines
Squeeze multiple blank lines.
-S ........ --chop-long-lines
Chop (truncate) long lines rather than wrapping.
-t [_t_a_g] .. --tag=[_t_a_g]
Find a tag.
-T [_t_a_g_s_f_i_l_e] --tag-file=[_t_a_g_s_f_i_l_e]
Use an alternate tags file.
-u -U .... --underline-special --UNDERLINE-SPECIAL
Change handling of backspaces.
-V ........ --version
Display the version number of "less".
-w ........ --hilite-unread
Highlight first new line after forward-screen.
-W ........ --HILITE-UNREAD
Highlight first new line after any forward movement.
-x [_N[,...]] --tabs=[_N[,...]]
Set tab stops.
-X ........ --no-init
Don't use termcap init/deinit strings.
-y [_N] .... --max-forw-scroll=[_N]
Forward scroll limit.
-z [_N] .... --window=[_N]
Set size of window.
-" [_c[_c]] . --quotes=[_c[_c]]
Set shell quote characters.
-~ ........ --tilde
Don't display tildes after end of file.
-# [_N] .... --shift=[_N]
Set horizontal scroll amount (0 = one half screen width).
--file-size
Automatically determine the size of the input file.
--follow-name
The F command changes files if the input file is renamed.
--incsearch
Search file as each pattern character is typed in.
--line-num-width=N
Set the width of the -N line number field to N characters.
--mouse
Enable mouse input.
--no-keypad
Don't send termcap keypad init/deinit strings.
--no-histdups
Remove duplicates from command history.
--rscroll=C
Set the character used to mark truncated lines.
--save-marks
Retain marks across invocations of less.
--status-col-width=N
Set the width of the -J status column to N characters.
--use-backslash
Subsequent options use backslash as escape char.
--use-color
Enables colored text.
--wheel-lines=N
Each click of the mouse wheel moves N lines.
---------------------------------------------------------------------------
LLIINNEE EEDDIITTIINNGG
These keys can be used to edit text being entered
on the "command line" at the bottom of the screen.
RightArrow ..................... ESC-l ... Move cursor right one character.
LeftArrow ...................... ESC-h ... Move cursor left one character.
ctrl-RightArrow ESC-RightArrow ESC-w ... Move cursor right one word.
ctrl-LeftArrow ESC-LeftArrow ESC-b ... Move cursor left one word.
HOME ........................... ESC-0 ... Move cursor to start of line.
END ............................ ESC-$ ... Move cursor to end of line.
BACKSPACE ................................ Delete char to left of cursor.
DELETE ......................... ESC-x ... Delete char under cursor.
ctrl-BACKSPACE ESC-BACKSPACE ........... Delete word to left of cursor.
ctrl-DELETE .... ESC-DELETE .... ESC-X ... Delete word under cursor.
ctrl-U ......... ESC (MS-DOS only) ....... Delete entire line.
UpArrow ........................ ESC-k ... Retrieve previous command line.
DownArrow ...................... ESC-j ... Retrieve next command line.
TAB ...................................... Complete filename & cycle.
SHIFT-TAB ...................... ESC-TAB Complete filename & reverse cycle.
ctrl-L ................................... Complete filename, list all.
@@ -0,0 +1,258 @@
SSUUMMMMAARRYY OOFF LLEESSSS CCOOMMMMAANNDDSS
Commands marked with * may be preceded by a number, _N.
Notes in parentheses indicate the behavior if _N is given.
A key preceded by a caret indicates the Ctrl key; thus ^K is ctrl-K.
h H Display this help.
q :q Q :Q ZZ Exit.
---------------------------------------------------------------------------
MMOOVVIINNGG
e ^E j ^N CR * Forward one line (or _N lines).
y ^Y k ^K ^P * Backward one line (or _N lines).
f ^F ^V SPACE * Forward one window (or _N lines).
b ^B ESC-v * Backward one window (or _N lines).
z * Forward one window (and set window to _N).
w * Backward one window (and set window to _N).
ESC-SPACE * Forward one window, but don't stop at end-of-file.
d ^D * Forward one half-window (and set half-window to _N).
u ^U * Backward one half-window (and set half-window to _N).
ESC-) RightArrow * Right one half screen width (or _N positions).
ESC-( LeftArrow * Left one half screen width (or _N positions).
ESC-} ^RightArrow Right to last column displayed.
ESC-{ ^LeftArrow Left to first column.
F Forward forever; like "tail -f".
ESC-F Like F but stop when search pattern is found.
r ^R ^L Repaint screen.
R Repaint screen, discarding buffered input.
---------------------------------------------------
Default "window" is the screen height.
Default "half-window" is half of the screen height.
---------------------------------------------------------------------------
SSEEAARRCCHHIINNGG
/_p_a_t_t_e_r_n * Search forward for (_N-th) matching line.
?_p_a_t_t_e_r_n * Search backward for (_N-th) matching line.
n * Repeat previous search (for _N-th occurrence).
N * Repeat previous search in reverse direction.
ESC-n * Repeat previous search, spanning files.
ESC-N * Repeat previous search, reverse dir. & spanning files.
ESC-u Undo (toggle) search highlighting.
ESC-U Clear search highlighting.
&_p_a_t_t_e_r_n * Display only matching lines.
---------------------------------------------------
A search pattern may begin with one or more of:
^N or ! Search for NON-matching lines.
^E or * Search multiple files (pass thru END OF FILE).
^F or @ Start search at FIRST file (for /) or last file (for ?).
^K Highlight matches, but don't move (KEEP position).
^R Don't use REGULAR EXPRESSIONS.
^W WRAP search if no match found.
---------------------------------------------------------------------------
JJUUMMPPIINNGG
g < ESC-< * Go to first line in file (or line _N).
G > ESC-> * Go to last line in file (or line _N).
p % * Go to beginning of file (or _N percent into file).
t * Go to the (_N-th) next tag.
T * Go to the (_N-th) previous tag.
{ ( [ * Find close bracket } ) ].
} ) ] * Find open bracket { ( [.
ESC-^F _<_c_1_> _<_c_2_> * Find close bracket _<_c_2_>.
ESC-^B _<_c_1_> _<_c_2_> * Find open bracket _<_c_1_>.
---------------------------------------------------
Each "find close bracket" command goes forward to the close bracket
matching the (_N-th) open bracket in the top line.
Each "find open bracket" command goes backward to the open bracket
matching the (_N-th) close bracket in the bottom line.
m_<_l_e_t_t_e_r_> Mark the current top line with <letter>.
M_<_l_e_t_t_e_r_> Mark the current bottom line with <letter>.
'_<_l_e_t_t_e_r_> Go to a previously marked position.
'' Go to the previous position.
^X^X Same as '.
ESC-M_<_l_e_t_t_e_r_> Clear a mark.
---------------------------------------------------
A mark is any upper-case or lower-case letter.
Certain marks are predefined:
^ means beginning of the file
$ means end of the file
---------------------------------------------------------------------------
CCHHAANNGGIINNGG FFIILLEESS
:e [_f_i_l_e] Examine a new file.
^X^V Same as :e.
:n * Examine the (_N-th) next file from the command line.
:p * Examine the (_N-th) previous file from the command line.
:x * Examine the first (or _N-th) file from the command line.
:d Delete the current file from the command line list.
= ^G :f Print current file name.
---------------------------------------------------------------------------
MMIISSCCEELLLLAANNEEOOUUSS CCOOMMMMAANNDDSS
-_<_f_l_a_g_> Toggle a command line option [see OPTIONS below].
--_<_n_a_m_e_> Toggle a command line option, by name.
__<_f_l_a_g_> Display the setting of a command line option.
___<_n_a_m_e_> Display the setting of an option, by name.
+_c_m_d Execute the less cmd each time a new file is examined.
!_c_o_m_m_a_n_d Execute the shell command with $SHELL.
|XX_c_o_m_m_a_n_d Pipe file between current pos & mark XX to shell command.
s _f_i_l_e Save input to a file.
v Edit the current file with $VISUAL or $EDITOR.
V Print version number of "less".
---------------------------------------------------------------------------
OOPPTTIIOONNSS
Most options may be changed either on the command line,
or from within less by using the - or -- command.
Options may be given in one of two forms: either a single
character preceded by a -, or a name preceded by --.
-? ........ --help
Display help (from command line).
-a ........ --search-skip-screen
Search skips current screen.
-A ........ --SEARCH-SKIP-SCREEN
Search starts just after target line.
-b [_N] .... --buffers=[_N]
Number of buffers.
-B ........ --auto-buffers
Don't automatically allocate buffers for pipes.
-c ........ --clear-screen
Repaint by clearing rather than scrolling.
-d ........ --dumb
Dumb terminal.
-D xx_c_o_l_o_r . --color=xx_c_o_l_o_r
Set screen colors.
-e -E .... --quit-at-eof --QUIT-AT-EOF
Quit at end of file.
-f ........ --force
Force open non-regular files.
-F ........ --quit-if-one-screen
Quit if entire file fits on first screen.
-g ........ --hilite-search
Highlight only last match for searches.
-G ........ --HILITE-SEARCH
Don't highlight any matches for searches.
-h [_N] .... --max-back-scroll=[_N]
Backward scroll limit.
-i ........ --ignore-case
Ignore case in searches that do not contain uppercase.
-I ........ --IGNORE-CASE
Ignore case in all searches.
-j [_N] .... --jump-target=[_N]
Screen position of target lines.
-J ........ --status-column
Display a status column at left edge of screen.
-k [_f_i_l_e] . --lesskey-file=[_f_i_l_e]
Use a lesskey file.
-K ........ --quit-on-intr
Exit less in response to ctrl-C.
-L ........ --no-lessopen
Ignore the LESSOPEN environment variable.
-m -M .... --long-prompt --LONG-PROMPT
Set prompt style.
-n -N .... --line-numbers --LINE-NUMBERS
Don't use line numbers.
-o [_f_i_l_e] . --log-file=[_f_i_l_e]
Copy to log file (standard input only).
-O [_f_i_l_e] . --LOG-FILE=[_f_i_l_e]
Copy to log file (unconditionally overwrite).
-p [_p_a_t_t_e_r_n] --pattern=[_p_a_t_t_e_r_n]
Start at pattern (from command line).
-P [_p_r_o_m_p_t] --prompt=[_p_r_o_m_p_t]
Define new prompt.
-q -Q .... --quiet --QUIET --silent --SILENT
Quiet the terminal bell.
-r -R .... --raw-control-chars --RAW-CONTROL-CHARS
Output "raw" control characters.
-s ........ --squeeze-blank-lines
Squeeze multiple blank lines.
-S ........ --chop-long-lines
Chop (truncate) long lines rather than wrapping.
-t [_t_a_g] .. --tag=[_t_a_g]
Find a tag.
-T [_t_a_g_s_f_i_l_e] --tag-file=[_t_a_g_s_f_i_l_e]
Use an alternate tags file.
-u -U .... --underline-special --UNDERLINE-SPECIAL
Change handling of backspaces.
-V ........ --version
Display the version number of "less".
-w ........ --hilite-unread
Highlight first new line after forward-screen.
-W ........ --HILITE-UNREAD
Highlight first new line after any forward movement.
-x [_N[,...]] --tabs=[_N[,...]]
Set tab stops.
-X ........ --no-init
Don't use termcap init/deinit strings.
-y [_N] .... --max-forw-scroll=[_N]
Forward scroll limit.
-z [_N] .... --window=[_N]
Set size of window.
-" [_c[_c]] . --quotes=[_c[_c]]
Set shell quote characters.
-~ ........ --tilde
Don't display tildes after end of file.
-# [_N] .... --shift=[_N]
Set horizontal scroll amount (0 = one half screen width).
--file-size
Automatically determine the size of the input file.
--follow-name
The F command changes files if the input file is renamed.
--incsearch
Search file as each pattern character is typed in.
--line-num-width=N
Set the width of the -N line number field to N characters.
--mouse
Enable mouse input.
--no-keypad
Don't send termcap keypad init/deinit strings.
--no-histdups
Remove duplicates from command history.
--rscroll=C
Set the character used to mark truncated lines.
--save-marks
Retain marks across invocations of less.
--status-col-width=N
Set the width of the -J status column to N characters.
--use-backslash
Subsequent options use backslash as escape char.
--use-color
Enables colored text.
--wheel-lines=N
Each click of the mouse wheel moves N lines.
---------------------------------------------------------------------------
LLIINNEE EEDDIITTIINNGG
These keys can be used to edit text being entered
on the "command line" at the bottom of the screen.
RightArrow ..................... ESC-l ... Move cursor right one character.
LeftArrow ...................... ESC-h ... Move cursor left one character.
ctrl-RightArrow ESC-RightArrow ESC-w ... Move cursor right one word.
ctrl-LeftArrow ESC-LeftArrow ESC-b ... Move cursor left one word.
HOME ........................... ESC-0 ... Move cursor to start of line.
END ............................ ESC-$ ... Move cursor to end of line.
BACKSPACE ................................ Delete char to left of cursor.
DELETE ......................... ESC-x ... Delete char under cursor.
ctrl-BACKSPACE ESC-BACKSPACE ........... Delete word to left of cursor.
ctrl-DELETE .... ESC-DELETE .... ESC-X ... Delete word under cursor.
ctrl-U ......... ESC (MS-DOS only) ....... Delete entire line.
UpArrow ........................ ESC-k ... Retrieve previous command line.
DownArrow ...................... ESC-j ... Retrieve next command line.
TAB ...................................... Complete filename & cycle.
SHIFT-TAB ...................... ESC-TAB Complete filename & reverse cycle.
ctrl-L ................................... Complete filename, list all.
@@ -0,0 +1,79 @@
= Illuminate\Database\Query\Builder {#8371
+connection: Illuminate\Database\MySqlConnection {#8375},
+grammar: Illuminate\Database\Query\Grammars\MySqlGrammar {#8374},
+processor: Illuminate\Database\Query\Processors\MySqlProcessor {#8373},
+bindings: [
"select" => [],
"from" => [],
"join" => [],
"where" => [],
"groupBy" => [],
"having" => [],
"order" => [],
"union" => [],
"unionOrder" => [],
],
+aggregate: null,
+columns: null,
+distinct: false,
+from: "form_up_country",
+indexHint: null,
+joins: null,
+wheres: [],
+groups: null,
+havings: null,
+orders: null,
+limit: null,
+groupLimit: null,
+offset: null,
+unions: null,
+unionLimit: null,
+unionOffset: null,
+unionOrders: null,
+lock: null,
+beforeQueryCallbacks: [],
+operators: [
"=",
"<",
">",
"<=",
">=",
"<>",
"!=",
"<=>",
"like",
"like binary",
"not like",
"ilike",
"&",
"|",
"^",
"<<",
">>",
"&~",
"is",
"is not",
"rlike",
"not rlike",
"regexp",
"not regexp",
"~",
"~*",
"!~",
"!~*",
"similar to",
"not similar to",
"not ilike",
"~~*",
"!~~*",
],
+bitwiseOperators: [
"&",
"|",
"^",
"<<",
">>",
"&~",
],
+useWritePdo: false,
}
@@ -0,0 +1,258 @@
SSUUMMMMAARRYY OOFF LLEESSSS CCOOMMMMAANNDDSS
Commands marked with * may be preceded by a number, _N.
Notes in parentheses indicate the behavior if _N is given.
A key preceded by a caret indicates the Ctrl key; thus ^K is ctrl-K.
h H Display this help.
q :q Q :Q ZZ Exit.
---------------------------------------------------------------------------
MMOOVVIINNGG
e ^E j ^N CR * Forward one line (or _N lines).
y ^Y k ^K ^P * Backward one line (or _N lines).
f ^F ^V SPACE * Forward one window (or _N lines).
b ^B ESC-v * Backward one window (or _N lines).
z * Forward one window (and set window to _N).
w * Backward one window (and set window to _N).
ESC-SPACE * Forward one window, but don't stop at end-of-file.
d ^D * Forward one half-window (and set half-window to _N).
u ^U * Backward one half-window (and set half-window to _N).
ESC-) RightArrow * Right one half screen width (or _N positions).
ESC-( LeftArrow * Left one half screen width (or _N positions).
ESC-} ^RightArrow Right to last column displayed.
ESC-{ ^LeftArrow Left to first column.
F Forward forever; like "tail -f".
ESC-F Like F but stop when search pattern is found.
r ^R ^L Repaint screen.
R Repaint screen, discarding buffered input.
---------------------------------------------------
Default "window" is the screen height.
Default "half-window" is half of the screen height.
---------------------------------------------------------------------------
SSEEAARRCCHHIINNGG
/_p_a_t_t_e_r_n * Search forward for (_N-th) matching line.
?_p_a_t_t_e_r_n * Search backward for (_N-th) matching line.
n * Repeat previous search (for _N-th occurrence).
N * Repeat previous search in reverse direction.
ESC-n * Repeat previous search, spanning files.
ESC-N * Repeat previous search, reverse dir. & spanning files.
ESC-u Undo (toggle) search highlighting.
ESC-U Clear search highlighting.
&_p_a_t_t_e_r_n * Display only matching lines.
---------------------------------------------------
A search pattern may begin with one or more of:
^N or ! Search for NON-matching lines.
^E or * Search multiple files (pass thru END OF FILE).
^F or @ Start search at FIRST file (for /) or last file (for ?).
^K Highlight matches, but don't move (KEEP position).
^R Don't use REGULAR EXPRESSIONS.
^W WRAP search if no match found.
---------------------------------------------------------------------------
JJUUMMPPIINNGG
g < ESC-< * Go to first line in file (or line _N).
G > ESC-> * Go to last line in file (or line _N).
p % * Go to beginning of file (or _N percent into file).
t * Go to the (_N-th) next tag.
T * Go to the (_N-th) previous tag.
{ ( [ * Find close bracket } ) ].
} ) ] * Find open bracket { ( [.
ESC-^F _<_c_1_> _<_c_2_> * Find close bracket _<_c_2_>.
ESC-^B _<_c_1_> _<_c_2_> * Find open bracket _<_c_1_>.
---------------------------------------------------
Each "find close bracket" command goes forward to the close bracket
matching the (_N-th) open bracket in the top line.
Each "find open bracket" command goes backward to the open bracket
matching the (_N-th) close bracket in the bottom line.
m_<_l_e_t_t_e_r_> Mark the current top line with <letter>.
M_<_l_e_t_t_e_r_> Mark the current bottom line with <letter>.
'_<_l_e_t_t_e_r_> Go to a previously marked position.
'' Go to the previous position.
^X^X Same as '.
ESC-M_<_l_e_t_t_e_r_> Clear a mark.
---------------------------------------------------
A mark is any upper-case or lower-case letter.
Certain marks are predefined:
^ means beginning of the file
$ means end of the file
---------------------------------------------------------------------------
CCHHAANNGGIINNGG FFIILLEESS
:e [_f_i_l_e] Examine a new file.
^X^V Same as :e.
:n * Examine the (_N-th) next file from the command line.
:p * Examine the (_N-th) previous file from the command line.
:x * Examine the first (or _N-th) file from the command line.
:d Delete the current file from the command line list.
= ^G :f Print current file name.
---------------------------------------------------------------------------
MMIISSCCEELLLLAANNEEOOUUSS CCOOMMMMAANNDDSS
-_<_f_l_a_g_> Toggle a command line option [see OPTIONS below].
--_<_n_a_m_e_> Toggle a command line option, by name.
__<_f_l_a_g_> Display the setting of a command line option.
___<_n_a_m_e_> Display the setting of an option, by name.
+_c_m_d Execute the less cmd each time a new file is examined.
!_c_o_m_m_a_n_d Execute the shell command with $SHELL.
|XX_c_o_m_m_a_n_d Pipe file between current pos & mark XX to shell command.
s _f_i_l_e Save input to a file.
v Edit the current file with $VISUAL or $EDITOR.
V Print version number of "less".
---------------------------------------------------------------------------
OOPPTTIIOONNSS
Most options may be changed either on the command line,
or from within less by using the - or -- command.
Options may be given in one of two forms: either a single
character preceded by a -, or a name preceded by --.
-? ........ --help
Display help (from command line).
-a ........ --search-skip-screen
Search skips current screen.
-A ........ --SEARCH-SKIP-SCREEN
Search starts just after target line.
-b [_N] .... --buffers=[_N]
Number of buffers.
-B ........ --auto-buffers
Don't automatically allocate buffers for pipes.
-c ........ --clear-screen
Repaint by clearing rather than scrolling.
-d ........ --dumb
Dumb terminal.
-D xx_c_o_l_o_r . --color=xx_c_o_l_o_r
Set screen colors.
-e -E .... --quit-at-eof --QUIT-AT-EOF
Quit at end of file.
-f ........ --force
Force open non-regular files.
-F ........ --quit-if-one-screen
Quit if entire file fits on first screen.
-g ........ --hilite-search
Highlight only last match for searches.
-G ........ --HILITE-SEARCH
Don't highlight any matches for searches.
-h [_N] .... --max-back-scroll=[_N]
Backward scroll limit.
-i ........ --ignore-case
Ignore case in searches that do not contain uppercase.
-I ........ --IGNORE-CASE
Ignore case in all searches.
-j [_N] .... --jump-target=[_N]
Screen position of target lines.
-J ........ --status-column
Display a status column at left edge of screen.
-k [_f_i_l_e] . --lesskey-file=[_f_i_l_e]
Use a lesskey file.
-K ........ --quit-on-intr
Exit less in response to ctrl-C.
-L ........ --no-lessopen
Ignore the LESSOPEN environment variable.
-m -M .... --long-prompt --LONG-PROMPT
Set prompt style.
-n -N .... --line-numbers --LINE-NUMBERS
Don't use line numbers.
-o [_f_i_l_e] . --log-file=[_f_i_l_e]
Copy to log file (standard input only).
-O [_f_i_l_e] . --LOG-FILE=[_f_i_l_e]
Copy to log file (unconditionally overwrite).
-p [_p_a_t_t_e_r_n] --pattern=[_p_a_t_t_e_r_n]
Start at pattern (from command line).
-P [_p_r_o_m_p_t] --prompt=[_p_r_o_m_p_t]
Define new prompt.
-q -Q .... --quiet --QUIET --silent --SILENT
Quiet the terminal bell.
-r -R .... --raw-control-chars --RAW-CONTROL-CHARS
Output "raw" control characters.
-s ........ --squeeze-blank-lines
Squeeze multiple blank lines.
-S ........ --chop-long-lines
Chop (truncate) long lines rather than wrapping.
-t [_t_a_g] .. --tag=[_t_a_g]
Find a tag.
-T [_t_a_g_s_f_i_l_e] --tag-file=[_t_a_g_s_f_i_l_e]
Use an alternate tags file.
-u -U .... --underline-special --UNDERLINE-SPECIAL
Change handling of backspaces.
-V ........ --version
Display the version number of "less".
-w ........ --hilite-unread
Highlight first new line after forward-screen.
-W ........ --HILITE-UNREAD
Highlight first new line after any forward movement.
-x [_N[,...]] --tabs=[_N[,...]]
Set tab stops.
-X ........ --no-init
Don't use termcap init/deinit strings.
-y [_N] .... --max-forw-scroll=[_N]
Forward scroll limit.
-z [_N] .... --window=[_N]
Set size of window.
-" [_c[_c]] . --quotes=[_c[_c]]
Set shell quote characters.
-~ ........ --tilde
Don't display tildes after end of file.
-# [_N] .... --shift=[_N]
Set horizontal scroll amount (0 = one half screen width).
--file-size
Automatically determine the size of the input file.
--follow-name
The F command changes files if the input file is renamed.
--incsearch
Search file as each pattern character is typed in.
--line-num-width=N
Set the width of the -N line number field to N characters.
--mouse
Enable mouse input.
--no-keypad
Don't send termcap keypad init/deinit strings.
--no-histdups
Remove duplicates from command history.
--rscroll=C
Set the character used to mark truncated lines.
--save-marks
Retain marks across invocations of less.
--status-col-width=N
Set the width of the -J status column to N characters.
--use-backslash
Subsequent options use backslash as escape char.
--use-color
Enables colored text.
--wheel-lines=N
Each click of the mouse wheel moves N lines.
---------------------------------------------------------------------------
LLIINNEE EEDDIITTIINNGG
These keys can be used to edit text being entered
on the "command line" at the bottom of the screen.
RightArrow ..................... ESC-l ... Move cursor right one character.
LeftArrow ...................... ESC-h ... Move cursor left one character.
ctrl-RightArrow ESC-RightArrow ESC-w ... Move cursor right one word.
ctrl-LeftArrow ESC-LeftArrow ESC-b ... Move cursor left one word.
HOME ........................... ESC-0 ... Move cursor to start of line.
END ............................ ESC-$ ... Move cursor to end of line.
BACKSPACE ................................ Delete char to left of cursor.
DELETE ......................... ESC-x ... Delete char under cursor.
ctrl-BACKSPACE ESC-BACKSPACE ........... Delete word to left of cursor.
ctrl-DELETE .... ESC-DELETE .... ESC-X ... Delete word under cursor.
ctrl-U ......... ESC (MS-DOS only) ....... Delete entire line.
UpArrow ........................ ESC-k ... Retrieve previous command line.
DownArrow ...................... ESC-j ... Retrieve next command line.
TAB ...................................... Complete filename & cycle.
SHIFT-TAB ...................... ESC-TAB Complete filename & reverse cycle.
ctrl-L ................................... Complete filename, list all.
+2700
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+13
View File
@@ -0,0 +1,13 @@
{
"resources/css/app.css": {
"file": "assets/app-DamXIAY7.css",
"src": "resources/css/app.css",
"isEntry": true
},
"resources/js/app.js": {
"file": "assets/app-BuG9aa18.js",
"name": "app",
"src": "resources/js/app.js",
"isEntry": true
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

@@ -1,23 +1,31 @@
@extends('layouts.app')
@section('title')
{{ $pageInfo['title'] }}
@endsection
@section('title', $pageInfo['title'])
@section('admin-content')
<section class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="row mb-3">
<div class="col-sm-6">
<h1>{{ $pageInfo['title'] }}</h1>
<h1 class="fw-bold text-dark">{{ $pageInfo['title'] }}</h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}">{{ __('Dashboard') }}</a></li>
<li class="breadcrumb-item active"><a href="{{ $pageInfo['path'] }}">{{ $pageInfo['title'] }}</a>
</li>
</ol>
<!-- Tombol Aksi Global (Kanan Atas) -->
<div class="float-sm-right d-flex align-items-center" style="gap: 10px;">
<a href="{{ route('admin.audit-trail.clear-cache') }}"
class="btn btn-outline-info btn-sm shadow-sm"
onclick="return confirm('Apakah Anda yakin ingin mereset cache sistem?')">
<i class="fa fa-sync"></i> Reset Cache
</a>
<form action="{{ route('admin.audit-trail.clear') }}" method="POST" onsubmit="return confirm('PERINGATAN! Semua log audit akan dihapus permanen. Lanjutkan?')">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-outline-danger btn-sm shadow-sm">
<i class="fa fa-trash"></i> Clear All Logs
</button>
</form>
</div>
</div>
</div>
</div>
@@ -25,82 +33,137 @@
<section class="content">
<div class="container-fluid">
<!-- CARD FILTER -->
<div class="card shadow-sm mb-4" style="border-radius: 12px; border: none; border-top: 3px solid #007bff;">
<div class="card-header bg-white py-3">
<h6 class="m-0 font-weight-bold text-primary"><i class="fa fa-filter mr-1"></i> Panel Pencarian & Filter</h6>
</div>
<div class="card-body">
<form action="{{ route('admin.audit-trail.index') }}" method="GET">
<div class="row">
<!-- Filter User -->
<div class="col-md-3 mb-3">
<label class="small font-weight-bold text-muted text-uppercase">Berdasarkan User</label>
<select name="admin_id" class="form-control select2 shadow-none">
<option value="">-- Semua User --</option>
@foreach($admins as $admin)
<option value="{{ $admin->id }}" {{ request('admin_id') == $admin->id ? 'selected' : '' }}>
{{ $admin->name }}
</option>
@endforeach
</select>
</div>
<!-- Filter Region -->
<div class="col-md-3 mb-3">
<label class="small font-weight-bold text-muted text-uppercase">Berdasarkan Region</label>
<select name="region" class="form-control shadow-none">
<option value="">-- Semua Region --</option>
@foreach($regions as $reg)
<option value="{{ $reg->code }}" {{ request('region') == $reg->code ? 'selected' : '' }}>
{{ $reg->name }}
</option>
@endforeach
</select>
</div>
<!-- Filter Rentang Waktu -->
<div class="col-md-4 mb-3">
<label class="small font-weight-bold text-muted text-uppercase">Rentang Waktu</label>
<div class="input-group">
<input type="date" name="start_date" class="form-control shadow-none" value="{{ request('start_date') }}">
<div class="input-group-append">
<span class="input-group-text bg-light border-left-0 border-right-0">s/d</span>
</div>
<input type="date" name="end_date" class="form-control shadow-none" value="{{ request('end_date') }}">
</div>
</div>
<!-- Tombol Eksekusi Filter -->
<div class="col-md-2 mb-3 text-right d-flex align-items-end justify-content-end" style="gap: 5px;">
<button type="submit" class="btn btn-primary btn-block mb-0 shadow-sm">
<i class="fa fa-search"></i> Cari
</button>
<a href="{{ route('admin.audit-trail.index') }}" class="btn btn-light border shadow-sm px-3" title="Reset Filter">
<i class="fa fa-undo"></i>
</a>
</div>
</div>
<hr class="my-3">
<!-- Tombol Export Excel -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-body">
<h4 class="header-title float-left">{{ $pageInfo['title'] }}</h4>
<div class="clearfix"></div>
<div class="data-tables">
<a href="{{ route('admin.audit-trail.export', request()->all()) }}" class="btn btn-success shadow-sm">
<i class="fa fa-file-excel"></i> Export Hasil Pencarian ke Excel
</a>
<span class="text-muted small ml-2 mt-2 d-inline-block">
* Menampilkan <strong>{{ $data->firstItem() }}</strong> sampai <strong>{{ $data->lastItem() }}</strong> dari <strong>{{ $data->total() }}</strong> log.
</span>
</div>
</div>
</form>
</div>
</div>
<!-- TABEL DATA -->
<div class="card shadow-sm" style="border-radius: 12px; border: none;">
<div class="card-body p-0">
@include('backend.layouts.partials.messages')
<div class="table-responsive">
<table id="dataTable" class="text-center">
<thead class="bg-light text-capitalize">
<table id="dataTable" class="table table-hover mb-0">
<thead class="bg-light">
<tr>
<th width="5%">{{ __('User') }}</th>
<th width="5%">{{ __('Ip Address') }}</th>
<th width="5%">{{ __('Action') }}</th>
<th width="15%">{{ __('Activity') }}</th>
<th width="5%">Browser</th>
<th width="15%">User Agent</th>
<th width="5%">Tanggal</th>
<th width="5%">Waktu</th>
<th class="border-top-0" width="10%">User</th>
<th class="border-top-0" width="10%">IP Address</th>
<th class="border-top-0" width="10%">Aksi</th>
<th class="border-top-0" width="35%">Aktivitas</th>
<th class="border-top-0" width="10%">Browser</th>
<th class="border-top-0" width="15%">Waktu</th>
</tr>
</thead>
<tbody>
@foreach ($data as $item)
<tr>
<td>
{{ $item->user ? $item->user->username : preg_replace('/\).*$/', '', explode('(', $item->activity)[1]) }}
<td class="align-middle">
<span class="text-primary font-weight-bold">{{ $item->user->username ?? 'System' }}</span>
</td>
<td>{{ $item->ip_address }}</td>
<td>
<td class="align-middle text-muted small">{{ $item->ip_address }}</td>
<td class="align-middle text-center">
@php
$color = 'info';
if ($item->action == 'Create') {
$color = 'success';
} elseif ($item->action == 'Update') {
$color = 'warning';
} elseif ($item->action == 'Delete' || $item->action == 'Error') {
$color = 'danger';
}
$badge = match($item->action) {
'Insert', 'Create' => 'success',
'Update' => 'warning',
'Delete', 'Reject', 'Error' => 'danger',
'Approve' => 'primary',
default => 'info'
};
@endphp
<div class="badge badge-{{ $color }} text-white">
{{ $item->action }}
<span class="badge badge-{{ $badge }} px-2 py-1 text-white shadow-xs">{{ $item->action }}</span>
</td>
<td class="align-middle text-left"><small class="text-dark">{{ $item->activity }}</small></td>
<td class="align-middle"><small class="text-muted">{{ $item->browser }}</small></td>
<td class="align-middle">
<div style="line-height: 1.1;">
<small class="d-block font-weight-bold">{{ $item->created_at->format('d M Y') }}</small>
<small class="text-muted" style="font-size: 10px;">{{ $item->created_at->format('H:i') }} ({{ $item->created_at->diffForHumans() }})</small>
</div>
</td>
<td>{{ $item->activity }}</td>
<td>{{ $item->browser }}</td>
<td>{{ $item->user_agent }}</td>
<td>{{ $item->created_at }}</td>
<td>{{ $item->created_at->diffForHumans() }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
<!-- FOOTER CARD UNTUK PAGINATION -->
<div class="card-footer bg-white py-3 border-top-0">
<div class="d-flex justify-content-center">
{{ $data->links() }}
</div>
</div>
</div>
</div>
</section>
@endsection
@section('scripts')
<script>
$(document).ready(function () {
if ($('#dataTable').length) {
$('#dataTable').DataTable({
responsive: false,
dom: 'Bfrtip',
buttons: [
'excel', 'pdf', 'print'
],
order: [[6, 'desc']]
});
}
});
</script>
@endsection
@@ -1,138 +1,273 @@
@extends('layouts.app')
@section('title')
Dashboard
@endsection
@section('title', 'Active Budgets Control')
@section('admin-content')
<section class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1>Active Budgets</h1>
<h1 class="fw-bold text-dark">Active Budgets Control</h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="{{ route('dashboard.index') }}">Dashboard</a></li>
</li>
<li class="breadcrumb-item active">Budget Control</li>
</ol>
</div>
</div>
</div>
</section>
<section class="content">
<div class="container-fluid">
<div class="card card-primary card-outline">
<div class="card-body">
<h4 class="header-title float-left">Current Active Budget</h4>
<p class="float-right mb-2">
@if (auth()->user()->can('budget_control.create'))
<a class="btn btn-primary text-white" href="{{ route('budget_control.create') }}">
Add New Budget
</a>
{{-- --- BAGIAN 1: PANEL FILTER DINAMIS --- --}}
<div class="card shadow-sm mb-4" style="border-radius: 15px; border-top: 3px solid #007bff;">
<div class="card-body py-3">
<form action="{{ route('budget_control') }}" method="GET" id="filter-form">
<div class="row align-items-end">
<div class="col-md-3 mb-2 mb-md-0">
<label class="small font-weight-bold text-muted text-uppercase">Region</label>
<select name="region_id" id="filter_region" class="form-control form-control-sm select2-filter">
{{-- Munculkan 'Semua Region' HANYA untuk role pusat --}}
@if(auth()->user()->hasAnyRole(['Superadmin', 'Super Admin', 'Accounting', 'accounting']))
<option value="">Semua Region</option>
@else
{{-- Untuk Admin Region, langsung arahkan ke pilihannya --}}
<option value="" disabled>Pilih Region Anda</option>
@endif
</p>
<div class="clearfix"></div>
@foreach($regions as $reg)
<option value="{{ $reg->id }}" {{ request('region_id', (count($regions) == 1 ? $regions->first()->id : '')) == $reg->id ? 'selected' : '' }}>
{{ $reg->name }}
</option>
@endforeach
</select>
</div>
<div class="col-md-3 mb-2 mb-md-0">
<label class="small font-weight-bold text-muted text-uppercase">Cabang</label>
<select name="cabang_id" id="filter_cabang" class="form-control form-control-sm select2-filter">
<option value="">Semua Cabang</option>
@foreach($cabangOptions as $cab)
<option value="{{ $cab->id }}" {{ request('cabang_id') == $cab->id ? 'selected' : '' }}>{{ $cab->name }}</option>
@endforeach
</select>
</div>
<div class="col-md-2 mb-2 mb-md-0">
<label class="small font-weight-bold text-muted text-uppercase">Bulan</label>
<select name="month" class="form-control form-control-sm select2-filter">
<option value="">Semua Bulan</option>
@foreach(['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] as $m)
<option value="{{ $m }}" {{ request('month') == $m ? 'selected' : '' }}>{{ $m }}</option>
@endforeach
</select>
</div>
<div class="col-md-2 mb-2 mb-md-0">
<label class="small font-weight-bold text-muted text-uppercase">Tahun</label>
<select name="year" class="form-control form-control-sm select2-filter">
@for($y = date('Y'); $y >= 2024; $y--)
<option value="{{ $y }}" {{ request('year', date('Y')) == $y ? 'selected' : '' }}>{{ $y }}</option>
@endfor
</select>
</div>
<div class="col-md-2">
<div class="btn-group w-100">
<button type="submit" class="btn btn-sm btn-primary shadow-sm"><i class="fas fa-filter mr-1"></i></button>
<a href="{{ route('budget_control') }}" class="btn btn-sm btn-secondary shadow-sm"><i class="fas fa-sync-alt"></i></a>
</div>
</div>
</div>
</form>
</div>
</div>
<div class="card card-primary card-outline shadow-sm" style="border-radius: 15px;">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="header-title m-0">Current Active Budget</h4>
@if (auth()->user()->can('budget_control.create'))
<div>
@if(auth()->user()->hasAnyRole(['Accounting', 'accounting', 'Admin Region', 'Superadmin', 'Super Admin']))
<button type="button" class="btn btn-success btn-sm rounded-pill px-3 shadow-sm mr-2" data-toggle="modal" data-target="#modalBulkUpload">
<i class="fas fa-file-excel mr-1"></i> Bulk Upload
</button>
@endif
<a class="btn btn-primary btn-sm rounded-pill px-3 shadow-sm" href="{{ route('budget_control.create') }}">
<i class="fas fa-plus-circle mr-1"></i> Add New Budget
</a>
</div>
@endif
</div>
<div class="dataTables_wrapper dt-bootstrap4 mt-2">
@include('backend.layouts.partials.messages')
<div class="table-responsive">
<table id="dataTable"
class="table table-bordered table-striped table-head-fixed dataTable dtr-inline"
style="width:100%">
<table id="dataTable" class="table table-bordered table-striped table-hover w-100">
<thead class="bg-light text-capitalize">
<tr>
<th>#</th>
<th>Penerima</th>
<th>Cabang</th>
<th width="3%">#</th>
<th>Penerima / Cabang</th>
<th>Pengirim</th>
<th>Total</th>
<th>Sisa Budget Tersedia</th>
<th>Keterangan</th>
<th>Assigned Budget</th>
<th>Real-time Sisa Budget</th>
<th>Periode</th>
<th>Status</th>
<th>Aksi</th>
<th>Status Budget</th>
<th>Status Transfer</th>
<th width="5%" class="text-center">Bukti</th>
<th width="10%">Aksi</th>
</tr>
</thead>
<tbody>
@php
use App\Helpers\BudgetHelper;
@endphp
@foreach ($data as $item)
@php
$realtimeBudget = $item->availableBudget ?? 0;
@endphp
<tr>
<td>{{ $loop->iteration }}</td>
<td>{{ $item->receiver->name }}</td>
<td>{{ $item->cabang->name }}</td>
<td>{{ $item->sender->name }}</td>
<td>{{ number_format($item->amount, 0, ',', '.') }}</td>
<td>{{ number_format($item->availableBudget, 0, ',', '.') }}</td>
<td>{{ $item->remarks }}</td>
<td>{{ strtoupper($item->periode_month) }} {{ $item->periode_year }}</td>
<td>{{ ($data->currentPage() - 1) * $data->perPage() + $loop->iteration }}</td>
<td>
@if ($item->status == 'Unassigned')
<span class="badge badge-warning text-white">{{ $item->status }}</span>
@elseif ($item->status == 'Assigned')
<span class="badge badge-success text-white">{{ $item->status }}</span>
<div class="fw-bold text-dark">{{ $item->receiver->name ?? '-' }}</div>
<small class="text-muted"><i class="fas fa-building mr-1"></i>{{ $item->cabang->name ?? '-' }}</small>
</td>
<td>{{ $item->sender->name ?? '-' }}</td>
<td class="align-middle">
<div class="fw-bold text-primary">Rp {{ number_format($item->amount, 0, ',', '.') }}</div>
</td>
<td class="align-middle">
<div class="fw-bold {{ $realtimeBudget >= 0 ? 'text-success' : 'text-danger' }}">
Rp {{ number_format($realtimeBudget, 0, ',', '.') }}
</div>
</td>
<td class="align-middle">
<span class="badge badge-light border shadow-sm px-2 py-1">
{{ strtoupper($item->periode_month) }} {{ $item->periode_year }}
</span>
</td>
<td class="align-middle text-center">
<span class="badge {{ $item->status == 'Assigned' ? 'badge-success' : 'badge-warning' }} px-2 py-1">
{{ $item->status }}
</span>
<div class="mt-1" style="line-height: 1.1;">
<small class="text-muted d-block" style="font-size: 10px;">{{ $item->created_at->format('d M Y H:i') }} WIB</small>
</div>
</td>
<td class="align-middle text-center">
@php
$tStatus = $item->transfer_status ?? 'Pending';
$badgeClass = match(strtolower($tStatus)) {
'validated' => 'badge-success',
'waiting approval' => 'badge-info',
'rejected' => 'badge-danger',
default => 'badge-secondary'
};
@endphp
<span class="badge {{ $badgeClass }} px-2 py-1">{{ $tStatus }}</span>
</td>
<td class="text-center align-middle">
@if($item->transfer_proof)
<a href="{{ asset(str_starts_with($item->transfer_proof, 'uploads/') ? $item->transfer_proof : 'uploads/' . $item->transfer_proof) }}"
target="_blank" class="btn btn-xs btn-info shadow-sm">
<i class="fas fa-eye"></i> View
</a>
@else
<span class="badge badge-danger text-white">{{ $item->status }}</span>
<span class="text-muted small">-</span>
@endif
</td>
<td>
@if (auth()->user()->can('budget_control.delete'))
<form action="{{ route('budget_control.destroy', $item->id) }}" method="POST" class="d-inline" onsubmit="return confirm('Are you sure you want to delete this item?');">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-danger btn-sm">
<i class="fas fa-trash"></i>
<td class="align-middle">
<div class="btn-group shadow-sm">
@if(strtolower($item->transfer_status) != 'validated')
<button type="button" class="btn btn-sm btn-outline-primary btn-upload" data-id="{{ $item->id }}" data-toggle="modal" data-target="#modalUploadProof">
<i class="fas fa-upload"></i>
</button>
@endif
@if(Auth::user()->hasAnyRole(['accounting', 'Accounting', 'admin', 'Admin', 'Superadmin']) && strtolower($item->transfer_status) == 'waiting approval')
<form action="{{ route('budget_control.approve_transfer', $item->id) }}" method="POST" class="d-inline ml-1">
@csrf
<button type="submit" class="btn btn-sm btn-success" onclick="return confirm('Sahkan transfer ini?')"><i class="fas fa-check"></i></button>
</form>
<form action="{{ route('budget_control.reject_transfer', $item->id) }}" method="POST" class="d-inline ml-1">
@csrf
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Tolak transfer ini?')"><i class="fas fa-times"></i></button>
</form>
@endif
@if (auth()->user()->can('budget_control.delete'))
<form action="{{ route('budget_control.destroy', $item->id) }}" method="POST" class="d-inline ml-1" onsubmit="return confirm('Hapus data?');">
@csrf @method('DELETE')
<button type="submit" class="btn btn-danger btn-sm"><i class="fas fa-trash"></i></button>
</form>
@endif
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
{{-- --- BAGIAN 2: NAVIGASI HALAMAN (SERVER-SIDE) --- --}}
<div class="mt-4 d-flex justify-content-between align-items-center">
<div class="small text-muted">
Menampilkan {{ $data->firstItem() }} sampai {{ $data->lastItem() }} dari {{ $data->total() }} data
</div>
<div>
{{ $data->appends(request()->query())->links('pagination::bootstrap-4') }}
</div>
</div>
</div>
</div>
</div>
</div>
</section>
@include('backend.pages.budget_control.partials.modals')
@endsection
@section('scripts')
<script>
function toggleUploadType() {
const isRegion = document.getElementById('type_region').checked;
document.getElementById('group_region').classList.toggle('d-none', !isRegion);
document.getElementById('group_cabang').classList.toggle('d-none', isRegion);
}
$(document).ready(function () {
// Inisialisasi Select2 untuk Filter
$('.select2-filter').select2({
theme: 'bootstrap4',
width: '100%'
}).on('change', function() {
$('#filter-form').submit();
});
// Inisialisasi DataTables
if ($('#dataTable').length) {
$('#dataTable').DataTable({
paging: false,
info: false,
responsive: false,
dom: 'Blfrtip',
lengthMenu: [ [10, 50, 100, -1], [10, 50, 100, "All"] ], // Control the entries dropdown
buttons: [
{
extend: 'excel',
text: 'Excel'
},
{
extend: 'pdf',
text: 'PDF',
orientation: 'landscape',
pageSize: 'A4',
exportOptions: {
columns: ':visible'
}
},
{
extend: 'print',
text: 'Print',
orientation: 'landscape',
exportOptions: {
columns: ':visible'
}
},
'colvis'
{ extend: 'excel', className: 'btn-sm btn-success', text: '<i class="fas fa-file-excel mr-1"></i> Excel' },
{ extend: 'pdf', className: 'btn-sm btn-danger', text: '<i class="fas fa-file-pdf mr-1"></i> PDF' }
]
});
}
$(document).on('click', '.btn-upload', function() {
$('#budget_id').val($(this).data('id'));
});
// Select2 di dalam modal
$('#modalBulkUpload').on('shown.bs.modal', function () {
$('.select2_modal').select2({
theme: 'bootstrap4',
width: '100%',
dropdownParent: $('#modalBulkUpload')
});
});
});
</script>
@endsection
@@ -1,90 +1,270 @@
@extends('layouts.app')
@section('title')
Dashboard
@endsection
@section('title', 'Riwayat Transfer & Realisasi')
@section('admin-content')
<section class="content-header">
<section class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1>Log Transfer Budgets</h1>
<h1 class="fw-bold text-dark">Log Riwayat Budget & Transaksi</h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="{{ route('dashboard.index') }}">Dashboard</a></li>
</li>
<li class="breadcrumb-item"><a href="{{ route('budget_control') }}">Budget Control</a></li>
<li class="breadcrumb-item active">Riwayat</li>
</ol>
</div>
</div>
</div>
</section>
<section class="content">
</section>
<section class="content">
<div class="container-fluid">
<div class="card card-primary card-outline">
{{-- --- BAGIAN BARU: PANEL FILTER --- --}}
<div class="card shadow-sm mb-4" style="border-radius: 15px; border-top: 3px solid #17a2b8;">
<div class="card-body py-3">
<form action="{{ route('budget_control.log') }}" method="GET" id="filter-form">
<div class="row align-items-end">
<div class="col-md-3 mb-2 mb-md-0">
<label class="small font-weight-bold text-muted text-uppercase">Region</label>
<select name="region_id" class="form-control form-control-sm select2" onchange="this.form.submit()">
<option value="">Semua Region</option>
@foreach($regions as $reg)
<option value="{{ $reg->id }}" {{ request('region_id') == $reg->id ? 'selected' : '' }}>{{ $reg->name }}</option>
@endforeach
</select>
</div>
<div class="col-md-3 mb-2 mb-md-0">
<label class="small font-weight-bold text-muted text-uppercase">Cabang</label>
<select name="cabang_id" class="form-control form-control-sm select2" onchange="this.form.submit()">
<option value="">Semua Cabang</option>
@foreach($cabangOptions as $cab)
<option value="{{ $cab->id }}" {{ request('cabang_id') == $cab->id ? 'selected' : '' }}>{{ $cab->name }}</option>
@endforeach
</select>
</div>
<div class="col-md-2 mb-2 mb-md-0">
<label class="small font-weight-bold text-muted text-uppercase">Bulan</label>
<select name="month" class="form-control form-control-sm" onchange="this.form.submit()">
<option value="">Semua Bulan</option>
@foreach(['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] as $m)
<option value="{{ $m }}" {{ request('month') == $m ? 'selected' : '' }}>{{ $m }}</option>
@endforeach
</select>
</div>
<div class="col-md-2 mb-2 mb-md-0">
<label class="small font-weight-bold text-muted text-uppercase">Tahun</label>
<select name="year" class="form-control form-control-sm" onchange="this.form.submit()">
@for($y = date('Y'); $y >= 2024; $y--)
<option value="{{ $y }}" {{ request('year', date('Y')) == $y ? 'selected' : '' }}>{{ $y }}</option>
@endfor
</select>
</div>
<div class="col-md-2">
<div class="btn-group w-100">
<button type="submit" class="btn btn-sm btn-primary px-3 shadow-sm">
<i class="fas fa-filter"></i>
</button>
<a href="{{ route('budget_control.log') }}" class="btn btn-sm btn-secondary shadow-sm" title="Reset Filter">
<i class="fas fa-sync-alt"></i>
</a>
</div>
</div>
</div>
</form>
</div>
</div>
{{-- --- BAGIAN GRAFIK PERBANDINGAN (TETAP ADA) --- --}}
<div class="row">
<div class="col-12">
<div class="card shadow-sm mb-4" style="border-radius: 15px; border-left: 5px solid #007bff;">
<div class="card-header bg-white border-0 pt-3">
<h3 class="card-title fw-bold text-primary">
<i class="fas fa-chart-bar mr-2"></i> Perbandingan Petty Cash vs Realisasi (Halaman Ini)
</h3>
</div>
<div class="card-body">
<h4 class="header-title float-left">Log Transfer Budgets</h4>
<div class="clearfix"></div>
<div class="dataTables_wrapper dt-bootstrap4 mt-2">
@include('backend.layouts.partials.messages')
<div style="height: 280px;">
<canvas id="logComparisonChart"></canvas>
</div>
</div>
</div>
</div>
</div>
{{-- --- TABEL DATA --- --}}
<div class="card card-primary card-outline shadow-sm" style="border-radius: 15px;">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="text-muted font-weight-bold m-0 text-uppercase small">Detail Transaksi Riwayat</h6>
<div id="dt-buttons-container"></div>
</div>
<div class="table-responsive">
<table id="dataTable"
class="table table-bordered table-striped table-head-fixed dataTable dtr-inline"
style="width:100%">
<thead class="bg-light text-capitalize">
<table id="logTable" class="table table-bordered table-striped table-hover w-100">
<thead class="bg-light text-center text-uppercase small font-weight-bold">
<tr>
<th>#</th>
<th>Penerima</th>
<th>Cabang</th>
<th>Pengirim</th>
<th>Total</th>
<th>Keterangan</th>
<th width="3%">#</th>
<th class="text-left">Cabang / Region</th>
<th>Periode</th>
<th>Total Budget (Petty Cash)</th>
<th>Total Debet (Masuk)</th>
<th>Total Kredit (Keluar)</th>
<th class="text-success">Realisasi (Sisa)</th>
<th>Waktu Assign</th>
<th>Status</th>
</tr>
</thead>
<tbody>
@foreach ($data as $item)
<tbody class="text-center">
@forelse ($data as $item)
<tr>
<td>{{ $loop->iteration }}</td>
<td>{{ $item->receiver->name }}</td>
<td>{{ $item->cabang->name }}</td>
<td>{{ $item->sender->name }}</td>
<td>{{ number_format($item->amount, 0, ',', '.') }}</td>
<td>{{ $item->remarks }}</td>
<td>{{ strtoupper($item->periode_month) }} {{ $item->periode_year }}</td>
<td>
@if ($item->status == 'Unassigned')
<span class="badge badge-warning text-white">{{ $item->status }}</span>
@elseif ($item->status == 'Assigned')
<span class="badge badge-success text-white">{{ $item->status }}</span>
@else
<span class="badge badge-danger text-white">{{ $item->status }}</span>
@endif
<td>{{ ($data->currentPage() - 1) * $data->perPage() + $loop->iteration }}</td>
<td class="text-left align-middle">
<div class="fw-bold text-dark">{{ $item->cabang->name ?? '-' }}</div>
<small class="text-muted">{{ $item->cabang->region->name ?? '-' }}</small>
</td>
<td class="align-middle">
<span class="badge badge-light border shadow-sm px-2 py-1">
{{ strtoupper($item->periode_month) }} {{ $item->periode_year }}
</span>
</td>
<td class="align-middle fw-bold text-dark">
Rp {{ number_format($item->total_budget, 0, ',', '.') }}
</td>
<td class="align-middle text-primary">
Rp {{ number_format($item->total_debet, 0, ',', '.') }}
</td>
<td class="align-middle text-danger">
Rp {{ number_format($item->total_credit, 0, ',', '.') }}
</td>
<td class="align-middle fw-bold text-success" style="font-size: 15px;">
Rp {{ number_format($item->realisasi, 0, ',', '.') }}
</td>
<td class="align-middle">
<small class="text-muted d-block">
<i class="fas fa-calendar-alt mr-1"></i>{{ $item->created_at->format('d M Y') }}
</small>
<small class="text-muted d-block">
<i class="fas fa-clock mr-1"></i>{{ $item->created_at->format('H:i') }} WIB
</small>
</td>
<td class="align-middle">
<span class="badge {{ $item->status == 'Assigned' ? 'badge-success' : 'badge-secondary' }} px-2 py-1 shadow-sm">
{{ $item->status }}
</span>
</td>
</tr>
@endforeach
@empty
<tr>
<td colspan="9" class="text-center py-5 text-muted">
<i class="fas fa-folder-open fa-2x mb-2"></i><br>
Belum ada riwayat budget yang tercatat.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
{{-- --- PAGINATION LARAVEL (TETAP ADA) --- --}}
<div class="mt-4 d-flex justify-content-between align-items-center">
<div class="small text-muted">
Menampilkan {{ $data->firstItem() }} sampai {{ $data->lastItem() }} dari {{ $data->total() }} data
</div>
<div>
{{ $data->appends(request()->query())->links('pagination::bootstrap-4') }}
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</section>
@endsection
@section('scripts')
<script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
$(document).ready(function () {
if ($('#dataTable').length) {
$('#dataTable').DataTable({
responsive: false,
dom: 'Bfrtip',
buttons: ['excel', 'pdf', 'print']
});
// 1. INISIALISASI GRAFIK PERBANDINGAN
const ctx = document.getElementById('logComparisonChart').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
labels: {!! json_encode($chartData['labels'] ?? []) !!},
datasets: [
{
label: 'Petty Cash (Budget)',
data: {!! json_encode($chartData['budget'] ?? []) !!},
backgroundColor: 'rgba(54, 162, 235, 0.7)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1,
borderRadius: 5
},
{
label: 'Sisa Realisasi',
data: {!! json_encode($chartData['realisasi'] ?? []) !!},
backgroundColor: 'rgba(40, 167, 69, 0.7)',
borderColor: 'rgba(40, 167, 69, 1)',
borderWidth: 1,
borderRadius: 5
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'top' }
},
scales: {
y: {
beginAtZero: true,
ticks: {
callback: function(value) { return 'Rp ' + value.toLocaleString('id-ID'); }
}
}
}
}
});
</script>
// 2. DATATABLES OPTIMIZATION
if ($('#logTable').length) {
var table = $('#logTable').DataTable({
paging: false,
info: false,
searching: true,
responsive: false,
dom: 'Bfrtip',
buttons: [
{
extend: 'excelHtml5',
className: 'btn-sm btn-success shadow-sm',
text: '<i class="fas fa-file-excel mr-1"></i> Export Excel',
title: 'Laporan Riwayat Budget - ' + new Date().toLocaleDateString('id-ID'),
exportOptions: { columns: ':visible' }
},
{
extend: 'pdfHtml5',
className: 'btn-sm btn-danger shadow-sm',
text: '<i class="fas fa-file-pdf mr-1"></i> Export PDF',
orientation: 'landscape',
pageSize: 'A4'
}
]
});
table.buttons().container().appendTo('#dt-buttons-container');
}
});
</script>
@endsection
@@ -0,0 +1,113 @@
{{-- Modal Upload Bukti Transfer --}}
<div class="modal fade" id="modalUploadProof" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content" style="border-radius: 15px; border: none;">
<form id="formUploadProof" action="{{ route('budget_control.upload_proof') }}" method="POST" enctype="multipart/form-data">
@csrf
<input type="hidden" name="budget_id" id="budget_id">
<div class="modal-header bg-primary text-white" style="border-radius: 15px 15px 0 0;">
<h5 class="modal-title font-weight-bold"><i class="fas fa-file-invoice mr-2"></i>Upload Bukti Transfer</h5>
<button type="button" class="close text-white" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="transfer_proof">Upload Dokumen Budget (PDF/Gambar) <span class="text-danger">*</span></label>
<input type="file" name="transfer_proof" id="transfer_proof" class="form-control" accept=".pdf, .jpg, .jpeg, .png" required>
<small class="form-text text-muted">Format yang diizinkan: PDF, JPG, JPEG, PNG. Maksimal ukuran file: 5MB.</small>
</div>
</div>
<div class="modal-footer bg-light" style="border-radius: 0 0 15px 15px;">
<button type="button" class="btn btn-secondary rounded-pill px-4" data-dismiss="modal">Batal</button>
<button type="submit" class="btn btn-primary rounded-pill px-4 shadow">Simpan Bukti</button>
</div>
</form>
</div>
</div>
</div>
{{-- Modal Bulk Upload Budget --}}
<div class="modal fade" id="modalBulkUpload" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content" style="border-radius: 15px; border: none;">
<div class="modal-header bg-success text-white" style="border-radius: 15px 15px 0 0;">
<h5 class="modal-title"><i class="fas fa-upload mr-2"></i> Bulk Upload Budget (Excel)</h5>
<button type="button" class="close text-white" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<form action="{{ route('budget_control.bulk_upload') }}" method="POST" enctype="multipart/form-data">
@csrf
<div class="modal-body">
<div class="alert alert-info small shadow-sm">
<div class="d-flex justify-content-between align-items-center">
<div>
<b>Format Kolom Excel (Baris 1 wajib Header):</b><br>
Kolom A: Kode Cabang | Kolom B: Bulan | Kolom C: Nominal | Kolom D: Keterangan
</div>
<div>
<a href="{{ route('budget_control.download_template') }}" class="btn btn-sm btn-light font-weight-bold shadow-sm text-info">
<i class="fas fa-download mr-1"></i> Download Template
</a>
</div>
</div>
</div>
<div class="form-group">
<label class="font-weight-bold small text-muted">METODE UPLOAD <span class="text-danger">*</span></label>
<div class="d-flex">
<div class="custom-control custom-radio mr-4">
<input type="radio" id="type_region" name="upload_type" value="region" class="custom-control-input" checked onchange="toggleUploadType()">
<label class="custom-control-label" for="type_region">Per Region</label>
</div>
<div class="custom-control custom-radio">
<input type="radio" id="type_cabang" name="upload_type" value="cabang" class="custom-control-input" onchange="toggleUploadType()">
<label class="custom-control-label" for="type_cabang">Pilih Cabang Tertentu</label>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6 form-group" id="group_region">
<label class="font-weight-bold small text-muted text-uppercase">Region <span class="text-danger">*</span></label>
<select name="region_id" class="form-control select2_modal">
<option value="">-- Pilih Region --</option>
@foreach($regions as $reg)
<option value="{{ $reg->id }}">{{ $reg->name }}</option>
@endforeach
</select>
</div>
<div class="col-md-6 form-group d-none" id="group_cabang">
<label class="font-weight-bold small text-muted text-uppercase">Cabang <span class="text-danger">*</span></label>
<select name="cabang_id[]" class="form-control select2_modal" multiple="multiple" style="width: 100%;">
@foreach($cabangOptions as $cab)
<option value="{{ $cab->id }}">{{ $cab->code }} - {{ $cab->name }}</option>
@endforeach
</select>
</div>
<div class="col-md-6 form-group">
<label class="font-weight-bold small text-muted text-uppercase">Tahun Periode <span class="text-danger">*</span></label>
<input type="number" name="periode_year" class="form-control" value="{{ date('Y') }}" required>
</div>
</div>
<hr>
<div class="form-group">
<label class="font-weight-bold small text-muted">FILE UPLOAD (.xlsx, .xls, .csv) <span class="text-danger">*</span></label>
<input type="file" name="file_excel" class="form-control-file" accept=".xlsx, .xls, .csv" required>
</div>
</div>
<div class="modal-footer bg-light" style="border-radius: 0 0 15px 15px;">
<button type="button" class="btn btn-secondary shadow-sm" data-dismiss="modal">Batal</button>
<button type="submit" class="btn btn-success shadow-sm"><i class="fas fa-check"></i> Proses Upload</button>
</div>
</form>
</div>
</div>
</div>
@@ -1,125 +1,357 @@
@extends('layouts.app')
@section('title')
Dashboard
@section('title', 'Dashboard Operational Analytics')
@section('styles')
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.7/css/dataTables.bootstrap4.min.css">
<link rel="stylesheet" href="https://cdn.datatables.net/buttons/2.4.2/css/buttons.bootstrap4.min.css">
{{-- Tambahan CDN Select2 untuk menjamin inisialisasi select2 dropdown berjalan lancar --}}
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ttskch/select2-bootstrap4-theme@x.x.x/dist/select2-bootstrap4.min.css">
<style>
.chart-container { position: relative; height: 300px; width: 100%; }
.select2-container--bootstrap4 .select2-selection--single { height: calc(1.8125rem + 2px) !important; }
#table-monitoring thead th a { color: inherit; text-decoration: none; display: block; width: 100%; }
#table-monitoring tbody tr:hover { background-color: rgba(0,123,255,0.05); transition: 0.2s; }
.section-title { border-left: 5px solid #007bff; padding-left: 15px; margin-bottom: 20px; font-weight: 700; color: #333; text-transform: uppercase; }
.row-region-group { background-color: #f4f6f9 !important; border-top: 2px solid #dee2e6; }
</style>
@endsection
@section('admin-content')
<section class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1>Home</h1>
<div class="container-fluid pt-5">
{{-- 1. HEADER & LIVE CLOCK --}}
<div class="alert bg-primary text-white shadow-sm d-flex justify-content-between align-items-center mb-4" style="border-radius: 15px; border: none;">
<div>
<span class="font-weight-bold"><i class="fas fa-clock mr-2"></i> Waktu Sistem: </span>
<span id="live-clock" class="badge badge-light text-primary px-3 py-2 shadow-sm" style="font-size: 0.95rem; border-radius: 10px;">
Memuat...
</span>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="{{ route('dashboard.index') }}">Dashboard</a></li>
</ol>
<div class="d-flex align-items-center">
<div class="bg-white text-primary px-4 py-2 shadow-sm d-none d-md-block ml-3" style="border-radius: 50px;">
<i class="far fa-calendar-alt mr-2"></i>
<span class="font-weight-bold">Periode Input: {{ $periodeLabel ?? '-' }}</span>
</div>
</div>
</div>
</section>
<section class="content">
<div class="container-fluid">
<!-- Statistic Cards Grid -->
<div class="row row-cols-2 row-cols-md-2 row-cols-lg-3 g-4">
<!-- Statistic Card 1 -->
<div class="col">
<div class="small-box bg-info">
<div class="inner">
<h3>Rp {{ number_format($forms['vehicle'] + $forms['entertainment'] + $forms['meeting'] + $forms['upcountry'] + $forms['others'], 0, ',', '.') }}</h3>
<p>All Approved Expenses</p>
</div>
<div class="icon">
<i class="fas fa-check-circle"></i>
</div>
<a href="#" class="small-box-footer">
More info <i class="fas fa-arrow-circle-right"></i>
</a>
{{-- 2. KARTU STATISTIK --}}
<div class="row">
<div class="col-lg-3 col-6 mb-3">
<div class="small-box bg-info shadow-sm h-100" style="border-radius: 15px;">
<div class="inner p-4"><h3>{{ $totalRegions ?? 0 }}</h3><p>Total Region</p></div>
<div class="icon"><i class="fas fa-map-marked-alt"></i></div>
</div>
</div>
<!-- Statistic Card 2 -->
<div class="col">
<div class="small-box bg-success">
<div class="inner">
<h3>Rp {{ number_format($forms['upcountry'], 0, ',', '.') }}</h3>
<p>Approved Up Country Expenses</p>
</div>
<div class="icon">
<i class="fa fa-flag" aria-hidden="true"></i>
</div>
<a href="{{ route('forms.up-country') }}" class="small-box-footer">
More info <i class="fas fa-arrow-circle-right"></i>
</a>
<div class="col-lg-3 col-6 mb-3">
<div class="small-box bg-success shadow-sm h-100" style="border-radius: 15px;">
<div class="inner p-4"><h3>{{ $totalCabangs ?? 0 }}</h3><p>Cabang Aktif</p></div>
<div class="icon"><i class="fas fa-building"></i></div>
</div>
</div>
<!-- Statistic Card 3 -->
<div class="col">
<div class="small-box bg-warning">
<div class="inner text-white">
<h3>Rp {{ number_format($forms['vehicle'], 0, ',', '.') }}</h3>
<p>Approved Vehicle Running Cost Expenses</p>
</div>
<div class="icon">
<i class="fa fa-car" aria-hidden="true"></i>
</div>
<a href="{{ route('forms.vehicle') }}" class="small-box-footer text-white">
<span class="text-white">More info</span> <i class="fas fa-arrow-circle-right text-white"></i>
</a>
<div class="col-lg-3 col-6 mb-3">
<div class="small-box bg-warning shadow-sm h-100" style="border-radius: 15px;">
<div class="inner p-4 text-white"><h3>{{ $pendingCount ?? 0 }}</h3><p>Total Dokumen Pending</p></div>
<div class="icon"><i class="fas fa-file-signature"></i></div>
</div>
</div>
<!-- Statistic Card 4 -->
<div class="col">
<div class="small-box bg-purple">
<div class="inner">
<h3>Rp {{ number_format($forms['entertainment'], 0, ',', '.') }}</h3>
<p>Approved Entertainment Presentation Expenses</p>
<div class="col-lg-3 col-6 mb-3">
<div class="small-box bg-danger shadow-sm h-100" style="border-radius: 15px;">
<div class="inner p-4">
<h3>{{ $totalKategori ?? 0 }}</h3>
<p>Master Kategori</p>
</div>
<div class="icon">
<i class="fa fa-desktop" aria-hidden="true"></i>
</div>
<a href="{{ route('forms.entertainment') }}" class="small-box-footer">
More info <i class="fas fa-arrow-circle-right"></i>
</a>
</div>
</div>
<!-- Statistic Card 5 -->
<div class="col">
<div class="small-box bg-danger">
<div class="inner">
<h3>Rp {{ number_format($forms['meeting'], 0, ',', '.') }}</h3>
<p>Approved Meeting Seminar Expenses</p>
</div>
<div class="icon">
<i class="fa fa-users" aria-hidden="true"></i>
</div>
<a href="{{ route('forms.meeting') }}" class="small-box-footer">
More info <i class="fas fa-arrow-circle-right"></i>
</a>
</div>
</div>
<!-- Statistic Card 6 -->
<div class="col">
<div class="small-box bg-pink">
<div class="inner">
<h3>Rp {{ number_format($forms['others'], 0, ',', '.') }}</h3>
<p>Approved Other Expenses</p>
</div>
<div class="icon">
<i class="fa fa-bars" aria-hidden="true"></i>
</div>
<a href="{{ route('forms.other') }}" class="small-box-footer">
More info <i class="fas fa-arrow-circle-right"></i>
</a>
<div class="icon"><i class="fas fa-tags"></i></div>
</div>
</div>
</div>
<!-- Welcome Card -->
<div class="card card-primary card-outline mt-4">
<div class="card-body text-center">
<p class="lead">Welcome to {{ config('app.name') }} (development version).</p>
{{-- FILTER KHUSUS GRAFIK PERFORMA --}}
<div class="card shadow-sm mb-4 mt-2" style="border-radius: 15px; background-color: #f8f9fa; border-left: 5px solid #17a2b8;">
<div class="card-body py-2 px-3">
<form action="{{ route('admin.dashboard') }}" method="GET" id="chart-filter-form" class="m-0">
<input type="hidden" name="region_id" value="{{ request('region_id') }}">
<input type="hidden" name="cabang_id" value="{{ request('cabang_id') }}">
<input type="hidden" name="category" value="{{ request('category') }}">
<input type="hidden" name="search" value="{{ request('search') }}">
<div class="d-flex justify-content-between align-items-center flex-wrap">
<h6 class="m-0 font-weight-bold text-info"><i class="fas fa-filter mr-2"></i>Filter Performa Closing Expense</h6>
<div class="d-flex align-items-center mt-2 mt-md-0">
<label class="small font-weight-bold text-muted mr-2 mb-0">Bulan:</label>
<select name="chart_month" class="form-control form-control-sm mr-3" style="width: 130px;" onchange="this.form.submit()">
<option value="">Semua Bulan</option>
@foreach(range(1, 12) as $m)
<option value="{{ sprintf('%02d', $m) }}" {{ (request('chart_month') ?? date('m')) == sprintf('%02d', $m) ? 'selected' : '' }}>
{{ Carbon\Carbon::create()->month($m)->translatedFormat('F') }}
</option>
@endforeach
</select>
<label class="small font-weight-bold text-muted mr-2 mb-0">Tahun:</label>
<select name="chart_year" class="form-control form-control-sm" style="width: 100px;" onchange="this.form.submit()">
@php $currentYear = date('Y'); @endphp
@for($y = $currentYear; $y >= 2023; $y--)
<option value="{{ $y }}" {{ (request('chart_year') ?? date('Y')) == $y ? 'selected' : '' }}>{{ $y }}</option>
@endfor
</select>
</div>
</div>
</form>
</div>
</div>
{{-- 3. GRAFIK PERFORMA CLOSING EXPENSE --}}
<h5 class="section-title mt-4">Respon Speed Expense</h5>
<div class="row mt-2">
<div class="col-md-6 mb-4">
<div class="card shadow-sm h-100" style="border-radius: 15px;">
<div class="card-header bg-white border-0 pt-3">
<h3 class="card-title font-weight-bold text-success text-uppercase small"><i class="fas fa-bolt mr-2"></i> 5 Cabang Tercepat</h3>
</div>
<div class="card-body">
<div class="chart-container"><canvas id="chartFastest"></canvas></div>
</div>
</div>
</div>
</section>
<div class="col-md-6 mb-4">
<div class="card shadow-sm h-100" style="border-radius: 15px;">
<div class="card-header bg-white border-0 pt-3">
<h3 class="card-title font-weight-bold text-danger text-uppercase small"><i class="fas fa-snail mr-2"></i> 5 Cabang Terlama</h3>
</div>
<div class="card-body">
<div class="chart-container"><canvas id="chartSlowest"></canvas></div>
</div>
</div>
</div>
</div>
{{-- 4. FILTER PANEL TABEL PENDING --}}
<div class="card shadow-sm mb-4" style="border-radius: 15px;">
<div class="card-body py-3">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="font-weight-bold text-muted m-0 small text-uppercase">Tabel Monitoring Dokumen Pending</h6>
{{-- Container Tombol Excel --}}
<div id="export-button-container"></div>
</div>
<form action="{{ route('admin.dashboard') }}" method="GET" id="filter-form">
<input type="hidden" name="chart_month" value="{{ request('chart_month') }}">
<input type="hidden" name="chart_year" value="{{ request('chart_year') }}">
<div class="row align-items-end">
<div class="col-md-3 mb-2 mb-md-0">
<label class="small font-weight-bold text-muted text-uppercase">Region</label>
<select name="region_id" class="form-control form-control-sm select2" onchange="this.form.submit()">
<option value="">Semua Region</option>
@foreach($regions as $reg)
<option value="{{ $reg->id }}" {{ request('region_id') == $reg->id ? 'selected' : '' }}>{{ $reg->name }}</option>
@endforeach
</select>
</div>
<div class="col-md-3 mb-2 mb-md-0">
<label class="small font-weight-bold text-muted text-uppercase">Cabang</label>
<select name="cabang_id" class="form-control form-control-sm select2" onchange="this.form.submit()">
<option value="">Semua Cabang</option>
@foreach($cabangOptions as $cab)
<option value="{{ $cab->id }}" {{ request('cabang_id') == $cab->id ? 'selected' : '' }}>{{ $cab->name }}</option>
@endforeach
</select>
</div>
<div class="col-md-3 mb-2 mb-md-0">
<label class="small font-weight-bold text-muted text-uppercase">Highlight Kategori</label>
<select name="category" class="form-control form-control-sm" onchange="this.form.submit()">
<option value="">Pilih Kategori</option>
@php
$categories = [
'up_country' => 'Up Country',
'vehicle' => 'Vehicle Running Cost',
'entertainment' => 'Entertainment',
'meeting' => 'Meeting Seminar',
'others' => 'Others'
];
@endphp
@foreach($categories as $key => $val)
<option value="{{ $key }}" {{ request('category') == $key ? 'selected' : '' }}>{{ $val }}</option>
@endforeach
</select>
</div>
<div class="col-md-3">
<div class="d-flex">
<input type="text" name="search" class="form-control form-control-sm mr-2" placeholder="Cari..." value="{{ request('search') }}">
<button type="submit" class="btn btn-sm btn-primary px-3 shadow-sm">Filter</button>
<a href="{{ route('admin.dashboard') }}" class="btn btn-sm btn-secondary ml-1 shadow-sm" title="Reset Filter"><i class="fas fa-sync-alt"></i></a>
</div>
</div>
</div>
</form>
</div>
</div>
{{-- 5. TABEL MONITORING --}}
<div class="card shadow-sm" style="border-radius: 15px; overflow: hidden;">
<div class="card-body p-0 table-responsive">
<table id="table-monitoring" class="table table-bordered m-0">
<thead class="bg-light text-center small text-uppercase font-weight-bold">
<tr>
<th rowspan="2" class="align-middle">
<a href="{{ request()->fullUrlWithQuery(['sort' => 'region', 'direction' => request('direction') == 'asc' ? 'desc' : 'asc']) }}">Region <i class="fas fa-sort ml-1 opacity-50"></i></a>
</th>
<th rowspan="2" class="align-middle text-left">
<a href="{{ request()->fullUrlWithQuery(['sort' => 'name', 'direction' => request('direction') == 'asc' ? 'desc' : 'asc']) }}">Cabang <i class="fas fa-sort ml-1 opacity-50"></i></a>
</th>
<th colspan="5" class="bg-white text-center align-middle">Rincian Dokumen Pending</th>
<th rowspan="2" class="align-middle text-center">
<a href="{{ request()->fullUrlWithQuery(['sort' => 'total_pending', 'direction' => request('direction') == 'asc' ? 'desc' : 'asc']) }}">Total <i class="fas fa-sort ml-1 opacity-50"></i></a>
</th>
<th rowspan="2" class="align-middle no-export text-center">Aksi</th>
</tr>
<tr class="bg-white">
<th class="{{ request('category') == 'up_country' ? 'bg-warning' : '' }}">UC</th>
<th class="{{ request('category') == 'vehicle' ? 'bg-warning' : '' }}">VC</th>
<th class="{{ request('category') == 'entertainment' ? 'bg-warning' : '' }}">EN</th>
<th class="{{ request('category') == 'meeting' ? 'bg-warning' : '' }}">MT</th>
<th class="{{ request('category') == 'others' ? 'bg-warning' : '' }}">OT</th>
</tr>
</thead>
<tbody class="text-center">
@php $currentRegion = null; @endphp
@forelse($cabangDetails as $detail)
@if($currentRegion != ($detail->region_id ?? null))
<tr class="row-region-group">
<td colspan="9" class="text-left font-weight-bold text-primary text-uppercase" style="letter-spacing: 1px;">
<i class="fas fa-map-marked-alt mr-2 text-warning"></i> REGION: {{ $detail->region->name ?? 'TANPA REGION' }}
</td>
</tr>
@php $currentRegion = $detail->region_id ?? null; @endphp
@endif
<tr>
<td class="text-left small text-muted"><i class="fas fa-level-up-alt fa-rotate-90 text-muted mr-1 ml-2"></i> {{ $detail->region->name ?? '-' }}</td>
<td class="text-left font-weight-bold text-dark">{{ $detail->name }}</td>
<td>{{ $detail->pending_details['up_country'] ?? 0 }}</td>
<td>{{ $detail->pending_details['vehicle'] ?? 0 }}</td>
<td>{{ $detail->pending_details['entertainment'] ?? 0 }}</td>
<td>{{ $detail->pending_details['meeting'] ?? 0 }}</td>
<td>{{ $detail->pending_details['others'] ?? 0 }}</td>
<td class="bg-light font-weight-bold text-center align-middle">{{ $detail->total_pending }}</td>
<td class="no-export text-center align-middle">
<a href="{{ route('admin.dashboard.show', $detail->id) }}" class="btn btn-xs btn-info shadow-sm" title="Lihat Detail"><i class="fas fa-eye"></i></a>
</td>
</tr>
@empty
<tr>
<td colspan="9" class="py-5 text-center text-muted">
<i class="fas fa-info-circle mr-2"></i> Data tidak ditemukan untuk filter ini.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<div class="card-footer bg-white border-0 py-3">
<div class="d-flex justify-content-between align-items-center">
@if(isset($cabangDetails) && method_exists($cabangDetails, 'firstItem'))
<div class="small text-muted">Menampilkan <b>{{ $cabangDetails->firstItem() ?? 0 }}</b> - <b>{{ $cabangDetails->lastItem() ?? 0 }}</b> dari <b>{{ $cabangDetails->total() ?? 0 }}</b> Cabang.</div>
<div>{{ $cabangDetails->appends(request()->query())->links('pagination::bootstrap-4') }}</div>
@endif
</div>
</div>
</div>
</div>
@endsection
@section('scripts')
{{-- Library Wajib DataTables --}}
<script src="https://cdn.datatables.net/1.13.7/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.13.7/js/dataTables.bootstrap4.min.js"></script>
{{-- Library Wajib untuk Tombol Export --}}
<script src="https://cdn.datatables.net/buttons/2.4.2/js/dataTables.buttons.min.js"></script>
<script src="https://cdn.datatables.net/buttons/2.4.2/js/buttons.bootstrap4.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<script src="https://cdn.datatables.net/buttons/2.4.2/js/buttons.html5.min.js"></script>
{{-- Library Grafik --}}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.0.0"></script>
{{-- Library Select2 untuk keindahan select input --}}
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script>
// 1. LIVE CLOCK
function updateClock() {
const now = new Date();
const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' };
const clockEl = document.getElementById('live-clock');
if(clockEl) clockEl.innerText = now.toLocaleDateString('id-ID', options) + ' WIB';
}
setInterval(updateClock, 1000); updateClock();
$(document).ready(function() {
// Inisialisasi Select2
$('.select2').select2({
theme: 'bootstrap4',
width: '100%'
});
// Hindari pesan error alert bawaan browser
$.fn.dataTable.ext.errMode = 'none';
// Inisialisasi DataTable
var table = $('#table-monitoring').DataTable({
paging: false,
searching: false,
info: false,
ordering: false,
autoWidth: false,
buttons: [
{
extend: 'excelHtml5',
text: '<i class="fas fa-file-excel mr-1"></i> Export Excel',
className: 'btn btn-sm btn-success font-weight-bold shadow-sm',
title: 'Laporan Pending - {{ $periodeLabel ?? "Mei 2026" }}',
exportOptions: {
columns: ':not(.no-export)'
}
}
]
});
// Append tombol ke container
if (table.buttons().container().length) {
table.buttons().container().appendTo('#export-button-container');
}
});
// 3. CHARTS PERFORMA
Chart.register(ChartDataLabels);
const configChart = (ctxId, labels, data, color, rawDurations) => {
const ctx = document.getElementById(ctxId);
if(!ctx || !labels || labels.length === 0) return;
new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{ data: data, backgroundColor: color, borderRadius: 8, barThickness: 20 }]
},
options: {
indexAxis: 'y', responsive: true, maintainAspectRatio: false,
layout: { padding: { right: 85 } },
plugins: {
legend: { display: false },
datalabels: {
anchor: 'end', align: 'right', color: '#444', font: { weight: 'bold', size: 10 },
formatter: (val, ctx) => rawDurations[ctx.dataIndex] || ''
}
},
scales: { x: { display: false }, y: { ticks: { font: { size: 10, weight: 'bold' } } } }
}
});
};
configChart('chartFastest', {!! json_encode($chartFastest['labels'] ?? []) !!}, {!! json_encode($chartFastest['data'] ?? []) !!}, '#28a745', {!! json_encode($chartFastest['raw'] ?? []) !!});
configChart('chartSlowest', {!! json_encode($chartSlowest['labels'] ?? []) !!}, {!! json_encode($chartSlowest['data'] ?? []) !!}, '#dc3545', {!! json_encode($chartSlowest['raw'] ?? []) !!});
</script>
@endsection
@@ -0,0 +1,42 @@
@extends('layouts.app')
@section('admin-content')
<div class="container-fluid">
<div class="card shadow-sm mt-3">
<div class="card-header bg-info text-white">
<h3 class="card-title">Detail Pending: {{ $cabang->name }} ({{ $cabang->region->name ?? '' }})</h3>
<div class="card-tools">
<a href="{{ route('admin.dashboard') }}" class="btn btn-tool text-white"><i class="fas fa-times"></i></a>
</div>
</div>
<div class="card-body">
{{-- Loop menggunakan variabel $data yang dikirim dari controller --}}
@foreach($data as $label => $items)
@if($items->count() > 0)
<h5 class="mt-4 text-uppercase font-weight-bold text-secondary">{{ $label }}</h5>
<table class="table table-sm table-striped">
<thead>
<tr>
<th>No. Expense</th>
<th>Tanggal</th>
<th>Total</th>
<th>Status</th>
</tr>
</thead>
<tbody>
@foreach($items as $item)
<tr>
<td>{{ $item->expense_number }}</td>
<td>{{ \Carbon\Carbon::parse($item->tanggal)->format('d/m/Y') }}</td>
<td>Rp {{ number_format($item->total, 0, ',', '.') }}</td>
<td><span class="badge badge-warning">{{ $item->status }}</span></td>
</tr>
@endforeach
</tbody>
</table>
@endif
@endforeach
</div>
</div>
</div>
@endsection
@@ -6,6 +6,7 @@
@section('admin-content')
<script src="https://cdn.jsdelivr.net/npm/autonumeric@4.6.0/dist/autoNumeric.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<section class="content-header">
<div class="container-fluid">
@@ -16,7 +17,6 @@
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="{{ route('dashboard.index') }}">Dashboard</a></li>
</li>
</ol>
</div>
</div>
@@ -57,7 +57,7 @@
<div class="mb-3">
<label class="form-label">Jenis <span class="font-italic font-weight-normal">(required)</span></label>
<select class="form-control form-control-md" name="jenis" required >
<option value="">Pilih Jenis</option>
<option value="" disabled {{ old('jenis') == '' ? 'selected' : '' }}>Pilih Jenis</option>
<option value="entertainment" {{ old('jenis') == 'entertainment' ? 'selected' : '' }} >Entertainment</option>
<option value="presentation" {{ old('jenis') == 'presentation' ? 'selected' : '' }}>Presentation</option>
<option value="sponsorship" {{ old('jenis') == 'sponsorship' ? 'selected' : '' }}>Sponsorship</option>
@@ -78,9 +78,25 @@
<label class="form-label">Alamat <span class="font-italic font-weight-normal">(required)</span></label>
<input type="text" class="form-control" name="alamat" id="alamat" required value="{{ old('alamat') }}">
</div>
<div class="mb-3">
<div class="row">
<div class="col-md-4">
<label>Nama Perusahaan <span class="font-italic font-weight-normal">(required)</span></label>
<input type="text" name="nama_perusahaan" class="form-control" placeholder="Contoh: PT. Maju Jaya" required value="{{ old('nama_perusahaan') }}">
</div>
<div class="col-md-4">
<label>Jabatan <span class="font-italic font-weight-normal">(required)</span></label>
<input type="text" name="jabatan" class="form-control" placeholder="Contoh: Manager Operasional" required value="{{ old('jabatan') }}">
</div>
<div class="col-md-4">
<label>Jenis Usaha <span class="font-italic font-weight-normal">(required)</span></label>
<input type="text" name="jenis_usaha" class="form-control" placeholder="Contoh: Farmasi / Manufaktur" required value="{{ old('jenis_usaha') }}">
</div>
</div>
<div class="mb-3 mt-3">
<label class="form-label">Total <span class="font-italic font-weight-normal">(required)</span></label>
<input type="string" class="form-control" name="total" id="total" required value="{{ old('total') }}">
<input type="text" class="form-control" name="total" id="total" required value="{{ old('total') }}">
</div>
<div class="mb-3">
<label class="form-label">Keterangan <span class="font-italic font-weight-normal">(required)</span></label>
@@ -128,7 +144,7 @@
<div class="spinner-wrapper">
<div class="spinner-border text-primary" role="status">
</div>
<p>Submitting, please wait...</p>
<p class="mt-2">Submitting, please wait...</p>
</div>
</div>
</div>
@@ -137,18 +153,47 @@
</section>
<script>
new AutoNumeric('#total', {
$(document).ready(function () {
// 1. Inisialisasi AutoNumeric
const totalInput = new AutoNumeric('#total', {
digitGroupSeparator: '.', // Pemisah ribuan
decimalCharacter: ',', // Karakter desimal (tidak digunakan karena tanpa desimal)
currencySymbol: 'Rp.', // Simbol mata uang
decimalCharacter: ',', // Karakter desimal
currencySymbol: 'Rp. ', // Simbol mata uang
decimalPlaces: 0, // Tidak ada angka desimal
unformatOnSubmit: true // Nilai asli tanpa format saat dikirimkan
minimumValue: '0', // Cegah minus
unformatOnSubmit: true // Nilai asli tanpa format saat dikirimkan
});
document.getElementById('expense-form').addEventListener('submit', function (e) {
const spinnerOverlay = document.getElementById('loading-spinner-overlay');
spinnerOverlay.classList.remove('d-none'); // Show overlay
spinnerOverlay.classList.add('d-flex'); // Use flexbox for centering
// 2. Mencegah Scroll Mouse Wheel pada Input Nominal
$('#total').on('wheel font-wheel', function(e) {
e.preventDefault();
$(this).blur();
});
// 3. Interseptor Form Submit untuk Validasi Maksimal Rp 1 Juta
$('#expense-form').on('submit', function (e) {
e.preventDefault(); // Tahan pengiriman sementara
const form = this;
const spinnerOverlay = $('#loading-spinner-overlay');
// Ambil nilai nominal mentah dari AutoNumeric
const totalExpense = totalInput.getNumber() || 0;
// Cek Aturan Bisnis: Tidak Boleh Lebih dari Rp 1.000.000
if (totalExpense > 1000000) {
Swal.fire({
title: 'Nominal Melebihi Batas!',
text: `Pengajuan Anda sebesar Rp ${new Intl.NumberFormat('id-ID').format(totalExpense)} melebihi batas maksimal yang diizinkan yaitu Rp 1.000.000. Mohon sesuaikan nominal Anda.`,
icon: 'error',
confirmButtonColor: '#d33',
confirmButtonText: 'Revisi Angka'
});
} else {
// Lolos Validasi -> Tampilkan Spinner & Eksekusi Submit
spinnerOverlay.removeClass('d-none').addClass('d-flex');
form.submit();
}
});
});
</script>
@@ -6,6 +6,7 @@
@section('admin-content')
<script src="https://cdn.jsdelivr.net/npm/autonumeric@4.6.0/dist/autoNumeric.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<section class="content-header">
<div class="container-fluid">
@@ -16,12 +17,12 @@
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="{{ route('dashboard.index') }}">Dashboard</a></li>
</li>
</ol>
</div>
</div>
</div>
</section>
<style>
#loading-spinner-overlay {
position: fixed;
@@ -29,18 +30,15 @@
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5); /* Semi-transparent black background */
z-index: 9999; /* High z-index to cover everything */
background-color: rgba(0, 0, 0, 0.5);
z-index: 9999;
display: flex;
justify-content: center;
align-items: center;
}
.spinner-wrapper {
text-align: center;
color: white;
}
.spinner-wrapper { text-align: center; color: white; }
</style>
<section class="content">
<div class="container-fluid">
<div class="card card-primary card-outline">
@@ -53,7 +51,8 @@
<div class="col-lg-6">
<div class="mb-3">
<label class="form-label">Tanggal <span class="font-italic font-weight-normal">(required)</span></label>
<input type="date" class="form-control" name="tanggal" required value="{{ $form->tanggal }}">
{{-- Diubah menjadi input date murni --}}
<input type="date" class="form-control" name="tanggal" required value="{{ \Carbon\Carbon::parse($form->tanggal)->format('Y-m-d') }}">
</div>
<div class="mb-3">
<label class="form-label">Jenis <span class="font-italic font-weight-normal">(required)</span></label>
@@ -79,9 +78,25 @@
<label class="form-label">Alamat <span class="font-italic font-weight-normal">(required)</span></label>
<input type="text" class="form-control" name="alamat" id="alamat" required value="{{ $form->alamat }}">
</div>
<div class="mb-3">
<div class="row">
<div class="col-md-4">
<label>Nama Perusahaan <span class="font-italic font-weight-normal">(required)</span></label>
<input type="text" name="nama_perusahaan" class="form-control" required value="{{ old('nama_perusahaan', $form->nama_perusahaan) }}">
</div>
<div class="col-md-4">
<label>Jabatan <span class="font-italic font-weight-normal">(required)</span></label>
<input type="text" name="jabatan" class="form-control" required value="{{ old('jabatan', $form->jabatan) }}">
</div>
<div class="col-md-4">
<label>Jenis Usaha <span class="font-italic font-weight-normal">(required)</span></label>
<input type="text" name="jenis_usaha" class="form-control" required value="{{ old('jenis_usaha', $form->jenis_usaha) }}">
</div>
</div>
<div class="mb-3 mt-3">
<label class="form-label">Total <span class="font-italic font-weight-normal">(required)</span></label>
<input type="string" class="form-control" name="total" id="total" required value="{{ $form->total }}">
<input type="text" class="form-control" name="total" id="total" required value="{{ $form->total }}">
</div>
<div class="mb-3">
<label class="form-label">Keterangan <span class="font-italic font-weight-normal">(required)</span></label>
@@ -89,6 +104,7 @@
</div>
</div>
{{-- Lampiran Existing --}}
<div class="col-12">
<hr class="my-4">
<h5 class="mb-3">Lampiran Saat Ini</h5>
@@ -100,55 +116,41 @@
<th>Nama File</th>
<th style="width: 120px;" class="text-center">Preview</th>
<th style="width: 120px;" class="text-center">Download</th>
<th style="width: 100px;" class="text-center">Delete</th>
<th style="width: 100px;" class="text-center">Aksi</th>
</tr>
</thead>
<tbody>
@forelse ($attachments as $attachment)
<tr class="entertainment-attachment-row">
<td>{{ $attachment['category_label'] ?? '-' }}</td>
<td>{{ ucwords(str_replace('_', ' ', $attachment['file_category'])) }}</td>
<td>{{ $attachment['filename'] }}</td>
<td class="text-center">
<button type="button"
class="btn btn-sm btn-outline-secondary entertainment-preview-trigger"
<button type="button" class="btn btn-sm btn-outline-secondary entertainment-preview-trigger"
data-preview-type="{{ $attachment['preview_type'] }}"
data-preview-source="{{ $attachment['preview_url'] ?? '' }}"
data-download-url="{{ $attachment['download_url'] ?? '' }}"
data-filename="{{ $attachment['filename'] }}">
Preview
</button>
data-preview-source="{{ $attachment['preview_url'] }}"
data-download-url="{{ $attachment['download_url'] }}"
data-filename="{{ $attachment['filename'] }}">Preview</button>
</td>
<td class="text-center">
@if (!empty($attachment['download_url']))
<a href="{{ $attachment['download_url'] }}" class="btn btn-sm btn-outline-success" target="_blank" rel="noopener">Download</a>
@else
<span class="text-muted">Tidak tersedia</span>
@endif
<a href="{{ $attachment['download_url'] }}" class="btn btn-sm btn-outline-success" target="_blank">Download</a>
</td>
<td class="text-center">
@if (!empty($attachment['can_delete']))
<button type="button"
class="btn btn-sm btn-outline-danger entertainment-delete-existing-attachment"
<button type="button" class="btn btn-sm btn-outline-danger entertainment-delete-existing-attachment"
data-delete-url="{{ route('forms.entertainment.attachments.destroy', [$form->id, $attachment['id']]) }}">
<i class="fas fa-trash"></i>
</button>
@else
<span class="text-muted">Tidak tersedia</span>
@endif
</td>
</tr>
@empty
<tr class="entertainment-empty-row">
<td colspan="5" class="text-center text-muted">Belum ada attachment.</td>
</tr>
<tr class="text-center text-muted"><td colspan="5">Belum ada lampiran tersimpan.</td></tr>
@endforelse
</tbody>
</table>
</div>
</div>
<div class="col-12">
<hr class="my-4">
{{-- Lampiran Baru --}}
<div class="col-12 mt-4">
<div class="d-flex justify-content-between align-items-center mb-2">
<h5 class="mb-0">Tambah Lampiran Baru</h5>
<button type="button" class="btn btn-sm btn-primary" id="entertainment-add-attachment-row">
@@ -156,7 +158,7 @@
</button>
</div>
<div class="table-responsive">
<table class="table table-bordered table-striped mb-0" id="entertainment-new-attachments-table" data-next-index="0">
<table class="table table-bordered align-middle" id="entertainment-new-attachments-table">
<thead class="bg-light">
<tr>
<th style="width: 30%;">Kategori</th>
@@ -166,15 +168,10 @@
</tr>
</thead>
<tbody>
<tr class="entertainment-empty-row">
<td colspan="4" class="text-center text-muted">Belum ada attachment.</td>
</tr>
<tr class="entertainment-empty-row"><td colspan="4" class="text-center text-muted">Belum ada lampiran baru.</td></tr>
</tbody>
</table>
</div>
<small class="text-muted d-block mt-2">
Maksimum 10 MB per file. Tidak diperbolehkan: {{ implode(', ', $entertainmentBlockedExtensions ?? []) }}.
</small>
</div>
<div class="col-12 text-right mt-4">
@@ -185,8 +182,7 @@
<div id="loading-spinner-overlay" class="d-none">
<div class="spinner-wrapper">
<div class="spinner-border text-primary" role="status">
</div>
<div class="spinner-border text-primary" role="status"></div>
<p>Submitting, please wait...</p>
</div>
</div>
@@ -196,18 +192,34 @@
</section>
<script>
new AutoNumeric('#total', {
digitGroupSeparator: '.', // Pemisah ribuan
decimalCharacter: ',', // Karakter desimal (tidak digunakan karena tanpa desimal)
currencySymbol: 'Rp.', // Simbol mata uang
decimalPlaces: 0, // Tidak ada angka desimal
unformatOnSubmit: true // Nilai asli tanpa format saat dikirimkan
$(document).ready(function() {
// 1. Inisialisasi AutoNumeric Aman
const totalInput = new AutoNumeric('#total', {
digitGroupSeparator: '.',
decimalCharacter: ',',
currencySymbol: 'Rp. ',
decimalPlaces: 0,
minimumValue: '0',
unformatOnSubmit: true
});
document.getElementById('expense-form').addEventListener('submit', function (e) {
const spinnerOverlay = document.getElementById('loading-spinner-overlay');
spinnerOverlay.classList.remove('d-none'); // Show overlay
spinnerOverlay.classList.add('d-flex'); // Use flexbox for centering
// 2. Cegah Scroll Wheel pada Input
$('#total').on('wheel', function(e) { e.preventDefault(); $(this).blur(); });
// 3. Interseptor Form Submit dengan Validasi 1 Juta
$('#expense-form').on('submit', function (e) {
const totalExpense = totalInput.getNumber() || 0;
if (totalExpense > 1000000) {
e.preventDefault();
Swal.fire({
title: 'Nominal Melebihi Batas!',
text: 'Total pengajuan melebihi batas Rp 1.000.000. Mohon revisi nominal Anda.',
icon: 'error'
});
} else {
$('#loading-spinner-overlay').removeClass('d-none').addClass('d-flex');
}
});
});
</script>
@@ -6,16 +6,72 @@
@section('admin-content')
<style>
/* Style Awal Anda */
#outstandingCard {
cursor: pointer;
}
#outstandingCard.active {
box-shadow: 0 0 0 0.25rem rgba(255, 255, 255, 0.35);
}
/* Style Baru: Mini Status Cards (Seragam dengan Up Country) */
.status-card {
cursor: pointer;
transition: all 0.2s ease-in-out;
border: 1px solid rgba(0,0,0,0.08);
border-radius: 6px;
background-color: #fff;
}
.status-card:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0,0,0,0.1); }
.status-card.active-filter {
border: 2px solid #343a40;
box-shadow: 0 2px 10px rgba(0,0,0,0.15);
}
.mini-stat-box {
padding: 6px 10px;
display: flex;
align-items: center;
justify-content: space-between;
min-height: 45px;
}
.mini-stat-box h6 { margin: 0; font-weight: 800; font-size: 0.95rem; color: #333; }
.mini-stat-box p { margin: 0; font-size: 0.65rem; text-transform: uppercase; color: #777; font-weight: 600; }
/* Indikator Warna Kiri */
.status-card[data-status="On Progress"] { border-left: 3px solid #ffc107; }
.status-card[data-status="Approved 1"] { border-left: 3px solid #17a2b8; }
.status-card[data-status="Approved 2"] { border-left: 3px solid #007bff; }
.status-card[data-status="Final Approved"] { border-left: 3px solid #28a745; }
.status-card[data-status="Rejected"] { border-left: 3px solid #dc3545; }
.status-card[data-status="Closed"] { border-left: 3px solid #6c757d; }
</style>
@php
$outstandingIdCollection = collect($outstandingIds ?? []);
// ==========================================
// FILTER PERIODE BERJALAN KHUSUS UNTUK MINI CARDS
// (Sinkronisasi Angka Realtime Siklus Tanggal 11-13)
// ==========================================
$startDayIndex = (int) env('STARTING_DATE', 11);
$closingDayIndex = (int) env('CLOSING_DATE', 13);
$nowIndex = \Carbon\Carbon::now();
if ($nowIndex->day >= $startDayIndex) {
$startPeriodIndex = \Carbon\Carbon::create($nowIndex->year, $nowIndex->month, $startDayIndex)->startOfDay();
$endPeriodIndex = \Carbon\Carbon::create($nowIndex->copy()->addMonth()->year, $nowIndex->copy()->addMonth()->month, $closingDayIndex)->endOfDay();
} else {
$startPeriodIndex = \Carbon\Carbon::create($nowIndex->copy()->subMonth()->year, $nowIndex->copy()->subMonth()->month, $startDayIndex)->startOfDay();
$endPeriodIndex = \Carbon\Carbon::create($nowIndex->year, $nowIndex->month, $closingDayIndex)->endOfDay();
}
// Filter koleksi data pengajuan murni yang berada dalam rentang aktif berjalan
$currentPeriodForms = $forms->filter(function($form) use ($startPeriodIndex, $endPeriodIndex) {
$tanggalForm = \Carbon\Carbon::parse($form->tanggal);
return $tanggalForm->between($startPeriodIndex, $endPeriodIndex);
});
@endphp
<section class="content-header">
<div class="container-fluid">
<div class="row mb-2">
@@ -25,52 +81,42 @@
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="{{ route('dashboard.index') }}">Dashboard</a></li>
</li>
</ol>
</div>
</div>
</div>
</section>
<section class="content">
<div class="container-fluid">
<div class="row row-cols-1 row-cols-md-2 row-cols-xl-5 g-3">
<!-- Total Expense -->
<div class="col d-flex">
<div class="small-box bg-info flex-fill">
<div class="inner">
<h3>Rp {{ number_format($forms->sum('total'), 0, ',', '.') }}</h3>
<p>Total Expense</p>
</div>
<div class="icon">
<i class="fas fa-wallet"></i>
<div class="icon"><i class="fas fa-wallet"></i></div>
</div>
</div>
</div>
<!-- Approved Expense -->
<div class="col d-flex">
<div class="small-box bg-success flex-fill">
<div class="inner">
<h3>Rp {{ number_format($forms->whereIn('status', ['Approved', 'Closed'])->sum('approved_total'), 0, ',', '.') }}</h3>
<h3>Rp {{ number_format($forms->whereIn('status', ['Approved', 'Closed', 'Final Approved'])->sum('approved_total'), 0, ',', '.') }}</h3>
<p>Approved Expenses</p>
</div>
<div class="icon">
<i class="fas fa-check-circle"></i>
<div class="icon"><i class="fas fa-check-circle"></i></div>
</div>
</div>
</div>
<!-- Pending Expense -->
<div class="col d-flex">
<div class="small-box bg-warning flex-fill">
<div class="inner">
<h3 class="text-white">Rp {{ number_format($forms->where('status', 'On Progress')->sum('total'), 0, ',', '.') }}</h3>
<p class="text-white">Pending Expenses</p>
</div>
<div class="icon">
<i class="fas fa-hourglass-half"></i>
<div class="icon"><i class="fas fa-hourglass-half"></i></div>
</div>
</div>
</div>
<!-- Outstanding Expense -->
<div class="col d-flex">
<div class="small-box bg-danger flex-fill" id="outstandingCard">
<div class="inner">
@@ -79,9 +125,7 @@
<p class="mb-0"><small>Total Rp {{ number_format($outstandingTotal, 0, ',', '.') }}</small></p>
<p class="mb-0 filter-hint"><small>Klik untuk menampilkan detail</small></p>
</div>
<div class="icon">
<i class="fas fa-exclamation-triangle"></i>
</div>
<div class="icon"><i class="fas fa-exclamation-triangle"></i></div>
</div>
</div>
@if($availableBudget > 0)
@@ -91,15 +135,28 @@
<h3 class="text-white">Rp {{ number_format($availableBudget, 0, ',', '.') }}</h3>
<p class="text-white">Available Budget</p>
</div>
<div class="icon">
<i class="fas fa-money-bill"></i>
</div>
<div class="icon"><i class="fas fa-money-bill"></i></div>
</div>
</div>
@endif
</div>
</div>
</section>
<section class="content mt-2">
<div class="container-fluid">
<div class="row row-cols-2 row-cols-sm-3 row-cols-md-4 row-cols-xl-7 g-2 mb-3">
<div class="col"><div class="status-card active-filter" data-status="all"><div class="mini-stat-box"><div><p>Semua</p><h6>{{ $currentPeriodForms->count() }}</h6></div><i class="fas fa-list-ul text-muted"></i></div></div></div>
<div class="col"><div class="status-card" data-status="On Progress"><div class="mini-stat-box"><div><p>Progress</p><h6>{{ $currentPeriodForms->where('status', 'On Progress')->count() }}</h6></div><i class="fas fa-sync text-warning fa-xs"></i></div></div></div>
<div class="col"><div class="status-card" data-status="Approved 1"><div class="mini-stat-box"><div><p>App 1</p><h6>{{ $currentPeriodForms->where('status', 'Approved 1')->count() }}</h6></div><i class="fas fa-user-check text-info fa-xs"></i></div></div></div>
<div class="col"><div class="status-card" data-status="Approved 2"><div class="mini-stat-box"><div><p>App 2</p><h6>{{ $currentPeriodForms->where('status', 'Approved 2')->count() }}</h6></div><i class="fas fa-users text-primary fa-xs"></i></div></div></div>
<div class="col"><div class="status-card" data-status="Final Approved"><div class="mini-stat-box"><div><p>Final</p><h6>{{ $currentPeriodForms->where('status', 'Final Approved')->count() }}</h6></div><i class="fas fa-check-double text-success fa-xs"></i></div></div></div>
<div class="col"><div class="status-card" data-status="Rejected"><div class="mini-stat-box"><div><p>Reject</p><h6>{{ $currentPeriodForms->where('status', 'Rejected')->count() }}</h6></div><i class="fas fa-ban text-danger fa-xs"></i></div></div></div>
<div class="col"><div class="status-card" data-status="Closed"><div class="mini-stat-box"><div><p>Closed</p><h6>{{ $currentPeriodForms->where('status', 'Closed')->count() }}</h6></div><i class="fas fa-lock text-secondary fa-xs"></i></div></div></div>
</div>
</div>
</section>
<section class="content">
<div class="container-fluid">
<div class="card card-primary card-outline">
@@ -107,18 +164,12 @@
<h4 class="header-title float-left">List Data Forms Entertainment & Presentation</h4>
<p class="float-right mb-2">
@if (auth()->user()->can('forms.entertainment.create'))
<a class="btn btn-primary text-white" href="{{ route('forms.entertainment.create') }}">
Add New Expense Entertainment & Presentation
</a>
<a class="btn btn-primary text-white" href="{{ route('forms.entertainment.create') }}">Add New Expense</a>
@endif
</p>
<div class="clearfix"></div>
<div class="dataTables_wrapper dt-bootstrap4 mt-2">
@include('backend.layouts.partials.messages')
<div class="table-responsive">
<table id="dataTable"
class="table table-bordered table-striped table-head-fixed dataTable dtr-inline"
style="width:100%">
<table id="dataTable" class="table table-bordered table-striped table-head-fixed dataTable dtr-inline" style="width:100%">
<thead class="bg-light text-capitalize">
<tr>
<th>#</th>
@@ -134,25 +185,20 @@
<th>Bukti</th>
<th>Account Number</th>
<th>Status</th>
@if (auth()->user()->can('approval.view'))
<th>Approval 1</th>
@endif
@if (auth()->user()->can('approval2.view'))
<th>Approval 2</th>
@endif
@if (auth()->user()->can('final_approval.view'))
<th>Final Approval</th>
@endif
@if (auth()->user()->can('approval.view')) <th>Approval 1</th> @endif
@if (auth()->user()->can('approval2.view')) <th>Approval 2</th> @endif
@if (auth()->user()->can('final_approval.view')) <th>Final Approval</th> @endif
<th>Aksi</th>
</tr>
</thead>
<tbody>
@php
// SUNTIKAN PERBAIKAN: Impor Class Helper NextCloud ke dalam scope Blade View
use App\Helpers\NextCloudHelper;
$role = auth()->user()->getRoleNames()[0];
@endphp
@foreach ($forms as $item)
<tr data-outstanding="{{ $outstandingIdCollection->contains($item->id) ? 1 : 0 }}">
<tr data-id="{{ $item->id }}" data-status="{{ $item->status }}" data-date="{{ \Carbon\Carbon::parse($item->tanggal)->format('Y-m-d') }}" data-outstanding="{{ $outstandingIdCollection->contains($item->id) ? '1' : '0' }}">
<td>{{ $loop->iteration }}</td>
<td>{{ $item->expense_number }}</td>
<td>{{ $item->user->name }}</td>
@@ -165,132 +211,67 @@
<td>{{ number_format($item->approved_total, 0, ',', '.') }}</td>
<td>
@if ($item->bukti_total)
<a href="{{ NextCloudHelper::getFileUrl($item->bukti_total) }}" target="_blank">
Download
</a>
@else
-
@endif
<a href="{{ NextCloudHelper::getFileUrl($item->bukti_total) }}" target="_blank">Download</a>
@else - @endif
</td>
<td>{{ $item->account_number }}</td>
<td>
@if ($item->status == 'On Progress')
<span class="badge badge-warning text-white">{{ $item->status }}</span>
@elseif ($item->status == 'Approved 1' || $item->status == 'Approved 2' || $item->status == 'Final Approved')
@elseif (in_array($item->status, ['Approved 1', 'Approved 2', 'Final Approved']))
<span class="badge badge-success text-white">{{ $item->status }}</span>
@else
<span class="badge badge-danger text-white">{{ $item->status }}</span>
@endif
</td>
@if (auth()->user()->can('approval.view'))
<td>
{{-- approve / reject button --}}
@if (auth()->user()->can('approval.approve'))
@if ($item->status == 'On Progress')
<form action="javascript:void(0);" class="d-inline">
<button type="button"
class="btn btn-success btn-sm final-approve-modal"
data-id="{{ route('forms.entertainment.approve', $item->id) }}">
Approve 1
</button>
</form>
<button type="button"
class="btn btn-danger btn-sm open-reject-modal"
data-action="{{ route('forms.entertainment.reject', $item->id) }}">
Reject
</button>
@endif
@endif
@if ($item->approved_at)
{{ \Carbon\Carbon::parse($item->approved_at)->locale('id')->isoFormat('D MMMM Y HH:mm') }}
<div class="d-none">
@if (auth()->user()->can('approval.approve') && $item->status == 'On Progress')
<button type="button" class="btn btn-success btn-sm final-approve-modal" data-id="{{ route('forms.entertainment.approve', $item->id) }}">Approve 1</button>
<button type="button" class="btn btn-danger btn-sm open-reject-modal" data-action="{{ route('forms.entertainment.reject', $item->id) }}">Reject</button>
@endif
</div>
@if ($item->approved_at) {{ \Carbon\Carbon::parse($item->approved_at)->format('d/m/y H:i') }} @endif
</td>
@endif
@if (auth()->user()->can('approval2.view'))
<td>
{{-- approve / reject button --}}
@if (auth()->user()->can('approval2.approve'))
@if ($item->status == 'Approved 1' && $item->approved_at)
<form action="javascript:void(0);" class="d-inline">
<button type="button"
class="btn btn-success btn-sm open-approve-modal"
data-id="{{ route('forms.entertainment.approve2', $item->id) }}">
Approve 2
</button>
</form>
<button type="button"
class="btn btn-danger btn-sm open-reject-modal"
data-action="{{ route('forms.entertainment.reject', $item->id) }}">
Reject
</button>
@endif
@endif
@if ($item->approved2_at)
{{ \Carbon\Carbon::parse($item->approved2_at)->locale('id')->isoFormat('D MMMM Y HH:mm') }}
<div class="d-none">
@if (auth()->user()->can('approval2.approve') && $item->status == 'Approved 1' && $item->approved_at)
<button type="button" class="btn btn-success btn-sm open-approve-modal" data-id="{{ route('forms.entertainment.approve2', $item->id) }}">Approve 2</button>
<button type="button" class="btn btn-danger btn-sm open-reject-modal" data-action="{{ route('forms.entertainment.reject', $item->id) }}">Reject</button>
@endif
</div>
@if ($item->approved2_at) {{ \Carbon\Carbon::parse($item->approved2_at)->format('d/m/y H:i') }} @endif
</td>
@endif
@if(auth()->user()->can('final_approval.view'))
<td>
{{-- approve / reject button --}}
<div class="d-none">
@if(auth()->user()->can('final_approval.approve'))
@if ($item->status == 'Approved 2' && $item->approved2_at && !$item->final_approved_at)
<form action="javascript:void(0);" class="d-inline">
<button type="button"
class="btn btn-success btn-sm open-approve-modal"
data-id="{{ route('forms.entertainment.final-approve', $item->id) }}">
Final Approve
</button>
</form>
@else
@if ($item->final_approved_at && auth()->user()->can('final_approval.open'))
<form action="{{ route('forms.entertainment.open', $item->id) }}" method="POST" class="d-inline" onsubmit="return confirm('Are you sure you want to approve this item?');">
@csrf
@method('PUT')
<button type="submit" class="btn btn-success btn-sm">
Open
</button>
<button type="button" class="btn btn-success btn-sm open-approve-modal" data-id="{{ route('forms.entertainment.final-approve', $item->id) }}">Final Approve</button>
<button type="button" class="btn btn-danger btn-sm open-reject-modal" data-action="{{ route('forms.entertainment.reject', $item->id) }}">Reject</button>
@elseif ($item->final_approved_at && auth()->user()->can('final_approval.open'))
<form action="{{ route('forms.entertainment.open', $item->id) }}" method="POST" class="d-inline" onsubmit="return confirm('Open item?');">
@csrf @method('PUT') <button type="submit" class="btn btn-success btn-sm">Open</button>
</form>
@endif
@endif
@endif
@if ($item->final_approved_at)
{{ \Carbon\Carbon::parse($item->final_approved_at)->locale('id')->isoFormat('D MMMM Y HH:mm') }}
@endif
</div>
@if ($item->final_approved_at) {{ \Carbon\Carbon::parse($item->final_approved_at)->format('d/m/y H:i') }} @endif
</td>
@endif
<td>
@if (auth()->user()->can('forms.entertainment.view'))
<a href="{{ route('forms.entertainment.view', $item->id) }}" class="btn btn-primary btn-sm">
<i class="fas fa-eye text-white"></i>
</a>
@endif
@php
$canEdit = false;
if (auth()->user()->can('forms.entertainment.edit')) {
if (in_array($role, ['Medical Representatif', 'Area Manager Cabang'])) {
$canEdit = $item->status == 'On Progress';
} else {
$canEdit = true;
}
}
@endphp
@if ($canEdit)
<a href="{{ route('forms.entertainment.edit', $item->id) }}" class="btn btn-warning btn-sm">
<i class="fas fa-edit text-white"></i>
</a>
@endif
@if (auth()->user()->can('forms.entertainment.view')) <a href="{{ route('forms.entertainment.view', $item->id) }}" class="btn btn-primary btn-sm"><i class="fas fa-eye text-white"></i></a> @endif
@php $canEdit = auth()->user()->can('forms.entertainment.edit') && (!in_array($role, ['Medical Representatif', 'Area Manager Cabang']) || $item->status == 'On Progress'); @endphp
@if ($canEdit) <a href="{{ route('forms.entertainment.edit', $item->id) }}" class="btn btn-warning btn-sm"><i class="fas fa-edit text-white"></i></a> @endif
@if (($role != 'Medical Representatif' && auth()->user()->can('forms.entertainment.delete')) || ($role == 'Medical Representatif' && $item->status == 'On Progress'))
<form action="{{ route('forms.entertainment.destroy', $item->id) }}" method="POST" class="d-inline" onsubmit="return confirm('Are you sure you want to delete this item?');">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-danger btn-sm">
<i class="fas fa-trash"></i>
</button>
<form action="{{ route('forms.entertainment.destroy', $item->id) }}" method="POST" class="d-inline" onsubmit="return confirm('Delete item?');">
@csrf @method('DELETE') <button type="submit" class="btn btn-danger btn-sm"><i class="fas fa-trash"></i></button>
</form>
@endif
</td>
@@ -302,382 +283,203 @@
</div>
</div>
</div>
</div>
</section>
<section>
<div class="modal fade" id="approveModal" tabindex="-1" role="dialog" aria-labelledby="approveModalLabel" aria-hidden="true">
{{-- MODAL CONTAINER DI BAWAH (TETAP UTUH UNTUK KEBUTUHAN AJAX ROUTING) --}}
<div class="modal fade" id="approveModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<form id="approveForm" method="POST" action="">
@csrf
@method('PUT')
<div class="modal-header">
<h5 class="modal-title" id="approveModalLabel">Approve Item</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
@csrf @method('PUT')
<div class="modal-header"><h5 class="modal-title">Approve Item</h5><button type="button" class="close" data-dismiss="modal">&times;</button></div>
<div class="modal-body">
<!-- Loading spinner -->
<div id="loadingSpinner" class="d-flex justify-content-center align-items-center" style="height: 200px;">
<div class="spinner-border text-primary" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
<!-- Modal content (hidden initially) -->
<div id="loadingSpinner" class="d-flex justify-content-center align-items-center" style="height: 200px;"><div class="spinner-border text-primary" role="status"></div></div>
<div id="modalContent" class="d-none">
<p>Please review the details below:</p>
<ul class="list-group">
<li class="list-group-item">
<strong>Expense Number:</strong> <span id="expense_number"></span>
</li>
<li class="list-group-item">
<strong>User:</strong> <span id="user"></span>
</li>
<li class="list-group-item">
<strong>NIK/NPWP Penerima:</strong> <span id="nik_or_npwp"></span>
</li>
<li class="list-group-item">
<strong>Tanggal:</strong> <span id="tanggal"></span>
</li>
<li class="list-group-item">
<strong>Nama Penerima:</strong> <span id="name"></span>
</li>
<li class="list-group-item">
<strong>Jenis:</strong> <span id="jenis"></span>
</li>
<li class="list-group-item">
<strong>Keterangan:</strong> <span id="keterangan"></span>
</li>
<li class="list-group-item">
<strong>Bukti Total:</strong> <span id="bukti_total"></span>
</li>
<li class="list-group-item">
<strong>Nominal Total:</strong> <span id="total_value"></span>
</li>
<li class="list-group-item"><strong>No Expense:</strong> <span id="expense_number"></span></li>
<li class="list-group-item"><strong>User:</strong> <span id="user"></span></li>
<li class="list-group-item"><strong>NIK/NPWP:</strong> <span id="nik_or_npwp"></span></li>
<li class="list-group-item"><strong>Penerima:</strong> <span id="name"></span></li>
<li class="list-group-item"><strong>Jenis:</strong> <span id="jenis"></span></li>
<li class="list-group-item"><strong>Nominal:</strong> <span id="total_value"></span></li>
</ul>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Approve</button>
</div>
<div class="modal-footer"><button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button><button type="submit" class="btn btn-primary">Approve</button></div>
</form>
</div>
</div>
</div>
</section>
<section>
<div class="modal fade" id="finalApproveModal" tabindex="-1" role="dialog" aria-labelledby="finalApproveModalLabel" aria-hidden="true">
<div class="modal fade" id="finalApproveModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<form id="finalApproveForm" method="POST" action="">
@csrf
@method('PUT')
<div class="modal-header">
<h5 class="modal-title" id="approveModalLabel">Approve Item</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
@csrf @method('PUT')
<div class="modal-header"><h5 class="modal-title">Final Approve Item</h5><button type="button" class="close" data-dismiss="modal">&times;</button></div>
<div class="modal-body">
<!-- Loading spinner -->
<div id="finalLoadingSpinner" class="d-flex justify-content-center align-items-center" style="height: 200px;">
<div class="spinner-border text-primary" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
<!-- Modal content (hidden initially) -->
<div id="finalLoadingSpinner" class="d-flex justify-content-center align-items-center" style="height: 200px;"><div class="spinner-border text-primary" role="status"></div></div>
<div id="finalModalContent" class="d-none">
<p>Please review the details below:</p>
<ul class="list-group">
<li class="list-group-item">
<strong>Expense Number:</strong> <span id="final_expense_number"></span>
</li>
<li class="list-group-item">
<strong>User:</strong> <span id="final_user"></span>
</li>
<li class="list-group-item">
<strong>NIK/NPWP Penerima:</strong> <span id="final_nik_or_npwp"></span>
</li>
<li class="list-group-item">
<strong>Tanggal:</strong> <span id="final_tanggal"></span>
</li>
<li class="list-group-item">
<strong>Nama Penerima:</strong> <span id="final_name"></span>
</li>
<li class="list-group-item">
<strong>Jenis:</strong> <span id="final_jenis"></span>
</li>
<li class="list-group-item">
<strong>Keterangan:</strong> <span id="final_keterangan"></span>
</li>
<li class="list-group-item">
<strong>Bukti Total:</strong> <span id="final_bukti_total"></span>
</li>
<ul class="list-group mb-3">
<li class="list-group-item"><strong>NIK/NPWP:</strong> <span id="final_nik_or_npwp"></span></li>
<li class="list-group-item"><strong>Penerima:</strong> <span id="final_name"></span></li>
</ul>
<p class="mt-2">Please select the items you want to approve:</p>
<ul class="list-group list-unstyled">
<li>
<div class="form-group">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="final_total" name="total" required>
<label class="custom-control-label" for="final_total">Total (<span id="final_total_value"></span>)</label>
<div class="custom-control custom-checkbox mb-3"><input type="checkbox" class="custom-control-input" id="final_total" name="total" required><label class="custom-control-label" for="final_total">Total (<span id="final_total_value"></span>)</label></div>
<div class="form-group"><label>Total Approved</label><input type="text" class="form-control font-weight-bold" id="total" name="total" value="0" readonly></div>
</div>
</div>
</li>
</ul>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Approve</button>
</div>
<div class="modal-footer"><button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button><button type="submit" class="btn btn-primary">Approve</button></div>
</form>
</div>
</div>
</div>
</section>
<section>
<div class="modal fade" id="rejectModal" tabindex="-1" role="dialog" aria-labelledby="rejectModalLabel" aria-hidden="true">
<div class="modal fade" id="rejectModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<form id="rejectForm" method="POST" action="">
@csrf
@method('PUT')
<div class="modal-header">
<h5 class="modal-title" id="rejectModalLabel">Reject Expense</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
@csrf @method('PUT')
<div class="modal-header"><h5 class="modal-title">Reject Expense</h5><button type="button" class="close" data-dismiss="modal">&times;</button></div>
<div class="modal-body">
<div class="form-group">
<label for="rejectRemarks">Remarks <span class="text-danger">*</span></label>
<textarea class="form-control" name="remarks" id="rejectRemarks" rows="3" required></textarea>
</div>
<p class="mb-0 text-muted small">Pengajuan akan ditolak setelah Anda mengirimkan catatan ini.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-danger">Reject</button>
<div class="form-group"><label>Remarks <span class="text-danger">*</span></label><textarea class="form-control" name="remarks" id="rejectRemarks" rows="3" required></textarea></div>
</div>
<div class="modal-footer"><button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button><button type="submit" class="btn btn-danger">Reject</button></div>
</form>
</div>
</div>
</div>
</section>
@endsection
@section('scripts')
<script>
<script>
$(document).ready(function () {
// Uncheck all checkboxes on page refresh
$('.custom-control-input').prop('checked', false);
if ($('#dataTable').length) {
let outstandingFilterActive = false;
const outstandingFilter = function(settings, data, dataIndex) {
if (!outstandingFilterActive) {
return true;
}
const row = settings.aoData[dataIndex].nTr;
return $(row).data('outstanding') === 1 || $(row).data('outstanding') === '1';
};
let statusFilterActive = 'all';
$.fn.dataTable.ext.search.push(outstandingFilter);
// Custom Search DataTables (Filter Sinkron: Status + Outstanding + Rentang Tanggal 11 - 13)
$.fn.dataTable.ext.search.push(function(settings, data, dataIndex) {
const row = $(settings.aoData[dataIndex].nTr);
const rowStatus = row.attr('data-status');
const isOutstanding = row.attr('data-outstanding') === '1' || row.attr('data-outstanding') === 1;
const rowDateStr = row.attr('data-date'); // Format: YYYY-MM-DD
// 1. Outstanding Filter Terpilih (Abaikan Tanggal Berjalan)
if (outstandingFilterActive) {
return isOutstanding;
}
// 2. Pembatasan Jendela Waktu Siklus Aktif 11 s/d 13 Bulanan (Berlaku Mutlak)
if (rowDateStr) {
const dateParts = rowDateStr.split('-');
const rowDate = new Date(dateParts[0], dateParts[1] - 1, dateParts[2], 12, 0, 0); // Kunci aman timezone
const now = new Date();
let startPeriod, endPeriod;
const startDay = 11;
const closingDay = 13;
if (now.getDate() >= startDay) {
startPeriod = new Date(now.getFullYear(), now.getMonth(), startDay, 0, 0, 0);
endPeriod = new Date(now.getFullYear(), now.getMonth() + 1, closingDay, 23, 59, 59);
} else {
startPeriod = new Date(now.getFullYear(), now.getMonth() - 1, startDay, 0, 0, 0);
endPeriod = new Date(now.getFullYear(), now.getMonth(), closingDay, 23, 59, 59);
}
if (rowDate < startPeriod || rowDate > endPeriod) {
return false;
}
}
// 3. Filter Kategori Status Card
if (statusFilterActive !== 'all' && rowStatus !== statusFilterActive) {
return false;
}
return true;
});
const table = $('#dataTable').DataTable({
responsive: false,
dom: 'Blfrtip',
lengthMenu: [ [10, 50, 100, -1], [10, 50, 100, "All"] ], // Control the entries dropdown
buttons: [
{
extend: 'excel',
text: 'Excel'
},
{
extend: 'pdf',
text: 'PDF',
orientation: 'landscape',
pageSize: 'A4',
exportOptions: {
columns: ':visible'
}
},
{
extend: 'print',
text: 'Print',
orientation: 'landscape',
exportOptions: {
columns: ':visible'
}
},
'colvis'
]
lengthMenu: [ [10, 50, 100, -1], [10, 50, 100, "All"] ],
buttons: [ { extend: 'excel', text: 'Excel' }, { extend: 'pdf', text: 'PDF', orientation: 'landscape' }, 'colvis' ]
});
const $outstandingCard = $('#outstandingCard');
const updateCardHint = (isActive) => {
const text = isActive ? 'Klik untuk menampilkan semua data' : 'Klik untuk menampilkan detail';
$outstandingCard.find('.filter-hint small').text(text);
};
updateCardHint(outstandingFilterActive);
// Klik Mini Status Card
$('.status-card').on('click', function() {
outstandingFilterActive = false;
$('#outstandingCard').removeClass('active');
$('#outstandingCard').find('.filter-hint small').text('Klik untuk detail');
$('.status-card').removeClass('active-filter');
$(this).addClass('active-filter');
statusFilterActive = $(this).data('status');
table.draw();
});
// Klik Outstanding Card Merah
const $outstandingCard = $('#outstandingCard');
$outstandingCard.on('click', function () {
outstandingFilterActive = !outstandingFilterActive;
$outstandingCard.toggleClass('active', outstandingFilterActive);
$(this).toggleClass('active', outstandingFilterActive);
if(outstandingFilterActive) {
$('.status-card').removeClass('active-filter');
$('.status-card[data-status="all"]').addClass('active-filter');
statusFilterActive = 'all';
}
table.draw();
updateCardHint(outstandingFilterActive);
$(this).find('.filter-hint small').text(outstandingFilterActive ? 'Tampilkan semua data' : 'Klik untuk detail');
});
}
// Handle Reject Button Click
$(document).on('click', '.open-reject-modal', function () {
const rejectUrl = $(this).data('action');
$('#rejectForm').attr('action', rejectUrl);
$('#rejectRemarks').val('');
$('#rejectForm').attr('action', $(this).data('action'));
$('#rejectRemarks').val('').focus();
$('#rejectModal').modal('show');
});
$('#rejectModal').on('shown.bs.modal', function () {
$('#rejectRemarks').trigger('focus');
});
$('#rejectModal').on('hidden.bs.modal', function () {
$('#rejectForm').attr('action', '');
$('#rejectRemarks').val('');
});
// Handle Approve Button Click
$(document).on('click', '.open-approve-modal', function() {
const approveUrl = $(this).data('id');
// Show spinner and hide content initially
$('#loadingSpinner').show();
$('#modalContent').addClass('d-none');
$('#approveForm').attr('action', approveUrl); // Set the form action
$('#loadingSpinner').addClass('d-flex');
$('#approveModal').modal('show'); // Show the modal
// Get detail from /forms/entertainment/detail/{id}
$.get(approveUrl.replace(/approve2|final-approve/g, 'detail'), function (data) {
const formatter = new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
});
$('#total_value').text(formatter.format(data.total));
// Detail data
$('#user').text(data.user.name);
$('#expense_number').text(data.expense_number);
$('#tanggal').text(new Date(data.created_at).toLocaleDateString('id-ID', {
year: 'numeric',
month: 'long',
day: 'numeric'
}));
$('#nik_or_npwp').text(data.nik_or_npwp);
$('#name').text(data.name);
$('#jenis').text(data.jenis);
$('#keterangan').text(data.keterangan);
$('#bukti_total').html(data.total ? '<a href="' + data.total + '" target="_blank">Download</a>' : '-');
// Hide spinner and show content
$('#loadingSpinner').hide();
$('#loadingSpinner').removeClass('d-flex');
$('#loadingSpinner').addClass('d-none');
$('#modalContent').removeClass('d-none');
// Reset the total to zero
$('#total').val(0);
const url = $(this).data('id');
$('#loadingSpinner').show(); $('#modalContent').addClass('d-none');
$('#approveForm').attr('action', url); $('#approveModal').modal('show');
$.get(url.replace(/approve2|final-approve/g, 'detail'), function (data) {
const fmt = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 });
$('#total_value').text(fmt.format(data.total));
$('#user').text(data.user.name); $('#expense_number').text(data.expense_number);
$('#nik_or_npwp').text(data.nik_or_npwp); $('#name').text(data.name);
$('#jenis').text(data.jenis); $('#keterangan').text(data.keterangan);
$('#loadingSpinner').hide(); $('#modalContent').removeClass('d-none');
});
});
// Handle Approve Button Click
$(document).on('click', '.final-approve-modal', function() {
const approveUrl = $(this).data('id');
// Show spinner and hide content initially
$('#finalLoadingSpinner').show();
$('#finalModalContent').addClass('d-none');
$('#finalApproveForm').attr('action', approveUrl); // Set the form action
$('#finalLoadingSpinner').addClass('d-flex');
$('#finalApproveModal').modal('show'); // Show the modal
// Get detail from /forms/entertainment/detail/{id}
$.get(approveUrl.replace('approve', 'detail'), function (data) {
console.log(data);
const formatter = new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
});
$('#final_total_value').text(formatter.format(data.total));
// Assign data values to checkboxes
const url = $(this).data('id');
$('#finalLoadingSpinner').show(); $('#finalModalContent').addClass('d-none');
$('#finalApproveForm').attr('action', url); $('#finalApproveModal').modal('show');
$.get(url.replace('approve', 'detail'), function (data) {
const fmt = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 });
$('#final_total_value').text(fmt.format(data.total));
$('#final_total').data('value', data.total);
// Detail data
$('#final_user').text(data.user.name);
$('#final_expense_number').text(data.expense_number);
$('#final_tanggal').text(new Date(data.created_at).toLocaleDateString('id-ID', {
year: 'numeric',
month: 'long',
day: 'numeric'
}));
$('#final_nik_or_npwp').text(data.nik_or_npwp);
$('#final_name').text(data.name);
$('#final_jenis').text(data.jenis);
$('#final_keterangan').text(data.keterangan);
$('#final_bukti_total').html(data.bukti_total ? '<a href="' + data.bukti_total + '" target="_blank">Download</a>' : '-');
// Hide spinner and show content
$('#finalLoadingSpinner').hide();
$('#finalLoadingSpinner').removeClass('d-flex');
$('#finalLoadingSpinner').addClass('d-none');
$('#finalModalContent').removeClass('d-none');
// Reset the total to zero
$('#final_nik_or_npwp').text(data.nik_or_npwp); $('#final_name').text(data.name);
$('#finalLoadingSpinner').hide(); $('#finalModalContent').removeClass('d-none');
$('#total').val(0);
});
});
// Update total dynamically
$('.custom-control-input').on('change', function () {
let total = 0;
$('.custom-control-input:checked').each(function () {
total += parseFloat($(this).data('value') || 0);
$('.custom-control-input:checked').each(function () { total += parseFloat($(this).data('value') || 0); });
const fmt = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 });
$('#total').val(fmt.format(total));
});
const formatter = new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0,
$('#finalApproveModal').on('hidden.bs.modal', function () { $('.custom-control-input').prop('checked', false); $('#total').val('0'); });
});
$('#total').val(formatter.format(total));
});
// Uncheck all checkboxes when the modal is closed
$('#finalApproveModal').on('hidden.bs.modal', function () {
$('.custom-control-input').prop('checked', false); // Uncheck all checkboxes
$('#total').val('0'); // Reset total to 0
});
});
</script>
</script>
@endsection
@@ -55,6 +55,10 @@
<label class="form-label">NPWP / NIK</label>
<input type="text" class="form-control" name="nik_or_npwp" id="nik_or_npwp" readonly value="{{ $form->nik_or_npwp }}">
</div>
<div class="mb-3">
<label class="form-label">Nama Perusahaan</label>
<input type="text" class="form-control" name="nama_perusahaan" id="nama_perusahaan" readonly value="{{ $form->nama_perusahaan }}">
</div>
</div>
<div class="col-lg-6">
@@ -62,6 +66,14 @@
<label class="form-label">Alamat</label>
<input type="text" class="form-control" name="alamat" id="alamat" readonly value="{{ $form->alamat }}">
</div>
<div class="mb-3">
<label class="form-label">Jabatan</label>
<input type="text" class="form-control" name="jabatan" id="jabatan" readonly value="{{ $form->jabatan }}">
</div>
<div class="mb-3">
<label class="form-label">Jenis Usaha</label>
<input type="text" class="form-control" name="jenis_usaha" id="jenis_usaha" readonly value="{{ $form->jenis_usaha }}">
</div>
<div class="mb-3">
<label class="form-label">Total</label>
<input type="string" class="form-control" name="total" id="total" readonly value="{{ $form->total }}">
@@ -167,40 +179,47 @@
</button>
</div>
<div class="modal-body">
<!-- Loading spinner -->
<div id="loadingSpinner" class="d-flex justify-content-center align-items-center" style="height: 200px;">
<div class="spinner-border text-primary" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
<!-- Modal content (hidden initially) -->
<div id="modalContent" class="d-none">
<p>Please review the details below:</p>
<ul class="list-group">
<li class="list-group-item">
<strong>Expense Number:</strong> <span id="expense_number"></span>
<strong>Expense Number:</strong> <span id="modal_expense_number"></span>
</li>
<li class="list-group-item">
<strong>User:</strong> <span id="user"></span>
<strong>User:</strong> <span id="modal_user"></span>
</li>
<li class="list-group-item">
<strong>NIK/NPWP Penerima:</strong> <span id="nik_or_npwp"></span>
<strong>NIK/NPWP Penerima:</strong> <span id="modal_nik_or_npwp"></span>
</li>
<li class="list-group-item">
<strong>Tanggal:</strong> <span id="tanggal"></span>
<strong>Nama Perusahaan:</strong> <span id="modal_nama_perusahaan"></span>
</li>
<li class="list-group-item">
<strong>Nama Penerima:</strong> <span id="name"></span>
<strong>Jabatan:</strong> <span id="modal_jabatan"></span>
</li>
<li class="list-group-item">
<strong>Jenis:</strong> <span id="jenis"></span>
<strong>Jenis Usaha:</strong> <span id="modal_jenis_usaha"></span>
</li>
<li class="list-group-item">
<strong>Keterangan:</strong> <span id="keterangan"></span>
<strong>Tanggal:</strong> <span id="modal_tanggal"></span>
</li>
<li class="list-group-item">
<strong>Bukti Total:</strong> <span id="bukti_total"></span>
<strong>Nama Penerima:</strong> <span id="modal_name"></span>
</li>
<li class="list-group-item">
<strong>Jenis:</strong> <span id="modal_jenis"></span>
</li>
<li class="list-group-item">
<strong>Keterangan:</strong> <span id="modal_keterangan"></span>
</li>
<li class="list-group-item">
<strong>Bukti Total:</strong> <span id="modal_bukti_total"></span>
</li>
<li class="list-group-item">
<strong>Nominal Total:</strong> <span id="total_value"></span>
@@ -226,20 +245,18 @@
@csrf
@method('PUT')
<div class="modal-header">
<h5 class="modal-title" id="approveModalLabel">Approve Item</h5>
<h5 class="modal-title" id="finalApproveModalLabel">Approve Item</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<!-- Loading spinner -->
<div id="finalLoadingSpinner" class="d-flex justify-content-center align-items-center" style="height: 200px;">
<div class="spinner-border text-primary" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
<!-- Modal content (hidden initially) -->
<div id="finalModalContent" class="d-none">
<p>Please review the details below:</p>
<ul class="list-group">
@@ -252,6 +269,15 @@
<li class="list-group-item">
<strong>NIK/NPWP Penerima:</strong> <span id="final_nik_or_npwp"></span>
</li>
<li class="list-group-item">
<strong>Nama Perusahaan:</strong> <span id="final_nama_perusahaan"></span>
</li>
<li class="list-group-item">
<strong>Jabatan:</strong> <span id="final_jabatan"></span>
</li>
<li class="list-group-item">
<strong>Jenis Usaha:</strong> <span id="final_jenis_usaha"></span>
</li>
<li class="list-group-item">
<strong>Tanggal:</strong> <span id="final_tanggal"></span>
</li>
@@ -329,7 +355,7 @@
decimalCharacter: ',', // Karakter desimal (tidak digunakan karena tanpa desimal)
currencySymbol: 'Rp.', // Simbol mata uang
decimalPlaces: 0, // Tidak ada angka desimal
unformatOnSubmit: true // Nilai asli tanpa format saat dikirimkan
unformatOnSubmit: true // Nilai asli tanpa format saat dikirimkan
});
</script>
@@ -372,20 +398,26 @@
$('#total_value').text(formatter.format(data.total));
// Detail data
$('#user').text(data.user.name);
$('#expense_number').text(data.expense_number);
$('#tanggal').text(new Date(data.created_at).toLocaleDateString('id-ID', {
$('#modal_user').text(data.user.name);
$('#modal_expense_number').text(data.expense_number);
$('#modal_tanggal').text(new Date(data.created_at).toLocaleDateString('id-ID', {
year: 'numeric',
month: 'long',
day: 'numeric'
}));
$('#nik_or_npwp').text(data.nik_or_npwp);
$('#name').text(data.name);
$('#jenis').text(data.jenis);
$('#keterangan').text(data.keterangan);
$('#modal_nik_or_npwp').text(data.nik_or_npwp);
$('#bukti_total').html(data.total ? '<a href="' + data.total + '" target="_blank">Download</a>' : '-');
// Tambahan Mapping Data Perusahaan
$('#modal_nama_perusahaan').text(data.nama_perusahaan || '-');
$('#modal_jabatan').text(data.jabatan || '-');
$('#modal_jenis_usaha').text(data.jenis_usaha || '-');
$('#modal_name').text(data.name);
$('#modal_jenis').text(data.jenis);
$('#modal_keterangan').text(data.keterangan);
$('#modal_bukti_total').html(data.total ? '<a href="' + data.total + '" target="_blank">Download</a>' : '-');
// Hide spinner and show content
$('#loadingSpinner').hide();
@@ -434,11 +466,16 @@
}));
$('#final_nik_or_npwp').text(data.nik_or_npwp);
// Tambahan Mapping Data Perusahaan
$('#final_nama_perusahaan').text(data.nama_perusahaan || '-');
$('#final_jabatan').text(data.jabatan || '-');
$('#final_jenis_usaha').text(data.jenis_usaha || '-');
$('#final_name').text(data.name);
$('#final_jenis').text(data.jenis);
$('#final_keterangan').text(data.keterangan);
$('#final_bukti_total').html(data.bukti_total ? '<a href="' + data.bukti_total + '" target="_blank">Download</a>' : '-');
// Hide spinner and show content
@@ -468,7 +505,6 @@
$('#total').val(formatter.format(total));
});
// Uncheck all checkboxes when the modal is closed
$('#finalApproveModal').on('hidden.bs.modal', function () {
$('.custom-control-input').prop('checked', false); // Uncheck all checkboxes
@@ -6,16 +6,72 @@
@section('admin-content')
<style>
/* Style Awal Anda */
#outstandingCard {
cursor: pointer;
}
#outstandingCard.active {
box-shadow: 0 0 0 0.25rem rgba(255, 255, 255, 0.35);
}
/* Style Baru: Mini Status Cards (Seragam dengan Up Country) */
.status-card {
cursor: pointer;
transition: all 0.2s ease-in-out;
border: 1px solid rgba(0,0,0,0.08);
border-radius: 6px;
background-color: #fff;
}
.status-card:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0,0,0,0.1); }
.status-card.active-filter {
border: 2px solid #343a40;
box-shadow: 0 2px 10px rgba(0,0,0,0.15);
}
.mini-stat-box {
padding: 6px 10px;
display: flex;
align-items: center;
justify-content: space-between;
min-height: 45px;
}
.mini-stat-box h6 { margin: 0; font-weight: 800; font-size: 0.95rem; color: #333; }
.mini-stat-box p { margin: 0; font-size: 0.65rem; text-transform: uppercase; color: #777; font-weight: 600; }
/* Indikator Warna Kiri */
.status-card[data-status="On Progress"] { border-left: 3px solid #ffc107; }
.status-card[data-status="Approved 1"] { border-left: 3px solid #17a2b8; }
.status-card[data-status="Approved 2"] { border-left: 3px solid #007bff; }
.status-card[data-status="Final Approved"] { border-left: 3px solid #28a745; }
.status-card[data-status="Rejected"] { border-left: 3px solid #dc3545; }
.status-card[data-status="Closed"] { border-left: 3px solid #6c757d; }
</style>
@php
$outstandingIdCollection = collect($outstandingIds ?? []);
// ==========================================
// FILTER PERIODE BERJALAN KHUSUS UNTUK MINI CARDS
// (Agar Angka sinkron dengan tabel filter JS di bawah)
// ==========================================
$startDayIndex = (int) env('STARTING_DATE', 11);
$closingDayIndex = (int) env('CLOSING_DATE', 13);
$nowIndex = \Carbon\Carbon::now();
if ($nowIndex->day >= $startDayIndex) {
$startPeriodIndex = \Carbon\Carbon::create($nowIndex->year, $nowIndex->month, $startDayIndex)->startOfDay();
$endPeriodIndex = \Carbon\Carbon::create($nowIndex->copy()->addMonth()->year, $nowIndex->copy()->addMonth()->month, $closingDayIndex)->endOfDay();
} else {
$startPeriodIndex = \Carbon\Carbon::create($nowIndex->copy()->subMonth()->year, $nowIndex->copy()->subMonth()->month, $startDayIndex)->startOfDay();
$endPeriodIndex = \Carbon\Carbon::create($nowIndex->year, $nowIndex->month, $closingDayIndex)->endOfDay();
}
// Penampung data pengajuan MURNI HANYA DI PERIODE BERJALAN SAAT INI
$currentPeriodForms = $forms->filter(function($form) use ($startPeriodIndex, $endPeriodIndex) {
$tanggalForm = \Carbon\Carbon::parse($form->tanggal);
return $tanggalForm->between($startPeriodIndex, $endPeriodIndex);
});
@endphp
<section class="content-header">
<div class="container-fluid">
<div class="row mb-2">
@@ -25,52 +81,42 @@
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="{{ route('dashboard.index') }}">Dashboard</a></li>
</li>
</ol>
</div>
</div>
</div>
</section>
<section class="content">
<div class="container-fluid">
<div class="row row-cols-1 row-cols-md-2 row-cols-xl-5 g-3">
<!-- Total Expense -->
<div class="col d-flex">
<div class="small-box bg-info flex-fill">
<div class="inner">
<h3>Rp {{ number_format($forms->sum('total'), 0, ',', '.') }}</h3>
<p>Total Expense</p>
</div>
<div class="icon">
<i class="fas fa-wallet"></i>
<div class="icon"><i class="fas fa-wallet"></i></div>
</div>
</div>
</div>
<!-- Approved Expense -->
<div class="col d-flex">
<div class="small-box bg-success flex-fill">
<div class="inner">
<h3>Rp {{ number_format($forms->whereIn('status', ['Approved', 'Closed'])->sum('approved_total'), 0, ',', '.') }}</h3>
<p>Approved Expenses</p>
</div>
<div class="icon">
<i class="fas fa-check-circle"></i>
<div class="icon"><i class="fas fa-check-circle"></i></div>
</div>
</div>
</div>
<!-- Pending Expense -->
<div class="col d-flex">
<div class="small-box bg-warning flex-fill">
<div class="inner">
<h3 class="text-white">Rp {{ number_format($forms->where('status', 'On Progress')->sum('total'), 0, ',', '.') }}</h3>
<p class="text-white">Pending Expenses</p>
</div>
<div class="icon">
<i class="fas fa-hourglass-half"></i>
<div class="icon"><i class="fas fa-hourglass-half"></i></div>
</div>
</div>
</div>
<!-- Outstanding Expense -->
<div class="col d-flex">
<div class="small-box bg-danger flex-fill" id="outstandingCard">
<div class="inner">
@@ -79,9 +125,7 @@
<p class="mb-0"><small>Total Rp {{ number_format($outstandingTotal, 0, ',', '.') }}</small></p>
<p class="mb-0 filter-hint"><small>Klik untuk menampilkan detail</small></p>
</div>
<div class="icon">
<i class="fas fa-exclamation-triangle"></i>
</div>
<div class="icon"><i class="fas fa-exclamation-triangle"></i></div>
</div>
</div>
@if($availableBudget > 0)
@@ -91,15 +135,28 @@
<h3 class="text-white">Rp {{ number_format($availableBudget, 0, ',', '.') }}</h3>
<p class="text-white">Available Budget</p>
</div>
<div class="icon">
<i class="fas fa-money-bill"></i>
</div>
<div class="icon"><i class="fas fa-money-bill"></i></div>
</div>
</div>
@endif
</div>
</div>
</section>
<section class="content mt-2">
<div class="container-fluid">
<div class="row row-cols-2 row-cols-sm-3 row-cols-md-4 row-cols-xl-7 g-2 mb-3">
<div class="col"><div class="status-card active-filter" data-status="all"><div class="mini-stat-box"><div><p>Semua</p><h6>{{ $currentPeriodForms->count() }}</h6></div><i class="fas fa-list-ul text-muted"></i></div></div></div>
<div class="col"><div class="status-card" data-status="On Progress"><div class="mini-stat-box"><div><p>Progress</p><h6>{{ $currentPeriodForms->where('status', 'On Progress')->count() }}</h6></div><i class="fas fa-sync text-warning fa-xs"></i></div></div></div>
<div class="col"><div class="status-card" data-status="Approved 1"><div class="mini-stat-box"><div><p>App 1</p><h6>{{ $currentPeriodForms->where('status', 'Approved 1')->count() }}</h6></div><i class="fas fa-user-check text-info fa-xs"></i></div></div></div>
<div class="col"><div class="status-card" data-status="Approved 2"><div class="mini-stat-box"><div><p>App 2</p><h6>{{ $currentPeriodForms->where('status', 'Approved 2')->count() }}</h6></div><i class="fas fa-users text-primary fa-xs"></i></div></div></div>
<div class="col"><div class="status-card" data-status="Final Approved"><div class="mini-stat-box"><div><p>Final</p><h6>{{ $currentPeriodForms->where('status', 'Final Approved')->count() }}</h6></div><i class="fas fa-check-double text-success fa-xs"></i></div></div></div>
<div class="col"><div class="status-card" data-status="Rejected"><div class="mini-stat-box"><div><p>Reject</p><h6>{{ $currentPeriodForms->where('status', 'Rejected')->count() }}</h6></div><i class="fas fa-ban text-danger fa-xs"></i></div></div></div>
<div class="col"><div class="status-card" data-status="Closed"><div class="mini-stat-box"><div><p>Closed</p><h6>{{ $currentPeriodForms->where('status', 'Closed')->count() }}</h6></div><i class="fas fa-lock text-secondary fa-xs"></i></div></div></div>
</div>
</div>
</section>
<section class="content">
<div class="container-fluid">
<div class="card card-primary card-outline">
@@ -107,18 +164,14 @@
<h4 class="header-title float-left">List Data Forms Meeting Seminar Anda</h4>
<p class="float-right mb-2">
@if (auth()->user()->can('forms.meeting.create'))
<a class="btn btn-primary text-white" href="{{ route('forms.meeting.create') }}">
Add New Expense Meeting Seminar
</a>
<a class="btn btn-primary text-white" href="{{ route('forms.meeting.create') }}">Add New Expense</a>
@endif
</p>
<div class="clearfix"></div>
<div class="dataTables_wrapper dt-bootstrap4 mt-2">
@include('backend.layouts.partials.messages')
<div class="table-responsive">
<table id="dataTable"
class="table table-bordered table-striped table-head-fixed dataTable dtr-inline"
style="width:100%">
<table id="dataTable" class="table table-bordered table-striped table-head-fixed dataTable dtr-inline" style="width:100%">
<thead class="bg-light text-capitalize">
<tr>
<th>#</th>
@@ -133,15 +186,9 @@
<th>Total Approved</th>
<th>Account Number</th>
<th>Status</th>
@if (auth()->user()->can('approval.view'))
<th>Approval 1</th>
@endif
@if (auth()->user()->can('approval2.view'))
<th>Approval 2</th>
@endif
@if (auth()->user()->can('final_approval.view'))
<th>Final Approval</th>
@endif
@if (auth()->user()->can('approval.view')) <th>Approval 1</th> @endif
@if (auth()->user()->can('approval2.view')) <th>Approval 2</th> @endif
@if (auth()->user()->can('final_approval.view')) <th>Final Approval</th> @endif
<th>Aksi</th>
</tr>
</thead>
@@ -151,12 +198,12 @@
$role = auth()->user()->getRoleNames()[0];
@endphp
@foreach ($forms as $item)
<tr data-outstanding="{{ $outstandingIdCollection->contains($item->id) ? 1 : 0 }}">
<tr data-id="{{ $item->id }}" data-status="{{ $item->status }}" data-date="{{ \Carbon\Carbon::parse($item->tanggal)->format('Y-m-d') }}" data-outstanding="{{ $outstandingIdCollection->contains($item->id) ? '1' : '0' }}">
<td>{{ $loop->iteration }}</td>
<td>{{ $item->expense_number }}</td>
<td>{{ $item->user->name }}</td>
<td>{{ $item->user->cabang->cabang->name }}</td>
<td>{{ \Carbon\Carbon::parse($item->tanggal)->locale('id')->isoFormat('D MMMM Y') }}</td>
<td>{{ \Carbon\Carbon::parse($item->tanggal)->locale('id')->isoFormat('dddd, D MMMM Y HH:mm:ss') }}</td>
<td>{{ number_format($item->allowance, 0, ',', '.') }}</td>
<td>{{ number_format($item->transport_ankot, 0, ',', '.') }}</td>
<td>{{ number_format($item->hotel, 0, ',', '.') }}</td>
@@ -166,121 +213,67 @@
<td>
@if ($item->status == 'On Progress')
<span class="badge badge-warning text-white">{{ $item->status }}</span>
@elseif ($item->status == 'Approved 1' || $item->status == 'Approved 2' || $item->status == 'Final Approved')
@elseif (in_array($item->status, ['Approved 1', 'Approved 2', 'Final Approved']))
<span class="badge badge-success text-white">{{ $item->status }}</span>
@else
<span class="badge badge-danger text-white">{{ $item->status }}</span>
@endif
</td>
{{-- SELEKTIF MENYEMBUNYIKAN TOMBOL APPROVE/REJECT DIRECT DI INDEX TANPA MENGHAPUS LOGIKA KODE --}}
@if (auth()->user()->can('approval.view'))
<td>
{{-- approve / reject button --}}
@if (auth()->user()->can('approval.approve'))
@if ($item->status == 'On Progress')
<form action="javascript:void(0);" class="d-inline">
<button type="button"
class="btn btn-success btn-sm final-approve-modal"
data-id="{{ route('forms.meeting.approve', $item->id) }}">
Approve 1
</button>
</form>
<button type="button"
class="btn btn-danger btn-sm open-reject-modal"
data-action="{{ route('forms.meeting.reject', $item->id) }}">
Reject
</button>
@endif
@endif
@if ($item->approved_at)
{{ \Carbon\Carbon::parse($item->approved_at)->locale('id')->isoFormat('D MMMM Y HH:mm') }}
<div class="d-none">
@if (auth()->user()->can('approval.approve') && $item->status == 'On Progress')
<button type="button" class="btn btn-success btn-sm final-approve-modal" data-id="{{ route('forms.meeting.approve', $item->id) }}">Approve 1</button>
<button type="button" class="btn btn-danger btn-sm open-reject-modal" data-action="{{ route('forms.meeting.reject', $item->id) }}">Reject</button>
@endif
</div>
@if ($item->approved_at) <br><small>{{ \Carbon\Carbon::parse($item->approved_at)->format('d/m/y H:i') }}</small> @endif
</td>
@endif
@if (auth()->user()->can('approval2.view'))
<td>
{{-- approve / reject button --}}
@if (auth()->user()->can('approval2.approve'))
@if ($item->status == 'Approved 1' && $item->approved_at)
<form action="javascript:void(0);" class="d-inline">
<button type="button"
class="btn btn-success btn-sm open-approve-modal"
data-id="{{ route('forms.meeting.approve2', $item->id) }}">
Approve 2
</button>
</form>
<button type="button"
class="btn btn-danger btn-sm open-reject-modal"
data-action="{{ route('forms.meeting.reject', $item->id) }}">
Reject
</button>
@endif
@endif
@if ($item->approved2_at)
{{ \Carbon\Carbon::parse($item->approved2_at)->locale('id')->isoFormat('D MMMM Y HH:mm') }}
<div class="d-none">
@if (auth()->user()->can('approval2.approve') && $item->status == 'Approved 1' && $item->approved_at)
<button type="button" class="btn btn-success btn-sm open-approve-modal" data-id="{{ route('forms.meeting.approve2', $item->id) }}">Approve 2</button>
<button type="button" class="btn btn-danger btn-sm open-reject-modal" data-action="{{ route('forms.meeting.reject', $item->id) }}">Reject</button>
@endif
</div>
@if ($item->approved2_at) <br><small>{{ \Carbon\Carbon::parse($item->approved2_at)->format('d/m/y H:i') }}</small> @endif
</td>
@endif
@if(auth()->user()->can('final_approval.view'))
<td>
{{-- approve / reject button --}}
<div class="d-none">
@if(auth()->user()->can('final_approval.approve'))
@if ($item->status == 'Approved 2' && $item->approved2_at && !$item->final_approved_at)
<form action="javascript:void(0);" class="d-inline">
<button type="button"
class="btn btn-success btn-sm open-approve-modal"
data-id="{{ route('forms.meeting.final-approve', $item->id) }}">
Final Approve
</button>
</form>
@else
@if ($item->final_approved_at && auth()->user()->can('final_approval.open'))
<form action="{{ route('forms.meeting.open', $item->id) }}" method="POST" class="d-inline"onsubmit="return confirm('Are you sure you want to approve this item?');">
@csrf
@method('PUT')
<button type="submit" class="btn btn-success btn-sm">
Open
</button>
<button type="button" class="btn btn-success btn-sm open-approve-modal" data-id="{{ route('forms.meeting.final-approve', $item->id) }}">Final Approve</button>
<button type="button" class="btn btn-danger btn-sm open-reject-modal" data-action="{{ route('forms.meeting.reject', $item->id) }}">Reject</button>
@elseif ($item->final_approved_at && auth()->user()->can('final_approval.open'))
<form action="{{ route('forms.meeting.open', $item->id) }}" method="POST" class="d-inline" onsubmit="return confirm('Confirm Open?');">
@csrf @method('PUT') <button type="submit" class="btn btn-success btn-sm">Open</button>
</form>
@endif
@endif
@endif
@if ($item->final_approved_at)
{{ \Carbon\Carbon::parse($item->final_approved_at)->locale('id')->isoFormat('D MMMM Y HH:mm') }}
@endif
</div>
@if ($item->final_approved_at) <br><small>{{ \Carbon\Carbon::parse($item->final_approved_at)->format('d/m/y H:i') }}</small> @endif
</td>
@endif
<td>
@if (auth()->user()->can('forms.meeting.view'))
<a href="{{ route('forms.meeting.view', $item->id) }}" class="btn btn-primary btn-sm">
<i class="fas fa-eye text-white"></i>
</a>
<a href="{{ route('forms.meeting.view', $item->id) }}" class="btn btn-primary btn-sm"><i class="fas fa-eye text-white"></i></a>
@endif
@php
$canEdit = false;
if (auth()->user()->can('forms.meeting.edit')) {
if (in_array($role, ['Medical Representatif', 'Area Manager Cabang'])) {
$canEdit = $item->status == 'On Progress';
} else {
$canEdit = true;
}
}
$canEdit = auth()->user()->can('forms.meeting.edit') && (!in_array($role, ['Medical Representatif', 'Area Manager Cabang']) || $item->status == 'On Progress');
@endphp
@if ($canEdit)
<a href="{{ route('forms.meeting.edit', $item->id) }}" class="btn btn-warning btn-sm">
<i class="fas fa-edit text-white"></i>
</a>
<a href="{{ route('forms.meeting.edit', $item->id) }}" class="btn btn-warning btn-sm"><i class="fas fa-edit text-white"></i></a>
@endif
@if (($role != 'Medical Representatif' && auth()->user()->can('forms.meeting.delete')) || ($role == 'Medical Representatif' && $item->status == 'On Progress'))
<form action="{{ route('forms.meeting.destroy', $item->id) }}" method="POST" class="d-inline" onsubmit="return confirm('Are you sure you want to delete this item?');">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-danger btn-sm">
<i class="fas fa-trash"></i>
</button>
<form action="{{ route('forms.meeting.destroy', $item->id) }}" method="POST" class="d-inline" onsubmit="return confirm('Delete?');">
@csrf @method('DELETE') <button type="submit" class="btn btn-danger btn-sm"><i class="fas fa-trash"></i></button>
</form>
@endif
</td>
@@ -294,401 +287,232 @@
</div>
</div>
</section>
<section>
<div class="modal fade" id="approveModal" tabindex="-1" role="dialog" aria-labelledby="approveModalLabel" aria-hidden="true">
{{-- MODAL CONTAINER DI BAWAH (TETAP UTUH UNTUK KEBUTUHAN ROUTING AJAX) --}}
<div class="modal fade" id="approveModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<form id="approveForm" method="POST" action="">
@csrf
@method('PUT')
@csrf @method('PUT')
<div class="modal-header">
<h5 class="modal-title" id="approveModalLabel">Approve Item</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
</div>
<div class="modal-body">
<!-- Loading spinner -->
<div id="loadingSpinner" class="d-flex justify-content-center align-items-center" style="height: 200px;">
<div class="spinner-border text-primary" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
<!-- Modal content (hidden initially) -->
<div id="loadingSpinner" class="d-flex justify-content-center align-items-center" style="height: 200px;"><div class="spinner-border text-primary" role="status"><span class="sr-only">Loading...</span></div></div>
<div id="modalContent" class="d-none">
<p>Please review the details below:</p>
<ul class="list-group">
<li class="list-group-item">
<strong>Expense Number:</strong> <span id="expense_number"></span>
</li>
<li class="list-group-item">
<strong>User:</strong> <span id="user"></span>
</li>
<li class="list-group-item">
<strong>Tipe:</strong> Meeting / Seminar
</li>
<li class="list-group-item">
<strong>Tanggal:</strong> <span id="tanggal"></span>
</li>
<li class="list-group-item">
<strong>Nominal Allowance:</strong> <span id="allowance_value"></span>
</li>
<li class="list-group-item">
<strong>Bukti Allowance:</strong> <span id="bukti_allowance"></span>
</li>
<li class="list-group-item">
<strong>Nominal Transport Antar Kota:</strong> <span id="transport_ankot_value"></span>
</li>
<li class="list-group-item">
<strong>Bukti Transport Antar Kota:</strong> <span id="bukti_transport_ankot"></span>
</li>
<li class="list-group-item">
<strong>Nominal Hotel:</strong> <span id="hotel_value"></span>
</li>
<li class="list-group-item">
<strong>Bukti Hotel:</strong> <span id="bukti_hotel"></span>
</li>
<li class="list-group-item"><strong>Expense Number:</strong> <span id="expense_number"></span></li>
<li class="list-group-item"><strong>User:</strong> <span id="user"></span></li>
<li class="list-group-item"><strong>Tanggal:</strong> <span id="tanggal"></span></li>
<li class="list-group-item"><strong>Allowance:</strong> <span id="allowance_value"></span> (<span id="bukti_allowance"></span>)</li>
<li class="list-group-item"><strong>Transport Ankot:</strong> <span id="transport_ankot_value"></span> (<span id="bukti_transport_ankot"></span>)</li>
<li class="list-group-item"><strong>Hotel:</strong> <span id="hotel_value"></span> (<span id="bukti_hotel"></span>)</li>
</ul>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Approve</button>
</div>
<div class="modal-footer"><button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button><button type="submit" class="btn btn-primary">Approve</button></div>
</form>
</div>
</div>
</div>
</section>
<section>
<div class="modal fade" id="finalApproveModal" tabindex="-1" role="dialog" aria-labelledby="finalApproveModalLabel" aria-hidden="true">
<div class="modal fade" id="finalApproveModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<form id="finalApproveForm" method="POST" action="">
@csrf
@method('PUT')
@csrf @method('PUT')
<div class="modal-header">
<h5 class="modal-title" id="approveModalLabel">Approve Item</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h5 class="modal-title" id="finalApproveModalLabel">Final Approve Item</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
</div>
<div class="modal-body">
<!-- Loading spinner -->
<div id="finalLoadingSpinner" class="d-flex justify-content-center align-items-center" style="height: 200px;">
<div class="spinner-border text-primary" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
<!-- Modal content (hidden initially) -->
<div id="finalLoadingSpinner" class="d-flex justify-content-center align-items-center" style="height: 200px;"><div class="spinner-border text-primary" role="status"><span class="sr-only">Loading...</span></div></div>
<div id="finalModalContent" class="d-none">
<p>Please review the details below:</p>
<ul class="list-group">
<li class="list-group-item">
<strong>Expense Number:</strong> <span id="final_expense_number"></span>
</li>
<li class="list-group-item">
<strong>User:</strong> <span id="final_user"></span>
</li>
<li class="list-group-item">
<strong>Tipe:</strong> Meeting / Seminar
</li>
<li class="list-group-item">
<strong>Tanggal:</strong> <span id="final_tanggal"></span>
</li>
<li class="list-group-item">
<strong>Bukti Allowance:</strong> <span id="final_bukti_allowance"></span>
</li>
<li class="list-group-item">
<strong>Bukti Transport Antar Kota:</strong> <span id="final_bukti_transport_ankot"></span>
</li>
<li class="list-group-item">
<strong>Bukti Hotel:</strong> <span id="final_bukti_hotel"></span>
</li>
<li class="list-group-item"><strong>Expense:</strong> <span id="final_expense_number"></span></li>
<li class="list-group-item"><strong>User:</strong> <span id="final_user"></span></li>
<li class="list-group-item"><strong>Tanggal:</strong> <span id="final_tanggal"></span></li>
<li class="list-group-item"><strong>Bukti Allowance:</strong> <span id="final_bukti_allowance"></span></li>
<li class="list-group-item"><strong>Bukti Transport:</strong> <span id="final_bukti_transport_ankot"></span></li>
<li class="list-group-item"><strong>Bukti Hotel:</strong> <span id="final_bukti_hotel"></span></li>
</ul>
<p class="mt-2">Please select the items you want to approve:</p>
<ul class="list-group list-unstyled">
<li>
<div class="form-group">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="final_allowance" name="allowance">
<label class="custom-control-label" for="final_allowance">Allowance (<span id="final_allowance_value"></span>)</label>
<div class="custom-control custom-checkbox mb-2"><input type="checkbox" class="custom-control-input" id="final_allowance" name="allowance"><label class="custom-control-label" for="final_allowance">Allowance (<span id="final_allowance_value"></span>)</label></div>
<div class="custom-control custom-checkbox mb-2"><input type="checkbox" class="custom-control-input" id="final_transport_ankot" name="transport_ankot"><label class="custom-control-label" for="final_transport_ankot">Transport Ankot (<span id="final_transport_ankot_value"></span>)</label></div>
<div class="custom-control custom-checkbox mb-3"><input type="checkbox" class="custom-control-input" id="final_hotel" name="hotel"><label class="custom-control-label" for="final_hotel">Hotel (<span id="final_hotel_value"></span>)</label></div>
<div class="form-group"><label>Total Approved</label><input type="text" class="form-control font-weight-bold" id="total" name="total" value="0" readonly></div>
</div>
</div>
</li>
<li>
<div class="form-group">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="final_transport_ankot" name="transport_ankot">
<label class="custom-control-label" for="final_transport_ankot">Transport Ankot (<span id="final_transport_ankot_value"></span>)</label>
</div>
</div>
</li>
<li>
<div class="form-group">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="final_hotel" name="hotel">
<label class="custom-control-label" for="final_hotel">Hotel (<span id="final_hotel_value"></span>)</label>
</div>
</div>
</li>
<li>
<div class="form-group">
<label for="total">Total</label>
<input type="text" class="form-control" id="total" name="total" value="0" readonly>
</div>
</li>
</ul>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Approve</button>
</div>
<div class="modal-footer"><button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button><button type="submit" class="btn btn-primary">Approve</button></div>
</form>
</div>
</div>
</div>
</section>
<section>
<div class="modal fade" id="rejectModal" tabindex="-1" role="dialog" aria-labelledby="rejectModalLabel" aria-hidden="true">
<div class="modal fade" id="rejectModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<form id="rejectForm" method="POST" action="">
@csrf
@method('PUT')
@csrf @method('PUT')
<div class="modal-header">
<h5 class="modal-title" id="rejectModalLabel">Reject Expense</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="rejectRemarks">Remarks <span class="text-danger">*</span></label>
<textarea class="form-control" name="remarks" id="rejectRemarks" rows="3" required></textarea>
</div>
<p class="mb-0 text-muted small">Pengajuan akan ditolak setelah Anda mengirimkan catatan ini.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-danger">Reject</button>
<div class="form-group"><label>Remarks <span class="text-danger">*</span></label><textarea class="form-control" name="remarks" id="rejectRemarks" rows="3" required></textarea></div>
</div>
<div class="modal-footer"><button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button><button type="submit" class="btn btn-danger">Reject</button></div>
</form>
</div>
</div>
</div>
</section>
@endsection
@section('scripts')
<script>
<script>
$(document).ready(function () {
// Uncheck all checkboxes on page refresh
$('.custom-control-input').prop('checked', false);
if ($('#dataTable').length) {
let outstandingFilterActive = false;
const outstandingFilter = function(settings, data, dataIndex) {
if (!outstandingFilterActive) {
return true;
}
const row = settings.aoData[dataIndex].nTr;
return $(row).data('outstanding') === 1 || $(row).data('outstanding') === '1';
};
let statusFilterActive = 'all';
$.fn.dataTable.ext.search.push(outstandingFilter);
// Custom Search DataTables (Filter Sinkron: Status + Outstanding + Siklus Periode Berjalan 11 - 13)
$.fn.dataTable.ext.search.push(function(settings, data, dataIndex) {
const row = $(settings.aoData[dataIndex].nTr);
const rowStatus = row.attr('data-status');
const isOutstanding = row.attr('data-outstanding') === '1' || row.attr('data-outstanding') === 1;
const rowDateStr = row.attr('data-date'); // Format: YYYY-MM-DD
// 1. Prioritas Pertama: Outstanding Filter Murni
if (outstandingFilterActive) {
return isOutstanding;
}
// 2. Prioritas Kedua: Aturan Jendela Waktu (Cut-off) Berlaku MUTLAK Untuk Semua Mini Card
if (rowDateStr) {
const dateParts = rowDateStr.split('-');
const rowDate = new Date(dateParts[0], dateParts[1] - 1, dateParts[2], 12, 0, 0);
const now = new Date();
let startPeriod, endPeriod;
const startDay = 11;
const closingDay = 13;
if (now.getDate() >= startDay) {
startPeriod = new Date(now.getFullYear(), now.getMonth(), startDay, 0, 0, 0);
endPeriod = new Date(now.getFullYear(), now.getMonth() + 1, closingDay, 23, 59, 59);
} else {
startPeriod = new Date(now.getFullYear(), now.getMonth() - 1, startDay, 0, 0, 0);
endPeriod = new Date(now.getFullYear(), now.getMonth(), closingDay, 23, 59, 59);
}
// TENGGELAMKAN ROW jika tidak berada di dalam siklus berjalan
if (rowDate < startPeriod || rowDate > endPeriod) {
return false;
}
}
// 3. Prioritas Ketiga: Filter Status Setelah Melewati Karantina Tanggal
if (statusFilterActive !== 'all' && rowStatus !== statusFilterActive) {
return false;
}
return true;
});
const table = $('#dataTable').DataTable({
responsive: false,
dom: 'Blfrtip',
lengthMenu: [ [10, 50, 100, -1], [10, 50, 100, "All"] ], // Control the entries dropdown
buttons: [
{
extend: 'excel',
text: 'Excel'
},
{
extend: 'pdf',
text: 'PDF',
orientation: 'landscape',
pageSize: 'A4',
exportOptions: {
columns: ':visible'
}
},
{
extend: 'print',
text: 'Print',
orientation: 'landscape',
exportOptions: {
columns: ':visible'
}
},
'colvis'
]
responsive: false, dom: 'Blfrtip',
lengthMenu: [ [10, 50, 100, -1], [10, 50, 100, "All"] ],
buttons: [ { extend: 'excel', text: 'Excel' }, { extend: 'pdf', text: 'PDF', orientation: 'landscape' }, 'colvis' ]
});
$('.status-card').on('click', function() {
outstandingFilterActive = false;
$('#outstandingCard').removeClass('active');
$('#outstandingCard').find('.filter-hint small').text('Klik untuk detail');
$('.status-card').removeClass('active-filter');
$(this).addClass('active-filter');
statusFilterActive = $(this).data('status');
table.draw();
});
const $outstandingCard = $('#outstandingCard');
const updateCardHint = (isActive) => {
const text = isActive ? 'Klik untuk menampilkan semua data' : 'Klik untuk menampilkan detail';
$outstandingCard.find('.filter-hint small').text(text);
};
updateCardHint(outstandingFilterActive);
$outstandingCard.on('click', function () {
outstandingFilterActive = !outstandingFilterActive;
$outstandingCard.toggleClass('active', outstandingFilterActive);
$(this).toggleClass('active', outstandingFilterActive);
if(outstandingFilterActive) {
$('.status-card').removeClass('active-filter');
$('.status-card[data-status="all"]').addClass('active-filter');
statusFilterActive = 'all';
}
table.draw();
updateCardHint(outstandingFilterActive);
$(this).find('.filter-hint small').text(outstandingFilterActive ? 'Tampilkan semua data' : 'Klik untuk detail');
});
}
// Handle Reject Button Click
$(document).on('click', '.open-reject-modal', function () {
const rejectUrl = $(this).data('action');
$('#rejectForm').attr('action', rejectUrl);
$('#rejectRemarks').val('');
$('#rejectForm').attr('action', $(this).data('action'));
$('#rejectRemarks').val('').focus();
$('#rejectModal').modal('show');
});
$('#rejectModal').on('shown.bs.modal', function () {
$('#rejectRemarks').trigger('focus');
});
$('#rejectModal').on('hidden.bs.modal', function () {
$('#rejectForm').attr('action', '');
$('#rejectRemarks').val('');
});
// Handle Approve Button Click
$(document).on('click', '.open-approve-modal', function() {
const approveUrl = $(this).data('id');
// Show spinner and hide content initially
$('#loadingSpinner').show();
$('#modalContent').addClass('d-none');
$('#approveForm').attr('action', approveUrl); // Set the form action
$('#loadingSpinner').addClass('d-flex');
$('#approveModal').modal('show'); // Show the modal
// Get detail from /forms/meeting/detail/{id}
$.get(approveUrl.replace(/approve2|final-approve/g, 'detail'), function (data) {
const formatter = new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
});
$('#allowance_value').text(formatter.format(data.allowance));
$('#transport_ankot_value').text(formatter.format(data.transport_ankot));
$('#hotel_value').text(formatter.format(data.hotel));
// Detail data
$('#user').text(data.user.name);
$('#expense_number').text(data.expense_number);
$('#tanggal').text(new Date(data.tanggal).toLocaleDateString('id-ID', {
year: 'numeric',
month: 'long',
day: 'numeric'
}));
$('#bukti_allowance').html(data.bukti_allowance ? '<a href="' + data.bukti_allowance + '" target="_blank">Download</a>' : '-');
$('#bukti_transport_ankot').html(data.bukti_transport_ankot ? '<a href="' + data.bukti_transport_ankot + '" target="_blank">Download</a>' : '-');
$('#bukti_hotel').html(data.bukti_hotel ? '<a href="' + data.bukti_hotel + '" target="_blank">Download</a>' : '-');
// Hide spinner and show content
$('#loadingSpinner').hide();
$('#loadingSpinner').removeClass('d-flex');
$('#loadingSpinner').addClass('d-none');
$('#modalContent').removeClass('d-none');
// Reset the total to zero
$('#total').val(0);
const url = $(this).data('id');
$('#loadingSpinner').show(); $('#modalContent').addClass('d-none');
$('#approveForm').attr('action', url); $('#approveModal').modal('show');
$.get(url.replace(/approve2|final-approve/g, 'detail'), function (data) {
const fmt = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 });
$('#allowance_value').text(fmt.format(data.allowance));
$('#transport_ankot_value').text(fmt.format(data.transport_ankot));
$('#hotel_value').text(fmt.format(data.hotel));
$('#user').text(data.user.name); $('#expense_number').text(data.expense_number);
$('#tanggal').text(new Date(data.tanggal).toLocaleDateString('id-ID', {year:'numeric', month:'long', day:'numeric'}));
$('#bukti_allowance').html(data.bukti_allowance ? '<a href="'+data.bukti_allowance+'" target="_blank">View</a>' : '-');
$('#bukti_transport_ankot').html(data.bukti_transport_ankot ? '<a href="'+data.bukti_transport_ankot+'" target="_blank">View</a>' : '-');
$('#bukti_hotel').html(data.bukti_hotel ? '<a href="'+data.bukti_hotel+'" target="_blank">View</a>' : '-');
$('#loadingSpinner').hide(); $('#modalContent').removeClass('d-none');
});
});
// Handle Approve Button Click
$(document).on('click', '.final-approve-modal', function() {
const approveUrl = $(this).data('id');
// Show spinner and hide content initially
$('#finalLoadingSpinner').show();
$('#finalModalContent').addClass('d-none');
$('#finalApproveForm').attr('action', approveUrl); // Set the form action
$('#finalLoadingSpinner').addClass('d-flex');
$('#finalApproveModal').modal('show'); // Show the modal
// Get detail from /forms/meeting/detail/{id}
$.get(approveUrl.replace('approve', 'detail'), function (data) {
console.log(data);
const formatter = new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
});
$('#final_allowance_value').text(formatter.format(data.allowance));
$('#final_transport_ankot_value').text(formatter.format(data.transport_ankot));
$('#final_hotel_value').text(formatter.format(data.hotel));
// Assign data values to checkboxes
const url = $(this).data('id');
$('#finalLoadingSpinner').show(); $('#finalModalContent').addClass('d-none');
$('#finalApproveForm').attr('action', url); $('#finalApproveModal').modal('show');
$.get(url.replace('approve', 'detail'), function (data) {
const fmt = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 });
$('#final_allowance_value').text(fmt.format(data.allowance));
$('#final_transport_ankot_value').text(fmt.format(data.transport_ankot));
$('#final_hotel_value').text(fmt.format(data.hotel));
$('#final_allowance').data('value', data.allowance);
$('#final_transport_ankot').data('value', data.transport_ankot);
$('#final_hotel').data('value', data.hotel);
// Detail data
$('#final_user').text(data.user.name);
$('#final_expense_number').text(data.expense_number);
$('#final_tanggal').text(new Date(data.tanggal).toLocaleDateString('id-ID', {
year: 'numeric',
month: 'long',
day: 'numeric'
}));
$('#final_bukti_allowance').html(data.bukti_allowance ? '<a href="' + data.bukti_allowance + '" target="_blank">Download</a>' : '-');
$('#final_bukti_transport_ankot').html(data.bukti_transport_ankot ? '<a href="' + data.bukti_transport_ankot + '" target="_blank">Download</a>' : '-');
$('#final_bukti_hotel').html(data.bukti_hotel ? '<a href="' + data.bukti_hotel + '" target="_blank">Download</a>' : '-');
// Hide spinner and show content
$('#finalLoadingSpinner').hide();
$('#finalLoadingSpinner').removeClass('d-flex');
$('#finalLoadingSpinner').addClass('d-none');
$('#finalModalContent').removeClass('d-none');
// Reset the total to zero
$('#final_user').text(data.user.name); $('#final_expense_number').text(data.expense_number);
$('#final_tanggal').text(new Date(data.tanggal).toLocaleDateString('id-ID', {year:'numeric', month:'long', day:'numeric'}));
$('#final_bukti_allowance').html(data.bukti_allowance ? '<a href="'+data.bukti_allowance+'" target="_blank">View</a>' : '-');
$('#final_bukti_transport_ankot').html(data.bukti_transport_ankot ? '<a href="'+data.bukti_transport_ankot+'" target="_blank">View</a>' : '-');
$('#final_bukti_hotel').html(data.bukti_hotel ? '<a href="'+data.bukti_hotel+'" target="_blank">View</a>' : '-');
$('#finalLoadingSpinner').hide(); $('#finalModalContent').removeClass('d-none');
$('#total').val(0);
});
});
// Update total dynamically
$('.custom-control-input').on('change', function () {
let total = 0;
$('.custom-control-input:checked').each(function () {
total += parseFloat($(this).data('value') || 0);
$('.custom-control-input:checked').each(function () { total += parseFloat($(this).data('value') || 0); });
const fmt = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 });
$('#total').val(fmt.format(total));
});
const formatter = new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0,
});
$('#total').val(formatter.format(total));
});
// Uncheck all checkboxes when the modal is closed
$('#finalApproveModal').on('hidden.bs.modal', function () {
$('.custom-control-input').prop('checked', false); // Uncheck all checkboxes
$('#total').val('0'); // Reset total to 0
$('.custom-control-input').prop('checked', false); $('#total').val('0');
});
});
</script>
</script>
@endsection
@@ -6,6 +6,7 @@
@section('admin-content')
<script src="https://cdn.jsdelivr.net/npm/autonumeric@4.6.0/dist/autoNumeric.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<section class="content-header">
<div class="container-fluid">
@@ -82,7 +83,7 @@
<div class="mb-3">
<div class="d-flex align-items-center justify-content-between">
<label class="form-label mb-0">Lampiran</label>
<button type="button" class="btn btn-outline-primary btn-sm" id="other-add-attachment-row">
<button type="button" class="btn btn-slate-custom btn-outline-primary btn-sm" id="other-add-attachment-row">
<i class="fas fa-plus mr-1"></i> Tambah Lampiran
</button>
</div>
@@ -133,19 +134,44 @@
</section>
<script>
new AutoNumeric('#total', {
// 1. Inisialisasi AutoNumeric dengan batasan angka minimum nol
const totalAutoNumeric = new AutoNumeric('#total', {
digitGroupSeparator: '.',
decimalCharacter: ',',
currencySymbol: 'Rp.',
decimalPlaces: 0,
minimumValue: '0',
unformatOnSubmit: true
});
// 2. Mematikan sliding wheel mouse (scrolling) pada kolom input nominal angka
document.getElementById('total').addEventListener('wheel', function (e) {
e.preventDefault();
this.blur();
});
const spinnerOverlay = document.getElementById('loading-spinner-overlay');
document.getElementById('expense-form').addEventListener('submit', function () {
// 3. Interseptor Validasi Submit Form dengan Batasan Nilai Rp 1.000.000 bersih
document.getElementById('expense-form').addEventListener('submit', function (e) {
// Mengambil angka murni tanpa format simbol dari instance AutoNumeric
const rawTotalValue = totalAutoNumeric.getNumber() || 0;
if (rawTotalValue > 1000000) {
e.preventDefault(); // Batalkan pengiriman form ke route backend
Swal.fire({
title: 'Batas Angka Terlampaui!',
text: 'Pengajuan nominal Expense Other tidak diperbolehkan melebihi batas Rp 1.000.000. Mohon periksa kembali input Anda.',
icon: 'error',
confirmButtonColor: '#d33',
confirmButtonText: 'Revisi Data'
});
} else {
// Lolos validasi, aktifkan spinner overlay UI
spinnerOverlay.classList.remove('d-none');
spinnerOverlay.classList.add('d-flex');
}
});
const attachmentCategories = @json($attachmentCategories ?? ($otherAttachmentCategories ?? []));
@@ -195,18 +195,44 @@
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
new AutoNumeric('#total', {
// 1. Inisialisasi AutoNumeric dengan batasan angka minimum nol
const totalAutoNumeric = new AutoNumeric('#total', {
digitGroupSeparator: '.',
decimalCharacter: ',',
currencySymbol: 'Rp.',
decimalPlaces: 0,
minimumValue: '0',
unformatOnSubmit: true
});
// 2. Mematikan sliding wheel mouse (scrolling) pada kolom input nominal angka
document.getElementById('total').addEventListener('wheel', function (e) {
e.preventDefault();
this.blur();
});
const spinnerOverlay = document.getElementById('loading-spinner-overlay');
document.getElementById('expense-form').addEventListener('submit', function () {
// 3. Interseptor Validasi Submit Form dengan Batasan Nilai Rp 1.000.000 bersih
document.getElementById('expense-form').addEventListener('submit', function (e) {
// Mengambil angka murni tanpa format simbol dari instance AutoNumeric
const rawTotalValue = totalAutoNumeric.getNumber() || 0;
if (rawTotalValue > 1000000) {
e.preventDefault(); // Batalkan pengiriman form ke route backend
Swal.fire({
title: 'Batas Angka Terlampaui!',
text: 'Pengajuan nominal Expense Other tidak diperbolehkan melebihi batas Rp 1.000.000. Mohon periksa kembali input Anda.',
icon: 'error',
confirmButtonColor: '#d33',
confirmButtonText: 'Revisi Data'
});
} else {
// Lolos validasi, aktifkan spinner overlay UI
spinnerOverlay.classList.remove('d-none');
spinnerOverlay.classList.add('d-flex');
}
});
const attachmentCategories = @json($attachmentCategories ?? ($otherAttachmentCategories ?? []));
@@ -6,16 +6,72 @@
@section('admin-content')
<style>
/* Style Awal Anda */
#outstandingCard {
cursor: pointer;
}
#outstandingCard.active {
box-shadow: 0 0 0 0.25rem rgba(255, 255, 255, 0.35);
}
/* Style Baru: Mini Status Cards */
.status-card {
cursor: pointer;
transition: all 0.2s ease-in-out;
border: 1px solid rgba(0,0,0,0.08);
border-radius: 6px;
background-color: #fff;
}
.status-card:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0,0,0,0.1); }
.status-card.active-filter {
border: 2px solid #343a40;
box-shadow: 0 2px 10px rgba(0,0,0,0.15);
}
.mini-stat-box {
padding: 6px 10px;
display: flex;
align-items: center;
justify-content: space-between;
min-height: 45px;
}
.mini-stat-box h6 { margin: 0; font-weight: 800; font-size: 0.95rem; color: #333; }
.mini-stat-box p { margin: 0; font-size: 0.65rem; text-transform: uppercase; color: #777; font-weight: 600; }
/* Border kiri mini untuk indikator warna status */
.status-card[data-status="On Progress"] { border-left: 3px solid #ffc107; }
.status-card[data-status="Approved 1"] { border-left: 3px solid #17a2b8; }
.status-card[data-status="Approved 2"] { border-left: 3px solid #007bff; }
.status-card[data-status="Final Approved"] { border-left: 3px solid #28a745; }
.status-card[data-status="Rejected"] { border-left: 3px solid #dc3545; }
.status-card[data-status="Closed"] { border-left: 3px solid #6c757d; }
</style>
@php
$outstandingIdCollection = collect($outstandingIds ?? []);
// ==========================================
// FILTER PERIODE BERJALAN KHUSUS UNTUK MINI CARDS
// (Sinkronisasi Angka Realtime Siklus Tanggal 11-13)
// ==========================================
$startDayIndex = (int) env('STARTING_DATE', 11);
$closingDayIndex = (int) env('CLOSING_DATE', 13);
$nowIndex = \Carbon\Carbon::now();
if ($nowIndex->day >= $startDayIndex) {
$startPeriodIndex = \Carbon\Carbon::create($nowIndex->year, $nowIndex->month, $startDayIndex)->startOfDay();
$endPeriodIndex = \Carbon\Carbon::create($nowIndex->copy()->addMonth()->year, $nowIndex->copy()->addMonth()->month, $closingDayIndex)->endOfDay();
} else {
$startPeriodIndex = \Carbon\Carbon::create($nowIndex->copy()->subMonth()->year, $nowIndex->copy()->subMonth()->month, $startDayIndex)->startOfDay();
$endPeriodIndex = \Carbon\Carbon::create($nowIndex->year, $nowIndex->month, $closingDayIndex)->endOfDay();
}
// Koleksi data pengajuan MURNI HANYA DI PERIODE BERJALAN AKTIF (11 s/d 13)
$currentPeriodForms = $forms->filter(function($form) use ($startPeriodIndex, $endPeriodIndex) {
$tanggalForm = \Carbon\Carbon::parse($form->tanggal);
return $tanggalForm->between($startPeriodIndex, $endPeriodIndex);
});
@endphp
<section class="content-header">
<div class="container-fluid">
<div class="row mb-2">
@@ -25,52 +81,42 @@
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="{{ route('dashboard.index') }}">Dashboard</a></li>
</li>
</ol>
</div>
</div>
</div>
</section>
<section class="content">
<div class="container-fluid">
<div class="row row-cols-1 row-cols-md-2 row-cols-xl-5 g-3">
<!-- Total Expense -->
<div class="col d-flex">
<div class="small-box bg-info flex-fill">
<div class="inner">
<h3>Rp {{ number_format($forms->sum('total'), 0, ',', '.') }}</h3>
<p>Total Expense</p>
</div>
<div class="icon">
<i class="fas fa-wallet"></i>
<div class="icon"><i class="fas fa-wallet"></i></div>
</div>
</div>
</div>
<!-- Approved Expense -->
<div class="col d-flex">
<div class="small-box bg-success flex-fill">
<div class="inner">
<h3>Rp {{ number_format($forms->whereIn('status', ['Approved', 'Closed'])->sum('approved_total'), 0, ',', '.') }}</h3>
<h3>Rp {{ number_format($forms->whereIn('status', ['Approved', 'Closed', 'Final Approved'])->sum('approved_total'), 0, ',', '.') }}</h3>
<p>Approved Expenses</p>
</div>
<div class="icon">
<i class="fas fa-check-circle"></i>
<div class="icon"><i class="fas fa-check-circle"></i></div>
</div>
</div>
</div>
<!-- Pending Expense -->
<div class="col d-flex">
<div class="small-box bg-warning flex-fill">
<div class="inner">
<h3 class="text-white">Rp {{ number_format($forms->where('status', 'On Progress')->sum('total'), 0, ',', '.') }}</h3>
<p class="text-white">Pending Expenses</p>
</div>
<div class="icon">
<i class="fas fa-hourglass-half"></i>
<div class="icon"><i class="fas fa-hourglass-half"></i></div>
</div>
</div>
</div>
<!-- Outstanding Expense -->
<div class="col d-flex">
<div class="small-box bg-danger flex-fill" id="outstandingCard">
<div class="inner">
@@ -79,9 +125,7 @@
<p class="mb-0"><small>Total Rp {{ number_format($outstandingTotal, 0, ',', '.') }}</small></p>
<p class="mb-0 filter-hint"><small>Klik untuk menampilkan detail</small></p>
</div>
<div class="icon">
<i class="fas fa-exclamation-triangle"></i>
</div>
<div class="icon"><i class="fas fa-exclamation-triangle"></i></div>
</div>
</div>
@if($availableBudget > 0)
@@ -91,15 +135,77 @@
<h3 class="text-white">Rp {{ number_format($availableBudget, 0, ',', '.') }}</h3>
<p class="text-white">Available Budget</p>
</div>
<div class="icon">
<i class="fas fa-money-bill"></i>
</div>
<div class="icon"><i class="fas fa-money-bill"></i></div>
</div>
</div>
@endif
</div>
</div>
</section>
<section class="content mt-2">
<div class="container-fluid">
<div class="row row-cols-2 row-cols-sm-3 row-cols-md-4 row-cols-xl-7 g-2 mb-3">
<div class="col">
<div class="status-card active-filter" data-status="all">
<div class="mini-stat-box">
<div><p>Semua</p><h6>{{ $currentPeriodForms->count() }}</h6></div>
<i class="fas fa-list-ul text-muted"></i>
</div>
</div>
</div>
<div class="col">
<div class="status-card" data-status="On Progress">
<div class="mini-stat-box">
<div><p>Progress</p><h6>{{ $currentPeriodForms->where('status', 'On Progress')->count() }}</h6></div>
<i class="fas fa-sync text-warning fa-xs"></i>
</div>
</div>
</div>
<div class="col">
<div class="status-card" data-status="Approved 1">
<div class="mini-stat-box">
<div><p>App 1</p><h6>{{ $currentPeriodForms->where('status', 'Approved 1')->count() }}</h6></div>
<i class="fas fa-user-check text-info fa-xs"></i>
</div>
</div>
</div>
<div class="col">
<div class="status-card" data-status="Approved 2">
<div class="mini-stat-box">
<div><p>App 2</p><h6>{{ $currentPeriodForms->where('status', 'Approved 2')->count() }}</h6></div>
<i class="fas fa-users text-primary fa-xs"></i>
</div>
</div>
</div>
<div class="col">
<div class="status-card" data-status="Final Approved">
<div class="mini-stat-box">
<div><p>Final</p><h6>{{ $currentPeriodForms->where('status', 'Final Approved')->count() }}</h6></div>
<i class="fas fa-check-double text-success fa-xs"></i>
</div>
</div>
</div>
<div class="col">
<div class="status-card" data-status="Rejected">
<div class="mini-stat-box">
<div><p>Reject</p><h6>{{ $currentPeriodForms->where('status', 'Rejected')->count() }}</h6></div>
<i class="fas fa-ban text-danger fa-xs"></i>
</div>
</div>
</div>
<div class="col">
<div class="status-card" data-status="Closed">
<div class="mini-stat-box">
<div><p>Closed</p><h6>{{ $currentPeriodForms->where('status', 'Closed')->count() }}</h6></div>
<i class="fas fa-lock text-secondary fa-xs"></i>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="content">
<div class="container-fluid">
<div class="card card-primary card-outline">
@@ -116,10 +222,8 @@
<div class="dataTables_wrapper dt-bootstrap4 mt-2">
@include('backend.layouts.partials.messages')
<div class="table-responsive">
<table id="dataTable"
class="table table-bordered table-striped w-100 dataTable dtr-inline"
style="width:100%">
<thead class="bg-light text-capitalize" style="width: 100%">
<table id="dataTable" class="table table-bordered table-striped table-head-fixed dataTable dtr-inline w-100" style="width:100%">
<thead class="bg-light text-capitalize">
<tr>
<th>#</th>
<th>No Expense</th>
@@ -133,25 +237,19 @@
<th>Bukti Total</th>
<th>Account Number</th>
<th>Status</th>
@if (auth()->user()->can('approval.view'))
<th>Approval 1</th>
@endif
@if (auth()->user()->can('approval2.view'))
<th>Approval 2</th>
@endif
@if (auth()->user()->can('final_approval.view'))
<th>Final Approval</th>
@endif
@if (auth()->user()->can('approval.view')) <th>Approval 1</th> @endif
@if (auth()->user()->can('approval2.view')) <th>Approval 2</th> @endif
@if (auth()->user()->can('final_approval.view')) <th>Final Approval</th> @endif
<th>Aksi</th>
</tr>
</thead>
<tbody style="width: 100%">
<tbody>
@php
use App\Helpers\NextCloudHelper;
$role = auth()->user()->getRoleNames()[0];
@endphp
@foreach ($forms as $item)
<tr data-outstanding="{{ $outstandingIdCollection->contains($item->id) ? 1 : 0 }}">
<tr data-id="{{ $item->id }}" data-status="{{ $item->status }}" data-date="{{ \Carbon\Carbon::parse($item->tanggal)->format('Y-m-d') }}" data-outstanding="{{ $outstandingIdCollection->contains($item->id) ? '1' : '0' }}">
<td>{{ $loop->iteration }}</td>
<td>{{ $item->expense_number }}</td>
<td>{{ $item->user->name }}</td>
@@ -163,132 +261,80 @@
<td>{{ number_format($item->approved_total, 0, ',', '.') }}</td>
<td>
@if ($item->bukti_total)
<a href="{{ NextCloudHelper::getFileUrl($item->bukti_total) }}" target="_blank">
Download
</a>
@else
-
@endif
<a href="{{ NextCloudHelper::getFileUrl($item->bukti_total) }}" target="_blank">Download</a>
@else - @endif
</td>
<td>{{ $item->account_number }}</td>
<td>
@if ($item->status == 'On Progress')
<span class="badge badge-warning text-white">{{ $item->status }}</span>
@elseif ($item->status == 'Approved 1' || $item->status == 'Approved 2' || $item->status == 'Final Approved')
@elseif (in_array($item->status, ['Approved 1', 'Approved 2', 'Final Approved', 'Closed']))
<span class="badge badge-success text-white">{{ $item->status }}</span>
@else
<span class="badge badge-danger text-white">{{ $item->status }}</span>
@endif
</td>
{{-- SINKRONISASI MENYEMBUNYIKAN TOMBOL AKSI DIRECT DI INDEX (TETAP ADA DI DOM UNTUK JS EVENT) --}}
@if (auth()->user()->can('approval.view'))
<td>
{{-- approve / reject button --}}
@if (auth()->user()->can('approval.approve'))
@if ($item->status == 'On Progress')
<div class="d-none">
@if (auth()->user()->can('approval.approve') && $item->status == 'On Progress')
<form action="javascript:void(0);" class="d-inline">
<button type="button"
class="btn btn-success btn-sm final-approve-modal"
data-id="{{ route('forms.other.approve', $item->id) }}">
Approve 1
</button>
<button type="button" class="btn btn-success btn-sm final-approve-modal" data-id="{{ route('forms.other.approve', $item->id) }}">Approve 1</button>
</form>
<button type="button"
class="btn btn-danger btn-sm open-reject-modal"
data-action="{{ route('forms.other.reject', $item->id) }}">
Reject
</button>
@endif
@endif
@if ($item->approved_at)
{{ \Carbon\Carbon::parse($item->approved_at)->locale('id')->isoFormat('D MMMM Y HH:mm') }}
<button type="button" class="btn btn-danger btn-sm open-reject-modal" data-action="{{ route('forms.other.reject', $item->id) }}">Reject</button>
@endif
</div>
@if ($item->approved_at) <small>{{ \Carbon\Carbon::parse($item->approved_at)->format('d/m/y H:i') }}</small> @endif
</td>
@endif
@if (auth()->user()->can('approval2.view'))
<td>
{{-- approve / reject button --}}
@if (auth()->user()->can('approval2.approve'))
@if ($item->status == 'Approved 1' && $item->approved_at)
<div class="d-none">
@if (auth()->user()->can('approval2.approve') && $item->status == 'Approved 1' && $item->approved_at)
<form action="javascript:void(0);" class="d-inline">
<button type="button"
class="btn btn-success btn-sm open-approve-modal"
data-id="{{ route('forms.other.approve2', $item->id) }}">
Approve 2
</button>
<button type="button" class="btn btn-success btn-sm open-approve-modal" data-id="{{ route('forms.other.approve2', $item->id) }}">Approve 2</button>
</form>
<button type="button"
class="btn btn-danger btn-sm open-reject-modal"
data-action="{{ route('forms.other.reject', $item->id) }}">
Reject
</button>
@endif
@endif
@if ($item->approved2_at)
{{ \Carbon\Carbon::parse($item->approved2_at)->locale('id')->isoFormat('D MMMM Y HH:mm') }}
<button type="button" class="btn btn-danger btn-sm open-reject-modal" data-action="{{ route('forms.other.reject', $item->id) }}">Reject</button>
@endif
</div>
@if ($item->approved2_at) <small>{{ \Carbon\Carbon::parse($item->approved2_at)->format('d/m/y H:i') }}</small> @endif
</td>
@endif
@if(auth()->user()->can('final_approval.view'))
<td>
{{-- approve / reject button --}}
<div class="d-none">
@if(auth()->user()->can('final_approval.approve'))
@if ($item->status == 'Approved 2' && $item->approved2_at && !$item->final_approved_at)
<form action="javascript:void(0);" class="d-inline">
<button type="button"
class="btn btn-success btn-sm open-approve-modal"
data-id="{{ route('forms.other.final-approve', $item->id) }}">
Final Approve
</button>
<button type="button" class="btn btn-success btn-sm open-approve-modal" data-id="{{ route('forms.other.final-approve', $item->id) }}">Final Approve</button>
</form>
@else
@if ($item->final_approved_at && auth()->user()->can('final_approval.open'))
<form action="{{ route('forms.other.open', $item->id) }}" method="POST" class="d-inline"onsubmit="return confirm('Are you sure you want to approve this item?');">
@csrf
@method('PUT')
<button type="submit" class="btn btn-success btn-sm">
Open
</button>
<button type="button" class="btn btn-danger btn-sm open-reject-modal" data-action="{{ route('forms.other.reject', $item->id) }}">Reject</button>
@elseif ($item->final_approved_at && auth()->user()->can('final_approval.open'))
<form action="{{ route('forms.other.open', $item->id) }}" method="POST" class="d-inline" onsubmit="return confirm('Are you sure you want to open this item?');">
@csrf @method('PUT') <button type="submit" class="btn btn-success btn-sm">Open</button>
</form>
@endif
@endif
@endif
@if ($item->final_approved_at)
{{ \Carbon\Carbon::parse($item->final_approved_at)->locale('id')->isoFormat('D MMMM Y HH:mm') }}
@endif
</div>
@if ($item->final_approved_at) <small>{{ \Carbon\Carbon::parse($item->final_approved_at)->format('d/m/y H:i') }}</small> @endif
</td>
@endif
<td>
@if (auth()->user()->can('forms.other.view'))
<a href="{{ route('forms.other.view', $item->id) }}" class="btn btn-primary btn-sm">
<i class="fas fa-eye text-white"></i>
</a>
<a href="{{ route('forms.other.view', $item->id) }}" class="btn btn-primary btn-sm"><i class="fas fa-eye text-white"></i></a>
@endif
@php
$canEdit = false;
if (auth()->user()->can('forms.other.edit')) {
if (in_array($role, ['Medical Representatif', 'Area Manager Cabang'])) {
$canEdit = $item->status == 'On Progress';
} else {
$canEdit = true;
}
}
$canEdit = auth()->user()->can('forms.other.edit') && (!in_array($role, ['Medical Representatif', 'Area Manager Cabang']) || $item->status == 'On Progress');
@endphp
@if ($canEdit)
<a href="{{ route('forms.other.edit', $item->id) }}" class="btn btn-warning btn-sm">
<i class="fas fa-edit text-white"></i>
</a>
<a href="{{ route('forms.other.edit', $item->id) }}" class="btn btn-warning btn-sm"><i class="fas fa-edit text-white"></i></a>
@endif
@if (($role != 'Medical Representatif' && auth()->user()->can('forms.other.delete')) || ($role == 'Medical Representatif' && $item->status == 'On Progress'))
<form action="{{ route('forms.other.destroy', $item->id) }}" method="POST" class="d-inline" onsubmit="return confirm('Are you sure you want to delete this item?');">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-danger btn-sm">
<i class="fas fa-trash"></i>
</button>
@csrf @method('DELETE') <button type="submit" class="btn btn-danger btn-sm"><i class="fas fa-trash"></i></button>
</form>
@endif
</td>
@@ -302,108 +348,57 @@
</div>
</div>
</section>
<section>
<div class="modal fade" id="approveModal" tabindex="-1" role="dialog" aria-labelledby="approveModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<form id="approveForm" method="POST" action="">
@csrf
@method('PUT')
@csrf @method('PUT')
<div class="modal-header">
<h5 class="modal-title" id="approveModalLabel">Approve Item</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
</div>
<div class="modal-body">
<!-- Loading spinner -->
<div id="loadingSpinner" class="d-flex justify-content-center align-items-center" style="height: 200px;">
<div class="spinner-border text-primary" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
<!-- Modal content (hidden initially) -->
<div id="loadingSpinner" class="d-flex justify-content-center align-items-center" style="height: 200px;"><div class="spinner-border text-primary" role="status"></div></div>
<div id="modalContent" class="d-none">
<p>Please review the details below:</p>
<ul class="list-group">
<li class="list-group-item">
<strong>Expense Number:</strong> <span id="expense_number"></span>
</li>
<li class="list-group-item">
<strong>User:</strong> <span id="user"></span>
</li>
<li class="list-group-item">
<strong>Kategori:</strong> <span id="kategori"></span>
</li>
<li class="list-group-item">
<strong>Tanggal:</strong> <span id="tanggal"></span>
</li>
<li class="list-group-item">
<strong>Keterangan:</strong> <span id="keterangan"></span>
</li>
<li class="list-group-item">
<strong>Bukti Total:</strong> <span id="bukti_total"></span>
</li>
<li class="list-group-item">
<strong>Nominal Total:</strong> <span id="total_value"></span>
</li>
<li class="list-group-item"><strong>Expense Number:</strong> <span id="expense_number"></span></li>
<li class="list-group-item"><strong>User:</strong> <span id="user"></span></li>
<li class="list-group-item"><strong>Kategori:</strong> <span id="kategori"></span></li>
<li class="list-group-item"><strong>Tanggal:</strong> <span id="tanggal"></span></li>
<li class="list-group-item"><strong>Keterangan:</strong> <span id="keterangan"></span></li>
<li class="list-group-item"><strong>Bukti Total:</strong> <span id="bukti_total"></span></li>
<li class="list-group-item"><strong>Nominal Total:</strong> <span id="total_value"></span></li>
</ul>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Approve</button>
</div>
<div class="modal-footer"><button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button><button type="submit" class="btn btn-primary">Approve</button></div>
</form>
</div>
</div>
</div>
</section>
<section>
<div class="modal fade" id="finalApproveModal" tabindex="-1" role="dialog" aria-labelledby="finalApproveModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<form id="finalApproveForm" method="POST" action="">
@csrf
@method('PUT')
@csrf @method('PUT')
<div class="modal-header">
<h5 class="modal-title" id="approveModalLabel">Approve Item</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h5 class="modal-title" id="finalApproveModalLabel">Approve Item</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
</div>
<div class="modal-body">
<!-- Loading spinner -->
<div id="finalLoadingSpinner" class="d-flex justify-content-center align-items-center" style="height: 200px;">
<div class="spinner-border text-primary" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
<!-- Modal content (hidden initially) -->
<div id="finalLoadingSpinner" class="d-flex justify-content-center align-items-center" style="height: 200px;"><div class="spinner-border text-primary" role="status"></div></div>
<div id="finalModalContent" class="d-none">
<p>Please review the details below:</p>
<ul class="list-group">
<li class="list-group-item">
<strong>Expense Number:</strong> <span id="final_expense_number"></span>
</li>
<li class="list-group-item">
<strong>User:</strong> <span id="final_user"></span>
</li>
<li class="list-group-item">
<strong>Kategori:</strong> <span id="final_kategori"></span>
</li>
<li class="list-group-item">
<strong>Tanggal:</strong> <span id="final_tanggal"></span>
</li>
<li class="list-group-item">
<strong>Keterangan:</strong> <span id="final_keterangan"></span>
</li>
<li class="list-group-item">
<strong>Bukti Total:</strong> <span id="final_bukti_total"></span>
</li>
<li class="list-group-item"><strong>Expense Number:</strong> <span id="final_expense_number"></span></li>
<li class="list-group-item"><strong>User:</strong> <span id="final_user"></span></li>
<li class="list-group-item"><strong>Kategori:</strong> <span id="final_kategori"></span></li>
<li class="list-group-item"><strong>Tanggal:</strong> <span id="final_tanggal"></span></li>
<li class="list-group-item"><strong>Keterangan:</strong> <span id="final_keterangan"></span></li>
<li class="list-group-item"><strong>Bukti Total:</strong> <span id="final_bukti_total"></span></li>
</ul>
<p class="mt-2">Please select the items you want to approve:</p>
<ul class="list-group list-unstyled">
@@ -415,224 +410,175 @@
</div>
</div>
</li>
<li>
<div class="form-group">
<label for="total">Total</label>
<input type="text" class="form-control" id="total" name="total" value="0" readonly>
</div>
</li>
</ul>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Approve</button>
</div>
<div class="modal-footer"><button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button><button type="submit" class="btn btn-primary">Approve</button></div>
</form>
</div>
</div>
</div>
</section>
<section>
<div class="modal fade" id="rejectModal" tabindex="-1" role="dialog" aria-labelledby="rejectModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<form id="rejectForm" method="POST" action="">
@csrf
@method('PUT')
@csrf @method('PUT')
<div class="modal-header">
<h5 class="modal-title" id="rejectModalLabel">Reject Expense</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="rejectRemarks">Remarks <span class="text-danger">*</span></label>
<textarea class="form-control" name="remarks" id="rejectRemarks" rows="3" required></textarea>
</div>
<div class="form-group"><label>Remarks <span class="text-danger">*</span></label><textarea class="form-control" name="remarks" id="rejectRemarks" rows="3" required></textarea></div>
<p class="mb-0 text-muted small">Pengajuan akan ditolak setelah Anda mengirimkan catatan ini.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-danger">Reject</button>
</div>
<div class="modal-footer"><button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button><button type="submit" class="btn btn-danger">Reject</button></div>
</form>
</div>
</div>
</div>
</section>
@endsection
@section('scripts')
<script>
<script>
$(document).ready(function () {
// Uncheck all checkboxes on page refresh
$('.custom-control-input').prop('checked', false);
if ($('#dataTable').length) {
let outstandingFilterActive = false;
const outstandingFilter = function(settings, data, dataIndex) {
if (!outstandingFilterActive) {
return true;
}
const row = settings.aoData[dataIndex].nTr;
return $(row).data('outstanding') === 1 || $(row).data('outstanding') === '1';
};
let statusFilterActive = 'all';
$.fn.dataTable.ext.search.push(outstandingFilter);
// Custom Search DataTables (Filter Sinkron: Status + Outstanding + Siklus Periode Berjalan 11 - 13)
$.fn.dataTable.ext.search.push(function(settings, data, dataIndex) {
const row = $(settings.aoData[dataIndex].nTr);
const rowStatus = row.attr('data-status');
const isOutstanding = row.attr('data-outstanding') === '1' || row.attr('data-outstanding') === 1;
const rowDateStr = row.attr('data-date'); // Format: YYYY-MM-DD
// 1. Prioritas Pertama: Outstanding Filter Murni (Abaikan Batasan Tanggal)
if (outstandingFilterActive) {
return isOutstanding;
}
// 2. Prioritas Kedua: Aturan Jendela Waktu Siklus Aktif 11 s/d 13 Bulanan (Mutlak)
if (rowDateStr) {
const dateParts = rowDateStr.split('-');
const rowDate = new Date(dateParts[0], dateParts[1] - 1, dateParts[2], 12, 0, 0); // Waktu aman timezone
const now = new Date();
let startPeriod, endPeriod;
const startDay = 11;
const closingDay = 13;
if (now.getDate() >= startDay) {
startPeriod = new Date(now.getFullYear(), now.getMonth(), startDay, 0, 0, 0);
endPeriod = new Date(now.getFullYear(), now.getMonth() + 1, closingDay, 23, 59, 59);
} else {
startPeriod = new Date(now.getFullYear(), now.getMonth() - 1, startDay, 0, 0, 0);
endPeriod = new Date(now.getFullYear(), now.getMonth(), closingDay, 23, 59, 59);
}
if (rowDate < startPeriod || rowDate > endPeriod) {
return false;
}
}
// 3. Prioritas Ketiga: Filter Kategori Status Card
if (statusFilterActive !== 'all' && rowStatus !== statusFilterActive) return false;
return true;
});
const table = $('#dataTable').DataTable({
responsive: false,
dom: 'Blfrtip',
lengthMenu: [ [10, 50, 100, -1], [10, 50, 100, "All"] ], // Control the entries dropdown
lengthMenu: [ [10, 50, 100, -1], [10, 50, 100, "All"] ],
buttons: [
{
extend: 'excel',
text: 'Excel'
},
{
extend: 'pdf',
text: 'PDF',
orientation: 'landscape',
pageSize: 'A4',
exportOptions: {
columns: ':visible'
}
},
{
extend: 'print',
text: 'Print',
orientation: 'landscape',
exportOptions: {
columns: ':visible'
}
},
{ extend: 'excel', text: 'Excel' },
{ extend: 'pdf', text: 'PDF', orientation: 'landscape', pageSize: 'A4', exportOptions: { columns: ':visible' } },
{ extend: 'print', text: 'Print', orientation: 'landscape', exportOptions: { columns: ':visible' } },
'colvis'
]
});
const $outstandingCard = $('#outstandingCard');
const updateCardHint = (isActive) => {
const text = isActive ? 'Klik untuk menampilkan semua data' : 'Klik untuk menampilkan detail';
$outstandingCard.find('.filter-hint small').text(text);
};
updateCardHint(outstandingFilterActive);
// Klik Status Card
$('.status-card').on('click', function() {
outstandingFilterActive = false;
$('#outstandingCard').removeClass('active');
$('#outstandingCard').find('.filter-hint small').text('Klik untuk detail');
$('.status-card').removeClass('active-filter');
$(this).addClass('active-filter');
statusFilterActive = $(this).data('status');
table.draw();
});
// Klik Outstanding Card
const $outstandingCard = $('#outstandingCard');
$outstandingCard.on('click', function () {
outstandingFilterActive = !outstandingFilterActive;
$outstandingCard.toggleClass('active', outstandingFilterActive);
$(this).toggleClass('active', outstandingFilterActive);
if(outstandingFilterActive) {
$('.status-card').removeClass('active-filter');
$('.status-card[data-status="all"]').addClass('active-filter');
statusFilterActive = 'all';
}
table.draw();
updateCardHint(outstandingFilterActive);
$(this).find('.filter-hint small').text(outstandingFilterActive ? 'Tampilkan semua data' : 'Klik untuk detail');
});
}
// Handle Reject Button Click
$(document).on('click', '.open-reject-modal', function () {
const rejectUrl = $(this).data('action');
$('#rejectForm').attr('action', rejectUrl);
$('#rejectRemarks').val('');
$('#rejectForm').attr('action', $(this).data('action'));
$('#rejectRemarks').val('').focus();
$('#rejectModal').modal('show');
});
$('#rejectModal').on('shown.bs.modal', function () {
$('#rejectRemarks').trigger('focus');
});
$('#rejectModal').on('hidden.bs.modal', function () {
$('#rejectForm').attr('action', '');
$('#rejectRemarks').val('');
});
$('#rejectModal').on('shown.bs.modal', function () { $('#rejectRemarks').trigger('focus'); });
$('#rejectModal').on('hidden.bs.modal', function () { $('#rejectForm').attr('action', ''); $('#rejectRemarks').val(''); });
// Handle Approve Button Click
$(document).on('click', '.open-approve-modal', function() {
const approveUrl = $(this).data('id');
$('#loadingSpinner').show(); $('#modalContent').addClass('d-none');
$('#approveForm').attr('action', approveUrl); $('#approveModal').modal('show');
// Show spinner and hide content initially
$('#loadingSpinner').show();
$('#modalContent').addClass('d-none');
$('#approveForm').attr('action', approveUrl); // Set the form action
$('#loadingSpinner').addClass('d-flex');
$('#approveModal').modal('show'); // Show the modal
// Get detail from /forms/vehicle/detail/{id}
$.get(approveUrl.replace(/approve2|final-approve/g, 'detail'), function (data) {
const formatter = new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
});
const formatter = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 });
$('#total_value').text(formatter.format(data.total));
// Detail data
$('#user').text(data.user.name);
$('#expense_number').text(data.expense_number);
$('#tanggal').text(new Date(data.created_at).toLocaleDateString('id-ID', {
year: 'numeric',
month: 'long',
day: 'numeric'
}));
$('#kategori').text(data.kategori.name);
$('#keterangan').text(data.keterangan);
$('#bukti_total').html(data.total ? '<a href="' + data.total + '" target="_blank">Download</a>' : '-');
// Hide spinner and show content
$('#loadingSpinner').hide();
$('#loadingSpinner').removeClass('d-flex');
$('#loadingSpinner').addClass('d-none');
$('#modalContent').removeClass('d-none');
// Reset the total to zero
$('#user').text(data.user.name); $('#expense_number').text(data.expense_number);
$('#tanggal').text(new Date(data.tanggal).toLocaleDateString('id-ID', { year: 'numeric', month: 'long', day: 'numeric' }));
$('#kategori').text(data.kategori.name); $('#keterangan').text(data.keterangan);
$('#bukti_total').html(data.bukti_total ? '<a href="' + data.bukti_total + '" target="_blank">Download</a>' : '-');
$('#loadingSpinner').hide(); $('#modalContent').removeClass('d-none');
$('#total').val(0);
});
});
// Handle Approve Button Click
// Handle Final Approve Modal
$(document).on('click', '.final-approve-modal', function() {
const approveUrl = $(this).data('id');
$('#finalLoadingSpinner').show(); $('#finalModalContent').addClass('d-none');
$('#finalApproveForm').attr('action', approveUrl); $('#finalApproveModal').modal('show');
// Show spinner and hide content initially
$('#finalLoadingSpinner').show();
$('#finalModalContent').addClass('d-none');
$('#finalApproveForm').attr('action', approveUrl); // Set the form action
$('#finalLoadingSpinner').addClass('d-flex');
$('#finalApproveModal').modal('show'); // Show the modal
// Get detail from /forms/vehicle/detail/{id}
$.get(approveUrl.replace('approve', 'detail'), function (data) {
console.log(data);
const formatter = new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
});
const formatter = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 });
$('#final_total_value').text(formatter.format(data.total));
// Assign data values to checkboxes
$('#final_total').data('value', data.total);
// Detail data
$('#final_user').text(data.user.name);
$('#final_expense_number').text(data.expense_number);
$('#final_tanggal').text(new Date(data.created_at).toLocaleDateString('id-ID', {
year: 'numeric',
month: 'long',
day: 'numeric'
}));
$('#final_kategori').text(data.kategori.name);
$('#final_keterangan').text(data.keterangan);
$('#final_user').text(data.user.name); $('#final_expense_number').text(data.expense_number);
$('#final_tanggal').text(new Date(data.tanggal).toLocaleDateString('id-ID', { year: 'numeric', month: 'long', day: 'numeric' }));
$('#final_kategori').text(data.kategori.name); $('#final_keterangan').text(data.keterangan);
$('#final_bukti_total').html(data.bukti_total ? '<a href="' + data.bukti_total + '" target="_blank">Download</a>' : '-');
// Hide spinner and show content
$('#finalLoadingSpinner').hide();
$('#finalLoadingSpinner').removeClass('d-flex');
$('#finalLoadingSpinner').addClass('d-none');
$('#finalModalContent').removeClass('d-none');
// Reset the total to zero
$('#finalLoadingSpinner').hide(); $('#finalModalContent').removeClass('d-none');
$('#total').val(0);
});
});
@@ -640,25 +586,14 @@
// Update total dynamically
$('.custom-control-input').on('change', function () {
let total = 0;
$('.custom-control-input:checked').each(function () {
total += parseFloat($(this).data('value') || 0);
});
const formatter = new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0,
});
$('.custom-control-input:checked').each(function () { total += parseFloat($(this).data('value') || 0); });
const formatter = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 });
$('#total').val(formatter.format(total));
});
// Uncheck all checkboxes when the modal is closed
$('#finalApproveModal').on('hidden.bs.modal', function () {
$('.custom-control-input').prop('checked', false); // Uncheck all checkboxes
$('#total').val('0'); // Reset total to 0
$('.custom-control-input').prop('checked', false); $('#total').val('0');
});
});
</script>
</script>
@endsection
@@ -5,7 +5,9 @@
@endsection
@section('admin-content')
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script src="https://cdn.jsdelivr.net/npm/autonumeric@4.6.0/dist/autoNumeric.min.js"></script>
<section class="content-header">
<div class="container-fluid">
<div class="row mb-2">
@@ -15,12 +17,12 @@
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="{{ route('dashboard.index') }}">Dashboard</a></li>
</li>
</ol>
</div>
</div>
</div>
</section>
<style>
#loading-spinner-overlay {
position: fixed;
@@ -28,8 +30,8 @@
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5); /* Semi-transparent black background */
z-index: 9999; /* High z-index to cover everything */
background-color: rgba(0, 0, 0, 0.5);
z-index: 9999;
display: flex;
justify-content: center;
align-items: center;
@@ -40,10 +42,12 @@
color: white;
}
</style>
@php
$attachmentCategoryLabels = $attachmentCategoryLabels ?? ($upCountryAttachmentCategories ?? []);
$attachmentCategories = $attachmentCategories ?? ($upCountryAttachmentCategoryKeys ?? array_keys($attachmentCategoryLabels));
@endphp
<section class="content">
<div class="container-fluid">
<div class="card card-primary card-outline">
@@ -78,22 +82,22 @@
</div>
<div class="mb-3">
<label class="form-label">Allowance <span class="font-italic font-weight-normal">(optional)</span></label>
<input type="string" class="form-control" name="allowance" id="allowance" value="{{ old('allowance') }}">
<input type="text" class="form-control autonumeric-input" name="allowance" id="allowance" value="{{ old('allowance') }}">
</div>
</div>
<div class="col-lg-6">
<div class="mb-3">
<label class="form-label">Transport Dalam Kota <span class="font-italic font-weight-normal">(optional)</span></label>
<input type="string" class="form-control" name="transport_dalkot" id="transport_dalkot" value="{{ old('transport_dalkot') }}">
<input type="text" class="form-control autonumeric-input" name="transport_dalkot" id="transport_dalkot" value="{{ old('transport_dalkot') }}">
</div>
<div class="mb-3">
<label class="form-label">Transport Antar Kota <span class="font-italic font-weight-normal">(optional)</span></label>
<input type="string" class="form-control" name="transport_ankot" id="transport_ankot" value="{{ old('transport_ankot') }}">
<input type="text" class="form-control autonumeric-input" name="transport_ankot" id="transport_ankot" value="{{ old('transport_ankot') }}">
</div>
<div class="mb-3">
<label class="form-label">Hotel <span class="font-italic font-weight-normal">(optional)</span></label>
<input type="string" class="form-control" name="hotel" id="hotel" value="{{ old('hotel') }}">
<input type="text" class="form-control autonumeric-input" name="hotel" id="hotel" value="{{ old('hotel') }}">
</div>
</div>
@@ -146,66 +150,68 @@
<div id="loading-spinner-overlay" class="d-none">
<div class="spinner-wrapper">
<div class="spinner-border text-primary" role="status">
</div>
<p>Sedang mengirim, mohon tunggu...</p>
<div class="spinner-border text-primary" role="status"></div>
<p class="mt-2">Sedang mengirim, mohon tunggu...</p>
</div>
</div>
</div>
</div>
</div>
</section>
@endsection
@section('scripts')
<script>
new AutoNumeric('#allowance', {
// Gunakan DOMContentLoaded agar script terisolasi dan langsung jalan sebelum jQuery external selesai
document.addEventListener('DOMContentLoaded', function() {
try {
// ==========================================
// 1. INISIALISASI AUTONUMERIC ASLI & AMAN
// ==========================================
const autoNumericConfig = {
digitGroupSeparator: '.',
decimalCharacter: ',',
currencySymbol: 'Rp.',
currencySymbol: 'Rp. ',
decimalPlaces: 0,
unformatOnSubmit: true
unformatOnSubmit: true // Mengirim nilai integer murni ke backend
};
const allowanceInput = new AutoNumeric('#allowance', autoNumericConfig);
const dalkotInput = new AutoNumeric('#transport_dalkot', autoNumericConfig);
const ankotInput = new AutoNumeric('#transport_ankot', autoNumericConfig);
const hotelInput = new AutoNumeric('#hotel', autoNumericConfig);
// ==========================================
// 2. BLOKIR SCROLL WHEEL DENGAN EVENT BROWSER NATIVE
// ==========================================
// Mencegah input berubah tanpa memicu error library
const inputIds = ['allowance', 'transport_dalkot', 'transport_ankot', 'hotel'];
inputIds.forEach(function(id) {
const el = document.getElementById(id);
if (el) {
el.addEventListener('wheel', function(e) {
e.preventDefault(); // Menghentikan angka bergulir
this.blur(); // Melepas fokus kursor
}, { passive: false });
}
});
new AutoNumeric('#transport_dalkot', {
digitGroupSeparator: '.',
decimalCharacter: ',',
currencySymbol: 'Rp.',
decimalPlaces: 0,
unformatOnSubmit: true
});
new AutoNumeric('#transport_ankot', {
digitGroupSeparator: '.',
decimalCharacter: ',',
currencySymbol: 'Rp.',
decimalPlaces: 0,
unformatOnSubmit: true
});
new AutoNumeric('#hotel', {
digitGroupSeparator: '.',
decimalCharacter: ',',
currencySymbol: 'Rp.',
decimalPlaces: 0,
unformatOnSubmit: true
});
// ==========================================
// 3. LOGIKA MULTI UPLOAD ATTACHMENT
// ==========================================
const attachmentCategories = @json($attachmentCategories ?? ($upCountryAttachmentCategoryKeys ?? []));
const attachmentCategoryLabels = @json($attachmentCategoryLabels ?? ($upCountryAttachmentCategories ?? []));
const fileAccept = '.jpg,.jpeg,.png,.pdf';
let attachmentIndex = 0;
function categoryToLabel(value) {
if (!value) {
return 'Pilih Kategori';
}
if (!value) return 'Pilih Kategori';
if (attachmentCategoryLabels && attachmentCategoryLabels[value]) {
return attachmentCategoryLabels[value];
}
return value
.replace(/_/g, ' ')
.replace(/\b\w/g, (char) => char.toUpperCase());
return value.replace(/_/g, ' ').replace(/\b\w/g, (char) => char.toUpperCase());
}
function buildCategoryOptions() {
@@ -215,8 +221,7 @@
}
function resetInlinePreview($row) {
$row
.find('.attachment-inline-preview')
$row.find('.attachment-inline-preview')
.removeClass('bg-light')
.html('<i class="fas fa-file-upload text-muted"></i>');
}
@@ -226,7 +231,6 @@
if (existingUrl) {
URL.revokeObjectURL(existingUrl);
}
$row.removeData('objectUrl');
$row.removeData('previewType');
$row.removeData('previewSource');
@@ -237,8 +241,6 @@
function addAttachmentRow() {
const attachmentTableBody = $('#attachments-table tbody');
const emptyRowMarkup = `<tr class="attachments-empty text-center text-muted"><td colspan="4">Belum ada lampiran ditambahkan.</td></tr>`;
if (attachmentTableBody.find('.attachments-empty').length) {
attachmentTableBody.empty();
}
@@ -257,7 +259,7 @@
<div class="attachment-inline-preview border rounded d-flex align-items-center justify-content-center bg-white" style="width: 56px; height: 56px;">
<i class="fas fa-file-upload text-muted"></i>
</div>
<input type="file" class="form-control attachment-file" name="attachments[${index}][file_path]" accept="${fileAccept}">
<input type="file" class="form-control attachment-file" name="attachments[${index}][file_path]" accept="${fileAccept}" required>
</div>
<div class="small text-muted mt-1 preview-filename"></div>
</td>
@@ -273,7 +275,6 @@
</td>
</tr>
`);
attachmentTableBody.append(row);
}
@@ -292,10 +293,9 @@
function setInlinePreview($row, type, source) {
const previewBox = $row.find('.attachment-inline-preview');
if (type === 'image' && source) {
previewBox
.html(`<img src="${source}" class="img-thumbnail" style="width: 100%; height: 100%; object-fit: cover;" alt="Pratinjau lampiran">`)
.html(`<img src="${source}" class="img-thumbnail" style="width: 100%; height: 100%; object-fit: cover;" alt="Pratinjau">`)
.addClass('bg-light');
} else if (type === 'pdf') {
previewBox
@@ -341,9 +341,7 @@
clearRowData($row);
const file = input.files && input.files[0];
if (!file) {
return;
}
if (!file) return;
filenameHolder.text(file.name);
const extension = file.name.split('.').pop().toLowerCase();
@@ -368,24 +366,54 @@
}
}
$(function () {
const spinnerOverlay = $('#loading-spinner-overlay');
$('#expense-form').on('submit', function () {
spinnerOverlay.removeClass('d-none').addClass('d-flex');
// ==========================================
// 4. INTERCEPTOR FORM SUBMISSION DENGAN SWEETALERT (> 1 JUTA)
// ==========================================
$('#expense-form').on('submit', function (e) {
e.preventDefault();
const form = this;
// Mengambil nilai desimal asli dari object AutoNumeric
const allowance = allowanceInput.getNumber() || 0;
const transportDalkot = dalkotInput.getNumber() || 0;
const transportAnkot = ankotInput.getNumber() || 0;
const hotel = hotelInput.getNumber() || 0;
const totalExpense = allowance + transportDalkot + transportAnkot + hotel;
if (totalExpense > 1000000) {
Swal.fire({
title: 'Nominal Melebihi Batas Expense!',
text: `Total pengajuan Anda adalah Rp ${new Intl.NumberFormat('id-ID').format(totalExpense)}. Jumlah ini melebihi batas standar Rp 1.000.000. Apakah yakin tetap ajukan?`,
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Yes, Tetap Ajukan!',
cancelButtonText: 'No, Batalkan'
}).then((result) => {
if (result.isConfirmed) {
$('#loading-spinner-overlay').removeClass('d-none').addClass('d-flex');
form.submit();
}
});
} else {
$('#loading-spinner-overlay').removeClass('d-none').addClass('d-flex');
form.submit();
}
});
$('#add-attachment-row').on('click', function () {
// ==========================================
// 5. EVENT BINDINGS
// ==========================================
$('#add-attachment-row').on('click', function (e) {
e.preventDefault();
addAttachmentRow();
});
$(document).on('click', '.remove-attachment-row', function () {
removeAttachmentRow(this);
});
$(document).on('change', '.attachment-file', function () {
previewFile(this);
});
$(document).on('click', '.remove-attachment-row', function () { removeAttachmentRow(this); });
$(document).on('change', '.attachment-file', function () { previewFile(this); });
$(document).on('click', '.preview-new-attachment', function () {
const $row = $(this).closest('tr');
@@ -396,6 +424,10 @@
openAttachmentModal(modalSelector, category, previewType, previewSource);
});
} catch (err) {
console.error("Local Script Error:", err);
}
});
</script>
@endsection
@@ -5,8 +5,9 @@
@endsection
@section('admin-content')
<script src="https://cdn.jsdelivr.net/npm/autonumeric@4.6.0/dist/autoNumeric.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script src="https://cdn.jsdelivr.net/npm/autonumeric@4.6.0/dist/autoNumeric.min.js"></script>
<section class="content-header">
<div class="container-fluid">
<div class="row mb-2">
@@ -16,12 +17,12 @@
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="{{ route('dashboard.index') }}">Dashboard</a></li>
</li>
</ol>
</div>
</div>
</div>
</section>
<style>
#loading-spinner-overlay {
position: fixed;
@@ -29,8 +30,8 @@
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5); /* Semi-transparent black background */
z-index: 9999; /* High z-index to cover everything */
background-color: rgba(0, 0, 0, 0.5);
z-index: 9999;
display: flex;
justify-content: center;
align-items: center;
@@ -41,10 +42,12 @@
color: white;
}
</style>
@php
$attachmentCategoryLabels = $attachmentCategoryLabels ?? ($upCountryAttachmentCategories ?? []);
$attachmentCategories = $attachmentCategories ?? ($upCountryAttachmentCategoryKeys ?? array_keys($attachmentCategoryLabels));
@endphp
<section class="content">
<div class="container-fluid">
<div class="card card-primary card-outline">
@@ -68,7 +71,8 @@
</div>
<div class="mb-3">
<label class="form-label">Tanggal <span class="font-italic font-weight-normal">(required)</span></label>
<input type="date" class="form-control" name="tanggal" required value="{{ $form->tanggal }}">
<input type="date" class="form-control" name="tanggal" required
value="{{ old('tanggal', !empty($form->tanggal) ? date('Y-m-d', strtotime($form->tanggal)) : '') }}">
</div>
<div class="mb-3">
<label class="form-label">Tujuan <span class="font-italic font-weight-normal">(required)</span></label>
@@ -80,22 +84,22 @@
</div>
<div class="mb-3">
<label class="form-label">Allowance <span class="font-italic font-weight-normal">(optional)</span></label>
<input type="string" class="form-control" name="allowance" id="allowance" value="{{ $form->allowance }}">
<input type="text" class="form-control" name="allowance" id="allowance" value="{{ $form->allowance }}">
</div>
</div>
<div class="col-lg-6">
<div class="mb-3">
<label class="form-label">Transport Dalam Kota <span class="font-italic font-weight-normal">(optional)</span></label>
<input type="string" class="form-control" name="transport_dalkot" id="transport_dalkot" value="{{ $form->transport_dalkot }}">
<input type="text" class="form-control" name="transport_dalkot" id="transport_dalkot" value="{{ $form->transport_dalkot }}">
</div>
<div class="mb-3">
<label class="form-label">Transport Antar Kota <span class="font-italic font-weight-normal">(optional)</span></label>
<input type="string" class="form-control" name="transport_ankot" id="transport_ankot" value="{{ $form->transport_ankot }}">
<input type="text" class="form-control" name="transport_ankot" id="transport_ankot" value="{{ $form->transport_ankot }}">
</div>
<div class="mb-3">
<label class="form-label">Hotel <span class="font-italic font-weight-normal">(optional)</span></label>
<input type="string" class="form-control" name="hotel" id="hotel" value="{{ $form->hotel }}">
<input type="text" class="form-control" name="hotel" id="hotel" value="{{ $form->hotel }}">
</div>
</div>
@@ -217,7 +221,7 @@
<div class="spinner-wrapper">
<div class="spinner-border text-primary" role="status">
</div>
<p>Sedang mengirim, mohon tunggu...</p>
<p class="mt-2">Sedang mengirim, mohon tunggu...</p>
</div>
</div>
</div>
@@ -226,59 +230,62 @@
</section>
<script>
new AutoNumeric('#allowance', {
$(document).ready(function() {
try {
// ==========================================
// 1. INISIALISASI AUTONUMERIC ASLI (KEMBALI NORMAL & ANTI MINUS)
// ==========================================
const autoNumericConfig = {
digitGroupSeparator: '.',
decimalCharacter: ',',
currencySymbol: 'Rp.',
currencySymbol: 'Rp. ',
decimalPlaces: 0,
unformatOnSubmit: true
minimumValue: '0', // Mencegah nilai minus
unformatOnSubmit: true // Mengirimkan angka bersih ke backend
};
const allowanceInput = new AutoNumeric('#allowance', autoNumericConfig);
const dalkotInput = new AutoNumeric('#transport_dalkot', autoNumericConfig);
const ankotInput = new AutoNumeric('#transport_ankot', autoNumericConfig);
const hotelInput = new AutoNumeric('#hotel', autoNumericConfig);
// ==========================================
// 2. SOLUSI FIX SCROLLING: Blokir Paksa Menggunakan jQuery Native
// ==========================================
$('#allowance, #transport_dalkot, #transport_ankot, #hotel').on('wheel font-wheel', function(e) {
e.preventDefault();
$(this).blur(); // Melepas fokus kursor saat roda scroll digerakkan
});
new AutoNumeric('#transport_dalkot', {
digitGroupSeparator: '.',
decimalCharacter: ',',
currencySymbol: 'Rp.',
decimalPlaces: 0,
unformatOnSubmit: true
});
new AutoNumeric('#transport_ankot', {
digitGroupSeparator: '.',
decimalCharacter: ',',
currencySymbol: 'Rp.',
decimalPlaces: 0,
unformatOnSubmit: true
});
new AutoNumeric('#hotel', {
digitGroupSeparator: '.',
decimalCharacter: ',',
currencySymbol: 'Rp.',
decimalPlaces: 0,
unformatOnSubmit: true
});
// ==========================================
// 3. LOGIKA MULTI UPLOAD ATTACHMENT
// ==========================================
const attachmentCategories = @json($attachmentCategories ?? ($upCountryAttachmentCategoryKeys ?? []));
const attachmentCategoryLabels = @json($attachmentCategoryLabels ?? ($upCountryAttachmentCategories ?? []));
const fileAccept = '.jpg,.jpeg,.png,.pdf';
let attachmentIndex = 0;
let newAttachmentsTableBody;
let existingAttachmentsTableBody;
let emptyNewRowMarkup = '';
let emptyExistingRowMarkup = '';
let newAttachmentsTableBody = $('#new-attachments-table tbody');
let existingAttachmentsTableBody = $('#existing-attachments-table tbody');
let emptyNewRowMarkup = `<tr class="new-attachments-empty text-center text-muted"><td colspan="4">Belum ada lampiran baru.</td></tr>`;
let emptyExistingRowMarkup = `<tr class="existing-attachments-empty text-center text-muted"><td colspan="5">Belum ada lampiran tersimpan.</td></tr>`;
function categoryToLabel(value) {
if (!value) {
return 'Pilih Kategori';
const csrfToken = $('meta[name="csrf-token"]').attr('content');
if (csrfToken) {
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': csrfToken
}
});
}
function categoryToLabel(value) {
if (!value) return 'Pilih Kategori';
if (attachmentCategoryLabels && attachmentCategoryLabels[value]) {
return attachmentCategoryLabels[value];
}
return value
.replace(/_/g, ' ')
.replace(/\b\w/g, (char) => char.toUpperCase());
return value.replace(/_/g, ' ').replace(/\b\w/g, (char) => char.toUpperCase());
}
function buildCategoryOptions() {
@@ -288,8 +295,7 @@
}
function resetInlinePreview($row) {
$row
.find('.attachment-inline-preview')
$row.find('.attachment-inline-preview')
.removeClass('bg-light')
.html('<i class="fas fa-file-upload text-muted"></i>');
}
@@ -299,7 +305,6 @@
if (existingUrl) {
URL.revokeObjectURL(existingUrl);
}
$row.removeData('objectUrl');
$row.removeData('previewType');
$row.removeData('previewSource');
@@ -308,10 +313,8 @@
$row.find('.preview-filename').text('');
}
function addAttachmentRow() {
if (!newAttachmentsTableBody) {
return;
}
window.addAttachmentRow = function() {
if (!newAttachmentsTableBody) return;
if (newAttachmentsTableBody.find('.new-attachments-empty').length) {
newAttachmentsTableBody.empty();
@@ -331,12 +334,12 @@
<div class="attachment-inline-preview border rounded d-flex align-items-center justify-content-center bg-white" style="width: 56px; height: 56px;">
<i class="fas fa-file-upload text-muted"></i>
</div>
<input type="file" class="form-control attachment-file" name="attachments[${index}][file_path]" accept="${fileAccept}">
<input type="file" class="form-control attachment-file" name="attachments[${index}][file_path]" accept="${fileAccept}" required>
</div>
<div class="small text-muted mt-1 preview-filename"></div>
</td>
<td class="text-center">
<button type="button" class="btn btn-sm btn-outline-secondary preview-new-attachment d-none">
<button type="button" class="btn btn-sm btn-outline-secondary preview-new-attachment d-none" data-modal="#newAttachmentPreviewModal">
Pratinjau
</button>
</td>
@@ -347,14 +350,11 @@
</td>
</tr>
`);
newAttachmentsTableBody.append(row);
}
function removeAttachmentRow(button) {
if (!newAttachmentsTableBody) {
return;
}
if (!newAttachmentsTableBody) return;
const $row = $(button).closest('tr');
clearRowData($row);
@@ -443,28 +443,47 @@
}
}
$(function () {
// ==========================================
// 4. INTERCEPTOR FORM SUBMIT & SWEETALERT (> 1 JUTA)
// ==========================================
const spinnerOverlay = $('#loading-spinner-overlay');
$('#expense-form').on('submit', function () {
$('#expense-form').on('submit', function (e) {
e.preventDefault();
const form = this;
const allowance = allowanceInput.getNumber() || 0;
const transportDalkot = dalkotInput.getNumber() || 0;
const transportAnkot = ankotInput.getNumber() || 0;
const hotel = hotelInput.getNumber() || 0;
const totalExpense = allowance + transportDalkot + transportAnkot + hotel;
if (totalExpense > 1000000) {
Swal.fire({
title: 'Nominal Melebihi Batas Expense!',
text: `Total pengajuan Anda adalah Rp ${new Intl.NumberFormat('id-ID').format(totalExpense)}. Jumlah ini melebihi batas standar Rp 1.000.000. Apakah yakin tetap ajukan?`,
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Yes, Tetap Ajukan!',
cancelButtonText: 'No, Batalkan'
}).then((result) => {
if (result.isConfirmed) {
spinnerOverlay.removeClass('d-none').addClass('d-flex');
});
newAttachmentsTableBody = $('#new-attachments-table tbody');
existingAttachmentsTableBody = $('#existing-attachments-table tbody');
emptyNewRowMarkup = `<tr class="new-attachments-empty text-center text-muted"><td colspan="4">Belum ada lampiran baru.</td></tr>`;
emptyExistingRowMarkup = `<tr class="existing-attachments-empty text-center text-muted"><td colspan="5">Belum ada lampiran tersimpan.</td></tr>`;
const csrfToken = $('meta[name="csrf-token"]').attr('content');
if (csrfToken) {
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': csrfToken
form.submit();
}
});
} else {
spinnerOverlay.removeClass('d-none').addClass('d-flex');
form.submit();
}
});
$('#add-new-attachment-row').on('click', function () {
// Event Listeners DOM
$('#add-new-attachment-row').on('click', function (e) {
e.preventDefault();
addAttachmentRow();
});
@@ -515,9 +534,7 @@
confirmButtonText: 'Ya, hapus',
cancelButtonText: 'Batal'
}).then((result) => {
if (!result.isConfirmed) {
return;
}
if (!result.isConfirmed) return;
$.ajax({
url: deleteUrl,
@@ -527,7 +544,6 @@
if (!existingAttachmentsTableBody.find('tr').not('.existing-attachments-empty').length) {
existingAttachmentsTableBody.html(emptyExistingRowMarkup);
}
Swal.fire('Berhasil', response?.message || 'Lampiran berhasil dihapus.', 'success');
},
error: function (xhr) {
@@ -537,6 +553,10 @@
});
});
});
} catch (err) {
console.error("Local Script Error:", err);
}
});
</script>
@endsection
File diff suppressed because it is too large Load Diff
@@ -6,6 +6,8 @@
@section('admin-content')
<script src="https://cdn.jsdelivr.net/npm/autonumeric@4.6.0/dist/autoNumeric.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<section class="content-header">
<div class="container-fluid">
<div class="row mb-2">
@@ -15,12 +17,12 @@
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="{{ route('dashboard.index') }}">Dashboard</a></li>
</li>
</ol>
</div>
</div>
</div>
</section>
<section class="content">
<div class="container-fluid">
<div class="card card-primary card-outline">
@@ -43,8 +45,9 @@
</select>
</div>
<div class="mb-3">
<label class="form-label">Tanggal</label>
<input type="date" class="form-control" name="tanggal" value="{{ $form->tanggal }}" readonly>
<label class="form-label">Tanggal Penggunaan</label>
<input type="text" class="form-control" name="tanggal"
value="{{ $form->tanggal ? \Carbon\Carbon::parse($form->tanggal)->locale('id')->translatedFormat('d F Y') : '-' }}" readonly>
</div>
<div class="mb-3">
<label class="form-label">Tujuan</label>
@@ -56,22 +59,22 @@
</div>
<div class="mb-3">
<label class="form-label">Allowance</label>
<input type="string" class="form-control" name="allowance" id="allowance" value="{{ $form->allowance }}" readonly>
<input type="text" class="form-control" name="allowance" id="allowance" value="{{ $form->allowance }}" readonly>
</div>
<div class="mb-3">
<label class="form-label">Transport Dalam Kota</label>
<input type="string" class="form-control" name="transport_dalkot" id="transport_dalkot" value="{{ $form->transport_dalkot }}" readonly>
<input type="text" class="form-control" name="transport_dalkot" id="transport_dalkot" value="{{ $form->transport_dalkot }}" readonly>
</div>
<div class="mb-3">
<label class="form-label">Transport Antar Kota</label>
<input type="string" class="form-control" name="transport_ankot" id="transport_ankot" value="{{ $form->transport_ankot }}" readonly>
<input type="text" class="form-control" name="transport_ankot" id="transport_ankot" value="{{ $form->transport_ankot }}" readonly>
</div>
</div>
<div class="col-lg-6">
<div class="mb-3">
<label class="form-label">Hotel</label>
<input type="string" class="form-control" name="hotel" id="hotel" value="{{ $form->hotel }}" readonly>
<input type="text" class="form-control" name="hotel" id="hotel" value="{{ $form->hotel }}" readonly>
</div>
<div class="mb-3">
<label class="form-label">Total</label>
@@ -83,14 +86,17 @@
</div>
<div class="mb-3">
<label class="form-label">UC Plan</label><br>
<a href="{{ $form->uc_plan }}" target="_blank">Lihat Bukti</a>
@if($form->uc_plan)
<a href="{{ $form->uc_plan }}" target="_blank" class="btn btn-sm btn-outline-info px-3"><i class="fas fa-file-alt mr-1"></i> Lihat Dokumen Plan</a>
@else
<span class="text-muted small font-italic">- Tidak ada plan diunggah -</span>
@endif
</div>
</div>
<div class="col-12">
<div class="mb-4">
<label class="form-label">Lampiran</label>
<label class="form-label">Lampiran Bukti Transaksi</label>
<div class="table-responsive">
<table class="table table-bordered align-middle">
<thead class="table-light">
@@ -130,7 +136,7 @@
</tr>
@empty
<tr class="text-center text-muted">
<td colspan="4">Tidak ada lampiran.</td>
<td colspan="4">Tidak ada lampiran bukti tersedia.</td>
</tr>
@endforelse
</tbody>
@@ -143,20 +149,10 @@
<a href="{{ route('forms.up-country') }}" class="btn btn-secondary">Kembali</a>
@if (auth()->user()->can('approval.approve') && $form->status == 'On Progress')
<button type="button" class="btn btn-success final-approve-modal" data-id="{{ route('forms.up-country.approve', $form->id) }}">Approve 1</button>
<button type="button"
class="btn btn-danger open-reject-modal"
data-action="{{ route('forms.up-country.reject', $form->id) }}">
Reject
</button>
<button type="button" class="btn btn-danger open-reject-modal" data-action="{{ route('forms.up-country.reject', $form->id) }}">Reject</button>
@elseif(auth()->user()->can('approval2.approve') && $form->status == 'Approved 1')
<button type="button" class="btn btn-success open-approve-modal" data-id="{{ route('forms.up-country.approve2', $form->id) }}">Approve 2</button>
<button type="button"
class="btn btn-danger open-reject-modal"
data-action="{{ route('forms.up-country.reject', $form->id) }}">
Reject
</button>
<button type="button" class="btn btn-danger open-reject-modal" data-action="{{ route('forms.up-country.reject', $form->id) }}">Reject</button>
@elseif(auth()->user()->can('final_approval.approve') && $form->status == 'Approved 2')
<button type="button" class="btn btn-primary open-approve-modal" data-id="{{ route('forms.up-country.final-approve', $form->id) }}">Final Approve</button>
@endif
@@ -167,163 +163,148 @@
</div>
</div>
</section>
{{-- MODAL PREVIEW DAN APPROVAL SYSTEM --}}
<section>
@include('backend.components.attachment-preview-modal', [
'modalId' => 'viewAttachmentPreviewModal',
'title' => 'Attachment Preview',
])
{{-- MODAL APPROVE 2 --}}
<div class="modal fade" id="approveModal" tabindex="-1" role="dialog" aria-labelledby="approveModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-content" style="border-radius: 12px; overflow: hidden;">
<form id="approveForm" method="POST" action="">
@csrf
@method('PUT')
<div class="modal-header">
<h5 class="modal-title" id="approveModalLabel">Approve Item</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title fw-bold" id="approveModalLabel"><i class="fas fa-check-heading mr-2"></i>Review & Approve Pengajuan</h5>
<button type="button" class="close text-white" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<!-- Loading spinner -->
<div id="loadingSpinner" class="d-flex justify-content-center align-items-center" style="height: 200px;">
<div class="spinner-border text-primary" role="status">
<span class="sr-only">Loading...</span>
<div class="modal-body bg-light">
<div id="loadingSpinner" class="d-flex justify-content-center align-items-center" style="height: 250px;">
<div class="text-center">
<div class="spinner-border text-primary" role="status"></div>
<p class="text-muted small mt-2">Memuat dokumen detail...</p>
</div>
</div>
<!-- Modal content (hidden initially) -->
<div id="modalContent" class="d-none">
<p>Please review the details below:</p>
<ul class="list-group">
<li class="list-group-item">
<strong>Expense Number:</strong> <span id="expense_number"></span>
</li>
<li class="list-group-item">
<strong>User:</strong> <span id="user"></span>
</li>
<li class="list-group-item">
<strong>Tipe:</strong> Up Country
</li>
<li class="list-group-item">
<strong>Rayon:</strong> <span id="rayon"></span>
</li>
<li class="list-group-item">
<strong>Tanggal:</strong> <span id="tanggal"></span>
</li>
<li class="list-group-item">
<strong>Tujuan:</strong> <span id="tujuan"></span>
</li>
<li class="list-group-item">
<strong>Jarak:</strong> <span id="jarak"></span>
</li>
<li class="list-group-item">
<strong>Nominal Allowance:</strong> <span id="allowance_value"></span>
</li>
<li class="list-group-item">
<strong>Bukti Allowance:</strong> <span id="bukti_allowance"></span>
</li>
<li class="list-group-item">
<strong>Nominal Transport Dalam Kota:</strong> <span id="transport_dalkot_value"></span>
</li>
<li class="list-group-item">
<strong>Bukti Transport Dalam Kota:</strong> <span id="bukti_transport_dalkot"></span>
</li>
<li class="list-group-item">
<strong>Nominal Transport Antar Kota:</strong> <span id="transport_ankot_value"></span>
</li>
<li class="list-group-item">
<strong>Bukti Transport Antar Kota:</strong> <span id="bukti_transport_ankot"></span>
</li>
<li class="list-group-item">
<strong>Nominal Hotel:</strong> <span id="hotel_value"></span>
</li>
<li class="list-group-item">
<strong>Bukti Hotel:</strong> <span id="bukti_hotel"></span>
</li>
<li class="list-group-item">
<strong>UC Plan:</strong> <span id="uc_plan"></span>
</li>
</ul>
<div id="approve1_log_box" class="alert alert-success border-0 mb-3 d-none shadow-sm" style="border-radius: 8px;">
<div class="d-flex align-items-center">
<i class="fas fa-check-circle fa-lg mr-2"></i>
<div>
<span class="d-block fw-bold" style="font-size: 13px;">Telah Disetujui pada Approve 1</span>
<small class="d-block" style="font-size: 11px; opacity: 0.9;">
<strong>Oleh:</strong> <span id="appr1_by"></span> | <strong>Waktu:</strong> <span id="appr1_at"></span> WIB
</small>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Approve</button>
<h6 class="text-muted fw-bold mb-3 small text-uppercase tracking-wider"><i class="fas fa-info-circle mr-1"></i> Detail Informasi Biaya</h6>
<ul class="list-group shadow-sm mb-3" style="border-radius: 8px; overflow: hidden;">
<li class="list-group-item d-flex justify-content-between"><strong>Expense Number:</strong> <span id="view_expense_number" class="fw-bold text-primary"></span></li>
<li class="list-group-item d-flex justify-content-between"><strong>User Pengaju:</strong> <span id="view_user" class="fw-bold"></span></li>
<li class="list-group-item d-flex justify-content-between"><strong>Rayon / Cabang:</strong> <span id="view_rayon"></span></li>
<li class="list-group-item d-flex justify-content-between"><strong>Tanggal Penggunaan:</strong> <span id="view_tanggal"></span></li>
<li class="list-group-item d-flex justify-content-between"><strong>Tujuan Lokasi:</strong> <span id="view_tujuan"></span></li>
<li class="list-group-item d-flex justify-content-between"><strong>Jarak Tempuh:</strong> <span id="view_jarak" class="badge badge-secondary p-2"></span></li>
</ul>
<h6 class="text-muted fw-bold mb-3 small text-uppercase tracking-wider"><i class="fas fa-money-bill-wave mr-1"></i> Rincian Finansial & Bukti Dokumen</h6>
<div class="table-responsive shadow-sm bg-white border rounded" style="border-radius: 8px;">
<table class="table table-sm table-hover mb-0">
<thead class="thead-light">
<tr>
<th>Komponen Biaya</th>
<th>Nominal Pengajuan</th>
<th class="text-center">Link File Bukti</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Allowance:</strong></td>
<td><span id="allowance_value" class="text-dark"></span></td>
<td class="text-center" id="bukti_allowance"></td>
</tr>
<tr>
<td><strong>Transport Dalam Kota (Dalkot):</strong></td>
<td><span id="transport_dalkot_value" class="text-dark"></span></td>
<td class="text-center" id="bukti_transport_dalkot"></td>
</tr>
<tr>
<td><strong>Transport Antar Kota (Ankot):</strong></td>
<td><span id="transport_ankot_value" class="text-dark"></span></td>
<td class="text-center" id="bukti_transport_ankot"></td>
</tr>
<tr>
<td><strong>Hotel:</strong></td>
<td><span id="hotel_value" class="text-dark"></span></td>
<td class="text-center" id="bukti_hotel"></td>
</tr>
<tr class="bg-light font-weight-bold">
<td><strong>UC Plan Dokumen:</strong></td>
<td>-</td>
<td class="text-center" id="uc_plan"></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="modal-footer bg-white border-top">
<button type="button" class="btn btn-secondary px-4 rounded-pill" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary px-4 rounded-pill"><i class="fas fa-check mr-1"></i> Konfirmasi Approve</button>
</div>
</form>
</div>
</div>
</div>
</section>
<section>
{{-- MODAL CHECKLIST UNTUK FINAL APPROVE --}}
<div class="modal fade" id="finalApproveModal" tabindex="-1" role="dialog" aria-labelledby="finalApproveModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-content" style="border-radius: 12px; overflow: hidden;">
<form id="finalApproveForm" method="POST" action="">
@csrf
@method('PUT')
<div class="modal-header">
<h5 class="modal-title" id="approveModalLabel">Approve Item</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<div class="modal-header bg-dark text-white">
<h5 class="modal-title fw-bold" id="finalApproveModalLabel"><i class="fas fa-clipboard-check mr-2"></i>Final Closing Checklist Budget</h5>
<button type="button" class="close text-white" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<!-- Loading spinner -->
<div class="modal-body bg-light">
<div id="finalLoadingSpinner" class="d-flex justify-content-center align-items-center" style="height: 200px;">
<div class="spinner-border text-primary" role="status">
<span class="sr-only">Loading...</span>
</div>
<div class="spinner-border text-primary" role="status"></div>
</div>
<!-- Modal content (hidden initially) -->
<div id="finalModalContent" class="d-none">
<p>Please review the details below:</p>
<ul class="list-group">
<li class="list-group-item">
<strong>Expense Number:</strong> <span id="final_expense_number"></span>
</li>
<li class="list-group-item">
<strong>User:</strong> <span id="final_user"></span>
</li>
<li class="list-group-item">
<strong>Tipe:</strong> Up Country
</li>
<li class="list-group-item">
<strong>Rayon:</strong> <span id="final_rayon"></span>
</li>
<li class="list-group-item">
<strong>Tanggal:</strong> <span id="final_tanggal"></span>
</li>
<li class="list-group-item">
<strong>Tujuan:</strong> <span id="final_tujuan"></span>
</li>
<li class="list-group-item">
<strong>Jarak:</strong> <span id="final_jarak"></span>
</li>
<li class="list-group-item">
<strong>Bukti Allowance:</strong> <span id="final_bukti_allowance"></span>
</li>
<li class="list-group-item">
<strong>Bukti Transport Dalam Kota:</strong> <span id="final_bukti_transport_dalkot"></span>
</li>
<li class="list-group-item">
<strong>Bukti Transport Antar Kota:</strong> <span id="final_bukti_transport_ankot"></span>
</li>
<li class="list-group-item">
<strong>Bukti Hotel:</strong> <span id="final_bukti_hotel"></span>
</li>
<li class="list-group-item">
<strong>UC Plan:</strong> <span id="final_uc_plan"></span>
</li>
<p class="small text-muted mb-3">Tinjau kembali berkas pengaju biaya dinas bawah sebelum melakukan pelepasan dana cabang:</p>
<ul class="list-group shadow-sm mb-3">
<li class="list-group-item d-flex justify-content-between"><strong>Expense Number:</strong> <span id="final_expense_number" class="fw-bold text-dark"></span></li>
<li class="list-group-item d-flex justify-content-between"><strong>User Pengaju:</strong> <span id="final_user"></span></li>
<li class="list-group-item d-flex justify-content-between"><strong>Rayon:</strong> <span id="final_rayon"></span></li>
<li class="list-group-item d-flex justify-content-between"><strong>Tanggal Dinas:</strong> <span id="final_tanggal"></span></li>
<li class="list-group-item d-flex justify-content-between"><strong>Tujuan:</strong> <span id="final_tujuan"></span></li>
<li class="list-group-item d-flex justify-content-between"><strong>Jarak Tempuh:</strong> <span id="final_jarak"></span></li>
<li class="list-group-item d-flex justify-content-between"><strong>File Bukti Allowance:</strong> <span id="final_bukti_allowance"></span></li>
<li class="list-group-item d-flex justify-content-between"><strong>File Bukti Transport Dalkot:</strong> <span id="final_bukti_transport_dalkot"></span></li>
<li class="list-group-item d-flex justify-content-between"><strong>File Bukti Transport Ankot:</strong> <span id="final_bukti_transport_ankot"></span></li>
<li class="list-group-item d-flex justify-content-between"><strong>File Bukti Hotel:</strong> <span id="final_bukti_hotel"></span></li>
<li class="list-group-item d-flex justify-content-between"><strong>UC Plan:</strong> <span id="final_uc_plan"></span></li>
</ul>
<p class="mt-2">Please select the items you want to approve:</p>
<ul class="list-group list-unstyled">
<p class="mt-3 fw-bold text-dark small text-uppercase"><i class="fas fa-tasks mr-1"></i> Pilih Item Komponen Yang Di-approve :</p>
<ul class="list-group shadow-sm bg-white p-3 border rounded">
<li>
<div class="form-group">
<div class="form-group mb-2">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="final_allowance" name="allowance">
<label class="custom-control-label" for="final_allowance">Allowance (<span id="final_allowance_value"></span>)</label>
@@ -331,7 +312,7 @@
</div>
</li>
<li>
<div class="form-group">
<div class="form-group mb-2">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="final_transport_dalkot" name="transport_dalkot">
<label class="custom-control-label" for="final_transport_dalkot">Transport Dalkot (<span id="final_transport_dalkot_value"></span>)</label>
@@ -339,7 +320,7 @@
</div>
</li>
<li>
<div class="form-group">
<div class="form-group mb-2">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="final_transport_ankot" name="transport_ankot">
<label class="custom-control-label" for="final_transport_ankot">Transport Ankot (<span id="final_transport_ankot_value"></span>)</label>
@@ -347,7 +328,7 @@
</div>
</li>
<li>
<div class="form-group">
<div class="form-group mb-2">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="final_hotel" name="hotel">
<label class="custom-control-label" for="final_hotel">Hotel (<span id="final_hotel_value"></span>)</label>
@@ -355,24 +336,24 @@
</div>
</li>
<li>
<div class="form-group">
<label for="total2">Total</label>
<input type="text" class="form-control" id="total2" name="total" value="0" readonly>
<div class="form-group mt-3 mb-0">
<label class="fw-bold text-muted">Total Approved Budget</label>
<input type="text" class="form-control bg-white font-weight-bold text-success" id="total2" name="total" value="0" readonly>
</div>
</li>
</ul>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Approve</button>
<div class="modal-footer bg-white border-top">
<button type="button" class="btn btn-secondary px-4 rounded-pill" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary px-4 rounded-pill">Approve</button>
</div>
</form>
</div>
</div>
</div>
</section>
<section>
<div class="modal fade" id="rejectModal" tabindex="-1" role="dialog" aria-labelledby="rejectModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
@@ -406,48 +387,205 @@
@section('scripts')
<script>
new AutoNumeric('#allowance', {
digitGroupSeparator: '.', // Pemisah ribuan
decimalCharacter: ',', // Karakter desimal (tidak digunakan karena tanpa desimal)
currencySymbol: 'Rp.', // Simbol mata uang
decimalPlaces: 0, // Tidak ada angka desimal
unformatOnSubmit: true // Nilai asli tanpa format saat dikirimkan
$(document).ready(function() {
// Inisialisasi AutoNumeric Readonly Masking
const configReadonly = {
digitGroupSeparator: '.',
decimalCharacter: ',',
currencySymbol: 'Rp. ',
decimalPlaces: 0,
readOnly: true
};
new AutoNumeric('#allowance', configReadonly);
new AutoNumeric('#transport_dalkot', configReadonly);
new AutoNumeric('#transport_ankot', configReadonly);
new AutoNumeric('#hotel', configReadonly);
new AutoNumeric('#total', configReadonly);
// Logika Klik Preview Attachment Gallery
$(document).on('click', '.preview-attachment', function () {
const button = $(this);
const previewType = button.data('preview-type');
const previewUrl = button.data('preview-url');
const downloadUrl = button.data('download-url');
const category = button.data('category') || 'Attachment';
if (!previewUrl) {
if (downloadUrl) {
window.open(downloadUrl, '_blank');
}
return;
}
openAttachmentModal('#viewAttachmentPreviewModal', category, previewType, previewUrl);
});
new AutoNumeric('#transport_dalkot', {
digitGroupSeparator: '.', // Pemisah ribuan
decimalCharacter: ',', // Karakter desimal (tidak digunakan karena tanpa desimal)
currencySymbol: 'Rp.', // Simbol mata uang
decimalPlaces: 0, // Tidak ada angka desimal
unformatOnSubmit: true // Nilai asli tanpa format saat dikirimkan
// Logika Pembukaan Modal Reject
$(document).on('click', '.open-reject-modal', function () {
const rejectUrl = $(this).data('action');
$('#rejectForm').attr('action', rejectUrl);
$('#rejectRemarks').val('');
$('#rejectModal').modal('show');
});
new AutoNumeric('#transport_ankot', {
digitGroupSeparator: '.', // Pemisah ribuan
decimalCharacter: ',', // Karakter desimal (tidak digunakan karena tanpa desimal)
currencySymbol: 'Rp.', // Simbol mata uang
decimalPlaces: 0, // Tidak ada angka desimal
unformatOnSubmit: true // Nilai asli tanpa format saat dikirimkan
$('#rejectModal').on('shown.bs.modal', function () {
$('#rejectRemarks').trigger('focus');
});
new AutoNumeric('#hotel', {
digitGroupSeparator: '.', // Pemisah ribuan
decimalCharacter: ',', // Karakter desimal (tidak digunakan karena tanpa desimal)
currencySymbol: 'Rp.', // Simbol mata uang
decimalPlaces: 0, // Tidak ada angka desimal
unformatOnSubmit: true // Nilai asli tanpa format saat dikirimkan
$('#rejectModal').on('hidden.bs.modal', function () {
$('#rejectForm').attr('action', '');
$('#rejectRemarks').val('');
});
new AutoNumeric('#total', {
digitGroupSeparator: '.', // Pemisah ribuan
decimalCharacter: ',', // Karakter desimal (tidak digunakan karena tanpa desimal)
currencySymbol: 'Rp.', // Simbol mata uang
decimalPlaces: 0, // Tidak ada angka desimal
unformatOnSubmit: true // Nilai asli tanpa format saat dikirimkan
});
</script>
// ==========================================
// FIX PREVIEW LOADING: MODAL APPROVE 2 (AM / MIS)
// ==========================================
$(document).on('click', '.open-approve-modal', function() {
const approveUrl = $(this).data('id');
<script>
// Hidupkan spinner, sembunyikan kotak data kontent
$('#loadingSpinner').removeClass('d-none').addClass('d-flex').show();
$('#modalContent').addClass('d-none');
$('#approve1_log_box').addClass('d-none'); // Reset log box
$('#approveForm').attr('action', approveUrl);
$('#approveModal').modal('show');
// Mengambil data detail JSON via API route backend
$.get(approveUrl.replace(/approve2|final-approve/g, 'detail'), function (data) {
const formatter = new Intl.NumberFormat('id-ID', {
style: 'currency', currency: 'IDR', minimumFractionDigits: 0
});
// Isi Rincian Finansial Biaya
$('#allowance_value').text(formatter.format(data.allowance));
$('#transport_dalkot_value').text(formatter.format(data.transport_dalkot));
$('#transport_ankot_value').text(formatter.format(data.transport_ankot));
$('#hotel_value').text(formatter.format(data.hotel));
// Isi Detail Data Atas (SINKRON DENGAN ID HTML BARU)
$('#view_user').text(data.user ? data.user.name : '-');
$('#view_expense_number').text(data.expense_number);
$('#view_rayon').text(data.rayon ? data.rayon.name : '-');
if (data.tanggal) {
$('#view_tanggal').text(new Date(data.tanggal).toLocaleDateString('id-ID', {
year: 'numeric', month: 'long', day: 'numeric'
}));
} else {
$('#view_tanggal').text('-');
}
$('#view_tujuan').text(data.tujuan ? data.tujuan : '-');
$('#view_jarak').text(data.jarak ? data.jarak + ' km' : '0 km');
// Suntik Element Link Download Bukti Transaksi Dokumen Fisik
$('#bukti_allowance').html(data.bukti_allowance ? '<a href="' + data.bukti_allowance + '" target="_blank" class="btn btn-xs btn-primary"><i class="fas fa-download"></i> Buka File</a>' : '<span class="text-muted">-</span>');
$('#bukti_transport_dalkot').html(data.bukti_transport_dalkot ? '<a href="' + data.bukti_transport_dalkot + '" target="_blank" class="btn btn-xs btn-primary"><i class="fas fa-download"></i> Buka File</a>' : '<span class="text-muted">-</span>');
$('#bukti_transport_ankot').html(data.bukti_transport_ankot ? '<a href="' + data.bukti_transport_ankot + '" target="_blank" class="btn btn-xs btn-primary"><i class="fas fa-download"></i> Buka File</a>' : '<span class="text-muted">-</span>');
$('#bukti_hotel').html(data.bukti_hotel ? '<a href="' + data.bukti_hotel + '" target="_blank" class="btn btn-xs btn-primary"><i class="fas fa-download"></i> Buka File</a>' : '<span class="text-muted">-</span>');
$('#uc_plan').html(data.uc_plan ? '<a href="' + data.uc_plan + '" target="_blank" class="btn btn-xs btn-info"><i class="fas fa-file-pdf"></i> Open Plan</a>' : '<span class="text-muted">-</span>');
// SUNTIK LOG AUDIT TRAIL DATA APPROVE 1
if(data.approved_at && data.status !== 'On Progress') {
$('#appr1_by').text(data.approved_by_user ? data.approved_by_user.name : 'Regional Admin');
// Format Tanggal Jam Approve 1
let dateObj = new Date(data.approved_at);
let formattedAppr1Date = dateObj.toLocaleDateString('id-ID', { day: 'numeric', month: 'short', year: 'numeric' }) + ' ' +
String(dateObj.getHours()).padStart(2, '0') + ':' +
String(dateObj.getMinutes()).padStart(2, '0');
$('#appr1_at').text(formattedAppr1Date);
$('#approve1_log_box').removeClass('d-none'); // Tampilkan box log hijau
}
// Hilangkan Loading Spinner, Tampilkan Kotak Konten Review Utama
$('#loadingSpinner').hide().removeClass('d-flex').addClass('d-none');
$('#modalContent').removeClass('d-none');
}).fail(function() {
Swal.fire('Error', 'Gagal memuat detail data kueri kuesioner biaya dari server.', 'error');
});
});
// Logika Pembukaan Modal Final Approve Checklist Dinamis
$(document).on('click', '.final-approve-modal', function() {
const approveUrl = $(this).data('id');
$('#finalLoadingSpinner').addClass('d-flex').show();
$('#finalModalContent').addClass('d-none');
$('#finalApproveForm').attr('action', approveUrl);
$('#finalApproveModal').modal('show');
$.get(approveUrl.replace('approve', 'detail'), function (data) {
const formatter = new Intl.NumberFormat('id-ID', {
style: 'currency', currency: 'IDR', minimumFractionDigits: 0
});
$('#final_allowance_value').text(formatter.format(data.allowance));
$('#final_transport_dalkot_value').text(formatter.format(data.transport_dalkot));
$('#final_transport_ankot_value').text(formatter.format(data.transport_ankot));
$('#final_hotel_value').text(formatter.format(data.hotel));
$('#final_allowance').data('value', data.allowance);
$('#final_transport_dalkot').data('value', data.transport_dalkot);
$('#final_transport_ankot').data('value', data.transport_ankot);
$('#final_hotel').data('value', data.hotel);
$('#final_user').text(data.user ? data.user.name : '-');
$('#final_expense_number').text(data.expense_number);
$('#final_rayon').text(data.rayon ? data.rayon.name : '-');
$('#final_tanggal').text(new Date(data.tanggal).toLocaleDateString('id-ID', {
year: 'numeric', month: 'long', day: 'numeric'
}));
$('#final_tujuan').text(data.tujuan);
$('#final_jarak').text(data.jarak + ' km');
// Set default ceklis aktif saat pertama kali dibuka
$('.custom-control-input').prop('checked', true);
$('#finalLoadingSpinner').hide().removeClass('d-flex').addClass('d-none');
$('#finalModalContent').removeClass('d-none');
// Hitung akumulasi awal
calculateShowPageApprovedTotal();
});
});
function calculateShowPageApprovedTotal() {
let totalApproved = 0;
$('.custom-control-input:checked').each(function () {
totalApproved += parseFloat($(this).data('value') || 0);
});
const formatter = new Intl.NumberFormat('id-ID', {
style: 'currency', currency: 'IDR', minimumFractionDigits: 0,
});
// Menampilkan ke field id #total2 milik view detail Anda
$('#total2').val(formatter.format(totalApproved));
// Suntik input hidden ke form finalApproveForm agar terbaca request backend
if($('#hidden_view_approved_total').length === 0) {
$('#finalApproveForm').append(`<input type="hidden" id="hidden_view_approved_total" name="approved_total" value="${totalApproved}">`);
} else {
$('#hidden_view_approved_total').val(totalApproved);
}
}
// Bind perubahan checklist
$(document).on('change', '.custom-control-input', function () {
calculateShowPageApprovedTotal();
});
$('#finalApproveModal').on('hidden.bs.modal', function () {
$('.custom-control-input').prop('checked', false);
$('#total2').val('0');
$('#hidden_view_approved_total').remove();
});
}); // <--- PENUTUP SEKAT READY FUNCTION JQUERY YANG HILANG SEBELUMNYA
// Handler Render Gallery Modal Core Engine (Sekarang Terbaca Sempurna Sebagai Fungsi Global)
function openAttachmentModal(modalSelector, title, type, source) {
const $modal = $(modalSelector);
const $image = $modal.find('.attachment-preview-image');
@@ -473,177 +611,5 @@
$modal.modal('show');
}
}
$(document).on('click', '.preview-attachment', function () {
const button = $(this);
const previewType = button.data('preview-type');
const previewUrl = button.data('preview-url');
const downloadUrl = button.data('download-url');
const category = button.data('category') || 'Attachment';
if (!previewUrl) {
if (downloadUrl) {
window.open(downloadUrl, '_blank');
}
return;
}
openAttachmentModal('#viewAttachmentPreviewModal', category, previewType, previewUrl);
});
$(document).on('click', '.open-reject-modal', function () {
const rejectUrl = $(this).data('action');
$('#rejectForm').attr('action', rejectUrl);
$('#rejectRemarks').val('');
$('#rejectModal').modal('show');
});
$('#rejectModal').on('shown.bs.modal', function () {
$('#rejectRemarks').trigger('focus');
});
$('#rejectModal').on('hidden.bs.modal', function () {
$('#rejectForm').attr('action', '');
$('#rejectRemarks').val('');
});
// Handle Approve Button Click
$(document).on('click', '.open-approve-modal', function() {
const approveUrl = $(this).data('id');
// Show spinner and hide content initially
$('#loadingSpinner').show();
$('#modalContent').addClass('d-none');
$('#approveForm').attr('action', approveUrl); // Set the form action
$('#loadingSpinner').addClass('d-flex');
$('#approveModal').modal('show'); // Show the modal
// Get detail from /forms/meeting/detail/{id}
$.get(approveUrl.replace(/approve2|final-approve/g, 'detail'), function (data) {
const formatter = new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
});
$('#allowance_value').text(formatter.format(data.allowance));
$('#transport_dalkot_value').text(formatter.format(data.transport_dalkot));
$('#transport_ankot_value').text(formatter.format(data.transport_ankot));
$('#hotel_value').text(formatter.format(data.hotel));
// Detail data
$('#user').text(data.user.name);
$('#expense_number').text(data.expense_number);
$('#rayon').text(data.rayon.code);
$('#tanggal').text(new Date(data.tanggal).toLocaleDateString('id-ID', {
year: 'numeric',
month: 'long',
day: 'numeric'
}));
$('#tujuan').text(data.tujuan);
$('#jarak').text(data.jarak + ' km');
$('#bukti_allowance').html(data.bukti_allowance ? '<a href="' + data.bukti_allowance + '" target="_blank">Download</a>' : '-');
$('#bukti_transport_dalkot').html(data.bukti_transport_dalkot ? '<a href="' + data.bukti_transport_dalkot + '" target="_blank">Download</a>' : '-');
$('#bukti_transport_ankot').html(data.bukti_transport_ankot ? '<a href="' + data.bukti_transport_ankot + '" target="_blank">Download</a>' : '-');
$('#bukti_hotel').html(data.bukti_hotel ? '<a href="' + data.bukti_hotel + '" target="_blank">Download</a>' : '-');
$('#uc_plan').text(data.uc_plan ? '<a href="' + data.uc_plan + '" target="_blank">Download</a>' : '-');
// Hide spinner and show content
$('#loadingSpinner').hide();
$('#loadingSpinner').removeClass('d-flex');
$('#loadingSpinner').addClass('d-none');
$('#modalContent').removeClass('d-none');
// Reset the total to zero
$('#total').val(0);
});
});
// Handle Approve Button Click
$(document).on('click', '.final-approve-modal', function() {
const approveUrl = $(this).data('id');
// Show spinner and hide content initially
$('#finalLoadingSpinner').show();
$('#finalModalContent').addClass('d-none');
$('#finalApproveForm').attr('action', approveUrl); // Set the form action
$('#finalLoadingSpinner').addClass('d-flex');
$('#finalApproveModal').modal('show'); // Show the modal
// Get detail from /forms/up-country/detail/{id}
$.get(approveUrl.replace('approve', 'detail'), function (data) {
console.log(data);
const formatter = new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
});
$('#final_allowance_value').text(formatter.format(data.allowance));
$('#final_transport_dalkot_value').text(formatter.format(data.transport_dalkot));
$('#final_transport_ankot_value').text(formatter.format(data.transport_ankot));
$('#final_hotel_value').text(formatter.format(data.hotel));
// Assign data values to checkboxes
$('#final_allowance').data('value', data.allowance);
$('#final_transport_dalkot').data('value', data.transport_dalkot);
$('#final_transport_ankot').data('value', data.transport_ankot);
$('#final_hotel').data('value', data.hotel);
// Detail data
$('#final_user').text(data.user.name);
$('#final_expense_number').text(data.expense_number);
$('#final_rayon').text(data.rayon.code);
$('#final_tanggal').text(new Date(data.tanggal).toLocaleDateString('id-ID', {
year: 'numeric',
month: 'long',
day: 'numeric'
}));
$('#final_tujuan').text(data.tujuan);
$('#final_jarak').text(data.jarak + ' km');
$('#final_bukti_allowance').html(data.bukti_allowance ? '<a href="' + data.bukti_allowance + '" target="_blank">Download</a>' : '-');
$('#final_bukti_transport_dalkot').html(data.bukti_transport_dalkot ? '<a href="' + data.bukti_transport_dalkot + '" target="_blank">Download</a>' : '-');
$('#final_bukti_transport_ankot').html(data.bukti_transport_ankot ? '<a href="' + data.bukti_transport_ankot + '" target="_blank">Download</a>' : '-');
$('#final_bukti_hotel').html(data.bukti_hotel ? '<a href="' + data.bukti_hotel + '" target="_blank">Download</a>' : '-');
$('#final_uc_plan').text(data.uc_plan ? '<a href="' + data.uc_plan + '" target="_blank">Download</a>' : '-');
// Hide spinner and show content
$('#finalLoadingSpinner').hide();
$('#finalLoadingSpinner').removeClass('d-flex');
$('#finalLoadingSpinner').addClass('d-none');
$('#finalModalContent').removeClass('d-none');
// Reset the total to zero
$('#total').val(0);
});
});
// Update total dynamically
$('.custom-control-input').on('change', function () {
let total = 0;
$('.custom-control-input:checked').each(function () {
total += parseFloat($(this).data('value') || 0);
});
const formatter = new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0,
});
$('#total2').val(formatter.format(total));
});
// Uncheck all checkboxes when the modal is closed
$('#finalApproveModal').on('hidden.bs.modal', function () {
$('.custom-control-input').prop('checked', false); // Uncheck all checkboxes
$('#total2').val('0'); // Reset total to 0
});
</script>
@endsection
@@ -6,6 +6,7 @@
@section('admin-content')
<script src="https://cdn.jsdelivr.net/npm/autonumeric@4.6.0/dist/autoNumeric.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<section class="content-header">
<div class="container-fluid">
@@ -28,8 +29,8 @@
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5); /* Semi-transparent black background */
z-index: 9999; /* High z-index to cover everything */
background-color: rgba(0, 0, 0, 0.5);
z-index: 9999;
display: flex;
justify-content: center;
align-items: center;
@@ -51,7 +52,8 @@
<div class="col-lg-6">
<div class="mb-3">
<label class="form-label">Tanggal <span class="font-italic font-weight-normal">(required)</span></label>
<input type="datetime-local" class="form-control" name="tanggal" required value="{{ old('tanggal') }}">
{{-- Mengubah datetime-local menjadi date --}}
<input type="date" class="form-control" name="tanggal" required value="{{ old('tanggal') }}">
</div>
<div class="mb-3">
<label class="form-label">Tipe Pengeluaran <span class="font-italic font-weight-normal">(required)</span></label>
@@ -63,11 +65,11 @@
<div class="mb-3 gasoline-fields">
<label class="form-label">Liter Bensin <span class="font-italic font-weight-normal">(required)</span></label>
<input type="text" class="form-control" name="liter" value="{{ old('liter') }}">
<input type="number" step="0.01" class="form-control" name="liter" id="liter" value="{{ old('liter') }}">
</div>
<div class="mb-3">
<label class="form-label">Total Harga <span class="font-italic font-weight-normal">(required)</span></label>
<input type="string" class="form-control" name="total" id="total" required value="{{ old('total') }}">
<input type="text" class="form-control" name="total" id="total" required value="{{ old('total') }}">
</div>
</div>
@@ -78,7 +80,8 @@
</div>
<div class="mb-3 gasoline-fields">
<label class="form-label">Tipe Bensin <span class="font-italic font-weight-normal">(required)</span></label>
<select class="form-control" name="tipe_bensin" id="tipe_bensin" value="{{ old('tipe_bensin') }}">
<select class="form-control" name="tipe_bensin" id="tipe_bensin">
<option value="" disabled selected>Pilih Tipe</option>
<option value="pertamax" {{ old('tipe_bensin') == 'pertamax' ? 'selected' : '' }}>Pertamax</option>
<option value="pertalite" {{ old('tipe_bensin') == 'pertalite' ? 'selected' : '' }}>Pertalite</option>
</select>
@@ -135,9 +138,8 @@
<div id="loading-spinner-overlay" class="d-none">
<div class="spinner-wrapper">
<div class="spinner-border text-primary" role="status">
</div>
<p>Submitting, please wait...</p>
<div class="spinner-border text-primary" role="status"></div>
<p class="mt-2">Submitting, please wait...</p>
</div>
</div>
</div>
@@ -146,26 +148,29 @@
</section>
<script>
new AutoNumeric('#total', {
$(document).ready(function() {
// 1. Inisialisasi Aman AutoNumeric (Sesuai Standar Up Country)
const autoNumericConfig = {
digitGroupSeparator: '.',
decimalCharacter: ',',
currencySymbol: 'Rp.',
currencySymbol: 'Rp. ',
decimalPlaces: 0,
minimumValue: '0', // Mencegah nilai minus
unformatOnSubmit: true
};
const totalInput = new AutoNumeric('#total', autoNumericConfig);
// 2. Cegah Scroll Wheel agar angka tidak berubah tanpa sengaja
$('#total, #liter, #jarak').on('wheel font-wheel', function(e) {
e.preventDefault();
$(this).blur();
});
const spinnerOverlay = document.getElementById('loading-spinner-overlay');
document.getElementById('expense-form').addEventListener('submit', function () {
spinnerOverlay.classList.remove('d-none');
spinnerOverlay.classList.add('d-flex');
});
// Toggle gasoline specific fields
// 3. Toggle Logika Field Bensin
const expenseTypeSelect = document.getElementById('expense_type');
const gasolineFields = document.querySelectorAll('.gasoline-fields');
const literInput = document.querySelector('input[name="liter"]');
const jarakInput = document.querySelector('input[name="jarak"]');
const literInput = document.getElementById('liter');
const jarakInput = document.getElementById('jarak');
const tipeBensinSelect = document.getElementById('tipe_bensin');
const nopolInput = document.getElementById('nopol');
@@ -192,6 +197,37 @@
toggleGasolineFields();
expenseTypeSelect.addEventListener('change', toggleGasolineFields);
// 4. Integrasi Interseptor Form Submit & SweetAlert
const spinnerOverlay = $('#loading-spinner-overlay');
$('#expense-form').on('submit', function (e) {
e.preventDefault();
const form = this;
const totalExpense = totalInput.getNumber() || 0;
if (totalExpense > 1000000) {
Swal.fire({
title: 'Nominal Melebihi Batas Expense!',
text: `Total pengajuan Anda adalah Rp ${new Intl.NumberFormat('id-ID').format(totalExpense)}. Jumlah ini melebihi batas standar Rp 1.000.000. Apakah yakin tetap ajukan?`,
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Yes, Tetap Ajukan!',
cancelButtonText: 'No, Batalkan'
}).then((result) => {
if (result.isConfirmed) {
spinnerOverlay.removeClass('d-none').addClass('d-flex');
form.submit();
}
});
} else {
spinnerOverlay.removeClass('d-none').addClass('d-flex');
form.submit();
}
});
// 5. Lampiran Engine (Tetap Utuh)
const attachmentCategories = @json($attachmentCategories ?? ($vehicleAttachmentCategories ?? []));
const blockedExtensions = ['exe', 'bat', 'sh', 'cmd', 'dll', 'msi'];
const maxFileSizeBytes = 10 * 1024 * 1024;
@@ -208,22 +244,14 @@
}
function categoryToLabel(value) {
if (!value) {
return 'Pilih Kategori';
}
return value.replace(/_/g, ' ').replace(/\b\w/g, function (char) {
return char.toUpperCase();
});
if (!value) return 'Pilih Kategori';
return value.replace(/_/g, ' ').replace(/\b\w/g, function (char) { return char.toUpperCase(); });
}
function detectPreviewType(extension) {
const images = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
if (images.indexOf(extension) !== -1) {
return 'image';
}
if (extension === 'pdf') {
return 'pdf';
}
if (images.indexOf(extension) !== -1) return 'image';
if (extension === 'pdf') return 'pdf';
return 'other';
}
@@ -238,9 +266,7 @@
$box.removeClass('bg-light').html('<i class="fas fa-file-upload text-muted"></i>');
if (type === 'image' && source) {
$box.addClass('bg-light').html(
'<img src="' + source + '" class="img-thumbnail" style="width:100%;height:100%;object-fit:cover;" alt="Preview">'
);
$box.addClass('bg-light').html('<img src="' + source + '" class="img-thumbnail" style="width:100%;height:100%;object-fit:cover;" alt="Preview">');
} else if (type === 'pdf') {
$box.addClass('bg-light').html('<i class="fas fa-file-pdf text-danger fa-lg"></i>');
} else if (type === 'other') {
@@ -250,41 +276,23 @@
function resetAttachmentRowData($row, clearInput = true) {
const existingUrl = $row.data('objectUrl');
if (existingUrl) {
URL.revokeObjectURL(existingUrl);
}
$row.removeData('objectUrl')
.removeData('previewType')
.removeData('previewSource')
.removeData('previewFileName')
.removeData('downloadUrl');
if (existingUrl) URL.revokeObjectURL(existingUrl);
$row.removeData('objectUrl').removeData('previewType').removeData('previewSource').removeData('previewFileName').removeData('downloadUrl');
setInlinePreview($row, null, null);
$row.find('.vehicle-preview-new-attachment').addClass('d-none');
$row.find('.vehicle-preview-filename').text('');
if (clearInput) {
$row.find('.vehicle-attachment-file').val('');
}
if (clearInput) $row.find('.vehicle-attachment-file').val('');
}
function openAttachmentModal(modalSelector, options) {
const settings = Object.assign({
title: 'Lampiran',
type: 'other',
source: null,
downloadUrl: null,
fileName: 'Lampiran'
}, options || {});
const settings = Object.assign({ title: 'Lampiran', type: 'other', source: null, downloadUrl: null, fileName: 'Lampiran' }, options || {});
const $modal = $(modalSelector);
const $image = $modal.find('.attachment-preview-image');
const $object = $modal.find('.attachment-preview-object');
const $placeholder = $modal.find('.attachment-preview-placeholder');
$modal.find('.attachment-preview-modal-title').text(settings.title || 'Lampiran');
$image.addClass('d-none').attr('src', '');
$object.addClass('d-none').attr('data', '').attr('src', '');
$placeholder.removeClass('d-none').html('Tidak ada file untuk ditampilkan.');
@@ -298,25 +306,15 @@
} else {
const safeName = escapeHtml(settings.fileName || 'Lampiran');
let message = '<p class="mb-2">' + safeName + '</p><p class="text-muted mb-0">Preview tidak tersedia. Silakan unduh file untuk melihat konten.</p>';
if (settings.downloadUrl) {
message += '<div class="mt-3"><a href="' + settings.downloadUrl + '" target="_blank" class="btn btn-sm btn-outline-primary">Download</a></div>';
}
if (settings.downloadUrl) message += '<div class="mt-3"><a href="' + settings.downloadUrl + '" target="_blank" class="btn btn-sm btn-outline-primary">Download</a></div>';
$placeholder.html(message);
}
if (window.bootstrap && bootstrap.Modal && typeof bootstrap.Modal.getOrCreateInstance === 'function') {
bootstrap.Modal.getOrCreateInstance($modal[0]).show();
} else {
$modal.modal('show');
}
}
function addVehicleAttachmentRow() {
const $tbody = $('#vehicle-attachments-table tbody');
if ($tbody.find('.vehicle-attachments-empty').length) {
$tbody.empty();
}
if ($tbody.find('.vehicle-attachments-empty').length) $tbody.empty();
const index = vehicleAttachmentIndex++;
const rowHtml = `
@@ -332,40 +330,29 @@
<div class="vehicle-attachment-inline-preview border rounded d-flex align-items-center justify-content-center bg-white mr-2" style="width:56px;height:56px;">
<i class="fas fa-file-upload text-muted"></i>
</div>
<input type="file" class="form-control vehicle-attachment-file" name="attachments[${index}][file_path]">
<input type="file" class="form-control vehicle-attachment-file" name="attachments[${index}][file_path]" required>
</div>
<div class="small text-muted mt-1 vehicle-preview-filename"></div>
</td>
<td class="text-center">
<button type="button" class="btn btn-sm btn-outline-secondary vehicle-preview-new-attachment d-none">
Preview
</button>
<button type="button" class="btn btn-sm btn-outline-secondary vehicle-preview-new-attachment d-none">Preview</button>
</td>
<td class="text-center">
<button type="button" class="btn btn-sm btn-outline-danger vehicle-remove-attachment-row">
<i class="fas fa-trash"></i>
</button>
<button type="button" class="btn btn-sm btn-outline-danger vehicle-remove-attachment-row"><i class="fas fa-trash"></i></button>
</td>
</tr>
`;
$tbody.append(rowHtml);
}
$(function () {
$('#vehicle-add-attachment-row').on('click', function () {
addVehicleAttachmentRow();
});
$('#vehicle-add-attachment-row').on('click', function () { addVehicleAttachmentRow(); });
$(document).on('click', '.vehicle-remove-attachment-row', function () {
const $row = $(this).closest('tr');
const $tbody = $('#vehicle-attachments-table tbody');
resetAttachmentRowData($row, true);
$row.remove();
if (!$tbody.find('.vehicle-attachment-row').length) {
$tbody.html(vehicleAttachmentEmptyRow);
}
if (!$tbody.find('.vehicle-attachment-row').length) $tbody.html(vehicleAttachmentEmptyRow);
});
$(document).on('change', '.vehicle-attachment-file', function () {
@@ -378,9 +365,7 @@
filenameHolder.text('');
const file = this.files && this.files[0];
if (!file) {
return;
}
if (!file) return;
if (file.size > maxFileSizeBytes) {
alert('Ukuran file melebihi 10 MB.');
@@ -419,7 +404,6 @@
$row.data('previewSource', null);
setInlinePreview($row, 'other', null);
}
previewButton.removeClass('d-none');
});
@@ -438,7 +422,7 @@
});
});
// Add initial attachment row for user convenience
// Berikan 1 baris input file kosong sebagai default awal
addVehicleAttachmentRow();
});
</script>
@@ -6,6 +6,8 @@
@section('admin-content')
<script src="https://cdn.jsdelivr.net/npm/autonumeric@4.6.0/dist/autoNumeric.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<section class="content-header">
<div class="container-fluid">
<div class="row mb-2">
@@ -27,8 +29,8 @@
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5); /* Semi-transparent black background */
z-index: 9999; /* High z-index to cover everything */
background-color: rgba(0, 0, 0, 0.5);
z-index: 9999;
display: flex;
justify-content: center;
align-items: center;
@@ -39,6 +41,12 @@
color: white;
}
</style>
@php
$attachmentCategoryLabels = $attachmentCategoryLabels ?? ($vehicleAttachmentCategories ?? []);
$attachmentCategories = $attachmentCategories ?? ($vehicleAttachmentCategoryKeys ?? array_keys($attachmentCategoryLabels));
@endphp
<section class="content">
<div class="container-fluid">
<div class="card card-primary card-outline">
@@ -51,9 +59,10 @@
<div class="col-lg-6">
<div class="mb-3">
<label class="form-label">Tanggal <span class="font-italic font-weight-normal">(required)</span></label>
<input type="datetime-local" class="form-control" name="tanggal" required value="{{ old('tanggal', $form->tanggal ? \Carbon\Carbon::parse($form->tanggal)->format('Y-m-d\TH:i') : '') }}">
{{-- Konversi Cerdas Format Input Tipe 'date' --}}
<input type="date" class="form-control" name="tanggal" required
value="{{ old('tanggal', $form->tanggal ? \Carbon\Carbon::parse($form->tanggal)->format('Y-m-d') : '') }}">
</div>
{{-- Updated: Expense Type dropdown with name="type" and $form->type for selection --}}
<div class="mb-3">
<label class="form-label">Tipe Pengeluaran <span class="font-italic font-weight-normal">(required)</span></label>
<select class="form-control" name="type" id="expense_type" required>
@@ -62,19 +71,17 @@
</select>
</div>
{{-- Wrap gasoline-specific fields with .gasoline-fields --}}
<div class="mb-3 gasoline-fields">
<label class="form-label">Liter Bensin <span class="font-italic font-weight-normal">(required)</span></label>
<input type="text" class="form-control" name="liter" value="{{ old('liter', $form->liter) }}">
<input type="number" step="0.01" class="form-control" name="liter" id="liter" value="{{ old('liter', $form->liter) }}">
</div>
<div class="mb-3">
<label class="form-label">Total Harga <span class="font-italic font-weight-normal">(required)</span></label>
<input type="string" class="form-control" name="total" id="total" required value="{{ old('total', $form->total) }}">
<input type="text" class="form-control" name="total" id="total" required value="{{ old('total', $form->total) }}">
</div>
</div>
<div class="col-lg-6">
{{-- Wrap gasoline-specific fields with .gasoline-fields --}}
<div class="mb-3 gasoline-fields">
<label class="form-label">Km (Odometer) <span class="font-italic font-weight-normal">(required)</span></label>
<input type="number" class="form-control" name="jarak" id="jarak" value="{{ old('jarak', $form->jarak) }}">
@@ -82,6 +89,7 @@
<div class="mb-3 gasoline-fields">
<label class="form-label">Tipe Bensin <span class="font-italic font-weight-normal">(required)</span></label>
<select class="form-control" name="tipe_bensin" id="tipe_bensin">
<option value="" disabled selected>Pilih Tipe</option>
<option value="pertamax" {{ old('tipe_bensin', $form->tipe_bensin) == 'pertamax' ? 'selected' : '' }}>Pertamax</option>
<option value="pertalite" {{ old('tipe_bensin', $form->tipe_bensin) == 'pertalite' ? 'selected' : '' }}>Pertalite</option>
</select>
@@ -96,53 +104,61 @@
</div>
</div>
<div class="col-12">
{{-- BAGIAN LAMPIRAN TERSIMPAN DATABASE --}}
<div class="col-lg-12">
<div class="mb-4">
<label class="form-label mb-0">Lampiran Saat Ini</label>
<p class="text-muted small mt-1 mb-2">Gunakan tombol preview untuk melihat lampiran.</p>
<label class="form-label mb-0">Lampiran Tersimpan</label>
<p class="text-muted small mt-1 mb-2">Pratinjau, unduh, atau hapus lampiran yang telah diunggah sebelumnya.</p>
<div class="table-responsive">
<table class="table table-bordered align-middle" id="vehicle-existing-attachments-table">
<table class="table table-bordered align-middle" id="existing-attachments-table">
<thead class="table-light">
<tr>
<th style="width: 30%">Kategori</th>
<th style="width: 35%">Nama File</th>
<th style="width: 15%" class="text-center">Preview</th>
<th style="width: 20%" class="text-center">Aksi</th>
<th style="width: 30%">Kategori Lampiran</th>
<th style="width: 30%">Nama File</th>
<th style="width: 15%" class="text-center">Pratinjau</th>
<th style="width: 15%" class="text-center">Unduh</th>
<th style="width: 10%" class="text-center">Aksi</th>
</tr>
</thead>
<tbody>
@forelse ($attachments as $attachment)
<tr class="vehicle-existing-attachment-row" data-attachment-id="{{ $attachment['id'] }}">
<td>{{ $attachment['file_category'] ? ucwords(str_replace('_', ' ', $attachment['file_category'])) : '-' }}</td>
@php
$categoryValue = $attachment['file_category'] ?? null;
$categoryLabel = $categoryValue
? ($attachmentCategoryLabels[$categoryValue] ?? ucwords(str_replace('_', ' ', $categoryValue)))
: '-';
@endphp
<tr data-attachment-id="{{ $attachment['id'] }}">
<td>{{ $categoryLabel }}</td>
<td>{{ $attachment['filename'] ?? basename($attachment['file_path']) }}</td>
<td class="text-center">
<button type="button"
class="btn btn-sm btn-outline-secondary vehicle-preview-existing-attachment"
class="btn btn-sm btn-outline-secondary preview-existing-attachment"
data-preview-url="{{ $attachment['preview_url'] }}"
data-download-url="{{ $attachment['download_url'] }}"
data-preview-type="{{ $attachment['preview_type'] }}"
data-file-name="{{ $attachment['filename'] ?? basename($attachment['file_path']) }}">
Preview
data-category="{{ $categoryLabel }}">
Pratinjau
</button>
</td>
<td class="text-center">
<a href="{{ $attachment['download_url'] }}"
target="_blank"
class="btn btn-sm btn-outline-primary mr-1">
Download
class="btn btn-sm btn-outline-primary">
Unduh
</a>
@if($attachment['can_delete'])
</td>
<td class="text-center">
<button type="button"
class="btn btn-sm btn-outline-danger vehicle-delete-attachment"
class="btn btn-sm btn-outline-danger delete-attachment"
data-delete-url="{{ route('forms.vehicle.attachments.destroy', [$form->id, $attachment['id']]) }}">
<i class="fas fa-trash"></i>
</button>
@endif
</td>
</tr>
@empty
<tr class="vehicle-existing-attachments-empty text-center text-muted">
<td colspan="4">Belum ada lampiran.</td>
<tr class="existing-attachments-empty text-center text-muted">
<td colspan="5">Belum ada lampiran tersimpan.</td>
</tr>
@endforelse
</tbody>
@@ -155,15 +171,15 @@
<div class="mb-3">
<div class="d-flex align-items-center justify-content-between">
<label class="form-label mb-0">Tambah Lampiran Baru</label>
<button type="button" class="btn btn-outline-primary btn-sm" id="vehicle-add-new-attachment-row">
<i class="fas fa-plus mr-1"></i> Tambah Lampiran
<button type="button" class="btn btn-outline-primary btn-sm" id="vehicle-add-attachment-row">
<i class="fas fa-plus mr-1"></i> Add attachment
</button>
</div>
<p class="text-muted small mt-1 mb-2">
Maksimal 10 MB per file. Semua tipe file diperbolehkan kecuali <code>.exe</code>, <code>.bat</code>, <code>.sh</code>, <code>.cmd</code>, <code>.dll</code>, dan <code>.msi</code>.
</p>
<div class="table-responsive">
<table class="table table-bordered align-middle" id="vehicle-new-attachments-table">
<table class="table table-bordered align-middle" id="vehicle-attachments-table">
<thead class="table-light">
<tr>
<th style="width: 30%">Kategori</th>
@@ -173,8 +189,8 @@
</tr>
</thead>
<tbody>
<tr class="vehicle-new-attachments-empty text-center text-muted">
<td colspan="4">Belum ada lampiran baru.</td>
<tr class="vehicle-attachments-empty text-center text-muted">
<td colspan="4">Belum ada lampiran.</td>
</tr>
</tbody>
</table>
@@ -182,13 +198,13 @@
</div>
</div>
<button type="submit" class="btn btn-primary ml-2">Save</button>
<button type="submit" class="btn btn-primary ml-2">Simpan Perubahan</button>
</div>
</form>
@include('backend.components.attachment-preview-modal', [
'modalId' => 'vehicleExistingAttachmentPreviewModal',
'title' => 'Preview Lampiran',
'modalId' => 'existingAttachmentPreviewModal',
'title' => 'Pratinjau Lampiran',
])
@include('backend.components.attachment-preview-modal', [
@@ -198,9 +214,8 @@
<div id="loading-spinner-overlay" class="d-none">
<div class="spinner-wrapper">
<div class="spinner-border text-primary" role="status">
</div>
<p>Submitting, please wait...</p>
<div class="spinner-border text-primary" role="status"></div>
<p class="mt-2">Submitting, please wait...</p>
</div>
</div>
</div>
@@ -208,28 +223,38 @@
</div>
</section>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
new AutoNumeric('#total', {
$(document).ready(function() {
// Setup Global Token CSRF Khusus untuk Delete File via AJAX
const csrfToken = $('meta[name="csrf-token"]').attr('content');
if (csrfToken) {
$.ajaxSetup({
headers: { 'X-CSRF-TOKEN': csrfToken }
});
}
// 1. Inisialisasi Aman AutoNumeric (Sesuai Standar Up Country)
const autoNumericConfig = {
digitGroupSeparator: '.',
decimalCharacter: ',',
currencySymbol: 'Rp.',
currencySymbol: 'Rp. ',
decimalPlaces: 0,
minimumValue: '0',
unformatOnSubmit: true
};
const totalInput = new AutoNumeric('#total', autoNumericConfig);
// 2. Cegah Scroll Wheel
$('#total, #liter, #jarak').on('wheel font-wheel', function(e) {
e.preventDefault();
$(this).blur();
});
const spinnerOverlay = document.getElementById('loading-spinner-overlay');
document.getElementById('expense-form').addEventListener('submit', function () {
spinnerOverlay.classList.remove('d-none');
spinnerOverlay.classList.add('d-flex');
});
// Toggle gasoline specific fields
// 3. Toggle Logika Field Bensin
const expenseTypeSelect = document.getElementById('expense_type');
const gasolineFields = document.querySelectorAll('.gasoline-fields');
const literInput = document.querySelector('input[name="liter"]');
const jarakInput = document.querySelector('input[name="jarak"]');
const literInput = document.getElementById('liter');
const jarakInput = document.getElementById('jarak');
const tipeBensinSelect = document.getElementById('tipe_bensin');
const nopolInput = document.getElementById('nopol');
@@ -256,39 +281,109 @@
toggleGasolineFields();
expenseTypeSelect.addEventListener('change', toggleGasolineFields);
// 4. Integrasi Interseptor Form Submit & SweetAlert
const spinnerOverlay = $('#loading-spinner-overlay');
$('#expense-form').on('submit', function (e) {
e.preventDefault();
const form = this;
const totalExpense = totalInput.getNumber() || 0;
if (totalExpense > 1000000) {
Swal.fire({
title: 'Nominal Melebihi Batas Expense!',
text: `Total pengajuan Anda adalah Rp ${new Intl.NumberFormat('id-ID').format(totalExpense)}. Jumlah ini melebihi batas standar Rp 1.000.000. Apakah yakin tetap ajukan?`,
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Yes, Tetap Simpan!',
cancelButtonText: 'No, Batalkan'
}).then((result) => {
if (result.isConfirmed) {
spinnerOverlay.removeClass('d-none').addClass('d-flex');
form.submit();
}
});
} else {
spinnerOverlay.removeClass('d-none').addClass('d-flex');
form.submit();
}
});
// 5. Lampiran Engine AJAX (Existing Table Delete)
$(document).on('click', '.delete-attachment', function () {
const button = $(this);
const deleteUrl = button.data('delete-url');
const $row = button.closest('tr');
const existingAttachmentsTableBody = $('#existing-attachments-table tbody');
const emptyExistingRowMarkup = '<tr class="existing-attachments-empty text-center text-muted"><td colspan="5">Belum ada lampiran tersimpan.</td></tr>';
Swal.fire({
title: 'Hapus lampiran?',
text: 'Lampiran yang dihapus tidak dapat dikembalikan.',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Ya, hapus',
cancelButtonText: 'Batal'
}).then((result) => {
if (!result.isConfirmed) return;
$.ajax({
url: deleteUrl,
type: 'DELETE',
success: function (response) {
$row.remove();
if (!existingAttachmentsTableBody.find('tr').not('.existing-attachments-empty').length) {
existingAttachmentsTableBody.html(emptyExistingRowMarkup);
}
Swal.fire('Berhasil', response?.message || 'Lampiran berhasil dihapus.', 'success');
},
error: function (xhr) {
const message = xhr?.responseJSON?.message || 'Gagal menghapus lampiran.';
Swal.fire('Error', message, 'error');
}
});
});
});
$(document).on('click', '.preview-existing-attachment', function () {
const button = $(this);
const previewType = button.data('preview-type');
const previewUrl = button.data('preview-url');
const downloadUrl = button.data('download-url');
const category = button.data('category') || 'Lampiran';
if (!previewUrl) {
if (downloadUrl) window.open(downloadUrl, '_blank');
return;
}
openAttachmentModal('#existingAttachmentPreviewModal', {
title: category, type: previewType, source: previewUrl
});
});
// 6. Lampiran Engine Tambahan Baru
const attachmentCategories = @json($attachmentCategories ?? ($vehicleAttachmentCategories ?? []));
const blockedExtensions = ['exe', 'bat', 'sh', 'cmd', 'dll', 'msi'];
const maxFileSizeBytes = 10 * 1024 * 1024;
const existingEmptyRow = '<tr class="vehicle-existing-attachments-empty text-center text-muted"><td colspan="4">Belum ada lampiran.</td></tr>';
const newEmptyRow = '<tr class="vehicle-new-attachments-empty text-center text-muted"><td colspan="4">Belum ada lampiran baru.</td></tr>';
let vehicleNewAttachmentIndex = 0;
const vehicleAttachmentEmptyRow = '<tr class="vehicle-attachments-empty text-center text-muted"><td colspan="4">Belum ada lampiran.</td></tr>';
let vehicleAttachmentIndex = 0;
function escapeHtml(value) {
return String(value || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;');
}
function categoryToLabel(value) {
if (!value) {
return 'Pilih Kategori';
}
return value.replace(/_/g, ' ').replace(/\b\w/g, function (char) {
return char.toUpperCase();
});
if (!value) return 'Pilih Kategori';
return value.replace(/_/g, ' ').replace(/\b\w/g, function (char) { return char.toUpperCase(); });
}
function detectPreviewType(extension) {
const images = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
if (images.indexOf(extension) !== -1) {
return 'image';
}
if (extension === 'pdf') {
return 'pdf';
}
if (images.indexOf(extension) !== -1) return 'image';
if (extension === 'pdf') return 'pdf';
return 'other';
}
@@ -299,13 +394,10 @@
}
function setInlinePreview($row, type, source) {
const $box = $row.find('.vehicle-new-attachment-inline-preview');
const $box = $row.find('.vehicle-attachment-inline-preview');
$box.removeClass('bg-light').html('<i class="fas fa-file-upload text-muted"></i>');
if (type === 'image' && source) {
$box.addClass('bg-light').html(
'<img src="' + source + '" class="img-thumbnail" style="width:100%;height:100%;object-fit:cover;" alt="Preview">'
);
$box.addClass('bg-light').html('<img src="' + source + '" class="img-thumbnail" style="width:100%;height:100%;object-fit:cover;" alt="Preview">');
} else if (type === 'pdf') {
$box.addClass('bg-light').html('<i class="fas fa-file-pdf text-danger fa-lg"></i>');
} else if (type === 'other') {
@@ -313,43 +405,25 @@
}
}
function resetNewAttachmentRowData($row, clearInput = true) {
function resetAttachmentRowData($row, clearInput = true) {
const existingUrl = $row.data('objectUrl');
if (existingUrl) {
URL.revokeObjectURL(existingUrl);
}
$row.removeData('objectUrl')
.removeData('previewType')
.removeData('previewSource')
.removeData('previewFileName')
.removeData('downloadUrl');
if (existingUrl) URL.revokeObjectURL(existingUrl);
$row.removeData('objectUrl').removeData('previewType').removeData('previewSource').removeData('previewFileName').removeData('downloadUrl');
setInlinePreview($row, null, null);
$row.find('.vehicle-preview-new-attachment').addClass('d-none');
$row.find('.vehicle-new-preview-filename').text('');
if (clearInput) {
$row.find('.vehicle-new-attachment-file').val('');
}
$row.find('.vehicle-preview-filename').text('');
if (clearInput) $row.find('.vehicle-attachment-file').val('');
}
function openAttachmentModal(modalSelector, options) {
const settings = Object.assign({
title: 'Lampiran',
type: 'other',
source: null,
downloadUrl: null,
fileName: 'Lampiran'
}, options || {});
const settings = Object.assign({ title: 'Lampiran', type: 'other', source: null, downloadUrl: null, fileName: 'Lampiran' }, options || {});
const $modal = $(modalSelector);
const $image = $modal.find('.attachment-preview-image');
const $object = $modal.find('.attachment-preview-object');
const $placeholder = $modal.find('.attachment-preview-placeholder');
$modal.find('.attachment-preview-modal-title').text(settings.title || 'Lampiran');
$image.addClass('d-none').attr('src', '');
$object.addClass('d-none').attr('data', '').attr('src', '');
$placeholder.removeClass('d-none').html('Tidak ada file untuk ditampilkan.');
@@ -363,100 +437,66 @@
} else {
const safeName = escapeHtml(settings.fileName || 'Lampiran');
let message = '<p class="mb-2">' + safeName + '</p><p class="text-muted mb-0">Preview tidak tersedia. Silakan unduh file untuk melihat konten.</p>';
if (settings.downloadUrl) {
message += '<div class="mt-3"><a href="' + settings.downloadUrl + '" target="_blank" class="btn btn-sm btn-outline-primary">Download</a></div>';
}
if (settings.downloadUrl) message += '<div class="mt-3"><a href="' + settings.downloadUrl + '" target="_blank" class="btn btn-sm btn-outline-primary">Download</a></div>';
$placeholder.html(message);
}
if (window.bootstrap && bootstrap.Modal && typeof bootstrap.Modal.getOrCreateInstance === 'function') {
bootstrap.Modal.getOrCreateInstance($modal[0]).show();
} else {
$modal.modal('show');
}
}
function refreshExistingEmptyState() {
const $tbody = $('#vehicle-existing-attachments-table tbody');
if (!$tbody.find('.vehicle-existing-attachment-row').length) {
$tbody.html(existingEmptyRow);
}
}
function addVehicleAttachmentRow() {
const $tbody = $('#vehicle-attachments-table tbody');
if ($tbody.find('.vehicle-attachments-empty').length) $tbody.empty();
function refreshNewEmptyState() {
const $tbody = $('#vehicle-new-attachments-table tbody');
if (!$tbody.find('.vehicle-new-attachment-row').length) {
$tbody.html(newEmptyRow);
}
}
function addVehicleNewAttachmentRow() {
const $tbody = $('#vehicle-new-attachments-table tbody');
if ($tbody.find('.vehicle-new-attachments-empty').length) {
$tbody.empty();
}
const index = vehicleNewAttachmentIndex++;
const index = vehicleAttachmentIndex++;
const rowHtml = `
<tr class="vehicle-new-attachment-row" data-index="${index}">
<tr class="vehicle-attachment-row" data-index="${index}">
<td>
<select class="form-control vehicle-new-attachment-category" name="attachments[${index}][file_category]" required>
<select class="form-control vehicle-attachment-category" name="attachments[${index}][file_category]" required>
<option value="">${escapeHtml(categoryToLabel(''))}</option>
${buildCategoryOptions()}
</select>
</td>
<td>
<div class="d-flex align-items-center">
<div class="vehicle-new-attachment-inline-preview border rounded d-flex align-items-center justify-content-center bg-white mr-2" style="width:56px;height:56px;">
<div class="vehicle-attachment-inline-preview border rounded d-flex align-items-center justify-content-center bg-white mr-2" style="width:56px;height:56px;">
<i class="fas fa-file-upload text-muted"></i>
</div>
<input type="file" class="form-control vehicle-new-attachment-file" name="attachments[${index}][file_path]">
<input type="file" class="form-control vehicle-attachment-file" name="attachments[${index}][file_path]" required>
</div>
<div class="small text-muted mt-1 vehicle-new-preview-filename"></div>
<div class="small text-muted mt-1 vehicle-preview-filename"></div>
</td>
<td class="text-center">
<button type="button" class="btn btn-sm btn-outline-secondary vehicle-preview-new-attachment d-none">
Preview
</button>
<button type="button" class="btn btn-sm btn-outline-secondary vehicle-preview-new-attachment d-none">Preview</button>
</td>
<td class="text-center">
<button type="button" class="btn btn-sm btn-outline-danger vehicle-remove-new-attachment-row">
<i class="fas fa-trash"></i>
</button>
<button type="button" class="btn btn-sm btn-outline-danger vehicle-remove-attachment-row"><i class="fas fa-trash"></i></button>
</td>
</tr>
`;
$tbody.append(rowHtml);
}
$(function () {
$('#vehicle-add-new-attachment-row').on('click', function () {
addVehicleNewAttachmentRow();
});
$('#vehicle-add-attachment-row').on('click', function () { addVehicleAttachmentRow(); });
$(document).on('click', '.vehicle-remove-new-attachment-row', function () {
$(document).on('click', '.vehicle-remove-attachment-row', function () {
const $row = $(this).closest('tr');
const $tbody = $('#vehicle-new-attachments-table tbody');
resetNewAttachmentRowData($row, true);
const $tbody = $('#vehicle-attachments-table tbody');
resetAttachmentRowData($row, true);
$row.remove();
refreshNewEmptyState();
if (!$tbody.find('.vehicle-attachment-row').length) $tbody.html(vehicleAttachmentEmptyRow);
});
$(document).on('change', '.vehicle-new-attachment-file', function () {
$(document).on('change', '.vehicle-attachment-file', function () {
const $input = $(this);
const $row = $input.closest('tr');
const previewButton = $row.find('.vehicle-preview-new-attachment');
const filenameHolder = $row.find('.vehicle-new-preview-filename');
const filenameHolder = $row.find('.vehicle-preview-filename');
resetNewAttachmentRowData($row, false);
resetAttachmentRowData($row, false);
filenameHolder.text('');
const file = this.files && this.files[0];
if (!file) {
return;
}
if (!file) return;
if (file.size > maxFileSizeBytes) {
alert('Ukuran file melebihi 10 MB.');
@@ -495,7 +535,6 @@
$row.data('previewSource', null);
setInlinePreview($row, 'other', null);
}
previewButton.removeClass('d-none');
});
@@ -513,73 +552,6 @@
fileName: fileName
});
});
$(document).on('click', '.vehicle-preview-existing-attachment', function () {
const $button = $(this);
const previewType = $button.data('preview-type') || 'other';
const previewUrl = $button.data('preview-url') || null;
const downloadUrl = $button.data('download-url') || null;
const fileName = $button.data('file-name') || 'Lampiran';
let type = previewType;
let source = previewUrl;
if (!previewUrl || (previewType !== 'image' && previewType !== 'pdf')) {
type = 'other';
source = null;
}
openAttachmentModal('#vehicleExistingAttachmentPreviewModal', {
title: fileName,
type: type,
source: source,
downloadUrl: downloadUrl,
fileName: fileName
});
});
$(document).on('click', '.vehicle-delete-attachment', function () {
const $button = $(this);
const deleteUrl = $button.data('delete-url');
const $row = $button.closest('tr');
if (!deleteUrl) {
return;
}
Swal.fire({
title: 'Hapus lampiran?',
text: 'Lampiran yang dihapus tidak dapat dikembalikan.',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Ya, hapus',
cancelButtonText: 'Batal'
}).then((result) => {
if (!result.isConfirmed) {
return;
}
$.ajax({
url: deleteUrl,
type: 'DELETE',
success: function (response) {
$row.remove();
refreshExistingEmptyState();
Swal.fire('Berhasil', response?.message || 'Lampiran berhasil dihapus.', 'success');
},
error: function (xhr) {
const message = xhr?.responseJSON?.message || 'Gagal menghapus lampiran.';
Swal.fire('Error', message, 'error');
}
});
});
});
refreshExistingEmptyState();
refreshNewEmptyState();
// Provide an initial row for new attachments
addVehicleNewAttachmentRow();
});
</script>
@endsection
@@ -6,16 +6,72 @@
@section('admin-content')
<style>
/* Style Awal Anda */
#outstandingCard {
cursor: pointer;
}
#outstandingCard.active {
box-shadow: 0 0 0 0.25rem rgba(255, 255, 255, 0.35);
}
/* Style Baru: Mini Status Cards */
.status-card {
cursor: pointer;
transition: all 0.2s ease-in-out;
border: 1px solid rgba(0,0,0,0.08);
border-radius: 6px;
background-color: #fff;
}
.status-card:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0,0,0,0.1); }
.status-card.active-filter {
border: 2px solid #343a40;
box-shadow: 0 2px 10px rgba(0,0,0,0.15);
}
.mini-stat-box {
padding: 6px 10px;
display: flex;
align-items: center;
justify-content: space-between;
min-height: 45px;
}
.mini-stat-box h6 { margin: 0; font-weight: 800; font-size: 0.95rem; color: #333; }
.mini-stat-box p { margin: 0; font-size: 0.65rem; text-transform: uppercase; color: #777; font-weight: 600; }
/* Border kiri mini untuk indikator warna */
.status-card[data-status="On Progress"] { border-left: 3px solid #ffc107; }
.status-card[data-status="Approved 1"] { border-left: 3px solid #17a2b8; }
.status-card[data-status="Approved 2"] { border-left: 3px solid #007bff; }
.status-card[data-status="Final Approved"] { border-left: 3px solid #28a745; }
.status-card[data-status="Rejected"] { border-left: 3px solid #dc3545; }
.status-card[data-status="Closed"] { border-left: 3px solid #6c757d; }
</style>
@php
$outstandingIdCollection = collect($outstandingIds ?? []);
// ==========================================
// FILTER PERIODE BERJALAN KHUSUS UNTUK MINI CARDS
// (Agar Angka sinkron dengan tabel filter JS di bawah)
// ==========================================
$startDayIndex = (int) env('STARTING_DATE', 11);
$closingDayIndex = (int) env('CLOSING_DATE', 13);
$nowIndex = \Carbon\Carbon::now();
if ($nowIndex->day >= $startDayIndex) {
$startPeriodIndex = \Carbon\Carbon::create($nowIndex->year, $nowIndex->month, $startDayIndex)->startOfDay();
$endPeriodIndex = \Carbon\Carbon::create($nowIndex->copy()->addMonth()->year, $nowIndex->copy()->addMonth()->month, $closingDayIndex)->endOfDay();
} else {
$startPeriodIndex = \Carbon\Carbon::create($nowIndex->copy()->subMonth()->year, $nowIndex->copy()->subMonth()->month, $startDayIndex)->startOfDay();
$endPeriodIndex = \Carbon\Carbon::create($nowIndex->year, $nowIndex->month, $closingDayIndex)->endOfDay();
}
// Penampung data pengajuan MURNI HANYA DI PERIODE BERJALAN SAAT INI
$currentPeriodForms = $forms->filter(function($form) use ($startPeriodIndex, $endPeriodIndex) {
$tanggalForm = \Carbon\Carbon::parse($form->tanggal);
return $tanggalForm->between($startPeriodIndex, $endPeriodIndex);
});
@endphp
<section class="content-header">
<div class="container-fluid">
<div class="row mb-2">
@@ -25,52 +81,42 @@
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="{{ route('dashboard.index') }}">Dashboard</a></li>
</li>
</ol>
</div>
</div>
</div>
</section>
<section class="content">
<div class="container-fluid">
<div class="row row-cols-1 row-cols-md-2 row-cols-xl-5 g-3">
<!-- Total Expense -->
<div class="col d-flex">
<div class="small-box bg-info flex-fill">
<div class="inner">
<h3>Rp {{ number_format($forms->sum('total'), 0, ',', '.') }}</h3>
<p>Total Expense</p>
</div>
<div class="icon">
<i class="fas fa-wallet"></i>
<div class="icon"><i class="fas fa-wallet"></i></div>
</div>
</div>
</div>
<!-- Approved Expense -->
<div class="col d-flex">
<div class="small-box bg-success flex-fill">
<div class="inner">
<h3>Rp {{ number_format($forms->whereIn('status', ['Approved', 'Closed'])->sum('approved_total'), 0, ',', '.') }}</h3>
<h3>Rp {{ number_format($forms->whereIn('status', ['Approved', 'Closed', 'Final Approved'])->sum('approved_total'), 0, ',', '.') }}</h3>
<p>Approved Expenses</p>
</div>
<div class="icon">
<i class="fas fa-check-circle"></i>
<div class="icon"><i class="fas fa-check-circle"></i></div>
</div>
</div>
</div>
<!-- Pending Expense -->
<div class="col d-flex">
<div class="small-box bg-warning flex-fill">
<div class="inner">
<h3 class="text-white">Rp {{ number_format($forms->where('status', 'On Progress')->sum('total'), 0, ',', '.') }}</h3>
<p class="text-white">Pending Expenses</p>
</div>
<div class="icon">
<i class="fas fa-hourglass-half"></i>
<div class="icon"><i class="fas fa-hourglass-half"></i></div>
</div>
</div>
</div>
<!-- Outstanding Expense -->
<div class="col d-flex">
<div class="small-box bg-danger flex-fill" id="outstandingCard">
<div class="inner">
@@ -79,9 +125,7 @@
<p class="mb-0"><small>Total Rp {{ number_format($outstandingTotal, 0, ',', '.') }}</small></p>
<p class="mb-0 filter-hint"><small>Klik untuk menampilkan detail</small></p>
</div>
<div class="icon">
<i class="fas fa-exclamation-triangle"></i>
</div>
<div class="icon"><i class="fas fa-exclamation-triangle"></i></div>
</div>
</div>
@if($availableBudget > 0)
@@ -91,15 +135,77 @@
<h3 class="text-white">Rp {{ number_format($availableBudget, 0, ',', '.') }}</h3>
<p class="text-white">Available Budget</p>
</div>
<div class="icon">
<i class="fas fa-money-bill"></i>
</div>
<div class="icon"><i class="fas fa-money-bill"></i></div>
</div>
</div>
@endif
</div>
</div>
</section>
<section class="content mt-2">
<div class="container-fluid">
<div class="row row-cols-2 row-cols-sm-3 row-cols-md-4 row-cols-xl-7 g-2 mb-3">
<div class="col">
<div class="status-card active-filter" data-status="all">
<div class="mini-stat-box">
<div><p>Semua</p><h6>{{ $currentPeriodForms->count() }}</h6></div>
<i class="fas fa-list-ul text-muted"></i>
</div>
</div>
</div>
<div class="col">
<div class="status-card" data-status="On Progress">
<div class="mini-stat-box">
<div><p>Progress</p><h6>{{ $currentPeriodForms->where('status', 'On Progress')->count() }}</h6></div>
<i class="fas fa-sync text-warning fa-xs"></i>
</div>
</div>
</div>
<div class="col">
<div class="status-card" data-status="Approved 1">
<div class="mini-stat-box">
<div><p>App 1</p><h6>{{ $currentPeriodForms->where('status', 'Approved 1')->count() }}</h6></div>
<i class="fas fa-user-check text-info fa-xs"></i>
</div>
</div>
</div>
<div class="col">
<div class="status-card" data-status="Approved 2">
<div class="mini-stat-box">
<div><p>App 2</p><h6>{{ $currentPeriodForms->where('status', 'Approved 2')->count() }}</h6></div>
<i class="fas fa-users text-primary fa-xs"></i>
</div>
</div>
</div>
<div class="col">
<div class="status-card" data-status="Final Approved">
<div class="mini-stat-box">
<div><p>Final</p><h6>{{ $currentPeriodForms->where('status', 'Final Approved')->count() }}</h6></div>
<i class="fas fa-check-double text-success fa-xs"></i>
</div>
</div>
</div>
<div class="col">
<div class="status-card" data-status="Rejected">
<div class="mini-stat-box">
<div><p>Reject</p><h6>{{ $currentPeriodForms->where('status', 'Rejected')->count() }}</h6></div>
<i class="fas fa-ban text-danger fa-xs"></i>
</div>
</div>
</div>
<div class="col">
<div class="status-card" data-status="Closed">
<div class="mini-stat-box">
<div><p>Closed</p><h6>{{ $currentPeriodForms->where('status', 'Closed')->count() }}</h6></div>
<i class="fas fa-lock text-secondary fa-xs"></i>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="content">
<div class="container-fluid">
<div class="card card-primary card-outline">
@@ -116,9 +222,7 @@
<div class="dataTables_wrapper dt-bootstrap4 mt-2">
@include('backend.layouts.partials.messages')
<div class="table-responsive">
<table id="dataTable"
class="table table-bordered table-striped table-head-fixed dataTable dtr-inline"
style="width:100%">
<table id="dataTable" class="table table-bordered table-striped table-head-fixed dataTable dtr-inline" style="width:100%">
<thead class="bg-light text-capitalize">
<tr>
<th>#</th>
@@ -137,15 +241,9 @@
<th>Bukti</th>
<th>Account Number</th>
<th>Status</th>
@if (auth()->user()->can('approval.view'))
<th>Approval 1</th>
@endif
@if (auth()->user()->can('approval2.view'))
<th>Approval 2</th>
@endif
@if (auth()->user()->can('final_approval.view'))
<th>Final Approval</th>
@endif
@if (auth()->user()->can('approval.view')) <th>Approval 1</th> @endif
@if (auth()->user()->can('approval2.view')) <th>Approval 2</th> @endif
@if (auth()->user()->can('final_approval.view')) <th>Final Approval</th> @endif
<th>Aksi</th>
</tr>
</thead>
@@ -155,14 +253,13 @@
$role = auth()->user()->getRoleNames()[0];
@endphp
@foreach ($forms as $item)
<tr data-outstanding="{{ $outstandingIdCollection->contains($item->id) ? 1 : 0 }}">
<tr data-id="{{ $item->id }}" data-status="{{ $item->status }}" data-date="{{ \Carbon\Carbon::parse($item->tanggal)->format('Y-m-d') }}" data-outstanding="{{ $outstandingIdCollection->contains($item->id) ? '1' : '0' }}">
<td>{{ $loop->iteration }}</td>
<td>{{ $item->expense_number }}</td>
<td>{{ $item->user->name }}</td>
<td>{{ $item->user->cabang->cabang->name }}</td>
<td>{{ \Carbon\Carbon::parse($item->tanggal)->locale('id')->isoFormat('dddd, D MMMM Y HH:mm:ss') }}</td>
<td>
{{-- gunakan badge --}}
@if ($item->type == 'gasoline')
<span class="badge badge-info">Gasoline</span>
@elseif ($item->type == 'parking_toll')
@@ -180,9 +277,7 @@
<td>{{ $item->nopol }}</td>
<td>
@if ($item->bukti_total)
<a href="{{ NextCloudHelper::getFileUrl($item->bukti_total) }}" target="_blank">
Download
</a>
<a href="{{ NextCloudHelper::getFileUrl($item->bukti_total) }}" target="_blank">Download</a>
@else
-
@endif
@@ -197,115 +292,69 @@
<span class="badge badge-danger text-white">{{ $item->status }}</span>
@endif
</td>
{{-- SELEKTIF MENYEMBUNYIKAN TOMBOL APPROVE/REJECT DIRECT DI INDEX TANPA MENGHAPUS LOGIKA KODE --}}
@if (auth()->user()->can('approval.view'))
<td>
{{-- approve / reject button --}}
@if (auth()->user()->can('approval.approve'))
@if ($item->status == 'On Progress')
<div class="d-none">
@if (auth()->user()->can('approval.approve') && $item->status == 'On Progress')
<form action="javascript:void(0);" class="d-inline">
<button type="button"
class="btn btn-success btn-sm final-approve-modal"
data-id="{{ route('forms.vehicle.approve', $item->id) }}">
Approve 1
</button>
<button type="button" class="btn btn-success btn-sm final-approve-modal" data-id="{{ route('forms.vehicle.approve', $item->id) }}">Approve 1</button>
</form>
<button type="button"
class="btn btn-danger btn-sm open-reject-modal"
data-action="{{ route('forms.vehicle.reject', $item->id) }}">
Reject
</button>
@endif
@endif
@if ($item->approved_at)
{{ \Carbon\Carbon::parse($item->approved_at)->locale('id')->isoFormat('D MMMM Y HH:mm') }}
<button type="button" class="btn btn-danger btn-sm open-reject-modal" data-action="{{ route('forms.vehicle.reject', $item->id) }}">Reject</button>
@endif
</div>
@if ($item->approved_at) {{ \Carbon\Carbon::parse($item->approved_at)->locale('id')->isoFormat('D MMMM Y HH:mm') }} @endif
</td>
@endif
@if (auth()->user()->can('approval2.view'))
<td>
{{-- approve / reject button --}}
@if (auth()->user()->can('approval2.approve'))
@if ($item->status == 'Approved 1' && $item->approved_at)
<div class="d-none">
@if (auth()->user()->can('approval2.approve') && $item->status == 'Approved 1' && $item->approved_at)
<form action="javascript:void(0);" class="d-inline">
<button type="button"
class="btn btn-success btn-sm open-approve-modal"
data-id="{{ route('forms.vehicle.approve2', $item->id) }}">
Approve 2
</button>
<button type="button" class="btn btn-success btn-sm open-approve-modal" data-id="{{ route('forms.vehicle.approve2', $item->id) }}">Approve 2</button>
</form>
<button type="button"
class="btn btn-danger btn-sm open-reject-modal"
data-action="{{ route('forms.vehicle.reject', $item->id) }}">
Reject
</button>
@endif
@endif
@if ($item->approved2_at)
{{ \Carbon\Carbon::parse($item->approved2_at)->locale('id')->isoFormat('D MMMM Y HH:mm') }}
<button type="button" class="btn btn-danger btn-sm open-reject-modal" data-action="{{ route('forms.vehicle.reject', $item->id) }}">Reject</button>
@endif
</div>
@if ($item->approved2_at) {{ \Carbon\Carbon::parse($item->approved2_at)->locale('id')->isoFormat('D MMMM Y HH:mm') }} @endif
</td>
@endif
@if(auth()->user()->can('final_approval.view'))
<td>
{{-- approve / reject button --}}
<div class="d-none">
@if(auth()->user()->can('final_approval.approve'))
@if ($item->status == 'Approved 2' && $item->approved2_at && !$item->final_approved_at)
<form action="javascript:void(0);" class="d-inline">
<button type="button"
class="btn btn-success btn-sm open-approve-modal"
data-id="{{ route('forms.vehicle.final-approve', $item->id) }}">
Final Approve
</button>
<button type="button" class="btn btn-success btn-sm open-approve-modal" data-id="{{ route('forms.vehicle.final-approve', $item->id) }}">Final Approve</button>
</form>
@else
@if ($item->final_approved_at && auth()->user()->can('final_approval.open'))
<form action="{{ route('forms.vehicle.open', $item->id) }}" method="POST" class="d-inline"onsubmit="return confirm('Are you sure you want to approve this item?');">
@csrf
@method('PUT')
<button type="submit" class="btn btn-success btn-sm">
Open
</button>
<button type="button" class="btn btn-danger btn-sm open-reject-modal" data-action="{{ route('forms.vehicle.reject', $item->id) }}">Reject</button>
@elseif ($item->final_approved_at && auth()->user()->can('final_approval.open'))
<form action="{{ route('forms.vehicle.open', $item->id) }}" method="POST" class="d-inline" onsubmit="return confirm('Are you sure you want to open this item?');">
@csrf @method('PUT')
<button type="submit" class="btn btn-success btn-sm">Open</button>
</form>
@endif
@endif
@endif
@if ($item->final_approved_at)
{{ \Carbon\Carbon::parse($item->final_approved_at)->locale('id')->isoFormat('D MMMM Y HH:mm') }}
@endif
</div>
@if ($item->final_approved_at) {{ \Carbon\Carbon::parse($item->final_approved_at)->locale('id')->isoFormat('D MMMM Y HH:mm') }} @endif
</td>
@endif
<td>
@if (auth()->user()->can('forms.vehicle.view'))
<a href="{{ route('forms.vehicle.view', $item->id) }}" class="btn btn-primary btn-sm">
<i class="fas fa-eye text-white"></i>
</a>
<a href="{{ route('forms.vehicle.view', $item->id) }}" class="btn btn-primary btn-sm"><i class="fas fa-eye text-white"></i></a>
@endif
@php
$canEdit = false;
if (auth()->user()->can('forms.vehicle.edit')) {
if (in_array($role, ['Medical Representatif', 'Area Manager Cabang'])) {
$canEdit = $item->status == 'On Progress';
} else {
$canEdit = true;
}
}
$canEdit = auth()->user()->can('forms.vehicle.edit') && (!in_array($role, ['Medical Representatif', 'Area Manager Cabang']) || $item->status == 'On Progress');
@endphp
@if ($canEdit)
<a href="{{ route('forms.vehicle.edit', $item->id) }}" class="btn btn-warning btn-sm">
<i class="fas fa-edit text-white"></i>
</a>
<a href="{{ route('forms.vehicle.edit', $item->id) }}" class="btn btn-warning btn-sm"><i class="fas fa-edit text-white"></i></a>
@endif
@if (($role != 'Medical Representatif' && auth()->user()->can('forms.vehicle.delete')) || ($role == 'Medical Representatif' && $item->status == 'On Progress'))
<form action="{{ route('forms.vehicle.destroy', $item->id) }}" method="POST" class="d-inline" onsubmit="return confirm('Are you sure you want to delete this item?');">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-danger btn-sm">
<i class="fas fa-trash"></i>
</button>
@csrf @method('DELETE')
<button type="submit" class="btn btn-danger btn-sm"><i class="fas fa-trash"></i></button>
</form>
@endif
</td>
@@ -319,126 +368,64 @@
</div>
</div>
</section>
<section>
<div class="modal fade" id="approveModal" tabindex="-1" role="dialog" aria-labelledby="approveModalLabel" aria-hidden="true">
{{-- MODAL CONTAINER DI BAWAH (TETAP UTUH UNTUK KEBUTUHAN ROUTING AJAX) --}}
<div class="modal fade" id="approveModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<form id="approveForm" method="POST" action="">
@csrf
@method('PUT')
@csrf @method('PUT')
<div class="modal-header">
<h5 class="modal-title" id="approveModalLabel">Approve Item</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
</div>
<div class="modal-body">
<!-- Loading spinner -->
<div id="loadingSpinner" class="d-flex justify-content-center align-items-center" style="height: 200px;">
<div class="spinner-border text-primary" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
<!-- Modal content (hidden initially) -->
<div id="loadingSpinner" class="d-flex justify-content-center align-items-center" style="height: 200px;"><div class="spinner-border text-primary" role="status"><span class="sr-only">Loading...</span></div></div>
<div id="modalContent" class="d-none">
<p>Please review the details below:</p>
<ul class="list-group">
<li class="list-group-item">
<strong>Expense Number:</strong> <span id="expense_number"></span>
</li>
<li class="list-group-item">
<strong>User:</strong> <span id="user"></span>
</li>
<li class="list-group-item">
<strong>Tipe:</strong> <span id="type"></span>
</li>
<li class="list-group-item">
<strong>Tanggal & Waktu:</strong> <span id="tanggal"></span>
</li>
<li class="list-group-item">
<strong>Jarak:</strong> <span id="jarak"></span> km
</li>
<li class="list-group-item">
<strong>Total Liter Bensin:</strong> <span id="liter"></span> liter
</li>
<li class="list-group-item">
<strong>No Polisi:</strong> <span id="nopol"></span>
</li>
<li class="list-group-item">
<strong>Tipe Bensin:</strong> <span id="tipe_bensin"></span>
</li>
<li class="list-group-item">
<strong>Bukti Total:</strong> <span id="bukti_total"></span>
</li>
<li class="list-group-item">
<strong>Nominal Total:</strong> <span id="total_value"></span>
</li>
<li class="list-group-item"><strong>Expense Number:</strong> <span id="expense_number"></span></li>
<li class="list-group-item"><strong>User:</strong> <span id="user"></span></li>
<li class="list-group-item"><strong>Tipe:</strong> <span id="type"></span></li>
<li class="list-group-item"><strong>Tanggal & Waktu:</strong> <span id="tanggal"></span></li>
<li class="list-group-item"><strong>Jarak:</strong> <span id="jarak"></span> km</li>
<li class="list-group-item"><strong>Total Liter Bensin:</strong> <span id="liter"></span> liter</li>
<li class="list-group-item"><strong>No Polisi:</strong> <span id="nopol"></span></li>
<li class="list-group-item"><strong>Tipe Bensin:</strong> <span id="tipe_bensin"></span></li>
<li class="list-group-item"><strong>Bukti Total:</strong> <span id="bukti_total"></span></li>
<li class="list-group-item"><strong>Nominal Total:</strong> <span id="total_value"></span></li>
</ul>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Approve</button>
</div>
<div class="modal-footer"><button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button><button type="submit" class="btn btn-primary">Approve</button></div>
</form>
</div>
</div>
</div>
</section>
<section>
<div class="modal fade" id="finalApproveModal" tabindex="-1" role="dialog" aria-labelledby="finalApproveModalLabel" aria-hidden="true">
<div class="modal fade" id="finalApproveModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<form id="finalApproveForm" method="POST" action="">
@csrf
@method('PUT')
@csrf @method('PUT')
<div class="modal-header">
<h5 class="modal-title" id="approveModalLabel">Approve Item</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h5 class="modal-title" id="finalApproveModalLabel">Approve Item</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
</div>
<div class="modal-body">
<!-- Loading spinner -->
<div id="finalLoadingSpinner" class="d-flex justify-content-center align-items-center" style="height: 200px;">
<div class="spinner-border text-primary" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
<!-- Modal content (hidden initially) -->
<div id="finalLoadingSpinner" class="d-flex justify-content-center align-items-center" style="height: 200px;"><div class="spinner-border text-primary" role="status"><span class="sr-only">Loading...</span></div></div>
<div id="finalModalContent" class="d-none">
<p>Please review the details below:</p>
<ul class="list-group">
<li class="list-group-item">
<strong>Expense Number:</strong> <span id="final_expense_number"></span>
</li>
<li class="list-group-item">
<strong>User:</strong> <span id="final_user"></span>
</li>
<li class="list-group-item">
<strong>Tipe:</strong> <span id="final_type"></span>
</li>
<li class="list-group-item">
<strong>Tanggal & Waktu:</strong> <span id="final_tanggal"></span>
</li>
<li class="list-group-item">
<strong>Jarak:</strong> <span id="final_jarak"></span> km
</li>
<li class="list-group-item">
<strong>Total Liter Bensin:</strong> <span id="final_liter"></span> liter
</li>
<li class="list-group-item">
<strong>No Polisi:</strong> <span id="final_nopol"></span>
</li>
<li class="list-group-item">
<strong>Tipe Bensin:</strong> <span id="final_tipe_bensin"></span>
</li>
<li class="list-group-item">
<strong>Bukti Total:</strong> <span id="final_bukti_total"></span>
</li>
<li class="list-group-item"><strong>Expense Number:</strong> <span id="final_expense_number"></span></li>
<li class="list-group-item"><strong>User:</strong> <span id="final_user"></span></li>
<li class="list-group-item"><strong>Tipe:</strong> <span id="final_type"></span></li>
<li class="list-group-item"><strong>Tanggal & Waktu:</strong> <span id="final_tanggal"></span></li>
<li class="list-group-item"><strong>Jarak:</strong> <span id="final_jarak"></span> km</li>
<li class="list-group-item"><strong>Total Liter Bensin:</strong> <span id="final_liter"></span> liter</li>
<li class="list-group-item"><strong>No Polisi:</strong> <span id="final_nopol"></span></li>
<li class="list-group-item"><strong>Tipe Bensin:</strong> <span id="final_tipe_bensin"></span></li>
<li class="list-group-item"><strong>Bukti Total:</strong> <span id="final_bukti_total"></span></li>
</ul>
<p class="mt-2">Please select the items you want to approve:</p>
<ul class="list-group list-unstyled">
@@ -450,233 +437,185 @@
</div>
</div>
</li>
<li>
<div class="form-group">
<label for="total">Total</label>
<input type="text" class="form-control" id="total" name="total" value="0" readonly>
</div>
</li>
</ul>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Approve</button>
</div>
<div class="modal-footer"><button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button><button type="submit" class="btn btn-primary">Approve</button></div>
</form>
</div>
</div>
</div>
</section>
<section>
<div class="modal fade" id="rejectModal" tabindex="-1" role="dialog" aria-labelledby="rejectModalLabel" aria-hidden="true">
<div class="modal fade" id="rejectModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<form id="rejectForm" method="POST" action="">
@csrf
@method('PUT')
@csrf @method('PUT')
<div class="modal-header">
<h5 class="modal-title" id="rejectModalLabel">Reject Expense</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="rejectRemarks">Remarks <span class="text-danger">*</span></label>
<textarea class="form-control" name="remarks" id="rejectRemarks" rows="3" required></textarea>
</div>
<div class="form-group"><label for="rejectRemarks">Remarks <span class="text-danger">*</span></label><textarea class="form-control" name="remarks" id="rejectRemarks" rows="3" required></textarea></div>
<p class="mb-0 text-muted small">Pengajuan akan ditolak setelah Anda mengirimkan catatan ini.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-danger">Reject</button>
</div>
<div class="modal-footer"><button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button><button type="submit" class="btn btn-danger">Reject</button></div>
</form>
</div>
</div>
</div>
</section>
@endsection
@section('scripts')
<script>
<script>
$(document).ready(function () {
// Uncheck all checkboxes on page refresh
$('.custom-control-input').prop('checked', false);
if ($('#dataTable').length) {
let outstandingFilterActive = false;
const outstandingFilter = function(settings, data, dataIndex) {
if (!outstandingFilterActive) {
return true;
}
const row = settings.aoData[dataIndex].nTr;
return $(row).data('outstanding') === 1 || $(row).data('outstanding') === '1';
};
let statusFilterActive = 'all';
$.fn.dataTable.ext.search.push(outstandingFilter);
// Custom Search DataTables (Filter Sinkron: Status + Outstanding + Siklus Periode Berjalan 11 - 13)
$.fn.dataTable.ext.search.push(function(settings, data, dataIndex) {
const row = $(settings.aoData[dataIndex].nTr);
const rowStatus = row.attr('data-status');
const isOutstanding = row.attr('data-outstanding') === '1' || row.attr('data-outstanding') === 1;
const rowDateStr = row.attr('data-date'); // Format: YYYY-MM-DD
// 1. Prioritas Pertama: Outstanding Filter Murni
if (outstandingFilterActive) {
return isOutstanding;
}
// 2. Prioritas Kedua: Aturan Jendela Waktu (Cut-off) Berlaku MUTLAK Untuk Semua Mini Card
if (rowDateStr) {
const dateParts = rowDateStr.split('-');
const rowDate = new Date(dateParts[0], dateParts[1] - 1, dateParts[2], 12, 0, 0);
const now = new Date();
let startPeriod, endPeriod;
const startDay = 11;
const closingDay = 13;
if (now.getDate() >= startDay) {
startPeriod = new Date(now.getFullYear(), now.getMonth(), startDay, 0, 0, 0);
endPeriod = new Date(now.getFullYear(), now.getMonth() + 1, closingDay, 23, 59, 59);
} else {
startPeriod = new Date(now.getFullYear(), now.getMonth() - 1, startDay, 0, 0, 0);
endPeriod = new Date(now.getFullYear(), now.getMonth(), closingDay, 23, 59, 59);
}
// TENGGELAMKAN ROW jika tidak berada di dalam siklus berjalan
if (rowDate < startPeriod || rowDate > endPeriod) {
return false;
}
}
// 3. Prioritas Ketiga: Filter Status Setelah Melewati Karantina Tanggal
if (statusFilterActive !== 'all' && rowStatus !== statusFilterActive) {
return false;
}
return true;
});
const table = $('#dataTable').DataTable({
responsive: false,
dom: 'Blfrtip',
lengthMenu: [ [10, 50, 100, -1], [10, 50, 100, "All"] ], // Control the entries dropdown
lengthMenu: [ [10, 50, 100, -1], [10, 50, 100, "All"] ],
buttons: [
{
extend: 'excel',
text: 'Excel'
},
{
extend: 'pdf',
text: 'PDF',
orientation: 'landscape',
pageSize: 'A4',
exportOptions: {
columns: ':visible'
}
},
{
extend: 'print',
text: 'Print',
orientation: 'landscape',
exportOptions: {
columns: ':visible'
}
},
{ extend: 'excel', text: 'Excel' },
{ extend: 'pdf', text: 'PDF', orientation: 'landscape', pageSize: 'A4', exportOptions: { columns: ':visible' } },
{ extend: 'print', text: 'Print', orientation: 'landscape', exportOptions: { columns: ':visible' } },
'colvis'
]
});
const $outstandingCard = $('#outstandingCard');
const updateCardHint = (isActive) => {
const text = isActive ? 'Klik untuk menampilkan semua data' : 'Klik untuk menampilkan detail';
$outstandingCard.find('.filter-hint small').text(text);
};
updateCardHint(outstandingFilterActive);
// Klik Status Card
$('.status-card').on('click', function() {
outstandingFilterActive = false;
$('#outstandingCard').removeClass('active');
$('#outstandingCard').find('.filter-hint small').text('Klik untuk detail');
$('.status-card').removeClass('active-filter');
$(this).addClass('active-filter');
statusFilterActive = $(this).data('status');
table.draw();
});
// Klik Outstanding Card
const $outstandingCard = $('#outstandingCard');
$outstandingCard.on('click', function () {
outstandingFilterActive = !outstandingFilterActive;
$outstandingCard.toggleClass('active', outstandingFilterActive);
$(this).toggleClass('active', outstandingFilterActive);
if(outstandingFilterActive) {
$('.status-card').removeClass('active-filter');
$('.status-card[data-status="all"]').addClass('active-filter');
statusFilterActive = 'all';
}
table.draw();
updateCardHint(outstandingFilterActive);
$(this).find('.filter-hint small').text(outstandingFilterActive ? 'Klik untuk menampilkan semua data' : 'Klik untuk menampilkan detail');
});
}
// Handle Reject Button Click
$(document).on('click', '.open-reject-modal', function () {
const rejectUrl = $(this).data('action');
$('#rejectForm').attr('action', rejectUrl);
$('#rejectForm').attr('action', $(this).data('action'));
$('#rejectRemarks').val('');
$('#rejectModal').modal('show');
});
$('#rejectModal').on('shown.bs.modal', function () {
$('#rejectRemarks').trigger('focus');
});
$('#rejectModal').on('hidden.bs.modal', function () {
$('#rejectForm').attr('action', '');
$('#rejectRemarks').val('');
});
$('#rejectModal').on('shown.bs.modal', function () { $('#rejectRemarks').trigger('focus'); });
$('#rejectModal').on('hidden.bs.modal', function () { $('#rejectForm').attr('action', ''); $('#rejectRemarks').val(''); });
// Handle Approve Button Click
$(document).on('click', '.open-approve-modal', function() {
const approveUrl = $(this).data('id');
$('#loadingSpinner').show(); $('#modalContent').addClass('d-none');
$('#approveForm').attr('action', approveUrl); $('#approveModal').modal('show');
// Show spinner and hide content initially
$('#loadingSpinner').show();
$('#modalContent').addClass('d-none');
$('#approveForm').attr('action', approveUrl); // Set the form action
$('#loadingSpinner').addClass('d-flex');
$('#approveModal').modal('show'); // Show the modal
// Get detail from /forms/vehicle/detail/{id}
$.get(approveUrl.replace(/approve2|final-approve/g, 'detail'), function (data) {
const formatter = new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
});
const formatter = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 });
$('#total_value').text(formatter.format(data.total));
// Detail data
$('#user').text(data.user.name);
$('#expense_number').text(data.expense_number);
$('#tanggal').text(new Date(data.created_at).toLocaleDateString('id-ID', {
year: 'numeric',
month: 'long',
day: 'numeric'
}));
// untuk type gunakan badge
$('#user').text(data.user.name); $('#expense_number').text(data.expense_number);
$('#tanggal').text(new Date(data.created_at).toLocaleDateString('id-ID', { year: 'numeric', month: 'long', day: 'numeric' }));
$('#type').html(data.type.split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '));
$('#jarak').text(data.jarak > 0 ? data.jarak : '-');
$('#liter').text(data.liter > 0 ? data.liter : '-');
$('#tipe_bensin').text(data.tipe_bensin);
$('#nopol').text(data.nopol);
$('#bukti_total').html(data.total ? '<a href="' + data.total + '" target="_blank">Download</a>' : '-');
// Hide spinner and show content
$('#loadingSpinner').hide();
$('#loadingSpinner').removeClass('d-flex');
$('#loadingSpinner').addClass('d-none');
$('#modalContent').removeClass('d-none');
// Reset the total to zero
$('#tipe_bensin').text(data.tipe_bensin); $('#nopol').text(data.nopol);
$('#bukti_total').html(data.bukti_total ? '<a href="' + data.bukti_total + '" target="_blank">Download</a>' : '-');
$('#loadingSpinner').hide(); $('#modalContent').removeClass('d-none');
$('#total').val(0);
});
});
// Handle Approve Button Click
// Handle Final Approve Modal
$(document).on('click', '.final-approve-modal', function() {
const approveUrl = $(this).data('id');
$('#finalLoadingSpinner').show(); $('#finalModalContent').addClass('d-none');
$('#finalApproveForm').attr('action', approveUrl); $('#finalApproveModal').modal('show');
// Show spinner and hide content initially
$('#finalLoadingSpinner').show();
$('#finalModalContent').addClass('d-none');
$('#finalApproveForm').attr('action', approveUrl); // Set the form action
$('#finalLoadingSpinner').addClass('d-flex');
$('#finalApproveModal').modal('show'); // Show the modal
// Get detail from /forms/vehicle/detail/{id}
$.get(approveUrl.replace('approve', 'detail'), function (data) {
console.log(data);
const formatter = new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
});
const formatter = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 });
$('#final_total_value').text(formatter.format(data.total));
// Assign data values to checkboxes
$('#final_total').data('value', data.total);
// Detail data
$('#final_user').text(data.user.name);
$('#final_expense_number').text(data.expense_number);
$('#final_tanggal').text(new Date(data.created_at).toLocaleDateString('id-ID', {
year: 'numeric',
month: 'long',
day: 'numeric'
}));
$('#final_user').text(data.user.name); $('#final_expense_number').text(data.expense_number);
$('#final_tanggal').text(new Date(data.created_at).toLocaleDateString('id-ID', { year: 'numeric', month: 'long', day: 'numeric' }));
$('#final_type').html(data.type.split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '));
$('#final_jarak').text(data.jarak > 0 ? data.jarak : '-');
$('#final_liter').text(data.liter > 0 ? data.liter : '-');
$('#final_tipe_bensin').text(data.tipe_bensin);
$('#final_nopol').text(data.nopol);
$('#final_tipe_bensin').text(data.tipe_bensin); $('#final_nopol').text(data.nopol);
$('#final_bukti_total').html(data.bukti_total ? '<a href="' + data.bukti_total + '" target="_blank">Download</a>' : '-');
// Hide spinner and show content
$('#finalLoadingSpinner').hide();
$('#finalLoadingSpinner').removeClass('d-flex');
$('#finalLoadingSpinner').addClass('d-none');
$('#finalModalContent').removeClass('d-none');
// Reset the total to zero
$('#finalLoadingSpinner').hide(); $('#finalModalContent').removeClass('d-none');
$('#total').val(0);
});
});
@@ -684,25 +623,14 @@
// Update total dynamically
$('.custom-control-input').on('change', function () {
let total = 0;
$('.custom-control-input:checked').each(function () {
total += parseFloat($(this).data('value') || 0);
});
const formatter = new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0,
});
$('.custom-control-input:checked').each(function () { total += parseFloat($(this).data('value') || 0); });
const formatter = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 });
$('#total').val(formatter.format(total));
});
// Uncheck all checkboxes when the modal is closed
$('#finalApproveModal').on('hidden.bs.modal', function () {
$('.custom-control-input').prop('checked', false); // Uncheck all checkboxes
$('#total').val('0'); // Reset total to 0
$('.custom-control-input').prop('checked', false); $('#total').val('0');
});
});
</script>
</script>
@endsection
+265 -132
View File
@@ -7,83 +7,69 @@
@section('admin-content')
<section class="content-header">
<div class="container-fluid">
<div class="row">
<div class="row mb-3">
<div class="col-sm-6">
<h1>{{ $pageInfo['title'] }}</h1>
<h1 class="fw-bold">{{ $pageInfo['title'] }}</h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-form"><a href="{{ route('admin.dashboard') }}">{{ __('Dashboard') }}</a> / </li>
<li class="breadcrumb-form active">&nbsp;{{ $pageInfo['title'] }}</li>
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}">{{ __('Dashboard') }}</a></li>
<li class="breadcrumb-item active">{{ $pageInfo['title'] }}</li>
</ol>
</div>
</div>
{{-- Summary Widgets --}}
<div class="row mt-4">
<!-- Total Expense -->
<div class="col-lg-3 col-6">
<div class="small-box bg-info">
<div class="small-box bg-info shadow-sm" style="border-radius: 15px;">
<div class="inner">
<h3>Rp {{ number_format($forms->sum('total'), 0, ',', '.') }}</h3>
<p>Total Expense</p>
<div class="fw-bold">Total Submission</div>
</div>
<div class="icon">
<i class="fas fa-wallet"></i>
<div class="icon"><i class="fas fa-wallet"></i></div>
</div>
</div>
</div>
<!-- Approved Expense -->
<div class="col-lg-3 col-6">
<div class="small-box bg-success">
<div class="small-box bg-success shadow-sm" style="border-radius: 15px;">
<div class="inner">
<h3>Rp {{ number_format($forms->whereIn('status', ['Approved', 'Closed'])->sum('approved_total'), 0, ',', '.') }}</h3>
<p>Approved Expenses</p>
<h3>Rp {{ number_format($forms->whereIn('status', ['Approved', 'Approved 1', 'Approved 2', 'Closed', 'Final Approved'])->sum('approved_total'), 0, ',', '.') }}</h3>
<div class="fw-bold">Total Approved</div>
</div>
<div class="icon">
<i class="fas fa-check-circle"></i>
<div class="icon"><i class="fas fa-check-circle"></i></div>
</div>
</div>
</div>
<!-- Pending Expense -->
<div class="col-lg-3 col-6">
<div class="small-box bg-warning">
<div class="small-box bg-warning shadow-sm" style="border-radius: 15px;">
<div class="inner">
<h3 class="text-white">Rp {{ number_format($forms->where('status', 'On Progress')->sum('total'), 0, ',', '.') }}</h3>
<p class="text-white">Pending Expenses</p>
<div class="text-white fw-bold">Total Pending</div>
</div>
<div class="icon">
<i class="fas fa-hourglass-half"></i>
<div class="icon"><i class="fas fa-hourglass-half"></i></div>
</div>
</div>
</div>
<!-- Budget Available -->
<div class="col-lg-3 col-6">
<div class="small-box bg-purple">
<div class="small-box bg-purple shadow-sm" style="border-radius: 15px; background-color: #6f42c1 !important;">
<div class="inner">
{{-- <h3 class="text-white">Rp {{ number_format($availableBudget, 0, ',', '.') }}</h3> --}}
{{-- check if availableBudget is not - --}}
<h3 class="text-white">{{ $availableBudget == '0' ? '-' : 'Rp' . number_format($availableBudget, 0, ',', '.') }}</h3>
<p class="text-white">Available Budget</p>
<h3 class="text-white">{{ !$availableBudget || $availableBudget == 0 ? '-' : 'Rp ' . number_format((float)$availableBudget, 0, ',', '.') }}</h3>
<div class="text-white fw-bold">Budget Available</div>
</div>
<div class="icon">
<i class="fas fa-money-bill"></i>
<div class="icon"><i class="fas fa-money-bill"></i></div>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="content">
<div class="container-fluid">
<div class="row">
{{-- Filter Section - Sistem Periode Bulan Akuntansi --}}
<div class="row mt-4">
<div class="col-md-12">
<div class="card">
<div class="card shadow-sm border-0" style="border-radius: 15px;">
<div class="card-body">
<form action="{{ route('reports.all') }}" method="GET">
<div class="d-flex">
<div class="row align-items-end">
<div class="col-md-3">
<div class="form-group mb-3">
<label for="region">Pilih Region</label>
<select name="region" id="region" class="form-control">
<label class="small fw-bold text-muted">Pilih Region</label>
<select name="region" id="region" class="form-control select2">
<option value="">Semua Region</option>
@foreach ($regions as $region)
<option value="{{ $region->code }}" {{ request()->region == $region->code ? 'selected' : '' }}>
@@ -95,66 +81,48 @@
</div>
<div class="col-md-3">
<div class="form-group mb-3">
<label for="cabang">Pilih Cabang</label>
<select name="cabang" id="cabang" class="form-control">
<label class="small fw-bold text-muted">Pilih Cabang</label>
<select name="cabang" id="cabang" class="form-control select2">
<option value="">Semua Cabang</option>
<!-- Cabang options will be dynamically populated -->
</select>
</div>
</div>
<div class="col-md-2">
<div class="form-group mb-3">
<label for="month">Pilih Periode Bulan</label>
<label class="small fw-bold text-muted">Periode Bulan</label>
<select name="month" id="month" class="form-control">
<option value="">Pilih Bulan</option>
<option value="january" {{ request()->month == 'january' ? 'selected' : '' }}>Januari</option>
<option value="february" {{ request()->month == 'february' ? 'selected' : '' }}>Februari</option>
<option value="march" {{ request()->month == 'march' ? 'selected' : '' }}>Maret</option>
<option value="april" {{ request()->month == 'april' ? 'selected' : '' }}>April</option>
<option value="may" {{ request()->month == 'may' ? 'selected' : '' }}>Mei</option>
<option value="june" {{ request()->month == 'june' ? 'selected' : '' }}>Juni</option>
<option value="july" {{ request()->month == 'july' ? 'selected' : '' }}>Juli</option>
<option value="august" {{ request()->month == 'august' ? 'selected' : '' }}>Agustus</option>
<option value="september" {{ request()->month == 'september' ? 'selected' : '' }}>September</option>
<option value="october" {{ request()->month == 'october' ? 'selected' : '' }}>Oktober</option>
<option value="november" {{ request()->month == 'november' ? 'selected' : '' }}>November</option>
<option value="december" {{ request()->month == 'december' ? 'selected' : '' }}>Desember</option>
@foreach(['january','february','march','april','may','june','july','august','september','october','november','december'] as $m)
<option value="{{ $m }}" {{ (request()->month ?? strtolower(date('F'))) == $m ? 'selected' : '' }}>{{ ucfirst($m) }}</option>
@endforeach
</select>
</div>
</div>
<div class="col-md-2">
<div class="form-group mb-3">
<label for="year">Pilih Periode Tahun</label>
<label class="small fw-bold text-muted">Tahun</label>
<select name="year" id="year" class="form-control">
<option value="">Pilih Tahun</option>
@for ($i = 2024; $i <= date('Y'); $i++)
<option value="{{ $i }}" {{ request()->year == $i ? 'selected' : '' }}>{{ $i }}</option>
<option value="{{ $i }}" {{ (request()->year ?? date('Y')) == $i ? 'selected' : '' }}>{{ $i }}</option>
@endfor
</select>
</div>
</div>
<div class="col-md-2">
<div class="form-group mb-3">
<label for="status">Status</label>
<label class="small fw-bold text-muted">Status</label>
<select name="status" id="status" class="form-control">
<option value="">Semua Status</option>
<option value="On Progress" {{ request()->status == 'On Progress' ? 'selected' : '' }}>On Progress</option>
<option value="Approved 1" {{ request()->status == 'Approved 1' ? 'selected' : '' }}>Approved 1</option>
<option value="Approved 2" {{ request()->status == 'Approved 2' ? 'selected' : '' }}>Approved 2</option>
<option value="Closed" {{ request()->status == 'Closed' ? 'selected' : '' }}>Closed</option>
<option value="Rejected" {{ request()->status == 'Rejected' ? 'selected' : '' }}>Rejected</option>
@foreach(['On Progress', 'Approved 1', 'Approved 2', 'Closed', 'Rejected'] as $st)
<option value="{{ $st }}" {{ request()->status == $st ? 'selected' : '' }}>{{ $st }}</option>
@endforeach
</select>
</div>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<button type="submit" class="btn btn-primary">Filter</button>
<a href="{{ route('reports.all') }}" class="btn btn-secondary">Reset</a>
<div class="col-md-12 text-right mt-2">
<button type="submit" class="btn btn-primary px-4 shadow-sm"><i class="fas fa-filter mr-1"></i> Filter</button>
<a href="{{ route('reports.all') }}" class="btn btn-outline-secondary px-4 shadow-sm">Reset</a>
</div>
</div>
</form>
@@ -165,55 +133,141 @@
</div>
</section>
@php
use App\Helpers\NextCloudHelper;
@endphp
@php use App\Helpers\NextCloudHelper; @endphp
<section class="content">
<div class="container-fluid">
<!-- All Forms Table -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card shadow-sm border-0" style="border-radius: 15px;">
<div class="card-body">
<h4 class="header-title">All Forms</h4>
<div class="data-tables">
@include('backend.layouts.partials.messages')
<h4 class="header-title mb-4">Database Konsolidasi (All Forms)</h4>
<div class="table-responsive">
<table id="dataTable-table">
@include('backend.layouts.partials.messages')
<table id="dataTable-table" class="table table-hover w-100">
<thead class="bg-light text-capitalize">
<tr>
<th>#</th>
<th>No Expense</th>
<th>Tipe</th>
<th>Nama</th>
<th>Region</th>
<th>Cabang</th>
<th>Total</th>
<th>Total Approved</th>
<th>Account Number</th>
<th>Kategori</th>
<th>Nama Pengaju</th>
<th>Region / Cabang</th>
<th>Tgl. Dibuat</th>
<th>Nominal (Appr)</th>
<th class="text-center">Bukti</th>
<th>Status</th>
<th>Detail Rejeksi</th>
</tr>
</thead>
<tbody>
@foreach ($forms as $item)
<tr>
<td>{{ $loop->iteration }}</td>
<td>{{ $item->expense_number }}</td>
<td>{{ $item->type }}</td>
<td>{{ $item->user->name }}</td>
<td>{{ $item->user->region->cabang->region->name }}</td>
<td>{{ $item->user->cabang->cabang->name }}</td>
<td>{{ number_format($item->total, 0, ',', '.') }}</td>
<td>{{ number_format($item->approved_total, 0, ',', '.') }}</td>
<td>{{ $item->account_number }}</td>
<td class="fw-bold text-primary">{{ $item->expense_number }}</td>
<td>
@if ($item->status == 'On Progress')
<span class="badge badge-warning text-white">{{ $item->status }}</span>
@elseif ($item->status == 'Approved 1' || $item->status == 'Approved 2' || $item->status == 'Final Approved')
<span class="badge badge-success text-white">{{ $item->status }}</span>
<span class="badge badge-light border text-capitalize px-2 py-1">
{{ str_replace(['form_', '_'], ['', ' '], (string)$item->type) }}
</span>
</td>
<td>{{ $item->user->name ?? '-' }}</td>
<td>
<span class="d-block small fw-bold text-dark">{{ $item->user->region->cabang->region->name ?? '-' }}</span>
<span class="text-muted small">{{ $item->user->cabang->cabang->name ?? '-' }}</span>
</td>
<td>
@php $createdAt = \Carbon\Carbon::parse($item->created_at); @endphp
<div class="fw-bold small">{{ $createdAt->locale('id')->translatedFormat('d F Y') }}</div>
<small class="text-muted">{{ $createdAt->format('H:i') }} WIB</small>
</td>
<td class="text-success fw-bold">
{{ $item->approved_total ? 'Rp ' . number_format($item->approved_total, 0, ',', '.') : '-' }}
</td>
<td class="text-center">
@php
$fileData = [];
$imgExts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'];
if (!empty($item->main_proof)) {
$ext = strtolower(pathinfo($item->main_proof, PATHINFO_EXTENSION));
$fileData[] = [
'url' => NextCloudHelper::getFileUrl($item->main_proof),
'is_img' => in_array($ext, $imgExts)
];
}
if(isset($item->extra_attachments)) {
foreach ($item->extra_attachments as $att) {
$ext = strtolower(pathinfo($att->file_path, PATHINFO_EXTENSION));
$fileData[] = [
'url' => NextCloudHelper::getFileUrl($att->file_path),
'is_img' => in_array($ext, $imgExts)
];
}
}
$uniqueFiles = collect($fileData)->unique('url')->values()->all();
@endphp
@if (count($uniqueFiles) > 0)
<button type="button" class="btn btn-sm btn-info rounded-pill px-3 shadow-sm btn-preview-bukti"
data-expense="{{ $item->expense_number }}"
data-files='@json($uniqueFiles)'>
<i class="fas fa-eye"></i> {{ count($uniqueFiles) }}
</button>
@else
<span class="badge badge-danger text-white">{{ $item->status }}</span>
<span class="text-muted small">-</span>
@endif
</td>
<td>
@php
$badgeClass = match($item->status) {
'On Progress' => 'badge-warning text-white',
'Approved 1', 'Approved 2', 'Closed', 'Final Approved' => 'badge-success text-white',
'Rejected' => 'badge-danger text-white',
default => 'badge-secondary'
};
@endphp
<span class="badge {{ $badgeClass }} px-3 py-2">{{ $item->status }}</span>
{{-- Tgl Final Approved --}}
@if(isset($item->report_date) && in_array($item->status, ['Approved 2', 'Closed', 'Final Approved']))
<div class="mt-2" style="line-height: 1.2;">
<small class="text-success fw-bold d-block" style="font-size: 10px;">
<i class="fas fa-check-double mr-1"></i> Final Approved:
</small>
<small class="text-muted d-block" style="font-size: 10px;">
{{ \Carbon\Carbon::parse($item->report_date)->translatedFormat('d M Y') }}
</small>
</div>
@endif
{{-- Penanda Tgl Waktu Rejeksi --}}
@if($item->status == 'Rejected' && isset($item->rejected_at))
<div class="mt-2" style="line-height: 1.2;">
<small class="text-danger fw-bold d-block" style="font-size: 10px;">
<i class="fas fa-times-circle mr-1"></i> Rejected At:
</small>
<small class="text-muted d-block" style="font-size: 10px;">
{{ \Carbon\Carbon::parse($item->rejected_at)->translatedFormat('d M Y H:i') }} WIB
</small>
</div>
@endif
</td>
{{-- Kolom Detail Rejeksi Sinkron --}}
<td>
@if($item->status == 'Rejected')
<div class="mt-1 p-2 bg-light border border-danger rounded" style="line-height: 1.3; min-width: 160px; border-style: dashed !important;">
<div class="mb-1">
<small class="fw-bold text-dark" style="font-size: 10px;">Oleh:</small>
<small class="text-primary font-weight-bold" style="font-size: 10px;">{{ $item->rejector->name ?? $item->rejected_by ?? 'System' }}</small>
</div>
<div>
<small class="fw-bold text-dark" style="font-size: 10px;">Alasan:</small>
<small class="text-danger font-italic d-block mt-1" style="font-size: 10px; line-height: 1.1;">"{{ $item->remarks ?? '-' }}"</small>
</div>
</div>
@else
<span class="text-muted small">-</span>
@endif
</td>
</tr>
@@ -226,67 +280,146 @@
</div>
</div>
</div>
</div>
</section>
{{-- MODAL PREVIEW --}}
<div class="modal fade" id="modalPreviewBukti" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content" style="border-radius: 15px; border: none; overflow: hidden;">
<div class="modal-header bg-primary text-white border-0">
<h5 class="modal-title fw-bold"><i class="fas fa-images mr-2"></i>Lampiran Bukti <span id="textExpenseNum"></span></h5>
<button type="button" class="close text-white" data-dismiss="modal">&times;</button>
</div>
<div class="modal-body bg-light" id="containerPreview" style="max-height: 70vh; overflow-y: auto; padding: 25px;"></div>
<div class="modal-footer bg-light border-0">
<button type="button" class="btn btn-secondary rounded-pill px-4" data-dismiss="modal">Tutup</button>
</div>
</div>
</div>
</div>
{{-- MODAL ZOOM --}}
<div class="modal fade" id="modalImageFull" tabindex="-2" role="dialog" aria-hidden="true" style="background: rgba(0,0,0,0.85); z-index: 1060;">
<div class="modal-dialog modal-dialog-centered modal-xl">
<div class="modal-content" style="background: transparent; border: none;">
<div class="modal-header border-0 p-0">
<button type="button" class="close text-white" data-dismiss="modal" style="font-size: 2.5rem; position: absolute; right: 15px; top: 10px; z-index: 999;">&times;</button>
</div>
<div class="modal-body p-0 text-center">
<img src="" id="imgFullDisplay" style="max-width: 100%; max-height: 90vh; border-radius: 8px;">
</div>
</div>
</div>
</div>
@endsection
@section('scripts')
<script>
$(document).ready(function() {
if ($('#dataTable-table').length) {
$('#dataTable-table').DataTable({
responsive: false,
pagging: true,
dom: 'Blfrtip',
lengthMenu: [ [10, 25, 50, -1], [10, 25, 50, "All"] ], // Control the entries dropdown
deferRender: true, // Optimasi performa loading render DOM fisik tabel
lengthMenu: [ [10, 25, 50, -1], [10, 25, 50, "All"] ],
buttons: [
{
extend: 'excel',
text: 'Excel'
},
{
extend: 'pdf',
text: 'PDF',
orientation: 'landscape',
pageSize: 'A4',
extend: 'excelHtml5',
text: '<i class="fas fa-file-excel mr-1"></i> Excel',
className: 'btn-success',
exportOptions: {
columns: ':visible'
columns: [0, 1, 2, 3, 4, 5, 6, 8, 9],
format: {
body: function (data, row, column, node) {
let text = node.textContent || node.innerText || "";
text = text.replace(/\s+/g, ' ').trim();
if (column === 6) {
return text.replace(/Rp\s?|[.]/g, '');
}
return text;
}
}
}
},
{
extend: 'print',
text: 'Print',
extend: 'pdfHtml5',
text: '<i class="fas fa-file-pdf mr-1"></i> PDF',
className: 'btn-danger',
orientation: 'landscape',
exportOptions: {
columns: ':visible'
pageSize: 'A4',
exportOptions: { columns: [0, 1, 2, 3, 4, 5, 6, 8, 9] },
customize: function(doc) {
doc.defaultStyle.fontSize = 7;
doc.styles.tableHeader.fontSize = 8;
}
},
'colvis'
]
});
}
</script>
<script>
document.getElementById('region').addEventListener('change', function () {
const regionCode = this.value;
const cabangSelect = document.getElementById('cabang');
$(document).on('click', '.btn-preview-bukti', function(e) {
e.preventDefault();
const expenseNum = $(this).data('expense');
const files = $(this).data('files');
let html = '<div class="row">';
// Clear existing options
cabangSelect.innerHTML = '<option value="">Semua Cabang</option>';
$('#textExpenseNum').text('#' + expenseNum);
$('#containerPreview').html('<div class="text-center py-5"><i class="fas fa-spinner fa-spin fa-3x text-primary"></i></div>');
if(files && files.length > 0) {
files.forEach((file) => {
html += `
<div class="col-md-6 mb-4">
<div class="card h-100 border-0 shadow-sm rounded-lg overflow-hidden">
<div class="card-body p-2 text-center bg-white">
<div class="mb-3" style="height: 220px; display: flex; align-items: center; justify-content: center; background: #f8f9fa; border-radius: 8px; overflow: hidden; border: 1px solid #eee;">
${file.is_img
? `<img src="${file.url}" class="img-fluid" style="max-height: 100%; object-fit: contain; cursor: zoom-in;" onclick="openFullImage('${file.url}')">`
: `<div class="text-center"><i class="fas fa-file-pdf fa-4x text-danger"></i><p class="small text-muted mt-2 fw-bold">Dokumen Digital</p></div>`
}
</div>
<div class="btn-group w-100">
${file.is_img
? `<button type="button" onclick="openFullImage('${file.url}')" class="btn btn-outline-primary btn-sm px-3"><i class="fas fa-eye"></i> Preview</button>`
: `<a href="${file.url}" target="_blank" class="btn btn-outline-primary btn-sm px-3"><i class="fas fa-external-link-alt"></i> Open PDF</a>`
}
<a href="${file.url}" download class="btn btn-primary btn-sm px-3"><i class="fas fa-download"></i> Download</a>
</div>
</div>
</div>
</div>`;
});
}
$('#containerPreview').html(html + '</div>');
$('#modalPreviewBukti').modal('show');
});
function loadCabang(regionCode, selectedCabang = null) {
const cabangSelect = $('#cabang');
if (regionCode) {
fetch(`/api/cabang-by-region/${regionCode}`)
.then(response => response.json())
.then(data => {
cabangSelect.html('<option value="">Semua Cabang</option>');
data.forEach(cabang => {
const option = document.createElement('option');
option.value = cabang.code;
option.textContent = cabang.name;
cabangSelect.appendChild(option);
const isSelected = (selectedCabang && cabang.code === selectedCabang) ? 'selected' : '';
cabangSelect.append(`<option value="${cabang.code}" ${isSelected}>${cabang.name}</option>`);
});
});
})
.catch(error => console.error('Error fetching cabang data:', error));
}
}
$('#region').on('change', function() { loadCabang(this.value); });
const currentRegion = '{{ request()->region }}';
const currentCabang = '{{ request()->cabang }}';
if (currentRegion) { loadCabang(currentRegion, currentCabang); }
});
function openFullImage(url) {
$('#imgFullDisplay').attr('src', url);
$('#modalImageFull').modal('show');
}
</script>
@endsection
@@ -9,25 +9,27 @@
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1>{{ $pageInfo['title'] }}</h1>
<h1 class="fw-bold">{{ $pageInfo['title'] }}</h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-form"><a href="{{ route('admin.dashboard') }}">{{ __('Dashboard') }}</a> / </li>
<li class="breadcrumb-form active">&nbsp;{{ $pageInfo['title'] }}</li>
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}">{{ __('Dashboard') }}</a></li>
<li class="breadcrumb-item active">{{ $pageInfo['title'] }}</li>
</ol>
</div>
</div>
{{-- Filter Section - Sistem Periode Bulan Akuntansi (11 s/d 13 Bulan Berikut) --}}
<div class="row mt-4">
<div class="col-md-12">
<div class="card">
<div class="card shadow-sm border-0" style="border-radius: 15px;">
<div class="card-body">
<form action="{{ route('reports.entertainment') }}" method="GET">
<div class="d-flex">
<div class="row align-items-end">
<div class="col-md-3">
<div class="form-group mb-3">
<label for="region">Pilih Region</label>
<select name="region" id="region" class="form-control">
<label class="small fw-bold text-muted">Pilih Region</label>
<select name="region" id="region" class="form-control select2">
<option value="">Semua Region</option>
@foreach ($regions as $region)
<option value="{{ $region->code }}" {{ request()->region == $region->code ? 'selected' : '' }}>
@@ -39,44 +41,48 @@
</div>
<div class="col-md-3">
<div class="form-group mb-3">
<label for="cabang">Pilih Cabang</label>
<select name="cabang" id="cabang" class="form-control">
<label class="small fw-bold text-muted">Pilih Cabang</label>
<select name="cabang" id="cabang" class="form-control select2">
<option value="">Semua Cabang</option>
<!-- Cabang options will be dynamically populated -->
</select>
</div>
</div>
<div class="col-md-2">
<div class="form-group mb-3">
<label for="status">Start Date</label>
<input type="date" name="start_date" class="form-control" value="{{ request()->start_date }}">
<label class="small fw-bold text-muted">Periode Bulan</label>
<select name="month" id="month" class="form-control">
<option value="">Pilih Bulan</option>
@foreach(['january','february','march','april','may','june','july','august','september','october','november','december'] as $m)
<option value="{{ $m }}" {{ (request()->month ?? strtolower(date('F'))) == $m ? 'selected' : '' }}>{{ ucfirst($m) }}</option>
@endforeach
</select>
</div>
</div>
<div class="col-md-2">
<div class="form-group mb-3">
<label for="status">End Date</label>
<input type="date" name="end_date" class="form-control" value="{{ request()->end_date }}">
<label class="small fw-bold text-muted">Tahun</label>
<select name="year" id="year" class="form-control">
<option value="">Pilih Tahun</option>
@for ($i = 2024; $i <= date('Y'); $i++)
<option value="{{ $i }}" {{ (request()->year ?? date('Y')) == $i ? 'selected' : '' }}>{{ $i }}</option>
@endfor
</select>
</div>
</div>
<div class="col-md-2">
<div class="form-group mb-3">
<label for="status">Status</label>
<label class="small fw-bold text-muted">Status</label>
<select name="status" id="status" class="form-control">
<option value="">Semua Status</option>
<option value="On Progress" {{ request()->status == 'On Progress' ? 'selected' : '' }}>On Progress</option>
<option value="Approved 1" {{ request()->status == 'Approved 1' ? 'selected' : '' }}>Approved 1</option>
<option value="Approved 2" {{ request()->status == 'Approved 2' ? 'selected' : '' }}>Approved 2</option>
<option value="Closed" {{ request()->status == 'Closed' ? 'selected' : '' }}>Closed</option>
<option value="Rejected" {{ request()->status == 'Rejected' ? 'selected' : '' }}>Rejected</option>
@foreach(['On Progress', 'Approved 1', 'Approved 2', 'Closed', 'Rejected'] as $st)
<option value="{{ $st }}" {{ request()->status == $st ? 'selected' : '' }}>{{ $st }}</option>
@endforeach
</select>
</div>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<button type="submit" class="btn btn-primary">Filter</button>
<a href="{{ route('reports.entertainment') }}" class="btn btn-secondary">Reset</a>
<div class="col-md-12 text-right mt-2">
<button type="submit" class="btn btn-primary px-4 shadow-sm"><i class="fas fa-filter mr-1"></i> Filter</button>
<a href="{{ route('reports.entertainment') }}" class="btn btn-outline-secondary px-4 shadow-sm">Reset</a>
</div>
</div>
</form>
@@ -87,37 +93,32 @@
</div>
</section>
@php
use App\Helpers\NextCloudHelper;
@endphp
@php use App\Helpers\NextCloudHelper; @endphp
<section class="content">
<div class="container-fluid">
<!-- Form Entertainment Presentation Table -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card shadow-sm border-0" style="border-radius: 15px;">
<div class="card-body">
<h4 class="header-title">Form Entertainment & Presentation</h4>
<div class="data-tables">
@include('backend.layouts.partials.messages')
<h4 class="header-title mb-4">List Report Entertainment & Presentation</h4>
<div class="table-responsive">
<table id="dataTable-table">
<table id="dataTable-table" class="table table-hover w-100">
<thead class="bg-light text-capitalize">
<tr>
<th>#</th>
<th>No Expense</th>
<th>User</th>
<th>Region</th>
<th>Cabang</th>
<th>Nama Penerima</th>
<th>Tanggal</th>
<th>Jenis</th>
<th>Keterangan</th>
<th>Total</th>
<th>Account No.</th>
<th>Pengaju / Penerima</th>
<th>NIK/NPWP & Alamat</th>
<th>Nama Perusahaan</th>
<th>Jabatan</th>
<th>Jenis Usaha</th>
<th>Region / Cabang</th>
<th>Tgl. Digunakan & Dibuat</th>
<th>Jenis & Keterangan</th>
<th>Total Approved</th>
<th>Bukti</th>
<th>Account Number</th>
<th class="text-center">Bukti</th>
<th>Status</th>
</tr>
</thead>
@@ -125,33 +126,131 @@
@foreach ($forms as $item)
<tr>
<td>{{ $loop->iteration }}</td>
<td>{{ $item->expense_number }}</td>
<td>{{ $item->user->name }}</td>
<td>{{ $item->user->region->cabang->region->name }}</td>
<td>{{ $item->user->cabang->cabang->name }}</td>
<td>{{ $item->name }}</td>
<td>{{ \Carbon\Carbon::parse($item->tanggal)->locale('id')->isoFormat('D MMMM Y') }}</td>
<td>{{ $item->jenis }}</td>
<td>{{ $item->keterangan }}</td>
<td>{{ number_format($item->total, 0, ',', '.') }}</td>
<td>{{ number_format($item->approved_total, 0, ',', '.') }}</td>
<td class="fw-bold text-primary">{{ $item->expense_number }}</td>
<td class="fw-bold text-center">{{ $item->account_number ?? ($item->kategori->account_number ?? '-') }}</td>
<td>
@if ($item->bukti_total)
<a href="{{ NextCloudHelper::getFileUrl($item->bukti_total) }}" target="_blank">
Download
</a>
<div class="fw-bold">{{ $item->user->name ?? '-' }}</div>
<small class="text-muted italic">Penerima: {{ $item->name }}</small>
</td>
<td>
<div class="small fw-bold text-dark">{{ $item->nik_or_npwp ?? '-' }}</div>
<div class="small text-muted text-truncate" style="max-width: 150px;" title="{{ $item->alamat }}">
{{ $item->alamat ?? '-' }}
</div>
</td>
<td class="small">{{ $item->nama_perusahaan ?? '-' }}</td>
<td class="small">{{ $item->jabatan ?? '-' }}</td>
<td class="small">{{ $item->jenis_usaha ?? '-' }}</td>
<td>
<span class="d-block small fw-bold">{{ $item->user->region->cabang->region->name ?? '-' }}</span>
<span class="text-muted small">{{ $item->user->cabang->cabang->name ?? '-' }}</span>
</td>
<td>
<div class="small fw-bold text-dark" title="Tgl Penggunaan">
<i class="far fa-calendar-check mr-1 text-primary"></i>{{ \Carbon\Carbon::parse($item->tanggal)->format('d M Y') }}
</div>
<div class="small text-muted mt-1" title="Tgl Dibuat">
<i class="fas fa-clock mr-1"></i>{{ $item->created_at->format('d M Y') }} <span class="ml-1">{{ $item->created_at->format('H:i') }}</span>
</div>
</td>
<td>
<span class="badge badge-light border mb-1">{{ $item->jenis }}</span>
<div class="small text-muted text-truncate" style="max-width: 120px;" title="{{ $item->keterangan }}">{{ $item->keterangan }}</div>
</td>
<td class="fw-bold text-success">
{{ $item->approved_total ? number_format($item->approved_total, 0, ',', '.') : '-' }}
</td>
<td class="text-center">
@php
$fileData = [];
$imgExts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'];
if (!empty($item->bukti_total)) {
$extension = strtolower(pathinfo($item->bukti_total, PATHINFO_EXTENSION));
$fileData[] = [
'url' => NextCloudHelper::getFileUrl($item->bukti_total),
'is_img' => in_array($extension, $imgExts)
];
}
if ($item->attachments) {
foreach ($item->attachments as $att) {
if ($att->file_path) {
$extension = strtolower(pathinfo($att->file_path, PATHINFO_EXTENSION));
$fileData[] = [
'url' => NextCloudHelper::getFileUrl($att->file_path),
'is_img' => in_array($extension, $imgExts)
];
}
}
}
$uniqueFiles = collect($fileData)->unique('url')->values()->all();
@endphp
@if (count($uniqueFiles) > 0)
<button type="button" class="btn btn-sm btn-info rounded-pill px-3 shadow-sm btn-preview-bukti"
data-expense="{{ $item->expense_number }}"
data-files='@json($uniqueFiles)'>
<i class="fas fa-eye"></i> {{ count($uniqueFiles) }}
</button>
@else
-
<span class="text-muted small">-</span>
@endif
</td>
<td>{{ $item->account_number }}</td>
<td>
@if ($item->status == 'On Progress')
<span class="badge badge-warning text-white">{{ $item->status }}</span>
@elseif ($item->status == 'Approved 1' || $item->status == 'Approved 2' || $item->status == 'Final Approved')
<span class="badge badge-success text-white">{{ $item->status }}</span>
@else
<span class="badge badge-danger text-white">{{ $item->status }}</span>
@php
$badgeClass = match($item->status) {
'On Progress' => 'badge-warning text-white',
'Approved 1', 'Approved 2', 'Closed' => 'badge-success text-white',
'Rejected' => 'badge-danger text-white',
default => 'badge-secondary'
};
@endphp
<span class="badge {{ $badgeClass }} px-3 py-2">{{ $item->status }}</span>
{{-- BLOK AUDIT TRAIL REJECTED --}}
@if($item->status === 'Rejected')
<div class="mt-2 p-2 bg-light border border-danger rounded" style="line-height: 1.3; min-width: 160px; border-style: dashed !important;">
<small class="text-danger fw-bold d-block mb-1" style="font-size: 10px;">
<i class="fas fa-times-circle mr-1"></i> Detail Penolakan:
</small>
<small class="text-dark d-block" style="font-size: 10px;">
<strong>Oleh:</strong> {{ $item->rejector->name ?? $item->rejected_by ?? 'System' }}
</small>
@if($item->rejected_at)
<small class="text-muted d-block" style="font-size: 10px;">
<strong>Waktu:</strong> {{ \Carbon\Carbon::parse($item->rejected_at)->translatedFormat('d M Y H:i') }} WIB
</small>
@elseif($item->updated_at && $item->remarks)
<small class="text-muted d-block" style="font-size: 10px;">
<strong>Waktu:</strong> {{ \Carbon\Carbon::parse($item->updated_at)->translatedFormat('d M Y H:i') }} WIB
</small>
@endif
@if($item->remarks)
<small class="text-danger d-block mt-1 italic" style="font-size: 10px; line-height: 1.1;">
<strong>Ket:</strong> "{{ $item->remarks }}"
</small>
@endif
</div>
@endif
{{-- BLOK EXISTING FINAL APPROVED / CLOSED --}}
@if($item->final_approved_at && in_array($item->status, ['Approved 2', 'Closed']))
<div class="mt-2" style="line-height: 1.2;">
<small class="text-success fw-bold d-block" style="font-size: 10px;">
<i class="fas fa-check-double mr-1"></i> Final Approved:
</small>
<small class="text-muted d-block" style="font-size: 10px;">
{{ \Carbon\Carbon::parse($item->final_approved_at)->translatedFormat('d M Y') }}
<span class="ml-1">{{ \Carbon\Carbon::parse($item->final_approved_at)->format('H:i') }} WIB</span>
</small>
</div>
@endif
</td>
</tr>
@@ -164,66 +263,186 @@
</div>
</div>
</div>
</div>
</section>
{{-- MODAL 1: DAFTAR LAMPIRAN --}}
<div class="modal fade" id="modalPreviewBukti" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content" style="border-radius: 15px; border: none; overflow: hidden;">
<div class="modal-header bg-primary text-white border-0">
<h5 class="modal-title fw-bold"><i class="fas fa-images mr-2"></i>Lampiran Bukti <span id="textExpenseNum"></span></h5>
<button type="button" class="close text-white" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body bg-light" id="containerPreview" style="max-height: 70vh; overflow-y: auto; padding: 25px;"></div>
<div class="modal-footer bg-light border-0">
<button type="button" class="btn btn-secondary rounded-pill px-4" data-dismiss="modal">Tutup</button>
</div>
</div>
</div>
</div>
{{-- MODAL 2: ZOOM GAMBAR FULL --}}
<div class="modal fade" id="modalImageFull" tabindex="-2" role="dialog" aria-hidden="true" style="background: rgba(0,0,0,0.85); z-index: 1060;">
<div class="modal-dialog modal-dialog-centered modal-xl">
<div class="modal-content" style="background: transparent; border: none;">
<div class="modal-header border-0 p-0">
<button type="button" class="close text-white" data-dismiss="modal" style="font-size: 2.5rem; position: absolute; right: 15px; top: 10px; z-index: 999; opacity: 1;">&times;</button>
</div>
<div class="modal-body p-0 text-center">
<img src="" id="imgFullDisplay" style="max-width: 100%; max-height: 90vh; border-radius: 8px; box-shadow: 0 0 30px rgba(0,0,0,0.5);">
</div>
</div>
</div>
</div>
@endsection
@section('scripts')
<script>
$(document).ready(function() {
if ($('#dataTable-table').length) {
$('#dataTable-table').DataTable({
responsive: false,
dom: 'Blfrtip',
lengthMenu: [ [10, 50, 100, -1], [10, 50, 100, "All"] ], // Control the entries dropdown
deferRender: true, // Membuat pemuatan ribuan baris menjadi instan & ringan
lengthMenu: [ [10, 50, 100, -1], [10, 50, 100, "All"] ],
buttons: [
{
extend: 'excel',
text: 'Excel'
},
{
extend: 'pdf',
text: 'PDF',
orientation: 'landscape',
pageSize: 'A4',
extend: 'excelHtml5',
text: '<i class="fas fa-file-excel mr-1"></i> Excel',
className: 'btn-success',
exportOptions: {
columns: ':visible'
columns: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13],
format: {
body: function (data, row, column, node) {
let text = node.textContent || node.innerText || "";
text = text.replace(/\s+/g, ' ').trim();
if (column === 11) {
return text.replace(/[.]/g, '');
}
return text;
}
}
}
},
{
extend: 'print',
text: 'Print',
extend: 'pdfHtml5',
text: '<i class="fas fa-file-pdf mr-1"></i> PDF',
className: 'btn-danger',
orientation: 'landscape',
pageSize: 'A4',
exportOptions: {
columns: ':visible'
columns: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13],
format: {
body: function (data, row, column, node) {
let text = node.textContent || node.innerText || "";
text = text.replace(/\s+/g, ' ').trim();
if (column === 11) {
return text.replace(/[.]/g, '');
}
return text;
}
}
},
customize: function(doc) {
doc.defaultStyle.fontSize = 6;
doc.styles.tableHeader.fontSize = 7;
doc.styles.tableHeader.alignment = 'center';
doc.pageMargins = [10, 15, 10, 15];
var tableObj;
for (var i = 0; i < doc.content.length; i++) {
if (doc.content[i].table !== undefined) {
tableObj = doc.content[i];
break;
}
}
if(tableObj) {
tableObj.table.widths = [
'2%', '11%', '6%', '8%', '9%', '8%', '7%', '7%', '8%', '9%', '11%', '8%', '6%'
];
tableObj.layout = {
hLineWidth: function(i, node) { return 0.5; },
vLineWidth: function(i, node) { return 0.5; },
hLineColor: function(i, node) { return '#aaa'; },
vLineColor: function(i, node) { return '#aaa'; },
paddingLeft: function(i, node) { return 3; },
paddingRight: function(i, node) { return 3; },
paddingTop: function(i, node) { return 3; },
paddingBottom: function(i, node) { return 3; }
};
}
}
},
'colvis'
]
});
}
</script>
<script>
document.getElementById('region').addEventListener('change', function () {
const regionCode = this.value;
const cabangSelect = document.getElementById('cabang');
$(document).on('click', '.btn-preview-bukti', function(e) {
e.preventDefault();
const expenseNum = $(this).data('expense');
const files = $(this).data('files');
let html = '<div class="row">';
// Clear existing options
cabangSelect.innerHTML = '<option value="">Semua Cabang</option>';
$('#textExpenseNum').text('#' + expenseNum);
$('#containerPreview').html('<div class="text-center py-5"><i class="fas fa-spinner fa-spin fa-3x text-primary"></i></div>');
if(files && files.length > 0) {
files.forEach((file) => {
html += `
<div class="col-md-6 mb-4">
<div class="card h-100 border-0 shadow-sm rounded-lg overflow-hidden">
<div class="card-body p-2 text-center bg-white">
<div class="thumbnail-container mb-3" style="height: 220px; display: flex; align-items: center; justify-content: center; background: #f8f9fa; border-radius: 8px; overflow: hidden; border: 1px solid #eee;">
${file.is_img
? `<img src="${file.url}" class="img-fluid" style="max-height: 100%; object-fit: contain; cursor: zoom-in;" onclick="openFullImage('${file.url}')">`
: `<div class="text-center"><i class="fas fa-file-pdf fa-4x text-danger"></i><p class="small text-muted mt-2 fw-bold">Dokumen Digital</p></div>`
}
</div>
<div class="btn-group w-100">
${file.is_img
? `<button type="button" onclick="openFullImage('${file.url}')" class="btn btn-outline-primary btn-sm px-3"><i class="fas fa-eye"></i> Preview Full</button>`
: `<a href="${file.url}" target="_blank" class="btn btn-outline-primary btn-sm px-3"><i class="fas fa-external-link-alt"></i> Buka PDF</a>`
}
<a href="${file.url}" download class="btn btn-primary btn-sm px-3"><i class="fas fa-download"></i> Download</a>
</div>
</div>
</div>
</div>`;
});
}
$('#containerPreview').html(html + '</div>');
$('#modalPreviewBukti').modal('show');
});
function loadCabang(regionCode, selectedCabang = null) {
const cabangSelect = $('#cabang');
if (regionCode) {
fetch(`/api/cabang-by-region/${regionCode}`)
.then(response => response.json())
.then(data => {
cabangSelect.html('<option value="">Semua Cabang</option>');
data.forEach(cabang => {
const option = document.createElement('option');
option.value = cabang.code;
option.textContent = cabang.name;
cabangSelect.appendChild(option);
const isSelected = (selectedCabang && cabang.code === selectedCabang) ? 'selected' : '';
cabangSelect.append(`<option value="${cabang.code}" ${isSelected}>${cabang.name}</option>`);
});
});
})
.catch(error => console.error('Error fetching cabang data:', error));
}
}
$('#region').on('change', function() { loadCabang(this.value); });
const currentRegion = '{{ request()->region }}';
const currentCabang = '{{ request()->cabang }}';
if (currentRegion) { loadCabang(currentRegion, currentCabang); }
});
function openFullImage(url) {
$('#imgFullDisplay').attr('src', url);
$('#modalImageFull').modal('show');
}
</script>
@endsection

Some files were not shown because too many files have changed in this diff Show More