Files
titipin/src/app/actions.ts
T

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)
}