feat: fase 3 - frontend next.js setup
- Next.js 15 + TypeScript + Bootstrap 5 + FontAwesome - Layout replica SAM.Master (sidebar collapsable + header) - Sidebar, Header, LoadingSpinner, ModalConfirmacion components - Login page with RUT validation - Dashboard page with summary cards - AuthContext + JWT auth with cookies - api.ts centralized HTTP client - useInactividad hook (30min timeout) - validarRut.ts + utils.ts migrated - Original CSS + images copied from project - Dockerfile multi-stage build
This commit is contained in:
+17
-14
@@ -77,20 +77,22 @@
|
||||
|
||||
### FASE 3: FRONTEND NEXT.JS (~6-8 semanas)
|
||||
|
||||
- [ ] **3.1 Setup** (create-next-app, packages, config)
|
||||
- [ ] **3.2 Layout Principal** (~1 sem)
|
||||
- [ ] layout.tsx (sidebar + header = SAM.Master)
|
||||
- [ ] Sidebar.tsx, Header.tsx
|
||||
- [ ] CSS original (style.css, ichn.css, etc.)
|
||||
- [ ] **3.3 Login + Auth** (~3 días)
|
||||
- [ ] **3.4 Dashboard** (~3 días)
|
||||
- [ ] **3.5 Módulo Leads** (~1 sem) — 3 páginas
|
||||
- [ ] **3.6 Módulo Cotizaciones/Pagos** (~1 sem)
|
||||
- [ ] **3.7 Módulo Empresas** (~1 sem) — 3 páginas
|
||||
- [ ] **3.8 Resto páginas** (~2-3 sem)
|
||||
- Arqueo, Cursos, Alumnos
|
||||
- [x] **3.1 Setup** (Next.js 15, TypeScript, packages)
|
||||
- [x] **3.2 Layout Principal**
|
||||
- [x] layout.tsx (sidebar + header = SAM.Master)
|
||||
- [x] Sidebar.tsx, Header.tsx, LoadingSpinner, ModalConfirmacion
|
||||
- [x] CSS original copiado + globals.css con sidebar styles
|
||||
- [x] Assets (favicon, ichnHD, samito, Invertido_ICN)
|
||||
- [x] **3.3 Login + Auth** (login page + useAuth hook + JWT)
|
||||
- [x] **3.4 Dashboard** (cards layout)
|
||||
- [x] **3.5 JS migration** (validarRut.ts, utils.ts, useInactividad)
|
||||
- [x] **3.6 api.ts** (centralized HTTP client with cookies)
|
||||
- [x] **Dockerfile Frontend** (multi-stage build)
|
||||
- [ ] **3.5-3.8 Páginas restantes** (~20 páginas)
|
||||
- Leads (listado, detalle, nuevo)
|
||||
- Cotizaciones, Arqueo, Cursos, Alumnos
|
||||
- Empresas (listado, detalle, ventas)
|
||||
- Reportes (3), Autorizador, Contacto
|
||||
- [ ] Dockerfile Frontend
|
||||
|
||||
### FASE 4: CONTENEDORES (~1 semana)
|
||||
|
||||
@@ -120,7 +122,8 @@
|
||||
| 2026-07-08 | F1 | 13 controllers, Ejecutivo entity, ArqueoService, JwtMiddleware, SpComplexQueries | Fase 2 |
|
||||
| 2026-07-08 | F1.7 | Reportes restantes: Anexo, ContratoBlack, Presupuesto, CAEMP/CAEMPSNC, CC_EMPCSD, PropuestaComercial | Fase 2 |
|
||||
| 2026-07-08 | F2 | ServicesExternos: DTE, Transbank, Email + API | Fase 3 |
|
||||
| 2026-07-08 | AUDIT | 27 bugs corregidos: JWT secret, QuestPDF license, schema SPs, Transbank MySQL, JwtMiddleware pipeline, Email disconnect, model validation, casts | Fase 3 |
|
||||
| 2026-07-08 | AUDIT | 27 bugs corregidos en F1+F2 | Fase 3 |
|
||||
| 2026-07-08 | F3 | Setup Next.js + Layout + Sidebar + Login + Auth + Dashboard + componentes + CSS | Resto páginas frontend |
|
||||
| | | | |
|
||||
| | | | |
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
FROM node:20-alpine AS build
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM node:20-alpine AS runtime
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/.next/standalone ./
|
||||
COPY --from=build /app/public ./public
|
||||
EXPOSE 3000
|
||||
ENV NODE_ENV=production
|
||||
CMD ["node", "server.js"]
|
||||
@@ -0,0 +1,9 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: 'standalone',
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "modulo-ventas",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "^15.0.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hot-toast": "^2.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
|
||||
|
||||
.btn-ichn {
|
||||
color: #fff;
|
||||
background-color: #00285d;
|
||||
border-color: #00285d
|
||||
}
|
||||
|
||||
|
||||
.btn-ichn:hover {
|
||||
color: #fff;
|
||||
background-color: #00142E;
|
||||
border-color: #00142E
|
||||
}
|
||||
|
||||
.btn-ichn.focus, .btn-ichn:focus {
|
||||
box-shadow: 0 0 0 .2rem #00285d
|
||||
}
|
||||
|
||||
.btn-ichn.disabled, .btn-ichn:disabled {
|
||||
color: #fff;
|
||||
background-color: #00285d;
|
||||
border-color: #00285d;
|
||||
}
|
||||
|
||||
.btn-ichn:not(:disabled):not(.disabled).active, .btn-ichn:not(:disabled):not(.disabled):active, .show > .btn-ichn.dropdown-toggle {
|
||||
color: #fff;
|
||||
background-color: #00285d;
|
||||
border-color: #00285d
|
||||
}
|
||||
|
||||
.btn-ichn:not(:disabled):not(.disabled).active:focus, .btn-ichn:not(:disabled):not(.disabled):active:focus, .show > .btn-ichn.dropdown-toggle:focus {
|
||||
box-shadow: 0 0 0 .2rem #00285d
|
||||
}
|
||||
|
||||
.btn-ichnRojo {
|
||||
color: #fff;
|
||||
background-color: #e22523;
|
||||
border-color: #e22523
|
||||
}
|
||||
|
||||
|
||||
.btn-ichnRojo:hover {
|
||||
color: #fff;
|
||||
background-color: #8A101C;
|
||||
border-color: #8A101C
|
||||
}
|
||||
|
||||
.btn-ichnRojo.focus, .btn-ichnRojo:focus {
|
||||
box-shadow: 0 0 0 .2rem #B91425
|
||||
}
|
||||
|
||||
.btn-ichnRojo.disabled, .btn-ichnRojo:disabled {
|
||||
color: #fff;
|
||||
background-color: #B91425;
|
||||
border-color: #B91425;
|
||||
}
|
||||
|
||||
.btn-ichnRojo:not(:disabled):not(.disabled).active, .btn-ichnRojo:not(:disabled):not(.disabled):active, .show > .btn-ichnRojo.dropdown-toggle {
|
||||
color: #fff;
|
||||
background-color: #B91425;
|
||||
border-color: #B91425
|
||||
}
|
||||
|
||||
.btn-ichnRojo:not(:disabled):not(.disabled).active:focus, .btn-ichnRojo:not(:disabled):not(.disabled):active:focus, .show > .btn-ichnRojo.dropdown-toggle:focus {
|
||||
box-shadow: 0 0 0 .2rem #B91425
|
||||
}
|
||||
|
||||
|
||||
.btn-ichnPurpura {
|
||||
color: #fff;
|
||||
background-color: #6B3B7C;
|
||||
border-color: #6B3B7C;
|
||||
}
|
||||
|
||||
.btn-ichnPurpura:hover {
|
||||
color: #fff;
|
||||
background-color: #562965;
|
||||
border-color: #562965
|
||||
}
|
||||
|
||||
.btn-ichnPurpura.focus, .btn-ichnPurpura:focus {
|
||||
box-shadow: 0 0 0 .2rem #844c98
|
||||
}
|
||||
|
||||
.btn-ichnPurpura.disabled, .btn-ichnPurpura:disabled {
|
||||
color: #fff;
|
||||
background-color: #844c98;
|
||||
border-color: #844c98;
|
||||
}
|
||||
|
||||
.btn-ichnPurpura:not(:disabled):not(.disabled).active, .btn-ichnPurpura:not(:disabled):not(.disabled):active, .show > .btn-ichnPurpura.dropdown-toggle {
|
||||
color: #fff;
|
||||
background-color: #844c98;
|
||||
border-color: #844c98
|
||||
}
|
||||
|
||||
.btn-ichnPurpura:not(:disabled):not(.disabled).active:focus, .btn-ichnPurpura:not(:disabled):not(.disabled):active:focus, .show > .btn-ichnPurpura.dropdown-toggle:focus {
|
||||
box-shadow: 0 0 0 .2rem #844c98
|
||||
}
|
||||
|
||||
|
||||
.btn-ichnVerde {
|
||||
color: #fff;
|
||||
background-color: #3D912F;
|
||||
border-color: #3D912F
|
||||
}
|
||||
|
||||
|
||||
.btn-ichnVerde:hover {
|
||||
color: #fff;
|
||||
background-color: #2A6D1E;
|
||||
border-color: #2A6D1E
|
||||
}
|
||||
|
||||
.btn-ichnVerde.focus, .btn-ichnVerde:focus {
|
||||
box-shadow: 0 0 0 .2rem #3D912F
|
||||
}
|
||||
|
||||
.btn-ichnVerde.disabled, .btn-ichnVerde:disabled {
|
||||
color: #fff;
|
||||
background-color: #3D912F;
|
||||
border-color: #3D912F;
|
||||
}
|
||||
|
||||
.btn-ichnVerde:not(:disabled):not(.disabled).active, .btn-ichnVerde:not(:disabled):not(.disabled):active, .show > .btn-ichnVerde.dropdown-toggle {
|
||||
color: #fff;
|
||||
background-color: #3D912F;
|
||||
border-color: #3D912F
|
||||
}
|
||||
|
||||
.btn-ichnVerde:not(:disabled):not(.disabled).active:focus, .btn-ichnVerde:not(:disabled):not(.disabled):active:focus, .show > .btn-ichnVerde.dropdown-toggle:focus {
|
||||
box-shadow: 0 0 0 .2rem #3D912F
|
||||
}
|
||||
|
||||
|
||||
.btn-ichnExcel {
|
||||
color: #fff;
|
||||
background-color: #008000;
|
||||
border-color: #008000
|
||||
}
|
||||
|
||||
|
||||
.btn-ichnExcel:hover {
|
||||
color: #fff;
|
||||
background-color: #009900;
|
||||
border-color: #009900;
|
||||
}
|
||||
|
||||
.btn-ichnExcel.focus, .btn-ichnExcel:focus {
|
||||
box-shadow: 0 0 0 .2rem #008000;
|
||||
}
|
||||
|
||||
.btn-ichnExcel.disabled, .btn-ichnExcel:disabled {
|
||||
color: #fff;
|
||||
background-color: #008000;
|
||||
border-color: #008000;
|
||||
}
|
||||
|
||||
.btn-ichnExcel:not(:disabled):not(.disabled).active, .btn-ichnExcel:not(:disabled):not(.disabled):active, .show > .btn-ichnExcel.dropdown-toggle {
|
||||
color: #fff;
|
||||
background-color: #008000;
|
||||
border-color: #008000
|
||||
}
|
||||
|
||||
.btn-ichnExcel:not(:disabled):not(.disabled).active:focus, .btn-ichnExcel:not(:disabled):not(.disabled):active:focus, .show > .btn-ichnExcel.dropdown-toggle:focus {
|
||||
box-shadow: 0 0 0 .2rem #008000
|
||||
}
|
||||
|
||||
|
||||
.examen:hover {
|
||||
background-image: linear-gradient( rgba(185, 20, 37 ), rgba(185, 20, 37) )
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
position: relative;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
pointer-events: auto;
|
||||
background-color: #fff;
|
||||
background-clip: padding-box;
|
||||
border: 0px solid;
|
||||
border-radius: .5rem;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: -ms-flexbox;
|
||||
display: block;
|
||||
-ms-flex-align: start;
|
||||
align-items: flex-start;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 1rem;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
border-top-left-radius: .3rem;
|
||||
border-top-right-radius: .3rem;
|
||||
}
|
||||
|
||||
.btn-circle {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
text-align: center;
|
||||
padding: 6px 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.428571429;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.btn-circle.btn-lg {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
padding: 10px 16px;
|
||||
font-size: 18px;
|
||||
line-height: 1.33;
|
||||
border-radius: 25px;
|
||||
}
|
||||
|
||||
.btn-circle.btn-xl {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
padding: 10px 16px;
|
||||
font-size: 24px;
|
||||
line-height: 1.33;
|
||||
border-radius: 35px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
.div_1 {
|
||||
height: 80px;
|
||||
background-color: #D1D1D1;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.div_2 {
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
label .prueba {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-item.show .nav-link, .nav-tabs .nav-link.active {
|
||||
color: #ffff;
|
||||
background-color: #22376c;
|
||||
border-color: #dee2e6 #dee2e6 #fff;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-item.show .nav-link, .nav-tabs .nav-link {
|
||||
color: #22376c;
|
||||
}
|
||||
|
||||
.spinner, .spinner:after {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
position: fixed;
|
||||
top: 90%;
|
||||
left: 50%;
|
||||
margin-top: -32px;
|
||||
margin-left: -32px;
|
||||
border-radius: 50%;
|
||||
z-index: 2
|
||||
}
|
||||
|
||||
.spinner {
|
||||
background-color: transparent;
|
||||
border-top: 10px solid rgb(0, 40, 93);
|
||||
border-right: 10px solid rgb(0, 40, 93);
|
||||
border-bottom: 10px solid rgb(0, 40, 93);
|
||||
border-left: 10px solid rgba(0, 40, 93,.2);
|
||||
transform: translateZ(0);
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: linear;
|
||||
animation-duration: 1.5s;
|
||||
animation-name: spinner-loading
|
||||
}
|
||||
|
||||
.menuIchnHover {
|
||||
background-color: #E8E8E8;
|
||||
}
|
||||
|
||||
.menuIchnHover a {
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.menuIchnHover:hover {
|
||||
background-color: #00285d;
|
||||
color: #fff;
|
||||
border-radius: 5px
|
||||
}
|
||||
|
||||
|
||||
.menuIchnHover:hover a {
|
||||
background-color: #00285d;
|
||||
color: #fff;
|
||||
border-radius: 5px
|
||||
}
|
||||
|
||||
|
||||
@keyframes spinner-loading {
|
||||
0% {
|
||||
transform: rotate(0deg)
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(1turn)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 75px;
|
||||
background: #00285d;
|
||||
padding: 6px 14px;
|
||||
z-index: 99;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.sidebar.open {
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.sidebar .logo-details {
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sidebar .logo-details .icon {
|
||||
opacity: 0;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.sidebar .logo-details .logo_name {
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
opacity: 0;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.sidebar.open .logo-details .icon,
|
||||
.sidebar.open .logo-details .logo_name {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sidebar .logo-details #btn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
transform: translateY(-50%);
|
||||
font-size: 22px;
|
||||
transition: all 0.4s ease;
|
||||
font-size: 23px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.sidebar.open .logo-details #btn {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.sidebar i {
|
||||
color: #fff;
|
||||
height: 60px;
|
||||
min-width: 50px;
|
||||
font-size: 28px;
|
||||
text-align: center;
|
||||
line-height: 60px;
|
||||
}
|
||||
|
||||
.sidebar .nav-list {
|
||||
margin-top: 20px;
|
||||
height: 100%;
|
||||
padding-left: 0rem;
|
||||
}
|
||||
|
||||
.sidebar li {
|
||||
position: relative;
|
||||
margin: 8px 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.sidebar li .tooltip {
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
left: calc(100% + 15px);
|
||||
z-index: 3;
|
||||
background: #fff;
|
||||
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.3);
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
opacity: 0;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
transition: 0s;
|
||||
}
|
||||
|
||||
.sidebar li:hover .tooltip {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transition: all 0.4s ease;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.sidebar.open li .tooltip {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar input {
|
||||
font-size: 15px;
|
||||
color: #FFF;
|
||||
font-weight: 400;
|
||||
outline: none;
|
||||
height: 50px;
|
||||
width: 100%;
|
||||
width: 50px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
transition: all 0.5s ease;
|
||||
background: #1d1b31;
|
||||
}
|
||||
|
||||
.sidebar.open input {
|
||||
padding: 0 20px 0 50px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sidebar .bx-search {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
transform: translateY(-50%);
|
||||
font-size: 22px;
|
||||
background: #1d1b31;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
.sidebar.open .bx-search:hover {
|
||||
background: #1d1b31;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
.sidebar .bx-search:hover {
|
||||
background: #FFF;
|
||||
color: #00285d;
|
||||
}
|
||||
|
||||
.sidebar li a {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: 12px;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
transition: all 0.4s ease;
|
||||
background: #00285d;
|
||||
}
|
||||
|
||||
.sidebar li a:hover {
|
||||
background: #FFF;
|
||||
}
|
||||
|
||||
.sidebar li a .links_name {
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: 0.4s;
|
||||
}
|
||||
|
||||
.sidebar.open li a .links_name {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.sidebar li a:hover .links_name,
|
||||
.sidebar li a:hover i {
|
||||
transition: all 0.5s ease;
|
||||
color: #00285d;
|
||||
}
|
||||
|
||||
.sidebar li i {
|
||||
height: 35px;
|
||||
line-height: 35px;
|
||||
font-size: 16px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.sidebar li.profile {
|
||||
position: fixed;
|
||||
height: 60px;
|
||||
width: 78px;
|
||||
left: 0;
|
||||
bottom: -8px;
|
||||
padding: 10px 14px;
|
||||
background: #001D44;
|
||||
transition: all 0.5s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar.open li.profile {
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.sidebar li .profile-details {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.sidebar li img {
|
||||
height: 45px;
|
||||
width: 45px;
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.sidebar li.profile .name,
|
||||
.sidebar li.profile .job {
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
color: #fff;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sidebar li.profile .job {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.sidebar .profile #log_out {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
transform: translateY(-50%);
|
||||
background: #1d1b31;
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
line-height: 60px;
|
||||
border-radius: 0px;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.sidebar.open .profile #log_out {
|
||||
width: 50px;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.home-section {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
top: 0;
|
||||
left: 75px;
|
||||
width: calc(100% - 75px);
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.sidebar.open ~ .home-section {
|
||||
left: 250px;
|
||||
width: calc(100% - 250px);
|
||||
transition: all 1s ease;
|
||||
}
|
||||
|
||||
.home-section .text {
|
||||
display: inline-block;
|
||||
margin: 18px
|
||||
}
|
||||
|
||||
@media (max-width: 620px) {
|
||||
.sidebar li .tooltip {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 166 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 166 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
@@ -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';
|
||||
|
||||
const cards = [
|
||||
{ title: 'Nuevos Leads', icon: 'fas fa-users', color: '#4e73df', endpoint: '/lead/nuevos' },
|
||||
{ title: 'Cotizaciones', icon: 'fas fa-file-invoice-dollar', color: '#1cc88a', endpoint: '/cotizacion/buscar' },
|
||||
{ title: 'Cursos', icon: 'fas fa-book', color: '#36b9cc', endpoint: '/curso' },
|
||||
{ title: 'Alumnos', icon: 'fas fa-user-graduate', color: '#f6c23e', endpoint: '/alumno' },
|
||||
];
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { user, loading } = useAuth();
|
||||
const router = useRouter();
|
||||
const [pageLoading, setPageLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !user) router.push('/login');
|
||||
if (!loading) setPageLoading(false);
|
||||
}, [user, loading, router]);
|
||||
|
||||
if (pageLoading) return <LoadingSpinner />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="mb-4">Bienvenido, {user?.nombre}</h2>
|
||||
<div className="row">
|
||||
{cards.map(card => (
|
||||
<div className="col-xl-3 col-md-6 mb-4" key={card.title}>
|
||||
<div className="card border-left-primary shadow h-100 py-2" style={{ borderLeft: `.25rem solid ${card.color}` }}>
|
||||
<div className="card-body">
|
||||
<div className="row no-gutters align-items-center">
|
||||
<div className="col mr-2">
|
||||
<div className="text-xs font-weight-bold text-primary text-uppercase mb-1">{card.title}</div>
|
||||
<div className="h5 mb-0 font-weight-bold text-gray-800">-</div>
|
||||
</div>
|
||||
<div className="col-auto">
|
||||
<i className={`${card.icon} fa-2x text-gray-300`}></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { AuthProvider } from '@/hooks/useAuth';
|
||||
import Sidebar from '@/components/Sidebar';
|
||||
import Header from '@/components/Header';
|
||||
import '@/styles/globals.css';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'SAM - Sistema de Administración de Ventas',
|
||||
description: 'Módulo de Ventas - Instituto Chileno Norteamericano',
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="es">
|
||||
<head>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||||
<link href="https://fonts.googleapis.com/css?family=Fira+Sans&display=swap" rel="stylesheet" />
|
||||
<link href="https://unpkg.com/boxicons@2.0.7/css/boxicons.min.css" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />
|
||||
</head>
|
||||
<body>
|
||||
<AuthProvider>
|
||||
<Sidebar />
|
||||
<section className="home-section alert-dark">
|
||||
<Header />
|
||||
<div className="container-fluid mt-3 px-4">
|
||||
{children}
|
||||
</div>
|
||||
</section>
|
||||
</AuthProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
'use client';
|
||||
|
||||
import { useState, FormEvent } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { validarRut, formatRut } from '@/lib/validarRut';
|
||||
|
||||
export default function LoginPage() {
|
||||
const [rut, setRut] = useState('');
|
||||
const [clave, setClave] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const { login } = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
const handleRutChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value.replace(/[^0-9kK-]/g, '');
|
||||
setRut(value);
|
||||
if (value.length > 2) {
|
||||
setRut(formatRut(value));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
|
||||
if (!validarRut(rut)) {
|
||||
setError('RUT inválido');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await login(rut, clave);
|
||||
router.push('/dashboard');
|
||||
} catch {
|
||||
setError('Credenciales inválidas');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mt-5">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-md-4">
|
||||
<div className="card shadow">
|
||||
<div className="card-body p-4">
|
||||
<div className="text-center mb-4">
|
||||
<img src="/img/ichnHD.png" alt="ICHN" height="60" />
|
||||
<h4 className="mt-2">Iniciar Sesión</h4>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">RUT</label>
|
||||
<input className="form-control" value={rut} onChange={handleRutChange} placeholder="12.345.678-5" />
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Clave</label>
|
||||
<input type="password" className="form-control" value={clave} onChange={e => setClave(e.target.value)} />
|
||||
</div>
|
||||
{error && <div className="alert alert-danger py-2">{error}</div>}
|
||||
<button type="submit" className="btn btn-primary w-100">Ingresar</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import LoadingSpinner from '@/components/LoadingSpinner';
|
||||
|
||||
export default function Home() {
|
||||
const router = useRouter();
|
||||
const { user, loading } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading) {
|
||||
router.push(user ? '/dashboard' : '/login');
|
||||
}
|
||||
}, [user, loading, router]);
|
||||
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
'use client';
|
||||
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function Header() {
|
||||
const { user, logout } = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
router.push('/login');
|
||||
};
|
||||
|
||||
return (
|
||||
<header>
|
||||
<div className="header-content">
|
||||
<span className="badge">Menu de Aplicaciones</span>
|
||||
<div className="user-info">
|
||||
<span className="user-name">
|
||||
<i className="fas fa-user"></i> {user?.nombre || 'Usuario'}
|
||||
</span>
|
||||
<button className="btn-logout" onClick={handleLogout}>
|
||||
<i className="fas fa-sign-out-alt"></i> Cerrar Sesión
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export default function LoadingSpinner({ text = 'Cargando...' }: { text?: string }) {
|
||||
return (
|
||||
<div className="loading-spinner">
|
||||
<div className="spinner"></div>
|
||||
<p>{text}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
'use client';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
title: string;
|
||||
message: string;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
confirmText?: string;
|
||||
}
|
||||
|
||||
export default function ModalConfirmacion({ open, title, message, onConfirm, onCancel, confirmText = 'Aceptar' }: Props) {
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div className="modal-overlay" onClick={onCancel}>
|
||||
<div className="modal-content" onClick={e => e.stopPropagation()}>
|
||||
<h3>{title}</h3>
|
||||
<p>{message}</p>
|
||||
<div className="modal-actions">
|
||||
<button className="btn btn-secondary" onClick={onCancel}>Cancelar</button>
|
||||
<button className="btn btn-primary" onClick={onConfirm}>{confirmText}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
|
||||
const menuItems = [
|
||||
{ href: '/dashboard', icon: 'fas fa-home', label: 'Dashboard' },
|
||||
{ href: '/leads', icon: 'fas fa-users', label: 'Leads' },
|
||||
{ href: '/cotizaciones', icon: 'fas fa-file-invoice-dollar', label: 'Cotizaciones' },
|
||||
{ href: '/cursos', icon: 'fas fa-book', label: 'Cursos' },
|
||||
{ href: '/alumnos', icon: 'fas fa-user-graduate', label: 'Alumnos' },
|
||||
{ href: '/arqueo', icon: 'fas fa-cash-register', label: 'Arqueo' },
|
||||
{ href: '/empresas', icon: 'fas fa-building', label: 'Empresas' },
|
||||
{ href: '/reportes/ventas', icon: 'fas fa-chart-bar', label: 'Reportes' },
|
||||
{ href: '/autorizador', icon: 'fas fa-check-double', label: 'Autorizador' },
|
||||
{ href: '/contacto', icon: 'fas fa-envelope', label: 'Contacto' },
|
||||
];
|
||||
|
||||
export default function Sidebar() {
|
||||
const pathname = usePathname();
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
|
||||
return (
|
||||
<div className={`sidebar ${collapsed ? 'collapsed' : ''}`}>
|
||||
<div className="logo-details">
|
||||
<img className="icon" src="/img/favicon.png" width="60" alt="SAM" />
|
||||
<div className="logo_name">SAM</div>
|
||||
<i className="bx bx-menu" id="btn" onClick={() => setCollapsed(!collapsed)}></i>
|
||||
</div>
|
||||
<ul className="nav-list">
|
||||
{menuItems.map(item => (
|
||||
<li key={item.href} className={pathname.startsWith(item.href) ? 'active' : ''}>
|
||||
<Link href={item.href}>
|
||||
<i className={item.icon}></i>
|
||||
<span className="links_name">{item.label}</span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
import { api } from '@/services/api';
|
||||
|
||||
interface User {
|
||||
nombre: string;
|
||||
sede?: string;
|
||||
perfil?: string;
|
||||
}
|
||||
|
||||
interface AuthContextType {
|
||||
user: User | null;
|
||||
loading: boolean;
|
||||
login: (rut: string, clave: string) => Promise<void>;
|
||||
logout: () => void;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType>({} as AuthContextType);
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const stored = sessionStorage.getItem('sam_user');
|
||||
if (stored) {
|
||||
setUser(JSON.parse(stored));
|
||||
}
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
const login = async (rut: string, clave: string) => {
|
||||
const res = await api.post<{ nombre: string; sede?: string; token: string }>('/auth/login', { rut, clave });
|
||||
sessionStorage.setItem('sam_user', JSON.stringify({ nombre: res.nombre, sede: res.sede }));
|
||||
setUser({ nombre: res.nombre, sede: res.sede });
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
sessionStorage.removeItem('sam_user');
|
||||
setUser(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ user, loading, login, logout }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useAuth = () => useContext(AuthContext);
|
||||
@@ -0,0 +1,25 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
const TIMEOUT_MS = 30 * 60 * 1000;
|
||||
|
||||
export function useInactividad(onTimeout: () => void) {
|
||||
const timerRef = useRef<NodeJS.Timeout>();
|
||||
|
||||
const resetTimer = () => {
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
timerRef.current = setTimeout(onTimeout, TIMEOUT_MS);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const events = ['mousedown', 'keydown', 'scroll', 'touchstart'];
|
||||
resetTimer();
|
||||
|
||||
events.forEach(event => window.addEventListener(event, resetTimer));
|
||||
return () => {
|
||||
events.forEach(event => window.removeEventListener(event, resetTimer));
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
};
|
||||
}, [onTimeout]);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export function formatCurrency(amount: number): string {
|
||||
return new Intl.NumberFormat('es-CL', { style: 'currency', currency: 'CLP', maximumFractionDigits: 0 }).format(amount);
|
||||
}
|
||||
|
||||
export function formatDate(date: string | Date): string {
|
||||
const d = typeof date === 'string' ? new Date(date) : date;
|
||||
return d.toLocaleDateString('es-CL', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
||||
}
|
||||
|
||||
export function classNames(...classes: (string | boolean | undefined)[]): string {
|
||||
return classes.filter(Boolean).join(' ');
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
export function validarRut(rut: string): boolean {
|
||||
const limpio = rut.replace(/\./g, '').replace(/-/g, '').toUpperCase();
|
||||
if (!/^[0-9]+[0-9K]$/.test(limpio)) return false;
|
||||
|
||||
const cuerpo = limpio.slice(0, -1);
|
||||
const dv = limpio.slice(-1);
|
||||
|
||||
let suma = 0;
|
||||
let multiplo = 2;
|
||||
|
||||
for (let i = cuerpo.length - 1; i >= 0; i--) {
|
||||
suma += parseInt(cuerpo[i]) * multiplo;
|
||||
multiplo = multiplo < 7 ? multiplo + 1 : 2;
|
||||
}
|
||||
|
||||
const dvEsperado = 11 - (suma % 11);
|
||||
const dvChar = dvEsperado === 11 ? '0' : dvEsperado === 10 ? 'K' : String(dvEsperado);
|
||||
|
||||
return dvChar === dv;
|
||||
}
|
||||
|
||||
export function formatRut(rut: string): string {
|
||||
const limpio = rut.replace(/\./g, '').replace(/-/g, '');
|
||||
if (limpio.length < 2) return limpio;
|
||||
const cuerpo = limpio.slice(0, -1);
|
||||
const dv = limpio.slice(-1);
|
||||
const formateado = cuerpo.replace(/\B(?=(\d{3})+(?!\d))/g, '.');
|
||||
return `${formateado}-${dv}`;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5087/api';
|
||||
|
||||
interface ApiOptions {
|
||||
method?: string;
|
||||
body?: unknown;
|
||||
params?: Record<string, string | number | boolean>;
|
||||
}
|
||||
|
||||
async function request<T>(endpoint: string, options: ApiOptions = {}): Promise<T> {
|
||||
const { method = 'GET', body, params } = options;
|
||||
|
||||
let url = `${API_URL}${endpoint}`;
|
||||
if (params) {
|
||||
const searchParams = new URLSearchParams();
|
||||
Object.entries(params).forEach(([k, v]) => searchParams.append(k, String(v)));
|
||||
url += `?${searchParams.toString()}`;
|
||||
}
|
||||
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.text();
|
||||
throw new Error(error || `HTTP ${res.status}`);
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export const api = {
|
||||
get: <T>(endpoint: string, params?: Record<string, string | number | boolean>) =>
|
||||
request<T>(endpoint, { params }),
|
||||
|
||||
post: <T>(endpoint: string, body?: unknown) =>
|
||||
request<T>(endpoint, { method: 'POST', body }),
|
||||
|
||||
put: <T>(endpoint: string, body?: unknown) =>
|
||||
request<T>(endpoint, { method: 'PUT', body }),
|
||||
};
|
||||
@@ -0,0 +1,272 @@
|
||||
@import url('https://fonts.googleapis.com/css?family=Fira+Sans&display=swap');
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Fira Sans', sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #e9ecef;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Sidebar (replica SAM.Master) */
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 78px;
|
||||
background: #11101d;
|
||||
padding: 6px 14px;
|
||||
z-index: 99;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.sidebar.open {
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.sidebar .logo-details {
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sidebar .logo-details .icon {
|
||||
opacity: 0;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.sidebar.open .logo-details .icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sidebar .logo-details .logo_name {
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
opacity: 0;
|
||||
transition: all 0.5s ease;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.sidebar.open .logo-details .logo_name {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sidebar .logo-details #btn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
transform: translateY(-50%);
|
||||
font-size: 23px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.5s ease;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.sidebar.open .logo-details #btn {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.sidebar i {
|
||||
color: #fff;
|
||||
height: 60px;
|
||||
min-width: 50px;
|
||||
font-size: 28px;
|
||||
text-align: center;
|
||||
line-height: 60px;
|
||||
}
|
||||
|
||||
.sidebar .nav-list {
|
||||
margin-top: 20px;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.sidebar .nav-list li {
|
||||
position: relative;
|
||||
list-style: none;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.sidebar .nav-list li a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
border-radius: 12px;
|
||||
transition: all 0.4s ease;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.sidebar .nav-list li a:hover {
|
||||
background: #FFF;
|
||||
}
|
||||
|
||||
.sidebar .nav-list li a:hover i,
|
||||
.sidebar .nav-list li a:hover .links_name {
|
||||
color: #11101d;
|
||||
}
|
||||
|
||||
.sidebar .nav-list li.active a {
|
||||
background: #FFF;
|
||||
}
|
||||
|
||||
.sidebar .nav-list li.active a i,
|
||||
.sidebar .nav-list li.active a .links_name {
|
||||
color: #11101d;
|
||||
}
|
||||
|
||||
.sidebar .nav-list li .links_name {
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: 0.4s;
|
||||
}
|
||||
|
||||
.sidebar.open .nav-list li .links_name {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* Home section */
|
||||
.home-section {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
left: 78px;
|
||||
width: calc(100% - 78px);
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.sidebar.open ~ .home-section {
|
||||
left: 250px;
|
||||
width: calc(100% - 250px);
|
||||
}
|
||||
|
||||
.home-section header {
|
||||
background: #fff;
|
||||
padding: 10px 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.badge {
|
||||
background: #4e73df;
|
||||
color: #fff;
|
||||
padding: 6px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.btn-logout {
|
||||
background: none;
|
||||
border: 1px solid #dc3545;
|
||||
color: #dc3545;
|
||||
padding: 5px 12px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-logout:hover {
|
||||
background: #dc3545;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Loading spinner */
|
||||
.loading-spinner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 60vh;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #4e73df;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: #fff;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
max-width: 400px;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* Card styles */
|
||||
.card {
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.card.shadow {
|
||||
box-shadow: 0 0.15rem 1.75rem 0 rgba(58,59,69,0.15);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
width: 60px;
|
||||
}
|
||||
.sidebar.open {
|
||||
width: 200px;
|
||||
}
|
||||
.home-section {
|
||||
left: 60px;
|
||||
width: calc(100% - 60px);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [{ "name": "next" }],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user