· 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>