Allow admins and supervisors to book or update reservations for other users
- Add role-based restrictions for booking or updating reservations on behalf of others (Admins/Supervisors only). - Extend reservation DTOs (`CreateReservationDto`, `UpdateReservationDto`) to include `userId` and `note` fields. - Update Prisma schema to allow nullable `userId` and add support for reservation notes. - Enhance API logic to validate permissions and handle ownership changes for reservations. - Improve UI for booking flow; introduce "Book for" options, user search, and reservation notes. - Localize new strings in `en.json` and `es-ES.json`.main
parent
0b6a043f19
commit
604d4c7244
|
|
@ -4,10 +4,16 @@
|
|||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="6c8b3c54-eabb-40e5-967f-c7b594c750bc" name="Changes" comment="Improve real-time space status handling and date-based resets - Add logic to clear real-time updates when the selected date changes. - Enhance space status updates to consider reservation time ranges for relevance. - Update relevant Svelte components (`SpaceCard`, `FloorplanViewer`) to handle date-sensitive real-time updates. - Update API to include reservation details in `space_status_changed` events for improved update precision.">
|
||||
<list default="true" id="6c8b3c54-eabb-40e5-967f-c7b594c750bc" name="Changes" comment="Prevent past bookings and updates across web and API - Add validation logic to block creating or updating reservations in the past. - Update date pickers to enforce minimum selectable date as today. - Extend i18n files (`en.json`, `es-ES.json`) with new error messages. - Refine UI to disable interaction with past time slots and display appropriate messages.">
|
||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/apps/api/prisma/schema.prisma" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/prisma/schema.prisma" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/apps/api/src/reservations/dto/create-reservation.dto.ts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/src/reservations/dto/create-reservation.dto.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/apps/api/src/reservations/dto/update-reservation.dto.ts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/src/reservations/dto/update-reservation.dto.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/apps/api/src/reservations/reservations.controller.ts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/src/reservations/reservations.controller.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/apps/api/src/reservations/reservations.service.ts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/src/reservations/reservations.service.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/apps/web/src/lib/components/BookingModal.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/lib/components/BookingModal.svelte" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/apps/web/src/lib/i18n/locales/en.json" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/lib/i18n/locales/en.json" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/apps/web/src/lib/i18n/locales/es-ES.json" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/lib/i18n/locales/es-ES.json" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/apps/web/src/routes/dashboard/my-bookings/+page.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/routes/dashboard/my-bookings/+page.svelte" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
|
|
@ -129,7 +135,7 @@
|
|||
<updated>1774643312599</updated>
|
||||
<workItem from="1774643313611" duration="74000" />
|
||||
<workItem from="1774643422240" duration="5278000" />
|
||||
<workItem from="1774706361502" duration="11708000" />
|
||||
<workItem from="1774706361502" duration="12417000" />
|
||||
</task>
|
||||
<task id="LOCAL-00001" summary="Initialize Hot Desking app with web and API services - Set up SvelteKit for the web interface with Tailwind CSS. - Build the API using NestJS with Prisma ORM for database interaction. - Add environment variable management and docker-compose for PostgreSQL and PgAdmin instances. - Create shared data models for consistent typing across services. - Establish basic routing and layouts for web and API services.">
|
||||
<option name="closed" value="true" />
|
||||
|
|
@ -315,17 +321,37 @@
|
|||
<option name="project" value="LOCAL" />
|
||||
<updated>1774717937214</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="24" />
|
||||
<task id="LOCAL-00024" summary="Add filtering for past and upcoming bookings in "My Bookings" page - Enhance UI to separate upcoming and past bookings with collapsible sections. - Add new i18n keys for handling booking filters (`en.json`, `es-ES.json`). - Update booking component logic to filter reservations based on time. - Introduce "Show/Hide Past Bookings" toggle for improved user experience.">
|
||||
<option name="closed" value="true" />
|
||||
<created>1774718091896</created>
|
||||
<option name="number" value="00024" />
|
||||
<option name="presentableId" value="LOCAL-00024" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1774718091896</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00025" summary="Refactor date picker components for improved accessibility and usability - Add `showPicker` event to trigger native date picker overlays. - Adjust z-index and positioning for hidden `input` elements to improve interaction. - Reorganize markup for consistent focus and selection behavior across components.">
|
||||
<option name="closed" value="true" />
|
||||
<created>1774718250404</created>
|
||||
<option name="number" value="00025" />
|
||||
<option name="presentableId" value="LOCAL-00025" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1774718250404</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00026" summary="Prevent past bookings and updates across web and API - Add validation logic to block creating or updating reservations in the past. - Update date pickers to enforce minimum selectable date as today. - Extend i18n files (`en.json`, `es-ES.json`) with new error messages. - Refine UI to disable interaction with past time slots and display appropriate messages.">
|
||||
<option name="closed" value="true" />
|
||||
<created>1774718397058</created>
|
||||
<option name="number" value="00026" />
|
||||
<option name="presentableId" value="LOCAL-00026" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1774718397058</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="27" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
<option name="version" value="3" />
|
||||
</component>
|
||||
<component name="VcsManagerConfiguration">
|
||||
<MESSAGE value="Initialize Hot Desking app with web and API services - Set up SvelteKit for the web interface with Tailwind CSS. - Build the API using NestJS with Prisma ORM for database interaction. - Add environment variable management and docker-compose for PostgreSQL and PgAdmin instances. - Create shared data models for consistent typing across services. - Establish basic routing and layouts for web and API services." />
|
||||
<MESSAGE value="Remove compiled API files from version control - Deleted compiled JavaScript files, type definitions, and source maps from the `dist` folder." />
|
||||
<MESSAGE value="Implement authentication, organization management, and floor layout features - Add API authentication with JWT strategy using NestJS. - Configure Prisma schema for organizations, users, buildings, floors, and spaces. - Seed initial database with test data and relationships. - Build SvelteKit web pages for dashboard and floor layout with secured routing and organization hierarchy display. - Remove deprecated `AppService` from API." />
|
||||
<MESSAGE value="Add floorplan editor for admin layout management - Implement `FloorplanEditor.svelte` component for adding, editing, and positioning spaces. - Add backend support for floorplan image upload and batch updating space positions. - Update Prisma schema to include `planImageUrl` for floors. - Enable API proxy in Vite configuration for straightforward local development. - Refactor API calls to use a centralized `apiFetch` utility with token handling." />
|
||||
<MESSAGE value="Add building-level dashboard and interactive floorplan viewer - Create `+page.svelte` for building-level dashboard with floor selection and layout preview. - Add `FloorplanViewer.svelte` component with WebSocket integration for real-time space status updates. - Extend `SpacesService` and API endpoints for building floors and floor details retrieval. - Update dashboard UI with cleaner building and floor presentation. - Improve loading states and error handling across dashboard and floor pages." />
|
||||
<MESSAGE value="Add booking modal and enhance reservation handling - Create `BookingModal.svelte` for booking spaces with custom time slots or full-day durations. - Add API endpoint to fetch reservations by space ID and date. - Update `SpacesService` with enhanced validation rules and conflict checks for reservations. - Extend WebSocket updates to include reservation data for real-time synchronization. - Enable date-based filtering and status handling in UI components (`SpaceCard.svelte`, `FloorplanViewer.svelte`)." />
|
||||
<MESSAGE value="Add support for space resizing and enhanced layout management - Update `SpacesService` to handle width and height properties for spaces. - Introduce `UpdateSpaceDto` for improved validation in batch update operations. - Extend `FloorplanEditor.svelte` and `FloorplanViewer.svelte` to support dynamic container scaling and space resizing. - Modify Prisma schema to include `width` and `height` fields for spaces. - Enhance floorplan rendering to maintain proportional layout during editing and viewing." />
|
||||
|
|
@ -347,6 +373,10 @@
|
|||
<MESSAGE value="Add i18n integration and translations for improved localization - Extend English (`en.json`) and Spanish (`es-ES.json`) locale files with additional keys for buildings, bookings, reports, and user management. - Update Svelte components and pages to use `svelte-i18n` for dynamic translations across the app. - Refactor labels, titles, and error messages for multilingual support. - Improve date formatting based on locale preferences." />
|
||||
<MESSAGE value="Add seed service to initialize default admin and organization - Implement `SeedService` to create a default admin account and organization on app startup. - Update `PrismaModule` to include `SeedService` as a provider. - Remove seeded credentials display from login page. - Refactor existing seed script for consistency with the new service." />
|
||||
<MESSAGE value="Improve real-time space status handling and date-based resets - Add logic to clear real-time updates when the selected date changes. - Enhance space status updates to consider reservation time ranges for relevance. - Update relevant Svelte components (`SpaceCard`, `FloorplanViewer`) to handle date-sensitive real-time updates. - Update API to include reservation details in `space_status_changed` events for improved update precision." />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="Improve real-time space status handling and date-based resets - Add logic to clear real-time updates when the selected date changes. - Enhance space status updates to consider reservation time ranges for relevance. - Update relevant Svelte components (`SpaceCard`, `FloorplanViewer`) to handle date-sensitive real-time updates. - Update API to include reservation details in `space_status_changed` events for improved update precision." />
|
||||
<MESSAGE value="Add past and upcoming reservations filtering in My Bookings - Introduce filters to separate and toggle display of past and upcoming reservations. - Enhance localization with new translations for filter-related actions in English and Spanish. - Update My Bookings UI to dynamically display reservations based on their timeline. - Improve reservation display logic using `isPast` from `date-fns`." />
|
||||
<MESSAGE value="Add filtering for past and upcoming bookings in "My Bookings" page - Enhance UI to separate upcoming and past bookings with collapsible sections. - Add new i18n keys for handling booking filters (`en.json`, `es-ES.json`). - Update booking component logic to filter reservations based on time. - Introduce "Show/Hide Past Bookings" toggle for improved user experience." />
|
||||
<MESSAGE value="Refactor date picker components for improved accessibility and usability - Add `showPicker` event to trigger native date picker overlays. - Adjust z-index and positioning for hidden `input` elements to improve interaction. - Reorganize markup for consistent focus and selection behavior across components." />
|
||||
<MESSAGE value="Prevent past bookings and updates across web and API - Add validation logic to block creating or updating reservations in the past. - Update date pickers to enforce minimum selectable date as today. - Extend i18n files (`en.json`, `es-ES.json`) with new error messages. - Refine UI to disable interaction with past time slots and display appropriate messages." />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="Prevent past bookings and updates across web and API - Add validation logic to block creating or updating reservations in the past. - Update date pickers to enforce minimum selectable date as today. - Extend i18n files (`en.json`, `es-ES.json`) with new error messages. - Refine UI to disable interaction with past time slots and display appropriate messages." />
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -96,12 +96,13 @@ model Reservation {
|
|||
id String @id @default(uuid())
|
||||
startTime DateTime
|
||||
endTime DateTime
|
||||
userId String
|
||||
userId String?
|
||||
spaceId String
|
||||
note String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
space Space @relation(fields: [spaceId], references: [id])
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
}
|
||||
|
||||
enum Role {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { IsString, IsDateString, IsNotEmpty } from 'class-validator';
|
||||
import { IsString, IsDateString, IsNotEmpty, IsOptional } from 'class-validator';
|
||||
|
||||
export class CreateReservationDto {
|
||||
@IsDateString()
|
||||
|
|
@ -12,4 +12,12 @@ export class CreateReservationDto {
|
|||
@IsString()
|
||||
@IsNotEmpty()
|
||||
spaceId: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
userId?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
note?: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,4 +12,12 @@ export class UpdateReservationDto {
|
|||
@IsString()
|
||||
@IsOptional()
|
||||
spaceId?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
userId?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
note?: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,11 +8,13 @@ import {
|
|||
UseGuards,
|
||||
Req,
|
||||
Patch,
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
import { ReservationsService } from './reservations.service';
|
||||
import { CreateReservationDto } from './dto/create-reservation.dto';
|
||||
import { UpdateReservationDto } from './dto/update-reservation.dto';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { Role } from '@prisma/client';
|
||||
|
||||
@Controller('reservations')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
|
|
@ -21,7 +23,16 @@ export class ReservationsController {
|
|||
|
||||
@Post()
|
||||
create(@Req() req, @Body() createReservationDto: CreateReservationDto) {
|
||||
return this.reservationsService.create(req.user.id, createReservationDto);
|
||||
let targetUserId = req.user.id;
|
||||
|
||||
if (createReservationDto.userId && createReservationDto.userId !== req.user.id) {
|
||||
if (req.user.role !== Role.ADMIN && req.user.role !== Role.SUPERVISOR) {
|
||||
throw new ForbiddenException('Only admins and supervisors can book for other people');
|
||||
}
|
||||
targetUserId = createReservationDto.userId;
|
||||
}
|
||||
|
||||
return this.reservationsService.create(targetUserId, createReservationDto);
|
||||
}
|
||||
|
||||
@Get('my')
|
||||
|
|
@ -37,11 +48,20 @@ export class ReservationsController {
|
|||
|
||||
@Patch(':id')
|
||||
update(@Param('id') id: string, @Req() req, @Body() updateReservationDto: UpdateReservationDto) {
|
||||
return this.reservationsService.update(req.user.id, id, updateReservationDto);
|
||||
let targetUserId = req.user.id;
|
||||
|
||||
if (updateReservationDto.userId && updateReservationDto.userId !== req.user.id) {
|
||||
if (req.user.role !== Role.ADMIN && req.user.role !== Role.SUPERVISOR) {
|
||||
throw new ForbiddenException('Only admins and supervisors can change the owner of a reservation');
|
||||
}
|
||||
targetUserId = updateReservationDto.userId;
|
||||
}
|
||||
|
||||
return this.reservationsService.update(targetUserId, id, updateReservationDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
cancel(@Param('id') id: string, @Req() req) {
|
||||
return this.reservationsService.cancel(req.user.id, id);
|
||||
return this.reservationsService.cancel(req.user.id, id, req.user.role);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ export class ReservationsService {
|
|||
) {}
|
||||
|
||||
async create(userId: string, createReservationDto: CreateReservationDto) {
|
||||
const { spaceId, startTime, endTime } = createReservationDto;
|
||||
const { spaceId, startTime, endTime, userId: requestedUserId } = createReservationDto;
|
||||
const targetUserId = requestedUserId || userId;
|
||||
|
||||
const space = await this.prisma.space.findUnique({
|
||||
where: { id: spaceId },
|
||||
|
|
@ -77,8 +78,9 @@ export class ReservationsService {
|
|||
data: {
|
||||
startTime: start,
|
||||
endTime: end,
|
||||
userId,
|
||||
userId: targetUserId,
|
||||
spaceId,
|
||||
note: createReservationDto.note,
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
|
|
@ -109,7 +111,7 @@ export class ReservationsService {
|
|||
return reservation;
|
||||
}
|
||||
|
||||
async cancel(userId: string, reservationId: string) {
|
||||
async cancel(userId: string, reservationId: string, userRole?: string) {
|
||||
const reservation = await this.prisma.reservation.findUnique({
|
||||
where: { id: reservationId },
|
||||
include: {
|
||||
|
|
@ -126,17 +128,8 @@ export class ReservationsService {
|
|||
throw new NotFoundException(`Reservation with ID ${reservationId} not found`);
|
||||
}
|
||||
|
||||
// Check if user exists to get role
|
||||
const requestingUser = await this.prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
});
|
||||
|
||||
if (!requestingUser) {
|
||||
throw new NotFoundException(`User with ID ${userId} not found`);
|
||||
}
|
||||
|
||||
if (reservation.userId !== userId && requestingUser.role !== 'ADMIN') {
|
||||
throw new ForbiddenException('You can only cancel your own reservations unless you are an administrator');
|
||||
if (reservation.userId !== userId && userRole !== Role.ADMIN && userRole !== Role.SUPERVISOR) {
|
||||
throw new ForbiddenException('You can only cancel your own reservations unless you are an administrator or supervisor');
|
||||
}
|
||||
|
||||
await this.prisma.reservation.delete({
|
||||
|
|
@ -191,13 +184,21 @@ export class ReservationsService {
|
|||
throw new NotFoundException(`Reservation with ID ${id} not found`);
|
||||
}
|
||||
|
||||
if (reservation.userId !== userId) {
|
||||
const isDifferentUser = reservation.userId !== userId;
|
||||
|
||||
if (isDifferentUser) {
|
||||
// Check if the current user is an admin or supervisor
|
||||
const requester = await this.prisma.user.findUnique({ where: { id: userId } });
|
||||
if (!requester || (requester.role !== Role.ADMIN && requester.role !== Role.SUPERVISOR)) {
|
||||
throw new ForbiddenException('You can only update your own reservations');
|
||||
}
|
||||
}
|
||||
|
||||
const spaceId = updateReservationDto.spaceId || reservation.spaceId;
|
||||
const startTime = updateReservationDto.startTime || reservation.startTime;
|
||||
const endTime = updateReservationDto.endTime || reservation.endTime;
|
||||
const targetUserId = updateReservationDto.userId === null ? null : (updateReservationDto.userId || reservation.userId);
|
||||
const note = updateReservationDto.note === undefined ? reservation.note : updateReservationDto.note;
|
||||
|
||||
const space = await this.prisma.space.findUnique({
|
||||
where: { id: spaceId },
|
||||
|
|
@ -262,6 +263,8 @@ export class ReservationsService {
|
|||
startTime: start,
|
||||
endTime: end,
|
||||
spaceId,
|
||||
userId: targetUserId,
|
||||
note,
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { createEventDispatcher } from 'svelte';
|
||||
import { user } from '$lib/stores/auth';
|
||||
import { apiFetch } from '$lib/api/client';
|
||||
import { X, Calendar, Clock, Loader2, Trash2, AlertTriangle } from 'lucide-svelte';
|
||||
import { X, Calendar, Clock, Loader2, Trash2, AlertTriangle, User as UserIcon, FileText, Search, ChevronDown } from 'lucide-svelte';
|
||||
import { t, locale } from 'svelte-i18n';
|
||||
import { format } from 'date-fns';
|
||||
import { es, enUS } from 'date-fns/locale';
|
||||
|
|
@ -48,9 +48,45 @@
|
|||
let confirmDelete = false;
|
||||
let error = "";
|
||||
|
||||
let users: any[] = [];
|
||||
let loadingUsers = false;
|
||||
let bookFor = 'myself'; // 'myself', 'other', 'none'
|
||||
let targetUserId = '';
|
||||
let note = '';
|
||||
let searchQuery = '';
|
||||
let showUserDropdown = false;
|
||||
|
||||
$: filteredUsers = users.filter(u =>
|
||||
(u.name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
u.email?.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
);
|
||||
|
||||
function selectUser(selectedUser: any) {
|
||||
targetUserId = selectedUser.id;
|
||||
searchQuery = selectedUser.name || selectedUser.email;
|
||||
showUserDropdown = false;
|
||||
}
|
||||
|
||||
$: isAdminOrSupervisor = $user?.role === 'ADMIN' || $user?.role === 'SUPERVISOR';
|
||||
$: isOwner = status.reservation?.userId === $user?.id || status.reservation?.user?.id === $user?.id;
|
||||
$: isAdmin = $user?.role === 'ADMIN';
|
||||
|
||||
async function fetchUsers() {
|
||||
if (!isAdminOrSupervisor) return;
|
||||
loadingUsers = true;
|
||||
try {
|
||||
users = await apiFetch('/users');
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch users", e);
|
||||
} finally {
|
||||
loadingUsers = false;
|
||||
}
|
||||
}
|
||||
|
||||
$: if (isAdminOrSupervisor && space) {
|
||||
fetchUsers();
|
||||
}
|
||||
|
||||
const timeOptions = [];
|
||||
for (let h = 7; h <= 20; h++) {
|
||||
for (let m = 0; m < 60; m += 15) {
|
||||
|
|
@ -109,6 +145,8 @@
|
|||
spaceId: space.id,
|
||||
startTime: start.toISOString(),
|
||||
endTime: end.toISOString(),
|
||||
userId: bookFor === 'other' ? targetUserId : (bookFor === 'none' ? null : $user?.id),
|
||||
note: note || undefined
|
||||
})
|
||||
});
|
||||
|
||||
|
|
@ -208,15 +246,32 @@
|
|||
<p class="text-yellow-800 font-bold mb-1">{$t('booking.currentlyBooked')}</p>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-3">
|
||||
{#if status.reservation?.user}
|
||||
<div class="w-10 h-10 rounded-full bg-yellow-200 flex items-center justify-center text-yellow-700 font-bold">
|
||||
{status.reservation?.user?.email?.charAt(0).toUpperCase()}
|
||||
{status.reservation.user.email?.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-yellow-900">{status.reservation?.user?.email}</p>
|
||||
<p class="text-sm font-medium text-yellow-900">{status.reservation.user.email}</p>
|
||||
{#if status.reservation.note}
|
||||
<p class="text-xs text-yellow-800 italic">"{status.reservation.note}"</p>
|
||||
{/if}
|
||||
<p class="text-xs text-yellow-700">
|
||||
{formatTime(status.reservation.startTime)} - {formatTime(status.reservation.endTime)}
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="w-10 h-10 rounded-full bg-yellow-200 flex items-center justify-center text-yellow-700 font-bold">
|
||||
<FileText size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-yellow-900">
|
||||
{status.reservation.note || $t('booking.nonEmployee')}
|
||||
</p>
|
||||
<p class="text-xs text-yellow-700">
|
||||
{formatTime(status.reservation.startTime)} - {formatTime(status.reservation.endTime)}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if isOwner || isAdmin}
|
||||
|
|
@ -266,6 +321,100 @@
|
|||
{/if}
|
||||
|
||||
<div class="space-y-4">
|
||||
{#if isAdminOrSupervisor}
|
||||
<div class="bg-gray-50 p-4 rounded-xl border border-gray-100 space-y-3">
|
||||
<label class="block text-sm font-bold text-gray-700 flex items-center gap-2">
|
||||
<UserIcon size={16} />
|
||||
{$t('booking.bookFor')}
|
||||
</label>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => bookFor = 'myself'}
|
||||
class="px-3 py-1.5 text-xs font-bold rounded-full transition-colors border
|
||||
{bookFor === 'myself' ? 'bg-indigo-600 text-white border-indigo-700' : 'bg-white text-gray-600 border-gray-200 hover:border-indigo-400'}"
|
||||
>
|
||||
{$t('booking.myself')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => bookFor = 'other'}
|
||||
class="px-3 py-1.5 text-xs font-bold rounded-full transition-colors border
|
||||
{bookFor === 'other' ? 'bg-indigo-600 text-white border-indigo-700' : 'bg-white text-gray-600 border-gray-200 hover:border-indigo-400'}"
|
||||
>
|
||||
{$t('booking.someoneElse')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => bookFor = 'none'}
|
||||
class="px-3 py-1.5 text-xs font-bold rounded-full transition-colors border
|
||||
{bookFor === 'none' ? 'bg-indigo-600 text-white border-indigo-700' : 'bg-white text-gray-600 border-gray-200 hover:border-indigo-400'}"
|
||||
>
|
||||
{$t('booking.nonEmployee')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if bookFor === 'other'}
|
||||
<div class="mt-2 relative">
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400">
|
||||
<Search size={16} />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={searchQuery}
|
||||
on:focus={() => showUserDropdown = true}
|
||||
placeholder={$t('booking.searchUserPlaceholder')}
|
||||
class="w-full pl-10 pr-10 py-2.5 bg-white border border-gray-200 rounded-lg text-sm focus:ring-2 focus:ring-indigo-500 outline-none"
|
||||
/>
|
||||
<div class="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400">
|
||||
<ChevronDown size={16} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showUserDropdown && (searchQuery || users.length > 0)}
|
||||
<div class="absolute z-20 mt-1 w-full bg-white border border-gray-200 rounded-lg shadow-xl max-h-48 overflow-y-auto">
|
||||
{#if filteredUsers.length === 0}
|
||||
<div class="p-3 text-sm text-gray-500 text-center">
|
||||
{$t('booking.noUsersFound')}
|
||||
</div>
|
||||
{:else}
|
||||
{#each filteredUsers as userItem}
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => selectUser(userItem)}
|
||||
class="w-full text-left p-3 hover:bg-indigo-50 transition-colors border-b border-gray-50 last:border-0"
|
||||
>
|
||||
<p class="text-sm font-bold text-gray-800">{userItem.name || userItem.email}</p>
|
||||
<p class="text-xs text-gray-500">{userItem.email}</p>
|
||||
</button>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showUserDropdown}
|
||||
<div
|
||||
class="fixed inset-0 z-10"
|
||||
on:click={() => showUserDropdown = false}
|
||||
></div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mt-2">
|
||||
<label class="block text-xs font-bold text-gray-500 mb-1">{$t('booking.note')}</label>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={note}
|
||||
placeholder={$t('booking.notePlaceholder')}
|
||||
class="w-full p-2.5 bg-white border border-gray-200 rounded-lg text-sm focus:ring-2 focus:ring-indigo-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-gray-700 mb-1 flex items-center gap-2">
|
||||
<Calendar size={16} />
|
||||
|
|
@ -311,7 +460,7 @@
|
|||
reservation ? 'bg-red-100 text-red-700 border-red-200' :
|
||||
startTime === time ? 'bg-indigo-600 text-white border-indigo-700 ring-2 ring-indigo-200' :
|
||||
'bg-white text-gray-600 border-gray-200 hover:border-indigo-400'}"
|
||||
title={reservation?.isPast ? $t('booking.past') : reservation ? $t('booking.bookedBy', { values: { user: reservation.user.email } }) : $t('booking.availableAt', { values: { time: time } })}
|
||||
title={reservation?.isPast ? $t('booking.past') : reservation ? $t('booking.bookedBy', { values: { user: reservation.user?.email || reservation.note || $t('booking.nonEmployee') } }) : $t('booking.availableAt', { values: { time: time } })}
|
||||
>
|
||||
{time}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -79,6 +79,15 @@
|
|||
"errorTime": "End time must be after start time",
|
||||
"errorPast": "Cannot book in the past",
|
||||
"past": "This time has already passed",
|
||||
"bookFor": "Book for",
|
||||
"myself": "Myself",
|
||||
"someoneElse": "Someone else / Event",
|
||||
"selectUser": "Select user",
|
||||
"nonEmployee": "Non-employee / Event (no user)",
|
||||
"note": "Note",
|
||||
"notePlaceholder": "Reason for booking, name of guest, event name, etc.",
|
||||
"searchUserPlaceholder": "Search by name or email...",
|
||||
"noUsersFound": "No users found",
|
||||
"spaceType": {
|
||||
"DESK": "Desk",
|
||||
"MEETING_ROOM": "Meeting Room",
|
||||
|
|
|
|||
|
|
@ -79,6 +79,15 @@
|
|||
"errorTime": "La hora de finalización debe ser posterior a la de inicio",
|
||||
"errorPast": "No se puede reservar en el pasado",
|
||||
"past": "Esta hora ya ha pasado",
|
||||
"bookFor": "Reservar para",
|
||||
"myself": "Mí mismo/a",
|
||||
"someoneElse": "Otra persona / Evento",
|
||||
"selectUser": "Seleccionar usuario",
|
||||
"nonEmployee": "No empleado / Evento (sin usuario)",
|
||||
"note": "Nota",
|
||||
"notePlaceholder": "Motivo de la reserva, nombre del invitado, nombre del evento, etc.",
|
||||
"searchUserPlaceholder": "Buscar por nombre o email...",
|
||||
"noUsersFound": "No se encontraron usuarios",
|
||||
"spaceType": {
|
||||
"DESK": "Puesto",
|
||||
"MEETING_ROOM": "Sala de reuniones",
|
||||
|
|
|
|||
Loading…
Reference in New Issue