EditeurCreneau.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. <template>
  2. <div class="editor-panel">
  3. <h3>Modifier un créneau</h3>
  4. <div class="actions">
  5. <button class="btn small primary" v-on:click="emitCreationOrder">
  6. <i class="material-icons right">create</i>Nouveau
  7. </button>
  8. <button
  9. class="btn small primary"
  10. :class="{ disabled: creneau === undefined }"
  11. v-on:click="emitDuplicateOrder"
  12. >
  13. <i class="material-icons right">content_copy</i>Dupliquer
  14. </button>
  15. <button
  16. class="btn small error"
  17. :class="{ disabled: creneau === null }"
  18. v-on:click="emitDeleteOrder"
  19. >
  20. <i class="material-icons right">delete_forever</i>Supprimer
  21. </button>
  22. </div>
  23. <div class="empty" v-if="creneau === undefined">Veuillez selectioner un creneau.</div>
  24. <div class="editor-body" v-else>
  25. <styled-input
  26. label="Titre"
  27. id="last_name"
  28. type="text"
  29. :modelValue="creneau.title"
  30. @input="inputListener($event, 'title')"
  31. />
  32. <date-picker class="s6" title="Date" id="creneauDate" lang="fr" target="day" v-model="jour" />
  33. <styled-input
  34. class="s6"
  35. label="Heure"
  36. id="creneauHeure"
  37. type="text"
  38. placeholder="hh:mm"
  39. v-model.lazy="heure"
  40. />
  41. <styled-input
  42. class="s6"
  43. label="Durée (min)"
  44. id="creneauDuree"
  45. type="number"
  46. v-model.lazy="duree"
  47. />
  48. <styled-input
  49. class="s6"
  50. label="Heure fin"
  51. id="disabled"
  52. type="text"
  53. :modelValue="endHour"
  54. disabled
  55. />
  56. <styled-input
  57. class="s6"
  58. label="Bénévole minimum"
  59. id="minAttendee"
  60. type="number"
  61. :modelValue="creneau.minAttendee"
  62. :disabled="creneau.isMeal"
  63. @input="inputListener($event, 'minAttendee')"
  64. />
  65. <styled-input
  66. class="s6"
  67. label="Bénévole max"
  68. id="minAttendee"
  69. type="number"
  70. :disabled="!creneau.isMeal"
  71. :modelValue="creneau.maxAttendee"
  72. @input="inputListener($event, 'maxAttendee')"
  73. />
  74. <styled-input
  75. label="Description"
  76. id="description"
  77. type="textarea"
  78. class="materialize-textarea"
  79. :modelValue="creneau.description"
  80. @input="inputListener($event, 'description')"
  81. >
  82. </styled-input>
  83. <chips-input
  84. label="Compétences & préférences associées"
  85. id="compétence_selection"
  86. placeholder="Choisir une condition"
  87. secondary-placeholder="+ compétence"
  88. :autocomplete-list="autocompleteCompetencesList"
  89. :strict-autocomplete="true"
  90. v-model="competencesStrIdList"
  91. ></chips-input>
  92. <chips-input
  93. label="Bénévoles"
  94. id="bénevole_selection"
  95. placeholder="Choisir un bénévole"
  96. secondary-placeholder="+ bénévole"
  97. :autocomplete-list="autocompleteBenevolesList"
  98. :strict-autocomplete="true"
  99. v-model="benevoleStrIdList"
  100. ></chips-input>
  101. <div class="s6 checkbox">
  102. <checkbox
  103. label="Créneau repas"
  104. :modelValue="creneau.isMeal"
  105. help="Indique au solveur que ce créneau est un créneau repas"
  106. @input="checkboxListener($event, 'isMeal')"
  107. />
  108. <checkbox
  109. label="Bénévole fixe"
  110. :modelValue="creneau.fixedAttendee"
  111. help="Indique au solveur de ne pas changer l'affectation de ce créneau"
  112. @input="checkboxListener($event, 'fixedAttendee')"
  113. />
  114. </div>
  115. <styled-input
  116. label="Pénibilité"
  117. id="penibility"
  118. type="number"
  119. class="s6"
  120. :modelValue="creneau.penibility"
  121. @input="inputListener($event, 'penibility')"
  122. style="display: none"
  123. />
  124. </div>
  125. </div>
  126. </template>
  127. <script lang="ts">
  128. import { defineComponent, PropType } from "vue";
  129. import Creneau from "@/models/Creneau";
  130. import AutocompleteOptions from "@/models/AutocompleteOptions";
  131. import styledInput from "./input.vue";
  132. import chipsInput from "@/components/SelectChipInput.vue";
  133. import DatePicker from "@/components/date-picker.vue";
  134. import checkbox from "./checkBox.vue";
  135. import dayjs from "dayjs";
  136. import "@/assets/css/editor-panel.css";
  137. import { MutationTypes } from "@/store/Mutations";
  138. import Benevole from "@/models/Benevole";
  139. import Competence from "@/models/Competence";
  140. // TODO Understand why the component only syncro after a first edition of the date
  141. export default defineComponent({
  142. name: "EditeurCreneau",
  143. components: { chipsInput, styledInput, checkbox, DatePicker },
  144. props: {
  145. creneau: {
  146. type: Object as PropType<Creneau>,
  147. },
  148. },
  149. data: function () {
  150. return {
  151. jour: "",
  152. heure: "",
  153. duree: "",
  154. benevoleStrIdList: [] as Array<string>,
  155. competencesStrIdList: [] as Array<string>,
  156. };
  157. },
  158. watch: {
  159. "creneau.start": function () {
  160. this.updateForm();
  161. },
  162. "creneau.end": function () {
  163. this.updateForm();
  164. },
  165. "creneau.benevoleIdList": function (val: Array<number>) {
  166. this.benevoleStrIdList = val.map((s) => s.toString());
  167. },
  168. creneau: function (val: Creneau) {
  169. this.competencesStrIdList = val.competencesIdList.map((s) => s.toString());
  170. },
  171. competencesStrIdList(val: Array<string>) {
  172. if (this.creneau) {
  173. const new_arr = val.map((s) => parseInt(s));
  174. const old_arr = this.creneau?.competencesIdList;
  175. if (
  176. new_arr.length !== old_arr?.length ||
  177. new_arr.reduce((acc: boolean, n: number) => acc && old_arr.includes(n), true)
  178. ) {
  179. this.updateCreneau("competencesIdList", new_arr);
  180. }
  181. }
  182. },
  183. benevoleStrIdList(new_val: Array<string>) {
  184. if (this.creneau) {
  185. const new_list = new_val.map((s) => parseInt(s));
  186. const old_list = this.creneau?.benevoleIdList;
  187. const addList = new_list.filter((i) => !old_list.includes(i));
  188. const removeList = old_list.filter((i) => !new_list.includes(i));
  189. addList.forEach(this.addBenevole2Creneau);
  190. removeList.forEach(this.removeBenevole2Creneau);
  191. }
  192. },
  193. jour: function () {
  194. this.updateDates();
  195. },
  196. heure: function () {
  197. this.updateDates();
  198. },
  199. duree: function () {
  200. this.updateDates();
  201. },
  202. },
  203. computed: {
  204. duration(): number {
  205. return parseFloat(this.duree) ?? 0;
  206. },
  207. concatStart(): string {
  208. return (
  209. this.jour +
  210. " " +
  211. this.heure
  212. .split(":")
  213. .map((c) => ("0" + c).slice(-2))
  214. .join(":")
  215. );
  216. },
  217. validStart(): boolean {
  218. return /\d{4}\/\d{1,2}\/\d{1,2}/.test(this.jour) && dayjs(this.concatStart).isValid();
  219. },
  220. startDate(): Date {
  221. return dayjs(this.concatStart, "YYYY/MM/DD HH:mm").toDate();
  222. },
  223. endDate(): Date {
  224. return dayjs(this.startDate).add(this.duration, "m").toDate();
  225. },
  226. endHour(): string {
  227. return this.endDate.toTimeString().substring(0, 5);
  228. },
  229. validDuree(): boolean {
  230. return !isNaN(parseFloat(this.duree));
  231. },
  232. autocompleteBenevolesList(): Array<AutocompleteOptions> {
  233. const precursor = this.$store.state.benevoleList
  234. .map((benevole) => {
  235. return {
  236. id: benevole.id + "",
  237. name: benevole.fullname,
  238. score: this.getbenevoleScore(benevole),
  239. collide: this.getBenevoleCollision(benevole),
  240. };
  241. })
  242. .sort((a, b) => {
  243. let s = a.score - b.score;
  244. if (s != 0) {
  245. return s;
  246. }
  247. // put the available volunteer first
  248. s = (a.collide ? 1 : 0) - (b.collide ? 1 : 0);
  249. if (s != 0) {
  250. return s;
  251. }
  252. return a.name.localeCompare(b.name);
  253. });
  254. const output = precursor.map((o) => {
  255. let classes = o.collide ? "unavailable" : "";
  256. if (o.score == 3) {
  257. classes = "error";
  258. }
  259. if (o.score == 2) {
  260. classes = "warning";
  261. }
  262. if (o.score == 1) {
  263. classes = "warning";
  264. }
  265. return {
  266. id: o.id,
  267. name: o.name,
  268. class: classes,
  269. };
  270. });
  271. return output;
  272. },
  273. competenceList(): Array<Competence> {
  274. const output = [];
  275. for (const idStr of this.competencesStrIdList) {
  276. const id = parseInt(idStr);
  277. if (id) {
  278. const c = this.$store.getters.getCompetenceById(id);
  279. if (c) output.push(c);
  280. }
  281. }
  282. return output;
  283. },
  284. autocompleteCompetencesList(): Array<AutocompleteOptions> {
  285. return this.$store.state.competenceList.map((competence) => {
  286. return { id: competence.id + "", name: competence.fullname };
  287. });
  288. },
  289. },
  290. methods: {
  291. getbenevoleScore(benevole: Benevole): number {
  292. const scoreList = this.competenceList.map((c) =>
  293. benevole.competenceIdList.includes(c.id) ? 0 : c.score
  294. );
  295. return scoreList.length == 0 ? 0 : Math.max(...scoreList);
  296. },
  297. getBenevoleCollision(benevole: Benevole) {
  298. if (this.creneau != undefined) {
  299. const current = this.creneau;
  300. const concurence = benevole.creneauIdList
  301. .filter((id) => id != this.creneau?.id)
  302. .map((id) => this.$store.getters.getCreneauById(id))
  303. .filter((o) => o != undefined) as Array<Creneau>;
  304. return concurence.map((c) => c.collide(current)).some((b) => b);
  305. } else {
  306. return false;
  307. }
  308. },
  309. updateDates: function (): void {
  310. if (this.creneau && this.validDuree && this.validStart) {
  311. if (Math.abs(this.startDate.getTime() - this.creneau.start.getTime()) > 1000)
  312. this.updateCreneau("start", this.startDate);
  313. if (Math.abs(this.endDate.getTime() - this.creneau.end.getTime()) > 1000)
  314. this.updateCreneau("end", this.endDate);
  315. }
  316. },
  317. updateCreneau<K extends keyof Creneau>(field: K, value: Creneau[K]) {
  318. if (this.creneau) {
  319. const payload = {
  320. id: this.creneau.id,
  321. field: field,
  322. value: value,
  323. };
  324. this.$emit("edit", payload);
  325. }
  326. },
  327. addBenevole2Creneau(id: number) {
  328. if (this.creneau)
  329. this.$store.commit(MutationTypes.addBenevole2Creneau, {
  330. creneauId: this.creneau.id,
  331. benevoleId: id,
  332. });
  333. },
  334. removeBenevole2Creneau(id: number) {
  335. if (this.creneau)
  336. this.$store.commit(MutationTypes.removeBenevole2Creneau, {
  337. creneauId: this.creneau.id,
  338. benevoleId: id,
  339. });
  340. },
  341. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  342. inputListener(event: any, field: keyof Creneau) {
  343. this.updateCreneau(field, event.target.value);
  344. },
  345. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  346. checkboxListener(event: any, field: keyof Creneau) {
  347. this.updateCreneau(field, event.target.checked);
  348. },
  349. updateForm: function () {
  350. if (this.creneau) {
  351. const startDate = dayjs(this.creneau.start);
  352. this.jour = startDate.format("YYYY/MM/DD");
  353. this.heure = startDate.format("HH:mm");
  354. this.duree = Math.round(
  355. (this.creneau.end.getTime() - this.creneau.start.getTime()) / 1000 / 60
  356. ).toString();
  357. }
  358. },
  359. emitDuplicateOrder: function () {
  360. this.$emit("duplicate", this.creneau);
  361. },
  362. emitCreationOrder: function () {
  363. this.$emit("create");
  364. },
  365. emitDeleteOrder: function () {
  366. this.$emit("delete", this.creneau);
  367. },
  368. },
  369. mounted() {
  370. this.updateForm();
  371. if (this.creneau) {
  372. this.benevoleStrIdList = this.creneau.benevoleIdList.map((s) => s.toString());
  373. this.competencesStrIdList = this.creneau.competencesIdList.map((s) => s.toString());
  374. }
  375. },
  376. });
  377. </script>
  378. <style scoped>
  379. .checkbox {
  380. padding: 4px;
  381. }
  382. </style>