Skip to content

Commit f923299

Browse files
authored
Merge pull request #78 from EBI-cyber/beleg-privat-schaerfe
Belegscan: klare Privat-Auswahl, private Belege speichern, schärfere Bilder
2 parents c8f6cc0 + df65b83 commit f923299

3 files changed

Lines changed: 38 additions & 14 deletions

File tree

‎src/components/BelegScanner.jsx‎

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export default function BelegScanner({ gewerke = [], projekt, verschleissProzent
5959
const autoZuschnitt = useRef(false)
6060
const kiBilder = useRef(null) // Stücke des zugeschnittenen Belegs für die KI
6161
const videoStuecke = useRef(null) // einzelne scharfe Abschnitte aus dem Video-Scan für die KI
62-
const galerie = useRef(null), bild = useRef(null), svg = useRef(null)
62+
const galerie = useRef(null), handyKamera = useRef(null), bild = useRef(null), svg = useRef(null)
6363
// Ziehen ohne React-Neuzeichnen: Positionen in einer Ref, SVG direkt aktualisieren
6464
const live = useRef(null), griff = useRef(null), lupe = useRef(null)
6565
// Lupe wie bei OneDrive: zeigt die Umgebung des gezogenen Punkts vergrößert mit Fadenkreuz
@@ -280,10 +280,12 @@ export default function BelegScanner({ gewerke = [], projekt, verschleissProzent
280280
const positionen = alle.filter((p) => !p.privat).map((p) => ({ ...p, anteil: p.verschleiss ? Math.max(0, Math.min(100, parseBetrag(p.anteil) || 0)) : 100 }))
281281
const privat = alle.filter((p) => p.privat).reduce((a, p) => a + p.gesamt, 0)
282282
// Originalfoto (komprimiert) und Ecken mitgeben, damit der Zuschnitt später korrigierbar bleibt
283-
const roh = { bild: quelle.toDataURL('image/jpeg', 0.7), ecken }
283+
// Bild und Rohfoto zusammen klein genug für den Cloud-Upload halten (sonst bleibt der Eintrag auf dem Handy hängen)
284+
let rohBild = quelle.toDataURL('image/jpeg', 0.6)
285+
if (rohBild.length > 300000) rohBild = quelle.toDataURL('image/jpeg', 0.4)
286+
const roh = { bild: rohBild, ecken }
284287
const privatPositionen = alle.filter((p) => p.privat)
285-
// Größe für den Cloud-Upload begrenzen
286-
const bildUrl = await ablagefassung(ergebnis)
288+
const bildUrl = await ablagefassung(ergebnis, 800000, modus === 'dokument')
287289
onFertig({ dataUrl: bildUrl, lieferant: ki.lieferant.trim(), datum: ki.datum, betrag, positionen, privat, privatPositionen, roh }, { weiter })
288290
}
289291

@@ -292,6 +294,9 @@ export default function BelegScanner({ gewerke = [], projekt, verschleissProzent
292294
<div className="space-y-2">
293295
<div className="text-white/75 text-xs">Beleg flach auf einen dunklen Untergrund legen und den ganzen Beleg fotografieren. Die App prüft, ob die Schrift scharf genug ist – bei langen Belegen führt sie dich danach durch einen Video-Scan.</div>
294296
<button onClick={() => setFotoKamera(true)} disabled={!!busy} className="w-full rounded-2xl py-3.5 font-bold btn-grad inline-flex items-center justify-center gap-2"><Camera className="w-5 h-5" /> Beleg fotografieren</button>
297+
{/* Kamera des Handys selbst: volle Sensorauflösung (deutlich schärfer bei langen Belegen), aber ohne Taschenlampe und Hinweise */}
298+
<button onClick={() => handyKamera.current.click()} disabled={!!busy} className="w-full rounded-2xl py-2.5 text-sm font-semibold bg-white/10 border border-white/20 inline-flex items-center justify-center gap-2"><Camera className="w-4 h-4" /> iPhone-Kamera-App öffnen (volle Auflösung)</button>
299+
<input ref={handyKamera} type="file" accept="image/*" capture="environment" className="hidden" onChange={laden} />
295300
<button onClick={() => galerie.current.click()} disabled={!!busy} className="w-full text-white/60 text-xs py-1 inline-flex items-center justify-center gap-1.5"><ImagePlus className="w-3.5 h-3.5" /> Vorhandenes Bild wählen</button>
296301
<input ref={galerie} type="file" accept="image/*" className="hidden" onChange={laden} />
297302
{busy && <div className="text-amber text-sm">{busy}</div>}
@@ -403,8 +408,12 @@ export default function BelegScanner({ gewerke = [], projekt, verschleissProzent
403408
{gewerke.map((g) => <option key={g} value={g}>{g}</option>)}
404409
</select>
405410
<input value={p.gesamt} onChange={(e) => setPos(i, { gesamt: e.target.value })} inputMode="decimal" className="w-20 bg-white/10 border border-white/20 rounded-lg px-2 py-1.5 text-sm text-right" />
406-
<button onClick={() => setPos(i, { privat: !p.privat, verschleiss: false })} className={'text-xs px-2 py-1 rounded-lg border ' + (p.privat ? 'border-ember/60 text-ember' : 'border-white/20 text-white/60')}>privat</button>
407-
<button onClick={() => setPos(i, { verschleiss: !p.verschleiss, privat: false, anteil: p.anteil || String(verschleissProzent) })} className={'text-xs px-2 py-1 rounded-lg border ' + (p.verschleiss ? 'border-amber/60 text-amber' : 'border-white/20 text-white/60')}>Verschleiß</button>
411+
{/* eindeutige Wahl: Baustelle (voll berechnet), Verschleiß (Anteil) oder privat (nicht berechnet) */}
412+
<div className="flex rounded-lg overflow-hidden border border-white/20 text-xs">
413+
<button onClick={() => setPos(i, { privat: false, verschleiss: false })} className={'px-2 py-1 ' + (!p.privat && !p.verschleiss ? 'bg-emerald-500/25 text-emerald-300 font-semibold' : 'text-white/60')}>Baustelle</button>
414+
<button onClick={() => setPos(i, { verschleiss: true, privat: false, anteil: p.anteil || String(verschleissProzent) })} className={'px-2 py-1 border-l border-white/20 ' + (p.verschleiss ? 'bg-amber/20 text-amber font-semibold' : 'text-white/60')}>Verschleiß</button>
415+
<button onClick={() => setPos(i, { privat: true, verschleiss: false })} className={'px-2 py-1 border-l border-white/20 ' + (p.privat ? 'bg-ember/20 text-ember font-semibold' : 'text-white/60')}>privat</button>
416+
</div>
408417
<button title="Auf dem Beleg zeigen" disabled={suchePos >= 0} onClick={async () => {
409418
setSuchePos(i)
410419
let markierung = null

‎src/lib/scan.js‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,24 @@ export function homographie(src, dst) {
6262
const abstand = (a, b) => Math.hypot(a.x - b.x, a.y - b.y)
6363

6464
// Bild für die Ablage (Cloud-Sync) klein halten: lange Belege auf max. 1000 px Breite,
65-
// Qualität so weit senken, bis die Daten-URL unter maxZeichen liegt
66-
export async function ablagefassung(dataUrl, maxZeichen = 900000) {
65+
// Qualität so weit senken, bis die Daten-URL unter maxZeichen liegt.
66+
// dokument: Schwarz-Weiß-Fassung zuerst als PNG mit 4 Graustufen versuchen – gestochen scharf, ohne JPEG-Schlieren
67+
export async function ablagefassung(dataUrl, maxZeichen = 900000, dokument = false) {
6768
if (!dataUrl || dataUrl.length <= maxZeichen) return dataUrl
6869
const img = await ladeBild(dataUrl)
6970
const f = Math.min(1, 1000 / img.width)
7071
const c = document.createElement('canvas')
7172
c.width = Math.round(img.width * f); c.height = Math.round(img.height * f)
72-
c.getContext('2d').drawImage(img, 0, 0, c.width, c.height)
73+
const ctx = c.getContext('2d')
74+
ctx.drawImage(img, 0, 0, c.width, c.height)
75+
if (dokument) {
76+
const d = ctx.getImageData(0, 0, c.width, c.height), p = d.data
77+
for (let i = 0; i < p.length; i += 4) { const g = Math.round((p[i] * 0.3 + p[i + 1] * 0.59 + p[i + 2] * 0.11) / 85) * 85; p[i] = p[i + 1] = p[i + 2] = g }
78+
const s = document.createElement('canvas'); s.width = c.width; s.height = c.height
79+
s.getContext('2d').putImageData(d, 0, 0)
80+
const png = s.toDataURL('image/png')
81+
if (png.length <= maxZeichen) return png
82+
}
7383
let url = ''
7484
for (const q of [0.8, 0.7, 0.6, 0.5, 0.4]) { url = c.toDataURL('image/jpeg', q); if (url.length <= maxZeichen) break }
7585
return url

‎src/screens/ProjektDetail.jsx‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -359,8 +359,10 @@ function AddSheet({ s, type, defaultGewerk, gewerkeList, onClose, onSave, editEn
359359
const fmt = (n) => n.toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' €'
360360
const notiz = 'Beleg' + (b.lieferant ? ' ' + b.lieferant : '') + (b.datum ? ' vom ' + new Date(b.datum + 'T12:00:00').toLocaleDateString('de-DE') : '') +
361361
' · ' + fmt(b.betrag) + (b.privat > 0 ? ' (davon privat ' + fmt(b.privat) + ', nicht verrechnet)' : '')
362-
const pos = b.positionen.length ? b.positionen : [{ bezeichnung: b.lieferant || 'Materialbeleg', menge: 1, gesamt: b.betrag - (b.privat || 0), gewerk }]
363-
if (pos.reduce((a, p) => a + p.gesamt, 0) <= 0) { melden('Alle Positionen sind als privat markiert, es gibt nichts zu verrechnen.'); return }
362+
// rein privater Beleg: trotzdem ablegen (nur als Dokumentation, nichts wird berechnet)
363+
const nurPrivat = !b.positionen.length && (b.privatPositionen || []).length > 0 && b.betrag - (b.privat || 0) <= 0.01
364+
const pos = b.positionen.length || nurPrivat ? [...b.positionen] : [{ bezeichnung: b.lieferant || 'Materialbeleg', menge: 1, gesamt: b.betrag - (b.privat || 0), gewerk }]
365+
if (!nurPrivat && pos.reduce((a, p) => a + p.gesamt, 0) <= 0) { melden('Der Beleg hat keinen Betrag, es gibt nichts zu verrechnen.'); return }
364366
const summe = pos.reduce((a, p) => a + p.gesamt, 0)
365367
// Privatanteil gehört nicht zur Baustelle: Differenz nur gegen den Baustellenanteil rechnen
366368
const baustelle = b.betrag - (b.privat || 0)
@@ -369,7 +371,7 @@ function AddSheet({ s, type, defaultGewerk, gewerkeList, onClose, onSave, editEn
369371
for (const p of (b.privatPositionen || [])) pos.push({ ...p, privatDoku: true })
370372
// je Position eine Millisekunde später: so bleibt die Reihenfolge wie auf dem Beleg erhalten
371373
const zeitpunkt = (i) => new Date(Date.parse(createdAt) + i).toISOString()
372-
onSave(pos.map((p, i) => {
374+
const liste = pos.map((p, i) => {
373375
if (p.privatDoku) return {
374376
type: 'material', gewerk: p.gewerk || gewerk, leistung: '', createdAt: zeitpunkt(i), note: notiz, qty: 1, unitCost: 0, dataUrl: '', belegRoh: null,
375377
label: (b.lieferant ? b.lieferant + ': ' : '') + p.bezeichnung + ' (privat, nicht berechnet: ' + fmt(p.gesamt) + ')',
@@ -381,8 +383,11 @@ function AddSheet({ s, type, defaultGewerk, gewerkeList, onClose, onSave, editEn
381383
type: 'material', gewerk: p.gewerk || gewerk, leistung: '', createdAt: zeitpunkt(i), note: notiz,
382384
label: (b.lieferant ? b.lieferant + ': ' : '') + p.bezeichnung + (anteil < 100 ? ' (Verschleißanteil ' + anteil + ' % von ' + fmt(p.gesamt) + ')' : ''),
383385
qty: p.menge > 0 ? p.menge : 1, unitCost: Math.round((berechnet / (p.menge > 0 ? p.menge : 1)) * 10000) / 10000,
384-
dataUrl: i === 0 ? b.dataUrl : '', belegRoh: i === 0 ? b.roh : null,
385-
} }), { weiter: opt && opt.weiter })
386+
dataUrl: '', belegRoh: null,
387+
} })
388+
// der erste Eintrag trägt das Belegbild (auch wenn er privat ist)
389+
liste[0] = { ...liste[0], dataUrl: b.dataUrl, belegRoh: b.roh }
390+
onSave(liste, { weiter: opt && opt.weiter })
386391
// Serienscan: Scanner für den nächsten Beleg frisch starten
387392
if (opt && opt.weiter) setScanNr((n) => n + 1)
388393
}} />

0 commit comments

Comments
 (0)