done payroll revision
parent
b0445011d1
commit
c6b63f7531
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Payroll;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ReportController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = Payroll::with('employee');
|
||||
|
||||
$startDate = now()->startOfMonth()->toDateString();
|
||||
$endDate = now()->endOfMonth()->toDateString();
|
||||
|
||||
if ($request->startDate != null && $request->endDate != null) {
|
||||
$query->whereBetween('date', [$request->startDate, $request->endDate]);
|
||||
$startDate = $request->startDate;
|
||||
$endDate = $request->endDate;
|
||||
} else {
|
||||
$query->whereBetween('date', [$startDate, $endDate]);
|
||||
}
|
||||
|
||||
return inertia('Report', [
|
||||
'payrolls' => $query->orderBy('date', 'desc')->paginate(10),
|
||||
'_startDate' => $startDate,
|
||||
'_endDate' => $endDate
|
||||
]);
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class PayrollItem extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'product_id',
|
||||
'payroll_id',
|
||||
'quantity',
|
||||
'price',
|
||||
];
|
||||
|
||||
public function product()
|
||||
{
|
||||
return $this->belongsTo(Product::class);
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreatePayrollItemsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('payroll_items', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('payroll_id')->constrained();
|
||||
$table->foreignId('product_id')->constrained();
|
||||
$table->decimal('quantity', 12, 2)->default(0);
|
||||
$table->decimal('price', 12, 2)->default(0);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('payroll_items');
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,22 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function CloseIcon({ onClick, className }) {
|
||||
return (
|
||||
<div className={className} onClick={onClick}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,37 +1,62 @@
|
||||
import { Link } from '@inertiajs/inertia-react';
|
||||
import { Inertia } from '@inertiajs/inertia'
|
||||
import qs from 'qs'
|
||||
|
||||
const PageLink = ({ active, label, url }) => {
|
||||
const className = `mr-1 mb-1 px-4 py-3 border border-solid border-gray-300 rounded text-sm bg-white hover:bg-white ${ active && 'focus:outline-none focus:border-indigo-700 focus:text-indigo-700 border-indigo-600 bg-indigo-600 text-white hover:bg-indigo-400'}`
|
||||
const PageLink = ({ active, label, url, params }) => {
|
||||
const className = `mr-1 mb-1 px-4 py-3 border border-solid border-gray-300 rounded text-sm bg-white hover:bg-white ${
|
||||
active &&
|
||||
'focus:outline-none focus:border-indigo-700 focus:text-indigo-700 border-indigo-600 bg-indigo-600 text-white hover:bg-indigo-400'
|
||||
}`
|
||||
|
||||
const onClick = () => {
|
||||
Inertia.get(
|
||||
`${url}&${qs.stringify(params)}`,
|
||||
{},
|
||||
{
|
||||
replace: true,
|
||||
preserveState: true,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Link className={className} href={url}>
|
||||
<div className={className} onClick={onClick}>
|
||||
<span dangerouslySetInnerHTML={{ __html: label }}></span>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Previous, if on first page
|
||||
// Next, if on last page
|
||||
// and dots, if exists (...)
|
||||
const PageInactive = ({ label }) => {
|
||||
const className = 'mr-1 mb-1 px-4 py-3 text-sm border rounded border-solid border-gray-300 text-gray'
|
||||
const className =
|
||||
'mr-1 mb-1 px-4 py-3 text-sm border rounded border-solid border-gray-300 text-gray'
|
||||
return (
|
||||
<div className={className} dangerouslySetInnerHTML={{ __html: label }} />
|
||||
);
|
||||
};
|
||||
<div
|
||||
className={className}
|
||||
dangerouslySetInnerHTML={{ __html: label }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default ({ links = [] }) => {
|
||||
export default ({ links = [], params = null }) => {
|
||||
// dont render, if there's only 1 page (previous, 1, next)
|
||||
if (links.length === 3) return null;
|
||||
if (links.length === 3) return null
|
||||
return (
|
||||
<div className="flex flex-wrap mt-6 -mb-1">
|
||||
{links.map(({ active, label, url }) => {
|
||||
return url === null ? (
|
||||
<PageInactive key={label} label={label} />
|
||||
) : (
|
||||
<PageLink key={label} label={label} active={active} url={url} />
|
||||
);
|
||||
<PageLink
|
||||
key={label}
|
||||
label={label}
|
||||
active={active}
|
||||
url={url}
|
||||
params={params}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
@ -1,129 +0,0 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Head } from '@inertiajs/inertia-react'
|
||||
import { Inertia } from '@inertiajs/inertia'
|
||||
import { toast } from 'react-toastify'
|
||||
|
||||
import { useModalState } from '@/Hooks'
|
||||
import Authenticated from '@/Layouts/Authenticated'
|
||||
import Pagination from '@/Components/Pagination'
|
||||
import ModalConfirm from '@/Components/ModalConfirm'
|
||||
import FormPayrollModal from '@/Modals/FormPayrollModal'
|
||||
import { formatIDR, formatDate } from '@/utils'
|
||||
|
||||
export default function Payrolls(props) {
|
||||
const { data: payrolls, links } = props.payrolls
|
||||
|
||||
const [payroll, setPayroll] = useState(null)
|
||||
const formModal = useModalState(false)
|
||||
const toggle = (payroll = null) => {
|
||||
setPayroll(payroll)
|
||||
formModal.toggle()
|
||||
}
|
||||
|
||||
const confirmModal = useModalState(false)
|
||||
const handleDelete = (payroll) => {
|
||||
confirmModal.setData(payroll)
|
||||
confirmModal.toggle()
|
||||
}
|
||||
|
||||
const onDelete = () => {
|
||||
const payroll = confirmModal.data
|
||||
if (payroll != null) {
|
||||
Inertia.delete(route('payrolls.destroy', payroll), {
|
||||
onSuccess: () => toast.success('The Data has been deleted'),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Authenticated
|
||||
auth={props.auth}
|
||||
errors={props.errors}
|
||||
header={
|
||||
<h2 className="font-semibold text-xl text-gray-800 leading-tight">
|
||||
Gaji
|
||||
</h2>
|
||||
}
|
||||
>
|
||||
<Head title="Payroll" />
|
||||
<div className="py-12">
|
||||
<div className="flex flex-col w-full sm:px-6 lg:px-8 space-y-2">
|
||||
<div className="card bg-white w-full">
|
||||
<div className="card-body">
|
||||
<div className="flex w-full mb-4 justify-between">
|
||||
<div
|
||||
className="btn btn-neutral"
|
||||
onClick={() => toggle()}
|
||||
>
|
||||
Tambah
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="table w-full table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tanggal</th>
|
||||
<th>Nama Karyawan</th>
|
||||
<th>Gaji</th>
|
||||
<th>Potongan</th>
|
||||
<th>Bonus</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{payrolls.map((payroll) => (
|
||||
<tr key={payroll.id}>
|
||||
<th>{formatDate(payroll.date)}</th>
|
||||
<td>{payroll.employee.name}</td>
|
||||
<td>
|
||||
{formatIDR(payroll.amount)}
|
||||
</td>
|
||||
<td>
|
||||
{formatIDR(payroll.cuts)}
|
||||
</td>
|
||||
<td>
|
||||
{formatIDR(payroll.bonus)}
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<div
|
||||
className="btn btn-primary mx-1"
|
||||
onClick={() =>
|
||||
toggle(payroll)
|
||||
}
|
||||
>
|
||||
Edit
|
||||
</div>
|
||||
<div
|
||||
className="btn btn-secondary mx-1"
|
||||
onClick={() =>
|
||||
handleDelete(
|
||||
payroll
|
||||
)
|
||||
}
|
||||
>
|
||||
Delete
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<Pagination links={links} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FormPayrollModal
|
||||
isOpen={formModal.isOpen}
|
||||
toggle={toggle}
|
||||
payroll={payroll}
|
||||
/>
|
||||
<ModalConfirm
|
||||
isOpen={confirmModal.isOpen}
|
||||
toggle={confirmModal.toggle}
|
||||
onConfirm={onDelete}
|
||||
/>
|
||||
</Authenticated>
|
||||
)
|
||||
}
|
@ -0,0 +1,333 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import DatePicker from 'react-datepicker'
|
||||
import NumberFormat from 'react-number-format'
|
||||
import { Inertia } from '@inertiajs/inertia'
|
||||
import { usePrevious } from 'react-use'
|
||||
import { toast } from 'react-toastify'
|
||||
import { Head, useForm } from '@inertiajs/inertia-react'
|
||||
|
||||
import Authenticated from '@/Layouts/Authenticated'
|
||||
import Pagination from '@/Components/Pagination'
|
||||
import CloseIcon from '@/Components/CloseIcon'
|
||||
import EmployeeSelectInput from '@/Selects/EmployeeSelectInput'
|
||||
import { formatIDR } from '@/utils'
|
||||
|
||||
export default function Create(props) {
|
||||
const { data: products, links } = props.products
|
||||
const { _search, _page } = props
|
||||
const { data, setData, post, errors, processing } = useForm({
|
||||
date: new Date(),
|
||||
employee_id: null,
|
||||
employee_name: '',
|
||||
cuts: 0,
|
||||
bonus: 0,
|
||||
items: [],
|
||||
})
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const [search, setSearch] = useState(_search)
|
||||
const preValue = usePrevious(search)
|
||||
|
||||
const handleSelectedEmployee = (employee) => {
|
||||
setData({
|
||||
...data,
|
||||
employee_id: employee.id,
|
||||
employee_name: `${employee.name} - ${employee.whatsapp}`,
|
||||
})
|
||||
}
|
||||
|
||||
const addItem = (product) => {
|
||||
const itemExist = data.items.find(item => item.id === product.id)
|
||||
if(itemExist) {
|
||||
setData(
|
||||
'items',
|
||||
data.items.map(item => {
|
||||
if(item.id === product.id){
|
||||
return {
|
||||
...item,
|
||||
quantity: +item.quantity + 1,
|
||||
}
|
||||
} else {
|
||||
return item
|
||||
}
|
||||
})
|
||||
)
|
||||
return
|
||||
}
|
||||
setData('items', data.items.concat({
|
||||
...product,
|
||||
product_id: product.id,
|
||||
quantity: 1
|
||||
}))
|
||||
}
|
||||
|
||||
const remoteItem = (product) => {
|
||||
setData('items', data.items.filter(item => item.id !== product.id))
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
setData({
|
||||
employee_id: null,
|
||||
employee_name: '',
|
||||
cuts: 0,
|
||||
bonus: 0,
|
||||
items: [],
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (data.items.length <= 0) {
|
||||
alert('barang belum di pilih')
|
||||
return
|
||||
}
|
||||
post(route('payrolls.store'), {
|
||||
onSuccess: () =>
|
||||
Promise.all([
|
||||
handleReset(),
|
||||
toast.success('The Data has been saved'),
|
||||
]),
|
||||
})
|
||||
}
|
||||
|
||||
const itemAmount = data.items.reduce((amt, item) => amt + (+item.quantity * +item.price), 0)
|
||||
const totalAmount = itemAmount - +data.cuts + +data.bonus
|
||||
|
||||
useEffect(() => {
|
||||
if (preValue) {
|
||||
setLoading(true)
|
||||
Inertia.get(
|
||||
route(route().current()),
|
||||
{ q: search, page: _page },
|
||||
{
|
||||
replace: true,
|
||||
preserveState: true,
|
||||
onSuccess: () => { setLoading(false) },
|
||||
}
|
||||
)
|
||||
}
|
||||
}, [search])
|
||||
|
||||
return (
|
||||
<Authenticated
|
||||
auth={props.auth}
|
||||
errors={props.errors}
|
||||
header={
|
||||
<h2 className="font-semibold text-xl text-gray-800 leading-tight">
|
||||
Gaji
|
||||
</h2>
|
||||
}
|
||||
>
|
||||
<Head title="Payrolls" />
|
||||
<div className="py-12">
|
||||
<div className="flex flex-col md:flex-row w-full sm:px-6 lg:px-8 space-y-4 md:space-x-4 md:space-y-0">
|
||||
<div className="card bg-white w-full md:w-2/3">
|
||||
<div className="p-4">
|
||||
<div className="flex flex-row justify-end mb-2">
|
||||
<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={`grid grid-cols-4 gap-4 ${
|
||||
loading && 'opacity-70'
|
||||
}`}
|
||||
>
|
||||
{products.map((product) => (
|
||||
<div
|
||||
className="rounded bg-white shadow-md"
|
||||
key={product.id}
|
||||
onClick={() => addItem(product)}
|
||||
>
|
||||
<img
|
||||
src={product.photo_url}
|
||||
style={{ height: '100px' }}
|
||||
/>
|
||||
<div className="p-4 flex flex-col justify-items-center items-center space-y-4">
|
||||
<div className="font-bold text-center">
|
||||
{product.name}
|
||||
</div>
|
||||
<div className="badge">
|
||||
{formatIDR(product.price)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<div>
|
||||
<Pagination
|
||||
links={links}
|
||||
params={{ q: search }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card bg-white w-full md:w-1/3">
|
||||
<div className="flex flex-col p-2 mb-4">
|
||||
<div>
|
||||
<DatePicker
|
||||
selected={data.date}
|
||||
onChange={(date) => setData('date', date)}
|
||||
format="dd/mm/yyyy"
|
||||
className={`input input-bordered ${
|
||||
errors.date ? 'input-error' : ''
|
||||
}`}
|
||||
nextMonthButtonLabel=">"
|
||||
previousMonthButtonLabel="<"
|
||||
/>
|
||||
{errors.date && (
|
||||
<label className="label">
|
||||
<span className="label-text-alt">
|
||||
{errors.date}
|
||||
</span>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<EmployeeSelectInput
|
||||
value={data.employee_name}
|
||||
onItemSelected={handleSelectedEmployee}
|
||||
invalid={errors.employee_id ? true : false}
|
||||
/>
|
||||
<label className="label">
|
||||
<span className="label-text-alt">
|
||||
{errors.employee_id}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
className="overflow-x-auto"
|
||||
style={{ minHeight: '280px' }}
|
||||
>
|
||||
<table className="table w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Barang</th>
|
||||
<th>Qty</th>
|
||||
<th>Subtotal</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.items.map((item) => (
|
||||
<tr key={item.id}>
|
||||
<td>{item.name}</td>
|
||||
<td>
|
||||
{formatIDR(item.quantity)}
|
||||
</td>
|
||||
<td>
|
||||
{formatIDR(
|
||||
item.quantity * item.price
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<CloseIcon
|
||||
className="btn btn-outline btn-sm px-0.5"
|
||||
onClick={() =>
|
||||
remoteItem(item)
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="card-actions">
|
||||
<div className="form-control w-full">
|
||||
<label className="input-group w-full">
|
||||
<span>Potongan</span>
|
||||
<NumberFormat
|
||||
thousandSeparator={true}
|
||||
className={`input input-bordered w-full text-right ${
|
||||
errors.cuts ? 'input-error' : ''
|
||||
}`}
|
||||
value={data.cuts}
|
||||
thousandSeparator="."
|
||||
decimalSeparator=","
|
||||
onValueChange={({ value }) =>
|
||||
setData('cuts', value)
|
||||
}
|
||||
placeholder="potongan"
|
||||
/>
|
||||
</label>
|
||||
{errors.cuts && (
|
||||
<label className="label">
|
||||
<span className="label-text-alt">
|
||||
{errors.cuts}
|
||||
</span>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
<div className="form-control w-full">
|
||||
<label className="input-group w-full">
|
||||
<span>Bonus</span>
|
||||
<NumberFormat
|
||||
thousandSeparator={true}
|
||||
className={`input input-bordered w-full text-right ${
|
||||
errors.bonus ? 'input-error' : ''
|
||||
}`}
|
||||
value={data.bonus}
|
||||
thousandSeparator="."
|
||||
decimalSeparator=","
|
||||
onValueChange={({ value }) =>
|
||||
setData('bonus', value)
|
||||
}
|
||||
placeholder="bonus"
|
||||
/>
|
||||
</label>
|
||||
{errors.bonus && (
|
||||
<label className="label">
|
||||
<span className="label-text-alt">
|
||||
{errors.bonus}
|
||||
</span>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
<div className="form-control w-full mt-2">
|
||||
<label className="input-group w-full">
|
||||
<span>Total</span>
|
||||
<NumberFormat
|
||||
thousandSeparator={true}
|
||||
className="input input-bordered w-full text-right"
|
||||
value={totalAmount}
|
||||
thousandSeparator="."
|
||||
decimalSeparator=","
|
||||
readOnly={true}
|
||||
placeholder="total"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 w-full">
|
||||
<div
|
||||
className="btn btn-primary"
|
||||
disabled={processing}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Simpan
|
||||
</div>
|
||||
<div
|
||||
className="btn btn-primary"
|
||||
disabled={processing}
|
||||
>
|
||||
Cetak
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Authenticated>
|
||||
)
|
||||
}
|
@ -0,0 +1,353 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import DatePicker from 'react-datepicker'
|
||||
import NumberFormat from 'react-number-format'
|
||||
import { Inertia } from '@inertiajs/inertia'
|
||||
import { usePrevious } from 'react-use'
|
||||
import { toast } from 'react-toastify'
|
||||
import { Head, useForm } from '@inertiajs/inertia-react'
|
||||
|
||||
import Authenticated from '@/Layouts/Authenticated'
|
||||
import Pagination from '@/Components/Pagination'
|
||||
import CloseIcon from '@/Components/CloseIcon'
|
||||
import EmployeeSelectInput from '@/Selects/EmployeeSelectInput'
|
||||
import { formatIDR } from '@/utils'
|
||||
|
||||
export default function Edit(props) {
|
||||
const { data: products, links } = props.products
|
||||
const { payroll, _search, _page } = props
|
||||
const { data, setData, put, errors, processing } = useForm({
|
||||
date: new Date(payroll.date),
|
||||
employee_id: payroll.employee_id,
|
||||
employee_name: `${payroll.employee.name} - ${payroll.employee.whatsapp}`,
|
||||
cuts: payroll.cuts,
|
||||
bonus: payroll.bonus,
|
||||
items: payroll.items.map(item => {
|
||||
return {
|
||||
product_id: item.product_id,
|
||||
quantity: item.quantity,
|
||||
...item.product,
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const [search, setSearch] = useState(_search)
|
||||
const preValue = usePrevious(search)
|
||||
|
||||
const handleSelectedEmployee = (employee) => {
|
||||
setData({
|
||||
...data,
|
||||
employee_id: employee.id,
|
||||
employee_name: `${employee.name} - ${employee.whatsapp}`,
|
||||
})
|
||||
}
|
||||
|
||||
const addItem = (product) => {
|
||||
const itemExist = data.items.find((item) => item.id === product.id)
|
||||
if (itemExist) {
|
||||
setData(
|
||||
'items',
|
||||
data.items.map((item) => {
|
||||
if (item.id === product.id) {
|
||||
return {
|
||||
...item,
|
||||
quantity: +item.quantity + 1,
|
||||
}
|
||||
} else {
|
||||
return item
|
||||
}
|
||||
})
|
||||
)
|
||||
return
|
||||
}
|
||||
setData(
|
||||
'items',
|
||||
data.items.concat({
|
||||
...product,
|
||||
product_id: product.id,
|
||||
quantity: 1,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const remoteItem = (product) => {
|
||||
setData(
|
||||
'items',
|
||||
data.items.filter((item) => item.id !== product.id)
|
||||
)
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
setData({
|
||||
employee_id: null,
|
||||
employee_name: '',
|
||||
cuts: 0,
|
||||
bonus: 0,
|
||||
items: [],
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (data.items.length <= 0) {
|
||||
alert('barang belum di pilih')
|
||||
return
|
||||
}
|
||||
put(route('payrolls.update', payroll), {
|
||||
onSuccess: () =>
|
||||
Promise.all([
|
||||
handleReset(),
|
||||
toast.success('The Data has been saved'),
|
||||
]),
|
||||
})
|
||||
}
|
||||
|
||||
const itemAmount = data.items.reduce(
|
||||
(amt, item) => amt + +item.quantity * +item.price,
|
||||
0
|
||||
)
|
||||
const totalAmount = itemAmount - +data.cuts + +data.bonus
|
||||
|
||||
useEffect(() => {
|
||||
if (preValue) {
|
||||
setLoading(true)
|
||||
Inertia.get(
|
||||
route(route().current(), payroll),
|
||||
{ q: search, page: _page },
|
||||
{
|
||||
replace: true,
|
||||
preserveState: true,
|
||||
onSuccess: () => {
|
||||
setLoading(false)
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
}, [search])
|
||||
|
||||
return (
|
||||
<Authenticated
|
||||
auth={props.auth}
|
||||
errors={props.errors}
|
||||
header={
|
||||
<h2 className="font-semibold text-xl text-gray-800 leading-tight">
|
||||
Gaji
|
||||
</h2>
|
||||
}
|
||||
>
|
||||
<Head title="Payrolls" />
|
||||
<div className="py-12">
|
||||
<div className="flex flex-col md:flex-row w-full sm:px-6 lg:px-8 space-y-4 md:space-x-4 md:space-y-0">
|
||||
<div className="card bg-white w-full md:w-2/3">
|
||||
<div className="p-4">
|
||||
<div className="flex flex-row justify-end mb-2">
|
||||
<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={`grid grid-cols-4 gap-4 ${
|
||||
loading && 'opacity-70'
|
||||
}`}
|
||||
>
|
||||
{products.map((product) => (
|
||||
<div
|
||||
className="rounded bg-white shadow-md"
|
||||
key={product.id}
|
||||
onClick={() => addItem(product)}
|
||||
>
|
||||
<img
|
||||
src={product.photo_url}
|
||||
style={{ height: '100px' }}
|
||||
/>
|
||||
<div className="p-4 flex flex-col justify-items-center items-center space-y-4">
|
||||
<div className="font-bold text-center">
|
||||
{product.name}
|
||||
</div>
|
||||
<div className="badge">
|
||||
{formatIDR(product.price)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<div>
|
||||
<Pagination
|
||||
links={links}
|
||||
params={{ q: search }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card bg-white w-full md:w-1/3">
|
||||
<div className="flex flex-col p-2 mb-4">
|
||||
<div>
|
||||
<DatePicker
|
||||
selected={data.date}
|
||||
onChange={(date) => setData('date', date)}
|
||||
format="dd/mm/yyyy"
|
||||
className={`input input-bordered ${
|
||||
errors.date ? 'input-error' : ''
|
||||
}`}
|
||||
nextMonthButtonLabel=">"
|
||||
previousMonthButtonLabel="<"
|
||||
/>
|
||||
{errors.date && (
|
||||
<label className="label">
|
||||
<span className="label-text-alt">
|
||||
{errors.date}
|
||||
</span>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<EmployeeSelectInput
|
||||
value={data.employee_name}
|
||||
onItemSelected={handleSelectedEmployee}
|
||||
invalid={errors.employee_id ? true : false}
|
||||
/>
|
||||
<label className="label">
|
||||
<span className="label-text-alt">
|
||||
{errors.employee_id}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
className="overflow-x-auto"
|
||||
style={{ minHeight: '280px' }}
|
||||
>
|
||||
<table className="table w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Barang</th>
|
||||
<th>Qty</th>
|
||||
<th>Subtotal</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.items.map((item) => (
|
||||
<tr key={item.id}>
|
||||
<td>{item.name}</td>
|
||||
<td>
|
||||
{formatIDR(item.quantity)}
|
||||
</td>
|
||||
<td>
|
||||
{formatIDR(
|
||||
item.quantity *
|
||||
item.price
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<CloseIcon
|
||||
className="btn btn-outline btn-sm px-0.5"
|
||||
onClick={() =>
|
||||
remoteItem(item)
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="card-actions">
|
||||
<div className="form-control w-full">
|
||||
<label className="input-group w-full">
|
||||
<span>Potongan</span>
|
||||
<NumberFormat
|
||||
thousandSeparator={true}
|
||||
className={`input input-bordered w-full text-right ${
|
||||
errors.cuts ? 'input-error' : ''
|
||||
}`}
|
||||
value={formatIDR(data.cuts)}
|
||||
thousandSeparator="."
|
||||
decimalSeparator=","
|
||||
onValueChange={({ value }) =>
|
||||
setData('cuts', value)
|
||||
}
|
||||
placeholder="potongan"
|
||||
/>
|
||||
</label>
|
||||
{errors.cuts && (
|
||||
<label className="label">
|
||||
<span className="label-text-alt">
|
||||
{errors.cuts}
|
||||
</span>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
<div className="form-control w-full">
|
||||
<label className="input-group w-full">
|
||||
<span>Bonus</span>
|
||||
<NumberFormat
|
||||
thousandSeparator={true}
|
||||
className={`input input-bordered w-full text-right ${
|
||||
errors.bonus
|
||||
? 'input-error'
|
||||
: ''
|
||||
}`}
|
||||
value={formatIDR(data.bonus)}
|
||||
thousandSeparator="."
|
||||
decimalSeparator=","
|
||||
onValueChange={({ value }) =>
|
||||
setData('bonus', value)
|
||||
}
|
||||
placeholder="bonus"
|
||||
/>
|
||||
</label>
|
||||
{errors.bonus && (
|
||||
<label className="label">
|
||||
<span className="label-text-alt">
|
||||
{errors.bonus}
|
||||
</span>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
<div className="form-control w-full mt-2">
|
||||
<label className="input-group w-full">
|
||||
<span>Total</span>
|
||||
<NumberFormat
|
||||
thousandSeparator={true}
|
||||
className="input input-bordered w-full text-right"
|
||||
value={totalAmount}
|
||||
thousandSeparator="."
|
||||
decimalSeparator=","
|
||||
readOnly={true}
|
||||
placeholder="total"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 w-full">
|
||||
<div
|
||||
className="btn btn-primary"
|
||||
disabled={processing}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Simpan
|
||||
</div>
|
||||
<div
|
||||
className="btn btn-primary"
|
||||
disabled={processing}
|
||||
>
|
||||
Cetak
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Authenticated>
|
||||
)
|
||||
}
|
@ -0,0 +1,217 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import DatePicker from 'react-datepicker'
|
||||
import moment from 'moment'
|
||||
import { Head, Link } from '@inertiajs/inertia-react'
|
||||
import { Inertia } from '@inertiajs/inertia'
|
||||
import { usePrevious } from 'react-use'
|
||||
import { toast } from 'react-toastify'
|
||||
|
||||
import { useModalState } from '@/Hooks'
|
||||
import Authenticated from '@/Layouts/Authenticated'
|
||||
import Pagination from '@/Components/Pagination'
|
||||
import ModalConfirm from '@/Components/ModalConfirm'
|
||||
import { formatIDR, formatDate } from '@/utils'
|
||||
|
||||
export default function Payrolls(props) {
|
||||
const { data: payrolls, links } = props.payrolls
|
||||
const { _startDate, _endDate } = props
|
||||
|
||||
const [startDate, setStartDate] = useState(_startDate ? new Date(_startDate) : new Date())
|
||||
const [endDate, setEndDate] = useState(_endDate ? new Date(_endDate) : new Date())
|
||||
const preValue = usePrevious(`${startDate}-${endDate}`)
|
||||
|
||||
const confirmModal = useModalState(false)
|
||||
const handleDelete = (payroll) => {
|
||||
confirmModal.setData(payroll)
|
||||
confirmModal.toggle()
|
||||
}
|
||||
|
||||
const onDelete = () => {
|
||||
const payroll = confirmModal.data
|
||||
if (payroll != null) {
|
||||
Inertia.delete(route('payrolls.destroy', payroll), {
|
||||
onSuccess: () => toast.success('The Data has been deleted'),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const params = {
|
||||
startDate: moment(startDate).format('yyyy-MM-DD'),
|
||||
endDate: moment(endDate).format('yyyy-MM-DD'),
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (preValue) {
|
||||
Inertia.get(
|
||||
route(route().current()),
|
||||
params,
|
||||
{
|
||||
replace: true,
|
||||
preserveState: true,
|
||||
}
|
||||
)
|
||||
}
|
||||
}, [startDate, endDate])
|
||||
|
||||
return (
|
||||
<Authenticated
|
||||
auth={props.auth}
|
||||
errors={props.errors}
|
||||
header={
|
||||
<h2 className="font-semibold text-xl text-gray-800 leading-tight">
|
||||
Gaji
|
||||
</h2>
|
||||
}
|
||||
>
|
||||
<Head title="Payroll" />
|
||||
<div className="py-12">
|
||||
<div className="flex flex-col w-full sm:px-6 lg:px-8 space-y-2">
|
||||
<div
|
||||
className="card bg-white w-full"
|
||||
style={{ minHeight: '400px' }}
|
||||
>
|
||||
<div className="card-body">
|
||||
<div className="flex flex-col md:flex-row space-y-2 md:space-y-0 items-start md:items-stretch w-full mb-4 justify-between">
|
||||
<Link
|
||||
className="btn btn-neutral my-auto"
|
||||
href={route('payrolls.create')}
|
||||
>
|
||||
Tambah
|
||||
</Link>
|
||||
<div className="flex flex-row md:space-x-4">
|
||||
<div>
|
||||
<label className="label">
|
||||
<span className="label-text">
|
||||
Tanggal Awal
|
||||
</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<DatePicker
|
||||
selected={startDate}
|
||||
onChange={(date) => {
|
||||
setStartDate(date)
|
||||
}}
|
||||
format="dd/mm/yyyy"
|
||||
className="input input-bordered"
|
||||
nextMonthButtonLabel=">"
|
||||
previousMonthButtonLabel="<"
|
||||
/>
|
||||
<div className="absolute right-2.5 rounded-l-none y-0 flex items-center top-2.5">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="label">
|
||||
<span className="label-text">
|
||||
Tanggal Akhir
|
||||
</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<DatePicker
|
||||
selected={endDate}
|
||||
onChange={(date) => {
|
||||
setEndDate(date)
|
||||
}}
|
||||
format="dd/mm/yyyy"
|
||||
className="input input-bordered"
|
||||
nextMonthButtonLabel=">"
|
||||
previousMonthButtonLabel="<"
|
||||
/>
|
||||
<div className="absolute right-2.5 rounded-l-none y-0 flex items-center top-2.5">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="table w-full table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tanggal</th>
|
||||
<th>Nama Karyawan</th>
|
||||
<th>Potongan</th>
|
||||
<th>Bonus</th>
|
||||
<th>Total</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{payrolls.map((payroll) => (
|
||||
<tr key={payroll.id}>
|
||||
<th>
|
||||
{formatDate(payroll.date)}
|
||||
</th>
|
||||
<td>{payroll.employee.name}</td>
|
||||
<td>
|
||||
{formatIDR(payroll.cuts)}
|
||||
</td>
|
||||
<td>
|
||||
{formatIDR(payroll.bonus)}
|
||||
</td>
|
||||
<td>
|
||||
{formatIDR(payroll.recived)}
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<Link
|
||||
className="btn btn-primary mx-1"
|
||||
href={route('payrolls.edit', payroll)}
|
||||
>
|
||||
Edit
|
||||
</Link>
|
||||
<div
|
||||
className="btn btn-secondary mx-1"
|
||||
onClick={() =>
|
||||
handleDelete(
|
||||
payroll
|
||||
)
|
||||
}
|
||||
>
|
||||
Delete
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<Pagination links={links} params={params} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ModalConfirm
|
||||
isOpen={confirmModal.isOpen}
|
||||
toggle={confirmModal.toggle}
|
||||
onConfirm={onDelete}
|
||||
/>
|
||||
</Authenticated>
|
||||
)
|
||||
}
|
Loading…
Reference in New Issue