From aca353126ecaf21eb0ff6584d1e024b0401dac87 Mon Sep 17 00:00:00 2001 From: Paul Woelfel Date: Fri, 12 Jan 2024 08:37:29 +0100 Subject: [PATCH] Add CSV export --- src/components/firebase/FirecallExport.tsx | 40 +----------- src/components/firebase/download.tsx | 74 ++++++++++++++++++++++ src/components/inputs/DownloadButton.tsx | 19 ++++++ src/components/pages/EinsatzTagebuch.tsx | 42 ++++++++++-- src/components/pages/Fahrzeuge.tsx | 38 ++++++++++- src/components/pages/Geschaeftsbuch.tsx | 42 +++++++++++- 6 files changed, 209 insertions(+), 46 deletions(-) create mode 100644 src/components/firebase/download.tsx create mode 100644 src/components/inputs/DownloadButton.tsx diff --git a/src/components/firebase/FirecallExport.tsx b/src/components/firebase/FirecallExport.tsx index b86d85b..a2f6e77 100644 --- a/src/components/firebase/FirecallExport.tsx +++ b/src/components/firebase/FirecallExport.tsx @@ -1,50 +1,12 @@ import DownloadIcon from '@mui/icons-material/Download'; import { IconButton } from '@mui/material'; -import { useCallback } from 'react'; import { exportFirecall } from '../../hooks/useExport'; +import { downloadBlob } from './download'; export interface FirecallExportProps { firecallId: string; } -export function downloadBlob(blob: Blob | MediaSource, filename: string) { - // Create an object URL for the blob object - const url = URL.createObjectURL(blob); - - // Create a new anchor element - const a = document.createElement('a'); - - // Set the href and download attributes for the anchor element - // You can optionally set other attributes like `title`, etc - // Especially, if the anchor element will be attached to the DOM - a.href = url; - a.download = filename || 'download'; - - // Click handler that releases the object URL after the element has been clicked - // This is required for one-off downloads of the blob content - const clickHandler = () => { - setTimeout(() => { - URL.revokeObjectURL(url); - removeEventListener('click', clickHandler); - }, 150); - }; - - // Add the click event listener on the anchor element - // Comment out this line if you don't want a one-off download of the blob content - a.addEventListener('click', clickHandler, false); - - // Programmatically trigger a click on the anchor element - // Useful if you want the download to happen automatically - // Without attaching the anchor element to the DOM - // Comment out this line if you don't want an automatic download of the blob content - a.click(); - - // Return the anchor element - // Useful if you want a reference to the element - // in order to attach it to the DOM or use it in some other way - return a; -} - async function exportAndDownloadFirecall(firecallId: string) { const firecallData = await exportFirecall(firecallId); const blob = new Blob([JSON.stringify(firecallData)], { diff --git a/src/components/firebase/download.tsx b/src/components/firebase/download.tsx new file mode 100644 index 0000000..411b080 --- /dev/null +++ b/src/components/firebase/download.tsx @@ -0,0 +1,74 @@ +/** + * download a Blob in the browser + * + * const blob = new Blob([JSON.stringify(firecallData)], { + type: 'application/json', + }); + downloadBlob(blob, `firecall-export-${firecallId}.json`); + * @param blob Blob to download + * @param filename filename to present to the browser + * @returns a HTMLAnchorelemnt used for the download + */ +export function downloadBlob(blob: Blob | MediaSource, filename: string) { + // Create an object URL for the blob object + const url = URL.createObjectURL(blob); + + // Create a new anchor element + const a = document.createElement('a'); + + // Set the href and download attributes for the anchor element + // You can optionally set other attributes like `title`, etc + // Especially, if the anchor element will be attached to the DOM + a.href = url; + a.download = filename || 'download'; + + // Click handler that releases the object URL after the element has been clicked + // This is required for one-off downloads of the blob content + const clickHandler = () => { + setTimeout(() => { + URL.revokeObjectURL(url); + removeEventListener('click', clickHandler); + }, 150); + }; + + // Add the click event listener on the anchor element + // Comment out this line if you don't want a one-off download of the blob content + a.addEventListener('click', clickHandler, false); + + // Programmatically trigger a click on the anchor element + // Useful if you want the download to happen automatically + // Without attaching the anchor element to the DOM + // Comment out this line if you don't want an automatic download of the blob content + a.click(); + + // Return the anchor element + // Useful if you want a reference to the element + // in order to attach it to the DOM or use it in some other way + return a; +} + +/** + * Download a string as file in the browser + * + * @param text file contents as string + * @param filename filename to download + * @param mimeType mimetype of the file + * @returns HTMLAnchor element used for the download + */ +export function downloadText(text: string, filename: string, mimeType: string) { + const blob = new Blob([text], { + type: mimeType, + }); + return downloadBlob(blob, filename); +} + +export function downloadRowsAsCsv(rows: any[][], filename: string) { + return downloadText( + rows + .map((row) => row.map((v) => (v ? '' + v : ''))) + .map((r) => JSON.stringify(r).replace(/^\[/, '').replace(/\]/, '')) + .join('\n'), + filename, + 'text/csv' + ); +} diff --git a/src/components/inputs/DownloadButton.tsx b/src/components/inputs/DownloadButton.tsx new file mode 100644 index 0000000..2ac71d6 --- /dev/null +++ b/src/components/inputs/DownloadButton.tsx @@ -0,0 +1,19 @@ +import DownloadIcon from '@mui/icons-material/Download'; +import { IconButton, IconButtonProps, Tooltip } from '@mui/material'; + +export interface DownloadButtonProps extends IconButtonProps { + tooltip: string; +} + +export function DownloadButton({ + tooltip, + ...iconButtonProps +}: DownloadButtonProps) { + return ( + + + + + + ); +} diff --git a/src/components/pages/EinsatzTagebuch.tsx b/src/components/pages/EinsatzTagebuch.tsx index cbccaed..fe83687 100644 --- a/src/components/pages/EinsatzTagebuch.tsx +++ b/src/components/pages/EinsatzTagebuch.tsx @@ -3,7 +3,6 @@ import DeleteIcon from '@mui/icons-material/Delete'; import EditIcon from '@mui/icons-material/Edit'; import Box from '@mui/material/Box'; import Fab from '@mui/material/Fab'; -import Grid from '@mui/material/Grid'; import IconButton from '@mui/material/IconButton'; import Tooltip from '@mui/material/Tooltip'; import Typography from '@mui/material/Typography'; @@ -11,14 +10,18 @@ import { DataGrid, GridColDef } from '@mui/x-data-grid'; import { addDoc, collection } from 'firebase/firestore'; import moment from 'moment'; import { useCallback, useEffect, useState } from 'react'; -import { dateTimeFormat, parseTimestamp } from '../../common/time-format'; +import { + dateTimeFormat, + formatTimestamp, + parseTimestamp, +} from '../../common/time-format'; import useFirebaseCollection from '../../hooks/useFirebaseCollection'; import useFirebaseLogin from '../../hooks/useFirebaseLogin'; import { useFirecallId } from '../../hooks/useFirecall'; import DeleteFirecallItemDialog from '../FirecallItems/DeleteFirecallItemDialog'; -import FirecallItemCard from '../FirecallItems/FirecallItemCard'; import FirecallItemDialog from '../FirecallItems/FirecallItemDialog'; import FirecallItemUpdateDialog from '../FirecallItems/FirecallItemUpdateDialog'; +import { downloadRowsAsCsv } from '../firebase/download'; import { firestore } from '../firebase/firebase'; import { Diary, @@ -26,6 +29,7 @@ import { Fzg, filterActiveItems, } from '../firebase/firestore'; +import { DownloadButton } from '../inputs/DownloadButton'; export function useDiaries() { const firecallId = useFirecallId(); @@ -144,6 +148,32 @@ export function useDiaries() { return { diaries, diaryCounter }; } +async function downloadDiaries(diaries: Diary[]) { + const rows: any[][] = [ + [ + 'Nummer', + 'Datum', + 'Von', + 'An', + 'Art', + 'Information', + 'Anmerkung', + 'erledigt', + ], + ...diaries.map((d) => [ + d.nummer, + formatTimestamp(d.datum), + d.von, + d.an, + d.art, + d.name, + d.beschreibung, + d.erledigt ? formatTimestamp(d.erledigt) : '', + ]), + ]; + downloadRowsAsCsv(rows, 'Einsatztagebuch.csv'); +} + export function DiaryButtons({ diary }: { diary: Diary }) { const [displayUpdateDialog, setDisplayUpdateDialog] = useState(false); const [deleteDialog, setDeleteDialog] = useState(false); @@ -251,7 +281,11 @@ export default function EinsatzTagebuch({ {columns && ( - Einsatz Tagebuch + Einsatz Tagebuch{' '} + downloadDiaries(diaries)} + tooltip="Einsatz Tagebuch als CSV herunterladen" + /> [ + v.name, + v.fw, + v.besatzung ? Number.parseInt(v.besatzung, 10) + 1 : 1, + v.ats, + v.beschreibung, + v.alarmierung ? formatTimestamp(v.alarmierung) : '', + v.eintreffen ? formatTimestamp(v.eintreffen) : '', + v.abruecken ? formatTimestamp(v.abruecken) : '', + ]), + ], + 'Fahrzeuge.csv' + ); +} export default function Fahrzeuge() { const { isAuthorized } = useFirebaseLogin(); @@ -18,7 +50,11 @@ export default function Fahrzeuge() { return ( - {vehicles.length} Fahrzeuge im Einsatz + {vehicles.length} Fahrzeuge im Einsatz{' '} + downloadVehicles(vehicles)} + /> {vehicles.map((fzg) => ( diff --git a/src/components/pages/Geschaeftsbuch.tsx b/src/components/pages/Geschaeftsbuch.tsx index 48be51e..94fbde6 100644 --- a/src/components/pages/Geschaeftsbuch.tsx +++ b/src/components/pages/Geschaeftsbuch.tsx @@ -10,7 +10,11 @@ import { DataGrid, GridColDef } from '@mui/x-data-grid'; import { addDoc, collection, where } from 'firebase/firestore'; import moment from 'moment'; import { useCallback, useEffect, useState } from 'react'; -import { dateTimeFormat, parseTimestamp } from '../../common/time-format'; +import { + dateTimeFormat, + formatTimestamp, + parseTimestamp, +} from '../../common/time-format'; import useFirebaseCollection from '../../hooks/useFirebaseCollection'; import useFirebaseLogin from '../../hooks/useFirebaseLogin'; import { useFirecallId } from '../../hooks/useFirecall'; @@ -24,6 +28,9 @@ import { filterActiveItems, } from '../firebase/firestore'; +import { downloadRowsAsCsv } from '../firebase/download'; +import { DownloadButton } from '../inputs/DownloadButton'; + interface GbDisplay extends GeschaeftsbuchEintrag { einaus: string; } @@ -143,6 +150,33 @@ export function useGridColumns() { return columns; } +async function downloadGb(eintraege: GbDisplay[]) { + const rows: any[][] = [ + [ + 'Nummer', + 'Datum', + 'Von', + 'An', + 'Art', + 'Information', + 'Anmerkung', + 'erledigt', + ], + ...eintraege.map((d) => [ + d.nummer, + formatTimestamp(d.datum), + d.einaus, + d.von, + d.an, + // d.art, + d.name, + d.beschreibung, + // d.erledigt ? formatTimestamp(d.erledigt) : '', + ]), + ]; + downloadRowsAsCsv(rows, 'Geschaeftsbuch.csv'); +} + export interface GeschaeftsbuchOptions { boxHeight?: string; } @@ -174,7 +208,11 @@ export default function Geschaeftsbuch({ {columns && ( - Geschäftsbuch + Geschäftsbuch{' '} + downloadGb(eintraege)} + tooltip="Geschäftsbuch als CSV herunterladen" + />