feat: complete fases 3,4,5 - frontend, containers, e2e tests
FASE 3 - Frontend (22 paginas): - Leads (listado, detalle, nuevo), Cotizaciones, Arqueo - Cursos, Alumnos, Empresas (listado, detalle, ventas) - Reportes (ventas, ventas-empresas, reimpresion) - Autorizador, Contacto FASE 4 - Contenedores: - docker-compose.yml (backend-api + services-externos + frontend) - Dockerfiles para backend, services-externos, frontend - .env.example con secretos FASE 5 - Pruebas E2E: - Playwright config + 3 spec files (login, leads, dashboard) - 7 escenarios: login, error, sidebar, crear lead, dashboard cards, timeout, reportes
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { api } from '@/services/api';
|
||||
import LoadingSpinner from '@/components/LoadingSpinner';
|
||||
|
||||
export default function AlumnosPage() {
|
||||
const { user, loading } = useAuth();
|
||||
const router = useRouter();
|
||||
const [alumnos, setAlumnos] = useState<any[]>([]);
|
||||
const [busqueda, setBusqueda] = useState('');
|
||||
const [loadingData, setLoadingData] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !user) router.push('/login');
|
||||
if (user) {
|
||||
api.get('/alumno', { tipoBusqueda: 'NOMBRE', nombre: '' }).then(data => {
|
||||
setAlumnos(Array.isArray(data) ? data : []);
|
||||
setLoadingData(false);
|
||||
}).catch(() => setLoadingData(false));
|
||||
}
|
||||
}, [user, loading, router]);
|
||||
|
||||
const handleSearch = async () => {
|
||||
setLoadingData(true);
|
||||
try {
|
||||
const data = await api.get('/alumno', { tipoBusqueda: 'NOMBRE', nombre: busqueda });
|
||||
setAlumnos(Array.isArray(data) ? data : []);
|
||||
} catch { /* ignore */ }
|
||||
setLoadingData(false);
|
||||
};
|
||||
|
||||
if (loading) return <LoadingSpinner />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="mb-3">Alumnos</h3>
|
||||
<div className="input-group mb-3">
|
||||
<input className="form-control" value={busqueda} onChange={e => setBusqueda(e.target.value)}
|
||||
placeholder="Buscar alumno..." onKeyDown={e => e.key === 'Enter' && handleSearch()} />
|
||||
<button className="btn btn-primary" onClick={handleSearch}>Buscar</button>
|
||||
</div>
|
||||
<div className="card shadow">
|
||||
<div className="card-body">
|
||||
<table className="table table-hover">
|
||||
<thead>
|
||||
<tr><th>RUT</th><th>Nombre</th><th>Mail</th><th>Teléfono</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{alumnos.map((a: any, i: number) => (
|
||||
<tr key={i}>
|
||||
<td>{a.AlumnoRut || a.rut || a.Rut}</td>
|
||||
<td>{a.NombreAlumno || a.nombre || a.Nombre}</td>
|
||||
<td>{a.Email || a.mail || a.Mail}</td>
|
||||
<td>{a.Telefono || a.telefono || a.Fono}</td>
|
||||
</tr>
|
||||
))}
|
||||
{alumnos.length === 0 && <tr><td colSpan={4} className="text-center">Sin resultados</td></tr>}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { api } from '@/services/api';
|
||||
import LoadingSpinner from '@/components/LoadingSpinner';
|
||||
|
||||
export default function ArqueoPage() {
|
||||
const { user, loading } = useAuth();
|
||||
const router = useRouter();
|
||||
const [ingresos, setIngresos] = useState<any[]>([]);
|
||||
const [loadingData, setLoadingData] = useState(true);
|
||||
const fecha = new Date().toISOString().split('T')[0];
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !user) router.push('/login');
|
||||
if (user) {
|
||||
api.get('/arqueo/todos', { fecha }).then(data => {
|
||||
setIngresos(Array.isArray(data) ? data : []);
|
||||
setLoadingData(false);
|
||||
}).catch(() => setLoadingData(false));
|
||||
}
|
||||
}, [user, loading, fecha, router]);
|
||||
|
||||
if (loading || loadingData) return <LoadingSpinner />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="mb-3">Arqueo de Caja</h3>
|
||||
<p className="text-muted">Fecha: {new Date().toLocaleDateString()}</p>
|
||||
<div className="card shadow">
|
||||
<div className="card-body">
|
||||
<table className="table table-hover">
|
||||
<thead>
|
||||
<tr><th>Cajero</th><th>Crédito</th><th>Débito</th><th>Total</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ingresos.map((r: any, i: number) => (
|
||||
<tr key={i}>
|
||||
<td>{r.Cajero || r.cajero}</td>
|
||||
<td>${(r.Credito || r.credito || 0).toLocaleString()}</td>
|
||||
<td>${(r.Debito || r.debito || 0).toLocaleString()}</td>
|
||||
<td>${(r.Total || r.total || 0).toLocaleString()}</td>
|
||||
</tr>
|
||||
))}
|
||||
{ingresos.length === 0 && <tr><td colSpan={4} className="text-center">Sin movimientos hoy</td></tr>}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { api } from '@/services/api';
|
||||
|
||||
export default function AutorizadorPage() {
|
||||
const [cotizacionId, setCotizacionId] = useState('');
|
||||
const [mensaje, setMensaje] = useState('');
|
||||
|
||||
const handleAutorizar = async () => {
|
||||
try {
|
||||
await api.put(`/cotizacion/${cotizacionId}/desactivar`);
|
||||
setMensaje('Cotización autorizada');
|
||||
} catch {
|
||||
setMensaje('Error al autorizar');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="mb-3">Autorizador de Descuentos</h3>
|
||||
<div className="card shadow p-4">
|
||||
<div className="mb-3">
|
||||
<label className="form-label">ID Cotización</label>
|
||||
<input className="form-control" value={cotizacionId} onChange={e => setCotizacionId(e.target.value)} />
|
||||
</div>
|
||||
<button className="btn btn-warning" onClick={handleAutorizar}>Autorizar Descuento</button>
|
||||
{mensaje && <div className="alert alert-info mt-3">{mensaje}</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
'use client';
|
||||
|
||||
import { useState, FormEvent } from 'react';
|
||||
import { api } from '@/services/api';
|
||||
|
||||
export default function ContactoPage() {
|
||||
const [mail, setMail] = useState('');
|
||||
const [mensaje, setMensaje] = useState('');
|
||||
|
||||
const handleSubmit = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await api.post('/email/send', { to: mail, subject: 'Contacto desde SAM', body: mensaje });
|
||||
alert('Correo enviado');
|
||||
setMail('');
|
||||
setMensaje('');
|
||||
} catch {
|
||||
alert('Error al enviar');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="mb-3">Contacto</h3>
|
||||
<div className="card shadow p-4">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Correo</label>
|
||||
<input type="email" className="form-control" value={mail} onChange={e => setMail(e.target.value)} required />
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Mensaje</label>
|
||||
<textarea className="form-control" rows={4} value={mensaje} onChange={e => setMensaje(e.target.value)} required />
|
||||
</div>
|
||||
<button type="submit" className="btn btn-primary">Enviar</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { api } from '@/services/api';
|
||||
import LoadingSpinner from '@/components/LoadingSpinner';
|
||||
|
||||
export default function CotizacionesPage() {
|
||||
const { user, loading } = useAuth();
|
||||
const router = useRouter();
|
||||
const [cotizaciones, setCotizaciones] = useState<any[]>([]);
|
||||
const [loadingData, setLoadingData] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !user) router.push('/login');
|
||||
if (user) {
|
||||
api.get('/cotizacion/buscar', { tipoBusqueda: 'NOMBRE', nombre: '' }).then(data => {
|
||||
setCotizaciones(Array.isArray(data) ? data : []);
|
||||
setLoadingData(false);
|
||||
}).catch(() => setLoadingData(false));
|
||||
}
|
||||
}, [user, loading, router]);
|
||||
|
||||
if (loading || loadingData) return <LoadingSpinner />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="mb-3">Cotizaciones</h3>
|
||||
<div className="card shadow">
|
||||
<div className="card-body">
|
||||
<table className="table table-hover">
|
||||
<thead>
|
||||
<tr><th>#</th><th>Cliente</th><th>Monto</th><th>Fecha</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{cotizaciones.map((c: any, i: number) => (
|
||||
<tr key={i}>
|
||||
<td>{c.Id || c.idCotizacionEmpresa || i}</td>
|
||||
<td>{c.NombreLead || c.nombre || c.Cliente}</td>
|
||||
<td>${(c.Monto || c.monto || 0).toLocaleString()}</td>
|
||||
<td>{c.Fecha ? new Date(c.Fecha).toLocaleDateString() : ''}</td>
|
||||
</tr>
|
||||
))}
|
||||
{cotizaciones.length === 0 && <tr><td colSpan={4} className="text-center">Sin cotizaciones</td></tr>}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { api } from '@/services/api';
|
||||
import LoadingSpinner from '@/components/LoadingSpinner';
|
||||
|
||||
export default function CursosPage() {
|
||||
const { user, loading } = useAuth();
|
||||
const router = useRouter();
|
||||
const [cursos, setCursos] = useState<any[]>([]);
|
||||
const [loadingData, setLoadingData] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !user) router.push('/login');
|
||||
if (user) {
|
||||
api.get('/curso', { tipoBusqueda: 'NOMBRE', nombre: '' }).then(data => {
|
||||
setCursos(Array.isArray(data) ? data : []);
|
||||
setLoadingData(false);
|
||||
}).catch(() => setLoadingData(false));
|
||||
}
|
||||
}, [user, loading, router]);
|
||||
|
||||
if (loading || loadingData) return <LoadingSpinner />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="mb-3">Cursos</h3>
|
||||
<div className="card shadow">
|
||||
<div className="card-body">
|
||||
<table className="table table-hover">
|
||||
<thead>
|
||||
<tr><th>ID</th><th>Nombre</th><th>Programa</th><th>Duración</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{cursos.map((c: any, i: number) => (
|
||||
<tr key={i}>
|
||||
<td>{c.idCursos || c.Id || i}</td>
|
||||
<td>{c.Curso || c.Nombre || c.nombre}</td>
|
||||
<td>{c.Programa || c.programa}</td>
|
||||
<td>{c.Duracion || c.duracion || '-'}</td>
|
||||
</tr>
|
||||
))}
|
||||
{cursos.length === 0 && <tr><td colSpan={4} className="text-center">Sin cursos</td></tr>}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { api } from '@/services/api';
|
||||
import LoadingSpinner from '@/components/LoadingSpinner';
|
||||
|
||||
export default function EmpresaDetallePage() {
|
||||
const { id } = useParams();
|
||||
const { user, loading } = useAuth();
|
||||
const router = useRouter();
|
||||
const [empresa, setEmpresa] = useState<any>(null);
|
||||
const [loadingData, setLoadingData] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !user) router.push('/login');
|
||||
if (user && id) {
|
||||
api.get('/empresa', { busqueda: 'IDEMP', rut: String(id), varB: '', varC: 0 }).then(data => {
|
||||
setEmpresa(Array.isArray(data) ? data[0] : data);
|
||||
setLoadingData(false);
|
||||
}).catch(() => setLoadingData(false));
|
||||
}
|
||||
}, [user, loading, id, router]);
|
||||
|
||||
if (loading || loadingData) return <LoadingSpinner />;
|
||||
if (!empresa) return <p>Empresa no encontrada</p>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3>Empresa {id}</h3>
|
||||
<div className="card shadow">
|
||||
<div className="card-body">
|
||||
<table className="table">
|
||||
<tbody>
|
||||
<tr><td>RUT</td><td>{empresa.Rut || empresa.rut}</td></tr>
|
||||
<tr><td>Razón Social</td><td>{empresa.RazonSocial || empresa.razonSocial}</td></tr>
|
||||
<tr><td>Dirección</td><td>{empresa.Direccion || empresa.direccion}</td></tr>
|
||||
<tr><td>Contacto</td><td>{empresa.Contacto || empresa.contacto}</td></tr>
|
||||
<tr><td>Mail</td><td>{empresa.Mail || empresa.mail}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { api } from '@/services/api';
|
||||
import LoadingSpinner from '@/components/LoadingSpinner';
|
||||
|
||||
export default function EmpresasPage() {
|
||||
const { user, loading } = useAuth();
|
||||
const router = useRouter();
|
||||
const [empresas, setEmpresas] = useState<any[]>([]);
|
||||
const [loadingData, setLoadingData] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !user) router.push('/login');
|
||||
if (user) {
|
||||
api.get('/empresa', { busqueda: 'LISTADO', rut: '', varB: '', varC: 0 }).then(data => {
|
||||
setEmpresas(Array.isArray(data) ? data : []);
|
||||
setLoadingData(false);
|
||||
}).catch(() => setLoadingData(false));
|
||||
}
|
||||
}, [user, loading, router]);
|
||||
|
||||
if (loading || loadingData) return <LoadingSpinner />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="mb-3">Empresas</h3>
|
||||
<div className="card shadow">
|
||||
<div className="card-body">
|
||||
<table className="table table-hover">
|
||||
<thead>
|
||||
<tr><th>RUT</th><th>Razón Social</th><th>Contacto</th><th>Mail</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{empresas.map((e: any, i: number) => (
|
||||
<tr key={i} style={{ cursor: 'pointer' }} onClick={() => router.push(`/empresas/${e.Rut || e.rut}`)}>
|
||||
<td>{e.Rut || e.rut}</td>
|
||||
<td>{e.RazonSocial || e.razonSocial || e.Nombre}</td>
|
||||
<td>{e.Contacto || e.contacto}</td>
|
||||
<td>{e.Mail || e.mail}</td>
|
||||
</tr>
|
||||
))}
|
||||
{empresas.length === 0 && <tr><td colSpan={4} className="text-center">Sin empresas</td></tr>}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { api } from '@/services/api';
|
||||
import LoadingSpinner from '@/components/LoadingSpinner';
|
||||
|
||||
export default function VentasEmpresaPage() {
|
||||
const { user, loading } = useAuth();
|
||||
const router = useRouter();
|
||||
const [ventas, setVentas] = useState<any[]>([]);
|
||||
const [loadingData, setLoadingData] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !user) router.push('/login');
|
||||
if (user) {
|
||||
api.get('/informe/ventas-empresa', { tipo: 'LISTADO', varA: '', varB: '', varC: 0 }).then(data => {
|
||||
setVentas(Array.isArray(data) ? data : []);
|
||||
setLoadingData(false);
|
||||
}).catch(() => setLoadingData(false));
|
||||
}
|
||||
}, [user, loading, router]);
|
||||
|
||||
if (loading || loadingData) return <LoadingSpinner />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="mb-3">Ventas Empresas</h3>
|
||||
<div className="card shadow">
|
||||
<div className="card-body">
|
||||
<table className="table table-hover">
|
||||
<thead>
|
||||
<tr><th>Empresa</th><th>Curso</th><th>Valor</th><th>Fecha</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ventas.map((v: any, i: number) => (
|
||||
<tr key={i}>
|
||||
<td>{v.Empresa || v.empresa}</td>
|
||||
<td>{v.Curso || v.curso}</td>
|
||||
<td>${(v.Valor || v.valor || 0).toLocaleString()}</td>
|
||||
<td>{v.Fecha ? new Date(v.Fecha).toLocaleDateString() : ''}</td>
|
||||
</tr>
|
||||
))}
|
||||
{ventas.length === 0 && <tr><td colSpan={4} className="text-center">Sin ventas</td></tr>}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { api } from '@/services/api';
|
||||
import LoadingSpinner from '@/components/LoadingSpinner';
|
||||
|
||||
export default function LeadDetallePage() {
|
||||
const { id } = useParams();
|
||||
const { user, loading } = useAuth();
|
||||
const router = useRouter();
|
||||
const [lead, setLead] = useState<any>(null);
|
||||
const [actividades, setActividades] = useState<any[]>([]);
|
||||
const [loadingData, setLoadingData] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !user) router.push('/login');
|
||||
if (user && id) {
|
||||
Promise.all([
|
||||
api.get(`/lead/${id}`),
|
||||
api.get(`/lead/${id}/actividades`, { tipo: '' })
|
||||
]).then(([leadData, actData]) => {
|
||||
setLead(Array.isArray(leadData) ? leadData[0] : leadData);
|
||||
setActividades(Array.isArray(actData) ? actData : []);
|
||||
setLoadingData(false);
|
||||
}).catch(() => setLoadingData(false));
|
||||
}
|
||||
}, [user, loading, id, router]);
|
||||
|
||||
if (loading || loadingData) return <LoadingSpinner />;
|
||||
if (!lead) return <p>Lead no encontrado</p>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3>Lead #{id}</h3>
|
||||
<div className="row">
|
||||
<div className="col-md-6">
|
||||
<div className="card shadow mb-3">
|
||||
<div className="card-body">
|
||||
<h5>Información</h5>
|
||||
<table className="table">
|
||||
<tbody>
|
||||
<tr><td>Nombre</td><td>{lead.Nombre || lead.nombre}</td></tr>
|
||||
<tr><td>Mail</td><td>{lead.Mail || lead.mail}</td></tr>
|
||||
<tr><td>Teléfono</td><td>{lead.Telefono || lead.telefono}</td></tr>
|
||||
<tr><td>Producto</td><td>{lead.Producto || lead.producto}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<div className="card shadow mb-3">
|
||||
<div className="card-body">
|
||||
<h5>Actividades</h5>
|
||||
{actividades.length === 0 ? <p className="text-muted">Sin actividades</p> : (
|
||||
<ul className="list-group">
|
||||
{actividades.map((a: any, i: number) => (
|
||||
<li key={i} className="list-group-item">
|
||||
<small>{a.Tipo || a.tipo}</small>
|
||||
<p className="mb-0">{a.Descripcion || a.descripcion}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
'use client';
|
||||
|
||||
import { useState, FormEvent } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { api } from '@/services/api';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export default function NuevoLeadPage() {
|
||||
const router = useRouter();
|
||||
const [nombre, setNombre] = useState('');
|
||||
const [mail, setMail] = useState('');
|
||||
const [fono, setFono] = useState('');
|
||||
const [producto, setProducto] = useState('');
|
||||
|
||||
const handleSubmit = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await api.post('/lead', { nombre, mail, telefono: fono, producto, contacto: 1, ejecutivoId: 1 });
|
||||
toast.success('Lead creado');
|
||||
router.push('/leads');
|
||||
} catch {
|
||||
toast.error('Error al crear lead');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card shadow p-4">
|
||||
<h3 className="mb-4">Nuevo Lead</h3>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Nombre</label>
|
||||
<input className="form-control" value={nombre} onChange={e => setNombre(e.target.value)} required />
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Mail</label>
|
||||
<input type="email" className="form-control" value={mail} onChange={e => setMail(e.target.value)} />
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Teléfono</label>
|
||||
<input className="form-control" value={fono} onChange={e => setFono(e.target.value)} />
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Producto</label>
|
||||
<input className="form-control" value={producto} onChange={e => setProducto(e.target.value)} />
|
||||
</div>
|
||||
<button type="submit" className="btn btn-primary">Guardar</button>
|
||||
<button type="button" className="btn btn-secondary ms-2" onClick={() => router.back()}>Cancelar</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { api } from '@/services/api';
|
||||
import LoadingSpinner from '@/components/LoadingSpinner';
|
||||
|
||||
export default function LeadsPage() {
|
||||
const { user, loading } = useAuth();
|
||||
const router = useRouter();
|
||||
const [leads, setLeads] = useState<any[]>([]);
|
||||
const [loadingData, setLoadingData] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !user) router.push('/login');
|
||||
if (user) {
|
||||
api.get('/lead/nuevos', { ejecutivo: 1 }).then(data => {
|
||||
setLeads(Array.isArray(data) ? data : []);
|
||||
setLoadingData(false);
|
||||
}).catch(() => setLoadingData(false));
|
||||
}
|
||||
}, [user, loading, router]);
|
||||
|
||||
if (loading || loadingData) return <LoadingSpinner />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="d-flex justify-content-between align-items-center mb-3">
|
||||
<h3>Leads</h3>
|
||||
<button className="btn btn-primary" onClick={() => router.push('/leads/nuevo')}>Nuevo Lead</button>
|
||||
</div>
|
||||
<div className="card shadow">
|
||||
<div className="card-body">
|
||||
<table className="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nombre</th>
|
||||
<th>Mail</th>
|
||||
<th>Teléfono</th>
|
||||
<th>Producto</th>
|
||||
<th>Fecha</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{leads.map((lead: any, i: number) => (
|
||||
<tr key={i} style={{ cursor: 'pointer' }} onClick={() => router.push(`/leads/${lead.Id || i}`)}>
|
||||
<td>{lead.Nombre || lead.nombre}</td>
|
||||
<td>{lead.Mail || lead.mail}</td>
|
||||
<td>{lead.Telefono || lead.telefono}</td>
|
||||
<td>{lead.Producto || lead.producto}</td>
|
||||
<td>{lead.FechaCreacion ? new Date(lead.FechaCreacion).toLocaleDateString() : ''}</td>
|
||||
<td><button className="btn btn-sm btn-outline-primary">Ver</button></td>
|
||||
</tr>
|
||||
))}
|
||||
{leads.length === 0 && <tr><td colSpan={6} className="text-center">Sin leads</td></tr>}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { api } from '@/services/api';
|
||||
|
||||
export default function ReimpresionPage() {
|
||||
const [tipo, setTipo] = useState('contrato');
|
||||
const [id, setId] = useState('');
|
||||
|
||||
const handleReimprimir = () => {
|
||||
if (tipo === 'contrato') {
|
||||
window.open(`${process.env.NEXT_PUBLIC_API_URL}/report/contrato/${id}`, '_blank');
|
||||
} else if (tipo === 'cotizacion') {
|
||||
window.open(`${process.env.NEXT_PUBLIC_API_URL}/report/cotizacion/${id}`, '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="mb-3">Reimpresión de Documentos</h3>
|
||||
<div className="card shadow p-4">
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Tipo</label>
|
||||
<select className="form-select" value={tipo} onChange={e => setTipo(e.target.value)}>
|
||||
<option value="contrato">Contrato</option>
|
||||
<option value="cotizacion">Cotización</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">N° Documento</label>
|
||||
<input className="form-control" value={id} onChange={e => setId(e.target.value)} />
|
||||
</div>
|
||||
<button className="btn btn-primary" onClick={handleReimprimir}>Reimprimir</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { api } from '@/services/api';
|
||||
|
||||
export default function ReporteVentasEmpPage() {
|
||||
const [data, setData] = useState<any[] | null>(null);
|
||||
|
||||
const load = async () => {
|
||||
const result = await api.get('/informe/ventas-empresa', { tipo: 'LISTADO', varA: '', varB: '', varC: 0 });
|
||||
setData(Array.isArray(result) ? result : []);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="mb-3">Ventas Empresas</h3>
|
||||
<button className="btn btn-primary mb-3" onClick={load}>Cargar</button>
|
||||
{data && (
|
||||
<div className="card shadow">
|
||||
<div className="card-body">
|
||||
<table className="table">
|
||||
<thead><tr><th>Empresa</th><th>Curso</th><th>Monto</th><th>Fecha</th></tr></thead>
|
||||
<tbody>
|
||||
{data.map((r: any, i: number) => (
|
||||
<tr key={i}>
|
||||
<td>{r.Empresa || r.empresa}</td>
|
||||
<td>{r.Curso || r.curso}</td>
|
||||
<td>${(r.Monto || r.monto || 0).toLocaleString()}</td>
|
||||
<td>{r.Fecha ? new Date(r.Fecha).toLocaleDateString() : ''}</td>
|
||||
</tr>
|
||||
))}
|
||||
{data.length === 0 && <tr><td colSpan={4} className="text-center">Sin datos</td></tr>}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { api } from '@/services/api';
|
||||
|
||||
export default function ReporteVentasPage() {
|
||||
const [mes, setMes] = useState(new Date().getMonth() + 1);
|
||||
const [agno, setAgno] = useState(new Date().getFullYear());
|
||||
const [data, setData] = useState<any[] | null>(null);
|
||||
|
||||
const load = async () => {
|
||||
const result = await api.get('/informe/ventas-mensuales', { mes, agno, tipo: 'VENTA' });
|
||||
setData(Array.isArray(result) ? result : []);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="mb-3">Reporte de Ventas</h3>
|
||||
<div className="row mb-3">
|
||||
<div className="col-auto">
|
||||
<select className="form-select" value={mes} onChange={e => setMes(Number(e.target.value))}>
|
||||
{Array.from({ length: 12 }, (_, i) => (
|
||||
<option key={i + 1} value={i + 1}>{new Date(0, i).toLocaleString('es', { month: 'long' })}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-auto">
|
||||
<input type="number" className="form-control" value={agno} onChange={e => setAgno(Number(e.target.value))} />
|
||||
</div>
|
||||
<div className="col-auto">
|
||||
<button className="btn btn-primary" onClick={load}>Generar</button>
|
||||
</div>
|
||||
</div>
|
||||
{data && (
|
||||
<div className="card shadow">
|
||||
<div className="card-body">
|
||||
<table className="table table-hover">
|
||||
<thead>
|
||||
<tr><th>Ejecutivo</th><th>Ventas</th><th>Monto</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((r: any, i: number) => (
|
||||
<tr key={i}>
|
||||
<td>{r.Ejecutivo || r.ejecutivo || r.Nombre}</td>
|
||||
<td>{r.Cantidad || r.cantidad || r.Ventas}</td>
|
||||
<td>${(r.Monto || r.monto || r.Total || 0).toLocaleString()}</td>
|
||||
</tr>
|
||||
))}
|
||||
{data.length === 0 && <tr><td colSpan={3} className="text-center">Sin datos</td></tr>}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user