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.main
parent
56c52bfcc7
commit
b618c10cdd
|
|
@ -4,13 +4,12 @@
|
|||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="6c8b3c54-eabb-40e5-967f-c7b594c750bc" name="Changes" comment="Add building and floor management with admin editor modal - Implement `BuildingEditorModal.svelte` for creating, editing, and deleting buildings and floors. - Add `CreateBuildingDto`, `UpdateBuildingDto`, `CreateFloorDto`, and `UpdateFloorDto` for validation. - Extend `SpacesService` and `SpacesController` with endpoints for building and floor CRUD operations. - Update Prisma schema to include building descriptions and image URLs, and floor names. - Enhance dashboard with building management functionality for admin users.">
|
||||
<change beforePath="$PROJECT_DIR$/apps/api/src/events/events.gateway.ts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/src/events/events.gateway.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/apps/api/src/reservations/reservations.service.ts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/src/reservations/reservations.service.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/apps/web/src/lib/components/BookingModal.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/lib/components/BookingModal.svelte" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/apps/web/src/lib/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/SpaceCard.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/lib/components/SpaceCard.svelte" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/apps/web/src/routes/dashboard/floor/[id]/+page.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/routes/dashboard/floor/[id]/+page.svelte" afterDir="false" />
|
||||
<list default="true" id="6c8b3c54-eabb-40e5-967f-c7b594c750bc" name="Changes" comment="Fix ownership checks and improve delete booking button UI - Update `isOwner` logic to account for nested user objects in reservation data. - Enhance delete button styling for better visibility and usability.">
|
||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/apps/api/apps/api/src/prisma/prisma.service.ts" beforeDir="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/app.module.ts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/src/app.module.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" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
|
|
@ -132,7 +131,7 @@
|
|||
<updated>1774643312599</updated>
|
||||
<workItem from="1774643313611" duration="74000" />
|
||||
<workItem from="1774643422240" duration="5278000" />
|
||||
<workItem from="1774706361502" duration="5157000" />
|
||||
<workItem from="1774706361502" duration="5853000" />
|
||||
</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" />
|
||||
|
|
@ -222,7 +221,23 @@
|
|||
<option name="project" value="LOCAL" />
|
||||
<updated>1774711356754</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="12" />
|
||||
<task id="LOCAL-00012" summary="Enhance real-time space status updates and handling - Improve WebSocket connection reliability with updated transport options. - Introduce logic to prioritize real-time updates over initial data for live views. - Add logging for better debugging of WebSocket events and data propagation. - Update UI components (`SpaceCard`, `FloorplanViewer`) to avoid overwrites from stale data. - Refactor date handling for consistent behavior across components.">
|
||||
<option name="closed" value="true" />
|
||||
<created>1774711643921</created>
|
||||
<option name="number" value="00012" />
|
||||
<option name="presentableId" value="LOCAL-00012" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1774711643921</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00013" summary="Fix ownership checks and improve delete booking button UI - Update `isOwner` logic to account for nested user objects in reservation data. - Enhance delete button styling for better visibility and usability.">
|
||||
<option name="closed" value="true" />
|
||||
<created>1774711743510</created>
|
||||
<option name="number" value="00013" />
|
||||
<option name="presentableId" value="LOCAL-00013" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1774711743510</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="14" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
|
|
@ -241,6 +256,8 @@
|
|||
<MESSAGE value="Add user management and registration features with invite-based onboarding - Implement `+page.svelte` for admin user management, allowing role updates, password resets, and invite link generation. Update `UsersController` and `UsersService` to support user role updates, password changes, and invite token generation. - Create DTOs (`RegisterDto`, `UpdateUserDto`) for input validation in user management endpoints. - Extend `AuthService` and `AuthController` with invite-based registration logic. - Update admin layout to include "Users" link for easy access to user management." />
|
||||
<MESSAGE value="Add "My Bookings" page and enable reservation updates and cancellations - Implement `+page.svelte` for managing user reservations with real-time updates. - Add API endpoints for updating and canceling reservations with enhanced validation and authorization. - Introduce `UpdateReservationDto` for refined reservation update handling. - Update `reservations.service.ts` to handle overlapping checks, time adjustments, and admin actions. - Extend layout to include a link to the "My Bookings" page. - Add `date-fns` library for date formatting." />
|
||||
<MESSAGE value="Add building and floor management with admin editor modal - Implement `BuildingEditorModal.svelte` for creating, editing, and deleting buildings and floors. - Add `CreateBuildingDto`, `UpdateBuildingDto`, `CreateFloorDto`, and `UpdateFloorDto` for validation. - Extend `SpacesService` and `SpacesController` with endpoints for building and floor CRUD operations. - Update Prisma schema to include building descriptions and image URLs, and floor names. - Enhance dashboard with building management functionality for admin users." />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="Add building and floor management with admin editor modal - Implement `BuildingEditorModal.svelte` for creating, editing, and deleting buildings and floors. - Add `CreateBuildingDto`, `UpdateBuildingDto`, `CreateFloorDto`, and `UpdateFloorDto` for validation. - Extend `SpacesService` and `SpacesController` with endpoints for building and floor CRUD operations. - Update Prisma schema to include building descriptions and image URLs, and floor names. - Enhance dashboard with building management functionality for admin users." />
|
||||
<MESSAGE value="Enhance real-time space status updates and handling - Improve WebSocket connection reliability with updated transport options. - Introduce logic to prioritize real-time updates over initial data for live views. - Add logging for better debugging of WebSocket events and data propagation. - Update UI components (`SpaceCard`, `FloorplanViewer`) to avoid overwrites from stale data. - Refactor date handling for consistent behavior across components." />
|
||||
<MESSAGE value="Fix ownership checks and improve delete booking button UI - Update `isOwner` logic to account for nested user objects in reservation data. - Enhance delete button styling for better visibility and usability." />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="Fix ownership checks and improve delete booking button UI - Update `isOwner` logic to account for nested user objects in reservation data. - Enhance delete button styling for better visibility and usability." />
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
@Injectable()
|
||||
export class PrismaService extends PrismaClient implements OnModuleInit {
|
||||
async onModuleInit() {
|
||||
await this.$connect();
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ enum SpaceType {
|
|||
model Organization {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
logoUrl String?
|
||||
buildings Building[]
|
||||
users User[]
|
||||
createdAt DateTime @default(now())
|
||||
|
|
|
|||
|
|
@ -8,10 +8,13 @@ async function main() {
|
|||
|
||||
const org = await prisma.organization.upsert({
|
||||
where: { id: 'test-org-id' },
|
||||
update: {},
|
||||
update: {
|
||||
logoUrl: 'https://images.unsplash.com/photo-1542744173-8e7e53415bb0?q=80&w=200&h=200&auto=format&fit=crop',
|
||||
},
|
||||
create: {
|
||||
id: 'test-org-id',
|
||||
name: 'Test Organization',
|
||||
logoUrl: 'https://images.unsplash.com/photo-1542744173-8e7e53415bb0?q=80&w=200&h=200&auto=format&fit=crop',
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { ReservationsModule } from './reservations/reservations.module';
|
|||
import { EventsModule } from './events/events.module';
|
||||
import { ReportsModule } from './reports/reports.module';
|
||||
import { UsersModule } from './users/users.module';
|
||||
import { OrganizationsModule } from './organizations/organizations.module';
|
||||
import { LoggerMiddleware } from './common/middleware/logger.middleware';
|
||||
|
||||
@Module({
|
||||
|
|
@ -21,6 +22,7 @@ import { LoggerMiddleware } from './common/middleware/logger.middleware';
|
|||
EventsModule,
|
||||
ReportsModule,
|
||||
UsersModule,
|
||||
OrganizationsModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ async function bootstrap() {
|
|||
whitelist: true,
|
||||
transform: true,
|
||||
forbidNonWhitelisted: true,
|
||||
skipMissingProperties: false,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
import { IsString, IsOptional, IsUrl } from 'class-validator';
|
||||
|
||||
export class UpdateOrganizationDto {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
logoUrl?: string | null;
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { Controller, Get, Patch, Body, Param, UseGuards, Req, ForbiddenException } from '@nestjs/common';
|
||||
import { OrganizationsService } from './organizations.service';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { RolesGuard } from '../auth/guards/roles.guard';
|
||||
import { Roles } from '../auth/decorators/roles.decorator';
|
||||
import { Role } from '@prisma/client';
|
||||
import { UpdateOrganizationDto } from './dto/update-organization.dto';
|
||||
|
||||
@Controller('organizations')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
export class OrganizationsController {
|
||||
constructor(private readonly organizationsService: OrganizationsService) {}
|
||||
|
||||
@Get(':id')
|
||||
async findOne(@Param('id') id: string, @Req() req) {
|
||||
// Only allow users to see their own organization (or implement more complex logic if needed)
|
||||
if (req.user.organizationId !== id && req.user.role !== Role.ADMIN) {
|
||||
throw new ForbiddenException('You can only access your own organization');
|
||||
}
|
||||
return this.organizationsService.findOne(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@Roles(Role.ADMIN)
|
||||
async update(@Param('id') id: string, @Req() req, @Body() updateOrganizationDto: UpdateOrganizationDto) {
|
||||
if (req.user.organizationId !== id) {
|
||||
throw new ForbiddenException('You can only update your own organization');
|
||||
}
|
||||
return this.organizationsService.update(id, updateOrganizationDto);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { OrganizationsService } from './organizations.service';
|
||||
import { OrganizationsController } from './organizations.controller';
|
||||
import { PrismaModule } from '../prisma/prisma.module';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
controllers: [OrganizationsController],
|
||||
providers: [OrganizationsService],
|
||||
exports: [OrganizationsService],
|
||||
})
|
||||
export class OrganizationsModule {}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { UpdateOrganizationDto } from './dto/update-organization.dto';
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationsService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async findOne(id: string) {
|
||||
const org = await this.prisma.organization.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
if (!org) {
|
||||
throw new NotFoundException(`Organization with ID ${id} not found`);
|
||||
}
|
||||
return org;
|
||||
}
|
||||
|
||||
async update(id: string, updateOrganizationDto: UpdateOrganizationDto) {
|
||||
return this.prisma.organization.update({
|
||||
where: { id },
|
||||
data: updateOrganizationDto,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import { writable } from "svelte/store";
|
||||
import { apiFetch } from "$lib/api/client";
|
||||
|
||||
export const organization = writable<{
|
||||
id: string;
|
||||
name: string;
|
||||
logoUrl?: string;
|
||||
} | null>(null);
|
||||
|
||||
export async function loadOrganization(orgId: string) {
|
||||
try {
|
||||
const org = await apiFetch<any>(`/organizations/${orgId}`);
|
||||
organization.set(org);
|
||||
} catch (e) {
|
||||
console.error("Failed to load organization", e);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +1,36 @@
|
|||
<script lang="ts">
|
||||
import "../app.css";
|
||||
import { token, user } from "$lib/stores/auth";
|
||||
import { organization, loadOrganization } from "$lib/stores/organization";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
onMount(async () => {
|
||||
if ($user?.organizationId) {
|
||||
await loadOrganization($user.organizationId);
|
||||
}
|
||||
});
|
||||
|
||||
$: if ($user?.organizationId && !$organization) {
|
||||
loadOrganization($user.organizationId);
|
||||
}
|
||||
</script>
|
||||
|
||||
<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">
|
||||
<div class="w-8 h-8 bg-indigo-600 rounded-lg flex items-center justify-center">
|
||||
<span class="text-white font-bold">H</span>
|
||||
</div>
|
||||
<h1 class="text-xl font-bold text-gray-800">Hot Desking</h1>
|
||||
{#if $organization?.logoUrl}
|
||||
<img src={$organization.logoUrl} alt="Logo" class="w-8 h-8 rounded-lg object-contain" />
|
||||
{:else}
|
||||
<div class="w-8 h-8 bg-indigo-600 rounded-lg flex items-center justify-center">
|
||||
<span class="text-white font-bold">{$organization?.name?.charAt(0) || 'H'}</span>
|
||||
</div>
|
||||
{/if}
|
||||
<h1 class="text-xl font-bold text-gray-800">{$organization?.name || 'Hot Desking'}</h1>
|
||||
</div>
|
||||
<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>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,112 @@
|
|||
<script lang="ts">
|
||||
import { organization, loadOrganization } from "$lib/stores/organization";
|
||||
import { user } from "$lib/stores/auth";
|
||||
import { apiFetch } from "$lib/api/client";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
let name = "";
|
||||
let logoUrl = "";
|
||||
let loading = false;
|
||||
let success = false;
|
||||
let error = "";
|
||||
|
||||
onMount(async () => {
|
||||
if ($user?.organizationId) {
|
||||
await loadOrganization($user.organizationId);
|
||||
if ($organization) {
|
||||
name = $organization.name;
|
||||
logoUrl = $organization.logoUrl || "";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
loading = true;
|
||||
success = false;
|
||||
error = "";
|
||||
try {
|
||||
const updated = await apiFetch<any>(`/organizations/${$user?.organizationId}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ name, logoUrl: logoUrl || null }),
|
||||
});
|
||||
organization.set(updated);
|
||||
success = true;
|
||||
} catch (e: any) {
|
||||
error = e.message || "Failed to update organization";
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleRemoveLogo() {
|
||||
logoUrl = "";
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="max-w-2xl mx-auto py-8">
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-6">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>
|
||||
<input
|
||||
id="orgName"
|
||||
type="text"
|
||||
bind:value={name}
|
||||
required
|
||||
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"
|
||||
placeholder="Enter organization name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="logoUrl" class="block text-sm font-medium text-gray-700">Logo URL</label>
|
||||
<input
|
||||
id="logoUrl"
|
||||
type="url"
|
||||
bind:value={logoUrl}
|
||||
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"
|
||||
placeholder="https://example.com/logo.png"
|
||||
/>
|
||||
<p class="mt-2 text-sm text-gray-500">Provide a URL for your organization's logo.</p>
|
||||
</div>
|
||||
|
||||
{#if logoUrl}
|
||||
<div class="mt-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<p class="text-sm font-medium text-gray-700">Logo Preview:</p>
|
||||
<button
|
||||
type="button"
|
||||
on:click={handleRemoveLogo}
|
||||
class="text-sm text-red-600 hover:text-red-800"
|
||||
>
|
||||
Remove Logo
|
||||
</button>
|
||||
</div>
|
||||
<div class="w-20 h-20 border rounded-lg flex items-center justify-center overflow-hidden bg-gray-50">
|
||||
<img src={logoUrl} alt="Logo preview" class="max-w-full max-h-full object-contain" />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if error}
|
||||
<div class="text-red-600 text-sm mt-2">{error}</div>
|
||||
{/if}
|
||||
|
||||
{#if success}
|
||||
<div class="text-green-600 text-sm mt-2">Organization settings updated successfully!</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
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"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
Loading…
Reference in New Issue