-
Notifications
You must be signed in to change notification settings - Fork 609
Open
Labels
area-executiontype-bugIncorrect behavior (everything from a crash to more subtle misbehavior)Incorrect behavior (everything from a crash to more subtle misbehavior)
Description
Relevant console output
Uncaught Library package:dartpad_sample/main.dart was previously defined but DDC is not currently executing a hot reload or a hot restart. Failed to define the library., error: Library package:dartpad_sample/main.dart was previously defined but DDC is not currently executing a hot reload or a hot restart. Failed to define the library.When did the error happen?
Uncaught Library package:dartpad_sample/main.dart was previously defined but DDC is not currently executing a hot reload or a hot restart. Failed to define the library., error: Library package:dartpad_sample/main.dart was previously defined but DDC is not currently executing a hot reload or a hot restart. Failed to define the library.
Your Dart code
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:provider/provider.dart';
import 'dart:async'; // Required for Future and async operations
// ---------- DATA MODELS ----------
class Report {
final String type;
final String desc;
final String loc;
final int ts;
Report({required this.type, required this.desc, required this.loc, required this.ts});
Map<String, dynamic> toJson() => {'type': type, 'desc': desc, 'loc': loc, 'ts': ts};
static Report fromJson(Map<String, dynamic> j) =>
Report(type: j['type'] as String, desc: j['desc'] as String, loc: j['loc'] as String, ts: j['ts'] as int);
}
enum Role { guest, user, authority, plumber }
class WorkItem {
final String id; // unique
final int reportTs; // links to Report.ts
String? assignedTo; // plumber name
String status; // 'pending' | 'completed'
final int createdAt;
int? completedAt;
WorkItem({
required this.id,
required this.reportTs,
this.assignedTo,
this.status = 'pending',
int? createdAt,
this.completedAt,
}) : createdAt = createdAt ?? DateTime.now().millisecondsSinceEpoch;
Map<String, dynamic> toJson() => {
'id': id,
'reportTs': reportTs,
'assignedTo': assignedTo,
'status': status,
'createdAt': createdAt,
'completedAt': completedAt,
};
static WorkItem fromJson(Map<String, dynamic> j) => WorkItem(
id: j['id'] as String,
reportTs: j['reportTs'] as int,
assignedTo: j['assignedTo'] as String?,
status: j['status'] as String,
createdAt: j['createdAt'] as int,
completedAt: j['completedAt'] as int?,
);
}
// ---------- STRINGS / I18N ----------
const storageKeyReports = 'waterdrop_reports_v2';
const storageKeyWork = 'waterdrop_work_v1';
const storageKeyUser = 'username';
const storageKeyRole = 'role';
const Map<String, Map<String, dynamic>> T = {
'en': {
'title': 'Water Issue Reporter',
'subtitle': 'Report leaks, overflows & shortages.',
'langLabel': 'Switch Language',
'reportHeading': 'Report an Issue',
'typeLabel': 'Issue Type',
'descLabel': 'Description',
'locLabel': 'Location',
'getLocBtn': 'Get Location (Simulated)',
'submitBtn': 'Submit',
'recentHeading': 'Recent Reports',
'noReports': 'No reports yet.',
'chartHeading': 'Issue Summary',
'footer': 'Community-led monitoring to reduce water wastage.',
'confirmSubmit': 'Report submitted.',
'badLocation': 'Invalid coordinates.',
'delete': 'Delete',
'profile': 'User Profile',
'reminder': 'Enable Weekly Reminder (Removed)',
'setName': 'Set / Change Name',
'authority': 'Authority',
'plumber': 'Plumber',
'login': 'Login',
'logout': 'Logout',
'assign': 'Assign',
'assigned': 'Assigned',
'unassigned': 'Unassigned',
'markDone': 'Mark Completed',
'assignedTo': 'Assigned to',
'tasks': 'Plumber Tasks',
'manage': 'Authority Dispatch',
'pickPlumber': 'Pick plumber',
},
'hi': {
'title': 'जल समस्या रिपोर्टर',
'subtitle': 'लीक, ओवरफ्लो और कमी की रिपोर्ट करें।',
'langLabel': 'भाषा बदलें',
'reportHeading': 'समस्या रिपोर्ट करें',
'typeLabel': 'समस्या प्रकार',
'descLabel': 'विवरण',
'locLabel': 'स्थान',
'getLocBtn': 'स्थान प्राप्त करें (सिम्युलेटेड)',
'submitBtn': 'सबमिट करें',
'recentHeading': 'हाल की रिपोर्टें',
'noReports': 'अभी कोई रिपोर्ट नहीं है।',
'chartHeading': 'समस्याओं का सारांश',
'footer': 'पानी की बर्बादी कम करने के लिए सामुदायिक निगरानी।',
'confirmSubmit': 'रिपोर्ट सबमिट की गई।',
'badLocation': 'अमान्य स्थान निर्देशांक।',
'delete': 'हटाएं',
'profile': 'उपयोगकर्ता प्रोफ़ाइल',
'reminder': 'साप्ताहिक रिमाइंडर सक्षम करें (हटाया गया)',
'setName': 'नाम सेट/बदलें',
'authority': 'प्राधिकरण',
'plumber': 'प्लंबर',
'login': 'लॉगिन',
'logout': 'लॉगआउट',
'assign': 'असाइन करें',
'assigned': 'असाइन किया गया',
'unassigned': 'अनअसाइन',
'markDone': 'पूर्ण चिह्नित करें',
'assignedTo': 'असाइन टू',
'tasks': 'प्लंबर कार्य',
'manage': 'डिस्पैच',
'pickPlumber': 'प्लंबर चुनें',
},
'te': {
'title': 'నీటి సమస్య రిపోర్టర్',
'subtitle': 'లీకులు, ఓవర్ఫ్లోలు & కొరతలను నివేదించండి.',
'langLabel': 'భాష మార్చండి',
'reportHeading': 'సమస్య నివేదించండి',
'typeLabel': 'సమస్య రకం',
'descLabel': 'వివరణ',
'locLabel': 'స్థానం',
'getLocBtn': 'స్థానం పొందండి (సిమ్యులేటెడ్)',
'submitBtn': 'సమర్పించు',
'recentHeading': 'ఇటీవలి నివేదికలు',
'noReports': 'ఇంకా నివేదికలు లేవు.',
'chartHeading': 'సమస్యల సారాంశం',
'footer': 'నీటి వృథా తగ్గించడానికి కమ్యూనిటీ పర్యవేక్షణ.',
'confirmSubmit': 'నివేదిక సమర్పించబడింది.',
'badLocation': 'తప్పుడు సమన్వయాలు.',
'delete': 'తొలగించు',
'profile': 'ఉపయోగదారు ప్రొఫైల్',
'reminder': 'వీక్లీ రిమైండర్ ఆన్ చేయి (తొలగించబడింది)',
'setName': 'పేరు సెట్ / మార్పు',
'authority': 'అథారిటీ',
'plumber': 'ప్లంబర్',
'login': 'లాగిన్',
'logout': 'లాగౌట్',
'assign': 'అసైన్ చేయి',
'assigned': 'అసైన్ అయింది',
'unassigned': 'అన్అసైన్',
'markDone': 'కంప్లీట్ చేయి',
'assignedTo': 'అసైన్ టు',
'tasks': 'ప్లంబర్ పనులు',
'manage': 'డిస్పాచ్',
'pickPlumber': 'ప్లంబర్ ఎంచుకోండి',
}
};
// ---------- WaterDropData ChangeNotifier ----------
class WaterDropData extends ChangeNotifier {
late SharedPreferences _prefs;
String _lang = 'en';
String _issueType = 'leak';
List<Report> _reports = [];
List<WorkItem> _work = [];
String? _userName;
Role _role = Role.guest;
bool _gettingLocation = false;
final List<String> _plumbers = const <String>[
'Sita Ram',
'Anaya Khan',
'Ravi Teja',
'Mohan Das',
'Priya Nair',
];
// Getters
String get lang => _lang;
String get issueType => _issueType;
List<Report> get reports => List<Report>.unmodifiable(_reports);
List<WorkItem> get work => List<WorkItem>.unmodifiable(_work);
String? get userName => _userName;
Role get role => _role;
bool get gettingLocation => _gettingLocation;
List<String> get plumbers => List<String>.unmodifiable(_plumbers);
// Initialization
Future<void> init() async {
_prefs = await SharedPreferences.getInstance();
await _loadStateFromPrefs();
notifyListeners();
}
// Persistence methods
Future<void> _loadStateFromPrefs() async {
final String? rawReports = _prefs.getString(storageKeyReports);
if (rawReports != null) {
try {
_reports = (jsonDecode(rawReports) as List<dynamic>)
.map<Report>((dynamic e) => Report.fromJson(e as Map<String, dynamic>))
.toList();
} catch (e) {
debugPrint('Error loading reports: $e');
}
}
final String? rawWork = _prefs.getString(storageKeyWork);
if (rawWork != null) {
try {
_work = (jsonDecode(rawWork) as List<dynamic>)
.map<WorkItem>((dynamic e) => WorkItem.fromJson(e as Map<String, dynamic>))
.toList();
} catch (e) {
debugPrint('Error loading work items: $e');
}
}
_userName = _prefs.getString(storageKeyUser);
final String? r = _prefs.getString(storageKeyRole);
_role = _roleFromString(r);
}
Future<void> _saveReports() async =>
_prefs.setString(storageKeyReports, jsonEncode(_reports.map((Report e) => e.toJson()).toList()));
Future<void> _saveWork() async =>
_prefs.setString(storageKeyWork, jsonEncode(_work.map((WorkItem e) => e.toJson()).toList()));
// I18n
String t(String k) {
final dynamic value = T[_lang]?[k];
if (value is String) return value;
return T['en']![k] as String? ?? k;
}
// State modification methods
void setLang(String newLang) {
_lang = newLang;
notifyListeners();
}
void setIssueType(String type) {
_issueType = type;
notifyListeners();
}
Future<String> getSimulatedLocation() async {
_gettingLocation = true;
notifyListeners();
await Future<void>.delayed(const Duration(seconds: 1)); // Simulate network delay
_gettingLocation = false;
notifyListeners();
return '12.9716, 77.5946 (Bengaluru)'; // Fixed dummy location
}
Future<void> submitReport({
required String type,
required String desc,
required String loc,
}) async {
final Report rep = Report(
type: type,
desc: desc,
loc: loc,
ts: DateTime.now().millisecondsSinceEpoch,
);
_reports.add(rep);
_work.add(WorkItem(id: 'W-${rep.ts}', reportTs: rep.ts, status: 'pending'));
await _saveReports();
await _saveWork();
notifyListeners();
}
Future<void> deleteReport(Report reportToDelete) async {
_reports.removeWhere((Report r) => r.ts == reportToDelete.ts);
_work.removeWhere((WorkItem w) => w.reportTs == reportToDelete.ts);
await _saveReports();
await _saveWork();
notifyListeners();
}
Map<String, int> getIssueCounts() {
final Map<String, int> c = {'leak': 0, 'overflow': 0, 'shortage': 0};
for (final Report r in _reports) {
c[r.type] = (c[r.type] ?? 0) + 1;
}
return c;
}
// Session / Login
Role _roleFromString(String? s) {
switch (s) {
case 'user':
return Role.user;
case 'authority':
return Role.authority;
case 'plumber':
return Role.plumber;
default:
return Role.guest;
}
}
String roleToString(Role r) {
switch (r) {
case Role.user:
return 'user';
case Role.authority:
return 'authority';
case Role.plumber:
return 'plumber';
default:
return 'guest';
}
}
Future<void> setUserName(String? name) async {
_userName = name?.trim().isEmpty == true ? null : name?.trim();
await _prefs.setString(storageKeyUser, _userName ?? '');
notifyListeners();
}
Future<void> loginAuthority(String name) async {
_userName = name.trim().isEmpty ? null : name.trim();
await _prefs.setString(storageKeyUser, _userName ?? '');
await _prefs.setString(storageKeyRole, 'authority');
_role = Role.authority;
notifyListeners();
}
Future<void> loginPlumber(String name) async {
_userName = name; // Assumed valid name from plumbers list
await _prefs.setString(storageKeyUser, _userName!);
await _prefs.setString(storageKeyRole, 'plumber');
_role = Role.plumber;
notifyListeners();
}
Future<void> logout() async {
await _prefs.setString(storageKeyRole, 'guest');
_role = Role.guest;
notifyListeners();
}
Future<void> assignWorkItem(WorkItem workItem, String plumberName) async {
final int index = _work.indexWhere((WorkItem w) => w.id == workItem.id);
if (index != -1) {
_work[index].assignedTo = plumberName;
await _saveWork();
notifyListeners();
}
}
Future<void> markWorkItemCompleted(WorkItem workItem) async {
final int index = _work.indexWhere((WorkItem w) => w.id == workItem.id);
if (index != -1) {
_work[index].status = 'completed';
_work[index].completedAt = DateTime.now().millisecondsSinceEpoch;
await _saveWork();
notifyListeners();
}
}
}
// ---------- APP ENTRY POINT ----------
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const WaterDropApp());
}
class WaterDropApp extends StatelessWidget {
const WaterDropApp({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<WaterDropData>(
create: (BuildContext context) => WaterDropData()..init(), // Initialize data model
builder: (BuildContext context, Widget? child) => MaterialApp(
title: 'WaterDrop',
debugShowCheckedModeBanner: false,
theme: ThemeData.dark(),
home: const HomeScreen(),
),
);
}
}
// ---------- HOME SCREEN (Stateless Widget) ----------
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
void _showSnack(BuildContext context, String msg) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: <Color>[Color(0xFF071029), Color(0xFF092037)],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Consumer<WaterDropData>(
builder: (BuildContext context, WaterDropData waterDropData, Widget? child) {
final Map<String, int> counts = waterDropData.getIssueCounts();
final double maxVal = (counts.values.isNotEmpty)
? counts.values.reduce((int a, int b) => a > b ? a : b).toDouble()
: 1.0;
return Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: <Widget>[
// Top bar: left profile, right language + quick logins
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
TopLeftProfileWidget(
userName: waterDropData.userName,
role: waterDropData.role,
onAskUserName: () => _askUserNameDialog(context, waterDropData),
onLogout: () async {
await waterDropData.logout();
_showSnack(context, waterDropData.t('logout'));
},
),
LanguageAndAuthButtons(
lang: waterDropData.lang,
onLangChanged: waterDropData.setLang,
onLoginAuthority: () => _loginAuthorityDialog(context, waterDropData),
onLoginPlumber: () => _loginPlumberDialog(context, waterDropData),
),
],
),
const SizedBox(height: 8),
// Title
Row(
children: <Widget>[
Expanded(
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[
Text(waterDropData.t('title'),
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
Text(waterDropData.t('subtitle'),
style: const TextStyle(color: Color(0xFF9AA4B2), fontSize: 13)),
]),
),
],
),
const SizedBox(height: 16),
// -- BLUE citizen report card (always visible)
ReportCardWidget(
issueType: waterDropData.issueType,
onIssueTypeChanged: waterDropData.setIssueType,
gettingLocation: waterDropData.gettingLocation,
onGetLocation: waterDropData.getSimulatedLocation,
onSubmitReport: (String type, String desc, String loc) async {
await waterDropData.submitReport(type: type, desc: desc, loc: loc);
_showSnack(context, waterDropData.t('confirmSubmit'));
},
t: waterDropData.t,
),
const SizedBox(height: 14),
RecentReportsWidget(
reports: waterDropData.reports,
workItems: waterDropData.work,
onDeleteReport: (Report report) async {
await waterDropData.deleteReport(report);
_showSnack(context, waterDropData.t('delete'));
},
t: waterDropData.t,
),
const SizedBox(height: 14),
IssueSummarySectionWidget(
data: counts,
maxVal: maxVal,
t: waterDropData.t,
),
// ---- ROLE PANELS (SAME PAGE) ----
const SizedBox(height: 14),
if (waterDropData.role == Role.authority)
AuthorityPanelWidget(
workItems: waterDropData.work,
reports: waterDropData.reports,
plumbers: waterDropData.plumbers,
onAssignWorkItem: (WorkItem workItem, String plumberName) async {
await waterDropData.assignWorkItem(workItem, plumberName);
_showSnack(context, '${waterDropData.t('assignedTo')}: $plumberName');
},
t: waterDropData.t,
),
if (waterDropData.role == Role.plumber)
PlumberPanelWidget(
workItems: waterDropData.work,
reports: waterDropData.reports,
plumberName: waterDropData.userName,
onMarkWorkItemCompleted: (WorkItem workItem) async {
await waterDropData.markWorkItemCompleted(workItem);
_showSnack(context, waterDropData.t('markDone'));
},
t: waterDropData.t,
),
const SizedBox(height: 14),
ProfileCardWidget(
userName: waterDropData.userName,
totalReports: waterDropData.reports.length,
completedWorkItems: waterDropData.work.where((WorkItem w) => w.status == 'completed').length,
onAskUserName: () => _askUserNameDialog(context, waterDropData),
t: waterDropData.t,
),
const SizedBox(height: 14),
Center(child: Text(waterDropData.t('footer'), style: const TextStyle(color: Color(0xFF9AA4B2)))),
]);
},
),
),
),
),
);
}
Future<void> _askUserNameDialog(BuildContext context, WaterDropData waterDropData) async {
final TextEditingController controller = TextEditingController(text: waterDropData.userName ?? '');
await showDialog<void>(
context: context,
builder: (BuildContext ctx) => AlertDialog(
title: const Text('Enter your name'),
content: TextField(controller: controller, autofocus: true),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.pop(ctx);
waterDropData.setUserName(controller.text);
},
child: const Text('Save'),
)
],
),
);
}
Future<void> _loginAuthorityDialog(BuildContext context, WaterDropData waterDropData) async {
final TextEditingController controller = TextEditingController();
String? name;
await showDialog<void>(
context: context,
builder: (BuildContext ctx) => AlertDialog(
title: Text('${waterDropData.t('authority')} ${waterDropData.t('login')}'),
content: TextField(controller: controller, decoration: const InputDecoration(labelText: 'Name')),
actions: <Widget>[
TextButton(
onPressed: () {
name = controller.text.trim();
Navigator.pop(ctx);
},
child: const Text('Continue'))
],
),
);
if ((name ?? '').isEmpty) return;
await waterDropData.loginAuthority(name!);
_showSnack(context, '${waterDropData.t('login')} as ${waterDropData.t('authority')}');
}
Future<void> _loginPlumberDialog(BuildContext context, WaterDropData waterDropData) async {
String? picked;
await showDialog<void>(
context: context,
builder: (BuildContext ctx) => AlertDialog(
title: Text('${waterDropData.t('plumber')} ${waterDropData.t('login')}'),
// The DropdownButtonFormField is wrapped in a StatefulBuilder to ensure its internal state updates correctly
// when a selection is made within the dialog.
content: StatefulBuilder(
builder: (BuildContext innerContext, StateSetter innerSetState) {
return DropdownButtonFormField<String>(
// Corrected: DropdownButtonFormField uses 'value' to control the selected item.
initialValue: picked,
items: waterDropData.plumbers.map<DropdownMenuItem<String>>((String p) => DropdownMenuItem<String>(value: p, child: Text(p))).toList(),
onChanged: (String? v) {
innerSetState(() { // Use innerSetState to update the dialog's local state
picked = v;
});
},
decoration: InputDecoration(labelText: waterDropData.t('pickPlumber')),
);
},
),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('Cancel')),
ElevatedButton(
onPressed: () {
if (picked != null) Navigator.pop(ctx);
},
child: const Text('OK'))
],
),
);
if (picked == null) return;
await waterDropData.loginPlumber(picked!);
_showSnack(context, '${waterDropData.t('login')} as ${waterDropData.t('plumber')}');
}
}
// ---------- UI Widgets (Converted from private build functions) ----------
class TopLeftProfileWidget extends StatelessWidget {
final String? userName;
final Role role;
final VoidCallback onAskUserName;
final VoidCallback onLogout;
const TopLeftProfileWidget({
super.key,
required this.userName,
required this.role,
required this.onAskUserName,
required this.onLogout,
});
@override
Widget build(BuildContext context) {
final WaterDropData waterDropData = Provider.of<WaterDropData>(context, listen: false);
final String r = waterDropData.roleToString(role);
final String label = userName == null || userName!.isEmpty ? 'Guest' : userName!;
return InkWell(
onTap: onAskUserName,
child: Row(
children: <Widget>[
const CircleAvatar(
radius: 18,
backgroundColor: Color(0xFF4EA5FF),
child: Icon(Icons.person, color: Colors.white, size: 18),
),
const SizedBox(width: 8),
Column(crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[
Text(label, style: const TextStyle(fontWeight: FontWeight.w600)),
Text(r.toUpperCase(), style: const TextStyle(color: Color(0xFF9AA4B2), fontSize: 11)),
]),
const SizedBox(width: 8),
if (role != Role.guest)
TextButton(onPressed: onLogout, child: Text(waterDropData.t('logout'))),
],
),
);
}
}
class LanguageAndAuthButtons extends StatelessWidget {
final String lang;
final ValueChanged<String> onLangChanged;
final VoidCallback onLoginAuthority;
final VoidCallback onLoginPlumber;
const LanguageAndAuthButtons({
super.key,
required this.lang,
required this.onLangChanged,
required this.onLoginAuthority,
required this.onLoginPlumber,
});
@override
Widget build(BuildContext context) {
final WaterDropData waterDropData = Provider.of<WaterDropData>(context, listen: false);
return Row(
children: <Widget>[
DropdownButton<String>(
value: lang,
dropdownColor: const Color(0xFF071029),
items: const <DropdownMenuItem<String>>[
DropdownMenuItem<String>(value: 'en', child: Text('English')),
DropdownMenuItem<String>(value: 'hi', child: Text('हिन्दी')),
DropdownMenuItem<String>(value: 'te', child: Text('తెలుగు')),
],
onChanged: (String? v) {
if (v != null) onLangChanged(v);
},
),
const SizedBox(width: 8),
TextButton(
onPressed: onLoginAuthority,
child: Text('${waterDropData.t('authority')} ${waterDropData.t('login')}'),
),
const SizedBox(width: 6),
TextButton(
onPressed: onLoginPlumber,
child: Text('${waterDropData.t('plumber')} ${waterDropData.t('login')}'),
),
],
);
}
}
class ReportCardWidget extends StatefulWidget {
final String issueType;
final ValueChanged<String> onIssueTypeChanged;
final bool gettingLocation;
final Future<String> Function() onGetLocation;
final Future<void> Function(String type, String desc, String loc) onSubmitReport;
final String Function(String key) t;
const ReportCardWidget({
super.key,
required this.issueType,
required this.onIssueTypeChanged,
required this.gettingLocation,
required this.onGetLocation,
required this.onSubmitReport,
required this.t,
});
@override
State<ReportCardWidget> createState() => _ReportCardWidgetState();
}
class _ReportCardWidgetState extends State<ReportCardWidget> {
final TextEditingController _descCtl = TextEditingController();
final TextEditingController _locCtl = TextEditingController();
@override
void dispose() {
_descCtl.dispose();
_locCtl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: const Color(0xFF0B2B57).withAlpha((0.45 * 255).round()), // blue-ish
borderRadius: BorderRadius.circular(10),
),
padding: const EdgeInsets.all(16),
child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: <Widget>[
Text(widget.t('reportHeading'), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 10),
DropdownButton<String>(
value: widget.issueType,
dropdownColor: const Color(0xFF0B2B57).withAlpha((0.9 * 255).round()), // Match card background
items: const <DropdownMenuItem<String>>[
DropdownMenuItem<String>(value: 'leak', child: Text('Leak')),
DropdownMenuItem<String>(value: 'overflow', child: Text('Overflow')),
DropdownMenuItem<String>(value: 'shortage', child: Text('Shortage')),
],
onChanged: (String? v) {
if (v != null) widget.onIssueTypeChanged(v);
},
),
TextField(controller: _descCtl, decoration: InputDecoration(labelText: widget.t('descLabel'))),
TextField(controller: _locCtl, decoration: InputDecoration(labelText: widget.t('locLabel'))),
const SizedBox(height: 8),
Row(children: <Widget>[
Expanded(
child: ElevatedButton(
onPressed: widget.gettingLocation
? null
: () async {
final String loc = await widget.onGetLocation();
_locCtl.text = loc;
},
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF4EA5FF)),
child: Text(widget.t('getLocBtn')),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton(
onPressed: () async {
await widget.onSubmitReport(
widget.issueType,
_descCtl.text.trim(),
_locCtl.text.trim(),
);
_descCtl.clear();
_locCtl.clear();
},
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF4EA5FF)),
child: Text(widget.t('submitBtn')),
),
),
]),
]),
);
}
}
class RecentReportsWidget extends StatelessWidget {
final List<Report> reports;
final List<WorkItem> workItems;
final Future<void> Function(Report report) onDeleteReport;
final String Function(String key) t;
const RecentReportsWidget({
super.key,
required this.reports,
required this.workItems,
required this.onDeleteReport,
required this.t,
});
@override
Widget build(BuildContext context) {
return Container(
decoration:
BoxDecoration(color: Colors.white.withAlpha((0.05 * 255).round()), borderRadius: BorderRadius.circular(10)),
padding: const EdgeInsets.all(16),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[
Text(t('recentHeading'), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 10),
reports.isEmpty
? Text(t('noReports'), style: const TextStyle(color: Color(0xFF9AA4B2)))
: Column(
children: reports.reversed.map<Widget>((Report r) {
final WorkItem wi = workItems.firstWhere(
(WorkItem w) => w.reportTs == r.ts,
orElse: () => WorkItem(id: 'W-${r.ts}', reportTs: r.ts),
);
final String tag = wi.assignedTo == null ? t('unassigned') : '${t('assignedTo')}: ${wi.assignedTo!}';
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(color: Colors.white10, borderRadius: BorderRadius.circular(8)),
child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[
Expanded(
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[
Text(r.type, style: const TextStyle(fontWeight: FontWeight.bold)),
if (r.desc.isNotEmpty) Text(r.desc, maxLines: 2, overflow: TextOverflow.ellipsis),
if (r.loc.isNotEmpty) Text(r.loc, style: const TextStyle(color: Colors.grey)),
Text(tag, style: const TextStyle(fontSize: 12, color: Colors.grey)),
]),
),
IconButton(
onPressed: () => onDeleteReport(r),
icon: const Icon(Icons.delete, color: Colors.red))
]),
);
}).toList(),
),
]),
);
}
}
class IssueSummarySectionWidget extends StatelessWidget {
final Map<String, int> data;
final double maxVal;
final String Function(String key) t;
const IssueSummarySectionWidget({
super.key,
required this.data,
required this.maxVal,
required this.t,
});
@override
Widget build(BuildContext context) {
return Container(
decoration:
BoxDecoration(color: Colors.white.withAlpha((0.05 * 255).round()), borderRadius: BorderRadius.circular(10)),
padding: const EdgeInsets.all(20),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[
Text(t('chartHeading'), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
BarRowWidget(label: 'Leak', value: data['leak'] ?? 0, maxVal: maxVal, color: const Color(0xFF4EA5FF)),
BarRowWidget(label: 'Overflow', value: data['overflow'] ?? 0, maxVal: maxVal, color: const Color(0xFFFF9F43)),
BarRowWidget(label: 'Shortage', value: data['shortage'] ?? 0, maxVal: maxVal, color: const Color(0xFFFF5C5C)),
const SizedBox(height: 10),
Center(child: Text(t('footer'), style: const TextStyle(color: Color(0xFF9AA4B2), fontSize: 13))),
]),
);
}
}
class BarRowWidget extends StatelessWidget {
final String label;
final int value;
final double maxVal;
final Color color;
const BarRowWidget({
super.key,
required this.label,
required this.value,
required this.maxVal,
required this.color,
});
@override
Widget build(BuildContext context) {
final double percent = maxVal > 0 ? (value / maxVal) : 0.0;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(children: <Widget>[
SizedBox(width: 80, child: Text(label)),
Expanded(
child: Stack(children: <Widget>[
Container(
height: 18,
decoration: BoxDecoration(color: Colors.white24, borderRadius: BorderRadius.circular(6))),
FractionallySizedBox(
widthFactor: percent,
child: Container(
height: 18,
decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(6))),
),
]),
),
const SizedBox(width: 10),
Text('$value'),
]),
);
}
}
class AuthorityPanelWidget extends StatelessWidget {
final List<WorkItem> workItems;
final List<Report> reports;
final List<String> plumbers;
final Future<void> Function(WorkItem workItem, String plumberName) onAssignWorkItem;
final String Function(String key) t;
const AuthorityPanelWidget({
super.key,
required this.workItems,
required this.reports,
required this.plumbers,
required this.onAssignWorkItem,
required this.t,
});
@override
Widget build(BuildContext context) {
final List<WorkItem> unassigned =
workItems.where((WorkItem w) => w.assignedTo == null && w.status == 'pending').toList();
final List<WorkItem> assigned = workItems.where((WorkItem w) => w.assignedTo != null).toList();
return Container(
decoration: BoxDecoration(
color: const Color(0xFFFFE0E6).withAlpha((0.12 * 255).round()),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFFFFA6B8).withAlpha((0.4 * 255).round())),
),
padding: const EdgeInsets.all(16),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[
Text('${t('authority')} • ${t('manage')}',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
if (unassigned.isEmpty)
Text('No unassigned items', style: const TextStyle(color: Colors.grey)),
...unassigned.map<Widget>((WorkItem w) {
final Report r = reports.firstWhere((Report e) => e.ts == w.reportTs,
orElse: () => Report(type: 'Unknown', desc: '-', loc: '-', ts: 0));
return _UnassignedWorkItemCard(
workItem: w,
report: r,
plumbers: plumbers,
onAssign: onAssignWorkItem,
t: t,
);
}).toList(),
if (assigned.isNotEmpty) ...<Widget>[
const SizedBox(height: 12),
Text(t('assigned'), style: const TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 6),
...assigned.map<Widget>((WorkItem w) {
final Report r = reports.firstWhere((Report e) => e.ts == w.reportTs,
orElse: () => Report(type: 'Unknown', desc: '-', loc: '-', ts: 0));
return Container(
margin: const EdgeInsets.only(top: 6),
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(color: Colors.white10, borderRadius: BorderRadius.circular(8)),
child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[
Expanded(
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[
Text('${r.type} • ${r.loc}'),
Text('${t('assignedTo')}: ${w.assignedTo!}', style: const TextStyle(color: Colors.grey)),
Text('Status: ${w.status}', style: const TextStyle(fontSize: 12, color: Colors.grey)),
]),
),
]),
);
}).toList(),
]
]),
);
}
}
class _UnassignedWorkItemCard extends StatefulWidget {
final WorkItem workItem;
final Report report;
final List<String> plumbers;
final Future<void> Function(WorkItem workItem, String plumberName) onAssign;
final String Function(String key) t;
const _UnassignedWorkItemCard({
required this.workItem,
required this.report,
required this.plumbers,
required this.onAssign,
required this.t,
});
@override
State<_UnassignedWorkItemCard> createState() => _UnassignedWorkItemCardState();
}
class _UnassignedWorkItemCardState extends State<_UnassignedWorkItemCard> {
String? _selectedPlumber;
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(top: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(color: Colors.white10, borderRadius: BorderRadius.circular(8)),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[
Text('${widget.report.type.toUpperCase()} • ${widget.report.loc}', style: const TextStyle(fontWeight: FontWeight.w600)),
if (widget.report.desc.isNotEmpty) Text(widget.report.desc),
const SizedBox(height: 8),
Row(children: <Widget>[
Expanded(
child: DropdownButton<String>(
isExpanded: true,
hint: Text(widget.t('pickPlumber')),
value: _selectedPlumber,
items: widget.plumbers.map<DropdownMenuItem<String>>((String p) => DropdownMenuItem<String>(value: p, child: Text(p))).toList(),
onChanged: (String? v) {
setState(() => _selectedPlumber = v);
},
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: _selectedPlumber == null
? null
: () async {
await widget.onAssign(widget.workItem, _selectedPlumber!);
},
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFFF7FA3)),
child: Text(widget.t('assign')),
)
])
]),
);
}
}
class PlumberPanelWidget extends StatelessWidget {
final List<WorkItem> workItems;
final List<Report> reports;
final String? plumberName;
final Future<void> Function(WorkItem workItem) onMarkWorkItemCompleted;
final String Function(String key) t;
const PlumberPanelWidget({
super.key,
required this.workItems,
required this.reports,
required this.plumberName,
required this.onMarkWorkItemCompleted,
required this.t,
});
@override
Widget build(BuildContext context) {
final List<WorkItem> myWorkItems =
workItems.where((WorkItem w) => w.assignedTo == plumberName).toList();
return Container(
decoration: BoxDecoration(
color: const Color(0xFFE7E0FF).withAlpha((0.12 * 255).round()),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFFBBA7FF).withAlpha((0.4 * 255).round())),
),
padding: const EdgeInsets.all(16),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[
Text('${t('plumber')} • ${t('tasks')}',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
if (myWorkItems.isEmpty) Text('No tasks', style: const TextStyle(color: Colors.grey)),
...myWorkItems.map<Widget>((WorkItem w) {
final Report r = reports.firstWhere((Report e) => e.ts == w.reportTs,
orElse: () => Report(type: 'Unknown', desc: '-', loc: '-', ts: 0));
final bool done = w.status == 'completed';
return Container(
margin: const EdgeInsets.only(top: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(color: Colors.white10, borderRadius: BorderRadius.circular(8)),
child: Row(children: <Widget>[
Expanded(
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[
Text('${r.type.toUpperCase()} • ${r.loc}', style: const TextStyle(fontWeight: FontWeight.w600)),
if (r.desc.isNotEmpty) Text(r.desc, maxLines: 2, overflow: TextOverflow.ellipsis,),
Text('Status: ${w.status}', style: const TextStyle(fontSize: 12, color: Colors.grey)),
]),
),
const SizedBox(width: 8),
ElevatedButton.icon(
onPressed: done ? null : () => onMarkWorkItemCompleted(w),
icon: const Icon(Icons.check_circle_outline),
label: Text(t('markDone')),
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF9C8CFF)),
),
]),
);
}).toList(),
]),
);
}
}
class ProfileCardWidget extends StatelessWidget {
final String? userName;
final int totalReports;
final int completedWorkItems;
final VoidCallback onAskUserName;
final String Function(String key) t;
const ProfileCardWidget({
super.key,
required this.userName,
required this.totalReports,
required this.completedWorkItems,
required this.onAskUserName,
required this.t,
});
@override
Widget build(BuildContext context) {
return Container(
decoration:
BoxDecoration(color: Colors.white.withAlpha((0.05 * 255).round()), borderRadius: BorderRadius.circular(10)),
padding: const EdgeInsets.all(20),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[
Text(t('profile'), style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
const SizedBox(height: 10),
Row(children: <Widget>[
const CircleAvatar(
radius: 28,
backgroundColor: Color(0xFF4EA5FF),
child: Icon(Icons.person, color: Colors.white),
),
const SizedBox(width: 12),
Column(crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[
Text(userName == null || userName!.isEmpty ? 'Guest User' : userName!,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
Text('$totalReports reports submitted',
style: const TextStyle(color: Color(0xFF9AA4B2))),
Text('Completed: $completedWorkItems', style: const TextStyle(color: Color(0xFF9AA4B2))),
]),
]),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: onAskUserName,
icon: const Icon(Icons.person_outline),
label: Text(t('setName')),
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF4EA5FF)),
),
]),
);
}
}What OS did this error occur on?
No response
What browser(s) did this error occur on?
No response
Do you have any additional information?
No response
Metadata
Metadata
Assignees
Labels
area-executiontype-bugIncorrect behavior (everything from a crash to more subtle misbehavior)Incorrect behavior (everything from a crash to more subtle misbehavior)