35
35
use Doctrine \ORM \EntityManagerInterface ;
36
36
use League \Csv \SyntaxError ;
37
37
use Omines \DataTablesBundle \DataTableFactory ;
38
+ use Psr \Log \LoggerInterface ;
38
39
use Symfony \Bundle \FrameworkBundle \Controller \AbstractController ;
39
40
use Symfony \Component \Form \Extension \Core \Type \CheckboxType ;
40
41
use Symfony \Component \Form \Extension \Core \Type \ChoiceType ;
@@ -100,9 +101,14 @@ public function build(Project $project, Request $request, ProjectBuildHelper $bu
100
101
$ this ->addFlash ('success ' , 'project.build.flash.success ' );
101
102
102
103
return $ this ->redirect (
103
- $ request ->get ('_redirect ' ,
104
- $ this ->generateUrl ('project_info ' , ['id ' => $ project ->getID ()]
105
- )));
104
+ $ request ->get (
105
+ '_redirect ' ,
106
+ $ this ->generateUrl (
107
+ 'project_info ' ,
108
+ ['id ' => $ project ->getID ()]
109
+ )
110
+ )
111
+ );
106
112
}
107
113
108
114
$ this ->addFlash ('error ' , 'project.build.flash.invalid_input ' );
@@ -118,9 +124,13 @@ public function build(Project $project, Request $request, ProjectBuildHelper $bu
118
124
}
119
125
120
126
#[Route(path: '/{id}/import_bom ' , name: 'project_import_bom ' , requirements: ['id ' => '\d+ ' ])]
121
- public function importBOM (Request $ request , EntityManagerInterface $ entityManager , Project $ project ,
122
- BOMImporter $ BOMImporter , ValidatorInterface $ validator ): Response
123
- {
127
+ public function importBOM (
128
+ Request $ request ,
129
+ EntityManagerInterface $ entityManager ,
130
+ Project $ project ,
131
+ BOMImporter $ BOMImporter ,
132
+ ValidatorInterface $ validator
133
+ ): Response {
124
134
$ this ->denyAccessUnlessGranted ('edit ' , $ project );
125
135
126
136
$ builder = $ this ->createFormBuilder ();
@@ -136,6 +146,7 @@ public function importBOM(Request $request, EntityManagerInterface $entityManage
136
146
'required ' => true ,
137
147
'choices ' => [
138
148
'project.bom_import.type.kicad_pcbnew ' => 'kicad_pcbnew ' ,
149
+ 'project.bom_import.type.kicad_schematic ' => 'kicad_schematic ' ,
139
150
]
140
151
]);
141
152
$ builder ->add ('clear_existing_bom ' , CheckboxType::class, [
@@ -159,25 +170,40 @@ public function importBOM(Request $request, EntityManagerInterface $entityManage
159
170
$ entityManager ->flush ();
160
171
}
161
172
173
+ $ import_type = $ form ->get ('type ' )->getData ();
174
+
162
175
try {
176
+ // For schematic imports, redirect to field mapping step
177
+ if ($ import_type === 'kicad_schematic ' ) {
178
+ // Store file content and options in session for field mapping step
179
+ $ file_content = $ form ->get ('file ' )->getData ()->getContent ();
180
+ $ clear_existing = $ form ->get ('clear_existing_bom ' )->getData ();
181
+
182
+ $ request ->getSession ()->set ('bom_import_data ' , $ file_content );
183
+ $ request ->getSession ()->set ('bom_import_clear ' , $ clear_existing );
184
+
185
+ return $ this ->redirectToRoute ('project_import_bom_map_fields ' , ['id ' => $ project ->getID ()]);
186
+ }
187
+
188
+ // For PCB imports, proceed directly
163
189
$ entries = $ BOMImporter ->importFileIntoProject ($ form ->get ('file ' )->getData (), $ project , [
164
- 'type ' => $ form -> get ( ' type ' )-> getData () ,
190
+ 'type ' => $ import_type ,
165
191
]);
166
192
167
- //Validate the project entries
193
+ // Validate the project entries
168
194
$ errors = $ validator ->validateProperty ($ project , 'bom_entries ' );
169
195
170
- //If no validation errors occured , save the changes and redirect to edit page
171
- if (count ($ errors ) === 0 ) {
196
+ // If no validation errors occurred , save the changes and redirect to edit page
197
+ if (count ($ errors ) === 0 ) {
172
198
$ this ->addFlash ('success ' , t ('project.bom_import.flash.success ' , ['%count% ' => count ($ entries )]));
173
199
$ entityManager ->flush ();
174
200
return $ this ->redirectToRoute ('project_edit ' , ['id ' => $ project ->getID ()]);
175
201
}
176
202
177
- //When we get here, there were validation errors
203
+ // When we get here, there were validation errors
178
204
$ this ->addFlash ('error ' , t ('project.bom_import.flash.invalid_entries ' ));
179
205
180
- } catch (\UnexpectedValueException | SyntaxError $ e ) {
206
+ } catch (\UnexpectedValueException | SyntaxError $ e ) {
181
207
$ this ->addFlash ('error ' , t ('project.bom_import.flash.invalid_file ' , ['%message% ' => $ e ->getMessage ()]));
182
208
}
183
209
}
@@ -189,11 +215,257 @@ public function importBOM(Request $request, EntityManagerInterface $entityManage
189
215
]);
190
216
}
191
217
218
+ #[Route(path: '/{id}/import_bom/map_fields ' , name: 'project_import_bom_map_fields ' , requirements: ['id ' => '\d+ ' ])]
219
+ public function importBOMMapFields (
220
+ Request $ request ,
221
+ EntityManagerInterface $ entityManager ,
222
+ Project $ project ,
223
+ BOMImporter $ BOMImporter ,
224
+ ValidatorInterface $ validator ,
225
+ LoggerInterface $ logger
226
+ ): Response {
227
+ $ this ->denyAccessUnlessGranted ('edit ' , $ project );
228
+
229
+ // Get stored data from session
230
+ $ file_content = $ request ->getSession ()->get ('bom_import_data ' );
231
+ $ clear_existing = $ request ->getSession ()->get ('bom_import_clear ' , false );
232
+
233
+
234
+ if (!$ file_content ) {
235
+ $ this ->addFlash ('error ' , 'project.bom_import.flash.session_expired ' );
236
+ return $ this ->redirectToRoute ('project_import_bom ' , ['id ' => $ project ->getID ()]);
237
+ }
238
+
239
+ // Detect fields and get suggestions
240
+ $ detected_fields = $ BOMImporter ->detectFields ($ file_content );
241
+ $ suggested_mapping = $ BOMImporter ->getSuggestedFieldMapping ($ detected_fields );
242
+
243
+ // Create mapping of original field names to sanitized field names for template
244
+ $ field_name_mapping = [];
245
+ foreach ($ detected_fields as $ field ) {
246
+ $ sanitized_field = preg_replace ('/[^a-zA-Z0-9_-]/ ' , '_ ' , $ field );
247
+ $ field_name_mapping [$ field ] = $ sanitized_field ;
248
+ }
249
+
250
+ // Create form for field mapping
251
+ $ builder = $ this ->createFormBuilder ();
252
+
253
+ // Add delimiter selection
254
+ $ builder ->add ('delimiter ' , ChoiceType::class, [
255
+ 'label ' => 'project.bom_import.delimiter ' ,
256
+ 'required ' => true ,
257
+ 'data ' => ', ' ,
258
+ 'choices ' => [
259
+ 'project.bom_import.delimiter.comma ' => ', ' ,
260
+ 'project.bom_import.delimiter.semicolon ' => '; ' ,
261
+ 'project.bom_import.delimiter.tab ' => "\t" ,
262
+ ]
263
+ ]);
264
+
265
+ // Get dynamic field mapping targets from BOMImporter
266
+ $ available_targets = $ BOMImporter ->getAvailableFieldTargets ();
267
+ $ target_fields = ['project.bom_import.field_mapping.ignore ' => '' ];
268
+
269
+ foreach ($ available_targets as $ target_key => $ target_info ) {
270
+ $ target_fields [$ target_info ['label ' ]] = $ target_key ;
271
+ }
272
+
273
+ foreach ($ detected_fields as $ field ) {
274
+ // Sanitize field name for form use - replace invalid characters with underscores
275
+ $ sanitized_field = preg_replace ('/[^a-zA-Z0-9_-]/ ' , '_ ' , $ field );
276
+ $ builder ->add ('mapping_ ' . $ sanitized_field , ChoiceType::class, [
277
+ 'label ' => $ field ,
278
+ 'required ' => false ,
279
+ 'choices ' => $ target_fields ,
280
+ 'data ' => $ suggested_mapping [$ field ] ?? '' ,
281
+ ]);
282
+ }
283
+
284
+ $ builder ->add ('submit ' , SubmitType::class, [
285
+ 'label ' => 'project.bom_import.preview ' ,
286
+ ]);
287
+
288
+ $ form = $ builder ->getForm ();
289
+ $ form ->handleRequest ($ request );
290
+
291
+ if ($ form ->isSubmitted () && $ form ->isValid ()) {
292
+ // Build field mapping array with priority support
293
+ $ field_mapping = [];
294
+ $ field_priorities = [];
295
+ $ delimiter = $ form ->get ('delimiter ' )->getData ();
296
+
297
+ foreach ($ detected_fields as $ field ) {
298
+ $ sanitized_field = preg_replace ('/[^a-zA-Z0-9_-]/ ' , '_ ' , $ field );
299
+ $ target = $ form ->get ('mapping_ ' . $ sanitized_field )->getData ();
300
+ if (!empty ($ target )) {
301
+ $ field_mapping [$ field ] = $ target ;
302
+
303
+ // Get priority from request (default to 10)
304
+ $ priority = $ request ->request ->get ('priority_ ' . $ sanitized_field , 10 );
305
+ $ field_priorities [$ field ] = (int ) $ priority ;
306
+ }
307
+ }
308
+
309
+ // Validate field mapping
310
+ $ validation = $ BOMImporter ->validateFieldMapping ($ field_mapping , $ detected_fields );
311
+
312
+ if (!$ validation ['is_valid ' ]) {
313
+ foreach ($ validation ['errors ' ] as $ error ) {
314
+ $ this ->addFlash ('error ' , $ error );
315
+ }
316
+ foreach ($ validation ['warnings ' ] as $ warning ) {
317
+ $ this ->addFlash ('warning ' , $ warning );
318
+ }
319
+
320
+ return $ this ->render ('projects/import_bom_map_fields.html.twig ' , [
321
+ 'project ' => $ project ,
322
+ 'form ' => $ form ->createView (),
323
+ 'detected_fields ' => $ detected_fields ,
324
+ 'suggested_mapping ' => $ suggested_mapping ,
325
+ 'field_name_mapping ' => $ field_name_mapping ,
326
+ ]);
327
+ }
328
+
329
+ // Show warnings but continue
330
+ foreach ($ validation ['warnings ' ] as $ warning ) {
331
+ $ this ->addFlash ('warning ' , $ warning );
332
+ }
333
+
334
+ try {
335
+ // Re-detect fields with chosen delimiter
336
+ $ detected_fields = $ BOMImporter ->detectFields ($ file_content , $ delimiter );
337
+
338
+ // Clear existing BOM entries if requested
339
+ if ($ clear_existing ) {
340
+ $ existing_count = $ project ->getBomEntries ()->count ();
341
+ $ logger ->info ('Clearing existing BOM entries ' , [
342
+ 'existing_count ' => $ existing_count ,
343
+ 'project_id ' => $ project ->getID (),
344
+ ]);
345
+ $ project ->getBomEntries ()->clear ();
346
+ $ entityManager ->flush ();
347
+ $ logger ->info ('Existing BOM entries cleared ' );
348
+ } else {
349
+ $ existing_count = $ project ->getBomEntries ()->count ();
350
+ $ logger ->info ('Keeping existing BOM entries ' , [
351
+ 'existing_count ' => $ existing_count ,
352
+ 'project_id ' => $ project ->getID (),
353
+ ]);
354
+ }
355
+
356
+ // Validate data before importing
357
+ $ validation_result = $ BOMImporter ->validateBOMData ($ file_content , [
358
+ 'type ' => 'kicad_schematic ' ,
359
+ 'field_mapping ' => $ field_mapping ,
360
+ 'field_priorities ' => $ field_priorities ,
361
+ 'delimiter ' => $ delimiter ,
362
+ ]);
363
+
364
+ // Log validation results
365
+ $ logger ->info ('BOM import validation completed ' , [
366
+ 'total_entries ' => $ validation_result ['total_entries ' ],
367
+ 'valid_entries ' => $ validation_result ['valid_entries ' ],
368
+ 'invalid_entries ' => $ validation_result ['invalid_entries ' ],
369
+ 'error_count ' => count ($ validation_result ['errors ' ]),
370
+ 'warning_count ' => count ($ validation_result ['warnings ' ]),
371
+ ]);
372
+
373
+ // Show validation warnings to user
374
+ foreach ($ validation_result ['warnings ' ] as $ warning ) {
375
+ $ this ->addFlash ('warning ' , $ warning );
376
+ }
377
+
378
+ // If there are validation errors, show them and stop
379
+ if (!empty ($ validation_result ['errors ' ])) {
380
+ foreach ($ validation_result ['errors ' ] as $ error ) {
381
+ $ this ->addFlash ('error ' , $ error );
382
+ }
383
+
384
+ return $ this ->render ('projects/import_bom_map_fields.html.twig ' , [
385
+ 'project ' => $ project ,
386
+ 'form ' => $ form ->createView (),
387
+ 'detected_fields ' => $ detected_fields ,
388
+ 'suggested_mapping ' => $ suggested_mapping ,
389
+ 'field_name_mapping ' => $ field_name_mapping ,
390
+ 'validation_result ' => $ validation_result ,
391
+ ]);
392
+ }
393
+
394
+ // Import with field mapping and priorities (validation already passed)
395
+ $ entries = $ BOMImporter ->stringToBOMEntries ($ file_content , [
396
+ 'type ' => 'kicad_schematic ' ,
397
+ 'field_mapping ' => $ field_mapping ,
398
+ 'field_priorities ' => $ field_priorities ,
399
+ 'delimiter ' => $ delimiter ,
400
+ ]);
401
+
402
+ // Log entry details for debugging
403
+ $ logger ->info ('BOM entries created ' , [
404
+ 'total_entries ' => count ($ entries ),
405
+ ]);
406
+
407
+ foreach ($ entries as $ index => $ entry ) {
408
+ $ logger ->debug ("BOM entry {$ index }" , [
409
+ 'name ' => $ entry ->getName (),
410
+ 'mountnames ' => $ entry ->getMountnames (),
411
+ 'quantity ' => $ entry ->getQuantity (),
412
+ 'comment ' => $ entry ->getComment (),
413
+ 'part_id ' => $ entry ->getPart ()?->getID(),
414
+ ]);
415
+ }
416
+
417
+ // Assign entries to project
418
+ $ logger ->info ('Adding BOM entries to project ' , [
419
+ 'entries_count ' => count ($ entries ),
420
+ 'project_id ' => $ project ->getID (),
421
+ ]);
422
+
423
+ foreach ($ entries as $ index => $ entry ) {
424
+ $ logger ->debug ("Adding BOM entry {$ index } to project " , [
425
+ 'name ' => $ entry ->getName (),
426
+ 'part_id ' => $ entry ->getPart ()?->getID(),
427
+ 'quantity ' => $ entry ->getQuantity (),
428
+ ]);
429
+ $ project ->addBomEntry ($ entry );
430
+ }
431
+
432
+ // Validate the project entries (includes collection constraints)
433
+ $ errors = $ validator ->validateProperty ($ project , 'bom_entries ' );
434
+
435
+ // If no validation errors occurred, save and redirect
436
+ if (count ($ errors ) === 0 ) {
437
+ $ this ->addFlash ('success ' , t ('project.bom_import.flash.success ' , ['%count% ' => count ($ entries )]));
438
+ $ entityManager ->flush ();
439
+
440
+ // Clear session data
441
+ $ request ->getSession ()->remove ('bom_import_data ' );
442
+ $ request ->getSession ()->remove ('bom_import_clear ' );
443
+
444
+ return $ this ->redirectToRoute ('project_edit ' , ['id ' => $ project ->getID ()]);
445
+ }
446
+
447
+ // When we get here, there were validation errors
448
+ $ this ->addFlash ('error ' , t ('project.bom_import.flash.invalid_entries ' ));
449
+
450
+ } catch (\UnexpectedValueException | SyntaxError $ e ) {
451
+ $ this ->addFlash ('error ' , t ('project.bom_import.flash.invalid_file ' , ['%message% ' => $ e ->getMessage ()]));
452
+ }
453
+ }
454
+
455
+ return $ this ->render ('projects/import_bom_map_fields.html.twig ' , [
456
+ 'project ' => $ project ,
457
+ 'form ' => $ form ,
458
+ 'detected_fields ' => $ detected_fields ,
459
+ 'suggested_mapping ' => $ suggested_mapping ,
460
+ 'field_name_mapping ' => $ field_name_mapping ,
461
+ ]);
462
+ }
463
+
192
464
#[Route(path: '/add_parts ' , name: 'project_add_parts_no_id ' )]
193
465
#[Route(path: '/{id}/add_parts ' , name: 'project_add_parts ' , requirements: ['id ' => '\d+ ' ])]
194
466
public function addPart (Request $ request , EntityManagerInterface $ entityManager , ?Project $ project ): Response
195
467
{
196
- if ($ project instanceof Project) {
468
+ if ($ project instanceof Project) {
197
469
$ this ->denyAccessUnlessGranted ('edit ' , $ project );
198
470
} else {
199
471
$ this ->denyAccessUnlessGranted ('@projects.edit ' );
@@ -240,7 +512,7 @@ public function addPart(Request $request, EntityManagerInterface $entityManager,
240
512
241
513
$ data = $ form ->getData ();
242
514
$ bom_entries = $ data ['bom_entries ' ];
243
- foreach ($ bom_entries as $ bom_entry ){
515
+ foreach ($ bom_entries as $ bom_entry ) {
244
516
$ target_project ->addBOMEntry ($ bom_entry );
245
517
}
246
518
0 commit comments