feat: start of pending screen

This commit is contained in:
alzalia1 2025-08-14 00:27:39 +02:00
parent 019a21f00e
commit ee9c4c3801
12 changed files with 425 additions and 67 deletions

View file

@ -22,6 +22,17 @@ class BalRepository {
}
}
Future<Result<List<Bal>>> _getBalsNoCache() async {
final result = await _apiClient.getBals();
switch (result) {
case Ok():
_bals = result.value;
return Result.ok(result.value);
case Error():
return result;
}
}
Future<Result<Bal>> balById(int id) async {
if (_bals == null) {
await getBals();
@ -39,13 +50,20 @@ class BalRepository {
}
}
Future<Result<void>> addBal(String name) async {
final result = await _apiClient.addBal(name);
switch (result) {
case Ok():
return result;
case Error():
return result;
}
Future<Result<Bal>> editBal(
int id,
String name,
DateTime start,
DateTime end,
) async {
final result = await _apiClient.editBal(id, name, start, end);
_getBalsNoCache();
return result;
}
Future<Result<void>> addBal(String name, DateTime start, DateTime end) async {
final result = await _apiClient.addBal(name, start, end);
_getBalsNoCache();
return result;
}
}

View file

@ -42,6 +42,38 @@ class ApiClient {
* =================
*/
Future<Result<Bal>> editBal(
int id,
String name,
DateTime start,
DateTime end,
) async {
final client = Client();
try {
final headers = await _getHeaders({"Content-Type": "application/json"});
final body = {
"name": name,
"start_timestamp": (start.millisecondsSinceEpoch / 1000).round(),
"end_timestamp": (end.millisecondsSinceEpoch / 1000).round(),
};
final response = await client.patch(
Uri.parse("https://$apiBasePath/bal/${id.toString()}"),
headers: headers,
body: jsonEncode(body),
);
if (response.statusCode == 200) {
final json = jsonDecode(response.body);
return Result.ok(Bal.fromJSON(json));
} else {
throw Exception("Something went wrong");
}
} catch (e) {
return Result.error(Exception(e));
} finally {
client.close();
}
}
Future<Result<Bal>> getBalById(int id) async {
final client = Client();
try {
@ -54,7 +86,7 @@ class ApiClient {
final json = jsonDecode(response.body);
return Result.ok(Bal.fromJSON(json));
} else if (response.statusCode == 403) {
return Result.error(Exception("You don't own the specified bal"));
throw Exception("You don't own the specified bal");
} else {
return Result.error(
Exception("No bal wirth this id exists the database"),
@ -67,11 +99,15 @@ class ApiClient {
}
}
Future<Result<Bal>> addBal(String name) async {
Future<Result<Bal>> addBal(String name, DateTime start, DateTime end) async {
final client = Client();
try {
final headers = await _getHeaders({"Content-Type": "application/json"});
final body = {"name": name};
final body = {
"name": name,
"start_timestamp": (start.millisecondsSinceEpoch / 1000).round(),
"end_timestamp": (end.millisecondsSinceEpoch / 1000).round(),
};
final response = await client.post(
Uri.parse("https://$apiBasePath/bal"),
headers: headers,
@ -81,10 +117,9 @@ class ApiClient {
final json = jsonDecode(response.body);
return Result.ok(Bal.fromJSON(json));
} else {
return Result.error(Exception("Something went wrong"));
throw Exception("Something went wrong");
}
} catch (e) {
debugPrint("\n\n\n\n${e.toString()}\n\n\n\n");
return Result.error(Exception(e));
} finally {
client.close();
@ -101,12 +136,17 @@ class ApiClient {
);
if (response.statusCode == 200) {
final json = jsonDecode(response.body) as List<dynamic>;
debugPrint("\n\n\n\nRECEIVED $json\n\n\n\n");
debugPrint("\n\n\n\nRECEIVED : $json\n\n\n\n");
debugPrint(
"\n\n\n\nFORMATTED : ${json.map((element) => Bal.fromJSON(element)).toList()}\n\n\n\n",
);
return Result.ok(json.map((element) => Bal.fromJSON(element)).toList());
} else {
return Result.error(Exception("Something wrong happened"));
throw Exception("Something wrong happened");
}
} catch (e) {
debugPrint("ERROR: ${e.toString()}");
return Result.error(Exception(e));
} finally {
client.close();
@ -127,7 +167,7 @@ class ApiClient {
} else if (response.statusCode == 404) {
return Result.ok(null);
} else {
return Result.error(Exception("Something went wrong"));
throw Exception("Something went wrong");
}
} catch (e) {
return Result.error(Exception(e));
@ -154,7 +194,7 @@ class ApiClient {
final json = jsonDecode(response.body);
return Result.ok(Book.fromJSON(json));
} else {
return Result.error(Exception("The book was not found"));
throw Exception("The book was not found");
}
} catch (e) {
return Result.error(Exception("API $e"));
@ -193,9 +233,9 @@ class ApiClient {
final json = jsonDecode(response.body);
return Result.ok(BookInstance.fromJSON(json));
} else if (response.statusCode == 403) {
return Result.error(Exception("You don't own that book instance"));
throw Exception("You don't own that book instance");
} else {
return Result.error(Exception("Something wrong happened"));
throw Exception("Something wrong happened");
}
} catch (e) {
return Result.error(Exception(e));
@ -225,7 +265,7 @@ class ApiClient {
json.map((element) => Owner.fromJSON(element)).toList(),
);
} else {
return Result.error(Exception("Invalid request"));
throw Exception("Invalid request");
}
} on Exception catch (error) {
return Result.error(error);
@ -257,7 +297,7 @@ class ApiClient {
final json = jsonDecode(response.body);
return Result.ok(Owner.fromJSON(json));
} else {
return Result.error(Exception("Invalid request"));
throw Exception("Invalid request");
}
} on Exception catch (error) {
return Result.error(error);

View file

@ -1,10 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:logging/logging.dart';
import 'package:provider/provider.dart';
import 'package:seshat/config/dependencies.dart';
import 'package:seshat/routing/router.dart';
void main() {
void main() async {
Logger.root.level = Level.ALL;
WidgetsFlutterBinding.ensureInitialized();
@ -19,6 +20,12 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp.router(
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: [const Locale("fr")],
routerConfig: router(context.read()),
theme: ThemeData.dark(),
);

View file

@ -16,6 +16,26 @@ class BalViewModel extends ChangeNotifier {
int? id;
Bal? get bal => _bal;
Future<Result<void>> editBal(
int id,
String name,
DateTime start,
DateTime end,
) async {
final result = await _balRepository.editBal(id, name, start, end);
switch (result) {
case Ok():
debugPrint("\n\n\n\nDID EDIT\n\n\n\n");
_bal = result.value;
notifyListeners();
break;
case Error():
debugPrint("\n\n\n\nERROR: ${result.error}");
break;
}
return result;
}
/*
* =================================
* =====[ COMMAND AND LOADING ]=====

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:seshat/domain/models/bal.dart';
import 'package:seshat/ui/bal_page/view_model/bal_view_model.dart';
import 'package:seshat/ui/bal_page/widget/pending/bal_pending_screen.dart';
import 'package:seshat/ui/core/ui/navigation_bar.dart';
import 'package:seshat/ui/core/ui/await_loading.dart';
@ -16,26 +17,29 @@ class BalPage extends StatefulWidget {
class _BalPageState extends State<BalPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: AppNavigationBar(startIndex: 0),
body: ListenableBuilder(
listenable: widget.viewModel,
builder: (context, child) {
return switch (widget.viewModel.isLoaded) {
false => AwaitLoading(),
true => switch (widget.viewModel.bal == null) {
true => Center(
return ListenableBuilder(
listenable: widget.viewModel,
builder: (context, child) {
return switch (widget.viewModel.isLoaded) {
false => Scaffold(
bottomNavigationBar: AppNavigationBar(startIndex: 0),
body: AwaitLoading(),
),
true => switch (widget.viewModel.bal == null) {
true => Scaffold(
bottomNavigationBar: AppNavigationBar(startIndex: 0),
body: Center(
child: Text("La BAL référencée n'est pas accessible"),
),
false => switch (widget.viewModel.bal!.state) {
BalState.pending => Center(child: Text("Pending")),
BalState.ongoing => Center(child: Text("Ongoing")),
BalState.ended => Center(child: Text("Ending")),
},
),
false => switch (widget.viewModel.bal!.state) {
BalState.pending => BalPendingScreen(viewModel: widget.viewModel),
BalState.ongoing => Center(child: Text("Ongoing")),
BalState.ended => Center(child: Text("Ending")),
},
};
},
),
},
};
},
);
}
}

View file

@ -1,11 +1,191 @@
import 'package:flutter/material.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:intl/intl.dart';
import 'package:seshat/domain/models/bal.dart';
import 'package:seshat/ui/bal_page/view_model/bal_view_model.dart';
import 'package:seshat/ui/core/ui/navigation_bar.dart';
class BalPendingScreen extends StatelessWidget {
const BalPendingScreen({super.key});
const BalPendingScreen({super.key, required this.viewModel});
final BalViewModel viewModel;
@override
Widget build(BuildContext context) {
// TODO: implement build
throw UnimplementedError();
return Scaffold(
bottomNavigationBar: AppNavigationBar(startIndex: 0),
appBar: AppBar(
title: Text(viewModel.bal!.name),
actions: [
IconButton(
onPressed: () {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => EditPopup(viewModel: viewModel),
);
},
icon: Icon(Icons.edit),
),
],
),
body: Center(
child: ElevatedButton(
onPressed: () {},
child: Text("Démarrer cette BAL"),
),
),
);
}
}
class EditPopup extends StatefulWidget {
const EditPopup({super.key, required this.viewModel});
final BalViewModel viewModel;
@override
State<EditPopup> createState() => _EditPopup();
}
class _EditPopup extends State<EditPopup> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
String? name;
DateTime? start;
Future<void> _selectStart() async {
final DateTime? pickedDate = await showDatePicker(
context: context,
initialDate: start ?? widget.viewModel.bal!.startTime,
firstDate: DateTime.now(),
lastDate: DateTime(DateTime.now().year + 2),
locale: Locale("fr", "FR"),
);
setState(() {
start = pickedDate;
});
}
DateTime? end;
Future<void> _selectEnd() async {
final DateTime? pickedDate = await showDatePicker(
context: context,
initialDate: end ?? widget.viewModel.bal!.endTime,
firstDate: DateTime.now(),
lastDate: DateTime(DateTime.now().year + 2),
locale: Locale("fr", "FR"),
);
setState(() {
end = pickedDate;
});
}
@override
Widget build(BuildContext context) {
initializeDateFormatting();
var format = DateFormat("dd MMM yyyy", "fr");
return AlertDialog(
title: Text("Créer une BAL"),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
decoration: InputDecoration(
labelText: "Nom de la BAL",
border: OutlineInputBorder(),
),
initialValue: widget.viewModel.bal!.name,
validator: (value) {
if (value == null || value.isEmpty) {
return "Veuillez entrer un nom";
}
return null;
},
onSaved: (newValue) {
name = newValue;
},
),
Row(
children: [
Text("Date de début : "),
TextButton(
onPressed: () {
_selectStart();
},
child: Text(
format.format(start ?? widget.viewModel.bal!.startTime),
locale: Locale("fr"),
),
),
],
),
Row(
children: [
Text("Date de fin : "),
TextButton(
onPressed: () {
_selectEnd();
},
child: Text(
format.format(end ?? widget.viewModel.bal!.endTime),
),
),
],
),
Text("Note: Les dates sont à titre purement indicatif."),
],
),
),
],
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text("Annuler"),
),
TextButton(
onPressed: () async {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
final Bal bal = widget.viewModel.bal!;
final result = await widget.viewModel.editBal(
bal.id,
name ?? bal.name,
start ?? bal.startTime,
end ?? bal.endTime,
);
if (result is Error && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Une erreur est survenue")),
);
}
if (context.mounted) {
Navigator.of(context).pop();
}
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
"Veuillez indiquer une date de début et de fin.",
),
),
);
}
},
child: Text("Valider"),
),
],
);
}
}

View file

@ -23,10 +23,12 @@ class _AwaitLoadingState extends State<AwaitLoading> {
@override
Widget build(BuildContext context) {
t = Timer(Duration(seconds: 8), () {
setState(() {
text =
"Il semblerait qu'il y ait un problème. Vérifiez que vous êtes connecté·e à internet.";
});
if (context.mounted) {
setState(() {
text =
"Il semblerait qu'il y ait un problème. Vérifiez que vous êtes connecté·e à internet.";
});
}
});
return Column(

View file

@ -24,8 +24,12 @@ class HomeViewModel extends ChangeNotifier {
Bal? _currentBal;
Bal? get currentBal => _currentBal;
Future<Result<void>> createBal(String name) async {
final result = await _balRepository.addBal(name);
Future<Result<void>> createBal(
String name,
DateTime start,
DateTime end,
) async {
final result = await _balRepository.addBal(name, start, end);
switch (result) {
case Ok():
final result2 = await _balRepository.getBals();
@ -34,7 +38,6 @@ class HomeViewModel extends ChangeNotifier {
_bals = result2.value..sort((a, b) => a.compareTo(b));
break;
case Error():
debugPrint("\n\n\n\n${result2.error.toString()}\n\n\n\n");
return result2;
}
break;

View file

@ -1,4 +1,6 @@
import 'package:flutter/material.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:intl/intl.dart';
import 'package:seshat/ui/home_page/view_model/home_view_model.dart';
class CreateConfirmationPopup extends StatefulWidget {
@ -14,8 +16,40 @@ class CreateConfirmationPopup extends StatefulWidget {
class _CreateConfirmationPopupState extends State<CreateConfirmationPopup> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
String? name;
DateTime? start;
Future<void> _selectStart() async {
final DateTime? pickedDate = await showDatePicker(
context: context,
initialDate: start ?? DateTime.now(),
firstDate: DateTime.now(),
lastDate: DateTime(DateTime.now().year + 2),
locale: Locale("fr"),
);
setState(() {
start = pickedDate;
});
}
DateTime? end;
Future<void> _selectEnd() async {
final DateTime? pickedDate = await showDatePicker(
context: context,
initialDate: end ?? DateTime.now(),
firstDate: DateTime.now(),
lastDate: DateTime(DateTime.now().year + 2),
locale: Locale("fr"),
);
setState(() {
end = pickedDate;
});
}
@override
Widget build(BuildContext context) {
initializeDateFormatting();
var format = DateFormat("dd MMM yyyy", "fr");
return AlertDialog(
title: Text("Créer une BAL"),
content: Column(
@ -41,6 +75,29 @@ class _CreateConfirmationPopupState extends State<CreateConfirmationPopup> {
name = newValue;
},
),
Row(
children: [
Text("Date de début : "),
TextButton(
onPressed: () {
_selectStart();
},
child: Text(format.format(start ?? DateTime.now())),
),
],
),
Row(
children: [
Text("Date de fin : "),
TextButton(
onPressed: () {
_selectEnd();
},
child: Text(format.format(end ?? DateTime.now())),
),
],
),
Text("Note: Les dates sont à titre purement indicatif."),
],
),
),
@ -55,12 +112,23 @@ class _CreateConfirmationPopupState extends State<CreateConfirmationPopup> {
),
TextButton(
onPressed: () async {
if (_formKey.currentState!.validate()) {
if (_formKey.currentState!.validate() &&
start != null &&
end != null) {
_formKey.currentState!.save();
await widget.viewModel.createBal(name!);
}
if (context.mounted) {
Navigator.of(context).pop();
await widget.viewModel.createBal(name!, start!, end!);
if (context.mounted) {
Navigator.of(context).pop();
}
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
"Veuillez indiquer une date de début et de fin.",
),
),
);
}
},
child: Text("Valider"),

View file

@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:intl/intl.dart';
import 'package:seshat/domain/models/bal.dart';
import 'package:seshat/ui/core/ui/navigation_bar.dart';
import 'package:seshat/ui/home_page/view_model/home_view_model.dart';
@ -18,6 +20,8 @@ class HomePage extends StatefulWidget {
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
initializeDateFormatting();
var format = DateFormat("dd MMM yyyy", "fr");
return Scaffold(
bottomNavigationBar: AppNavigationBar(startIndex: 0),
appBar: AppBar(
@ -55,30 +59,24 @@ class _HomePageState extends State<HomePage> {
title: Text(bal.name),
subtitle: switch (bal.state) {
BalState.pending => Text(
"À venir · Débute le ${bal.startTime.toString()}",
"À venir · Débute le ${format.format(bal.startTime)}",
),
BalState.ongoing => Text("En cours"),
BalState.ended => Text("Terminée"),
},
trailing: switch (bal.state) {
BalState.pending => IconButton(
onPressed: () {
_moveToBal(context, bal.id);
},
icon: Icon(Icons.edit),
),
BalState.ongoing => IconButton(
onPressed: () {
_moveToBal(context, bal.id);
},
icon: Icon(Icons.arrow_forward),
),
BalState.ended => IconButton(
onPressed: () {
_moveToBal(context, bal.id);
},
icon: Icon(Icons.analytics),
),
_ => IconButton(
onPressed: () {
_moveToBal(context, bal.id);
},
icon: Icon(Icons.arrow_forward),
),
},
),
),
@ -113,6 +111,7 @@ class _HomePageState extends State<HomePage> {
onPressed: () {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) {
return CreateConfirmationPopup(
viewModel: widget.viewModel,

View file

@ -94,6 +94,11 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.0.0"
flutter_localizations:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_secure_storage:
dependency: "direct main"
description:
@ -184,6 +189,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.2"
intl:
dependency: "direct main"
description:
name: intl
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev"
source: hosted
version: "0.20.2"
js:
dependency: transitive
description:

View file

@ -31,6 +31,9 @@ dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
@ -44,6 +47,7 @@ dependencies:
nested: ^1.0.0
flutter_secure_storage: ^9.2.4
rxdart: ^0.28.0
intl: ^0.20.2
dev_dependencies:
flutter_test: