diff --git a/src/Component/Image/ImageFacade.php b/src/Component/Image/ImageFacade.php
index 8862b194e5..bd7bb9a653 100644
--- a/src/Component/Image/ImageFacade.php
+++ b/src/Component/Image/ImageFacade.php
@@ -240,6 +240,15 @@ function () use ($image, $domainConfig) {
. $this->imageLocator->getRelativeImageFilepathWithSlug($image, $friendlyUrlSeoEntityName);
}
+ /**
+ * @param \Shopsys\FrameworkBundle\Component\Domain\Config\DomainConfig $domainConfig
+ * @return string
+ */
+ public function getEmptyImageUrl(DomainConfig $domainConfig): string
+ {
+ return $this->cdnFacade->resolveDomainUrlForAssets($domainConfig) . '/public/frontend/images/noimage.png';
+ }
+
/**
* @param object $imageOrEntity
* @param string|null $type
diff --git a/src/Component/Money/HiddenMoney.php b/src/Component/Money/HiddenMoney.php
new file mode 100644
index 0000000000..2db55d0ac5
--- /dev/null
+++ b/src/Component/Money/HiddenMoney.php
@@ -0,0 +1,158 @@
+createForm(QuickSearchFormType::class, new QuickSearchFormData());
+ $quickSearchForm->handleRequest($request);
+
+ $queryBuilder = $this->inquiryFacade->getInquiryListQueryBuilderByQuickSearchData(
+ $quickSearchForm->getData(),
+ $this->localization->getAdminLocale(),
+ );
+
+ $selectedDomainId = $this->adminDomainFilterTabsFacade->getSelectedDomainId($domainFilterNamespace);
+
+ if ($selectedDomainId !== null) {
+ $queryBuilder
+ ->andWhere('i.domainId = :selectedDomainId')
+ ->setParameter('selectedDomainId', $selectedDomainId);
+ }
+
+ return $this->render('@ShopsysFramework/Admin/Content/Inquiry/list.html.twig', [
+ 'gridView' => $this->inquiryGridFactory->createView($queryBuilder, $this->getCurrentAdministrator()),
+ 'domainFilterNamespace' => $domainFilterNamespace,
+ 'quickSearchForm' => $quickSearchForm->createView(),
+ ]);
+ }
+
+ /**
+ * @param int $id
+ * @return \Symfony\Component\HttpFoundation\Response
+ */
+ #[Route(path: '/inquiry/detail/{id}', requirements: ['id' => '\d+'])]
+ public function detailAction(int $id): Response
+ {
+ $inquiry = $this->inquiryFacade->getById($id);
+
+ return $this->render('@ShopsysFramework/Admin/Content/Inquiry/detail.html.twig', [
+ 'inquiry' => $inquiry,
+ ]);
+ }
+}
diff --git a/src/Form/Admin/Product/ProductFormType.php b/src/Form/Admin/Product/ProductFormType.php
index 92270b4ca4..134e33261c 100644
--- a/src/Form/Admin/Product/ProductFormType.php
+++ b/src/Form/Admin/Product/ProductFormType.php
@@ -36,6 +36,7 @@
use Shopsys\FrameworkBundle\Model\Product\Product;
use Shopsys\FrameworkBundle\Model\Product\ProductData;
use Shopsys\FrameworkBundle\Model\Product\ProductFacade;
+use Shopsys\FrameworkBundle\Model\Product\ProductTypeEnum;
use Shopsys\FrameworkBundle\Model\Product\Unit\UnitFacade;
use Shopsys\FrameworkBundle\Model\Seo\SeoSettingFacade;
use Shopsys\FrameworkBundle\Model\Transport\TransportFacade;
@@ -69,6 +70,7 @@ class ProductFormType extends AbstractType
* @param \Shopsys\FrameworkBundle\Model\Product\ProductFacade $productFacade
* @param \Shopsys\FrameworkBundle\Model\Transport\TransportFacade $transportFacade
* @param \Symfony\Component\Routing\Generator\UrlGeneratorInterface $urlGenerator
+ * @param \Shopsys\FrameworkBundle\Model\Product\ProductTypeEnum $productTypeEnum
*/
public function __construct(
private readonly BrandFacade $brandFacade,
@@ -83,6 +85,7 @@ public function __construct(
private readonly ProductFacade $productFacade,
private readonly TransportFacade $transportFacade,
private readonly UrlGeneratorInterface $urlGenerator,
+ private readonly ProductTypeEnum $productTypeEnum,
) {
}
@@ -167,6 +170,14 @@ private function createBasicInformationGroup(
'label' => t('Basic information'),
]);
+ if (!$this->isProductMainVariant($product)) {
+ $builderBasicInformationGroup->add('productType', ChoiceType::class, [
+ 'required' => true,
+ 'choices' => $this->productTypeEnum->getAllIndexedByTranslations(),
+ 'label' => t('Product type'),
+ ]);
+ }
+
$builderBasicInformationGroup->add('catnum', TextType::class, [
'required' => true,
'constraints' => [
@@ -747,7 +758,7 @@ private function createParametersGroup(FormBuilderInterface $builder): FormBuild
'error_bubbling' => false,
'render_form_row' => false,
])
- ->addModelTransformer($this->productParameterValueToProductParameterValuesLocalizedTransformer));
+ ->addModelTransformer($this->productParameterValueToProductParameterValuesLocalizedTransformer));
return $builderParametersGroup;
}
diff --git a/src/Migrations/Version20240924082209.php b/src/Migrations/Version20240924082209.php
new file mode 100644
index 0000000000..4106f16ec9
--- /dev/null
+++ b/src/Migrations/Version20240924082209.php
@@ -0,0 +1,28 @@
+sql('ALTER TABLE products ADD product_type VARCHAR(32) DEFAULT NULL');
+ $this->sql('UPDATE products SET product_type = \'basic\'');
+ $this->sql('ALTER TABLE products ALTER product_type SET NOT NULL');
+ }
+
+ /**
+ * @param \Doctrine\DBAL\Schema\Schema $schema
+ */
+ public function down(Schema $schema): void
+ {
+ }
+}
diff --git a/src/Migrations/Version20240926162253.php b/src/Migrations/Version20240926162253.php
new file mode 100644
index 0000000000..f8b6c46e5b
--- /dev/null
+++ b/src/Migrations/Version20240926162253.php
@@ -0,0 +1,62 @@
+sql('
+ CREATE TABLE inquiries (
+ id SERIAL NOT NULL,
+ domain_id INT NOT NULL,
+ product_id INT DEFAULT NULL,
+ product_catnum VARCHAR(100) NOT NULL,
+ first_name VARCHAR(100) NOT NULL,
+ last_name VARCHAR(100) NOT NULL,
+ email VARCHAR(255) NOT NULL,
+ telephone VARCHAR(30) NOT NULL,
+ company_name VARCHAR(100) DEFAULT NULL,
+ company_number VARCHAR(50) DEFAULT NULL,
+ company_tax_number VARCHAR(50) DEFAULT NULL,
+ note TEXT DEFAULT NULL,
+ created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
+ customer_user_id INT DEFAULT NULL,
+ PRIMARY KEY(id)
+ )');
+ $this->sql('CREATE INDEX IDX_1CCE4D54584665A ON inquiries (product_id)');
+ $this->sql('
+ ALTER TABLE
+ inquiries
+ ADD
+ CONSTRAINT FK_1CCE4D54584665A FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE
+ SET
+ NULL NOT DEFERRABLE INITIALLY IMMEDIATE');
+
+ $this->sql('COMMENT ON COLUMN inquiries.created_at IS \'(DC2Type:datetime_immutable)\'');
+
+ $this->sql('
+ ALTER TABLE
+ inquiries
+ ADD
+ CONSTRAINT FK_1CCE4D5BBB3772B FOREIGN KEY (customer_user_id) REFERENCES customer_users (id) ON DELETE
+ SET
+ NULL NOT DEFERRABLE INITIALLY IMMEDIATE');
+ $this->sql('CREATE INDEX IDX_1CCE4D5BBB3772B ON inquiries (customer_user_id)');
+ }
+
+ /**
+ * @param \Doctrine\DBAL\Schema\Schema $schema
+ */
+ public function down(Schema $schema): void
+ {
+ }
+}
diff --git a/src/Migrations/Version20241001113136.php b/src/Migrations/Version20241001113136.php
new file mode 100644
index 0000000000..b66326c74e
--- /dev/null
+++ b/src/Migrations/Version20241001113136.php
@@ -0,0 +1,100 @@
+createMailTemplateIfNotExist(InquiryMail::CUSTOMER_MAIL_TEMPLATE_NAME);
+ $this->createMailTemplateIfNotExist(InquiryMail::ADMIN_MAIL_TEMPLATE_NAME);
+
+ foreach ($this->getAllDomainIds() as $domainId) {
+ $domainLocale = $this->getDomainLocale($domainId);
+
+ $this->updateMailTemplate(
+ InquiryMail::CUSTOMER_MAIL_TEMPLATE_NAME,
+ t('Thank you for your product inquiry', [], Translator::DATA_FIXTURES_TRANSLATION_DOMAIN, $domainLocale),
+ t('
Dear {fullName},
Thank you for your interest in {productName}.
We have received your inquiry and will be reaching out to you shortly with further information.
If you need immediate assistance or have additional questions, feel free to reply to this email or contact us.
Best regards
', [], Translator::DATA_FIXTURES_TRANSLATION_DOMAIN, $domainLocale),
+ $domainId,
+ );
+
+ $this->updateMailTemplate(
+ InquiryMail::ADMIN_MAIL_TEMPLATE_NAME,
+ t('New product inquiry received', [], Translator::DATA_FIXTURES_TRANSLATION_DOMAIN, $domainLocale),
+ t('A new product inquiry has just been received from {fullName}.
Please find the details below:
- Customer name: {fullName}
- Customer email: {email}
- Customer telephone: {telephone}
- Company name: {companyName}
- Company number: {companyNumber}
- Company tax number: {companyTaxNumber}
Note from customer:
{note}
Inquired product:
{productName} (Catalog number: {productCatnum})
Please review the inquiry and take the necessary steps to follow up.
Review Inquiry
+', [], Translator::DATA_FIXTURES_TRANSLATION_DOMAIN, $domainLocale),
+ $domainId,
+ );
+ }
+ }
+
+ /**
+ * @param string $mailTemplateName
+ */
+ private function createMailTemplateIfNotExist(
+ string $mailTemplateName,
+ ): void {
+ foreach ($this->getAllDomainIds() as $domainId) {
+ $mailTemplateCount = $this->sql(
+ 'SELECT count(*) FROM mail_templates WHERE name = :mailTemplateName and domain_id = :domainId',
+ [
+ 'mailTemplateName' => $mailTemplateName,
+ 'domainId' => $domainId,
+ ],
+ )->fetchOne();
+
+ if ($mailTemplateCount !== 0) {
+ continue;
+ }
+
+ $this->sql(
+ 'INSERT INTO mail_templates (name, domain_id, send_mail) VALUES (:mailTemplateName, :domainId, :sendMail)',
+ [
+ 'mailTemplateName' => $mailTemplateName,
+ 'domainId' => $domainId,
+ 'sendMail' => true,
+ ],
+ );
+ }
+ }
+
+ /**
+ * @param string $mailTemplateName
+ * @param string $subject
+ * @param string $body
+ * @param int $domainId
+ */
+ private function updateMailTemplate(string $mailTemplateName, string $subject, string $body, int $domainId): void
+ {
+ $this->sql(
+ 'UPDATE mail_templates SET subject = :subject, body = :body WHERE name = :mailTemplateName AND domain_id = :domainId',
+ [
+ 'subject' => $subject,
+ 'body' => $body,
+ 'mailTemplateName' => $mailTemplateName,
+ 'domainId' => $domainId,
+ ],
+ );
+ }
+
+ /**
+ * @param \Doctrine\DBAL\Schema\Schema $schema
+ */
+ public function down(Schema $schema): void
+ {
+ }
+}
diff --git a/src/Model/AdminNavigation/ConfigureMenuEvent.php b/src/Model/AdminNavigation/ConfigureMenuEvent.php
index 9a4e153816..450e450a9d 100644
--- a/src/Model/AdminNavigation/ConfigureMenuEvent.php
+++ b/src/Model/AdminNavigation/ConfigureMenuEvent.php
@@ -13,6 +13,7 @@ class ConfigureMenuEvent extends Event
public const SIDE_MENU_ROOT = 'shopsys.admin_side_menu.configure_root';
public const SIDE_MENU_DASHBOARD = 'shopsys.admin_side_menu.configure_dashboard';
public const SIDE_MENU_ORDERS = 'shopsys.admin_side_menu.configure_orders';
+ public const SIDE_MENU_INQUIRIES = 'shopsys.admin_side_menu.configure_inquiries';
public const SIDE_MENU_CUSTOMERS = 'shopsys.admin_side_menu.configure_customers';
public const SIDE_MENU_PRODUCTS = 'shopsys.admin_side_menu.configure_products';
public const SIDE_MENU_PRICING = 'shopsys.admin_side_menu.configure_pricing';
diff --git a/src/Model/AdminNavigation/SideMenuBuilder.php b/src/Model/AdminNavigation/SideMenuBuilder.php
index 8ac8b87be0..32f5e3acf9 100644
--- a/src/Model/AdminNavigation/SideMenuBuilder.php
+++ b/src/Model/AdminNavigation/SideMenuBuilder.php
@@ -36,6 +36,7 @@ public function createMenu(): ItemInterface
$menu->addChild($this->createDashboardMenu());
$menu->addChild($this->createOrdersMenu());
+ $menu->addChild($this->createInquiriesMenu());
$menu->addChild($this->createCustomersMenu());
$menu->addChild($this->createProductsMenu());
$menu->addChild($this->createPricingMenu());
@@ -101,6 +102,28 @@ protected function createOrdersMenu(): ItemInterface
return $menu;
}
+ /**
+ * @return \Knp\Menu\ItemInterface
+ */
+ protected function createInquiriesMenu(): ItemInterface
+ {
+ $menu = $this->menuFactory->createItem('inquiries', [
+ 'route' => 'admin_inquiry_list',
+ 'label' => t('Inquiries'),
+ ]);
+ $menu->setExtra('icon', 'letter');
+
+ $menu->addChild('detail', [
+ 'route' => 'admin_inquiry_detail',
+ 'label' => t('Inquiry detail'),
+ 'display' => false,
+ ]);
+
+ $this->dispatchConfigureMenuEvent(ConfigureMenuEvent::SIDE_MENU_INQUIRIES, $menu);
+
+ return $menu;
+ }
+
/**
* @return \Knp\Menu\ItemInterface
*/
diff --git a/src/Model/Cart/Watcher/CartWatcher.php b/src/Model/Cart/Watcher/CartWatcher.php
index c5491f57b3..570e6de969 100644
--- a/src/Model/Cart/Watcher/CartWatcher.php
+++ b/src/Model/Cart/Watcher/CartWatcher.php
@@ -10,6 +10,7 @@
use Shopsys\FrameworkBundle\Model\Customer\User\CurrentCustomerUser;
use Shopsys\FrameworkBundle\Model\Product\Exception\ProductNotFoundException;
use Shopsys\FrameworkBundle\Model\Product\Pricing\ProductPriceCalculationForCustomerUser;
+use Shopsys\FrameworkBundle\Model\Product\ProductTypeEnum;
use Shopsys\FrameworkBundle\Model\Product\ProductVisibilityFacade;
class CartWatcher
@@ -60,6 +61,13 @@ public function getNotListableItems(Cart $cart, CurrentCustomerUser $currentCust
foreach ($cart->getItems() as $item) {
try {
$product = $item->getProduct();
+
+ if ($product->getProductType() === ProductTypeEnum::TYPE_INQUIRY) {
+ $notListableItems[] = $item;
+
+ continue;
+ }
+
$productVisibility = $this->productVisibilityFacade
->getProductVisibility(
$product,
diff --git a/src/Model/Inquiry/Exception/InquiryNotFoundException.php b/src/Model/Inquiry/Exception/InquiryNotFoundException.php
new file mode 100644
index 0000000000..a4a1d0c12c
--- /dev/null
+++ b/src/Model/Inquiry/Exception/InquiryNotFoundException.php
@@ -0,0 +1,11 @@
+createdAt = $inquiryData->createdAt ?? new DateTimeImmutable();
+ $this->domainId = $inquiryData->domainId;
+
+ $this->setData($inquiryData);
+ }
+
+ /**
+ * @param \Shopsys\FrameworkBundle\Model\Inquiry\InquiryData $inquiryData
+ */
+ protected function setData(InquiryData $inquiryData): void
+ {
+ if ($inquiryData->product === null && $inquiryData->productCatnum === null) {
+ throw new LogicException('Either product or productCatnum must be set to properly create an inquiry.');
+ }
+
+ if ($inquiryData->product !== null && $inquiryData->productCatnum !== null) {
+ throw new LogicException('Only one of product or productCatnum can be set to properly create an inquiry.');
+ }
+
+ $this->firstName = $inquiryData->firstName;
+ $this->lastName = $inquiryData->lastName;
+ $this->email = $inquiryData->email;
+ $this->telephone = $inquiryData->telephone;
+ $this->companyName = $inquiryData->companyName;
+ $this->companyNumber = $inquiryData->companyNumber;
+ $this->companyTaxNumber = $inquiryData->companyTaxNumber;
+ $this->note = $inquiryData->note;
+ $this->product = $inquiryData->product;
+ $this->productCatnum = $inquiryData->product ? $inquiryData->product->getCatnum() : $inquiryData->productCatnum;
+ $this->customerUser = $inquiryData->customerUser;
+ }
+
+ /**
+ * @return int
+ */
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ /**
+ * @return string
+ */
+ public function getFirstName()
+ {
+ return $this->firstName;
+ }
+
+ /**
+ * @return string
+ */
+ public function getLastName()
+ {
+ return $this->lastName;
+ }
+
+ /**
+ * @return string
+ */
+ public function getFullName(): string
+ {
+ return $this->firstName . ' ' . $this->lastName;
+ }
+
+ /**
+ * @return int
+ */
+ public function getDomainId()
+ {
+ return $this->domainId;
+ }
+
+ /**
+ * @return string
+ */
+ public function getEmail()
+ {
+ return $this->email;
+ }
+
+ /**
+ * @return string
+ */
+ public function getTelephone()
+ {
+ return $this->telephone;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getCompanyName()
+ {
+ return $this->companyName;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getCompanyNumber()
+ {
+ return $this->companyNumber;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getCompanyTaxNumber()
+ {
+ return $this->companyTaxNumber;
+ }
+
+ /**
+ * @return \DateTimeImmutable
+ */
+ public function getCreatedAt()
+ {
+ return $this->createdAt;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getNote()
+ {
+ return $this->note;
+ }
+
+ /**
+ * @return \Shopsys\FrameworkBundle\Model\Product\Product|null
+ */
+ public function getProduct()
+ {
+ return $this->product;
+ }
+
+ /**
+ * @return string
+ */
+ public function getProductCatnum()
+ {
+ return $this->productCatnum;
+ }
+
+ /**
+ * @return \Shopsys\FrameworkBundle\Model\Customer\User\CustomerUser|null
+ */
+ public function getCustomerUser()
+ {
+ return $this->customerUser;
+ }
+}
diff --git a/src/Model/Inquiry/InquiryData.php b/src/Model/Inquiry/InquiryData.php
new file mode 100644
index 0000000000..968a28065b
--- /dev/null
+++ b/src/Model/Inquiry/InquiryData.php
@@ -0,0 +1,73 @@
+createInstance();
+ $inquiryData->domainId = $domainId;
+
+ return $inquiryData;
+ }
+}
diff --git a/src/Model/Inquiry/InquiryFacade.php b/src/Model/Inquiry/InquiryFacade.php
new file mode 100644
index 0000000000..3f6a935b68
--- /dev/null
+++ b/src/Model/Inquiry/InquiryFacade.php
@@ -0,0 +1,83 @@
+inquiryRepository->getById($id);
+ }
+
+ /**
+ * @param \Shopsys\FrameworkBundle\Model\Inquiry\InquiryData $inquiryData
+ * @return \Shopsys\FrameworkBundle\Model\Inquiry\Inquiry
+ */
+ public function create(InquiryData $inquiryData): Inquiry
+ {
+ $inquiry = $this->inquiryFactory->create($inquiryData);
+
+ $this->em->persist($inquiry);
+ $this->em->flush();
+
+ return $inquiry;
+ }
+
+ /**
+ * @param \Shopsys\FrameworkBundle\Form\Admin\QuickSearch\QuickSearchFormData $quickSearchData
+ * @param string $locale
+ * @return \Doctrine\ORM\QueryBuilder
+ */
+ public function getInquiryListQueryBuilderByQuickSearchData(
+ QuickSearchFormData $quickSearchData,
+ string $locale,
+ ): QueryBuilder {
+ $queryBuilder = $this->inquiryRepository->getInquiriesQueryBuilder($locale);
+
+ if ($quickSearchData->text !== null && $quickSearchData->text !== '') {
+ $queryBuilder
+ ->andWhere('(
+ NORMALIZED(i.productCatnum) LIKE NORMALIZED(:text)
+ OR
+ NORMALIZED(p.catnum) LIKE NORMALIZED(:text)
+ OR
+ NORMALIZED(pt.name) LIKE NORMALIZED(:text)
+ OR
+ i.companyNumber LIKE :text
+ OR
+ NORMALIZED(i.lastName) LIKE NORMALIZED(:text)
+ OR
+ NORMALIZED(i.companyName) LIKE NORMALIZED(:text)
+ OR
+ NORMALIZED(i.email) LIKE NORMALIZED(:text)
+ )');
+ $querySearchText = DatabaseSearching::getFullTextLikeSearchString($quickSearchData->text);
+ $queryBuilder->setParameter('text', $querySearchText);
+ }
+
+ return $queryBuilder;
+ }
+}
diff --git a/src/Model/Inquiry/InquiryFactory.php b/src/Model/Inquiry/InquiryFactory.php
new file mode 100644
index 0000000000..eaa48df7a3
--- /dev/null
+++ b/src/Model/Inquiry/InquiryFactory.php
@@ -0,0 +1,29 @@
+entityNameResolver->resolve(Inquiry::class);
+
+ return new $entityClassName($inquiryData);
+ }
+}
diff --git a/src/Model/Inquiry/InquiryGridFactory.php b/src/Model/Inquiry/InquiryGridFactory.php
new file mode 100644
index 0000000000..40599f9d97
--- /dev/null
+++ b/src/Model/Inquiry/InquiryGridFactory.php
@@ -0,0 +1,58 @@
+gridFactory->create('inquiryList', $dataSource);
+
+ $grid->enablePaging();
+ $grid->setDefaultOrder('createdAt', DataSourceInterface::ORDER_DESC);
+
+ $grid->addColumn('productName', 'productName', t('Product name'), true);
+ $grid->addColumn('fullName', 'fullName', t('Full name'), true);
+ $grid->addColumn('email', 'i.email', t('Email'), true);
+ $grid->addColumn('telephone', 'i.telephone', t('Phone'));
+ $grid->addColumn('company', 'company', t('Company (Company number)'), true);
+ $grid->addColumn('createdAt', 'i.createdAt', t('Created'), true);
+
+ $grid->addActionColumn('file-all', t('Show detail'), 'admin_inquiry_detail', ['id' => 'i.id']);
+
+ $grid->setTheme('@ShopsysFramework/Admin/Content/Inquiry/listGrid.html.twig');
+
+ $this->administratorGridFacade->restoreAndRememberGridLimit($administrator, $grid);
+
+ return $grid->createView();
+ }
+}
diff --git a/src/Model/Inquiry/InquiryRepository.php b/src/Model/Inquiry/InquiryRepository.php
new file mode 100644
index 0000000000..41a558629a
--- /dev/null
+++ b/src/Model/Inquiry/InquiryRepository.php
@@ -0,0 +1,63 @@
+em->getRepository(Inquiry::class);
+ }
+
+ /**
+ * @param int $id
+ * @return \Shopsys\FrameworkBundle\Model\Inquiry\Inquiry
+ */
+ public function getById(int $id): Inquiry
+ {
+ $inquiry = $this->getInquiryRepository()->find($id);
+
+ if ($inquiry === null) {
+ throw new InquiryNotFoundException();
+ }
+
+ return $inquiry;
+ }
+
+ /**
+ * @param string $locale
+ * @return \Doctrine\ORM\QueryBuilder
+ */
+ public function getInquiriesQueryBuilder(string $locale): QueryBuilder
+ {
+ return $this->getInquiryRepository()
+ ->createQueryBuilder('i')
+ ->addSelect('IDENTITY(i.product) as productId')
+ ->addSelect('pt.name as productName')
+ ->addSelect('CONCAT(i.lastName, \' \', i.firstName) as fullName')
+ ->addSelect('CONCAT(i.companyName, \' (\', i.companyNumber, \')\') as company')
+ ->leftJoin('i.product', 'p')
+ ->leftJoin('p.translations', 'pt', Join::WITH, 'pt.locale = :locale')
+ ->setParameter('locale', $locale)
+ ->orderBy('i.createdAt', 'DESC');
+ }
+}
diff --git a/src/Model/Inquiry/Mail/InquiryMail.php b/src/Model/Inquiry/Mail/InquiryMail.php
new file mode 100644
index 0000000000..39b9b4c44d
--- /dev/null
+++ b/src/Model/Inquiry/Mail/InquiryMail.php
@@ -0,0 +1,191 @@
+setting->getForDomain(MailSetting::MAIN_ADMIN_MAIL, $inquiry->getDomainId()),
+ $template->getBccEmail(),
+ $template->getBody(),
+ $template->getSubject(),
+ $this->setting->getForDomain(MailSetting::MAIN_ADMIN_MAIL, $inquiry->getDomainId()),
+ $this->setting->getForDomain(MailSetting::MAIN_ADMIN_MAIL_NAME, $inquiry->getDomainId()),
+ $this->getBodyVariablesReplacementsForAdmin($inquiry),
+ $this->getSubjectVariablesReplacements($inquiry),
+ );
+ }
+
+ /**
+ * @param \Shopsys\FrameworkBundle\Model\Mail\MailTemplate $template
+ * @param \Shopsys\FrameworkBundle\Model\Inquiry\Inquiry $inquiry
+ * @return \Shopsys\FrameworkBundle\Model\Mail\MessageData
+ */
+ public function createMessageForCustomer(MailTemplate $template, Inquiry $inquiry): MessageData
+ {
+ return new MessageData(
+ $inquiry->getEmail(),
+ $template->getBccEmail(),
+ $template->getBody(),
+ $template->getSubject(),
+ $this->setting->getForDomain(MailSetting::MAIN_ADMIN_MAIL, $inquiry->getDomainId()),
+ $this->setting->getForDomain(MailSetting::MAIN_ADMIN_MAIL_NAME, $inquiry->getDomainId()),
+ $this->getBodyVariablesReplacements($inquiry),
+ $this->getSubjectVariablesReplacements($inquiry),
+ );
+ }
+
+ /**
+ * @param \Shopsys\FrameworkBundle\Model\Inquiry\Inquiry $inquiry
+ * @return array
+ */
+ protected function getSubjectVariablesReplacements(Inquiry $inquiry): array
+ {
+ return [
+ self::VARIABLE_FULL_NAME => htmlspecialchars($inquiry->getFullName(), ENT_QUOTES),
+ self::VARIABLE_EMAIL => htmlspecialchars($inquiry->getEmail(), ENT_QUOTES),
+ self::VARIABLE_TELEPHONE => htmlspecialchars($inquiry->getTelephone(), ENT_QUOTES),
+ self::VARIABLE_COMPANY_NAME => $this->escapeOptionalString($inquiry->getCompanyName()),
+ self::VARIABLE_COMPANY_NUMBER => $this->escapeOptionalString($inquiry->getCompanyNumber()),
+ self::VARIABLE_COMPANY_TAX_NUMBER => $this->escapeOptionalString($inquiry->getCompanyTaxNumber()),
+ self::VARIABLE_PRODUCT_NAME => $this->escapeOptionalString($inquiry->getProduct()?->getName()),
+ self::VARIABLE_PRODUCT_CATALOG_NUMBER => htmlspecialchars($inquiry->getProductCatnum(), ENT_QUOTES),
+ ];
+ }
+
+ /**
+ * @param \Shopsys\FrameworkBundle\Model\Inquiry\Inquiry $inquiry
+ * @return array
+ */
+ protected function getBodyVariablesReplacements(Inquiry $inquiry): array
+ {
+ return [
+ ...$this->getSubjectVariablesReplacements($inquiry),
+ self::VARIABLE_NOTE => $this->escapeOptionalString($inquiry->getNote()),
+ self::VARIABLE_PRODUCT_URL => $this->getProductUrl($inquiry),
+ self::VARIABLE_PRODUCT_IMAGE => $this->getProductImageUrl($inquiry),
+ ];
+ }
+
+ /**
+ * @param \Shopsys\FrameworkBundle\Model\Inquiry\Inquiry $inquiry
+ * @return array
+ */
+ protected function getBodyVariablesReplacementsForAdmin(Inquiry $inquiry): array
+ {
+ return [
+ ...$this->getBodyVariablesReplacements($inquiry),
+ self::VARIABLE_ADMIN_INQUIRY_DETAIL_URL => $this->domainRouterFactory->getRouter(Domain::MAIN_ADMIN_DOMAIN_ID)->generate(
+ 'admin_inquiry_detail',
+ ['id' => $inquiry->getId()],
+ UrlGeneratorInterface::ABSOLUTE_URL,
+ ),
+ ];
+ }
+
+ /**
+ * @param string|null $string
+ * @return string
+ */
+ protected function escapeOptionalString(?string $string): string
+ {
+ if ($string === null) {
+ return '-';
+ }
+
+ return htmlspecialchars($string, ENT_QUOTES);
+ }
+
+ /**
+ * @param \Shopsys\FrameworkBundle\Model\Inquiry\Inquiry $inquiry
+ * @return string
+ */
+ protected function getProductUrl(Inquiry $inquiry): string
+ {
+ if ($inquiry->getProduct() === null) {
+ return '';
+ }
+
+ return $this->domainRouterFactory->getRouter($inquiry->getDomainId())->generate(
+ 'front_product_detail',
+ ['id' => $inquiry->getProduct()->getId()],
+ UrlGeneratorInterface::ABSOLUTE_URL,
+ );
+ }
+
+ /**
+ * @param \Shopsys\FrameworkBundle\Model\Inquiry\Inquiry $inquiry
+ * @return string
+ */
+ protected function getProductImageUrl(Inquiry $inquiry): string
+ {
+ $domainConfig = $this->domain->getDomainConfigById($inquiry->getDomainId());
+
+ if ($inquiry->getProduct() === null) {
+ return $this->imageFacade->getEmptyImageUrl($domainConfig);
+ }
+
+ try {
+ $imageUrl = $this->imageFacade->getImageUrl(
+ $domainConfig,
+ $inquiry->getProduct(),
+ );
+
+ return $imageUrl . '?width=100';
+ } catch (ImageNotFoundException) {
+ return $this->imageFacade->getEmptyImageUrl($domainConfig);
+ }
+ }
+}
diff --git a/src/Model/Inquiry/Mail/InquiryMailFacade.php b/src/Model/Inquiry/Mail/InquiryMailFacade.php
new file mode 100644
index 0000000000..550dbca3bb
--- /dev/null
+++ b/src/Model/Inquiry/Mail/InquiryMailFacade.php
@@ -0,0 +1,43 @@
+mailTemplateFacade->get(InquiryMail::ADMIN_MAIL_TEMPLATE_NAME, $inquiry->getDomainId());
+ $messageData = $this->inquiryMail->createMessageForAdmin($mailTemplate, $inquiry);
+ $messageData->attachments = $this->uploadedFileFacade->getUploadedFilesByEntity($mailTemplate);
+ $this->mailer->sendForDomain($messageData, $inquiry->getDomainId());
+
+ $mailTemplate = $this->mailTemplateFacade->get(InquiryMail::CUSTOMER_MAIL_TEMPLATE_NAME, $inquiry->getDomainId());
+ $messageData = $this->inquiryMail->createMessageForCustomer($mailTemplate, $inquiry);
+ $messageData->attachments = $this->uploadedFileFacade->getUploadedFilesByEntity($mailTemplate);
+ $this->mailer->sendForDomain($messageData, $inquiry->getDomainId());
+ }
+}
diff --git a/src/Model/Inquiry/Mail/InquiryMailTemplateVariablesProvider.php b/src/Model/Inquiry/Mail/InquiryMailTemplateVariablesProvider.php
new file mode 100644
index 0000000000..dc096ace0d
--- /dev/null
+++ b/src/Model/Inquiry/Mail/InquiryMailTemplateVariablesProvider.php
@@ -0,0 +1,91 @@
+addVariable(
+ InquiryMail::VARIABLE_FULL_NAME,
+ t('Customer name'),
+ );
+
+ $mailTemplateVariables->addVariable(
+ InquiryMail::VARIABLE_EMAIL,
+ t('Customer email address'),
+ );
+
+ $mailTemplateVariables->addVariable(
+ InquiryMail::VARIABLE_TELEPHONE,
+ t('Customer phone number'),
+ );
+
+ $mailTemplateVariables->addVariable(
+ InquiryMail::VARIABLE_COMPANY_NAME,
+ t('Company name'),
+ );
+
+ $mailTemplateVariables->addVariable(
+ InquiryMail::VARIABLE_COMPANY_NUMBER,
+ t('Company number'),
+ );
+
+ $mailTemplateVariables->addVariable(
+ InquiryMail::VARIABLE_COMPANY_TAX_NUMBER,
+ t('Company Tax number'),
+ );
+
+ $mailTemplateVariables->addVariable(
+ InquiryMail::VARIABLE_NOTE,
+ t('Note'),
+ MailTemplateVariables::CONTEXT_BODY,
+ );
+
+ $mailTemplateVariables->addVariable(
+ InquiryMail::VARIABLE_PRODUCT_NAME,
+ t('Product name'),
+ );
+
+ $mailTemplateVariables->addVariable(
+ InquiryMail::VARIABLE_PRODUCT_CATALOG_NUMBER,
+ t('Product catnum'),
+ );
+
+ $mailTemplateVariables->addVariable(
+ InquiryMail::VARIABLE_PRODUCT_URL,
+ t('Product detail URL'),
+ MailTemplateVariables::CONTEXT_BODY,
+ );
+
+ $mailTemplateVariables->addVariable(
+ InquiryMail::VARIABLE_PRODUCT_IMAGE,
+ t('Product image URL'),
+ MailTemplateVariables::CONTEXT_BODY,
+ );
+
+ if ($mailTemplateType === InquiryMail::ADMIN_MAIL_TEMPLATE_NAME) {
+ $mailTemplateVariables->addVariable(
+ InquiryMail::VARIABLE_ADMIN_INQUIRY_DETAIL_URL,
+ t('Admin inquiry detail URL'),
+ MailTemplateVariables::CONTEXT_BODY,
+ );
+ }
+
+ return $mailTemplateVariables;
+ }
+}
diff --git a/src/Model/Mail/MailTemplateConfiguration.php b/src/Model/Mail/MailTemplateConfiguration.php
index 22167b38da..74f20af4bd 100644
--- a/src/Model/Mail/MailTemplateConfiguration.php
+++ b/src/Model/Mail/MailTemplateConfiguration.php
@@ -10,6 +10,8 @@
use Shopsys\FrameworkBundle\Model\Customer\Mail\CustomerActivationMail;
use Shopsys\FrameworkBundle\Model\Customer\Mail\RegistrationMail;
use Shopsys\FrameworkBundle\Model\Customer\Mail\ResetPasswordMail;
+use Shopsys\FrameworkBundle\Model\Inquiry\Mail\InquiryMail;
+use Shopsys\FrameworkBundle\Model\Inquiry\Mail\InquiryMailTemplateVariablesProvider;
use Shopsys\FrameworkBundle\Model\Mail\Exception\InvalidMailTemplateVariablesConfigurationException;
use Shopsys\FrameworkBundle\Model\Mail\Exception\MailTemplateNotFoundException;
use Shopsys\FrameworkBundle\Model\Order\Mail\OrderMail;
@@ -34,16 +36,19 @@ class MailTemplateConfiguration
/**
* @param \Shopsys\FrameworkBundle\Model\Order\Status\OrderStatusFacade $orderStatusFacade
* @param \Shopsys\FrameworkBundle\Model\Complaint\Status\ComplaintStatusFacade $complaintStatusFacade
+ * @param \Shopsys\FrameworkBundle\Model\Inquiry\Mail\InquiryMailTemplateVariablesProvider $inquiryMailTemplateVariablesProvider
*/
public function __construct(
protected readonly OrderStatusFacade $orderStatusFacade,
protected readonly ComplaintStatusFacade $complaintStatusFacade,
+ protected readonly InquiryMailTemplateVariablesProvider $inquiryMailTemplateVariablesProvider,
) {
$this->registerStaticMailTemplates();
$this->registerOrderStatusMailTemplates();
$this->registerComplaintStatusMailTemplates();
$this->registerTwoFactorAuthenticationCodeMailTemplate();
$this->registerCustomerActivationMailTemplate();
+ $this->registerInquiryMailTemplates();
}
/**
@@ -323,4 +328,13 @@ protected function registerCustomerActivationMailTemplate(): void
$this->addMailTemplateVariables(CustomerActivationMail::CUSTOMER_ACTIVATION_NAME, $mailTemplateVariables);
}
+
+ protected function registerInquiryMailTemplates(): void
+ {
+ $inquiryMailTemplateVariables = $this->inquiryMailTemplateVariablesProvider->create(InquiryMail::CUSTOMER_MAIL_TEMPLATE_NAME);
+ $this->addMailTemplateVariables(InquiryMail::CUSTOMER_MAIL_TEMPLATE_NAME, $inquiryMailTemplateVariables);
+
+ $inquiryMailTemplateVariables = $this->inquiryMailTemplateVariablesProvider->create(InquiryMail::ADMIN_MAIL_TEMPLATE_NAME);
+ $this->addMailTemplateVariables(InquiryMail::ADMIN_MAIL_TEMPLATE_NAME, $inquiryMailTemplateVariables);
+ }
}
diff --git a/src/Model/Pricing/Price.php b/src/Model/Pricing/Price.php
index 65c6655c27..485d0e457b 100644
--- a/src/Model/Pricing/Price.php
+++ b/src/Model/Pricing/Price.php
@@ -4,6 +4,7 @@
namespace Shopsys\FrameworkBundle\Model\Pricing;
+use Shopsys\FrameworkBundle\Component\Money\HiddenMoney;
use Shopsys\FrameworkBundle\Component\Money\Money;
class Price
@@ -112,4 +113,15 @@ public function isZero(): bool
{
return $this->priceWithoutVat->isZero() && $this->priceWithVat->isZero();
}
+
+ /**
+ * @return self
+ */
+ public static function createHiddenPrice(): self
+ {
+ return new self(
+ new HiddenMoney(),
+ new HiddenMoney(),
+ );
+ }
}
diff --git a/src/Model/Product/Elasticsearch/ProductExportRepository.php b/src/Model/Product/Elasticsearch/ProductExportRepository.php
index d3da67583e..efe2d477d1 100644
--- a/src/Model/Product/Elasticsearch/ProductExportRepository.php
+++ b/src/Model/Product/Elasticsearch/ProductExportRepository.php
@@ -23,6 +23,7 @@
use Shopsys\FrameworkBundle\Model\Product\Product;
use Shopsys\FrameworkBundle\Model\Product\ProductFacade;
use Shopsys\FrameworkBundle\Model\Product\ProductRepository;
+use Shopsys\FrameworkBundle\Model\Product\ProductTypeEnum;
use Shopsys\FrameworkBundle\Model\Product\ProductVisibility;
use Shopsys\FrameworkBundle\Model\Product\ProductVisibilityFacade;
use Shopsys\FrameworkBundle\Model\Seo\HreflangLinksFacade;
@@ -185,6 +186,9 @@ protected function getExportedFieldValue(int $domainId, Product $product, string
ProductExportFieldProvider::SEO_META_DESCRIPTION => $product->getSeoMetaDescription($domainId),
ProductExportFieldProvider::ACCESSORIES => $this->extractAccessoriesIds($product),
ProductExportFieldProvider::HREFLANG_LINKS => $this->hreflangLinksFacade->getForProduct($product, $domainId),
+ ProductExportFieldProvider::PRODUCT_TYPE => $this->extractProductType($product, $domainId),
+ ProductExportFieldProvider::PRIORITY_BY_PRODUCT_TYPE => $this->extractPriorityByProductType($product, $domainId),
+
default => throw new InvalidArgumentException(sprintf('There is no definition for exporting "%s" field to Elasticsearch', $field)),
};
}
@@ -309,6 +313,42 @@ protected function extractParameters(string $locale, Product $product): array
return $parameters;
}
+ /**
+ * @param \Shopsys\FrameworkBundle\Model\Product\Product $product
+ * @param int $domainId
+ * @return string
+ */
+ protected function extractProductType(Product $product, int $domainId): string
+ {
+ if ($product->isMainVariant()) {
+ foreach ($this->getVariantsForDefaultPricingGroup($product, $domainId) as $variant) {
+ if ($variant->getProductType() === ProductTypeEnum::TYPE_BASIC) {
+ return ProductTypeEnum::TYPE_BASIC;
+ }
+ }
+
+ return ProductTypeEnum::TYPE_INQUIRY;
+ }
+
+ return $product->getProductType();
+ }
+
+ /**
+ * @param \Shopsys\FrameworkBundle\Model\Product\Product $product
+ * @param int $domainId
+ * @return int
+ */
+ protected function extractPriorityByProductType(Product $product, int $domainId): int
+ {
+ $productType = $this->extractProductType($product, $domainId);
+
+ return match ($productType) {
+ ProductTypeEnum::TYPE_BASIC => 20,
+ ProductTypeEnum::TYPE_INQUIRY => 10,
+ default => -100,
+ };
+ }
+
/**
* @param int $domainId
* @param \Shopsys\FrameworkBundle\Model\Product\Product $product
diff --git a/src/Model/Product/Elasticsearch/Scope/ProductExportFieldProvider.php b/src/Model/Product/Elasticsearch/Scope/ProductExportFieldProvider.php
index 95cf3ca246..5c77b9efe0 100644
--- a/src/Model/Product/Elasticsearch/Scope/ProductExportFieldProvider.php
+++ b/src/Model/Product/Elasticsearch/Scope/ProductExportFieldProvider.php
@@ -43,6 +43,8 @@ class ProductExportFieldProvider
public const string SEO_META_DESCRIPTION = 'seo_meta_description';
public const string ACCESSORIES = 'accessories';
public const string HREFLANG_LINKS = 'hreflang_links';
+ public const string PRODUCT_TYPE = 'product_type';
+ public const string PRIORITY_BY_PRODUCT_TYPE = 'priority_by_product_type';
/**
* @return string[]
diff --git a/src/Model/Product/Filter/ProductFilterConfigIdsDataFactory.php b/src/Model/Product/Filter/ProductFilterConfigIdsDataFactory.php
index 506d67e9d8..d5388983d4 100644
--- a/src/Model/Product/Filter/ProductFilterConfigIdsDataFactory.php
+++ b/src/Model/Product/Filter/ProductFilterConfigIdsDataFactory.php
@@ -75,7 +75,7 @@ protected function extractFlagIds(array $aggregationResult): array
*/
protected function extractPriceRange(array $aggregationResult): PriceRange
{
- $pricesData = $aggregationResult['prices']['filter_pricing_group'];
+ $pricesData = $aggregationResult['prices']['nested_prices']['filter_pricing_group'];
$minPrice = Money::create((string)($pricesData['min_price']['value'] ?? 0));
$maxPrice = Money::create((string)($pricesData['max_price']['value'] ?? 0));
diff --git a/src/Model/Product/Pricing/ProductPrice.php b/src/Model/Product/Pricing/ProductPrice.php
index cfcbc73826..d407d2ec1a 100644
--- a/src/Model/Product/Pricing/ProductPrice.php
+++ b/src/Model/Product/Pricing/ProductPrice.php
@@ -28,4 +28,15 @@ public function isPriceFrom()
{
return $this->priceFrom;
}
+
+ /**
+ * @return self
+ */
+ public static function createHiddenProductPrice(): self
+ {
+ return new self(
+ self::createHiddenPrice(),
+ false,
+ );
+ }
}
diff --git a/src/Model/Product/Pricing/ProductPriceCalculation.php b/src/Model/Product/Pricing/ProductPriceCalculation.php
index 2b11ba25fb..9cfb36b9f7 100644
--- a/src/Model/Product/Pricing/ProductPriceCalculation.php
+++ b/src/Model/Product/Pricing/ProductPriceCalculation.php
@@ -13,6 +13,7 @@
use Shopsys\FrameworkBundle\Model\Product\Pricing\Exception\MainVariantPriceCalculationException;
use Shopsys\FrameworkBundle\Model\Product\Product;
use Shopsys\FrameworkBundle\Model\Product\ProductRepository;
+use Shopsys\FrameworkBundle\Model\Product\ProductTypeEnum;
class ProductPriceCalculation
{
@@ -61,6 +62,11 @@ protected function calculateMainVariantPrice(Product $mainVariant, $domainId, Pr
$pricingGroup,
);
+ $variants = array_filter(
+ $variants,
+ static fn (Product $variant) => $variant->getProductType() !== ProductTypeEnum::TYPE_INQUIRY,
+ );
+
if (count($variants) === 0) {
$message = 'Main variant ID = ' . $mainVariant->getId() . ' has no sellable variants.';
diff --git a/src/Model/Product/Product.php b/src/Model/Product/Product.php
index 7ea7acbd7c..8334aac13d 100644
--- a/src/Model/Product/Product.php
+++ b/src/Model/Product/Product.php
@@ -171,6 +171,12 @@ class Product extends AbstractTranslatableEntity
*/
protected $excludedTransports;
+ /**
+ * @var string
+ * @ORM\Column(type="string", length=32, nullable=false)
+ */
+ protected $productType;
+
/**
* @param \Shopsys\FrameworkBundle\Model\Product\ProductData $productData
* @param \Shopsys\FrameworkBundle\Model\Product\Product[]|null $variants
@@ -234,6 +240,7 @@ protected function setData(ProductData $productData): void
$this->brand = $productData->brand;
$this->unit = $productData->unit;
$this->weight = $productData->weight;
+ $this->productType = $productData->productType;
$this->setTranslations($productData);
$this->setExcludedTransports($productData->excludedTransports);
}
@@ -749,6 +756,14 @@ public function getSaleExclusion(int $domainId)
return $this->getProductDomain($domainId)->getSaleExclusion();
}
+ /**
+ * @return string
+ */
+ public function getProductType()
+ {
+ return $this->productType;
+ }
+
/**
* @return \Shopsys\FrameworkBundle\Model\Product\ProductTranslation
*/
diff --git a/src/Model/Product/ProductData.php b/src/Model/Product/ProductData.php
index f30a4dc657..16f182439e 100644
--- a/src/Model/Product/ProductData.php
+++ b/src/Model/Product/ProductData.php
@@ -198,6 +198,11 @@ class ProductData
*/
public $excludedTransports;
+ /**
+ * @var string|null
+ */
+ public $productType;
+
public function __construct()
{
$this->name = [];
@@ -227,5 +232,6 @@ public function __construct()
$this->saleExclusion = [];
$this->domainHidden = [];
$this->excludedTransports = [];
+ $this->productType = ProductTypeEnum::TYPE_BASIC;
}
}
diff --git a/src/Model/Product/ProductDataFactory.php b/src/Model/Product/ProductDataFactory.php
index 2de27aaf37..8aaa10980a 100644
--- a/src/Model/Product/ProductDataFactory.php
+++ b/src/Model/Product/ProductDataFactory.php
@@ -184,6 +184,7 @@ protected function fillFromProduct(ProductData $productData, Product $product):
$productData->weight = $product->getWeight();
$productData->files = $this->uploadedFileDataFactory->createByEntity($product);
$productData->excludedTransports = $product->getExcludedTransports();
+ $productData->productType = $product->getProductType();
$this->fillProductStockByProduct($productData, $product);
}
diff --git a/src/Model/Product/ProductFacade.php b/src/Model/Product/ProductFacade.php
index 44abf64864..bbc85082c3 100644
--- a/src/Model/Product/ProductFacade.php
+++ b/src/Model/Product/ProductFacade.php
@@ -149,6 +149,7 @@ public function edit(
string $priority = ProductRecalculationPriorityEnum::REGULAR,
): Product {
$product = $this->productRepository->getById($productId);
+ $originalNames = $product->getFullnames();
$productCategoryDomains = $this->productCategoryDomainFactory->createMultiple(
$product,
@@ -174,7 +175,7 @@ public function edit(
$this->uploadedFileFacade->manageFiles($product, $productData->files);
$this->friendlyUrlFacade->saveUrlListFormData('front_product_detail', $product->getId(), $productData->urls);
- $this->createFriendlyUrlsWhenRenamed($product, $product->getFullnames());
+ $this->createFriendlyUrlsWhenRenamed($product, $originalNames);
$this->pluginCrudExtensionFacade->saveAllData('product', $product->getId(), $productData->pluginData);
diff --git a/src/Model/Product/ProductRepository.php b/src/Model/Product/ProductRepository.php
index 4484ec90a6..f7e50c4ad4 100644
--- a/src/Model/Product/ProductRepository.php
+++ b/src/Model/Product/ProductRepository.php
@@ -115,6 +115,20 @@ public function getAllVisibleQueryBuilder($domainId, PricingGroup $pricingGroup)
return $queryBuilder;
}
+ /**
+ * @param int $domainId
+ * @param \Shopsys\FrameworkBundle\Model\Pricing\Group\PricingGroup $pricingGroup
+ * @return \Doctrine\ORM\QueryBuilder
+ */
+ public function getAllVisibleWithoutInquiriesQueryBuilder(int $domainId, PricingGroup $pricingGroup): QueryBuilder
+ {
+ $queryBuilder = $this->getAllVisibleQueryBuilder($domainId, $pricingGroup);
+ $queryBuilder->andWhere('p.productType != :inquiryProductType')
+ ->setParameter('inquiryProductType', ProductTypeEnum::TYPE_INQUIRY);
+
+ return $queryBuilder;
+ }
+
/**
* @param \Doctrine\ORM\QueryBuilder $queryBuilder
* @param string $locale
diff --git a/src/Model/Product/ProductSellingDeniedRecalculator.php b/src/Model/Product/ProductSellingDeniedRecalculator.php
index 3e069afa3d..ff8e4766ab 100644
--- a/src/Model/Product/ProductSellingDeniedRecalculator.php
+++ b/src/Model/Product/ProductSellingDeniedRecalculator.php
@@ -75,6 +75,8 @@ protected function calculatePerDomain(array $productIds): void
pd.domain_hidden = TRUE
OR (
p.variant_type != :variantTypeMain
+ AND
+ p.product_type != :inquiryProductType
AND
NOT EXISTS(
SELECT 1
@@ -102,6 +104,7 @@ protected function calculatePerDomain(array $productIds): void
$params = [];
$params['productIds'] = $productIds;
$params['variantTypeMain'] = Product::VARIANT_TYPE_MAIN;
+ $params['inquiryProductType'] = ProductTypeEnum::TYPE_INQUIRY;
foreach ($this->domain->getAll() as $domain) {
$params['domainId'] = $domain->getId();
@@ -112,6 +115,7 @@ protected function calculatePerDomain(array $productIds): void
[
'productIds' => ArrayParameterType::INTEGER,
'variantTypeMain' => Types::STRING,
+ 'inquiryProductType' => Types::STRING,
'domainId' => Types::INTEGER,
],
);
diff --git a/src/Model/Product/ProductTypeEnum.php b/src/Model/Product/ProductTypeEnum.php
new file mode 100644
index 0000000000..7822563b13
--- /dev/null
+++ b/src/Model/Product/ProductTypeEnum.php
@@ -0,0 +1,25 @@
+
+ */
+ public function getAllIndexedByTranslations(): array
+ {
+ return [
+ t('Basic') => self::TYPE_BASIC,
+ t('Upon inquiry') => self::TYPE_INQUIRY,
+ ];
+ }
+}
diff --git a/src/Model/Product/ProductVisibilityRepository.php b/src/Model/Product/ProductVisibilityRepository.php
index e57d47865d..8c73622a48 100644
--- a/src/Model/Product/ProductVisibilityRepository.php
+++ b/src/Model/Product/ProductVisibilityRepository.php
@@ -139,6 +139,7 @@ protected function calculateIndependentVisibility(?array $productIds)
'domainId' => null,
'pricingGroupId' => null,
'variantTypeMain' => Product::VARIANT_TYPE_MAIN,
+ 'inquiryProductType' => ProductTypeEnum::TYPE_INQUIRY,
];
$variableTypes = [
@@ -147,6 +148,7 @@ protected function calculateIndependentVisibility(?array $productIds)
'domainId' => Types::INTEGER,
'pricingGroupId' => Types::INTEGER,
'variantTypeMain' => Types::STRING,
+ 'inquiryProductType' => Types::STRING,
];
if ($productIds !== null) {
@@ -169,6 +171,8 @@ protected function calculateIndependentVisibility(?array $productIds)
(
p.variant_type = :variantTypeMain
OR
+ p.product_type = :inquiryProductType
+ OR
EXISTS (
SELECT 1
FROM product_manual_input_prices as pmip
diff --git a/src/Model/Product/Search/FilterQuery.php b/src/Model/Product/Search/FilterQuery.php
index 053ec16527..ad1fc50c0f 100644
--- a/src/Model/Product/Search/FilterQuery.php
+++ b/src/Model/Product/Search/FilterQuery.php
@@ -7,6 +7,7 @@
use Shopsys\FrameworkBundle\Component\Money\Money;
use Shopsys\FrameworkBundle\Model\Pricing\Group\PricingGroup;
use Shopsys\FrameworkBundle\Model\Product\Listing\ProductListOrderingConfig;
+use Shopsys\FrameworkBundle\Model\Product\ProductTypeEnum;
use stdClass;
class FilterQuery
@@ -74,6 +75,7 @@ public function applyOrdering(string $orderingModeId, PricingGroup $pricingGroup
];
if ($orderingModeId === ProductListOrderingConfig::ORDER_BY_PRIORITY) {
+ $clone->sorting['priority_by_product_type'] = 'desc';
$clone->sorting['ordering_priority'] = 'desc';
$clone->sorting['name.keyword'] = 'asc';
@@ -93,6 +95,15 @@ public function applyOrdering(string $orderingModeId, PricingGroup $pricingGroup
}
if ($orderingModeId === ProductListOrderingConfig::ORDER_BY_PRICE_ASC) {
+ $clone->sorting['_script'] = [
+ 'type' => 'number',
+ 'script' => [
+ 'lang' => 'painless',
+ 'source' => 'doc[\'product_type\'].value == \'inquiry\' ? 1 : 0',
+ ],
+ 'order' => 'asc',
+ ];
+
$clone->sorting['prices.price_with_vat'] = [
'order' => 'asc',
'nested' => [
@@ -111,6 +122,15 @@ public function applyOrdering(string $orderingModeId, PricingGroup $pricingGroup
}
if ($orderingModeId === ProductListOrderingConfig::ORDER_BY_PRICE_DESC) {
+ $clone->sorting['_script'] = [
+ 'type' => 'number',
+ 'script' => [
+ 'lang' => 'painless',
+ 'source' => 'doc[\'product_type\'].value == \'inquiry\' ? 1 : 0',
+ ],
+ 'order' => 'asc',
+ ];
+
$clone->sorting['prices.price_with_vat'] = [
'order' => 'desc',
'nested' => [
@@ -241,40 +261,48 @@ public function filterByPrices(
}
$clone->filters[] = [
- 'nested' => [
- 'path' => 'prices',
- 'query' => [
- 'bool' => [
- 'must' => [
- 'match_all' => new stdClass(),
- ],
- 'filter' => [
- 'bool' => [
- 'must' => [
- [
- 'range' => [
- 'prices.filtering_minimal_price' => [
- 'gte' => $priceGte,
+ 'bool' => [
+ 'should' => [
+ [
+ 'nested' => [
+ 'path' => 'prices',
+ 'query' => [
+ 'bool' => [
+ 'must' => [
+ 'match_all' => new stdClass(),
+ ],
+ 'filter' => [
+ [
+ 'range' => [
+ 'prices.filtering_minimal_price' => [
+ 'gte' => $priceGte,
+ ],
],
],
- ],
- [
- 'range' => [
- 'prices.filtering_maximal_price' => [
- 'lte' => $priceLte,
+ [
+ 'range' => [
+ 'prices.filtering_maximal_price' => [
+ 'lte' => $priceLte,
+ ],
],
],
- ],
- [
- 'term' => [
- 'prices.pricing_group_id' => $pricingGroup->getId(),
+ [
+ 'term' => [
+ 'prices.pricing_group_id' => $pricingGroup->getId(),
+ ],
],
],
],
],
],
],
+ [
+ 'term' => [
+ 'product_type' => ProductTypeEnum::TYPE_INQUIRY,
+ ],
+ ],
],
+ 'minimum_should_match' => 1,
],
];
@@ -806,31 +834,43 @@ public function getAggregationQueryForProductFilterConfig(int $pricingGroupId):
$query = $this->getAbsoluteNumbersWithParametersQuery();
$query['body']['aggs']['prices'] = [
- 'nested' => [
- 'path' => 'prices',
- ],
- 'aggs' => [
- 'filter_pricing_group' => [
- 'filter' => [
+ 'filter' => [
+ 'bool' => [
+ 'must_not' => [
'term' => [
- 'prices.pricing_group_id' => $pricingGroupId,
+ 'product_type' => ProductTypeEnum::TYPE_INQUIRY,
],
],
+ ],
+ ],
+ 'aggs' => [
+ 'nested_prices' => [
+ 'nested' => [
+ 'path' => 'prices',
+ ],
'aggs' => [
- 'min_price' => [
- 'min' => [
- 'field' => 'prices.price_with_vat',
+ 'filter_pricing_group' => [
+ 'filter' => [
+ 'term' => [
+ 'prices.pricing_group_id' => $pricingGroupId,
+ ],
],
- ],
- 'max_price' => [
- 'max' => [
- 'field' => 'prices.price_with_vat',
+ 'aggs' => [
+ 'min_price' => [
+ 'min' => [
+ 'field' => 'prices.price_with_vat',
+ ],
+ ],
+ 'max_price' => [
+ 'max' => [
+ 'field' => 'prices.price_with_vat',
+ ],
+ ],
],
],
],
],
],
-
];
return $query;
diff --git a/src/Model/Product/Search/ProductElasticsearchConverter.php b/src/Model/Product/Search/ProductElasticsearchConverter.php
index d3b5afd53c..3f0fab70fc 100644
--- a/src/Model/Product/Search/ProductElasticsearchConverter.php
+++ b/src/Model/Product/Search/ProductElasticsearchConverter.php
@@ -4,6 +4,8 @@
namespace Shopsys\FrameworkBundle\Model\Product\Search;
+use Shopsys\FrameworkBundle\Model\Product\ProductTypeEnum;
+
class ProductElasticsearchConverter
{
/**
@@ -52,6 +54,8 @@ public function fillEmptyFields(array $product): array
$result['seo_title'] = $product['seo_title'] ?? null;
$result['seo_meta_description'] = $product['seo_meta_description'] ?? null;
$result['hreflang_links'] = $product['hreflang_links'] ?? [];
+ $result['product_type'] = $product['product_type'] ?? ProductTypeEnum::TYPE_BASIC;
+ $result['priority_by_product_type'] = $product['priority_by_product_type'] ?? 0;
return $result;
}
diff --git a/src/Model/Security/MenuItemsGrantedRolesSetting.php b/src/Model/Security/MenuItemsGrantedRolesSetting.php
index 016df61f55..5f83ae37ac 100644
--- a/src/Model/Security/MenuItemsGrantedRolesSetting.php
+++ b/src/Model/Security/MenuItemsGrantedRolesSetting.php
@@ -28,6 +28,9 @@ protected function getPagesGrantedRoles(): array
'orders' => [
Roles::ROLE_ORDER_VIEW,
],
+ 'inquiries' => [
+ Roles::ROLE_INQUIRY_VIEW,
+ ],
'customers' . static::MENU_ITEM_PATH_SEPARATOR . 'customers_overview' => [
Roles::ROLE_CUSTOMER_VIEW,
],
@@ -185,6 +188,7 @@ protected function getPagesGrantedRoles(): array
protected function getSectionsGrantedRoles(array $pagesGrantedRoles): array
{
$sectionsGrantedRoles = [
+ 'inquiries' => [],
'customers' => [],
'products' => [],
'pricing' => [],
diff --git a/src/Model/Security/Roles.php b/src/Model/Security/Roles.php
index 568ec5908e..821bbe0887 100644
--- a/src/Model/Security/Roles.php
+++ b/src/Model/Security/Roles.php
@@ -166,6 +166,8 @@ class Roles
public const string ROLE_COMPLAINT_STATUS_FULL = 'ROLE_COMPLAINT_STATUS_FULL';
public const string ROLE_COMPLAINT_STATUS_VIEW = 'ROLE_COMPLAINT_STATUS_VIEW';
+ public const string ROLE_INQUIRY_VIEW = 'ROLE_INQUIRY_VIEW';
+
/**
* @return array
*/
@@ -390,6 +392,9 @@ public function getAvailableAdministratorRolesGrid(): array
static::ROLE_COMPLAINT_STATUS_FULL => t('Complaint statuses - full'),
static::ROLE_COMPLAINT_STATUS_VIEW => t('Complaint statuses - view'),
],
+ [
+ static::ROLE_INQUIRY_VIEW => t('Inquiries - view'),
+ ],
];
}
diff --git a/src/Resources/config/services.yaml b/src/Resources/config/services.yaml
index 24fe820d74..c4f5052f78 100644
--- a/src/Resources/config/services.yaml
+++ b/src/Resources/config/services.yaml
@@ -746,7 +746,6 @@ services:
Shopsys\FrameworkBundle\Model\Heureka\HeurekaShopCertificationLocaleHelper: ~
-
Shopsys\FrameworkBundle\Model\ImageSitemap\ImageSitemapDumperFactory:
arguments:
- '@event_dispatcher'
@@ -762,6 +761,8 @@ services:
tags:
- { name: 'kernel.event_subscriber', priority: 100 }
+ Shopsys\FrameworkBundle\Model\Inquiry\Mail\InquiryMail: ~
+
Shopsys\FrameworkBundle\Model\Localization\Localization:
arguments:
$adminLocale: '%shopsys.admin_locale%'
diff --git a/src/Resources/translations/dataFixtures.cs.po b/src/Resources/translations/dataFixtures.cs.po
index 77aa94cb8d..bef548350a 100644
--- a/src/Resources/translations/dataFixtures.cs.po
+++ b/src/Resources/translations/dataFixtures.cs.po
@@ -4,9 +4,21 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Language: cs\n"
+msgid "A new product inquiry has just been received from {fullName}.
Please find the details below:
- Customer name: {fullName}
- Customer email: {email}
- Customer telephone: {telephone}
- Company name: {companyName}
- Company number: {companyNumber}
- Company tax number: {companyTaxNumber}
Note from customer:
{note}
Inquired product:
{productName} (Catalog number: {productCatnum})
Please review the inquiry and take the necessary steps to follow up.
Review Inquiry
"
+msgstr "Nová poptávka produktu byla právě přijata od {fullName}.
Podrobnosti naleznete níže:
- Jméno zákazníka: {fullName}
- E-mail zákazníka: {email}
- Telefon zákazníka: {telephone}
- Název firmy: {companyName}
- IČ: {companyNumber}
- DIČ: {companyTaxNumber}
Poznámka od zákazníka:
{note}
Poptávaný produkt:
{productName} (Katalogové číslo: {productCatnum})
Prosím zkontrolujte tuto poptávku a proveďte potřebné kroky k jejímu vyřízení.
Zobrazit poptávku
"
+
+msgid "Dear {fullName},
Thank you for your interest in {productName}.
We have received your inquiry and will be reaching out to you shortly with further information.
If you need immediate assistance or have additional questions, feel free to reply to this email or contact us.
Best regards
"
+msgstr "Vážený/á {fullName},
Děkujeme za váš zájem o {productName}.
Obdrželi jsme vaši poptávku a brzy se vám ozveme s dalšími informacemi.
Pokud potřebujete okamžitou pomoc nebo máte další otázky, neváhejte odpovědět na tento email nebo nás kontaktovat.
S pozdravem
"
+
msgid " Payment for order number {number} has been successful.
Track the status of your order.
{transport_instructions}
"
msgstr " Platba pro objednávku číslo {number} proběhla úspěšně.
Stav Vaší objednávky můžete sledovat zde.
{transport_instructions}
"
msgid " Payment for order number {number} has failed.
Please contact us to resolve the issue.
"
msgstr " Platba pro objednávku číslo {number} nebyla úspěšná.
Prosím kontaktujte nás pro vyřešení problému.
"
+msgid "New product inquiry received"
+msgstr "Byla přijata nová poptávka produktu"
+
+msgid "Thank you for your product inquiry"
+msgstr "Děkujeme za vaši poptávku produktu"
+
diff --git a/src/Resources/translations/dataFixtures.en.po b/src/Resources/translations/dataFixtures.en.po
index a8f63a5d75..cef65066ff 100644
--- a/src/Resources/translations/dataFixtures.en.po
+++ b/src/Resources/translations/dataFixtures.en.po
@@ -4,9 +4,21 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Language: en\n"
+msgid "A new product inquiry has just been received from {fullName}.
Please find the details below:
- Customer name: {fullName}
- Customer email: {email}
- Customer telephone: {telephone}
- Company name: {companyName}
- Company number: {companyNumber}
- Company tax number: {companyTaxNumber}
Note from customer:
{note}
Inquired product:
{productName} (Catalog number: {productCatnum})
Please review the inquiry and take the necessary steps to follow up.
Review Inquiry
"
+msgstr ""
+
+msgid "Dear {fullName},
Thank you for your interest in {productName}.
We have received your inquiry and will be reaching out to you shortly with further information.
If you need immediate assistance or have additional questions, feel free to reply to this email or contact us.
Best regards
"
+msgstr ""
+
msgid " Payment for order number {number} has been successful.
Track the status of your order.
{transport_instructions}
"
msgstr ""
msgid " Payment for order number {number} has failed.
Please contact us to resolve the issue.
"
msgstr ""
+msgid "New product inquiry received"
+msgstr ""
+
+msgid "Thank you for your product inquiry"
+msgstr ""
+
diff --git a/src/Resources/translations/messages.cs.po b/src/Resources/translations/messages.cs.po
index eb9a73e068..bc52d62f09 100644
--- a/src/Resources/translations/messages.cs.po
+++ b/src/Resources/translations/messages.cs.po
@@ -172,6 +172,9 @@ msgstr "Doplňující informace k přihlášení"
msgid "Address"
msgstr "Adresa"
+msgid "Admin inquiry detail URL"
+msgstr "URL detailu poptávky v administraci"
+
msgid "Administration"
msgstr "Administrace"
@@ -691,6 +694,12 @@ msgstr "Komunikace se zákazníkem"
msgid "Company"
msgstr "Firma"
+msgid "Company (Company number)"
+msgstr "Firma (IČ)"
+
+msgid "Company Tax number"
+msgstr "DIČ firmy"
+
msgid "Company data"
msgstr "Firemní údaje"
@@ -1978,6 +1987,9 @@ msgstr "Vygenerované dávky"
msgid "Go to Frontend"
msgstr "Přejít na Frontend"
+msgid "Go to associated customer user"
+msgstr "Přejít na přiřazeného zákazníka"
+
msgid "Go to page:"
msgstr "Přejít na stránku:"
@@ -2128,6 +2140,30 @@ msgstr "Vstupní cena s DPH"
msgid "Input price without VAT"
msgstr "Vstupní cena bez DPH"
+msgid "Inquired product"
+msgstr "Poptávaný produkt"
+
+msgid "Inquiries"
+msgstr "Poptávky"
+
+msgid "Inquiries - view"
+msgstr "Poptávky - zobrazení"
+
+msgid "Inquiries overview"
+msgstr "Přehled poptávek"
+
+msgid "Inquiry by"
+msgstr "Poptávka od"
+
+msgid "Inquiry detail"
+msgstr "Detail poptávky"
+
+msgid "Inquiry info sent to administrator"
+msgstr "Informace o poptávce odesílaná administrátorovi"
+
+msgid "Inquiry info sent to customer"
+msgstr "Informace o poptávce odesílaná zákazníkovi"
+
msgid "Instagram URL"
msgstr "Instagram URL"
@@ -2569,6 +2605,9 @@ msgstr "Žádné soubory nebyly nalezeny."
msgid "No flags found. You have to create some first."
msgstr "Nebyly nalezeny žádné příznaky. Nejprve musíte nějaké vytvořit."
+msgid "No inquiries found."
+msgstr "Nebyly nalezeny žádné poptávky."
+
msgid "No logs created so far."
msgstr "Žádné protokoly nejsou zatím vytvořeny."
@@ -3100,9 +3139,21 @@ msgstr "Bylo upraveno zboží {{ product|productDi
msgid "Product {{ product|productDisplayName }} deleted"
msgstr "Produkt {{ product|productDisplayName }} byl smazán"
+msgid "Product associated with this inquiry is no longer available in the catalog."
+msgstr "Produkt v této poptávce není již dostupný v katalogu."
+
+msgid "Product catnum"
+msgstr "Katalogové číslo produktu"
+
+msgid "Product detail URL"
+msgstr "URL detailu produktu"
+
msgid "Product excluded from sale"
msgstr "Zboží je vyřazeno z prodeje"
+msgid "Product image URL"
+msgstr "URL obrázku produktu"
+
msgid "Product in order"
msgstr "Zboží v objednávce"
@@ -3130,6 +3181,9 @@ msgstr "Zboží bylo uloženo do objednávky"
msgid "Product settings on the main page successfully changed"
msgstr "Nastavení zboží na titulce bylo úspěšně změněno."
+msgid "Product type"
+msgstr "Typ produktu"
+
msgid "Products"
msgstr "Zboží"
@@ -3418,6 +3472,9 @@ msgstr "Vyhledávání hledá v názvu. Je možné používat operátory * a ?."
msgid "Searching searches in order number, registered customer email, customer's email in order, customer's last name and company name. It is possible to use wildcards * and ?."
msgstr "Vyhledávání hledá v číslu objednávky, e-mailu registrovaného zákazníka, e-mailu zákazníka v objednávce, příjmení zákazníka a názvu firmy. Je možné používat operátory * a ?."
+msgid "Searching searches in product name, product catalog number, company number, company name, customer last name, and customer email. It is possible to use wildcards * and ?."
+msgstr "Vyhledávání hledá v názvu produktu, katalogovém čísle produktu, IČ, názvu firmy, příjmení zákazníka, a e-mailu zákazníka. Je možné používat operátory * a ?."
+
msgid "Searching searches in slugs. It is possible to use wildcards * and ?."
msgstr "Vyhledávání hledá ve sluzích. Je možné používat operátory * a ?."
@@ -3613,6 +3670,9 @@ msgstr "Krátký popis kategorie"
msgid "Show combinations"
msgstr "Zobrazit kombinace"
+msgid "Show detail"
+msgstr "Zobrazit detail"
+
msgid "Show in the category"
msgstr "Zobrazit v rozcestníku"
@@ -3769,6 +3829,9 @@ msgstr "Čím vyšší priorita, tím bude stát zobrazen výše v seznamech. St
msgid "The optimal size of the icon is 46x26 px. Only PNG format is allowed."
msgstr "Optimální velikost ikony je 46x26 px. Je povolen pouze PNG formát."
+msgid "The product is no longer in the catalog. This was the catalog number at the time of the inquiry."
+msgstr "Produkt již není v katalogu. Toto bylo katalogové číslo v době vytvoření poptávky."
+
msgid "There are no parameter values of type color yet."
msgstr "Žádná hodnota parametru typu barva zatím nebyla vytvořena."
@@ -4042,6 +4105,9 @@ msgstr "Soubor {{ name }} byl upraven
msgid "Uploading..."
msgstr "Nahrávám..."
+msgid "Upon inquiry"
+msgstr "Na poptávku"
+
msgid "Url address"
msgstr "Url adresa"
@@ -4342,6 +4408,9 @@ msgstr "v kategorii (mezi prvním a druhým řádkem produktů)"
msgid "include"
msgstr "obsahuje"
+msgid "inquiries"
+msgstr "poptávky"
+
msgid "is"
msgstr "je"
diff --git a/src/Resources/translations/messages.en.po b/src/Resources/translations/messages.en.po
index 74f724598c..e2c5eb72fc 100644
--- a/src/Resources/translations/messages.en.po
+++ b/src/Resources/translations/messages.en.po
@@ -172,6 +172,9 @@ msgstr ""
msgid "Address"
msgstr ""
+msgid "Admin inquiry detail URL"
+msgstr ""
+
msgid "Administration"
msgstr ""
@@ -691,6 +694,12 @@ msgstr ""
msgid "Company"
msgstr ""
+msgid "Company (Company number)"
+msgstr ""
+
+msgid "Company Tax number"
+msgstr ""
+
msgid "Company data"
msgstr ""
@@ -1978,6 +1987,9 @@ msgstr ""
msgid "Go to Frontend"
msgstr ""
+msgid "Go to associated customer user"
+msgstr ""
+
msgid "Go to page:"
msgstr ""
@@ -2128,6 +2140,30 @@ msgstr ""
msgid "Input price without VAT"
msgstr ""
+msgid "Inquired product"
+msgstr ""
+
+msgid "Inquiries"
+msgstr ""
+
+msgid "Inquiries - view"
+msgstr ""
+
+msgid "Inquiries overview"
+msgstr ""
+
+msgid "Inquiry by"
+msgstr ""
+
+msgid "Inquiry detail"
+msgstr ""
+
+msgid "Inquiry info sent to administrator"
+msgstr ""
+
+msgid "Inquiry info sent to customer"
+msgstr ""
+
msgid "Instagram URL"
msgstr ""
@@ -2569,6 +2605,9 @@ msgstr ""
msgid "No flags found. You have to create some first."
msgstr ""
+msgid "No inquiries found."
+msgstr ""
+
msgid "No logs created so far."
msgstr ""
@@ -3100,9 +3139,21 @@ msgstr ""
msgid "Product {{ product|productDisplayName }} deleted"
msgstr ""
+msgid "Product associated with this inquiry is no longer available in the catalog."
+msgstr ""
+
+msgid "Product catnum"
+msgstr ""
+
+msgid "Product detail URL"
+msgstr ""
+
msgid "Product excluded from sale"
msgstr ""
+msgid "Product image URL"
+msgstr ""
+
msgid "Product in order"
msgstr ""
@@ -3130,6 +3181,9 @@ msgstr ""
msgid "Product settings on the main page successfully changed"
msgstr ""
+msgid "Product type"
+msgstr ""
+
msgid "Products"
msgstr ""
@@ -3418,6 +3472,9 @@ msgstr ""
msgid "Searching searches in order number, registered customer email, customer's email in order, customer's last name and company name. It is possible to use wildcards * and ?."
msgstr ""
+msgid "Searching searches in product name, product catalog number, company number, company name, customer last name, and customer email. It is possible to use wildcards * and ?."
+msgstr ""
+
msgid "Searching searches in slugs. It is possible to use wildcards * and ?."
msgstr ""
@@ -3613,6 +3670,9 @@ msgstr ""
msgid "Show combinations"
msgstr ""
+msgid "Show detail"
+msgstr ""
+
msgid "Show in the category"
msgstr ""
@@ -3769,6 +3829,9 @@ msgstr ""
msgid "The optimal size of the icon is 46x26 px. Only PNG format is allowed."
msgstr ""
+msgid "The product is no longer in the catalog. This was the catalog number at the time of the inquiry."
+msgstr ""
+
msgid "There are no parameter values of type color yet."
msgstr ""
@@ -4042,6 +4105,9 @@ msgstr ""
msgid "Uploading..."
msgstr ""
+msgid "Upon inquiry"
+msgstr ""
+
msgid "Url address"
msgstr ""
@@ -4342,6 +4408,9 @@ msgstr ""
msgid "include"
msgstr ""
+msgid "inquiries"
+msgstr ""
+
msgid "is"
msgstr ""
diff --git a/src/Resources/views/Admin/Content/Inquiry/detail.html.twig b/src/Resources/views/Admin/Content/Inquiry/detail.html.twig
new file mode 100644
index 0000000000..341e6c2c63
--- /dev/null
+++ b/src/Resources/views/Admin/Content/Inquiry/detail.html.twig
@@ -0,0 +1,93 @@
+{% extends '@ShopsysFramework/Admin/Layout/layoutWithPanel.html.twig' %}
+
+{% block title %}- {{ 'Inquiry by'|trans }} {{ inquiry.email }}{% endblock %}
+{% block h1 %}{{ 'Inquiry by'|trans }} {{ inquiry.email }}{% endblock %}
+
+{% block main_content %}
+
+
+
+
+ {% embed '@ShopsysFramework/Admin/Inline/FixedBar/fixedBar.html.twig' %}
+ {% block fixed_bar_content %}
+ {{ 'Back to overview'|trans }}
+ {% endblock %}
+ {% endembed %}
+{% endblock %}
diff --git a/src/Resources/views/Admin/Content/Inquiry/list.html.twig b/src/Resources/views/Admin/Content/Inquiry/list.html.twig
new file mode 100644
index 0000000000..bad0087466
--- /dev/null
+++ b/src/Resources/views/Admin/Content/Inquiry/list.html.twig
@@ -0,0 +1,26 @@
+{% extends '@ShopsysFramework/Admin/Layout/layoutWithPanel.html.twig' %}
+
+{% block title %}- {{ 'Inquiries overview'|trans }}{% endblock %}
+{% block h1 %}{{ 'Inquiries overview'|trans }}{% endblock %}
+
+{% block main_content %}
+
+
+
+
+ {% include '@ShopsysFramework/Admin/Content/Inquiry/quickSearchFormContent.html.twig' with {quickSearchForm: quickSearchForm} %}
+
+
+
+
+ {{ render(controller('Shopsys\\FrameworkBundle\\Controller\\Admin\\DomainFilterController::domainFilterTabsAction', { namespace: domainFilterNamespace })) }}
+
+ {{ gridView.render() }}
+{% endblock %}
diff --git a/src/Resources/views/Admin/Content/Inquiry/listGrid.html.twig b/src/Resources/views/Admin/Content/Inquiry/listGrid.html.twig
new file mode 100644
index 0000000000..7d66af1d73
--- /dev/null
+++ b/src/Resources/views/Admin/Content/Inquiry/listGrid.html.twig
@@ -0,0 +1,32 @@
+{% extends '@ShopsysFramework/Admin/Grid/Grid.html.twig' %}
+
+{% block grid_value_cell_id_productName %}
+ {% if value %}
+ {{ value }}
+ {% else %}
+ {{ row.i.productCatnum }}
+
+ {{ icon('info') }}
+
+ {% endif %}
+ {% if row.productId %}
+ {{ icon('forward-page') }}
+ {% endif %}
+{% endblock %}
+
+{% block grid_value_cell_id_createdAt %}
+ {{ value|formatDateTime }}
+{% endblock %}
+
+{% block grid_no_data %}
+ {{ 'No inquiries found.'|trans }}
+{% endblock %}
+
+{% block grid_pager_totalcount %}
+ {% set entityName = 'inquiries'|trans %}
+ {{ parent() }}
+{% endblock %}
diff --git a/src/Resources/views/Admin/Content/Inquiry/quickSearchFormContent.html.twig b/src/Resources/views/Admin/Content/Inquiry/quickSearchFormContent.html.twig
new file mode 100644
index 0000000000..8089abdc94
--- /dev/null
+++ b/src/Resources/views/Admin/Content/Inquiry/quickSearchFormContent.html.twig
@@ -0,0 +1,12 @@
+{{ form_start(quickSearchForm) }}
+
+
+ {{ form_widget(quickSearchForm.text) }}
+
+
+ {{ form_widget(quickSearchForm.submit, { label: 'Search [verb]'|trans, attr: { class: 'box-quick-search__btn'}, full_name: '' }) }}
+
+{{ form_end(quickSearchForm) }}
diff --git a/src/Twig/HiddenPriceExtension.php b/src/Twig/HiddenPriceExtension.php
index 2eb0c8b2f5..1a68535616 100644
--- a/src/Twig/HiddenPriceExtension.php
+++ b/src/Twig/HiddenPriceExtension.php
@@ -4,6 +4,7 @@
namespace Shopsys\FrameworkBundle\Twig;
+use Shopsys\FrameworkBundle\Component\Money\HiddenMoney;
use Shopsys\FrameworkBundle\Model\Customer\User\CustomerUser;
use Shopsys\FrameworkBundle\Model\Customer\User\Role\CustomerUserRoleResolver;
use Twig\Extension\AbstractExtension;
@@ -11,8 +12,6 @@
class HiddenPriceExtension extends AbstractExtension
{
- protected const string HIDDEN_FORMAT = '***';
-
/**
* @param \Shopsys\FrameworkBundle\Model\Customer\User\Role\CustomerUserRoleResolver $customerUserRoleResolver
*/
@@ -42,7 +41,7 @@ public function getFilters(): array
public function hidePriceFilter(string $price, ?CustomerUser $customerUser): string
{
if (!$this->customerUserRoleResolver->canCustomerUserSeePrices($customerUser)) {
- return static::HIDDEN_FORMAT;
+ return HiddenMoney::HIDDEN_FORMAT;
}
return $price;
diff --git a/tests/Unit/Model/Product/Search/ProductElasticsearchConverterTest.php b/tests/Unit/Model/Product/Search/ProductElasticsearchConverterTest.php
index 3e75db429a..5ba1a1d797 100644
--- a/tests/Unit/Model/Product/Search/ProductElasticsearchConverterTest.php
+++ b/tests/Unit/Model/Product/Search/ProductElasticsearchConverterTest.php
@@ -53,6 +53,8 @@ public function testFillEmptyFields(): void
'seo_title' => null,
'seo_meta_description' => null,
'hreflang_links' => [],
+ 'product_type' => 'basic',
+ 'priority_by_product_type' => 0,
];
$converter = new ProductElasticsearchConverter();
@@ -118,6 +120,8 @@ public function testFillEmptyParameterFields(): void
'seo_title' => null,
'seo_meta_description' => null,
'hreflang_links' => [],
+ 'product_type' => 'basic',
+ 'priority_by_product_type' => 0,
];
$converter = new ProductElasticsearchConverter();