intermediate commit. optical adjustments to mass edit ... functional adjustments pending
This commit is contained in:
parent
9ef5fe9fc7
commit
3658372271
21 changed files with 934 additions and 345 deletions
99
.gitea/workflows/auto-tag.yml
Normal file
99
.gitea/workflows/auto-tag.yml
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
name: Auto-Tag Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
tag-release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # Alle Tags holen
|
||||||
|
|
||||||
|
- name: Determine version bump
|
||||||
|
id: bump_type
|
||||||
|
run: |
|
||||||
|
# Hole die letzten Commits seit dem letzten Tag
|
||||||
|
LATEST_TAG=$(git tag -l "v*" --sort=-v:refname | head -n 1)
|
||||||
|
|
||||||
|
if [ -z "$LATEST_TAG" ]; then
|
||||||
|
echo "bump=minor" >> $GITHUB_OUTPUT
|
||||||
|
echo "Kein Tag vorhanden, starte mit minor"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Analysiere Commits seit letztem Tag
|
||||||
|
COMMITS=$(git log ${LATEST_TAG}..HEAD --pretty=format:"%s")
|
||||||
|
|
||||||
|
echo "=== Commits seit ${LATEST_TAG} ==="
|
||||||
|
echo "$COMMITS"
|
||||||
|
echo "=================================="
|
||||||
|
|
||||||
|
# Prüfe auf Breaking Changes / Major Updates
|
||||||
|
if echo "$COMMITS" | grep -qiE "^BREAKING CHANGE:|^[^:]+!:|breaking:|major:"; then
|
||||||
|
echo "bump=major" >> $GITHUB_OUTPUT
|
||||||
|
echo "✓ Breaking Change gefunden → MAJOR"
|
||||||
|
# Prüfe auf Features / Minor Updates
|
||||||
|
elif echo "$COMMITS" | grep -qiE "^feat:|^feature:|minor:"; then
|
||||||
|
echo "bump=minor" >> $GITHUB_OUTPUT
|
||||||
|
echo "✓ Feature gefunden → MINOR"
|
||||||
|
# Prüfe auf Bugfixes
|
||||||
|
elif echo "$COMMITS" | grep -qiE "^fix:|bugfix:"; then
|
||||||
|
echo "bump=patch" >> $GITHUB_OUTPUT
|
||||||
|
echo "✓ Bugfix gefunden → PATCH"
|
||||||
|
# Prüfe auf Chores/Docs/etc
|
||||||
|
elif echo "$COMMITS" | grep -qiE "^chore:|^docs:|^style:|^refactor:|^test:|^build:|^ci:"; then
|
||||||
|
echo "bump=patch" >> $GITHUB_OUTPUT
|
||||||
|
echo "✓ Chore/Docs gefunden → PATCH"
|
||||||
|
# Fallback: Kein Pattern erkannt → PATCH
|
||||||
|
else
|
||||||
|
echo "bump=patch" >> $GITHUB_OUTPUT
|
||||||
|
echo "⚠ Kein Pattern erkannt → PATCH (Fallback)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Calculate new tag
|
||||||
|
id: get_tag
|
||||||
|
run: |
|
||||||
|
LATEST_TAG=$(git tag -l "v*" --sort=-v:refname | head -n 1)
|
||||||
|
|
||||||
|
if [ -z "$LATEST_TAG" ]; then
|
||||||
|
NEW_TAG="v1.0.0"
|
||||||
|
else
|
||||||
|
VERSION=${LATEST_TAG#v}
|
||||||
|
MAJOR=$(echo $VERSION | cut -d. -f1)
|
||||||
|
MINOR=$(echo $VERSION | cut -d. -f2)
|
||||||
|
PATCH=$(echo $VERSION | cut -d. -f3)
|
||||||
|
|
||||||
|
case ${{ steps.bump_type.outputs.bump }} in
|
||||||
|
major)
|
||||||
|
MAJOR=$((MAJOR + 1))
|
||||||
|
MINOR=0
|
||||||
|
PATCH=0
|
||||||
|
;;
|
||||||
|
minor)
|
||||||
|
MINOR=$((MINOR + 1))
|
||||||
|
PATCH=0
|
||||||
|
;;
|
||||||
|
patch)
|
||||||
|
PATCH=$((PATCH + 1))
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
NEW_TAG="v${MAJOR}.${MINOR}.${PATCH}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "new_tag=${NEW_TAG}" >> $GITHUB_OUTPUT
|
||||||
|
echo "Neues Tag: ${NEW_TAG}"
|
||||||
|
|
||||||
|
- name: Create and push tag
|
||||||
|
run: |
|
||||||
|
git config user.name "Gitea Actions"
|
||||||
|
git config user.email "actions@gitea.local"
|
||||||
|
|
||||||
|
git tag -a ${{ steps.get_tag.outputs.new_tag }} -m "Release ${{ steps.get_tag.outputs.new_tag }}"
|
||||||
|
git push origin ${{ steps.get_tag.outputs.new_tag }}
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/favicon.ico">
|
<link rel="icon" href="/favicon.ico">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>LCC</title>
|
<title>Logistics Cost Calculation Tool</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ export default {
|
||||||
|
|
||||||
html {
|
html {
|
||||||
font-size: 62.5%;
|
font-size: 62.5%;
|
||||||
font-family: 'Poppins', sans-serif;
|
font-family: 'Arial', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,11 @@ export default{
|
||||||
type: String,
|
type: String,
|
||||||
default: 'primary',
|
default: 'primary',
|
||||||
validator: (value) => ['primary', 'secondary', 'grey', 'exception', 'skeleton'].includes(value)
|
validator: (value) => ['primary', 'secondary', 'grey', 'exception', 'skeleton'].includes(value)
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: 'default',
|
||||||
|
validator: (value) => ['default', 'compact'].includes(value)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -31,7 +36,8 @@ export default{
|
||||||
},
|
},
|
||||||
batchClasses() {
|
batchClasses() {
|
||||||
return [
|
return [
|
||||||
`batch--${this.variant}`
|
`batch--${this.variant}`,
|
||||||
|
`batch--${this.size}`
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
iconComponent() {
|
iconComponent() {
|
||||||
|
|
@ -65,6 +71,12 @@ export default{
|
||||||
height: 2.4rem;
|
height: 2.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.batch-container.batch--compact {
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
|
gap: 0.4rem;
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
.batch--primary {
|
.batch--primary {
|
||||||
background-color: #5AF0B4;
|
background-color: #5AF0B4;
|
||||||
color: #002F54;
|
color: #002F54;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
<div class="checkbox-container">
|
<div class="checkbox-container">
|
||||||
<label class="checkbox-item" :class="{ disabled: disabled }" @change="setFilter">
|
<label class="checkbox-item" :class="{ disabled: disabled }" @change="setFilter">
|
||||||
<input
|
<input
|
||||||
|
@keydown.enter="$emit('enter', $event)"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:checked="isChecked"
|
:checked="isChecked"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
|
|
@ -72,6 +73,9 @@ export default{
|
||||||
this.updateIndeterminateState(this.isIndeterminate);
|
this.updateIndeterminateState(this.isIndeterminate);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
focus() {
|
||||||
|
this.$refs.checkboxInput?.focus();
|
||||||
|
},
|
||||||
setFilter(event) {
|
setFilter(event) {
|
||||||
if (this.disabled) return;
|
if (this.disabled) return;
|
||||||
this.isChecked = event.target.checked;
|
this.isChecked = event.target.checked;
|
||||||
|
|
|
||||||
127
src/frontend/src/components/UI/CircleBadge.vue
Normal file
127
src/frontend/src/components/UI/CircleBadge.vue
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
<template>
|
||||||
|
<div class="circle-badge" :class="circleClasses">
|
||||||
|
<component
|
||||||
|
:is="iconComponent"
|
||||||
|
weight="bold"
|
||||||
|
:size="iconSize"
|
||||||
|
class="circle-icon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default{
|
||||||
|
name: "CircleBadge",
|
||||||
|
props:{
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
default: 'check'
|
||||||
|
},
|
||||||
|
variant: {
|
||||||
|
type: String,
|
||||||
|
default: 'primary',
|
||||||
|
validator: (value) => ['primary', 'secondary', 'grey', 'exception', 'skeleton', 'skeleton-grey'].includes(value)
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: 'small',
|
||||||
|
validator: (value) => ['small', 'medium', 'large'].includes(value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
circleClasses() {
|
||||||
|
return [
|
||||||
|
`circle-badge--${this.variant}`,
|
||||||
|
`circle-badge--${this.size}`
|
||||||
|
]
|
||||||
|
},
|
||||||
|
iconComponent() {
|
||||||
|
const iconName = this.icon.charAt(0).toUpperCase() + this.icon.slice(1);
|
||||||
|
return `Ph${iconName}`;
|
||||||
|
},
|
||||||
|
iconSize() {
|
||||||
|
const sizes = {
|
||||||
|
small: 12,
|
||||||
|
medium: 16,
|
||||||
|
large: 20
|
||||||
|
};
|
||||||
|
return sizes[this.size];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
.circle-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 0.2rem solid transparent;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
outline: none;
|
||||||
|
user-select: none;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Größen */
|
||||||
|
.circle-badge--small {
|
||||||
|
width: 2.4rem;
|
||||||
|
height: 2.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle-badge--medium {
|
||||||
|
width: 3.2rem;
|
||||||
|
height: 3.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle-badge--large {
|
||||||
|
width: 4rem;
|
||||||
|
height: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Varianten */
|
||||||
|
.circle-badge--primary {
|
||||||
|
background-color: #5AF0B4;
|
||||||
|
color: #002F54;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle-badge--secondary {
|
||||||
|
background-color: #c3cfdf;
|
||||||
|
color: #002F54;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle-badge--exception {
|
||||||
|
background-color: #BC2B72;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle-badge--grey {
|
||||||
|
background-color: #DCDCDC;
|
||||||
|
color: #002F54;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle-badge--skeleton {
|
||||||
|
border: 0.1rem solid #002F54;
|
||||||
|
background-color: transparent;
|
||||||
|
color: #002F54;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle-badge--skeleton-grey {
|
||||||
|
border: 0.1rem solid #6b7280;
|
||||||
|
background-color: transparent;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle-badge--skeleton-primary {
|
||||||
|
border: 0.1rem solid #5AF0B4;
|
||||||
|
color: #5AF0B4;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.circle-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -1,149 +1,249 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="bulk-edit-row">
|
<div class="bulk-edit-row">
|
||||||
<div class="edit-calculation-checkbox-cell">
|
<div class="bulk-edit-row__checkbox">
|
||||||
<checkbox :checked="isSelected" @checkbox-changed="updateSelected"></checkbox>
|
<checkbox :checked="isSelected" @checkbox-changed="updateSelected"></checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="bulk-edit-row__cell-container">
|
||||||
<div class="edit-calculation-cell-container">
|
<div class="bulk-edit-row__cell bulk-edit-row__cell--status bulk-edit-row__cell--clickable"
|
||||||
<div class="edit-calculation-cell copyable-cell" @click="action('material')">
|
@click.exact="action('material')" @click.ctrl="action('material-select')">
|
||||||
<div class="edit-calculation-cell-line">{{ premise.material.part_number }}</div>
|
<div class="bulk-edit-row__data">
|
||||||
<div class="edit-calculation-cell-line edit-calculation-cell-subline" v-if="premise.hs_code">
|
<div class="bulk-edit-row__line">
|
||||||
HS Code:
|
<ph-package size="16"/>
|
||||||
{{ premise.hs_code }}
|
{{ premise.material.part_number ?? 'N/A' }}
|
||||||
|
</div>
|
||||||
|
<div class="bulk-edit-row__line bulk-edit-row__line--sub">
|
||||||
|
HS:
|
||||||
|
{{ premise.hs_code ?? 'N/A' }}
|
||||||
|
</div>
|
||||||
|
<div class="bulk-edit-row__line bulk-edit-row__line--sub">
|
||||||
|
<ph-hand-coins size="16"/>
|
||||||
|
{{ toPercent(premise.tariff_rate) ?? 'N/A' }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="edit-calculation-cell-line edit-calculation-cell-subline"
|
<div class="bulk-edit-row__status">
|
||||||
v-if="(premise.tariff_rate ?? null) !== null">
|
<transition name="badge-transition" mode="out-in">
|
||||||
Tariff rate:
|
<circle-badge v-if="materialCheck" :key="'check-' + id" variant="skeleton-grey" icon="check"></circle-badge>
|
||||||
{{ toPercent(premise.tariff_rate) }}
|
<circle-badge v-else :key="'error-' + id" variant="exception" icon="exclamation-mark"></circle-badge>
|
||||||
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="bulk-edit-row__cell-container">
|
||||||
<div class="edit-calculation-cell-container">
|
<div class="bulk-edit-row__cell bulk-edit-row__cell--status bulk-edit-row__cell--clickable"
|
||||||
<div class="edit-calculation-cell copyable-cell" @click="action('price')" v-if="showPrice">
|
@click="action('price')">
|
||||||
<div class="edit-calculation-cell-line">{{ toFixed(premise.material_cost) }} EUR</div>
|
<div class="bulk-edit-row__data">
|
||||||
<div class="edit-calculation-cell-line edit-calculation-cell-subline">Oversea share:
|
<div class="bulk-edit-row__line bulk-edit-row__line--sub">
|
||||||
{{ toPercent(premise.oversea_share) }} %
|
<ph-tag size="16"/>
|
||||||
|
{{ toFixed(premise.material_cost, '€') }}
|
||||||
|
</div>
|
||||||
|
<div class="bulk-edit-row__line bulk-edit-row__line--sub">
|
||||||
|
<ph-chart-pie-slice size="16"/>
|
||||||
|
{{ toPercent(premise.oversea_share) }}
|
||||||
|
</div>
|
||||||
|
<div class="bulk-edit-row__line bulk-edit-row__line--sub" v-if="premise.is_fca_enabled">
|
||||||
|
<basic-badge icon="plus" size="compact" variant="primary">FCA</basic-badge>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="edit-calculation-cell-line edit-calculation-cell-subline" v-if="premise.is_fca_enabled">
|
<div class="bulk-edit-row__status">
|
||||||
<basic-badge icon="plus" variant="primary">FCA FEE</basic-badge>
|
<transition name="badge-transition" mode="out-in">
|
||||||
|
<circle-badge v-if="priceCheck" :key="'check-price-' + id" variant="skeleton-grey"
|
||||||
|
icon="check"></circle-badge>
|
||||||
|
<circle-badge v-else :key="'error-price-' + id" variant="exception" icon="exclamation-mark"></circle-badge>
|
||||||
|
</transition>
|
||||||
</div>
|
</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 class="edit-calculation-empty copyable-cell" v-else @click="action('price')">
|
|
||||||
<basic-badge variant="exception" icon="warning">INCOMPLETE</basic-badge>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="bulk-edit-row__cell-container">
|
||||||
<div class="edit-calculation-cell-container">
|
<div class="bulk-edit-row__cell bulk-edit-row__cell--status bulk-edit-row__cell--clickable"
|
||||||
<div v-if="showHu" class="edit-calculation-cell copyable-cell"
|
|
||||||
@click="action('packaging')">
|
@click="action('packaging')">
|
||||||
<div class="edit-calculation-cell-line">
|
<div class="bulk-edit-row__data">
|
||||||
<PhVectorThree/>
|
<div class="bulk-edit-row__line bulk-edit-row__line--sub">
|
||||||
{{ premise.handling_unit.length }} x
|
<PhVectorThree size="16"/>
|
||||||
{{ premise.handling_unit.width }} x
|
{{ toDimension(premise.handling_unit) }}
|
||||||
{{ premise.handling_unit.height }} {{ premise.handling_unit.dimension_unit }}
|
</div>
|
||||||
|
<div class="bulk-edit-row__line bulk-edit-row__line--sub">
|
||||||
|
<PhBarbell size="16"/>
|
||||||
|
<span>{{ toFixed(premise.handling_unit.weight, premise.handling_unit.weight_unit, 0) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="bulk-edit-row__line bulk-edit-row__line--sub">
|
||||||
|
<PhHash size="16"/>
|
||||||
|
{{ toFixed(premise.handling_unit.content_unit_count, 'pcs.', 0) }}
|
||||||
|
</div>
|
||||||
|
<div class="bulk-edit-row__badges">
|
||||||
|
<basic-badge v-if="premise.is_stackable" size="compact" variant="primary" icon="stack">STACKABLE
|
||||||
|
</basic-badge>
|
||||||
|
<basic-badge v-if="premise.is_mixable" size="compact" variant="secondary" icon="shuffle">MIXABLE
|
||||||
|
</basic-badge>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="edit-calculation-cell-line edit-calculation-cell-subline">
|
<div class="bulk-edit-row__status">
|
||||||
<PhBarbell/>
|
<transition name="badge-transition" mode="out-in">
|
||||||
<span>{{ premise.handling_unit.weight }} {{ premise.handling_unit.weight_unit }}</span>
|
<circle-badge v-if="packagingCheck" :key="'check-packaging-' + id" variant="skeleton-grey"
|
||||||
</div>
|
icon="check"></circle-badge>
|
||||||
<div class="edit-calculation-cell-line edit-calculation-cell-subline">
|
<circle-badge v-else :key="'error-packaging-' + id" variant="exception"
|
||||||
<PhHash/>
|
icon="exclamation-mark"></circle-badge>
|
||||||
{{ premise.handling_unit.content_unit_count }} pcs.
|
</transition>
|
||||||
</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 class="edit-calculation-empty copyable-cell" v-else
|
|
||||||
@click="action('packaging')">
|
|
||||||
<basic-badge variant="exception" icon="warning">INCOMPLETE</basic-badge>
|
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="bulk-edit-row__cell-container">
|
||||||
<div class="edit-calculation-cell-container">
|
<div class="bulk-edit-row__cell bulk-edit-row__cell--status bulk-edit-row__cell--clickable"
|
||||||
<div class="edit-calculation-cell copyable-cell" v-if="showDestinations"
|
@click.ctrl="action('supplier-select')">
|
||||||
@click="action('destinations')">
|
<div class="bulk-edit-row__data">
|
||||||
<div class="edit-calculation-cell-line">
|
<div class="bulk-edit-row__line bulk-edit-row__line--sub">
|
||||||
<span class="number-circle"> {{ destinationsCount }} </span> Destinations
|
<ph-factory style="display: inline-block; vertical-align: middle;" size="16"/>
|
||||||
|
{{ premise.supplier.name }}
|
||||||
|
</div>
|
||||||
</div>
|
</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 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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="bulk-edit-row__cell-container">
|
||||||
|
<div
|
||||||
|
class="bulk-edit-row__cell bulk-edit-row__cell--status bulk-edit-row__cell--clickable bulk-edit-row__cell--destinations"
|
||||||
|
@click="action('destinations')">
|
||||||
|
<div class="bulk-edit-row__data bulk-edit-row__data--destinations">
|
||||||
|
<div class="bulk-edit-row__dest-line"
|
||||||
|
v-for="(destination, index) in premise.destinations.slice(0, 3)"
|
||||||
|
:key="index">
|
||||||
|
<div>
|
||||||
|
<ph-stack size="16"/>
|
||||||
|
</div>
|
||||||
|
<div>{{ toFixed(destination.annual_amount, 'pcs.', 0) }}</div>
|
||||||
|
<div>
|
||||||
|
<basic-badge size="compact" variant="secondary">{{ toDestination(destination) }}</basic-badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bulk-edit-row__dest-line" v-if="premise.destinations.length > 3">
|
||||||
|
<div></div>
|
||||||
|
<div> more ...</div>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Expanded destinations overlay -->
|
||||||
|
<div class="bulk-edit-row__destinations-expanded" v-if="premise.destinations.length > 3">
|
||||||
|
<div class="bulk-edit-row__dest-line"
|
||||||
|
v-for="(destination, index) in premise.destinations"
|
||||||
|
:key="index">
|
||||||
|
<div>
|
||||||
|
<ph-stack size="16"/>
|
||||||
|
</div>
|
||||||
|
<div>{{ toFixed(destination.annual_amount, 'pcs.', 0) }}</div>
|
||||||
|
<div>
|
||||||
|
<basic-badge size="compact" variant="secondary">{{ toDestination(destination) }}</basic-badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="edit-calculation-actions-cell">
|
<div class="bulk-edit-row__status">
|
||||||
|
<transition name="badge-transition" mode="out-in">
|
||||||
|
<circle-badge v-if="destinationCheck" :key="'check-dest-' + id" variant="skeleton-grey"
|
||||||
|
icon="check"></circle-badge>
|
||||||
|
<circle-badge v-else :key="'error-dest-' + id" variant="exception" icon="exclamation-mark"></circle-badge>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bulk-edit-row__cell-container">
|
||||||
|
<div
|
||||||
|
class="bulk-edit-row__cell bulk-edit-row__cell--status bulk-edit-row__cell--clickable bulk-edit-row__cell--destinations"
|
||||||
|
@click="action('routes')">
|
||||||
|
<div class="bulk-edit-row__data">
|
||||||
|
<div class="bulk-edit-row__route-line"
|
||||||
|
v-for="(destination, index) in premise.destinations.slice(0, 3)"
|
||||||
|
:key="index">
|
||||||
|
<div>
|
||||||
|
<component :is="toRouteIcon(destination)" size="16"></component>
|
||||||
|
</div>
|
||||||
|
<div>{{ toRoute(destination) }}</div>
|
||||||
|
<div>
|
||||||
|
<basic-badge size="compact" variant="secondary">{{ toDestination(destination, 15) }}</basic-badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bulk-edit-row__route-line" v-if="premise.destinations.length > 3">
|
||||||
|
<div></div>
|
||||||
|
<div> more ...</div>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Expanded destinations overlay -->
|
||||||
|
<div class="bulk-edit-row__destinations-expanded" v-if="premise.destinations.length > 3">
|
||||||
|
<div class="bulk-edit-row__route-line"
|
||||||
|
v-for="(destination, index) in premise.destinations"
|
||||||
|
:key="index">
|
||||||
|
<div>
|
||||||
|
<component :is="toRouteIcon(destination)" size="16"></component>
|
||||||
|
</div>
|
||||||
|
<div>{{ toRoute(destination) }}</div>
|
||||||
|
<div>
|
||||||
|
<basic-badge size="compact" variant="secondary">{{ toDestination(destination, 15) }}</basic-badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bulk-edit-row__status">
|
||||||
|
<transition name="badge-transition" mode="out-in">
|
||||||
|
<circle-badge v-if="destinationCheck" :key="'check-route-' + id" variant="skeleton-grey"
|
||||||
|
icon="check"></circle-badge>
|
||||||
|
<circle-badge v-else :key="'error-route-' + id" variant="exception" icon="exclamation-mark"></circle-badge>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bulk-edit-row__actions">
|
||||||
<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"
|
||||||
@click="editSingle"></icon-button>
|
@click="editSingle"></icon-button>
|
||||||
<icon-button icon="x" help-text="Remove from mass edit" help-text-position="left" @click="remove"></icon-button>
|
<icon-button icon="x" help-text="Remove from mass edit" help-text-position="left" @click="remove"></icon-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Checkbox from "@/components/UI/Checkbox.vue";
|
import Checkbox from "@/components/UI/Checkbox.vue";
|
||||||
import IconButton from "@/components/UI/IconButton.vue";
|
import IconButton from "@/components/UI/IconButton.vue";
|
||||||
import Flag from "@/components/UI/Flag.vue";
|
|
||||||
import {mapStores} from "pinia";
|
import {mapStores} from "pinia";
|
||||||
import {usePremiseEditStore} from "@/store/premiseEdit.js";
|
import {usePremiseEditStore} from "@/store/premiseEdit.js";
|
||||||
import BasicBadge from "@/components/UI/BasicBadge.vue";
|
import BasicBadge from "@/components/UI/BasicBadge.vue";
|
||||||
import {
|
import {
|
||||||
PhBarbell,
|
PhBarbell, PhBoat,
|
||||||
PhBarcode,
|
PhChartPieSlice,
|
||||||
PhEmpty,
|
|
||||||
PhFactory,
|
PhFactory,
|
||||||
|
PhHandCoins,
|
||||||
PhHash,
|
PhHash,
|
||||||
PhMapPin,
|
PhMapPinLine,
|
||||||
PhPercent,
|
PhPackage, PhPath,
|
||||||
PhVectorThree,
|
PhTag, PhTrain, PhTruck,
|
||||||
PhVectorTwo
|
PhVectorThree
|
||||||
} from "@phosphor-icons/vue";
|
} from "@phosphor-icons/vue";
|
||||||
import {UrlSafeBase64} from "@/common.js";
|
import {UrlSafeBase64} from "@/common.js";
|
||||||
import Spinner from "@/components/UI/Spinner.vue";
|
import CircleBadge from "@/components/UI/CircleBadge.vue";
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "BulkEditRow",
|
name: "BulkEditRow",
|
||||||
emits: ['remove', 'action'],
|
emits: ['remove', 'action', 'select'],
|
||||||
components: {
|
components: {
|
||||||
Spinner,
|
PhPath,
|
||||||
PhMapPin,
|
PhMapPinLine,
|
||||||
|
PhPackage,
|
||||||
|
PhTag,
|
||||||
|
PhChartPieSlice,
|
||||||
|
PhHandCoins,
|
||||||
|
CircleBadge,
|
||||||
PhFactory,
|
PhFactory,
|
||||||
PhPercent,
|
PhBarbell,
|
||||||
PhBarcode, PhBarbell, PhHash, PhVectorThree, PhVectorTwo, PhEmpty, BasicBadge, Flag, IconButton, Checkbox
|
PhHash,
|
||||||
|
PhVectorThree,
|
||||||
|
BasicBadge,
|
||||||
|
IconButton,
|
||||||
|
Checkbox
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
id: {
|
id: {
|
||||||
|
|
@ -156,61 +256,113 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
destinationsCount() {
|
materialCheck() {
|
||||||
return this.premise.destinations?.length ?? 0;
|
return (this.premise?.material.part_number != null && this.premise?.hs_code != null && this.premise?.tariff_rate != null)
|
||||||
},
|
},
|
||||||
destinationsText() {
|
priceCheck() {
|
||||||
return this.premise.destinations.map(d => d.destination_node.name).join(', ');
|
return (this.premise?.material_cost != null && this.premise?.oversea_share != null);
|
||||||
},
|
|
||||||
destinationNames() {
|
|
||||||
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 > names.length) {
|
|
||||||
names.push('and more ...');
|
|
||||||
}
|
|
||||||
|
|
||||||
return names;
|
|
||||||
},
|
|
||||||
showDestinationIncomplete() {
|
|
||||||
return this.premise.destinations.some(p => (
|
|
||||||
(((p.annual_amount ?? null) === null) || p.annual_amount === 0 ||
|
|
||||||
((p.routes?.every(r => !r.is_selected) && !p.is_d2d) ||
|
|
||||||
(p.is_d2d && ((p.rate_d2d ?? null) === null || p.rate_d2d === 0 || (p.lead_time_d2d ?? null) === null || p.lead_time_d2d === 0))
|
|
||||||
)
|
|
||||||
)));
|
|
||||||
},
|
|
||||||
showDestinations() {
|
|
||||||
return (this.destinationsCount > 0);
|
|
||||||
},
|
|
||||||
showMassEdit() {
|
|
||||||
return this.premiseEditStore.showProcessingModal;
|
|
||||||
},
|
|
||||||
showHu() {
|
|
||||||
return (this.hu.width && this.hu.length && this.hu.height && this.hu.weight && this.hu.content_unit_count)
|
|
||||||
},
|
|
||||||
showPrice() {
|
|
||||||
return !(this.premise.material_cost === null) || (this.premise.material_cost === 0)
|
|
||||||
},
|
|
||||||
showPriceIncomplete() {
|
|
||||||
return (this.premise.oversea_share === null)
|
|
||||||
},
|
|
||||||
isSelected() {
|
|
||||||
return this.premise.selected;
|
|
||||||
},
|
},
|
||||||
hu() {
|
hu() {
|
||||||
return this.premise.handling_unit;
|
return this.premise.handling_unit;
|
||||||
},
|
},
|
||||||
|
packagingCheck() {
|
||||||
|
return this.hu?.length != null && this.hu?.width != null && this.hu?.height != null && this.hu?.weight != null && this.hu?.content_unit_count;
|
||||||
|
},
|
||||||
|
destinationCheck() {
|
||||||
|
|
||||||
|
if (((this.premise?.destinations ?? null) === null) || this.premise?.destinations.length === 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return !this.premise?.destinations?.some(d => d.annual_amount == null);
|
||||||
|
|
||||||
|
},
|
||||||
|
isSelected() {
|
||||||
|
return this.premiseEditStore.isChecked(this.id);
|
||||||
|
},
|
||||||
...mapStores(usePremiseEditStore),
|
...mapStores(usePremiseEditStore),
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toFixed(value) {
|
toDestination(destination, limit = 15) {
|
||||||
return value !== null ? (value).toFixed(2) : '0.00';
|
return this.toNode(destination.destination_node, limit);
|
||||||
|
},
|
||||||
|
toNode(node, limit = 5) {
|
||||||
|
if (!node)
|
||||||
|
return 'N/A';
|
||||||
|
|
||||||
|
const name = node.name;
|
||||||
|
const mappingId = node.external_mapping_id;
|
||||||
|
const needsShortName = name.length > limit;
|
||||||
|
const useMappingId = ((mappingId ?? null) !== null) && ((name ?? null) === null || needsShortName);
|
||||||
|
const shortName = name?.substring(0, limit).concat("...") ?? 'N/A';
|
||||||
|
return `${useMappingId ? mappingId.replace("_", " ") : (needsShortName ? shortName : name)}`;
|
||||||
|
|
||||||
|
},
|
||||||
|
toDimension(handlingUnit) {
|
||||||
|
if (((handlingUnit ?? null) == null)
|
||||||
|
|| ((handlingUnit.length ?? null) == null)
|
||||||
|
|| ((handlingUnit.width ?? null) == null)
|
||||||
|
|| ((handlingUnit.height ?? null) == null)
|
||||||
|
) return 'N/A';
|
||||||
|
|
||||||
|
return `${this.toFixed(handlingUnit.length, null, 0)} x ${this.toFixed(handlingUnit.width, null, 0)} x ${this.toFixed(handlingUnit.height, null, 0)} ${handlingUnit.dimension_unit}`;
|
||||||
|
},
|
||||||
|
toRoute(destination, limit = 32) {
|
||||||
|
const route = destination?.routes?.find((route) => route.is_selected) ?? null;
|
||||||
|
|
||||||
|
if (destination.is_d2d)
|
||||||
|
return 'D2D Routing';
|
||||||
|
|
||||||
|
if (!route)
|
||||||
|
return 'N/A';
|
||||||
|
|
||||||
|
const nodes = route.transit_nodes?.map((node) => this.toNode(node)) ?? [];
|
||||||
|
|
||||||
|
if (nodes.length === 0)
|
||||||
|
return 'N/A';
|
||||||
|
|
||||||
|
const separator = " > ";
|
||||||
|
let fullString = nodes.join(separator);
|
||||||
|
|
||||||
|
if (fullString.length <= limit)
|
||||||
|
return fullString;
|
||||||
|
|
||||||
|
const front = nodes[0].concat(separator).concat("...").concat(separator);
|
||||||
|
let back = [];
|
||||||
|
|
||||||
|
for (const node of nodes.slice().reverse()) {
|
||||||
|
|
||||||
|
back.unshift(node);
|
||||||
|
const temp = front.concat(back.join(separator));
|
||||||
|
|
||||||
|
if (temp.length > limit) {
|
||||||
|
return front.concat(back.slice(1).join(separator));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toRouteIcon(destination) {
|
||||||
|
const route = destination?.routes?.find((route) => route.is_selected) ?? null;
|
||||||
|
|
||||||
|
if (destination.is_d2d)
|
||||||
|
return 'PhShippingContainer';
|
||||||
|
|
||||||
|
if (route?.type === 'SEA')
|
||||||
|
return 'PhBoat';
|
||||||
|
|
||||||
|
if (route?.type === 'RAIL')
|
||||||
|
return 'PhTrain';
|
||||||
|
|
||||||
|
if (route?.type === 'ROAD')
|
||||||
|
return 'PhTruck';
|
||||||
|
|
||||||
|
|
||||||
|
return 'PhEmpty';
|
||||||
|
},
|
||||||
|
toFixed(value, unit = null, decimals = 2) {
|
||||||
|
return value !== null ? `${(value).toFixed(decimals)} ${unit ?? ''}` : 'N/A';
|
||||||
},
|
},
|
||||||
toPercent(value) {
|
toPercent(value) {
|
||||||
return value !== null ? (value * 100).toFixed(2) : '0.00';
|
return value !== null ? `${(value * 100).toFixed(2)} %` : 'N/A';
|
||||||
},
|
|
||||||
updateSelected(value) {
|
|
||||||
this.premiseEditStore.setSelectTo([this.id], value);
|
|
||||||
},
|
},
|
||||||
editSingle() {
|
editSingle() {
|
||||||
const bulkQuery = this.$route.params.ids;
|
const bulkQuery = this.$route.params.ids;
|
||||||
|
|
@ -220,6 +372,9 @@ export default {
|
||||||
action(action) {
|
action(action) {
|
||||||
this.$emit('action', {id: this.id, action: action});
|
this.$emit('action', {id: this.id, action: action});
|
||||||
},
|
},
|
||||||
|
updateSelected(value) {
|
||||||
|
this.$emit("select", {id: this.id, checked: value});
|
||||||
|
},
|
||||||
remove() {
|
remove() {
|
||||||
this.premiseEditStore.removePremise(this.id);
|
this.premiseEditStore.removePremise(this.id);
|
||||||
this.$emit('remove', this.id);
|
this.$emit('remove', this.id);
|
||||||
|
|
@ -229,127 +384,212 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
/* Main container */
|
||||||
.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;
|
|
||||||
padding: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit-calculation-empty {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
margin: 1.6rem 0;
|
|
||||||
padding: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copyable-cell {
|
|
||||||
|
|
||||||
border-radius: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Standard hover ohne copy mode */
|
|
||||||
.copyable-cell:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: #f8fafc;
|
|
||||||
border-radius: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bulk-edit-row {
|
.bulk-edit-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 6rem 1fr 1fr 1.5fr 1.5fr 1.5fr 10rem;
|
grid-template-columns: 6rem 0.8fr 0.7fr 1fr 1fr 1.2fr 2fr 10rem;
|
||||||
gap: 1.6rem;
|
gap: 1.6rem;
|
||||||
padding: 0 2.4rem;
|
padding: 0 2.4rem;
|
||||||
border-bottom: 0.16rem solid #f3f4f6;
|
border-bottom: 0.16rem solid #f3f4f6;
|
||||||
align-items: center;
|
align-items: stretch;
|
||||||
transition: background-color 0.2s ease;
|
transition: background-color 0.2s ease;
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
height: 14rem;
|
height: 14rem;
|
||||||
overflow: hidden;
|
overflow: visible;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bulk-edit-row:last-child {
|
.bulk-edit-row:last-child {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-calculation-checkbox-cell {
|
/* Erhöhe z-index der gesamten Row beim Hover über destinations */
|
||||||
|
.bulk-edit-row:has(.bulk-edit-row__destinations-expanded:hover) {
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Checkbox cell */
|
||||||
|
.bulk-edit-row__checkbox {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Cell container */
|
||||||
.edit-calculation-cell--price {
|
.bulk-edit-row__cell-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.2rem;
|
align-self: stretch;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-calculation-cell--supplier {
|
/* Cell */
|
||||||
display: flex;
|
.bulk-edit-row__cell {
|
||||||
gap: 1.2rem;
|
flex: 1 1 auto;
|
||||||
height: 90%;
|
margin: 1.6rem 0;
|
||||||
|
padding: 0.8rem;
|
||||||
|
border-radius: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-calculation-cell--supplier-container {
|
.bulk-edit-row__cell--status {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
gap: 0.8rem;
|
gap: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-calculation-cell--supplier-flag {
|
.bulk-edit-row__cell--clickable:hover {
|
||||||
display: flex;
|
cursor: pointer;
|
||||||
align-items: center;
|
background-color: #f8fafc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-calculation-cell--packaging, .edit-calculation-cell--material, .edit-calculation-cell--destination {
|
.bulk-edit-row__cell--destinations {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cell data */
|
||||||
|
.bulk-edit-row__data {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.2rem;
|
gap: 0.2rem;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-calculation-cell-line {
|
.bulk-edit-row__data--destinations {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cell status */
|
||||||
|
.bulk-edit-row__status {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badge Transition Animation */
|
||||||
|
.badge-transition-enter-active {
|
||||||
|
animation: badge-enter 0.3s ease-out 0.1s both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-transition-leave-active {
|
||||||
|
animation: badge-leave 0.2s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes badge-enter {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.5);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes badge-leave {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(0.8);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lines */
|
||||||
|
.bulk-edit-row__line {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
gap: 0.8rem;
|
gap: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-calculation-cell-subline {
|
.bulk-edit-row__line--sub {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-calculation-packaging-badges {
|
/* Destination lines */
|
||||||
|
.bulk-edit-row__dest-line {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr 2fr;
|
||||||
|
gap: 0.4rem;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Destination lines */
|
||||||
|
.bulk-edit-row__route-line {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 2fr 1fr;
|
||||||
|
gap: 0.4rem;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badges */
|
||||||
|
.bulk-edit-row__badges {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.8rem;
|
gap: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bulk-edit-row__warning-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.8rem;
|
||||||
|
right: 0.8rem;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Expanded destinations overlay */
|
||||||
|
.bulk-edit-row__destinations-expanded {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: white;
|
||||||
|
border: 0.1rem solid #E3EDFF;
|
||||||
|
border-radius: 0.8rem;
|
||||||
|
box-shadow: 0 0.4rem 0.6rem rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 0.8rem;
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.2rem;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-0.4rem);
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.edit-calculation-actions-cell {
|
.bulk-edit-row__cell--destinations:hover .bulk-edit-row__destinations-expanded {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
pointer-events: auto;
|
||||||
|
max-height: 50rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-edit-row__cell--destinations:hover .bulk-edit-row__data--destinations {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-edit-row__cell--destinations:not(:has(.bulk-edit-row__destinations-expanded)):hover .bulk-edit-row__data--destinations {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Actions */
|
||||||
|
.bulk-edit-row__actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1.6rem;
|
gap: 1.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.number-circle {
|
|
||||||
display: inline-block;
|
|
||||||
width: 1.6rem;
|
|
||||||
height: 1.6rem;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: #002F54;
|
|
||||||
color: white;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1.6rem;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -181,8 +181,9 @@ export default {
|
||||||
.calculation-list-supplier-data {
|
.calculation-list-supplier-data {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
gap: 0.4rem;
|
gap: 0.4rem;
|
||||||
height: 90%
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.supplier-name {
|
.supplier-name {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="outer-container">
|
<div class="outer-container">
|
||||||
<div class="container" :class="{ 'responsive': responsive }" @focusout="focusLost">
|
<div class="container" :class="{ 'responsive': responsive }" @focusout="focusLost">
|
||||||
|
|
||||||
<!-- Wiederholende Felder als Array definieren und mit v-for rendern -->
|
|
||||||
<template v-for="field in displayFields" :key="field.name">
|
<template v-for="field in displayFields" :key="field.name">
|
||||||
<div class="caption-column">{{ field.label }}</div>
|
<div class="caption-column">{{ field.label }}</div>
|
||||||
<div class="input-column">
|
<div class="input-column">
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
@blur="field.onBlur"
|
@blur="field.onBlur"
|
||||||
class="input-field"
|
class="input-field"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
|
:placeholder="fromMassEdit ? '<keep>' : ''"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -92,6 +93,10 @@ export default {
|
||||||
responsive: {
|
responsive: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
|
},
|
||||||
|
fromMassEdit: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
<div class="text-container">
|
<div class="text-container">
|
||||||
<input ref="lengthInput" :value="huLength" @blur="validateDimension('length', $event)"
|
<input ref="lengthInput" :value="huLength" @blur="validateDimension('length', $event)"
|
||||||
@keydown.enter="handleEnter('lengthInput', $event)" class="input-field"
|
@keydown.enter="handleEnter('lengthInput', $event)" class="input-field"
|
||||||
|
:placeholder="fromMassEdit ? '<keep>' : ''"
|
||||||
autocomplete="off"/>
|
autocomplete="off"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -20,6 +21,7 @@
|
||||||
<div class="text-container">
|
<div class="text-container">
|
||||||
<input ref="widthInput" :value="huWidth" @blur="validateDimension('width', $event)"
|
<input ref="widthInput" :value="huWidth" @blur="validateDimension('width', $event)"
|
||||||
@keydown.enter="handleEnter('widthInput', $event)" class="input-field"
|
@keydown.enter="handleEnter('widthInput', $event)" class="input-field"
|
||||||
|
:placeholder="fromMassEdit ? '<keep>' : ''"
|
||||||
autocomplete="off"/>
|
autocomplete="off"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -32,6 +34,7 @@
|
||||||
<div class="text-container">
|
<div class="text-container">
|
||||||
<input ref="heightInput" :value="huHeight" @blur="validateDimension('height', $event)"
|
<input ref="heightInput" :value="huHeight" @blur="validateDimension('height', $event)"
|
||||||
@keydown.enter="handleEnter('heightInput', $event)" class="input-field"
|
@keydown.enter="handleEnter('heightInput', $event)" class="input-field"
|
||||||
|
:placeholder="fromMassEdit ? '<keep>' : ''"
|
||||||
autocomplete="off"/>
|
autocomplete="off"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -44,6 +47,7 @@
|
||||||
<div class="text-container">
|
<div class="text-container">
|
||||||
<input ref="weightInput" :value="huWeight" @blur="validateWeight('weight', $event)"
|
<input ref="weightInput" :value="huWeight" @blur="validateWeight('weight', $event)"
|
||||||
@keydown.enter="handleEnter('weightInput', $event)" class="input-field"
|
@keydown.enter="handleEnter('weightInput', $event)" class="input-field"
|
||||||
|
:placeholder="fromMassEdit ? '<keep>' : ''"
|
||||||
autocomplete="off"/>
|
autocomplete="off"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -57,6 +61,7 @@
|
||||||
<div class="text-container">
|
<div class="text-container">
|
||||||
<input ref="unitCountInput" :value="huUnitCount" @blur="validateCount"
|
<input ref="unitCountInput" :value="huUnitCount" @blur="validateCount"
|
||||||
@keydown.enter="handleEnter('unitCountInput', $event)" class="input-field"
|
@keydown.enter="handleEnter('unitCountInput', $event)" class="input-field"
|
||||||
|
:placeholder="fromMassEdit ? '<keep>' : ''"
|
||||||
autocomplete="off"/>
|
autocomplete="off"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -125,6 +130,10 @@ export default {
|
||||||
responsive: {
|
responsive: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
|
},
|
||||||
|
fromMassEdit: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -204,6 +213,12 @@ export default {
|
||||||
const inputOrder = ['lengthInput', 'widthInput', 'heightInput', 'weightInput', 'unitCountInput'];
|
const inputOrder = ['lengthInput', 'widthInput', 'heightInput', 'weightInput', 'unitCountInput'];
|
||||||
|
|
||||||
const currentIndex = inputOrder.indexOf(currentRef);
|
const currentIndex = inputOrder.indexOf(currentRef);
|
||||||
|
|
||||||
|
if(currentIndex >= inputOrder.length - 1) {
|
||||||
|
this.$emit('accept');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (currentIndex !== -1 && currentIndex < inputOrder.length - 1) {
|
if (currentIndex !== -1 && currentIndex < inputOrder.length - 1) {
|
||||||
const nextRef = inputOrder[currentIndex + 1];
|
const nextRef = inputOrder[currentIndex + 1];
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@
|
||||||
<div class="caption-column">MEK_A [EUR]</div>
|
<div class="caption-column">MEK_A [EUR]</div>
|
||||||
<div class="input-column">
|
<div class="input-column">
|
||||||
<div class="text-container">
|
<div class="text-container">
|
||||||
<input :value="priceFormatted" @blur="validatePrice" class="input-field"
|
<input ref="priceInput" @keydown.enter="handleEnter('priceInput', $event)" :value="priceFormatted" @blur="validatePrice" class="input-field"
|
||||||
|
:placeholder="fromMassEdit ? '<keep>' : ''"
|
||||||
autocomplete="off"/>
|
autocomplete="off"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -15,7 +16,8 @@
|
||||||
<div class="caption-column">Oversea share [%]</div>
|
<div class="caption-column">Oversea share [%]</div>
|
||||||
<div class="input-column">
|
<div class="input-column">
|
||||||
<div class="text-container">
|
<div class="text-container">
|
||||||
<input :value="overSeaSharePercent" @blur="validateOverSeaShare" class="input-field"
|
<input ref="overseaShareInput" @keydown.enter="handleEnter('overseaShareInput', $event)" :value="overSeaSharePercent" @blur="validateOverSeaShare" class="input-field"
|
||||||
|
:placeholder="fromMassEdit ? '<keep>' : ''"
|
||||||
autocomplete="off"/>
|
autocomplete="off"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -25,7 +27,7 @@
|
||||||
<div class="caption-column">Include FCA Fee</div>
|
<div class="caption-column">Include FCA Fee</div>
|
||||||
<div class="input-column">
|
<div class="input-column">
|
||||||
<tooltip text="Select if a additional FCA has to be added during calculation">
|
<tooltip text="Select if a additional FCA has to be added during calculation">
|
||||||
<checkbox :checked="includeFcaFee" @checkbox-changed="updateIncludeFcaFee"></checkbox>
|
<checkbox ref="fcaInput" @enter="handleEnter('fcaInput', $event)" @keydown.enter="handleEnter('fcaInput', $event)" :checked="includeFcaFee" @checkbox-changed="updateIncludeFcaFee"></checkbox>
|
||||||
</tooltip>
|
</tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -62,6 +64,10 @@ export default {
|
||||||
responsive: {
|
responsive: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
|
},
|
||||||
|
fromMassEdit: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -73,6 +79,29 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
handleEnter(currentRef, event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
// Define the navigation order
|
||||||
|
const inputOrder = ['priceInput', 'overseaShareInput', 'fcaInput'];
|
||||||
|
|
||||||
|
const currentIndex = inputOrder.indexOf(currentRef);
|
||||||
|
|
||||||
|
if(currentIndex >= inputOrder.length - 1) {
|
||||||
|
this.$emit('accept');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentIndex !== -1 && currentIndex < inputOrder.length - 1) {
|
||||||
|
const nextRef = inputOrder[currentIndex + 1];
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs[nextRef]) {
|
||||||
|
this.$refs[nextRef].focus();
|
||||||
|
this.$refs[nextRef].select();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
focusLost(event) {
|
focusLost(event) {
|
||||||
if (!this.$el.contains(event.relatedTarget)) {
|
if (!this.$el.contains(event.relatedTarget)) {
|
||||||
this.$emit('save', 'price');
|
this.$emit('save', 'price');
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import router from './router.js';
|
import router from './router.js';
|
||||||
//import store from './store/index.js';
|
|
||||||
import {setupErrorBuffer} from './store/notification.js'
|
import {setupErrorBuffer} from './store/notification.js'
|
||||||
import {createApp} from 'vue'
|
import {createApp} from 'vue'
|
||||||
import {createPinia} from 'pinia';
|
import {createPinia} from 'pinia';
|
||||||
|
|
@ -34,7 +33,8 @@ import {
|
||||||
PhTruckTrailer,
|
PhTruckTrailer,
|
||||||
PhUpload,
|
PhUpload,
|
||||||
PhWarning,
|
PhWarning,
|
||||||
PhX
|
PhX,
|
||||||
|
PhExclamationMark, PhMapPin, PhEmpty, PhShippingContainer
|
||||||
} from "@phosphor-icons/vue";
|
} from "@phosphor-icons/vue";
|
||||||
import {setupSessionRefresh} from "@/store/activeuser.js";
|
import {setupSessionRefresh} from "@/store/activeuser.js";
|
||||||
|
|
||||||
|
|
@ -61,6 +61,8 @@ app.component('PhTruckTrailer', PhTruckTrailer);
|
||||||
app.component('PhTruck', PhTruck);
|
app.component('PhTruck', PhTruck);
|
||||||
app.component('PhBoat', PhBoat);
|
app.component('PhBoat', PhBoat);
|
||||||
app.component('PhTrain', PhTrain);
|
app.component('PhTrain', PhTrain);
|
||||||
|
app.component('PhEmpty', PhEmpty);
|
||||||
|
app.component('PhShippingContainer', PhShippingContainer);
|
||||||
app.component('PhPencilSimple', PhPencilSimple);
|
app.component('PhPencilSimple', PhPencilSimple);
|
||||||
app.component('PhX', PhX);
|
app.component('PhX', PhX);
|
||||||
app.component('PhCloudArrowUp', PhCloudArrowUp);
|
app.component('PhCloudArrowUp', PhCloudArrowUp);
|
||||||
|
|
@ -74,6 +76,9 @@ app.component('PhFile', PhFile);
|
||||||
app.component("PhDesktop", PhDesktop );
|
app.component("PhDesktop", PhDesktop );
|
||||||
app.component("PhHardDrives", PhHardDrives );
|
app.component("PhHardDrives", PhHardDrives );
|
||||||
app.component("PhClipboard", PhClipboard );
|
app.component("PhClipboard", PhClipboard );
|
||||||
|
app.component("PhExclamationMark", PhExclamationMark );
|
||||||
|
app.component("PhMapPin", PhMapPin);
|
||||||
|
|
||||||
|
|
||||||
app.use(router);
|
app.use(router);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,15 @@
|
||||||
<h2 class="page-header">Mass edit calculation</h2>
|
<h2 class="page-header">Mass edit calculation</h2>
|
||||||
<div class="header-controls">
|
<div class="header-controls">
|
||||||
<basic-button :show-icon="false"
|
<basic-button :show-icon="false"
|
||||||
:disabled="premiseEditStore.selectedLoading"
|
:disabled="disableButtons"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
@click="closeMassEdit"
|
@click="close"
|
||||||
>Close
|
>Close
|
||||||
</basic-button>
|
</basic-button>
|
||||||
<basic-button :show-icon="true"
|
<basic-button :show-icon="true"
|
||||||
:disabled="premiseEditStore.selectedLoading"
|
:disabled="disableButtons"
|
||||||
icon="Calculator" variant="primary"
|
icon="Calculator" variant="primary"
|
||||||
@click="startCalculation"
|
@click="calculate"
|
||||||
>Calculate & close
|
>Calculate & close
|
||||||
</basic-button>
|
</basic-button>
|
||||||
|
|
||||||
|
|
@ -24,13 +24,15 @@
|
||||||
|
|
||||||
<div class="edit-calculation-list-header" key="header">
|
<div class="edit-calculation-list-header" key="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>Price</div>
|
<div>Price</div>
|
||||||
<div>Packaging</div>
|
<div>Packaging</div>
|
||||||
<div>Supplier</div>
|
<div>Supplier</div>
|
||||||
<div>Destinations & routes</div>
|
<div>Annual Quantity</div>
|
||||||
|
<div>Routes</div>
|
||||||
<div>Actions</div>
|
<div>Actions</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -43,8 +45,8 @@
|
||||||
<span class="space-around">No Calculations found.</span>
|
<span class="space-around">No Calculations found.</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<bulk-edit-row v-else class="edit-calculation-list-item" v-for="premise of this.premiseEditStore.getPremisses"
|
<bulk-edit-row v-else class="edit-calculation-list-item" v-for="premise of this.premises"
|
||||||
:key="premise.id" :id="premise.id" :premise="premise" @action="onClickAction"
|
:key="premise.id" :id="premise.id" :premise="premise" @action="onClickAction" @select="updateCheckBox"
|
||||||
@remove="updateUrl">
|
@remove="updateUrl">
|
||||||
</bulk-edit-row>
|
</bulk-edit-row>
|
||||||
|
|
||||||
|
|
@ -65,9 +67,11 @@
|
||||||
v-model:tariffRate="componentProps.tariffRate"
|
v-model:tariffRate="componentProps.tariffRate"
|
||||||
v-model:tariffUnlocked="componentProps.tariffUnlocked"
|
v-model:tariffUnlocked="componentProps.tariffUnlocked"
|
||||||
v-model:description="componentProps.description"
|
v-model:description="componentProps.description"
|
||||||
|
|
||||||
v-model:price="componentProps.price"
|
v-model:price="componentProps.price"
|
||||||
v-model:overSeaShare="componentProps.overSeaShare"
|
v-model:overSeaShare="componentProps.overSeaShare"
|
||||||
v-model:includeFcaFee="componentProps.includeFcaFee"
|
v-model:includeFcaFee="componentProps.includeFcaFee"
|
||||||
|
|
||||||
v-model:length="componentProps.length"
|
v-model:length="componentProps.length"
|
||||||
v-model:width="componentProps.width"
|
v-model:width="componentProps.width"
|
||||||
v-model:height="componentProps.height"
|
v-model:height="componentProps.height"
|
||||||
|
|
@ -77,16 +81,24 @@
|
||||||
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:hideDescription="componentProps.hideDescription"
|
v-model:hideDescription="componentProps.hideDescription"
|
||||||
|
|
||||||
|
:fromMassEdit="true"
|
||||||
:countryId=null
|
:countryId=null
|
||||||
:responsive="false"
|
:responsive="false"
|
||||||
|
|
||||||
@close="closeEditModalAction('cancel')"
|
@close="closeEditModalAction('cancel')"
|
||||||
|
@accept="closeEditModalAction('accept')"
|
||||||
|
|
||||||
>
|
>
|
||||||
</component>
|
</component>
|
||||||
<div class="modal-content-footer" >
|
|
||||||
<basic-button v-if="!modalCloseOnly" :show-icon="false" @click="closeEditModalAction('accept')">OK</basic-button>
|
<div class="modal-content-footer">
|
||||||
<basic-button variant="secondary" :show-icon="false" @click="closeEditModalAction('cancel')"> {{ modalCloseOnly ? "Close" : "Cancel" }}
|
<basic-button v-if="!modalCloseOnly" :show-icon="false" @click="closeEditModalAction('accept')">OK
|
||||||
|
</basic-button>
|
||||||
|
<basic-button variant="secondary" :show-icon="false" @click="closeEditModalAction('cancel')">
|
||||||
|
{{ modalCloseOnly ? "Close" : "Cancel" }}
|
||||||
</basic-button>
|
</basic-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -137,11 +149,20 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapStores(usePremiseEditStore, useNotificationStore),
|
...mapStores(usePremiseEditStore, useNotificationStore),
|
||||||
|
disableButtons() {
|
||||||
|
return this.premiseEditStore.selectedLoading;
|
||||||
|
},
|
||||||
|
premises() {
|
||||||
|
return this.premiseEditStore.getPremisses;
|
||||||
|
},
|
||||||
hasSelection() {
|
hasSelection() {
|
||||||
if (this.premiseEditStore.isLoading || this.premiseEditStore.selectedLoading) {
|
if (this.premiseEditStore.isLoading || this.premiseEditStore.selectedLoading) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return this.premiseEditStore.getSelectedPremissesIds?.length > 0;
|
return this.premiseEditStore.someChecked;
|
||||||
|
},
|
||||||
|
showMultiselectAction() {
|
||||||
|
return this.selectCount > 0;
|
||||||
},
|
},
|
||||||
selectCount() {
|
selectCount() {
|
||||||
return this.selectedPremisses?.length ?? 0;
|
return this.selectedPremisses?.length ?? 0;
|
||||||
|
|
@ -158,14 +179,8 @@ export default {
|
||||||
showData() {
|
showData() {
|
||||||
return this.premiseEditStore.showData;
|
return this.premiseEditStore.showData;
|
||||||
},
|
},
|
||||||
overallCheck() {
|
|
||||||
return this.premiseEditStore.isLoading ? false : this.premiseEditStore.getPremisses?.every(p => p.selected === true) ?? false;
|
|
||||||
},
|
|
||||||
showMultiselectAction() {
|
|
||||||
return this.selectCount > 0;
|
|
||||||
},
|
|
||||||
modalCloseOnly() {
|
modalCloseOnly() {
|
||||||
return this.modalType === 'material' && !this.componentProps.tariffUnlocked;
|
return this.modalType === 'material' && !this.componentProps.tariffUnlocked; //TODO: check all selected.
|
||||||
},
|
},
|
||||||
showEditModal() {
|
showEditModal() {
|
||||||
return ((this.modalType ?? null) !== null);
|
return ((this.modalType ?? null) !== null);
|
||||||
|
|
@ -188,10 +203,9 @@ export default {
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
showProcessingModal(newState, _) {
|
showProcessingModal(newState, _) {
|
||||||
if(newState) {
|
if (newState) {
|
||||||
this.notificationStore.setSpinner(this.shownProcessingMessage);
|
this.notificationStore.setSpinner(this.shownProcessingMessage);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
this.notificationStore.clearSpinner();
|
this.notificationStore.clearSpinner();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -201,15 +215,25 @@ export default {
|
||||||
this.ids = new UrlSafeBase64().decodeIds(this.$route.params.ids);
|
this.ids = new UrlSafeBase64().decodeIds(this.$route.params.ids);
|
||||||
this.premiseEditStore.loadPremissesForced(this.ids);
|
this.premiseEditStore.loadPremissesForced(this.ids);
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
ids: [],
|
ids: [],
|
||||||
|
overallCheck: false,
|
||||||
|
overallIndeterminate: false,
|
||||||
bulkQuery: null,
|
bulkQuery: null,
|
||||||
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: null, tariffRate: null, tariffUnlocked: false, description: "", hideDescription: false}},
|
material: {
|
||||||
|
props: {
|
||||||
|
partNumber: "",
|
||||||
|
hsCode: null,
|
||||||
|
tariffRate: null,
|
||||||
|
tariffUnlocked: false,
|
||||||
|
description: "",
|
||||||
|
hideDescription: false
|
||||||
|
}
|
||||||
|
},
|
||||||
packaging: {
|
packaging: {
|
||||||
props: {
|
props: {
|
||||||
length: 0,
|
length: 0,
|
||||||
|
|
@ -247,27 +271,48 @@ export default {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async startCalculation() {
|
async calculate() {
|
||||||
this.showCalculationModal = true;
|
this.showCalculationModal = true;
|
||||||
const error = await this.premiseEditStore.startCalculation();
|
const error = await this.premiseEditStore.startCalculation();
|
||||||
|
|
||||||
if (error === null) {
|
if (error === null) {
|
||||||
this.closeMassEdit()
|
this.close()
|
||||||
}
|
}
|
||||||
this.showCalculationModal = false;
|
this.showCalculationModal = false;
|
||||||
},
|
},
|
||||||
closeMassEdit() {
|
close() {
|
||||||
this.$router.push({name: "calculation-list"});
|
this.$router.push({name: "calculation-list"});
|
||||||
},
|
},
|
||||||
|
updateCheckBox(data) {
|
||||||
|
this.premiseEditStore.setChecked(data.id, data.checked);
|
||||||
|
this.updateOverallCheckBox();
|
||||||
|
},
|
||||||
updateCheckBoxes(value) {
|
updateCheckBoxes(value) {
|
||||||
|
console.log("set all", value)
|
||||||
this.premiseEditStore.setAll(value);
|
this.premiseEditStore.setAll(value);
|
||||||
|
this.updateOverallCheckBox();
|
||||||
|
},
|
||||||
|
updateOverallCheckBox() {
|
||||||
|
this.overallCheck = this.premiseEditStore.allChecked;
|
||||||
|
|
||||||
|
if (!this.overallCheck)
|
||||||
|
this.overallIndeterminate = this.premiseEditStore.someChecked;
|
||||||
},
|
},
|
||||||
multiselectAction(action) {
|
multiselectAction(action) {
|
||||||
this.openModal(action, this.selectedPremisses.map(p => p.id));
|
this.openModal(action, this.selectedPremisses.map(p => p.id));
|
||||||
},
|
},
|
||||||
onClickAction(data) {
|
onClickAction(data) {
|
||||||
const massEdit = 0 !== this.selectCount
|
if (data.action === 'supplier-select') {
|
||||||
this.openModal(data.action, massEdit ? this.premiseEditStore.getSelectedPremissesIds : [data.id], data.id, massEdit);
|
this.premiseEditStore.setBy('supplier', data.id);
|
||||||
|
this.updateOverallCheckBox();
|
||||||
|
} else if (data.action === 'material-select') {
|
||||||
|
this.premiseEditStore.setBy('material', data.id);
|
||||||
|
this.updateOverallCheckBox();
|
||||||
|
} else {
|
||||||
|
const massEdit = 0 !== this.selectCount
|
||||||
|
this.openModal(data.action, massEdit ? this.premiseEditStore.getSelectedPremissesIds : [data.id], data.id, massEdit);
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
openModal(type, ids, dataSource = -1, massEdit = true) {
|
openModal(type, ids, dataSource = -1, massEdit = true) {
|
||||||
|
|
||||||
|
|
@ -383,7 +428,7 @@ export default {
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
/* 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(.bulk-edit-row__cell--clickable: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: #f8fafc;
|
background-color: #f8fafc;
|
||||||
border-radius: 0.8rem;
|
border-radius: 0.8rem;
|
||||||
|
|
@ -456,7 +501,7 @@ export default {
|
||||||
|
|
||||||
.edit-calculation-list-header {
|
.edit-calculation-list-header {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 6rem 1fr 1fr 1.5fr 1.5fr 1.5fr 10rem;
|
grid-template-columns: 6rem 0.8fr 0.7fr 1fr 1fr 1.2fr 2fr 10rem;
|
||||||
gap: 1.6rem;
|
gap: 1.6rem;
|
||||||
padding: 2.4rem;
|
padding: 2.4rem;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
|
|
|
||||||
|
|
@ -215,6 +215,13 @@ export default {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.edit-calculation-spinner-container
|
||||||
|
{
|
||||||
|
display: flex;
|
||||||
|
padding-top: 10rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.header-container {
|
.header-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,7 @@ export const useNotificationStore = defineStore('notification', {
|
||||||
const pinia = this.$pinia || getActivePinia()
|
const pinia = this.$pinia || getActivePinia()
|
||||||
if (pinia && pinia._s) {
|
if (pinia && pinia._s) {
|
||||||
pinia._s.forEach((store, storeId) => {
|
pinia._s.forEach((store, storeId) => {
|
||||||
if (storeId !== 'error' && storeId !== 'errorLog' && store.$state) {
|
if (storeId !== 'notification' && storeId !== 'errorLog' && store.$state) {
|
||||||
storeState[storeId] = {
|
storeState[storeId] = {
|
||||||
...toRaw(store.$state)
|
...toRaw(store.$state)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
||||||
state() {
|
state() {
|
||||||
return {
|
return {
|
||||||
premisses: null,
|
premisses: null,
|
||||||
|
selectedIds: [],
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* set to true while the store is loading the premises.
|
* set to true while the store is loading the premises.
|
||||||
|
|
@ -20,14 +21,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
||||||
*/
|
*/
|
||||||
selectedLoading: false,
|
selectedLoading: false,
|
||||||
|
|
||||||
|
|
||||||
destinations: null,
|
|
||||||
processDestinationMassEdit: false,
|
|
||||||
|
|
||||||
selectedDestination: null,
|
|
||||||
|
|
||||||
throwsException: true,
|
throwsException: true,
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
|
|
@ -54,21 +48,21 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* Returns the ids of all premises.
|
// * Returns the ids of all premises.
|
||||||
* @param state
|
// * @param state
|
||||||
* @returns {*}
|
// * @returns {*}
|
||||||
*/
|
// */
|
||||||
getPremiseIds(state) {
|
// getPremiseIds(state) {
|
||||||
if (state.loading) {
|
// if (state.loading) {
|
||||||
if (state.throwsException)
|
// if (state.throwsException)
|
||||||
throw new Error("Premises are accessed while still loading.");
|
// throw new Error("Premises are accessed while still loading.");
|
||||||
|
//
|
||||||
return null;
|
// return null;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
return state.premisses?.map(p => p.id);
|
// return state.premisses?.map(p => p.id);
|
||||||
},
|
// },
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the premises.
|
* Returns the premises.
|
||||||
|
|
@ -242,7 +236,30 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
||||||
return state.premisses?.find(p => p.selected);
|
return state.premisses?.find(p => p.selected);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
allChecked(state) {
|
||||||
|
if (state.premisses.length > state.selectedIds.length)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
for (const premise of state.premisses) {
|
||||||
|
if (!state.selectedIds.includes(premise.id))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.premisses.length !== 0;
|
||||||
|
},
|
||||||
|
someChecked(state) {
|
||||||
|
for (const premise of state.premisses) {
|
||||||
|
if (state.selectedIds.includes(premise.id))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
isChecked(state) {
|
||||||
|
return (id) => {
|
||||||
|
return state.selectedIds.includes(id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
@ -280,7 +297,6 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
||||||
this.premisses = updatedPremises;
|
this.premisses = updatedPremises;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return await this.saveMaterial(ids, materialData);
|
return await this.saveMaterial(ids, materialData);
|
||||||
},
|
},
|
||||||
async batchUpdatePackaging(ids, packagingData) {
|
async batchUpdatePackaging(ids, packagingData) {
|
||||||
|
|
@ -633,22 +649,6 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replace the premisses with the loaded ones by id.
|
|
||||||
* This is used to update the premisses after a "Set" change.
|
|
||||||
* @param premisses
|
|
||||||
* @param loadedData
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
replacePremissesById(premisses, loadedData) {
|
|
||||||
const replacementMap = new Map(loadedData.map(obj => [obj.id, obj]));
|
|
||||||
const replaced = premisses.map(obj => replacementMap.get(obj.id) || obj);
|
|
||||||
logger.info("Replaced", replaced);
|
|
||||||
return replaced;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save
|
* Save
|
||||||
*/
|
*/
|
||||||
|
|
@ -733,71 +733,51 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
||||||
* PREMISE stuff
|
* PREMISE stuff
|
||||||
* =================
|
* =================
|
||||||
*/
|
*/
|
||||||
deselectPremise() {
|
|
||||||
this.selectedLoading = true;
|
|
||||||
|
|
||||||
this.premisses.forEach(p => p.selected = false);
|
|
||||||
|
|
||||||
this.destinations = null;
|
|
||||||
this.selectedDestination = null;
|
|
||||||
this.selectedLoading = false;
|
|
||||||
},
|
|
||||||
setAll(value) {
|
setAll(value) {
|
||||||
|
|
||||||
this.selectedLoading = true;
|
this.selectedLoading = true;
|
||||||
|
|
||||||
const updatedPremises = this.premisses.map(p => ({
|
const temp = [];
|
||||||
...p,
|
|
||||||
selected: value
|
if (value)
|
||||||
}));
|
this.premisses.forEach(p => temp.push(p.id));
|
||||||
this.premisses = updatedPremises;
|
|
||||||
|
this.selectedIds = temp;
|
||||||
|
|
||||||
this.selectedLoading = false;
|
this.selectedLoading = false;
|
||||||
|
|
||||||
},
|
},
|
||||||
setSelectTo(ids, value) {
|
setChecked(premiseId, checked) {
|
||||||
this.selectedLoading = true;
|
this.selectedLoading = true;
|
||||||
|
|
||||||
const idsSet = new Set(ids);
|
if (checked) {
|
||||||
const updatedPremises = this.premisses.map(p => ({
|
if (!this.selectedIds.includes(premiseId)) {
|
||||||
...p,
|
this.selectedIds.push(premiseId);
|
||||||
selected: idsSet.has(p.id) ? value : p.selected
|
}
|
||||||
}));
|
} else {
|
||||||
this.premisses = updatedPremises;
|
const idx = this.selectedIds.indexOf(premiseId);
|
||||||
|
if (idx !== -1)
|
||||||
|
this.selectedIds.splice(idx, 1);
|
||||||
|
}
|
||||||
|
|
||||||
this.selectedLoading = false;
|
this.selectedLoading = false;
|
||||||
},
|
},
|
||||||
async selectSinglePremise(id, ids) {
|
setBy(type, ofId) {
|
||||||
this.selectedLoading = true;
|
this.selectedLoading = true;
|
||||||
|
const premise = this.premisses.find(p => p.id === ofId);
|
||||||
|
|
||||||
await this.loadPremissesIfNeeded(ids);
|
const temp = [];
|
||||||
|
|
||||||
this.premisses.forEach(p => p.selected = String(id) === String(p.id));
|
this.premisses.forEach(p => {
|
||||||
|
if(type === 'supplier' && p.supplier.id === premise.supplier.id) {
|
||||||
this.prepareDestinations(id, [id]);
|
temp.push(p.id);
|
||||||
|
} else if(type === 'material' && p.material.id === premise.material.id) {
|
||||||
this.selectedLoading = false;
|
temp.push(p.id);
|
||||||
},
|
}
|
||||||
async loadAndSelectSinglePremise(id) {
|
|
||||||
|
|
||||||
this.loading = true;
|
|
||||||
this.selectedLoading = true;
|
|
||||||
this.premises = [];
|
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
params.append('premissIds', `${[id]}`);
|
|
||||||
const url = `${config.backendUrl}/calculation/edit/${params.size === 0 ? '' : '?'}${params.toString()}`;
|
|
||||||
|
|
||||||
const {data: data, headers: headers} = await performRequest(this, 'GET', url, null).catch(e => {
|
|
||||||
this.selectedLoading = false;
|
|
||||||
this.loading = false;
|
|
||||||
});
|
});
|
||||||
this.premisses = data;
|
|
||||||
|
|
||||||
this.premisses.forEach(p => p.selected = true);
|
this.selectedIds = temp;
|
||||||
this.prepareDestinations(id, [id]);
|
|
||||||
this.selectedLoading = false;
|
this.selectedLoading = false;
|
||||||
this.loading = false;
|
|
||||||
},
|
},
|
||||||
async loadPremissesIfNeeded(ids, exact = false) {
|
async loadPremissesIfNeeded(ids, exact = false) {
|
||||||
const reload = this.premisses ? !ids.every((id) => this.premisses.find(d => d.id === id) && (!exact || ids.length === this.premisses.length)) : true;
|
const reload = this.premisses ? !ids.every((id) => this.premisses.find(d => d.id === id) && (!exact || ids.length === this.premisses.length)) : true;
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,7 @@ public class GlobalExceptionHandler {
|
||||||
public ResponseEntity<ErrorResponseDTO> handlePremiseValidationException(PremiseValidationError exception) {
|
public ResponseEntity<ErrorResponseDTO> handlePremiseValidationException(PremiseValidationError exception) {
|
||||||
ErrorDTO error = new ErrorDTO(
|
ErrorDTO error = new ErrorDTO(
|
||||||
exception.getClass().getName(),
|
exception.getClass().getName(),
|
||||||
"Premiss validation error",
|
"Validation error",
|
||||||
exception.getMessage(),
|
exception.getMessage(),
|
||||||
Arrays.asList(exception.getStackTrace())
|
Arrays.asList(exception.getStackTrace())
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -278,7 +278,10 @@ public class DestinationRepository {
|
||||||
Destination entity = new Destination();
|
Destination entity = new Destination();
|
||||||
|
|
||||||
entity.setId(rs.getInt("id"));
|
entity.setId(rs.getInt("id"));
|
||||||
entity.setAnnualAmount(rs.getInt("annual_amount"));
|
|
||||||
|
var amount = rs.getInt("annual_amount");
|
||||||
|
entity.setAnnualAmount(rs.wasNull() ? null : amount);
|
||||||
|
|
||||||
entity.setPremiseId(rs.getInt("premise_id"));
|
entity.setPremiseId(rs.getInt("premise_id"));
|
||||||
entity.setDestinationNodeId(rs.getInt("destination_node_id"));
|
entity.setDestinationNodeId(rs.getInt("destination_node_id"));
|
||||||
entity.setRateD2d(rs.getBigDecimal("rate_d2d"));
|
entity.setRateD2d(rs.getBigDecimal("rate_d2d"));
|
||||||
|
|
|
||||||
|
|
@ -140,7 +140,6 @@ public class PremisesService {
|
||||||
calculationIds.add(calculationJobRepository.insert(job));
|
calculationIds.add(calculationJobRepository.insert(job));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
premiseRepository.setStatus(premises, PremiseState.COMPLETED);
|
premiseRepository.setStatus(premises, PremiseState.COMPLETED);
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import de.avatic.lcc.model.db.error.SysErrorType;
|
||||||
import de.avatic.lcc.repositories.bulk.BulkOperationRepository;
|
import de.avatic.lcc.repositories.bulk.BulkOperationRepository;
|
||||||
import de.avatic.lcc.repositories.error.SysErrorRepository;
|
import de.avatic.lcc.repositories.error.SysErrorRepository;
|
||||||
import de.avatic.lcc.service.transformer.error.SysErrorTransformer;
|
import de.avatic.lcc.service.transformer.error.SysErrorTransformer;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.scheduling.annotation.Async;
|
import org.springframework.scheduling.annotation.Async;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
@ -19,28 +21,32 @@ import java.util.concurrent.TimeUnit;
|
||||||
@Service
|
@Service
|
||||||
public class BulkOperationExecutionService {
|
public class BulkOperationExecutionService {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(BulkOperationExecutionService.class);
|
||||||
private final BulkOperationRepository bulkOperationRepository;
|
private final BulkOperationRepository bulkOperationRepository;
|
||||||
private final BulkExportService bulkExportService;
|
private final BulkExportService bulkExportService;
|
||||||
private final BulkImportService bulkImportService;
|
private final BulkImportService bulkImportService;
|
||||||
private final SysErrorRepository sysErrorRepository;
|
private final SysErrorRepository sysErrorRepository;
|
||||||
private final SysErrorTransformer sysErrorTransformer;
|
private final SysErrorTransformer sysErrorTransformer;
|
||||||
private final Executor bulkProcessingExecutor;
|
|
||||||
|
|
||||||
public BulkOperationExecutionService(BulkOperationRepository bulkOperationRepository, BulkExportService bulkExportService, BulkImportService bulkImportService, SysErrorRepository sysErrorRepository, SysErrorTransformer sysErrorTransformer, @Qualifier("bulkProcessingExecutor") Executor bulkProcessingExecutor) {
|
|
||||||
|
public BulkOperationExecutionService(BulkOperationRepository bulkOperationRepository, BulkExportService bulkExportService, BulkImportService bulkImportService, SysErrorRepository sysErrorRepository, SysErrorTransformer sysErrorTransformer) {
|
||||||
this.bulkOperationRepository = bulkOperationRepository;
|
this.bulkOperationRepository = bulkOperationRepository;
|
||||||
this.bulkExportService = bulkExportService;
|
this.bulkExportService = bulkExportService;
|
||||||
this.bulkImportService = bulkImportService;
|
this.bulkImportService = bulkImportService;
|
||||||
this.sysErrorRepository = sysErrorRepository;
|
this.sysErrorRepository = sysErrorRepository;
|
||||||
this.sysErrorTransformer = sysErrorTransformer;
|
this.sysErrorTransformer = sysErrorTransformer;
|
||||||
this.bulkProcessingExecutor = bulkProcessingExecutor;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Async("bulkProcessingExecutor")
|
@Async("bulkProcessingExecutor")
|
||||||
public CompletableFuture<Void> launchExecution(Integer id) {
|
public CompletableFuture<Void> launchExecution(Integer id) {
|
||||||
|
logger.info("Starting bulk operation execution for ID: {}", id);
|
||||||
try {
|
try {
|
||||||
execution(id);
|
execution(id);
|
||||||
|
logger.info("Bulk operation execution completed successfully for ID: {}", id);
|
||||||
return CompletableFuture.completedFuture(null);
|
return CompletableFuture.completedFuture(null);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
|
logger.error("Error during bulk operation execution for ID: {}", id, e);
|
||||||
bulkOperationRepository.updateState(id, BulkOperationState.EXCEPTION);
|
bulkOperationRepository.updateState(id, BulkOperationState.EXCEPTION);
|
||||||
|
|
||||||
var error = new SysError();
|
var error = new SysError();
|
||||||
|
|
@ -59,13 +65,14 @@ public class BulkOperationExecutionService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void execution(Integer id) {
|
public void execution(Integer id) {
|
||||||
|
logger.debug("Executing bulk operation for ID: {}", id);
|
||||||
var operation = bulkOperationRepository.getOperationById(id);
|
var operation = bulkOperationRepository.getOperationById(id);
|
||||||
|
|
||||||
if (operation.isPresent()) {
|
if (operation.isPresent()) {
|
||||||
var op = operation.get();
|
var op = operation.get();
|
||||||
|
|
||||||
if (op.getProcessState() == BulkOperationState.SCHEDULED) {
|
if (op.getProcessState() == BulkOperationState.SCHEDULED) {
|
||||||
|
logger.info("Processing bulk operation ID: {}, type: {}, file type: {}", id, op.getProcessingType(), op.getFileType());
|
||||||
bulkOperationRepository.updateState(id, BulkOperationState.PROCESSING);
|
bulkOperationRepository.updateState(id, BulkOperationState.PROCESSING);
|
||||||
try {
|
try {
|
||||||
if (op.getProcessingType() == BulkProcessingType.EXPORT) {
|
if (op.getProcessingType() == BulkProcessingType.EXPORT) {
|
||||||
|
|
@ -78,6 +85,9 @@ public class BulkOperationExecutionService {
|
||||||
|
|
||||||
}
|
}
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
|
|
||||||
|
logger.error("Error during bulk operation execution for ID: {}", id, e);
|
||||||
|
|
||||||
op.setProcessState(BulkOperationState.EXCEPTION);
|
op.setProcessState(BulkOperationState.EXCEPTION);
|
||||||
|
|
||||||
var error = new SysError();
|
var error = new SysError();
|
||||||
|
|
|
||||||
|
|
@ -168,12 +168,13 @@ public class MaterialFastExcelMapper {
|
||||||
// Extract and validate data
|
// Extract and validate data
|
||||||
String partNumber = getCellValue(row, MaterialHeader.PART_NUMBER.ordinal(), rowNumber);
|
String partNumber = getCellValue(row, MaterialHeader.PART_NUMBER.ordinal(), rowNumber);
|
||||||
String description = getCellValue(row, MaterialHeader.DESCRIPTION.ordinal(), rowNumber);
|
String description = getCellValue(row, MaterialHeader.DESCRIPTION.ordinal(), rowNumber);
|
||||||
String hsCode = getCellValue(row, MaterialHeader.HS_CODE.ordinal(), rowNumber);
|
String hsCode = getCellValueAllowEmpty(row, MaterialHeader.HS_CODE.ordinal(), rowNumber);
|
||||||
String operation = getCellValue(row, MaterialHeader.OPERATION.ordinal(), rowNumber);
|
String operation = getCellValue(row, MaterialHeader.OPERATION.ordinal(), rowNumber);
|
||||||
|
|
||||||
// Validate lengths
|
// Validate lengths
|
||||||
validateLength(partNumber, 0, 12, "Part Number", rowNumber);
|
validateLength(partNumber, 0, 12, "Part Number", rowNumber);
|
||||||
validateLength(hsCode, 0, 11, "HS Code", rowNumber);
|
if (hsCode != null)
|
||||||
|
validateLength(hsCode, 0, 11, "HS Code", rowNumber);
|
||||||
validateLength(description, 1, 500, "Description", rowNumber);
|
validateLength(description, 1, 500, "Description", rowNumber);
|
||||||
|
|
||||||
// Validate operation enum
|
// Validate operation enum
|
||||||
|
|
@ -212,6 +213,13 @@ public class MaterialFastExcelMapper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a cell value as string with proper error handling
|
||||||
|
*/
|
||||||
|
private String getCellValueAllowEmpty(Row row, int columnIndex, int rowNumber) {
|
||||||
|
return row.getCellAsString(columnIndex).orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a cell value as string with proper error handling
|
* Gets a cell value as string with proper error handling
|
||||||
*/
|
*/
|
||||||
|
|
@ -248,6 +256,6 @@ public class MaterialFastExcelMapper {
|
||||||
* Validates HS Code (placeholder for API validation)
|
* Validates HS Code (placeholder for API validation)
|
||||||
*/
|
*/
|
||||||
private boolean validateHsCode(String hsCode) {
|
private boolean validateHsCode(String hsCode) {
|
||||||
return hsCode.length() >= 10 && hsCode.length() <= 12 && hsCode.matches("[0-9]+");
|
return hsCode == null || (hsCode.length() >= 8 && hsCode.length() <= 12 && hsCode.matches("[0-9]+"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Add table
Reference in a new issue