Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CSV export #26

Merged
merged 1 commit into from
Jan 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 1 addition & 39 deletions src/components/firebase/FirecallExport.tsx
Original file line number Diff line number Diff line change
@@ -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)], {
Expand Down
74 changes: 74 additions & 0 deletions src/components/firebase/download.tsx
Original file line number Diff line number Diff line change
@@ -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'
);
}
19 changes: 19 additions & 0 deletions src/components/inputs/DownloadButton.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Tooltip title={tooltip}>
<IconButton {...iconButtonProps}>
<DownloadIcon />
</IconButton>
</Tooltip>
);
}
42 changes: 38 additions & 4 deletions src/components/pages/EinsatzTagebuch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,33 @@ 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';
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,
FirecallItem,
Fzg,
filterActiveItems,
} from '../firebase/firestore';
import { DownloadButton } from '../inputs/DownloadButton';

export function useDiaries() {
const firecallId = useFirecallId();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -251,7 +281,11 @@ export default function EinsatzTagebuch({
{columns && (
<Box sx={{ p: 2, m: 2, height: boxHeight }}>
<Typography variant="h3" gutterBottom>
Einsatz Tagebuch
Einsatz Tagebuch{' '}
<DownloadButton
onClick={() => downloadDiaries(diaries)}
tooltip="Einsatz Tagebuch als CSV herunterladen"
/>
</Typography>
<DataGrid
rows={diaries}
Expand Down
38 changes: 37 additions & 1 deletion src/components/pages/Fahrzeuge.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,42 @@
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import Typography from '@mui/material/Typography';
import { formatTimestamp } from '../../common/time-format';
import useFirebaseLogin from '../../hooks/useFirebaseLogin';
import { useFirecallId } from '../../hooks/useFirecall';
import useVehicles from '../../hooks/useVehicles';
import FirecallItemCard from '../FirecallItems/FirecallItemCard';
import { downloadRowsAsCsv } from '../firebase/download';
import { Fzg } from '../firebase/firestore';
import { DownloadButton } from '../inputs/DownloadButton';

function downloadVehicles(vehicles: Fzg[]) {
downloadRowsAsCsv(
[
[
'Bezeichnung',
'Feuerwehr',
'Besatzung',
'ATS',
'Beschreibung',
'Alarmierung',
'Eintreffen',
'Abrücken',
],
...vehicles.map((v) => [
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();
Expand All @@ -18,7 +50,11 @@ export default function Fahrzeuge() {
return (
<Box sx={{ p: 2, m: 2 }}>
<Typography variant="h3" gutterBottom>
{vehicles.length} Fahrzeuge im Einsatz
{vehicles.length} Fahrzeuge im Einsatz{' '}
<DownloadButton
tooltip="Fahrzeuge als CSV herunterladen"
onClick={() => downloadVehicles(vehicles)}
/>
</Typography>
<Grid container spacing={2}>
{vehicles.map((fzg) => (
Expand Down
42 changes: 40 additions & 2 deletions src/components/pages/Geschaeftsbuch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -174,7 +208,11 @@ export default function Geschaeftsbuch({
{columns && (
<Box sx={{ p: 2, m: 2, height: boxHeight }}>
<Typography variant="h3" gutterBottom>
Geschäftsbuch
Geschäftsbuch{' '}
<DownloadButton
onClick={() => downloadGb(eintraege)}
tooltip="Geschäftsbuch als CSV herunterladen"
/>
</Typography>
<DataGrid
rows={eintraege}
Expand Down