FRONTEND:
- Introduced `StagedRatesStore` with support for staged changes and expiry handling. - Updated `Rates.vue` to integrate staged rates check and display. - Enhanced `TableView` with flag support for improved visuals. - Adjusted bulk operation timer behavior to fix async issues. - Incorporated Kosovo flag asset and updated styles for better layout. BACKEND: - Added Matrix/Container Rate import service. - Added renewal function for expired rates.
This commit is contained in:
parent
2f7df47df4
commit
8698031689
28 changed files with 790 additions and 125 deletions
92
src/frontend/src/assets/flags/XK.svg
Normal file
92
src/frontend/src/assets/flags/XK.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.9 KiB |
|
|
@ -1,12 +1,20 @@
|
|||
<template>
|
||||
<div class="flag-container">
|
||||
<img :src="path" :alt="iso" :class="flagSizeClass">
|
||||
<tooltip v-if="tooltipText != null" :text="tooltipText"><img :src="path" :alt="iso" :class="flagSizeClass"></tooltip>
|
||||
<img v-else :src="path" :alt="iso" :class="flagSizeClass">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Tooltip from "@/components/UI/Tooltip.vue";
|
||||
|
||||
export default {
|
||||
components: {Tooltip},
|
||||
props: {
|
||||
tooltipText: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
iso: {
|
||||
type: String,
|
||||
required: true
|
||||
|
|
|
|||
|
|
@ -67,7 +67,9 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
setActiveTab(index) {
|
||||
|
||||
if (index >= 0 && index < this.tabs.length) {
|
||||
|
||||
this.hasChanged = true;
|
||||
this.activeTab = index;
|
||||
this.$emit('tab-changed', {
|
||||
|
|
|
|||
|
|
@ -32,15 +32,20 @@
|
|||
</tr>
|
||||
</tbody>
|
||||
<tbody v-else>
|
||||
<tr v-for="(item, index) in data" :key="index" class="table-row" @click="$emit('row-click', item)" :class="{'table-row--hover': mouseOver}">
|
||||
<tr v-for="(item, index) in data" :key="index" class="table-row" @click="$emit('row-click', item)"
|
||||
:class="{'table-row--hover': mouseOver}">
|
||||
<td v-for="column in columns" :key="column.key" class="table-cell" :class="getAlignment(column.align)">
|
||||
<span v-if="column.iconResolver == null">{{ getCellValue(item, column) }}</span>
|
||||
|
||||
<flag v-if="column.showFlag" :tooltip-text="getCellValue(item, column)" :iso="getCellValue(item, column)"></flag>
|
||||
<span v-else-if="column.iconResolver == null">{{ getCellValue(item, column) }}</span>
|
||||
<component v-else
|
||||
:is="getCellValue(item, column)"
|
||||
weight="regular"
|
||||
size="24"
|
||||
class="table-icon"
|
||||
/>
|
||||
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
|
@ -62,10 +67,11 @@ import BasicButton from "@/components/UI/BasicButton.vue";
|
|||
import SearchBar from "@/components/UI/SearchBar.vue";
|
||||
import Box from "@/components/UI/Box.vue";
|
||||
import Pagination from "@/components/UI/Pagination.vue";
|
||||
import Flag from "@/components/UI/Flag.vue";
|
||||
|
||||
export default {
|
||||
name: "TableView",
|
||||
components: {Pagination, Box, SearchBar, BasicButton, Checkbox, Spinner},
|
||||
components: {Flag, Pagination, Box, SearchBar, BasicButton, Checkbox, Spinner},
|
||||
emits: ['row-click'],
|
||||
props: {
|
||||
dataSource: {
|
||||
|
|
@ -200,7 +206,6 @@ export default {
|
|||
gap: 1.6rem;
|
||||
overflow: visible;
|
||||
border-radius: 0.8rem;
|
||||
box-shadow: 0 0.4rem 0.6rem -0.1rem rgba(0, 0, 0, 0.1);
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
|
|
@ -234,7 +239,7 @@ export default {
|
|||
text-transform: uppercase;
|
||||
letter-spacing: 0.08rem;
|
||||
text-align: left;
|
||||
padding: 2.4rem;
|
||||
padding: 1.6rem 1.6rem 1.6rem 0;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
|
|
@ -244,12 +249,18 @@ export default {
|
|||
}
|
||||
|
||||
.table-cell {
|
||||
padding: 2.4rem;
|
||||
padding: 0.8rem;
|
||||
background-color: transparent;
|
||||
border-bottom: 0.1rem solid #E3EDFF;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.table-cell-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.table-cell--left {
|
||||
text-align: left;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
|
||||
<div class="bulk-operation-caption">validity period</div>
|
||||
<div class="bulk-operation-data">
|
||||
<div class="period-select-container">
|
||||
<dropdown :options="periods"
|
||||
emptyText="No property set available"
|
||||
class="period-select"
|
||||
|
|
@ -33,6 +34,7 @@
|
|||
:disabled="!showValidityPeriod"
|
||||
></dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bulk-operation-action-footer">
|
||||
<basic-button @click="downloadFile" icon="download">Export</basic-button>
|
||||
|
|
@ -114,6 +116,19 @@ export default {
|
|||
processId: null,
|
||||
}
|
||||
},
|
||||
props: {
|
||||
isSelected: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
async isSelected(newVal) {
|
||||
if(newVal === true)
|
||||
await this.validityPeriodStore.loadPeriods();
|
||||
await this.bulkOperationStore.manageStatus();
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useValidityPeriodStore, useBulkOperationStore),
|
||||
showValidityPeriod() {
|
||||
|
|
@ -133,28 +148,23 @@ export default {
|
|||
periods() {
|
||||
const periods = [];
|
||||
|
||||
const ps = this.validityPeriodStore.getPeriods;
|
||||
const current = this.validityPeriodStore.getCurrentPeriodId;
|
||||
const vp = this.validityPeriodStore.getPeriods;
|
||||
|
||||
if ((ps ?? null) === null) {
|
||||
if ((vp ?? null) === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const p of ps) {
|
||||
const value = (p.state === "DRAFT" || p.state === "VALID") ? "CURRENT" : `${this.buildDate(p.start_date)} - ${this.buildDate(p.end_date)} ${p.state === "INVALID" ? "(INVALID)" : ""}`;
|
||||
for (const p of vp) {
|
||||
const value = (p.state === "DRAFT") ? "DRAFT" : (p.state === "VALID") ? "CURRENT" : `${this.buildDate(p.start_date)} - ${this.buildDate(p.end_date)} ${p.state === "INVALID" ? "(INVALID)" : ""}`;
|
||||
const period = {id: p.id, value: value};
|
||||
|
||||
if (p.state !== "VALID" || p.id === current)
|
||||
|
||||
periods.push(period);
|
||||
}
|
||||
|
||||
return periods;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.validityPeriodStore.loadPeriods();
|
||||
this.bulkOperationStore.updateStatus();
|
||||
},
|
||||
methods: {
|
||||
async fetchFile(id) {
|
||||
logger.info(`Fetching file ${id}`);
|
||||
|
|
@ -316,4 +326,14 @@ export default {
|
|||
color: #002F54;
|
||||
}
|
||||
|
||||
.period-select-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
gap: 1.6rem;
|
||||
font-size: 1.4rem;
|
||||
min-width: 30rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -2,7 +2,8 @@
|
|||
<div class="country-container">
|
||||
<div class="country-search-container">
|
||||
<div>Find country to edit:</div>
|
||||
<div class="country-search-box-container"><autosuggest-searchbar
|
||||
<div class="country-search-box-container">
|
||||
<autosuggest-searchbar
|
||||
:fetch-suggestions="query"
|
||||
flag-resolver="iso_code"
|
||||
placeholder="Search country..."
|
||||
|
|
@ -12,20 +13,22 @@
|
|||
subtitle-resolver="iso_code"
|
||||
:reset-on-select="true"
|
||||
@selected="selectCountry"
|
||||
></autosuggest-searchbar></div>
|
||||
></autosuggest-searchbar>
|
||||
</div>
|
||||
</div>
|
||||
<div class="country-edit-container">
|
||||
<collapsible-box v-if="selectedCountry" :is-collapsable="false" variant="border" :title="selectedCountry?.name ?? 'no country'">
|
||||
<div class="country-info-container">
|
||||
<box v-if="selectedCountry" class="country-info-box">
|
||||
<flag size="xl" :iso="selectedCountry.iso_code"></flag>
|
||||
<div class="country-info-text" >
|
||||
<div class="country-info-text-caption">{{ selectedCountry.name }} ({{ selectedCountry.iso_code}})</div>
|
||||
<div class="country-info-text-subline">{{ selectedCountry.region_code}}</div>
|
||||
<div class="country-info-text">
|
||||
<div class="country-info-text-caption">{{ selectedCountry.name }} ({{ selectedCountry.iso_code }})</div>
|
||||
<div class="country-info-text-subline">{{ selectedCountry.region_code }}</div>
|
||||
</div>
|
||||
|
||||
</box>
|
||||
</div>
|
||||
<div v-if="selectedCountry" >
|
||||
<div v-if="selectedCountry">
|
||||
<transition name="properties-fade" mode="out-in">
|
||||
<div v-if="!loading" class="properties-list">
|
||||
<transition-group name="property-item" tag="div">
|
||||
|
|
@ -39,8 +42,13 @@
|
|||
</transition>
|
||||
|
||||
</div>
|
||||
</collapsible-box>
|
||||
<div v-else>
|
||||
No country selected.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
@ -57,10 +65,11 @@ import Tooltip from "@/components/UI/Tooltip.vue";
|
|||
import ModalDialog from "@/components/UI/ModalDialog.vue";
|
||||
import Dropdown from "@/components/UI/Dropdown.vue";
|
||||
import {usePropertiesStore} from "@/store/properties.js";
|
||||
import CollapsibleBox from "@/components/UI/CollapsibleBox.vue";
|
||||
|
||||
export default {
|
||||
name: "CountryProperties",
|
||||
components: {Dropdown, ModalDialog, Tooltip, IconButton, Property, AutosuggestSearchbar, Box, Flag},
|
||||
components: {CollapsibleBox, Dropdown, ModalDialog, Tooltip, IconButton, Property, AutosuggestSearchbar, Box, Flag},
|
||||
computed: {
|
||||
periods() {
|
||||
const periods = [];
|
||||
|
|
@ -167,7 +176,7 @@ export default {
|
|||
display: flex;
|
||||
justify-content: flex-start;
|
||||
gap: 1.6rem;
|
||||
margin: 3.2rem 0 ;
|
||||
margin: 3.2rem 0;
|
||||
}
|
||||
|
||||
.country-info-box {
|
||||
|
|
@ -217,6 +226,7 @@ export default {
|
|||
}
|
||||
|
||||
.country-edit-container {
|
||||
padding-top: 2.4rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.6rem;
|
||||
|
|
@ -225,6 +235,4 @@ export default {
|
|||
}
|
||||
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
<template>
|
||||
<table-view ref="tableViewRef" :data-source="fetch" :columns="materialColumns" :page="pagination.page" :page-size="pageSize" :page-count="pagination.pageCount" :total-count="pagination.totalCount"></table-view>
|
||||
<div>
|
||||
<table-view ref="tableViewRef" :data-source="fetch" :columns="materialColumns" :page="pagination.page"
|
||||
:page-size="pageSize" :page-count="pagination.pageCount"
|
||||
:total-count="pagination.totalCount"></table-view>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
|
|
@ -12,10 +16,20 @@ export default {
|
|||
name: "materials",
|
||||
components: {TableView},
|
||||
computed: {
|
||||
... mapStores(useMaterialStore),
|
||||
...mapStores(useMaterialStore),
|
||||
},
|
||||
created() {
|
||||
this.materialStore.updateMaterials()
|
||||
props: {
|
||||
isSelected: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
async isSelected(newVal) {
|
||||
if(newVal === true) {
|
||||
await this.materialStore.updateMaterials();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async fetch(query) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
<template>
|
||||
<table-view ref="tableViewRef" :data-source="fetch" :columns="nodeColumns" :page="pagination.page" :page-size="pageSize" :page-count="pagination.pageCount" :total-count="pagination.totalCount"></table-view>
|
||||
<div>
|
||||
<table-view ref="tableViewRef" :data-source="fetch" :columns="nodeColumns" :page="pagination.page"
|
||||
:page-size="pageSize" :page-count="pagination.pageCount"
|
||||
:total-count="pagination.totalCount"></table-view>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
|
|
@ -10,9 +15,22 @@ import {useNodeStore} from "@/store/node.js";
|
|||
|
||||
export default {
|
||||
name: "Nodes",
|
||||
props: {
|
||||
isSelected: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
async isSelected(newVal) {
|
||||
if(newVal === true) {
|
||||
await this.nodeStore.loadNodes();
|
||||
}
|
||||
}
|
||||
},
|
||||
components: {TableView},
|
||||
computed: {
|
||||
... mapStores(useNodeStore),
|
||||
...mapStores(useNodeStore),
|
||||
},
|
||||
created() {
|
||||
this.nodeStore.loadNodes()
|
||||
|
|
@ -41,13 +59,14 @@ export default {
|
|||
},
|
||||
{
|
||||
key: 'country.iso_code',
|
||||
label: 'Country (ISO Code)',
|
||||
label: 'Country',
|
||||
showFlag: true
|
||||
},
|
||||
{
|
||||
key: 'types',
|
||||
label: 'Type',
|
||||
formatter: (value) => {
|
||||
return value.map(v => v.toUpperCase()).join(', ');
|
||||
return value.map(v => String(v).charAt(0).toUpperCase() + String(v).slice(1).toLowerCase()).join(', ');
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -63,7 +82,7 @@ export default {
|
|||
pageCount: 1,
|
||||
totalCount: 0
|
||||
},
|
||||
pageSize: 10
|
||||
pageSize: 20
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="properties-container">
|
||||
|
||||
<staged-changes></staged-changes>
|
||||
<div v-if="!loading" class="period-select-container">
|
||||
<span class="period-select-caption">Property set:</span>
|
||||
<dropdown :options="periods"
|
||||
|
|
@ -34,9 +34,7 @@
|
|||
</div>
|
||||
</transition>
|
||||
|
||||
<box variant="border">
|
||||
<country-properties></country-properties>
|
||||
</box>
|
||||
<country-properties v-if="!loading"></country-properties>
|
||||
|
||||
|
||||
</div>
|
||||
|
|
@ -56,12 +54,15 @@ import {usePropertySetsStore} from "@/store/propertySets.js";
|
|||
import {useCountryStore} from "@/store/country.js";
|
||||
import CountryProperties from "@/components/layout/config/CountryProperties.vue";
|
||||
import Box from "@/components/UI/Box.vue";
|
||||
import StagedChanges from "@/components/layout/config/StagedChanges.vue";
|
||||
|
||||
export default {
|
||||
name: "Properties",
|
||||
components: {
|
||||
StagedChanges,
|
||||
Box,
|
||||
CountryProperties, NotificationBar, ModalDialog, Tooltip, IconButton, BasicButton, Dropdown, Property},
|
||||
CountryProperties, NotificationBar, ModalDialog, Tooltip, IconButton, BasicButton, Dropdown, Property
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
modalDialogDeleteState: false,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
<template>
|
||||
<div class="container-rate-container">
|
||||
|
||||
<staged-rates ref="stagedRatesRef"></staged-rates>
|
||||
|
||||
<div class="container-rate-header">
|
||||
|
||||
<div class="container-rate-search-container">
|
||||
|
|
@ -32,7 +34,9 @@
|
|||
|
||||
</div>
|
||||
|
||||
<table-view ref="tableViewRef" :data-source="fetch" :columns="selectedTypeColumns" :page="pagination.page" :page-size="pageSize" :page-count="pagination.pageCount" :total-count="pagination.totalCount"></table-view>
|
||||
<table-view ref="tableViewRef" :data-source="fetch" :columns="selectedTypeColumns" :page="pagination.page"
|
||||
:page-size="pageSize" :page-count="pagination.pageCount"
|
||||
:total-count="pagination.totalCount"></table-view>
|
||||
|
||||
|
||||
</div>
|
||||
|
|
@ -52,10 +56,30 @@ import TableView from "@/components/UI/TableView.vue";
|
|||
import RadioOption from "@/components/UI/RadioOption.vue";
|
||||
import {useMatrixRateStore} from "@/store/matrixRate.js";
|
||||
import {useContainerRateStore} from "@/store/containerRate.js";
|
||||
import StagedRates from "@/components/layout/config/StagedRates.vue";
|
||||
|
||||
export default {
|
||||
name: "Rates",
|
||||
components: {RadioOption, TableView, DataTable, AutosuggestSearchbar, ModalDialog, Tooltip, IconButton, Dropdown},
|
||||
props: {
|
||||
isSelected: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
async isSelected(newVal) {
|
||||
if (newVal === true) {
|
||||
await this.validityPeriodStore.loadPeriods();
|
||||
await this.matrixRateStore.setQuery();
|
||||
await this.containerRateStore.setQuery();
|
||||
this.$refs.stagedRatesRef.checkChanges();
|
||||
this.pagination = this.rateType === 'container' ? this.containerRateStore.getPagination : this.matrixRateStore.getPagination;
|
||||
}
|
||||
}
|
||||
},
|
||||
components: {
|
||||
StagedRates,
|
||||
RadioOption, TableView, DataTable, AutosuggestSearchbar, ModalDialog, Tooltip, IconButton, Dropdown},
|
||||
computed: {
|
||||
...mapStores(useValidityPeriodStore, useMatrixRateStore, useContainerRateStore),
|
||||
loadingPeriods() {
|
||||
|
|
@ -75,6 +99,7 @@ export default {
|
|||
return this.rateTypeValue;
|
||||
},
|
||||
set(value) {
|
||||
|
||||
if (value === "matrix") {
|
||||
this.selectedTypeColumns = this.matrixColumns;
|
||||
} else {
|
||||
|
|
@ -91,16 +116,13 @@ export default {
|
|||
},
|
||||
async set(value) {
|
||||
this.validityPeriodStore.setSelectedPeriod(value);
|
||||
// await this.propertiesStore.loadProperties(value);
|
||||
// await this.countryStore.selectPeriod(value);
|
||||
this.$refs.tableViewRef.reload('', 1);
|
||||
}
|
||||
},
|
||||
periods() {
|
||||
const periods = [];
|
||||
|
||||
const vp = this.validityPeriodStore.getPeriods;
|
||||
// const current = this.propertySetsStore.getCurrentPeriodId; //TODO
|
||||
const current = 1;
|
||||
|
||||
if ((vp ?? null) === null) {
|
||||
return null;
|
||||
|
|
@ -110,7 +132,7 @@ export default {
|
|||
const value = (p.state === "DRAFT") ? "DRAFT" : (p.state === "VALID") ? "CURRENT" : `${this.buildDate(p.start_date)} - ${this.buildDate(p.end_date)} ${p.state === "INVALID" ? "(INVALID)" : ""}`;
|
||||
const period = {id: p.id, value: value};
|
||||
|
||||
if (p.state !== "VALID" || p.id === current)
|
||||
|
||||
periods.push(period);
|
||||
}
|
||||
|
||||
|
|
@ -119,18 +141,18 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
pageSize: 10,
|
||||
pagination: { page: 1, pageCount: 10, totalCount: 1 },
|
||||
pageSize: 20,
|
||||
pagination: {page: 1, pageCount: 1, totalCount: 1},
|
||||
rateTypeValue: "container",
|
||||
selectedTypeColumns: [],
|
||||
matrixColumns: [
|
||||
{key: 'source.iso_code', label: 'From Country (ISO code)'},
|
||||
{key: 'source.iso_code', label: 'From Country (ISO code)' },
|
||||
{key: 'destination.iso_code', label: 'To Country (ISO code)'},
|
||||
{key: 'rate', align: 'right', label: 'Rate [EUR/km]'},
|
||||
],
|
||||
containerColumns: [
|
||||
{
|
||||
key: 'type', label: 'Type', align: 'center', iconResolver: (rawValue, item) => {
|
||||
key: 'type', label: 'Type', align: 'center', iconResolver: (rawValue, _) => {
|
||||
|
||||
if (rawValue === "SEA") {
|
||||
return "PhBoat";
|
||||
|
|
@ -143,10 +165,10 @@ export default {
|
|||
},
|
||||
{key: 'source.name', align: 'left', label: 'From node'},
|
||||
{key: 'destination.name', align: 'left', label: 'To node'},
|
||||
{key: 'lead_time', align: 'right', label: 'Lead time [days]'},
|
||||
{key: 'rates.TEU', align: 'right',label: '20 ft. GP rate [EUR]'},
|
||||
{key: 'rates.FEU', align: 'right',label: '40 ft. GP rate [EUR]'},
|
||||
{key: 'rates.TEU', align: 'right', label: '20 ft. GP rate [EUR]'},
|
||||
{key: 'rates.FEU', align: 'right', label: '40 ft. GP rate [EUR]'},
|
||||
{key: 'rates.HC', align: 'right', label: '40 ft. HC rate [EUR]'},
|
||||
{key: 'lead_time', align: 'right', label: 'Lead time [days]'},
|
||||
],
|
||||
modalDialogDeleteState: false,
|
||||
selectedPeriodId: null
|
||||
|
|
@ -157,6 +179,7 @@ export default {
|
|||
await this.validityPeriodStore.loadPeriods();
|
||||
await this.matrixRateStore.setQuery();
|
||||
await this.containerRateStore.setQuery();
|
||||
this.pagination = this.rateType === 'container' ? this.containerRateStore.getPagination : this.matrixRateStore.getPagination;
|
||||
},
|
||||
methods: {
|
||||
async fetch(query) {
|
||||
|
|
@ -192,7 +215,6 @@ export default {
|
|||
<style scoped>
|
||||
|
||||
|
||||
|
||||
.table-view {
|
||||
margin-top: 1.6rem;
|
||||
}
|
||||
|
|
@ -218,6 +240,7 @@ export default {
|
|||
margin-bottom: 1rem;
|
||||
gap: 1.6rem;
|
||||
font-size: 1.4rem;
|
||||
|
||||
}
|
||||
|
||||
.container-rate-search-searchbar {
|
||||
|
|
@ -231,6 +254,7 @@ export default {
|
|||
margin-bottom: 1rem;
|
||||
gap: 1.6rem;
|
||||
font-size: 1.4rem;
|
||||
min-width: 50rem;
|
||||
}
|
||||
|
||||
.period-select-caption {
|
||||
|
|
|
|||
135
src/frontend/src/components/layout/config/StagedRates.vue
Normal file
135
src/frontend/src/components/layout/config/StagedRates.vue
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
<template>
|
||||
<div>
|
||||
<transition name="fade">
|
||||
<div v-if="showSelf" class="staged-changes-container">
|
||||
<div class="staged-changes-info">
|
||||
<ph-warning size="18px"></ph-warning>
|
||||
{{ message }}
|
||||
</div>
|
||||
<div class="staged-changes-save">
|
||||
<icon-button icon="floppy-disk" @click="applyChanges" variant="blue"></icon-button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<modal-dialog :title="modalTitle" dismiss-text="No" accept-text="Yes"
|
||||
:state="modalDialogStagedChangesState"
|
||||
:message="modalText"
|
||||
@click="applyChangesModalClick"
|
||||
>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import IconButton from "@/components/UI/IconButton.vue";
|
||||
import ModalDialog from "@/components/UI/ModalDialog.vue";
|
||||
import {mapStores} from "pinia";
|
||||
import {usePropertiesStore} from "@/store/properties.js";
|
||||
import {useStagedRatesStore} from "@/store/stagedRates.js";
|
||||
|
||||
export default {
|
||||
name: "StagedRates",
|
||||
components: {ModalDialog, IconButton},
|
||||
data() {
|
||||
return {
|
||||
modalDialogStagedChangesState: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useStagedRatesStore, usePropertiesStore),
|
||||
showSelf() {
|
||||
return this.stagedChanges || this.expiresSoon;
|
||||
},
|
||||
stagedChanges() {
|
||||
return this.stagedRatesStore.hasStagedChanges;
|
||||
},
|
||||
expiresSoon() {
|
||||
return this.stagedRatesStore.expiresSoon;
|
||||
},
|
||||
message() {
|
||||
if(this.stagedChanges)
|
||||
return "There are changes to kilometer rates and/or container rates. Please review them thoroughly and press save icon to apply them.";
|
||||
else
|
||||
return "The validity of the rates will expire soon. Please upload new rates or extend the validity of the current rates by clicking the save icon.";
|
||||
},
|
||||
modalTitle() {
|
||||
if(this.stagedChanges)
|
||||
return "Do you really want to apply the current changes?";
|
||||
else
|
||||
return "Do you really want to extend the validity of the current rates?";
|
||||
},
|
||||
modalText() {
|
||||
if(this.stagedChanges)
|
||||
return "As soon as you change the kilometer rates or container rates, new calculations are no longer comparable with previous calculations. " +
|
||||
"Therefore, a new \"validity period\" is created. Only calculations that were created with the same \"validity period\" can be compared within reporting. Would you like to continue?";
|
||||
else
|
||||
return "The accuracy of logistics calculations can only be ensured if transportation costs are regularly updated to the correct values. You should only extend the validity period if transportation costs have not changed. " +
|
||||
"Do you want to extend the validity of the current rates?";
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
applyChanges() {
|
||||
this.modalDialogStagedChangesState = true;
|
||||
},
|
||||
async applyChangesModalClick(action) {
|
||||
this.modalDialogStagedChangesState = false;
|
||||
if (action === 'accept') {
|
||||
await this.stagedRatesStore.applyChanges();
|
||||
this.$emit('rates-update');
|
||||
}
|
||||
},
|
||||
checkChanges() {
|
||||
this.stagedRatesStore.checkStagedChanges();
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.checkChanges();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.staged-changes-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 1.4rem;
|
||||
gap: 1.6rem;
|
||||
background-color: #5AF0B4;
|
||||
color: #002F54;
|
||||
border-radius: 0.8rem;
|
||||
padding: 1.6rem;
|
||||
margin-bottom: 1.6rem;
|
||||
}
|
||||
|
||||
.staged-changes-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.staged-changes-save {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
/* Fade transition styles */
|
||||
.fade-enter-active {
|
||||
transition: opacity 0.3s ease-in;
|
||||
}
|
||||
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease-out;
|
||||
}
|
||||
|
||||
.fade-enter-from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -27,7 +27,8 @@ import {
|
|||
PhArchive,
|
||||
PhFloppyDisk,
|
||||
PhArrowCounterClockwise,
|
||||
PhCheck, PhBug, PhShuffle, PhStack, PhFile, PhFilePlus, PhDownloadSimple
|
||||
PhCheck, PhBug, PhShuffle, PhStack, PhFile, PhFilePlus, PhDownloadSimple, PhMonitor, PhCpu, PhFileJs, PhFileCloud,
|
||||
PhCloudX, PhDesktop, PhHardDrives
|
||||
} from "@phosphor-icons/vue";
|
||||
|
||||
const app = createApp(App);
|
||||
|
|
@ -62,6 +63,8 @@ app.component('PhBug', PhBug);
|
|||
app.component('PhShuffle', PhShuffle);
|
||||
app.component('PhStack', PhStack );
|
||||
app.component('PhFile', PhFile);
|
||||
app.component("PhDesktop", PhDesktop );
|
||||
app.component("PhHardDrives", PhHardDrives );
|
||||
|
||||
app.use(router);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,9 +3,8 @@
|
|||
<div class="header-container">
|
||||
<h2 class="page-header">Configuration</h2>
|
||||
<div>
|
||||
<staged-changes></staged-changes>
|
||||
<box class="box-container">
|
||||
<tab-container :tabs="tabsConfig" class="tab-container">
|
||||
<tab-container :tabs="tabsConfig" class="tab-container" @tab-changed="handleTabChange">
|
||||
</tab-container>
|
||||
</box>
|
||||
</div>
|
||||
|
|
@ -35,32 +34,50 @@ export default {
|
|||
currentTab: null,
|
||||
tabsConfig: [
|
||||
{
|
||||
title: 'System properties',
|
||||
title: 'Properties',
|
||||
component: markRaw(Properties),
|
||||
props: { isSelected: false},
|
||||
},
|
||||
{
|
||||
title: 'System error log',
|
||||
title: 'System log',
|
||||
component: markRaw(ErrorLog),
|
||||
props: { isSelected: false},
|
||||
},
|
||||
{
|
||||
title: 'Materials',
|
||||
component: markRaw(Materials),
|
||||
props: { isSelected: false},
|
||||
},
|
||||
{
|
||||
title: 'Nodes',
|
||||
component: markRaw(Nodes),
|
||||
props: { isSelected: false},
|
||||
},
|
||||
{
|
||||
title: 'Rates',
|
||||
component: markRaw(Rates),
|
||||
props: { isSelected: false},
|
||||
},
|
||||
{
|
||||
title: 'Bulk operations',
|
||||
component: markRaw(BulkOperations),
|
||||
props: { isSelected: false},
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleTabChange(eventData) {
|
||||
|
||||
console.log("handleTabChange")
|
||||
|
||||
const { index, tab } = eventData;
|
||||
console.log(`Tab ${index} activated:`, tab.title);
|
||||
|
||||
this.tabsConfig.forEach(t => t.props.isSelected = t.title === tab.title);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -25,14 +25,25 @@ export default {
|
|||
return {
|
||||
showModal: false,
|
||||
error: null,
|
||||
pageSize: 10,
|
||||
pageSize: 20,
|
||||
pagination: { page: 1, pageCount: 10, totalCount: 1 },
|
||||
columns: [
|
||||
{key: 'type', label: 'Type', align: "center", iconResolver: (rawValue, item) => {
|
||||
|
||||
if (rawValue === "FRONTEND") {
|
||||
return "PhDesktop";
|
||||
} else if (rawValue === "BACKEND") {
|
||||
return "PhHardDrives"
|
||||
} else if (rawValue === "BULK") {
|
||||
return "PhStack"
|
||||
} else if(rawValue === "CALCULATION") {
|
||||
return "PhCalculator"
|
||||
}
|
||||
}},
|
||||
{key: 'timestamp', label: 'Timestamp', formatter: (value) => this.buildDate(value) },
|
||||
{key: 'user_id', label: 'User'},
|
||||
{key: 'type', label: 'Type'},
|
||||
{key: 'title', label: 'Title'},
|
||||
{key: 'message', label: 'Message'},
|
||||
{key: 'message', label: 'Message', formatter: (value) => (value?.length > 100 ? `${value?.substring(0,100)} ...` : value)},
|
||||
{key: 'code', label: 'Exception'},
|
||||
|
||||
],
|
||||
|
|
@ -41,8 +52,28 @@ export default {
|
|||
computed: {
|
||||
...mapStores(useErrorLogStore)
|
||||
},
|
||||
props: {
|
||||
isSelected: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
async isSelected(newVal) {
|
||||
if(newVal === true) {
|
||||
const query = {
|
||||
searchTerm: '',
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
}
|
||||
await this.fetchData(query);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
async fetchData(query) {
|
||||
console.log("fetchData")
|
||||
await this.errorLogStore.setQuery(query);
|
||||
this.pagination = this.errorLogStore.getPagination;
|
||||
return this.errorLogStore.getErrors;
|
||||
|
|
|
|||
|
|
@ -34,11 +34,9 @@ export const useBulkOperationStore = defineStore('bulkOperation', {
|
|||
this.startTimer();
|
||||
},
|
||||
async timerMethod() {
|
||||
this.updateStatus();
|
||||
await this.updateStatus();
|
||||
const restart = this.restartNeeded();
|
||||
|
||||
console.log("state " + this.bulkOperations.map(b => b.state).join(", ") + "restarting " + restart);
|
||||
|
||||
this.stopTimer();
|
||||
|
||||
if(restart) {
|
||||
|
|
@ -46,6 +44,11 @@ export const useBulkOperationStore = defineStore('bulkOperation', {
|
|||
}
|
||||
|
||||
},
|
||||
async manageStatus() {
|
||||
await this.updateStatus();
|
||||
if(this.restartNeeded())
|
||||
this.startTimer();
|
||||
},
|
||||
async updateStatus() {
|
||||
this.loading = true;
|
||||
|
||||
|
|
@ -79,15 +82,12 @@ export const useBulkOperationStore = defineStore('bulkOperation', {
|
|||
startTimer() {
|
||||
if (this.updateTimer) return
|
||||
|
||||
console.log("start timer")
|
||||
this.updateTimer = setTimeout(() => {
|
||||
this.timerMethod()
|
||||
this.updateTimer = setTimeout(async () => {
|
||||
await this.timerMethod()
|
||||
}, this.updateInterval)
|
||||
},
|
||||
stopTimer() {
|
||||
if (this.updateTimer) {
|
||||
console.log("stop timer")
|
||||
|
||||
clearTimeout(this.updateTimer)
|
||||
this.updateTimer = null
|
||||
}
|
||||
|
|
|
|||
33
src/frontend/src/store/stagedRates.js
Normal file
33
src/frontend/src/store/stagedRates.js
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import {defineStore} from 'pinia'
|
||||
import {config} from '@/config'
|
||||
import {useErrorStore} from "@/store/error.js";
|
||||
import performRequest from "@/backend.js";
|
||||
|
||||
export const useStagedRatesStore = defineStore('stagedRates', {
|
||||
state() {
|
||||
return {
|
||||
stagedChanges: {staged_changes: false, expires: null}
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
hasStagedChanges(state) {
|
||||
return state.stagedChanges.staged_changes;
|
||||
},
|
||||
expiresSoon(state) {
|
||||
return !state.stagedChanges.staged_changes && state.stagedChanges.expires !== null && (state.stagedChanges.expires < 10);
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
async checkStagedChanges() {
|
||||
const url = `${config.backendUrl}/rates/staged_changes`;
|
||||
const resp = await performRequest(this, 'GET', url, null,);
|
||||
this.stagedChanges = resp.data;
|
||||
},
|
||||
async applyChanges() {
|
||||
const url = `${config.backendUrl}/rates/staged_changes`;
|
||||
await performRequest(this, 'PUT', url, null, false);
|
||||
this.stagedChanges = {staged_changes: false, expires: null};
|
||||
},
|
||||
}
|
||||
|
||||
});
|
||||
|
|
@ -2,6 +2,7 @@ package de.avatic.lcc.controller.configuration;
|
|||
|
||||
import de.avatic.lcc.dto.configuration.matrixrates.MatrixRateDTO;
|
||||
import de.avatic.lcc.dto.configuration.rates.ContainerRateDTO;
|
||||
import de.avatic.lcc.dto.configuration.rates.StagedRatesDTO;
|
||||
import de.avatic.lcc.dto.generic.ValidityPeriodDTO;
|
||||
import de.avatic.lcc.repositories.pagination.SearchQueryResult;
|
||||
import de.avatic.lcc.service.access.ContainerRateService;
|
||||
|
|
@ -166,8 +167,8 @@ public class RateController {
|
|||
* whether rate drafts exist (true) or not (false).
|
||||
*/
|
||||
@GetMapping( {"/staged_changes", "/staged_changes/"})
|
||||
public ResponseEntity<Boolean> checkRateDrafts() {
|
||||
return ResponseEntity.ok(rateApprovalService.hasRateDrafts());
|
||||
public ResponseEntity<StagedRatesDTO> checkRateDrafts() {
|
||||
return ResponseEntity.ok(rateApprovalService.getStagedRateDTO());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
package de.avatic.lcc.dto.configuration.rates;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class StagedRatesDTO {
|
||||
|
||||
@JsonProperty("staged_changes")
|
||||
private boolean stagedChanges;
|
||||
|
||||
private Integer expires;
|
||||
|
||||
public boolean isStagedChanges() {
|
||||
return stagedChanges;
|
||||
}
|
||||
|
||||
public void setStagedChanges(boolean stagedChanges) {
|
||||
this.stagedChanges = stagedChanges;
|
||||
}
|
||||
|
||||
public Integer getExpires() {
|
||||
return expires;
|
||||
}
|
||||
|
||||
public void setExpires(Integer expires) {
|
||||
this.expires = expires;
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,8 @@ public class MatrixRate {
|
|||
@NotNull
|
||||
private Integer toCountry;
|
||||
|
||||
private Integer validityPeriodId;
|
||||
|
||||
public Integer getId() {
|
||||
return id;
|
||||
}
|
||||
|
|
@ -49,4 +51,12 @@ public class MatrixRate {
|
|||
public void setToCountry(Integer toCountry) {
|
||||
this.toCountry = toCountry;
|
||||
}
|
||||
|
||||
public void setValidityPeriodId(Integer validityPeriodId) {
|
||||
this.validityPeriodId = validityPeriodId;
|
||||
}
|
||||
|
||||
public Integer getValidityPeriodId() {
|
||||
return validityPeriodId;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ public class ValidityPeriod {
|
|||
|
||||
private ValidityPeriodState state;
|
||||
|
||||
private int renewals;
|
||||
|
||||
public Integer getId() {
|
||||
return id;
|
||||
|
|
@ -46,4 +47,12 @@ public class ValidityPeriod {
|
|||
public void setState(ValidityPeriodState state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public void setRenewals(int renewals) {
|
||||
this.renewals = renewals;
|
||||
}
|
||||
|
||||
public int getRenewals() {
|
||||
return renewals;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -180,6 +180,32 @@ public class ContainerRateRepository {
|
|||
return Optional.of(route.getFirst());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void insert(ContainerRate containerRate) {
|
||||
String sql = """
|
||||
INSERT INTO container_rate
|
||||
(from_node_id, to_node_id, container_rate_type, rate_teu, rate_feu, rate_hc, lead_time, validity_period_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
container_rate_type = VALUES(container_rate_type),
|
||||
rate_teu = VALUES(rate_teu),
|
||||
rate_feu = VALUES(rate_feu),
|
||||
rate_hc = VALUES(rate_hc),
|
||||
lead_time = VALUES(lead_time)
|
||||
""";
|
||||
|
||||
jdbcTemplate.update(sql,
|
||||
containerRate.getFromNodeId(),
|
||||
containerRate.getToNodeId(),
|
||||
containerRate.getType().name(),
|
||||
containerRate.getRateTeu(),
|
||||
containerRate.getRateFeu(),
|
||||
containerRate.getRateHc(),
|
||||
containerRate.getLeadTime(),
|
||||
containerRate.getValidityPeriodId()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private static class ContainerRateMapper implements RowMapper<ContainerRate> {
|
||||
|
||||
|
|
|
|||
|
|
@ -134,6 +134,22 @@ public class MatrixRateRepository {
|
|||
return Optional.of(rates.getFirst());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void insert(MatrixRate rate) {
|
||||
String sql = """
|
||||
INSERT INTO country_matrix_rate (from_country_id, to_country_id, rate, validity_period_id)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
rate = VALUES(rate)
|
||||
""";
|
||||
|
||||
jdbcTemplate.update(sql,
|
||||
rate.getFromCountry(),
|
||||
rate.getToCountry(),
|
||||
rate.getRate(),
|
||||
rate.getValidityPeriodId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps rows of a {@link ResultSet} to {@link MatrixRate} objects as required by
|
||||
* the {@link JdbcTemplate}.
|
||||
|
|
@ -155,6 +171,7 @@ public class MatrixRateRepository {
|
|||
entity.setRate(rs.getBigDecimal("rate"));
|
||||
entity.setFromCountry(rs.getInt("from_country_id"));
|
||||
entity.setToCountry(rs.getInt("to_country_id"));
|
||||
entity.setValidityPeriodId(rs.getInt("validity_period_id"));
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package de.avatic.lcc.repositories.rates;
|
|||
import de.avatic.lcc.model.ValidityTuple;
|
||||
import de.avatic.lcc.model.rates.ValidityPeriod;
|
||||
import de.avatic.lcc.model.rates.ValidityPeriodState;
|
||||
import de.avatic.lcc.util.exception.internalerror.DatabaseException;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
|
@ -80,6 +82,7 @@ public class ValidityPeriodRepository {
|
|||
* @param id the unique identifier of the validity period.
|
||||
* @return the {@link ValidityPeriod} corresponding to the ID.
|
||||
*/
|
||||
@Transactional
|
||||
public ValidityPeriod getById(Integer id) {
|
||||
String query = "SELECT * FROM validity_period WHERE id = ?";
|
||||
return jdbcTemplate.queryForObject(query, new ValidityPeriodMapper(), id);
|
||||
|
|
@ -89,7 +92,8 @@ public class ValidityPeriodRepository {
|
|||
* Creates a draft validity period if none exists in the database.
|
||||
*/
|
||||
private void createSet() {
|
||||
jdbcTemplate.update("INSERT INTO validity_period (state) SELECT ? WHERE NOT EXISTS (SELECT 1 FROM validity_period WHERE state = ?)", ValidityPeriodState.DRAFT.name(), ValidityPeriodState.DRAFT.name());
|
||||
final Timestamp currentTimestamp = new Timestamp(System.currentTimeMillis());
|
||||
jdbcTemplate.update("INSERT INTO validity_period (state, start_date) SELECT ?, ? WHERE NOT EXISTS (SELECT 1 FROM validity_period WHERE state = ?)", ValidityPeriodState.DRAFT.name(), currentTimestamp, ValidityPeriodState.DRAFT.name());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -97,6 +101,7 @@ public class ValidityPeriodRepository {
|
|||
*
|
||||
* @return the ID of the valid {@link ValidityPeriod}.
|
||||
*/
|
||||
@Transactional
|
||||
public Optional<Integer> getValidPeriodId() {
|
||||
return getValidPeriod().map(ValidityPeriod::getId);
|
||||
}
|
||||
|
|
@ -106,6 +111,7 @@ public class ValidityPeriodRepository {
|
|||
*
|
||||
* @return the {@link ValidityPeriod} in the {@code VALID} state.
|
||||
*/
|
||||
@Transactional
|
||||
public Optional<ValidityPeriod> getValidPeriod() {
|
||||
String query = "SELECT * FROM validity_period WHERE state = ?";
|
||||
var period = jdbcTemplate.query(query, new ValidityPeriodMapper(), ValidityPeriodState.VALID.name());
|
||||
|
|
@ -121,9 +127,17 @@ public class ValidityPeriodRepository {
|
|||
*
|
||||
* @return the {@link ValidityPeriod} in the {@code DRAFT} state.
|
||||
*/
|
||||
@Transactional
|
||||
public ValidityPeriod getDraftPeriod() {
|
||||
createSet();
|
||||
|
||||
String query = "SELECT * FROM validity_period WHERE state = ?";
|
||||
return jdbcTemplate.queryForObject(query, new ValidityPeriodMapper(), ValidityPeriodState.DRAFT.name());
|
||||
var period = jdbcTemplate.query(query, new ValidityPeriodMapper(), ValidityPeriodState.DRAFT.name());
|
||||
|
||||
if(period.isEmpty())
|
||||
throw new DatabaseException("No draft validity period exists");
|
||||
|
||||
return period.getFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -131,6 +145,7 @@ public class ValidityPeriodRepository {
|
|||
*
|
||||
* @return the ID of the draft {@link ValidityPeriod}.
|
||||
*/
|
||||
@Transactional
|
||||
public Integer getDraftPeriodId() {
|
||||
return getDraftPeriod().getId();
|
||||
}
|
||||
|
|
@ -148,8 +163,14 @@ public class ValidityPeriodRepository {
|
|||
if (id == null) return false;
|
||||
|
||||
String query = "SELECT COUNT(*) FROM country_matrix_rate WHERE validity_period_id = ?";
|
||||
var totalCount = jdbcTemplate.queryForObject(query, Integer.class, id);
|
||||
return totalCount != null && totalCount > 0;
|
||||
var matrixCount = jdbcTemplate.queryForObject(query, Integer.class, id);
|
||||
|
||||
query = "SELECT COUNT(*) FROM container_rate WHERE validity_period_id = ?";
|
||||
var containerCount = jdbcTemplate.queryForObject(query, Integer.class, id);
|
||||
|
||||
int totalCount = (matrixCount != null ? matrixCount : 0) + (containerCount != null ? containerCount : 0);
|
||||
|
||||
return totalCount > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -219,6 +240,7 @@ public class ValidityPeriodRepository {
|
|||
return Optional.of(periods.getFirst());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public List<ValidityTuple> findValidityPeriodsWithReportByMaterialId(Integer materialId) {
|
||||
|
||||
String validityPeriodSql = """
|
||||
|
|
@ -231,7 +253,12 @@ public class ValidityPeriodRepository {
|
|||
return jdbcTemplate.query(validityPeriodSql, (rs, cnt) -> new ValidityTuple(rs.getInt("validity_period_id"), rs.getInt("property_set_id")), materialId);
|
||||
}
|
||||
|
||||
;
|
||||
@Transactional
|
||||
public void increaseRenewal(Integer increase) {
|
||||
String sql = "UPDATE validity_period SET renewals = renewals + ? WHERE state = 'VALID'";
|
||||
jdbcTemplate.update(sql, increase);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Maps rows of a {@link ResultSet} to {@link ValidityPeriod} objects.
|
||||
|
|
@ -254,6 +281,8 @@ public class ValidityPeriodRepository {
|
|||
String stateStr = rs.getString("state");
|
||||
period.setState(stateStr != null ? ValidityPeriodState.valueOf(stateStr) : null);
|
||||
|
||||
period.setRenewals(rs.getInt("renewals"));
|
||||
|
||||
|
||||
return period;
|
||||
|
||||
|
|
|
|||
|
|
@ -3,9 +3,7 @@ package de.avatic.lcc.service.bulk;
|
|||
import de.avatic.lcc.model.bulk.BulkFileTypes;
|
||||
import de.avatic.lcc.model.bulk.BulkOperation;
|
||||
import de.avatic.lcc.repositories.NodeRepository;
|
||||
import de.avatic.lcc.service.bulk.bulkImport.MaterialBulkImportService;
|
||||
import de.avatic.lcc.service.bulk.bulkImport.NodeBulkImportService;
|
||||
import de.avatic.lcc.service.bulk.bulkImport.PackagingBulkImportService;
|
||||
import de.avatic.lcc.service.bulk.bulkImport.*;
|
||||
import de.avatic.lcc.service.excelMapper.*;
|
||||
import de.avatic.lcc.service.transformer.generic.NodeTransformer;
|
||||
import de.avatic.lcc.util.exception.internalerror.ExcelValidationError;
|
||||
|
|
@ -32,8 +30,10 @@ public class BulkImportService {
|
|||
private final NodeBulkImportService nodeBulkImportService;
|
||||
private final PackagingBulkImportService packagingBulkImportService;
|
||||
private final MaterialBulkImportService materialBulkImportService;
|
||||
private final MatrixRateImportService matrixRateImportService;
|
||||
private final ContainerRateImportService containerRateImportService;
|
||||
|
||||
public BulkImportService(MatrixRateExcelMapper matrixRateExcelMapper, ContainerRateExcelMapper containerRateExcelMapper, MaterialExcelMapper materialExcelMapper, PackagingExcelMapper packagingExcelMapper, NodeExcelMapper nodeExcelMapper, NodeRepository nodeRepository, NodeTransformer nodeTransformer, NodeBulkImportService nodeBulkImportService, PackagingBulkImportService packagingBulkImportService, MaterialBulkImportService materialBulkImportService) {
|
||||
public BulkImportService(MatrixRateExcelMapper matrixRateExcelMapper, ContainerRateExcelMapper containerRateExcelMapper, MaterialExcelMapper materialExcelMapper, PackagingExcelMapper packagingExcelMapper, NodeExcelMapper nodeExcelMapper, NodeRepository nodeRepository, NodeTransformer nodeTransformer, NodeBulkImportService nodeBulkImportService, PackagingBulkImportService packagingBulkImportService, MaterialBulkImportService materialBulkImportService, MatrixRateImportService matrixRateImportService, ContainerRateImportService containerRateImportService) {
|
||||
this.matrixRateExcelMapper = matrixRateExcelMapper;
|
||||
this.containerRateExcelMapper = containerRateExcelMapper;
|
||||
this.materialExcelMapper = materialExcelMapper;
|
||||
|
|
@ -44,6 +44,8 @@ public class BulkImportService {
|
|||
this.nodeBulkImportService = nodeBulkImportService;
|
||||
this.packagingBulkImportService = packagingBulkImportService;
|
||||
this.materialBulkImportService = materialBulkImportService;
|
||||
this.matrixRateImportService = matrixRateImportService;
|
||||
this.containerRateImportService = containerRateImportService;
|
||||
}
|
||||
|
||||
public void processOperation(BulkOperation op) throws IOException {
|
||||
|
|
@ -56,14 +58,18 @@ public class BulkImportService {
|
|||
try (Workbook workbook = new XSSFWorkbook(in)) {
|
||||
Sheet sheet = workbook.getSheet(BulkFileTypes.valueOf(type.name()).getSheetName());
|
||||
|
||||
if(sheet == null)
|
||||
throw new ExcelValidationError("Provided file does not contain a sheet named " + BulkFileTypes.valueOf(type.name()).getSheetName());
|
||||
|
||||
|
||||
switch (type) {
|
||||
case CONTAINER_RATE:
|
||||
var containerRates = containerRateExcelMapper.extractSheet(sheet);
|
||||
containerRateImportService.processContainerRates(containerRates);
|
||||
break;
|
||||
case COUNTRY_MATRIX:
|
||||
var matrixRates = matrixRateExcelMapper.extractSheet(sheet);
|
||||
matrixRates.forEach(System.out::println);
|
||||
matrixRateImportService.processMatrixRates(matrixRates);
|
||||
break;
|
||||
case MATERIAL:
|
||||
var materials = materialExcelMapper.extractSheet(sheet);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
package de.avatic.lcc.service.bulk.bulkImport;
|
||||
|
||||
import de.avatic.lcc.model.rates.ContainerRate;
|
||||
import de.avatic.lcc.repositories.rates.ContainerRateRepository;
|
||||
import de.avatic.lcc.repositories.rates.ValidityPeriodRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class ContainerRateImportService {
|
||||
|
||||
private final ValidityPeriodRepository validityPeriodRepository;
|
||||
private final ContainerRateRepository containerRateRepository;
|
||||
|
||||
public ContainerRateImportService(ValidityPeriodRepository validityPeriodRepository, ContainerRateRepository containerRateRepository) {
|
||||
this.validityPeriodRepository = validityPeriodRepository;
|
||||
this.containerRateRepository = containerRateRepository;
|
||||
}
|
||||
|
||||
public void processContainerRates(List<ContainerRate> containerRates) {
|
||||
Integer periodId = validityPeriodRepository.getDraftPeriodId();
|
||||
containerRates.forEach(rate -> processContainerRate(rate,periodId));
|
||||
}
|
||||
|
||||
public void processContainerRate(ContainerRate containerRate, Integer periodId) {
|
||||
containerRate.setValidityPeriodId(periodId);
|
||||
containerRateRepository.insert(containerRate);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package de.avatic.lcc.service.bulk.bulkImport;
|
||||
|
||||
import de.avatic.lcc.model.rates.MatrixRate;
|
||||
import de.avatic.lcc.repositories.rates.MatrixRateRepository;
|
||||
import de.avatic.lcc.repositories.rates.ValidityPeriodRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class MatrixRateImportService {
|
||||
|
||||
private final MatrixRateRepository matrixRateRepository;
|
||||
private final ValidityPeriodRepository validityPeriodRepository;
|
||||
|
||||
public MatrixRateImportService(MatrixRateRepository matrixRateRepository, ValidityPeriodRepository validityPeriodRepository) {
|
||||
this.matrixRateRepository = matrixRateRepository;
|
||||
this.validityPeriodRepository = validityPeriodRepository;
|
||||
}
|
||||
|
||||
public void processMatrixRate(MatrixRate rate, Integer periodId) {
|
||||
rate.setValidityPeriodId(periodId);
|
||||
matrixRateRepository.insert(rate);
|
||||
}
|
||||
|
||||
public void processMatrixRates(List<MatrixRate> matrixRates) {
|
||||
Integer periodId = validityPeriodRepository.getDraftPeriodId();
|
||||
matrixRates.forEach(rate -> processMatrixRate(rate,periodId));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,15 @@
|
|||
package de.avatic.lcc.service.configuration;
|
||||
|
||||
import de.avatic.lcc.repositories.rates.MatrixRateRepository;
|
||||
import de.avatic.lcc.dto.configuration.rates.StagedRatesDTO;
|
||||
import de.avatic.lcc.model.properties.SystemPropertyMappingId;
|
||||
import de.avatic.lcc.model.rates.ValidityPeriod;
|
||||
import de.avatic.lcc.repositories.rates.ValidityPeriodRepository;
|
||||
import de.avatic.lcc.service.access.PropertyService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* A service class responsible for approving or verifying rate drafts.
|
||||
|
|
@ -15,9 +21,11 @@ public class RateApprovalService {
|
|||
|
||||
|
||||
private final ValidityPeriodRepository validityPeriodRepository;
|
||||
private final PropertyService propertyService;
|
||||
|
||||
public RateApprovalService(ValidityPeriodRepository validityPeriodRepository) {
|
||||
public RateApprovalService(ValidityPeriodRepository validityPeriodRepository, PropertyService propertyService) {
|
||||
this.validityPeriodRepository = validityPeriodRepository;
|
||||
this.propertyService = propertyService;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -25,14 +33,65 @@ public class RateApprovalService {
|
|||
*
|
||||
* @return {@code true} if rate drafts exist, {@code false} otherwise.
|
||||
*/
|
||||
public boolean hasRateDrafts() {
|
||||
return validityPeriodRepository.hasRateDrafts();
|
||||
public StagedRatesDTO getStagedRateDTO() {
|
||||
|
||||
var optValidPeriod = validityPeriodRepository.getValidPeriod();
|
||||
StagedRatesDTO stagedRatesDTO = new StagedRatesDTO();
|
||||
|
||||
stagedRatesDTO.setExpires(optValidPeriod.map(this::expires).orElse(null));
|
||||
stagedRatesDTO.setStagedChanges(validityPeriodRepository.hasRateDrafts());
|
||||
|
||||
return stagedRatesDTO;
|
||||
}
|
||||
|
||||
private Integer expires(ValidityPeriod validPeriod) {
|
||||
Optional<Integer> validityProperty = propertyService.getProperty(SystemPropertyMappingId.VALID_DAYS);
|
||||
|
||||
if (validityProperty.isPresent()) {
|
||||
var validDays = validityProperty.get() * (validPeriod.getRenewals() + 1);
|
||||
return Math.toIntExact(Duration.between(LocalDateTime.now(), validPeriod.getStartDate().plusDays(validDays)).toDays());
|
||||
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Integer getRenewalIncrease() {
|
||||
Optional<Integer> validityProperty = propertyService.getProperty(SystemPropertyMappingId.VALID_DAYS);
|
||||
var optValidPeriod = validityPeriodRepository.getValidPeriod();
|
||||
|
||||
if (validityProperty.isPresent() && optValidPeriod.isPresent()) {
|
||||
var validPeriod = optValidPeriod.get();
|
||||
var validDays = validityProperty.get() * (validPeriod.getRenewals() + 1);
|
||||
|
||||
var expiresIn = Math.toIntExact(Duration.between(LocalDateTime.now(), validPeriod.getStartDate().plusDays(validDays)).toDays()) - 10;
|
||||
|
||||
if (expiresIn < 0) {
|
||||
return ((-1 * expiresIn) / validDays) + 1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Approves and applies all staged (draft) rates.
|
||||
*/
|
||||
public void approveRateDrafts() {
|
||||
|
||||
if (validityPeriodRepository.hasRateDrafts()) {
|
||||
validityPeriodRepository.applyDraft();
|
||||
} else {
|
||||
Integer increase = getRenewalIncrease();
|
||||
|
||||
if (increase != null) {
|
||||
validityPeriodRepository.increaseRenewal(increase);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -218,6 +218,7 @@ CREATE TABLE IF NOT EXISTS validity_period
|
|||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
start_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
end_date TIMESTAMP DEFAULT NULL,
|
||||
renewals INT UNSIGNED DEFAULT 0,
|
||||
state CHAR(8) NOT NULL CHECK (state IN ('DRAFT', 'VALID', 'INVALID', 'EXPIRED')),
|
||||
CONSTRAINT `chk_validity_date_range` CHECK (`end_date` IS NULL OR `end_date` > `start_date`)
|
||||
);
|
||||
|
|
@ -237,7 +238,8 @@ CREATE TABLE IF NOT EXISTS container_rate
|
|||
FOREIGN KEY (to_node_id) REFERENCES node (id),
|
||||
FOREIGN KEY (validity_period_id) REFERENCES validity_period (id),
|
||||
INDEX idx_from_to_nodes (from_node_id, to_node_id),
|
||||
INDEX idx_validity_period_id (validity_period_id)
|
||||
INDEX idx_validity_period_id (validity_period_id),
|
||||
CONSTRAINT uk_container_rate_unique UNIQUE (from_node_id, to_node_id, validity_period_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS country_matrix_rate
|
||||
|
|
@ -251,7 +253,8 @@ CREATE TABLE IF NOT EXISTS country_matrix_rate
|
|||
FOREIGN KEY (to_country_id) REFERENCES country (id),
|
||||
FOREIGN KEY (validity_period_id) REFERENCES validity_period (id),
|
||||
INDEX idx_from_to_country (from_country_id, to_country_id),
|
||||
INDEX idx_validity_period_id (validity_period_id)
|
||||
INDEX idx_validity_period_id (validity_period_id),
|
||||
CONSTRAINT uk_country_matrix_rate_unique UNIQUE (from_country_id, to_country_id, validity_period_id)
|
||||
);
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue