From be0f537dc4fe384eef09436833c6407e6482c16d Mon Sep 17 00:00:00 2001 From: Rafi Zadanly Date: Thu, 9 Nov 2023 15:40:16 +0700 Subject: Initial commit --- src/modules/stock-opname/index.tsx | 261 +++++++++++++++++++++++ src/modules/stock-opname/stock-opname.module.css | 15 ++ 2 files changed, 276 insertions(+) create mode 100644 src/modules/stock-opname/index.tsx create mode 100644 src/modules/stock-opname/stock-opname.module.css (limited to 'src/modules/stock-opname') diff --git a/src/modules/stock-opname/index.tsx b/src/modules/stock-opname/index.tsx new file mode 100644 index 0000000..0a5c848 --- /dev/null +++ b/src/modules/stock-opname/index.tsx @@ -0,0 +1,261 @@ +"use client"; +import { Location, Product } from "@prisma/client"; +import AsyncSelect from "react-select/async" +import styles from "./stock-opname.module.css" +import { Button, Card, CardBody, Input, Modal, ModalBody, ModalContent, ModalHeader, Spacer, Spinner } from "@nextui-org/react"; +import { SelectOption } from "@/common/types/select"; +import { useStockOpnameStore } from "@/common/stores/useStockOpnameStore"; +import { ActionMeta, SingleValue } from "react-select"; +import { useMutation } from "@tanstack/react-query"; +import { useEffect, useState } from "react"; +import toast from "@/common/libs/toast"; +import Scanner from "@/common/components/Scanner"; +import { AlertCircleIcon, ScanIcon, ScanLineIcon } from "lucide-react"; + +type ActiveScanner = "product" | "location" | null +type ScannerOption = { value: number, label: string } + +const StockOpname = () => { + const { + form, updateForm, + oldOpname, setOldOpname, + resetForm + } = useStockOpnameStore() + + useEffect(() => { + const productId = form.product?.value + const locationId = form.location?.value + + if ( + typeof productId === 'number' && + typeof locationId === 'number' + ) { + loadOldQuantity(productId, locationId) + .then((data) => setOldOpname(data)) + } + + }, [form.product, form.location, setOldOpname]) + + const handleSelectChange = (val: SingleValue, action: ActionMeta) => { + updateForm(action.name as keyof typeof form, val) + } + + const handleInputChange = (e: React.ChangeEvent) => { + updateForm(e.target.name as keyof typeof form, e.target.value) + } + + const saveMutation = useMutation({ + mutationKey: ['stock-opname'], + mutationFn: async (data: typeof form) => await fetch("/api/stock-opname", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + location: data.location?.value, + product: data.product?.value, + quantity: parseInt(data.quantity), + }), + }), + onSuccess(data) { + if (data.status === 200) { + toast('Data berhasil disimpan') + resetForm() + } + }, + }) + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + saveMutation.mutate(form) + } + + const [activeScanner, setActiveScanner] = useState(null) + const [scannerOptions, setScannerOptions] = useState() + const [scannerOptionLoading, setScannerOptionLoading] = useState(false) + + const handleOnScan = async (data: string) => { + if (!activeScanner) return + const loadFunc = activeScanner === 'product' ? loadProduct : loadLocation + setScannerOptionLoading(true) + const response = await loadFunc(data) + setScannerOptionLoading(false) + setScannerOptions(response) + } + + const handleScannerOptionPress = (option: ScannerOption) => { + toast(`${activeScanner} berhasil diubah`) + updateForm(activeScanner as keyof typeof form, option) + closeModal() + } + + const closeModal = () => { + setActiveScanner(null) + setScannerOptions(undefined) + } + + return ( + <> +
+ Stock Opname +
+ + + +
+
+ +
+ + +
+
+ +
+ +
+ + +
+
+ +
+ + + {oldOpname && ( +
+ Jumlah terakhir adalah {' '} + {oldOpname.quantity} + {' '} diisi oleh {' '} + {oldOpname.user.name}. + {' '} + +
+ )} +
+ + +
+ + + + Scan {activeScanner} + + {!scannerOptions && !scannerOptionLoading && ( + + )} + + {!scannerOptions && scannerOptionLoading && } + + {!!scannerOptions && ( + <> +
+ {scannerOptions.map((option) => ( + handleScannerOptionPress(option)}> + {option.label} + + ))} +
+ {scannerOptions.length === 0 && ( +
+ + Tidak ada opsi untuk ditampilkan +
+ )} + + + + )} +
+
+
+ + ) +} + +const loadOldQuantity = async (productId: number, locationId: number) => { + const queryParams = new URLSearchParams({ + productId: productId.toString(), + locationId: locationId.toString() + }) + + const response = await fetch(`/api/stock-opname/quantity?${queryParams}`) + const data = await response.json() + + return data +} + +const loadLocation = async (inputValue: string) => { + const response = await fetch(`/api/location?search=${inputValue}`) + const data: Location[] = await response.json() || [] + + return data.map((location) => ({ + value: location.id, + label: location.name + })) +} + +const loadProduct = async (inputValue: string) => { + const response = await fetch(`/api/product?search=${inputValue}`) + const data: { products: Product[] } = await response.json() || [] + + return data?.products.map((product) => { + let label = '' + if (product.itemCode) label += `[${product.itemCode}]` + label += ` ${product.name}` + if (product.barcode) label += ` [${product.barcode}]` + + return { + value: product.id, + label + } + }) +} + +export default StockOpname \ No newline at end of file diff --git a/src/modules/stock-opname/stock-opname.module.css b/src/modules/stock-opname/stock-opname.module.css new file mode 100644 index 0000000..b020549 --- /dev/null +++ b/src/modules/stock-opname/stock-opname.module.css @@ -0,0 +1,15 @@ +.label { + @apply block mb-2; +} + +.form { + @apply grid grid-cols-1 gap-y-4; +} + +.inputGroup { + @apply flex gap-x-2; +} + +.scanButton { + @apply min-w-fit px-4 text-default-700; +} -- cgit v1.2.3