// 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; } @Component({ selector: 'app-project-edit-dialog', standalone: true, imports: [ CommonModule, ReactiveFormsModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatSelectModule, MatCheckboxModule, MatButtonModule, MatIconModule, MatChipsModule, MatDividerModule, MatSnackBarModule, MatProgressSpinnerModule ], template: `

{{ data.mode === 'create' ? 'Create New Project' : 'Edit Project' }}

Name* Use only letters, numbers, and underscores {{ getErrorMessage('name') }} Caption* {{ getErrorMessage('caption') }} Icon @for (icon of projectIcons; track icon) { {{ icon }} {{ icon }} } Description Default Locale @if (loadingLocales) { Loading Locales... } @for (locale of availableLocales; track locale.code) { {{ locale.name }} {{ locale.code }} } translate Primary Locale for this project Supported Locales
@for (lang of form.get('supportedLocales')?.value || []; track lang; let last = $last) { {{ getLocaleName(lang) }}@if (!last) {, } }
@for (locale of availableLocales; track locale.code) { {{ locale.name }} {{ locale.code }} }
locale Locales available in this project
Timezone @for (tz of timezones; track tz) { {{ tz }} } Region
`, 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(); 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, @Inject(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' }); } }