Files
Paperless-Contracts/frontend/src/routes/ContractDetail.tsx
2025-10-11 19:34:54 +02:00

194 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}