@@ -34,6 +34,9 @@ import { admins } from '../../frontend/src/constants/HomeConsts';
3434const cuaptsEmail = process . env . CUAPTS_EMAIL ;
3535const cuaptsEmailPassword = process . env . CUAPTS_EMAIL_APP_PASSWORD ;
3636
37+ // Google Maps API key
38+ const { REACT_APP_MAPS_API_KEY } = process . env ;
39+
3740// Collections in the Firestore database
3841const reviewCollection = db . collection ( 'reviews' ) ;
3942const landlordCollection = db . collection ( 'landlords' ) ;
@@ -495,10 +498,10 @@ app.get('/api/search-with-query-and-filters', async (req, res) => {
495498 // Extract all query parameters
496499 const query = req . query . q as string ;
497500 const locations = req . query . locations as string ;
498- const minPrice = req . query . minPrice ? parseInt ( req . query . minPrice as string , 10 ) : null ;
499- const maxPrice = req . query . maxPrice ? parseInt ( req . query . maxPrice as string , 10 ) : null ;
500- const bedrooms = req . query . bedrooms ? parseInt ( req . query . bedrooms as string , 10 ) : null ;
501- const bathrooms = req . query . bathrooms ? parseInt ( req . query . bathrooms as string , 10 ) : null ;
501+ // const minPrice = req.query.minPrice ? parseInt(req.query.minPrice as string, 10) : null;
502+ // const maxPrice = req.query.maxPrice ? parseInt(req.query.maxPrice as string, 10) : null;
503+ // const bedrooms = req.query.bedrooms ? parseInt(req.query.bedrooms as string, 10) : null;
504+ // const bathrooms = req.query.bathrooms ? parseInt(req.query.bathrooms as string, 10) : null;
502505 const size = req . query . size ? parseInt ( req . query . size as string , 10 ) : null ;
503506 const sortBy = req . query . sortBy || 'numReviews' ;
504507
@@ -1302,6 +1305,254 @@ app.post('/api/add-pending-building', authenticate, async (req, res) => {
13021305 }
13031306} ) ;
13041307
1308+ /**
1309+ * Update Apartment Information - Updates the information of an existing apartment.
1310+ *
1311+ * @remarks
1312+ * This endpoint allows admins to update apartment information including name, address,
1313+ * landlord, amenities, photos, and other details. Only admins can access this endpoint.
1314+ *
1315+ * @route PUT /api/admin/update-apartment/:apartmentId
1316+ *
1317+ * @input {string} req.params.apartmentId - The ID of the apartment to update
1318+ * @input {Partial<Apartment>} req.body - The updated apartment information
1319+ *
1320+ * @status
1321+ * - 200: Successfully updated apartment information
1322+ * - 400: Invalid apartment data or missing required fields
1323+ * - 401: Authentication failed
1324+ * - 403: Unauthorized - Admin access required
1325+ * - 404: Apartment not found
1326+ * - 500: Server error while updating apartment
1327+ */
1328+ app . put ( '/api/admin/update-apartment/:apartmentId' , authenticate , async ( req , res ) => {
1329+ if ( ! req . user ) throw new Error ( 'Not authenticated' ) ;
1330+
1331+ const { email } = req . user ;
1332+ const isAdmin = email && admins . includes ( email ) ;
1333+
1334+ if ( ! isAdmin ) {
1335+ res . status ( 403 ) . send ( 'Unauthorized: Admin access required' ) ;
1336+ return ;
1337+ }
1338+
1339+ try {
1340+ const { apartmentId } = req . params ;
1341+ const updatedApartmentData = req . body as Partial < Apartment > ;
1342+
1343+ // Check if apartment exists
1344+ const apartmentDoc = buildingsCollection . doc ( apartmentId ) ;
1345+ const apartmentSnapshot = await apartmentDoc . get ( ) ;
1346+
1347+ if ( ! apartmentSnapshot . exists ) {
1348+ res . status ( 404 ) . send ( 'Apartment not found' ) ;
1349+ return ;
1350+ }
1351+
1352+ // Validate required fields if they're being updated
1353+ if ( updatedApartmentData . name !== undefined && updatedApartmentData . name === '' ) {
1354+ res . status ( 400 ) . send ( 'Error: Apartment name cannot be empty' ) ;
1355+ return ;
1356+ }
1357+
1358+ if ( updatedApartmentData . address !== undefined && updatedApartmentData . address === '' ) {
1359+ res . status ( 400 ) . send ( 'Error: Apartment address cannot be empty' ) ;
1360+ return ;
1361+ }
1362+
1363+ // Update the apartment document
1364+ await apartmentDoc . update ( updatedApartmentData ) ;
1365+
1366+ res . status ( 200 ) . send ( 'Apartment updated successfully' ) ;
1367+ } catch ( err ) {
1368+ console . error ( err ) ;
1369+ res . status ( 500 ) . send ( 'Error updating apartment' ) ;
1370+ }
1371+ } ) ;
1372+
1373+ /**
1374+ * geocodeAddress – Converts an address to latitude and longitude coordinates using Google Geocoding API.
1375+ *
1376+ * @remarks
1377+ * Makes an HTTP request to the Google Geocoding API to convert an address string into
1378+ * precise latitude and longitude coordinates.
1379+ *
1380+ * @param {string } address - The address to geocode
1381+ * @return {Promise<{latitude: number, longitude: number}> } - Object containing latitude and longitude
1382+ */
1383+ async function geocodeAddress ( address : string ) : Promise < { latitude : number ; longitude : number } > {
1384+ const response = await axios . get (
1385+ `https://maps.googleapis.com/maps/api/geocode/json?address=${ encodeURIComponent (
1386+ address
1387+ ) } &key=${ REACT_APP_MAPS_API_KEY } `
1388+ ) ;
1389+
1390+ if ( response . data . status !== 'OK' || ! response . data . results . length ) {
1391+ throw new Error ( 'Geocoding failed: Invalid address or no results found' ) ;
1392+ }
1393+
1394+ const { location} = response . data . results [ 0 ] . geometry ;
1395+ return {
1396+ latitude : location . lat ,
1397+ longitude : location . lng ,
1398+ } ;
1399+ }
1400+
1401+ /**
1402+ * Add New Apartment - Creates a new apartment with duplicate checking and distance calculation.
1403+ *
1404+ * @remarks
1405+ * This endpoint allows admins to create new apartments. It includes a multi-step process:
1406+ * 1. Validates input data and checks for existing apartments at the same coordinates
1407+ * 2. Calculates latitude/longitude and distance to campus using Google Maps API
1408+ * 3. Returns preliminary data for admin confirmation
1409+ * 4. Creates the apartment in the database after confirmation
1410+ *
1411+ * @route POST /api/admin/add-apartment
1412+ *
1413+ * @input {object} req.body - Apartment creation data containing:
1414+ * - name: string (required)
1415+ * - address: string (required)
1416+ * - landlordId: string (required)
1417+ * - numBaths: number (optional)
1418+ * - numBeds: number (optional)
1419+ * - photos: string[] (optional)
1420+ * - area: 'COLLEGETOWN' | 'WEST' | 'NORTH' | 'DOWNTOWN' | 'OTHER' (required)
1421+ * - confirm: boolean (optional) - if true, creates the apartment; if false, returns preliminary data
1422+ *
1423+ * @status
1424+ * - 200: Successfully created apartment (when confirm=true)
1425+ * - 201: Preliminary data returned for confirmation (when confirm=false)
1426+ * - 400: Invalid apartment data, missing required fields, or duplicate apartment found
1427+ * - 401: Authentication failed
1428+ * - 403: Unauthorized - Admin access required
1429+ * - 500: Server error while processing request
1430+ */
1431+ app . post ( '/api/admin/add-apartment' , authenticate , async ( req , res ) => {
1432+ if ( ! req . user ) throw new Error ( 'Not authenticated' ) ;
1433+
1434+ const { email } = req . user ;
1435+ const isAdmin = email && admins . includes ( email ) ;
1436+
1437+ if ( ! isAdmin ) {
1438+ res . status ( 403 ) . send ( 'Unauthorized: Admin access required' ) ;
1439+ return ;
1440+ }
1441+
1442+ try {
1443+ const {
1444+ name,
1445+ address,
1446+ landlordId,
1447+ numBaths,
1448+ numBeds,
1449+ photos = [ ] ,
1450+ area,
1451+ confirm = false ,
1452+ } = req . body ;
1453+
1454+ // Validate required fields
1455+ if ( ! name || ! address || ! landlordId || ! area ) {
1456+ res . status ( 400 ) . send ( 'Error: Missing required fields (name, address, landlordId, area)' ) ;
1457+ return ;
1458+ }
1459+
1460+ // Validate area is one of the allowed values
1461+ const validAreas = [ 'COLLEGETOWN' , 'WEST' , 'NORTH' , 'DOWNTOWN' , 'OTHER' ] ;
1462+ if ( ! validAreas . includes ( area ) ) {
1463+ res
1464+ . status ( 400 )
1465+ . send ( 'Error: Invalid area. Must be one of: COLLEGETOWN, WEST, NORTH, DOWNTOWN, OTHER' ) ;
1466+ return ;
1467+ }
1468+
1469+ // Check if landlord exists
1470+ const landlordDoc = landlordCollection . doc ( landlordId ) ;
1471+ const landlordSnapshot = await landlordDoc . get ( ) ;
1472+ if ( ! landlordSnapshot . exists ) {
1473+ res . status ( 400 ) . send ( 'Error: Landlord not found' ) ;
1474+ return ;
1475+ }
1476+
1477+ // Geocode the address to get coordinates
1478+ const coordinates = await geocodeAddress ( address ) ;
1479+ const { latitude, longitude } = coordinates ;
1480+
1481+ // Calculate travel times using the coordinates
1482+ const { protocol } = req ;
1483+ const host = req . get ( 'host' ) ;
1484+ const baseUrl = `${ protocol } ://${ host } ` ;
1485+
1486+ const travelTimesResponse = await axios . post ( `${ baseUrl } /api/calculate-travel-times` , {
1487+ origin : `${ latitude } ,${ longitude } ` ,
1488+ } ) ;
1489+
1490+ const travelTimes = travelTimesResponse . data ;
1491+ const distanceToCampus = travelTimes . hoPlazaWalking ;
1492+
1493+ // Check for existing apartments at the same coordinates (with small tolerance)
1494+ const tolerance = 0.0001 ; // Approximately 11 meters
1495+ const existingApartments = await buildingsCollection
1496+ . where ( 'latitude' , '>=' , latitude - tolerance )
1497+ . where ( 'latitude' , '<=' , latitude + tolerance )
1498+ . where ( 'longitude' , '>=' , longitude - tolerance )
1499+ . where ( 'longitude' , '<=' , longitude + tolerance )
1500+ . get ( ) ;
1501+
1502+ if ( ! existingApartments . empty ) {
1503+ res . status ( 400 ) . send ( 'Error: An apartment already exists at this location' ) ;
1504+ return ;
1505+ }
1506+
1507+ // Prepare apartment data
1508+ const apartmentData : Apartment = {
1509+ name,
1510+ address,
1511+ landlordId,
1512+ numBaths : numBaths || null ,
1513+ numBeds : numBeds || null ,
1514+ photos,
1515+ area,
1516+ latitude,
1517+ longitude,
1518+ price : 0 , // Default price, can be updated later
1519+ distanceToCampus,
1520+ } ;
1521+
1522+ if ( ! confirm ) {
1523+ // Return preliminary data for admin confirmation
1524+ res . status ( 201 ) . json ( {
1525+ message : 'Preliminary data calculated. Please confirm to create apartment.' ,
1526+ apartmentData,
1527+ travelTimes,
1528+ coordinates : { latitude, longitude } ,
1529+ } ) ;
1530+ return ;
1531+ }
1532+
1533+ // Create the apartment in the database
1534+ const apartmentDoc = buildingsCollection . doc ( ) ;
1535+ await apartmentDoc . set ( apartmentData ) ;
1536+
1537+ // Update landlord's properties list
1538+ const landlordData = landlordSnapshot . data ( ) ;
1539+ const updatedProperties = [ ...( landlordData ?. properties || [ ] ) , apartmentDoc . id ] ;
1540+ await landlordDoc . update ( { properties : updatedProperties } ) ;
1541+
1542+ // Store travel times for the new apartment
1543+ await travelTimesCollection . doc ( apartmentDoc . id ) . set ( travelTimes ) ;
1544+
1545+ res . status ( 200 ) . json ( {
1546+ message : 'Apartment created successfully' ,
1547+ apartmentId : apartmentDoc . id ,
1548+ apartmentData,
1549+ } ) ;
1550+ } catch ( err ) {
1551+ console . error ( err ) ;
1552+ res . status ( 500 ) . send ( 'Error creating apartment' ) ;
1553+ }
1554+ } ) ;
1555+
13051556/**
13061557 * Update Pending Building Status - Updates the status of a pending building report.
13071558 *
@@ -1427,7 +1678,6 @@ app.put(
14271678 }
14281679) ;
14291680
1430- const { REACT_APP_MAPS_API_KEY } = process . env ;
14311681const LANDMARKS = {
14321682 eng_quad : '42.4445,-76.4836' , // Duffield Hall
14331683 ag_quad : '42.4489,-76.4780' , // Mann Library
0 commit comments