reworked "my calculation" page:

- select over multiple pages
- reopen completed calculations
This commit is contained in:
Jan 2025-10-17 22:35:33 +02:00
parent 3141e62b08
commit 20fa52826a
25 changed files with 746 additions and 303 deletions

View file

@ -5,9 +5,11 @@
type="checkbox"
:checked="isChecked"
:disabled="disabled"
:indeterminate.prop="isIndeterminate"
v-model="isChecked"
ref="checkboxInput"
>
<span class="checkmark"></span>
<span class="checkmark" :class="{ indeterminate: isIndeterminate }"></span>
<span class="checkbox-label"><slot></slot></span>
</label>
</div>
@ -26,12 +28,18 @@ export default{
type: Boolean,
default: false,
required: false
},
indeterminate: {
type: Boolean,
default: false,
required: false
}
},
name: "Checkbox",
data() {
return {
internalChecked: this.checked,
internalIndeterminate: this.indeterminate,
}
},
computed: {
@ -42,20 +50,36 @@ export default{
set(value) {
if (this.disabled) return; // Prevent changes when disabled
this.internalChecked = value;
this.internalIndeterminate = false;
this.$emit('checkbox-changed', value);
}
},
isIndeterminate() {
return this.internalIndeterminate && !this.internalChecked;
}
},
watch: {
checked(newVal) {
this.internalChecked = newVal;
this.updateIndeterminateState(this.internalIndeterminate);
},
indeterminate(newVal) {
this.internalIndeterminate = newVal;
this.updateIndeterminateState(newVal);
}
},
mounted() {
this.updateIndeterminateState(this.isIndeterminate);
},
methods: {
setFilter(event) {
if (this.disabled) return; // Prevent action when disabled
// The computed setter will handle the emit
if (this.disabled) return;
this.isChecked = event.target.checked;
},
updateIndeterminateState(value) {
if (this.$refs.checkboxInput) {
this.$refs.checkboxInput.indeterminate = value;
}
}
}
}
@ -130,6 +154,16 @@ export default{
border-color: #6b7280;
}
.checkmark.indeterminate {
background-color: #002F54;
border-color: #002F54;
}
.checkbox-item.disabled .checkmark.indeterminate {
background-color: #6b7280;
border-color: #6b7280;
}
.checkmark::after {
content: "";
position: absolute;
@ -148,10 +182,26 @@ export default{
display: block;
}
.checkmark.indeterminate::after {
display: block;
width: 1rem;
height: 0;
border-width: 0 0 0.2rem 0;
transform: rotate(0deg);
top: 50%;
left: 50%;
margin-left: -0.5rem;
margin-top: -0.1rem;
}
.checkbox-item:not(.disabled):hover input:checked ~ .checkmark::after {
border-color: #8DB3FE;
}
.checkbox-item:not(.disabled):hover .checkmark.indeterminate::after {
border-color: #8DB3FE;
}
.checkbox-label {
color: #002F54;
font-size: 1.4rem;

View file

@ -6,12 +6,16 @@
>
<div v-if="show" class="list-edit">
<icon-button variant="blue" icon="pencil-simple" help-text="Edit all selected calculations" @click="handleAction('edit')"></icon-button>
<icon-button variant="blue" icon="trash" help-text="Delete all selected calculations" @click="handleAction('delete')"></icon-button>
<icon-button variant="blue" icon="archive" help-text="Archive all selected calculations" @click="handleAction('archive')"></icon-button>
<!-- <icon-button variant="blue" icon="pencil-simple" ></icon-button>-->
<!-- <icon-button variant="blue" icon="trash" ></icon-button>-->
<!-- <icon-button variant="blue" icon="archive" ></icon-button>-->
<div class="icon-container">
<ph-selection size="24"/>
<span class="number-circle">{{ selectCount }}</span></div>
<basic-button icon="pencil-simple" @click="handleAction('edit')">Edit</basic-button>
<basic-button icon="trash" @click="handleAction('delete')">Delete</basic-button>
<basic-button icon="archive" @click="handleAction('archive')">Archive</basic-button>
<basic-button icon="X" @click="handleAction('deselect')">Cancel</basic-button>
</div>
</transition>
@ -19,15 +23,21 @@
<script>
import IconButton from "@/components/UI/IconButton.vue";
import {PhSelection} from "@phosphor-icons/vue";
import BasicButton from "@/components/UI/BasicButton.vue";
export default {
name: "ListEdit",
components: {IconButton},
components: {BasicButton, PhSelection, IconButton},
emits: ['action'],
props: {
show: {
type: Boolean,
default: false
},
selectCount: {
type: Number,
default: 1
}
},
methods: {
@ -45,21 +55,49 @@ export default{
bottom: 2rem;
left: 50%;
transform: translate(-50%, 0);
z-index: 9999;
z-index: 4000;
}
.list-edit {
display: flex;
justify-content: center;
align-items: center;
gap: 2rem;
gap: 1.2rem;
background-color: #5AF0B4;
border-radius: 0.8rem;
flex: 0 0 auto;
padding: 0.8rem 1.6rem;
box-shadow: 0 0.4rem 0.6rem -0.1rem rgba(0, 0, 0, 0.1);
font-size: 1.4rem;
}
.list-edit-action {
display: flex;
align-items: center;
gap: 1.2rem;
}
.number-circle {
position: relative;
top: 0.8rem;
left: -1.2rem;
display: inline-block;
border-radius: 50%;
/*background-color: #002F54;
color: white;*/
color: #002F54;
text-align: center;
line-height: 1.6rem;
font-size: 1.2rem;
font-weight: 500;
}
.icon-container {
display: flex;
align-items: center;
gap: 1.2rem;
}
/* Transition animations */

View file

@ -10,7 +10,6 @@
<div class="list-edit-button" @click="handleAction('material')">Material</div>
<div class="list-edit-button" @click="handleAction('price')">Price</div>
<div class="list-edit-button" @click="handleAction('packaging')">Packaging</div>
<div class="list-edit-button" @click="handleAction('supplier')">Supplier</div>
<div class="list-edit-button" @click="handleAction('destinations')">Destinations & Routes</div>
</div>

View file

@ -5,7 +5,7 @@
<div class="modal-dialog-title sub-header">{{ title }}</div>
<div class="modal-dialog-message">{{ message }}</div>
<div class="modal-dialog-actions">
<basic-button :show-icon="false" @click="action('accept')">{{ acceptText }}</basic-button>
<basic-button :show-icon="false" @click="action('accept')" v-if="acceptText!=null">{{ acceptText }}</basic-button>
<basic-button :show-icon="false" @click="action('deny')" v-if="denyText!=null">{{ denyText }}</basic-button>
<basic-button :show-icon="false" @click="action('dismiss')" variant="secondary">{{ dismissText }}</basic-button>
</div>

View file

@ -142,8 +142,8 @@ export default {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin: 20px 0;
gap: 0.8rem;
margin: 2.4rem 0;
flex-wrap: wrap;
}
@ -151,9 +151,9 @@ export default {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
padding: 8px 12px;
border: 0.2rem solid #E3EDFF;
gap: 0.4rem;
padding: 0.8rem 1.6rem;
border: 0 solid rgba(107, 134, 156, 0.1);
background: transparent;
color: #002F54;
cursor: pointer;
@ -161,24 +161,22 @@ export default {
font-size: 1.4rem;
font-family: inherit;
transition: all 0.2s ease;
min-width: 40px;
min-width: 4rem;
}
.pagination-btn:hover:not(:disabled) {
background: #EEF4FF;
border-color: #8DB3FE;
transform: scale(1.01);
background: rgba(107, 134, 156, 0.1);
}
.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;
background: #5AF0B4;
}
@ -187,9 +185,9 @@ export default {
}
.ellipsis {
padding: 8px 4px;
padding: 0.8rem 0.4rem;
color: #666;
font-size: 14px;
font-size: 1.6rem;
}
.page-info {

View file

@ -3,12 +3,11 @@
<div class="edit-calculation-checkbox-cell">
<checkbox :checked="isSelected" @checkbox-changed="updateSelected"></checkbox>
</div>
<div class="edit-calculation-cell--material copyable-cell"
@click="action('material')">
<div class="edit-calculation-cell-container">
<div class="edit-calculation-cell copyable-cell" @click="action('material')">
<div class="edit-calculation-cell-line">{{ premise.material.part_number }}</div>
<!-- <div class="edit-calculation-cell-line edit-calculation-cell-subline" v-if="premise.material.name">-->
<!-- {{ premise.material.name }}-->
<!-- </div>-->
<div class="edit-calculation-cell-line edit-calculation-cell-subline" v-if="premise.hs_code">
HS Code:
{{ premise.material.hs_code }}
@ -19,8 +18,11 @@
{{ toPercent(premise.tariff_rate) }} %
</div>
</div>
<div class="edit-calculation-cell--price copyable-cell" v-if="showPrice"
@click="action('price')">
</div>
<div class="edit-calculation-cell-container">
<div class="edit-calculation-cell copyable-cell" @click="action('price')" v-if="showPrice">
<div class="edit-calculation-cell-line">{{ toFixed(premise.material_cost) }} EUR</div>
<div class="edit-calculation-cell-line edit-calculation-cell-subline">Oversea share:
{{ toPercent(premise.oversea_share) }} %
@ -35,7 +37,11 @@
<div class="edit-calculation-empty copyable-cell" v-else @click="action('price')">
<basic-badge variant="exception" icon="warning">INCOMPLETE</basic-badge>
</div>
<div v-if="showHu" class="edit-calculation-cell edit-calculation-cell--packaging copyable-cell"
</div>
<div class="edit-calculation-cell-container">
<div v-if="showHu" class="edit-calculation-cell copyable-cell"
@click="action('packaging')">
<div class="edit-calculation-cell-line">
<PhVectorThree/>
@ -60,19 +66,23 @@
@click="action('packaging')">
<basic-badge variant="exception" icon="warning">INCOMPLETE</basic-badge>
</div>
<div class="edit-calculation-cell--supplier copyable-cell"
@click="action('supplier')">
<div class="edit-calculation-cell--supplier-container" v-if="premise.supplier">
<!-- <div class="edit-calculation-cell&#45;&#45;supplier-flag">-->
<!-- <flag :iso="premise.supplier.country.iso_code" size="m"></flag>-->
<!-- </div>-->
</div>
<div class="edit-calculation-cell-container">
<div class="edit-calculation-cell" v-if="premise.supplier">
<div class="calculation-list-supplier-data">
<div class="edit-calculation-cell-line">{{ premise.supplier.name }}</div>
<div class="edit-calculation-cell-subline"> {{ premise.supplier.address }}</div>
</div>
</div>
</div>
<div class="edit-calculation-cell--destination copyable-cell" v-if="showDestinations"
<div class="edit-calculation-cell-container">
<div class="edit-calculation-cell copyable-cell" v-if="showDestinations"
@click="action('destinations')">
<div class="edit-calculation-cell-line">
<span class="number-circle"> {{ destinationsCount }} </span> Destinations
@ -89,6 +99,9 @@
@click="action('destinations')">
<basic-badge variant="exception" icon="warning">INCOMPLETE</basic-badge>
</div>
</div>
<div class="edit-calculation-actions-cell">
<icon-button icon="pencil-simple" help-text="Edit this calculation" help-text-position="left"
@ -150,9 +163,9 @@ export default {
return this.premise.destinations.map(d => d.destination_node.name).join(', ');
},
destinationNames() {
const spliceCnt = ((this.premise.destinations.length === 4) ? 4 : 3) - this.showDestinationIncomplete;
const spliceCnt = ((this.premise.destinations.length === 4) ? 4 : 3) - (this.showDestinationIncomplete ? 1 : 0);
const names = this.premise.destinations.map(d => d.destination_node.name).slice(0, spliceCnt);
if (this.premise.destinations.length > 4) {
if (this.premise.destinations.length > names.length) {
names.push('and more ...');
}
@ -216,18 +229,33 @@ export default {
<style scoped>
.edit-calculation-cell-container {
display: flex;
flex-direction: column;
align-self: stretch;
justify-self: stretch;
}
.edit-calculation-cell {
flex: 1 1 auto;
margin: 1.6rem 0;
}
.edit-calculation-empty {
flex: 1 1 auto;
margin: 1.6rem 0;
}
.copyable-cell {
padding: 0.8rem;
border-radius: 0.8rem;
height: 90%;
}
/* Standard hover ohne copy mode */
.copyable-cell:hover {
cursor: pointer;
background-color: rgba(107, 134, 156, 0.05);
background-color: #f8fafc;
border-radius: 0.8rem;
box-shadow: 0 0.4rem 0.6rem -0.1rem rgba(0, 0, 0, 0.1);
}
.bulk-edit-row {
@ -254,31 +282,6 @@ export default {
justify-content: center;
}
.edit-calculation-cell {
padding: 0.8rem;
border-radius: 0.8rem;
height: 90%;
}
.edit-calculation-cell:hover {
cursor: pointer;
background-color: rgba(107, 134, 156, 0.05);
border-radius: 0.8rem;
box-shadow: 0 0.4rem 0.6rem -0.1rem rgba(0, 0, 0, 0.1);
}
.edit-calculation-cell--copy-mode {
padding: 0.8rem;
border-radius: 0.8rem;
height: 90%;
}
.edit-calculation-cell--copy-mode:hover {
cursor: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyOCIgaGVpZ2h0PSIyOCIgdmlld0JveD0iMCAwIDI1NiAyNTYiPgogIDxyZWN0IHg9Ijg0IiB5PSIzMiIgd2lkdGg9IjEzNiIgaGVpZ2h0PSIxMzYiIGZpbGw9IndoaXRlIiBzdHJva2U9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iOCIgcng9IjQiLz4KICA8cmVjdCB4PSIzNiIgeT0iODQiIHdpZHRoPSIxMzYiIGhlaWdodD0iMTM2IiBmaWxsPSJ3aGl0ZSIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IjgiIHJ4PSI0Ii8+Cjwvc3ZnPg==") 12 12, pointer;
background-color: rgba(107, 134, 156, 0.05);
border-radius: 0.8rem;
box-shadow: 0 0.4rem 0.6rem -0.1rem rgba(0, 0, 0, 0.1);
}
.edit-calculation-cell--price {
display: flex;
@ -289,11 +292,11 @@ export default {
.edit-calculation-cell--supplier {
display: flex;
gap: 1.2rem;
height: 90%;
}
.edit-calculation-cell--supplier-container {
display: flex;
height: fit-content;
gap: 0.8rem;
}
@ -325,15 +328,7 @@ export default {
gap: 0.8rem;
}
.edit-calculation-empty {
display: flex;
align-items: flex-start;
justify-content: flex-start;
font-size: 1.2rem;
font-weight: 400;
color: #6b7280;
gap: 0.8rem;
}
.edit-calculation-actions-cell {
display: flex;

View file

@ -44,6 +44,7 @@ import {UrlSafeBase64} from "@/common.js";
export default {
name: "CalculationListItem",
components: {Flag, Checkbox, BasicBadge, IconButton},
emits: ['updateCheckbox', 'updatePagination'],
props: {
id: {
type: Number,
@ -95,7 +96,7 @@ export default {
},
methods: {
updateCheckBox(checked) {
this.premise.checked = checked;
this.$emit('updateCheckbox', {checked: checked, id: this.id});
},
editClick() {
if(this.premise.state === 'DRAFT') {
@ -106,11 +107,13 @@ export default {
deleteClick() {
if(this.premise.state === 'DRAFT') {
this.premiseStore.deletePremisses([this.id])
this.$emit('updatePagination');
}
},
archiveClick() {
if(this.premise.state !== 'DRAFT' && this.premise.state !== 'ARCHIVED') {
this.premiseStore.archivePremisses([this.id])
this.$emit('updatePagination');
}
}
}
@ -123,7 +126,7 @@ export default {
display: grid;
grid-template-columns: 6rem 1fr 2fr 14rem 10rem;
gap: 1.6rem;
padding: 2.4rem;
padding: 1.6rem;
border-bottom: 0.16rem solid #f3f4f6;
align-items: center;
transition: background-color 0.2s ease;
@ -179,6 +182,7 @@ export default {
display: flex;
flex-direction: column;
gap: 0.4rem;
height: 90%
}
.supplier-name {

View file

@ -11,8 +11,9 @@
autocomplete="off"
>
</div>
<checkbox :checked="archived" @checkbox-changed="setArchived">archived</checkbox>
<checkbox :checked="draft" @checkbox-changed="setDraft">draft</checkbox>
<checkbox :checked="done" @checkbox-changed="setDone">completed</checkbox>
<checkbox :checked="archived" @checkbox-changed="setArchived">archived</checkbox>
<basic-button variant="primary" :showIcon="true" icon="plus" :link="true" to="assistant">New calculation</basic-button>
</div>
</template>
@ -33,6 +34,7 @@ export default {
searchTerm: null,
archived: false,
done: false,
draft: true,
}
},
created() {
@ -46,6 +48,7 @@ export default {
searchTerm: this.searchTerm,
archived: this.archived,
done: this.done,
draft: this.draft,
}
this.debouncedSearch(searchQuery);
@ -60,6 +63,10 @@ export default {
setDone(value) {
this.done = value;
this.updateQuery();
},
setDraft(value) {
this.draft = value;
this.updateQuery();
}
}
}

View file

@ -8,7 +8,6 @@
<modal :state="modalSelectMaterial" @close="closeEditModal">
<select-material :part-number="partNumber" @close="modalEditClick"/>
</modal>
<!-- <icon-button icon="pencil-simple" @click="activateEditMode"></icon-button>-->
</div>
</div>
@ -69,7 +68,7 @@ export default {
emits: ["update:tariffRate", "updateMaterial", "update:partNumber", "update:hsCode", "save", "close"],
props: {
description: {
type: String,
type: [String, null],
required: true,
},
hsCode: {

View file

@ -55,8 +55,9 @@ export default {
validator: (value) => value === null || typeof value === 'number',
},
includeFcaFee: {
type: Boolean,
type: [Boolean, null],
required: true,
},
responsive: {
type: Boolean,

View file

@ -49,6 +49,7 @@ import AutosuggestSearchbar from "@/components/UI/AutoSuggestSearchBar.vue";
import {mapStores} from "pinia";
import {useNodeStore} from "@/store/node.js";
import Checkbox from "@/components/UI/Checkbox.vue";
import logger from "@/logger.js";
export default {
name: "SelectNode",
@ -65,7 +66,7 @@ export default {
},
},
created() {
console.log("SelectNode created with openSelectDirect: " + this.openSelectDirect, this.preSelectedNode);
logger.info("SelectNode created with openSelectDirect: " + this.openSelectDirect, this.preSelectedNode);
if(this.openSelectDirect) {
this.node = this.preSelectedNode;
@ -79,7 +80,7 @@ export default {
this.updateMasterData = value;
},
async fetchSupplier(query) {
console.log("Fetching supplier for query: " + query);
logger.info("Fetching supplier for query: " + query);
await this.nodeStore.setSearch({searchTerm: query, nodeType: 'SOURCE', includeUserNode: true});
return this.nodeStore.nodes;
},
@ -87,7 +88,7 @@ export default {
return node.country.iso_code;
},
selected(node) {
console.log("Selected node: ", node);
logger.info("Selected node: ", node);
this.$refs.searchbar.clearSuggestions();
this.node = node;
},

View file

@ -84,11 +84,7 @@
v-model:unitCount="componentProps.unitCount"
v-model:mixable="componentProps.mixable"
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')"
>
@ -123,6 +119,7 @@ import PackagingEdit from "@/components/layout/edit/PackagingEdit.vue";
import DestinationListView from "@/components/layout/edit/DestinationListView.vue";
import SelectNode from "@/components/layout/node/SelectNode.vue";
import Toast from "@/components/UI/Toast.vue";
import logger from "@/logger.js";
const COMPONENT_TYPES = {
@ -196,7 +193,7 @@ export default {
modalType: null,
componentsData: {
price: {props: {price: 0, overSeaShare: 0, includeFcaFee: false}},
material: {props: {partNumber: "", hsCode: "", tariffRate: 0.00, description: "", openSelectDirect: true}},
material: {props: {partNumber: "", hsCode: "", tariffRate: 0.00, description: "", openSelectDirect: false}},
packaging: {
props: {
length: 0,
@ -210,12 +207,6 @@ export default {
stackable: true
}
},
supplier: {
props: {
preSelectedNode: null,
openSelectDirect: false
}
},
destinations: {props: {}},
},
editIds: null,
@ -244,24 +235,6 @@ export default {
closeMassEdit() {
this.$router.push({name: "calculation-list"});
},
async updateSupplier(data) {
console.log("update supplier", data.nodeId, data.action, data.updateMasterData, this.editIds);
console.log("update supplier", data.nodeId, data.action, data.updateMasterData, this.editIds);
this.modalType = null;
if (data.action === 'accept') {
await this.premiseEditStore.setSupplier(data.nodeId, data.updateMasterData, this.editIds);
}
},
async updateMaterial(id, action) {
console.log(id, action, this.editIds);
await this.premiseEditStore.setMaterial(id, action === 'updateMasterData', this.editIds);
if (this.editIds[0] !== null) {
this.fillData("material", this.editIds[0]);
}
},
updateCheckBoxes(value) {
this.premiseEditStore.setAll(value);
},
@ -284,7 +257,7 @@ export default {
this.editIds = ids;
this.modalType = type;
console.log("open modal", massEdit, this.modalType, this.editIds, this.dataSourceId)
logger.info("open modal", massEdit, this.modalType, this.editIds, this.dataSourceId)
},
async closeEditModalAction(action) {
@ -319,27 +292,21 @@ export default {
if (id === -1) {
// clear
this.componentsData = {
price: {props: {price: 0, overSeaShare: 0.0, includeFcaFee: false}},
material: {props: {partNumber: "", hsCode: "", tariffRate: 0.00, description: "", openSelectDirect: true}},
price: {props: {price: null, overSeaShare: null, includeFcaFee: null}},
material: {props: {partNumber: "", hsCode: "", tariffRate: null, description: null, openSelectDirect: true}},
packaging: {
props: {
length: 0,
width: 0,
height: 0,
weight: 0,
length: null,
width: null,
height: null,
weight: null,
weightUnit: "KG",
dimensionUnit: "MM",
unitCount: 1,
unitCount: null,
mixable: true,
stackable: true
}
},
supplier: {
props: {
preSelectedNode: null,
openSelectDirect: false
}
},
destinations: {props: {}},
};
} else {
@ -389,11 +356,11 @@ export default {
/* Global style für copy-mode cursor */
.edit-calculation-container.has-selection :deep(.copyable-cell:hover) {
cursor: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyOCIgaGVpZ2h0PSIyOCIgdmlld0JveD0iMCAwIDI1NiAyNTYiPgogIDxyZWN0IHg9Ijg0IiB5PSIzMiIgd2lkdGg9IjEzNiIgaGVpZ2h0PSIxMzYiIGZpbGw9IndoaXRlIiBzdHJva2U9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iOCIgcng9IjQiLz4KICA8cmVjdCB4PSIzNiIgeT0iODQiIHdpZHRoPSIxMzYiIGhlaWdodD0iMTM2IiBmaWxsPSJ3aGl0ZSIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IjgiIHJ4PSI0Ii8+Cjwvc3ZnPg==") 12 12, pointer;
background-color: rgba(107, 134, 156, 0.05);
background-color: #f8fafc;
border-radius: 0.8rem;
box-shadow: 0 0.4rem 0.6rem -0.1rem rgba(0, 0, 0, 0.1);
}
.space-around {
margin: 3rem;
}

View file

@ -6,12 +6,13 @@
<div class="calculation-list-container">
<the-calculation-search @execute-search="executeSearch"/>
<the-calculation-search @execute-search="updateFilter"/>
<!-- Header -->
<div class="calculation-list-header">
<div>
<checkbox @checkbox-changed="updateCheckBoxes" :checked="overallCheck"></checkbox>
<checkbox @checkbox-changed="updateCheckBoxes" :checked="overallCheck"
:indeterminate="overallIndeterminate"></checkbox>
</div>
<div>Material</div>
<div>Supplier</div>
@ -21,7 +22,9 @@
<transition name="list-container" mode="out-in">
<div v-if="premiseStore.showData">
<calculation-list-item v-for="premise in premiseStore.premises" :key="premise.id" :id="premise.id"
:checked="premise.checked"></calculation-list-item>
:checked="premiseStore.isChecked(premise.id)"
@update-checkbox="updateCheckbox"
@update-pagination="updatePagination"></calculation-list-item>
</div>
<div v-else-if="premiseStore.showEmpty" class="empty-container">
<span class="space-around">No Calculations found.</span>
@ -31,10 +34,15 @@
<spinner class="space-around"></spinner>
</div>
</transition>
<pagination :page="pagination.page" :page-count="pagination.pageCount"
:total-count="pagination.totalCount" @page-change="updatePage"></pagination>
</div>
<list-edit :show="showListEdit" @action="handleMultiselectAction"></list-edit>
<modal-dialog :title="modal.title" :state="modal.state" :message="modal.message" :accept-text="modal.acceptText"
:deny-text="modal.denyText" :dismiss-text="modal.dismissText"
@click="handleModalAction"></modal-dialog>
<list-edit :show="showListEdit" :select-count="premiseStore.selectedIds.length"
@action="handleMultiselectAction"></list-edit>
</div>
@ -57,10 +65,15 @@ import {mapStores} from "pinia";
import Spinner from "@/components/UI/Spinner.vue";
import ListEdit from "@/components/UI/ListEdit.vue";
import {UrlSafeBase64} from "@/common.js";
import Pagination from "@/components/UI/Pagination.vue";
import ModalDialog from "@/components/UI/ModalDialog.vue";
import modal from "@/components/UI/Modal.vue";
export default {
name: "Calculation",
components: {
ModalDialog,
Pagination,
ListEdit,
Spinner,
CalculationListItem, Checkbox, NotificationBar, IconButton, BasicBadge, TheCalculationSearch, Flag
@ -68,59 +81,210 @@ export default {
computed: {
...mapStores(usePremiseStore),
showListEdit() {
return usePremiseStore().premises.some(p => p.checked === true);
return this.premiseStore.globallySomeChecked;
}
},
data() {
return {
overallCheck: false
selectedStatus: null,
modal: {
title: "",
message: "",
acceptText: "OK",
denyText: null,
dismissText: "Cancel",
state: false,
action: "",
},
overallCheck: false,
overallIndeterminate: false,
pagination: {
page: 1,
pageCount: 1,
totalCount: 0
},
query: {
searchTerm: null,
page: 1,
pageSize: 10,
archived: false,
done: false,
draft: true,
}
}
},
created() {
this.premiseStore.setQuery({});
async created() {
this.premiseStore.resetSelectedIds();
await this.executeSearch();
},
methods: {
handleMultiselectAction(action) {
const selectedPremisses = this.premiseStore.premises.filter(p => p.checked === true);
if (action === "delete") {
const ids = selectedPremisses.filter(p => p.state === "DRAFT").map(p => p.id)
if (ids.length !== 0) {
this.premiseStore.deletePremisses(ids);
async handleModalAction(action) {
if (action === 'dismiss') {
this.modal.state = false;
return;
}
if (action === 'accept' && this.modal.action === "delete") {
await this.executeMultiselectAction(this.modal.action, this.selectedStatus.draft)
}
if (action === 'accept' && this.modal.action === "archive") {
await this.executeMultiselectAction(this.modal.action, this.selectedStatus.completed)
}
if (this.modal.action === "edit") {
if (action === 'deny')
await this.executeMultiselectAction(this.modal.action, this.selectedStatus.draft);
else {
const ids = await this.premiseStore.createDrafts([...this.selectedStatus.archived, ...this.selectedStatus.completed]);
await this.executeMultiselectAction(this.modal.action, [...this.selectedStatus.draft, ...ids]);
}
}
this.modal.state = false;
},
async executeMultiselectAction(action, ids) {
if (action === "delete") {
if (ids.length !== 0) {
await this.premiseStore.deletePremisses(ids);
this.premiseStore.resetSelectedIds();
this.updateOverallCheckBox();
this.updatePagination();
}
} else if (action === "edit") {
const ids = selectedPremisses.filter(p => p.state === "DRAFT").map(p => p.id)
if (ids.length === 1) {
this.$router.push({name: "edit", params: {id: new UrlSafeBase64().encodeIds([ids[0]])}});
} else if (ids.length !== 0) {
this.$router.push({name: "bulk", params: {ids: new UrlSafeBase64().encodeIds(ids)}});
}
} else if (action === "archive") {
if (ids.length !== 0) {
await this.premiseStore.archivePremisses(ids);
this.premiseStore.resetSelectedIds();
this.updateOverallCheckBox();
this.updatePagination();
}
}
},
async handleMultiselectAction(action) {
this.selectedStatus = await this.premiseStore.resolveStatus();
console.log("status: ", this.selectedStatus)
if (action === "delete") {
if (0 === this.selectedStatus.draft.length) {
this.modal.title = "Unable to delete calculations";
this.modal.message = `All of the ${this.premiseStore.selectedIds.length} selected calculations are already completed or archived and can no longer be deleted.`;
this.modal.action = "delete";
this.modal.state = true;
this.modal.acceptText = null;
this.modal.denyText = null;
this.modal.dismissText = "OK";
} else if (0 < (this.selectedStatus.completed.length + this.selectedStatus.archived.length)) {
this.modal.title = "Delete calculations";
this.modal.message = `${(this.selectedStatus.completed.length + this.selectedStatus.archived.length)} of the ${this.premiseStore.selectedIds.length} selected calculations are already completed or archived and can no longer be deleted. Do you still want to delete the remaining ${this.selectedStatus.draft.length} calculations?`;
this.modal.action = "delete";
this.modal.state = true;
this.modal.acceptText = "Delete";
this.modal.denyText = null;
this.modal.dismissText = "Cancel";
} else
this.executeMultiselectAction(action, this.selectedStatus.draft);
} else if (action === "edit") {
if (0 === this.selectedStatus.draft.length) {
this.modal.title = "Edit calculations";
this.modal.message = `All of the ${this.premiseStore.selectedIds.length} selected calculations are already completed or archived and can no longer be edited. Would you like to create new drafts based on the data from the existing calculations, or continue the operation without these calculations?`;
this.modal.action = "edit";
this.modal.state = true;
this.modal.acceptText = "Create new drafts & edit";
this.modal.denyText = null;
this.modal.dismissText = "Cancel";
} else if (0 < (this.selectedStatus.completed.length + this.selectedStatus.archived.length)) {
this.modal.title = "Edit calculations";
this.modal.message = `${(this.selectedStatus.completed.length + this.selectedStatus.archived.length)} of the ${this.premiseStore.selectedIds.length} selected calculations are already completed or archived and can no longer be edited. Would you like to create new drafts based on the data from the existing calculations, or continue the operation without these calculations?`;
this.modal.action = "edit";
this.modal.state = true;
this.modal.acceptText = "Create new drafts & edit";
this.modal.denyText = "Edit drafts only";
this.modal.dismissText = "Cancel";
} else
this.executeMultiselectAction(action, this.selectedStatus.draft);
} else if (action === "archive") {
const ids = selectedPremisses.filter(p => p.state !== "DRAFT" && p.state !== "ARCHIVED").map(p => p.id)
if (ids.length !== 0) {
this.premiseStore.archivePremisses(ids);
}
if (0 === this.selectedStatus.completed.length) {
this.modal.title = "Unable to archive calculations";
this.modal.message = `All of the ${this.premiseStore.selectedIds.length} selected calculations are in draft status or already archived and cannot be archived.`;
this.modal.action = "archive";
this.modal.state = true;
this.modal.acceptText = null;
this.modal.denyText = null;
this.modal.dismissText = "OK";
} else if (0 < (this.selectedStatus.draft.length + this.selectedStatus.archived.length)) {
this.modal.title = "Archive calculations";
this.modal.message = `${(this.selectedStatus.draft.length + this.selectedStatus.archived.length)} of the ${this.premiseStore.selectedIds.length} selected calculations are in draft status or already archived and cannot be archived. Do you still want to archive the remaining ${this.selectedStatus.completed.length} calculations?`;
this.modal.action = "archive";
this.modal.state = true;
this.modal.acceptText = "Archive";
this.modal.denyText = null;
this.modal.dismissText = "Cancel";
} else
this.executeMultiselectAction(action, this.selectedStatus.completed);
} else if (action === "deselect") {
this.selectedStatus = null;
this.premiseStore.resetSelectedIds();
this.updateOverallCheckBox();
}
},
updatePagination() {
this.pagination = this.premiseStore.pagination;
},
updateCheckbox(data) {
this.premiseStore.setChecked(data.id, data.checked);
this.updateOverallCheckBox();
},
updateCheckBoxes(checked) {
this.overallCheck = checked;
this.premiseStore.premises.forEach(p => {
p.checked = checked;
this.premiseStore.setChecked(p.id, checked);
})
this.updateOverallCheckBox();
},
executeSearch(query) {
this.overallCheck = false;
this.premiseStore.setQuery(query);
updateOverallCheckBox() {
this.overallCheck = this.premiseStore.allChecked;
if (!this.overallCheck)
this.overallIndeterminate = this.premiseStore.someChecked;
},
async updatePage(page) {
this.query.page = page;
await this.executeSearch();
},
async updateFilter(query) {
if (this.query.archived !== query.archived ||
this.query.done !== query.done ||
this.query.draft !== query.draft ||
this.query.searchTerm !== query.searchTerm) {
this.query.page = 1;
}
this.query.searchTerm = query.searchTerm;
this.query.archived = query.archived;
this.query.done = query.done;
this.query.draft = query.draft;
await this.executeSearch();
},
async executeSearch() {
await this.premiseStore.setQuery(this.query);
this.updatePagination();
this.updateOverallCheckBox();
}
}
}
@ -209,7 +373,7 @@ export default {
display: grid;
grid-template-columns: 6rem 1fr 2fr 14rem 10rem;
gap: 1.6rem;
padding: 2.4rem;
padding: 1.6rem;
background-color: #ffffff;
border-bottom: 1px solid rgba(107, 134, 156, 0.2);
font-weight: 500;

View file

@ -2,10 +2,12 @@ import {defineStore} from 'pinia'
import {config} from '@/config'
import {useErrorStore} from "@/store/error.js";
import performRequest from "@/backend.js";
import logger from "@/logger.js";
export const usePremiseStore = defineStore('premise', {
state: () => ({
premises: [],
selectedIds: [],
loading: false,
empty: true,
error: null,
@ -16,17 +18,74 @@ export const usePremiseStore = defineStore('premise', {
getById: (state) => {
return (id) => state.premises.find(p => p.id === id)
},
isChecked(state) {
return function (premiseId) {
return state.selectedIds.includes(premiseId);
}
},
allChecked(state) {
if (state.premises.length > state.selectedIds.length)
return false;
for (const premise of state.premises) {
if (!state.selectedIds.includes(premise.id))
return false;
}
return state.premises.length !== 0;
},
globallySomeChecked(state) {
return state.selectedIds.length > 0;
},
someChecked(state) {
for (const premise of state.premises) {
if (state.selectedIds.includes(premise.id))
return true;
}
return false;
},
showData: (state) => !state.loading && state.empty === false,
showLoading: (state) => state.loading,
showEmpty: (state) => !state.loading && state.empty === true,
selectedIds: state => state.premises?.filter(p => p.checked).map(p => p.id) ?? [],
},
actions: {
setQuery(query) {
async createDrafts(ids) {
const params = new URLSearchParams();
params.append('premissIds', ids.join(', '));
const data = await performRequest(this, "GET", `${config.backendUrl}/calculation/duplicate/${params.size === 0 ? '' : '?'}${params.toString()}`, null, true)
return data.data;
},
async resolveStatus() {
const params = new URLSearchParams();
params.append('premissIds', this.selectedIds.join(', '));
const data = await performRequest(this, 'GET', `${config.backendUrl}/calculation/resolve/${params.size === 0 ? '' : '?'}${params.toString()}`, null, true);
return data.data;
},
resetSelectedIds() {
this.selectedIds = [];
},
setChecked(premiseId, checked) {
if (checked) {
if (!this.selectedIds.includes(premiseId)) {
this.selectedIds.push(premiseId);
}
} else {
const idx = this.selectedIds.indexOf(premiseId);
if (idx !== -1)
this.selectedIds.splice(idx, 1);
}
},
async setQuery(query) {
this.query = query;
this.updatePremises();
await this.updatePremises();
},
async deletePremisses(ids) {
ids.forEach(id => this.setChecked(id, false));
const params = new URLSearchParams();
params.append('premissIds', ids.join(', '));
await performRequest(this, 'POST', `${config.backendUrl}/calculation/delete/${params.size === 0 ? '' : '?'}${params.toString()}`, null, false);
@ -55,17 +114,29 @@ export const usePremiseStore = defineStore('premise', {
if (this.query.done)
params.append('done', this.query.done);
if (this.query.deleted)
params.append('deleted', this.query.deleted);
if (this.query.draft)
params.append('draft', this.query.draft);
if (this.query?.page)
params.append('page', this.query.page);
if (this.query?.pageSize)
params.append('limit', this.query.pageSize);
const url = `${config.backendUrl}/calculation/view/${params.size === 0 ? '' : '?'}${params.toString()}`;
const resp = await performRequest(this, 'GET', url, null, true);
const data = resp.data;
const {data: data, headers: headers} = await performRequest(this, 'GET', url, null, true);
this.pagination = {
page: parseInt(headers.get('X-Current-Page')),
pageCount: parseInt(headers.get('X-Page-Count')),
totalCount: parseInt(headers.get('X-Total-Count'))
};
logger.log("pagination (premise store)", this.pagination);
this.loading = false;
this.empty = data.length === 0;
data.forEach(p => p.checked = false);
this.premises = data;
}

View file

@ -255,9 +255,9 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
if (ids.includes(p.id)) {
return {
...p,
material_cost: priceData.price,
oversea_share: priceData.overSeaShare,
is_fca_enabled: priceData.includeFcaFee
...(priceData.price !== null && {material_cost: priceData.price}),
...(priceData.overSeaShare !== null && {oversea_share: priceData.overSeaShare}),
...(priceData.includeFcaFee !== null && {is_fca_enabled: priceData.includeFcaFee})
};
}
return p;
@ -273,10 +273,10 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
...p,
material: {
...p.material,
part_number: materialData.partNumber,
hs_code: materialData.hsCode
...(materialData.partNumber !== null && {part_number: materialData.partNumber}),
...(materialData.hsCode !== null && {hs_code: materialData.hsCode})
},
tariff_rate: materialData.tariffRate
...(materialData.tariffRate !== null && {tariff_rate: materialData.tariffRate})
};
}
return p;
@ -292,16 +292,16 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
...p,
handling_unit: {
...p.handling_unit,
weight: packagingData.weight,
width: packagingData.width,
length: packagingData.length,
height: packagingData.height,
weight_unit: packagingData.weightUnit,
dimension_unit: packagingData.dimensionUnit,
content_unit_count: packagingData.unitCount
...(packagingData.weight !== null && {weight: packagingData.weight}),
...(packagingData.width !== null && {width: packagingData.width}),
...(packagingData.length !== null && {length: packagingData.length}),
...(packagingData.height !== null && {height: packagingData.height}),
...(packagingData.weightUnit !== null && {weight_unit: packagingData.weightUnit}),
...(packagingData.dimensionUnit !== null && {dimension_unit: packagingData.dimensionUnit}),
...(packagingData.unitCount !== null && {content_unit_count: packagingData.unitCount})
},
is_stackable: packagingData.stackable,
is_mixable: packagingData.mixable
...(packagingData.stackable !== null && {is_stackable: packagingData.stackable}),
...(packagingData.mixable !== null && {is_mixable: packagingData.mixable})
};
}
return p;

View file

@ -4,6 +4,7 @@ package de.avatic.lcc.controller.calculation;
import de.avatic.lcc.dto.calculation.CalculationStatus;
import de.avatic.lcc.dto.calculation.DestinationDTO;
import de.avatic.lcc.dto.calculation.PremiseDTO;
import de.avatic.lcc.dto.calculation.ResolvePremiseDTO;
import de.avatic.lcc.dto.calculation.create.CreatePremiseDTO;
import de.avatic.lcc.dto.calculation.create.PremiseSearchResultDTO;
import de.avatic.lcc.dto.calculation.edit.PremiseDetailDTO;
@ -65,11 +66,11 @@ public class PremiseController {
@RequestParam(defaultValue = "20") @Min(1) int limit,
@RequestParam(defaultValue = "1") @Min(1) int page,
@RequestParam(name = "user", required = false) Integer userId,
@RequestParam(required = false) Boolean deleted,
@RequestParam(required = false) Boolean draft,
@RequestParam(required = false) Boolean archived,
@RequestParam(required = false) Boolean done) {
var premises = premisesServices.listPremises(filter, page, limit, userId, deleted, archived, done);
var premises = premisesServices.listPremises(filter, page, limit, userId, draft, archived, done);
return ResponseEntity.ok()
.header("X-Total-Count", String.valueOf(premises.getTotalElements()))
@ -106,6 +107,17 @@ public class PremiseController {
dto.createEmpty()));
}
@GetMapping({"/resolve", "/resolve/"})
@PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
public ResponseEntity<ResolvePremiseDTO> resolvePremiseStatus(@RequestParam List<Integer> premissIds) {
return ResponseEntity.ok(premisesServices.resolveStatus(premissIds));
}
@GetMapping({"/duplicate", "/duplicate/"})
@PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
public ResponseEntity<List<Integer>> duplicate(@RequestParam List<Integer> premissIds) {
return ResponseEntity.ok(premisesServices.duplicate(premissIds));
}
@PostMapping({"/delete", "/delete/"})
@PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")

View file

@ -0,0 +1,36 @@
package de.avatic.lcc.dto.calculation;
import java.util.List;
public class ResolvePremiseDTO {
private List<Integer> completed;
private List<Integer> draft;
private List<Integer> archived;
public List<Integer> getArchived() {
return archived;
}
public void setArchived(List<Integer> archive) {
this.archived = archive;
}
public List<Integer> getCompleted() {
return completed;
}
public void setCompleted(List<Integer> completed) {
this.completed = completed;
}
public List<Integer> getDraft() {
return draft;
}
public void setDraft(List<Integer> draft) {
this.draft = draft;
}
}

View file

@ -2,5 +2,5 @@ package de.avatic.lcc.model.premises;
public enum PremiseState {
DRAFT, COMPLETED, ARCHIVED, DELETED
DRAFT, COMPLETED, ARCHIVED
}

View file

@ -61,6 +61,17 @@ public class CalculationJobRepository {
return Optional.of(job.getFirst());
}
@Transactional
public void reschedule(Integer id) {
String sql = "UPDATE calculation_job SET job_state = ?, calculation_date = ? WHERE id = ?";
var affectedRows = jdbcTemplate.update(sql, CalculationJobState.CREATED.name(), java.time.LocalDateTime.now(), id);
if(1 != affectedRows) {
throw new DatabaseException("Unable to update calculation job with id " + id);
}
}
@Transactional
public Optional<CalculationJob> getCalculationJobWithJobStateValid(Integer periodId, Integer nodeId, Integer materialId) {
@ -86,6 +97,27 @@ public class CalculationJobRepository {
}
}
@Transactional
public Optional<CalculationJob> findJob(Integer premiseId, Integer validSetId, Integer validPeriodId) {
String sql = "SELECT * FROM calculation_job WHERE premise_id = ? AND validity_period_id = ? AND property_set_id = ?";
var jobs = jdbcTemplate.query(sql, new CalculationJobMapper(), premiseId, validPeriodId, validSetId);
return jobs.isEmpty() ? Optional.empty() : Optional.ofNullable(jobs.getFirst());
}
@Transactional
public void invalidateByPropertySetId(Integer id) {
String sql = "UPDATE calculation_job SET job_state = ? WHERE property_set_id = ?";
jdbcTemplate.update(sql, CalculationJobState.INVALIDATED.name(), id);
}
@Transactional
public void invalidateByPeriodId(Integer id) {
String sql = "UPDATE calculation_job SET job_state = ? WHERE validity_period_id = ?";
jdbcTemplate.update(sql, CalculationJobState.INVALIDATED.name(), id);
}
private static class CalculationJobMapper implements RowMapper<CalculationJob> {
@Override

View file

@ -45,11 +45,11 @@ public class PremiseRepository {
@Transactional
public SearchQueryResult<PremiseListEntry> listPremises(String filter, SearchQueryPagination pagination,
Integer userId, Boolean deleted, Boolean archived, Boolean done) {
Integer userId, Boolean draft, Boolean archived, Boolean done) {
QueryBuilder queryBuilder = new QueryBuilder()
.withFilter(filter)
.withDeleted(deleted)
.withDraft(draft)
.withArchived(archived)
.withDone(done);
@ -428,12 +428,15 @@ public class PremiseRepository {
if (premiseIds == null || premiseIds.isEmpty()) return;
String placeholders = String.join(",", Collections.nCopies(premiseIds.size(), "?"));
String query = """
DELETE FROM premise
WHERE id IN (""" + placeholders + ") AND state = '" + PremiseState.DRAFT.name() + "'";
String query = "DELETE FROM premise WHERE id IN (" + placeholders + ") AND state = ?";
Object[] params = new Object[premiseIds.size() + 1];
for (int i = 0; i < premiseIds.size(); i++) {
params[i] = premiseIds.get(i);
}
params[premiseIds.size()] = PremiseState.DRAFT.name();
jdbcTemplate.update(query, premiseIds.toArray());
jdbcTemplate.update(query, params);
}
@ -678,7 +681,7 @@ public class PremiseRepository {
"user_n.name LIKE ? OR m.name LIKE ? OR m.name LIKE ? OR m.part_number LIKE ?)";
private String filter;
private Boolean deleted;
private Boolean draft;
private Boolean archived;
private Boolean done;
@ -687,8 +690,8 @@ public class PremiseRepository {
return this;
}
public QueryBuilder withDeleted(Boolean deleted) {
this.deleted = deleted;
public QueryBuilder withDraft(Boolean draft) {
this.draft = draft;
return this;
}
@ -725,7 +728,7 @@ public class PremiseRepository {
user_n.country_id as 'user_n.country_id', user_n.geo_lat as 'user_n.geo_lat', user_n.geo_lng as 'user_n.geo_lng'
""").append(BASE_JOIN_QUERY);
appendConditions(queryBuilder);
queryBuilder.append(" ORDER BY p.updated_at DESC");
queryBuilder.append(" ORDER BY p.updated_at, p.id DESC");
queryBuilder.append(" LIMIT ? OFFSET ?");
return queryBuilder.toString();
}
@ -735,13 +738,13 @@ public class PremiseRepository {
queryBuilder.append(FILTER_CONDITION);
}
if (deleted != null && deleted || archived != null && archived || done != null && done) {
if (draft != null && draft || archived != null && archived || done != null && done) {
boolean concat = false;
queryBuilder.append(" AND (");
if (deleted != null && deleted) {
queryBuilder.append(" p.state").append(" = 'DELETED'");
if (draft != null && draft) {
queryBuilder.append(" p.state").append(" = 'DRAFT'");
concat = true;
}

View file

@ -1,7 +1,7 @@
package de.avatic.lcc.service.access;
import de.avatic.lcc.config.LccOidcUser;
import de.avatic.lcc.dto.calculation.CalculationStatus;
import de.avatic.lcc.dto.calculation.ResolvePremiseDTO;
import de.avatic.lcc.dto.calculation.PremiseDTO;
import de.avatic.lcc.dto.calculation.edit.PremiseDetailDTO;
import de.avatic.lcc.dto.calculation.edit.masterData.MaterialUpdateDTO;
@ -29,14 +29,13 @@ import de.avatic.lcc.service.users.AuthorizationService;
import de.avatic.lcc.util.exception.base.InternalErrorException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
@ -83,10 +82,10 @@ public class PremisesService {
}
@Transactional(readOnly = true)
public SearchQueryResult<PremiseDTO> listPremises(String filter, Integer page, Integer limit, Integer userId, Boolean deleted, Boolean archived, Boolean done) {
public SearchQueryResult<PremiseDTO> listPremises(String filter, Integer page, Integer limit, Integer userId, Boolean draft, Boolean archived, Boolean done) {
var admin = authorizationService.isSuper();
userId = authorizationService.getUserId();
return SearchQueryResult.map(premiseRepository.listPremises(filter, new SearchQueryPagination(page, limit), userId, deleted, archived, done), admin ? premiseTransformer::toPremiseDTOWithUserInfo : premiseTransformer::toPremiseDTO);
return SearchQueryResult.map(premiseRepository.listPremises(filter, new SearchQueryPagination(page, limit), userId, draft, archived, done), admin ? premiseTransformer::toPremiseDTOWithUserInfo : premiseTransformer::toPremiseDTO);
}
@Transactional(readOnly = true)
@ -112,6 +111,18 @@ public class PremisesService {
var calculationIds = new ArrayList<Integer>();
premises.forEach(p -> {
var existingJob = calculationJobRepository.findJob(p, validSetId, validPeriodId);
if (existingJob.isPresent()) {
if (CalculationJobState.EXCEPTION == existingJob.get().getJobState()) {
calculationJobRepository.setStateTo(existingJob.get().getId(), CalculationJobState.CREATED);
calculationIds.add(existingJob.get().getId());
} else if (CalculationJobState.SCHEDULED == existingJob.get().getJobState() && existingJob.get().getCalculationDate().plusMinutes(15).isBefore(LocalDateTime.now())) {
calculationJobRepository.reschedule(existingJob.get().getId());
calculationIds.add(existingJob.get().getId());
}
} else {
CalculationJob job = new CalculationJob();
job.setPremiseId(p);
@ -122,6 +133,9 @@ public class PremisesService {
job.setValidityPeriodId(validPeriodId);
calculationIds.add(calculationJobRepository.insert(job));
}
});
premiseRepository.setStatus(premises, PremiseState.COMPLETED);
@ -157,8 +171,6 @@ public class PremisesService {
}
}
} catch (CompletionException | InterruptedException | ExecutionException e) {
throw new InternalErrorException("Calculation execution failed.", e);
}
@ -214,8 +226,7 @@ public class PremisesService {
// only delete drafts.
var toBeDeleted = premiseRepository.getPremisesById(premiseIds).stream().filter(p -> p.getState().equals(PremiseState.DRAFT)).map(Premise::getId).toList();
if(!toBeDeleted.isEmpty())
{
if (!toBeDeleted.isEmpty()) {
destinationService.deleteAllDestinationsByPremiseId(toBeDeleted, false);
premiseRepository.deletePremisesById(toBeDeleted);
}
@ -230,4 +241,49 @@ public class PremisesService {
var premisses = premiseRepository.getPremisesById(premiseIds);
premiseRepository.setStatus(premisses.stream().filter(p -> p.getState().equals(PremiseState.COMPLETED)).map(Premise::getId).toList(), PremiseState.ARCHIVED);
}
public ResolvePremiseDTO resolveStatus(List<Integer> premissIds) {
ResolvePremiseDTO resolveDTO = new ResolvePremiseDTO();
var drafts = new ArrayList<Integer>();
var completed = new ArrayList<Integer>();
var archived = new ArrayList<Integer>();
var premisses = premiseRepository.getPremisesById(premissIds);
premisses.forEach(p -> {
if(PremiseState.DRAFT.equals(p.getState())) drafts.add(p.getId());
else if(PremiseState.COMPLETED.equals(p.getState())) completed.add(p.getId());
else if(PremiseState.ARCHIVED.equals(p.getState())) archived.add(p.getId());
});
resolveDTO.setCompleted(completed);
resolveDTO.setDraft(drafts);
resolveDTO.setArchived(archived);
return resolveDTO;
}
public List<Integer> duplicate(List<Integer> premissIds) {
var userId = authorizationService.getUserId();
premiseRepository.checkOwner(premissIds, userId);
var newIds = new ArrayList<Integer>();
premissIds.forEach(id -> {
var old = premiseRepository.getPremiseById(id).orElseThrow();
var newId = premiseRepository.insert(old.getMaterialId(), old.getSupplierNodeId(), old.getUserSupplierNodeId(), BigDecimal.valueOf(old.getLocation().getLatitude()), BigDecimal.valueOf(old.getLocation().getLongitude()), old.getCountryId(), userId);
premiseRepository.updateMaterial(Collections.singletonList(newId), old.getHsCode(), old.getTariffRate());
premiseRepository.updatePrice(Collections.singletonList(newId), old.getMaterialCost(), old.getFcaEnabled(), old.getOverseaShare());
premiseRepository.updatePackaging(Collections.singletonList(newId), dimensionTransformer.toDimensionEntity(old), old.getHuStackable(), old.getHuMixable());
premiseRepository.setPackagingId(newId, old.getPackagingId());
destinationService.duplicate(old.getId(), newId);
newIds.add(newId);
});
return newIds;
}
}

View file

@ -4,6 +4,7 @@ import de.avatic.lcc.dto.generic.PropertyDTO;
import de.avatic.lcc.dto.generic.ValidityPeriodDTO;
import de.avatic.lcc.model.properties.SystemPropertyMappingId;
import de.avatic.lcc.model.rates.ValidityPeriodState;
import de.avatic.lcc.repositories.calculation.CalculationJobRepository;
import de.avatic.lcc.repositories.properties.PropertyRepository;
import de.avatic.lcc.repositories.properties.PropertySetRepository;
import de.avatic.lcc.service.transformer.rates.ValidityPeriodTransformer;
@ -36,6 +37,7 @@ public class PropertyService {
private final PropertySetRepository propertySetRepository;
private final ValidityPeriodTransformer validityPeriodTransformer;
private final PropertyValidationService propertyValidationService;
private final CalculationJobRepository calculationJobRepository;
/**
@ -45,11 +47,12 @@ public class PropertyService {
* @param propertySetRepository the repository to manage property sets
* @param validityPeriodTransformer the transformer to convert property sets to validity period DTOs
*/
public PropertyService(PropertyRepository propertyRepository, PropertySetRepository propertySetRepository, ValidityPeriodTransformer validityPeriodTransformer, PropertyValidationService propertyValidationService) {
public PropertyService(PropertyRepository propertyRepository, PropertySetRepository propertySetRepository, ValidityPeriodTransformer validityPeriodTransformer, PropertyValidationService propertyValidationService, CalculationJobRepository calculationJobRepository) {
this.propertyRepository = propertyRepository;
this.propertySetRepository = propertySetRepository;
this.validityPeriodTransformer = validityPeriodTransformer;
this.propertyValidationService = propertyValidationService;
this.calculationJobRepository = calculationJobRepository;
}
/**
@ -116,6 +119,7 @@ public class PropertyService {
if (!propertySetRepository.invalidateById(id))
throw new NotFoundException(NotFoundType.EXPIRED_PROPERTY_SET, "id", id.toString());
calculationJobRepository.invalidateByPropertySetId(id);
}

View file

@ -1,6 +1,7 @@
package de.avatic.lcc.service.access;
import de.avatic.lcc.dto.generic.ValidityPeriodDTO;
import de.avatic.lcc.repositories.calculation.CalculationJobRepository;
import de.avatic.lcc.repositories.rates.ValidityPeriodRepository;
import de.avatic.lcc.service.transformer.rates.ValidityPeriodTransformer;
import de.avatic.lcc.util.exception.badrequest.NotFoundException;
@ -22,6 +23,7 @@ public class ValidityPeriodService {
private final ValidityPeriodRepository validityPeriodRepository;
private final ValidityPeriodTransformer validityPeriodTransformer;
private final CalculationJobRepository calculationJobRepository;
/**
* Constructs a new ValidityPeriodService with the required dependencies.
@ -29,9 +31,10 @@ public class ValidityPeriodService {
* @param validityPeriodRepository Repository for accessing validity period data
* @param validityPeriodTransformer Transformer for converting between entity and DTO representations
*/
public ValidityPeriodService(ValidityPeriodRepository validityPeriodRepository, ValidityPeriodTransformer validityPeriodTransformer) {
public ValidityPeriodService(ValidityPeriodRepository validityPeriodRepository, ValidityPeriodTransformer validityPeriodTransformer, CalculationJobRepository calculationJobRepository) {
this.validityPeriodRepository = validityPeriodRepository;
this.validityPeriodTransformer = validityPeriodTransformer;
this.calculationJobRepository = calculationJobRepository;
}
@ -60,5 +63,6 @@ public class ValidityPeriodService {
if (!validityPeriodRepository.invalidateById(id))
throw new NotFoundException(NotFoundException.NotFoundType.EXPIRED_VALIDITY_PERIOD, "id", id.toString());
calculationJobRepository.invalidateByPeriodId(id);
}
}

View file

@ -97,6 +97,8 @@ public class PremiseCreationService {
return premiseRepository.getPremisesById(premises.stream().map(TemporaryPremise::getId).toList()).stream().map(premiseTransformer::toPremiseDetailDTO).toList();
}
private void copyPremise(TemporaryPremise p, Integer userId) {
var old = p.getPremise();

View file

@ -373,7 +373,7 @@ CREATE TABLE IF NOT EXISTS premise
FOREIGN KEY (packaging_id) REFERENCES packaging (id),
FOREIGN KEY (user_id) REFERENCES sys_user (id),
CONSTRAINT `chk_premise_state_values` CHECK (`state` IN
('DRAFT', 'COMPLETED', 'ARCHIVED', 'DELETED')),
('DRAFT', 'COMPLETED', 'ARCHIVED')),
CONSTRAINT `chk_premise_displayed_dimension_unit` CHECK (`hu_displayed_dimension_unit` IN
('MM', 'CM', 'M')),
CONSTRAINT `chk_premise_displayed_weight_unit` CHECK (`hu_displayed_weight_unit` IN