mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-12-06 02:59:29 +00:00
More sophisticated two-step bulk import from info providers
This commit is contained in:
parent
5ab7ac4d4b
commit
c91d37d2a4
14 changed files with 2004 additions and 9 deletions
32
migrations/Version20250802153643.php
Normal file
32
migrations/Version20250802153643.php
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250802153643 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add bulk info provider import jobs table';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('CREATE TABLE bulk_info_provider_import_jobs (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name CLOB NOT NULL, part_ids CLOB NOT NULL, field_mappings CLOB NOT NULL, search_results CLOB NOT NULL, status VARCHAR(20) NOT NULL, created_at DATETIME NOT NULL, completed_at DATETIME DEFAULT NULL, prefetch_details BOOLEAN NOT NULL, progress CLOB NOT NULL, created_by_id INTEGER NOT NULL, CONSTRAINT FK_7F58C1EDB03A8386 FOREIGN KEY (created_by_id) REFERENCES "users" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
|
||||
$this->addSql('CREATE INDEX IDX_7F58C1EDB03A8386 ON bulk_info_provider_import_jobs (created_by_id)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('DROP TABLE bulk_info_provider_import_jobs');
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,8 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\BulkInfoProviderImportJob;
|
||||
use App\Entity\BulkImportJobStatus;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Entity\Parts\Supplier;
|
||||
use App\Form\InfoProviderSystem\GlobalFieldMappingType;
|
||||
|
|
@ -36,8 +38,6 @@ use Symfony\Component\HttpFoundation\Request;
|
|||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
use function Symfony\Component\Translation\t;
|
||||
|
||||
#[Route('/tools/bulk-info-provider-import')]
|
||||
class BulkInfoProviderImportController extends AbstractController
|
||||
{
|
||||
|
|
@ -87,7 +87,8 @@ class BulkInfoProviderImportController extends AbstractController
|
|||
$initialData = [
|
||||
'field_mappings' => [
|
||||
['field' => 'mpn', 'providers' => []]
|
||||
]
|
||||
],
|
||||
'prefetch_details' => false
|
||||
];
|
||||
|
||||
$form = $this->createForm(GlobalFieldMappingType::class, $initialData, [
|
||||
|
|
@ -98,7 +99,20 @@ class BulkInfoProviderImportController extends AbstractController
|
|||
$searchResults = null;
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$fieldMappings = $form->getData()['field_mappings'];
|
||||
$formData = $form->getData();
|
||||
$fieldMappings = $formData['field_mappings'];
|
||||
$prefetchDetails = $formData['prefetch_details'] ?? false;
|
||||
|
||||
// Create and save the job
|
||||
$job = new BulkInfoProviderImportJob();
|
||||
$job->setPartIds(array_map(fn($part) => $part->getId(), $parts));
|
||||
$job->setFieldMappings($fieldMappings);
|
||||
$job->setPrefetchDetails($prefetchDetails);
|
||||
$job->setCreatedBy($this->getUser());
|
||||
|
||||
$this->entityManager->persist($job);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$searchResults = [];
|
||||
|
||||
foreach ($parts as $part) {
|
||||
|
|
@ -161,16 +175,100 @@ class BulkInfoProviderImportController extends AbstractController
|
|||
|
||||
$searchResults[] = $partResult;
|
||||
}
|
||||
|
||||
// Save search results to job
|
||||
$job->setSearchResults($this->serializeSearchResults($searchResults));
|
||||
$job->markAsInProgress();
|
||||
$this->entityManager->flush();
|
||||
|
||||
// Prefetch details if requested
|
||||
if ($prefetchDetails && !empty($searchResults)) {
|
||||
$this->prefetchDetailsForResults($searchResults, $exceptionLogger);
|
||||
}
|
||||
|
||||
// Redirect to step 2 with the job
|
||||
return $this->redirectToRoute('bulk_info_provider_step2', ['jobId' => $job->getId()]);
|
||||
}
|
||||
|
||||
// Get existing in-progress jobs for current user
|
||||
$existingJobs = $this->entityManager->getRepository(BulkInfoProviderImportJob::class)
|
||||
->findBy(['createdBy' => $this->getUser(), 'status' => BulkImportJobStatus::IN_PROGRESS], ['createdAt' => 'DESC'], 10);
|
||||
|
||||
return $this->render('info_providers/bulk_import/step1.html.twig', [
|
||||
'form' => $form,
|
||||
'parts' => $parts,
|
||||
'search_results' => $searchResults,
|
||||
'existing_jobs' => $existingJobs,
|
||||
'fieldChoices' => $fieldChoices
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/manage', name: 'bulk_info_provider_manage')]
|
||||
public function manageBulkJobs(): Response
|
||||
{
|
||||
// Get all jobs for current user
|
||||
$allJobs = $this->entityManager->getRepository(BulkInfoProviderImportJob::class)
|
||||
->findBy([], ['createdAt' => 'DESC']);
|
||||
|
||||
// Check and auto-complete jobs that should be completed
|
||||
$updatedJobs = false;
|
||||
foreach ($allJobs as $job) {
|
||||
if ($job->isAllPartsCompleted() && !$job->isCompleted()) {
|
||||
$job->markAsCompleted();
|
||||
$updatedJobs = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Flush changes if any jobs were updated
|
||||
if ($updatedJobs) {
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
return $this->render('info_providers/bulk_import/manage.html.twig', [
|
||||
'jobs' => $allJobs
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/job/{jobId}/delete', name: 'bulk_info_provider_delete', methods: ['DELETE'])]
|
||||
public function deleteJob(int $jobId): Response
|
||||
{
|
||||
$job = $this->entityManager->getRepository(BulkInfoProviderImportJob::class)->find($jobId);
|
||||
|
||||
if (!$job || $job->getCreatedBy() !== $this->getUser()) {
|
||||
return $this->json(['error' => 'Job not found or access denied'], 404);
|
||||
}
|
||||
|
||||
// Only allow deletion of completed, failed, or stopped jobs
|
||||
if (!$job->isCompleted() && !$job->isFailed() && !$job->isStopped()) {
|
||||
return $this->json(['error' => 'Cannot delete active job'], 400);
|
||||
}
|
||||
|
||||
$this->entityManager->remove($job);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $this->json(['success' => true]);
|
||||
}
|
||||
|
||||
#[Route('/job/{jobId}/stop', name: 'bulk_info_provider_stop', methods: ['POST'])]
|
||||
public function stopJob(int $jobId): Response
|
||||
{
|
||||
$job = $this->entityManager->getRepository(BulkInfoProviderImportJob::class)->find($jobId);
|
||||
|
||||
if (!$job || $job->getCreatedBy() !== $this->getUser()) {
|
||||
return $this->json(['error' => 'Job not found or access denied'], 404);
|
||||
}
|
||||
|
||||
// Only allow stopping of pending or in-progress jobs
|
||||
if (!$job->canBeStopped()) {
|
||||
return $this->json(['error' => 'Cannot stop job in current status'], 400);
|
||||
}
|
||||
|
||||
$job->markAsStopped();
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $this->json(['success' => true]);
|
||||
}
|
||||
|
||||
private function getKeywordFromField(Part $part, string $field): ?string
|
||||
{
|
||||
return match ($field) {
|
||||
|
|
@ -207,4 +305,232 @@ class BulkInfoProviderImportController extends AbstractController
|
|||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch details for all search results to populate cache
|
||||
*/
|
||||
private function prefetchDetailsForResults(array $searchResults, LoggerInterface $logger): void
|
||||
{
|
||||
$prefetchCount = 0;
|
||||
|
||||
foreach ($searchResults as $partResult) {
|
||||
foreach ($partResult['search_results'] as $result) {
|
||||
$dto = $result['dto'];
|
||||
|
||||
try {
|
||||
// This call will cache the details for later use
|
||||
$this->infoRetriever->getDetails($dto->provider_key, $dto->provider_id);
|
||||
$prefetchCount++;
|
||||
} catch (\Exception $e) {
|
||||
$logger->warning('Failed to prefetch details for provider part', [
|
||||
'provider_key' => $dto->provider_key,
|
||||
'provider_id' => $dto->provider_id,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($prefetchCount > 0) {
|
||||
$this->addFlash('success', "Prefetched details for {$prefetchCount} search results");
|
||||
}
|
||||
}
|
||||
|
||||
#[Route('/step2/{jobId}', name: 'bulk_info_provider_step2')]
|
||||
public function step2(int $jobId): Response
|
||||
{
|
||||
$job = $this->entityManager->getRepository(BulkInfoProviderImportJob::class)->find($jobId);
|
||||
|
||||
if (!$job) {
|
||||
$this->addFlash('error', 'Bulk import job not found');
|
||||
return $this->redirectToRoute('bulk_info_provider_step1');
|
||||
}
|
||||
|
||||
// Check if user owns this job
|
||||
if ($job->getCreatedBy() !== $this->getUser()) {
|
||||
$this->addFlash('error', 'Access denied to this bulk import job');
|
||||
return $this->redirectToRoute('bulk_info_provider_step1');
|
||||
}
|
||||
|
||||
// Get the parts and deserialize search results
|
||||
$partRepository = $this->entityManager->getRepository(Part::class);
|
||||
$parts = $partRepository->getElementsFromIDArray($job->getPartIds());
|
||||
$searchResults = $this->deserializeSearchResults($job->getSearchResults(), $parts);
|
||||
|
||||
return $this->render('info_providers/bulk_import/step2.html.twig', [
|
||||
'job' => $job,
|
||||
'parts' => $parts,
|
||||
'search_results' => $searchResults,
|
||||
]);
|
||||
}
|
||||
|
||||
private function serializeSearchResults(array $searchResults): array
|
||||
{
|
||||
$serialized = [];
|
||||
|
||||
foreach ($searchResults as $partResult) {
|
||||
$partData = [
|
||||
'part_id' => $partResult['part']->getId(),
|
||||
'search_results' => [],
|
||||
'errors' => $partResult['errors']
|
||||
];
|
||||
|
||||
foreach ($partResult['search_results'] as $result) {
|
||||
$dto = $result['dto'];
|
||||
$partData['search_results'][] = [
|
||||
'dto' => [
|
||||
'provider_key' => $dto->provider_key,
|
||||
'provider_id' => $dto->provider_id,
|
||||
'name' => $dto->name,
|
||||
'description' => $dto->description,
|
||||
'manufacturer' => $dto->manufacturer,
|
||||
'mpn' => $dto->mpn,
|
||||
'provider_url' => $dto->provider_url,
|
||||
'preview_image_url' => $dto->preview_image_url,
|
||||
'_source_field' => $dto->_source_field ?? null,
|
||||
'_source_keyword' => $dto->_source_keyword ?? null,
|
||||
],
|
||||
'localPart' => $result['localPart'] ? $result['localPart']->getId() : null
|
||||
];
|
||||
}
|
||||
|
||||
$serialized[] = $partData;
|
||||
}
|
||||
|
||||
return $serialized;
|
||||
}
|
||||
|
||||
private function deserializeSearchResults(array $serializedResults, array $parts): array
|
||||
{
|
||||
$partsById = [];
|
||||
foreach ($parts as $part) {
|
||||
$partsById[$part->getId()] = $part;
|
||||
}
|
||||
|
||||
$searchResults = [];
|
||||
|
||||
foreach ($serializedResults as $partData) {
|
||||
$part = $partsById[$partData['part_id']] ?? null;
|
||||
if (!$part) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$partResult = [
|
||||
'part' => $part,
|
||||
'search_results' => [],
|
||||
'errors' => $partData['errors']
|
||||
];
|
||||
|
||||
foreach ($partData['search_results'] as $resultData) {
|
||||
$dtoData = $resultData['dto'];
|
||||
|
||||
$dto = new \App\Services\InfoProviderSystem\DTOs\SearchResultDTO(
|
||||
provider_key: $dtoData['provider_key'],
|
||||
provider_id: $dtoData['provider_id'],
|
||||
name: $dtoData['name'],
|
||||
description: $dtoData['description'],
|
||||
manufacturer: $dtoData['manufacturer'],
|
||||
mpn: $dtoData['mpn'],
|
||||
provider_url: $dtoData['provider_url'],
|
||||
preview_image_url: $dtoData['preview_image_url']
|
||||
);
|
||||
|
||||
// Add the source field info
|
||||
$dto->_source_field = $dtoData['_source_field'];
|
||||
$dto->_source_keyword = $dtoData['_source_keyword'];
|
||||
|
||||
$localPart = null;
|
||||
if ($resultData['localPart']) {
|
||||
$localPart = $this->entityManager->getRepository(Part::class)->find($resultData['localPart']);
|
||||
}
|
||||
|
||||
$partResult['search_results'][] = [
|
||||
'dto' => $dto,
|
||||
'localPart' => $localPart
|
||||
];
|
||||
}
|
||||
|
||||
$searchResults[] = $partResult;
|
||||
}
|
||||
|
||||
return $searchResults;
|
||||
}
|
||||
|
||||
#[Route('/job/{jobId}/part/{partId}/mark-completed', name: 'bulk_info_provider_mark_completed', methods: ['POST'])]
|
||||
public function markPartCompleted(int $jobId, int $partId): Response
|
||||
{
|
||||
$job = $this->entityManager->getRepository(BulkInfoProviderImportJob::class)->find($jobId);
|
||||
|
||||
if (!$job || $job->getCreatedBy() !== $this->getUser()) {
|
||||
return $this->json(['error' => 'Job not found or access denied'], 404);
|
||||
}
|
||||
|
||||
$job->markPartAsCompleted($partId);
|
||||
|
||||
// Auto-complete job if all parts are done
|
||||
if ($job->isAllPartsCompleted() && !$job->isCompleted()) {
|
||||
$job->markAsCompleted();
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $this->json([
|
||||
'success' => true,
|
||||
'progress' => $job->getProgressPercentage(),
|
||||
'completed_count' => $job->getCompletedPartsCount(),
|
||||
'total_count' => $job->getPartCount(),
|
||||
'job_completed' => $job->isCompleted()
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/job/{jobId}/part/{partId}/mark-skipped', name: 'bulk_info_provider_mark_skipped', methods: ['POST'])]
|
||||
public function markPartSkipped(int $jobId, int $partId, Request $request): Response
|
||||
{
|
||||
$job = $this->entityManager->getRepository(BulkInfoProviderImportJob::class)->find($jobId);
|
||||
|
||||
if (!$job || $job->getCreatedBy() !== $this->getUser()) {
|
||||
return $this->json(['error' => 'Job not found or access denied'], 404);
|
||||
}
|
||||
|
||||
$reason = $request->request->get('reason', '');
|
||||
$job->markPartAsSkipped($partId, $reason);
|
||||
|
||||
// Auto-complete job if all parts are done
|
||||
if ($job->isAllPartsCompleted() && !$job->isCompleted()) {
|
||||
$job->markAsCompleted();
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $this->json([
|
||||
'success' => true,
|
||||
'progress' => $job->getProgressPercentage(),
|
||||
'completed_count' => $job->getCompletedPartsCount(),
|
||||
'skipped_count' => $job->getSkippedPartsCount(),
|
||||
'total_count' => $job->getPartCount(),
|
||||
'job_completed' => $job->isCompleted()
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/job/{jobId}/part/{partId}/mark-pending', name: 'bulk_info_provider_mark_pending', methods: ['POST'])]
|
||||
public function markPartPending(int $jobId, int $partId): Response
|
||||
{
|
||||
$job = $this->entityManager->getRepository(BulkInfoProviderImportJob::class)->find($jobId);
|
||||
|
||||
if (!$job || $job->getCreatedBy() !== $this->getUser()) {
|
||||
return $this->json(['error' => 'Job not found or access denied'], 404);
|
||||
}
|
||||
|
||||
$job->markPartAsPending($partId);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $this->json([
|
||||
'success' => true,
|
||||
'progress' => $job->getProgressPercentage(),
|
||||
'completed_count' => $job->getCompletedPartsCount(),
|
||||
'skipped_count' => $job->getSkippedPartsCount(),
|
||||
'total_count' => $job->getPartCount(),
|
||||
'job_completed' => $job->isCompleted()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -131,7 +131,43 @@ class PartController extends AbstractController
|
|||
{
|
||||
$this->denyAccessUnlessGranted('edit', $part);
|
||||
|
||||
return $this->renderPartForm('edit', $request, $part);
|
||||
// Check if this is part of a bulk import job
|
||||
$jobId = $request->query->get('jobId');
|
||||
$bulkJob = null;
|
||||
if ($jobId) {
|
||||
$bulkJob = $this->em->getRepository(\App\Entity\BulkInfoProviderImportJob::class)->find($jobId);
|
||||
// Verify user owns this job
|
||||
if ($bulkJob && $bulkJob->getCreatedBy() !== $this->getUser()) {
|
||||
$bulkJob = null;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->renderPartForm('edit', $request, $part, [], [
|
||||
'bulk_job' => $bulkJob
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/bulk-import-complete/{jobId}', name: 'part_bulk_import_complete', methods: ['POST'])]
|
||||
public function markBulkImportComplete(Part $part, int $jobId, Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('edit', $part);
|
||||
|
||||
if (!$this->isCsrfTokenValid('bulk_complete_' . $part->getId(), $request->request->get('_token'))) {
|
||||
throw $this->createAccessDeniedException('Invalid CSRF token');
|
||||
}
|
||||
|
||||
$bulkJob = $this->em->getRepository(\App\Entity\BulkInfoProviderImportJob::class)->find($jobId);
|
||||
if (!$bulkJob || $bulkJob->getCreatedBy() !== $this->getUser()) {
|
||||
throw $this->createNotFoundException('Bulk import job not found');
|
||||
}
|
||||
|
||||
$bulkJob->markPartAsCompleted($part->getId());
|
||||
$this->em->persist($bulkJob);
|
||||
$this->em->flush();
|
||||
|
||||
$this->addFlash('success', 'Part marked as completed in bulk import');
|
||||
|
||||
return $this->redirectToRoute('bulk_info_provider_step2', ['jobId' => $jobId]);
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/delete', name: 'part_delete', methods: ['DELETE'])]
|
||||
|
|
@ -273,10 +309,22 @@ class PartController extends AbstractController
|
|||
|
||||
$this->addFlash('notice', t('part.merge.flash.please_review'));
|
||||
|
||||
// Check if this is part of a bulk import job
|
||||
$jobId = $request->query->get('jobId');
|
||||
$bulkJob = null;
|
||||
if ($jobId) {
|
||||
$bulkJob = $this->em->getRepository(\App\Entity\BulkInfoProviderImportJob::class)->find($jobId);
|
||||
// Verify user owns this job
|
||||
if ($bulkJob && $bulkJob->getCreatedBy() !== $this->getUser()) {
|
||||
$bulkJob = null;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->renderPartForm('update_from_ip', $request, $part, [
|
||||
'info_provider_dto' => $dto,
|
||||
], [
|
||||
'tname_before' => $old_name
|
||||
'tname_before' => $old_name,
|
||||
'bulk_job' => $bulkJob
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -352,6 +400,12 @@ class PartController extends AbstractController
|
|||
return $this->redirectToRoute('part_new');
|
||||
}
|
||||
|
||||
// Check if we're in bulk import mode and preserve jobId
|
||||
$jobId = $request->query->get('jobId');
|
||||
if ($jobId && isset($merge_infos['bulk_job'])) {
|
||||
return $this->redirectToRoute('part_edit', ['id' => $new_part->getID(), 'jobId' => $jobId]);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('part_edit', ['id' => $new_part->getID()]);
|
||||
}
|
||||
|
||||
|
|
@ -375,7 +429,9 @@ class PartController extends AbstractController
|
|||
'part' => $new_part,
|
||||
'form' => $form,
|
||||
'merge_old_name' => $merge_infos['tname_before'] ?? null,
|
||||
'merge_other' => $merge_infos['other_part'] ?? null
|
||||
'merge_other' => $merge_infos['other_part'] ?? null,
|
||||
'bulk_job' => $merge_infos['bulk_job'] ?? null,
|
||||
'jobId' => $request->query->get('jobId')
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
|||
336
src/Entity/BulkInfoProviderImportJob.php
Normal file
336
src/Entity/BulkInfoProviderImportJob.php
Normal file
|
|
@ -0,0 +1,336 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Entity\Base\AbstractDBElement;
|
||||
use App\Entity\UserSystem\User;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
enum BulkImportJobStatus: string
|
||||
{
|
||||
case PENDING = 'pending';
|
||||
case IN_PROGRESS = 'in_progress';
|
||||
case COMPLETED = 'completed';
|
||||
case STOPPED = 'stopped';
|
||||
case FAILED = 'failed';
|
||||
}
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'bulk_info_provider_import_jobs')]
|
||||
class BulkInfoProviderImportJob extends AbstractDBElement
|
||||
{
|
||||
#[ORM\Column(type: Types::TEXT)]
|
||||
private string $name = '';
|
||||
|
||||
#[ORM\Column(type: Types::JSON)]
|
||||
private array $partIds = [];
|
||||
|
||||
#[ORM\Column(type: Types::JSON)]
|
||||
private array $fieldMappings = [];
|
||||
|
||||
#[ORM\Column(type: Types::JSON)]
|
||||
private array $searchResults = [];
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 20, enumType: BulkImportJobStatus::class)]
|
||||
private BulkImportJobStatus $status = BulkImportJobStatus::PENDING;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
|
||||
private \DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
|
||||
private ?\DateTimeImmutable $completedAt = null;
|
||||
|
||||
#[ORM\Column(type: Types::BOOLEAN)]
|
||||
private bool $prefetchDetails = false;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private User $createdBy;
|
||||
|
||||
#[ORM\Column(type: Types::JSON)]
|
||||
private array $progress = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->createdAt = new \DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getDisplayNameKey(): string
|
||||
{
|
||||
return 'info_providers.bulk_import.job_name_template';
|
||||
}
|
||||
|
||||
public function getDisplayNameParams(): array
|
||||
{
|
||||
return ['%count%' => $this->getPartCount()];
|
||||
}
|
||||
|
||||
public function getFormattedTimestamp(): string
|
||||
{
|
||||
return $this->createdAt->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
public function setName(string $name): self
|
||||
{
|
||||
$this->name = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPartIds(): array
|
||||
{
|
||||
return $this->partIds;
|
||||
}
|
||||
|
||||
public function setPartIds(array $partIds): self
|
||||
{
|
||||
$this->partIds = $partIds;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFieldMappings(): array
|
||||
{
|
||||
return $this->fieldMappings;
|
||||
}
|
||||
|
||||
public function setFieldMappings(array $fieldMappings): self
|
||||
{
|
||||
$this->fieldMappings = $fieldMappings;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSearchResults(): array
|
||||
{
|
||||
return $this->searchResults;
|
||||
}
|
||||
|
||||
public function setSearchResults(array $searchResults): self
|
||||
{
|
||||
$this->searchResults = $searchResults;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStatus(): BulkImportJobStatus
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function setStatus(BulkImportJobStatus $status): self
|
||||
{
|
||||
$this->status = $status;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): \DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function getCompletedAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->completedAt;
|
||||
}
|
||||
|
||||
public function setCompletedAt(?\DateTimeImmutable $completedAt): self
|
||||
{
|
||||
$this->completedAt = $completedAt;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isPrefetchDetails(): bool
|
||||
{
|
||||
return $this->prefetchDetails;
|
||||
}
|
||||
|
||||
public function setPrefetchDetails(bool $prefetchDetails): self
|
||||
{
|
||||
$this->prefetchDetails = $prefetchDetails;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedBy(): User
|
||||
{
|
||||
return $this->createdBy;
|
||||
}
|
||||
|
||||
public function setCreatedBy(User $createdBy): self
|
||||
{
|
||||
$this->createdBy = $createdBy;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getProgress(): array
|
||||
{
|
||||
return $this->progress;
|
||||
}
|
||||
|
||||
public function setProgress(array $progress): self
|
||||
{
|
||||
$this->progress = $progress;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function markAsCompleted(): self
|
||||
{
|
||||
$this->status = BulkImportJobStatus::COMPLETED;
|
||||
$this->completedAt = new \DateTimeImmutable();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function markAsFailed(): self
|
||||
{
|
||||
$this->status = BulkImportJobStatus::FAILED;
|
||||
$this->completedAt = new \DateTimeImmutable();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function markAsStopped(): self
|
||||
{
|
||||
$this->status = BulkImportJobStatus::STOPPED;
|
||||
$this->completedAt = new \DateTimeImmutable();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function markAsInProgress(): self
|
||||
{
|
||||
$this->status = BulkImportJobStatus::IN_PROGRESS;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isPending(): bool
|
||||
{
|
||||
return $this->status === BulkImportJobStatus::PENDING;
|
||||
}
|
||||
|
||||
public function isInProgress(): bool
|
||||
{
|
||||
return $this->status === BulkImportJobStatus::IN_PROGRESS;
|
||||
}
|
||||
|
||||
public function isCompleted(): bool
|
||||
{
|
||||
return $this->status === BulkImportJobStatus::COMPLETED;
|
||||
}
|
||||
|
||||
public function isFailed(): bool
|
||||
{
|
||||
return $this->status === BulkImportJobStatus::FAILED;
|
||||
}
|
||||
|
||||
public function isStopped(): bool
|
||||
{
|
||||
return $this->status === BulkImportJobStatus::STOPPED;
|
||||
}
|
||||
|
||||
public function canBeStopped(): bool
|
||||
{
|
||||
return $this->status === BulkImportJobStatus::PENDING || $this->status === BulkImportJobStatus::IN_PROGRESS;
|
||||
}
|
||||
|
||||
public function getPartCount(): int
|
||||
{
|
||||
return count($this->partIds);
|
||||
}
|
||||
|
||||
public function getResultCount(): int
|
||||
{
|
||||
$count = 0;
|
||||
foreach ($this->searchResults as $partResult) {
|
||||
$count += count($partResult['search_results'] ?? []);
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
|
||||
public function markPartAsCompleted(int $partId): self
|
||||
{
|
||||
$this->progress[$partId] = [
|
||||
'status' => 'completed',
|
||||
'completed_at' => (new \DateTimeImmutable())->format('c')
|
||||
];
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function markPartAsSkipped(int $partId, string $reason = ''): self
|
||||
{
|
||||
$this->progress[$partId] = [
|
||||
'status' => 'skipped',
|
||||
'reason' => $reason,
|
||||
'completed_at' => (new \DateTimeImmutable())->format('c')
|
||||
];
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function markPartAsPending(int $partId): self
|
||||
{
|
||||
// Remove from progress array to mark as pending
|
||||
unset($this->progress[$partId]);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isPartCompleted(int $partId): bool
|
||||
{
|
||||
return isset($this->progress[$partId]) && $this->progress[$partId]['status'] === 'completed';
|
||||
}
|
||||
|
||||
public function isPartSkipped(int $partId): bool
|
||||
{
|
||||
return isset($this->progress[$partId]) && $this->progress[$partId]['status'] === 'skipped';
|
||||
}
|
||||
|
||||
public function getCompletedPartsCount(): int
|
||||
{
|
||||
return count(array_filter($this->progress, fn($p) => $p['status'] === 'completed'));
|
||||
}
|
||||
|
||||
public function getSkippedPartsCount(): int
|
||||
{
|
||||
return count(array_filter($this->progress, fn($p) => $p['status'] === 'skipped'));
|
||||
}
|
||||
|
||||
public function getProgressPercentage(): float
|
||||
{
|
||||
$total = $this->getPartCount();
|
||||
if ($total === 0) {
|
||||
return 100.0;
|
||||
}
|
||||
|
||||
$completed = $this->getCompletedPartsCount() + $this->getSkippedPartsCount();
|
||||
return round(($completed / $total) * 100, 1);
|
||||
}
|
||||
|
||||
public function isAllPartsCompleted(): bool
|
||||
{
|
||||
$total = $this->getPartCount();
|
||||
if ($total === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$completed = $this->getCompletedPartsCount() + $this->getSkippedPartsCount();
|
||||
return $completed >= $total;
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ namespace App\Entity\LogSystem;
|
|||
|
||||
use App\Entity\Attachments\Attachment;
|
||||
use App\Entity\Attachments\AttachmentType;
|
||||
use App\Entity\BulkInfoProviderImportJob;
|
||||
use App\Entity\LabelSystem\LabelProfile;
|
||||
use App\Entity\Parameters\AbstractParameter;
|
||||
use App\Entity\Parts\Category;
|
||||
|
|
@ -67,6 +68,7 @@ enum LogTargetType: int
|
|||
case LABEL_PROFILE = 19;
|
||||
|
||||
case PART_ASSOCIATION = 20;
|
||||
case BULK_INFO_PROVIDER_IMPORT_JOB = 21;
|
||||
|
||||
/**
|
||||
* Returns the class name of the target type or null if the target type is NONE.
|
||||
|
|
@ -96,6 +98,7 @@ enum LogTargetType: int
|
|||
self::PARAMETER => AbstractParameter::class,
|
||||
self::LABEL_PROFILE => LabelProfile::class,
|
||||
self::PART_ASSOCIATION => PartAssociation::class,
|
||||
self::BULK_INFO_PROVIDER_IMPORT_JOB => BulkInfoProviderImportJob::class,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ declare(strict_types=1);
|
|||
namespace App\Form\InfoProviderSystem;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
|
@ -46,6 +47,12 @@ class GlobalFieldMappingType extends AbstractType
|
|||
'label' => false,
|
||||
]);
|
||||
|
||||
$builder->add('prefetch_details', CheckboxType::class, [
|
||||
'label' => 'info_providers.bulk_import.prefetch_details',
|
||||
'required' => false,
|
||||
'help' => 'info_providers.bulk_import.prefetch_details_help',
|
||||
]);
|
||||
|
||||
$builder->add('submit', SubmitType::class, [
|
||||
'label' => 'info_providers.bulk_search.submit'
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ use App\Entity\Attachments\AttachmentContainingDBElement;
|
|||
use App\Entity\Attachments\Attachment;
|
||||
use App\Entity\Attachments\AttachmentType;
|
||||
use App\Entity\Base\AbstractDBElement;
|
||||
use App\Entity\BulkInfoProviderImportJob;
|
||||
use App\Entity\Contracts\NamedElementInterface;
|
||||
use App\Entity\Parts\PartAssociation;
|
||||
use App\Entity\ProjectSystem\Project;
|
||||
|
|
@ -79,6 +80,7 @@ class ElementTypeNameGenerator
|
|||
AbstractParameter::class => $this->translator->trans('parameter.label'),
|
||||
LabelProfile::class => $this->translator->trans('label_profile.label'),
|
||||
PartAssociation::class => $this->translator->trans('part_association.label'),
|
||||
BulkInfoProviderImportJob::class => $this->translator->trans('bulk_info_provider_import_job.label'),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -138,6 +138,11 @@ class ToolsTreeBuilder
|
|||
$this->translator->trans('info_providers.search.title'),
|
||||
$this->urlGenerator->generate('info_providers_search')
|
||||
))->setIcon('fa-treeview fa-fw fa-solid fa-cloud-arrow-down');
|
||||
|
||||
$nodes[] = (new TreeViewNode(
|
||||
$this->translator->trans('info_providers.bulk_import.manage_jobs'),
|
||||
$this->urlGenerator->generate('bulk_info_provider_manage')
|
||||
))->setIcon('fa-treeview fa-fw fa-solid fa-tasks');
|
||||
}
|
||||
|
||||
return $nodes;
|
||||
|
|
|
|||
168
templates/info_providers/bulk_import/manage.html.twig
Normal file
168
templates/info_providers/bulk_import/manage.html.twig
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
{% extends "main_card.html.twig" %}
|
||||
|
||||
{% block title %}
|
||||
{% trans %}info_providers.bulk_import.manage_jobs{% endtrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block card_title %}
|
||||
<i class="fas fa-tasks"></i> {% trans %}info_providers.bulk_import.manage_jobs{% endtrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block card_content %}
|
||||
|
||||
<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 %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{% if jobs is not empty %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans %}info_providers.bulk_import.job_name{% endtrans %}</th>
|
||||
<th>{% trans %}info_providers.bulk_import.parts_count{% endtrans %}</th>
|
||||
<th>{% trans %}info_providers.bulk_import.results_count{% endtrans %}</th>
|
||||
<th>{% trans %}info_providers.bulk_import.progress{% endtrans %}</th>
|
||||
<th>{% trans %}info_providers.bulk_import.status{% endtrans %}</th>
|
||||
<th>{% trans %}info_providers.bulk_import.created_by{% endtrans %}</th>
|
||||
<th>{% trans %}info_providers.bulk_import.created_at{% endtrans %}</th>
|
||||
<th>{% trans %}info_providers.bulk_import.completed_at{% endtrans %}</th>
|
||||
<th>{% trans %}info_providers.bulk_import.action.label{% endtrans %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for job in jobs %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{{ job.displayNameKey|trans(job.displayNameParams) }} - {{ job.formattedTimestamp }}</strong>
|
||||
{% if job.isInProgress %}
|
||||
<span class="badge bg-info ms-2">Active</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ job.partCount }}</td>
|
||||
<td>{{ job.resultCount }}</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="progress me-2" style="width: 80px; height: 12px;">
|
||||
<div class="progress-bar {% if job.isCompleted %}bg-success{% elseif job.isFailed %}bg-danger{% else %}bg-info{% endif %}"
|
||||
role="progressbar"
|
||||
style="width: {{ job.progressPercentage }}%"
|
||||
aria-valuenow="{{ job.progressPercentage }}"
|
||||
aria-valuemin="0" aria-valuemax="100">
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-muted">{{ job.progressPercentage }}%</small>
|
||||
</div>
|
||||
<small class="text-muted">
|
||||
{% trans with {'%current%': job.completedPartsCount + job.skippedPartsCount, '%total%': job.partCount} %}info_providers.bulk_import.progress_label{% endtrans %}
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
{% 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.isStopped %}
|
||||
<span class="badge bg-secondary">{% trans %}info_providers.bulk_import.status.stopped{% endtrans %}</span>
|
||||
{% elseif job.isFailed %}
|
||||
<span class="badge bg-danger">{% trans %}info_providers.bulk_import.status.failed{% endtrans %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ job.createdBy.username }}</td>
|
||||
<td>{{ job.createdAt|date('Y-m-d H:i') }}</td>
|
||||
<td>
|
||||
{% if job.completedAt %}
|
||||
{{ job.completedAt|date('Y-m-d H:i') }}
|
||||
{% else %}
|
||||
<span class="text-muted">-</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
{% if job.isInProgress or job.isCompleted or job.isStopped %}
|
||||
<a href="{{ path('bulk_info_provider_step2', {'jobId': job.id}) }}" class="btn btn-primary">
|
||||
<i class="fas fa-eye"></i> {% trans %}info_providers.bulk_import.view_results{% endtrans %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if job.canBeStopped %}
|
||||
<button type="button" class="btn btn-warning" onclick="stopJob({{ 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 }})">
|
||||
<i class="fas fa-trash"></i> {% trans %}info_providers.bulk_import.action.delete{% endtrans %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info" role="alert">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
{% trans %}info_providers.bulk_import.no_jobs_found{% endtrans %}
|
||||
<a href="{{ path('bulk_info_provider_step1') }}" class="alert-link">
|
||||
{% trans %}info_providers.bulk_import.create_first_job{% endtrans %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% 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,74 @@
|
|||
|
||||
{% block card_content %}
|
||||
|
||||
<!-- Show existing jobs -->
|
||||
{% if existing_jobs is not empty %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">{% trans %}info_providers.bulk_import.existing_jobs{% endtrans %}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans %}info_providers.bulk_import.job_name{% endtrans %}</th>
|
||||
<th>{% trans %}info_providers.bulk_import.parts_count{% endtrans %}</th>
|
||||
<th>{% trans %}info_providers.bulk_import.results_count{% endtrans %}</th>
|
||||
<th>{% trans %}info_providers.bulk_import.progress{% endtrans %}</th>
|
||||
<th>{% trans %}info_providers.bulk_import.status{% endtrans %}</th>
|
||||
<th>{% trans %}info_providers.bulk_import.created_at{% endtrans %}</th>
|
||||
<th>{% trans %}action.label{% endtrans %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for job in existing_jobs %}
|
||||
<tr>
|
||||
<td>{{ job.displayNameKey|trans(job.displayNameParams) }} - {{ job.formattedTimestamp }}</td>
|
||||
<td>{{ job.partCount }}</td>
|
||||
<td>{{ job.resultCount }}</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="progress me-2" style="width: 60px; height: 8px;">
|
||||
<div class="progress-bar {% if job.isCompleted %}bg-success{% else %}bg-info{% endif %}"
|
||||
role="progressbar"
|
||||
style="width: {{ job.progressPercentage }}%"
|
||||
aria-valuenow="{{ job.progressPercentage }}"
|
||||
aria-valuemin="0" aria-valuemax="100">
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-muted">{{ job.progressPercentage }}%</small>
|
||||
</div>
|
||||
<small class="text-muted">{{ job.completedPartsCount }}/{{ job.partCount }}</small>
|
||||
</td>
|
||||
<td>
|
||||
{% 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 %}
|
||||
</td>
|
||||
<td>{{ job.createdAt|date('Y-m-d H:i') }}</td>
|
||||
<td>
|
||||
{% if job.isInProgress or job.isCompleted %}
|
||||
<a href="{{ path('bulk_info_provider_step2', {'jobId': job.id}) }}" class="btn btn-primary btn-sm">
|
||||
<i class="fas fa-eye"></i> {% trans %}info_providers.bulk_import.view_results{% endtrans %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="alert alert-info" role="alert">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
{% trans %}info_providers.bulk_import.step1.global_mapping_description{% endtrans %}
|
||||
|
|
@ -70,7 +138,7 @@
|
|||
<tr>
|
||||
<th>{% trans %}info_providers.bulk_search.search_field{% endtrans %}</th>
|
||||
<th>{% trans %}info_providers.bulk_search.providers{% endtrans %}</th>
|
||||
<th width="100">{% trans %}action.label{% endtrans %}</th>
|
||||
<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') }}">
|
||||
|
|
@ -95,7 +163,18 @@
|
|||
</div>
|
||||
|
||||
<div class="mb-2 d-flex flex-column align-items-start gap-2">
|
||||
<a class="mb-2" href="{{ path('info_providers_list') }}">{% trans %}info_providers.search.info_providers_list{% endtrans %}</a>
|
||||
<div class="mb-2">
|
||||
<a href="{{ path('info_providers_list') }}">{% trans %}info_providers.search.info_providers_list{% endtrans %}</a>
|
||||
|
|
||||
<a href="{{ path('bulk_info_provider_manage') }}">{% trans %}info_providers.bulk_import.manage_jobs{% endtrans %}</a>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-2">
|
||||
{{ form_widget(form.prefetch_details, {'attr': {'class': 'form-check-input'}}) }}
|
||||
{{ form_label(form.prefetch_details, null, {'label_attr': {'class': 'form-check-label'}}) }}
|
||||
{{ form_help(form.prefetch_details) }}
|
||||
</div>
|
||||
|
||||
{{ form_widget(form.submit) }}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
348
templates/info_providers/bulk_import/step2.html.twig
Normal file
348
templates/info_providers/bulk_import/step2.html.twig
Normal file
|
|
@ -0,0 +1,348 @@
|
|||
{% 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 %}
|
||||
|
|
@ -4,6 +4,32 @@
|
|||
{% trans with {'%name%': part.name|escape } %}part.edit.title{% endtrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block before_card %}
|
||||
{% if bulk_job and jobId %}
|
||||
<div class="alert alert-info mb-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex align-items-center">
|
||||
<a href="{{ path('bulk_info_provider_step2', {jobId: bulk_job.id}) }}" class="btn btn-outline-primary btn-sm me-2">
|
||||
<i class="fas fa-arrow-left fa-fw" aria-hidden="true"></i>
|
||||
{% trans %}info_providers.bulk_import.back{% endtrans %}
|
||||
</a>
|
||||
<form method="post" action="{{ path('part_bulk_import_complete', {id: part.id, jobId: bulk_job.id}) }}" style="display: inline;">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('bulk_complete_' ~ part.id) }}">
|
||||
<button type="submit" class="btn btn-primary btn-sm me-3">
|
||||
<i class="fas fa-check fa-fw" aria-hidden="true"></i>
|
||||
{% trans %}info_providers.bulk_import.complete{% endtrans %}
|
||||
</button>
|
||||
</form>
|
||||
<div>
|
||||
<i class="fas fa-cloud-download fa-fw" aria-hidden="true"></i>
|
||||
{% trans %}info_providers.bulk_import.editing_part{% endtrans %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block card_title %}
|
||||
<i class="fas fa-edit fa-fw" aria-hidden="true"></i>
|
||||
{% trans with {'%name%': part.name|escape } %}part.edit.card_title{% endtrans %}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,19 @@
|
|||
{% block card_border %}border-info{% endblock %}
|
||||
{% block card_type %}bg-info text-bg-info{% endblock %}
|
||||
|
||||
{% block before_card %}
|
||||
{% if bulk_job and jobId %}
|
||||
<div class="alert alert-info mb-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<i class="fas fa-cloud-download fa-fw" aria-hidden="true"></i>
|
||||
{% trans %}info_providers.bulk_import.editing_part{% endtrans %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
{% trans %}info_providers.update_part.title{% endtrans %}: {{ merge_old_name }}
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -8944,6 +8944,12 @@ Element 1 -> Element 1.2]]></target>
|
|||
<target>Edit part</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk099" name="part_list.action.scrollable_hint">
|
||||
<segment state="translated">
|
||||
<source>part_list.action.scrollable_hint</source>
|
||||
<target>Scroll to see all actions</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="w3XOUoO" name="part_list.action.action.title">
|
||||
<segment state="translated">
|
||||
<source>part_list.action.action.title</source>
|
||||
|
|
@ -9334,6 +9340,84 @@ Element 1 -> Element 1.2]]></target>
|
|||
<target>Attachment name</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="BlkJob01" name="filter.bulk_import_job.label">
|
||||
<segment state="translated">
|
||||
<source>filter.bulk_import_job.label</source>
|
||||
<target>Bulk Import Job</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="BlkJob02" name="filter.bulk_import_job.job_status">
|
||||
<segment state="translated">
|
||||
<source>filter.bulk_import_job.job_status</source>
|
||||
<target>Job Status</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="BlkJob03" name="filter.bulk_import_job.part_status_in_job">
|
||||
<segment state="translated">
|
||||
<source>filter.bulk_import_job.part_status_in_job</source>
|
||||
<target>Part Status in Job</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="BlkJob04" name="filter.bulk_import_job.status.any">
|
||||
<segment state="translated">
|
||||
<source>filter.bulk_import_job.status.any</source>
|
||||
<target>Any Status</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="BlkJob05" name="filter.bulk_import_job.status.pending">
|
||||
<segment state="translated">
|
||||
<source>filter.bulk_import_job.status.pending</source>
|
||||
<target>Pending</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="BlkJob06" name="filter.bulk_import_job.status.in_progress">
|
||||
<segment state="translated">
|
||||
<source>filter.bulk_import_job.status.in_progress</source>
|
||||
<target>In Progress</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="BlkJob07" name="filter.bulk_import_job.status.completed">
|
||||
<segment state="translated">
|
||||
<source>filter.bulk_import_job.status.completed</source>
|
||||
<target>Completed</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="BlkJob08" name="filter.bulk_import_job.status.stopped">
|
||||
<segment state="translated">
|
||||
<source>filter.bulk_import_job.status.stopped</source>
|
||||
<target>Stopped</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="BlkJob09" name="filter.bulk_import_job.status.failed">
|
||||
<segment state="translated">
|
||||
<source>filter.bulk_import_job.status.failed</source>
|
||||
<target>Failed</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="BlkJob10" name="filter.bulk_import_job.part_status.any">
|
||||
<segment state="translated">
|
||||
<source>filter.bulk_import_job.part_status.any</source>
|
||||
<target>Any Part Status</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="BlkJob11" name="filter.bulk_import_job.part_status.pending">
|
||||
<segment state="translated">
|
||||
<source>filter.bulk_import_job.part_status.pending</source>
|
||||
<target>Pending</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="BlkJob12" name="filter.bulk_import_job.part_status.completed">
|
||||
<segment state="translated">
|
||||
<source>filter.bulk_import_job.part_status.completed</source>
|
||||
<target>Completed</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="BlkJob13" name="filter.bulk_import_job.part_status.skipped">
|
||||
<segment state="translated">
|
||||
<source>filter.bulk_import_job.part_status.skipped</source>
|
||||
<target>Skipped</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="YcIyWEZ" name="filter.choice_constraint.operator.ANY">
|
||||
<segment state="translated">
|
||||
<source>filter.choice_constraint.operator.ANY</source>
|
||||
|
|
@ -13153,6 +13237,12 @@ Please note, that you can not impersonate a disabled user. If you try you will g
|
|||
<target>Info Providers</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk016" name="info_providers.bulk_import.actions.label">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.actions.label</source>
|
||||
<target>Actions</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk016" name="info_providers.bulk_search.providers.help">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_search.providers.help</source>
|
||||
|
|
@ -13165,6 +13255,12 @@ Please note, that you can not impersonate a disabled user. If you try you will g
|
|||
<target>Search All Parts</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk017a" name="info_providers.bulk_search.field.select">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_search.field.select</source>
|
||||
<target>Select a field to search by</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk018" name="info_providers.bulk_search.field.mpn">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_search.field.mpn</source>
|
||||
|
|
@ -13207,5 +13303,503 @@ Please note, that you can not impersonate a disabled user. If you try you will g
|
|||
<target>SPN (Supplier Part Number) is recommended for better results. Add a mapping for each supplier to use their SPNs.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk025" name="info_providers.bulk_import.update_part">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.update_part</source>
|
||||
<target>Update Part</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk026" name="info_providers.bulk_import.prefetch_details">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.prefetch_details</source>
|
||||
<target>Prefetch Details</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk027" name="info_providers.bulk_import.prefetch_details_help">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.prefetch_details_help</source>
|
||||
<target>Prefetch details for all results. This will take longer, but will speed up workflow for updating parts.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk028a" name="info_providers.bulk_import.step2.title">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.step2.title</source>
|
||||
<target>Bulk import from info providers</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk028b" name="info_providers.bulk_import.step2.card_title">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.step2.card_title</source>
|
||||
<target>Bulk import for %count% parts - %date%</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk029" name="info_providers.bulk_import.parts">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.parts</source>
|
||||
<target>parts</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk030" name="info_providers.bulk_import.results">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.results</source>
|
||||
<target>results</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk031" name="info_providers.bulk_import.created_at">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.created_at</source>
|
||||
<target>Created at</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk032" name="info_providers.bulk_import.status.in_progress">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.status.in_progress</source>
|
||||
<target>In Progress</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk033" name="info_providers.bulk_import.status.completed">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.status.completed</source>
|
||||
<target>Completed</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk034" name="info_providers.bulk_import.status.failed">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.status.failed</source>
|
||||
<target>Failed</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk035" name="info_providers.bulk_import.results_found">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.results_found</source>
|
||||
<target>%count% results found</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk036" name="info_providers.bulk_import.table.name">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.table.name</source>
|
||||
<target>Name</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk037" name="info_providers.bulk_import.table.description">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.table.description</source>
|
||||
<target>Description</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk038" name="info_providers.bulk_import.table.manufacturer">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.table.manufacturer</source>
|
||||
<target>Manufacturer</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk039" name="info_providers.bulk_import.table.provider">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.table.provider</source>
|
||||
<target>Provider</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk040" name="info_providers.bulk_import.table.source_field">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.table.source_field</source>
|
||||
<target>Source Field</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk041" name="info_providers.bulk_import.table.action">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.table.action</source>
|
||||
<target>Action</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk042" name="info_providers.bulk_import.action.select">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.action.select</source>
|
||||
<target>Select</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk043" name="info_providers.bulk_import.action.deselect">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.action.deselect</source>
|
||||
<target>Deselect</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk044" name="info_providers.bulk_import.action.view_details">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.action.view_details</source>
|
||||
<target>View Details</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk045" name="info_providers.bulk_import.no_results">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.no_results</source>
|
||||
<target>No results found</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk046" name="info_providers.bulk_import.processing">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.processing</source>
|
||||
<target>Processing...</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk047" name="info_providers.bulk_import.error">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.error</source>
|
||||
<target>Error occurred during import</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk048" name="info_providers.bulk_import.success">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.success</source>
|
||||
<target>Import completed successfully</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk049" name="info_providers.bulk_import.partial_success">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.partial_success</source>
|
||||
<target>Import completed with some errors</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk050" name="info_providers.bulk_import.retry">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.retry</source>
|
||||
<target>Retry</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk051" name="info_providers.bulk_import.cancel">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.cancel</source>
|
||||
<target>Cancel</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk052" name="info_providers.bulk_import.confirm">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.confirm</source>
|
||||
<target>Confirm Import</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk053" name="info_providers.bulk_import.back">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.back</source>
|
||||
<target>Back</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk054" name="info_providers.bulk_import.next">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.next</source>
|
||||
<target>Next</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk055" name="info_providers.bulk_import.finish">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.finish</source>
|
||||
<target>Finish</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk056" name="info_providers.bulk_import.progress">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.progress</source>
|
||||
<target>Progress:</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk057" name="info_providers.bulk_import.time_remaining">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.time_remaining</source>
|
||||
<target>Estimated time remaining: %time%</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk058" name="info_providers.bulk_import.details_modal.title">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.details_modal.title</source>
|
||||
<target>Part Details</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk059" name="info_providers.bulk_import.details_modal.close">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.details_modal.close</source>
|
||||
<target>Close</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk060" name="info_providers.bulk_import.details_modal.select_this_part">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.details_modal.select_this_part</source>
|
||||
<target>Select This Part</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk061" name="info_providers.bulk_import.status.pending">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.status.pending</source>
|
||||
<target>Pending</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk062" name="info_providers.bulk_import.completed">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.completed</source>
|
||||
<target>completed</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk063" name="info_providers.bulk_import.skipped">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.skipped</source>
|
||||
<target>skipped</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk064" name="info_providers.bulk_import.errors">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.errors</source>
|
||||
<target>errors</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk065" name="info_providers.bulk_import.mark_completed">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.mark_completed</source>
|
||||
<target>Mark Completed</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk066" name="info_providers.bulk_import.mark_skipped">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.mark_skipped</source>
|
||||
<target>Mark Skipped</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk067" name="info_providers.bulk_import.mark_pending">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.mark_pending</source>
|
||||
<target>Mark Pending</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk068" name="info_providers.bulk_import.skip_reason">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.skip_reason</source>
|
||||
<target>Skip reason</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk070" name="info_providers.bulk_import.source_field">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.source_field</source>
|
||||
<target>Source Field</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk071" name="info_providers.bulk_import.update_part">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.update_part</source>
|
||||
<target>Update Part</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk072" name="info_providers.bulk_import.view_existing">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.view_existing</source>
|
||||
<target>View Existing</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk073" name="info_providers.search.no_results">
|
||||
<segment state="translated">
|
||||
<source>info_providers.search.no_results</source>
|
||||
<target>No results found</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk074" name="info_providers.table.provider.label">
|
||||
<segment state="translated">
|
||||
<source>info_providers.table.provider.label</source>
|
||||
<target>Provider</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk075" name="info_providers.bulk_import.editing_part">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.editing_part</source>
|
||||
<target>Editing part as part of bulk import</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk076" name="info_providers.bulk_import.complete">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.complete</source>
|
||||
<target>Complete</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk077" name="info_providers.bulk_import.existing_jobs">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.existing_jobs</source>
|
||||
<target>Existing Jobs</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk078" name="info_providers.bulk_import.job_name">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.job_name</source>
|
||||
<target>Job Name</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk079" name="info_providers.bulk_import.parts_count">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.parts_count</source>
|
||||
<target>Parts Count</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk080" name="info_providers.bulk_import.results_count">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.results_count</source>
|
||||
<target>Results Count</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk081" name="info_providers.bulk_import.progress_label">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.progress_label</source>
|
||||
<target>Progress: %current%/%total%</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk082" name="info_providers.bulk_import.manage_jobs">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.manage_jobs</source>
|
||||
<target>Manage Bulk Import Jobs</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk083" name="info_providers.bulk_import.view_results">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.view_results</source>
|
||||
<target>View Results</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk084" name="info_providers.bulk_import.status">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.status</source>
|
||||
<target>Status</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk085" name="info_providers.bulk_import.manage_jobs_description">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.manage_jobs_description</source>
|
||||
<target>View and manage all your bulk import jobs. To create a new job, select parts and click "Bulk import from info providers".</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk087" name="info_providers.bulk_import.no_jobs_found">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.no_jobs_found</source>
|
||||
<target>No bulk import jobs found.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk088" name="info_providers.bulk_import.create_first_job">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.create_first_job</source>
|
||||
<target>Create your first bulk import job</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk089" name="info_providers.bulk_import.confirm_delete_job">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.confirm_delete_job</source>
|
||||
<target>Are you sure you want to delete this job?</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk090" name="info_providers.bulk_import.job_name_template">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.job_name_template</source>
|
||||
<target>Bulk import for %count% parts</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk091" name="info_providers.bulk_import.step2.instructions.title">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.step2.instructions.title</source>
|
||||
<target>How to use bulk import</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk092" name="info_providers.bulk_import.step2.instructions.description">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.step2.instructions.description</source>
|
||||
<target>Follow these steps to efficiently update your parts:</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk093" name="info_providers.bulk_import.step2.instructions.step1">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.step2.instructions.step1</source>
|
||||
<target>Click "Update Part" to edit a part with the supplier data</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk094" name="info_providers.bulk_import.step2.instructions.step2">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.step2.instructions.step2</source>
|
||||
<target>Review and modify the part information as needed. Note: You need to click "Save" twice to save the changes.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk095" name="info_providers.bulk_import.step2.instructions.step3">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.step2.instructions.step3</source>
|
||||
<target>Click "Complete" to mark the part as done and return to this overview</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk096" name="info_providers.bulk_import.created_by">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.created_by</source>
|
||||
<target>Created By</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk097" name="info_providers.bulk_import.completed_at">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.completed_at</source>
|
||||
<target>Completed At</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk098" name="info_providers.bulk_import.action.label">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.action.label</source>
|
||||
<target>Action</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk099" name="info_providers.bulk_import.action.delete">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.action.delete</source>
|
||||
<target>Delete</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk100" name="info_providers.bulk_import.status.active">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.status.active</source>
|
||||
<target>Active</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk101" name="info_providers.bulk_import.progress.title">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.progress.title</source>
|
||||
<target>Progress</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk102" name="info_providers.bulk_import.progress.completed_text">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.progress.completed_text</source>
|
||||
<target>%completed% / %total% completed</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk103" name="info_providers.bulk_import.error.deleting_job">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.error.deleting_job</source>
|
||||
<target>Error deleting job</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk104" name="info_providers.bulk_import.error.unknown">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.error.unknown</source>
|
||||
<target>Unknown error</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk105" name="info_providers.bulk_import.error.deleting_job_with_details">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.error.deleting_job_with_details</source>
|
||||
<target>Error deleting job: %error%</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk106" name="info_providers.bulk_import.status.stopped">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.status.stopped</source>
|
||||
<target>Stopped</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk107" name="info_providers.bulk_import.action.stop">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.action.stop</source>
|
||||
<target>Stop</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="bulk108" name="info_providers.bulk_import.confirm_stop_job">
|
||||
<segment state="translated">
|
||||
<source>info_providers.bulk_import.confirm_stop_job</source>
|
||||
<target>Are you sure you want to stop this job?</target>
|
||||
</segment>
|
||||
</unit>
|
||||
</file>
|
||||
</xliff>
|
||||
Loading…
Add table
Add a link
Reference in a new issue