- Introduced "devpage" to show calculation dumps and change active user in "dev"-profile

This commit is contained in:
Jan 2025-10-04 10:47:52 +02:00
parent 10687ffe5d
commit 4c34a1bade
28 changed files with 1071 additions and 208 deletions

View file

@ -1,12 +1,23 @@
import logger from "@/logger.js";
import {useErrorStore} from "@/store/error.js";
const getCsrfToken = () => {
const value = `; ${document.cookie}`;
const parts = value.split(`; XSRF-TOKEN=`);
if (parts.length === 2) {
return decodeURIComponent(parts.pop().split(';').shift());
}
return null;
}
const performRequest = async (requestingStore, method, url, body, expectResponse = true, expectedException = null) => {
const params = {
method: method,
credentials: 'include',
headers: {
'Content-Type': 'application/json'
'Content-Type': 'application/json',
'X-XSRF-TOKEN': getCsrfToken()
}
};
@ -27,6 +38,10 @@ const performDownload = async (url, toFile, expectResponse = true, expectedExcep
const params = {
method: 'GET',
credentials: 'include',
headers: {
'X-XSRF-TOKEN': getCsrfToken()
}
};
const request = {url: url, params: params, expectResponse: expectResponse, expectedException: expectedException, type: 'blob'};
@ -56,6 +71,10 @@ const performUpload = async (url, file, expectResponse = true, expectedException
const params = {
method: 'POST',
credentials: 'include',
headers: {
'X-XSRF-TOKEN': getCsrfToken()
},
body: formData
};

View file

@ -60,7 +60,6 @@ export default{
}
}
</script>
<style>
.checkbox-container {
@ -142,12 +141,17 @@ export default{
transform: rotate(45deg);
top: 1px;
left: 5px;
transition: border-color 0.3s ease;
}
.checkbox-item input:checked ~ .checkmark::after {
display: block;
}
.checkbox-item:not(.disabled):hover input:checked ~ .checkmark::after {
border-color: #8DB3FE;
}
.checkbox-label {
color: #002F54;
font-size: 1.4rem;

View file

@ -5,7 +5,8 @@
<box>
<div class="container">
<div>
<div class="button-container">
<basic-button variant="secondary" :show-icon="false" @click="$router.push({name: 'dev-page', state: {show: 'dumps'}})">Back</basic-button>
<basic-button variant="secondary" :show-icon="false" @click="expand = !expand">
{{ expand === true ? 'Collapse all items' : 'Expand all items' }}
</basic-button>
@ -34,7 +35,7 @@ export default {
components: {Box, BasicButton, ToggleSwitch, JsonTreeViewer},
async created() {
const resp = await performRequest(null, "GET", `${config.backendUrl}/error/dump/${this.$route.params.id}`, null);
const resp = await performRequest(null, "GET", `${config.backendUrl}/dev/dump/${this.$route.params.id}`, null);
this.dump = resp.data;
},
@ -49,6 +50,12 @@ export default {
</script>
<style scoped>
.button-container {
display: flex;
gap: 1rem;
}
.container {
display: flex;
flex-direction: column;

View file

@ -0,0 +1,95 @@
<template>
<div>
<table-view :columns="columns" :data-source="fetch" :page-size="pageSize" :page="pagination.page" :page-count="pagination.pageCount" :total-count="pagination.totalCount" @row-click="showDump" :mouse-over="true"></table-view>
</div>
</template>
<script>
import TableView from "@/components/UI/TableView.vue";
import performRequest from "@/backend.js";
import {config} from "@/config.js";
import Box from "@/components/UI/Box.vue";
import JsonTreeViewer from "@/components/UI/JsonTreeViewer.vue";
import BasicButton from "@/components/UI/BasicButton.vue";
import Modal from "@/components/UI/Modal.vue";
export default {
name: "CalculationDumpList",
components: {Modal, BasicButton, JsonTreeViewer, Box, TableView},
methods: {
showDump(dump) {
this.$router.push({name: 'dev-calculation-dump', params: {id: dump.id}});
},
async fetch(query) {
const params = new URLSearchParams();
if (query?.searchTerm && query.searchTerm !== '')
params.append('filter', query.searchTerm);
if(query?.periodId)
params.append('valid', query.periodId);
if(query?.page)
params.append('page', query.page);
if(query?.pageSize)
params.append('limit', query.pageSize);
const resp = await performRequest(null, "GET", `${config.backendUrl}/dev/dump/${params.size === 0 ? '' : '?'}${params.toString()}`, null);
this.dump = resp.data;
this.pagination = { page: parseInt(resp.headers.get('X-Current-Page')), pageCount: parseInt(resp.headers.get('X-Page-Count')), totalCount: parseInt(resp.headers.get('X-Total-Count'))};
return this.dump;
}
},
data() {
return {
dump: null,
pageSize: 20,
pagination: {page: 1, pageCount: 1, totalCount: 1},
columns: [
{
key: 'id',
label: 'ID',
},
{
key: 'calculation_date',
label: 'Calculation date',
},
{
key: 'job_state',
label: 'State',
},
{
key: 'user_id',
label: 'User ID',
},
{
key: 'premise.material.part_number',
label: 'Material',
},
{
key: 'premise.supplier.name',
label: 'Supplier',
},
{
key: 'premise.destinations',
label: 'Destinations',
formatter: (value) => {
return value.map(v => v.destination_node.name).join(', ');
}
},
],
}
}
}
</script>
<style scoped>
</style>

View file

@ -0,0 +1,123 @@
<template>
<div>
<box variant="border">
<div class="active-user"> Currently selected user: {{ activeUser }}</div>
</box>
<modal-dialog title="Do you want to change active user?" :state="showModal"
:message="`Do you want to set the active user to: ${userName}`" accept-text="Yes" dismiss-text="No"
@click="activateUser"></modal-dialog>
<table-view :columns="columns" :data-source="fetch" @row-click="selectUser" :mouse-over="true"></table-view>
</div>
</template>
<script>
import performRequest from "@/backend.js";
import {config} from "@/config.js";
import TableView from "@/components/UI/TableView.vue";
import ModalDialog from "@/components/UI/ModalDialog.vue";
import Box from "@/components/UI/Box.vue";
export default {
name: "DevUserControl",
components: {Box, ModalDialog, TableView},
created() {
this.getActiveUser();
},
methods: {
async fetch() {
const resp = await performRequest(null, "GET", `${config.backendUrl}/dev/user`, null);
this.users = resp.data;
return this.users;
},
selectUser(user) {
this.selectedUser = user;
this.showModal = true;
},
async activateUser(action) {
if (action === "accept") {
await performRequest(null, "POST", `${config.backendUrl}/dev/user`, this.selectedUser, false);
await this.getActiveUser();
}
this.showModal = false;
},
async getActiveUser() {
const resp = await performRequest(null, "GET", `${config.backendUrl}/active-user`, null);
this.activeUserData = resp.data;
}
},
computed: {
userName() {
return this.selectedUser?.firstname + " " + this.selectedUser?.lastname;
},
activeUser() {
return this.activeUserData?.firstname + " " + this.activeUserData?.lastname;
}
},
data() {
return {
showModal: false,
activeUserData: null,
selectedUser: null,
users: null,
columns: [
{
key: 'firstname',
label: 'First name',
},
{
key: 'lastname',
label: 'Last name',
},
{
key: 'mail',
label: 'E-Mail',
},
{
key: 'workday_id',
label: 'Workday ID',
},
{
key: 'groups',
label: 'User groups',
formatter: (value) => {
return value.join(", ")
}
},
{
key: 'is_active',
label: 'active',
formatter: (value) => {
return value ? 'Yes' : 'No';
}
},
],
pagination: {
page: 1,
pageCount: 1,
totalCount: 0
},
pageSize: 20
}
},
}
</script>
<style scoped>
.active-user {
font-size: 1.6rem
}
</style>

View file

@ -1,47 +1,46 @@
<template>
<div class="container" @focusout="focusLost">
<div class="container" :class="{ 'responsive': responsive }" @focusout="focusLost">
<div class="caption-column">Part number</div>
<div class="input-column">
<span>{{ partNumber }}</span>
<modal :state="modalSelectMaterial" @close="closeEditModal">
<select-material :part-number="partNumber" @close="modalEditClick"/>
</modal>
<icon-button icon="pencil-simple" @click="activateEditMode"></icon-button>
</div>
<div class="caption-column">Description</div>
<div class="input-column">
<span>{{ description }}</span>
</div>
<div class="caption-column">HS code</div>
<div class="input-column">
<div class="hs-code-container">
<autosuggest-searchbar :activate-watcher="true" :fetch-suggestions="fetchHsCode" :initial-value="hsCode"
placeholder="Find hs code" no-results-text="Not found."></autosuggest-searchbar>
<icon-button icon="ArrowCounterClockwise"></icon-button>
<div class="field-group">
<div class="caption-column">Part number</div>
<div class="input-column">
<span>{{ partNumber }}</span>
<modal :state="modalSelectMaterial" @close="closeEditModal">
<select-material :part-number="partNumber" @close="modalEditClick"/>
</modal>
<icon-button icon="pencil-simple" @click="activateEditMode"></icon-button>
</div>
</div>
<div class="field-group">
<div class="caption-column">Description</div>
<div class="input-column">
<span>{{ description }}</span>
</div>
</div>
<div class="field-group">
<div class="caption-column">HS code</div>
<div class="input-column">
<div class="hs-code-container">
<autosuggest-searchbar :activate-watcher="true" :fetch-suggestions="fetchHsCode" :initial-value="hsCode"
placeholder="Find hs code" no-results-text="Not found."></autosuggest-searchbar>
</div>
</div>
</div>
<div class="field-group">
<div class="caption-column">Tariff rate [%]</div>
<div class="text-container input-field-tariffrate">
<input ref="tariffRateInput" :value="tariffRatePercent" @blur="validateTariffRate"
class="input-field"
autocomplete="off"/>
<div class="input-column">
<div class="text-container input-field-tariffrate">
<input ref="tariffRateInput" :value="tariffRatePercent" @blur="validateTariffRate"
class="input-field"
autocomplete="off"/>
</div>
</div>
</div>
</div>
</template>
@ -88,6 +87,10 @@ export default {
openSelectDirect: {
type: Boolean,
default: false,
},
responsive: {
type: Boolean,
default: true,
}
},
computed: {
@ -164,16 +167,63 @@ export default {
display: flex;
align-items: center;
gap: 0.8rem;
width: 100%;
}
.container {
display: grid;
grid-template-columns: auto 1fr;
grid-template-rows: repeat(3, fit-content(0));
grid-template-rows: repeat(4, fit-content(0));
gap: 1.6rem;
flex: 1 1 auto;
}
/* Responsive Layout für Breiten unter 1500px - nur wenn responsive aktiviert ist */
@media (max-width: 1500px) {
.container.responsive {
grid-template-columns:
minmax(auto, max-content) /* Spalte 1: Label */
minmax(120px, 1fr) /* Spalte 2: Input */
minmax(auto, max-content) /* Spalte 3: Label */
minmax(120px, 1fr) /* Spalte 4: Input/Dropdown */
minmax(auto, max-content) /* Spalte 5: Label */
minmax(120px, 1fr) /* Spalte 6: Input */
minmax(auto, max-content) /* Spalte 7: Label */
minmax(120px, 1fr); /* Spalte 8: Input/Dropdown */
grid-template-rows: auto;
gap: 1.2rem 1rem;
align-items: center;
}
.container.responsive .field-group {
display: contents;
}
.container.responsive .caption-column {
justify-self: start;
}
.container.responsive .input-column {
min-width: 0;
}
.container.responsive .field-group {
display: contents;
}
.container.responsive .caption-column {
justify-self: start;
}
.container.responsive .input-column {
min-width: 0;
}
}
.field-group {
display: contents;
}
.input-column {
display: flex;
justify-content: space-between;
@ -201,7 +251,6 @@ export default {
background: white;
border-radius: 0.4rem;
padding: 0.6rem 1.2rem;
/* box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);*/
border: 0.2rem solid #E3EDFF;
transition: all 0.1s ease;
flex: 1 1 auto;
@ -210,7 +259,6 @@ export default {
.text-container:hover {
background: #EEF4FF;
border: 0.2rem solid #8DB3FE;
/*transform: translateY(2px);*/
transform: scale(1.01);
}
@ -224,8 +272,16 @@ export default {
}
.input-field-tariffrate {
min-width: 10rem;
min-width: 20rem;
flex: 0 1 10rem;
}
/* Optimierung für kleinere Bildschirme unter 1500px - nur wenn responsive aktiviert ist */
@media (max-width: 1500px) {
.container.responsive .input-field-tariffrate {
min-width: auto;
flex: 1 1 auto;
}
}
</style>

View file

@ -1,14 +1,16 @@
<template>
<div class="container" @focusout="focusLost">
<div class="container" :class="{ 'responsive': responsive }" @focusout="focusLost">
<div class="caption-column">Length</div>
<div class="input-column">
<div class="text-container">
<input ref="lengthInput" :value="huLength" @blur="validateDimension('length', $event)" class="input-field"
<input ref="lengthInput" :value="huLength" @blur="validateDimension('length', $event)"
@keydown.enter="handleEnter('lengthInput', $event)" class="input-field"
autocomplete="off"/>
</div>
</div>
<div class="caption-column">Dimension unit</div>
<div class="input-column">
<div class="caption-column dimension-unit-label">Dimension unit</div>
<div class="input-column dimension-unit-dropdown">
<dropdown :options="huDimensionUnits" v-model="huDimensionUnitSelected"></dropdown>
</div>
@ -16,7 +18,8 @@
<div class="caption-column">Width</div>
<div class="input-column">
<div class="text-container">
<input ref="widthInput" :value="huWidth" @blur="validateDimension('width', $event)" class="input-field"
<input ref="widthInput" :value="huWidth" @blur="validateDimension('width', $event)"
@keydown.enter="handleEnter('widthInput', $event)" class="input-field"
autocomplete="off"/>
</div>
</div>
@ -27,7 +30,8 @@
<div class="caption-column">Height</div>
<div class="input-column">
<div class="text-container">
<input ref="heightInput" :value="huHeight" @blur="validateDimension('height', $event)" class="input-field"
<input ref="heightInput" :value="huHeight" @blur="validateDimension('height', $event)"
@keydown.enter="handleEnter('heightInput', $event)" class="input-field"
autocomplete="off"/>
</div>
</div>
@ -38,7 +42,8 @@
<div class="caption-column">Weight</div>
<div class="input-column">
<div class="text-container">
<input ref="weightInput" :value="huWeight" @blur="validateWeight('weight', $event)" class="input-field"
<input ref="weightInput" :value="huWeight" @blur="validateWeight('weight', $event)"
@keydown.enter="handleEnter('weightInput', $event)" class="input-field"
autocomplete="off"/>
</div>
</div>
@ -50,7 +55,8 @@
<div class="caption-column">Pieces per HU</div>
<div class="input-column">
<div class="text-container">
<input ref="unitCountInput" :value="huUnitCount" @blur="validateCount" class="input-field"
<input ref="unitCountInput" :value="huUnitCount" @blur="validateCount"
@keydown.enter="handleEnter('unitCountInput', $event)" class="input-field"
autocomplete="off"/>
</div>
</div>
@ -115,6 +121,10 @@ export default {
stackable: {
type: Boolean,
required: true,
},
responsive: {
type: Boolean,
default: true,
}
},
computed: {
@ -153,14 +163,14 @@ export default {
const unitType = this.huDimensionUnits.find(unit => unit.id === value)?.value;
const decimals = (unitType === 'cm') ? 2 : ((unitType === 'm') ? 3 : 0);
const parsedLength = (this.length ?? null) === null ? null : parseFloat(this.length.toFixed(decimals));
this.$emit('update:length', parsedLength);
const parsedLength = (this.length ?? null) === null ? null : parseFloat(this.length.toFixed(decimals));
this.$emit('update:length', parsedLength);
const parsedHeight = (this.height ?? null) === null ? null : parseFloat(this.height.toFixed(decimals));
this.$emit('update:height', parsedHeight);
const parsedHeight = (this.height ?? null) === null ? null : parseFloat(this.height.toFixed(decimals));
this.$emit('update:height', parsedHeight);
const parsedWidth = (this.width ?? null) === null ? null : parseFloat(this.width.toFixed(decimals));
this.$emit('update:width', parsedWidth);
const parsedWidth = (this.width ?? null) === null ? null : parseFloat(this.width.toFixed(decimals));
this.$emit('update:width', parsedWidth);
this.$emit('update:dimensionUnit', unitType);
@ -187,6 +197,23 @@ export default {
}
},
methods: {
handleEnter(currentRef, event) {
event.preventDefault();
// Define the navigation order
const inputOrder = ['lengthInput', 'widthInput', 'heightInput', 'weightInput', 'unitCountInput'];
const currentIndex = inputOrder.indexOf(currentRef);
if (currentIndex !== -1 && currentIndex < inputOrder.length - 1) {
const nextRef = inputOrder[currentIndex + 1];
this.$nextTick(() => {
if (this.$refs[nextRef]) {
this.$refs[nextRef].focus();
this.$refs[nextRef].select();
}
});
}
},
focusLost(event) {
if (!this.$el.contains(event.relatedTarget)) {
this.$emit('save', 'packaging');
@ -252,10 +279,127 @@ export default {
<style scoped>
.container {
display: grid;
grid-template-columns: auto 1fr auto auto;
grid-template-columns: auto auto auto auto;
grid-template-rows: repeat(5, fit-content(0));
gap: 1.2rem 1.6rem;
flex: 1 1 auto;
flex: 0 0 auto;
width: 100%;
}
/* Responsive Layout für Breiten unter 1500px - nur wenn responsive aktiviert ist */
@media (max-width: 1500px) {
.container.responsive {
grid-template-columns:
minmax(auto, max-content) /* Spalte 1: Label */
minmax(120px, 1fr) /* Spalte 2: Input */
minmax(auto, max-content) /* Spalte 3: Label */
minmax(120px, 1fr) /* Spalte 4: Input/Dropdown */
minmax(auto, max-content) /* Spalte 5: Label */
minmax(120px, 1fr) /* Spalte 6: Input */
minmax(auto, max-content) /* Spalte 7: Label */
minmax(120px, 1fr); /* Spalte 8: Input/Dropdown */
grid-template-rows: auto;
gap: 1.2rem 1rem;
align-items: center;
}
.container.responsive .field-group {
display: contents;
}
.container.responsive .caption-column {
justify-self: start;
}
.container.responsive .input-column {
min-width: 0;
}
/* First row: Length, Dimension unit, Weight, Weight unit */
.container.responsive > .caption-column:nth-child(1) { /* Length label */
grid-column: 1;
grid-row: 1;
}
.container.responsive > .input-column:nth-child(2) { /* Length input */
grid-column: 2;
grid-row: 1;
}
.container.responsive > .caption-column:nth-child(3) { /* Dimension unit label */
grid-column: 3;
grid-row: 1;
}
.container.responsive > .input-column:nth-child(4) { /* Dimension unit dropdown */
grid-column: 4;
grid-row: 1;
}
.container.responsive > .caption-column:nth-child(13) { /* Weight label */
grid-column: 5;
grid-row: 1;
}
.container.responsive > .input-column:nth-child(14) { /* Weight input */
grid-column: 6;
grid-row: 1;
}
.container.responsive > .caption-column:nth-child(15) { /* Weight unit label */
grid-column: 7;
grid-row: 1;
}
.container.responsive > .input-column:nth-child(16) { /* Weight unit dropdown */
grid-column: 8;
grid-row: 1;
}
/* Second row: Width, Pieces per HU */
.container.responsive > .caption-column:nth-child(5) { /* Width label */
grid-column: 1;
grid-row: 2;
}
.container.responsive > .input-column:nth-child(6) { /* Width input */
grid-column: 2;
grid-row: 2;
}
.container.responsive > .caption-column:nth-child(17) { /* Pieces per HU label */
grid-column: 5;
grid-row: 2;
}
.container.responsive > .input-column:nth-child(18) { /* Pieces per HU input */
grid-column: 6;
grid-row: 2;
}
/* Third row: Height, Checkboxes */
.container.responsive > .caption-column:nth-child(9) { /* Height label */
grid-column: 1;
grid-row: 3;
}
.container.responsive > .input-column:nth-child(10) { /* Height input */
grid-column: 2;
grid-row: 3;
}
.container.responsive .input-column-chk {
grid-column: 7 / -1;
grid-row: 3;
justify-content: flex-start;
}
/* Hide empty caption and input columns */
.container.responsive > .caption-column:nth-child(7),
.container.responsive > .input-column:nth-child(8),
.container.responsive > .caption-column:nth-child(11),
.container.responsive > .input-column:nth-child(12) {
display: none;
}
}
.input-field {
@ -268,7 +412,6 @@ export default {
color: #002F54;
width: 100%;
min-width: 5rem;
max-width: 10rem;
}
.input-column-chk {
@ -288,6 +431,7 @@ export default {
font-size: 1.4rem;
font-weight: 300;
color: #6b7280;
flex: 1 1 auto;
}
.text-container {
@ -296,17 +440,14 @@ export default {
background: white;
border-radius: 0.4rem;
padding: 0.6rem 1.2rem;
/* box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);*/
border: 0.2rem solid #E3EDFF;
transition: all 0.1s ease;
flex: 0 1 min(30rem, 100%);
flex: 1 1 auto
}
.text-container:hover {
background: #EEF4FF;
border: 0.2rem solid #8DB3FE;
/*transform: translateY(2px);*/
transform: scale(1.01);
}

View file

@ -1,25 +1,33 @@
<template>
<div @focusout="focusLost">
<div class="container">
<div class="caption-column">MEK_A [EUR]</div>
<div class="input-column">
<div class="text-container">
<input :value="priceFormatted" @blur="validatePrice" class="input-field"
autocomplete="off"/>
<div @focusout="focusLost">
<div class="container" :class="{ 'responsive': responsive }">
<div class="field-group">
<div class="caption-column">MEK_A [EUR]</div>
<div class="input-column">
<div class="text-container">
<input :value="priceFormatted" @blur="validatePrice" class="input-field"
autocomplete="off"/>
</div>
</div>
</div>
<div class="caption-column">Oversea share [%]</div>
<div class="input-column">
<div class="text-container">
<input :value="overSeaSharePercent" @blur="validateOverSeaShare" class="input-field"
autocomplete="off"/>
<div class="field-group">
<div class="caption-column">Oversea share [%]</div>
<div class="input-column">
<div class="text-container">
<input :value="overSeaSharePercent" @blur="validateOverSeaShare" class="input-field"
autocomplete="off"/>
</div>
</div>
</div>
<div class="caption-column">Include FCA Fee</div>
<div class="input-column">
<tooltip text="Select if a additional FCA has to be added during calculation">
<checkbox :checked="includeFcaFee" @checkbox-changed="updateIncludeFcaFee"></checkbox>
</tooltip>
<div class="field-group">
<div class="caption-column">Include FCA Fee</div>
<div class="input-column">
<tooltip text="Select if a additional FCA has to be added during calculation">
<checkbox :checked="includeFcaFee" @checkbox-changed="updateIncludeFcaFee"></checkbox>
</tooltip>
</div>
</div>
</div>
@ -49,6 +57,10 @@ export default {
includeFcaFee: {
type: Boolean,
required: true,
},
responsive: {
type: Boolean,
default: true,
}
},
computed: {
@ -109,7 +121,39 @@ export default {
flex: 1 1 auto;
}
/* Responsive Layout für Breiten unter 1500px - nur wenn responsive aktiviert ist */
@media (max-width: 1500px) {
.container.responsive {
grid-template-columns:
minmax(auto, max-content) /* Spalte 1: Label */
minmax(120px, 1fr) /* Spalte 2: Input */
minmax(auto, max-content) /* Spalte 3: Label */
minmax(120px, 1fr) /* Spalte 4: Input/Dropdown */
minmax(auto, max-content) /* Spalte 5: Label */
minmax(120px, 1fr) /* Spalte 6: Input */
minmax(auto, max-content) /* Spalte 7: Label */
minmax(120px, 1fr); /* Spalte 8: Input/Dropdown */
grid-template-rows: auto;
gap: 1.2rem 1rem;
align-items: center;
}
.container.responsive .field-group {
display: contents;
}
.container.responsive .caption-column {
justify-self: start;
}
.container.responsive .input-column {
min-width: 0;
}
}
.field-group {
display: contents;
}
.input-column {
display: flex;
@ -139,16 +183,14 @@ export default {
background: white;
border-radius: 0.4rem;
padding: 0.6rem 1.2rem;
/* box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);*/
border: 0.2rem solid #E3EDFF;
transition: all 0.1s ease;
flex: 1 1 fit-content(80rem);
flex: 1 1 auto
}
.text-container:hover {
background: #EEF4FF;
border: 0.2rem solid #8DB3FE;
/*transform: translateY(2px);*/
transform: scale(1.01);
}

View file

@ -176,7 +176,7 @@ export default {
.container {
display: flex;
flex-direction: column;
flex-direction: row;
gap: 1.6rem;
flex: 1 1 auto;
}

View file

@ -40,7 +40,7 @@
<div v-if="showRoutes" key="routes" class="destination-edit-cell-routes">
<destination-route v-for="route in destination.routes" :key="route.id" :route="route"
:selected="route.is_selected" @click="selectRoute(route.id)"></destination-route>
:selected="route.is_selected" @click="selectRoute(route.id)" :responsive="false"></destination-route>
</div>
<div v-else-if="showRouteWarning">
<div class="destination-edit-route-warning">

View file

@ -5,9 +5,11 @@
<ph-train :size="18" v-else-if="isRail" class="destination-route-icon"></ph-train>
<ph-truck :size="18" v-else-if="isRoad" class="destination-route-icon"></ph-truck>
<ph-navigation-arrow :size="18" v-else class="destination-route-icon"></ph-navigation-arrow>
<div><span v-for="element in routeElements" class="destination-route-element"> {{ element }} </span></div>
<basic-badge v-if="cheapest" variant="secondary">CHEAPEST</basic-badge>
<basic-badge v-if="fastest" variant="primary">FASTEST</basic-badge>
<div :class="{ 'route-details': true, 'no-responsive': !responsive }">
<span v-for="element in routeElements" class="destination-route-element"> {{ element }} </span>
</div>
<!-- <basic-badge v-if="cheapest" variant="secondary">CHEAPEST</basic-badge>-->
<!-- <basic-badge v-if="fastest" variant="primary">FASTEST</basic-badge>-->
</div>
</div>
@ -44,6 +46,11 @@ export default {
type: Boolean,
required: false,
default: false
},
responsive: {
type: Boolean,
required: false,
default: true
}
},
computed: {
@ -108,7 +115,7 @@ export default {
}
.destination-route-inner-container--selected {
background: #ffffff;
background: #EEF4FF;
border: 0.2rem solid #8DB3FE;
}
@ -140,7 +147,7 @@ export default {
margin-right: 0;
}
.destination-route-inner-container > div {
.destination-route-inner-container > div:not(.no-responsive) {
display: none;
}
@ -149,4 +156,13 @@ export default {
}
}
/* When responsive is disabled, always show content */
.no-responsive {
display: block !important;
}
.no-responsive ~ .destination-route-icon {
margin-right: 0.8rem !important;
}
</style>

View file

@ -86,6 +86,7 @@
v-model:stackable="componentProps.stackable"
v-model:preSelectedNode="componentProps.preSelectedNode"
v-model:openSelectDirect="componentProps.openSelectDirect"
:responsive="false"
@update-material="updateMaterial"
@update-supplier="updateSupplier"
@close="closeEditModalAction('cancel')"
@ -318,7 +319,7 @@ export default {
if (id === -1) {
// clear
this.componentsData = {
price: {props: {price: 0, overSeaShare: 0.0, includeFcaFee: true}},
price: {props: {price: 0, overSeaShare: 0.0, includeFcaFee: false}},
material: {props: {partNumber: "", hsCode: "", tariffRate: 0.00, description: "", openSelectDirect: true}},
packaging: {
props: {

View file

@ -28,28 +28,20 @@
</div>
<div v-else>
<h3 class="sub-header">Master data</h3>
<div class="master-data-container">
<box class="master-data-item master-data-stretched-item">
<h3 class="sub-header">Supplier</h3>
<div class="supplier-container">
<box class="supplier-item">
<supplier-view :supplier-address="premise.supplier.address"
:supplier-name="premise.supplier.name"
:supplier-coordinates="premise.supplier.location"
:iso-code="premise.supplier.country.iso_code"
@update-supplier="updateSupplier"
></supplier-view>
</box>
<box class="master-data-item master-data-stretched-item master-data-packaging">
<packaging-edit v-model:length="premise.handling_unit.length"
v-model:width="premise.handling_unit.width"
v-model:height="premise.handling_unit.height"
v-model:weight="premise.handling_unit.weight"
v-model:weight-unit="premise.handling_unit.weight_unit"
v-model:dimension-unit="premise.handling_unit.dimension_unit"
v-model:unit-count="premise.handling_unit.content_unit_count"
v-model:stackable="premise.is_stackable"
v-model:mixable="premise.is_mixable"
@save="save"></packaging-edit>
></supplier-view>
</box>
</div>
<h3 class="sub-header">Master data</h3>
<div class="master-data-container">
<box class="master-data-item">
<material-edit :part-number="premise.material.part_number"
:description="premise.material.name"
@ -65,7 +57,18 @@
v-model:price="premise.material_cost"
@save="save"></price-edit>
</box>
<box class="master-data-item">
<packaging-edit v-model:length="premise.handling_unit.length"
v-model:width="premise.handling_unit.width"
v-model:height="premise.handling_unit.height"
v-model:weight="premise.handling_unit.weight"
v-model:weight-unit="premise.handling_unit.weight_unit"
v-model:dimension-unit="premise.handling_unit.dimension_unit"
v-model:unit-count="premise.handling_unit.content_unit_count"
v-model:stackable="premise.is_stackable"
v-model:mixable="premise.is_mixable"
@save="save"></packaging-edit>
</box>
</div>
<h3 class="sub-header">Destinations & routes</h3>
@ -226,41 +229,29 @@ export default {
flex-wrap: wrap;
}
.supplier-container {
margin: 2.4rem 0;
}
.supplier-item {
width: 100%;
}
.master-data-container {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: auto auto;
grid-template-columns: 4fr 4fr 5fr;
gap: 1.6rem;
margin: 2.4rem 0;
}
.master-data-stretched-item {
grid-row: span 2 / span 2;
}
.master-data-packaging {
grid-column-start: 3;
grid-row-start: 1;
.master-data-item {
padding: 2.4rem;
}
/* Responsive layout for viewports below 1500px */
@media (max-width: 1500px) {
.master-data-container {
grid-template-columns: 1fr;
grid-template-rows: auto;
}
.master-data-stretched-item {
grid-row: auto;
}
.master-data-packaging {
grid-column-start: auto;
grid-row-start: auto;
}
.master-data-item {
padding: 2.4rem;
}
}

View file

@ -0,0 +1,65 @@
<template>
<div class="edit-calculation-container">
<div class="header-container">
<h2 class="page-header">DevPage</h2>
<div>
<box class="box-container">
<tab-container ref="refTab" :tabs="tabsConfig" class="tab-container" @tab-changed="handleTabChange">
</tab-container>
</box>
</div>
</div>
</div>
</template>
<script>
import Box from "@/components/UI/Box.vue";
import TabContainer from "@/components/UI/TabContainer.vue";
import {markRaw} from "vue";
import CalculationDumpList from "@/components/layout/dev/CalculationDumpList.vue";
import DevUserControl from "@/components/layout/dev/DevUserControl.vue";
export default {
name: "DevPage",
components: {TabContainer, Box},
mounted() {
if(history.state.show === 'dumps') {
this.$refs.refTab.setActiveTab(1);
}
},
data() {
return {
currentTab: null,
tabsConfig: [
{
title: 'User control',
component: markRaw(DevUserControl),
props: {isSelected: false},
},
{
title: 'Calculation dump',
component: markRaw(CalculationDumpList),
props: {isSelected: false},
}
]
}
},
methods: {
handleTabChange(eventData) {
console.log("handleTabChange")
const { index, tab } = eventData;
console.log(`Tab ${index} activated:`, tab.title);
this.tabsConfig.forEach(t => t.props.isSelected = t.title === tab.title);
}
}
}
</script>
<style scoped>
</style>

View file

@ -6,7 +6,8 @@ import CalculationSingleEdit from "@/pages/CalculationSingleEdit.vue";
import CalculationMassEdit from "@/pages/CalculationMassEdit.vue";
import CalculationAssistant from "@/pages/CalculationAssistant.vue";
import ErrorLog from "@/pages/ErrorLog.vue";
import CalcualtionDump from "@/pages/CalcualtionDump.vue";
import CalculationDump from "@/components/layout/dev/CalculationDump.vue";
import DevPage from "@/pages/DevPage.vue";
const router = createRouter({
history: createWebHistory(),
@ -56,10 +57,16 @@ const router = createRouter({
},
{
path: '/error/dump/:id',
component: CalcualtionDump
path: '/dev/dump/:id',
component: CalculationDump,
name: 'dev-calculation-dump'
},
{
path: '/dev',
component: DevPage,
name: 'dev-page',
},
{
path: '/:pathMatch(.*)*',
redirect: '/calculations'

View file

@ -43,7 +43,7 @@ public class CorsConfig implements WebMvcConfigurer {
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.exposedHeaders("X-Total-Count", "X-Page-Count", "X-Current-Page")
.allowCredentials(false);
.allowCredentials(true);
} else {
// Production CORS configuration
registry.addMapping("/api/**")

View file

@ -3,22 +3,34 @@ package de.avatic.lcc.config;
import de.avatic.lcc.model.users.User;
import de.avatic.lcc.repositories.users.GroupRepository;
import de.avatic.lcc.repositories.users.UserRepository;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jetbrains.annotations.NotNull;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.security.web.csrf.CsrfTokenRequestHandler;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Supplier;
@Configuration
public class SecurityConfig {
@ -28,11 +40,18 @@ public class SecurityConfig {
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/**").authenticated()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.defaultSuccessUrl("/", true)
);
)
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(new LccCsrfTokenRequestHandler())
)
.addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class);
return http.build();
}
@ -42,7 +61,11 @@ public class SecurityConfig {
public SecurityFilterChain devSecurityFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
.csrf(AbstractHttpConfigurer::disable)
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(new LccCsrfTokenRequestHandler())
)
.addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class)
.build();
}
@ -55,7 +78,7 @@ public class SecurityConfig {
OidcUser oidcUser = delegate.loadUser(userRequest);
Integer userId = null;
// // Debug: Print all claims
// Debug: Print all claims
// System.out.println("=== ID Token Claims ===");
// oidcUser.getIdToken().getClaims().forEach((key, value) ->
// System.out.println(key + ": " + value)
@ -64,6 +87,15 @@ public class SecurityConfig {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>(oidcUser.getAuthorities());
String workdayId = oidcUser.getAttribute("workday_id");
if (workdayId != null) {
User user = userRepository.getByWorkdayId(workdayId);
if (user != null) {
user.getGroups().forEach(group -> mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + group.getName())));
userId = user.getId();
}
}
// Try different ways to get email
String email = oidcUser.getEmail();
if (email == null) {
@ -79,10 +111,10 @@ public class SecurityConfig {
if (email != null) {
User user = userRepository.getByEmail(email);
if (user != null) {
user.getGroups().forEach(group -> mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + group.getName())));
user.getGroups().forEach(group -> mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + group.getName().toUpperCase())));
userId = user.getId();
} else {
mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_default"));
mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_BASIC"));
}
}
@ -95,4 +127,35 @@ public class SecurityConfig {
);
};
}
public static final class LccCsrfTokenRequestHandler extends CsrfTokenRequestAttributeHandler {
private final CsrfTokenRequestHandler delegate = new CsrfTokenRequestAttributeHandler();
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
Supplier<CsrfToken> csrfToken) {
this.delegate.handle(request, response, csrfToken);
}
@Override
public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
if (StringUtils.hasText(request.getHeader(csrfToken.getHeaderName()))) {
return super.resolveCsrfTokenValue(request, csrfToken);
}
return this.delegate.resolveCsrfTokenValue(request, csrfToken);
}
}
public static final class CsrfCookieFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response,
@NotNull FilterChain filterChain) throws ServletException, IOException {
CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
if (csrfToken != null) {
csrfToken.getToken();
}
filterChain.doFilter(request, response);
}
}
}

View file

@ -18,11 +18,9 @@ import java.util.Optional;
public class ErrorController {
private final SysErrorService sysErrorService;
private final DumpRepository dumpRepository;
public ErrorController(SysErrorService sysErrorService, DumpRepository dumpRepository) {
public ErrorController(SysErrorService sysErrorService) {
this.sysErrorService = sysErrorService;
this.dumpRepository = dumpRepository;
}
@PostMapping
@ -43,9 +41,6 @@ public class ErrorController {
.body(errors.toList());
}
@GetMapping({"/dump/{id}", "/dump/{id}/"})
public ResponseEntity<CalculationJobDumpDTO> getDump(@PathVariable Integer id) {
return ResponseEntity.ok(dumpRepository.getDump(id));
}
}

View file

@ -0,0 +1,76 @@
package de.avatic.lcc.controller.dev;
import com.azure.core.annotation.BodyParam;
import de.avatic.lcc.dto.error.CalculationJobDumpDTO;
import de.avatic.lcc.dto.users.UserDTO;
import de.avatic.lcc.repositories.error.DumpRepository;
import de.avatic.lcc.repositories.pagination.SearchQueryPagination;
import de.avatic.lcc.repositories.pagination.SearchQueryResult;
import de.avatic.lcc.repositories.users.UserRepository;
import de.avatic.lcc.service.transformer.users.UserTransformer;
import de.avatic.lcc.service.users.authorization.AuthorizationService;
import jakarta.validation.constraints.Min;
import org.springframework.context.annotation.Profile;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@Profile("dev")
@RequestMapping({"/api/dev", "/api/dev/"})
public class DevController {
private final AuthorizationService authorizationService;
private final DumpRepository dumpRepository;
private final UserRepository userRepository;
private final UserTransformer userTransformer;
public DevController(AuthorizationService authorizationService, DumpRepository dumpRepository, UserRepository userRepository, UserTransformer userTransformer) {
this.authorizationService = authorizationService;
this.dumpRepository = dumpRepository;
this.userRepository = userRepository;
this.userTransformer = userTransformer;
}
@GetMapping({"/dump/{id}", "/dump/{id}/"})
public ResponseEntity<CalculationJobDumpDTO> getDump(@PathVariable Integer id) {
return ResponseEntity.ok(dumpRepository.getDump(id));
}
@GetMapping({"/dump/", "/dump"})
public ResponseEntity<List<CalculationJobDumpDTO>> listDumps(
@RequestParam(defaultValue = "20") @Min(1) int limit,
@RequestParam(defaultValue = "1") @Min(1) int page) {
var dump = dumpRepository.listDumps(new SearchQueryPagination(page, limit));
return ResponseEntity.ok()
.header("X-Total-Count", String.valueOf(dump.getTotalElements()))
.header("X-Page-Count", String.valueOf(dump.getTotalPages()))
.header("X-Current-Page", String.valueOf(page))
.body(dump.toList());
}
@GetMapping({"/user"})
public ResponseEntity<List<UserDTO>> listUser(@RequestParam(defaultValue = "20") @Min(1) int limit,
@RequestParam(defaultValue = "1") @Min(1) int page) {
var users = SearchQueryResult.map(userRepository.listUsers(new SearchQueryPagination(page, limit)), userTransformer::toUserDTO);
return ResponseEntity.ok()
.header("X-Total-Count", String.valueOf(users.getTotalElements()))
.header("X-Page-Count", String.valueOf(users.getTotalPages()))
.header("X-Current-Page", String.valueOf(page))
.body(users.toList());
}
@PostMapping({"/user", "/user/"})
public ResponseEntity<Void> setActiveUser(@RequestBody UserDTO user) {
authorizationService.setActiveUser(user.getEmail());
return ResponseEntity.ok().build();
}
}

View file

@ -0,0 +1,29 @@
package de.avatic.lcc.controller.users;
import de.avatic.lcc.dto.users.UserDTO;
import de.avatic.lcc.service.transformer.users.UserTransformer;
import de.avatic.lcc.service.users.authorization.AuthorizationService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping({"/api/active-user", "/api/active-user/"})
public class ActiveUserController {
private final AuthorizationService authorizationService;
private final UserTransformer userTransformer;
public ActiveUserController(AuthorizationService authorizationService, UserTransformer userTransformer) {
this.authorizationService = authorizationService;
this.userTransformer = userTransformer;
}
@GetMapping
public ResponseEntity<UserDTO> getActiveUser() {
return ResponseEntity.ok(userTransformer.toUserDTO(authorizationService.getActiveUser()));
}
}

View file

@ -6,9 +6,12 @@ import de.avatic.lcc.dto.error.CalculationJobRouteSectionDumpDTO;
import de.avatic.lcc.dto.error.ErrorLogDTO;
import de.avatic.lcc.dto.error.ErrorLogTraceItemDto;
import de.avatic.lcc.dto.calculation.edit.PremiseDetailDTO;
import de.avatic.lcc.repositories.pagination.SearchQueryPagination;
import de.avatic.lcc.repositories.pagination.SearchQueryResult;
import de.avatic.lcc.repositories.premise.PremiseRepository;
import de.avatic.lcc.service.transformer.premise.PremiseTransformer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
@ -52,29 +55,7 @@ public class DumpRepository {
CalculationJobDumpDTO dump = namedParameterJdbcTemplate.queryForObject(
calculationJobQuery,
params,
(rs, rowNum) -> {
CalculationJobDumpDTO dto = new CalculationJobDumpDTO();
dto.setId(rs.getInt("id"));
dto.setPremiseId(rs.getInt("premise_id"));
Timestamp calculationDate = rs.getTimestamp("calculation_date");
if (calculationDate != null) {
dto.setCalculationDate(calculationDate.toString());
}
dto.setValidityPeriodId(rs.getInt("validity_period_id"));
dto.setPropertySetId(rs.getInt("property_set_id"));
dto.setJobState(rs.getString("job_state"));
dto.setUserId(rs.getInt("user_id"));
// Check if there's an error_id
Integer errorId = rs.getObject("error_id", Integer.class);
if (errorId != null) {
dto.setErrorLog(loadErrorLog(errorId));
}
return dto;
});
new CalculationMapper());
// Load premise details
dump.setPremise(loadPremiseDetails(dump.getPremiseId()));
@ -229,28 +210,7 @@ public class DumpRepository {
CalculationJobDumpDTO dump = jdbcTemplate.queryForObject(
calculationJobQuery,
(rs, rowNum) -> {
CalculationJobDumpDTO dto = new CalculationJobDumpDTO();
dto.setId(rs.getInt("id"));
dto.setPremiseId(rs.getInt("premise_id"));
Timestamp calculationDate = rs.getTimestamp("calculation_date");
if (calculationDate != null) {
dto.setCalculationDate(calculationDate.toString());
}
dto.setValidityPeriodId(rs.getInt("validity_period_id"));
dto.setPropertySetId(rs.getInt("property_set_id"));
dto.setJobState(rs.getString("job_state"));
dto.setUserId(rs.getInt("user_id"));
Integer errorId = rs.getObject("error_id", Integer.class);
if (errorId != null) {
dto.setErrorLog(loadErrorLogAlternative(errorId));
}
return dto;
},
new CalculationMapper(),
id // Parameter passed directly without Object array
);
@ -307,6 +267,92 @@ public class DumpRepository {
);
}
public SearchQueryResult<CalculationJobDumpDTO> listDumps(SearchQueryPagination searchQueryPagination) {
String calculationJobQuery = """
SELECT cj.id, cj.premise_id, cj.calculation_date, cj.validity_period_id,
cj.property_set_id, cj.job_state, cj.error_id, cj.user_id
FROM calculation_job cj
ORDER BY id DESC LIMIT :limit OFFSET :offset
""";
MapSqlParameterSource params = new MapSqlParameterSource();
params.addValue("offset", searchQueryPagination.getOffset());
params.addValue("limit", searchQueryPagination.getLimit());
var dumps = namedParameterJdbcTemplate.query(
calculationJobQuery,
params,
(rs, _) -> {
CalculationJobDumpDTO dto = new CalculationJobDumpDTO();
dto.setId(rs.getInt("id"));
dto.setPremiseId(rs.getInt("premise_id"));
Timestamp calculationDate = rs.getTimestamp("calculation_date");
if (calculationDate != null) {
dto.setCalculationDate(calculationDate.toString());
}
dto.setValidityPeriodId(rs.getInt("validity_period_id"));
dto.setPropertySetId(rs.getInt("property_set_id"));
dto.setJobState(rs.getString("job_state"));
dto.setUserId(rs.getInt("user_id"));
// Check if there's an error_id
Integer errorId = rs.getObject("error_id", Integer.class);
if (errorId != null) {
dto.setErrorLog(loadErrorLog(errorId));
}
return dto;
});
for(var dump : dumps) {
// Load premise details
dump.setPremise(loadPremiseDetails(dump.getPremiseId()));
// Load destinations
dump.setDestinations(loadCalculationJobDestinations(dump.getId()));
}
return new SearchQueryResult<>(dumps, searchQueryPagination.getPage(),countCalculations(), searchQueryPagination.getLimit());
}
private class CalculationMapper implements RowMapper<CalculationJobDumpDTO> {
@Override
public CalculationJobDumpDTO mapRow(ResultSet rs, int rowNum) throws SQLException {
CalculationJobDumpDTO dto = new CalculationJobDumpDTO();
dto.setId(rs.getInt("id"));
dto.setPremiseId(rs.getInt("premise_id"));
Timestamp calculationDate = rs.getTimestamp("calculation_date");
if (calculationDate != null) {
dto.setCalculationDate(calculationDate.toString());
}
dto.setValidityPeriodId(rs.getInt("validity_period_id"));
dto.setPropertySetId(rs.getInt("property_set_id"));
dto.setJobState(rs.getString("job_state"));
dto.setUserId(rs.getInt("user_id"));
// Check if there's an error_id
Integer errorId = rs.getObject("error_id", Integer.class);
if (errorId != null) {
dto.setErrorLog(loadErrorLog(errorId));
}
return dto;
}
}
private Integer countCalculations() {
return jdbcTemplate.queryForObject("SELECT COUNT(*) FROM calculation_job", Integer.class);
}
private static class CalculationJobDestinationRowMapper implements RowMapper<CalculationJobDestinationDumpDTO> {
@Override
public CalculationJobDestinationDumpDTO mapRow(ResultSet rs, int rowNum) throws SQLException {

View file

@ -63,7 +63,7 @@ public class UserRepository {
@Transactional
public void update(User user) {
Integer userId = findUserId(user.getWorkdayId());
Integer userId = getUserIdByWorkdayId(user.getWorkdayId());
List<Integer> groupIds = findGroupIds(user.getGroups().stream().map(Group::getName).toList());
@ -141,7 +141,8 @@ public class UserRepository {
}
private Integer findUserId(String workdayId) {
@Transactional
public Integer getUserIdByWorkdayId(String workdayId) {
List<Integer> results = jdbcTemplate.query("SELECT id FROM sys_user WHERE workday_id = ?",
(rs, rowNum) -> rs.getInt("id"),
workdayId);
@ -149,6 +150,15 @@ public class UserRepository {
return results.isEmpty() ? null : results.getFirst();
}
@Transactional
public User getByWorkdayId(String workdayId) {
List<User> results = jdbcTemplate.query("SELECT id FROM sys_user WHERE workday_id = ?",
new UserMapper(),
workdayId);
return results.isEmpty() ? null : results.getFirst();
}
@Transactional
public User getById(Integer id) {
String query = """

View file

@ -88,12 +88,6 @@ public class PremisesService {
//TODO use actual user.
userId = 1;
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//todo make a service. and simulate user rights in dev profile.
if (authentication != null && authentication.getPrincipal() instanceof LccOidcUser) {
LccOidcUser oidcUser = (LccOidcUser) authentication.getPrincipal();
}
return SearchQueryResult.map(premiseRepository.listPremises(filter, new SearchQueryPagination(page, limit), userId, deleted, archived, done), admin ? premiseTransformer::toPremiseDTOWithUserInfo : premiseTransformer::toPremiseDTO);

View file

@ -1,10 +1,13 @@
package de.avatic.lcc.service.users;
import de.avatic.lcc.config.LccOidcUser;
import de.avatic.lcc.dto.users.UserDTO;
import de.avatic.lcc.repositories.pagination.SearchQueryPagination;
import de.avatic.lcc.repositories.pagination.SearchQueryResult;
import de.avatic.lcc.repositories.users.UserRepository;
import de.avatic.lcc.service.transformer.users.UserTransformer;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
/**
@ -42,4 +45,21 @@ public class UserService {
userRepository.update(userTransformer.fromUserDTO(user));
}
public boolean isSuper() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//todo make a service. and simulate user rights in dev profile.
if (authentication != null && authentication.getPrincipal() instanceof LccOidcUser) {
LccOidcUser oidcUser = (LccOidcUser) authentication.getPrincipal();
return oidcUser.getAuthorities().stream().anyMatch(authority -> authority.getAuthority().equals("ROLE_SUPER"));
}
return false;
}
// public boolean canCalculate() {
// Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// }
}

View file

@ -0,0 +1,12 @@
package de.avatic.lcc.service.users.authorization;
import de.avatic.lcc.dto.users.UserDTO;
import de.avatic.lcc.model.users.User;
import org.springframework.stereotype.Service;
@Service
public interface AuthorizationService {
default void setActiveUser(String id) { throw new UnsupportedOperationException(); }
User getActiveUser();
}

View file

@ -0,0 +1,15 @@
package de.avatic.lcc.service.users.authorization;
import de.avatic.lcc.model.users.User;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
@Service
@Profile("!dev & !test")
public class DefaultAuthorizationService implements AuthorizationService{
@Override
public User getActiveUser() {
return null;
}
}

View file

@ -0,0 +1,36 @@
package de.avatic.lcc.service.users.authorization;
import de.avatic.lcc.dto.users.UserDTO;
import de.avatic.lcc.model.users.User;
import de.avatic.lcc.repositories.users.UserRepository;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
@Service
@Profile("dev | test")
public class SimulatedAuthorizationService implements AuthorizationService {
private final UserRepository userRepository;
private User activeUser;
public SimulatedAuthorizationService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public void setActiveUser(String mail) {
this.activeUser = userRepository.getByEmail(mail);
System.out.println(activeUser);
}
@Override
public User getActiveUser() {
if(activeUser == null){
activeUser = userRepository.getById(1);
}
return activeUser;
}
}

View file

@ -8,16 +8,16 @@ VALUES ('USR001', 'john.doe@company.com', 'John', 'Doe', TRUE),
ON DUPLICATE KEY UPDATE email = VALUES(email);
INSERT INTO sys_group(group_name, group_description)
VALUES ('default', 'Default user: Can login and generate reports');
VALUES ('basic', 'Login, generate reports');
INSERT INTO sys_group(group_name, group_description)
VALUES ('LCE', 'Logistic cost expert: Can login, generate reports and do calculations');
VALUES ('calculation', 'Login, generate reports, do calculations');
INSERT INTO sys_group(group_name, group_description)
VALUES ('freight', 'Freight key user: Can login, generate reports and edit freight rates');
VALUES ('freight', 'Login, generate reports, edit freight rates');
INSERT INTO sys_group(group_name, group_description)
VALUES ('packaging', 'Packaging key user: Can login, generate reports and edit packaging data');
VALUES ('packaging', 'Login, generate reports, edit packaging data');
INSERT INTO sys_group(group_name, group_description)
VALUES ('super',
'Super key user: Can login, generate reports, do calculations, edit freight rates, edit packaging data');
'Login, generate reports, do calculations, edit freight rates, edit packaging data');
INSERT INTO sys_user_group_mapping (user_id, group_id)
VALUES ((SELECT id FROM sys_group WHERE group_name = 'LCE'),