summaryrefslogtreecommitdiff
path: root/src/main
diff options
context:
space:
mode:
authorPaul-Christian Volkmer2026-01-06 16:34:12 +0100
committerGitHub2026-01-06 15:34:12 +0000
commit7be91444a867774362eb5b57bdd246fb50189e7d (patch)
tree6a325575bf19e4016ead259a92803b110071eb4f /src/main
parent2a106a49d91699d0699af1134c41a43b942b85e8 (diff)
feat: block further initial submissions (#232)
Diffstat (limited to 'src/main')
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt1
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt56
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt2
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt28
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/monitoring/RequestStatus.kt1
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/monitoring/SubmissionType.kt12
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/output/RestMtbFileSender.kt3
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt75
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/web/HomeController.kt34
-rw-r--r--src/main/resources/db/migration/mariadb/V0_5_0__SubmissionType.sql2
-rw-r--r--src/main/resources/db/migration/postgresql/V0_5_0__SubmissionType.sql2
-rw-r--r--src/main/resources/static/style.css17
-rw-r--r--src/main/resources/templates/fragments.html14
-rw-r--r--src/main/resources/templates/index.html11
14 files changed, 219 insertions, 39 deletions
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt
index 7d795c8..96e48ea 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt
@@ -29,6 +29,7 @@ data class AppConfigProperties(
var maxRetryAttempts: Int = 3,
var duplicationDetection: Boolean = true,
var genomDeTestSubmission: Boolean = false,
+ var postInitialSubmissionBlock: Boolean = false,
) {
companion object {
const val NAME = "app"
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt
index f21c09b..40c290a 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt
@@ -32,6 +32,8 @@ import dev.dnpm.etl.processor.security.TokenService
import dev.dnpm.etl.processor.services.ConsentProcessor
import dev.dnpm.etl.processor.services.Transformation
import dev.dnpm.etl.processor.services.TransformationService
+import kotlin.time.Duration.Companion.seconds
+import kotlin.time.toJavaDuration
import org.apache.cxf.jaxws.JaxWsProxyFactoryBean
import org.slf4j.LoggerFactory
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition
@@ -58,8 +60,6 @@ import org.springframework.security.provisioning.InMemoryUserDetailsManager
import org.springframework.web.client.HttpClientErrorException
import org.springframework.web.client.RestTemplate
import reactor.core.publisher.Sinks
-import kotlin.time.Duration.Companion.seconds
-import kotlin.time.toJavaDuration
@Configuration
@EnableConfigurationProperties(
@@ -77,12 +77,14 @@ class AppConfiguration {
private val logger = LoggerFactory.getLogger(AppConfiguration::class.java)
- fun stringHttpMessageConverter(): StringHttpMessageConverter {
- return StringHttpMessageConverter()
- }
+ fun stringHttpMessageConverter(): StringHttpMessageConverter {
+ return StringHttpMessageConverter()
+ }
- @Bean
- fun mappingJacksonHttpMessageConverter(objectMapper: ObjectMapper): MappingJackson2HttpMessageConverter {
+ @Bean
+ fun mappingJacksonHttpMessageConverter(
+ objectMapper: ObjectMapper
+ ): MappingJackson2HttpMessageConverter {
val converter = MappingJackson2HttpMessageConverter()
converter.setObjectMapper(objectMapper)
return converter
@@ -91,7 +93,10 @@ class AppConfiguration {
@Bean
fun restTemplate(objectMapper: ObjectMapper): RestTemplate {
return RestTemplateBuilder()
- .messageConverters(stringHttpMessageConverter(), mappingJacksonHttpMessageConverter(objectMapper))
+ .messageConverters(
+ stringHttpMessageConverter(),
+ mappingJacksonHttpMessageConverter(objectMapper),
+ )
.build()
}
@@ -273,16 +278,21 @@ class AppConfiguration {
return GicsConsentService(gIcsConfigProperties, retryTemplate, restTemplate, appFhirConfig)
}
- @Conditional(GicsGetBroadConsentEnabledCondition::class)
- @Bean
- fun gicsGetBroadConsentService(
- gIcsConfigProperties: GIcsConfigProperties,
- retryTemplate: RetryTemplate,
- restTemplate: RestTemplate,
- appFhirConfig: AppFhirConfig,
- ): IConsentService {
- return GicsGetBroadConsentService(gIcsConfigProperties, retryTemplate, restTemplate, appFhirConfig)
- }
+ @Conditional(GicsGetBroadConsentEnabledCondition::class)
+ @Bean
+ fun gicsGetBroadConsentService(
+ gIcsConfigProperties: GIcsConfigProperties,
+ retryTemplate: RetryTemplate,
+ restTemplate: RestTemplate,
+ appFhirConfig: AppFhirConfig,
+ ): IConsentService {
+ return GicsGetBroadConsentService(
+ gIcsConfigProperties,
+ retryTemplate,
+ restTemplate,
+ appFhirConfig,
+ )
+ }
@Conditional(GicsEnabledCondition::class)
@Bean
@@ -336,9 +346,9 @@ class GicsEnabledCondition :
class GicsGetBroadConsentEnabledCondition :
AnyNestedCondition(ConfigurationCondition.ConfigurationPhase.REGISTER_BEAN) {
- @ConditionalOnProperty(name = ["app.consent.service"], havingValue = "gics_get_bc")
- @ConditionalOnProperty(name = ["app.consent.gics.uri"])
- class OnGicsGetBroadConsentServiceSelected {
- // Just for Condition
- }
+ @ConditionalOnProperty(name = ["app.consent.service"], havingValue = "gics_get_bc")
+ @ConditionalOnProperty(name = ["app.consent.gics.uri"])
+ class OnGicsGetBroadConsentServiceSelected {
+ // Just for Condition
+ }
}
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt
index 6b7cab8..e0f24cf 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt
@@ -94,6 +94,7 @@ class AppSecurityConfiguration(private val securityConfigProperties: SecurityCon
authorize("/mtbfile/**", hasAnyRole("MTBFILE", "ADMIN", "USER"))
authorize("/mtb/**", hasAnyRole("MTBFILE", "ADMIN", "USER"))
authorize("/report/**", hasAnyRole("ADMIN", "USER"))
+ authorize("/submission/**", hasAnyRole("ADMIN", "USER"))
authorize("*.css", permitAll)
authorize("*.ico", permitAll)
authorize("*.jpeg", permitAll)
@@ -161,6 +162,7 @@ class AppSecurityConfiguration(private val securityConfigProperties: SecurityCon
authorize("/mtbfile/**", hasAnyRole("MTBFILE", "ADMIN"))
authorize("/mtb/**", hasAnyRole("MTBFILE", "ADMIN"))
authorize("/report/**", hasRole("ADMIN"))
+ authorize("/submission/**", hasAnyRole("ADMIN"))
authorize(anyRequest, permitAll)
}
httpBasic { realmName = "ETL-Processor" }
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt
index 71731f1..b1cc1ed 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt
@@ -41,9 +41,11 @@ data class Request(
val pid: PatientId,
@Column("fingerprint") val fingerprint: Fingerprint,
val type: RequestType,
+ @Column("submission_type") val submissionType: SubmissionType,
var status: RequestStatus,
var processedAt: Instant = Instant.now(),
@Embedded.Nullable var report: Report? = null,
+ @Column("submission_accepted") var submissionAccepted: Boolean = false,
) {
constructor(
uuid: RequestId,
@@ -51,8 +53,19 @@ data class Request(
pid: PatientId,
fingerprint: Fingerprint,
type: RequestType,
+ submissionType: SubmissionType,
status: RequestStatus,
- ) : this(null, uuid, patientPseudonym, pid, fingerprint, type, status, Instant.now())
+ ) : this(
+ null,
+ uuid,
+ patientPseudonym,
+ pid,
+ fingerprint,
+ type,
+ submissionType,
+ status,
+ Instant.now(),
+ )
constructor(
uuid: RequestId,
@@ -60,9 +73,20 @@ data class Request(
pid: PatientId,
fingerprint: Fingerprint,
type: RequestType,
+ submissionType: SubmissionType,
status: RequestStatus,
processedAt: Instant,
- ) : this(null, uuid, patientPseudonym, pid, fingerprint, type, status, processedAt)
+ ) : this(
+ null,
+ uuid,
+ patientPseudonym,
+ pid,
+ fingerprint,
+ type,
+ submissionType,
+ status,
+ processedAt,
+ )
fun isPendingUnknown(): Boolean {
return this.status == RequestStatus.UNKNOWN &&
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/RequestStatus.kt b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/RequestStatus.kt
index 5487a05..a0cd4ad 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/RequestStatus.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/RequestStatus.kt
@@ -28,4 +28,5 @@ enum class RequestStatus(
UNKNOWN("unknown"),
DUPLICATION("duplication"),
NO_CONSENT("no-consent"),
+ BLOCKED_INITIAL("blocked-initial"),
}
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/SubmissionType.kt b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/SubmissionType.kt
new file mode 100644
index 0000000..351281e
--- /dev/null
+++ b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/SubmissionType.kt
@@ -0,0 +1,12 @@
+package dev.dnpm.etl.processor.monitoring
+
+enum class SubmissionType(
+ val value: String,
+) {
+ UNKNOWN("unknown"),
+ INITIAL("initial"),
+ ADDITION("addition"),
+ CORRECTION("correction"),
+ FOLLOWUP("followup"),
+ TEST("test"),
+}
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/output/RestMtbFileSender.kt b/src/main/kotlin/dev/dnpm/etl/processor/output/RestMtbFileSender.kt
index 9c22ec0..a2a88b6 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/output/RestMtbFileSender.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/output/RestMtbFileSender.kt
@@ -53,7 +53,8 @@ abstract class RestMtbFileSender(
return retryTemplate.execute<MtbFileSender.Response, Exception> {
val headers = getHttpHeaders(request)
val entityReq = HttpEntity(request.content, headers)
- val response = restTemplate.exchange(sendUrl(), HttpMethod.POST, entityReq, String::class.java)
+ val response =
+ restTemplate.exchange(sendUrl(), HttpMethod.POST, entityReq, String::class.java)
if (!response.statusCode.is2xxSuccessful) {
logger.warn("Error sending to remote system: {}", response.body)
return@execute MtbFileSender.Response(
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt b/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt
index 22a25ee..2b80167 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt
@@ -27,6 +27,7 @@ import dev.dnpm.etl.processor.monitoring.Report
import dev.dnpm.etl.processor.monitoring.Request
import dev.dnpm.etl.processor.monitoring.RequestStatus
import dev.dnpm.etl.processor.monitoring.RequestType
+import dev.dnpm.etl.processor.monitoring.SubmissionType
import dev.dnpm.etl.processor.output.DeleteRequest
import dev.dnpm.etl.processor.output.DnpmV2MtbFileRequest
import dev.dnpm.etl.processor.output.MtbFileRequest
@@ -38,6 +39,7 @@ import dev.dnpm.etl.processor.pseudonym.pseudonymizeWith
import dev.pcvolkmer.mv64e.mtb.ConsentProvision
import dev.pcvolkmer.mv64e.mtb.ModelProjectConsentPurpose
import dev.pcvolkmer.mv64e.mtb.Mtb
+import dev.pcvolkmer.mv64e.mtb.MvhSubmissionType
import java.time.Instant
import java.util.*
import org.apache.commons.codec.binary.Base32
@@ -94,6 +96,53 @@ class RequestProcessor(
}
private fun <T> saveAndSend(request: MtbFileRequest<T>) {
+ var submissionType: SubmissionType =
+ when (request) {
+ is DnpmV2MtbFileRequest -> {
+ when (request.content.metadata?.type) {
+ MvhSubmissionType.TEST -> SubmissionType.TEST
+ MvhSubmissionType.INITIAL -> SubmissionType.INITIAL
+ MvhSubmissionType.ADDITION -> SubmissionType.ADDITION
+ MvhSubmissionType.CORRECTION -> SubmissionType.CORRECTION
+ MvhSubmissionType.FOLLOWUP -> SubmissionType.FOLLOWUP
+ else -> SubmissionType.UNKNOWN
+ }
+ }
+ }
+
+ if (
+ appConfigProperties.postInitialSubmissionBlock &&
+ hasSuccessfullInitialSubmission(request.patientPseudonym()) &&
+ hasUnacceptedInitialSubmission(request.patientPseudonym())
+ ) {
+ requestService.save(
+ Request(
+ request.requestId,
+ request.patientPseudonym(),
+ emptyPatientId(),
+ fingerprint(request),
+ RequestType.MTB_FILE,
+ submissionType,
+ RequestStatus.BLOCKED_INITIAL,
+ )
+ )
+ // Exit - no further processing
+ return
+ }
+
+ if (
+ appConfigProperties.postInitialSubmissionBlock &&
+ hasSuccessfullInitialSubmission(request.patientPseudonym()) &&
+ !hasUnacceptedInitialSubmission(request.patientPseudonym())
+ ) {
+ // Use "addition" after "intial" with "Meldebestaetigung"
+ request.content.metadata?.let {
+ logger.warn("Override submission type using 'addition' after first initial submission!")
+ it.type = MvhSubmissionType.ADDITION
+ submissionType = SubmissionType.ADDITION
+ }
+ }
+
requestService.save(
Request(
request.requestId,
@@ -101,6 +150,7 @@ class RequestProcessor(
emptyPatientId(),
fingerprint(request),
RequestType.MTB_FILE,
+ submissionType,
RequestStatus.UNKNOWN,
)
)
@@ -128,6 +178,20 @@ class RequestProcessor(
)
}
+ private fun hasSuccessfullInitialSubmission(patientPseudonym: PatientPseudonym): Boolean {
+ return this.requestService.allRequestsByPatientPseudonym(patientPseudonym).any {
+ it.submissionType == SubmissionType.INITIAL &&
+ (it.status == RequestStatus.SUCCESS || it.status == RequestStatus.WARNING)
+ }
+ }
+
+ private fun hasUnacceptedInitialSubmission(patientPseudonym: PatientPseudonym): Boolean {
+ return this.requestService.allRequestsByPatientPseudonym(patientPseudonym).any {
+ it.submissionType == SubmissionType.INITIAL &&
+ !(it.submissionAccepted || it.status == RequestStatus.BLOCKED_INITIAL)
+ }
+ }
+
private fun <T> isDuplication(pseudonymizedMtbFileRequest: MtbFileRequest<T>): Boolean {
val patientPseudonym =
when (pseudonymizedMtbFileRequest) {
@@ -179,6 +243,7 @@ class RequestProcessor(
emptyPatientId(),
fingerprint(patientPseudonym.value),
RequestType.DELETE,
+ SubmissionType.UNKNOWN,
requestStatus,
)
)
@@ -206,6 +271,7 @@ class RequestProcessor(
fingerprint = Fingerprint.empty(),
status = RequestStatus.ERROR,
type = RequestType.DELETE,
+ submissionType = SubmissionType.UNKNOWN,
report = Report("Fehler bei der Pseudonymisierung"),
)
)
@@ -219,11 +285,8 @@ class RequestProcessor(
}
private fun fingerprint(s: String): Fingerprint {
- return Fingerprint(
- Base32()
- .encodeAsString(DigestUtils.sha256(s))
- .replace("=", "")
- .lowercase()
- )
+ return Fingerprint(Base32().encodeAsString(DigestUtils.sha256(s))
+ .replace("=", "")
+ .lowercase())
}
}
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/web/HomeController.kt b/src/main/kotlin/dev/dnpm/etl/processor/web/HomeController.kt
index 082cd20..ed0f264 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/web/HomeController.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/web/HomeController.kt
@@ -22,6 +22,7 @@ package dev.dnpm.etl.processor.web
import dev.dnpm.etl.processor.NotFoundException
import dev.dnpm.etl.processor.PatientPseudonym
import dev.dnpm.etl.processor.RequestId
+import dev.dnpm.etl.processor.config.AppConfigProperties
import dev.dnpm.etl.processor.monitoring.ReportService
import dev.dnpm.etl.processor.services.RequestService
import org.springframework.data.domain.Pageable
@@ -29,8 +30,10 @@ import org.springframework.data.domain.Sort
import org.springframework.data.web.PageableDefault
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
+import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
+import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestMapping
@Controller
@@ -38,6 +41,7 @@ import org.springframework.web.bind.annotation.RequestMapping
class HomeController(
private val requestService: RequestService,
private val reportService: ReportService,
+ private val appConfigProperties: AppConfigProperties,
) {
@GetMapping
fun index(
@@ -47,7 +51,7 @@ class HomeController(
): String {
val requests = requestService.findAll(pageable)
model.addAttribute("requests", requests)
-
+ model.addAttribute("postInitialSubmissionBlock", appConfigProperties.postInitialSubmissionBlock)
return "index"
}
@@ -76,4 +80,32 @@ class HomeController(
return "report"
}
+
+ @PutMapping(path = ["/submission/{id}/accepted"])
+ fun acceptSubmission(
+ @PathVariable id: RequestId,
+ model: Model,
+ ): String {
+ val request = requestService.findByUuid(id).orElse(null) ?: throw NotFoundException()
+ request.submissionAccepted = true
+ val savedRequest = requestService.save(request)
+
+ model.addAttribute("request", savedRequest)
+
+ return "fragments :: accept-initial"
+ }
+
+ @DeleteMapping(path = ["/submission/{id}/accepted"])
+ fun unacceptSubmission(
+ @PathVariable id: RequestId,
+ model: Model,
+ ): String {
+ val request = requestService.findByUuid(id).orElse(null) ?: throw NotFoundException()
+ request.submissionAccepted = false
+ val savedRequest = requestService.save(request)
+
+ model.addAttribute("request", savedRequest)
+
+ return "fragments :: accept-initial"
+ }
}
diff --git a/src/main/resources/db/migration/mariadb/V0_5_0__SubmissionType.sql b/src/main/resources/db/migration/mariadb/V0_5_0__SubmissionType.sql
new file mode 100644
index 0000000..79f89d8
--- /dev/null
+++ b/src/main/resources/db/migration/mariadb/V0_5_0__SubmissionType.sql
@@ -0,0 +1,2 @@
+ALTER TABLE request ADD COLUMN submission_type varchar(16) not null default 'UNKNOWN';
+ALTER TABLE request ADD COLUMN submission_accepted boolean not null default false; \ No newline at end of file
diff --git a/src/main/resources/db/migration/postgresql/V0_5_0__SubmissionType.sql b/src/main/resources/db/migration/postgresql/V0_5_0__SubmissionType.sql
new file mode 100644
index 0000000..79f89d8
--- /dev/null
+++ b/src/main/resources/db/migration/postgresql/V0_5_0__SubmissionType.sql
@@ -0,0 +1,2 @@
+ALTER TABLE request ADD COLUMN submission_type varchar(16) not null default 'UNKNOWN';
+ALTER TABLE request ADD COLUMN submission_accepted boolean not null default false; \ No newline at end of file
diff --git a/src/main/resources/static/style.css b/src/main/resources/static/style.css
index 83d98c3..33e5d20 100644
--- a/src/main/resources/static/style.css
+++ b/src/main/resources/static/style.css
@@ -360,7 +360,7 @@ form.samplecode-input input:focus-visible {
}
.border {
- padding: 1.5rem;
+ padding: 1rem;
border: 1px solid var(--table-border);
border-radius: .5rem;
background: white;
@@ -382,7 +382,7 @@ table {
}
table.config-table td:first-child {
- width: 24rem;
+ width: 26rem;
min-width: fit-content;
}
@@ -451,10 +451,13 @@ th {
}
td {
- font-family: monospace;
border-bottom: 1px solid var(--bg-gray-op);
}
+td, td > a {
+ font-family: monospace;
+}
+
tr:last-of-type > td {
border-bottom: none;
}
@@ -465,10 +468,9 @@ td > small {
}
td.patient-id {
- width: 32rem;
+ min-width: 20rem;
text-overflow: ellipsis;
overflow: hidden;
- display: block;
}
td.bg-blue, th.bg-blue,
@@ -571,6 +573,11 @@ td.clipboard.clipped {
color: white;
}
+.btn.btn-green {
+ background: var(--bg-green);
+ color: white;
+}
+
.btn.btn-blue {
background: var(--bg-blue);
color: white;
diff --git a/src/main/resources/templates/fragments.html b/src/main/resources/templates/fragments.html
index f3d95c9..8ecd507 100644
--- a/src/main/resources/templates/fragments.html
+++ b/src/main/resources/templates/fragments.html
@@ -35,6 +35,20 @@
</ul>
</nav>
</div>
+ <th:block th:fragment="accept-initial" sec:authorize="hasRole('USER') or hasRole('ADMIN')">
+ <button class="btn" hx-swap="outerHTML" th:hx-delete="@{/submission/{requestId}/accepted(requestId=${request.uuid})}" th:if="${
+ request.submissionType.value == 'initial'
+ and (request.status.value == 'success' or request.status.value == 'warning')
+ and request.submissionAccepted == true}" title="Keine Meldebestätigung - blockieren">
+ đź”’
+ </button>
+ <button class="btn" hx-swap="outerHTML" th:hx-put="@{/submission/{requestId}/accepted(requestId=${request.uuid})}" th:if="${
+ request.submissionType.value == 'initial'
+ and (request.status.value == 'success' or request.status.value == 'warning')
+ and request.submissionAccepted == false}" title="Meldebestätigung vorhanden - nicht weiter blockieren">
+ 🔓
+ </button>
+ </th:block>
<footer th:fragment="footer">
<div class="container">
<div>
diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html
index 5021bc4..a732fa2 100644
--- a/src/main/resources/templates/index.html
+++ b/src/main/resources/templates/index.html
@@ -42,6 +42,7 @@
<tr>
<th>Status</th>
<th>Typ</th>
+ <th sec:authorize="hasRole('USER') or hasRole('ADMIN')" th:if="${postInitialSubmissionBlock}">Aktion</th>
<th>ID</th>
<th>Datum</th>
<th>Patienten-ID</th>
@@ -56,7 +57,14 @@
<td th:if="${request.status.value == 'unknown' and request.isPendingUnknown()}" class="bg-yellow"><small>⏰ [[ ${request.status} ]] ⏰</small></td>
<td th:if="${request.status.value == 'duplication'}" class="bg-gray"><small>[[ ${request.status} ]]</small></td>
<td th:if="${request.status.value == 'no-consent'}" class="bg-blue"><small>[[ ${request.status} ]]</small></td>
- <td th:style="${request.type.value == 'delete'} ? 'color: red;'"><small>[[ ${request.type} ]]</small></td>
+ <td th:if="${request.status.value == 'blocked-initial'}" class="bg-gray"><small>[[ ${request.status} ]]</small></td>
+ <td th:style="${request.type.value == 'delete'} ? 'color: red;'">
+ <small>
+ [[ ${request.type} ]]
+ <th:block th:if="${request.submissionType.value != 'unknown'}">([[ ${request.submissionType} ]])</th:block>
+ </small>
+ </td>
+ <td th:insert="~{fragments :: accept-initial}" sec:authorize="hasRole('USER') or hasRole('ADMIN')" th:if="${postInitialSubmissionBlock}"></td>
<td th:if="not ${request.report}">[[ ${request.uuid} ]]</td>
<td th:if="${request.report}">
<a th:href="@{/report/{id}(id=${request.uuid})}" sec:authorize="hasRole('USER') or hasRole('ADMIN')">[[ ${request.uuid} ]]</a>
@@ -78,6 +86,7 @@
</main>
<footer th:replace="~{fragments.html :: footer}"></footer>
<script th:src="@{/scripts.js}"></script>
+ <script th:src="@{/webjars/htmx.org/dist/htmx.min.js}"></script>
<script>
window.addEventListener('load', () => {
let keyBindings = {