Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/workflows/check-version-on-pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Check package version on PR to master

on:
pull_request:
branches:
- master
types: [opened, synchronize, reopened]

jobs:
check-version:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Get current app version from pubspec
id: pkg
run: |
version=$(grep '^version:' pubspec.yaml | head -n1 | sed -E 's/^version:[[:space:]]*([0-9]+\.[0-9]+\.[0-9]+).*$/\1/')
echo "version=$version" >> $GITHUB_OUTPUT

- name: Get latest tag
id: tag
run: |
latest_tag=$(git tag --sort=-creatordate | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1)
echo "latest_tag=$latest_tag" >> $GITHUB_OUTPUT
if [ -z "$latest_tag" ]; then
echo "Aucun tag trouvé, la vérification de version est ignorée."
fi

- name: Compare app version with latest tag
run: |
pkg_version=${{ steps.pkg.outputs.version }}
latest_tag=${{ steps.tag.outputs.latest_tag }}
if [ -z "$latest_tag" ]; then
echo "Aucun tag de version trouvé, la vérification est ignorée."
exit 0
fi
tag_version=${latest_tag#v}
if [ "$pkg_version" = "$tag_version" ]; then
echo "❌ La version du pubspec.yaml ($pkg_version) n'a pas été modifiée depuis le dernier tag ($latest_tag). Veuillez incrémenter la version avant de merger."
exit 1
else
echo "✅ La version du pubspec.yaml ($pkg_version) est différente du dernier tag ($latest_tag)."
fi
42 changes: 42 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Create GitHub Release

on:
push:
branches:
- master

permissions:
contents: write

jobs:
release:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Get app version from pubspec
id: vars
run: |
version=$(grep '^version:' pubspec.yaml | head -n1 | sed -E 's/^version:[[:space:]]*([0-9]+\.[0-9]+\.[0-9]+).*$/\1/')
echo "version=$version" >> $GITHUB_OUTPUT

- name: Format repository name
id: repo_name
run: |
repo="${GITHUB_REPOSITORY}"
# On enlève le propriétaire
repo_name="${repo#*/}"
# On remplace les underscores par des espaces
repo_name="${repo_name//_/ }"
echo "name=$repo_name" >> $GITHUB_OUTPUT

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ steps.vars.outputs.version }}
name: "${{ steps.repo_name.outputs.name }} v${{ steps.vars.outputs.version }}"
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
44 changes: 32 additions & 12 deletions lib/pages/home_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -106,32 +106,52 @@ class _HomePageState extends State<HomePage> {
final pageItems = ops.skip((_page - 1) * _pageSize).take(_pageSize).toList();

// --- Monthly totals calculation (same banner as OperationsPage) ---
// Revenue: sliding 1-month window (from same day previous month until today).
// Expenses: keep calendar month (1st -> end of month).
final now = DateTime.now();
final currentYear = now.year;
final currentMonth = now.month;

double revenueCurrent = _operations
.where((o) => o.levyDate.year == currentYear && o.levyDate.month == currentMonth && o.amount > 0)
// compute start date as the same day last month; if that day doesn't exist
// in the previous month, use the last day of the previous month.
DateTime startOfSlidingMonth(DateTime ref) {
final int d = ref.day;
DateTime candidate = DateTime(ref.year, ref.month - 1, d);
// if overflow happened, DateTime will roll forward into the current month
if (candidate.month == ref.month) {
candidate = DateTime(ref.year, ref.month, 0); // last day of previous month
}
return candidate;
}

final DateTime slidingStart = startOfSlidingMonth(now);
double revenueSliding = _operations
.where((o) => o.amount > 0)
.where((o) => (o.levyDate.isAfter(slidingStart) || o.levyDate.isAtSameMomentAs(slidingStart))
&& (o.levyDate.isBefore(now) || o.levyDate.isAtSameMomentAs(now)))
.fold(0.0, (s, o) => s + o.amount);

// expenses stay in current calendar month
double expenseCurrent = _operations
.where((o) => o.levyDate.year == currentYear && o.levyDate.month == currentMonth && o.amount < 0)
.fold(0.0, (s, o) => s + o.amount.abs());

// If sliding window has no revenue, fallback to previous sliding window
int displayRevenueYear = currentYear;
int displayRevenueMonth = currentMonth;
double revenueToShow = revenueCurrent;
if (revenueCurrent == 0) {
if (currentMonth == 1) {
displayRevenueMonth = 12;
displayRevenueYear = currentYear - 1;
} else {
displayRevenueMonth = currentMonth - 1;
displayRevenueYear = currentYear;
}
double revenueToShow = revenueSliding;
if (revenueSliding == 0) {
final DateTime prevEnd = slidingStart;
final DateTime prevStart = startOfSlidingMonth(prevEnd);
revenueToShow = _operations
.where((o) => o.levyDate.year == displayRevenueYear && o.levyDate.month == displayRevenueMonth && o.amount > 0)
.where((o) => o.amount > 0)
.where((o) => (o.levyDate.isAfter(prevStart) || o.levyDate.isAtSameMomentAs(prevStart))
&& (o.levyDate.isBefore(prevEnd) || o.levyDate.isAtSameMomentAs(prevEnd)))
.fold(0.0, (s, o) => s + o.amount);

// display label for the previous period (use end month of that window)
displayRevenueYear = prevEnd.year;
displayRevenueMonth = prevEnd.month;
}

String monthLabel(int y, int m) => DateFormat.yMMMM('fr_FR').format(DateTime(y, m));
Expand Down
40 changes: 26 additions & 14 deletions lib/pages/operations_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -119,34 +119,46 @@ class _OperationsPageState extends State<OperationsPage> {
Widget build(BuildContext context) {
final ops = _filteredSorted;
// --- Monthly totals calculation ---
// Revenue: sliding 1-month window (from same day previous month until today).
// Expenses: keep calendar month (1st -> end of month).
final now = DateTime.now();
final currentYear = now.year;
final currentMonth = now.month;

double revenueCurrent = _operations
.where((o) => o.levyDate.year == currentYear && o.levyDate.month == currentMonth && o.amount > 0)
DateTime startOfSlidingMonth(DateTime ref) {
final int d = ref.day;
DateTime candidate = DateTime(ref.year, ref.month - 1, d);
if (candidate.month == ref.month) {
candidate = DateTime(ref.year, ref.month, 0);
}
return candidate;
}

final DateTime slidingStart = startOfSlidingMonth(now);
double revenueSliding = _operations
.where((o) => o.amount > 0)
.where((o) => (o.levyDate.isAfter(slidingStart) || o.levyDate.isAtSameMomentAs(slidingStart))
&& (o.levyDate.isBefore(now) || o.levyDate.isAtSameMomentAs(now)))
.fold(0.0, (s, o) => s + o.amount);

double expenseCurrent = _operations
.where((o) => o.levyDate.year == currentYear && o.levyDate.month == currentMonth && o.amount < 0)
.fold(0.0, (s, o) => s + o.amount.abs());

// If current month has no revenues, display previous month revenues instead
int displayRevenueYear = currentYear;
int displayRevenueMonth = currentMonth;
double revenueToShow = revenueCurrent;
if (revenueCurrent == 0) {
// previous month
if (currentMonth == 1) {
displayRevenueMonth = 12;
displayRevenueYear = currentYear - 1;
} else {
displayRevenueMonth = currentMonth - 1;
displayRevenueYear = currentYear;
}
double revenueToShow = revenueSliding;
if (revenueSliding == 0) {
final DateTime prevEnd = slidingStart;
final DateTime prevStart = startOfSlidingMonth(prevEnd);
revenueToShow = _operations
.where((o) => o.levyDate.year == displayRevenueYear && o.levyDate.month == displayRevenueMonth && o.amount > 0)
.where((o) => o.amount > 0)
.where((o) => (o.levyDate.isAfter(prevStart) || o.levyDate.isAtSameMomentAs(prevStart))
&& (o.levyDate.isBefore(prevEnd) || o.levyDate.isAtSameMomentAs(prevEnd)))
.fold(0.0, (s, o) => s + o.amount);

displayRevenueYear = prevEnd.year;
displayRevenueMonth = prevEnd.month;
}

String monthLabel(int y, int m) => DateFormat.yMMMM('fr_FR').format(DateTime(y, m));
Expand Down
7 changes: 3 additions & 4 deletions lib/pages/profile_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class _ProfilePageState extends State<ProfilePage> {
int? id;
bool? _isConnected;
bool? _isVerifiedEmail;
DateTime? _lastLogin;
DateTime? _lastLogout;
DateTime? _createdAt;
DateTime? updatedAt;
String? _appVersion;
Expand Down Expand Up @@ -61,7 +61,7 @@ class _ProfilePageState extends State<ProfilePage> {
_nameC.text = _name ?? '';
_isConnected = j['is_connected'];
_isVerifiedEmail = j['is_verified_email'];
_lastLogin = j['last_login'] == null ? null : DateTime.tryParse(j['last_login']?.toString() ?? '');
_lastLogout = j['last_logout_at'] == null ? null : DateTime.tryParse(j['last_logout_at']?.toString() ?? '');
_createdAt = j['created_at'] == null ? null : DateTime.tryParse(j['created_at']?.toString() ?? '');
updatedAt = j['updated_at'] == null ? null : DateTime.tryParse(j['updated_at']?.toString() ?? '');
_error = null;
Expand Down Expand Up @@ -200,7 +200,7 @@ class _ProfilePageState extends State<ProfilePage> {
),

// Account status summary (from API)
if (_isConnected != null || _isVerifiedEmail != null || _lastLogin != null || _createdAt != null)
if (_isConnected != null || _isVerifiedEmail != null || _createdAt != null)
Card(
elevation: 1,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
Expand All @@ -213,7 +213,6 @@ class _ProfilePageState extends State<ProfilePage> {
if (_isConnected != null) const SizedBox(height: 6),
if (_isVerifiedEmail != null) Row(children: [const Icon(Icons.email), const SizedBox(width: 8), Text('Email vérifié : ${_isVerifiedEmail! ? 'Oui' : 'Non'}'),]),
if (_isVerifiedEmail != null) const SizedBox(height: 6),
if (_lastLogin != null) Row(children: [const Icon(Icons.access_time), const SizedBox(width: 8), Text('Dernière connexion : ${_lastLogin != null ? _lastLogin!.toLocal().toString() : ''}'),]),
if (_createdAt != null) Row(children: [const Icon(Icons.calendar_today), const SizedBox(width: 8), Text('Créé le : ${_createdAt != null ? _createdAt!.toLocal().toString().split('.').first : ''}'),]),
]),
),
Expand Down
3 changes: 1 addition & 2 deletions lib/widgets/operation_dialogs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -571,8 +571,7 @@ class _OperationEditDialogState extends State<OperationEditDialog> {

final body = {
'label': _nameC.text,
// store positive amount for subscription
'amount': finalAmount.abs(),
'amount': finalAmount,
'category': _categoryC.text,
'source': _sourceC.text.isEmpty ? null : _sourceC.text,
'destination': _destC.text.isEmpty ? null : _destC.text,
Expand Down
17 changes: 15 additions & 2 deletions lib/widgets/operations_table.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,22 @@ class OperationsTable extends StatelessWidget {
case 'name':
return DataCell(Text(o.label), onTap: () => onRowTap?.call(o));
case 'amount':
return DataCell(Text(o.amount.toStringAsFixed(2)), onTap: () => onRowTap?.call(o));
Color? amountColor = o.amount > 0 ? Colors.green : (o.amount < 0 ? Colors.red : null);
return DataCell(
Text(
o.amount.toStringAsFixed(2),
style: amountColor != null ? TextStyle(color: amountColor) : null,
),
onTap: () => onRowTap?.call(o),
);
case 'validated':
return DataCell(Icon(o.isValidate ? Icons.check_circle : Icons.remove_circle), onTap: () => onRowTap?.call(o));
return DataCell(
Icon(
o.isValidate ? Icons.check_circle : Icons.remove_circle,
color: o.isValidate ? Colors.green : Colors.blue,
),
onTap: () => onRowTap?.call(o),
);
case 'category':
return DataCell(Text(o.category), onTap: () => onRowTap?.call(o));
case 'source':
Expand Down
17 changes: 15 additions & 2 deletions lib/widgets/subscriptions_table.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,22 @@ class SubscriptionsTable extends StatelessWidget {
case 'name':
return DataCell(Text(s.label), onTap: () => onRowTap?.call(s));
case 'amount':
return DataCell(Text(s.amount.toStringAsFixed(2)), onTap: () => onRowTap?.call(s));
Color? amountColor = s.amount > 0 ? Colors.green : (s.amount < 0 ? Colors.red : null);
return DataCell(
Text(
s.amount.toStringAsFixed(2),
style: amountColor != null ? TextStyle(color: amountColor) : null,
),
onTap: () => onRowTap?.call(s),
);
case 'active':
return DataCell(Icon(s.active ? Icons.check_circle : Icons.remove_circle), onTap: () => onRowTap?.call(s));
return DataCell(
Icon(
s.active ? Icons.check_circle : Icons.remove_circle,
color: s.active ? Colors.green : Colors.red,
),
onTap: () => onRowTap?.call(s)
);
case 'category':
return DataCell(Text(s.category), onTap: () => onRowTap?.call(s));
case 'interval':
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 2.0.1+1
version: 2.1.0+1

environment:
sdk: '>=3.2.5 <4.0.0'
Expand Down