Skip to content

Commit 6fd8dcf

Browse files
committed
Implement Phase 1 AI features and fix SuperAdmin syntax error
1 parent c900451 commit 6fd8dcf

7 files changed

Lines changed: 523 additions & 3 deletions

File tree

app-core/app/Config/Routes.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,14 @@
8282
$routes->post('dashboard/keuangan/category/save', 'Admin::saveFinanceCategory');
8383
$routes->post('dashboard/keuangan/category/delete', 'Admin::deleteFinanceCategory');
8484

85+
// Finance AI Features
86+
$routes->get('dashboard/keuangan/import-csv', 'FinanceAI::importCSV');
87+
$routes->post('dashboard/keuangan/import-csv/process', 'FinanceAI::processCSV');
88+
$routes->get('dashboard/keuangan/review-csv', 'FinanceAI::reviewCSV');
89+
$routes->post('dashboard/keuangan/import-csv/save', 'FinanceAI::saveCSV');
90+
$routes->get('dashboard/keuangan/report', 'FinanceAI::generateReport');
91+
$routes->post('dashboard/keuangan/report', 'FinanceAI::generateReport');
92+
8593
$routes->get('dashboard/warga', 'Admin::warga');
8694
$routes->get('dashboard/warga/new', 'Admin::createWarga');
8795
$routes->get('dashboard/warga/edit/(:num)', 'Admin::editWarga/$1');
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
<?php
2+
3+
namespace App\Controllers;
4+
5+
use App\Models\MasjidFinanceTransactionModel;
6+
use App\Models\MasjidFinanceCategoryModel;
7+
use App\Models\MasjidProgramModel;
8+
use App\Libraries\SumoPodAI;
9+
10+
class FinanceAI extends BaseController
11+
{
12+
public function importCSV()
13+
{
14+
$masjidId = session()->get('masjid_id');
15+
return view('dashboard/keuangan/import_csv', [
16+
'title' => 'Import Mutasi Bank (AI) - Masj.id'
17+
]);
18+
}
19+
20+
public function processCSV()
21+
{
22+
$masjidId = session()->get('masjid_id');
23+
$file = $this->request->getFile('csv_file');
24+
25+
if (!$file || !$file->isValid() || $file->getExtension() !== 'csv') {
26+
return redirect()->back()->with('error', 'Silakan unggah file CSV yang valid.');
27+
}
28+
29+
$csvData = array_map('str_getcsv', file($file->getTempName()));
30+
if (count($csvData) < 2) {
31+
return redirect()->back()->with('error', 'File CSV kosong atau tidak valid.');
32+
}
33+
34+
// Asumsi format CSV: Tanggal, Deskripsi, Jumlah, Tipe (Masuk/Keluar)
35+
// Kita hanya butuh list transaksi untuk dianalisis AI
36+
$header = array_shift($csvData);
37+
38+
$transactions = [];
39+
foreach ($csvData as $idx => $row) {
40+
if (count($row) >= 4) {
41+
$transactions[] = [
42+
'id' => $idx,
43+
'date' => trim($row[0]),
44+
'description' => trim($row[1]),
45+
'amount' => (float) str_replace(['Rp', '.', ','], '', trim($row[2])),
46+
'type' => strtolower(trim($row[3])) === 'keluar' ? 'pengeluaran' : 'pemasukan'
47+
];
48+
}
49+
}
50+
51+
if (empty($transactions)) {
52+
return redirect()->back()->with('error', 'Tidak ada data transaksi yang dapat diproses.');
53+
}
54+
55+
// Get Categories
56+
$categoryModel = new MasjidFinanceCategoryModel();
57+
$categories = $categoryModel->where('masjid_id', $masjidId)->findAll();
58+
$catMap = [];
59+
foreach ($categories as $cat) {
60+
$catMap[$cat['type']][] = ['id' => $cat['id'], 'name' => $cat['name']];
61+
}
62+
63+
// Call AI
64+
$sumoPod = new SumoPodAI();
65+
$prompt = "Anda adalah AI Akuntan Masjid. Tugas Anda adalah mengkategorikan transaksi bank berikut ke dalam kategori yang tepat.\n\n";
66+
$prompt .= "Kategori Pemasukan Tersedia: " . json_encode($catMap['pemasukan'] ?? []) . "\n";
67+
$prompt .= "Kategori Pengeluaran Tersedia: " . json_encode($catMap['pengeluaran'] ?? []) . "\n\n";
68+
$prompt .= "Data Transaksi:\n" . json_encode($transactions) . "\n\n";
69+
$prompt .= "Output HANYA dalam format array JSON valid dengan key:\n";
70+
$prompt .= "- 'id' (id dari input)\n";
71+
$prompt .= "- 'category_id' (id kategori yang paling cocok, jika tidak ada berikan null)\n";
72+
$prompt .= "- 'suggested_category_name' (nama kategori yang dipilih atau saran kategori baru)\n";
73+
$prompt .= "TIDAK ADA teks lain selain JSON.";
74+
75+
$response = $sumoPod->chatCompletion($prompt, [
76+
'temperature' => 0.1,
77+
'max_tokens' => 1500
78+
]);
79+
80+
$aiResults = [];
81+
if ($response) {
82+
$response = str_replace(['```json', '```'], '', trim($response));
83+
$aiResults = json_decode($response, true) ?? [];
84+
}
85+
86+
// Merge AI Results with Transactions
87+
$mergedTransactions = [];
88+
foreach ($transactions as $t) {
89+
$aiMatch = array_filter($aiResults, fn($r) => $r['id'] === $t['id']);
90+
$aiMatch = reset($aiMatch);
91+
92+
$t['category_id'] = $aiMatch['category_id'] ?? null;
93+
$t['suggested_category_name'] = $aiMatch['suggested_category_name'] ?? 'Lainnya';
94+
$mergedTransactions[] = $t;
95+
}
96+
97+
// Store temporarily in session for review page
98+
session()->set('temp_csv_transactions', $mergedTransactions);
99+
100+
return redirect()->to('/dashboard/keuangan/review-csv');
101+
}
102+
103+
public function reviewCSV()
104+
{
105+
$transactions = session()->get('temp_csv_transactions');
106+
if (!$transactions) {
107+
return redirect()->to('/dashboard/keuangan')->with('error', 'Tidak ada data CSV yang sedang direview.');
108+
}
109+
110+
$masjidId = session()->get('masjid_id');
111+
$categoryModel = new MasjidFinanceCategoryModel();
112+
$categories = $categoryModel->where('masjid_id', $masjidId)->findAll();
113+
114+
return view('dashboard/keuangan/review_csv', [
115+
'title' => 'Review Mutasi Bank - Masj.id',
116+
'transactions' => $transactions,
117+
'categories' => $categories
118+
]);
119+
}
120+
121+
public function saveCSV()
122+
{
123+
$masjidId = session()->get('masjid_id');
124+
$data = $this->request->getPost('transactions');
125+
126+
if (empty($data)) {
127+
return redirect()->to('/dashboard/keuangan')->with('error', 'Tidak ada data yang disimpan.');
128+
}
129+
130+
$transactionModel = new MasjidFinanceTransactionModel();
131+
$categoryModel = new MasjidFinanceCategoryModel();
132+
133+
$insertData = [];
134+
foreach ($data as $t) {
135+
if (!empty($t['date']) && !empty($t['amount'])) {
136+
// If AI suggested a new category or missing category
137+
$catId = $t['category_id'];
138+
if (empty($catId) && !empty($t['suggested_category_name'])) {
139+
$catType = $t['type'];
140+
$slug = url_title($t['suggested_category_name'], '-', true);
141+
142+
// Check if exists
143+
$exist = $categoryModel->where('masjid_id', $masjidId)->where('slug', $slug)->first();
144+
if ($exist) {
145+
$catId = $exist['id'];
146+
} else {
147+
// Create new category
148+
$catId = $categoryModel->insert([
149+
'masjid_id' => $masjidId,
150+
'name' => $t['suggested_category_name'],
151+
'slug' => $slug,
152+
'type' => $catType
153+
]);
154+
}
155+
}
156+
157+
// Format date from dd/mm/yyyy or yyyy-mm-dd to yyyy-mm-dd
158+
$dateRaw = $t['date'];
159+
if (strpos($dateRaw, '/') !== false) {
160+
$parts = explode('/', $dateRaw);
161+
if (count($parts) === 3) {
162+
$dateRaw = $parts[2] . '-' . $parts[1] . '-' . $parts[0];
163+
}
164+
}
165+
166+
$insertData[] = [
167+
'masjid_id' => $masjidId,
168+
'category_id' => $catId,
169+
'date' => $dateRaw,
170+
'amount' => $t['amount'],
171+
'type' => $t['type'],
172+
'description' => $t['description']
173+
];
174+
}
175+
}
176+
177+
if (!empty($insertData)) {
178+
$transactionModel->insertBatch($insertData);
179+
session()->remove('temp_csv_transactions');
180+
return redirect()->to('/dashboard/keuangan')->with('success', count($insertData) . ' Transaksi berhasil disimpan.');
181+
}
182+
183+
return redirect()->back()->with('error', 'Gagal menyimpan transaksi.');
184+
}
185+
186+
public function generateReport()
187+
{
188+
$masjidId = session()->get('masjid_id');
189+
$transactionModel = new MasjidFinanceTransactionModel();
190+
$programModel = new MasjidProgramModel();
191+
192+
// Get this month's data
193+
$currentMonth = date('Y-m');
194+
$transactions = $transactionModel->where('masjid_id', $masjidId)->like('date', $currentMonth)->findAll();
195+
196+
$totalPemasukan = 0;
197+
$totalPengeluaran = 0;
198+
foreach ($transactions as $t) {
199+
if ($t['type'] === 'pemasukan') $totalPemasukan += $t['amount'];
200+
if ($t['type'] === 'pengeluaran') $totalPengeluaran += $t['amount'];
201+
}
202+
203+
$activePrograms = $programModel->where('masjid_id', $masjidId)
204+
->where('date_end >=', date('Y-m-d'))
205+
->countAllResults();
206+
207+
if ($this->request->getMethod() === 'POST' || $this->request->getMethod() === 'post') {
208+
$sumoPod = new SumoPodAI();
209+
210+
$prompt = "Kamu adalah Sekretaris Masjid yang profesional, hangat, dan komunikatif.\n";
211+
$prompt .= "Buat draf narasi/copywriting Laporan Keuangan dan Kegiatan Bulanan yang cocok dikirim melalui WhatsApp Broadcast ke jamaah.\n";
212+
$prompt .= "Data Bulan Ini (" . date('F Y') . "):\n";
213+
$prompt .= "- Total Pemasukan: Rp " . number_format($totalPemasukan, 0, ',', '.') . "\n";
214+
$prompt .= "- Total Pengeluaran: Rp " . number_format($totalPengeluaran, 0, ',', '.') . "\n";
215+
$prompt .= "- Saldo Tersisa: Rp " . number_format($totalPemasukan - $totalPengeluaran, 0, ',', '.') . "\n";
216+
$prompt .= "- Jumlah Program/Kegiatan Aktif: $activePrograms\n\n";
217+
$prompt .= "Instruksi:\n";
218+
$prompt .= "1. Gunakan gaya bahasa yang sopan, bersyukur (Alhamdulillah), dan mengapresiasi donatur.\n";
219+
$prompt .= "2. Jangan terlalu kaku, gunakan sedikit emoji.\n";
220+
$prompt .= "3. Output HARUS BERUPA TEKS LANGSUNG yang siap di-copy-paste, JANGAN format JSON.";
221+
222+
$response = $sumoPod->chatCompletion($prompt, [
223+
'temperature' => 0.7,
224+
'max_tokens' => 800
225+
]);
226+
227+
return $this->response->setJSON(['status' => 'success', 'data' => $response]);
228+
}
229+
230+
return view('dashboard/keuangan/report_generator', [
231+
'title' => 'Generate Laporan (AI) - Masj.id',
232+
'totalPemasukan' => $totalPemasukan,
233+
'totalPengeluaran' => $totalPengeluaran,
234+
'activePrograms' => $activePrograms
235+
]);
236+
}
237+
}

