barang done
parent
d344243dcc
commit
1d8cc39b1f
@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Product;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class ProductController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
if ($request->q != null) {
|
||||
$query = Product::where('name', 'like', '%'.$request->q.'%')->orWhere('description', 'like', '%'.$request->q.'%')->orderBy('id');
|
||||
} else {
|
||||
$query = Product::orderBy('id');
|
||||
}
|
||||
|
||||
return inertia('Products', [
|
||||
'products' => $query->paginate(10),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'name' => 'required|string',
|
||||
'price' => 'nullable|numeric',
|
||||
'description' => 'nullable|string',
|
||||
'photo' => 'nullable|image'
|
||||
]);
|
||||
|
||||
$product = Product::make($request->only(['name', 'price', 'description']));
|
||||
$photo = $request->file('photo');
|
||||
if ($photo != null) {
|
||||
$photo->store('public');
|
||||
$product->photo = $photo->hashName();
|
||||
}
|
||||
|
||||
$product->save();
|
||||
|
||||
return redirect()->route('products.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param int $id
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function update(Request $request, Product $product)
|
||||
{
|
||||
$request->validate([
|
||||
'name' => 'required|string',
|
||||
'price' => 'nullable|numeric',
|
||||
'description' => 'nullable|string',
|
||||
'photo' => 'nullable|image'
|
||||
]);
|
||||
|
||||
$product->fill($request->only(['name', 'price', 'description']));
|
||||
|
||||
$photo = $request->file('photo');
|
||||
if ($photo != null) {
|
||||
if ($product->photo != null) {
|
||||
Storage::delete('public/'.$product->photo);
|
||||
$product->photo = null;
|
||||
}
|
||||
$photo->store('public');
|
||||
$product->photo = $photo->hashName();
|
||||
}
|
||||
|
||||
$product->save();
|
||||
|
||||
return redirect()->route('products.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*
|
||||
* @param int $id
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function destroy(Product $product)
|
||||
{
|
||||
if ($product->photo != null) {
|
||||
Storage::delete('public/'.$product->photo);
|
||||
}
|
||||
$product->delete();
|
||||
|
||||
return redirect()->route('products.index');
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,190 @@
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import NumberFormat from 'react-number-format'
|
||||
import { useForm } from '@inertiajs/inertia-react'
|
||||
import { toast } from 'react-toastify'
|
||||
import { formatIDR } from '@/utils'
|
||||
|
||||
export default function FormProductModal(props) {
|
||||
const { isOpen, toggle = () => {}, product = null } = props
|
||||
|
||||
const { data, setData, post, processing, errors, clearErrors } =
|
||||
useForm({
|
||||
name: '',
|
||||
price: 0,
|
||||
description: '',
|
||||
photo: null,
|
||||
img_alt: null,
|
||||
})
|
||||
|
||||
const inputPhoto = useRef()
|
||||
|
||||
const handleOnChange = (event) => {
|
||||
setData(event.target.name, event.target.value)
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
setData({
|
||||
name: '',
|
||||
price: 0,
|
||||
description: '',
|
||||
photo: null,
|
||||
img_alt: null,
|
||||
})
|
||||
clearErrors()
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
handleReset()
|
||||
toggle()
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (product !== null) {
|
||||
post(route('products.update', product), {
|
||||
forceFormData: true,
|
||||
onSuccess: () =>
|
||||
Promise.all([
|
||||
handleReset(),
|
||||
toggle(),
|
||||
toast.success('The Data has been changed'),
|
||||
]),
|
||||
})
|
||||
return
|
||||
}
|
||||
post(route('products.store'), {
|
||||
onSuccess: () =>
|
||||
Promise.all([
|
||||
handleReset(),
|
||||
toggle(),
|
||||
toast.success('The Data has been saved'),
|
||||
]),
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setData({
|
||||
name: product?.name ? product.name : '',
|
||||
price: formatIDR(product?.price ? product.price : 0),
|
||||
description: product?.description ? product.description : '',
|
||||
img_alt: product?.photo_url ? product.photo_url : null,
|
||||
})
|
||||
}, [product])
|
||||
|
||||
return (
|
||||
<div
|
||||
className="modal"
|
||||
style={
|
||||
isOpen
|
||||
? {
|
||||
opacity: 1,
|
||||
pointerEvents: 'auto',
|
||||
visibility: 'visible',
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<div className="modal-box">
|
||||
<h1 className="font-bold text-2xl pb-8">Barang</h1>
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text">Nama</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="nama"
|
||||
className={`input input-bordered ${
|
||||
errors.name && 'input-error'
|
||||
}`}
|
||||
name="name"
|
||||
value={data.name}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
<label className="label">
|
||||
<span className="label-text-alt">{errors.name}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text">Harga</span>
|
||||
</label>
|
||||
<NumberFormat
|
||||
thousandSeparator={true}
|
||||
className={`input input-bordered ${
|
||||
errors.price ? 'input-error' : ''
|
||||
}`}
|
||||
value={data.price}
|
||||
thousandSeparator="."
|
||||
decimalSeparator=","
|
||||
onValueChange={({ value }) => setData('price', value)}
|
||||
placeholder="harga"
|
||||
/>
|
||||
<label className="label">
|
||||
<span className="label-text-alt">{errors.price}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text">Deskripsi</span>
|
||||
</label>
|
||||
<textarea
|
||||
className={`textarea h-24 textarea-bordered ${
|
||||
errors.description && 'input-error'
|
||||
}`}
|
||||
name="description"
|
||||
placeholder="Deskripsi"
|
||||
value={data.description}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
<label className="label">
|
||||
<span className="label-text-alt">
|
||||
{errors.description}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text">Foto</span>
|
||||
</label>
|
||||
<div
|
||||
className={`input input-bordered ${
|
||||
errors.photo && 'input-error'
|
||||
}`}
|
||||
onClick={() => {console.log(inputPhoto.current.click())}}
|
||||
>{data.photo ? data.photo.name : ''}</div>
|
||||
<input
|
||||
ref={inputPhoto}
|
||||
type="file"
|
||||
className="hidden"
|
||||
name="photo"
|
||||
onChange={(e) => setData('photo', e.target.files[0])}
|
||||
accept="image/png, image/jpeg, image/jpg"
|
||||
/>
|
||||
<label className="label">
|
||||
<span className="label-text-alt">{errors.photo}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-control">
|
||||
{data.img_alt !== null && (
|
||||
<img src={data.img_alt}/>
|
||||
)}
|
||||
</div>
|
||||
<div className="modal-action">
|
||||
<div
|
||||
onClick={handleSubmit}
|
||||
className="btn btn-primary"
|
||||
disabled={processing}
|
||||
>
|
||||
Simpan
|
||||
</div>
|
||||
<div
|
||||
onClick={handleCancel}
|
||||
className="btn btn-secondary"
|
||||
disabled={processing}
|
||||
>
|
||||
Batal
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,24 +1,163 @@
|
||||
import React from 'react'
|
||||
import Authenticated from '@/Layouts/Authenticated'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Head } from '@inertiajs/inertia-react'
|
||||
import { Inertia } from '@inertiajs/inertia'
|
||||
import { usePrevious } from 'react-use'
|
||||
import { toast } from 'react-toastify'
|
||||
|
||||
import { useModalState } from '@/Hooks'
|
||||
import { formatIDR } from '@/utils'
|
||||
import Authenticated from '@/Layouts/Authenticated'
|
||||
import Pagination from '@/Components/Pagination'
|
||||
import ModalConfirm from '@/Components/ModalConfirm'
|
||||
import FormProductModal from '@/Modals/FormProductModal'
|
||||
|
||||
export default function Products(props) {
|
||||
return (
|
||||
<Authenticated
|
||||
auth={props.auth}
|
||||
errors={props.errors}
|
||||
header={
|
||||
<h2 className="font-semibold text-xl text-gray-800 leading-tight">
|
||||
Barang
|
||||
</h2>
|
||||
}
|
||||
>
|
||||
<Head title="Products" />
|
||||
<div className="py-12">
|
||||
<div className="flex flex-col sm:px-6 lg:px-8 space-x-4">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</Authenticated>
|
||||
)
|
||||
}
|
||||
const { data: products, links } = props.products
|
||||
|
||||
const [search, setSearch] = useState('')
|
||||
const preValue = usePrevious(search)
|
||||
|
||||
const [product, setProduct] = useState(null)
|
||||
const formModal = useModalState(false)
|
||||
const toggle = (product = null) => {
|
||||
setProduct(product)
|
||||
formModal.toggle()
|
||||
}
|
||||
|
||||
const confirmModal = useModalState(false)
|
||||
const handleDelete = (product) => {
|
||||
confirmModal.setData(product)
|
||||
confirmModal.toggle()
|
||||
}
|
||||
|
||||
const onDelete = () => {
|
||||
const product = confirmModal.data
|
||||
if (product != null) {
|
||||
Inertia.delete(route('products.destroy', product), {
|
||||
onSuccess: () => toast.success('The Data has been deleted'),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (preValue) {
|
||||
Inertia.get(
|
||||
route(route().current()),
|
||||
{ q: search },
|
||||
{
|
||||
replace: true,
|
||||
preserveState: true,
|
||||
}
|
||||
)
|
||||
}
|
||||
}, [search])
|
||||
|
||||
return (
|
||||
<Authenticated
|
||||
auth={props.auth}
|
||||
errors={props.errors}
|
||||
header={
|
||||
<h2 className="font-semibold text-xl text-gray-800 leading-tight">
|
||||
Barang
|
||||
</h2>
|
||||
}
|
||||
>
|
||||
<Head title="Products" />
|
||||
<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 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>Id</th>
|
||||
<th>Nama</th>
|
||||
<th>Harga</th>
|
||||
<th>Deskripsi</th>
|
||||
<th>Foto</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{products?.map((product) => (
|
||||
<tr key={product.id}>
|
||||
<th>{product.id}</th>
|
||||
<td>{product.name}</td>
|
||||
<td>
|
||||
{formatIDR(product.price)}
|
||||
</td>
|
||||
<td>{product.description}</td>
|
||||
<td>
|
||||
{product.photo_url !==
|
||||
null && (
|
||||
<img
|
||||
width="100px"
|
||||
src={
|
||||
product.photo_url
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<div
|
||||
className="btn btn-primary mx-1"
|
||||
onClick={() =>
|
||||
toggle(product)
|
||||
}
|
||||
>
|
||||
Edit
|
||||
</div>
|
||||
<div
|
||||
className="btn btn-secondary mx-1"
|
||||
onClick={() =>
|
||||
handleDelete(
|
||||
product
|
||||
)
|
||||
}
|
||||
>
|
||||
Delete
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<Pagination links={links} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FormProductModal
|
||||
isOpen={formModal.isOpen}
|
||||
toggle={toggle}
|
||||
product={product}
|
||||
/>
|
||||
<ModalConfirm
|
||||
isOpen={confirmModal.isOpen}
|
||||
toggle={confirmModal.toggle}
|
||||
onConfirm={onDelete}
|
||||
/>
|
||||
</Authenticated>
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,8 @@
|
||||
export const formatDate = (date) => {
|
||||
return date.toLocaleDateString()
|
||||
}
|
||||
|
||||
export function formatIDR(amount) {
|
||||
const idFormatter = new Intl.NumberFormat('id-ID')
|
||||
return idFormatter.format(amount)
|
||||
}
|
Loading…
Reference in New Issue