diff --git a/src/commands/generate.command.ts b/src/commands/generate.command.ts index b086a93..dceac0e 100644 --- a/src/commands/generate.command.ts +++ b/src/commands/generate.command.ts @@ -44,12 +44,18 @@ export class GenerateCommand extends CommandRunner { case 'all': console.log('--- Generating Entity ---'); - await this.entityGeneratorService.generate(name); + const columns = await this.entityGeneratorService.generate(name); + + if (!columns) { + console.error('❌ Entity generation failed. Aborting subsequent steps.'); + return; + } + console.log('\n--- Generating CRUD Module ---'); await this.crudGeneratorService.generate(name); console.log('\n✨ All backend files generated successfully! ✨'); console.log('\n--- Generating Angular Files ---'); - await this.angularGeneratorService.generate(name); + await this.angularGeneratorService.generate(name,columns); console.log('\n✨ All files generated successfully! ✨'); break; diff --git a/src/services/angular-generator.service.ts b/src/services/angular-generator.service.ts index af29f30..87979ba 100644 --- a/src/services/angular-generator.service.ts +++ b/src/services/angular-generator.service.ts @@ -6,6 +6,7 @@ import { ModuleUpdaterService } from './module-updater.service'; import * as path from 'path'; import * as fs from 'fs'; import { promises as fsPromises } from 'fs'; +import { TableColumn } from 'typeorm'; interface NamingConvention { [key: string]: string; // This index signature makes it compatible with Record @@ -25,20 +26,31 @@ export class AngularGeneratorService { private readonly moduleUpdaterService: ModuleUpdaterService, ) {} - public async generate(tableName: string): Promise { + public async generate(tableName: string, columns?: TableColumn[]): Promise { console.log(`Generating Angular module for table: ${tableName}...`); const names = this.getNamingConvention(tableName); + + const numericFields = (columns || []) + .filter(c => { + const tsType = this.mapDbToTsType(c.type); + return tsType === 'number' && !c.isPrimary; + }) + .map(c => c.name); + + names['numericFieldsArray'] = JSON.stringify(numericFields); + const config = this.configService.get(); const adminRoot = path.resolve(process.cwd(), config.admin.path); const featureDir = path.join(adminRoot, 'src', 'app', 'features', names.plural); + try { await this.generateModel(names, featureDir); await this.generateService(names, featureDir); await this.generateFilterComponent(names, featureDir); await this.generateListComponent(names, featureDir); - // NEW: Generate the details component await this.generateDetailsComponent(names, featureDir); + await this.generateFormComponent(names, featureDir); const listComponentPath = path.join( featureDir, @@ -51,7 +63,13 @@ export class AngularGeneratorService { featureDir, 'components', `${names.singular}-details`, `${names.singular}-details.component.ts` ); - // Add details route FIRST, as it's more specific (`/products/:id`) + const formCompPath = path.join(featureDir, 'components', `${names.singular}-form`, `${names.singular}-form.component.ts`); + + + await this.moduleUpdaterService.addRouteToAngularApp(`${names.pascal}FormComponent`, formCompPath, `${names.plural}/new`); + + await this.moduleUpdaterService.addRouteToAngularApp(`${names.pascal}FormComponent`, formCompPath, `${names.plural}/:id/edit`); + await this.moduleUpdaterService.addRouteToAngularApp( `${names.pascal}DetailsComponent`, detailsComponentPath, @@ -72,6 +90,16 @@ export class AngularGeneratorService { } } + private async generateFormComponent(names: NamingConvention, featureDir: string) { + const compDir = path.join(featureDir, 'components', `${names.singular}-form`); + const tsContent = this.templateService.render('angular/form.component.ts.tpl', names); + const htmlContent = this.templateService.render('angular/form.component.html.tpl', names); + + await fsPromises.mkdir(compDir, { recursive: true }); + fs.writeFileSync(path.join(compDir, `${names.singular}-form.component.ts`), tsContent); + fs.writeFileSync(path.join(compDir, `${names.singular}-form.component.html`), htmlContent); + } + private async generateDetailsComponent(names: NamingConvention, featureDir: string) { const compDir = path.join(featureDir, 'components', `${names.singular}-details`); const tsContent = this.templateService.render('angular/details.component.ts.tpl', names); @@ -143,4 +171,13 @@ export class AngularGeneratorService { const pascal = this.toPascalCase(text); return pascal.charAt(0).toLowerCase() + pascal.slice(1); } + + private mapDbToTsType(dbType: string): string { + if (dbType.includes('int') || dbType.includes('serial')) return 'number'; + if (['float', 'double', 'decimal', 'numeric', 'real'].includes(dbType)) return 'number'; + if (dbType.includes('char') || dbType.includes('text') || dbType === 'uuid') return 'string'; + if (dbType === 'boolean' || dbType === 'bool') return 'boolean'; + if (dbType.includes('date') || dbType.includes('time')) return 'Date'; + return 'any'; + } } \ No newline at end of file diff --git a/src/services/entity-generator.service.ts b/src/services/entity-generator.service.ts index fd5d014..29ebcbf 100644 --- a/src/services/entity-generator.service.ts +++ b/src/services/entity-generator.service.ts @@ -13,7 +13,7 @@ export class EntityGeneratorService { private readonly moduleUpdaterService: ModuleUpdaterService) {} - public async generate(tableName: string): Promise { + public async generate(tableName: string): Promise { console.log(`Generating entity for table: ${tableName}...`); const config = this.configService.get(); @@ -56,8 +56,11 @@ export class EntityGeneratorService { // Now the call will work correctly await this.moduleUpdaterService.addEntityToTypeOrm(className, outputPath); + return columns; + } catch (error) { console.error('❌ An error occurred:', error.message); + return null; } finally { if (dataSource.isInitialized) { await dataSource.destroy(); diff --git a/src/templates/angular/details.component.html.tpl b/src/templates/angular/details.component.html.tpl index 49cec59..2339bec 100644 --- a/src/templates/angular/details.component.html.tpl +++ b/src/templates/angular/details.component.html.tpl @@ -28,7 +28,7 @@
Back to List - + Edit
diff --git a/src/templates/angular/form.component.html.tpl b/src/templates/angular/form.component.html.tpl new file mode 100644 index 0000000..067f42b --- /dev/null +++ b/src/templates/angular/form.component.html.tpl @@ -0,0 +1,51 @@ + + + +
+
+
+

+ {{ isEditMode ? 'Edit' : 'Create' }} {{title}} +

+ +
+ + +
+ + +
+ Name is required. +
+
+ + +
+ + +
+ + +
+ +
+ + + +
+ Cancel + +
+
+
+
+
\ No newline at end of file diff --git a/src/templates/angular/form.component.ts.tpl b/src/templates/angular/form.component.ts.tpl new file mode 100644 index 0000000..a74c741 --- /dev/null +++ b/src/templates/angular/form.component.ts.tpl @@ -0,0 +1,87 @@ +// dvbooking-cli/src/templates/angular/form.component.ts.tpl + +// Generated by the CLI +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms'; +import { ActivatedRoute, Router, RouterModule } from '@angular/router'; +import { Observable, of } from 'rxjs'; +import { switchMap, tap } from 'rxjs/operators'; +import { {{pascal}} } from '../../models/{{singular}}.model'; +import { {{pascal}}Service } from '../../services/{{singular}}.service'; + +@Component({ + selector: 'app-{{kebab}}-form', + templateUrl: './{{singular}}-form.component.html', + standalone: true, + imports: [CommonModule, ReactiveFormsModule, RouterModule], +}) +export class {{pascal}}FormComponent implements OnInit { + form: FormGroup; + isEditMode = false; + id: number | null = null; + + private numericFields = {{numericFieldsArray}}; + + constructor( + private fb: FormBuilder, + private route: ActivatedRoute, + private router: Router, + private {{camel}}Service: {{pascal}}Service + ) { + this.form = this.fb.group({ + // Add your form controls here. Match them to your model/entity. + name: ['', Validators.required], + price: [null], + is_available: [true], + }); + } + + ngOnInit(): void { + this.route.params.pipe( + tap(params => { + if (params['id']) { + this.isEditMode = true; + this.id = +params['id']; + } + }), + switchMap(() => { + if (this.isEditMode && this.id) { + return this.{{camel}}Service.findOne(this.id); + } + return of(null); + }) + ).subscribe({{camel}} => { + if ({{camel}}) { + this.form.patchValue({{camel}}); + } + }); + } + + onSubmit(): void { + if (this.form.invalid) { + return; + } + + const payload = { ...this.form.value }; + + for (const field of this.numericFields) { + if (payload[field] != null && payload[field] !== '') { + payload[field] = parseFloat(payload[field]); + } + } + + let action$: Observable<{{pascal}}>; + + if (this.isEditMode && this.id) { + action$ = this.{{camel}}Service.update(this.id, payload); + } else { + action$ = this.{{camel}}Service.create(payload); + } + + action$.subscribe({ + next: () => this.router.navigate(['/{{plural}}']), + error: (err) => console.error('Failed to save {{singular}}', err) + }); + } +} \ No newline at end of file diff --git a/src/templates/angular/list.component.html.tpl b/src/templates/angular/list.component.html.tpl index 89350de..fd08c00 100644 --- a/src/templates/angular/list.component.html.tpl +++ b/src/templates/angular/list.component.html.tpl @@ -1,7 +1,7 @@

{{title}}s

- + Create New @@ -24,7 +24,7 @@ View - + Edit