From 00d25e09d818f807e3a4628c3d6b59415c5f3573 Mon Sep 17 00:00:00 2001
From: ajikamaludin
Date: Wed, 11 Jan 2023 23:08:53 +0700
Subject: [PATCH] final
---
app/Exports/ExpensesExport.php | 6 +-
app/Http/Controllers/ExpenseController.php | 84 +++++++-
app/Models/Expense.php | 22 +-
app/Providers/AppServiceProvider.php | 15 +-
resources/css/app.css | 2 +-
resources/js/Components/DatePickerInput.jsx | 18 ++
resources/js/Components/Modal.jsx | 76 ++-----
resources/js/Pages/Booking/Index.jsx | 2 +-
resources/js/Pages/Expense/FormModal.jsx | 196 ++++++++++++++++++
resources/js/Pages/Expense/Index.jsx | 136 +++++++++---
resources/js/Pages/Profile/Edit.jsx | 1 -
.../Pages/Profile/Partials/DeleteUserForm.jsx | 99 ---------
routes/web.php | 5 +
13 files changed, 459 insertions(+), 203 deletions(-)
create mode 100644 resources/js/Pages/Expense/FormModal.jsx
delete mode 100644 resources/js/Pages/Profile/Partials/DeleteUserForm.jsx
diff --git a/app/Exports/ExpensesExport.php b/app/Exports/ExpensesExport.php
index 59d126a..a842ea6 100644
--- a/app/Exports/ExpensesExport.php
+++ b/app/Exports/ExpensesExport.php
@@ -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');
diff --git a/app/Http/Controllers/ExpenseController.php b/app/Http/Controllers/ExpenseController.php
index 5ac320c..024e3b4 100644
--- a/app/Http/Controllers/ExpenseController.php
+++ b/app/Http/Controllers/ExpenseController.php
@@ -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');
+ }
}
diff --git a/app/Models/Expense.php b/app/Models/Expense.php
index e0c4dc9..c65faf9 100644
--- a/app/Models/Expense.php
+++ b/app/Models/Expense.php
@@ -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];
+ }
}
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index ee8ca5b..40c4154 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -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
+ ]
+ );
+ });
+ }
}
}
diff --git a/resources/css/app.css b/resources/css/app.css
index c903134..7c1d6b5 100644
--- a/resources/css/app.css
+++ b/resources/css/app.css
@@ -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 {
diff --git a/resources/js/Components/DatePickerInput.jsx b/resources/js/Components/DatePickerInput.jsx
index eab2a28..bdfdf8c 100644
--- a/resources/js/Components/DatePickerInput.jsx
+++ b/resources/js/Components/DatePickerInput.jsx
@@ -35,3 +35,21 @@ export const DatePickerRangeInput = ({
);
};
+
+export const DatePickerInput = ({
+ value,
+ onChange,
+}) => {
+ return (
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/resources/js/Components/Modal.jsx b/resources/js/Components/Modal.jsx
index cf6d39f..2622068 100644
--- a/resources/js/Components/Modal.jsx
+++ b/resources/js/Components/Modal.jsx
@@ -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 (
-
-
-
-
-
-
-
-
- {children}
-
-
-
-
- );
-}
+
+
+
+ {title}
+
+ {children}
+
+
+ )
+}
\ No newline at end of file
diff --git a/resources/js/Pages/Booking/Index.jsx b/resources/js/Pages/Booking/Index.jsx
index 69ce38b..d9893d1 100644
--- a/resources/js/Pages/Booking/Index.jsx
+++ b/resources/js/Pages/Booking/Index.jsx
@@ -161,7 +161,7 @@ export default function Dashboard(props) {
-
+
handleToggleForm()}>Tambah
diff --git a/resources/js/Pages/Expense/FormModal.jsx b/resources/js/Pages/Expense/FormModal.jsx
new file mode 100644
index 0000000..adeae6f
--- /dev/null
+++ b/resources/js/Pages/Expense/FormModal.jsx
@@ -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 (
+
+ {+user.role === 1 && (
+
+ )}
+
+ {data.isIncome === 0 && (
+ <>
+
+ Nama
+
+
+
+
+ Job Number
+
+
+
+ >
+ )}
+
+
+ Deskripsi
+
+
+
+
+ Tanggal
+ setData('date_expense', date)}
+ />
+
+
+
+ Amount
+
+
+
+
+ {data.isIncome === 0 && (
+
+ Status
+
+ Draft
+ Paid
+ Approve
+ Reject
+
+
+
+ )}
+
+
+
+ Simpan
+
+
+ Batal
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/resources/js/Pages/Expense/Index.jsx b/resources/js/Pages/Expense/Index.jsx
index e6167f9..a93ff08 100644
--- a/resources/js/Pages/Expense/Index.jsx
+++ b/resources/js/Pages/Expense/Index.jsx
@@ -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 (
-
Tambah
+
toggle()}
+ >
+ Tambah
+
{+auth.user.role === 1 ? (
-
Export Excel
+
+ Export Excel
+
) : (
Tanggal
Name
Job Number
-
Description
+
Description
Amount
Status
Opsi
@@ -214,32 +261,52 @@ export default function Dashboard(props) {
{expense.description}
-
- {+expense.isIncome === 0 ? '-' : '+'}
+
{formatIDR(expense.amount)}
- {+expense.is_paid === 0 ? 'Unpaid' : 'Paid'}
+ {expense.status}
- {}}
- className="btn btn-xs"
- >
- Detail
-
- {}}
- className="btn btn-xs"
- >
- Edit
-
- handleDelete(expense)}
- className="btn btn-xs"
- >
- Hapus
-
+ {+auth.user.role === 3 ? (
+ <>
+
+ Approve
+
+
+ Reject
+
+ >
+ ) : (
+ <>
+ {}}
+ className="btn btn-xs"
+ >
+ Detail
+
+ toggle(expense)}
+ className="btn btn-xs"
+ >
+ Edit
+
+ handleDelete(expense)}
+ className="btn btn-xs"
+ >
+ Hapus
+
+ >
+ )}
)
@@ -293,6 +360,9 @@ export default function Dashboard(props) {
toggle={confirmModal.toggle}
onConfirm={onDelete}
/>
+
);
}
diff --git a/resources/js/Pages/Profile/Edit.jsx b/resources/js/Pages/Profile/Edit.jsx
index ed18c84..bba7e58 100644
--- a/resources/js/Pages/Profile/Edit.jsx
+++ b/resources/js/Pages/Profile/Edit.jsx
@@ -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';
diff --git a/resources/js/Pages/Profile/Partials/DeleteUserForm.jsx b/resources/js/Pages/Profile/Partials/DeleteUserForm.jsx
deleted file mode 100644
index 3d9cb5d..0000000
--- a/resources/js/Pages/Profile/Partials/DeleteUserForm.jsx
+++ /dev/null
@@ -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 (
-
-
- Delete Account
-
-
- 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.
-
-
-
- Delete Account
-
-
-
-
-
- );
-}
diff --git a/routes/web.php b/routes/web.php
index 8cabf82..db0abc3 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -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');