summaryrefslogtreecommitdiff
path: root/src/modules/result/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/modules/result/components')
-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
8 files changed, 161 insertions, 66 deletions
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;
-}