diff --git a/routes/listingRoutes.js b/routes/listingRoutes.js index 2c12198..4d2204c 100644 --- a/routes/listingRoutes.js +++ b/routes/listingRoutes.js @@ -3,623 +3,397 @@ import pool from "../db/pool.js"; const router = express.Router(); -// Get all listings +const createNewLocation = async (client, userId, locationData) => { + const { + lat, + lng, + source_type, + // Location Details + is_saved_address, + location_type, + country, + state, + district, + city_village, + pincode, + } = locationData; + + // 1a. Insert into locations + const insertLocationQuery = ` + INSERT INTO locations (user_id, lat, lng, source_type) + VALUES ($1, $2, $3, $4) + RETURNING id + `; + const locationValues = [userId, lat, lng, source_type || "manual"]; + const locationResult = await client.query(insertLocationQuery, locationValues); + const locationId = locationResult.rows[0].id; + + // 1b. Insert into location_details + const insertLocationDetailsQuery = ` + INSERT INTO location_details ( + location_id, is_saved_address, location_type, + country, state, district, city_village, pincode + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + `; + const detailsValues = [ + locationId, + is_saved_address || false, + location_type || "other", + country, + state, + district, + city_village, + pincode, + ]; + await client.query(insertLocationDetailsQuery, detailsValues); + + return locationId; +}; + +// 1. GET / (Main Feed) - Optimized with idx_listings_feed_optimized router.get("/", async (req, res) => { - const speciesId = req.query.species_id; - const breedId = req.query.breed_id; - const state = req.query.state; - const district = req.query.district; - const minPrice = req.query.min_price; - const listingType = req.query.listing_type; - - let baseQuery = ` - SELECT l.*, a.species_id, a.breed_id, loc.state, loc.district - FROM listings l - LEFT JOIN animals a ON a.id = l.animal_id - LEFT JOIN locations loc ON loc.id = a.location_id - WHERE 1=1 - `; - const queryParams = []; - let paramIndex = 1; - - if (speciesId) { - baseQuery += ` AND a.species_id = $${paramIndex}`; - queryParams.push(speciesId); - paramIndex++; - } - if (breedId) { - baseQuery += ` AND a.breed_id = $${paramIndex}`; - queryParams.push(breedId); - paramIndex++; - } - if (state) { - baseQuery += ` AND loc.state = $${paramIndex}`; - queryParams.push(state); - paramIndex++; - } - if (district) { - baseQuery += ` AND loc.district = $${paramIndex}`; - queryParams.push(district); - paramIndex++; - } - if (minPrice) { - baseQuery += ` AND l.price >= $${paramIndex}`; - queryParams.push(minPrice); - paramIndex++; - } - if (listingType) { - baseQuery += ` AND l.listing_type = $${paramIndex}`; - queryParams.push(listingType); - paramIndex++; - } - try { - const listingsResult = await pool.query(baseQuery, queryParams); - res.status(200).json(listingsResult.rows); - } catch (error) { - res.status(500).json({ - error: "Internal Server Error in fetching listings", - }); + const { status = "active", species_id, price_min, price_max, limit = 20, offset = 0 } = req.query; + + let queryText = ` + SELECT * FROM listings + WHERE deleted = FALSE + AND status = $1 + `; + const queryParams = [status]; + let paramCount = 1; + + if (species_id) { + paramCount++; + queryText += ` AND filter_species_id = $${paramCount}`; + queryParams.push(species_id); + } + + if (price_min) { + paramCount++; + queryText += ` AND price >= $${paramCount}`; + queryParams.push(price_min); + } + + if (price_max) { + paramCount++; + queryText += ` AND price <= $${paramCount}`; + queryParams.push(price_max); + } + + queryText += ` ORDER BY created_at DESC LIMIT $${paramCount + 1} OFFSET $${paramCount + 2}`; + queryParams.push(limit, offset); + + const result = await pool.query(queryText, queryParams); + res.json(result.rows); + } catch (err) { + console.error("Error fetching listings feed:", err); + res.status(500).json({ error: "Internal server error" }); } }); -// Get listing by ID -router.get("/:id", async (req, res) => { - const listingId = req.params.id; +// 2. GET /near-me (Spatial Search) - Optimized with idx_listings_spatial +router.get("/near-me", async (req, res) => { try { - const listingResult = await pool.query( - "SELECT * FROM listings WHERE id = $1", - [listingId] - ); - if (listingResult.rows.length === 0) { - return res.status(404).json({ error: "Listing not found" }); + const { lat, lng, radius_meters = 50000, limit = 20, offset = 0 } = req.query; + + if (!lat || !lng) { + return res.status(400).json({ error: "Latitude and Longitude are required" }); } - res.status(200).json(listingResult.rows[0]); - } catch (error) { - res.status(500).json({ - error: `Internal Server Error in fetching the specified ${listingId} listing`, - }); + + const queryText = ` + SELECT * FROM listings + WHERE deleted = FALSE + AND ST_DWithin(filter_location_geog, ST_SetSRID(ST_MakePoint($1, $2), 4326)::geography, $3) + ORDER BY filter_location_geog <-> ST_SetSRID(ST_MakePoint($1, $2), 4326)::geography + LIMIT $4 OFFSET $5 + `; + const queryParams = [lng, lat, radius_meters, limit, offset]; // Note: PostGIS uses (lng, lat) for Points + + const result = await pool.query(queryText, queryParams); + res.json(result.rows); + } catch (err) { + console.error("Error fetching nearby listings:", err); + res.status(500).json({ error: "Internal server error" }); } }); -// Update listing (and optionally its animal) by ID -router.put("/:id", async (req, res) => { - const listingId = req.params.id; - 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(); - +// 3. GET /search TODO: (Full Text Search) - Optimized with idx_listings_search_gin +router.get("/search", async (req, res) => { try { - await client.query("BEGIN"); + const { q, limit = 20, offset = 0 } = req.query; - // 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" }); + if (!q) { + return res.status(400).json({ error: "Search query 'q' is required" }); } - const currentListing = listingResult.rows[0]; - let updatedListing = currentListing; + const queryText = ` + SELECT * FROM listings + WHERE deleted = FALSE + AND search_vector @@ plainto_tsquery('english', $1) + LIMIT $2 OFFSET $3 + `; + const queryParams = [q, limit, offset]; - // 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(); + const result = await pool.query(queryText, queryParams); + res.json(result.rows); + } catch (err) { + console.error("Error searching listings:", err); + res.status(500).json({ error: "Internal server error" }); } }); -// Submit a new listing +// 4. GET /seller/:sellerId (My Listings) - Optimized with idx_listings_seller_status +router.get("/seller/:sellerId", async (req, res) => { + try { + const { sellerId } = req.params; + const { status } = req.query; + + let queryText = ` + SELECT * FROM listings + WHERE deleted = FALSE + AND seller_id = $1 + `; + const queryParams = [sellerId]; + let paramCount = 1; + + if (status) { + paramCount++; + queryText += ` AND status = $${paramCount}`; + queryParams.push(status); + } + + queryText += ` ORDER BY created_at DESC`; + + const result = await pool.query(queryText, queryParams); + res.json(result.rows); + } catch (err) { + console.error("Error fetching seller listings:", err); + res.status(500).json({ error: "Internal server error" }); + } +}); + +// 5. CRUD Operations + +// CREATE Listing router.post("/", async (req, res) => { const client = await pool.connect(); try { await client.query("BEGIN"); const { + // Listing details seller_id, title, price, currency, is_negotiable, listing_type, - animal, + status, + // Animal details + species_id, + breed_id, + location_id, + sex, + age_months, + weight_kg, + color_markings, + quantity, + purpose, + health_status, + vaccinated, + dewormed, + pregnancy_status, + milk_yield_litre_per_day, + ear_tag_no, + description, + // New Location details + new_location, } = req.body; - // Add a new location if provided - if (!animal.location_id && animal.new_location) { - const newLocation = { - ...animal.new_location, - }; - if ( - newLocation.is_saved_address && - !newLocation.user_id && - seller_id - ) { - newLocation.user_id = seller_id; - } - animal.location_id = await addNewLocation(client, newLocation); + let final_location_id = location_id; + + // 1. Create Location (if needed) + if (!final_location_id && new_location) { + final_location_id = await createNewLocation(client, seller_id, new_location); } - const animalId = await addAnimalToListing(client, animal); + // 2. Create Animal + const insertAnimalQuery = ` + INSERT INTO animals ( + species_id, breed_id, location_id, sex, age_months, weight_kg, + color_markings, quantity, purpose, health_status, vaccinated, + dewormed, pregnancy_status, milk_yield_litre_per_day, ear_tag_no, description + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) + RETURNING id + `; + const animalValues = [ + species_id, + breed_id, + final_location_id, + sex, + age_months, + weight_kg, + color_markings, + quantity || 1, // Default to 1 + purpose, + health_status || "healthy", // Default + vaccinated || false, + dewormed || false, + pregnancy_status || "unknown", // Default + milk_yield_litre_per_day, + ear_tag_no, + description, + ]; - const listingInsertQuery = - "INSERT INTO listings (seller_id, animal_id, title, price, currency, is_negotiable, listing_type, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW()) RETURNING *"; + const animalResult = await client.query(insertAnimalQuery, animalValues); + const animal_id = animalResult.rows[0].id; + + // 3. Create Listing + const insertListingQuery = ` + INSERT INTO listings (seller_id, animal_id, title, price, currency, is_negotiable, listing_type, status) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + RETURNING * + `; const listingValues = [ seller_id, - animalId, + animal_id, title, price, currency, is_negotiable, listing_type, + status, ]; - const listingResult = await client.query( - listingInsertQuery, - listingValues - ); - if ( - req.body.media && - Array.isArray(req.body.media) && - req.body.media.length > 0 - ) { - const listingId = listingResult.rows[0].id; - const mediaInsertQuery = ` - 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 < req.body.media.length; i++) { - const media = req.body.media[i]; - await client.query(mediaInsertQuery, [ - listingId, - media.media_url, - media.media_type, - typeof media.is_primary !== "undefined" - ? media.is_primary - : false, - typeof media.sort_order !== "undefined" - ? media.sort_order - : i + 1, - ]); - } - } + const listingResult = await client.query(insertListingQuery, listingValues); await client.query("COMMIT"); + res.status(201).json(listingResult.rows[0]); - } catch (error) { + } catch (err) { await client.query("ROLLBACK"); - res.status(500).json({ - error: - "Internal Server Error in creating new listing: " + - error.message, - }); + console.error("Error creating listing with animal:", err); + res.status(500).json({ error: "Internal server error" }); } finally { client.release(); } }); -const addAnimalToListing = async (client, animal) => { +// GET Single Listing +router.get("/:id", async (req, res) => { try { - const animalInsertQuery = - "INSERT INTO animals (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, created_from, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, NOW(), NOW()) RETURNING id"; - const animalValues = [ - animal.species_id, - animal.breed_id, - animal.sex, - animal.age_months, - animal.weight_kg, - animal.color_markings, - animal.quantity, - animal.purpose, - animal.health_status, - animal.vaccinated, - animal.dewormed, - animal.previous_pregnancies_count, - animal.pregnancy_status, - animal.milk_yield_litre_per_day, - animal.ear_tag_no, - animal.description, - animal.suggested_care, - animal.location_id, - animal.created_from || "listing", - ]; - const animalResult = await client.query( - animalInsertQuery, - animalValues - ); - const animalId = animalResult.rows[0].id; - return animalId; - } catch (error) { - throw new Error("Error adding animal to listing: " + error.message); - } -}; + const { id } = req.params; -const addNewLocation = async (client, location) => { - try { - const { - user_id = null, - is_saved_address = false, - location_type = null, - country = null, - state = null, - district = null, - city_village = null, - pincode = null, - lat = null, - lng = null, - source_type = "unknown", - source_confidence = "medium", - } = location; + const queryText = ` + SELECT l.*, row_to_json(a) as animal + FROM listings l + JOIN animals a ON l.animal_id = a.id + WHERE l.id = $1 AND l.deleted = FALSE + `; + const result = await pool.query(queryText, [id]); - const locationInsertQuery = - "INSERT INTO locations (user_id, is_saved_address, location_type, country, state, district, city_village, pincode, lat, lng, source_type, source_confidence, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, NOW(), NOW()) RETURNING id"; - const locationValues = [ - user_id, - is_saved_address, - location_type, - country, - state, - district, - city_village, - pincode, - lat, - lng, - source_type, - source_confidence, - ]; - const locationResult = await client.query( - locationInsertQuery, - locationValues - ); - return locationResult.rows[0].id; - } catch (error) { - throw new Error("Error adding new location: " + error.message); - } -}; - -// Delete listing by ID -router.delete("/:id", async (req, res) => { - const listingId = req.params.id; - try { - const deleteResult = await pool.query( - "DELETE FROM listings WHERE id = $1 RETURNING *", - [listingId] - ); - if (deleteResult.rows.length === 0) { - return res - .status(404) - .json({ error: "Listing not found or already deleted" }); - } - res.status(200).json({ - message: "Listing deleted successfully", - listing: deleteResult.rows[0], - }); - } catch (error) { - res.status(500).json({ - error: `Internal Server Error in deleting the specified ${listingId} listing`, - }); - } -}); - -// Get all listings for a user -router.get("/user/:user_id", async (req, res) => { - const userId = req.params.user_id; - try { - const listingsResult = await pool.query( - "SELECT * FROM listings WHERE seller_id = $1 ORDER BY created_at DESC", - [userId] - ); - res.status(200).json(listingsResult.rows); - } catch (error) { - res.status(500).json({ - error: "Internal Server Error in fetching user listings", - }); - } -}); - -// 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}`, - }); + + res.json(result.rows[0]); + } catch (err) { + console.error("Error fetching listing:", err); + res.status(500).json({ error: "Internal server error" }); } }); -// 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", - }); - } - +// UPDATE Listing +router.put("/:id", async (req, res) => { try { - const updateResult = await pool.query( - ` - UPDATE listings - SET ${type} = COALESCE(${type}, 0) + $1, updated_at = NOW() - WHERE id = $2 - RETURNING *; - `, - [increment, listingId] - ); + const { id } = req.params; + const { title, price, currency, is_negotiable, listing_type, status } = req.body; - if (updateResult.rows.length === 0) { + const queryText = ` + UPDATE listings + SET title = COALESCE($1, title), + price = COALESCE($2, price), + currency = COALESCE($3, currency), + is_negotiable = COALESCE($4, is_negotiable), + listing_type = COALESCE($5, listing_type), + status = COALESCE($6, status) + WHERE id = $7 AND deleted = FALSE + RETURNING * + `; + const queryParams = [title, price, currency, is_negotiable, listing_type, status, id]; + + const result = await pool.query(queryText, queryParams); + + if (result.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}`, - }); + res.json(result.rows[0]); + } catch (err) { + console.error("Error updating listing:", err); + res.status(500).json({ error: "Internal server error" }); } }); -// Mark listing as sold or update listing status -router.patch("/:id/status", async (req, res) => { - const listingId = req.params.id; - const { status } = req.body; - if (!status) { - return res.status(400).json({ error: "Status must be provided" }); - } +// SOFT DELETE Listing +router.delete("/:id", async (req, res) => { try { - const updateResult = await pool.query( - "UPDATE listings SET status = $1, updated_at = NOW() WHERE id = $2 RETURNING *", - [status, listingId] - ); - if (updateResult.rows.length === 0) { - return res - .status(404) - .json({ error: "Listing not found for status update" }); + const { id } = req.params; + const { deleted_reason } = req.body; + + const queryText = ` + UPDATE listings + SET deleted = TRUE, deleted_reason = $1 + WHERE id = $2 + RETURNING * + `; + const result = await pool.query(queryText, [deleted_reason, id]); + + if (result.rows.length === 0) { + return res.status(404).json({ error: "Listing not found" }); } - res.status(200).json({ - message: "Listing status updated", - listing: updateResult.rows[0], - }); - } catch (error) { - res.status(500).json({ - error: `Internal Server Error updating listing status for ${listingId}`, - }); + + res.json({ message: "Listing deleted successfully", listing: result.rows[0] }); + } catch (err) { + console.error("Error deleting listing:", err); + res.status(500).json({ error: "Internal server error" }); + } +}); + + +// Get the list of breeds +router.get("/breeds", async (req, res) => { + try { + const queryText = "SELECT * FROM breeds"; + const result = await pool.query(queryText); + res.json(result.rows); + } catch (err) { + console.error("Error fetching breeds:", err); + res.status(500).json({ error: "Internal server error" }); + } +}); + + +// Get user favorite listings +router.get("/user/:userId/favorites", async (req, res) => { + try { + const { userId } = req.params; + const queryText = ` + SELECT l.*, row_to_json(a) as animal + FROM favorites f + JOIN listings l ON f.listing_id = l.id + JOIN animals a ON l.animal_id = a.id + WHERE f.user_id = $1 AND f.deleted = FALSE AND l.deleted = FALSE + ORDER BY f.created_at DESC + `; + const result = await pool.query(queryText, [userId]); + res.json(result.rows); + } catch (err) { + console.error("Error fetching favorite listings:", err); + res.status(500).json({ error: "Internal server error" }); } }); diff --git a/routes/locationRoutes.js b/routes/locationRoutes.js index e2831c3..7fc64ce 100644 --- a/routes/locationRoutes.js +++ b/routes/locationRoutes.js @@ -3,28 +3,18 @@ import pool from "../db/pool.js"; const router = express.Router(); -// Add a new location +// 1. CREATE Location router.post("/", async (req, res) => { - const { - user_id, - is_saved_address, - location_type, - country, - state, - district, - city_village, - pincode, - lat, - lng, - source_type, - source_confidence, - } = req.body; - + const client = await pool.connect(); try { - const insertQuery = - "INSERT INTO locations (user_id, is_saved_address, location_type, country, state, district, city_village, pincode, lat, lng, source_type, source_confidence) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING *"; - const values = [ + await client.query("BEGIN"); + + const { user_id, + lat, + lng, + source_type, + // Location Details is_saved_address, location_type, country, @@ -32,48 +22,106 @@ router.post("/", async (req, res) => { district, city_village, pincode, - lat, - lng, - source_type, - source_confidence, - ]; + } = req.body; + + // 1. Insert into locations + const insertLocationQuery = ` + INSERT INTO locations (user_id, lat, lng, source_type) + VALUES ($1, $2, $3, $4) + RETURNING id, user_id, lat, lng, source_type, created_at + `; + const locationValues = [user_id, lat, lng, source_type || "manual"]; + const locationResult = await client.query(insertLocationQuery, locationValues); + const location = locationResult.rows[0]; + + // 2. Insert into location_details + const insertDetailsQuery = ` + INSERT INTO location_details ( + location_id, is_saved_address, location_type, + country, state, district, city_village, pincode + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + RETURNING * + `; + const detailsValues = [ + location.id, + is_saved_address || false, + location_type || "other", + country, + state, + district, + city_village, + pincode, + ]; + const detailsResult = await client.query(insertDetailsQuery, detailsValues); + + await client.query("COMMIT"); - const result = await pool.query(insertQuery, values); res.status(201).json({ - message: "Location added successfully", - location: result.rows[0], - }); - } catch (error) { - console.error("Error adding location:", error); - res.status(500).json({ - error: "Internal server error adding location", + ...location, + details: detailsResult.rows[0], }); + } catch (err) { + await client.query("ROLLBACK"); + console.error("Error creating location:", err); + res.status(500).json({ error: "Internal server error" }); + } finally { + client.release(); } }); -// Update a location by ID -router.put("/:id", async (req, res) => { - const locationId = req.params.id; - const { - user_id, - is_saved_address, - location_type, - country, - state, - district, - city_village, - pincode, - lat, - lng, - source_type, - source_confidence, - } = req.body; - +// 2. GET User Locations +router.get("/user/:userId", async (req, res) => { try { - const updateQuery = - "UPDATE locations SET user_id = $1, is_saved_address = $2, location_type = $3, country = $4, state = $5, district = $6, city_village = $7, pincode = $8, lat = $9, lng = $10, source_type = $11, source_confidence = $12 WHERE id = $13 RETURNING *"; - const values = [ - user_id, + const { userId } = req.params; + const queryText = ` + SELECT l.*, row_to_json(ld) as details + FROM locations l + LEFT JOIN location_details ld ON l.id = ld.location_id + WHERE l.user_id = $1 AND l.deleted = FALSE + ORDER BY l.created_at DESC + `; + const result = await pool.query(queryText, [userId]); + res.json(result.rows); + } catch (err) { + console.error("Error fetching user locations:", err); + res.status(500).json({ error: "Internal server error" }); + } +}); + +// 3. GET Single Location +router.get("/:id", async (req, res) => { + try { + const { id } = req.params; + const queryText = ` + SELECT l.*, row_to_json(ld) as details + FROM locations l + LEFT JOIN location_details ld ON l.id = ld.location_id + WHERE l.id = $1 AND l.deleted = FALSE + `; + const result = await pool.query(queryText, [id]); + + if (result.rows.length === 0) { + return res.status(404).json({ error: "Location not found" }); + } + + res.json(result.rows[0]); + } catch (err) { + console.error("Error fetching location:", err); + res.status(500).json({ error: "Internal server error" }); + } +}); + +// 4. UPDATE Location +router.put("/:id", async (req, res) => { + const client = await pool.connect(); + try { + const { id } = req.params; + const { + lat, + lng, + source_type, + // Location Details is_saved_address, location_type, country, @@ -81,119 +129,92 @@ router.put("/:id", async (req, res) => { district, city_village, pincode, - lat, - lng, - source_type, - source_confidence, - locationId, - ]; + } = req.body; - const result = await pool.query(updateQuery, values); - if (result.rows.length === 0) { + await client.query("BEGIN"); + + // 1. Update locations + const updateLocationQuery = ` + UPDATE locations + SET lat = COALESCE($1, lat), + lng = COALESCE($2, lng), + source_type = COALESCE($3, source_type) + WHERE id = $4 AND deleted = FALSE + RETURNING * + `; + const locationResult = await client.query(updateLocationQuery, [lat, lng, source_type, id]); + + if (locationResult.rows.length === 0) { + await client.query("ROLLBACK"); return res.status(404).json({ error: "Location not found" }); } - res.status(200).json({ - message: "Location updated successfully", - location: result.rows[0], - }); - } catch (error) { - console.error("Error updating location:", error); - res.status(500).json({ - error: "Internal server error updating location", + // 2. Update location_details + // Note: location_details might not exist depending on legacy data, so we use ON CONFLICT or just UPDATE/INSERT logic. + // For simplicity and assuming details always exist creation: + const updateDetailsQuery = ` + UPDATE location_details + SET is_saved_address = COALESCE($1, is_saved_address), + location_type = COALESCE($2, location_type), + country = COALESCE($3, country), + state = COALESCE($4, state), + district = COALESCE($5, district), + city_village = COALESCE($6, city_village), + pincode = COALESCE($7, pincode) + WHERE location_id = $8 + RETURNING * + `; + const detailsResult = await client.query(updateDetailsQuery, [ + is_saved_address, + location_type, + country, + state, + district, + city_village, + pincode, + id, + ]); + + await client.query("COMMIT"); + + res.json({ + ...locationResult.rows[0], + details: detailsResult.rows[0] || null, }); + } catch (err) { + await client.query("ROLLBACK"); + console.error("Error updating location:", err); + res.status(500).json({ error: "Internal server error" }); + } finally { + client.release(); } }); -// Get all locations for a user -router.get("/user/:user_id", async (req, res) => { - const userId = req.params.user_id; - try { - const result = await pool.query( - "SELECT * FROM locations WHERE user_id = $1 ORDER BY created_at DESC", - [userId] - ); - res.status(200).json(result.rows); - } catch (error) { - console.error("Error fetching user's locations:", error); - res.status(500).json({ - error: "Internal server error fetching user's locations", - }); - } -}); - -// TODO: Remove this route later -// Get all users -router.get("/users", async (req, res) => { - try { - const result = await pool.query( - "SELECT * FROM users ORDER BY created_at DESC" - ); - res.status(200).json(result.rows); - } catch (error) { - console.error("Error fetching all users:", error); - res.status(500).json({ - error: "Internal server error fetching users", - }); - } -}); - -// Get all locations -router.get("/", async (req, res) => { - try { - const result = await pool.query( - "SELECT * FROM locations ORDER BY created_at DESC" - ); - res.status(200).json(result.rows); - } catch (error) { - console.error("Error fetching all locations:", error); - res.status(500).json({ - error: "Internal server error fetching locations", - }); - } -}); - -// Get location by ID -router.get("/:id", async (req, res) => { - const locationId = req.params.id; - - try { - const selectQuery = "SELECT * FROM locations WHERE id = $1"; - const result = await pool.query(selectQuery, [locationId]); - - if (result.rows.length === 0) { - return res.status(404).json({ error: "Location not found" }); - } - - res.status(200).json(result.rows[0]); - } catch (error) { - console.error("Error fetching location:", error); - res.status(500).json({ - error: "Internal server error fetching location", - }); - } -}); - -// Delete a location by ID +// 5. DELETE Location router.delete("/:id", async (req, res) => { - const locationId = req.params.id; try { - const deleteQuery = "DELETE FROM locations WHERE id = $1 RETURNING *"; - const result = await pool.query(deleteQuery, [locationId]); + const { id } = req.params; + const queryText = ` + UPDATE locations + SET deleted = TRUE + WHERE id = $1 + RETURNING id + `; + const result = await pool.query(queryText, [id]); if (result.rows.length === 0) { return res.status(404).json({ error: "Location not found" }); } - res.status(200).json({ - message: "Location deleted successfully", - location: result.rows[0], - }); - } catch (error) { - console.error("Error deleting location:", error); - res.status(500).json({ - error: "Internal server error deleting location", - }); + // Optionally mark details as deleted if they had a deleted column, but they don't seem to based on schema view earlier? + // Checking schema: location_details has 'deleted'. + + await pool.query("UPDATE location_details SET deleted = TRUE WHERE location_id = $1", [id]); + + res.json({ message: "Location deleted successfully" }); + } catch (err) { + console.error("Error deleting location:", err); + res.status(500).json({ error: "Internal server error" }); } });