{compact ? (
-

+

) : (
<>
-

+
v{pkg.version}
>
)}
diff --git a/frontend/src/context/branding-context.jsx b/frontend/src/context/branding-context.jsx
new file mode 100644
index 0000000..a443a4b
--- /dev/null
+++ b/frontend/src/context/branding-context.jsx
@@ -0,0 +1,35 @@
+import { createContext, useCallback, useContext, useEffect, useState } from 'react';
+import { getCompanyInfo, companyLogoUrl } from '../services/company.js';
+
+const DEFAULT_BRANDING = { name: 'LinkedCorp', hasLogo: false, logoVersion: null };
+
+const BrandingContext = createContext({ ...DEFAULT_BRANDING, logoUrl: null, reload: () => {} });
+
+export function BrandingProvider({ children }) {
+ const [company, setCompany] = useState(DEFAULT_BRANDING);
+
+ const reload = useCallback(async () => {
+ try {
+ setCompany(await getCompanyInfo());
+ } catch {
+ // Publico y de solo lectura: si falla (backend caido, etc.) se sigue con el nombre/logo
+ // por defecto en vez de romper login/registro.
+ }
+ }, []);
+
+ useEffect(() => { reload(); }, [reload]);
+
+ // El nombre de archivo (unico por subida) sirve de cache-buster para que el logo se refresque
+ // de inmediato tras cambiarlo, sin depender de que el navegador invalide su cache.
+ const logoUrl = company.hasLogo ? `${companyLogoUrl()}?v=${encodeURIComponent(company.logoVersion)}` : null;
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useBranding() {
+ return useContext(BrandingContext);
+}
diff --git a/frontend/src/i18n/messages.js b/frontend/src/i18n/messages.js
index cd7ca25..2477d49 100644
--- a/frontend/src/i18n/messages.js
+++ b/frontend/src/i18n/messages.js
@@ -48,6 +48,13 @@ export const messages = {
user_approve_title: 'Aprobar usuario',
user_reject_confirm_msg: '¿Seguro que deseas rechazar esta solicitud de registro? Se eliminara permanentemente.',
+ config_company: 'Empresa',
+ company_name: 'Nombre de la empresa',
+ company_logo: 'Logo',
+ company_logo_hint: 'PNG, JPG, WEBP o SVG. Se usa en el menu lateral y en las pantallas de acceso.',
+ company_logo_none: 'Sin logo',
+ company_logo_choose: 'Elegir imagen',
+
config_smtp: 'Correo (SMTP)',
config_smtp_superadmin_only: 'Solo el superadministrador puede configurar el correo del sistema.',
smtp_host: 'Servidor (host)',
@@ -282,6 +289,13 @@ export const messages = {
user_approve_title: 'Approve user',
user_reject_confirm_msg: 'Are you sure you want to reject this registration request? It will be permanently deleted.',
+ config_company: 'Company',
+ company_name: 'Company name',
+ company_logo: 'Logo',
+ company_logo_hint: 'PNG, JPG, WEBP or SVG. Used in the sidebar and on the sign-in screens.',
+ company_logo_none: 'No logo',
+ company_logo_choose: 'Choose image',
+
config_smtp: 'Email (SMTP)',
config_smtp_superadmin_only: 'Only the superadmin can configure the system email.',
smtp_host: 'Host',
diff --git a/frontend/src/pages/Configuracion/CompanySettingsPanel.jsx b/frontend/src/pages/Configuracion/CompanySettingsPanel.jsx
new file mode 100644
index 0000000..16dc1b8
--- /dev/null
+++ b/frontend/src/pages/Configuracion/CompanySettingsPanel.jsx
@@ -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
{t('common_loading')}
;
+
+ const currentLogoSrc = preview || branding.logoUrl;
+
+ return (
+
+
+ setName(e.target.value)} placeholder="LinkedCorp" />
+
+
+
+
+ {currentLogoSrc ?

:
{t('company_logo_none')}}
+
+
setLogoFile(e.target.files?.[0] || null)}
+ />
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/pages/Configuracion/ConfiguracionPage.jsx b/frontend/src/pages/Configuracion/ConfiguracionPage.jsx
index 2071048..dbed569 100644
--- a/frontend/src/pages/Configuracion/ConfiguracionPage.jsx
+++ b/frontend/src/pages/Configuracion/ConfiguracionPage.jsx
@@ -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() {
{t('config_title')}
-
+
+
+
+
+