DataScope26/WEB
0
1import React, { useState, useRef, useEffect } from 'react';2import { ArrowLeft, Loader } from 'lucide-react';3 4import './StepCompanyData.css';5import ButtonPrimary from './ButtonPrimary.jsx';6import ButtonSecondary from './ButtonSecondary.jsx';7import ButtonAlert from './ButtonAlert.jsx';8 9const CROP_SIZE = 280;10const OUTPUT_SIZE = 500;11 12export default function StepCompanyData({ data, updateData, onBack, onSubmit, isSubmitting, errors = {} }) {13 const isFormValid = data.name && data.legalName;14 const [validationErrors, setValidationErrors] = useState({});15 const [showLogoModal, setShowLogoModal] = useState(false);16 const [selectedLogoUrl, setSelectedLogoUrl] = useState(null);17 const [logoScale, setLogoScale] = useState(1);18 const [logoMinScale, setLogoMinScale] = useState(1);19 const [logoOffset, setLogoOffset] = useState({ x: 0, y: 0 });20 const [isSavingLogo, setIsSavingLogo] = useState(false);21 22 const fileInputRef = useRef(null);23 const logoCanvasRef = useRef(null);24 const logoImgRef = useRef(null);25 const isDraggingRef = useRef(false);26 const lastPosRef = useRef({ x: 0, y: 0 });27 28 const companyLogo = data.logo && data.logo.trim() ? data.logo : '';29 30 const openLogoModal = () => {31 setSelectedLogoUrl(null);32 setLogoScale(1);33 setLogoMinScale(1);34 setLogoOffset({ x: 0, y: 0 });35 setShowLogoModal(true);36 };37 38 const closeLogoModal = () => {39 if (isSavingLogo) return;40 41 if (selectedLogoUrl) {42 URL.revokeObjectURL(selectedLogoUrl);43 }44 45 setSelectedLogoUrl(null);46 setShowLogoModal(false);47 };48 49 const handleLogoChosen = (e) => {50 const file = e.target.files?.[0];51 if (!file) return;52 53 if (!file.type.startsWith('image/')) {54 return;55 }56 57 const url = URL.createObjectURL(file);58 59 if (selectedLogoUrl) {60 URL.revokeObjectURL(selectedLogoUrl);61 }62 63 setSelectedLogoUrl(url);64 setLogoScale(1);65 setLogoMinScale(1);66 setLogoOffset({ x: 0, y: 0 });67 e.target.value = '';68 };69 70 const triggerLogoFileSelect = () => {71 fileInputRef.current?.click();72 };73 74 useEffect(() => {75 if (!selectedLogoUrl) {76 logoImgRef.current = null;77 return;78 }79 80 const img = new Image();81 82 img.onload = () => {83 logoImgRef.current = img;84 85 const coverScale = Math.max(86 CROP_SIZE / img.width,87 CROP_SIZE / img.height88 );89 90 setLogoMinScale(coverScale);91 setLogoScale(coverScale);92 93 const drawWidth = img.width * coverScale;94 const drawHeight = img.height * coverScale;95 96 setLogoOffset({97 x: (CROP_SIZE - drawWidth) / 2,98 y: (CROP_SIZE - drawHeight) / 2,99 });100 };101 102 img.src = selectedLogoUrl;103 }, [selectedLogoUrl]);104 105 const clampOffset = (nextOffset, currentScale) => {106 const img = logoImgRef.current;107 108 if (!img) return nextOffset;109 110 const drawWidth = img.width * currentScale;111 const drawHeight = img.height * currentScale;112 113 const minX = CROP_SIZE - drawWidth;114 const minY = CROP_SIZE - drawHeight;115 116 return {117 x: Math.min(0, Math.max(minX, nextOffset.x)),118 y: Math.min(0, Math.max(minY, nextOffset.y)),119 };120 };121 122 useEffect(() => {123 const canvas = logoCanvasRef.current;124 const img = logoImgRef.current;125 126 if (!canvas || !img || !selectedLogoUrl) return;127 128 const ctx = canvas.getContext('2d');129 ctx.clearRect(0, 0, CROP_SIZE, CROP_SIZE);130 ctx.drawImage(131 img,132 logoOffset.x,133 logoOffset.y,134 img.width * logoScale,135 img.height * logoScale136 );137 }, [logoScale, logoOffset, selectedLogoUrl]);138 139 const handleLogoZoomChange = (e) => {140 const newScale = Number.parseFloat(e.target.value);141 const center = CROP_SIZE / 2;142 143 // Punto de la imagen que actualmente está en el centro del círculo de recorte.144 const imgCenterX = (center - logoOffset.x) / logoScale;145 const imgCenterY = (center - logoOffset.y) / logoScale;146 147 // Recalculamos el offset para que ese mismo punto siga en el centro148 // después de aplicar el nuevo scale (zoom centrado, no desde la esquina).149 const nextOffset = {150 x: center - imgCenterX * newScale,151 y: center - imgCenterY * newScale,152 };153 154 setLogoScale(newScale);155 setLogoOffset(clampOffset(nextOffset, newScale));156 };157 158 const handleLogoPointerDown = (e) => {159 if (!logoImgRef.current) return;160 161 isDraggingRef.current = true;162 const point = e.touches ? e.touches[0] : e;163 164 lastPosRef.current = {165 x: point.clientX,166 y: point.clientY,167 };168 };169 170 const handleLogoPointerMove = (e) => {171 if (!isDraggingRef.current || !logoImgRef.current) return;172 173 const point = e.touches ? e.touches[0] : e;174 const dx = point.clientX - lastPosRef.current.x;175 const dy = point.clientY - lastPosRef.current.y;176 177 lastPosRef.current = {178 x: point.clientX,179 y: point.clientY,180 };181 182 setLogoOffset((prev) =>183 clampOffset(184 {185 x: prev.x + dx,186 y: prev.y + dy,187 },188 logoScale189 )190 );191 };192 193 const handleLogoPointerUp = () => {194 isDraggingRef.current = false;195 };196 197 const handleSaveCroppedLogo = async () => {198 const img = logoImgRef.current;199 if (!img) return;200 201 setIsSavingLogo(true);202 203 try {204 const outputCanvas = document.createElement('canvas');205 outputCanvas.width = OUTPUT_SIZE;206 outputCanvas.height = OUTPUT_SIZE;207 208 const ctx = outputCanvas.getContext('2d');209 ctx.imageSmoothingEnabled = true;210 ctx.imageSmoothingQuality = 'high';211 212 const ratio = OUTPUT_SIZE / CROP_SIZE;213 214 ctx.drawImage(215 img,216 logoOffset.x * ratio,217 logoOffset.y * ratio,218 img.width * logoScale * ratio,219 img.height * logoScale * ratio220 );221 222 const webpBlob = await new Promise((resolve, reject) => {223 outputCanvas.toBlob(224 (blob) => {225 if (!blob) {226 reject(new Error('Failed to create image'));227 return;228 }229 resolve(blob);230 },231 'image/webp',232 0.9233 );234 });235 236 const croppedFile = new File([webpBlob], 'company-logo.webp', {237 type: 'image/webp',238 });239 240 const previewUrl = URL.createObjectURL(webpBlob);241 updateData({242 logo: previewUrl,243 logoFile: croppedFile,244 });245 246 if (selectedLogoUrl) {247 URL.revokeObjectURL(selectedLogoUrl);248 }249 250 setSelectedLogoUrl(null);251 setShowLogoModal(false);252 } catch (error) {253 console.error('Error al guardar el logo de la empresa:', error);254 } finally {255 setIsSavingLogo(false);256 }257 };258 259 const handleRemoveLogo = () => {260 if (companyLogo) {261 URL.revokeObjectURL(companyLogo);262 }263 264 updateData({265 logo: '',266 logoFile: null,267 });268 };269 270 // Validation rules271 const allowedRe = /^[\p{L}\d .-]+$/u; // Unicode letters, digits, space, dot, hyphen272 273 const validateName = (value) => {274 if (!value || String(value).trim() === '') {275 return 'El nombre es obligatorio.';276 }277 278 if (!/[A-Za-z]/.test(String(value))) {279 return 'El nombre debe contener al menos una letra (A–Z).';280 }281 282 if (!allowedRe.test(String(value))) {283 return 'El nombre solo puede contener letras, números, espacios, puntos y guiones.';284 }285 286 return null;287 };288 289 const validateLegalName = (value) => {290 if (!value || String(value).trim() === '') {291 return 'La razón social es obligatoria.';292 }293 // must include at least one latin letter as well294 if (!/[A-Za-z]/.test(String(value))) {295 return 'La razón social debe contener al menos una letra (A–Z).';296 }297 298 if (!allowedRe.test(String(value))) {299 return 'La razón social solo puede contener letras, números, espacios, puntos y guiones.';300 }301 302 return null;303 };304 305 const handleNameChange = (val) => {306 updateData({ name: val });307 setValidationErrors((prev) => {308 const next = { ...prev };309 const err = validateName(val);310 if (err) next.name = err; else delete next.name;311 return next;312 });313 };314 315 const handleLegalNameChange = (val) => {316 updateData({ legalName: val });317 setValidationErrors((prev) => {318 const next = { ...prev };319 const err = validateLegalName(val);320 if (err) next.legalName = err; else delete next.legalName;321 return next;322 });323 };324 325 const handleNext = () => {326 const nameErr = validateName(data.name);327 const legalErr = validateLegalName(data.legalName);328 329 const nextErrors = {};330 if (nameErr) nextErrors.name = nameErr;331 if (legalErr) nextErrors.legalName = legalErr;332 333 setValidationErrors(nextErrors);334 335 if (Object.keys(nextErrors).length > 0) return;336 337 if (onSubmit) onSubmit();338 };339 340 return (341 <div className="company-data-content">342 <div className="company-data-header">343 <div className="company-data-subtitle-row">344 <ButtonSecondary className="step-back-button" onClick={onBack} aria-label="Volver atrás">345 <ArrowLeft size={20} />346 </ButtonSecondary>347 <div className="company-data-subtitle">Datos generales</div>348 </div>349 <h2 className="company-data-title">Datos de la Organización</h2>350 </div>351 352 <div className="company-data-form">353 <div className="form-input-group">354 <label>Nombre de la Compañía *</label>355 <input356 type="text"357 placeholder="Ej: DataScope Analytics"358 value={data.name}359 onChange={(e) => updateData({ name: e.target.value })}360 style={(errors.name || validationErrors.name) ? { borderColor: '#e53e3e' } : {}}361 maxLength={50}362 />363 {(validationErrors.name || errors.name) && (364 <span className="field-error">{validationErrors.name || errors.name}</span>365 )}366 </div>367 368 <div className="form-input-group">369 <label>Razón Social *</label>370 <input371 type="text"372 placeholder="Ej: DataScope S.A."373 value={data.legalName}374 onChange={(e) => updateData({ legalName: e.target.value })}375 style={errors.legalName ? { borderColor: '#e53e3e' } : {}}376 maxLength={100}377 />378 {errors.legalName && (379 <span className="field-error">{errors.legalName}</span>380 )}381 </div>382 383 <div className="form-input-group company-logo-upload-group">384 <label>LOGO DE LA COMPAÑÍA (OPCIONAL)</label>385 386 <div className="company-logo-picker">387 <button388 type="button"389 className="company-logo-preview"390 onClick={openLogoModal}391 aria-label="Seleccionar logo de la empresa"392 >393 {companyLogo ? (394 <img src={companyLogo} alt="Logo de la empresa" className="company-logo-image" />395 ) : (396 <div className="company-logo-placeholder">+</div>397 )}398 </button>399 400 <div className="company-logo-actions">401 <ButtonSecondary402 type="button"403 className="company-logo-button"404 onClick={openLogoModal}405 >406 {companyLogo ? 'Cambiar logo' : 'Seleccionar logo'}407 </ButtonSecondary>408 409 {companyLogo && (410 <ButtonAlert411 type="button"412 className="company-logo-remove"413 onClick={handleRemoveLogo}414 >415 Quitar416 </ButtonAlert>417 )}418 </div>419 </div>420 421 {errors.logo && (422 <span className="field-error">{errors.logo}</span>423 )}424 </div>425 </div>426 427 <div className="company-data-actions">428 {isSubmitting ? (429 <ButtonPrimary className="btn-next" style={{ cursor: 'default' }}>430 <Loader size={18} className="loader-icon-animation" />431 </ButtonPrimary>432 ) : (433 <ButtonPrimary434 onClick={handleNext}435 disabled={!isFormValid}436 className="btn-next"437 style={{ opacity: isFormValid ? 1 : 0.5, cursor: isFormValid ? 'pointer' : 'not-allowed' }}438 >439 Continuar440 </ButtonPrimary>441 )}442 </div>443 444 {showLogoModal && (445 <div className="cs-logo-modal-overlay" onClick={closeLogoModal}>446 <div className="cs-logo-modal-content" onClick={(e) => e.stopPropagation()}>447 <h3 className="cs-logo-modal-title">Logo de la empresa</h3>448 449 {!selectedLogoUrl && (450 <>451 <div className="cs-logo-current-frame">452 {companyLogo ? (453 <img className="cs-logo-current-image" src={companyLogo} alt="Logo actual de la empresa" />454 ) : (455 <div className="cs-logo-current-placeholder">+</div>456 )}457 </div>458 <ButtonPrimary type="button" className="btn-change-photo" onClick={triggerLogoFileSelect}>459 Cambiar logo460 </ButtonPrimary>461 </>462 )}463 464 {selectedLogoUrl && (465 <>466 <div467 className="cs-logo-crop-container"468 onMouseDown={handleLogoPointerDown}469 onMouseMove={handleLogoPointerMove}470 onMouseUp={handleLogoPointerUp}471 onMouseLeave={handleLogoPointerUp}472 onTouchStart={handleLogoPointerDown}473 onTouchMove={handleLogoPointerMove}474 onTouchEnd={handleLogoPointerUp}475 >476 <canvas ref={logoCanvasRef} width={CROP_SIZE} height={CROP_SIZE} />477 <div className="cs-logo-crop-grid" />478 </div>479 480 <div className="cs-logo-zoom-controls">481 <span className="cs-logo-zoom-label">-</span>482 <input483 className="cs-logo-zoom-slider"484 type="range"485 min={logoMinScale}486 max={logoMinScale * 4}487 step={0.01}488 value={logoScale}489 onChange={handleLogoZoomChange}490 />491 <span className="cs-logo-zoom-label">+</span>492 </div>493 494 <ButtonPrimary type="button" className="btn-change-photo" onClick={triggerLogoFileSelect}>495 Elegir otro logo496 </ButtonPrimary>497 </>498 )}499 500 <input501 ref={fileInputRef}502 type="file"503 accept="image/*"504 onChange={handleLogoChosen}505 style={{ display: 'none' }}506 />507 508 <div className="cs-logo-modal-actionss">509 <ButtonPrimary510 type="button"511 className="btn-save"512 disabled={!selectedLogoUrl || isSavingLogo}513 onClick={handleSaveCroppedLogo}514 >515 {isSavingLogo ? 'Guardando...' : 'Guardar logo'}516 </ButtonPrimary>517 <ButtonAlert518 type="button"519 className="btn-logout"520 onClick={closeLogoModal}521 disabled={isSavingLogo}522 >523 Cancelar524 </ButtonAlert>525 </div>526 </div>527 </div>528 )}529 </div>530 );531}