diff options
| -rw-r--r-- | prisma/migrations/20260215035919_test/migration.sql | 5 | ||||
| -rw-r--r-- | prisma/schema.prisma | 3 | ||||
| -rw-r--r-- | src/app/api/product/import/route.tsx | 55 | ||||
| -rw-r--r-- | src/app/api/product/route.tsx | 2 | ||||
| -rw-r--r-- | src/modules/result/components/ImportModal.tsx | 11 | ||||
| -rw-r--r-- | src/modules/result/components/ProductModal.tsx | 4 | ||||
| -rw-r--r-- | src/modules/stock-opname/index.tsx | 29 |
7 files changed, 93 insertions, 16 deletions
diff --git a/prisma/migrations/20260215035919_test/migration.sql b/prisma/migrations/20260215035919_test/migration.sql new file mode 100644 index 0000000..8b85792 --- /dev/null +++ b/prisma/migrations/20260215035919_test/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "Product" ADD COLUMN "locationId" INTEGER; + +-- AddForeignKey +ALTER TABLE "Product" ADD CONSTRAINT "Product_locationId_fkey" FOREIGN KEY ("locationId") REFERENCES "Location"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 68bc387..bcbc9a2 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -33,6 +33,7 @@ model Location { name String company Company @relation(fields: [companyId], references: [id]) companyId Int + products Product[] stockOpnames StockOpname[] } @@ -48,6 +49,8 @@ model Product { isDifferent Boolean company Company @relation(fields: [companyId], references: [id]) companyId Int + location Location? @relation(fields: [locationId], references: [id]) + locationId Int? stockOpnames StockOpname[] } diff --git a/src/app/api/product/import/route.tsx b/src/app/api/product/import/route.tsx index 37b4690..f96c2d9 100644 --- a/src/app/api/product/import/route.tsx +++ b/src/app/api/product/import/route.tsx @@ -16,18 +16,49 @@ export async function POST(request: NextRequest) { const fileData: any[] = XLSX.utils.sheet_to_json(worksheet, { header: 1 }) fileData.shift(); - const newProducts = fileData.map(row => ({ - id: undefined, - isDifferent: false, - name: row[0]?.toString() || '', // Handle undefined values - barcode: row[1]?.toString() || '', // Handle undefined values - itemCode: row[2]?.toString() || '', // Handle undefined values - onhandQty: row[3] || 0, // Handle undefined values - differenceQty: row[4] || 0, // Handle undefined values - companyId: intCompanyId, // Use the parsed company ID - externalId: row[6] ? row[6].toString() : null, // Handle undefined values - value: row[7] != null ? Number(row[7]) || 0 : null, - })); + // Fetch all locations for this company for location validation + const locations = await prisma.location.findMany({ + where: { companyId: intCompanyId } + }) + + // Create a map for quick location lookup by name (case-insensitive) and by id + const locationByName = new Map( + locations.map(loc => [loc.name.toLowerCase(), loc.id]) + ) + const locationById = new Map( + locations.map(loc => [loc.id.toString(), loc.id]) + ) + + const newProducts = fileData.map(row => { + let locationId: number | null = null + + // Parse locationId from column 8 + const locationValue = row[8]?.toString().trim() + + if (locationValue) { + // Try to match by ID first, then by name + const idAsNumber = parseInt(locationValue) + if (!isNaN(idAsNumber) && locationById.has(idAsNumber.toString())) { + locationId = idAsNumber + } else if (locationByName.has(locationValue.toLowerCase())) { + locationId = locationByName.get(locationValue.toLowerCase()) || null + } + } + + return { + id: undefined, + isDifferent: false, + name: row[0]?.toString() || '', // Handle undefined values + barcode: row[1]?.toString() || '', // Handle undefined values + itemCode: row[2]?.toString() || '', // Handle undefined values + onhandQty: row[3] || 0, // Handle undefined values + differenceQty: row[4] || 0, // Handle undefined values + companyId: intCompanyId, // Use the parsed company ID + externalId: row[6] ? row[6].toString() : null, // Handle undefined values + value: row[7] != null ? Number(row[7]) || 0 : null, + locationId: locationId, // Set locationId or null if not found/provided + } + }); const whereCompany = { companyId: intCompanyId } diff --git a/src/app/api/product/route.tsx b/src/app/api/product/route.tsx index de8a482..23bb065 100644 --- a/src/app/api/product/route.tsx +++ b/src/app/api/product/route.tsx @@ -36,7 +36,7 @@ export async function GET(request: NextRequest) { const products = await prisma.product.findMany({ where, - include: { company: true }, + include: { company: true, location: true }, take: PAGE_SIZE, skip: (intPage - 1) * PAGE_SIZE, orderBy: { name: 'asc' } diff --git a/src/modules/result/components/ImportModal.tsx b/src/modules/result/components/ImportModal.tsx index 1bb7b26..daacb55 100644 --- a/src/modules/result/components/ImportModal.tsx +++ b/src/modules/result/components/ImportModal.tsx @@ -62,6 +62,17 @@ const ImportModal = ({ modal }: Props) => { <form className='pb-6' onSubmit={handleSubmit}> <input type='file' onChange={handleFileChange} accept='.xls, .xlsx' /> <Spacer y={4} /> + + <div className='text-xs p-3 bg-info-100 border border-info-200 text-info-700 rounded-medium flex items-start gap-2'> + <div className='flex-shrink-0'>ℹ️</div> + <div> + <p className='font-semibold'>Format File Excel:</p> + <p>Kolom 1-7: Nama, Barcode, Item Code, Qty On-hand, Qty Gudang Selisih, companyId, ID / External ID, value (per Unit)</p> + <p>Kolom 8 (opsional): Location</p> + </div> + </div> + <Spacer y={4} /> + <Input type="text" label='Ketik ulang untuk konfirmasi' diff --git a/src/modules/result/components/ProductModal.tsx b/src/modules/result/components/ProductModal.tsx index cfd7382..df0a911 100644 --- a/src/modules/result/components/ProductModal.tsx +++ b/src/modules/result/components/ProductModal.tsx @@ -36,7 +36,7 @@ const ProductModal = ({ modal }: Props) => { }) const response = await fetch(`/api/product?${searchParams}`) const data: { - products: (Product & { company: { id: number, name: string } })[], + products: (Product & { company: { id: number, name: string }, location?: { id: number, name: string } | null })[], page: number, totalPage: number } = await response.json() @@ -89,6 +89,7 @@ const ProductModal = ({ modal }: Props) => { <TableColumn>BARCODE</TableColumn> <TableColumn>ON-HAND QTY</TableColumn> <TableColumn>DIFFERENCE QTY</TableColumn> + <TableColumn>LOCATION</TableColumn> <TableColumn>COMPANY</TableColumn> </TableHeader> <TableBody items={data?.products || []}> @@ -99,6 +100,7 @@ const ProductModal = ({ modal }: Props) => { <TableCell>{product.barcode}</TableCell> <TableCell>{product.onhandQty}</TableCell> <TableCell>{product.differenceQty}</TableCell> + <TableCell>{(product as any).location?.name || '-'}</TableCell> <TableCell>{product.company.name}</TableCell> </TableRow> )} diff --git a/src/modules/stock-opname/index.tsx b/src/modules/stock-opname/index.tsx index 2937a98..4456b11 100644 --- a/src/modules/stock-opname/index.tsx +++ b/src/modules/stock-opname/index.tsx @@ -37,6 +37,31 @@ const StockOpname = () => { }, [form.product, form.location, setOldOpname]) + // Auto-populate location when a product with assigned location is selected + useEffect(() => { + const loadProductAndSetLocation = async () => { + if (form.product?.value) { + try { + const response = await fetch(`/api/product?search=`) + const data: { products: Product[] } = await response.json() + const selectedProduct = data?.products.find(p => p.id === form.product?.value) + + if (selectedProduct && (selectedProduct as any).location && !form.location?.value) { + const location = (selectedProduct as any).location + updateForm('location', { + value: location.id, + label: location.name + }) + } + } catch (error) { + console.error('Error loading product location:', error) + } + } + } + + loadProductAndSetLocation() + }, [form.product?.value, form.location?.value, updateForm]) + const handleSelectChange = (val: SingleValue<SelectOption>, action: ActionMeta<SelectOption>) => { updateForm(action.name as keyof typeof form, val) } @@ -253,9 +278,9 @@ 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) => ({ + return data?.products.map((product: any) => ({ value: product.id, - label: `${product.itemCode ? `[${product.itemCode}]` : ''} ${product.name} ${product.barcode ? ` [${product.barcode}]` : ''}` + label: `${product.itemCode ? `[${product.itemCode}]` : ''} ${product.name} ${product.barcode ? ` [${product.barcode}]` : ''} ${product.location ? `(Lokasi: ${product.location.name})` : ''}` } )) } |
