diff --git a/routes/listingRoutes.js b/routes/listingRoutes.js index 96662ca..2c12198 100644 --- a/routes/listingRoutes.js +++ b/routes/listingRoutes.js @@ -82,26 +82,159 @@ router.get("/:id", async (req, res) => { } }); -// Update listing by ID +// Update listing (and optionally its animal) by ID router.put("/:id", async (req, res) => { const listingId = req.params.id; - const { title, description, price } = req.body; - try { - const updateResult = await pool.query( - "UPDATE listings SET title = $1, description = $2, price = $3, updated_at = NOW() WHERE id = $4 RETURNING *", - [title, description, price, listingId] - ); - if (updateResult.rows.length === 0) { - return res - .status(404) - .json({ error: "Listing not found for update" }); - } - res.status(200).json(updateResult.rows[0]); - } catch (error) { - res.status(500).json({ - error: `Internal Server Error in updating the specified ${listingId} listing`, + const { title, price, currency, is_negotiable, listing_type } = req.body; + const { animal } = req.body; + + if ( + typeof title === "undefined" && + typeof price === "undefined" && + typeof currency === "undefined" && + typeof is_negotiable === "undefined" && + typeof listing_type === "undefined" && + !animal + ) { + return res.status(400).json({ + error: "Nothing to update. Provide at least one listing field or an animal object.", }); } + + const client = await pool.connect(); + + try { + await client.query("BEGIN"); + + // Lock listing row and get its animal_id + const listingResult = await client.query( + "SELECT * FROM listings WHERE id = $1 FOR UPDATE", + [listingId] + ); + + if (listingResult.rows.length === 0) { + await client.query("ROLLBACK"); + return res.status(404).json({ error: "Listing not found" }); + } + + const currentListing = listingResult.rows[0]; + let updatedListing = currentListing; + + // Build dynamic listing UPDATE if any listing fields are provided + const listingUpdates = []; + const listingValues = []; + let idx = 1; + + if (typeof title !== "undefined") { + listingUpdates.push(`title = $${idx++}`); + listingValues.push(title); + } + if (typeof price !== "undefined") { + listingUpdates.push(`price = $${idx++}`); + listingValues.push(price); + } + if (typeof currency !== "undefined") { + listingUpdates.push(`currency = $${idx++}`); + listingValues.push(currency); + } + if (typeof is_negotiable !== "undefined") { + listingUpdates.push(`is_negotiable = $${idx++}`); + listingValues.push(is_negotiable); + } + if (typeof listing_type !== "undefined") { + listingUpdates.push(`listing_type = $${idx++}`); + listingValues.push(listing_type); + } + + if (listingUpdates.length > 0) { + listingUpdates.push("updated_at = NOW()"); + const listingUpdateQuery = ` + UPDATE listings + SET ${listingUpdates.join(", ")} + WHERE id = $${idx} + RETURNING *; + `; + listingValues.push(listingId); + + const updateResult = await client.query( + listingUpdateQuery, + listingValues + ); + updatedListing = updateResult.rows[0]; + } + + let updatedAnimal = null; + + // Update the linked animal if payload is provided + if (animal && updatedListing.animal_id) { + const animalUpdates = []; + const animalValues = []; + let aIdx = 1; + + const updatableAnimalFields = [ + "species_id", + "breed_id", + "sex", + "age_months", + "weight_kg", + "color_markings", + "quantity", + "purpose", + "health_status", + "vaccinated", + "dewormed", + "previous_pregnancies_count", + "pregnancy_status", + "milk_yield_litre_per_day", + "ear_tag_no", + "description", + "suggested_care", + "location_id", + ]; + + for (const field of updatableAnimalFields) { + if (Object.prototype.hasOwnProperty.call(animal, field)) { + animalUpdates.push(`${field} = $${aIdx++}`); + animalValues.push(animal[field]); + } + } + + if (animalUpdates.length > 0) { + animalUpdates.push("updated_at = NOW()"); + const animalUpdateQuery = ` + UPDATE animals + SET ${animalUpdates.join(", ")} + WHERE id = $${aIdx} + RETURNING *; + `; + animalValues.push(updatedListing.animal_id); + + const animalResult = await client.query( + animalUpdateQuery, + animalValues + ); + + if (animalResult.rows.length > 0) { + updatedAnimal = animalResult.rows[0]; + } + } + } + + await client.query("COMMIT"); + + return res.status(200).json({ + message: "Listing (and animal, if provided) updated", + listing: updatedListing, + animal: updatedAnimal, + }); + } catch (error) { + await client.query("ROLLBACK"); + return res.status(500).json({ + error: `Internal Server Error updating listing ${listingId}: ${error.message}`, + }); + } finally { + client.release(); + } }); // Submit a new listing @@ -313,6 +446,155 @@ router.get("/user/:user_id", async (req, res) => { } }); +// Add media to listing +router.post("/:id/media", async (req, res) => { + const listingId = req.params.id; + const { media } = req.body; + + if (!media || (Array.isArray(media) && media.length === 0)) { + return res + .status(400) + .json({ error: "media array is required and cannot be empty" }); + } + + const items = Array.isArray(media) ? media : [media]; + + try { + const inserted = []; + const insertSql = + "INSERT INTO listing_media (listing_id, media_url, media_type, is_primary, sort_order, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, NOW(), NOW()) RETURNING *"; + + for (let i = 0; i < items.length; i++) { + const item = items[i]; + const result = await pool.query(insertSql, [ + listingId, + item.media_url, + item.media_type, + typeof item.is_primary !== "undefined" + ? item.is_primary + : false, + typeof item.sort_order !== "undefined" + ? item.sort_order + : i + 1, + ]); + inserted.push(result.rows[0]); + } + + res.status(200).json(inserted); + } catch (error) { + res.status(500).json({ + error: `Internal Server Error adding media to listing ${listingId}: ${error.message}`, + }); + } +}); + +// Update listing score and score status +router.patch("/:id/score", async (req, res) => { + const listingId = req.params.id; + const { listing_score, listing_score_status } = req.body; + + if ( + typeof listing_score === "undefined" && + typeof listing_score_status === "undefined" + ) { + return res.status(400).json({ + error: "At least one of listing_score or listing_score_status must be provided", + }); + } + + const updates = []; + const values = []; + let idx = 1; + + if (typeof listing_score !== "undefined") { + updates.push(`listing_score = $${idx++}`); + values.push(listing_score); + } + if (typeof listing_score_status !== "undefined") { + updates.push(`listing_score_status = $${idx++}`); + values.push(listing_score_status); + } + + updates.push(`updated_at = NOW()`); + + const query = ` + UPDATE listings + SET ${updates.join(", ")} + WHERE id = $${idx} + RETURNING *; + `; + values.push(listingId); + + try { + const result = await pool.query(query, values); + if (result.rows.length === 0) { + return res.status(404).json({ error: "Listing not found" }); + } + res.status(200).json({ + message: + "Listing listing_score and/or listing_score_status updated", + listing: result.rows[0], + }); + } catch (error) { + res.status(500).json({ + error: `Internal Server Error updating listing_score/listing_score_status for listing ${listingId}: ${error.message}`, + }); + } +}); + +// Update listing counters (views, bookmarks, enquiries, clicks) by incrementing +router.patch("/:id/counter", async (req, res) => { + const listingId = req.params.id; + const { type, increment = 1 } = req.body; + + const validTypes = [ + "views_count", + "bookmarks_count", + "enquiries_call_count", + "enquiries_whatsapp_count", + "clicks_count", + ]; + + if (!type || !validTypes.includes(type)) { + return res.status(400).json({ + error: + "Invalid or missing type. Must be one of: " + + validTypes.join(", "), + }); + } + + if (typeof increment !== "number") { + return res.status(400).json({ + error: "Increment must be a number", + }); + } + + try { + const updateResult = await pool.query( + ` + UPDATE listings + SET ${type} = COALESCE(${type}, 0) + $1, updated_at = NOW() + WHERE id = $2 + RETURNING *; + `, + [increment, listingId] + ); + + if (updateResult.rows.length === 0) { + return res.status(404).json({ error: "Listing not found" }); + } + + return res.status(200).json({ + message: `Listing ${type} incremented by ${increment}`, + listing: updateResult.rows[0], + }); + } catch (error) { + res.status(500).json({ + error: `Internal Server Error incrementing ${type} for listing ${listingId}: ${error.message}`, + }); + } +}); + // Mark listing as sold or update listing status router.patch("/:id/status", async (req, res) => { const listingId = req.params.id;