Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d22f1e85e | |||
| b87b1eceae | |||
| f2fb81024e | |||
| 76f0e83a65 | |||
| c85a7b78a6 | |||
| 6f9716c32b | |||
| 96f4a5d228 | |||
| 827dfcdf2f | |||
| 9afe335384 | |||
| c14a28b093 | |||
| 568bccb37e | |||
| e5a24df13d |
@@ -1,3 +1,3 @@
|
|||||||
825448
|
834000
|
||||||
825449
|
834001
|
||||||
825463
|
834015
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import yaml
|
import yaml
|
||||||
import logging
|
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")
|
log = logging.getLogger("ainventory")
|
||||||
|
|
||||||
@@ -52,8 +52,8 @@ class ConfigManager:
|
|||||||
log.info(f"✅ Updated {path} with new values.")
|
log.info(f"✅ Updated {path} with new values.")
|
||||||
|
|
||||||
# Reload the global config
|
# Reload the global config
|
||||||
load_config()
|
loader_load_config()
|
||||||
return get_config()
|
return loader_get_config()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"❌ Failed to write {path}: {e}")
|
log.error(f"❌ Failed to write {path}: {e}")
|
||||||
raise
|
raise
|
||||||
@@ -72,10 +72,78 @@ class ConfigManager:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return False
|
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
|
@staticmethod
|
||||||
def get_masked_key(key_name: str):
|
def get_masked_key(key_name: str):
|
||||||
"""Returns a masked version of a configuration value or env var."""
|
"""Returns a masked version of a configuration value or env var."""
|
||||||
config = get_config()
|
config = loader_get_config()
|
||||||
|
|
||||||
# Try to find in config first
|
# Try to find in config first
|
||||||
val = None
|
val = None
|
||||||
@@ -113,6 +181,10 @@ def update_config(updates: dict) -> dict:
|
|||||||
"""Update backend.yaml with new values and return updated config."""
|
"""Update backend.yaml with new values and return updated config."""
|
||||||
return ConfigManager.update_config(updates)
|
return ConfigManager.update_config(updates)
|
||||||
|
|
||||||
|
def get_config() -> dict:
|
||||||
|
"""Return the current global configuration."""
|
||||||
|
return loader_get_config()
|
||||||
|
|
||||||
def validate_config_file() -> bool:
|
def validate_config_file() -> bool:
|
||||||
"""Validate backend.yaml syntax and required fields."""
|
"""Validate backend.yaml syntax and required fields."""
|
||||||
return ConfigManager.validate_config_file()
|
return ConfigManager.validate_config_file()
|
||||||
|
|||||||
@@ -67,8 +67,11 @@ def get_ai_config(
|
|||||||
current_admin: auth.TokenData = Depends(auth.get_current_admin)
|
current_admin: auth.TokenData = Depends(auth.get_current_admin)
|
||||||
):
|
):
|
||||||
"""Check AI provider status and active provider."""
|
"""Check AI provider status and active provider."""
|
||||||
gemini_key = os.environ.get("GEMINI_API_KEY")
|
config = ConfigManager.get_config() # Alias for config_loader.get_config
|
||||||
claude_key = os.environ.get("CLAUDE_API_KEY")
|
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()
|
provider_setting = db.query(models.SystemSetting).filter(models.SystemSetting.key == "ai_provider").first()
|
||||||
active_provider = provider_setting.value if provider_setting else "gemini"
|
active_provider = provider_setting.value if provider_setting else "gemini"
|
||||||
@@ -112,10 +115,14 @@ def update_ai_keys(
|
|||||||
if updates:
|
if updates:
|
||||||
ConfigManager.update_keys(updates)
|
ConfigManager.update_keys(updates)
|
||||||
|
|
||||||
|
# Re-load config to return accurate status
|
||||||
|
config = ConfigManager.get_config()
|
||||||
|
ai_cfg = config.get("ai", {})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"gemini_configured": bool(os.environ.get("GEMINI_API_KEY")),
|
"gemini_configured": bool(ai_cfg.get("gemini_api_key")),
|
||||||
"claude_configured": bool(os.environ.get("CLAUDE_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 datetime import datetime
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import StreamingResponse
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from backend.database import get_db
|
from backend.database import get_db
|
||||||
@@ -61,7 +61,7 @@ async def export_inventory_snapshot(
|
|||||||
# Log export action
|
# Log export action
|
||||||
from backend.models import AuditLog
|
from backend.models import AuditLog
|
||||||
audit_entry = AuditLog(
|
audit_entry = AuditLog(
|
||||||
user_id=admin_user.id,
|
user_id=admin_user.sub,
|
||||||
action="EXPORT_INVENTORY_SNAPSHOT",
|
action="EXPORT_INVENTORY_SNAPSHOT",
|
||||||
details=f"Exported in {format_type} format",
|
details=f"Exported in {format_type} format",
|
||||||
)
|
)
|
||||||
@@ -70,20 +70,16 @@ async def export_inventory_snapshot(
|
|||||||
|
|
||||||
# Return file response
|
# Return file response
|
||||||
if format_type == "csv":
|
if format_type == "csv":
|
||||||
# For CSV, use FileResponse with bytes
|
return StreamingResponse(
|
||||||
import io
|
|
||||||
return FileResponse(
|
|
||||||
io.BytesIO(content.encode("utf-8")),
|
io.BytesIO(content.encode("utf-8")),
|
||||||
media_type=media_type,
|
media_type=media_type,
|
||||||
filename=filename,
|
headers={"Content-Disposition": f"attachment; filename={filename}"}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# For Excel, content is already bytes
|
return StreamingResponse(
|
||||||
import io
|
|
||||||
return FileResponse(
|
|
||||||
io.BytesIO(content),
|
io.BytesIO(content),
|
||||||
media_type=media_type,
|
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
|
# Log export action
|
||||||
audit_entry = AuditLog(
|
audit_entry = AuditLog(
|
||||||
user_id=admin_user.id,
|
user_id=admin_user.sub,
|
||||||
action="EXPORT_AUDIT_TRAIL",
|
action="EXPORT_AUDIT_TRAIL",
|
||||||
details=f"Exported in {format_type} format",
|
details=f"Exported in {format_type} format",
|
||||||
)
|
)
|
||||||
@@ -126,22 +122,20 @@ async def export_audit_trail(
|
|||||||
|
|
||||||
# Return file response
|
# Return file response
|
||||||
if format_type == "csv":
|
if format_type == "csv":
|
||||||
import io
|
return StreamingResponse(
|
||||||
return FileResponse(
|
|
||||||
io.BytesIO(content.encode("utf-8")),
|
io.BytesIO(content.encode("utf-8")),
|
||||||
media_type=media_type,
|
media_type=media_type,
|
||||||
filename=filename,
|
headers={"Content-Disposition": f"attachment; filename={filename}"}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
import io
|
return StreamingResponse(
|
||||||
return FileResponse(
|
|
||||||
io.BytesIO(content),
|
io.BytesIO(content),
|
||||||
media_type=media_type,
|
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(
|
async def export_db(
|
||||||
format: str = Query("csv", description="Export format: csv or xlsx"),
|
format: str = Query("csv", description="Export format: csv or xlsx"),
|
||||||
type: str = Query("inventory", description="Export type: inventory, audit, or combined"),
|
type: str = Query("inventory", description="Export type: inventory, audit, or combined"),
|
||||||
@@ -217,7 +211,7 @@ async def export_db(
|
|||||||
|
|
||||||
# Log export action
|
# Log export action
|
||||||
audit_entry = AuditLog(
|
audit_entry = AuditLog(
|
||||||
user_id=admin_user.id,
|
user_id=admin_user.sub,
|
||||||
action="EXPORT_DB",
|
action="EXPORT_DB",
|
||||||
details=f"Exported {type} in {format_type} format",
|
details=f"Exported {type} in {format_type} format",
|
||||||
)
|
)
|
||||||
@@ -226,14 +220,14 @@ async def export_db(
|
|||||||
|
|
||||||
# Return file response
|
# Return file response
|
||||||
if format_type == "csv":
|
if format_type == "csv":
|
||||||
return FileResponse(
|
return StreamingResponse(
|
||||||
io.BytesIO(content.encode("utf-8")),
|
io.BytesIO(content.encode("utf-8")),
|
||||||
media_type=media_type,
|
media_type=media_type,
|
||||||
filename=filename,
|
headers={"Content-Disposition": f"attachment; filename={filename}"}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return FileResponse(
|
return StreamingResponse(
|
||||||
io.BytesIO(content),
|
io.BytesIO(content),
|
||||||
media_type=media_type,
|
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
|
# Environment: BACKEND_APPLICATION_LOGS_DIR or LOGS_DIR
|
||||||
logs_dir: "./logs"
|
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 ---
|
# --- Feature Flags ---
|
||||||
features:
|
features:
|
||||||
# Enable AI image extraction and OCR?
|
# Enable AI image extraction and OCR?
|
||||||
|
|||||||
@@ -6,11 +6,9 @@
|
|||||||
|
|
||||||
# --- API Connection ---
|
# --- API Connection ---
|
||||||
api:
|
api:
|
||||||
# Base URL of the backend API
|
# Note: backend_url is dynamically injected by the launcher script
|
||||||
# Default: http://localhost:8916
|
# based on network.yaml settings to ensure SSOT integrity.
|
||||||
# Environment: FRONTEND_API_BACKEND_URL or BACKEND_URL
|
|
||||||
backend_url: "http://localhost:8916"
|
|
||||||
|
|
||||||
# API timeout in milliseconds
|
# API timeout in milliseconds
|
||||||
# Default: 30000 (30 seconds)
|
# Default: 30000 (30 seconds)
|
||||||
# Environment: FRONTEND_API_TIMEOUT_MS
|
# Environment: FRONTEND_API_TIMEOUT_MS
|
||||||
|
|||||||
@@ -38,36 +38,18 @@ ssl:
|
|||||||
certificate_path: ""
|
certificate_path: ""
|
||||||
|
|
||||||
# Path to SSL private key
|
# Path to SSL private key
|
||||||
# Environment: NETWORK_SSL_KEY_PATH
|
# Path to SSL private key
|
||||||
key_path: ""
|
key_path: ""
|
||||||
|
|
||||||
# --- Proxy (Caddy) Configuration ---
|
# --- Master Infrastructure Settings ---
|
||||||
proxy:
|
# Establish this file as the SSOT for network topology.
|
||||||
# Caddy log level (debug|info|warn|error)
|
application:
|
||||||
# Default: info
|
# The IP address or hostname of the server (Used for CORS and Frontend API)
|
||||||
# Environment: NETWORK_PROXY_CADDY_LOG_LEVEL
|
# Default: localhost
|
||||||
caddy_log_level: "info"
|
# Environment: SERVER_IP
|
||||||
|
server_ip: "localhost"
|
||||||
|
|
||||||
# Maximum timeout for reading requests in seconds
|
# Comma-separated list of extra allowed CORS origins (IPs/FQDNs/Subnets)
|
||||||
# Default: 60
|
# Environment: EXTRA_ALLOWED_ORIGINS
|
||||||
# Environment: NETWORK_PROXY_READ_TIMEOUT_S
|
cors_origins: ""
|
||||||
proxy_read_timeout_s: 60
|
|
||||||
|
|
||||||
# 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",
|
"version": "1.14.16",
|
||||||
"last_build": "2026-04-23-1358",
|
"last_build": "2026-04-23-1518",
|
||||||
"codename": "ConfigCore",
|
"codename": "ConfigCore",
|
||||||
"commit": "a5a460e7"
|
"commit": "b87b1ece"
|
||||||
}
|
}
|
||||||
@@ -1,136 +1,116 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { FileDown, Loader2 } from "lucide-react";
|
import { FileDown, Loader2, FileSpreadsheet, FileText } from "lucide-react";
|
||||||
import { useExport } from "@/hooks/useExport";
|
import { useExport } from "@/hooks/useExport";
|
||||||
import { Toast } from "@/components/Toast";
|
import { toast } from "react-hot-toast";
|
||||||
|
|
||||||
export function ExportPanel() {
|
export function ExportPanel() {
|
||||||
const { exportSnapshot, exportAuditTrail, isLoading, error } = useExport();
|
const { exportSnapshot, exportAuditTrail, isLoading, error } = useExport();
|
||||||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const handleExportSnapshot = async (format: "csv" | "xlsx") => {
|
const handleExportSnapshot = async (format: "csv" | "xlsx") => {
|
||||||
try {
|
try {
|
||||||
setSuccessMessage(null);
|
|
||||||
await exportSnapshot(format);
|
await exportSnapshot(format);
|
||||||
const formatName = format === "csv" ? "CSV" : "Excel";
|
const formatName = format === "csv" ? "CSV" : "Excel";
|
||||||
setSuccessMessage(`Inventory snapshot exported as ${formatName}`);
|
toast.success(`Inventory snapshot exported as ${formatName}`);
|
||||||
setTimeout(() => setSuccessMessage(null), 4000);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Error handled by useExport hook
|
// Error handled by hook
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleExportAuditTrail = async (format: "csv" | "xlsx") => {
|
const handleExportAuditTrail = async (format: "csv" | "xlsx") => {
|
||||||
try {
|
try {
|
||||||
setSuccessMessage(null);
|
|
||||||
await exportAuditTrail(format);
|
await exportAuditTrail(format);
|
||||||
const formatName = format === "csv" ? "CSV" : "Excel";
|
const formatName = format === "csv" ? "CSV" : "Excel";
|
||||||
setSuccessMessage(`Audit trail exported as ${formatName}`);
|
toast.success(`Audit trail exported as ${formatName}`);
|
||||||
setTimeout(() => setSuccessMessage(null), 4000);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Error handled by useExport hook
|
// Error handled by hook
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<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="rounded-lg border border-slate-200 bg-white p-6">
|
<div className="flex items-center gap-3 mb-4 md:mb-6">
|
||||||
<div className="mb-6 flex items-center gap-3">
|
<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">
|
||||||
<div className="rounded-lg bg-primary/10 p-4 border border-primary/20">
|
<FileDown size={20} />
|
||||||
<FileDown className="text-primary" size={24} />
|
</div>
|
||||||
</div>
|
<div>
|
||||||
<div>
|
<h2 className="text-xl font-normal text-white tracking-tight">Export & Reports</h2>
|
||||||
<h2 className="text-2xl font-normal">Export & Reports</h2>
|
<p className="text-[10px] md:text-xs text-muted font-normal tracking-tight">Download system snapshots and action logs</p>
|
||||||
<p className="text-xs text-slate-500">
|
</div>
|
||||||
Download inventory snapshots and audit trails in CSV or Excel formats
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
|
{/* Inventory Snapshot Section */}
|
||||||
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
{/* 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
|
|
||||||
</p>
|
|
||||||
<div className="flex flex-col gap-2 sm:flex-row">
|
|
||||||
<button
|
<button
|
||||||
onClick={() => handleExportSnapshot("csv")}
|
onClick={() => handleExportSnapshot("csv")}
|
||||||
disabled={isLoading}
|
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"
|
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"
|
||||||
aria-label="Export inventory snapshot as CSV"
|
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? <Loader2 size={12} className="animate-spin" /> : <FileText size={12} />}
|
||||||
<Loader2 size={16} className="animate-spin" />
|
CSV
|
||||||
) : (
|
|
||||||
<FileDown size={16} />
|
|
||||||
)}
|
|
||||||
{isLoading ? "Exporting..." : "Export as CSV"}
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleExportSnapshot("xlsx")}
|
onClick={() => handleExportSnapshot("xlsx")}
|
||||||
disabled={isLoading}
|
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"
|
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"
|
||||||
aria-label="Export inventory snapshot as Excel"
|
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? <Loader2 size={12} className="animate-spin" /> : <FileSpreadsheet size={12} />}
|
||||||
<Loader2 size={16} className="animate-spin" />
|
Excel
|
||||||
) : (
|
|
||||||
<FileDown size={16} />
|
|
||||||
)}
|
|
||||||
{isLoading ? "Exporting..." : "Export as Excel"}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Audit Trail Section */}
|
{/* Audit Trail Section */}
|
||||||
<div className="space-y-3">
|
<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">
|
||||||
<h3 className="font-normal text-slate-700">Audit Trail</h3>
|
<div className="mb-4">
|
||||||
<p className="text-sm text-slate-500">
|
<h3 className="text-sm font-normal text-secondary flex items-center gap-2">
|
||||||
Export complete audit log of all system actions and changes
|
<FileText size={14} className="text-indigo-400" />
|
||||||
</p>
|
Audit Trail
|
||||||
<div className="flex flex-col gap-2 sm:flex-row">
|
</h3>
|
||||||
|
<p className="text-xs text-muted mt-1 leading-relaxed">
|
||||||
|
Complete history of system actions, changes, and user operations.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleExportAuditTrail("csv")}
|
onClick={() => handleExportAuditTrail("csv")}
|
||||||
disabled={isLoading}
|
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"
|
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"
|
||||||
aria-label="Export audit trail as CSV"
|
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? <Loader2 size={12} className="animate-spin" /> : <FileText size={12} />}
|
||||||
<Loader2 size={16} className="animate-spin" />
|
CSV
|
||||||
) : (
|
|
||||||
<FileDown size={16} />
|
|
||||||
)}
|
|
||||||
{isLoading ? "Exporting..." : "Export as CSV"}
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleExportAuditTrail("xlsx")}
|
onClick={() => handleExportAuditTrail("xlsx")}
|
||||||
disabled={isLoading}
|
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"
|
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"
|
||||||
aria-label="Export audit trail as Excel"
|
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? <Loader2 size={12} className="animate-spin" /> : <FileSpreadsheet size={12} />}
|
||||||
<Loader2 size={16} className="animate-spin" />
|
Excel
|
||||||
) : (
|
|
||||||
<FileDown size={16} />
|
|
||||||
)}
|
|
||||||
{isLoading ? "Exporting..." : "Export as Excel"}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Status Messages */}
|
|
||||||
{error && (
|
{error && (
|
||||||
<Toast
|
<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">
|
||||||
type="error"
|
<div className="w-1.5 h-1.5 bg-rose-500 rounded-full animate-pulse" />
|
||||||
message={`Export failed: ${error}`}
|
<span className="font-normal truncate">Error: {error}</span>
|
||||||
onClose={() => {}}
|
</div>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{successMessage && (
|
|
||||||
<Toast type="success" message={successMessage} onClose={() => {}} />
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export function useExport(): UseExportReturn {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axiosInstance.get(
|
const response = await axiosInstance.get(
|
||||||
`/admin/db/export?format=${format}&type=inventory`,
|
`/admin/reports/export?format=${format}&type=inventory`,
|
||||||
{
|
{
|
||||||
responseType: "blob"
|
responseType: "blob"
|
||||||
}
|
}
|
||||||
@@ -83,7 +83,7 @@ export function useExport(): UseExportReturn {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axiosInstance.get(
|
const response = await axiosInstance.get(
|
||||||
`/admin/db/export?format=${format}&type=audit`,
|
`/admin/reports/export?format=${format}&type=audit`,
|
||||||
{
|
{
|
||||||
responseType: "blob"
|
responseType: "blob"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user