Spaces:
Paused
Paused
| // project-edit-dialog.component.ts | |
| import { Component, Inject, OnInit, OnDestroy } from '@angular/core'; | |
| import { CommonModule } from '@angular/common'; | |
| import { FormBuilder, FormGroup, Validators, ReactiveFormsModule, FormArray } from '@angular/forms'; | |
| import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog'; | |
| import { MatFormFieldModule } from '@angular/material/form-field'; | |
| import { MatInputModule } from '@angular/material/input'; | |
| import { MatSelectModule } from '@angular/material/select'; | |
| import { MatCheckboxModule } from '@angular/material/checkbox'; | |
| import { MatButtonModule } from '@angular/material/button'; | |
| import { MatIconModule } from '@angular/material/icon'; | |
| import { MatChipsModule } from '@angular/material/chips'; | |
| import { MatDividerModule } from '@angular/material/divider'; | |
| import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; | |
| import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; | |
| import { ApiService } from '../../services/api.service'; | |
| import { LocaleManagerService, Locale } from '../../services/locale-manager.service'; | |
| import { Subject, takeUntil } from 'rxjs'; | |
| import { HttpErrorResponse } from '@angular/common/http'; | |
| export interface ProjectDialogData { | |
| mode: 'create' | 'edit'; | |
| project?: any; | |
| } | |
| ({ | |
| selector: 'app-project-edit-dialog', | |
| standalone: true, | |
| imports: [ | |
| CommonModule, | |
| ReactiveFormsModule, | |
| MatDialogModule, | |
| MatFormFieldModule, | |
| MatInputModule, | |
| MatSelectModule, | |
| MatCheckboxModule, | |
| MatButtonModule, | |
| MatIconModule, | |
| MatChipsModule, | |
| MatDividerModule, | |
| MatSnackBarModule, | |
| MatProgressSpinnerModule | |
| ], | |
| template: ` | |
| <h2 mat-dialog-title>{{ data.mode === 'create' ? 'Create New Project' : 'Edit Project' }}</h2> | |
| <mat-dialog-content> | |
| <form [formGroup]="form"> | |
| <mat-form-field appearance="outline" class="full-width"> | |
| <mat-label>Name*</mat-label> | |
| <input matInput formControlName="name" | |
| [readonly]="data.mode === 'edit'" | |
| placeholder="e.g., airline_agent"> | |
| <mat-hint>Use only letters, numbers, and underscores</mat-hint> | |
| <mat-error>{{ getErrorMessage('name') }}</mat-error> | |
| </mat-form-field> | |
| <mat-form-field appearance="outline" class="full-width"> | |
| <mat-label>Caption*</mat-label> | |
| <input matInput formControlName="caption" | |
| placeholder="e.g., Airline Customer Service Agent"> | |
| <mat-error>{{ getErrorMessage('caption') }}</mat-error> | |
| </mat-form-field> | |
| <mat-form-field appearance="outline" class="full-width"> | |
| <mat-label>Icon</mat-label> | |
| <mat-select formControlName="icon"> | |
| @for (icon of projectIcons; track icon) { | |
| <mat-option [value]="icon"> | |
| <mat-icon>{{ icon }}</mat-icon> | |
| {{ icon }} | |
| </mat-option> | |
| } | |
| </mat-select> | |
| </mat-form-field> | |
| <mat-form-field appearance="outline" class="full-width"> | |
| <mat-label>Description</mat-label> | |
| <textarea matInput formControlName="description" rows="3"></textarea> | |
| </mat-form-field> | |
| <!-- Default Locale --> | |
| <mat-form-field appearance="outline" class="full-width"> | |
| <mat-label>Default Locale</mat-label> | |
| <mat-select | |
| formControlName="defaultLocale" | |
| (selectionChange)="onDefaultLocaleChange()"> | |
| @if (loadingLocales) { | |
| <mat-option disabled> | |
| <mat-spinner diameter="20"></mat-spinner> | |
| Loading Locales... | |
| </mat-option> | |
| } | |
| @for (locale of availableLocales; track locale.code) { | |
| <mat-option [value]="locale.code"> <!-- locale.name yerine locale.code --> | |
| {{ locale.name }} | |
| <span class="locale-code">{{ locale.code }}</span> | |
| </mat-option> | |
| } | |
| </mat-select> | |
| <mat-icon matPrefix>translate</mat-icon> | |
| <mat-hint>Primary Locale for this project</mat-hint> | |
| </mat-form-field> | |
| <!-- Supported Locales --> | |
| <mat-form-field appearance="outline" class="full-width"> | |
| <mat-label>Supported Locales</mat-label> | |
| <mat-select | |
| formControlName="supportedLocales" | |
| (selectionChange)="onSupportedLocalesChange()" | |
| multiple> | |
| <mat-select-trigger> | |
| <div class="selected-locales"> | |
| @for (lang of form.get('supportedLocales')?.value || []; track lang; let last = $last) { | |
| <span>{{ getLocaleName(lang) }}@if (!last) {, }</span> | |
| } | |
| </div> | |
| </mat-select-trigger> | |
| @for (locale of availableLocales; track locale.code) { | |
| <mat-option [value]="locale.code"> | |
| {{ locale.name }} | |
| <span class="locale-code">{{ locale.code }}</span> | |
| </mat-option> | |
| } | |
| </mat-select> | |
| <mat-icon matPrefix>locale</mat-icon> | |
| <mat-hint>Locales available in this project</mat-hint> | |
| </mat-form-field> | |
| <mat-form-field appearance="outline" class="full-width"> | |
| <mat-label>Timezone</mat-label> | |
| <mat-select formControlName="timezone"> | |
| @for (tz of timezones; track tz) { | |
| <mat-option [value]="tz">{{ tz }}</mat-option> | |
| } | |
| </mat-select> | |
| </mat-form-field> | |
| <mat-form-field appearance="outline" class="full-width"> | |
| <mat-label>Region</mat-label> | |
| <input matInput formControlName="region" placeholder="e.g., tr-TR"> | |
| </mat-form-field> | |
| </form> | |
| </mat-dialog-content> | |
| <mat-dialog-actions align="end"> | |
| <button mat-button (click)="close()">Cancel</button> | |
| <button mat-raised-button color="primary" | |
| (click)="save()" | |
| [disabled]="form.invalid || saving"> | |
| {{ saving ? 'Saving...' : (data.mode === 'create' ? 'Create' : 'Save') }} | |
| </button> | |
| </mat-dialog-actions> | |
| `, | |
| styleUrls: ['./project-edit-dialog.component.scss'] | |
| }) | |
| export default class ProjectEditDialogComponent implements OnInit, OnDestroy { | |
| form!: FormGroup; | |
| saving = false; | |
| loadingLocales = true; | |
| availableLocales: Locale[] = []; | |
| // Memory leak prevention | |
| private destroyed$ = new Subject<void>(); | |
| projectIcons = ['folder', 'work', 'shopping_cart', 'school', 'local_hospital', 'restaurant', 'home', 'business']; | |
| timezones = [ | |
| 'Europe/Istanbul', | |
| 'Europe/London', | |
| 'Europe/Berlin', | |
| 'America/New_York', | |
| 'America/Los_Angeles', | |
| 'Asia/Tokyo' | |
| ]; | |
| constructor( | |
| private fb: FormBuilder, | |
| private apiService: ApiService, | |
| private localeManager: LocaleManagerService, | |
| private snackBar: MatSnackBar, | |
| public dialogRef: MatDialogRef<ProjectEditDialogComponent>, | |
| (MAT_DIALOG_DATA) public data: ProjectDialogData | |
| ) {} | |
| ngOnInit() { | |
| this.initializeForm(); | |
| this.loadAvailableLocales(); | |
| } | |
| ngOnDestroy() { | |
| this.destroyed$.next(); | |
| this.destroyed$.complete(); | |
| } | |
| initializeForm() { | |
| const defaultValues = this.data.mode === 'edit' && this.data.project ? { | |
| name: this.data.project.name, | |
| caption: this.data.project.caption || '', | |
| icon: this.data.project.icon || 'folder', | |
| description: this.data.project.description || '', | |
| defaultLocale: this.data.project.default_locale || 'tr', | |
| supportedLocales: this.data.project.supported_locales || ['tr'], // Düzeltildi: supportedLolcales -> supportedLocales | |
| timezone: this.data.project.timezone || 'Europe/Istanbul', | |
| region: this.data.project.region || 'tr-TR' | |
| } : { | |
| name: '', | |
| caption: '', | |
| icon: 'folder', | |
| description: '', | |
| defaultLocale: 'tr', | |
| supportedLocales: ['tr'], | |
| timezone: 'Europe/Istanbul', | |
| region: 'tr-TR' | |
| }; | |
| this.form = this.fb.group({ | |
| name: [defaultValues.name, [Validators.required, Validators.pattern(/^[a-z0-9_]+$/)]], | |
| caption: [defaultValues.caption, Validators.required], | |
| icon: [defaultValues.icon], | |
| description: [defaultValues.description], | |
| defaultLocale: [defaultValues.defaultLocale], | |
| supportedLocales: [defaultValues.supportedLocales], | |
| timezone: [defaultValues.timezone], | |
| region: [defaultValues.region] | |
| }); | |
| // Disable name field in edit mode | |
| if (this.data.mode === 'edit') { | |
| this.form.get('name')?.disable(); | |
| } | |
| } | |
| loadAvailableLocales() { | |
| this.loadingLocales = true; | |
| this.localeManager.getAvailableLocales() | |
| .pipe(takeUntil(this.destroyed$)) | |
| .subscribe({ | |
| next: (locales) => { | |
| this.availableLocales = locales; | |
| this.loadingLocales = false; | |
| this.validateSelectedLocales(); | |
| }, | |
| error: (err) => { | |
| this.showMessage('Failed to load available locales', 'error'); | |
| this.loadingLocales = false; | |
| // Use fallback locales | |
| this.availableLocales = [ | |
| { code: 'tr', name: 'Türkçe', english_name: 'Turkish' }, | |
| { code: 'en', name: 'English', english_name: 'English' } | |
| ]; | |
| } | |
| }); | |
| } | |
| validateSelectedLocales() { | |
| const availableCodes = this.availableLocales.map(l => l.code); | |
| const currentSupported = this.form.get('supportedLocales')?.value || []; | |
| const currentDefault = this.form.get('defaultLocale')?.value; | |
| // Filter out any unsupported Locales | |
| const validSupported = currentSupported.filter((lang: string) => | |
| availableCodes.includes(lang) | |
| ); | |
| // Update form if any Locales were removed | |
| if (validSupported.length !== currentSupported.length) { | |
| this.form.patchValue({ supportedLocales: validSupported }); | |
| } | |
| // Ensure default Locale is valid | |
| if (!availableCodes.includes(currentDefault)) { | |
| const newDefault = availableCodes[0] || 'tr-TR'; | |
| this.form.patchValue({ | |
| defaultLocale: newDefault, | |
| supportedLocales: [...validSupported, newDefault] | |
| }); | |
| } | |
| } | |
| onDefaultLocaleChange() { | |
| // Default Locale değiştiğinde bir şey yapmaya gerek yok | |
| // Çünkü default_locale (Türkçe) ve supported_locales (tr-TR) farklı tipte | |
| } | |
| onSupportedLocalesChange() { | |
| // Supported locales değiştiğinde de bir şey yapmaya gerek yok | |
| // En az bir dil seçili olduğu sürece sorun yok | |
| const supportedLocales = this.form.get('supportedLocales')?.value || []; | |
| if (supportedLocales.length === 0) { | |
| // En az bir dil seçilmeli | |
| this.form.patchValue({ | |
| supportedLocales: ['tr-TR'] | |
| }); | |
| } | |
| } | |
| getLocaleName(code: string): string { | |
| // Önce availableLocales'da ara | |
| const locale = this.availableLocales.find(l => l.code === code); | |
| if (locale) { | |
| return locale.name; | |
| } | |
| // Bulamazsan fallback locale isimleri kullan | |
| const localeNames: { [key: string]: string } = { | |
| 'tr': 'Türkçe', | |
| 'tr-TR': 'Türkçe', | |
| 'en': 'English', | |
| 'en-US': 'English', | |
| 'en-GB': 'English (UK)', | |
| 'de': 'Deutsch', | |
| 'de-DE': 'Deutsch', | |
| 'fr': 'Français', | |
| 'fr-FR': 'Français', | |
| 'es': 'Español', | |
| 'es-ES': 'Español', | |
| 'ar': 'العربية', | |
| 'ar-SA': 'العربية', | |
| 'ru': 'Русский', | |
| 'ru-RU': 'Русский', | |
| 'zh': '中文', | |
| 'zh-CN': '中文', | |
| 'ja': '日本語', | |
| 'ja-JP': '日本語', | |
| 'ko': '한국어', | |
| 'ko-KR': '한국어' | |
| }; | |
| return localeNames[code] || code; | |
| } | |
| getErrorMessage(fieldName: string): string { | |
| const control = this.form.get(fieldName); | |
| if (!control) return ''; | |
| if (control.hasError('required')) { | |
| return `${this.getFieldLabel(fieldName)} is required`; | |
| } | |
| if (control.hasError('pattern')) { | |
| return `${this.getFieldLabel(fieldName)} contains invalid characters`; | |
| } | |
| if (control.hasError('server')) { | |
| return control.errors?.['server']; | |
| } | |
| return ''; | |
| } | |
| private getFieldLabel(fieldName: string): string { | |
| const labels: { [key: string]: string } = { | |
| 'name': 'Project Name', | |
| 'caption': 'Caption', | |
| 'description': 'Description', | |
| 'defaultLocale': 'Default Locale', | |
| 'supportedLocales': 'Supported Locales', | |
| 'timezone': 'Timezone', | |
| 'region': 'Region', | |
| 'icon': 'Icon' | |
| }; | |
| return labels[fieldName] || fieldName; | |
| } | |
| handleValidationError(error: HttpErrorResponse): void { | |
| if (error.status === 422 && error.error?.details) { | |
| // Show specific field errors | |
| error.error.details.forEach((detail: any) => { | |
| const control = this.form.get(detail.field); | |
| if (control) { | |
| control.setErrors({ server: detail.message }); | |
| control.markAsTouched(); | |
| } | |
| }); | |
| this.snackBar.open( | |
| 'Please fix the validation errors', | |
| 'Close', | |
| { | |
| duration: 5000, | |
| panelClass: ['error-snackbar'] | |
| } | |
| ); | |
| } else { | |
| // Generic error handling | |
| this.showMessage( | |
| error.error?.detail || error.message || 'Operation failed', | |
| 'error' | |
| ); | |
| } | |
| } | |
| save() { | |
| console.log('Save clicked - Form valid:', this.form.valid, 'Saving:', this.saving); | |
| console.log('Form errors:', this.form.errors); | |
| console.log('Form value:', this.form.value); | |
| if (this.form.invalid || this.saving) { | |
| // Mark all fields as touched to show validation errors | |
| Object.keys(this.form.controls).forEach(key => { | |
| const control = this.form.get(key); | |
| if (control) { | |
| control.markAsTouched(); | |
| if (control.errors) { | |
| console.log(`Field ${key} errors:`, control.errors); | |
| } | |
| } | |
| }); | |
| if (this.form.invalid) { | |
| this.showMessage('Please fill all required fields correctly', 'error'); | |
| } | |
| return; | |
| } | |
| this.saving = true; | |
| const formValue = this.form.getRawValue(); // getRawValue to include disabled fields | |
| // Project data format matching backend expectations | |
| const projectData = { | |
| name: formValue.name, | |
| caption: formValue.caption, | |
| icon: formValue.icon, | |
| description: formValue.description, | |
| default_locale: formValue.defaultLocale, | |
| supported_locales: formValue.supportedLocales, | |
| timezone: formValue.timezone, | |
| region: formValue.region | |
| }; | |
| const saveOperation = this.data.mode === 'create' | |
| ? this.apiService.createProject(projectData) | |
| : this.apiService.updateProject(this.data.project.id, { | |
| ...projectData, | |
| last_update_date: this.data.project.last_update_date || '' | |
| }); | |
| saveOperation | |
| .pipe(takeUntil(this.destroyed$)) | |
| .subscribe({ | |
| next: (result) => { | |
| this.saving = false; | |
| this.showMessage( | |
| this.data.mode === 'create' | |
| ? 'Project created successfully!' | |
| : 'Project updated successfully!' | |
| ); | |
| this.dialogRef.close(result); | |
| }, | |
| error: (error: HttpErrorResponse) => { | |
| this.saving = false; | |
| // Race condition handling | |
| if (error.status === 409) { | |
| const details = error.error?.details || {}; | |
| this.snackBar.open( | |
| `Project was modified by ${details.last_update_user || 'another user'}. Please reload.`, | |
| 'Reload', | |
| { duration: 0 } | |
| ).onAction().subscribe(() => { | |
| this.dialogRef.close('reload'); | |
| }); | |
| } else if (error.status === 422) { | |
| this.handleValidationError(error); | |
| } else { | |
| this.showMessage( | |
| error.error?.detail || 'Operation failed', | |
| 'error' | |
| ); | |
| } | |
| } | |
| }); | |
| } | |
| close() { | |
| this.dialogRef.close(); | |
| } | |
| private showMessage(message: string, type: 'success' | 'error' = 'success') { | |
| this.snackBar.open(message, 'Close', { | |
| duration: 5000, | |
| panelClass: type === 'error' ? ['error-snackbar'] : ['success-snackbar'], | |
| horizontalPosition: 'right', | |
| verticalPosition: 'top' | |
| }); | |
| } | |
| } |