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" />
|
||||
</component>
|
||||
<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$/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/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/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/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/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$/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>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
|
|
@ -115,8 +115,8 @@
|
|||
</configuration>
|
||||
<recent_temporary>
|
||||
<list>
|
||||
<item itemvalue="npm.api > start:dev" />
|
||||
<item itemvalue="npm.web > dev" />
|
||||
<item itemvalue="npm.api > start:dev" />
|
||||
<item itemvalue="Docker.docker-compose.yml: Compose Deployment" />
|
||||
</list>
|
||||
</recent_temporary>
|
||||
|
|
@ -137,7 +137,7 @@
|
|||
<updated>1774643312599</updated>
|
||||
<workItem from="1774643313611" duration="74000" />
|
||||
<workItem from="1774643422240" duration="5278000" />
|
||||
<workItem from="1774706361502" duration="8461000" />
|
||||
<workItem from="1774706361502" duration="9064000" />
|
||||
</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" />
|
||||
|
|
@ -259,7 +259,15 @@
|
|||
<option name="project" value="LOCAL" />
|
||||
<updated>1774714222515</updated>
|
||||
</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 />
|
||||
</component>
|
||||
<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 - 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." />
|
||||
<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>
|
||||
</project>
|
||||
|
|
@ -10,6 +10,7 @@ datasource db {
|
|||
model Organization {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
language String @default("en")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
logoUrl String?
|
||||
|
|
|
|||
|
|
@ -9,4 +9,8 @@ export class UpdateOrganizationDto {
|
|||
// @IsUrl()
|
||||
@IsString()
|
||||
logoUrl?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
language?: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export class UsersService {
|
|||
avatarUrl: true,
|
||||
avatarId: true,
|
||||
role: true,
|
||||
organizationId: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
|
|
@ -38,6 +39,7 @@ export class UsersService {
|
|||
avatarUrl: true,
|
||||
avatarId: true,
|
||||
role: true,
|
||||
organizationId: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
|
|
@ -68,6 +70,7 @@ export class UsersService {
|
|||
avatarUrl: true,
|
||||
avatarId: true,
|
||||
role: true,
|
||||
organizationId: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -31,6 +31,7 @@
|
|||
"date-fns": "^4.1.0",
|
||||
"import-meta-resolve": "^4.2.0",
|
||||
"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 { apiFetch } from '$lib/api/client';
|
||||
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 status: any;
|
||||
|
|
@ -13,6 +16,9 @@
|
|||
let reservations: any[] = [];
|
||||
let loadingReservations = false;
|
||||
|
||||
$: dateLocale = $locale === 'es-ES' ? es : enUS;
|
||||
$: formattedDate = format(selectedDate, 'PPP', { locale: dateLocale });
|
||||
|
||||
$: {
|
||||
if (selectedDate) {
|
||||
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>
|
||||
<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>
|
||||
<button on:click={() => dispatch('close')} class="p-2 hover:bg-gray-200 rounded-full transition-colors text-gray-400">
|
||||
<X size={24} />
|
||||
|
|
@ -180,7 +186,7 @@
|
|||
<div class="p-6">
|
||||
{#if space.type === 'DESK' && status.isOccupied}
|
||||
<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 gap-3">
|
||||
<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"
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
Delete Booking
|
||||
{$t('booking.delete')}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -212,27 +218,27 @@
|
|||
<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">
|
||||
<AlertTriangle size={16} />
|
||||
Warning: Admin Action
|
||||
{$t('booking.adminWarning')}
|
||||
</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">
|
||||
<button
|
||||
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"
|
||||
>
|
||||
Confirm Delete
|
||||
{$t('booking.confirmDelete')}
|
||||
</button>
|
||||
<button
|
||||
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"
|
||||
>
|
||||
Cancel
|
||||
{$t('common.cancel')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</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}
|
||||
{#if error}
|
||||
<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>
|
||||
<label class="block text-sm font-bold text-gray-700 mb-1 flex items-center gap-2">
|
||||
<Calendar size={16} />
|
||||
Booking Date
|
||||
{$t('booking.bookingDate')}
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
|
|
@ -258,7 +264,7 @@
|
|||
<div class="space-y-4">
|
||||
<label class="block text-sm font-bold text-gray-700 mb-1 flex items-center gap-2">
|
||||
<Clock size={16} />
|
||||
Availability for {bookingDate}
|
||||
{$t('booking.availabilityFor', { values: { date: bookingDate } })}
|
||||
</label>
|
||||
|
||||
{#if loadingReservations}
|
||||
|
|
@ -278,7 +284,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 ? `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}
|
||||
</button>
|
||||
|
|
@ -299,7 +305,7 @@
|
|||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-gray-700 mb-1">
|
||||
Start Time
|
||||
{$t('booking.startTime')}
|
||||
</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">
|
||||
{#each timeOptions as time}
|
||||
|
|
@ -309,7 +315,7 @@
|
|||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-gray-700 mb-1">
|
||||
End Time
|
||||
{$t('booking.endTime')}
|
||||
</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">
|
||||
{#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;
|
||||
name: string;
|
||||
logoUrl?: string;
|
||||
language?: string;
|
||||
} | null>(null);
|
||||
|
||||
export async function loadOrganization(orgId: string) {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
import { onMount } from "svelte";
|
||||
import { resolveAssetUrl } from "$lib/utils/url";
|
||||
import Avatar from "$lib/components/Avatar.svelte";
|
||||
import { setupI18n } from "$lib/i18n";
|
||||
import { t, locale, waitLocale } from "svelte-i18n";
|
||||
|
||||
setupI18n();
|
||||
|
||||
onMount(async () => {
|
||||
if ($user?.organizationId) {
|
||||
|
|
@ -12,11 +16,20 @@
|
|||
}
|
||||
});
|
||||
|
||||
$: if ($organization?.language) {
|
||||
locale.set($organization.language);
|
||||
}
|
||||
|
||||
$: if ($user?.organizationId && !$organization) {
|
||||
loadOrganization($user.organizationId);
|
||||
}
|
||||
</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">
|
||||
<nav class="bg-white shadow-sm px-4 py-3 flex justify-between items-center">
|
||||
<div class="flex items-center space-x-2">
|
||||
|
|
@ -32,18 +45,18 @@
|
|||
<div class="space-x-4 flex items-center">
|
||||
{#if $token}
|
||||
{#if $user?.role === 'ADMIN'}
|
||||
<a href="/admin/organization" class="text-gray-600 hover:text-indigo-600 font-medium">Organization</a>
|
||||
<a href="/admin/users" class="text-gray-600 hover:text-indigo-600 font-medium">Users</a>
|
||||
<a href="/admin/reports" class="text-gray-600 hover:text-indigo-600 font-medium">Reports</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">{$t('nav.users')}</a>
|
||||
<a href="/admin/reports" class="text-gray-600 hover:text-indigo-600 font-medium">{$t('nav.reports')}</a>
|
||||
{/if}
|
||||
<a href="/dashboard" class="text-gray-600 hover:text-indigo-600 font-medium">Dashboard</a>
|
||||
<a href="/dashboard/my-bookings" class="text-gray-600 hover:text-indigo-600 font-medium">My Bookings</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">{$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">
|
||||
<Avatar user={$user} size="sm" />
|
||||
<span>Profile</span>
|
||||
<span>{$t('nav.profile')}</span>
|
||||
</a>
|
||||
{: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}
|
||||
</div>
|
||||
</nav>
|
||||
|
|
@ -52,3 +65,4 @@
|
|||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
{/await}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,11 @@
|
|||
import { apiFetch } from "$lib/api/client";
|
||||
import { onMount } from "svelte";
|
||||
import { resolveAssetUrl } from "$lib/utils/url";
|
||||
import { t } from "svelte-i18n";
|
||||
|
||||
let name = "";
|
||||
let logoUrl = "";
|
||||
let language = "en";
|
||||
let logoFile: File | null = null;
|
||||
let loading = false;
|
||||
let success = false;
|
||||
|
|
@ -23,11 +25,18 @@
|
|||
if ($organization) {
|
||||
name = $organization.name;
|
||||
logoUrl = $organization.logoUrl || "";
|
||||
language = $organization.language || "en";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
const orgId = $user?.organizationId;
|
||||
if (!orgId) {
|
||||
error = "User organization ID is missing";
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
success = false;
|
||||
error = "";
|
||||
|
|
@ -36,7 +45,7 @@
|
|||
if (logoFile) {
|
||||
const formData = new FormData();
|
||||
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',
|
||||
body: formData,
|
||||
});
|
||||
|
|
@ -45,9 +54,9 @@
|
|||
logoUrl = finalLogoUrl;
|
||||
}
|
||||
|
||||
const updated = await apiFetch<any>(`/organizations/${$user?.organizationId}`, {
|
||||
const updated = await apiFetch<any>(`/organizations/${orgId}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ name, logoUrl: finalLogoUrl || "" }),
|
||||
body: JSON.stringify({ name, logoUrl: finalLogoUrl || "", language }),
|
||||
});
|
||||
organization.set(updated);
|
||||
success = true;
|
||||
|
|
@ -95,12 +104,12 @@
|
|||
</script>
|
||||
|
||||
<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">
|
||||
<form on:submit|preventDefault={handleSubmit} class="space-y-6">
|
||||
<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
|
||||
id="orgName"
|
||||
type="text"
|
||||
|
|
@ -112,7 +121,19 @@
|
|||
</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">
|
||||
<input
|
||||
id="logoFile"
|
||||
|
|
@ -130,7 +151,7 @@
|
|||
<label for="logoUrl" class="block text-sm font-medium text-gray-700">Logo URL (Optional)</label>
|
||||
<input
|
||||
id="logoUrl"
|
||||
type="url"
|
||||
type="text"
|
||||
bind:value={logoUrl}
|
||||
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"
|
||||
|
|
@ -162,7 +183,7 @@
|
|||
{/if}
|
||||
|
||||
{#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}
|
||||
|
||||
<div class="flex justify-end">
|
||||
|
|
@ -171,7 +192,7 @@
|
|||
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"
|
||||
>
|
||||
{loading ? "Saving..." : "Save Changes"}
|
||||
{loading ? $t('common.loading') : $t('common.save')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
import { Building2, ArrowRight, Plus, Edit2 } from "lucide-svelte";
|
||||
import BuildingEditorModal from "$lib/components/admin/BuildingEditorModal.svelte";
|
||||
import { resolveAssetUrl } from "$lib/utils/url";
|
||||
import { t } from "svelte-i18n";
|
||||
|
||||
let buildings = [];
|
||||
let loading = true;
|
||||
|
|
@ -48,8 +49,8 @@
|
|||
<div class="p-8 max-w-6xl mx-auto">
|
||||
<div class="flex justify-between items-center mb-8">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900">Dashboard</h1>
|
||||
<p class="text-gray-500 mt-1">Welcome back, {$user?.email}!</p>
|
||||
<h1 class="text-3xl font-bold text-gray-900">{$t('dashboard.title')}</h1>
|
||||
<p class="text-gray-500 mt-1">{$t('dashboard.welcome')}, {$user?.email}!</p>
|
||||
</div>
|
||||
{#if $user?.role === 'ADMIN'}
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import { apiFetch } from "$lib/api/client";
|
||||
import { Calendar, Clock, MapPin, Trash2, Edit2, ChevronRight, X } from "lucide-svelte";
|
||||
import { format } from "date-fns";
|
||||
import { t } from "svelte-i18n";
|
||||
|
||||
let reservations = [];
|
||||
let loading = true;
|
||||
|
|
@ -23,7 +24,7 @@
|
|||
try {
|
||||
reservations = await apiFetch("/reservations/my");
|
||||
} catch (e) {
|
||||
error = "Failed to load bookings";
|
||||
error = $t('myBookings.loadFailed');
|
||||
console.error(e);
|
||||
} finally {
|
||||
loading = false;
|
||||
|
|
@ -31,13 +32,13 @@
|
|||
}
|
||||
|
||||
async function deleteReservation(id) {
|
||||
if (!confirm("Are you sure you want to cancel this booking?")) return;
|
||||
if (!confirm($t('myBookings.cancelConfirm'))) return;
|
||||
|
||||
try {
|
||||
await apiFetch(`/reservations/${id}`, { method: "DELETE" });
|
||||
reservations = reservations.filter(r => r.id !== id);
|
||||
} catch (e) {
|
||||
alert("Failed to cancel booking");
|
||||
alert($t('myBookings.cancelFailed'));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -65,26 +66,22 @@
|
|||
await fetchReservations();
|
||||
closeEditModal();
|
||||
} catch (e: any) {
|
||||
alert(e.message || "Failed to update booking");
|
||||
alert(e.message || $t('myBookings.updateFailed'));
|
||||
} finally {
|
||||
saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
function getSpaceTypeLabel(type) {
|
||||
switch (type) {
|
||||
case 'DESK': return 'Desk';
|
||||
case 'MEETING_ROOM': return 'Meeting Room';
|
||||
default: return type;
|
||||
}
|
||||
return $t(`booking.spaceType.${type}`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="flex justify-between items-center mb-8">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900">My Bookings</h1>
|
||||
<p class="text-gray-500 mt-1">Manage your upcoming and past reservations.</p>
|
||||
<h1 class="text-3xl font-bold text-gray-900">{$t('myBookings.title')}</h1>
|
||||
<p class="text-gray-500 mt-1">{$t('myBookings.description')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -99,10 +96,10 @@
|
|||
{:else if reservations.length === 0}
|
||||
<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} />
|
||||
<h3 class="text-lg font-medium text-gray-900">No bookings found</h3>
|
||||
<p class="text-gray-500 mt-1">You haven't made any reservations yet.</p>
|
||||
<h3 class="text-lg font-medium text-gray-900">{$t('myBookings.noBookings')}</h3>
|
||||
<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">
|
||||
Go to dashboard to book a space <ChevronRight size={16} />
|
||||
{$t('myBookings.goToDashboard')} <ChevronRight size={16} />
|
||||
</a>
|
||||
</div>
|
||||
{:else}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { user } from '$lib/stores/auth';
|
||||
import { user, logout } from '$lib/stores/auth';
|
||||
import { apiFetch } from '$lib/api/client';
|
||||
import { goto } from '$app/navigation';
|
||||
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 avatarUrl = $user?.avatarUrl || '';
|
||||
|
|
@ -84,11 +85,15 @@
|
|||
} catch (e: any) {
|
||||
error = e.message || 'Failed to upload avatar';
|
||||
} finally {
|
||||
uploadingAvatar = false;
|
||||
// Reset input
|
||||
if (fileInput) fileInput.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
logout();
|
||||
goto('/login');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="max-w-4xl mx-auto p-6">
|
||||
|
|
@ -152,6 +157,14 @@
|
|||
<UserCircle size={16} class="text-gray-400 shrink-0" />
|
||||
<span class="truncate">ID: {$user?.id}</span>
|
||||
</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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue