Title, Provider, tags from paperless

This commit is contained in:
MDeeApp
2025-10-11 11:32:04 +02:00
parent 8eb060f380
commit 342a73ecb5
11 changed files with 530 additions and 36 deletions

View File

@@ -13,7 +13,7 @@ import {
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { zodResolver } from "@hookform/resolvers/zod";
import { Controller, useForm } from "react-hook-form";
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "react-router-dom";
import { z } from "zod";
@@ -22,6 +22,7 @@ import {
createContract,
fetchContract,
fetchPaperlessDocument,
fetchPaperlessDocumentById,
updateContract
} from "../api/contracts";
import { fetchServerConfig } from "../api/config";
@@ -29,6 +30,7 @@ import PaperlessSearchDialog from "../components/PaperlessSearchDialog";
import PageHeader from "../components/PageHeader";
import { useSnackbar } from "../hooks/useSnackbar";
import { ContractPayload, PaperlessDocument } from "../types";
import { extractPaperlessProvider, extractPaperlessTags, extractPaperlessTitle } from "../utils/paperless";
const formSchema = z.object({
title: z.string().min(1, "Titel erforderlich"),
@@ -94,7 +96,8 @@ export default function ContractForm({ mode }: Props) {
formState: { errors },
reset,
setValue,
watch
watch,
getValues
} = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
@@ -134,6 +137,25 @@ export default function ContractForm({ mode }: Props) {
const [selectedDocument, setSelectedDocument] = useState<PaperlessDocument | null>(null);
const paperlessDocumentId = watch("paperlessDocumentId");
const watchedTitle = watch("title");
const watchedProvider = watch("provider");
const watchedCategory = watch("category");
const watchedPaperlessId = watch("paperlessDocumentId");
const watchedTags = watch("tags");
const providerSuggestion = useMemo(
() => (selectedDocument ? extractPaperlessProvider(selectedDocument) : null),
[selectedDocument]
);
const tagsSuggestion = useMemo(
() => (selectedDocument ? extractPaperlessTags(selectedDocument) : []),
[selectedDocument]
);
const titleSuggestion = useMemo(
() => (selectedDocument ? extractPaperlessTitle(selectedDocument) : null),
[selectedDocument]
);
useEffect(() => {
if (mode === "edit" && contract) {
reset({
@@ -179,6 +201,33 @@ export default function ContractForm({ mode }: Props) {
}
}, [paperlessDocumentId, selectedDocument]);
useEffect(() => {
if (!selectedDocument) {
return;
}
if (import.meta.env.DEV) {
console.debug("Paperless document details", selectedDocument);
console.debug("Extracted suggestions", {
title: titleSuggestion,
provider: providerSuggestion,
tags: tagsSuggestion
});
}
if (titleSuggestion && !getValues("title")?.trim()) {
setValue("title", titleSuggestion, { shouldDirty: true });
}
if (providerSuggestion && !getValues("provider")?.trim()) {
setValue("provider", providerSuggestion, { shouldDirty: true });
}
if (tagsSuggestion.length > 0 && !getValues("tags")?.trim()) {
setValue("tags", tagsSuggestion.join(", "), { shouldDirty: true });
}
}, [selectedDocument, titleSuggestion, providerSuggestion, tagsSuggestion, getValues, setValue]);
const mutation = useMutation({
mutationFn: async (values: FormValues) => {
const payload: ContractPayload = {
@@ -243,22 +292,34 @@ export default function ContractForm({ mode }: Props) {
label={t("contractForm.fields.title")}
fullWidth
required
InputLabelProps={{ shrink: Boolean(watchedTitle?.trim()) }}
{...register("title")}
error={Boolean(errors.title)}
helperText={errors.title?.message}
/>
</Grid>
<Grid item xs={12} md={6}>
<TextField label={t("contractForm.fields.provider")} fullWidth {...register("provider")} />
<TextField
label={t("contractForm.fields.provider")}
fullWidth
InputLabelProps={{ shrink: Boolean(watchedProvider?.trim()) }}
{...register("provider")}
/>
</Grid>
<Grid item xs={12} md={6}>
<TextField label={t("contractForm.fields.category")} fullWidth {...register("category")} />
<TextField
label={t("contractForm.fields.category")}
fullWidth
InputLabelProps={{ shrink: Boolean(watchedCategory?.trim()) }}
{...register("category")}
/>
</Grid>
<Grid item xs={12} md={6}>
<Stack direction={{ xs: "column", sm: "row" }} spacing={1} alignItems="flex-start">
<TextField
label={t("contractForm.fields.paperlessId")}
fullWidth
InputLabelProps={{ shrink: Boolean(watchedPaperlessId?.trim()) }}
{...register("paperlessDocumentId")}
error={Boolean(errors.paperlessDocumentId)}
helperText={errors.paperlessDocumentId?.message}
@@ -283,6 +344,17 @@ export default function ContractForm({ mode }: Props) {
})}
</Typography>
)}
{(providerSuggestion || tagsSuggestion.length > 0) && (
<Typography variant="caption" color="text.secondary" sx={{ display: "block", mt: 0.5 }}>
{providerSuggestion
? t("contractForm.suggestionProvider", { provider: providerSuggestion })
: null}
{providerSuggestion && tagsSuggestion.length > 0 ? " • " : ""}
{tagsSuggestion.length > 0
? t("contractForm.suggestionTags", { tags: tagsSuggestion.slice(0, 5).join(", ") })
: null}
</Typography>
)}
</Grid>
<Grid item xs={12} md={6}>
<TextField
@@ -368,7 +440,8 @@ export default function ContractForm({ mode }: Props) {
<TextField
label={t("contractForm.fields.tags")}
fullWidth
placeholder={t("contractForm.tagsPlaceholder")}
placeholder={watchedTags?.trim() ? undefined : t("contractForm.tagsPlaceholder")}
InputLabelProps={{ shrink: Boolean(watchedTags?.trim()) }}
{...register("tags")}
/>
</Grid>
@@ -390,9 +463,23 @@ export default function ContractForm({ mode }: Props) {
<PaperlessSearchDialog
open={searchDialogOpen}
onClose={() => setSearchDialogOpen(false)}
onSelect={(doc) => {
onSelect={async (doc) => {
const idValue = doc.id ? String(doc.id) : "";
setValue("paperlessDocumentId", idValue, { shouldDirty: true });
if (doc.id) {
try {
const detailed = await fetchPaperlessDocumentById(doc.id);
if (detailed) {
setSelectedDocument(detailed);
return;
}
} catch (error) {
// Fallback to the selected doc if the detail fetch fails.
console.error("Failed to fetch Paperless document details", error);
}
}
setSelectedDocument(doc);
}}
/>

View File

@@ -0,0 +1 @@
<placeholder original snippet>