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">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>LCC</title>
|
||||
<title>Logistics Cost Calculation Tool</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export default {
|
|||
|
||||
html {
|
||||
font-size: 62.5%;
|
||||
font-family: 'Poppins', sans-serif;
|
||||
font-family: 'Arial', sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,11 @@ export default{
|
|||
type: String,
|
||||
default: 'primary',
|
||||
validator: (value) => ['primary', 'secondary', 'grey', 'exception', 'skeleton'].includes(value)
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
validator: (value) => ['default', 'compact'].includes(value)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -31,7 +36,8 @@ export default{
|
|||
},
|
||||
batchClasses() {
|
||||
return [
|
||||
`batch--${this.variant}`
|
||||
`batch--${this.variant}`,
|
||||
`batch--${this.size}`
|
||||
]
|
||||
},
|
||||
iconComponent() {
|
||||
|
|
@ -65,6 +71,12 @@ export default{
|
|||
height: 2.4rem;
|
||||
}
|
||||
|
||||
.batch-container.batch--compact {
|
||||
padding: 0.2rem 0.4rem;
|
||||
gap: 0.4rem;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.batch--primary {
|
||||
background-color: #5AF0B4;
|
||||
color: #002F54;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
<div class="checkbox-container">
|
||||
<label class="checkbox-item" :class="{ disabled: disabled }" @change="setFilter">
|
||||
<input
|
||||
@keydown.enter="$emit('enter', $event)"
|
||||
type="checkbox"
|
||||
:checked="isChecked"
|
||||
:disabled="disabled"
|
||||
|
|
@ -72,6 +73,9 @@ export default{
|
|||
this.updateIndeterminateState(this.isIndeterminate);
|
||||
},
|
||||
methods: {
|
||||
focus() {
|
||||
this.$refs.checkboxInput?.focus();
|
||||
},
|
||||
setFilter(event) {
|
||||
if (this.disabled) return;
|
||||
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>
|
||||
<div class="bulk-edit-row">
|
||||
<div class="edit-calculation-checkbox-cell">
|
||||
<div class="bulk-edit-row__checkbox">
|
||||
<checkbox :checked="isSelected" @checkbox-changed="updateSelected"></checkbox>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="edit-calculation-cell-container">
|
||||
<div class="edit-calculation-cell copyable-cell" @click="action('material')">
|
||||
<div class="edit-calculation-cell-line">{{ premise.material.part_number }}</div>
|
||||
<div class="edit-calculation-cell-line edit-calculation-cell-subline" v-if="premise.hs_code">
|
||||
HS Code:
|
||||
{{ premise.hs_code }}
|
||||
<div class="bulk-edit-row__cell-container">
|
||||
<div class="bulk-edit-row__cell bulk-edit-row__cell--status bulk-edit-row__cell--clickable"
|
||||
@click.exact="action('material')" @click.ctrl="action('material-select')">
|
||||
<div class="bulk-edit-row__data">
|
||||
<div class="bulk-edit-row__line">
|
||||
<ph-package size="16"/>
|
||||
{{ premise.material.part_number ?? 'N/A' }}
|
||||
</div>
|
||||
<div class="edit-calculation-cell-line edit-calculation-cell-subline"
|
||||
v-if="(premise.tariff_rate ?? null) !== null">
|
||||
Tariff rate:
|
||||
{{ toPercent(premise.tariff_rate) }}
|
||||
<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 class="bulk-edit-row__status">
|
||||
<transition name="badge-transition" mode="out-in">
|
||||
<circle-badge v-if="materialCheck" :key="'check-' + id" variant="skeleton-grey" icon="check"></circle-badge>
|
||||
<circle-badge v-else :key="'error-' + id" variant="exception" icon="exclamation-mark"></circle-badge>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="edit-calculation-cell-container">
|
||||
<div class="edit-calculation-cell copyable-cell" @click="action('price')" v-if="showPrice">
|
||||
<div class="edit-calculation-cell-line">{{ toFixed(premise.material_cost) }} EUR</div>
|
||||
<div class="edit-calculation-cell-line edit-calculation-cell-subline">Oversea share:
|
||||
{{ toPercent(premise.oversea_share) }} %
|
||||
<div class="bulk-edit-row__cell-container">
|
||||
<div class="bulk-edit-row__cell bulk-edit-row__cell--status bulk-edit-row__cell--clickable"
|
||||
@click="action('price')">
|
||||
<div class="bulk-edit-row__data">
|
||||
<div class="bulk-edit-row__line bulk-edit-row__line--sub">
|
||||
<ph-tag size="16"/>
|
||||
{{ toFixed(premise.material_cost, '€') }}
|
||||
</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 class="bulk-edit-row__line bulk-edit-row__line--sub">
|
||||
<ph-chart-pie-slice size="16"/>
|
||||
{{ toPercent(premise.oversea_share) }}
|
||||
</div>
|
||||
<div class="edit-calculation-cell-line edit-calculation-cell-subline" v-if="showPriceIncomplete">
|
||||
<basic-badge variant="exception" icon="warning">INCOMPLETE</basic-badge>
|
||||
<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 class="edit-calculation-empty copyable-cell" v-else @click="action('price')">
|
||||
<basic-badge variant="exception" icon="warning">INCOMPLETE</basic-badge>
|
||||
<div class="bulk-edit-row__status">
|
||||
<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>
|
||||
|
||||
|
||||
<div class="edit-calculation-cell-container">
|
||||
<div v-if="showHu" class="edit-calculation-cell copyable-cell"
|
||||
<div class="bulk-edit-row__cell-container">
|
||||
<div class="bulk-edit-row__cell bulk-edit-row__cell--status bulk-edit-row__cell--clickable"
|
||||
@click="action('packaging')">
|
||||
<div class="edit-calculation-cell-line">
|
||||
<PhVectorThree/>
|
||||
{{ premise.handling_unit.length }} x
|
||||
{{ premise.handling_unit.width }} x
|
||||
{{ premise.handling_unit.height }} {{ premise.handling_unit.dimension_unit }}
|
||||
<div class="bulk-edit-row__data">
|
||||
<div class="bulk-edit-row__line bulk-edit-row__line--sub">
|
||||
<PhVectorThree size="16"/>
|
||||
{{ toDimension(premise.handling_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 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="edit-calculation-cell-line edit-calculation-cell-subline">
|
||||
<PhHash/>
|
||||
{{ premise.handling_unit.content_unit_count }} pcs.
|
||||
<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="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 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 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 class="bulk-edit-row__status">
|
||||
<transition name="badge-transition" mode="out-in">
|
||||
<circle-badge v-if="packagingCheck" :key="'check-packaging-' + id" variant="skeleton-grey"
|
||||
icon="check"></circle-badge>
|
||||
<circle-badge v-else :key="'error-packaging-' + 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"
|
||||
@click.ctrl="action('supplier-select')">
|
||||
<div class="bulk-edit-row__data">
|
||||
<div class="bulk-edit-row__line bulk-edit-row__line--sub">
|
||||
<ph-factory style="display: inline-block; vertical-align: middle;" size="16"/>
|
||||
{{ premise.supplier.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="edit-calculation-cell-container">
|
||||
<div class="edit-calculation-cell copyable-cell" v-if="showDestinations"
|
||||
<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="edit-calculation-cell-line">
|
||||
<span class="number-circle"> {{ destinationsCount }} </span> 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 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>{{ toFixed(destination.annual_amount, 'pcs.', 0) }}</div>
|
||||
<div>
|
||||
<basic-badge size="compact" variant="secondary">{{ toDestination(destination) }}</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 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="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="edit-calculation-actions-cell">
|
||||
<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"
|
||||
@click="editSingle"></icon-button>
|
||||
<icon-button icon="x" help-text="Remove from mass edit" help-text-position="left" @click="remove"></icon-button>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Checkbox from "@/components/UI/Checkbox.vue";
|
||||
import IconButton from "@/components/UI/IconButton.vue";
|
||||
import Flag from "@/components/UI/Flag.vue";
|
||||
import {mapStores} from "pinia";
|
||||
import {usePremiseEditStore} from "@/store/premiseEdit.js";
|
||||
import BasicBadge from "@/components/UI/BasicBadge.vue";
|
||||
import {
|
||||
PhBarbell,
|
||||
PhBarcode,
|
||||
PhEmpty,
|
||||
PhBarbell, PhBoat,
|
||||
PhChartPieSlice,
|
||||
PhFactory,
|
||||
PhHandCoins,
|
||||
PhHash,
|
||||
PhMapPin,
|
||||
PhPercent,
|
||||
PhVectorThree,
|
||||
PhVectorTwo
|
||||
PhMapPinLine,
|
||||
PhPackage, PhPath,
|
||||
PhTag, PhTrain, PhTruck,
|
||||
PhVectorThree
|
||||
} from "@phosphor-icons/vue";
|
||||
import {UrlSafeBase64} from "@/common.js";
|
||||
import Spinner from "@/components/UI/Spinner.vue";
|
||||
|
||||
import CircleBadge from "@/components/UI/CircleBadge.vue";
|
||||
|
||||
export default {
|
||||
name: "BulkEditRow",
|
||||
emits: ['remove', 'action'],
|
||||
emits: ['remove', 'action', 'select'],
|
||||
components: {
|
||||
Spinner,
|
||||
PhMapPin,
|
||||
PhPath,
|
||||
PhMapPinLine,
|
||||
PhPackage,
|
||||
PhTag,
|
||||
PhChartPieSlice,
|
||||
PhHandCoins,
|
||||
CircleBadge,
|
||||
PhFactory,
|
||||
PhPercent,
|
||||
PhBarcode, PhBarbell, PhHash, PhVectorThree, PhVectorTwo, PhEmpty, BasicBadge, Flag, IconButton, Checkbox
|
||||
PhBarbell,
|
||||
PhHash,
|
||||
PhVectorThree,
|
||||
BasicBadge,
|
||||
IconButton,
|
||||
Checkbox
|
||||
},
|
||||
props: {
|
||||
id: {
|
||||
|
|
@ -156,61 +256,113 @@ export default {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
destinationsCount() {
|
||||
return this.premise.destinations?.length ?? 0;
|
||||
materialCheck() {
|
||||
return (this.premise?.material.part_number != null && this.premise?.hs_code != null && this.premise?.tariff_rate != null)
|
||||
},
|
||||
destinationsText() {
|
||||
return this.premise.destinations.map(d => d.destination_node.name).join(', ');
|
||||
},
|
||||
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;
|
||||
priceCheck() {
|
||||
return (this.premise?.material_cost != null && this.premise?.oversea_share != null);
|
||||
},
|
||||
hu() {
|
||||
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),
|
||||
},
|
||||
methods: {
|
||||
toFixed(value) {
|
||||
return value !== null ? (value).toFixed(2) : '0.00';
|
||||
toDestination(destination, limit = 15) {
|
||||
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) {
|
||||
return value !== null ? (value * 100).toFixed(2) : '0.00';
|
||||
},
|
||||
updateSelected(value) {
|
||||
this.premiseEditStore.setSelectTo([this.id], value);
|
||||
return value !== null ? `${(value * 100).toFixed(2)} %` : 'N/A';
|
||||
},
|
||||
editSingle() {
|
||||
const bulkQuery = this.$route.params.ids;
|
||||
|
|
@ -220,6 +372,9 @@ export default {
|
|||
action(action) {
|
||||
this.$emit('action', {id: this.id, action: action});
|
||||
},
|
||||
updateSelected(value) {
|
||||
this.$emit("select", {id: this.id, checked: value});
|
||||
},
|
||||
remove() {
|
||||
this.premiseEditStore.removePremise(this.id);
|
||||
this.$emit('remove', this.id);
|
||||
|
|
@ -229,127 +384,212 @@ export default {
|
|||
</script>
|
||||
|
||||
<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;
|
||||
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;
|
||||
}
|
||||
|
||||
/* Main container */
|
||||
.bulk-edit-row {
|
||||
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;
|
||||
padding: 0 2.4rem;
|
||||
border-bottom: 0.16rem solid #f3f4f6;
|
||||
align-items: center;
|
||||
align-items: stretch;
|
||||
transition: background-color 0.2s ease;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
height: 14rem;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.bulk-edit-row:last-child {
|
||||
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;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
|
||||
.edit-calculation-cell--price {
|
||||
/* Cell container */
|
||||
.bulk-edit-row__cell-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
align-self: stretch;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.edit-calculation-cell--supplier {
|
||||
display: flex;
|
||||
gap: 1.2rem;
|
||||
height: 90%;
|
||||
/* Cell */
|
||||
.bulk-edit-row__cell {
|
||||
flex: 1 1 auto;
|
||||
margin: 1.6rem 0;
|
||||
padding: 0.8rem;
|
||||
border-radius: 0.8rem;
|
||||
}
|
||||
|
||||
.edit-calculation-cell--supplier-container {
|
||||
.bulk-edit-row__cell--status {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.edit-calculation-cell--supplier-flag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.bulk-edit-row__cell--clickable:hover {
|
||||
cursor: pointer;
|
||||
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;
|
||||
flex-direction: column;
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
.edit-calculation-cell-subline {
|
||||
.bulk-edit-row__line--sub {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 400;
|
||||
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;
|
||||
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;
|
||||
align-items: center;
|
||||
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>
|
||||
|
|
@ -181,8 +181,9 @@ export default {
|
|||
.calculation-list-supplier-data {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
gap: 0.4rem;
|
||||
height: 90%
|
||||
}
|
||||
|
||||
.supplier-name {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<div class="outer-container">
|
||||
<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">
|
||||
<div class="caption-column">{{ field.label }}</div>
|
||||
<div class="input-column">
|
||||
|
|
@ -15,6 +15,7 @@
|
|||
@blur="field.onBlur"
|
||||
class="input-field"
|
||||
autocomplete="off"
|
||||
:placeholder="fromMassEdit ? '<keep>' : ''"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -92,6 +93,10 @@ export default {
|
|||
responsive: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
fromMassEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
<div class="text-container">
|
||||
<input ref="lengthInput" :value="huLength" @blur="validateDimension('length', $event)"
|
||||
@keydown.enter="handleEnter('lengthInput', $event)" class="input-field"
|
||||
:placeholder="fromMassEdit ? '<keep>' : ''"
|
||||
autocomplete="off"/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -20,6 +21,7 @@
|
|||
<div class="text-container">
|
||||
<input ref="widthInput" :value="huWidth" @blur="validateDimension('width', $event)"
|
||||
@keydown.enter="handleEnter('widthInput', $event)" class="input-field"
|
||||
:placeholder="fromMassEdit ? '<keep>' : ''"
|
||||
autocomplete="off"/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -32,6 +34,7 @@
|
|||
<div class="text-container">
|
||||
<input ref="heightInput" :value="huHeight" @blur="validateDimension('height', $event)"
|
||||
@keydown.enter="handleEnter('heightInput', $event)" class="input-field"
|
||||
:placeholder="fromMassEdit ? '<keep>' : ''"
|
||||
autocomplete="off"/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -44,6 +47,7 @@
|
|||
<div class="text-container">
|
||||
<input ref="weightInput" :value="huWeight" @blur="validateWeight('weight', $event)"
|
||||
@keydown.enter="handleEnter('weightInput', $event)" class="input-field"
|
||||
:placeholder="fromMassEdit ? '<keep>' : ''"
|
||||
autocomplete="off"/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -57,6 +61,7 @@
|
|||
<div class="text-container">
|
||||
<input ref="unitCountInput" :value="huUnitCount" @blur="validateCount"
|
||||
@keydown.enter="handleEnter('unitCountInput', $event)" class="input-field"
|
||||
:placeholder="fromMassEdit ? '<keep>' : ''"
|
||||
autocomplete="off"/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -125,6 +130,10 @@ export default {
|
|||
responsive: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
fromMassEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -204,6 +213,12 @@ export default {
|
|||
const inputOrder = ['lengthInput', 'widthInput', 'heightInput', 'weightInput', 'unitCountInput'];
|
||||
|
||||
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(() => {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@
|
|||
<div class="caption-column">MEK_A [EUR]</div>
|
||||
<div class="input-column">
|
||||
<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"/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -15,7 +16,8 @@
|
|||
<div class="caption-column">Oversea share [%]</div>
|
||||
<div class="input-column">
|
||||
<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"/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -25,7 +27,7 @@
|
|||
<div class="caption-column">Include FCA Fee</div>
|
||||
<div class="input-column">
|
||||
<tooltip text="Select if a additional FCA has to be added during calculation">
|
||||
<checkbox :checked="includeFcaFee" @checkbox-changed="updateIncludeFcaFee"></checkbox>
|
||||
<checkbox ref="fcaInput" @enter="handleEnter('fcaInput', $event)" @keydown.enter="handleEnter('fcaInput', $event)" :checked="includeFcaFee" @checkbox-changed="updateIncludeFcaFee"></checkbox>
|
||||
</tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -62,6 +64,10 @@ export default {
|
|||
responsive: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
fromMassEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -73,6 +79,29 @@ export default {
|
|||
}
|
||||
},
|
||||
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) {
|
||||
if (!this.$el.contains(event.relatedTarget)) {
|
||||
this.$emit('save', 'price');
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import router from './router.js';
|
||||
//import store from './store/index.js';
|
||||
import {setupErrorBuffer} from './store/notification.js'
|
||||
import {createApp} from 'vue'
|
||||
import {createPinia} from 'pinia';
|
||||
|
|
@ -34,7 +33,8 @@ import {
|
|||
PhTruckTrailer,
|
||||
PhUpload,
|
||||
PhWarning,
|
||||
PhX
|
||||
PhX,
|
||||
PhExclamationMark, PhMapPin, PhEmpty, PhShippingContainer
|
||||
} from "@phosphor-icons/vue";
|
||||
import {setupSessionRefresh} from "@/store/activeuser.js";
|
||||
|
||||
|
|
@ -61,6 +61,8 @@ app.component('PhTruckTrailer', PhTruckTrailer);
|
|||
app.component('PhTruck', PhTruck);
|
||||
app.component('PhBoat', PhBoat);
|
||||
app.component('PhTrain', PhTrain);
|
||||
app.component('PhEmpty', PhEmpty);
|
||||
app.component('PhShippingContainer', PhShippingContainer);
|
||||
app.component('PhPencilSimple', PhPencilSimple);
|
||||
app.component('PhX', PhX);
|
||||
app.component('PhCloudArrowUp', PhCloudArrowUp);
|
||||
|
|
@ -74,6 +76,9 @@ app.component('PhFile', PhFile);
|
|||
app.component("PhDesktop", PhDesktop );
|
||||
app.component("PhHardDrives", PhHardDrives );
|
||||
app.component("PhClipboard", PhClipboard );
|
||||
app.component("PhExclamationMark", PhExclamationMark );
|
||||
app.component("PhMapPin", PhMapPin);
|
||||
|
||||
|
||||
app.use(router);
|
||||
|
||||
|
|
|
|||
|
|
@ -4,15 +4,15 @@
|
|||
<h2 class="page-header">Mass edit calculation</h2>
|
||||
<div class="header-controls">
|
||||
<basic-button :show-icon="false"
|
||||
:disabled="premiseEditStore.selectedLoading"
|
||||
:disabled="disableButtons"
|
||||
variant="secondary"
|
||||
@click="closeMassEdit"
|
||||
@click="close"
|
||||
>Close
|
||||
</basic-button>
|
||||
<basic-button :show-icon="true"
|
||||
:disabled="premiseEditStore.selectedLoading"
|
||||
:disabled="disableButtons"
|
||||
icon="Calculator" variant="primary"
|
||||
@click="startCalculation"
|
||||
@click="calculate"
|
||||
>Calculate & close
|
||||
</basic-button>
|
||||
|
||||
|
|
@ -24,13 +24,15 @@
|
|||
|
||||
<div class="edit-calculation-list-header" key="header">
|
||||
<div>
|
||||
<checkbox @checkbox-changed="updateCheckBoxes" :checked="overallCheck"></checkbox>
|
||||
<checkbox @checkbox-changed="updateCheckBoxes" :checked="overallCheck"
|
||||
:indeterminate="overallIndeterminate"></checkbox>
|
||||
</div>
|
||||
<div>Material</div>
|
||||
<div>Price</div>
|
||||
<div>Packaging</div>
|
||||
<div>Supplier</div>
|
||||
<div>Destinations & routes</div>
|
||||
<div>Annual Quantity</div>
|
||||
<div>Routes</div>
|
||||
<div>Actions</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -43,8 +45,8 @@
|
|||
<span class="space-around">No Calculations found.</span>
|
||||
</div>
|
||||
|
||||
<bulk-edit-row v-else class="edit-calculation-list-item" v-for="premise of this.premiseEditStore.getPremisses"
|
||||
:key="premise.id" :id="premise.id" :premise="premise" @action="onClickAction"
|
||||
<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" @select="updateCheckBox"
|
||||
@remove="updateUrl">
|
||||
</bulk-edit-row>
|
||||
|
||||
|
|
@ -65,9 +67,11 @@
|
|||
v-model:tariffRate="componentProps.tariffRate"
|
||||
v-model:tariffUnlocked="componentProps.tariffUnlocked"
|
||||
v-model:description="componentProps.description"
|
||||
|
||||
v-model:price="componentProps.price"
|
||||
v-model:overSeaShare="componentProps.overSeaShare"
|
||||
v-model:includeFcaFee="componentProps.includeFcaFee"
|
||||
|
||||
v-model:length="componentProps.length"
|
||||
v-model:width="componentProps.width"
|
||||
v-model:height="componentProps.height"
|
||||
|
|
@ -77,16 +81,24 @@
|
|||
v-model:unitCount="componentProps.unitCount"
|
||||
v-model:mixable="componentProps.mixable"
|
||||
v-model:stackable="componentProps.stackable"
|
||||
|
||||
v-model:hideDescription="componentProps.hideDescription"
|
||||
|
||||
:fromMassEdit="true"
|
||||
:countryId=null
|
||||
:responsive="false"
|
||||
|
||||
@close="closeEditModalAction('cancel')"
|
||||
@accept="closeEditModalAction('accept')"
|
||||
|
||||
>
|
||||
</component>
|
||||
|
||||
<div class="modal-content-footer">
|
||||
<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 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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -137,11 +149,20 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
...mapStores(usePremiseEditStore, useNotificationStore),
|
||||
disableButtons() {
|
||||
return this.premiseEditStore.selectedLoading;
|
||||
},
|
||||
premises() {
|
||||
return this.premiseEditStore.getPremisses;
|
||||
},
|
||||
hasSelection() {
|
||||
if (this.premiseEditStore.isLoading || this.premiseEditStore.selectedLoading) {
|
||||
return false;
|
||||
}
|
||||
return this.premiseEditStore.getSelectedPremissesIds?.length > 0;
|
||||
return this.premiseEditStore.someChecked;
|
||||
},
|
||||
showMultiselectAction() {
|
||||
return this.selectCount > 0;
|
||||
},
|
||||
selectCount() {
|
||||
return this.selectedPremisses?.length ?? 0;
|
||||
|
|
@ -158,14 +179,8 @@ export default {
|
|||
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() {
|
||||
return this.modalType === 'material' && !this.componentProps.tariffUnlocked;
|
||||
return this.modalType === 'material' && !this.componentProps.tariffUnlocked; //TODO: check all selected.
|
||||
},
|
||||
showEditModal() {
|
||||
return ((this.modalType ?? null) !== null);
|
||||
|
|
@ -190,8 +205,7 @@ export default {
|
|||
showProcessingModal(newState, _) {
|
||||
if (newState) {
|
||||
this.notificationStore.setSpinner(this.shownProcessingMessage);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
this.notificationStore.clearSpinner();
|
||||
}
|
||||
}
|
||||
|
|
@ -201,15 +215,25 @@ export default {
|
|||
this.ids = new UrlSafeBase64().decodeIds(this.$route.params.ids);
|
||||
this.premiseEditStore.loadPremissesForced(this.ids);
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
ids: [],
|
||||
overallCheck: false,
|
||||
overallIndeterminate: false,
|
||||
bulkQuery: null,
|
||||
modalType: null,
|
||||
componentsData: {
|
||||
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: {
|
||||
props: {
|
||||
length: 0,
|
||||
|
|
@ -247,27 +271,48 @@ export default {
|
|||
});
|
||||
}
|
||||
},
|
||||
async startCalculation() {
|
||||
async calculate() {
|
||||
this.showCalculationModal = true;
|
||||
const error = await this.premiseEditStore.startCalculation();
|
||||
|
||||
if (error === null) {
|
||||
this.closeMassEdit()
|
||||
this.close()
|
||||
}
|
||||
this.showCalculationModal = false;
|
||||
},
|
||||
closeMassEdit() {
|
||||
close() {
|
||||
this.$router.push({name: "calculation-list"});
|
||||
},
|
||||
updateCheckBox(data) {
|
||||
this.premiseEditStore.setChecked(data.id, data.checked);
|
||||
this.updateOverallCheckBox();
|
||||
},
|
||||
updateCheckBoxes(value) {
|
||||
console.log("set all", value)
|
||||
this.premiseEditStore.setAll(value);
|
||||
this.updateOverallCheckBox();
|
||||
},
|
||||
updateOverallCheckBox() {
|
||||
this.overallCheck = this.premiseEditStore.allChecked;
|
||||
|
||||
if (!this.overallCheck)
|
||||
this.overallIndeterminate = this.premiseEditStore.someChecked;
|
||||
},
|
||||
multiselectAction(action) {
|
||||
this.openModal(action, this.selectedPremisses.map(p => p.id));
|
||||
},
|
||||
onClickAction(data) {
|
||||
if (data.action === 'supplier-select') {
|
||||
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) {
|
||||
|
||||
|
|
@ -383,7 +428,7 @@ export default {
|
|||
<style scoped>
|
||||
|
||||
/* 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;
|
||||
background-color: #f8fafc;
|
||||
border-radius: 0.8rem;
|
||||
|
|
@ -456,7 +501,7 @@ export default {
|
|||
|
||||
.edit-calculation-list-header {
|
||||
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;
|
||||
padding: 2.4rem;
|
||||
background-color: #ffffff;
|
||||
|
|
|
|||
|
|
@ -215,6 +215,13 @@ export default {
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
.edit-calculation-spinner-container
|
||||
{
|
||||
display: flex;
|
||||
padding-top: 10rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ export const useNotificationStore = defineStore('notification', {
|
|||
const pinia = this.$pinia || getActivePinia()
|
||||
if (pinia && pinia._s) {
|
||||
pinia._s.forEach((store, storeId) => {
|
||||
if (storeId !== 'error' && storeId !== 'errorLog' && store.$state) {
|
||||
if (storeId !== 'notification' && storeId !== 'errorLog' && store.$state) {
|
||||
storeState[storeId] = {
|
||||
...toRaw(store.$state)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
state() {
|
||||
return {
|
||||
premisses: null,
|
||||
selectedIds: [],
|
||||
|
||||
/**
|
||||
* set to true while the store is loading the premises.
|
||||
|
|
@ -20,14 +21,7 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
*/
|
||||
selectedLoading: false,
|
||||
|
||||
|
||||
destinations: null,
|
||||
processDestinationMassEdit: false,
|
||||
|
||||
selectedDestination: null,
|
||||
|
||||
throwsException: true,
|
||||
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
|
|
@ -54,21 +48,21 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the ids of all premises.
|
||||
* @param state
|
||||
* @returns {*}
|
||||
*/
|
||||
getPremiseIds(state) {
|
||||
if (state.loading) {
|
||||
if (state.throwsException)
|
||||
throw new Error("Premises are accessed while still loading.");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return state.premisses?.map(p => p.id);
|
||||
},
|
||||
// /**
|
||||
// * Returns the ids of all premises.
|
||||
// * @param state
|
||||
// * @returns {*}
|
||||
// */
|
||||
// getPremiseIds(state) {
|
||||
// if (state.loading) {
|
||||
// if (state.throwsException)
|
||||
// throw new Error("Premises are accessed while still loading.");
|
||||
//
|
||||
// return null;
|
||||
// }
|
||||
//
|
||||
// return state.premisses?.map(p => p.id);
|
||||
// },
|
||||
|
||||
/**
|
||||
* Returns the premises.
|
||||
|
|
@ -242,7 +236,30 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
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;
|
||||
|
||||
|
||||
|
||||
return await this.saveMaterial(ids, materialData);
|
||||
},
|
||||
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
|
||||
*/
|
||||
|
|
@ -733,71 +733,51 @@ export const usePremiseEditStore = defineStore('premiseEdit', {
|
|||
* PREMISE stuff
|
||||
* =================
|
||||
*/
|
||||
deselectPremise() {
|
||||
this.selectedLoading = true;
|
||||
|
||||
this.premisses.forEach(p => p.selected = false);
|
||||
|
||||
this.destinations = null;
|
||||
this.selectedDestination = null;
|
||||
this.selectedLoading = false;
|
||||
},
|
||||
setAll(value) {
|
||||
|
||||
this.selectedLoading = true;
|
||||
|
||||
const updatedPremises = this.premisses.map(p => ({
|
||||
...p,
|
||||
selected: value
|
||||
}));
|
||||
this.premisses = updatedPremises;
|
||||
const temp = [];
|
||||
|
||||
if (value)
|
||||
this.premisses.forEach(p => temp.push(p.id));
|
||||
|
||||
this.selectedIds = temp;
|
||||
|
||||
this.selectedLoading = false;
|
||||
|
||||
},
|
||||
setSelectTo(ids, value) {
|
||||
setChecked(premiseId, checked) {
|
||||
this.selectedLoading = true;
|
||||
|
||||
const idsSet = new Set(ids);
|
||||
const updatedPremises = this.premisses.map(p => ({
|
||||
...p,
|
||||
selected: idsSet.has(p.id) ? value : p.selected
|
||||
}));
|
||||
this.premisses = updatedPremises;
|
||||
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);
|
||||
}
|
||||
|
||||
this.selectedLoading = false;
|
||||
},
|
||||
async selectSinglePremise(id, ids) {
|
||||
setBy(type, ofId) {
|
||||
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.prepareDestinations(id, [id]);
|
||||
|
||||
this.selectedLoading = false;
|
||||
},
|
||||
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.forEach(p => {
|
||||
if(type === 'supplier' && p.supplier.id === premise.supplier.id) {
|
||||
temp.push(p.id);
|
||||
} else if(type === 'material' && p.material.id === premise.material.id) {
|
||||
temp.push(p.id);
|
||||
}
|
||||
});
|
||||
this.premisses = data;
|
||||
|
||||
this.premisses.forEach(p => p.selected = true);
|
||||
this.prepareDestinations(id, [id]);
|
||||
this.selectedIds = temp;
|
||||
|
||||
this.selectedLoading = false;
|
||||
this.loading = 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;
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ public class GlobalExceptionHandler {
|
|||
public ResponseEntity<ErrorResponseDTO> handlePremiseValidationException(PremiseValidationError exception) {
|
||||
ErrorDTO error = new ErrorDTO(
|
||||
exception.getClass().getName(),
|
||||
"Premiss validation error",
|
||||
"Validation error",
|
||||
exception.getMessage(),
|
||||
Arrays.asList(exception.getStackTrace())
|
||||
);
|
||||
|
|
|
|||
|
|
@ -278,7 +278,10 @@ public class DestinationRepository {
|
|||
Destination entity = new Destination();
|
||||
|
||||
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.setDestinationNodeId(rs.getInt("destination_node_id"));
|
||||
entity.setRateD2d(rs.getBigDecimal("rate_d2d"));
|
||||
|
|
|
|||
|
|
@ -140,7 +140,6 @@ public class PremisesService {
|
|||
calculationIds.add(calculationJobRepository.insert(job));
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
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.error.SysErrorRepository;
|
||||
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.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
|
@ -19,28 +21,32 @@ import java.util.concurrent.TimeUnit;
|
|||
@Service
|
||||
public class BulkOperationExecutionService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(BulkOperationExecutionService.class);
|
||||
private final BulkOperationRepository bulkOperationRepository;
|
||||
private final BulkExportService bulkExportService;
|
||||
private final BulkImportService bulkImportService;
|
||||
private final SysErrorRepository sysErrorRepository;
|
||||
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.bulkExportService = bulkExportService;
|
||||
this.bulkImportService = bulkImportService;
|
||||
this.sysErrorRepository = sysErrorRepository;
|
||||
this.sysErrorTransformer = sysErrorTransformer;
|
||||
this.bulkProcessingExecutor = bulkProcessingExecutor;
|
||||
|
||||
}
|
||||
|
||||
@Async("bulkProcessingExecutor")
|
||||
public CompletableFuture<Void> launchExecution(Integer id) {
|
||||
logger.info("Starting bulk operation execution for ID: {}", id);
|
||||
try {
|
||||
execution(id);
|
||||
logger.info("Bulk operation execution completed successfully for ID: {}", id);
|
||||
return CompletableFuture.completedFuture(null);
|
||||
} catch (Throwable e) {
|
||||
logger.error("Error during bulk operation execution for ID: {}", id, e);
|
||||
bulkOperationRepository.updateState(id, BulkOperationState.EXCEPTION);
|
||||
|
||||
var error = new SysError();
|
||||
|
|
@ -59,13 +65,14 @@ public class BulkOperationExecutionService {
|
|||
}
|
||||
|
||||
public void execution(Integer id) {
|
||||
|
||||
logger.debug("Executing bulk operation for ID: {}", id);
|
||||
var operation = bulkOperationRepository.getOperationById(id);
|
||||
|
||||
if (operation.isPresent()) {
|
||||
var op = operation.get();
|
||||
|
||||
if (op.getProcessState() == BulkOperationState.SCHEDULED) {
|
||||
logger.info("Processing bulk operation ID: {}, type: {}, file type: {}", id, op.getProcessingType(), op.getFileType());
|
||||
bulkOperationRepository.updateState(id, BulkOperationState.PROCESSING);
|
||||
try {
|
||||
if (op.getProcessingType() == BulkProcessingType.EXPORT) {
|
||||
|
|
@ -78,6 +85,9 @@ public class BulkOperationExecutionService {
|
|||
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
|
||||
logger.error("Error during bulk operation execution for ID: {}", id, e);
|
||||
|
||||
op.setProcessState(BulkOperationState.EXCEPTION);
|
||||
|
||||
var error = new SysError();
|
||||
|
|
|
|||
|
|
@ -168,11 +168,12 @@ public class MaterialFastExcelMapper {
|
|||
// Extract and validate data
|
||||
String partNumber = getCellValue(row, MaterialHeader.PART_NUMBER.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);
|
||||
|
||||
// Validate lengths
|
||||
validateLength(partNumber, 0, 12, "Part Number", rowNumber);
|
||||
if (hsCode != null)
|
||||
validateLength(hsCode, 0, 11, "HS Code", rowNumber);
|
||||
validateLength(description, 1, 500, "Description", rowNumber);
|
||||
|
||||
|
|
@ -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
|
||||
*/
|
||||
|
|
@ -248,6 +256,6 @@ public class MaterialFastExcelMapper {
|
|||
* Validates HS Code (placeholder for API validation)
|
||||
*/
|
||||
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