summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorRafi Zadanly <zadanlyr@gmail.com>2023-11-17 15:16:23 +0700
committerRafi Zadanly <zadanlyr@gmail.com>2023-11-17 15:16:23 +0700
commit07819844d5ef7e323fd956eacfedecb2f4f4bb80 (patch)
treed83ba8c6ff7f47f568889ff8371ee9d9918ed166 /src
parent10ce1ad59969576244ba786d7d17da2da3fe6f61 (diff)
Update result feature
Diffstat (limited to 'src')
-rw-r--r--src/app/api/location/route.tsx9
-rw-r--r--src/app/api/product/import/route.tsx17
-rw-r--r--src/app/api/product/route.tsx16
-rw-r--r--src/app/api/stock-opname/export/route.tsx68
-rw-r--r--src/app/api/stock-opname/location/route.tsx41
-rw-r--r--src/app/api/stock-opname/quantity/route.tsx5
-rw-r--r--src/app/api/stock-opname/route.tsx111
-rw-r--r--src/common/components/Authenticated/index.tsx5
-rw-r--r--src/common/constants/team.ts5
-rw-r--r--src/common/stores/useResultStore.ts10
-rw-r--r--src/modules/login/index.tsx4
-rw-r--r--src/modules/login/login.module.css2
-rw-r--r--src/modules/profile-card/index.tsx10
-rw-r--r--src/modules/result/components/DetailRow.tsx38
-rw-r--r--src/modules/result/components/Filter.tsx70
-rw-r--r--src/modules/result/components/ImportModal.tsx20
-rw-r--r--src/modules/result/components/MoreMenu.tsx4
-rw-r--r--src/modules/result/components/ProductModal.tsx6
-rw-r--r--src/modules/result/components/Table.tsx83
-rw-r--r--src/modules/result/components/filter.module.css2
-rw-r--r--src/modules/result/components/table.module.css4
-rw-r--r--src/modules/result/index.tsx2
-rw-r--r--src/modules/stock-opname/index.tsx27
23 files changed, 366 insertions, 193 deletions
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<Quantity> => {
- 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<Quantity> => {
+ 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<State & Action>((set) => ({
filter: {
search: "",
company: "",
+ show: "1",
},
updateFilter: (name, value) =>
set((state) => ({
@@ -23,4 +28,9 @@ export const useResultStore = create<State & Action>((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 = () => {
<div className={styles.wrapper}>
<div className={styles.header}>
<h1 className={styles.title}>Stock Opname</h1>
- <Spacer y={1} />
+ <Spacer y={2} />
<h2 className={styles.subtitle}>Masuk terlebih dahulu untuk melanjutkan</h2>
</div>
- <Spacer y={10} />
+ <Spacer y={12} />
<Form />
</div>
)
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 && (
<Card shadow='sm'>
@@ -21,7 +19,7 @@ const ProfileCard = () => {
</span>
&#183;
<span>
- Tim {teamAliases[credential.team].name}
+ Tim {TEAM_ALIASES[credential.team].name}
</span>
</div>
</div>
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 (
<tr>
- <td colSpan={7}>
+ <td colSpan={8}>
<div className='grid grid-cols-1 gap-y-2 w-full'>
<Skeleton className='w-full h-8' />
<Skeleton className='w-full h-8' />
@@ -54,6 +55,9 @@ const DetailRow = ({ productId }: { productId: number }) => {
<QuantityColumn data={location.COUNT2} />
</td>
<td className={styles.td}>
+ <QuantityColumn data={location.COUNT3} />
+ </td>
+ <td className={styles.td}>
<QuantityColumn data={location.VERIFICATION} />
</td>
<td className={styles.td} />
@@ -64,18 +68,24 @@ const DetailRow = ({ productId }: { productId: number }) => {
)
}
-const QuantityColumn = ({ data }: { data: { quantity?: number | undefined, user?: User } }) => (
- <div className='grid grid-cols-1'>
- {typeof data?.quantity !== 'number' && '-'}
- {data.quantity !== null && (
- <>
- <span>{data.quantity}</span>
- <div className='text-xs'>
- {data.user?.name}
- </div>
- </>
- )}
- </div>
-)
+const QuantityColumn = ({ data }: { data: { quantity?: number | undefined, user?: User } }) => {
+ const credential = getClientCredential()
+
+ if (!(credential?.team == "VERIFICATION")) return '-'
+
+ return (
+ <div className='grid grid-cols-1'>
+ {typeof data?.quantity !== 'number' && '-'}
+ {data.quantity !== null && (
+ <>
+ <span>{data.quantity}</span>
+ <div className='text-xs text-neutral-500'>
+ {data.user?.name}
+ </div>
+ </>
+ )}
+ </div>
+ )
+}
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<SelectOption[]>([])
+ 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<HTMLInputElement>) => {
const { name, value } = e.target
@@ -37,26 +34,41 @@ const Filter = () => {
}
return (
- <div className={styles.wrapper}>
- <Input
- className="w-7/12 md:w-10/12"
- type="text"
- label="Produk"
- name="search"
- onChange={handleInputChange}
- value={filter.search}
- />
- <Select
- className="w-5/12 md:w-2/12"
- label="Perusahaan"
- selectedKeys={filter.company}
- name="company"
- onChange={handleSelectChange}
- >
- {companies.map((company) => (
- <SelectItem key={company.value} value={company.value}>{company.label}</SelectItem>
- ))}
- </Select>
+ <div className="flex flex-wrap md:flex-nowrap gap-3">
+ <div className="w-full md:w-10/12">
+ <Input
+ type="text"
+ label="Produk"
+ name="search"
+ onChange={handleInputChange}
+ value={filter.search}
+ />
+ </div>
+ <div className="flex-1 md:w-2/12">
+ <Select
+ label="Perusahaan"
+ selectedKeys={filter.company}
+ name="company"
+ onChange={handleSelectChange}
+ isLoading={!filter.company}
+ >
+ {companies.map((company) => (
+ <SelectItem key={company.value}>{company.label}</SelectItem>
+ ))}
+ </Select>
+ </div>
+ <div className="flex-1 md:w-2/12">
+ <Select
+ label="Tampilkan"
+ selectedKeys={filter.show}
+ name="show"
+ onChange={handleSelectChange}
+ >
+ {SHOWING_SELECTIONS.map((selection) => (
+ <SelectItem key={selection.key}>{selection.label}</SelectItem>
+ ))}
+ </Select>
+ </div>
</div>
)
}
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<File>()
+ const { companies, filter } = useResultStore()
+
+ const selectedCompany = useMemo(() => {
+ return companies.find((c) => c.value == filter.company)
+ }, [companies, filter.company])
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
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) => {
<Button type='submit' color='primary' className='mt-4 w-full' isDisabled={!file || importMutation.isPending}>
{importMutation.isPending ? 'Loading...' : 'Submit'}
</Button>
+
+ <div className='text-xs p-4 bg-danger-600 text-neutral-50 rounded-medium mt-4 flex items-center gap-x-4'>
+ <div>
+ <AlertTriangleIcon size={28} />
+ </div>
+ <span>
+ Hati-hati aksi ini akan menghapus semua data produk dan hasil stock opname di perusahaan {selectedCompany?.label}
+ </span>
+ </div>
</form>
</ModalBody>
</ModalContent>
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' && (
<>
<Dropdown>
<DropdownTrigger>
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<boolean>(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 (
<>
+ <div className="flex">
+ <Button type="button" onPress={exportResult} disabled={exportLoad} variant="flat" className="ml-auto mb-4">
+ {exportLoad ? <><Spinner size="sm" />Exporting...</> : 'Export'}
+ </Button>
+ </div>
+
<div className="w-full overflow-auto pb-4">
<table className="w-full">
<thead className={styles.thead}>
@@ -41,6 +78,7 @@ const Table = () => {
<th className={clsxm(styles.th, '!text-left')}>NAMA PRODUK</th>
<th className={styles.th}>TIM HITUNG 1</th>
<th className={styles.th}>TIM HITUNG 2</th>
+ <th className={styles.th}>TIM HITUNG 3</th>
<th className={styles.th}>TIM VERIFIKASI</th>
<th className={styles.th}>ON-HAND QTY</th>
<th className={styles.th}>GUDANG SELISIH</th>
@@ -48,13 +86,17 @@ const Table = () => {
<tbody className={styles.tbody}>
{stockOpnames.data?.result.map((stockOpname: StockOpnameRes['result']) => (
<>
- <tr key={stockOpname.id} className="border-t border-neutral-200">
+ <tr key={stockOpname.id} className={clsxm("border-t border-neutral-200", {
+ "text-danger-600": stockOpname.isDifferent,
+ "text-success-600": !stockOpname.isDifferent
+ })}
+ >
<td className={styles.td}>
- <div className={clsxm("w-full rounded-lg mr-1 mt-1.5 p-1 text-xs text-white whitespace-nowrap", {
+ <div className={clsxm("w-full rounded-lg mr-1 p-1 text-xs text-white whitespace-nowrap", {
"bg-danger-600": stockOpname.isDifferent,
"bg-success-600": !stockOpname.isDifferent,
})}>
- {stockOpname.isDifferent ? 'Tidak Sesuai' : 'Sesuai'}
+ {stockOpname.isDifferent ? 'Selisih' : 'Aman'}
</div>
</td>
<td className={clsxm(styles.td, '!text-left flex min-w-[250px]')}>
@@ -63,19 +105,22 @@ const Table = () => {
{stockOpname.barcode ? ` [${stockOpname.barcode}]` : ''}
</td>
<td className={styles.td}>
- {stockOpname.quantity.COUNT1 || '-'}
+ {credential?.team == 'VERIFICATION' && typeof stockOpname.quantity.COUNT1 === 'number' ? stockOpname.quantity.COUNT1 : '-'}
+ </td>
+ <td className={styles.td}>
+ {credential?.team == 'VERIFICATION' && typeof stockOpname.quantity.COUNT2 === 'number' ? stockOpname.quantity.COUNT2 : '-'}
</td>
<td className={styles.td}>
- {stockOpname.quantity.COUNT2 || '-'}
+ {credential?.team == 'VERIFICATION' && typeof stockOpname.quantity.COUNT3 === 'number' ? stockOpname.quantity.COUNT3 : '-'}
</td>
<td className={styles.td}>
- {stockOpname.quantity.VERIFICATION || '-'}
+ {credential?.team == 'VERIFICATION' && typeof stockOpname.quantity.VERIFICATION === 'number' ? stockOpname.quantity.VERIFICATION : '-'}
</td>
<td className={styles.td}>
- {stockOpname.onhandQty}
+ {credential?.team == 'VERIFICATION' ? stockOpname.onhandQty : '-'}
</td>
<td className={styles.td}>
- {stockOpname.differenceQty}
+ {credential?.team == 'VERIFICATION' ? stockOpname.differenceQty : '-'}
</td>
</tr>
@@ -85,7 +130,19 @@ const Table = () => {
{stockOpnames.data?.result.length === 0 && (
<tr>
- <td colSpan={7} className="text-center text-neutral-600 py-4">Belum ada data untuk ditampilkan</td>
+ <td colSpan={8} className="text-center text-neutral-600 py-4">Belum ada data untuk ditampilkan</td>
+ </tr>
+ )}
+
+ {stockOpnames.isLoading && (
+ <tr>
+ <td colSpan={8}>
+ <div className="flex flex-col gap-y-2">
+ {Array.from({ length: 6 }, (_, i) => (
+ <Skeleton className="h-8" key={i} />
+ ))}
+ </div>
+ </td>
</tr>
)}
</tbody>
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 = () => {
</div>
<Spacer y={6} />
<Filter />
- <Spacer y={4} />
+ <Spacer y={8} />
<Table />
</>
)
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"
/>
- <Button type="button" className={styles.scanButton} variant="flat" onClick={() => setActiveScanner('location')}>
+ <Button type="button" className={styles.scanButton} variant="flat" onPress={() => setActiveScanner('location')}>
<ScanLineIcon size={20} />
</Button>
</div>
@@ -137,7 +138,7 @@ const StockOpname = () => {
onChange={handleSelectChange}
className="flex-1"
/>
- <Button type="button" className={styles.scanButton} variant="flat" onClick={() => setActiveScanner('product')}>
+ <Button type="button" className={styles.scanButton} variant="flat" onPress={() => setActiveScanner('product')}>
<ScanLineIcon size={20} />
</Button>
</div>
@@ -179,6 +180,8 @@ const StockOpname = () => {
>
{saveMutation.isPending ? <Spinner color="white" size="sm" /> : 'Simpan'}
</Button>
+
+ <Button as={Link} href="/result" variant="flat">Lihat hasil</Button>
</form>
<Modal isOpen={!!activeScanner} onOpenChange={closeModal}>
@@ -195,7 +198,7 @@ const StockOpname = () => {
<>
<div className="max-h-[60vh] overflow-auto grid grid-cols-1 gap-y-4">
{scannerOptions.map((option) => (
- <Card key={option.value} isPressable shadow="none" className="border border-default-300" onPress={() => handleScannerOptionPress(option)}>
+ <Card key={option.value} isPressable shadow="none" className="bg-default-50 border border-default-300" onPress={() => handleScannerOptionPress(option)}>
<CardBody>{option.label}</CardBody>
</Card>
))}
@@ -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