diff options
16 files changed, 709 insertions, 207 deletions
diff --git a/api/src/main/java/med/voll/api/controller/AppointmentController.java b/api/src/main/java/med/voll/api/controller/AppointmentController.java index e5a2ba4..1344f20 100644 --- a/api/src/main/java/med/voll/api/controller/AppointmentController.java +++ b/api/src/main/java/med/voll/api/controller/AppointmentController.java @@ -3,12 +3,23 @@ package med.voll.api.controller; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import jakarta.validation.Valid; import med.voll.api.domain.appointment.*; +import med.voll.api.domain.doctor.Doctor; +import med.voll.api.domain.doctor.DoctorDetailingData; +import med.voll.api.domain.doctor.DoctorListingData; +import med.voll.api.domain.doctor.DoctorUpdateData; +import med.voll.api.domain.patient.Patient; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; import org.springframework.web.util.UriComponentsBuilder; +import javax.print.Doc; + @RestController @RequestMapping("/appointments") @SecurityRequirement(name = "bearer-key") @@ -23,6 +34,23 @@ public class AppointmentController { return ResponseEntity.ok(dto); } + @GetMapping + public ResponseEntity<Page<AppointmentDetailingData>> list(@PageableDefault(sort = {"date"}, direction = Sort.Direction.ASC) Pageable pagination) { + var page = appointmentsSchedule.list(pagination); + return ResponseEntity.ok(page); + } + + @PutMapping + @Transactional + public ResponseEntity update(@RequestBody @Valid AppointmentUpdateData data) throws Exception { + Appointment appointment = appointmentsSchedule.getReferenceById(data.id()); + Patient patient = appointmentsSchedule.getPatient(data.idPatient()); + Doctor doctor = appointmentsSchedule.getDoctor(data); + appointment.updateInformation(data, doctor, patient); + + return ResponseEntity.ok(new AppointmentListingData(appointment)); + } + @DeleteMapping @Transactional public ResponseEntity cancel(@RequestBody @Valid AppointmentDeletionData data) { @@ -30,5 +58,12 @@ public class AppointmentController { return ResponseEntity.noContent().build(); } + @GetMapping("/{id}") + public ResponseEntity detail(@PathVariable Long id) { + Appointment appointment = appointmentsSchedule.getReferenceById(id); + + return ResponseEntity.ok(new AppointmentListingData(appointment)); + } + } diff --git a/api/src/main/java/med/voll/api/controller/PatientController.java b/api/src/main/java/med/voll/api/controller/PatientController.java index 5675cfb..16c0973 100644 --- a/api/src/main/java/med/voll/api/controller/PatientController.java +++ b/api/src/main/java/med/voll/api/controller/PatientController.java @@ -2,6 +2,7 @@ package med.voll.api.controller; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import jakarta.validation.Valid; +import jakarta.validation.ValidationException; import med.voll.api.domain.patient.PatientListingData; import med.voll.api.domain.patient.Patient; import med.voll.api.domain.patient.PatientRepository; @@ -53,6 +54,10 @@ public class PatientController { @Transactional public ResponseEntity delete(@PathVariable Long id) { Patient patient = patientRepository.getReferenceById(id); + var appointments = patientRepository.findAnyAppointmentFrom(id); + if (appointments != null) { + throw new ValidationException("Can't delete patient with active appointments"); + } patient.delete(); return ResponseEntity.noContent().build(); diff --git a/api/src/main/java/med/voll/api/domain/appointment/Appointment.java b/api/src/main/java/med/voll/api/domain/appointment/Appointment.java index a48c15f..be236cf 100644 --- a/api/src/main/java/med/voll/api/domain/appointment/Appointment.java +++ b/api/src/main/java/med/voll/api/domain/appointment/Appointment.java @@ -8,6 +8,7 @@ import lombok.NoArgsConstructor; import med.voll.api.domain.doctor.Doctor; import med.voll.api.domain.patient.Patient; +import javax.print.Doc; import java.time.LocalDateTime; @Table(name = "appointments") @@ -44,4 +45,16 @@ public class Appointment { this.reasonForCancellation = reasonForCancellation; this.active = false; } + + public void updateInformation(AppointmentUpdateData data, Doctor doctor, Patient patient) { + if (doctor != null) { + this.doctor = doctor; + } + if (patient != null) { + this.patient = patient; + } + if (data.date() != null) { + this.date = data.date(); + } + } } diff --git a/api/src/main/java/med/voll/api/domain/appointment/AppointmentDetailingData.java b/api/src/main/java/med/voll/api/domain/appointment/AppointmentDetailingData.java new file mode 100644 index 0000000..c4e1dc5 --- /dev/null +++ b/api/src/main/java/med/voll/api/domain/appointment/AppointmentDetailingData.java @@ -0,0 +1,13 @@ +package med.voll.api.domain.appointment; + +import med.voll.api.domain.doctor.Specialty; + +import java.time.LocalDate; +import java.time.LocalTime; + +public record AppointmentDetailingData(Long id, LocalDate date, LocalTime time, String doctorName, Specialty specialty, String patientName) { + + public AppointmentDetailingData(Appointment appointment) { + this(appointment.getId(), appointment.getDate().toLocalDate(), appointment.getDate().toLocalTime(), appointment.getDoctor().getName(), appointment.getDoctor().getSpecialty(), appointment.getPatient().getName()); + } +} diff --git a/api/src/main/java/med/voll/api/domain/appointment/AppointmentUpdateData.java b/api/src/main/java/med/voll/api/domain/appointment/AppointmentUpdateData.java index c13712c..ddcde78 100644 --- a/api/src/main/java/med/voll/api/domain/appointment/AppointmentUpdateData.java +++ b/api/src/main/java/med/voll/api/domain/appointment/AppointmentUpdateData.java @@ -2,18 +2,16 @@ package med.voll.api.domain.appointment; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import med.voll.api.domain.doctor.Specialty; import java.time.LocalDateTime; public record AppointmentUpdateData( @NotNull Long id, - Integer year, - Integer month, - Integer day, - Integer hour, - Integer minute, - String doctor, - String patient + LocalDateTime date, + Long idDoctor, + Long idPatient, + Specialty specialty ) { } diff --git a/api/src/main/java/med/voll/api/domain/appointment/AppointmentsSchedule.java b/api/src/main/java/med/voll/api/domain/appointment/AppointmentsSchedule.java index fd7d4e8..3ad069e 100644 --- a/api/src/main/java/med/voll/api/domain/appointment/AppointmentsSchedule.java +++ b/api/src/main/java/med/voll/api/domain/appointment/AppointmentsSchedule.java @@ -5,8 +5,11 @@ import med.voll.api.domain.appointment.validations.cancellation.AppointmentCance import med.voll.api.domain.appointment.validations.scheduling.AppointmentSchedulingValidator; import med.voll.api.domain.doctor.Doctor; import med.voll.api.domain.doctor.DoctorRepository; +import med.voll.api.domain.patient.Patient; import med.voll.api.domain.patient.PatientRepository; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import java.util.List; @@ -49,6 +52,10 @@ public class AppointmentsSchedule { return new AppointmentListingData(appointment); } + public Page<AppointmentDetailingData> list(Pageable pagination) { + return appointmentRepository.findAllByActiveTrue(pagination).map(AppointmentDetailingData::new); + } + public void cancel(AppointmentDeletionData data) { if (!appointmentRepository.existsById(data.idAppointment())) { throw new ValidationException("Invalid appointment id"); @@ -72,4 +79,22 @@ public class AppointmentsSchedule { return doctorRepository.chooseRandomDoctorAvailable(data.specialty(), data.date()); } + public Appointment getReferenceById(Long id) { + return appointmentRepository.getReferenceById(id); + } + + public Doctor getDoctor(AppointmentUpdateData data) throws Exception { + if (data.idDoctor() != null) { + return doctorRepository.getReferenceById(data.idDoctor()); + } + Doctor doctor = doctorRepository.chooseRandomDoctorAvailable(data.specialty(), data.date()); + if (doctor == null) { + throw new ValidationException("No doctor available!"); + } + return doctor; + } + + public Patient getPatient(Long idPatient) { + return patientRepository.getReferenceById(idPatient); + } } diff --git a/api/src/main/java/med/voll/api/domain/patient/PatientRepository.java b/api/src/main/java/med/voll/api/domain/patient/PatientRepository.java index dbdcd87..c53d47a 100644 --- a/api/src/main/java/med/voll/api/domain/patient/PatientRepository.java +++ b/api/src/main/java/med/voll/api/domain/patient/PatientRepository.java @@ -15,4 +15,9 @@ public interface PatientRepository extends JpaRepository<Patient, Long> { where p.id = :id """) boolean findActiveById(Long id); + + @Query(""" + select p.id from Patient p join Appointment a on p.id = a.patient.id where a.active = true and p.id = :id + """) + Long findAnyAppointmentFrom(Long id); } diff --git a/web-client/voll/src/components/AppointmentModal.vue b/web-client/voll/src/components/AppointmentModal.vue new file mode 100644 index 0000000..451c039 --- /dev/null +++ b/web-client/voll/src/components/AppointmentModal.vue @@ -0,0 +1,123 @@ +<template> + <div class="none modal" id="modal" ref="modal"> + <div class="modal-content"> + <i class="fa-regular fa-user modal-icon"></i> + <Title text="Do you want to cancel this appointment?" Class="title large" /> + <div class="modal-dados" v-if="appointment"> + <Title id="modal-name" :text="appointment.date + ' at ' + appointment.time" Class="title large" /> + <div class="margin-top"> + <p class="title">{{ appointment.doctorName }}</p> + <p class="gray">{{ appointment.specialty }}</p> + <p class="title">{{ appointment.patientName }}</p> + <p class="gray">Patient</p> + </div> + <Fieldset> + <p>Select the reason for cancellation:</p> + <label class="field-label" for="reason">Reason</label> + <select id="reason" name="reason" v-model="appointment.reasonForCancellation" class="field-input"> + <option disabled value="">Select the reason</option> + <option value="PATIENT_GAVE_UP">Patient gave up</option> + <option value="LONG_WAITING_TIME">Long waiting time</option> + <option value="DOCTOR_CANCELED">Doctor canceled</option> + <option value="OTHER_REASON">Other reason</option> + </select> + <span class="error-message"></span> + </Fieldset> + </div> + <Button @click="closeModal($event)" @keyup.enter="closeModal($event)" id="modal-deactivate" + Class="btn form-btn" Value="Cancel appointment" /> + <ButtonReverse @click="closeModal($event)" @keyup.enter="closeModal($event)" + Class="btn-reverse form-btn" Value="Don't cancel" /> + </div> + </div> +</template> + +<script lang="ts"> +import IAppointment from '@/interfaces/IAppointment'; +import axios from 'axios'; +import { defineComponent } from 'vue'; +import Button from './Button.vue'; +import ButtonReverse from './ButtonReverse.vue'; +import Fieldset from './Fieldset.vue'; +import Title from './Title.vue'; + + +export default defineComponent({ + name: "Modal", + components: { + Title, + Button, + ButtonReverse, + Fieldset +}, + data() { + return { + token: sessionStorage.getItem('token'), + appointment: null as IAppointment + } + }, + emits: ['list', 'treatForbidden'], + props: { + page: { + type: String + } + }, + methods: { + closeModal(e) { + if (this.appointment.reasonForCancellation) { + const url = `http://localhost:8081/${this.page}` + axios.delete(url, { + headers: { + Authorization: `Bearer ${this.token}` + }, + data: { + idAppointment: this.appointment.id, + reasonForCancellation: this.appointment.reasonForCancellation + } + }).then(response => { + console.log(response) + }).catch(error => { + console.log(error) + }) + } + e.target.parentNode.parentNode.classList.add('none') + this.$emit('list') + }, + showModal(appointment) { + (this.$refs.modal as HTMLDivElement).classList.remove('none'); + this.appointment = appointment; + } + } +}) +</script> + +<style scoped> + +.modal-content { + background-color: #fefefe; + border: 1px solid #888; + border-radius: 1rem; + margin: 4% auto; + padding: 20px; + width: 80%; +} + +.title { + font-size: 1.1rem; + margin-bottom: 0; + font-weight: 600; +} + +.gray { + font-size: 1.05rem; + margin-top: 5px; +} + +.large { + font-size: 1.5rem; +} + +.margin-top { + margin-top: 2rem; +} +</style>
\ No newline at end of file diff --git a/web-client/voll/src/components/AppointmentsList.vue b/web-client/voll/src/components/AppointmentsList.vue new file mode 100644 index 0000000..3a6623b --- /dev/null +++ b/web-client/voll/src/components/AppointmentsList.vue @@ -0,0 +1,109 @@ +<template> + <SmallLogo /> + <Search :page="page" :entities="appointments" @filter="update" @list="list" /> + <ul class="list" id="list" v-if="appointments && appointments.length"> + <li class="list-item" v-for="appointment, index in appointments" :key="index"> + <h3 class="title">{{ appointment.date }} + <i @click="detail($event)" @keyup.enter="detail($event)" + class="fa-solid fa-caret-down fa-caret-up" tabindex="0"></i> + </h3> + <p class="gray">{{ appointment.time }}</p> + <p class="title">{{ appointment.doctorName }}</p> + <p class="gray">{{ appointment.specialty }}</p> + <p class="title">{{ appointment.patientName }}</p> + <p class="gray">Patient</p> + <div class="none"> + <router-link :to="`form/${page}/${appointment.id}`"> + <ButtonReverse Class="list-btn btn-reverse" Value="Edit"></ButtonReverse> + </router-link> + <ButtonReverse @click="showModal(appointment)" + @keyup.enter="showModal(appointment)" + Class="list-btn btn-reverse" Value="Cancel appointment"> + </ButtonReverse> + </div> + </li> + </ul> + <AppointmentModal ref="modal" :page="page" /> +</template> + +<script lang="ts"> +import axios from 'axios'; +import { defineComponent } from 'vue'; +import SmallLogo from './SmallLogo.vue'; +import ButtonReverse from './ButtonReverse.vue'; +import AppointmentModal from './AppointmentModal.vue'; +import Search from './Search.vue'; +import IAppointment from '@/interfaces/IAppointment'; + + +export default defineComponent({ + name: "List", + components: { + ButtonReverse, + AppointmentModal, + SmallLogo, + Search + }, + data() { + return { + page: this.$route.name as string, + token: sessionStorage.getItem('token'), + appointments: [] as IAppointment[] + } + }, + methods: { + update(filteredAppointments) { + this.appointments = filteredAppointments + }, + detail(event) { + event.target.classList.toggle('rotate') + const li = event.target.parentNode.parentNode + const divDetails = li.children[6] + divDetails.classList.toggle('none') + }, + list() { + const url = `http://localhost:8081/${this.page}` + axios.get(url, { + headers: { + Authorization: `Bearer ${this.token}` + } + }).then(response => { + this.appointments = response.data.content + this.appointments.forEach(appointment => { + appointment.date = (appointment.date.substring(5) + '/' + appointment.date.substring(0,4)).replaceAll('-','/') + appointment.time = appointment.time.substring(0,5) + appointment.specialty = appointment.specialty.charAt(0) + appointment.specialty.substring(1).toLowerCase() + }) + }).catch(error => { + console.log(error) + }) + }, + handleForbidden(code) { + if (code === 403) { + sessionStorage.clear() + this.$router.push('/login') + } + }, + showModal(appointment) { + (this.$refs.modal as typeof AppointmentModal).showModal(appointment) + } + }, + created() { + this.list() + } +}) +</script> + +<style scoped> + +.title { + font-size: 1.1rem; + margin-bottom: 0; + font-weight: 600; +} + +.gray { + font-size: 1.05rem; + margin-top: 5px; +} +</style>
\ No newline at end of file diff --git a/web-client/voll/src/components/List.vue b/web-client/voll/src/components/List.vue index ef6e25d..0ca646a 100644 --- a/web-client/voll/src/components/List.vue +++ b/web-client/voll/src/components/List.vue @@ -1,40 +1,40 @@ <template> <SmallLogo /> <Search :page="page" :entities="entities" @filter="update" @list="list" /> - <ul class="list" id="list" v-if="entities && entities.length"> - <li class="list-item" v-for="entity,index in entities" :key="index"> - <h3 class="title">{{ entity.name }} - <i @click="detail($event, entity.id, index)" @keyup.enter="detail($event, entity.id, index)" - class="fa-solid fa-caret-down fa-caret-up" tabindex="0"></i> - </h3> - <p v-if="page === 'doctors'">{{ - entity.specialty.charAt(0).toUpperCase() + - entity.specialty.toLowerCase().slice(1) - }}</p> - <p v-else-if="page === 'patients'">{{ entity.phone }}</p> - <div class="none"> - <p class="gray">{{ entity.email }}</p> - <p v-if="page === 'doctors'" class="gray">{{ entity.phone }}</p> - <p v-else-if="page === 'patients'" class="gray">{{ entity.ssn }}</p> - <p class="gray">{{ entity.address && entity.address.addressLine1 }}</p> - <p class="gray">{{ entity.address && entity.address.addressLine2 }}</p> - <p class="gray">{{ entity.address && entity.address.city }} {{ entity.address && entity.address.state }} - {{ entity.address && entity.address.postalCode }}</p> - <router-link :to="`form/${page}/${entity.id}`"> - <ButtonReverse Class="list-btn btn-reverse" Value="Edit"></ButtonReverse> - </router-link> - <ButtonReverse v-if="page === 'doctors'" @click="showModal(entity.id, entity.name, entity.specialty)" - @keyup.enter="showModal(entity.id, entity.name, entity.specialty)" Class="list-btn btn-reverse" - Value="Deactivate profile"> - </ButtonReverse> - <ButtonReverse v-else-if="page === 'patients'" @click="showModal(entity.id, entity.name, entity.phone)" - @keyup.enter="showModal(entity.id, entity.name, entity.phone)" Class="list-btn btn-reverse" - Value="Deactivate profile"> - </ButtonReverse> - </div> - </li> - </ul> - <Modal ref="modal" /> + <ul class="list" id="list" v-if="entities && entities.length"> + <li class="list-item" v-for="entity,index in entities" :key="index"> + <h3 class="title">{{ entity.name }} + <i @click="detail($event, entity.id, index)" @keyup.enter="detail($event, entity.id, index)" + class="fa-solid fa-caret-down fa-caret-up" tabindex="0"></i> + </h3> + <p v-if="page === 'doctors'">{{ + entity.specialty.charAt(0).toUpperCase() + + entity.specialty.toLowerCase().slice(1) + }}</p> + <p v-else-if="page === 'patients'">{{ entity.phone }}</p> + <div class="none"> + <p class="gray">{{ entity.email }}</p> + <p v-if="page === 'doctors'" class="gray">{{ entity.phone }}</p> + <p v-else-if="page === 'patients'" class="gray">{{ entity.ssn }}</p> + <p class="gray">{{ entity.address && entity.address.addressLine1 }}</p> + <p class="gray">{{ entity.address && entity.address.addressLine2 }}</p> + <p class="gray">{{ entity.address && entity.address.city }} {{ entity.address && entity.address.state }} + {{ entity.address && entity.address.postalCode }}</p> + <router-link :to="`form/${page}/${entity.id}`"> + <ButtonReverse Class="list-btn btn-reverse" Value="Edit"></ButtonReverse> + </router-link> + <ButtonReverse v-if="page === 'doctors'" @click="showModal(entity.id, entity.name, entity.specialty)" + @keyup.enter="showModal(entity.id, entity.name, entity.specialty)" Class="list-btn btn-reverse" + Value="Deactivate profile"> + </ButtonReverse> + <ButtonReverse v-else-if="page === 'patients'" @click="showModal(entity.id, entity.name, entity.phone)" + @keyup.enter="showModal(entity.id, entity.name, entity.phone)" Class="list-btn btn-reverse" + Value="Deactivate profile"> + </ButtonReverse> + </div> + </li> + </ul> + <Modal @list="list" ref="modal" :page="page"/> </template> <script lang="ts"> @@ -110,8 +110,6 @@ export default defineComponent({ } }).catch(error => { console.log(error) - const code = error.response.status - this.handleForbidden(code) }) }, handleForbidden(code) { diff --git a/web-client/voll/src/components/Modal.vue b/web-client/voll/src/components/Modal.vue index 917a09f..acd5c83 100644 --- a/web-client/voll/src/components/Modal.vue +++ b/web-client/voll/src/components/Modal.vue @@ -4,17 +4,26 @@ <i class="fa-regular fa-user modal-icon"></i> <Title text="Do you want to deactivate this profile?" Class="title large" /> <div class="modal-dados"> - <Title id="modal-name" ref="modalName" text="Name" Class="title" /> + <Title id="modal-name" ref="modalName" :text="name" Class="title" /> <div> <p id="modal-info" ref="modalInfo" class="gray"></p> </div> </div> <p>By deactivating this profile, your information will be disabled for searches and future appointments.</p> <p>Make sure there are no appointments scheduled, if so, the deactivation cannot be completed.</p> - <Button @click="closeModal($event)" @keyup.enter="closeModal($event)" id="modal-deactivate" + <Button @click="closeModal($event, true)" @keyup.enter="closeModal($event, true)" id="modal-deactivate" Class="btn form-btn" Value="Deactivate this profile" /> - <ButtonReverse @click="closeModal($event)" @keyup.enter="closeModal($event)" Class="btn-reverse form-btn" - Value="Cancel" /> + <ButtonReverse @click="closeModal($event, false)" @keyup.enter="closeModal($event, false)" + Class="btn-reverse form-btn" Value="Cancel" /> + </div> + </div> + <div class="none modal" id="modal2" ref="modal2"> + <div class="modal-content"> + <i class="fa-regular fa-user modal-icon"></i> + <Title text="It wasn't possible to deactivate the profile" Class="title large" /> + <p>Make sure there are no appointments scheduled, if so, the deactivation cannot be completed.</p> + <ButtonReverse @click="closeModal2($event)" @keyup.enter="closeModal2($event)" Class="btn-reverse form-btn" + Value="Got it!" /> </div> </div> </template> @@ -34,9 +43,11 @@ export default defineComponent({ Button, ButtonReverse }, - data () { + data() { return { - token: sessionStorage.getItem('token') + token: sessionStorage.getItem('token'), + id: null, + name: null } }, emits: ['list', 'treatForbidden'], @@ -46,33 +57,35 @@ export default defineComponent({ } }, methods: { - closeModal(e) { - e.target.parentNode.parentNode.classList.add('none') - }, - showModal(id, name, info) { - (this.$refs.modal as HTMLDivElement).classList.remove('none'); - (this.$refs.modalName as HTMLHeadingElement).innerHTML = name; - - if (this.page === 'doctors') { - (this.$refs.modalInfo as HTMLParagraphElement).innerHTML = info.charAt(0).toUpperCase() + info.toLowerCase().slice(1); - } else if (this.page === 'patients') { - (this.$refs.modalInfo as HTMLParagraphElement).innerHTML = info - } - - const deactivateElement = document.getElementById('modal-deactivate') - const url = `http://localhost:8081/${this.page}/${id}` - deactivateElement.onclick = () => { + closeModal(e, deleting) { + if (deleting) { + const url = `http://localhost:8081/${this.page}/${this.id}` axios.delete(url, { headers: { Authorization: `Bearer ${this.token}` } }).then(response => { console.log(response) - this.$emit('list') }).catch(error => { - this.$emit('treatForbidden', error.response.status) + console.log(error) + e.target.parentNode.parentNode.nextSibling.classList.remove('none') }) } + e.target.parentNode.parentNode.classList.add('none') + this.$emit('list') + }, + closeModal2(e) { + e.target.parentNode.parentNode.classList.add('none') + }, + showModal(id, name, info) { + (this.$refs.modal as HTMLDivElement).classList.remove('none'); + this.name = name; + this.id = id + if (this.page === 'doctors') { + (this.$refs.modalInfo as HTMLParagraphElement).innerHTML = info.charAt(0).toUpperCase() + info.toLowerCase().slice(1); + } else if (this.page === 'patients') { + (this.$refs.modalInfo as HTMLParagraphElement).innerHTML = info + } } } }) diff --git a/web-client/voll/src/components/Search.vue b/web-client/voll/src/components/Search.vue index a96ffc9..6512a25 100644 --- a/web-client/voll/src/components/Search.vue +++ b/web-client/voll/src/components/Search.vue @@ -32,15 +32,20 @@ export default defineComponent({ return } - const info = this.page === 'doctors' ? 'specialty' : 'phone' - - this.filteredEntities = this.entities.filter(entity => { - return entity.name.toLowerCase().indexOf(e.target.value.toLowerCase()) != -1 || - entity[info].toLowerCase().indexOf(e.target.value.toLowerCase()) != -1; - }) - this.filteredEntities.forEach(en => { - en.i = this.filteredEntities.indexOf(en) - }) + const info = this.page === 'doctors' ? 'specialty' : this.page === 'patients' ? 'phone' : null + + if (info) { + this.filteredEntities = this.entities.filter(entity => { + return entity.name.toLowerCase().indexOf(e.target.value.toLowerCase()) != -1 || + entity[info].toLowerCase().indexOf(e.target.value.toLowerCase()) != -1; + }) + } else { + this.filteredEntities = this.entities.filter(appointment => { + return appointment.doctorName.toLowerCase().indexOf(e.target.value.toLowerCase()) != -1 || + appointment.patientName.toLowerCase().indexOf(e.target.value.toLowerCase()) != -1 + }) + } + this.$emit('filter', this.filteredEntities) } diff --git a/web-client/voll/src/interfaces/IAppointment.ts b/web-client/voll/src/interfaces/IAppointment.ts new file mode 100644 index 0000000..e0bb503 --- /dev/null +++ b/web-client/voll/src/interfaces/IAppointment.ts @@ -0,0 +1,11 @@ +export default interface IAppointment { + patient: string, + patientName: string, + doctor: string, + doctorName: string, + specialty: string, + date: string, + time: string, + reasonForCancellation: string, + id: number +}
\ No newline at end of file diff --git a/web-client/voll/src/views/AppointmentsView.vue b/web-client/voll/src/views/AppointmentsView.vue index 21a164a..9615a70 100644 --- a/web-client/voll/src/views/AppointmentsView.vue +++ b/web-client/voll/src/views/AppointmentsView.vue @@ -1,3 +1,27 @@ <template> - <h1>Appointments</h1> -</template>
\ No newline at end of file + <Header/> + <main class="container"> + <Title text="Appointments" Class="title large"/> + <router-link to="/form/appointments/new" class="btn btn-register">Schedule new appointment</router-link> + <div> + <AppointmentsList /> + </div> + </main> +</template> + +<script lang="ts"> +import { defineComponent } from 'vue'; +import Header from '@/components/Header.vue'; +import Title from '../components/Title.vue'; +import AppointmentsList from '@/components/AppointmentsList.vue'; + + +export default defineComponent({ + name: 'AppointmentsView', + components: { + Header, + Title, + AppointmentsList +} +}) +</script>
\ No newline at end of file diff --git a/web-client/voll/src/views/FormView.vue b/web-client/voll/src/views/FormView.vue index 5900274..f2eef9b 100644 --- a/web-client/voll/src/views/FormView.vue +++ b/web-client/voll/src/views/FormView.vue @@ -24,20 +24,188 @@ </Fieldset> </div> - <div class="form-div occupational"> - <h3 v-if="typeIs === 'doctors'" class="title">Occupational</h3> - <h3 v-if="typeIs === 'patients'" class="title">Patient</h3> + <div class="entity" + v-if="typeIs === 'doctors' && entity.address || typeIs === 'patients' && entity.address"> + <div class="form-div occupational"> + <h3 v-if="typeIs === 'doctors'" class="title">Occupational</h3> + <h3 v-if="typeIs === 'patients'" class="title">Patient</h3> + <div class="form-pair"> + <Fieldset> + <label class="field-label" for="name">Name</label> + <input type="text" class="field-input" id="name" v-model="entity.name" required /> + <span class="error-message"></span> + </Fieldset> + <Fieldset v-if="typeIs === 'doctors'"> + <label class="field-label" for="specialty" required>Specialty</label> + <select id="specialty" name="specialty" class="field-input" v-model="entity.specialty" + required> + <option disabled value="">Select a specialty</option> + <option value="ANESTHESIOLOGY">ANESTHESIOLOGY</option> + <option value="CARDIOLOGY">CARDIOLOGY</option> + <option value="DERMATOLOGY">DERMATOLOGY</option> + <option value="GYNECOLOGY">GYNECOLOGY</option> + <option value="IMMUNOLOGY">IMMUNOLOGY</option> + <option value="NEUROLOGY">NEUROLOGY</option> + <option value="OPHTHALMOLOGY">OPHTHALMOLOGY</option> + <option value="ORTHOPEDICS">ORTHOPEDICS</option> + <option value="PATHOLOGY">PATHOLOGY</option> + <option value="PEDIATRICS">PEDIATRICS</option> + <option value="PSYCHIATRY">PSYCHIATRY</option> + <option value="RADIOLOGY">RADIOLOGY</option> + <option value="SURGERY">SURGERY</option> + <option value="UROLOGY">UROLOGY</option> + </select> + <span class="error-message"></span> + </Fieldset> + <Fieldset v-if="typeIs === 'patients'"> + <label class="field-label" for="ssn">SSN</label> + <input type="text" name="SSN" class="field-input" id="ssn" v-model="entity.ssn" v-maska + data-maska="###-##-####" minlength="11" maxlength="11" :disabled="editing" required /> + <span class="error-message"></span> + </Fieldset> + </div> + </div> + + <div class="form-div contact"> + <h3 class="title">Contact</h3> + <div class="form-pair"> + <Fieldset> + <label class="field-label" for="email">Email</label> + <input type="email" class="field-input" id="email" v-model="entity.email" + :disabled="editing" required /> + <span class="error-message"></span> + </Fieldset> + <Fieldset> + <label class="field-label" for="tel">Phone number</label> + <input type="tel" class="field-input" id="tel" v-maska data-maska="(###) ###-####" + placeholder="(206) 555-1212" v-model="entity.phone" minlength="14" maxlength="14" + required /> + <span class="error-message"></span> + </Fieldset> + </div> + </div> + + <div class="form-div address"> + <h3 class="title">Professional address</h3> + <Fieldset id="zip-field"> + <label class="field-label" for="zip">Zip code</label> + <input type="text" class="field-input" id="zip" v-model="entity.address.postalCode" + maxlength="5" required /> + <span class="error-message"></span> + </Fieldset> + <div class="form-pair"> + <Fieldset> + <label class="field-label" for="address1">Address line 1</label> + <input type="text" class="field-input" id="address1" v-model="entity.address.addressLine1" + required /> + <span class="error-message"></span> + </Fieldset> + <Fieldset> + <label class="field-label" for="address2">Address line 2</label> + <input type="text" class="field-input" id="address2" + v-model="entity.address.addressLine2" /> + <span class="error-message"></span> + </Fieldset> + </div> + <div class="form-pair"> + <Fieldset> + <label class="field-label" for="city">City</label> + <input type="text" class="field-input" id="city" v-model="entity.address.city" required /> + <span class="error-message"></span> + </Fieldset> + <Fieldset> + <label class="field-label" for="state">State</label> + <select id="state" name="state" v-model="entity.address.state" class="field-input" required> + <option disabled value="">Select a state</option> + <option value="AL">Alabama</option> + <option value="AK">Alaska</option> + <option value="AZ">Arizona</option> + <option value="AR">Arkansas</option> + <option value="CA">California</option> + <option value="CO">Colorado</option> + <option value="CT">Connecticut</option> + <option value="DE">Delaware</option> + <option value="DC">District Of Columbia</option> + <option value="FL">Florida</option> + <option value="GA">Georgia</option> + <option value="HI">Hawaii</option> + <option value="ID">Idaho</option> + <option value="IL">Illinois</option> + <option value="IN">Indiana</option> + <option value="IA">Iowa</option> + <option value="KS">Kansas</option> + <option value="KY">Kentucky</option> + <option value="LA">Louisiana</option> + <option value="ME">Maine</option> + <option value="MD">Maryland</option> + <option value="MA">Massachusetts</option> + <option value="MI">Michigan</option> + <option value="MN">Minnesota</option> + <option value="MS">Mississippi</option> + <option value="MO">Missouri</option> + <option value="MT">Montana</option> + <option value="NE">Nebraska</option> + <option value="NV">Nevada</option> + <option value="NH">New Hampshire</option> + <option value="NJ">New Jersey</option> + <option value="NM">New Mexico</option> + <option value="NY">New York</option> + <option value="NC">North Carolina</option> + <option value="ND">North Dakota</option> + <option value="OH">Ohio</option> + <option value="OK">Oklahoma</option> + <option value="OR">Oregon</option> + <option value="PA">Pennsylvania</option> + <option value="RI">Rhode Island</option> + <option value="SC">South Carolina</option> + <option value="SD">South Dakota</option> + <option value="TN">Tennessee</option> + <option value="TX">Texas</option> + <option value="UT">Utah</option> + <option value="VT">Vermont</option> + <option value="VA">Virginia</option> + <option value="WA">Washington</option> + <option value="WV">West Virginia</option> + <option value="WI">Wisconsin</option> + <option value="WY">Wyoming</option> + </select> + <span class="error-message"></span> + </Fieldset> + </div> + </div> + </div> + <div v-else-if="typeIs === 'appointments'" class="entity"> + <h3 class="title">People involved</h3> <div class="form-pair"> <Fieldset> - <label class="field-label" for="name">Name</label> - <input type="text" class="field-input" id="name" v-model="entity.name" required /> + <label class="field-label" for="patient">Patient</label> + <select id="patient" name="patient" v-model="appointment.patientName" class="field-input" + required> + <option disabled value="">Select a patient</option> + <option v-for="patient, index in patients" :key="index" :value="patient.id">{{ patient.name + }}</option> + </select> <span class="error-message"></span> </Fieldset> - <Fieldset v-if="typeIs === 'doctors'"> - <label class="field-label" for="specialty" required>Specialty</label> - <select id="specialty" name="specialty" class="field-input" v-model="entity.specialty" - required> + <Fieldset v-if="!appointment.specialty"> + <label class="field-label" for="doctor">Doctor</label> + <select id="doctor" name="doctor" v-model="appointment.doctorName" class="field-input"> + <option disabled value="">Select a doctor</option> + <option value=""></option> + <option v-for="doctor, index in doctors" :key="index" :value="doctor.id">{{ doctor.name + + ' (' + doctor.specialty + ')' }}</option> + </select> + <span class="error-message"></span> + </Fieldset> + + </div> + <div v-if="!appointment.doctorName"> + <h3 class="title">Random doctor</h3> + <Fieldset> + <label class="field-label" for="specialty">Specialty</label> + <select id="specialty" name="specialty" v-model="appointment.specialty" class="field-input"> <option disabled value="">Select a specialty</option> + <option value=""></option> <option value="ANESTHESIOLOGY">ANESTHESIOLOGY</option> <option value="CARDIOLOGY">CARDIOLOGY</option> <option value="DERMATOLOGY">DERMATOLOGY</option> @@ -55,117 +223,19 @@ </select> <span class="error-message"></span> </Fieldset> - <Fieldset v-if="typeIs === 'patients'"> - <label class="field-label" for="ssn">SSN</label> - <input type="text" name="SSN" class="field-input" id="ssn" v-model="entity.ssn" v-maska - data-maska="###-##-####" minlength="11" maxlength="11" :disabled="editing" required /> - <span class="error-message"></span> - </Fieldset> - </div> - </div> - - <div class="form-div contact"> - <h3 class="title">Contact</h3> - <div class="form-pair"> - <Fieldset> - <label class="field-label" for="email">Email</label> - <input type="email" class="field-input" id="email" v-model="entity.email" - :disabled="editing" required /> - <span class="error-message"></span> - </Fieldset> - <Fieldset> - <label class="field-label" for="tel">Phone number</label> - <input type="tel" class="field-input" id="tel" v-maska data-maska="(###) ###-####" - placeholder="(206) 555-1212" v-model="entity.phone" minlength="14" maxlength="14" - required /> - <span class="error-message"></span> - </Fieldset> - </div> - </div> - - <div class="form-div address"> - <h3 class="title">Professional address</h3> - <Fieldset id="zip-field"> - <label class="field-label" for="zip">Zip code</label> - <input type="text" class="field-input" id="zip" v-model="entity.address.postalCode" - maxlength="5" required /> - <span class="error-message"></span> - </Fieldset> - <div class="form-pair"> - <Fieldset> - <label class="field-label" for="address1">Address line 1</label> - <input type="text" class="field-input" id="address1" v-model="entity.address.addressLine1" - required /> - <span class="error-message"></span> - </Fieldset> - <Fieldset> - <label class="field-label" for="address2">Address line 2</label> - <input type="text" class="field-input" id="address2" - v-model="entity.address.addressLine2" /> - <span class="error-message"></span> - </Fieldset> </div> + <h3 class="title">Date and time</h3> <div class="form-pair"> <Fieldset> - <label class="field-label" for="city">City</label> - <input type="text" class="field-input" id="city" v-model="entity.address.city" required /> + <label class="field-label" for="date">Date</label> + <input type="date" class="field-input" id="date" v-model="appointment.date" required /> <span class="error-message"></span> </Fieldset> <Fieldset> - <label class="field-label" for="state">State</label> - <select id="state" name="state" v-model="entity.address.state" class="field-input" required> - <option disabled value="">Select a state</option> - <option value="AL">Alabama</option> - <option value="AK">Alaska</option> - <option value="AZ">Arizona</option> - <option value="AR">Arkansas</option> - <option value="CA">California</option> - <option value="CO">Colorado</option> - <option value="CT">Connecticut</option> - <option value="DE">Delaware</option> - <option value="DC">District Of Columbia</option> - <option value="FL">Florida</option> - <option value="GA">Georgia</option> - <option value="HI">Hawaii</option> - <option value="ID">Idaho</option> - <option value="IL">Illinois</option> - <option value="IN">Indiana</option> - <option value="IA">Iowa</option> - <option value="KS">Kansas</option> - <option value="KY">Kentucky</option> - <option value="LA">Louisiana</option> - <option value="ME">Maine</option> - <option value="MD">Maryland</option> - <option value="MA">Massachusetts</option> - <option value="MI">Michigan</option> - <option value="MN">Minnesota</option> - <option value="MS">Mississippi</option> - <option value="MO">Missouri</option> - <option value="MT">Montana</option> - <option value="NE">Nebraska</option> - <option value="NV">Nevada</option> - <option value="NH">New Hampshire</option> - <option value="NJ">New Jersey</option> - <option value="NM">New Mexico</option> - <option value="NY">New York</option> - <option value="NC">North Carolina</option> - <option value="ND">North Dakota</option> - <option value="OH">Ohio</option> - <option value="OK">Oklahoma</option> - <option value="OR">Oregon</option> - <option value="PA">Pennsylvania</option> - <option value="RI">Rhode Island</option> - <option value="SC">South Carolina</option> - <option value="SD">South Dakota</option> - <option value="TN">Tennessee</option> - <option value="TX">Texas</option> - <option value="UT">Utah</option> - <option value="VT">Vermont</option> - <option value="VA">Virginia</option> - <option value="WA">Washington</option> - <option value="WV">West Virginia</option> - <option value="WI">Wisconsin</option> - <option value="WY">Wyoming</option> + <label class="field-label" for="time">Time</label> + <select id="time" name="time" class="field-input" v-model="appointment.time" required> + <option disabled value="">Select the time</option> + <option v-for="time, index in timeAvailable" :key="index" :value="time">{{ time }}</option> </select> <span class="error-message"></span> </Fieldset> @@ -173,8 +243,14 @@ </div> <div v-if="!editing"> - <input @click.prevent="submit($event)" @keyup.enter.prevent="submit($event)" - value="Register profile" class="btn form-btn" id="send" type="submit"> + <div v-if="typeIs === 'appointments'"> + <input @click.prevent="submit($event)" @keyup.enter.prevent="submit($event)" + value="Schedule appointment" class="btn form-btn margin-top" id="send" type="submit"> + </div> + <div v-else> + <input @click.prevent="submit($event)" @keyup.enter.prevent="submit($event)" + value="Register profile" class="btn form-btn" id="send" type="submit"> + </div> </div> <div v-else> <div v-if="typeIs === 'doctors'"> @@ -186,19 +262,17 @@ </div> <div v-if="typeIs === 'patients'"> <p @click="showModal(entity.id, entity.name, entity.ssn)" - @keyup.enter="showModal(entity.id, entity.name, entity.ssn)" tabindex="0" - class="deactivate"> + @keyup.enter="showModal(entity.id, entity.name, entity.ssn)" tabindex="0" class="deactivate"> Deactivate profile</p> </div> - <Modal ref="modal" /> + <Modal ref="modal" :page="typeIs"/> <div> - <input @click.prevent="submit($event)" @keyup.enter.prevent="submit($event)" - value="Edit profile" class="btn form-btn" id="send" type="submit"> + <input @click.prevent="submit($event)" @keyup.enter.prevent="submit($event)" value="Edit profile" + class="btn form-btn" id="send" type="submit"> </div> </div> <div> - <router-link to="/"><input class="btn-reverse form-btn" value="Cancel" - type="button" /></router-link> + <router-link to="/"><input class="btn-reverse form-btn" value="Cancel" type="button" /></router-link> </div> </form> </main> @@ -217,6 +291,7 @@ import Header from '@/components/Header.vue'; import IDoctor from '@/interfaces/IDoctor'; import IPatient from '@/interfaces/IPatient'; import IAddress from '@/interfaces/IAddress'; +import IAppointment from '@/interfaces/IAppointment'; import handleBadRequest from '@/utilities/handleBadRequest'; export default defineComponent({ @@ -240,12 +315,16 @@ export default defineComponent({ idIs: this.$route.params.id as string, token: sessionStorage.getItem('token'), phoneMask: new Mask({ mask: "(###) ###-####" }), - ssnMask: new Mask({ mask: "###-##-####" }) + ssnMask: new Mask({ mask: "###-##-####" }), + appointment: {} as IAppointment, + timeAvailable: [] as string[], + doctors: [] as IDoctor[], + patients: [] as IPatient[] } }, methods: { - showModal(id, name, specialty) { - (this.$refs.modal as typeof Modal).showModal(id, name, specialty) + showModal(id, name, specialtyOrSSN) { + (this.$refs.modal as typeof Modal).showModal(id, name, specialtyOrSSN) }, submit(e) { e.target.blur() @@ -258,6 +337,15 @@ export default defineComponent({ addressData: this.entity.address } + const date = this.appointment.date + 'T' + this.appointment.time + ':00' + + const dataAppointment = { + idPatient: this.appointment.patientName, + ...(this.appointment.doctorName) && { idDoctor: this.appointment.doctorName }, + ...(!this.appointment.doctorName) && { specialty: this.appointment.specialty }, + date: date, + } + let method = 'post' if (this.editing) { method = 'put' @@ -266,7 +354,8 @@ export default defineComponent({ const url = `http://localhost:8081/${this.typeIs}` axios[method](url, { ...(this.editing) && { id: this.idIs }, - ...data + ...(this.typeIs === 'patients' || this.typeIs === 'doctors') && { ...data }, + ...(this.typeIs === 'appointments') && { ...dataAppointment } }, { headers: { Authorization: `Bearer ${this.token}` @@ -279,12 +368,36 @@ export default defineComponent({ .catch(error => { const code = error.response.status const errors = error.response.data + console.log(error.response) this.handleBadRequest(code, errors) }) }, - handleBadRequest + handleBadRequest, + getEntities() { + ['doctors', 'patients'].forEach(element => { + const url = `http://localhost:8081/${element}` + axios.get(url, { + headers: { + Authorization: `Bearer ${this.token}` + } + }).then(response => { + this[element] = response.data.content + if (element === 'patients') { + this[element].forEach(entity => { + entity.phone = this.phoneMask.masked(entity.phone) + }) + } + }).catch(error => { + console.log(error) + }) + }) + } }, created() { + for (let hours = 7; hours < 19; hours++) { + this.timeAvailable.push(hours + ':00') + } + this.getEntities() this.entity.address = {} as IAddress this.entity.ssn = '' this.entity.phone = '' @@ -300,6 +413,14 @@ export default defineComponent({ } }) .then(response => { + if (this.typeIs === 'appointments') { + this.appointment = response.data + this.appointment.time = response.data.date.substring(11, 16) + this.appointment.date = this.appointment.date.substring(0, 10) + this.appointment.doctorName = response.data.idDoctor + this.appointment.patientName = response.data.idPatient + return + } this.entity = response.data if (this.typeIs === 'patients') { @@ -334,6 +455,10 @@ export default defineComponent({ font-weight: 500; } +.margin-top { + margin-top: 2rem; +} + @media screen and (max-width: 900px) { .block { display: inline; diff --git a/web-client/voll/tsconfig.json b/web-client/voll/tsconfig.json index 1048ed4..d8529f7 100644 --- a/web-client/voll/tsconfig.json +++ b/web-client/voll/tsconfig.json @@ -39,4 +39,4 @@ "exclude": [ "node_modules" ] -} +}
\ No newline at end of file |
