281 lines
8.8 KiB
JavaScript
281 lines
8.8 KiB
JavaScript
// server/controllers/salesController.js
|
|
import supabase from "../config/supabaseClient.js";
|
|
|
|
// server/controllers/salesController.js
|
|
export const getSales = async (req, res) => {
|
|
try {
|
|
const { page = 1, limit = 10 } = req.query;
|
|
const pageNum = parseInt(page);
|
|
const limitNum = parseInt(limit);
|
|
const offset = (pageNum - 1) * limitNum;
|
|
|
|
// Get total count
|
|
const { count, error: countError } = await supabase
|
|
.from('sales')
|
|
.select('*', { count: 'exact', head: true });
|
|
|
|
if (countError) throw countError;
|
|
|
|
// Get paginated sales
|
|
const { data: sales, error } = await supabase
|
|
.from('sales')
|
|
.select(`
|
|
*,
|
|
users(full_name),
|
|
sale_items(
|
|
*,
|
|
products(name, price)
|
|
)
|
|
`)
|
|
.order('sale_date', { ascending: false })
|
|
.range(offset, offset + limitNum - 1);
|
|
|
|
if (error) throw error;
|
|
|
|
res.json({
|
|
sales,
|
|
pagination: {
|
|
currentPage: pageNum,
|
|
totalPages: Math.ceil(count / limitNum),
|
|
totalSales: count,
|
|
hasNext: pageNum < Math.ceil(count / limitNum),
|
|
hasPrev: pageNum > 1
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Error fetching sales:', error);
|
|
res.status(500).json({ error: 'Failed to fetch sales' });
|
|
}
|
|
};
|
|
|
|
export const createSale = async (req, res) => {
|
|
const {
|
|
user_id,
|
|
items,
|
|
payment_type,
|
|
amount_tendered,
|
|
discount_type,
|
|
discount_amount,
|
|
customer_name
|
|
} = req.body;
|
|
|
|
try {
|
|
// Calculate total amount from items
|
|
const subtotal = items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
|
|
|
|
// Calculate discount
|
|
const discount = discount_amount || 0;
|
|
|
|
// Calculate tax (no tax if discount is applied)
|
|
const taxable_amount = subtotal - discount;
|
|
const tax = (discount > 0) ? 0 : taxable_amount * 0.12;
|
|
|
|
const total_amount = subtotal - discount + tax;
|
|
|
|
// For GCash and Card, amount_tendered should equal total_amount (no change)
|
|
let change_due = 0;
|
|
let final_amount_tendered = amount_tendered;
|
|
|
|
if (payment_type === 'cash') {
|
|
change_due = amount_tendered - total_amount;
|
|
if (change_due < 0) {
|
|
return res.status(400).json({ error: 'Insufficient payment amount' });
|
|
}
|
|
} else {
|
|
// For GCash and Card, payment should be exact
|
|
final_amount_tendered = total_amount;
|
|
change_due = 0;
|
|
}
|
|
|
|
// Create sale record
|
|
const { data: sale, error: saleError } = await supabase
|
|
.from('sales')
|
|
.insert([
|
|
{
|
|
user_id,
|
|
total_amount,
|
|
payment_type,
|
|
amount_tendered: final_amount_tendered,
|
|
change_due,
|
|
discount_type,
|
|
discount_amount: discount,
|
|
tax_amount: tax,
|
|
customer_name: customer_name || null, // Add customer name
|
|
status: 'completed'
|
|
}
|
|
])
|
|
.select()
|
|
.single();
|
|
|
|
if (saleError) throw saleError;
|
|
|
|
// Insert sale items
|
|
const saleItems = items.map(item => ({
|
|
sale_id: sale.sale_id,
|
|
product_id: item.product_id,
|
|
quantity: item.quantity,
|
|
price: item.price
|
|
}));
|
|
|
|
const { error: itemsError } = await supabase
|
|
.from('sale_items')
|
|
.insert(saleItems);
|
|
|
|
if (itemsError) throw itemsError;
|
|
|
|
// Update inventory and stock together
|
|
for (const item of items) {
|
|
// Fetch current stock
|
|
const { data: product, error: fetchError } = await supabase
|
|
.from('products')
|
|
.select('stock')
|
|
.eq('product_id', item.product_id)
|
|
.single();
|
|
if (fetchError) throw fetchError;
|
|
|
|
if (product.stock < item.quantity) {
|
|
return res.status(400).json({ error: `Insufficient stock for product ${item.product_id}` });
|
|
}
|
|
|
|
// Update stock
|
|
const { error: updateError } = await supabase
|
|
.from('products')
|
|
.update({ stock: product.stock - item.quantity })
|
|
.eq('product_id', item.product_id);
|
|
if (updateError) throw updateError;
|
|
|
|
// Insert inventory log
|
|
const { error: inventoryError } = await supabase
|
|
.from('inventory_logs')
|
|
.insert([
|
|
{
|
|
product_id: item.product_id,
|
|
user_id: user_id,
|
|
action: 'sale',
|
|
quantity: -item.quantity,
|
|
remarks: `Sale #${sale.sale_id}`
|
|
}
|
|
]);
|
|
if (inventoryError) throw inventoryError;
|
|
}
|
|
|
|
res.status(201).json(sale);
|
|
} catch (error) {
|
|
console.error('Error creating sale:', error);
|
|
res.status(500).json({ error: 'Failed to create sale' });
|
|
}
|
|
};
|
|
|
|
export const voidSale = async (req, res) => {
|
|
const { sale_id } = req.params;
|
|
|
|
try {
|
|
// Get sale details with items
|
|
const { data: sale, error: saleError } = await supabase
|
|
.from('sales')
|
|
.select(`
|
|
*,
|
|
sale_items(
|
|
*,
|
|
products(name)
|
|
)
|
|
`)
|
|
.eq('sale_id', sale_id)
|
|
.single();
|
|
|
|
if (saleError) throw saleError;
|
|
|
|
// Check if sale can be voided
|
|
if (sale.status !== 'completed') {
|
|
return res.status(400).json({ error: 'Only completed sales can be voided' });
|
|
}
|
|
|
|
// Start transaction - update sale status and restore inventory
|
|
const { error: updateError } = await supabase
|
|
.from('sales')
|
|
.update({ status: 'voided' })
|
|
.eq('sale_id', sale_id);
|
|
|
|
if (updateError) throw updateError;
|
|
|
|
// Restore inventory for each item
|
|
for (const item of sale.sale_items) {
|
|
const { error: inventoryError } = await supabase
|
|
.from('inventory_logs')
|
|
.insert([
|
|
{
|
|
product_id: item.product_id,
|
|
user_id: req.user?.user_id, // from auth middleware
|
|
action: 'adjust',
|
|
quantity: item.quantity,
|
|
remarks: `Voided sale #${sale_id} - ${item.products.name}`
|
|
}
|
|
]);
|
|
|
|
if (inventoryError) throw inventoryError;
|
|
}
|
|
|
|
res.json({ message: 'Sale voided successfully', sale_id });
|
|
|
|
} catch (error) {
|
|
console.error('Error voiding sale:', error);
|
|
res.status(500).json({ error: 'Failed to void sale' });
|
|
}
|
|
};
|
|
|
|
export const refundSale = async (req, res) => {
|
|
const { sale_id } = req.params;
|
|
|
|
try {
|
|
// Get sale details with items
|
|
const { data: sale, error: saleError } = await supabase
|
|
.from('sales')
|
|
.select(`
|
|
*,
|
|
sale_items(
|
|
*,
|
|
products(name)
|
|
)
|
|
`)
|
|
.eq('sale_id', sale_id)
|
|
.single();
|
|
|
|
if (saleError) throw saleError;
|
|
|
|
// Check if sale can be refunded
|
|
if (sale.status !== 'completed') {
|
|
return res.status(400).json({ error: 'Only completed sales can be refunded' });
|
|
}
|
|
|
|
// Update sale status
|
|
const { error: updateError } = await supabase
|
|
.from('sales')
|
|
.update({ status: 'refunded' })
|
|
.eq('sale_id', sale_id);
|
|
|
|
if (updateError) throw updateError;
|
|
|
|
// Restore inventory for each item
|
|
for (const item of sale.sale_items) {
|
|
const { error: inventoryError } = await supabase
|
|
.from('inventory_logs')
|
|
.insert([
|
|
{
|
|
product_id: item.product_id,
|
|
user_id: req.user?.user_id, // from auth middleware
|
|
action: 'adjust',
|
|
quantity: item.quantity,
|
|
remarks: `Refunded sale #${sale_id} - ${item.products.name}`
|
|
}
|
|
]);
|
|
|
|
if (inventoryError) throw inventoryError;
|
|
}
|
|
|
|
res.json({ message: 'Sale refunded successfully', sale_id });
|
|
|
|
} catch (error) {
|
|
console.error('Error refunding sale:', error);
|
|
res.status(500).json({ error: 'Failed to refund sale' });
|
|
}
|
|
}; |