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
1 change: 1 addition & 0 deletions backend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ async function bootstrap() {
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
validationError: { target: false, value: false },
}),
);

Expand Down
4 changes: 2 additions & 2 deletions backend/src/modules/booking/booking.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,14 +208,14 @@ export class BookingController {

/**
* Create a new booking.
* Accessible by users and requesters within their organization.
* Accessible by users, requesters, storage managers, and tenant admins within their organization.
* @param dto - Booking data
* @param req - Authenticated request object
* @returns Created booking
*/
//TODO: attach activeRole to the booking
@Post()
@Roles(["user", "requester"], {
@Roles(["user", "requester", "storage_manager", "tenant_admin"], {
match: "any",
sameOrg: true,
})
Expand Down
37 changes: 36 additions & 1 deletion backend/src/modules/booking/booking.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,35 @@ export class BookingService {
throw new BadRequestException("No userId found: user_id is required");
}

// 3.0. Check if user has completed required profile information
const { data: userProfile, error: profileError } = await supabase
.from("user_profiles")
.select("full_name, phone")
.eq("id", userId)
.single();

if (profileError || !userProfile) {
throw new BadRequestException("User profile not found");
}

// Require full name for booking (phone is optional but recommended)
if (!userProfile.full_name || userProfile.full_name.trim() === "") {
// Use a specific error code that frontend can catch to show the modal
const error = new BadRequestException({
message: "Profile incomplete: Full name is required to create bookings",
errorCode: "PROFILE_INCOMPLETE",
missingFields: ["full_name"],
hasPhone: !!(userProfile.phone && userProfile.phone.trim()),
});
throw error;
}

// Check for phone number and prepare warning message
let warningMessage: string | null = null;
if (!userProfile.phone || userProfile.phone.trim() === "") {
warningMessage =
"We recommend adding a phone number to your profile for easier communication about your bookings.";
}

for (const item of dto.items) {
const { item_id, quantity, start_date, end_date } = item;
Expand All @@ -369,8 +397,15 @@ export class BookingService {

// Warn for short notice (< 24h). dayDiffFromToday returns 0 for same-day future times.
if (diffDays < 1) {
warningMessage =
const shortNoticeWarning =
"Heads up: bookings made less than 24 hours in advance might not be approved in time.";

// Combine warnings if both exist
if (warningMessage) {
warningMessage = `${warningMessage} ${shortNoticeWarning}`;
} else {
warningMessage = shortNoticeWarning;
}
}

// 3.1. Check availability for requested date range
Expand Down
58 changes: 29 additions & 29 deletions common/supabase.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ export type Database = {
Functions: {
graphql: {
Args: {
extensions?: Json
operationName?: string
query?: string
variables?: Json
extensions?: Json
}
Returns: Json
}
Expand Down Expand Up @@ -1364,14 +1364,14 @@ export type Database = {
}
create_notification: {
Args: {
p_user_id: string
p_type: Database["public"]["Enums"]["notification_type"]
p_title: string
p_message?: string
p_channel?: Database["public"]["Enums"]["notification_channel"]
p_severity?: Database["public"]["Enums"]["notification_severity"]
p_metadata?: Json
p_idempotency_key?: string
p_message?: string
p_metadata?: Json
p_severity?: Database["public"]["Enums"]["notification_severity"]
p_title: string
p_type: Database["public"]["Enums"]["notification_type"]
p_user_id: string
}
Returns: undefined
}
Expand All @@ -1380,11 +1380,11 @@ export type Database = {
Returns: string
}
get_all_full_bookings: {
Args: { in_offset: number; in_limit: number }
Args: { in_limit: number; in_offset: number }
Returns: Json
}
get_all_full_orders: {
Args: { in_offset?: number; in_limit?: number }
Args: { in_limit?: number; in_offset?: number }
Returns: Json
}
get_full_booking: {
Expand All @@ -1396,26 +1396,26 @@ export type Database = {
Returns: Json
}
get_full_user_booking: {
Args: { in_user_id: string; in_offset: number; in_limit: number }
Args: { in_limit: number; in_offset: number; in_user_id: string }
Returns: Json
}
get_full_user_order: {
Args: { in_user_id: string; in_offset?: number; in_limit?: number }
Args: { in_limit?: number; in_offset?: number; in_user_id: string }
Returns: Json
}
get_latest_ban_record: {
Args: { check_user_id: string }
Returns: {
id: string
ban_type: string
action: string
ban_reason: string
is_permanent: boolean
banned_by: string
ban_type: string
banned_at: string
unbanned_at: string
banned_by: string
id: string
is_permanent: boolean
organization_id: string
role_assignment_id: string
unbanned_at: string
}[]
}
get_request_user_id: {
Expand All @@ -1432,46 +1432,46 @@ export type Database = {
get_user_roles: {
Args: { user_uuid: string }
Returns: {
created_at: string
id: string
user_id: string
organization_id: string
role_id: string
is_active: boolean
created_at: string
role_name: string
organization_id: string
organization_name: string
organization_slug: string
role_id: string
role_name: string
user_id: string
}[]
}
is_admin: {
Args: { p_user_id: string; p_org_id?: string }
Args: { p_org_id?: string; p_user_id: string }
Returns: boolean
}
is_user_banned_for_app: {
Args: { check_user_id: string }
Returns: boolean
}
is_user_banned_for_org: {
Args: { check_user_id: string; check_org_id: string }
Args: { check_org_id: string; check_user_id: string }
Returns: boolean
}
is_user_banned_for_role: {
Args: {
check_user_id: string
check_org_id: string
check_role_id: string
check_user_id: string
}
Returns: boolean
}
notify: {
Args: {
p_user_id: string
p_type_txt: string
p_title?: string
p_message?: string
p_channel?: Database["public"]["Enums"]["notification_channel"]
p_severity?: Database["public"]["Enums"]["notification_severity"]
p_message?: string
p_metadata?: Json
p_severity?: Database["public"]["Enums"]["notification_severity"]
p_title?: string
p_type_txt: string
p_user_id: string
}
Returns: undefined
}
Expand Down
10 changes: 5 additions & 5 deletions frontend/src/api/services/users.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CreateUserDto, UserProfile } from "@common/user.types";
import { api } from "../axios";
import { Address } from "@/types/address";
import { Address, CreateAddressInput } from "@/types/address";
import { store } from "@/store/store";
import { ApiResponse } from "@/types/api";
import { OrderedUsersParams } from "@/types/user";
Expand Down Expand Up @@ -88,23 +88,23 @@ export const usersApi = {
/**
* Add a new address for a user
* @param id - User ID to add the address to
* @param address - Address data to add
* @param address - Address data to add (without user_id as it's in the URL)
* @returns Promise with the newly added address
*/
addAddress: (id: string, address: Address): Promise<Address> =>
addAddress: (id: string, address: CreateAddressInput): Promise<Address> =>
api.post(`/users/${id}/addresses`, address),

/**
* Update an existing address for a user
* @param id - User ID to update the address for
* @param addressId - Address ID to update
* @param address - Updated address data
* @param address - Updated address data (without user_id as it's in the URL)
* @returns Promise with the updated address
*/
updateAddress: (
id: string,
addressId: string,
address: Address,
address: CreateAddressInput,
): Promise<Address> =>
api.put(`/users/${id}/addresses/${addressId}`, address),

Expand Down
Loading
Loading