pull/2/head
ajikamaludin 2 years ago
parent 5743ebb0a1
commit 00d25e09d8
Signed by: ajikamaludin
GPG Key ID: 476C9A2B4B794EBB

@ -11,11 +11,11 @@ use Maatwebsite\Excel\Concerns\WithHeadings;
class ExpensesExport implements WithHeadings, FromView
{
public $begining_balance = 0;
public function view(): View
{
$this->begining_balance = 0;
$expenses = Expense::all();
$today = \Carbon\Carbon::now();
$today = now();
$query = Expense::query()->orderBy('date_expense', 'ASC');

@ -2,11 +2,14 @@
namespace App\Http\Controllers;
use App\Exports\ExpensesExport;
use App\Models\Expense;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Maatwebsite\Excel\Facades\Excel;
class ExpenseController extends Controller
{
@ -24,11 +27,10 @@ class ExpenseController extends Controller
$query->where('isIncome', 0)->orderBy('created_at', 'DESC');
}
if ($request->q) {
if ($request->q != null) {
$query->where('name', 'like', '%'.$request->q.'%')
->orWhere('description', 'like', '%'.$request->q.'%')
->orWhere('job_number', 'like', '%'.$request->q.'%')
->orWhere('amount', 'like', '%'.$request->q.'%');
->orWhere('job_number', 'like', '%'.$request->q.'%');
}
$endDate = Carbon::now()->toDateString();
@ -43,17 +45,89 @@ class ExpenseController extends Controller
->whereDate('date_expense', '>=', $startDate);
$limit = $request->limit ? $request->limit : 10;
return inertia('Expense/Index', [
'expenses' => $query->paginate($limit),
'_startDate' => $startDate,
'_endDate' => $endDate,
'_limit' => $limit
'_limit' => $limit,
'_q' => $request->q
]);
}
public function store(Request $request)
{
$request->validate([
'description' => ['required'],
'date_expense' => ['required', 'date'],
'amount' => ['required', 'numeric'],
'is_paid' => ['required', 'in:0,1,2,3'],
'isIncome' => ['required', 'in:0,1'],
]);
if ($request->isIncome === 0) {
$request->validate([
'name' => ['required'],
'job_number' => ['required'],
]);
}
Expense::create([
'name' => $request->name,
'description' => $request->description,
'job_number' => $request->job_number,
'date_expense' => Carbon::parse($request->date_expense)->toDateString(),
'amount' => $request->amount,
'is_paid' => $request->is_paid,
'isIncome' => $request->isIncome,
]);
return redirect()->route('expenses.index');
}
public function update(Request $request, Expense $expense)
{
$request->validate([
'description' => ['required'],
'date_expense' => ['required', 'date'],
'amount' => ['required', 'numeric'],
'is_paid' => ['required', 'in:0,1,2,3'],
'isIncome' => ['required', 'in:0,1'],
]);
if ($request->isIncome === 0) {
$request->validate([
'name' => ['required'],
'job_number' => ['required'],
]);
}
$expense->update([
'name' => $request->name,
'description' => $request->description,
'job_number' => $request->job_number,
'date_expense' => Carbon::parse($request->date_expense)->toDateString(),
'amount' => $request->amount,
'is_paid' => $request->is_paid,
'isIncome' => $request->isIncome,
]);
return redirect()->route('expenses.index');
}
public function decision(Expense $expense, $status)
{
$expense->update(['is_paid' => $status]);
return redirect()->route('expenses.index');
}
public function destroy(Expense $expense)
{
$expense->delete();
}
public function export()
{
return Excel::download(new ExpensesExport, 'expenses.xlsx');
}
}

@ -9,11 +9,11 @@ class Expense extends Model
{
use HasFactory;
const IS_PAID_DRAFT = 1;
const IS_PAID_UNPAID = 1;
const IS_PAID_PAID = 2;
const IS_PAID_APPROVE = 3;
const IS_PAID_REJECT = 4;
const IS_PAID_DRAFT = 0;
const IS_PAID_UNPAID = 0;
const IS_PAID_PAID = 1;
const IS_PAID_APPROVE = 2;
const IS_PAID_REJECT = 3;
/**
* The attributes that are mass assignable.
@ -30,4 +30,16 @@ class Expense extends Model
'isIncome',
'is_paid',
];
protected $appends = ['status'];
public function getStatusAttribute()
{
return [
self::IS_PAID_DRAFT => 'Draft',
self::IS_PAID_PAID => 'Paid',
self::IS_PAID_APPROVE => 'Approve',
self::IS_PAID_REJECT => 'Reject',
][$this->is_paid];
}
}

@ -2,6 +2,8 @@
namespace App\Providers;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
@ -23,6 +25,17 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot()
{
//
if (app()->isProduction() == false) {
DB::listen(function ($query) {
Log::info(
$query->sql,
[
'bindings' => $query->bindings,
'time' => $query->time,
'connectionName' => $query->connectionName
]
);
});
}
}
}

@ -8,7 +8,7 @@
}
.react-datepicker-popper {
@apply z-40 w-72 text-sm bg-white shadow px-3 py-2 border-2 border-gray-200 rounded;
@apply z-10 w-72 text-sm bg-white shadow px-3 py-2 border-2 border-gray-200 rounded;
}
.react-datepicker-left {

@ -35,3 +35,21 @@ export const DatePickerRangeInput = ({
</div>
);
};
export const DatePickerInput = ({
value,
onChange,
}) => {
return (
<div className="relative w-full">
<DatePicker
selected={value}
onChange={onChange}
nextMonthButtonLabel=">"
previousMonthButtonLabel="<"
popperClassName="react-datepicker-left"
dateFormat={'dd-MM-yyyy'}
/>
</div>
)
}

@ -1,57 +1,25 @@
import { Fragment } from 'react';
import { Dialog, Transition } from '@headlessui/react';
import React from "react";
export default function Modal({ children, show = false, maxWidth = '2xl', closeable = true, onClose = () => {} }) {
const close = () => {
if (closeable) {
onClose();
}
};
const maxWidthClass = {
sm: 'sm:max-w-sm',
md: 'sm:max-w-md',
lg: 'sm:max-w-lg',
xl: 'sm:max-w-xl',
'2xl': 'sm:max-w-2xl',
}[maxWidth];
export default function Modal({ isOpen, toggle = () => {}, children, title = ''}) {
return (
<Transition show={show} as={Fragment} leave="duration-200">
<Dialog
as="div"
id="modal"
className="fixed inset-0 flex overflow-y-auto px-4 py-6 sm:px-0 items-center z-50 transform transition-all"
onClose={close}
>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="absolute inset-0 bg-gray-500/75" />
</Transition.Child>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel
className={`mb-6 bg-white rounded-lg overflow-hidden shadow-xl transform transition-all sm:w-full sm:mx-auto ${maxWidthClass}`}
>
{children}
</Dialog.Panel>
</Transition.Child>
</Dialog>
</Transition>
);
}
<div
className={`modal`}
style={
isOpen
? {
opacity: 1,
pointerEvents: 'auto',
visibility: 'visible',
}
: {}
}>
<div className={`modal-box`} style={{minHeight: '40em'}}>
<h1 className="font-bold text-2xl pb-8">
{title}
</h1>
{children}
</div>
</div>
)
}

@ -161,7 +161,7 @@ export default function Dashboard(props) {
<Head title="Booking" />
<div className="p-4">
<div className="mx-auto max-w-7xl p-4 bg-white overflow-hidden shadow-sm sm:rounded-lg min-h-screen">
<div className="mx-auto max-w-7xl p-4 bg-white overflow-hidden shadow-sm sm:rounded-lg" style={{minHeight: '500px'}}>
<div className='flex justify-between space-x-1 flex-row mb-2'>
<div className='flex space-x-1'>
<div className='btn' onClick={() => handleToggleForm()}>Tambah</div>

@ -0,0 +1,196 @@
import React, { useEffect, useState } from "react";
import Modal from "@/Components/Modal";
import InputError from "@/Components/InputError";
import { DatePickerInput } from "@/Components/DatePickerInput";
import { useForm, usePage } from "@inertiajs/inertia-react";
import { toast } from "react-toastify";
export default function FormModal(props) {
const { auth: { user } } = usePage().props
const { modalState } = props
const { data, setData, post, put, processing, errors, reset, clearErrors } = useForm({
id: null,
name: "",
description: "",
job_number: "",
date_expense: new Date(),
amount: "",
isIncome: 0,
is_paid: 0,
})
const setType = (type) => {
setData('isIncome',type)
}
const handleOnChange = (event) => {
setData(event.target.name, event.target.value)
}
const handleReset = () => {
modalState.setData(null)
reset()
clearErrors()
}
const handleClose = () => {
handleReset()
modalState.toggle()
}
const handleSubmit = () => {
const expense = modalState.data
if(expense !== null) {
put(route('expenses.update', expense), {
onSuccess: () => {
toast.success('item updated')
handleClose()
}
})
return
}
post(route('expenses.store'), {
onSuccess: () => {
toast.success('item created')
handleClose()
}
})
}
const title = data.id ? 'Edit Data' : 'Tambah Data'
useEffect(() => {
const expense = modalState.data
if (expense !== null) {
setData({
id: expense?.id,
name: expense?.name,
description: expense?.description,
job_number: expense?.job_number,
date_expense: new Date(expense?.date_expense),
amount: expense?.amount,
isIncome: expense?.isIncome,
is_paid: expense?.is_paid,
})
return
}
}, [modalState])
return (
<Modal
isOpen={modalState.isOpen}
toggle={handleClose}
title={title}
>
{+user.role === 1 && (
<div className="tabs w-full mb-2">
<a
className={`tab tab-bordered w-1/2 ${data.isIncome === 0 ? 'tab-active' : ''}`}
onClick={() => setType(0)}
>
Pengeluaran
</a>
<a
className={`tab tab-bordered w-1/2 ${data.isIncome === 1 ? 'tab-active' : ''}`}
onClick={() => setType(1)}
>
Pemasukan
</a>
</div>
)}
{data.isIncome === 0 && (
<>
<div className="form-control mb-2">
<label>Nama</label>
<input
className="input input-bordered"
name="name"
value={data.name}
onChange={handleOnChange}
/>
<InputError message={errors.name} className="mt-2" />
</div>
<div className="form-control mb-2">
<label>Job Number</label>
<input
className="input input-bordered"
name="job_number"
value={data.job_number}
onChange={handleOnChange}
/>
<InputError message={errors.job_number} className="mt-2" />
</div>
</>
)}
<div className="form-control mb-2">
<label>Deskripsi</label>
<textarea
className="textarea textarea-bordered"
name="description"
value={data.description}
onChange={handleOnChange}
rows={5}
/>
<InputError message={errors.description} className="mt-2" />
</div>
<div className="form-control mb-2">
<label>Tanggal</label>
<DatePickerInput
value={data.date_expense}
onChange={date => setData('date_expense', date)}
/>
<InputError message={errors.date_expense} className="mt-2" />
</div>
<div className="form-control mb-2">
<label>Amount</label>
<input
type="number"
className="input input-bordered"
name="amount"
value={data.amount}
onChange={handleOnChange}
/>
<InputError message={errors.amount} className="mt-2" />
</div>
{data.isIncome === 0 && (
<div className="form-control mb-2">
<label>Status</label>
<select
name="is_paid"
className="select select-bordered w-full"
disabled={+user.role === 2}
value={data.is_paid}
onChange={handleOnChange}
>
<option value="0">Draft</option>
<option value="1">Paid</option>
<option value="2">Approve</option>
<option value="3">Reject</option>
</select>
<InputError message={errors.amount} className="mt-2" />
</div>
)}
<div className="w-full flex justify-end space-x-1 items-center mt-2">
<button
className="btn"
onClick={handleSubmit}
disabled={processing}
>
Simpan
</button>
<button
className="btn btn-secondary"
onClick={handleClose}
type="secondary"
>
Batal
</button>
</div>
</Modal>
)
}

@ -1,7 +1,9 @@
import React, { useState, useEffect, useRef } from 'react';
import ReactToPrint from 'react-to-print';
import qs from 'qs'
import { Inertia } from '@inertiajs/inertia';
import { Head, usePage, useForm } from '@inertiajs/inertia-react';
import { Link } from '@inertiajs/inertia-react';
import { Head } from '@inertiajs/inertia-react';
import { usePrevious } from 'react-use';
import { toast } from 'react-toastify';
@ -9,24 +11,20 @@ import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import Pagination from '@/Components/Pagination';
import Print from './Print';
import ModalConfirm from '@/Components/ModalConfirm';
import FormModal from './FormModal';
import { DatePickerRangeInput } from '@/Components/DatePickerInput';
import { useModalState } from '@/Hook';
import { formatDate, formatIDR } from '@/Utils';
export default function Dashboard(props) {
const { auth, expenses: { data, links, total, last_page }, _startDate, _endDate, _limit } = props
const { auth, expenses: { data, links, total, last_page }, _startDate, _endDate, _limit, _q } = props
const [items, setItems] = useState(data.map(item => {
return {
...item,
isChecked: false
}
}))
const [items, setItems] = useState([])
const [startDate] = useState(_startDate)
const [endDate] = useState(_endDate)
const [filterDate, setFilterDate] = useState([_startDate, _endDate])
const [search, setSearch] = useState("");
const [search, setSearch] = useState(_q);
const [limit, setLimit] = useState(_limit)
const preValue = usePrevious(`${search}-${filterDate[0]}-${filterDate[1]}-${limit}`);
@ -60,6 +58,12 @@ export default function Dashboard(props) {
}))
}
const formModal = useModalState()
const toggle = (expense = null) => {
formModal.setData(expense)
formModal.toggle()
}
// TODO:
// add -> operator hanya expense, kasir expense/income
// edit -> menyesuaikan
@ -79,6 +83,30 @@ export default function Dashboard(props) {
}))
}
const handleExport = () => {
const params = items
.map((item) => {
if (item.isChecked) {
return item.id;
}
})
.filter((isChecked) => {
return isChecked !== undefined;
});
fetch(route('expenses.export') +'?'+ qs.stringify({ids: params, start_date: filterDate[0], end_date: filterDate[1]}, { encodeValuesOnly:true }))
.then( res => res.blob() )
.then( blob => {
var file = window.URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = file;
a.download = "expenses.xlsx";
document.body.appendChild(a);
a.click();
a.remove();
});
};
const confirmModal = useModalState(false);
const handleDelete = (expense) => {
confirmModal.setData(expense);
@ -116,6 +144,15 @@ export default function Dashboard(props) {
}
}, [search, filterDate, limit])
useEffect(() => {
setItems(data.map(item => {
return {
...item,
isChecked: false
}
}))
}, [data])
return (
<AuthenticatedLayout
auth={props.auth}
@ -126,9 +163,19 @@ export default function Dashboard(props) {
<div className="p-4">
<div className="mx-auto max-w-7xl p-4 bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div className='flex justify-between space-x-0 lg:space-x-1 flex-row mb-2'>
<div className='btn'>Tambah</div>
<div
className='btn'
onClick={() => toggle()}
>
Tambah
</div>
{+auth.user.role === 1 ? (
<div className='btn'>Export Excel</div>
<div
className='btn'
onClick={handleExport}
>
Export Excel
</div>
) : (
<div
className='btn'
@ -178,7 +225,7 @@ export default function Dashboard(props) {
<th>Tanggal</th>
<th>Name</th>
<th>Job Number</th>
<th>Description</th>
<th className='w-10'>Description</th>
<th>Amount</th>
<th>Status</th>
<th>Opsi</th>
@ -214,32 +261,52 @@ export default function Dashboard(props) {
<td className="text-sm text-gray-500">
{expense.description}
</td>
<td className="text-sm text-gray-500">
{+expense.isIncome === 0 ? '-' : '+'}
<td className="text-sm text-right text-gray-500">
{formatIDR(expense.amount)}
</td>
<td>
{+expense.is_paid === 0 ? 'Unpaid' : 'Paid'}
{expense.status}
</td>
<td className="flex gap-1 text-sm text-gray-500">
<button
onClick={() => {}}
className="btn btn-xs"
>
Detail
</button>
<button
onClick={() => {}}
className="btn btn-xs"
>
Edit
</button>
<button
onClick={() => handleDelete(expense)}
className="btn btn-xs"
>
Hapus
</button>
{+auth.user.role === 3 ? (
<>
<Link
href={route('expenses.decision', [expense.id, 2])}
method="put"
className="btn btn-xs"
>
Approve
</Link>
<Link
href={route('expenses.decision', [expense.id, 3])}
method="put"
className="btn btn-xs"
>
Reject
</Link>
</>
) : (
<>
<button
onClick={() => {}}
className="btn btn-xs"
>
Detail
</button>
<button
onClick={() => toggle(expense)}
className="btn btn-xs"
>
Edit
</button>
<button
onClick={() => handleDelete(expense)}
className="btn btn-xs"
>
Hapus
</button>
</>
)}
</td>
</tr>
)
@ -293,6 +360,9 @@ export default function Dashboard(props) {
toggle={confirmModal.toggle}
onConfirm={onDelete}
/>
<FormModal
modalState={formModal}
/>
</AuthenticatedLayout>
);
}

@ -1,5 +1,4 @@
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import DeleteUserForm from './Partials/DeleteUserForm';
import UpdatePasswordForm from './Partials/UpdatePasswordForm';
import UpdateProfileInformationForm from './Partials/UpdateProfileInformationForm';
import { Head } from '@inertiajs/inertia-react';

@ -1,99 +0,0 @@
import { useRef, useState } from 'react';
import DangerButton from '@/Components/DangerButton';
import InputError from '@/Components/InputError';
import InputLabel from '@/Components/InputLabel';
import Modal from '@/Components/Modal';
import SecondaryButton from '@/Components/SecondaryButton';
import TextInput from '@/Components/TextInput';
import { useForm } from '@inertiajs/inertia-react';
export default function DeleteUserForm({ className }) {
const [confirmingUserDeletion, setConfirmingUserDeletion] = useState(false);
const passwordInput = useRef();
const {
data,
setData,
delete: destroy,
processing,
reset,
errors,
} = useForm({
password: '',
});
const confirmUserDeletion = () => {
setConfirmingUserDeletion(true);
};
const deleteUser = (e) => {
e.preventDefault();
destroy(route('profile.destroy'), {
preserveScroll: true,
onSuccess: () => closeModal(),
onError: () => passwordInput.current.focus(),
onFinish: () => reset(),
});
};
const closeModal = () => {
setConfirmingUserDeletion(false);
reset();
};
return (
<section className={`space-y-6 ${className}`}>
<header>
<h2 className="text-lg font-medium text-gray-900">Delete Account</h2>
<p className="mt-1 text-sm text-gray-600">
Once your account is deleted, all of its resources and data will be permanently deleted. Before
deleting your account, please download any data or information that you wish to retain.
</p>
</header>
<DangerButton onClick={confirmUserDeletion}>Delete Account</DangerButton>
<Modal show={confirmingUserDeletion} onClose={closeModal}>
<form onSubmit={deleteUser} className="p-6">
<h2 className="text-lg font-medium text-gray-900">
Are you sure you want to delete your account?
</h2>
<p className="mt-1 text-sm text-gray-600">
Once your account is deleted, all of its resources and data will be permanently deleted. Please
enter your password to confirm you would like to permanently delete your account.
</p>
<div className="mt-6">
<InputLabel for="password" value="Password" className="sr-only" />
<TextInput
id="password"
type="password"
name="password"
ref={passwordInput}
value={data.password}
handleChange={(e) => setData('password', e.target.value)}
className="mt-1 block w-3/4"
isFocused
placeholder="Password"
/>
<InputError message={errors.password} className="mt-2" />
</div>
<div className="mt-6 flex justify-end">
<SecondaryButton onClick={closeModal}>Cancel</SecondaryButton>
<DangerButton className="ml-3" processing={processing}>
Delete Account
</DangerButton>
</div>
</form>
</Modal>
</section>
);
}

@ -27,7 +27,12 @@ Route::middleware(['auth', 'verified'])->group(function () {
// Expense & Income Page
Route::get('/expenses', [ExpenseController::class, 'index'])->name('expenses.index');
Route::post('/expenses', [ExpenseController::class, 'store'])->name('expenses.store');
Route::put('/expenses/{expense}', [ExpenseController::class, 'update'])->name('expenses.update');
Route::put('/expenses/{expense}/{status}', [ExpenseController::class, 'decision'])->name('expenses.decision');
Route::delete('/expenses/{expense}', [ExpenseController::class, 'destroy'])->name('expenses.destroy');
Route::get('expenses/export', [ExpenseController::class, 'export'])->name('expenses.export');
// Monitor Booking
Route::get('/monitoring-booking', [BookingController::class, 'index'])->name('monitoring-booking.index');

Loading…
Cancel
Save