diff options
| -rw-r--r-- | src/app/api/product/[id]/toggle-different/route.tsx | 16 | ||||
| -rw-r--r-- | src/app/api/product/route.tsx | 11 | ||||
| -rw-r--r-- | src/app/api/stock-opname/route.tsx | 19 | ||||
| -rw-r--r-- | src/common/constants/product.ts | 4 | ||||
| -rw-r--r-- | src/common/styles/globals.css | 2 | ||||
| -rw-r--r-- | src/modules/profile-card/profile-card.module.css | 2 | ||||
| -rw-r--r-- | src/modules/result/components/DetailRow.tsx | 32 | ||||
| -rw-r--r-- | src/modules/result/components/ProductModal.tsx | 34 | ||||
| -rw-r--r-- | src/modules/result/components/Table.tsx | 52 |
9 files changed, 131 insertions, 41 deletions
diff --git a/src/app/api/product/[id]/toggle-different/route.tsx b/src/app/api/product/[id]/toggle-different/route.tsx new file mode 100644 index 0000000..987dbf3 --- /dev/null +++ b/src/app/api/product/[id]/toggle-different/route.tsx @@ -0,0 +1,16 @@ +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "prisma/client"; + +type Params = { params: { id: string } } +export async function POST(request: NextRequest, { params }: Params) { + const intId = parseInt(params.id) + const product = await prisma.product.findUnique({ where: { id: intId } }) + const updatedProduct = await prisma.product.update({ + where: { id: intId }, + data: { + isDifferent: !product?.isDifferent + } + }) + + return NextResponse.json(updatedProduct) +}
\ No newline at end of file diff --git a/src/app/api/product/route.tsx b/src/app/api/product/route.tsx index 63813aa..de8a482 100644 --- a/src/app/api/product/route.tsx +++ b/src/app/api/product/route.tsx @@ -8,11 +8,15 @@ export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams; const search = searchParams.get('search'); + const page = searchParams.get('page'); const intPage: number = page ? parseInt(page) : 1; + const paramCompanyId = searchParams.get('companyId') const companyId = paramCompanyId ? parseInt(paramCompanyId) : null + const show = searchParams.get('show') + const credential = getServerCredential() if (!credential) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) @@ -24,8 +28,10 @@ export async function GET(request: NextRequest) { { barcode: { mode: 'insensitive', contains: search ?? '' } }, { itemCode: { mode: 'insensitive', contains: search ?? '' } }, ], - companyId: companyId ?? credential.companyId - } + companyId: companyId ?? credential.companyId, + stockOpnames: { none: show === 'not-count' ? {} : undefined }, + onhandQty: { gt: show === 'not-count' ? 0 : undefined }, + }, } const products = await prisma.product.findMany({ @@ -40,6 +46,7 @@ export async function GET(request: NextRequest) { const pagination = { page: intPage, totalPage: Math.ceil(count / PAGE_SIZE), + count } return NextResponse.json({ products, ...pagination }) diff --git a/src/app/api/stock-opname/route.tsx b/src/app/api/stock-opname/route.tsx index 3b3ad5a..e98c3b2 100644 --- a/src/app/api/stock-opname/route.tsx +++ b/src/app/api/stock-opname/route.tsx @@ -113,9 +113,7 @@ export async function POST(request: NextRequest) { team } - const stockOpname = await prisma.stockOpname.findFirst({ - where: query - }) + const stockOpname = await prisma.stockOpname.findFirst({ where: query }) const data = { ...query, @@ -165,6 +163,7 @@ const computeIsDifferent = async ({ const stockOpnames: StockOpnameLocationRes[] = await stockOpnamesFetch.json() let isDifferent: boolean = false + let verificationCounter: number = 0 for (const opname of stockOpnames) { let { COUNT1, COUNT2, COUNT3, VERIFICATION } = opname @@ -182,25 +181,31 @@ const computeIsDifferent = async ({ if (_.isNumber(COUNT1.quantity) && _.isNumber(COUNT2.quantity) && COUNT1.quantity !== COUNT2.quantity) isDifferent = true if (_.isNumber(COUNT1.quantity) && _.isNumber(COUNT3.quantity) && COUNT1.quantity !== COUNT3.quantity) isDifferent = true if (_.isNumber(COUNT2.quantity) && _.isNumber(COUNT3.quantity) && COUNT2.quantity !== COUNT3.quantity) isDifferent = true + + if (_.isNumber(VERIFICATION.quantity)) verificationCounter++ } const product = await prisma.product.findFirst({ where: { id: productId } }) if (!product) return - const onhandQty = product?.onhandQty || 0 + const onhandQty = product.onhandQty + const differenceQty = product.differenceQty + const allQty = onhandQty + differenceQty if (!isDifferent) { const conditional = { - wasVerified: typeof totalQty['VERIFICATION'] === 'number', + verificationCheckAll: verificationCounter > 0 && verificationCounter === stockOpnames.length, anyCountEqWithOnhand: [totalQty['COUNT1'], totalQty['COUNT2'], totalQty['COUNT3']].includes(onhandQty), + anyCountEqWithAllQty: [totalQty['COUNT1'], totalQty['COUNT2'], totalQty['COUNT3']].includes(allQty), count1EqWithCount2: totalQty['COUNT1'] !== null && totalQty['COUNT2'] !== null && totalQty['COUNT1'] === totalQty['COUNT2'], count1EqWithCount3: totalQty['COUNT1'] !== null && totalQty['COUNT3'] !== null && totalQty['COUNT1'] === totalQty['COUNT3'], - count2EqWithCount3: totalQty['COUNT2'] !== null && totalQty['COUNT3'] !== null && totalQty['COUNT2'] === totalQty['COUNT3'] + count2EqWithCount3: totalQty['COUNT2'] !== null && totalQty['COUNT3'] !== null && totalQty['COUNT2'] === totalQty['COUNT3'], } isDifferent = !( - conditional.wasVerified || + conditional.verificationCheckAll || conditional.anyCountEqWithOnhand || + conditional.anyCountEqWithAllQty || conditional.count1EqWithCount2 || conditional.count1EqWithCount3 || conditional.count2EqWithCount3 diff --git a/src/common/constants/product.ts b/src/common/constants/product.ts new file mode 100644 index 0000000..873b2e6 --- /dev/null +++ b/src/common/constants/product.ts @@ -0,0 +1,4 @@ +export const SHOWING_SELECTIONS = [ + { key: "1", value: "", label: "Semua" }, + { key: "2", value: "not-count", label: "Belum dihitung" }, +]; diff --git a/src/common/styles/globals.css b/src/common/styles/globals.css index 861f1c0..ef8c0b8 100644 --- a/src/common/styles/globals.css +++ b/src/common/styles/globals.css @@ -4,7 +4,7 @@ html, body { - @apply bg-neutral-100 font-primary; + @apply bg-neutral-100 font-primary antialiased; } .react-select__control { diff --git a/src/modules/profile-card/profile-card.module.css b/src/modules/profile-card/profile-card.module.css index b7d00fe..7c7bcd2 100644 --- a/src/modules/profile-card/profile-card.module.css +++ b/src/modules/profile-card/profile-card.module.css @@ -3,7 +3,7 @@ } .cardBody { - @apply flex flex-row items-center gap-x-4; + @apply flex flex-row items-center gap-x-4 p-4; } .name { diff --git a/src/modules/result/components/DetailRow.tsx b/src/modules/result/components/DetailRow.tsx index 18dcb8f..bb5b1ef 100644 --- a/src/modules/result/components/DetailRow.tsx +++ b/src/modules/result/components/DetailRow.tsx @@ -4,10 +4,11 @@ import { StockOpnameLocationRes } from '@/common/types/stockOpname'; import { Skeleton } from '@nextui-org/react'; import { useQuery } from '@tanstack/react-query' import styles from './table.module.css' -import { CornerDownRightIcon } from 'lucide-react'; -import { User } from 'prisma/generated/client'; +import { CheckIcon, CornerDownRightIcon, XIcon } from 'lucide-react'; +import { Team, User } from 'prisma/generated/client'; import clsxm from '@/common/libs/clsxm'; import getClientCredential from '@/common/libs/getClientCredential'; +import _ from 'lodash'; const DetailRow = ({ productId }: { productId: number }) => { const { filter } = useResultStore() @@ -48,16 +49,16 @@ const DetailRow = ({ productId }: { productId: number }) => { </div> </td> <td className={styles.td}> - <QuantityColumn data={location.COUNT1} /> + <Column team='COUNT1' data={location.COUNT1} /> </td> <td className={styles.td}> - <QuantityColumn data={location.COUNT2} /> + <Column team='COUNT2' data={location.COUNT2} /> </td> <td className={styles.td}> - <QuantityColumn data={location.COUNT3} /> + <Column team='COUNT3' data={location.COUNT3} /> </td> <td className={styles.td}> - <QuantityColumn data={location.VERIFICATION} /> + <Column team='VERIFICATION' data={location.VERIFICATION} /> </td> <td className={styles.td} /> <td className={styles.td} /> @@ -67,14 +68,29 @@ const DetailRow = ({ productId }: { productId: number }) => { ) } -const QuantityColumn = ({ data }: { data: { quantity?: number | undefined, user?: User } }) => { +type Data = { quantity?: number | undefined, user?: User } + +const Column = ({ team, data }: { team: Team, data: Data }) => { + const credential = getClientCredential() + + return credential?.team === team && credential.team !== 'VERIFICATION' ? ( + <div className='flex justify-center'> + {_.isNumber(data.quantity) && <CheckIcon size={16} />} + {_.isUndefined(data.quantity) && <XIcon size={16} />} + </div> + ) : ( + <QuantityColumn data={data} /> + ) +} + +const QuantityColumn = ({ data }: { data: Data }) => { const credential = getClientCredential() if (!(credential?.team == "VERIFICATION")) return '-' return ( <div className='grid grid-cols-1'> - {typeof data?.quantity !== 'number' && '-'} + {_.isUndefined(data.quantity) && '-'} {data.quantity !== null && ( <> <span>{data.quantity}</span> diff --git a/src/modules/result/components/ProductModal.tsx b/src/modules/result/components/ProductModal.tsx index 1fe4130..0d7a7fc 100644 --- a/src/modules/result/components/ProductModal.tsx +++ b/src/modules/result/components/ProductModal.tsx @@ -1,9 +1,10 @@ import { useResultStore } from '@/common/stores/useResultStore' -import { Input, Modal, ModalBody, ModalContent, ModalHeader, Pagination, Skeleton, Table, TableBody, TableCell, TableColumn, TableHeader, TableRow } from '@nextui-org/react' +import { Input, Modal, ModalBody, ModalContent, ModalHeader, Pagination, Select, SelectItem, Skeleton, Table, TableBody, TableCell, TableColumn, TableHeader, TableRow } from '@nextui-org/react' import { Product } from 'prisma/generated/client' import { useQuery } from '@tanstack/react-query' import React, { useEffect, useMemo, useState } from 'react' import { useDebounce } from 'usehooks-ts' +import { SHOWING_SELECTIONS } from '@/common/constants/product' type Props = { modal: { @@ -15,20 +16,23 @@ type Props = { const ProductModal = ({ modal }: Props) => { const [page, setPage] = useState(1) const [search, setSearch] = useState("") + const [show, setShow] = useState("1") const debouncedSearch = useDebounce(search, 500) const { filter } = useResultStore() useEffect(() => { setPage(1) - }, [debouncedSearch]) + }, [debouncedSearch, show, filter.company]) const { data } = useQuery({ - queryKey: ['product', page, debouncedSearch, filter.company], + queryKey: ['product', page, debouncedSearch, filter.company, show], queryFn: async () => { + const showValue = SHOWING_SELECTIONS.find((item) => item.key === show)?.value || '' const searchParams = new URLSearchParams({ page: page.toString(), search: debouncedSearch, - companyId: filter.company + companyId: filter.company, + show: showValue }) const response = await fetch(`/api/product?${searchParams}`) const data: { @@ -47,12 +51,32 @@ const ProductModal = ({ modal }: Props) => { if (data?.totalPage) setTotalPage(data?.totalPage) }, [data?.totalPage]) + const handleSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => { + setShow(e.target.value) + } + return ( <Modal isOpen={modal.isOpen} onOpenChange={modal.onOpenChange} size='5xl'> <ModalContent> <ModalHeader>Product List</ModalHeader> <ModalBody className='pb-6'> - <Input type='text' label="Search" value={search} onChange={(e) => setSearch(e.target.value)} /> + <div className="flex flex-wrap gap-3"> + <div className="w-10/12"> + <Input type='text' label="Search" value={search} onChange={(e) => setSearch(e.target.value)} /> + </div> + <div className="flex-1 md:w-2/12"> + <Select + label="Tampilkan" + selectedKeys={show} + name="show" + onChange={handleSelectChange} + > + {SHOWING_SELECTIONS.map((selection) => ( + <SelectItem key={selection.key}>{selection.label}</SelectItem> + ))} + </Select> + </div> + </div> {!data && ( <Skeleton isLoaded={!!data} className='rounded-lg h-[600px]' /> )} diff --git a/src/modules/result/components/Table.tsx b/src/modules/result/components/Table.tsx index a6a9f30..7478e1b 100644 --- a/src/modules/result/components/Table.tsx +++ b/src/modules/result/components/Table.tsx @@ -1,7 +1,7 @@ "use client"; import { useResultStore } from "@/common/stores/useResultStore"; import { StockOpnameRes } from "@/common/types/stockOpname"; -import { Button, Pagination, Skeleton, Spacer, Spinner } from "@nextui-org/react" +import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Pagination, Skeleton, Spacer, Spinner } from "@nextui-org/react" import { keepPreviousData, useQuery } from "@tanstack/react-query"; import styles from "./table.module.css" import clsxm from "@/common/libs/clsxm"; @@ -11,6 +11,9 @@ import getClientCredential from "@/common/libs/getClientCredential"; import { SHOWING_SELECTIONS } from "@/common/constants/result"; import { useEffect, useState } from "react"; import moment from "moment"; +import { MoreVerticalIcon } from "lucide-react"; +import toast from "@/common/libs/toast"; +import { Product } from "prisma/generated/client"; const Table = () => { const credential = getClientCredential() @@ -37,7 +40,10 @@ const Table = () => { searchParams.set('page', page.toString()); return await fetch(`/api/stock-opname?${searchParams}`) - .then(res => res.json()) + .then(res => { + window.scrollTo({ top: 0, 'behavior': 'smooth' }) + return res.json() + }) }, placeholderData: keepPreviousData }) @@ -63,21 +69,17 @@ const Table = () => { setExportLoad(false) } - let debounceTimeout: NodeJS.Timeout | null = null; - - const paginationOnChange = (page: number) => { - updateFilter('page', page) - - if (debounceTimeout) clearTimeout(debounceTimeout) - - debounceTimeout = setTimeout(() => { - window.scrollTo({ top: 0, 'behavior': 'smooth' }); - debounceTimeout = null - }, 1000); + const toggleDifferent = async (id: number) => { + const response = await fetch(`/api/product/${id}/toggle-different`, { method: 'POST' }) + const product: Product = await response.json() + toast(`Berhasil mengubah status barang ${product.itemCode} ${product.name} menjadi ${product.isDifferent ? 'selisih' : 'aman'}`, { duration: 10000 }) + stockOpnames.refetch() } const isLoading = stockOpnames.isLoading || stockOpnames.isRefetching + const COL_LENGTH = 9 + return ( <> <div className="flex"> @@ -97,6 +99,7 @@ const Table = () => { <th className={styles.th}>TIM VERIFIKASI</th> <th className={styles.th}>ON-HAND QTY</th> <th className={styles.th}>GUDANG SELISIH</th> + <th className={styles.th}></th> </thead> <tbody className={styles.tbody}> {!isLoading && stockOpnames.data?.result.map((stockOpname: StockOpnameRes['result']) => ( @@ -137,6 +140,22 @@ const Table = () => { <td className={styles.td}> {credential?.team == 'VERIFICATION' ? stockOpname.differenceQty : '-'} </td> + <td> + {credential?.team == 'VERIFICATION' && ( + <Dropdown> + <DropdownTrigger> + <Button variant="light" className="p-1 min-w-fit"> + <MoreVerticalIcon size={16} /> + </Button> + </DropdownTrigger> + <DropdownMenu> + <DropdownItem key="toggleDifferent" onPress={() => toggleDifferent(stockOpname.id)}> + Tandai {stockOpname.isDifferent ? 'aman' : 'selisih'} + </DropdownItem> + </DropdownMenu> + </Dropdown> + )} + </td> </tr> <DetailRow productId={stockOpname.id} /> @@ -145,13 +164,13 @@ const Table = () => { {!isLoading && stockOpnames.data?.result.length === 0 && ( <tr> - <td colSpan={8} className="text-center text-neutral-600 py-4">Belum ada data untuk ditampilkan</td> + <td colSpan={COL_LENGTH} className="text-center text-neutral-600 py-4">Belum ada data untuk ditampilkan</td> </tr> )} {isLoading && ( <tr> - <td colSpan={8}> + <td colSpan={COL_LENGTH}> <div className="flex flex-col gap-y-2.5"> {Array.from({ length: 10 }, (_, i) => ( <Skeleton className="h-16" key={i} /> @@ -165,10 +184,9 @@ const Table = () => { <Spacer y={4} /> <Pagination - isCompact page={stockOpnames.data?.page || 1} total={stockOpnames.data?.totalPage || 1} - onChange={paginationOnChange} + onChange={(page) => updateFilter('page', page)} /> </div> </> |
