a
    h}                     @   s  d dl mZmZmZmZmZmZmZ d dlm	Z	 d dl
mZ d dlmZmZ d dlmZ d dlZd dlZd dlZd dlZd dlmZmZ edZeeZd	e_d
ejd< dejd< ee ejeje Z!ej"e!dZ#ej$e#dd dej"e#d ejd< dejd< e	eZ%G dd de%j&Z'G dd de%j&Z(e)  e%*  W d   n1 sb0    Y  ddiZ+dd Z,dd Z-d d! Z.ej/d"d#gd$d%d& Z0ej/d'd(gd$d)d* Z1ej/d+d(gd$d,d- Z2ej/d.d#gd$d/d0 Z3ej/d1d#gd$d2d3 Z4ej/d4d#gd$e.d5d6 Z5ej/d7d#gd$d8d9 Z6dZ7dZ8d:d; Z9ej/d<d#d(gd$d=d> Z:e/d?d@dA Z;e/dBe9dCdD Z<dTdEdFZ=e/dGe9dHdI Z>ej/dJd(gd$e9dKdL Z?dMZ@dNZAdOZBedPkrejCddQdRdS dS )U    )Flaskrequestjsonifyrender_template_stringsessionredirecturl_for)
SQLAlchemy)CORS)datetime	timedeltawrapsN)generar_html_emailenviar_email_smtpzEurope/Madridz$lotify-secret-key-2025-backup-systemz$lotify-jwt-secret-key-2025-api-tokenJWT_SECRET_KEY   JWT_EXPIRATION_HOURSdataT)exist_okz
sqlite:///zbackup_peticiones.dbZSQLALCHEMY_DATABASE_URIFZSQLALCHEMY_TRACK_MODIFICATIONSc                   @   sn  e Zd ZdZejejddZejejddd dZ	ejej
ddZejej
ddZejej
ddZejej
ddZejej
ddZejej
ddZejej
ddZejej
ddZejej
ddZejej
ddZejej
ddZejej
ddZejej
ddZejej
ddZejej
ddZejej
ddZejej
ddZejej
ddZejej
ddZd	d
 ZdS )PeticionBackupZpeticiones_backupTZprimary_keyFc                   C   s
   t tS Nr   nowTIMEZONE_MADRID r   r   /var/www/html/requester/app.py<lambda>$       zPeticionBackup.<lambda>nullabledefaultr!   c                 C   s^   | j | j | j| j| j| j| j| j| j	| j
| j| j| j| j| j| j| j| j| j| j| jdS )N)iddatetime_registroempresaempleadotelefonoemailcodigo_lote	tipo_loteentrega	direccionfranja_horaria_desdefranja_horaria_hastadesglose_direcciondesglose_detalle_direcciondesglose_cpdesglose_poblaciondesglose_provinciaidentificador_clienteobservacionessociedad
delegacion)r$   r%   	isoformatr&   r'   r(   r)   r*   r+   r,   r-   r.   r/   r0   r1   r2   r3   r4   r5   r6   r7   r8   )selfr   r   r   to_dict;   s,    zPeticionBackup.to_dictN)__name__
__module____qualname____tablename__dbColumnIntegerr$   DateTimer%   Textr&   r'   r(   r)   r*   r+   r,   r-   r.   r/   r0   r1   r2   r3   r4   r5   r6   r7   r8   r;   r   r   r   r   r       s.   r   c                   @   s   e Zd ZdZejejddZejejedddZ	ejej
ddZejejddd d	Zejej
ddZejej
ddZejd
ddZdS )
EmailEnvioZemail_enviosTr   zpeticiones_backup.idFr#   c                   C   s
   t tS r   r   r   r   r   r   r   [   r   zEmailEnvio.<lambda>r    r   Zenvios_email)ZbackrefN)r<   r=   r>   r?   r@   rA   rB   r$   Z
ForeignKeypeticion_idrD   email_destinorC   fecha_envioestadomensaje_errorZrelationshippeticionr   r   r   r   rE   U   s   rE   ZlotifyzLot1fyrequest3r$c                 C   s>   | t  ttjd d t  d}tj|tjd dd}|S )z-
    Genera un JWT token para el usuario
    r   )hours)usernameexpZiatr   HS256)	algorithm)r   utcnowr   appconfigjwtencode)rM   payloadtokenr   r   r   generar_tokenj   s    rX   c                 C   sN   zt j| tjd dgd}|W S  t jy4   Y dS  t jyH   Y dS 0 dS )z,
    Verifica y decodifica un JWT token
    r   rO   )Z
algorithmsN)rT   decoderR   rS   ZExpiredSignatureErrorZInvalidTokenError)rW   rV   r   r   r   verificar_tokenv   s    rZ   c                    s   t   fdd}|S )z3
    Decorador para proteger endpoints con JWT
    c                     s   d }dt jv rPt jd }z|dd }W n$ tyN   tddddf Y S 0 |sftddddfS t|}|d u rtdd	ddfS  | d
|d i|S )NZAuthorization    Fu/   Formato de token inválido. Use: Bearer <token>successerror  zHToken no proporcionado. Incluya el header: Authorization: Bearer <token>u   Token inválido o expiradocurrent_userrM   )r   headerssplit
IndexErrorr   rZ   )argskwargsrW   Zauth_headerrV   fr   r   	decorated   s8    


z!token_required.<locals>.decoratedr   )rh   ri   r   rg   r   token_required   s     rj   /ZGET)methodsc                   C   s   dS )u   
    Ruta raíz
    )zSilence is gold   r   r   r   r   r   index   s    rn   z/api/auth/loginPOSTc               
   C   s   zt  } | s"tddddfW S | d}| d}|r>|sRtddddfW S |tvsft| |krztdddd	fW S t|}td
|tjd d dddfW S  ty } z(tddt	| ddfW  Y d}~S d}~0 0 dS )a@  
    Endpoint para obtener un token JWT.
    
    Body (JSON):
    {
        "username": "lotify",
        "password": "Lot1fyrequest3r$"
    }
    
    Respuesta exitosa:
    {
        "success": true,
        "token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
        "expires_in": 86400,
        "token_type": "Bearer"
    }
    Fz(Se requiere JSON con username y passwordr]     rM   passwordz Se requieren username y passwordu   Credenciales inválidasr`   Tr   i  ZBearer)r^   rW   Z
expires_in
token_typerm   zError al generar token:   N)
r   get_jsonr   getAPI_CREDENTIALSrX   rR   rS   	Exceptionstr)r   rM   rq   rW   er   r   r   	api_login   sR    

rz   z/backupc                  C   s6  zt jdddpi } t| d| d| d| d| d| d| d	| d
| d| d| d| d| d| d| d| d| d| d| dd}tj| tj  tdd|j	|j
 ddfW S  ty0 } z.tj  tddt|ddfW  Y d}~S d}~0 0 dS )um   
    Endpoint para guardar peticiones de backup.
    Acepta cualquier tipo de dato, nulos, vacíos, etc.
    T)forceZsilentr&   r'   r(   r)   r*   r+   r,   r-   r.   r/   r0   r1   r2   r3   r4   r5   r6   r7   r8   )r&   r'   r(   r)   r*   r+   r,   r-   r.   r/   r0   r1   r2   r3   r4   r5   r6   r7   r8   zBackup guardado correctamente)r^   messager$   r      FzError al guardar backup)r^   r|   r_   rs   N)r   rt   r   ru   r@   r   addcommitr   r$   r%   r9   rw   Zrollbackrx   )r   Znueva_peticionry   r   r   r   guardar_backup   sT    

r   z/backup/<int:id>c                 C   s   t j| }t| dfS )z;
    Endpoint opcional para consultar un backup por ID
    rm   )r   query
get_or_404r   r;   )r$   rK   r   r   r   obtener_backup  s    r   z/backup/listc                  C   sn   t jjddtd} t jjddtd}tjtj j	| |dd}t
|j|j|j|jdd	 |jD d
dfS )u   
    Endpoint opcional para listar todos los backups
    Soporta paginación con parámetros: page (default 1) y per_page (default 50)
    pager\   typeper_page2   Fr   r   Z	error_outc                 S   s   g | ]}|  qS r   r;   .0pr   r   r   
<listcomp>8  r   z"listar_backups.<locals>.<listcomp>)totalr   r   pagesr   rm   )r   re   ru   intr   r   order_byr%   descpaginater   r   r   r   r   items)r   r   
peticionesr   r   r   listar_backups&  s    r   z/api/registrosc                 C   sl  z"t jjddtd}tt jjddtdd}t jdd }tj}t jd	d
 }|rv|	tj
d| d}t jdd
 }|rz@d|v rt|d}nt|d}t|}|	tj|k}W n$ ty   tddidf Y W S 0 t jdd
 }|rzRd|v r$t|d}	nt|d}	|	jdddd}	t|	}	|	tj|	k}W n& ty   tddidf Y W S 0 |dkr|tj }n|tj }|j||dd}
d|
j|
j|
j|
j|
j|
j|r|nd|r|nd|r|nd|ddd |
jD d 	}t|d!fW S  tyf } z(tdd"t | d#d$fW  Y d}~S d}~0 0 dS )%uJ  
    Endpoint para consultar registros con filtros opcionales.
    Requiere autenticación con Bearer Token.
    
    Headers requeridos:
    - Authorization: Bearer <token>
    
    Parámetros de query:
    - empresa: Filtrar por nombre de empresa (búsqueda parcial, case-insensitive)
    - fecha_desde: Filtrar desde fecha (formato: YYYY-MM-DD o YYYY-MM-DD HH:MM:SS)
    - fecha_hasta: Filtrar hasta fecha (formato: YYYY-MM-DD o YYYY-MM-DD HH:MM:SS)
    - page: Número de página (default: 1)
    - per_page: Registros por página (default: 100, max: 1000)
    - order: Orden de resultados 'asc' o 'desc' (default: 'desc')
    
    Ejemplos:
    - /api/registros
    - /api/registros?empresa=Acme
    - /api/registros?fecha_desde=2025-01-01&fecha_hasta=2025-01-31
    - /api/registros?empresa=Acme&fecha_desde=2025-01-01&per_page=50
    r   r\   r   r   d   i  orderr   r&    %fecha_desder[   %Y-%m-%d %H:%M:%Sz%Y-%m-%dr_   uF   Formato de fecha_desde inválido. Use YYYY-MM-DD o YYYY-MM-DD HH:MM:SSrp   fecha_hasta   ;   )hourminuteseconduF   Formato de fecha_hasta inválido. Use YYYY-MM-DD o YYYY-MM-DD HH:MM:SSascFr   TN)r&   r   r   r   c                 S   s   g | ]}|  qS r   r   r   r   r   r   r     r   z'consultar_registros.<locals>.<listcomp>)	r^   r   r   r   r   has_nexthas_prevZfiltros_aplicadosr   rm   zError al consultar registros: r]   rs   )!r   re   ru   r   minlowerr   r   stripfilterr&   iliker   strptimer   Zlocalizer%   
ValueErrorr   replacer   r   r   r   r   r   r   r   r   r   r   rw   rx   )ra   r   r   r   r   r&   r   Zfecha_desde_dtr   Zfecha_hasta_dtr   responsery   r   r   r   consultar_registros;  s~    



r   z/healthc                   C   s   t ddddfS )uC   
    Endpoint para verificar que el servicio está funcionando
    okz+Sistema de backup funcionando correctamente)statusr|   rm   )r   r   r   r   r   health_check  s    r   c                    s   t   fdd}|S )Nc                     s$   t dsttdS  | i |S N	logged_inlogin)r   ru   r   r   )re   rf   rg   r   r   decorated_function  s    
z*login_required.<locals>.decorated_functionr   )rh   r   r   rg   r   login_required  s    r   z/admin/loginc                  C   s^   t jdkrRt jd} t jd}| tkrF|tkrFdtd< ttdS t	t
ddS t	t
ddS )	Nro   rM   rq   Tr   
admin_view)r_   F)r   methodZformru   USUARIO_ADMINCONTRASENA_ADMINr   r   r   r   LOGIN_TEMPLATE)rM   rq   r   r   r   r     s    
r   z/admin/logoutc                   C   s   t dd  ttdS r   )r   popr   r   r   r   r   r   logout  s    r   z/adminc            
      C   s  t jjddtd} d}t jdd}t jdd}i }d	D ](}t jd
| d }|r:|||< q:tj}| D ]*\}}tt|}|	|
d| d}qrtt|rtt|}|dkr|| }q|| }n|tj }|j| |dd}	tt|	|||dS )Nr   r\   r   r   sortr%   r   r   )r&   r'   r(   r)   r*   r+   r,   r-   r3   r4   r5   r7   r8   Zfilter_r   r   r   Fr   )r   sort_by
sort_orderfilters)r   re   ru   r   r   r   r   r   getattrr   r   hasattrr   r   r   r%   r   r   ADMIN_TEMPLATE)
r   r   r   r   r   fieldvaluer   columnr   r   r   r   r     s4    



r   c              
   C   s   d|  }ddd}z4t j||dd}|  | }d|v rD|W S W dS  ty| } ztd	|  W Y d}~dS d}~0 0 dS )
zF
    Obtiene datos completos de la empresa desde la API de Lotify
    z3https://sigan.lotify.es/lotify_api/plataforma/data/Z	requesterpass)Zoirausu
access_key
   )paramstimeoutZ
plataformaNz"Error al obtener datos de la API: )requestsru   Zraise_for_statusZjsonrw   print)r&   Zidentificadorr   urlr   r   r   ry   r   r   r   api_datos_empresa_completo   s    
r   z/admin/email/<int:id>c                 C   sD   t j| }tjj| dtj  }t	|t
}tt|||dS )N)rF   )rK   
html_email	historial)r   r   r   rE   Z	filter_byr   rH   r   allr   r   r   EMAIL_VIEWER_TEMPLATE)r$   rK   r   r   r   r   r   	ver_email  s    
r   z/admin/email/<int:id>/enviarc                 C   s   t j| }t }|d|j}|s8tddddfS t|t	}t
|d|\}}t| ||r`dnd|d	}tj| tj  |rtd
d| |j|j|jd|jdddfS tdd| ddfS d S )Nr)   FzEmail de destino requerido)r^   r|   rp   zReserva Lotify recibidaZexitosor_   )rF   rG   rI   rJ   TzEmail enviado correctamente a r   )r$   r)   ZfecharI   )r^   r|   enviorm   zError al enviar email: rs   )r   r   r   r   rt   ru   r)   r   r   r   r   rE   r@   r   r~   r   r$   rG   rH   strftimerI   )r$   rK   r   rG   Zhtml_contentZexitor_   r   r   r   r   enviar_email$  s8    



	r   u:(  
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Ver Email - Lotify Backup</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            background: #f5f5f5;
            display: flex;
            height: 100vh;
            overflow: hidden;
        }
        .email-content {
            flex: 1;
            overflow-y: auto;
            background: white;
        }
        .sidebar {
            width: 300px;
            background: white;
            border-left: 1px solid #ddd;
            display: flex;
            flex-direction: column;
            overflow-y: auto;
        }
        .sidebar-header {
            padding: 20px;
            border-bottom: 1px solid #ddd;
            background: #667eea;
            color: white;
        }
        .sidebar-header h2 {
            font-size: 18px;
            margin-bottom: 10px;
        }
        .btn-enviar {
            width: 100%;
            padding: 12px;
            background: #28a745;
            color: white;
            border: none;
            border-radius: 6px;
            font-size: 14px;
            font-weight: 600;
            cursor: pointer;
            transition: background 0.3s;
        }
        .btn-enviar:hover {
            background: #218838;
        }
        .historial {
            padding: 20px;
            flex: 1;
        }
        .historial h3 {
            font-size: 16px;
            margin-bottom: 15px;
            color: #333;
        }
        .historial-item {
            padding: 12px;
            background: #f8f9fa;
            border-radius: 6px;
            margin-bottom: 10px;
            border-left: 4px solid #28a745;
        }
        .historial-item.error {
            border-left-color: #dc3545;
        }
        .historial-item .email {
            font-weight: 600;
            color: #333;
            margin-bottom: 4px;
        }
        .historial-item .fecha {
            font-size: 12px;
            color: #666;
        }
        .historial-item .estado {
            font-size: 12px;
            margin-top: 4px;
        }
        .historial-item .estado.exitoso {
            color: #28a745;
        }
        .historial-item .estado.error {
            color: #dc3545;
        }
        .historial-empty {
            text-align: center;
            color: #999;
            padding: 20px;
        }
        
        /* Modal */
        .modal {
            display: none;
            position: fixed;
            z-index: 1000;
            left: 0;
            top: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0,0,0,0.5);
            align-items: center;
            justify-content: center;
        }
        .modal.show {
            display: flex;
        }
        .modal-content {
            background: white;
            padding: 30px;
            border-radius: 10px;
            width: 90%;
            max-width: 500px;
            box-shadow: 0 10px 40px rgba(0,0,0,0.3);
        }
        .modal-header {
            margin-bottom: 20px;
        }
        .modal-header h2 {
            font-size: 20px;
            color: #333;
        }
        .form-group {
            margin-bottom: 20px;
        }
        .form-group label {
            display: block;
            margin-bottom: 8px;
            color: #555;
            font-weight: 500;
        }
        .form-group input {
            width: 100%;
            padding: 12px;
            border: 2px solid #e0e0e0;
            border-radius: 6px;
            font-size: 14px;
        }
        .form-group input:focus {
            outline: none;
            border-color: #667eea;
        }
        .modal-buttons {
            display: flex;
            gap: 10px;
            justify-content: flex-end;
        }
        .btn-modal {
            padding: 10px 20px;
            border: none;
            border-radius: 6px;
            font-size: 14px;
            font-weight: 600;
            cursor: pointer;
            transition: background 0.3s;
        }
        .btn-modal.cancelar {
            background: #6c757d;
            color: white;
        }
        .btn-modal.cancelar:hover {
            background: #5a6268;
        }
        .btn-modal.confirmar {
            background: #28a745;
            color: white;
        }
        .btn-modal.confirmar:hover {
            background: #218838;
        }
        .alert {
            padding: 12px;
            border-radius: 6px;
            margin-bottom: 15px;
            display: none;
        }
        .alert.show {
            display: block;
        }
        .alert.success {
            background: #d4edda;
            color: #155724;
            border: 1px solid #c3e6cb;
        }
        .alert.error {
            background: #f8d7da;
            color: #721c24;
            border: 1px solid #f5c6cb;
        }
    </style>
