Skip to content

Commit

Permalink
price list CSV import/export (#3713)
Browse files Browse the repository at this point in the history
  • Loading branch information
grossmannmartin authored Jan 22, 2025
2 parents 213cf86 + 5b399a4 commit e7bab7c
Show file tree
Hide file tree
Showing 33 changed files with 1,074 additions and 20 deletions.
51 changes: 51 additions & 0 deletions assets/js/admin/components/ImportPriceList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import Ajax from '../../common/utils/Ajax';
import Register from '../../common/utils/Register';

export default class ImportPriceList {

constructor ($container) {
const $selectListField = $container.filterAllNodes('.js-import-price-list-select-list');
$selectListField.on('change', (event) => this.onSelectPriceList(event));

if ($selectListField.val() !== '') {
const $domainIdField = $('.js-import-price-list-domain-id').parents('.form-line');
$domainIdField.hide();
}
}

onSelectPriceList (event) {
const priceListId = event.target.value;
const $domainIdField = $('.js-import-price-list-domain-id').parents('.form-line');

if (priceListId === '') {
$domainIdField.show();
$('.js-import-price-list-name').val('');
$('.js-import-price-list-valid-from').val('');
$('.js-import-price-list-valid-to').val('');

return;
}

$domainIdField.hide();

let loadMetadataUrl = $(event.target).data('load-metadata-url');
loadMetadataUrl = loadMetadataUrl.replace(/\/0\b/, `/${priceListId}`);

Ajax.ajax({
url: loadMetadataUrl,
method: 'POST',
success: function (data) {
$('.js-import-price-list-name').val(data.name);
$('.js-import-price-list-valid-from').val(data.validFrom);
$('.js-import-price-list-valid-to').val(data.validTo);
}
});
}

static init ($container) {
// eslint-disable-next-line no-new
new ImportPriceList($container);
}
}

(new Register()).registerCallback(ImportPriceList.init, 'ImportPriceList.init');
1 change: 1 addition & 0 deletions assets/js/admin/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,4 @@ import './Statistics';
import './SymfonyToolbarSupport';
import './ToggleMenu';
import './TransportPriceWithWeightLimitCollection';
import './ImportPriceList';
8 changes: 8 additions & 0 deletions assets/js/admin/validation/form/validationPriceList.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ export default function priceListValidator ($container) {
}
}
});

window.$('form[name="import_price_list_form"]').jsFormValidator({
callbacks: {
checkDateValidity: function () {
// JS validation is not necessary
}
}
});
}

(new Register()).registerCallback(priceListValidator, 'priceListValidator');
2 changes: 1 addition & 1 deletion assets/styles/admin/components/table/grid.less
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
}

&--actions {
width: 90px;
width: 130px;
padding-left: 17px;
padding-right: 17px;

Expand Down
4 changes: 0 additions & 4 deletions assets/styles/admin/todo.less
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,6 @@
width: 335px;
}

.administrator_customer_list .table-grid__cell--actions {
width: 130px;
}

.form-bg-color {
background-color: @color-f;
}
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"symfony/proxy-manager-bridge": "^6.4",
"symfony/rate-limiter": "^6.4",
"symfony/security-bundle": "^6.4",
"symfony/serializer": "^6.4",
"symfony/service-contracts": "^2.5.2",
"symfony-cmf/routing": "^3.0.3",
"symfony-cmf/routing-bundle": "^3.0.3",
Expand Down
2 changes: 2 additions & 0 deletions src/Component/FlashMessage/FlashMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ class FlashMessage
public const KEY_INFO = 'info';

public const KEY_SUCCESS = 'success';

public const KEY_WARNING = 'warning';
}
27 changes: 22 additions & 5 deletions src/Component/FlashMessage/FlashMessageTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,14 @@ public function addSuccessFlash(string $message): void
$this->addFlashMessage(FlashMessage::KEY_SUCCESS, $message);
}

/**
* @param string $message
*/
public function addWarningFlash(string $message): void
{
$this->addFlashMessage(FlashMessage::KEY_WARNING, $message);
}

/**
* @param string $type
* @param string $message
Expand Down Expand Up @@ -98,36 +106,45 @@ public function isFlashMessageBagEmpty(): bool

return !$flashBag->has(FlashMessage::KEY_ERROR)
&& !$flashBag->has(FlashMessage::KEY_INFO)
&& !$flashBag->has(FlashMessage::KEY_SUCCESS);
&& !$flashBag->has(FlashMessage::KEY_SUCCESS)
&& !$flashBag->has(FlashMessage::KEY_WARNING);
}

/**
* @return array
* @return string[]
*/
public function getErrorMessages()
{
return $this->getMessages(FlashMessage::KEY_ERROR);
}

