==================================================
File: components/closers/ContractProcessModal.tsx
==================================================
import{MessageSquare,Shield,FileText,User,Mail,X,Smartphone,Clock,ArrowRight,Check}from "lucide-react";import{useState}from "react";import{Button}from "../ui";export const ContractProcessModal =({isOpen,onClose,onSendSMS,customerData})=>{const [step,setStep] = useState(1);const [isLoading,setIsLoading] = useState(false);const [smsCode,setSmsCode] = useState("");if(!isOpen)return null;const processSteps = [{id: 1,title: "Envío de SMS",description: "SMS con enlace y código al cliente",icon: ,status: "pending",},{id: 2,title: "Validación de Código",description: "Cliente ingresa código de 4 cifras",icon: ,status: "pending",},{id: 3,title: "Revisión de Detalles",description: "Cliente verifica curso y pago",icon: ,status: "pending",},{id: 4,title: "Firma en Canvas",description: "Cliente firma digitalmente",icon: ,status: "pending",},{id: 5,title: "Procesamiento",description: "Creación de usuario y emails",icon: ,status: "pending",},];const generateCode =()=>{return Math.floor(1000 + Math.random()* 9000).toString()};const handleSendSMS = async()=>{setIsLoading(true);const code = generateCode();setSmsCode(code);try{await new Promise((resolve)=> setTimeout(resolve,2000));setStep(3);onSendSMS(code)}catch(error){console.error("Error enviando SMS:",error);alert("Error al enviar SMS. Inténtalo de nuevo.")}finally{setIsLoading(false)}};return(
Proceso de Firma Digital
{}{step === 1 &&(<>
El cliente recibirá un SMS con un enlace seguro y un código de 4 cifras para completar la firma del contrato.
Datos del Cliente
Nombre:{customerData?.nombre || "N/A"}
Móvil:{customerData?.movil || "N/A"}
Email:{customerData?.email || "N/A"}
{processSteps.map((step,index)=>(
{step.icon}
{step.title}
{step.description}
))}
>)}{}{step === 2 &&(
Enviar SMS al Cliente
Se enviará un SMS al número{" "}{customerData?.movil} con:
- Enlace seguro para acceder al contrato
- Código de validación de 4 cifras
- Instrucciones para completar la firma
}>{isLoading ? "Enviando SMS..." : "Enviar SMS al Cliente"}
)}{}{step === 3 &&(
SMS Enviado Exitosamente
El cliente ha recibido el enlace y el código de validación
Información enviada al cliente:
Código de validación: {smsCode}
Enlace: https:
Estado: Esperando que el cliente complete la firma digital
Recibirás una notificación cuando el cliente complete la firma
)}
)};
==================================================
File: components/ui/Button/Button.tsx
==================================================
import React,{ButtonHTMLAttributes,forwardRef}from 'react';import{clsx}from 'clsx';import{Loader2}from 'lucide-react';export interface ButtonProps extends ButtonHTMLAttributes{variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger';size?: 'sm' | 'md' | 'lg';isLoading?: boolean;fullWidth?: boolean;leftIcon?: React.ReactNode;rightIcon?: React.ReactNode}const Button = forwardRef(({className,variant = 'primary',size = 'md',isLoading = false,fullWidth = false,leftIcon,rightIcon,children,disabled,...props},ref)=>{const baseClasses = 'inline-flex items-center justify-center font-medium rounded-lg transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed';const variantClasses ={primary: 'bg-primary-600 text-white hover:bg-primary-700 focus:ring-primary-500 shadow-sm hover:shadow-md',secondary: 'bg-secondary-100 text-secondary-900 hover:bg-secondary-200 focus:ring-secondary-500',outline: 'border border-secondary-300 text-secondary-700 hover:bg-secondary-50 focus:ring-secondary-500',ghost: 'text-secondary-600 hover:bg-secondary-100 focus:ring-secondary-500',danger: 'bg-error-600 text-white hover:bg-error-700 focus:ring-error-500 shadow-sm hover:shadow-md'};const sizeClasses ={sm: 'px-3 py-1.5 text-sm',md: 'px-4 py-2 text-sm',lg: 'px-6 py-3 text-base'};const widthClasses = fullWidth ? 'w-full' : '';const combinedClasses = clsx(baseClasses,variantClasses[variant],sizeClasses[size],widthClasses,className);const iconSize = size === 'sm' ? 16 : size === 'lg' ? 20 : 18;return()});Button.displayName = 'Button';export default Button;
==================================================
File: components/ui/Button/index.ts
==================================================
export{default}from './Button';export type{ButtonProps}from './Button';
==================================================
File: components/ui/Input/Input.tsx
==================================================
import React,{InputHTMLAttributes,forwardRef,useState}from 'react';import{clsx}from 'clsx';import{Eye,EyeOff,AlertCircle}from 'lucide-react';export interface InputProps extends Omit,'size'>{label?: string;error?: string;helperText?: string;size?: 'sm' | 'md' | 'lg';leftIcon?: React.ReactNode;rightIcon?: React.ReactNode;fullWidth?: boolean}const Input = forwardRef(({className,type = 'text',label,error,helperText,size = 'md',leftIcon,rightIcon,fullWidth = true,disabled,...props},ref)=>{const [showPassword,setShowPassword] = useState(false);const isPassword = type === 'password';const inputType = isPassword && showPassword ? 'text' : type;const baseClasses = 'border rounded-lg transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-1 disabled:opacity-50 disabled:cursor-not-allowed';const sizeClasses ={sm: 'px-3 py-1.5 text-sm',md: 'px-3 py-2 text-sm',lg: 'px-4 py-3 text-base'};const stateClasses = error ? 'border-error-300 focus:border-error-500 focus:ring-error-200' : 'border-secondary-300 focus:border-primary-500 focus:ring-primary-200';const widthClasses = fullWidth ? 'w-full' : '';const inputClasses = clsx(baseClasses,sizeClasses[size],stateClasses,widthClasses,leftIcon && 'pl-10',(rightIcon || isPassword)&& 'pr-10',className);const iconSize = size === 'sm' ? 16 : size === 'lg' ? 20 : 18;return({label &&(
)}
{leftIcon &&(
{leftIcon}
)}
{isPassword &&(
)}{rightIcon && !isPassword &&(
{rightIcon}
)}
{error &&(
)}{helperText && !error &&(
{helperText}
)}
)});Input.displayName = 'Input';export default Input;
==================================================
File: components/ui/Input/index.ts
==================================================
export{default}from './Input';export type{InputProps}from './Input';
==================================================
File: components/ui/LoadingSpinner/LoadingSpinner.tsx
==================================================
import React from 'react';import{clsx}from 'clsx';import{Loader2}from 'lucide-react';export interface LoadingSpinnerProps{size?: 'sm' | 'md' | 'lg';variant?: 'primary' | 'secondary';className?: string;text?: string}const LoadingSpinner: React.FC =({size = 'md',variant = 'primary',className,text})=>{const sizeClasses ={sm: 'w-4 h-4',md: 'w-6 h-6',lg: 'w-8 h-8'};const variantClasses ={primary: 'text-primary-600',secondary: 'text-secondary-500'};const textSizeClasses ={sm: 'text-sm',md: 'text-base',lg: 'text-lg'};if(text){return( {text}
)}return(
)};export default LoadingSpinner;
==================================================
File: components/ui/LoadingSpinner/index.ts
==================================================
export{default}from './LoadingSpinner';export type{LoadingSpinnerProps}from './LoadingSpinner';
==================================================
File: components/ui/Modal/Modal.tsx
==================================================
import React,{ReactNode,useEffect}from 'react';import{createPortal}from 'react-dom';import{clsx}from 'clsx';import{X}from 'lucide-react';import Button from '../Button';export interface ModalProps{isOpen: boolean;onClose:()=> void;title?: string;children: ReactNode;size?: 'sm' | 'md' | 'lg' | 'xl';closeOnOverlayClick?: boolean;showCloseButton?: boolean;footer?: ReactNode}const Modal: React.FC =({isOpen,onClose,title,children,size = 'md',closeOnOverlayClick = true,showCloseButton = true,footer})=>{useEffect(()=>{const handleEscape =(event: KeyboardEvent)=>{if(event.key === 'Escape' && isOpen){onClose()}};if(isOpen){document.addEventListener('keydown',handleEscape);document.body.style.overflow = 'hidden'}return()=>{document.removeEventListener('keydown',handleEscape);document.body.style.overflow = 'unset'}},[isOpen,onClose]);if(!isOpen)return null;const sizeClasses ={sm: 'max-w-md',md: 'max-w-lg',lg: 'max-w-2xl',xl: 'max-w-4xl'};const handleOverlayClick =(event: React.MouseEvent)=>{if(event.target === event.currentTarget && closeOnOverlayClick){onClose()}};const modalContent =( {}
{}
{}{(title || showCloseButton)&&(
{title &&(
{title}
)}{showCloseButton &&()})}{}
{children}
{}{footer &&(
{footer}
)}
);return createPortal(modalContent,document.body)};export default Modal;
==================================================
File: components/ui/Modal/index.ts
==================================================
export{default}from './Modal';export type{ModalProps}from './Modal';
==================================================
File: components/ui/Select/Select.tsx
==================================================
import React,{SelectHTMLAttributes,forwardRef}from 'react';import{clsx}from 'clsx';import{ChevronDown,AlertCircle}from 'lucide-react';import{SelectOption}from '@types';export interface SelectProps extends Omit,'size'>{label?: string;error?: string;helperText?: string;size?: 'sm' | 'md' | 'lg';fullWidth?: boolean;options: SelectOption[];placeholder?: string}const Select = forwardRef(({className,label,error,helperText,size = 'md',fullWidth = true,options,placeholder,disabled,...props},ref)=>{const baseClasses = 'border rounded-lg transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-1 disabled:opacity-50 disabled:cursor-not-allowed appearance-none bg-white cursor-pointer';const sizeClasses ={sm: 'px-3 py-1.5 text-sm pr-8',md: 'px-3 py-2 text-sm pr-8',lg: 'px-4 py-3 text-base pr-10'};const stateClasses = error ? 'border-error-300 focus:border-error-500 focus:ring-error-200' : 'border-secondary-300 focus:border-primary-500 focus:ring-primary-200';const widthClasses = fullWidth ? 'w-full' : '';const selectClasses = clsx(baseClasses,sizeClasses[size],stateClasses,widthClasses,className);const iconSize = size === 'sm' ? 16 : size === 'lg' ? 20 : 18;return({label &&(
)}
{error &&(
)}{helperText && !error &&(
{helperText}
)}
)});Select.displayName = 'Select';export default Select;
==================================================
File: components/ui/Select/index.ts
==================================================
export{default}from './Select';export type{SelectProps}from './Select';
==================================================
File: components/ui/index.ts
==================================================
export{default as Button}from './Button';export{default as Input}from './Input';export{default as Select}from './Select';export{default as Modal}from './Modal';export{default as LoadingSpinner}from './LoadingSpinner';export type{ButtonProps}from './Button';export type{InputProps}from './Input';export type{SelectProps}from './Select';export type{ModalProps}from './Modal';export type{LoadingSpinnerProps}from './LoadingSpinner';
==================================================
File: pages/sales/SalesFormPage.tsx
==================================================
import{useState}from "react";import{FileText,}from "lucide-react";import{Button,Input,LoadingSpinner,Select}from "@/components/ui";import{ContractProcessModal}from "@/components/closers/ContractProcessModal";const SalesFormPage =()=>{const [isSubmitting,setIsSubmitting] = useState(false);const [showContractModal,setShowContractModal] = useState(false);const [saleData,setSaleData] = useState(null);const [sentSMSCode,setSentSMSCode] = useState("");const [formData,setFormData] = useState({nombre: "",email: "",movil: "",socio_o_pagador: "MISMO_CLIENTE",datos_fiscales: "PERSONA_FISICA",transferencia_anticipada: "NO",modalidad_de_pago: "Hotmart",numero_pagos_meses: 1,fecha_pago: "",curso: "",formacion: "",costo_formacion: "",bonus:{mentoria_individualizada_1: false,mentoria_individualizada_2: false,mentoria_individualizada_3: false,mentoria_validacion_productos: false,},otros_detalles: "",closer_de_cierre: "",setter: "",urgencia_contacto: "No Urgente",});const [errors,setErrors] = useState({});const PAYMENT_METHODS = [{value: "Hotmart",label: "Hotmart"},{value: "Sequra",label: "Sequra"},{value: "Stripe",label: "Stripe"},{value: "Transferencia",label: "Transferencia"},{value: "Sequra Pass",label: "Sequra Pass"},];const PAYMENT_INSTALLMENTS = [{value: 1,label: "1 pago"},{value: 2,label: "2 pagos"},{value: 3,label: "3 pagos"},{value: 6,label: "6 pagos"},{value: 9,label: "9 pagos"},{value: 12,label: "12 pagos"},{value: 18,label: "18 pagos"},{value: 24,label: "24 pagos"},];const FISCAL_DATA_TYPES = [{value: "PERSONA_FISICA",label: "Persona física"},{value: "AUTONOMO",label: "Autónomo"},{value: "EMPRESA",label: "Empresa"},];const CONTACT_URGENCY_OPTIONS = [{value: "No Urgente",label: "No Urgente"},{value: "Urgente",label: "Urgente"},];const PERSON_TYPE_OPTIONS = [{value: "MISMO_CLIENTE",label: "El mismo cliente"},{value: "OTRA_PERSONA",label: "Otra persona"},];const YES_NO_OPTIONS = [{value: "SI",label: "Sí"},{value: "NO",label: "No"},];const cursosOptions = [{value: "1",label: "React Avanzado con TypeScript"},{value: "2",label: "Marketing Digital para Principiantes"},{value: "3",label: "Diseño UX/UI Profesional"},{value: "4",label: "Python para Data Science"},];const closersOptions = [{value: "1",label: "Carlos Ruiz"},{value: "2",label: "María González"},{value: "3",label: "Ana López"},{value: "4",label: "Juan Martín"},];const handleInputChange =(field,value)=>{if(field.includes(".")){const [parent,child] = field.split(".");setFormData((prev)=>({...prev,[parent]:{...prev[parent],[child]: value,},}))}else{setFormData((prev)=>({...prev,[field]: value,}))}};const validateForm =()=>{const newErrors ={};if(!formData.nombre.trim())newErrors.nombre = "El nombre es obligatorio";if(!formData.email.trim())newErrors.email = "El email es obligatorio";if(!formData.movil.trim())newErrors.movil = "El móvil es obligatorio";if(!formData.fecha_pago)newErrors.fecha_pago = "La fecha de pago es obligatoria";if(!formData.curso)newErrors.curso = "El curso es obligatorio";if(!formData.formacion.trim())newErrors.formacion = "La formación es obligatoria";if(!formData.costo_formacion.trim())newErrors.costo_formacion = "El costo es obligatorio";if(!formData.closer_de_cierre)newErrors.closer_de_cierre = "El closer es obligatorio";if(!formData.setter.trim())newErrors.setter = "El setter es obligatorio";setErrors(newErrors);return Object.keys(newErrors).length === 0};const onSubmit = async()=>{if(!validateForm()){alert("Por favor,completa todos los campos obligatorios");return}setIsSubmitting(true);try{await new Promise((resolve)=> setTimeout(resolve,2000));setSaleData(formData);setShowContractModal(true)}catch(error){console.error("Error al crear la venta:",error);alert("Error al crear la venta. Inténtalo de nuevo.")}finally{setIsSubmitting(false)}};const handleSMSSent =(code)=>{setSentSMSCode(code);console.log("SMS enviado con código:",code)};const handleReset =()=>{setFormData({nombre: "",email: "",movil: "",socio_o_pagador: "MISMO_CLIENTE",datos_fiscales: "PERSONA_FISICA",transferencia_anticipada: "NO",modalidad_de_pago: "Hotmart",numero_pagos_meses: 1,fecha_pago: "",curso: "",formacion: "",costo_formacion: "",bonus:{mentoria_individualizada_1: false,mentoria_individualizada_2: false,mentoria_individualizada_3: false,mentoria_validacion_productos: false,},otros_detalles: "",closer_de_cierre: "",setter: "",urgencia_contacto: "No Urgente",});setErrors({})};if(isSubmitting){return(
)}return({}
Formulario de Ventas
Registra una nueva venta en el sistema
{}
{}
{}
Datos de Facturación
{}
{}
{}
{}
Otros Detalles
{}
{}
}>{isSubmitting ? "Procesando..." : "Crear Venta y Enviar Contrato"}
{}
setShowContractModal(false)}onSendSMS={handleSMSSent}customerData={saleData}/> )};export default SalesFormPage;
==================================================
Project Structure:
==================================================
├── [34m[1m.bolt[22m[39m
│ ├── [36mconfig.json[39m
│ └── [36mprompt[39m
├── [36mbun.lock[39m
├── [36meslint.config.js[39m
├── [36mindex.html[39m
├── [36mpackage-lock.json[39m
├── [36mpackage.json[39m
├── [36mpostcss.config.js[39m
├── [34m[1msrc[22m[39m
│ ├── [36mApp.tsx[39m
│ ├── [34m[1mcomponents[22m[39m
│ ├── [34m[1mconstants[22m[39m
│ │ ├── [36mapi.ts[39m
│ │ ├── [36mindex.ts[39m
│ │ ├── [36mpaymentMethods.ts[39m
│ │ ├── [36mpermissions.ts[39m
│ │ ├── [36mroles.ts[39m
│ │ └── [36mroutes.ts[39m
│ ├── [34m[1mcontext[22m[39m
│ │ ├── [36mAuthContext.tsx[39m
│ │ └── [36mindex.ts[39m
│ ├── [34m[1mhooks[22m[39m
│ │ ├── [36mindex.ts[39m
│ │ ├── [36museClosers.ts[39m
│ │ ├── [36museCourses.ts[39m
│ │ ├── [36museDebounce.ts[39m
│ │ └── [36museLocalStorage.ts[39m
│ ├── [34m[1mi18n[22m[39m
│ │ └── [36mindex.ts[39m
│ ├── [36mindex.css[39m
│ ├── [36mmain.tsx[39m
│ ├── [34m[1mpages[22m[39m
│ │ └── [36mNotFoundPage.tsx[39m
│ ├── [36mrouter.tsx[39m
│ ├── [34m[1mservices[22m[39m
│ ├── [34m[1mtypes[22m[39m
│ │ ├── [36mapi.types.ts[39m
│ │ ├── [36mauth.types.ts[39m
│ │ ├── [36mclosers.types.ts[39m
│ │ ├── [36mcommon.types.ts[39m
│ │ ├── [36mcourses.types.ts[39m
│ │ ├── [36mindex.ts[39m
│ │ └── [36msales.types.ts[39m
│ ├── [34m[1mutils[22m[39m
│ │ ├── [36mapi.ts[39m
│ │ ├── [36mdates.ts[39m
│ │ ├── [36mformatters.ts[39m
│ │ ├── [36mindex.ts[39m
│ │ ├── [36mpermissions.ts[39m
│ │ ├── [36mstorage.ts[39m
│ │ └── [36mvalidators.ts[39m
│ └── [36mvite-env.d.ts[39m
├── [36mtailwind.config.js[39m
├── [36mtsconfig.app.json[39m
├── [36mtsconfig.json[39m
├── [36mtsconfig.node.json[39m
└── [36mvite.config.ts[39m
==================================================
Processed Files:
==================================================
├── [36mcomponents[39m
│ ├── [36mclosers[39m
│ │ └── [36mContractProcessModal.tsx[39m
│ └── [36mui[39m
│ ├── [36mButton[39m
│ │ ├── [36mButton.tsx[39m
│ │ └── [36mindex.ts[39m
│ ├── [36mInput[39m
│ │ ├── [36mInput.tsx[39m
│ │ └── [36mindex.ts[39m
│ ├── [36mLoadingSpinner[39m
│ │ ├── [36mLoadingSpinner.tsx[39m
│ │ └── [36mindex.ts[39m
│ ├── [36mModal[39m
│ │ ├── [36mModal.tsx[39m
│ │ └── [36mindex.ts[39m
│ ├── [36mSelect[39m
│ │ ├── [36mSelect.tsx[39m
│ │ └── [36mindex.ts[39m
│ └── [36mindex.ts[39m
└── [36mpages[39m
└── [36msales[39m
└── [36mSalesFormPage.tsx[39m