Skip to content
Merged
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
18 changes: 2 additions & 16 deletions backend/package.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,6 @@
{
"name": "backend",
"version": "1.1.0",
"authors": [
{
"name": "Jonathan Gian",
"email": "[email protected]"
},
{
"name": "Next person",
"email": "[email protected]"
}
],
"description": "A Storage and Booking Backend run with NestJS and Supabase",
"private": true,
"authors": [
{
"name": "Athina Kantis",
Expand Down Expand Up @@ -40,27 +28,25 @@
"url": "https://github.com/JonathanGian"
}
],
"description": "A Storage and Booking Backend run with NestJS and Supabase",
"private": true,
"scripts": {
"build": "nest build",
"postbuild": "echo \"✅ Backend build completed successfully!\" && echo \"📁 Backend dist contents:\" && ls -la dist/backend || ls -la dist",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",

"start": "node -r tsconfig-paths/register dist/backend/src/main.js",
"start:watch": "nest start --watch -r tsconfig-paths/register",
"start:dev": "nodemon --watch src --ext ts --exec \"ts-node -r tsconfig-paths/register src/main.ts\"",
"start:local": "env-cmd -f ../supabase-local.env npm run start:watch",
"start:debug": "nest start --debug --watch -r tsconfig-paths/register",
"start:prod": "node -r tsconfig-paths/register dist/backend/src/main.js",

"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" ",
"lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",

"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",

"email:dev": "email dev -p 3001",
"generate:types": "npx supabase gen types typescript --project-id rcbddkhvysexkvgqpcud > src/types/supabase.types.ts"
},
Expand Down
26 changes: 21 additions & 5 deletions backend/src/modules/booking/booking.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,22 @@ export class BookingController {
) {
const pageNumber = parseInt(page, 10);
const limitNumber = parseInt(limit, 10);
const supabase = req.supabase;
const activeOrgId = req.headers["x-org-id"] as string;
const activeRole = req.headers["x-role-name"] as string;
const userId = req.user?.id;
if (!activeOrgId || !activeRole) {
throw new BadRequestException("Organization context is required");
}
if (!userId) {
throw new BadRequestException("Valid user ID is required");
}
return this.bookingService.getUserBookings(
req,
supabase,
pageNumber,
limitNumber,
activeOrgId,
activeRole,
userId,
);
}

Expand Down Expand Up @@ -164,7 +174,6 @@ export class BookingController {
* @param limit - Number of items per page (default: 10)
* @returns Paginated list of the user's bookings
*/
//TODO: limit to activeContext organization
@Get("user/:userId")
@Roles(["storage_manager", "tenant_admin"], {
match: "any",
Expand All @@ -178,15 +187,22 @@ export class BookingController {
) {
const pageNumber = parseInt(page, 10);
const limitNumber = parseInt(limit, 10);
const activeOrgId = req.headers["x-org-id"] as string;
const activeRole = req.headers["x-role-name"] as string;
if (!activeOrgId || !activeRole) {
throw new BadRequestException("Organization context is required");
}
if (!userId) {
throw new UnauthorizedException("User ID is required");
}
const supabase = req.supabase;

return this.bookingService.getUserBookings(
req,
supabase,
pageNumber,
limitNumber,
activeOrgId,
activeRole,
userId,
);
}

Expand Down
14 changes: 4 additions & 10 deletions backend/src/modules/booking/booking.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,20 +177,14 @@ export class BookingService {
*/
async getUserBookings(
req: AuthRequest,
supabase: SupabaseClient<Database>,
page: number,
limit: number,
activeOrgId?: string,
activeRole?: string,
userId?,
) {
const { from, to } = getPaginationRange(page, limit);

// Extract user ID and role from the request
const userId = req.user?.id;
const activeRole = req.headers["x-role-name"] as string;
const activeOrgId = req.headers["x-org-id"] as string;

if (!userId) {
throw new BadRequestException("Valid user ID is required");
}
const supabase = req.supabase;

// Restrict User to only their own bookings
if (activeRole === "user") {
Expand Down
24 changes: 24 additions & 0 deletions frontend/src/api/services/bookings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,30 @@ export const bookingsApi = {
return api.get(`/bookings/user/${userId}?${params.toString()}`);
},

/**
* Get bookings for the current authenticated user
* @param activeOrgId - Active organization ID
* @param activeRole - Active role of the user
* @param userId - User ID to fetch bookings for
* @param page - Page number for pagination (default: 1)
* @param limit - Number of items per page (default: 10)
* @returns Promise with the user's bookings
*/
getOwnBookings: async (
activeOrgId: string,
activeRole: string,
userId: string,
page: number = 1,
limit: number = 10,
): Promise<ApiResponse<BookingPreview>> => {
const params = new URLSearchParams();
params.append("page", page.toString());
params.append("limit", limit.toString());
return api.get(
`/bookings/my?activeOrgId=${activeOrgId}&activeRole=${activeRole}&userId=${userId}&${params.toString()}`,
);
},

/**
* Get bookings for a specific user
* @param user_id - User ID of booking
Expand Down
80 changes: 68 additions & 12 deletions frontend/src/components/MyBookings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
selectCurrentBookingLoading,
getBookingItems,
clearCurrentBookingItems,
clearUserBookings,
getOwnBookings,
} from "@/store/slices/bookingsSlice";
import { selectSelectedUser } from "@/store/slices/usersSlice";
import {
Expand Down Expand Up @@ -55,6 +57,7 @@ import { StatusBadge } from "./StatusBadge";
import InlineTimeframePicker from "./InlineTimeframeSelector";
import { itemsApi } from "@/api/services/items";
import Spinner from "./Spinner";
import { useRoles } from "../hooks/useRoles";

const MyBookings = () => {
const dispatch = useAppDispatch();
Expand Down Expand Up @@ -98,6 +101,8 @@ const MyBookings = () => {
const { lang } = useLanguage();
const { formatDate: formatDateLocalized } = useFormattedDate();

const { activeContext } = useRoles();

const handleEditBooking = async (booking: BookingPreview) => {
setLoadingAvailability(true);
setShowEditModal(true);
Expand All @@ -108,6 +113,15 @@ const MyBookings = () => {
await dispatch(getBookingItems(booking.id));
};

useEffect(() => {
if (
activeContext.roleName !== "user" &&
activeContext.roleName !== "requester"
) {
dispatch(clearUserBookings());
}
}, [activeContext.roleName, dispatch]);

useEffect(() => {
if (
selectedBooking &&
Expand Down Expand Up @@ -154,13 +168,43 @@ const MyBookings = () => {
return;
}

if (user && !hasFetchedBookings) {
void dispatch(getUserBookings({ user_id: user.id, page: 1, limit: 10 }))
const { roleName } = activeContext;
if (roleName !== "user" && roleName !== "requester") {
return;
}

if (!hasFetchedBookings || activeContext) {
const { organizationId } = activeContext;
const userId = user.id;

// Ensure organizationId is valid
if (!organizationId) {
toast.error(t.myBookings.error.invalidContext[lang]);
return;
}

void dispatch(
getOwnBookings({
page: currentPage + 1,
limit: 10,
activeOrgId: organizationId,
activeRole: roleName,
userId,
}),
)
.unwrap()
.then(() => setHasFetchedBookings(true)) // Mark as fetched on success
.catch(() => setHasFetchedBookings(true)); // Mark as fetched even on error
}
}, [dispatch, navigate, user, lang, hasFetchedBookings]); // Apply filters to bookings
}, [
dispatch,
navigate,
user,
lang,
hasFetchedBookings,
currentPage,
activeContext,
]);

// Reset to first page when filters change
useEffect(() => {
Expand Down Expand Up @@ -508,21 +552,33 @@ const MyBookings = () => {
</div>

{/* BookingPreview table or empty state */}
{bookings.length === 0 && (
{activeContext.roleName !== "user" &&
activeContext.roleName !== "requester" ? (
<div className="text-center py-8 bg-slate-50 rounded-lg">
<p className="text-lg mb-2">
{t.myBookings.emptyState.title[lang]}
{t.myBookings.error.insufficientRole[lang]}
</p>
<p className="text-muted-foreground mb-4">
{t.myBookings.emptyState.description[lang]}
{t.myBookings.error.insufficientRoleDescription[lang]}
</p>
<Button
onClick={() => navigate("/storage")}
className="bg-background text-secondary border-secondary border hover:bg-secondary hover:text-white"
>
{t.myBookings.buttons.browseItems[lang]}
</Button>
</div>
) : (
bookings.length === 0 && (
<div className="text-center py-8 bg-slate-50 rounded-lg">
<p className="text-lg mb-2">
{t.myBookings.emptyState.title[lang]}
</p>
<p className="text-muted-foreground mb-4">
{t.myBookings.emptyState.description[lang]}
</p>
<Button
onClick={() => navigate("/storage")}
className="bg-background text-secondary border-secondary border hover:bg-secondary hover:text-white"
>
{t.myBookings.buttons.browseItems[lang]}
</Button>
</div>
)
)}
{isMobile && (
<Accordion type="multiple" className="w-full space-y-2">
Expand Down
8 changes: 6 additions & 2 deletions frontend/src/pages/MyProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { toastConfirm } from "../components/ui/toastConfirm";
import MyBookings from "@/components/MyBookings";
import { CurrentUserRoles } from "@/components/Admin/Roles/CurrentUserRoles";
import ProfilePictureUploader from "@/components/ProfilePictureUploader";
import { useRoles } from "@/hooks/useRoles";

const MyProfile = () => {
const dispatch = useAppDispatch();
Expand All @@ -71,7 +72,8 @@ const MyProfile = () => {
userAddresses || [],
);
const [showAddAddressForm, setShowAddAddressForm] = useState(false);
// const profileImage = profilePlaceholder;

const { activeContext } = useRoles();

useEffect(() => {
if (selectedUser) {
Expand Down Expand Up @@ -218,7 +220,9 @@ const MyProfile = () => {
{t.myProfile.tabs.userDetails[lang]}
</TabsTrigger>
<TabsTrigger value="bookings">
{t.myProfile.tabs.bookings[lang]}
{activeContext.roleName === "user"
? t.myProfile.tabs.bookings.myBookings[lang]
: t.myProfile.tabs.bookings.orgBookings[lang]}
</TabsTrigger>
</TabsList>

Expand Down
Loading
Loading