summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/app/api/product/[id]/toggle-different/route.tsx16
-rw-r--r--src/app/api/product/route.tsx11
-rw-r--r--src/app/api/stock-opname/route.tsx19
-rw-r--r--src/common/constants/product.ts4
-rw-r--r--src/common/styles/globals.css2
-rw-r--r--src/modules/profile-card/profile-card.module.css2
-rw-r--r--src/modules/result/components/DetailRow.tsx32
-rw-r--r--src/modules/result/components/ProductModal.tsx34
-rw-r--r--src/modules/result/components/Table.tsx52
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>
</>