Spaces:
Sleeping
Sleeping
| import prisma from '../utils/db'; | |
| import { catatAudit } from '../utils/audit'; | |
| import { moveToTrash } from '../utils/trash'; | |
| import { StokService } from './stok.service'; | |
| import { deleteFromSupabase } from '../utils/supabase'; | |
| import { APIError } from '../utils/errors'; | |
| export class ProdukService { | |
| static async getProduk(skip = 0, limit = 10, kategoriId?: number, supplierId?: number, pencarian?: string) { | |
| const where: any = { | |
| deleted_at: null, | |
| }; | |
| if (kategoriId) where.kategori_id = kategoriId; | |
| if (supplierId) where.supplier_id = supplierId; | |
| if (pencarian) { | |
| where.OR = [ | |
| { kode: { contains: pencarian } }, | |
| { barcode: { contains: pencarian } }, | |
| { nama: { contains: pencarian } }, | |
| ]; | |
| } | |
| const total = await prisma.produk.count({ where }); | |
| const products = await prisma.produk.findMany({ | |
| where, | |
| skip, | |
| take: limit, | |
| include: { | |
| kategori: true, | |
| supplier: true, | |
| harga_tingkat: true, | |
| }, | |
| }); | |
| return { products, total }; | |
| } | |
| static async getProdukById(id: number) { | |
| const product = await prisma.produk.findFirst({ | |
| where: { id, deleted_at: null }, | |
| include: { | |
| kategori: true, | |
| supplier: true, | |
| harga_tingkat: true, | |
| }, | |
| }); | |
| if (!product) { | |
| throw new APIError('Produk tidak ditemukan', 404); | |
| } | |
| return product; | |
| } | |
| static async getProdukByBarcodeOrKode(term: string) { | |
| const product = await prisma.produk.findFirst({ | |
| where: { | |
| OR: [ | |
| { barcode: term }, | |
| { kode: term }, | |
| ], | |
| deleted_at: null, | |
| }, | |
| include: { | |
| kategori: true, | |
| supplier: true, | |
| harga_tingkat: true, | |
| }, | |
| }); | |
| if (!product) { | |
| throw new APIError('Produk dengan kode/barcode tersebut tidak ditemukan', 404); | |
| } | |
| return product; | |
| } | |
| static async create(data: any, actorId: number, ip: string, userAgent: string) { | |
| // Check duplicate | |
| const existingCode = await prisma.produk.findFirst({ | |
| where: { kode: data.kode, deleted_at: null }, | |
| }); | |
| const existingBarcode = data.barcode ? await prisma.produk.findFirst({ | |
| where: { barcode: data.barcode, deleted_at: null }, | |
| }) : null; | |
| const existing = existingCode || existingBarcode; | |
| if (existing) { | |
| const fieldType = existing.id === existingCode?.id ? 'Kode' : 'Barcode'; | |
| const fieldVal = existing.id === existingCode?.id ? data.kode : data.barcode; | |
| throw new APIError(`${fieldType} produk '${fieldVal}' sudah digunakan`, 400); | |
| } | |
| // Check soft-deleted code or barcode | |
| const softDeletedCode = await prisma.produk.findFirst({ | |
| where: { kode: data.kode, NOT: { deleted_at: null } }, | |
| }); | |
| const softDeletedBarcode = data.barcode ? await prisma.produk.findFirst({ | |
| where: { barcode: data.barcode, NOT: { deleted_at: null } }, | |
| }) : null; | |
| const softDeleted = softDeletedCode || softDeletedBarcode; | |
| if (softDeleted) { | |
| // Reactivate | |
| const restored = await prisma.produk.update({ | |
| where: { id: softDeleted.id }, | |
| data: { | |
| deleted_at: null, | |
| is_aktif: 1, | |
| nama: data.nama, | |
| kategori_id: data.kategori_id, | |
| supplier_id: data.supplier_id ?? null, | |
| satuan: data.satuan ?? 'pcs', | |
| harga_beli: data.harga_beli, | |
| harga_jual: data.harga_jual, | |
| stok_min: data.stok_min ?? 5, | |
| expired_date: data.expired_date ?? null, | |
| foto_url: data.foto_url ?? null, | |
| qr_url: data.qr_url ?? null, | |
| deskripsi: data.deskripsi ?? null, | |
| is_ecer: data.is_ecer ?? 1, | |
| }, | |
| }); | |
| // Reset pricing levels | |
| await prisma.harga_tingkat.deleteMany({ | |
| where: { produk_id: restored.id }, | |
| }); | |
| if (data.harga_tingkat && data.harga_tingkat.length > 0) { | |
| for (const ht of data.harga_tingkat) { | |
| await prisma.harga_tingkat.create({ | |
| data: { | |
| produk_id: restored.id, | |
| tingkat: ht.tingkat, | |
| min_qty: ht.min_qty, | |
| harga: ht.harga, | |
| }, | |
| }); | |
| } | |
| } | |
| await catatAudit({ | |
| userId: actorId, | |
| aksi: 'restore', | |
| entitas: 'produk', | |
| entitasId: restored.id, | |
| dataBaru: { | |
| kode: restored.kode, | |
| nama: restored.nama, | |
| harga_beli: Number(restored.harga_beli), | |
| harga_jual: Number(restored.harga_jual), | |
| }, | |
| ipAddress: ip, | |
| userAgent, | |
| }); | |
| return restored; | |
| } | |
| // Create new product | |
| const product = await prisma.produk.create({ | |
| data: { | |
| kode: data.kode, | |
| barcode: data.barcode ?? null, | |
| nama: data.nama, | |
| kategori_id: data.kategori_id, | |
| supplier_id: data.supplier_id ?? null, | |
| satuan: data.satuan ?? 'pcs', | |
| harga_beli: data.harga_beli, | |
| harga_jual: data.harga_jual, | |
| stok: 0.00, | |
| stok_min: data.stok_min ?? 5, | |
| expired_date: data.expired_date ?? null, | |
| foto_url: data.foto_url ?? null, | |
| qr_url: data.qr_url ?? null, | |
| deskripsi: data.deskripsi ?? null, | |
| is_ecer: data.is_ecer ?? 1, | |
| is_aktif: data.is_aktif ?? 1, | |
| }, | |
| }); | |
| // Create pricing levels | |
| if (data.harga_tingkat && data.harga_tingkat.length > 0) { | |
| for (const ht of data.harga_tingkat) { | |
| await prisma.harga_tingkat.create({ | |
| data: { | |
| produk_id: product.id, | |
| tingkat: ht.tingkat, | |
| min_qty: ht.min_qty, | |
| harga: ht.harga, | |
| }, | |
| }); | |
| } | |
| } | |
| // Tambah stok awal jika disediakan > 0 | |
| if (data.stok && data.stok > 0) { | |
| await StokService.tambahStok({ | |
| produkId: product.id, | |
| qty: data.stok, | |
| hargaBeli: data.harga_beli, | |
| batchNo: 'BATCH-AWAL', | |
| expiredDate: data.expired_date ?? null, | |
| supplierId: data.supplier_id ?? null, | |
| userId: actorId, | |
| catatan: 'Stok awal produk baru', | |
| referensi: 'STOK-AWAL', | |
| }); | |
| } | |
| await catatAudit({ | |
| userId: actorId, | |
| aksi: 'create', | |
| entitas: 'produk', | |
| entitasId: product.id, | |
| dataBaru: { | |
| kode: product.kode, | |
| nama: product.nama, | |
| harga_jual: Number(product.harga_jual), | |
| stok: Number(data.stok ?? 0), | |
| }, | |
| ipAddress: ip, | |
| userAgent, | |
| }); | |
| return product; | |
| } | |
| static async update(id: number, data: any, actorId: number, ip: string, userAgent: string) { | |
| const product = await this.getProdukById(id); | |
| const dataLama = { | |
| kode: product.kode, | |
| barcode: product.barcode, | |
| nama: product.nama, | |
| kategori_id: product.kategori_id, | |
| supplier_id: product.supplier_id, | |
| satuan: product.satuan, | |
| harga_beli: Number(product.harga_beli), | |
| harga_jual: Number(product.harga_jual), | |
| stok: Number(product.stok), | |
| stok_min: Number(product.stok_min), | |
| is_ecer: product.is_ecer, | |
| is_aktif: product.is_aktif, | |
| }; | |
| const updateData: any = {}; | |
| if (data.kode !== undefined) { | |
| const dup = await prisma.produk.findFirst({ | |
| where: { kode: data.kode, id: { not: id }, deleted_at: null }, | |
| }); | |
| if (dup) throw new APIError(`Kode produk '${data.kode}' sudah digunakan oleh produk lain`, 400); | |
| updateData.kode = data.kode; | |
| } | |
| if (data.barcode !== undefined) { | |
| if (data.barcode) { | |
| const dup = await prisma.produk.findFirst({ | |
| where: { barcode: data.barcode, id: { not: id }, deleted_at: null }, | |
| }); | |
| if (dup) throw new APIError(`Barcode '${data.barcode}' sudah digunakan oleh produk lain`, 400); | |
| } | |
| updateData.barcode = data.barcode; | |
| } | |
| if (data.nama !== undefined) updateData.nama = data.nama; | |
| if (data.kategori_id !== undefined) updateData.kategori_id = data.kategori_id; | |
| if (data.supplier_id !== undefined) updateData.supplier_id = data.supplier_id; | |
| if (data.satuan !== undefined) updateData.satuan = data.satuan; | |
| if (data.harga_beli !== undefined) updateData.harga_beli = data.harga_beli; | |
| if (data.harga_jual !== undefined) updateData.harga_jual = data.harga_jual; | |
| if (data.stok_min !== undefined) updateData.stok_min = data.stok_min; | |
| if (data.expired_date !== undefined) updateData.expired_date = data.expired_date; | |
| if (data.qr_url !== undefined) updateData.qr_url = data.qr_url; | |
| if (data.deskripsi !== undefined) updateData.deskripsi = data.deskripsi; | |
| if (data.is_ecer !== undefined) updateData.is_ecer = data.is_ecer; | |
| if (data.is_aktif !== undefined) updateData.is_aktif = data.is_aktif; | |
| // Handle foto_url change and delete old images from Supabase | |
| if (data.foto_url !== undefined && data.foto_url !== product.foto_url) { | |
| const oldFoto = product.foto_url; | |
| if (oldFoto) { | |
| let urls: string[] = []; | |
| try { | |
| const parsed = JSON.parse(oldFoto); | |
| if (Array.isArray(parsed)) { | |
| urls = parsed; | |
| } else { | |
| urls = [String(parsed)]; | |
| } | |
| } catch (e) { | |
| urls = [oldFoto]; | |
| } | |
| const searchStr = '/storage/v1/object/public/produk/'; | |
| for (const url of urls) { | |
| if (url.includes(searchStr)) { | |
| const filePath = url.split(searchStr).pop(); | |
| if (filePath) { | |
| await deleteFromSupabase('produk', filePath); | |
| } | |
| } | |
| } | |
| } | |
| updateData.foto_url = data.foto_url; | |
| } | |
| const updated = await prisma.produk.update({ | |
| where: { id }, | |
| data: updateData, | |
| }); | |
| // Update pricing levels if provided | |
| if (data.harga_tingkat !== undefined) { | |
| await prisma.harga_tingkat.deleteMany({ | |
| where: { produk_id: id }, | |
| }); | |
| if (data.harga_tingkat && data.harga_tingkat.length > 0) { | |
| for (const ht of data.harga_tingkat) { | |
| await prisma.harga_tingkat.create({ | |
| data: { | |
| produk_id: id, | |
| tingkat: ht.tingkat, | |
| min_qty: ht.min_qty, | |
| harga: ht.harga, | |
| }, | |
| }); | |
| } | |
| } | |
| } | |
| const dataBaru = { | |
| kode: updated.kode, | |
| barcode: updated.barcode, | |
| nama: updated.nama, | |
| kategori_id: updated.kategori_id, | |
| supplier_id: updated.supplier_id, | |
| satuan: updated.satuan, | |
| harga_beli: Number(updated.harga_beli), | |
| harga_jual: Number(updated.harga_jual), | |
| stok: Number(updated.stok), | |
| stok_min: Number(updated.stok_min), | |
| is_ecer: updated.is_ecer, | |
| is_aktif: updated.is_aktif, | |
| }; | |
| await catatAudit({ | |
| userId: actorId, | |
| aksi: 'update', | |
| entitas: 'produk', | |
| entitasId: updated.id, | |
| dataLama, | |
| dataBaru, | |
| ipAddress: ip, | |
| userAgent, | |
| }); | |
| return updated; | |
| } | |
| static async delete(id: number, actorId: number, ip: string, userAgent: string) { | |
| const product = await this.getProdukById(id); | |
| await moveToTrash({ | |
| instance: product, | |
| entityType: 'produk', | |
| actorId, | |
| }); | |
| await catatAudit({ | |
| userId: actorId, | |
| aksi: 'delete', | |
| entitas: 'produk', | |
| entitasId: product.id, | |
| dataLama: { kode: product.kode, nama: product.nama, deleted: false }, | |
| dataBaru: { kode: product.kode, nama: product.nama, deleted: true }, | |
| ipAddress: ip, | |
| userAgent, | |
| }); | |
| return true; | |
| } | |
| } | |