Merge pull request 'newUI' (#1) from newUI into master

Reviewed-on: #1
This commit is contained in:
neviyn 2020-09-18 09:49:04 +01:00
commit ab752160c9
13 changed files with 423 additions and 1704 deletions

View File

@ -21,9 +21,9 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<kotlin.version>1.3.40</kotlin.version>
<kotlin.version>1.4.10</kotlin.version>
<kotlin.compiler.jvmTarget>1.8</kotlin.compiler.jvmTarget>
<spring.version>2.1.6.RELEASE</spring.version>
<spring.version>2.3.3.RELEASE</spring.version>
</properties>
<dependencyManagement>
@ -103,11 +103,12 @@
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.10.6</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-joda</artifactId>
<version>2.9.9</version>
<version>2.11.2</version>
</dependency>
<dependency>
<groupId>org.jadira.usertype</groupId>
@ -117,19 +118,23 @@
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.6.2</version>
<version>2.8.5</version>
</dependency>
<dependency>
<groupId>org.simplejavamail</groupId>
<artifactId>simple-java-mail</artifactId>
<version>6.0.3</version>
<version>6.4.3</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-core</artifactId>
<version>1.3.0-M2</version>
<version>1.3.9</version>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>

View File

@ -93,7 +93,7 @@ data class Observation(
) {
fun toCsvFormat(): String {
fun escapeSpecialCharacters(data: String): String {
return data.replace("\"", "\"\"")
return data.replace("\"", "\"\"").replace("\n", "")
}
val dataPortion = "${date.toString("dd/MM/yyyy")},\"$person, ${type.name} ${scenarios.joinToString { it.title }}\"," +
"\"Training\",\"Operations - Shift Operations\",\"${site.name}\",\"N/A\"," +
@ -121,7 +121,7 @@ data class Observation(
}
private fun roundScore(input: Double?): String {
if (input != null) {
if (input != null && input > 0) {
return input.roundToInt().toString()
}
return ""
@ -185,7 +185,7 @@ data class Scenario(
) {
private fun ratingValid(rating: Byte): Boolean {
return rating in 1..5
return rating in 0..5
}
fun ratingsAllValid(): Boolean {
return ratingValid(monitoringRating) &&

View File

@ -1,218 +0,0 @@
package uk.co.neviyn.observationdatabase.controller
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.joda.time.LocalDate
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.core.env.Environment
import org.springframework.core.env.get
import org.springframework.http.HttpStatus
import org.springframework.messaging.simp.SimpMessagingTemplate
import org.springframework.web.bind.annotation.CrossOrigin
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.server.ResponseStatusException
import uk.co.neviyn.observationdatabase.Email
import uk.co.neviyn.observationdatabase.GroupObservation
import uk.co.neviyn.observationdatabase.GroupObservationInit
import uk.co.neviyn.observationdatabase.GroupSessionManager
import uk.co.neviyn.observationdatabase.Observation
import uk.co.neviyn.observationdatabase.ObservationRepository
import uk.co.neviyn.observationdatabase.SiteRepository
import uk.co.neviyn.observationdatabase.TutorRepository
import java.net.Inet4Address
import java.net.NetworkInterface
import javax.validation.Valid
@RestController
@RequestMapping("/api/grpob")
@CrossOrigin
class GroupSessionController {
private val logger: Logger = LoggerFactory.getLogger(javaClass)!!
@Autowired
lateinit var environment: Environment
@Autowired
lateinit var siteRepository: SiteRepository
@Autowired
lateinit var tutorRepository: TutorRepository
@Autowired
lateinit var observationRepository: ObservationRepository
@Autowired
lateinit var websocketMessenger: SimpMessagingTemplate
@Autowired
lateinit var mailer: Email
/**
* Start a new Group Observation session
*/
@PostMapping("/start")
fun startGroupObservation(@Valid @RequestBody initData: GroupObservationInit): Map<String, String> {
val site = siteRepository.findById(initData.site)
val tutors = tutorRepository.findAllById(initData.tutors).toSet()
if (!site.isPresent) {
logger.info("Attempted to add Observation without a site.")
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Site required")
}
if (tutors.isEmpty() || tutors.size != initData.tutors.size) {
logger.info("Attempted to add Observation without a tutor")
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Tutor required")
}
val sessionId = GroupSessionManager.startNewSession(site.get(), tutors, initData.type, initData.scenarioTitles)
return getConnectionDetails().plus("id" to sessionId.toString())
}
/**
* Reconnect to the last Group Observation session
*/
@GetMapping("/recover")
fun reconnectToGroupObservation(): Map<String, Any> {
if (GroupSessionManager.isValid()) {
logger.info("Previous group observation requested, current id ${GroupSessionManager.sessionId}")
return getConnectionDetails().plus(mapOf("id" to GroupSessionManager.sessionId.toString(), "scenarios" to GroupSessionManager.asScenarioView()))
}
logger.info("Tried to recover a session but no session is active")
throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "No group session is currently running")
}
/**
* Check whether there is an active group session with [id]
*/
@GetMapping("/valid/{id}")
fun checkGroupObservationValidityById(@PathVariable id: Int): Map<String, Any> {
if (GroupSessionManager.isValid(id)) {
return mapOf("titles" to GroupSessionManager.scenarioTitles!!)
}
if (GroupSessionManager.isValid()) {
logger.warn("Group observation requested with id $id but id is currently ${GroupSessionManager.sessionId})")
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Session ID incorrect")
}
logger.warn("Group observation requested with id $id but there is no valid session")
throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "No group session is currently running")
}
/**
* Check whether there is any valid group session
*/
@GetMapping("/valid")
fun checkGroupObservationValidity(): Boolean {
return GroupSessionManager.isValid()
}
/**
* Save an Observation to the database
*/
fun saveObservation(observation: Observation): Observation {
logger.debug("Saving new Observation to database")
val committedObservation = observationRepository.save(observation)
logger.debug("Adding Observation data to Tutor records")
committedObservation.tutors.forEach {
it.observations.add(committedObservation)
tutorRepository.save(it)
}
logger.debug("Observation addition completed")
return committedObservation
}
/**
* Get the current observation data for a user with [name] in the current session.
*/
@GetMapping("/participant/{name}")
fun getParticipantData(@PathVariable name: String): GroupObservation {
if (GroupSessionManager.participantExistsInSession(name))
return GroupSessionManager.getObservationDataForParticipant(name)!!
throw ResponseStatusException(HttpStatus.NOT_FOUND, "No participant with the name:'$name'")
}
/**
* Submit an observation to be added to the session state and actual database
*/
@PostMapping("/submit")
fun addGroupObservation(@Valid @RequestBody observationData: GroupObservation) {
val titles = observationData.scenarios.map { it.title }
if (GroupSessionManager.scenarioTitles!!.size != titles.size || !GroupSessionManager.scenarioTitles!!.containsAll(titles)) {
logger.warn("Received scenario data but titles did not match\nInput:$titles\nRequired:${GroupSessionManager.scenarioTitles}")
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Submission data contains non-matching title(s)")
}
GroupSessionManager.updateObservationData(observationData)
websocketMessenger.convertAndSend("/ws/scenarios", mapOf("scenarios" to GroupSessionManager.asScenarioView()))
}
@PostMapping("/complete")
fun pushObservationsToDatabase(): Map<String, String> {
if (GroupSessionManager.isValid() && GroupSessionManager.dataComplete()) {
logger.info("Completing session ${GroupSessionManager.sessionId}")
val tutors = tutorRepository.findAllById(GroupSessionManager.tutors!!).toSet()
val observations = mutableListOf<Observation>()
GroupSessionManager.observations.values.forEach { x ->
val observation = Observation(
site = GroupSessionManager.site!!,
date = LocalDate.now(),
type = GroupSessionManager.trainingType!!,
observed = x.scenarios.joinToString { it.title },
monitoring = x.scenarios.map { it.monitoringRating }.average(),
conservatism = x.scenarios.map { it.conservatismRating }.average(),
controlProcedural = x.scenarios.map { it.controlProceduralRating }.average(),
control = x.scenarios.map { it.controlRating }.average(),
teamworkCommunications = x.scenarios.map { it.teamworkCommunicationsRating }.average(),
teamworkLeadership = x.scenarios.map { it.teamworkLeadershipRating }.average(),
teamworkWorkload = x.scenarios.map { it.teamworkWorkloadRating }.average(),
knowledge = x.scenarios.map { it.knowledgeRating }.average(),
scenarios = x.scenarios,
tutors = tutors,
person = x.person.toUpperCase()
)
saveObservation(observation)
observations.add(observation)
}
GroupSessionManager.invalidate()
if (this::environment.isInitialized && environment.getProperty("smtp.autosendoncomplete")!!.toBoolean())
GlobalScope.launch {
if (::mailer.isInitialized)
mailer.sendObservationData(observations)
else
logger.error("Mailer has not been initialized.")
}
else
logger.debug("Environment or Mailer is unavailable")
websocketMessenger.convertAndSend("/ws/status", mapOf("status" to "complete"))
return mapOf("success" to "The submission was successfully completed.")
} else if (!GroupSessionManager.dataComplete()) {
logger.info("Tried to complete a session whilst data was incomplete\n${GroupSessionManager.observations}")
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Data is incomplete")
}
throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "No valid session")
}
/**
* Get details needed to connect to this group session on the LAN
*/
@GetMapping("/address")
fun getConnectionDetails(): Map<String, String> {
var ipv4: String? = null
var retryCount = 0
while (ipv4 == null && retryCount < 3) {
ipv4 = NetworkInterface.getNetworkInterfaces().asSequence()
.filter { !it.isLoopback }.map { x -> x.inetAddresses.asSequence()
.filter { it is Inet4Address }.map { it.hostAddress } }.flatten().firstOrNull()
retryCount++
Thread.sleep(1_000) // Sleep for 1 second
}
return if (ipv4 != null && this::environment.isInitialized)
mapOf("ip" to ipv4, "port" to environment["local.server.port"]!!)
else if (ipv4 == null) {
logger.error("IP Address could not be determined")
mapOf("error" to "Could not determine IP Address")
} else {
logger.error("Port could not be determined, environment not initialised")
mapOf("error" to "Could not determine port")
}
}
}

View File

@ -1,13 +1,11 @@
package uk.co.neviyn.observationdatabase
import junit.framework.TestCase.assertFalse
import junit.framework.TestCase.assertNotNull
import org.junit.Test
import org.junit.runner.RunWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.junit4.SpringRunner
import uk.co.neviyn.observationdatabase.controller.GroupSessionController
import uk.co.neviyn.observationdatabase.controller.ObservationsController
@RunWith(SpringRunner::class)
@ -16,13 +14,9 @@ class ObservationDatabaseApplicationTests {
@Autowired
lateinit var observationsController: ObservationsController
@Autowired
lateinit var groupSessionController: GroupSessionController
@Test
fun contextLoads() {
assertNotNull(observationsController)
assertNotNull(groupSessionController)
assertFalse(GroupSessionManager.isValid())
}
}

View File

@ -1,167 +0,0 @@
package uk.co.neviyn.observationdatabase.controller
import org.joda.time.LocalDate
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.junit.MockitoJUnitRunner
import org.springframework.messaging.simp.SimpMessagingTemplate
import org.springframework.web.server.ResponseStatusException
import uk.co.neviyn.observationdatabase.Email
import uk.co.neviyn.observationdatabase.GroupObservation
import uk.co.neviyn.observationdatabase.GroupObservationInit
import uk.co.neviyn.observationdatabase.GroupSessionManager
import uk.co.neviyn.observationdatabase.Observation
import uk.co.neviyn.observationdatabase.ObservationRepository
import uk.co.neviyn.observationdatabase.Scenario
import uk.co.neviyn.observationdatabase.Site
import uk.co.neviyn.observationdatabase.SiteRepository
import uk.co.neviyn.observationdatabase.TrainingType
import uk.co.neviyn.observationdatabase.Tutor
import uk.co.neviyn.observationdatabase.TutorRepository
import java.util.Optional
@RunWith(MockitoJUnitRunner::class)
class GroupSessionControllerTest {
@InjectMocks
lateinit var controller: GroupSessionController
@Mock
lateinit var websocketMessenger: SimpMessagingTemplate
@Mock
lateinit var siteRepository: SiteRepository
@Mock
lateinit var tutorRepository: TutorRepository
@Mock
lateinit var observationRepository: ObservationRepository
@Mock
lateinit var mailer: Email
@After
fun tearDown() {
GroupSessionManager.invalidate()
}
@Test
fun testStartSession() {
val site = Site(1, "Test site")
Mockito.doReturn(Optional.of(site)).`when`(siteRepository).findById(1)
Mockito.doReturn(listOf(Tutor(1, "Mr X", site))).`when`(tutorRepository).findAllById(listOf(1))
controller.startGroupObservation(GroupObservationInit(1, TrainingType.INITIAL, listOf(1), listOf("Sample title")))
assertTrue(GroupSessionManager.isValid())
}
@Test(expected = ResponseStatusException::class)
fun testStartSession_NoSite() {
controller.startGroupObservation(GroupObservationInit(0, TrainingType.INITIAL, listOf(1), listOf("Sample title")))
}
@Test(expected = ResponseStatusException::class)
fun testStartSession_NoTutor() {
val site = Site(1, "Test site")
Mockito.doReturn(Optional.of(site)).`when`(siteRepository).findById(1)
controller.startGroupObservation(GroupObservationInit(1, TrainingType.INITIAL, listOf(0), listOf("Sample title")))
}
@Test
fun testRecoverSession() {
val site = Site(1, "Test site")
Mockito.doReturn(Optional.of(site)).`when`(siteRepository).findById(1)
Mockito.doReturn(listOf(Tutor(1, "Mr X", site))).`when`(tutorRepository).findAllById(listOf(1))
controller.startGroupObservation(GroupObservationInit(1, TrainingType.INITIAL, listOf(1), listOf("Sample title")))
assertNotNull(controller.reconnectToGroupObservation())
}
@Test(expected = ResponseStatusException::class)
fun testRecoverSession_NoActiveSession() {
controller.reconnectToGroupObservation()
}
@Test
fun testCompleteSession() {
val site = Site(1, "Test site")
val tutor = Tutor(1, "Mr X", site)
Mockito.doReturn(Optional.of(site)).`when`(siteRepository).findById(1)
Mockito.doReturn(listOf(tutor)).`when`(tutorRepository).findAllById(listOf(1))
val person = "A Student"
val scenario = Scenario(0, "Sample title", 5, "", "", 5, "", "", 5, "", "", 5, "", "", 5, "", "", 5, "", "", 5, "", "", 5, "", "")
Mockito.doReturn(Observation(1, site, LocalDate.now(), TrainingType.INITIAL, "Sample title", 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, listOf(scenario), setOf(tutor), person)).`when`(observationRepository).save(any())
controller.startGroupObservation(GroupObservationInit(1, TrainingType.INITIAL, listOf(1), listOf("Sample title")))
controller.addGroupObservation(GroupObservation("A Student", listOf(scenario)))
controller.pushObservationsToDatabase()
verify(observationRepository, times(1)).save(any())
verify(websocketMessenger, times(1)).convertAndSend("/ws/scenarios", mapOf("scenarios" to mapOf("Sample title" to listOf(scenario.copy(title = "A Student")))))
assertEquals(1, tutor.observations.size)
}
@Test(expected = ResponseStatusException::class)
fun testCompleteSession_NoObservationData() {
val site = Site(1, "Test site")
Mockito.doReturn(Optional.of(site)).`when`(siteRepository).findById(1)
Mockito.doReturn(listOf(Tutor(1, "Mr X", site))).`when`(tutorRepository).findAllById(listOf(1))
controller.startGroupObservation(GroupObservationInit(1, TrainingType.INITIAL, listOf(1), listOf("Sample title")))
controller.pushObservationsToDatabase()
}
@Test(expected = ResponseStatusException::class)
fun testCompleteSession_PartialObservationData() {
val site = Site(1, "Test site")
Mockito.doReturn(Optional.of(site)).`when`(siteRepository).findById(1)
Mockito.doReturn(listOf(Tutor(1, "Mr X", site))).`when`(tutorRepository).findAllById(listOf(1))
controller.startGroupObservation(GroupObservationInit(1, TrainingType.INITIAL, listOf(1), listOf("Sample title")))
controller.addGroupObservation(GroupObservation("A Student", listOf(Scenario(0, "Sample title", 0, "", "", 5, "", "", 5, "", "", 5, "", "", 5, "", "", 5, "", "", 5, "", "", 5, "", ""))))
verify(websocketMessenger, times(1)).convertAndSend("/ws/scenarios", mapOf("scenarios" to mapOf("Sample title" to listOf(Scenario(0, "A Student", 0, "", "", 5, "", "", 5, "", "", 5, "", "", 5, "", "", 5, "", "", 5, "", "", 5, "", "")))))
controller.pushObservationsToDatabase()
}
@Test(expected = ResponseStatusException::class)
fun testSubmit_NonMatchingTitles() {
val site = Site(1, "Test site")
Mockito.doReturn(Optional.of(site)).`when`(siteRepository).findById(1)
Mockito.doReturn(listOf(Tutor(1, "Mr X", site))).`when`(tutorRepository).findAllById(listOf(1))
controller.startGroupObservation(GroupObservationInit(1, TrainingType.INITIAL, listOf(1), listOf("Sample title")))
controller.addGroupObservation(GroupObservation("A Student", listOf(Scenario(0, "Different Title", 5, "", "", 5, "", "", 5, "", "", 5, "", "", 5, "", "", 5, "", "", 5, "", "", 5, "", ""))))
}
@Test
fun testSubmit_PartialData() {
val site = Site(1, "Test site")
Mockito.doReturn(Optional.of(site)).`when`(siteRepository).findById(1)
Mockito.doReturn(listOf(Tutor(1, "Mr X", site))).`when`(tutorRepository).findAllById(listOf(1))
controller.startGroupObservation(GroupObservationInit(1, TrainingType.INITIAL, listOf(1), listOf("Sample title")))
controller.addGroupObservation(GroupObservation("A Student", listOf(Scenario(0, "Sample title", 0, "", "", 5, "", "", 5, "", "", 5, "", "", 5, "", "", 5, "", "", 5, "", "", 5, "", ""))))
assertEquals(1, GroupSessionManager.observations.size)
assertEquals("A Student", GroupSessionManager.observations.keys.first())
assertEquals("A Student", GroupSessionManager.observations.values.first().person)
}
@Test(expected = ResponseStatusException::class)
fun testGetParticipantData_NoParticipant() {
controller.getParticipantData("Someone")
}
@Test(expected = ResponseStatusException::class)
fun testGetParticipantData_WrongName() {
val testData = GroupObservation("A Student", listOf(Scenario(0, "Sample title", 5, "", "", 5, "", "", 5, "", "", 5, "", "", 5, "", "", 5, "", "", 5, "", "", 5, "", "")))
GroupSessionManager.observations["A Student"] = testData
controller.getParticipantData("Another Student")
}
@Test
fun testGetParticipantData() {
val testData = GroupObservation("A Student", listOf(Scenario(0, "Sample title", 5, "", "", 5, "", "", 5, "", "", 5, "", "", 5, "", "", 5, "", "", 5, "", "", 5, "", "")))
GroupSessionManager.observations["A Student"] = testData
val output = controller.getParticipantData("A Student")
assertEquals(testData, output)
}
}

View File

@ -3,7 +3,7 @@
<b-row>
<b-col cols="3">
<p>{{ description }}</p>
<h4 v-bind:class="{ scorewarn: rating < 3 }">
<h4 v-bind:class="{ scorewarn: rating < 3}" v-if="rating > 0">
{{ rating }}
</h4>
</b-col>

View File

@ -3,11 +3,12 @@
<b-form-radio-group
buttons
button-variant="outline-info"
size="lg"
size="custom"
v-model="propModel"
invalid-feedback="Please select a score."
required
>
<b-form-radio button-variant="0" value="0">&nbsp;</b-form-radio>
<b-form-radio button-variant="1" value="1">1</b-form-radio>
<b-form-radio button-variant="2" value="2">2</b-form-radio>
<b-form-radio button-variant="3" value="3">3</b-form-radio>
@ -27,13 +28,16 @@ export default {
},
set(value) {
this.$emit("newselection", value);
}
}
}
},
},
},
};
</script>
<style>
b-form-radio-group {
font-family: monospace;
}
.btn-1 {
color: #ffffff;
background-color: #cc3232;
@ -243,4 +247,51 @@ fieldset[disabled] .btn-5.active {
color: #2dc937;
background-color: #ffffff;
}
.btn-0 {
color: #ffffff;
background-color: #6c757d;
border-color: #000000;
}
.btn-0:hover,
.btn-0:focus,
.btn-0:active,
.btn-0.active,
.open .dropdown-toggle.btn-0 {
color: #ffffff;
background-color: #4285f4;
border-color: #000000;
}
.btn-0:active,
.btn-0.active,
.open .dropdown-toggle.btn-0 {
background-image: none;
}
.btn-0.disabled,
.btn-0[disabled],
fieldset[disabled] .btn-0,
.btn-0.disabled:hover,
.btn-0[disabled]:hover,
fieldset[disabled] .btn-0:hover,
.btn-0.disabled:focus,
.btn-0[disabled]:focus,
fieldset[disabled] .btn-0:focus,
.btn-0.disabled:active,
.btn-0[disabled]:active,
fieldset[disabled] .btn-0:active,
.btn-0.disabled.active,
.btn-0[disabled].active,
fieldset[disabled] .btn-0.active {
background-color: #6c757d;
border-color: #4285f4;
}
.btn-0 .badge {
color: #6c757d;
background-color: #ffffff;
}
.btn-custom {
padding: 10px 13px;
font-size: 20px;
border-radius: 10px;
}
</style>

View File

@ -9,8 +9,6 @@ const ViewObservations = () => import("./views/ViewObservations.vue");
const ObservationComplete = () => import("./views/ObservationComplete.vue");
const DBError = () => import("./views/DatabaseUnavailable.vue");
const About = () => import("./views/About.vue");
const GroupSession = () => import("./views/GroupSession.vue");
const GroupSessionInput = () => import("./views/GroupSessionInput.vue");
Vue.use(Router);
@ -60,17 +58,6 @@ export default new Router({
path: "/about",
name: "about",
component: About
},
{
path: "/groupsession",
name: "groupsession",
component: GroupSession
},
{
path: "/groupsession/:id",
name: "groupsessioninput",
component: GroupSessionInput,
props: true
}
]
});

