EditeurCreneau.vue 13 KB

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