feat(forecast): expose DOCX & PPTX export buttons (#958) #1121

Merged
bot-backend merged 1 commit from feat/forecast-docx-pptx-buttons into main 2026-06-07 10:13:25 +00:00

View file

@ -11,12 +11,17 @@
* Backend (live on prod):
* GET /api/v1/parcels/{cad}/forecast/export?format=md text/markdown attachment
* GET /api/v1/parcels/{cad}/forecast/export?format=json application/json attachment
* GET /api/v1/parcels/{cad}/forecast/export?format=docx Word (.docx) attachment
* GET /api/v1/parcels/{cad}/forecast/export?format=pptx PowerPoint (.pptx) attachment
* GET /api/v1/parcels/{cad}/forecast/export?format=tg text/plain inline (TG snippet)
* No forecast run 404 inline «Отчёт ещё не готов» (no crash, no console spam).
*
* docx/pptx are binary zip-based OOXML they go through the SAME response.blob()
* path as md/json (never response.text(), which would corrupt the archive).
*/
import { useEffect, useRef, useState } from "react";
import { Download, Copy, Check } from "lucide-react";
import { Download, Copy, Check, FileText, Presentation } from "lucide-react";
import { API_BASE_URL } from "@/lib/api";
import { triggerDownload } from "@/lib/download";
@ -25,7 +30,9 @@ interface Props {
cad: string;
}
type ExportFormat = "md" | "json";
// Binary OOXML formats (docx/pptx) share md/json's blob download path; only the
// query param + filename extension differ, so one discriminant covers them all.
type ExportFormat = "md" | "json" | "docx" | "pptx";
const NOT_READY_MSG = "Отчёт ещё не готов";
const GENERIC_ERR_MSG = "Не удалось получить отчёт";
@ -37,7 +44,7 @@ function sanitizeCad(cad: string): string {
return cad.replace(/:/g, "_");
}
function exportUrl(cad: string, format: "md" | "json" | "tg"): string {
function exportUrl(cad: string, format: ExportFormat | "tg"): string {
return `${API_BASE_URL}/api/v1/parcels/${encodeURIComponent(
cad,
)}/forecast/export?format=${format}`;
@ -136,6 +143,8 @@ export function ForecastExportButtons({ cad }: Props) {
const mdLoading = downloadingFormat === "md";
const jsonLoading = downloadingFormat === "json";
const docxLoading = downloadingFormat === "docx";
const pptxLoading = downloadingFormat === "pptx";
const anyLoading = downloadingFormat !== null || tgLoading;
return (
@ -176,6 +185,26 @@ export function ForecastExportButtons({ cad }: Props) {
onClick={() => void handleDownload("json")}
icon={<Download size={14} strokeWidth={1.5} />}
/>
<ExportButton
label="Скачать .docx"
loadingLabel="Загрузка…"
loading={docxLoading}
disabled={anyLoading}
ariaLabel="Скачать отчёт прогноза в формате Word (.docx)"
onClick={() => void handleDownload("docx")}
icon={<FileText size={14} strokeWidth={1.5} />}
/>
<ExportButton
label="Скачать .pptx"
loadingLabel="Загрузка…"
loading={pptxLoading}
disabled={anyLoading}
ariaLabel="Скачать отчёт прогноза в формате PowerPoint (.pptx)"
onClick={() => void handleDownload("pptx")}
icon={<Presentation size={14} strokeWidth={1.5} />}
/>
</div>
{error && (