Estándares de Código
UNIMAST ERP sigue estándares de código rigurosos para mantener la calidad, consistencia y mantenibilidad del código. Estos estándares se aplican a través de herramientas automatizadas, configuraciones de linting, y convenciones de equipo.
🔧 Herramientas de Calidad de Código
ESLint - Linting Automatizado
// app/eslint.config.mjs
import { FlatCompat } from '@eslint/eslintrc'
const compat = new FlatCompat({
baseDirectory: import.meta.dirname,
})
const eslintConfig = [
...compat.config({
plugins: ['simple-import-sort'], // Plugin de ordenamiento de imports
extends: ['next'], // Configuración base de Next.js
rules: {
'react/no-unescaped-entities': 'off', // Permitir entidades HTML
'@next/next/no-page-custom-font': 'off', // Permitir fuentes personalizadas
'react-hooks/exhaustive-deps': 0, // Deshabilitar regla de dependencias
'simple-import-sort/imports': 'error', // Error en imports desordenados
'simple-import-sort/exports': 'error', // Error en exports desordenados
'no-unused-vars': 'warn', // Warning para variables no usadas
'eol-last': ['error', 'always'] // Nueva línea al final obligatoria
},
ignorePatterns: [
'next.config.ts',
'tailwind.config.js',
'.next',
'node_modules'
],
}),
]
export default eslintConfigPrettier - Formateo Automatizado
// app/package.json
{
"prettier": {
"arrowParens": "always", // Paréntesis siempre en arrow functions
"singleQuote": true, // Comillas simples
"tabWidth": 2, // Ancho de tabulación de 2 espacios
"trailingComma": "none", // Sin coma final
"semi": true // Punto y coma al final
}
}TypeScript - Configuración Estricta
// app/tsconfig.json
{
"compilerOptions": {
"strict": true, // Modo estricto habilitado
"forceConsistentCasingInFileNames": true, // Consistencia en nombres de archivos
"noEmit": true, // No generar archivos de salida
"isolatedModules": true, // Módulos aislados
"baseUrl": ".", // URL base para imports
"paths": {
"@/*": ["./*"] // Alias de paths
}
}
}📝 Estándares de TypeScript
1. Tipado Estricto y Explícito
// ✅ CORRECTO: Tipado explícito y estricto
interface UserFormData {
name: string;
email: string;
password?: string;
roleId: string;
}
export async function createUser(data: UserFormData): Promise<User> {
const user = await prisma.user.create({
data: {
name: data.name,
email: data.email,
password: data.password ? await hashPassword(data.password) : null,
roleId: data.roleId
}
});
return user;
}
// ❌ INCORRECTO: Uso de any o tipado implícito
export async function createUser(data: any) {
const user = await prisma.user.create({ data });
return user;
}2. Interfaces y Tipos
// ✅ CORRECTO: Interfaces descriptivas y específicas
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
size?: 'default' | 'sm' | 'lg' | 'icon';
}
// ✅ CORRECTO: Tipos union para valores específicos
export type TaxDocumentType =
| 'E31' | 'E32' | 'E33' | 'E34'
| 'E41' | 'E43' | 'E44' | 'E45' | 'E46' | 'E47';
// ✅ CORRECTO: Tipos utilitarios de Prisma
export type EcfIssued = Prisma.TaxDocumentGetPayload<{
include: {
creditNote: { include: { invoice: { include: { customer: true } } } };
invoice: { include: { customer: true } };
};
}>;3. Generics y Tipos Reutilizables
// ✅ CORRECTO: Generics para funciones reutilizables
export function useDebounceValue<T>(value: T, delay = 300): T {
const [debouncedValue, setDebouncedValue] = React.useState<T>(value);
React.useEffect(() => {
const timeout = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timeout);
}, [value, delay]);
return debouncedValue;
}
// ✅ CORRECTO: Tipos condicionales
export type ConditionalUser = User & {
password: string | null;
role: Role;
};
// ✅ CORRECTO: Tipos de mapeo
export const STATUS_MAP: Record<string, TaxDocumentStatus> = {
Aceptado: TaxDocumentStatus.APPROVED,
Rechazado: TaxDocumentStatus.REJECTED,
['Aceptado Condicional']: TaxDocumentStatus.CONDITIONALLY_APPROVED,
['En Proceso']: TaxDocumentStatus.IN_PROCESS
};🎨 Estándares de Estilo y Formato
1. Nomenclatura de Archivos y Directorios
# ✅ CORRECTO: Nomenclatura kebab-case para archivos
app/
├── (dashboard)/ # Grupos de rutas
│ ├── invoices/ # Módulos en plural
│ │ ├── page.tsx # Páginas principales
│ │ ├── create-invoice-dialog.tsx # Componentes descriptivos
│ │ ├── invoice-row-actions.tsx # Acciones específicas
│ │ └── invoices-filter.tsx # Filtros del módulo
│ └── settings/ # Configuraciones
│ ├── general/ # Configuración general
│ └── users/ # Gestión de usuarios
├── components/
│ ├── ui/ # Componentes base (shadcn/ui)
│ │ ├── button.tsx # Componentes simples
│ │ ├── form.tsx # Componentes de formulario
│ │ └── table.tsx # Componentes de datos
│ └── submit-button.tsx # Componentes especializados
└── lib/
├── auth/ # Autenticación
├── ecf/ # Facturación electrónica
├── queue/ # Colas de trabajo
└── utils/ # Utilidades generales2. Nomenclatura de Variables y Funciones
// ✅ CORRECTO: Nombres descriptivos y específicos
export async function getTaxDocuments(searchParams: SearchParams) {
const { page = 1, perPage = 10, status, type, startDate, endDate } = searchParams;
const whereClause = buildWhereClause({ status, type, startDate, endDate });
const skip = (page - 1) * perPage;
const [documents, totalCount] = await Promise.all([
prisma.taxDocument.findMany({
where: whereClause,
skip,
take: perPage,
include: { invoice: true, creditNote: true }
}),
prisma.taxDocument.count({ where: whereClause })
]);
return { documents, totalCount, page, perPage };
}
// ✅ CORRECTO: Constantes en UPPER_SNAKE_CASE
export const TAX_DOCUMENT_STATUS_LABEL = {
[TaxDocumentStatus.APPROVED]: 'Aceptado',
[TaxDocumentStatus.REJECTED]: 'Rechazado',
[TaxDocumentStatus.CONDITIONALLY_APPROVED]: 'Aceptado Condicional',
[TaxDocumentStatus.IN_PROCESS]: 'En Proceso'
} as const;
// ✅ CORRECTO: Funciones con verbos descriptivos
export function validateInvoiceForECF(invoice: Invoice): ValidationResult
export function processDocumentSubmission(inputData: ElectronicInvoice): Promise<TaxDocument>
export function queueSendTaxDocument(encf: string): Promise<Job>3. Estructura de Imports
// ✅ CORRECTO: Orden de imports (automático con simple-import-sort)
// 1. Imports de React y Next.js
import { useEffect, useState } from 'react';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
// 2. Imports de librerías externas
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { z } from 'zod';
// 3. Imports de Prisma y tipos
import { Role, User } from '@prisma/client';
// 4. Imports de componentes UI
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogFooter } from '@/components/ui/dialog';
import { Form, FormControl, FormField } from '@/components/ui/form';
// 5. Imports locales del módulo
import { createUser, updateUser } from './actions';
// ❌ INCORRECTO: Imports desordenados
import { Button } from '@/components/ui/button';
import { z } from 'zod';
import { useEffect, useState } from 'react';
import { createUser, updateUser } from './actions';🏗️ Estándares de Arquitectura
1. Separación de Responsabilidades
// ✅ CORRECTO: Separación clara de responsabilidades
// app/(dashboard)/invoices/page.tsx - Página principal (UI)
export default async function InvoicesPage({ searchParams }: Props) {
const { invoices, totalCount, page, perPage } = await getInvoices(searchParams);
return (
<div>
<InvoicesHeader />
<InvoicesFilter />
<InvoicesTable invoices={invoices} />
<Pagination totalCount={totalCount} currentPage={page} perPage={perPage} />
</div>
);
}
// app/(dashboard)/invoices/actions.ts - Lógica de negocio
export async function getInvoices(searchParams: SearchParams) {
const { page = 1, perPage = 10, status, type } = searchParams;
const whereClause = buildWhereClause({ status, type });
const [invoices, totalCount] = await Promise.all([
prisma.invoice.findMany({
where: whereClause,
skip: (page - 1) * perPage,
take: perPage,
include: { customer: true }
}),
prisma.invoice.count({ where: whereClause })
]);
return { invoices, totalCount, page, perPage };
}
// app/(dashboard)/invoices/invoices-filter.tsx - Componente de filtros
export default function InvoicesFilter() {
const [filters, setFilters] = useState(initialFilters);
const applyFilters = () => {
// Lógica de aplicación de filtros
};
return (
<Card>
<CardContent>
{/* UI de filtros */}
</CardContent>
</Card>
);
}2. Patrones de Componentes
// ✅ CORRECTO: Componentes funcionales con forwardRef
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = "Button";
// ✅ CORRECTO: Hooks personalizados para lógica reutilizable
export function useCustomSearchParams() {
const searchParams = useSearchParams();
const pathname = usePathname();
const { replace } = useRouter();
const updateSearchParams = useCallback((updates: Record<string, string>) => {
const params = new URLSearchParams(searchParams);
Object.entries(updates).forEach(([key, value]) => {
if (value) {
params.set(key, value);
} else {
params.delete(key);
}
});
replace(`${pathname}?${params.toString()}`);
}, [searchParams, pathname, replace]);
return { searchParams, updateSearchParams };
}3. Manejo de Estado
// ✅ CORRECTO: Estado local para componentes específicos
export function CreateInvoiceDialog() {
const [isOpen, setIsOpen] = useState(false);
const [items, setItems] = useState<InvoiceItem[]>([]);
const addItem = useCallback(() => {
setItems(prev => [...prev, { description: '', quantity: 1, price: 0 }]);
}, []);
const removeItem = useCallback((index: number) => {
setItems(prev => prev.filter((_, i) => i !== index));
}, []);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
{/* Contenido del diálogo */}
</Dialog>
);
}
// ✅ CORRECTO: Estado global en contextos
export const SessionProvider = ({ children, session }: SessionProviderProps) => {
return (
<SessionContext.Provider value={{ session }}>
{children}
</SessionContext.Provider>
);
};
export const useSession = () => {
const context = useContext(SessionContext);
if (!context) {
throw new Error('useSession must be used within SessionProvider');
}
return context;
};🔄 Control de Flujo y Manejo de Errores
1. Early Returns y Validaciones
// ✅ CORRECTO: Early returns para validaciones
export async function sendInvoiceECF(invoiceId: string) {
const session = await getSession();
if (!session) {
throw new Error('No tienes permiso para realizar esta acción.');
}
const cert = await getCertificate();
if (!cert || !cert.key || !cert.cert) {
throw new Error('Certificado eCF no encontrado. Configure el certificado en Configuración > General.');
}
const ecfResult = await getNextECF(type);
if (!ecfResult) {
throw new Error(`No existen secuencias disponibles para el tipo de documento ${type}. Contacte al administrador.`);
}
// Procesar envío si todas las validaciones pasan
const result = await processDocumentSubmission(inputData, ecfResult);
return result;
}
// ❌ INCORRECTO: Anidación profunda
export async function sendInvoiceECF(invoiceId: string) {
const session = await getSession();
if (session) {
const cert = await getCertificate();
if (cert && cert.key && cert.cert) {
const ecfResult = await getNextECF(type);
if (ecfResult) {
const result = await processDocumentSubmission(inputData, ecfResult);
return result;
} else {
throw new Error('No existen secuencias...');
}
} else {
throw new Error('Certificado no encontrado...');
}
} else {
throw new Error('No tienes permiso...');
}
}2. Manejo Explícito de Errores
// ✅ CORRECTO: Manejo explícito y descriptivo de errores
export async function processDocumentSubmission(inputData: ElectronicInvoice) {
try {
const cert = await getCertificate();
if (!cert || !cert.key || !cert.cert) {
throw new Error('No ECF certificate found');
}
const ecf = new ECF(cert, ECF_ENV);
await ecf.authenticate();
const transformer = new Transformer();
const jsonData = await ECFBuilder(inputData);
const xmlDoc = transformer.json2xml(jsonData);
const signature = new Signature(cert.key, cert.cert);
const signedXml = signature.signXml(xmlDoc, 'ECF');
const securityCode = getCodeSixDigitfromSignature(signedXml);
if (!securityCode) {
throw new Error('Unable to get the first 6 digits of the SignatureValue');
}
// Continuar con el procesamiento...
return await saveAndSendDocument(signedXml, securityCode);
} catch (error) {
console.error('Error processing document submission:', {
error: error.message,
inputData: { encf: inputData.encf, type: inputData.type },
stack: error.stack
});
// Re-lanzar error con contexto adicional
throw new Error(`Failed to process document ${inputData.encf}: ${error.message}`);
}
}
// ✅ CORRECTO: Errores tipados y específicos
export class ECFError extends Error {
constructor(
message: string,
public code: 'CERTIFICATE_NOT_FOUND' | 'NO_SEQUENCES' | 'AUTHENTICATION_FAILED',
public details?: Record<string, any>
) {
super(message);
this.name = 'ECFError';
}
}
// Uso
if (!cert) {
throw new ECFError(
'Certificado eCF no encontrado',
'CERTIFICATE_NOT_FOUND',
{ path: settings.ecfCertificatePath }
);
}3. Evitar Anidación Profunda
// ✅ CORRECTO: Extraer lógica compleja a funciones separadas
export async function handleInvoiceCreation(formData: FormData) {
try {
const invoiceData = await validateAndParseInvoiceData(formData);
const customer = await findOrCreateCustomer(invoiceData.customer);
const invoice = await createInvoiceWithItems(invoiceData, customer.id);
if (invoiceData.shouldSendECF) {
await queueECFSending(invoice.id);
}
return { success: true, invoice };
} catch (error) {
return { success: false, error: error.message };
}
}
async function validateAndParseInvoiceData(formData: FormData) {
const rawData = Object.fromEntries(formData.entries());
const validatedData = invoiceSchema.parse(rawData);
return validatedData;
}
async function findOrCreateCustomer(customerData: CustomerData) {
const existingCustomer = await prisma.customer.findUnique({
where: { taxId: customerData.taxId }
});
if (existingCustomer) return existingCustomer;
return await prisma.customer.create({ data: customerData });
}
// ❌ INCORRECTO: Lógica anidada en una sola función
export async function handleInvoiceCreation(formData: FormData) {
try {
const rawData = Object.fromEntries(formData.entries());
const validatedData = invoiceSchema.parse(rawData);
const existingCustomer = await prisma.customer.findUnique({
where: { taxId: validatedData.customer.taxId }
});
let customer;
if (existingCustomer) {
customer = existingCustomer;
} else {
customer = await prisma.customer.create({ data: validatedData.customer });
}
const invoice = await prisma.invoice.create({
data: {
...validatedData,
customerId: customer.id,
items: { create: validatedData.items }
}
});
if (validatedData.shouldSendECF) {
await queueECFSending(invoice.id);
}
return { success: true, invoice };
} catch (error) {
return { success: false, error: error.message };
}
}♿ Estándares de Accesibilidad (UI/UX)
1. Labels y Aria Attributes
// ✅ CORRECTO: Labels explícitos y aria attributes
export function UserForm() {
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)}>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel htmlFor="user-name">Nombre Completo</FormLabel>
<FormControl>
<Input
id="user-name"
placeholder="Ingrese su nombre completo"
aria-describedby="name-help"
{...field}
/>
</FormControl>
<FormDescription id="name-help">
Ingrese su nombre completo como aparece en documentos oficiales
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel htmlFor="user-email">Correo Electrónico</FormLabel>
<FormControl>
<Input
id="user-email"
type="email"
placeholder="usuario@ejemplo.com"
aria-describedby="email-help"
{...field}
/>
</FormControl>
<FormDescription id="email-help">
Este correo se utilizará para notificaciones del sistema
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
);
}2. Focus Management y Keyboard Navigation
// ✅ CORRECTO: Manejo de focus y navegación por teclado
export function ConfirmationDialog({ isOpen, onConfirm, onCancel }: Props) {
const confirmButtonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (isOpen && confirmButtonRef.current) {
confirmButtonRef.current.focus();
}
}, [isOpen]);
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
onCancel();
}
};
return (
<Dialog open={isOpen} onOpenChange={onCancel}>
<DialogContent onKeyDown={handleKeyDown}>
<DialogHeader>
<DialogTitle>Confirmar Acción</DialogTitle>
<DialogDescription>
Esta acción no se puede deshacer. ¿Está seguro?
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={onCancel}>
Cancelar
</Button>
<Button
ref={confirmButtonRef}
onClick={onConfirm}
variant="destructive"
>
Confirmar
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}3. Estados de Carga y Feedback
// ✅ CORRECTO: Estados de carga y feedback no bloqueantes
export function SubmitButton({ children, loading, ...props }: SubmitButtonProps) {
return (
<Button
type="submit"
disabled={loading}
{...props}
>
{loading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Procesando...
</>
) : (
children
)}
</Button>
);
}
// ✅ CORRECTO: Toasts para feedback no bloqueante
export function UserModal({ user, roles }: UserModalProps) {
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async (values: UserFormData) => {
setIsSubmitting(true);
try {
if (user) {
await updateUser(user.id, values);
toast.success('Usuario actualizado exitosamente');
} else {
await createUser(values);
toast.success('Usuario creado exitosamente');
}
onClose();
} catch (error) {
toast.error(`Error: ${error.message}`);
} finally {
setIsSubmitting(false);
}
};
return (
<Dialog>
<DialogContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)}>
{/* Campos del formulario */}
<DialogFooter>
<Button type="button" variant="outline" onClick={onClose}>
Cancelar
</Button>
<SubmitButton loading={isSubmitting}>
{user ? 'Actualizar' : 'Crear'}
</SubmitButton>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
}📁 Organización y Estructura
1. Nombres Descriptivos por Dominio/Acción
// ✅ CORRECTO: Nombres que reflejan el dominio y la acción
// Archivos de acciones
export async function createUser(userData: CreateUserData): Promise<User>
export async function updateUser(userId: string, userData: UpdateUserData): Promise<User>
export async function deleteUser(userId: string): Promise<void>
export async function getUserById(userId: string): Promise<User | null>
export async function getUsersWithPagination(params: PaginationParams): Promise<UsersResult>
// Archivos de componentes
export function UserList({ users, onUserSelect }: UserListProps)
export function UserForm({ user, onSubmit, onCancel }: UserFormProps)
export function UserModal({ user, roles, isOpen, onClose }: UserModalProps)
export function UserRowActions({ user, onEdit, onDelete }: UserRowActionsProps)
// Archivos de utilidades
export function validateUserData(data: unknown): UserData
export function formatUserName(user: User): string
export function getUserInitials(user: User): string
export function isUserActive(user: User): boolean2. Extracción de Utilidades y Hooks
// ✅ CORRECTO: Extraer lógica reutilizable
// app/lib/hooks/use-debounce.ts
export function useDebounceValue<T>(value: T, delay = 300): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const timeout = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timeout);
}, [value, delay]);
return debouncedValue;
}
// app/lib/utils/validation.ts
export function validateEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
export function validateTaxId(taxId: string): boolean {
// Validación específica para RNC dominicano
const taxIdRegex = /^\d{9}$/;
return taxIdRegex.test(taxId);
}
// app/lib/utils/formatting.ts
export function formatCurrency(amount: number, currency = 'DOP'): string {
return new Intl.NumberFormat('es-DO', {
style: 'currency',
currency,
minimumFractionDigits: 2
}).format(amount);
}
export function formatDate(date: Date, locale = 'es-DO'): string {
return new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'long',
day: 'numeric'
}).format(date);
}3. Estructura de Directorios por Módulo
# ✅ CORRECTO: Estructura modular y organizada
app/
├── (dashboard)/
│ ├── invoices/ # Módulo de facturas
│ │ ├── page.tsx # Página principal
│ │ ├── actions.ts # Acciones del módulo
│ │ ├── constants.ts # Constantes específicas
│ │ ├── types.ts # Tipos del módulo
│ │ ├── components/ # Componentes específicos
│ │ │ ├── create-invoice-dialog.tsx
│ │ │ ├── invoice-row-actions.tsx
│ │ │ └── invoices-filter.tsx
│ │ └── hooks/ # Hooks específicos
│ │ └── use-invoice-filters.ts
│ ├── customers/ # Módulo de clientes
│ │ ├── page.tsx
│ │ ├── actions.ts
│ │ ├── components/
│ │ └── hooks/
│ └── settings/ # Módulo de configuraciones
│ ├── general/
│ ├── users/
│ └── roles/
├── components/
│ ├── ui/ # Componentes base
│ ├── forms/ # Componentes de formularios
│ └── shared/ # Componentes compartidos
└── lib/
├── auth/ # Autenticación
├── ecf/ # Facturación electrónica
├── queue/ # Colas de trabajo
├── storage/ # Almacenamiento
├── utils/ # Utilidades generales
└── hooks/ # Hooks compartidos🚀 Mejores Prácticas
1. Performance y Optimización
// ✅ CORRECTO: Memoización inteligente
export function AdvancedFilters({ columns, onFilter }: AdvancedFiltersProps) {
const [filterState, setFilterState] = useState<FilterRule[]>([]);
// Memoizar agrupación de columnas para evitar recálculos
const groupedColumns = useMemo(() => {
return columns.reduce((acc, column) => {
acc[column.key] = column;
return acc;
}, {} as Record<string, Column>);
}, [columns]);
// Memoizar validación de filtros
const isValidFilter = useMemo(() => {
return filterState.every(rule =>
rule.field && rule.operator && rule.value
);
}, [filterState]);
// Callback estable para evitar re-renders
const handleFilterChange = useCallback((index: number, field: keyof FilterRule, value: any) => {
setFilterState(prev => prev.map((rule, i) =>
i === index ? { ...rule, [field]: value } : rule
));
}, []);
return (
<div>
{/* Renderizado de filtros */}
<Button
disabled={!isValidFilter}
onClick={() => onFilter(filterState)}
>
Aplicar Filtros
</Button>
</div>
);
}
// ❌ INCORRECTO: Sin memoización
export function BadAdvancedFilters({ columns, onFilter }: AdvancedFiltersProps) {
const [filterState, setFilterState] = useState<FilterRule[]>([]);
// Se recalcula en cada render
const groupedColumns = columns.reduce((acc, column) => {
acc[column.key] = column;
return acc;
}, {} as Record<string, Column>);
// Se valida en cada render
const isValidFilter = filterState.every(rule =>
rule.field && rule.operator && rule.value
);
// Nueva función en cada render
const handleFilterChange = (index: number, field: keyof FilterRule, value: any) => {
setFilterState(prev => prev.map((rule, i) =>
i === index ? { ...rule, [field]: value } : rule
));
};
return (
<div>
{/* Renderizado de filtros */}
<Button
disabled={!isValidFilter}
onClick={() => onFilter(filterState)}
>
Aplicar Filtros
</Button>
</div>
);
}2. Manejo de Errores y Logging
// ✅ CORRECTO: Logging estructurado y manejo de errores
export async function processECFDocument(encf: string) {
const startTime = Date.now();
const logContext = { encf, startTime };
console.log('[ECF] Starting document processing', logContext);
try {
const document = await getDocument(encf);
if (!document) {
throw new Error(`Document ${encf} not found`);
}
const result = await sendToDGII(document);
const duration = Date.now() - startTime;
console.log('[ECF] Document processed successfully', {
...logContext,
duration,
result: { trackId: result.trackId, status: result.status }
});
return result;
} catch (error) {
const duration = Date.now() - startTime;
console.error('[ECF] Document processing failed', {
...logContext,
duration,
error: {
message: error.message,
stack: error.stack,
name: error.name
}
});
// Re-lanzar error con contexto adicional
throw new Error(`Failed to process ECF document ${encf}: ${error.message}`);
}
}
// ❌ INCORRECTO: Logging básico y manejo de errores pobre
export async function processECFDocument(encf: string) {
try {
const document = await getDocument(encf);
const result = await sendToDGII(document);
return result;
} catch (error) {
console.error('Error:', error);
throw error;
}
}3. Testing y Validación
// ✅ CORRECTO: Validación con Zod y manejo de tipos
const userSchema = z.object({
name: z.string()
.min(1, 'El nombre es obligatorio')
.min(2, 'El nombre debe tener al menos 2 caracteres')
.max(50, 'El nombre no puede exceder 50 caracteres'),
email: z.string()
.email('Correo electrónico no válido')
.min(1, 'El correo es obligatorio'),
password: z.string()
.min(6, 'La contraseña debe tener al menos 6 caracteres')
.regex(/[A-Z]/, 'Debe contener al menos una mayúscula')
.regex(/[0-9]/, 'Debe contener al menos un número')
.optional(),
roleId: z.string()
.nonempty('El rol es obligatorio')
.cuid('ID de rol inválido')
});
export async function createUser(userData: unknown) {
// Validar datos de entrada
const validatedData = userSchema.parse(userData);
// Verificar que el rol existe
const role = await prisma.role.findUnique({
where: { id: validatedData.roleId }
});
if (!role) {
throw new Error('Rol especificado no existe');
}
// Crear usuario con datos validados
const user = await prisma.user.create({
data: {
name: validatedData.name,
email: validatedData.email,
password: validatedData.password ? await hashPassword(validatedData.password) : null,
roleId: validatedData.roleId
}
});
return user;
}🔧 Scripts y Comandos
Scripts de Desarrollo
# Linting y formateo
pnpm lint # Ejecutar ESLint
pnpm lint:fix # Ejecutar ESLint con auto-fix
# TypeScript
pnpm tsc --noEmit # Verificar tipos sin generar archivos
pnpm tsc --build # Build de TypeScript
# Base de datos
pnpm prisma generate # Generar cliente de Prisma
pnpm prisma migrate dev # Ejecutar migraciones
pnpm prisma db seed # Ejecutar seed de base de datos
pnpm prisma studio # Abrir Prisma Studio
# Desarrollo
pnpm dev # Servidor de desarrollo con Turbopack
pnpm build # Build de producción
pnpm start # Servidor de producciónConfiguración de Pre-commit Hooks
// .husky/pre-commit
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# Linting automático
pnpm lint:fix
# Verificación de tipos
pnpm tsc --noEmit
# Formateo automático
pnpm format🔍 Troubleshooting
Problemas Comunes
1. Errores de ESLint
# Verificar configuración
npx eslint --print-config app/components/ui/button.tsx
# Verificar reglas específicas
npx eslint --rule 'simple-import-sort/imports: error' app/components/ui/button.tsx
# Auto-fix de problemas
pnpm lint:fix2. Errores de TypeScript
# Verificar tipos sin generar archivos
pnpm tsc --noEmit
# Verificar archivo específico
npx tsc --noEmit app/components/ui/button.tsx
# Verificar configuración
npx tsc --showConfig3. Problemas de Formateo
# Verificar configuración de Prettier
npx prettier --config app/package.json --check .
# Formatear archivos
npx prettier --write .
# Verificar archivo específico
npx prettier --check app/components/ui/button.tsxDebug de Estándares
// Función de debug para estándares de código
export function debugCodingStandards() {
console.log('=== UNIMAST ERP Coding Standards Debug ===');
// Configuración de ESLint
console.log('ESLint Configuration:');
console.log('- Next.js config:', 'enabled');
console.log('- Simple import sort:', 'enabled');
console.log('- Strict rules:', 'enabled');
// Configuración de TypeScript
console.log('\nTypeScript Configuration:');
console.log('- Strict mode:', 'enabled');
console.log('- Target:', 'ES5');
console.log('- Module resolution:', 'node');
// Configuración de Prettier
console.log('\nPrettier Configuration:');
console.log('- Single quotes:', 'enabled');
console.log('- Tab width:', '2 spaces');
console.log('- Trailing comma:', 'disabled');
console.log('- Semicolons:', 'enabled');
// Estándares del proyecto
console.log('\nProject Standards:');
console.log('- File naming:', 'kebab-case');
console.log('- Component naming:', 'PascalCase');
console.log('- Function naming:', 'camelCase');
console.log('- Constant naming:', 'UPPER_SNAKE_CASE');
}¿Necesitas más detalles sobre algún aspecto específico? Revisa la documentación de environment-and-config para entender cómo se configuran las herramientas de calidad de código, o la documentación de UI y formularios para ver ejemplos de implementación de estándares de accesibilidad.