974 lines
26 KiB
TypeScript
974 lines
26 KiB
TypeScript
'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<string, { user: any; amount: number }>()
|
|
|
|
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<string, { creator: any; amount: number }>()
|
|
|
|
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<string, string> = {}
|
|
settings.forEach((s) => {
|
|
obj[s.key] = s.value
|
|
})
|
|
return obj
|
|
}
|
|
|
|
export async function updateSettings(settings: Record<string, string>) {
|
|
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<string, any>()
|
|
|
|
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)
|
|
}
|