Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,5 @@ See docs: https://angular.dev/guide/templates/control-flow
7. Whenever creating new Angular components and services, please make sure to create them using Angular CLI. Make sure that you walk into the Deliveroo dash frontend directory and then you run `ng generate service <service-name>` or `ng generate service <component-name>` or other types of the things that you can generate using ng CLI.

8. Any HTTP requests should be performed by specialized Angular services. So for each entity, there should be a separate Angular service that includes only methods which perform the requests. For example, if there is a `Vehicle` entity, there should be a corresponding `VehicleHTTP.service.ts` file. Make sure that such service is created using NG CLI.

9. Avoid creating tests, "spec" files, if there are not gonna be any meaningful tests inside.
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ globs:
alwaysApply: true
---
1. Whenever using the data loaded from a REST API, first I want this interface to be created in a separate file that would have a `*.model.ts` extension and the filename and then it should have a `DTO` suffix in the name. So for instance, if my entity is called vehicle, then it should be stored in a `vehicle.model.ts` file and its name should be `VehicleDTO`.

2. Avoid using simple console logs or standard outputs for logging - use designated loggers instead.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ This project contains two separate `package.json` files:
- @deliveroo-frontend/package.json

**When adding, removing, or updating dependencies:**
- Always ensure you are in the correct directory (`deliveroo-backend` or `feliveroo-frontend`) before running any package manager commands (e.g., `npm install`, `yarn add`).
- Always ensure you are in the correct directory (`deliveroo-backend` or `deliveroo-frontend`) before running any package manager commands (e.g., `npm install`, `yarn add`).
- Only modify the `package.json` file relevant to the part of the project you are working on.
- Do not make changes to both `package.json` files unless explicitly instructed to do so.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
export type EmployeeRole = 'Driver' | 'Dispatcher' | 'Manager';
export type EmployeeStatus = 'Active' | 'On Leave' | 'Inactive';

export interface EmployeeDTO {
id: string;
employeeIdNumber?: string;
firstName: string;
lastName: string;
dateOfBirth?: string;
contactPhoneNumber?: string;
contactEmail: string;
employeeRole: EmployeeRole;
employeeStatus: EmployeeStatus;
driverLicenseNumber?: string;
licenseExpirationDate?: string;
streetAddressLine1?: string;
streetAddressLine2?: string;
city?: string;
stateProvince?: string;
postalCode?: string;
createdAt?: string;
updatedAt?: string;
}

const dbToDtoRoleMap: Record<string, EmployeeRole> = {
'driver': 'Driver',
'dispatcher': 'Dispatcher',
'manager': 'Manager',
};

const dbToDtoStatusMap: Record<string, EmployeeStatus> = {
'active': 'Active',
'on leave': 'On Leave',
'inactive': 'Inactive',
};

export function mapEmployeeRowToDTO(employee: any): EmployeeDTO {
return {
id: employee.id,
employeeIdNumber: employee.employee_id_number,
firstName: employee.first_name,
lastName: employee.last_name,
dateOfBirth: employee.date_of_birth,
contactPhoneNumber: employee.contact_phone_number,
contactEmail: employee.contact_email,
employeeRole: dbToDtoRoleMap[employee.employee_role] || 'Driver',
employeeStatus: dbToDtoStatusMap[employee.employee_status] || 'Active',
driverLicenseNumber: employee.driver_license_number,
licenseExpirationDate: employee.license_expiration_date,
streetAddressLine1: employee.street_address_line1,
streetAddressLine2: employee.street_address_line2,
city: employee.city,
stateProvince: employee.state_province,
postalCode: employee.postal_code,
createdAt: employee.created_at,
updatedAt: employee.updated_at
};
}
68 changes: 58 additions & 10 deletions webinar-04-fullstack/deliveroo/deliveroo-backend/src/queries.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pool from './database';
import { EmployeeDTO } from './employee.model';

