Skip to content

Commit 590a533

Browse files
committed
Implemented Admin Page Apartment Editing UI\
\ - Implemented apartment information edit endpoint - Implemented create new apartment endpoint - Implemented admin page apartment pagination, sorting, and editing functionalities
1 parent 1f98ab2 commit 590a533

File tree

2 files changed

+629
-51
lines changed

2 files changed

+629
-51
lines changed

backend/src/app.ts

Lines changed: 255 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ import { admins } from '../../frontend/src/constants/HomeConsts';
3434
const cuaptsEmail = process.env.CUAPTS_EMAIL;
3535
const 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
3841
const reviewCollection = db.collection('reviews');
3942
const 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;
14311681
const LANDMARKS = {
14321682
eng_quad: '42.4445,-76.4836', // Duffield Hall
14331683
ag_quad: '42.4489,-76.4780', // Mann Library

0 commit comments

Comments
 (0)