barang done

pull/1/head
Aji Kamaludin 3 years ago
parent d344243dcc
commit 1d8cc39b1f
No known key found for this signature in database
GPG Key ID: 670E1F26AD5A8099

@ -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');
}
}

@ -4,6 +4,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;
class Product extends Model class Product extends Model
{ {
@ -15,4 +16,14 @@ class Product extends Model
'price', 'price',
'description', 'description',
]; ];
protected $appends = ['photo_url'];
public function getPhotoUrlAttribute()
{
if ($this->photo != null) {
return asset(Storage::url($this->photo));
}
return null;
}
} }

63
public/css/app.css vendored

@ -1801,6 +1801,27 @@ html {
left: 0; left: 0;
z-index: 10; z-index: 10;
} }
.textarea {
flex-shrink: 1;
transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform;
transition-duration: .2s;
transition-timing-function: cubic-bezier(.4, 0, .2, 1);
font-size: .875rem;
line-height: 2;
padding: .5rem 1rem;
min-height: 3rem;
--tw-bg-opacity: 1;
background-color: hsla(var(--b1) / var(--tw-bg-opacity, 1));
--tw-border-opacity: 0;
border-color: hsla(var(--bc) / var(--tw-border-opacity, 1));
border-width: 1px;
border-radius: var(--rounded-btn, .5rem);
}
.textarea:focus {
outline: 2px solid transparent;
outline-offset: 2px;
box-shadow: 0 0 0 2px hsl(var(--b1)), 0 0 0 4px hsla(var(--bc) / .2);
}
.toggle:focus { .toggle:focus {
outline: 2px solid transparent; outline: 2px solid transparent;
outline-offset: 2px; outline-offset: 2px;
@ -2293,6 +2314,11 @@ html {
--tw-text-opacity: 0.4; --tw-text-opacity: 0.4;
color: hsla(var(--bc) / var(--tw-text-opacity, 1)); color: hsla(var(--bc) / var(--tw-text-opacity, 1));
} }
.mockup-phone .display {
overflow: hidden;
border-radius: 40px;
margin-top: -25px;
}
.modal-box { .modal-box {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: hsla(var(--b1) / var(--tw-bg-opacity, 1)); background-color: hsla(var(--b1) / var(--tw-bg-opacity, 1));
@ -2421,6 +2447,37 @@ html {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: hsla(var(--b2) / var(--tw-bg-opacity, 1)); background-color: hsla(var(--b2) / var(--tw-bg-opacity, 1));
} }
.textarea-bordered {
--tw-border-opacity: 0.2;
}
.textarea-disabled,.textarea[disabled] {
--tw-bg-opacity: 1;
background-color: hsla(var(--b2) / var(--tw-bg-opacity, 1));
--tw-border-opacity: 1;
border-color: hsla(var(--b2) / var(--tw-border-opacity, 1));
cursor: not-allowed;
--tw-text-opacity: 0.2;
}
.textarea-disabled::-moz-placeholder,.textarea[disabled]::-moz-placeholder {
--tw-placeholder-opacity: 0.2;
color: hsla(var(--bc) / var(--tw-placeholder-opacity, 1));
}
.textarea-disabled:-ms-input-placeholder,.textarea[disabled]:-ms-input-placeholder {
--tw-placeholder-opacity: 0.2;
color: hsla(var(--bc) / var(--tw-placeholder-opacity, 1));
}
.textarea-disabled::-moz-placeholder, .textarea[disabled]::-moz-placeholder {
--tw-placeholder-opacity: 0.2;
color: hsla(var(--bc) / var(--tw-placeholder-opacity, 1));
}
.textarea-disabled:-ms-input-placeholder, .textarea[disabled]:-ms-input-placeholder {
--tw-placeholder-opacity: 0.2;
color: hsla(var(--bc) / var(--tw-placeholder-opacity, 1));
}
.textarea-disabled::placeholder,.textarea[disabled]::placeholder {
--tw-placeholder-opacity: 0.2;
color: hsla(var(--bc) / var(--tw-placeholder-opacity, 1));
}
.toggle { .toggle {
--chkbg: hsla(var(--bc) / .2); --chkbg: hsla(var(--bc) / .2);
--focus-shadow: 0 0 0; --focus-shadow: 0 0 0;
@ -2463,6 +2520,9 @@ html {
.visible { .visible {
visibility: visible; visibility: visible;
} }
.invisible {
visibility: hidden;
}
.fixed { .fixed {
position: fixed; position: fixed;
} }
@ -2622,6 +2682,9 @@ html {
.h-6 { .h-6 {
height: 1.5rem; height: 1.5rem;
} }
.h-24 {
height: 6rem;
}
.min-h-screen { .min-h-screen {
min-height: 100vh; min-height: 100vh;
} }

1841
public/js/app.js vendored

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 React, { useState, useEffect } from 'react'
import Authenticated from '@/Layouts/Authenticated'
import { Head } from '@inertiajs/inertia-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) { export default function Products(props) {
return ( const { data: products, links } = props.products
<Authenticated
auth={props.auth} const [search, setSearch] = useState('')
errors={props.errors} const preValue = usePrevious(search)
header={
<h2 className="font-semibold text-xl text-gray-800 leading-tight"> const [product, setProduct] = useState(null)
Barang const formModal = useModalState(false)
</h2> const toggle = (product = null) => {
} setProduct(product)
> formModal.toggle()
<Head title="Products" /> }
<div className="py-12">
<div className="flex flex-col sm:px-6 lg:px-8 space-x-4"> const confirmModal = useModalState(false)
const handleDelete = (product) => {
</div> confirmModal.setData(product)
</div> confirmModal.toggle()
</Authenticated> }
)
} 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)
}

@ -4,6 +4,7 @@ use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use App\Http\Controllers\DashboardController; use App\Http\Controllers\DashboardController;
use App\Http\Controllers\UserController; use App\Http\Controllers\UserController;
use App\Http\Controllers\ProductController;
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -28,7 +29,11 @@ Route::middleware(['auth'])->group(function () {
Route::put('/users/{user}', [UserController::class, 'update'])->name('users.update'); Route::put('/users/{user}', [UserController::class, 'update'])->name('users.update');
Route::delete('/users/{user}', [UserController::class, 'destroy'])->name('users.destroy'); Route::delete('/users/{user}', [UserController::class, 'destroy'])->name('users.destroy');
Route::get('/products', fn () => inertia('Products'))->name('products.index'); Route::get('/products', [ProductController::class, 'index'])->name('products.index');
Route::post('/products', [ProductController::class, 'store'])->name('products.store');
Route::post('/products/{product}', [ProductController::class, 'update'])->name('products.update');
Route::delete('/products/{product}', [ProductController::class, 'destroy'])->name('products.destroy');
Route::get('/employees', fn () => inertia('Employees'))->name('employees.index'); Route::get('/employees', fn () => inertia('Employees'))->name('employees.index');
Route::get('/payrolls', fn () => inertia('Payrolls'))->name('payrolls.index'); Route::get('/payrolls', fn () => inertia('Payrolls'))->name('payrolls.index');
Route::get('/report', fn () => inertia('Report'))->name('report'); Route::get('/report', fn () => inertia('Report'))->name('report');

Loading…
Cancel
Save