/**
* Returns a list of all vehicles.
Expand All @@ -13,7 +14,7 @@ export async function getAllVehiclesWithDriver() {
const result = await pool.query(`
SELECT
v.*,
e.name AS driver_name
CONCAT(e.first_name, ' ', e.last_name) AS driver_name
FROM vehicles v
LEFT JOIN vehicle_employee ve ON v.id = ve.vehicle_id
LEFT JOIN employees e ON ve.employee_id = e.id
Expand All @@ -31,6 +32,53 @@ export async function getAllEmployees() {
return result.rows;
}

/**
* Creates a new employee in the database.
*/
export async function createEmployee(employeeData: {
employeeId?: string;
firstName: string;
lastName: string;
dateOfBirth?: string;
phoneNumber?: string;
email: string;
role: string;
licenseNumber?: string;
licenseExpiration?: string;
addressLine1?: string;
addressLine2?: string;
city?: string;
state?: string;
postalCode?: string;
}) {
const result = await pool.query(
`INSERT INTO employees(
employee_id_number, first_name, last_name, date_of_birth,
contact_phone_number, contact_email, employee_role, employee_status,
driver_license_number, license_expiration_date, street_address_line1,
street_address_line2, city, state_province, postal_code
) VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING *`,
[
employeeData.employeeId || null,
employeeData.firstName,
employeeData.lastName,
employeeData.dateOfBirth || null,
employeeData.phoneNumber || null,
employeeData.email,
employeeData.role.toLowerCase(),
'active', // Default status
employeeData.licenseNumber || null,
employeeData.licenseExpiration || null,
employeeData.addressLine1 || null,
employeeData.addressLine2 || null,
employeeData.city || null,
employeeData.state || null,
employeeData.postalCode || null,
]
);
return result.rows[0];
}

