1172 lines
36 KiB
JavaScript
1172 lines
36 KiB
JavaScript
#!/usr/bin/env node
|
||
|
||
const readline = require('readline');
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const { execSync } = require('child_process');
|
||
|
||
const rl = readline.createInterface({
|
||
input: process.stdin,
|
||
output: process.stdout
|
||
});
|
||
|
||
// 🎨 Cores para console
|
||
const colors = {
|
||
reset: '\x1b[0m',
|
||
bright: '\x1b[1m',
|
||
red: '\x1b[31m',
|
||
green: '\x1b[32m',
|
||
yellow: '\x1b[33m',
|
||
blue: '\x1b[34m',
|
||
magenta: '\x1b[35m',
|
||
cyan: '\x1b[36m'
|
||
};
|
||
|
||
const log = {
|
||
info: (msg) => console.log(`${colors.blue}ℹ️ ${msg}${colors.reset}`),
|
||
success: (msg) => console.log(`${colors.green}✅ ${msg}${colors.reset}`),
|
||
warning: (msg) => console.log(`${colors.yellow}⚠️ ${msg}${colors.reset}`),
|
||
error: (msg) => console.log(`${colors.red}❌ ${msg}${colors.reset}`),
|
||
title: (msg) => console.log(`${colors.cyan}${colors.bright}🚀 ${msg}${colors.reset}\n`)
|
||
};
|
||
|
||
// 📝 Configuração do domínio
|
||
let domainConfig = {
|
||
name: '',
|
||
displayName: '',
|
||
menuPosition: '',
|
||
hasPhotos: false,
|
||
hasSideCard: false,
|
||
hasKilometer: false,
|
||
hasColor: false,
|
||
hasStatus: false,
|
||
remoteSelects: []
|
||
};
|
||
|
||
// 🎯 Função principal
|
||
async function main() {
|
||
try {
|
||
log.title('CRIADOR DE DOMÍNIOS - SISTEMA PRAFROTA');
|
||
|
||
// Verificar pré-requisitos
|
||
await checkPrerequisites();
|
||
|
||
// Coletar informações
|
||
await gatherDomainInfo();
|
||
|
||
// Confirmar configuração
|
||
await confirmConfiguration();
|
||
|
||
// Criar branch para o desenvolvimento
|
||
await createBranch();
|
||
|
||
// Gerar domínio
|
||
await generateDomain();
|
||
|
||
// Compilar e testar
|
||
await compileAndTest();
|
||
|
||
// Commit automático
|
||
await autoCommit();
|
||
|
||
log.success('🎉 DOMÍNIO CRIADO COM SUCESSO!');
|
||
log.success('🚀 Sistema totalmente integrado e funcional!');
|
||
log.info('📝 Próximos passos:');
|
||
log.info(` 1. Testar: http://localhost:4200/app/${domainConfig.name}`);
|
||
log.info(` 2. Push: git push origin feature/domain-${domainConfig.name}`);
|
||
log.info(` 3. Criar PR: Para integrar na branch main`);
|
||
|
||
} catch (error) {
|
||
log.error(`Erro: ${error.message}`);
|
||
process.exit(1);
|
||
} finally {
|
||
rl.close();
|
||
}
|
||
}
|
||
|
||
// ✅ Verificar pré-requisitos
|
||
async function checkPrerequisites() {
|
||
log.info('Verificando pré-requisitos...');
|
||
|
||
// Verificar se está na branch main
|
||
try {
|
||
const currentBranch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();
|
||
if (currentBranch !== 'main') {
|
||
throw new Error(`Você deve estar na branch main. Branch atual: ${currentBranch}`);
|
||
}
|
||
log.success('Branch main ativa');
|
||
} catch (error) {
|
||
throw new Error('Erro ao verificar branch Git');
|
||
}
|
||
|
||
// Verificar configuração Git
|
||
try {
|
||
const userName = execSync('git config user.name', { encoding: 'utf8' }).trim();
|
||
const userEmail = execSync('git config user.email', { encoding: 'utf8' }).trim();
|
||
|
||
if (!userName) {
|
||
throw new Error('Nome do usuário Git não configurado');
|
||
}
|
||
|
||
if (!userEmail || !userEmail.endsWith('@grupopralog.com.br')) {
|
||
throw new Error('Email deve ter domínio @grupopralog.com.br');
|
||
}
|
||
|
||
log.success(`Git configurado: ${userName} <${userEmail}>`);
|
||
} catch (error) {
|
||
throw new Error('Configuração Git inválida. Execute: git config --global user.email "seu.email@grupopralog.com.br"');
|
||
}
|
||
}
|
||
|
||
// 📝 Coletar informações do domínio
|
||
async function gatherDomainInfo() {
|
||
log.title('CONFIGURAÇÃO DO DOMÍNIO');
|
||
|
||
// Nome do domínio
|
||
domainConfig.name = await question('📝 Nome do domínio (singular, minúsculo): ');
|
||
if (!domainConfig.name.match(/^[a-z]+$/)) {
|
||
throw new Error('Nome deve ser singular, minúsculo, sem espaços');
|
||
}
|
||
|
||
domainConfig.displayName = await question('📋 Nome para exibição (plural): ');
|
||
|
||
// Posição no menu
|
||
console.log('\n🧭 Opções de posição no menu:');
|
||
console.log('1. vehicles (após Veículos)');
|
||
console.log('2. drivers (após Motoristas)');
|
||
console.log('3. routes (após Rotas)');
|
||
console.log('4. finances (após Finanças)');
|
||
console.log('5. reports (após Relatórios)');
|
||
console.log('6. settings (após Configurações)');
|
||
|
||
const menuChoice = await question('Escolha a posição (1-6): ');
|
||
const menuOptions = ['', 'vehicles', 'drivers', 'routes', 'finances', 'reports', 'settings'];
|
||
domainConfig.menuPosition = menuOptions[parseInt(menuChoice)] || 'vehicles';
|
||
|
||
// Recursos opcionais
|
||
domainConfig.hasPhotos = await askYesNo('📸 Terá sub-aba de fotos?');
|
||
domainConfig.hasSideCard = await askYesNo('🃏 Terá Side Card (painel lateral)?');
|
||
|
||
// Componentes especializados
|
||
log.info('\n🎨 Componentes especializados:');
|
||
domainConfig.hasKilometer = await askYesNo('🛣️ Terá campo de quilometragem?');
|
||
domainConfig.hasColor = await askYesNo('🎨 Terá campo de cor?');
|
||
domainConfig.hasStatus = await askYesNo('📊 Terá campo de status?');
|
||
|
||
// Remote selects
|
||
const hasRemoteSelects = await askYesNo('🔍 Haverá campos para buscar dados de outras APIs?');
|
||
if (hasRemoteSelects) {
|
||
await gatherRemoteSelects();
|
||
}
|
||
}
|
||
|
||
// 🔍 Coletar informações dos remote selects
|
||
async function gatherRemoteSelects() {
|
||
log.info('\n🔗 Configuração de campos Remote-Select:');
|
||
|
||
while (true) {
|
||
const fieldName = await question('Nome do campo (ou "fim" para terminar): ');
|
||
if (fieldName.toLowerCase() === 'fim') break;
|
||
|
||
console.log('\nOpções de API:');
|
||
console.log('1. drivers (Motoristas)');
|
||
console.log('2. vehicles (Veículos)');
|
||
console.log('3. suppliers (Fornecedores)');
|
||
console.log('4. outro');
|
||
|
||
const apiChoice = await question('Escolha a API (1-4): ');
|
||
const apiOptions = ['', 'drivers', 'vehicles', 'suppliers', 'custom'];
|
||
const apiType = apiOptions[parseInt(apiChoice)] || 'custom';
|
||
|
||
let serviceName = apiType;
|
||
if (apiType === 'custom') {
|
||
serviceName = await question('Nome do service (ex: SuppliersService): ');
|
||
}
|
||
|
||
domainConfig.remoteSelects.push({
|
||
fieldName,
|
||
apiType,
|
||
serviceName
|
||
});
|
||
|
||
log.success(`Campo ${fieldName} adicionado`);
|
||
}
|
||
}
|
||
|
||
// ❓ Perguntar sim/não
|
||
async function askYesNo(prompt) {
|
||
const answer = await question(`${prompt} (s/n): `);
|
||
return answer.toLowerCase().startsWith('s');
|
||
}
|
||
|
||
// 🌿 Criar branch para desenvolvimento
|
||
async function createBranch() {
|
||
const branchName = `feature/domain-${domainConfig.name}`;
|
||
log.title('CRIAÇÃO DE BRANCH');
|
||
|
||
try {
|
||
// Verificar se a branch já existe
|
||
try {
|
||
execSync(`git show-ref --verify --quiet refs/heads/${branchName}`, { stdio: 'ignore' });
|
||
log.warning(`Branch '${branchName}' já existe`);
|
||
|
||
const switchToBranch = await askYesNo('🔄 Deseja mudar para a branch existente?');
|
||
if (switchToBranch) {
|
||
execSync(`git checkout ${branchName}`);
|
||
log.success(`Mudado para branch existente: ${branchName}`);
|
||
} else {
|
||
log.error('Operação cancelada. Escolha um nome diferente ou use a branch existente.');
|
||
process.exit(1);
|
||
}
|
||
} catch (error) {
|
||
// Branch não existe, criar nova
|
||
log.info(`Criando nova branch: ${branchName}`);
|
||
execSync(`git checkout -b ${branchName}`);
|
||
log.success(`Branch criada e ativada: ${branchName}`);
|
||
}
|
||
|
||
log.info(`📝 Descrição da branch: Implementação do domínio ${domainConfig.displayName}`);
|
||
log.info(`🎯 Funcionalidades: ${getFeaturesDescription()}`);
|
||
|
||
} catch (error) {
|
||
throw new Error(`Erro ao criar/mudar branch: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
// 📋 Gerar descrição das funcionalidades
|
||
function getFeaturesDescription() {
|
||
const features = [];
|
||
features.push('CRUD básico');
|
||
if (domainConfig.hasPhotos) features.push('upload de fotos');
|
||
if (domainConfig.hasSideCard) features.push('painel lateral');
|
||
if (domainConfig.hasKilometer) features.push('campo quilometragem');
|
||
if (domainConfig.hasColor) features.push('seleção de cores');
|
||
if (domainConfig.hasStatus) features.push('controle de status');
|
||
if (domainConfig.remoteSelects.length > 0) features.push('integração com APIs');
|
||
|
||
return features.join(', ');
|
||
}
|
||
|
||
// 📋 Confirmar configuração
|
||
async function confirmConfiguration() {
|
||
log.title('CONFIRMAÇÃO DA CONFIGURAÇÃO');
|
||
|
||
console.log(`📝 Nome: ${domainConfig.name}`);
|
||
console.log(`📋 Exibição: ${domainConfig.displayName}`);
|
||
console.log(`🧭 Menu: após ${domainConfig.menuPosition}`);
|
||
console.log(`📸 Fotos: ${domainConfig.hasPhotos ? 'Sim' : 'Não'}`);
|
||
console.log(`🃏 Side Card: ${domainConfig.hasSideCard ? 'Sim' : 'Não'}`);
|
||
console.log(`🛣️ Quilometragem: ${domainConfig.hasKilometer ? 'Sim' : 'Não'}`);
|
||
console.log(`🎨 Cor: ${domainConfig.hasColor ? 'Sim' : 'Não'}`);
|
||
console.log(`📊 Status: ${domainConfig.hasStatus ? 'Sim' : 'Não'}`);
|
||
|
||
if (domainConfig.remoteSelects.length > 0) {
|
||
console.log(`🔗 Remote Selects: ${domainConfig.remoteSelects.map(rs => rs.fieldName).join(', ')}`);
|
||
}
|
||
|
||
console.log(`\n🌿 Branch: feature/domain-${domainConfig.name}`);
|
||
console.log(`🎯 Funcionalidades: ${getFeaturesDescription()}`);
|
||
|
||
const confirm = await askYesNo('\n✅ Confirma a criação do domínio?');
|
||
if (!confirm) {
|
||
log.error('Operação cancelada pelo usuário');
|
||
process.exit(0);
|
||
}
|
||
}
|
||
|
||
// 🛠️ Gerar domínio
|
||
async function generateDomain() {
|
||
log.info('Gerando estrutura do domínio...');
|
||
|
||
// Criar diretório
|
||
const domainPath = `projects/idt_app/src/app/domain/${domainConfig.name}`;
|
||
if (!fs.existsSync(domainPath)) {
|
||
fs.mkdirSync(domainPath, { recursive: true });
|
||
}
|
||
|
||
// Gerar arquivos
|
||
await generateComponent();
|
||
await generateService();
|
||
await generateInterface();
|
||
await generateTemplate();
|
||
await generateStyles();
|
||
await updateRouting();
|
||
await updateSidebar();
|
||
await updateMCP();
|
||
|
||
log.success('Estrutura gerada com sucesso!');
|
||
}
|
||
|
||
// 📄 Gerar component
|
||
async function generateComponent() {
|
||
const componentName = capitalize(domainConfig.name);
|
||
const template = generateComponentTemplate(componentName);
|
||
|
||
const filePath = `projects/idt_app/src/app/domain/${domainConfig.name}/${domainConfig.name}.component.ts`;
|
||
fs.writeFileSync(filePath, template);
|
||
|
||
log.success(`${componentName}Component criado`);
|
||
}
|
||
|
||
// 🔧 Template do componente
|
||
function generateComponentTemplate(componentName) {
|
||
const className = `${componentName}Component`;
|
||
const interfaceName = componentName;
|
||
const serviceName = `${componentName}Service`;
|
||
|
||
return `import { Component, ChangeDetectorRef } from "@angular/core";
|
||
import { CommonModule, DatePipe } from "@angular/common";
|
||
|
||
import { TitleService } from "../../shared/services/theme/title.service";
|
||
import { HeaderActionsService } from "../../shared/services/header-actions.service";
|
||
import { ${serviceName} } from "./${domainConfig.name}.service";
|
||
import { ${interfaceName} } from "./${domainConfig.name}.interface";
|
||
|
||
import { TabSystemComponent } from "../../shared/components/tab-system/tab-system.component";
|
||
import { BaseDomainComponent, DomainConfig } from "../../shared/components/base-domain/base-domain.component";
|
||
import { TabFormConfig } from "../../shared/interfaces/generic-tab-form.interface";
|
||
import { TabFormConfigService } from "../../shared/components/tab-system/services/tab-form-config.service";
|
||
|
||
/**
|
||
* 🎯 ${className} - Gestão de ${domainConfig.displayName}
|
||
*
|
||
* ✨ Implementa BaseDomainComponent + Registry Pattern
|
||
* 🚀 Auto-registro de configurações para escalabilidade infinita!
|
||
*/
|
||
@Component({
|
||
selector: 'app-${domainConfig.name}',
|
||
standalone: true,
|
||
imports: [CommonModule, TabSystemComponent],
|
||
providers: [DatePipe],
|
||
templateUrl: './${domainConfig.name}.component.html',
|
||
styleUrl: './${domainConfig.name}.component.scss'
|
||
})
|
||
export class ${className} extends BaseDomainComponent<${interfaceName}> {
|
||
|
||
constructor(
|
||
private ${domainConfig.name}Service: ${serviceName},
|
||
titleService: TitleService,
|
||
headerActionsService: HeaderActionsService,
|
||
cdr: ChangeDetectorRef,
|
||
private datePipe: DatePipe,
|
||
private tabFormConfigService: TabFormConfigService
|
||
) {
|
||
super(titleService, headerActionsService, cdr, ${domainConfig.name}Service);
|
||
this.registerFormConfig();
|
||
}
|
||
|
||
private registerFormConfig(): void {
|
||
this.tabFormConfigService.registerFormConfig('${domainConfig.name}', () => this.getFormConfig());
|
||
}
|
||
|
||
protected override getDomainConfig(): DomainConfig {
|
||
return {
|
||
domain: '${domainConfig.name}',
|
||
title: '${domainConfig.displayName}',
|
||
entityName: '${domainConfig.name}',
|
||
pageSize: 50,
|
||
subTabs: ['dados'${domainConfig.hasPhotos ? ", 'photos'" : ''}],
|
||
columns: [
|
||
{ field: "id", header: "Id", sortable: true, filterable: true, search: true, searchType: "number" },
|
||
{ field: "name", header: "Nome", sortable: true, filterable: true, search: true, searchType: "text" },
|
||
${domainConfig.hasStatus ? `{
|
||
field: "status",
|
||
header: "Status",
|
||
sortable: true,
|
||
filterable: true,
|
||
allowHtml: true,
|
||
search: true,
|
||
searchType: "select",
|
||
searchOptions: [
|
||
{ value: 'active', label: 'Ativo' },
|
||
{ value: 'inactive', label: 'Inativo' }
|
||
],
|
||
label: (value: any) => {
|
||
const statusConfig: { [key: string]: { label: string, class: string } } = {
|
||
'active': { label: 'Ativo', class: 'status-active' },
|
||
'inactive': { label: 'Inativo', class: 'status-inactive' }
|
||
};
|
||
const config = statusConfig[value?.toLowerCase()] || { label: value, class: 'status-unknown' };
|
||
return \`<span class="status-badge \${config.class}">\${config.label}</span>\`;
|
||
}
|
||
},` : ''}
|
||
{
|
||
field: "created_at",
|
||
header: "Criado em",
|
||
sortable: true,
|
||
filterable: true,
|
||
search: true,
|
||
searchType: "date",
|
||
label: (date: any) => this.datePipe.transform(date, "dd/MM/yyyy HH:mm") || "-"
|
||
}
|
||
]${domainConfig.hasSideCard ? `,
|
||
sideCard: {
|
||
enabled: true,
|
||
title: "Resumo do ${componentName}",
|
||
position: "right",
|
||
width: "400px",
|
||
component: "summary",
|
||
data: {
|
||
displayFields: [
|
||
{
|
||
key: "name",
|
||
label: "Nome",
|
||
type: "text"
|
||
},
|
||
${domainConfig.hasStatus ? `{
|
||
key: "status",
|
||
label: "Status",
|
||
type: "status"
|
||
},` : ''}
|
||
{
|
||
key: "created_at",
|
||
label: "Criado em",
|
||
type: "date"
|
||
}
|
||
],
|
||
statusField: "status"
|
||
}
|
||
}` : ''}
|
||
};
|
||
}
|
||
|
||
getFormConfig(): TabFormConfig {
|
||
return {
|
||
title: 'Dados do ${componentName}',
|
||
entityType: '${domainConfig.name}',
|
||
fields: [],
|
||
submitLabel: 'Salvar ${componentName}',
|
||
showCancelButton: true,
|
||
subTabs: [
|
||
{
|
||
id: 'dados',
|
||
label: 'Dados Básicos',
|
||
icon: 'fa-info-circle',
|
||
enabled: true,
|
||
order: 1,
|
||
templateType: 'fields',
|
||
requiredFields: ['name'],
|
||
fields: [
|
||
{
|
||
key: 'name',
|
||
label: 'Nome',
|
||
type: 'text',
|
||
required: true,
|
||
placeholder: 'Digite o nome'
|
||
}${domainConfig.hasKilometer ? `,
|
||
{
|
||
key: 'odometer',
|
||
label: 'Quilometragem',
|
||
type: 'kilometer-input',
|
||
placeholder: '0',
|
||
formatOptions: {
|
||
locale: 'pt-BR',
|
||
useGrouping: true,
|
||
suffix: ' km'
|
||
}
|
||
}` : ''}${domainConfig.hasColor ? `,
|
||
{
|
||
key: 'color',
|
||
label: 'Cor',
|
||
type: 'color-input',
|
||
required: false,
|
||
options: [
|
||
{ value: { name: 'Branco', code: '#ffffff' }, label: 'Branco' },
|
||
{ value: { name: 'Preto', code: '#000000' }, label: 'Preto' },
|
||
{ value: { name: 'Azul', code: '#0000ff' }, label: 'Azul' },
|
||
{ value: { name: 'Vermelho', code: '#ff0000' }, label: 'Vermelho' }
|
||
]
|
||
}` : ''}${domainConfig.hasStatus ? `,
|
||
{
|
||
key: 'status',
|
||
label: 'Status',
|
||
type: 'select',
|
||
required: true,
|
||
options: [
|
||
{ value: 'active', label: 'Ativo' },
|
||
{ value: 'inactive', label: 'Inativo' }
|
||
]
|
||
}` : ''}${generateRemoteSelectFields()}
|
||
]
|
||
}${domainConfig.hasPhotos ? `,
|
||
{
|
||
id: 'photos',
|
||
label: 'Fotos',
|
||
icon: 'fa-camera',
|
||
enabled: true,
|
||
order: 2,
|
||
templateType: 'fields',
|
||
requiredFields: [],
|
||
fields: [
|
||
{
|
||
key: 'photoIds',
|
||
label: 'Fotos',
|
||
type: 'send-image',
|
||
required: false,
|
||
imageConfiguration: {
|
||
maxImages: 10,
|
||
maxSizeMb: 5,
|
||
allowedTypes: ['image/jpeg', 'image/png', 'image/webp'],
|
||
existingImages: []
|
||
}
|
||
}
|
||
]
|
||
}` : ''}
|
||
]
|
||
};
|
||
}
|
||
|
||
protected override getNewEntityData(): Partial<${interfaceName}> {
|
||
return {
|
||
name: '',
|
||
${domainConfig.hasStatus ? "status: 'active'," : ''}
|
||
${domainConfig.hasKilometer ? "odometer: 0," : ''}
|
||
${domainConfig.hasColor ? "color: { name: '', code: '#ffffff' }," : ''}
|
||
};
|
||
}
|
||
}`;
|
||
}
|
||
|
||
// 🔗 Gerar campos remote-select
|
||
function generateRemoteSelectFields() {
|
||
if (domainConfig.remoteSelects.length === 0) return '';
|
||
|
||
return domainConfig.remoteSelects.map(rs => `
|
||
{
|
||
key: '${rs.fieldName}',
|
||
label: '${capitalize(rs.fieldName)}',
|
||
type: 'remote-select',
|
||
remoteConfig: {
|
||
service: this.${rs.serviceName.toLowerCase()},
|
||
searchField: 'name',
|
||
displayField: 'name',
|
||
valueField: 'id',
|
||
modalTitle: 'Selecionar ${capitalize(rs.fieldName)}',
|
||
placeholder: 'Digite para buscar...'
|
||
}
|
||
}`).join(',');
|
||
}
|
||
|
||
// 🛠️ Gerar service, interface, template, styles...
|
||
async function generateService() {
|
||
const serviceName = `${capitalize(domainConfig.name)}Service`;
|
||
const interfaceName = capitalize(domainConfig.name);
|
||
|
||
const template = `import { Injectable } from '@angular/core';
|
||
import { Observable, map } from 'rxjs';
|
||
import { Papa } from 'ngx-papaparse';
|
||
import { ApiClientService } from '../../shared/services/api/api-client.service';
|
||
import { ${interfaceName} } from './${domainConfig.name}.interface';
|
||
import { PaginatedResponse } from '../../shared/interfaces/paginate.interface';
|
||
import { DomainService } from '../../shared/components/base-domain/base-domain.component';
|
||
|
||
@Injectable({
|
||
providedIn: 'root'
|
||
})
|
||
export class ${serviceName} implements DomainService<${interfaceName}> {
|
||
|
||
constructor(
|
||
private apiClient: ApiClientService,
|
||
private papa: Papa
|
||
) {}
|
||
|
||
get${interfaceName}s(
|
||
page = 1,
|
||
limit = 10,
|
||
filters?: {[key: string]: string}
|
||
): Observable<PaginatedResponse<${interfaceName}>> {
|
||
|
||
let url = \`${domainConfig.name}?page=\${page}&limit=\${limit}\`;
|
||
|
||
if (filters) {
|
||
const params = new URLSearchParams();
|
||
|
||
for (const [key, value] of Object.entries(filters)) {
|
||
if (value) {
|
||
params.append(key, value);
|
||
}
|
||
}
|
||
|
||
url += \`&\${params.toString()}\`;
|
||
}
|
||
|
||
return this.apiClient.get<PaginatedResponse<${interfaceName}>>(url);
|
||
}
|
||
|
||
/**
|
||
* Busca um ${domainConfig.name} específico por ID
|
||
*/
|
||
getById(id: string | number): Observable<${interfaceName}> {
|
||
return this.apiClient.get<${interfaceName}>(\`${domainConfig.name}/\${id}\`);
|
||
}
|
||
|
||
/**
|
||
* Remove um ${domainConfig.name}
|
||
*/
|
||
delete(id: string | number): Observable<void> {
|
||
return this.apiClient.delete<void>(\`${domainConfig.name}/\${id}\`);
|
||
}
|
||
|
||
// ========================================
|
||
// 🎯 MÉTODOS ESPERADOS PELO BaseDomainComponent
|
||
// ========================================
|
||
|
||
/**
|
||
* ✅ Método genérico para listar - chamado automaticamente pelo BaseDomainComponent
|
||
*/
|
||
getEntities(page: number, pageSize: number, filters: any): Observable<{
|
||
data: ${interfaceName}[];
|
||
totalCount: number;
|
||
pageCount: number;
|
||
currentPage: number;
|
||
}> {
|
||
return this.get${interfaceName}s(page, pageSize, filters).pipe(
|
||
map(response => ({
|
||
data: response.data,
|
||
totalCount: response.totalCount,
|
||
pageCount: response.pageCount,
|
||
currentPage: response.currentPage
|
||
}))
|
||
);
|
||
}
|
||
|
||
/**
|
||
* ✅ Método genérico para criar - chamado automaticamente pelo BaseDomainComponent
|
||
*/
|
||
create(data: any): Observable<${interfaceName}> {
|
||
return this.apiClient.post<${interfaceName}>('${domainConfig.name}', data);
|
||
}
|
||
|
||
/**
|
||
* ✅ Método genérico para atualizar - chamado automaticamente pelo BaseDomainComponent
|
||
*/
|
||
update(id: any, data: any): Observable<${interfaceName}> {
|
||
return this.apiClient.patch<${interfaceName}>(\`${domainConfig.name}/\${id}\`, data);
|
||
}
|
||
}`;
|
||
|
||
const filePath = `projects/idt_app/src/app/domain/${domainConfig.name}/${domainConfig.name}.service.ts`;
|
||
fs.writeFileSync(filePath, template);
|
||
|
||
log.success(`${serviceName} criado`);
|
||
}
|
||
|
||
async function generateInterface() {
|
||
const interfaceName = capitalize(domainConfig.name);
|
||
|
||
const template = `export interface ${interfaceName} {
|
||
id: number;
|
||
name: string;
|
||
${domainConfig.hasStatus ? "status: 'active' | 'inactive';" : ''}
|
||
${domainConfig.hasKilometer ? "odometer?: number;" : ''}
|
||
${domainConfig.hasColor ? "color?: { name: string; code: string };" : ''}
|
||
${domainConfig.remoteSelects.map(rs => `${rs.fieldName}_id?: number;`).join('\n ')}
|
||
created_at?: string;
|
||
updated_at?: string;
|
||
}`;
|
||
|
||
const filePath = `projects/idt_app/src/app/domain/${domainConfig.name}/${domainConfig.name}.interface.ts`;
|
||
fs.writeFileSync(filePath, template);
|
||
|
||
log.success(`Interface ${interfaceName} criada`);
|
||
}
|
||
|
||
async function generateTemplate() {
|
||
const template = `<div class="domain-container">
|
||
<div class="main-content">
|
||
<app-tab-system
|
||
#tabSystem
|
||
[config]="tabConfig"
|
||
[events]="tabEvents"
|
||
(tableEvent)="onTableEvent($event)">
|
||
</app-tab-system>
|
||
</div>
|
||
</div>`;
|
||
|
||
const filePath = `projects/idt_app/src/app/domain/${domainConfig.name}/${domainConfig.name}.component.html`;
|
||
fs.writeFileSync(filePath, template);
|
||
|
||
log.success('Template HTML criado');
|
||
}
|
||
|
||
async function generateStyles() {
|
||
const template = `// 🎨 Estilos específicos do componente ${capitalize(domainConfig.name)}
|
||
// ERP SaaS - Sistema PraFrota
|
||
|
||
.domain-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
background-color: var(--background-color, #f8f9fa);
|
||
|
||
.main-content {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
|
||
app-tab-system {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 📱 Responsividade para ERP
|
||
@media (max-width: 768px) {
|
||
.domain-container {
|
||
padding: 0.5rem;
|
||
|
||
.main-content {
|
||
height: calc(100vh - 1rem);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 🎯 Classes específicas do domínio
|
||
.${domainConfig.name}-specific {
|
||
// Estilos específicos do ${domainConfig.name} aqui
|
||
}
|
||
|
||
${domainConfig.hasStatus ? `
|
||
// 📊 Status badges para ERP
|
||
.status-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
padding: 4px 12px;
|
||
border-radius: 16px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
border: 1px solid transparent;
|
||
transition: all 0.2s ease;
|
||
|
||
&.status-active {
|
||
background-color: #d4edda;
|
||
color: #155724;
|
||
border-color: #c3e6cb;
|
||
|
||
&::before {
|
||
content: '●';
|
||
margin-right: 4px;
|
||
color: #28a745;
|
||
}
|
||
}
|
||
|
||
&.status-inactive {
|
||
background-color: #f8d7da;
|
||
color: #721c24;
|
||
border-color: #f5c6cb;
|
||
|
||
&::before {
|
||
content: '●';
|
||
margin-right: 4px;
|
||
color: #dc3545;
|
||
}
|
||
}
|
||
|
||
&.status-unknown {
|
||
background-color: #e2e3e5;
|
||
color: #383d41;
|
||
border-color: #d6d8db;
|
||
|
||
&::before {
|
||
content: '●';
|
||
margin-right: 4px;
|
||
color: #6c757d;
|
||
}
|
||
}
|
||
}` : ''}
|
||
|
||
${domainConfig.hasColor ? `
|
||
// 🎨 Color display para ERP
|
||
.color-display {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
|
||
.color-circle {
|
||
width: 20px;
|
||
height: 20px;
|
||
border-radius: 50%;
|
||
border: 2px solid #fff;
|
||
box-shadow: 0 0 0 1px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.color-name {
|
||
font-size: 13px;
|
||
color: var(--text-color, #495057);
|
||
}
|
||
}` : ''}
|
||
|
||
// 🏢 ERP Theme compatibility
|
||
:host {
|
||
display: block;
|
||
height: 100%;
|
||
|
||
// CSS Variables para temas
|
||
--primary-color: #007bff;
|
||
--secondary-color: #6c757d;
|
||
--success-color: #28a745;
|
||
--danger-color: #dc3545;
|
||
--warning-color: #ffc107;
|
||
--info-color: #17a2b8;
|
||
--light-color: #f8f9fa;
|
||
--dark-color: #343a40;
|
||
}
|
||
|
||
// 🎯 Print styles para relatórios ERP
|
||
@media print {
|
||
.domain-container {
|
||
background: white !important;
|
||
|
||
.main-content {
|
||
overflow: visible !important;
|
||
}
|
||
|
||
.status-badge {
|
||
border: 1px solid #000 !important;
|
||
background: white !important;
|
||
color: black !important;
|
||
}
|
||
}
|
||
}`;
|
||
|
||
const filePath = `projects/idt_app/src/app/domain/${domainConfig.name}/${domainConfig.name}.component.scss`;
|
||
fs.writeFileSync(filePath, template);
|
||
|
||
log.success('Estilos SCSS criados');
|
||
}
|
||
|
||
// 🔄 Atualizar arquivos do sistema
|
||
async function updateRouting() {
|
||
log.info('Atualizando sistema de rotas...');
|
||
|
||
const routesPath = 'projects/idt_app/src/app/app.routes.ts';
|
||
|
||
try {
|
||
let routesContent = fs.readFileSync(routesPath, 'utf8');
|
||
|
||
// Encontrar a posição de inserção baseada na menuPosition
|
||
const newRoute = ` {
|
||
path: '${domainConfig.name}',
|
||
loadComponent: () => import('./domain/${domainConfig.name}/${domainConfig.name}.component')
|
||
.then(m => m.${capitalize(domainConfig.name)}Component)
|
||
},`;
|
||
|
||
// Inserir a nova rota na posição apropriada
|
||
const routePattern = /children: \[([\s\S]*?)\]/;
|
||
const match = routesContent.match(routePattern);
|
||
|
||
if (match) {
|
||
const childrenContent = match[1];
|
||
const insertPosition = findInsertPosition(childrenContent, domainConfig.menuPosition);
|
||
|
||
// Dividir o conteúdo das rotas filhas
|
||
const lines = childrenContent.split('\n');
|
||
let insertIndex = -1;
|
||
|
||
// Encontrar o índice de inserção baseado na posição do menu
|
||
for (let i = 0; i < lines.length; i++) {
|
||
if (lines[i].includes(`path: '${domainConfig.menuPosition}'`)) {
|
||
// Inserir após a rota de referência
|
||
insertIndex = i + 3; // path, loadComponent, then
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (insertIndex === -1) {
|
||
// Se não encontrou a posição de referência, inserir antes do último item (redirect)
|
||
for (let i = lines.length - 1; i >= 0; i--) {
|
||
if (lines[i].includes("redirectTo:")) {
|
||
insertIndex = i - 1;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (insertIndex > -1) {
|
||
lines.splice(insertIndex, 0, newRoute);
|
||
const newChildrenContent = lines.join('\n');
|
||
routesContent = routesContent.replace(routePattern, `children: [${newChildrenContent}]`);
|
||
|
||
fs.writeFileSync(routesPath, routesContent);
|
||
log.success('Rota adicionada ao sistema de roteamento');
|
||
} else {
|
||
log.warning('Posição de inserção não encontrada - rota deve ser adicionada manualmente');
|
||
}
|
||
}
|
||
} catch (error) {
|
||
log.warning(`Erro ao atualizar rotas: ${error.message} - será atualizado manualmente`);
|
||
}
|
||
}
|
||
|
||
async function updateSidebar() {
|
||
log.info('Atualizando menu da sidebar...');
|
||
|
||
const sidebarPath = 'projects/idt_app/src/app/shared/components/sidebar/sidebar.component.ts';
|
||
|
||
try {
|
||
let sidebarContent = fs.readFileSync(sidebarPath, 'utf8');
|
||
|
||
// Criar a nova entrada do menu
|
||
const newMenuItem = ` { id: '${domainConfig.name}', label: '${domainConfig.displayName}', icon: 'fas fa-${getIconForDomain(domainConfig.name)}', notifications: 0 },`;
|
||
|
||
// Encontrar a posição de inserção no array menuItems
|
||
const menuPattern = /menuItems: MenuItem\[\] = \[([\s\S]*?)\]/;
|
||
const match = sidebarContent.match(menuPattern);
|
||
|
||
if (match) {
|
||
const menuContent = match[1];
|
||
const lines = menuContent.split('\n');
|
||
let insertIndex = -1;
|
||
|
||
// Encontrar posição baseada na menuPosition
|
||
for (let i = 0; i < lines.length; i++) {
|
||
if (lines[i].includes(`id: '${domainConfig.menuPosition}'`)) {
|
||
// Inserir após o item de referência
|
||
insertIndex = findMenuItemEnd(lines, i) + 1;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (insertIndex === -1) {
|
||
// Se não encontrou a posição de referência, inserir antes do último item
|
||
for (let i = lines.length - 1; i >= 0; i--) {
|
||
if (lines[i].trim().startsWith("{ id:") && !lines[i].includes("children")) {
|
||
insertIndex = i + 1;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (insertIndex > -1) {
|
||
lines.splice(insertIndex, 0, newMenuItem);
|
||
const newMenuContent = lines.join('\n');
|
||
sidebarContent = sidebarContent.replace(menuPattern, `menuItems: MenuItem[] = [${newMenuContent}]`);
|
||
|
||
fs.writeFileSync(sidebarPath, sidebarContent);
|
||
log.success('Menu adicionado à sidebar');
|
||
} else {
|
||
log.warning('Posição de inserção na sidebar não encontrada - deve ser adicionado manualmente');
|
||
}
|
||
}
|
||
} catch (error) {
|
||
log.warning(`Erro ao atualizar sidebar: ${error.message} - será atualizado manualmente`);
|
||
}
|
||
}
|
||
|
||
async function updateMCP() {
|
||
log.info('Atualizando configuração MCP...');
|
||
|
||
const mcpPath = '.mcp/config.json';
|
||
|
||
try {
|
||
const mcpContent = JSON.parse(fs.readFileSync(mcpPath, 'utf8'));
|
||
|
||
// Adicionar contexto do novo domínio
|
||
const domainContext = {
|
||
description: `Domain ${domainConfig.displayName} - Generated automatically`,
|
||
files: [
|
||
`projects/idt_app/src/app/domain/${domainConfig.name}/${domainConfig.name}.component.ts`,
|
||
`projects/idt_app/src/app/domain/${domainConfig.name}/${domainConfig.name}.service.ts`,
|
||
`projects/idt_app/src/app/domain/${domainConfig.name}/${domainConfig.name}.interface.ts`
|
||
],
|
||
features: [
|
||
"BaseDomainComponent integration",
|
||
"Registry Pattern auto-registration",
|
||
"CRUD operations",
|
||
"Data table with filters and pagination",
|
||
domainConfig.hasPhotos ? "Photo upload sub-tab" : null,
|
||
domainConfig.hasSideCard ? "Side card panel" : null,
|
||
domainConfig.hasKilometer ? "Kilometer input component" : null,
|
||
domainConfig.hasColor ? "Color input component" : null,
|
||
domainConfig.hasStatus ? "Status badges" : null
|
||
].filter(Boolean),
|
||
apis: domainConfig.remoteSelects.map(rs => ({
|
||
field: rs.fieldName,
|
||
service: rs.serviceName,
|
||
type: rs.apiType
|
||
})),
|
||
generated: new Date().toISOString(),
|
||
menuPosition: domainConfig.menuPosition
|
||
};
|
||
|
||
// Adicionar ao contexto domains
|
||
if (!mcpContent.contexts.domains) {
|
||
mcpContent.contexts.domains = {};
|
||
}
|
||
|
||
mcpContent.contexts.domains[domainConfig.name] = domainContext;
|
||
|
||
// Atualizar array de domínios na seção recent-improvements
|
||
if (mcpContent.contexts["recent-improvements"] && mcpContent.contexts["recent-improvements"]["automatic-domain-generation"]) {
|
||
if (!mcpContent.contexts["recent-improvements"]["automatic-domain-generation"]["generated-domains"]) {
|
||
mcpContent.contexts["recent-improvements"]["automatic-domain-generation"]["generated-domains"] = [];
|
||
}
|
||
|
||
mcpContent.contexts["recent-improvements"]["automatic-domain-generation"]["generated-domains"].push({
|
||
name: domainConfig.name,
|
||
displayName: domainConfig.displayName,
|
||
generatedAt: new Date().toISOString(),
|
||
features: domainContext.features
|
||
});
|
||
}
|
||
|
||
fs.writeFileSync(mcpPath, JSON.stringify(mcpContent, null, 2));
|
||
log.success('Configuração MCP atualizada');
|
||
|
||
} catch (error) {
|
||
log.warning(`Erro ao atualizar MCP: ${error.message} - será atualizado manualmente`);
|
||
}
|
||
}
|
||
|
||
// Compilar e testar automaticamente
|
||
async function compileAndTest() {
|
||
log.title('COMPILAÇÃO E TESTES AUTOMÁTICOS');
|
||
|
||
try {
|
||
log.info('Compilando aplicação...');
|
||
execSync('ng build idt_app --configuration development', {
|
||
stdio: 'inherit',
|
||
timeout: 120000 // 2 minutos timeout
|
||
});
|
||
log.success('Compilação realizada com sucesso! ✨');
|
||
|
||
// Opcional: executar testes se existirem
|
||
try {
|
||
log.info('Verificando se há testes para executar...');
|
||
const testCommand = `ng test idt_app --watch=false --browsers=ChromeHeadless`;
|
||
execSync(testCommand, {
|
||
stdio: 'inherit',
|
||
timeout: 60000 // 1 minuto timeout
|
||
});
|
||
log.success('Testes executados com sucesso! 🧪');
|
||
} catch (testError) {
|
||
log.warning('Testes não executados (podem não existir ou estar configurados)');
|
||
}
|
||
|
||
} catch (error) {
|
||
log.error(`Erro na compilação: ${error.message}`);
|
||
throw new Error('Falha na compilação - verifique os erros acima');
|
||
}
|
||
}
|
||
|
||
// Commit automático
|
||
async function autoCommit() {
|
||
log.title('COMMIT AUTOMÁTICO');
|
||
|
||
try {
|
||
const branchName = `feature/domain-${domainConfig.name}`;
|
||
|
||
// Adicionar todos os arquivos
|
||
execSync('git add .', { stdio: 'inherit' });
|
||
|
||
// Criar mensagem de commit
|
||
const commitMessage = `feat: add ${domainConfig.displayName} domain
|
||
|
||
✨ Features implementadas:
|
||
- Component: ${capitalize(domainConfig.name)}Component
|
||
- Service: ${capitalize(domainConfig.name)}Service com ApiClientService
|
||
- Interface: ${capitalize(domainConfig.name)} TypeScript
|
||
- Templates: HTML e SCSS arquivos separados (ERP SaaS)
|
||
- Registry Pattern: Auto-registro no TabFormConfigService
|
||
${domainConfig.hasPhotos ? '- Sub-aba de fotos com send-image component' : ''}
|
||
${domainConfig.hasSideCard ? '- Side card com resumo e status' : ''}
|
||
${domainConfig.hasKilometer ? '- Campo quilometragem com kilometer-input' : ''}
|
||
${domainConfig.hasColor ? '- Campo cor com color-input' : ''}
|
||
${domainConfig.hasStatus ? '- Campo status com badges coloridos' : ''}
|
||
${domainConfig.remoteSelects.length > 0 ? `- Remote-selects: ${domainConfig.remoteSelects.map(rs => rs.fieldName).join(', ')}` : ''}
|
||
|
||
🔧 Integração:
|
||
- Roteamento: app.routes.ts
|
||
- Menu: sidebar.component.ts (após ${domainConfig.menuPosition})
|
||
- MCP: .mcp/config.json
|
||
|
||
🎯 Gerado automaticamente via scripts/create-domain.js`;
|
||
|
||
// Fazer commit
|
||
execSync(`git commit -m "${commitMessage}"`, { stdio: 'inherit' });
|
||
|
||
log.success(`Commit realizado na branch ${branchName}! 📝`);
|
||
log.info(`Para fazer push: git push origin ${branchName}`);
|
||
|
||
} catch (error) {
|
||
log.warning(`Erro no commit automático: ${error.message}`);
|
||
log.info('Você pode fazer o commit manualmente depois');
|
||
}
|
||
}
|
||
|
||
// Funções auxiliares
|
||
function findInsertPosition(content, position) {
|
||
// Encontrar posição de inserção baseada na menuPosition
|
||
const lines = content.split('\n');
|
||
for (let i = 0; i < lines.length; i++) {
|
||
if (lines[i].includes(`'${position}'`)) {
|
||
return i;
|
||
}
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
function findMenuItemEnd(lines, startIndex) {
|
||
// Encontrar o final de um item do menu (considerando children)
|
||
let braceCount = 0;
|
||
let inMenuItem = false;
|
||
|
||
for (let i = startIndex; i < lines.length; i++) {
|
||
const line = lines[i];
|
||
|
||
if (line.includes('{ id:')) {
|
||
inMenuItem = true;
|
||
}
|
||
|
||
if (inMenuItem) {
|
||
const openBraces = (line.match(/{/g) || []).length;
|
||
const closeBraces = (line.match(/}/g) || []).length;
|
||
braceCount += openBraces - closeBraces;
|
||
|
||
if (braceCount === 0 && line.includes('}')) {
|
||
return i;
|
||
}
|
||
}
|
||
}
|
||
|
||
return startIndex;
|
||
}
|
||
|
||
function getIconForDomain(domainName) {
|
||
// Mapear ícones baseados no nome do domínio
|
||
const iconMap = {
|
||
contracts: 'file-contract',
|
||
suppliers: 'truck',
|
||
employees: 'users',
|
||
products: 'box',
|
||
clients: 'handshake',
|
||
orders: 'shopping-cart',
|
||
inventory: 'warehouse',
|
||
companies: 'building'
|
||
};
|
||
|
||
return iconMap[domainName] || 'folder';
|
||
}
|
||
|
||
// 🛠️ Funções utilitárias
|
||
function capitalize(str) {
|
||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||
}
|
||
|
||
function question(prompt) {
|
||
return new Promise((resolve) => {
|
||
rl.question(prompt, resolve);
|
||
});
|
||
}
|
||
|
||
// 🚀 Executar
|
||
if (require.main === module) {
|
||
main();
|
||
}
|
||
|
||
module.exports = { main };
|