194 lines
7.4 KiB
TypeScript
194 lines
7.4 KiB
TypeScript
import LaunchIcon from "@mui/icons-material/Launch";
|
||
import {
|
||
Box,
|
||
Button,
|
||
Chip,
|
||
Divider,
|
||
Grid,
|
||
Paper,
|
||
Stack,
|
||
Typography
|
||
} from "@mui/material";
|
||
import { useQuery } from "@tanstack/react-query";
|
||
import { useTranslation } from "react-i18next";
|
||
import { useParams, useNavigate } from "react-router-dom";
|
||
|
||
import { fetchContract, fetchPaperlessDocument } from "../api/contracts";
|
||
import { fetchServerConfig, ServerConfig } from "../api/config";
|
||
import PageHeader from "../components/PageHeader";
|
||
import { formatCurrency, formatDate } from "../utils/date";
|
||
import { translateCategoryName } from "../utils/categories";
|
||
|
||
export default function ContractDetail() {
|
||
const { contractId } = useParams<{ contractId: string }>();
|
||
const navigate = useNavigate();
|
||
const id = Number(contractId);
|
||
const { t, i18n } = useTranslation();
|
||
|
||
const { data: contract, isLoading } = useQuery({
|
||
queryKey: ["contracts", id],
|
||
queryFn: () => fetchContract(id),
|
||
enabled: Number.isFinite(id)
|
||
});
|
||
|
||
const { data: serverConfig } = useQuery<ServerConfig>({
|
||
queryKey: ["server-config"],
|
||
queryFn: fetchServerConfig
|
||
});
|
||
|
||
const {
|
||
data: paperlessDoc,
|
||
error: paperlessError
|
||
} = useQuery({
|
||
queryKey: ["contracts", id, "paperless"],
|
||
queryFn: () => fetchPaperlessDocument(id),
|
||
enabled: Number.isFinite(id)
|
||
});
|
||
|
||
const paperlessAppUrl = serverConfig?.paperlessExternalUrl ?? serverConfig?.paperlessBaseUrl ?? null;
|
||
const terminationValue =
|
||
contract?.terminationNoticeDays !== undefined && contract?.terminationNoticeDays !== null
|
||
? t("deadlineList.daysLabel", { count: contract.terminationNoticeDays })
|
||
: "–";
|
||
const renewalValue =
|
||
contract?.renewalPeriodMonths
|
||
? `${t("contractDetail.monthsLabel", { count: contract.renewalPeriodMonths })}${contract.autoRenew ? `, ${t("contractForm.fields.autoRenew")}` : ""}`
|
||
: contract?.autoRenew
|
||
? t("contractForm.fields.autoRenew")
|
||
: "–";
|
||
const notesValue = contract?.notes ?? t("contractDetail.noNotes");
|
||
const categoryValue = contract?.category
|
||
? translateCategoryName(contract.category, i18n.language) || contract.category
|
||
: "–";
|
||
|
||
if (!Number.isFinite(id)) {
|
||
return <Typography>{t("contractForm.loadError")}</Typography>;
|
||
}
|
||
|
||
if (isLoading || !contract) {
|
||
return <Typography>{t("contractForm.loading")}</Typography>;
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<PageHeader
|
||
title={contract.title}
|
||
subtitle={contract.provider ?? ""}
|
||
action={
|
||
<Button variant="contained" onClick={() => navigate(`/contracts/${contract.id}/edit`)}>
|
||
{t("contractDetail.edit")}
|
||
</Button>
|
||
}
|
||
/>
|
||
|
||
<Grid container spacing={3}>
|
||
<Grid item xs={12} md={8}>
|
||
<Paper variant="outlined" sx={{ borderRadius: 3, p: 3 }}>
|
||
<Typography variant="h6" gutterBottom>
|
||
{t("contractDetail.details")}
|
||
</Typography>
|
||
<Stack spacing={1.5}>
|
||
<Detail label={t("contractDetail.start")} value={formatDate(contract.contractStartDate)} />
|
||
<Detail label={t("contractDetail.end")} value={formatDate(contract.contractEndDate)} />
|
||
<Detail label={t("contractDetail.notice")} value={terminationValue} />
|
||
<Detail label={t("contractDetail.renewal")} value={renewalValue} />
|
||
<Detail label={t("contractDetail.price")} value={formatCurrency(contract.price, contract.currency ?? "EUR")} />
|
||
<Detail label={t("contractDetail.category")} value={categoryValue} />
|
||
<Detail label={t("contractDetail.notes")} value={notesValue} />
|
||
</Stack>
|
||
|
||
<Divider sx={{ my: 3 }} />
|
||
|
||
<Typography variant="subtitle1" gutterBottom>
|
||
{t("contractDetail.tags")}
|
||
</Typography>
|
||
<Box display="flex" flexWrap="wrap" gap={1}>
|
||
{(contract.tags ?? []).length > 0 ? (
|
||
contract.tags!.map((tag) => <Chip key={tag} label={tag} />)
|
||
) : (
|
||
<Typography variant="body2" color="text.secondary">
|
||
{t("contractDetail.noTags")}
|
||
</Typography>
|
||
)}
|
||
</Box>
|
||
</Paper>
|
||
</Grid>
|
||
|
||
<Grid item xs={12} md={4}>
|
||
<Paper variant="outlined" sx={{ borderRadius: 3, p: 3, mb: 3 }}>
|
||
<Typography variant="h6" gutterBottom>
|
||
{t("contractDetail.document")}
|
||
</Typography>
|
||
{paperlessError ? (
|
||
<Typography variant="body2" color="error">
|
||
{t("contractDetail.documentError", { error: (paperlessError as Error).message })}
|
||
</Typography>
|
||
) : paperlessDoc ? (
|
||
<Stack spacing={1}>
|
||
<Typography variant="subtitle2">
|
||
{String(paperlessDoc.title ?? t("contractDetail.documentFallback"))}
|
||
</Typography>
|
||
<Typography variant="body2" color="text.secondary">
|
||
{t("contractDetail.created")}: {paperlessDoc.created ? formatDate(String(paperlessDoc.created)) : "–"}
|
||
</Typography>
|
||
<Typography variant="body2" color="text.secondary">
|
||
{t("contractDetail.updated")}: {paperlessDoc.modified ? formatDate(String(paperlessDoc.modified)) : "–"}
|
||
</Typography>
|
||
{paperlessDoc.notes && (
|
||
<Typography variant="body2" color="text.secondary">
|
||
{String(paperlessDoc.notes)}
|
||
</Typography>
|
||
)}
|
||
<Button
|
||
variant="outlined"
|
||
startIcon={<LaunchIcon />}
|
||
sx={{ alignSelf: "flex-start", mt: 1 }}
|
||
disabled={!serverConfig || !paperlessAppUrl || !contract.paperlessDocumentId}
|
||
onClick={() => {
|
||
if (!paperlessAppUrl || !contract.paperlessDocumentId) return;
|
||
const url = `${paperlessAppUrl.replace(/\/$/, "")}/documents/${contract.paperlessDocumentId}`;
|
||
window.open(url, "_blank", "noopener");
|
||
}}
|
||
>
|
||
{t("contractDetail.openInPaperless")}
|
||
</Button>
|
||
{!paperlessAppUrl && (
|
||
<Typography variant="caption" color="text.secondary">
|
||
{t("contractDetail.configurePaperless")}
|
||
</Typography>
|
||
)}
|
||
</Stack>
|
||
) : (
|
||
<Typography variant="body2" color="text.secondary">
|
||
{t("contractDetail.documentMissing")}
|
||
</Typography>
|
||
)}
|
||
</Paper>
|
||
|
||
<Paper variant="outlined" sx={{ borderRadius: 3, p: 3 }}>
|
||
<Typography variant="h6" gutterBottom>
|
||
{t("contractDetail.metadata")}
|
||
</Typography>
|
||
<Stack spacing={1.5}>
|
||
<Detail label={t("contractDetail.id")} value={`#${contract.id}`} />
|
||
<Detail label={t("contractDetail.created")} value={formatDate(contract.createdAt)} />
|
||
<Detail label={t("contractDetail.updated")} value={formatDate(contract.updatedAt)} />
|
||
</Stack>
|
||
</Paper>
|
||
</Grid>
|
||
</Grid>
|
||
</>
|
||
);
|
||
}
|
||
|
||
function Detail({ label, value }: { label: string; value: string }) {
|
||
return (
|
||
<Box>
|
||
<Typography variant="caption" color="text.secondary" textTransform="uppercase">
|
||
{label}
|
||
</Typography>
|
||
<Typography variant="body1">{value}</Typography>
|
||
</Box>
|
||
);
|
||
}
|