tender controller -- Fixes mailing issue
Mon Jun 23 2025 10:09:17 GMT+0000 (Coordinated Universal Time)
Saved by @SrijanVerma
import Tender from "../models/tenderSchema.js"; import Quotation from "../models/quotationSchema.js"; import { generateSignedUrl } from "../utils/minioClient.js"; import mongoose from "mongoose"; import User from "../models/userSchema.js"; // Replace with your actual user model path import { sendMail } from "../utils/sendMail.js"; // You must have this utility created import userModel from "../models/userSchema.js"; // ✅ Create Tender with bidding window + delivery window import moment from "moment-timezone"; export const createTender = async (req, res) => { try { const { dispatchLocation, address, pincode, materials, transporters, remarks, closeDate, deliveryWindow, biddingStart, biddingEnd, totalWeight, totalQuantity, projectName, projectCode, purchaseOrder, projectRemark, maxBidAmount, maxBidUnit, } = req.body; console.log("Received Bidding Start (Local):", biddingStart); console.log("Received Bidding End (Local):", biddingEnd); // 🔐 Basic validations if (!projectName || !projectCode || !purchaseOrder) { return res.status(400).json({ success: false, message: "Project name, code and PO are required", }); } if (!biddingStart || !biddingEnd) { return res.status(400).json({ success: false, message: "Bidding start and end time are required", }); } if ( maxBidAmount === undefined || isNaN(maxBidAmount) || Number(maxBidAmount) < 0) { return res.status(400).json({ success: false, message: "Valid max bid amount is required and must be non-negative", }); } if (!maxBidUnit || !["Per MT", "Per Tender"].includes(maxBidUnit)) { return res.status(400).json({ success: false, message: "Valid max bid unit is required (Per MT or Per Tender)", }); } if (!deliveryWindow?.from || !deliveryWindow?.to) { return res.status(400).json({ success: false, message: "Delivery window (from and to dates) is required", }); } if (!materials || !Array.isArray(materials) || materials.length === 0) { return res.status(400).json({ success: false, message: "At least one material entry is required", }); } // 🌐 Convert all date/time inputs from 'Asia/Kolkata' to UTC const timezone = "Asia/Kolkata"; const utcBiddingStart = moment.tz(biddingStart, timezone).utc().toDate(); const utcBiddingEnd = moment.tz(biddingEnd, timezone).utc().toDate(); const utcDeliveryFrom = moment .tz(deliveryWindow.from, timezone) .utc() .toDate(); const utcDeliveryTo = moment.tz(deliveryWindow.to, timezone).utc().toDate(); let utcCloseDate = null; if (closeDate) { const [year, month, day] = closeDate.split("-").map(Number); utcCloseDate = new Date(Date.UTC(year, month - 1, day)); } const normalizedMaterials = materials.map((mat) => ({ material: mat.material, subMaterial: mat.subMaterial || "", weight: mat.weight, quantity: mat.quantity, })); const tender = new Tender({ createdBy: req.user.id, dispatchLocation, address, pincode, materials: normalizedMaterials, transporters, remarks: remarks || "", closeDate: utcCloseDate, biddingStart: utcBiddingStart, biddingEnd: utcBiddingEnd, deliveryWindow: { from: utcDeliveryFrom, to: utcDeliveryTo, }, totalWeight, totalQuantity, projectName, projectCode, purchaseOrder, projectRemark: projectRemark || "", maxBidAmount, maxBidUnit, }); await tender.save(); // ✅ Respond Immediately res.status(201).json({ success: true, data: tender }); // 📨 Fire-and-forget Email Notification to Transporters (async () => { try { // 📨 Notify transporters const transporterUsers = await User.find({ _id: { $in: transporters.map((id) => new mongoose.Types.ObjectId(id)) }, }); const transporterEmails = transporterUsers.map((user) => user.email); const subject = "📦 New Tender Invitation - RR ISPAT"; // 🌟 Professional Multilingual Email Body const websiteUrl = "https://logiyatra.rrispat.in/signin"; const supportEmail = "techsupport@rrispat.com"; const backgroundImageUrl = "https://images.unsplash.com/photo-1526403227912-7d60d6cc6b4a?ixlib=rb-4.0.3&auto=format&fit=crop&w=1950&q=80"; const htmlBody = ` <div style="margin:0; padding:0; background-image: url('${backgroundImageUrl}'); background-size: cover; background-position: center; background-repeat: no-repeat; background-color: #f4f4f4;"> <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width:620px; background: #ffffff; margin-top:30px; margin-bottom:30px; border-radius:10px; overflow:hidden; box-shadow: 0 6px 20px rgba(0,0,0,0.15);"> <tr> <td align="center" style="background:rgb(12, 25, 206); background-size: 600% 600%; animation: gradientBG 8s ease infinite; padding: 20px;"> <div style="font-family: Arial, sans-serif; font-size: 28px; font-weight: bold;"> <span style="color: #e74c3c;">RR</span> <span style="color: #ffffff;">ISPAT</span> </div> <div style="font-family: Arial, sans-serif; font-size: 14px; margin-top: 5px; color:#ffffff;"> A Unit of Godawari Power and Ispat Limited </div> </td> </tr> <tr> <td style="padding: 40px 30px 30px 30px; font-family: Arial, sans-serif; color: #333; font-size: 16px;"> <h2 style="color:#2E86C1; text-align:center;">📢 New Tender Invitation</h2> <p>Dear Transporter,<br/><small>(प्रिय ट्रांसपोर्टर)</small></p> <p>We are excited to invite you to participate in a new tender from <strong>RR ISPAT</strong>.<br/><small>(RR ISPAT द्वारा एक नए टेंडर में भाग लेने के लिए आपका स्वागत है।)</small></p> <table cellpadding="5" cellspacing="0" width="100%" style="margin: 25px 0;"> <tr> <td style="font-weight:bold; width:40%;">📍 Dispatch Location:</td> <td> ${dispatchLocation} <br/> <small style="color:#555;">(डिस्पैच स्थान: ${dispatchLocation})</small> </td> </tr> <tr> <td style="font-weight:bold;">🚚 Delivery Window:</td> <td> ${moment(utcDeliveryFrom) .tz(timezone) .format("DD MMM YYYY")} to ${moment(utcDeliveryTo) .tz(timezone) .format("DD MMM YYYY")} <br/> <small style="color:#555;">(वितरण अवधि: ${moment( utcDeliveryFrom ) .tz(timezone) .format("DD MMM YYYY")} से ${moment(utcDeliveryTo) .tz(timezone) .format("DD MMM YYYY")})</small> </td> </tr> <tr> <td style="font-weight:bold;">🕒 Bidding Starts:</td> <td> ${moment(utcBiddingStart) .tz(timezone) .format("DD MMM YYYY, hh:mm A")} <br/> <small style="color:#555;">(बिडिंग प्रारंभ: ${moment( utcBiddingStart ) .tz(timezone) .format("DD MMM YYYY, hh:mm A")})</small> </td> </tr> <tr> <td style="font-weight:bold;">⏳ Bidding Ends:</td> <td> ${moment(utcBiddingEnd) .tz(timezone) .format("DD MMM YYYY, hh:mm A")} <br/> <small style="color:#555;">(बिडिंग समाप्ति: ${moment( utcBiddingEnd ) .tz(timezone) .format("DD MMM YYYY, hh:mm A")})</small> </td> </tr> </table> <div style="text-align:center; margin:40px 0;"> <a href="${websiteUrl}" target="_blank" style="background:rgb(12, 25, 206); color:#fff; padding:14px 28px; font-size:16px; border-radius:6px; text-decoration:none; display:inline-block; box-shadow:0 4px 8px rgba(0,0,0,0.2);"> 🔗 Login to Dashboard<br/><small style="font-size:12px;">(डैशबोर्ड में लॉगिन करें)</small> </a> </div> <p style="text-align:center; margin-top:30px;"> Need help? Email us at <a href="mailto:${supportEmail}" style="color:#2E86C1;">${supportEmail}</a><br/> <small>(सहायता चाहिए? हमें ईमेल करें: ${supportEmail})</small> </p> <p style="margin-top:50px; font-family: Arial, sans-serif; font-size: 16px; color: #333; text-align: center;"> Thanks & Regards,<br> <span style="font-weight:bold; font-size:18px;">RR ISPAT</span><br> <small style="font-size: 13px; ">- A Unit of Godawari Power and Ispat Limited</small> </p> </td> </tr> <tr> <td style="background-color: #f1f1f1; text-align: center; padding: 20px; font-size: 12px; color: #777;"> Growing Stronger Together | <a href="${websiteUrl}" style="color: #2E86C1; text-decoration: none;">www.logiyatra.rrispat.in</a><br/> </td> </tr> </table> <style> @keyframes gradientBG { 0% {background-position: 0% 50%;} 50% {background-position: 100% 50%;} 100% {background-position: 0% 50%;} } </style> </div>`; for (const email of transporterEmails) { await sendMail({ to: email, subject, html: htmlBody }); } } catch (emailErr) { console.error("Email sending failed:", emailErr.message); } })(); } catch (error) { console.error("Tender creation failed:", error); res.status(500).json({ success: false, message: error.message }); } }; // ✅ 2. Finalize Tender export const finalizeTender = async (req, res) => { try { const { quotationId, finalPrice } = req.body; console.log(finalPrice); // 🔎 Find tender const tender = await Tender.findById(req.params.id); if (!tender) { return res .status(404) .json({ success: false, message: "Tender not found" }); } if (tender.status === "finalized" || tender.selectedQuotation) { return res.status(400).json({ success: false, message: "Tender has already been finalized and cannot be changed.", }); } if (tender.createdBy.toString() !== req.user.id) { return res.status(403).json({ success: false, message: "Unauthorized" }); } // 🔎 Find quotation const quotation = await Quotation.findOne({ _id: quotationId, tender: tender._id, }); if (!quotation) { return res .status(400) .json({ success: false, message: "Invalid quotation" }); } // 🔎 Find transport user const transportUser = await userModel.findById(quotation.transportUser); if (!transportUser) { return res .status(400) .json({ success: false, message: "Transport user not found" }); } // ✅ Update tender with finalization tender.selectedQuotation = quotation._id; tender.finalTransporter = quotation.transportUser; tender.finalPrice = finalPrice; tender.status = "finalized"; await tender.save(); // ✅ Email Notification to Transport User try { await sendMail({ to: transportUser.email, subject: "🎉 Congratulations! Your Quotation Has Been Accepted - RR ISPAT", html: ` <div style="margin:0; padding:0; background-color:#f4f4f4;"> <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width:620px; background:#ffffff; margin-top:30px; margin-bottom:30px; border-radius:10px; overflow:hidden; box-shadow: 0 6px 20px rgba(0,0,0,0.15);"> <tr> <td align="center" style="background: #ffffff; padding: 30px;"> <div style="font-family: Arial, sans-serif; font-size: 28px; font-weight: bold;"> <span style="color: #e74c3c;">RR</span> <span style="color: #000;">ISPAT</span> </div> <div style="font-family: Arial, sans-serif; font-size: 13px; margin-top: 5px; color: #777;"> A Unit of Godawari Power and Ispat Limited </div> </td> </tr> <tr> <td style="padding: 40px 30px; font-family: Arial, sans-serif; color: #333; font-size: 16px;"> <p>Hello <strong>${transportUser.name}</strong>,</p> <p style="margin-top:20px;"> 🎉 <strong>Congratulations!</strong> Your quotation has been <span style="color: #2E86C1;">ACCEPTED</span> for the following journey: </p> <table cellpadding="5" cellspacing="0" width="100%" style="margin: 20px 0;"> <tr> <td style="font-weight:bold;">📍 Dispatch Location:</td> <td>${tender.dispatchLocation}</td> </tr> <tr> <td style="font-weight:bold;">🚚 Delivery Window:</td> <td>${moment(tender.deliveryWindow.from) .tz("Asia/Kolkata") .format("DD MMM YYYY")} to ${moment( tender.deliveryWindow.to ) .tz("Asia/Kolkata") .format("DD MMM YYYY")}</td> </tr> </table> <p style="margin-top:30px;"><strong>📦 Tender Items:</strong></p> <ul style="margin-top:10px; padding-left:20px;"> ${tender.materials .map( (mat) => ` <li>${mat.material} (${mat.subMaterial || "N/A"}) - ${mat.weight } MT, ${mat.quantity} Qty</li> ` ) .join("")} </ul> <p style="margin-top:30px;"> <strong>✅ Finalized Price:</strong> ₹${finalPrice} </p> <p style="margin-top:30px;"> We sincerely appreciate your cooperation. Further communication regarding dispatch schedules will follow shortly. </p> <p style="margin-top:40px;"> Thanks & Regards,<br> <span style="font-weight:bold; font-size:18px;">RR ISPAT</span><br> <small style="color:#777;">A Unit of Godawari Power and Ispat Limited</small> </p> </td> </tr> <tr> <td style="background-color: #f1f1f1; text-align: center; padding: 15px; font-size: 12px; color: #777;"> Building Strong Foundations | <a href="https://www.rrispat.com" style="color: #2E86C1; text-decoration: none;">www.rrispat.com</a> </td> </tr> </table> </div> `, }); } catch (emailErr) { console.error("Failed to send finalization email:", emailErr.message); } res.status(200).json({ success: true, message: "Tender finalized and email sent to transport user", tender, }); } catch (error) { console.error("Error in finalizeTender:", error); res.status(400).json({ success: false, message: error.message }); } }; // ✅ 3. Get All Tenders Created by RR User export const getAllTendersByRRUser = async (req, res) => { try { const tenders = await Tender.find({ createdBy: req.user.id }) .sort({ createdAt: -1 }) .populate("selectedQuotation"); res.status(200).json({ success: true, data: tenders }); } catch (error) { res.status(500).json({ success: false, message: error.message }); } }; // ✅ 4. Get Tenders Assigned to a Transporter (excluding already quoted ones) export const getTendersForTransporter = async (req, res) => { try { const transporterId = req.user.id; const now = new Date(); // Step 1: Get all open tenders assigned to this transporter and within bidding window const tenders = await Tender.find({ transporters: transporterId, status: "open", biddingStart: { $lte: now }, biddingEnd: { $gte: now }, }).sort({ createdAt: -1 }); const tenderIds = tenders.map((t) => t._id); // Step 2: Get all quotations by this transporter for these tenders const transporterQuotations = await Quotation.find({ transportUser: transporterId, tender: { $in: tenderIds }, }).select("tender"); const quotedTenderIds = new Set( transporterQuotations.map((q) => q.tender.toString()) ); // Count how many times transporter quoted per tender const bidCountMap = {}; transporterQuotations.forEach((q) => { const id = q.tender.toString(); bidCountMap[id] = (bidCountMap[id] || 0) + 1; }); // Step 3: Prepare tender list with hasQuoted and bidsLeft const tendersWithStatus = tenders.map((tender) => { const tenderObj = tender.toObject(); const tid = tender._id.toString(); tenderObj.hasQuoted = quotedTenderIds.has(tid); tenderObj.bidsUsed = bidCountMap[tid] || 0; tenderObj.bidsRemaining = Math.max(0, 3 - tenderObj.bidsUsed); return tenderObj; }); // Step 4: Populate createdBy field await Tender.populate(tendersWithStatus, { path: "createdBy", select: "name email", }); res.status(200).json({ success: true, data: tendersWithStatus }); } catch (error) { console.error("Error in getTendersForTransporter:", error); res.status(500).json({ success: false, message: error.message }); } }; //upcomming tender export const getUpcomingTendersForTransporter = async (req, res) => { try { const transporterId = req.user.id; const now = new Date(); // 🟡 Step 1: Find upcoming tenders assigned to this transporter const upcomingTenders = await Tender.find({ transporters: transporterId, status: "open", biddingStart: { $gt: now }, }) .sort({ biddingStart: 1 }) .populate("createdBy", "name email"); // 🕒 Step 2: Add biddingOpensIn to each tender const tendersWithCountdown = upcomingTenders.map((tender) => { const tenderObj = tender.toObject(); tenderObj.biddingOpensIn = new Date(tender.biddingStart).getTime() - now.getTime(); // in milliseconds return tenderObj; }); res.status(200).json({ success: true, data: tendersWithCountdown }); } catch (error) { console.error("Error in getUpcomingTendersForTransporter:", error); res.status(500).json({ success: false, message: error.message }); } }; // ✅ 5. Get Quotations for a Tender export const getTenderQuotations = async (req, res) => { try { const tenderId = req.params.id; const userId = req.user.id; const tender = await Tender.findById(tenderId).populate( "createdBy", "name email" ); if (!tender) { return res .status(404) .json({ success: false, message: "Tender not found" }); } if (tender.createdBy._id.toString() !== userId) { return res.status(403).json({ success: false, message: "Unauthorized" }); } const now = new Date(); if (now < tender.biddingEnd) { return res.status(403).json({ success: false, message: "Top quotations can be viewed only after the bidding window closes.", }); } const allQuotes = await Quotation.find({ tender: tenderId }) .populate("transportUser", "name email") .sort({ price: 1, createdAt: 1 }); const seen = new Set(); const bestQuotes = []; for (const q of allQuotes) { const uid = q.transportUser._id.toString(); if (!seen.has(uid)) { seen.add(uid); bestQuotes.push(q); } } let quotesToReturn = []; if (tender.reopenCount === 0) { quotesToReturn = bestQuotes.slice(0, 3); // L1, L2, L3 } else if (tender.reopenCount === 1) { quotesToReturn = bestQuotes.slice(1, 3); // L2, L3 } else if (tender.reopenCount === 2) { quotesToReturn = bestQuotes.slice(2, 3); // Only L3 } const ranked = quotesToReturn.map((q, index) => { const signedFiles = (q.files || []).map((file) => { const key = file.url?.split("/").pop(); return { ...file, url: generateSignedUrl(key) }; }); return { rank: `L${bestQuotes.indexOf(q) + 1}`, transportUser: q.transportUser, price: q.price, vehicleNumber: q.vehicleNumber, createdAt: q.createdAt, files: signedFiles, _id: q._id, }; }); res.status(200).json({ success: true, data: ranked }); } catch (error) { console.error("Error fetching top 3 quotations:", error); res.status(500).json({ success: false, message: error.message }); } }; //reopen tender export const reopenTender = async (req, res) => { try { const { id } = req.params; const { reason } = req.body; if (!reason || reason.trim() === "") { return res.status(400).json({ error: "Reason is required." }); } const tender = await Tender.findById(id); if (!tender) return res.status(404).json({ success: false, message: "Not found" }); // 🚫 Prevent reopening more than twice if (tender.reopenCount >= 2) { return res.status(403).json({ success: false, message: "This tender has already been reopened twice and cannot be reopened again.", }); } // ✅ Perform reopen tender.status = "open"; tender.selectedQuotation = null; tender.finalTransporter = null; tender.finalPrice = null; tender.reopenCount = (tender.reopenCount || 0) + 1; // ✅ increment counter tender.winnerComment = `[Reopened: ${reason}]`; await tender.save(); res.status(200).json({ success: true, message: "Tender reopened successfully", reopenCount: tender.reopenCount, }); } catch (error) { res.status(500).json({ success: false, message: error.message }); } }; // ✅ 6. Get Single Tender export const getSingleTender = async (req, res) => { try { const tender = await Tender.findById(req.params.id) .populate("createdBy", "name email") .populate("quotations") .populate("selectedQuotation"); if (!tender) { return res .status(404) .json({ success: false, message: "Tender not found" }); } res.status(200).json({ success: true, data: tender }); } catch (error) { res.status(400).json({ success: false, message: error.message }); } }; // ✅ 7. Delete Tender (by RR User) export const deleteTender = async (req, res) => { try { const tender = await Tender.findById(req.params.id); if (!tender) { return res .status(404) .json({ success: false, message: "Tender not found" }); } if (tender.createdBy.toString() !== req.user.id) { return res.status(403).json({ success: false, message: "Unauthorized" }); } await Tender.findByIdAndDelete(req.params.id); res .status(200) .json({ success: true, message: "Tender deleted successfully" }); } catch (error) { res.status(500).json({ success: false, message: error.message }); } }; // ✅ Get Quotation History for Transporter export const getQuotationHistoryForTransporter = async (req, res) => { try { const transporterId = new mongoose.Types.ObjectId(req.user.id); const today = new Date(); today.setHours(0, 0, 0, 0); // normalize to start of today // ✅ Get all quotations by this transporter, grouped by tender const quotations = await Quotation.find({ transportUser: transporterId }) .populate({ path: "tender", populate: { path: "createdBy", select: "name email", }, }) .sort({ createdAt: 1 }); const tenderQuotesMap = new Map(); // ✅ Group quotations by tender ID (and skip tenders whose closeDate >= today) for (const q of quotations) { const tender = q.tender; const tenderId = tender?._id?.toString(); if (!tenderId) continue; const biddingEndTime = new Date(tender.biddingEnd); if (biddingEndTime > new Date()) continue; if (!tenderQuotesMap.has(tenderId)) { tenderQuotesMap.set(tenderId, []); } tenderQuotesMap.get(tenderId).push(q); } const result = []; for (const [tenderId, tenderQuotes] of tenderQuotesMap.entries()) { const tender = tenderQuotes[0].tender; // ✅ Determine if any of the quotations match the selectedQuotation const isSelected = tender.selectedQuotation && tenderQuotes.some( (q) => q._id.toString() === tender.selectedQuotation.toString() ); const formattedQuotes = tenderQuotes.map((q) => { const signedFiles = (q.files || []).map((file) => { const key = file.url?.split("/").pop(); return { ...file, url: generateSignedUrl(key), }; }); return { _id: q._id, price: q.price, vehicleNumber: q.vehicleNumber, createdAt: q.createdAt, files: signedFiles, }; }); result.push({ tenderId: tender._id, tender: { dispatchLocation: tender.dispatchLocation, address: tender.address, deliveryWindow: tender.deliveryWindow || { from: null, to: null }, closeDate: tender.closeDate, status: tender.status, remarks: tender.remarks, materials: tender.materials || [], totalWeight: tender.totalWeight, totalQuantity: tender.totalQuantity, createdBy: tender.createdBy || null, maxBidAmount: tender.maxBidAmount, maxBidUnit: tender.maxBidUnit || null, finalizedStatus: isSelected ? "Your quotation was finalized" : "Your quotation was not selected", }, quotations: formattedQuotes, }); } res.status(200).json({ success: true, data: result, }); } catch (error) { console.error("Error fetching quotation history:", error); res.status(500).json({ success: false, message: error.message }); } }; //all finalized tenders // export const getAllFinalizedTendersWithQuotations = async (req, res) => { // try { // const rrUserId = req.user.id; // // ✅ Get all finalized tenders created by this RR user // const finalizedTenders = await Tender.find({ // createdBy: rrUserId, // status: "finalized", // }) // .populate({ // path: "selectedQuotation", // populate: { path: "transportUser", select: "name email" }, // }) // .populate("createdBy", "name email") // .sort({ updatedAt: -1 }); // const results = []; // for (const tender of finalizedTenders) { // // ✅ Get all quotations for this tender // const quotations = await Quotation.find({ tender: tender._id }) // .populate("transportUser", "name email") // .sort({ createdAt: -1 }); // const allQuotations = quotations.map((q) => { // const signedFiles = (q.files || []).map((file) => { // const key = file.url?.split("/").pop(); // return { // ...file, // url: generateSignedUrl(key), // }; // }); // return { // _id: q._id, // price: q.price, // vehicleNumber: q.vehicleNumber, // createdAt: q.createdAt, // transportUser: q.transportUser, // files: signedFiles, // }; // }); // const selectedQuotationId = tender.selectedQuotation?._id?.toString(); // results.push({ // tender: { // _id: tender._id, // dispatchLocation: tender.dispatchLocation, // address: tender.address, // deliveryWindow: tender.deliveryWindow, // ✅ Updated from dateOfDelivery // closeDate: tender.closeDate, // remarks: tender.remarks, // status: tender.status, // finalPrice: tender.finalPrice, // materials: tender.materials, // totalWeight: tender.totalWeight, // totalQuantity: tender.totalQuantity, // createdBy: tender.createdBy, // }, // selectedQuotation: tender.selectedQuotation // ? { // _id: tender.selectedQuotation._id, // price: tender.selectedQuotation.price, // vehicleNumber: tender.selectedQuotation.vehicleNumber, // transportUser: tender.selectedQuotation.transportUser, // files: (tender.selectedQuotation.files || []).map((file) => { // const key = file.url?.split("/").pop(); // return { // ...file, // url: generateSignedUrl(key), // }; // }), // } // : null, // allQuotations: allQuotations.map((q) => ({ // ...q, // selected: q._id.toString() === selectedQuotationId, // })), // }); // } // res.status(200).json({ success: true, data: results }); // } catch (error) { // console.error("Error fetching finalized tenders for RR user:", error); // res.status(500).json({ success: false, message: error.message }); // } // }; //your position for transporters export const getMyQuotationPosition = async (req, res) => { try { const { tenderId } = req.params; const userId = req.user.id; console.log("Fetching position for tenderId:", tenderId); // ✅ Validate Tender ID if (!tenderId || !mongoose.Types.ObjectId.isValid(tenderId)) { return res.status(400).json({ message: "Invalid or missing Tender ID" }); } // ✅ Get all quotations for the tender, sorted by price + createdAt const allQuotes = await Quotation.find({ tender: tenderId }).sort({ price: 1, createdAt: 1, }); // ✅ Group best (lowest) quote per transporter const bestQuotesMap = new Map(); // transportUserId => bestQuotation for (const quote of allQuotes) { const uid = quote.transportUser.toString(); if (!bestQuotesMap.has(uid)) { bestQuotesMap.set(uid, quote); // first lowest quote per transporter } } // ✅ Sort those best quotes by price (and createdAt to break ties) const sortedBestQuotes = Array.from(bestQuotesMap.entries()).sort( ([, q1], [, q2]) => { if (q1.price === q2.price) { return new Date(q1.createdAt) - new Date(q2.createdAt); } return q1.price - q2.price; } ); // ✅ Find current user's rank + their best quote let position = null; let bestQuote = null; for (let i = 0; i < sortedBestQuotes.length; i++) { const [uid, quote] = sortedBestQuotes[i]; if (uid === userId) { position = `L${i + 1}`; bestQuote = { _id: quote._id, price: quote.price, vehicleNumber: quote.vehicleNumber, createdAt: quote.createdAt, }; break; } } // ✅ No bids yet? if (!bestQuote) { return res.status(200).json({ position: null, message: "You have not submitted any bids yet.", }); } // ✅ Success response with rank + best bid res.status(200).json({ position, bestQuotation: bestQuote, }); } catch (error) { console.error("Error getting bid position:", error); res.status(500).json({ message: "Internal server error" }); } };
Comments