diff --git a/car.mileage b/car.mileage deleted file mode 100644 index e69de29..0000000 diff --git a/car.nickname b/car.nickname deleted file mode 100644 index e69de29..0000000 diff --git a/car.plate b/car.plate deleted file mode 100644 index e69de29..0000000 diff --git a/car.vin b/car.vin deleted file mode 100644 index e69de29..0000000 diff --git a/lib/bups/main-20221111.dart b/lib/bups/main-20221111.dart new file mode 100644 index 0000000..c830184 --- /dev/null +++ b/lib/bups/main-20221111.dart @@ -0,0 +1,856 @@ +import 'dart:collection'; +import 'package:dash/utils/dbhelper_sqflite.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +// import 'package:path/path.dart'; +// import 'package:sqflite/sqflite.dart'; +import 'package:dash/models/car.dart'; +import 'package:dash/models/txn.dart'; +// import 'package:flutter/src/widgets/form.dart'; + +void main() async { + runApp( + ChangeNotifierProvider( + create: (context) => GarageModel(), + child: const MaterialApp( + title: 'Dashboard', + home: MyApp(), +/* initialRoute: '/', + routes: { + '/': (context) => const MyApp(), + '/about': (context) => const AboutScreen(), + '/newcar': (context) => const NewCarScreen(), + '/detail': (context) => CarDetailScreen(), + '/edit': (context) => EditCarScreen(), + }, */ + ), + ), + ); + // WidgetsFlutterBinding.ensureInitialized(); // is this necessary? +} + +class GarageModel extends ChangeNotifier { + /// internal state of garage + late List _cars = []; + late List _txns = []; // hold current car txns, repl on new load + final DbHelperSqlite _dbHelper = DbHelperSqlite.instance; + UnmodifiableListView get cars => UnmodifiableListView(_cars); + UnmodifiableListView get txns => UnmodifiableListView(_txns); + + void add(Car car) { + // _cars.add(car); + _dbHelper.insertCar(car); + getCars(); // prob nec to update `id` + notifyListeners(); + } + + void update(Car car, int carIndex) { + _cars[carIndex] = car; // replace list car with new values + _dbHelper.updateCar(car); + print('updated car ${car.toMap()}'); + getCars(); + // print(_cars); + notifyListeners(); + } + + void delete(Car car) { + // ignore: no_leading_underscores_for_local_identifiers + _cars.removeWhere((_car) => _car.id == car.id); + _dbHelper.deleteCar(car); + print('$car with id ${car.id} deleted'); + getCars(); + notifyListeners(); + } + + void clearCarList() { + // clear _cars; doesn't affect database + // useful for clearing cars that didn't make it into the db + _cars.clear(); + notifyListeners(); + } + + void deleteAllCars() { + // delete rows from CarTable + _cars.clear(); + _dbHelper.deleteAll(); + notifyListeners(); + } + + Future getCars() async { + // _cars.clear(); + print('getcars'); + _cars = await _dbHelper.fetchCars(); + print(_cars); + // notifyListeners(); //for some reason this inits db infinitely + } + + void insertTxn(Txn txn) async { + await _dbHelper.insertTxn(txn); + // update car + await getTxns(txn.carid!); + notifyListeners(); + } + + void deleteTxn(Txn txn) { + _dbHelper.deleteTxn(txn); + // get old mileage from car + notifyListeners(); + } + + Future getTxns(int carid) async { + _txns = await _dbHelper.fetchTxns(carid); + print('get txns: $_txns'); + } +} + +class Garage extends StatefulWidget { + const Garage({super.key}); + @override + State createState() => _Garage(); +} + +class _Garage extends State { + late Future _cars; + + @override + void initState() { + _cars = Provider.of(context, listen: false).getCars(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _cars, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center( + child: CircularProgressIndicator(), + ); + } else { + if (snapshot.error != null) { + return const Center( + child: Text('An error occurred'), + ); + } else { + return Expanded( + child: Consumer(builder: (context, garage, child) { + return ListView.separated( + itemCount: garage._cars.length, + itemBuilder: (context, index) => ListTile( + leading: const Icon(Icons.directions_car), + title: Text(garage.cars[index].nickname ?? "nick_ph"), + subtitle: Text(garage.cars[index].vin ?? "vin_ph"), + onTap: () { + Navigator.of(context) + .push(MaterialPageRoute( + builder: (context) => + CarDetailScreen(carIndex: index), + )) + .then((value) => setState(() {})); + }, + ), + separatorBuilder: (BuildContext context, int index) => + const Divider(), + ); + })); + } + } + }, + ); + } +} + +class CurrentCar extends StatefulWidget { + const CurrentCar({super.key, required this.car}); + final Car car; + + @override + State createState() => _CurrentCar(); +} + +class _CurrentCar extends State { + late Future _txns; + late Car car = widget.car; + + @override + void initState() { + _txns = Provider.of(context, listen: false).getTxns(car.id!); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _txns, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center( + child: CircularProgressIndicator(), + ); + } else { + if (snapshot.error != null) { + return const Center( + child: Text('An error occurred'), + ); + } else { + return Expanded(child: Consumer( + builder: (context, garage, child) { + return ListView.separated( + // padding: const EdgeInsets.all(8), + itemCount: garage.txns.length, + itemBuilder: (context, index) => ListTile( + // dense: true, //only affects text, use visualDensity instead + visualDensity: + const VisualDensity(horizontal: 0, vertical: -4), + leading: const Icon(Icons.local_gas_station), + title: Text(garage.txns[index].txntype ?? "type"), + subtitle: Text(garage.txns[index].note ?? "note"), + onTap: (() => VoidCallback)), + separatorBuilder: (BuildContext context, int index) => + const Divider(), + ); + }, + )); + } + } + }); + } +} + +class MyDrawer extends StatelessWidget { + const MyDrawer({super.key}); + + @override + Widget build(BuildContext context) { + return Drawer( + child: ListView( + padding: EdgeInsets.zero, + children: [ + const SizedBox( + height: 80.0, + child: DrawerHeader( + decoration: BoxDecoration( + color: Color.fromARGB(255, 185, 47, 5), + ), + child: Text( + 'Options', + style: TextStyle( + color: Colors.white, + fontSize: 18, + ), + ), + ), + ), + ListTile( + leading: const Icon(Icons.settings), + title: const Text('Settings'), + onTap: () { + print('settings'); + }, + // do something + ), + ListTile( + leading: const Icon(Icons.sync), + title: const Text('Setup Database'), + onTap: () { + print('dbsetup'); + }, + ), + ListTile( + leading: const Icon(Icons.info_outline), + title: const Text('About'), + onTap: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => const AboutScreen())); + }, + ), + ], + ), + ); + } +} + +class MyApp extends StatefulWidget { + const MyApp({super.key}); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + @override + Widget build(BuildContext context) { + var garage = context.watch(); // testing + return Scaffold( + appBar: AppBar( + backgroundColor: const Color.fromARGB(255, 185, 47, 5), + title: const Text('Dashboard'), + ), + drawer: const MyDrawer(), + body: Container( + padding: const EdgeInsets.all(32), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: ElevatedButton( + onPressed: (() => garage.clearCarList()), + // onPressed: (() => VoidCallback), + onLongPress: () { + garage.deleteAllCars(); + // VoidCallback; + }, + child: Row( + children: const [ + Icon(Icons.delete), + Text('Remove All Cars'), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: ElevatedButton( + onPressed: (() => garage.getCars()), + // onPressed: (() => VoidCallback), + child: Row( + children: const [ + Icon(Icons.sync), + Text('Refresh garage'), + ], + ), + ), + ), + const Garage(), // list of cars in garage + ], + ), + ), + floatingActionButton: FloatingActionButton( + child: const Icon(Icons.add), + onPressed: () { + Navigator.push(context, + MaterialPageRoute(builder: (context) => const NewCarScreen())); + }, + ), + ); + } +} + +class AboutScreen extends StatelessWidget { + // change to AboutDialog + const AboutScreen( + {super.key}); //https://api.flutter.dev/flutter/material/AboutDialog-class.html + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('About Page'), + backgroundColor: const Color.fromARGB(255, 185, 47, 5), + ), + body: const Center( + child: Text('About Page Text, Lorum Ipsum and whatnot.'), + )); + } +} + +class NewCarScreen extends StatefulWidget { + const NewCarScreen({super.key}); + + @override + State createState() => _NewCarScreenState(); +} + +class _NewCarScreenState extends State { + final GlobalKey _formKey = GlobalKey(); + Car _car = Car(); //initialization is required + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Add New Car'), + backgroundColor: Colors.indigo, + ), + body: Container( + padding: const EdgeInsets.all(32), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + //new car form text fields + TextFormField( + decoration: const InputDecoration( + labelText: 'Nickname', + ), + onSaved: (val) => setState(() => _car.nickname = val), + validator: (String? value) { + if (value == null || value.isEmpty) { + return 'Please enter a nickname'; + } + return null; + }, + ), + TextFormField( + decoration: const InputDecoration( + labelText: 'VIN', + ), + onSaved: (val) => setState(() => _car.vin = val), + validator: (String? value) { + if (value == null || value.isEmpty) { + return 'Please enter a unique VIN'; + } + return null; + }, + ), + TextFormField( + decoration: const InputDecoration( + labelText: 'License Plate', + ), + onSaved: (val) => setState(() => _car.plate = val), + validator: (String? value) { + if (value == null || value.isEmpty) { + return 'Please enter a license plate'; + } + return null; + }, + ), + TextFormField( + decoration: const InputDecoration( + labelText: 'Mileage', + ), + onSaved: (val) => + setState(() => _car.mileage = int.parse(val ?? "")), + // inputFormatters: [FilteringTextInputFormatter.digitsOnly] //try this? + keyboardType: TextInputType.number, + validator: (String? value) { + if (value == null || value.isEmpty) { + return 'Please enter car\'s current mileage'; + } + return null; + }, + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: ElevatedButton( + // onPressed: () => _onSubmit(context), + onPressed: () { + final _form = _formKey.currentState!; + if (_form.validate()) { + //validate form + _form + .save(); //save values (reqd before putting them anywhere) + setState(() { + //modify state of car (necessary?) + _car = Car( + vin: _car.vin, + nickname: _car.nickname, + plate: _car.plate, + mileage: _car.mileage); + }); + var garage = context.read(); + garage.add(_car); + _form.reset(); + Navigator.pushNamed(context, '/'); + } + }, + child: Row( + children: const [ + Icon(Icons.add), + Text('Add Car'), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } + + _onSubmit(context) async { + final _form = _formKey.currentState!; + if (_form.validate()) { + //validate form + _form.save(); //save values (reqd before putting them anywhere) + setState(() { + //modify state of car (necessary?) + _car = Car( + vin: _car.vin, + nickname: _car.nickname, + plate: _car.plate, + mileage: _car.mileage); + }); + var garage = context.read(); //implement Provider + garage.add(_car); + _form.reset(); + Navigator.pushNamed(context, '/'); + } + } +} + +class CarDetailScreen extends StatefulWidget { + CarDetailScreen({super.key, required this.carIndex}); + final int carIndex; + @override + State createState() => _CarDetailScreenState(); +} + +class _CarDetailScreenState extends State { + late Car car = context.watch()._cars[widget.carIndex]; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: const Color.fromARGB(255, 185, 47, 5), + title: Text(car.nickname ?? ""), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + const Text("plh img picker"), + Center( + // edit car screen + child: Ink( + decoration: const ShapeDecoration( + color: Colors.lightBlue, shape: CircleBorder()), + child: IconButton( + icon: const Icon(Icons.edit), + color: Colors.white, + onPressed: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => EditCarScreen( + car: car, carIndex: widget.carIndex)), + ); + }, + ), + ), + ), + ], + ), + IconButton( + iconSize: 48.0, + icon: const Icon(Icons.directions_car), + onPressed: (() => VoidCallback), + ), + Text("Nickname: ${car.nickname}"), + Text("VIN: ${car.vin}"), + Text("License Plate: ${car.plate}"), + Text("Mileage: ${car.mileage.toString()}"), + CurrentCar(car: car), + ], + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => NewTxn(car: car, carIndex: widget.carIndex), + ), + ); + }, + child: const Icon(Icons.add), + ), + ); + } +} + +class EditCarScreen extends StatefulWidget { + EditCarScreen({super.key, required this.car, required this.carIndex}); + Car car; + int carIndex; + + @override + State createState() => _EditCarScreenState(); +} + +class _EditCarScreenState extends State { + final GlobalKey _formKey = GlobalKey(); + late Car car = widget.car; + late int carIndex = widget.carIndex; + + @override + Widget build(BuildContext context) { + var garage = context.read(); + return Scaffold( + appBar: AppBar( + title: Text("Edit ${car.nickname}"), + backgroundColor: const Color.fromARGB(255, 185, 47, 5), + ), + body: Container( + padding: const EdgeInsets.all(16), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextFormField( + initialValue: car.nickname, + decoration: const InputDecoration( + labelText: 'Nickname', + ), + onSaved: (val) => setState(() => car.nickname = val), + validator: (String? value) { + if (value == null || value.isEmpty) { + return 'Please enter a nickname'; + } + return null; + }, + ), + TextFormField( + initialValue: car.vin, + decoration: const InputDecoration( + labelText: 'VIN', + ), + onSaved: (val) => setState(() => car.vin = val), + validator: (String? value) { + if (value == null || value.isEmpty) { + return 'Please enter a unique VIN'; + } + return null; + }, + ), + TextFormField( + initialValue: car.plate, + decoration: const InputDecoration( + labelText: 'License Plate', + ), + onSaved: (val) => setState(() => car.plate = val), + validator: (String? value) { + if (value == null || value.isEmpty) { + return 'Please enter a license plate'; + } + return null; + }, + ), + TextFormField( + initialValue: car.mileage.toString(), + decoration: const InputDecoration( + labelText: 'Mileage', + ), + onSaved: (val) => + setState(() => car.mileage = int.parse(val ?? "")), + // inputFormatters: [FilteringTextInputFormatter.digitsOnly] //try this? + keyboardType: TextInputType.number, + validator: (String? value) { + if (value == null || value.isEmpty) { + return 'Please enter car\'s current mileage'; + } + return null; + }, + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: ElevatedButton( + onPressed: () { + final updateForm = _formKey.currentState!; + if (updateForm.validate()) { + //validate form + updateForm + .save(); //save values (reqd before putting them anywhere) + garage.update(car, carIndex); + updateForm.reset(); + Navigator.pushNamed(context, '/'); + } + }, + child: Row( + children: const [ + //expand these children, too tight + Icon(Icons.save), + Text('Save Changes'), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: ElevatedButton( + onPressed: (() => VoidCallback), + onLongPress: () { + garage.delete(car); + Navigator.pushNamed(context, '/'); + }, + child: Row(children: const [ + Icon(Icons.delete), + Text('Delete'), + ]), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: ElevatedButton( + onPressed: (() => VoidCallback), + child: Row(children: const [ + Icon(Icons.sync_alt), + Text('enable/disable'), + ]), + ), + ), + ], + ), + ), + ), + ); + } +} + +class NewTxn extends StatefulWidget { + NewTxn({super.key, required this.car, required this.carIndex}); + Car car; + int carIndex; + + @override + State createState() => _NewTxnState(); +} + +class _NewTxnState extends State { + final GlobalKey _formKey = GlobalKey(); + late Car car = widget.car; + late int carIndex = widget.carIndex; + DateTime datetime = DateTime.now(); + Txn txn = Txn(); + bool refresh = false; + + Future _selectDate(BuildContext context) async { + final DateTime? timepicked = await showDatePicker( + context: context, + initialDate: datetime, + firstDate: DateTime.utc(1776, 7, 4), + lastDate: DateTime.utc(2222, 2, 22)); + if (timepicked != null && timepicked != datetime) { + setState(() => datetime = timepicked); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text("Add new txn for ${car.nickname}"), + backgroundColor: const Color.fromARGB(255, 185, 47, 5), + ), + body: Container( + padding: const EdgeInsets.all(16), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // txntype - change to dropdown + + TextFormField( + decoration: const InputDecoration( + labelText: 'Type', + ), + onSaved: (val) => setState(() => txn.txntype = val), + ), + Row( + children: [ + Flexible( + flex: 1, + child: IconButton( + onPressed: () => _selectDate(context), + icon: const Icon(Icons.calendar_month)), + ), + Flexible( + flex: 3, + child: InputDatePickerFormField( + firstDate: DateTime.utc(1776, 7, 4), + lastDate: DateTime.utc(2222, 2, 22), + initialDate: datetime, + onDateSaved: (val) => setState( + () => txn.datetime = val.millisecondsSinceEpoch)), + ), + ], + ), + TextFormField( + decoration: const InputDecoration( + labelText: 'Cost', + ), + initialValue: "0", + onSaved: (val) => + setState(() => txn.cost = double.parse(val ?? "0")), + keyboardType: TextInputType.number, + ), + TextFormField( + initialValue: car.mileage.toString(), + decoration: const InputDecoration( + labelText: 'Mileage', + ), + onSaved: (val) => + setState(() => txn.mileage = int.parse(val ?? "")), + keyboardType: TextInputType.number, + validator: (String? value) { + if (value == null || value.isEmpty) { + return "Must be >= current mileage"; + } else if (int.parse(value) < car.mileage!.toInt()) { + return 'Must be >= current mileage'; + } + return null; + }, + ), + TextFormField( + decoration: const InputDecoration( + labelText: 'Note', + ), + onSaved: (val) => setState(() => txn.note = val), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: ElevatedButton( + onPressed: () { + final form = _formKey.currentState!; + if (form.validate()) { + form.save(); + setState(() { + txn.datetime = DateTime.now().millisecondsSinceEpoch; + txn.carid = car.id; + car.mileage = txn.mileage; // update car mileage too + }); + var garage = context.read(); + garage.update(car, carIndex); + print(txn.toMap()); + garage.insertTxn(txn); + form.reset(); + Navigator.pop(context); + } + }, + child: Row( + children: const [ + //expand these children, too tight + Icon(Icons.save), + Text('Save Changes'), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } +} + +/* class IconPicker extends SimpleDialog { + IconPicker({super.key}) + + @override + Widget build (BuildContext context) { + return SimpleDialog( + title: const Text("Pick an icon"), + + ); + } +} */ \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 201ea14..812b2cc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,11 +1,13 @@ import 'dart:collection'; -import 'package:dash/utils/dbhelper_sqflite.dart'; +import 'package:dash/utils/garage_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -// import 'package:path/path.dart'; +import 'package:path/path.dart'; // import 'package:sqflite/sqflite.dart'; +import 'package:dash/utils/dbhelper_sqflite.dart'; import 'package:dash/models/car.dart'; import 'package:dash/models/txn.dart'; +import 'package:dash/screens/screens.dart'; // import 'package:flutter/src/widgets/form.dart'; void main() async { @@ -29,196 +31,6 @@ void main() async { // WidgetsFlutterBinding.ensureInitialized(); // is this necessary? } -class GarageModel extends ChangeNotifier { - /// internal state of garage - late List _cars = []; - late List _txns = []; // hold current car txns, repl on new load - final DbHelperSqlite _dbHelper = DbHelperSqlite.instance; - UnmodifiableListView get cars => UnmodifiableListView(_cars); - UnmodifiableListView get txns => UnmodifiableListView(_txns); - - void add(Car car) { - // _cars.add(car); - _dbHelper.insertCar(car); - getCars(); // prob nec to update `id` - notifyListeners(); - } - - void update(Car car, int carIndex) { - _cars[carIndex] = car; // replace list car with new values - _dbHelper.updateCar(car); - print('updated car ${car.toMap()}'); - getCars(); - // print(_cars); - notifyListeners(); - } - - void delete(Car car) { - // ignore: no_leading_underscores_for_local_identifiers - _cars.removeWhere((_car) => _car.id == car.id); - _dbHelper.deleteCar(car); - print('$car with id ${car.id} deleted'); - getCars(); - notifyListeners(); - } - - void clearCarList() { - // clear _cars; doesn't affect database - // useful for clearing cars that didn't make it into the db - _cars.clear(); - notifyListeners(); - } - - void deleteAllCars() { - // delete rows from CarTable - _cars.clear(); - _dbHelper.deleteAll(); - notifyListeners(); - } - - Future getCars() async { - // _cars.clear(); - print('getcars'); - _cars = await _dbHelper.fetchCars(); - print(_cars); - // notifyListeners(); //for some reason this inits db infinitely - } - - void insertTxn(Txn txn) async { - await _dbHelper.insertTxn(txn); - // update car - await getTxns(txn.carid!); - notifyListeners(); - } - - void deleteTxn(Txn txn) { - _dbHelper.deleteTxn(txn); - // get old mileage from car - notifyListeners(); - } - - Future getTxns(int carid) async { - _txns = await _dbHelper.fetchTxns(carid); - print('get txns: $_txns'); - } -} - -class Garage extends StatefulWidget { - const Garage({super.key}); - @override - State createState() => _Garage(); -} - -class _Garage extends State { - late Future _cars; - - @override - void initState() { - _cars = Provider.of(context, listen: false).getCars(); - super.initState(); - } - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: _cars, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center( - child: CircularProgressIndicator(), - ); - } else { - if (snapshot.error != null) { - return const Center( - child: Text('An error occurred'), - ); - } else { - return Expanded( - child: Consumer(builder: (context, garage, child) { - return ListView.separated( - itemCount: garage._cars.length, - itemBuilder: (context, index) => ListTile( - leading: const Icon(Icons.directions_car), - title: Text(garage.cars[index].nickname ?? "nick_ph"), - subtitle: Text(garage.cars[index].vin ?? "vin_ph"), - onTap: () { - Navigator.of(context) - .push( - MaterialPageRoute( - builder: (context) => - CarDetailScreen(carIndex: index), - )) - .then((value) => setState(() {})); - }, - ), - separatorBuilder: (BuildContext context, int index) => - const Divider(), - ); - })); - } - } - }, - ); - } -} - -class CurrentCar extends StatefulWidget { - CurrentCar({super.key, required this.car}); - Car car; - - @override - State createState() => _CurrentCar(); -} - -class _CurrentCar extends State { - late Future _txns; - late Car car = widget.car; - - @override - void initState() { - _txns = Provider.of(context, listen: false).getTxns(car.id!); - super.initState(); - } - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: _txns, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center( - child: CircularProgressIndicator(), - ); - } else { - if (snapshot.error != null) { - return const Center( - child: Text('An error occurred'), - ); - } else { - return Expanded(child: Consumer( - builder: (context, garage, child) { - return ListView.separated( - // padding: const EdgeInsets.all(8), - itemCount: garage.txns.length, - itemBuilder: (context, index) => ListTile( - // dense: true, //only affects text, use visualDensity instead - visualDensity: - const VisualDensity(horizontal: 0, vertical: -4), - leading: const Icon(Icons.local_gas_station), - title: Text(garage.txns[index].txntype ?? "type"), - subtitle: Text(garage.txns[index].note ?? "note"), - onTap: (() => VoidCallback)), - separatorBuilder: (BuildContext context, int index) => - const Divider(), - ); - }, - )); - } - } - }); - } -} - class MyDrawer extends StatelessWidget { const MyDrawer({super.key}); @@ -293,35 +105,43 @@ class _MyAppState extends State { padding: const EdgeInsets.all(32), child: Column( children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: ElevatedButton( - onPressed: (() => garage.clearCarList()), - // onPressed: (() => VoidCallback), - onLongPress: () { - garage.deleteAllCars(); - // VoidCallback; - }, - child: Row( - children: const [ - Icon(Icons.delete), - Text('Remove All Cars'), - ], + Row( + children: [ + ElevatedButton( + onPressed: (() => garage.clearCarList()), + // onPressed: (() => VoidCallback), + onLongPress: () { + garage.deleteAllCars(); + // VoidCallback; + }, + child: Row( + children: const [ + Icon(Icons.delete), + Text('Del Cars'), + ], + ), ), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: ElevatedButton( - onPressed: (() => garage.getCars()), - // onPressed: (() => VoidCallback), - child: Row( - children: const [ - Icon(Icons.sync), - Text('Refresh garage'), - ], + ElevatedButton( + onPressed: (() => garage.getCars()), + // onPressed: (() => VoidCallback), + child: Row( + children: const [ + Icon(Icons.sync), + Text('garage'), + ], + ), ), - ), + ElevatedButton( + onLongPress: (() => garage.deleteAllTxns()), + onPressed: (() => VoidCallback), + child: Row( + children: const [ + Icon(Icons.delete_forever), + Text('Del Txns'), + ], + ), + ), + ], ), const Garage(), // list of cars in garage ], @@ -337,524 +157,3 @@ class _MyAppState extends State { ); } } - -class AboutScreen extends StatelessWidget { - // change to AboutDialog - const AboutScreen( - {super.key}); //https://api.flutter.dev/flutter/material/AboutDialog-class.html - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('About Page'), - backgroundColor: const Color.fromARGB(255, 185, 47, 5), - ), - body: const Center( - child: Text('About Page Text, Lorum Ipsum and whatnot.'), - )); - } -} - -class NewCarScreen extends StatefulWidget { - const NewCarScreen({super.key}); - - @override - State createState() => _NewCarScreenState(); -} - -class _NewCarScreenState extends State { - final GlobalKey _formKey = GlobalKey(); - Car _car = Car(); //initialization is required - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Add New Car'), - backgroundColor: Colors.indigo, - ), - body: Container( - padding: const EdgeInsets.all(32), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - //new car form text fields - TextFormField( - decoration: const InputDecoration( - labelText: 'Nickname', - ), - onSaved: (val) => setState(() => _car.nickname = val), - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Please enter a nickname'; - } - return null; - }, - ), - TextFormField( - decoration: const InputDecoration( - labelText: 'VIN', - ), - onSaved: (val) => setState(() => _car.vin = val), - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Please enter a unique VIN'; - } - return null; - }, - ), - TextFormField( - decoration: const InputDecoration( - labelText: 'License Plate', - ), - onSaved: (val) => setState(() => _car.plate = val), - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Please enter a license plate'; - } - return null; - }, - ), - TextFormField( - decoration: const InputDecoration( - labelText: 'Mileage', - ), - onSaved: (val) => - setState(() => _car.mileage = int.parse(val ?? "")), - // inputFormatters: [FilteringTextInputFormatter.digitsOnly] //try this? - keyboardType: TextInputType.number, - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Please enter car\'s current mileage'; - } - return null; - }, - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: ElevatedButton( - // onPressed: () => _onSubmit(context), - onPressed: () { - final _form = _formKey.currentState!; - if (_form.validate()) { - //validate form - _form - .save(); //save values (reqd before putting them anywhere) - setState(() { - //modify state of car (necessary?) - _car = Car( - vin: _car.vin, - nickname: _car.nickname, - plate: _car.plate, - mileage: _car.mileage); - }); - var garage = - context.read(); - garage.add(_car); - _form.reset(); - Navigator.pushNamed(context, '/'); - } - }, - child: Row( - children: const [ - Icon(Icons.add), - Text('Add Car'), - ], - ), - ), - ), - ], - ), - ), - ), - ); - } - - _onSubmit(context) async { - final _form = _formKey.currentState!; - if (_form.validate()) { - //validate form - _form.save(); //save values (reqd before putting them anywhere) - setState(() { - //modify state of car (necessary?) - _car = Car( - vin: _car.vin, - nickname: _car.nickname, - plate: _car.plate, - mileage: _car.mileage); - }); - var garage = context.read(); //implement Provider - garage.add(_car); - _form.reset(); - Navigator.pushNamed(context, '/'); - } - } -} - -class CarDetailScreen extends StatefulWidget { - CarDetailScreen({super.key, required this.carIndex}); - final int carIndex; - @override - State createState() => _CarDetailScreenState(); -} - -class _CarDetailScreenState extends State { - late Car car = context.watch()._cars[widget.carIndex]; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - backgroundColor: const Color.fromARGB(255, 185, 47, 5), - title: Text(car.nickname ?? ""), - ), - body: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - const Text("plh img picker"), - Center( - // edit car screen - child: Ink( - decoration: const ShapeDecoration( - color: Colors.lightBlue, shape: CircleBorder()), - child: IconButton( - icon: const Icon(Icons.edit), - color: Colors.white, - onPressed: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => EditCarScreen( - car: car, carIndex: widget.carIndex)), - ); - }, - ), - ), - ), - ], - ), - IconButton( - iconSize: 48.0, - icon: const Icon(Icons.directions_car), - onPressed: (() => VoidCallback), - ), - Text("Nickname: ${car.nickname}"), - Text("VIN: ${car.vin}"), - Text("License Plate: ${car.plate}"), - Text("Mileage: ${car.mileage.toString()}"), - CurrentCar(car: car), - ], - ), - ), - floatingActionButton: FloatingActionButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => NewTxn( - car: car, - carIndex: widget.carIndex), - ), - ); - }, - child: const Icon(Icons.add), - ), - ); - } -} - -class EditCarScreen extends StatefulWidget { - EditCarScreen({super.key, required this.car, required this.carIndex}); - Car car; - int carIndex; - - @override - State createState() => _EditCarScreenState(); -} - -class _EditCarScreenState extends State { - final GlobalKey _formKey = GlobalKey(); - late Car car = widget.car; - late int carIndex = widget.carIndex; - - @override - Widget build(BuildContext context) { - var garage = context.read(); - return Scaffold( - appBar: AppBar( - title: Text("Edit ${car.nickname}"), - backgroundColor: const Color.fromARGB(255, 185, 47, 5), - ), - body: Container( - padding: const EdgeInsets.all(16), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TextFormField( - initialValue: car.nickname, - decoration: const InputDecoration( - labelText: 'Nickname', - ), - onSaved: (val) => setState(() => car.nickname = val), - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Please enter a nickname'; - } - return null; - }, - ), - TextFormField( - initialValue: car.vin, - decoration: const InputDecoration( - labelText: 'VIN', - ), - onSaved: (val) => setState(() => car.vin = val), - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Please enter a unique VIN'; - } - return null; - }, - ), - TextFormField( - initialValue: car.plate, - decoration: const InputDecoration( - labelText: 'License Plate', - ), - onSaved: (val) => setState(() => car.plate = val), - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Please enter a license plate'; - } - return null; - }, - ), - TextFormField( - initialValue: car.mileage.toString(), - decoration: const InputDecoration( - labelText: 'Mileage', - ), - onSaved: (val) => - setState(() => car.mileage = int.parse(val ?? "")), - // inputFormatters: [FilteringTextInputFormatter.digitsOnly] //try this? - keyboardType: TextInputType.number, - validator: (String? value) { - if (value == null || value.isEmpty) { - return 'Please enter car\'s current mileage'; - } - return null; - }, - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: ElevatedButton( - onPressed: () { - final updateForm = _formKey.currentState!; - if (updateForm.validate()) { - //validate form - updateForm - .save(); //save values (reqd before putting them anywhere) - garage.update(car, carIndex); - updateForm.reset(); - Navigator.pushNamed(context, '/'); - } - }, - child: Row( - children: const [ - //expand these children, too tight - Icon(Icons.save), - Text('Save Changes'), - ], - ), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: ElevatedButton( - onPressed: (() => VoidCallback), - onLongPress: () { - garage.delete(car); - Navigator.pushNamed(context, '/'); - }, - child: Row(children: const [ - Icon(Icons.delete), - Text('Delete'), - ]), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: ElevatedButton( - onPressed: (() => VoidCallback), - child: Row(children: const [ - Icon(Icons.sync_alt), - Text('enable/disable'), - ]), - ), - ), - ], - ), - ), - ), - ); - } -} - -class NewTxn extends StatefulWidget { - NewTxn({super.key, required this.car, required this.carIndex}); - Car car; - int carIndex; - - @override - State createState() => _NewTxnState(); -} - -class _NewTxnState extends State { - final GlobalKey _formKey = GlobalKey(); - late Car car = widget.car; - late int carIndex = widget.carIndex; - DateTime datetime = DateTime.now(); - Txn txn = Txn(); - bool refresh = false; - - Future _selectDate(BuildContext context) async { - final DateTime? timepicked = await showDatePicker( - context: context, - initialDate: datetime, - firstDate: DateTime.utc(1776, 7, 4), - lastDate: DateTime.utc(2222, 2, 22)); - if (timepicked != null && timepicked != datetime) { - setState(() => datetime = timepicked); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text("Add new txn for ${car.nickname}"), - backgroundColor: const Color.fromARGB(255, 185, 47, 5), - ), - body: Container( - padding: const EdgeInsets.all(16), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // txntype - change to dropdown - - TextFormField( - decoration: const InputDecoration( - labelText: 'Type', - ), - onSaved: (val) => setState(() => txn.txntype = val), - ), - Row( - children: [ - Flexible( - flex: 1, - child: IconButton( - onPressed: () => _selectDate(context), - icon: const Icon(Icons.calendar_month)), - ), - Flexible( - flex: 3, - child: InputDatePickerFormField( - firstDate: DateTime.utc(1776, 7, 4), - lastDate: DateTime.utc(2222, 2, 22), - initialDate: datetime, - onDateSaved: (val) => setState( - () => txn.datetime = val.millisecondsSinceEpoch)), - ), - ], - ), - TextFormField( - decoration: const InputDecoration( - labelText: 'Cost', - ), - initialValue: "0", - onSaved: (val) => - setState(() => txn.cost = double.parse(val ?? "0")), - keyboardType: TextInputType.number, - ), - TextFormField( - initialValue: car.mileage.toString(), - decoration: const InputDecoration( - labelText: 'Mileage', - ), - onSaved: (val) => - setState(() => txn.mileage = int.parse(val ?? "")), - keyboardType: TextInputType.number, - validator: (String? value) { - if (value == null || value.isEmpty) { - return "Must be >= current mileage"; - } else if (int.parse(value) < car.mileage!.toInt()) { - return 'Must be >= current mileage'; - } - return null; - }, - ), - TextFormField( - decoration: const InputDecoration( - labelText: 'Note', - ), - onSaved: (val) => setState(() => txn.note = val), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: ElevatedButton( - onPressed: () { - final form = _formKey.currentState!; - if (form.validate()) { - form.save(); - setState(() { - txn.datetime = DateTime.now().millisecondsSinceEpoch; - txn.carid = car.id; - car.mileage = txn.mileage; // update car mileage too - }); - var garage = context.read(); - garage.update(car, carIndex); - print(txn.toMap()); - garage.insertTxn(txn); - form.reset(); - Navigator.pop(context); - } - }, - child: Row( - children: const [ - //expand these children, too tight - Icon(Icons.save), - Text('Save Changes'), - ], - ), - ), - ), - ], - ), - ), - ), - ); - } -} - -/* class IconPicker extends SimpleDialog { - IconPicker({super.key}) - - @override - Widget build (BuildContext context) { - return SimpleDialog( - title: const Text("Pick an icon"), - - ); - } -} */ \ No newline at end of file diff --git a/lib/screens/about.dart b/lib/screens/about.dart new file mode 100644 index 0000000..3317af7 --- /dev/null +++ b/lib/screens/about.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +class AboutScreen extends StatelessWidget { + // change to AboutDialog + const AboutScreen( + {super.key}); //https://api.flutter.dev/flutter/material/AboutDialog-class.html + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('About Page'), + backgroundColor: const Color.fromARGB(255, 185, 47, 5), + ), + body: const Center( + child: Text('About Page Text, Lorum Ipsum and whatnot.'), + )); + } +} diff --git a/lib/screens/car_detail.dart b/lib/screens/car_detail.dart new file mode 100644 index 0000000..6d39260 --- /dev/null +++ b/lib/screens/car_detail.dart @@ -0,0 +1,155 @@ +import 'package:flutter/material.dart'; +// import 'package:path/path.dart'; +import 'package:provider/provider.dart'; +import 'package:dash/utils/garage_model.dart'; +// import 'package:dash/main.dart'; +import 'package:dash/models/car.dart'; +import 'package:dash/screens/screens.dart'; + +class CarDetailScreen extends StatefulWidget { + const CarDetailScreen({super.key, required this.carIndex}); + final int carIndex; + + @override + State createState() => _CarDetailScreenState(); +} + +class _CarDetailScreenState extends State { + //investigate + // late Car car = context.watch()._cars[widget.carIndex]; + + @override + Widget build(BuildContext context) { + // late Car car = Provider.of(context).cars[widget.carIndex]; + // late Car car = context.watch()_cars[widget.carIndex]; + return Scaffold( + appBar: AppBar( + backgroundColor: const Color.fromARGB(255, 185, 47, 5), + // title: Text(car.nickname ?? ""), + title: const Text("View Car"), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + const Text("plh img picker"), + Center( + // edit car screen + child: Ink( + decoration: const ShapeDecoration( + color: Colors.lightBlue, shape: CircleBorder()), + child: IconButton( + icon: const Icon(Icons.edit), + color: Colors.white, + onPressed: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + EditCarScreen(carIndex: widget.carIndex)), + ); + }, + ), + ), + ), + ], + ), + IconButton( + iconSize: 48.0, + icon: const Icon(Icons.directions_car), + onPressed: (() => VoidCallback), + ), + Consumer( + builder: (context, garage, child) => Column( + children: [ + Text("Nickname: ${garage.cars[widget.carIndex].nickname}"), + Text("VIN: ${garage.cars[widget.carIndex].vin}"), + Text("License Plate: ${garage.cars[widget.carIndex].plate}"), + Text( + "Mileage: ${garage.cars[widget.carIndex].mileage.toString()}"), + ], + ), + ), + CarTxns(carIndex: widget.carIndex), + ], + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => NewTxnScreen(carIndex: widget.carIndex), + ), + ); + }, + child: const Icon(Icons.add), + ), + ); + } +} + +class CarTxns extends StatefulWidget { + const CarTxns({super.key, required this.carIndex}); + final int carIndex; + + @override + State createState() => _CurrentCar(); +} + +class _CurrentCar extends State { + late Future _txns; + late Car car; + + @override + void initState() { + car = + Provider.of(context, listen: false).cars[widget.carIndex]; + _txns = Provider.of(context, listen: false).getTxns(car.id!); + super.initState(); + } + + @override + Widget build(BuildContext context) { + // car = + // Provider.of(context, listen: false).cars[widget.carIndex]; + // _txns = Provider.of(context, listen: false).getTxns(car.id!); + return FutureBuilder( + future: _txns, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center( + child: CircularProgressIndicator(), + ); + } else { + if (snapshot.error != null) { + return const Center( + child: Text('An error occurred'), + ); + } else { + return Expanded(child: Consumer( + builder: (context, garage, child) { + return ListView.separated( + // padding: const EdgeInsets.all(8), + itemCount: garage.txns.length, + itemBuilder: (context, index) => ListTile( + // dense: true, //only affects text, use visualDensity instead + visualDensity: + const VisualDensity(horizontal: 0, vertical: -4), + leading: const Icon(Icons.local_gas_station), + title: Text(garage.txns[index].txntype ?? "type"), + subtitle: Text(garage.txns[index].note ?? "note"), + onTap: (() => VoidCallback)), + separatorBuilder: (BuildContext context, int index) => + const Divider(), + ); + }, + )); + } + } + }); + } +} diff --git a/lib/screens/car_edit.dart b/lib/screens/car_edit.dart new file mode 100644 index 0000000..3e64eb9 --- /dev/null +++ b/lib/screens/car_edit.dart @@ -0,0 +1,156 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:dash/utils/garage_model.dart'; +import 'package:dash/models/car.dart'; + +class EditCarScreen extends StatefulWidget { + const EditCarScreen({super.key, required this.carIndex}); + final int carIndex; + + @override + State createState() => _EditCarScreenState(); +} + +class _EditCarScreenState extends State { + final GlobalKey _formKey = GlobalKey(); + late GarageModel garage; + late Car car; + late int carIndex = widget.carIndex; + + @override + void initState() { + super.initState(); + // garage = Provider.of(context); + // car = garage.cars[carIndex]; + // car = Provider.of(context).cars[carIndex]; + // may need to add garage here, too + } + + @override + Widget build(BuildContext context) { + // var garage = context.read(); + garage = Provider.of(context); + car = garage.cars[carIndex]; + return Scaffold( + appBar: AppBar( + title: Text("Edit ${car.nickname}"), + backgroundColor: const Color.fromARGB(255, 185, 47, 5), + ), + body: Container( + padding: const EdgeInsets.all(16), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextFormField( + initialValue: car.nickname, + decoration: const InputDecoration( + labelText: 'Nickname', + ), + onSaved: (val) => setState(() => car.nickname = val), + validator: (String? value) { + if (value == null || value.isEmpty) { + return 'Please enter a nickname'; + } + return null; + }, + ), + TextFormField( + initialValue: car.vin, + decoration: const InputDecoration( + labelText: 'VIN', + ), + onSaved: (val) => setState(() => car.vin = val), + validator: (String? value) { + if (value == null || value.isEmpty) { + return 'Please enter a unique VIN'; + } + return null; + }, + ), + TextFormField( + initialValue: car.plate, + decoration: const InputDecoration( + labelText: 'License Plate', + ), + onSaved: (val) => setState(() => car.plate = val), + validator: (String? value) { + if (value == null || value.isEmpty) { + return 'Please enter a license plate'; + } + return null; + }, + ), + TextFormField( + initialValue: car.mileage.toString(), + decoration: const InputDecoration( + labelText: 'Mileage', + ), + onSaved: (val) => + setState(() => car.mileage = int.parse(val ?? "")), + // inputFormatters: [FilteringTextInputFormatter.digitsOnly] //try this? + keyboardType: TextInputType.number, + validator: (String? value) { + if (value == null || value.isEmpty) { + return 'Please enter car\'s current mileage'; + } + return null; + }, + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: ElevatedButton( + onPressed: () { + final updateForm = _formKey.currentState!; + if (updateForm.validate()) { + //validate form + updateForm + .save(); //save values (reqd before putting them anywhere) + garage.update(car, carIndex); + updateForm.reset(); + Navigator.pop(context); + // Navigator.pushNamed(context, '/'); + } + }, + child: Row( + children: const [ + //expand these children, too tight + Icon(Icons.save), + Text('Save Changes'), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: ElevatedButton( + onPressed: (() => VoidCallback), + onLongPress: () { + garage.delete(car); + Navigator.pop(context); + // Navigator.pushNamed(context, '/'); + }, + child: Row(children: const [ + Icon(Icons.delete), + Text('Delete'), + ]), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: ElevatedButton( + onPressed: (() => VoidCallback), + child: Row(children: const [ + Icon(Icons.sync_alt), + Text('enable/disable'), + ]), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/car_new.dart b/lib/screens/car_new.dart new file mode 100644 index 0000000..93a2419 --- /dev/null +++ b/lib/screens/car_new.dart @@ -0,0 +1,166 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:dash/utils/garage_model.dart'; +import 'package:dash/models/car.dart'; +import '../utils/garage_model.dart'; + +class NewCarScreen extends StatefulWidget { + const NewCarScreen({super.key}); + + @override + State createState() => _NewCarScreenState(); +} + +class _NewCarScreenState extends State { + final GlobalKey formKey = GlobalKey(); + Car car = Car(); //initialization is required + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Add New Car'), + backgroundColor: const Color.fromARGB(255, 185, 47, 5), + ), + body: Container( + padding: const EdgeInsets.all(32), + child: Form( + key: formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + //new car form text fields + TextFormField( + decoration: const InputDecoration( + labelText: 'Nickname', + ), + onSaved: (val) => setState(() => car.nickname = val), + validator: (String? value) { + if (value == null || value.isEmpty) { + return 'Please enter a nickname'; + } + return null; + }, + ), + TextFormField( + decoration: const InputDecoration( + labelText: 'VIN', + ), + onSaved: (val) => setState(() => car.vin = val), + validator: (String? value) { + if (value == null || value.isEmpty) { + return 'Please enter a unique VIN'; + } + return null; + }, + ), + TextFormField( + decoration: const InputDecoration( + labelText: 'License Plate', + ), + onSaved: (val) => setState(() => car.plate = val), + validator: (String? value) { + if (value == null || value.isEmpty) { + return 'Please enter a license plate'; + } + return null; + }, + ), + TextFormField( + decoration: const InputDecoration( + labelText: 'Mileage', + ), + onSaved: (val) => + setState(() => car.mileage = int.parse(val ?? "")), + // inputFormatters: [FilteringTextInputFormatter.digitsOnly] //try this? + keyboardType: TextInputType.number, + validator: (String? value) { + if (value == null || value.isEmpty) { + return 'Please enter car\'s current mileage'; + } + return null; + }, + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: ElevatedButton( + // onPressed: () => _onSubmit(context), + onPressed: () { + final form = formKey.currentState!; + if (form.validate()) { + //validate form + form.save(); //save values (reqd before putting them anywhere) + setState(() { + //modify state of car (necessary?) + car = Car( + vin: car.vin, + nickname: car.nickname, + plate: car.plate, + mileage: car.mileage); + }); + // var garage = context.read(); + // garage.add(car); + Provider.of(context, listen: false).add(car); + form.reset(); + // Navigator.pop(context); + Navigator.pushNamed(context, '/'); + } + }, + child: Row( + children: const [ + Icon(Icons.add), + Text('Add Car'), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } + + _onSubmit(context) async { + final form = formKey.currentState!; + if (form.validate()) { + //validate form + form.save(); //save values (reqd before putting them anywhere) + setState(() { + //modify state of car (necessary?) + car = Car( + vin: car.vin, + nickname: car.nickname, + plate: car.plate, + mileage: car.mileage); + }); + var garage = context.read(); //implement Provider + garage.add(car); + form.reset(); + Navigator.pushNamed(context, '/'); + } + } + + void submit(BuildContext context) async { + var garage = context.read(); + + final form = formKey.currentState!; + if (form.validate()) { + //validate form + form.save(); //save values (reqd before putting them anywhere) + setState(() { + //modify state of car (necessary?) + car = Car( + vin: car.vin, + nickname: car.nickname, + plate: car.plate, + mileage: car.mileage); + }); + garage.add(car); + // Provider.of(context).add(car); + form.reset(); + Navigator.pop(context); + // Navigator.pushNamed(context, '/'); + } + } +} diff --git a/lib/screens/screens.dart b/lib/screens/screens.dart new file mode 100644 index 0000000..3a48043 --- /dev/null +++ b/lib/screens/screens.dart @@ -0,0 +1,6 @@ +export 'package:dash/screens/about.dart'; +export 'package:dash/screens/car_new.dart'; +export 'package:dash/screens/car_detail.dart'; +export 'package:dash/screens/car_edit.dart'; +export 'package:dash/screens/txn_new.dart'; +export 'package:dash/screens/txn_type.dart'; diff --git a/lib/screens/txn_new.dart b/lib/screens/txn_new.dart new file mode 100644 index 0000000..b7b51af --- /dev/null +++ b/lib/screens/txn_new.dart @@ -0,0 +1,149 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:dash/utils/garage_model.dart'; +import 'package:dash/models/car.dart'; +import 'package:dash/models/txn.dart'; +import 'package:dash/screens/txn_type.dart'; + +class NewTxnScreen extends StatefulWidget { + const NewTxnScreen({super.key, required this.carIndex}); + final int carIndex; + + @override + State createState() => _NewTxnScreenState(); +} + +class _NewTxnScreenState extends State { + final GlobalKey _formKey = GlobalKey(); + late Car car; + late int carIndex = widget.carIndex; + // late var garage; + DateTime datetime = DateTime.now(); + Txn txn = Txn(); + late TxnType txnType = TxnType(); + + @override + void initState() { + car = Provider.of(context, listen: false).cars[carIndex]; + // garage = Provider.of(context, listen: false); + super.initState(); + } + + Future _selectDate(BuildContext context) async { + final DateTime? timepicked = await showDatePicker( + context: context, + initialDate: datetime, + firstDate: DateTime.utc(1776, 7, 4), + lastDate: DateTime.utc(2222, 2, 22)); + if (timepicked != null && timepicked != datetime) { + setState(() => datetime = timepicked); + } + } + + @override + Widget build(BuildContext context) { + // txnType = TxnType(); + return Scaffold( + appBar: AppBar( + title: Text("Add new txn for ${car.nickname}"), + backgroundColor: const Color.fromARGB(255, 185, 47, 5), + ), + body: Container( + padding: const EdgeInsets.all(16), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + txnType, + Row( + children: [ + Flexible( + flex: 1, + child: IconButton( + onPressed: () => _selectDate(context), + icon: const Icon(Icons.calendar_month)), + ), + Flexible( + flex: 3, + child: InputDatePickerFormField( + firstDate: DateTime.utc(1776, 7, 4), + lastDate: DateTime.utc(2222, 2, 22), + initialDate: datetime, + onDateSaved: (val) => setState( + () => txn.datetime = val.millisecondsSinceEpoch)), + ), + ], + ), + TextFormField( + decoration: const InputDecoration( + labelText: 'Cost', + ), + initialValue: "0", + onSaved: (val) => + setState(() => txn.cost = double.parse(val ?? "0")), + keyboardType: TextInputType.number, + ), + TextFormField( + initialValue: car.mileage.toString(), + decoration: const InputDecoration( + labelText: 'Mileage', + ), + onSaved: (val) => + setState(() => txn.mileage = int.parse(val ?? "")), + keyboardType: TextInputType.number, + validator: (String? value) { + if (value == null || value.isEmpty) { + return "Must be >= current mileage"; + } else if (int.parse(value) < car.mileage!.toInt()) { + return 'Must be >= current mileage'; + } + return null; + }, + ), + TextFormField( + decoration: const InputDecoration( + labelText: 'Note', + ), + onSaved: (val) => setState(() => txn.note = val), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: ElevatedButton( + onPressed: () { + final form = _formKey.currentState!; + if (form.validate()) { + form.save(); + setState(() { + txn.datetime = DateTime.now().millisecondsSinceEpoch; + txn.txntype = txnType.selectedText; + print( + "txntype set to ${txn.txntype} from radio ${txnType.selectedText}"); + txn.carid = car.id; + car.mileage = txn.mileage; // update car mileage too + }); + print(txn.toMap()); + Provider.of(context, listen: false) + .update(car, carIndex); + Provider.of(context, listen: false) + .insertTxn(txn); + form.reset(); + Navigator.pop(context); + } + }, + child: Row( + children: const [ + //expand these children, too tight + Icon(Icons.save), + Text('Save Changes'), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/txn_type.dart b/lib/screens/txn_type.dart new file mode 100644 index 0000000..ce383ef --- /dev/null +++ b/lib/screens/txn_type.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; + +class TxnType extends StatefulWidget { + TxnType({super.key}); + late String selectedText; + + @override + TxnTypeState createState() => TxnTypeState(); +} + +class TxnTypeState extends State { + int selectedIndex = 0; + +// should I do this? or set manually in the declaration + @override + void initState() { + super.initState(); + selectedIndex = 0; + widget.selectedText = "Gas"; + } + +// how to cough up value? + @override + Widget build(BuildContext context) { + return Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ + rIcon(index: 0, text: "Gas", icon: Icons.local_gas_station), + rIcon(index: 1, text: "Oil", icon: Icons.water_drop), + rIcon(index: 2, text: "Other", icon: Icons.build), + // rIcon(index: 3, text: "Renewal", icon: Icons.calendar_view_day), + ]); + } + + Widget rIcon( + {required int index, required String text, required IconData icon}) { + return Container( + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: selectedIndex == index + ? const Color.fromARGB(255, 185, 47, 5) + : null, + ), + child: Ink( + child: InkResponse( + child: Column( + // mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + color: selectedIndex == index ? Colors.white : null, + ), + Text(text, + style: TextStyle( + color: + selectedIndex == index ? Colors.white : null)) + ], + ), + onTap: () => setState(() { + selectedIndex = index; + widget.selectedText = text; + })))); + } +} diff --git a/lib/shared/datepicker.dart b/lib/shared/datepicker.dart deleted file mode 100644 index 3007f96..0000000 --- a/lib/shared/datepicker.dart +++ /dev/null @@ -1 +0,0 @@ -class DateTimePicker extends DateTimePicker {} diff --git a/lib/utils/dbhelper_sqflite.dart b/lib/utils/dbhelper_sqflite.dart index fb9e1ab..09a2a66 100644 --- a/lib/utils/dbhelper_sqflite.dart +++ b/lib/utils/dbhelper_sqflite.dart @@ -88,7 +88,7 @@ class DbHelperSqlite { .delete(Car.tblCars, where: '${Car.colId}=?', whereArgs: [car.id]); } - Future deleteAll() async { + Future deleteAllCars() async { Database db = await instance.database as Database; return await db.rawDelete("DELETE FROM ${Car.tblCars}"); } diff --git a/lib/utils/garage_model.dart b/lib/utils/garage_model.dart new file mode 100644 index 0000000..759de3a --- /dev/null +++ b/lib/utils/garage_model.dart @@ -0,0 +1,144 @@ +// ignore_for_file: file_names + +import 'dart:collection'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:dash/utils/dbhelper_sqflite.dart'; +import 'package:dash/models/car.dart'; +import 'package:dash/models/txn.dart'; +import 'package:dash/screens/screens.dart'; + +class GarageModel extends ChangeNotifier { + /// internal state of garage + late List _cars = []; + late List _txns = []; // hold current car txns, repl on new load + final DbHelperSqlite _dbHelper = DbHelperSqlite.instance; + UnmodifiableListView get cars => UnmodifiableListView(_cars); + UnmodifiableListView get txns => UnmodifiableListView(_txns); + + void add(Car car) { + // _cars.add(car); + _dbHelper.insertCar(car); + getCars(); // prob nec to update `id` + notifyListeners(); + } + + void update(Car car, int carIndex) { + _cars[carIndex] = car; // replace list car with new values + _dbHelper.updateCar(car); + print('updated car ${car.toMap()}'); + getCars(); + // print(_cars); + notifyListeners(); + } + + void delete(Car car) { + // ignore: no_leading_underscores_for_local_identifiers + _cars.removeWhere((_car) => _car.id == car.id); + _dbHelper.deleteCar(car); + print('$car with id ${car.id} deleted'); + getCars(); + notifyListeners(); + } + + void clearCarList() { + // clear _cars; doesn't affect database + // useful for clearing cars that didn't make it into the db + _cars.clear(); + notifyListeners(); + } + + void deleteAllCars() { + // delete rows from CarTable AND TxnTable + _cars.clear(); + _dbHelper.deleteAllCars(); + _dbHelper.deleteAllTxns(); + notifyListeners(); + } + + Future getCars() async { + // _cars.clear(); + print('getcars'); + _cars = await _dbHelper.fetchCars(); + print(_cars); + // notifyListeners(); //for some reason this inits db infinitely + } + + void insertTxn(Txn txn) async { + await _dbHelper.insertTxn(txn); + // update car + await getTxns(txn.carid!); + notifyListeners(); + } + + void deleteTxn(Txn txn) { + _dbHelper.deleteTxn(txn); + // get old mileage from car + notifyListeners(); + } + + void deleteAllTxns() { + // delete rows from TxnTabls + _dbHelper.deleteAllTxns(); + // get old mileage from car + notifyListeners(); + } + + Future getTxns(int carid) async { + _txns = await _dbHelper.fetchTxns(carid); + print('get txns: $_txns'); + } +} + +class Garage extends StatefulWidget { + const Garage({super.key}); + @override + State createState() => _Garage(); +} + +class _Garage extends State { + late Future _cars; + + @override + Widget build(BuildContext context) { + _cars = Provider.of(context, listen: false).getCars(); + return FutureBuilder( + future: _cars, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center( + child: CircularProgressIndicator(), + ); + } else { + if (snapshot.error != null) { + return const Center( + child: Text('An error occurred'), + ); + } else { + return Expanded( + child: Consumer(builder: (context, garage, child) { + return ListView.separated( + itemCount: garage._cars.length, + itemBuilder: (context, index) => ListTile( + leading: const Icon(Icons.directions_car), + title: Text(garage.cars[index].nickname ?? "nick_ph"), + subtitle: Text(garage.cars[index].vin ?? "vin_ph"), + onTap: () { + Navigator.of(context) + .push(MaterialPageRoute( + builder: (context) => + CarDetailScreen(carIndex: index), + )) + .then((value) => setState(() {})); + }, + ), + separatorBuilder: (BuildContext context, int index) => + const Divider(), + ); + })); + } + } + }, + ); + } +}