Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d22f1e85e | |||
| b87b1eceae | |||
| f2fb81024e | |||
| 76f0e83a65 | |||
| c85a7b78a6 | |||
| 6f9716c32b | |||
| 96f4a5d228 | |||
| 827dfcdf2f | |||
| 9afe335384 | |||
| c14a28b093 | |||
| 568bccb37e | |||
| e5a24df13d |
@@ -1,3 +1,3 @@
|
||||
825448
|
||||
825449
|
||||
825463
|
||||
834000
|
||||
834001
|
||||
834015
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import os
|
||||
import yaml
|
||||
import logging
|
||||
from .config_loader import load_config, get_config
|
||||
from .config_loader import load_config as loader_load_config, get_config as loader_get_config
|
||||
|
||||
log = logging.getLogger("ainventory")
|
||||
|
||||
@@ -52,8 +52,8 @@ class ConfigManager:
|
||||
log.info(f"✅ Updated {path} with new values.")
|
||||
|
||||
# Reload the global config
|
||||
load_config()
|
||||
return get_config()
|
||||
loader_load_config()
|
||||
return loader_get_config()
|
||||
except Exception as e:
|
||||
log.error(f"❌ Failed to write {path}: {e}")
|
||||
raise
|
||||
@@ -72,10 +72,78 @@ class ConfigManager:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get_secrets_path():
|
||||
"""Returns the absolute path to secrets.yaml."""
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
project_root = os.path.dirname(base_dir)
|
||||
return os.path.join(project_root, "config", "secrets.yaml")
|
||||
|
||||
@staticmethod
|
||||
def update_secrets(updates: dict) -> dict:
|
||||
"""Update secrets.yaml with new values (flattened structure)."""
|
||||
path = ConfigManager.get_secrets_path()
|
||||
|
||||
current_secrets = {}
|
||||
if os.path.exists(path):
|
||||
try:
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
current_secrets = yaml.safe_load(f) or {}
|
||||
except Exception as e:
|
||||
log.error(f"❌ Failed to read {path}: {e}")
|
||||
|
||||
# Update flattened secrets
|
||||
current_secrets.update(updates)
|
||||
|
||||
try:
|
||||
with open(path, 'w', encoding='utf-8') as f:
|
||||
# Header for clarity
|
||||
f.write("# TFM aInventory - Managed Secrets (Updated via Admin UI)\n")
|
||||
yaml.dump(current_secrets, f, default_flow_style=False, sort_keys=True)
|
||||
log.info(f"✅ Updated {path} with new secrets.")
|
||||
|
||||
# Reload the global config
|
||||
loader_load_config()
|
||||
return loader_get_config()
|
||||
except Exception as e:
|
||||
log.error(f"❌ Failed to write {path}: {e}")
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def update_keys(updates: dict) -> dict:
|
||||
"""
|
||||
Intelligent key update: routes sensitive AI keys to secrets.yaml
|
||||
and others to backend.yaml.
|
||||
"""
|
||||
secrets_updates = {}
|
||||
backend_updates = {}
|
||||
|
||||
sensitive_keys = ["GEMINI_API_KEY", "CLAUDE_API_KEY", "JWT_SECRET_KEY", "LDAP_PASSWORD"]
|
||||
|
||||
for key, val in updates.items():
|
||||
if key in sensitive_keys:
|
||||
secrets_updates[key] = val
|
||||
else:
|
||||
# Non-sensitive or complex nested settings
|
||||
backend_updates[key] = val
|
||||
|
||||
if secrets_updates:
|
||||
ConfigManager.update_secrets(secrets_updates)
|
||||
|
||||
if backend_updates:
|
||||
ConfigManager.update_config(backend_updates)
|
||||
|
||||
return loader_get_config()
|
||||
|
||||
@staticmethod
|
||||
def get_config() -> dict:
|
||||
"""Return the current global configuration."""
|
||||
return loader_get_config()
|
||||
|
||||
@staticmethod
|
||||
def get_masked_key(key_name: str):
|
||||
"""Returns a masked version of a configuration value or env var."""
|
||||
config = get_config()
|
||||
config = loader_get_config()
|
||||
|
||||
# Try to find in config first
|
||||
val = None
|
||||
@@ -113,6 +181,10 @@ def update_config(updates: dict) -> dict:
|
||||
"""Update backend.yaml with new values and return updated config."""
|
||||
return ConfigManager.update_config(updates)
|
||||
|
||||
def get_config() -> dict:
|
||||
"""Return the current global configuration."""
|
||||
return loader_get_config()
|
||||
|
||||
def validate_config_file() -> bool:
|
||||
"""Validate backend.yaml syntax and required fields."""
|
||||
return ConfigManager.validate_config_file()
|
||||
|
||||
@@ -67,8 +67,11 @@ def get_ai_config(
|
||||
current_admin: auth.TokenData = Depends(auth.get_current_admin)
|
||||
):
|
||||
"""Check AI provider status and active provider."""
|
||||
gemini_key = os.environ.get("GEMINI_API_KEY")
|
||||
claude_key = os.environ.get("CLAUDE_API_KEY")
|
||||
config = ConfigManager.get_config() # Alias for config_loader.get_config
|
||||
ai_config = config.get("ai", {})
|
||||
|
||||
gemini_key = ai_config.get("gemini_api_key")
|
||||
claude_key = ai_config.get("claude_api_key")
|
||||
|
||||
provider_setting = db.query(models.SystemSetting).filter(models.SystemSetting.key == "ai_provider").first()
|
||||
active_provider = provider_setting.value if provider_setting else "gemini"
|
||||
@@ -112,10 +115,14 @@ def update_ai_keys(
|
||||
if updates:
|
||||
ConfigManager.update_keys(updates)
|
||||
|
||||
# Re-load config to return accurate status
|
||||
config = ConfigManager.get_config()
|
||||
ai_cfg = config.get("ai", {})
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"gemini_configured": bool(os.environ.get("GEMINI_API_KEY")),
|
||||
"claude_configured": bool(os.environ.get("CLAUDE_API_KEY"))
|
||||
"gemini_configured": bool(ai_cfg.get("gemini_api_key")),
|
||||
"claude_configured": bool(ai_cfg.get("claude_api_key"))
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ Supports CSV and Excel formats.
|
||||
|
||||
from datetime import datetime
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from backend.database import get_db
|
||||
@@ -61,7 +61,7 @@ async def export_inventory_snapshot(
|
||||
# Log export action
|
||||
from backend.models import AuditLog
|
||||
audit_entry = AuditLog(
|
||||
user_id=admin_user.id,
|
||||
user_id=admin_user.sub,
|
||||
action="EXPORT_INVENTORY_SNAPSHOT",
|
||||
details=f"Exported in {format_type} format",
|
||||
)
|
||||
@@ -70,20 +70,16 @@ async def export_inventory_snapshot(
|
||||
|
||||
# Return file response
|
||||
if format_type == "csv":
|
||||
# For CSV, use FileResponse with bytes
|
||||
import io
|
||||
return FileResponse(
|
||||
return StreamingResponse(
|
||||
io.BytesIO(content.encode("utf-8")),
|
||||
media_type=media_type,
|
||||
filename=filename,
|
||||
headers={"Content-Disposition": f"attachment; filename={filename}"}
|
||||
)
|
||||
else:
|
||||
# For Excel, content is already bytes
|
||||
import io
|
||||
return FileResponse(
|
||||
return StreamingResponse(
|
||||
io.BytesIO(content),
|
||||
media_type=media_type,
|
||||
filename=filename,
|
||||
headers={"Content-Disposition": f"attachment; filename={filename}"}
|
||||
)
|
||||
|
||||
|
||||
@@ -117,7 +113,7 @@ async def export_audit_trail(
|
||||
|
||||
# Log export action
|
||||
audit_entry = AuditLog(
|
||||
user_id=admin_user.id,
|
||||
user_id=admin_user.sub,
|
||||
action="EXPORT_AUDIT_TRAIL",
|
||||
details=f"Exported in {format_type} format",
|
||||
)
|
||||
@@ -126,22 +122,20 @@ async def export_audit_trail(
|
||||
|
||||
# Return file response
|
||||
if format_type == "csv":
|
||||
import io
|
||||
return FileResponse(
|
||||
return StreamingResponse(
|
||||
io.BytesIO(content.encode("utf-8")),
|
||||
media_type=media_type,
|
||||
filename=filename,
|
||||
headers={"Content-Disposition": f"attachment; filename={filename}"}
|
||||
)
|
||||
else:
|
||||
import io
|
||||
return FileResponse(
|
||||
return StreamingResponse(
|
||||
io.BytesIO(content),
|
||||
media_type=media_type,
|
||||
filename=filename,
|
||||
headers={"Content-Disposition": f"attachment; filename={filename}"}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/db/export")
|
||||
@router.get("/reports/export")
|
||||
async def export_db(
|
||||
format: str = Query("csv", description="Export format: csv or xlsx"),
|
||||
type: str = Query("inventory", description="Export type: inventory, audit, or combined"),
|
||||
@@ -217,7 +211,7 @@ async def export_db(
|
||||
|
||||
# Log export action
|
||||
audit_entry = AuditLog(
|
||||
user_id=admin_user.id,
|
||||
user_id=admin_user.sub,
|
||||
action="EXPORT_DB",
|
||||
details=f"Exported {type} in {format_type} format",
|
||||
)
|
||||
@@ -226,14 +220,14 @@ async def export_db(
|
||||
|
||||
# Return file response
|
||||
if format_type == "csv":
|
||||
return FileResponse(
|
||||
return StreamingResponse(
|
||||
io.BytesIO(content.encode("utf-8")),
|
||||
media_type=media_type,
|
||||
filename=filename,
|
||||
headers={"Content-Disposition": f"attachment; filename={filename}"}
|
||||
)
|
||||
else:
|
||||
return FileResponse(
|
||||
return StreamingResponse(
|
||||
io.BytesIO(content),
|
||||
media_type=media_type,
|
||||
filename=filename,
|
||||
headers={"Content-Disposition": f"attachment; filename={filename}"}
|
||||
)
|
||||
|
||||
BIN
backups/ainventory_2026-04-23_13-58-34.tar.gz
Normal file
BIN
backups/ainventory_2026-04-23_13-58-34.tar.gz
Normal file
Binary file not shown.
BIN
backups/ainventory_2026-04-23_14-04-08.tar.gz
Normal file
BIN
backups/ainventory_2026-04-23_14-04-08.tar.gz
Normal file
Binary file not shown.
BIN
backups/ainventory_2026-04-23_14-45-07.tar.gz
Normal file
BIN
backups/ainventory_2026-04-23_14-45-07.tar.gz
Normal file
Binary file not shown.
BIN
backups/ainventory_2026-04-23_14-51-52.tar.gz
Normal file
BIN
backups/ainventory_2026-04-23_14-51-52.tar.gz
Normal file
Binary file not shown.
BIN
backups/ainventory_2026-04-23_15-08-46.tar.gz
Normal file
BIN
backups/ainventory_2026-04-23_15-08-46.tar.gz
Normal file
Binary file not shown.
BIN
backups/ainventory_2026-04-23_15-15-00.tar.gz
Normal file
BIN
backups/ainventory_2026-04-23_15-15-00.tar.gz
Normal file
Binary file not shown.
@@ -111,10 +111,6 @@ application:
|
||||
# Environment: BACKEND_APPLICATION_LOGS_DIR or LOGS_DIR
|
||||
logs_dir: "./logs"
|
||||
|
||||
# Comma-separated list of extra allowed CORS origins (IPs/FQDNs)
|
||||
# Environment: BACKEND_APPLICATION_CORS_ORIGINS or EXTRA_ALLOWED_ORIGINS
|
||||
cors_origins: "http://localhost:8917"
|
||||
|
||||
# --- Feature Flags ---
|
||||
features:
|
||||
# Enable AI image extraction and OCR?
|
||||
|
||||
@@ -6,10 +6,8 @@
|
||||
|
||||
# --- API Connection ---
|
||||
api:
|
||||
# Base URL of the backend API
|
||||
# Default: http://localhost:8916
|
||||
# Environment: FRONTEND_API_BACKEND_URL or BACKEND_URL
|
||||
backend_url: "http://localhost:8916"
|
||||
# Note: backend_url is dynamically injected by the launcher script
|
||||
# based on network.yaml settings to ensure SSOT integrity.
|
||||
|
||||
# API timeout in milliseconds
|
||||
# Default: 30000 (30 seconds)
|
||||
|
||||
@@ -38,36 +38,18 @@ ssl:
|
||||
certificate_path: ""
|
||||
|
||||
# Path to SSL private key
|
||||
# Environment: NETWORK_SSL_KEY_PATH
|
||||
# Path to SSL private key
|
||||
key_path: ""
|
||||
|
||||
# --- Proxy (Caddy) Configuration ---
|
||||
proxy:
|
||||
# Caddy log level (debug|info|warn|error)
|
||||
# Default: info
|
||||
# Environment: NETWORK_PROXY_CADDY_LOG_LEVEL
|
||||
caddy_log_level: "info"
|
||||
# --- Master Infrastructure Settings ---
|
||||
# Establish this file as the SSOT for network topology.
|
||||
application:
|
||||
# The IP address or hostname of the server (Used for CORS and Frontend API)
|
||||
# Default: localhost
|
||||
# Environment: SERVER_IP
|
||||
server_ip: "localhost"
|
||||
|
||||
# Maximum timeout for reading requests in seconds
|
||||
# Default: 60
|
||||
# Environment: NETWORK_PROXY_READ_TIMEOUT_S
|
||||
proxy_read_timeout_s: 60
|
||||
# Comma-separated list of extra allowed CORS origins (IPs/FQDNs/Subnets)
|
||||
# Environment: EXTRA_ALLOWED_ORIGINS
|
||||
cors_origins: ""
|
||||
|
||||
# Maximum request body size in MB
|
||||
# Default: 10
|
||||
# Environment: NETWORK_PROXY_MAX_REQUEST_SIZE_MB
|
||||
max_request_size_mb: 10
|
||||
|
||||
# --- CORS Policies ---
|
||||
cors:
|
||||
# Comma-separated list of allowed origins
|
||||
# Environment: NETWORK_CORS_ALLOWED_ORIGINS or EXTRA_ALLOWED_ORIGINS
|
||||
allowed_origins: "*"
|
||||
|
||||
# Allowed HTTP methods
|
||||
# Environment: NETWORK_CORS_ALLOWED_METHODS
|
||||
allowed_methods: "GET,POST,PUT,DELETE,OPTIONS"
|
||||
|
||||
# Allowed HTTP headers
|
||||
# Environment: NETWORK_CORS_ALLOWED_HEADERS
|
||||
allowed_headers: "Authorization,Content-Type,Accept"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": "1.14.10",
|
||||
"last_build": "2026-04-23-1358",
|
||||
"version": "1.14.16",
|
||||
"last_build": "2026-04-23-1518",
|
||||
"codename": "ConfigCore",
|
||||
"commit": "a5a460e7"
|
||||
"commit": "b87b1ece"
|
||||
}
|
||||
@@ -1,136 +1,116 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { FileDown, Loader2 } from "lucide-react";
|
||||
import { FileDown, Loader2, FileSpreadsheet, FileText } from "lucide-react";
|
||||
import { useExport } from "@/hooks/useExport";
|
||||
import { Toast } from "@/components/Toast";
|
||||
import { toast } from "react-hot-toast";
|
||||
|
||||
export function ExportPanel() {
|
||||
const { exportSnapshot, exportAuditTrail, isLoading, error } = useExport();
|
||||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||
|
||||
const handleExportSnapshot = async (format: "csv" | "xlsx") => {
|
||||
try {
|
||||
setSuccessMessage(null);
|
||||
await exportSnapshot(format);
|
||||
const formatName = format === "csv" ? "CSV" : "Excel";
|
||||
setSuccessMessage(`Inventory snapshot exported as ${formatName}`);
|
||||
setTimeout(() => setSuccessMessage(null), 4000);
|
||||
toast.success(`Inventory snapshot exported as ${formatName}`);
|
||||
} catch (err) {
|
||||
// Error handled by useExport hook
|
||||
// Error handled by hook
|
||||
}
|
||||
};
|
||||
|
||||
const handleExportAuditTrail = async (format: "csv" | "xlsx") => {
|
||||
try {
|
||||
setSuccessMessage(null);
|
||||
await exportAuditTrail(format);
|
||||
const formatName = format === "csv" ? "CSV" : "Excel";
|
||||
setSuccessMessage(`Audit trail exported as ${formatName}`);
|
||||
setTimeout(() => setSuccessMessage(null), 4000);
|
||||
toast.success(`Audit trail exported as ${formatName}`);
|
||||
} catch (err) {
|
||||
// Error handled by useExport hook
|
||||
// Error handled by hook
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="rounded-lg border border-slate-200 bg-white p-6">
|
||||
<div className="mb-6 flex items-center gap-3">
|
||||
<div className="rounded-lg bg-primary/10 p-4 border border-primary/20">
|
||||
<FileDown className="text-primary" size={24} />
|
||||
<div data-testid="admin-tab-exports" className="bg-surface/50 border border-slate-800/50 rounded-[2.5rem] p-4 md:p-6 flex flex-col shadow-2xl transition-all h-full">
|
||||
<div className="flex items-center gap-3 mb-4 md:mb-6">
|
||||
<div className="w-10 h-10 rounded-xl bg-indigo-500/10 flex items-center justify-center text-indigo-400 border border-indigo-500/20">
|
||||
<FileDown size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-normal">Export & Reports</h2>
|
||||
<p className="text-xs text-slate-500">
|
||||
Download inventory snapshots and audit trails in CSV or Excel formats
|
||||
</p>
|
||||
<h2 className="text-xl font-normal text-white tracking-tight">Export & Reports</h2>
|
||||
<p className="text-[10px] md:text-xs text-muted font-normal tracking-tight">Download system snapshots and action logs</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
{/* Inventory Snapshot Section */}
|
||||
<div className="mb-8 space-y-3">
|
||||
<h3 className="font-normal text-slate-700">Inventory Snapshot</h3>
|
||||
<p className="text-sm text-slate-500">
|
||||
Export current inventory state with all item details
|
||||
<div className="p-4 bg-background/40 border border-slate-800/40 rounded-2xl hover:border-primary/20 transition-all group flex flex-col justify-between">
|
||||
<div className="mb-4">
|
||||
<h3 className="text-sm font-normal text-secondary flex items-center gap-2">
|
||||
<FileSpreadsheet size={14} className="text-primary" />
|
||||
Inventory Snapshot
|
||||
</h3>
|
||||
<p className="text-xs text-muted mt-1 leading-relaxed">
|
||||
Export current inventory state with all item details and locations.
|
||||
</p>
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
onClick={() => handleExportSnapshot("csv")}
|
||||
disabled={isLoading}
|
||||
className="flex items-center justify-center gap-2 rounded-md bg-blue-500 px-4 py-2 text-sm font-normal text-white hover:bg-blue-600 disabled:bg-slate-400 disabled:cursor-not-allowed transition-colors"
|
||||
aria-label="Export inventory snapshot as CSV"
|
||||
className="bg-surface border border-slate-800 hover:text-primary hover:border-primary/30 rounded-xl py-2 px-3 text-xs font-normal text-white transition-all active:scale-[0.98] disabled:opacity-50 flex items-center justify-center gap-2"
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 size={16} className="animate-spin" />
|
||||
) : (
|
||||
<FileDown size={16} />
|
||||
)}
|
||||
{isLoading ? "Exporting..." : "Export as CSV"}
|
||||
{isLoading ? <Loader2 size={12} className="animate-spin" /> : <FileText size={12} />}
|
||||
CSV
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleExportSnapshot("xlsx")}
|
||||
disabled={isLoading}
|
||||
className="flex items-center justify-center gap-2 rounded-md bg-green-500 px-4 py-2 text-sm font-normal text-white hover:bg-green-600 disabled:bg-slate-400 disabled:cursor-not-allowed transition-colors"
|
||||
aria-label="Export inventory snapshot as Excel"
|
||||
className="bg-surface border border-slate-800 hover:text-emerald-400 hover:border-emerald-500/30 rounded-xl py-2 px-3 text-xs font-normal text-white transition-all active:scale-[0.98] disabled:opacity-50 flex items-center justify-center gap-2"
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 size={16} className="animate-spin" />
|
||||
) : (
|
||||
<FileDown size={16} />
|
||||
)}
|
||||
{isLoading ? "Exporting..." : "Export as Excel"}
|
||||
{isLoading ? <Loader2 size={12} className="animate-spin" /> : <FileSpreadsheet size={12} />}
|
||||
Excel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Audit Trail Section */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-normal text-slate-700">Audit Trail</h3>
|
||||
<p className="text-sm text-slate-500">
|
||||
Export complete audit log of all system actions and changes
|
||||
<div className="p-4 bg-background/40 border border-slate-800/40 rounded-2xl hover:border-primary/20 transition-all group flex flex-col justify-between">
|
||||
<div className="mb-4">
|
||||
<h3 className="text-sm font-normal text-secondary flex items-center gap-2">
|
||||
<FileText size={14} className="text-indigo-400" />
|
||||
Audit Trail
|
||||
</h3>
|
||||
<p className="text-xs text-muted mt-1 leading-relaxed">
|
||||
Complete history of system actions, changes, and user operations.
|
||||
</p>
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
onClick={() => handleExportAuditTrail("csv")}
|
||||
disabled={isLoading}
|
||||
className="flex items-center justify-center gap-2 rounded-md bg-blue-500 px-4 py-2 text-sm font-normal text-white hover:bg-blue-600 disabled:bg-slate-400 disabled:cursor-not-allowed transition-colors"
|
||||
aria-label="Export audit trail as CSV"
|
||||
className="bg-surface border border-slate-800 hover:text-primary hover:border-primary/30 rounded-xl py-2 px-3 text-xs font-normal text-white transition-all active:scale-[0.98] disabled:opacity-50 flex items-center justify-center gap-2"
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 size={16} className="animate-spin" />
|
||||
) : (
|
||||
<FileDown size={16} />
|
||||
)}
|
||||
{isLoading ? "Exporting..." : "Export as CSV"}
|
||||
{isLoading ? <Loader2 size={12} className="animate-spin" /> : <FileText size={12} />}
|
||||
CSV
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleExportAuditTrail("xlsx")}
|
||||
disabled={isLoading}
|
||||
className="flex items-center justify-center gap-2 rounded-md bg-green-500 px-4 py-2 text-sm font-normal text-white hover:bg-green-600 disabled:bg-slate-400 disabled:cursor-not-allowed transition-colors"
|
||||
aria-label="Export audit trail as Excel"
|
||||
className="bg-surface border border-slate-800 hover:text-emerald-400 hover:border-emerald-500/30 rounded-xl py-2 px-3 text-xs font-normal text-white transition-all active:scale-[0.98] disabled:opacity-50 flex items-center justify-center gap-2"
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 size={16} className="animate-spin" />
|
||||
) : (
|
||||
<FileDown size={16} />
|
||||
)}
|
||||
{isLoading ? "Exporting..." : "Export as Excel"}
|
||||
{isLoading ? <Loader2 size={12} className="animate-spin" /> : <FileSpreadsheet size={12} />}
|
||||
Excel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Messages */}
|
||||
{error && (
|
||||
<Toast
|
||||
type="error"
|
||||
message={`Export failed: ${error}`}
|
||||
onClose={() => {}}
|
||||
/>
|
||||
)}
|
||||
{successMessage && (
|
||||
<Toast type="success" message={successMessage} onClose={() => {}} />
|
||||
<div className="mt-4 p-3 bg-rose-500/10 border border-rose-500/20 rounded-xl text-rose-500 text-[10px] flex items-center gap-2 animate-in slide-in-from-top-2 duration-300">
|
||||
<div className="w-1.5 h-1.5 bg-rose-500 rounded-full animate-pulse" />
|
||||
<span className="font-normal truncate">Error: {error}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -50,7 +50,7 @@ export function useExport(): UseExportReturn {
|
||||
|
||||
try {
|
||||
const response = await axiosInstance.get(
|
||||
`/admin/db/export?format=${format}&type=inventory`,
|
||||
`/admin/reports/export?format=${format}&type=inventory`,
|
||||
{
|
||||
responseType: "blob"
|
||||
}
|
||||
@@ -83,7 +83,7 @@ export function useExport(): UseExportReturn {
|
||||
|
||||
try {
|
||||
const response = await axiosInstance.get(
|
||||
`/admin/db/export?format=${format}&type=audit`,
|
||||
`/admin/reports/export?format=${format}&type=audit`,
|
||||
{
|
||||
responseType: "blob"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user