757 lines
26 KiB
PHP
757 lines
26 KiB
PHP
<?php
|
|
/**
|
|
* Página pública de carga de archivos grandes
|
|
*
|
|
* Los clientes acceden a esta página desde un enlace enviado por WhatsApp.
|
|
* NO requiere autenticación — se valida por token único.
|
|
*
|
|
* URL: /upload.php?token=XXXXX
|
|
*/
|
|
|
|
// No requiere session ni auth del panel
|
|
error_reporting(E_ERROR | E_PARSE);
|
|
ini_set('display_errors', 0);
|
|
|
|
require_once __DIR__ . '/config/config.php';
|
|
|
|
// Obtener token de la URL
|
|
$token = $_GET['token'] ?? '';
|
|
|
|
if (empty($token) || !preg_match('/^[a-zA-Z0-9]{32,64}$/', $token)) {
|
|
http_response_code(404);
|
|
showErrorPage('Enlace no válido', 'El enlace al que intentas acceder no es válido.');
|
|
exit;
|
|
}
|
|
|
|
// Buscar la solicitud en la BD
|
|
try {
|
|
$db = Database::getInstance();
|
|
$request = $db->fetch(
|
|
"SELECT fr.*, u.name as user_name
|
|
FROM file_requests fr
|
|
LEFT JOIN users u ON fr.user_id = u.id
|
|
WHERE fr.token = ?",
|
|
[$token]
|
|
);
|
|
} catch (Exception $e) {
|
|
http_response_code(500);
|
|
showErrorPage('Error del sistema', 'No se pudo verificar el enlace. Intenta de nuevo más tarde.');
|
|
exit;
|
|
}
|
|
|
|
if (!$request) {
|
|
http_response_code(404);
|
|
showErrorPage('Enlace no encontrado', 'Este enlace de carga no existe o ya fue eliminado.');
|
|
exit;
|
|
}
|
|
|
|
// Verificar expiración
|
|
if (strtotime($request['expires_at']) < time()) {
|
|
http_response_code(410);
|
|
showErrorPage('Enlace expirado', 'Este enlace de carga ha expirado. Solicita uno nuevo al asesor.');
|
|
exit;
|
|
}
|
|
|
|
// Verificar estado
|
|
if ($request['status'] === 'cancelled') {
|
|
http_response_code(410);
|
|
showErrorPage('Enlace cancelado', 'Esta solicitud fue cancelada. Contacta a tu asesor.');
|
|
exit;
|
|
}
|
|
|
|
// Verificar archivos ya subidos
|
|
$uploadedFiles = [];
|
|
try {
|
|
$uploadedFiles = $db->fetchAll(
|
|
"SELECT * FROM file_request_uploads WHERE request_id = ? ORDER BY created_at DESC",
|
|
[$request['id']]
|
|
);
|
|
} catch (Exception $e) {
|
|
// No crítico
|
|
}
|
|
|
|
$alreadyUploaded = ($request['status'] === 'uploaded' && count($uploadedFiles) > 0);
|
|
|
|
// Calcular tamaño máximo legible
|
|
$maxSizeMB = round($request['max_file_size'] / (1024 * 1024));
|
|
$allowedTypes = explode(',', $request['allowed_types']);
|
|
|
|
// Auto-detectar APP_URL
|
|
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
|
$appUrl = $protocol . '://' . $_SERVER['HTTP_HOST'];
|
|
|
|
function showErrorPage($title, $message) {
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title><?= htmlspecialchars($title) ?></title>
|
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
|
|
<style>
|
|
body { min-height: 100vh; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #e5ddd5 0%, #f0f4f7 100%); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
|
|
.error-card { background: white; border-radius: 16px; padding: 40px; text-align: center; box-shadow: 0 8px 30px rgba(0,0,0,0.08); max-width: 400px; }
|
|
.error-icon { font-size: 64px; margin-bottom: 16px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="error-card">
|
|
<div class="error-icon">⚠️</div>
|
|
<h4><?= htmlspecialchars($title) ?></h4>
|
|
<p class="text-muted"><?= htmlspecialchars($message) ?></p>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
<?php
|
|
}
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>📎 Subir archivo</title>
|
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
|
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
|
<style>
|
|
:root {
|
|
--wa-green: #25d366;
|
|
--wa-green-dark: #128c7e;
|
|
--wa-dark: #075e54;
|
|
}
|
|
|
|
* { box-sizing: border-box; }
|
|
|
|
body {
|
|
min-height: 100vh;
|
|
background: linear-gradient(135deg, #e5ddd5 0%, #f0f4f7 100%);
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
margin: 0;
|
|
padding: 16px;
|
|
}
|
|
|
|
.upload-container {
|
|
max-width: 480px;
|
|
margin: 0 auto;
|
|
padding-top: 20px;
|
|
}
|
|
|
|
.upload-header {
|
|
background: linear-gradient(180deg, var(--wa-dark) 0%, #054f45 100%);
|
|
color: white;
|
|
padding: 20px 24px;
|
|
border-radius: 16px 16px 0 0;
|
|
text-align: center;
|
|
}
|
|
|
|
.upload-header h4 {
|
|
margin: 0 0 4px 0;
|
|
font-size: 18px;
|
|
}
|
|
|
|
.upload-header p {
|
|
margin: 0;
|
|
opacity: 0.85;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.upload-body {
|
|
background: white;
|
|
padding: 24px;
|
|
border-radius: 0 0 16px 16px;
|
|
box-shadow: 0 8px 30px rgba(0,0,0,0.08);
|
|
}
|
|
|
|
.drop-zone {
|
|
border: 2px dashed #ccc;
|
|
border-radius: 12px;
|
|
padding: 40px 20px;
|
|
text-align: center;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
background: #fafafa;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.drop-zone:hover, .drop-zone.drag-over {
|
|
border-color: var(--wa-green);
|
|
background: rgba(37, 211, 102, 0.04);
|
|
}
|
|
|
|
.drop-zone.drag-over {
|
|
transform: scale(1.01);
|
|
}
|
|
|
|
.drop-zone-icon {
|
|
font-size: 48px;
|
|
color: var(--wa-green);
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.drop-zone-text {
|
|
font-size: 15px;
|
|
color: #555;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.drop-zone-hint {
|
|
font-size: 12px;
|
|
color: #999;
|
|
}
|
|
|
|
.file-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.file-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 10px 12px;
|
|
background: #f8f9fa;
|
|
border-radius: 10px;
|
|
border: 1px solid #eee;
|
|
}
|
|
|
|
.file-item-icon {
|
|
font-size: 28px;
|
|
color: var(--wa-green);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.file-item-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.file-item-name {
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.file-item-size {
|
|
font-size: 12px;
|
|
color: #888;
|
|
}
|
|
|
|
.file-item-remove {
|
|
color: #dc3545;
|
|
cursor: pointer;
|
|
padding: 4px;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.file-item-remove:hover {
|
|
color: #a71d2a;
|
|
}
|
|
|
|
.upload-progress {
|
|
display: none;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.progress {
|
|
height: 6px;
|
|
border-radius: 3px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.progress-bar {
|
|
background: var(--wa-green);
|
|
transition: width 0.3s ease;
|
|
}
|
|
|
|
.btn-upload {
|
|
background: var(--wa-green);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 12px;
|
|
padding: 14px 24px;
|
|
width: 100%;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.btn-upload:hover:not(:disabled) {
|
|
background: var(--wa-green-dark);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.btn-upload:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.success-card {
|
|
text-align: center;
|
|
padding: 30px;
|
|
}
|
|
|
|
.success-icon {
|
|
font-size: 64px;
|
|
color: var(--wa-green);
|
|
margin-bottom: 16px;
|
|
animation: bounceIn 0.5s ease;
|
|
}
|
|
|
|
@keyframes bounceIn {
|
|
0% { transform: scale(0.3); opacity: 0; }
|
|
50% { transform: scale(1.05); }
|
|
100% { transform: scale(1); opacity: 1; }
|
|
}
|
|
|
|
.uploaded-files {
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.uploaded-file {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 8px 12px;
|
|
background: #e8f5e9;
|
|
border-radius: 8px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.info-banner {
|
|
background: #fff3e0;
|
|
border: 1px solid rgba(255, 152, 0, 0.2);
|
|
border-radius: 10px;
|
|
padding: 12px 16px;
|
|
margin-bottom: 20px;
|
|
font-size: 13px;
|
|
color: #5c3a00;
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 10px;
|
|
}
|
|
|
|
.info-banner i {
|
|
color: #ff9800;
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.security-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-size: 12px;
|
|
color: #888;
|
|
margin-top: 16px;
|
|
}
|
|
|
|
.security-badge i {
|
|
color: var(--wa-green);
|
|
}
|
|
|
|
/* Responsive */
|
|
@media (max-width: 480px) {
|
|
body { padding: 8px; }
|
|
.upload-body { padding: 16px; }
|
|
.drop-zone { padding: 28px 16px; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="upload-container">
|
|
<div class="upload-header">
|
|
<h4><i class="fas fa-cloud-upload-alt me-2"></i>Subir archivo</h4>
|
|
<p>Envía archivos de hasta <?= $maxSizeMB ?> MB de forma segura</p>
|
|
</div>
|
|
|
|
<div class="upload-body">
|
|
<?php if ($alreadyUploaded): ?>
|
|
<!-- Ya se subieron archivos -->
|
|
<div class="success-card">
|
|
<div class="success-icon"><i class="fas fa-check-circle"></i></div>
|
|
<h5>¡Archivos recibidos!</h5>
|
|
<p class="text-muted">Tus archivos fueron enviados correctamente. El asesor los recibirá en breve.</p>
|
|
|
|
<div class="uploaded-files">
|
|
<?php foreach ($uploadedFiles as $file): ?>
|
|
<div class="uploaded-file">
|
|
<i class="fas fa-file-check text-success"></i>
|
|
<div style="flex:1; text-align:left;">
|
|
<div style="font-weight:600; font-size:13px;"><?= htmlspecialchars($file['original_filename']) ?></div>
|
|
<div style="font-size:12px; color:#888;"><?= formatSize($file['file_size']) ?></div>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
|
|
<div class="info-banner mt-3">
|
|
<i class="fas fa-info-circle"></i>
|
|
<span>Puedes subir archivos adicionales si lo necesitas.</span>
|
|
</div>
|
|
|
|
<button class="btn-upload" onclick="showUploadForm()">
|
|
<i class="fas fa-plus me-2"></i>Subir más archivos
|
|
</button>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<div id="upload-form" style="<?= $alreadyUploaded ? 'display:none' : '' ?>">
|
|
<div class="info-banner">
|
|
<i class="fas fa-shield-alt"></i>
|
|
<span>
|
|
Este enlace es seguro y exclusivo para ti. Los archivos serán recibidos directamente por tu asesor.
|
|
<br><strong>Máximo: <?= $maxSizeMB ?> MB por archivo.</strong>
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Zona de arrastre -->
|
|
<div class="drop-zone" id="drop-zone" onclick="document.getElementById('file-input-public').click()">
|
|
<div class="drop-zone-icon"><i class="fas fa-cloud-upload-alt"></i></div>
|
|
<div class="drop-zone-text">Toca para seleccionar o arrastra tus archivos aquí</div>
|
|
<div class="drop-zone-hint">
|
|
<?php
|
|
$typeLabels = [];
|
|
if (in_array('image', $allowedTypes)) $typeLabels[] = 'imágenes';
|
|
if (in_array('document', $allowedTypes)) $typeLabels[] = 'documentos';
|
|
if (in_array('video', $allowedTypes)) $typeLabels[] = 'videos';
|
|
if (in_array('audio', $allowedTypes)) $typeLabels[] = 'audios';
|
|
echo implode(', ', $typeLabels) . ' — hasta ' . $maxSizeMB . ' MB';
|
|
?>
|
|
</div>
|
|
</div>
|
|
|
|
<input type="file" id="file-input-public" multiple accept="<?= getAcceptString($allowedTypes) ?>"
|
|
style="display:none" onchange="handleFileSelect(event)">
|
|
|
|
<!-- Lista de archivos seleccionados -->
|
|
<div class="file-list" id="file-list"></div>
|
|
|
|
<!-- Barra de progreso -->
|
|
<div class="upload-progress" id="upload-progress">
|
|
<div class="d-flex justify-content-between mb-1">
|
|
<small id="progress-text">Subiendo...</small>
|
|
<small id="progress-pct">0%</small>
|
|
</div>
|
|
<div class="progress">
|
|
<div class="progress-bar" id="progress-bar" style="width: 0%"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Botón enviar -->
|
|
<button class="btn-upload" id="btn-submit" onclick="submitFiles()" disabled>
|
|
<i class="fas fa-paper-plane me-2"></i>Enviar archivos
|
|
</button>
|
|
|
|
<div class="security-badge">
|
|
<i class="fas fa-lock"></i>
|
|
Conexión cifrada y segura
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Estado de éxito (post-upload) -->
|
|
<div id="upload-success" style="display:none">
|
|
<div class="success-card">
|
|
<div class="success-icon"><i class="fas fa-check-circle"></i></div>
|
|
<h5>¡Archivos enviados!</h5>
|
|
<p class="text-muted">Tu asesor recibirá los archivos en breve. Puedes cerrar esta página.</p>
|
|
<button class="btn-upload mt-3" onclick="showUploadForm()">
|
|
<i class="fas fa-plus me-2"></i>Subir más archivos
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const CONFIG = {
|
|
token: '<?= htmlspecialchars($token) ?>',
|
|
maxFileSize: <?= intval($request['max_file_size']) ?>,
|
|
maxFileSizeMB: <?= $maxSizeMB ?>,
|
|
allowedTypes: <?= json_encode($allowedTypes) ?>,
|
|
apiUrl: 'api/file_request_upload.php'
|
|
};
|
|
|
|
let selectedFiles = [];
|
|
|
|
// === Drag & Drop ===
|
|
const dropZone = document.getElementById('drop-zone');
|
|
|
|
['dragenter', 'dragover'].forEach(evt => {
|
|
dropZone.addEventListener(evt, e => {
|
|
e.preventDefault();
|
|
dropZone.classList.add('drag-over');
|
|
});
|
|
});
|
|
|
|
['dragleave', 'drop'].forEach(evt => {
|
|
dropZone.addEventListener(evt, e => {
|
|
e.preventDefault();
|
|
dropZone.classList.remove('drag-over');
|
|
});
|
|
});
|
|
|
|
dropZone.addEventListener('drop', e => {
|
|
const files = Array.from(e.dataTransfer.files);
|
|
addFiles(files);
|
|
});
|
|
|
|
// === Selección de archivos ===
|
|
function handleFileSelect(event) {
|
|
const files = Array.from(event.target.files);
|
|
addFiles(files);
|
|
event.target.value = ''; // Reset para permitir re-selección
|
|
}
|
|
|
|
function addFiles(files) {
|
|
for (const file of files) {
|
|
// Validar tamaño
|
|
if (file.size > CONFIG.maxFileSize) {
|
|
alert(`El archivo "${file.name}" excede el límite de ${CONFIG.maxFileSizeMB} MB.`);
|
|
continue;
|
|
}
|
|
|
|
// Validar tipo
|
|
if (!isAllowedType(file)) {
|
|
alert(`El tipo de archivo "${file.name}" no está permitido.`);
|
|
continue;
|
|
}
|
|
|
|
// Evitar duplicados
|
|
if (selectedFiles.some(f => f.name === file.name && f.size === file.size)) {
|
|
continue;
|
|
}
|
|
|
|
selectedFiles.push(file);
|
|
}
|
|
|
|
renderFileList();
|
|
updateSubmitButton();
|
|
}
|
|
|
|
function isAllowedType(file) {
|
|
const type = file.type || '';
|
|
const ext = file.name.split('.').pop().toLowerCase();
|
|
|
|
for (const allowed of CONFIG.allowedTypes) {
|
|
switch (allowed) {
|
|
case 'image':
|
|
if (type.startsWith('image/') || ['jpg','jpeg','png','gif','webp','bmp','svg'].includes(ext)) return true;
|
|
break;
|
|
case 'document':
|
|
if (['pdf','doc','docx','xls','xlsx','ppt','pptx','txt','csv','rtf','odt','ods','zip','rar','7z'].includes(ext)) return true;
|
|
if (type.startsWith('application/')) return true;
|
|
break;
|
|
case 'video':
|
|
if (type.startsWith('video/') || ['mp4','mov','avi','mkv','webm','3gp'].includes(ext)) return true;
|
|
break;
|
|
case 'audio':
|
|
if (type.startsWith('audio/') || ['mp3','ogg','wav','aac','m4a','opus'].includes(ext)) return true;
|
|
break;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function removeFile(index) {
|
|
selectedFiles.splice(index, 1);
|
|
renderFileList();
|
|
updateSubmitButton();
|
|
}
|
|
|
|
function renderFileList() {
|
|
const list = document.getElementById('file-list');
|
|
if (selectedFiles.length === 0) {
|
|
list.innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
list.innerHTML = selectedFiles.map((file, i) => `
|
|
<div class="file-item">
|
|
<div class="file-item-icon">
|
|
<i class="${getFileIcon(file)}"></i>
|
|
</div>
|
|
<div class="file-item-info">
|
|
<div class="file-item-name">${escapeHtml(file.name)}</div>
|
|
<div class="file-item-size">${formatSize(file.size)}</div>
|
|
</div>
|
|
<div class="file-item-remove" onclick="removeFile(${i})">
|
|
<i class="fas fa-times-circle"></i>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function updateSubmitButton() {
|
|
const btn = document.getElementById('btn-submit');
|
|
btn.disabled = selectedFiles.length === 0;
|
|
if (selectedFiles.length > 0) {
|
|
btn.innerHTML = `<i class="fas fa-paper-plane me-2"></i>Enviar ${selectedFiles.length} archivo${selectedFiles.length > 1 ? 's' : ''}`;
|
|
} else {
|
|
btn.innerHTML = '<i class="fas fa-paper-plane me-2"></i>Enviar archivos';
|
|
}
|
|
}
|
|
|
|
// === Subida de archivos ===
|
|
async function submitFiles() {
|
|
if (selectedFiles.length === 0) return;
|
|
|
|
const btn = document.getElementById('btn-submit');
|
|
const progress = document.getElementById('upload-progress');
|
|
const progressBar = document.getElementById('progress-bar');
|
|
const progressText = document.getElementById('progress-text');
|
|
const progressPct = document.getElementById('progress-pct');
|
|
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Subiendo...';
|
|
progress.style.display = 'block';
|
|
|
|
let uploaded = 0;
|
|
const total = selectedFiles.length;
|
|
let errors = [];
|
|
|
|
for (let i = 0; i < selectedFiles.length; i++) {
|
|
const file = selectedFiles[i];
|
|
progressText.textContent = `Subiendo ${file.name}... (${i + 1}/${total})`;
|
|
|
|
try {
|
|
const formData = new FormData();
|
|
formData.append('token', CONFIG.token);
|
|
formData.append('file', file);
|
|
|
|
const response = await uploadWithProgress(formData, (pct) => {
|
|
const totalPct = Math.round(((i + pct / 100) / total) * 100);
|
|
progressBar.style.width = totalPct + '%';
|
|
progressPct.textContent = totalPct + '%';
|
|
});
|
|
|
|
if (response.success) {
|
|
uploaded++;
|
|
} else {
|
|
errors.push(`${file.name}: ${response.error || 'Error desconocido'}`);
|
|
}
|
|
} catch (e) {
|
|
errors.push(`${file.name}: Error de conexión`);
|
|
}
|
|
}
|
|
|
|
if (uploaded > 0) {
|
|
// Mostrar éxito
|
|
document.getElementById('upload-form').style.display = 'none';
|
|
document.getElementById('upload-success').style.display = 'block';
|
|
selectedFiles = [];
|
|
|
|
if (errors.length > 0) {
|
|
alert(`Se subieron ${uploaded}/${total} archivos.\n\nErrores:\n${errors.join('\n')}`);
|
|
}
|
|
} else {
|
|
alert('No se pudieron subir los archivos.\n\n' + errors.join('\n'));
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i class="fas fa-paper-plane me-2"></i>Reintentar';
|
|
}
|
|
|
|
progress.style.display = 'none';
|
|
progressBar.style.width = '0%';
|
|
}
|
|
|
|
function uploadWithProgress(formData, onProgress) {
|
|
return new Promise((resolve, reject) => {
|
|
const xhr = new XMLHttpRequest();
|
|
|
|
xhr.upload.addEventListener('progress', e => {
|
|
if (e.lengthComputable) {
|
|
onProgress(Math.round((e.loaded / e.total) * 100));
|
|
}
|
|
});
|
|
|
|
xhr.addEventListener('load', () => {
|
|
try {
|
|
const data = JSON.parse(xhr.responseText);
|
|
resolve(data);
|
|
} catch (e) {
|
|
resolve({ success: xhr.status === 200, error: 'Respuesta inválida' });
|
|
}
|
|
});
|
|
|
|
xhr.addEventListener('error', () => reject(new Error('Error de red')));
|
|
xhr.addEventListener('timeout', () => reject(new Error('Tiempo de espera agotado')));
|
|
|
|
xhr.open('POST', CONFIG.apiUrl);
|
|
xhr.timeout = 120000; // 2 minutos por archivo
|
|
xhr.send(formData);
|
|
});
|
|
}
|
|
|
|
function showUploadForm() {
|
|
document.getElementById('upload-success').style.display = 'none';
|
|
const successCards = document.querySelectorAll('.success-card');
|
|
successCards.forEach(c => c.style.display = 'none');
|
|
document.getElementById('upload-form').style.display = 'block';
|
|
selectedFiles = [];
|
|
renderFileList();
|
|
updateSubmitButton();
|
|
}
|
|
|
|
// === Utilidades ===
|
|
function getFileIcon(file) {
|
|
const ext = file.name.split('.').pop().toLowerCase();
|
|
if (file.type.startsWith('image/')) return 'fas fa-file-image text-primary';
|
|
if (file.type.startsWith('video/')) return 'fas fa-file-video text-danger';
|
|
if (file.type.startsWith('audio/')) return 'fas fa-file-audio text-warning';
|
|
const icons = {
|
|
pdf: 'fas fa-file-pdf text-danger',
|
|
doc: 'fas fa-file-word text-primary', docx: 'fas fa-file-word text-primary',
|
|
xls: 'fas fa-file-excel text-success', xlsx: 'fas fa-file-excel text-success',
|
|
ppt: 'fas fa-file-powerpoint text-warning', pptx: 'fas fa-file-powerpoint text-warning',
|
|
zip: 'fas fa-file-archive text-secondary', rar: 'fas fa-file-archive text-secondary',
|
|
txt: 'fas fa-file-alt text-muted', csv: 'fas fa-file-csv text-success',
|
|
};
|
|
return icons[ext] || 'fas fa-file text-muted';
|
|
}
|
|
|
|
function formatSize(bytes) {
|
|
if (bytes === 0) return '0 B';
|
|
const k = 1024;
|
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return (bytes / Math.pow(k, i)).toFixed(1) + ' ' + sizes[i];
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const d = document.createElement('div');
|
|
d.textContent = text;
|
|
return d.innerHTML;
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|
|
|
|
<?php
|
|
// Helpers PHP
|
|
function formatSize($bytes) {
|
|
if ($bytes == 0) return '0 B';
|
|
$k = 1024;
|
|
$sizes = ['B', 'KB', 'MB', 'GB'];
|
|
$i = floor(log($bytes) / log($k));
|
|
return round($bytes / pow($k, $i), 1) . ' ' . $sizes[$i];
|
|
}
|
|
|
|
function getAcceptString($types) {
|
|
$accepts = [];
|
|
if (in_array('image', $types)) $accepts[] = 'image/*';
|
|
if (in_array('document', $types)) {
|
|
$accepts = array_merge($accepts, [
|
|
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
|
|
'.txt', '.csv', '.rtf', '.odt', '.zip', '.rar', '.7z'
|
|
]);
|
|
}
|
|
if (in_array('video', $types)) $accepts[] = 'video/*';
|
|
if (in_array('audio', $types)) $accepts[] = 'audio/*';
|
|
return implode(',', $accepts);
|
|
}
|
|
?>
|