This commit is contained in:
alealien666 2025-06-07 18:52:25 +07:00
parent 91dff8b258
commit 0cc138f0a2
31 changed files with 723 additions and 555 deletions

View File

@ -1,65 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Models\DetailPayment;
use Illuminate\Http\Request;
class DetailPaymentController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
//
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
*/
public function show(DetailPayment $detailPayment)
{
//
}
/**
* Show the form for editing the specified resource.
*/
public function edit(DetailPayment $detailPayment)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, DetailPayment $detailPayment)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(DetailPayment $detailPayment)
{
//
}
}

View File

@ -4,11 +4,62 @@
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Inertia\Inertia; use Inertia\Inertia;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
use App\Models\User;
use App\Models\Wallet;
use App\Models\DetailPayment;
class HomeController extends Controller class HomeController extends Controller
{ {
public function index() public function index()
{ {
return Inertia::render('Dashboard'); $now = Carbon::now();
$month = $now->month;
$year = $now->year;
$monthlyIncome = DetailPayment::where('status', 'paid')
->where('payment_month', $month)
->where('payment_year', $year)
->whereHas('payments', fn($q) => $q->where('payment_status', 'success'))
->sum(DB::raw('IFNULL(amount,0) + IFNULL(penalty,0)'));
$studentCount = User::where('level', 2)
->where('status_santri', 'aktif')
->count();
$totalBalance = Wallet::sum('saldo');
$paymentTrend = DetailPayment::select('payment_month', 'payment_year')
->selectRaw('SUM(IFNULL(amount,0) + IFNULL(penalty,0)) as total')
->where('status', 'paid')
->whereHas('payments', fn($q) => $q->where('payment_status', 'success'))
->whereBetween(DB::raw("STR_TO_DATE(CONCAT(payment_year,'-',payment_month,'-01'), '%Y-%m-%d')"), [
now()->subMonths(11)->startOfMonth()->toDateString(),
now()->endOfMonth()->toDateString(),
])
->groupBy('payment_year', 'payment_month')
->orderBy('payment_year')
->orderBy('payment_month')
->get();
$labels = [];
$data = [];
foreach ($paymentTrend as $pt) {
$labels[] = Carbon::createFromDate($pt->payment_year, $pt->payment_month, 1)->format('M Y');
$data[] = (int)$pt->total;
}
return Inertia()->render('Dashboard', [
'monthlyIncome' => $monthlyIncome,
'studentCount' => $studentCount,
'totalBalance' => $totalBalance,
'paymentTrend' => [
'labels' => $labels,
'data' => $data,
],
]);
} }
} }

View File

