EditeurCreneau.vue 12 KB

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