Add rotation support for spaces in floorplan editor
- Extend `Space` model and Prisma schema to include `rotation` property. - Update API services and DTOs to handle rotation data. - Add UI controls in `FloorplanEditor.svelte` for rotating spaces. - Refactor space rendering logic in both editor and viewer to support rotation.main
parent
4bc2454f44
commit
e03335be67
|
|
@ -4,7 +4,14 @@
|
|||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="6c8b3c54-eabb-40e5-967f-c7b594c750bc" name="Changes" comment="Update prompts to include progress indicator for documentation readability" />
|
||||
<list default="true" id="6c8b3c54-eabb-40e5-967f-c7b594c750bc" name="Changes" comment="Add Docker deployment support for API and Web services - Add Dockerfiles for API and Web apps with multi-stage builds. - Configure `docker-compose.yml` to manage services (Postgres, API, Web, pgAdmin). - Add `.dockerignore` for optimized builds. - Include `DOCKER_SETUP.md` for deployment steps and guidance.">
|
||||
<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/spaces/dto/update-space.dto.ts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/src/spaces/dto/update-space.dto.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/apps/api/src/spaces/spaces.service.ts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/src/spaces/spaces.service.ts" 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/admin/FloorplanEditor.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/lib/components/admin/FloorplanEditor.svelte" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||
|
|
@ -125,7 +132,7 @@
|
|||
<updated>1774643312599</updated>
|
||||
<workItem from="1774643313611" duration="74000" />
|
||||
<workItem from="1774643422240" duration="5278000" />
|
||||
<workItem from="1774706361502" duration="9383000" />
|
||||
<workItem from="1774706361502" duration="10075000" />
|
||||
</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" />
|
||||
|
|
@ -271,7 +278,15 @@
|
|||
<option name="project" value="LOCAL" />
|
||||
<updated>1774715728737</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="19" />
|
||||
<task id="LOCAL-00019" summary="Add Docker deployment support for API and Web services - Add Dockerfiles for API and Web apps with multi-stage builds. - Configure `docker-compose.yml` to manage services (Postgres, API, Web, pgAdmin). - Add `.dockerignore` for optimized builds. - Include `DOCKER_SETUP.md` for deployment steps and guidance.">
|
||||
<option name="closed" value="true" />
|
||||
<created>1774715983233</created>
|
||||
<option name="number" value="00019" />
|
||||
<option name="presentableId" value="LOCAL-00019" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1774715983233</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="20" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
|
|
@ -298,6 +313,7 @@
|
|||
<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`)." />
|
||||
<MESSAGE value="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`." />
|
||||
<MESSAGE value="Update prompts to include progress indicator for documentation readability" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="Update prompts to include progress indicator for documentation readability" />
|
||||
<MESSAGE value="Add Docker deployment support for API and Web services - Add Dockerfiles for API and Web apps with multi-stage builds. - Configure `docker-compose.yml` to manage services (Postgres, API, Web, pgAdmin). - Add `.dockerignore` for optimized builds. - Include `DOCKER_SETUP.md` for deployment steps and guidance." />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="Add Docker deployment support for API and Web services - Add Dockerfiles for API and Web apps with multi-stage builds. - Configure `docker-compose.yml` to manage services (Postgres, API, Web, pgAdmin). - Add `.dockerignore` for optimized builds. - Include `DOCKER_SETUP.md` for deployment steps and guidance." />
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -71,6 +71,7 @@ model Space {
|
|||
updatedAt DateTime @updatedAt
|
||||
height Float?
|
||||
width Float?
|
||||
rotation Float? @default(0)
|
||||
reservations Reservation[]
|
||||
floor Floor @relation(fields: [floorId], references: [id])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ export class UpdateSpaceDto {
|
|||
@IsOptional()
|
||||
height?: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
rotation?: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
name?: string;
|
||||
|
|
|
|||
|
|
@ -167,6 +167,7 @@ export class SpacesService {
|
|||
y: data.y,
|
||||
width: data.width,
|
||||
height: data.height,
|
||||
rotation: data.rotation,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -177,6 +178,7 @@ export class SpacesService {
|
|||
y: data.y,
|
||||
width: data.width,
|
||||
height: data.height,
|
||||
rotation: data.rotation,
|
||||
name: data.name,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -149,18 +149,25 @@
|
|||
{@const status = spaceStatus[space.id] || { isOccupied: false }}
|
||||
<button
|
||||
on:click={() => handleSpaceClick(space)}
|
||||
class="absolute p-2 border-2 rounded-lg shadow-md transition-all {space.type !== 'AMENITY' ? 'hover:scale-105 cursor-pointer' : 'cursor-default opacity-80'} {status.isOccupied ? 'bg-yellow-50 border-yellow-500' : 'bg-green-50 border-green-500'}"
|
||||
class="absolute p-2 border-2 rounded-lg shadow-md transition-all overflow-hidden flex items-center justify-center {space.type !== 'AMENITY' ? 'hover:scale-105 cursor-pointer' : 'cursor-default opacity-80'} {status.isOccupied ? 'bg-yellow-50 border-yellow-500' : 'bg-green-50 border-green-500'}"
|
||||
style="left: 0; top: 0; width: {(space.width / 100) * containerWidth}px; height: {(space.height / 100) * containerHeight}px; transform: translate({(space.x / 100) * containerWidth}px, {(space.y / 100) * containerHeight}px)"
|
||||
title="{space.name} ({status.isOccupied ? 'Booked' : 'Available'})"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<div
|
||||
class="flex items-center gap-1 shrink-0 justify-center"
|
||||
style="
|
||||
transform: rotate({space.rotation || 0}deg);
|
||||
width: {(space.rotation % 180 === 90) ? (space.height / 100) * containerHeight : (space.width / 100) * containerWidth}px;
|
||||
height: {(space.rotation % 180 === 90) ? (space.width / 100) * containerWidth : (space.height / 100) * containerHeight}px;
|
||||
"
|
||||
>
|
||||
<div class="relative">
|
||||
<div class={status.isOccupied ? 'text-yellow-600' : 'text-green-600'}>
|
||||
<svelte:component this={getSpaceTypeIcon(space.type, space.name)} size={16} />
|
||||
</div>
|
||||
|
||||
{#if status.isOccupied && space.type === 'DESK'}
|
||||
<div class="absolute -top-4 -left-4 shadow-sm">
|
||||
<div class="absolute -top-4 -left-4 shadow-sm" style="transform: rotate(-{space.rotation || 0}deg);">
|
||||
<Avatar
|
||||
user={status.reservation.user}
|
||||
size="sm"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import interact from 'interactjs';
|
||||
import { Laptop, Bath, Coffee, Users, Save, Upload } from 'lucide-svelte';
|
||||
import { Laptop, Bath, Coffee, Users, Save, Upload, RotateCw } from 'lucide-svelte';
|
||||
import { apiFetch } from '$lib/api/client';
|
||||
import { resolveAssetUrl } from '$lib/utils/url';
|
||||
|
||||
|
|
@ -219,6 +219,7 @@
|
|||
y: 5,
|
||||
width: item.type === 'DESK' ? 12 : 20, // Default widths as percentage
|
||||
height: item.type === 'DESK' ? 8 : 15, // Default heights as percentage
|
||||
rotation: 0,
|
||||
floorId
|
||||
};
|
||||
spaces = [...spaces, newSpace];
|
||||
|
|
@ -287,6 +288,24 @@
|
|||
editingId = null;
|
||||
}
|
||||
|
||||
function rotateSpace(id: string) {
|
||||
const index = spaces.findIndex(s => s.id === id);
|
||||
if (index !== -1) {
|
||||
const space = spaces[index];
|
||||
const oldWidth = space.width;
|
||||
const oldHeight = space.height;
|
||||
|
||||
// Swap width and height
|
||||
spaces[index].width = oldHeight * (containerHeight / containerWidth);
|
||||
spaces[index].height = oldWidth * (containerWidth / containerHeight);
|
||||
|
||||
// Update rotation
|
||||
spaces[index].rotation = ((spaces[index].rotation || 0) + 90) % 360;
|
||||
|
||||
spaces = [...spaces];
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent, id: string) {
|
||||
if (event.key === 'Enter') {
|
||||
stopEditing();
|
||||
|
|
@ -368,13 +387,20 @@
|
|||
{#each spaces as space (space.id)}
|
||||
<div
|
||||
use:interactAction={space}
|
||||
class="draggable-space absolute p-2 bg-white border-2 border-blue-500 rounded-lg shadow-lg group {space.type !== 'DESK' ? 'resizable-space' : ''}"
|
||||
class="draggable-space absolute p-2 bg-white border-2 border-blue-500 rounded-lg shadow-lg group overflow-hidden flex items-center justify-center {space.type !== 'DESK' ? 'resizable-space' : ''}"
|
||||
style="left: 0; top: 0; width: {(space.width / 100) * containerWidth}px; height: {(space.height / 100) * containerHeight}px; transform: translate({(space.x / 100) * containerWidth}px, {(space.y / 100) * containerHeight}px)"
|
||||
data-id={space.id}
|
||||
data-x={(space.x / 100) * containerWidth}
|
||||
data-y={(space.y / 100) * containerHeight}
|
||||
>
|
||||
<div class="flex flex-col h-full drag-handle cursor-move">
|
||||
<div
|
||||
class="flex flex-col drag-handle cursor-move shrink-0 items-center justify-center"
|
||||
style="
|
||||
transform: rotate({space.rotation || 0}deg);
|
||||
width: {(space.rotation % 180 === 90) ? (space.height / 100) * containerHeight : (space.width / 100) * containerWidth}px;
|
||||
height: {(space.rotation % 180 === 90) ? (space.width / 100) * containerWidth : (space.height / 100) * containerHeight}px;
|
||||
"
|
||||
>
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<div class="text-blue-600 shrink-0">
|
||||
{#if space.type === 'DESK'}
|
||||
|
|
@ -408,14 +434,33 @@
|
|||
>
|
||||
{space.name}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
on:click|stopPropagation={() => removeSpace(space.id)}
|
||||
class="hidden group-hover:flex items-center justify-center shrink-0 w-4 h-4 text-white bg-red-500 rounded-full hover:bg-red-600 ml-auto"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
{#if space.type === 'DESK'}
|
||||
<button
|
||||
on:click|stopPropagation={() => rotateSpace(space.id)}
|
||||
class="hidden group-hover:flex items-center justify-center shrink-0 w-4 h-4 text-white bg-blue-500 rounded-full hover:bg-blue-600 ml-auto"
|
||||
title="Rotate Desk"
|
||||
style="transform: rotate(-{space.rotation || 0}deg);"
|
||||
>
|
||||
<RotateCw size={10} />
|
||||
</button>
|
||||
<button
|
||||
on:click|stopPropagation={() => removeSpace(space.id)}
|
||||
class="hidden group-hover:flex items-center justify-center shrink-0 w-4 h-4 text-white bg-red-500 rounded-full hover:bg-red-600 ml-1"
|
||||
style="transform: rotate(-{space.rotation || 0}deg);"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
on:click|stopPropagation={() => removeSpace(space.id)}
|
||||
class="hidden group-hover:flex items-center justify-center shrink-0 w-4 h-4 text-white bg-red-500 rounded-full hover:bg-red-600 ml-auto"
|
||||
style="transform: rotate(-{space.rotation || 0}deg);"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if space.type !== 'DESK'}
|
||||
|
|
|
|||
Loading…
Reference in New Issue