feat(import): resolve color variety ID mappings mismatch, support unnamed parents in virtual litters, and implement SaleContract updates
This commit is contained in:
Binary file not shown.
@@ -2,7 +2,7 @@
|
||||
import { useMemo } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { de } from '../strings/de'
|
||||
import { contractFileUrl, deleteContract, listContracts } from '../api/contracts'
|
||||
import { contractFileUrl, contractPdfUrl, deleteContract, listContracts } from '../api/contracts'
|
||||
import { listContactsPaged } from '../api/contacts'
|
||||
import { useApi, useMutation } from '../hooks/useApi'
|
||||
import { useInfiniteList, useInfiniteSentinel } from '../hooks/useInfiniteList'
|
||||
@@ -81,6 +81,9 @@ export default function VertraegeListPage() {
|
||||
{t.fields.handoverDate} {formatDate(c.handoverDate)}
|
||||
</span>
|
||||
<span className="head-actions">
|
||||
<a className="btn btn--primary" href={contractPdfUrl(c)}>
|
||||
{t.downloadPdf}
|
||||
</a>
|
||||
<a className="btn" href={contractFileUrl(c)}>
|
||||
{t.download}
|
||||
</a>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* serverseitig in EINER Transaktion: .docx erzeugen + speichern, Vertrag
|
||||
* anlegen, Tiere auf „Abgegeben“ stellen (Abnehmer + Abgabedatum).
|
||||
*/
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { de } from '../strings/de'
|
||||
import { listGerbils } from '../api/gerbils'
|
||||
@@ -16,6 +16,7 @@ import { createContact, listContactsPaged } from '../api/contacts'
|
||||
import { contractFileUrl, createContract, type SaleContract } from '../api/contracts'
|
||||
import { getBreederProfile, isBreederProfileComplete } from '../api/settings'
|
||||
import { listColorVarieties } from '../api/lookups'
|
||||
import { listGerbilPhotos, photoSrc, profilePhoto } from '../api/photos'
|
||||
import { useApi, useMutation } from '../hooks/useApi'
|
||||
import { formatDate, genderLabel } from '../format/labels'
|
||||
import './vertragWizard.css'
|
||||
@@ -110,6 +111,11 @@ export default function VertragWizardPage() {
|
||||
return next
|
||||
})
|
||||
|
||||
/* Vertragsfoto je Tier: GerbilId → PhotoId ('' = bewusst kein Foto). */
|
||||
const [animalPhotos, setAnimalPhotos] = useState<Record<string, string>>({})
|
||||
const setAnimalPhoto = (gerbilId: string, photoId: string) =>
|
||||
setAnimalPhotos((m) => ({ ...m, [gerbilId]: photoId }))
|
||||
|
||||
/* ── Schritt 3: Preis & Datum ── */
|
||||
const [priceText, setPriceText] = useState('')
|
||||
const [handoverDate, setHandoverDate] = useState(todayIso())
|
||||
@@ -127,6 +133,12 @@ export default function VertragWizardPage() {
|
||||
price: parsePrice(priceText)!,
|
||||
handoverDate,
|
||||
contractDate: contractDate || null,
|
||||
// nur ausgewählte Tiere mit tatsächlich gewähltem Foto übermitteln
|
||||
animalPhotos: Object.fromEntries(
|
||||
[...selectedIds]
|
||||
.map((id) => [id, animalPhotos[id]] as const)
|
||||
.filter(([, pid]) => Boolean(pid)),
|
||||
),
|
||||
}),
|
||||
)
|
||||
async function onGenerate() {
|
||||
@@ -339,6 +351,13 @@ export default function VertragWizardPage() {
|
||||
.join(' · ')}
|
||||
</span>
|
||||
</label>
|
||||
{selectedIds.has(g.id) && (
|
||||
<ContractPhotoPicker
|
||||
gerbilId={g.id}
|
||||
chosen={animalPhotos[g.id]}
|
||||
onChange={(photoId) => setAnimalPhoto(g.id, photoId)}
|
||||
/>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -450,3 +469,57 @@ export default function VertragWizardPage() {
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Foto-Auswahl je Tier für den Vertrag. Lädt die Tierfotos lazy; Standard ist
|
||||
* das Profilfoto (erstes nach SortOrder). '' = bewusst „Kein Foto“.
|
||||
* Rendert nichts, wenn das Tier keine Fotos hat.
|
||||
*/
|
||||
function ContractPhotoPicker({
|
||||
gerbilId,
|
||||
chosen,
|
||||
onChange,
|
||||
}: {
|
||||
gerbilId: string
|
||||
chosen: string | undefined
|
||||
onChange: (photoId: string) => void
|
||||
}) {
|
||||
const t = de.pages.vertraege.wizard
|
||||
const photos = useApi(() => listGerbilPhotos(gerbilId), [gerbilId])
|
||||
const list = useMemo(() => photos.data ?? [], [photos.data])
|
||||
|
||||
// Sobald Fotos da sind und noch nichts entschieden ist: Profilfoto vorauswählen.
|
||||
useEffect(() => {
|
||||
if (list.length > 0 && chosen === undefined) {
|
||||
const p = profilePhoto(list)
|
||||
if (p) onChange(p.id)
|
||||
}
|
||||
}, [list, chosen, onChange])
|
||||
|
||||
if (photos.loading || list.length === 0) return null
|
||||
|
||||
return (
|
||||
<div className="contract-photopick">
|
||||
<span className="contract-photopick__label">{t.photoLabel}</span>
|
||||
<div className="contract-photopick__thumbs">
|
||||
<button
|
||||
type="button"
|
||||
className={`contract-photopick__thumb contract-photopick__thumb--none${chosen === '' ? ' is-selected' : ''}`}
|
||||
onClick={() => onChange('')}
|
||||
>
|
||||
{t.photoNone}
|
||||
</button>
|
||||
{list.map((p) => (
|
||||
<button
|
||||
key={p.id}
|
||||
type="button"
|
||||
className={`contract-photopick__thumb${chosen === p.id ? ' is-selected' : ''}`}
|
||||
onClick={() => onChange(p.id)}
|
||||
>
|
||||
<img src={photoSrc(p)} alt={p.caption ?? ''} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -140,3 +140,50 @@
|
||||
grid-row: 1 / span 2;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
/* ── Vertragsfoto-Auswahl je Tier (Schritt „Tiere“) ── */
|
||||
.contract-photopick {
|
||||
margin: 0.5rem 0 0.25rem 1.9rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.contract-photopick__label {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-muted, #8c7f6e);
|
||||
}
|
||||
.contract-photopick__thumbs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.contract-photopick__thumb {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
padding: 0;
|
||||
border: 2px solid var(--color-border, #d9ccb4);
|
||||
border-radius: 10px;
|
||||
background: var(--color-surface, #fff);
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
.contract-photopick__thumb img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
.contract-photopick__thumb--none {
|
||||
font-size: 0.72rem;
|
||||
color: var(--color-muted, #8c7f6e);
|
||||
text-align: center;
|
||||
line-height: 1.1;
|
||||
}
|
||||
.contract-photopick__thumb.is-selected {
|
||||
border-color: var(--color-accent, #a85f2e);
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-accent, #a85f2e) 30%, transparent);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user