mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-12-06 02:59:29 +00:00
Refactor bulk import functionality to make controller smaller (use services) add DTOs and use stimulus controllers on frontend
This commit is contained in:
parent
65d840c444
commit
d6ac16ede0
14 changed files with 1382 additions and 716 deletions
|
|
@ -10,6 +10,12 @@
|
|||
|
||||
{% block card_content %}
|
||||
|
||||
<div data-controller="bulk-job-manage"
|
||||
data-bulk-job-manage-delete-url-value="{{ path('bulk_info_provider_delete', {'jobId': '__JOB_ID__'}) }}"
|
||||
data-bulk-job-manage-stop-url-value="{{ path('bulk_info_provider_stop', {'jobId': '__JOB_ID__'}) }}"
|
||||
data-bulk-job-manage-delete-confirm-message-value="{% trans %}info_providers.bulk_import.confirm_delete_job{% endtrans %}"
|
||||
data-bulk-job-manage-stop-confirm-message-value="{% trans %}info_providers.bulk_import.confirm_stop_job{% endtrans %}">
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<p class="text-muted mb-0">
|
||||
{% trans %}info_providers.bulk_import.manage_jobs_description{% endtrans %}
|
||||
|
|
@ -89,12 +95,12 @@
|
|||
</a>
|
||||
{% endif %}
|
||||
{% if job.canBeStopped %}
|
||||
<button type="button" class="btn btn-warning" onclick="stopJob({{ job.id }})">
|
||||
<button type="button" class="btn btn-warning" data-action="click->bulk-job-manage#stopJob" data-job-id="{{ job.id }}">
|
||||
<i class="fas fa-stop"></i> {% trans %}info_providers.bulk_import.action.stop{% endtrans %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if job.isCompleted or job.isFailed or job.isStopped %}
|
||||
<button type="button" class="btn btn-danger" onclick="deleteJob({{ job.id }})">
|
||||
<button type="button" class="btn btn-danger" data-action="click->bulk-job-manage#deleteJob" data-job-id="{{ job.id }}">
|
||||
<i class="fas fa-trash"></i> {% trans %}info_providers.bulk_import.action.delete{% endtrans %}
|
||||
</button>
|
||||
{% endif %}
|
||||
|
|
@ -115,54 +121,6 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function deleteJob(jobId) {
|
||||
if (confirm('{% trans %}info_providers.bulk_import.confirm_delete_job{% endtrans %}')) {
|
||||
fetch(`{{ path('bulk_info_provider_delete', {'jobId': '__JOB_ID__'}) }}`.replace('__JOB_ID__', jobId), {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error deleting job: ' + (data.error || 'Unknown error'));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Error deleting job');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function stopJob(jobId) {
|
||||
if (confirm('{% trans %}info_providers.bulk_import.confirm_stop_job{% endtrans %}')) {
|
||||
fetch(`{{ path('bulk_info_provider_stop', {'jobId': '__JOB_ID__'}) }}`.replace('__JOB_ID__', jobId), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error stopping job: ' + (data.error || 'Unknown error'));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Error stopping job');
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
@ -14,6 +14,8 @@
|
|||
|
||||
{% block card_content %}
|
||||
|
||||
<div>
|
||||
|
||||
<!-- Show existing jobs -->
|
||||
{% if existing_jobs is not empty %}
|
||||
<div class="card mb-3">
|
||||
|
|
@ -134,7 +136,12 @@
|
|||
|
||||
{{ form_start(form) }}
|
||||
|
||||
<div class="card">
|
||||
<div class="card"
|
||||
data-controller="field-mapping"
|
||||
data-field-mapping-mapping-index-value="{{ form.field_mappings|length }}"
|
||||
data-field-mapping-max-mappings-value="{{ fieldChoices|length }}"
|
||||
data-field-mapping-prototype-value="{{ form_widget(form.field_mappings.vars.prototype)|e('js') }}"
|
||||
data-field-mapping-max-mappings-reached-message-value="{{ 'info_providers.bulk_import.max_mappings_reached'|trans|e('js') }}">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">{% trans %}info_providers.bulk_import.field_mappings{% endtrans %}</h5>
|
||||
<small class="text-muted">{% trans %}info_providers.bulk_import.field_mappings_help{% endtrans %}</small>
|
||||
|
|
@ -150,14 +157,14 @@
|
|||
<th width="100">{% trans %}info_providers.bulk_import.actions.label{% endtrans %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="field-mappings-tbody" data-prototype="{{ form_widget(form.field_mappings.vars.prototype)|e('html_attr') }}">
|
||||
<tbody id="field-mappings-tbody" data-field-mapping-target="tbody">
|
||||
{% for mapping in form.field_mappings %}
|
||||
<tr class="mapping-row">
|
||||
<td>{{ form_widget(mapping.field) }}{{ form_errors(mapping.field) }}</td>
|
||||
<td>{{ form_widget(mapping.providers) }}{{ form_errors(mapping.providers) }}</td>
|
||||
<td>{{ form_widget(mapping.priority) }}{{ form_errors(mapping.priority) }}</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-danger btn-sm" onclick="removeMapping(this)">
|
||||
<button type="button" class="btn btn-danger btn-sm" data-action="click->field-mapping#removeMapping">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
|
|
@ -165,7 +172,7 @@
|
|||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<button type="button" class="btn btn-success btn-sm" onclick="addMapping()">
|
||||
<button type="button" class="btn btn-success btn-sm" id="addMappingBtn" data-field-mapping-target="addButton">
|
||||
<i class="fas fa-plus"></i> {% trans %}info_providers.bulk_import.add_mapping{% endtrans %}
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -185,7 +192,7 @@
|
|||
{{ form_help(form.prefetch_details) }}
|
||||
</div>
|
||||
|
||||
{{ form_widget(form.submit) }}
|
||||
{{ form_widget(form.submit, {'attr': {'class': 'btn btn-primary', 'data-field-mapping-target': 'submitButton'}}) }}
|
||||
</div>
|
||||
|
||||
{{ form_end(form) }}
|
||||
|
|
@ -291,140 +298,7 @@
|
|||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
let mappingIndex = {{ form.field_mappings|length }};
|
||||
const maxMappings = {{ fieldChoices|length }};
|
||||
|
||||
function addMapping() {
|
||||
const tbody = document.getElementById('field-mappings-tbody');
|
||||
const currentMappings = tbody.querySelectorAll('.mapping-row').length;
|
||||
|
||||
// Check if we've reached the maximum number of mappings
|
||||
if (currentMappings >= maxMappings) {
|
||||
alert('{% trans %}info_providers.bulk_import.max_mappings_reached{% endtrans %}');
|
||||
return;
|
||||
}
|
||||
|
||||
const prototype = tbody.dataset.prototype;
|
||||
|
||||
// Replace __name__ placeholder with current index
|
||||
const newRowHtml = prototype.replace(/__name__/g, mappingIndex);
|
||||
|
||||
// Create temporary div to parse the prototype HTML
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = newRowHtml;
|
||||
|
||||
// Extract field, provider, and priority widgets
|
||||
const fieldWidget = tempDiv.querySelector('select[name*="[field]"]') || tempDiv.children[0];
|
||||
const providerWidget = tempDiv.querySelector('select[name*="[providers]"]') || tempDiv.children[1];
|
||||
const priorityWidget = tempDiv.querySelector('input[name*="[priority]"]') || tempDiv.children[2];
|
||||
|
||||
// Create new row
|
||||
const newRow = document.createElement('tr');
|
||||
newRow.className = 'mapping-row';
|
||||
newRow.innerHTML = `
|
||||
<td>${fieldWidget ? fieldWidget.outerHTML : ''}</td>
|
||||
<td>${providerWidget ? providerWidget.outerHTML : ''}</td>
|
||||
<td>${priorityWidget ? priorityWidget.outerHTML : ''}</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-danger btn-sm" onclick="removeMapping(this)">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
|
||||
tbody.appendChild(newRow);
|
||||
mappingIndex++;
|
||||
|
||||
// Add change listener to the new field select and clear its selection
|
||||
const newFieldSelect = newRow.querySelector('select[name*="[field]"]');
|
||||
if (newFieldSelect) {
|
||||
// Clear the selection for new rows - select the placeholder option
|
||||
newFieldSelect.value = '';
|
||||
addFieldChangeListener(newFieldSelect);
|
||||
}
|
||||
|
||||
// Update field options to hide already selected fields
|
||||
updateFieldOptions();
|
||||
|
||||
// Update add button state
|
||||
updateAddButtonState();
|
||||
}
|
||||
|
||||
function removeMapping(button) {
|
||||
const row = button.closest('tr');
|
||||
row.remove();
|
||||
updateFieldOptions();
|
||||
updateAddButtonState();
|
||||
}
|
||||
|
||||
|
||||
|
||||
function updateFieldOptions() {
|
||||
const tbody = document.getElementById('field-mappings-tbody');
|
||||
const fieldSelects = tbody.querySelectorAll('select[name*="[field]"]');
|
||||
|
||||
// Get all currently selected field values (excluding empty values)
|
||||
const selectedFields = Array.from(fieldSelects)
|
||||
.map(select => select.value)
|
||||
.filter(value => value && value !== '');
|
||||
|
||||
console.log('Selected fields:', selectedFields);
|
||||
|
||||
// Update each field select to disable already selected options
|
||||
fieldSelects.forEach(select => {
|
||||
Array.from(select.options).forEach(option => {
|
||||
// Don't disable if this is the current select's value or if option is empty
|
||||
const isCurrentValue = option.value === select.value;
|
||||
const isEmptyOption = !option.value || option.value === '';
|
||||
const isAlreadySelected = selectedFields.includes(option.value);
|
||||
|
||||
if (!isEmptyOption && isAlreadySelected && !isCurrentValue) {
|
||||
option.disabled = true;
|
||||
option.style.display = 'none';
|
||||
} else {
|
||||
option.disabled = false;
|
||||
option.style.display = '';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function updateAddButtonState() {
|
||||
const tbody = document.getElementById('field-mappings-tbody');
|
||||
const addButton = document.querySelector('button[onclick="addMapping()"]');
|
||||
const currentMappings = tbody.querySelectorAll('.mapping-row').length;
|
||||
|
||||
if (addButton) {
|
||||
if (currentMappings >= maxMappings) {
|
||||
addButton.disabled = true;
|
||||
addButton.title = '{% trans %}info_providers.bulk_import.max_mappings_reached{% endtrans %}';
|
||||
} else {
|
||||
addButton.disabled = false;
|
||||
addButton.title = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add event listener for field changes
|
||||
function addFieldChangeListener(select) {
|
||||
select.addEventListener('change', function() {
|
||||
updateFieldOptions();
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize add button state on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
updateAddButtonState();
|
||||
updateFieldOptions();
|
||||
|
||||
// Add change listeners to existing field selects
|
||||
const fieldSelects = document.querySelectorAll('select[name*="[field]"]');
|
||||
fieldSelects.forEach(addFieldChangeListener);
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,14 @@
|
|||
|
||||
{% block card_content %}
|
||||
|
||||
<div {{ stimulus_controller('bulk-import', {
|
||||
'jobId': job.id,
|
||||
'researchUrl': path('bulk_info_provider_research_part', {'jobId': job.id, 'partId': '__PART_ID__'}),
|
||||
'researchAllUrl': path('bulk_info_provider_research_all', {'jobId': job.id}),
|
||||
'markCompletedUrl': path('bulk_info_provider_mark_completed', {'jobId': job.id, 'partId': '__PART_ID__'}),
|
||||
'markSkippedUrl': path('bulk_info_provider_mark_skipped', {'jobId': job.id, 'partId': '__PART_ID__'}),
|
||||
'markPendingUrl': path('bulk_info_provider_mark_pending', {'jobId': job.id, 'partId': '__PART_ID__'})
|
||||
}) }}>
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<h5 class="mb-1">{{ job.displayNameKey|trans(job.displayNameParams) }} - {{ job.formattedTimestamp }}</h5>
|
||||
|
|
@ -41,10 +49,10 @@
|
|||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h6 class="mb-0">Progress</h6>
|
||||
<span id="progress-text">{{ job.completedPartsCount }} / {{ job.partCount }} completed</span>
|
||||
<span data-bulk-import-target="progressText">{{ job.completedPartsCount }} / {{ job.partCount }} completed</span>
|
||||
</div>
|
||||
<div class="progress" style="height: 8px;">
|
||||
<div id="progress-bar" class="progress-bar" role="progressbar"
|
||||
<div data-bulk-import-target="progressBar" class="progress-bar" role="progressbar"
|
||||
style="width: {{ job.progressPercentage }}%"
|
||||
aria-valuenow="{{ job.progressPercentage }}" aria-valuemin="0" aria-valuemax="100">
|
||||
</div>
|
||||
|
|
@ -72,12 +80,32 @@
|
|||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Research Controls -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6 class="mb-1">{% trans %}info_providers.bulk_import.research.title{% endtrans %}</h6>
|
||||
<small class="text-muted">{% trans %}info_providers.bulk_import.research.description{% endtrans %}</small>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm me-2"
|
||||
data-action="click->bulk-import#researchAllParts"
|
||||
id="research-all-btn">
|
||||
<span class="spinner-border spinner-border-sm me-1" style="display: none;" id="research-all-spinner"></span>
|
||||
<i class="fas fa-search"></i> {% trans %}info_providers.bulk_import.research.all_pending{% endtrans %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% for part_result in search_results %}
|
||||
{% set part = part_result.part %}
|
||||
{% set isCompleted = job.isPartCompleted(part.id) %}
|
||||
{% set isSkipped = job.isPartSkipped(part.id) %}
|
||||
<div class="card mb-3 {% if isCompleted %}border-success{% elseif isSkipped %}border-warning{% endif %}"
|
||||
id="part-card-{{ part.id }}"
|
||||
data-part-id="{{ part.id }}"
|
||||
{% if isCompleted %}style="background-color: rgba(25, 135, 84, 0.1);"{% endif %}>
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
|
|
@ -101,19 +129,26 @@
|
|||
</h5>
|
||||
</div>
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-outline-info btn-sm"
|
||||
data-action="click->bulk-import#researchPart"
|
||||
data-part-id="{{ part.id }}"
|
||||
title="{% trans %}info_providers.bulk_import.research.part_tooltip{% endtrans %}">
|
||||
<span class="spinner-border spinner-border-sm me-1" style="display: none;" data-research-spinner="{{ part.id }}"></span>
|
||||
<i class="fas fa-search"></i> {% trans %}info_providers.bulk_import.research.part{% endtrans %}
|
||||
</button>
|
||||
{% if not isCompleted and not isSkipped %}
|
||||
<button type="button" class="btn btn-success btn-sm" onclick="markPartCompleted({{ job.id }}, {{ part.id }})">
|
||||
<button type="button" class="btn btn-success btn-sm" data-action="click->bulk-import#markCompleted" data-part-id="{{ part.id }}">
|
||||
<i class="fas fa-check"></i> {% trans %}info_providers.bulk_import.mark_completed{% endtrans %}
|
||||
</button>
|
||||
<button type="button" class="btn btn-warning btn-sm" onclick="markPartSkipped({{ job.id }}, {{ part.id }})">
|
||||
<button type="button" class="btn btn-warning btn-sm" data-action="click->bulk-import#markSkipped" data-part-id="{{ part.id }}">
|
||||
<i class="fas fa-forward"></i> {% trans %}info_providers.bulk_import.mark_skipped{% endtrans %}
|
||||
</button>
|
||||
{% elseif isCompleted %}
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="markPartPending({{ job.id }}, {{ part.id }})">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" data-action="click->bulk-import#markPending" data-part-id="{{ part.id }}">
|
||||
<i class="fas fa-undo"></i> {% trans %}info_providers.bulk_import.mark_pending{% endtrans %}
|
||||
</button>
|
||||
{% elseif isSkipped %}
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="markPartPending({{ job.id }}, {{ part.id }})">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" data-action="click->bulk-import#markPending" data-part-id="{{ part.id }}">
|
||||
<i class="fas fa-undo"></i> {% trans %}info_providers.bulk_import.mark_pending{% endtrans %}
|
||||
</button>
|
||||
{% endif %}
|
||||
|
|
@ -172,7 +207,7 @@
|
|||
<span class="badge bg-info">{{ result.source_field ?? 'unknown' }}</span>
|
||||
{% if result.source_keyword %}
|
||||
<br><small class="text-muted">{{ result.source_keyword }}</small>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group-vertical btn-group-sm" role="group">
|
||||
|
|
@ -197,152 +232,6 @@
|
|||
</div>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function markPartCompleted(jobId, partId) {
|
||||
fetch(`{{ path('bulk_info_provider_mark_completed', {'jobId': '__JOB_ID__', 'partId': '__PART_ID__'}) }}`.replace('__JOB_ID__', jobId).replace('__PART_ID__', partId), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
updatePartStatus(partId, 'completed');
|
||||
updateProgress(data);
|
||||
if (data.job_completed) {
|
||||
location.reload(); // Refresh to show completed status
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => console.error('Error:', error));
|
||||
}
|
||||
|
||||
function markPartSkipped(jobId, partId) {
|
||||
const reason = prompt('{% trans %}info_providers.bulk_import.skip_reason{% endtrans %}:', '');
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('reason', reason || '');
|
||||
|
||||
fetch(`{{ path('bulk_info_provider_mark_skipped', {'jobId': '__JOB_ID__', 'partId': '__PART_ID__'}) }}`.replace('__JOB_ID__', jobId).replace('__PART_ID__', partId), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
updatePartStatus(partId, 'skipped');
|
||||
updateProgress(data);
|
||||
if (data.job_completed) {
|
||||
location.reload(); // Refresh to show completed status
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => console.error('Error:', error));
|
||||
}
|
||||
|
||||
function markPartPending(jobId, partId) {
|
||||
fetch(`{{ path('bulk_info_provider_mark_pending', {'jobId': '__JOB_ID__', 'partId': '__PART_ID__'}) }}`.replace('__JOB_ID__', jobId).replace('__PART_ID__', partId), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
updatePartStatus(partId, 'pending');
|
||||
updateProgress(data);
|
||||
}
|
||||
})
|
||||
.catch(error => console.error('Error:', error));
|
||||
}
|
||||
|
||||
function updatePartStatus(partId, status) {
|
||||
const card = document.getElementById(`part-card-${partId}`);
|
||||
const cardHeader = card.querySelector('.card-header');
|
||||
|
||||
// Remove existing status classes and background
|
||||
card.classList.remove('border-success', 'border-warning');
|
||||
card.style.backgroundColor = '';
|
||||
|
||||
// Remove existing status badges
|
||||
const existingBadges = cardHeader.querySelectorAll('.badge.bg-success, .badge.bg-warning');
|
||||
existingBadges.forEach(badge => {
|
||||
if (badge.innerHTML.includes('fas fa-check') || badge.innerHTML.includes('fas fa-forward')) {
|
||||
badge.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Add new status
|
||||
if (status === 'completed') {
|
||||
card.classList.add('border-success');
|
||||
card.style.backgroundColor = 'rgba(25, 135, 84, 0.1)';
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'badge bg-success';
|
||||
badge.innerHTML = '<i class="fas fa-check"></i> {% trans %}info_providers.bulk_import.completed{% endtrans %}';
|
||||
cardHeader.querySelector('.card-title').appendChild(badge);
|
||||
} else if (status === 'skipped') {
|
||||
card.classList.add('border-warning');
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'badge bg-warning';
|
||||
badge.innerHTML = '<i class="fas fa-forward"></i> {% trans %}info_providers.bulk_import.skipped{% endtrans %}';
|
||||
cardHeader.querySelector('.card-title').appendChild(badge);
|
||||
}
|
||||
|
||||
// Update buttons and Update Part button states
|
||||
const buttonGroup = cardHeader.querySelector('.btn-group');
|
||||
const updateButtons = card.querySelectorAll('.btn-primary');
|
||||
|
||||
if (status === 'completed' || status === 'skipped') {
|
||||
buttonGroup.innerHTML = `
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="markPartPending({{ job.id }}, ${partId})">
|
||||
<i class="fas fa-undo"></i> {% trans %}info_providers.bulk_import.mark_pending{% endtrans %}
|
||||
</button>
|
||||
`;
|
||||
// Disable Update Part buttons
|
||||
updateButtons.forEach(btn => {
|
||||
btn.classList.add('disabled');
|
||||
btn.setAttribute('aria-disabled', 'true');
|
||||
btn.href = '#';
|
||||
});
|
||||
} else {
|
||||
buttonGroup.innerHTML = `
|
||||
<button type="button" class="btn btn-success btn-sm" onclick="markPartCompleted({{ job.id }}, ${partId})">
|
||||
<i class="fas fa-check"></i> {% trans %}info_providers.bulk_import.mark_completed{% endtrans %}
|
||||
</button>
|
||||
<button type="button" class="btn btn-warning btn-sm" onclick="markPartSkipped({{ job.id }}, ${partId})">
|
||||
<i class="fas fa-forward"></i> {% trans %}info_providers.bulk_import.mark_skipped{% endtrans %}
|
||||
</button>
|
||||
`;
|
||||
// Enable Update Part buttons
|
||||
updateButtons.forEach(btn => {
|
||||
btn.classList.remove('disabled');
|
||||
btn.removeAttribute('aria-disabled');
|
||||
// Restore original href - this would need to be stored somewhere
|
||||
location.reload(); // For now, just reload to restore the original state
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateProgress(data) {
|
||||
document.getElementById('progress-bar').style.width = data.progress + '%';
|
||||
document.getElementById('progress-bar').setAttribute('aria-valuenow', data.progress);
|
||||
document.getElementById('progress-percentage').textContent = data.progress + '%';
|
||||
document.getElementById('completed-count').textContent = data.completed_count;
|
||||
document.getElementById('progress-text').textContent = `${data.completed_count} / ${data.total_count} completed`;
|
||||
|
||||
if (data.skipped_count !== undefined) {
|
||||
document.getElementById('skipped-count').textContent = data.skipped_count;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Loading…
Add table
Add a link
Reference in a new issue