From 6aaeb37caf69e629c49582ebf39ae777d5f8afc9 Mon Sep 17 00:00:00 2001 From: ajikamaludin Date: Thu, 4 May 2023 02:12:51 +0700 Subject: [PATCH] create sale --- .../Controllers/Api/CustomerController.php | 29 +++ app/Http/Controllers/SaleController.php | 73 +++++++ resources/js/Pages/Sale/Form.jsx | 197 ++++++++++++++++++ resources/js/Pages/Sale/Index.jsx | 159 ++++++++++++++ 4 files changed, 458 insertions(+) create mode 100644 app/Http/Controllers/Api/CustomerController.php create mode 100644 app/Http/Controllers/SaleController.php create mode 100644 resources/js/Pages/Sale/Form.jsx create mode 100644 resources/js/Pages/Sale/Index.jsx diff --git a/app/Http/Controllers/Api/CustomerController.php b/app/Http/Controllers/Api/CustomerController.php new file mode 100644 index 0000000..8b866e5 --- /dev/null +++ b/app/Http/Controllers/Api/CustomerController.php @@ -0,0 +1,29 @@ +q) { + $query->where('name', 'like', "%{$request->q}%"); + } + + if ($request->except_id) { + $query->where('id', '!=', $request->except_id); + } + + if ($request->all == 1) { + return $query->get(); + } + + return $query->get(); + } +} diff --git a/app/Http/Controllers/SaleController.php b/app/Http/Controllers/SaleController.php new file mode 100644 index 0000000..96481d6 --- /dev/null +++ b/app/Http/Controllers/SaleController.php @@ -0,0 +1,73 @@ +with(['items.product.category', 'customer']); + + if ($request->q) { + $query->where('code', 'like', "%{$request->q}%"); + } + + $query->orderBy('created_at', 'desc'); + + return inertia('Sale/Index', [ + 'query' => $query->paginate(10), + ]); + } + + public function create(Request $request) + { + $products = Product::query()->orderBy('updated_at', 'desc'); + + if ($request->q != '') { + $products->where('name', 'like', "%$request->q%"); + } + + return inertia('Sale/Form', [ + '_products' => $products->paginate(16), + '_page' => $request->page ?? 1, + ]); + } + + public function store(Request $request) + { + $request->validate([ + 'date' => 'required|date', + 'customer_id' => 'nullable|exists:customers,id', + 'items' => 'required|array', + 'items.*.id' => 'required|exists:products,id', + 'items.*.qty' => 'required|numeric' + ]); + + DB::beginTransaction(); + $sale = Sale::create([ + 'code' => Str::upper(Str::random(6)), + 'date' => $request->date, + 'customer_id' => $request->customer_id, + 'total' => collect($request->items)->sum(fn ($item) => $item['qty'] * $item['price']) + ]); + + foreach($request->items as $item) { + $sale->items()->create([ + "product_id" => $item['id'], + "price" => $item['price'], + "cost" => $item['cost'], + "quantity" => $item['qty'], + ]); + } + DB::commit(); + + return redirect()->route('sale.index') + ->with('message', ['type' => 'success', 'message' => 'Item has beed saved']); + } +} diff --git a/resources/js/Pages/Sale/Form.jsx b/resources/js/Pages/Sale/Form.jsx new file mode 100644 index 0000000..3e7f9ff --- /dev/null +++ b/resources/js/Pages/Sale/Form.jsx @@ -0,0 +1,197 @@ +import React, { useEffect, useState } from 'react'; +import { Head, Link, router, useForm } from '@inertiajs/react'; +import { usePrevious } from 'react-use'; +import { HiXCircle } from 'react-icons/hi'; + +import { dateToString, formatIDR } from '@/utils'; +import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'; +import SearchInput from '@/Components/SearchInput'; +import Button from '@/Components/Button'; +import FormInputDate from '@/Components/FormInputDate'; +import Pagination from '@/Components/Pagination'; +import FormInput from '@/Components/FormInput'; +import CustomerSelectionInput from '../Customer/SelectionInput'; +import { Spinner } from 'flowbite-react'; + +export default function Sale(props) { + const { _products: { data: products, links}, _page } = props + + const [loading, setLoading] = useState(false) + const [search, setSearch] = useState('') + const preValue = usePrevious(search) + + const { data, setData, post, processing, errors } = useForm({ + date: dateToString(new Date()), + customer_id: null, + items: [] + }) + + const addItem = (product) => { + const isExist = data.items.find(item => item.id === product.id) + if (isExist) { + return + } + setData('items', data.items.concat({ + ...product, + qty: 1 + })) + } + + const removeItem = (id) => { + setData('items', data.items.filter(item => item.id !== id)) + } + + const setQuantityItem = (id, qty) => { + setData('items', data.items.map(item => { + if (item.id === id) { + return { + ...item, + qty: qty, + } + } + return item + })) + } + + const handleSubmit = () => { + post(route('sale.store')) + } + + const params = { q: search, page: _page } + useEffect(() => { + if (preValue) { + setLoading(true) + router.get( + route(route().current()), + { q: search, page: _page }, + { + replace: true, + preserveState: true, + onSuccess: () => { + setLoading(false) + }, + } + ) + } + }, [search]) + + console.log(data) + const total = data.items.reduce((amt, item) => amt + (+item.qty * +item.price), 0) + + return ( + + + +
+
+
+
+ setSearch(e.target.value)} + value={search} + /> +
+ {loading ? ( +
+ +
+ ) : ( +
+ {products.map(item => ( +
addItem(item)} + > +
+ {item.name} +
+
+ Rp. {formatIDR(item.price)} +
+
+ ))} +
+ )} +
+
+ +
+
+
+
+ setData('customer_id', id)} + error={errors.customer_id} + /> + setData("date", date)} + placeholder='Tanggal' + error={errors.date} + /> +
+
+
+ Nama +
+
+ Jumlah +
+
+ Subtotal +
+
+
+
+
+ {data.items.map(item => ( +
+
+ {item.name} +
+
+ setQuantityItem(item.id, e.target.value)} + className="text-right" + /> +
+
+ {formatIDR(item.qty * item.price)} +
+
removeItem(item.id)}> + +
+
+ ))} +
+
+
Total:
+
{formatIDR(total)}
+
+ +
+
+
+
+ ); +} \ No newline at end of file diff --git a/resources/js/Pages/Sale/Index.jsx b/resources/js/Pages/Sale/Index.jsx new file mode 100644 index 0000000..cc97138 --- /dev/null +++ b/resources/js/Pages/Sale/Index.jsx @@ -0,0 +1,159 @@ +import React, { useEffect, useState } from 'react'; +import { router } from '@inertiajs/react'; +import { usePrevious } from 'react-use'; +import { Head } from '@inertiajs/react'; +import { Button, Dropdown } from 'flowbite-react'; +import { HiEye, HiPencil, HiTrash } from 'react-icons/hi'; +import { useModalState } from '@/hooks'; + +import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'; +import Pagination from '@/Components/Pagination'; +import ModalConfirm from '@/Components/ModalConfirm'; +import SearchInput from '@/Components/SearchInput'; +import { formatDate, formatIDR, hasPermission } from '@/utils'; + +export default function Sale(props) { + const { query: { links, data }, auth } = props + + const [search, setSearch] = useState('') + const preValue = usePrevious(search) + + const confirmModal = useModalState() + + const toggleFormModal = () => { + router.get(route('sale.create')) + } + + const handleDeleteClick = (sale) => { + confirmModal.setData(sale) + confirmModal.toggle() + } + + const onDelete = () => { + if(confirmModal.data !== null) { + router.delete(route('sale.destroy', confirmModal.data.id)) + } + } + + const params = { q: search } + useEffect(() => { + if (preValue) { + router.get( + route(route().current()), + { q: search }, + { + replace: true, + preserveState: true, + } + ) + } + }, [search]) + + const canCreate = hasPermission(auth, 'create-sale') + const canUpdate = hasPermission(auth, 'update-sale') + const canDelete = hasPermission(auth, 'delete-sale') + + return ( + + + +
+
+
+
+ {canCreate && ( + + )} +
+ setSearch(e.target.value)} + value={search} + /> +
+
+
+
+ + + + + + + + + + + {data.map(sale => ( + + + + + + + + ))} + +
+ Kode + + Tanggal + + Pelanggan + + Total + +
+ {sale.code} + + {formatDate(sale.date)} + + {sale.customer?.name} + + {formatIDR(sale.total)} + + + {canUpdate && ( + toggleFormModal(sale)}> +
+ +
Detail
+
+
+ )} + {canDelete && ( + handleDeleteClick(sale)}> +
+ +
Hapus
+
+
+ )} +
+
+
+
+ +
+
+
+
+
+ +
+ ); +} \ No newline at end of file