View File

@ -1,375 +0,0 @@
<template>
<b-container fluid>
<b-row v-if="active">
<b-col cols="12" md="3">
<vue-qrcode v-model="qrdata" :options="{ width: 250 }"></vue-qrcode>
<p>
Scan the code or navigate to
<br />
{{ qrdata }}
</p>
</b-col>
<b-col cols="12" md="9">
<b-card-group v-for="(item, index) in data" v-bind:key="index">
<b-card
border-variant="secondary"
header-border-variant="secondary"
:header="index"
>
<b-card-text>
<b-table :fields="tableFields" :items="item"></b-table>
</b-card-text>
</b-card>
</b-card-group>
</b-col>
<b-col>
<b-button size="lg" variant="primary" v-on:click="showCompletionModal()"
>Finish Session</b-button
>
</b-col>
</b-row>
<b-row v-else-if="complete">
<b-col>
<b-row>
<b-col>
<h2>Session Complete</h2>
</b-col>
</b-row>
<b-row>
<b-col>
<p>
Observation data for this session is now saved in the database.
</p>
</b-col>
</b-row>
<b-row>
<b-col>
<b-button
size="lg"
variant="primary"
v-on:click="window.location.reload()"
>Start a New Session</b-button
>
</b-col>
</b-row>
</b-col>
</b-row>
<b-row v-else>
<b-col>
<b-form id="submission-form" novalidate @submit="onSubmit">
<b-row align-h="center">
<b-col>
<b-form-group label="Site">
<b-form-select
v-model="site"
:options="siteOptions"
style="text-align:center;"
required
></b-form-select>
</b-form-group>
</b-col>
</b-row>
<b-row align-h="center">
<b-col>
<b-form-group label="Type">
<b-form-select
v-model="type"
style="text-align:center;"
required
>
<option :value="null">Select a training type</option>
<option value="INITIAL">INITIAL</option>
<option value="CONTINUING">CONTINUING</option>
</b-form-select>
</b-form-group>
</b-col>
</b-row>
<b-row align-h="center">
<b-col>
<b-form-group label="Tutor(s)">
<p v-if="site == null">Select a site first.</p>
<b-form-checkbox-group
v-model="tutors"
:options="tutorOptions"
></b-form-checkbox-group>
</b-form-group>
</b-col>
</b-row>
<b-row
v-for="(item, index) in scenarioTitles"
v-bind:key="index"
class="border bottom-buffer"
fluid
>
<b-col>
<b-form-input
v-model="item.data"
type="text"
placeholder="Enter scenario description."
></b-form-input>
</b-col>
<b-col cols="1">
<b-button
v-on:click="scenarioTitles.splice(index, 1)"
variant="danger"
>
<b>Delete</b>
</b-button>
</b-col>
</b-row>
<b-row align-h="center">
<b-col>
<b-button
v-on:click="scenarioTitles.push({ data: '' })"
size="lg"
variant="primary"
>Add Another Scenario</b-button
>
</b-col>
</b-row>
<b-row>
<b-col>
<br />
</b-col>
</b-row>
<b-row align-h="center">
<b-col>
<b-button type="submit" size="lg" variant="primary"
>Start</b-button
>
</b-col>
<b-col>
<b-button
size="lg"
variant="secondary"
v-on:click="connectToPrevious()"
>Connect to Previous Session</b-button
>
</b-col>
</b-row>
</b-form>
</b-col>
</b-row>
<b-modal
id="submissionModal"
ref="submissionModal"
title="Enter password to confirm session start"
@ok="handleOk"
@shown="clearPassword"
>
<form @submit.stop.prevent="handleSubmit">
<b-form-input
type="password"
placeholder="Enter password"
v-model="submitPassword"
></b-form-input>
</form>
</b-modal>
<b-modal
id="completionModal"
ref="completionModal"
title="Enter password to confirm submission"
@ok="handleSubmitComplete"
@shown="clearPassword"
>
<form @submit.stop.prevent="handleSubmit">
<b-form-input
type="password"
placeholder="Enter password"
v-model="submitPassword"
></b-form-input>
</form>
</b-modal>
</b-container>
</template>
<script>
import Vue from "vue";
import VueQrcode from "@chenfengyuan/vue-qrcode";
import webstomp from "webstomp-client";
import SockJS from "sockjs-client";
export default {
name: "groupsession",
title: "Group Session",
components: { VueQrcode },
data: function() {
return {
active: false,
stompclient: null,
complete: false,
qrdata: "N/A",
data: [{ person: { name: "No data yet received." } }],
site: null,
tutors: null,
siteOptions: [],
tutorOptions: [],
scenarioTitles: [{ data: "" }, { data: "" }, { data: "" }],
type: null,
submitPassword: null,
tableFields: [
{ key: "title", label: "Name" },
{ key: "monitoring.rating", label: "Monitoring" },
{ key: "controlProcedural.rating", label: "Control Procedural" },
{ key: "control.rating", label: "Control" },
{ key: "conservatism.rating", label: "Conservatism" },
{
key: "teamworkCommunications.rating",
label: "Teamwork Communications"
},
{ key: "teamworkLeadership.rating", label: "Teamwork Leadership" },
{ key: "teamworkWorkload.rating", label: "Teamwork Workload" },
{ key: "knowledge.rating", label: "Knowledge" }
]
};
},
watch: {
site: function() {
this.tutorOptions = [];
this.getTutors();
}
},
mounted() {
Vue.axios
.get("/site")
.then(response => {
this.siteOptions = response.data;
})
.catch(error => {
if (error.response.status === 404) {
//this.$router.push("/dberror");
return;
}
});
},
methods: {
startSession: function() {
var self = this;
let axiosConfig = {
auth: {
username: "admin",
password: this.submitPassword
}
};
Vue.axios
.post(
"/grpob/start",
{
site: self.site,
tutors: self.tutors,
scenarioTitles: self.scenarioTitles.map(x => x.data),
type: self.type
},
axiosConfig
)
.then(function(response) {
if (!("error" in response.data)) {
self.setupSession(response.data);
}
})
.catch(function() {
self.active = false;
});
},
connectToPrevious: function() {
var self = this;
Vue.axios
.get("/grpob/recover")
.then(function(response) {
if (!("error" in response.data)) {
self.setupSession(response.data);
}
})
.catch(function(error) {
if (error.response.status === 404) {
this.$router.push("/dberror");
return;
}
});
},
setupSession: function(rdata) {
var self = this;
self.qrdata = `http://${rdata.ip}:${rdata.port}/#/groupsession/${
rdata.id
}`;
self.active = true;
self.stompclient = webstomp.over(
new SockJS("http://127.0.0.1:8080/websocket", {
heartbeat: false
})
);
self.stompclient.connect([], function() {
self.stompclient.subscribe("/ws/scenarios", function(incomingData) {
self.data = JSON.parse(incomingData.body).scenarios;
});
});
self.$refs.submissionModal.hide();
self.clearPassword();
},
getTutors: function() {
if (this.site != null) {
Vue.axios
.get("/site/" + this.site + "/tutors")
.then(response => {
this.tutorOptions = response.data;
})
.catch(error => {
if (error.response.status === 404) {
this.$router.push("/dberror");
return;
}
});
}
},
onSubmit: function(e) {
e.preventDefault();
e.stopPropagation();
var form = document.getElementById("submission-form");
if (form.checkValidity()) {
this.showModal();
}
form.classList.add("was-validated");
},
showModal() {
this.$refs.submissionModal.show();
},
showCompletionModal() {
this.$refs.completionModal.show();
},
clearPassword() {
this.submitPassword = null;
},
handleOk(evt) {
// Prevent modal from closing
evt.preventDefault();
if (this.submitPassword !== null) {
this.startSession();
}
},
handleSubmitComplete(evt) {
// Prevent modal from closing
var self = this;
evt.preventDefault();
if (this.submitPassword !== null) {
let axiosConfig = {
auth: {
username: "admin",
password: this.submitPassword
}
};
Vue.axios
.post("/grpob/complete", {}, axiosConfig)
.then(function(response) {
if ("success" in response.data) {
self.complete = true;
self.active = false;
}
});
}
}
},
beforeDestroy() {
if (this.stompclient != null) {
this.stompclient.disconnect();
}
}
};
</script>

