generated from bastian/boilerplate
mandatory
This commit is contained in:
@@ -22,6 +22,7 @@ import {
|
|||||||
ProjectDocumentEntity,
|
ProjectDocumentEntity,
|
||||||
RenovationTaskEntity,
|
RenovationTaskEntity,
|
||||||
RoomEntity,
|
RoomEntity,
|
||||||
|
RoomTypeEntity,
|
||||||
TaskCommentEntity,
|
TaskCommentEntity,
|
||||||
TaskDependencyEntity,
|
TaskDependencyEntity,
|
||||||
TaskCommentMentionEntity,
|
TaskCommentMentionEntity,
|
||||||
@@ -54,6 +55,7 @@ export const entities = [
|
|||||||
BuildingEntity,
|
BuildingEntity,
|
||||||
FloorEntity,
|
FloorEntity,
|
||||||
RoomEntity,
|
RoomEntity,
|
||||||
|
RoomTypeEntity,
|
||||||
RenovationTaskEntity,
|
RenovationTaskEntity,
|
||||||
ChecklistItemEntity,
|
ChecklistItemEntity,
|
||||||
TaskDependencyEntity,
|
TaskDependencyEntity,
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import type { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddCustomRoomTypes1720000009000 implements MigrationInterface {
|
||||||
|
name = 'AddCustomRoomTypes1720000009000';
|
||||||
|
|
||||||
|
async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`CREATE TABLE room_types (
|
||||||
|
id char(36) NOT NULL,
|
||||||
|
project_id char(36) NOT NULL,
|
||||||
|
\`key\` varchar(40) NOT NULL,
|
||||||
|
name varchar(100) NOT NULL,
|
||||||
|
sort_order int NOT NULL DEFAULT 100,
|
||||||
|
version int NOT NULL DEFAULT 1,
|
||||||
|
created_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
updated_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
|
PRIMARY KEY(id),
|
||||||
|
UNIQUE KEY uq_room_types_project_key(project_id, \`key\`),
|
||||||
|
UNIQUE KEY uq_room_types_project_name(project_id, name),
|
||||||
|
CONSTRAINT fk_room_types_project FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`);
|
||||||
|
await queryRunner.query(
|
||||||
|
'ALTER TABLE furniture_requirements MODIFY room_id char(36) NULL',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
'ALTER TABLE furniture_requirements MODIFY room_id char(36) NOT NULL',
|
||||||
|
);
|
||||||
|
await queryRunner.query('DROP TABLE room_types');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import { AddMentionsAndReminders1720000005000 } from './migrations/1720000005000
|
|||||||
import { AddDefaultProjectFloors1720000006000 } from './migrations/1720000006000-AddDefaultProjectFloors';
|
import { AddDefaultProjectFloors1720000006000 } from './migrations/1720000006000-AddDefaultProjectFloors';
|
||||||
import { AddFurniturePlanning1720000007000 } from './migrations/1720000007000-AddFurniturePlanning';
|
import { AddFurniturePlanning1720000007000 } from './migrations/1720000007000-AddFurniturePlanning';
|
||||||
import { AddRoleAssignmentSources1720000008000 } from './migrations/1720000008000-AddRoleAssignmentSources';
|
import { AddRoleAssignmentSources1720000008000 } from './migrations/1720000008000-AddRoleAssignmentSources';
|
||||||
|
import { AddCustomRoomTypes1720000009000 } from './migrations/1720000009000-AddCustomRoomTypes';
|
||||||
|
|
||||||
const config = loadConfigForCli();
|
const config = loadConfigForCli();
|
||||||
|
|
||||||
@@ -37,5 +38,6 @@ export default new DataSource({
|
|||||||
AddDefaultProjectFloors1720000006000,
|
AddDefaultProjectFloors1720000006000,
|
||||||
AddFurniturePlanning1720000007000,
|
AddFurniturePlanning1720000007000,
|
||||||
AddRoleAssignmentSources1720000008000,
|
AddRoleAssignmentSources1720000008000,
|
||||||
|
AddCustomRoomTypes1720000009000,
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { AddMentionsAndReminders1720000005000 } from './migrations/1720000005000
|
|||||||
import { AddDefaultProjectFloors1720000006000 } from './migrations/1720000006000-AddDefaultProjectFloors';
|
import { AddDefaultProjectFloors1720000006000 } from './migrations/1720000006000-AddDefaultProjectFloors';
|
||||||
import { AddFurniturePlanning1720000007000 } from './migrations/1720000007000-AddFurniturePlanning';
|
import { AddFurniturePlanning1720000007000 } from './migrations/1720000007000-AddFurniturePlanning';
|
||||||
import { AddRoleAssignmentSources1720000008000 } from './migrations/1720000008000-AddRoleAssignmentSources';
|
import { AddRoleAssignmentSources1720000008000 } from './migrations/1720000008000-AddRoleAssignmentSources';
|
||||||
|
import { AddCustomRoomTypes1720000009000 } from './migrations/1720000009000-AddCustomRoomTypes';
|
||||||
|
|
||||||
export function typeOrmOptionsFactory(
|
export function typeOrmOptionsFactory(
|
||||||
config: AppConfigService,
|
config: AppConfigService,
|
||||||
@@ -37,6 +38,7 @@ export function typeOrmOptionsFactory(
|
|||||||
AddDefaultProjectFloors1720000006000,
|
AddDefaultProjectFloors1720000006000,
|
||||||
AddFurniturePlanning1720000007000,
|
AddFurniturePlanning1720000007000,
|
||||||
AddRoleAssignmentSources1720000008000,
|
AddRoleAssignmentSources1720000008000,
|
||||||
|
AddCustomRoomTypes1720000009000,
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
26
apps/backend/src/renovation/dto/furniture.dto.spec.ts
Normal file
26
apps/backend/src/renovation/dto/furniture.dto.spec.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { plainToInstance } from 'class-transformer';
|
||||||
|
import { validate } from 'class-validator';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import {
|
||||||
|
CreateFurnitureOptionDto,
|
||||||
|
CreateFurnitureRequirementDto,
|
||||||
|
} from './furniture.dto';
|
||||||
|
|
||||||
|
describe('optional furniture fields', () => {
|
||||||
|
it('accepts a furniture requirement without a name or room assignment', async () => {
|
||||||
|
const dto = plainToInstance(CreateFurnitureRequirementDto, {});
|
||||||
|
|
||||||
|
expect(await validate(dto)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts an empty optional delivery date', async () => {
|
||||||
|
const dto = plainToInstance(CreateFurnitureOptionDto, {
|
||||||
|
expectedDeliveryDate: '',
|
||||||
|
productUrl: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(dto.expectedDeliveryDate).toBeUndefined();
|
||||||
|
expect(dto.productUrl).toBeUndefined();
|
||||||
|
expect(await validate(dto)).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Transform, Type } from 'class-transformer';
|
import { Transform, Type, type TransformFnParams } from 'class-transformer';
|
||||||
import {
|
import {
|
||||||
ArrayUnique,
|
ArrayUnique,
|
||||||
IsArray,
|
IsArray,
|
||||||
@@ -28,6 +28,11 @@ import {
|
|||||||
FurnitureScenarioType,
|
FurnitureScenarioType,
|
||||||
} from '../entities/furniture.entities';
|
} from '../entities/furniture.entities';
|
||||||
|
|
||||||
|
const emptyStringToUndefined = ({ value }: TransformFnParams): unknown => {
|
||||||
|
const input: unknown = value;
|
||||||
|
return input === '' ? undefined : input;
|
||||||
|
};
|
||||||
|
|
||||||
export class FurnitureListQueryDto {
|
export class FurnitureListQueryDto {
|
||||||
@IsOptional() @Type(() => Number) @IsInt() @Min(1) page = 1;
|
@IsOptional() @Type(() => Number) @IsInt() @Min(1) page = 1;
|
||||||
@IsOptional() @Type(() => Number) @IsInt() @Min(1) @Max(100) pageSize = 25;
|
@IsOptional() @Type(() => Number) @IsInt() @Min(1) @Max(100) pageSize = 25;
|
||||||
@@ -110,10 +115,12 @@ export class FurnitureOptionListQueryDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class CreateFurnitureRequirementDto {
|
export class CreateFurnitureRequirementDto {
|
||||||
@IsUUID() roomId!: string;
|
@IsOptional() @IsUUID() roomId?: string;
|
||||||
@IsString() @Length(1, 160) name!: string;
|
@IsOptional() @IsString() @Length(0, 160) name?: string;
|
||||||
@IsOptional() @IsString() @Length(0, 5000) description?: string;
|
@IsOptional() @IsString() @Length(0, 5000) description?: string;
|
||||||
@IsEnum(FurnitureRequirementCategory) category!: FurnitureRequirementCategory;
|
@IsOptional()
|
||||||
|
@IsEnum(FurnitureRequirementCategory)
|
||||||
|
category = FurnitureRequirementCategory.Other;
|
||||||
@IsEnum(FurnitureRequirementPriority) priority =
|
@IsEnum(FurnitureRequirementPriority) priority =
|
||||||
FurnitureRequirementPriority.Normal;
|
FurnitureRequirementPriority.Normal;
|
||||||
@Type(() => Number) @IsInt() @Min(1) @Max(10000) requiredQuantity = 1;
|
@Type(() => Number) @IsInt() @Min(1) @Max(10000) requiredQuantity = 1;
|
||||||
@@ -128,23 +135,24 @@ export class UpdateFurnitureRequirementDto extends CreateFurnitureRequirementDto
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class CreateFurnitureOptionDto {
|
export class CreateFurnitureOptionDto {
|
||||||
@IsString() @Length(1, 180) name!: string;
|
@IsOptional() @IsString() @Length(0, 180) name?: string;
|
||||||
@IsOptional() @IsString() @Length(0, 160) manufacturer?: string;
|
@IsOptional() @IsString() @Length(0, 160) manufacturer?: string;
|
||||||
@IsOptional() @IsString() @Length(0, 160) model?: string;
|
@IsOptional() @IsString() @Length(0, 160) model?: string;
|
||||||
@IsOptional() @IsString() @Length(0, 5000) description?: string;
|
@IsOptional() @IsString() @Length(0, 5000) description?: string;
|
||||||
@IsOptional() @IsString() @Length(0, 180) retailer?: string;
|
@IsOptional() @IsString() @Length(0, 180) retailer?: string;
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
@Transform(emptyStringToUndefined)
|
||||||
@IsUrl({ require_protocol: true, protocols: ['http', 'https'] })
|
@IsUrl({ require_protocol: true, protocols: ['http', 'https'] })
|
||||||
@Length(0, 1000)
|
@Length(0, 1000)
|
||||||
productUrl?: string;
|
productUrl?: string;
|
||||||
@IsOptional() @IsString() @Length(0, 120) articleNumber?: string;
|
@IsOptional() @IsString() @Length(0, 120) articleNumber?: string;
|
||||||
@Type(() => Number) @IsNumber() @Min(0) unitPrice!: number;
|
@IsOptional() @Type(() => Number) @IsNumber() @Min(0) unitPrice = 0;
|
||||||
@IsOptional() @Type(() => Number) @IsNumber() @Min(0) originalPrice?: number;
|
@IsOptional() @Type(() => Number) @IsNumber() @Min(0) originalPrice?: number;
|
||||||
@IsOptional() @Type(() => Number) @IsNumber() @Min(0) shippingCost = 0;
|
@IsOptional() @Type(() => Number) @IsNumber() @Min(0) shippingCost = 0;
|
||||||
@IsOptional() @Type(() => Number) @IsNumber() @Min(0) additionalCost = 0;
|
@IsOptional() @Type(() => Number) @IsNumber() @Min(0) additionalCost = 0;
|
||||||
@IsOptional() @Type(() => Number) @IsNumber() @Min(0) discount = 0;
|
@IsOptional() @Type(() => Number) @IsNumber() @Min(0) discount = 0;
|
||||||
@IsString() @Length(3, 3) currency = 'EUR';
|
@IsString() @Length(3, 3) currency = 'EUR';
|
||||||
@Type(() => Number) @IsInt() @Min(1) @Max(10000) quantity = 1;
|
@IsOptional() @Type(() => Number) @IsInt() @Min(1) @Max(10000) quantity = 1;
|
||||||
@IsOptional() @Type(() => Number) @IsNumber() @Min(0) width?: number;
|
@IsOptional() @Type(() => Number) @IsNumber() @Min(0) width?: number;
|
||||||
@IsOptional() @Type(() => Number) @IsNumber() @Min(0) height?: number;
|
@IsOptional() @Type(() => Number) @IsNumber() @Min(0) height?: number;
|
||||||
@IsOptional() @Type(() => Number) @IsNumber() @Min(0) depth?: number;
|
@IsOptional() @Type(() => Number) @IsNumber() @Min(0) depth?: number;
|
||||||
@@ -152,9 +160,18 @@ export class CreateFurnitureOptionDto {
|
|||||||
@IsOptional() @IsString() @Length(0, 100) color?: string;
|
@IsOptional() @IsString() @Length(0, 100) color?: string;
|
||||||
@IsOptional() @IsString() @Length(0, 160) material?: string;
|
@IsOptional() @IsString() @Length(0, 160) material?: string;
|
||||||
@IsOptional() @Type(() => Number) @IsInt() @Min(0) deliveryDays?: number;
|
@IsOptional() @Type(() => Number) @IsInt() @Min(0) deliveryDays?: number;
|
||||||
@IsOptional() @IsDateString() earliestDeliveryDate?: string;
|
@Transform(emptyStringToUndefined)
|
||||||
@IsOptional() @IsDateString() expectedDeliveryDate?: string;
|
@IsOptional()
|
||||||
@IsOptional() @IsDateString() returnDeadline?: string;
|
@IsDateString()
|
||||||
|
earliestDeliveryDate?: string;
|
||||||
|
@Transform(emptyStringToUndefined)
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
expectedDeliveryDate?: string;
|
||||||
|
@Transform(emptyStringToUndefined)
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
returnDeadline?: string;
|
||||||
@IsEnum(FurnitureAvailability) availability = FurnitureAvailability.Unknown;
|
@IsEnum(FurnitureAvailability) availability = FurnitureAvailability.Unknown;
|
||||||
@IsOptional() @IsBoolean() favorite = false;
|
@IsOptional() @IsBoolean() favorite = false;
|
||||||
@IsEnum(FurnitureOptionStatus) status = FurnitureOptionStatus.Idea;
|
@IsEnum(FurnitureOptionStatus) status = FurnitureOptionStatus.Idea;
|
||||||
|
|||||||
19
apps/backend/src/renovation/dto/renovation.dto.spec.ts
Normal file
19
apps/backend/src/renovation/dto/renovation.dto.spec.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { plainToInstance } from 'class-transformer';
|
||||||
|
import { validate } from 'class-validator';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { CreateRoomDto, CreateRoomTypeDto } from './renovation.dto';
|
||||||
|
|
||||||
|
describe('optional room fields', () => {
|
||||||
|
it('accepts room creation without user-provided fields', async () => {
|
||||||
|
const dto = plainToInstance(CreateRoomDto, {});
|
||||||
|
|
||||||
|
expect(dto.status).toBe('unplanned');
|
||||||
|
expect(await validate(dto)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects a room type without a visible name', async () => {
|
||||||
|
const dto = plainToInstance(CreateRoomTypeDto, { name: ' ' });
|
||||||
|
|
||||||
|
expect(await validate(dto)).not.toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -27,6 +27,11 @@ function commaSeparated(value: unknown): unknown {
|
|||||||
return typeof value === 'string' ? value.split(',').filter(Boolean) : value;
|
return typeof value === 'string' ? value.split(',').filter(Boolean) : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function trimmedString({ value }: TransformFnParams): unknown {
|
||||||
|
const input: unknown = value;
|
||||||
|
return typeof input === 'string' ? input.trim() : input;
|
||||||
|
}
|
||||||
|
|
||||||
export class VersionDto {
|
export class VersionDto {
|
||||||
@Type(() => Number) @IsInt() @Min(1) version!: number;
|
@Type(() => Number) @IsInt() @Min(1) version!: number;
|
||||||
}
|
}
|
||||||
@@ -52,10 +57,10 @@ export class UpdateFloorDto extends CreateFloorDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class CreateRoomDto {
|
export class CreateRoomDto {
|
||||||
@IsUUID() floorId!: string;
|
@IsOptional() @IsUUID() floorId?: string;
|
||||||
@IsString() @Length(1, 160) name!: string;
|
@IsOptional() @IsString() @Length(0, 160) name?: string;
|
||||||
@IsOptional() @IsString() @Length(0, 4000) description?: string;
|
@IsOptional() @IsString() @Length(0, 4000) description?: string;
|
||||||
@IsString() @Length(1, 40) type!: string;
|
@IsOptional() @IsString() @Length(0, 40) type?: string;
|
||||||
@IsEnum(RoomStatus) status: RoomStatus = RoomStatus.Unplanned;
|
@IsEnum(RoomStatus) status: RoomStatus = RoomStatus.Unplanned;
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Type(() => Number)
|
@Type(() => Number)
|
||||||
@@ -66,6 +71,11 @@ export class CreateRoomDto {
|
|||||||
@IsOptional() @Type(() => Number) @IsNumber() @Min(0) plannedBudget?: number;
|
@IsOptional() @Type(() => Number) @IsNumber() @Min(0) plannedBudget?: number;
|
||||||
@IsOptional() @Type(() => Number) @IsInt() sortOrder?: number;
|
@IsOptional() @Type(() => Number) @IsInt() sortOrder?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class CreateRoomTypeDto {
|
||||||
|
@Transform(trimmedString) @IsString() @Length(1, 100) name!: string;
|
||||||
|
@IsOptional() @Type(() => Number) @IsInt() sortOrder?: number;
|
||||||
|
}
|
||||||
export class UpdateRoomDto extends CreateRoomDto {
|
export class UpdateRoomDto extends CreateRoomDto {
|
||||||
@Type(() => Number) @IsInt() @Min(1) version!: number;
|
@Type(() => Number) @IsInt() @Min(1) version!: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,7 +115,8 @@ export enum FurnitureScenarioStatus {
|
|||||||
])
|
])
|
||||||
export class FurnitureRequirementEntity extends FurnitureVersionedEntity {
|
export class FurnitureRequirementEntity extends FurnitureVersionedEntity {
|
||||||
@Column({ name: 'project_id', type: 'char', length: 36 }) projectId!: string;
|
@Column({ name: 'project_id', type: 'char', length: 36 }) projectId!: string;
|
||||||
@Column({ name: 'room_id', type: 'char', length: 36 }) roomId!: string;
|
@Column({ name: 'room_id', type: 'char', length: 36, nullable: true })
|
||||||
|
roomId!: string | null;
|
||||||
@Column({ type: 'varchar', length: 160 }) name!: string;
|
@Column({ type: 'varchar', length: 160 }) name!: string;
|
||||||
@Column({ type: 'text', nullable: true }) description!: string | null;
|
@Column({ type: 'text', nullable: true }) description!: string | null;
|
||||||
@Column({ type: 'varchar', length: 30 })
|
@Column({ type: 'varchar', length: 30 })
|
||||||
|
|||||||
@@ -68,6 +68,16 @@ export enum RoomStatus {
|
|||||||
Omitted = 'omitted',
|
Omitted = 'omitted',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Entity('room_types')
|
||||||
|
@Index('uq_room_types_project_key', ['projectId', 'key'], { unique: true })
|
||||||
|
@Index('uq_room_types_project_name', ['projectId', 'name'], { unique: true })
|
||||||
|
export class RoomTypeEntity extends VersionedEntity {
|
||||||
|
@Column({ name: 'project_id', type: 'char', length: 36 }) projectId!: string;
|
||||||
|
@Column({ type: 'varchar', length: 40 }) key!: string;
|
||||||
|
@Column({ type: 'varchar', length: 100 }) name!: string;
|
||||||
|
@Column({ name: 'sort_order', type: 'int', default: 100 }) sortOrder!: number;
|
||||||
|
}
|
||||||
|
|
||||||
@Entity('rooms')
|
@Entity('rooms')
|
||||||
@Index('idx_rooms_project_floor_sort', ['projectId', 'floorId', 'sortOrder'])
|
@Index('idx_rooms_project_floor_sort', ['projectId', 'floorId', 'sortOrder'])
|
||||||
export class RoomEntity extends VersionedEntity {
|
export class RoomEntity extends VersionedEntity {
|
||||||
|
|||||||
@@ -96,8 +96,8 @@ export class FurnitureService {
|
|||||||
const saved = await this.furniture.requirements.save(
|
const saved = await this.furniture.requirements.save(
|
||||||
this.furniture.requirements.create({
|
this.furniture.requirements.create({
|
||||||
projectId,
|
projectId,
|
||||||
roomId: dto.roomId,
|
roomId: dto.roomId ?? null,
|
||||||
name: dto.name.trim(),
|
name: dto.name?.trim() || 'Unbenannter Möbelbedarf',
|
||||||
description: dto.description?.trim() || null,
|
description: dto.description?.trim() || null,
|
||||||
category: dto.category,
|
category: dto.category,
|
||||||
priority: dto.priority,
|
priority: dto.priority,
|
||||||
@@ -144,8 +144,8 @@ export class FurnitureService {
|
|||||||
id,
|
id,
|
||||||
dto.version,
|
dto.version,
|
||||||
{
|
{
|
||||||
roomId: dto.roomId,
|
roomId: dto.roomId ?? null,
|
||||||
name: dto.name.trim(),
|
name: dto.name?.trim() || 'Unbenannter Möbelbedarf',
|
||||||
description: dto.description?.trim() || null,
|
description: dto.description?.trim() || null,
|
||||||
category: dto.category,
|
category: dto.category,
|
||||||
priority: dto.priority,
|
priority: dto.priority,
|
||||||
@@ -159,10 +159,10 @@ export class FurnitureService {
|
|||||||
await this.activity(
|
await this.activity(
|
||||||
projectId,
|
projectId,
|
||||||
userId,
|
userId,
|
||||||
before.roomId === dto.roomId
|
before.roomId === (dto.roomId ?? null)
|
||||||
? 'furniture.requirement-updated'
|
? 'furniture.requirement-updated'
|
||||||
: 'furniture.requirement-moved',
|
: 'furniture.requirement-moved',
|
||||||
{ requirementId: id, roomId: dto.roomId },
|
{ requirementId: id, roomId: dto.roomId ?? null },
|
||||||
);
|
);
|
||||||
return this.owned(this.furniture.requirements, projectId, id);
|
return this.owned(this.furniture.requirements, projectId, id);
|
||||||
}
|
}
|
||||||
@@ -1016,7 +1016,7 @@ export class FurnitureService {
|
|||||||
const requirement = requirements.find(
|
const requirement = requirements.find(
|
||||||
(entry) => entry.id === selection.requirementId,
|
(entry) => entry.id === selection.requirementId,
|
||||||
);
|
);
|
||||||
if (requirement)
|
if (requirement?.roomId)
|
||||||
byRoom[requirement.roomId] = sumMoney([
|
byRoom[requirement.roomId] = sumMoney([
|
||||||
byRoom[requirement.roomId] ?? '0',
|
byRoom[requirement.roomId] ?? '0',
|
||||||
totalFor(selection),
|
totalFor(selection),
|
||||||
@@ -1123,7 +1123,7 @@ export class FurnitureService {
|
|||||||
const decimal = (value?: number) =>
|
const decimal = (value?: number) =>
|
||||||
value === undefined ? null : value.toFixed(2);
|
value === undefined ? null : value.toFixed(2);
|
||||||
return {
|
return {
|
||||||
name: dto.name.trim(),
|
name: dto.name?.trim() || 'Unbenannter Möbelvorschlag',
|
||||||
manufacturer: text(dto.manufacturer),
|
manufacturer: text(dto.manufacturer),
|
||||||
model: text(dto.model),
|
model: text(dto.model),
|
||||||
description: text(dto.description),
|
description: text(dto.description),
|
||||||
@@ -1222,10 +1222,10 @@ export class FurnitureService {
|
|||||||
}
|
}
|
||||||
private async validateRequirementLinks(
|
private async validateRequirementLinks(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
roomId: string,
|
roomId?: string,
|
||||||
responsibleUserId?: string,
|
responsibleUserId?: string,
|
||||||
) {
|
) {
|
||||||
await this.owned(this.renovation.rooms, projectId, roomId);
|
if (roomId) await this.owned(this.renovation.rooms, projectId, roomId);
|
||||||
if (responsibleUserId) {
|
if (responsibleUserId) {
|
||||||
const member = await this.projects.findMembership(
|
const member = await this.projects.findMembership(
|
||||||
projectId,
|
projectId,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
CreateFloorDto,
|
CreateFloorDto,
|
||||||
CreateMilestoneDto,
|
CreateMilestoneDto,
|
||||||
CreateRoomDto,
|
CreateRoomDto,
|
||||||
|
CreateRoomTypeDto,
|
||||||
CreateTaskDto,
|
CreateTaskDto,
|
||||||
DocumentMetadataDto,
|
DocumentMetadataDto,
|
||||||
DocumentListQueryDto,
|
DocumentListQueryDto,
|
||||||
@@ -126,6 +127,26 @@ export class RenovationController {
|
|||||||
) {
|
) {
|
||||||
return this.service.rooms(p, this.user(r), q);
|
return this.service.rooms(p, this.user(r), q);
|
||||||
}
|
}
|
||||||
|
@Get('projects/:projectId/room-types') roomTypes(
|
||||||
|
@Req() r: AuthenticatedRequest,
|
||||||
|
@Param('projectId') p: string,
|
||||||
|
) {
|
||||||
|
return this.service.roomTypes(p, this.user(r));
|
||||||
|
}
|
||||||
|
@Post('projects/:projectId/room-types') createRoomType(
|
||||||
|
@Req() r: AuthenticatedRequest,
|
||||||
|
@Param('projectId') p: string,
|
||||||
|
@Body() d: CreateRoomTypeDto,
|
||||||
|
) {
|
||||||
|
return this.service.createRoomType(p, this.user(r), d);
|
||||||
|
}
|
||||||
|
@Delete('projects/:projectId/room-types/:id') deleteRoomType(
|
||||||
|
@Req() r: AuthenticatedRequest,
|
||||||
|
@Param('projectId') p: string,
|
||||||
|
@Param('id') id: string,
|
||||||
|
) {
|
||||||
|
return this.service.deleteRoomType(p, id, this.user(r));
|
||||||
|
}
|
||||||
@Post('projects/:projectId/rooms') createRoom(
|
@Post('projects/:projectId/rooms') createRoom(
|
||||||
@Req() r: AuthenticatedRequest,
|
@Req() r: AuthenticatedRequest,
|
||||||
@Param('projectId') p: string,
|
@Param('projectId') p: string,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
ProjectDocumentEntity,
|
ProjectDocumentEntity,
|
||||||
RenovationTaskEntity,
|
RenovationTaskEntity,
|
||||||
RoomEntity,
|
RoomEntity,
|
||||||
|
RoomTypeEntity,
|
||||||
TaskCommentEntity,
|
TaskCommentEntity,
|
||||||
TaskDependencyEntity,
|
TaskDependencyEntity,
|
||||||
TaskCommentMentionEntity,
|
TaskCommentMentionEntity,
|
||||||
@@ -48,6 +49,7 @@ const renovationEntities = [
|
|||||||
BuildingEntity,
|
BuildingEntity,
|
||||||
FloorEntity,
|
FloorEntity,
|
||||||
RoomEntity,
|
RoomEntity,
|
||||||
|
RoomTypeEntity,
|
||||||
RenovationTaskEntity,
|
RenovationTaskEntity,
|
||||||
ChecklistItemEntity,
|
ChecklistItemEntity,
|
||||||
TaskDependencyEntity,
|
TaskDependencyEntity,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
ProjectDocumentEntity,
|
ProjectDocumentEntity,
|
||||||
RenovationTaskEntity,
|
RenovationTaskEntity,
|
||||||
RoomEntity,
|
RoomEntity,
|
||||||
|
RoomTypeEntity,
|
||||||
TaskCommentEntity,
|
TaskCommentEntity,
|
||||||
TaskCommentMentionEntity,
|
TaskCommentMentionEntity,
|
||||||
TaskDependencyEntity,
|
TaskDependencyEntity,
|
||||||
@@ -34,6 +35,8 @@ export class RenovationRepository {
|
|||||||
readonly buildings: Repository<BuildingEntity>,
|
readonly buildings: Repository<BuildingEntity>,
|
||||||
@InjectRepository(FloorEntity) readonly floors: Repository<FloorEntity>,
|
@InjectRepository(FloorEntity) readonly floors: Repository<FloorEntity>,
|
||||||
@InjectRepository(RoomEntity) readonly rooms: Repository<RoomEntity>,
|
@InjectRepository(RoomEntity) readonly rooms: Repository<RoomEntity>,
|
||||||
|
@InjectRepository(RoomTypeEntity)
|
||||||
|
readonly roomTypes: Repository<RoomTypeEntity>,
|
||||||
@InjectRepository(RenovationTaskEntity)
|
@InjectRepository(RenovationTaskEntity)
|
||||||
readonly tasks: Repository<RenovationTaskEntity>,
|
readonly tasks: Repository<RenovationTaskEntity>,
|
||||||
@InjectRepository(ChecklistItemEntity)
|
@InjectRepository(ChecklistItemEntity)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { randomUUID } from 'node:crypto';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectDataSource } from '@nestjs/typeorm';
|
import { InjectDataSource } from '@nestjs/typeorm';
|
||||||
import { DataSource, EntityManager } from 'typeorm';
|
import { DataSource, EntityManager } from 'typeorm';
|
||||||
@@ -29,6 +30,7 @@ import {
|
|||||||
CreateFloorDto,
|
CreateFloorDto,
|
||||||
CreateMilestoneDto,
|
CreateMilestoneDto,
|
||||||
CreateRoomDto,
|
CreateRoomDto,
|
||||||
|
CreateRoomTypeDto,
|
||||||
CreateTaskDto,
|
CreateTaskDto,
|
||||||
UpdateBudgetCategoryDto,
|
UpdateBudgetCategoryDto,
|
||||||
UpdateBuildingDto,
|
UpdateBuildingDto,
|
||||||
@@ -69,6 +71,7 @@ import type { TaskTemplate } from './templates';
|
|||||||
import { newMentionUserIds } from './mentions';
|
import { newMentionUserIds } from './mentions';
|
||||||
import { FurnitureService } from './furniture.service';
|
import { FurnitureService } from './furniture.service';
|
||||||
import { FurnitureRequirementEntity } from './entities/furniture.entities';
|
import { FurnitureRequirementEntity } from './entities/furniture.entities';
|
||||||
|
import { customRoomTypeKey, defaultRoomTypes } from './room-types';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RenovationService {
|
export class RenovationService {
|
||||||
@@ -91,6 +94,78 @@ export class RenovationService {
|
|||||||
return this.repo.listFloors(projectId);
|
return this.repo.listFloors(projectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async roomTypes(projectId: string, userId: string) {
|
||||||
|
await this.read(projectId, userId);
|
||||||
|
const customTypes = await this.repo.roomTypes.find({
|
||||||
|
where: { projectId },
|
||||||
|
order: { sortOrder: 'ASC', name: 'ASC' },
|
||||||
|
});
|
||||||
|
return [
|
||||||
|
...defaultRoomTypes,
|
||||||
|
...customTypes.map((type) => ({
|
||||||
|
id: type.id,
|
||||||
|
key: type.key,
|
||||||
|
name: type.name,
|
||||||
|
custom: true,
|
||||||
|
sortOrder: type.sortOrder,
|
||||||
|
})),
|
||||||
|
].sort(
|
||||||
|
(left, right) =>
|
||||||
|
left.sortOrder - right.sortOrder || left.name.localeCompare(right.name),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createRoomType(
|
||||||
|
projectId: string,
|
||||||
|
userId: string,
|
||||||
|
dto: CreateRoomTypeDto,
|
||||||
|
) {
|
||||||
|
await this.edit(projectId, userId);
|
||||||
|
const name = dto.name.trim();
|
||||||
|
const duplicateDefault = defaultRoomTypes.some(
|
||||||
|
(type) =>
|
||||||
|
type.name.toLocaleLowerCase('de-DE') ===
|
||||||
|
name.toLocaleLowerCase('de-DE'),
|
||||||
|
);
|
||||||
|
const duplicateCustom = await this.repo.roomTypes.findOneBy({
|
||||||
|
projectId,
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
if (duplicateDefault || duplicateCustom)
|
||||||
|
this.conflict('Dieser Raumtyp ist bereits vorhanden.');
|
||||||
|
const saved = await this.repo.roomTypes.save(
|
||||||
|
this.repo.roomTypes.create({
|
||||||
|
projectId,
|
||||||
|
key: customRoomTypeKey(name, randomUUID().replaceAll('-', '')),
|
||||||
|
name,
|
||||||
|
sortOrder: dto.sortOrder ?? 100,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
id: saved.id,
|
||||||
|
key: saved.key,
|
||||||
|
name: saved.name,
|
||||||
|
custom: true,
|
||||||
|
sortOrder: saved.sortOrder,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteRoomType(projectId: string, id: string, userId: string) {
|
||||||
|
await this.edit(projectId, userId);
|
||||||
|
const roomType = await this.requireOwned(
|
||||||
|
this.repo.roomTypes,
|
||||||
|
projectId,
|
||||||
|
id,
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
await this.repo.rooms.exist({ where: { projectId, type: roomType.key } })
|
||||||
|
)
|
||||||
|
this.conflict(
|
||||||
|
'Der Raumtyp wird noch von mindestens einem Raum verwendet.',
|
||||||
|
);
|
||||||
|
await this.repo.roomTypes.delete({ id, projectId });
|
||||||
|
}
|
||||||
|
|
||||||
async ensureDefaultFloors(projectId: string, userId: string) {
|
async ensureDefaultFloors(projectId: string, userId: string) {
|
||||||
await this.edit(projectId, userId);
|
await this.edit(projectId, userId);
|
||||||
return this.dataSource.transaction(async (manager) => {
|
return this.dataSource.transaction(async (manager) => {
|
||||||
@@ -580,14 +655,16 @@ export class RenovationService {
|
|||||||
|
|
||||||
async createRoom(projectId: string, userId: string, dto: CreateRoomDto) {
|
async createRoom(projectId: string, userId: string, dto: CreateRoomDto) {
|
||||||
await this.edit(projectId, userId);
|
await this.edit(projectId, userId);
|
||||||
await this.requireOwned(this.repo.floors, projectId, dto.floorId);
|
const floorId = await this.resolveRoomFloor(projectId, userId, dto.floorId);
|
||||||
|
const type = dto.type?.trim() || 'other';
|
||||||
|
await this.validateRoomType(projectId, type);
|
||||||
const saved = await this.repo.rooms.save(
|
const saved = await this.repo.rooms.save(
|
||||||
this.repo.rooms.create({
|
this.repo.rooms.create({
|
||||||
projectId,
|
projectId,
|
||||||
floorId: dto.floorId,
|
floorId,
|
||||||
name: dto.name.trim(),
|
name: dto.name?.trim() || 'Unbenannter Raum',
|
||||||
description: dto.description?.trim() || null,
|
description: dto.description?.trim() || null,
|
||||||
type: dto.type,
|
type,
|
||||||
status: dto.status,
|
status: dto.status,
|
||||||
area: this.decimal(dto.area),
|
area: this.decimal(dto.area),
|
||||||
plannedBudget: this.decimal(dto.plannedBudget),
|
plannedBudget: this.decimal(dto.plannedBudget),
|
||||||
@@ -608,13 +685,16 @@ export class RenovationService {
|
|||||||
dto: UpdateRoomDto,
|
dto: UpdateRoomDto,
|
||||||
) {
|
) {
|
||||||
await this.edit(projectId, userId);
|
await this.edit(projectId, userId);
|
||||||
await this.requireOwned(this.repo.floors, projectId, dto.floorId);
|
|
||||||
const before = await this.requireOwned(this.repo.rooms, projectId, id);
|
const before = await this.requireOwned(this.repo.rooms, projectId, id);
|
||||||
|
const floorId = dto.floorId ?? before.floorId;
|
||||||
|
await this.requireOwned(this.repo.floors, projectId, floorId);
|
||||||
|
const type = dto.type?.trim() || before.type;
|
||||||
|
await this.validateRoomType(projectId, type);
|
||||||
await this.versioned(this.repo.rooms, projectId, id, dto.version, {
|
await this.versioned(this.repo.rooms, projectId, id, dto.version, {
|
||||||
floorId: dto.floorId,
|
floorId,
|
||||||
name: dto.name.trim(),
|
name: dto.name?.trim() || 'Unbenannter Raum',
|
||||||
description: dto.description?.trim() || null,
|
description: dto.description?.trim() || null,
|
||||||
type: dto.type,
|
type,
|
||||||
status: dto.status,
|
status: dto.status,
|
||||||
area: this.decimal(dto.area),
|
area: this.decimal(dto.area),
|
||||||
plannedBudget: this.decimal(dto.plannedBudget),
|
plannedBudget: this.decimal(dto.plannedBudget),
|
||||||
@@ -1611,6 +1691,42 @@ export class RenovationService {
|
|||||||
private read(projectId: string, userId: string) {
|
private read(projectId: string, userId: string) {
|
||||||
return this.access.require(projectId, userId, 'read');
|
return this.access.require(projectId, userId, 'read');
|
||||||
}
|
}
|
||||||
|
private async resolveRoomFloor(
|
||||||
|
projectId: string,
|
||||||
|
userId: string,
|
||||||
|
requestedFloorId?: string,
|
||||||
|
): Promise<string> {
|
||||||
|
if (requestedFloorId) {
|
||||||
|
await this.requireOwned(this.repo.floors, projectId, requestedFloorId);
|
||||||
|
return requestedFloorId;
|
||||||
|
}
|
||||||
|
const existing = await this.repo.floors.findOne({
|
||||||
|
where: { projectId },
|
||||||
|
order: { sortOrder: 'ASC' },
|
||||||
|
});
|
||||||
|
if (existing) return existing.id;
|
||||||
|
const defaults = await this.ensureDefaultFloors(projectId, userId);
|
||||||
|
const first = defaults.floors[0];
|
||||||
|
if (!first)
|
||||||
|
throw new ApiError(
|
||||||
|
ErrorCode.ValidationFailed,
|
||||||
|
'Es konnte keine Standardetage angelegt werden.',
|
||||||
|
400,
|
||||||
|
);
|
||||||
|
return first.id;
|
||||||
|
}
|
||||||
|
private async validateRoomType(
|
||||||
|
projectId: string,
|
||||||
|
key: string,
|
||||||
|
): Promise<void> {
|
||||||
|
if (defaultRoomTypes.some((type) => type.key === key)) return;
|
||||||
|
if (await this.repo.roomTypes.exist({ where: { projectId, key } })) return;
|
||||||
|
throw new ApiError(
|
||||||
|
ErrorCode.ValidationFailed,
|
||||||
|
'Der ausgewählte Raumtyp existiert nicht.',
|
||||||
|
400,
|
||||||
|
);
|
||||||
|
}
|
||||||
private edit(projectId: string, userId: string) {
|
private edit(projectId: string, userId: string) {
|
||||||
return this.access.require(projectId, userId, 'edit');
|
return this.access.require(projectId, userId, 'edit');
|
||||||
}
|
}
|
||||||
|
|||||||
21
apps/backend/src/renovation/room-types.spec.ts
Normal file
21
apps/backend/src/renovation/room-types.spec.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { customRoomTypeKey, defaultRoomTypes } from './room-types';
|
||||||
|
|
||||||
|
describe('room types', () => {
|
||||||
|
it('provides stable defaults including the fallback type', () => {
|
||||||
|
expect(defaultRoomTypes.map((type) => type.key)).toContain('other');
|
||||||
|
expect(new Set(defaultRoomTypes.map((type) => type.key)).size).toBe(
|
||||||
|
defaultRoomTypes.length,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates bounded technical keys from user-defined labels', () => {
|
||||||
|
expect(customRoomTypeKey('Gäste- & Arbeitszimmer', '1234567890')).toBe(
|
||||||
|
'custom_gaste_arbeitszimmer_12345678',
|
||||||
|
);
|
||||||
|
expect(customRoomTypeKey('🎉', 'abcdef12')).toBe('custom_raum_abcdef12');
|
||||||
|
expect(
|
||||||
|
customRoomTypeKey('x'.repeat(100), 'abcdef12').length,
|
||||||
|
).toBeLessThanOrEqual(40);
|
||||||
|
});
|
||||||
|
});
|
||||||
44
apps/backend/src/renovation/room-types.ts
Normal file
44
apps/backend/src/renovation/room-types.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
export interface RoomTypeDefinition {
|
||||||
|
id: string | null;
|
||||||
|
key: string;
|
||||||
|
name: string;
|
||||||
|
custom: boolean;
|
||||||
|
sortOrder: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultRoomTypes: readonly RoomTypeDefinition[] = [
|
||||||
|
{
|
||||||
|
id: null,
|
||||||
|
key: 'living_room',
|
||||||
|
name: 'Wohnzimmer',
|
||||||
|
custom: false,
|
||||||
|
sortOrder: 10,
|
||||||
|
},
|
||||||
|
{ id: null, key: 'kitchen', name: 'Küche', custom: false, sortOrder: 20 },
|
||||||
|
{
|
||||||
|
id: null,
|
||||||
|
key: 'bathroom',
|
||||||
|
name: 'Badezimmer',
|
||||||
|
custom: false,
|
||||||
|
sortOrder: 30,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: null,
|
||||||
|
key: 'bedroom',
|
||||||
|
name: 'Schlafzimmer',
|
||||||
|
custom: false,
|
||||||
|
sortOrder: 40,
|
||||||
|
},
|
||||||
|
{ id: null, key: 'other', name: 'Sonstiges', custom: false, sortOrder: 1000 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function customRoomTypeKey(name: string, suffix: string): string {
|
||||||
|
const slug = name
|
||||||
|
.normalize('NFKD')
|
||||||
|
.replace(/[\u0300-\u036f]/g, '')
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, '_')
|
||||||
|
.replace(/^_+|_+$/g, '')
|
||||||
|
.slice(0, 24);
|
||||||
|
return `custom_${slug || 'raum'}_${suffix.slice(0, 8)}`;
|
||||||
|
}
|
||||||
@@ -15,6 +15,30 @@ import { FurniturePlanningComponent } from './furniture-planning.component';
|
|||||||
registerLocaleData(localeDe);
|
registerLocaleData(localeDe);
|
||||||
|
|
||||||
describe('FurniturePlanningComponent', () => {
|
describe('FurniturePlanningComponent', () => {
|
||||||
|
it('allows optional furniture and delivery fields to stay empty', async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [FurniturePlanningComponent],
|
||||||
|
providers: [provideHttpClient()],
|
||||||
|
}).compileComponents();
|
||||||
|
const component = TestBed.createComponent(FurniturePlanningComponent).componentInstance;
|
||||||
|
|
||||||
|
component.requirementForm.reset({
|
||||||
|
name: '',
|
||||||
|
roomId: '',
|
||||||
|
description: '',
|
||||||
|
category: 'other',
|
||||||
|
priority: 'normal',
|
||||||
|
requiredQuantity: 1,
|
||||||
|
maximumBudget: null,
|
||||||
|
status: 'identified',
|
||||||
|
sortOrder: 0,
|
||||||
|
});
|
||||||
|
component.optionForm.patchValue({ name: '', expectedDeliveryDate: '' });
|
||||||
|
|
||||||
|
expect(component.requirementForm.valid).toBe(true);
|
||||||
|
expect(component.optionForm.valid).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('explains the planning concepts and recommends the next useful step', async () => {
|
it('explains the planning concepts and recommends the next useful step', async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [FurniturePlanningComponent],
|
imports: [FurniturePlanningComponent],
|
||||||
|
|||||||
@@ -224,20 +224,22 @@ const statusLabels: Record<string, string> = {
|
|||||||
<h3 id="requirement-form-title">
|
<h3 id="requirement-form-title">
|
||||||
{{ editingRequirement() ? 'Möbelbedarf bearbeiten' : 'Möbelbedarf hinzufügen' }}
|
{{ editingRequirement() ? 'Möbelbedarf bearbeiten' : 'Möbelbedarf hinzufügen' }}
|
||||||
</h3>
|
</h3>
|
||||||
|
<p class="ui-meta wide">Alle Angaben sind optional und können später ergänzt werden.</p>
|
||||||
|
<label>Name<input formControlName="name" placeholder="Optional" /></label>
|
||||||
<label
|
<label
|
||||||
>Name *<input formControlName="name" />
|
>Raum<select formControlName="roomId">
|
||||||
@if (requirementForm.controls.name.touched && requirementForm.controls.name.invalid) {
|
<option value="">Keinem Raum zuordnen</option>
|
||||||
<small class="error">Bitte einen Namen eingeben.</small>
|
|
||||||
}
|
|
||||||
</label>
|
|
||||||
<label
|
|
||||||
>Raum *<select formControlName="roomId">
|
|
||||||
<option value="">Bitte wählen</option>
|
|
||||||
@for (room of rooms; track room.id) {
|
@for (room of rooms; track room.id) {
|
||||||
<option [value]="room.id">{{ room.name }}</option>
|
<option [value]="room.id">{{ room.name }}</option>
|
||||||
}
|
}
|
||||||
</select></label
|
</select></label
|
||||||
>
|
>
|
||||||
|
@if (
|
||||||
|
optionTargetForm.controls.requirementId.touched &&
|
||||||
|
optionTargetForm.controls.requirementId.invalid
|
||||||
|
) {
|
||||||
|
<small class="error" role="alert">Bitte einen Bedarf auswählen.</small>
|
||||||
|
}
|
||||||
<label
|
<label
|
||||||
>Kategorie<select formControlName="category">
|
>Kategorie<select formControlName="category">
|
||||||
@for (entry of categories; track entry[0]) {
|
@for (entry of categories; track entry[0]) {
|
||||||
@@ -506,19 +508,20 @@ const statusLabels: Record<string, string> = {
|
|||||||
<h3 id="option-form-title">
|
<h3 id="option-form-title">
|
||||||
{{ editingOption() ? 'Möbelvorschlag bearbeiten' : 'Möbelvorschlag hinzufügen' }}
|
{{ editingOption() ? 'Möbelvorschlag bearbeiten' : 'Möbelvorschlag hinzufügen' }}
|
||||||
</h3>
|
</h3>
|
||||||
<label>Name *<input formControlName="name" /></label
|
<p class="ui-meta wide">Alle Angaben einschließlich Liefertermin sind optional.</p>
|
||||||
|
<label>Name<input formControlName="name" placeholder="Optional" /></label
|
||||||
><label>Hersteller<input formControlName="manufacturer" /></label
|
><label>Hersteller<input formControlName="manufacturer" /></label
|
||||||
><label>Modell<input formControlName="model" /></label
|
><label>Modell<input formControlName="model" /></label
|
||||||
><label>Händler<input formControlName="retailer" /></label>
|
><label>Händler<input formControlName="retailer" /></label>
|
||||||
<label>Produkt-URL<input type="url" formControlName="productUrl" /></label
|
<label>Produkt-URL<input type="url" formControlName="productUrl" /></label
|
||||||
><label>Artikelnummer<input formControlName="articleNumber" /></label>
|
><label>Artikelnummer<input formControlName="articleNumber" /></label>
|
||||||
<label
|
<label
|
||||||
>Einzelpreis *<input
|
>Einzelpreis<input
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
formControlName="unitPrice" /></label
|
formControlName="unitPrice" /></label
|
||||||
><label>Menge *<input type="number" min="1" formControlName="quantity" /></label
|
><label>Menge<input type="number" min="1" formControlName="quantity" /></label
|
||||||
><label
|
><label
|
||||||
>Versand<input
|
>Versand<input
|
||||||
type="number"
|
type="number"
|
||||||
@@ -548,7 +551,9 @@ const statusLabels: Record<string, string> = {
|
|||||||
><label
|
><label
|
||||||
>Lieferzeit (Tage)<input type="number" min="0" formControlName="deliveryDays" /></label
|
>Lieferzeit (Tage)<input type="number" min="0" formControlName="deliveryDays" /></label
|
||||||
><label
|
><label
|
||||||
>Erwartete Lieferung<input type="date" formControlName="expectedDeliveryDate"
|
>Erwartete Lieferung (optional)<input
|
||||||
|
type="date"
|
||||||
|
formControlName="expectedDeliveryDate"
|
||||||
/></label>
|
/></label>
|
||||||
<label
|
<label
|
||||||
>Verfügbarkeit<select formControlName="availability">
|
>Verfügbarkeit<select formControlName="availability">
|
||||||
@@ -786,6 +791,12 @@ const statusLabels: Record<string, string> = {
|
|||||||
}
|
}
|
||||||
</select></label
|
</select></label
|
||||||
>
|
>
|
||||||
|
@if (
|
||||||
|
assignmentForm.controls.scenarioId.touched &&
|
||||||
|
assignmentForm.controls.scenarioId.invalid
|
||||||
|
) {
|
||||||
|
<small class="error" role="alert">Bitte ein Szenario auswählen.</small>
|
||||||
|
}
|
||||||
<label
|
<label
|
||||||
>Bedarf *<select
|
>Bedarf *<select
|
||||||
formControlName="requirementId"
|
formControlName="requirementId"
|
||||||
@@ -799,6 +810,12 @@ const statusLabels: Record<string, string> = {
|
|||||||
}
|
}
|
||||||
</select></label
|
</select></label
|
||||||
>
|
>
|
||||||
|
@if (
|
||||||
|
assignmentForm.controls.requirementId.touched &&
|
||||||
|
assignmentForm.controls.requirementId.invalid
|
||||||
|
) {
|
||||||
|
<small class="error" role="alert">Bitte einen Bedarf auswählen.</small>
|
||||||
|
}
|
||||||
<fieldset class="option-selection">
|
<fieldset class="option-selection">
|
||||||
<legend>Möbelvorschlag auswählen *</legend>
|
<legend>Möbelvorschlag auswählen *</legend>
|
||||||
<div class="option-choice-grid">
|
<div class="option-choice-grid">
|
||||||
@@ -841,6 +858,11 @@ const statusLabels: Record<string, string> = {
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
@if (
|
||||||
|
assignmentForm.controls.optionId.touched && assignmentForm.controls.optionId.invalid
|
||||||
|
) {
|
||||||
|
<small class="error" role="alert">Bitte einen Möbelvorschlag auswählen.</small>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
@if (!scenarios().length) {
|
@if (!scenarios().length) {
|
||||||
@@ -1140,14 +1162,8 @@ export class FurniturePlanningComponent implements OnChanges {
|
|||||||
sortBy: new FormControl('sortOrder', { nonNullable: true }),
|
sortBy: new FormControl('sortOrder', { nonNullable: true }),
|
||||||
});
|
});
|
||||||
readonly requirementForm = new FormGroup({
|
readonly requirementForm = new FormGroup({
|
||||||
name: new FormControl('', {
|
name: new FormControl('', { nonNullable: true }),
|
||||||
nonNullable: true,
|
roomId: new FormControl('', { nonNullable: true }),
|
||||||
validators: [(control) => Validators.required(control)],
|
|
||||||
}),
|
|
||||||
roomId: new FormControl('', {
|
|
||||||
nonNullable: true,
|
|
||||||
validators: [(control) => Validators.required(control)],
|
|
||||||
}),
|
|
||||||
description: new FormControl('', { nonNullable: true }),
|
description: new FormControl('', { nonNullable: true }),
|
||||||
category: new FormControl('other', { nonNullable: true }),
|
category: new FormControl('other', { nonNullable: true }),
|
||||||
priority: new FormControl('normal', { nonNullable: true }),
|
priority: new FormControl('normal', { nonNullable: true }),
|
||||||
@@ -1162,10 +1178,7 @@ export class FurniturePlanningComponent implements OnChanges {
|
|||||||
sortOrder: new FormControl(0, { nonNullable: true }),
|
sortOrder: new FormControl(0, { nonNullable: true }),
|
||||||
});
|
});
|
||||||
readonly optionForm = new FormGroup({
|
readonly optionForm = new FormGroup({
|
||||||
name: new FormControl('', {
|
name: new FormControl('', { nonNullable: true }),
|
||||||
nonNullable: true,
|
|
||||||
validators: [(control) => Validators.required(control)],
|
|
||||||
}),
|
|
||||||
manufacturer: new FormControl('', { nonNullable: true }),
|
manufacturer: new FormControl('', { nonNullable: true }),
|
||||||
model: new FormControl('', { nonNullable: true }),
|
model: new FormControl('', { nonNullable: true }),
|
||||||
description: new FormControl('', { nonNullable: true }),
|
description: new FormControl('', { nonNullable: true }),
|
||||||
@@ -1276,7 +1289,8 @@ export class FurniturePlanningComponent implements OnChanges {
|
|||||||
this.filterForm.reset({ search: '', roomId: '', status: '', sortBy: 'sortOrder' });
|
this.filterForm.reset({ search: '', roomId: '', status: '', sortBy: 'sortOrder' });
|
||||||
this.load(1);
|
this.load(1);
|
||||||
}
|
}
|
||||||
roomName(id: string) {
|
roomName(id: string | null) {
|
||||||
|
if (!id) return 'Keinem Raum zugeordnet';
|
||||||
return this.rooms.find((room) => room.id === id)?.name ?? 'Unbekannter Raum';
|
return this.rooms.find((room) => room.id === id)?.name ?? 'Unbekannter Raum';
|
||||||
}
|
}
|
||||||
category(value: string) {
|
category(value: string) {
|
||||||
@@ -1293,7 +1307,7 @@ export class FurniturePlanningComponent implements OnChanges {
|
|||||||
this.editingRequirement.set(null);
|
this.editingRequirement.set(null);
|
||||||
this.requirementForm.reset({
|
this.requirementForm.reset({
|
||||||
name: '',
|
name: '',
|
||||||
roomId: this.filterForm.controls.roomId.value || this.rooms[0]?.id || '',
|
roomId: this.filterForm.controls.roomId.value,
|
||||||
description: '',
|
description: '',
|
||||||
category: 'other',
|
category: 'other',
|
||||||
priority: 'normal',
|
priority: 'normal',
|
||||||
@@ -1309,7 +1323,7 @@ export class FurniturePlanningComponent implements OnChanges {
|
|||||||
this.editingRequirement.set(r);
|
this.editingRequirement.set(r);
|
||||||
this.requirementForm.reset({
|
this.requirementForm.reset({
|
||||||
name: r.name,
|
name: r.name,
|
||||||
roomId: r.roomId,
|
roomId: r.roomId ?? '',
|
||||||
description: r.description ?? '',
|
description: r.description ?? '',
|
||||||
category: r.category,
|
category: r.category,
|
||||||
priority: r.priority,
|
priority: r.priority,
|
||||||
@@ -1331,13 +1345,11 @@ export class FurniturePlanningComponent implements OnChanges {
|
|||||||
if (this.requirementForm.invalid) return this.requirementForm.markAllAsTouched();
|
if (this.requirementForm.invalid) return this.requirementForm.markAllAsTouched();
|
||||||
this.saving.set(true);
|
this.saving.set(true);
|
||||||
const existing = this.editingRequirement();
|
const existing = this.editingRequirement();
|
||||||
|
const value = this.requirementForm.getRawValue();
|
||||||
|
const body = { ...value, roomId: value.roomId || undefined };
|
||||||
const request = existing
|
const request = existing
|
||||||
? this.api.updateFurnitureRequirement(
|
? this.api.updateFurnitureRequirement(this.projectId, existing, body)
|
||||||
this.projectId,
|
: this.api.createFurnitureRequirement(this.projectId, body);
|
||||||
existing,
|
|
||||||
this.requirementForm.getRawValue(),
|
|
||||||
)
|
|
||||||
: this.api.createFurnitureRequirement(this.projectId, this.requirementForm.getRawValue());
|
|
||||||
request.subscribe({
|
request.subscribe({
|
||||||
next: (saved) => {
|
next: (saved) => {
|
||||||
this.saving.set(false);
|
this.saving.set(false);
|
||||||
@@ -1457,7 +1469,13 @@ export class FurniturePlanningComponent implements OnChanges {
|
|||||||
const requirement = this.optionRequirement();
|
const requirement = this.optionRequirement();
|
||||||
if (!requirement || this.optionForm.invalid) return this.optionForm.markAllAsTouched();
|
if (!requirement || this.optionForm.invalid) return this.optionForm.markAllAsTouched();
|
||||||
this.saving.set(true);
|
this.saving.set(true);
|
||||||
const body = { ...this.optionForm.getRawValue(), currency: 'EUR' };
|
const value = this.optionForm.getRawValue();
|
||||||
|
const body = {
|
||||||
|
...value,
|
||||||
|
name: value.name.trim() || undefined,
|
||||||
|
expectedDeliveryDate: value.expectedDeliveryDate || undefined,
|
||||||
|
currency: 'EUR',
|
||||||
|
};
|
||||||
const existing = this.editingOption();
|
const existing = this.editingOption();
|
||||||
const request = existing
|
const request = existing
|
||||||
? this.api.updateFurnitureOption(this.projectId, existing, body)
|
? this.api.updateFurnitureOption(this.projectId, existing, body)
|
||||||
|
|||||||
@@ -40,6 +40,13 @@ export interface Room extends Versioned {
|
|||||||
description: string | null;
|
description: string | null;
|
||||||
previewDocumentId: string | null;
|
previewDocumentId: string | null;
|
||||||
}
|
}
|
||||||
|
export interface RoomType {
|
||||||
|
id: string | null;
|
||||||
|
key: string;
|
||||||
|
name: string;
|
||||||
|
custom: boolean;
|
||||||
|
sortOrder: number;
|
||||||
|
}
|
||||||
export interface RenovationTask extends Versioned {
|
export interface RenovationTask extends Versioned {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
roomId: string | null;
|
roomId: string | null;
|
||||||
@@ -231,7 +238,7 @@ export interface FurnitureOption extends Versioned {
|
|||||||
}
|
}
|
||||||
export interface FurnitureRequirement extends Versioned {
|
export interface FurnitureRequirement extends Versioned {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
roomId: string;
|
roomId: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
category: string;
|
category: string;
|
||||||
@@ -318,6 +325,15 @@ export class HauspilotApiService {
|
|||||||
params: this.params(query),
|
params: this.params(query),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
roomTypes(id: string) {
|
||||||
|
return this.http.get<RoomType[]>(`${this.base}/${id}/room-types`);
|
||||||
|
}
|
||||||
|
createRoomType(id: string, name: string) {
|
||||||
|
return this.http.post<RoomType>(`${this.base}/${id}/room-types`, { name });
|
||||||
|
}
|
||||||
|
deleteRoomType(id: string, roomTypeId: string) {
|
||||||
|
return this.http.delete<void>(`${this.base}/${id}/room-types/${roomTypeId}`);
|
||||||
|
}
|
||||||
tasks(id: string, query: ListQuery = {}) {
|
tasks(id: string, query: ListQuery = {}) {
|
||||||
return this.http.get<PageResult<RenovationTask>>(`${this.base}/${id}/tasks`, {
|
return this.http.get<PageResult<RenovationTask>>(`${this.base}/${id}/tasks`, {
|
||||||
params: this.params(query),
|
params: this.params(query),
|
||||||
@@ -385,7 +401,15 @@ export class HauspilotApiService {
|
|||||||
version: floor.version,
|
version: floor.version,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
createRoom(id: string, body: { floorId: string; name: string; type: string; status: string }) {
|
createRoom(
|
||||||
|
id: string,
|
||||||
|
body: {
|
||||||
|
floorId?: string | undefined;
|
||||||
|
name?: string | undefined;
|
||||||
|
type?: string | undefined;
|
||||||
|
status?: string | undefined;
|
||||||
|
},
|
||||||
|
) {
|
||||||
return this.http.post<Room>(`${this.base}/${id}/rooms`, body);
|
return this.http.post<Room>(`${this.base}/${id}/rooms`, body);
|
||||||
}
|
}
|
||||||
updateRoom(id: string, room: Room, body: object) {
|
updateRoom(id: string, room: Room, body: object) {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import type {
|
|||||||
ProjectDocument,
|
ProjectDocument,
|
||||||
RenovationTask,
|
RenovationTask,
|
||||||
Room,
|
Room,
|
||||||
|
RoomType,
|
||||||
} from './hauspilot-api.service';
|
} from './hauspilot-api.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -151,6 +152,9 @@ import type {
|
|||||||
[formGroup]="roomForm"
|
[formGroup]="roomForm"
|
||||||
(ngSubmit)="createRoom()"
|
(ngSubmit)="createRoom()"
|
||||||
>
|
>
|
||||||
|
<p class="ui-meta form-wide">
|
||||||
|
Alle Angaben sind optional. Ohne Etage wird die erste vorhandene Etage verwendet.
|
||||||
|
</p>
|
||||||
<label class="ui-form-field"
|
<label class="ui-form-field"
|
||||||
><span class="ui-label">Etage</span
|
><span class="ui-label">Etage</span
|
||||||
><select class="ui-control" formControlName="floorId">
|
><select class="ui-control" formControlName="floorId">
|
||||||
@@ -167,13 +171,53 @@ import type {
|
|||||||
><label class="ui-form-field"
|
><label class="ui-form-field"
|
||||||
><span class="ui-label">Raumtyp</span
|
><span class="ui-label">Raumtyp</span
|
||||||
><select class="ui-control" formControlName="type">
|
><select class="ui-control" formControlName="type">
|
||||||
<option value="living_room">Wohnzimmer</option>
|
@for (roomType of roomTypes(); track roomType.key) {
|
||||||
<option value="kitchen">Küche</option>
|
<option [value]="roomType.key">{{ roomType.name }}</option>
|
||||||
<option value="bathroom">Badezimmer</option>
|
}
|
||||||
<option value="bedroom">Schlafzimmer</option>
|
|
||||||
<option value="other">Sonstiges</option>
|
|
||||||
</select></label
|
</select></label
|
||||||
><label class="ui-form-field"
|
>
|
||||||
|
<div class="ui-form-field">
|
||||||
|
<span class="ui-label">Eigener Raumtyp</span>
|
||||||
|
@if (showRoomTypeForm()) {
|
||||||
|
<input
|
||||||
|
class="ui-control"
|
||||||
|
[formControl]="roomTypeName"
|
||||||
|
aria-describedby="room-type-error"
|
||||||
|
placeholder="z. B. Hobbyraum"
|
||||||
|
/>
|
||||||
|
@if (roomTypeName.touched && roomTypeName.invalid) {
|
||||||
|
<small id="room-type-error" class="error" role="alert"
|
||||||
|
>Bitte eine Bezeichnung für den Raumtyp eingeben.</small
|
||||||
|
>
|
||||||
|
}
|
||||||
|
<div class="actions">
|
||||||
|
<button
|
||||||
|
class="ui-button ui-button--secondary"
|
||||||
|
type="button"
|
||||||
|
[disabled]="saving()"
|
||||||
|
(click)="createRoomType()"
|
||||||
|
>
|
||||||
|
Raumtyp speichern
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="ui-button ui-button--ghost"
|
||||||
|
type="button"
|
||||||
|
(click)="cancelRoomType()"
|
||||||
|
>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<button
|
||||||
|
class="ui-button ui-button--secondary"
|
||||||
|
type="button"
|
||||||
|
(click)="showRoomTypeForm.set(true)"
|
||||||
|
>
|
||||||
|
Neuen Raumtyp definieren
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<label class="ui-form-field"
|
||||||
><span class="ui-label">Status</span
|
><span class="ui-label">Status</span
|
||||||
><select class="ui-control" formControlName="status">
|
><select class="ui-control" formControlName="status">
|
||||||
<option value="unplanned">Nicht geplant</option>
|
<option value="unplanned">Nicht geplant</option>
|
||||||
@@ -739,6 +783,7 @@ export class ProjectWorkspacePageComponent implements OnInit {
|
|||||||
readonly buildings = signal<Building[]>([]);
|
readonly buildings = signal<Building[]>([]);
|
||||||
readonly floors = signal<Floor[]>([]);
|
readonly floors = signal<Floor[]>([]);
|
||||||
readonly rooms = signal<Room[]>([]);
|
readonly rooms = signal<Room[]>([]);
|
||||||
|
readonly roomTypes = signal<RoomType[]>([]);
|
||||||
readonly tasks = signal<RenovationTask[]>([]);
|
readonly tasks = signal<RenovationTask[]>([]);
|
||||||
readonly milestones = signal<Milestone[]>([]);
|
readonly milestones = signal<Milestone[]>([]);
|
||||||
readonly budgets = signal<BudgetCategory[]>([]);
|
readonly budgets = signal<BudgetCategory[]>([]);
|
||||||
@@ -751,6 +796,14 @@ export class ProjectWorkspacePageComponent implements OnInit {
|
|||||||
readonly error = signal<string | null>(null);
|
readonly error = signal<string | null>(null);
|
||||||
readonly showRoomForm = signal(false);
|
readonly showRoomForm = signal(false);
|
||||||
readonly editingRoom = signal<Room | null>(null);
|
readonly editingRoom = signal<Room | null>(null);
|
||||||
|
readonly showRoomTypeForm = signal(false);
|
||||||
|
readonly roomTypeName = new FormControl('', {
|
||||||
|
nonNullable: true,
|
||||||
|
validators: [
|
||||||
|
(control) => Validators.required(control),
|
||||||
|
(control) => Validators.pattern(/\S/)(control),
|
||||||
|
],
|
||||||
|
});
|
||||||
readonly showTaskForm = signal(false);
|
readonly showTaskForm = signal(false);
|
||||||
readonly statusFilter = new FormControl('', { nonNullable: true });
|
readonly statusFilter = new FormControl('', { nonNullable: true });
|
||||||
readonly mineFilter = new FormControl(false, { nonNullable: true });
|
readonly mineFilter = new FormControl(false, { nonNullable: true });
|
||||||
@@ -759,14 +812,8 @@ export class ProjectWorkspacePageComponent implements OnInit {
|
|||||||
readonly taskPage = signal(1);
|
readonly taskPage = signal(1);
|
||||||
readonly taskTotal = signal(0);
|
readonly taskTotal = signal(0);
|
||||||
readonly roomForm = new FormGroup({
|
readonly roomForm = new FormGroup({
|
||||||
floorId: new FormControl('', {
|
floorId: new FormControl('', { nonNullable: true }),
|
||||||
nonNullable: true,
|
name: new FormControl('', { nonNullable: true }),
|
||||||
validators: [(control) => Validators.required(control)],
|
|
||||||
}),
|
|
||||||
name: new FormControl('', {
|
|
||||||
nonNullable: true,
|
|
||||||
validators: [(control) => Validators.required(control)],
|
|
||||||
}),
|
|
||||||
type: new FormControl('other', { nonNullable: true }),
|
type: new FormControl('other', { nonNullable: true }),
|
||||||
status: new FormControl('unplanned', { nonNullable: true }),
|
status: new FormControl('unplanned', { nonNullable: true }),
|
||||||
description: new FormControl('', { nonNullable: true }),
|
description: new FormControl('', { nonNullable: true }),
|
||||||
@@ -962,9 +1009,16 @@ export class ProjectWorkspacePageComponent implements OnInit {
|
|||||||
createRoom() {
|
createRoom() {
|
||||||
if (this.roomForm.invalid) return;
|
if (this.roomForm.invalid) return;
|
||||||
const editing = this.editingRoom();
|
const editing = this.editingRoom();
|
||||||
|
const value = this.roomForm.getRawValue();
|
||||||
|
const body = {
|
||||||
|
...value,
|
||||||
|
floorId: value.floorId || undefined,
|
||||||
|
name: value.name.trim() || undefined,
|
||||||
|
type: value.type || undefined,
|
||||||
|
};
|
||||||
const request = editing
|
const request = editing
|
||||||
? this.api.updateRoom(this.projectId, editing, this.roomForm.getRawValue())
|
? this.api.updateRoom(this.projectId, editing, body)
|
||||||
: this.api.createRoom(this.projectId, this.roomForm.getRawValue());
|
: this.api.createRoom(this.projectId, body);
|
||||||
this.save(request, (room) => {
|
this.save(request, (room) => {
|
||||||
this.rooms.update((items) =>
|
this.rooms.update((items) =>
|
||||||
editing ? items.map((item) => (item.id === room.id ? room : item)) : [...items, room],
|
editing ? items.map((item) => (item.id === room.id ? room : item)) : [...items, room],
|
||||||
@@ -983,6 +1037,23 @@ export class ProjectWorkspacePageComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
createRoomType() {
|
||||||
|
if (this.roomTypeName.invalid) {
|
||||||
|
this.roomTypeName.markAsTouched();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.save(this.api.createRoomType(this.projectId, this.roomTypeName.value.trim()), (type) => {
|
||||||
|
this.roomTypes.update((types) =>
|
||||||
|
[...types, type].sort((a, b) => a.name.localeCompare(b.name)),
|
||||||
|
);
|
||||||
|
this.roomForm.controls.type.setValue(type.key);
|
||||||
|
this.cancelRoomType();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
cancelRoomType() {
|
||||||
|
this.showRoomTypeForm.set(false);
|
||||||
|
this.roomTypeName.reset('');
|
||||||
|
}
|
||||||
editRoom(room: Room) {
|
editRoom(room: Room) {
|
||||||
this.editingRoom.set(room);
|
this.editingRoom.set(room);
|
||||||
this.showRoomForm.set(true);
|
this.showRoomForm.set(true);
|
||||||
@@ -1203,6 +1274,10 @@ export class ProjectWorkspacePageComponent implements OnInit {
|
|||||||
this.api
|
this.api
|
||||||
.rooms(this.projectId)
|
.rooms(this.projectId)
|
||||||
.subscribe({ next: (v) => this.rooms.set(v.items), error: (e: unknown) => this.fail(e) }),
|
.subscribe({ next: (v) => this.rooms.set(v.items), error: (e: unknown) => this.fail(e) }),
|
||||||
|
this.api.roomTypes(this.projectId).subscribe({
|
||||||
|
next: (types) => this.roomTypes.set(types),
|
||||||
|
error: (e: unknown) => this.fail(e),
|
||||||
|
}),
|
||||||
this.api.tasks(this.projectId).subscribe({
|
this.api.tasks(this.projectId).subscribe({
|
||||||
next: (v) => {
|
next: (v) => {
|
||||||
this.tasks.set(v.items);
|
this.tasks.set(v.items);
|
||||||
|
|||||||
Reference in New Issue
Block a user