payment phase 2

This commit is contained in:
alealien666 2025-05-08 02:25:41 +07:00
parent 1238ca53fd
commit 260942462c
49 changed files with 1634 additions and 464 deletions

View File

@ -37,10 +37,9 @@ public function indexManualPayment(cekDenda $cekDenda, GenerateMonthlyBill $gene
'penalty' => $penalty,
'bill' => $bill,
'fields' => [
'nis' => 'text',
'nama' => 'text',
'status_santri' => 'text',
'role_santri' => 'text',
'nis' => ['type' => 'text', 'readonly' => true],
'nama' => ['type' => 'text', 'readonly' => true],
'status_santri' => ['type' => 'text', 'readonly' => true],
],
'options' => [
'payment_type' => $paymentType,
@ -54,103 +53,118 @@ public function manualPayment(Request $request, $paymentId)
{
try {
DB::beginTransaction();
DB::enableQueryLog();
$range = $request->input('range');
$payment = Payment::find($paymentId);
$userId = User::findOrFail($request->id);
$range = (int) $request->input('range');
$userId = $request->id;
$typeId = $request->input('type_id');
if (!$payment) {
Payment::create([
'payment_status' => 'pending',
'amount_payment' => 0,
'bank' => null,
'no_va' => null,
'expired_at' => null,
'user_id' => $userId
]);
}
$existingPayment = Payment::where('user_id', $userId)
->where('payment_status', 'pending')
->first();
$unpaidDetails = DetailPayment::where('payment_id', $paymentId)
$unpaidDetails = $existingPayment
? DetailPayment::where('payment_id', $existingPayment->id)
->where('status', 'unpaid')
->orderBy('payment_year', 'asc')
->orderBy('payment_month', 'asc')
->get();
->orderBy('payment_year')
->orderBy('payment_month')
->get()
: collect();
$jumlahUnpaid = $unpaidDetails->count();
$totalAmount = 0;
if ($existingPayment && $unpaidDetails->count() > 0) {
$totalAmount = 0;
$toPay = $unpaidDetails->take($range);
$jumlahUnpaid = $toPay->count();
Log::info("Jumlah Unpaid: $jumlahUnpaid | Range: $range");
if ($jumlahUnpaid > 0) {
foreach ($unpaidDetails->take($range) as $detail) {
foreach ($toPay as $detail) {
$nominal = PaymentType::where('id', $detail->type_id)->value('nominal') ?? 0;
$penalty = $detail->penalty ?? 0;
$total = $nominal + $penalty;
Log::info("Update DetailPayment ID {$detail->id} | Total: $total");
$detail->update([
'status' => 'paid',
'amount' => $total,
'amount' => $nominal,
'penalty' => $penalty,
]);
$totalAmount += $total;
}
}
$sisa = max(0, $range - $jumlahUnpaid);
dd("Sisa pembayaran baru yang perlu dibuat: $sisa");
$sisa = $range - $jumlahUnpaid;
$bulan = $unpaidDetails->last()?->payment_month ?? now()->month;
$tahun = $unpaidDetails->last()?->payment_year ?? now()->year;
if ($sisa > 0) {
$latestUnpaid = $unpaidDetails->last();
$bulanTerakhir = $latestUnpaid ? $latestUnpaid->payment_month : now()->month;
$tahunTerakhir = $latestUnpaid ? $latestUnpaid->payment_year : now()->year;
$typeId = $latestUnpaid ? $latestUnpaid->type_id : PaymentType::first()->id;
$nominal = PaymentType::where('id', $typeId)->value('nominal') ?? 0;
for ($i = 1; $i <= $sisa; $i++) {
$bulanTerakhir++;
if ($bulanTerakhir > 12) {
$bulanTerakhir = 1;
$tahunTerakhir++;
for ($i = 0; $i < $sisa; $i++) {
$bulan++;
if ($bulan > 12) {
$bulan = 1;
$tahun++;
}
$totalAmount += $nominal;
Log::info("Buat DetailPayment baru untuk bulan: $bulanTerakhir, tahun: $tahunTerakhir");
$nominal = PaymentType::where('id', $typeId)->value('nominal') ?? 0;
DetailPayment::create([
'payment_id' => $paymentId,
'payment_month' => $bulanTerakhir,
'payment_year' => $tahunTerakhir,
'payment_id' => $existingPayment->id,
'payment_month' => $bulan,
'payment_year' => $tahun,
'amount' => $nominal,
'penalty' => 0,
'status' => 'paid',
'type_id' => $typeId,
]);
$totalAmount += $nominal;
}
$existingPayment->update([
'amount_payment' => DetailPayment::where('payment_id', $existingPayment->id)->sum('amount'),
'payment_status' => DetailPayment::where('payment_id', $existingPayment->id)->where('status', 'unpaid')->exists() ? 'pending' : 'success',
]);
DB::commit();
return redirect()->back()->with('success', 'Pembayaran berhasil menggunakan data existing');
}
$newPayment = Payment::create([
'payment_status' => 'success',
'amount_payment' => 0,
'bank' => null,
'no_va' => null,
'expired_at' => null,
'user_id' => $userId,
]);
$bulan = now()->month;
$tahun = now()->year;
$nominal = PaymentType::where('id', $typeId)->value('nominal') ?? 0;
$totalAmount = 0;
for ($i = 0; $i < $range; $i++) {
DetailPayment::create([
'payment_id' => $newPayment->id,
'payment_month' => $bulan,
'payment_year' => $tahun,
'amount' => $nominal,
'penalty' => 0,
'status' => 'paid',
'type_id' => $typeId,
]);
$totalAmount += $nominal;
$bulan++;
if ($bulan > 12) {
$bulan = 1;
$tahun++;
}
}
$totalAmountFinal = DetailPayment::where('payment_id', $paymentId)->sum('amount');
Log::info("Total pembayaran akhir: $totalAmountFinal");
$payment->update([
'amount_payment' => $totalAmountFinal,
'payment_status' => DetailPayment::where('payment_id', $paymentId)
->where('status', 'unpaid')
->exists() ? 'pending' : 'success'
$newPayment->update([
'amount_payment' => $totalAmount,
]);
Log::info("Update Payment ID $paymentId | amount_payment: $totalAmountFinal");
DB::commit();
// dd($request->all());
return redirect()->back()->with('success', 'Berhasil Melakukan Pembayaran');
dd($newPayment);
return redirect()->back()->with('success', 'Pembayaran baru berhasil dibuat');
} catch (Exception $e) {
DB::rollBack();
return dd('Error:', $e->getMessage());

View File

@ -3,61 +3,74 @@
namespace App\Http\Controllers;
use App\Http\Requests\ProfileUpdateRequest;
use App\Models\User;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Redirect;
use Inertia\Inertia;
use Inertia\Response;
class ProfileController extends Controller
{
/**
* Display the user's profile form.
*/
public function edit(Request $request): Response
{
return Inertia::render('Profile/Edit', [
'mustVerifyEmail' => $request->user() instanceof MustVerifyEmail,
'status' => session('status'),
]);
// $user = User::all();
return Inertia::render('Profile/Profile');
}
/**
* Update the user's profile information.
*/
public function update(ProfileUpdateRequest $request): RedirectResponse
{
$request->user()->fill($request->validated());
if ($request->user()->isDirty('email')) {
$request->user()->email_verified_at = null;
}
$request->user()->save();
return Redirect::route('profile.edit');
}
/**
* Delete the user's account.
*/
public function destroy(Request $request): RedirectResponse
public function updateProfile(Request $request, $id)
{
$request->validate([
'password' => ['required', 'current_password'],
'password' => 'nullable',
'nama' => 'required',
'alamat' => 'required',
'jk' => 'required',
'tanggal_lahir' => 'required|date',
'foto' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048'
], [
'nama.required' => 'wajib mengisi nama santri',
'alamat.required' => 'wajib mengisi alamat santri',
'jk.required' => 'wajib mengisi gender',
'tanggal_lahir.required' => 'wajib mengisi tanggal lahir santri',
'tanggal_lahir.date' => 'tanggal lahir harus dalam format tanggal yang benar',
]);
$user = $request->user();
$santri = User::findOrFail($id);
try {
Auth::logout();
$updateData = [
'nama' => $request->nama,
'alamat' => $request->alamat,
'jk' => $request->jk,
'tanggal_lahir' => $request->tanggal_lahir,
];
$user->delete();
if ($request->hasFile('foto')) {
if ($santri->foto && File::exists(public_path($santri->foto))) {
File::delete(public_path($santri->foto));
}
$request->session()->invalidate();
$request->session()->regenerateToken();
$foto = $request->file('foto');
$fotoName = time() . '_' . $foto->getClientOriginalName();
$foto->move(public_path('fotoSantri'), $fotoName);
$updateData['foto'] = 'fotoSantri/' . $fotoName;
}
if ($request->filled('password')) {
$updateData['password'] = Hash::make($request->password);
}
return Redirect::to('/');
// dd($updateData);
// return $updateData;
$santri->update($updateData);
return redirect()->back()->with('success', 'Data Berhasil Diubah');
} catch (\Throwable $th) {
return redirect()->back()->with('error', 'Gagal memperbarui data: ' . $th->getMessage());
// return $th->getMessage();
}
}
}

View File

@ -21,12 +21,12 @@ public function index()
'nis' => 'text',
'password' => 'password',
'alamat' => 'text',
'no_telp' => 'text',
'status_santri' => 'select',
'role_santri' => 'select',
'jk' => 'select',
'level' => 'select',
'tanggal_lahir' => 'date',
'foto' => 'file'
'foto' => 'file',
],
'options' => [
'status_santri' => ['lulus' => 'Lulus', 'aktif' => 'Aktif'],
@ -46,9 +46,9 @@ public function store(Request $request)
'nama' => 'required',
'alamat' => 'required',
'status_santri' => 'required',
'role_santri' => 'required',
'jk' => 'required',
'tanggal_lahir' => 'required|date',
'no_telp' => 'required',
'foto' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048'
], [
@ -58,10 +58,10 @@ public function store(Request $request)
'nama.required' => 'wajib mengisi nama santri',
'alamat.required' => 'wajib mengisi alamat santri',
'status_santri.required' => 'wajib mengisi status santri',
'role.required' => 'wajib mengisi role santri',
'gender.required' => 'wajib mengisi gender',
'ttl.required' => 'wajib mengisi tanggal lahir santri',
'ttl.date' => 'tanggal lahir harus dalam format tanggal yang benar',
'jk.required' => 'wajib mengisi gender',
'no_telp' => 'wajib mengisi no telp',
'tanggal_lahir.required' => 'wajib mengisi tanggal lahir santri',
'tanggal_lahir.date' => 'tanggal lahir harus dalam format tanggal yang benar',
]);
$fotoPath = null;
@ -82,13 +82,13 @@ public function store(Request $request)
'nama' => $request->nama,
'alamat' => $request->alamat,
'status_santri' => $request->status_santri,
'role_santri' => $request->role_santri,
'jk' => $request->jk,
'tanggal_lahir' => $request->tanggal_lahir,
'no_telp' => $request->no_telp,
'foto' => $fotoPath
]);
// dd($santri);
// dd($santri);
return redirect()->back()->with('success', 'Data berhasil ditambahkan');
} catch (\Throwable $th) {
// dd($th->getMessage());
@ -105,7 +105,6 @@ public function update(Request $request, $id)
'nama' => 'required',
'alamat' => 'required',
'status_santri' => 'required',
'role_santri' => 'required',
'jk' => 'required',
'tanggal_lahir' => 'required|date',
'foto' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048'
@ -115,10 +114,9 @@ public function update(Request $request, $id)
'nama.required' => 'wajib mengisi nama santri',
'alamat.required' => 'wajib mengisi alamat santri',
'status_santri.required' => 'wajib mengisi status santri',
'role.required' => 'wajib mengisi role santri',
'gender.required' => 'wajib mengisi gender',
'ttl.required' => 'wajib mengisi tanggal lahir santri',
'ttl.date' => 'tanggal lahir harus dalam format tanggal yang benar',
'jk.required' => 'wajib mengisi gender',
'tanggal_lahir.required' => 'wajib mengisi tanggal lahir santri',
'tanggal_lahir.date' => 'tanggal lahir harus dalam format tanggal yang benar',
]);
try {
@ -129,7 +127,6 @@ public function update(Request $request, $id)
'nama' => $request->nama,
'alamat' => $request->alamat,
'status_santri' => $request->status_santri,
'role_santri' => $request->role_santri,
'jk' => $request->jk,
'tanggal_lahir' => $request->tanggal_lahir,
];

View File

@ -2,64 +2,17 @@
namespace App\Http\Controllers;
use App\Models\Wallet;
use App\Models\User;
use Illuminate\Http\Request;
use Inertia\Inertia;
class WalletController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
public function walletUser()
{
//
}
$wallet = User::with('wallet')->get();
// dd($wallet);
/**
* 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(Wallet $wallet)
{
//
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Wallet $wallet)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, Wallet $wallet)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Wallet $wallet)
{
//
return Inertia::render('list-admin/payment/WalletUser', compact('wallet'));
}
}

View File

@ -18,8 +18,8 @@ public function up(): void
$table->string('level');
$table->string('nama');
$table->string('alamat');
$table->bigInteger('no_telp');
$table->enum('status_santri', ['lulus', 'aktif']);
$table->enum('role_santri', ['santri', 'pengurus']);
$table->enum('jk', ['laki laki', 'perempuan']);
$table->date('tanggal_lahir');
$table->string('foto')->nullable();

View File

@ -14,7 +14,7 @@ public function up(): void
Schema::create('payment_types', function (Blueprint $table) {
$table->id();
$table->string('payment_type');
$table->float('nominal');
$table->decimal('nominal');
$table->timestamps();
});
}

View File

@ -14,7 +14,7 @@ public function up(): void
Schema::create('wallets', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained('users')->onDelete('cascade');
$table->float('saldo');
$table->decimal('saldo');
$table->timestamps();
});
}

View File

@ -13,9 +13,10 @@ public function up(): void
{
Schema::create('wallet_transactions', function (Blueprint $table) {
$table->id();
$table->string('order_id')->unique();
$table->foreignId('wallet_id')->constrained('wallets')->onDelete('cascade');
$table->enum('transaction_type', ['topup', 'payment']);
$table->float('amount');
$table->decimal('amount');
$table->string('description');
$table->enum('status', ['pending', 'success', 'failed']);
$table->timestamps();

View File

@ -13,8 +13,9 @@ public function up(): void
{
Schema::create('payments', function (Blueprint $table) {
$table->id();
$table->string('order_id')->unique()->nullable();
$table->enum('payment_status', ['pending', 'failed', 'success']);
$table->float('amount_payment')->nullable();
$table->decimal('amount_payment')->nullable();
$table->String('bank')->nullable();
$table->string('no_va')->nullable();
$table->dateTime('expired_at')->nullable();

View File

@ -15,8 +15,8 @@ public function up(): void
$table->id();
$table->foreignId('payment_id')->constrained('payments')->onDelete('cascade');
$table->enum('status', ['paid', 'unpaid']);
$table->float('amount')->nullable();
$table->float('penalty')->nullable();
$table->decimal('amount')->nullable();
$table->decimal('penalty')->nullable();
$table->integer('payment_month');
$table->integer('payment_year');
$table->foreignId('type_id')->constrained('payment_types')->onDelete('cascade');

View File

@ -22,22 +22,22 @@ public function run(): void
'nama' => 'Admin',
'alamat' => 'Jl. MH. Thamrin No. 10, Ajong',
'status_santri' => 'aktif',
'role_santri' => 'santri',
'jk' => 'laki laki',
'tanggal_lahir' => '2005-08-15',
'foto' => null
'foto' => null,
'no_telp' => '80989080980'
],
[
'nis' => '0987654321',
'password' => Hash::make('pitik123'),
'level' => 1,
'level' => 2,
'nama' => 'Ahmad Kasim',
'alamat' => 'Jl. Pesantren No. 10, Jakarta',
'status_santri' => 'aktif',
'role_santri' => 'santri',
'jk' => 'laki laki',
'tanggal_lahir' => '2003-08-15',
'foto' => null
'foto' => null,
'no_telp' => '80989080980'
]
],);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 411 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 979 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 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: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 KiB

View File

@ -8,7 +8,7 @@ import SunIcon from '@heroicons/react/24/outline/SunIcon'
import { openRightDrawer } from './features/common/rightDrawerSlice'
import { RIGHT_DRAWER_TYPES } from '../../../public/utils/globalConstantUtil'
import { Link } from '@inertiajs/react'
import { useForm } from '@inertiajs/react'
import { useForm, usePage } from '@inertiajs/react'
function Header() {
const dispatch = useDispatch()
@ -18,6 +18,8 @@ function Header() {
(window.matchMedia('(prefers-color-scheme: dark)').matches ? "dark" : "light")
);
const { auth } = usePage().props
useEffect(() => {
themeChange(false);
document.documentElement.setAttribute("data-theme", theme);
@ -62,11 +64,13 @@ function Header() {
<div className="dropdown dropdown-end ml-4">
<label tabIndex={0} className="btn btn-ghost btn-circle avatar">
<div className="w-10 rounded-full">
<img src={"/public/fotoSantri/no-pic.png"} alt="profile" />
<img src={auth.user.foto ? `${auth.user.foto}` : `/fotoSantri/no-pic.png`} alt="profile" />
</div>
</label>
<ul tabIndex={0} className="menu menu-compact dropdown-content mt-3 p-2 shadow bg-base-100 rounded-box w-52">
<li><Link href="/app/settings-profile">Profile Settings</Link></li>
<li className="ml-3" >Welcome, {auth.user.nama}</li>
<div className="divider mt-0 mb-0"></div>
<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>

View File

@ -13,15 +13,15 @@ function LeftSidebar() {
return (
<div className="drawer-side z-30">
<label htmlFor="left-sidebar-drawer" className="drawer-overlay"></label>
<ul className="menu pt-2 w-80 bg-base-100 min-h-full text-base-content">
<ul className="menu pt-2 w-64 bg-base-100 min-h-full text-base-content">
<button
className="btn btn-ghost bg-base-300 btn-circle z-50 top-0 right-0 mt-4 mr-2 absolute lg:hidden"
onClick={close}
>
<XMarkIcon className="h-5 w-5" />
</button>
<li className="mb-2 font-semibold text-xl">
<Link href="/dashboard">GoGoSantri</Link>
<li className="mb-2 font-semibold">
<Link href="/dashboard" className='flex justify-center'><img src="/assets/gogoSantri.png" alt="pp" width={100} /></Link>
</li>
{routes.map((route, k) => (
<li key={k}>

View File

@ -28,8 +28,8 @@ const ModalInput = ({ fields, tableName, options, initialData, onClose, showPaym
if (!selectedPayments.includes(value)) {
setSelectedPayments([...selectedPayments, value]);
const nominal = options.payment_nominal?.[value] || 0;
const penalty = options.payment_penalty?.[value] || 0;
const nominal = parseInt(options.payment_nominal?.[value]) || 0;
const penalty = parseInt(options.payment_penalty?.[value]) || 0;
setPaymentDetails({
...paymentDetails,
@ -69,177 +69,203 @@ const ModalInput = ({ fields, tableName, options, initialData, onClose, showPaym
};
const handleSubmit = (e) => {
e.preventDefault()
// console.log("tableName:", tableName);
e.preventDefault();
const formDataObj = new FormData()
const formDataObj = new FormData();
Object.keys(formData).forEach((key) => {
if (key === 'foto' && !(formData[key] instanceof File)) {
return
return;
}
formDataObj.append(key, formData[key])
})
formDataObj.append(key, formData[key]);
});
if (initialData) {
Inertia.post(`/update${tableName}/${initialData.id}`, formDataObj, {
forceFormData: true,
onError: (errors) => setErrors(errors),
onSuccess: () => {
document.getElementById('modal_input').checked = false
setFormData({})
setErrors({})
onClose({})
}
})
} else {
Inertia.post(`/add${tableName}`, formDataObj, {
forceFormData: true,
onError: (errors) => setErrors(errors),
onSuccess: () => {
document.getElementById('modal_input').checked = false
setFormData({})
setErrors({})
}
})
if (showPayments) {
formDataObj.append('payment_details', JSON.stringify(paymentDetails));
}
}
const url = initialData
? `/update${tableName}/${initialData.id}`
: `/add${tableName}`;
Inertia.post(url, formDataObj, {
forceFormData: true,
onError: (errors) => setErrors(errors),
onSuccess: () => {
document.getElementById('modal_input').checked = false;
setFormData({});
setErrors({});
onClose({});
},
});
};
return (
<div>
<input type="checkbox" id="modal_input" className="modal-toggle" />
<div className="modal" role="dialog">
<div className="modal-box">
<h2 className="font-bold text-lg text-center mb-5">
{initialData ? "Edit Data" : "Tambah Data"}
<div className="modal-box max-w-lg">
<h2 className="font-bold text-xl text-center mb-6 border-b pb-2">
Form Data
</h2>
<form onSubmit={handleSubmit} encType="multipart/form-data">
{Object.keys(fields).map((field) => (
<div key={field} className="mb-2">
<label className="input input-bordered input-secondary flex items-center gap-2">
{field.replace("_", " ")}
{fields[field] === "select" ? (
<form onSubmit={handleSubmit} encType="multipart/form-data" className="space-y-4">
{Object.entries(fields).map(([field, config]) => {
const type = typeof config === "string" ? config : config.type;
const readOnly = typeof config === "object" && config.readonly;
return (
<div key={field} className="form-control">
<label className="label font-semibold capitalize">{field.replace("_", " ")}</label>
{type === "select" ? (
<select
name={field}
value={formData[field] || ""}
onChange={handleChange}
className="select select-bordered w-full select-secondary"
className="select select-info focus:outline-none focus:ring-1 ring-info w-full"
>
<option disabled value="">
Pilih {field.replace("_", " ")}
</option>
{options[field] && Object.entries(options[field]).map(([key, value]) => (
<option key={key} value={key}>{value}</option>
))}
{options[field] &&
Object.entries(options[field]).map(([key, value]) => (
<option key={key} value={key}>
{value}
</option>
))}
</select>
) : fields[field] === "file" ? (
) : type === "file" ? (
<input
type="file"
name={field}
onChange={handleChange}
className="file-input file-input-bordered w-full file-input-secondary"
className="file-input file-input-info focus:outline-none focus:ring-1 ring-info w-full"
/>
) : type === "password" ? (
<input
type="password"
name={field}
onChange={handleChange}
value={formData[field] || ""}
className="input input-info focus:outline-none focus:ring-1 ring-info w-full"
placeholder={
initialData
? 'Kosongkan jika tidak ingin mengubah password'
: 'Masukkan password broh'
}
/>
) : (
fields[field] === "password" ? (
<input
type="password"
name={field}
onChange={handleChange}
value=""
className="grow border-none focus:ring-0"
placeholder="Kosongkan jika tidak ingin mengubah password"
/>
) : (
<input
type={fields[field]}
name={field}
value={formData[field] || ""}
onChange={handleChange}
className="grow border-none focus:ring-0"
placeholder={`Masukkan ${field.replace("_", " ")}`}
/>
)
<input
type={type}
name={field}
value={formData[field] || ""}
onChange={handleChange}
readOnly={readOnly}
className="input input-info focus:outline-none focus:ring-1 ring-info w-full"
placeholder={`Masukkan ${field.replace("_", " ")}`}
/>
)}
</label>
{errors[field] && <p className="text-red-500 text-sm">{errors[field]}</p>}
</div>
))}
{errors[field] && (
<p className="text-red-500 text-sm mt-1">{errors[field]}</p>
)}
</div>
);
})}
{showPayments && (
<div>
<div className="mb-2">
<label className="input input-bordered input-secondary flex items-center gap-2">
Payment Type
<select onChange={handlePaymentChange} className="select select-bordered w-full select-secondary">
<option disabled value="">Pilih Payment Type</option>
{options.payment_type &&
Object.entries(options.payment_type).map(([key, value]) => (
<option key={key} value={key}>{value}</option>
))}
</select>
</label>
<div className="space-y-4 mt-4">
<div className="form-control">
<label className="label font-semibold">Payment Type</label>
<select
onChange={handlePaymentChange}
className="select select-info focus:outline-none focus:ring-1 ring-info w-full"
>
<option disabled value="">
Pilih Payment Type
</option>
{options.payment_type &&
Object.entries(options.payment_type).map(([key, value]) => (
<option key={key} value={key}>
{value}
</option>
))}
</select>
</div>
{selectedPayments.map((paymentType) => (
<div key={paymentType} className="mb-2 border p-2 rounded relative">
<div
key={paymentType}
className="border rounded-lg p-4 relative bg-gray-50 shadow-sm"
>
<button
type="button"
onClick={() => handleRemovePayment(paymentType)}
className="absolute top-0 right-0 bg-red-500 text-white px-2 py-1 rounded-full text-xs"
className="absolute top-2 right-2 bg-red-500 hover:bg-red-600 text-white px-2 py-1 rounded-full text-xs"
>
X
</button>
<div className="mb-2">
<label className="input input-bordered input-secondary flex items-center gap-2">
<div className="form-control mb-2">
<label className="label font-semibold">
{options.payment_type[paymentType]} - Range Bulan
<input
type="number"
min="1"
value={paymentDetails[paymentType].range}
onChange={(e) => handleRangeChange(paymentType, parseInt(e.target.value))}
className="grow border-none focus:ring-0" name="range"
/>
</label>
<input
type="number"
min="1"
value={paymentDetails[paymentType].range}
onChange={(e) =>
handleRangeChange(paymentType, parseInt(e.target.value))
}
className="input input-info w-full"
name="range"
/>
</div>
<div className="mb-2">
<label className="input input-bordered input-secondary flex items-center gap-2">
Nominal
<input type="text" value={paymentDetails[paymentType].nominal} readOnly className="grow border-none focus:ring-0 bg-gray-100" />
</label>
<div className="form-control mb-2">
<label className="label font-semibold">Nominal</label>
<input
type="text"
value={paymentDetails[paymentType].nominal}
readOnly
className="input bg-gray-100 w-full"
/>
</div>
<div className="mb-2">
<label className="input input-bordered input-secondary flex items-center gap-2">
Penalty
<input type="text" value={paymentDetails[paymentType].penalty} readOnly className="grow border-none focus:ring-0 bg-gray-100" />
</label>
<div className="form-control mb-2">
<label className="label font-semibold">Penalty</label>
<input
type="text"
value={paymentDetails[paymentType].penalty}
readOnly
className="input bg-gray-100 w-full"
/>
</div>
</div>
))}
<div className="mb-2">
<label className="input input-bordered input-secondary flex items-center gap-2">
Total Amount
<input
type="text"
value={Object.values(paymentDetails).reduce((sum, p) => sum + (p.amount || 0), 0)}
readOnly
className="grow border-none focus:ring-0 bg-gray-100"
/>
</label>
<div className="form-control mt-4">
<label className="label font-semibold">Total Amount</label>
<input
type="text"
value={Object.values(paymentDetails).reduce(
(sum, p) => sum + (p.amount || 0),
0
)}
readOnly
className="input bg-gray-100 w-full"
/>
</div>
</div>
)}
<button type="submit" className="btn btn-secondary w-full mt-3">
{initialData ? "Simpan Perubahan" : "Tambah Data"}
<button type="submit" className="btn btn-info text-white font-bold w-full mt-4">
{initialData ? "submit" : "Tambah Data"}
</button>
</form>
</div>
<label className="modal-backdrop" htmlFor="modal_input">Close</label>
<label className="modal-backdrop" htmlFor="modal_input">
Close
</label>
</div>
</div>
);

View File

@ -0,0 +1,150 @@
import React, { useState, useEffect } from 'react';
import { useForm, usePage } from '@inertiajs/react';
import { useDispatch } from 'react-redux';
import { setPageTitle } from '@/Components/features/common/headerSlice';
import { Head } from '@inertiajs/react';
export default function ProfilePage() {
const { auth } = usePage().props;
const [preview, setPreview] = useState(null);
const id = auth.user.id
const { data, setData, post, processing, errors } = useForm({
nama: auth.user.nama || '',
alamat: auth.user.alamat || '',
no_telp: auth.user.no_telp || '',
jk: auth.user.jk || 'laki laki',
tanggal_lahir: auth.user.tanggal_lahir || '',
password: '',
foto: null
});
const dispatch = useDispatch();
useEffect(() => {
dispatch(setPageTitle("Profile Page"));
}, [dispatch]);
const handleSubmit = (e) => {
e.preventDefault();
post(route('profile.update', id));
};
const handlePhotoChange = (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
setPreview(reader.result);
};
reader.readAsDataURL(file);
setData('foto', file);
}
};
return (
<div className="w-full max-w-4xl mx-auto bg-base-100 shadow-sm rounded-lg overflow-hidden">
<Head title="Profile Page" />
<div className="px-6 py-4 flex items-center justify-between">
<div className="flex items-center">
<div className="relative h-16 w-16 rounded-full overflow-hidden mr-4 border border-gray-200 cursor-pointer group">
<label htmlFor="photo-upload" className="block relative h-16 w-16 rounded-full overflow-hidden border-4 border-transparent hover:border-blue-500 cursor-pointer">
<img
src={preview || auth.user.foto || "/fotoSantri/no-pic.png"}
alt="Profile"
className="h-full w-full object-cover transition duration-200"
/>
<div className="absolute inset-0 flex items-center justify-center bg-black opacity-0 hover:opacity-30 transition duration-200 rounded-full">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-8 w-8 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M12 4.5c-3.315 0-6 2.685-6 6s2.685 6 6 6 6-2.685 6-6-2.685-6-6-6zM12 10.5a2.25 2.25 0 100-4.5 2.25 2.25 0 000 4.5zm-6 5.25h12c.828 0 1.5.672 1.5 1.5v3c0 .828-.672 1.5-1.5 1.5H6c-.828 0-1.5-.672-1.5-1.5v-3c0-.828.672-1.5 1.5-1.5z"
/>
</svg>
</div>
<input
id="photo-upload"
type="file"
accept="image/*"
className="hidden"
onChange={handlePhotoChange}
/>
</label>
</div>
<div>
<h2 className="text-xl font-semibold">{auth.user.nama}</h2>
<p className="text-sm">{auth.user.level == 1 ? 'Admin' : ''}</p>
</div>
</div>
</div>
<form onSubmit={handleSubmit} className="px-6 py-4" encType='multipart/form-data'>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Input label="Nama Lengkap" name="nama" value={data.nama} onChange={setData} error={errors.nama} />
<Input label="Alamat" name="alamat" value={data.alamat} onChange={setData} error={errors.alamat} />
<Input label="No Telepon" name="no_telp" value={data.no_telp} onChange={setData} error={errors.no_telp} />
<Input label="Tanggal Lahir" name="tanggal_lahir" type="date" value={data.tanggal_lahir} onChange={setData} error={errors.tanggal_lahir} />
<Select label="Jenis Kelamin" name="jk" value={data.jk} onChange={setData} options={['laki laki', 'perempuan']} error={errors.jk} />
<Input label="Password" name="password" type="password" value={data.password} onChange={setData} error={errors.password} placeholder="Kosongkan jika tidak ingin mengganti password" />
</div>
<div className="mt-6 text-right">
<button
type='submit'
className="bg-blue-500 text-white px-4 py-2 rounded-md text-sm font-medium"
disabled={processing}
>
Simpan
</button>
</div>
</form>
</div>
);
}
function Input({ label, name, value, onChange, type = 'text', error, placeholder = '' }) {
return (
<div>
<label className="block text-sm font-medium mb-1">{label}</label>
<input
type={type}
name={name}
value={value}
onChange={(e) => onChange(name, e.target.value)}
placeholder={placeholder}
className={`w-full px-4 py-2 bg-gray-50 border ${error ? 'border-red-500' : 'border-gray-200'} rounded-md focus:outline-none`}
/>
{error && <div className="text-red-500 text-sm mt-1">{error}</div>}
</div>
);
}
function Select({ label, name, value, onChange, options = [], error }) {
return (
<div>
<label className="block text-sm font-medium mb-1">{label}</label>
<select
name={name}
value={value}
onChange={(e) => onChange(name, e.target.value)}
className={`w-full px-4 py-2 bg-gray-50 border ${error ? 'border-red-500' : 'border-gray-200'} rounded-md focus:outline-none`}
>
{options.map((opt) => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
{error && <div className="text-red-500 text-sm mt-1">{error}</div>}
</div>
);
}

View File

@ -100,7 +100,7 @@ export default function ManualPayment({ santri, fields, options }) {
</div>
<div>
<div className="font-bold">{item.nama}</div>
<div className="text-sm opacity-50">{item.role_santri}</div>
<div className="text-sm opacity-50">{item.level == 1 ? 'Admin' : 'User'}</div>
</div>
</div>
</td>
@ -181,7 +181,7 @@ export default function ManualPayment({ santri, fields, options }) {
</tr>
</thead>
<tbody>
{selectedSantri ? selectedPayments.map((payment, idx) =>
{selectedPayments.map((payment, idx) =>
payment.detail_payments.map((detail) => (
<tr key={`${idx}-${detail.id}`}>
<td>{detail.payment_type?.payment_type}</td>
@ -195,7 +195,7 @@ export default function ManualPayment({ santri, fields, options }) {
</td>
</tr>
))
) : 'tidak ada data pembayaran santri'}
)}
</tbody>
</table>
</div>

View File

@ -5,7 +5,7 @@ import DeleteButton from '@/Components/DeleteButton';
import { useDispatch } from 'react-redux';
import { setPageTitle } from '@/Components/features/common/headerSlice';
export default function PaymentType({ tableName, paymentType, fields }) {
export default function PaymentType({ paymentType, fields }) {
const [selectedPaymentType, setSelectedPaymentType] = useState(null);
const [isDeleteOpen, setDeleteOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
@ -27,15 +27,15 @@ export default function PaymentType({ tableName, paymentType, fields }) {
}
}, [searchTerm, paymentType]);
const handleSearch = (e) => {
setSearchTerm(e.target.value);
};
const deleteModal = (item) => {
setSelectedPaymentType(item);
setDeleteOpen(true);
};
const handleSearch = (e) => {
setSearchTerm(e.target.value);
};
return (
<div>
<Head title="Daftar Payment Type" />

View File

@ -0,0 +1,135 @@
import React, { useState, useEffect } from 'react';
import { Head } from '@inertiajs/react';
import ModalInput from '@/Components/ModalInput';
import { useDispatch } from 'react-redux';
import { setPageTitle } from '@/Components/features/common/headerSlice';
import { CurrencyDollarIcon } from '@heroicons/react/24/outline'
export default function ManualPayment({ wallet }) {
const [searchTerm, setSearchTerm] = useState('');
const [filteredSantri, setFilteredSantri] = useState([]);
const dispatch = useDispatch();
useEffect(() => {
dispatch(setPageTitle("Data Dompet Santri"));
}, [dispatch]);
// console.log(santri.id)
useEffect(() => {
if (wallet) {
setFilteredSantri(
wallet.filter(item =>
item.nama.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.nis.toString().includes(searchTerm)
)
);
}
}, [searchTerm, wallet]);
const handleSearch = (e) => {
setSearchTerm(e.target.value);
};
return (
<div>
<Head title="Dompet 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 Dompet Santri</h1>
<div className="ml-auto">
<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 className="flex justify-between items-center mb-4">
<div className="form-control">
<div className="input-group">
<input
type="text"
placeholder="Cari Santri..."
className="input input-bordered"
value={searchTerm}
onChange={handleSearch}
/>
</div>
</div>
</div>
<div className="overflow-x-auto">
<table className="table table-zebra w-full">
<thead>
<tr>
<th>Nama</th>
<th>Nis</th>
<th>Status</th>
<th>Saldo</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>{item.nis}</td>
<td>
<div className={`badge ${item.status_santri === 'aktif' ? 'badge-success' : 'badge-warning'} gap-2 text-white`}>
{item.status_santri || "Open"}
</div>
</td>
<td>
{item.wallet ? item.wallet.saldo : 'Tidak ada saldo'}
</td>
</tr>
))
) : (
<tr>
<td colSpan="4" className="text-center">Tidak ada data santri.</td>
</tr>
)}
</tbody>
</table>
<div className="flex justify-center mt-4">
{wallet?.links?.map((link, index) => (
<button
key={index}
className={`px-4 py-2 mx-1 ${link.active ? "bg-blue-500 text-white" : "bg-gray-200"
}`}
onClick={() => {
if (link.url) window.location.href = link.url;
}}
disabled={!link.url}
>
{link.label.replace('&laquo;', '«').replace('&raquo;', '»')}
</button>
))}
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -102,7 +102,7 @@ export default function IndexSantri({ santri, fields, options }) {
</div>
<div>
<div className="font-bold">{item.nama}</div>
<div className="text-sm opacity-50">{item.role_santri || "Santri"}</div>
<div className="text-sm opacity-50">{item.level == 1 ? 'Admin' : 'User'}</div>
</div>
</div>
</td>

View File

@ -6,6 +6,7 @@ const Dashboard = lazy(() => import('@/Pages/protected/Dashboard'))
const IndexSantri = lazy(() => import('@/Pages/list-admin/santri/IndexSantri'))
const PaymentType = lazy(() => import('@/Pages/list-admin/payment/PaymentType'))
const ManualPayment = lazy(() => import('@/Pages/list-admin/payment/ManualPayment'))
const WalletUser = lazy(() => import('@pages/list-admin/wallet/WalletUser'))
console.log(route('dashboard'))
@ -26,6 +27,10 @@ const routes = [
path: route('indexManualPayment'),
component: ManualPayment,
},
{
path: route('walletUser'),
componenet: WalletUser
}
]
export default routes

View File

@ -47,6 +47,11 @@ const routes = [
icon: <ArrowRightIcon className={submenuIconClasses} />,
name: 'Data Santri'
},
{
path: '/data-payment-type',
icon: <ArrowRightIcon className={submenuIconClasses} />,
name: 'Tipe Pembayaran'
},
]
},
@ -56,16 +61,16 @@ const routes = [
icon: <CurrencyDollarIcon className={`${iconClasses} inline`} />,
name: 'Data Pembayaran',
submenu: [
{
path: '/data-payment-type',
icon: <ArrowRightIcon className={submenuIconClasses} />,
name: 'Tipe Pembayaran'
},
{
path: '/index-manual-payment',
icon: <ArrowRightIcon className={submenuIconClasses} />,
name: 'Data Pembayaran'
},
{
path: '/wallet-user',
icon: <ArrowRightIcon className={submenuIconClasses} />,
name: 'Wallet User'
}
],
},

View File

@ -8,7 +8,7 @@ import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import store from '../js/Auth/store';
import { Provider } from 'react-redux';
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
const appName = import.meta.env.VITE_APP_NAME || 'GogoSantri';
const Layout = lazy(() => import('@/Components/Layout'));
createInertiaApp({
@ -16,7 +16,6 @@ createInertiaApp({
resolve: (name) => {
return resolvePageComponent(`./Pages/${name}.jsx`, import.meta.glob('./Pages/**/**.jsx'))
.then((page) => {
// Pastikan semua halaman pakai LayoutWrapper
page.default.layout = (page) => <LayoutWrapper>{page}</LayoutWrapper>;
return page;
});
@ -39,7 +38,7 @@ createInertiaApp({
import { usePage } from '@inertiajs/react';
const LayoutWrapper = ({ children }) => {
const { url } = usePage(); // Sekarang usePage() ada di dalam Inertia App
const { url } = usePage();
const isLoginPage = url.startsWith('/login') || url.startsWith('/auth/login');
return isLoginPage ? <>{children}</> : <Layout>{children}</Layout>;

View File

@ -1,22 +1,25 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title inertia>{{ config('app.name', 'Laravel') }}</title>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
<title inertia>{{ config('app.name', 'GogoSantri') }}</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
<!-- Scripts -->
@routes
@viteReactRefresh
@vite(['resources/js/app.jsx', "resources/js/Pages/{$page['component']}.jsx"])
@inertiaHead
</head>
<body class="font-sans antialiased">
@inertia
</body>
<!-- Scripts -->
@routes
@viteReactRefresh
@vite(['resources/js/app.jsx', "resources/js/Pages/{$page['component']}.jsx"])
@inertiaHead
</head>
<body class="font-sans antialiased">
@inertia
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,7 @@
<?php
use App\Http\Controllers\ProfileController;
use App\Http\Controllers\WalletController;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\SantriController;
@ -9,17 +10,6 @@
use App\Http\Controllers\HomeController;
use Inertia\Inertia;
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/
Route::get('/', function () {
return redirect()->route('login');
});
@ -28,17 +18,14 @@
return redirect()->route('login');
});
// Route::get('/dashboard', function () {
// return Inertia::render('Dashboard');
// })->middleware(['auth', 'verified'])->name('dashboard');
Route::middleware('auth')->group(function () {
// dashboard
Route::get('/dashboard', [HomeController::class, 'index'])->name('dashboard');
// profile
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
Route::post('/update-profile/{id}', [ProfileController::class, 'updateProfile'])->name('profile.update');
// santri
Route::get('/data-santri', [SantriController::class, 'index'])->name('indexSantri');
@ -56,6 +43,9 @@
Route::get('/index-manual-payment', [PaymentController::class, 'indexManualPayment'])->name('indexManualPayment');
Route::post('/updatepayments/{paymentId}', [PaymentController::class, 'manualPayment'])->name('manualPayment');
// wallet
Route::get('/wallet-user', [WalletController::class, 'walletUser'])->name('walletUser');
Route::get('profile-settings', function () {
return Inertia::render('ProfileSettings');
});