Intermediate commit:

- bulk operations
This commit is contained in:
Jan 2025-09-23 16:47:39 +02:00
parent ac85db0064
commit fa01115c03
20 changed files with 482 additions and 127 deletions

View file

@ -32,9 +32,6 @@ export default {
box-shadow: 0 0.4rem 0.6rem -0.1rem rgba(0, 0, 0, 0.1);
}
.box-bordered:hover {
background-color: rgba(107, 134, 156, 0.02);
}
.box-bordered {

View file

@ -39,6 +39,10 @@ export default {
disabled: {
type: Boolean,
default: false
},
minWidth: {
type: String,
default: '200px'
}
},
data() {
@ -52,8 +56,9 @@ export default {
return `tooltip-${this.position}`
},
tooltipStyles() {
// Hier können zusätzliche dynamische Styles hinzugefügt werden
return {}
return {
minWidth: this.minWidth
}
}
},
methods: {
@ -99,9 +104,11 @@ export default {
font-size: 14px;
line-height: 1.4;
z-index: 1000;
white-space: nowrap;
white-space: normal; /* Changed from nowrap to normal */
word-wrap: break-word; /* Only break very long words */
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
pointer-events: none;
max-width: 800px; /* Further increased default max width */
}
/* Positionierung */
@ -146,7 +153,7 @@ export default {
transform: translateX(-50%);
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 6px solid #333;
border-top: 6px solid #001D33; /* Fixed color to match background */
}
.tooltip-arrow-bottom {
@ -155,7 +162,7 @@ export default {
transform: translateX(-50%);
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid #333;
border-bottom: 6px solid #001D33; /* Fixed color to match background */
}
.tooltip-arrow-left {
@ -164,7 +171,7 @@ export default {
transform: translateY(-50%);
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
border-left: 6px solid #333;
border-left: 6px solid #001D33; /* Fixed color to match background */
}
.tooltip-arrow-right {
@ -173,7 +180,7 @@ export default {
transform: translateY(-50%);
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
border-right: 6px solid #333;
border-right: 6px solid #001D33; /* Fixed color to match background */
}
/* Animation */
@ -244,8 +251,8 @@ export default {
.tooltip {
font-size: 12px;
padding: 6px 10px;
max-width: 200px;
white-space: normal;
max-width: 350px; /* Increased mobile max width */
white-space: normal; /* Ensure line breaks on mobile */
}
}
</style>

View file

@ -0,0 +1,126 @@
<template>
<div class="bulk-operation-container">
<div class="bulk-operation-info-container">
<div class="bulk-operation-info-icon">
<ph-file size="28"></ph-file>
</div>
<div class="bulk-operation-info">
<div>{{ operation.processing_type.toLowerCase() }} {{ operation.file_type.toLowerCase() }}</div>
<div class="bulk-operation-date">{{ buildDate(operation.timestamp) }}</div>
</div>
</div>
<div class="bulk-operation-status">
<div v-if="operation.state === 'EXCEPTION'">
<tooltip min-width="500px" :text="shortend(operation.error.message)" position="left">
<basic-badge variant="exception">ERROR</basic-badge>
</tooltip>
</div>
<basic-badge v-else-if="operation.state === 'COMPLETED'">COMPLETED</basic-badge>
<basic-badge v-else-if="operation.state === 'SCHEDULED'" variant="skeleton">SCHEDULED</basic-badge>
<basic-badge v-else-if="operation.state === 'PROCESSING'" variant="skeleton">PROCESSING</basic-badge>
</div>
<div class="bulk-operation-file">
<spinner size="s" v-if="operation.state === 'PROCESSING' || operation.state === 'SCHEDULED'"></spinner>
<div v-else-if="operation.processing_type === 'EXPORT'">
<tooltip text="Download data export" position="left">
<icon-button icon="download" @click="$emit('download' , operation.id)"></icon-button>
</tooltip>
</div>
</div>
</div>
</template>
<script>
import BasicButton from "@/components/UI/BasicButton.vue";
import IconButton from "@/components/UI/IconButton.vue";
import Tooltip from "@/components/UI/Tooltip.vue";
import Spinner from "@/components/UI/Spinner.vue";
import BasicBadge from "@/components/UI/BasicBadge.vue";
export default {
name: "BulkOperation",
components: {BasicBadge, Spinner, Tooltip, IconButton, BasicButton},
props: {
operation: {
type: Object,
required: true
}
},
methods: {
shortend(string) {
if(((string ?? null) === null) || string.length < 350)
return string;
return string.substring(0,350) + "..."
},
buildDate(date) {
return `${date[0]}-${date[1].toString().padStart(2, '0')}-${date[2].toString().padStart(2, '0')} ${date[3].toString().padStart(2, '0')}:${date[4]?.toString().padStart(2, '0') || '00'}:${date[5]?.toString().padStart(2, '0') || '00'}`
},
},
data() {
return {}
}
};
</script>
<style scoped>
.bulk-operation-file {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
.bulk-operation-info-icon {
color: #6B869C;
}
.bulk-operation-info-container {
display: flex;
flex-direction: row;
align-items: center;
gap: 1.2rem;
width: 100%;
}
.bulk-operation-container {
padding: 0.8rem 0;
}
.bulk-operation-info {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.bulk-operation-status {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
}
.bulk-operation-container {
display: grid;
grid-template-columns: 1fr 3fr 5rem;
font-size: 1.4rem;
gap: 3.6rem;
color: #001D33;
width: 100%;
}
.bulk-operation-date {
font-size: 1.2rem;
color: #6B869C;
}
</style>

View file

@ -2,11 +2,6 @@
<div>
<div class="bulk-operations-container">
<box variant="border" class="bulk-operation-box-status-container">
<div class="bulk-operations-sub-container">
<div class="bulk-operation-header">Bulk operation status</div>
</div>
</box>
<box variant="border" class="bulk-operations-box-container">
<div class="bulk-operations-sub-container">
@ -40,7 +35,7 @@
</div>
<div class="bulk-operation-action-footer">
<basic-button @click="downloadFile" icon="download">Schedule Export</basic-button>
<basic-button @click="downloadFile" icon="download">Export</basic-button>
</div>
</div>
</box>
@ -73,16 +68,20 @@
<div id="selectedFile" class="selected-file" v-if="selectedFileName">{{ selectedFileName }}</div>
</div>
<div class="bulk-operation-action-footer">
<basic-button @click="uploadFile" icon="upload" :disabled="!selectedFile">Import</basic-button>
</div>
</div>
</box>
<div class="bulk-operation-box-status-container">
<div class="bulk-operation-status">
<div class="bulk-operation-header">History</div>
<div v-if="this.bulkOperationStore.getBulkOperations.length === 0" class="empty-container">No recent bulk operations</div>
<bulk-operation v-else v-for="bulk in this.bulkOperationStore.getBulkOperations" :key="bulk.id" :operation="bulk" @download="fetchFile"></bulk-operation>
</div>
</div>
</div>
</div>
</template>
@ -97,10 +96,13 @@ import {config} from "@/config.js";
import Dropdown from "@/components/UI/Dropdown.vue";
import {mapStores} from "pinia";
import {useValidityPeriodStore} from "@/store/validityPeriod.js";
import {useBulkOperationStore} from "@/store/bulkOperation.js";
import BulkOperation from "@/components/layout/bulkoperation/BulkOperation.vue";
import logger from "@/logger.js";
export default {
name: "BulkOperations",
components: {Dropdown, RadioOption, BasicButton, Box},
components: {BulkOperation, Dropdown, RadioOption, BasicButton, Box},
data() {
return {
exportType: "templates",
@ -113,10 +115,13 @@ export default {
}
},
computed: {
...mapStores(useValidityPeriodStore),
...mapStores(useValidityPeriodStore, useBulkOperationStore),
showValidityPeriod() {
return this.exportType === "download" && (this.exportDataset === "COUNTRY_MATRIX" || this.exportDataset === "CONTAINER_RATE");
},
currentPeriod() {
return this.validityPeriodStore.getCurrentPeriodId;
},
selectedPeriod: {
get() {
return this.validityPeriodStore.getSelectedPeriod
@ -148,13 +153,22 @@ export default {
},
created() {
this.validityPeriodStore.loadPeriods();
this.bulkOperationStore.updateStatus();
},
methods: {
async fetchFile(id) {
logger.info(`Fetching file ${id}`);
this.bulkOperationStore.downloadBulkFile(id);
},
async downloadFile() {
const fileName = `lcc_export_${this.exportDataset.toLowerCase()}_${this.exportType.toLowerCase()}.xlsx`;
const url = `${config.backendUrl}/bulk/${this.exportType}/${this.exportDataset}/`
this.processId = await performDownload(url, fileName);
if (this.exportType === "templates") {
const fileName = `lcc_export_${this.exportType.toLowerCase()}_${this.exportDataset.toLowerCase()}.xlsx`;
const url = `${config.backendUrl}/bulk/templates/${this.exportDataset}/`
await performDownload(url, fileName);
} else {
const isCurrent = this.selectedPeriod === this.currentPeriod;
await this.bulkOperationStore.scheduleDownload(this.exportDataset, isCurrent ? null : this.selectedPeriod);
}
},
inputFile(event) {
const file = event.target.files[0];
@ -171,8 +185,7 @@ export default {
if (!this.selectedFile)
return;
const url = `${config.backendUrl}/bulk/upload/${this.importDataset}/`
this.processId = await performUpload(url, this.selectedFile).catch(error => {});
await this.bulkOperationStore.scheduleUpload(this.importDataset, this.selectedFile);
}
}
}
@ -181,6 +194,23 @@ export default {
<style scoped>
.empty-container {
display: flex;
justify-content: center;
align-items: center;
font-size: 1.6rem;
flex: 1;
}
.bulk-operation-status {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
gap: 1.2rem;
flex: 1;
}
.bulk-operations-container {
display: grid;
grid-template-columns: 1fr 1fr;

View file

@ -26,16 +26,6 @@
</box>
</div>
<div v-if="selectedCountry" >
<div v-if="!loading" class="period-select-container">
<span class="period-select-caption">Property set:</span>
<dropdown :options="periods"
emptyText="No property set available"
class="period-select"
placeholder="Select a property set"
v-model="selectedPeriod"
></dropdown>
</div>
<transition name="properties-fade" mode="out-in">
<div v-if="!loading" class="properties-list">
<transition-group name="property-item" tag="div">
@ -209,7 +199,7 @@ export default {
.country-container {
display: flex;
flex-direction: column;
min-height: 50vh;
}
.country-search-container {

View file

@ -33,6 +33,12 @@
</transition-group>
</div>
</transition>
<box variant="border">
<country-properties></country-properties>
</box>
</div>
</template>
@ -48,10 +54,14 @@ import ModalDialog from "@/components/UI/ModalDialog.vue";
import NotificationBar from "@/components/UI/NotificationBar.vue";
import {usePropertySetsStore} from "@/store/propertySets.js";
import {useCountryStore} from "@/store/country.js";
import CountryProperties from "@/components/layout/config/CountryProperties.vue";
import Box from "@/components/UI/Box.vue";
export default {
name: "Properties",
components: {NotificationBar, ModalDialog, Tooltip, IconButton, BasicButton, Dropdown, Property},
components: {
Box,
CountryProperties, NotificationBar, ModalDialog, Tooltip, IconButton, BasicButton, Dropdown, Property},
data() {
return {
modalDialogDeleteState: false,

View file

@ -20,12 +20,12 @@ import TabContainer from "@/components/UI/TabContainer.vue";
import {markRaw} from "vue";
import Properties from "@/components/layout/config/Properties.vue";
import Box from "@/components/UI/Box.vue";
import CountryProperties from "@/components/layout/config/CountryProperties.vue";
import StagedChanges from "@/components/layout/config/StagedChanges.vue";
import BulkOperations from "@/components/layout/config/BulkOperations.vue";
import Rates from "@/components/layout/config/Rates.vue";
import Nodes from "@/components/layout/config/Nodes.vue";
import Materials from "@/components/layout/config/Materials.vue";
import ErrorLog from "@/pages/ErrorLog.vue";
export default {
name: "Config",
@ -39,8 +39,8 @@ export default {
component: markRaw(Properties),
},
{
title: 'Countries',
component: markRaw(CountryProperties),
title: 'System error log',
component: markRaw(ErrorLog),
},
{
title: 'Materials',

View file

@ -1,7 +1,5 @@
<template>
<div>
<h2 class="page-header">Errors</h2>
<modal :state="showModal">
<error-log-modal :error="error" @close="showModal = false"></error-log-modal>
</modal>
@ -30,12 +28,12 @@ export default {
pageSize: 10,
pagination: { page: 1, pageCount: 10, totalCount: 1 },
columns: [
{key: 'timestamp', label: 'Timestamp', formatter: (value) => this.buildDate(value) },
{key: 'user_id', label: 'User'},
{key: 'type', label: 'Type'},
{key: 'title', label: 'Title'},
{key: 'message', label: 'Message'},
{key: 'code', label: 'Exception'},
{key: 'timestamp', label: 'Timestamp', formatter: (value) => this.buildDate(value) },
{key: 'user_id', label: 'User'},
],
}

View file

@ -0,0 +1,96 @@
import {defineStore} from 'pinia'
import {config} from '@/config'
import {useErrorStore} from "@/store/error.js";
import performRequest, {performDownload, performUpload} from "@/backend.js";
export const useBulkOperationStore = defineStore('bulkOperation', {
state() {
return {
bulkOperations: [],
loading: false,
updateInterval: 3000,
updateTimer: null
}
},
getters: {
getBulkOperations(state) {
return state.bulkOperations;
},
isLoading(state) {
return state.loading;
}
},
actions: {
async scheduleDownload(dataset, period) {
const url = `${config.backendUrl}/bulk/download/${dataset}/${period === null ? '' : period}`;
await performRequest(this, 'GET', url, null, false);
await this.updateStatus();
this.startTimer();
},
async scheduleUpload(dataset, file) {
const url = `${config.backendUrl}/bulk/upload/${dataset}/`
await performUpload(url, file, false);
await this.updateStatus();
this.startTimer();
},
async timerMethod() {
this.updateStatus();
const restart = this.restartNeeded();
console.log("state " + this.bulkOperations.map(b => b.state).join(", ") + "restarting " + restart);
this.stopTimer();
if(restart) {
this.startTimer();
}
},
async updateStatus() {
this.loading = true;
const url = `${config.backendUrl}/bulk/status`;
const {data: data, headers: headers} = await performRequest(this, "GET", url, null, true);
this.bulkOperations = data;
this.loading = false;
},
restartNeeded() {
if((this.updateTimer === null) && this.bulkOperations.some(op => (op.state === 'SCHEDULED' || op.state === 'PROCESSING')))
return true;
if (!this.bulkOperations.some(op => (op.state === 'SCHEDULED' || op.state === 'PROCESSING')))
return false;
return true;
},
async downloadBulkFile(id) {
const op = this.bulkOperations.find(op => op.id === id);
if ((op ?? null) === null) return;
const fileName = this.getFileName(op.file_type.toLowerCase(), op.processing_type.toLowerCase());
const url = `${config.backendUrl}/bulk/file/${id}`;
await performDownload(url, fileName);
},
getFileName(dataset, type) {
return `lcc_export_${type}_${dataset}.xlsx`;
},
startTimer() {
if (this.updateTimer) return
console.log("start timer")
this.updateTimer = setTimeout(() => {
this.timerMethod()
}, this.updateInterval)
},
stopTimer() {
if (this.updateTimer) {
console.log("stop timer")
clearTimeout(this.updateTimer)
this.updateTimer = null
}
},
}
});

View file

@ -36,7 +36,7 @@ public class BulkOperationController {
@GetMapping({"/status/", "/status"})
public ResponseEntity<List<BulkOperationDTO>> getUploadStatus() {
public ResponseEntity<List<BulkOperationDTO>> getBulkStatus() {
return ResponseEntity.ok(bulkOperationService.getStatus());
}

View file

@ -1,6 +1,7 @@
package de.avatic.lcc.dto.bulk;
import com.fasterxml.jackson.annotation.JsonProperty;
import de.avatic.lcc.dto.error.ErrorLogDTO;
import java.time.LocalDateTime;
@ -19,6 +20,8 @@ public class BulkOperationDTO {
@JsonProperty("timestamp")
private LocalDateTime createdAt;
private ErrorLogDTO error;
public int getId() {
return id;
}
@ -58,4 +61,12 @@ public class BulkOperationDTO {
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public void setError(ErrorLogDTO error) {
this.error = error;
}
public ErrorLogDTO getError() {
return error;
}
}

View file

@ -32,6 +32,8 @@ public class BulkOperationRepository {
@Transactional
public Integer insert(BulkOperation operation) {
removeOld(operation.getUserId());
String sql = """
INSERT INTO bulk_operation (user_id, bulk_file_type, bulk_processing_type, state, file, validity_period_id)
VALUES (?, ?, ?, ?, ?, ?)
@ -63,14 +65,44 @@ public class BulkOperationRepository {
}
@Transactional
public void removeOld() {
String sql = """
DELETE FROM bulk_operation
WHERE created_at < DATE_SUB(NOW(), INTERVAL 7 DAY)
AND state NOT IN ('QUEUED', 'PROCESSING')
""";
public void removeOld(Integer userId) {
// First, update sys_error records to set bulk_operation_id to NULL
// for bulk operations that will be deleted (all but the 10 newest for the current user)
String updateErrorsSql = """
UPDATE sys_error
SET bulk_operation_id = NULL
WHERE bulk_operation_id IN (
SELECT id FROM (
SELECT id
FROM bulk_operation
WHERE user_id = ?
AND state NOT IN ('SCHEDULED', 'PROCESSING')
ORDER BY created_at DESC
LIMIT 18446744073709551615 OFFSET 10
) AS old_operations
)
""";
jdbcTemplate.update(sql);
jdbcTemplate.update(updateErrorsSql, userId);
// Then delete the old bulk_operation entries (keeping only the 10 newest for the current user)
String deleteBulkSql = """
DELETE FROM bulk_operation
WHERE user_id = ?
AND state NOT IN ('SCHEDULED', 'PROCESSING')
AND id NOT IN (
SELECT id FROM (
SELECT id
FROM bulk_operation
WHERE user_id = ?
AND state NOT IN ('SCHEDULED', 'PROCESSING')
ORDER BY created_at DESC
LIMIT 10
) AS newest_operations
)
""";
jdbcTemplate.update(deleteBulkSql, userId, userId);
}
@Transactional
@ -90,7 +122,8 @@ public class BulkOperationRepository {
SELECT id, user_id, bulk_file_type, bulk_processing_type, state, created_at
FROM bulk_operation
WHERE user_id = ?
ORDER BY created_at DESC
ORDER BY created_at DESC LIMIT 10
""";
return jdbcTemplate.query(sql, new BulkOperationRowMapper(true), userId);

View file

@ -7,6 +7,7 @@ import de.avatic.lcc.model.error.SysErrorType;
import de.avatic.lcc.repositories.pagination.SearchQueryPagination;
import de.avatic.lcc.repositories.pagination.SearchQueryResult;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.support.GeneratedKeyHolder;
@ -14,7 +15,10 @@ import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import javax.swing.text.html.Option;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.*;
import java.util.stream.Collectors;
@ -137,20 +141,7 @@ public class SysErrorRepository {
parameters.addValue("offset", pagination.getOffset());
// Execute query
List<SysError> errors = namedParameterJdbcTemplate.query(sql, parameters, (rs, rowNum) -> {
SysError error = new SysError();
error.setId(rs.getInt("id"));
error.setUserId(rs.getObject("user_id", Integer.class));
error.setTitle(rs.getString("title"));
error.setCode(rs.getString("code"));
error.setMessage(rs.getString("message"));
error.setPinia(rs.getString("pinia"));
error.setCalculationJobId(rs.getObject("calculation_job_id", Integer.class));
error.setBulkOperationId(rs.getObject("bulk_operation_id", Integer.class));
error.setType(SysErrorType.valueOf(rs.getString("type")));
error.setCreatedAt(rs.getTimestamp("created_at").toLocalDateTime());
return error;
});
List<SysError> errors = namedParameterJdbcTemplate.query(sql, parameters, new SysErrorMapper());
// Load trace items for each error
if (!errors.isEmpty()) {
@ -183,16 +174,7 @@ public class SysErrorRepository {
List<SysErrorTraceItem> allTraceItems = namedParameterJdbcTemplate.query(
traceSql,
traceParameters,
(rs, rowNum) -> {
SysErrorTraceItem traceItem = new SysErrorTraceItem();
traceItem.setErrorId(rs.getInt("error_id"));
traceItem.setId(rs.getInt("id"));
traceItem.setLine(rs.getObject("line", Integer.class));
traceItem.setFile(rs.getString("file"));
traceItem.setMethod(rs.getString("method"));
traceItem.setFullPath(rs.getString("fullPath"));
return traceItem;
}
new SysErrorTraceItemMapper()
);
// Group trace items by error ID
@ -206,4 +188,55 @@ public class SysErrorRepository {
});
}
public Optional<SysError> getByBulkOperationId(Integer id) {
String sql = """
SELECT * FROM sys_error WHERE bulk_operation_id = :id
""";
MapSqlParameterSource parameters = new MapSqlParameterSource("id", id);
List<SysError> errors = namedParameterJdbcTemplate.query(sql, parameters, new SysErrorMapper());
if(errors.isEmpty()) return Optional.empty();
SysError error = errors.getFirst();
loadTraceItemsForErrors(List.of(error));
return Optional.of(error);
}
public static class SysErrorMapper implements RowMapper<SysError> {
@Override
public SysError mapRow(ResultSet rs, int rowNum) throws SQLException {
SysError error = new SysError();
error.setId(rs.getInt("id"));
error.setUserId(rs.getObject("user_id", Integer.class));
error.setTitle(rs.getString("title"));
error.setCode(rs.getString("code"));
error.setMessage(rs.getString("message"));
error.setPinia(rs.getString("pinia"));
error.setCalculationJobId(rs.getObject("calculation_job_id", Integer.class));
error.setBulkOperationId(rs.getObject("bulk_operation_id", Integer.class));
error.setType(SysErrorType.valueOf(rs.getString("type")));
error.setCreatedAt(rs.getTimestamp("created_at").toLocalDateTime());
return error;
}
}
public static class SysErrorTraceItemMapper implements RowMapper<SysErrorTraceItem> {
@Override
public SysErrorTraceItem mapRow(ResultSet rs, int rowNum) throws SQLException {
SysErrorTraceItem traceItem = new SysErrorTraceItem();
traceItem.setErrorId(rs.getInt("error_id"));
traceItem.setId(rs.getInt("id"));
traceItem.setLine(rs.getObject("line", Integer.class));
traceItem.setFile(rs.getString("file"));
traceItem.setMethod(rs.getString("method"));
traceItem.setFullPath(rs.getString("fullPath"));
return traceItem;
}
}
}

View file

@ -1,11 +1,7 @@
package de.avatic.lcc.service.bulk;
import de.avatic.lcc.excelmodel.ExcelNode;
import de.avatic.lcc.model.bulk.BulkFileTypes;
import de.avatic.lcc.model.bulk.BulkInstruction;
import de.avatic.lcc.model.bulk.BulkInstructionType;
import de.avatic.lcc.model.bulk.BulkOperation;
import de.avatic.lcc.model.nodes.Node;
import de.avatic.lcc.repositories.NodeRepository;
import de.avatic.lcc.service.bulk.bulkImport.NodeBulkImportService;
import de.avatic.lcc.service.bulk.bulkImport.PackagingBulkImportService;
@ -14,13 +10,13 @@ import de.avatic.lcc.service.transformer.generic.NodeTransformer;
import de.avatic.lcc.util.exception.internalerror.ExcelValidationError;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.util.RecordFormatException;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Service;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
@Service
public class BulkImportService {
@ -54,34 +50,36 @@ public class BulkImportService {
InputStream in = new ByteArrayInputStream(file);
Workbook workbook = new XSSFWorkbook(in);
Sheet sheet = workbook.getSheet(BulkFileTypes.valueOf(type.name()).getSheetName());
switch (type) {
case CONTAINER_RATE:
var containerRates = containerRateExcelMapper.extractSheet(sheet);
try (Workbook workbook = new XSSFWorkbook(in)) {
Sheet sheet = workbook.getSheet(BulkFileTypes.valueOf(type.name()).getSheetName());
break;
case COUNTRY_MATRIX:
var matrixRates = matrixRateExcelMapper.extractSheet(sheet);
break;
case MATERIAL:
var materials = materialExcelMapper.extractSheet(sheet);
break;
case PACKAGING:
var packaging = packagingExcelMapper.extractSheet(sheet);
packaging.forEach(packagingBulkImportService::processPackagingInstructions);
break;
case NODE:
var nodeInstructions = nodeExcelMapper.extractSheet(sheet);
nodeInstructions.forEach(nodeBulkImportService::processNodeInstructions);
break;
default:
switch (type) {
case CONTAINER_RATE:
var containerRates = containerRateExcelMapper.extractSheet(sheet);
break;
case COUNTRY_MATRIX:
var matrixRates = matrixRateExcelMapper.extractSheet(sheet);
break;
case MATERIAL:
var materials = materialExcelMapper.extractSheet(sheet);
// materials.forEach((material) -> materialBulkImportService);
break;
case PACKAGING:
var packaging = packagingExcelMapper.extractSheet(sheet);
packaging.forEach(packagingBulkImportService::processPackagingInstructions);
break;
case NODE:
var nodeInstructions = nodeExcelMapper.extractSheet(sheet);
nodeInstructions.forEach(nodeBulkImportService::processNodeInstructions);
break;
default:
}
} catch (RecordFormatException e) {
throw new ExcelValidationError("Unable to open excel file. File is too large.");
}
}

View file

@ -30,7 +30,6 @@ public class BulkOperationExecutionService {
this.sysErrorTransformer = sysErrorTransformer;
}
@Transactional
@Async("bulkProcessingExecutor")
public void launchExecution(Integer id) {
@ -54,12 +53,10 @@ public class BulkOperationExecutionService {
} catch (Exception e) {
op.setProcessState(BulkOperationState.EXCEPTION);
var error = new SysError();
error.setType(SysErrorType.BULK);
error.setCode(e.getClass().getSimpleName());
error.setTitle("Bulk Operation Execution " + op.getId() + " failed");
error.setTitle("Bulk operation execution " + op.getProcessingType() + " of " + op.getFileType() + " failed");
error.setMessage(e.getMessage());
error.setUserId(op.getUserId());
error.setBulkOperationId(op.getId());

View file

@ -122,7 +122,7 @@ public class ConstraintGenerator {
}
}
throw new ExcelValidationError("Unable to validate row " + (row.getRowNum() + 1) + " column " + toExcelLetter(columnIdx) + ": Expected numeric value within range: " + min + " - " + max);
throw new ExcelValidationError("Unable to validate row " + (row.getRowNum() + 1) + " column " + toExcelLetter(columnIdx) + ": Expected numeric value within range: [" + min + ", " + max+ "]");
}
@ -157,7 +157,7 @@ public class ConstraintGenerator {
}
}
throw new ExcelValidationError("Unable to validate row " + (row.getRowNum() + 1) + " column " + toExcelLetter(columnIdx) + ": Expected string with length within range: " + min + " - " + max);
throw new ExcelValidationError("Unable to validate row " + (row.getRowNum() + 1) + " column " + toExcelLetter(columnIdx) + ": Expected string with length within range: [" + min + ", " + max+ "]");
}
@ -233,7 +233,7 @@ public class ConstraintGenerator {
}
}
throw new ExcelValidationError("Unable to validate row " + (row.getRowNum() + 1) + " column " + toExcelLetter(columnIdx) + ": Expected numeric integer value within range: " + min + " - " + max);
throw new ExcelValidationError("Unable to validate row " + (row.getRowNum() + 1) + " column " + toExcelLetter(columnIdx) + ": Expected numeric integer value within range: [" + min + " , " + max + "]");
}
}

View file

@ -20,7 +20,7 @@ public class HeaderGenerator {
for(H header : EnumSet.allOf(headers)){
Cell cell = row.getCell(header.ordinal());
if(cell == null || !cell.getStringCellValue().equals(header.getHeader())){
throw new ExcelValidationError("Unable to validate header " + header.getHeader() + ": Header of column " + toExcelLetter(header.ordinal()) + " has to be " + header.getHeader());
throw new ExcelValidationError("Unable to validate header \"" + header.getHeader() + "\": Header of column " + toExcelLetter(header.ordinal()) + " has to be \"" + header.getHeader() + "\"");
}
}
}
@ -31,7 +31,7 @@ public class HeaderGenerator {
for(String header : headers){
Cell cell = row.getCell(idx++);
if(cell == null || !cell.getStringCellValue().equals(header)){
throw new ExcelValidationError("Unable to validate header " + header + ": Header of column " + toExcelLetter(idx) + " has to be " + header);
throw new ExcelValidationError("Unable to validate header \"" + header + "\": Header of column " + toExcelLetter(idx) + " has to be \"" + header + "\"");
}
}

View file

@ -42,14 +42,6 @@ public class HandlingCostCalculationService {
var destinationDisposal = destination.getDisposalCost();
var destinationRepacking = destination.getRepackingCost();
// BigDecimal huAnnualAmount = BigDecimal.valueOf(destination.getAnnualAmount()).divide(BigDecimal.valueOf(hu.getContentUnitCount()),4,RoundingMode.UP);
// double handling = Double.parseDouble(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.KLT_HANDLING).orElseThrow().getCurrentValue());
// double release = Double.parseDouble(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.KLT_RELEASE).orElseThrow().getCurrentValue());
// double dispatch = Double.parseDouble(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.KLT_DISPATCH).orElseThrow().getCurrentValue());
// double wageFactor = Double.parseDouble(countryPropertyRepository.getByMappingIdAndCountryId(CountryPropertyMappingId.WAGE, destination.getCountryId()).orElseThrow().getCurrentValue());
// double booking = Double.parseDouble(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.BOOKING).orElseThrow().getCurrentValue());
BigDecimal huAnnualAmount = BigDecimal.valueOf(destination.getAnnualAmount()).divide(BigDecimal.valueOf(hu.getContentUnitCount()),4, RoundingMode.UP );
BigDecimal handling = destinationHandling != null ? destinationHandling : BigDecimal.valueOf(Double.parseDouble(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.GLT_HANDLING).orElseThrow().getCurrentValue()));
BigDecimal release = BigDecimal.valueOf(Double.parseDouble(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.GLT_RELEASE).orElseThrow().getCurrentValue()));

View file

@ -1,13 +1,24 @@
package de.avatic.lcc.service.transformer.bulk;
import de.avatic.lcc.dto.bulk.BulkOperationDTO;
import de.avatic.lcc.dto.bulk.BulkOperationState;
import de.avatic.lcc.model.bulk.BulkOperation;
import de.avatic.lcc.repositories.error.SysErrorRepository;
import de.avatic.lcc.service.transformer.error.SysErrorTransformer;
import org.springframework.stereotype.Service;
@Service
public class BulkOperationTransformer {
private final SysErrorRepository sysErrorRepository;
private final SysErrorTransformer sysErrorTransformer;
public BulkOperationTransformer(SysErrorRepository sysErrorRepository, SysErrorTransformer sysErrorTransformer) {
this.sysErrorRepository = sysErrorRepository;
this.sysErrorTransformer = sysErrorTransformer;
}
public BulkOperationDTO toBulkOperationDTO(BulkOperation entity) {
BulkOperationDTO dto = new BulkOperationDTO();
@ -18,6 +29,13 @@ public class BulkOperationTransformer {
dto.setState(entity.getProcessState());
dto.setCreatedAt(entity.getCreatedAt());
if (entity.getProcessState() == BulkOperationState.EXCEPTION) {
var error = sysErrorRepository.getByBulkOperationId(entity.getId());
error.ifPresent(sysError -> dto.setError(sysErrorTransformer.toSysErrorDto(sysError)));
}
return dto;
}

View file

@ -410,113 +410,132 @@ SET @packaging_28523500576 = LAST_INSERT_ID();
-- Stackable und Rust Prevention Property Type IDs ermitteln
SET @stackable_type_id = (SELECT id FROM packaging_property_type WHERE external_mapping_id = 'STACKABLE' LIMIT 1);
SET @rust_prevention_type_id = (SELECT id FROM packaging_property_type WHERE external_mapping_id = 'RUST_PREVENTION' LIMIT 1);
SET @mixable_type_id = (SELECT id FROM packaging_property_type WHERE external_mapping_id = 'MIXABLE' LIMIT 1);
-- Part Number: 28152640129 - Stackable: Yes, Rust Prevention: No
INSERT INTO packaging_property (packaging_property_type_id, packaging_id, property_value)
VALUES
(@stackable_type_id, @packaging_28152640129, 'true'),
(@mixable_type_id, @packaging_28152640129, 'true'),
(@rust_prevention_type_id, @packaging_28152640129, 'false');
-- Part Number: 8222640822 - Stackable: No, Rust Prevention: No
INSERT INTO packaging_property (packaging_property_type_id, packaging_id, property_value)
VALUES
(@stackable_type_id, @packaging_8222640822, 'false'),
(@mixable_type_id, @packaging_8222640822, 'true'),
(@rust_prevention_type_id, @packaging_8222640822, 'false');
-- Part Number: 3064540201 - Stackable: Yes, Rust Prevention: No
INSERT INTO packaging_property (packaging_property_type_id, packaging_id, property_value)
VALUES
(@stackable_type_id, @packaging_3064540201, 'true'),
(@mixable_type_id, @packaging_3064540201, 'true'),
(@rust_prevention_type_id, @packaging_3064540201, 'false');
-- Part Number: 8212640113 - Stackable: Yes, Rust Prevention: No
INSERT INTO packaging_property (packaging_property_type_id, packaging_id, property_value)
VALUES
(@stackable_type_id, @packaging_8212640113, 'true'),
(@mixable_type_id, @packaging_8212640113, 'true'),
(@rust_prevention_type_id, @packaging_8212640113, 'false');
-- Part Number: 28152643516 - Stackable: Yes, Rust Prevention: No
INSERT INTO packaging_property (packaging_property_type_id, packaging_id, property_value)
VALUES
(@stackable_type_id, @packaging_28152643516, 'true'),
(@mixable_type_id, @packaging_28152643516, 'true'),
(@rust_prevention_type_id, @packaging_28152643516, 'false');
-- Part Number: 4222640104 - Stackable: Yes, Rust Prevention: No
INSERT INTO packaging_property (packaging_property_type_id, packaging_id, property_value)
VALUES
(@stackable_type_id, @packaging_4222640104, 'true'),
(@mixable_type_id, @packaging_4222640104, 'true'),
(@rust_prevention_type_id, @packaging_4222640104, 'false');
-- Part Number: 28152643502 - Stackable: Yes, Rust Prevention: No
INSERT INTO packaging_property (packaging_property_type_id, packaging_id, property_value)
VALUES
(@stackable_type_id, @packaging_28152643502, 'true'),
(@mixable_type_id, @packaging_28152643502, 'true'),
(@rust_prevention_type_id, @packaging_28152643502, 'false');
-- Part Number: 28152640804 - Stackable: Yes, Rust Prevention: No
INSERT INTO packaging_property (packaging_property_type_id, packaging_id, property_value)
VALUES
(@stackable_type_id, @packaging_28152640804, 'true'),
(@mixable_type_id, @packaging_28152640804, 'true'),
(@rust_prevention_type_id, @packaging_28152640804, 'false');
-- Part Number: 4222640805 - Stackable: Yes, Rust Prevention: No
INSERT INTO packaging_property (packaging_property_type_id, packaging_id, property_value)
VALUES
(@stackable_type_id, @packaging_4222640805, 'true'),
(@mixable_type_id, @packaging_4222640805, 'true'),
(@rust_prevention_type_id, @packaging_4222640805, 'false');
-- Part Number: 4222640803 - Stackable: Yes, Rust Prevention: No
INSERT INTO packaging_property (packaging_property_type_id, packaging_id, property_value)
VALUES
(@stackable_type_id, @packaging_4222640803, 'true'),
(@mixable_type_id, @packaging_4222640803, 'true'),
(@rust_prevention_type_id, @packaging_4222640803, 'false');
-- Part Number: 8212640811 - Stackable: Yes, Rust Prevention: No
INSERT INTO packaging_property (packaging_property_type_id, packaging_id, property_value)
VALUES
(@stackable_type_id, @packaging_8212640811, 'true'),
(@mixable_type_id, @packaging_8212640811, 'true'),
(@rust_prevention_type_id, @packaging_8212640811, 'false');
-- Part Number: 8212640827 - Stackable: Yes, Rust Prevention: No
INSERT INTO packaging_property (packaging_property_type_id, packaging_id, property_value)
VALUES
(@stackable_type_id, @packaging_8212640827, 'true'),
(@mixable_type_id, @packaging_8212640827, 'true'),
(@rust_prevention_type_id, @packaging_8212640827, 'false');
-- Part Number: 5512640104 - Stackable: Yes, Rust Prevention: No
INSERT INTO packaging_property (packaging_property_type_id, packaging_id, property_value)
VALUES
(@stackable_type_id, @packaging_5512640104, 'true'),
(@mixable_type_id, @packaging_5512640104, 'true'),
(@rust_prevention_type_id, @packaging_5512640104, 'false');
-- Part Number: 5512640106 - Stackable: Yes, Rust Prevention: No
INSERT INTO packaging_property (packaging_property_type_id, packaging_id, property_value)
VALUES
(@stackable_type_id, @packaging_5512640106, 'true'),
(@mixable_type_id, @packaging_5512640106, 'true'),
(@rust_prevention_type_id, @packaging_5512640106, 'false');
-- Part Number: 8263500575 - Stackable: Yes, Rust Prevention: No
INSERT INTO packaging_property (packaging_property_type_id, packaging_id, property_value)
VALUES
(@stackable_type_id, @packaging_8263500575, 'true'),
(@mixable_type_id, @packaging_8263500575, 'true'),
(@rust_prevention_type_id, @packaging_8263500575, 'false');
-- Part Number: 8263500576 - Stackable: Yes, Rust Prevention: No
INSERT INTO packaging_property (packaging_property_type_id, packaging_id, property_value)
VALUES
(@stackable_type_id, @packaging_8263500576, 'true'),
(@mixable_type_id, @packaging_8263500576, 'true'),
(@rust_prevention_type_id, @packaging_8263500576, 'false');
-- Part Number: 28523500575 - Stackable: Yes, Rust Prevention: No
INSERT INTO packaging_property (packaging_property_type_id, packaging_id, property_value)
VALUES
(@stackable_type_id, @packaging_28523500575, 'true'),
(@mixable_type_id, @packaging_28523500575, 'true'),
(@rust_prevention_type_id, @packaging_28523500575, 'false');
-- Part Number: 28523500576 - Stackable: Yes, Rust Prevention: No
INSERT INTO packaging_property (packaging_property_type_id, packaging_id, property_value)
VALUES
(@stackable_type_id, @packaging_28523500576, 'true'),
(@mixable_type_id, @packaging_28523500576, 'true'),
(@rust_prevention_type_id, @packaging_28523500576, 'false');
-- ============================================