mirror of
https://github.com/FH-Complete/FHC-Core.git
synced 2026-10-12 15:04:04 +00:00
coodle survey edit form
This commit is contained in:
@@ -33,6 +33,10 @@ html {
|
||||
background-color: var(--fhc-primary) !important;
|
||||
}
|
||||
|
||||
.fhc-primary-border-color {
|
||||
border-color: var(--fhc-primary) !important;
|
||||
}
|
||||
|
||||
.fhc-primary-highlight-bg {
|
||||
background-color: var(--fhc-primary-highlight) !important;
|
||||
}
|
||||
@@ -860,4 +864,12 @@ html {
|
||||
border: 1px solid rgba(0, 0, 0, 0.15);
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
#coodleParticipantsSearchResults {
|
||||
background-color: var(--fhc-background);
|
||||
border: 1px solid var(--fhc-border);
|
||||
}
|
||||
.coodleParticipantSearchResult:hover {
|
||||
opacity: 80%;
|
||||
}
|
||||
@@ -1,14 +1,20 @@
|
||||
import CoodleSurveyVotingTable from "./CoodleSurveyVotingTable.js";
|
||||
import CoodleSurveyVotingTable from "./CoodleSurvey/CoodleSurveyVotingTable.js";
|
||||
import CoodleSurveyCalendar from "./CoodleSurvey/CoodleSurveyCalendar.js";
|
||||
import CoodleSurveyParticipants from "./CoodleSurvey/CoodleSurveyParticipants.js";
|
||||
import CoodleSurveyDurationSelector from "./CoodleSurvey/CoodleSurveyDurationSelector.js";
|
||||
import CoodleSurveyBasicInfo from "./CoodleSurvey/CoodleSurveyBasicInfo.js";
|
||||
|
||||
import {
|
||||
formatDate,
|
||||
numberPadding,
|
||||
addMinutesToDate,
|
||||
} from "../../../../helpers/DateHelpers.js";
|
||||
import { formatDate, numberPadding } from "../../../../helpers/DateHelpers.js";
|
||||
|
||||
export default {
|
||||
name: "CoodleSurvey",
|
||||
components: { CoodleSurveyVotingTable },
|
||||
components: {
|
||||
CoodleSurveyVotingTable,
|
||||
CoodleSurveyCalendar,
|
||||
CoodleSurveyParticipants,
|
||||
CoodleSurveyDurationSelector,
|
||||
CoodleSurveyBasicInfo,
|
||||
},
|
||||
props: {
|
||||
survey: { type: Object | null },
|
||||
uid: { type: String | null },
|
||||
@@ -75,57 +81,6 @@ export default {
|
||||
return "New survey";
|
||||
}
|
||||
},
|
||||
formattedSurveyDescription() {
|
||||
return this.$props.survey?.description?.replaceAll("\n", "<br>");
|
||||
},
|
||||
formattedSurveyTimeslotDuration() {
|
||||
const minutes = this.$props.survey.timeslotDuration % 60;
|
||||
const hours = (this.$props.survey.timeslotDuration - minutes) / 60;
|
||||
let formattedDuration = hours ? hours + " hr " : "";
|
||||
formattedDuration += minutes + " min";
|
||||
return formattedDuration;
|
||||
},
|
||||
formattedSurveyEndsAt() {
|
||||
if (this.$props.survey?.endsAt) {
|
||||
return formatDate(this.$props.survey.endsAt);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
formattedSurveyCreatedAt() {
|
||||
if (this.$props.survey?.createdAt) {
|
||||
return formatDate(this.$props.survey.createdAt);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
formattedSurveyUpdatedAt() {
|
||||
if (this.$props.survey?.updatedAt) {
|
||||
return formatDate(this.$props.survey.updatedAt);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
formattedSurveyCanceledAt() {
|
||||
if (this.$props.survey?.canceledAt) {
|
||||
return formatDate(this.$props.survey.canceledAt);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
formattedSurveyCompletedAt() {
|
||||
if (this.$props.survey?.completedAt) {
|
||||
return formatDate(this.$props.survey.completedAt);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
surveyCreatorProfileHref() {
|
||||
return this.$router.resolve({
|
||||
name: "ProfilView",
|
||||
params: { uid: this.survey?.creator?.uid },
|
||||
}).href;
|
||||
},
|
||||
parsedSelectedTimeslot() {
|
||||
return this.$props.survey?.selectedTimeslotId
|
||||
? this.parseTimeslot(
|
||||
@@ -149,7 +104,7 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
survey: {
|
||||
handler() {
|
||||
async handler() {
|
||||
this.setSurveyFormData();
|
||||
if (!this.survey?.id) {
|
||||
this.isEditInProgress = true;
|
||||
@@ -157,38 +112,52 @@ export default {
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
isEditInProgress() {
|
||||
if (!this.isEditInProgress) {
|
||||
this.setSurveyFormData();
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setSurveyFormData() {
|
||||
if (this.$props.survey) {
|
||||
this.surveyFormData = { ...this.$props.survey };
|
||||
return;
|
||||
} else {
|
||||
let defaultEndsAtDate = new Date();
|
||||
defaultEndsAtDate.setDate(defaultEndsAtDate.getDate() + 7);
|
||||
|
||||
this.surveyFormData = {
|
||||
id: null,
|
||||
creator: null,
|
||||
title: "",
|
||||
description: "",
|
||||
timeslotDuration: 60,
|
||||
maxSelections: 1,
|
||||
areParticipantsAnonymized: false,
|
||||
areSelectionsAnonymized: false,
|
||||
selectedTimeslot: null,
|
||||
endsAt: defaultEndsAtDate.toISOString().slice(0, 10),
|
||||
completedAt: null,
|
||||
canceledAt: null,
|
||||
updatedAt: null,
|
||||
createdAt: null,
|
||||
timeslots: [],
|
||||
participants: [],
|
||||
};
|
||||
}
|
||||
|
||||
this.surveyFormData = {
|
||||
id: null,
|
||||
creator: null,
|
||||
title: "",
|
||||
description: "",
|
||||
timeslotDuration: 60,
|
||||
maxSelections: 1,
|
||||
areParticipantsAnonymized: false,
|
||||
areSelectionsAnonymized: false,
|
||||
selectedTimeslot: null,
|
||||
endsAt: null, //todo: set to current date + 7
|
||||
completedAt: null,
|
||||
canceledAt: null,
|
||||
updatedAt: null,
|
||||
createdAt: null,
|
||||
timeslots: [],
|
||||
participants: [],
|
||||
};
|
||||
this.surveyFormData.participants.forEach(
|
||||
(participant) => {
|
||||
participant.isCalendarShown = false;
|
||||
},
|
||||
);
|
||||
},
|
||||
parseTimeslot(timeslot) {
|
||||
const timeslotStartsAt = new Date(timeslot.startsAt + " UTC");
|
||||
const timeslotEndsAt = addMinutesToDate(
|
||||
timeslotStartsAt,
|
||||
this.$props.survey?.timeslotDuration ?? 0,
|
||||
let timeslotEndsAt = new Date(timeslot.startsAt + " UTC");
|
||||
timeslotEndsAt.setMinutes(
|
||||
timeslotEndsAt.getMinutes() +
|
||||
(this.$props.survey?.timeslotDuration ?? 0),
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -224,6 +193,10 @@ export default {
|
||||
console.log("canceling...");
|
||||
// todo
|
||||
},
|
||||
submitForm() {
|
||||
console.log("submitting...");
|
||||
// todo
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.setSurveyFormData();
|
||||
@@ -269,91 +242,65 @@ export default {
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div v-if="!isEditInProgress" class="d-flex flex-column gap-3">
|
||||
<span v-if="$props.survey?.completedAt" class="fst-italic">
|
||||
{{ "This survey was completed on " + formattedSurveyCompletedAt + ". " }}
|
||||
<span v-if="$props.survey?.selectedTimeslotId">
|
||||
{{
|
||||
"The timeslot (((timeslot))) was selected.".replace(
|
||||
"(((timeslot)))",
|
||||
parsedSelectedTimeslot.weekday +
|
||||
" " +
|
||||
parsedSelectedTimeslot.fullDate +
|
||||
" " +
|
||||
parsedSelectedTimeslot.startTime +
|
||||
"-" +
|
||||
parsedSelectedTimeslot.endTime
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<span v-else>{{ "No timeslot was selected." }}</span>
|
||||
</span>
|
||||
<span v-else-if="$props.survey?.canceledAt" class="fst-italic">
|
||||
{{ "This survey was canceled on " + formattedSurveyCanceledAt + "." }}
|
||||
</span>
|
||||
<span
|
||||
v-if="$props.survey?.description?.length"
|
||||
class="text-wrap"
|
||||
v-html="formattedSurveyDescription"
|
||||
></span>
|
||||
<div class="d-flex flex-column gap-1">
|
||||
<div>
|
||||
{{ "Created by " }}
|
||||
<a :href="surveyCreatorProfileHref" :target="'_blank'">{{ this.$props.survey?.creator?.name }}</a>
|
||||
{{ " on " + formattedSurveyCreatedAt}}
|
||||
</div>
|
||||
<div>{{ "Last edited on " + formattedSurveyUpdatedAt }}</div>
|
||||
<div>{{ "Planned to end on " + formattedSurveyEndsAt }}</div>
|
||||
</div>
|
||||
<div class="d-flex flex-column gap-1">
|
||||
<div class="d-flex flex-row">
|
||||
<div class="d-flex flex-row justify-content-center align-items-center" style="width: 30px;">
|
||||
<i class="fa-solid fa-clock fa-lg"></i>
|
||||
</div>
|
||||
<span>
|
||||
<span class="fw-bold">{{ "Proposed appointment duration: " }}</span>
|
||||
{{ formattedSurveyTimeslotDuration }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="d-flex flex-row">
|
||||
<div class="d-flex flex-row justify-content-center align-items-center" style="width: 30px;">
|
||||
<i class="fa-solid fa-calendar-check fa-lg"></i>
|
||||
</div>
|
||||
<span>
|
||||
<span class="fw-bold">{{ "Maximum selectable timeslots: " }}</span>
|
||||
{{ $props.survey.maxSelections}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="d-flex flex-row">
|
||||
<div class="d-flex flex-row justify-content-center align-items-center" style="width: 30px;">
|
||||
<i class="fa-solid fa-user-slash fa-lg"></i>
|
||||
</div>
|
||||
<span>
|
||||
<span class="fw-bold">{{ "Are participants anonymized: " }}</span>
|
||||
{{ $props.survey.areParticipantsAnonymized ? "yes" : "no" }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="d-flex flex-row">
|
||||
<div class="d-flex flex-row justify-content-center align-items-center" style="width: 30px;">
|
||||
<i class="fa-solid fa-person-booth fa-lg"></i>
|
||||
</div>
|
||||
<span>
|
||||
<span class="fw-bold">{{ "Are votes anonymized: " }}</span>
|
||||
{{ $props.survey.areSelectionsAnonymized ? "yes" : "no" }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="my-2">
|
||||
<div class="d-flex flex-column gap-2">
|
||||
<div class="d-flex flex-column gap-3">
|
||||
<coodle-survey-basic-info
|
||||
v-if="$props.survey"
|
||||
v-model:surveyFormDataModelValue="surveyFormData"
|
||||
:survey="$props.survey"
|
||||
:parsedSelectedTimeslot="parsedSelectedTimeslot"
|
||||
:isEditInProgress="isEditInProgress"
|
||||
/>
|
||||
<hr>
|
||||
<div v-if="!isEditInProgress" class="d-flex flex-column gap-2">
|
||||
<coodle-survey-voting-table
|
||||
v-if="$props.survey"
|
||||
:uid="$props.uid"
|
||||
:survey="$props.survey"
|
||||
:timeslots="parsedTimeslotsForVotingTable"
|
||||
:survey="$props.survey"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="d-flex flex-column gap-3">
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-4">
|
||||
<coodle-survey-participants
|
||||
v-if="surveyFormData?.participants"
|
||||
v-model:participantsModelValue="surveyFormData.participants"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-lg-8">
|
||||
<div class="d-flex flex-column gap-3">
|
||||
<coodle-survey-duration-selector
|
||||
v-if="surveyFormData?.timeslotDuration"
|
||||
v-model:durationModelValue="surveyFormData.timeslotDuration"
|
||||
:survey="$props.survey"
|
||||
/>
|
||||
<coodle-survey-calendar />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-row gap-2 justify-content-end align-items-start px-0">
|
||||
<div>
|
||||
<div
|
||||
@click="isEditInProgress = false"
|
||||
:class="isDarkMode ? 'btn-outline-light' : 'btn-outline-dark'"
|
||||
class="btn text-nowrap"
|
||||
>
|
||||
{{ "Cancel" }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
@click="submitForm()"
|
||||
:class="isDarkMode ? 'btn-light' : 'btn-dark'"
|
||||
class="btn text-nowrap"
|
||||
>
|
||||
{{ "Save" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`,
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
import { formatDate } from "../../../../../helpers/DateHelpers.js";
|
||||
|
||||
export default {
|
||||
name: "CoodleSurveyBasicInfo",
|
||||
props: {
|
||||
survey: Object | null,
|
||||
surveyFormDataModelValue: Object | null,
|
||||
parsedSelectedTimeslot: Object | null,
|
||||
isEditInProgress: Boolean,
|
||||
},
|
||||
emits: ["update:surveyFormDataModelValue"],
|
||||
computed: {
|
||||
formattedSurveyDescription() {
|
||||
return this.$props.survey?.description?.replaceAll("\n", "<br>");
|
||||
},
|
||||
formattedSurveyTimeslotDuration() {
|
||||
const minutes = this.$props.survey.timeslotDuration % 60;
|
||||
const hours = (this.$props.survey.timeslotDuration - minutes) / 60;
|
||||
let formattedDuration = hours ? hours + " hr " : "";
|
||||
formattedDuration += minutes + " min";
|
||||
return formattedDuration;
|
||||
},
|
||||
formattedSurveyEndsAt() {
|
||||
if (this.$props.survey?.endsAt) {
|
||||
return formatDate(this.$props.survey.endsAt);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
formattedSurveyCreatedAt() {
|
||||
if (this.$props.survey?.createdAt) {
|
||||
return formatDate(this.$props.survey.createdAt);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
formattedSurveyUpdatedAt() {
|
||||
if (this.$props.survey?.updatedAt) {
|
||||
return formatDate(this.$props.survey.updatedAt);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
formattedSurveyCanceledAt() {
|
||||
if (this.$props.survey?.canceledAt) {
|
||||
return formatDate(this.$props.survey.canceledAt);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
formattedSurveyCompletedAt() {
|
||||
if (this.$props.survey?.completedAt) {
|
||||
return formatDate(this.$props.survey.completedAt);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
surveyCreatorProfileHref() {
|
||||
return this.$router.resolve({
|
||||
name: "ProfilView",
|
||||
params: { uid: this.survey?.creator?.uid },
|
||||
}).href;
|
||||
},
|
||||
surveyFormData: {
|
||||
get() {
|
||||
return this.$props.surveyFormDataModelValue;
|
||||
},
|
||||
set(newValue) {
|
||||
this.$emit("update:surveyFormDataModelValue", newValue);
|
||||
},
|
||||
},
|
||||
},
|
||||
template: /*html*/ `
|
||||
<div class="d-flex flex-column gap-3">
|
||||
<template v-if="!$props.isEditInProgress">
|
||||
<span v-if="$props.survey?.completedAt" class="fst-italic">
|
||||
{{ "This survey was completed on " + formattedSurveyCompletedAt + ". " }}
|
||||
<span v-if="$props.survey?.selectedTimeslotId">
|
||||
{{
|
||||
"The timeslot (((timeslot))) was selected.".replace(
|
||||
"(((timeslot)))",
|
||||
parsedSelectedTimeslot?.weekday +
|
||||
" " +
|
||||
parsedSelectedTimeslot?.fullDate +
|
||||
" " +
|
||||
parsedSelectedTimeslot?.startTime +
|
||||
"-" +
|
||||
parsedSelectedTimeslot?.endTime
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<span v-else>{{ "No timeslot was selected." }}</span>
|
||||
</span>
|
||||
<span v-else-if="$props.survey?.canceledAt" class="fst-italic">
|
||||
{{ "This survey was canceled on " + formattedSurveyCanceledAt + "." }}
|
||||
</span>
|
||||
<span
|
||||
v-if="$props.survey?.description?.length"
|
||||
class="text-wrap"
|
||||
v-html="formattedSurveyDescription"
|
||||
></span>
|
||||
<div class="d-flex flex-column gap-1">
|
||||
<div>
|
||||
{{ "Created by " }}
|
||||
<a :href="surveyCreatorProfileHref" :target="'_blank'">{{ this.$props.survey?.creator?.name }}</a>
|
||||
{{ " on " + formattedSurveyCreatedAt}}
|
||||
</div>
|
||||
<div>{{ "Last edited on " + formattedSurveyUpdatedAt }}</div>
|
||||
<div>{{ "Planned to end on " + formattedSurveyEndsAt }}</div>
|
||||
</div>
|
||||
<div class="d-flex flex-column gap-1">
|
||||
<div class="d-flex flex-row">
|
||||
<div class="d-flex flex-row justify-content-center align-items-center" style="width: 30px;">
|
||||
<i class="fa-solid fa-clock fa-lg"></i>
|
||||
</div>
|
||||
<span>
|
||||
<span class="fw-bold">{{ "Proposed appointment duration: " }}</span>
|
||||
{{ formattedSurveyTimeslotDuration }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="d-flex flex-row">
|
||||
<div class="d-flex flex-row justify-content-center align-items-center" style="width: 30px;">
|
||||
<i class="fa-solid fa-calendar-check fa-lg"></i>
|
||||
</div>
|
||||
<span>
|
||||
<span class="fw-bold">{{ "Maximum selectable timeslots: " }}</span>
|
||||
{{ $props.survey.maxSelections}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="d-flex flex-row">
|
||||
<div class="d-flex flex-row justify-content-center align-items-center" style="width: 30px;">
|
||||
<i class="fa-solid fa-user-slash fa-lg"></i>
|
||||
</div>
|
||||
<span>
|
||||
<span class="fw-bold">{{ "Are participants anonymized: " }}</span>
|
||||
{{ $props.survey.areParticipantsAnonymized ? "yes" : "no" }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="d-flex flex-row">
|
||||
<div class="d-flex flex-row justify-content-center align-items-center" style="width: 30px;">
|
||||
<i class="fa-solid fa-person-booth fa-lg"></i>
|
||||
</div>
|
||||
<span>
|
||||
<span class="fw-bold">{{ "Are votes anonymized: " }}</span>
|
||||
{{ $props.survey.areSelectionsAnonymized ? "yes" : "no" }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="fst-italic">{{ "Required inputs are marked with *." }}</span>
|
||||
<div class="row">
|
||||
<div class="d-flex flex-column gap-2 col-12 col-md-6 pb-2">
|
||||
<div class="d-flex flex-column gap-1">
|
||||
<label for="surveyTitleInput" class="fw-bold">{{ "* " + "Title" }}</label>
|
||||
<input v-model="surveyFormData.title" id="surveyTitleInput" type="text" />
|
||||
</div>
|
||||
<div class="d-flex flex-column gap-1">
|
||||
<label for="surveyDescriptionInput" class="fw-bold">{{ "Description" }}</label>
|
||||
<textarea v-model="surveyFormData.description" rows="4" class="flex-grow-1" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="row">
|
||||
<div class="col-12 col-xxl-6 pb-2">
|
||||
<div class="d-flex flex-column gap-1">
|
||||
<label
|
||||
for="surveyMaxSelectionsInput"
|
||||
class="fw-bold text-nowrap text-truncate"
|
||||
>
|
||||
{{ "Maximum number of selections" }}
|
||||
</label>
|
||||
<input
|
||||
v-model="surveyFormData.maxSelections"
|
||||
id="surveyMaxSelectionsInput"
|
||||
type="number"
|
||||
class="flex-grow-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-xxl-6 pb-2">
|
||||
<div class="d-flex flex-column gap-1">
|
||||
<label
|
||||
for="surveyEndsAtInput"
|
||||
class="fw-bold text-nowrap text-truncate"
|
||||
>
|
||||
{{ "* " + "Planned end date" }}
|
||||
</label>
|
||||
<input
|
||||
v-model="surveyFormData.endsAt"
|
||||
id="surveyEndsAtInput"
|
||||
type="date"
|
||||
class="flex-grow-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-6 d-flex flex-row align-items-center gap-2 pb-2">
|
||||
<label
|
||||
for="surveyAreParticipantsAnonymizedInput"
|
||||
class="fw-bold text-nowrap text-truncate"
|
||||
>
|
||||
{{ "Are participants anonymous?" }}
|
||||
</label>
|
||||
<input
|
||||
v-model="surveyFormData.areParticipantsAnonymized"
|
||||
id="surveyAreParticipantsAnonymizedInput"
|
||||
type="checkbox"
|
||||
style="height:15px;width:15px;"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 d-flex flex-row align-items-center gap-2 pb-2">
|
||||
<label
|
||||
for="surveyAreSelectionsAnonymizedInput"
|
||||
class="fw-bold text-nowrap text-truncate"
|
||||
>
|
||||
{{ "Are votes anonymous?" }}
|
||||
</label>
|
||||
<input
|
||||
v-model="surveyFormData.areSelectionsAnonymized"
|
||||
id="surveyAreSelectionsAnonymizedInput"
|
||||
type="checkbox"
|
||||
style="height:15px;width:15px;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
export default {
|
||||
name: "CoodleSurveyCalendar",
|
||||
components: {},
|
||||
props: {},
|
||||
emits: [],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {},
|
||||
watch: {},
|
||||
methods: {},
|
||||
created() {},
|
||||
template: /*html*/ `
|
||||
<div>
|
||||
calendar placeholder
|
||||
</div>
|
||||
`,
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
export default {
|
||||
name: "CoodleSurveyDurationSelector",
|
||||
props: {
|
||||
durationModelValue: Number | null,
|
||||
survey: Object,
|
||||
},
|
||||
emits: ["update:durationModelValue"],
|
||||
data() {
|
||||
return {
|
||||
appointmentDurationPresets: [15, 30, 45, 60, 90, 120],
|
||||
selectedDurationPreset: null,
|
||||
durationCustomInput: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
duration: {
|
||||
get() {
|
||||
return this.$props.durationModelValue;
|
||||
},
|
||||
set(newValue) {
|
||||
this.$emit("update:durationModelValue", newValue);
|
||||
},
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
durationCustomInput() {
|
||||
if (this.durationCustomInput) {
|
||||
this.duration = this.durationCustomInput;
|
||||
this.selectedDurationPreset = null;
|
||||
}
|
||||
},
|
||||
survey: {
|
||||
handler() {
|
||||
this.initializeInputs();
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
selectAppointmentDurationPreset(preset) {
|
||||
this.selectedDurationPreset = preset;
|
||||
this.duration = preset;
|
||||
this.durationCustomInput = null;
|
||||
},
|
||||
initializeInputs() {
|
||||
this.selectedDurationPreset = null;
|
||||
this.durationCustomInput = null;
|
||||
if (this.appointmentDurationPresets.includes(this.duration)) {
|
||||
this.selectedDurationPreset = this.duration;
|
||||
} else {
|
||||
this.durationCustomInput = this.duration;
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.initializeInputs();
|
||||
},
|
||||
template: /*html*/ `
|
||||
<div class="d-flex flex-column gap-2">
|
||||
<label class="fw-bold">{{ "* " + "Appointment duration (in minutes)" }}</label>
|
||||
<div class="overflow-x-auto">
|
||||
<div class="d-flex flex-row align-items-center gap-2">
|
||||
<div
|
||||
v-for="durationPreset in appointmentDurationPresets"
|
||||
@click="selectAppointmentDurationPreset(durationPreset)"
|
||||
type="button"
|
||||
:class="{'fhc-primary-border-color': selectedDurationPreset === durationPreset}"
|
||||
class="border border-2 d-flex justify-content-center align-items-center"
|
||||
style="min-width:60px;height:40px;"
|
||||
>
|
||||
<span>{{ durationPreset }}</span>
|
||||
</div>
|
||||
<span class="text-nowrap ps-2">{{ "Custom duration: " }}</span>
|
||||
<div
|
||||
:class="{'fhc-primary-border-color': durationCustomInput}"
|
||||
class="border border-2 d-flex justify-content-center align-items-center px-1"
|
||||
style="width:60px;height:40px;"
|
||||
>
|
||||
<input
|
||||
v-model="durationCustomInput"
|
||||
type="number"
|
||||
min="1"
|
||||
class="border-0"
|
||||
style="min-width:0; outline: none; background: none;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
@@ -0,0 +1,251 @@
|
||||
export default {
|
||||
name: "CoodleSurveyParticipants",
|
||||
components: {},
|
||||
props: {
|
||||
participantsModelValue: Array,
|
||||
},
|
||||
emits: ["update:participantsModelValue"],
|
||||
data() {
|
||||
return {
|
||||
searchInput: "",
|
||||
isSearchingForParticipants: false,
|
||||
searchResults: [],
|
||||
areSearchResultsShown: false,
|
||||
// todo: increase depending on feedback
|
||||
warningGroupSize: 5,
|
||||
// todo: remove
|
||||
searchResultsDevExample: [
|
||||
{
|
||||
uid: "ma1000",
|
||||
name: "Test User 10",
|
||||
type: "user",
|
||||
},
|
||||
{
|
||||
uid: "ma1001",
|
||||
name: "Test User 11",
|
||||
type: "user",
|
||||
},
|
||||
{
|
||||
uid: "ma1002",
|
||||
name: "Test User 12",
|
||||
type: "user",
|
||||
},
|
||||
{
|
||||
uid: "ma1003",
|
||||
name: "Test User 13",
|
||||
type: "user",
|
||||
},
|
||||
{
|
||||
uid: "ABC-DEF",
|
||||
name: "ABC-DEF",
|
||||
type: "group",
|
||||
users: [
|
||||
{
|
||||
uid: "ma1003",
|
||||
name: "Test User 13",
|
||||
},
|
||||
{
|
||||
uid: "ma1004",
|
||||
name: "Test User 14",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
uid: "GHI-JKL",
|
||||
name: "GHI-JKL",
|
||||
type: "group",
|
||||
users: [
|
||||
{
|
||||
uid: "ma1004",
|
||||
name: "Test User 14",
|
||||
},
|
||||
{
|
||||
uid: "ma1005",
|
||||
name: "Test User 15",
|
||||
},
|
||||
{
|
||||
uid: "ma1006",
|
||||
name: "Test User 16",
|
||||
},
|
||||
{
|
||||
uid: "ma1007",
|
||||
name: "Test User 17",
|
||||
},
|
||||
{
|
||||
uid: "ma1008",
|
||||
name: "Test User 18",
|
||||
},
|
||||
{
|
||||
uid: "ma1009",
|
||||
name: "Test User 19",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
participants: {
|
||||
get() {
|
||||
return this.$props.participantsModelValue;
|
||||
},
|
||||
set(newValue) {
|
||||
this.$emit("update:participantsModelValue", newValue);
|
||||
},
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
searchInput() {
|
||||
if (this.searchInput.length >= 2) {
|
||||
this.areSearchResultsShown = true;
|
||||
this.searchParticipants();
|
||||
} else {
|
||||
this.areSearchResultsShown = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async hideSearchResults() {
|
||||
// todo: clean up, don't use timeout
|
||||
setTimeout(() => {
|
||||
this.areSearchResultsShown = false;
|
||||
}, 100);
|
||||
},
|
||||
searchParticipants() {
|
||||
this.isSearchingForParticipants = true;
|
||||
|
||||
// todo
|
||||
this.searchResults = this.searchResultsDevExample;
|
||||
|
||||
this.isSearchingForParticipants = false;
|
||||
},
|
||||
selectSearchResult(searchResult) {
|
||||
if (
|
||||
searchResult.type === "group" &&
|
||||
searchResult.users.length > this.warningGroupSize
|
||||
) {
|
||||
let warningMessage =
|
||||
"Group (((groupName))) contains (((n))) users. Are you sure you want to add all (((n))) users to your survey?";
|
||||
warningMessage = warningMessage.replace(
|
||||
"(((groupName)))",
|
||||
searchResult.name,
|
||||
);
|
||||
warningMessage = warningMessage.replaceAll(
|
||||
"(((n)))",
|
||||
searchResult.users.length,
|
||||
);
|
||||
if (!window.confirm(warningMessage)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const addedParticipants =
|
||||
searchResult.type === "group"
|
||||
? searchResult.users
|
||||
: [searchResult];
|
||||
this.addParticipants(addedParticipants);
|
||||
},
|
||||
addParticipants(newParticipants) {
|
||||
newParticipants = newParticipants.filter((newParticipant) => {
|
||||
return !this.participants.some(
|
||||
(existingParticipant) =>
|
||||
existingParticipant.uid === newParticipant.uid,
|
||||
);
|
||||
}).map((newParticipant) => {
|
||||
return {
|
||||
uid: newParticipant.uid,
|
||||
name: newParticipant.name,
|
||||
isCalendarShown: false,
|
||||
}
|
||||
});
|
||||
|
||||
this.participants = this.participants.concat(newParticipants);
|
||||
},
|
||||
removeParticipant(participantToBeRemoved) {
|
||||
this.participants = this.participants.filter(
|
||||
(participant) => participant.uid !== participantToBeRemoved.uid,
|
||||
);
|
||||
},
|
||||
showCalendar(participantForCalendarToBeDisplayed) {
|
||||
this.participants = this.participants.map((participant) => {
|
||||
let updatedParticipant = { ...participant };
|
||||
if (
|
||||
participant.uid === participantForCalendarToBeDisplayed.uid
|
||||
) {
|
||||
updatedParticipant.isCalendarShown = true;
|
||||
}
|
||||
return updatedParticipant;
|
||||
});
|
||||
},
|
||||
},
|
||||
template: /*html*/ `
|
||||
<div class="d-flex flex-column gap-2 mb-3">
|
||||
<label for="searchInput" class="fw-bold">{{ "Participants" }}</label>
|
||||
<div class="position-relative" style="max-width:300px;">
|
||||
<input
|
||||
v-model="searchInput"
|
||||
@blur="hideSearchResults()"
|
||||
id="searchInput"
|
||||
class="w-100"
|
||||
/>
|
||||
<div
|
||||
v-if="areSearchResultsShown"
|
||||
id="coodleParticipantsSearchResults"
|
||||
class="position-absolute w-100 d-flex flex-column overflow-y-auto"
|
||||
style="height:200px;"
|
||||
>
|
||||
<div
|
||||
v-if="isSearchingForParticipants"
|
||||
class="flex-grow-1 d-flex flex-row justify-content-center align-items-center"
|
||||
>
|
||||
<i class="fa-solid fa-spinner fa-spin fa-xl"></i>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="!searchResults.length"
|
||||
class="d-flex flex-row justify-content-center align-items-center py-3"
|
||||
>
|
||||
<span>{{ "No results found!" }}</span>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div
|
||||
v-for="searchResult in searchResults"
|
||||
@click="selectSearchResult(searchResult)"
|
||||
type="button"
|
||||
class="coodleParticipantSearchResult d-flex flex-row gap-2 justify-content-start align-items-center py-1 px-2 border-bottom border-1"
|
||||
>
|
||||
<i v-if="searchResult.type === 'user'" class="fa-solid fa-user"></i>
|
||||
<i v-else-if="searchResult.type === 'group'" class="fa-solid fa-user-group"></i>
|
||||
<span>{{ searchResult.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span v-if="!participants.length" class="fst-italic">
|
||||
{{ "No participants added yet!" }}
|
||||
</span>
|
||||
<div v-else class="d-flex flex-row gap-2 flex-wrap">
|
||||
<div
|
||||
v-for="participant in participants"
|
||||
class="d-flex flex-row align-items-center gap-4 py-1 px-3 border border-1 rounded-pill"
|
||||
>
|
||||
<span>{{ participant.name }}</span>
|
||||
<div class="d-flex flex-row align-items-center gap-3">
|
||||
<i
|
||||
v-if="participant.isCalendarShown"
|
||||
@click="participant.isCalendarShown = false"
|
||||
type="button"
|
||||
class="fa-solid fa-calendar-check fhc-primary-color"
|
||||
></i>
|
||||
<i
|
||||
v-else
|
||||
@click="showCalendar(participant)"
|
||||
type="button"
|
||||
class="fa-regular fa-calendar"
|
||||
></i>
|
||||
<i @click="removeParticipant(participant)" type="button" class="fa-solid fa-xmark"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
+33
-1
@@ -143,6 +143,7 @@ export default {
|
||||
},
|
||||
submitVote() {
|
||||
// todo
|
||||
console.log("voting...");
|
||||
},
|
||||
submitFinalSelection() {
|
||||
if (!this.$props.survey.id) return;
|
||||
@@ -153,7 +154,7 @@ export default {
|
||||
window.alert("You haven't made a selection!");
|
||||
return;
|
||||
} else if (this.selectedTimeslotId === "none") {
|
||||
selectedTimeslot = "\"No appointment is possible\"";
|
||||
selectedTimeslot = '"No appointment is possible"';
|
||||
} else {
|
||||
const selectedTimeslotInfo = this.$props.timeslots.find(
|
||||
(timeslot) => timeslot.id === this.selectedTimeslotId,
|
||||
@@ -181,6 +182,37 @@ export default {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let participantsThatHaveNotVoted =
|
||||
this.participantsWithoutAuthUser.reduce((sum, participant) => {
|
||||
return (
|
||||
sum +
|
||||
(!Object.values(participant.selection).filter(
|
||||
(isSelected) => isSelected,
|
||||
).length
|
||||
? 1
|
||||
: 0)
|
||||
);
|
||||
}, 0);
|
||||
|
||||
if (
|
||||
!Object.values(this.authUserParticipant.selection).filter(
|
||||
(isSelected) => isSelected,
|
||||
).length
|
||||
) {
|
||||
participantsThatHaveNotVoted++;
|
||||
}
|
||||
|
||||
if (participantsThatHaveNotVoted) {
|
||||
if (
|
||||
!window.confirm(
|
||||
"Not all participants have voted. Are you sure you want to proceed?",
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log("finalizing...");
|
||||
// todo
|
||||
},
|
||||
@@ -9,8 +9,9 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
uid: null,
|
||||
// todo: return view to activeSurveysTable and survey to null
|
||||
view: "survey",
|
||||
// survey: null,
|
||||
// todo: delete comment after dev
|
||||
survey: {
|
||||
id: 1,
|
||||
creator: {
|
||||
@@ -19,13 +20,13 @@ export default {
|
||||
},
|
||||
title: "Test Meeting",
|
||||
description:
|
||||
"To discuss many important matters. \n Another line of the description.",
|
||||
"To discuss many important matters.\nAnother line of the description.",
|
||||
timeslotDuration: 75,
|
||||
maxSelections: 2,
|
||||
areParticipantsAnonymized: false,
|
||||
areSelectionsAnonymized: false,
|
||||
selectedTimeslotId: null,
|
||||
endsAt: "2026-06-23 23:30:30",
|
||||
endsAt: "2026-06-23",
|
||||
completedAt: null,
|
||||
canceledAt: null,
|
||||
createdAt: "2026-06-14 23:30:30",
|
||||
@@ -43,6 +44,14 @@ export default {
|
||||
id: 3,
|
||||
startsAt: "2026-06-25 07:00:00",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
startsAt: "2026-06-24 09:00:00",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
startsAt: "2026-06-27 12:30:00",
|
||||
},
|
||||
],
|
||||
participants: [
|
||||
{
|
||||
@@ -57,12 +66,12 @@ export default {
|
||||
},
|
||||
{
|
||||
uid: "ma1436",
|
||||
name: "Test User 2",
|
||||
name: "Test Userrr 2",
|
||||
selection: null,
|
||||
},
|
||||
{
|
||||
uid: "ma1437",
|
||||
name: "Test User 3",
|
||||
name: "Test Userrrrr 3",
|
||||
selection: [2,3],
|
||||
},
|
||||
],
|
||||
@@ -70,6 +79,8 @@ export default {
|
||||
1: 0,
|
||||
2: 2,
|
||||
3: 1,
|
||||
4: 0,
|
||||
5: 0,
|
||||
"none": 1,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -61,14 +61,3 @@ export function formatDate(d) {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* adds some number of minutes to a date
|
||||
*
|
||||
* @param {Date} d - the date to which the minutes are added
|
||||
* @param {int} m - number of minutes
|
||||
* @returns {Date} updated date
|
||||
*/
|
||||
export function addMinutesToDate(d, m) {
|
||||
return new Date(d.getTime() + m * 60000);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user