From 78af4360fcb1e3ee11b50c533b1ecd1a90c97fd7 Mon Sep 17 00:00:00 2001
From: beseira13
Date: Mon, 9 Feb 2026 16:41:30 -0300
Subject: [PATCH] cambios para produccion
---
.env | 6 +-
.env.example | 4 +
src/App.jsx | 231 +++++++++++++++++++++---
src/components/AutoReprintModal.jsx | 263 ++++++++++++++++++++++++++++
src/components/FileRequestModal.jsx | 244 ++++++++++++++++++++++++++
src/components/Login.jsx | 8 +-
src/components/ResultTable.jsx | 112 ++++++------
src/components/StoreSelector.jsx | 69 ++++++--
src/components/ToolsMenu.jsx | 164 +++++++++++++++++
src/services/storeStatusService.js | 31 ++++
10 files changed, 1035 insertions(+), 97 deletions(-)
create mode 100644 src/components/AutoReprintModal.jsx
create mode 100644 src/components/FileRequestModal.jsx
create mode 100644 src/components/ToolsMenu.jsx
create mode 100644 src/services/storeStatusService.js
diff --git a/.env b/.env
index 5519a30..8cb02ad 100644
--- a/.env
+++ b/.env
@@ -13,4 +13,8 @@ VITE_USER_PRIVILEGIO=READ
VITE_FTP_HOST=ftp://134.122.126.66:21
VITE_FTP_USER=mysql_inyect
VITE_FTP_PASS=YTJjrRdnCPwjTSrN
-VITE_FTP_PATH=/carpeta_respaldos/
\ No newline at end of file
+VITE_FTP_PATH=/carpeta_respaldos/
+
+# URLs de Servicio de Reimpresión
+VITE_REPRINT_URL_PERU=https://ws-posvirtual-peru-n.sial.cl/public/reimpresion_directa_asistenten/
+VITE_REPRINT_URL_COLOMBIA=https://ws-posvirtual-colombia.sial.cl/public/reimpresion_directa_asistenten/
\ No newline at end of file
diff --git a/.env.example b/.env.example
index 85dd1c8..01932ce 100644
--- a/.env.example
+++ b/.env.example
@@ -14,3 +14,7 @@ VITE_FTP_HOST=ftp.tuservidor.com
VITE_FTP_USER=usuario_ftp
VITE_FTP_PASS=clave_ftp
VITE_FTP_PATH=/carpeta_respaldos/
+
+# URLs de Servicio de Reimpresión
+VITE_REPRINT_URL_PERU=https://ws-posvirtual-peru.sial.cl/public/reimpresion_directa_asistenten/
+VITE_REPRINT_URL_COLOMBIA=https://ws-posvirtual-colombia.sial.cl/public/reimpresion_directa_asistenten/
diff --git a/src/App.jsx b/src/App.jsx
index b65e5c7..5a25e4d 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -7,8 +7,12 @@ import CommandHistory from './components/CommandHistory';
import DatabaseSelector from './components/DatabaseSelector';
import Login from './components/Login';
import BackupModal from './components/BackupModal';
+import FileRequestModal from './components/FileRequestModal';
+import AutoReprintModal from './components/AutoReprintModal';
+import ToolsMenu from './components/ToolsMenu';
import { commandService, authService } from './services/api';
import { injectDatabaseToSql } from './utils/sqlValidator';
+import { FileText } from 'lucide-react';
function App() {
const [currentUser, setCurrentUser] = useState(null);
@@ -23,6 +27,8 @@ function App() {
const [sidebarOpen, setSidebarOpen] = useState(true);
const [selectedClient, setSelectedClient] = useState('2'); // Peru is 2, Colombia is 3
const [isBackupModalOpen, setIsBackupModalOpen] = useState(false);
+ const [isFileModalOpen, setIsFileModalOpen] = useState(false);
+ const [isAutoReprintModalOpen, setIsAutoReprintModalOpen] = useState(false);
useEffect(() => {
// Check if user is already logged in
@@ -160,11 +166,6 @@ function App() {
setPollingProgress(null);
try {
- const now = new Date();
- const dateStr = now.toISOString().split('T')[0];
- const timeStr = now.getTime().toString().slice(-3);
- const commandId = `BK-${dateStr}-${timeStr}`;
-
const backupSql = `BACKUP ${JSON.stringify({
database: backupData.database,
ftp: {
@@ -175,14 +176,13 @@ function App() {
}
})}`;
- // Step 1: Create the command
+ // Step 1: Create the command - Let API generate numeric ID
const createResponse = await commandService.create({
store_id: parseInt(selectedStore.id, 10),
sql: backupSql,
- privilegio: 'CRUD', // As per requirements
+ privilegio: 'CRUD',
id_user: currentUser.id,
id_cliente: parseInt(selectedClient, 10),
- command_id: commandId,
timeout: 600
});
@@ -192,31 +192,34 @@ function App() {
return;
}
+ // IMPORTANTE: Usar el command_id que devuelve la API
+ const actualCommandId = createResponse.command_id;
+
// Update status to polling
setExecutionStatus('polling');
setCurrentResult({
message: 'Solicitud de respaldo enviada. El agente está procesando...',
- command_id: commandId,
- stats: { info: `Backup #${commandId} iniciado. Esperando compresión y envío...` }
+ command_id: actualCommandId,
+ stats: { info: `Backup #${actualCommandId} iniciado. Esperando compresión y envío...` }
});
- // Step 2: Poll for results
- const result = await commandService.pollForResult(commandId, {
- interval: 5000, // Poll every 5 seconds for backups
- maxAttempts: 120, // Wait up to 10 minutes (600 seconds)
+ // Step 2: Poll for results using numeric ID
+ const result = await commandService.pollForResult(actualCommandId, {
+ interval: 5000,
+ maxAttempts: 120,
onProgress: ({ attempts, maxAttempts, status }) => {
setPollingProgress({ attempts, maxAttempts, status });
setCurrentResult(prev => ({
...prev,
message: `Procesando respaldo remoto... (${attempts}/${maxAttempts})`,
- stats: { info: `Backup #${commandId} - Intento ${attempts} de ${maxAttempts}` }
+ stats: { info: `Backup #${actualCommandId} - Intento ${attempts} de ${maxAttempts}` }
}));
}
});
setExecutionStatus('completed');
setPollingProgress(null);
- setCurrentResult(normalizeApiResponse(result, commandId));
+ setCurrentResult(normalizeApiResponse(result, actualCommandId));
} catch (error) {
console.error(error);
@@ -229,6 +232,169 @@ function App() {
}
};
+ const handleFileRequest = async (fileData) => {
+ if (!selectedStore) return;
+
+ setIsFileModalOpen(false);
+ setExecutionStatus('running');
+ setCurrentResult(null);
+ setPollingProgress(null);
+
+ try {
+ // El agente espera un objeto con type: file y opcionalmente path
+ const fileSql = `FILE ${JSON.stringify({
+ type: 'file',
+ path: fileData.path,
+ filename: fileData.filename
+ })}`;
+
+ // Step 1: Create the command - Let API generate numeric ID
+ const createResponse = await commandService.create({
+ store_id: parseInt(selectedStore.id, 10),
+ sql: fileSql,
+ privilegio: 'CRUD', // High privilege for file system access
+ id_user: currentUser.id,
+ id_cliente: parseInt(selectedClient, 10),
+ timeout: 300
+ });
+
+ if (!createResponse.success) {
+ setExecutionStatus('error');
+ setCurrentResult({ error: createResponse.message || 'Error al solicitar archivo' });
+ return;
+ }
+
+ // IMPORTANTE: Usar el command_id que devuelve la API
+ const actualCommandId = createResponse.command_id;
+
+ // Update status to polling
+ setExecutionStatus('polling');
+ setCurrentResult({
+ message: 'Solicitud de archivo enviada. El agente está procesando...',
+ command_id: actualCommandId,
+ stats: { info: `Archivo #${actualCommandId} solicitado. Esperando agente...` }
+ });
+
+ // Step 2: Poll for results using numeric ID
+ const result = await commandService.pollForResult(actualCommandId, {
+ interval: 3000, // Poll every 3 seconds
+ maxAttempts: 100, // Wait up to 5 minutes
+ onProgress: ({ attempts, maxAttempts, status }) => {
+ setPollingProgress({ attempts, maxAttempts, status });
+ setCurrentResult(prev => ({
+ ...prev,
+ message: `Buscando archivo en el equipo remoto... (${attempts}/${maxAttempts})`,
+ stats: { info: `Archivo #${actualCommandId} - Intento ${attempts} de ${maxAttempts}` }
+ }));
+ }
+ });
+
+ setExecutionStatus('completed');
+ setPollingProgress(null);
+ const normalized = normalizeApiResponse(result, actualCommandId);
+ setCurrentResult(normalized);
+
+ // DESCARGA AUTOMÁTICA TXT
+ if (normalized && !normalized.error) {
+ const dRows = normalized.rows || normalized.data || [];
+ const dCols = normalized.columns || (dRows[0] ? Object.keys(dRows[0]) : []);
+
+ // Si tiene una sola fila y una sola columna, o si el comando es FILE
+ if (dRows.length === 1) {
+ const content = Array.isArray(dRows[0]) ? dRows[0][0] : (dCols[0] ? dRows[0][dCols[0]] : null);
+ if (content && typeof content === 'string' && content.length > 0) {
+ const blob = new Blob([content], { type: 'text/plain;charset=utf-8;' });
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = `file_${actualCommandId}.txt`;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ URL.revokeObjectURL(url);
+ }
+ }
+ }
+
+ } catch (error) {
+ console.error(error);
+ setExecutionStatus('error');
+ setPollingProgress(null);
+ setCurrentResult({
+ error: error.message || "Error al procesar la petición de archivo",
+ stats: { info: 'Error durante la ejecución del comando FILE' }
+ });
+ }
+ };
+
+ const handleAutoReprintRequest = async (reprintData) => {
+ if (!selectedStore) return;
+
+ setIsAutoReprintModalOpen(false);
+ setExecutionStatus('running');
+ setCurrentResult(null);
+ setPollingProgress(null);
+
+ try {
+ // Determine service URL based on country/client from environment variables
+ const serviceUrls = {
+ '2': import.meta.env.VITE_REPRINT_URL_PERU, // Peru
+ '3': import.meta.env.VITE_REPRINT_URL_COLOMBIA // Colombia
+ };
+
+ const serviceUrl = serviceUrls[selectedClient] || serviceUrls['2']; // Default to Peru
+
+ // Prepare data for direct HTTP POST call
+ const requestData = {
+ nlocal: selectedStore.id,
+ tdoc: reprintData.documentType,
+ fecha: reprintData.date,
+ ndoc: reprintData.documentNumber,
+ tipo: 'herramienta'
+ };
+
+ setCurrentResult({
+ message: 'Enviando solicitud de reimpresión al servicio externo...',
+ stats: { info: 'Conectando con el servicio de reimpresión...' }
+ });
+
+ // Make direct HTTP POST call to external service
+ const response = await fetch(serviceUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(requestData)
+ });
+
+ if (!response.ok) {
+ throw new Error(`Error del servicio: ${response.status} ${response.statusText}`);
+ }
+
+ const result = await response.json();
+
+ setExecutionStatus('completed');
+ setPollingProgress(null);
+ setCurrentResult({
+ message: '✅ Solicitud ingresada con éxito. Por favor, constate con la tienda si el documento se imprimió correctamente.',
+ data: result,
+ stats: {
+ info: 'Solicitud procesada por el servicio externo',
+ service_url: serviceUrl
+ }
+ });
+
+ } catch (error) {
+ console.error(error);
+ setExecutionStatus('error');
+ setPollingProgress(null);
+ setCurrentResult({
+ error: error.message || "Error al procesar la reimpresión automática",
+ stats: { info: 'Error al conectar con el servicio de reimpresión' }
+ });
+ }
+ };
+
/**
* Normalizes different API response formats into a standard structure for ResultTable
@@ -257,12 +423,12 @@ function App() {
// Check for download URL in dataContainer
if (dataContainer && (dataContainer.download_url || dataContainer.url || dataContainer.file_url)) {
normalizedResult.downloadUrl = dataContainer.download_url || dataContainer.url || dataContainer.file_url;
- normalizedResult.message = '¡Respaldo listo para descargar!';
+ normalizedResult.message = String(commandId).startsWith('FILE-') ? '¡Archivo listo para descargar!' : '¡Respaldo listo para descargar!';
}
// Fallback search in the whole result object
else if (result.download_url || result.url || result.file_url) {
normalizedResult.downloadUrl = result.download_url || result.url || result.file_url;
- normalizedResult.message = '¡Respaldo listo para descargar!';
+ normalizedResult.message = String(commandId).startsWith('FILE-') ? '¡Archivo listo para descargar!' : '¡Respaldo listo para descargar!';
}
if (dataContainer) {
@@ -422,14 +588,12 @@ function App() {
{selectedStore && (
-
setIsBackupModalOpen(true)}
- className="btn-primary"
- style={{ padding: '0.375rem 0.75rem', fontSize: '0.75rem', backgroundColor: 'var(--warning)' }}
- title="Solicitar Respaldo de Base de Datos"
- >
- Respaldo DB
-
+
setIsFileModalOpen(true)}
+ onBackupRequest={() => setIsBackupModalOpen(true)}
+ onAutoReprint={() => setIsAutoReprintModalOpen(true)}
+ disabled={false}
+ />
)}
setCurrentResult(null)}
/>
@@ -488,6 +653,20 @@ function App() {
onConfirm={handleBackupRequest}
/>
+ setIsFileModalOpen(false)}
+ store={selectedStore}
+ onConfirm={handleFileRequest}
+ />
+
+ setIsAutoReprintModalOpen(false)}
+ store={selectedStore}
+ onConfirm={handleAutoReprintRequest}
+ />
+
+
+ );
+}
diff --git a/src/components/FileRequestModal.jsx b/src/components/FileRequestModal.jsx
new file mode 100644
index 0000000..c0e8581
--- /dev/null
+++ b/src/components/FileRequestModal.jsx
@@ -0,0 +1,244 @@
+import React, { useState } from 'react';
+import { X, FileText, Folder, File, Shield, AlertCircle } from 'lucide-react';
+
+export default function FileRequestModal({ isOpen, onClose, store, onConfirm }) {
+ const [formData, setFormData] = useState({
+ path: '',
+ filename: ''
+ });
+
+ if (!isOpen) return null;
+
+ const handleSubmit = (e) => {
+ e.preventDefault();
+ onConfirm(formData);
+ };
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
+ Solicitar Archivo Remoto
+
+
+
+
+
+
+
+ {/* Content */}
+
+
+
+ {/* Reusing styles from BackupModal via class names where possible,
+ but since BackupModal styles are scoped in a style tag,
+ I should probably ensure they are available or duplicate them if needed.
+ The user's CSS for BackupModal is quite comprehensive. */}
+
+
+ );
+}
diff --git a/src/components/Login.jsx b/src/components/Login.jsx
index ff08a6e..a427199 100644
--- a/src/components/Login.jsx
+++ b/src/components/Login.jsx
@@ -22,11 +22,15 @@ export default function Login({ onLoginSuccess }) {
localStorage.setItem('user_data', JSON.stringify(response.user));
onLoginSuccess(response.user);
} else {
- setError(response.message || 'Credenciales inválidas');
+ // Display error message from backend (error or mensaje field)
+ const errorMsg = response.mensaje || response.error || response.message || 'Credenciales inválidas';
+ setError(errorMsg);
}
} catch (err) {
console.error('Login error:', err);
- setError(err.message || 'Error al conectar con el servidor');
+ // Try to extract error message from response
+ const errorMsg = err.mensaje || err.error || err.message || 'Error al conectar con el servidor';
+ setError(errorMsg);
} finally {
setLoading(false);
}
diff --git a/src/components/ResultTable.jsx b/src/components/ResultTable.jsx
index 284f0c7..cfaf7db 100644
--- a/src/components/ResultTable.jsx
+++ b/src/components/ResultTable.jsx
@@ -3,7 +3,7 @@ import { AlertCircle, Clock, Database, FileX, Loader2, CheckCircle2, Hash, Copy,
const MAX_DISPLAY_ROWS = 1000;
-export default function ResultTable({ result, loading, pollingProgress }) {
+export default function ResultTable({ result, loading, pollingProgress, onClear }) {
const [copied, setCopied] = useState(false);
// Normalize data
@@ -40,36 +40,19 @@ export default function ResultTable({ result, loading, pollingProgress }) {
return [header, ...csvRows].join('\n');
};
- // Download as CSV
- const handleDownloadCSV = (cols, dataRows) => {
- const csv = generateCSV(cols, dataRows);
- const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
+ // Download file
+ const handleDownload = (content, filename, type = 'text/csv') => {
+ const blob = new Blob([content], { type: `${type};charset=utf-8;` });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.setAttribute('href', url);
- link.setAttribute('download', `query_result_${Date.now()}.csv`);
+ link.setAttribute('download', filename);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
- // Auto-download if too many rows
- React.useEffect(() => {
- if (rows.length > MAX_DISPLAY_ROWS) {
- handleDownloadCSV(columns, rows);
- }
- }, [result]);
-
- // Copy to clipboard
- const handleCopy = (cols, dataRows) => {
- const csv = generateCSV(cols, dataRows);
- navigator.clipboard.writeText(csv).then(() => {
- setCopied(true);
- setTimeout(() => setCopied(false), 2000);
- });
- };
-
// Loading state
if (loading) {
return (
@@ -246,7 +229,7 @@ export default function ResultTable({ result, loading, pollingProgress }) {
Para mantener el rendimiento del navegador, los resultados que superan las {MAX_DISPLAY_ROWS} filas no se muestran en pantalla y se descargan automáticamente como CSV.
handleDownloadCSV(columns, rows)}
+ onClick={() => handleDownload(generateCSV(columns, rows), `query_result_${Date.now()}.csv`)}
className="flex items-center gap-2 px-6 py-3 rounded-lg font-medium transition-all hover:scale-105"
style={{
background: 'var(--accent)',
@@ -268,7 +251,48 @@ export default function ResultTable({ result, loading, pollingProgress }) {
);
}
- // Data table display - DARK THEME
+ // Final safety check for file content
+ const isFileContent = rows.length === 1 && columns.length === 1 && (
+ columns[0]?.toUpperCase() === 'CONTENT' ||
+ columns[0]?.toUpperCase() === 'TEXT' ||
+ columns[0]?.toUpperCase() === 'FILE' ||
+ (result?.command_id && String(result.command_id).startsWith('FILE-'))
+ );
+
+ if (isFileContent) {
+ return (
+
+
+
+
+
¡Archivo procesado con éxito!
+
+ El contenido del archivo #{result.command_id} ha sido recibido y la descarga debería haber iniciado automáticamente.
+
+
+
{
+ const rawText = Array.isArray(rows[0]) ? rows[0][0] : rows[0][columns[0]];
+ handleDownload(rawText, `file_${result.command_id}.txt`, 'text/plain');
+ }}
+ className="px-6 py-2 rounded-lg font-medium transition-all"
+ style={{ background: 'var(--accent)', color: 'white', border: 'none', cursor: 'pointer' }}
+ >
+ Descargar .txt de nuevo
+
+
+ Nueva consulta
+
+
+
+ );
+ }
+
+ // Data table display - STABLE standard view
return (
{/* Stats Header */}
@@ -293,9 +317,14 @@ export default function ResultTable({ result, loading, pollingProgress }) {
)}
- {/* Copy Button */}
handleCopy(columns, rows)}
+ onClick={() => {
+ const csv = generateCSV(columns, rows);
+ navigator.clipboard.writeText(csv).then(() => {
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ });
+ }}
className="flex items-center gap-1 px-3 py-1.5 text-xs rounded transition-colors"
style={{
background: copied ? 'var(--success)' : 'var(--border)',
@@ -303,34 +332,21 @@ export default function ResultTable({ result, loading, pollingProgress }) {
border: 'none',
cursor: 'pointer'
}}
- title="Copiar al portapapeles"
>
{copied ? : }
{copied ? 'Copiado!' : 'Copiar'}
- {/* Download CSV Button */}
handleDownloadCSV(columns, rows)}
+ onClick={() => handleDownload(generateCSV(columns, rows), `query_result_${Date.now()}.csv`)}
className="flex items-center gap-1 px-3 py-1.5 text-xs rounded transition-colors"
- style={{
- background: 'var(--accent)',
- color: 'white',
- border: 'none',
- cursor: 'pointer'
- }}
- title="Descargar CSV"
+ style={{ background: 'var(--accent)', color: 'white', border: 'none', cursor: 'pointer' }}
>
-
- CSV
+ CSV
-
-
- Completado
-
- {/* Data Table */}
+ {/* Standard Table View */}
@@ -346,12 +362,7 @@ export default function ResultTable({ result, loading, pollingProgress }) {
{rIdx + 1}
{columns.map((col, cIdx) => {
- let val;
- if (Array.isArray(row)) {
- val = row[cIdx];
- } else {
- val = row[col];
- }
+ let val = Array.isArray(row) ? row[cIdx] : row[col];
return (
{val === null ? (
@@ -359,7 +370,7 @@ export default function ResultTable({ result, loading, pollingProgress }) {
) : typeof val === 'object' ? (
{JSON.stringify(val)}
) : (
- String(val)
+ String(val).length > 200 ? String(val).substring(0, 200) + '...' : String(val)
)}
);
@@ -372,3 +383,4 @@ export default function ResultTable({ result, loading, pollingProgress }) {
);
}
+
diff --git a/src/components/StoreSelector.jsx b/src/components/StoreSelector.jsx
index f4876a6..be5fab4 100644
--- a/src/components/StoreSelector.jsx
+++ b/src/components/StoreSelector.jsx
@@ -1,6 +1,7 @@
import React, { useEffect, useState } from 'react';
import { Store, Circle, Search, Globe } from 'lucide-react';
import { fetchClientStores } from '../services/storeClientService';
+import { fetchStoreStatus } from '../services/storeStatusService';
export default function StoreSelector({ selectedStore, onSelect, onClientChange }) {
const [stores, setStores] = useState([]);
@@ -23,26 +24,54 @@ export default function StoreSelector({ selectedStore, onSelect, onClientChange
}
}, []);
- useEffect(() => {
- async function loadStores() {
- setLoading(true);
- setError(null);
- setStores([]); // Clear previous stores while loading
+ // Function to load stores and their status
+ const loadStoresWithStatus = async () => {
+ setLoading(true);
+ setError(null);
- try {
- const data = await fetchClientStores(selectedClient);
- setStores(data);
- if (data.length === 0 && selectedClient === '3') {
- // Optional user feedback for empty Colombia
- setError('No hay configuración de URL para Colombia aún.');
- }
- } catch (err) {
- setError("Error cargando tiendas: " + err.message);
- } finally {
- setLoading(false);
+ try {
+ // Fetch both store list and status in parallel
+ const [storeList, statusData] = await Promise.all([
+ fetchClientStores(selectedClient),
+ fetchStoreStatus(selectedClient)
+ ]);
+
+ // Merge store data with status information
+ const mergedStores = storeList.map(store => {
+ const statusInfo = statusData.find(s => s.store_id === parseInt(store.id));
+ return {
+ ...store,
+ connectivity: statusInfo?.connectivity || 'OFFLINE',
+ last_ping: statusInfo?.last_ping,
+ seconds_since_ping: statusInfo?.seconds_since_ping,
+ status: statusInfo?.connectivity === 'ONLINE' ? 'online' : 'offline'
+ };
+ });
+
+ setStores(mergedStores);
+
+ if (mergedStores.length === 0 && selectedClient === '3') {
+ setError('No hay configuración de URL para Colombia aún.');
}
+ } catch (err) {
+ setError("Error cargando tiendas: " + err.message);
+ } finally {
+ setLoading(false);
}
- loadStores();
+ };
+
+ // Load stores when client changes
+ useEffect(() => {
+ loadStoresWithStatus();
+ }, [selectedClient]);
+
+ // Auto-refresh status every 30 seconds
+ useEffect(() => {
+ const interval = setInterval(() => {
+ loadStoresWithStatus();
+ }, 30000); // 30 seconds
+
+ return () => clearInterval(interval);
}, [selectedClient]);
const handleClientChange = (clientId) => {
@@ -111,7 +140,11 @@ export default function StoreSelector({ selectedStore, onSelect, onClientChange
ID: {store.id} {store.extra?.turnorecojo && | {store.extra.turnorecojo} }
-
+
{
+ const handleClickOutside = (event) => {
+ if (menuRef.current && !menuRef.current.contains(event.target)) {
+ setIsOpen(false);
+ }
+ };
+
+ if (isOpen) {
+ document.addEventListener('mousedown', handleClickOutside);
+ }
+
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, [isOpen]);
+
+ const handleMenuItemClick = (action) => {
+ setIsOpen(false);
+ action();
+ };
+
+ return (
+
+
setIsOpen(!isOpen)}
+ className="btn-tools"
+ disabled={disabled}
+ title="Herramientas del Sistema"
+ >
+
+ Herramientas
+
+
+ {isOpen && (
+
+
handleMenuItemClick(onFileRequest)}
+ >
+
+ Petición Archivo
+
+
handleMenuItemClick(onBackupRequest)}
+ >
+
+ Respaldo DB
+
+
+
handleMenuItemClick(onAutoReprint)}
+ >
+
+ Reimpresión Automática
+
+
+ )}
+
+
+
+ );
+}
diff --git a/src/services/storeStatusService.js b/src/services/storeStatusService.js
new file mode 100644
index 0000000..2a078a8
--- /dev/null
+++ b/src/services/storeStatusService.js
@@ -0,0 +1,31 @@
+/**
+ * Service for fetching real-time store connectivity status
+ */
+
+const API_URL = import.meta.env.VITE_API_URL;
+
+/**
+ * Fetch store status from the backend
+ * @param {string} clientId - Client ID (2 for Peru, 3 for Colombia)
+ * @returns {Promise} Array of store status objects
+ */
+export const fetchStoreStatus = async (clientId) => {
+ try {
+ const url = `${API_URL}/stores/status?filter=all&id_cliente=${clientId}`;
+
+ const response = await fetch(url);
+
+ if (!response.ok) {
+ throw new Error(`HTTP Error: ${response.status}`);
+ }
+
+ const data = await response.json();
+
+ // Return the stores array from the response
+ return data.stores || [];
+ } catch (error) {
+ console.error('Error fetching store status:', error);
+ // Return empty array on error to prevent breaking the UI
+ return [];
+ }
+};