diff --git a/legalconsenthub-backend/api/legalconsenthub.yml b/legalconsenthub-backend/api/legalconsenthub.yml index 8a3c20d..73d0be3 100644 --- a/legalconsenthub-backend/api/legalconsenthub.yml +++ b/legalconsenthub-backend/api/legalconsenthub.yml @@ -193,6 +193,37 @@ paths: "503": $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServiceUnavailable" + /application-forms/{id}/submit: + post: + summary: Submit an application form + operationId: submitApplicationForm + tags: + - application-form + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + "200": + description: Application form successfully submitted + content: + application/json: + schema: + $ref: "#/components/schemas/ApplicationFormDto" + "400": + $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/BadRequest" + "401": + $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/Unauthorized" + "404": + $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/NotFound" + "500": + $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServerError" + "503": + $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServiceUnavailable" + ####### Application Form Templates ####### /application-form-templates: get: @@ -672,6 +703,7 @@ components: - lastModifiedBy - createdAt - modifiedAt + - status properties: id: type: string @@ -696,6 +728,8 @@ components: modifiedAt: type: string format: date-time + status: + $ref: "#/components/schemas/ApplicationFormStatus" CreateApplicationFormDto: required: @@ -715,6 +749,9 @@ components: default: false organizationId: type: string + status: + $ref: "#/components/schemas/ApplicationFormStatus" + default: DRAFT PagedApplicationFormDto: type: object @@ -1009,6 +1046,15 @@ components: - WARNING - CRITICAL + ApplicationFormStatus: + type: string + enum: + - DRAFT + - SUBMITTED + - APPROVED + - REJECTED + - SIGNED + ####### Supporting components ####### Page: type: object diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationForm.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationForm.kt index bd68e5d..5a27f18 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationForm.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationForm.kt @@ -2,12 +2,15 @@ package com.betriebsratkanzlei.legalconsenthub.application_form import com.betriebsratkanzlei.legalconsenthub.form_element.FormElementSection import com.betriebsratkanzlei.legalconsenthub.user.User +import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormStatus import jakarta.persistence.AttributeOverride import jakarta.persistence.AttributeOverrides import jakarta.persistence.CascadeType import jakarta.persistence.Column import jakarta.persistence.Entity import jakarta.persistence.EntityListeners +import jakarta.persistence.Enumerated +import jakarta.persistence.EnumType import jakarta.persistence.GeneratedValue import jakarta.persistence.Id import jakarta.persistence.OneToMany @@ -36,6 +39,10 @@ class ApplicationForm( var organizationId: String = "", + @Enumerated(EnumType.STRING) + @Column(nullable = false) + var status: ApplicationFormStatus = ApplicationFormStatus.DRAFT, + @Embedded @AttributeOverrides( AttributeOverride(name = "id", column = Column(name = "created_by_id", nullable = false)), diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormController.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormController.kt index 35ade87..7f3c410 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormController.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormController.kt @@ -77,4 +77,12 @@ class ApplicationFormController( applicationFormService.deleteApplicationFormByID(id) return ResponseEntity.noContent().build() } + + override fun submitApplicationForm(id: UUID): ResponseEntity { + return ResponseEntity.ok( + applicationFormMapper.toApplicationFormDto( + applicationFormService.submitApplicationForm(id) + ) + ) + } } diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormMapper.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormMapper.kt index 23bddf1..4c22b95 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormMapper.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormMapper.kt @@ -22,7 +22,8 @@ class ApplicationFormMapper(private val formElementSectionMapper: FormElementSec createdBy = userMapper.toUserDto(applicationForm.createdBy), lastModifiedBy = userMapper.toUserDto(applicationForm.lastModifiedBy), createdAt = applicationForm.createdAt ?: LocalDateTime.now(), - modifiedAt = applicationForm.modifiedAt ?: LocalDateTime.now() + modifiedAt = applicationForm.modifiedAt ?: LocalDateTime.now(), + status = applicationForm.status ) } @@ -33,6 +34,7 @@ class ApplicationFormMapper(private val formElementSectionMapper: FormElementSec formElementSections = applicationForm.formElementSections.map { formElementSectionMapper.toFormElementSection(it) }.toMutableList(), isTemplate = applicationForm.isTemplate, organizationId = applicationForm.organizationId, + status = applicationForm.status, createdBy = userMapper.toUser(applicationForm.createdBy), lastModifiedBy = userMapper.toUser(applicationForm.lastModifiedBy), createdAt = applicationForm.createdAt, @@ -50,6 +52,7 @@ class ApplicationFormMapper(private val formElementSectionMapper: FormElementSec name = createApplicationFormDto.name, isTemplate = createApplicationFormDto.isTemplate, organizationId = createApplicationFormDto.organizationId ?: "", + status = createApplicationFormDto.status ?: com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormStatus.DRAFT, createdBy = createdBy, lastModifiedBy = lastModifiedBy, ) diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormService.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormService.kt index 51ba940..187acd1 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormService.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormService.kt @@ -1,10 +1,12 @@ package com.betriebsratkanzlei.legalconsenthub.application_form +import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormInvalidStateException import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotCreatedException import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotDeletedException import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotFoundException import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotUpdatedException import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormDto +import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormStatus import com.betriebsratkanzlei.legalconsenthub_api.model.CreateApplicationFormDto import org.springframework.data.domain.Page import org.springframework.data.domain.PageRequest @@ -59,4 +61,25 @@ class ApplicationFormService( throw ApplicationFormNotDeletedException(e) } } + + fun submitApplicationForm(id: UUID): ApplicationForm { + val applicationForm = getApplicationFormById(id) + + if (applicationForm.status != ApplicationFormStatus.DRAFT) { + throw ApplicationFormInvalidStateException( + applicationFormId = id, + currentState = applicationForm.status, + expectedState = ApplicationFormStatus.DRAFT, + operation = "submit" + ) + } + + applicationForm.status = ApplicationFormStatus.SUBMITTED + + return try { + applicationFormRepository.save(applicationForm) + } catch (e: Exception) { + throw ApplicationFormNotUpdatedException(e, id) + } + } } diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/error/ApplicationFormInvalidStateException.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/error/ApplicationFormInvalidStateException.kt new file mode 100644 index 0000000..a812670 --- /dev/null +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/error/ApplicationFormInvalidStateException.kt @@ -0,0 +1,11 @@ +package com.betriebsratkanzlei.legalconsenthub.error + +import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormStatus +import java.util.UUID + +class ApplicationFormInvalidStateException( + val applicationFormId: UUID, + val currentState: ApplicationFormStatus, + val expectedState: ApplicationFormStatus, + val operation: String +) : RuntimeException("Cannot $operation application form with ID $applicationFormId. Current state: $currentState, expected state: $expectedState") \ No newline at end of file diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/error/ExceptionHandler.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/error/ExceptionHandler.kt index 9a9b398..48490e0 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/error/ExceptionHandler.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/error/ExceptionHandler.kt @@ -31,6 +31,22 @@ class ExceptionHandler { ) } + @ResponseBody + @ExceptionHandler(ApplicationFormInvalidStateException::class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + fun handleInvalidStateError(e: ApplicationFormInvalidStateException): ResponseEntity { + logger.warn(e.message, e) + return ResponseEntity.status(HttpStatus.BAD_REQUEST).contentType(MediaType.APPLICATION_PROBLEM_JSON) + .body( + ProblemDetails( + title = "Invalid State", + status = HttpStatus.BAD_REQUEST.value(), + type = URI.create("about:blank"), + detail = e.message ?: "Operation not allowed in current state" + ) + ) + } + @ResponseBody @ExceptionHandler( ApplicationFormNotCreatedException::class, diff --git a/legalconsenthub-backend/src/main/resources/db/migrations/001-schema.sql b/legalconsenthub-backend/src/main/resources/db/migrations/001-schema.sql index 0ef19dd..8466d50 100644 --- a/legalconsenthub-backend/src/main/resources/db/migrations/001-schema.sql +++ b/legalconsenthub-backend/src/main/resources/db/migrations/001-schema.sql @@ -10,18 +10,20 @@ create table application_form last_modified_by_name varchar(255) not null, name varchar(255) not null, organization_id varchar(255), + status enum ('APPROVED','DRAFT','REJECTED','SIGNED','SUBMITTED') not null, primary key (id) ); create table comment ( - created_at timestamp(6) not null, - modified_at timestamp(6) not null, - form_element_id uuid not null, - id uuid not null, - created_by_id varchar(255) not null, - created_by_name varchar(255) not null, - message varchar(255) not null, + created_at timestamp(6) not null, + modified_at timestamp(6) not null, + application_form_id uuid not null, + form_element_id uuid not null, + id uuid not null, + created_by_id varchar(255) not null, + created_by_name varchar(255) not null, + message varchar(255) not null, primary key (id) ); @@ -36,12 +38,29 @@ create table form_element_options create table form_element ( - type tinyint not null check (type between 0 and 4), - application_form_id uuid not null, - id uuid not null, + type tinyint not null check (type between 0 and 4), + form_element_section_id uuid not null, + id uuid not null, + description varchar(255), + title varchar(255), primary key (id) ); +create table form_element_section +( + application_form_id uuid not null, + id uuid not null, + description varchar(255), + short_title varchar(255), + title varchar(255) not null, + primary key (id) +); + +alter table if exists comment + add constraint FKlavy9axrt26sepreg5lqtuoap + foreign key (application_form_id) + references application_form; + alter table if exists comment add constraint FKfg84w0i76tw9os13950272c6f foreign key (form_element_id) @@ -53,6 +72,11 @@ alter table if exists form_element_options references form_element; alter table if exists form_element - add constraint FKdniyq3l10lncw48tft15js5gb + add constraint FKdpr6k93m4hqllqjsvoa4or6mp + foreign key (form_element_section_id) + references form_element_section; + +alter table if exists form_element_section + add constraint FKtn0lreovauwf2v29doo70o3qs foreign key (application_form_id) references application_form; diff --git a/legalconsenthub/composables/applicationForm/useApplicationForm.ts b/legalconsenthub/composables/applicationForm/useApplicationForm.ts index dc810f0..ff5749b 100644 --- a/legalconsenthub/composables/applicationForm/useApplicationForm.ts +++ b/legalconsenthub/composables/applicationForm/useApplicationForm.ts @@ -83,11 +83,29 @@ export function useApplicationForm() { } } + async function submitApplicationForm(id: string): Promise { + if (!id) { + return Promise.reject(new Error('ID missing')) + } + + try { + return await applicationFormApi.submitApplicationForm(id) + } catch (e: unknown) { + if (e instanceof ResponseError) { + console.error(`Failed submitting application form with ID ${id}:`, e.response) + } else { + console.error(`Failed submitting application form with ID ${id}:`, e) + } + return Promise.reject(e) + } + } + return { createApplicationForm, getAllApplicationForms, getApplicationFormById, updateApplicationForm, - deleteApplicationFormById + deleteApplicationFormById, + submitApplicationForm } } diff --git a/legalconsenthub/composables/applicationForm/useApplicationFormApi.ts b/legalconsenthub/composables/applicationForm/useApplicationFormApi.ts index e56e7a5..b4b976c 100644 --- a/legalconsenthub/composables/applicationForm/useApplicationFormApi.ts +++ b/legalconsenthub/composables/applicationForm/useApplicationFormApi.ts @@ -45,11 +45,16 @@ export function useApplicationFormApi() { return applicationFormApiClient.deleteApplicationForm({ id }) } + async function submitApplicationForm(id: string): Promise { + return applicationFormApiClient.submitApplicationForm({ id }) + } + return { createApplicationForm, getAllApplicationForms, getApplicationFormById, updateApplicationForm, - deleteApplicationFormById + deleteApplicationFormById, + submitApplicationForm } } diff --git a/legalconsenthub/pages/application-forms/[id]/[sectionIndex].vue b/legalconsenthub/pages/application-forms/[id]/[sectionIndex].vue index 1fbafee..3c944ac 100644 --- a/legalconsenthub/pages/application-forms/[id]/[sectionIndex].vue +++ b/legalconsenthub/pages/application-forms/[id]/[sectionIndex].vue @@ -15,7 +15,15 @@ @@ -51,14 +59,15 @@ > Next - - Submit - + +
+ + Save + + + Submit + +
@@ -70,13 +79,14 @@ import type { ApplicationFormDto, FormElementSectionDto } from '~/.api-client' import type { StepperItem } from '@nuxt/ui' -const { getApplicationFormById, updateApplicationForm } = useApplicationForm() +const { getApplicationFormById, updateApplicationForm, submitApplicationForm } = useApplicationForm() const route = useRoute() const { user } = useAuth() +const toast = useToast() definePageMeta({ // Prevent whole page from re-rendering when navigating between sections to keep state - key: (route) => `${route.params.id}`, + key: (route) => `${route.params.id}` }) onMounted(() => { @@ -136,10 +146,18 @@ async function navigateStepper(direction: 'forward' | 'backward') { await navigateTo(`/application-forms/${route.params.id}/${activeStepperItemIndex.value}`) } -async function onSubmit() { +async function onSave() { if (data?.value) { await updateApplicationForm(data.value.id, data.value) + toast.add({ title: 'Success', description: 'Application form saved', color: 'success' }) + } +} + +async function onSubmit() { + if (data?.value) { + await submitApplicationForm(data.value.id) await navigateTo('/') + toast.add({ title: 'Success', description: 'Application form submitted', color: 'success' }) } } diff --git a/legalconsenthub/pages/create.vue b/legalconsenthub/pages/create.vue index 961fdaf..4bb633d 100644 --- a/legalconsenthub/pages/create.vue +++ b/legalconsenthub/pages/create.vue @@ -16,14 +16,11 @@ @@ -98,10 +84,11 @@ import type { FormElementId } from '~/types/FormElement' import type { StepperItem } from '@nuxt/ui' const { getAllApplicationFormTemplates } = useApplicationFormTemplate() -const { createApplicationForm } = useApplicationForm() +const { createApplicationForm, submitApplicationForm } = useApplicationForm() const { validateFormElements, getHighestComplianceStatus } = useApplicationFormValidator() const { userDto, selectedOrganization } = useAuth() const { canCreateApplicationForm, getCurrentRoleInfo } = usePermissions() +const toast = useToast() // Get current role information for display const currentRoleInfo = computed(() => getCurrentRoleInfo()) @@ -174,7 +161,7 @@ watch( { deep: true } ) -const ampelStatusEmoji = computed(() => { +const trafficLightStatusEmoji = computed(() => { switch (validationStatus.value) { case ComplianceStatus.Critical: return '🔴' @@ -187,16 +174,32 @@ const ampelStatusEmoji = computed(() => { } }) -async function onSubmit() { - if (applicationFormTemplate.value) { - applicationFormTemplate.value.createdBy = userDto.value - applicationFormTemplate.value.lastModifiedBy = userDto.value - applicationFormTemplate.value.organizationId = selectedOrganization.value?.id ?? '' - - await createApplicationForm(applicationFormTemplate.value) - await navigateTo('/') - } else { - console.error('Application form data is undefined') +async function onSave() { + const applicationForm = await prepareAndCreateApplicationForm() + if (applicationForm) { + toast.add({ title: 'Success', description: 'Application form saved', color: 'success' }) } } + +async function onSubmit() { + const applicationForm = await prepareAndCreateApplicationForm() + if (applicationForm) { + await submitApplicationForm(applicationForm.id) + await navigateTo('/') + toast.add({ title: 'Success', description: 'Application form submitted', color: 'success' }) + } +} + +async function prepareAndCreateApplicationForm() { + if (!applicationFormTemplate.value) { + console.error('Application form data is undefined') + return null + } + + applicationFormTemplate.value.createdBy = userDto.value + applicationFormTemplate.value.lastModifiedBy = userDto.value + applicationFormTemplate.value.organizationId = selectedOrganization.value?.id ?? '' + + return await createApplicationForm(applicationFormTemplate.value) +} diff --git a/legalconsenthub/pages/index.vue b/legalconsenthub/pages/index.vue index c7b55ac..6b063bb 100644 --- a/legalconsenthub/pages/index.vue +++ b/legalconsenthub/pages/index.vue @@ -49,6 +49,11 @@

Erstellt von {{ applicationFormElem.createdBy.name }} am {{ formatDate(applicationFormElem.createdAt) }}

+
+ + {{ applicationFormElem.status }} + +