Can attach multiple inbox entries to movements at once
This commit is contained in:
63
moneymgr_web/src/widgets/InboxEntryWidget.tsx
Normal file
63
moneymgr_web/src/widgets/InboxEntryWidget.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import CallMadeIcon from "@mui/icons-material/CallMade";
|
||||
import CallReceivedIcon from "@mui/icons-material/CallReceived";
|
||||
import QuestionMarkIcon from "@mui/icons-material/QuestionMark";
|
||||
import React from "react";
|
||||
import { match } from "ts-pattern";
|
||||
import { InboxEntry } from "../api/InboxApi";
|
||||
import { fmtDateFromTime } from "../utils/DateUtils";
|
||||
import { AmountWidget } from "./AmountWidget";
|
||||
import { UploadedFileWidget } from "./UploadedFileWidget";
|
||||
|
||||
export function InboxEntryWidget(p: { entry: InboxEntry }): React.ReactElement {
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
{/* File */}
|
||||
<UploadedFileWidget small file_id={p.entry.file_id} />
|
||||
|
||||
{/* Icon */}
|
||||
{match(p.entry.amount)
|
||||
.when(
|
||||
(v) => v === undefined || v === null,
|
||||
() => <QuestionMarkIcon color="secondary" />
|
||||
)
|
||||
.when(
|
||||
(v) => v > 0,
|
||||
() => <CallReceivedIcon color="success" />
|
||||
)
|
||||
.otherwise(() => (
|
||||
<CallMadeIcon color="error" />
|
||||
))}
|
||||
|
||||
<span
|
||||
style={{
|
||||
marginLeft: "5px",
|
||||
display: "inline-flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<span style={{ height: "1em", lineHeight: 1 }}>
|
||||
{(p.entry.label?.length ?? 0) > 0 ? p.entry.label : <i>No label</i>}
|
||||
</span>
|
||||
<span style={{ display: "flex", alignItems: "center", lineHeight: 1 }}>
|
||||
{p.entry.amount ? (
|
||||
<AmountWidget amount={p.entry.amount} />
|
||||
) : (
|
||||
<i>No amount</i>
|
||||
)}
|
||||
<span style={{ width: "0.5em" }} />
|
||||
•
|
||||
<span style={{ width: "0.5em" }} />
|
||||
{fmtDateFromTime(p.entry.time)}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
@ -1,13 +1,16 @@
|
||||
import ImageIcon from "@mui/icons-material/Image";
|
||||
import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf";
|
||||
import { Button } from "@mui/material";
|
||||
import { Button, IconButton } from "@mui/material";
|
||||
import { filesize } from "filesize";
|
||||
import React from "react";
|
||||
import { FileApi, UploadedFile } from "../api/FileApi";
|
||||
import { AsyncWidget } from "./AsyncWidget";
|
||||
import { FileViewerDialog } from "../dialogs/FileViewerDialog";
|
||||
import { AsyncWidget } from "./AsyncWidget";
|
||||
|
||||
export function UploadedFileWidget(p: { file_id: number }): React.ReactElement {
|
||||
export function UploadedFileWidget(p: {
|
||||
file_id: number;
|
||||
small?: boolean;
|
||||
}): React.ReactElement {
|
||||
const [file, setFile] = React.useState<UploadedFile | null>(null);
|
||||
|
||||
const load = async () => {
|
||||
@ -17,7 +20,7 @@ export function UploadedFileWidget(p: { file_id: number }): React.ReactElement {
|
||||
return (
|
||||
<AsyncWidget
|
||||
errMsg="Failed"
|
||||
build={() => <UploadedFileWidgetInner file={file!} />}
|
||||
build={() => <UploadedFileWidgetInner file={file!} small={p.small} />}
|
||||
loadKey={p.file_id}
|
||||
load={load}
|
||||
/>
|
||||
@ -25,6 +28,7 @@ export function UploadedFileWidget(p: { file_id: number }): React.ReactElement {
|
||||
}
|
||||
|
||||
function UploadedFileWidgetInner(p: {
|
||||
small?: boolean;
|
||||
file: UploadedFile;
|
||||
}): React.ReactElement {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
@ -33,20 +37,36 @@ function UploadedFileWidgetInner(p: {
|
||||
<FileViewerDialog
|
||||
open={open}
|
||||
file={p.file}
|
||||
onClose={() => { setOpen(false); }}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
startIcon={
|
||||
p.file.mime_type === "application/pdf" ? (
|
||||
<PictureAsPdfIcon />
|
||||
) : (
|
||||
<ImageIcon />
|
||||
)
|
||||
}
|
||||
onClick={() => { setOpen(true); }}
|
||||
>
|
||||
{p.file.file_name} ({filesize(p.file.file_size)})
|
||||
</Button>
|
||||
{p.small ? (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
<FileIcon file={p.file} />
|
||||
</IconButton>
|
||||
) : (
|
||||
<Button
|
||||
startIcon={<FileIcon file={p.file} />}
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
{p.file.file_name} ({filesize(p.file.file_size)})
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function FileIcon(p: { file: UploadedFile }): React.ReactElement {
|
||||
return p.file.mime_type === "application/pdf" ? (
|
||||
<PictureAsPdfIcon />
|
||||
) : (
|
||||
<ImageIcon />
|
||||
);
|
||||
}
|
||||
|
31
moneymgr_web/src/widgets/forms/MovementSelect.tsx
Normal file
31
moneymgr_web/src/widgets/forms/MovementSelect.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { MenuItem, Select, SelectChangeEvent } from "@mui/material";
|
||||
import { Movement } from "../../api/MovementsApi";
|
||||
import { MovementWidget } from "../MovementWidget";
|
||||
|
||||
export function MovementSelect(p: {
|
||||
list: Movement[];
|
||||
value: Movement | undefined;
|
||||
onChange: (value: Movement | undefined) => void;
|
||||
}): React.ReactElement {
|
||||
const handleChange = (event: SelectChangeEvent) => {
|
||||
if (!event.target.value) {
|
||||
p.onChange(undefined);
|
||||
return;
|
||||
}
|
||||
const id = Number(event.target.value);
|
||||
p.onChange(p.list.find((m) => m.id === id));
|
||||
};
|
||||
|
||||
return (
|
||||
<Select value={p.value?.id.toString()} onChange={handleChange}>
|
||||
<MenuItem value={undefined}>
|
||||
<i>None</i>
|
||||
</MenuItem>
|
||||
{p.list.map((l) => (
|
||||
<MenuItem key={l.id} value={l.id.toString()}>
|
||||
<MovementWidget movement={l} />
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user