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
Build Android APK / Build Release APKs (push) Successful in 14m49s
This commit is contained in:
@@ -19,17 +19,20 @@ class PostItem {
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.description,
|
||||
this.imageUrl = '',
|
||||
String imageUrl = '',
|
||||
List<String>? imageUrls,
|
||||
required this.createdDate,
|
||||
this.authorUid,
|
||||
this.street,
|
||||
this.category,
|
||||
this.status = 'Gemeldet',
|
||||
this.status,
|
||||
this.pdfUrl,
|
||||
this.webUrl,
|
||||
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) {
|
||||
final data = doc.data() as Map<String, dynamic>? ?? {};
|
||||
@@ -49,6 +52,9 @@ class PostItem {
|
||||
images = [legacyImage];
|
||||
}
|
||||
|
||||
final hasIssueFields = data['street'] != null || data['category'] != null;
|
||||
final status = data['status'] as String? ?? (hasIssueFields ? 'Gemeldet' : null);
|
||||
|
||||
return PostItem(
|
||||
id: doc.id,
|
||||
title: data['title'] as String? ?? '',
|
||||
@@ -59,7 +65,7 @@ class PostItem {
|
||||
authorUid: data['author_uid'] as String?,
|
||||
street: data['street'] as String?,
|
||||
category: data['category'] as String?,
|
||||
status: data['status'] as String? ?? 'Gemeldet',
|
||||
status: status,
|
||||
pdfUrl: data['pdfUrl'] as String?,
|
||||
webUrl: data['webUrl'] as String?,
|
||||
isAussendung: data['isAussendung'] as bool? ?? false,
|
||||
@@ -119,17 +125,23 @@ class PostItem {
|
||||
images = [legacyImage];
|
||||
}
|
||||
|
||||
final hasIssueFields = map['street'] != null || map['category'] != null;
|
||||
final status =
|
||||
map['status'] as String? ?? (hasIssueFields ? 'Gemeldet' : null);
|
||||
|
||||
return PostItem(
|
||||
id: map['id'] as String? ?? '',
|
||||
title: map['title'] 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,
|
||||
createdDate: date,
|
||||
authorUid: map['authorUid'] as String?,
|
||||
street: map['street'] as String?,
|
||||
category: map['category'] as String?,
|
||||
status: map['status'] as String? ?? 'Gemeldet',
|
||||
status: status,
|
||||
pdfUrl: map['pdfUrl'] as String?,
|
||||
webUrl: map['webUrl'] as String?,
|
||||
isAussendung: map['isAussendung'] as bool? ?? false,
|
||||
|
||||
+154
-54
@@ -27,6 +27,8 @@ class DetailScreen extends StatefulWidget {
|
||||
|
||||
class _DetailScreenState extends State<DetailScreen> {
|
||||
late PostItem _item;
|
||||
int _currentImageIndex = 0;
|
||||
final PageController _pageController = PageController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -34,6 +36,12 @@ class _DetailScreenState extends State<DetailScreen> {
|
||||
_item = widget.item;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pageController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _editItem() async {
|
||||
final collection = widget.collectionName ??
|
||||
(_item.street != null ? 'Issues' : 'CitizensForum');
|
||||
@@ -118,6 +126,7 @@ class _DetailScreenState extends State<DetailScreen> {
|
||||
final isAuthor = currentUser != null && _item.authorUid == currentUser.uid;
|
||||
final canEdit = !_item.isAussendung && (isAuthor || auth.isAdmin);
|
||||
final canDelete = !_item.isAussendung && (isAuthor || auth.isAdmin);
|
||||
final isIssue = widget.collectionName == 'Issues' || _item.street != null;
|
||||
|
||||
final formattedDate =
|
||||
DateFormat('dd.MM.yyyy HH:mm').format(_item.createdDate);
|
||||
@@ -155,27 +164,131 @@ class _DetailScreenState extends State<DetailScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (_item.imageUrls.isNotEmpty)
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: _item.imageUrls.first,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (context, url) => const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(32.0),
|
||||
child: CircularProgressIndicator(),
|
||||
if (_item.imageUrls.isNotEmpty) ...[
|
||||
if (_item.imageUrls.length == 1)
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: _item.imageUrls.first,
|
||||
width: double.infinity,
|
||||
height: 240,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (context, url) => const Center(
|
||||
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,
|
||||
color: Colors.grey.shade200,
|
||||
child: const Icon(Icons.image_not_supported, size: 48),
|
||||
),
|
||||
)
|
||||
else
|
||||
Stack(
|
||||
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(
|
||||
_item.title,
|
||||
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
|
||||
@@ -189,22 +302,37 @@ class _DetailScreenState extends State<DetailScreen> {
|
||||
formattedDate,
|
||||
style: TextStyle(fontSize: 13, color: Colors.grey.shade700),
|
||||
),
|
||||
if (_item.status != null) ...[
|
||||
if (isIssue && _item.status != null) ...[
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10, vertical: 4),
|
||||
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),
|
||||
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(
|
||||
_item.status!,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.blue.shade800),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
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),
|
||||
// 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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -61,8 +61,9 @@ class _PdfViewerScreenState extends State<PdfViewerScreen>
|
||||
} else {
|
||||
// At standard size -> zoom in to tapped point (2.5x)
|
||||
final position = _doubleTapDetails?.localPosition ?? Offset.zero;
|
||||
final x = -position.dx * (2.5 - 1.0);
|
||||
final y = -position.dy * (2.5 - 1.0);
|
||||
final scenePoint = _transformationController.toScene(position);
|
||||
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)
|
||||
..setTranslationRaw(x, y, 0.0);
|
||||
}
|
||||
@@ -205,7 +206,8 @@ class _PdfViewerScreenState extends State<PdfViewerScreen>
|
||||
panEnabled: true,
|
||||
scaleEnabled: true,
|
||||
constrained: false,
|
||||
alignment: Alignment.topCenter,
|
||||
boundaryMargin: const EdgeInsets.symmetric(
|
||||
horizontal: 48, vertical: 80),
|
||||
child: SizedBox(
|
||||
width: viewportWidth,
|
||||
child: Padding(
|
||||
|
||||
@@ -429,6 +429,8 @@ class _PostListScreenState extends State<PostListScreen> {
|
||||
final canEdit = !item.isAussendung && (isAuthor || auth.isAdmin);
|
||||
final formattedDate =
|
||||
DateFormat('dd.MM.yyyy').format(item.createdDate);
|
||||
final isIssue =
|
||||
widget.collectionName == 'Issues' || item.street != null;
|
||||
|
||||
Widget? leadingWidget;
|
||||
if (item.imageUrl.isNotEmpty) {
|
||||
@@ -657,7 +659,64 @@ class _PostListScreenState extends State<PostListScreen> {
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
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),
|
||||
Row(
|
||||
children: [
|
||||
|
||||
+106
-18
@@ -35,8 +35,15 @@ class PdfService {
|
||||
/// Print single post item as PDF
|
||||
static Future<void> printSinglePost(PostItem item) async {
|
||||
final pdf = pw.Document();
|
||||
final formattedDate = DateFormat('dd.MM.yyyy HH:mm').format(item.createdDate);
|
||||
final pw.ImageProvider? imageProvider = await _fetchImage(item.imageUrl);
|
||||
final formattedDate =
|
||||
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(
|
||||
pw.MultiPage(
|
||||
@@ -58,7 +65,8 @@ class PdfService {
|
||||
),
|
||||
pw.Text(
|
||||
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,
|
||||
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) ...[
|
||||
pw.SizedBox(height: 6),
|
||||
pw.Text(
|
||||
@@ -84,13 +139,29 @@ class PdfService {
|
||||
],
|
||||
pw.Divider(thickness: 1, color: PdfColors.grey400),
|
||||
pw.SizedBox(height: 10),
|
||||
if (imageProvider != null) ...[
|
||||
pw.Center(
|
||||
child: pw.Container(
|
||||
height: 220,
|
||||
child: pw.Image(imageProvider, fit: pw.BoxFit.contain),
|
||||
if (imageProviders.isNotEmpty) ...[
|
||||
if (imageProviders.length == 1)
|
||||
pw.Center(
|
||||
child: pw.Container(
|
||||
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.Text(
|
||||
@@ -123,8 +194,10 @@ class PdfService {
|
||||
// Preload all images in parallel
|
||||
final Map<String, pw.ImageProvider?> imageMap = {};
|
||||
await Future.wait(items.map((item) async {
|
||||
if (item.imageUrl.isNotEmpty) {
|
||||
imageMap[item.id] = await _fetchImage(item.imageUrl);
|
||||
final url =
|
||||
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),
|
||||
padding: const pw.EdgeInsets.only(bottom: 6),
|
||||
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(
|
||||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
||||
@@ -152,7 +226,8 @@ class PdfService {
|
||||
),
|
||||
pw.Text(
|
||||
'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) => [
|
||||
pw.TableHelper.fromTextArray(
|
||||
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),
|
||||
cellHeight: 65,
|
||||
cellAlignments: {
|
||||
@@ -170,20 +246,32 @@ class PdfService {
|
||||
3: pw.Alignment.centerLeft,
|
||||
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) {
|
||||
final formattedDate = DateFormat('dd.MM.yyyy').format(item.createdDate);
|
||||
final locInfo = '${item.street ?? "Keine Angabe"}\n(${item.category ?? "-"})';
|
||||
final formattedDate =
|
||||
DateFormat('dd.MM.yyyy').format(item.createdDate);
|
||||
final locInfo =
|
||||
'${item.street ?? "Keine Angabe"}\n(${item.category ?? "-"})';
|
||||
final descSnippet = item.description.length > 120
|
||||
? '${item.description.substring(0, 120)}...'
|
||||
: item.description;
|
||||
final imageCount = item.imageUrls.length;
|
||||
final imageLabel = imageMap[item.id] != null
|
||||
? (imageCount > 1 ? '$imageCount Fotos' : 'Foto vorhanden')
|
||||
: '-';
|
||||
|
||||
return [
|
||||
formattedDate,
|
||||
locInfo,
|
||||
'${item.title}\n$descSnippet',
|
||||
item.status ?? 'Gemeldet',
|
||||
imageMap[item.id] != null ? 'Foto vorhanden' : '-',
|
||||
imageLabel,
|
||||
];
|
||||
}).toList(),
|
||||
),
|
||||
|
||||
@@ -14,6 +14,7 @@ void main() {
|
||||
createdDate: now,
|
||||
street: 'Hauptstraße',
|
||||
category: 'Straßenbeleuchtung',
|
||||
status: 'Gemeldet',
|
||||
);
|
||||
|
||||
expect(item.id, 'test-123');
|
||||
@@ -24,6 +25,46 @@ void main() {
|
||||
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', () {
|
||||
final now = DateTime.now();
|
||||
final item = PostItem(
|
||||
|
||||
Reference in New Issue
Block a user