app-core/app/Controllers/SuperAdmin.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ public function updatePassword()
168168
]);
169169

170170
return redirect()->to('superadmin/profile')->with('success', 'Password berhasil diperbarui.');
171+
}
171172
// --------------------------------------------------------------------
172173
// LMS MANAGEMENT (SUPER ADMIN)
173174
// --------------------------------------------------------------------
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?= $this->extend('layout/dashboard') ?>
2+
3+
<?= $this->section('content') ?>
4+
<div class="px-8 py-8">
5+
<div class="mb-8">
6+
<a href="<?= base_url('dashboard/keuangan') ?>" class="text-[#608a7e] hover:text-primary flex items-center gap-2 mb-4 transition-colors">
7+
<span class="material-symbols-outlined text-sm">arrow_back</span>
8+
Kembali ke Keuangan
9+
</a>
10+
<h1 class="text-3xl font-black text-[#111816] dark:text-white tracking-tight">Impor Mutasi (AI)</h1>
11+
<p class="text-[#608a7e] mt-2">Unggah file CSV dari mutasi bank (BCA, Mandiri, BSI, dll) dan biarkan AI mengkategorikannya secara otomatis.</p>
12+
</div>
13+
14+
<?php if(session()->getFlashdata('error')): ?>
15+
<div class="p-4 mb-6 text-sm text-red-800 rounded-2xl bg-red-50 dark:bg-red-900/20 dark:text-red-400">
16+
<?= session()->getFlashdata('error') ?>
17+
</div>
18+
<?php endif; ?>
19+
20+
<div class="bg-white dark:bg-white/5 rounded-[3rem] border border-[#e5e7eb] dark:border-white/10 p-10 max-w-2xl mx-auto text-center shadow-sm">
21+
<div class="mb-8">
22+
<span class="material-symbols-outlined text-7xl text-primary/30">upload_file</span>
23+
</div>
24+
25+
<form action="<?= base_url('dashboard/keuangan/import-csv/process') ?>" method="POST" enctype="multipart/form-data">
26+
<?= csrf_field() ?>
27+
<div class="mb-8">
28+
<label class="flex flex-col items-center justify-center w-full h-64 border-2 border-dashed border-gray-300 dark:border-white/20 rounded-3xl cursor-pointer hover:bg-gray-50 dark:hover:bg-white/5 transition-colors group">
29+
<div class="flex flex-col items-center justify-center pt-5 pb-6">
30+
<span class="material-symbols-outlined text-4xl text-gray-400 group-hover:text-primary mb-3 transition-colors">csv</span>
31+
<p class="mb-2 text-sm text-gray-500 dark:text-gray-400"><span class="font-bold">Klik untuk unggah</span> atau drag and drop</p>
32+
<p class="text-xs text-gray-400 dark:text-gray-500">File CSV Mutasi Bank (Format: Tanggal, Deskripsi, Jumlah, Tipe)</p>
33+
<p id="file-name" class="mt-4 font-bold text-primary"></p>
34+
</div>
35+
<input type="file" name="csv_file" class="hidden" accept=".csv" required onchange="document.getElementById('file-name').innerText = this.files[0].name" />
36+
</label>
37+
</div>
38+
39+
<button type="submit" class="w-full px-6 py-4 bg-primary text-white rounded-2xl font-black shadow-lg shadow-primary/20 hover:bg-emerald-900 transition-all flex items-center justify-center gap-2">
40+
<span class="material-symbols-outlined">auto_awesome</span>
41+
Proses dengan AI
42+
</button>
43+
</form>
44+
45+
<div class="mt-10 text-left bg-gray-50 dark:bg-white/5 rounded-2xl p-6">
46+
<h4 class="font-bold text-sm mb-2 text-[#111816] dark:text-white">Format Kolom CSV yang didukung:</h4>
47+
<ul class="text-xs text-[#608a7e] space-y-2 list-disc list-inside">
48+
<li>Kolom 1: Tanggal Transaksi (DD/MM/YYYY atau YYYY-MM-DD)</li>
49+
<li>Kolom 2: Deskripsi / Keterangan Mutasi</li>
50+
<li>Kolom 3: Jumlah Nominal (Rp)</li>
51+
<li>Kolom 4: Tipe (Masuk / Keluar)</li>
52+
</ul>
53+
</div>
54+
</div>
55+
</div>
56+
<?= $this->endSection() ?>