View File

@ -1,462 +0,0 @@
<template>
<b-container fluid>
<b-container v-if="error !== null">
<p>An error has occurred.</p>
<p>{{ error.status }}</p>
<p>{{ error.data }}</p>
</b-container>
<b-container v-else-if="complete">
<h2>Submission Complete</h2>
<p>Thank you.</p>
<p>
This observation session is now closed and your data submitted to the
database.
</p>
</b-container>
<b-container v-else-if="!valid">
<p>Getting session data from server</p>
</b-container>
<b-container v-else-if="scenarios.length === 0">
<p>
No scenarios defined for this session, please setup a new group session
</p>
</b-container>
<b-container v-else fluid>
<b-form>
<b-row align-h="center">
<b-col>
<b-form-group label="Participant">
<b-form-input
v-model="participant"
type="text"
style="text-align:center;"
placeholder="Enter your name (Initial and Surname)"
required
></b-form-input>
</b-form-group>
</b-col>
<b-col>
<b-button v-on:click="attemptReconnect()">Reconnect </b-button>
</b-col>
</b-row>
<b-row
v-for="(item, index) in scenarios"
v-bind:key="index"
class="border bottom-buffer"
>
<b-col>
<b-row>
<b-col>
<b-form-input
v-model="item.title"
type="text"
readonly
></b-form-input>
</b-col>
</b-row>
<b-row>
<b-col cols="12" md="6" xl="3" class="border">
<b-row>
<b-col>
<h5>Monitoring</h5>
<score-selector
:score-value="item.monitoringRating"
v-on:newselection="
item.monitoringRating = $event;
actuallySubmit();
"
></score-selector>
</b-col>
</b-row>
<b-row>
<b-col>
<b-form-textarea
v-model="item.monitoringStrengths"
placeholder="Strengths"
:rows="1"
:max-rows="2"
no-resize
class="strength"
></b-form-textarea>
<b-form-textarea
v-model="item.monitoringImprovements"
placeholder="AFIs"
:rows="1"
:max-rows="2"
no-resize
class="afi"
></b-form-textarea>
</b-col>
</b-row>
</b-col>
<b-col cols="12" md="6" xl="3" class="border">
<b-row>
<b-col>
<h5>Control Procedural</h5>
<score-selector
:score-value="item.controlProceduralRating"
v-on:newselection="
item.controlProceduralRating = $event;
actuallySubmit();
"
></score-selector>
</b-col>
</b-row>
<b-row>
<b-col>
<b-form-textarea
v-model="item.controlProceduralStrengths"
placeholder="Strengths"
:rows="1"
:max-rows="2"
no-resize
class="strength"
></b-form-textarea>
<b-form-textarea
v-model="item.controlProceduralImprovements"
placeholder="AFIs"
:rows="1"
:max-rows="2"
no-resize
class="afi"
></b-form-textarea>
</b-col>
</b-row>
</b-col>
<b-col cols="12" md="6" xl="3" class="border">
<b-row>
<b-col>
<h5>Control</h5>
<score-selector
:score-value="item.controlRating"
v-on:newselection="
item.controlRating = $event;
actuallySubmit();
"
></score-selector>
</b-col>
</b-row>
<b-row>
<b-col>
<b-form-textarea
v-model="item.controlStrengths"
placeholder="Strengths"
:rows="1"
:max-rows="2"
no-resize
class="strength"
></b-form-textarea>
<b-form-textarea
v-model="item.controlImprovements"
placeholder="AFIs"
:rows="1"
:max-rows="2"
no-resize
class="afi"
></b-form-textarea>
</b-col>
</b-row>
</b-col>
<b-col cols="12" md="6" xl="3" class="border">
<b-row>
<b-col>
<h5>Conservatism</h5>
<score-selector
:score-value="item.conservatismRating"
v-on:newselection="
item.conservatismRating = $event;
actuallySubmit();
"
></score-selector>
</b-col>
</b-row>
<b-row>
<b-col>
<b-form-textarea
v-model="item.conservatismStrengths"
placeholder="Strengths"
:rows="1"
:max-rows="2"
no-resize
class="strength"
></b-form-textarea>
<b-form-textarea
v-model="item.conservatismImprovements"
placeholder="AFIs"
:rows="1"
:max-rows="2"
no-resize
class="afi"
></b-form-textarea>
</b-col>
</b-row>
</b-col>
<b-col cols="12" md="6" xl="3" class="border">
<b-row>
<b-col>
<h5>Teamwork Communications</h5>
<score-selector
:score-value="item.teamworkCommunicationsRating"
v-on:newselection="
item.teamworkCommunicationsRating = $event;
actuallySubmit();
"
></score-selector>
</b-col>
</b-row>
<b-row>
<b-col>
<b-form-textarea
v-model="item.teamworkCommunicationsStrengths"
placeholder="Strengths"
:rows="1"
:max-rows="2"
no-resize
class="strength"
></b-form-textarea>
<b-form-textarea
v-model="item.teamworkCommunicationsImprovements"
placeholder="AFIs"
:rows="1"
:max-rows="2"
no-resize
class="afi"
></b-form-textarea>
</b-col>
</b-row>
</b-col>
<b-col cols="12" md="6" xl="3" class="border">
<b-row>
<b-col>
<h5>Teamwork Leadership</h5>
<score-selector
:score-value="item.teamworkLeadershipRating"
v-on:newselection="
item.teamworkLeadershipRating = $event;
actuallySubmit();
"
></score-selector>
</b-col>
</b-row>
<b-row>
<b-col>
<b-form-textarea
v-model="item.teamworkLeadershipStrengths"
placeholder="Strengths"
:rows="1"
:max-rows="2"
no-resize
class="strength"
></b-form-textarea>
<b-form-textarea
v-model="item.teamworkLeadershipImprovements"
placeholder="AFIs"
:rows="1"
:max-rows="2"
no-resize
class="afi"
></b-form-textarea>
</b-col>
</b-row>
</b-col>
<b-col cols="12" md="6" xl="3" class="border">
<b-row>
<b-col>
<h5>Teamwork Workload</h5>
<score-selector
:score-value="item.teamworkWorkloadRating"
v-on:newselection="
item.teamworkWorkloadRating = $event;
actuallySubmit();
"
></score-selector>
</b-col>
</b-row>
<b-row>
<b-col>
<b-form-textarea
v-model="item.teamworkWorkloadStrengths"
placeholder="Strengths"
:rows="1"
:max-rows="2"
no-resize
class="strength"
></b-form-textarea>
<b-form-textarea
v-model="item.teamworkWorkloadImprovements"
placeholder="AFIs"
:rows="1"
:max-rows="2"
no-resize
class="afi"
></b-form-textarea>
</b-col>
</b-row>
</b-col>
<b-col cols="12" md="6" xl="3" class="border">
<b-row>
<b-col>
<h5>Knowledge</h5>
<score-selector
:score-value="item.knowledgeRating"
v-on:newselection="
item.knowledgeRating = $event;
actuallySubmit();
"
></score-selector>
</b-col>
</b-row>
<b-row>
<b-col>
<b-form-textarea
v-model="item.knowledgeStrengths"
placeholder="Strengths"
:rows="1"
:max-rows="2"
no-resize
class="strength"
></b-form-textarea>
<b-form-textarea
v-model="item.knowledgeImprovements"
placeholder="AFIs"
:rows="1"
:max-rows="2"
no-resize
class="afi"
></b-form-textarea>
</b-col>
</b-row>
</b-col>
</b-row>
</b-col>
</b-row>
<br />
<b-button variant="primary" v-on:click="actuallySubmit()"
>Update</b-button
>
</b-form>
</b-container>
</b-container>
</template>
<script>
import Vue from "vue";
import ScoreSelector from "../components/ScoreSelector.vue";
import webstomp from "webstomp-client";
import SockJS from "sockjs-client";
export default {
name: "groupsessioninput",
title: "Group Session - Input",
props: ["id"],
components: { ScoreSelector },
data: function() {
return {
scenarios: [],
participant: null,
valid: false,
complete: false,
error: null,
stompclient: null
};
},
mounted() {
let self = this;
if (this.id != null) {
Vue.axios
.get(`/grpob/valid/${this.id}`)
.then(function(response) {
if (response.data.titles != null) {
response.data.titles.forEach(function(x) {
self.addAnotherObservation(x);
});
self.valid = true;
self.setupSession();
}
})
.catch(function(error) {
if (error.response.status === 404) {
self.$router.push("/dberror");
return;
}
self.error = error.response;
});
}
},
methods: {
addAnotherObservation: function(newTitle) {
this.scenarios.push({
title: newTitle,
monitoringRating: 0,
monitoringStrengths: "",
monitoringImprovements: "",
controlProceduralRating: 0,
controlProceduralStrengths: "",
controlProceduralImprovements: "",
controlRating: 0,
controlStrengths: "",
controlImprovements: "",
conservatismRating: 0,
conservatismStrengths: "",
conservatismImprovements: "",
teamworkCommunicationsRating: 0,
teamworkCommunicationsStrengths: "",
teamworkCommunicationsImprovements: "",
teamworkLeadershipRating: 0,
teamworkLeadershipStrengths: "",
teamworkLeadershipImprovements: "",
teamworkWorkloadRating: 0,
teamworkWorkloadStrengths: "",
teamworkWorkloadImprovements: "",
knowledgeRating: 0,
knowledgeStrengths: "",
knowledgeImprovements: ""
});
},
actuallySubmit() {
var self = this;
var payload = {
person: self.participant,
scenarios: self.scenarios
};
Vue.axios.post("/grpob/submit", payload).catch(function(error) {
self.error = error;
});
},
setupSession: function() {
var self = this;
self.stompclient = webstomp.over(
new SockJS(`http://${window.location.host}/websocket`, {
heartbeat: false
})
);
self.stompclient.connect([], function() {
self.stompclient.subscribe("/ws/status", function(incomingData) {
var data = JSON.parse(incomingData.body);
if (data.status === "complete") {
self.complete = true;
self.stompclient.disconnect();
}
});
});
},
attemptReconnect: function() {
var self = this;
Vue.axios
.get(`/grpob/participant/${self.participant}`)
.then(function(response) {
self.scenarios = response.data;
});
}
}
};
</script>
<style scoped>
.strength {
background-color: honeydew;
}
.afi {
background-color: mistyrose;
}
.bottom-buffer {
margin-bottom: 10px;
}
</style>

