CoolFace
Apppublic

DataScope26/WEB

sourceHugging Faceupdated 7d agoView on Hugging Face
0likes
CompanySettings.jsx789 linesDownload Raw Back to pages
1import {2  Building2,3  CreditCard,4  AlertTriangle,5  FileText,6} from "lucide-react";7import { useState, useRef, useEffect, useCallback } from "react";8import "./CompanySettings.css";9import { useAuth } from "../context/AuthContext.jsx";10import { useNavigate } from "react-router-dom";11import { useCompanySettings } from "../hooks/useCompanySettings.jsx";12import ButtonAlert from "../components/ButtonAlert.jsx";13import ButtonPrimary from "../components/ButtonPrimary.jsx";14import ButtonSecondary from "../components/ButtonSecondary.jsx";15import AreaManagementCard from "../components/AreaManagementCard.jsx";16 17const CROP_SIZE = 280;18const OUTPUT_SIZE = 500;19 20// Map tiers to user-facing labels and optional classes21const PLAN_META = {22  basic: { label: "Acceso Anticipado", className: "cs-plan--basic" },23  standard: { label: "Pro", className: "cs-plan--pro" },24  advanced: { label: "Enterprise", className: "cs-plan--enterprise" },25};26 27function CardShell({ icon, title, badge, children, className = "" }) {28  return (29    <section className={`cs-card ${className}`}>30      <div className="cs-card-header">31        <div className="cs-card-header-left">32          <span className="cs-card-icon">{icon}</span>33          <h2 className="cs-card-title">{title}</h2>34        </div>35 36        {badge}37      </div>38 39      {children}40    </section>41  );42}43 44function Field({ label, children }) {45  return (46    <label className="cs-field">47      <span className="cs-field-label">{label}</span>48      {children}49    </label>50  );51}52 53export default function CompanySettings() {54  const { user, selectedCompany, setSelectedCompany, updateCompanyInUser, setUser } = useAuth();55  const navigate = useNavigate();56  const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);57  const [showDiscardConfirm, setShowDiscardConfirm] = useState(false);58  const [discardNotice, setDiscardNotice] = useState("");59  const [showLogoModal, setShowLogoModal] = useState(false);60  const [selectedLogoUrl, setSelectedLogoUrl] = useState(null);61  const [logoScale, setLogoScale] = useState(1);62  const [logoMinScale, setLogoMinScale] = useState(1);63  const [logoOffset, setLogoOffset] = useState({ x: 0, y: 0 });64  const [isSavingLogo, setIsSavingLogo] = useState(false);65 66  const { legalName, setLegalName, saving, deleting, uploadingLogo,67    statusMessage, errorMessage, handleDiscard, handleSave, handleDelete,68    handleLogoUpload, } = useCompanySettings(selectedCompany);69 70  const fileInputRef = useRef(null);71  const logoCanvasRef = useRef(null);72  const logoImgRef = useRef(null);73  const isDraggingRef = useRef(false);74  const lastPosRef = useRef({ x: 0, y: 0 });75 76  const companyLogo = selectedCompany?.logo && selectedCompany.logo.trim() ? selectedCompany.logo : "";77 78  const confirmDiscardChanges = () => {79    handleDiscard();80    setDiscardNotice("Cambios descartados.");81    setShowDiscardConfirm(false);82    navigate("/")83    setTimeout(() => setDiscardNotice(""), 3000);84  };85 86  // Protect route on load/reload: if user has no companies, redirect to overview (dashboard shows create prompt).87  // If user's company list no longer contains the currently selected company (deleted), pick the first available.88  useEffect(() => {89    if (!user) return;90 91    const companies = user.companies || [];92 93    if (companies.length === 0) {94      setSelectedCompany(null);95      navigate("/");96      return;97    }98 99    const selectedId = selectedCompany?.id ?? selectedCompany?.companyId;100    const hasSelected = selectedId && companies.some((c) => String(c.id ?? c.companyId) === String(selectedId));101 102    if (!hasSelected) {103      // pick first available company104      setSelectedCompany(companies[0]);105    }106  }, [user, selectedCompany, setSelectedCompany, navigate]);107 108  const openLogoModal = () => {109    setSelectedLogoUrl(null);110    setLogoScale(1);111    setLogoMinScale(1);112    setLogoOffset({ x: 0, y: 0 });113    setShowLogoModal(true);114  };115 116  const closeLogoModal = () => {117    if (isSavingLogo) return;118 119    if (selectedLogoUrl) {120      URL.revokeObjectURL(selectedLogoUrl);121    }122 123    setSelectedLogoUrl(null);124    setShowLogoModal(false);125  };126 127  const handleLogoChosen = (e) => {128    const file = e.target.files?.[0];129    if (!file) return;130 131    if (!file.type.startsWith("image/")) {132      return;133    }134 135    const url = URL.createObjectURL(file);136 137    if (selectedLogoUrl) {138      URL.revokeObjectURL(selectedLogoUrl);139    }140 141    setSelectedLogoUrl(url);142    setLogoScale(1);143    setLogoMinScale(1);144    setLogoOffset({ x: 0, y: 0 });145    e.target.value = "";146  };147 148  const triggerLogoFileSelect = () => {149    fileInputRef.current?.click();150  };151 152  useEffect(() => {153    if (!selectedLogoUrl) {154      logoImgRef.current = null;155      return;156    }157 158    const img = new Image();159 160    img.onload = () => {161      logoImgRef.current = img;162 163      const coverScale = Math.max(164        CROP_SIZE / img.width,165        CROP_SIZE / img.height166      );167 168      setLogoMinScale(coverScale);169      setLogoScale(coverScale);170 171      const drawWidth = img.width * coverScale;172      const drawHeight = img.height * coverScale;173 174      setLogoOffset({175        x: (CROP_SIZE - drawWidth) / 2,176        y: (CROP_SIZE - drawHeight) / 2,177      });178    };179 180    img.src = selectedLogoUrl;181  }, [selectedLogoUrl]);182 183  const clampOffset = useCallback((nextOffset, currentScale) => {184    const img = logoImgRef.current;185 186    if (!img) return nextOffset;187 188    const drawWidth = img.width * currentScale;189    const drawHeight = img.height * currentScale;190 191    const minX = CROP_SIZE - drawWidth;192    const minY = CROP_SIZE - drawHeight;193 194    return {195      x: Math.min(0, Math.max(minX, nextOffset.x)),196      y: Math.min(0, Math.max(minY, nextOffset.y)),197    };198  }, []);199 200  useEffect(() => {201    const canvas = logoCanvasRef.current;202    const img = logoImgRef.current;203 204    if (!canvas || !img || !selectedLogoUrl) return;205 206    const ctx = canvas.getContext("2d");207    ctx.clearRect(0, 0, CROP_SIZE, CROP_SIZE);208    ctx.drawImage(209      img,210      logoOffset.x,211      logoOffset.y,212      img.width * logoScale,213      img.height * logoScale214    );215  }, [logoScale, logoOffset, selectedLogoUrl]);216 217  const handleLogoZoomChange = (e) => {218    const newScale = parseFloat(e.target.value);219    const center = CROP_SIZE / 2;220 221    // Punto de la imagen que actualmente está en el centro del círculo de recorte.222    const imgCenterX = (center - logoOffset.x) / logoScale;223    const imgCenterY = (center - logoOffset.y) / logoScale;224 225    // Recalculamos el offset para que ese mismo punto siga en el centro226    // después de aplicar el nuevo scale (zoom centrado, no desde la esquina).227    const nextOffset = {228      x: center - imgCenterX * newScale,229      y: center - imgCenterY * newScale,230    };231 232    setLogoScale(newScale);233    setLogoOffset(clampOffset(nextOffset, newScale));234  };235 236  const handleLogoPointerDown = (e) => {237    if (!logoImgRef.current) return;238 239    isDraggingRef.current = true;240    const point = e.touches ? e.touches[0] : e;241 242    lastPosRef.current = {243      x: point.clientX,244      y: point.clientY,245    };246  };247 248  const handleLogoPointerMove = (e) => {249    if (!isDraggingRef.current || !logoImgRef.current) return;250 251    const point = e.touches ? e.touches[0] : e;252    const dx = point.clientX - lastPosRef.current.x;253    const dy = point.clientY - lastPosRef.current.y;254 255    lastPosRef.current = {256      x: point.clientX,257      y: point.clientY,258    };259 260    setLogoOffset((prev) =>261      clampOffset(262        {263          x: prev.x + dx,264          y: prev.y + dy,265        },266        logoScale267      )268    );269  };270 271  const handleLogoPointerUp = () => {272    isDraggingRef.current = false;273  };274 275  const handleSaveCroppedLogo = async () => {276    const img = logoImgRef.current;277    if (!img) return;278 279    setIsSavingLogo(true);280 281    try {282      const outputCanvas = document.createElement("canvas");283      outputCanvas.width = OUTPUT_SIZE;284      outputCanvas.height = OUTPUT_SIZE;285 286      const ctx = outputCanvas.getContext("2d");287      ctx.imageSmoothingEnabled = true;288      ctx.imageSmoothingQuality = "high";289 290      const ratio = OUTPUT_SIZE / CROP_SIZE;291 292      ctx.drawImage(293        img,294        logoOffset.x * ratio,295        logoOffset.y * ratio,296        img.width * logoScale * ratio,297        img.height * logoScale * ratio298      );299 300      const webpBlob = await new Promise((resolve, reject) => {301        outputCanvas.toBlob(302          (blob) => {303            if (!blob) {304              reject(new Error("Failed to create image"));305              return;306            }307            resolve(blob);308          },309          "image/webp",310          0.9311        );312      });313 314      const croppedFile = new File([webpBlob], "company-logo.webp", {315        type: "image/webp",316      });317 318      await handleLogoUpload(croppedFile, (updatedCompany) => {319        updateCompanyInUser(updatedCompany);320        setSelectedCompany((currentCompany) => {321          if (!currentCompany) return updatedCompany;322 323          const currentId = currentCompany.id ?? currentCompany.companyId;324          const updatedId = updatedCompany.id ?? updatedCompany.companyId;325 326          return currentId && updatedId && currentId === updatedId327            ? { ...currentCompany, ...updatedCompany }328            : updatedCompany;329        });330      });331 332      if (selectedLogoUrl) {333        URL.revokeObjectURL(selectedLogoUrl);334      }335 336      setSelectedLogoUrl(null);337      setShowLogoModal(false);338    } catch (err) {339      console.error("Error al guardar el logo de empresa:", err);340    } finally {341      setIsSavingLogo(false);342    }343  };344 345  const onLogoFileChange = (e) => {346    const file = e.target.files?.[0];347    if (!file) return;348    handleLogoUpload(file, (updatedCompany) => {349      updateCompanyInUser(updatedCompany);350      setSelectedCompany((currentCompany) => {351        if (!currentCompany) return updatedCompany;352 353        const currentId = currentCompany.id ?? currentCompany.companyId;354        const updatedId = updatedCompany.id ?? updatedCompany.companyId;355 356        return currentId && updatedId && currentId === updatedId357          ? { ...currentCompany, ...updatedCompany }358          : updatedCompany;359      });360    });361    e.target.value = "";362  };363 364  const planMeta =365    PLAN_META[selectedCompany?.tier] || {366      label: "Personalizado",367      className: "",368    };369 370  return (371    <div className="cs-page">372      <div className="cs-container">373        {!selectedCompany ? (374          <div className="cs-no-company">375            <h2>No hay empresa seleccionada</h2>376 377            <p>378              Seleccioná una empresa en{" "}379              <ButtonPrimary380                type="button"381                className="cs-link"382                onClick={() => navigate("/my-companies")}383              >384                Mis Empresas385              </ButtonPrimary>{" "}386              para editar su configuración.387            </p>388          </div>389        ) : (390          <>391            {/* Header */}392            <header className="cs-header">393              <h1 className="cs-title">394                {selectedCompany.name ||395                  "Configuración de Empresa"}396              </h1>397 398              <p className="cs-subtitle">399                {selectedCompany.name400                  ? `Configura ${selectedCompany.name} — administra identidad, políticas y permisos.`401                  : "Administra los parámetros críticos de tu organización."}402              </p>403            </header>404 405            <div className="cs-stack">406              {/* Row 1: Perfil + Plan y Facturación */}407              <div className="cs-row">408                <CardShell409                  icon={<Building2 size={17} />}410                  title="Perfil de la Empresa"411                  className="cs-col-2"412                >413                  <div className="cs-profile-top">414                    <ButtonPrimary415                      type="button"416                      className="cs-logo-button"417                      onClick={openLogoModal}418                      aria-label={`Cambiar logo de ${selectedCompany.name || "la empresa"}`}419                    >420                      {companyLogo ? (421                        <img src={companyLogo} alt={`Logo de ${selectedCompany.name || "la empresa"}`} className="cs-logo-img" />422                      ) : (423                        <div className="cs-logo-mark" aria-label={`Sin logo — ${selectedCompany.name || "la empresa"}`}>424                          <Building2 size={32} strokeWidth={1.5} />425                        </div>426                      )}427                    </ButtonPrimary>428 429                    <div className="cs-profile-fields">430                      <Field label="Nombre Legal">431                        <input432                          type="text"433                          value={legalName}434                          onChange={(e) => setLegalName(e.target.value)}435                          className="cs-input"436                        />437                      </Field>438                    </div>439                  </div>440                </CardShell>441 442                <CardShell443                  icon={<CreditCard size={17} />}444                  title="Plan y Facturación"445                  badge={446                    <span447                      className={`cs-badge ${planMeta.className}`}448                    >449                      {planMeta.label}450                    </span>451                  }452                  className="cs-col-3"453                >454                  <div className="cs-billing-top">455                    <div>456                      <p className="cs-billing-label">457                        Próxima Factura458                      </p>459 460                      <p className="cs-billing-date">461                        1 Enero, 2020462                      </p>463                    </div>464 465                    <p className="cs-billing-price">466                      {planMeta.label === "Basic"467                        ? "$5"468                        : planMeta.label === "Pro" ? "$15"469                          : planMeta.label === "Enterprise" ? "$50"470                            : "$0"}471                      <span className="cs-billing-price-unit">472                        /mes473                      </span>474                    </p>475                  </div>476 477                  <div className="cs-billing-card">478                    <div className="cs-billing-card-left">479                      <CreditCard480                        size={18}481                        className="cs-billing-card-icon"482                      />483 484                      <div>485                        <p className="cs-billing-card-title">486                          Visa terminada en 1111487                        </p>488 489                        <p className="cs-billing-card-sub">490                          Vence 1/1491                        </p>492                      </div>493                    </div>494 495                    <ButtonSecondary496                      type="button"497                      className="cs-btn-ghost"498                    >499                      Gestionar500                    </ButtonSecondary>501                  </div>502 503                </CardShell>504              </div>505 506              {/* Area Management */}507              <AreaManagementCard companyId={selectedCompany?.id || selectedCompany?.companyId} />508 509              {/* Row 2: Zona de Peligro */}510              <div className="cs-row">511                <CardShell512                  icon={513                    <AlertTriangle514                      size={17}515                      className="cs-card-icon--danger"516                    />517                  }518                  title="Zona de Peligro"519                  className="cs-col-2 cs-card--danger"520                >521                  <p className="cs-danger-text">522                    Estas acciones son irreversibles. Al523                    eliminar tu cuenta, todos los modelos524                    entrenados, logs y configuraciones serán525                    borrados permanentemente de nuestros526                    clústeres.527                  </p>528 529                  <div className="cs-danger-actions">                    530                    <ButtonAlert531                      type="button"532                      className="cs-btn-danger-solid"533                      onClick={() => setShowDeleteConfirm(true)}534                      disabled={deleting}535                    >536                      {deleting ? "Borrando..." : "Borrar Empresa"}537                    </ButtonAlert>538                  </div>539                </CardShell>540              </div>541            </div>542 543            {/* Footer actions */}544            <div className="cs-footer">545              <ButtonSecondary546                type="button"547                className="cs-btn-ghost"548                onClick={() => setShowDiscardConfirm(true)}549                disabled={saving}550              >551                Descartar552              </ButtonSecondary>553 554              <ButtonPrimary555                type="button"556                className="cs-btn-primary"557                style={{558                  padding: "10px 20px",559                }}560                onClick={() =>561                  handleSave((updatedCompany) => {562                    563                    updateCompanyInUser(updatedCompany);564                    setSelectedCompany((currentCompany) => {565                      if (!currentCompany) return updatedCompany;566 567                      const currentId = currentCompany.id ?? currentCompany.companyId;568                      const updatedId = updatedCompany.id ?? updatedCompany.companyId;569                      570                      return currentId && updatedId && currentId === updatedId571                        ? { ...currentCompany, ...updatedCompany }572                        : updatedCompany;573                    });574 575                    navigate("/overview");576                    577                  })578                }579                disabled={saving}580              >581                {saving582                  ? "Guardando..."583                  : "Guardar Cambios"}584              </ButtonPrimary>585            </div>586 587            {discardNotice && (588              <p className="cs-status-message" role="status">589                {discardNotice}590              </p>591            )}592 593            {statusMessage && (594              <p className="cs-status-message" role="status">595                {statusMessage}596              </p>597            )}598 599            {errorMessage && (600              <p className="cs-error-message" role="alert">601                {errorMessage}602              </p>603            )}604 605            {showDeleteConfirm && (606              <div className="cs-modal-overlay">607                <div className="cs-modal">608                  <h3 className="cs-modal-title">609                    ¿Estás completamente seguro?610                  </h3>611 612                  <p className="cs-modal-description">613                    Esta acción es irreversible. Se borrarán permanentemente:614                  </p>615 616                  <ul className="cs-modal-list">617                    <li>La empresa y todos sus datos</li>618                    <li>Todos los modelos entrenados</li>619                    <li>Todos los logs y configuraciones</li>620                  </ul>621 622                  <div className="cs-modal-actions">623                    <ButtonSecondary624                      type="button"625                      className="cs-btn-ghost"626                      onClick={() => setShowDeleteConfirm(false)}627                      disabled={deleting}628                    >629                      Cancelar630                    </ButtonSecondary>631 632                    <ButtonAlert633                      type="button"634                      className="cs-btn-danger-solid"635                      onClick={() =>636                        handleDelete((response) => {637                          // Remove deleted company from user.companies and pick another company if available638                          const deletedCompanyId = selectedCompany?.id ?? selectedCompany?.companyId;639 640                          setUser((currentUser) => {641                            if (!currentUser) return currentUser;642 643                            const nextCompanies = (currentUser.companies || []).filter((c) => {644                              const id = c.id ?? c.companyId;645                              return id && String(id) !== String(deletedCompanyId);646                            });647 648                            return { ...currentUser, companies: nextCompanies };649                          });650 651                          // Clear current selected company — AuthContext will pick a new one if any remain652                          setSelectedCompany(null);653 654                          setShowDeleteConfirm(false);655                          navigate("/");656                        })657                      }658                      disabled={deleting}659                    >660                      {deleting ? "Borrando..." : "Sí, Borrar Permanentemente"}661                    </ButtonAlert>662                  </div>663                </div>664              </div>665            )}666 667            {showDiscardConfirm && (668              <div className="cs-modal-overlay">669                <div className="cs-modal">670                  <h3 className="cs-modal-title">Descartar cambios</h3>671 672                  <p className="cs-modal-description">673                    Si continúas, se perderán los cambios hechos en la configuración de la empresa.674                  </p>675 676                  <div className="cs-modal-actions">677                    <ButtonSecondary678                      type="button"679                      className="cs-btn-ghost"680                      onClick={() => setShowDiscardConfirm(false)}681                    >682                      Cancelar683                    </ButtonSecondary>684 685                    <ButtonAlert686                      type="button"687                      className="cs-btn-danger-solid"688                      onClick={confirmDiscardChanges}689                    >690                      Sí, descartar691                    </ButtonAlert>692                  </div>693                </div>694              </div>695            )}696 697            {showLogoModal && (698              <div className="cs-logo-modal-overlay" onClick={closeLogoModal}>699                <div className="cs-logo-modal-content" onClick={(e) => e.stopPropagation()}>700                  <h3 className="cs-logo-modal-title">Logo de la empresa</h3>701 702                  {!selectedLogoUrl && (703                    <>704                      <div className="cs-logo-current-frame">705                        {companyLogo ? (706                          <img className="cs-logo-current-image" src={companyLogo} alt="Logo actual de la empresa" />707                        ) : (708                        <div className="cs-logo-current-placeholder">709  <Building2 size={96} strokeWidth={1.5} />710</div>711                        )}712                      </div>713                      <ButtonPrimary type="button" className="btn-change-photo" onClick={triggerLogoFileSelect}>714                        Cambiar logo715                      </ButtonPrimary>716                    </>717                  )}718 719                  {selectedLogoUrl && (720                    <>721                      <div722                        className="cs-logo-crop-container"723                        onMouseDown={handleLogoPointerDown}724                        onMouseMove={handleLogoPointerMove}725                        onMouseUp={handleLogoPointerUp}726                        onMouseLeave={handleLogoPointerUp}727                        onTouchStart={handleLogoPointerDown}728                        onTouchMove={handleLogoPointerMove}729                        onTouchEnd={handleLogoPointerUp}730                      >731                        <canvas ref={logoCanvasRef} width={CROP_SIZE} height={CROP_SIZE} />732                        <div className="cs-logo-crop-grid" />733                      </div>734 735                      <div className="cs-logo-zoom-controls">736                        <span className="cs-logo-zoom-label">-</span>737                        <input738                          className="cs-logo-zoom-slider"739                          type="range"740                          min={logoMinScale}741                          max={logoMinScale * 4}742                          step={0.01}743                          value={logoScale}744                          onChange={handleLogoZoomChange}745                        />746                        <span className="cs-logo-zoom-label">+</span>747                      </div>748 749                      <ButtonPrimary type="button" className="btn-change-photo" onClick={triggerLogoFileSelect}>750                        Elegir otro logo751                      </ButtonPrimary>752                    </>753                  )}754 755                  <input756                    ref={fileInputRef}757                    type="file"758                    accept="image/*"759                    onChange={handleLogoChosen}760                    style={{ display: "none" }}761                  />762 763                  <div className="cs-logo-modal-actions">764                    <ButtonPrimary765                      type="button"766                      className="btn-save"767                      disabled={!selectedLogoUrl || isSavingLogo || uploadingLogo}768                      onClick={handleSaveCroppedLogo}769                    >770                      {isSavingLogo || uploadingLogo ? "Guardando..." : "Guardar logo"}771                    </ButtonPrimary>772                    <ButtonAlert773                      type="button"774                      className="btn-logout"775                      onClick={closeLogoModal}776                      disabled={isSavingLogo || uploadingLogo}777                    >778                      Cancelar779                    </ButtonAlert>780                  </div>781                </div>782              </div>783            )}784          </>785        )}786      </div>787    </div>788  );789}