feat: add HelpText model, service, and controller for dynamic help text management
Test Suite / backend-pr-quick (push) Has been skipped
Test Suite / quick-import-pr-quick (push) Has been skipped
Test Suite / backend-full (push) Successful in 2m27s
Test Suite / flutter-quality (push) Successful in 1m47s

This commit is contained in:
Nils-Johan Gynther
2026-05-13 16:20:04 +02:00
parent 0da4bbf4cf
commit 3d9b124766
11 changed files with 349 additions and 3 deletions
@@ -0,0 +1,14 @@
import { IsBoolean, IsOptional, IsString, MaxLength } from 'class-validator';
export class UpsertHelpTextDto {
@IsString()
@MaxLength(120)
title!: string;
@IsString()
content!: string;
@IsOptional()
@IsBoolean()
isActive?: boolean;
}
@@ -0,0 +1,28 @@
import { Body, Controller, Get, Param, Put } from '@nestjs/common';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { Roles } from '../auth/decorators/roles.decorator';
import { UpsertHelpTextDto } from './dto/upsert-help-text.dto';
import { HelpTextsService } from './help-texts.service';
@Controller('help-texts')
export class HelpTextsController {
constructor(private readonly helpTextsService: HelpTextsService) {}
@Get(':key')
getByKey(
@Param('key') key: string,
@CurrentUser() user: { role?: string },
) {
return this.helpTextsService.getResolvedByKey(key, user?.role);
}
@Roles('admin')
@Put(':key/:scope')
upsert(
@Param('key') key: string,
@Param('scope') scope: string,
@Body() dto: UpsertHelpTextDto,
) {
return this.helpTextsService.upsert(key, scope, dto);
}
}
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { PrismaModule } from '../prisma/prisma.module';
import { HelpTextsController } from './help-texts.controller';
import { HelpTextsService } from './help-texts.service';
@Module({
imports: [PrismaModule],
controllers: [HelpTextsController],
providers: [HelpTextsService],
})
export class HelpTextsModule {}
@@ -0,0 +1,99 @@
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { UpsertHelpTextDto } from './dto/upsert-help-text.dto';
type HelpTextScope = 'default' | 'user' | 'admin';
@Injectable()
export class HelpTextsService {
private readonly allowedScopes: HelpTextScope[] = ['default', 'user', 'admin'];
constructor(private readonly prisma: PrismaService) {}
async getResolvedByKey(keyRaw: string, roleRaw?: string) {
const key = this.normalizeKey(keyRaw);
const role = (roleRaw ?? 'user').toLowerCase();
const scopePriority: HelpTextScope[] = role === 'admin'
? ['admin', 'user', 'default']
: ['user', 'default'];
const rows = await this.prisma.helpText.findMany({
where: {
key,
isActive: true,
scope: { in: scopePriority },
},
select: {
key: true,
scope: true,
title: true,
content: true,
updatedAt: true,
},
});
for (const scope of scopePriority) {
const hit = rows.find((row) => row.scope === scope);
if (hit) {
return {
'key': hit.key,
'scope': hit.scope,
'title': hit.title,
'content': hit.content,
'updatedAt': hit.updatedAt,
};
}
}
throw new NotFoundException(`Ingen aktiv hjälptext hittades för key '${key}'.`);
}
async upsert(keyRaw: string, scopeRaw: string, dto: UpsertHelpTextDto) {
const key = this.normalizeKey(keyRaw);
const scope = this.normalizeScope(scopeRaw);
return this.prisma.helpText.upsert({
where: {
key_scope: { key, scope },
},
update: {
title: dto.title.trim(),
content: dto.content.trim(),
isActive: dto.isActive ?? true,
},
create: {
key,
scope,
title: dto.title.trim(),
content: dto.content.trim(),
isActive: dto.isActive ?? true,
},
select: {
key: true,
scope: true,
title: true,
content: true,
isActive: true,
updatedAt: true,
},
});
}
private normalizeKey(value: string): string {
const normalized = value.trim().toLowerCase();
if (!normalized) {
throw new BadRequestException('Hjälptext-nyckel måste anges.');
}
return normalized;
}
private normalizeScope(value: string): HelpTextScope {
const normalized = value.trim().toLowerCase() as HelpTextScope;
if (!this.allowedScopes.includes(normalized)) {
throw new BadRequestException(
`Ogiltig scope '${value}'. Tillåtna scopes: ${this.allowedScopes.join(', ')}`,
);
}
return normalized;
}
}