</head>
<body>
    <div class="email-content">
        {{ html_email|safe }}
    </div>
    
    <div class="sidebar">
        <div class="sidebar-header">
            <h2>📧 Gestión de Email</h2>
            <button class="btn-enviar" onclick="abrirModal()">Reenviar Email</button>
        </div>
        
        <div class="historial">
            <h3>Historial de Envíos</h3>
            <div id="historial-container">
                {% if historial %}
                    {% for envio in historial %}
                    <div class="historial-item {{ envio.estado }}">
                        <div class="email">{{ envio.email_destino }}</div>
                        <div class="fecha">{{ envio.fecha_envio.strftime('%Y-%m-%d %H:%M:%S') }}</div>
                        <div class="estado {{ envio.estado }}">
                            {% if envio.estado == 'exitoso' %}
                                ✓ Enviado correctamente
                            {% else %}
                                ✗ Error: {{ envio.mensaje_error }}
                            {% endif %}
                        </div>
                    </div>
                    {% endfor %}
                {% else %}
                    <div class="historial-empty">No hay envíos registrados</div>
                {% endif %}
            </div>
        </div>
    </div>
    
    <!-- Modal -->
    <div id="modal" class="modal">
        <div class="modal-content">
            <div class="modal-header">
                <h2>¿Dónde quieres reenviarlo?</h2>
            </div>
            <div id="alert" class="alert"></div>
            <form id="formEnviar" onsubmit="enviarEmail(event)">
                <div class="form-group">
                    <label>Email de destino</label>
                    <input type="email" id="emailDestino" value="{{ peticion.email or '' }}" required>
                </div>
                <div class="modal-buttons">
                    <button type="button" class="btn-modal cancelar" onclick="cerrarModal()">Cancelar</button>
                    <button type="submit" class="btn-modal confirmar">Enviar</button>
                </div>
            </form>
        </div>
    </div>
    
    <script>
        function abrirModal() {
            document.getElementById('modal').classList.add('show');
            document.getElementById('alert').classList.remove('show');
        }
        
        function cerrarModal() {
            document.getElementById('modal').classList.remove('show');
        }
        
        function mostrarAlerta(mensaje, tipo) {
            const alert = document.getElementById('alert');
            alert.textContent = mensaje;
            alert.className = 'alert show ' + tipo;
        }
        
        async function enviarEmail(event) {
            event.preventDefault();
            
            const email = document.getElementById('emailDestino').value;
            const btnSubmit = event.target.querySelector('button[type="submit"]');
            btnSubmit.disabled = true;
            btnSubmit.textContent = 'Enviando...';
            
            try {
                const response = await fetch('/admin/email/{{ peticion.id }}/enviar', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify({ email: email })
                });
                
                const data = await response.json();
                
                if (data.success) {
                    mostrarAlerta(data.message, 'success');
                    
                    // Agregar al historial
                    const container = document.getElementById('historial-container');
                    const empty = container.querySelector('.historial-empty');
                    if (empty) empty.remove();
                    
                    const nuevoItem = document.createElement('div');
                    nuevoItem.className = 'historial-item exitoso';
                    nuevoItem.innerHTML = `
                        <div class="email">${data.envio.email}</div>
                        <div class="fecha">${data.envio.fecha}</div>
                        <div class="estado exitoso">✓ Enviado correctamente</div>
                    `;
                    container.insertBefore(nuevoItem, container.firstChild);
                    
                    setTimeout(() => {
                        cerrarModal();
                    }, 2000);
                } else {
                    mostrarAlerta(data.message, 'error');
                }
            } catch (error) {
                mostrarAlerta('Error de conexión: ' + error.message, 'error');
            } finally {
                btnSubmit.disabled = false;
                btnSubmit.textContent = 'Enviar';
            }
        }
        
        // Cerrar modal al hacer clic fuera
        document.getElementById('modal').addEventListener('click', function(e) {
            if (e.target === this) {
                cerrarModal();
            }
        });
    </script>
