================================================== 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
)}{}{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 &&(
{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 &&(
{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 del Cliente

handleInputChange("nombre",e.target.value)}error={errors.nombre}/> handleInputChange("email",e.target.value)}error={errors.email}/> handleInputChange("movil",e.target.value)}error={errors.movil}/>
{}

Datos del Socio/Pagador

handleInputChange("nombre_2_opcional",e.target.value)}/> handleInputChange("email_2_opcional",e.target.value)}/> handleInputChange("movil_2_opcional",e.target.value)}/>
)}
{}

Datos de Facturación

handleInputChange("transferencia_anticipada",e.target.value)}error={errors.transferencia_anticipada}/>
{}

Datos de Pago

handleInputChange("numero_pagos_meses",parseInt(e.target.value))}error={errors.numero_pagos_meses}/> handleInputChange("fecha_pago",e.target.value)}error={errors.fecha_pago}/>
{}

Datos del Curso

handleInputChange("formacion",e.target.value)}error={errors.formacion}/> handleInputChange("costo_formacion",e.target.value)}error={errors.costo_formacion}/>
{}

Bonus Incluidos

{}

Otros Detalles