401 lines
11 KiB
Dart
401 lines
11 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:http/http.dart' as http;
|
|
|
|
import '../config/environment.dart';
|
|
import '../service/token_storage_service.dart';
|
|
|
|
class NhanceTopBar extends StatefulWidget implements PreferredSizeWidget {
|
|
const NhanceTopBar({super.key});
|
|
|
|
@override
|
|
Size get preferredSize => const Size.fromHeight(64);
|
|
|
|
@override
|
|
State<NhanceTopBar> createState() => _NhanceTopBarState();
|
|
}
|
|
|
|
class _NhanceTopBarState extends State<NhanceTopBar> {
|
|
final tokenStorage = TokenStorageService();
|
|
|
|
List<Map<String, dynamic>> branches = [];
|
|
Map<String, dynamic>? selectedBranch;
|
|
|
|
/// client_id → logo URL
|
|
final Map<String, String> _logoByClientId = {};
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadBranches();
|
|
}
|
|
|
|
void _loadBranches() {
|
|
branches = tokenStorage.getCombinedBranches();
|
|
selectedBranch = tokenStorage.getSelectedBranch();
|
|
setState(() {});
|
|
_loadBranchLogos();
|
|
}
|
|
|
|
String _clientKey(Map<String, dynamic> branch) {
|
|
return (branch['client_id'] ?? branch['id'] ?? '').toString();
|
|
}
|
|
|
|
String? _logoFromBranch(Map<String, dynamic> branch) {
|
|
final candidates = [
|
|
branch['client_logo'],
|
|
branch['logo'],
|
|
branch['clientLogo'],
|
|
];
|
|
for (final value in candidates) {
|
|
final url = value?.toString().trim() ?? '';
|
|
if (url.isNotEmpty) return url;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
Map<String, dynamic>? _decodeBranchToken(String token) {
|
|
try {
|
|
final parts = token.split('.');
|
|
if (parts.length != 3) return null;
|
|
|
|
final payload = base64Url.normalize(parts[1]);
|
|
final decoded = utf8.decode(base64Url.decode(payload));
|
|
final parsed = json.decode(decoded);
|
|
if (parsed is Map<String, dynamic>) return parsed;
|
|
if (parsed is Map) return Map<String, dynamic>.from(parsed);
|
|
return null;
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// Read post/pre client + branch ids from selected branch JWT payload.
|
|
Map<String, String>? _clientDetailsParamsFromBranch(
|
|
Map<String, dynamic> branch,
|
|
) {
|
|
final token = branch['token']?.toString() ??
|
|
tokenStorage.getCurrentToken() ??
|
|
'';
|
|
if (token.isEmpty) return null;
|
|
|
|
final decoded = _decodeBranchToken(token);
|
|
if (decoded == null) return null;
|
|
|
|
final postClientId = decoded['post_client_id']?.toString().trim() ?? '';
|
|
final postBranchId = decoded['post_branch_id']?.toString().trim() ?? '';
|
|
final preClientId = decoded['pre_client_id']?.toString().trim() ?? '';
|
|
final preBranchId = decoded['pre_branch_id']?.toString().trim() ?? '';
|
|
|
|
if (postClientId.isEmpty &&
|
|
postBranchId.isEmpty &&
|
|
preClientId.isEmpty &&
|
|
preBranchId.isEmpty) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
'post_client_id': postClientId,
|
|
'post_branch_id': postBranchId,
|
|
'pre_client_id': preClientId,
|
|
'pre_branch_id': preBranchId,
|
|
};
|
|
}
|
|
|
|
Future<void> _loadBranchLogos() async {
|
|
final uniqueBranches = <String, Map<String, dynamic>>{};
|
|
for (final branch in branches) {
|
|
final key = _clientKey(branch);
|
|
if (key.isEmpty) continue;
|
|
uniqueBranches.putIfAbsent(key, () => branch);
|
|
}
|
|
|
|
for (final entry in uniqueBranches.entries) {
|
|
final key = entry.key;
|
|
if (_logoByClientId.containsKey(key)) continue;
|
|
|
|
final fromBranch = _logoFromBranch(entry.value);
|
|
if (fromBranch != null) {
|
|
if (!mounted) return;
|
|
setState(() => _logoByClientId[key] = fromBranch);
|
|
continue;
|
|
}
|
|
|
|
final logoUrl = await _fetchClientLogoUrl(entry.value);
|
|
if (!mounted) return;
|
|
if (logoUrl != null && logoUrl.isNotEmpty) {
|
|
setState(() => _logoByClientId[key] = logoUrl);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<String?> _fetchClientLogoUrl(Map<String, dynamic> branch) async {
|
|
try {
|
|
final token = branch['token']?.toString() ??
|
|
tokenStorage.getCurrentToken() ??
|
|
'';
|
|
if (token.isEmpty) return null;
|
|
|
|
final params = _clientDetailsParamsFromBranch(branch);
|
|
if (params == null) return null;
|
|
|
|
final url = Uri.parse('${Environment.apiUrl}getClientDetails').replace(
|
|
queryParameters: params,
|
|
);
|
|
|
|
final response = await http.get(
|
|
url,
|
|
headers: {
|
|
'Authorization': 'Bearer $token',
|
|
'APP-SIGNATURE':
|
|
'nhance-2025-signature-T8f6gV9k5fK2wf5V5c4vId3b0J8z6cAm2x8Y',
|
|
},
|
|
);
|
|
|
|
if (response.statusCode != 200) return null;
|
|
final data = jsonDecode(response.body);
|
|
if (data is! Map || data['status'] != 'success') return null;
|
|
|
|
final logo = data['data']?['client']?['client_logo']?.toString().trim();
|
|
if (logo == null || logo.isEmpty) return null;
|
|
return logo;
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
Future<String> _getBranchSwitchRoute() async {
|
|
final postRaw = await tokenStorage.readValue('empAllowed_modules');
|
|
final postModules = postRaw != null && postRaw.isNotEmpty
|
|
? List<int>.from(jsonDecode(postRaw))
|
|
: <int>[];
|
|
|
|
if (postModules.isNotEmpty && postModules.contains(5)) {
|
|
return 'claimsOverviewDashboard';
|
|
}
|
|
return 'policies';
|
|
}
|
|
|
|
Future<void> _onBranchSelected(Map<String, dynamic> branch) async {
|
|
final tokenStorage = TokenStorageService();
|
|
|
|
await tokenStorage.resetSessionAndSwitchBranch(branch);
|
|
|
|
if (!mounted) return;
|
|
|
|
_loadBranches();
|
|
|
|
final route = await _getBranchSwitchRoute();
|
|
|
|
if (!mounted) return;
|
|
Navigator.pushReplacementNamed(context, route);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final selectedLogo = selectedBranch == null
|
|
? null
|
|
: _logoByClientId[_clientKey(selectedBranch!)];
|
|
|
|
return AppBar(
|
|
automaticallyImplyLeading: false,
|
|
backgroundColor: const Color(0xFFBFEFEF),
|
|
elevation: 0,
|
|
title: Row(
|
|
children: [
|
|
Image.asset('assets/nhance_client_logo.png', height: 32),
|
|
const Spacer(),
|
|
if (selectedBranch != null)
|
|
_BranchPopup(
|
|
clientName: selectedBranch!['client_name']?.toString() ?? '',
|
|
branchName: selectedBranch!['branch_name']?.toString() ?? '',
|
|
selectedLogoUrl: selectedLogo,
|
|
branches: branches,
|
|
logoByClientId: _logoByClientId,
|
|
clientKeyBuilder: _clientKey,
|
|
onSelected: _onBranchSelected,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _BranchPopup extends StatelessWidget {
|
|
final String clientName;
|
|
final String branchName;
|
|
final String? selectedLogoUrl;
|
|
final List<Map<String, dynamic>> branches;
|
|
final Map<String, String> logoByClientId;
|
|
final String Function(Map<String, dynamic>) clientKeyBuilder;
|
|
final Function(Map<String, dynamic>) onSelected;
|
|
|
|
const _BranchPopup({
|
|
required this.clientName,
|
|
required this.branchName,
|
|
required this.selectedLogoUrl,
|
|
required this.branches,
|
|
required this.logoByClientId,
|
|
required this.clientKeyBuilder,
|
|
required this.onSelected,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return PopupMenuButton<Map<String, dynamic>>(
|
|
tooltip: '',
|
|
offset: const Offset(0, 48),
|
|
onSelected: onSelected,
|
|
itemBuilder: (context) {
|
|
return branches.map((branch) {
|
|
final logoUrl = logoByClientId[clientKeyBuilder(branch)];
|
|
return PopupMenuItem<Map<String, dynamic>>(
|
|
value: branch,
|
|
child: Row(
|
|
children: [
|
|
_BranchLogo(logoUrl: logoUrl, size: 24),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
branch['client_name']?.toString() ?? '',
|
|
style: const TextStyle(fontSize: 13),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
const SizedBox(width: 10),
|
|
const Icon(Icons.location_on, size: 14, color: Colors.grey),
|
|
const SizedBox(width: 4),
|
|
Flexible(
|
|
child: Text(
|
|
branch['branch_name']?.toString() ?? '',
|
|
style: const TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.grey,
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}).toList();
|
|
},
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
_BranchLogo(logoUrl: selectedLogoUrl, size: 32),
|
|
const SizedBox(width: 8),
|
|
Container(
|
|
height: 40,
|
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(22),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.08),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Text(
|
|
clientName,
|
|
style: const TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Row(
|
|
children: [
|
|
const Icon(
|
|
Icons.location_on,
|
|
size: 14,
|
|
color: Colors.grey,
|
|
),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
branchName,
|
|
style: const TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.grey,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(width: 6),
|
|
const Icon(
|
|
Icons.keyboard_arrow_down,
|
|
size: 20,
|
|
color: Colors.orange,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _BranchLogo extends StatelessWidget {
|
|
final String? logoUrl;
|
|
final double size;
|
|
|
|
const _BranchLogo({
|
|
required this.logoUrl,
|
|
required this.size,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final url = logoUrl?.trim() ?? '';
|
|
if (url.isEmpty) {
|
|
return _placeholder();
|
|
}
|
|
|
|
return ClipRRect(
|
|
borderRadius: BorderRadius.circular(4),
|
|
child: Image.network(
|
|
url,
|
|
width: size,
|
|
height: size,
|
|
fit: BoxFit.contain,
|
|
errorBuilder: (_, __, ___) => _placeholder(),
|
|
loadingBuilder: (context, child, progress) {
|
|
if (progress == null) return child;
|
|
return SizedBox(
|
|
width: size,
|
|
height: size,
|
|
child: const Padding(
|
|
padding: EdgeInsets.all(4),
|
|
child: CircularProgressIndicator(strokeWidth: 1.5),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _placeholder() {
|
|
return Container(
|
|
width: size,
|
|
height: size,
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFE8F5F5),
|
|
borderRadius: BorderRadius.circular(4),
|
|
border: Border.all(color: const Color(0xFFBFEFEF)),
|
|
),
|
|
child: Icon(
|
|
Icons.business,
|
|
size: size * 0.6,
|
|
color: const Color(0xFF00999E),
|
|
),
|
|
);
|
|
}
|
|
}
|