Files
velocity-pay-flutter/lib/screens/workspaces/workspace_screen.dart
2026-06-29 15:10:37 +02:00

348 lines
12 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:qpay/screens/workspaces/workspace_model.dart';
import 'package:qpay/screens/workspaces/workspace_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:uuid/uuid.dart';
import '../../auth_state.dart';
/// Full-screen workspace selector displayed immediately after the splash
/// screen. Fetches the user's workspaces from the backend and:
///
/// * If exactly one workspace exists → auto-navigates to the home screen.
/// * If multiple workspaces exist → shows a picker so the user can choose.
/// * On error (e.g. 403 for guests) → falls through to the home screen.
class WorkspaceScreen extends StatefulWidget {
const WorkspaceScreen({super.key});
@override
State<WorkspaceScreen> createState() => _WorkspaceScreenState();
}
class _WorkspaceScreenState extends State<WorkspaceScreen>
with TickerProviderStateMixin {
late AnimationController _fadeController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
AuthState authState = AuthState();
@override
void initState() {
super.initState();
_fadeController = AnimationController(
duration: const Duration(milliseconds: 400),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(parent: _fadeController, curve: Curves.easeOut));
_slideAnimation =
Tween<Offset>(begin: const Offset(0, 0.04), end: Offset.zero).animate(
CurvedAnimation(parent: _fadeController, curve: Curves.easeOutCubic),
);
// Kick off the workspace fetch after the first frame.
WidgetsBinding.instance.addPostFrameCallback((_) {
_checkAuthAndLoad();
});
}
Future<void> _checkAuthAndLoad() async {
final prefs = await SharedPreferences.getInstance();
if (prefs.getString("token") != null) {
_loadWorkspaces();
} else {
// if not logged in then create temp workspace
if (prefs.getString("workspaceId") == null) {
await prefs.setString("workspaceId", const Uuid().v4());
}
_navigateToHome();
}
}
Future<void> _loadWorkspaces() async {
// Ensure a userId exists in prefs.
final prefs = await SharedPreferences.getInstance();
if (prefs.getString("userId") == null) {
await prefs.setString("userId", const Uuid().v4());
}
final userId = prefs.getString("userId")!;
if (!mounted) return;
final provider = context.read<WorkspaceProvider>();
// Start the fade-in animation.
_fadeController.forward();
await provider.fetchWorkspaces(userId: userId);
if (!mounted) return;
_evaluateNavigation(provider);
}
void _evaluateNavigation(WorkspaceProvider provider) {
final workspaces = provider.workspaces;
if (workspaces.length == 1) {
// Store the selected workspace ID so downstream screens can
// reference it if needed.
SharedPreferences.getInstance().then((prefs) {
prefs.setString('workspaceId', workspaces.first.id);
prefs.setString('workspaceInitials', workspaces.first.name[0]);
});
_navigateToHome();
} else if (workspaces.isEmpty) {
// No workspaces or an error occurred go to home.
_navigateToHome();
}
// 2+ workspaces: stay on this screen so the user can pick one.
}
void _onWorkspaceSelected(WorkspaceModel ws) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('workspaceId', ws.id);
await prefs.setString('workspaceInitials', ws.name[0]);
if (!mounted) return;
_navigateToHome();
}
void _navigateToHome() {
context.go('/');
}
@override
void dispose() {
_fadeController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
backgroundColor: const Color(0xFFF8F9FA),
body: Consumer<WorkspaceProvider>(
builder: (context, provider, _) {
if (provider.isLoading) {
return const Center(child: CircularProgressIndicator());
}
// Error or empty: navigate away (handled above, but show
// a brief fallback if navigation hasn't triggered yet).
if (provider.hasError || provider.workspaces.isEmpty) {
return const SizedBox.shrink();
}
// Multiple workspaces → show the picker.
final workspaces = provider.workspaces;
return SafeArea(
child: Center(
child: FadeTransition(
opacity: _fadeAnimation,
child: SlideTransition(
position: _slideAnimation,
child: SizedBox(
width: kIsWeb ? 600 : double.infinity,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 48),
// Header
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: theme.colorScheme.primary.withValues(
alpha: 0.1,
),
borderRadius: BorderRadius.circular(16),
),
child: Icon(
Icons.business_rounded,
color: theme.colorScheme.primary,
size: 28,
),
),
const SizedBox(height: 24),
Text(
'Select a workspace',
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w700,
color: Colors.black87,
),
),
const SizedBox(height: 8),
Text(
'Choose the workspace you want to work in.',
style: theme.textTheme.bodyMedium?.copyWith(
color: Colors.grey.shade600,
),
),
const SizedBox(height: 32),
// Workspace list
Expanded(
child: ListView.separated(
itemCount: workspaces.length,
separatorBuilder: (_, __) =>
const SizedBox(height: 12),
itemBuilder: (context, index) {
final ws = workspaces[index];
return _WorkspaceCard(
name: ws.name,
description: ws.description,
email: ws.email,
phone: ws.phone,
onTap: () => _onWorkspaceSelected(ws),
);
},
),
),
const SizedBox(height: 24),
],
),
),
),
),
),
),
);
},
),
);
}
}
/// A single workspace card shown in the picker list.
class _WorkspaceCard extends StatelessWidget {
final String name;
final String description;
final String email;
final String phone;
final VoidCallback onTap;
const _WorkspaceCard({
required this.name,
required this.description,
required this.email,
required this.phone,
required this.onTap,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
return Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(16),
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: isDark ? const Color(0xFF1E1E1E) : Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isDark
? Colors.white.withValues(alpha: 0.06)
: Colors.grey.shade200,
),
),
child: Row(
children: [
// Avatar placeholder
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: theme.colorScheme.primary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Text(
name.isNotEmpty ? name[0].toUpperCase() : 'W',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w700,
color: theme.colorScheme.primary,
),
),
),
),
const SizedBox(width: 16),
// Details
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (description.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
description,
style: TextStyle(
fontSize: 13,
color: Colors.grey.shade500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
if (email.isNotEmpty) ...[
const SizedBox(height: 4),
Row(
children: [
Icon(
Icons.email_outlined,
size: 14,
color: Colors.grey.shade400,
),
const SizedBox(width: 6),
Expanded(
child: Text(
email,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade400,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
],
],
),
),
const SizedBox(width: 8),
Icon(Icons.chevron_right_rounded, color: Colors.grey.shade400),
],
),
),
),
);
}
}