View File

@ -27,15 +27,6 @@
>
</b-col>
</b-row>
<!--
<b-row class="my-3" align-h="center">
<b-col lg="6" sm="12">
<b-button class="scale-in-center" size="lg" to="/groupsession" block
><v-icon name="users" /> Start a Group Session</b-button
>
</b-col>
</b-row>
-->
</b-container>
</template>

View File

@ -1,125 +1,121 @@
<template>
<b-container fluid>
<b-container
v-if="type != null && whom != null && site != null && tutors != null"
fluid
style="padding-left: 130px;"
>
<b-container v-if="type != null && whom != null && site != null && tutors != null" fluid style="padding-left: 130px;">
<h3>
<v-icon name="tag" scale="1.5" />
{{ type }}&nbsp;/&nbsp;{{ whom }}
</h3>
<b-container class="sidebar">
<b-row
align-v="center"
class="sidebar-vert-padding"
v-if="totals[0] > 0"
>
<b-row align-v="center" class="sidebar-vert-padding">
<b-col class="centered-image">
<img
src="../assets/Monitoring.svg"
class="image-opacity"
v-bind:class="{ scorewarning: totals[0] < warningBound }"
v-bind:class="{
scorewarning: totals[0] < warningBound && totals[0] > 0,
}"
/>
<div class="image-centered-text">{{ totals[0] }}</div>
<div class="image-centered-text" v-if="totals[0] > 0">
{{ totals[0] }}
</div>
</b-col>
</b-row>
<b-row
align-v="center"
class="sidebar-vert-padding"
v-if="totals[1] > 0"
>
<b-row align-v="center" class="sidebar-vert-padding">
<b-col class="centered-image">
<img
src="../assets/Control.svg"
class="image-opacity"
v-bind:class="{ scorewarning: totals[1] < warningBound }"
v-bind:class="{
scorewarning: totals[1] < warningBound && totals[1] > 0,
}"
/>
<div class="image-centered-text">{{ totals[1] }}</div>
<div class="image-centered-text" v-if="totals[1] > 0">
{{ totals[1] }}
</div>
</b-col>
</b-row>
<b-row
align-v="center"
class="sidebar-vert-padding"
v-if="totals[2] > 0"
>
<b-row align-v="center" class="sidebar-vert-padding">
<b-col class="centered-image">
<img
src="../assets/Control.svg"
class="image-opacity"
v-bind:class="{ scorewarning: totals[2] < warningBound }"
v-bind:class="{
scorewarning: totals[2] < warningBound && totals[2] > 0,
}"
/>
<div class="image-centered-text">{{ totals[2] }}</div>
<div class="image-centered-text" v-if="totals[2] > 0">
{{ totals[2] }}
</div>
</b-col>
</b-row>
<b-row
align-v="center"
class="sidebar-vert-padding"
v-if="totals[3] > 0"
>
<b-row align-v="center" class="sidebar-vert-padding">
<b-col class="centered-image">
<img
src="../assets/Conservatism.svg"
class="image-opacity"
v-bind:class="{ scorewarning: totals[3] < warningBound }"
v-bind:class="{
scorewarning: totals[3] < warningBound && totals[3] > 0,
}"
/>
<div class="image-centered-text">{{ totals[3] }}</div>
<div class="image-centered-text" v-if="totals[3] > 0">
{{ totals[3] }}
</div>
</b-col>
</b-row>
<b-row
align-v="center"
class="sidebar-vert-padding"
v-if="totals[4] > 0"
>
<b-row align-v="center" class="sidebar-vert-padding">
<b-col class="centered-image">
<img
src="../assets/Teamwork.svg"
class="image-opacity"
v-bind:class="{ scorewarning: totals[4] < warningBound }"
v-bind:class="{
scorewarning: totals[4] < warningBound && totals[4] > 0,
}"
/>
<div class="image-centered-text">{{ totals[4] }}</div>
<div class="image-centered-text" v-if="totals[4] > 0">
{{ totals[4] }}
</div>
</b-col>
</b-row>
<b-row
align-v="center"
class="sidebar-vert-padding"
v-if="totals[5] > 0"
>
<b-row align-v="center" class="sidebar-vert-padding">
<b-col class="centered-image">
<img
src="../assets/Teamwork.svg"
class="image-opacity"
v-bind:class="{ scorewarning: totals[5] < warningBound }"
v-bind:class="{
scorewarning: totals[5] < warningBound && totals[5] > 0,
}"
/>
<div class="image-centered-text">{{ totals[5] }}</div>
<div class="image-centered-text" v-if="totals[5] > 0">
{{ totals[5] }}
</div>
</b-col>
</b-row>
<b-row
align-v="center"
class="sidebar-vert-padding"
v-if="totals[6] > 0"
>
<b-row align-v="center" class="sidebar-vert-padding">
<b-col class="centered-image">
<img
src="../assets/Teamwork.svg"
class="image-opacity"
v-bind:class="{ scorewarning: totals[6] < warningBound }"
v-bind:class="{
scorewarning: totals[6] < warningBound && totals[6] > 0,
}"
/>
<div class="image-centered-text">{{ totals[6] }}</div>
<div class="image-centered-text" v-if="totals[6] > 0">
{{ totals[6] }}
</div>
</b-col>
</b-row>
<b-row
align-v="center"
class="sidebar-vert-padding"
v-if="totals[7] > 0"
>
<b-row align-v="center" class="sidebar-vert-padding">
<b-col class="centered-image">
<img
src="../assets/Knowledge.svg"
class="image-opacity"
v-bind:class="{ scorewarning: totals[7] < warningBound }"
v-bind:class="{
scorewarning: totals[7] < warningBound && totals[7] != 0,
}"
/>
<div class="image-centered-text">{{ totals[7] }}</div>
<div class="image-centered-text" v-if="totals[7] > 0">
{{ totals[7] }}
</div>
</b-col>
</b-row>
</b-container>
@ -140,273 +136,75 @@
></b-form-input>
</b-col>
<b-col cols="1">
<b-button v-on:click="deleteObservation(index)" variant="danger"
<b-button
v-on:click="deleteObservation(index)"
variant="outline-danger"
><b>Delete</b></b-button
>
</b-col>
</b-row>
<b-row>
<b-col cols="6" class="border">
<b-row>
<b-col cols="4">
<h5>Monitoring</h5>
<score-selector
:score-value="item.monitoringRating"
v-on:newselection="
item.monitoringRating = $event;
updateTotals();
"
></score-selector>
</b-col>
<b-col cols="8">
<b-form-textarea
v-model="item.monitoringStrengths"
placeholder="Strengths"
:rows="1"
:max-rows="2"
no-resize
class="strength"
></b-form-textarea>
<b-form-textarea
v-model="item.monitoringImprovements"
placeholder="AFIs"
:rows="1"
:max-rows="2"
no-resize
class="afi"
></b-form-textarea>
</b-col>
</b-row>
</b-col>
<b-col cols="6" class="border">
<b-row>
<b-col cols="4">
<h5>Control Procedural</h5>
<score-selector
:score-value="item.controlProceduralRating"
v-on:newselection="
item.controlProceduralRating = $event;
updateTotals();
"
></score-selector>
</b-col>
<b-col cols="8">
<b-form-textarea
v-model="item.controlProceduralStrengths"
placeholder="Strengths"
:rows="1"
:max-rows="2"
no-resize
class="strength"
></b-form-textarea>
<b-form-textarea
v-model="item.controlProceduralImprovements"
placeholder="AFIs"
:rows="1"
:max-rows="2"
no-resize
class="afi"
></b-form-textarea>
</b-col>
</b-row>
</b-col>
</b-row>
<b-row>
<b-col cols="6" class="border">
<b-row>
<b-col cols="4">
<h5>Control</h5>
<score-selector
:score-value="item.controlRating"
v-on:newselection="
item.controlRating = $event;
updateTotals();
"
></score-selector>
</b-col>
<b-col cols="8">
<b-form-textarea
v-model="item.controlStrengths"
placeholder="Strengths"
:rows="1"
:max-rows="2"
no-resize
class="strength"
></b-form-textarea>
<b-form-textarea
v-model="item.controlImprovements"
placeholder="AFIs"
:rows="1"
:max-rows="2"
no-resize
class="afi"
></b-form-textarea>
</b-col>
</b-row>
</b-col>
<b-col cols="6" class="border">
<b-row>
<b-col cols="4">
<h5>Conservatism</h5>
<score-selector
:score-value="item.conservatismRating"
v-on:newselection="
item.conservatismRating = $event;
updateTotals();
"
></score-selector>
</b-col>
<b-col cols="8">
<b-form-textarea
v-model="item.conservatismStrengths"
placeholder="Strengths"
:rows="1"
:max-rows="2"
no-resize
class="strength"
></b-form-textarea>
<b-form-textarea
v-model="item.conservatismImprovements"
placeholder="AFIs"
:rows="1"
:max-rows="2"
no-resize
class="afi"
></b-form-textarea>
</b-col>
</b-row>
</b-col>
</b-row>
<b-row>
<b-col cols="6" class="border">
<b-row>
<b-col cols="4">
<h5>Teamwork Communications</h5>
<score-selector
:score-value="item.teamworkCommunicationsRating"
v-on:newselection="
item.teamworkCommunicationsRating = $event;
updateTotals();
"
></score-selector>
</b-col>
<b-col cols="8">
<b-form-textarea
v-model="item.teamworkCommunicationsStrengths"
placeholder="Strengths"
:rows="1"
:max-rows="2"
no-resize
class="strength"
></b-form-textarea>
<b-form-textarea
v-model="item.teamworkCommunicationsImprovements"
placeholder="AFIs"
:rows="1"
:max-rows="2"
no-resize
class="afi"
></b-form-textarea>
</b-col>
</b-row>
</b-col>
<b-col cols="6" class="border">
<b-row>
<b-col cols="4">
<h5>Teamwork Leadership</h5>
<score-selector
:score-value="item.teamworkLeadershipRating"
v-on:newselection="
item.teamworkLeadershipRating = $event;
updateTotals();
"
></score-selector>
</b-col>
<b-col cols="8">
<b-form-textarea
v-model="item.teamworkLeadershipStrengths"
placeholder="Strengths"
:rows="1"
:max-rows="2"
no-resize
class="strength"
></b-form-textarea>
<b-form-textarea
v-model="item.teamworkLeadershipImprovements"
placeholder="AFIs"
:rows="1"
:max-rows="2"
no-resize
class="afi"
></b-form-textarea>
</b-col>
</b-row>
</b-col>
</b-row>
<b-row>
<b-col cols="6" class="border">
<b-row>
<b-col cols="4">
<h5>Teamwork Workload</h5>
<score-selector
:score-value="item.teamworkWorkloadRating"
v-on:newselection="
item.teamworkWorkloadRating = $event;
updateTotals();
"
></score-selector>
</b-col>
<b-col cols="8">
<b-form-textarea
v-model="item.teamworkWorkloadStrengths"
placeholder="Strengths"
:rows="1"
:max-rows="2"
no-resize
class="strength"
></b-form-textarea>
<b-form-textarea
v-model="item.teamworkWorkloadImprovements"
placeholder="AFIs"
:rows="1"
:max-rows="2"
no-resize
class="afi"
></b-form-textarea>
</b-col>
</b-row>
</b-col>
<b-col cols="6" class="border">
<b-row>
<b-col cols="4">
<h5>Knowledge</h5>
<score-selector
:score-value="item.knowledgeRating"
v-on:newselection="
item.knowledgeRating = $event;
updateTotals();
"
></score-selector>
</b-col>
<b-col cols="8">
<b-form-textarea
v-model="item.knowledgeStrengths"
placeholder="Strengths"
:rows="1"
:max-rows="2"
no-resize
class="strength"
></b-form-textarea>
<b-form-textarea
v-model="item.knowledgeImprovements"
placeholder="AFIs"
:rows="1"
:max-rows="2"
no-resize
class="afi"
></b-form-textarea>
</b-col>
</b-row>
<b-row cols="2">
<div
v-for="(entry, entryIndex) in item.entries"
v-bind:key="entryIndex"
>
<b-col class="border">
<b-row>
<b-col cols="4">
<b-form-select
v-model="entry.type"
:options="entryTypeOptions"
v-on:change="updateTotals()"
class="my-3"
></b-form-select>
<div class="d-flex">
<b-button
class="mr-2"
variant="outline-danger"
size="sm"
v-on:click="deleteEntry(index, entryIndex)"
v-b-tooltip.hover
title="Delete this entry"
>X</b-button
>
<score-selector
:score-value="entry.rating"
v-on:newselection="
entry.rating = $event;
updateTotals();
"
></score-selector>
</div>
</b-col>
<b-col cols="8">
<b-form-textarea
v-model="entry.strengths"
placeholder="Strengths"
:rows="1"
:max-rows="2"
no-resize
class="strength"
></b-form-textarea>
<b-form-textarea
v-model="entry.AFIs"
placeholder="AFIs"
:rows="1"
:max-rows="2"
no-resize
class="afi"
></b-form-textarea>
</b-col>
</b-row>
</b-col>
</div>
<b-col cols="6" align-self="center">
<b-button
v-on:click="addAnotherEntry(item)"
size="lg"
style="font-size: 200%"
class="py-4 my-4"
>&nbsp;&nbsp;&nbsp;+&nbsp;&nbsp;&nbsp;</b-button
>
</b-col>
</b-row>
</b-container>
@ -479,68 +277,46 @@ export default {
scenarios: [
{
title: "",
monitoringRating: null,
monitoringStrengths: "",
monitoringImprovements: "",
controlProceduralRating: null,
controlProceduralStrengths: "",
controlProceduralImprovements: "",
controlRating: null,
controlStrengths: "",
controlImprovements: "",
conservatismRating: null,
conservatismStrengths: "",
conservatismImprovements: "",
teamworkCommunicationsRating: null,
teamworkCommunicationsStrengths: "",
teamworkCommunicationsImprovements: "",
teamworkLeadershipRating: null,
teamworkLeadershipStrengths: "",
teamworkLeadershipImprovements: "",
teamworkWorkloadRating: null,
teamworkWorkloadStrengths: "",
teamworkWorkloadImprovements: "",
knowledgeRating: null,
knowledgeStrengths: "",
knowledgeImprovements: ""
}
entries: [
{
type: null,
rating: 0,
strengths: "",
AFIs: "",
},
],
},
],
totals: [0, 0, 0, 0, 0, 0, 0, 0],
warningBound: 2.5,
submitPassword: null
submitPassword: null,
entryTypeOptions: [
{ value: "monitoring", text: "Monitoring" },
{ value: "controlProcedural", text: "Control Procedural" },
{ value: "control", text: "Control" },
{ value: "conservatism", text: "Conservatism" },
{ value: "teamworkCommunications", text: "Teamwork Communications" },
{ value: "teamworkLeadership", text: "Teamwork Leadership" },
{ value: "teamworkWorkload", text: "Teamwork Workload" },
{ value: "knowledge", text: "Knowledge" },
],
};
},
computed: {
...mapState(["type", "whom", "site", "tutors"])
...mapState(["type", "whom", "site", "tutors"]),
},
methods: {
addAnotherObservation: function() {
this.scenarios.push({
title: "",
monitoringRating: null,
monitoringStrengths: "",
monitoringImprovements: "",
controlProceduralRating: null,
controlProceduralStrengths: "",
controlProceduralImprovements: "",
controlRating: null,
controlStrengths: "",
controlImprovements: "",
conservatismRating: null,
conservatismStrengths: "",
conservatismImprovements: "",
teamworkCommunicationsRating: null,
teamworkCommunicationsStrengths: "",
teamworkCommunicationsImprovements: "",
teamworkLeadershipRating: null,
teamworkLeadershipStrengths: "",
teamworkLeadershipImprovements: "",
teamworkWorkloadRating: null,
teamworkWorkloadStrengths: "",
teamworkWorkloadImprovements: "",
knowledgeRating: null,
knowledgeStrengths: "",
knowledgeImprovements: ""
entries: [
{
type: null,
rating: 0,
strengths: "",
AFIs: "",
},
],
});
Vue.nextTick(function() {
window.scrollTo(
@ -556,42 +332,41 @@ export default {
}
this.updateTotals();
},
addAnotherEntry: function(scenario) {
scenario.entries.push({
type: null,
rating: 0,
strengths: "",
AFIs: "",
});
},
deleteEntry: function(scenarioIndex, entryIndex) {
this.$delete(this.scenarios[scenarioIndex].entries, entryIndex);
if (this.scenarios[scenarioIndex].entries.length === 0) {
this.addAnotherEntry(this.scenarios[scenarioIndex]);
}
this.updateTotals();
},
updateTotals: function() {
var iTotals = [0, 0, 0, 0, 0, 0, 0, 0];
var counts = [0, 0, 0, 0, 0, 0, 0, 0];
this.scenarios.forEach(function(element) {
if (element.monitoringRating) {
iTotals[0] += parseInt(element.monitoringRating);
counts[0] += 1;
}
if (element.controlProceduralRating) {
iTotals[1] += parseInt(element.controlProceduralRating);
counts[1] += 1;
}
if (element.controlRating) {
iTotals[2] += parseInt(element.controlRating);
counts[2] += 1;
}
if (element.conservatismRating) {
iTotals[3] += parseInt(element.conservatismRating);
counts[3] += 1;
}
if (element.teamworkCommunicationsRating) {
iTotals[4] += parseInt(element.teamworkCommunicationsRating);
counts[4] += 1;
}
if (element.teamworkLeadershipRating) {
iTotals[5] += parseInt(element.teamworkLeadershipRating);
counts[5] += 1;
}
if (element.teamworkWorkloadRating) {
iTotals[6] += parseInt(element.teamworkWorkloadRating);
counts[6] += 1;
}
if (element.knowledgeRating) {
iTotals[7] += parseInt(element.knowledgeRating);
counts[7] += 1;
}
var indices = {
monitoring: 0,
controlProcedural: 1,
control: 2,
conservatism: 3,
teamworkCommunications: 4,
teamworkLeadership: 5,
teamworkWorkload: 6,
knowledge: 7,
};
this.scenarios.forEach(function(scenario) {
scenario.entries.forEach(function(entry) {
if (entry.type !== null && entry.rating > 0) {
iTotals[indices[entry.type]] += parseInt(entry.rating);
counts[indices[entry.type]] += 1;
}
});
});
for (var i = 0; i < iTotals.length; i++) {
if (counts[i] !== 0) {
@ -625,14 +400,155 @@ export default {
},
handleSubmit() {
var form = document.getElementById("submission-form");
var notObservedValue = 0
if (form.checkValidity()) {
let axiosConfig = {
auth: {
username: "admin",
password: this.submitPassword
}
password: this.submitPassword,
},
};
var self = this;
var transformedScenarioData = [];
this.scenarios.forEach(function(scenario) {
var monitoring = scenario.entries.filter(
(entry) => entry.type === "monitoring"
);
var controlProcedural = scenario.entries.filter(
(entry) => entry.type === "controlProcedural"
);
var control = scenario.entries.filter(
(entry) => entry.type === "control"
);
var conservatism = scenario.entries.filter(
(entry) => entry.type === "conservatism"
);
var teamworkCommunications = scenario.entries.filter(
(entry) => entry.type === "teamworkCommunications"
);
var teamworkLeadership = scenario.entries.filter(
(entry) => entry.type === "teamworkLeadership"
);
var teamworkWorkload = scenario.entries.filter(
(entry) => entry.type === "teamworkWorkload"
);
var knowledge = scenario.entries.filter(
(entry) => entry.type === "knowledge"
);
transformedScenarioData.push({
title: scenario.title,
monitoringRating:
monitoring.length > 0
? monitoring.length > 1
? monitoring.reduce(
(a, b) => parseInt(a.rating) + parseInt(b.rating)
) / monitoring.length
: monitoring[0].rating
: notObservedValue,
monitoringStrengths: monitoring
.map((entry) => entry.strengths)
.join("; "),
monitoringImprovements: monitoring
.map((entry) => entry.AFIs)
.join("; "),
controlProceduralRating:
controlProcedural.length > 0
? controlProcedural.length > 1
? controlProcedural.reduce(
(a, b) => parseInt(a.rating) + parseInt(b.rating)
) / controlProcedural.length
: controlProcedural[0].rating
: notObservedValue,
controlProceduralStrengths: controlProcedural
.map((entry) => entry.strengths)
.join("; "),
controlProceduralImprovements: controlProcedural
.map((entry) => entry.AFIs)
.join("; "),
controlRating:
control.length > 0
? control.length > 1
? control.reduce(
(a, b) => parseInt(a.rating) + parseInt(b.rating)
) / control.length
: control[0].rating
: notObservedValue,
controlStrengths: control
.map((entry) => entry.strengths)
.join("; "),
controlImprovements: control.map((entry) => entry.AFIs).join("; "),
conservatismRating:
conservatism.length > 0
? conservatism.length > 1
? conservatism.reduce(
(a, b) => parseInt(a.rating) + parseInt(b.rating)
) / conservatism.length
: conservatism[0].rating
: notObservedValue,
conservatismStrengths: conservatism
.map((entry) => entry.strengths)
.join(";"),
conservatismImprovements: conservatism
.map((entry) => entry.strengths)
.join(";"),
teamworkCommunicationsRating:
teamworkCommunications.length > 0
? teamworkCommunications.length > 1
? teamworkCommunications.reduce(
(a, b) => parseInt(a.rating) + parseInt(b.rating)
) / teamworkCommunications.length
: teamworkCommunications[0].rating
: notObservedValue,
teamworkCommunicationsStrengths: teamworkCommunications
.map((entry) => entry.strengths)
.join(";"),
teamworkCommunicationsImprovements: teamworkCommunications
.map((entry) => entry.AFIs)
.join(";"),
teamworkLeadershipRating:
teamworkLeadership.length > 0
? teamworkLeadership.length > 1
? teamworkLeadership.reduce(
(a, b) => parseInt(a.rating) + parseInt(b.rating)
) / teamworkLeadership.length
: teamworkLeadership[0].rating
: notObservedValue,
teamworkLeadershipStrengths: teamworkLeadership
.map((entry) => entry.strengths)
.join(";"),
teamworkLeadershipImprovements: teamworkLeadership
.map((entry) => entry.AFIs)
.join(";"),
teamworkWorkloadRating:
teamworkWorkload.length > 0
? teamworkWorkload.length > 1
? teamworkWorkload.reduce(
(a, b) => parseInt(a.rating) + parseInt(b.rating)
) / teamworkWorkload.length
: teamworkWorkload[0].rating
: notObservedValue,
teamworkWorkloadStrengths: teamworkWorkload
.map((entry) => entry.strengths)
.join(";"),
teamworkWorkloadImprovements: teamworkWorkload
.map((entry) => entry.AFIs)
.join(";"),
knowledgeRating:
knowledge.length > 0
? knowledge.length > 1
? knowledge.reduce(
(a, b) => parseInt(a.rating) + parseInt(b.rating)
) / knowledge.length
: knowledge[0].rating
: notObservedValue,
knowledgeStrengths: knowledge
.map((entry) => entry.strengths)
.join(";"),
knowledgeImprovements: knowledge
.map((entry) => entry.AFIs)
.join(";"),
});
});
Vue.axios
.post(
"/observation",
@ -641,8 +557,8 @@ export default {
tutors: this.tutors,
person: this.whom,
type: this.type,
observed: this.scenarios.map(x => x.title).join(", "),
scenarios: JSON.parse(JSON.stringify(this.scenarios))
observed: this.scenarios.map((x) => x.title).join("; "),
scenarios: JSON.parse(JSON.stringify(transformedScenarioData)),
},
axiosConfig
)
@ -654,8 +570,8 @@ export default {
}
this.clearPassword();
form.classList.add("was-validated");
}
}
},
},
};
</script>
@ -698,7 +614,7 @@ export default {
}
.sidebar {
width: 160px; /* Set the width of the sidebar */
width: 8%; /* Set the width of the sidebar */
position: fixed; /* Fixed Sidebar (stay in place on scroll) */
z-index: 1; /* Stay on top */
top: 10%; /* Stay at the top */

View File

@ -148,102 +148,99 @@
</b-row>
<br />
<b-row class="mb-2">
<b-col class="centered-image" v-if="observation.monitoring">
<b-col class="centered-image">
<img
src="../assets/Monitoring.svg"
class="image-opacity"
v-bind:class="{ scorewarning: observation.monitoring < 2.5 }"
v-bind:class="{ scorewarning: observation.monitoring < 2.5 && observation.monitoring > 0}"
/>
<div class="image-centered-text">
<div class="image-centered-text" v-if="observation.monitoring > 0">
{{ observation.monitoring.toFixed(1) }}
</div>
</b-col>
<b-col
class="centered-image"
v-if="observation.controlProcedural"
>
<img
src="../assets/Control.svg"
class="image-opacity"
v-bind:class="{
scorewarning: observation.controlProcedural < 2.5
scorewarning: observation.controlProcedural < 2.5 && observation.controlProcedural > 0
}"
/>
<div class="image-centered-text">
<div class="image-centered-text" v-if="observation.controlProcedural > 0">
{{ observation.controlProcedural.toFixed(1) }}
</div>
</b-col>
<b-col class="centered-image" v-if="observation.control">
<b-col class="centered-image">
<img
src="../assets/Control.svg"
class="image-opacity"
v-bind:class="{ scorewarning: observation.control < 2.5 }"
v-bind:class="{ scorewarning: observation.control < 2.5 && observation.control > 0}"
/>
<div class="image-centered-text">
<div class="image-centered-text" v-if="observation.control > 0">
{{ observation.control.toFixed(1) }}
</div>
</b-col>
<b-col class="centered-image" v-if="observation.conservatism">
<b-col class="centered-image">
<img
src="../assets/Conservatism.svg"
class="image-opacity"
v-bind:class="{
scorewarning: observation.conservatism < 2.5
scorewarning: observation.conservatism < 2.5 && observation.conservatism > 0
}"
/>
<div class="image-centered-text">
<div class="image-centered-text" v-if="observation.conservatism > 0">
{{ observation.conservatism.toFixed(1) }}
</div>
</b-col>
<b-col
class="centered-image"
v-if="observation.teamworkCommunications"
>
<img
src="../assets/Teamwork.svg"
class="image-opacity"
v-bind:class="{
scorewarning: observation.teamworkCommunications < 2.5
scorewarning: observation.teamworkCommunications < 2.5 && observation.teamworkCommunications > 0
}"
/>
<div class="image-centered-text">
<div class="image-centered-text" v-if="observation.teamworkCommunications > 0">
{{ observation.teamworkCommunications.toFixed(1) }}
</div>
</b-col>
<b-col
class="centered-image"
v-if="observation.teamworkLeadership"
>
<img
src="../assets/Teamwork.svg"
class="image-opacity"
v-bind:class="{
scorewarning: observation.teamworkLeadership < 2.5
scorewarning: observation.teamworkLeadership < 2.5 && observation.teamworkLeadership > 0
}"
/>
<div class="image-centered-text">
<div class="image-centered-text" v-if="observation.teamworkLeadership > 0">
{{ observation.teamworkLeadership.toFixed(1) }}
</div>
</b-col>
<b-col class="centered-image" v-if="observation.teamworkWorkload">
<b-col class="centered-image">
<img
src="../assets/Teamwork.svg"
class="image-opacity"
v-bind:class="{
scorewarning: observation.teamworkWorkload < 2.5
scorewarning: observation.teamworkWorkload < 2.5 && observation.teamworkWorkload > 0
}"
/>
<div class="image-centered-text">
<div class="image-centered-text" v-if="observation.teamworkWorkload > 0">
{{ observation.teamworkWorkload.toFixed(1) }}
</div>
</b-col>
<b-col class="centered-image" v-if="observation.knowledge">
<b-col class="centered-image">
<img
src="../assets/Knowledge.svg"
class="image-opacity"
v-bind:class="{ scorewarning: observation.knowledge < 2.5 }"
v-bind:class="{ scorewarning: observation.knowledge < 2.5 && observation.knowledge > 0 }"
/>
<div class="image-centered-text">
<div class="image-centered-text" v-if="observation.knowledge > 0">
{{ observation.knowledge.toFixed(1) }}
</div>
</b-col>