|
@@ -0,0 +1,180 @@
|
|
|
|
|
+<?php
|
|
|
|
|
+
|
|
|
|
|
+declare(strict_types=1);
|
|
|
|
|
+
|
|
|
|
|
+namespace App\Services\Export;
|
|
|
|
|
+
|
|
|
|
|
+use App\Models\CommonCatalogItem;
|
|
|
|
|
+use App\Models\File;
|
|
|
|
|
+use App\Services\TechnicalDescriptionService;
|
|
|
|
|
+use Illuminate\Support\Collection;
|
|
|
|
|
+use Illuminate\Support\Facades\Storage;
|
|
|
|
|
+use Illuminate\Support\Str;
|
|
|
|
|
+use PhpOffice\PhpWord\Settings as PhpWordSettings;
|
|
|
|
|
+use PhpOffice\PhpWord\TemplateProcessor;
|
|
|
|
|
+use RuntimeException;
|
|
|
|
|
+use ZipArchive;
|
|
|
|
|
+
|
|
|
|
|
+class ExportTechnicalDescriptionsService
|
|
|
|
|
+{
|
|
|
|
|
+ private const COMBINED_TEMPLATE = 'массовый.docx';
|
|
|
|
|
+ private const SINGLE_TEMPLATE = 'одиночный.docx';
|
|
|
|
|
+
|
|
|
|
|
+ public function __construct(
|
|
|
|
|
+ private readonly TechnicalDescriptionService $technicalDescriptions,
|
|
|
|
|
+ ) {}
|
|
|
|
|
+
|
|
|
|
|
+ /** @param list<int> $itemIds */
|
|
|
|
|
+ public function handle(
|
|
|
|
|
+ array $itemIds,
|
|
|
|
|
+ string $descriptionMode,
|
|
|
|
|
+ bool $separateDocuments,
|
|
|
|
|
+ int $userId,
|
|
|
|
|
+ ): File {
|
|
|
|
|
+ $items = CommonCatalogItem::query()
|
|
|
|
|
+ ->with('imageFile')
|
|
|
|
|
+ ->whereIn('id', $itemIds)
|
|
|
|
|
+ ->orderBy('series')
|
|
|
|
|
+ ->orderBy('article')
|
|
|
|
|
+ ->get();
|
|
|
|
|
+
|
|
|
|
|
+ if ($items->isEmpty()) {
|
|
|
|
|
+ throw new RuntimeException('Не выбраны позиции для экспорта технических описаний.');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (! array_key_exists($descriptionMode, TechnicalDescriptionService::DESCRIPTION_MODES)) {
|
|
|
|
|
+ throw new RuntimeException('Выбран неизвестный вариант технического описания.');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ PhpWordSettings::setOutputEscapingEnabled(true);
|
|
|
|
|
+
|
|
|
|
|
+ return $separateDocuments
|
|
|
|
|
+ ? $this->separateDocuments($items, $descriptionMode, $userId)
|
|
|
|
|
+ : $this->combinedDocument($items, $descriptionMode, $userId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** @param Collection<int, CommonCatalogItem> $items */
|
|
|
|
|
+ private function combinedDocument(Collection $items, string $descriptionMode, int $userId): File
|
|
|
|
|
+ {
|
|
|
|
|
+ $filename = 'Технические_описания_'.now()->format('Ymd_His').'.docx';
|
|
|
|
|
+ $path = $this->generatedPath($userId, $filename);
|
|
|
|
|
+ Storage::disk('local')->makeDirectory(dirname($path));
|
|
|
|
|
+
|
|
|
|
|
+ $processor = new TemplateProcessor($this->templatePath(self::COMBINED_TEMPLATE));
|
|
|
|
|
+ $processor->cloneBlock('product_block', $items->count(), true, true);
|
|
|
|
|
+
|
|
|
|
|
+ foreach ($items->values() as $index => $item) {
|
|
|
|
|
+ $this->fillProduct($processor, $item, $descriptionMode, $index + 1, $index + 1);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $processor->saveAs(Storage::disk('local')->path($path));
|
|
|
|
|
+
|
|
|
|
|
+ return $this->registerFile($path, $filename, 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', $userId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** @param Collection<int, CommonCatalogItem> $items */
|
|
|
|
|
+ private function separateDocuments(Collection $items, string $descriptionMode, int $userId): File
|
|
|
|
|
+ {
|
|
|
|
|
+ $filename = 'Технические_описания_'.now()->format('Ymd_His').'.zip';
|
|
|
|
|
+ $path = $this->generatedPath($userId, $filename);
|
|
|
|
|
+ $temporaryDirectory = 'generated/technical-descriptions/tmp/'.Str::uuid();
|
|
|
|
|
+ Storage::disk('local')->makeDirectory(dirname($path));
|
|
|
|
|
+ Storage::disk('local')->makeDirectory($temporaryDirectory);
|
|
|
|
|
+
|
|
|
|
|
+ $zip = new ZipArchive;
|
|
|
|
|
+ if ($zip->open(Storage::disk('local')->path($path), ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
|
|
|
|
|
+ throw new RuntimeException('Не удалось создать ZIP-архив технических описаний.');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ foreach ($items->values() as $index => $item) {
|
|
|
|
|
+ $documentName = sprintf(
|
|
|
|
|
+ '%s_%04d.docx',
|
|
|
|
|
+ $this->safeFilename((string) $item->article),
|
|
|
|
|
+ $index + 1,
|
|
|
|
|
+ );
|
|
|
|
|
+ $documentPath = $temporaryDirectory.'/'.$documentName;
|
|
|
|
|
+ $processor = new TemplateProcessor($this->templatePath(self::SINGLE_TEMPLATE));
|
|
|
|
|
+ $processor->cloneBlock('product_block', 1, true, true);
|
|
|
|
|
+ $this->fillProduct($processor, $item, $descriptionMode, 1, 1);
|
|
|
|
|
+ $processor->setValue('page_br#1', '');
|
|
|
|
|
+ $processor->saveAs(Storage::disk('local')->path($documentPath));
|
|
|
|
|
+ $zip->addFile(Storage::disk('local')->path($documentPath), $documentName);
|
|
|
|
|
+ }
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ $zip->close();
|
|
|
|
|
+ Storage::disk('local')->deleteDirectory($temporaryDirectory);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return $this->registerFile($path, $filename, 'application/zip', $userId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function fillProduct(
|
|
|
|
|
+ TemplateProcessor $processor,
|
|
|
|
|
+ CommonCatalogItem $item,
|
|
|
|
|
+ string $descriptionMode,
|
|
|
|
|
+ int $variableIndex,
|
|
|
|
|
+ int $number,
|
|
|
|
|
+ ): void {
|
|
|
|
|
+ $suffix = '#'.$variableIndex;
|
|
|
|
|
+ $processor->setValue('product_group'.$suffix, $item->product_group ?: $item->kind ?: '');
|
|
|
|
|
+ $processor->setValue('num'.$suffix, (string) $number);
|
|
|
|
|
+ $processor->setValue('name_for_form'.$suffix, $item->print_name ?: $item->calculator_name ?: '');
|
|
|
|
|
+ $processor->setValue('price'.$suffix, $this->technicalDescriptions->formattedPrice($item));
|
|
|
|
|
+ $processor->setValue(
|
|
|
|
|
+ 'description'.$suffix,
|
|
|
|
|
+ $this->technicalDescriptions->description($item, $descriptionMode),
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ $imagePath = $item->imageFile?->path;
|
|
|
|
|
+ if ($imagePath && Storage::disk('public')->exists($imagePath)) {
|
|
|
|
|
+ $processor->setImageValue('image'.$suffix, [
|
|
|
|
|
+ 'path' => Storage::disk('public')->path($imagePath),
|
|
|
|
|
+ 'width' => 270,
|
|
|
|
|
+ 'height' => 180,
|
|
|
|
|
+ 'ratio' => true,
|
|
|
|
|
+ ]);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ $processor->setValue('image'.$suffix, '');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function registerFile(string $path, string $filename, string $mimeType, int $userId): File
|
|
|
|
|
+ {
|
|
|
|
|
+ $file = File::query()->create([
|
|
|
|
|
+ 'link' => '',
|
|
|
|
|
+ 'path' => $path,
|
|
|
|
|
+ 'user_id' => $userId,
|
|
|
|
|
+ 'original_name' => $filename,
|
|
|
|
|
+ 'mime_type' => $mimeType,
|
|
|
|
|
+ 'is_generated' => true,
|
|
|
|
|
+ ]);
|
|
|
|
|
+ $file->update([
|
|
|
|
|
+ 'link' => route('common-catalog.technical-description.download', $file),
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ return $file;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function generatedPath(int $userId, string $filename): string
|
|
|
|
|
+ {
|
|
|
|
|
+ return "generated/technical-descriptions/{$userId}/".Str::uuid()."/{$filename}";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function templatePath(string $filename): string
|
|
|
|
|
+ {
|
|
|
|
|
+ $path = base_path('templates/technical-descriptions/'.$filename);
|
|
|
|
|
+ if (! is_file($path)) {
|
|
|
|
|
+ throw new RuntimeException("Шаблон технического описания {$filename} не найден.");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return $path;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function safeFilename(string $value): string
|
|
|
|
|
+ {
|
|
|
|
|
+ $value = preg_replace('/[^\pL\pN._-]+/u', '_', trim($value)) ?? '';
|
|
|
|
|
+
|
|
|
|
|
+ return $value === '' ? 'позиция' : mb_substr($value, 0, 100);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|