Initial commit — DLS lab-app Flutter project
This commit is contained in:
@@ -0,0 +1,742 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../core/providers/auth_provider.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../models/tenant.dart';
|
||||
import 'tenant_team_repository.dart';
|
||||
|
||||
class TenantTeamScreen extends ConsumerStatefulWidget {
|
||||
const TenantTeamScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<TenantTeamScreen> createState() => _TenantTeamScreenState();
|
||||
}
|
||||
|
||||
class _TenantTeamScreenState extends ConsumerState<TenantTeamScreen> {
|
||||
List<TeamMember> _members = [];
|
||||
bool _loading = true;
|
||||
String? _error;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_load();
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
setState(() {
|
||||
_loading = true;
|
||||
_error = null;
|
||||
});
|
||||
try {
|
||||
final tenantId = ref.read(authProvider).activeTenant!.tenant.id;
|
||||
final members = await TenantTeamRepository.instance.listMembers(tenantId);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_members = members;
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_error = e.toString();
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool get _canManage =>
|
||||
ref.read(authProvider).activeTenant?.canManageUsers ?? false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Ekip'),
|
||||
actions: [
|
||||
if (_canManage)
|
||||
TextButton.icon(
|
||||
onPressed: () => _showAddMemberSheet(context),
|
||||
icon: const Icon(Icons.person_add_outlined, size: 18),
|
||||
label: const Text('Üye Ekle'),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: _loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _error != null
|
||||
? _ErrorView(error: _error!, onRetry: _load)
|
||||
: RefreshIndicator(
|
||||
onRefresh: _load,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
_SectionHeader(
|
||||
title: 'Üyeler',
|
||||
count: _members.length,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_MembersList(
|
||||
members: _members,
|
||||
canManage: _canManage,
|
||||
currentUserId:
|
||||
ref.read(authProvider).profile?.id ?? '',
|
||||
onRoleChange: _changeRole,
|
||||
onRemove: _removeMember,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showAddMemberSheet(BuildContext context) async {
|
||||
final tenantId = ref.read(authProvider).activeTenant!.tenant.id;
|
||||
await showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: AppColors.background,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (_) => _AddMemberSheet(
|
||||
onAdd: (firstName, lastName, email, password, role) async {
|
||||
await TenantTeamRepository.instance.addMember(
|
||||
tenantId: tenantId,
|
||||
email: email,
|
||||
password: password,
|
||||
firstName: firstName,
|
||||
lastName: lastName,
|
||||
role: role,
|
||||
);
|
||||
await _load();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _changeRole(TeamMember member, TenantRole newRole) async {
|
||||
try {
|
||||
await TenantTeamRepository.instance.changeMemberRole(
|
||||
member.memberId, newRole);
|
||||
await _load();
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(SnackBar(content: Text('Hata: $e')));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _removeMember(TeamMember member) async {
|
||||
final name = member.user.displayName.isNotEmpty
|
||||
? member.user.displayName
|
||||
: member.user.email;
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(
|
||||
title: const Text('Üyeyi Çıkar'),
|
||||
content: Text('$name adlı üyeyi ekipten çıkarmak istiyor musunuz?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('Vazgeç')),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
style: FilledButton.styleFrom(backgroundColor: AppColors.cancelled),
|
||||
child: const Text('Çıkar'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed != true) return;
|
||||
try {
|
||||
await TenantTeamRepository.instance.removeMember(member.memberId);
|
||||
await _load();
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(SnackBar(content: Text('Hata: $e')));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Section header ─────────────────────────────────────────────────────────
|
||||
|
||||
class _SectionHeader extends StatelessWidget {
|
||||
const _SectionHeader({required this.title, required this.count});
|
||||
final String title;
|
||||
final int count;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.textSecondary,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.inProgressBg,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
'$count',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.inProgress,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Members list ───────────────────────────────────────────────────────────
|
||||
|
||||
class _MembersList extends StatelessWidget {
|
||||
const _MembersList({
|
||||
required this.members,
|
||||
required this.canManage,
|
||||
required this.currentUserId,
|
||||
required this.onRoleChange,
|
||||
required this.onRemove,
|
||||
});
|
||||
|
||||
final List<TeamMember> members;
|
||||
final bool canManage;
|
||||
final String currentUserId;
|
||||
final Future<void> Function(TeamMember, TenantRole) onRoleChange;
|
||||
final Future<void> Function(TeamMember) onRemove;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (members.isEmpty) {
|
||||
return const _EmptyCard(message: 'Henüz üye yok.');
|
||||
}
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.border),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4))
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: members.asMap().entries.map((entry) {
|
||||
final i = entry.key;
|
||||
final m = entry.value;
|
||||
final isLast = i == members.length - 1;
|
||||
return _MemberTile(
|
||||
member: m,
|
||||
isSelf: m.user.id == currentUserId,
|
||||
canManage: canManage && m.role != TenantRole.owner,
|
||||
showDivider: !isLast,
|
||||
onRoleChange: (role) => onRoleChange(m, role),
|
||||
onRemove: () => onRemove(m),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MemberTile extends StatelessWidget {
|
||||
const _MemberTile({
|
||||
required this.member,
|
||||
required this.isSelf,
|
||||
required this.canManage,
|
||||
required this.showDivider,
|
||||
required this.onRoleChange,
|
||||
required this.onRemove,
|
||||
});
|
||||
|
||||
final TeamMember member;
|
||||
final bool isSelf;
|
||||
final bool canManage;
|
||||
final bool showDivider;
|
||||
final void Function(TenantRole) onRoleChange;
|
||||
final VoidCallback onRemove;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final name = member.user.displayName.isNotEmpty
|
||||
? member.user.displayName
|
||||
: member.user.email;
|
||||
final initials = name.trim().isNotEmpty
|
||||
? name.trim()[0].toUpperCase()
|
||||
: '?';
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.inProgressBg,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
initials,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.inProgress,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
name,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
if (isSelf) ...[
|
||||
const SizedBox(width: 6),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.successBg,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: const Text(
|
||||
'Sen',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.success,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
Text(
|
||||
member.user.email,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (canManage) ...[
|
||||
_RoleChip(
|
||||
role: member.role,
|
||||
onChanged: onRoleChange,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
IconButton(
|
||||
onPressed: onRemove,
|
||||
icon: const Icon(Icons.remove_circle_outline,
|
||||
color: AppColors.cancelled, size: 20),
|
||||
tooltip: 'Çıkar',
|
||||
constraints: const BoxConstraints(),
|
||||
padding: const EdgeInsets.all(6),
|
||||
),
|
||||
] else
|
||||
_RoleBadge(role: member.role),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (showDivider)
|
||||
const Divider(height: 1, indent: 68, color: AppColors.border),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RoleChip extends StatelessWidget {
|
||||
const _RoleChip({required this.role, required this.onChanged});
|
||||
final TenantRole role;
|
||||
final void Function(TenantRole) onChanged;
|
||||
|
||||
static const _selectableRoles = [
|
||||
TenantRole.admin,
|
||||
TenantRole.technician,
|
||||
TenantRole.delivery,
|
||||
TenantRole.finance,
|
||||
TenantRole.doctor,
|
||||
TenantRole.member,
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopupMenuButton<TenantRole>(
|
||||
initialValue: role,
|
||||
onSelected: onChanged,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
itemBuilder: (_) => _selectableRoles
|
||||
.map((r) => PopupMenuItem(
|
||||
value: r,
|
||||
child: Text(r.label),
|
||||
))
|
||||
.toList(),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: _roleBg(role),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
role.label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: _roleColor(role),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Icon(Icons.arrow_drop_down, size: 16, color: _roleColor(role)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RoleBadge extends StatelessWidget {
|
||||
const _RoleBadge({required this.role});
|
||||
final TenantRole role;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: _roleBg(role),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
role.label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: _roleColor(role),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Color _roleBg(TenantRole r) => switch (r) {
|
||||
TenantRole.owner => AppColors.inProgressBg,
|
||||
TenantRole.admin => AppColors.inProgressBg,
|
||||
TenantRole.doctor => AppColors.successBg,
|
||||
_ => AppColors.surface,
|
||||
};
|
||||
|
||||
Color _roleColor(TenantRole r) => switch (r) {
|
||||
TenantRole.owner => AppColors.inProgress,
|
||||
TenantRole.admin => AppColors.inProgress,
|
||||
TenantRole.doctor => AppColors.success,
|
||||
_ => AppColors.textSecondary,
|
||||
};
|
||||
|
||||
// ── Add member sheet ────────────────────────────────────────────────────────
|
||||
|
||||
class _AddMemberSheet extends StatefulWidget {
|
||||
const _AddMemberSheet({required this.onAdd});
|
||||
final Future<void> Function(
|
||||
String firstName,
|
||||
String lastName,
|
||||
String email,
|
||||
String password,
|
||||
TenantRole role,
|
||||
) onAdd;
|
||||
|
||||
@override
|
||||
State<_AddMemberSheet> createState() => _AddMemberSheetState();
|
||||
}
|
||||
|
||||
class _AddMemberSheetState extends State<_AddMemberSheet> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _firstNameController = TextEditingController();
|
||||
final _lastNameController = TextEditingController();
|
||||
final _emailController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
TenantRole _selectedRole = TenantRole.member;
|
||||
bool _saving = false;
|
||||
bool _obscurePassword = true;
|
||||
|
||||
static const _selectableRoles = [
|
||||
TenantRole.admin,
|
||||
TenantRole.technician,
|
||||
TenantRole.delivery,
|
||||
TenantRole.finance,
|
||||
TenantRole.doctor,
|
||||
TenantRole.member,
|
||||
];
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_firstNameController.dispose();
|
||||
_lastNameController.dispose();
|
||||
_emailController.dispose();
|
||||
_passwordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
setState(() => _saving = true);
|
||||
try {
|
||||
await widget.onAdd(
|
||||
_firstNameController.text.trim(),
|
||||
_lastNameController.text.trim(),
|
||||
_emailController.text.trim(),
|
||||
_passwordController.text,
|
||||
_selectedRole,
|
||||
);
|
||||
if (mounted) Navigator.pop(context);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
final msg = _friendlyError(e);
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(SnackBar(content: Text(msg)));
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _saving = false);
|
||||
}
|
||||
}
|
||||
|
||||
static String _friendlyError(Object e) {
|
||||
final s = e.toString();
|
||||
if (s.contains('email') && s.contains('unique')) {
|
||||
return 'Bu e-posta adresi zaten kayıtlı.';
|
||||
}
|
||||
final msgMatch = RegExp(r'message: ([^,}]+)').firstMatch(s);
|
||||
if (msgMatch != null) return msgMatch.group(1)!.trim();
|
||||
if (s.length > 120) return 'Sunucu hatası';
|
||||
return s;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bottom = MediaQuery.of(context).viewInsets.bottom;
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(24, 24, 24, 24 + bottom),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Text(
|
||||
'Üye Ekle',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _firstNameController,
|
||||
textCapitalization: TextCapitalization.words,
|
||||
textInputAction: TextInputAction.next,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'İsim',
|
||||
prefixIcon: Icon(Icons.person_outline),
|
||||
),
|
||||
validator: (val) {
|
||||
if (val == null || val.trim().isEmpty) return 'Zorunlu';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _lastNameController,
|
||||
textCapitalization: TextCapitalization.words,
|
||||
textInputAction: TextInputAction.next,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Soyisim',
|
||||
),
|
||||
validator: (val) {
|
||||
if (val == null || val.trim().isEmpty) return 'Zorunlu';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
textInputAction: TextInputAction.next,
|
||||
autocorrect: false,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'E-posta',
|
||||
hintText: 'ornek@email.com',
|
||||
prefixIcon: Icon(Icons.email_outlined),
|
||||
),
|
||||
validator: (val) {
|
||||
if (val == null || val.trim().isEmpty) return 'E-posta zorunludur';
|
||||
if (!val.contains('@')) return 'Geçerli bir e-posta girin';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
obscureText: _obscurePassword,
|
||||
textInputAction: TextInputAction.done,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Şifre',
|
||||
prefixIcon: const Icon(Icons.lock_outline),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(_obscurePassword
|
||||
? Icons.visibility_outlined
|
||||
: Icons.visibility_off_outlined),
|
||||
onPressed: () =>
|
||||
setState(() => _obscurePassword = !_obscurePassword),
|
||||
),
|
||||
),
|
||||
validator: (val) {
|
||||
if (val == null || val.isEmpty) return 'Şifre zorunludur';
|
||||
if (val.length < 8) return 'En az 8 karakter olmalı';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<TenantRole>(
|
||||
value: _selectedRole,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Rol',
|
||||
prefixIcon: Icon(Icons.badge_outlined),
|
||||
),
|
||||
items: _selectableRoles
|
||||
.map((r) => DropdownMenuItem(
|
||||
value: r,
|
||||
child: Text(r.label),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: (v) {
|
||||
if (v != null) setState(() => _selectedRole = v);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton.icon(
|
||||
onPressed: _saving ? null : _submit,
|
||||
icon: _saving
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2, color: Colors.white),
|
||||
)
|
||||
: const Icon(Icons.person_add_outlined, size: 18),
|
||||
label: const Text('Üye Ekle'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
class _EmptyCard extends StatelessWidget {
|
||||
const _EmptyCard({required this.message});
|
||||
final String message;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.border),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
message,
|
||||
style: const TextStyle(color: AppColors.textSecondary),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ErrorView extends StatelessWidget {
|
||||
const _ErrorView({required this.error, required this.onRetry});
|
||||
final String error;
|
||||
final VoidCallback onRetry;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.error_outline, color: AppColors.cancelled, size: 40),
|
||||
const SizedBox(height: 12),
|
||||
Text(error,
|
||||
style: const TextStyle(color: AppColors.textSecondary),
|
||||
textAlign: TextAlign.center),
|
||||
const SizedBox(height: 12),
|
||||
TextButton(onPressed: onRetry, child: const Text('Tekrar Dene')),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user