mirror of
https://github.com/pcvolkmer/grz-metadata-processor.git
synced 2025-07-03 04:42:54 +00:00
Initial commit
This commit is contained in:
@ -0,0 +1,11 @@
|
||||
package dev.pcvolkmer.onco.grzmetadataprocessor
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.runApplication
|
||||
|
||||
@SpringBootApplication
|
||||
class GrzMetadataProcessorApplication
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<GrzMetadataProcessorApplication>(*args)
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package dev.pcvolkmer.onco.grzmetadataprocessor.data
|
||||
|
||||
import org.springframework.data.annotation.Id
|
||||
import org.springframework.data.relational.core.mapping.Table
|
||||
import org.springframework.data.repository.CrudRepository
|
||||
|
||||
@Table("tbl_case")
|
||||
data class Case(
|
||||
@Id val id: Long? = null,
|
||||
var localCaseId: String = "",
|
||||
var coverageType: CoverageType = CoverageType.UNK,
|
||||
var submitterId: String = "",
|
||||
var genomicDataCenterId: String = "",
|
||||
var clinicalDataCenterId: String = "",
|
||||
var diseaseType: DiseaseType? = null,
|
||||
var genomicStudyType: GenomicStudyType? = null,
|
||||
var genomicStudySubtype: GenomicStudySubtype? = null,
|
||||
var labName: String = "",
|
||||
)
|
||||
|
||||
enum class CoverageType {
|
||||
GKV,
|
||||
PKV,
|
||||
BG,
|
||||
SEL,
|
||||
SOZ,
|
||||
GPV,
|
||||
PPV,
|
||||
BEI,
|
||||
SKT,
|
||||
UNK
|
||||
}
|
||||
|
||||
enum class DiseaseType(value: String) {
|
||||
ONCOLOGICAL("oncological"),
|
||||
RARE("rare"),
|
||||
HEREDITARY("hereditary"),
|
||||
}
|
||||
|
||||
enum class GenomicStudyType(value: String) {
|
||||
SINGLE("single"),
|
||||
DUO("duo"),
|
||||
TRIO("trio"),
|
||||
}
|
||||
|
||||
enum class GenomicStudySubtype(value: String) {
|
||||
TUMOR_ONLY("tumor-only"),
|
||||
TUMOR_GERMLINE("tumor+germline"),
|
||||
GERMLINE_ONLY("germline-only"),
|
||||
}
|
||||
|
||||
interface CaseRepository : CrudRepository<Case, Long>
|
@ -0,0 +1,35 @@
|
||||
package dev.pcvolkmer.onco.grzmetadataprocessor.data
|
||||
|
||||
import org.springframework.data.annotation.Id
|
||||
import org.springframework.data.relational.core.mapping.Table
|
||||
import org.springframework.data.repository.CrudRepository
|
||||
|
||||
@Table("tbl_donor")
|
||||
data class Donor(
|
||||
@Id val id: Long? = null,
|
||||
val caseId: Long?,
|
||||
val donorPseudonym: String = "",
|
||||
val gender: Gender? = null,
|
||||
val relation: Relation? = null,
|
||||
)
|
||||
|
||||
enum class Gender(val value: String) {
|
||||
MALE("male"),
|
||||
FEMALE("female"),
|
||||
OTHER("other"),
|
||||
UNKNOWN("unknown")
|
||||
}
|
||||
|
||||
enum class Relation(val value: String) {
|
||||
MOTHER("mother"),
|
||||
FATHER("father"),
|
||||
BROTHER("brother"),
|
||||
SISTER("sister"),
|
||||
CHILD("child"),
|
||||
INDEX("index"),
|
||||
OTHER("other")
|
||||
}
|
||||
|
||||
interface DonorRepository : CrudRepository<Donor, Long> {
|
||||
fun findByCaseId(caseId: Long): MutableList<Donor>
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
package dev.pcvolkmer.onco.grzmetadataprocessor.data
|
||||
|
||||
import org.apache.tomcat.util.buf.HexUtils
|
||||
import org.springframework.data.annotation.Id
|
||||
import org.springframework.data.relational.core.mapping.Table
|
||||
import org.springframework.data.repository.CrudRepository
|
||||
import java.nio.file.Path
|
||||
import java.security.DigestInputStream
|
||||
import java.security.MessageDigest
|
||||
import java.util.*
|
||||
import kotlin.io.path.fileSize
|
||||
import kotlin.io.path.inputStream
|
||||
|
||||
@Table("tbl_file")
|
||||
data class File(
|
||||
@Id val id: Long? = null,
|
||||
val labDataId: Long?,
|
||||
val filePath: String? = null,
|
||||
val fileType: FileType? = null,
|
||||
var fileChecksum: String? = null,
|
||||
var fileSizeInBytes: Long? = null,
|
||||
) {
|
||||
init {
|
||||
if (fileChecksum.isNullOrBlank()) {
|
||||
fileChecksum = calcFileChecksum()
|
||||
}
|
||||
|
||||
if (fileSizeInBytes?.or(0)!! < 1) {
|
||||
fileSizeInBytes = calcFileSize()
|
||||
}
|
||||
}
|
||||
|
||||
fun calcFileChecksum(): String {
|
||||
if (filePath == null) {
|
||||
return ""
|
||||
}
|
||||
val path = Path.of(filePath)
|
||||
val messageDigest = MessageDigest.getInstance("SHA-256")
|
||||
|
||||
val digestInputStream = DigestInputStream(path.inputStream(), messageDigest)
|
||||
digestInputStream.readAllBytes()
|
||||
return HexUtils.toHexString(messageDigest.digest())
|
||||
}
|
||||
|
||||
fun calcFileSize(): Long {
|
||||
if (filePath == null) {
|
||||
return 0
|
||||
}
|
||||
return Path.of(filePath).fileSize()
|
||||
}
|
||||
}
|
||||
|
||||
enum class FileType(val value: String) {
|
||||
BAM("bam"),
|
||||
VCF("vcf"),
|
||||
BED("bed"),
|
||||
FASTQ("fastq")
|
||||
}
|
||||
|
||||
interface FileRepository : CrudRepository<File, Long> {
|
||||
fun findByLabDataId(labDataId: Long): MutableList<File>
|
||||
fun findByLabDataIdIsNull(): List<File>
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
package dev.pcvolkmer.onco.grzmetadataprocessor.data
|
||||
|
||||
import org.springframework.data.annotation.Id
|
||||
import org.springframework.data.relational.core.mapping.Table
|
||||
import org.springframework.data.repository.CrudRepository
|
||||
|
||||
@Table("tbl_lab_data")
|
||||
data class LabData(
|
||||
@Id val id: Long? = null,
|
||||
val donorId: Long?,
|
||||
val einsendenummer: String = "",
|
||||
val labDataName: String = "",
|
||||
val sampleDate: String = "",
|
||||
val sampleConservation: SampleConservation? = null,
|
||||
val sequenceType: SequenceType? = null,
|
||||
val sequenceSubtype: SequenceSubtype? = null,
|
||||
val fragmentationMethod: FragmentationMethod? = null,
|
||||
val libraryType: LibraryType? = null,
|
||||
val libraryPrepKit: String = "",
|
||||
val libraryPrepKitManufacturer: String = "",
|
||||
val sequencerModel: String = "",
|
||||
val sequencerManufacturer: String = "",
|
||||
val kitName: String = "",
|
||||
val kitManufacturer: String = "",
|
||||
val enrichmentKit: String = "",
|
||||
val enrichmentKitManufacturer: EnrichmentKitManufacturer? = null,
|
||||
val sequencingLayout: SequencingLayout? = null,
|
||||
val tumorCellCount: Int = 0,
|
||||
val tumorCellCountMethod: TumorCellCountMethod? = null,
|
||||
val bioinformaticsPipelineName: String = "",
|
||||
val bioinformaticsPipelineVersion: String = "",
|
||||
val referenceGenome: ReferenceGenome? = null,
|
||||
)
|
||||
|
||||
enum class SampleConservation(val value: String) {
|
||||
FRESH_TISSUE("fresh-tissue"),
|
||||
CRYO_FROZEN("cryo-frozen"),
|
||||
FFPE("ffpe"),
|
||||
OTHER("other"),
|
||||
UNKNOWN("unknown")
|
||||
}
|
||||
|
||||
enum class SequenceType(val value: String) {
|
||||
DNA("dna"),
|
||||
RNA("rna")
|
||||
}
|
||||
|
||||
enum class SequenceSubtype(val value: String) {
|
||||
GERMLINE("germline"),
|
||||
SOMATIC("somatic"),
|
||||
OTHER("other"),
|
||||
UNKNOWN("unknown")
|
||||
}
|
||||
|
||||
enum class FragmentationMethod(val value: String) {
|
||||
SONICATION("sonication"),
|
||||
ENZYMATIC("enzymatic"),
|
||||
NONE("none"),
|
||||
OTHER("other"),
|
||||
UNKNOWN("unknown")
|
||||
}
|
||||
|
||||
enum class LibraryType(val value: String) {
|
||||
PANEL("panel"),
|
||||
PANEL_LR("panel_lr"),
|
||||
WES("wes"),
|
||||
WES_LR("wes_lr"),
|
||||
WGS("wgs"),
|
||||
WGS_LR("wgs_lr"),
|
||||
WXS("wxs"),
|
||||
WXS_LR("wxs_lr"),
|
||||
OTHER("other"),
|
||||
UNKNOWN("unknown")
|
||||
}
|
||||
|
||||
enum class EnrichmentKitManufacturer(val value: String) {
|
||||
ILLUMINA("Illumina"),
|
||||
AGILENT("Agilent"),
|
||||
TWIST("Twist"),
|
||||
NEB("NEB"),
|
||||
OTHER("other"),
|
||||
UNKNOWN("unknown"),
|
||||
NONE("none")
|
||||
}
|
||||
|
||||
enum class SequencingLayout(val value: String) {
|
||||
SINGLE_END("single-end"),
|
||||
PAIRED_END("paired-end"),
|
||||
REVERSE("reverse"),
|
||||
OTHER("other"),
|
||||
}
|
||||
|
||||
enum class TumorCellCountMethod(val value: String) {
|
||||
PATHOLOGY("pathology"),
|
||||
BIOINFORMATICS("bioinformatics"),
|
||||
OTHER("other"),
|
||||
UNKNOWN("unknown")
|
||||
}
|
||||
|
||||
enum class ReferenceGenome(val value: String) {
|
||||
GRCH37("GRCh37"),
|
||||
GRCH38("GRCh38"),
|
||||
}
|
||||
|
||||
interface LabDataRepository : CrudRepository<LabData, Long> {
|
||||
fun findByDonorId(donorId: Long): MutableList<LabData>
|
||||
fun countLabDataByDonorIdIsNull(): Long
|
||||
fun findByEinsendenummer(einsendenummer: String): LabData
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
package dev.pcvolkmer.onco.grzmetadataprocessor.exceptions
|
||||
|
||||
class NotFoundException: RuntimeException()
|
@ -0,0 +1,65 @@
|
||||
package dev.pcvolkmer.onco.grzmetadataprocessor.web
|
||||
|
||||
import dev.pcvolkmer.onco.grzmetadataprocessor.data.Case
|
||||
import dev.pcvolkmer.onco.grzmetadataprocessor.data.CaseRepository
|
||||
import dev.pcvolkmer.onco.grzmetadataprocessor.exceptions.NotFoundException
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.stereotype.Controller
|
||||
import org.springframework.ui.Model
|
||||
import org.springframework.web.bind.annotation.*
|
||||
|
||||
@Controller
|
||||
@RequestMapping(path = ["/cases"])
|
||||
class CaseController(
|
||||
private val repository: CaseRepository
|
||||
) {
|
||||
|
||||
@GetMapping
|
||||
fun getAllCase(model: Model): String {
|
||||
model.addAttribute("cases", repository.findAll())
|
||||
return "cases"
|
||||
}
|
||||
|
||||
@GetMapping(path = ["{caseId}"])
|
||||
fun getCase(@PathVariable caseId: Long, model: Model): String {
|
||||
repository.findById(caseId).ifPresentOrElse({
|
||||
model.addAttribute("case", it)
|
||||
}, {
|
||||
throw NotFoundException()
|
||||
})
|
||||
return "case"
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
fun postCase(model: Model): ResponseEntity<Unit> {
|
||||
val createdCase = repository.save(Case())
|
||||
model.addAttribute("cases", repository.findAll())
|
||||
return ResponseEntity
|
||||
.noContent()
|
||||
.headers { headers ->
|
||||
headers.add("HX-Redirect", "/cases/${createdCase.id}")
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
@PutMapping(path = ["{caseId}"], consumes = [MediaType.APPLICATION_FORM_URLENCODED_VALUE])
|
||||
fun putCase(@PathVariable caseId: Long, case: Case, model: Model): String {
|
||||
val savedCase = repository.save(case)
|
||||
model.addAttribute("case", savedCase)
|
||||
return "case"
|
||||
}
|
||||
|
||||
@DeleteMapping(path = ["{caseId}"])
|
||||
fun deleteCase(@PathVariable caseId: Long, model: Model): ResponseEntity<Unit> {
|
||||
repository.deleteById(caseId)
|
||||
model.addAttribute("cases", repository.findAll())
|
||||
return ResponseEntity
|
||||
.noContent()
|
||||
.headers { headers ->
|
||||
headers.add("HX-Redirect", "/")
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package dev.pcvolkmer.onco.grzmetadataprocessor.web
|
||||
|
||||
import dev.pcvolkmer.onco.grzmetadataprocessor.exceptions.NotFoundException
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler
|
||||
|
||||
@ControllerAdvice
|
||||
class ControllerAdvices {
|
||||
|
||||
@ExceptionHandler(NotFoundException::class)
|
||||
fun handleNotFoundException(e: NotFoundException): String {
|
||||
return "error/notfound"
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package dev.pcvolkmer.onco.grzmetadataprocessor.web
|
||||
|
||||
import dev.pcvolkmer.onco.grzmetadataprocessor.data.Donor
|
||||
import dev.pcvolkmer.onco.grzmetadataprocessor.data.DonorRepository
|
||||
import org.springframework.stereotype.Controller
|
||||
import org.springframework.ui.Model
|
||||
import org.springframework.web.bind.annotation.*
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/cases/{caseId}/donors")
|
||||
class DonorController(
|
||||
private val repository: DonorRepository
|
||||
) {
|
||||
|
||||
@GetMapping
|
||||
fun getAllDonors(@PathVariable caseId: Long, model: Model): String {
|
||||
model.addAttribute("caseId", caseId)
|
||||
model.addAttribute("donors", repository.findByCaseId(caseId))
|
||||
return "donors"
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
fun postDonor(@PathVariable caseId: Long, model: Model): String {
|
||||
repository.save(Donor(caseId = caseId))
|
||||
model.addAttribute("caseId", caseId)
|
||||
model.addAttribute("donors", repository.findByCaseId(caseId))
|
||||
return "donors"
|
||||
}
|
||||
|
||||
@PutMapping(path = ["{donorId}"])
|
||||
fun postDonor(@PathVariable caseId: Long, @PathVariable donorId: Long, donor: Donor, model: Model): String {
|
||||
repository.save(donor)
|
||||
model.addAttribute("caseId", caseId)
|
||||
model.addAttribute("donors", repository.findByCaseId(caseId))
|
||||
return "donors"
|
||||
}
|
||||
|
||||
@DeleteMapping(path = ["{donorId}"])
|
||||
fun deleteDonor(@PathVariable caseId: Long, @PathVariable donorId: Long, model: Model): String {
|
||||
repository.deleteById(donorId)
|
||||
model.addAttribute("donors", repository.findByCaseId(caseId))
|
||||
return "donors"
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
package dev.pcvolkmer.onco.grzmetadataprocessor.web
|
||||
|
||||
import dev.pcvolkmer.onco.grzmetadataprocessor.data.File
|
||||
import dev.pcvolkmer.onco.grzmetadataprocessor.data.FileRepository
|
||||
import dev.pcvolkmer.onco.grzmetadataprocessor.data.LabDataRepository
|
||||
import org.springframework.stereotype.Controller
|
||||
import org.springframework.ui.Model
|
||||
import org.springframework.web.bind.annotation.*
|
||||
|
||||
@Controller
|
||||
@RequestMapping
|
||||
class FileController(
|
||||
private val fileRepository: FileRepository,
|
||||
private val labDataRepository: LabDataRepository
|
||||
) {
|
||||
|
||||
@GetMapping(path = ["/labdatas/{labDataId}/files"])
|
||||
fun getAllFiles(@PathVariable labDataId: Long, model: Model): String {
|
||||
model.addAttribute("labDataId", labDataId)
|
||||
model.addAttribute("files", fileRepository.findByLabDataId(labDataId))
|
||||
return "files"
|
||||
}
|
||||
|
||||
@GetMapping(path = ["/files/unused"])
|
||||
fun getAllUnusedFiles(model: Model): String {
|
||||
model.addAttribute("labDatas", labDataRepository.findAll().filterNot { it.einsendenummer.isBlank() })
|
||||
model.addAttribute("files", fileRepository.findByLabDataIdIsNull())
|
||||
return "unusedfiles"
|
||||
}
|
||||
|
||||
@PostMapping(path = ["/labdatas/{labDataId}/files"])
|
||||
fun postFile(@PathVariable labDataId: Long, model: Model): String {
|
||||
fileRepository.save(File(labDataId = labDataId))
|
||||
model.addAttribute("labDataId", labDataId)
|
||||
model.addAttribute("files", fileRepository.findByLabDataId(labDataId))
|
||||
return "files"
|
||||
}
|
||||
|
||||
@PutMapping(path = ["/labdatas/{labDataId}/files/{fileId}"])
|
||||
fun putFile(@PathVariable labDataId: Long, @PathVariable fileId: Long, file: File, model: Model): String {
|
||||
fileRepository.save(file)
|
||||
model.addAttribute("labDataId", labDataId)
|
||||
model.addAttribute("files", fileRepository.findByLabDataId(labDataId))
|
||||
return "files"
|
||||
}
|
||||
|
||||
@PutMapping(path = ["/files/unused/{fileId}"])
|
||||
fun putUnusedFile(@PathVariable fileId: Long, file: File, model: Model): String {
|
||||
fileRepository.save(file)
|
||||
model.addAttribute("files", fileRepository.findByLabDataIdIsNull())
|
||||
return "unusedfiles"
|
||||
}
|
||||
|
||||
@DeleteMapping(path = ["/labdatas/{labDataId}/files/{fileId}"])
|
||||
fun deleteFile(@PathVariable fileId: Long, @PathVariable labDataId: Long, model: Model): String {
|
||||
fileRepository.deleteById(fileId)
|
||||
model.addAttribute("fileId", fileId)
|
||||
model.addAttribute("files", fileRepository.findByLabDataId(labDataId))
|
||||
return "files"
|
||||
}
|
||||
|
||||
@DeleteMapping(path = ["/files/unused/{fileId}"])
|
||||
fun deleteUnusedFile(@PathVariable fileId: Long, model: Model): String {
|
||||
fileRepository.deleteById(fileId)
|
||||
model.addAttribute("files", fileRepository.findByLabDataIdIsNull())
|
||||
return "unusedfiles"
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package dev.pcvolkmer.onco.grzmetadataprocessor.web
|
||||
|
||||
import dev.pcvolkmer.onco.grzmetadataprocessor.data.LabData
|
||||
import dev.pcvolkmer.onco.grzmetadataprocessor.data.LabDataRepository
|
||||
import org.springframework.stereotype.Controller
|
||||
import org.springframework.ui.Model
|
||||
import org.springframework.web.bind.annotation.*
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/donors/{donorId}/labdatas")
|
||||
class LabDataController(
|
||||
private val repository: LabDataRepository
|
||||
) {
|
||||
|
||||
@GetMapping
|
||||
fun getAllLabData(@PathVariable donorId: Long, model: Model): String {
|
||||
model.addAttribute("labdatas", repository.findByDonorId(donorId))
|
||||
return "labdatas"
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
fun postLabData(@PathVariable donorId: Long, model: Model): String {
|
||||
repository.save(LabData(donorId = donorId))
|
||||
model.addAttribute("labdatas", repository.findByDonorId(donorId))
|
||||
return "labdatas"
|
||||
}
|
||||
|
||||
@PutMapping(path = ["{labdataId}"])
|
||||
fun putLabData(@PathVariable donorId: Long, labData: LabData, model: Model): String {
|
||||
repository.save(labData)
|
||||
model.addAttribute("labdatas", repository.findByDonorId(donorId))
|
||||
return "labdatas"
|
||||
}
|
||||
|
||||
@DeleteMapping(path = ["{labdataId}"])
|
||||
fun deleteLabData(@PathVariable donorId: Long, @PathVariable labdataId: Long, model: Model): String {
|
||||
repository.deleteById(labdataId)
|
||||
model.addAttribute("labdatas", repository.findByDonorId(donorId))
|
||||
return "labdatas"
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package dev.pcvolkmer.onco.grzmetadataprocessor.web
|
||||
|
||||
import dev.pcvolkmer.onco.grzmetadataprocessor.data.CaseRepository
|
||||
import dev.pcvolkmer.onco.grzmetadataprocessor.data.FileRepository
|
||||
import dev.pcvolkmer.onco.grzmetadataprocessor.data.LabDataRepository
|
||||
import org.springframework.stereotype.Controller
|
||||
import org.springframework.ui.Model
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
|
||||
@Controller
|
||||
class MenuController(
|
||||
private val caseRepository: CaseRepository,
|
||||
private val labDataRepository: LabDataRepository,
|
||||
private val fileRepository: FileRepository
|
||||
) {
|
||||
|
||||
@GetMapping("/cases/menu")
|
||||
fun getCaseMenus(model: Model): String {
|
||||
model.addAttribute("cases", caseRepository.findAll().count())
|
||||
model.addAttribute("unusedLabData", labDataRepository.countLabDataByDonorIdIsNull())
|
||||
model.addAttribute("unusedFiles", fileRepository.findByLabDataIdIsNull().size)
|
||||
return "casemenu"
|
||||
}
|
||||
|
||||
}
|
4
src/main/resources/application-dev.yml
Normal file
4
src/main/resources/application-dev.yml
Normal file
@ -0,0 +1,4 @@
|
||||
spring:
|
||||
docker:
|
||||
compose:
|
||||
file: compose-dev.yml
|
5
src/main/resources/application.yml
Normal file
5
src/main/resources/application.yml
Normal file
@ -0,0 +1,5 @@
|
||||
spring:
|
||||
application:
|
||||
name: grz-metadata-processor
|
||||
flyway:
|
||||
locations: "classpath:db/migrations/{vendor}"
|
61
src/main/resources/db/migrations/mariadb/V0_1_0__Init.sql
Normal file
61
src/main/resources/db/migrations/mariadb/V0_1_0__Init.sql
Normal file
@ -0,0 +1,61 @@
|
||||
CREATE TABLE IF NOT EXISTS tbl_case
|
||||
(
|
||||
id int auto_increment primary key,
|
||||
local_case_id varchar(255) not null,
|
||||
coverage_type varchar(255) not null,
|
||||
submitter_id varchar(16) not null,
|
||||
genomic_data_center_id varchar(16) not null,
|
||||
clinical_data_center_id varchar(16) not null,
|
||||
disease_type varchar(24),
|
||||
genomic_study_type varchar(24),
|
||||
genomic_study_subtype varchar(24),
|
||||
lab_name varchar(255) not null
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tbl_donor
|
||||
(
|
||||
id int auto_increment primary key,
|
||||
case_id int,
|
||||
donor_pseudonym varchar(255) not null,
|
||||
gender varchar(16),
|
||||
relation varchar(16)
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tbl_lab_data
|
||||
(
|
||||
id int auto_increment primary key,
|
||||
donor_id int,
|
||||
einsendenummer varchar(255) unique,
|
||||
lab_data_name varchar(255) not null,
|
||||
sample_date varchar(16) not null,
|
||||
sample_conservation varchar(255),
|
||||
sequence_type varchar(255),
|
||||
sequence_subtype varchar(255),
|
||||
fragmentation_method varchar(255),
|
||||
library_type varchar(255),
|
||||
library_prep_kit varchar(255) not null,
|
||||
library_prep_kit_manufacturer varchar(255) not null,
|
||||
sequencer_model varchar(255) not null,
|
||||
sequencer_manufacturer varchar(255) not null,
|
||||
kit_name varchar(255) not null,
|
||||
kit_manufacturer varchar(255) not null,
|
||||
enrichment_kit varchar(255) not null,
|
||||
enrichment_kit_manufacturer varchar(255),
|
||||
sequencing_layout varchar(255),
|
||||
tumor_cell_count int,
|
||||
tumor_cell_count_method varchar(255),
|
||||
bioinformatics_pipeline_name varchar(255) not null,
|
||||
bioinformatics_pipeline_version varchar(255) not null,
|
||||
reference_genome varchar(8)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tbl_file
|
||||
(
|
||||
id int auto_increment primary key,
|
||||
lab_data_id int,
|
||||
file_path varchar(255) unique,
|
||||
file_type varchar(8),
|
||||
file_checksum varchar(64) unique,
|
||||
file_size_in_bytes int
|
||||
);
|
118
src/main/resources/templates/case.html
Normal file
118
src/main/resources/templates/case.html
Normal file
@ -0,0 +1,118 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" xmlns="http://www.w3.org/1999/html" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
<link th:href="@{/main.css}" rel="stylesheet" />
|
||||
<base href="@{/}">
|
||||
</head>
|
||||
<body>
|
||||
<aside th:hx-get="@{/cases/menu}" hx-trigger="load"></aside>
|
||||
|
||||
<main>
|
||||
<div>
|
||||
<h1>Fall <tt th:text="${case.localCaseId}"></tt></h1>
|
||||
|
||||
<form class="case">
|
||||
<input type="hidden" name="id" th:value="${case.id}" />
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Lokale Fall-ID
|
||||
<input type="text" name="localCaseId" th:value="${case.localCaseId}" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Versicherungsart
|
||||
<select name="coverageType" th:field="${case.coverageType}">
|
||||
<option th:value="'GKV'">GKV</option>
|
||||
<option th:value="'PKV'">PKV</option>
|
||||
<option th:value="'BG'">BG</option>
|
||||
<option th:value="'SEL'">SEL</option>
|
||||
<option th:value="'SOZ'">SOZ</option>
|
||||
<option th:value="'GPV'">GPV</option>
|
||||
<option th:value="'PPV'">PPV</option>
|
||||
<option th:value="'BEI'">BEI</option>
|
||||
<option th:value="'SKT'">SKT</option>
|
||||
<option th:value="'UNK'">UNK</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Submitter-ID
|
||||
<input type="text" name="submitterId" th:value="${case.submitterId}" pattern="[0-9]{9}" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
GRZ-ID
|
||||
<input type="text" name="genomicDataCenterId" th:value="${case.genomicDataCenterId}" pattern="^(GRZ)[A-Z0-9]{3}[0-9]{3}$" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
KDK-ID
|
||||
<input type="text" name="clinicalDataCenterId" th:value="${case.clinicalDataCenterId}" pattern="^(GRZ)[A-Z0-9]{3}[0-9]{3}$" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Erkrankungsart
|
||||
<select name="diseaseType" th:field="${case.diseaseType}">
|
||||
<option th:value="ONCOLOGICAL">oncological</option>
|
||||
<option th:value="RARE">rare</option>
|
||||
<option th:value="HEREDITARY">hereditary</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Genomische Studienart
|
||||
<select name="genomicStudyType" th:field="${case.genomicStudyType}">
|
||||
<option th:value="SINGLE">single</option>
|
||||
<option th:value="DUO">duo</option>
|
||||
<option th:value="TRIO">trio</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Genomische Unterstudienart
|
||||
<select name="genomicStudySubtype" th:field="${case.genomicStudySubtype}">
|
||||
<option th:value="TUMOR_ONLY">tumor-only</option>
|
||||
<option th:value="TUMOR_GERMLINE">tumor+germline</option>
|
||||
<option th:value="GERMLINE_ONLY">germline-only</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Laborname
|
||||
<input name="labName" th:value="${case.labName}" type="text" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="save" th:hx-put="@{/cases/{caseId}(caseId=${case.id})}" hx-target="body">Änderungen übernehmen</button>
|
||||
<button class="new" th:hx-post="@{/cases}">Neu</button>
|
||||
<button class="delete" th:hx-delete="@{/cases/{caseId}(caseId=${case.id})}">Löschen</button>
|
||||
</div>
|
||||
|
||||
<div th:hx-get="@{/cases/{caseId}/donors(caseId=${case.id})}" hx-trigger="load"></div>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script th:src="@{/main.js}"></script>
|
||||
</body>
|
||||
</html>
|
25
src/main/resources/templates/casemenu.html
Normal file
25
src/main/resources/templates/casemenu.html
Normal file
@ -0,0 +1,25 @@
|
||||
<div class="title">GRZ-Metadaten</div>
|
||||
|
||||
<nav>
|
||||
<ul>
|
||||
<li>
|
||||
<a th:href="@{/}">
|
||||
<span>Fallübersicht</span>
|
||||
<span class="counter">[[ ${cases} ]]</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a th:href="@{/}">
|
||||
<span>Ungenutzte Proben</span>
|
||||
<span th:if="${unusedLabData > 0}" class="counter">[[ ${unusedLabData} ]]</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a th:href="@{/files/unused}">
|
||||
<span>Ungenutzte Dateien</span>
|
||||
<span th:if="${unusedFiles}" class="counter">[[ ${unusedFiles} ]]</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
23
src/main/resources/templates/cases.html
Normal file
23
src/main/resources/templates/cases.html
Normal file
@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" xmlns="http://www.w3.org/1999/html" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<h1>
|
||||
[[ ${cases.size()} ]] Fälle
|
||||
</h1>
|
||||
<form class="case">
|
||||
<ul class="case-list">
|
||||
<li th:each="case : ${cases}">
|
||||
<a th:href="@{/cases/{caseId}(caseId=${case.id})}">#[[ ${case.id} ]] - Fallnummer: [[ ${case.localCaseId} ]]</a>
|
||||
<button class="delete left" th:hx-delete="@{/cases/{caseId}(caseId=${case.id})}" hx-target="#cases">Eintrag löschen</button>
|
||||
</li>
|
||||
</ul>
|
||||
<button class="new" th:hx-post="@{/cases}">Neuen Fall anlegen</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
72
src/main/resources/templates/donors.html
Normal file
72
src/main/resources/templates/donors.html
Normal file
@ -0,0 +1,72 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" xmlns="http://www.w3.org/1999/html" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
</head>
|
||||
<body>
|
||||
<div th:id="'cases_' + ${caseId} + '_donors'">
|
||||
|
||||
<div th:if="${donors.isEmpty()}">
|
||||
<h2>Probenspender</h2>
|
||||
<form class="donor" th:hx-post="@{/cases/{caseId}/donors(caseId=${caseId})}" th:hx-target="'#cases_' + ${caseId} + '_donors'">
|
||||
<div>Keine Probenspender gefunden</div>
|
||||
<button>Neuen Probenspender hinzufügen</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Probenspender -->
|
||||
<div th:each="donor : ${donors}">
|
||||
<h2>Probenspender</h2>
|
||||
|
||||
<form class="donor" th:id="'cases_' + ${caseId} + '_donors'">
|
||||
<input type="hidden" name="id" th:value="${donor.id}">
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Spender-Pseudonym
|
||||
<input type="text" name="donorPseudonym" th:value="${donor.donorPseudonym}" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Gender
|
||||
<select name="gender">
|
||||
<option th:selected="${donor.gender != null and donor.gender.value == 'male'}" value="MALE">male</option>
|
||||
<option th:selected="${donor.gender != null and donor.gender.value == 'female'}" value="FEMALE">female</option>
|
||||
<option th:selected="${donor.gender != null and donor.gender.value == 'other'}" value="OTHER">other</option>
|
||||
<option th:selected="${donor.gender != null and donor.gender.value == 'unknown'}" value="UNKNOWN">unknown</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Verwandschaft
|
||||
<select name="relation">
|
||||
<option th:selected="${donor.relation != null and donor.relation.value == 'mother'}" value="MOTHER">mother</option>
|
||||
<option th:selected="${donor.relation != null and donor.relation.value == 'father'}" value="FATHER">father</option>
|
||||
<option th:selected="${donor.relation != null and donor.relation.value == 'brother'}" value="BROTHER">brother</option>
|
||||
<option th:selected="${donor.relation != null and donor.relation.value == 'sister'}" value="SISTER">sister</option>
|
||||
<option th:selected="${donor.relation != null and donor.relation.value == 'child'}" value="CHILD">child</option>
|
||||
<option th:selected="${donor.relation != null and donor.relation.value == 'index'}" value="INDEX">index</option>
|
||||
<option th:selected="${donor.relation != null and donor.relation.value == 'other'}" value="OTHER">other</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="save" th:hx-put="@{/cases/{caseId}/donors/{donorId}(caseId=${caseId},donorId=${donor.id})}" th:hx-target="'#cases_' + ${caseId} + '_donors'">Änderungen übernehmen</button>
|
||||
<button class="new" th:hx-post="@{/cases/{caseId}/donors(caseId=${caseId})}" th:hx-target="'#cases_' + ${caseId} + '_donors'">Neu</button>
|
||||
<button class="delete" th:hx-delete="@{/cases/{caseId}/donors/{donorId}(caseId=${caseId},donorId=${donor.id})}" th:hx-target="'#cases_' + ${caseId} + '_donors'">Löschen</button>
|
||||
</div>
|
||||
|
||||
<div th:hx-get="@{/donors/{donorId}/labdatas(donorId=${donor.id})}" hx-trigger="load"></div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
10
src/main/resources/templates/error/notfound.html
Normal file
10
src/main/resources/templates/error/notfound.html
Normal file
@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" xmlns="http://www.w3.org/1999/html" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Fehler</title>
|
||||
</head>
|
||||
<body>
|
||||
<a th:href="@{/}">Zurück</a>
|
||||
</body>
|
||||
</html>
|
70
src/main/resources/templates/files.html
Normal file
70
src/main/resources/templates/files.html
Normal file
@ -0,0 +1,70 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" xmlns="http://www.w3.org/1999/html" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div th:id="'labdatas_' + ${labDataId} + '_files'">
|
||||
<div th:if="${files.isEmpty()}">
|
||||
<h4>Datei</h4>
|
||||
<form class="file" th:hx-post="@{/labdatas/{labDataId}/files(labDataId=${labDataId})}" th:hx-target="'#labdatas_' + ${labDataId} + '_files'">
|
||||
<div>Keine Datei gefunden</div>
|
||||
<button>Neuen Datei hinzufügen</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div th:each="file : ${files}">
|
||||
<!-- File -->
|
||||
<h4>Datei</h4>
|
||||
<form class="file">
|
||||
<input type="hidden" name="id" th:value="${file.id}" />
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Datei
|
||||
<input type="text" name="filePath" th:value="${file.filePath}" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Dateityp
|
||||
<select name="fileType">
|
||||
<option th:selected="${file.fileType != null and file.fileType.value == 'bam'}">BAM</option>
|
||||
<option th:selected="${file.fileType != null and file.fileType.value == 'vcf'}">Vcf</option>
|
||||
<option th:selected="${file.fileType != null and file.fileType.value == 'bed'}">BED</option>
|
||||
<option th:selected="${file.fileType != null and file.fileType.value == 'fastq'}">FASTQ</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Prüfsumme - SHA256
|
||||
<input type="text" pattern="[A-Fa-f0-9]{64}" name="fileChecksum" th:value="${file.fileChecksum} " />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Dateigröße in Bytes
|
||||
<input type="number" min="0" name="fileSizeInBytes" th:value="${file.fileSizeInBytes} " />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="save" th:hx-put="@{/labdatas/{labDataId}/files/{fileId}(labDataId=${labDataId},fileId=${file.id})}" th:hx-target="'#labdatas_' + ${labDataId} + '_files'">Änderungen übernehmen</button>
|
||||
<button class="new" th:hx-post="@{/labdatas/{labDataId}/files(labDataId=${labDataId})}" th:hx-target="'#labdatas_' + ${labDataId} + '_files'">Neu</button>
|
||||
<button class="delete" th:hx-delete="@{/labdatas/{labDataId}/files/{fileId}(labDataId=${labDataId},fileId=${file.id})}" th:hx-target="'#labdatas_' + ${labDataId} + '_files'">Löschen</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script th:src="@{/main.js}"></script>
|
||||
</body>
|
||||
</html>
|
18
src/main/resources/templates/index.html
Normal file
18
src/main/resources/templates/index.html
Normal file
@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" xmlns="http://www.w3.org/1999/html" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
<link th:href="@{/main.css}" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<aside th:hx-get="@{/cases/menu}" hx-trigger="load"></aside>
|
||||
|
||||
<main>
|
||||
<div id="cases" th:hx-get="@{/cases}" hx-trigger="load"></div>
|
||||
</main>
|
||||
|
||||
<script th:src="@{/main.js}"></script>
|
||||
</body>
|
||||
</html>
|
232
src/main/resources/templates/labdatas.html
Normal file
232
src/main/resources/templates/labdatas.html
Normal file
@ -0,0 +1,232 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" xmlns="http://www.w3.org/1999/html" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div th:id="'donors_' + ${donorId} + '_labdatas'">
|
||||
<div th:if="${labdatas.isEmpty()}">
|
||||
<h3>Probe</h3>
|
||||
<form class="lab-data" th:hx-post="@{/donors/{donorId}/labdatas(donorId=${donorId})}" th:hx-target="'#donors_' + ${donorId} + '_labdatas'">
|
||||
<div>Keine Probe gefunden</div>
|
||||
<button>Neuen Probe hinzufügen</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div th:each="labdata : ${labdatas}">
|
||||
<h3>Probe/Sequenzierung</h3>
|
||||
|
||||
<form class="lab-data">
|
||||
<input type="hidden" name="id" th:value="${labdata.id}" />
|
||||
|
||||
<div>
|
||||
<label class="optional">
|
||||
Einsendenummer (Optionale Angabe)
|
||||
<input type="text" name="einsendenummer" th:value="${labdata.einsendenummer}" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
LabData Name
|
||||
<input type="text" name="labDataName" th:value="${labdata.labDataName}"/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Datum der Probe
|
||||
<input type="date" name="sampleDate" th:value="${labdata.sampleDate}" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Konservierung der Probe
|
||||
<select name="sampleConservation">
|
||||
<option th:selected="${labdata.sampleConservation != null and labdata.sampleConservation.value == 'FRESH_TISSUE'}" value="FRESH_TISSUE">fresh-tissue</option>
|
||||
<option th:selected="${labdata.sampleConservation != null and labdata.sampleConservation.value == 'CRYO_FROZEN'}" value="CRYO_FROZEN">cryo-frozen</option>
|
||||
<option th:selected="${labdata.sampleConservation != null and labdata.sampleConservation.value == 'FFPE'}" value="FFPE">ffpe</option>
|
||||
<option th:selected="${labdata.sampleConservation != null and labdata.sampleConservation.value == 'OTHER'}" value="OTHER">other</option>
|
||||
<option th:selected="${labdata.sampleConservation != null and labdata.sampleConservation.value == 'UNKNOWN'}" value="UNKNOWN">unknown</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Sequenzierungsart
|
||||
<select name="sequenceType">
|
||||
<option th:selected="${labdata.sequenceType != null and labdata.sequenceType.value == 'DNA'}" value="DNA">dna</option>
|
||||
<option th:selected="${labdata.sequenceType != null and labdata.sequenceType.value == 'RNA'}" value="RNA">rna</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Sequenzierungsunterart
|
||||
<select name="sequenceSubtype">
|
||||
<option th:selected="${labdata.sequenceSubtype != null and labdata.sequenceSubtype.value == 'GERMLINE'}" value="GERMLINE">germline</option>
|
||||
<option th:selected="${labdata.sequenceSubtype != null and labdata.sequenceSubtype.value == 'SOMATIC'}" value="SOMATIC">somatic</option>
|
||||
<option th:selected="${labdata.sequenceSubtype != null and labdata.sequenceSubtype.value == 'OTHER'}" value="OTHER">other</option>
|
||||
<option th:selected="${labdata.sequenceSubtype != null and labdata.sequenceSubtype.value == 'UNKNOWN'}" value="UNKNOWN">unknown</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Fragmentierungsart
|
||||
<select name="fragmentationMethod">
|
||||
<option th:selected="${labdata.fragmentationMethod != null and labdata.fragmentationMethod.value == 'SONICATION'}" value="SONICATION">sonication</option>
|
||||
<option th:selected="${labdata.fragmentationMethod != null and labdata.fragmentationMethod.value == 'ENZYMATIC'}" value="ENZYMATIC">enzymatic</option>
|
||||
<option th:selected="${labdata.fragmentationMethod != null and labdata.fragmentationMethod.value == 'NONE'}" value="NONE">none</option>
|
||||
<option th:selected="${labdata.fragmentationMethod != null and labdata.fragmentationMethod.value == 'OTHER'}" value="OTHER">other</option>
|
||||
<option th:selected="${labdata.fragmentationMethod != null and labdata.fragmentationMethod.value == 'UNKNOWN'}" value="UNKNOWN">unknown</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Library Type
|
||||
<select name="libraryType">
|
||||
<option th:selected="${labdata.libraryType != null and labdata.libraryType.value == 'PANEL'}" value="PANEL">panel</option>
|
||||
<option th:selected="${labdata.libraryType != null and labdata.libraryType.value == 'PANEL_LR'}" value="PANEL_LR">panel_lr</option>
|
||||
<option th:selected="${labdata.libraryType != null and labdata.libraryType.value == 'WES'}" value="WES">wes</option>
|
||||
<option th:selected="${labdata.libraryType != null and labdata.libraryType.value == 'WES_LR'}" value="WES_LR">wes_lr</option>
|
||||
<option th:selected="${labdata.libraryType != null and labdata.libraryType.value == 'WGS'}" value="WGS">wgs</option>
|
||||
<option th:selected="${labdata.libraryType != null and labdata.libraryType.value == 'WGS_LR'}" value="WGS_LR">wgs_lr</option>
|
||||
<option th:selected="${labdata.libraryType != null and labdata.libraryType.value == 'WXS'}" value="WXS">wxs</option>
|
||||
<option th:selected="${labdata.libraryType != null and labdata.libraryType.value == 'WXS_LR'}" value="WXS_LR">wxs_lr</option>
|
||||
<option th:selected="${labdata.libraryType != null and labdata.libraryType.value == 'OTHER'}" value="OTHER">other</option>
|
||||
<option th:selected="${labdata.libraryType != null and labdata.libraryType.value == 'UNKNOWN'}" value="UNKNOWN">unknown</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Preparation Kit - Name
|
||||
<input type="text" name="libraryPrepKit" th:value="${labdata.libraryPrepKit}" />
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Hersteller
|
||||
<input type="text" name="libraryPrepKitManufacturer" th:value="${labdata.libraryPrepKitManufacturer}" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Sequencer - Modell
|
||||
<input type="text" name="sequencerModel" th:value="${labdata.sequencerModel}" />
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Hersteller
|
||||
<input type="text" name="sequencerManufacturer" th:value="${labdata.sequencerManufacturer}" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Kit - Name/Version
|
||||
<input type="text" name="kitName" th:value="${labdata.kitName}" />
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Hersteller
|
||||
<input type="text" name="kitManufacturer" th:value="${labdata.kitManufacturer}" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Enrichment Kit - Name und Version
|
||||
<input type="text" name="enrichmentKit" th:value="${labdata.enrichmentKit}" />
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Hersteller
|
||||
<select name="enrichmentKitManufacturer">
|
||||
<option th:selected="${labdata.enrichmentKitManufacturer != null and labdata.enrichmentKitManufacturer.value == 'ILLUMINA'}" value="ILLUMINA">Illumina</option>
|
||||
<option th:selected="${labdata.enrichmentKitManufacturer != null and labdata.enrichmentKitManufacturer.value == 'AGILENT'}" value="AGILENT">Agilent</option>
|
||||
<option th:selected="${labdata.enrichmentKitManufacturer != null and labdata.enrichmentKitManufacturer.value == 'TIST'}" value="TIST">Twist</option>
|
||||
<option th:selected="${labdata.enrichmentKitManufacturer != null and labdata.enrichmentKitManufacturer.value == 'NEB'}" value="NEB">NEB</option>
|
||||
<option th:selected="${labdata.enrichmentKitManufacturer != null and labdata.enrichmentKitManufacturer.value == 'OTHER'}" value="OTHER">other</option>
|
||||
<option th:selected="${labdata.enrichmentKitManufacturer != null and labdata.enrichmentKitManufacturer.value == 'UNKNOWN'}" value="UNKNOWN">unknown</option>
|
||||
<option th:selected="${labdata.enrichmentKitManufacturer != null and labdata.enrichmentKitManufacturer.value == 'NONE'}" value="NONE">none</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Sequencing Layout
|
||||
<select name="sequencingLayout">
|
||||
<option th:selected="${labdata.sequencingLayout != null and labdata.sequencingLayout.value == 'SINGLE_END'}" value="SINGLE_END">single-end</option>
|
||||
<option th:selected="${labdata.sequencingLayout != null and labdata.sequencingLayout.value == 'PAIRED_END'}" value="PAIRED_END">paired-end</option>
|
||||
<option th:selected="${labdata.sequencingLayout != null and labdata.sequencingLayout.value == 'REVERSE'}" value="REVERSE">reverse</option>
|
||||
<option th:selected="${labdata.sequencingLayout != null and labdata.sequencingLayout.value == 'OTHER'}" value="OTHER">other</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Tumorzellgehalt in %
|
||||
<input type="number" name="tumorCellCount" th:value="${labdata.tumorCellCount}" />
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Methode
|
||||
<select name="tumorCellCountMethod">
|
||||
<option th:selected="${labdata.tumorCellCountMethod != null and labdata.tumorCellCountMethod.value == 'PATHOLOGY'}" value="PATHOLOGY">pathology</option>
|
||||
<option th:selected="${labdata.tumorCellCountMethod != null and labdata.tumorCellCountMethod.value == 'BIOINFORMATICS'}" value="BIOINFORMATICS">bioinformatics</option>
|
||||
<option th:selected="${labdata.tumorCellCountMethod != null and labdata.tumorCellCountMethod.value == 'OTHER'}" value="OTHER">other</option>
|
||||
<option th:selected="${labdata.tumorCellCountMethod != null and labdata.tumorCellCountMethod.value == 'UNKNOWN'}" value="UNKNOWN">unknown</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Bioinformatik-Pipeline
|
||||
<input type="text" name="bioinformaticsPipelineName" th:value="${labdata.bioinformaticsPipelineName}" />
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Version
|
||||
<input type="text" name="bioinformaticsPipelineVersion" th:value="${labdata.bioinformaticsPipelineVersion}" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Referenz-Genom
|
||||
<select name="referenceGenome">
|
||||
<option th:selected="${labdata.referenceGenome != null and labdata.referenceGenome.value == 'GRCH37'}" value="GRCH37">GRCh37</option>
|
||||
<option th:selected="${labdata.referenceGenome != null and labdata.referenceGenome.value == 'GRCH38'}" value="GRCH38">GRCh38</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="save" th:hx-put="@{/donors/{donorId}/labdatas/{labdataId}(donorId=${donorId},labdataId=${labdata.id})}" th:hx-target="'#donors_' + ${donorId} + '_labdatas'">Änderungen übernehmen</button>
|
||||
<button class="new" th:hx-post="@{/donors/{donorId}/labdatas(donorId=${donorId})}" th:hx-target="'#donors_' + ${donorId} + '_labdatas'">Neu</button>
|
||||
<button class="delete" th:hx-delete="@{/donors/{donorId}/labdatas/{labdataId}(donorId=${donorId},labdataId=${labdata.id})}" th:hx-target="'#donors_' + ${donorId} + '_labdatas'">Löschen</button>
|
||||
</div>
|
||||
|
||||
<div th:hx-get="@{/labdatas/{labdataId}/files(labdataId=${labdata.id})}" hx-trigger="load"></div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
83
src/main/resources/templates/unusedfiles.html
Normal file
83
src/main/resources/templates/unusedfiles.html
Normal file
@ -0,0 +1,83 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" xmlns="http://www.w3.org/1999/html" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
<link th:href="@{/main.css}" rel="stylesheet" />
|
||||
<base href="@{/}">
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<aside th:hx-get="@{/cases/menu}" hx-trigger="load"></aside>
|
||||
|
||||
<main>
|
||||
<div id="unused_files">
|
||||
|
||||
<div th:if="${files.isEmpty()}">
|
||||
<h4>Datei</h4>
|
||||
<form class="file">
|
||||
<div>Keine ungenutzte Datei gefunden</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div th:each="file : ${files}">
|
||||
<!-- File -->
|
||||
<h4>Datei</h4>
|
||||
<form class="file">
|
||||
<input type="hidden" name="id" th:value="${file.id}" />
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Datei
|
||||
<input type="text" name="filePath" th:value="${file.filePath}" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="optional">
|
||||
Zugehörige Probe (Einsendenummer)
|
||||
<select name="labDataId">
|
||||
<option>Keine Zuordnung</option>
|
||||
<option th:each="labData : ${labDatas}" th:selected="${file.labDataId == labData.id}" th:value="${labData.id}" th:text="${labData.einsendenummer}"></option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Dateityp
|
||||
<select name="fileType">
|
||||
<option th:selected="${file.fileType.value == 'bam'}">BAM</option>
|
||||
<option th:selected="${file.fileType.value == 'vcf'}">Vcf</option>
|
||||
<option th:selected="${file.fileType.value == 'bed'}">BED</option>
|
||||
<option th:selected="${file.fileType.value == 'fastq'}">FASTQ</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Prüfsumme - SHA256
|
||||
<input type="text" pattern="[A-Fa-f0-9]{64}" name="fileChecksum" th:value="${file.fileChecksum} " />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Dateigröße in Bytes
|
||||
<input type="number" min="0" name="fileSizeInBytes" th:value="${file.fileSizeInBytes} " />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="save" th:hx-put="@{/files/unused/{fileId}(fileId=${file.id})}" hx-target="body">Änderungen übernehmen</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script th:src="@{/main.js}"></script>
|
||||
</body>
|
||||
</html>
|
30
src/main/web/rspack.config.js
Normal file
30
src/main/web/rspack.config.js
Normal file
@ -0,0 +1,30 @@
|
||||
export default {
|
||||
entry: {
|
||||
main: './src/main/web/script.js',
|
||||
},
|
||||
output: {
|
||||
path: './src/main/resources/static',
|
||||
chunkFilename: '[id].js'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [{
|
||||
loader: "postcss-loader",
|
||||
options: {
|
||||
postcssOptions: {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
}
|
||||
}
|
||||
}],
|
||||
type: "css"
|
||||
},
|
||||
]
|
||||
},
|
||||
experiments: {
|
||||
css: true,
|
||||
}
|
||||
}
|
3
src/main/web/script.js
Normal file
3
src/main/web/script.js
Normal file
@ -0,0 +1,3 @@
|
||||
import 'htmx.org';
|
||||
|
||||
import * as styles from './style.css';
|
190
src/main/web/style.css
Normal file
190
src/main/web/style.css
Normal file
@ -0,0 +1,190 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
body {
|
||||
@apply bg-white dark:bg-gray-950 flex flex-col h-screen;
|
||||
}
|
||||
|
||||
aside {
|
||||
@apply fixed top-0 left-0 z-40 w-80 p-5 h-screen bg-gray-100 text-gray-600
|
||||
}
|
||||
|
||||
aside > .title {
|
||||
@apply m-2 text-2xl font-bold
|
||||
}
|
||||
|
||||
aside > nav {
|
||||
@apply mt-2
|
||||
}
|
||||
|
||||
aside > nav ul {
|
||||
@apply m-2
|
||||
}
|
||||
|
||||
aside > nav a:before {
|
||||
content: '🗎';
|
||||
margin-right: 4px;
|
||||
opacity: 0.8;
|
||||
width: 1.2em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
aside > nav a:hover:before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
aside > nav a:hover {
|
||||
@apply text-gray-900 bg-gray-300
|
||||
}
|
||||
|
||||
aside > nav a {
|
||||
@apply flex p-1 rounded-sm
|
||||
}
|
||||
|
||||
aside > nav a > span {
|
||||
@apply nth-[1]:flex-1
|
||||
}
|
||||
|
||||
aside > nav a .counter {
|
||||
@apply inline-flex items-center justify-center min-w-3 h-3 p-3 font-bold text-xs text-white bg-gray-500 rounded-full
|
||||
}
|
||||
|
||||
main {
|
||||
@apply mx-auto pl-84 px-10 py-1 w-full;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4 {
|
||||
@apply mt-5 text-white py-1 px-3 rounded-t-sm
|
||||
}
|
||||
|
||||
h1 {
|
||||
@apply text-3xl font-bold bg-blue-900
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply text-2xl font-bold bg-green-900
|
||||
}
|
||||
|
||||
h3 {
|
||||
@apply text-xl font-bold bg-yellow-900
|
||||
}
|
||||
|
||||
h4 {
|
||||
@apply font-bold bg-red-900
|
||||
}
|
||||
|
||||
form {
|
||||
@apply p-2 rounded-b-sm
|
||||
}
|
||||
|
||||
label {
|
||||
@apply mr-4 w-1/2 text-sm
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
@apply text-base w-full text-black
|
||||
}
|
||||
|
||||
label.optional {
|
||||
@apply italic
|
||||
}
|
||||
label.optional input,
|
||||
label.optional select {
|
||||
font-style: normal;
|
||||
background: repeating-linear-gradient(135deg, var(--color-white), var(--color-white) 4px, transparent 4px, transparent 8px)
|
||||
}
|
||||
|
||||
input[type=text],
|
||||
input[type=date],
|
||||
input[type=number],
|
||||
select {
|
||||
@apply block font-mono border-b-2 border-b-gray-300 bg-gray-50 focus:outline-none focus:border-gray-600 focus:bg-gray-200 h-8 p-1
|
||||
}
|
||||
|
||||
input[type=submit],
|
||||
input[type=reset],
|
||||
button {
|
||||
@apply cursor-pointer bg-gray-700 text-white rounded-sm px-6 py-2 mx-auto my-2 w-fit transition-colors
|
||||
}
|
||||
|
||||
input[type=submit]:hover,
|
||||
input[type=reset]:hover,
|
||||
button:hover {
|
||||
@apply bg-gray-900
|
||||
}
|
||||
|
||||
div:has(> label) {
|
||||
@apply my-2 flex
|
||||
}
|
||||
|
||||
.case {
|
||||
@apply border-l-4 border-l-blue-900 bg-blue-100
|
||||
}
|
||||
|
||||
.case input[type=text],
|
||||
.case input[type=date],
|
||||
.case input[type=number],
|
||||
.case select {
|
||||
@apply focus:border-blue-600 focus:bg-blue-200
|
||||
}
|
||||
|
||||
.donor {
|
||||
@apply border-l-4 border-l-green-900 bg-green-100
|
||||
}
|
||||
|
||||
.donor input[type=text],
|
||||
.donor input[type=date],
|
||||
.donor input[type=number],
|
||||
.donor select {
|
||||
@apply focus:border-green-600 focus:bg-green-200
|
||||
}
|
||||
|
||||
.lab-data {
|
||||
@apply border-l-4 border-l-yellow-900 bg-yellow-100
|
||||
}
|
||||
|
||||
.lab-data input[type=text],
|
||||
.lab-data input[type=date],
|
||||
.lab-data input[type=number],
|
||||
.lab-data select {
|
||||
@apply focus:border-yellow-600 focus:bg-yellow-200
|
||||
}
|
||||
|
||||
.file {
|
||||
@apply border-l-4 border-l-red-900 bg-red-100
|
||||
}
|
||||
|
||||
.file input[type=text],
|
||||
.file input[type=date],
|
||||
.file input[type=number],
|
||||
.file select {
|
||||
@apply focus:border-red-600 focus:bg-red-200
|
||||
}
|
||||
|
||||
button {
|
||||
@apply text-base font-normal
|
||||
}
|
||||
|
||||
button.left {
|
||||
@apply m-0 py-0 px-1 mx-1 float-right text-base font-normal
|
||||
}
|
||||
button.new:before {
|
||||
content: '➕';
|
||||
margin-right: .5em;
|
||||
}
|
||||
button.delete:before {
|
||||
content: '🗑';
|
||||
margin-right: .5em;
|
||||
}
|
||||
button.save:before {
|
||||
content: '💾';
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
.case-list > li {
|
||||
@apply my-2 p-1 odd:bg-blue-200
|
||||
}
|
||||
|
||||
.case-list > li > a {
|
||||
@apply w-full
|
||||
}
|
Reference in New Issue
Block a user