1
+ <?php
2
+ /*
3
+ * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
4
+ *
5
+ * Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
6
+ *
7
+ * This program is free software: you can redistribute it and/or modify
8
+ * it under the terms of the GNU Affero General Public License as published
9
+ * by the Free Software Foundation, either version 3 of the License, or
10
+ * (at your option) any later version.
11
+ *
12
+ * This program is distributed in the hope that it will be useful,
13
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ * GNU Affero General Public License for more details.
16
+ *
17
+ * You should have received a copy of the GNU Affero General Public License
18
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
19
+ */
20
+
21
+ declare (strict_types=1 );
22
+
23
+ namespace App \Controller ;
24
+
25
+ use App \Entity \Parts \Part ;
26
+ use App \Entity \Parts \Supplier ;
27
+ use App \Form \InfoProviderSystem \GlobalFieldMappingType ;
28
+ use App \Services \InfoProviderSystem \PartInfoRetriever ;
29
+ use App \Services \InfoProviderSystem \ProviderRegistry ;
30
+ use App \Services \InfoProviderSystem \ExistingPartFinder ;
31
+ use Doctrine \ORM \EntityManagerInterface ;
32
+ use Psr \Log \LoggerInterface ;
33
+ use Symfony \Bundle \FrameworkBundle \Controller \AbstractController ;
34
+ use Symfony \Component \HttpClient \Exception \ClientException ;
35
+ use Symfony \Component \HttpFoundation \Request ;
36
+ use Symfony \Component \HttpFoundation \Response ;
37
+ use Symfony \Component \Routing \Attribute \Route ;
38
+
39
+ use function Symfony \Component \Translation \t ;
40
+
41
+ #[Route('/tools/bulk-info-provider-import ' )]
42
+ class BulkInfoProviderImportController extends AbstractController
43
+ {
44
+ public function __construct (
45
+ private readonly ProviderRegistry $ providerRegistry ,
46
+ private readonly PartInfoRetriever $ infoRetriever ,
47
+ private readonly ExistingPartFinder $ existingPartFinder ,
48
+ private readonly EntityManagerInterface $ entityManager
49
+ ) {
50
+ }
51
+
52
+ #[Route('/step1 ' , name: 'bulk_info_provider_step1 ' )]
53
+ public function step1 (Request $ request , LoggerInterface $ exceptionLogger ): Response
54
+ {
55
+ $ this ->denyAccessUnlessGranted ('@info_providers.create_parts ' );
56
+
57
+ $ ids = $ request ->query ->get ('ids ' );
58
+ if (!$ ids ) {
59
+ $ this ->addFlash ('error ' , 'No parts selected for bulk import ' );
60
+ return $ this ->redirectToRoute ('homepage ' );
61
+ }
62
+
63
+ // Get the selected parts
64
+ $ partIds = explode (', ' , $ ids );
65
+ $ partRepository = $ this ->entityManager ->getRepository (Part::class);
66
+ $ parts = $ partRepository ->getElementsFromIDArray ($ partIds );
67
+
68
+ if (empty ($ parts )) {
69
+ $ this ->addFlash ('error ' , 'No valid parts found for bulk import ' );
70
+ return $ this ->redirectToRoute ('homepage ' );
71
+ }
72
+
73
+ // Generate field choices
74
+ $ fieldChoices = [
75
+ 'info_providers.bulk_search.field.mpn ' => 'mpn ' ,
76
+ 'info_providers.bulk_search.field.name ' => 'name ' ,
77
+ ];
78
+
79
+ // Add dynamic supplier fields
80
+ $ suppliers = $ this ->entityManager ->getRepository (Supplier::class)->findAll ();
81
+ foreach ($ suppliers as $ supplier ) {
82
+ $ supplierKey = strtolower (str_replace ([' ' , '- ' , '_ ' ], '_ ' , $ supplier ->getName ()));
83
+ $ fieldChoices ["Supplier: " . $ supplier ->getName () . " (SPN) " ] = $ supplierKey . '_spn ' ;
84
+ }
85
+
86
+ // Initialize form with useful default mappings
87
+ $ initialData = [
88
+ 'field_mappings ' => [
89
+ ['field ' => 'mpn ' , 'providers ' => []]
90
+ ]
91
+ ];
92
+
93
+ $ form = $ this ->createForm (GlobalFieldMappingType::class, $ initialData , [
94
+ 'field_choices ' => $ fieldChoices
95
+ ]);
96
+ $ form ->handleRequest ($ request );
97
+
98
+ $ searchResults = null ;
99
+
100
+ if ($ form ->isSubmitted () && $ form ->isValid ()) {
101
+ $ fieldMappings = $ form ->getData ()['field_mappings ' ];
102
+ $ searchResults = [];
103
+
104
+ foreach ($ parts as $ part ) {
105
+ $ partResult = [
106
+ 'part ' => $ part ,
107
+ 'search_results ' => [],
108
+ 'errors ' => []
109
+ ];
110
+
111
+ // Collect all DTOs from all applicable field mappings
112
+ $ allDtos = [];
113
+
114
+ foreach ($ fieldMappings as $ mapping ) {
115
+ $ field = $ mapping ['field ' ];
116
+ $ providers = $ mapping ['providers ' ] ?? [];
117
+
118
+ if (empty ($ providers )) {
119
+ continue ;
120
+ }
121
+
122
+ $ keyword = $ this ->getKeywordFromField ($ part , $ field );
123
+
124
+ if ($ keyword ) {
125
+ try {
126
+ $ dtos = $ this ->infoRetriever ->searchByKeyword (
127
+ keyword: $ keyword ,
128
+ providers: $ providers
129
+ );
130
+
131
+ // Add field info to each DTO for tracking
132
+ foreach ($ dtos as $ dto ) {
133
+ $ dto ->_source_field = $ field ;
134
+ $ dto ->_source_keyword = $ keyword ;
135
+ }
136
+
137
+ $ allDtos = array_merge ($ allDtos , $ dtos );
138
+ } catch (ClientException $ e ) {
139
+ $ partResult ['errors ' ][] = "Error searching with {$ field }: " . $ e ->getMessage ();
140
+ $ exceptionLogger ->error ('Error during bulk info provider search for part ' . $ part ->getId () . " field {$ field }: " . $ e ->getMessage (), ['exception ' => $ e ]);
141
+ }
142
+ }
143
+ }
144
+
145
+ // Remove duplicates based on provider_key + provider_id
146
+ $ uniqueDtos = [];
147
+ $ seenKeys = [];
148
+ foreach ($ allDtos as $ dto ) {
149
+ $ key = $ dto ->provider_key . '| ' . $ dto ->provider_id ;
150
+ if (!in_array ($ key , $ seenKeys )) {
151
+ $ seenKeys [] = $ key ;
152
+ $ uniqueDtos [] = $ dto ;
153
+ }
154
+ }
155
+
156
+ // Convert DTOs to result format
157
+ $ partResult ['search_results ' ] = array_map (
158
+ fn ($ dto ) => ['dto ' => $ dto , 'localPart ' => $ this ->existingPartFinder ->findFirstExisting ($ dto )],
159
+ $ uniqueDtos
160
+ );
161
+
162
+ $ searchResults [] = $ partResult ;
163
+ }
164
+ }
165
+
166
+ return $ this ->render ('info_providers/bulk_import/step1.html.twig ' , [
167
+ 'form ' => $ form ,
168
+ 'parts ' => $ parts ,
169
+ 'search_results ' => $ searchResults ,
170
+ 'fieldChoices ' => $ fieldChoices
171
+ ]);
172
+ }
173
+
174
+ private function getKeywordFromField (Part $ part , string $ field ): ?string
175
+ {
176
+ return match ($ field ) {
177
+ 'mpn ' => $ part ->getManufacturerProductNumber (),
178
+ 'name ' => $ part ->getName (),
179
+ default => $ this ->getSupplierPartNumber ($ part , $ field )
180
+ };
181
+ }
182
+
183
+ private function getSupplierPartNumber (Part $ part , string $ field ): ?string
184
+ {
185
+ // Check if this is a supplier SPN field
186
+ if (!str_ends_with ($ field , '_spn ' )) {
187
+ return null ;
188
+ }
189
+
190
+ // Extract supplier key (remove _spn suffix)
191
+ $ supplierKey = substr ($ field , 0 , -4 );
192
+
193
+ // Get all suppliers to find matching one
194
+ $ suppliers = $ this ->entityManager ->getRepository (Supplier::class)->findAll ();
195
+
196
+ foreach ($ suppliers as $ supplier ) {
197
+ $ normalizedName = strtolower (str_replace ([' ' , '- ' , '_ ' ], '_ ' , $ supplier ->getName ()));
198
+ if ($ normalizedName === $ supplierKey ) {
199
+ // Find order detail for this supplier
200
+ $ orderDetail = $ part ->getOrderdetails ()->filter (
201
+ fn ($ od ) => $ od ->getSupplier ()?->getId() === $ supplier ->getId ()
202
+ )->first ();
203
+
204
+ return $ orderDetail ? $ orderDetail ->getSupplierpartnr () : null ;
205
+ }
206
+ }
207
+
208
+ return null ;
209
+ }
210
+ }
0 commit comments