This commit is contained in:
MDeeApp
2025-10-11 01:17:31 +02:00
commit 8eb060f380
1223 changed files with 265299 additions and 0 deletions

View File

@@ -0,0 +1,189 @@
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";
export default function ContractDetail() {
const { contractId } = useParams<{ contractId: string }>();
const navigate = useNavigate();
const id = Number(contractId);
const { t } = 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");
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={contract.category ?? ""} />
<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>
);
}