Merge branch 'dev'

This commit is contained in:
2026-04-23 15:08:46 +03:00
6 changed files with 77 additions and 88 deletions

View File

@@ -1,3 +1,3 @@
831167
831168
831182
831457
831458
831472

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

View File

@@ -1,6 +1,6 @@
{
"version": "1.14.13",
"last_build": "2026-04-23-1451",
"version": "1.14.14",
"last_build": "2026-04-23-1508",
"codename": "ConfigCore",
"commit": "827dfcdf"
"commit": "6f9716c3"
}

View File

@@ -1,136 +1,131 @@
"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="p-4 md:p-6 bg-surface border border-slate-800 rounded-3xl space-y-4 md:space-y-6">
<header className="flex items-center gap-3">
<div className="p-2 md:p-3 bg-emerald-500/10 rounded-2xl text-emerald-400 border border-emerald-500/20 shadow-inner">
<FileDown size={24} className="md:w-7 md:h-7" />
</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
<h2 className="text-xl md:text-2xl font-normal text-white">Export & Reports</h2>
<p className="text-[10px] md:text-xs text-muted font-normal tracking-tight mt-0.5">
Download inventory snapshots and audit trails
</p>
</div>
</div>
</header>
<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">
<div className="p-4 bg-slate-800/30 border border-slate-800 rounded-2xl space-y-4">
<div>
<h3 className="text-sm font-normal text-secondary">Inventory Snapshot</h3>
<p className="text-xs text-muted mt-0.5">
Export current inventory state with all item details
</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"
className="bg-surface border border-slate-800 hover:bg-slate-800/50 text-white font-normal py-2 px-3 rounded-xl transition-all active:scale-[0.98] disabled:opacity-50 disabled:pointer-events-none flex items-center justify-center gap-2 text-xs focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none"
aria-label="Export inventory snapshot as CSV"
>
{isLoading ? (
<Loader2 size={16} className="animate-spin" />
<Loader2 size={14} className="animate-spin" />
) : (
<FileDown size={16} />
<FileText size={14} />
)}
{isLoading ? "Exporting..." : "Export as CSV"}
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"
className="bg-surface border border-slate-800 hover:bg-slate-800/50 text-white font-normal py-2 px-3 rounded-xl transition-all active:scale-[0.98] disabled:opacity-50 disabled:pointer-events-none flex items-center justify-center gap-2 text-xs focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none"
aria-label="Export inventory snapshot as Excel"
>
{isLoading ? (
<Loader2 size={16} className="animate-spin" />
<Loader2 size={14} className="animate-spin" />
) : (
<FileDown size={16} />
<FileSpreadsheet size={14} />
)}
{isLoading ? "Exporting..." : "Export as Excel"}
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-slate-800/30 border border-slate-800 rounded-2xl space-y-4">
<div>
<h3 className="text-sm font-normal text-secondary">Audit Trail</h3>
<p className="text-xs text-muted mt-0.5">
Complete log of system actions and changes
</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"
className="bg-surface border border-slate-800 hover:bg-slate-800/50 text-white font-normal py-2 px-3 rounded-xl transition-all active:scale-[0.98] disabled:opacity-50 disabled:pointer-events-none flex items-center justify-center gap-2 text-xs focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none"
aria-label="Export audit trail as CSV"
>
{isLoading ? (
<Loader2 size={16} className="animate-spin" />
<Loader2 size={14} className="animate-spin" />
) : (
<FileDown size={16} />
<FileText size={14} />
)}
{isLoading ? "Exporting..." : "Export as CSV"}
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"
className="bg-surface border border-slate-800 hover:bg-slate-800/50 text-white font-normal py-2 px-3 rounded-xl transition-all active:scale-[0.98] disabled:opacity-50 disabled:pointer-events-none flex items-center justify-center gap-2 text-xs focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none"
aria-label="Export audit trail as Excel"
>
{isLoading ? (
<Loader2 size={16} className="animate-spin" />
<Loader2 size={14} className="animate-spin" />
) : (
<FileDown size={16} />
<FileSpreadsheet size={14} />
)}
{isLoading ? "Exporting..." : "Export as Excel"}
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="p-3 bg-rose-500/10 border border-rose-500/20 rounded-xl text-rose-500 text-xs flex items-center gap-2">
<span>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"
}