fix: Database schema fixes. CI/CD improvements.

This commit is contained in:
Eric Gullickson
2025-12-27 16:23:22 -06:00
parent 344df5184c
commit dc2c731119
26 changed files with 242360 additions and 481192 deletions

View File

@@ -347,11 +347,10 @@ export const useImportApply = () => {
return useMutation({
mutationFn: (previewId: string) => adminApi.importApply(previewId),
onSuccess: (result) => {
onSuccess: () => {
// Invalidate cache to refresh catalog data
queryClient.invalidateQueries({ queryKey: ['catalogSearch'] });
toast.success(
`Import completed: ${result.created} created, ${result.updated} updated`
);
// Note: Toast and dialog behavior now handled by parent components
},
onError: (error: ApiError) => {
toast.error(error.response?.data?.error || 'Failed to apply import');

View File

@@ -13,6 +13,8 @@ import {
MoreVert,
Close,
History,
ExpandMore,
ExpandLess,
} from '@mui/icons-material';
import toast from 'react-hot-toast';
import { useAdminAccess } from '../../../core/auth/useAdminAccess';
@@ -29,6 +31,7 @@ import { adminApi } from '../api/admin.api';
import {
CatalogSearchResult,
ImportPreviewResult,
ImportApplyResult,
} from '../types/admin.types';
export const AdminCatalogMobileScreen: React.FC = () => {
@@ -54,6 +57,8 @@ export const AdminCatalogMobileScreen: React.FC = () => {
// Import state
const [importSheet, setImportSheet] = useState(false);
const [importPreview, setImportPreview] = useState<ImportPreviewResult | null>(null);
const [importResult, setImportResult] = useState<ImportApplyResult | null>(null);
const [errorsExpanded, setErrorsExpanded] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
// Hooks
@@ -144,15 +149,38 @@ export const AdminCatalogMobileScreen: React.FC = () => {
if (!importPreview?.previewId) return;
try {
await importApplyMutation.mutateAsync(importPreview.previewId);
setImportSheet(false);
setImportPreview(null);
const result = await importApplyMutation.mutateAsync(importPreview.previewId);
setImportResult(result);
if (result.errors.length > 0) {
toast.error(
`Import completed with ${result.errors.length} error(s): ${result.created} created, ${result.updated} updated`
);
// Keep sheet open for error review
} else {
toast.success(
`Import completed successfully: ${result.created} created, ${result.updated} updated`
);
// Auto-close on complete success
setImportSheet(false);
setImportPreview(null);
setImportResult(null);
}
refetch();
} catch {
// Error handled by mutation
// Error handled by mutation's onError
}
}, [importPreview, importApplyMutation, refetch]);
const handleImportSheetClose = useCallback(() => {
if (importApplyMutation.isPending) return;
setImportSheet(false);
setImportPreview(null);
setImportResult(null);
setErrorsExpanded(false);
}, [importApplyMutation.isPending]);
// Export handler
const handleExport = useCallback(() => {
setMenuOpen(false);
@@ -435,17 +463,16 @@ export const AdminCatalogMobileScreen: React.FC = () => {
</div>
)}
{/* Import Preview Sheet */}
{importSheet && importPreview && (
{/* Import Preview/Results Sheet */}
{importSheet && (importPreview || importResult) && (
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-end justify-center">
<div className="bg-white rounded-t-2xl w-full max-w-lg p-6 space-y-4 animate-slide-up max-h-[80vh] overflow-y-auto">
<div className="flex items-center justify-between">
<h2 className="text-xl font-bold text-slate-800">Import Preview</h2>
<h2 className="text-xl font-bold text-slate-800">
{importResult ? 'Import Results' : 'Import Preview'}
</h2>
<button
onClick={() => {
setImportSheet(false);
setImportPreview(null);
}}
onClick={handleImportSheetClose}
disabled={importApplyMutation.isPending}
className="p-2 text-slate-500 hover:text-slate-700"
style={{ minHeight: '44px', minWidth: '44px' }}
@@ -454,74 +481,127 @@ export const AdminCatalogMobileScreen: React.FC = () => {
</button>
</div>
{/* Summary */}
<div className="flex gap-4 text-sm">
<div className="bg-green-100 text-green-800 px-3 py-2 rounded-lg">
<strong>{importPreview.toCreate.length}</strong> to create
</div>
<div className="bg-blue-100 text-blue-800 px-3 py-2 rounded-lg">
<strong>{importPreview.toUpdate.length}</strong> to update
</div>
</div>
{/* Preview Mode */}
{importPreview && !importResult && (
<>
<div className="flex gap-4 text-sm">
<div className="bg-green-100 text-green-800 px-3 py-2 rounded-lg">
<strong>{importPreview.toCreate.length}</strong> to create
</div>
<div className="bg-blue-100 text-blue-800 px-3 py-2 rounded-lg">
<strong>{importPreview.toUpdate.length}</strong> to update
</div>
</div>
{/* Errors */}
{importPreview.errors.length > 0 && (
<div className="bg-red-50 border border-red-200 rounded-lg p-3">
<p className="text-red-800 font-semibold mb-2">
{importPreview.errors.length} Error(s) Found:
</p>
<ul className="text-red-700 text-sm space-y-1">
{importPreview.errors.slice(0, 5).map((err, idx) => (
<li key={idx}>
Row {err.row}: {err.error}
</li>
))}
{importPreview.errors.length > 5 && (
<li>...and {importPreview.errors.length - 5} more errors</li>
)}
</ul>
</div>
{importPreview.errors.length > 0 && (
<div className="bg-red-50 border border-red-200 rounded-lg p-3">
<p className="text-red-800 font-semibold mb-2">
{importPreview.errors.length} Error(s) Found:
</p>
<ul className="text-red-700 text-sm space-y-1">
{importPreview.errors.slice(0, 5).map((err, idx) => (
<li key={idx}>
Row {err.row}: {err.error}
</li>
))}
{importPreview.errors.length > 5 && (
<li>...and {importPreview.errors.length - 5} more errors</li>
)}
</ul>
</div>
)}
{importPreview.valid ? (
<div className="bg-green-50 border border-green-200 rounded-lg p-3">
<p className="text-green-800">
The import file is valid and ready to be applied.
</p>
</div>
) : (
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3">
<p className="text-amber-800">
Please fix the errors above before importing.
</p>
</div>
)}
</>
)}
{/* Status */}
{importPreview.valid ? (
<div className="bg-green-50 border border-green-200 rounded-lg p-3">
<p className="text-green-800">
The import file is valid and ready to be applied.
</p>
</div>
) : (
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3">
<p className="text-amber-800">
Please fix the errors above before importing.
</p>
</div>
{/* Results Mode */}
{importResult && (
<>
<div className="flex gap-4 text-sm">
<div className="bg-green-100 text-green-800 px-3 py-2 rounded-lg">
<strong>{importResult.created}</strong> created
</div>
<div className="bg-blue-100 text-blue-800 px-3 py-2 rounded-lg">
<strong>{importResult.updated}</strong> updated
</div>
</div>
{importResult.errors.length > 0 && (
<div className="border border-red-500 rounded-lg overflow-hidden">
<button
onClick={() => setErrorsExpanded(!errorsExpanded)}
className="w-full flex items-center justify-between p-4 bg-red-100 hover:bg-red-200 transition"
style={{ minHeight: '44px' }}
>
<span className="text-red-900 font-semibold">
{importResult.errors.length} Error(s) Occurred
</span>
<span className="text-red-900">
{errorsExpanded ? <ExpandLess /> : <ExpandMore />}
</span>
</button>
{errorsExpanded && (
<div className="max-h-96 overflow-y-auto p-4 bg-white">
<ul className="space-y-2">
{importResult.errors.map((err, idx) => (
<li key={idx} className="text-sm font-mono text-slate-700">
<strong>Row {err.row}:</strong> {err.error}
</li>
))}
</ul>
</div>
)}
</div>
)}
{importResult.errors.length === 0 && (
<div className="bg-green-50 border border-green-200 rounded-lg p-3">
<p className="text-green-800">
Import completed successfully with no errors.
</p>
</div>
)}
</>
)}
{/* Action Buttons */}
<div className="flex gap-2 pt-2">
<button
onClick={() => {
setImportSheet(false);
setImportPreview(null);
}}
onClick={handleImportSheetClose}
disabled={importApplyMutation.isPending}
className="flex-1 bg-slate-200 text-slate-700 py-3 rounded-lg font-medium hover:bg-slate-300 transition disabled:opacity-50"
style={{ minHeight: '44px' }}
>
Cancel
</button>
<button
onClick={handleImportConfirm}
disabled={!importPreview.valid || importApplyMutation.isPending}
className="flex-1 bg-blue-600 text-white py-3 rounded-lg font-medium hover:bg-blue-700 transition disabled:opacity-50"
style={{ minHeight: '44px' }}
>
{importApplyMutation.isPending ? (
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white mx-auto" />
) : (
'Apply Import'
)}
{importResult ? 'Close' : 'Cancel'}
</button>
{!importResult && (
<button
onClick={handleImportConfirm}
disabled={!importPreview?.valid || importApplyMutation.isPending}
className="flex-1 bg-blue-600 text-white py-3 rounded-lg font-medium hover:bg-blue-700 transition disabled:opacity-50"
style={{ minHeight: '44px' }}
>
{importApplyMutation.isPending ? (
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white mx-auto" />
) : (
'Apply Import'
)}
</button>
)}
</div>
</div>
</div>