Skip to main content

Black Friday 2025! Only until December 1st: coupon FRIDAY25 for 40% off Yearly/Lifetime membership!

Read more here

ajikamaludin/expense-tracker-inertiajs

6 stars
4 code files
View ajikamaludin/expense-tracker-inertiajs on GitHub

composer.json

Open in GitHub
{
//
"require": {
//
"inertiajs/inertia-laravel": "^0.3.5",
//
},
//
}

package.json

Open in GitHub
{
//
"devDependencies": {
//
"@inertiajs/inertia": "^0.9.0",
"@inertiajs/inertia-react": "^0.6.0",
"@inertiajs/progress": "^0.2.4",
//
},
//
}

app/Http/Controllers/CategoryController.php

Open in GitHub
use Illuminate\Http\Request;
use App\Models\Category;
use Illuminate\Validation\Rule;
 
class CategoryController extends Controller
{
public function index(Request $request)
{
if ($request->q != null) {
$query = Category::where('name', 'like', '%'.$request->q.'%')
->orWhere('description', 'like', '%'.$request->q.'%')
->orderBy('created_at', 'asc')
->paginate(10);
} else {
$query = Category::orderBy('created_at', 'asc')->paginate(10);
}
return inertia('Category', [
'categories' => $query,
'_search' => $request->q ? $request->q : ''
]);
}
 
public function store(Request $request)
{
$request->validate([
'name' => [
'required',
'string',
'max:255',
Rule::unique('categories', 'name')->where(function ($query) {
return $query->where('deleted_at', null);
})
],
'description' => 'required|string|max:255',
'amount' => 'required|numeric|max:999999999|min:1'
]);
 
$category = Category::create([
'name' => $request->name,
'description' => $request->description,
'default_budget' => $request->amount
]);
 
$category->budgets()->create([
'budget' => $request->amount,
'start_date' => now()->toDateString(),
'end_date' => null,
'remain' => $request->amount
]);
 
return redirect()->route('categories');
}
 
public function update(Request $request, Category $category)
{
$request->validate([
'name' => 'required|string|max:255',
'description' => 'required|string|max:255',
'amount' => 'required|numeric|max:999999999|min:1'
]);
 
$category->update([
'name' => $request->name,
'description' => $request->description,
'default_budget' => $request->amount,
]);
 
$budget = $category->budgets()->where('end_date', null)->first();
$budget->update([
'budget' => $request->amount,
'remain' => ($request->amount + $budget->rollover) - ($budget->total_used)
]);
 
return redirect()->route('categories');
}
 
public function destroy(Category $category)
{
$category->budgets()->delete();
$category->delete();
 
return redirect()->route('categories');
}
}

resources/js/Pages/Category.js

Open in GitHub
import React, { useState, useEffect } from 'react'
import NumberFormat from 'react-number-format'
import { usePrevious } from 'react-use'
import { toast } from 'react-toastify'
import { Head, useForm } from '@inertiajs/inertia-react'
import { Inertia } from '@inertiajs/inertia'
import { formatIDR } from '@/utils'
import Pagination from '@/Components/Pagination'
import Authenticated from '@/Layouts/Authenticated'
 
