eigentliche Handwerksapp
This commit is contained in:
50
services/auftrag_repository.dart
Normal file
50
services/auftrag_repository.dart
Normal file
@@ -0,0 +1,50 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
|
||||
import '../models/auftrag.dart';
|
||||
|
||||
class AuftragRepository {
|
||||
AuftragRepository({FirebaseFirestore? firestore, FirebaseAuth? auth})
|
||||
: _db = firestore ?? FirebaseFirestore.instance,
|
||||
_auth = auth ?? FirebaseAuth.instance;
|
||||
|
||||
final FirebaseFirestore _db;
|
||||
final FirebaseAuth _auth;
|
||||
|
||||
CollectionReference<Map<String, dynamic>> get _col {
|
||||
final uid = _auth.currentUser?.uid;
|
||||
if (uid == null) {
|
||||
throw StateError('Nicht angemeldet');
|
||||
}
|
||||
return _db.collection('users').doc(uid).collection('auftraege');
|
||||
}
|
||||
|
||||
Stream<List<Auftrag>> watchAuftraege() {
|
||||
return _col.orderBy('createdAt', descending: true).snapshots().map(
|
||||
(snap) => snap.docs.map(Auftrag.fromDoc).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Auftrag?> get(String id) async {
|
||||
final doc = await _col.doc(id).get();
|
||||
if (!doc.exists) return null;
|
||||
return Auftrag.fromDoc(doc);
|
||||
}
|
||||
|
||||
/// Neue Dokument-ID ohne Schreibzugriff (für Storage-Pfade vor erstem Speichern).
|
||||
String neueId() => _col.doc().id;
|
||||
|
||||
Future<void> speichern({
|
||||
required String id,
|
||||
required Auftrag daten,
|
||||
bool isNeu = false,
|
||||
}) async {
|
||||
final payload = daten.toMap();
|
||||
if (isNeu) {
|
||||
payload['createdAt'] = FieldValue.serverTimestamp();
|
||||
}
|
||||
await _col.doc(id).set(payload, SetOptions(merge: true));
|
||||
}
|
||||
|
||||
Future<void> loeschen(String id) => _col.doc(id).delete();
|
||||
}
|
||||
38
services/auftrag_storage_service.dart
Normal file
38
services/auftrag_storage_service.dart
Normal file
@@ -0,0 +1,38 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:firebase_storage/firebase_storage.dart';
|
||||
|
||||
class AuftragStorageService {
|
||||
AuftragStorageService({FirebaseStorage? storage, FirebaseAuth? auth})
|
||||
: _storage = storage ?? FirebaseStorage.instance,
|
||||
_auth = auth ?? FirebaseAuth.instance;
|
||||
|
||||
final FirebaseStorage _storage;
|
||||
final FirebaseAuth _auth;
|
||||
|
||||
Reference _basis(String auftragId) {
|
||||
final uid = _auth.currentUser?.uid;
|
||||
if (uid == null) throw StateError('Nicht angemeldet');
|
||||
return _storage.ref('users/$uid/auftraege/$auftragId');
|
||||
}
|
||||
|
||||
Future<String> hochladenFoto(String auftragId, Uint8List bytes, String dateiname) async {
|
||||
final ref = _basis(auftragId).child('fotos/$dateiname');
|
||||
await ref.putData(
|
||||
bytes,
|
||||
SettableMetadata(contentType: 'image/jpeg'),
|
||||
);
|
||||
return ref.getDownloadURL();
|
||||
}
|
||||
|
||||
Future<String> hochladenUnterschrift(String auftragId, Uint8List pngBytes) async {
|
||||
final ref = _basis(auftragId).child('unterschrift.png');
|
||||
await ref.putData(
|
||||
pngBytes,
|
||||
SettableMetadata(contentType: 'image/png'),
|
||||
);
|
||||
return ref.getDownloadURL();
|
||||
}
|
||||
|
||||
}
|
||||
138
services/pdf_export_service.dart
Normal file
138
services/pdf_export_service.dart
Normal file
@@ -0,0 +1,138 @@
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:pdf/pdf.dart';
|
||||
import 'package:pdf/widgets.dart' as pw;
|
||||
import 'package:share_plus/share_plus.dart' show ShareParams, SharePlus, XFile;
|
||||
|
||||
import '../models/auftrag.dart';
|
||||
|
||||
class PdfExportService {
|
||||
static Future<File> buildPdf({
|
||||
required Auftrag auftrag,
|
||||
required List<Uint8List?> fotoBytes,
|
||||
Uint8List? unterschriftBytes,
|
||||
}) async {
|
||||
final doc = pw.Document();
|
||||
final nr = auftrag.rechnungsnummer.isEmpty
|
||||
? 'Entwurf'
|
||||
: auftrag.rechnungsnummer;
|
||||
|
||||
doc.addPage(
|
||||
pw.MultiPage(
|
||||
pageFormat: PdfPageFormat.a4,
|
||||
margin: const pw.EdgeInsets.all(40),
|
||||
build: (ctx) => [
|
||||
pw.Header(
|
||||
level: 0,
|
||||
child: pw.Text(
|
||||
'Rechnung',
|
||||
style: pw.TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: pw.FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
pw.SizedBox(height: 8),
|
||||
pw.Text('Rechnungs-Nr.: $nr',
|
||||
style: pw.TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: pw.FontWeight.bold,
|
||||
)),
|
||||
pw.SizedBox(height: 16),
|
||||
pw.Text('Leistung / Titel: ${auftrag.titel}',
|
||||
style: const pw.TextStyle(fontSize: 14)),
|
||||
pw.SizedBox(height: 8),
|
||||
pw.Text('Kunde: ${auftrag.kundenName}',
|
||||
style: const pw.TextStyle(fontSize: 14)),
|
||||
if (auftrag.kundenEmail.isNotEmpty) ...[
|
||||
pw.SizedBox(height: 4),
|
||||
pw.Text('E-Mail: ${auftrag.kundenEmail}',
|
||||
style: const pw.TextStyle(fontSize: 11)),
|
||||
],
|
||||
if (auftrag.betragText.isNotEmpty) ...[
|
||||
pw.SizedBox(height: 12),
|
||||
pw.Text('Betrag (Brutto): ${auftrag.betragText} €',
|
||||
style: pw.TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: pw.FontWeight.bold,
|
||||
)),
|
||||
],
|
||||
pw.SizedBox(height: 16),
|
||||
pw.Text('Beschreibung / Positionen:',
|
||||
style: pw.TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: pw.FontWeight.bold,
|
||||
)),
|
||||
pw.SizedBox(height: 6),
|
||||
pw.Text(
|
||||
auftrag.beschreibung.isEmpty ? '—' : auftrag.beschreibung,
|
||||
style: const pw.TextStyle(fontSize: 11),
|
||||
),
|
||||
pw.SizedBox(height: 20),
|
||||
if (fotoBytes.any((b) => b != null && b.isNotEmpty)) ...[
|
||||
pw.Text('Fotos',
|
||||
style: pw.TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: pw.FontWeight.bold,
|
||||
)),
|
||||
pw.SizedBox(height: 8),
|
||||
for (final b in fotoBytes)
|
||||
if (b != null && b.isNotEmpty)
|
||||
pw.Padding(
|
||||
padding: const pw.EdgeInsets.only(bottom: 12),
|
||||
child: pw.Image(
|
||||
pw.MemoryImage(b),
|
||||
fit: pw.BoxFit.contain,
|
||||
height: 200,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (unterschriftBytes != null && unterschriftBytes.isNotEmpty) ...[
|
||||
pw.SizedBox(height: 16),
|
||||
pw.Text('Unterschrift Kunde',
|
||||
style: pw.TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: pw.FontWeight.bold,
|
||||
)),
|
||||
pw.SizedBox(height: 8),
|
||||
pw.Image(
|
||||
pw.MemoryImage(unterschriftBytes),
|
||||
height: 100,
|
||||
fit: pw.BoxFit.contain,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
final bytes = await doc.save();
|
||||
final dir = await getTemporaryDirectory();
|
||||
final safeNr = nr.replaceAll(RegExp(r'[^\w\-]'), '_');
|
||||
final name = 'rechnung_${safeNr.isEmpty ? auftrag.id.substring(0, 8) : safeNr}.pdf';
|
||||
final file = File('${dir.path}/$name');
|
||||
await file.writeAsBytes(bytes);
|
||||
return file;
|
||||
}
|
||||
|
||||
static Future<Uint8List?> ladeUrl(String url) async {
|
||||
try {
|
||||
final r = await http.get(Uri.parse(url));
|
||||
if (r.statusCode == 200) return r.bodyBytes;
|
||||
} catch (_) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Future<void> teilen(File file, {String? rechnungsnummer}) async {
|
||||
await SharePlus.instance.share(
|
||||
ShareParams(
|
||||
files: [XFile(file.path)],
|
||||
subject: rechnungsnummer != null && rechnungsnummer.isNotEmpty
|
||||
? 'Rechnung $rechnungsnummer'
|
||||
: 'Rechnung PDF',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user