AE Clubs Portal
Every club in the student union needs the same three things: a date for its event, a room, and equipment from the union's warehouse. The AE Clubs Portal puts all three in one place, so a club can see what is already booked before it asks for anything.
AE CLUBS PORTAL · SYNTHETIC DATA ON LOCAL EMULATORS
Why this exists
Each club plans on its own, but they all draw on the same shared resources: one Glass Room, one warehouse of chairs, speakers and projectors, and one student union that has to approve it all. Without a common view, two clubs can book the same evening and a request can ask for equipment that is already out. The portal exists to make that shared state visible to everyone before they commit to a date.
1 # What was broken
2
3 - every club plans on its own
4 - one room, one warehouse, shared by all
5 - double bookings found too late
6 - equipment requested blind to stock
7
8 > goal: one shared picture, before anyone commits
The decisions I made
I joined a codebase other students started in 2024 and worked inside its shape: Angular with Firebase Realtime Database, so every screen updates live. My part was the material requests (quantities, filters, a stepper form), the interactive calendar, the statistics page, the documents flow with email notifications, mobile layouts, and the link to the warehouse: the request form reads stock from the AE Internal App, and handing equipment over or getting it back updates that stock. The Glass Room bookings moved from direct database writes to one callable Cloud Function, so the rules stay closed and every booking is checked on the server.
1 Angular app ──▶ Realtime Database (live)
2 │ ▲
3 │ onValueCreated → email
4 ├──▶ gerirReservaSala (callable, transaction)
5 └──▶ AE Internal App API
6 getInventory · syncInventory
7
8 // trade-off: rules closed to direct writes
The part I'm proud of
A booking is applied inside a database transaction, so two clubs racing for the same hour cannot both win: the second one re-runs against the new state and fails. Every edit carries the version it was based on, so a stale form is rejected instead of silently overwriting someone else's change. Times are read in Lisbon time on the server, which also means the hours that do not exist when the clocks change are refused.
1 // stale edits fail instead of overwriting
2 if ((anterior.versao ?? 0) !== pedido.versao)
3 throw new HttpsError('aborted', ...);
4
5 // overlap check against every active booking
6 if (inicio < outroFim && fim > outroInicio)
7 throw new HttpsError('already-exists', ...);
8
9 // runs inside ref.transaction() → no race wins twice
What changed afterwards
The clubs now plan against one calendar that shows events, room bookings and equipment pick-ups together, and the union sees requests per club and the most requested equipment as they come in. The booking rework was tested with 12 unit tests and 9 tests on Firebase emulators, including concurrency and network failures; its Cloud Function is waiting on the union's billing account before it can be deployed. What I would do differently: put the integration key behind a server call from day one, instead of shipping it in the browser bundle as the first version did.
1 ## Shipped
2
3 calendar events · rooms · equipment
4 requests live stock from the warehouse
5 bookings edit · cancel · history
6 tests 12 unit · 9 on emulators
7
8 ### What I'd do differently
9 keep integration keys off the client