summaryrefslogtreecommitdiff
path: root/src/app/api
diff options
context:
space:
mode:
authorRafi Zadanly <zadanlyr@gmail.com>2023-11-09 15:40:16 +0700
committerRafi Zadanly <zadanlyr@gmail.com>2023-11-09 15:40:16 +0700
commitbe0f537dc4fe384eef09436833c6407e6482c16d (patch)
tree194b1ad3f34396cb8149075bbbd38b854aedf361 /src/app/api
parent5d5401ae36e7e0c8eb38ccd943c1aa44a9573d35 (diff)
Initial commit
Diffstat (limited to 'src/app/api')
-rw-r--r--src/app/api/auth/login/route.tsx36
-rw-r--r--src/app/api/company/route.tsx8
-rw-r--r--src/app/api/hash/route.tsx10
-rw-r--r--src/app/api/location/route.tsx27
-rw-r--r--src/app/api/product/import/route.tsx27
-rw-r--r--src/app/api/product/route.tsx48
-rw-r--r--src/app/api/stock-opname/location/route.tsx57
-rw-r--r--src/app/api/stock-opname/quantity/route.tsx34
-rw-r--r--src/app/api/stock-opname/route.tsx218
9 files changed, 465 insertions, 0 deletions
diff --git a/src/app/api/auth/login/route.tsx b/src/app/api/auth/login/route.tsx
new file mode 100644
index 0000000..d4da662
--- /dev/null
+++ b/src/app/api/auth/login/route.tsx
@@ -0,0 +1,36 @@
+import { NextRequest, NextResponse } from "next/server";
+import { prisma } from "prisma/client";
+import { cookies } from "next/headers"
+import { Credential } from "@/common/types/auth"
+import bcrypt from "bcrypt";
+import jwt from "jsonwebtoken";
+
+const JWT_SECRET = process.env.JWT_SECRET as string
+
+export async function POST(request: NextRequest) {
+ const body = await request.json()
+
+ const user = await prisma.user.findUnique({
+ where: { username: body.username },
+ include: {
+ company: true
+ }
+ })
+
+ if (!user) {
+ return NextResponse.json({ error: 'User not found' }, { status: 404 })
+ }
+
+ if (!await bcrypt.compare(body.password, user.password)) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+ }
+
+ const credential: Credential = {
+ ...user,
+ token: jwt.sign(user, JWT_SECRET, { expiresIn: '10y' })
+ }
+
+ cookies().set('credential', JSON.stringify(credential))
+
+ return NextResponse.json(credential)
+} \ No newline at end of file
diff --git a/src/app/api/company/route.tsx b/src/app/api/company/route.tsx
new file mode 100644
index 0000000..c970108
--- /dev/null
+++ b/src/app/api/company/route.tsx
@@ -0,0 +1,8 @@
+import { NextResponse } from "next/server";
+import { prisma } from "prisma/client";
+
+export async function GET() {
+ const companies = await prisma.company.findMany()
+
+ return NextResponse.json(companies)
+} \ No newline at end of file
diff --git a/src/app/api/hash/route.tsx b/src/app/api/hash/route.tsx
new file mode 100644
index 0000000..2727e1f
--- /dev/null
+++ b/src/app/api/hash/route.tsx
@@ -0,0 +1,10 @@
+import { NextRequest, NextResponse } from "next/server";
+import bcrypt from "bcrypt"
+
+export async function GET(request: NextRequest) {
+ const searchParams = request.nextUrl.searchParams;
+ const password = searchParams.get('password') ?? '';
+ const hash = await bcrypt.hash(password, 10);
+
+ return NextResponse.json({ hash });
+} \ No newline at end of file
diff --git a/src/app/api/location/route.tsx b/src/app/api/location/route.tsx
new file mode 100644
index 0000000..452a85d
--- /dev/null
+++ b/src/app/api/location/route.tsx
@@ -0,0 +1,27 @@
+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
+
+ if (!credential) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+ }
+
+ const { companyId } = credential
+
+ const locations = await prisma.location.findMany({
+ where: {
+ companyId,
+ name: { contains: search ?? '' }
+ },
+ take: 20
+ })
+
+ return NextResponse.json(locations)
+} \ No newline at end of file
diff --git a/src/app/api/product/import/route.tsx b/src/app/api/product/import/route.tsx
new file mode 100644
index 0000000..7358205
--- /dev/null
+++ b/src/app/api/product/import/route.tsx
@@ -0,0 +1,27 @@
+import { NextRequest, NextResponse } from "next/server";
+import { prisma } from "prisma/client";
+import * as XLSX from "xlsx";
+
+export async function POST(request: NextRequest) {
+ const body = await request.arrayBuffer();
+ const workbook = XLSX.read(body, { type: 'buffer' })
+ const worksheetName = workbook.SheetNames[0]
+ const worksheet = workbook.Sheets[worksheetName]
+ 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],
+ barcode: row[1],
+ itemCode: row[2],
+ onhandQty: row[3],
+ differenceQty: row[4],
+ companyId: row[5],
+ }));
+
+ await prisma.product.createMany({ data: newProducts })
+
+ return NextResponse.json(true)
+} \ No newline at end of file
diff --git a/src/app/api/product/route.tsx b/src/app/api/product/route.tsx
new file mode 100644
index 0000000..1161a4e
--- /dev/null
+++ b/src/app/api/product/route.tsx
@@ -0,0 +1,48 @@
+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 credentialStr = request.cookies.get('credential')?.value
+ const credential: Credential | null = credentialStr ? JSON.parse(credentialStr) : null
+
+ if (!credential) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+ }
+
+ const { companyId } = credential
+
+ const where = {
+ AND: {
+ OR: [
+ { name: { contains: search ?? '' } },
+ { barcode: { contains: search ?? '' } },
+ { itemCode: { contains: search ?? '' } },
+ ],
+ companyId: type == 'all' ? undefined : companyId
+ }
+ }
+
+ const products = await prisma.product.findMany({
+ where,
+ include: { company: true },
+ take: PAGE_SIZE,
+ skip: (intPage - 1) * PAGE_SIZE
+ })
+
+ const count = await prisma.product.count({ where })
+ const pagination = {
+ page: intPage,
+ totalPage: Math.ceil(count / PAGE_SIZE),
+ }
+
+ return NextResponse.json({ products, ...pagination })
+} \ 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
new file mode 100644
index 0000000..1009486
--- /dev/null
+++ b/src/app/api/stock-opname/location/route.tsx
@@ -0,0 +1,57 @@
+import { DetailTeam, StockOpnameLocationRes } from "@/common/types/stockOpname";
+import { Location, 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') ?? '';
+ const companyId = searchParams.get('companyId');
+ const intProductId = parseInt(productId);
+
+ if (!companyId) {
+ return NextResponse.json({ error: 'Bad Request. Missing companyId' }, { status: 400 })
+ }
+
+ const intCompanyId = parseInt(companyId);
+
+ const opnameLocByProduct = await prisma.stockOpname.groupBy({
+ by: ['locationId'],
+ where: { productId: intProductId, companyId: intCompanyId },
+ })
+
+ const locationIds = opnameLocByProduct.map((opname) => opname.locationId)
+
+ const result: StockOpnameLocationRes[] = []
+
+ for (const locationId of locationIds) {
+ const detail = await getOpnameQuantity({ locationId, productId: intProductId })
+ const location = await prisma.location.findFirst({
+ where: { id: locationId, companyId: intCompanyId }
+ })
+ if (!location) continue
+ result.push({ ...location, ...detail })
+ }
+
+ return NextResponse.json(result)
+} \ 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
new file mode 100644
index 0000000..621297f
--- /dev/null
+++ b/src/app/api/stock-opname/quantity/route.tsx
@@ -0,0 +1,34 @@
+import { Credential } from "@/common/types/auth";
+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
+
+ if (!credential) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+
+ const searchParams = request.nextUrl.searchParams;
+ let locationId = searchParams.get('locationId');
+ let productId = searchParams.get('productId');
+
+ if (!locationId || !productId) return NextResponse.json({ error: 'Bad Request. Missing locationId and productId' }, { status: 400 })
+ let intLocationId = parseInt(locationId)
+ let intProductId = parseInt(productId)
+
+ const { companyId, team } = credential
+
+ const query = {
+ locationId: intLocationId,
+ productId: intProductId,
+ companyId,
+ team
+ }
+
+ const stockOpname = await prisma.stockOpname.findFirst({
+ where: query,
+ select: { id: true, quantity: true, user: true }
+ })
+
+ return NextResponse.json(stockOpname)
+} \ No newline at end of file
diff --git a/src/app/api/stock-opname/route.tsx b/src/app/api/stock-opname/route.tsx
new file mode 100644
index 0000000..50feacd
--- /dev/null
+++ b/src/app/api/stock-opname/route.tsx
@@ -0,0 +1,218 @@
+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";
+
+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;
+
+ if (!companyId) {
+ return NextResponse.json({ error: 'Bad Request. Missing companyId' }, { status: 400 })
+ }
+
+ const where = {
+ AND: {
+ stockOpnames: { some: {} },
+ companyId: parseInt(companyId),
+ OR: [
+ { name: { contains: search ?? '' } },
+ { itemCode: { contains: search ?? '' } },
+ { barcode: { contains: search ?? '' } },
+ ]
+ }
+ }
+
+ const products = await prisma.product.findMany({
+ skip: (intPage - 1) * PAGE_SIZE,
+ take: PAGE_SIZE,
+ where,
+ select: {
+ id: true,
+ name: true,
+ itemCode: true,
+ barcode: true,
+ onhandQty: true,
+ differenceQty: true,
+ isDifferent: true
+ }
+ })
+
+ const productCount = await prisma.product.count({ where })
+
+ const pagination = {
+ page: intPage,
+ totalPage: Math.ceil(productCount / PAGE_SIZE),
+ }
+
+ type ProductWithSum = typeof products[0] & { quantity: Quantity }
+
+ const productsWithSum: ProductWithSum[] = []
+
+ for (const product of products) {
+ const quantity = await calculateOpnameQuantity({
+ productId: product.id,
+ companyId: parseInt(companyId)
+ })
+ productsWithSum.push({ ...product, quantity })
+ }
+
+ return NextResponse.json({
+ result: productsWithSum,
+ ...pagination
+ })
+}
+
+export async function POST(request: NextRequest) {
+ const credentialStr = request.cookies.get('credential')?.value
+ const credential: Credential | null = credentialStr ? JSON.parse(credentialStr) : null
+
+ if (!credential) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+
+ const body: StockOpnameRequest = await request.json()
+
+ const { companyId, team } = credential
+
+ const query = {
+ locationId: body.location,
+ productId: body.product,
+ companyId,
+ team
+ }
+
+ const stockOpname = await prisma.stockOpname.findFirst({
+ where: query
+ })
+
+ const data = {
+ ...query,
+ userId: credential.id,
+ quantity: body.quantity,
+ isDifferent: false
+ }
+
+ let newStockOpname = null
+
+ if (!stockOpname) {
+ newStockOpname = await prisma.stockOpname.create({ data })
+ } else {
+ newStockOpname = await prisma.stockOpname.update({
+ where: { id: stockOpname.id },
+ data
+ })
+ }
+
+ computeIsDifferent({ productId: body.product, companyId: companyId })
+
+ return NextResponse.json(newStockOpname)
+}
+
+const SELF_HOST = process.env.SELF_HOST as string
+
+const computeIsDifferent = async ({
+ companyId,
+ productId
+}: {
+ companyId: number,
+ productId: number
+}) => {
+ const totalQty: { [key in keyof typeof Team]: number | null } = {
+ COUNT1: null,
+ COUNT2: null,
+ VERIFICATION: null
+ }
+
+ const searchParams = new URLSearchParams({
+ companyId: companyId.toString(),
+ productId: productId.toString()
+ })
+
+ const stockOpnamesFetch = await fetch(`${SELF_HOST}/api/stock-opname/location?${searchParams}`)
+ const stockOpnames: StockOpnameLocationRes[] = await stockOpnamesFetch.json()
+
+ const count2Count = await prisma.stockOpname.count({
+ where: { companyId, productId, team: 'COUNT2' }
+ })
+
+ const isCount2Counted: boolean = count2Count > 0
+
+ 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
+
+ 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 (isCount2Counted && COUNT1.quantity != COUNT2.quantity) {
+ isDifferent = true
+ }
+ }
+
+ 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
+ }
+
+ if (isCount2Counted && totalQty['COUNT1'] != onhandQty) {
+ isDifferent = true
+ }
+ }
+
+ await prisma.product.update({
+ where: { id: product.id },
+ data: { isDifferent }
+ })
+} \ No newline at end of file