mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-12-24 11:59:31 +00:00
348 lines
No EOL
18 KiB
Twig
348 lines
No EOL
18 KiB
Twig
{% extends "main_card.html.twig" %}
|
|
|
|
{% import "info_providers/providers.macro.html.twig" as providers_macro %}
|
|
{% import "helper.twig" as helper %}
|
|
|
|
{% block title %}
|
|
{% trans %}info_providers.bulk_import.step2.title{% endtrans %}
|
|
{% endblock %}
|
|
|
|
{% block card_title %}
|
|
<i class="fas fa-search"></i> {% trans %}info_providers.bulk_import.step2.title{% endtrans %}
|
|
<span class="badge bg-secondary">{{ job.displayNameKey|trans(job.displayNameParams) }} - {{ job.formattedTimestamp }}</span>
|
|
{% endblock %}
|
|
|
|
{% block card_content %}
|
|
|
|
<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>
|
|
<small class="text-muted">
|
|
{{ job.partCount }} {% trans %}info_providers.bulk_import.parts{% endtrans %} •
|
|
{{ job.resultCount }} {% trans %}info_providers.bulk_import.results{% endtrans %} •
|
|
{% trans %}info_providers.bulk_import.created_at{% endtrans %}: {{ job.createdAt|date('Y-m-d H:i') }}
|
|
</small>
|
|
</div>
|
|
<div>
|
|
{% if job.isPending %}
|
|
<span class="badge bg-warning">{% trans %}info_providers.bulk_import.status.pending{% endtrans %}</span>
|
|
{% elseif job.isInProgress %}
|
|
<span class="badge bg-info">{% trans %}info_providers.bulk_import.status.in_progress{% endtrans %}</span>
|
|
{% elseif job.isCompleted %}
|
|
<span class="badge bg-success">{% trans %}info_providers.bulk_import.status.completed{% endtrans %}</span>
|
|
{% elseif job.isFailed %}
|
|
<span class="badge bg-danger">{% trans %}info_providers.bulk_import.status.failed{% endtrans %}</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Progress Bar -->
|
|
<div class="card mb-3">
|
|
<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>
|
|
</div>
|
|
<div class="progress" style="height: 8px;">
|
|
<div id="progress-bar" class="progress-bar" role="progressbar"
|
|
style="width: {{ job.progressPercentage }}%"
|
|
aria-valuenow="{{ job.progressPercentage }}" aria-valuemin="0" aria-valuemax="100">
|
|
</div>
|
|
</div>
|
|
<div class="d-flex justify-content-between mt-2">
|
|
<small class="text-muted">
|
|
<span id="completed-count">{{ job.completedPartsCount }}</span> {% trans %}info_providers.bulk_import.completed{% endtrans %} •
|
|
<span id="skipped-count">{{ job.skippedPartsCount }}</span> {% trans %}info_providers.bulk_import.skipped{% endtrans %}
|
|
</small>
|
|
<small class="text-muted"><span id="progress-percentage">{{ job.progressPercentage }}%</span></small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tutorial/Instructions -->
|
|
<div class="alert alert-info mb-4" role="alert">
|
|
<h6 class="alert-heading">
|
|
<i class="fas fa-info-circle"></i> {% trans %}info_providers.bulk_import.step2.instructions.title{% endtrans %}
|
|
</h6>
|
|
<p class="mb-2">{% trans %}info_providers.bulk_import.step2.instructions.description{% endtrans %}</p>
|
|
<ul class="mb-0 ps-3">
|
|
<li>{% trans %}info_providers.bulk_import.step2.instructions.step1{% endtrans %}</li>
|
|
<li>{% trans %}info_providers.bulk_import.step2.instructions.step2{% endtrans %}</li>
|
|
<li>{% trans %}info_providers.bulk_import.step2.instructions.step3{% endtrans %}</li>
|
|
</ul>
|
|
</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 }}"
|
|
{% 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>
|
|
<h5 class="card-title mb-0">
|
|
<a href="{{ path('app_part_show', {'id': part.id}) }}" class="text-decoration-none">
|
|
{{ part.name }}
|
|
</a>
|
|
{% if isCompleted %}
|
|
<span class="badge bg-success">
|
|
<i class="fas fa-check"></i> {% trans %}info_providers.bulk_import.completed{% endtrans %}
|
|
</span>
|
|
{% elseif isSkipped %}
|
|
<span class="badge bg-warning">
|
|
<i class="fas fa-forward"></i> {% trans %}info_providers.bulk_import.skipped{% endtrans %}
|
|
</span>
|
|
{% endif %}
|
|
{% if part_result.errors is not empty %}
|
|
<span class="badge bg-danger">{% trans with {'%count%': part_result.errors|length} %}info_providers.bulk_import.errors{% endtrans %}</span>
|
|
{% endif %}
|
|
<span class="badge bg-info">{% trans with {'%count%': part_result.search_results|length} %}info_providers.bulk_import.results_found{% endtrans %}</span>
|
|
</h5>
|
|
</div>
|
|
<div class="btn-group" role="group">
|
|
{% if not isCompleted and not isSkipped %}
|
|
<button type="button" class="btn btn-success btn-sm" onclick="markPartCompleted({{ job.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 }})">
|
|
<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 }})">
|
|
<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 }})">
|
|
<i class="fas fa-undo"></i> {% trans %}info_providers.bulk_import.mark_pending{% endtrans %}
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
{% if part_result.errors is not empty %}
|
|
{% for error in part_result.errors %}
|
|
<div class="alert alert-warning" role="alert">
|
|
<i class="fas fa-exclamation-triangle"></i>
|
|
{{ error }}
|
|
</div>
|
|
{% endfor %}
|
|
{% endif %}
|
|
|
|
{% if part_result.search_results|length > 0 %}
|
|
<div class="table-responsive">
|
|
<table class="table table-sm">
|
|
<thead>
|
|
<tr>
|
|
<th></th>
|
|
<th>{% trans %}name.label{% endtrans %}</th>
|
|
<th>{% trans %}description.label{% endtrans %}</th>
|
|
<th>{% trans %}manufacturer.label{% endtrans %}</th>
|
|
<th>{% trans %}info_providers.table.provider.label{% endtrans %}</th>
|
|
<th>{% trans %}info_providers.bulk_import.source_field{% endtrans %}</th>
|
|
<th>{% trans %}action.label{% endtrans %}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for result in part_result.search_results %}
|
|
{% set dto = result.dto %}
|
|
{% set localPart = result.localPart %}
|
|
<tr>
|
|
<td>
|
|
<img src="{{ dto.preview_image_url }}" data-thumbnail="{{ dto.preview_image_url }}"
|
|
class="hoverpic" style="max-width: 30px;" {{ stimulus_controller('elements/hoverpic') }}>
|
|
</td>
|
|
<td>
|
|
{% if dto.provider_url is not null %}
|
|
<a href="{{ dto.provider_url }}" target="_blank" rel="noopener">{{ dto.name }}</a>
|
|
{% else %}
|
|
{{ dto.name }}
|
|
{% endif %}
|
|
{% if dto.mpn is not null %}
|
|
<br><small class="text-muted">{{ dto.mpn }}</small>
|
|
{% endif %}
|
|
</td>
|
|
<td>{{ dto.description }}</td>
|
|
<td>{{ dto.manufacturer ?? '' }}</td>
|
|
<td>
|
|
{{ info_provider_label(dto.provider_key)|default(dto.provider_key) }}
|
|
<br><small class="text-muted">{{ dto.provider_id }}</small>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-info">{{ dto._source_field ?? 'unknown' }}</span>
|
|
{% if dto._source_keyword %}
|
|
<br><small class="text-muted">{{ dto._source_keyword }}</small>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<div class="btn-group-vertical btn-group-sm" role="group">
|
|
{% set updateHref = path('info_providers_update_part',
|
|
{'id': part.id, 'providerKey': dto.provider_key, 'providerId': dto.provider_id}) ~ '?jobId=' ~ job.id %}
|
|
<a class="btn btn-primary{% if isCompleted %} disabled{% endif %}" href="{% if not isCompleted %}{{ updateHref }}{% else %}#{% endif %}" target="_blank"{% if isCompleted %} aria-disabled="true"{% endif %}>
|
|
<i class="fas fa-edit"></i> {% trans %}info_providers.bulk_import.update_part{% endtrans %}
|
|
</a>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<div class="alert alert-info" role="alert">
|
|
{% trans %}info_providers.search.no_results{% endtrans %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
|
|
{% 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 %} |