diff --git a/apps/backend/src/trpc/pharmanager/v1/billing.ts b/apps/backend/src/trpc/pharmanager/v1/billing.ts index 097b085..01b3bda 100644 --- a/apps/backend/src/trpc/pharmanager/v1/billing.ts +++ b/apps/backend/src/trpc/pharmanager/v1/billing.ts @@ -8,7 +8,7 @@ export const BillItemSchema = z.object({ product_name: z.string(), brand: z.string().nullable(), batch_id: z.number().int().nullable(), - strips: z.number().int(), + packs: z.number().int(), loose: z.number().int(), qty: z.number().int(), original_price: z.number(), @@ -37,7 +37,7 @@ const BillItemInputSchema = z.object({ product_name: z.string(), brand: z.string().nullable().optional(), batch_id: z.number().int().nullable().optional(), - strips: z.number().int(), + packs: z.number().int(), loose: z.number().int(), qty: z.number().int(), original_price: z.number(), diff --git a/apps/pharmanager/src/routes/billing/$id.tsx b/apps/pharmanager/src/routes/billing/$id.tsx index 2da352e..f43a957 100644 --- a/apps/pharmanager/src/routes/billing/$id.tsx +++ b/apps/pharmanager/src/routes/billing/$id.tsx @@ -67,8 +67,7 @@ function BillDetailsPage() { Payment Summary
- Subtotal₹{bill.subtotal.toFixed(2)} - Tax ({bill.tax_rate}%)₹{bill.tax.toFixed(2)} + Amount₹{bill.subtotal.toFixed(2)} Discount{bill.discount_percent}% Total ₹{bill.total.toFixed(2)} @@ -85,7 +84,7 @@ function BillDetailsPage() { Product - Strips + Packs Loose Price Total @@ -98,7 +97,7 @@ function BillDetailsPage() {
{item.product_name}
{item.brand &&
{item.brand}
} - {item.strips} + {item.packs} {item.loose} {item.original_price !== item.selling_price ? ( diff --git a/apps/pharmanager/src/routes/billing/index.tsx b/apps/pharmanager/src/routes/billing/index.tsx index 08b9b16..6de720b 100644 --- a/apps/pharmanager/src/routes/billing/index.tsx +++ b/apps/pharmanager/src/routes/billing/index.tsx @@ -25,7 +25,7 @@ interface BillItemForm { product_name: string; brand: string; batch_id: number | null; - strips: number; + packs: number; loose: number; qty: number; original_price: number; @@ -110,12 +110,12 @@ function BillingPage() { product_name: product.name, brand: product.brand, batch_id: defaultBatch?.id ?? null, - strips: product.units_per_strip ? 1 : 0, - loose: 0, - qty: product.units_per_strip ? product.units_per_strip * 1 : 1, - original_price: product.selling_price, - selling_price: product.selling_price, - total: product.units_per_strip ? product.units_per_strip * 1 * product.selling_price : product.selling_price, + packs: 1, + loose: 0, + qty: product.loose_sale_allowed ? (product.units_per_pack ?? 1) : 1, + original_price: product.selling_price, + selling_price: product.selling_price, + total: product.selling_price, batchOptions: productBatches.map((b) => ({ id: b.id, batch_no: b.batch_no, @@ -132,7 +132,7 @@ function BillingPage() { product_name: "", brand: "", batch_id: null, - strips: 0, + packs: 0, loose: 0, qty: 0, original_price: 0, @@ -153,24 +153,27 @@ function BillingPage() { prev.map((item, i) => { if (i !== idx) return item; - let updated = { ...item, [field]: value }; + let updated = { ...item, [field]: value }; + const product = products?.find((p) => p.id === item.product_id); + const ups = product?.units_per_pack ?? 0; + const looseAllowed = product?.loose_sale_allowed ?? false; + const isQuantityField = field === "batch_id" || field === "selling_price" || field === "packs" || field === "loose"; + + if (isQuantityField) { if (field === "batch_id") { const batch = item.batchOptions.find((b) => b.id === Number(value)); - if (batch) updated.qty = updated.strips * (products?.find((p) => p.id === item.product_id)?.units_per_strip ?? 1) + updated.loose; + if (!batch) return updated; } - if (field === "selling_price") { - const ups = products?.find((p) => p.id === item.product_id)?.units_per_strip ?? 1; - updated.qty = updated.strips * ups + updated.loose; - updated.total = Number(updated.qty) * Number(updated.selling_price); - } - - if (field === "strips" || field === "loose") { - const ups = products?.find((p) => p.id === item.product_id)?.units_per_strip ?? 1; - updated.qty = Number(updated.strips) * ups + Number(updated.loose); - updated.total = Number(updated.qty) * Number(updated.selling_price); + if (looseAllowed) { + updated.qty = Number(updated.packs) * ups + Number(updated.loose); + updated.total = Number(updated.packs) * Number(updated.selling_price) + Number(updated.loose) * (Number(updated.selling_price) / ups); + } else { + updated.qty = Number(updated.packs); + updated.total = Number(updated.packs) * Number(updated.selling_price); } + } return updated; }), @@ -183,21 +186,22 @@ function BillingPage() { setItems((prev) => prev.map((item, i) => { if (i !== idx) return item; - const ups = product.units_per_strip ?? 1; - const strips = product.units_per_strip ? 1 : 0; - const qty = strips * ups; - return { - ...item, - product_id: product.id, - product_name: product.name, - brand: product.brand, - batch_id: defaultBatch?.id ?? null, - strips, - loose: 0, - qty, - original_price: product.selling_price, - selling_price: product.selling_price, - total: qty * product.selling_price, + const packs = 1; + const loose = 0; + const ups = product.units_per_pack ?? 0; + const qty = product.loose_sale_allowed ? ups : 1; + return { + ...item, + product_id: product.id, + product_name: product.name, + brand: product.brand, + batch_id: defaultBatch?.id ?? null, + packs, + loose, + qty, + original_price: product.selling_price, + selling_price: product.selling_price, + total: product.loose_sale_allowed ? ups * product.selling_price : product.selling_price, batchOptions: productBatches.map((b) => ({ id: b.id, batch_no: b.batch_no, @@ -210,8 +214,7 @@ function BillingPage() { } const subtotal = items.reduce((s, i) => s + i.total, 0); - const tax = subtotal * 0.18; - const total = subtotal + tax; + const total = subtotal; function handleGenerateBill() { if (!customerMobile) return; @@ -224,8 +227,8 @@ function BillingPage() { customer_mobile: customerMobile, customer_name: customerName || null, subtotal, - tax, - tax_rate: 18, + tax: 0, + tax_rate: 0, total, discount: 0, discount_percent: 0, @@ -235,7 +238,7 @@ function BillingPage() { product_name: i.product_name, brand: i.brand || null, batch_id: i.batch_id, - strips: i.strips, + packs: i.packs, loose: i.loose, qty: i.qty, original_price: i.original_price, @@ -344,8 +347,8 @@ function BillingPage() { Product Batch - Strips - Loose + Packs + Loose Price Total @@ -391,25 +394,25 @@ function BillingPage() { )} - - updateItem(idx, "strips", Number(e.target.value))} - className="w-full px-2 py-1.5 border border-slate-200 rounded text-xs text-center" - min={0} - disabled={!products?.find((p) => p.id === item.product_id)?.units_per_strip} - /> - - - updateItem(idx, "loose", Number(e.target.value))} - className="w-full px-2 py-1.5 border border-slate-200 rounded text-xs text-center" - min={0} - /> - + + updateItem(idx, "packs", Number(e.target.value))} + className="w-full px-2 py-1.5 border border-slate-200 rounded text-xs text-center" + min={0} + /> + + + updateItem(idx, "loose", Number(e.target.value))} + className="w-full px-2 py-1.5 border border-slate-200 rounded text-xs text-center" + min={0} + disabled={!products?.find((p) => p.id === item.product_id)?.loose_sale_allowed} + /> +
-
- Subtotal - ₹{subtotal.toFixed(2)} -
-
- Tax (18% GST) - ₹{tax.toFixed(2)} -
Total ₹{total.toFixed(2)} diff --git a/apps/pharmanager/src/routes/products/$id.tsx b/apps/pharmanager/src/routes/products/$id.tsx index d3b0e4d..323e6c5 100644 --- a/apps/pharmanager/src/routes/products/$id.tsx +++ b/apps/pharmanager/src/routes/products/$id.tsx @@ -73,7 +73,7 @@ function ProductDetailsPage() { {product.units_per_strip && ( - + )}
@@ -84,6 +84,7 @@ function ProductDetailsPage() { Inventory
+
diff --git a/apps/pharmanager/src/routes/products/add.tsx b/apps/pharmanager/src/routes/products/add.tsx index ed98d44..cd95eb0 100644 --- a/apps/pharmanager/src/routes/products/add.tsx +++ b/apps/pharmanager/src/routes/products/add.tsx @@ -27,7 +27,8 @@ const formSchema = CreateProductInput.extend({ selling_price: z.coerce.number().min(0, "Must be ≥ 0"), size: z.coerce.number().int().default(0), reorder_level: z.coerce.number().int().default(0), - units_per_strip: z.coerce.number().int().nullable().optional(), + units_per_pack: z.coerce.number().int().nullable().optional(), + loose_sale_allowed: z.coerce.boolean().default(false), }); type FormValues = z.infer; @@ -87,7 +88,8 @@ function AddProductPage() { selling_price: 0, size: 0, reorder_level: 0, - units_per_strip: null, + units_per_pack: null, + loose_sale_allowed: false, hide_product_from_public: false, hide_price_from_public: false, compositions: [{ drug_name: "", quantity: "", unit_name: "" }], @@ -99,9 +101,15 @@ function AddProductPage() { name: "compositions", }); - const category = watch("category"); const hideProduct = watch("hide_product_from_public"); - const showUnitsPerStrip = category === "Tablets" || category === "Capsules"; + const looseSaleAllowed = watch("loose_sale_allowed"); + const showUnitsPerPack = looseSaleAllowed; + + useEffect(() => { + if (!looseSaleAllowed) { + setValue("units_per_pack", null); + } + }, [looseSaleAllowed, setValue]); useEffect(() => { if (hideProduct) { @@ -122,7 +130,8 @@ function AddProductPage() { selling_price: existingProduct.selling_price, size: existingProduct.size, reorder_level: existingProduct.reorder_level, - units_per_strip: existingProduct.units_per_strip, + units_per_pack: existingProduct.units_per_pack, + loose_sale_allowed: existingProduct.loose_sale_allowed, hide_product_from_public: existingProduct.hide_product_from_public, hide_price_from_public: existingProduct.hide_price_from_public, compositions: existingProduct.compositions.map((c) => ({ @@ -212,12 +221,19 @@ function AddProductPage() { /> - {showUnitsPerStrip && ( +
+ +
+ + {showUnitsPerPack && (
- +
)} @@ -240,7 +256,7 @@ function AddProductPage() { {errors.unit_name &&

{errors.unit_name.message}

} -
+