feat(import): RennmausPro-III-Backup-Importer (analyze+execute, Dedup, Fotos)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@ import NotFoundPage from './pages/NotFoundPage'
|
||||
import StammbaumPage from './pages/StammbaumPage'
|
||||
import StatistikPage from './pages/StatistikPage'
|
||||
import HilfePage from './pages/HilfePage'
|
||||
import RennmausProImportPage from './pages/RennmausProImportPage'
|
||||
import FarbkatalogPage from './pages/FarbkatalogPage'
|
||||
import VertraegeListPage from './pages/VertraegeListPage'
|
||||
import VertragWizardPage from './pages/VertragWizardPage'
|
||||
@@ -71,6 +72,8 @@ export default function App() {
|
||||
<Route path="abgabe" element={<AbgabePage />} />
|
||||
<Route path="statistik" element={<StatistikPage />} />
|
||||
<Route path="hilfe" element={<HilfePage />} />
|
||||
{/* RPRO3: RennmausPro-III-Backup-Import (Rubrik unter „Hilfe") */}
|
||||
<Route path="hilfe/rennmauspro-import" element={<RennmausProImportPage />} />
|
||||
<Route path="farbkatalog" element={<FarbkatalogPage />} />
|
||||
{/* FEAT-13: Abgabeverträge + Einstellungen (Zuchtprofil) */}
|
||||
<Route path="vertraege">
|
||||
|
||||
103
gerbil-manager-web/src/api/rpro3.ts
Normal file
103
gerbil-manager-web/src/api/rpro3.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* RPRO3: API-Client für den RennmausPro-III-Backup-Import.
|
||||
* POST /import/rpro3/analyze -> multipart 'backup' (+ optional 'images') -> Auswertung (kein Schreiben)
|
||||
* POST /import/rpro3/execute -> derselbe Upload -> idempotenter Import
|
||||
*
|
||||
* Upload via FormData (nicht api.post — das erzwingt application/json); der Browser setzt
|
||||
* den multipart-Boundary-Header selbst.
|
||||
*/
|
||||
import { API_BASE_URL, ApiError } from './client'
|
||||
import { de } from '../strings/de'
|
||||
|
||||
export interface Rpro3Counts {
|
||||
ownAnimals: number
|
||||
externalRaw: number
|
||||
externalAfterDedup: number
|
||||
litters: number
|
||||
contacts: number
|
||||
genotypes: number
|
||||
duplicatesMerged: number
|
||||
mergeClusters: number
|
||||
}
|
||||
|
||||
export interface Rpro3AmbiguousVariant {
|
||||
count: number
|
||||
dob: string
|
||||
farbe: string
|
||||
origin: string
|
||||
isOwn: boolean
|
||||
}
|
||||
|
||||
export interface Rpro3AmbiguousName {
|
||||
name: string
|
||||
variants: Rpro3AmbiguousVariant[]
|
||||
bareCount: number
|
||||
}
|
||||
|
||||
export interface Rpro3MergeSample {
|
||||
name: string
|
||||
recordCount: number
|
||||
dob: string
|
||||
farbe: string
|
||||
origin: string
|
||||
}
|
||||
|
||||
export interface Rpro3AnalyzeResult {
|
||||
counts: Rpro3Counts
|
||||
newVsCurrentNew: number
|
||||
newVsCurrentExisting: number
|
||||
topMerges: Rpro3MergeSample[]
|
||||
ambiguousNames: Rpro3AmbiguousName[]
|
||||
photosProvided: boolean
|
||||
photoFilesAvailable: number
|
||||
}
|
||||
|
||||
export interface Rpro3ExecuteResult {
|
||||
gerbilsImported: number
|
||||
littersImported: number
|
||||
contactsImported: number
|
||||
healthRecordsImported: number
|
||||
weightRecordsImported: number
|
||||
photosImported: number
|
||||
message: string
|
||||
}
|
||||
|
||||
async function postUpload<T>(path: string, backup: File, images: File | null): Promise<T> {
|
||||
const form = new FormData()
|
||||
form.append('backup', backup)
|
||||
if (images) form.append('images', images)
|
||||
|
||||
let response: Response
|
||||
try {
|
||||
response = await fetch(`${API_BASE_URL}${path}`, { method: 'POST', body: form })
|
||||
} catch {
|
||||
throw new ApiError(de.api.errors.network, null, path)
|
||||
}
|
||||
if (!response.ok) {
|
||||
// 400 trägt eine deutsche Klartext-Fehlermeldung (z. B. „keine _rpro3.db gefunden").
|
||||
let body: unknown
|
||||
let text: string | null = null
|
||||
try {
|
||||
text = await response.clone().text()
|
||||
body = text ? JSON.parse(text) : undefined
|
||||
} catch {
|
||||
body = undefined
|
||||
}
|
||||
const message =
|
||||
response.status === 400 && text
|
||||
? text.replace(/^"|"$/g, '')
|
||||
: response.status >= 500
|
||||
? de.api.errors.server
|
||||
: de.api.errors.unknown
|
||||
throw new ApiError(message, response.status, path, body)
|
||||
}
|
||||
return (await response.json()) as T
|
||||
}
|
||||
|
||||
export function analyzeRpro3(backup: File, images: File | null): Promise<Rpro3AnalyzeResult> {
|
||||
return postUpload<Rpro3AnalyzeResult>('/import/rpro3/analyze', backup, images)
|
||||
}
|
||||
|
||||
export function executeRpro3(backup: File, images: File | null): Promise<Rpro3ExecuteResult> {
|
||||
return postUpload<Rpro3ExecuteResult>('/import/rpro3/execute', backup, images)
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
* Accordion via <details>/<summary> — kein JS-State nötig, barrierefrei, mobiltauglich.
|
||||
* Texte referenzieren nur Bezeichnungen aus de.ts (bleiben bei Umbenennung korrekt).
|
||||
*/
|
||||
import { Link } from 'react-router-dom'
|
||||
import { de } from '../strings/de'
|
||||
import './hilfe.css'
|
||||
|
||||
@@ -210,6 +211,20 @@ const sections: Section[] = [
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'rpro3-import',
|
||||
title: t.sections.rpro3Import,
|
||||
content: (
|
||||
<>
|
||||
<p>{t.rpro3Link.text}</p>
|
||||
<p>
|
||||
<Link to="/hilfe/rennmauspro-import" className="hilfe-cta">
|
||||
{t.rpro3Link.button} →
|
||||
</Link>
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
export default function HilfePage() {
|
||||
|
||||
255
gerbil-manager-web/src/pages/RennmausProImportPage.tsx
Normal file
255
gerbil-manager-web/src/pages/RennmausProImportPage.tsx
Normal file
@@ -0,0 +1,255 @@
|
||||
/**
|
||||
* RPRO3: Rubrik unter „Hilfe" für den RennmausPro-III-Backup-Import.
|
||||
* Laienverständlicher 3-Schritt-Ablauf:
|
||||
* 1) .backup (+ optional _bilder.zip) auswählen → „Auswerten" (schreibt nichts)
|
||||
* 2) Auswertung: Zählungen, Dubletten, mehrdeutige Namen, neu/vorhanden
|
||||
* 3) „Import durchführen" → Ergebnis
|
||||
* Alle Texte aus de.ts.
|
||||
*/
|
||||
import { useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { de } from '../strings/de'
|
||||
import { ApiError } from '../api/client'
|
||||
import {
|
||||
analyzeRpro3,
|
||||
executeRpro3,
|
||||
type Rpro3AnalyzeResult,
|
||||
type Rpro3ExecuteResult,
|
||||
} from '../api/rpro3'
|
||||
import './rpro3-import.css'
|
||||
|
||||
const t = de.pages.rpro3Import
|
||||
|
||||
type Phase = 'select' | 'analyzing' | 'analyzed' | 'executing' | 'done'
|
||||
|
||||
export default function RennmausProImportPage() {
|
||||
const [backup, setBackup] = useState<File | null>(null)
|
||||
const [images, setImages] = useState<File | null>(null)
|
||||
const [phase, setPhase] = useState<Phase>('select')
|
||||
const [analysis, setAnalysis] = useState<Rpro3AnalyzeResult | null>(null)
|
||||
const [result, setResult] = useState<Rpro3ExecuteResult | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
function reset() {
|
||||
setBackup(null)
|
||||
setImages(null)
|
||||
setPhase('select')
|
||||
setAnalysis(null)
|
||||
setResult(null)
|
||||
setError(null)
|
||||
}
|
||||
|
||||
async function handleAnalyze() {
|
||||
if (!backup) {
|
||||
setError(t.noBackup)
|
||||
return
|
||||
}
|
||||
setError(null)
|
||||
setPhase('analyzing')
|
||||
try {
|
||||
const res = await analyzeRpro3(backup, images)
|
||||
setAnalysis(res)
|
||||
setPhase('analyzed')
|
||||
} catch (err) {
|
||||
setError(err instanceof ApiError ? err.message : de.api.errors.unknown)
|
||||
setPhase('select')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleExecute() {
|
||||
if (!backup) return
|
||||
setError(null)
|
||||
setPhase('executing')
|
||||
try {
|
||||
const res = await executeRpro3(backup, images)
|
||||
setResult(res)
|
||||
setPhase('done')
|
||||
} catch (err) {
|
||||
setError(err instanceof ApiError ? err.message : de.api.errors.unknown)
|
||||
setPhase('analyzed')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="page rpro3-page">
|
||||
<h2>{t.title}</h2>
|
||||
<p className="rpro3-subtitle">{t.subtitle}</p>
|
||||
<p className="rpro3-intro">{t.intro}</p>
|
||||
|
||||
{error && (
|
||||
<div className="rpro3-error" role="alert">
|
||||
<strong>{t.errorTitle}:</strong> {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Schritt 1: Dateien auswählen */}
|
||||
{phase !== 'done' && (
|
||||
<div className="rpro3-card">
|
||||
<h3>{t.step1Title}</h3>
|
||||
<div className="rpro3-field">
|
||||
<label htmlFor="rpro3-backup">{t.backupLabel}</label>
|
||||
<input
|
||||
id="rpro3-backup"
|
||||
type="file"
|
||||
accept=".backup,.zip,.db"
|
||||
disabled={phase === 'analyzing' || phase === 'executing'}
|
||||
onChange={(e) => setBackup(e.target.files?.[0] ?? null)}
|
||||
/>
|
||||
<small>{t.backupHint}</small>
|
||||
</div>
|
||||
<div className="rpro3-field">
|
||||
<label htmlFor="rpro3-images">{t.imagesLabel}</label>
|
||||
<input
|
||||
id="rpro3-images"
|
||||
type="file"
|
||||
accept=".zip"
|
||||
disabled={phase === 'analyzing' || phase === 'executing'}
|
||||
onChange={(e) => setImages(e.target.files?.[0] ?? null)}
|
||||
/>
|
||||
<small>{t.imagesHint}</small>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="rpro3-btn rpro3-btn-primary"
|
||||
disabled={!backup || phase === 'analyzing' || phase === 'executing'}
|
||||
onClick={handleAnalyze}
|
||||
>
|
||||
{phase === 'analyzing' ? t.analyzing : t.analyzeButton}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Schritt 2: Auswertung */}
|
||||
{analysis && phase !== 'done' && (
|
||||
<AnalysisView analysis={analysis} />
|
||||
)}
|
||||
|
||||
{/* Schritt 3: Import durchführen */}
|
||||
{analysis && (phase === 'analyzed' || phase === 'executing') && (
|
||||
<div className="rpro3-card">
|
||||
<h3>{t.step3Title}</h3>
|
||||
<p className="rpro3-warning">{t.executeWarning}</p>
|
||||
<button
|
||||
type="button"
|
||||
className="rpro3-btn rpro3-btn-primary"
|
||||
disabled={phase === 'executing'}
|
||||
onClick={handleExecute}
|
||||
>
|
||||
{phase === 'executing' ? t.executing : t.executeButton}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Ergebnis */}
|
||||
{result && phase === 'done' && (
|
||||
<div className="rpro3-card rpro3-success">
|
||||
<h3>{t.successTitle}</h3>
|
||||
<ul className="rpro3-result-list">
|
||||
<li><strong>{result.gerbilsImported}</strong> {t.resultCounts.gerbils}</li>
|
||||
<li><strong>{result.littersImported}</strong> {t.resultCounts.litters}</li>
|
||||
<li><strong>{result.contactsImported}</strong> {t.resultCounts.contacts}</li>
|
||||
<li><strong>{result.healthRecordsImported}</strong> {t.resultCounts.health}</li>
|
||||
<li><strong>{result.weightRecordsImported}</strong> {t.resultCounts.weights}</li>
|
||||
<li><strong>{result.photosImported}</strong> {t.resultCounts.photos}</li>
|
||||
</ul>
|
||||
<div className="rpro3-actions">
|
||||
<Link to="/rennmaeuse" className="rpro3-btn">{de.nav.gerbils}</Link>
|
||||
<button type="button" className="rpro3-btn" onClick={reset}>{t.restart}</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function AnalysisView({ analysis }: { analysis: Rpro3AnalyzeResult }) {
|
||||
const c = analysis.counts
|
||||
return (
|
||||
<div className="rpro3-card">
|
||||
<h3>{t.step2Title}</h3>
|
||||
|
||||
<h4>{t.overviewTitle}</h4>
|
||||
<dl className="rpro3-counts">
|
||||
<Count label={t.counts.ownAnimals} value={c.ownAnimals} />
|
||||
<Count label={t.counts.externalAfterDedup} value={c.externalAfterDedup} />
|
||||
<Count label={t.counts.litters} value={c.litters} />
|
||||
<Count label={t.counts.contacts} value={c.contacts} />
|
||||
<Count label={t.counts.genotypes} value={c.genotypes} />
|
||||
<Count label={t.counts.duplicatesMerged} value={c.duplicatesMerged} />
|
||||
</dl>
|
||||
|
||||
<p className="rpro3-hint">{t.dedupExplain}</p>
|
||||
|
||||
<h4>{t.newVsExistingTitle}</h4>
|
||||
<p>
|
||||
<span className="rpro3-badge rpro3-badge-new">{t.newCount(analysis.newVsCurrentNew)}</span>{' '}
|
||||
<span className="rpro3-badge">{t.existingCount(analysis.newVsCurrentExisting)}</span>
|
||||
</p>
|
||||
|
||||
<p className="rpro3-photos-note">
|
||||
{analysis.photosProvided ? t.photosNote(analysis.photoFilesAvailable) : t.photosNoneNote}
|
||||
</p>
|
||||
|
||||
{analysis.topMerges.length > 0 && (
|
||||
<details className="rpro3-details">
|
||||
<summary>{t.topMergesTitle}</summary>
|
||||
<div className="rpro3-table-wrap">
|
||||
<table className="rpro3-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t.topMergesCol.name}</th>
|
||||
<th>{t.topMergesCol.count}</th>
|
||||
<th>{t.topMergesCol.dob}</th>
|
||||
<th>{t.topMergesCol.farbe}</th>
|
||||
<th>{t.topMergesCol.origin}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{analysis.topMerges.map((m, i) => (
|
||||
<tr key={i}>
|
||||
<td>{m.name}</td>
|
||||
<td>{m.recordCount}</td>
|
||||
<td>{m.dob || '—'}</td>
|
||||
<td>{m.farbe || '—'}</td>
|
||||
<td>{m.origin || '—'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</details>
|
||||
)}
|
||||
|
||||
{analysis.ambiguousNames.length > 0 && (
|
||||
<details className="rpro3-details">
|
||||
<summary>{t.ambiguousTitle}</summary>
|
||||
<p className="rpro3-hint">{t.ambiguousIntro}</p>
|
||||
<ul className="rpro3-ambiguous">
|
||||
{analysis.ambiguousNames.map((a, i) => (
|
||||
<li key={i}>
|
||||
<strong>{a.name}</strong>
|
||||
<ul>
|
||||
{a.variants.map((v, j) => (
|
||||
<li key={j}>
|
||||
{v.isOwn && <span className="rpro3-star" title={t.ambiguousOwn}>★ </span>}
|
||||
{t.ambiguousVariant(v.count, v.dob, v.farbe, v.origin)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Count({ label, value }: { label: string; value: number }) {
|
||||
return (
|
||||
<div className="rpro3-count">
|
||||
<dt>{label}</dt>
|
||||
<dd>{value}</dd>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -103,3 +103,17 @@
|
||||
.hilfe-warning {
|
||||
color: var(--color-warning-text, #92400e);
|
||||
}
|
||||
|
||||
.hilfe-cta {
|
||||
display: inline-block;
|
||||
padding: 0.55rem 1rem;
|
||||
border-radius: 6px;
|
||||
background: var(--color-primary, #2563eb);
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.hilfe-cta:hover {
|
||||
background: var(--color-primary-dark, #1d4ed8);
|
||||
}
|
||||
|
||||
235
gerbil-manager-web/src/pages/rpro3-import.css
vendored
Normal file
235
gerbil-manager-web/src/pages/rpro3-import.css
vendored
Normal file
@@ -0,0 +1,235 @@
|
||||
.rpro3-page {
|
||||
max-width: 760px;
|
||||
}
|
||||
|
||||
.rpro3-subtitle {
|
||||
color: var(--color-muted, #666);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.rpro3-intro {
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.rpro3-card {
|
||||
border: 1px solid var(--color-border, #ddd);
|
||||
border-radius: 8px;
|
||||
background: var(--color-surface, #fff);
|
||||
padding: 1.25rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.rpro3-card h3 {
|
||||
margin: 0 0 1rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.rpro3-card h4 {
|
||||
margin: 1.25rem 0 0.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.rpro3-field {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.rpro3-field label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
.rpro3-field input[type='file'] {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.rpro3-field small {
|
||||
display: block;
|
||||
color: var(--color-muted, #666);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.rpro3-btn {
|
||||
display: inline-block;
|
||||
padding: 0.6rem 1.1rem;
|
||||
border: 1px solid var(--color-border, #ccc);
|
||||
border-radius: 6px;
|
||||
background: var(--color-surface, #fff);
|
||||
color: inherit;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.rpro3-btn:hover:not(:disabled) {
|
||||
background: var(--color-hover, #f5f5f5);
|
||||
}
|
||||
|
||||
.rpro3-btn-primary {
|
||||
background: var(--color-primary, #2563eb);
|
||||
border-color: var(--color-primary, #2563eb);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.rpro3-btn-primary:hover:not(:disabled) {
|
||||
background: var(--color-primary-dark, #1d4ed8);
|
||||
}
|
||||
|
||||
.rpro3-btn:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.rpro3-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.rpro3-error {
|
||||
background: var(--color-danger-bg, #fef2f2);
|
||||
border-left: 3px solid var(--color-danger, #dc2626);
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0 4px 4px 0;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.rpro3-warning {
|
||||
background: var(--color-warning-bg, #fffbeb);
|
||||
border-left: 3px solid var(--color-warning, #f59e0b);
|
||||
padding: 0.6rem 0.9rem;
|
||||
border-radius: 0 4px 4px 0;
|
||||
line-height: 1.55;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.rpro3-hint {
|
||||
background: var(--color-info-bg, #eff6ff);
|
||||
border-left: 3px solid var(--color-info, #3b82f6);
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 0 4px 4px 0;
|
||||
font-size: 0.93rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.rpro3-photos-note {
|
||||
color: var(--color-muted, #666);
|
||||
font-size: 0.93rem;
|
||||
}
|
||||
|
||||
/* Zählungen als Kachel-Grid */
|
||||
.rpro3-counts {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
|
||||
gap: 0.75rem;
|
||||
margin: 0.5rem 0 0.5rem;
|
||||
}
|
||||
|
||||
.rpro3-count {
|
||||
border: 1px solid var(--color-border, #eee);
|
||||
border-radius: 6px;
|
||||
padding: 0.6rem 0.75rem;
|
||||
background: var(--color-hover, #fafafa);
|
||||
}
|
||||
|
||||
.rpro3-count dt {
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-muted, #666);
|
||||
}
|
||||
|
||||
.rpro3-count dd {
|
||||
margin: 0.2rem 0 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.rpro3-badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.6rem;
|
||||
border-radius: 999px;
|
||||
background: var(--color-hover, #f1f5f9);
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.rpro3-badge-new {
|
||||
background: var(--color-info-bg, #eff6ff);
|
||||
color: var(--color-info, #1d4ed8);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.rpro3-details {
|
||||
margin-top: 1rem;
|
||||
border: 1px solid var(--color-border, #eee);
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.rpro3-details summary {
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.rpro3-table-wrap {
|
||||
overflow-x: auto;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.rpro3-table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.rpro3-table th,
|
||||
.rpro3-table td {
|
||||
border: 1px solid var(--color-border, #eee);
|
||||
padding: 0.35rem 0.55rem;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.rpro3-table th {
|
||||
background: var(--color-hover, #f5f5f5);
|
||||
}
|
||||
|
||||
.rpro3-ambiguous {
|
||||
margin: 0.75rem 0 0 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.rpro3-ambiguous > li {
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
|
||||
.rpro3-ambiguous ul {
|
||||
margin: 0.2rem 0 0 1rem;
|
||||
font-size: 0.92rem;
|
||||
color: var(--color-muted, #555);
|
||||
}
|
||||
|
||||
.rpro3-star {
|
||||
color: var(--color-primary, #2563eb);
|
||||
}
|
||||
|
||||
.rpro3-success {
|
||||
border-color: var(--color-success, #16a34a);
|
||||
}
|
||||
|
||||
.rpro3-result-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.rpro3-result-list li {
|
||||
background: var(--color-hover, #f6f6f6);
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
@@ -820,6 +820,65 @@ export const de = {
|
||||
},
|
||||
},
|
||||
},
|
||||
// ── RPRO3: RennmausPro-III-Backup-Import (Rubrik unter „Hilfe") ──
|
||||
rpro3Import: {
|
||||
title: 'RennmausPro-Import',
|
||||
subtitle:
|
||||
'Daten aus deiner alten RennmausPro-III-Software übernehmen — Tiere, Würfe, Stammbäume, Kontakte und Genotypen.',
|
||||
intro:
|
||||
'Lade hier deine RennmausPro-Sicherungsdatei hoch (die Datei endet auf „.backup"). Optional kannst du zusätzlich das zugehörige Bilder-Archiv (endet auf „_bilder.zip") hochladen. Zuerst wird die Datei nur ausgewertet — es wird noch nichts gespeichert. Du siehst dann eine Übersicht und entscheidest selbst, ob du den Import durchführst.',
|
||||
step1Title: '1. Dateien auswählen',
|
||||
backupLabel: 'RennmausPro-Sicherung (.backup)',
|
||||
backupHint: 'Pflicht. Die Datei aus deiner RennmausPro-III-Software.',
|
||||
imagesLabel: 'Bilder-Archiv (_bilder.zip)',
|
||||
imagesHint: 'Optional. Wird gebraucht, um die Tierfotos zuzuordnen.',
|
||||
analyzeButton: 'Auswerten',
|
||||
analyzing: 'Datei wird ausgewertet …',
|
||||
noBackup: 'Bitte zuerst eine RennmausPro-Sicherung (.backup) auswählen.',
|
||||
step2Title: '2. Auswertung',
|
||||
overviewTitle: 'Was steckt in der Datei?',
|
||||
counts: {
|
||||
ownAnimals: 'Eigene Tiere',
|
||||
externalAfterDedup: 'Externe Ahnen (nach Bereinigung)',
|
||||
externalRaw: 'Externe Ahnen (roh, vor Bereinigung)',
|
||||
litters: 'Würfe',
|
||||
contacts: 'Kontakte (Züchter & Abnehmer)',
|
||||
genotypes: 'Hinterlegte Genotypen',
|
||||
duplicatesMerged: 'Erkannte Dubletten (zusammengelegt)',
|
||||
},
|
||||
newVsExistingTitle: 'Abgleich mit deinem aktuellen Bestand',
|
||||
newCount: (n: number) => `${n} Tiere wären neu`,
|
||||
existingCount: (n: number) => `${n} Tiere sind vermutlich schon vorhanden`,
|
||||
dedupExplain:
|
||||
'RennmausPro hat beim Import vorhandene Tiere nicht erkannt — dadurch kommt dasselbe Tier oft mehrfach vor. Diese Dubletten werden automatisch anhand von Name, Geburtsdatum, Farbe und Herkunft zusammengelegt.',
|
||||
topMergesTitle: 'Größte automatische Zusammenlegungen',
|
||||
topMergesCol: { name: 'Tier', count: 'Datensätze', dob: 'Geburtsdatum', farbe: 'Farbe', origin: 'Herkunft' },
|
||||
ambiguousTitle: 'Mehrdeutige Namen — bitte später prüfen',
|
||||
ambiguousIntro:
|
||||
'Bei diesen Namen gibt es mehrere unterschiedliche Tiere. Sie werden vorsichtshalber NICHT automatisch zusammengelegt, damit nichts falsch verknüpft wird. Du kannst sie nach dem Import in der Tierliste prüfen.',
|
||||
ambiguousVariant: (count: number, dob: string, farbe: string, origin: string) =>
|
||||
`${count}× · ${dob} · ${farbe} · ${origin}`,
|
||||
ambiguousOwn: 'eigenes Tier',
|
||||
ambiguousMore: (n: number) => `… und ${n} weitere mehrdeutige Namen.`,
|
||||
photosNote: (n: number) => `Bilder-Archiv erkannt: ${n} Bilddateien werden den Tieren zugeordnet.`,
|
||||
photosNoneNote: 'Kein Bilder-Archiv hochgeladen — Tiere werden ohne Fotos importiert.',
|
||||
step3Title: '3. Import durchführen',
|
||||
executeWarning:
|
||||
'Beim Import werden die Tiere, Würfe, Kontakte und Genotypen aus RennmausPro übernommen. Ein erneuter Import aktualisiert dieselben Daten (es entstehen keine Dubletten). Deine manuell angelegten Tiere bleiben unberührt.',
|
||||
executeButton: 'Import jetzt durchführen',
|
||||
executing: 'Import läuft … das kann einen Moment dauern.',
|
||||
successTitle: 'Import abgeschlossen',
|
||||
resultCounts: {
|
||||
gerbils: 'Tiere importiert',
|
||||
litters: 'Würfe importiert',
|
||||
contacts: 'Kontakte importiert',
|
||||
health: 'Gesundheitseinträge',
|
||||
weights: 'Gewichtseinträge',
|
||||
photos: 'Fotos',
|
||||
},
|
||||
errorTitle: 'Es ist ein Fehler aufgetreten',
|
||||
restart: 'Neue Datei auswählen',
|
||||
},
|
||||
},
|
||||
// ── HELP-1: In-App-Anleitung ──
|
||||
hilfe: {
|
||||
@@ -838,6 +897,12 @@ export const de = {
|
||||
einstellungen: 'Zuchtprofil (Einstellungen)',
|
||||
statistik: 'Statistik',
|
||||
datensicherung: 'Datensicherung',
|
||||
rpro3Import: 'Daten aus RennmausPro übernehmen',
|
||||
},
|
||||
// Verweis auf die eigene RennmausPro-Import-Rubrik.
|
||||
rpro3Link: {
|
||||
text: 'Hast du früher mit der Software „RennmausPro III" gearbeitet? Du kannst deine alten Daten (Tiere, Würfe, Stammbäume, Kontakte) bequem übernehmen.',
|
||||
button: 'Zum RennmausPro-Import',
|
||||
},
|
||||
},
|
||||
api: {
|
||||
|
||||
Reference in New Issue
Block a user