/**
* @return array
* @return string[]
*/
public function getInfoMessages()
{
return $this->getMessages(FlashMessage::KEY_INFO);
}

/**
* @return array
* @return string[]
*/
public function getSuccessMessages()
{
return $this->getMessages(FlashMessage::KEY_SUCCESS);
}

/**
* @return string[]
*/
public function getWarningMessages(): array
{
return $this->getMessages(FlashMessage::KEY_WARNING);
}

/**
* @param string $key
* @return array
* @return string[]
*/
protected function getMessages($key)
{
Expand Down
33 changes: 33 additions & 0 deletions src/Component/HttpFoundation/CsvResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace Shopsys\FrameworkBundle\Component\HttpFoundation;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Serializer\Encoder\CsvEncoder;

class CsvResponse extends Response
{
/**
* @param array $data
* @param string $fileName
* @param array|null $csvHeaders
*/
public function __construct(array $data, string $fileName, ?array $csvHeaders = null)
{
$csvEncoder = new CsvEncoder();
$context = [];

if ($csvHeaders === null) {
$context = [CsvEncoder::HEADERS_KEY => $csvHeaders];
}

$content = $csvEncoder->encode($data, CsvEncoder::FORMAT, $context);

parent::__construct($content);

$this->headers->set('Content-Type', 'text/csv');
$this->headers->set('Content-Disposition', 'attachment; filename="' . $fileName . '"');
}
}
1 change: 1 addition & 0 deletions src/Controller/Admin/FlashMessageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public function indexAction()
'errorMessages' => $this->getErrorMessages(),
'infoMessages' => $this->getInfoMessages(),
'successMessages' => $this->getSuccessMessages(),
'warningMessages' => $this->getWarningMessages(),
]);
}
}
145 changes: 145 additions & 0 deletions src/Controller/Admin/PriceListController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,19 @@

use Shopsys\FrameworkBundle\Component\Domain\AdminDomainFilterTabsFacade;
use Shopsys\FrameworkBundle\Component\Domain\Domain;
use Shopsys\FrameworkBundle\Component\HttpFoundation\CsvResponse;
use Shopsys\FrameworkBundle\Component\Localization\DisplayTimeZoneProviderInterface;
use Shopsys\FrameworkBundle\Component\Router\Security\Annotation\CsrfProtection;
use Shopsys\FrameworkBundle\Component\String\TransformString;
use Shopsys\FrameworkBundle\Form\Admin\PriceList\ImportPriceListFormType;
use Shopsys\FrameworkBundle\Form\Admin\PriceList\PriceListFormType;
use Shopsys\FrameworkBundle\Model\AdminNavigation\BreadcrumbOverrider;
use Shopsys\FrameworkBundle\Model\PriceList\Exception\PriceListNotFoundException;
use Shopsys\FrameworkBundle\Model\PriceList\PriceListCsvColumnsEnum;
use Shopsys\FrameworkBundle\Model\PriceList\PriceListDataFactory;
use Shopsys\FrameworkBundle\Model\PriceList\PriceListFacade;
use Shopsys\FrameworkBundle\Model\PriceList\PriceListGridFactory;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
Expand All @@ -29,6 +35,8 @@ class PriceListController extends AdminBaseController
* @param \Shopsys\FrameworkBundle\Component\Domain\AdminDomainFilterTabsFacade $adminDomainFilterTabsFacade
* @param \Shopsys\FrameworkBundle\Component\Domain\Domain $domain
* @param \Shopsys\FrameworkBundle\Model\AdminNavigation\BreadcrumbOverrider $breadcrumbOverrider
* @param \Shopsys\FrameworkBundle\Model\PriceList\PriceListCsvColumnsEnum $priceListCsvColumnsEnum
* @param \Shopsys\FrameworkBundle\Component\Localization\DisplayTimeZoneProviderInterface $displayTimeZoneProvider
*/
public function __construct(
protected readonly PriceListGridFactory $priceListGridFactory,
Expand All @@ -37,6 +45,8 @@ public function __construct(
protected readonly AdminDomainFilterTabsFacade $adminDomainFilterTabsFacade,
protected readonly Domain $domain,
protected readonly BreadcrumbOverrider $breadcrumbOverrider,
protected readonly PriceListCsvColumnsEnum $priceListCsvColumnsEnum,
protected readonly DisplayTimeZoneProviderInterface $displayTimeZoneProvider,
) {
}

Expand Down Expand Up @@ -174,4 +184,139 @@ public function deleteAction(int $id): RedirectResponse

return $this->redirectToRoute('admin_pricelist_list');
}