</body>
</html>
u
  
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Login - Lotify Backup</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        .login-container {
            background: white;
            padding: 40px;
            border-radius: 10px;
            box-shadow: 0 10px 40px rgba(0,0,0,0.2);
            width: 100%;
            max-width: 400px;
        }
        h1 {
            color: #333;
            margin-bottom: 30px;
            text-align: center;
            font-size: 24px;
        }
        .form-group {
            margin-bottom: 20px;
        }
        label {
            display: block;
            margin-bottom: 8px;
            color: #555;
            font-weight: 500;
        }
        input {
            width: 100%;
            padding: 12px;
            border: 2px solid #e0e0e0;
            border-radius: 6px;
            font-size: 14px;
            transition: border-color 0.3s;
        }
        input:focus {
            outline: none;
            border-color: #667eea;
        }
        button {
            width: 100%;
            padding: 12px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border: none;
            border-radius: 6px;
            font-size: 16px;
            font-weight: 600;
            cursor: pointer;
            transition: transform 0.2s;
        }
        button:hover {
            transform: translateY(-2px);
        }
        .error {
            background: #fee;
            color: #c33;
            padding: 12px;
            border-radius: 6px;
            margin-bottom: 20px;
            text-align: center;
        }
    </style>
</head>
<body>
    <div class="login-container">
        <h1>🔒 Lotify Backup</h1>
        {% if error %}
        <div class="error">Usuario o contraseña incorrectos</div>
        {% endif %}
        <form method="POST">
            <div class="form-group">
                <label>Usuario</label>
                <input type="text" name="username" required autofocus>
            </div>
            <div class="form-group">
                <label>Contraseña</label>
                <input type="password" name="password" required>
            </div>
            <button type="submit">Iniciar Sesión</button>
        </form>
    </div>
