diff --git a/lib/api_service.dart b/lib/api_service.dart index d9d2962..201bbb1 100644 --- a/lib/api_service.dart +++ b/lib/api_service.dart @@ -14,6 +14,8 @@ class Profile { final String gid, fullName, role; final String? employeeCode, departmentName, designationTitle, reportingManagerName; final bool isManager; + /// Org base currency (ISO 4217); null on older servers ⇒ INR. + final String? currency; Profile.fromJson(Map j) : gid = j['gid'] ?? '', fullName = j['fullName'] ?? '', @@ -22,7 +24,8 @@ class Profile { departmentName = j['departmentName'], designationTitle = j['designationTitle'], reportingManagerName = j['reportingManagerName'], - isManager = j['isManager'] == true; + isManager = j['isManager'] == true, + currency = j['currency'] as String?; } class Today { @@ -81,13 +84,16 @@ class LeaveRequest { class Payslip { final String gid, periodMonth, status; final num gross, totalDeductions, netPay; + /// Org base currency (ISO 4217); null on older servers ⇒ INR. + final String? currency; Payslip.fromJson(Map j) : gid = j['gid'] ?? '', periodMonth = j['periodMonth'] ?? '', status = j['status'] ?? '', gross = j['gross'] ?? 0, totalDeductions = j['totalDeductions'] ?? 0, - netPay = j['netPay'] ?? 0; + netPay = j['netPay'] ?? 0, + currency = j['currency'] as String?; } class TeamMember { @@ -110,6 +116,9 @@ class ApiService { final Settings settings; ApiService(this.settings); + /// Org base currency from the last profile seen (login or /me/profile). + String? currency; + Uri _uri(String p, [Map? q]) => Uri.parse('${Settings.baseUrl}$p').replace(queryParameters: q); @@ -135,12 +144,17 @@ class ApiService { throw ApiException(_msg(body) ?? 'Invalid email or PIN', res.statusCode); } final p = Profile.fromJson(body['profile'] as Map); + currency = p.currency ?? currency; await settings.saveSession(token: body['accessToken'] as String, name: p.fullName, role: p.role); return p; } // ---- me ---- - Future profile() async => Profile.fromJson(await _get('/api/v1/me/profile')); + Future profile() async { + final p = Profile.fromJson(await _get('/api/v1/me/profile')); + currency = p.currency ?? currency; + return p; + } Future today() async => Today.fromJson(await _get('/api/v1/me/attendance/today')); Future checkIn({double? lat, double? lng, String? selfie}) => diff --git a/lib/money.dart b/lib/money.dart new file mode 100644 index 0000000..7affe3e --- /dev/null +++ b/lib/money.dart @@ -0,0 +1,18 @@ +import 'package:intl/intl.dart'; + +/// Formats a payroll amount in the organisation's currency. +/// +/// [currency] is the ISO 4217 code the server sends on payslips / profile +/// (`currency`). Missing or blank ⇒ INR, which keeps the original output: +/// rupee sign, Indian lakh grouping, no decimals (e.g. `₹1,23,457`). +/// Other currencies print the ISO code and the currency's own minor units +/// (e.g. `AED 12,345.50`, `BHD 1,234.500`). +String formatMoney(num v, [String? currency]) { + final code = (currency ?? '').trim().toUpperCase(); + if (code.isEmpty || code == 'INR') { + return '₹${NumberFormat('#,##,###').format(v)}'; + } + // NumberFormat.currency picks the ISO default fraction digits for `name` + // (BHD/KWD/OMR 3, JPY 0, most others 2). + return NumberFormat.currency(locale: 'en', name: code, symbol: '$code ').format(v); +} diff --git a/lib/screens/payslips_screen.dart b/lib/screens/payslips_screen.dart index f5ac025..8644718 100644 --- a/lib/screens/payslips_screen.dart +++ b/lib/screens/payslips_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import '../api_service.dart'; +import '../money.dart'; import '../theme.dart'; class PayslipsScreen extends StatefulWidget { @@ -40,7 +41,8 @@ class _PayslipsScreenState extends State { } } - String _inr(num v) => '₹${NumberFormat('#,##,###').format(v)}'; + /// Payslip currency, else the profile's, else INR (older servers). + String _money(Payslip p, num v) => formatMoney(v, p.currency ?? widget.api.currency); @override Widget build(BuildContext context) { @@ -62,7 +64,7 @@ class _PayslipsScreenState extends State { title: Text(_month(p.periodMonth), style: const TextStyle(fontWeight: FontWeight.w600)), subtitle: const Text('Net pay'), trailing: Column(mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, children: [ - Text(_inr(p.netPay), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: AppColors.emerald)), + Text(_money(p, p.netPay), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: AppColors.emerald)), Text(p.status, style: const TextStyle(fontSize: 11, color: AppColors.muted)), ]), onTap: () => showModalBottomSheet( @@ -72,10 +74,10 @@ class _PayslipsScreenState extends State { child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text(_month(p.periodMonth), style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: AppColors.navy)), const SizedBox(height: 16), - _row('Gross earnings', _inr(p.gross)), - _row('Total deductions', '− ${_inr(p.totalDeductions)}'), + _row('Gross earnings', _money(p, p.gross)), + _row('Total deductions', '− ${_money(p, p.totalDeductions)}'), const Divider(height: 24), - _row('Net pay', _inr(p.netPay), bold: true), + _row('Net pay', _money(p, p.netPay), bold: true), const SizedBox(height: 16), const Text('Full PDF payslip is available on the Scalar360 web portal.', style: TextStyle(fontSize: 12, color: AppColors.muted)), diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100644 index c2cff6a..0000000 --- a/test/widget_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:scalar_attendance/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -}