mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-12-06 02:59:29 +00:00
Implement functionality to import schematic csv (or any other csv for that matter), with ability to map input columns to output columns with input validation and error handling
This commit is contained in:
parent
4277f42285
commit
d0f2422e0d
6 changed files with 1733 additions and 28 deletions
186
templates/projects/_bom_validation_results.html.twig
Normal file
186
templates/projects/_bom_validation_results.html.twig
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
{# BOM Validation Results Component #}
|
||||
{#
|
||||
Usage:
|
||||
{% include 'projects/_bom_validation_results.html.twig' with {
|
||||
validation_result: validation_result,
|
||||
show_summary: true,
|
||||
show_details: true
|
||||
} %}
|
||||
#}
|
||||
|
||||
{% if validation_result is defined and validation_result is not empty %}
|
||||
{% set stats = validation_result %}
|
||||
|
||||
{# Validation Summary #}
|
||||
{% if show_summary is defined and show_summary %}
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fa-solid fa-chart-bar fa-fw"></i>
|
||||
{% trans %}project.bom_import.validation.summary{% endtrans %}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="text-center">
|
||||
<div class="h3 text-primary">{{ stats.total_entries }}</div>
|
||||
<small class="text-muted">{% trans %}project.bom_import.validation.total_entries{% endtrans %}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="text-center">
|
||||
<div class="h3 text-success">{{ stats.valid_entries }}</div>
|
||||
<small class="text-muted">{% trans %}project.bom_import.validation.valid_entries{% endtrans %}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="text-center">
|
||||
<div class="h3 text-warning">{{ stats.invalid_entries }}</div>
|
||||
<small class="text-muted">{% trans %}project.bom_import.validation.invalid_entries{% endtrans %}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="text-center">
|
||||
<div class="h3 text-info">
|
||||
{% if stats.total_entries > 0 %}
|
||||
{{ ((stats.valid_entries / stats.total_entries) * 100) | round(1) }}%
|
||||
{% else %}
|
||||
0%
|
||||
{% endif %}
|
||||
</div>
|
||||
<small class="text-muted">{% trans %}project.bom_import.validation.success_rate{% endtrans %}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Validation Messages #}
|
||||
{% if validation_result.errors is defined and validation_result.errors is not empty %}
|
||||
<div class="alert alert-danger">
|
||||
<h4><i class="fa-solid fa-exclamation-triangle fa-fw"></i> {% trans %}project.bom_import.validation.errors.title{% endtrans %}</h4>
|
||||
<p class="mb-2">{% trans %}project.bom_import.validation.errors.description{% endtrans %}</p>
|
||||
<ul class="mb-0">
|
||||
{% for error in validation_result.errors %}
|
||||
<li>{{ error|raw }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if validation_result.warnings is defined and validation_result.warnings is not empty %}
|
||||
<div class="alert alert-warning">
|
||||
<h4><i class="fa-solid fa-exclamation-circle fa-fw"></i> {% trans %}project.bom_import.validation.warnings.title{% endtrans %}</h4>
|
||||
<p class="mb-2">{% trans %}project.bom_import.validation.warnings.description{% endtrans %}</p>
|
||||
<ul class="mb-0">
|
||||
{% for warning in validation_result.warnings %}
|
||||
<li>{{ warning|raw }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if validation_result.info is defined and validation_result.info is not empty %}
|
||||
<div class="alert alert-info">
|
||||
<h4><i class="fa-solid fa-info-circle fa-fw"></i> {% trans %}project.bom_import.validation.info.title{% endtrans %}</h4>
|
||||
<ul class="mb-0">
|
||||
{% for info in validation_result.info %}
|
||||
<li>{{ info|raw }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Detailed Line-by-Line Results #}
|
||||
{% if show_details is defined and show_details and validation_result.line_results is defined %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fa-solid fa-list fa-fw"></i>
|
||||
{% trans %}project.bom_import.validation.details.title{% endtrans %}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans %}project.bom_import.validation.details.line{% endtrans %}</th>
|
||||
<th>{% trans %}project.bom_import.validation.details.status{% endtrans %}</th>
|
||||
<th>{% trans %}project.bom_import.validation.details.messages{% endtrans %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for line_result in validation_result.line_results %}
|
||||
<tr class="{% if line_result.is_valid %}table-success{% else %}table-danger{% endif %}">
|
||||
<td>
|
||||
<strong>{{ line_result.line_number }}</strong>
|
||||
</td>
|
||||
<td>
|
||||
{% if line_result.is_valid %}
|
||||
<span class="badge bg-success">
|
||||
<i class="fa-solid fa-check fa-fw"></i>
|
||||
{% trans %}project.bom_import.validation.details.valid{% endtrans %}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">
|
||||
<i class="fa-solid fa-times fa-fw"></i>
|
||||
{% trans %}project.bom_import.validation.details.invalid{% endtrans %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if line_result.errors is not empty %}
|
||||
<div class="text-danger">
|
||||
{% for error in line_result.errors %}
|
||||
<div><i class="fa-solid fa-exclamation-triangle fa-fw"></i> {{ error|raw }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if line_result.warnings is not empty %}
|
||||
<div class="text-warning">
|
||||
{% for warning in line_result.warnings %}
|
||||
<div><i class="fa-solid fa-exclamation-circle fa-fw"></i> {{ warning|raw }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if line_result.info is not empty %}
|
||||
<div class="text-info">
|
||||
{% for info in line_result.info %}
|
||||
<div><i class="fa-solid fa-info-circle fa-fw"></i> {{ info|raw }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Action Buttons #}
|
||||
{% if validation_result.is_valid is defined %}
|
||||
<div class="mt-3">
|
||||
{% if validation_result.is_valid %}
|
||||
<div class="alert alert-success">
|
||||
<i class="fa-solid fa-check-circle fa-fw"></i>
|
||||
{% trans %}project.bom_import.validation.all_valid{% endtrans %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-danger">
|
||||
<i class="fa-solid fa-exclamation-triangle fa-fw"></i>
|
||||
{% trans %}project.bom_import.validation.fix_errors{% endtrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
204
templates/projects/import_bom_map_fields.html.twig
Normal file
204
templates/projects/import_bom_map_fields.html.twig
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
{% extends "main_card.html.twig" %}
|
||||
|
||||
{% block title %}{% trans %}project.bom_import.map_fields{% endtrans %}{% endblock %}
|
||||
|
||||
{% block card_title %}
|
||||
<i class="fa-solid fa-arrows-left-right fa-fw"></i>
|
||||
{% trans %}project.bom_import.map_fields{% endtrans %}{% if project %}: <i>{{ project.name }}</i>{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block card_content %}
|
||||
{% if validation_result is defined %}
|
||||
{% include 'projects/_bom_validation_results.html.twig' with {
|
||||
validation_result: validation_result,
|
||||
show_summary: true,
|
||||
show_details: false
|
||||
} %}
|
||||
{% endif %}
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info">
|
||||
<i class="fa-solid fa-info-circle fa-fw"></i>
|
||||
{% trans %}project.bom_import.map_fields.help{% endtrans %}
|
||||
</div>
|
||||
<div class="alert alert-warning">
|
||||
<i class="fa-solid fa-lightbulb fa-fw"></i>
|
||||
{% trans %}project.bom_import.field_mapping.priority_note{% endtrans %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ form_start(form) }}
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
{{ form_row(form.delimiter) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fa-solid fa-table-columns fa-fw"></i>
|
||||
{% trans %}project.bom_import.field_mapping.title{% endtrans %}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans %}project.bom_import.field_mapping.csv_field{% endtrans %}</th>
|
||||
<th>{% trans %}project.bom_import.field_mapping.maps_to{% endtrans %}</th>
|
||||
<th>{% trans %}project.bom_import.field_mapping.suggestion{% endtrans %}</th>
|
||||
<th>{% trans %}project.bom_import.field_mapping.priority{% endtrans %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for field in detected_fields %}
|
||||
<tr>
|
||||
<td>
|
||||
<code>{{ field }}</code>
|
||||
</td>
|
||||
<td>
|
||||
{{ form_widget(form['mapping_' ~ field_name_mapping[field]], {
|
||||
'attr': {
|
||||
'class': 'form-select field-mapping-select',
|
||||
'data-field': field
|
||||
}
|
||||
}) }}
|
||||
</td>
|
||||
<td>
|
||||
{% if suggested_mapping[field] is defined %}
|
||||
<span class="badge bg-success">
|
||||
<i class="fa-solid fa-magic fa-fw"></i>
|
||||
{{ suggested_mapping[field] }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="text-muted">
|
||||
<i class="fa-solid fa-question fa-fw"></i>
|
||||
{% trans %}project.bom_import.field_mapping.no_suggestion{% endtrans %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<input type="number"
|
||||
class="form-control form-control-sm priority-input"
|
||||
min="1"
|
||||
value="10"
|
||||
style="width: 80px;"
|
||||
data-field="{{ field }}"
|
||||
title="{% trans %}project.bom_import.field_mapping.priority_help{% endtrans %}">
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<h6>{% trans %}project.bom_import.field_mapping.summary{% endtrans %}:</h6>
|
||||
<div id="mapping-summary" class="alert alert-info">
|
||||
<i class="fa-solid fa-info-circle fa-fw"></i>
|
||||
{% trans %}project.bom_import.field_mapping.select_to_see_summary{% endtrans %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
{{ form_widget(form.submit, {
|
||||
'attr': {
|
||||
'class': 'btn btn-primary'
|
||||
}
|
||||
}) }}
|
||||
<a href="{{ path('project_import_bom', {'id': project.id}) }}" class="btn btn-secondary">
|
||||
<i class="fa-solid fa-arrow-left fa-fw"></i>
|
||||
{% trans %}common.back{% endtrans %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{{ form_end(form) }}
|
||||
|
||||
<script>
|
||||
// Function to initialize the field mapping page
|
||||
function initializeFieldMapping() {
|
||||
const suggestions = {{ suggested_mapping|json_encode|raw }};
|
||||
const fieldNameMapping = {{ field_name_mapping|json_encode|raw }};
|
||||
|
||||
Object.keys(suggestions).forEach(function(field) {
|
||||
// Use the sanitized field name from the server-side mapping
|
||||
const sanitizedField = fieldNameMapping[field];
|
||||
const select = document.querySelector('[name="form[mapping_' + sanitizedField + ']"]');
|
||||
if (select && !select.value) {
|
||||
select.value = suggestions[field];
|
||||
}
|
||||
});
|
||||
|
||||
// Update mapping summary
|
||||
updateMappingSummary();
|
||||
|
||||
// Add event listeners for dynamic updates
|
||||
document.querySelectorAll('.field-mapping-select').forEach(function(select) {
|
||||
select.addEventListener('change', updateMappingSummary);
|
||||
});
|
||||
|
||||
document.querySelectorAll('.priority-input').forEach(function(input) {
|
||||
input.addEventListener('change', updateMappingSummary);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize on both DOMContentLoaded and Turbo events
|
||||
document.addEventListener('DOMContentLoaded', initializeFieldMapping);
|
||||
document.addEventListener('turbo:load', initializeFieldMapping);
|
||||
document.addEventListener('turbo:frame-load', function(event) {
|
||||
// Only initialize if this frame contains our field mapping content
|
||||
if (event.target.id === 'content' || event.target.closest('#content')) {
|
||||
initializeFieldMapping();
|
||||
}
|
||||
});
|
||||
|
||||
function updateMappingSummary() {
|
||||
const summary = document.getElementById('mapping-summary');
|
||||
const mappings = {};
|
||||
const priorities = {};
|
||||
|
||||
// Collect all mappings and priorities
|
||||
document.querySelectorAll('.field-mapping-select').forEach(function(select) {
|
||||
const field = select.getAttribute('data-field');
|
||||
const target = select.value;
|
||||
const priorityInput = document.querySelector('.priority-input[data-field="' + field + '"]');
|
||||
const priority = priorityInput ? parseInt(priorityInput.value) || 10 : 10;
|
||||
|
||||
if (target && target !== '') {
|
||||
if (!mappings[target]) {
|
||||
mappings[target] = [];
|
||||
}
|
||||
mappings[target].push({
|
||||
field: field,
|
||||
priority: priority
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Sort by priority and build summary
|
||||
let summaryHtml = '<div class="row">';
|
||||
Object.keys(mappings).forEach(function(target) {
|
||||
const fieldMappings = mappings[target].sort((a, b) => a.priority - b.priority);
|
||||
const fieldList = fieldMappings.map(m => m.field + ' (' + '{{ "project.bom_import.field_mapping.priority_short"|trans }}' + m.priority + ')').join(', ');
|
||||
|
||||
summaryHtml += '<div class="col-md-6 mb-2">';
|
||||
summaryHtml += '<strong>' + target + ':</strong> ' + fieldList;
|
||||
summaryHtml += '</div>';
|
||||
});
|
||||
summaryHtml += '</div>';
|
||||
|
||||
if (Object.keys(mappings).length === 0) {
|
||||
summary.innerHTML = '<i class="fa-solid fa-info-circle fa-fw"></i> {{ "project.bom_import.field_mapping.select_to_see_summary"|trans }}';
|
||||
} else {
|
||||
summary.innerHTML = summaryHtml;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Loading…
Add table
Add a link
Reference in a new issue