11import { Component , EventEmitter , OnDestroy , OnInit , Output , Input , inject } from '@angular/core' ;
22import { CommonModule } from '@angular/common' ;
3- import { FormBuilder , ReactiveFormsModule , Validators } from '@angular/forms' ;
3+ import { AbstractControl , FormBuilder , ReactiveFormsModule , ValidationErrors , ValidatorFn , Validators } from '@angular/forms' ;
44import { LucideAngularModule , BarcodeIcon , Trash2Icon } from 'lucide-angular' ;
55import { BarcodeService } from './barcode.service' ;
66import { BarcodeRequest , StandardBarcodeType } from './models' ;
77import { Subject , EMPTY } from 'rxjs' ;
88import { catchError , debounceTime , distinctUntilChanged , map , startWith , switchMap , takeUntil } from 'rxjs/operators' ;
99
10+
11+ /** EAN check-digit calculation (Luhn-style, same logic as backend). */
12+ function eanCheckDigit ( digits : string ) : string {
13+ let sum = 0 ;
14+ for ( let i = 0 ; i < digits . length ; i ++ ) {
15+ sum += parseInt ( digits [ i ] , 10 ) * ( i % 2 === 0 ? 1 : 3 ) ;
16+ }
17+ return String ( ( 10 - ( sum % 10 ) ) % 10 ) ;
18+ }
19+
20+ /**
21+ * Returns a ValidatorFn that checks barcode text against the selected type.
22+ * The validator is re-created each time the type changes via setValidators().
23+ */
24+ function barcodeTextValidator ( getType : ( ) => string ) : ValidatorFn {
25+ return ( ctrl : AbstractControl ) : ValidationErrors | null => {
26+ const text : string = ( ctrl . value ?? '' ) . trim ( ) ;
27+ if ( ! text ) return null ; // required is handled by Validators.required
28+ const type = getType ( ) ;
29+
30+ switch ( type ) {
31+ case 'ean13' : {
32+ if ( ! / ^ \d { 12 , 13 } $ / . test ( text ) )
33+ return { barcodeFormat : 'EAN-13: exactly 12 or 13 digits required' } ;
34+ if ( text . length === 13 && eanCheckDigit ( text . slice ( 0 , 12 ) ) !== text [ 12 ] )
35+ return { barcodeFormat : `EAN-13: wrong check digit (expected ${ eanCheckDigit ( text . slice ( 0 , 12 ) ) } )` } ;
36+ return null ;
37+ }
38+ case 'ean8' : {
39+ if ( ! / ^ \d { 7 , 8 } $ / . test ( text ) )
40+ return { barcodeFormat : 'EAN-8: exactly 7 or 8 digits required' } ;
41+ if ( text . length === 8 && eanCheckDigit ( text . slice ( 0 , 7 ) ) !== text [ 7 ] )
42+ return { barcodeFormat : `EAN-8: wrong check digit (expected ${ eanCheckDigit ( text . slice ( 0 , 7 ) ) } )` } ;
43+ return null ;
44+ }
45+ case 'upca' : {
46+ if ( ! / ^ \d { 11 , 12 } $ / . test ( text ) )
47+ return { barcodeFormat : 'UPC-A: exactly 11 or 12 digits required' } ;
48+ if ( text . length === 12 && eanCheckDigit ( text . slice ( 0 , 11 ) ) !== text [ 11 ] )
49+ return { barcodeFormat : `UPC-A: wrong check digit (expected ${ eanCheckDigit ( text . slice ( 0 , 11 ) ) } )` } ;
50+ return null ;
51+ }
52+ case 'itf14' : {
53+ if ( ! / ^ \d { 13 , 14 } $ / . test ( text ) )
54+ return { barcodeFormat : 'ITF-14: exactly 13 or 14 digits required' } ;
55+ return null ;
56+ }
57+ default :
58+ return null ;
59+ }
60+ } ;
61+ }
62+
1063@Component ( {
1164 standalone : true ,
1265 selector : 'app-barcode-editor-item' ,
@@ -26,6 +79,7 @@ export class BarcodeEditorItemComponent implements OnInit, OnDestroy {
2679
2780 previewUrl : string | null = null ;
2881 loading = false ;
82+ errorMsg : string | null = null ;
2983 private destroy$ = new Subject < void > ( ) ;
3084
3185 form = this . fb . group ( {
@@ -37,21 +91,39 @@ export class BarcodeEditorItemComponent implements OnInit, OnDestroy {
3791 } ) ;
3892
3993 ngOnInit ( ) : void {
94+ // Attach type-aware validator and re-run it when the type changes
95+ const textCtrl = this . form . controls [ 'text' ] ;
96+ const getType = ( ) => this . form . controls [ 'type' ] . value ;
97+ textCtrl . addValidators ( barcodeTextValidator ( getType ) ) ;
98+
99+ this . form . controls [ 'type' ] . valueChanges . pipe ( takeUntil ( this . destroy$ ) ) . subscribe ( ( ) => {
100+ textCtrl . updateValueAndValidity ( ) ;
101+ } ) ;
102+
40103 this . form . valueChanges . pipe (
41104 startWith ( this . form . getRawValue ( ) ) ,
42105 map ( ( ) => this . buildReq ( ) ) ,
43106 distinctUntilChanged ( ( a , b ) => JSON . stringify ( a ) === JSON . stringify ( b ) ) ,
44107 debounceTime ( 300 ) ,
45108 switchMap ( req => {
109+ // Show validation error inline instead of firing the request
110+ const formatErr = textCtrl . errors ?. [ 'barcodeFormat' ] ;
111+ if ( formatErr ) {
112+ this . errorMsg = formatErr ;
113+ this . revokePreview ( ) ;
114+ return EMPTY ;
115+ }
116+ this . errorMsg = null ;
46117 if ( ! this . form . valid || ! req . text ) {
47118 this . revokePreview ( ) ;
48119 return EMPTY ;
49120 }
50121 this . loading = true ;
51122 return this . api . preview$ ( req ) . pipe (
52123 catchError ( err => {
53- console . error ( err ) ;
124+ this . errorMsg = err ?. error ?. message ?? err ?. message ?? 'Preview failed' ;
54125 this . revokePreview ( ) ;
126+ this . loading = false ;
55127 return EMPTY ;
56128 } )
57129 ) ;
0 commit comments