Add i18n support with English and Spanish translations
- Introduce `svelte-i18n` for internationalization. - Add English (`en.json`) and Spanish (`es-ES.json`) locale files. - Implement `setupI18n` function for initializing and managing locales. - Update project dependencies to include `svelte-i18n`.main
parent
e80984faa1
commit
71404d0ed8
|
|
@ -4,18 +4,18 @@
|
||||||
<option name="autoReloadType" value="SELECTIVE" />
|
<option name="autoReloadType" value="SELECTIVE" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="6c8b3c54-eabb-40e5-967f-c7b594c750bc" name="Changes" comment="Add assets module with file upload and retrieval support - Introduce `AssetsModule` with APIs for uploading and retrieving organization logos, building images, and floor plans. - Implement file validation for type and size (10MB max). - Add `resolveAssetUrl` utility for generating absolute asset URLs. - Enhance admin UI components to support image file uploads and previews. - Update Prisma schema to associate assets with organizations, buildings, and floors. - Modify existing update logic to clear references when external URLs are used.">
|
<list default="true" id="6c8b3c54-eabb-40e5-967f-c7b594c750bc" name="Changes" comment="Add user profile management with avatar upload and update support - Create `+page.svelte` for managing user profiles, including name, avatar, and password updates. - Add `Avatar.svelte` component for rendering user avatars with Gravatar and fallback initials support. - Implement backend APIs for fetching and updating user profiles (`/users/profile`). - Add support for avatar uploads with file validation (max size: 5MB). - Update `FloorplanViewer.svelte` to display user names and avatars in reservations. - Introduce utilities for Gravatar URLs and user avatar management (`getGravatarUrl`, `getInitials`).">
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
<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/prisma/schema.prisma" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/prisma/schema.prisma" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/apps/api/src/auth/auth.service.ts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/src/auth/auth.service.ts" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/apps/api/src/organizations/dto/update-organization.dto.ts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/src/organizations/dto/update-organization.dto.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/api/src/users/dto/update-user.dto.ts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/src/users/dto/update-user.dto.ts" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/apps/api/src/users/users.controller.ts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/src/users/users.controller.ts" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/apps/api/src/users/users.service.ts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/src/users/users.service.ts" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/apps/api/src/users/users.service.ts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/src/users/users.service.ts" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/apps/web/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/package.json" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/apps/web/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/package.json" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/apps/web/src/lib/components/FloorplanViewer.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/lib/components/FloorplanViewer.svelte" 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/stores/organization.ts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/lib/stores/organization.ts" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/apps/web/src/routes/+layout.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/routes/+layout.svelte" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/apps/web/src/routes/+layout.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/routes/+layout.svelte" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/package-lock.json" beforeDir="false" afterPath="$PROJECT_DIR$/package-lock.json" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/apps/web/src/routes/admin/organization/+page.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/routes/admin/organization/+page.svelte" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/apps/web/src/routes/dashboard/+page.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/routes/dashboard/+page.svelte" 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>
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
|
|
@ -115,8 +115,8 @@
|
||||||
</configuration>
|
</configuration>
|
||||||
<recent_temporary>
|
<recent_temporary>
|
||||||
<list>
|
<list>
|
||||||
<item itemvalue="npm.api > start:dev" />
|
|
||||||
<item itemvalue="npm.web > dev" />
|
<item itemvalue="npm.web > dev" />
|
||||||
|
<item itemvalue="npm.api > start:dev" />
|
||||||
<item itemvalue="Docker.docker-compose.yml: Compose Deployment" />
|
<item itemvalue="Docker.docker-compose.yml: Compose Deployment" />
|
||||||
</list>
|
</list>
|
||||||
</recent_temporary>
|
</recent_temporary>
|
||||||
|
|
@ -137,7 +137,7 @@
|
||||||
<updated>1774643312599</updated>
|
<updated>1774643312599</updated>
|
||||||
<workItem from="1774643313611" duration="74000" />
|
<workItem from="1774643313611" duration="74000" />
|
||||||
<workItem from="1774643422240" duration="5278000" />
|
<workItem from="1774643422240" duration="5278000" />
|
||||||
<workItem from="1774706361502" duration="8461000" />
|
<workItem from="1774706361502" duration="9064000" />
|
||||||
</task>
|
</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.">
|
<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" />
|
<option name="closed" value="true" />
|
||||||
|
|
@ -259,7 +259,15 @@
|
||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1774714222515</updated>
|
<updated>1774714222515</updated>
|
||||||
</task>
|
</task>
|
||||||
<option name="localTasksCounter" value="16" />
|
<task id="LOCAL-00016" summary="Add user profile management with avatar upload and update support - Create `+page.svelte` for managing user profiles, including name, avatar, and password updates. - Add `Avatar.svelte` component for rendering user avatars with Gravatar and fallback initials support. - Implement backend APIs for fetching and updating user profiles (`/users/profile`). - Add support for avatar uploads with file validation (max size: 5MB). - Update `FloorplanViewer.svelte` to display user names and avatars in reservations. - Introduce utilities for Gravatar URLs and user avatar management (`getGravatarUrl`, `getInitials`).">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1774714977147</created>
|
||||||
|
<option name="number" value="00016" />
|
||||||
|
<option name="presentableId" value="LOCAL-00016" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1774714977147</updated>
|
||||||
|
</task>
|
||||||
|
<option name="localTasksCounter" value="17" />
|
||||||
<servers />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
<component name="TypeScriptGeneratedFilesManager">
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
|
|
@ -283,6 +291,7 @@
|
||||||
<MESSAGE value="Add organization management with logo support - Introduce `OrganizationsModule` with related logic in `AppModule`. - Update Prisma schema to include `logoUrl` field for organizations. - Enhance seeds to include default organization logo. - Update layout to dynamically display organization details, including logo and name. - Add UI support for organization-based navigation in admin routes." />
|
<MESSAGE value="Add organization management with logo support - Introduce `OrganizationsModule` with related logic in `AppModule`. - Update Prisma schema to include `logoUrl` field for organizations. - Enhance seeds to include default organization logo. - Update layout to dynamically display organization details, including logo and name. - Add UI support for organization-based navigation in admin routes." />
|
||||||
<MESSAGE value="Add organization management with logo support - Implement organization settings page (`+page.svelte`) for updating organization name and logo. - Create backend APIs for retrieving and updating organization details with role-based access control. - Add `OrganizationsModule`, `OrganizationsService`, and DTOs for input validation in NestJS. - Update Prisma schema to include `logoUrl` field for organizations. - Enhance navigation to dynamically display organization logo and name." />
|
<MESSAGE value="Add organization management with logo support - Implement organization settings page (`+page.svelte`) for updating organization name and logo. - Create backend APIs for retrieving and updating organization details with role-based access control. - Add `OrganizationsModule`, `OrganizationsService`, and DTOs for input validation in NestJS. - Update Prisma schema to include `logoUrl` field for organizations. - Enhance navigation to dynamically display organization logo and name." />
|
||||||
<MESSAGE value="Add assets module with file upload and retrieval support - Introduce `AssetsModule` with APIs for uploading and retrieving organization logos, building images, and floor plans. - Implement file validation for type and size (10MB max). - Add `resolveAssetUrl` utility for generating absolute asset URLs. - Enhance admin UI components to support image file uploads and previews. - Update Prisma schema to associate assets with organizations, buildings, and floors. - Modify existing update logic to clear references when external URLs are used." />
|
<MESSAGE value="Add assets module with file upload and retrieval support - Introduce `AssetsModule` with APIs for uploading and retrieving organization logos, building images, and floor plans. - Implement file validation for type and size (10MB max). - Add `resolveAssetUrl` utility for generating absolute asset URLs. - Enhance admin UI components to support image file uploads and previews. - Update Prisma schema to associate assets with organizations, buildings, and floors. - Modify existing update logic to clear references when external URLs are used." />
|
||||||
<option name="LAST_COMMIT_MESSAGE" value="Add assets module with file upload and retrieval support - Introduce `AssetsModule` with APIs for uploading and retrieving organization logos, building images, and floor plans. - Implement file validation for type and size (10MB max). - Add `resolveAssetUrl` utility for generating absolute asset URLs. - Enhance admin UI components to support image file uploads and previews. - Update Prisma schema to associate assets with organizations, buildings, and floors. - Modify existing update logic to clear references when external URLs are used." />
|
<MESSAGE value="Add user profile management with avatar upload and update support - Create `+page.svelte` for managing user profiles, including name, avatar, and password updates. - Add `Avatar.svelte` component for rendering user avatars with Gravatar and fallback initials support. - Implement backend APIs for fetching and updating user profiles (`/users/profile`). - Add support for avatar uploads with file validation (max size: 5MB). - Update `FloorplanViewer.svelte` to display user names and avatars in reservations. - Introduce utilities for Gravatar URLs and user avatar management (`getGravatarUrl`, `getInitials`)." />
|
||||||
|
<option name="LAST_COMMIT_MESSAGE" value="Add user profile management with avatar upload and update support - Create `+page.svelte` for managing user profiles, including name, avatar, and password updates. - Add `Avatar.svelte` component for rendering user avatars with Gravatar and fallback initials support. - Implement backend APIs for fetching and updating user profiles (`/users/profile`). - Add support for avatar uploads with file validation (max size: 5MB). - Update `FloorplanViewer.svelte` to display user names and avatars in reservations. - Introduce utilities for Gravatar URLs and user avatar management (`getGravatarUrl`, `getInitials`)." />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
|
|
@ -10,6 +10,7 @@ datasource db {
|
||||||
model Organization {
|
model Organization {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
name String
|
name String
|
||||||
|
language String @default("en")
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
logoUrl String?
|
logoUrl String?
|
||||||
|
|
|
||||||
|
|
@ -9,4 +9,8 @@ export class UpdateOrganizationDto {
|
||||||
// @IsUrl()
|
// @IsUrl()
|
||||||
@IsString()
|
@IsString()
|
||||||
logoUrl?: string | null;
|
logoUrl?: string | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
language?: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ export class UsersService {
|
||||||
avatarUrl: true,
|
avatarUrl: true,
|
||||||
avatarId: true,
|
avatarId: true,
|
||||||
role: true,
|
role: true,
|
||||||
|
organizationId: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
updatedAt: true,
|
updatedAt: true,
|
||||||
},
|
},
|
||||||
|
|
@ -38,6 +39,7 @@ export class UsersService {
|
||||||
avatarUrl: true,
|
avatarUrl: true,
|
||||||
avatarId: true,
|
avatarId: true,
|
||||||
role: true,
|
role: true,
|
||||||
|
organizationId: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
updatedAt: true,
|
updatedAt: true,
|
||||||
},
|
},
|
||||||
|
|
@ -68,6 +70,7 @@ export class UsersService {
|
||||||
avatarUrl: true,
|
avatarUrl: true,
|
||||||
avatarId: true,
|
avatarId: true,
|
||||||
role: true,
|
role: true,
|
||||||
|
organizationId: true,
|
||||||
updatedAt: true,
|
updatedAt: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -31,6 +31,7 @@
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"import-meta-resolve": "^4.2.0",
|
"import-meta-resolve": "^4.2.0",
|
||||||
"interactjs": "^1.10.27",
|
"interactjs": "^1.10.27",
|
||||||
"socket.io-client": "^4.8.3"
|
"socket.io-client": "^4.8.3",
|
||||||
|
"svelte-i18n": "^4.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@
|
||||||
import { user } from '$lib/stores/auth';
|
import { user } from '$lib/stores/auth';
|
||||||
import { apiFetch } from '$lib/api/client';
|
import { apiFetch } from '$lib/api/client';
|
||||||
import { X, Calendar, Clock, Loader2, Trash2, AlertTriangle } from 'lucide-svelte';
|
import { X, Calendar, Clock, Loader2, Trash2, AlertTriangle } from 'lucide-svelte';
|
||||||
|
import { t, locale } from 'svelte-i18n';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { es, enUS } from 'date-fns/locale';
|
||||||
|
|
||||||
export let space: any;
|
export let space: any;
|
||||||
export let status: any;
|
export let status: any;
|
||||||
|
|
@ -13,6 +16,9 @@
|
||||||
let reservations: any[] = [];
|
let reservations: any[] = [];
|
||||||
let loadingReservations = false;
|
let loadingReservations = false;
|
||||||
|
|
||||||
|
$: dateLocale = $locale === 'es-ES' ? es : enUS;
|
||||||
|
$: formattedDate = format(selectedDate, 'PPP', { locale: dateLocale });
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (selectedDate) {
|
if (selectedDate) {
|
||||||
bookingDate = selectedDate.toISOString().split('T')[0];
|
bookingDate = selectedDate.toISOString().split('T')[0];
|
||||||
|
|
@ -170,7 +176,7 @@
|
||||||
<div class="p-6 border-b flex justify-between items-center bg-gray-50">
|
<div class="p-6 border-b flex justify-between items-center bg-gray-50">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-xl font-bold text-gray-900">{space.name}</h3>
|
<h3 class="text-xl font-bold text-gray-900">{space.name}</h3>
|
||||||
<p class="text-sm text-gray-500 uppercase tracking-wider font-semibold">{space.type.replace('_', ' ')}</p>
|
<p class="text-sm text-gray-500 uppercase tracking-wider font-semibold">{$t(`booking.spaceType.${space.type}`)}</p>
|
||||||
</div>
|
</div>
|
||||||
<button on:click={() => dispatch('close')} class="p-2 hover:bg-gray-200 rounded-full transition-colors text-gray-400">
|
<button on:click={() => dispatch('close')} class="p-2 hover:bg-gray-200 rounded-full transition-colors text-gray-400">
|
||||||
<X size={24} />
|
<X size={24} />
|
||||||
|
|
@ -180,7 +186,7 @@
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
{#if space.type === 'DESK' && status.isOccupied}
|
{#if space.type === 'DESK' && status.isOccupied}
|
||||||
<div class="bg-yellow-50 border border-yellow-200 rounded-xl p-4 mb-6">
|
<div class="bg-yellow-50 border border-yellow-200 rounded-xl p-4 mb-6">
|
||||||
<p class="text-yellow-800 font-bold mb-1">Currently Booked</p>
|
<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 justify-between gap-3">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="w-10 h-10 rounded-full bg-yellow-200 flex items-center justify-center text-yellow-700 font-bold">
|
<div class="w-10 h-10 rounded-full bg-yellow-200 flex items-center justify-center text-yellow-700 font-bold">
|
||||||
|
|
@ -202,7 +208,7 @@
|
||||||
class="flex items-center gap-2 px-4 py-2 bg-red-50 text-red-600 hover:bg-red-100 rounded-xl transition-colors font-bold text-sm border border-red-100 shadow-sm"
|
class="flex items-center gap-2 px-4 py-2 bg-red-50 text-red-600 hover:bg-red-100 rounded-xl transition-colors font-bold text-sm border border-red-100 shadow-sm"
|
||||||
>
|
>
|
||||||
<Trash2 size={18} />
|
<Trash2 size={18} />
|
||||||
Delete Booking
|
{$t('booking.delete')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -212,27 +218,27 @@
|
||||||
<div class="mt-4 p-3 bg-red-100 border border-red-200 rounded-lg">
|
<div class="mt-4 p-3 bg-red-100 border border-red-200 rounded-lg">
|
||||||
<div class="flex items-center gap-2 text-red-800 text-sm font-bold mb-2">
|
<div class="flex items-center gap-2 text-red-800 text-sm font-bold mb-2">
|
||||||
<AlertTriangle size={16} />
|
<AlertTriangle size={16} />
|
||||||
Warning: Admin Action
|
{$t('booking.adminWarning')}
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-red-700 mb-3">You are about to delete another user's booking. This action cannot be undone.</p>
|
<p class="text-xs text-red-700 mb-3">{$t('booking.adminDeleteInfo')}</p>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button
|
<button
|
||||||
on:click={() => handleDelete(status.reservation.id)}
|
on:click={() => handleDelete(status.reservation.id)}
|
||||||
class="flex-1 py-2 bg-red-600 text-white text-xs font-bold rounded-md hover:bg-red-700"
|
class="flex-1 py-2 bg-red-600 text-white text-xs font-bold rounded-md hover:bg-red-700"
|
||||||
>
|
>
|
||||||
Confirm Delete
|
{$t('booking.confirmDelete')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
on:click={() => confirmDelete = false}
|
on:click={() => confirmDelete = false}
|
||||||
class="flex-1 py-2 bg-white text-gray-700 text-xs font-bold border border-gray-300 rounded-md hover:bg-gray-50"
|
class="flex-1 py-2 bg-white text-gray-700 text-xs font-bold border border-gray-300 rounded-md hover:bg-gray-50"
|
||||||
>
|
>
|
||||||
Cancel
|
{$t('common.cancel')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<p class="text-center text-gray-500 text-sm">This space is currently unavailable for booking.</p>
|
<p class="text-center text-gray-500 text-sm">{$t('booking.unavailable')}</p>
|
||||||
{:else}
|
{:else}
|
||||||
{#if error}
|
{#if error}
|
||||||
<div class="bg-red-50 text-red-600 p-3 rounded-lg mb-4 text-sm font-medium border border-red-100">
|
<div class="bg-red-50 text-red-600 p-3 rounded-lg mb-4 text-sm font-medium border border-red-100">
|
||||||
|
|
@ -244,7 +250,7 @@
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-bold text-gray-700 mb-1 flex items-center gap-2">
|
<label class="block text-sm font-bold text-gray-700 mb-1 flex items-center gap-2">
|
||||||
<Calendar size={16} />
|
<Calendar size={16} />
|
||||||
Booking Date
|
{$t('booking.bookingDate')}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
|
|
@ -258,7 +264,7 @@
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<label class="block text-sm font-bold text-gray-700 mb-1 flex items-center gap-2">
|
<label class="block text-sm font-bold text-gray-700 mb-1 flex items-center gap-2">
|
||||||
<Clock size={16} />
|
<Clock size={16} />
|
||||||
Availability for {bookingDate}
|
{$t('booking.availabilityFor', { values: { date: bookingDate } })}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
{#if loadingReservations}
|
{#if loadingReservations}
|
||||||
|
|
@ -278,7 +284,7 @@
|
||||||
{reservation ? 'bg-red-100 text-red-700 border-red-200' :
|
{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' :
|
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'}"
|
'bg-white text-gray-600 border-gray-200 hover:border-indigo-400'}"
|
||||||
title={reservation ? `Booked by ${reservation.user.email}` : `Available at ${time}`}
|
title={reservation ? $t('booking.bookedBy', { values: { user: reservation.user.email } }) : $t('booking.availableAt', { values: { time: time } })}
|
||||||
>
|
>
|
||||||
{time}
|
{time}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -299,7 +305,7 @@
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-bold text-gray-700 mb-1">
|
<label class="block text-sm font-bold text-gray-700 mb-1">
|
||||||
Start Time
|
{$t('booking.startTime')}
|
||||||
</label>
|
</label>
|
||||||
<select bind:value={startTime} class="w-full p-3 bg-gray-50 border border-gray-200 rounded-xl focus:ring-2 focus:ring-indigo-500 outline-none font-medium">
|
<select bind:value={startTime} class="w-full p-3 bg-gray-50 border border-gray-200 rounded-xl focus:ring-2 focus:ring-indigo-500 outline-none font-medium">
|
||||||
{#each timeOptions as time}
|
{#each timeOptions as time}
|
||||||
|
|
@ -309,7 +315,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-bold text-gray-700 mb-1">
|
<label class="block text-sm font-bold text-gray-700 mb-1">
|
||||||
End Time
|
{$t('booking.endTime')}
|
||||||
</label>
|
</label>
|
||||||
<select bind:value={endTime} class="w-full p-3 bg-gray-50 border border-gray-200 rounded-xl focus:ring-2 focus:ring-indigo-500 outline-none font-medium">
|
<select bind:value={endTime} class="w-full p-3 bg-gray-50 border border-gray-200 rounded-xl focus:ring-2 focus:ring-indigo-500 outline-none font-medium">
|
||||||
{#each timeOptions as time}
|
{#each timeOptions as time}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { register, init, getLocaleFromNavigator } from 'svelte-i18n';
|
||||||
|
|
||||||
|
register('en', () => import('./locales/en.json'));
|
||||||
|
register('es-ES', () => import('./locales/es-ES.json'));
|
||||||
|
|
||||||
|
export function setupI18n(initialLocale = 'en') {
|
||||||
|
init({
|
||||||
|
fallbackLocale: 'en',
|
||||||
|
initialLocale: initialLocale || getLocaleFromNavigator() || 'en',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"organization": "Organization",
|
||||||
|
"users": "Users",
|
||||||
|
"reports": "Reports",
|
||||||
|
"dashboard": "Dashboard",
|
||||||
|
"myBookings": "My Bookings",
|
||||||
|
"profile": "Profile",
|
||||||
|
"login": "Login"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"save": "Save",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"edit": "Edit",
|
||||||
|
"delete": "Delete",
|
||||||
|
"actions": "Actions",
|
||||||
|
"loading": "Loading...",
|
||||||
|
"language": "Language"
|
||||||
|
},
|
||||||
|
"organization": {
|
||||||
|
"settings": "Organization Settings",
|
||||||
|
"name": "Organization Name",
|
||||||
|
"logo": "Organization Logo",
|
||||||
|
"updateSuccess": "Organization updated successfully"
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"title": "Dashboard",
|
||||||
|
"welcome": "Welcome back",
|
||||||
|
"selectBuilding": "Select a building to see available floors",
|
||||||
|
"noBuildings": "No buildings found"
|
||||||
|
},
|
||||||
|
"booking": {
|
||||||
|
"book": "Book",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"confirm": "Confirm Booking",
|
||||||
|
"startTime": "Start Time",
|
||||||
|
"endTime": "End Time",
|
||||||
|
"date": "Date",
|
||||||
|
"delete": "Delete Booking",
|
||||||
|
"confirmDelete": "Confirm Delete",
|
||||||
|
"adminWarning": "Warning: Admin Action",
|
||||||
|
"adminDeleteInfo": "You are about to delete another user's booking. This action cannot be undone.",
|
||||||
|
"unavailable": "This space is currently unavailable for booking.",
|
||||||
|
"bookingDate": "Booking Date",
|
||||||
|
"availabilityFor": "Availability for {date}",
|
||||||
|
"bookedBy": "Booked by {user}",
|
||||||
|
"availableAt": "Available at {time}",
|
||||||
|
"room": "Room",
|
||||||
|
"desk": "Desk",
|
||||||
|
"amenity": "Amenity",
|
||||||
|
"currentlyBooked": "Currently Booked",
|
||||||
|
"spaceType": {
|
||||||
|
"DESK": "Desk",
|
||||||
|
"MEETING_ROOM": "Meeting Room",
|
||||||
|
"AMENITY": "Amenity"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"myBookings": {
|
||||||
|
"title": "My Bookings",
|
||||||
|
"description": "Manage your upcoming and past reservations.",
|
||||||
|
"noBookings": "No bookings found",
|
||||||
|
"noBookingsDesc": "You haven't made any reservations yet.",
|
||||||
|
"goToDashboard": "Go to dashboard to book a space",
|
||||||
|
"cancelConfirm": "Are you sure you want to cancel this booking?",
|
||||||
|
"cancelFailed": "Failed to cancel booking",
|
||||||
|
"updateFailed": "Failed to update booking",
|
||||||
|
"loadFailed": "Failed to load bookings"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"organization": "Organización",
|
||||||
|
"users": "Usuarios",
|
||||||
|
"reports": "Informes",
|
||||||
|
"dashboard": "Panel de control",
|
||||||
|
"myBookings": "Mis reservas",
|
||||||
|
"profile": "Perfil",
|
||||||
|
"login": "Iniciar sesión"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"save": "Guardar",
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"edit": "Editar",
|
||||||
|
"delete": "Eliminar",
|
||||||
|
"actions": "Acciones",
|
||||||
|
"loading": "Cargando...",
|
||||||
|
"language": "Idioma"
|
||||||
|
},
|
||||||
|
"organization": {
|
||||||
|
"settings": "Configuración de la organización",
|
||||||
|
"name": "Nombre de la organización",
|
||||||
|
"logo": "Logotipo de la organización",
|
||||||
|
"updateSuccess": "Organización actualizada con éxito"
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"title": "Panel de control",
|
||||||
|
"welcome": "Bienvenido/a de nuevo",
|
||||||
|
"selectBuilding": "Seleccione un edificio para ver las plantas disponibles",
|
||||||
|
"noBuildings": "No se han encontrado edificios"
|
||||||
|
},
|
||||||
|
"booking": {
|
||||||
|
"book": "Reservar",
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"confirm": "Confirmar reserva",
|
||||||
|
"startTime": "Hora de inicio",
|
||||||
|
"endTime": "Hora de finalización",
|
||||||
|
"date": "Fecha",
|
||||||
|
"delete": "Eliminar reserva",
|
||||||
|
"confirmDelete": "Confirmar eliminación",
|
||||||
|
"adminWarning": "Advertencia: Acción de administrador",
|
||||||
|
"adminDeleteInfo": "Está a punto de eliminar la reserva de otro usuario. Esta acción no se puede deshacer.",
|
||||||
|
"unavailable": "Este espacio no está disponible actualmente para reservar.",
|
||||||
|
"bookingDate": "Fecha de la reserva",
|
||||||
|
"availabilityFor": "Disponibilidad para {date}",
|
||||||
|
"bookedBy": "Reservado por {user}",
|
||||||
|
"availableAt": "Disponible a las {time}",
|
||||||
|
"room": "Sala",
|
||||||
|
"desk": "Puesto",
|
||||||
|
"amenity": "Servicio",
|
||||||
|
"currentlyBooked": "Reservado actualmente",
|
||||||
|
"spaceType": {
|
||||||
|
"DESK": "Puesto",
|
||||||
|
"MEETING_ROOM": "Sala de reuniones",
|
||||||
|
"AMENITY": "Servicio"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"myBookings": {
|
||||||
|
"title": "Mis reservas",
|
||||||
|
"description": "Gestione sus reservas próximas y pasadas.",
|
||||||
|
"noBookings": "No se han encontrado reservas",
|
||||||
|
"noBookingsDesc": "Aún no ha realizado ninguna reserva.",
|
||||||
|
"goToDashboard": "Ir al panel para reservar un espacio",
|
||||||
|
"cancelConfirm": "¿Está seguro/a de que desea cancelar esta reserva?",
|
||||||
|
"cancelFailed": "Error al cancelar la reserva",
|
||||||
|
"updateFailed": "Error al actualizar la reserva",
|
||||||
|
"loadFailed": "Error al cargar las reservas"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ export const organization = writable<{
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
logoUrl?: string;
|
logoUrl?: string;
|
||||||
|
language?: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
export async function loadOrganization(orgId: string) {
|
export async function loadOrganization(orgId: string) {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,10 @@
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { resolveAssetUrl } from "$lib/utils/url";
|
import { resolveAssetUrl } from "$lib/utils/url";
|
||||||
import Avatar from "$lib/components/Avatar.svelte";
|
import Avatar from "$lib/components/Avatar.svelte";
|
||||||
|
import { setupI18n } from "$lib/i18n";
|
||||||
|
import { t, locale, waitLocale } from "svelte-i18n";
|
||||||
|
|
||||||
|
setupI18n();
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if ($user?.organizationId) {
|
if ($user?.organizationId) {
|
||||||
|
|
@ -12,11 +16,20 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$: if ($organization?.language) {
|
||||||
|
locale.set($organization.language);
|
||||||
|
}
|
||||||
|
|
||||||
$: if ($user?.organizationId && !$organization) {
|
$: if ($user?.organizationId && !$organization) {
|
||||||
loadOrganization($user.organizationId);
|
loadOrganization($user.organizationId);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{#await waitLocale()}
|
||||||
|
<div class="min-h-screen flex items-center justify-center">
|
||||||
|
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600"></div>
|
||||||
|
</div>
|
||||||
|
{:then}
|
||||||
<div class="min-h-screen bg-gray-50">
|
<div class="min-h-screen bg-gray-50">
|
||||||
<nav class="bg-white shadow-sm px-4 py-3 flex justify-between items-center">
|
<nav class="bg-white shadow-sm px-4 py-3 flex justify-between items-center">
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
|
|
@ -32,18 +45,18 @@
|
||||||
<div class="space-x-4 flex items-center">
|
<div class="space-x-4 flex items-center">
|
||||||
{#if $token}
|
{#if $token}
|
||||||
{#if $user?.role === 'ADMIN'}
|
{#if $user?.role === 'ADMIN'}
|
||||||
<a href="/admin/organization" class="text-gray-600 hover:text-indigo-600 font-medium">Organization</a>
|
<a href="/admin/organization" class="text-gray-600 hover:text-indigo-600 font-medium">{$t('nav.organization')}</a>
|
||||||
<a href="/admin/users" class="text-gray-600 hover:text-indigo-600 font-medium">Users</a>
|
<a href="/admin/users" class="text-gray-600 hover:text-indigo-600 font-medium">{$t('nav.users')}</a>
|
||||||
<a href="/admin/reports" class="text-gray-600 hover:text-indigo-600 font-medium">Reports</a>
|
<a href="/admin/reports" class="text-gray-600 hover:text-indigo-600 font-medium">{$t('nav.reports')}</a>
|
||||||
{/if}
|
{/if}
|
||||||
<a href="/dashboard" class="text-gray-600 hover:text-indigo-600 font-medium">Dashboard</a>
|
<a href="/dashboard" class="text-gray-600 hover:text-indigo-600 font-medium">{$t('nav.dashboard')}</a>
|
||||||
<a href="/dashboard/my-bookings" class="text-gray-600 hover:text-indigo-600 font-medium">My Bookings</a>
|
<a href="/dashboard/my-bookings" class="text-gray-600 hover:text-indigo-600 font-medium">{$t('nav.myBookings')}</a>
|
||||||
<a href="/dashboard/profile" class="bg-indigo-50 text-indigo-700 px-3 py-1.5 rounded-md hover:bg-indigo-100 font-medium flex items-center space-x-2 transition-all">
|
<a href="/dashboard/profile" class="bg-indigo-50 text-indigo-700 px-3 py-1.5 rounded-md hover:bg-indigo-100 font-medium flex items-center space-x-2 transition-all">
|
||||||
<Avatar user={$user} size="sm" />
|
<Avatar user={$user} size="sm" />
|
||||||
<span>Profile</span>
|
<span>{$t('nav.profile')}</span>
|
||||||
</a>
|
</a>
|
||||||
{:else}
|
{:else}
|
||||||
<a href="/login" class="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700 font-medium transition-colors">Login</a>
|
<a href="/login" class="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700 font-medium transition-colors">{$t('nav.login')}</a>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
@ -52,3 +65,4 @@
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
{/await}
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,11 @@
|
||||||
import { apiFetch } from "$lib/api/client";
|
import { apiFetch } from "$lib/api/client";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { resolveAssetUrl } from "$lib/utils/url";
|
import { resolveAssetUrl } from "$lib/utils/url";
|
||||||
|
import { t } from "svelte-i18n";
|
||||||
|
|
||||||
let name = "";
|
let name = "";
|
||||||
let logoUrl = "";
|
let logoUrl = "";
|
||||||
|
let language = "en";
|
||||||
let logoFile: File | null = null;
|
let logoFile: File | null = null;
|
||||||
let loading = false;
|
let loading = false;
|
||||||
let success = false;
|
let success = false;
|
||||||
|
|
@ -23,11 +25,18 @@
|
||||||
if ($organization) {
|
if ($organization) {
|
||||||
name = $organization.name;
|
name = $organization.name;
|
||||||
logoUrl = $organization.logoUrl || "";
|
logoUrl = $organization.logoUrl || "";
|
||||||
|
language = $organization.language || "en";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
|
const orgId = $user?.organizationId;
|
||||||
|
if (!orgId) {
|
||||||
|
error = "User organization ID is missing";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
loading = true;
|
loading = true;
|
||||||
success = false;
|
success = false;
|
||||||
error = "";
|
error = "";
|
||||||
|
|
@ -36,7 +45,7 @@
|
||||||
if (logoFile) {
|
if (logoFile) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', logoFile);
|
formData.append('file', logoFile);
|
||||||
const assetResponse = await apiFetch<any>(`/assets/organization/${$user?.organizationId}/logo`, {
|
const assetResponse = await apiFetch<any>(`/assets/organization/${orgId}/logo`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
});
|
});
|
||||||
|
|
@ -45,9 +54,9 @@
|
||||||
logoUrl = finalLogoUrl;
|
logoUrl = finalLogoUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updated = await apiFetch<any>(`/organizations/${$user?.organizationId}`, {
|
const updated = await apiFetch<any>(`/organizations/${orgId}`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
body: JSON.stringify({ name, logoUrl: finalLogoUrl || "" }),
|
body: JSON.stringify({ name, logoUrl: finalLogoUrl || "", language }),
|
||||||
});
|
});
|
||||||
organization.set(updated);
|
organization.set(updated);
|
||||||
success = true;
|
success = true;
|
||||||
|
|
@ -95,12 +104,12 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="max-w-2xl mx-auto py-8">
|
<div class="max-w-2xl mx-auto py-8">
|
||||||
<h2 class="text-2xl font-bold text-gray-900 mb-6">Organization Settings</h2>
|
<h2 class="text-2xl font-bold text-gray-900 mb-6">{$t('organization.settings')}</h2>
|
||||||
|
|
||||||
<div class="bg-white shadow rounded-lg p-6">
|
<div class="bg-white shadow rounded-lg p-6">
|
||||||
<form on:submit|preventDefault={handleSubmit} class="space-y-6">
|
<form on:submit|preventDefault={handleSubmit} class="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<label for="orgName" class="block text-sm font-medium text-gray-700">Organization Name</label>
|
<label for="orgName" class="block text-sm font-medium text-gray-700">{$t('organization.name')}</label>
|
||||||
<input
|
<input
|
||||||
id="orgName"
|
id="orgName"
|
||||||
type="text"
|
type="text"
|
||||||
|
|
@ -112,7 +121,19 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="logoFile" class="block text-sm font-medium text-gray-700">Logo</label>
|
<label for="orgLanguage" class="block text-sm font-medium text-gray-700">{$t('common.language')}</label>
|
||||||
|
<select
|
||||||
|
id="orgLanguage"
|
||||||
|
bind:value={language}
|
||||||
|
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
||||||
|
>
|
||||||
|
<option value="en">English</option>
|
||||||
|
<option value="es-ES">Español (Ibérico)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="logoFile" class="block text-sm font-medium text-gray-700">{$t('organization.logo')}</label>
|
||||||
<div class="mt-1 flex items-center space-x-4">
|
<div class="mt-1 flex items-center space-x-4">
|
||||||
<input
|
<input
|
||||||
id="logoFile"
|
id="logoFile"
|
||||||
|
|
@ -130,7 +151,7 @@
|
||||||
<label for="logoUrl" class="block text-sm font-medium text-gray-700">Logo URL (Optional)</label>
|
<label for="logoUrl" class="block text-sm font-medium text-gray-700">Logo URL (Optional)</label>
|
||||||
<input
|
<input
|
||||||
id="logoUrl"
|
id="logoUrl"
|
||||||
type="url"
|
type="text"
|
||||||
bind:value={logoUrl}
|
bind:value={logoUrl}
|
||||||
on:input={handleUrlChange}
|
on:input={handleUrlChange}
|
||||||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
||||||
|
|
@ -162,7 +183,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if success}
|
{#if success}
|
||||||
<div class="text-green-600 text-sm mt-2">Organization settings updated successfully!</div>
|
<div class="text-green-600 text-sm mt-2">{$t('organization.updateSuccess')}</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
|
|
@ -171,7 +192,7 @@
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
class="bg-indigo-600 border border-transparent rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
class="bg-indigo-600 border border-transparent rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||||
>
|
>
|
||||||
{loading ? "Saving..." : "Save Changes"}
|
{loading ? $t('common.loading') : $t('common.save')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
import { Building2, ArrowRight, Plus, Edit2 } from "lucide-svelte";
|
import { Building2, ArrowRight, Plus, Edit2 } from "lucide-svelte";
|
||||||
import BuildingEditorModal from "$lib/components/admin/BuildingEditorModal.svelte";
|
import BuildingEditorModal from "$lib/components/admin/BuildingEditorModal.svelte";
|
||||||
import { resolveAssetUrl } from "$lib/utils/url";
|
import { resolveAssetUrl } from "$lib/utils/url";
|
||||||
|
import { t } from "svelte-i18n";
|
||||||
|
|
||||||
let buildings = [];
|
let buildings = [];
|
||||||
let loading = true;
|
let loading = true;
|
||||||
|
|
@ -48,8 +49,8 @@
|
||||||
<div class="p-8 max-w-6xl mx-auto">
|
<div class="p-8 max-w-6xl mx-auto">
|
||||||
<div class="flex justify-between items-center mb-8">
|
<div class="flex justify-between items-center mb-8">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-3xl font-bold text-gray-900">Dashboard</h1>
|
<h1 class="text-3xl font-bold text-gray-900">{$t('dashboard.title')}</h1>
|
||||||
<p class="text-gray-500 mt-1">Welcome back, {$user?.email}!</p>
|
<p class="text-gray-500 mt-1">{$t('dashboard.welcome')}, {$user?.email}!</p>
|
||||||
</div>
|
</div>
|
||||||
{#if $user?.role === 'ADMIN'}
|
{#if $user?.role === 'ADMIN'}
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
import { apiFetch } from "$lib/api/client";
|
import { apiFetch } from "$lib/api/client";
|
||||||
import { Calendar, Clock, MapPin, Trash2, Edit2, ChevronRight, X } from "lucide-svelte";
|
import { Calendar, Clock, MapPin, Trash2, Edit2, ChevronRight, X } from "lucide-svelte";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
|
import { t } from "svelte-i18n";
|
||||||
|
|
||||||
let reservations = [];
|
let reservations = [];
|
||||||
let loading = true;
|
let loading = true;
|
||||||
|
|
@ -23,7 +24,7 @@
|
||||||
try {
|
try {
|
||||||
reservations = await apiFetch("/reservations/my");
|
reservations = await apiFetch("/reservations/my");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = "Failed to load bookings";
|
error = $t('myBookings.loadFailed');
|
||||||
console.error(e);
|
console.error(e);
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
|
|
@ -31,13 +32,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteReservation(id) {
|
async function deleteReservation(id) {
|
||||||
if (!confirm("Are you sure you want to cancel this booking?")) return;
|
if (!confirm($t('myBookings.cancelConfirm'))) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await apiFetch(`/reservations/${id}`, { method: "DELETE" });
|
await apiFetch(`/reservations/${id}`, { method: "DELETE" });
|
||||||
reservations = reservations.filter(r => r.id !== id);
|
reservations = reservations.filter(r => r.id !== id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert("Failed to cancel booking");
|
alert($t('myBookings.cancelFailed'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,26 +66,22 @@
|
||||||
await fetchReservations();
|
await fetchReservations();
|
||||||
closeEditModal();
|
closeEditModal();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
alert(e.message || "Failed to update booking");
|
alert(e.message || $t('myBookings.updateFailed'));
|
||||||
} finally {
|
} finally {
|
||||||
saving = false;
|
saving = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSpaceTypeLabel(type) {
|
function getSpaceTypeLabel(type) {
|
||||||
switch (type) {
|
return $t(`booking.spaceType.${type}`);
|
||||||
case 'DESK': return 'Desk';
|
|
||||||
case 'MEETING_ROOM': return 'Meeting Room';
|
|
||||||
default: return type;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="max-w-4xl mx-auto">
|
<div class="max-w-4xl mx-auto">
|
||||||
<div class="flex justify-between items-center mb-8">
|
<div class="flex justify-between items-center mb-8">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-3xl font-bold text-gray-900">My Bookings</h1>
|
<h1 class="text-3xl font-bold text-gray-900">{$t('myBookings.title')}</h1>
|
||||||
<p class="text-gray-500 mt-1">Manage your upcoming and past reservations.</p>
|
<p class="text-gray-500 mt-1">{$t('myBookings.description')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -99,10 +96,10 @@
|
||||||
{:else if reservations.length === 0}
|
{:else if reservations.length === 0}
|
||||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-12 text-center">
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-12 text-center">
|
||||||
<Calendar class="mx-auto text-gray-300 mb-4" size={48} />
|
<Calendar class="mx-auto text-gray-300 mb-4" size={48} />
|
||||||
<h3 class="text-lg font-medium text-gray-900">No bookings found</h3>
|
<h3 class="text-lg font-medium text-gray-900">{$t('myBookings.noBookings')}</h3>
|
||||||
<p class="text-gray-500 mt-1">You haven't made any reservations yet.</p>
|
<p class="text-gray-500 mt-1">{$t('myBookings.noBookingsDesc')}</p>
|
||||||
<a href="/dashboard" class="mt-6 inline-flex items-center text-indigo-600 font-medium hover:underline">
|
<a href="/dashboard" class="mt-6 inline-flex items-center text-indigo-600 font-medium hover:underline">
|
||||||
Go to dashboard to book a space <ChevronRight size={16} />
|
{$t('myBookings.goToDashboard')} <ChevronRight size={16} />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { user } from '$lib/stores/auth';
|
import { user, logout } from '$lib/stores/auth';
|
||||||
import { apiFetch } from '$lib/api/client';
|
import { apiFetch } from '$lib/api/client';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
import Avatar from '$lib/components/Avatar.svelte';
|
import Avatar from '$lib/components/Avatar.svelte';
|
||||||
import { User, Mail, Shield, Briefcase, Save, Key, UserCircle } from 'lucide-svelte';
|
import { User, Mail, Shield, Briefcase, Save, Key, UserCircle, LogOut } from 'lucide-svelte';
|
||||||
|
|
||||||
let name = $user?.name || '';
|
let name = $user?.name || '';
|
||||||
let avatarUrl = $user?.avatarUrl || '';
|
let avatarUrl = $user?.avatarUrl || '';
|
||||||
|
|
@ -84,11 +85,15 @@
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
error = e.message || 'Failed to upload avatar';
|
error = e.message || 'Failed to upload avatar';
|
||||||
} finally {
|
} finally {
|
||||||
uploadingAvatar = false;
|
|
||||||
// Reset input
|
// Reset input
|
||||||
if (fileInput) fileInput.value = '';
|
if (fileInput) fileInput.value = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleLogout() {
|
||||||
|
logout();
|
||||||
|
goto('/login');
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="max-w-4xl mx-auto p-6">
|
<div class="max-w-4xl mx-auto p-6">
|
||||||
|
|
@ -152,6 +157,14 @@
|
||||||
<UserCircle size={16} class="text-gray-400 shrink-0" />
|
<UserCircle size={16} class="text-gray-400 shrink-0" />
|
||||||
<span class="truncate">ID: {$user?.id}</span>
|
<span class="truncate">ID: {$user?.id}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
on:click={handleLogout}
|
||||||
|
class="w-full flex items-center justify-center gap-2 mt-4 py-2 px-4 border border-red-200 text-red-600 rounded-lg font-medium hover:bg-red-50 transition-all"
|
||||||
|
>
|
||||||
|
<LogOut size={16} />
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue