import React, { useState, useEffect } from 'react';
import {
LayoutDashboard,
Users,
Briefcase,
FileText,
Settings,
Search,
Plus,
CheckCircle2,
Clock,
AlertCircle,
Menu,
X,
Palette,
Printer,
Trash2,
Calculator,
ArrowLeft,
Edit,
Save,
AlertTriangle,
Archive,
Info
} from 'lucide-react';
const initialClients = [
{ id: 1, name: 'Empresa Alpha', contact: 'Juan Pérez', email: 'juan@alpha.com', phone: '11-2345-6789' },
{ id: 2, name: 'Librería El Estudiante', contact: 'María Gómez', email: 'contacto@elestudiante.com', phone: '11-9876-5432' },
{ id: 3, name: 'Restaurante Sabores', contact: 'Carlos Ruiz', email: 'info@sabores.com', phone: '11-4567-8901' },
];
const initialProjects = [
{ id: 101, title: 'Rediseño de Logo', clientId: 1, type: 'Diseño', status: 'En Proceso', deadline: '2026-03-20' },
{ id: 102, title: 'Impresión Lonas 3x2m', clientId: 2, type: 'Impresión', status: 'Pendiente', deadline: '2026-03-15' },
{ id: 103, title: 'Menús QR y Físicos', clientId: 3, type: 'Mixto', status: 'Completado', deadline: '2026-03-10' },
{ id: 104, title: 'Tarjetas Personales (x1000)', clientId: 1, type: 'Impresión', status: 'En Proceso', deadline: '2026-03-18' },
];
const initialQuotes = [
{ id: 1001, clientId: 1, description: 'Campaña Redes Sociales', amount: 150000, status: 'Aprobado', date: '2026-03-01', items: [], variables: {} },
{ id: 1002, clientId: 2, description: 'Cartelería Vidriera', amount: 85000, status: 'Pendiente', date: '2026-03-11', items: [], variables: {} },
{ id: 1003, clientId: 3, description: 'Diseño Packaging', amount: 220000, status: 'Rechazado', date: '2026-02-28', items: [], variables: {} },
];
const defaultCatalog = [
{ id: 'p1', category: 'Perfiles', name: 'Trimcap 20mm', unit: 'ml', price: 4166.67 },
{ id: 'p2', category: 'Perfiles', name: 'Perfil Agujereado', unit: 'ml', price: 7916.67 },
{ id: 'p3', category: 'Perfiles', name: 'Perfil liso 70mm Aluminio', unit: 'ml', price: 6250.00 },
{ id: 'pl1', category: 'Placas', name: 'Acrílico', unit: 'm2', price: 200000.00 },
{ id: 'pl2', category: 'Placas', name: 'Policarbonato Clear', unit: 'm2', price: 60000.00 },
{ id: 'pl3', category: 'Placas', name: 'PVC 10mm', unit: 'm2', price: 55000.00 },
{ id: 'pl4', category: 'Placas', name: 'PVC 3mm', unit: 'm2', price: 40000.00 },
{ id: 'pl5', category: 'Placas', name: 'Vinilo Impreso Clear', unit: 'm2', price: 50000.00 },
{ id: 'n1', category: 'Iluminación', name: 'Neón flex', unit: 'ml', price: 4000.00 },
{ id: 'n2', category: 'Iluminación', name: 'Rollo LED (fracción/m)', unit: 'ml', price: 4700.00 },
{ id: 'n3', category: 'Iluminación', name: 'Módulos LED', unit: 'un', price: 1000.00 },
{ id: 'f1', category: 'Fuentes', name: 'Fuente 12V 5A', unit: 'un', price: 20000.00 },
{ id: 'f2', category: 'Fuentes', name: 'Fuente 12V 10A', unit: 'un', price: 30000.00 },
{ id: 'o1', category: 'Otros', name: 'Pegatutti c/ activador 100g', unit: 'un', price: 18000.00 },
];
const safeGetStorage = (key, initialValue) => {
try {
if (typeof window !== 'undefined') {
const saved = window.localStorage.getItem(key);
return saved ? JSON.parse(saved) : initialValue;
}
} catch (e) {
console.warn("LocalStorage bloqueado, usando memoria temporal.");
}
return initialValue;
};
const safeSetStorage = (key, value) => {
try {
if (typeof window !== 'undefined') {
window.localStorage.setItem(key, JSON.stringify(value));
}
} catch (e) {}
};
// Función Helper Global para generar el número de presupuesto (DDMM-ID-INICIALES)
const generateQuoteNumber = (dateString, id, clientName) => {
const initials = clientName && clientName !== 'Desconocido' ? clientName.split(' ').map(n => n && n[0]).join('').toUpperCase().substring(0, 2) : '';
if (!dateString) return `0000-${id}${initials ? '-' + initials : ''}`;
const [y, m, d] = dateString.split('-');
return `${d}${m}-${id}${initials ? '-' + initials : ''}`;
};
// =====================================================================
// SUBCOMPONENTE: COTIZADOR DE CORPÓREOS
// =====================================================================
function CotizadorCorporeos({ onBack, quoteId, clients, onSave, requestConfirm, existingQuote, autoPrint }) {
const [internalTab, setInternalTab] = useState('cotizador');
const [printMode, setPrintMode] = useState(false);
const [catalog, setCatalog] = useState(defaultCatalog);
// Datos Generales del Presupuesto
const [clientId, setClientId] = useState(existingQuote?.clientId?.toString() || '');
const [date, setDate] = useState(existingQuote?.date || new Date().toISOString().split('T')[0]);
const [publicDescription, setPublicDescription] = useState(existingQuote?.description || 'Fabricación de Cartel Corpóreo...');
const currentId = existingQuote ? existingQuote.id : quoteId;
const selectedClient = clients.find(c => c.id === Number(clientId));
const clientName = selectedClient ? selectedClient.name : '';
const quoteNumber = generateQuoteNumber(date, currentId, clientName);
// Ítems de Materiales
const [items, setItems] = useState(existingQuote?.items?.length > 0 ? existingQuote.items : [
{ id: 1, description: 'Acrílico', quantity: 3.2, unitPrice: 200000.00 },
{ id: 2, description: 'Trimcap 20mm', quantity: 20, unitPrice: 4166.67 },
]);
// Variables y Costos Extra
const [variables, setVariables] = useState(existingQuote?.variables && Object.keys(existingQuote.variables).length > 0 ? existingQuote.variables : {
horasManoObra: 50,
precioHora: 10000,
instalacion: 150000,
viaticos: 50000,
tornillos: 50000,
electricidad: 30000,
acrilicoLaser: 200000,
margenGanancia: 40
});
const [totals, setTotals] = useState({ materiales: 0, fijos: 0, manoObra: 0, costoTotal: 0, ganancia: 0, precioVenta: 0 });
useEffect(() => {
const costoMateriales = items.reduce((acc, item) => acc + (item.quantity * item.unitPrice), 0);
const costoManoObra = variables.horasManoObra * variables.precioHora;
const costoFijos =
Number(variables.instalacion) +
Number(variables.viaticos) +
Number(variables.tornillos) +
Number(variables.electricidad) +
Number(variables.acrilicoLaser);
const costoTotal = costoMateriales + costoManoObra + costoFijos;
const ganancia = costoTotal * (variables.margenGanancia / 100);
const precioVenta = costoTotal + ganancia;
setTotals({
materiales: costoMateriales,
manoObra: costoManoObra,
fijos: costoFijos,
costoTotal: costoTotal,
ganancia: ganancia,
precioVenta: precioVenta
});
}, [items, variables]);
useEffect(() => {
if (autoPrint && clientId) {
handlePrint();
}
}, [autoPrint, clientId]);
const handleItemChange = (id, field, value) => {
setItems(items.map(item => item.id === id ? { ...item, [field]: value } : item));
};
const handleCatalogSelect = (itemId, catalogItemId) => {
const selectedProduct = catalog.find(c => c.id === catalogItemId);
if (selectedProduct) {
setItems(items.map(item =>
item.id === itemId
? { ...item, description: selectedProduct.name, unitPrice: selectedProduct.price }
: item
));
}
};
const addItem = () => setItems([...items, { id: Date.now(), description: '', quantity: 1, unitPrice: 0 }]);
const removeItem = (id) => {
if (items.length <= 1) return;
requestConfirm('Eliminar Material', '¿Estás seguro de quitar este material del presupuesto?', () => {
setItems(prevItems => prevItems.filter(item => item.id !== id));
});
};
const handleVariableChange = (field, value) => {
setVariables({ ...variables, [field]: Number(value) || 0 });
};
const handlePrint = () => {
if (!clientId) {
requestConfirm('Atención', 'Selecciona un cliente antes de imprimir el presupuesto.', null);
return;
}
setPrintMode(true);
setTimeout(() => {
try { window.print(); } catch (e) { console.log(e); }
}, 500);
};
const formatCurrency = (val) => '$ ' + Number(val).toLocaleString('es-AR', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
if (printMode) {
return (
Vista Previa del Documento
Si no se abrió la ventana, presiona Ctrl + P (Windows) o Cmd + P (Mac) en tu teclado.
IG
IDEAGRÁFICA
www.ideagrafica.com.ar
PRESUPUESTO
Nº {quoteNumber}
Fecha: {date ? date.split('-').reverse().join('/') : ''}
Preparado para:
{clientName || 'Cliente No Seleccionado'}
Inversión Total
{formatCurrency(totals.precioVenta)}
Los precios no incluyen IVA. Presupuesto válido por 15 días.
Gracias por confiar en Ideagráfica.
Si tiene alguna duda sobre este presupuesto, no dude en contactarnos.
);
}
return (
IG
IDEAGRÁFICA
www.ideagrafica.com.ar
PRESUPUESTO
Nº {quoteNumber}
{internalTab === 'cotizador' && (
setDate(e.target.value)}
className="w-full p-3 bg-slate-50 border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500 font-medium print:bg-transparent print:border-none print:p-0 print:text-lg"
/>
📋 Costos Internos de Materiales (No visible en PDF)
Mano de Obra
Total Mano de Obra:
{formatCurrency(totals.manoObra)}
Costo de Materiales:
{formatCurrency(totals.materiales)}
Costo de Mano de Obra:
{formatCurrency(totals.manoObra)}
Costos Extras:
{formatCurrency(totals.fijos)}
Costo Total Interno:
{formatCurrency(totals.costoTotal)}
Inversión Total
{formatCurrency(totals.precioVenta)}
Los precios no incluyen IVA. Presupuesto válido por 15 días.
💡 ¿Cómo guardar en PDF o en el Sistema?
Usa "Guardar Presupuesto" para registrarlo. Usa "Imprimir / PDF" para descargarlo.
)}
{internalTab === 'precios' && (
Lista de Precios Base
Actualiza los costos unitarios de tus materiales.
)}
Gracias por confiar en Ideagráfica.
);
}
// =====================================================================
// COMPONENTE PRINCIPAL: GESTIÓN IDEA GRÁFICA
// =====================================================================
export default function GestionIdeaGrafica() {
const [activeTab, setActiveTab] = useState('dashboard');
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [showCotizador, setShowCotizador] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [confirmModal, setConfirmModal] = useState({ isOpen: false, title: '', message: '', onConfirm: null });
const [showNewClientModal, setShowNewClientModal] = useState(false);
const [editingClient, setEditingClient] = useState(null);
const [newClientData, setNewClientData] = useState({ name: '', contact: '', email: '', phone: '' });
const [showNewProjectModal, setShowNewProjectModal] = useState(false);
const [editingProject, setEditingProject] = useState(null);
const [newProjectData, setNewProjectData] = useState({ title: '', clientId: '', type: 'Diseño', status: 'Pendiente', deadline: '' });
const [editingQuote, setEditingQuote] = useState(null);
const [autoPrintQuote, setAutoPrintQuote] = useState(false);
// AUTO-GUARDADO (LocalStorage seguro)
const [clients, setClients] = useState(() => safeGetStorage('ig_clients', initialClients));
const [projects, setProjects] = useState(() => safeGetStorage('ig_projects', initialProjects));
const [quotes, setQuotes] = useState(() => safeGetStorage('ig_quotes', initialQuotes));
useEffect(() => { safeSetStorage('ig_clients', clients); }, [clients]);
useEffect(() => { safeSetStorage('ig_projects', projects); }, [projects]);
useEffect(() => { safeSetStorage('ig_quotes', quotes); }, [quotes]);
const requestConfirm = (title, message, onConfirmCallback) => {
setConfirmModal({ isOpen: true, title, message, onConfirm: onConfirmCallback });
};
const handleSaveClient = (e) => {
e.preventDefault();
if (editingClient) {
setClients(clients.map(c => c.id === editingClient.id ? { ...c, ...newClientData } : c));
} else {
const newId = clients.length > 0 ? Math.max(...clients.map(c => c.id)) + 1 : 1;
setClients([...clients, { id: newId, ...newClientData }]);
}
setShowNewClientModal(false);
setEditingClient(null);
setNewClientData({ name: '', contact: '', email: '', phone: '' });
};
const handleSaveProject = (e) => {
e.preventDefault();
if (editingProject) {
setProjects(projects.map(p => p.id === editingProject.id ? { ...p, ...newProjectData, clientId: Number(newProjectData.clientId) } : p));
} else {
const newId = projects.length > 0 ? Math.max(...projects.map(p => p.id)) + 1 : 101;
setProjects([...projects, { id: newId, ...newProjectData, clientId: Number(newProjectData.clientId) }]);
}
setShowNewProjectModal(false);
setEditingProject(null);
setNewProjectData({ title: '', clientId: '', type: 'Diseño', status: 'Pendiente', deadline: '' });
};
const handleSaveQuote = (quoteData) => {
if (editingQuote) {
setQuotes(quotes.map(q => q.id === editingQuote.id ? { ...q, ...quoteData } : q));
} else {
const newId = quotes.length > 0 ? Math.max(...quotes.map(q => q.id)) + 1 : 1001;
setQuotes([{ id: newId, ...quoteData }, ...quotes]);
}
setShowCotizador(false);
setEditingQuote(null);
setActiveTab('presupuestos');
};
const handleUpdateQuoteStatus = (id, newStatus) => {
const currentQuote = quotes.find(q => q.id === id);
// Si el estado cambia a 'Aprobado', se genera un trabajo en Pendiente automáticamente
if (currentQuote && newStatus === 'Aprobado' && currentQuote.status !== 'Aprobado') {
setProjects(prevProjects => {
const newProjectId = prevProjects.length > 0 ? Math.max(...prevProjects.map(p => p.id)) + 1 : 101;
const newProject = {
id: newProjectId,
title: currentQuote.description || 'Trabajo desde Presupuesto',
clientId: currentQuote.clientId,
type: 'Corpóreo',
status: 'Pendiente',
deadline: ''
};
return [newProject, ...prevProjects];
});
requestConfirm(
'¡Presupuesto Aprobado!',
'Se ha generado automáticamente una nueva Orden de Trabajo en estado "Pendiente".',
null
);
}
setQuotes(quotes.map(q => q.id === id ? { ...q, status: newStatus } : q));
};
const handleDeleteQuote = (id) => {
requestConfirm('Eliminar Presupuesto', '¿Estás seguro de eliminar este presupuesto? Esta acción no se puede deshacer.', () => {
setQuotes(prev => prev.filter(q => q.id !== id));
});
};
const openCotizador = (quote = null, print = false) => {
setEditingQuote(quote);
setAutoPrintQuote(print);
setShowCotizador(true);
};
const closeCotizador = () => {
setShowCotizador(false);
setEditingQuote(null);
setAutoPrintQuote(false);
};
const getClientName = (id) => {
const client = clients.find(c => c.id === id);
return client ? client.name : 'Desconocido';
};
const getStatusColor = (status) => {
switch (status) {
case 'Completado': return 'bg-green-100 text-green-800 border-green-200';
case 'En Proceso': return 'bg-blue-100 text-blue-800 border-blue-200';
case 'Pendiente': return 'bg-orange-100 text-orange-800 border-orange-200';
default: return 'bg-gray-100 text-gray-800 border-gray-200';
}
};
const getQuoteStatusColor = (status) => {
switch (status) {
case 'Aprobado': return 'text-green-600 bg-green-50';
case 'Rechazado': return 'text-red-600 bg-red-50';
case 'Pendiente': return 'text-yellow-600 bg-yellow-50';
default: return 'text-gray-600 bg-gray-50';
}
};
const formatCurrency = (amount) => {
return new Intl.NumberFormat('es-AR', { style: 'currency', currency: 'ARS' }).format(amount);
};
const navigateTo = (tab) => {
setActiveTab(tab);
setShowCotizador(false);
};
const renderDashboard = () => {
const pendingProjects = projects.filter(p => p.status !== 'Completado').length;
const pendingQuotes = quotes.filter(q => q.status === 'Pendiente').length;
const totalClients = clients.length;
const recentRevenue = quotes.filter(q => q.status === 'Aprobado').reduce((acc, curr) => acc + curr.amount, 0);
return (
Panel General
navigateTo('proyectos')}
className="bg-white p-6 rounded-xl border border-slate-100 shadow-sm flex items-center space-x-4 cursor-pointer hover:-translate-y-1 hover:shadow-md hover:border-indigo-200 transition-all"
>
Trabajos Activos
{pendingProjects}
navigateTo('presupuestos')}
className="bg-white p-6 rounded-xl border border-slate-100 shadow-sm flex items-center space-x-4 cursor-pointer hover:-translate-y-1 hover:shadow-md hover:border-orange-200 transition-all"
>
Presupuestos Pdtes.
{pendingQuotes}
navigateTo('clientes')}
className="bg-white p-6 rounded-xl border border-slate-100 shadow-sm flex items-center space-x-4 cursor-pointer hover:-translate-y-1 hover:shadow-md hover:border-emerald-200 transition-all"
>
Clientes Totales
{totalClients}
navigateTo('presupuestos')}
className="bg-white p-6 rounded-xl border border-slate-100 shadow-sm flex items-center space-x-4 cursor-pointer hover:-translate-y-1 hover:shadow-md hover:border-blue-200 transition-all"
>
Ingresos Aprobados
{formatCurrency(recentRevenue)}
Trabajos en Curso
{projects.filter(p => p.status !== 'Completado').map(project => (
{project.type === 'Diseño' ?
:
}
{project.title}
{getClientName(project.clientId)}
Entrega: {project.deadline ? project.deadline.split('-').reverse().join('/') : '-'}
{project.status}
))}
);
};
const renderClientes = () => {
const filteredClients = clients.filter(c =>
c.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
c.contact.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
Directorio de Clientes
{showNewClientModal && (
{editingClient ? 'Editar Cliente' : 'Agregar Nuevo Cliente'}
)}
| Empresa / Nombre |
Contacto |
Email |
Teléfono |
Acciones |
{filteredClients.map(client => (
| {client.name} |
{client.contact} |
{client.email} |
{client.phone} |
|
))}
{filteredClients.length === 0 && (
No se encontraron clientes.
)}
);
};
const renderProyectos = () => {
const filteredProjects = projects.filter(p =>
p.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
getClientName(p.clientId).toLowerCase().includes(searchQuery.toLowerCase()) ||
p.type.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
Gestión de Trabajos
{showNewProjectModal && (
{editingProject ? 'Editar Orden de Trabajo' : 'Agregar Nueva Orden de Trabajo'}
)}
Pendientes
{filteredProjects.filter(p => p.status === 'Pendiente').map(project => (
{ setEditingProject(project); setNewProjectData(project); setShowNewProjectModal(true); }}
className="bg-white p-4 rounded-lg shadow-sm border border-slate-100 cursor-pointer hover:border-indigo-300 hover:shadow-md transition-all relative group"
>
{project.type}
#{project.id}
{project.title}
{getClientName(project.clientId)}
Vence: {project.deadline ? project.deadline.split('-').reverse().join('/') : '-'}
))}
En Proceso
{filteredProjects.filter(p => p.status === 'En Proceso').map(project => (
{ setEditingProject(project); setNewProjectData(project); setShowNewProjectModal(true); }}
className="bg-white p-4 rounded-lg shadow-sm border border-slate-100 cursor-pointer hover:border-indigo-300 hover:shadow-md transition-all relative group"
>
{project.type}
#{project.id}
{project.title}
{getClientName(project.clientId)}
Vence: {project.deadline ? project.deadline.split('-').reverse().join('/') : '-'}
))}
Completados
{filteredProjects.filter(p => p.status === 'Completado').map(project => (
{ setEditingProject(project); setNewProjectData(project); setShowNewProjectModal(true); }}
className="bg-white p-4 rounded-lg shadow-sm border border-slate-100 opacity-75 cursor-pointer hover:border-indigo-300 transition-all relative group"
>
{project.type}
#{project.id}
{project.title}
{getClientName(project.clientId)}
Entregado
))}
);
};
const renderTerminados = () => {
const completedProjects = projects.filter(p => {
if (p.status !== 'Completado') return false;
const search = searchQuery.toLowerCase();
return p.title.toLowerCase().includes(search) ||
getClientName(p.clientId).toLowerCase().includes(search) ||
p.type.toLowerCase().includes(search);
});
return (
Historial de Trabajos Terminados
| Trabajo |
Cliente |
Fecha de Entrega |
Presupuesto Aprobado |
Acciones |
{completedProjects.map(project => {
const clientQuotes = quotes.filter(q => q.clientId === project.clientId && q.status === 'Aprobado');
const totalApproved = clientQuotes.reduce((sum, q) => sum + q.amount, 0);
return (
|
{project.type}
{project.title}
|
{getClientName(project.clientId)} |
{project.deadline ? project.deadline.split('-').reverse().join('/') : '-'} |
{clientQuotes.length > 0 ? (
{formatCurrency(totalApproved)}
) : (
Sin presupuesto registrado
)}
|
|
);
})}
{completedProjects.length === 0 && (
No hay trabajos terminados que coincidan con la búsqueda.
)}
);
};
const renderPresupuestos = () => {
const filteredQuotes = quotes.filter(q =>
q.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
getClientName(q.clientId).toLowerCase().includes(searchQuery.toLowerCase())
);
return (
Presupuestos y Cotizaciones
| Nº |
Fecha |
Cliente |
Descripción |
Monto |
Estado |
Acciones |
{filteredQuotes.map(quote => (
| PRE-{generateQuoteNumber(quote.date, quote.id, getClientName(quote.clientId))} |
{quote.date ? quote.date.split('-').reverse().join('/') : '-'} |
{getClientName(quote.clientId)} |
{quote.description} |
{formatCurrency(quote.amount)} |
|
|
))}
{filteredQuotes.length === 0 && (
No se encontraron presupuestos.
)}
);
};
return (
{isMobileMenuOpen &&
setIsMobileMenuOpen(false)} />}
{activeTab === 'dashboard' && renderDashboard()}
{activeTab === 'clientes' && renderClientes()}
{activeTab === 'proyectos' && renderProyectos()}
{activeTab === 'terminados' && renderTerminados()}
{activeTab === 'presupuestos' && !showCotizador && renderPresupuestos()}
{activeTab === 'presupuestos' && showCotizador && (
0 ? Math.max(...quotes.map(q => q.id)) + 1 : 1001}
clients={clients}
onSave={handleSaveQuote}
requestConfirm={requestConfirm}
existingQuote={editingQuote}
autoPrint={autoPrintQuote}
/>
)}
{/* --- MODAL DE CONFIRMACIÓN / ALERTA SEGURO --- */}
{confirmModal.isOpen && (
{confirmModal.onConfirm ?
:
}
{confirmModal.title}
{confirmModal.message}
{confirmModal.onConfirm && (
)}
)}
);
}