From 07819844d5ef7e323fd956eacfedecb2f4f4bb80 Mon Sep 17 00:00:00 2001 From: Rafi Zadanly Date: Fri, 17 Nov 2023 15:16:23 +0700 Subject: Update result feature --- src/app/api/location/route.tsx | 9 +- src/app/api/product/import/route.tsx | 17 +++- src/app/api/product/route.tsx | 16 ++-- src/app/api/stock-opname/export/route.tsx | 68 +++++++++++++++ src/app/api/stock-opname/location/route.tsx | 41 +++++---- src/app/api/stock-opname/quantity/route.tsx | 5 +- src/app/api/stock-opname/route.tsx | 111 ++++++++++++------------ src/common/components/Authenticated/index.tsx | 5 +- src/common/constants/team.ts | 5 +- src/common/stores/useResultStore.ts | 10 +++ src/modules/login/index.tsx | 4 +- src/modules/login/login.module.css | 2 +- src/modules/profile-card/index.tsx | 10 +-- src/modules/result/components/DetailRow.tsx | 38 +++++--- src/modules/result/components/Filter.tsx | 70 ++++++++------- src/modules/result/components/ImportModal.tsx | 20 ++++- src/modules/result/components/MoreMenu.tsx | 4 +- src/modules/result/components/ProductModal.tsx | 6 +- src/modules/result/components/Table.tsx | 83 +++++++++++++++--- src/modules/result/components/filter.module.css | 2 +- src/modules/result/components/table.module.css | 4 - src/modules/result/index.tsx | 2 +- src/modules/stock-opname/index.tsx | 27 +++--- 23 files changed, 366 insertions(+), 193 deletions(-) create mode 100644 src/app/api/stock-opname/export/route.tsx diff --git a/src/app/api/location/route.tsx b/src/app/api/location/route.tsx index 452a85d..bc4cfff 100644 --- a/src/app/api/location/route.tsx +++ b/src/app/api/location/route.tsx @@ -1,17 +1,14 @@ +import getServerCredential from "@/common/libs/getServerCredential"; import { NextRequest, NextResponse } from "next/server"; import { prisma } from "prisma/client"; -import { Credential } from "@/common/types/auth" export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams; const search = searchParams.get('search'); - const credentialStr = request.cookies.get('credential')?.value - const credential: Credential | null = credentialStr ? JSON.parse(credentialStr) : null + const credential = getServerCredential() - if (!credential) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } + if (!credential) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) const { companyId } = credential diff --git a/src/app/api/product/import/route.tsx b/src/app/api/product/import/route.tsx index 7358205..eb87f07 100644 --- a/src/app/api/product/import/route.tsx +++ b/src/app/api/product/import/route.tsx @@ -3,6 +3,12 @@ import { prisma } from "prisma/client"; import * as XLSX from "xlsx"; export async function POST(request: NextRequest) { + const searchParams = request.nextUrl.searchParams + const companyId = searchParams.get('companyId') + + if (!companyId) return NextResponse.json({ error: 'Bad Request. Missing companyId' }, { status: 400 }) + const intCompanyId = parseInt(companyId) + const body = await request.arrayBuffer(); const workbook = XLSX.read(body, { type: 'buffer' }) const worksheetName = workbook.SheetNames[0] @@ -13,14 +19,19 @@ export async function POST(request: NextRequest) { const newProducts = fileData.map(row => ({ id: undefined, isDifferent: false, - name: row[0], - barcode: row[1], - itemCode: row[2], + name: row[0].toString(), + barcode: row[1].toString(), + itemCode: row[2].toString(), onhandQty: row[3], differenceQty: row[4], companyId: row[5], })); + const whereCompany = { companyId: intCompanyId } + + await prisma.stockOpname.deleteMany({ where: whereCompany }) + await prisma.product.deleteMany({ where: whereCompany }) + await prisma.product.createMany({ data: newProducts }) return NextResponse.json(true) diff --git a/src/app/api/product/route.tsx b/src/app/api/product/route.tsx index 1161a4e..07ba9c7 100644 --- a/src/app/api/product/route.tsx +++ b/src/app/api/product/route.tsx @@ -1,24 +1,20 @@ +import getServerCredential from "@/common/libs/getServerCredential"; import { NextRequest, NextResponse } from "next/server"; import { prisma } from "prisma/client"; -import { Credential } from "@/common/types/auth" export async function GET(request: NextRequest) { const PAGE_SIZE = 30; const searchParams = request.nextUrl.searchParams; - const type = searchParams.get('type') 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 credentialStr = request.cookies.get('credential')?.value - const credential: Credential | null = credentialStr ? JSON.parse(credentialStr) : null + const credential = getServerCredential() - if (!credential) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - - const { companyId } = credential + if (!credential) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) const where = { AND: { @@ -27,7 +23,7 @@ export async function GET(request: NextRequest) { { barcode: { contains: search ?? '' } }, { itemCode: { contains: search ?? '' } }, ], - companyId: type == 'all' ? undefined : companyId + companyId: companyId ?? credential.companyId } } diff --git a/src/app/api/stock-opname/export/route.tsx b/src/app/api/stock-opname/export/route.tsx new file mode 100644 index 0000000..1595f20 --- /dev/null +++ b/src/app/api/stock-opname/export/route.tsx @@ -0,0 +1,68 @@ +import { StockOpnameLocationRes } from "@/common/types/stockOpname"; +import { Product } from "@prisma/client"; +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "prisma/client"; +import * as XLSX from "xlsx" + +const SELF_HOST = process.env.SELF_HOST as string + +export async function GET(request: NextRequest) { + const searchParams = request.nextUrl.searchParams + const paramCompanyId = searchParams.get('companyId') + + if (!paramCompanyId) return NextResponse.json({ error: 'Bad Request. Missing companyId' }, { status: 400 }) + + const companyId = parseInt(paramCompanyId) + + const stockOpnames = await prisma.stockOpname.groupBy({ + by: ['productId'], + where: { companyId } + }) + + type SOLocationProduct = (StockOpnameLocationRes & { product: Product | null }) + + const datas: SOLocationProduct[] = [] + + for (const opname of stockOpnames) { + const requestParams = new URLSearchParams({ + productId: opname.productId.toString(), + companyId: companyId.toString() + }) + const detailsFetch = await fetch(`${SELF_HOST}/api/stock-opname/location?${requestParams}`) + const details: StockOpnameLocationRes[] = await detailsFetch.json() + + const product = await prisma.product.findFirst({ where: { id: opname.productId } }) + const mappedData: SOLocationProduct[] = details.map((data) => ({ + ...data, + product + })) + + datas.push(...mappedData) + } + + const dataSheet = datas.map((data) => ({ + location: data.name, + name: data.product?.name, + itemCode: data.product?.itemCode, + barcode: data.product?.barcode, + onhandQty: data.product?.onhandQty, + differenceQty: data.product?.differenceQty, + count1: data.COUNT1.quantity, + count2: data.COUNT2.quantity, + count3: data.COUNT3.quantity, + verification: data.VERIFICATION.quantity, + isDifferent: data.product?.isDifferent + })) + + const worksheet = XLSX.utils.json_to_sheet(dataSheet) + const workbook = XLSX.utils.book_new() + XLSX.utils.book_append_sheet(workbook, worksheet, "Sheet1") + const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', 'type': 'buffer' }) + + return new NextResponse(excelBuffer, { + headers: { + 'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'Content-Disposition': 'attachment; filename=export.xlsx' + } + }) +} \ No newline at end of file diff --git a/src/app/api/stock-opname/location/route.tsx b/src/app/api/stock-opname/location/route.tsx index 1009486..e6c20ec 100644 --- a/src/app/api/stock-opname/location/route.tsx +++ b/src/app/api/stock-opname/location/route.tsx @@ -1,28 +1,8 @@ import { DetailTeam, StockOpnameLocationRes } from "@/common/types/stockOpname"; -import { Location, Team } from "@prisma/client"; +import { Team } from "@prisma/client"; import { NextRequest, NextResponse } from "next/server"; import { prisma } from "prisma/client"; - - -const getOpnameQuantity = async (where: { locationId: number, productId: number }) => { - const detailTeam: DetailTeam = { - COUNT1: { quantity: undefined, user: undefined }, - COUNT2: { quantity: undefined, user: undefined }, - VERIFICATION: { quantity: undefined, user: undefined }, - } - for (const team of Object.keys(Team)) { - const opname = await prisma.stockOpname.findFirst({ - where: { ...where, team: team as Team }, - select: { quantity: true, user: true }, - }) - if (!opname) continue - detailTeam[team as Team]['quantity'] = opname?.quantity - detailTeam[team as Team]['user'] = opname?.user - } - return detailTeam -} - export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams; const productId = searchParams.get('productId') ?? ''; @@ -54,4 +34,23 @@ export async function GET(request: NextRequest) { } return NextResponse.json(result) +} + +const getOpnameQuantity = async (where: { locationId: number, productId: number }) => { + const detailTeam: DetailTeam = { + COUNT1: { quantity: undefined, user: undefined }, + COUNT2: { quantity: undefined, user: undefined }, + COUNT3: { quantity: undefined, user: undefined }, + VERIFICATION: { quantity: undefined, user: undefined }, + } + for (const team of Object.keys(Team)) { + const opname = await prisma.stockOpname.findFirst({ + where: { ...where, team: team as Team }, + select: { quantity: true, user: true }, + }) + if (!opname) continue + detailTeam[team as Team]['quantity'] = opname?.quantity + detailTeam[team as Team]['user'] = opname?.user + } + return detailTeam } \ No newline at end of file diff --git a/src/app/api/stock-opname/quantity/route.tsx b/src/app/api/stock-opname/quantity/route.tsx index 621297f..fe38c0b 100644 --- a/src/app/api/stock-opname/quantity/route.tsx +++ b/src/app/api/stock-opname/quantity/route.tsx @@ -1,10 +1,9 @@ -import { Credential } from "@/common/types/auth"; +import getServerCredential from "@/common/libs/getServerCredential"; import { NextRequest, NextResponse } from "next/server"; import { prisma } from "prisma/client"; export async function GET(request: NextRequest) { - const credentialStr = request.cookies.get('credential')?.value - const credential: Credential | null = credentialStr ? JSON.parse(credentialStr) : null + const credential = getServerCredential() if (!credential) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) diff --git a/src/app/api/stock-opname/route.tsx b/src/app/api/stock-opname/route.tsx index 50feacd..406981b 100644 --- a/src/app/api/stock-opname/route.tsx +++ b/src/app/api/stock-opname/route.tsx @@ -1,44 +1,23 @@ -import { Credential } from "@/common/types/auth"; import { StockOpnameLocationRes, StockOpnameRequest } from "@/common/types/stockOpname"; import { Team } from "@prisma/client"; import { NextRequest, NextResponse } from "next/server"; import { prisma } from "prisma/client"; +import _ from "lodash" +import getServerCredential from "@/common/libs/getServerCredential"; type Quantity = { [key in keyof typeof Team]: number | null } -const calculateOpnameQuantity = async ( - where: { - productId: number, - companyId: number - } -): Promise => { - const quantity: Quantity = { - COUNT1: null, - COUNT2: null, - VERIFICATION: null, - } - for (const team of Object.values(Team)) { - const opnameQty = await prisma.stockOpname.groupBy({ - by: ['productId', 'team'], - _sum: { quantity: true }, - where: { team, ...where } - }) - if (opnameQty.length === 0) continue - quantity[team] = opnameQty[0]._sum.quantity - } - return quantity -} - export async function GET(request: NextRequest) { const PAGE_SIZE = 30; const params = request.nextUrl.searchParams const companyId = params.get('companyId') const search = params.get('search') - const page = params.get('page') ?? null; - const intPage = page ? parseInt(page) : 1; + const page = params.get('page') ?? null + const show = params.get('show') + const intPage = page ? parseInt(page) : 1 if (!companyId) { return NextResponse.json({ error: 'Bad Request. Missing companyId' }, { status: 400 }) @@ -48,6 +27,7 @@ export async function GET(request: NextRequest) { AND: { stockOpnames: { some: {} }, companyId: parseInt(companyId), + isDifferent: show ? (show == 'diff' ? true : false) : undefined, OR: [ { name: { contains: search ?? '' } }, { itemCode: { contains: search ?? '' } }, @@ -96,9 +76,28 @@ export async function GET(request: NextRequest) { }) } +const calculateOpnameQuantity = async ( + where: { + productId: number, + companyId: number + } +): Promise => { + const quantity: Quantity = { COUNT1: null, COUNT2: null, COUNT3: null, VERIFICATION: null } + + for (const team of Object.values(Team)) { + const opnameQty = await prisma.stockOpname.groupBy({ + by: ['productId', 'team'], + _sum: { quantity: true }, + where: { team, ...where } + }) + if (opnameQty.length === 0) continue + quantity[team] = opnameQty[0]._sum.quantity + } + return quantity +} + export async function POST(request: NextRequest) { - const credentialStr = request.cookies.get('credential')?.value - const credential: Credential | null = credentialStr ? JSON.parse(credentialStr) : null + const credential = getServerCredential() if (!credential) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) @@ -135,7 +134,7 @@ export async function POST(request: NextRequest) { }) } - computeIsDifferent({ productId: body.product, companyId: companyId }) + await computeIsDifferent({ productId: body.product, companyId: companyId }) return NextResponse.json(newStockOpname) } @@ -152,6 +151,7 @@ const computeIsDifferent = async ({ const totalQty: { [key in keyof typeof Team]: number | null } = { COUNT1: null, COUNT2: null, + COUNT3: null, VERIFICATION: null } @@ -172,43 +172,44 @@ const computeIsDifferent = async ({ let isDifferent: boolean = false for (const opname of stockOpnames) { - let { COUNT1, COUNT2, VERIFICATION } = opname - - if (!totalQty['COUNT1'] && COUNT1.quantity) totalQty['COUNT1'] = 0 - if (!totalQty['COUNT2'] && COUNT2.quantity) totalQty['COUNT2'] = 0 - if (!totalQty['VERIFICATION'] && VERIFICATION.quantity) totalQty['VERIFICATION'] = 0 + let { COUNT1, COUNT2, COUNT3, VERIFICATION } = opname - if (totalQty['COUNT1'] !== null && COUNT1.quantity) totalQty['COUNT1'] += COUNT1.quantity - if (totalQty['COUNT2'] !== null && COUNT2.quantity) totalQty['COUNT2'] += COUNT2.quantity - if (totalQty['VERIFICATION'] !== null && VERIFICATION.quantity) totalQty['VERIFICATION'] += VERIFICATION.quantity + if (totalQty['COUNT1'] === null && _.isNumber(COUNT1.quantity)) totalQty['COUNT1'] = 0 + if (totalQty['COUNT2'] === null && _.isNumber(COUNT2.quantity)) totalQty['COUNT2'] = 0 + if (totalQty['COUNT3'] === null && _.isNumber(COUNT3.quantity)) totalQty['COUNT3'] = 0 + if (totalQty['VERIFICATION'] === null && _.isNumber(VERIFICATION.quantity)) totalQty['VERIFICATION'] = 0 - if (isCount2Counted && COUNT1.quantity != COUNT2.quantity) { - isDifferent = true - } + if (_.isNumber(totalQty['COUNT1']) && _.isNumber(COUNT1.quantity)) totalQty['COUNT1'] += COUNT1.quantity + if (_.isNumber(totalQty['COUNT2']) && _.isNumber(COUNT2.quantity)) totalQty['COUNT2'] += COUNT2.quantity + if (_.isNumber(totalQty['COUNT3']) && _.isNumber(COUNT3.quantity)) totalQty['COUNT3'] += COUNT3.quantity + if (_.isNumber(totalQty['VERIFICATION']) && _.isNumber(VERIFICATION.quantity)) totalQty['VERIFICATION'] += VERIFICATION.quantity } - const product = await prisma.product.findFirst({ - where: { id: productId } - }) + const product = await prisma.product.findFirst({ where: { id: productId } }) if (!product) return const onhandQty = product?.onhandQty || 0 if (!isDifferent) { - if ( - (typeof totalQty['VERIFICATION'] === 'number' && totalQty['VERIFICATION'] > 0) || - totalQty['COUNT2'] == onhandQty || - totalQty['COUNT1'] == onhandQty || - totalQty['COUNT1'] == totalQty['COUNT2'] - ) { - isDifferent = false - } else { - isDifferent = true + const conditional = { + wasVerified: typeof totalQty['VERIFICATION'] === 'number' && totalQty['VERIFICATION'] > 0, + anyCountEqWithOnhand: [totalQty['COUNT1'], totalQty['COUNT2'], totalQty['COUNT3']].includes(onhandQty), + 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'] } - if (isCount2Counted && totalQty['COUNT1'] != onhandQty) { - isDifferent = true - } + isDifferent = !( + conditional.wasVerified || + conditional.anyCountEqWithOnhand || + conditional.count1EqWithCount2 || + conditional.count1EqWithCount3 || + conditional.count2EqWithCount3 + ) + + // if (isCount2Counted && totalQty['COUNT1'] != onhandQty) { + // isDifferent = true + // } } await prisma.product.update({ diff --git a/src/common/components/Authenticated/index.tsx b/src/common/components/Authenticated/index.tsx index cf7086e..3957a8d 100644 --- a/src/common/components/Authenticated/index.tsx +++ b/src/common/components/Authenticated/index.tsx @@ -1,10 +1,9 @@ -import { cookies } from 'next/headers' +import getServerCredential from '@/common/libs/getServerCredential' import { redirect } from 'next/navigation' import React from 'react' const Authenticated = ({ children }: { children: React.ReactNode }) => { - const credentialStr = cookies().get('credential')?.value - const credential: Credential | null = credentialStr ? JSON.parse(credentialStr) : null + const credential = getServerCredential() if (!credential) redirect('/login') diff --git a/src/common/constants/team.ts b/src/common/constants/team.ts index cfb895b..8daad6a 100644 --- a/src/common/constants/team.ts +++ b/src/common/constants/team.ts @@ -1,12 +1,15 @@ import { TeamAliases } from "../types/team"; -export const teamAliases: TeamAliases = { +export const TEAM_ALIASES: TeamAliases = { COUNT1: { name: "Hitung 1", }, COUNT2: { name: "Hitung 2", }, + COUNT3: { + name: "Hitung 3", + }, VERIFICATION: { name: "Verifikasi", }, diff --git a/src/common/stores/useResultStore.ts b/src/common/stores/useResultStore.ts index d8da56c..09e8146 100644 --- a/src/common/stores/useResultStore.ts +++ b/src/common/stores/useResultStore.ts @@ -1,20 +1,25 @@ import { create } from "zustand"; +import { SelectOption } from "../types/select"; type State = { filter: { search: string; company: string; + show: string; }; + companies: SelectOption[]; }; type Action = { updateFilter: (name: string, value: string) => void; + setCompanies: (data: SelectOption[]) => void; }; export const useResultStore = create((set) => ({ filter: { search: "", company: "", + show: "1", }, updateFilter: (name, value) => set((state) => ({ @@ -23,4 +28,9 @@ export const useResultStore = create((set) => ({ [name]: value, }, })), + companies: [], + setCompanies: (data) => + set(() => ({ + companies: data, + })), })); diff --git a/src/modules/login/index.tsx b/src/modules/login/index.tsx index 7247af4..e2b3675 100644 --- a/src/modules/login/index.tsx +++ b/src/modules/login/index.tsx @@ -7,11 +7,11 @@ const Login = () => {

Stock Opname

- +

Masuk terlebih dahulu untuk melanjutkan

- +
) diff --git a/src/modules/login/login.module.css b/src/modules/login/login.module.css index 99575d5..384dc09 100644 --- a/src/modules/login/login.module.css +++ b/src/modules/login/login.module.css @@ -7,7 +7,7 @@ } .title { - @apply text-2xl text-blue-600 font-semibold; + @apply text-3xl text-primary-500 font-bold; } .subtitle { diff --git a/src/modules/profile-card/index.tsx b/src/modules/profile-card/index.tsx index 08c4478..881561d 100644 --- a/src/modules/profile-card/index.tsx +++ b/src/modules/profile-card/index.tsx @@ -1,13 +1,11 @@ -import { Credential } from "@/common/types/auth" import { Avatar, AvatarIcon, Card, CardBody } from '@nextui-org/react'; -import { teamAliases } from '@/common/constants/team'; +import { TEAM_ALIASES } from '@/common/constants/team'; import styles from "./profile-card.module.css" import Dropdown from './components/Dropdown'; -import { cookies } from 'next/headers'; +import getServerCredential from '@/common/libs/getServerCredential'; const ProfileCard = () => { - const credentialStr = cookies().get('credential')?.value - const credential: Credential | null = credentialStr ? JSON.parse(credentialStr) : null + const credential = getServerCredential() return credential && ( @@ -21,7 +19,7 @@ const ProfileCard = () => { · - Tim {teamAliases[credential.team].name} + Tim {TEAM_ALIASES[credential.team].name} diff --git a/src/modules/result/components/DetailRow.tsx b/src/modules/result/components/DetailRow.tsx index 99ccb01..29cf984 100644 --- a/src/modules/result/components/DetailRow.tsx +++ b/src/modules/result/components/DetailRow.tsx @@ -7,6 +7,7 @@ import styles from './table.module.css' import { CornerDownRightIcon } from 'lucide-react'; import { User } from '@prisma/client'; import clsxm from '@/common/libs/clsxm'; +import getClientCredential from '@/common/libs/getClientCredential'; const DetailRow = ({ productId }: { productId: number }) => { const { filter } = useResultStore() @@ -26,7 +27,7 @@ const DetailRow = ({ productId }: { productId: number }) => { if (detailLocation.isLoading) { return ( - +
@@ -53,6 +54,9 @@ const DetailRow = ({ productId }: { productId: number }) => { + + + @@ -64,18 +68,24 @@ const DetailRow = ({ productId }: { productId: number }) => { ) } -const QuantityColumn = ({ data }: { data: { quantity?: number | undefined, user?: User } }) => ( -
- {typeof data?.quantity !== 'number' && '-'} - {data.quantity !== null && ( - <> - {data.quantity} -
- {data.user?.name} -
- - )} -
-) +const QuantityColumn = ({ data }: { data: { quantity?: number | undefined, user?: User } }) => { + const credential = getClientCredential() + + if (!(credential?.team == "VERIFICATION")) return '-' + + return ( +
+ {typeof data?.quantity !== 'number' && '-'} + {data.quantity !== null && ( + <> + {data.quantity} +
+ {data.user?.name} +
+ + )} +
+ ) +} export default DetailRow \ No newline at end of file diff --git a/src/modules/result/components/Filter.tsx b/src/modules/result/components/Filter.tsx index f8bc7b5..73f346c 100644 --- a/src/modules/result/components/Filter.tsx +++ b/src/modules/result/components/Filter.tsx @@ -1,19 +1,16 @@ "use client"; import { Input, Select, SelectItem } from "@nextui-org/react" -import styles from "./filter.module.css" import { Company } from "@prisma/client" -import { useEffect, useState } from "react" +import { useEffect } from "react" import { SelectOption } from "@/common/types/select" import { useResultStore } from "@/common/stores/useResultStore"; -import { getCookie } from "cookies-next"; -import { Credential } from "@/common/types/auth"; +import getClientCredential from "@/common/libs/getClientCredential"; +import { SHOWING_SELECTIONS } from "@/common/constants/result"; const Filter = () => { - const { filter, updateFilter } = useResultStore() - const [companies, setCompanies] = useState([]) + const { filter, updateFilter, companies, setCompanies } = useResultStore() - const credentialStr = getCookie('credential') - const credential: Credential | null = credentialStr ? JSON.parse(credentialStr) : null + const credential = getClientCredential() useEffect(() => { if (credential && !filter.company) @@ -24,7 +21,7 @@ const Filter = () => { loadCompany().then((data: SelectOption[]) => { setCompanies(data) }) - }, [updateFilter]) + }, [setCompanies]) const handleInputChange = (e: React.ChangeEvent) => { const { name, value } = e.target @@ -37,26 +34,41 @@ const Filter = () => { } return ( -
- - +
+
+ +
+
+ +
+
+ +
) } diff --git a/src/modules/result/components/ImportModal.tsx b/src/modules/result/components/ImportModal.tsx index 85e4a97..1551ab2 100644 --- a/src/modules/result/components/ImportModal.tsx +++ b/src/modules/result/components/ImportModal.tsx @@ -1,7 +1,9 @@ import toast from '@/common/libs/toast' +import { useResultStore } from '@/common/stores/useResultStore' import { Button, Modal, ModalBody, ModalContent, ModalHeader } from '@nextui-org/react' import { useMutation } from '@tanstack/react-query' -import React, { ChangeEvent, FormEvent, useState } from 'react' +import { AlertTriangleIcon } from 'lucide-react' +import React, { ChangeEvent, FormEvent, useMemo, useState } from 'react' type Props = { modal: { @@ -12,6 +14,11 @@ type Props = { const ImportModal = ({ modal }: Props) => { const [file, setFile] = useState() + const { companies, filter } = useResultStore() + + const selectedCompany = useMemo(() => { + return companies.find((c) => c.value == filter.company) + }, [companies, filter.company]) const handleFileChange = (e: ChangeEvent) => { if (e.target.files) setFile(e.target.files[0]) @@ -21,7 +28,7 @@ const ImportModal = ({ modal }: Props) => { mutationKey: ['import-product'], mutationFn: async () => { if (!file) return - return await fetch('/api/product/import', { + return await fetch(`/api/product/import?companyId=${filter.company}`, { method: 'POST', body: file, headers: { 'content-type': file.type, 'content-length': `${file.size}` } @@ -52,6 +59,15 @@ const ImportModal = ({ modal }: Props) => { + +
+
+ +
+ + Hati-hati aksi ini akan menghapus semua data produk dan hasil stock opname di perusahaan {selectedCompany?.label} + +
diff --git a/src/modules/result/components/MoreMenu.tsx b/src/modules/result/components/MoreMenu.tsx index a7380f4..03c9786 100644 --- a/src/modules/result/components/MoreMenu.tsx +++ b/src/modules/result/components/MoreMenu.tsx @@ -4,12 +4,14 @@ import { MoreVerticalIcon } from 'lucide-react' import React from 'react' import ImportModal from './ImportModal'; import ProductModal from './ProductModal'; +import getClientCredential from '@/common/libs/getClientCredential'; const MoreMenu = () => { + const credential = getClientCredential() const importModal = useDisclosure(); const productModal = useDisclosure(); - return ( + return credential && credential.team == 'VERIFICATION' && ( <> diff --git a/src/modules/result/components/ProductModal.tsx b/src/modules/result/components/ProductModal.tsx index a4ef49e..001c946 100644 --- a/src/modules/result/components/ProductModal.tsx +++ b/src/modules/result/components/ProductModal.tsx @@ -1,3 +1,4 @@ +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 { Product } from '@prisma/client' import { useQuery } from '@tanstack/react-query' @@ -15,18 +16,19 @@ const ProductModal = ({ modal }: Props) => { const [page, setPage] = useState(1) const [search, setSearch] = useState("") const debouncedSearch = useDebounce(search, 500) + const { filter } = useResultStore() useEffect(() => { setPage(1) }, [debouncedSearch]) const { data } = useQuery({ - queryKey: ['product', page, debouncedSearch], + queryKey: ['product', page, debouncedSearch, filter.company], queryFn: async () => { const searchParams = new URLSearchParams({ page: page.toString(), search: debouncedSearch, - type: 'all' + companyId: filter.company }) const response = await fetch(`/api/product?${searchParams}`) const data: { diff --git a/src/modules/result/components/Table.tsx b/src/modules/result/components/Table.tsx index d2e5af4..021dfef 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 { Badge, Pagination, Spacer } from "@nextui-org/react" +import { Button, Pagination, Skeleton, Spacer, Spinner } from "@nextui-org/react" import { useQuery } from "@tanstack/react-query"; import { useSearchParams } from "next/navigation"; import { useRouter } from "next/navigation"; @@ -9,31 +9,68 @@ import styles from "./table.module.css" import clsxm from "@/common/libs/clsxm"; import DetailRow from "./DetailRow"; import { useDebounce } from "usehooks-ts"; +import getClientCredential from "@/common/libs/getClientCredential"; +import { SHOWING_SELECTIONS } from "@/common/constants/result"; +import Link from "next/link"; +import { useState } from "react"; const Table = () => { + const credential = getClientCredential() + const params = useSearchParams() const router = useRouter() const page = params.get('page') ?? '1' - const { filter: { company, search } } = useResultStore() + const { filter: { company, search, show } } = useResultStore() const debouncedSearch = useDebounce(search, 500) const stockOpnames = useQuery({ - queryKey: ['stockOpnames', company, debouncedSearch, page], + queryKey: ['stockOpnames', company, debouncedSearch, page, show], queryFn: async () => { const searchParams = new URLSearchParams() if (!company) return null searchParams.set('companyId', company) - searchParams.set('page', page); + + const showValue = SHOWING_SELECTIONS.find((item) => item.key === show)?.value || '' + searchParams.set('show', showValue); + if (debouncedSearch) searchParams.set('search', debouncedSearch) + searchParams.set('page', page); + return await fetch(`/api/stock-opname?${searchParams}`) .then(res => res.json()) }, }) + const { filter } = useResultStore() + + const [exportLoad, setExportLoad] = useState(false) + + const exportResult = async () => { + setExportLoad(true) + const response = await fetch(`/api/stock-opname/export?companyId=${filter.company}`) + const buffer = await response.arrayBuffer() + + const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }) + const url = window.URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = 'export.xlsx' + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + setExportLoad(false) + } + return ( <> +
+ +
+
@@ -41,6 +78,7 @@ const Table = () => { + @@ -48,13 +86,17 @@ const Table = () => { {stockOpnames.data?.result.map((stockOpname: StockOpnameRes['result']) => ( <> - + + @@ -85,7 +130,19 @@ const Table = () => { {stockOpnames.data?.result.length === 0 && ( - + + + )} + + {stockOpnames.isLoading && ( + + )} diff --git a/src/modules/result/components/filter.module.css b/src/modules/result/components/filter.module.css index 7142d3e..d9171eb 100644 --- a/src/modules/result/components/filter.module.css +++ b/src/modules/result/components/filter.module.css @@ -1,3 +1,3 @@ .wrapper { - @apply flex gap-x-2; + @apply flex flex-wrap gap-y-3; } diff --git a/src/modules/result/components/table.module.css b/src/modules/result/components/table.module.css index c888070..997e9c2 100644 --- a/src/modules/result/components/table.module.css +++ b/src/modules/result/components/table.module.css @@ -17,7 +17,3 @@ first:rounded-md last:rounded-md; } - -.td { - @apply text-neutral-800; -} diff --git a/src/modules/result/index.tsx b/src/modules/result/index.tsx index 73e613a..d69d53f 100644 --- a/src/modules/result/index.tsx +++ b/src/modules/result/index.tsx @@ -13,7 +13,7 @@ const Result = () => { - +
NAMA PRODUK TIM HITUNG 1 TIM HITUNG 2TIM HITUNG 3 TIM VERIFIKASI ON-HAND QTY GUDANG SELISIH
-
- {stockOpname.isDifferent ? 'Tidak Sesuai' : 'Sesuai'} + {stockOpname.isDifferent ? 'Selisih' : 'Aman'}
@@ -63,19 +105,22 @@ const Table = () => { {stockOpname.barcode ? ` [${stockOpname.barcode}]` : ''} - {stockOpname.quantity.COUNT1 || '-'} + {credential?.team == 'VERIFICATION' && typeof stockOpname.quantity.COUNT1 === 'number' ? stockOpname.quantity.COUNT1 : '-'} + + {credential?.team == 'VERIFICATION' && typeof stockOpname.quantity.COUNT2 === 'number' ? stockOpname.quantity.COUNT2 : '-'} - {stockOpname.quantity.COUNT2 || '-'} + {credential?.team == 'VERIFICATION' && typeof stockOpname.quantity.COUNT3 === 'number' ? stockOpname.quantity.COUNT3 : '-'} - {stockOpname.quantity.VERIFICATION || '-'} + {credential?.team == 'VERIFICATION' && typeof stockOpname.quantity.VERIFICATION === 'number' ? stockOpname.quantity.VERIFICATION : '-'} - {stockOpname.onhandQty} + {credential?.team == 'VERIFICATION' ? stockOpname.onhandQty : '-'} - {stockOpname.differenceQty} + {credential?.team == 'VERIFICATION' ? stockOpname.differenceQty : '-'}
Belum ada data untuk ditampilkanBelum ada data untuk ditampilkan
+
+ {Array.from({ length: 6 }, (_, i) => ( + + ))} +
+
) diff --git a/src/modules/stock-opname/index.tsx b/src/modules/stock-opname/index.tsx index 0a5c848..eb8b796 100644 --- a/src/modules/stock-opname/index.tsx +++ b/src/modules/stock-opname/index.tsx @@ -10,7 +10,8 @@ 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"; +import { AlertCircleIcon, ScanLineIcon } from "lucide-react"; +import Link from "next/link"; type ActiveScanner = "product" | "location" | null type ScannerOption = { value: number, label: string } @@ -116,7 +117,7 @@ const StockOpname = () => { onChange={handleSelectChange} className="flex-1" /> - @@ -137,7 +138,7 @@ const StockOpname = () => { onChange={handleSelectChange} className="flex-1" /> - @@ -179,6 +180,8 @@ const StockOpname = () => { > {saveMutation.isPending ? : 'Simpan'} + + @@ -195,7 +198,7 @@ const StockOpname = () => { <>
{scannerOptions.map((option) => ( - handleScannerOptionPress(option)}> + handleScannerOptionPress(option)}> {option.label} ))} @@ -245,17 +248,11 @@ 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 - } - }) + return data?.products.map((product) => ({ + value: product.id, + label: `${product.itemCode ? `[${product.itemCode}]` : ''} ${product.name} ${product.barcode ? ` [${product.barcode}]` : ''}` + } + )) } export default StockOpname \ No newline at end of file -- cgit v1.2.3