fix: PDF zoom boundaries, Mängel status display in list view, and swipeable image gallery with PDF export
Build Android APK / Build Release APKs (push) Successful in 14m49s

This commit is contained in:
2026-09-18 13:46:21 +02:00
parent f10d5ad5ed
commit 974223f7e7
6 changed files with 384 additions and 82 deletions
+18 -6
View File
@@ -19,17 +19,20 @@ class PostItem {
required this.id, required this.id,
required this.title, required this.title,
required this.description, required this.description,
this.imageUrl = '', String imageUrl = '',
List<String>? imageUrls, List<String>? imageUrls,
required this.createdDate, required this.createdDate,
this.authorUid, this.authorUid,
this.street, this.street,
this.category, this.category,
this.status = 'Gemeldet', this.status,
this.pdfUrl, this.pdfUrl,
this.webUrl, this.webUrl,
this.isAussendung = false, this.isAussendung = false,
}) : imageUrls = imageUrls ?? (imageUrl.isNotEmpty ? [imageUrl] : []); }) : imageUrls = imageUrls ?? (imageUrl.isNotEmpty ? [imageUrl] : []),
imageUrl = imageUrl.isNotEmpty
? imageUrl
: (imageUrls != null && imageUrls.isNotEmpty ? imageUrls.first : '');
factory PostItem.fromFirestore(DocumentSnapshot doc) { factory PostItem.fromFirestore(DocumentSnapshot doc) {
final data = doc.data() as Map<String, dynamic>? ?? {}; final data = doc.data() as Map<String, dynamic>? ?? {};
@@ -49,6 +52,9 @@ class PostItem {
images = [legacyImage]; images = [legacyImage];
} }
final hasIssueFields = data['street'] != null || data['category'] != null;
final status = data['status'] as String? ?? (hasIssueFields ? 'Gemeldet' : null);
return PostItem( return PostItem(
id: doc.id, id: doc.id,
title: data['title'] as String? ?? '', title: data['title'] as String? ?? '',
@@ -59,7 +65,7 @@ class PostItem {
authorUid: data['author_uid'] as String?, authorUid: data['author_uid'] as String?,
street: data['street'] as String?, street: data['street'] as String?,
category: data['category'] as String?, category: data['category'] as String?,
status: data['status'] as String? ?? 'Gemeldet', status: status,
pdfUrl: data['pdfUrl'] as String?, pdfUrl: data['pdfUrl'] as String?,
webUrl: data['webUrl'] as String?, webUrl: data['webUrl'] as String?,
isAussendung: data['isAussendung'] as bool? ?? false, isAussendung: data['isAussendung'] as bool? ?? false,
@@ -119,17 +125,23 @@ class PostItem {
images = [legacyImage]; images = [legacyImage];
} }
final hasIssueFields = map['street'] != null || map['category'] != null;
final status =
map['status'] as String? ?? (hasIssueFields ? 'Gemeldet' : null);
return PostItem( return PostItem(
id: map['id'] as String? ?? '', id: map['id'] as String? ?? '',
title: map['title'] as String? ?? '', title: map['title'] as String? ?? '',
description: map['description'] as String? ?? '', description: map['description'] as String? ?? '',
imageUrl: legacyImage.isNotEmpty ? legacyImage : (images.isNotEmpty ? images.first : ''), imageUrl: legacyImage.isNotEmpty
? legacyImage
: (images.isNotEmpty ? images.first : ''),
imageUrls: images, imageUrls: images,
createdDate: date, createdDate: date,
authorUid: map['authorUid'] as String?, authorUid: map['authorUid'] as String?,
street: map['street'] as String?, street: map['street'] as String?,
category: map['category'] as String?, category: map['category'] as String?,
status: map['status'] as String? ?? 'Gemeldet', status: status,
pdfUrl: map['pdfUrl'] as String?, pdfUrl: map['pdfUrl'] as String?,
webUrl: map['webUrl'] as String?, webUrl: map['webUrl'] as String?,
isAussendung: map['isAussendung'] as bool? ?? false, isAussendung: map['isAussendung'] as bool? ?? false,
+154 -54
View File
@@ -27,6 +27,8 @@ class DetailScreen extends StatefulWidget {
class _DetailScreenState extends State<DetailScreen> { class _DetailScreenState extends State<DetailScreen> {
late PostItem _item; late PostItem _item;
int _currentImageIndex = 0;
final PageController _pageController = PageController();
@override @override
void initState() { void initState() {
@@ -34,6 +36,12 @@ class _DetailScreenState extends State<DetailScreen> {
_item = widget.item; _item = widget.item;
} }
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
Future<void> _editItem() async { Future<void> _editItem() async {
final collection = widget.collectionName ?? final collection = widget.collectionName ??
(_item.street != null ? 'Issues' : 'CitizensForum'); (_item.street != null ? 'Issues' : 'CitizensForum');
@@ -118,6 +126,7 @@ class _DetailScreenState extends State<DetailScreen> {
final isAuthor = currentUser != null && _item.authorUid == currentUser.uid; final isAuthor = currentUser != null && _item.authorUid == currentUser.uid;
final canEdit = !_item.isAussendung && (isAuthor || auth.isAdmin); final canEdit = !_item.isAussendung && (isAuthor || auth.isAdmin);
final canDelete = !_item.isAussendung && (isAuthor || auth.isAdmin); final canDelete = !_item.isAussendung && (isAuthor || auth.isAdmin);
final isIssue = widget.collectionName == 'Issues' || _item.street != null;
final formattedDate = final formattedDate =
DateFormat('dd.MM.yyyy HH:mm').format(_item.createdDate); DateFormat('dd.MM.yyyy HH:mm').format(_item.createdDate);
@@ -155,27 +164,131 @@ class _DetailScreenState extends State<DetailScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (_item.imageUrls.isNotEmpty) if (_item.imageUrls.isNotEmpty) ...[
ClipRRect( if (_item.imageUrls.length == 1)
borderRadius: BorderRadius.circular(12), ClipRRect(
child: CachedNetworkImage( borderRadius: BorderRadius.circular(12),
imageUrl: _item.imageUrls.first, child: CachedNetworkImage(
width: double.infinity, imageUrl: _item.imageUrls.first,
fit: BoxFit.cover, width: double.infinity,
placeholder: (context, url) => const Center( height: 240,
child: Padding( fit: BoxFit.cover,
padding: EdgeInsets.all(32.0), placeholder: (context, url) => const Center(
child: CircularProgressIndicator(), child: Padding(
padding: EdgeInsets.all(32.0),
child: CircularProgressIndicator(),
),
),
errorWidget: (context, url, error) => Container(
height: 200,
color: Colors.grey.shade200,
child: const Icon(Icons.image_not_supported, size: 48),
), ),
), ),
errorWidget: (context, url, error) => Container( )
height: 180, else
color: Colors.grey.shade200, Stack(
child: const Icon(Icons.image_not_supported, size: 48), alignment: Alignment.bottomCenter,
), children: [
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: SizedBox(
height: 260,
width: double.infinity,
child: PageView.builder(
controller: _pageController,
itemCount: _item.imageUrls.length,
onPageChanged: (index) {
setState(() {
_currentImageIndex = index;
});
},
itemBuilder: (context, index) {
return CachedNetworkImage(
imageUrl: _item.imageUrls[index],
width: double.infinity,
height: 260,
fit: BoxFit.cover,
placeholder: (context, url) => const Center(
child: Padding(
padding: EdgeInsets.all(32.0),
child: CircularProgressIndicator(),
),
),
errorWidget: (context, url, error) => Container(
height: 260,
color: Colors.grey.shade200,
child: const Icon(Icons.image_not_supported,
size: 48),
),
);
},
),
),
),
// Page indicator dots
Positioned(
bottom: 10,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.6),
borderRadius: BorderRadius.circular(16),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(
_item.imageUrls.length,
(i) => Container(
margin:
const EdgeInsets.symmetric(horizontal: 3),
width: _currentImageIndex == i ? 16 : 6,
height: 6,
decoration: BoxDecoration(
color: _currentImageIndex == i
? Colors.white
: Colors.white54,
borderRadius: BorderRadius.circular(3),
),
),
),
),
),
),
// Photo counter badge (e.g. 1 / 2)
Positioned(
top: 10,
right: 10,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.6),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.photo_library,
color: Colors.white, size: 14),
const SizedBox(width: 4),
Text(
'${_currentImageIndex + 1} / ${_item.imageUrls.length}',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
],
), ),
), const SizedBox(height: 16),
const SizedBox(height: 16), ],
Text( Text(
_item.title, _item.title,
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold), style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
@@ -189,22 +302,37 @@ class _DetailScreenState extends State<DetailScreen> {
formattedDate, formattedDate,
style: TextStyle(fontSize: 13, color: Colors.grey.shade700), style: TextStyle(fontSize: 13, color: Colors.grey.shade700),
), ),
if (_item.status != null) ...[ if (isIssue && _item.status != null) ...[
const Spacer(), const Spacer(),
Container( Container(
padding: padding: const EdgeInsets.symmetric(
const EdgeInsets.symmetric(horizontal: 10, vertical: 4), horizontal: 10, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.blue.shade50, color: _item.status == 'Erledigt'
? Colors.green.shade50
: (_item.status == 'In Bearbeitung'
? Colors.blue.shade50
: Colors.orange.shade50),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.blue.shade200), border: Border.all(
color: _item.status == 'Erledigt'
? Colors.green.shade200
: (_item.status == 'In Bearbeitung'
? Colors.blue.shade200
: Colors.orange.shade200),
),
), ),
child: Text( child: Text(
_item.status!, _item.status!,
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.blue.shade800), color: _item.status == 'Erledigt'
? Colors.green.shade800
: (_item.status == 'In Bearbeitung'
? Colors.blue.shade800
: Colors.orange.shade900),
),
), ),
), ),
], ],
@@ -284,34 +412,6 @@ class _DetailScreenState extends State<DetailScreen> {
), ),
], ],
const SizedBox(height: 24), const SizedBox(height: 24),
// Additional images gallery if there are multiple images
if (_item.imageUrls.length > 1) ...[
const Text(
'Weitere Fotos',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
SizedBox(
height: 120,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: _item.imageUrls.length - 1,
separatorBuilder: (ctx, i) => const SizedBox(width: 8),
itemBuilder: (ctx, index) {
final url = _item.imageUrls[index + 1];
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: CachedNetworkImage(
imageUrl: url,
width: 120,
height: 120,
fit: BoxFit.cover,
),
);
},
),
),
],
], ],
), ),
), ),
+5 -3
View File
@@ -61,8 +61,9 @@ class _PdfViewerScreenState extends State<PdfViewerScreen>
} else { } else {
// At standard size -> zoom in to tapped point (2.5x) // At standard size -> zoom in to tapped point (2.5x)
final position = _doubleTapDetails?.localPosition ?? Offset.zero; final position = _doubleTapDetails?.localPosition ?? Offset.zero;
final x = -position.dx * (2.5 - 1.0); final scenePoint = _transformationController.toScene(position);
final y = -position.dy * (2.5 - 1.0); final x = position.dx - scenePoint.dx * 2.5;
final y = position.dy - scenePoint.dy * 2.5;
targetMatrix = Matrix4.diagonal3Values(2.5, 2.5, 1.0) targetMatrix = Matrix4.diagonal3Values(2.5, 2.5, 1.0)
..setTranslationRaw(x, y, 0.0); ..setTranslationRaw(x, y, 0.0);
} }
@@ -205,7 +206,8 @@ class _PdfViewerScreenState extends State<PdfViewerScreen>
panEnabled: true, panEnabled: true,
scaleEnabled: true, scaleEnabled: true,
constrained: false, constrained: false,
alignment: Alignment.topCenter, boundaryMargin: const EdgeInsets.symmetric(
horizontal: 48, vertical: 80),
child: SizedBox( child: SizedBox(
width: viewportWidth, width: viewportWidth,
child: Padding( child: Padding(
+60 -1
View File
@@ -429,6 +429,8 @@ class _PostListScreenState extends State<PostListScreen> {
final canEdit = !item.isAussendung && (isAuthor || auth.isAdmin); final canEdit = !item.isAussendung && (isAuthor || auth.isAdmin);
final formattedDate = final formattedDate =
DateFormat('dd.MM.yyyy').format(item.createdDate); DateFormat('dd.MM.yyyy').format(item.createdDate);
final isIssue =
widget.collectionName == 'Issues' || item.street != null;
Widget? leadingWidget; Widget? leadingWidget;
if (item.imageUrl.isNotEmpty) { if (item.imageUrl.isNotEmpty) {
@@ -657,7 +659,64 @@ class _PostListScreenState extends State<PostListScreen> {
subtitle: Column( subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (item.street != null && item.street!.isNotEmpty) ...[ if (isIssue && item.status != null && item.status!.isNotEmpty) ...[
const SizedBox(height: 3),
Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
spacing: 6,
runSpacing: 4,
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: item.status == 'Erledigt'
? Colors.green.shade50
: (item.status == 'In Bearbeitung'
? Colors.blue.shade50
: Colors.orange.shade50),
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: item.status == 'Erledigt'
? Colors.green.shade300
: (item.status == 'In Bearbeitung'
? Colors.blue.shade300
: Colors.orange.shade300),
),
),
child: Text(
item.status!,
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: item.status == 'Erledigt'
? Colors.green.shade800
: (item.status == 'In Bearbeitung'
? Colors.blue.shade800
: Colors.orange.shade900),
),
),
),
if (item.street != null && item.street!.isNotEmpty)
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.location_on,
size: 12, color: Colors.redAccent),
const SizedBox(width: 2),
Text(
item.street!,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
),
],
),
],
),
] else if (item.street != null && item.street!.isNotEmpty) ...[
const SizedBox(height: 2), const SizedBox(height: 2),
Row( Row(
children: [ children: [
+106 -18
View File
@@ -35,8 +35,15 @@ class PdfService {
/// Print single post item as PDF /// Print single post item as PDF
static Future<void> printSinglePost(PostItem item) async { static Future<void> printSinglePost(PostItem item) async {
final pdf = pw.Document(); final pdf = pw.Document();
final formattedDate = DateFormat('dd.MM.yyyy HH:mm').format(item.createdDate); final formattedDate =
final pw.ImageProvider? imageProvider = await _fetchImage(item.imageUrl); DateFormat('dd.MM.yyyy HH:mm').format(item.createdDate);
final List<String> urlsToFetch = item.imageUrls.isNotEmpty
? item.imageUrls
: (item.imageUrl.isNotEmpty ? [item.imageUrl] : []);
final results =
await Future.wait(urlsToFetch.map((url) => _fetchImage(url)));
final imageProviders = results.whereType<pw.ImageProvider>().toList();
pdf.addPage( pdf.addPage(
pw.MultiPage( pw.MultiPage(
@@ -58,7 +65,8 @@ class PdfService {
), ),
pw.Text( pw.Text(
formattedDate, formattedDate,
style: const pw.TextStyle(fontSize: 12, color: PdfColors.grey700), style:
const pw.TextStyle(fontSize: 12, color: PdfColors.grey700),
), ),
], ],
), ),
@@ -68,6 +76,53 @@ class PdfService {
item.title, item.title,
style: pw.TextStyle(fontSize: 22, fontWeight: pw.FontWeight.bold), style: pw.TextStyle(fontSize: 22, fontWeight: pw.FontWeight.bold),
), ),
if (item.status != null && item.status!.isNotEmpty) ...[
pw.SizedBox(height: 6),
pw.Row(
children: [
pw.Text(
'Status: ',
style: pw.TextStyle(
fontSize: 12,
fontWeight: pw.FontWeight.bold,
color: PdfColors.grey800,
),
),
pw.Container(
padding:
const pw.EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: pw.BoxDecoration(
color: item.status == 'Erledigt'
? const PdfColor.fromInt(0xFFE8F5E9)
: (item.status == 'In Bearbeitung'
? const PdfColor.fromInt(0xFFE3F2FD)
: const PdfColor.fromInt(0xFFFFF3E0)),
borderRadius:
const pw.BorderRadius.all(pw.Radius.circular(4)),
border: pw.Border.all(
color: item.status == 'Erledigt'
? const PdfColor.fromInt(0xFF81C784)
: (item.status == 'In Bearbeitung'
? const PdfColor.fromInt(0xFF64B5F6)
: const PdfColor.fromInt(0xFFFFB74D)),
),
),
child: pw.Text(
item.status!,
style: pw.TextStyle(
fontSize: 11,
fontWeight: pw.FontWeight.bold,
color: item.status == 'Erledigt'
? const PdfColor.fromInt(0xFF2E7D32)
: (item.status == 'In Bearbeitung'
? const PdfColor.fromInt(0xFF1565C0)
: const PdfColor.fromInt(0xFFE65100)),
),
),
),
],
),
],
if (item.street != null && item.street!.isNotEmpty) ...[ if (item.street != null && item.street!.isNotEmpty) ...[
pw.SizedBox(height: 6), pw.SizedBox(height: 6),
pw.Text( pw.Text(
@@ -84,13 +139,29 @@ class PdfService {
], ],
pw.Divider(thickness: 1, color: PdfColors.grey400), pw.Divider(thickness: 1, color: PdfColors.grey400),
pw.SizedBox(height: 10), pw.SizedBox(height: 10),
if (imageProvider != null) ...[ if (imageProviders.isNotEmpty) ...[
pw.Center( if (imageProviders.length == 1)
child: pw.Container( pw.Center(
height: 220, child: pw.Container(
child: pw.Image(imageProvider, fit: pw.BoxFit.contain), height: 220,
child: pw.Image(imageProviders.first, fit: pw.BoxFit.contain),
),
)
else
pw.Row(
children: [
for (int i = 0; i < imageProviders.length; i++) ...[
if (i > 0) pw.SizedBox(width: 12),
pw.Expanded(
child: pw.Container(
height: 180,
child: pw.Image(imageProviders[i],
fit: pw.BoxFit.contain),
),
),
],
],
), ),
),
pw.SizedBox(height: 14), pw.SizedBox(height: 14),
], ],
pw.Text( pw.Text(
@@ -123,8 +194,10 @@ class PdfService {
// Preload all images in parallel // Preload all images in parallel
final Map<String, pw.ImageProvider?> imageMap = {}; final Map<String, pw.ImageProvider?> imageMap = {};
await Future.wait(items.map((item) async { await Future.wait(items.map((item) async {
if (item.imageUrl.isNotEmpty) { final url =
imageMap[item.id] = await _fetchImage(item.imageUrl); item.imageUrls.isNotEmpty ? item.imageUrls.first : item.imageUrl;
if (url.isNotEmpty) {
imageMap[item.id] = await _fetchImage(url);
} }
})); }));
@@ -137,7 +210,8 @@ class PdfService {
margin: const pw.EdgeInsets.only(bottom: 12), margin: const pw.EdgeInsets.only(bottom: 12),
padding: const pw.EdgeInsets.only(bottom: 6), padding: const pw.EdgeInsets.only(bottom: 6),
decoration: const pw.BoxDecoration( decoration: const pw.BoxDecoration(
border: pw.Border(bottom: pw.BorderSide(color: PdfColors.blue800, width: 2)), border: pw.Border(
bottom: pw.BorderSide(color: PdfColors.blue800, width: 2)),
), ),
child: pw.Row( child: pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
@@ -152,7 +226,8 @@ class PdfService {
), ),
pw.Text( pw.Text(
'Erstellt am: ${DateFormat('dd.MM.yyyy HH:mm').format(DateTime.now())}', 'Erstellt am: ${DateFormat('dd.MM.yyyy HH:mm').format(DateTime.now())}',
style: const pw.TextStyle(fontSize: 10, color: PdfColors.grey700), style:
const pw.TextStyle(fontSize: 10, color: PdfColors.grey700),
), ),
], ],
), ),
@@ -160,7 +235,8 @@ class PdfService {
build: (context) => [ build: (context) => [
pw.TableHelper.fromTextArray( pw.TableHelper.fromTextArray(
border: pw.TableBorder.all(color: PdfColors.grey400, width: 0.5), border: pw.TableBorder.all(color: PdfColors.grey400, width: 0.5),
headerStyle: pw.TextStyle(fontWeight: pw.FontWeight.bold, fontSize: 10), headerStyle:
pw.TextStyle(fontWeight: pw.FontWeight.bold, fontSize: 10),
headerDecoration: const pw.BoxDecoration(color: PdfColors.grey200), headerDecoration: const pw.BoxDecoration(color: PdfColors.grey200),
cellHeight: 65, cellHeight: 65,
cellAlignments: { cellAlignments: {
@@ -170,20 +246,32 @@ class PdfService {
3: pw.Alignment.centerLeft, 3: pw.Alignment.centerLeft,
4: pw.Alignment.center, 4: pw.Alignment.center,
}, },
headers: <String>['Datum', 'Örtlichkeit / Kat.', 'Titel & Mangel', 'Status', 'Foto'], headers: <String>[
'Datum',
'Örtlichkeit / Kat.',
'Titel & Mangel',
'Status',
'Foto'
],
data: items.map((item) { data: items.map((item) {
final formattedDate = DateFormat('dd.MM.yyyy').format(item.createdDate); final formattedDate =
final locInfo = '${item.street ?? "Keine Angabe"}\n(${item.category ?? "-"})'; DateFormat('dd.MM.yyyy').format(item.createdDate);
final locInfo =
'${item.street ?? "Keine Angabe"}\n(${item.category ?? "-"})';
final descSnippet = item.description.length > 120 final descSnippet = item.description.length > 120
? '${item.description.substring(0, 120)}...' ? '${item.description.substring(0, 120)}...'
: item.description; : item.description;
final imageCount = item.imageUrls.length;
final imageLabel = imageMap[item.id] != null
? (imageCount > 1 ? '$imageCount Fotos' : 'Foto vorhanden')
: '-';
return [ return [
formattedDate, formattedDate,
locInfo, locInfo,
'${item.title}\n$descSnippet', '${item.title}\n$descSnippet',
item.status ?? 'Gemeldet', item.status ?? 'Gemeldet',
imageMap[item.id] != null ? 'Foto vorhanden' : '-', imageLabel,
]; ];
}).toList(), }).toList(),
), ),
+41
View File
@@ -14,6 +14,7 @@ void main() {
createdDate: now, createdDate: now,
street: 'Hauptstraße', street: 'Hauptstraße',
category: 'Straßenbeleuchtung', category: 'Straßenbeleuchtung',
status: 'Gemeldet',
); );
expect(item.id, 'test-123'); expect(item.id, 'test-123');
@@ -24,6 +25,46 @@ void main() {
expect(item.status, 'Gemeldet'); expect(item.status, 'Gemeldet');
}); });
test('defaults status to Gemeldet only for issues and null for news/ideas', () {
final issueJson = {
'id': 'issue-1',
'title': 'Schlagloch',
'description': 'Tiefes Schlagloch',
'createdDate': '2026-09-18T10:00:00.000',
'street': 'Kirchengasse',
'category': 'Straße',
};
final issueItem = PostItem.fromJson(issueJson);
expect(issueItem.status, 'Gemeldet');
final newsJson = {
'id': 'news-1',
'title': 'Dorffest Ankündigung',
'description': 'Am Samstag findet das Fest statt.',
'createdDate': '2026-09-18T10:00:00.000',
};
final newsItem = PostItem.fromJson(newsJson);
expect(newsItem.status, isNull);
});
test('handles multiple images in PostItem', () {
final item = PostItem(
id: 'multi-img-1',
title: 'Baustelle',
description: 'Zwei Fotos vorhanden',
createdDate: DateTime.now(),
imageUrls: [
'https://example.com/img1.jpg',
'https://example.com/img2.jpg',
],
);
expect(item.imageUrls.length, 2);
expect(item.imageUrls[0], 'https://example.com/img1.jpg');
expect(item.imageUrls[1], 'https://example.com/img2.jpg');
expect(item.imageUrl, 'https://example.com/img1.jpg');
});
test('serializes to Firestore map accurately', () { test('serializes to Firestore map accurately', () {
final now = DateTime.now(); final now = DateTime.now();
final item = PostItem( final item = PostItem(