fileweaver
Version:
A CLI tool to weave files together based on regex patterns
168 lines (139 loc) • 34.4 kB
Plain Text
==================================================
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: <MessageSquare className="w-5 h-5" />,status: "pending",},{id: 2,title: "Validación de Código",description: "Cliente ingresa código de 4 cifras",icon: <Shield className="w-5 h-5" />,status: "pending",},{id: 3,title: "Revisión de Detalles",description: "Cliente verifica curso y pago",icon: <FileText className="w-5 h-5" />,status: "pending",},{id: 4,title: "Firma en Canvas",description: "Cliente firma digitalmente",icon: <User className="w-5 h-5" />,status: "pending",},{id: 5,title: "Procesamiento",description: "Creación de usuario y emails",icon: <Mail className="w-5 h-5" />,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(<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <div className="bg-white rounded-lg shadow-xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto"> <div className="flex items-center justify-between p-6 border-b"> <h2 className="text-xl font-semibold text-gray-900"> Proceso de Firma Digital </h2> <button onClick={onClose}className="text-gray-400 hover:text-gray-600" > <X className="w-6 h-6" /> </button> </div> <div className="p-6">{}{step === 1 &&(<> <div className="mb-6"> <p className="text-gray-600 mb-4"> El cliente recibirá un SMS con un enlace seguro y un código de 4 cifras para completar la firma del contrato. </p> <div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6"> <div className="flex items-start space-x-3"> <Smartphone className="w-5 h-5 text-blue-600 mt-0.5" /> <div> <h4 className="font-medium text-blue-900"> Datos del Cliente </h4> <p className="text-sm text-blue-700 mt-1"> <strong>Nombre:</strong>{customerData?.nombre || "N/A"}<br /> <strong>Móvil:</strong>{customerData?.movil || "N/A"}<br /> <strong>Email:</strong>{customerData?.email || "N/A"}</p> </div> </div> </div> </div> <div className="space-y-4">{processSteps.map((step,index)=>(<div key={step.id}className="flex items-start space-x-4 p-4 border border-gray-200 rounded-lg" > <div className="flex-shrink-0 w-8 h-8 bg-blue-100 text-blue-600 rounded-full flex items-center justify-center">{step.icon}</div> <div className="flex-1"> <h3 className="font-medium text-gray-900">{step.title}</h3> <p className="text-sm text-gray-600 mt-1">{step.description}</p> </div> <div className="flex-shrink-0"> <Clock className="w-5 h-5 text-gray-400" /> </div> </div>))}</div> <div className="mt-8 flex justify-end space-x-3"> <Button variant="outline" onClick={onClose}> Cancelar </Button> <Button onClick={()=> setStep(2)}rightIcon={<ArrowRight className="w-4 h-4" />}> Continuar </Button> </div> </>)}{}{step === 2 &&(<div className="text-center"> <MessageSquare className="w-16 h-16 text-blue-600 mx-auto mb-6" /> <h3 className="text-lg font-semibold text-gray-900 mb-4"> Enviar SMS al Cliente </h3> <div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6"> <p className="text-sm text-yellow-800"> Se enviará un SMS al número{" "}<strong>{customerData?.movil}</strong> con: </p> <ul className="text-sm text-yellow-700 mt-2 list-disc list-inside"> <li>Enlace seguro para acceder al contrato</li> <li>Código de validación de 4 cifras</li> <li>Instrucciones para completar la firma</li> </ul> </div> <div className="space-y-4"> <Button onClick={handleSendSMS}isLoading={isLoading}disabled={isLoading}size="lg" leftIcon={<MessageSquare className="w-5 h-5" />}>{isLoading ? "Enviando SMS..." : "Enviar SMS al Cliente"}</Button> <Button variant="outline" onClick={()=> setStep(1)}> Volver </Button> </div> </div>)}{}{step === 3 &&(<div className="text-center"> <div className="mb-6"> <div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4"> <Check className="w-8 h-8 text-green-600" /> </div> <h3 className="text-lg font-semibold text-gray-900 mb-2"> SMS Enviado Exitosamente </h3> <p className="text-gray-600"> El cliente ha recibido el enlace y el código de validación </p> </div> <div className="bg-green-50 border border-green-200 rounded-lg p-6 mb-6"> <div className="text-left"> <h4 className="font-medium text-green-800 mb-3"> Información enviada al cliente: </h4> <div className="space-y-2 text-sm text-green-700"> <div className="flex justify-between"> <span>Código de validación:</span> <span className="font-mono font-bold">{smsCode}</span> </div> <div className="flex justify-between"> <span>Enlace:</span> <span className="font-mono text-xs"> https: </span> </div> </div> </div> </div> <div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6"> <div className="flex items-center space-x-3"> <Clock className="w-5 h-5 text-blue-600" /> <p className="text-sm text-blue-800"> <strong>Estado:</strong> Esperando que el cliente complete la firma digital </p> </div> </div> <div className="space-y-3"> <Button variant="outline" onClick={onClose}className="w-full"> Cerrar y Continuar Trabajando </Button> <p className="text-xs text-gray-500"> Recibirás una notificación cuando el cliente complete la firma </p> </div> </div>)}</div> </div> </div>)};
==================================================
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<HTMLButtonElement>{variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger';size?: 'sm' | 'md' | 'lg';isLoading?: boolean;fullWidth?: boolean;leftIcon?: React.ReactNode;rightIcon?: React.ReactNode}const Button = forwardRef<HTMLButtonElement,ButtonProps>(({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 ref={ref}className={combinedClasses}disabled={disabled || isLoading}{...props}>{isLoading ?(<Loader2 size={iconSize}className="animate-spin mr-2" />): leftIcon ?(<span className="mr-2" style={{width: iconSize,height: iconSize}}>{leftIcon}</span>): null}{children}{rightIcon && !isLoading &&(<span className="ml-2" style={{width: iconSize,height: iconSize}}>{rightIcon}</span>)}</button>)});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<InputHTMLAttributes<HTMLInputElement>,'size'>{label?: string;error?: string;helperText?: string;size?: 'sm' | 'md' | 'lg';leftIcon?: React.ReactNode;rightIcon?: React.ReactNode;fullWidth?: boolean}const Input = forwardRef<HTMLInputElement,InputProps>(({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(<div className={clsx('relative',fullWidth && 'w-full')}>{label &&(<label className="block text-sm font-medium text-secondary-700 mb-1">{label}</label>)}<div className="relative">{leftIcon &&(<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> <span className="text-secondary-400" style={{width: iconSize,height: iconSize}}>{leftIcon}</span> </div>)}<input ref={ref}type={inputType}className={inputClasses}disabled={disabled}{...props}/>{isPassword &&(<div className="absolute inset-y-0 right-0 pr-3 flex items-center"> <button type="button" onClick={()=> setShowPassword(!showPassword)}className="text-secondary-400 hover:text-secondary-600 focus:outline-none" disabled={disabled}>{showPassword ?(<EyeOff size={iconSize}/>):(<Eye size={iconSize}/>)}</button> </div>)}{rightIcon && !isPassword &&(<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none"> <span className="text-secondary-400" style={{width: iconSize,height: iconSize}}>{rightIcon}</span> </div>)}</div>{error &&(<div className="mt-1 flex items-center text-sm text-error-600"> <AlertCircle size={14}className="mr-1" />{error}</div>)}{helperText && !error &&(<p className="mt-1 text-sm text-secondary-500">{helperText}</p>)}</div>)});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<LoadingSpinnerProps> =({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(<div className={clsx('flex items-center justify-center space-x-2',className)}> <Loader2 className={clsx('animate-spin',sizeClasses[size],variantClasses[variant])}/> <span className={clsx('text-secondary-600',textSizeClasses[size])}>{text}</span> </div>)}return(<div className={clsx('flex justify-center',className)}> <Loader2 className={clsx('animate-spin',sizeClasses[size],variantClasses[variant])}/> </div>)};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<ModalProps> =({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<HTMLDivElement>)=>{if(event.target === event.currentTarget && closeOnOverlayClick){onClose()}};const modalContent =(<div className="fixed inset-0 z-50 overflow-y-auto"> <div className="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0" onClick={handleOverlayClick}>{}<div className="fixed inset-0 transition-opacity bg-black bg-opacity-50" />{}<div className={clsx('inline-block w-full overflow-hidden text-left align-bottom transition-all transform bg-white rounded-lg shadow-xl sm:align-middle',sizeClasses[size])}>{}{(title || showCloseButton)&&(<div className="flex items-center justify-between px-6 py-4 border-b border-secondary-200">{title &&(<h3 className="text-lg font-medium text-secondary-900">{title}</h3>)}{showCloseButton &&(<Button variant="ghost" size="sm" onClick={onClose}className="p-1 -mr-1" > <X size={20}/> </Button>)}</div>)}{}<div className="px-6 py-4">{children}</div>{}{footer &&(<div className="px-6 py-4 border-t border-secondary-200 bg-secondary-50">{footer}</div>)}</div> </div> </div>);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<SelectHTMLAttributes<HTMLSelectElement>,'size'>{label?: string;error?: string;helperText?: string;size?: 'sm' | 'md' | 'lg';fullWidth?: boolean;options: SelectOption[];placeholder?: string}const Select = forwardRef<HTMLSelectElement,SelectProps>(({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(<div className={clsx('relative',fullWidth && 'w-full')}>{label &&(<label className="block text-sm font-medium text-secondary-700 mb-1">{label}</label>)}<div className="relative"> <select ref={ref}className={selectClasses}disabled={disabled}{...props}>{placeholder &&(<option value="" disabled>{placeholder}</option>)}{options.map((option)=>(<option key={option.value}value={option.value}disabled={option.disabled}>{option.label}</option>))}</select> <div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none"> <ChevronDown size={iconSize}className="text-secondary-400" /> </div> </div>{error &&(<div className="mt-1 flex items-center text-sm text-error-600"> <AlertCircle size={14}className="mr-1" />{error}</div>)}{helperText && !error &&(<p className="mt-1 text-sm text-secondary-500">{helperText}</p>)}</div>)});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(<div className="min-h-screen flex items-center justify-center"> <LoadingSpinner size="lg" text="Procesando venta..." /> </div>)}return(<div className="max-w-4xl mx-auto space-y-8">{}<div> <h1 className="text-2xl font-bold text-gray-900"> Formulario de Ventas </h1> <p className="text-gray-600">Registra una nueva venta en el sistema</p> </div> <div className="space-y-8">{}<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <h2 className="text-lg font-semibold text-gray-900 mb-4"> Datos del Cliente </h2> <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <Input label="Nombre completo *" value={formData.nombre}onChange={(e)=> handleInputChange("nombre",e.target.value)}error={errors.nombre}/> <Input type="email" label="Correo electrónico *" value={formData.email}onChange={(e)=> handleInputChange("email",e.target.value)}error={errors.email}/> <Input type="tel" label="Móvil *" placeholder="612345678" value={formData.movil}onChange={(e)=> handleInputChange("movil",e.target.value)}error={errors.movil}/> </div> </div>{}<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <h2 className="text-lg font-semibold text-gray-900 mb-4"> Datos del Socio/Pagador </h2> <div className="mb-4"> <Select label="¿Quién paga? *" options={PERSON_TYPE_OPTIONS}value={formData.socio_o_pagador}onChange={(e)=> handleInputChange("socio_o_pagador",e.target.value)}error={errors.socio_o_pagador}/> </div>{formData.socio_o_pagador === "OTRA_PERSONA" &&(<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <Input label="Nombre del pagador" value={formData.nombre_2_opcional || ""}onChange={(e)=> handleInputChange("nombre_2_opcional",e.target.value)}/> <Input type="email" label="Email del pagador" value={formData.email_2_opcional || ""}onChange={(e)=> handleInputChange("email_2_opcional",e.target.value)}/> <Input type="tel" label="Móvil del pagador" placeholder="612345678" value={formData.movil_2_opcional || ""}onChange={(e)=> handleInputChange("movil_2_opcional",e.target.value)}/> </div>)}</div>{}<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <h2 className="text-lg font-semibold text-gray-900 mb-4"> Datos de Facturación </h2> <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <Select label="Tipo de datos fiscales *" options={FISCAL_DATA_TYPES}value={formData.datos_fiscales}onChange={(e)=> handleInputChange("datos_fiscales",e.target.value)}error={errors.datos_fiscales}/> <Select label="¿Transferencia anticipada? *" options={YES_NO_OPTIONS}value={formData.transferencia_anticipada}onChange={(e)=> handleInputChange("transferencia_anticipada",e.target.value)}error={errors.transferencia_anticipada}/> </div> </div>{}<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <h2 className="text-lg font-semibold text-gray-900 mb-4"> Datos de Pago </h2> <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <Select label="Modalidad de pago *" options={PAYMENT_METHODS}value={formData.modalidad_de_pago}onChange={(e)=> handleInputChange("modalidad_de_pago",e.target.value)}error={errors.modalidad_de_pago}/> <Select label="Número de pagos *" options={PAYMENT_INSTALLMENTS}value={formData.numero_pagos_meses}onChange={(e)=> handleInputChange("numero_pagos_meses",parseInt(e.target.value))}error={errors.numero_pagos_meses}/> <Input type="date" label="Fecha de pago *" value={formData.fecha_pago}onChange={(e)=> handleInputChange("fecha_pago",e.target.value)}error={errors.fecha_pago}/> </div> </div>{}<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <h2 className="text-lg font-semibold text-gray-900 mb-4"> Datos del Curso </h2> <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <Select label="Curso *" options={cursosOptions}value={formData.curso}onChange={(e)=> handleInputChange("curso",e.target.value)}error={errors.curso}/> <Input label="Formación *" value={formData.formacion}onChange={(e)=> handleInputChange("formacion",e.target.value)}error={errors.formacion}/> <Input label="Costo de la formación(€)*" placeholder="1299.99" value={formData.costo_formacion}onChange={(e)=> handleInputChange("costo_formacion",e.target.value)}error={errors.costo_formacion}/> </div> </div>{}<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <h2 className="text-lg font-semibold text-gray-900 mb-4"> Bonus Incluidos </h2> <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="space-y-3"> <label className="flex items-center"> <input type="checkbox" checked={formData.bonus.mentoria_individualizada_1}onChange={(e)=> handleInputChange("bonus.mentoria_individualizada_1",e.target.checked)}className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" /> <span className="ml-2 text-sm text-gray-700"> Mentoría individualizada 1 </span> </label> <label className="flex items-center"> <input type="checkbox" checked={formData.bonus.mentoria_individualizada_2}onChange={(e)=> handleInputChange("bonus.mentoria_individualizada_2",e.target.checked)}className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" /> <span className="ml-2 text-sm text-gray-700"> Mentoría individualizada 2 </span> </label> </div> <div className="space-y-3"> <label className="flex items-center"> <input type="checkbox" checked={formData.bonus.mentoria_individualizada_3}onChange={(e)=> handleInputChange("bonus.mentoria_individualizada_3",e.target.checked)}className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" /> <span className="ml-2 text-sm text-gray-700"> Mentoría individualizada 3 </span> </label> <label className="flex items-center"> <input type="checkbox" checked={formData.bonus.mentoria_validacion_productos}onChange={(e)=> handleInputChange("bonus.mentoria_validacion_productos",e.target.checked)}className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" /> <span className="ml-2 text-sm text-gray-700"> Mentoría validación de productos </span> </label> </div> </div> </div>{}<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <h2 className="text-lg font-semibold text-gray-900 mb-4"> Otros Detalles </h2> <textarea rows={4}className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="Información adicional,notas especiales,etc." value={formData.otros_detalles}onChange={(e)=> handleInputChange("otros_detalles",e.target.value)}/> </div>{}<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <h2 className="text-lg font-semibold text-gray-900 mb-4"> Asignaciones </h2> <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <Select label="Closer de cierre *" options={closersOptions}value={formData.closer_de_cierre}onChange={(e)=> handleInputChange("closer_de_cierre",e.target.value)}error={errors.closer_de_cierre}/> <Input label="Setter *" value={formData.setter}onChange={(e)=> handleInputChange("setter",e.target.value)}error={errors.setter}/> <Select label="Urgencia de contacto *" options={CONTACT_URGENCY_OPTIONS}value={formData.urgencia_contacto}onChange={(e)=> handleInputChange("urgencia_contacto",e.target.value)}error={errors.urgencia_contacto}/> </div> </div>{}<div className="flex justify-end space-x-4"> <Button type="button" variant="outline" onClick={handleReset}> Limpiar Formulario </Button> <Button onClick={onSubmit}isLoading={isSubmitting}disabled={isSubmitting}leftIcon={<FileText className="w-4 h-4" />}>{isSubmitting ? "Procesando..." : "Crear Venta y Enviar Contrato"}</Button> </div>{}<ContractProcessModal isOpen={showContractModal}onClose={()=> setShowContractModal(false)}onSendSMS={handleSMSSent}customerData={saleData}/> </div> </div>)};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