feat(forecast): expose DOCX & PPTX export buttons (#958) #1121
1 changed files with 32 additions and 3 deletions
|
|
@ -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 && (
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue