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" />
|
<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 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.">
|
<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$/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$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" 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/apps/api/src/prisma/prisma.service.ts" beforeDir="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/api/prisma/schema.prisma" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/prisma/schema.prisma" 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/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/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/+layout.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/routes/+layout.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>
|
</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" />
|
||||||
|
|
@ -132,7 +131,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="5157000" />
|
<workItem from="1774706361502" duration="5853000" />
|
||||||
</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" />
|
||||||
|
|
@ -222,7 +221,23 @@
|
||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1774711356754</updated>
|
<updated>1774711356754</updated>
|
||||||
</task>
|
</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 />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
<component name="TypeScriptGeneratedFilesManager">
|
<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 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 "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." />
|
<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>
|
</component>
|
||||||
</project>
|
</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 {
|
model Organization {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
name String
|
name String
|
||||||
|
logoUrl String?
|
||||||
buildings Building[]
|
buildings Building[]
|
||||||
users User[]
|
users User[]
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,13 @@ async function main() {
|
||||||
|
|
||||||
const org = await prisma.organization.upsert({
|
const org = await prisma.organization.upsert({
|
||||||
where: { id: 'test-org-id' },
|
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: {
|
create: {
|
||||||
id: 'test-org-id',
|
id: 'test-org-id',
|
||||||
name: 'Test Organization',
|
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 { EventsModule } from './events/events.module';
|
||||||
import { ReportsModule } from './reports/reports.module';
|
import { ReportsModule } from './reports/reports.module';
|
||||||
import { UsersModule } from './users/users.module';
|
import { UsersModule } from './users/users.module';
|
||||||
|
import { OrganizationsModule } from './organizations/organizations.module';
|
||||||
import { LoggerMiddleware } from './common/middleware/logger.middleware';
|
import { LoggerMiddleware } from './common/middleware/logger.middleware';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
|
@ -21,6 +22,7 @@ import { LoggerMiddleware } from './common/middleware/logger.middleware';
|
||||||
EventsModule,
|
EventsModule,
|
||||||
ReportsModule,
|
ReportsModule,
|
||||||
UsersModule,
|
UsersModule,
|
||||||
|
OrganizationsModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
providers: [AppService],
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ async function bootstrap() {
|
||||||
whitelist: true,
|
whitelist: true,
|
||||||
transform: true,
|
transform: true,
|
||||||
forbidNonWhitelisted: 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">
|
<script lang="ts">
|
||||||
import "../app.css";
|
import "../app.css";
|
||||||
import { token, user } from "$lib/stores/auth";
|
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>
|
</script>
|
||||||
|
|
||||||
<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">
|
||||||
<div class="w-8 h-8 bg-indigo-600 rounded-lg flex items-center justify-center">
|
{#if $organization?.logoUrl}
|
||||||
<span class="text-white font-bold">H</span>
|
<img src={$organization.logoUrl} alt="Logo" class="w-8 h-8 rounded-lg object-contain" />
|
||||||
</div>
|
{:else}
|
||||||
<h1 class="text-xl font-bold text-gray-800">Hot Desking</h1>
|
<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>
|
||||||
<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/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">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">Reports</a>
|
||||||
{/if}
|
{/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