DOCX Export Progress Bar Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Add a progress modal to the Weekly Reports DOCX export feature, showing step-based percentage progress (0-100%) with status text.
Architecture: Create a reusable ExportProgressModal component that displays export progress. Modify WeeklyReportService.exportAsDOCX() to accept an optional progress callback. Update both report pages (list and edit) to show the modal during export.
Tech Stack: React 19, TypeScript 5, Tailwind CSS 4, Iconify icons, docx library
Task 1: Create ExportProgressModal Component
Files:
- Create:
src/components/shared/ExportModal/ExportProgressModal.tsx - Modify:
src/components/shared/ExportModal/index.ts
Step 1: Create the ExportProgressModal component
Create src/components/shared/ExportModal/ExportProgressModal.tsx:
/**
* ExportProgressModal - Shows progress during DOCX export
* Displays step-based percentage with status text and error handling
*/
import { Icon } from "@iconify/react";
export interface ExportProgressModalProps {
isOpen: boolean;
progress: number; // 0-100
currentStep: string; // e.g., "Processing images..."
currentDetail?: string; // e.g., "3 of 5"
error?: string | null; // Error message if failed
onRetry?: () => void; // Retry callback
onCancel: () => void; // Cancel/close callback
}
export function ExportProgressModal({
isOpen,
progress,
currentStep,
currentDetail,
error,
onRetry,
onCancel,
}: ExportProgressModalProps) {
if (!isOpen) return null;
const clampedProgress = Math.max(0, Math.min(100, progress));
const isComplete = clampedProgress >= 100 && !error;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
<div className="w-full max-w-md bg-white dark:bg-slate-800 rounded-xl shadow-2xl overflow-hidden">
{/* Header */}
<div className="px-6 py-4 border-b border-slate-200 dark:border-slate-700">
<h2 className="text-lg font-semibold text-slate-900 dark:text-slate-100">
{error ? "Export Failed" : isComplete ? "Export Complete" : "Exporting Report"}
</h2>
</div>
{/* Content */}
<div className="px-6 py-6">
{error ? (
// Error State
<div className="text-center">
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-red-100 dark:bg-red-900/20 flex items-center justify-center">
<Icon
className="w-8 h-8 text-red-600 dark:text-red-400"
icon="solar:danger-triangle-bold"
/>
</div>
<p className="text-sm text-red-600 dark:text-red-400 mb-4">
{error}
</p>
</div>
) : isComplete ? (
// Complete State
<div className="text-center">
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-green-100 dark:bg-green-900/20 flex items-center justify-center">
<Icon
className="w-8 h-8 text-green-600 dark:text-green-400"
icon="solar:check-circle-bold"
/>
</div>
<p className="text-sm text-slate-600 dark:text-slate-400">
Your report has been downloaded successfully.
</p>
</div>
) : (
// Progress State
<div className="space-y-4">
{/* Progress Bar */}
<div className="relative">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-slate-700 dark:text-slate-300">
{currentStep}
</span>
<span className="text-sm font-medium text-slate-700 dark:text-slate-300">
{Math.round(clampedProgress)}%
</span>
</div>
<div className="h-2 bg-slate-200 dark:bg-slate-700 rounded-full overflow-hidden">
<div
className="h-full bg-primary rounded-full transition-all duration-300 ease-out"
style={{ width: `${clampedProgress}%` }}
/>
</div>
{currentDetail && (
<p className="mt-2 text-xs text-slate-500 dark:text-slate-400 text-center">
{currentDetail}
</p>
)}
</div>
{/* Spinner */}
<div className="flex justify-center">
<Icon
className="w-6 h-6 text-primary animate-spin"
icon="svg-spinners:ring-resize"
/>
</div>
</div>
)}
</div>
{/* Footer */}
<div className="px-6 py-4 border-t border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-900/50 flex justify-end gap-3">
{error ? (
<>
<button
className="px-4 py-2 text-sm font-medium text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-lg transition-colors"
type="button"
onClick={onCancel}
>
Cancel
</button>
{onRetry && (
<button
className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-primary rounded-lg hover:bg-primary/90 transition-colors"
type="button"
onClick={onRetry}
>
<Icon className="w-4 h-4" icon="solar:restart-bold" />
Retry
</button>
)}
</>
) : isComplete ? (
<button
className="px-4 py-2 text-sm font-medium text-white bg-primary rounded-lg hover:bg-primary/90 transition-colors"
type="button"
onClick={onCancel}
>
Done
</button>
) : (
<button
className="px-4 py-2 text-sm font-medium text-slate-400 dark:text-slate-500 cursor-not-allowed rounded-lg"
disabled
type="button"
>
Please wait...
</button>
)}
</div>
</div>
</div>
);
}Step 2: Export from index
Modify src/components/shared/ExportModal/index.ts - add the new export:
export { BaseExportModal } from "./BaseExportModal";
export type {
ChartOption,
ModelOption,
BaseExportConfig,
BaseExportModalProps,
} from "./types";
export { ExportProgressModal } from "./ExportProgressModal";
export type { ExportProgressModalProps } from "./ExportProgressModal";Step 3: Verify build passes
Run: pnpm build
Expected: Build succeeds with no TypeScript errors.
Step 4: Commit
git add src/components/shared/ExportModal/ExportProgressModal.tsx src/components/shared/ExportModal/index.ts
git commit -m "feat(export): add ExportProgressModal component"Task 2: Add Progress Callback Type to Types File
Files:
- Modify:
src/components/shared/ExportModal/types.ts
Step 1: Add ProgressCallback type
Add to src/components/shared/ExportModal/types.ts at the end of the file:
/**
* Callback for reporting export progress
* @param progress - Percentage complete (0-100)
* @param step - Current step description
* @param detail - Optional detail (e.g., "3 of 5")
*/
export type ExportProgressCallback = (
progress: number,
step: string,
detail?: string
) => void;Step 2: Verify build passes
Run: pnpm build
Expected: Build succeeds.
Step 3: Commit
git add src/components/shared/ExportModal/types.ts
git commit -m "feat(export): add ExportProgressCallback type"Task 3: Modify WeeklyReportService.exportAsDOCX with Progress Callback
Files:
- Modify:
src/features/weekly-reports/services/weeklyReportService.ts
Step 1: Update exportAsDOCX method signature and add progress reporting
Find the exportAsDOCX method (around line 585) and replace it entirely:
/**
* Export report as DOCX file
* @param report - The report to export
* @param onProgress - Optional callback for progress updates
*/
static async exportAsDOCX(
report: WeeklyReport,
onProgress?: (progress: number, step: string, detail?: string) => void
): Promise<void> {
const reportProgress = (progress: number, step: string, detail?: string) => {
onProgress?.(progress, step, detail);
};
// Step 1: Loading libraries (0-10%)
reportProgress(0, "Loading export libraries...");
const {
Document,
Paragraph,
TextRun,
HeadingLevel,
AlignmentType,
ImageRun,
convertInchesToTwip,
} = await import("docx");
const { saveAs } = await import("file-saver");
reportProgress(10, "Building document...");
const formatDate = (dateStr: string) => {
const date = new Date(dateStr);
return date.toLocaleDateString("en-AU", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const children: any[] = [];
// Title
children.push(
new Paragraph({
text: "6.0 Report – Observations & Improvements",
heading: HeadingLevel.HEADING_1,
thematicBreak: true,
})
);
children.push(
new Paragraph({
text: "Dust Ranger Deployment – Weekly Update",
heading: HeadingLevel.HEADING_2,
})
);
children.push(
new Paragraph({
children: [
new TextRun({
text: `Reporting Period: `,
bold: true,
}),
new TextRun({
text: `Week Ending ${formatDate(report.report_period_end)}`,
}),
],
})
);
children.push(
new Paragraph({
children: [
new TextRun({
text: `Welshpool Week Starting: `,
bold: true,
}),
new TextRun({
text: formatDate(report.welshpool_week_starting),
}),
],
spacing: { after: 400 },
})
);
reportProgress(15, "Building document...");
// Section 1: Site Observations
children.push(
new Paragraph({
text: "1. Site Observations & Progression",
heading: HeadingLevel.HEADING_2,
spacing: { before: 400 },
})
);
// Count total images for progress tracking
let totalImages = 0;
let processedImages = 0;
if (report.site_observations.length > 0) {
for (const site of report.site_observations) {
for (const device of site.devices) {
if (device.observations) {
// Count images in observations (rough estimate based on img tags)
const imgMatches = device.observations.match(/<img[^>]+>/g);
if (imgMatches) {
totalImages += imgMatches.length;
}
}
}
}
}
reportProgress(20, "Processing site observations...");
if (report.site_observations.length > 0) {
for (const site of report.site_observations) {
children.push(
new Paragraph({
text: site.site_name,
heading: HeadingLevel.HEADING_3,
})
);
if (site.site_level_notes) {
children.push(
new Paragraph({
text: site.site_level_notes,
spacing: { after: 200 },
})
);
}
if (site.devices.length > 0) {
for (const device of site.devices) {
const deviceText = [
new TextRun({ text: `${device.device_id}`, bold: true }),
];
if (device.status) {
deviceText.push(
new TextRun({
text: ` - Status: ${device.status.replace(/_/g, " ")}`,
})
);
}
children.push(
new Paragraph({
children: deviceText,
bullet: { level: 0 },
})
);
if (device.observations) {
try {
// Update progress before processing each device's images
const deviceImgCount = (device.observations.match(/<img[^>]+>/g) || []).length;
const observationBlocks =
await WeeklyReportService.buildObservationDocxBlocks(
device.observations,
convertInchesToTwip(0.5),
ImageRun
);
children.push(...observationBlocks);
// Update progress after processing images
processedImages += deviceImgCount;
if (totalImages > 0) {
// Images processing is 25-85% of total progress
const imageProgress = 25 + (processedImages / totalImages) * 60;
reportProgress(
Math.round(imageProgress),
"Processing images...",
`${processedImages} of ${totalImages}`
);
}
} catch (error) {
console.error(
"Failed to process device observations for DOCX:",
device.device_id,
error
);
}
}
}
}
}
} else {
children.push(
new Paragraph({
children: [
new TextRun({
text: "No site observations for this period.",
italics: true,
}),
],
})
);
}
reportProgress(85, "Generating file...");
// Section 2: Flow Meter Usage
children.push(
new Paragraph({
text: "2. Flow Meter Usage Updates",
heading: HeadingLevel.HEADING_2,
spacing: { before: 400 },
})
);
if (report.flow_meter_date_range) {
children.push(
new Paragraph({
children: [
new TextRun({
text: "Data Period: ",
bold: true,
}),
new TextRun({
text: `${formatDate(report.flow_meter_date_range.start_date)} - ${formatDate(report.flow_meter_date_range.end_date)}`,
}),
],
spacing: { after: 200 },
})
);
}
if (report.flow_meter_usage.length > 0) {
report.flow_meter_usage.forEach((usage) => {
const usageText = [
new TextRun({ text: `${usage.site_name}: `, bold: true }),
new TextRun({ text: usage.usage_dates }),
];
if (usage.volume_used) {
usageText.push(new TextRun({ text: `, ${usage.volume_used}` }));
}
if (usage.notes) {
usageText.push(new TextRun({ text: ` - ${usage.notes}` }));
}
children.push(
new Paragraph({
children: usageText,
bullet: { level: 0 },
})
);
});
} else {
children.push(
new Paragraph({
children: [
new TextRun({
text: "No flow meter usage for this period.",
italics: true,
}),
],
})
);
}
// Section 3: Dashboard Updates
children.push(
new Paragraph({
text: "3. Dashboard Updates & Issues",
heading: HeadingLevel.HEADING_2,
spacing: { before: 400 },
})
);
if (report.dashboard_updates.length > 0) {
report.dashboard_updates.forEach((update) => {
children.push(
new Paragraph({
text: update.category,
heading: HeadingLevel.HEADING_3,
})
);
children.push(
new Paragraph({
text: update.description,
spacing: { after: 200 },
})
);
});
} else {
children.push(
new Paragraph({
children: [
new TextRun({
text: "No dashboard updates for this period.",
italics: true,
}),
],
})
);
}
// Section 4: Vendor Activities
children.push(
new Paragraph({
text: "4. Connect Source / Other Vendors",
heading: HeadingLevel.HEADING_2,
spacing: { before: 400 },
})
);
if (report.vendor_activities.length > 0) {
report.vendor_activities.forEach((activity) => {
children.push(
new Paragraph({
text: `${activity.vendor_name} - ${activity.activity_type}`,
heading: HeadingLevel.HEADING_3,
})
);
children.push(
new Paragraph({
text: activity.description,
spacing: { after: 200 },
})
);
});
} else {
children.push(
new Paragraph({
children: [
new TextRun({
text: "No vendor activities for this period.",
italics: true,
}),
],
})
);
}
// Section 5: Water Truck Testing
children.push(
new Paragraph({
text: "5. Water Truck Project Testing",
heading: HeadingLevel.HEADING_2,
spacing: { before: 400 },
})
);
if (report.water_truck_testing.sites) {
children.push(
new Paragraph({
children: [
new TextRun({ text: "Site(s) / location(s): ", bold: true }),
new TextRun({ text: report.water_truck_testing.sites }),
],
bullet: { level: 0 },
})
);
children.push(
new Paragraph({
children: [
new TextRun({ text: "Hardware used: ", bold: true }),
new TextRun({ text: report.water_truck_testing.hardware }),
],
bullet: { level: 0 },
})
);
children.push(
new Paragraph({
children: [
new TextRun({
text: "Test summary:",
bold: true,
}),
],
bullet: { level: 0 },
})
);
children.push(
new Paragraph({
children: [
new TextRun({ text: "What was tested: ", bold: true }),
new TextRun({
text: report.water_truck_testing.test_summary.what_tested,
}),
],
bullet: { level: 1 },
})
);
children.push(
new Paragraph({
children: [
new TextRun({ text: "Key issues observed: ", bold: true }),
new TextRun({
text: report.water_truck_testing.test_summary.issues_observed,
}),
],
bullet: { level: 1 },
})
);
children.push(
new Paragraph({
children: [
new TextRun({ text: "Requested improvements: ", bold: true }),
new TextRun({
text: report.water_truck_testing.test_summary
.improvements_requested,
}),
],
bullet: { level: 1 },
})
);
children.push(
new Paragraph({
children: [
new TextRun({ text: "Next steps: ", bold: true }),
new TextRun({
text: report.water_truck_testing.test_summary.next_steps,
}),
],
bullet: { level: 1 },
})
);
} else {
children.push(
new Paragraph({
children: [
new TextRun({
text: "No water truck testing for this period.",
italics: true,
}),
],
})
);
}
// Section 6: Hardware & Installations
children.push(
new Paragraph({
text: "6. Hardware & Installations",
heading: HeadingLevel.HEADING_2,
spacing: { before: 400 },
})
);
if (report.hardware_installations.length > 0) {
report.hardware_installations.forEach((hw) => {
children.push(
new Paragraph({
children: [
new TextRun({ text: hw.type, bold: true }),
new TextRun({ text: ` at ${hw.location}: ${hw.details}` }),
],
bullet: { level: 0 },
})
);
});
} else {
children.push(
new Paragraph({
children: [
new TextRun({
text: "No hardware installations for this period.",
italics: true,
}),
],
})
);
}
// Section 7: Admin & Reporting
children.push(
new Paragraph({
text: "7. Admin & Reporting",
heading: HeadingLevel.HEADING_2,
spacing: { before: 400 },
})
);
if (report.admin_reporting.travel.length > 0) {
children.push(
new Paragraph({
text: "Travel & logistics:",
heading: HeadingLevel.HEADING_3,
})
);
report.admin_reporting.travel.forEach((item) => {
children.push(
new Paragraph({
children: [
new TextRun({ text: `${item.type}: `, bold: true }),
new TextRun({ text: item.details }),
],
bullet: { level: 0 },
})
);
});
}
if (report.admin_reporting.reporting.length > 0) {
children.push(
new Paragraph({
text: "Reporting & records:",
heading: HeadingLevel.HEADING_3,
})
);
report.admin_reporting.reporting.forEach((item) => {
children.push(
new Paragraph({
children: [
new TextRun({ text: `${item.type}: `, bold: true }),
new TextRun({ text: item.details }),
],
bullet: { level: 0 },
})
);
});
}
if (
report.admin_reporting.travel.length === 0 &&
report.admin_reporting.reporting.length === 0
) {
children.push(
new Paragraph({
children: [
new TextRun({
text: "No admin activities for this period.",
italics: true,
}),
],
})
);
}
// Section 8: Other Tasks
children.push(
new Paragraph({
text: "8. Other Tasks Completed",
heading: HeadingLevel.HEADING_2,
spacing: { before: 400 },
})
);
let hasOtherTasks = false;
if (report.other_tasks.site_support.length > 0) {
children.push(
new Paragraph({
text: "Site support:",
heading: HeadingLevel.HEADING_3,
})
);
report.other_tasks.site_support.forEach((task) => {
children.push(
new Paragraph({
text: task.description,
bullet: { level: 0 },
})
);
});
hasOtherTasks = true;
}
if (report.other_tasks.stakeholder.length > 0) {
children.push(
new Paragraph({
text: "Stakeholder engagement:",
heading: HeadingLevel.HEADING_3,
})
);
report.other_tasks.stakeholder.forEach((task) => {
children.push(
new Paragraph({
text: task.description,
bullet: { level: 0 },
})
);
});
hasOtherTasks = true;
}
if (report.other_tasks.internal.length > 0) {
children.push(
new Paragraph({
text: "Internal improvement:",
heading: HeadingLevel.HEADING_3,
})
);
report.other_tasks.internal.forEach((task) => {
children.push(
new Paragraph({
text: task.description,
bullet: { level: 0 },
})
);
});
hasOtherTasks = true;
}
if (!hasOtherTasks) {
children.push(
new Paragraph({
children: [
new TextRun({
text: "No other tasks for this period.",
italics: true,
}),
],
})
);
}
reportProgress(90, "Generating file...");
// Create document
const doc = new Document({
sections: [
{
properties: {},
children,
},
],
});
// Generate and save
const { Packer } = await import("docx");
const blob = await Packer.toBlob(doc);
reportProgress(95, "Starting download...");
const filename = `Weekly_Report_${report.report_period_end}.docx`;
saveAs(blob, filename);
reportProgress(100, "Complete!");
}Step 2: Verify build passes
Run: pnpm build
Expected: Build succeeds with no TypeScript errors.
Step 3: Commit
git add src/features/weekly-reports/services/weeklyReportService.ts
git commit -m "feat(export): add progress callback to exportAsDOCX"Task 4: Integrate Progress Modal into Report List Page
Files:
- Modify:
src/app/(admin)/(pages)/report-template/index.tsx
Step 1: Add progress modal state and handlers
Add these imports at the top of the file (after existing imports):
import { ExportProgressModal } from "@/components/shared/ExportModal";Add these state variables inside the ReportTemplatePage component (after the existing state declarations around line 16-23):
// DOCX export progress state
const [exportProgress, setExportProgress] = useState(0);
const [exportStep, setExportStep] = useState("");
const [exportDetail, setExportDetail] = useState<string | undefined>();
const [exportError, setExportError] = useState<string | null>(null);
const [showExportModal, setShowExportModal] = useState(false);
const [exportingReport, setExportingReport] = useState<WeeklyReport | null>(null);Step 2: Replace the handleExportDOCX function
Find and replace the handleExportDOCX function (around line 111-118):
const handleExportDOCX = async (report: WeeklyReport) => {
// Reset state
setExportProgress(0);
setExportStep("Initializing...");
setExportDetail(undefined);
setExportError(null);
setExportingReport(report);
setShowExportModal(true);
try {
await WeeklyReportService.exportAsDOCX(report, (progress, step, detail) => {
setExportProgress(progress);
setExportStep(step);
setExportDetail(detail);
});
} catch (error) {
console.error("Failed to export as DOCX:", error);
setExportError(
error instanceof Error
? error.message
: "Failed to export as DOCX. Please try again."
);
}
};
const handleExportRetry = () => {
if (exportingReport) {
void handleExportDOCX(exportingReport);
}
};
const handleExportCancel = () => {
setShowExportModal(false);
setExportingReport(null);
setExportError(null);
};Step 3: Add the ExportProgressModal component
Add this JSX right before the closing </> of the return statement (before line 503):
{/* Export Progress Modal */}
<ExportProgressModal
currentDetail={exportDetail}
currentStep={exportStep}
error={exportError}
isOpen={showExportModal}
progress={exportProgress}
onCancel={handleExportCancel}
onRetry={handleExportRetry}
/>Step 4: Verify build passes
Run: pnpm build
Expected: Build succeeds.
Step 5: Commit
git add src/app/\(admin\)/\(pages\)/report-template/index.tsx
git commit -m "feat(export): add progress modal to report list page"Task 5: Integrate Progress Modal into Report Edit Page
Files:
- Modify:
src/app/(admin)/(pages)/report-template/edit/[id].tsx
Step 1: Add progress modal imports and state
Add this import at the top (after existing imports):
import { ExportProgressModal } from "@/components/shared/ExportModal";Add these state variables inside the EditReportPage component (after line 28 where exporting state is declared):
// DOCX export progress state
const [exportProgress, setExportProgress] = useState(0);
const [exportStep, setExportStep] = useState("");
const [exportDetail, setExportDetail] = useState<string | undefined>();
const [exportError, setExportError] = useState<string | null>(null);
const [showExportModal, setShowExportModal] = useState(false);Step 2: Replace the handleExport function
Find and replace the handleExport function (around line 96-125):
const handleExport = async () => {
// Reset state and show modal
setExportProgress(0);
setExportStep("Initializing...");
setExportDetail(undefined);
setExportError(null);
setShowExportModal(true);
setExporting(true);
try {
// Build a WeeklyReport object from current state
const report: WeeklyReport = {
id: id || "",
user_id: user?.id || "",
report_period_end: reportPeriodEnd,
welshpool_week_starting: welshpoolWeekStarting,
status: "draft",
site_observations: siteObservations,
flow_meter_usage: flowMeterUsage,
flow_meter_date_range: flowMeterDateRange,
dashboard_updates: dashboardUpdates,
vendor_activities: vendorActivities,
water_truck_testing: waterTruckTesting,
hardware_installations: hardwareInstallations,
admin_reporting: adminReporting,
other_tasks: otherTasks,
created_at: "",
updated_at: "",
};
await WeeklyReportService.exportAsDOCX(report, (progress, step, detail) => {
setExportProgress(progress);
setExportStep(step);
setExportDetail(detail);
});
} catch (error) {
console.error("Failed to export report:", error);
setExportError(
error instanceof Error
? error.message
: "Failed to export report. Please try again."
);
} finally {
setExporting(false);
}
};
const handleExportRetry = () => {
void handleExport();
};
const handleExportCancel = () => {
setShowExportModal(false);
setExportError(null);
};Step 3: Add the ExportProgressModal component
Add this JSX right before the closing </main> tag (before line 376):
{/* Export Progress Modal */}
<ExportProgressModal
currentDetail={exportDetail}
currentStep={exportStep}
error={exportError}
isOpen={showExportModal}
progress={exportProgress}
onCancel={handleExportCancel}
onRetry={handleExportRetry}
/>Step 4: Verify build passes
Run: pnpm build
Expected: Build succeeds.
Step 5: Commit
git add src/app/\(admin\)/\(pages\)/report-template/edit/\[id\].tsx
git commit -m "feat(export): add progress modal to report edit page"Task 6: Final Verification
Step 1: Run full build
Run: pnpm build
Expected: Build succeeds with no errors.
Step 2: Run linter
Run: pnpm lint
Expected: No new lint errors introduced.
Step 3: Start dev server and manual test
Run: pnpm dev
Manual verification steps:
Navigate to
/report-templateClick "Export" on any report, then "Download as DOCX"
Verify progress modal appears with percentage and step text
Verify modal shows "Complete!" state when finished
Click "Done" to close
Navigate to
/report-template/edit/{id}for any reportClick "Export DOCX" button
Verify same progress modal behavior
Step 4: Final commit
git add -A
git commit -m "feat(export): complete DOCX export progress bar implementation"Summary
This implementation adds:
- ExportProgressModal - A reusable modal component showing progress percentage, step text, and error handling
- Progress callback in exportAsDOCX - Reports progress at key stages (loading libraries, building document, processing images, generating file, download)
- Integration in report list page - Shows progress when exporting from the reports table
- Integration in report edit page - Shows progress when exporting from the edit view
Progress stages:
- 0-10%: Loading libraries
- 10-25%: Building document structure
- 25-85%: Processing images (with "X of Y" detail)
- 85-95%: Generating file
- 95-100%: Starting download