Compare commits

..

6 Commits

8 changed files with 82 additions and 108 deletions

View File

@@ -1,3 +1,3 @@
831167
831168
831182
834000
834001
834015

View File

@@ -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}"}
)

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,6 +1,6 @@
{
"version": "1.14.13",
"last_build": "2026-04-23-1451",
"version": "1.14.16",
"last_build": "2026-04-23-1518",
"codename": "ConfigCore",
"commit": "827dfcdf"
"commit": "b87b1ece"
}

View File

@@ -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>
<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
<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-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="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>
</div>
{/* 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">
<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
</p>
<div className="flex flex-col gap-2 sm:flex-row">
<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>
<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>
);

View File

@@ -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"
}