FRONTEND: bulk download working.
Added views for nodes, materials and rates
This commit is contained in:
parent
3b683018de
commit
e5bd56d3a9
55 changed files with 2174 additions and 310 deletions
|
|
@ -17,13 +17,13 @@ const performRequest = async (requestingStore, method, url, body, expectResponse
|
|||
const request = {url: url, params: params, expectResponse: expectResponse, expectedException: expectedException};
|
||||
logger.info("Request:", request);
|
||||
|
||||
const data = await executeRequest(requestingStore, request);
|
||||
const resp = await executeRequest(requestingStore, request);
|
||||
|
||||
logger.info("Response:", data);
|
||||
return data;
|
||||
logger.info("Response:", resp);
|
||||
return resp;
|
||||
}
|
||||
|
||||
const performDownload = async (url, expectResponse = true, expectedException = null) => {
|
||||
const performDownload = async (url, toFile, expectResponse = true, expectedException = null) => {
|
||||
|
||||
const params = {
|
||||
method: 'GET',
|
||||
|
|
@ -32,15 +32,15 @@ const performDownload = async (url, expectResponse = true, expectedException = n
|
|||
const request = {url: url, params: params, expectResponse: expectResponse, expectedException: expectedException, type: 'blob'};
|
||||
logger.info("Request:", request);
|
||||
|
||||
const blob = await executeRequest(null, request);
|
||||
const resp = await executeRequest(null, request);
|
||||
|
||||
|
||||
const downloadUrl = window.URL.createObjectURL(blob);
|
||||
const downloadUrl = window.URL.createObjectURL(resp.data);
|
||||
|
||||
// Create temporary link element and trigger download
|
||||
const link = document.createElement('a');
|
||||
link.href = downloadUrl;
|
||||
link.download = `export.xlsx`; // or get filename from response headers
|
||||
link.download = toFile;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
|
|
@ -62,10 +62,10 @@ const performUpload = async (url, file, expectResponse = true, expectedException
|
|||
const request = {url: url, params: params, expectResponse: expectResponse, expectedException: expectedException};
|
||||
logger.info("Request:", request);
|
||||
|
||||
const processId = await executeRequest(null, request);
|
||||
const resp = await executeRequest(null, request);
|
||||
|
||||
logger.info("Response:", processId);
|
||||
return processId;
|
||||
logger.info("Response:", resp.data);
|
||||
return resp.data;
|
||||
}
|
||||
|
||||
function handleErrorResponse(data, requestingStore, request) {
|
||||
|
|
@ -104,6 +104,7 @@ const executeRequest = async (requestingStore, request) => {
|
|||
throw e;
|
||||
});
|
||||
|
||||
|
||||
let data = null;
|
||||
if (request.expectResponse) {
|
||||
try {
|
||||
|
|
@ -149,7 +150,7 @@ const executeRequest = async (requestingStore, request) => {
|
|||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
return {data: data, headers: response.headers};
|
||||
}
|
||||
|
||||
export default performRequest;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="box" :class="{'box-shadowed': variant === 'shadow', 'box-bordered': variant === 'border', 'stretch-content': stretchContent}">
|
||||
<div class="box" :class="{'box-padding': !removePadding,'box-shadowed': variant === 'shadow', 'box-bordered': variant === 'border', 'stretch-content': stretchContent}">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -17,6 +17,10 @@ export default {
|
|||
stretchContent: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
removePadding: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -35,7 +39,6 @@ export default {
|
|||
|
||||
.box-bordered {
|
||||
border: 0.1rem solid #E3EDFF;
|
||||
|
||||
}
|
||||
|
||||
.box {
|
||||
|
|
@ -43,6 +46,10 @@ export default {
|
|||
background: white;
|
||||
border-radius: 0.8rem;
|
||||
position: relative;
|
||||
|
||||
}
|
||||
|
||||
.box-padding {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
|
|
|
|||
265
src/frontend/src/components/UI/DataTable.vue
Normal file
265
src/frontend/src/components/UI/DataTable.vue
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
<template>
|
||||
<div class="table-container">
|
||||
<!-- Search Bar -->
|
||||
<div class="search-container">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
class="search-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="loading">
|
||||
Loading...
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div v-else-if="error" class="error">
|
||||
Error: {{ error }}
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div v-else class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="column in columns" :key="column.key" class="table-header">
|
||||
{{ column.label }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="filteredData.length === 0" class="no-data">
|
||||
<td :colspan="columns.length" class="no-data-cell">
|
||||
{{ searchQuery ? 'No results found' : 'No data available' }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-else v-for="(item, index) in filteredData" :key="index" class="table-row">
|
||||
<td v-for="column in columns" :key="column.key" class="table-cell">
|
||||
{{ getCellValue(item, column) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'DataTable',
|
||||
props: {
|
||||
// Callback function that returns array of objects
|
||||
dataSource: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
// Array of column configurations
|
||||
// Each column should have: { key: 'fieldName', label: 'Display Name', formatter: fn (optional) }
|
||||
columns: {
|
||||
type: Array,
|
||||
required: true,
|
||||
validator(columns) {
|
||||
return columns.every(col => col.key && col.label);
|
||||
}
|
||||
},
|
||||
// Fields to search in (if not provided, searches all column keys)
|
||||
searchFields: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
// Auto-load data on mount
|
||||
autoLoad: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
data: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
searchQuery: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
filteredData() {
|
||||
if (!this.searchQuery.trim()) {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
const query = this.searchQuery.toLowerCase().trim();
|
||||
const fieldsToSearch = this.searchFields.length > 0
|
||||
? this.searchFields
|
||||
: this.columns.map(col => col.key);
|
||||
|
||||
return this.data.filter(item => {
|
||||
return fieldsToSearch.some(field => {
|
||||
const value = this.getNestedValue(item, field);
|
||||
return value && value.toString().toLowerCase().includes(query);
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.autoLoad) {
|
||||
this.loadData();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadData() {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
const result = await this.dataSource();
|
||||
if (Array.isArray(result)) {
|
||||
this.data = result;
|
||||
} else {
|
||||
throw new Error('Data source must return an array');
|
||||
}
|
||||
} catch (err) {
|
||||
this.error = err.message || 'Failed to load data';
|
||||
this.data = [];
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Get nested object values using dot notation (e.g., 'user.profile.name')
|
||||
getNestedValue(obj, path) {
|
||||
return path.split('.').reduce((current, key) => {
|
||||
return current && current[key] !== undefined ? current[key] : null;
|
||||
}, obj);
|
||||
},
|
||||
|
||||
// Get cell value with optional formatting
|
||||
getCellValue(item, column) {
|
||||
const rawValue = this.getNestedValue(item, column.key);
|
||||
|
||||
if (column.formatter && typeof column.formatter === 'function') {
|
||||
return column.formatter(rawValue, item);
|
||||
}
|
||||
|
||||
return rawValue !== null && rawValue !== undefined ? rawValue : '';
|
||||
},
|
||||
|
||||
// Public method to refresh data
|
||||
refresh() {
|
||||
this.loadData();
|
||||
},
|
||||
|
||||
// Public method to clear search
|
||||
clearSearch() {
|
||||
this.searchQuery = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.table-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
.loading, .error {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #dc3545;
|
||||
background-color: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
overflow-x: auto;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
background-color: #f8f9fa;
|
||||
color: #495057;
|
||||
font-weight: 600;
|
||||
padding: 12px 16px;
|
||||
text-align: left;
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.table-row:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.table-row:nth-child(even) {
|
||||
background-color: #fdfdfd;
|
||||
}
|
||||
|
||||
.table-cell {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.no-data-cell {
|
||||
padding: 40px 16px;
|
||||
text-align: center;
|
||||
color: #6c757d;
|
||||
font-style: italic;
|
||||
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.search-input {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.table-header,
|
||||
.table-cell {
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
201
src/frontend/src/components/UI/Pagination.vue
Normal file
201
src/frontend/src/components/UI/Pagination.vue
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
```vue
|
||||
<template>
|
||||
<div class="pagination" v-if="pageCount > 1">
|
||||
<!-- Previous button -->
|
||||
<button
|
||||
class="pagination-btn"
|
||||
:disabled="page <= 1"
|
||||
@click="goToPage(page - 1)"
|
||||
>
|
||||
<PhCaretLeft :size="18" /> Previous
|
||||
</button>
|
||||
|
||||
<!-- First page -->
|
||||
<button
|
||||
v-if="showFirstPage"
|
||||
class="pagination-btn page-number"
|
||||
:class="{ active: page === 1 }"
|
||||
@click="goToPage(1)"
|
||||
>
|
||||
1
|
||||
</button>
|
||||
|
||||
<!-- First ellipsis -->
|
||||
<span v-if="showFirstEllipsis" class="ellipsis">...</span>
|
||||
|
||||
<!-- Page numbers around current page -->
|
||||
<button
|
||||
v-for="pageNum in visiblePages"
|
||||
:key="pageNum"
|
||||
class="pagination-btn page-number"
|
||||
:class="{ active: page === pageNum }"
|
||||
@click="goToPage(pageNum)"
|
||||
>
|
||||
{{ pageNum }}
|
||||
</button>
|
||||
|
||||
<!-- Last ellipsis -->
|
||||
<span v-if="showLastEllipsis" class="ellipsis">...</span>
|
||||
|
||||
<!-- Last page -->
|
||||
<button
|
||||
v-if="showLastPage"
|
||||
class="pagination-btn page-number"
|
||||
:class="{ active: page === pageCount }"
|
||||
@click="goToPage(pageCount)"
|
||||
>
|
||||
{{ pageCount }}
|
||||
</button>
|
||||
|
||||
<!-- Next button -->
|
||||
<button
|
||||
class="pagination-btn"
|
||||
:disabled="page >= pageCount"
|
||||
@click="goToPage(page + 1)"
|
||||
>
|
||||
Next <PhCaretRight :size="18" />
|
||||
</button>
|
||||
|
||||
<!-- Page info -->
|
||||
<div class="page-info">
|
||||
Page {{ page }} of {{ pageCount }} ({{ totalCount }} total items)
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {PhCaretLeft, PhCaretRight} from "@phosphor-icons/vue";
|
||||
|
||||
export default {
|
||||
name: "Pagination",
|
||||
components: {PhCaretLeft, PhCaretRight},
|
||||
props: {
|
||||
page: {
|
||||
type: Number,
|
||||
required: true,
|
||||
default: 1
|
||||
},
|
||||
pageCount: {
|
||||
type: Number,
|
||||
required: true,
|
||||
default: 1
|
||||
},
|
||||
totalCount: {
|
||||
type: Number,
|
||||
required: true,
|
||||
default: 0
|
||||
},
|
||||
maxVisiblePages: {
|
||||
type: Number,
|
||||
default: 5
|
||||
}
|
||||
},
|
||||
emits: ['page-change'],
|
||||
computed: {
|
||||
visiblePages() {
|
||||
const delta = Math.floor(this.maxVisiblePages / 2);
|
||||
let start = Math.max(2, this.page - delta);
|
||||
let end = Math.min(this.pageCount - 1, this.page + delta);
|
||||
|
||||
// Adjust if we're near the beginning or end
|
||||
if (this.page <= delta + 1) {
|
||||
end = Math.min(this.pageCount - 1, this.maxVisiblePages);
|
||||
}
|
||||
if (this.page >= this.pageCount - delta) {
|
||||
start = Math.max(2, this.pageCount - this.maxVisiblePages + 1);
|
||||
}
|
||||
|
||||
const pages = [];
|
||||
for (let i = start; i <= end; i++) {
|
||||
if (i !== 1 && i !== this.pageCount) {
|
||||
pages.push(i);
|
||||
}
|
||||
}
|
||||
return pages;
|
||||
},
|
||||
showFirstPage() {
|
||||
return this.pageCount > 1;
|
||||
},
|
||||
showLastPage() {
|
||||
return this.pageCount > 1 && this.pageCount !== 1;
|
||||
},
|
||||
showFirstEllipsis() {
|
||||
return this.visiblePages.length > 0 && this.visiblePages[0] > 2;
|
||||
},
|
||||
showLastEllipsis() {
|
||||
return this.visiblePages.length > 0 &&
|
||||
this.visiblePages[this.visiblePages.length - 1] < this.pageCount - 1;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goToPage(pageNumber) {
|
||||
if (pageNumber >= 1 && pageNumber <= this.pageCount && pageNumber !== this.page) {
|
||||
this.$emit('page-change', pageNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin: 20px 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pagination-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
padding: 8px 12px;
|
||||
border: 0.2rem solid #E3EDFF;
|
||||
background: transparent;
|
||||
color: #002F54;
|
||||
cursor: pointer;
|
||||
border-radius: 0.4rem;
|
||||
font-size: 1.4rem;
|
||||
font-family: inherit;
|
||||
transition: all 0.2s ease;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.pagination-btn:hover:not(:disabled) {
|
||||
background: #EEF4FF;
|
||||
border-color: #8DB3FE;
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
.pagination-btn:disabled {
|
||||
background: rgba(238, 244, 255, 0.2);
|
||||
border-color: rgba(141, 179, 254, 0.2);
|
||||
color: rgba(0, 47, 84, 0.2);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.pagination-btn.active {
|
||||
background: #EEF4FF;
|
||||
|
||||
}
|
||||
|
||||
.page-number {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
padding: 8px 4px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.page-info {
|
||||
margin-left: 1.6rem;
|
||||
font-size: 1.2rem;
|
||||
color: #6b7280;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
117
src/frontend/src/components/UI/SearchBar.vue
Normal file
117
src/frontend/src/components/UI/SearchBar.vue
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
<template>
|
||||
<div class="search-bar-container">
|
||||
<div class="search-wrapper">
|
||||
<PhMagnifyingGlass :size="32" weight="bold" class="search-icon"/>
|
||||
<input
|
||||
@input="inputChanged"
|
||||
v-model="value"
|
||||
type="text"
|
||||
class="search-input"
|
||||
:placeholder="placeholder"
|
||||
autocomplete="off"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {useDebounceFn} from "@vueuse/core";
|
||||
|
||||
export default {
|
||||
name: "SearchBar",
|
||||
emits: ['update:modelValue', 'inputChanged'],
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'Search term'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
debouncedSearch: null,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.debouncedSearch = useDebounceFn((query) => {
|
||||
this.handleSearch(query);
|
||||
}, 300);
|
||||
},
|
||||
methods: {
|
||||
handleSearch(query) {
|
||||
this.$emit('inputChanged', query);
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
value: {
|
||||
get() {
|
||||
return this.modelValue;
|
||||
},
|
||||
set(value) {
|
||||
this.debouncedSearch(value);
|
||||
this.$emit('update:modelValue', value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.search-bar-container {
|
||||
background: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2.4rem;
|
||||
}
|
||||
|
||||
|
||||
.search-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: white;
|
||||
border-radius: 0.4rem;
|
||||
padding: 0.6rem 1.2rem;
|
||||
/* box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(10px);*/
|
||||
border: 0.2rem solid #E3EDFF;
|
||||
transition: all 0.1s ease;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.search-wrapper:hover {
|
||||
background: #EEF4FF;
|
||||
border: 0.2rem solid #8DB3FE;
|
||||
/*transform: translateY(2px);*/
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
|
||||
.search-icon {
|
||||
width: 1.8rem;
|
||||
height: 1.8rem;
|
||||
margin-right: 1.2rem;
|
||||
color: #6B869C;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1 1 auto;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 1.4rem;
|
||||
color: #002F54;
|
||||
background: transparent;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: #6B869C;
|
||||
font-weight: 400;
|
||||
}
|
||||
</style>
|
||||
265
src/frontend/src/components/UI/TableView.vue
Normal file
265
src/frontend/src/components/UI/TableView.vue
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
<template>
|
||||
<div class="table-container">
|
||||
|
||||
<div class="table-search-bar-container">
|
||||
|
||||
<search-bar class="search-bar" v-model="filter" @input-changed="reload"></search-bar>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="column in columns" :key="column.key" class="table-header" :class="getAlignment(column.align)">
|
||||
{{ column.label }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<transition name="list-container" mode="out-in">
|
||||
<tbody v-if="data.length === 0">
|
||||
<tr class="no-data">
|
||||
<td :colspan="columns.length" class="no-data-cell">
|
||||
{{ filter ? 'No results found' : 'No data available' }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-else-if="loading">
|
||||
<tr class="no-data">
|
||||
<td :colspan="columns.length" class="no-data-cell">
|
||||
<spinner class="space-around"></spinner>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-else>
|
||||
<tr v-for="(item, index) in data" :key="index" class="table-row">
|
||||
<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>
|
||||
<component v-else
|
||||
:is="getCellValue(item, column)"
|
||||
weight="regular"
|
||||
size="24"
|
||||
class="table-icon"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</transition>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<pagination :total-count="totalCount" :page-count="pageCount" :page="page" @page-change="updatePage"></pagination>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import Spinner from "@/components/UI/Spinner.vue";
|
||||
import Checkbox from "@/components/UI/Checkbox.vue";
|
||||
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";
|
||||
|
||||
export default {
|
||||
name: "TableView",
|
||||
components: {Pagination, Box, SearchBar, BasicButton, Checkbox, Spinner},
|
||||
props: {
|
||||
dataSource: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
columns: {
|
||||
type: Array,
|
||||
required: true,
|
||||
validator(columns) {
|
||||
return columns.every(col => col.key && col.label);
|
||||
}
|
||||
},
|
||||
page: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
pageSize: {
|
||||
type: Number,
|
||||
default: 10
|
||||
},
|
||||
pageCount: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
totalCount: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.reload();
|
||||
},
|
||||
computed: {},
|
||||
methods: {
|
||||
async updatePage(page) {
|
||||
this.reload(this.filter, page);
|
||||
},
|
||||
async reload(filter, page = 1) {
|
||||
this.loading = true;
|
||||
|
||||
const query = {searchTerm: filter, page: page, pageSize: this.pageSize};
|
||||
|
||||
this.data = await this.dataSource(query);
|
||||
this.loading = false;
|
||||
},
|
||||
getNestedValue(obj, path) {
|
||||
return path.split('.').reduce((current, key) => {
|
||||
return current && current[key] !== undefined ? current[key] : null;
|
||||
}, obj);
|
||||
},
|
||||
getAlignment(alignment) {
|
||||
if (alignment == null || alignment === 'left') {
|
||||
return 'table-cell--left';
|
||||
} else if (alignment === 'right') {
|
||||
return 'table-cell--right';
|
||||
} else if (alignment === 'center') {
|
||||
return 'table-cell--center';
|
||||
}
|
||||
},
|
||||
getCellValue(item, column) {
|
||||
const rawValue = this.getNestedValue(item, column.key);
|
||||
|
||||
if (column.iconResolver && typeof column.iconResolver === 'function') {
|
||||
return column.iconResolver(rawValue, item);
|
||||
}
|
||||
|
||||
if (column.iconResolver) {
|
||||
return 'PhImageBroken';
|
||||
}
|
||||
|
||||
if (column.formatter && typeof column.formatter === 'function') {
|
||||
return column.formatter(rawValue, item);
|
||||
}
|
||||
|
||||
return rawValue !== null && rawValue !== undefined ? rawValue : '';
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
data: [],
|
||||
filter: '',
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
/* Container Animation */
|
||||
.list-container-enter-active, .list-container-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.list-container-enter-from, .list-container-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.table-icon {
|
||||
transition: all 0.1s ease-in-out;
|
||||
color: #6B869C;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.table-search-bar-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 1.6rem;
|
||||
}
|
||||
|
||||
.no-data-cell {
|
||||
text-align: center;
|
||||
font-size: 1.6rem;
|
||||
color: #6B869C;
|
||||
padding: 2.4rem;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 1.6rem;
|
||||
overflow: visible;
|
||||
border-radius: 0.8rem;
|
||||
box-shadow: 0 0.4rem 0.6rem -0.1rem rgba(0, 0, 0, 0.1);
|
||||
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.space-around {
|
||||
margin: 3rem;
|
||||
}
|
||||
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
background-color: transparent;
|
||||
border: 0 solid #E3EDFF;
|
||||
|
||||
}
|
||||
|
||||
.table-header {
|
||||
|
||||
border-bottom: 1px solid rgba(107, 134, 156, 0.2);
|
||||
font-weight: 500;
|
||||
font-size: 1.4rem;
|
||||
color: #6B869C;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08rem;
|
||||
text-align: left;
|
||||
padding: 2.4rem;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
font-weight: 400;
|
||||
font-size: 1.4rem;
|
||||
color: #6B869C;
|
||||
}
|
||||
|
||||
.table-cell {
|
||||
padding: 2.4rem;
|
||||
background-color: transparent;
|
||||
border-bottom: 0.1rem solid #E3EDFF;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.table-cell--left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
||||
.table-cell--right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.table-cell--center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.table-row:hover {
|
||||
background-color: rgba(107, 134, 156, 0.05);
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
|
@ -2,6 +2,12 @@
|
|||
<div>
|
||||
<div class="bulk-operations-container">
|
||||
|
||||
<box variant="border" class="bulk-operation-box-status-container">
|
||||
<div class="bulk-operations-sub-container">
|
||||
<div class="bulk-operation-header">Bulk operation status</div>
|
||||
</div>
|
||||
</box>
|
||||
|
||||
<box variant="border" class="bulk-operations-box-container">
|
||||
<div class="bulk-operations-sub-container">
|
||||
|
||||
|
|
@ -14,14 +20,27 @@
|
|||
<div class="bulk-operation-caption">dataset</div>
|
||||
<div class="bulk-operation-data">
|
||||
<radio-option name="export-dataset" value="NODE" v-model="exportDataset">nodes</radio-option>
|
||||
<radio-option name="export-dataset" value="COUNTRY_MATRIX" v-model="exportDataset">kilometer rates</radio-option>
|
||||
<radio-option name="export-dataset" value="CONTAINER_RATE" v-model="exportDataset">container rates</radio-option>
|
||||
<radio-option name="export-dataset" value="COUNTRY_MATRIX" v-model="exportDataset">kilometer rates
|
||||
</radio-option>
|
||||
<radio-option name="export-dataset" value="CONTAINER_RATE" v-model="exportDataset">container rates
|
||||
</radio-option>
|
||||
<radio-option name="export-dataset" value="MATERIAL" v-model="exportDataset">materials</radio-option>
|
||||
<radio-option name="export-dataset" value="PACKAGING" v-model="exportDataset">packaging</radio-option>
|
||||
</div>
|
||||
|
||||
<div class="bulk-operation-caption">validity period</div>
|
||||
<div class="bulk-operation-data">
|
||||
<dropdown :options="periods"
|
||||
emptyText="No property set available"
|
||||
class="period-select"
|
||||
placeholder="Select a property set"
|
||||
v-model="selectedPeriod"
|
||||
:disabled="!showValidityPeriod"
|
||||
></dropdown>
|
||||
</div>
|
||||
|
||||
<div class="bulk-operation-action-footer">
|
||||
<basic-button @click="downloadFile" icon="download">Export</basic-button>
|
||||
<basic-button @click="downloadFile" icon="download">Schedule Export</basic-button>
|
||||
</div>
|
||||
</div>
|
||||
</box>
|
||||
|
|
@ -33,8 +52,10 @@
|
|||
<div class="bulk-operation-caption">dataset</div>
|
||||
<div class="bulk-operation-data">
|
||||
<radio-option name="import-dataset" value="NODE" v-model="importDataset">nodes</radio-option>
|
||||
<radio-option name="import-dataset" value="COUNTRY_MATRIX" v-model="importDataset">kilometer rates</radio-option>
|
||||
<radio-option name="import-dataset" value="CONTAINER_RATE" v-model="importDataset">container rates</radio-option>
|
||||
<radio-option name="import-dataset" value="COUNTRY_MATRIX" v-model="importDataset">kilometer rates
|
||||
</radio-option>
|
||||
<radio-option name="import-dataset" value="CONTAINER_RATE" v-model="importDataset">container rates
|
||||
</radio-option>
|
||||
<radio-option name="import-dataset" value="MATERIAL" v-model="importDataset">materials</radio-option>
|
||||
<radio-option name="import-dataset" value="PACKAGING" v-model="importDataset">packaging</radio-option>
|
||||
</div>
|
||||
|
|
@ -50,7 +71,8 @@
|
|||
<label for="upload" class="file-button-label">
|
||||
Choose file
|
||||
</label>
|
||||
<input @change="inputFile" class="file-select-button" type="file" id="upload" name="upload" accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" />
|
||||
<input @change="inputFile" class="file-select-button" type="file" id="upload" name="upload"
|
||||
accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"/>
|
||||
</div>
|
||||
|
||||
<div id="selectedFile" class="selected-file" v-if="selectedFileName">{{ selectedFileName }}</div>
|
||||
|
|
@ -77,10 +99,13 @@ import BasicButton from "@/components/UI/BasicButton.vue";
|
|||
import RadioOption from "@/components/UI/RadioOption.vue";
|
||||
import performRequest, {performUpload, performDownload} from "@/backend.js";
|
||||
import {config} from "@/config.js";
|
||||
import Dropdown from "@/components/UI/Dropdown.vue";
|
||||
import {mapStores} from "pinia";
|
||||
import {useValidityPeriodStore} from "@/store/validityPeriod.js";
|
||||
|
||||
export default {
|
||||
name: "BulkOperations",
|
||||
components: {RadioOption, BasicButton, Box},
|
||||
components: {Dropdown, RadioOption, BasicButton, Box},
|
||||
data() {
|
||||
return {
|
||||
exportType: "templates",
|
||||
|
|
@ -93,11 +118,49 @@ export default {
|
|||
processId: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useValidityPeriodStore),
|
||||
showValidityPeriod() {
|
||||
return this.exportType === "download" && (this.exportDataset === "COUNTRY_MATRIX" || this.exportDataset === "CONTAINER_RATE");
|
||||
},
|
||||
selectedPeriod: {
|
||||
get() {
|
||||
return this.validityPeriodStore.getSelectedPeriod
|
||||
},
|
||||
async set(value) {
|
||||
this.validityPeriodStore.setSelectedPeriod(value);
|
||||
}
|
||||
},
|
||||
periods() {
|
||||
const periods = [];
|
||||
|
||||
const ps = this.validityPeriodStore.getPeriods;
|
||||
const current = this.validityPeriodStore.getCurrentPeriodId;
|
||||
|
||||
if ((ps ?? 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)" : ""}`;
|
||||
const period = {id: p.id, value: value};
|
||||
|
||||
if (p.state !== "VALID" || p.id === current)
|
||||
periods.push(period);
|
||||
}
|
||||
|
||||
return periods;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.validityPeriodStore.loadPeriods();
|
||||
},
|
||||
methods: {
|
||||
async downloadFile() {
|
||||
|
||||
const fileName = `lcc_export_${this.exportDataset.toLowerCase()}_${this.exportType.toLowerCase()}.xlsx`;
|
||||
const url = `${config.backendUrl}/bulk/${this.exportType}/${this.exportDataset}/`
|
||||
this.processId = await performDownload(url);
|
||||
this.processId = await performDownload(url, fileName);
|
||||
},
|
||||
inputFile(event) {
|
||||
const file = event.target.files[0];
|
||||
|
|
@ -130,6 +193,10 @@ export default {
|
|||
gap: 1rem;
|
||||
}
|
||||
|
||||
.bulk-operation-box-status-container {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.bulk-operations-box-container {
|
||||
flex: 1;
|
||||
}
|
||||
|
|
|
|||
64
src/frontend/src/components/layout/config/Materials.vue
Normal file
64
src/frontend/src/components/layout/config/Materials.vue
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<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>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TableView from "@/components/UI/TableView.vue";
|
||||
import {mapStores} from "pinia";
|
||||
import {useMaterialStore} from "@/store/material.js";
|
||||
|
||||
export default {
|
||||
name: "materials",
|
||||
components: {TableView},
|
||||
computed: {
|
||||
... mapStores(useMaterialStore),
|
||||
},
|
||||
created() {
|
||||
this.materialStore.updateMaterials()
|
||||
},
|
||||
methods: {
|
||||
async fetch(query) {
|
||||
await this.materialStore.setQueryForList(query);
|
||||
this.pagination = this.materialStore.pagination;
|
||||
return this.materialStore.materials;
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
materialColumns: [
|
||||
{
|
||||
key: 'part_number',
|
||||
label: 'Part number',
|
||||
},
|
||||
{
|
||||
key: 'name',
|
||||
label: 'Description',
|
||||
},
|
||||
{
|
||||
key: 'hs_code',
|
||||
label: 'HS Code',
|
||||
},
|
||||
{
|
||||
key: 'is_deprecated',
|
||||
label: 'active',
|
||||
formatter: (value) => {
|
||||
return !value ? 'Yes' : 'No';
|
||||
}
|
||||
},
|
||||
],
|
||||
pagination: {
|
||||
page: 1,
|
||||
pageCount: 1,
|
||||
totalCount: 0
|
||||
},
|
||||
pageSize: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
75
src/frontend/src/components/layout/config/Nodes.vue
Normal file
75
src/frontend/src/components/layout/config/Nodes.vue
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
<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>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TableView from "@/components/UI/TableView.vue";
|
||||
import {mapStores} from "pinia";
|
||||
import {useNodeStore} from "@/store/node.js";
|
||||
|
||||
export default {
|
||||
name: "Nodes",
|
||||
components: {TableView},
|
||||
computed: {
|
||||
... mapStores(useNodeStore),
|
||||
},
|
||||
created() {
|
||||
this.nodeStore.loadNodes()
|
||||
},
|
||||
methods: {
|
||||
async fetch(query) {
|
||||
await this.nodeStore.setQuery(query);
|
||||
this.pagination = this.nodeStore.pagination;
|
||||
return this.nodeStore.nodes;
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
nodeColumns: [
|
||||
{
|
||||
key: 'external_mapping_id',
|
||||
label: 'Mapping ID',
|
||||
},
|
||||
{
|
||||
key: 'name',
|
||||
label: 'Name',
|
||||
},
|
||||
{
|
||||
key: 'address',
|
||||
label: 'Address',
|
||||
},
|
||||
{
|
||||
key: 'country.iso_code',
|
||||
label: 'Country (ISO Code)',
|
||||
},
|
||||
{
|
||||
key: 'types',
|
||||
label: 'Type',
|
||||
formatter: (value) => {
|
||||
return value.map(v => v.toUpperCase()).join(', ');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'is_deprecated',
|
||||
label: 'active',
|
||||
formatter: (value) => {
|
||||
return !value ? 'Yes' : 'No';
|
||||
}
|
||||
},
|
||||
],
|
||||
pagination: {
|
||||
page: 1,
|
||||
pageCount: 1,
|
||||
totalCount: 0
|
||||
},
|
||||
pageSize: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
241
src/frontend/src/components/layout/config/Rates.vue
Normal file
241
src/frontend/src/components/layout/config/Rates.vue
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
<template>
|
||||
<div class="container-rate-container">
|
||||
|
||||
<div class="container-rate-header">
|
||||
|
||||
<div class="container-rate-search-container">
|
||||
<span class="period-select-caption">Rate type:</span>
|
||||
<radio-option name="rateType" value="container" v-model="rateType">Container rates</radio-option>
|
||||
<radio-option name="rateType" value="matrix" v-model="rateType">Kilometer rates</radio-option>
|
||||
</div>
|
||||
|
||||
<div v-if="!loadingPeriods" class="period-select-container">
|
||||
<span class="period-select-caption">Validity period:</span>
|
||||
<dropdown :options="periods"
|
||||
emptyText="No validity period available"
|
||||
class="period-select"
|
||||
placeholder="Select a validity period"
|
||||
v-model="selectedPeriod"
|
||||
></dropdown>
|
||||
<tooltip position="left" text="Invalidate the selected validity period">
|
||||
<icon-button icon="trash" @click="deletePeriod" :disabled="disableDeleteButton"></icon-button>
|
||||
</tooltip>
|
||||
<modal-dialog title="Do you really want to invalidate this validity period?"
|
||||
dismiss-text="No"
|
||||
accept-text="Yes"
|
||||
:state="modalDialogDeleteState"
|
||||
message="If you invalidate this property set, this will also invalidate all calculations done with this property set. This cannot be undone!"
|
||||
@click="deleteModalClick"
|
||||
>
|
||||
</modal-dialog>
|
||||
</div>
|
||||
|
||||
</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>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Dropdown from "@/components/UI/Dropdown.vue";
|
||||
import IconButton from "@/components/UI/IconButton.vue";
|
||||
import Tooltip from "@/components/UI/Tooltip.vue";
|
||||
import ModalDialog from "@/components/UI/ModalDialog.vue";
|
||||
import {mapStores} from "pinia";
|
||||
import {useValidityPeriodStore} from "@/store/validityPeriod.js";
|
||||
import AutosuggestSearchbar from "@/components/UI/AutoSuggestSearchBar.vue";
|
||||
import DataTable from "@/components/UI/DataTable.vue";
|
||||
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";
|
||||
|
||||
export default {
|
||||
name: "Rates",
|
||||
components: {RadioOption, TableView, DataTable, AutosuggestSearchbar, ModalDialog, Tooltip, IconButton, Dropdown},
|
||||
computed: {
|
||||
...mapStores(useValidityPeriodStore, useMatrixRateStore, useContainerRateStore),
|
||||
loadingPeriods() {
|
||||
// return this.propertiesStore.isLoading;
|
||||
return false;
|
||||
},
|
||||
isValidPeriodActive() {
|
||||
const state = this.validityPeriodStore.getPeriodState(this.selectedPeriod);
|
||||
return state === "VALID" || state === "DRAFT";
|
||||
},
|
||||
disableDeleteButton() {
|
||||
const state = this.validityPeriodStore.getPeriodState(this.selectedPeriod);
|
||||
return state === "VALID" || state === "INVALID" || state === "DRAFT";
|
||||
},
|
||||
rateType: {
|
||||
get() {
|
||||
return this.rateTypeValue;
|
||||
},
|
||||
set(value) {
|
||||
if (value === "matrix") {
|
||||
this.selectedTypeColumns = this.matrixColumns;
|
||||
} else {
|
||||
this.selectedTypeColumns = this.containerColumns;
|
||||
}
|
||||
|
||||
this.rateTypeValue = value;
|
||||
this.$refs.tableViewRef.reload('', 1);
|
||||
}
|
||||
},
|
||||
selectedPeriod: {
|
||||
get() {
|
||||
return this.validityPeriodStore.getSelectedPeriod
|
||||
},
|
||||
async set(value) {
|
||||
this.validityPeriodStore.setSelectedPeriod(value);
|
||||
// await this.propertiesStore.loadProperties(value);
|
||||
// await this.countryStore.selectPeriod(value);
|
||||
}
|
||||
},
|
||||
periods() {
|
||||
const periods = [];
|
||||
|
||||
const vp = this.validityPeriodStore.getPeriods;
|
||||
// const current = this.propertySetsStore.getCurrentPeriodId; //TODO
|
||||
const current = 1;
|
||||
|
||||
if ((vp ?? null) === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
pageSize: 10,
|
||||
pagination: { page: 1, pageCount: 10, totalCount: 1 },
|
||||
rateTypeValue: "container",
|
||||
selectedTypeColumns: [],
|
||||
matrixColumns: [
|
||||
{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) => {
|
||||
|
||||
if (rawValue === "SEA") {
|
||||
return "PhBoat";
|
||||
} else if (rawValue === "RAIL") {
|
||||
return "PhTrain"
|
||||
} else if (rawValue === "ROAD" || rawValue === "POST_RUN") {
|
||||
return "PhTruck"
|
||||
}
|
||||
}
|
||||
},
|
||||
{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.HC', align: 'right', label: '40 ft. HC rate [EUR]'},
|
||||
],
|
||||
modalDialogDeleteState: false,
|
||||
selectedPeriodId: null
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
this.selectedTypeColumns = this.containerColumns;
|
||||
await this.validityPeriodStore.loadPeriods();
|
||||
await this.matrixRateStore.setQuery();
|
||||
await this.containerRateStore.setQuery();
|
||||
},
|
||||
methods: {
|
||||
async fetch(query) {
|
||||
|
||||
query.periodId = this.selectedPeriod;
|
||||
|
||||
if (this.rateType === "container") {
|
||||
await this.containerRateStore.setQuery(query);
|
||||
this.pagination = this.containerRateStore.getPagination;
|
||||
|
||||
return this.containerRateStore.getRates;
|
||||
} else if (this.rateType === "matrix") {
|
||||
await this.matrixRateStore.setQuery(query);
|
||||
|
||||
this.pagination = this.matrixRateStore.getPagination;
|
||||
return this.matrixRateStore.getRates;
|
||||
}
|
||||
return [];
|
||||
},
|
||||
deletePeriod() {
|
||||
if (!this.disableDeleteButton) {
|
||||
this.modalDialogDeleteState = true;
|
||||
}
|
||||
},
|
||||
deleteModalClick() {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
|
||||
|
||||
.table-view {
|
||||
margin-top: 1.6rem;
|
||||
}
|
||||
|
||||
.container-rate-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.container-rate-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
gap: 3.2rem;
|
||||
}
|
||||
|
||||
.container-rate-search-container {
|
||||
display: flex;
|
||||
justify-content: stretch;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
margin-bottom: 1rem;
|
||||
gap: 1.6rem;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.container-rate-search-searchbar {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.period-select-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
gap: 1.6rem;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.period-select-caption {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
|
@ -85,7 +85,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
async fetchDestinations(query) {
|
||||
await this.nodeStore.setQuery({searchTerm: query, nodeType: "DESTINATION", includeUserNode: false});
|
||||
await this.nodeStore.setSearch({searchTerm: query, nodeType: "DESTINATION", includeUserNode: false});
|
||||
return this.nodeStore.nodes;
|
||||
},
|
||||
resolveFlag(node) {
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ export default {
|
|||
},
|
||||
async fetchSupplier(query) {
|
||||
console.log("Fetching supplier for query: " + query);
|
||||
await this.nodeStore.setQuery({searchTerm: query, nodeType: 'SOURCE', includeUserNode: true});
|
||||
await this.nodeStore.setSearch({searchTerm: query, nodeType: 'SOURCE', includeUserNode: true});
|
||||
return this.nodeStore.nodes;
|
||||
},
|
||||
resolveFlag(node) {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
PhX,
|
||||
PhTrain,
|
||||
PhTruckTrailer,
|
||||
PhTruck,
|
||||
PhBoat,
|
||||
PhPencilSimple,
|
||||
PhLock,
|
||||
|
|
@ -48,6 +49,7 @@ app.component('PhWarning', PhWarning);
|
|||
app.component('PhLock', PhLock);
|
||||
app.component('PhLockOpen', PhLockOpen);
|
||||
app.component('PhTruckTrailer', PhTruckTrailer);
|
||||
app.component('PhTruck', PhTruck);
|
||||
app.component('PhBoat', PhBoat);
|
||||
app.component('PhTrain', PhTrain);
|
||||
app.component('PhPencilSimple', PhPencilSimple);
|
||||
|
|
|
|||
|
|
@ -23,6 +23,9 @@ import Box from "@/components/UI/Box.vue";
|
|||
import CountryProperties from "@/components/layout/config/CountryProperties.vue";
|
||||
import StagedChanges from "@/components/layout/config/StagedChanges.vue";
|
||||
import BulkOperations from "@/components/layout/config/BulkOperations.vue";
|
||||
import Rates from "@/components/layout/config/Rates.vue";
|
||||
import Nodes from "@/components/layout/config/Nodes.vue";
|
||||
import Materials from "@/components/layout/config/Materials.vue";
|
||||
|
||||
export default {
|
||||
name: "Config",
|
||||
|
|
@ -39,6 +42,18 @@ export default {
|
|||
title: 'Countries',
|
||||
component: markRaw(CountryProperties),
|
||||
},
|
||||
{
|
||||
title: 'Materials',
|
||||
component: markRaw(Materials),
|
||||
},
|
||||
{
|
||||
title: 'Nodes',
|
||||
component: markRaw(Nodes),
|
||||
},
|
||||
{
|
||||
title: 'Rates',
|
||||
component: markRaw(Rates),
|
||||
},
|
||||
{
|
||||
title: 'Bulk operations',
|
||||
component: markRaw(BulkOperations),
|
||||
|
|
|
|||
|
|
@ -91,15 +91,18 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
downloadReport() {
|
||||
this.reportsStore.downloadReport();
|
||||
},
|
||||
createReport() {
|
||||
this.showModal = true;
|
||||
},
|
||||
buildDate(date) {
|
||||
return `${date[0]}-${date[1].toString().padStart(2, '0')}-${date[2].toString().padStart(2, '0')}`
|
||||
},
|
||||
closeModal(data) {
|
||||
async closeModal(data) {
|
||||
if (data.action === 'accept') {
|
||||
this.reportsStore.fetchReports(data.materialId, data.supplierIds);
|
||||
await this.reportsStore.fetchReports(data.materialId, data.supplierIds);
|
||||
}
|
||||
this.showModal = false;
|
||||
}
|
||||
|
|
|
|||
0
src/frontend/src/store/bulkOperation.js
Normal file
0
src/frontend/src/store/bulkOperation.js
Normal file
63
src/frontend/src/store/containerRate.js
Normal file
63
src/frontend/src/store/containerRate.js
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import {defineStore} from 'pinia'
|
||||
import {config} from '@/config'
|
||||
import {useErrorStore} from "@/store/error.js";
|
||||
import performRequest from "@/backend.js";
|
||||
|
||||
export const useContainerRateStore = defineStore('containerRate', {
|
||||
state() {
|
||||
return {
|
||||
rates: [],
|
||||
loading: false,
|
||||
query: {},
|
||||
pagination: {}
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
isLoading(state) {
|
||||
return state.loading;
|
||||
},
|
||||
getRates(state) {
|
||||
return state.rates;
|
||||
},
|
||||
getPagination(state) {
|
||||
return state.pagination;
|
||||
}
|
||||
|
||||
},
|
||||
actions: {
|
||||
async setQuery(query = {}) {
|
||||
this.query = query;
|
||||
await this.updateMatrixRates();
|
||||
},
|
||||
async updateMatrixRates() {
|
||||
this.loading = true;
|
||||
|
||||
this.rates = [];
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (this.query?.searchTerm && this.query.searchTerm !== '')
|
||||
params.append('filter', this.query.searchTerm);
|
||||
if(this.query?.periodId)
|
||||
params.append('valid', this.query.periodId);
|
||||
|
||||
if(this.query?.page)
|
||||
params.append('page', this.query.page);
|
||||
|
||||
if(this.query?.pageSize)
|
||||
params.append('limit', this.query.pageSize);
|
||||
|
||||
const url = `${config.backendUrl}/rates/container/${params.size === 0 ? '' : '?'}${params.toString()}`;
|
||||
|
||||
const {data: data, headers: headers} = await performRequest(this, "GET", url, null);
|
||||
|
||||
this.rates = data;
|
||||
this.pagination = { page: parseInt(headers.get('X-Current-Page')), pageCount: parseInt(headers.get('X-Page-Count')), totalCount: parseInt(headers.get('X-Total-Count'))};
|
||||
|
||||
console.log(this.pagination)
|
||||
|
||||
this.loading = false;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import {defineStore} from 'pinia'
|
||||
import {config} from '@/config'
|
||||
import {useErrorStore} from "@/store/error.js";
|
||||
import performRequest from "@/backend.js";
|
||||
|
||||
export const useMaterialStore = defineStore('material', {
|
||||
state() {
|
||||
|
|
@ -15,6 +16,11 @@ export const useMaterialStore = defineStore('material', {
|
|||
},
|
||||
getters: {},
|
||||
actions: {
|
||||
async setQueryForList(query) {
|
||||
this.query = query;
|
||||
this.query.excludeDeprecated = false;
|
||||
await this.updateMaterials();
|
||||
},
|
||||
async setQuery(query) {
|
||||
this.query = query;
|
||||
await this.updateMaterials();
|
||||
|
|
@ -30,48 +36,28 @@ export const useMaterialStore = defineStore('material', {
|
|||
if (this.query.searchTerm)
|
||||
params.append('filter', this.query.searchTerm);
|
||||
|
||||
if(this.query?.page)
|
||||
params.append('page', this.query.page);
|
||||
|
||||
if(this.query?.pageSize)
|
||||
params.append('limit', this.query.pageSize);
|
||||
|
||||
if(this.query?.excludeDeprecated !== null)
|
||||
params.append('excludeDeprecated', this.query.excludeDeprecated);
|
||||
|
||||
|
||||
const url = `${config.backendUrl}/materials/${params.size === 0 ? '' : '?'}${params.toString()}`;
|
||||
|
||||
const request = { url: url, params: {method: 'GET'}};
|
||||
|
||||
const response = await fetch(url).catch(e => {
|
||||
this.error = {code: 'Network error.', message: "Please check your internet connection.", trace: null}
|
||||
this.loading = false;
|
||||
|
||||
console.error(this.error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(this.error, { store: this, request: request});
|
||||
const {data: data, headers: headers} = await performRequest(this, "GET", url, null, true);
|
||||
|
||||
|
||||
throw e;
|
||||
});
|
||||
|
||||
const data = await response.json().catch(e => {
|
||||
this.error = {
|
||||
code: 'Malformed response',
|
||||
message: "Malformed server response. Please contact support.",
|
||||
trace: null
|
||||
}
|
||||
this.loading = false;
|
||||
|
||||
console.error(this.error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(this.error, { store: this, request: request});
|
||||
|
||||
throw e;
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
this.error = {code: data.error.code, title: data.error.title, message: data.error.message, trace: data.error.trace };
|
||||
this.loading = false;
|
||||
|
||||
console.error(this.error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(this.error, { store: this, request: request});
|
||||
|
||||
return;
|
||||
}
|
||||
this.pagination = {
|
||||
page: parseInt(headers.get('X-Current-Page')),
|
||||
pageCount: parseInt(headers.get('X-Page-Count')),
|
||||
totalCount: parseInt(headers.get('X-Total-Count'))
|
||||
};
|
||||
|
||||
this.loading = false;
|
||||
this.empty = data.length === 0;
|
||||
|
|
|
|||
60
src/frontend/src/store/matrixRate.js
Normal file
60
src/frontend/src/store/matrixRate.js
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import {defineStore} from 'pinia'
|
||||
import {config} from '@/config'
|
||||
import {useErrorStore} from "@/store/error.js";
|
||||
import performRequest from "@/backend.js";
|
||||
|
||||
export const useMatrixRateStore = defineStore('matrixRate', {
|
||||
state() {
|
||||
return {
|
||||
rates: [],
|
||||
loading: false,
|
||||
empty: true,
|
||||
error: null,
|
||||
query: {},
|
||||
pagination: {}
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
isLoading(state) {
|
||||
return state.loading;
|
||||
},
|
||||
getRates(state) {
|
||||
return state.rates;
|
||||
},
|
||||
getPagination(state) {
|
||||
return state.pagination;
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
async setQuery(query = {}) {
|
||||
this.query = query;
|
||||
await this.updateMatrixRates();
|
||||
},
|
||||
async updateMatrixRates() {
|
||||
this.loading = true;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (this.query?.searchTerm && this.query.searchTerm !== '')
|
||||
params.append('filter', this.query.searchTerm);
|
||||
if(this.query?.periodId)
|
||||
params.append('valid', this.query.periodId);
|
||||
|
||||
if(this.query?.page)
|
||||
params.append('page', this.query.page);
|
||||
|
||||
if(this.query?.pageSize)
|
||||
params.append('limit', this.query.pageSize);
|
||||
|
||||
const url = `${config.backendUrl}/rates/matrix/${params.size === 0 ? '' : '?'}${params.toString()}`;
|
||||
|
||||
const {data: data, headers: headers} = await performRequest(this, "GET", url, null);
|
||||
|
||||
this.rates = data;
|
||||
this.pagination = { page: parseInt(headers.get('X-Current-Page')), pageCount: parseInt(headers.get('X-Page-Count')), totalCount: parseInt(headers.get('X-Total-Count'))};
|
||||
|
||||
this.loading = false;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import {defineStore} from 'pinia'
|
||||
import {config} from '@/config'
|
||||
import {useErrorStore} from "@/store/error.js";
|
||||
import performRequest from "@/backend.js";
|
||||
|
||||
|
||||
export const useNodeStore = defineStore('node', {
|
||||
|
|
@ -18,8 +19,14 @@ export const useNodeStore = defineStore('node', {
|
|||
}
|
||||
},
|
||||
actions: {
|
||||
async setSearch(query) {
|
||||
this.query = query;
|
||||
this.query.type = 'search';
|
||||
await this.loadNodes();
|
||||
},
|
||||
async setQuery(query) {
|
||||
this.query = query;
|
||||
this.query.type = 'list';
|
||||
await this.loadNodes();
|
||||
},
|
||||
async loadNodes() {
|
||||
|
|
@ -35,47 +42,27 @@ export const useNodeStore = defineStore('node', {
|
|||
if (this.query.includeUserNode)
|
||||
params.append('include_user_node', this.query.includeUserNode);
|
||||
|
||||
const url = `${config.backendUrl}/nodes/search/${params.size === 0 ? '' : '?'}${params.toString()}`;
|
||||
if(this.query?.page)
|
||||
params.append('page', this.query.page);
|
||||
|
||||
const request = { url: url, params: {method: 'GET'}};
|
||||
if(this.query?.pageSize)
|
||||
params.append('limit', this.query.pageSize);
|
||||
|
||||
console.log(url)
|
||||
const response = await fetch(url).catch(e => {
|
||||
this.error = {code: 'Network error.', message: "Please check your internet connection.", trace: null}
|
||||
this.loading = false;
|
||||
|
||||
console.error(this.error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(this.error, { store: this, request: request});
|
||||
const endpoint = this.query.type === 'list' ? '/nodes' : '/nodes/search'
|
||||
const url = `${config.backendUrl}${endpoint}/${params.size === 0 ? '' : '?'}${params.toString()}`;
|
||||
|
||||
throw e;
|
||||
});
|
||||
|
||||
const data = await response.json().catch(e => {
|
||||
this.error = {
|
||||
code: 'Malformed response',
|
||||
message: "Malformed server response. Please contact support.",
|
||||
trace: null
|
||||
}
|
||||
this.loading = false;
|
||||
const {data: data, headers: headers} = await performRequest(this, "GET", url, null, true);
|
||||
|
||||
console.error(this.error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(this.error, { store: this, request: request});
|
||||
|
||||
throw e;
|
||||
});
|
||||
if (this.query.type === 'list')
|
||||
this.pagination = {
|
||||
page: parseInt(headers.get('X-Current-Page')),
|
||||
pageCount: parseInt(headers.get('X-Page-Count')),
|
||||
totalCount: parseInt(headers.get('X-Total-Count'))
|
||||
};
|
||||
|
||||
if (!response.ok) {
|
||||
this.error = {code: data.error.code, title: data.error.title, message: data.error.message, trace: data.error.trace };
|
||||
this.loading = false;
|
||||
|
||||
console.error(this.error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(this.error, { store: this, request: request});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
this.empty = data.length === 0;
|
||||
|
|
|
|||
|
|
@ -396,10 +396,10 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
const body = {destinations: destinations, premise_id: this.destinations.premise_ids};
|
||||
const url = `${config.backendUrl}/calculation/destination/`;
|
||||
|
||||
const data = await performRequest(this,'PUT', url, body).catch(e => {
|
||||
const { data: data, headers: headers } = await performRequest(this,'PUT', url, body).catch(e => {
|
||||
this.destinations = null;
|
||||
this.processDestinationMassEdit = false;
|
||||
})
|
||||
});
|
||||
|
||||
if (data) {
|
||||
for (const id of Object.keys(data)) {
|
||||
|
|
@ -605,7 +605,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
const url = `${config.backendUrl}/calculation/destination/`;
|
||||
|
||||
|
||||
const destinations = await performRequest(this,'POST', url, body).catch(e => {
|
||||
const {data: destinations } = await performRequest(this,'POST', url, body).catch(e => {
|
||||
this.loading = false;
|
||||
this.selectedLoading = false;
|
||||
throw e;
|
||||
|
|
@ -666,7 +666,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
body.premise_id = toBeUpdated;
|
||||
logger.info(url, body)
|
||||
|
||||
const data = await performRequest(this,'PUT', url, body).catch(e => {
|
||||
const {data: data} = await performRequest(this,'PUT', url, body).catch(e => {
|
||||
this.loading = false;
|
||||
});
|
||||
|
||||
|
|
@ -835,10 +835,11 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
params.append('premissIds', `${[id]}`);
|
||||
const url = `${config.backendUrl}/calculation/edit/${params.size === 0 ? '' : '?'}${params.toString()}`;
|
||||
|
||||
this.premisses = await performRequest(this,'GET', url, null).catch(e => {
|
||||
const { data: data, headers: headers } = await performRequest(this,'GET', url, null).catch(e => {
|
||||
this.selectedLoading = false;
|
||||
this.loading = false;
|
||||
});
|
||||
this.premisses = data;
|
||||
|
||||
this.premisses.forEach(p => p.selected = true);
|
||||
this.prepareDestinations(id, [id]);
|
||||
|
|
@ -861,9 +862,10 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
params.append('premissIds', ids.join(', '));
|
||||
const url = `${config.backendUrl}/calculation/edit/${params.size === 0 ? '' : '?'}${params.toString()}`;
|
||||
|
||||
this.premisses = await performRequest(this,'GET', url, null).catch(e => {
|
||||
const { data: data, headers: headers } = await performRequest(this,'GET', url, null).catch(e => {
|
||||
this.loading = false;
|
||||
});
|
||||
this.premisses = data;
|
||||
|
||||
this.premisses.forEach(p => p.selected = false);
|
||||
this.loading = false;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import {defineStore} from 'pinia'
|
||||
import {config} from '@/config'
|
||||
import performRequest from '@/backend.js'
|
||||
import performRequest, {performDownload} from '@/backend.js'
|
||||
|
||||
|
||||
export const useReportsStore = defineStore('reports', {
|
||||
|
|
@ -9,6 +9,8 @@ export const useReportsStore = defineStore('reports', {
|
|||
reports: [],
|
||||
showComparableWarning: false,
|
||||
loading: false,
|
||||
materialId: null,
|
||||
supplierIds: [],
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
|
|
@ -28,6 +30,19 @@ export const useReportsStore = defineStore('reports', {
|
|||
}
|
||||
},
|
||||
actions: {
|
||||
async downloadReport() {
|
||||
|
||||
if(this.materialId === null && this.supplierIds?.length === 0)
|
||||
return;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.append('material', this.materialId);
|
||||
params.append('sources', this.supplierIds);
|
||||
|
||||
const url = `${config.backendUrl}/reports/download/${params.size === 0 ? '' : '?'}${params.toString()}`;
|
||||
const fileName = `report_${this.materialId}_${this.supplierIds.join('_')}.xlsx`;
|
||||
await performDownload(url,fileName);
|
||||
},
|
||||
reset() {
|
||||
this.reports = [];
|
||||
},
|
||||
|
|
@ -37,15 +52,19 @@ export const useReportsStore = defineStore('reports', {
|
|||
this.loading = true;
|
||||
this.reports = [];
|
||||
|
||||
this.materialId = materialId;
|
||||
this.supplierIds = supplierIds;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.append('material', materialId);
|
||||
params.append('sources', supplierIds);
|
||||
|
||||
const url = `${config.backendUrl}/reports/view/${params.size === 0 ? '' : '?'}${params.toString()}`;
|
||||
|
||||
this.reports = await performRequest(this, 'GET', url, null).catch(e => {
|
||||
const {data: data} = await performRequest(this, 'GET', url, null).catch(e => {
|
||||
this.loading = false;
|
||||
});
|
||||
this.reports = data;
|
||||
|
||||
|
||||
this.showComparableWarning = false;
|
||||
|
|
|
|||
157
src/frontend/src/store/validityPeriod.js
Normal file
157
src/frontend/src/store/validityPeriod.js
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import {defineStore} from 'pinia'
|
||||
import {config} from '@/config'
|
||||
import {useErrorStore} from "@/store/error.js";
|
||||
import { useStageStore } from './stage.js'
|
||||
|
||||
export const useValidityPeriodStore = defineStore('validityPeriod', {
|
||||
state() {
|
||||
return {
|
||||
periods: null,
|
||||
selectedPeriod: null,
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
getPeriods(state) {
|
||||
return state.periods;
|
||||
},
|
||||
getCurrentPeriodId(state) {
|
||||
if (state.periods === null)
|
||||
return null;
|
||||
|
||||
for (const period of state.periods) {
|
||||
if (period.state === "DRAFT")
|
||||
return period.id;
|
||||
}
|
||||
|
||||
for (const period of state.periods) {
|
||||
if (period.state === "VALID")
|
||||
return period.id;
|
||||
}
|
||||
},
|
||||
getSelectedPeriod(state) {
|
||||
return state.selectedPeriod;
|
||||
},
|
||||
getPeriodState(state) {
|
||||
return function(periodId) {
|
||||
if (state.periods === null)
|
||||
return null;
|
||||
|
||||
return state.periods.find(p => p.id === periodId)?.state;
|
||||
}
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
setSelectedPeriod(periodId) {
|
||||
this.selectedPeriod = periodId;
|
||||
},
|
||||
async invalidate() {
|
||||
|
||||
const url = `${config.backendUrl}/rates/periods/${this.getSelectedPeriod}/`;
|
||||
this.periods = await this.performRequest('DELETE', url, null, false);
|
||||
|
||||
await this.reload();
|
||||
|
||||
},
|
||||
async loadPeriods() {
|
||||
this.loading = true;
|
||||
const url = `${config.backendUrl}/rates/periods`;
|
||||
this.periods = await this.performRequest('GET', url, null, );
|
||||
this.selectedPeriod = this.getCurrentPeriodId;
|
||||
this.loading = false;
|
||||
},
|
||||
async performRequest(method, url, body, expectResponse = true) {
|
||||
|
||||
const params = {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
|
||||
if ((body ?? null) !== null) {
|
||||
params.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
const request = {url: url, params: params};
|
||||
console.log("Request:", request);
|
||||
|
||||
const response = await fetch(url, params
|
||||
).catch(e => {
|
||||
const error = {
|
||||
code: 'Network error.',
|
||||
message: "Please check your internet connection.",
|
||||
trace: null
|
||||
}
|
||||
|
||||
console.error(error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(error, {store: this, request: request});
|
||||
|
||||
throw e;
|
||||
});
|
||||
|
||||
let data = null;
|
||||
if (expectResponse) {
|
||||
data = await response.json().catch(e => {
|
||||
const error = {
|
||||
code: 'Malformed response',
|
||||
message: "Malformed server response. Please contact support.",
|
||||
trace: null
|
||||
}
|
||||
|
||||
console.error(error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(error, {store: this, request: request});
|
||||
throw e;
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = {
|
||||
code: data.error.code,
|
||||
title: data.error.title,
|
||||
message: data.error.message,
|
||||
trace: data.error.trace
|
||||
}
|
||||
|
||||
console.error(error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(error, {store: this, request: request});
|
||||
throw new Error('Internal backend error');
|
||||
}
|
||||
} else {
|
||||
if (!response.ok) {
|
||||
|
||||
const data = await response.json().catch(e => {
|
||||
const error = {
|
||||
code: "Return code error " + response.status,
|
||||
message: "Server returned wrong response code",
|
||||
trace: null
|
||||
}
|
||||
console.error(error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(error, {store: this, request: request});
|
||||
throw new Error('Internal backend error');
|
||||
|
||||
});
|
||||
|
||||
const error = {
|
||||
code: data.error.code,
|
||||
title: data.error.title,
|
||||
message: data.error.message,
|
||||
trace: data.error.trace
|
||||
}
|
||||
|
||||
console.error(error);
|
||||
const errorStore = useErrorStore();
|
||||
void errorStore.addError(error, {store: this, request: request});
|
||||
throw new Error('Internal backend error');
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Response:", data);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -4,6 +4,8 @@ import org.jetbrains.annotations.NotNull;
|
|||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
|
|
@ -13,6 +15,7 @@ import java.util.Arrays;
|
|||
|
||||
@Configuration
|
||||
@EnableWebMvc
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
public class CorsConfig implements WebMvcConfigurer {
|
||||
|
||||
@Autowired
|
||||
|
|
@ -25,19 +28,27 @@ public class CorsConfig implements WebMvcConfigurer {
|
|||
public void addCorsMappings(@NotNull CorsRegistry registry) {
|
||||
String[] activeProfiles = environment.getActiveProfiles();
|
||||
|
||||
System.out.println("Active profiles: " + Arrays.toString(activeProfiles));
|
||||
System.out.println("Allowed CORS: " + allowedCors);
|
||||
|
||||
if (Arrays.asList(activeProfiles).contains("dev")) {
|
||||
|
||||
System.out.println("Applying DEV CORS configuration");
|
||||
|
||||
// Development CORS configuration
|
||||
registry.addMapping("/api/**")
|
||||
.allowedOriginPatterns("http://localhost:*")
|
||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||
.allowedHeaders("*")
|
||||
.allowCredentials(true);
|
||||
.exposedHeaders("X-Total-Count", "X-Page-Count", "X-Current-Page")
|
||||
.allowCredentials(false);
|
||||
} else {
|
||||
// Production CORS configuration
|
||||
registry.addMapping("/api/**")
|
||||
.allowedOrigins(allowedCors)
|
||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||
.allowedHeaders("*")
|
||||
.exposedHeaders("X-Total-Count", "X-Page-Count", "X-Current-Page")
|
||||
.allowCredentials(true);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,29 +18,35 @@ public class CorsFilter implements Filter {
|
|||
private String allowedOrigins;
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
|
||||
HttpServletRequest request = (HttpServletRequest) req;
|
||||
HttpServletResponse response = (HttpServletResponse) res;
|
||||
|
||||
String origin = request.getHeader("Origin");
|
||||
|
||||
// Check if it's a development environment and localhost
|
||||
if (origin != null && origin.startsWith("http://localhost:")) {
|
||||
response.setHeader("Access-Control-Allow-Origin", origin);
|
||||
response.setHeader("Access-Control-Allow-Credentials", "true");
|
||||
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
||||
response.setHeader("Access-Control-Allow-Headers", "*");
|
||||
response.setHeader("Access-Control-Max-Age", "3600");
|
||||
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
|
||||
filterChain.doFilter(servletRequest, servletResponse);
|
||||
}
|
||||
|
||||
// Handle preflight OPTIONS requests
|
||||
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
|
||||
response.setStatus(HttpServletResponse.SC_OK);
|
||||
return;
|
||||
}
|
||||
|
||||
chain.doFilter(req, res);
|
||||
}
|
||||
// @Override
|
||||
// public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
|
||||
// throws IOException, ServletException {
|
||||
//
|
||||
// HttpServletRequest request = (HttpServletRequest) req;
|
||||
// HttpServletResponse response = (HttpServletResponse) res;
|
||||
//
|
||||
// String origin = request.getHeader("Origin");
|
||||
//
|
||||
// // Check if it's a development environment and localhost
|
||||
// if (origin != null && origin.startsWith("http://localhost:")) {
|
||||
// response.setHeader("Access-Control-Allow-Origin", origin);
|
||||
// response.setHeader("Access-Control-Allow-Credentials", "true");
|
||||
// response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
||||
// response.setHeader("Access-Control-Allow-Headers", "*");
|
||||
// response.setHeader("Access-Control-Expose-Headers", "X-Total-Count, X-Page-Count, X-Current-Page");
|
||||
// response.setHeader("Access-Control-Max-Age", "3600");
|
||||
// }
|
||||
//
|
||||
// // Handle preflight OPTIONS requests
|
||||
// if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
|
||||
// response.setStatus(HttpServletResponse.SC_OK);
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// chain.doFilter(req, res);
|
||||
// }
|
||||
}
|
||||
|
|
@ -6,10 +6,17 @@ import org.springframework.context.annotation.Profile;
|
|||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
|
||||
|
||||
@Bean
|
||||
@Profile("!dev & !test") // Only active when NOT in dev profile
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
|
|
|
|||
|
|
@ -37,11 +37,12 @@ public class MaterialController {
|
|||
*/
|
||||
@GetMapping("/")
|
||||
public ResponseEntity<List<MaterialDTO>> listMaterials(
|
||||
@RequestParam(defaultValue = "true") String excludeDeprecated,
|
||||
@RequestParam(defaultValue = "20") @Min(1) int limit,
|
||||
@RequestParam(defaultValue = "1") @Min(1) int page,
|
||||
@RequestParam(required = false) Optional<String> filter) {
|
||||
|
||||
SearchQueryResult<MaterialDTO> materials = materialService.listMaterial(filter, page, limit);
|
||||
SearchQueryResult<MaterialDTO> materials = materialService.listMaterial(filter, page, limit,Boolean.parseBoolean(excludeDeprecated));
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header("X-Total-Count", String.valueOf(materials.getTotalElements()))
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ public class NodeController {
|
|||
this.userNodeService = userNodeService;
|
||||
}
|
||||
|
||||
@GetMapping("/")
|
||||
@GetMapping({"","/"})
|
||||
public ResponseEntity<List<NodeDTO>> listNodes(@RequestParam(required = false) String filter, @RequestParam(defaultValue = "1") @Min(1) Integer page, @RequestParam(defaultValue = "20") @Min(1) Integer limit) {
|
||||
nodeService.listNodes(filter, page, limit);
|
||||
|
||||
|
|
|
|||
|
|
@ -51,8 +51,9 @@ public class RateController {
|
|||
* @param validAt the specific date and time to filter container rates, optional
|
||||
* @return a ResponseEntity containing the list of container rates and additional pagination headers
|
||||
*/
|
||||
@GetMapping("/container")
|
||||
@GetMapping({"/container", "/container/" })
|
||||
public ResponseEntity<List<ContainerRateDTO>> listContainerRates(
|
||||
@RequestParam(defaultValue = "") String filter,
|
||||
@RequestParam(defaultValue = "20") @Min(1) int limit,
|
||||
@RequestParam(defaultValue = "1") @Min(1) int page,
|
||||
@RequestParam(name= "valid", required = false) Integer validityPeriodId,
|
||||
|
|
@ -61,13 +62,13 @@ public class RateController {
|
|||
SearchQueryResult<ContainerRateDTO> containerRates = null;
|
||||
|
||||
if(validAt != null) {
|
||||
containerRates = containerRateService.listRates(limit, page, validAt);
|
||||
containerRates = containerRateService.listRates(filter, limit, page, validAt);
|
||||
}
|
||||
else if(validityPeriodId != null) {
|
||||
containerRates = containerRateService.listRates(limit, page, validityPeriodId);
|
||||
containerRates = containerRateService.listRates(filter, limit, page, validityPeriodId);
|
||||
}
|
||||
else {
|
||||
containerRates = containerRateService.listRates(limit, page);
|
||||
containerRates = containerRateService.listRates(filter, limit, page);
|
||||
}
|
||||
|
||||
return ResponseEntity.ok()
|
||||
|
|
@ -83,7 +84,7 @@ public class RateController {
|
|||
* @param id the unique identifier of the container whose rate information is to be retrieved
|
||||
* @return a ResponseEntity containing the ContainerRateDTO with the rate information of the specified container
|
||||
*/
|
||||
@GetMapping("/container/{id}")
|
||||
@GetMapping({"/container/{id}", "/container/{id}/"})
|
||||
public ResponseEntity<ContainerRateDTO> getContainerRate(@PathVariable Integer id) {
|
||||
return ResponseEntity.ok(containerRateService.getContainerRate(id));
|
||||
}
|
||||
|
|
@ -98,8 +99,9 @@ public class RateController {
|
|||
* @return a {@link ResponseEntity} containing a list of {@link MatrixRateDTO} wrapped in the response body,
|
||||
* including pagination headers.
|
||||
*/
|
||||
@GetMapping("/matrix")
|
||||
@GetMapping({"/matrix","/matrix/"})
|
||||
public ResponseEntity<List<MatrixRateDTO>> listMatrixRates(
|
||||
@RequestParam(defaultValue = "") String filter,
|
||||
@RequestParam(defaultValue = "20") @Min(1) int limit,
|
||||
@RequestParam(defaultValue = "1") @Min(1) int page,
|
||||
@RequestParam(required = false) Integer valid,
|
||||
|
|
@ -108,13 +110,13 @@ public class RateController {
|
|||
SearchQueryResult<MatrixRateDTO> rates = null;
|
||||
|
||||
if(validAt != null) {
|
||||
rates = matrixRateService.listRates(limit, page, validAt);
|
||||
rates = matrixRateService.listRates(filter, limit, page, validAt);
|
||||
}
|
||||
else if(valid != null) {
|
||||
rates = matrixRateService.listRates(limit, page, valid);
|
||||
rates = matrixRateService.listRates(filter, limit, page, valid);
|
||||
}
|
||||
else {
|
||||
rates = matrixRateService.listRates(limit, page);
|
||||
rates = matrixRateService.listRates(filter, limit, page);
|
||||
}
|
||||
|
||||
return ResponseEntity.ok()
|
||||
|
|
@ -130,7 +132,7 @@ public class RateController {
|
|||
* @param id the unique identifier of the matrix rate to retrieve
|
||||
* @return a ResponseEntity containing the MatrixRateDTO for the specified ID
|
||||
*/
|
||||
@GetMapping("/matrix/{id}")
|
||||
@GetMapping({"/matrix/{id}", "/matrix/{id}/"})
|
||||
public ResponseEntity<MatrixRateDTO> getMatrixRate(@PathVariable Integer id) {
|
||||
return ResponseEntity.ok(matrixRateService.getRate(id));
|
||||
}
|
||||
|
|
@ -140,7 +142,7 @@ public class RateController {
|
|||
*
|
||||
* @return ResponseEntity containing the list of ValidityPeriodDTO objects.
|
||||
*/
|
||||
@GetMapping("/periods")
|
||||
@GetMapping({"/periods", "/periods/"})
|
||||
public ResponseEntity<List<ValidityPeriodDTO>> listPeriods() {
|
||||
return ResponseEntity.ok(validityPeriodService.listPeriods());
|
||||
}
|
||||
|
|
@ -151,7 +153,7 @@ public class RateController {
|
|||
* @param id The ID of the validity period to invalidate.
|
||||
* @return ResponseEntity indicating the operation status.
|
||||
*/
|
||||
@DeleteMapping("/periods/{id}")
|
||||
@DeleteMapping({"/periods/{id}", "/periods/{id}/"})
|
||||
public ResponseEntity<Void> invalidatePeriod(@PathVariable Integer id) {
|
||||
validityPeriodService.invalidate(id);
|
||||
return ResponseEntity.ok().build();
|
||||
|
|
@ -163,7 +165,7 @@ public class RateController {
|
|||
* @return ResponseEntity containing a Boolean value indicating
|
||||
* whether rate drafts exist (true) or not (false).
|
||||
*/
|
||||
@GetMapping("/staged_changes")
|
||||
@GetMapping( {"/staged_changes", "/staged_changes/"})
|
||||
public ResponseEntity<Boolean> checkRateDrafts() {
|
||||
return ResponseEntity.ok(rateApprovalService.hasRateDrafts());
|
||||
}
|
||||
|
|
@ -173,7 +175,7 @@ public class RateController {
|
|||
*
|
||||
* @return ResponseEntity with HTTP 200 status if the operation is successful.
|
||||
*/
|
||||
@PutMapping("/staged_changes")
|
||||
@PutMapping({"/staged_changes", "/staged_changes/"})
|
||||
public ResponseEntity<Void> approveRateDrafts() {
|
||||
rateApprovalService.approveRateDrafts();
|
||||
return ResponseEntity.ok().build();
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ public class ReportingController {
|
|||
* @param nodeIds A list of node IDs (sources) to include in the downloaded report.
|
||||
* @return The Excel file as an attachment in the response.
|
||||
*/
|
||||
@GetMapping("/download")
|
||||
@GetMapping({"/download","/download/"})
|
||||
public ResponseEntity<InputStreamResource> downloadReport(@RequestParam(value = "material") Integer materialId, @RequestParam(value = "sources") List<Integer> nodeIds) {
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
package de.avatic.lcc.dto.configuration.matrixrates;
|
||||
|
||||
import de.avatic.lcc.dto.generic.NodeDTO;
|
||||
import de.avatic.lcc.dto.generic.CountryDTO;
|
||||
|
||||
public class MatrixRateDTO {
|
||||
private Integer id;
|
||||
private NodeDTO origin;
|
||||
private NodeDTO destination;
|
||||
private CountryDTO source;
|
||||
private CountryDTO destination;
|
||||
private Number rate;
|
||||
|
||||
public Integer getId() {
|
||||
|
|
@ -16,22 +16,6 @@ public class MatrixRateDTO {
|
|||
this.id = id;
|
||||
}
|
||||
|
||||
public NodeDTO getOrigin() {
|
||||
return origin;
|
||||
}
|
||||
|
||||
public void setOrigin(NodeDTO origin) {
|
||||
this.origin = origin;
|
||||
}
|
||||
|
||||
public NodeDTO getDestination() {
|
||||
return destination;
|
||||
}
|
||||
|
||||
public void setDestination(NodeDTO destination) {
|
||||
this.destination = destination;
|
||||
}
|
||||
|
||||
public Number getRate() {
|
||||
return rate;
|
||||
}
|
||||
|
|
@ -39,4 +23,20 @@ public class MatrixRateDTO {
|
|||
public void setRate(Number rate) {
|
||||
this.rate = rate;
|
||||
}
|
||||
|
||||
public CountryDTO getSource() {
|
||||
return source;
|
||||
}
|
||||
|
||||
public void setSource(CountryDTO source) {
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
public CountryDTO getDestination() {
|
||||
return destination;
|
||||
}
|
||||
|
||||
public void setDestination(CountryDTO destination) {
|
||||
this.destination = destination;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ public class ContainerRateDTO {
|
|||
|
||||
private Integer id;
|
||||
|
||||
private NodeDTO origin;
|
||||
private NodeDTO source;
|
||||
|
||||
private NodeDTO destination;
|
||||
|
||||
|
|
@ -33,12 +33,12 @@ public class ContainerRateDTO {
|
|||
this.id = id;
|
||||
}
|
||||
|
||||
public NodeDTO getOrigin() {
|
||||
return origin;
|
||||
public NodeDTO getSource() {
|
||||
return source;
|
||||
}
|
||||
|
||||
public void setOrigin(NodeDTO origin) {
|
||||
this.origin = origin;
|
||||
public void setSource(NodeDTO source) {
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
public NodeDTO getDestination() {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package de.avatic.lcc.dto.generic;
|
||||
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.Objects;
|
||||
|
|
@ -15,6 +16,9 @@ public class MaterialDTO {
|
|||
@JsonProperty("hs_code")
|
||||
private String hsCode;
|
||||
|
||||
@JsonProperty("is_deprecated")
|
||||
private Boolean isDeprecated;
|
||||
|
||||
public MaterialDTO() {
|
||||
}
|
||||
|
||||
|
|
@ -57,6 +61,15 @@ public class MaterialDTO {
|
|||
this.name = name;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public Boolean getDeprecated() {
|
||||
return isDeprecated;
|
||||
}
|
||||
|
||||
public void setDeprecated(Boolean deprecated) {
|
||||
isDeprecated = deprecated;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "MaterialSummaryDTO{" +
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package de.avatic.lcc.model.bulk;
|
|||
public enum NodeHeader implements HeaderProvider {
|
||||
MAPPING_ID("Mapping ID"), NAME("Name"), ADDRESS("Address"),
|
||||
COUNTRY("Country (ISO 3166-1)"), GEO_LATITUDE("Latitude"), GEO_LONGITUDE("Longitude"),
|
||||
IS_ORIGIN("Origin"), IS_INTERMEDIATE("Intermediate"), IS_DESTINATION("Destination"),
|
||||
IS_SOURCE("Source"), IS_INTERMEDIATE("Intermediate"), IS_DESTINATION("Destination"),
|
||||
OUTBOUND_COUNTRIES("Outbound countries (ISO 3166-1)"), PREDECESSOR_NODES("Predecessor Nodes (Mapping ID)"),
|
||||
IS_PREDECESSOR_MANDATORY("Predecessors mandatory");
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ public enum DimensionUnit {
|
|||
}
|
||||
|
||||
throw new IllegalArgumentException("Unknown DimensionUnit: " + value +
|
||||
". Valid values are: t, kg, g (case insensitive)");
|
||||
". Valid values are: mm, cm, m (case insensitive)");
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -106,6 +106,18 @@ public class MaterialRepository {
|
|||
return new SearchQueryResult<>(materials, pagination.getPage(), totalCount, pagination.getLimit());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Optional<Material> getByIdIncludeDeprecated(Integer id) {
|
||||
String query = "SELECT * FROM material WHERE id = ?";
|
||||
|
||||
var material = jdbcTemplate.query(query, new MaterialMapper(), id);
|
||||
|
||||
if(material.isEmpty())
|
||||
return Optional.empty();
|
||||
|
||||
return Optional.ofNullable(material.getFirst());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Optional<Material> getById(Integer id) {
|
||||
String query = "SELECT * FROM material WHERE id = ? AND is_deprecated = FALSE";
|
||||
|
|
|
|||
|
|
@ -26,12 +26,62 @@ public class ContainerRateRepository {
|
|||
this.jdbcTemplate = jdbcTemplate;
|
||||
}
|
||||
|
||||
public SearchQueryResult<ContainerRate> listRatesByPeriodId(SearchQueryPagination pagination, Integer periodId) {
|
||||
String query = "SELECT * FROM container_rate WHERE validity_period_id = ? ORDER BY id LIMIT ? OFFSET ?";
|
||||
String countQuery = "SELECT COUNT(*) FROM container_rate WHERE validity_period_id = ?";
|
||||
Integer totalCount = jdbcTemplate.queryForObject(countQuery, Integer.class, periodId);
|
||||
/**
|
||||
* Retrieves a paginated list of {@link ContainerRate} entries filtered by a specific validity period ID.
|
||||
* Optionally filters results by searching node names and external mapping IDs for both source and destination nodes.
|
||||
*
|
||||
* @param filter optional search term to filter by node names or external mapping IDs (case-insensitive partial match).
|
||||
* Searches across from_node and to_node names and external_mapping_ids. Can be null or empty.
|
||||
* @param pagination the {@link SearchQueryPagination} object containing limit, offset, and page details
|
||||
* @param periodId the ID of the validity period to filter the rates by
|
||||
* @return a {@link SearchQueryResult} containing a list of filtered {@link ContainerRate} entities,
|
||||
* total count, and pagination details
|
||||
*/
|
||||
public SearchQueryResult<ContainerRate> listRatesByPeriodId(String filter, SearchQueryPagination pagination, Integer periodId) {
|
||||
StringBuilder queryBuilder = new StringBuilder();
|
||||
StringBuilder countQueryBuilder = new StringBuilder();
|
||||
List<Object> params = new ArrayList<>();
|
||||
List<Object> countParams = new ArrayList<>();
|
||||
|
||||
return new SearchQueryResult<>(jdbcTemplate.query(query, new ContainerRateMapper(), periodId, pagination.getLimit(), pagination.getOffset()), pagination.getPage(), totalCount, pagination.getLimit());
|
||||
// Base query with node joins
|
||||
String baseQuery = """
|
||||
FROM container_rate cr
|
||||
JOIN node fn ON cr.from_node_id = fn.id
|
||||
JOIN node tn ON cr.to_node_id = tn.id
|
||||
WHERE cr.validity_period_id = ?
|
||||
""";
|
||||
|
||||
queryBuilder.append("SELECT cr.* ").append(baseQuery);
|
||||
countQueryBuilder.append("SELECT COUNT(*) ").append(baseQuery);
|
||||
|
||||
params.add(periodId);
|
||||
countParams.add(periodId);
|
||||
|
||||
// Add filter conditions if filter is provided
|
||||
if (filter != null && !filter.trim().isEmpty()) {
|
||||
String filterCondition = """
|
||||
AND (fn.name LIKE ? OR fn.external_mapping_id LIKE ?
|
||||
OR tn.name LIKE ? OR tn.external_mapping_id LIKE ?)
|
||||
""";
|
||||
|
||||
queryBuilder.append(filterCondition);
|
||||
countQueryBuilder.append(filterCondition);
|
||||
|
||||
String filterParam = "%" + filter.trim() + "%";
|
||||
for (int i = 0; i < 4; i++) {
|
||||
params.add(filterParam);
|
||||
countParams.add(filterParam);
|
||||
}
|
||||
}
|
||||
|
||||
queryBuilder.append(" ORDER BY cr.id LIMIT ? OFFSET ?");
|
||||
params.add(pagination.getLimit());
|
||||
params.add(pagination.getOffset());
|
||||
|
||||
Integer totalCount = jdbcTemplate.queryForObject(countQueryBuilder.toString(), Integer.class, countParams.toArray());
|
||||
var results = jdbcTemplate.query(queryBuilder.toString(), new ContainerRateMapper(), params.toArray());
|
||||
|
||||
return new SearchQueryResult<>(results, pagination.getPage(), totalCount, pagination.getLimit());
|
||||
}
|
||||
|
||||
public Optional<ContainerRate> getById(Integer id) {
|
||||
|
|
@ -48,7 +98,7 @@ public class ContainerRateRepository {
|
|||
public List<ContainerRate> listAllRatesByPeriodId(Integer periodId) {
|
||||
String query = "SELECT * FROM container_rate WHERE validity_period_id = ?";
|
||||
|
||||
return jdbcTemplate.query(query, new ContainerRateMapper());
|
||||
return jdbcTemplate.query(query, new ContainerRateMapper(), periodId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import org.springframework.transaction.annotation.Transactional;
|
|||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
|
|
@ -47,17 +48,61 @@ public class MatrixRateRepository {
|
|||
|
||||
/**
|
||||
* Retrieves a paginated list of {@link MatrixRate} entries filtered by a specific validity period ID.
|
||||
* Optionally filters results by searching country names and ISO codes for both source and destination countries.
|
||||
*
|
||||
* @param filter optional search term to filter by country names or ISO codes (case-insensitive partial match).
|
||||
* Searches across from_country and to_country names and iso_codes. Can be null or empty.
|
||||
* @param pagination the {@link SearchQueryPagination} object containing limit, offset, and page details
|
||||
* @param periodId the ID of the validity period to filter the rates by
|
||||
* @return a {@link SearchQueryResult} containing a list of filtered {@link MatrixRate} entities,
|
||||
* total count, and pagination details
|
||||
*/
|
||||
@Transactional
|
||||
public SearchQueryResult<MatrixRate> listRatesByPeriodId(SearchQueryPagination pagination, Integer periodId) {
|
||||
String query = "SELECT * FROM country_matrix_rate WHERE validity_period_id = ? ORDER BY id LIMIT ? OFFSET ?";
|
||||
var totalCount = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM country_matrix_rate WHERE validity_period_id = ?", Integer.class, periodId);
|
||||
return new SearchQueryResult<>(jdbcTemplate.query(query, new MatrixRateMapper(), periodId, pagination.getLimit(), pagination.getOffset()), pagination.getPage(), totalCount, pagination.getLimit());
|
||||
public SearchQueryResult<MatrixRate> listRatesByPeriodId(String filter, SearchQueryPagination pagination, Integer periodId) {
|
||||
StringBuilder queryBuilder = new StringBuilder();
|
||||
StringBuilder countQueryBuilder = new StringBuilder();
|
||||
List<Object> params = new ArrayList<>();
|
||||
List<Object> countParams = new ArrayList<>();
|
||||
|
||||
// Base query with country joins
|
||||
String baseQuery = """
|
||||
FROM country_matrix_rate cmr
|
||||
JOIN country fc ON cmr.from_country_id = fc.id
|
||||
JOIN country tc ON cmr.to_country_id = tc.id
|
||||
WHERE cmr.validity_period_id = ?
|
||||
""";
|
||||
|
||||
queryBuilder.append("SELECT cmr.* ").append(baseQuery);
|
||||
countQueryBuilder.append("SELECT COUNT(*) ").append(baseQuery);
|
||||
|
||||
params.add(periodId);
|
||||
countParams.add(periodId);
|
||||
|
||||
// Add filter conditions if filter is provided
|
||||
if (filter != null && !filter.trim().isEmpty()) {
|
||||
String filterCondition = """
|
||||
AND (fc.name LIKE ? OR fc.iso_code LIKE ?
|
||||
OR tc.name LIKE ? OR tc.iso_code LIKE ?)
|
||||
""";
|
||||
|
||||
queryBuilder.append(filterCondition);
|
||||
countQueryBuilder.append(filterCondition);
|
||||
|
||||
String filterParam = "%" + filter.trim() + "%";
|
||||
for (int i = 0; i < 4; i++) {
|
||||
params.add(filterParam);
|
||||
countParams.add(filterParam);
|
||||
}
|
||||
}
|
||||
|
||||
queryBuilder.append(" ORDER BY cmr.id LIMIT ? OFFSET ?");
|
||||
params.add(pagination.getLimit());
|
||||
params.add(pagination.getOffset());
|
||||
|
||||
var totalCount = jdbcTemplate.queryForObject(countQueryBuilder.toString(), Integer.class, countParams.toArray());
|
||||
var results = jdbcTemplate.query(queryBuilder.toString(), new MatrixRateMapper(), params.toArray());
|
||||
|
||||
return new SearchQueryResult<>(results, pagination.getPage(), totalCount, pagination.getLimit());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -75,7 +120,7 @@ public class MatrixRateRepository {
|
|||
@Transactional
|
||||
public List<MatrixRate> listAllRatesByPeriodId(Integer periodId) {
|
||||
String query = "SELECT * FROM country_matrix_rate WHERE validity_period_id = ?";
|
||||
return jdbcTemplate.query(query, new MatrixRateMapper());
|
||||
return jdbcTemplate.query(query, new MatrixRateMapper(), periodId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
|
|
|
|||
|
|
@ -51,9 +51,9 @@ public class ContainerRateService {
|
|||
* @return a {@link SearchQueryResult} containing container rate DTOs with pagination information
|
||||
*/
|
||||
@Transactional
|
||||
public SearchQueryResult<ContainerRateDTO> listRates(int limit, int page, LocalDateTime validAt) {
|
||||
public SearchQueryResult<ContainerRateDTO> listRates(String filter, int limit, int page, LocalDateTime validAt) {
|
||||
Optional<Integer> periodId = validityPeriodRepository.getPeriodId(validAt);
|
||||
return listRates(limit, page, periodId.orElseThrow());
|
||||
return listRates(filter, limit, page, periodId.orElseThrow());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -65,11 +65,11 @@ public class ContainerRateService {
|
|||
* @return a {@link SearchQueryResult} containing container rate DTOs with pagination information
|
||||
*/
|
||||
@Transactional
|
||||
public SearchQueryResult<ContainerRateDTO> listRates(int limit, int page, Integer periodId) {
|
||||
public SearchQueryResult<ContainerRateDTO> listRates(String filter, int limit, int page, Integer periodId) {
|
||||
if(null == periodId)
|
||||
return listRates(limit, page);
|
||||
return listRates(filter,limit, page);
|
||||
|
||||
return SearchQueryResult.map(containerRateRepository.listRatesByPeriodId(new SearchQueryPagination(limit, page), periodId), this::toContainerRateDTO);
|
||||
return SearchQueryResult.map(containerRateRepository.listRatesByPeriodId(filter, new SearchQueryPagination(page,limit), periodId), this::toContainerRateDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -81,8 +81,8 @@ public class ContainerRateService {
|
|||
* @return a {@link SearchQueryResult} containing container rate DTOs with pagination information
|
||||
*/
|
||||
@Transactional
|
||||
public SearchQueryResult<ContainerRateDTO> listRates(int limit, int page) {
|
||||
var data = validityPeriodRepository.getValidPeriodId().map(id -> containerRateRepository.listRatesByPeriodId(new SearchQueryPagination(page, limit), id)).orElseThrow(() -> new InternalErrorException("No validity period that is VALID"));
|
||||
public SearchQueryResult<ContainerRateDTO> listRates(String filter, int limit, int page) {
|
||||
var data = validityPeriodRepository.getValidPeriodId().map(id -> containerRateRepository.listRatesByPeriodId(filter, new SearchQueryPagination(page, limit), id)).orElseThrow(() -> new InternalErrorException("No validity period that is VALID"));
|
||||
return SearchQueryResult.map(data, this::toContainerRateDTO);
|
||||
}
|
||||
|
||||
|
|
@ -111,7 +111,7 @@ public class ContainerRateService {
|
|||
dto.setType(TransportType.valueOf(entity.getType().name()));
|
||||
dto.setValidityPeriod(validityPeriodTransformer.toValidityPeriodDTO(validityPeriodRepository.getById(entity.getValidityPeriodId())));
|
||||
dto.setLeadTime(entity.getLeadTime());
|
||||
dto.setOrigin(nodeTransformer.toNodeDTO(nodeRepository.getById(entity.getFromNodeId()).orElseThrow()));
|
||||
dto.setSource(nodeTransformer.toNodeDTO(nodeRepository.getById(entity.getFromNodeId()).orElseThrow()));
|
||||
dto.setDestination(nodeTransformer.toNodeDTO(nodeRepository.getById(entity.getToNodeId()).orElseThrow()));
|
||||
|
||||
var rates = new HashMap<String, Number>();
|
||||
|
|
|
|||
|
|
@ -40,15 +40,17 @@ public class MaterialService {
|
|||
}
|
||||
|
||||
/**
|
||||
* Lists materials based on a filter and pagination parameters.
|
||||
* Lists materials based on the provided criteria, including optional filtering,
|
||||
* pagination, and exclusion of deprecated entries.
|
||||
*
|
||||
* @param filter the search filter to apply
|
||||
* @param page the page number for pagination (zero-based)
|
||||
* @param limit the maximum number of items per page
|
||||
* @return a {@link SearchQueryResult} containing a list of material DTOs and pagination details
|
||||
* @param filter an optional filter to apply to the material list, such as a search term
|
||||
* @param page the page number for pagination
|
||||
* @param limit the maximum number of materials to include in the result
|
||||
* @param excludeDeprecated a flag indicating whether to exclude deprecated materials
|
||||
* @return a {@code SearchQueryResult<MaterialDTO>} containing the paginated list of materials
|
||||
*/
|
||||
public SearchQueryResult<MaterialDTO> listMaterial(Optional<String> filter, int page, int limit) {
|
||||
SearchQueryResult<Material> queryResult = materialRepository.listMaterials(filter, true, new SearchQueryPagination(page, limit));
|
||||
public SearchQueryResult<MaterialDTO> listMaterial(Optional<String> filter, int page, int limit, boolean excludeDeprecated) {
|
||||
SearchQueryResult<Material> queryResult = materialRepository.listMaterials(filter, excludeDeprecated, new SearchQueryPagination(page, limit));
|
||||
return SearchQueryResult.map(queryResult, materialTransformer::toMaterialDTO);
|
||||
}
|
||||
|
||||
|
|
@ -60,7 +62,7 @@ public class MaterialService {
|
|||
* @throws MaterialNotFoundException if no material with the given ID is found
|
||||
*/
|
||||
public MaterialDetailDTO getMaterial(Integer id) {
|
||||
return materialDetailTransformer.toMaterialDetailDTO(materialRepository.getById(id).orElseThrow(() -> new NotFoundException(NotFoundException.NotFoundType.MATERIAL,id.toString())));
|
||||
return materialDetailTransformer.toMaterialDetailDTO(materialRepository.getByIdIncludeDeprecated(id).orElseThrow(() -> new NotFoundException(NotFoundException.NotFoundType.MATERIAL,id.toString())));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -2,12 +2,13 @@ package de.avatic.lcc.service.access;
|
|||
|
||||
import de.avatic.lcc.dto.configuration.matrixrates.MatrixRateDTO;
|
||||
import de.avatic.lcc.model.rates.MatrixRate;
|
||||
import de.avatic.lcc.model.rates.ValidityPeriod;
|
||||
import de.avatic.lcc.repositories.NodeRepository;
|
||||
import de.avatic.lcc.repositories.country.CountryRepository;
|
||||
import de.avatic.lcc.repositories.pagination.SearchQueryPagination;
|
||||
import de.avatic.lcc.repositories.pagination.SearchQueryResult;
|
||||
import de.avatic.lcc.repositories.rates.MatrixRateRepository;
|
||||
import de.avatic.lcc.repositories.rates.ValidityPeriodRepository;
|
||||
import de.avatic.lcc.service.transformer.generic.CountryTransformer;
|
||||
import de.avatic.lcc.service.transformer.generic.NodeTransformer;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
|
@ -25,15 +26,15 @@ public class MatrixRateService {
|
|||
|
||||
private final ValidityPeriodRepository validityPeriodRepository;
|
||||
private final MatrixRateRepository matrixRateRepository;
|
||||
private final NodeRepository nodeRepository;
|
||||
private final NodeTransformer nodeTransformer;
|
||||
private final CountryRepository countryRepository;
|
||||
private final CountryTransformer countryTransformer;
|
||||
|
||||
public MatrixRateService(ValidityPeriodRepository validityPeriodRepository, MatrixRateRepository matrixRateRepository, NodeRepository nodeRepository, NodeTransformer nodeTransformer) {
|
||||
public MatrixRateService(ValidityPeriodRepository validityPeriodRepository, MatrixRateRepository matrixRateRepository, CountryRepository countryRepository, CountryTransformer countryTransformer) {
|
||||
|
||||
this.validityPeriodRepository = validityPeriodRepository;
|
||||
this.matrixRateRepository = matrixRateRepository;
|
||||
this.nodeRepository = nodeRepository;
|
||||
this.nodeTransformer = nodeTransformer;
|
||||
this.countryRepository = countryRepository;
|
||||
this.countryTransformer = countryTransformer;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -45,9 +46,9 @@ public class MatrixRateService {
|
|||
* @return a {@link SearchQueryResult} containing matrix rate DTOs with pagination information
|
||||
*/
|
||||
@Transactional
|
||||
public SearchQueryResult<MatrixRateDTO> listRates(int limit, int page, LocalDateTime validAt) {
|
||||
public SearchQueryResult<MatrixRateDTO> listRates(String filter, int limit, int page, LocalDateTime validAt) {
|
||||
Optional<Integer> periodId = validityPeriodRepository.getPeriodId(validAt);
|
||||
return listRates(limit, page, periodId.orElseThrow());
|
||||
return listRates(filter, limit, page, periodId.orElseThrow());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -59,11 +60,11 @@ public class MatrixRateService {
|
|||
* @return a {@link SearchQueryResult} containing matrix rate DTOs with pagination information
|
||||
*/
|
||||
@Transactional
|
||||
public SearchQueryResult<MatrixRateDTO> listRates(int limit, int page, Integer periodId) {
|
||||
public SearchQueryResult<MatrixRateDTO> listRates(String filter, int limit, int page, Integer periodId) {
|
||||
if (null == periodId)
|
||||
return listRates(limit, page);
|
||||
return listRates(filter,limit, page);
|
||||
|
||||
return SearchQueryResult.map(matrixRateRepository.listRatesByPeriodId(new SearchQueryPagination(page, limit), periodId), this::toMatrixRateDTO);
|
||||
return SearchQueryResult.map(matrixRateRepository.listRatesByPeriodId(filter, new SearchQueryPagination(page, limit), periodId), this::toMatrixRateDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -74,9 +75,9 @@ public class MatrixRateService {
|
|||
* @return a {@link SearchQueryResult} containing matrix rate DTOs with pagination information
|
||||
*/
|
||||
@Transactional
|
||||
public SearchQueryResult<MatrixRateDTO> listRates(int limit, int page) {
|
||||
public SearchQueryResult<MatrixRateDTO> listRates(String filter, int limit, int page) {
|
||||
Integer id = validityPeriodRepository.getValidPeriodId().orElseThrow(() -> new IllegalStateException("No valid period found that is VALID"));
|
||||
return SearchQueryResult.map(matrixRateRepository.listRatesByPeriodId(new SearchQueryPagination(page, limit), id), this::toMatrixRateDTO);
|
||||
return SearchQueryResult.map(matrixRateRepository.listRatesByPeriodId(filter, new SearchQueryPagination(page, limit), id), this::toMatrixRateDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -100,8 +101,8 @@ public class MatrixRateService {
|
|||
MatrixRateDTO dto = new MatrixRateDTO();
|
||||
|
||||
dto.setId(entity.getId());
|
||||
dto.setOrigin(nodeTransformer.toNodeDTO(nodeRepository.getById(entity.getFromCountry()).orElseThrow()));
|
||||
dto.setDestination(nodeTransformer.toNodeDTO(nodeRepository.getById(entity.getToCountry()).orElseThrow()));
|
||||
dto.setSource(countryTransformer.toCountryDTO(countryRepository.getById(entity.getFromCountry()).orElseThrow()));
|
||||
dto.setDestination(countryTransformer.toCountryDTO(countryRepository.getById(entity.getToCountry()).orElseThrow()));
|
||||
dto.setRate(entity.getRate());
|
||||
|
||||
return dto;
|
||||
|
|
|
|||
|
|
@ -66,30 +66,35 @@ public class BulkExportService {
|
|||
var hiddenCountrySheet = workbook.createSheet(HiddenTableType.COUNTRY_HIDDEN_TABLE.getSheetName());
|
||||
hiddenCountryExcelMapper.fillSheet(hiddenCountrySheet, style);
|
||||
hiddenCountrySheet.protectSheet(sheetPassword);
|
||||
workbook.setSheetVisibility(workbook.getSheetIndex(hiddenCountrySheet), SheetVisibility.VERY_HIDDEN);
|
||||
workbook.setSheetVisibility(workbook.getSheetIndex(hiddenCountrySheet), SheetVisibility.HIDDEN);
|
||||
} else if (bulkFileType.equals(BulkFileType.CONTAINER_RATE) || bulkFileType.equals(BulkFileType.PACKAGING)) {
|
||||
var hiddenNodeSheet = workbook.createSheet(HiddenTableType.NODE_HIDDEN_TABLE.getSheetName());
|
||||
hiddenNodeExcelMapper.fillSheet(hiddenNodeSheet, style, BulkFileType.PACKAGING.equals(bulkFileType));
|
||||
hiddenNodeSheet.protectSheet(sheetPassword);
|
||||
workbook.setSheetVisibility(workbook.getSheetIndex(hiddenNodeSheet), SheetVisibility.VERY_HIDDEN);
|
||||
workbook.setSheetVisibility(workbook.getSheetIndex(hiddenNodeSheet), SheetVisibility.HIDDEN);
|
||||
}
|
||||
|
||||
// Create headers based on the bulk file type
|
||||
switch (bulkFileType) {
|
||||
case CONTAINER_RATE:
|
||||
containerRateExcelMapper.fillSheet(worksheet, style, periodId);
|
||||
containerRateExcelMapper.createConstraints(workbook, worksheet);
|
||||
break;
|
||||
case COUNTRY_MATRIX:
|
||||
matrixRateExcelMapper.fillSheet(worksheet, style, periodId);
|
||||
matrixRateExcelMapper.createConstraints(workbook, worksheet);
|
||||
break;
|
||||
case MATERIAL:
|
||||
materialExcelMapper.fillSheet(worksheet, style);
|
||||
materialExcelMapper.createConstraints(worksheet);
|
||||
break;
|
||||
case PACKAGING:
|
||||
packagingExcelMapper.fillSheet(worksheet, style);
|
||||
packagingExcelMapper.createConstraints(workbook, worksheet);
|
||||
break;
|
||||
case NODE:
|
||||
nodeExcelMapper.fillSheet(worksheet, style);
|
||||
nodeExcelMapper.createConstraints(workbook, worksheet);
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@ import de.avatic.lcc.model.bulk.*;
|
|||
import de.avatic.lcc.service.bulk.helper.HeaderCellStyleProvider;
|
||||
import de.avatic.lcc.service.bulk.helper.HeaderGenerator;
|
||||
import de.avatic.lcc.service.excelMapper.*;
|
||||
import org.apache.poi.ss.usermodel.*;
|
||||
import org.apache.poi.ss.usermodel.CellStyle;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.ss.usermodel.SheetVisibility;
|
||||
import org.apache.poi.ss.usermodel.Workbook;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
|
|
@ -44,26 +47,24 @@ public class TemplateExportService {
|
|||
}
|
||||
|
||||
public InputStreamSource generateTemplate(BulkFileType bulkFileType) {
|
||||
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
||||
Workbook workbook = new XSSFWorkbook();
|
||||
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
Workbook workbook = new XSSFWorkbook();) {
|
||||
|
||||
Sheet sheet = workbook.createSheet(BulkFileTypes.valueOf(bulkFileType.name()).getSheetName());
|
||||
|
||||
CellStyle style = headerCellStyleProvider.createHeaderCellStyle(workbook);
|
||||
|
||||
|
||||
if(bulkFileType.equals(BulkFileType.COUNTRY_MATRIX) || bulkFileType.equals(BulkFileType.NODE))
|
||||
{
|
||||
if (bulkFileType.equals(BulkFileType.COUNTRY_MATRIX) || bulkFileType.equals(BulkFileType.NODE)) {
|
||||
var hiddenCountrySheet = workbook.createSheet(HiddenTableType.COUNTRY_HIDDEN_TABLE.getSheetName());
|
||||
hiddenCountryExcelMapper.fillSheet(hiddenCountrySheet, style);
|
||||
hiddenCountrySheet.protectSheet(sheetPassword);
|
||||
workbook.setSheetVisibility(workbook.getSheetIndex(hiddenCountrySheet), SheetVisibility.VERY_HIDDEN);
|
||||
}
|
||||
else if(bulkFileType.equals(BulkFileType.CONTAINER_RATE) || bulkFileType.equals(BulkFileType.PACKAGING))
|
||||
{
|
||||
workbook.setSheetVisibility(workbook.getSheetIndex(hiddenCountrySheet), SheetVisibility.HIDDEN);
|
||||
} else if (bulkFileType.equals(BulkFileType.CONTAINER_RATE) || bulkFileType.equals(BulkFileType.PACKAGING)) {
|
||||
var hiddenNodeSheet = workbook.createSheet(HiddenTableType.NODE_HIDDEN_TABLE.getSheetName());
|
||||
hiddenNodeExcelMapper.fillSheet(hiddenNodeSheet, style, BulkFileType.PACKAGING.equals(bulkFileType));
|
||||
hiddenNodeSheet.protectSheet(sheetPassword);
|
||||
workbook.setSheetVisibility(workbook.getSheetIndex(hiddenNodeSheet), SheetVisibility.VERY_HIDDEN);
|
||||
workbook.setSheetVisibility(workbook.getSheetIndex(hiddenNodeSheet), SheetVisibility.HIDDEN);
|
||||
}
|
||||
|
||||
// Create headers and constraints based on the bulk file type
|
||||
|
|
@ -71,22 +72,27 @@ public class TemplateExportService {
|
|||
case CONTAINER_RATE:
|
||||
headerGenerator.generateHeader(sheet, ContainerRateHeader.class, style);
|
||||
containerRateExcelMapper.createConstraints(workbook, sheet);
|
||||
headerGenerator.fixWidth(sheet, ContainerRateHeader.class);
|
||||
break;
|
||||
case COUNTRY_MATRIX:
|
||||
headerGenerator.generateHeader(sheet, MatrixRateHeader.class, style);
|
||||
matrixRateExcelMapper.createConstraints(workbook, sheet);
|
||||
headerGenerator.fixWidth(sheet, MatrixRateHeader.class);
|
||||
break;
|
||||
case MATERIAL:
|
||||
headerGenerator.generateHeader(sheet, MaterialHeader.class, style);
|
||||
materialExcelMapper.createConstraints(sheet);
|
||||
headerGenerator.fixWidth(sheet, MaterialHeader.class);
|
||||
break;
|
||||
case PACKAGING:
|
||||
headerGenerator.generateHeader(sheet, PackagingHeader.class, style);
|
||||
packagingExcelMapper.createConstraints(workbook, sheet);
|
||||
headerGenerator.fixWidth(sheet, PackagingHeader.class);
|
||||
break;
|
||||
case NODE:
|
||||
headerGenerator.generateHeader(sheet, NodeHeader.class, style);
|
||||
nodeExcelMapper.createConstraints(workbook, sheet);
|
||||
headerGenerator.fixWidth(sheet, NodeHeader.class);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported bulk file type: " + bulkFileType);
|
||||
|
|
|
|||
|
|
@ -10,10 +10,13 @@ import org.apache.poi.ss.util.CellRangeAddressList;
|
|||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.function.Function;
|
||||
|
||||
@Service
|
||||
public class ConstraintGenerator {
|
||||
|
||||
private static final int MAX_ROWS = 10000;
|
||||
|
||||
|
||||
public void createBooleanConstraint(Sheet sheet, Integer columnIdx) {
|
||||
createConstraint(sheet, columnIdx, new String[]{"true", "false"});
|
||||
|
|
@ -23,12 +26,25 @@ public class ConstraintGenerator {
|
|||
createConstraint(sheet, columnIdx, EnumSet.allOf(values).stream().map(Enum::name).toArray(String[]::new));
|
||||
}
|
||||
|
||||
public <T extends Enum<T>> void createEnumConstraint(Sheet sheet, Integer columnIdx, Class<T> values, Function<T, String> resolver) {
|
||||
createConstraint(sheet, columnIdx, EnumSet.allOf(values).stream().map(Enum::name).toArray(String[]::new));
|
||||
}
|
||||
|
||||
private void createConstraint(Sheet sheet, Integer columnIdx, String[] values) {
|
||||
var helper = sheet.getDataValidationHelper();
|
||||
var constraint = helper.createExplicitListConstraint(values);
|
||||
|
||||
var validation = helper.createValidation(constraint, new CellRangeAddressList(1, SpreadsheetVersion.EXCEL2007.getMaxRows(), columnIdx, columnIdx));
|
||||
validation.createErrorBox("Invalid Value", values.length > 8 ? "Please check dropdown for allowed values." : String.format("Allowed values are: %s.", String.join(", ", values)));
|
||||
|
||||
|
||||
var validation = helper.createValidation(constraint, new CellRangeAddressList(1, MAX_ROWS, columnIdx, columnIdx));
|
||||
|
||||
String errorMessage = values.length > 8 ? "Please check dropdown for allowed values." :
|
||||
String.format("Allowed values: %s", String.join(", ", values));
|
||||
if (errorMessage.length() > 200) {
|
||||
errorMessage = "Please check dropdown for allowed values.";
|
||||
}
|
||||
|
||||
validation.createErrorBox("Invalid Value", errorMessage);
|
||||
validation.setShowErrorBox(true);
|
||||
sheet.addValidationData(validation);
|
||||
}
|
||||
|
|
@ -37,7 +53,7 @@ public class ConstraintGenerator {
|
|||
var helper = sheet.getDataValidationHelper();
|
||||
var constraint = helper.createFormulaListConstraint(namedReference);
|
||||
|
||||
var validation = helper.createValidation(constraint, new CellRangeAddressList(1, SpreadsheetVersion.EXCEL2007.getMaxRows(), columnIdx, columnIdx));
|
||||
var validation = helper.createValidation(constraint, new CellRangeAddressList(1, MAX_ROWS, columnIdx, columnIdx));
|
||||
validation.createErrorBox("Invalid Value", "Please check dropdown for allowed values.");
|
||||
validation.setShowErrorBox(true);
|
||||
sheet.addValidationData(validation);
|
||||
|
|
@ -47,7 +63,7 @@ public class ConstraintGenerator {
|
|||
var helper = sheet.getDataValidationHelper();
|
||||
var constraint = helper.createTextLengthConstraint(DataValidationConstraint.OperatorType.BETWEEN, String.valueOf(min), String.valueOf(max));
|
||||
|
||||
var validation = helper.createValidation(constraint, new CellRangeAddressList(1, SpreadsheetVersion.EXCEL2007.getMaxRows(), columnIdx, columnIdx));
|
||||
var validation = helper.createValidation(constraint, new CellRangeAddressList(1, MAX_ROWS, columnIdx, columnIdx));
|
||||
validation.createErrorBox("Invalid Value", String.format("Allowed length is between %d and %d characters.", min, max));
|
||||
validation.setShowErrorBox(true);
|
||||
sheet.addValidationData(validation);
|
||||
|
|
@ -57,7 +73,7 @@ public class ConstraintGenerator {
|
|||
var helper = sheet.getDataValidationHelper();
|
||||
var constraint = helper.createDecimalConstraint(DataValidationConstraint.OperatorType.BETWEEN, String.valueOf(min), String.valueOf(max));
|
||||
|
||||
var validation = helper.createValidation(constraint, new CellRangeAddressList(1, SpreadsheetVersion.EXCEL2007.getMaxRows(), columnIdx, columnIdx));
|
||||
var validation = helper.createValidation(constraint, new CellRangeAddressList(1, MAX_ROWS, columnIdx, columnIdx));
|
||||
validation.createErrorBox("Invalid Value", String.format("Allowed value is between %f and %f.", min.floatValue(), max.floatValue()));
|
||||
validation.setShowErrorBox(true);
|
||||
sheet.addValidationData(validation);
|
||||
|
|
@ -67,23 +83,39 @@ public class ConstraintGenerator {
|
|||
var helper = sheet.getDataValidationHelper();
|
||||
var constraint = helper.createIntegerConstraint(DataValidationConstraint.OperatorType.BETWEEN, String.valueOf(min), String.valueOf(max));
|
||||
|
||||
var validation = helper.createValidation(constraint, new CellRangeAddressList(1, SpreadsheetVersion.EXCEL2007.getMaxRows(), columnIdx, columnIdx));
|
||||
var validation = helper.createValidation(constraint, new CellRangeAddressList(1, MAX_ROWS, columnIdx, columnIdx));
|
||||
validation.createErrorBox("Invalid Value", String.format("Allowed value is between %f and %f.", min.floatValue(), max.floatValue()));
|
||||
validation.setShowErrorBox(true);
|
||||
sheet.addValidationData(validation);
|
||||
}
|
||||
|
||||
public Name createReference(Workbook workbook, Integer columnIdx, HiddenTableType hiddenTableType ) {
|
||||
public Name createReference(Workbook workbook, Integer columnIdx, HiddenTableType hiddenTableType) {
|
||||
// Check if the referenced sheet exists
|
||||
Sheet hiddenSheet = workbook.getSheet(hiddenTableType.getSheetName());
|
||||
if (hiddenSheet == null) {
|
||||
throw new IllegalStateException("Hidden sheet " + hiddenTableType.getSheetName() + " does not exist");
|
||||
}
|
||||
|
||||
Name namedRange = workbook.createName();
|
||||
namedRange.setNameName(hiddenTableType.getReferenceName());
|
||||
String reference = hiddenTableType.getSheetName()+ "!$"+toExcelLetter(columnIdx)+"$1:$"+toExcelLetter(columnIdx)+"$" + SpreadsheetVersion.EXCEL2007.getMaxRows();
|
||||
|
||||
// Create a more conservative range - only to the last row with data
|
||||
int lastRow = Math.max(hiddenSheet.getLastRowNum(), 1); // At least row 1
|
||||
String columnLetter = toExcelLetter(columnIdx);
|
||||
String reference = hiddenTableType.getSheetName() + "!$" + columnLetter + "$2:$" + columnLetter + "$" + (lastRow + 1);
|
||||
|
||||
namedRange.setRefersToFormula(reference);
|
||||
return namedRange;
|
||||
}
|
||||
|
||||
private String toExcelLetter(int columnIdx) {
|
||||
int digit = columnIdx % 26;
|
||||
int quotient = columnIdx / 26;
|
||||
return (quotient==0 ? "" : toExcelLetter(quotient)) + (char) ('A' + digit);
|
||||
StringBuilder result = new StringBuilder();
|
||||
columnIdx++; // Convert from 0-based to 1-based for the algorithm
|
||||
while (columnIdx > 0) {
|
||||
columnIdx--; // Adjust for 1-based indexing
|
||||
result.insert(0, (char) ('A' + columnIdx % 26));
|
||||
columnIdx /= 26;
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,37 @@
|
|||
package de.avatic.lcc.service.bulk.helper;
|
||||
|
||||
import org.apache.poi.ss.usermodel.*;
|
||||
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
|
||||
import org.apache.poi.xssf.usermodel.XSSFColor;
|
||||
import org.apache.poi.xssf.usermodel.XSSFFont;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class HeaderCellStyleProvider {
|
||||
|
||||
public CellStyle createHeaderCellStyle(Workbook workbook) {
|
||||
|
||||
|
||||
|
||||
XSSFFont headerFont = (XSSFFont) workbook.createFont();
|
||||
|
||||
XSSFColor customTextColor = new XSSFColor(new byte[]{(byte)0, (byte)47, (byte)84}, null); // Blue
|
||||
headerFont.setColor(customTextColor);
|
||||
|
||||
headerFont.setFontName("Arial");
|
||||
headerFont.setFontHeightInPoints((short)10);
|
||||
headerFont.setBold(true);
|
||||
|
||||
|
||||
|
||||
XSSFColor customColor = new XSSFColor(new byte[]{(byte)90, (byte)240, (byte)180}, null);
|
||||
|
||||
CellStyle headerStyle = workbook.createCellStyle();
|
||||
Font headerFont = workbook.createFont();
|
||||
headerFont.setBold(true);
|
||||
headerStyle.setFont(headerFont);
|
||||
headerStyle.setFillForegroundColor(IndexedColors.LIGHT_CORNFLOWER_BLUE.getIndex());
|
||||
headerStyle.setFillForegroundColor(customColor);
|
||||
|
||||
|
||||
headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
|
||||
headerStyle.setBorderBottom(BorderStyle.THIN);
|
||||
headerStyle.setBorderTop(BorderStyle.THIN);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package de.avatic.lcc.service.bulk.helper;
|
||||
|
||||
import de.avatic.lcc.model.bulk.HeaderProvider;
|
||||
import de.avatic.lcc.model.bulk.NodeHeader;
|
||||
import org.apache.poi.ss.usermodel.Cell;
|
||||
import org.apache.poi.ss.usermodel.CellStyle;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
|
|
@ -12,6 +13,8 @@ import java.util.EnumSet;
|
|||
@Service
|
||||
public class HeaderGenerator {
|
||||
|
||||
private static final int ADD_COLUMN_SIZE = (10*256);
|
||||
|
||||
public <H extends Enum<H> & HeaderProvider> boolean validateHeader(Sheet sheet, Class<H> headers) {
|
||||
Row row = sheet.getRow(0);
|
||||
for(H header : EnumSet.allOf(headers)){
|
||||
|
|
@ -57,4 +60,19 @@ public class HeaderGenerator {
|
|||
}
|
||||
|
||||
|
||||
public void fixWidth(Sheet sheet, String[] headers) {
|
||||
int idx = 0;
|
||||
for (String header : headers) {
|
||||
sheet.autoSizeColumn(idx);
|
||||
sheet.setColumnWidth(idx,sheet.getColumnWidth(idx)+ADD_COLUMN_SIZE);
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
|
||||
public <H extends Enum<H> & HeaderProvider> void fixWidth(Sheet sheet, Class<H> headers) {
|
||||
for (H header : EnumSet.allOf(headers)) {
|
||||
sheet.autoSizeColumn(header.ordinal());
|
||||
sheet.setColumnWidth(header.ordinal(),sheet.getColumnWidth(header.ordinal())+ADD_COLUMN_SIZE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ public class ContainerRateExcelMapper {
|
|||
public void fillSheet(Sheet sheet, CellStyle headerStyle, Integer periodId) {
|
||||
headerGenerator.generateHeader(sheet, ContainerRateHeader.class, headerStyle);
|
||||
containerRateRepository.listAllRatesByPeriodId(periodId).forEach(rate -> mapToRow(rate, sheet.createRow(sheet.getLastRowNum()+1)));
|
||||
headerGenerator.fixWidth(sheet, ContainerRateHeader.class);
|
||||
}
|
||||
|
||||
private void mapToRow(ContainerRate rate, Row row) {
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ public class MaterialExcelMapper {
|
|||
public void fillSheet(Sheet sheet, CellStyle headerStyle) {
|
||||
headerGenerator.generateHeader(sheet, MaterialHeader.class, headerStyle);
|
||||
materialRepository.listAllMaterials().forEach(material -> mapToRow(material, sheet.createRow(sheet.getLastRowNum()+1)));
|
||||
headerGenerator.fixWidth(sheet, MaterialHeader.class);
|
||||
}
|
||||
|
||||
private void mapToRow(Material material, Row row) {
|
||||
|
|
@ -39,7 +40,7 @@ public class MaterialExcelMapper {
|
|||
|
||||
public void createConstraints(Sheet sheet) {
|
||||
constraintGenerator.createLengthConstraint(sheet, MaterialHeader.PART_NUMBER.ordinal(), 0, 12);
|
||||
constraintGenerator.createLengthConstraint(sheet, MaterialHeader.HS_CODE.ordinal(), 0, 8);
|
||||
constraintGenerator.createLengthConstraint(sheet, MaterialHeader.HS_CODE.ordinal(), 0, 11);
|
||||
constraintGenerator.createLengthConstraint(sheet, MaterialHeader.DESCRIPTION.ordinal(), 1, 500);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ public class MatrixRateExcelMapper {
|
|||
public void fillSheet(Sheet sheet, CellStyle headerStyle, Integer periodId) {
|
||||
headerGenerator.generateHeader(sheet, MatrixRateHeader.class, headerStyle);
|
||||
matrixRateRepository.listAllRatesByPeriodId(periodId).forEach(rate -> mapToRow(rate, sheet.createRow(sheet.getLastRowNum()+1)));
|
||||
headerGenerator.fixWidth(sheet, MatrixRateHeader.class);
|
||||
}
|
||||
|
||||
public List<MatrixRate> extractSheet(Sheet sheet) {
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ public class NodeExcelMapper {
|
|||
{
|
||||
headerGenerator.generateHeader(sheet, NodeHeader.class, headerStyle);
|
||||
nodeRepository.listAllNodes(false).forEach(node -> mapToRow(node, sheet.createRow(sheet.getLastRowNum()+1)));
|
||||
headerGenerator.fixWidth(sheet, NodeHeader.class);
|
||||
}
|
||||
|
||||
private void mapToRow(Node node, Row row) {
|
||||
|
|
@ -50,16 +51,16 @@ public class NodeExcelMapper {
|
|||
row.createCell(NodeHeader.COUNTRY.ordinal()).setCellValue(countryRepository.getById(node.getCountryId()).orElseThrow().getIsoCode().getCode());
|
||||
row.createCell(NodeHeader.GEO_LATITUDE.ordinal()).setCellValue(node.getGeoLat().doubleValue());
|
||||
row.createCell(NodeHeader.GEO_LONGITUDE.ordinal()).setCellValue(node.getGeoLng().doubleValue());
|
||||
row.createCell(NodeHeader.IS_ORIGIN.ordinal()).setCellValue(node.getSource());
|
||||
row.createCell(NodeHeader.IS_INTERMEDIATE.ordinal()).setCellValue(node.getIntermediate());
|
||||
row.createCell(NodeHeader.IS_DESTINATION.ordinal()).setCellValue(node.getDestination());
|
||||
row.createCell(NodeHeader.IS_SOURCE.ordinal()).setCellValue(node.getSource() ? "true":"false");
|
||||
row.createCell(NodeHeader.IS_INTERMEDIATE.ordinal()).setCellValue(node.getIntermediate()? "true":"false");
|
||||
row.createCell(NodeHeader.IS_DESTINATION.ordinal()).setCellValue(node.getDestination()? "true":"false");
|
||||
row.createCell(NodeHeader.OUTBOUND_COUNTRIES.ordinal()).setCellValue(mapOutboundCountriesToCell(node.getOutboundCountries()));
|
||||
row.createCell(NodeHeader.PREDECESSOR_NODES.ordinal()).setCellValue(mapChains(node.getNodePredecessors()));
|
||||
row.createCell(NodeHeader.IS_PREDECESSOR_MANDATORY.ordinal()).setCellValue(node.getPredecessorRequired());
|
||||
row.createCell(NodeHeader.IS_PREDECESSOR_MANDATORY.ordinal()).setCellValue(node.getPredecessorRequired()? "true":"false");
|
||||
}
|
||||
|
||||
private String mapChains(List<Map<Integer, Integer>> chains) {
|
||||
return chains.stream().map(this::mapChain).collect(Collectors.joining(", "));
|
||||
return chains.stream().map(this::mapChain).collect(Collectors.joining("; "));
|
||||
}
|
||||
|
||||
private String mapChain(Map<Integer, Integer> chain) {
|
||||
|
|
@ -78,7 +79,7 @@ public class NodeExcelMapper {
|
|||
constraintGenerator.createFormulaListConstraint(sheet, NodeHeader.COUNTRY.ordinal(), namedRange.getNameName());
|
||||
constraintGenerator.createDecimalConstraint(sheet, NodeHeader.GEO_LATITUDE.ordinal(), -90.0, 90.0);
|
||||
constraintGenerator.createDecimalConstraint(sheet, NodeHeader.GEO_LONGITUDE.ordinal(), -180.0, 180.0);
|
||||
constraintGenerator.createBooleanConstraint(sheet, NodeHeader.IS_ORIGIN.ordinal());
|
||||
constraintGenerator.createBooleanConstraint(sheet, NodeHeader.IS_SOURCE.ordinal());
|
||||
constraintGenerator.createBooleanConstraint(sheet, NodeHeader.IS_INTERMEDIATE.ordinal());
|
||||
constraintGenerator.createBooleanConstraint(sheet, NodeHeader.IS_DESTINATION.ordinal());
|
||||
constraintGenerator.createBooleanConstraint(sheet, NodeHeader.IS_PREDECESSOR_MANDATORY.ordinal());
|
||||
|
|
@ -104,7 +105,7 @@ public class NodeExcelMapper {
|
|||
entity.setCountryId(IsoCode.valueOf(row.getCell(NodeHeader.COUNTRY.ordinal()).getStringCellValue()));
|
||||
entity.setGeoLat(BigDecimal.valueOf(row.getCell(NodeHeader.GEO_LATITUDE.ordinal()).getNumericCellValue()));
|
||||
entity.setGeoLng(BigDecimal.valueOf(row.getCell(NodeHeader.GEO_LONGITUDE.ordinal()).getNumericCellValue()));
|
||||
entity.setSource(row.getCell(NodeHeader.IS_ORIGIN.ordinal()).getBooleanCellValue());
|
||||
entity.setSource(row.getCell(NodeHeader.IS_SOURCE.ordinal()).getBooleanCellValue());
|
||||
entity.setIntermediate(row.getCell(NodeHeader.IS_INTERMEDIATE.ordinal()).getBooleanCellValue());
|
||||
entity.setDestination(row.getCell(NodeHeader.IS_DESTINATION.ordinal()).getBooleanCellValue());
|
||||
entity.setPredecessorRequired(row.getCell(NodeHeader.IS_PREDECESSOR_MANDATORY.ordinal()).getBooleanCellValue());
|
||||
|
|
|
|||
|
|
@ -54,27 +54,28 @@ public class PackagingExcelMapper {
|
|||
|
||||
headerGenerator.generateHeader(sheet, headers.toArray(String[]::new), headerStyle);
|
||||
packagingRepository.listAllPackaging().forEach(p -> mapToRow(p, headers, sheet.createRow(sheet.getLastRowNum() + 1)));
|
||||
headerGenerator.fixWidth(sheet, headers.toArray(String[]::new));
|
||||
}
|
||||
|
||||
private void mapToRow(Packaging packaging, ArrayList<String> headers, Row row) {
|
||||
Optional<PackagingDimension> shu = packagingDimensionRepository.getById(packaging.getShuId());
|
||||
Optional<PackagingDimension> hu = packagingDimensionRepository.getById(packaging.getShuId());
|
||||
|
||||
row.createCell(PackagingHeader.PART_NUMBER.ordinal()).setCellValue(materialRepository.getById(packaging.getMaterialId()).orElseThrow().getPartNumber());
|
||||
row.createCell(PackagingHeader.PART_NUMBER.ordinal()).setCellValue(materialRepository.getByIdIncludeDeprecated(packaging.getMaterialId()).orElseThrow().getPartNumber());
|
||||
row.createCell(PackagingHeader.SUPPLIER.ordinal()).setCellValue(nodeRepository.getById(packaging.getSupplierId()).orElseThrow().getExternalMappingId());
|
||||
|
||||
mapToCell(row.createCell(PackagingHeader.SHU_HEIGHT.ordinal()), shu, PackagingDimension::getHeight);
|
||||
mapToCell(row.createCell(PackagingHeader.SHU_WIDTH.ordinal()), shu, PackagingDimension::getWidth);
|
||||
mapToCell(row.createCell(PackagingHeader.SHU_LENGTH.ordinal()), shu, PackagingDimension::getLength);
|
||||
mapToCell(row.createCell(PackagingHeader.SHU_WEIGHT.ordinal()), shu, PackagingDimension::getWeight);
|
||||
mapDimensionToCell(row.createCell(PackagingHeader.SHU_HEIGHT.ordinal()), shu, PackagingDimension::getHeight);
|
||||
mapDimensionToCell(row.createCell(PackagingHeader.SHU_WIDTH.ordinal()), shu, PackagingDimension::getWidth);
|
||||
mapDimensionToCell(row.createCell(PackagingHeader.SHU_LENGTH.ordinal()), shu, PackagingDimension::getLength);
|
||||
mapWeightToCell(row.createCell(PackagingHeader.SHU_WEIGHT.ordinal()), shu, PackagingDimension::getWeight);
|
||||
mapToCell(row.createCell(PackagingHeader.SHU_UNIT_COUNT.ordinal()), shu, PackagingDimension::getContentUnitCount);
|
||||
mapToCell(row.createCell(PackagingHeader.SHU_DIMENSION_UNIT.ordinal()), shu, PackagingDimension::getDimensionUnit);
|
||||
mapToCell(row.createCell(PackagingHeader.SHU_WEIGHT_UNIT.ordinal()), shu, PackagingDimension::getWeightUnit);
|
||||
|
||||
mapToCell(row.createCell(PackagingHeader.HU_HEIGHT.ordinal()), hu, PackagingDimension::getHeight);
|
||||
mapToCell(row.createCell(PackagingHeader.HU_WIDTH.ordinal()), hu, PackagingDimension::getWidth);
|
||||
mapToCell(row.createCell(PackagingHeader.HU_LENGTH.ordinal()), hu, PackagingDimension::getLength);
|
||||
mapToCell(row.createCell(PackagingHeader.HU_WEIGHT.ordinal()), hu, PackagingDimension::getWeight);
|
||||
mapDimensionToCell(row.createCell(PackagingHeader.HU_HEIGHT.ordinal()), hu, PackagingDimension::getHeight);
|
||||
mapDimensionToCell(row.createCell(PackagingHeader.HU_WIDTH.ordinal()), hu, PackagingDimension::getWidth);
|
||||
mapDimensionToCell(row.createCell(PackagingHeader.HU_LENGTH.ordinal()), hu, PackagingDimension::getLength);
|
||||
mapWeightToCell(row.createCell(PackagingHeader.HU_WEIGHT.ordinal()), hu, PackagingDimension::getWeight);
|
||||
mapToCell(row.createCell(PackagingHeader.HU_UNIT_COUNT.ordinal()), hu, PackagingDimension::getContentUnitCount);
|
||||
mapToCell(row.createCell(PackagingHeader.HU_DIMENSION_UNIT.ordinal()), hu, PackagingDimension::getDimensionUnit);
|
||||
mapToCell(row.createCell(PackagingHeader.HU_WEIGHT_UNIT.ordinal()), hu, PackagingDimension::getWeightUnit);
|
||||
|
|
@ -84,6 +85,18 @@ public class PackagingExcelMapper {
|
|||
|
||||
}
|
||||
|
||||
private void mapDimensionToCell(Cell cell, Optional<PackagingDimension> packaging, Function<PackagingDimension, Integer> provider) {
|
||||
if (packaging.isPresent())
|
||||
cell.setCellValue(packaging.get().getDimensionUnit().convertFromMM(provider.apply(packaging.get())));
|
||||
else cell.setBlank();
|
||||
}
|
||||
|
||||
private void mapWeightToCell(Cell cell, Optional<PackagingDimension> packaging, Function<PackagingDimension, Integer> provider) {
|
||||
if (packaging.isPresent())
|
||||
cell.setCellValue(packaging.get().getWeightUnit().convertFromG(provider.apply(packaging.get())));
|
||||
else cell.setBlank();
|
||||
}
|
||||
|
||||
private <T, R> void mapToCell(Cell cell, Optional<T> dimension, Function<T, R> resolver) {
|
||||
if (dimension.isPresent()) {
|
||||
if (resolver.apply(dimension.get()) instanceof Integer)
|
||||
|
|
@ -91,16 +104,23 @@ public class PackagingExcelMapper {
|
|||
if (resolver.apply(dimension.get()) instanceof String)
|
||||
cell.setCellValue((String) resolver.apply(dimension.get()));
|
||||
if (resolver.apply(dimension.get()) instanceof DimensionUnit)
|
||||
cell.setCellValue(((DimensionUnit) resolver.apply(dimension.get())).name());
|
||||
cell.setCellValue(((DimensionUnit) resolver.apply(dimension.get())).getDisplayedName());
|
||||
if (resolver.apply(dimension.get()) instanceof WeightUnit)
|
||||
cell.setCellValue(((WeightUnit) resolver.apply(dimension.get())).getDisplayedName());
|
||||
} else cell.setBlank();
|
||||
}
|
||||
|
||||
public void createConstraints(Workbook workbook, Sheet sheet) {
|
||||
var namedRange = constraintGenerator.createReference(workbook, HiddenNodeHeader.MAPPING_ID.ordinal(), HiddenTableType.ORIGIN_HIDDEN_TABLE);
|
||||
var namedRange = constraintGenerator.createReference(workbook, HiddenNodeHeader.MAPPING_ID.ordinal(), HiddenTableType.NODE_HIDDEN_TABLE);
|
||||
|
||||
constraintGenerator.createFormulaListConstraint(sheet, PackagingHeader.SUPPLIER.ordinal(), namedRange.getNameName());
|
||||
constraintGenerator.createLengthConstraint(sheet, PackagingHeader.PART_NUMBER.ordinal(), 0, 12);
|
||||
|
||||
constraintGenerator.createEnumConstraint(sheet, PackagingHeader.SHU_DIMENSION_UNIT.ordinal(), DimensionUnit.class);
|
||||
constraintGenerator.createEnumConstraint(sheet, PackagingHeader.SHU_WEIGHT_UNIT.ordinal(), WeightUnit.class);
|
||||
constraintGenerator.createEnumConstraint(sheet, PackagingHeader.HU_DIMENSION_UNIT.ordinal(), DimensionUnit.class);
|
||||
constraintGenerator.createEnumConstraint(sheet, PackagingHeader.HU_WEIGHT_UNIT.ordinal(), WeightUnit.class);
|
||||
|
||||
|
||||
//TODO: check hu dimensions...
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,12 @@ public class ExcelReportingService {
|
|||
Sheet sheet = workbook.createSheet("report");
|
||||
|
||||
CellStyle headerStyle = headerCellStyleProvider.createHeaderCellStyle(workbook);
|
||||
headerGenerator.generateHeader(sheet, reports.stream().map(ReportDTO::getSupplier).map(NodeDTO::getName).toArray(String[]::new), headerStyle);
|
||||
|
||||
ArrayList<String> headers = new ArrayList<>();
|
||||
headers.add("");
|
||||
headers.addAll(reports.stream().map(ReportDTO::getSupplier).map(NodeDTO::getName).toList());
|
||||
|
||||
headerGenerator.generateHeader(sheet, headers.toArray(String[]::new), headerStyle);
|
||||
|
||||
List<ReportFlattener> flatterers = reports.stream().map(ReportFlattener::new).toList();
|
||||
|
||||
|
|
@ -57,6 +62,7 @@ public class ExcelReportingService {
|
|||
|
||||
Cell cell = row.createCell(cellIdx);
|
||||
|
||||
|
||||
if(flattener.hasData(row.getRowNum())) {
|
||||
if(!headerWritten) {
|
||||
row.createCell(0).setCellValue(flattener.getHeader(row.getRowNum()));
|
||||
|
|
@ -72,6 +78,8 @@ public class ExcelReportingService {
|
|||
if(!hasData) break;
|
||||
}
|
||||
|
||||
headerGenerator.fixWidth(sheet, headers.toArray(String[]::new));
|
||||
|
||||
// Return the Excel file as an InputStreamSource
|
||||
workbook.write(outputStream);
|
||||
return new ByteArrayResource(outputStream.toByteArray());
|
||||
|
|
@ -116,8 +124,8 @@ public class ExcelReportingService {
|
|||
|
||||
|
||||
private final ReportDTO report;
|
||||
private List<String> data = new ArrayList<>();
|
||||
private List<String> dataHeader = new ArrayList<>();
|
||||
private final List<String> data = new ArrayList<>();
|
||||
private final List<String> dataHeader = new ArrayList<>();
|
||||
|
||||
public ReportFlattener(ReportDTO report) {
|
||||
this.report = report;
|
||||
|
|
@ -129,6 +137,7 @@ public class ExcelReportingService {
|
|||
addData(SUPPLIER_NAME, report.getSupplier().getName());
|
||||
addData(SUPPLIER_ADDRESS, report.getSupplier().getAddress());
|
||||
|
||||
// TODO: hardcoded (otherwise values wont match
|
||||
report.getCost().keySet().forEach(costName -> addData(costName, report.getCost().get(costName)));
|
||||
report.getRisk().keySet().forEach(riskName -> addData(riskName, report.getRisk().get(riskName)));
|
||||
|
||||
|
|
@ -143,6 +152,7 @@ public class ExcelReportingService {
|
|||
addData(DESTINATION_HS_CODE, destination.getHsCode());
|
||||
addData(DESTINATION_TARIFF_RATE, destination.getTariffRate().toString());
|
||||
addData(DESTINATION_OVERSHARE, destination.getOverseaShare().toString());
|
||||
if(destination.getAirFreightShare() != null)
|
||||
addData(DESTINATION_AIR_FREIGHT_SHARE, destination.getAirFreightShare().toString());
|
||||
addData(DESTINATION_TRANSPORT_TIME, destination.getTransportTime().toString());
|
||||
addData(DESTINATION_SAFETY_STOCK, destination.getSafetyStock().toString());
|
||||
|
|
@ -172,7 +182,7 @@ public class ExcelReportingService {
|
|||
|
||||
private void addData(String header, ReportEntryDTO data) {
|
||||
this.dataHeader.add(header);
|
||||
this.data.add(data.getTotal() + " (" + data.getPercentage().doubleValue()*100 + "%)");
|
||||
this.data.add(data.getTotal().toString()); // + " (" + data.getPercentage().doubleValue()*100 + "%)");
|
||||
}
|
||||
|
||||
public String getCell(int rowIdx) {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ public class MaterialTransformer {
|
|||
dtoEntry.setName(entity.getName());
|
||||
dtoEntry.setHsCode(entity.getHsCode());
|
||||
|
||||
dtoEntry.setDeprecated(entity.getDeprecated());
|
||||
|
||||
return dtoEntry;
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ public class PremiseTransformer {
|
|||
var dto = new PremiseDetailDTO();
|
||||
|
||||
dto.setId(entity.getId());
|
||||
dto.setMaterial(materialTransformer.toMaterialDTO(materialRepository.getById(entity.getMaterialId()).orElseThrow()));
|
||||
dto.setMaterial(materialTransformer.toMaterialDTO(materialRepository.getByIdIncludeDeprecated(entity.getMaterialId()).orElseThrow()));
|
||||
dto.setMixable(entity.getHuMixable());
|
||||
dto.setStackable(entity.getHuStackable());
|
||||
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ public class ReportTransformer {
|
|||
var weightedTotalCost = getWeightedTotalCosts(sections);
|
||||
|
||||
Premise premise = premiseRepository.getPremiseById(job.getPremiseId()).orElseThrow();
|
||||
reportDTO.setMaterial(materialTransformer.toMaterialDTO(materialRepository.getById(premise.getMaterialId()).orElseThrow()));
|
||||
reportDTO.setMaterial(materialTransformer.toMaterialDTO(materialRepository.getByIdIncludeDeprecated(premise.getMaterialId()).orElseThrow()));
|
||||
|
||||
var period = getPeriod(job);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue