Se modifico panel de notificaciones
This commit is contained in:
8
.env
8
.env
@@ -1,4 +1,6 @@
|
|||||||
DEBUG_MODE=true
|
VITE_DEBUG_MODE=false
|
||||||
|
|
||||||
VITE_EFC_API_URL=https://api.efc-aduanasoft.com/api/v1
|
VITE_EFC_API_URL=http://192.168.1.195:8000/api/v1
|
||||||
VITE_EFC_MICROSERVICE_URL=https://api.efc-aduanasoft.com/microservice/api/v1
|
VITE_EFC_MICROSERVICE_URL=http://192.168.1.195:8001/api/v1
|
||||||
|
|
||||||
|
VITE_WEBSOCKET_URL=ws://192.168.1.195:8000/ws/notifications/
|
||||||
|
|||||||
128
package-lock.json
generated
128
package-lock.json
generated
@@ -19,7 +19,6 @@
|
|||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-router-dom": "^7.6.2",
|
"react-router-dom": "^7.6.2",
|
||||||
"react-window": "^1.8.11",
|
"react-window": "^1.8.11",
|
||||||
"socket.io-client": "^4.8.1",
|
|
||||||
"styled-components": "^6.1.19"
|
"styled-components": "^6.1.19"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -1462,11 +1461,6 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@socket.io/component-emitter": {
|
|
||||||
"version": "3.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
|
||||||
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
|
|
||||||
},
|
|
||||||
"node_modules/@tanstack/query-core": {
|
"node_modules/@tanstack/query-core": {
|
||||||
"version": "5.81.5",
|
"version": "5.81.5",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.81.5.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.81.5.tgz",
|
||||||
@@ -2100,42 +2094,6 @@
|
|||||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/engine.io-client": {
|
|
||||||
"version": "6.6.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
|
|
||||||
"integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
|
|
||||||
"dependencies": {
|
|
||||||
"@socket.io/component-emitter": "~3.1.0",
|
|
||||||
"debug": "~4.3.1",
|
|
||||||
"engine.io-parser": "~5.2.1",
|
|
||||||
"ws": "~8.17.1",
|
|
||||||
"xmlhttprequest-ssl": "~2.1.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/engine.io-client/node_modules/debug": {
|
|
||||||
"version": "4.3.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
|
||||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"ms": "^2.1.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"supports-color": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/engine.io-parser": {
|
|
||||||
"version": "5.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
|
||||||
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/error-ex": {
|
"node_modules/error-ex": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
|
||||||
@@ -3707,64 +3665,6 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/socket.io-client": {
|
|
||||||
"version": "4.8.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
|
|
||||||
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"@socket.io/component-emitter": "~3.1.0",
|
|
||||||
"debug": "~4.3.2",
|
|
||||||
"engine.io-client": "~6.6.1",
|
|
||||||
"socket.io-parser": "~4.2.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/socket.io-client/node_modules/debug": {
|
|
||||||
"version": "4.3.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
|
||||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"ms": "^2.1.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"supports-color": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/socket.io-parser": {
|
|
||||||
"version": "4.2.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
|
||||||
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
|
||||||
"dependencies": {
|
|
||||||
"@socket.io/component-emitter": "~3.1.0",
|
|
||||||
"debug": "~4.3.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/socket.io-parser/node_modules/debug": {
|
|
||||||
"version": "4.3.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
|
||||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"ms": "^2.1.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"supports-color": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/source-map": {
|
"node_modules/source-map": {
|
||||||
"version": "0.5.7",
|
"version": "0.5.7",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
||||||
@@ -4356,34 +4256,6 @@
|
|||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ws": {
|
|
||||||
"version": "8.17.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
|
||||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"bufferutil": "^4.0.1",
|
|
||||||
"utf-8-validate": ">=5.0.2"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"bufferutil": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"utf-8-validate": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/xmlhttprequest-ssl": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||||
|
|||||||
@@ -21,7 +21,6 @@
|
|||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-router-dom": "^7.6.2",
|
"react-router-dom": "^7.6.2",
|
||||||
"react-window": "^1.8.11",
|
"react-window": "^1.8.11",
|
||||||
"socket.io-client": "^4.8.1",
|
|
||||||
"styled-components": "^6.1.19"
|
"styled-components": "^6.1.19"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,391 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { login } from '../api/auth';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import { colors } from '../theme';
|
|
||||||
|
|
||||||
export default function Login() {
|
|
||||||
const [username, setUsername] = useState('');
|
|
||||||
const [password, setPassword] = useState('');
|
|
||||||
const [error, setError] = useState('');
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setError('');
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const data = await login(username, password);
|
|
||||||
localStorage.setItem('access', data.access);
|
|
||||||
localStorage.setItem('refresh', data.refresh);
|
|
||||||
|
|
||||||
// Disparar evento personalizado para que el navbar se actualice
|
|
||||||
window.dispatchEvent(new CustomEvent('authStateChanged'));
|
|
||||||
|
|
||||||
// Redirigir al dashboard
|
|
||||||
window.location.href = '/admin';
|
|
||||||
} catch (err) {
|
|
||||||
setError('Usuario o contraseña incorrectos');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="min-h-screen bg-light-gray flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
|
||||||
{/* Background pattern */}
|
|
||||||
<div className="absolute inset-0 opacity-20">
|
|
||||||
<div className="absolute inset-0" style={{
|
|
||||||
backgroundImage: `url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fillRule='evenodd'%3E%3Cg fill='%23${colors.primary.navy.substring(1)}' fillOpacity='0.1'%3E%3Ccircle cx='30' cy='30' r='2'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")`
|
|
||||||
}}></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative max-w-md w-full">
|
|
||||||
{/* Main Card */}
|
|
||||||
<div className="bg-white backdrop-blur-md rounded-3xl shadow-2xl border border-gray-200 overflow-hidden">
|
|
||||||
{/* Header with navy background */}
|
|
||||||
<div className="bg-navy px-8 py-10 text-center">
|
|
||||||
<div className="mb-4">
|
|
||||||
<Link to="/" className="inline-block">
|
|
||||||
<h1 className="text-4xl font-bold text-white">
|
|
||||||
EFC
|
|
||||||
</h1>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<h2 className="text-2xl font-bold text-white mb-2">
|
|
||||||
Bienvenido de vuelta
|
|
||||||
</h2>
|
|
||||||
<p className="text-white/80 text-sm">
|
|
||||||
Inicia sesión para acceder a tu plataforma aduanal
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Form */}
|
|
||||||
<div className="px-8 py-8">
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
|
||||||
{/* Username Field */}
|
|
||||||
<div>
|
|
||||||
<label htmlFor="username" className="block text-sm font-semibold text-text-primary mb-2">
|
|
||||||
Usuario
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
||||||
<svg className="h-5 w-5 text-text-secondary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
id="username"
|
|
||||||
name="username"
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
className="block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-xl text-text-primary placeholder-text-secondary focus:outline-none focus:ring-2 focus:ring-info focus:border-transparent transition duration-200 hover:border-info/50"
|
|
||||||
placeholder="Ingresa tu usuario"
|
|
||||||
value={username}
|
|
||||||
onChange={e => setUsername(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Password Field */}
|
|
||||||
<div>
|
|
||||||
<label htmlFor="password" className="block text-sm font-semibold text-text-primary mb-2">
|
|
||||||
Contraseña
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
||||||
<svg className="h-5 w-5 text-text-secondary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
type={showPassword ? "text" : "password"}
|
|
||||||
required
|
|
||||||
className="block w-full pl-10 pr-12 py-3 border border-gray-300 rounded-xl text-text-primary placeholder-text-secondary focus:outline-none focus:ring-2 focus:ring-info focus:border-transparent transition duration-200 hover:border-info/50"
|
|
||||||
placeholder="Ingresa tu contraseña"
|
|
||||||
value={password}
|
|
||||||
onChange={e => setPassword(e.target.value)}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
|
||||||
onClick={() => setShowPassword(!showPassword)}
|
|
||||||
>
|
|
||||||
{showPassword ? (
|
|
||||||
<svg className="h-5 w-5 text-text-secondary hover:text-text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L8.464 8.464m1.414 1.414L12 12m0 0l2.122 2.122m0 0l1.414 1.414" />
|
|
||||||
</svg>
|
|
||||||
) : (
|
|
||||||
<svg className="h-5 w-5 text-text-secondary hover:text-text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Error Message */}
|
|
||||||
{error && (
|
|
||||||
<div className="rounded-xl bg-red-50 border border-error/20 p-4 animate-pulse">
|
|
||||||
<div className="flex">
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<svg className="h-5 w-5 text-error" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div className="ml-3">
|
|
||||||
<h3 className="text-sm font-medium text-error">
|
|
||||||
{error}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Login Button */}
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={loading}
|
|
||||||
className="group relative w-full flex justify-center py-3 px-4 border border-transparent text-sm font-semibold rounded-xl text-white bg-navy hover:bg-navy-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-navy disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-300 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5"
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<>
|
|
||||||
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" fill="none" viewBox="0 0 24 24">
|
|
||||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
||||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
||||||
</svg>
|
|
||||||
Ingresando...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<span>Ingresar</span>
|
|
||||||
<svg className="ml-2 w-4 h-4 group-hover:translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 7l5 5m0 0l-5 5m5-5H6" />
|
|
||||||
</svg>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Additional Links */}
|
|
||||||
<div className="text-center space-y-3">
|
|
||||||
<div className="text-sm">
|
|
||||||
<a href="#" className="text-info hover:text-info-dark font-medium transition-colors duration-200">
|
|
||||||
¿Olvidaste tu contraseña?
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div className="border-t border-gray-200 pt-4">
|
|
||||||
<Link
|
|
||||||
to="/"
|
|
||||||
className="inline-flex items-center text-info hover:text-info-dark text-sm font-medium group transition-colors duration-200"
|
|
||||||
>
|
|
||||||
<svg className="mr-2 w-4 h-4 group-hover:-translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11 17l-5-5m0 0l5-5m-5 5h12" />
|
|
||||||
</svg>
|
|
||||||
Volver al inicio
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Footer */}
|
|
||||||
<div className="bg-light-gray px-8 py-4 border-t border-gray-200">
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-xs text-text-secondary">
|
|
||||||
Desarrollado por <span className="font-semibold text-navy">@AduanaSoft</span>
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-text-secondary mt-1">
|
|
||||||
Solución especializada para Agentes Aduanales
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Floating elements with new colors */}
|
|
||||||
<div className="absolute -top-4 -left-4 w-24 h-24 bg-navy/10 rounded-full blur-xl"></div>
|
|
||||||
<div className="absolute -bottom-4 -right-4 w-32 h-32 bg-info/20 rounded-full blur-xl"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
<div className="relative max-w-md w-full">
|
|
||||||
{/* Main Card */}
|
|
||||||
<div className="bg-white/95 backdrop-blur-md rounded-3xl shadow-2xl border border-white/20 overflow-hidden">
|
|
||||||
{/* Header with gradient */}
|
|
||||||
<div className="bg-gradient-to-r from-indigo-600 to-purple-600 px-8 py-10 text-center">
|
|
||||||
<div className="mb-4">
|
|
||||||
<Link to="/" className="inline-block">
|
|
||||||
<h1 className="text-4xl font-bold text-white">
|
|
||||||
EFC
|
|
||||||
</h1>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<h2 className="text-2xl font-bold text-white mb-2">
|
|
||||||
Bienvenido de vuelta
|
|
||||||
</h2>
|
|
||||||
<p className="text-indigo-100 text-sm">
|
|
||||||
Inicia sesión para acceder a tu plataforma aduanal
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Form */}
|
|
||||||
<div className="px-8 py-8">
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
|
||||||
{/* Username Field */}
|
|
||||||
<div>
|
|
||||||
<label htmlFor="username" className="block text-sm font-semibold text-gray-700 mb-2">
|
|
||||||
Usuario
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
||||||
<svg className="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
id="username"
|
|
||||||
name="username"
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
className="block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-xl text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition duration-200 hover:border-indigo-300"
|
|
||||||
placeholder="Ingresa tu usuario"
|
|
||||||
value={username}
|
|
||||||
onChange={e => setUsername(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Password Field */}
|
|
||||||
<div>
|
|
||||||
<label htmlFor="password" className="block text-sm font-semibold text-gray-700 mb-2">
|
|
||||||
Contraseña
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
||||||
<svg className="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
type={showPassword ? "text" : "password"}
|
|
||||||
required
|
|
||||||
className="block w-full pl-10 pr-12 py-3 border border-gray-300 rounded-xl text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition duration-200 hover:border-indigo-300"
|
|
||||||
placeholder="Ingresa tu contraseña"
|
|
||||||
value={password}
|
|
||||||
onChange={e => setPassword(e.target.value)}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
|
||||||
onClick={() => setShowPassword(!showPassword)}
|
|
||||||
>
|
|
||||||
{showPassword ? (
|
|
||||||
<svg className="h-5 w-5 text-gray-400 hover:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L8.464 8.464m1.414 1.414L12 12m0 0l2.122 2.122m0 0l1.414 1.414" />
|
|
||||||
</svg>
|
|
||||||
) : (
|
|
||||||
<svg className="h-5 w-5 text-gray-400 hover:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Error Message */}
|
|
||||||
{error && (
|
|
||||||
<div className="rounded-xl bg-red-50 border border-red-200 p-4 animate-pulse">
|
|
||||||
<div className="flex">
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div className="ml-3">
|
|
||||||
<h3 className="text-sm font-medium text-red-800">
|
|
||||||
{error}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Login Button */}
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={loading}
|
|
||||||
className="group relative w-full flex justify-center py-3 px-4 border border-transparent text-sm font-semibold rounded-xl text-white bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-700 hover:to-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-300 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5"
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<>
|
|
||||||
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" fill="none" viewBox="0 0 24 24">
|
|
||||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
||||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
||||||
</svg>
|
|
||||||
Ingresando...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<span>Ingresar</span>
|
|
||||||
<svg className="ml-2 w-4 h-4 group-hover:translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 7l5 5m0 0l-5 5m5-5H6" />
|
|
||||||
</svg>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Additional Links */}
|
|
||||||
<div className="text-center space-y-3">
|
|
||||||
<div className="text-sm">
|
|
||||||
<a href="#" className="text-info hover:text-info-dark font-medium transition-colors duration-200">
|
|
||||||
¿Olvidaste tu contraseña?
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div className="border-t border-gray-200 pt-4">
|
|
||||||
<Link
|
|
||||||
to="/"
|
|
||||||
className="inline-flex items-center text-info hover:text-info-dark text-sm font-medium group transition-colors duration-200"
|
|
||||||
>
|
|
||||||
<svg className="mr-2 w-4 h-4 group-hover:-translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11 17l-5-5m0 0l5-5m-5 5h12" />
|
|
||||||
</svg>
|
|
||||||
Volver al inicio
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Footer */}
|
|
||||||
<div className="bg-light-gray px-8 py-4 border-t border-gray-200">
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-xs text-text-secondary">
|
|
||||||
Desarrollado por <span className="font-semibold text-navy">@AduanaSoft</span>
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-text-secondary mt-1">
|
|
||||||
Solución especializada para Agentes Aduanales
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Floating elements */}
|
|
||||||
<div className="absolute -top-4 -left-4 w-24 h-24 bg-white/10 rounded-full blur-xl"></div>
|
|
||||||
<div className="absolute -bottom-4 -right-4 w-32 h-32 bg-navy/20 rounded-full blur-xl"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,286 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { login } from '../api/auth';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import { colors } from '../theme';
|
|
||||||
|
|
||||||
export default function Login() {
|
|
||||||
const [username, setUsername] = useState('');
|
|
||||||
const [password, setPassword] = useState('');
|
|
||||||
const [error, setError] = useState('');
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setError('');
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const data = await login(username, password);
|
|
||||||
localStorage.setItem('access', data.access);
|
|
||||||
localStorage.setItem('refresh', data.refresh);
|
|
||||||
|
|
||||||
// Disparar evento personalizado para que el navbar se actualice
|
|
||||||
window.dispatchEvent(new CustomEvent('authStateChanged'));
|
|
||||||
|
|
||||||
// Redirigir al dashboard
|
|
||||||
window.location.href = '/admin';
|
|
||||||
} catch (err) {
|
|
||||||
setError('Usuario o contraseña incorrectos');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#F2F4F7' }}>
|
|
||||||
{/* Background pattern */}
|
|
||||||
<div className="absolute inset-0 opacity-20">
|
|
||||||
<div className="absolute inset-0" style={{
|
|
||||||
backgroundImage: `url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fillRule='evenodd'%3E%3Cg fill='%231B2A41' fillOpacity='0.1'%3E%3Ccircle cx='30' cy='30' r='2'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")`
|
|
||||||
}}></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative max-w-md w-full">
|
|
||||||
{/* Main Card */}
|
|
||||||
<div className="bg-white backdrop-blur-md rounded-3xl shadow-2xl border border-gray-200 overflow-hidden">
|
|
||||||
{/* Header with navy background */}
|
|
||||||
<div className="px-8 py-10 text-center" style={{ backgroundColor: '#1B2A41' }}>
|
|
||||||
<div className="mb-4">
|
|
||||||
<Link to="/" className="inline-block">
|
|
||||||
<h1 className="text-4xl font-bold text-white">
|
|
||||||
EFC
|
|
||||||
</h1>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<h2 className="text-2xl font-bold text-white mb-2">
|
|
||||||
Bienvenido de vuelta
|
|
||||||
</h2>
|
|
||||||
<p className="text-sm" style={{ color: 'rgba(255, 255, 255, 0.8)' }}>
|
|
||||||
Inicia sesión para acceder a tu plataforma aduanal
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Form */}
|
|
||||||
<div className="px-8 py-8">
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
|
||||||
{/* Username Field */}
|
|
||||||
<div>
|
|
||||||
<label htmlFor="username" className="block text-sm font-semibold mb-2" style={{ color: '#333333' }}>
|
|
||||||
Usuario
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
||||||
<svg className="h-5 w-5" style={{ color: '#7A7A7A' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
id="username"
|
|
||||||
name="username"
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
className="block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:border-transparent transition duration-200"
|
|
||||||
style={{
|
|
||||||
color: '#333333',
|
|
||||||
borderColor: '#d1d5db',
|
|
||||||
':focus': {
|
|
||||||
ringColor: '#4DA6FF',
|
|
||||||
borderColor: 'transparent'
|
|
||||||
},
|
|
||||||
':hover': {
|
|
||||||
borderColor: '#4DA6FF'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder="Ingresa tu usuario"
|
|
||||||
value={username}
|
|
||||||
onChange={e => setUsername(e.target.value)}
|
|
||||||
onFocus={(e) => {
|
|
||||||
e.target.style.borderColor = 'transparent';
|
|
||||||
e.target.style.boxShadow = '0 0 0 2px #4DA6FF';
|
|
||||||
}}
|
|
||||||
onBlur={(e) => {
|
|
||||||
e.target.style.borderColor = '#d1d5db';
|
|
||||||
e.target.style.boxShadow = 'none';
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => {
|
|
||||||
if (document.activeElement !== e.target) {
|
|
||||||
e.target.style.borderColor = '#4DA6FF';
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
if (document.activeElement !== e.target) {
|
|
||||||
e.target.style.borderColor = '#d1d5db';
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Password Field */}
|
|
||||||
<div>
|
|
||||||
<label htmlFor="password" className="block text-sm font-semibold mb-2" style={{ color: '#333333' }}>
|
|
||||||
Contraseña
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
||||||
<svg className="h-5 w-5" style={{ color: '#7A7A7A' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
type={showPassword ? "text" : "password"}
|
|
||||||
required
|
|
||||||
className="block w-full pl-10 pr-12 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:border-transparent transition duration-200"
|
|
||||||
style={{
|
|
||||||
color: '#333333',
|
|
||||||
borderColor: '#d1d5db'
|
|
||||||
}}
|
|
||||||
placeholder="Ingresa tu contraseña"
|
|
||||||
value={password}
|
|
||||||
onChange={e => setPassword(e.target.value)}
|
|
||||||
onFocus={(e) => {
|
|
||||||
e.target.style.borderColor = 'transparent';
|
|
||||||
e.target.style.boxShadow = '0 0 0 2px #4DA6FF';
|
|
||||||
}}
|
|
||||||
onBlur={(e) => {
|
|
||||||
e.target.style.borderColor = '#d1d5db';
|
|
||||||
e.target.style.boxShadow = 'none';
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => {
|
|
||||||
if (document.activeElement !== e.target) {
|
|
||||||
e.target.style.borderColor = '#4DA6FF';
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
if (document.activeElement !== e.target) {
|
|
||||||
e.target.style.borderColor = '#d1d5db';
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="absolute inset-y-0 right-0 pr-3 flex items-center transition-colors duration-200"
|
|
||||||
onClick={() => setShowPassword(!showPassword)}
|
|
||||||
>
|
|
||||||
{showPassword ? (
|
|
||||||
<svg className="h-5 w-5 hover:opacity-70" style={{ color: '#7A7A7A' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L8.464 8.464m1.414 1.414L12 12m0 0l2.122 2.122m0 0l1.414 1.414" />
|
|
||||||
</svg>
|
|
||||||
) : (
|
|
||||||
<svg className="h-5 w-5 hover:opacity-70" style={{ color: '#7A7A7A' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Error Message */}
|
|
||||||
{error && (
|
|
||||||
<div className="rounded-xl bg-red-50 border p-4 animate-pulse" style={{ borderColor: 'rgba(198, 40, 40, 0.2)' }}>
|
|
||||||
<div className="flex">
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<svg className="h-5 w-5" style={{ color: '#C62828' }} viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div className="ml-3">
|
|
||||||
<h3 className="text-sm font-medium" style={{ color: '#C62828' }}>
|
|
||||||
{error}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Login Button */}
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={loading}
|
|
||||||
className="group relative w-full flex justify-center py-3 px-4 border border-transparent text-sm font-semibold rounded-xl text-white focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-300 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5"
|
|
||||||
style={{
|
|
||||||
backgroundColor: '#1B2A41',
|
|
||||||
'--tw-ring-color': '#1B2A41'
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => {
|
|
||||||
if (!loading) {
|
|
||||||
e.target.style.backgroundColor = '#162234';
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
if (!loading) {
|
|
||||||
e.target.style.backgroundColor = '#1B2A41';
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<>
|
|
||||||
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" fill="none" viewBox="0 0 24 24">
|
|
||||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
||||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
||||||
</svg>
|
|
||||||
Ingresando...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<span>Ingresar</span>
|
|
||||||
<svg className="ml-2 w-4 h-4 group-hover:translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 7l5 5m0 0l-5 5m5-5H6" />
|
|
||||||
</svg>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Additional Links */}
|
|
||||||
<div className="text-center space-y-3">
|
|
||||||
<div className="text-sm">
|
|
||||||
<a href="#" className="font-medium transition-colors duration-200" style={{ color: '#4DA6FF' }}
|
|
||||||
onMouseEnter={(e) => e.target.style.color = '#1976D2'}
|
|
||||||
onMouseLeave={(e) => e.target.style.color = '#4DA6FF'}
|
|
||||||
>
|
|
||||||
¿Olvidaste tu contraseña?
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div className="border-t border-gray-200 pt-4">
|
|
||||||
<Link
|
|
||||||
to="/"
|
|
||||||
className="inline-flex items-center text-sm font-medium group transition-colors duration-200"
|
|
||||||
style={{ color: '#4DA6FF' }}
|
|
||||||
onMouseEnter={(e) => e.target.style.color = '#1976D2'}
|
|
||||||
onMouseLeave={(e) => e.target.style.color = '#4DA6FF'}
|
|
||||||
>
|
|
||||||
<svg className="mr-2 w-4 h-4 group-hover:-translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11 17l-5-5m0 0l5-5m-5 5h12" />
|
|
||||||
</svg>
|
|
||||||
Volver al inicio
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Footer */}
|
|
||||||
<div className="px-8 py-4 border-t border-gray-200" style={{ backgroundColor: '#F2F4F7' }}>
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-xs" style={{ color: '#7A7A7A' }}>
|
|
||||||
Desarrollado por <span className="font-semibold" style={{ color: '#1B2A41' }}>@AduanaSoft</span>
|
|
||||||
</p>
|
|
||||||
<p className="text-xs mt-1" style={{ color: '#7A7A7A' }}>
|
|
||||||
Solución especializada para Agentes Aduanales
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Floating elements */}
|
|
||||||
<div className="absolute -top-4 -left-4 w-24 h-24 rounded-full blur-xl" style={{ backgroundColor: 'rgba(255, 255, 255, 0.1)' }}></div>
|
|
||||||
<div className="absolute -bottom-4 -right-4 w-32 h-32 rounded-full blur-xl" style={{ backgroundColor: 'rgba(27, 42, 65, 0.2)' }}></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,110 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { fetchNotificaciones, fetchAllNotifications, marcarNotificacionComoVista } from '../api/notificaciones';
|
import { fetchNotificaciones, fetchAllNotifications, marcarNotificacionComoVista } from '../api/notificaciones';
|
||||||
|
|
||||||
|
// Función para obtener el icono apropiado según el tipo de notificación
|
||||||
|
const getNotificationIcon = (tipo) => {
|
||||||
|
const iconProps = "w-6 h-6";
|
||||||
|
|
||||||
|
switch (tipo) {
|
||||||
|
case 'success':
|
||||||
|
case 'exito':
|
||||||
|
return (
|
||||||
|
<div className="p-2 bg-green-100 rounded-full">
|
||||||
|
<svg className={`${iconProps} text-green-600`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case 'error':
|
||||||
|
case 'danger':
|
||||||
|
return (
|
||||||
|
<div className="p-2 bg-red-100 rounded-full">
|
||||||
|
<svg className={`${iconProps} text-red-600`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4m0 4h.01" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case 'warning':
|
||||||
|
case 'advertencia':
|
||||||
|
return (
|
||||||
|
<div className="p-2 bg-yellow-100 rounded-full">
|
||||||
|
<svg className={`${iconProps} text-yellow-600`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.728-.833-2.5 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case 'info':
|
||||||
|
case 'informacion':
|
||||||
|
return (
|
||||||
|
<div className="p-2 bg-blue-100 rounded-full">
|
||||||
|
<svg className={`${iconProps} text-blue-600`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case 'documento':
|
||||||
|
case 'document':
|
||||||
|
return (
|
||||||
|
<div className="p-2 bg-purple-100 rounded-full">
|
||||||
|
<svg className={`${iconProps} text-purple-600`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case 'proceso':
|
||||||
|
case 'process':
|
||||||
|
return (
|
||||||
|
<div className="p-2 bg-indigo-100 rounded-full">
|
||||||
|
<svg className={`${iconProps} text-indigo-600`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<div className="p-2 bg-gray-100 rounded-full">
|
||||||
|
<svg className={`${iconProps} text-gray-600`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 17h5l-5 5v-5zM4 4h5l-5 5v-5z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Función para formatear timestamps
|
||||||
|
const formatTimestamp = (timestamp) => {
|
||||||
|
if (!timestamp) return 'Fecha no disponible';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
const now = new Date();
|
||||||
|
const diffInMinutes = Math.floor((now - date) / (1000 * 60));
|
||||||
|
const diffInHours = Math.floor(diffInMinutes / 60);
|
||||||
|
const diffInDays = Math.floor(diffInHours / 24);
|
||||||
|
|
||||||
|
if (diffInMinutes < 1) {
|
||||||
|
return 'Ahora mismo';
|
||||||
|
} else if (diffInMinutes < 60) {
|
||||||
|
return `Hace ${diffInMinutes} min`;
|
||||||
|
} else if (diffInHours < 24) {
|
||||||
|
return `Hace ${diffInHours} h`;
|
||||||
|
} else if (diffInDays < 7) {
|
||||||
|
return `Hace ${diffInDays} días`;
|
||||||
|
} else {
|
||||||
|
return date.toLocaleDateString('es-ES', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return 'Fecha inválida';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default function Notificaciones() {
|
export default function Notificaciones() {
|
||||||
const [notificaciones, setNotificaciones] = useState([]);
|
const [notificaciones, setNotificaciones] = useState([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -57,102 +161,262 @@ export default function Notificaciones() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 max-w-5xl mx-auto">
|
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-blue-50 p-4 lg:p-6">
|
||||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between mb-4 gap-2">
|
<div className="max-w-7xl mx-auto">
|
||||||
<h1 className="text-2xl font-bold">Notificaciones</h1>
|
{/* Header moderno */}
|
||||||
<div className="flex gap-2 items-center">
|
<div className="bg-white/80 backdrop-blur-xl rounded-2xl shadow-xl border border-blue-100/50 p-6 mb-6">
|
||||||
|
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<div className="p-3 bg-gradient-to-br from-blue-500 to-blue-700 rounded-xl shadow-lg">
|
||||||
|
<svg className="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 17h5l-5 5v-5zM4 4h5l-5 5v-5z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900">Notificaciones</h1>
|
||||||
|
<p className="text-gray-600 mt-1">
|
||||||
|
{count > 0 ? `${count} notificaciones encontradas` : 'No hay notificaciones'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row gap-3">
|
||||||
|
{/* Filtro mejorado */}
|
||||||
|
<div className="relative">
|
||||||
<select
|
<select
|
||||||
className="border rounded px-2 py-1"
|
className="appearance-none bg-white border border-gray-300 rounded-xl px-4 py-3 pr-10 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 shadow-sm hover:shadow-md"
|
||||||
value={filtroVisto}
|
value={filtroVisto}
|
||||||
onChange={handleFiltroChange}
|
onChange={handleFiltroChange}
|
||||||
>
|
>
|
||||||
<option value="todos">Todas</option>
|
<option value="todos">📋 Todas las notificaciones</option>
|
||||||
<option value="visto">Vistas</option>
|
<option value="visto">✅ Solo vistas</option>
|
||||||
<option value="novisto">No vistas</option>
|
<option value="novisto">🔴 Solo no vistas</option>
|
||||||
</select>
|
</select>
|
||||||
|
<div className="absolute inset-y-0 right-0 flex items-center px-2 pointer-events-none">
|
||||||
|
<svg className="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Botón de acción mejorado */}
|
||||||
<button
|
<button
|
||||||
onClick={handleActualizarTodas}
|
onClick={handleActualizarTodas}
|
||||||
className="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded shadow"
|
disabled={loading || notificaciones.filter(n => !n.visto).length === 0}
|
||||||
disabled={loading}
|
className="flex items-center space-x-2 bg-gradient-to-r from-blue-500 to-blue-700 hover:from-blue-600 hover:to-blue-800 disabled:from-gray-400 disabled:to-gray-500 text-white px-6 py-3 rounded-xl font-medium transition-all duration-200 shadow-lg hover:shadow-xl disabled:cursor-not-allowed disabled:opacity-60 transform hover:scale-105"
|
||||||
>
|
>
|
||||||
Actualizar todas como leídas
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<svg className="animate-spin h-5 w-5" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
<span>Procesando...</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
<span>Marcar como leídas</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Contenido principal */}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="text-blue-500 py-8 text-center">Cargando...</div>
|
<div className="bg-white/80 backdrop-blur-xl rounded-2xl shadow-xl border border-blue-100/50 p-12 text-center">
|
||||||
|
<div className="inline-flex items-center space-x-3 text-blue-600">
|
||||||
|
<svg className="animate-spin h-8 w-8" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
<span className="text-xl font-semibold">Cargando notificaciones...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<div className="text-red-500 py-8 text-center">{error}</div>
|
<div className="bg-red-50/80 backdrop-blur-xl rounded-2xl shadow-xl border border-red-200/50 p-12 text-center">
|
||||||
|
<div className="inline-flex items-center space-x-3 text-red-600">
|
||||||
|
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<span className="text-xl font-semibold">{error}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Vista Desktop - Tabla */}
|
||||||
|
<div className="hidden lg:block bg-white/80 backdrop-blur-xl rounded-2xl shadow-xl border border-blue-100/50 overflow-hidden">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="min-w-full divide-y divide-gray-200 rounded-lg overflow-hidden">
|
<table className="min-w-full divide-y divide-gray-200">
|
||||||
<thead className="bg-gradient-to-r from-gray-50 sticky top-0 z-20">
|
<thead className="bg-gradient-to-r from-blue-500 to-blue-700">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-6 py-3 text-center text-xs font-bold text-blue-700 uppercase tracking-wider border-b border-gray-200">ID</th>
|
<th className="px-6 py-4 text-left text-xs font-bold text-white uppercase tracking-wider">ID</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider border-b border-gray-200">Tipo</th>
|
<th className="px-6 py-4 text-left text-xs font-bold text-white uppercase tracking-wider">Tipo</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider border-b border-gray-200">Mensaje</th>
|
<th className="px-6 py-4 text-left text-xs font-bold text-white uppercase tracking-wider">Mensaje</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider border-b border-gray-200">Fecha</th>
|
<th className="px-6 py-4 text-left text-xs font-bold text-white uppercase tracking-wider">Fecha</th>
|
||||||
<th className="px-6 py-3 text-center text-xs font-bold text-blue-700 uppercase tracking-wider border-b border-gray-200">Visto</th>
|
<th className="px-6 py-4 text-center text-xs font-bold text-white uppercase tracking-wider">Estado</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white divide-y divide-gray-100" style={{ position: 'relative', minHeight: 'calc(8 * 56px)' }}>
|
<tbody className="bg-white/50 divide-y divide-gray-100">
|
||||||
{Array.from({ length: pageSize }).map((_, idx) => {
|
{notificaciones.map((n, idx) => (
|
||||||
const n = notificaciones[idx];
|
<tr key={n.id} className={`transition-all duration-200 hover:bg-blue-50 hover:shadow-md ${!n.visto ? 'bg-blue-50/70 border-l-4 border-blue-500' : ''}`}>
|
||||||
if (n) {
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||||
return (
|
#{n.id}
|
||||||
<tr key={n.id} className={`transition-all duration-200 hover:bg-blue-100 hover:shadow-lg ${n.visto ? '' : 'bg-blue-50'}`}>
|
</td>
|
||||||
<td className="px-6 py-4 text-center align-middle">{n.id}</td>
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<td className="px-6 py-4 whitespace-nowrap align-middle font-medium text-blue-900">{n.tipo?.descripcion || n.tipo?.tipo}</td>
|
<div className="flex items-center">
|
||||||
<td className="px-6 py-4 whitespace-nowrap align-middle text-gray-800">{n.mensaje}</td>
|
{getNotificationIcon(n.tipo?.tipo)}
|
||||||
<td className="px-6 py-4 whitespace-nowrap align-middle text-gray-700">{new Date(n.fecha_envio || n.created_at).toLocaleString()}</td>
|
<span className="ml-3 text-sm font-medium text-gray-900">
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-center align-middle">
|
{n.tipo?.descripcion || n.tipo?.tipo || 'Notificación'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-sm text-gray-700 max-w-md truncate">
|
||||||
|
{n.mensaje}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
{formatTimestamp(n.fecha_envio || n.created_at)}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-center">
|
||||||
{n.visto ? (
|
{n.visto ? (
|
||||||
<span className="text-green-600 font-semibold">Sí</span>
|
<span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||||
|
<svg className="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4" />
|
||||||
|
</svg>
|
||||||
|
Leída
|
||||||
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-red-600 font-semibold">No</span>
|
<span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800">
|
||||||
|
<svg className="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4m0 4h.01" />
|
||||||
|
</svg>
|
||||||
|
Nueva
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
))}
|
||||||
} else {
|
{/* Filas vacías para mantener altura consistente */}
|
||||||
return (
|
{Array.from({ length: Math.max(0, pageSize - notificaciones.length) }).map((_, idx) => (
|
||||||
<tr key={"empty-" + idx}>
|
<tr key={`empty-${idx}`} className="h-16">
|
||||||
<td className="px-6 py-4 text-center text-gray-300">-</td>
|
<td colSpan="5" className="px-6 py-4 text-center text-gray-300">-</td>
|
||||||
<td className="px-6 py-4 text-gray-300">-</td>
|
|
||||||
<td className="px-6 py-4 text-gray-300">-</td>
|
|
||||||
<td className="px-6 py-4 text-gray-300">-</td>
|
|
||||||
<td className="px-6 py-4 text-center text-gray-300">-</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
);
|
))}
|
||||||
}
|
|
||||||
})}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
{/* Paginación */}
|
|
||||||
<div className="flex justify-between items-center mt-4">
|
|
||||||
<div>
|
|
||||||
Página {page} de {Math.ceil(count / pageSize) || 1}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 items-center">
|
|
||||||
|
{/* Vista Mobile - Cards */}
|
||||||
|
<div className="lg:hidden space-y-4">
|
||||||
|
{notificaciones.length === 0 ? (
|
||||||
|
<div className="bg-white/80 backdrop-blur-xl rounded-2xl shadow-xl border border-blue-100/50 p-12 text-center">
|
||||||
|
<div className="mx-auto w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mb-4">
|
||||||
|
<svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 17h5l-5 5v-5zM13 3l-4 9h6l-4 9" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-1">No hay notificaciones</h3>
|
||||||
|
<p className="text-gray-500">No se encontraron notificaciones con los filtros aplicados.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
notificaciones.map((n, idx) => (
|
||||||
|
<div
|
||||||
|
key={n.id}
|
||||||
|
className={`bg-white/80 backdrop-blur-xl rounded-2xl shadow-xl border transition-all duration-200 hover:shadow-2xl transform hover:scale-[1.02] ${
|
||||||
|
!n.visto ? 'border-blue-300 bg-blue-50/80' : 'border-gray-200/50'
|
||||||
|
}`}
|
||||||
|
style={{ animationDelay: `${idx * 100}ms` }}
|
||||||
|
>
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex items-start space-x-4">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
{getNotificationIcon(n.tipo?.tipo)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<p className="text-sm font-semibold text-gray-900 truncate">
|
||||||
|
{n.tipo?.descripcion || n.tipo?.tipo || 'Notificación'}
|
||||||
|
</p>
|
||||||
|
{n.visto ? (
|
||||||
|
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800 ml-2">
|
||||||
|
<svg className="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4" />
|
||||||
|
</svg>
|
||||||
|
Leída
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800 ml-2">
|
||||||
|
<svg className="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4m0 4h.01" />
|
||||||
|
</svg>
|
||||||
|
Nueva
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-700 mb-3 leading-relaxed">{n.mensaje}</p>
|
||||||
|
<div className="flex items-center justify-between text-xs text-gray-500">
|
||||||
|
<span className="flex items-center">
|
||||||
|
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
|
||||||
|
</svg>
|
||||||
|
ID: #{n.id}
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center">
|
||||||
|
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
{formatTimestamp(n.fecha_envio || n.created_at)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Paginación mejorada */}
|
||||||
|
{!loading && !error && count > 0 && (
|
||||||
|
<div className="bg-white/80 backdrop-blur-xl rounded-2xl shadow-xl border border-blue-100/50 p-6 mt-6">
|
||||||
|
<div className="flex flex-col sm:flex-row justify-between items-center gap-4">
|
||||||
|
<div className="text-sm text-gray-700">
|
||||||
|
<span className="font-medium">Página {page}</span> de <span className="font-medium">{Math.ceil(count / pageSize) || 1}</span>
|
||||||
|
<span className="text-gray-500 ml-2">• {count} notificaciones totales</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||||
disabled={page === 1}
|
disabled={page === 1}
|
||||||
className="px-3 py-1 rounded bg-gray-200 hover:bg-gray-300 disabled:opacity-50"
|
className="flex items-center space-x-2 px-4 py-2 rounded-xl bg-gray-100 hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 text-sm font-medium"
|
||||||
>
|
>
|
||||||
Anterior
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
<span>Anterior</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setPage((p) => (p * pageSize < count ? p + 1 : p))}
|
onClick={() => setPage((p) => (p * pageSize < count ? p + 1 : p))}
|
||||||
disabled={page * pageSize >= count}
|
disabled={page * pageSize >= count}
|
||||||
className="px-3 py-1 rounded bg-gray-200 hover:bg-gray-300 disabled:opacity-50"
|
className="flex items-center space-x-2 px-4 py-2 rounded-xl bg-blue-500 hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed text-white transition-all duration-200 text-sm font-medium"
|
||||||
>
|
>
|
||||||
Siguiente
|
<span>Siguiente</span>
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<span className="ml-2 text-sm text-gray-500">15 por página</span>
|
<span className="text-sm text-gray-500">15 por página</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user