</body>
</html>
uD  
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Admin - Lotify Backup</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            background: #f5f5f5;
            padding: 20px;
        }
        .header {
            background: white;
            padding: 20px;
            border-radius: 10px;
            margin-bottom: 20px;
            display: flex;
            justify-content: space-between;
            align-items: center;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        h1 {
            color: #333;
            font-size: 24px;
        }
        .logout-btn {
            padding: 10px 20px;
            background: #dc3545;
            color: white;
            text-decoration: none;
            border-radius: 6px;
            font-weight: 500;
            transition: background 0.3s;
        }
        .logout-btn:hover {
            background: #c82333;
        }
        .stats {
            background: white;
            padding: 20px;
            border-radius: 10px;
            margin-bottom: 20px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        .stats-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
            gap: 20px;
        }
        .stat-card {
            padding: 20px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border-radius: 8px;
            text-align: center;
        }
        .stat-number {
            font-size: 32px;
            font-weight: bold;
            margin-bottom: 5px;
        }
        .stat-label {
            font-size: 14px;
            opacity: 0.9;
        }
        .table-container {
            background: white;
            border-radius: 10px;
            overflow: hidden;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        table {
            width: 100%;
            border-collapse: collapse;
        }
        th {
            background: #667eea;
            color: white;
            padding: 15px;
            text-align: left;
            font-weight: 600;
            font-size: 13px;
            text-transform: uppercase;
            letter-spacing: 0.5px;
        }
        td {
            padding: 12px 15px;
            border-bottom: 1px solid #f0f0f0;
            font-size: 14px;
            color: #333;
        }
        tr:hover {
            background: #f8f9fa;
        }
        .pagination {
            display: flex;
            justify-content: center;
            gap: 10px;
            padding: 20px;
            background: white;
            border-radius: 0 0 10px 10px;
        }
        .pagination a {
            padding: 8px 16px;
            background: #667eea;
            color: white;
            text-decoration: none;
            border-radius: 6px;
            transition: background 0.3s;
        }
        .pagination a:hover {
            background: #764ba2;
        }
        .pagination .disabled {
            background: #ccc;
            pointer-events: none;
        }
        .pagination .current {
            background: #764ba2;
        }
        .empty {
            text-align: center;
            padding: 40px;
            color: #999;
        }
        .overflow {
            max-width: 200px;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }
        .btn-email {
            padding: 6px 12px;
            background: #28a745;
            color: white;
            text-decoration: none;
            border-radius: 4px;
            font-size: 12px;
            font-weight: 500;
            transition: background 0.3s;
            display: inline-block;
        }
        .btn-email:hover {
            background: #218838;
        }
        .sortable {
            cursor: pointer;
            user-select: none;
            position: relative;
            padding-right: 20px;
        }
        .sortable:hover {
            background: #5568d3;
        }
        .sort-icon {
            position: absolute;
            right: 5px;
            font-size: 10px;
        }
        .filter-row input {
            width: 100%;
            padding: 6px;
            border: 1px solid #ddd;
            border-radius: 4px;
            font-size: 12px;
        }
        .filter-row input:focus {
            outline: none;
            border-color: #667eea;
        }
        .filter-row th {
            background: #f8f9fa;
            padding: 8px;
        }
        .btn-clear-filters {
            padding: 8px 16px;
            background: #6c757d;
            color: white;
            text-decoration: none;
            border-radius: 6px;
            font-size: 12px;
            margin-left: 10px;
        }
        .btn-clear-filters:hover {
            background: #5a6268;
        }
    </style>
</head>
<body>
    <div class="header">
        <h1>LOTIFY Requester - Backup de Pedidos</h1>
        <a href="{{ url_for('logout') }}" class="logout-btn">Cerrar Sesión</a>
    </div>
    
    <div class="stats">
        <div class="stats-grid">
            <div class="stat-card">
                <div class="stat-number">{{ peticiones.total }}</div>
                <div class="stat-label">Total Registros</div>
            </div>
            <div class="stat-card">
                <div class="stat-number">{{ peticiones.pages }}</div>
                <div class="stat-label">Total Páginas</div>
            </div>
            <div class="stat-card">
                <div class="stat-number">{{ peticiones.page }}</div>
                <div class="stat-label">Página Actual</div>
            </div>
        </div>
    </div>
    
    <div class="table-container">
        {% if peticiones.items %}
        <div style="overflow-x: auto;">
        <form method="GET" action="{{ url_for('admin_view') }}" id="filterForm">
            <input type="hidden" name="sort" value="{{ sort_by }}">
            <input type="hidden" name="order" value="{{ sort_order }}">
        <table>
            <thead>
                <tr>
                    <th>Acciones</th>
                    <th class="sortable" onclick="sortTable('id')">
                        ID <span class="sort-icon">{% if sort_by == 'id' %}{{ '▼' if sort_order == 'desc' else '▲' }}{% else %}⇅{% endif %}</span>
                    </th>
                    <th class="sortable" onclick="sortTable('datetime_registro')">
                        Fecha/Hora <span class="sort-icon">{% if sort_by == 'datetime_registro' %}{{ '▼' if sort_order == 'desc' else '▲' }}{% else %}⇅{% endif %}</span>
                    </th>
                    <th class="sortable" onclick="sortTable('empresa')">
                        Empresa <span class="sort-icon">{% if sort_by == 'empresa' %}{{ '▼' if sort_order == 'desc' else '▲' }}{% else %}⇅{% endif %}</span>
                    </th>
                    <th class="sortable" onclick="sortTable('empleado')">
                        Empleado <span class="sort-icon">{% if sort_by == 'empleado' %}{{ '▼' if sort_order == 'desc' else '▲' }}{% else %}⇅{% endif %}</span>
                    </th>
                    <th class="sortable" onclick="sortTable('telefono')">
                        Teléfono <span class="sort-icon">{% if sort_by == 'telefono' %}{{ '▼' if sort_order == 'desc' else '▲' }}{% else %}⇅{% endif %}</span>
                    </th>
                    <th class="sortable" onclick="sortTable('email')">
                        Email <span class="sort-icon">{% if sort_by == 'email' %}{{ '▼' if sort_order == 'desc' else '▲' }}{% else %}⇅{% endif %}</span>
                    </th>
                    <th class="sortable" onclick="sortTable('codigo_lote')">
                        Código Lote <span class="sort-icon">{% if sort_by == 'codigo_lote' %}{{ '▼' if sort_order == 'desc' else '▲' }}{% else %}⇅{% endif %}</span>
                    </th>
                    <th class="sortable" onclick="sortTable('tipo_lote')">
                        Tipo Lote <span class="sort-icon">{% if sort_by == 'tipo_lote' %}{{ '▼' if sort_order == 'desc' else '▲' }}{% else %}⇅{% endif %}</span>
                    </th>
                    <th class="sortable" onclick="sortTable('entrega')">
                        Entrega <span class="sort-icon">{% if sort_by == 'entrega' %}{{ '▼' if sort_order == 'desc' else '▲' }}{% else %}⇅{% endif %}</span>
                    </th>
                    <th class="sortable" onclick="sortTable('direccion')">
                        Dirección <span class="sort-icon">{% if sort_by == 'direccion' %}{{ '▼' if sort_order == 'desc' else '▲' }}{% else %}⇅{% endif %}</span>
                    </th>
                    <th>Franja Desde</th>
                    <th>Franja Hasta</th>
                    <th>Desglose Dir.</th>
                    <th>Detalle Dir.</th>
                    <th>CP</th>
                    <th class="sortable" onclick="sortTable('desglose_poblacion')">
                        Población <span class="sort-icon">{% if sort_by == 'desglose_poblacion' %}{{ '▼' if sort_order == 'desc' else '▲' }}{% else %}⇅{% endif %}</span>
                    </th>
                    <th class="sortable" onclick="sortTable('desglose_provincia')">
                        Provincia <span class="sort-icon">{% if sort_by == 'desglose_provincia' %}{{ '▼' if sort_order == 'desc' else '▲' }}{% else %}⇅{% endif %}</span>
                    </th>
                    <th class="sortable" onclick="sortTable('identificador_cliente')">
                        ID Cliente <span class="sort-icon">{% if sort_by == 'identificador_cliente' %}{{ '▼' if sort_order == 'desc' else '▲' }}{% else %}⇅{% endif %}</span>
                    </th>
                    <th>Observaciones</th>
                    <th class="sortable" onclick="sortTable('sociedad')">
                        Sociedad <span class="sort-icon">{% if sort_by == 'sociedad' %}{{ '▼' if sort_order == 'desc' else '▲' }}{% else %}⇅{% endif %}</span>
                    </th>
                    <th class="sortable" onclick="sortTable('delegacion')">
                        Delegación <span class="sort-icon">{% if sort_by == 'delegacion' %}{{ '▼' if sort_order == 'desc' else '▲' }}{% else %}⇅{% endif %}</span>
                    </th>
                </tr>
                <tr class="filter-row">
                    <th></th>
                    <th></th>
                    <th></th>
                    <th><input type="text" name="filter_empresa" value="{{ filters.get('empresa', '') }}" placeholder="Filtrar..."></th>
                    <th><input type="text" name="filter_empleado" value="{{ filters.get('empleado', '') }}" placeholder="Filtrar..."></th>
                    <th><input type="text" name="filter_telefono" value="{{ filters.get('telefono', '') }}" placeholder="Filtrar..."></th>
                    <th><input type="text" name="filter_email" value="{{ filters.get('email', '') }}" placeholder="Filtrar..."></th>
                    <th><input type="text" name="filter_codigo_lote" value="{{ filters.get('codigo_lote', '') }}" placeholder="Filtrar..."></th>
                    <th><input type="text" name="filter_tipo_lote" value="{{ filters.get('tipo_lote', '') }}" placeholder="Filtrar..."></th>
                    <th><input type="text" name="filter_entrega" value="{{ filters.get('entrega', '') }}" placeholder="Filtrar..."></th>
                    <th><input type="text" name="filter_direccion" value="{{ filters.get('direccion', '') }}" placeholder="Filtrar..."></th>
                    <th></th>
                    <th></th>
                    <th></th>
                    <th></th>
                    <th></th>
                    <th><input type="text" name="filter_desglose_poblacion" value="{{ filters.get('desglose_poblacion', '') }}" placeholder="Filtrar..."></th>
                    <th><input type="text" name="filter_desglose_provincia" value="{{ filters.get('desglose_provincia', '') }}" placeholder="Filtrar..."></th>
                    <th><input type="text" name="filter_identificador_cliente" value="{{ filters.get('identificador_cliente', '') }}" placeholder="Filtrar..."></th>
                    <th></th>
                    <th><input type="text" name="filter_sociedad" value="{{ filters.get('sociedad', '') }}" placeholder="Filtrar..."></th>
                    <th><input type="text" name="filter_delegacion" value="{{ filters.get('delegacion', '') }}" placeholder="Filtrar..."></th>
                </tr>
            </thead>
            <tbody>
                {% for p in peticiones.items %}
                <tr>
                    <td style="white-space: nowrap;">
                        <a href="{{ url_for('ver_email', id=p.id) }}" target="_blank" class="btn-email">📧 Ver Email</a>
                    </td>
                    <td>{{ p.id }}</td>
                    <td style="white-space: nowrap;">{{ p.datetime_registro.strftime('%Y-%m-%d %H:%M:%S') }}</td>
                    <td class="overflow">{{ p.empresa or '-' }}</td>
                    <td class="overflow">{{ p.empleado or '-' }}</td>
                    <td>{{ p.telefono or '-' }}</td>
                    <td class="overflow">{{ p.email or '-' }}</td>
                    <td>{{ p.codigo_lote or '-' }}</td>
                    <td>{{ p.tipo_lote or '-' }}</td>
                    <td>{{ p.entrega or '-' }}</td>
                    <td class="overflow">{{ p.direccion or '-' }}</td>
                    <td>{{ p.franja_horaria_desde or '-' }}</td>
                    <td>{{ p.franja_horaria_hasta or '-' }}</td>
                    <td class="overflow">{{ p.desglose_direccion or '-' }}</td>
                    <td class="overflow">{{ p.desglose_detalle_direccion or '-' }}</td>
                    <td>{{ p.desglose_cp or '-' }}</td>
                    <td>{{ p.desglose_poblacion or '-' }}</td>
                    <td>{{ p.desglose_provincia or '-' }}</td>
                    <td>{{ p.identificador_cliente or '-' }}</td>
                    <td class="overflow">{{ p.observaciones or '-' }}</td>
                    <td>{{ p.sociedad or '-' }}</td>
                    <td>{{ p.delegacion or '-' }}</td>
                </tr>
                {% endfor %}
            </tbody>
        </table>
        </form>
        </div>
        
        <div class="pagination">
            {% if peticiones.page > 1 %}
            <a href="?page=1&sort={{ sort_by }}&order={{ sort_order }}{% for k, v in filters.items() %}&filter_{{ k }}={{ v }}{% endfor %}">⏮ Primera</a>
            {% else %}
            <a class="disabled">⏮ Primera</a>
            {% endif %}
            
            {% if peticiones.has_prev %}
            <a href="?page={{ peticiones.prev_num }}&sort={{ sort_by }}&order={{ sort_order }}{% for k, v in filters.items() %}&filter_{{ k }}={{ v }}{% endfor %}">← Anterior</a>
            {% else %}
            <a class="disabled">← Anterior</a>
            {% endif %}
            
            <a class="current">Página {{ peticiones.page }} de {{ peticiones.pages }}</a>
            
            {% if peticiones.has_next %}
            <a href="?page={{ peticiones.next_num }}&sort={{ sort_by }}&order={{ sort_order }}{% for k, v in filters.items() %}&filter_{{ k }}={{ v }}{% endfor %}">Siguiente →</a>
            {% else %}
            <a class="disabled">Siguiente →</a>
            {% endif %}
            
            {% if peticiones.page < peticiones.pages %}
            <a href="?page={{ peticiones.pages }}&sort={{ sort_by }}&order={{ sort_order }}{% for k, v in filters.items() %}&filter_{{ k }}={{ v }}{% endfor %}">Última ⏭</a>
            {% else %}
            <a class="disabled">Última ⏭</a>
            {% endif %}
            
            {% if filters %}
            <a href="{{ url_for('admin_view') }}" class="btn-clear-filters">✖ Limpiar Filtros</a>
            {% endif %}
        </div>
        {% else %}
        <div class="empty">
            <p>No hay registros disponibles</p>
        </div>
        {% endif %}
    </div>
    
    <script>
        function sortTable(column) {
            const currentSort = '{{ sort_by }}';
            const currentOrder = '{{ sort_order }}';
            let newOrder = 'asc';
            
            if (column === currentSort) {
                newOrder = currentOrder === 'asc' ? 'desc' : 'asc';
            }
            
            const form = document.getElementById('filterForm');
            form.querySelector('[name="sort"]').value = column;
            form.querySelector('[name="order"]').value = newOrder;
            form.submit();
        }
        
        // Auto-submit al escribir en filtros (con debounce)
        let filterTimeout;
        document.querySelectorAll('.filter-row input').forEach(input => {
            input.addEventListener('input', function() {
                clearTimeout(filterTimeout);
                filterTimeout = setTimeout(() => {
                    document.getElementById('filterForm').submit();
                }, 800);
            });
        });
    </script>
</body>
</html>
__main__z0.0.0.0i  )debughostport)NN)DZflaskr   r   r   r   r   r   r   Zflask_sqlalchemyr	   Z
flask_corsr
   r   r   	functoolsr   osr   ZpytzrT   Zemail_functionsr   r   timezoner   r<   rR   Z
secret_keyrS   pathabspathdirname__file__ZbasedirjoinZdata_dirmakedirsr@   ZModelr   rE   Zapp_contextZ
create_allrv   rX   rZ   rj   Zroutern   rz   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   runr   r   r   r   <module>   s   $



5
('

;
1

m



+
&  Lc   
