· 4 years ago · Mar 31, 2021, 12:44 PM
1<template>
2 <div class="shipments-page">
3 <PageTitle :page_title="text.title" :page_sub_title="text.sub_title" />
4
5 <InlineContainer>
6 <div>
7 <button
8 type="button"
9 class="btn btn-danger"
10 v-on:click="handleDeleteModal"
11 :disabled="this.selection.length < 1"
12 >
13 Delete shipments
14 </button>
15 <span class="ml-4"
16 >{{ selection.length }} shipments have been selected</span
17 >
18 <Modal
19 class="delete-modal shipments-modal"
20 v-model="deleteModalIsOpened"
21 ok-text="Remove shipments"
22 :styles="{ borderRadius: '12px' }"
23 >
24 <p slot="header" class="delete-modal-header">
25 <span>Delete selected shipments?</span>
26 </p>
27 <p>
28 You are about to permanently delete
29 {{ this.selection.length }} shipment(s). If you delete them/it, you
30 will lose the ability to retrieve any information about them.
31 </p>
32 <div slot="footer">
33 <Button type="default" size="large" @click="handleDeleteModal"
34 >Cancel</Button
35 >
36 <Button
37 type="error"
38 size="large"
39 class="delete-modal-delete-btn"
40 @click="handleDeleteModalOk"
41 >Delete shipments</Button
42 >
43 </div>
44 </Modal>
45 </div>
46 <div>
47 <button
48 type="button"
49 class="btn btn-success"
50 v-on:click="handleAddModal"
51 >
52 + Add new shipment
53 </button>
54 <Modal
55 class-name="add-modal shipments-modal"
56 v-model="addModalIsOpened"
57 title="Create new shipment"
58 :footer-hide="true"
59 @on-cancel="handleAddModalReset"
60 fullscreen
61 >
62 <ShipmentForm
63 :schema="schema"
64 ref="addShipmentForm"
65 @on-submit="handleAddModalOk"
66 />
67 </Modal>
68 </div>
69 </InlineContainer>
70
71 <InlineContainer>
72 <InlineContainer justifyContent="flex-start">
73 <SearchSelect
74 v-for="filter of Object.values(filters)"
75 :key="filter.key"
76 :filterKey="filter.key"
77 :value="filter.value"
78 :name="filter.name"
79 :options="filter.options"
80 :size="{
81 width: '100px',
82 }"
83 @on-change="handleFilterChange"
84 />
85 </InlineContainer>
86 <DefinedDatePicker
87 @on-change="handleTimeframeChange"
88 ref="datePicker"
89 style="position: static"
90 />
91 </InlineContainer>
92
93 <Modal
94 class-name="shipments-modal edit-shipment-modal"
95 v-model="detailsModal.isOpened"
96 :title="`Details for shipment ID: ${detailsModal.shipment.id}`"
97 :footer-hide="detailsModal.isInEditMode"
98 :width="60"
99 >
100 <ShipmentDetails
101 v-show="!detailsModal.isInEditMode"
102 :shipment="detailsModal.shipment"
103 :schema="schema"
104 />
105
106 <ShipmentForm
107 v-show="detailsModal.isInEditMode"
108 formSubmitButtonText="Save"
109 :shipment="detailsModal.shipment"
110 :schema="schema"
111 @on-submit="handleSaveEditedDetails"
112 removeSteps
113 />
114 <div slot="footer">
115 <button class="btn btn-primary" v-on:click="handleEditDetails">
116 Edit
117 </button>
118 </div>
119 </Modal>
120
121 <Table
122 ref="selection"
123 :columns="tableColumns"
124 :data="tableData"
125 :loading="tableIsLoading"
126 @on-selection-change="handleTableSelection"
127 @on-sort-change="handleSortChange"
128 />
129 <Page
130 :current="tablePagination.page.value"
131 :total="tablePagination.total_number_entries"
132 :page-size="tablePagination.per_page.value"
133 :page-size-opts="tablePagination.pageSizeOpts"
134 @on-change="handlePageChange"
135 @on-page-size-change="handlePageSizeChange"
136 show-sizer
137 ></Page>
138 </div>
139</template>
140<script>
141import InlineContainer from "@/components/carrier_product/shipments/InlineContainer";
142import SearchSelect from "@/components/carrier_product/shipments/SearchSelect";
143import ShipmentDetails from "@/components/carrier_product/shipments/ShipmentDetails";
144import ShipmentForm from "@/components/carrier_product/shipments/ShipmentForm";
145
146import DefinedDatePicker from "@/components/product_common/global_components/DefinedDatePicker";
147import PageTitle from "@/components/product_common/global_components/PageTitle";
148
149import datetime from "@/components/product_common/global_components/datetime";
150import localization from "@/components/product_common/global_components/localization";
151
152export default {
153 name: "CarrierShipments.vue",
154 mixins: [datetime, localization],
155 components: {
156 PageTitle,
157 DefinedDatePicker,
158 InlineContainer,
159 SearchSelect,
160 ShipmentDetails,
161 ShipmentForm,
162 },
163 data() {
164 return {
165 text: {
166 title: this.$gettext("SHIPMENTS"),
167 sub_title: this.$gettext("Shipments list"),
168 },
169 timeframe: {
170 date_from: "",
171 date_to: "",
172 },
173 tableColumns: [
174 {
175 type: "selection",
176 width: 60,
177 align: "center",
178 key: "selection",
179 },
180 {
181 title: "Shipment",
182 key: "id",
183 sortable: true,
184 render: (h, { row }) => {
185 return h(
186 "a",
187 {
188 on: {
189 click: () => this.handleDetailsModal(row),
190 },
191 },
192 [row.id]
193 );
194 },
195 },
196 {
197 title: "Shipper",
198 key: "shipper_company_name",
199 sortable: true,
200 },
201 {
202 title: "Start city",
203 key: "pickup_city",
204 sortable: true,
205 },
206 {
207 title: "End city",
208 key: "dropoff_city",
209 sortable: true,
210 },
211 {
212 title: "Start Time",
213 key: "pickup_date_time",
214 sortable: true,
215 },
216 {
217 title: "End Time",
218 key: "dropoff_date_time",
219 sortable: true,
220 },
221 ],
222 schema: {
223 truckInformations: {
224 name: "Truck Informations",
225 key: "truckInformations",
226 subSections: {
227 main: {
228 key: "main",
229 name: "",
230 fields: {
231 trailer_id: {
232 label: "Trailer ID",
233 key: "trailer_id",
234 },
235 trailer_type: {
236 label: "Trailer type",
237 key: "trailer_type",
238 },
239 truck_id: {
240 label: "Truck ID",
241 key: "truck_id",
242 },
243 truck_type: {
244 label: "Truck type",
245 key: "truck_type",
246 },
247 type_of_goods: {
248 label: "Type of goods",
249 key: "type_of_goods",
250 },
251 weight_kg: {
252 label: "Weight of goods",
253 key: "weight_kg",
254 },
255 },
256 },
257 },
258 },
259
260 packageInformations: {
261 name: "Package Informations",
262 key: "packageInformations",
263 subSections: {
264 main: {
265 key: "main",
266 name: "",
267 fields: {
268 shipper_company_name: {
269 label: "Shipper name",
270 key: "shipper_company_name",
271 },
272 package_height_m: {
273 label: "Package height",
274 key: "package_height_m",
275 },
276 package_length_m: {
277 label: "Package length",
278 key: "package_length_m",
279 },
280 package_width_m: {
281 label: "Package width",
282 key: "package_width_m",
283 },
284 },
285 },
286 },
287 },
288
289 deliveryInformations: {
290 name: "Delivery Informations",
291 key: "deliveryInformations",
292 subSections: {
293 pickup: {
294 key: "pickup",
295 name: "Pick-Up",
296 fields: {
297 pickup_city: {
298 label: "Pick-up city",
299 key: "pickup_city",
300 },
301 pickup_country: {
302 label: "Pick-up country",
303 key: "pickup_country",
304 },
305 pickup_date_time: {
306 label: "Pick-up date",
307 key: "pickup_date_time",
308 },
309 pickup_latitude: {
310 label: "Pick-up latitude",
311 key: "pickup_latitude",
312 gpsMode: true,
313 },
314 pickup_longitude: {
315 label: "Pick-up longitude",
316 key: "pickup_longitude",
317 gpsMode: true,
318 },
319 pickup_postcode: {
320 label: "Pick-up post code",
321 key: "pickup_postcode",
322 },
323 pickup_street: {
324 label: "Pick-up street",
325 key: "pickup_street",
326 },
327 },
328 },
329 dropoff: {
330 key: "dropoff",
331 name: "Drop-Off",
332 fields: {
333 dropoff_city: {
334 label: "Drop-off city",
335 key: "dropoff_city",
336 },
337 dropoff_country: {
338 label: "Drop-off country",
339 key: "dropoff_country",
340 },
341 dropoff_date_time: {
342 label: "Drop-off date time",
343 key: "dropoff_date_time",
344 },
345 dropoff_latitude: {
346 label: "Drop-off latitude",
347 key: "dropoff_latitude",
348 gpsMode: true,
349 },
350 dropoff_longitude: {
351 label: "Drop-off longitude",
352 key: "dropoff_longitude",
353 gpsMode: true,
354 },
355 dropoff_postcode: {
356 label: "Drop-off post code",
357 key: "dropoff_postcode",
358 },
359 dropoff_street: {
360 label: "Drop-off street",
361 key: "dropoff_street",
362 },
363 },
364 },
365 },
366 },
367 },
368 filters: {},
369 tableData: [],
370 selection: [],
371 tableIsLoading: false,
372 deleteModalIsOpened: false,
373 addModalIsOpened: false,
374 detailsModal: {
375 isOpened: false,
376 isInEditMode: false,
377 shipment: {},
378 },
379 tablePagination: {
380 per_page: {
381 value: 10,
382 key: "per_page",
383 suffix: "",
384 },
385 page: {
386 value: 1,
387 key: "page",
388 suffix: "",
389 },
390 order_by: {
391 value: "id",
392 key: "order_by",
393 suffix: "",
394 },
395 total_number_entries: 0,
396 pageSizeOpts: [10, 20, 50],
397 },
398 };
399 },
400 computed: {
401 /*
402 queryString: function () {
403 // to be implemented
404 // used for memoization/cache
405 return null
406 },
407 */
408 },
409 watch: {
410 /*
411 queryString(next, prev) {
412 // memoization
413 // implement cache here
414 },
415 */
416 },
417 methods: {
418 //utils methods
419 formatDate: function (date) {
420 const hyphenated = [
421 date.slice(0, 4),
422 date.slice(4, 6),
423 date.slice(-2),
424 ].join("-");
425 return `${hyphenated} 00:00:00`;
426 },
427 composeQueryString: function (...args) {
428 const timeframeParams = {
429 date_from: {
430 suffix: "__gte",
431 columnName: "pickup_date_time",
432 },
433 date_to: {
434 suffix: "__lte",
435 columnName: "dropoff_date_time",
436 },
437 };
438 const flattened = args
439 .map((arg) => Object.entries(arg))
440 .reduce((acc, curr) => [...acc, ...curr], []);
441
442 return flattened
443 .reduce((acc, [key, dimension]) => {
444 //we are going to manipulate timeframe dimensions first as they have a different structure
445 if (key === "date_from" || key === "date_to") {
446 const { suffix, columnName } = timeframeParams[key];
447 const formattedEncodedDate = encodeURIComponent(
448 this.formatDate(dimension)
449 );
450 return [...acc, `${columnName}${suffix}=${formattedEncodedDate}`];
451 }
452
453 const { value, suffix } = dimension;
454 // then we can manipulate the filters, where value is a filter object
455 // if filter is empty just return the current queryString as it is
456 if (!value) return acc;
457
458 if (value === "None") {
459 return [...acc, `${key}__isnull=true`];
460 }
461
462 //if not compose the key and append to the array of queryString elements
463 return [...acc, `${key}${suffix}=${encodeURIComponent(value)}`];
464 }, [])
465 .join("&");
466 },
467
468 //functions to be called when component mounted
469 createFilters: async function (columns) {
470 const filtersOptions = (await this.getFiltersOptions()).data;
471 const nonFilterableColumns = [
472 "pickup_date_time",
473 "dropoff_date_time",
474 "selection",
475 ];
476 const suffixes = {
477 id: "",
478 shipper_company_name: "__icontains",
479 pickup_city: "__icontains",
480 dropoff_city: "__icontains",
481 };
482 return columns.reduce((acc, { key, title }) => {
483 if (nonFilterableColumns.some((col) => col === key)) return acc;
484 return {
485 ...acc,
486 [key]: {
487 name: title,
488 key: key,
489 value: "",
490 suffix: suffixes[key],
491 options: filtersOptions[key].map((option) => ({
492 label: option !== null ? option : "None",
493 value: option !== null ? option : "None",
494 })),
495 },
496 };
497 }, {});
498 },
499 initializeMessageConfig() {
500 this.$Message.config({
501 top: 100,
502 duration: 3,
503 });
504 },
505
506 //API and HTTP calls functions
507 getTableData: async function () {
508 const url = `${Urls.road_carrier_shipments()}?${this.composeQueryString(
509 this.filters,
510 this.timeframe,
511 this.tablePagination
512 )}`;
513 return this.axios.get(url);
514 },
515 getFiltersOptions: async function () {
516 return this.axios.get(Urls.road_carrier_shipments_filters_options());
517 },
518 postNewShipment: async function (body) {
519 if (!body) {
520 return;
521 }
522 return this.axios.post(Urls.road_carrier_shipments_add(), body);
523 },
524 putShipment: function (id, shipment) {
525 return this.axios.put(Urls.road_carrier_shipments_update(id), shipment);
526 },
527 deleteSelectedShipments: function () {
528 return this.axios.post(Urls.road_carrier_shipments_delete(), {
529 ids: this.selection.map((row) => row.id),
530 });
531 },
532
533 // Updating state functions
534 // http call side effects
535 updateTableData: async function (action) {
536 this.tableIsLoading = true;
537
538 const { status, data, statusText } = await this.getTableData();
539 const { shipments, pagination } = data;
540
541 if (status === 200) {
542 this.handleTableSelection([]);
543 this.tableData = shipments;
544 this.tablePagination = {
545 ...this.tablePagination,
546 total_number_entries: pagination.total_number_entries,
547 };
548 if (action !== "GET") this.updateFiltersOptions();
549 } else {
550 this.$Message.error(statusText);
551 }
552
553 this.tableIsLoading = false;
554 },
555 updateFiltersOptions: async function () {
556 const newOptions = (await this.getFiltersOptions()).data;
557 const updatedFilters = Object.values(this.filters).reduce(
558 (filters, currentFilter) => {
559 return {
560 ...filters,
561 [currentFilter.key]: {
562 ...currentFilter,
563 options: newOptions[currentFilter.key].map((option) => ({
564 label: option !== null ? option : "None",
565 value: option !== null ? option : "None",
566 })),
567 },
568 };
569 },
570 {}
571 );
572
573 this.filters = updatedFilters;
574 },
575 // Updating state functions
576 // UI logic
577 handleFilterChange: async function ({ filter, value }) {
578 this.filters[filter] = {
579 ...this.filters[filter],
580 value: value,
581 };
582 this.updateTableData("GET");
583 },
584 handleTimeframeChange: function () {
585 const { date_from, date_to } = this.$refs.datePicker;
586 this.timeframe = {
587 date_from: date_from,
588 date_to: date_to,
589 };
590 this.updateTableData("GET");
591 },
592 handleTableSelection: function (selection) {
593 this.selection = selection;
594 },
595 handleDeleteModal: function () {
596 this.deleteModalIsOpened = !this.deleteModalIsOpened;
597 },
598 handleAddModal: function () {
599 this.addModalIsOpened = !this.addModalIsOpened;
600 },
601 handleDetailsModal: function (shipment) {
602 this.detailsModal = {
603 ...this.detailsModal,
604 isInEditMode: false,
605 isOpened: !this.detailsModal.isOpened,
606 shipment: shipment,
607 };
608 },
609 handleEditDetails: function () {
610 this.detailsModal = {
611 ...this.detailsModal,
612 isInEditMode: true,
613 };
614 },
615 handleSaveEditedDetails: async function (editedDetails) {
616 const { id } = this.detailsModal.shipment;
617 const { status, data, statusText } = await this.putShipment(
618 id,
619 editedDetails
620 );
621
622 if (status === 200) {
623 this.handleDetailsModal({});
624 this.$Message.success(
625 `Shipment ${data.shipments[0].id} was successfully updated`
626 );
627 this.updateTableData("PUT");
628 return;
629 }
630 this.$Message.error(statusText);
631 },
632 handleDeleteModalOk: async function () {
633 this.handleDeleteModal();
634 const { status, data, statusText } = await this.deleteSelectedShipments();
635
636 if (status === 200) {
637 const successMessage =
638 data.ids.length === 1
639 ? `Shipment ${data.ids[0]} has been successfully deleted`
640 : `Shipments ${data.ids.join(", ")} have been successfully deleted`;
641 this.$Message.success(successMessage);
642 this.updateTableData("DELETE");
643 return;
644 }
645
646 this.$Message.error(statusText);
647 },
648 handleAddModalOk: async function (body) {
649 this.handleAddModal();
650 const { status, statusText } = await this.postNewShipment(body);
651
652 if (status === 200) {
653 this.handleAddModalReset();
654 this.$Message.success("Shipment was successfully added to the list");
655 this.updateTableData("POST");
656 return;
657 }
658
659 this.$Message.error(statusText);
660 },
661 handleAddModalReset: function () {
662 const { addShipmentForm } = this.$refs;
663 addShipmentForm.handleReset();
664 },
665 handlePageChange: function (page) {
666 this.tablePagination.page.value = page;
667 this.updateTableData("GET");
668 },
669 handlePageSizeChange: function (pageSize) {
670 this.tablePagination.per_page.value = pageSize;
671 this.updateTableData("GET");
672 },
673 handleSortChange: function (sorter) {
674 const { key, order } = sorter;
675
676 if (order === "normal") {
677 this.tablePagination = {
678 ...this.tablePagination,
679 order_by: {
680 ...this.tablePagination.order_by,
681 value: "id",
682 },
683 };
684 } else {
685 const prefix = order === "desc" ? "-" : "";
686 this.tablePagination = {
687 ...this.tablePagination,
688 order_by: {
689 ...this.tablePagination.order_by,
690 value: `${prefix}${key}`,
691 },
692 };
693 }
694 this.updateTableData("GET");
695 },
696 },
697 async mounted() {
698 this.handleTimeframeChange();
699 this.filters = await this.createFilters(this.tableColumns);
700 this.initializeMessageConfig();
701 },
702};
703</script>
704
705<style lang="scss">
706//need to import variables properly
707.shipments-page {
708 .ivu-page {
709 margin: 1rem 0rem;
710 }
711}
712
713.defined-date-range,
714.defined-date-range .ivu-input {
715 cursor: pointer;
716}
717
718.shipments-modal {
719 .ivu-modal-header {
720 border-bottom: 1px solid rgb(232, 234, 236) !important;
721 padding: 1rem !important;
722 height: 100%;
723 }
724
725 .ivu-modal-header p,
726 .ivu-modal-header-inner {
727 color: black !important;
728 font-weight: 500 !important;
729 line-height: 24px;
730 }
731
732 .ivu-modal-close {
733 top: 12px;
734 }
735
736 .ivu-modal-close .ivu-icon-ios-close {
737 color: #999 !important;
738 }
739}
740.edit-shipment-modal {
741 .ivu-modal {
742 top: 50px;
743 }
744
745 .ivu-modal-header {
746 background-color: var(--green-primary);
747 .ivu-modal-header-inner {
748 color: #fff !important;
749 font-weight: 500 !important;
750 }
751 }
752
753 .ivu-modal-close {
754 .ivu-icon-ios-close {
755 color: #fff !important;
756 }
757 }
758
759 .ivu-modal-body {
760 height: 60vh;
761 overflow: auto;
762 }
763}
764.delete-modal {
765 .ivu-modal-content {
766 border-radius: 8px !important;
767 border-top: solid #e6215a 8px;
768
769 .delete-modal-header {
770 font-size: 1.5rem;
771 margin-top: 0.5rem; //top border of the modal
772 margin-bottom: 0;
773 .ivu-icon-close {
774 top: 16px;
775 }
776 }
777
778 .ivu-modal-footer {
779 height: auto;
780 padding: 16px;
781 .delete-modal-delete-btn {
782 background-color: #e6215a;
783 font-weight: 500;
784 }
785 }
786 }
787}
788
789.add-modal {
790 z-index: 1000000 !important;
791}
792</style>