initial
This commit is contained in:
189
frontend/src/routes/ContractDetail.tsx
Normal file
189
frontend/src/routes/ContractDetail.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user