app-core/app/Views/dashboard/keuangan/index.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@
99
<p class="text-[#608a7e]">Pantau saldo, catat pemasukan, dan kelola pengeluaran masjid.</p>
1010
</div>
1111
<div class="flex items-center gap-3">
12-
<a href="<?= base_url('dashboard/keuangan/mutasi') ?>" class="flex items-center gap-2 px-5 py-2.5 rounded-xl border border-primary/20 bg-primary/5 text-primary text-sm font-bold hover:bg-primary/10 transition-all">
13-
<span class="material-symbols-outlined text-sm">sync_alt</span>
14-
Impor Mutasi Bank
12+
<a href="<?= base_url('dashboard/keuangan/report') ?>" class="flex items-center gap-2 px-5 py-2.5 rounded-xl border border-primary/20 bg-primary/5 text-primary text-sm font-bold hover:bg-primary/10 transition-all">
13+
<span class="material-symbols-outlined text-sm">auto_awesome</span>
14+
Laporan AI
15+
</a>
16+
<a href="<?= base_url('dashboard/keuangan/import-csv') ?>" class="flex items-center gap-2 px-5 py-2.5 rounded-xl border border-[#dbe6e3] dark:border-white/10 text-sm font-bold hover:bg-white dark:hover:bg-white/5 transition-all">
17+
<span class="material-symbols-outlined text-sm">csv</span>
18+
Impor Mutasi (AI)
1519
</a>
1620
<button onclick="openCategoryModal()" class="flex items-center gap-2 px-5 py-2.5 rounded-xl border border-[#dbe6e3] dark:border-white/10 text-sm font-bold hover:bg-white dark:hover:bg-white/5 transition-all">
1721
<span class="material-symbols-outlined text-sm">category</span>

0 commit comments

Comments
 (0)