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

View file

@ -5,29 +5,39 @@
class="list-edit-container" class="list-edit-container"
> >
<div v-if="show" class="list-edit"> <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> <div class="icon-container">
<icon-button variant="blue" icon="trash" help-text="Delete all selected calculations" @click="handleAction('delete')"></icon-button> <ph-selection size="24"/>
<icon-button variant="blue" icon="archive" help-text="Archive all selected calculations" @click="handleAction('archive')"></icon-button> <span class="number-circle">{{ selectCount }}</span></div>
<!-- <icon-button variant="blue" icon="pencil-simple" ></icon-button>-->
<!-- <icon-button variant="blue" icon="trash" ></icon-button>--> <basic-button icon="pencil-simple" @click="handleAction('edit')">Edit</basic-button>
<!-- <icon-button variant="blue" icon="archive" ></icon-button>--> <basic-button icon="trash" @click="handleAction('delete')">Delete</basic-button>
</div> <basic-button icon="archive" @click="handleAction('archive')">Archive</basic-button>
<basic-button icon="X" @click="handleAction('deselect')">Cancel</basic-button>
</div>
</transition> </transition>
</template> </template>
<script> <script>
import IconButton from "@/components/UI/IconButton.vue"; import IconButton from "@/components/UI/IconButton.vue";
import {PhSelection} from "@phosphor-icons/vue";
import BasicButton from "@/components/UI/BasicButton.vue";
export default{ export default {
name: "ListEdit", name: "ListEdit",
components: {IconButton}, components: {BasicButton, PhSelection, IconButton},
emits: ['action'], emits: ['action'],
props: { props: {
show: { show: {
type: Boolean, type: Boolean,
default: false default: false
},
selectCount: {
type: Number,
default: 1
} }
}, },
methods: { methods: {
@ -43,23 +53,51 @@ export default{
.list-edit-container { .list-edit-container {
position: fixed; position: fixed;
bottom: 2rem; bottom: 2rem;
left: 50%; left: 50%;
transform: translate(-50%, 0); transform: translate(-50%, 0);
z-index: 9999; z-index: 4000;
} }
.list-edit { .list-edit {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
gap: 2rem; gap: 1.2rem;
background-color: #5AF0B4; background-color: #5AF0B4;
border-radius: 0.8rem; border-radius: 0.8rem;
flex: 0 0 auto; flex: 0 0 auto;
padding: 0.8rem 1.6rem; padding: 0.8rem 1.6rem;
box-shadow: 0 0.4rem 0.6rem -0.1rem rgba(0, 0, 0, 0.1); 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 */ /* 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('material')">Material</div>
<div class="list-edit-button" @click="handleAction('price')">Price</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('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 class="list-edit-button" @click="handleAction('destinations')">Destinations & Routes</div>
</div> </div>

View file

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

View file

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

View file

@ -3,92 +3,105 @@
<div class="edit-calculation-checkbox-cell"> <div class="edit-calculation-checkbox-cell">
<checkbox :checked="isSelected" @checkbox-changed="updateSelected"></checkbox> <checkbox :checked="isSelected" @checkbox-changed="updateSelected"></checkbox>
</div> </div>
<div class="edit-calculation-cell--material copyable-cell"
@click="action('material')">
<div class="edit-calculation-cell-line">{{ premise.material.part_number }}</div> <div class="edit-calculation-cell-container">
<!-- <div class="edit-calculation-cell-line edit-calculation-cell-subline" v-if="premise.material.name">--> <div class="edit-calculation-cell copyable-cell" @click="action('material')">
<!-- {{ premise.material.name }}--> <div class="edit-calculation-cell-line">{{ premise.material.part_number }}</div>
<!-- </div>--> <div class="edit-calculation-cell-line edit-calculation-cell-subline" v-if="premise.hs_code">
<div class="edit-calculation-cell-line edit-calculation-cell-subline" v-if="premise.hs_code"> HS Code:
HS Code: {{ premise.material.hs_code }}
{{ premise.material.hs_code }} </div>
</div> <div class="edit-calculation-cell-line edit-calculation-cell-subline"
<div class="edit-calculation-cell-line edit-calculation-cell-subline" v-if="premise.tariff_rate && premise.tariff_rate > 0">
v-if="premise.tariff_rate && premise.tariff_rate > 0"> Tariff rate:
Tariff rate: {{ toPercent(premise.tariff_rate) }} %
{{ toPercent(premise.tariff_rate) }} % </div>
</div> </div>
</div> </div>
<div class="edit-calculation-cell--price copyable-cell" v-if="showPrice"
@click="action('price')">
<div class="edit-calculation-cell-line">{{ toFixed(premise.material_cost) }} EUR</div> <div class="edit-calculation-cell-container">
<div class="edit-calculation-cell-line edit-calculation-cell-subline">Oversea share: <div class="edit-calculation-cell copyable-cell" @click="action('price')" v-if="showPrice">
{{ toPercent(premise.oversea_share) }} % <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) }} %
</div>
<div class="edit-calculation-cell-line edit-calculation-cell-subline" v-if="premise.is_fca_enabled">
<basic-badge icon="plus" variant="primary">FCA FEE</basic-badge>
</div>
<div class="edit-calculation-cell-line edit-calculation-cell-subline" v-if="showPriceIncomplete">
<basic-badge variant="exception" icon="warning">INCOMPLETE</basic-badge>
</div>
</div> </div>
<div class="edit-calculation-cell-line edit-calculation-cell-subline" v-if="premise.is_fca_enabled"> <div class="edit-calculation-empty copyable-cell" v-else @click="action('price')">
<basic-badge icon="plus" variant="primary">FCA FEE</basic-badge>
</div>
<div class="edit-calculation-cell-line edit-calculation-cell-subline" v-if="showPriceIncomplete">
<basic-badge variant="exception" icon="warning">INCOMPLETE</basic-badge> <basic-badge variant="exception" icon="warning">INCOMPLETE</basic-badge>
</div> </div>
</div> </div>
<div class="edit-calculation-empty copyable-cell" v-else @click="action('price')">
<basic-badge variant="exception" icon="warning">INCOMPLETE</basic-badge>
</div> <div class="edit-calculation-cell-container">
<div v-if="showHu" class="edit-calculation-cell edit-calculation-cell--packaging copyable-cell" <div v-if="showHu" class="edit-calculation-cell copyable-cell"
@click="action('packaging')"> @click="action('packaging')">
<div class="edit-calculation-cell-line"> <div class="edit-calculation-cell-line">
<PhVectorThree/> <PhVectorThree/>
{{ premise.handling_unit.length }} x {{ premise.handling_unit.length }} x
{{ premise.handling_unit.width }} x {{ premise.handling_unit.width }} x
{{ premise.handling_unit.height }} {{ premise.handling_unit.dimension_unit }} {{ premise.handling_unit.height }} {{ premise.handling_unit.dimension_unit }}
</div>
<div class="edit-calculation-cell-line edit-calculation-cell-subline">
<PhBarbell/>
<span>{{ premise.handling_unit.weight }} {{ premise.handling_unit.weight_unit }}</span>
</div>
<div class="edit-calculation-cell-line edit-calculation-cell-subline">
<PhHash/>
{{ premise.handling_unit.content_unit_count }} pcs.
</div>
<div class="edit-calculation-packaging-badges">
<basic-badge v-if="premise.is_stackable" variant="primary" icon="stack">STACKABLE</basic-badge>
<basic-badge v-if="premise.is_mixable" variant="skeleton" icon="shuffle">MIXABLE</basic-badge>
</div>
</div> </div>
<div class="edit-calculation-cell-line edit-calculation-cell-subline"> <div class="edit-calculation-empty copyable-cell" v-else
<PhBarbell/> @click="action('packaging')">
<span>{{ premise.handling_unit.weight }} {{ premise.handling_unit.weight_unit }}</span> <basic-badge variant="exception" icon="warning">INCOMPLETE</basic-badge>
</div>
<div class="edit-calculation-cell-line edit-calculation-cell-subline">
<PhHash/>
{{ premise.handling_unit.content_unit_count }} pcs.
</div>
<div class="edit-calculation-packaging-badges">
<basic-badge v-if="premise.is_stackable" variant="primary" icon="stack">STACKABLE</basic-badge>
<basic-badge v-if="premise.is_mixable" variant="skeleton" icon="shuffle">MIXABLE</basic-badge>
</div> </div>
</div> </div>
<div class="edit-calculation-empty copyable-cell" v-else
@click="action('packaging')">
<basic-badge variant="exception" icon="warning">INCOMPLETE</basic-badge>
</div>
<div class="edit-calculation-cell--supplier copyable-cell" <div class="edit-calculation-cell-container">
@click="action('supplier')"> <div class="edit-calculation-cell" v-if="premise.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 class="calculation-list-supplier-data"> <div class="calculation-list-supplier-data">
<div class="edit-calculation-cell-line">{{ premise.supplier.name }}</div> <div class="edit-calculation-cell-line">{{ premise.supplier.name }}</div>
<div class="edit-calculation-cell-subline"> {{ premise.supplier.address }}</div> <div class="edit-calculation-cell-subline"> {{ premise.supplier.address }}</div>
</div> </div>
</div> </div>
</div> </div>
<div class="edit-calculation-cell--destination copyable-cell" v-if="showDestinations"
@click="action('destinations')">
<div class="edit-calculation-cell-line"> <div class="edit-calculation-cell-container">
<span class="number-circle"> {{ destinationsCount }} </span> Destinations <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
</div>
<div class="edit-calculation-cell-subline" v-for="name in destinationNames"> {{ name }}</div>
<div class="edit-calculation-cell-subline" v-if="showDestinationIncomplete">
<basic-badge variant="exception" icon="warning">INCOMPLETE</basic-badge>
</div>
</div> </div>
<div class="edit-calculation-cell-subline" v-for="name in destinationNames"> {{ name }}</div> <div class="edit-calculation-empty" v-else-if="showMassEdit">
<div class="edit-calculation-cell-subline" v-if="showDestinationIncomplete"> <spinner></spinner>
</div>
<div class="edit-calculation-empty copyable-cell" v-else
@click="action('destinations')">
<basic-badge variant="exception" icon="warning">INCOMPLETE</basic-badge> <basic-badge variant="exception" icon="warning">INCOMPLETE</basic-badge>
</div> </div>
</div> </div>
<div class="edit-calculation-empty" v-else-if="showMassEdit">
<spinner></spinner>
</div>
<div class="edit-calculation-empty copyable-cell" v-else
@click="action('destinations')">
<basic-badge variant="exception" icon="warning">INCOMPLETE</basic-badge>
</div>
<div class="edit-calculation-actions-cell"> <div class="edit-calculation-actions-cell">
<icon-button icon="pencil-simple" help-text="Edit this calculation" help-text-position="left" <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(', '); return this.premise.destinations.map(d => d.destination_node.name).join(', ');
}, },
destinationNames() { 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); 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 ...'); names.push('and more ...');
} }
@ -216,18 +229,33 @@ export default {
<style scoped> <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 { .copyable-cell {
padding: 0.8rem; padding: 0.8rem;
border-radius: 0.8rem; border-radius: 0.8rem;
height: 90%;
} }
/* Standard hover ohne copy mode */ /* Standard hover ohne copy mode */
.copyable-cell:hover { .copyable-cell:hover {
cursor: pointer; cursor: pointer;
background-color: rgba(107, 134, 156, 0.05); background-color: #f8fafc;
border-radius: 0.8rem; border-radius: 0.8rem;
box-shadow: 0 0.4rem 0.6rem -0.1rem rgba(0, 0, 0, 0.1);
} }
.bulk-edit-row { .bulk-edit-row {
@ -254,31 +282,6 @@ export default {
justify-content: center; 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 { .edit-calculation-cell--price {
display: flex; display: flex;
@ -289,11 +292,11 @@ export default {
.edit-calculation-cell--supplier { .edit-calculation-cell--supplier {
display: flex; display: flex;
gap: 1.2rem; gap: 1.2rem;
height: 90%;
} }
.edit-calculation-cell--supplier-container { .edit-calculation-cell--supplier-container {
display: flex; display: flex;
height: fit-content;
gap: 0.8rem; gap: 0.8rem;
} }
@ -325,15 +328,7 @@ export default {
gap: 0.8rem; 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 { .edit-calculation-actions-cell {
display: flex; display: flex;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,10 +2,12 @@ import {defineStore} from 'pinia'
import {config} from '@/config' import {config} from '@/config'
import {useErrorStore} from "@/store/error.js"; import {useErrorStore} from "@/store/error.js";
import performRequest from "@/backend.js"; import performRequest from "@/backend.js";
import logger from "@/logger.js";
export const usePremiseStore = defineStore('premise', { export const usePremiseStore = defineStore('premise', {
state: () => ({ state: () => ({
premises: [], premises: [],
selectedIds: [],
loading: false, loading: false,
empty: true, empty: true,
error: null, error: null,
@ -16,17 +18,74 @@ export const usePremiseStore = defineStore('premise', {
getById: (state) => { getById: (state) => {
return (id) => state.premises.find(p => p.id === id) 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, showData: (state) => !state.loading && state.empty === false,
showLoading: (state) => state.loading, showLoading: (state) => state.loading,
showEmpty: (state) => !state.loading && state.empty === true, showEmpty: (state) => !state.loading && state.empty === true,
selectedIds: state => state.premises?.filter(p => p.checked).map(p => p.id) ?? [],
}, },
actions: { 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.query = query;
this.updatePremises(); await this.updatePremises();
}, },
async deletePremisses(ids) { async deletePremisses(ids) {
ids.forEach(id => this.setChecked(id, false));
const params = new URLSearchParams(); const params = new URLSearchParams();
params.append('premissIds', ids.join(', ')); params.append('premissIds', ids.join(', '));
await performRequest(this, 'POST', `${config.backendUrl}/calculation/delete/${params.size === 0 ? '' : '?'}${params.toString()}`, null, false); 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) if (this.query.done)
params.append('done', this.query.done); params.append('done', this.query.done);
if (this.query.deleted) if (this.query.draft)
params.append('deleted', this.query.deleted); 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 url = `${config.backendUrl}/calculation/view/${params.size === 0 ? '' : '?'}${params.toString()}`;
const resp = await performRequest(this, 'GET', url, null, true); const {data: data, headers: headers} = await performRequest(this, 'GET', url, null, true);
const data = resp.data;
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.loading = false;
this.empty = data.length === 0; this.empty = data.length === 0;
data.forEach(p => p.checked = false);
this.premises = data; this.premises = data;
} }

View file

@ -255,9 +255,9 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
if (ids.includes(p.id)) { if (ids.includes(p.id)) {
return { return {
...p, ...p,
material_cost: priceData.price, ...(priceData.price !== null && {material_cost: priceData.price}),
oversea_share: priceData.overSeaShare, ...(priceData.overSeaShare !== null && {oversea_share: priceData.overSeaShare}),
is_fca_enabled: priceData.includeFcaFee ...(priceData.includeFcaFee !== null && {is_fca_enabled: priceData.includeFcaFee})
}; };
} }
return p; return p;
@ -273,10 +273,10 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
...p, ...p,
material: { material: {
...p.material, ...p.material,
part_number: materialData.partNumber, ...(materialData.partNumber !== null && {part_number: materialData.partNumber}),
hs_code: materialData.hsCode ...(materialData.hsCode !== null && {hs_code: materialData.hsCode})
}, },
tariff_rate: materialData.tariffRate ...(materialData.tariffRate !== null && {tariff_rate: materialData.tariffRate})
}; };
} }
return p; return p;
@ -292,16 +292,16 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
...p, ...p,
handling_unit: { handling_unit: {
...p.handling_unit, ...p.handling_unit,
weight: packagingData.weight, ...(packagingData.weight !== null && {weight: packagingData.weight}),
width: packagingData.width, ...(packagingData.width !== null && {width: packagingData.width}),
length: packagingData.length, ...(packagingData.length !== null && {length: packagingData.length}),
height: packagingData.height, ...(packagingData.height !== null && {height: packagingData.height}),
weight_unit: packagingData.weightUnit, ...(packagingData.weightUnit !== null && {weight_unit: packagingData.weightUnit}),
dimension_unit: packagingData.dimensionUnit, ...(packagingData.dimensionUnit !== null && {dimension_unit: packagingData.dimensionUnit}),
content_unit_count: packagingData.unitCount ...(packagingData.unitCount !== null && {content_unit_count: packagingData.unitCount})
}, },
is_stackable: packagingData.stackable, ...(packagingData.stackable !== null && {is_stackable: packagingData.stackable}),
is_mixable: packagingData.mixable ...(packagingData.mixable !== null && {is_mixable: packagingData.mixable})
}; };
} }
return p; return p;
@ -316,7 +316,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
const url = `${config.backendUrl}/calculation/start/`; const url = `${config.backendUrl}/calculation/start/`;
let error = null; let error = null;
await performRequest(this,'PUT', url, body, false, 'Premiss validation error').catch(e => { await performRequest(this, 'PUT', url, body, false, 'Premiss validation error').catch(e => {
console.log("startCalculation exception", e.errorObj); console.log("startCalculation exception", e.errorObj);
error = e.errorObj; error = e.errorObj;
}) })
@ -371,7 +371,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
}; };
const url = `${config.backendUrl}/calculation/destination/${toDest.id}`; const url = `${config.backendUrl}/calculation/destination/${toDest.id}`;
performRequest(this,'PUT', url, body, false); performRequest(this, 'PUT', url, body, false);
}); });
}); });
@ -396,7 +396,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
const body = {destinations: destinations, premise_id: this.destinations.premise_ids}; const body = {destinations: destinations, premise_id: this.destinations.premise_ids};
const url = `${config.backendUrl}/calculation/destination/`; const url = `${config.backendUrl}/calculation/destination/`;
const { data: data, headers: headers } = await performRequest(this,'PUT', url, body).catch(e => { const {data: data, headers: headers} = await performRequest(this, 'PUT', url, body).catch(e => {
this.destinations = null; this.destinations = null;
this.processDestinationMassEdit = false; this.processDestinationMassEdit = false;
}); });
@ -512,7 +512,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
logger.info(body) logger.info(body)
const url = `${config.backendUrl}/calculation/destination/${toDest.id}`; const url = `${config.backendUrl}/calculation/destination/${toDest.id}`;
await performRequest(this,'PUT', url, body, false); await performRequest(this, 'PUT', url, body, false);
} }
@ -545,7 +545,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
const origId = id.substring(1); const origId = id.substring(1);
const url = `${config.backendUrl}/calculation/destination/${origId}`; const url = `${config.backendUrl}/calculation/destination/${origId}`;
await performRequest(this,'DELETE', url, null, false).catch(async e => { await performRequest(this, 'DELETE', url, null, false).catch(async e => {
logger.error("Unable to delete destination: " + origId + ""); logger.error("Unable to delete destination: " + origId + "");
logger.error(e); logger.error(e);
await this.loadPremissesIfNeeded(this.premisses.map(p => p.id)); await this.loadPremissesIfNeeded(this.premisses.map(p => p.id));
@ -605,7 +605,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
const url = `${config.backendUrl}/calculation/destination/`; const url = `${config.backendUrl}/calculation/destination/`;
const {data: destinations } = await performRequest(this,'POST', url, body).catch(e => { const {data: destinations} = await performRequest(this, 'POST', url, body).catch(e => {
this.loading = false; this.loading = false;
this.selectedLoading = false; this.selectedLoading = false;
throw e; throw e;
@ -666,7 +666,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
body.premise_id = toBeUpdated; body.premise_id = toBeUpdated;
logger.info(url, body) logger.info(url, body)
const {data: data} = await performRequest(this,'PUT', url, body).catch(e => { const {data: data} = await performRequest(this, 'PUT', url, body).catch(e => {
this.loading = false; this.loading = false;
}); });
@ -711,7 +711,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
is_fca_enabled: toBeUpdated[0].is_fca_enabled is_fca_enabled: toBeUpdated[0].is_fca_enabled
}; };
await performRequest(this,'POST', `${config.backendUrl}/calculation/price/`, body, false).catch(e => { await performRequest(this, 'POST', `${config.backendUrl}/calculation/price/`, body, false).catch(e => {
success = false; success = false;
}) })
@ -746,7 +746,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
}; };
await performRequest(this,'POST', `${config.backendUrl}/calculation/packaging/`, body, false).catch(() => { await performRequest(this, 'POST', `${config.backendUrl}/calculation/packaging/`, body, false).catch(() => {
success = false; success = false;
}) })
@ -767,7 +767,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
tariff_rate: toBeUpdated[0].tariff_rate, tariff_rate: toBeUpdated[0].tariff_rate,
}; };
await performRequest(this,'POST', `${config.backendUrl}/calculation/material/`, body, false).catch(() => { await performRequest(this, 'POST', `${config.backendUrl}/calculation/material/`, body, false).catch(() => {
success = false; success = false;
}) })
@ -835,7 +835,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
params.append('premissIds', `${[id]}`); params.append('premissIds', `${[id]}`);
const url = `${config.backendUrl}/calculation/edit/${params.size === 0 ? '' : '?'}${params.toString()}`; const url = `${config.backendUrl}/calculation/edit/${params.size === 0 ? '' : '?'}${params.toString()}`;
const { data: data, headers: headers } = await performRequest(this,'GET', url, null).catch(e => { const {data: data, headers: headers} = await performRequest(this, 'GET', url, null).catch(e => {
this.selectedLoading = false; this.selectedLoading = false;
this.loading = false; this.loading = false;
}); });
@ -862,7 +862,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
params.append('premissIds', ids.join(', ')); params.append('premissIds', ids.join(', '));
const url = `${config.backendUrl}/calculation/edit/${params.size === 0 ? '' : '?'}${params.toString()}`; const url = `${config.backendUrl}/calculation/edit/${params.size === 0 ? '' : '?'}${params.toString()}`;
const { data: data, headers: headers } = await performRequest(this,'GET', url, null).catch(e => { const {data: data, headers: headers} = await performRequest(this, 'GET', url, null).catch(e => {
this.loading = false; this.loading = false;
}); });
this.premisses = data; this.premisses = data;

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.CalculationStatus;
import de.avatic.lcc.dto.calculation.DestinationDTO; import de.avatic.lcc.dto.calculation.DestinationDTO;
import de.avatic.lcc.dto.calculation.PremiseDTO; 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.CreatePremiseDTO;
import de.avatic.lcc.dto.calculation.create.PremiseSearchResultDTO; import de.avatic.lcc.dto.calculation.create.PremiseSearchResultDTO;
import de.avatic.lcc.dto.calculation.edit.PremiseDetailDTO; import de.avatic.lcc.dto.calculation.edit.PremiseDetailDTO;
@ -65,11 +66,11 @@ public class PremiseController {
@RequestParam(defaultValue = "20") @Min(1) int limit, @RequestParam(defaultValue = "20") @Min(1) int limit,
@RequestParam(defaultValue = "1") @Min(1) int page, @RequestParam(defaultValue = "1") @Min(1) int page,
@RequestParam(name = "user", required = false) Integer userId, @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 archived,
@RequestParam(required = false) Boolean done) { @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() return ResponseEntity.ok()
.header("X-Total-Count", String.valueOf(premises.getTotalElements())) .header("X-Total-Count", String.valueOf(premises.getTotalElements()))
@ -106,6 +107,17 @@ public class PremiseController {
dto.createEmpty())); 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/"}) @PostMapping({"/delete", "/delete/"})
@PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')") @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 { public enum PremiseState {
DRAFT, COMPLETED, ARCHIVED, DELETED DRAFT, COMPLETED, ARCHIVED
} }

View file

@ -61,6 +61,17 @@ public class CalculationJobRepository {
return Optional.of(job.getFirst()); 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 @Transactional
public Optional<CalculationJob> getCalculationJobWithJobStateValid(Integer periodId, Integer nodeId, Integer materialId) { 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> { private static class CalculationJobMapper implements RowMapper<CalculationJob> {
@Override @Override

View file

@ -45,11 +45,11 @@ public class PremiseRepository {
@Transactional @Transactional
public SearchQueryResult<PremiseListEntry> listPremises(String filter, SearchQueryPagination pagination, 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() QueryBuilder queryBuilder = new QueryBuilder()
.withFilter(filter) .withFilter(filter)
.withDeleted(deleted) .withDraft(draft)
.withArchived(archived) .withArchived(archived)
.withDone(done); .withDone(done);
@ -428,12 +428,15 @@ public class PremiseRepository {
if (premiseIds == null || premiseIds.isEmpty()) return; if (premiseIds == null || premiseIds.isEmpty()) return;
String placeholders = String.join(",", Collections.nCopies(premiseIds.size(), "?")); String placeholders = String.join(",", Collections.nCopies(premiseIds.size(), "?"));
String query = """ String query = "DELETE FROM premise WHERE id IN (" + placeholders + ") AND state = ?";
DELETE FROM premise
WHERE id IN (""" + placeholders + ") AND state = '" + PremiseState.DRAFT.name() + "'";
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 ?)"; "user_n.name LIKE ? OR m.name LIKE ? OR m.name LIKE ? OR m.part_number LIKE ?)";
private String filter; private String filter;
private Boolean deleted; private Boolean draft;
private Boolean archived; private Boolean archived;
private Boolean done; private Boolean done;
@ -687,8 +690,8 @@ public class PremiseRepository {
return this; return this;
} }
public QueryBuilder withDeleted(Boolean deleted) { public QueryBuilder withDraft(Boolean draft) {
this.deleted = deleted; this.draft = draft;
return this; 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' 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); """).append(BASE_JOIN_QUERY);
appendConditions(queryBuilder); appendConditions(queryBuilder);
queryBuilder.append(" ORDER BY p.updated_at DESC"); queryBuilder.append(" ORDER BY p.updated_at, p.id DESC");
queryBuilder.append(" LIMIT ? OFFSET ?"); queryBuilder.append(" LIMIT ? OFFSET ?");
return queryBuilder.toString(); return queryBuilder.toString();
} }
@ -735,13 +738,13 @@ public class PremiseRepository {
queryBuilder.append(FILTER_CONDITION); 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; boolean concat = false;
queryBuilder.append(" AND ("); queryBuilder.append(" AND (");
if (deleted != null && deleted) { if (draft != null && draft) {
queryBuilder.append(" p.state").append(" = 'DELETED'"); queryBuilder.append(" p.state").append(" = 'DRAFT'");
concat = true; concat = true;
} }

View file

@ -1,7 +1,7 @@
package de.avatic.lcc.service.access; 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.CalculationStatus;
import de.avatic.lcc.dto.calculation.ResolvePremiseDTO;
import de.avatic.lcc.dto.calculation.PremiseDTO; import de.avatic.lcc.dto.calculation.PremiseDTO;
import de.avatic.lcc.dto.calculation.edit.PremiseDetailDTO; import de.avatic.lcc.dto.calculation.edit.PremiseDetailDTO;
import de.avatic.lcc.dto.calculation.edit.masterData.MaterialUpdateDTO; 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 de.avatic.lcc.util.exception.base.InternalErrorException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionException;
@ -83,10 +82,10 @@ public class PremisesService {
} }
@Transactional(readOnly = true) @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(); var admin = authorizationService.isSuper();
userId = authorizationService.getUserId(); 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) @Transactional(readOnly = true)
@ -112,16 +111,31 @@ public class PremisesService {
var calculationIds = new ArrayList<Integer>(); var calculationIds = new ArrayList<Integer>();
premises.forEach(p -> { premises.forEach(p -> {
CalculationJob job = new CalculationJob();
job.setPremiseId(p); var existingJob = calculationJobRepository.findJob(p, validSetId, validPeriodId);
job.setJobState(CalculationJobState.CREATED);
job.setCalculationDate(java.time.LocalDateTime.now()); if (existingJob.isPresent()) {
job.setUserId(userId); if (CalculationJobState.EXCEPTION == existingJob.get().getJobState()) {
job.setPropertySetId(validSetId); calculationJobRepository.setStateTo(existingJob.get().getId(), CalculationJobState.CREATED);
job.setValidityPeriodId(validPeriodId); 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);
job.setJobState(CalculationJobState.CREATED);
job.setCalculationDate(java.time.LocalDateTime.now());
job.setUserId(userId);
job.setPropertySetId(validSetId);
job.setValidityPeriodId(validPeriodId);
calculationIds.add(calculationJobRepository.insert(job));
}
calculationIds.add(calculationJobRepository.insert(job));
}); });
premiseRepository.setStatus(premises, PremiseState.COMPLETED); premiseRepository.setStatus(premises, PremiseState.COMPLETED);
@ -136,7 +150,7 @@ public class PremisesService {
var jobResult = future.get(); var jobResult = future.get();
if (jobResult.getState().equals(CalculationJobState.EXCEPTION)) { if (jobResult.getState().equals(CalculationJobState.EXCEPTION)) {
calculationJobRepository.setStateTo(jobResult.getJobId(), CalculationJobState.EXCEPTION); calculationJobRepository.setStateTo(jobResult.getJobId(), CalculationJobState.EXCEPTION);
throw new InternalErrorException("Execution of calculation was not successful. Please contact Administrator.",jobResult.getException().getMessage(), new Exception(jobResult.getException())); throw new InternalErrorException("Execution of calculation was not successful. Please contact Administrator.", jobResult.getException().getMessage(), new Exception(jobResult.getException()));
} else { } else {
postCalculationCheckService.doPostcheck(jobResult); postCalculationCheckService.doPostcheck(jobResult);
@ -157,8 +171,6 @@ public class PremisesService {
} }
} }
} catch (CompletionException | InterruptedException | ExecutionException e) { } catch (CompletionException | InterruptedException | ExecutionException e) {
throw new InternalErrorException("Calculation execution failed.", e); throw new InternalErrorException("Calculation execution failed.", e);
} }
@ -214,8 +226,7 @@ public class PremisesService {
// only delete drafts. // only delete drafts.
var toBeDeleted = premiseRepository.getPremisesById(premiseIds).stream().filter(p -> p.getState().equals(PremiseState.DRAFT)).map(Premise::getId).toList(); 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); destinationService.deleteAllDestinationsByPremiseId(toBeDeleted, false);
premiseRepository.deletePremisesById(toBeDeleted); premiseRepository.deletePremisesById(toBeDeleted);
} }
@ -230,4 +241,49 @@ public class PremisesService {
var premisses = premiseRepository.getPremisesById(premiseIds); var premisses = premiseRepository.getPremisesById(premiseIds);
premiseRepository.setStatus(premisses.stream().filter(p -> p.getState().equals(PremiseState.COMPLETED)).map(Premise::getId).toList(), PremiseState.ARCHIVED); 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.dto.generic.ValidityPeriodDTO;
import de.avatic.lcc.model.properties.SystemPropertyMappingId; import de.avatic.lcc.model.properties.SystemPropertyMappingId;
import de.avatic.lcc.model.rates.ValidityPeriodState; 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.PropertyRepository;
import de.avatic.lcc.repositories.properties.PropertySetRepository; import de.avatic.lcc.repositories.properties.PropertySetRepository;
import de.avatic.lcc.service.transformer.rates.ValidityPeriodTransformer; import de.avatic.lcc.service.transformer.rates.ValidityPeriodTransformer;
@ -36,6 +37,7 @@ public class PropertyService {
private final PropertySetRepository propertySetRepository; private final PropertySetRepository propertySetRepository;
private final ValidityPeriodTransformer validityPeriodTransformer; private final ValidityPeriodTransformer validityPeriodTransformer;
private final PropertyValidationService propertyValidationService; private final PropertyValidationService propertyValidationService;
private final CalculationJobRepository calculationJobRepository;
/** /**
@ -45,11 +47,12 @@ public class PropertyService {
* @param propertySetRepository the repository to manage property sets * @param propertySetRepository the repository to manage property sets
* @param validityPeriodTransformer the transformer to convert property sets to validity period DTOs * @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.propertyRepository = propertyRepository;
this.propertySetRepository = propertySetRepository; this.propertySetRepository = propertySetRepository;
this.validityPeriodTransformer = validityPeriodTransformer; this.validityPeriodTransformer = validityPeriodTransformer;
this.propertyValidationService = propertyValidationService; this.propertyValidationService = propertyValidationService;
this.calculationJobRepository = calculationJobRepository;
} }
/** /**
@ -116,6 +119,7 @@ public class PropertyService {
if (!propertySetRepository.invalidateById(id)) if (!propertySetRepository.invalidateById(id))
throw new NotFoundException(NotFoundType.EXPIRED_PROPERTY_SET, "id", id.toString()); 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; package de.avatic.lcc.service.access;
import de.avatic.lcc.dto.generic.ValidityPeriodDTO; 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.repositories.rates.ValidityPeriodRepository;
import de.avatic.lcc.service.transformer.rates.ValidityPeriodTransformer; import de.avatic.lcc.service.transformer.rates.ValidityPeriodTransformer;
import de.avatic.lcc.util.exception.badrequest.NotFoundException; import de.avatic.lcc.util.exception.badrequest.NotFoundException;
@ -22,6 +23,7 @@ public class ValidityPeriodService {
private final ValidityPeriodRepository validityPeriodRepository; private final ValidityPeriodRepository validityPeriodRepository;
private final ValidityPeriodTransformer validityPeriodTransformer; private final ValidityPeriodTransformer validityPeriodTransformer;
private final CalculationJobRepository calculationJobRepository;
/** /**
* Constructs a new ValidityPeriodService with the required dependencies. * Constructs a new ValidityPeriodService with the required dependencies.
@ -29,9 +31,10 @@ public class ValidityPeriodService {
* @param validityPeriodRepository Repository for accessing validity period data * @param validityPeriodRepository Repository for accessing validity period data
* @param validityPeriodTransformer Transformer for converting between entity and DTO representations * @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.validityPeriodRepository = validityPeriodRepository;
this.validityPeriodTransformer = validityPeriodTransformer; this.validityPeriodTransformer = validityPeriodTransformer;
this.calculationJobRepository = calculationJobRepository;
} }
@ -60,5 +63,6 @@ public class ValidityPeriodService {
if (!validityPeriodRepository.invalidateById(id)) if (!validityPeriodRepository.invalidateById(id))
throw new NotFoundException(NotFoundException.NotFoundType.EXPIRED_VALIDITY_PERIOD, "id", id.toString()); 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(); return premiseRepository.getPremisesById(premises.stream().map(TemporaryPremise::getId).toList()).stream().map(premiseTransformer::toPremiseDetailDTO).toList();
} }
private void copyPremise(TemporaryPremise p, Integer userId) { private void copyPremise(TemporaryPremise p, Integer userId) {
var old = p.getPremise(); 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 (packaging_id) REFERENCES packaging (id),
FOREIGN KEY (user_id) REFERENCES sys_user (id), FOREIGN KEY (user_id) REFERENCES sys_user (id),
CONSTRAINT `chk_premise_state_values` CHECK (`state` IN 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 CONSTRAINT `chk_premise_displayed_dimension_unit` CHECK (`hu_displayed_dimension_unit` IN
('MM', 'CM', 'M')), ('MM', 'CM', 'M')),
CONSTRAINT `chk_premise_displayed_weight_unit` CHECK (`hu_displayed_weight_unit` IN CONSTRAINT `chk_premise_displayed_weight_unit` CHECK (`hu_displayed_weight_unit` IN