/**
* Lists all drivers currently assigned to at least one vehicle, including assignment details.
* Returns driver info, vehicle license plate, and assignment period.
Expand All @@ -39,7 +87,7 @@ export async function getDriversWithVehicles() {
const result = await pool.query(`
SELECT
e.id AS driver_id,
e.name AS driver_name,
CONCAT(e.first_name, ' ', e.last_name) AS driver_name,
v.license_plate AS vehicle,
v.last_maintenance_date,
ve.since_date AS assigned_since,
Expand All @@ -49,10 +97,10 @@ export async function getDriversWithVehicles() {
JOIN vehicle_employee ve ON e.id = ve.employee_id
JOIN vehicles v ON ve.vehicle_id = v.id
WHERE
e.role = 'driver'
e.employee_role = 'driver'
AND (ve.since_date <= CURRENT_DATE AND (ve.planned_leave_date IS NULL OR ve.planned_leave_date >= CURRENT_DATE))
ORDER BY
e.name, ve.since_date;
CONCAT(e.first_name, ' ', e.last_name), ve.since_date;
`);
return result.rows;
}
Expand All @@ -65,19 +113,19 @@ export async function getDriversWithoutVehicles() {
const result = await pool.query(`
SELECT
e.id AS driver_id,
e.name AS driver_name
CONCAT(e.first_name, ' ', e.last_name) AS driver_name
FROM
employees e
WHERE
e.role = 'driver'
e.employee_role = 'driver'
AND NOT EXISTS (
SELECT 1
FROM vehicle_employee ve
WHERE ve.employee_id = e.id
AND (ve.since_date <= CURRENT_DATE AND (ve.planned_leave_date IS NULL OR ve.planned_leave_date >= CURRENT_DATE))
)
ORDER BY
e.name;
CONCAT(e.first_name, ' ', e.last_name);
`);
return result.rows;
}
Expand All @@ -89,7 +137,7 @@ export async function getTotalDrivers() {
const result = await pool.query(`
SELECT COUNT(*) AS total_drivers
FROM employees
WHERE role = 'driver';
WHERE employee_role = 'driver';
`);
return Number(result.rows[0].total_drivers);
}
Expand All @@ -102,7 +150,7 @@ export async function getDriversWithVehiclesCount() {
SELECT COUNT(DISTINCT e.id) AS drivers_with_vehicles
FROM employees e
JOIN vehicle_employee ve ON e.id = ve.employee_id
WHERE e.role = 'driver'
WHERE e.employee_role = 'driver'
AND (ve.since_date <= CURRENT_DATE AND (ve.planned_leave_date IS NULL OR ve.planned_leave_date >= CURRENT_DATE));
`);
return Number(result.rows[0].drivers_with_vehicles);
Expand All @@ -115,7 +163,7 @@ export async function getDriversWithoutVehiclesCount() {
const result = await pool.query(`
SELECT COUNT(*) AS drivers_without_vehicles
FROM employees e
WHERE role = 'driver'
WHERE employee_role = 'driver'
AND NOT EXISTS (
SELECT 1
FROM vehicle_employee ve
Expand Down
64 changes: 61 additions & 3 deletions webinar-04-fullstack/deliveroo/deliveroo-backend/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import { invokeMemoryLeak } from './memory-leak';
import { assertEnvVars } from './env';
import pool from './database';
import redisClient from './redis';
import { getAllEmployees, getAllVehiclesWithDriver } from './queries';
import { getAllEmployees, getAllVehiclesWithDriver, createEmployee } from './queries';
import { mapVehicleRowsToDTOs } from './vehicle.model';
import { mapEmployeeRowToDTO, EmployeeDTO, EmployeeRole } from './employee.model';
import logger from './logger';

const app = express();
Expand Down Expand Up @@ -37,6 +38,7 @@ const corsOptions = {
allowedHeaders: ['Content-Type', 'Authorization']
};
app.use(cors(corsOptions));
app.use(express.json()); // Enable JSON body parsing

// GET /vehicles endpoint
app.get('/vehicles', async (req: Request, res: Response): Promise<void> => {
Expand Down Expand Up @@ -70,12 +72,14 @@ app.get('/employees', async (req: Request, res: Response): Promise<void> => {
// Try to get employees from Redis cache
const cachedEmployees = await redisClient.get('employees');
if (cachedEmployees) {
logger.info('Returning employees from cache');
logger.info('Returning employees from cache!');
res.json(JSON.parse(cachedEmployees));
return;
}
// If not in cache, get from PostgreSQL
const employees = await getAllEmployees();
const employeesRaw = await getAllEmployees();
// Map to DTO with camelCase
const employees = employeesRaw.map(mapEmployeeRowToDTO);
// Store in Redis cache with expiration of 60 seconds
await redisClient.set('employees', JSON.stringify(employees), { EX: 60 });
res.json(employees);
Expand All @@ -85,6 +89,60 @@ app.get('/employees', async (req: Request, res: Response): Promise<void> => {
}
});

interface EmployeeRequestBody {
employeeId?: string;
firstName: string;
lastName: string;
dateOfBirth?: string;
phoneNumber?: string;
email: string;
role: string;
licenseNumber?: string;
licenseExpiration?: string;
addressLine1?: string;
addressLine2?: string;
city?: string;
state?: string;
postalCode?: string;
}

// POST /employees endpoint to add a new employee
app.post('/employees', async (req: Request, res: Response): Promise<void> => {
invokeMemoryLeak();
try {
const employeeData: EmployeeRequestBody = req.body;

// Validate required fields
if (!employeeData.firstName || !employeeData.lastName || !employeeData.email || !employeeData.role) {
logger.error('Missing required fields in employee creation request');
res.status(400).json({ error: 'Missing required fields: firstName, lastName, email, role' });
return;
}

// Validate role against allowed enum values
const validRoles = ['driver', 'dispatcher', 'manager'];
if (!validRoles.includes(employeeData.role.toLowerCase())) {
logger.error(`Invalid role provided: ${employeeData.role}. Must be one of: ${validRoles.join(', ')}`);
res.status(400).json({ error: `Invalid role. Must be one of: ${validRoles.join(', ')}` });
return;
}

const newEmployee = await createEmployee(employeeData);
const mappedEmployee = mapEmployeeRowToDTO(newEmployee);

logger.info('New employee added to DB:', mappedEmployee);

// Invalidate the employees cache
await redisClient.del('employees');
logger.info('Invalidated employees cache');

res.status(201).json({ message: 'Employee added successfully', employee: mappedEmployee });
} catch (err) {
logger.error('Error adding employee: ' + err, { err });
res.status(500).json({ error: 'Internal server error' });
}
});

app.get('/', (req: Request, res: Response): void => {
res.json({ status: 'Deliveroo backend is running!', timestamp: new Date() });
});
Expand Down
Loading