@ -13,13 +13,12 @@
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Exception; use Exception;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
class PaymentController extends Controller class PaymentController extends Controller
{ {
public function indexManualPayment(cekDenda $cekDenda, GenerateMonthlyBill $generateMonthlyBill) public function indexManualPayment(cekDenda $cekDenda, GenerateMonthlyBill $generateMonthlyBill)
{ {
$penalty = $cekDenda->applyPenalty();
$bill = $generateMonthlyBill->generateAutoBill();
$paymentTypes = PaymentType::get(['id', 'payment_type', 'nominal']); $paymentTypes = PaymentType::get(['id', 'payment_type', 'nominal']);
@ -29,12 +28,11 @@ public function indexManualPayment(cekDenda $cekDenda, GenerateMonthlyBill $gene
$santri = User::with([ $santri = User::with([
'payments.detailPayments.paymentType', 'payments.detailPayments.paymentType',
])->paginate(10); ])->where('level', 2)->paginate(10);
return Inertia::render('list-admin/payment/ManualPayment', [ return Inertia::render('list-admin/payment/ManualPayment', [
'santri' => $santri->items(), 'santri' => $santri,
'penalty' => $penalty, 'penalty' => 0,
'bill' => $bill,
'fields' => [ 'fields' => [
'nis' => ['type' => 'text', 'readonly' => true], 'nis' => ['type' => 'text', 'readonly' => true],
'nama' => ['type' => 'text', 'readonly' => true], 'nama' => ['type' => 'text', 'readonly' => true],
@ -48,113 +46,85 @@ public function indexManualPayment(cekDenda $cekDenda, GenerateMonthlyBill $gene
]); ]);
} }
public function manualPayment(Request $request, $paymentId) public function manualPayment(Request $request, $userId)
{ {
// $request->validate([ $validator = Validator::make($request->all(), [
// 'items' => 'required|array', 'items' => 'required|array',
// 'items.*.type_id' => 'required|exists:payment_types,id', 'items.*.type_id' => 'required|exists:payment_types,id',
// 'items.*.range' => 'required|integer|min:1' 'items.*.range' => 'required|integer|min:1',
// ]); ]);
if ($validator->fails()) {
return redirect()->back()->withErrors($validator)->withInput();
}
$items = $request->input('items'); DB::beginTransaction();
$userId = $request->id;
$items = json_decode($request->input('items'), true);
try { try {
DB::beginTransaction();
$paymentTypes = PaymentType::pluck('nominal', 'id'); $paymentTypes = PaymentType::pluck('nominal', 'id');
$user = User::findOrFail($userId);
$existingPayment = Payment::where('user_id', $userId) $existingPayment = Payment::where('user_id', $userId)
->where('payment_status', 'pending') ->where('payment_status', 'pending')
->lockForUpdate()
->first(); ->first();
if ($existingPayment) { if ($existingPayment) {
$totalAmount = 0; $totalAmountExisting = DetailPayment::where('payment_id', $existingPayment->id)->sum('amount');
$hasUnpaid = DetailPayment::where('payment_id', $existingPayment->id)
foreach ($items as $item) { ->where('status', 'unpaid')
$typeId = $item['type_id']; ->exists();
$range = $item['range'];
$nominal = $paymentTypes[$typeId] ?? 0;
$unpaidDetails = DetailPayment::where('payment_id', $existingPayment->id)
->where('status', 'unpaid')
->where('type_id', $typeId)
->orderBy('payment_year')
->orderBy('payment_month')
->get();
$toPay = $unpaidDetails->take($range);
$toPay->each(function ($detail) use (&$totalAmount, $nominal) {
$detail->update([
'status' => 'paid',
'amount' => $nominal,
'penalty' => $detail->penalty ?? 0,
]);
$totalAmount += $nominal + $detail->penalty;
});
$sisa = $range - $toPay->count();
if ($sisa > 0) {
$lastDetail = DetailPayment::where('payment_id', $existingPayment->id)
->orderBy('payment_year', 'desc')
->orderBy('payment_month', 'desc')
->first();
$bulan = $lastDetail->payment_month ?? now()->month;
$tahun = $lastDetail->payment_year ?? now()->year;
for ($i = 0; $i < $sisa; $i++) {
$bulan++;
if ($bulan > 12) {
$bulan = 1;
$tahun++;
}
DetailPayment::create([
'payment_id' => $existingPayment->id,
'payment_month' => $bulan,
'payment_year' => $tahun,
'amount' => $nominal,
'penalty' => 0,
'status' => 'paid',
'type_id' => $typeId,
]);
$totalAmount += $nominal;
}
}
}
$existingPayment->update([ $existingPayment->update([
'amount_payment' => DetailPayment::where('payment_id', $existingPayment->id)->sum('amount'), 'amount_payment' => $totalAmountExisting,
'payment_status' => DetailPayment::where('payment_id', $existingPayment->id) 'payment_status' => $hasUnpaid ? 'pending' : 'success',
->where('status', 'unpaid')
->exists() ? 'pending' : 'success'
]); ]);
DB::commit(); DB::commit();
return response()->json(['message' => 'Pembayaran berhasil diupdate']);
return redirect()->back()->with('success', 'Pembayaran yang pending sudah diupdate.');
} }
$newPayment = Payment::create([ $newPayment = Payment::create([
'payment_status' => 'success', 'payment_status' => 'success',
'amount_payment' => 0, 'amount_payment' => 0,
'transaction_type' => 'payment',
'user_id' => $userId, 'user_id' => $userId,
'order_id' => 'TRX' . uniqid(),
]); ]);
$bulan = now()->month;
$tahun = now()->year;
$totalAmount = 0; $totalAmount = 0;
foreach ($items as $item) { foreach ($request->items as $item) {
$typeId = $item['type_id']; $typeId = $item['type_id'];
$range = $item['range']; $range = (int) $item['range'];
$nominal = $paymentTypes[$typeId] ?? 0; $nominal = $paymentTypes[$typeId] ?? 0;
$lastDetail = DetailPayment::whereHas('payments', function ($q) use ($userId) {
$q->where('user_id', $userId);
})
->where('type_id', $typeId)
->orderBy('payment_year', 'desc')
->orderBy('payment_month', 'desc')
->first();
if ($lastDetail) {
$bulan = $lastDetail->payment_month;
$tahun = $lastDetail->payment_year;
} else {
$bulan = now()->month;
$tahun = now()->year;
}
for ($i = 0; $i < $range; $i++) { for ($i = 0; $i < $range; $i++) {
if ($i > 0 || $lastDetail) {
$bulan++;
if ($bulan > 12) {
$bulan = 1;
$tahun++;
}
}
DetailPayment::create([ DetailPayment::create([
'payment_id' => $newPayment->id, 'payment_id' => $newPayment->id,
'payment_month' => $bulan, 'payment_month' => $bulan,
@ -166,24 +136,29 @@ public function manualPayment(Request $request, $paymentId)
]); ]);
$totalAmount += $nominal; $totalAmount += $nominal;
$bulan++;
if ($bulan > 12) {
$bulan = 1;
$tahun++;
}
} }
} }
$newPayment->update(['amount_payment' => $totalAmount]); $newPayment->update(['amount_payment' => $totalAmount]);
DB::commit();
// dd('berhasil lur');
return redirect()->back()->with(['success' => 'Pembayaran baru berhasil dibuat']); DB::commit();
} catch (Exception $e) {
return redirect()->back()->with('success', 'Pembayaran baru berhasil dibuat');
} catch (\Exception $e) {
DB::rollBack(); DB::rollBack();
// dd('gagal' . $e->getMessage()); return redirect()->back()->with('error', 'Gagal membuat pembayaran: ' . $e->getMessage());
return redirect()->back()->with(['error' => 'gagal' . $e->getMessage()]);
} }
} }
public function transaction()
{
$transaction = User::with('payments', 'payments.detailPayments', 'wallet.walletTransactions', 'payments.detailPayments.paymentType')
->where('level', 10)
->paginate(2);
// dd($transaction);
return Inertia::render('list-admin/payment/Transaction', compact('transaction'));
}
} }

View File

@ -3,6 +3,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\User; use App\Models\User;
use App\Models\Wallet;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Inertia\Inertia; use Inertia\Inertia;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
@ -13,7 +14,7 @@ class SantriController extends Controller
{ {
public function index() public function index()
{ {
$santri = User::all(); $santri = User::where('level', 2)->paginate(10);
return Inertia::render('list-admin/santri/IndexSantri', [ return Inertia::render('list-admin/santri/IndexSantri', [
'santri' => $santri, 'santri' => $santri,
'fields' => [ 'fields' => [
@ -88,6 +89,8 @@ public function store(Request $request)
'foto' => $fotoPath 'foto' => $fotoPath
]); ]);
$santri->wallet()->create(['saldo' => 0]);
// dd($santri); // dd($santri);
return redirect()->back()->with('success', 'Data berhasil ditambahkan'); return redirect()->back()->with('success', 'Data berhasil ditambahkan');
} catch (\Throwable $th) { } catch (\Throwable $th) {

View File

@ -10,8 +10,9 @@ class WalletController extends Controller
{ {
public function walletUser() public function walletUser()
{ {
$wallet = User::with('wallet')->get(); $wallet = User::with('wallet')
// dd($wallet); ->where('level', 2)
->paginate(10);
return Inertia::render('list-admin/payment/WalletUser', compact('wallet')); return Inertia::render('list-admin/payment/WalletUser', compact('wallet'));
} }

View File

@ -1,65 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Models\WalletTransaction;
use Illuminate\Http\Request;
class WalletTransactionController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
//
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
*/
public function show(WalletTransaction $walletTransaction)
{
//
}
/**
* Show the form for editing the specified resource.
*/
public function edit(WalletTransaction $walletTransaction)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, WalletTransaction $walletTransaction)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(WalletTransaction $walletTransaction)
{
//
}
}

View File

@ -15,10 +15,12 @@ public function up(): void
$table->id(); $table->id();
$table->string('order_id')->unique()->nullable(); $table->string('order_id')->unique()->nullable();
$table->enum('payment_status', ['pending', 'failed', 'success']); $table->enum('payment_status', ['pending', 'failed', 'success']);
$table->enum('transaction_type', ['topup', 'payment']);
$table->decimal('amount_payment')->nullable(); $table->decimal('amount_payment')->nullable();
$table->String('bank')->nullable(); $table->String('bank')->nullable();
$table->string('no_va')->nullable(); $table->string('no_va')->nullable();
$table->dateTime('expired_at')->nullable(); $table->dateTime('expired_at')->nullable();
$table->string('snap_token')->nullable();
$table->foreignId('user_id')->constrained('users')->onDelete('cascade'); $table->foreignId('user_id')->constrained('users')->onDelete('cascade');
$table->foreignId('wallet_id')->nullable()->constrained('wallets')->onDelete('cascade'); $table->foreignId('wallet_id')->nullable()->constrained('wallets')->onDelete('cascade');
$table->timestamps(); $table->timestamps();

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 906 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 906 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

@ -30,9 +30,9 @@ function Header() {
setTheme(prevTheme => prevTheme === "dark" ? "light" : "dark"); setTheme(prevTheme => prevTheme === "dark" ? "light" : "dark");
} }
const openNotification = () => { // const openNotification = () => {
dispatch(openRightDrawer({ header: "Notifications", bodyType: RIGHT_DRAWER_TYPES.NOTIFICATION })) // dispatch(openRightDrawer({ header: "Notifications", bodyType: RIGHT_DRAWER_TYPES.NOTIFICATION }))
} // }
const { post } = useForm() const { post } = useForm()
const logoutUser = (e) => { const logoutUser = (e) => {
@ -54,12 +54,12 @@ function Header() {
{theme === "dark" ? <SunIcon className="w-6 h-6" /> : <MoonIcon className="w-6 h-6" />} {theme === "dark" ? <SunIcon className="w-6 h-6" /> : <MoonIcon className="w-6 h-6" />}
</button> </button>
<button className="btn btn-ghost ml-4 btn-circle" onClick={openNotification}> {/* <button className="btn btn-ghost ml-4 btn-circle" onClick={openNotification}>
<div className="indicator"> <div className="indicator">
<BellIcon className="h-6 w-6" /> <BellIcon className="h-6 w-6" />
{noOfNotifications > 0 && <span className="indicator-item badge badge-secondary badge-sm">{noOfNotifications}</span>} {noOfNotifications > 0 && <span className="indicator-item badge badge-secondary badge-sm">{noOfNotifications}</span>}
</div> </div>
</button> </button> */}
<div className="dropdown dropdown-end ml-4"> <div className="dropdown dropdown-end ml-4">
<label tabIndex={0} className="btn btn-ghost btn-circle avatar"> <label tabIndex={0} className="btn btn-ghost btn-circle avatar">
@ -71,8 +71,6 @@ function Header() {
<li className="ml-3" >Welcome, {auth.user.nama}</li> <li className="ml-3" >Welcome, {auth.user.nama}</li>
<div className="divider mt-0 mb-0"></div> <div className="divider mt-0 mb-0"></div>
<li><Link href={route('profile.edit')}>Profile Settings</Link></li> <li><Link href={route('profile.edit')}>Profile Settings</Link></li>
<li><Link href="/app/settings-billing">Bill History</Link></li>
<div className="divider mt-0 mb-0"></div>
<li><a href="#" onClick={logoutUser}>Logout</a></li> <li><a href="#" onClick={logoutUser}>Logout</a></li>
</ul> </ul>
</div> </div>

View File

@ -1,6 +1,12 @@
import { Inertia } from "@inertiajs/inertia"; import { Inertia } from "@inertiajs/inertia";
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
const getImageUrl = (foto) => {
if (!foto) return null;
if (typeof foto === 'string') return `${foto}`;
return URL.createObjectURL(foto);
};
const ModalInput = ({ fields, tableName, options, initialData, onClose, showPayments = false }) => { const ModalInput = ({ fields, tableName, options, initialData, onClose, showPayments = false }) => {
const [formData, setFormData] = useState({}); const [formData, setFormData] = useState({});
const [errors, setErrors] = useState({}); const [errors, setErrors] = useState({});
@ -86,11 +92,17 @@ const ModalInput = ({ fields, tableName, options, initialData, onClose, showPaym
...detail, ...detail,
type_id: parseInt(type_id) type_id: parseInt(type_id)
})); }));
console.log('Payment Details:', detailsArray);
formDataObj.append('items', JSON.stringify(detailsArray)); // Append items satu per satu
detailsArray.forEach((item, index) => {
formDataObj.append(`items[${index}][type_id]`, item.type_id);
formDataObj.append(`items[${index}][range]`, item.range);
// Kalau mau append nominal & penalty juga bisa, tapi biasanya backend gak butuh karena bisa hitung ulang
});
} }
const url = initialData const url = initialData
? `/update${tableName}/${initialData.id}` ? `/update${tableName}/${initialData.id}`
: `/add${tableName}`; : `/add${tableName}`;
@ -107,6 +119,14 @@ const ModalInput = ({ fields, tableName, options, initialData, onClose, showPaym
}); });
}; };
useEffect(() => {
if (formData.foto instanceof File) {
const url = URL.createObjectURL(formData.foto);
return () => URL.revokeObjectURL(url);
}
}, [formData.foto]);
return ( return (
<div> <div>
@ -141,12 +161,26 @@ const ModalInput = ({ fields, tableName, options, initialData, onClose, showPaym
))} ))}
</select> </select>
) : type === "file" ? ( ) : type === "file" ? (
<input <div>
type="file" <input
name={field} type="file"
onChange={handleChange} name={field}
className="file-input file-input-info focus:outline-none focus:ring-1 ring-info w-full" onChange={handleChange}
/> className="file-input file-input-info focus:outline-none focus:ring-1 ring-info w-full"
/>
{field === 'foto' && (
<div className="mt-2">
{formData.foto && (
<img
src={getImageUrl(formData.foto)}
alt="Preview Foto"
className="w-32 h-32 object-cover rounded-lg border"
/>
)}
</div>
)}
</div>
) : type === "password" ? ( ) : type === "password" ? (
<input <input
type="password" type="password"
@ -191,10 +225,9 @@ const ModalInput = ({ fields, tableName, options, initialData, onClose, showPaym
<option disabled value=""> <option disabled value="">
Pilih Payment Type Pilih Payment Type
</option> </option>
// Di bagian dropdown payment type:
{options.payment_type && {options.payment_type &&
Object.entries(options.payment_type).map(([id, name]) => ( Object.entries(options.payment_type).map(([id, name]) => (
<option key={id} value={id}> {/* Gunakan ID sebagai value */} <option key={id} value={id}>
{name} {name}
</option> </option>
))} ))}

View File

@ -1,8 +1,8 @@
import { useEffect } from 'react'; import { useEffect, useState } from 'react';
import { Head, Link, useForm } from '@inertiajs/react'; import { Head, Link, useForm } from '@inertiajs/react';
import { usePage } from '@inertiajs/react'; import { usePage } from '@inertiajs/react';
import Swal from "sweetalert2" import Swal from "sweetalert2"
// import { InertiaLink, usePage } from '@inertiajs/react'; import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline';
function Login({ status }) { function Login({ status }) {
const { data, setData, post, processing, errors, reset } = useForm({ const { data, setData, post, processing, errors, reset } = useForm({
@ -11,6 +11,7 @@ function Login({ status }) {
remember: false, remember: false,
}); });
const { flash } = usePage().props; const { flash } = usePage().props;
const [showPassword, setShowPassword] = useState(false);
useEffect(() => { useEffect(() => {
if (flash.success) { if (flash.success) {
@ -59,30 +60,41 @@ function Login({ status }) {
name="nis" name="nis"
value={data.nis} value={data.nis}
className="w-full p-3 border border-gray-700 rounded-lg text-gray-700" className="w-full p-3 border border-gray-700 rounded-lg text-gray-700"
placeholder="name@company.com" placeholder="Masukkan Nis Anda"
autoComplete="username" autoComplete="username"
onChange={(e) => setData('nis', e.target.value)} onChange={(e) => setData('nis', e.target.value)}
/> />
{errors.nis && <p className="mt-1 text-sm text-red-500">{errors.nis}</p>} {errors.nis && <p className="mt-1 text-sm text-red-500">{errors.nis}</p>}
</div> </div>
<div className="mb-6"> <div className="mb-6 relative">
<label htmlFor="password" className="block text-xl mb-2 text-white"> <label htmlFor="password" className="block text-xl mb-2 text-white">
Password Password
</label> </label>
<input <input
id="password" id="password"
type="password" type={showPassword ? 'text' : 'password'}
name="password" name="password"
value={data.password} value={data.password}
className="w-full p-3 border border-gray-700 rounded-lg" className="w-full p-3 border border-gray-700 rounded-lg pr-12"
placeholder="••••••••" placeholder="••••••••"
autoComplete="current-password" autoComplete="current-password"
onChange={(e) => setData('password', e.target.value)} onChange={(e) => setData('password', e.target.value)}
/> />
<div
className="absolute top-[50px] right-4 cursor-pointer text-gray-400 hover:text-black"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<EyeSlashIcon className="h-5 w-5" />
) : (
<EyeIcon className="h-5 w-5" />
)}
</div>
{errors.password && <p className="mt-1 text-sm text-red-500">{errors.password}</p>} {errors.password && <p className="mt-1 text-sm text-red-500">{errors.password}</p>}
</div> </div>
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<div className="flex items-center"> <div className="flex items-center">
<input <input

View File

@ -1,48 +1,45 @@
import React from 'react'; import React, { useEffect } from 'react';
import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend, ArcElement } from 'chart.js'; import {
import { Bar, Doughnut } from 'react-chartjs-2'; Chart as ChartJS,
import { Head } from '@inertiajs/react'; CategoryScale,
import { useEffect } from 'react'; LinearScale,
BarElement,
Title,
Tooltip,
Legend,
ArcElement,
} from 'chart.js';
import { Bar } from 'react-chartjs-2';
import { Head, usePage } from '@inertiajs/react';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { setPageTitle } from '@/Components/features/common/headerSlice'; import { setPageTitle } from '@/Components/features/common/headerSlice';
import Squares2X2Icon from '@heroicons/react/24/outline/Squares2X2Icon' import { Squares2X2Icon } from '@heroicons/react/24/outline';
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend, ArcElement); ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend, ArcElement);
const Dashboard = () => { const Dashboard = () => {
const barChartData = { const dispatch = useDispatch();
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct'],
datasets: [
{
label: 'CHN',
data: [12, 19, 8, 15, 12, 8, 16, 20, 15, 14],
backgroundColor: 'rgb(196, 151, 239)',
},
{
label: 'USA',
data: [19, 12, 15, 8, 9, 16, 14, 12, 10, 15],
backgroundColor: 'rgb(86, 207, 225)',
},
{
label: 'UK',
data: [15, 15, 10, 12, 20, 14, 8, 16, 10, 18],
backgroundColor: 'rgb(255, 170, 145)',
},
],
};
const doughnutChartData = { // Ambil data dari Inertia props
labels: ['Social Media', 'Direct', 'Email', 'Organic'], const { props } = usePage();
const {
monthlyIncome = 0,
studentCount = 0,
totalBalance = 0,
paymentTrend = { labels: [], data: [] },
} = props;
useEffect(() => {
dispatch(setPageTitle('Dashboard'));
}, [dispatch]);
const barChartData = {
labels: paymentTrend.labels,
datasets: [ datasets: [
{ {
data: [35, 30, 15, 20], label: 'Pemasukan Bulanan',
backgroundColor: [ data: paymentTrend.data,
'rgb(255, 99, 132)', backgroundColor: 'rgb(86, 207, 225)',
'rgb(54, 162, 235)',
'rgb(153, 102, 255)',
'rgb(75, 192, 192)',
],
borderWidth: 0,
}, },
], ],
}; };
@ -50,45 +47,34 @@ const Dashboard = () => {
const barChartOptions = { const barChartOptions = {
responsive: true, responsive: true,
plugins: { plugins: {
legend: { legend: { display: false },
display: false, title: {
display: true,
text: 'Tren Pemasukan Bulanan',
font: { size: 16, weight: 'bold' },
},
tooltip: {
callbacks: {
label: function (context) {
return `Rp ${context.parsed.y.toLocaleString()}`;
},
},
}, },
}, },
scales: { scales: {
x: { x: { grid: { display: false } },
grid: {
display: false,
},
},
y: { y: {
grid: { grid: { display: false },
display: false,
},
ticks: { ticks: {
display: false, callback: function (value) {
return `Rp ${value.toLocaleString()}`;
},
}, },
}, },
}, },
maintainAspectRatio: false, maintainAspectRatio: false,
}; };
const doughnutChartOptions = {
responsive: true,
plugins: {
legend: {
display: false,
},
},
cutout: '70%',
maintainAspectRatio: false,
};
const dispatch = useDispatch();
useEffect(() => {
dispatch(setPageTitle("Dashboard"));
}, [dispatch]);
return ( return (
<div className="card bg-base-100 shadow-xl"> <div className="card bg-base-100 shadow-xl">
<Head title="Dashboard" /> <Head title="Dashboard" />
@ -100,117 +86,45 @@ const Dashboard = () => {
<h1 className="text-2xl font-bold">Dashboard</h1> <h1 className="text-2xl font-bold">Dashboard</h1>
<div className="ml-auto"> <div className="ml-auto">
<span className="text-gray-700">Overview</span> <span className="text-gray-700">Overview</span>
<span className="ml-2 text-gray-500">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 inline" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</span>
</div> </div>
</div> </div>
{/* Kartu info utama */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div className="rounded-lg overflow-hidden bg-gradient-to-r from-orange-300 to-pink-400 text-white p-6"> <div className="rounded-lg overflow-hidden bg-gradient-to-r from-orange-300 to-pink-400 text-white p-6">
<div className="flex justify-between"> <p className="text-xl font-medium mb-4">Pemasukan Bulan Ini</p>
<div> <p className="text-4xl font-bold mb-6">
<p className="text-xl font-medium mb-4">Weekly Sales</p> Rp {(Number(monthlyIncome) || 0).toLocaleString()}
<p className="text-4xl font-bold mb-6">$ 15,0000</p> </p>
<p>Increased by 60%</p>
</div>
<div>
<svg xmlns="http://www.w3.org/2000/svg" className="h-10 w-10 text-white opacity-80" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
</div>
</div> </div>
<div className="rounded-lg overflow-hidden bg-gradient-to-r from-blue-300 to-blue-500 text-white p-6"> <div className="rounded-lg overflow-hidden bg-gradient-to-r from-blue-300 to-blue-500 text-white p-6">
<div className="flex justify-between"> <p className="text-xl font-medium mb-4">Santri Aktif</p>
<div> <p className="text-4xl font-bold mb-6">{studentCount || 0}</p>
<p className="text-xl font-medium mb-4">Weekly Orders</p>
<p className="text-4xl font-bold mb-6">45,6334</p>
<p>Decreased by 10%</p>
</div>
<div>
<svg xmlns="http://www.w3.org/2000/svg" className="h-10 w-10 text-white opacity-80" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
</svg>
</div>
</div>
</div> </div>
<div className="rounded-lg overflow-hidden bg-gradient-to-r from-green-300 to-teal-500 text-white p-6"> <div className="rounded-lg overflow-hidden bg-gradient-to-r from-green-300 to-teal-500 text-white p-6">
<div className="flex justify-between"> <p className="text-xl font-medium mb-4">Total Saldo Wallet</p>
<div> <p className="text-4xl font-bold mb-6">
<p className="text-xl font-medium mb-4">Visitors Online</p> Rp {(Number(totalBalance) || 0).toLocaleString()}
<p className="text-4xl font-bold mb-6">95,5741</p> </p>
<p>Increased by 5%</p>
</div>
<div>
<svg xmlns="http://www.w3.org/2000/svg" className="h-10 w-10 text-white opacity-80" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 12l-1.25-1.25M12 20l1.25-1.25M4 12l1.25 1.25M12 4l-1.25 1.25M20 12h-2M12 20v-2M4 12h2M12 4v2" />
</svg>
</div>
</div>
</div> </div>
</div> </div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4"> {/* Grafik Tren Pembayaran */}
<div className=" rounded-lg p-6 shadow"> <div className="w-full bg-base-100">
<div className="rounded-lg p-6 shadow">
<div className="flex justify-between items-center mb-6"> <div className="flex justify-between items-center mb-6">
<h2 className="text-xl font-bold">Visit And Sales Statistics</h2> <h2 className="text-xl font-bold">Tren Pembayaran 12 Bulan Terakhir</h2>
<div className="flex space-x-4">
<div className="flex items-center">
<span className="h-3 w-3 rounded-full bg-purple-400 mr-1"></span>
<span className="text-gray-500 text-sm">CHN</span>
</div>
<div className="flex items-center">
<span className="h-3 w-3 rounded-full bg-cyan-400 mr-1"></span>
<span className="text-gray-500 text-sm">USA</span>
</div>
<div className="flex items-center">
<span className="h-3 w-3 rounded-full bg-orange-300 mr-1"></span>
<span className="text-gray-500 text-sm">UK</span>
</div>
</div>
</div> </div>
<div className="h-64"> <div className="h-64">
<Bar data={barChartData} options={barChartOptions} /> <Bar data={barChartData} options={barChartOptions} />
</div> </div>
</div> </div>
<div className="rounded-lg p-6 shadow">
<div className="mb-6">
<h2 className="text-xl font-bold">Traffic Sources</h2>
</div>
<div className="flex justify-center">
<div className="h-64 w-64">
<Doughnut data={doughnutChartData} options={doughnutChartOptions} />
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 mt-6">
<div className="flex items-center justify-center">
<span className="h-3 w-3 rounded-full bg-pink-400 mr-2"></span>
<span className="text-gray-700 text-sm">Social Media</span>
</div>
<div className="flex items-center justify-center">
<span className="h-3 w-3 rounded-full bg-blue-400 mr-2"></span>
<span className="text-gray-700 text-sm">Direct</span>
</div>
<div className="flex items-center justify-center">
<span className="h-3 w-3 rounded-full bg-purple-400 mr-2"></span>
<span className="text-gray-700 text-sm">Email</span>
</div>
<div className="flex items-center justify-center">
<span className="h-3 w-3 rounded-full bg-teal-400 mr-2"></span>
<span className="text-gray-700 text-sm">Organic</span>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
); );
}; };
export default Dashboard; export default Dashboard;

View File

@ -17,35 +17,20 @@ export default function ManualPayment({ santri, fields, options }) {
const dispatch = useDispatch(); const dispatch = useDispatch();
useEffect(() => {
if (flash.success) {
Swal.fire({
icon: 'success',
title: 'Success',
text: flash.success
});
} else if (flash.error) {
Swal.fire({
icon: 'success',
title: 'Success',
text: flash.success
});
}
}, [flash]);
useEffect(() => { useEffect(() => {
dispatch(setPageTitle("Data Payment")); dispatch(setPageTitle("Data Payment"));
}, [dispatch]); }, [dispatch]);
// console.log(santri.id) // console.log(santri.id)
useEffect(() => { useEffect(() => {
if (santri) { if (santri?.data) {
setFilteredSantri( if (santri?.data) {
santri.filter(item => const filtered = santri.data.filter(item =>
item.nama.toLowerCase().includes(searchTerm.toLowerCase()) || item.nama.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.nis.toString().includes(searchTerm) item.nis.toString().includes(searchTerm)
) );
); setFilteredSantri(filtered);
}
} }
}, [searchTerm, santri]); }, [searchTerm, santri]);
@ -53,6 +38,12 @@ export default function ManualPayment({ santri, fields, options }) {
setSearchTerm(e.target.value); setSearchTerm(e.target.value);
}; };
const monthNames = [
"Januari", "Februari", "Maret", "April", "Mei", "Juni",
"Juli", "Agustus", "September", "Oktober", "November", "Desember"
];
return ( return (
<div> <div>
@ -163,22 +154,45 @@ export default function ManualPayment({ santri, fields, options }) {
)} )}
</tbody> </tbody>
</table> </table>
<div className="flex justify-center mt-4"> </div>
{santri?.links?.map((link, index) => ( <div className="flex justify-center mt-4">
{santri.current_page > 1 && (
<button className="btn btn-sm mx-1" onClick={() => (window.location.href = `?page=1`)}>
&laquo;
</button>
)}
{santri.current_page > 3 && (
<span className="btn btn-sm mx-1 pointer-events-none">...</span>
)}
{Array.from({ length: santri.last_page }, (_, i) => i + 1)
.filter((page) =>
page === 1 ||
page === santri.last_page ||
Math.abs(page - santri.current_page) <= 2
)
.map((page) => (
<button <button
key={index} key={page}
className={`px-4 py-2 mx-1 ${link.active ? "bg-blue-500 text-white" : "bg-gray-200" className={`btn btn-sm mx-1 ${santri.current_page === page ? 'btn-active btn-primary text-white' : ''}`}
}`}
onClick={() => { onClick={() => {
if (link.url) window.location.href = link.url; window.location.href = `?page=${page}`;
}} }}
disabled={!link.url}
> >
{link.label.replace('&laquo;', '«').replace('&raquo;', '»')} {page}
</button> </button>
))} ))}
</div>
{santri.current_page < santri.last_page - 2 && (
<span className="btn btn-sm mx-1 pointer-events-none">...</span>
)}
{santri.current_page < santri.last_page && (
<button className="btn btn-sm mx-1" onClick={() => (window.location.href = `?page=${santri.last_page}`)}>
&raquo;
</button>
)}
</div> </div>
</div> </div>
</div> </div>
@ -205,7 +219,7 @@ export default function ManualPayment({ santri, fields, options }) {
<tr key={`${idx}-${detail.id}`}> <tr key={`${idx}-${detail.id}`}>
<td>{detail.payment_type?.payment_type}</td> <td>{detail.payment_type?.payment_type}</td>
<td className="text-center">{detail.payment_year}</td> <td className="text-center">{detail.payment_year}</td>
<td className="text-center">{detail.payment_month}</td> <td className="text-center">{monthNames[detail.payment_month - 1] || detail.payment_month}</td>
<td className="text-center">{detail.penalty}</td> <td className="text-center">{detail.penalty}</td>
<td className="text-center"> <td className="text-center">
<span className={`p-2 rounded-md ${detail.status === 'paid' ? 'bg-green-500 text-white' : 'bg-red-600 text-white'}`}> <span className={`p-2 rounded-md ${detail.status === 'paid' ? 'bg-green-500 text-white' : 'bg-red-600 text-white'}`}>
@ -215,6 +229,7 @@ export default function ManualPayment({ santri, fields, options }) {
</tr> </tr>
)) ))
)} )}
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@ -4,34 +4,15 @@ import ModalInput from '@/Components/ModalInput';
import DeleteButton from '@/Components/DeleteButton'; import DeleteButton from '@/Components/DeleteButton';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { setPageTitle } from '@/Components/features/common/headerSlice'; import { setPageTitle } from '@/Components/features/common/headerSlice';
import { usePage } from '@inertiajs/react';
import Swal from "sweetalert2"
export default function PaymentType({ paymentType, fields }) { export default function PaymentType({ paymentType, fields }) {
const [selectedPaymentType, setSelectedPaymentType] = useState(null); const [selectedPaymentType, setSelectedPaymentType] = useState(null);
const [isDeleteOpen, setDeleteOpen] = useState(false); const [isDeleteOpen, setDeleteOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [filteredPaymentTypes, setFilteredPaymentTypes] = useState([]); const [filteredPaymentTypes, setFilteredPaymentTypes] = useState([]);
const { flash } = usePage().props;
const dispatch = useDispatch(); const dispatch = useDispatch();
useEffect(() => {
if (flash.success) {
Swal.fire({
icon: 'success',
title: 'Success',
text: flash.success
});
} else if (flash.error) {
Swal.fire({
icon: 'success',
title: 'Success',
text: flash.success
});
}
}, [flash]);
useEffect(() => { useEffect(() => {
dispatch(setPageTitle("Data Tipe Pembayaran")); dispatch(setPageTitle("Data Tipe Pembayaran"));
}, [dispatch]); }, [dispatch]);

View File

@ -0,0 +1,239 @@
// Import wajib
import React, { useState, useEffect } from 'react';
import { Head } from '@inertiajs/react';
import { useDispatch } from 'react-redux';
import { setPageTitle } from '@/Components/features/common/headerSlice';
import { CurrencyDollarIcon } from '@heroicons/react/24/outline';
import { Inertia } from '@inertiajs/inertia';
export default function Transaction({ transaction }) {
const [searchTerm, setSearchTerm] = useState('');
const [filteredSantri, setFilteredSantri] = useState([]);
const [selectedSantri, setSelectedSantri] = useState(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedMonth, setSelectedMonth] = useState(new Date().getMonth() + 1);
const [selectedYear, setSelectedYear] = useState(new Date().getFullYear());
const dispatch = useDispatch();
const bulanIndo = [
'Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni',
'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'
];
useEffect(() => {
dispatch(setPageTitle("Data Transaksi Santri"));
}, []);
useEffect(() => {
if (transaction?.data) {
const filtered = transaction.data.filter(item => {
const matchSearch = item.nama.toLowerCase().includes(searchTerm.toLowerCase()) || item.nis?.toString().includes(searchTerm);
const hasPaymentThisMonthYear = item.payments?.some(payment =>
payment.detail_payments?.some(d =>
d.payment_month === selectedMonth &&
(selectedYear === 'all' || d.payment_year === selectedYear)
)
);
return matchSearch && hasPaymentThisMonthYear;
});
setFilteredSantri(filtered);
}
}, [searchTerm, selectedMonth, selectedYear, transaction]);
const closeModal = () => {
setIsModalOpen(false);
setSelectedSantri(null);
};
return (
<div>
<Head title="Transaksi Santri" />
<div className="card bg-base-100 shadow-xl">
<div className="card-body">
<div className="flex items-center mb-6">
<div className="bg-gradient-to-tr from-blue-400 to-blue-600 p-3 rounded-lg mr-3">
<CurrencyDollarIcon className="h-6 w-6 text-white" />
</div>
<h1 className="text-2xl font-bold">Data Transaksi User</h1>
</div>
<div className="flex justify-between items-center mb-4 gap-2">
<input
type="text"
placeholder="Cari Santri..."
className="input input-bordered"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<select
className="select select-bordered"
value={selectedMonth}
onChange={(e) => setSelectedMonth(Number(e.target.value))}
>
{bulanIndo.map((b, i) => <option key={i} value={i + 1}>{b}</option>)}
</select>
<select
className="select select-bordered"
value={selectedYear}
onChange={(e) => setSelectedYear(e.target.value === 'all' ? 'all' : Number(e.target.value))}
>
<option value="all">Semua Tahun</option>
{Array.from({ length: 5 }, (_, i) => {
const year = new Date().getFullYear() - 2 + i;
return <option key={year} value={year}>{year}</option>
})}
</select>
</div>
{/* TABEL SANTRI */}
<div className="overflow-x-auto">
<table className="table table-zebra w-full">
<thead>
<tr>
<th>Nama</th>
<th>Detail</th>
</tr>
</thead>
<tbody>
{filteredSantri.length > 0 ? (
filteredSantri.map((item, i) => (
<tr key={i}>
<td>
<div className="flex items-center space-x-3">
<div className="avatar">
<div className="mask mask-squircle w-12 h-12">
<img src={`https://ui-avatars.com/api/?name=${item.nama}`} alt="Avatar" />
</div>
</div>
<div>
<div className="font-bold">{item.nama}</div>
<div className="text-sm opacity-50">{item.level === 1 ? 'Admin' : 'Santri'}</div>
</div>
</div>
</td>
<td>
<button
className='btn btn-success text-white'
onClick={() => {
setSelectedSantri(item);
setIsModalOpen(true);
}}
>
Detail
</button>
</td>
</tr>
))
) : (
<tr>
<td colSpan="2" className="text-center">Tidak ada data santri.</td>
</tr>
)}
</tbody>
</table>
</div>
{/* PAGINATION */}
<div className="flex justify-center mt-4">
{transaction.current_page > 1 && (
<button
className="btn btn-sm mx-1"
onClick={() =>
Inertia.visit(`${window.location.pathname}?page=${transaction.current_page - 1}`)
}
>&laquo;</button>
)}
{Array.from({ length: transaction.last_page }, (_, i) => i + 1)
.filter(page => page === 1 || page === transaction.last_page || Math.abs(page - transaction.current_page) <= 2)
.map((page, i, arr) => {
const prev = arr[i - 1];
const showDots = prev && page - prev > 1;
return (
<React.Fragment key={page}>
{showDots && <span className="btn btn-sm mx-1 pointer-events-none">...</span>}
<button
className={`btn btn-sm mx-1 ${transaction.current_page === page ? 'btn-active btn-primary text-white' : ''}`}
onClick={() =>
Inertia.visit(`${window.location.pathname}?page=${page}`)
}
>
{page}
</button>
</React.Fragment>
);
})}
{transaction.current_page < transaction.last_page && (
<button
className="btn btn-sm mx-1"
onClick={() =>
Inertia.visit(`${window.location.pathname}?page=${transaction.current_page + 1}`)
}
>&raquo;</button>
)}
</div>
</div>
</div>
{/* MODAL DETAIL TRANSAKSI */}
{isModalOpen && selectedSantri && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50" onClick={closeModal}>
<div className="bg-base-100 rounded-2xl shadow-xl p-6 w-11/12 max-w-3xl relative" onClick={e => e.stopPropagation()}>
<button onClick={closeModal} className="absolute top-3 right-3 text-red-500 hover:text-red-700"></button>
<h2 className="text-2xl font-bold mb-4 text-center text-gray-800">
Detail Transaksi <span className="text-blue-600">{selectedSantri.nama}</span>
</h2>
<div className="space-y-4 max-h-[70vh] overflow-y-auto">
{selectedSantri.payments
?.map(p => ({
...p,
details: p.detail_payments?.filter(d => d.payment_month === selectedMonth && (selectedYear === 'all' || d.payment_year === selectedYear))
}))
.filter(p => p.details?.length)
.map((payment, idx) => {
const total = payment.details.reduce((sum, d) => sum + Number(d.amount) + Number(d.penalty || 0), 0);
return (
<div key={idx} className="bg-base-200 p-4 rounded-xl shadow-md border border-gray-200">
<div className="flex flex-wrap justify-between items-center text-sm mb-2">
<div><strong>Order ID:</strong> {payment.order_id || '-'}</div>
<div className={payment.payment_status === 'success' ? 'bg-green-600 text-white px-2 py-1 rounded' : 'bg-orange-600 text-white px-2 py-1 rounded'}>
{payment.payment_status}
</div>
</div>
<table className="table w-full text-sm">
<thead>
<tr className="bg-gray-100 text-gray-700">
<th>Status</th>
<th>Jumlah</th>
<th>Jenis</th>
<th>Tipe</th>
<th>Denda</th>
<th>Total</th>
</tr>
</thead>
<tbody>
{payment.details.map((detail, i) => (
<tr key={i}>
<td>{detail.status}</td>
<td>Rp {Number(detail.amount).toLocaleString()}</td>
<td>{payment.transaction_type === 'payment' ? 'Pembayaran' : 'Topup'}</td>
<td>{detail.payment_type?.payment_type || '-'}</td>
<td>Rp {Number(detail.penalty || 0).toLocaleString()}</td>
<td>Rp {(Number(detail.amount) + Number(detail.penalty || 0)).toLocaleString()}</td>
</tr>
))}
</tbody>
</table>
<div className="text-right mt-2 font-bold text-black">
Total: Rp {total.toLocaleString()}
</div>
</div>
);
})}
</div>
</div>
</div>
)}
</div>
);
}

View File

@ -18,9 +18,9 @@ export default function ManualPayment({ wallet }) {
// console.log(santri.id) // console.log(santri.id)
useEffect(() => { useEffect(() => {
if (wallet) { if (wallet?.data) {
setFilteredSantri( setFilteredSantri(
wallet.filter(item => wallet.data.filter(item =>
item.nama.toLowerCase().includes(searchTerm.toLowerCase()) || item.nama.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.nis.toString().includes(searchTerm) item.nis.toString().includes(searchTerm)
) )
@ -100,7 +100,7 @@ export default function ManualPayment({ wallet }) {
</div> </div>
</td> </td>
<td> <td>
{item.wallet ? item.wallet.saldo : 'Tidak ada saldo'} Rp. {item.wallet ? item.wallet.saldo : 'Tidak ada saldo'}
</td> </td>
</tr> </tr>
)) ))
@ -112,20 +112,53 @@ export default function ManualPayment({ wallet }) {
</tbody> </tbody>
</table> </table>
<div className="flex justify-center mt-4"> <div className="flex justify-center mt-4">
{wallet?.links?.map((link, index) => ( {wallet.current_page > 1 && (
<button <button
key={index} className="btn btn-sm mx-1"
className={`px-4 py-2 mx-1 ${link.active ? "bg-blue-500 text-white" : "bg-gray-200" onClick={() => (window.location.href = `?page=1`)}
}`}
onClick={() => {
if (link.url) window.location.href = link.url;
}}
disabled={!link.url}
> >
{link.label.replace('&laquo;', '«').replace('&raquo;', '»')} &laquo;
</button> </button>
))} )}
{wallet.current_page > 3 && (
<span className="btn btn-sm mx-1 pointer-events-none">...</span>
)}
{Array.from({ length: wallet.last_page }, (_, i) => i + 1)
.filter(
(page) =>
page === 1 ||
page === wallet.last_page ||
Math.abs(page - wallet.current_page) <= 2
)
.map((page) => (
<button
key={page}
className={`btn btn-sm mx-1 ${wallet.current_page === page ? 'btn-active btn-primary text-white' : ''
}`}
onClick={() => {
window.location.href = `?page=${page}`;
}}
>
{page}
</button>
))}
{wallet.current_page < wallet.last_page - 2 && (
<span className="btn btn-sm mx-1 pointer-events-none">...</span>
)}
{wallet.current_page < wallet.last_page && (
<button
className="btn btn-sm mx-1"
onClick={() => (window.location.href = `?page=${wallet.last_page}`)}
>
&raquo;
</button>
)}
</div> </div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,13 +1,10 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Head } from '@inertiajs/react'; import { Head, usePage } from '@inertiajs/react';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { setPageTitle } from '@/Components/features/common/headerSlice'; import { setPageTitle } from '@/Components/features/common/headerSlice';
import ModalInput from '@/Components/ModalInput'; import ModalInput from '@/Components/ModalInput';
import DeleteButton from '@/Components/DeleteButton'; import DeleteButton from '@/Components/DeleteButton';
import { UserGroupIcon } from '@heroicons/react/24/outline' import { UserGroupIcon } from '@heroicons/react/24/outline';
import { usePage } from '@inertiajs/react';
import Swal from "sweetalert2"
export default function IndexSantri({ santri, fields, options }) { export default function IndexSantri({ santri, fields, options }) {
const [selectedSantri, setSelectedSantri] = useState(null); const [selectedSantri, setSelectedSantri] = useState(null);
@ -18,38 +15,21 @@ export default function IndexSantri({ santri, fields, options }) {
const { flash } = usePage().props; const { flash } = usePage().props;
const openDeleteModal = (item) => { const openDeleteModal = (item) => {
setSelectedSantri(item) setSelectedSantri(item);
setDeleteOpen(true) setDeleteOpen(true);
} };
useEffect(() => {
if (flash.success) {
Swal.fire({
icon: 'success',
title: 'Success',
text: flash.success
});
} else if (flash.error) {
Swal.fire({
icon: 'success',
title: 'Success',
text: flash.success
});
}
}, [flash]);
useEffect(() => { useEffect(() => {
dispatch(setPageTitle("Data Santri")); dispatch(setPageTitle("Data Santri"));
}, [dispatch]); }, [dispatch]);
useEffect(() => { useEffect(() => {
if (santri) { if (santri?.data) {
setFilteredSantri( const filtered = santri.data.filter(item =>
santri.filter(item => item.nama.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.nama.toLowerCase().includes(searchTerm.toLowerCase()) || item.nis.toString().includes(searchTerm)
item.nis.toString().includes(searchTerm)
)
); );
setFilteredSantri(filtered);
} }
}, [searchTerm, santri]); }, [searchTerm, santri]);
@ -69,13 +49,9 @@ export default function IndexSantri({ santri, fields, options }) {
<h1 className="text-2xl font-bold">Data Santri</h1> <h1 className="text-2xl font-bold">Data Santri</h1>
<div className="ml-auto"> <div className="ml-auto">
<span className="text-gray-700">Overview</span> <span className="text-gray-700">Overview</span>
<span className="ml-2 text-gray-500">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 inline" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</span>
</div> </div>
</div> </div>
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
<div className="form-control"> <div className="form-control">
<div className="input-group"> <div className="input-group">
@ -110,53 +86,55 @@ export default function IndexSantri({ santri, fields, options }) {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{filteredSantri.length > 0 ? filteredSantri.map((item, i) => ( {filteredSantri.length > 0 ? (
<tr key={i}> filteredSantri.map((item, i) => (
<td> <tr key={i}>
<div className="flex items-center space-x-3"> <td>
<div className="avatar"> <div className="flex items-center space-x-3">
<div className="mask mask-squircle w-12 h-12"> <div className="avatar">
<img src={`https://ui-avatars.com/api/?name=${item.nama}`} alt="Avatar" /> <div className="mask mask-squircle w-12 h-12">
<img src={`https://ui-avatars.com/api/?name=${item.nama}`} alt="Avatar" />
</div>
</div>
<div>
<div className="font-bold">{item.nama}</div>
<div className="text-sm opacity-50">{item.level === 1 ? 'Admin' : 'User'}</div>
</div> </div>
</div> </div>
<div> </td>
<div className="font-bold">{item.nama}</div> <td>{item.nis}</td>
<div className="text-sm opacity-50">{item.level == 1 ? 'Admin' : 'User'}</div> <td>
<div className={`badge ${item.status_santri === 'aktif' ? 'badge-success' : 'badge-warning'} gap-2 text-white`}>
{item.status_santri || "Open"}
</div> </div>
</div> </td>
</td> <td>{item.alamat || "-"}</td>
<td>{item.nis}</td> <td>
<td> <div className="flex space-x-2">
<div className={`badge ${item.status_santri === 'aktif' ? 'badge-success' : 'badge-warning'} gap-2 text-white`}> <button
{item.status_santri || "Open"} className="btn btn-sm btn-primary text-white"
</div> onClick={() => {
</td> setSelectedSantri(item);
<td>{item.alamat || "-"}</td> document.getElementById('modal_input').checked = true;
<td> }}
<div className="flex space-x-2"> >
<button <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
className="btn btn-sm btn-primary text-white" <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
onClick={() => { </svg>
setSelectedSantri(item); </button>
document.getElementById('modal_input').checked = true; <button
}} className="btn btn-sm btn-error text-white"
> onClick={() => openDeleteModal(item)}
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> >
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" /> <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
</svg> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</button> </svg>
<button </button>
className="btn btn-sm btn-error text-white" </div>
onClick={() => openDeleteModal(item)} </td>
> </tr>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> ))
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /> ) : (
</svg>
</button>
</div>
</td>
</tr>
)) : (
<tr> <tr>
<td colSpan="5" className="text-center">Tidak ada data santri.</td> <td colSpan="5" className="text-center">Tidak ada data santri.</td>
</tr> </tr>
@ -164,6 +142,58 @@ export default function IndexSantri({ santri, fields, options }) {
</tbody> </tbody>
</table> </table>
</div> </div>
<div className="flex justify-center mt-4">
{santri.current_page > 1 && (
<button
className="btn btn-sm mx-1"
onClick={() => (window.location.href = `?page=${santri.current_page - 1}`)}
>
&laquo;
</button>
)}
{Array.from({ length: santri.last_page }, (_, i) => i + 1)
.filter((page) =>
page === 1 ||
page === santri.last_page ||
Math.abs(page - santri.current_page) <= 2
)
.map((page, index, array) => {
const prevPage = array[index - 1];
const showEllipsis = prevPage && page - prevPage > 1;
return (
<React.Fragment key={page}>
{showEllipsis && (
<span className="btn btn-sm mx-1 pointer-events-none">...</span>
)}
<button
className={`btn btn-sm mx-1 ${santri.current_page === page
? 'btn-active btn-primary text-white'
: ''
}`}
onClick={() => {
window.location.href = `?page=${page}`;
}}
>
{page}
</button>
</React.Fragment>
);
})}
{/* Tombol terakhir */}
{santri.current_page < santri.last_page && (
<button
className="btn btn-sm mx-1"
onClick={() => (window.location.href = `?page=${santri.current_page + 1}`)}
>
&raquo;
</button>
)}
</div>
</div> </div>
</div> </div>
@ -171,7 +201,7 @@ export default function IndexSantri({ santri, fields, options }) {
isOpen={isDeleteOpen} isOpen={isDeleteOpen}
onClose={() => setDeleteOpen(false)} onClose={() => setDeleteOpen(false)}
item={selectedSantri} item={selectedSantri}
tableName="payment_types" tableName="users"
/> />
</div> </div>
); );

View File

@ -7,8 +7,9 @@ const IndexSantri = lazy(() => import('@/Pages/list-admin/santri/IndexSantri'))
const PaymentType = lazy(() => import('@/Pages/list-admin/payment/PaymentType')) const PaymentType = lazy(() => import('@/Pages/list-admin/payment/PaymentType'))
const ManualPayment = lazy(() => import('@/Pages/list-admin/payment/ManualPayment')) const ManualPayment = lazy(() => import('@/Pages/list-admin/payment/ManualPayment'))
const WalletUser = lazy(() => import('@pages/list-admin/wallet/WalletUser')) const WalletUser = lazy(() => import('@pages/list-admin/wallet/WalletUser'))
const Transaction = lazy(() => import('@pages/list-admin/payment/Transaction'))
console.log(route('dashboard')) // console.log(route('dashboard'))
const routes = [ const routes = [
{ {
@ -30,6 +31,10 @@ const routes = [
{ {
path: route('walletUser'), path: route('walletUser'),
componenet: WalletUser componenet: WalletUser
},
{
path: route('transaction'),
component: Transaction
} }
] ]

View File

@ -70,6 +70,11 @@ const routes = [
path: '/wallet-user', path: '/wallet-user',
icon: <ArrowRightIcon className={submenuIconClasses} />, icon: <ArrowRightIcon className={submenuIconClasses} />,
name: 'Wallet User' name: 'Wallet User'
},
{
path: '/transaksi-user',
icon: <ArrowRightIcon className={submenuIconClasses} />,
name: 'Transaksi User'
} }
], ],
}, },

View File

@ -6,6 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title inertia>{{ config('app.name', 'GogoSantri') }}</title> <title inertia>{{ config('app.name', 'GogoSantri') }}</title>
<link rel="icon" href="{{ asset('assets/gogoSantri.png') }}">
<!-- Fonts --> <!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net"> <link rel="preconnect" href="https://fonts.bunny.net">

View File

@ -10,12 +10,14 @@
use App\Http\Controllers\HomeController; use App\Http\Controllers\HomeController;
use Inertia\Inertia; use Inertia\Inertia;
Route::get('/', function () { Route::middleware('guest')->group(function () {
return redirect()->route('login'); Route::get('/', function () {
}); return redirect()->route('login');
});
Route::get('/login', function () { Route::get('/login', function () {
return redirect()->route('login'); return redirect()->route('login');
});
}); });
Route::middleware('auth')->group(function () { Route::middleware('auth')->group(function () {
@ -46,9 +48,7 @@
// wallet // wallet
Route::get('/wallet-user', [WalletController::class, 'walletUser'])->name('walletUser'); Route::get('/wallet-user', [WalletController::class, 'walletUser'])->name('walletUser');
Route::get('profile-settings', function () { Route::get('/transaksi-user', [PaymentController::class, 'transaction'])->name('transaction');
return Inertia::render('ProfileSettings');
});
}); });
require __DIR__ . '/auth.php'; require __DIR__ . '/auth.php';