// 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' }}
`,
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'
});
}
}