'use server' import prisma from '@/lib/prisma' import { revalidatePath } from 'next/cache' import { hashPassword, verifyPassword, encrypt, decrypt } from '@/lib/auth' import { cookies } from 'next/headers' // === AUTHENTICATION ACTIONS === export async function loginUser(username: string, password: string) { try { const user = await prisma.user.findUnique({ where: { username } }) if (!user) return { success: false, error: 'Username tidak ditemukan.' } if (!user.is_active) return { success: false, error: 'Akun Anda dinonaktifkan.' } const isMatch = await verifyPassword(password, user.password) if (!isMatch) return { success: false, error: 'Password salah.' } const sessionData = { id: user.id, username: user.username, name: user.name, role: user.role, photo: user.photo, } const sessionToken = await encrypt(sessionData) ;(await cookies()).set('session', sessionToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', path: '/', }) return { success: true, user: sessionData } } catch (e) { console.error('Login Error:', e) return { success: false, error: 'Terjadi kesalahan saat login.' } } } export async function registerUser(name: string, username: string, password: string) { try { const hashedPassword = await hashPassword(password) const user = await prisma.user.create({ data: { name, username, password: hashedPassword }, }) const sessionData = { id: user.id, username: user.username, name: user.name, role: user.role, photo: user.photo, } const sessionToken = await encrypt(sessionData) ;(await cookies()).set('session', sessionToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', path: '/', }) return { success: true, user: sessionData } } catch (error: any) { console.error('Register Error:', error) if (error.code === 'P2002') return { success: false, error: 'Username sudah digunakan.' } return { success: false, error: 'Terjadi kesalahan.' } } } export async function logoutUser() { ;(await cookies()).delete('session') return { success: true } } export async function getSessionUser() { const cookie = (await cookies()).get('session')?.value if (!cookie) return null return await decrypt(cookie) } export async function updateProfile(id: string, name: string, photo?: string | null) { try { const user = await prisma.user.update({ where: { id }, data: { name, photo: photo === undefined ? undefined : photo || null }, }) // Update session cookie with new data const sessionData = { id: user.id, username: user.username, name: user.name, role: user.role, photo: user.photo, } const sessionToken = await encrypt(sessionData) ;(await cookies()).set('session', sessionToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', path: '/', }) revalidatePath('/') revalidatePath('/profile') return { success: true, user: sessionData } } catch (error: any) { return { success: false, error: 'Gagal memperbarui profil.' } } } export async function changePassword(id: string, oldPass: string, newPass: string) { try { const user = await prisma.user.findUnique({ where: { id } }) if (!user) return { success: false, error: 'User tidak ditemukan.' } const isMatch = await verifyPassword(oldPass, user.password) if (!isMatch) return { success: false, error: 'Password lama salah.' } const hashed = await hashPassword(newPass) await prisma.user.update({ where: { id }, data: { password: hashed }, }) return { success: true } } catch (e) { return { success: false, error: 'Gagal mengganti password.' } } } // === USER MANAGEMENT (SUPERADMIN) === export async function getUsers(page = 1, limit = 10) { const skip = (page - 1) * limit const [users, total] = await Promise.all([ prisma.user.findMany({ skip, take: limit, orderBy: { created_at: 'desc' } }), prisma.user.count(), ]) return { users, total, totalPages: Math.ceil(total / limit) } } export async function createUserByAdmin( name: string, username: string, password: string, role: string ) { try { const hashedPassword = await hashPassword(password) await prisma.user.create({ data: { name, username, password: hashedPassword, role }, }) revalidatePath('/users') return { success: true } } catch (e: any) { if (e.code === 'P2002') return { success: false, error: 'Username sudah digunakan.' } return { success: false, error: 'Gagal membuat user.' } } } export async function toggleUserActive(id: string, is_active: boolean) { try { const user = await prisma.user.findUnique({ where: { id } }) if (user?.role === 'superadmin') return { success: false, error: 'Data Superadmin tidak bisa dinonaktifkan.' } await prisma.user.update({ where: { id }, data: { is_active }, }) revalidatePath('/users') return { success: true } } catch (e) { return { success: false, error: 'Gagal mengupdate status user.' } } } export async function deleteUser(id: string) { try { const user = await prisma.user.findUnique({ where: { id } }) if (user?.role === 'superadmin') return { success: false, error: 'Data Superadmin tidak bisa dihapus.' } await prisma.user.delete({ where: { id } }) revalidatePath('/users') return { success: true } } catch (e) { return { success: false, error: 'Gagal menghapus user.' } } } export async function resetUserPassword(id: string, newPass: string) { try { const hashed = await hashPassword(newPass) await prisma.user.update({ where: { id }, data: { password: hashed }, }) revalidatePath('/users') return { success: true } } catch (e) { return { success: false, error: 'Gagal mereset password.' } } } // === ORDER ACTIONS (DASHBOARD & CREATE) === export async function getAvailableOrders() { const startOfDay = new Date() startOfDay.setHours(0, 0, 0, 0) const endOfDay = new Date() endOfDay.setHours(23, 59, 59, 999) return await prisma.order.findMany({ where: { status: 'OPEN', date: { gte: startOfDay, lte: endOfDay }, }, include: { creator: true, available_items: true, submissions: { select: { user_id: true } }, }, orderBy: { date: 'desc' }, }) } export async function getMyOrders(creator_id: string) { return await prisma.order.findMany({ where: { creator_id }, include: { available_items: true, submissions: { select: { id: true, payment_status: true } }, }, orderBy: { date: 'desc' }, }) } export async function createOrder(data: { creator_id: string title: string description?: string date: Date allow_custom: boolean available_items: string[] }) { try { const order = await prisma.order.create({ data: { title: data.title, description: data.description || null, date: data.date, allow_custom: data.allow_custom, creator_id: data.creator_id, status: 'DRAFT', available_items: { create: data.available_items.map((name) => ({ name })), }, }, }) revalidatePath('/my-orders') return { success: true, order } } catch (error) { console.error('Create Order Error:', error) return { success: false, error: 'Gagal membuat order.' } } } export async function updateOrder( order_id: string, data: { title: string description?: string allow_custom: boolean available_items: { name: string; is_sold_out: boolean }[] } ) { try { const order = await prisma.order.findUnique({ where: { id: order_id }, }) if (!order) return { success: false, error: 'Order tidak ditemukan' } if (order.status === 'CLOSE') { return { success: false, error: 'Order dengan status CLOSE tidak dapat diedit.' } } // Check if order date is today const orderDate = new Date(order.date) const today = new Date() if ( orderDate.getDate() !== today.getDate() || orderDate.getMonth() !== today.getMonth() || orderDate.getFullYear() !== today.getFullYear() ) { return { success: false, error: 'Hanya pesanan hari ini yang dapat diedit.' } } // Delete existing available items and insert updated ones await prisma.availableItem.deleteMany({ where: { order_id }, }) const updatedOrder = await prisma.order.update({ where: { id: order_id }, data: { title: data.title, description: data.description || null, allow_custom: data.allow_custom, available_items: { create: data.available_items.map((item) => ({ name: item.name, is_sold_out: item.is_sold_out, })), }, }, }) revalidatePath('/my-orders') revalidatePath(`/my-orders/${order_id}`) revalidatePath('/') return { success: true, order: updatedOrder } } catch (error: any) { console.error('Update Order Error:', error) return { success: false, error: 'Gagal memperbarui order: ' + error?.message } } } export async function updateOrderStatus(order_id: string, status: string) { try { await prisma.order.update({ where: { id: order_id }, data: { status }, }) revalidatePath('/my-orders') revalidatePath('/') return { success: true } } catch (e) { return { success: false, error: 'Gagal mengubah status.' } } } export async function deleteOrder(order_id: string) { try { const order = await prisma.order.findUnique({ where: { id: order_id }, }) if (!order) return { success: false, error: 'Order tidak ditemukan.' } if (order.status === 'OPEN') { return { success: false, error: 'Order berstatus OPEN tidak dapat dihapus. Silakan tutup (CLOSE) terlebih dahulu.', } } await prisma.order.delete({ where: { id: order_id }, }) revalidatePath('/my-orders') revalidatePath('/') return { success: true } } catch (e) { return { success: false, error: 'Gagal menghapus order.' } } } // === ORDER DETAIL === export async function getOrderDetail(order_id: string) { return await prisma.order.findUnique({ where: { id: order_id }, include: { creator: true, available_items: true, submissions: { include: { user: true, items: true, }, }, }, }) } export async function updateSubmissionPayment( submission_id: string, bill: number | null, payment_status: string, paid_amount: number | null = null, saldo_used: number = 0 ) { try { await prisma.submission.update({ where: { id: submission_id }, data: { bill, payment_status, paid_amount, saldo_used }, }) revalidatePath(`/my-orders`) revalidatePath(`/my-purchases`) revalidatePath(`/reports`) revalidatePath(`/balances`) return { success: true } } catch (e) { console.error(e) return { success: false, error: 'Gagal menyimpan tagihan.' } } } export async function processBulkPayment(data: { submission_ids: string[] cash_amount: number use_balance: boolean creator_id: string user_id: string }) { try { const me = await getSessionUser() if (!me || me.id !== data.creator_id) { return { success: false, error: 'Unauthorized' } } let currentBalance = 0 if (data.use_balance) { const balanceData = await getBalancesAsCreator(data.creator_id) const userBalance = balanceData.find((b) => b.user.id === data.user_id) if (userBalance) currentBalance = userBalance.amount } const submissions = await prisma.submission.findMany({ where: { id: { in: data.submission_ids } }, include: { order: { select: { date: true, status: true } } }, }) // Sort by order date ascending (oldest first) submissions.sort((a, b) => new Date(a.order.date).getTime() - new Date(b.order.date).getTime()) let remainingBalance = currentBalance let remainingCash = data.cash_amount let totalAvailable = remainingBalance + remainingCash const ops = [] for (let i = 0; i < submissions.length; i++) { const sub = submissions[i] const isLast = i === submissions.length - 1 // Don't process if order isn't CLOSE if (sub.order.status !== 'CLOSE') continue const subBill = sub.bill || 0 const prevPaid = sub.paid_amount || 0 const prevSaldoUsed = sub.saldo_used || 0 const amountToCover = Math.max(0, subBill - prevPaid - prevSaldoUsed) if (amountToCover === 0) { // It's already fully paid somehow, just mark LUNAS. Give excess cash if last. let finalPaid = prevPaid if (isLast && remainingCash > 0) { finalPaid += remainingCash remainingCash = 0 } ops.push( prisma.submission.update({ where: { id: sub.id }, data: { payment_status: 'LUNAS', paid_amount: finalPaid }, }) ) continue } if (totalAvailable >= amountToCover) { // Fully covered -> LUNAS let balanceToUse = Math.min(remainingBalance, amountToCover) remainingBalance -= balanceToUse let cashToUse = amountToCover - balanceToUse remainingCash -= cashToUse totalAvailable -= amountToCover let finalPaid = prevPaid + cashToUse if (isLast && remainingCash > 0) { finalPaid += remainingCash remainingCash = 0 } let finalSaldoUsed = prevSaldoUsed + balanceToUse ops.push( prisma.submission.update({ where: { id: sub.id }, data: { payment_status: 'LUNAS', paid_amount: finalPaid, saldo_used: finalSaldoUsed }, }) ) } else if (totalAvailable > 0) { // Partially covered -> BELUM_BAYAR let balanceToUse = Math.min(remainingBalance, totalAvailable) remainingBalance -= balanceToUse let cashToUse = totalAvailable - balanceToUse remainingCash -= cashToUse let finalPaid = prevPaid + cashToUse let finalSaldoUsed = prevSaldoUsed + balanceToUse totalAvailable = 0 ops.push( prisma.submission.update({ where: { id: sub.id }, data: { payment_status: 'BELUM_BAYAR', paid_amount: finalPaid, saldo_used: finalSaldoUsed, }, }) ) } } await prisma.$transaction(ops) revalidatePath(`/my-orders`) revalidatePath(`/my-purchases`) revalidatePath(`/reports`) revalidatePath(`/balances`) return { success: true } } catch (e) { console.error(e) return { success: false, error: 'Gagal memproses pembayaran massal cerdas.' } } } // === SUBMISSION (PESANAN SAYA) === export async function getUserSubmission(order_id: string, user_id: string) { return await prisma.submission.findFirst({ where: { order_id, user_id }, include: { items: true }, }) } export async function getMyPurchases(user_id: string) { return await prisma.submission.findMany({ where: { user_id }, include: { order: { include: { creator: true, available_items: true, }, }, items: true, }, orderBy: { order: { date: 'desc' }, }, }) } export async function submitOrder(data: { order_id: string user_id: string items: Array<{ name: string; qty: number; is_custom: boolean; note?: string }> }) { try { if (data.items.length === 0) { return { success: false, error: 'Pilih minimal 1 item.' } } const order = await prisma.order.findUnique({ where: { id: data.order_id } }) if (!order) return { success: false, error: 'Pesanan tidak ditemukan.' } if (order.status !== 'OPEN') return { success: false, error: 'Pesanan sudah ditutup atau masih draft.' } const itemsToCreate = data.items.map((item) => ({ name: item.name, qty: item.qty, is_custom: item.is_custom, note: item.note || null, })) const existing = await prisma.submission.findFirst({ where: { order_id: data.order_id, user_id: data.user_id }, }) if (existing) { await prisma.submissionItem.deleteMany({ where: { submission_id: existing.id } }) await prisma.submission.update({ where: { id: existing.id }, data: { items: { create: itemsToCreate, }, }, }) } else { await prisma.submission.create({ data: { order_id: data.order_id, user_id: data.user_id, items: { create: itemsToCreate, }, }, }) } revalidatePath('/') revalidatePath('/my-purchases') revalidatePath(`/my-orders/${data.order_id}`) return { success: true } } catch (error) { console.error(error) return { success: false, error: 'Gagal mengirim pesanan.' } } } // === REPORT ACTIONS === export async function getSubmittorReport(user_id: string, startDate?: Date, endDate?: Date) { return await prisma.submission.findMany({ where: { user_id, order: { status: 'CLOSE', ...(startDate && endDate ? { date: { gte: startDate, lte: endDate } } : {}), }, }, include: { order: { include: { creator: true }, }, items: true, }, orderBy: { order: { date: 'desc' }, }, }) } export async function getCreatorReport(creator_id: string, startDate?: Date, endDate?: Date) { return await prisma.order.findMany({ where: { creator_id, status: 'CLOSE', ...(startDate && endDate ? { date: { gte: startDate, lte: endDate } } : {}), }, include: { submissions: { include: { user: true, items: true, }, }, }, orderBy: { date: 'desc' }, }) } // === BALANCE ACTIONS === export async function getBalancesAsCreator(creator_id: string) { const [submissions, withdrawals] = await Promise.all([ prisma.submission.findMany({ where: { order: { creator_id } }, include: { user: { select: { id: true, name: true, photo: true } } }, }), prisma.balanceWithdrawal.findMany({ where: { creator_id }, include: { user: { select: { id: true, name: true, photo: true } } }, }), ]) const balanceMap = new Map() submissions.forEach((sub) => { let diff = 0 if (sub.payment_status === 'LUNAS') { if (sub.bill != null) { diff = (sub.paid_amount || 0) - sub.bill } } else { diff = -(sub.saldo_used || 0) } if (diff !== 0) { if (!balanceMap.has(sub.user.id)) { balanceMap.set(sub.user.id, { user: sub.user, amount: diff }) } else { balanceMap.get(sub.user.id)!.amount += diff } } }) // Kurangi saldo dengan total withdrawal yang sudah dilakukan withdrawals.forEach((w) => { if (!balanceMap.has(w.user_id)) { balanceMap.set(w.user_id, { user: w.user, amount: -w.amount }) } else { balanceMap.get(w.user_id)!.amount -= w.amount } }) return Array.from(balanceMap.values()).filter((b) => b.amount !== 0) } export async function getBalancesAsSubmittor(user_id: string) { const [submissions, withdrawals] = await Promise.all([ prisma.submission.findMany({ where: { user_id }, include: { order: { include: { creator: { select: { id: true, name: true, photo: true } }, }, }, }, }), prisma.balanceWithdrawal.findMany({ where: { user_id }, include: { creator: { select: { id: true, name: true, photo: true } } }, }), ]) const balanceMap = new Map() submissions.forEach((sub) => { let diff = 0 if (sub.payment_status === 'LUNAS') { if (sub.bill != null) { diff = (sub.paid_amount || 0) - sub.bill } } else { diff = -(sub.saldo_used || 0) } if (diff !== 0) { const creator = sub.order.creator if (!balanceMap.has(creator.id)) { balanceMap.set(creator.id, { creator: creator, amount: diff }) } else { balanceMap.get(creator.id)!.amount += diff } } }) // Kurangi saldo dengan total withdrawal yang dilakukan kreator withdrawals.forEach((w) => { if (!balanceMap.has(w.creator_id)) { balanceMap.set(w.creator_id, { creator: w.creator, amount: -w.amount }) } else { balanceMap.get(w.creator_id)!.amount -= w.amount } }) return Array.from(balanceMap.values()).filter((b) => b.amount !== 0) } // === WITHDRAWAL ACTIONS === export async function createWithdrawal(data: { creator_id: string user_id: string amount: number note?: string }) { try { const me = await getSessionUser() if (!me || me.id !== data.creator_id) { return { success: false, error: 'Unauthorized' } } if (!data.amount || data.amount <= 0) { return { success: false, error: 'Nominal harus lebih dari 0' } } // Hitung saldo tersedia saat ini untuk user ini const balances = await getBalancesAsCreator(data.creator_id) const userBalance = balances.find((b) => b.user.id === data.user_id) const available = userBalance ? userBalance.amount : 0 if (data.amount > available) { return { success: false, error: `Nominal melebihi saldo tersedia (${new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', maximumFractionDigits: 0 }).format(available)})`, } } await prisma.balanceWithdrawal.create({ data: { creator_id: data.creator_id, user_id: data.user_id, amount: data.amount, note: data.note || null, }, }) revalidatePath('/balances') revalidatePath('/my-orders') revalidatePath('/reports') return { success: true } } catch (e) { console.error(e) return { success: false, error: 'Gagal mencatat penarikan saldo.' } } } export async function getWithdrawalHistory(creator_id: string, user_id?: string) { return await prisma.balanceWithdrawal.findMany({ where: { creator_id, ...(user_id ? { user_id } : {}), }, include: { user: { select: { id: true, name: true, photo: true } }, }, orderBy: { created_at: 'desc' }, }) } export async function deleteWithdrawal(id: string) { try { const me = await getSessionUser() const record = await prisma.balanceWithdrawal.findUnique({ where: { id } }) if (!record) return { success: false, error: 'Data tidak ditemukan' } if (!me || me.id !== record.creator_id) { return { success: false, error: 'Unauthorized' } } await prisma.balanceWithdrawal.delete({ where: { id } }) revalidatePath('/balances') revalidatePath('/my-orders') revalidatePath('/reports') return { success: true } } catch (e) { console.error(e) return { success: false, error: 'Gagal menghapus riwayat penarikan.' } } } export async function deleteSubmission(submission_id: string, user_id: string) { try { const submission = await prisma.submission.findUnique({ where: { id: submission_id }, include: { order: true }, }) if (!submission) return { success: false, error: 'Pesanan tidak ditemukan' } if (submission.user_id !== user_id) return { success: false, error: 'Tidak ada akses' } if (submission.order.status !== 'OPEN') return { success: false, error: 'Hanya bisa membatalkan PO yang OPEN' } await prisma.submission.delete({ where: { id: submission_id }, }) return { success: true } } catch (error) { return { success: false, error: 'Gagal membatalkan pesanan' } } } // --- SETTINGS ACTIONS --- export async function getSettings() { const settings = await prisma.setting.findMany() const obj: Record = {} settings.forEach((s) => { obj[s.key] = s.value }) return obj } export async function updateSettings(settings: Record) { try { for (const key of Object.keys(settings)) { await prisma.setting.upsert({ where: { key }, update: { value: settings[key] }, create: { key, value: settings[key] }, }) } return { success: true } } catch (error) { console.error(error) return { success: false, error: 'Gagal update settings' } } } // --- BROADCAST ACTIONS --- export async function broadcastToMattermost(orderId: string, orderUrl: string) { try { const order = await prisma.order.findUnique({ where: { id: orderId }, }) if (!order) return { success: false, error: 'Order not found' } const settings = await getSettings() const webhookUrl = settings.MATTERMOST_WEBHOOK_URL let template = settings.MATTERMOST_TEMPLATE || 'Halo tim! Lagi ada orderan open nih untuk {title}. Mumpung belum di-checkout, yang mau ikutan nitip bisa langsung cek ke sini ya: {url}' if (!webhookUrl) return { success: false, error: 'Webhook URL belum di-setting' } const message = template.replace('{title}', order.title).replace('{url}', orderUrl) const res = await fetch(webhookUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: message }), }) if (!res.ok) { throw new Error(`Mattermost API Error: ${res.statusText}`) } return { success: true } } catch (error) { console.error(error) return { success: false, error: 'Gagal mengirim ke Mattermost' } } } export async function getPiutangSummary(creator_id: string) { const unpaidSubmissions = await prisma.submission.findMany({ where: { order: { creator_id, status: 'CLOSE', }, payment_status: 'BELUM_BAYAR', }, include: { user: { select: { id: true, name: true, photo: true }, }, order: { select: { id: true, title: true, date: true }, }, }, orderBy: { order: { date: 'asc' }, }, }) const grouped = new Map() unpaidSubmissions.forEach((sub) => { if (!grouped.has(sub.user_id)) { grouped.set(sub.user_id, { user: sub.user, totalPiutang: 0, submissions: [], }) } const u = grouped.get(sub.user_id) const paidAmount = (sub.paid_amount || 0) + (sub.saldo_used || 0) const unpaid = Math.max(0, (sub.bill || 0) - paidAmount) if (unpaid > 0) { u.totalPiutang += unpaid u.submissions.push({ ...sub, unpaidAmount: unpaid, }) } }) return Array.from(grouped.values()).filter((g) => g.totalPiutang > 0) }