feat: nombre y logo de empresa configurables (solo dev, no desplegado a prod)
Nueva marca configurable desde Configuracion -> Empresa (solo superadmin): nombre y logo, guardados en system_settings (mismo patron que SMTP, sin tabla nueva). Nuevo endpoint publico GET /api/company (+ /logo) para que login/registro/recuperacion de clave puedan mostrar la marca antes de iniciar sesion. Sin configurar, se ve "LinkedCorp" y el logo estatico de siempre. Probado en dev (build + Playwright: panel de Configuracion, sidebar expandido/compacto, login publico), sin desplegar a produccion por pedido explicito del usuario -- este commit sirve de punto de control en git antes de decidir el despliegue. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Save, Upload } from 'lucide-react';
|
||||
import { useUi } from '../../context/ui-context.jsx';
|
||||
import { useBranding } from '../../context/branding-context.jsx';
|
||||
import { useAsync, useAction } from '../../hooks/useAsync.js';
|
||||
import { getCompanyInfo, updateCompanyInfo } from '../../services/company.js';
|
||||
import { Field } from '../../components/Field.jsx';
|
||||
import { Button } from '../../components/Button.jsx';
|
||||
|
||||
export default function CompanySettingsPanel() {
|
||||
const { t, pushToast } = useUi();
|
||||
const branding = useBranding();
|
||||
const runAction = useAction();
|
||||
const { data: company, reload } = useAsync(getCompanyInfo, []);
|
||||
const [name, setName] = useState('');
|
||||
const [logoFile, setLogoFile] = useState(null);
|
||||
const [preview, setPreview] = useState(null);
|
||||
const fileInputRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (company) setName(company.name);
|
||||
}, [company]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!logoFile) { setPreview(null); return; }
|
||||
const url = URL.createObjectURL(logoFile);
|
||||
setPreview(url);
|
||||
return () => URL.revokeObjectURL(url);
|
||||
}, [logoFile]);
|
||||
|
||||
async function save() {
|
||||
await runAction(async () => {
|
||||
await updateCompanyInfo({ name, logoFile });
|
||||
setLogoFile(null);
|
||||
if (fileInputRef.current) fileInputRef.current.value = '';
|
||||
await reload();
|
||||
await branding.reload();
|
||||
pushToast({ tone: 'success', message: t('common_saved') });
|
||||
});
|
||||
}
|
||||
|
||||
if (!company) return <div className="text-muted">{t('common_loading')}</div>;
|
||||
|
||||
const currentLogoSrc = preview || branding.logoUrl;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Field label={t('company_name')}>
|
||||
<input className="input" value={name} onChange={(e) => setName(e.target.value)} placeholder="LinkedCorp" />
|
||||
</Field>
|
||||
<Field label={t('company_logo')} hint={t('company_logo_hint')}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
|
||||
<div className="company-logo-preview">
|
||||
{currentLogoSrc ? <img src={currentLogoSrc} alt={name} /> : <span className="text-muted" style={{ fontSize: '.7rem' }}>{t('company_logo_none')}</span>}
|
||||
</div>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/png,image/jpeg,image/webp,image/svg+xml"
|
||||
style={{ display: 'none' }}
|
||||
onChange={(e) => setLogoFile(e.target.files?.[0] || null)}
|
||||
/>
|
||||
<Button variant="standard" icon={Upload} onClick={() => fileInputRef.current?.click()}>
|
||||
{t('company_logo_choose')}
|
||||
</Button>
|
||||
</div>
|
||||
</Field>
|
||||
<Button variant="accent" icon={Save} onClick={save}>{t('common_save')}</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { Palette, ListChecks, Flag, FolderKanban, Layers, Mail } from 'lucide-react';
|
||||
import { Palette, ListChecks, Flag, FolderKanban, Layers, Mail, Building2 } from 'lucide-react';
|
||||
import { useUi } from '../../context/ui-context.jsx';
|
||||
import { useAuth } from '../../context/auth-context.jsx';
|
||||
import { Expander } from '../../components/Expander.jsx';
|
||||
@@ -7,6 +7,7 @@ import { Field, FieldRow } from '../../components/Field.jsx';
|
||||
import { ComboBox } from '../../components/ComboBox.jsx';
|
||||
import { CatalogEditor } from './CatalogEditor.jsx';
|
||||
import SmtpSettingsPanel from './SmtpSettingsPanel.jsx';
|
||||
import CompanySettingsPanel from './CompanySettingsPanel.jsx';
|
||||
|
||||
export default function ConfiguracionPage() {
|
||||
const { t, theme, setTheme, lang, setLang, buttonMode, setButtonMode } = useUi();
|
||||
@@ -20,7 +21,11 @@ export default function ConfiguracionPage() {
|
||||
<div>
|
||||
<h2 className="page-title mt-0" style={{ marginBottom: 16 }}>{t('config_title')}</h2>
|
||||
|
||||
<Expander icon={Palette} title={t('config_appearance')} defaultOpen>
|
||||
<Expander icon={Building2} title={t('config_company')} defaultOpen>
|
||||
<CompanySettingsPanel />
|
||||
</Expander>
|
||||
|
||||
<Expander icon={Palette} title={t('config_appearance')}>
|
||||
<FieldRow>
|
||||
<Field label="Tema / Theme">
|
||||
<ComboBox
|
||||
|
||||
Reference in New Issue
Block a user