diff --git a/backend/src/controllers/dto.ts b/backend/src/controllers/dto.ts index b050f605..dad2d324 100644 --- a/backend/src/controllers/dto.ts +++ b/backend/src/controllers/dto.ts @@ -34,6 +34,7 @@ import { EmailTemplateSize, FrontendSettings, Settings, + ProjectSettings, } from "../entities/settings"; import { User } from "../entities/user"; import { UserRole } from "../entities/user-role"; @@ -91,6 +92,17 @@ export class ApplicationSettingsDTO implements DTO { @IsNumber() @Expose() public hoursToConfirm!: number; + @Type(() => Date) + @IsDate() + @Expose() + public acceptanceDeadline!: Date; + @Type(() => Date) + @IsDate() + @Expose() + public confirmSpotUntil!: Date; +} + +export class ProjectSettingsDTO implements DTO { @IsBoolean() @Expose() public allowRatingProjects!: boolean; @@ -166,6 +178,10 @@ export class SettingsDTO implements DTO> { @ValidateNested() @Expose() public email!: EmailSettingsDTO; + @Type(() => ProjectSettingsDTO) + @ValidateNested() + @Expose() + public project!: ProjectSettingsDTO; } export abstract class QuestionConfigurationDTOBase { diff --git a/backend/src/entities/application-settings.ts b/backend/src/entities/application-settings.ts index d37586d6..6dd0dc66 100644 --- a/backend/src/entities/application-settings.ts +++ b/backend/src/entities/application-settings.ts @@ -11,6 +11,9 @@ import { FormSettings } from "./form-settings"; // TODO all other settings are part of the settings table, whereas ApplicationSettings // is a separate table. Move into settings table just like EmailSettings. +/** + * Application as in "Peoples application for the event" + */ @Entity() export class ApplicationSettings { @PrimaryGeneratedColumn() @@ -29,6 +32,8 @@ export class ApplicationSettings { public allowProfileFormUntil!: Date; @Column() public hoursToConfirm!: number; - @Column({ default: false }) - public allowRatingProjects!: boolean; + @Column({ default: () => "CURRENT_TIMESTAMP" }) + public acceptanceDeadline!: Date; + @Column({ default: () => "CURRENT_TIMESTAMP" }) + public confirmSpotUntil!: Date; } diff --git a/backend/src/entities/settings.ts b/backend/src/entities/settings.ts index a7920725..e4b6ca05 100644 --- a/backend/src/entities/settings.ts +++ b/backend/src/entities/settings.ts @@ -48,6 +48,11 @@ export class EmailSettings { public forgotPasswordEmail!: EmailTemplate; } +export class ProjectSettings { + @Column({ default: false }) + public allowRatingProjects!: boolean; +} + @Entity() export class Settings { @PrimaryGeneratedColumn() @@ -64,6 +69,9 @@ export class Settings { @Type(() => EmailSettings) @Column(() => EmailSettings) public email!: EmailSettings; + @Type(() => ProjectSettings) + @Column(() => ProjectSettings) + public project!: ProjectSettings; } /** diff --git a/backend/src/services/project-service.ts b/backend/src/services/project-service.ts index 2d901904..f964bada 100644 --- a/backend/src/services/project-service.ts +++ b/backend/src/services/project-service.ts @@ -72,7 +72,7 @@ export class ProjectService implements IProjectService { .map((team) => team.id); const [settings] = await this._settings.find(); - const allowRatingProjects = settings.application.allowRatingProjects; + const allowRatingProjects = settings.project.allowRatingProjects; const isAdmin = user.role === UserRole.Root; const projects = await this._projects.find(); diff --git a/backend/src/services/rating-service.ts b/backend/src/services/rating-service.ts index 4cabd694..0ec8927a 100644 --- a/backend/src/services/rating-service.ts +++ b/backend/src/services/rating-service.ts @@ -236,10 +236,8 @@ export class RatingService implements IRatingService { } const settings = await this._settings.getSettings(); - if (!settings.application.allowRatingProjects) { - throw new ForbiddenError( - "Rating is not allowed due to application settings", - ); + if (!settings.project.allowRatingProjects) { + throw new ForbiddenError("Rating is not allowed due to settings"); } const project = await this._projects.findOneBy({ id: rating.project.id }); diff --git a/backend/src/services/settings-service.ts b/backend/src/services/settings-service.ts index ea91129b..8f1fe9d9 100644 --- a/backend/src/services/settings-service.ts +++ b/backend/src/services/settings-service.ts @@ -9,6 +9,7 @@ import { EmailTemplate, FrontendSettings, Settings, + ProjectSettings, } from "../entities/settings"; import { ConfigurationServiceToken, @@ -102,6 +103,7 @@ export class SettingsService implements ISettingsService { settings.application = this.getDefaultApplicationSettings(); settings.frontend = this.getDefaultFrontendSettings(); settings.email = this.getDefaultEmailSettings(); + settings.project = this.getDefaultProjectSettings(); return settings; } @@ -115,10 +117,20 @@ export class SettingsService implements ISettingsService { applicationSettings.allowProfileFormFrom = new Date(); applicationSettings.allowProfileFormUntil = new Date(); applicationSettings.hoursToConfirm = 24; - applicationSettings.allowRatingProjects = false; + applicationSettings.acceptanceDeadline = new Date(); + applicationSettings.confirmSpotUntil = new Date(); return applicationSettings; } + /** + * Creates a project settings object with default values. + */ + private getDefaultProjectSettings(): ProjectSettings { + const projectSettings = new ProjectSettings(); + projectSettings.allowRatingProjects = false; + return projectSettings; + } + /** * Creates a form settings object with default values. */ diff --git a/backend/test/services/mock/mock-settings-service.ts b/backend/test/services/mock/mock-settings-service.ts index 921fd0fe..d8dc7683 100644 --- a/backend/test/services/mock/mock-settings-service.ts +++ b/backend/test/services/mock/mock-settings-service.ts @@ -25,7 +25,11 @@ export const defaultSettings = { }, allowProfileFormFrom: new Date(), allowProfileFormUntil: new Date(), + acceptanceDeadline: new Date(), + confirmSpotUntil: new Date(), hoursToConfirm: 24, + }, + project: { allowRatingProjects: false, }, frontend: { diff --git a/backend/test/services/project-service-get-all-projects.spec.ts b/backend/test/services/project-service-get-all-projects.spec.ts index d3843a76..a1a57a87 100644 --- a/backend/test/services/project-service-get-all-projects.spec.ts +++ b/backend/test/services/project-service-get-all-projects.spec.ts @@ -56,8 +56,8 @@ describe(ProjectService.name, () => { await settingsRepo.save([ { ...defaultSettings, - application: { - ...defaultSettings.application, + project: { + ...defaultSettings.project, allowRatingProjects: false, }, }, @@ -71,7 +71,7 @@ describe(ProjectService.name, () => { const allowRatingProjects = async (value: boolean): Promise => { const settingsRepo = database.getRepository(Settings); const settings = { - application: { + project: { allowRatingProjects: value, }, }; diff --git a/backend/test/services/rating-service.spec.ts b/backend/test/services/rating-service.spec.ts index 544afe39..1c10411c 100644 --- a/backend/test/services/rating-service.spec.ts +++ b/backend/test/services/rating-service.spec.ts @@ -121,7 +121,7 @@ describe("RatingService", () => { expect.assertions(1); settingsService.mocks.getSettings.mockResolvedValue({ - application: { allowRatingProjects: false }, + project: { allowRatingProjects: false }, } as any); const rating = Object.assign(new Rating(), { @@ -140,7 +140,7 @@ describe("RatingService", () => { expect.assertions(1); settingsService.mocks.getSettings.mockResolvedValue({ - application: { allowRatingProjects: true }, + project: { allowRatingProjects: true }, } as any); const rating = Object.assign(new Rating(), { @@ -159,7 +159,7 @@ describe("RatingService", () => { expect.assertions(1); settingsService.mocks.getSettings.mockResolvedValue({ - application: { allowRatingProjects: true }, + project: { allowRatingProjects: true }, } as any); await projectRepo.update(mockProject.id, { allowRating: false }); @@ -181,7 +181,7 @@ describe("RatingService", () => { expect.assertions(1); settingsService.mocks.getSettings.mockResolvedValue({ - application: { allowRatingProjects: true }, + project: { allowRatingProjects: true }, } as any); const rating = Object.assign(new Rating(), { @@ -200,7 +200,7 @@ describe("RatingService", () => { expect.assertions(2); settingsService.mocks.getSettings.mockResolvedValue({ - application: { allowRatingProjects: true }, + project: { allowRatingProjects: true }, } as any); const rating = Object.assign(new Rating(), { @@ -220,7 +220,7 @@ describe("RatingService", () => { expect.assertions(1); settingsService.mocks.getSettings.mockResolvedValue({ - application: { allowRatingProjects: true }, + project: { allowRatingProjects: true }, } as any); const rating = Object.assign(new Rating(), { diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 09b2efcb..ea07a051 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -182,6 +182,12 @@ export class ApiClient { allowProfileFormUntil: this.reviveDate( settings.application.allowProfileFormUntil, ), + acceptanceDeadline: this.reviveDate( + settings.application.acceptanceDeadline, + ), + confirmSpotUntil: this.reviveDate( + settings.application.confirmSpotUntil, + ), }, }; } diff --git a/frontend/src/components/pages/status.tsx b/frontend/src/components/pages/status.tsx index 0a5b1fab..a8c30ae7 100644 --- a/frontend/src/components/pages/status.tsx +++ b/frontend/src/components/pages/status.tsx @@ -29,6 +29,18 @@ export const Status = () => { const { user, updateUser } = useLoginContext(); const confirmationDays = Math.floor(settings.application.hoursToConfirm / 24); + + const allowProfileFormFrom = dateToString( + settings.application.allowProfileFormFrom, + ); + const allowProfileFormUntil = dateToString( + settings.application.allowProfileFormUntil, + ); + const acceptanceDeadline = dateToString( + settings.application.acceptanceDeadline, + ); + const confirmSpotUntil = dateToString(settings.application.confirmSpotUntil); + const isExpired = user == null ? false : isConfirmationExpired(user); const isNotAttending = isExpired || user?.declined; const deadline = user?.confirmationExpiresAt; @@ -98,7 +110,10 @@ export const Status = () => { profile form - , any time between 01.03.2026 - 31.04.2026 + , any time between{" "} + + {allowProfileFormFrom} - {allowProfileFormUntil} + )} @@ -145,7 +160,7 @@ export const Status = () => { <> We will come back to you and send you a acceptance mail until{" "} - 01.05.2026. + {acceptanceDeadline}. )} @@ -173,7 +188,7 @@ export const Status = () => { <> If you got accepted, you need to confirm your spot until{" "} - 08.05.2026 + {confirmSpotUntil} {user?.admitted && ( <> {" "} diff --git a/frontend/src/components/settings/application/application-settings.tsx b/frontend/src/components/settings/application/application-settings.tsx index b4a9c748..7fb0d959 100644 --- a/frontend/src/components/settings/application/application-settings.tsx +++ b/frontend/src/components/settings/application/application-settings.tsx @@ -1,3 +1,4 @@ +import { Dispatch, SetStateAction } from "react"; import * as React from "react"; import { useCallback } from "react"; import type { ApplicationSettingsDTO } from "../../../api/types/dto"; @@ -40,30 +41,24 @@ export const ApplicationSettings = () => { [settings], ); - const handleAllowProfileFormFromChange = useCallback( - (value: string) => { - setAllowProfileFormFrom(value); - const date = new Date(value); - - if (!isValidDate(date)) { - return; - } + const [allowProfileFormUntil, setAllowProfileFormUntil] = useDerivedState( + () => settings.application.allowProfileFormUntil.toISOString(), + [settings], + ); - updateApplicationSettings({ - allowProfileFormFrom: date, - }); - }, - [updateApplicationSettings], + const [acceptanceDeadline, setAcceptanceDeadline] = useDerivedState( + () => settings.application.acceptanceDeadline.toISOString(), + [settings], ); - const [allowProfileFormUntil, setAllowProfileFormUntil] = useDerivedState( - () => settings.application.allowProfileFormUntil.toISOString(), + const [confirmSpotUntil, setConfirmSpotUntil] = useDerivedState( + () => settings.application.confirmSpotUntil.toISOString(), [settings], ); - const handleAllowProfileFormUntilChange = useCallback( - (value: string) => { - setAllowProfileFormUntil(value); + const handleDateChange = useCallback( + (value: string, setter: Dispatch>, key: string) => { + setter(value); const date = new Date(value); if (!isValidDate(date)) { @@ -71,7 +66,7 @@ export const ApplicationSettings = () => { } updateApplicationSettings({ - allowProfileFormUntil: date, + [key]: date, }); }, [updateApplicationSettings], @@ -112,11 +107,18 @@ export const ApplicationSettings = () => { placeholder="keep it fair, e.g. 240 for 10 days" /> - + + + handleDateChange( + value, + setAllowProfileFormFrom, + "allowProfileFormFrom", + ) + } title="Open registration on" placeholder="1970-01-01 00:00:00" /> @@ -125,12 +127,45 @@ export const ApplicationSettings = () => { + handleDateChange( + value, + setAllowProfileFormUntil, + "allowProfileFormUntil", + ) + } title="Close registration on" placeholder="1970-01-01 00:00:00" /> + + + + handleDateChange( + value, + setAcceptanceDeadline, + "acceptanceDeadline", + ) + } + title="When we will accept people" + placeholder="1970-01-01 00:00:00" + /> + + + + + handleDateChange(value, setConfirmSpotUntil, "confirmSpotUntil") + } + title="Until when accepted people need to confirm their spot" + placeholder="1970-01-01 00:00:00" + /> + + Use the add button to add new questions and the edit button in the top diff --git a/frontend/src/components/settings/project-rating/rating-criteria-settings.tsx b/frontend/src/components/settings/project-rating/rating-criteria-settings.tsx index 3b6e01d9..2d8d6a41 100644 --- a/frontend/src/components/settings/project-rating/rating-criteria-settings.tsx +++ b/frontend/src/components/settings/project-rating/rating-criteria-settings.tsx @@ -91,7 +91,7 @@ export const ProjectRatingSettings = () => { useEffect(() => { // Only update if settings are loaded - if (settings.application) { + if (settings.project) { api.updateSettings(settings as SettingsDTO); } }, [settings]); @@ -138,8 +138,8 @@ export const ProjectRatingSettings = () => { setSettings((prev) => { const changedSettings = { ...prev, - application: { - ...prev.application, + project: { + ...prev.project, allowRatingProjects: value, }, }; @@ -155,7 +155,7 @@ export const ProjectRatingSettings = () => { }