import { Component, OnInit, OnDestroy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { MatDialog, MatDialogModule } from '@angular/material/dialog'; import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; import { MatTableModule } from '@angular/material/table'; import { MatProgressBarModule } from '@angular/material/progress-bar'; import { MatButtonModule } from '@angular/material/button'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatButtonToggleModule } from '@angular/material/button-toggle'; import { MatCardModule } from '@angular/material/card'; import { MatChipsModule } from '@angular/material/chips'; import { MatIconModule } from '@angular/material/icon'; import { MatMenuModule } from '@angular/material/menu'; import { MatDividerModule } from '@angular/material/divider'; import { ApiService, Project } from '../../services/api.service'; import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { authInterceptor } from '../../interceptors/auth.interceptor'; import { Subject, takeUntil } from 'rxjs'; // Dynamic imports for dialogs const loadProjectEditDialog = () => import('../../dialogs/project-edit-dialog/project-edit-dialog.component'); const loadVersionEditDialog = () => import('../../dialogs/version-edit-dialog/version-edit-dialog.component'); const loadConfirmDialog = () => import('../../dialogs/confirm-dialog/confirm-dialog.component'); @Component({ selector: 'app-projects', standalone: true, imports: [ CommonModule, FormsModule, HttpClientModule, MatTableModule, MatProgressBarModule, MatButtonModule, MatCheckboxModule, MatFormFieldModule, MatInputModule, MatButtonToggleModule, MatCardModule, MatChipsModule, MatIconModule, MatMenuModule, MatDividerModule, MatDialogModule, MatSnackBarModule ], providers: [ ApiService ], templateUrl: './projects.component.html', styleUrls: ['./projects.component.scss'] }) export class ProjectsComponent implements OnInit, OnDestroy { projects: Project[] = []; filteredProjects: Project[] = []; searchTerm = ''; showDeleted = false; viewMode: 'list' | 'card' = 'card'; loading = false; message = ''; isError = false; // For table view displayedColumns: string[] = ['name', 'caption', 'versions', 'status', 'lastUpdate', 'actions']; // Memory leak prevention private destroyed$ = new Subject(); constructor( private apiService: ApiService, private dialog: MatDialog, private snackBar: MatSnackBar ) {} ngOnInit() { this.loadProjects(); this.loadEnvironment(); } ngOnDestroy() { this.destroyed$.next(); this.destroyed$.complete(); } isSparkTabVisible(): boolean { // Environment bilgisini cache'ten al (eğer varsa) const env = localStorage.getItem('flare_environment'); if (env) { const config = JSON.parse(env); return !config.work_mode?.startsWith('gpt4o'); } return true; // Default olarak göster } loadProjects() { this.loading = true; this.apiService.getProjects(this.showDeleted) .pipe(takeUntil(this.destroyed$)) .subscribe({ next: (projects) => { this.projects = projects || []; this.applyFilter(); this.loading = false; }, error: (error) => { this.loading = false; this.showMessage('Failed to load projects', true); console.error('Load projects error:', error); } }); } private loadEnvironment() { this.apiService.getEnvironment() .pipe(takeUntil(this.destroyed$)) .subscribe({ next: (env) => { localStorage.setItem('flare_environment', JSON.stringify(env)); }, error: (err) => { console.error('Failed to load environment:', err); } }); } applyFilter() { this.filteredProjects = this.projects.filter(project => { const matchesSearch = !this.searchTerm || project.name.toLowerCase().includes(this.searchTerm.toLowerCase()) || (project.caption || '').toLowerCase().includes(this.searchTerm.toLowerCase()); const matchesDeleted = this.showDeleted || !project.deleted; return matchesSearch && matchesDeleted; }); } filterProjects() { this.applyFilter(); } onSearchChange() { this.applyFilter(); } onShowDeletedChange() { this.loadProjects(); } async createProject() { try { const { default: ProjectEditDialogComponent } = await loadProjectEditDialog(); const dialogRef = this.dialog.open(ProjectEditDialogComponent, { width: '500px', data: { mode: 'create' } }); dialogRef.afterClosed() .pipe(takeUntil(this.destroyed$)) .subscribe(result => { if (result) { this.loadProjects(); this.showMessage('Project created successfully', false); } }); } catch (error) { console.error('Failed to load dialog:', error); this.showMessage('Failed to open dialog', true); } } async editProject(project: Project, event?: Event) { if (event) { event.stopPropagation(); } try { const { default: ProjectEditDialogComponent } = await loadProjectEditDialog(); const dialogRef = this.dialog.open(ProjectEditDialogComponent, { width: '500px', data: { mode: 'edit', project: { ...project } } }); dialogRef.afterClosed() .pipe(takeUntil(this.destroyed$)) .subscribe(result => { if (result) { // Listeyi güncelle const index = this.projects.findIndex(p => p.id === result.id); if (index !== -1) { this.projects[index] = result; this.applyFilter(); // Filtreyi yeniden uygula } else { this.loadProjects(); // Bulunamazsa tüm listeyi yenile } this.showMessage('Project updated successfully', false); } }); } catch (error) { console.error('Failed to load dialog:', error); this.showMessage('Failed to open dialog', true); } } toggleProject(project: Project, event?: Event) { if (event) { event.stopPropagation(); } const action = project.enabled ? 'disable' : 'enable'; const confirmMessage = `Are you sure you want to ${action} "${project.caption}"?`; this.confirmAction( `${action.charAt(0).toUpperCase() + action.slice(1)} Project`, confirmMessage, action.charAt(0).toUpperCase() + action.slice(1), !project.enabled ).then(confirmed => { if (confirmed) { this.apiService.toggleProject(project.id) .pipe(takeUntil(this.destroyed$)) .subscribe({ next: (result) => { project.enabled = result.enabled; this.showMessage( `Project ${project.enabled ? 'enabled' : 'disabled'} successfully`, false ); }, error: (error) => this.handleUpdateError(error, project.caption) }); } }); } async manageVersions(project: Project, event?: Event) { if (event) { event.stopPropagation(); } try { const { default: VersionEditDialogComponent } = await loadVersionEditDialog(); const dialogRef = this.dialog.open(VersionEditDialogComponent, { width: '90vw', maxWidth: '1200px', height: '90vh', data: { project } }); dialogRef.afterClosed() .pipe(takeUntil(this.destroyed$)) .subscribe(result => { if (result) { this.loadProjects(); } }); } catch (error) { console.error('Failed to load dialog:', error); this.showMessage('Failed to open dialog', true); } } deleteProject(project: Project, event?: Event) { if (event) { event.stopPropagation(); } const hasVersions = project.versions && project.versions.length > 0; const message = hasVersions ? `Project "${project.name}" has ${project.versions.length} version(s). Are you sure you want to delete it?` : `Are you sure you want to delete project "${project.name}"?`; this.confirmAction('Delete Project', message, 'Delete', true).then(confirmed => { if (confirmed) { this.apiService.deleteProject(project.id) .pipe(takeUntil(this.destroyed$)) .subscribe({ next: () => { this.showMessage('Project deleted successfully', false); this.loadProjects(); }, error: (error) => { const message = error.error?.detail || 'Failed to delete project'; this.showMessage(message, true); } }); } }); } exportProject(project: Project, event?: Event) { if (event) { event.stopPropagation(); } this.apiService.exportProject(project.id) .pipe(takeUntil(this.destroyed$)) .subscribe({ next: (data) => { // Create and download file const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const url = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `${project.name}_export_${new Date().getTime()}.json`; link.click(); window.URL.revokeObjectURL(url); this.showMessage('Project exported successfully', false); }, error: (error) => { this.showMessage('Failed to export project', true); console.error('Export error:', error); } }); } importProject() { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json'; input.onchange = async (event: any) => { const file = event.target.files[0]; if (!file) return; try { const text = await file.text(); const data = JSON.parse(text); this.apiService.importProject(data) .pipe(takeUntil(this.destroyed$)) .subscribe({ next: () => { this.showMessage('Project imported successfully', false); this.loadProjects(); }, error: (error) => { const message = error.error?.detail || 'Failed to import project'; this.showMessage(message, true); } }); } catch (error) { this.showMessage('Invalid file format', true); } }; input.click(); } getPublishedCount(project: Project): number { return project.versions?.filter(v => v.published).length || 0; } getRelativeTime(timestamp: string | undefined): string { if (!timestamp) return 'Never'; const date = new Date(timestamp); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); if (diffMins < 60) return `${diffMins} minutes ago`; if (diffHours < 24) return `${diffHours} hours ago`; if (diffDays < 7) return `${diffDays} days ago`; return date.toLocaleDateString(); } trackByProjectId(index: number, project: Project): number { return project.id; } handleUpdateError(error: any, projectName?: string): void { if (error.status === 409 || error.raceCondition) { const details = error.error?.details || error; const lastUpdateUser = details.last_update_user || error.lastUpdateUser || 'another user'; const lastUpdateDate = details.last_update_date || error.lastUpdateDate; const message = projectName ? `Project "${projectName}" was modified by ${lastUpdateUser}. Please reload.` : `Project was modified by ${lastUpdateUser}. Please reload.`; this.snackBar.open( message, 'Reload', { duration: 0, panelClass: ['error-snackbar', 'race-condition-snackbar'] } ).onAction().subscribe(() => { this.loadProjects(); }); // Log additional info if available if (lastUpdateDate) { console.info(`Last updated at: ${lastUpdateDate}`); } } else { // Generic error handling this.snackBar.open( error.error?.detail || error.message || 'Operation failed', 'Close', { duration: 5000, panelClass: ['error-snackbar'] } ); } } private async confirmAction(title: string, message: string, confirmText: string, dangerous: boolean): Promise { try { const { default: ConfirmDialogComponent } = await loadConfirmDialog(); const dialogRef = this.dialog.open(ConfirmDialogComponent, { width: '400px', data: { title, message, confirmText, confirmColor: dangerous ? 'warn' : 'primary' } }); return await dialogRef.afterClosed().toPromise() || false; } catch (error) { console.error('Failed to load confirm dialog:', error); return false; } } private showMessage(message: string, isError: boolean) { this.message = message; this.isError = isError; setTimeout(() => { this.message = ''; }, 5000); } }