export default function Category(props) {
const { _search } = props
const [search, setSearch] = useState(_search)
const preValue = usePrevious(search)
const [category, setCategory] = useState(null)
 
const { data: categories , links } = props.categories
const { data, setData, errors, post, put, processing, delete: destroy } = useForm({
name: '',
description: '',
amount: 0
})
 
const handleChange = (e) => {
const key = e.target.id;
const value = e.target.value
setData(key, value)
}
 
const handleReset = () => {
setCategory(null)
setData({
name: '',
description: '',
amount: ''
})
}
 
const handleEdit = (category) => {
setCategory(category)
setData({
name: category.name,
description: category.description,
amount: category.default_budget
})
}
 
const handleDelete = (category) => {
destroy(route('categories.destroy', category), {
onBefore: () => confirm('Are you sure you want to delete this record?'),
onSuccess: () => Promise.all([
handleReset(),
toast.success('data has been deleted')
])
})
}
 
const handleSubmit = (e) => {
e.preventDefault()
if(category !== null) {
put(route('categories.update', category), {
onSuccess: () => Promise.all([
handleReset(),
toast.success('The Data has been changed')
])
})
return
}
post(route('categories.store'), {
onSuccess: () => Promise.all([
handleReset(),
toast.success('Data has been saved')
])
})
}
 
useEffect(() => {
if (preValue) {
Inertia.get(route(route().current()), { q: search }, {
replace: true,
preserveState: true,
})
}
}, [search])
 
return (
<Authenticated
errors={props.errors}
header={
<h2 className="font-semibold text-xl text-gray-800 leading-tight">
Category
</h2>
}
>
<Head title="Category" />
 
<div className="flex flex-col space-y-2 md:space-y-0 md:flex-row py-12">
<div className="w-full md:w-1/3 px-6 md:pl-8">
<div className="card bg-white">
<div className="card-body">
<div className="form-control">
<label className="label">
<span className="label-text">
Category Name
</span>
</label>
<input
type="text"
placeholder="Name"
className={`input input-bordered ${
errors.name ? 'input-error' : ''
}`}
id="name"
value={data.name}
onChange={handleChange}
/>
<label className="label">
<span className="label-text-alt">
{errors.name}
</span>
</label>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">
Description
</span>
</label>
<input
type="text"
placeholder="Description"
className={`input input-bordered ${
errors.description ? 'input-error' : ''
}`}
id="description"
value={data.description}
onChange={handleChange}
/>
<label className="label">
<span className="label-text-alt">
{errors.description}
</span>
</label>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">Amount</span>
</label>
<NumberFormat
thousandSeparator={true}
className={`input input-bordered ${
errors.amount ? 'input-error' : ''
}`}
value={data.amount}
thousandSeparator="."
decimalSeparator=","
onValueChange={({ value }) =>
setData('amount', value)
}
/>
<label className="label">
<span className="label-text-alt">
{errors.amount}
</span>
</label>
</div>
<div className="card-actions">
<button
className={`btn btn-primary ${
processing && 'animate-spin'
}`}
onClick={handleSubmit}
disabled={processing}
>
Add
</button>
<button
className="btn btn-secondary"
onClick={handleReset}
disabled={processing}
>
Clear
</button>
</div>
</div>
</div>
</div>
 
<div className="w-full md:w-2/3 px-6 md:pr-8">
<div className="card bg-white">
<div className="card-body">
<div className="flex justify-end my-1">
<div className="form-control">
<input
type="text"
className="input input-bordered"
value={search}
onChange={(e) =>
setSearch(e.target.value)
}
placeholder="Search"
/>
</div>
</div>
<div className="overflow-x-auto">
<table className="table w-full table-zebra">
<thead>
<tr>
<th></th>
<th className="w-36">
Category Name
</th>
<th>Description</th>
<th>Amount</th>
<th className="w-52"></th>
</tr>
</thead>
<tbody
className={processing ? 'opacity-70' : ''}
>
{categories?.map((category) => (
<tr key={category.id}>
<th>{category.id}</th>
<td>{category.name}</td>
<td>{category.description}</td>
<td>
{formatIDR(
category.default_budget
)}
</td>
<td>
<div
className="btn btn-warning mx-1"
onClick={() =>
handleEdit(category)
}
>
Edit
</div>
<div
className="btn btn-error mx-1"
onClick={() =>
handleDelete(category)
}
>
Delete
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
<Pagination links={links} params={{ q: search }}/>
</div>
</div>
</div>
</div>
</Authenticated>
)
}

We'd Love Your Feedback

Tell us what you like or what we can improve

Feel free to share anything you like or dislike about this page or the platform in general.