/**
* @param int $id
* @return \Symfony\Component\HttpFoundation\Response
*/
#[Route(path: '/pricing/price-list/export/{id}', requirements: ['id' => '\d+'])]
public function exportAction(int $id): Response
{
try {
$priceList = $this->priceListFacade->getById($id);
$sanitizedPriceListName = TransformString::safeFilename($priceList->getName());

$priceListDataToExport = $this->priceListFacade->getPriceListDataToExport($id);

return new CsvResponse(
$priceListDataToExport,
'price_list_' . $id . '_' . $sanitizedPriceListName . '.csv',
$this->priceListCsvColumnsEnum->getAllCases(),
);
} catch (PriceListNotFoundException) {
$this->addErrorFlash(t('Selected price list does not exist.'));

return $this->redirectToRoute('admin_pricelist_list');
}
}

/**
* @param \Symfony\Component\HttpFoundation\Request $request
* @return \Symfony\Component\HttpFoundation\Response
*/
#[Route(path: '/pricing/price-list/import')]
public function importAction(Request $request): Response
{
$form = $this->createForm(ImportPriceListFormType::class);
$form->handleRequest($request);

if ($form->isSubmitted() && $form->isValid()) {
$data = $form->getData();

if ($data['selectPriceList'] === null) {
$priceListData = $this->priceListDataFactory->create();
$priceListData->domainId = $data['domainId'];
} else {
$priceListData = $this->priceListDataFactory->createFromPriceList($data['selectPriceList']);
}
$priceListData->name = $data['name'];
$priceListData->validFrom = $data['validFrom'];
$priceListData->validTo = $data['validTo'];

$importResult = $this->priceListFacade->importPriceList(
$priceListData,
$data['csvFile'],
);

if ($importResult->hasErrors()) {
$this->addErrorFlash(t('Error while importing CSV file.'));

foreach ($importResult->getErrors() as $error) {
$this->addErrorFlash($error);
}

return $this->render('@ShopsysFramework/Admin/Content/PriceList/import.html.twig', [
'form' => $form->createView(),
]);
}

$this->addSuccessFlash(t(
'%count% item from CSV file was imported successfully.|%count% items from CSV file were imported successfully.',
['%count%' => $importResult->getImportedCount()],
));

if ($data['selectPriceList'] === null) {
$this->addSuccessFlash(t(
'New price list <strong><a href="{{ url }}">{{ name }}</a></strong> was created.',
[
'{{ name }}' => $importResult->getPriceListName(),
'{{ url }}' => $this->generateUrl('admin_pricelist_edit', ['id' => $importResult->getPriceListId()]),
],
));
} else {
$this->addSuccessFlash(t(
'Price list <strong><a href="{{ url }}">{{ name }}</a></strong> was replaced.',
[
'{{ name }}' => $importResult->getPriceListName(),
'{{ url }}' => $this->generateUrl('admin_pricelist_edit', ['id' => $importResult->getPriceListId()]),
],
));
}

if ($importResult->hasWarnings()) {
$this->addWarningFlash(t('Some items cannot be imported due to errors:'));

foreach ($importResult->getWarnings() as $warning) {
$this->addWarningFlash($warning);
}
}

return $this->redirectToRoute('admin_pricelist_list');
}

if ($form->isSubmitted() && !$form->isValid()) {
$this->addErrorFlash(t('Error while importing CSV file.'));
}

return $this->render('@ShopsysFramework/Admin/Content/PriceList/import.html.twig', [
'form' => $form->createView(),
]);
}

/**
* @param int $id
* @return \Symfony\Component\HttpFoundation\Response
*/
#[Route(path: '/pricing/price-list/loadMetadata/{id}', requirements: ['id' => '\d+'], condition: 'request.isXmlHttpRequest()')]
public function loadMetadataAction(int $id): Response
{
try {
$priceList = $this->priceListFacade->getById($id);

$validFrom = $priceList->getValidFrom()->setTimezone($this->displayTimeZoneProvider->getDisplayTimeZoneForAdmin());
$validTo = $priceList->getValidTo()->setTimezone($this->displayTimeZoneProvider->getDisplayTimeZoneForAdmin());

return new JsonResponse([
'result' => 'valid',
'name' => $priceList->getName(),
'validFrom' => $validFrom->format('d.m.Y H:i:s'),
'validTo' => $validTo->format('d.m.Y H:i:s'),
]);
} catch (PriceListNotFoundException) {
return new JsonResponse([
'result' => 'invalid',
'errors' => 'Price list not found',
]);
}
}
}
Loading

0 comments on commit e7bab7c

Please sign in to comment.