orgs first commit

This commit is contained in:
Leon Bösche
2025-12-17 16:20:29 +01:00
parent 5b71c6b9d2
commit 3da1ef99a9
5 changed files with 291 additions and 54 deletions

View File

@@ -18,6 +18,7 @@ class OrganizationBloc extends Bloc<OrganizationEvent, OrganizationState> {
: super(OrganizationInitial()) {
on<LoadOrganizations>(_onLoadOrganizations);
on<SelectOrganization>(_onSelectOrganization);
on<CreateOrganization>(_onCreateOrganization);
}
void _onLoadOrganizations(
@@ -26,12 +27,12 @@ class OrganizationBloc extends Bloc<OrganizationEvent, OrganizationState> {
) async {
emit(OrganizationLoading());
// Simulate loading orgs from API
await Future.delayed(const Duration(seconds: 1));
final orgs = [
const Organization(id: 'org1', name: 'Personal', role: 'admin'),
const Organization(id: 'org2', name: 'Company Inc', role: 'edit'),
const Organization(id: 'org3', name: 'Side Project', role: 'admin'),
];
emit(OrganizationLoaded(organizations: orgs));
emit(OrganizationLoaded(organizations: orgs, selectedOrg: orgs.first));
}
void _onSelectOrganization(
@@ -58,4 +59,25 @@ class OrganizationBloc extends Bloc<OrganizationEvent, OrganizationState> {
permissionBloc.add(LoadPermissions(event.orgId));
}
}
void _onCreateOrganization(
CreateOrganization event,
Emitter<OrganizationState> emit,
) {
final currentState = state;
if (currentState is OrganizationLoaded) {
final newOrg = Organization(
id: 'org${currentState.organizations.length + 1}',
name: event.name,
role: 'admin',
);
final updatedOrgs = [...currentState.organizations, newOrg];
emit(OrganizationLoaded(organizations: updatedOrgs, selectedOrg: newOrg));
// Reset blocs and load permissions for new org
permissionBloc.add(PermissionsReset());
fileBrowserBloc.add(ResetFileBrowser());
uploadBloc.add(ResetUploads());
permissionBloc.add(LoadPermissions(newOrg.id));
}
}
}

View File

@@ -17,3 +17,12 @@ class SelectOrganization extends OrganizationEvent {
@override
List<Object> get props => [orgId];
}
class CreateOrganization extends OrganizationEvent {
final String name;
const CreateOrganization(this.name);
@override
List<Object> get props => [name];
}

View File

@@ -4,15 +4,19 @@ class Organization extends Equatable {
final String id;
final String name;
final String role; // view, edit, admin
final String? shortCode;
final int? color;
const Organization({
required this.id,
required this.name,
required this.role,
this.shortCode,
this.color,
});
@override
List<Object> get props => [id, name, role];
List<Object?> get props => [id, name, role, shortCode, color];
}
abstract class OrganizationState extends Equatable {

View File

@@ -56,13 +56,6 @@ class MainApp extends StatelessWidget {
providers: [
BlocProvider<AuthBloc>(create: (_) => AuthBloc()),
BlocProvider<SessionBloc>(create: (_) => SessionBloc()),
BlocProvider<OrganizationBloc>(
create: (context) => OrganizationBloc(
context.read<PermissionBloc>(),
context.read<FileBrowserBloc>(),
context.read<UploadBloc>(),
),
),
BlocProvider<PermissionBloc>(create: (_) => PermissionBloc()),
BlocProvider<FileBrowserBloc>(
create: (_) => FileBrowserBloc(FileService(MockFileRepository())),
@@ -70,6 +63,14 @@ class MainApp extends StatelessWidget {
BlocProvider<UploadBloc>(
create: (_) => UploadBloc(MockFileRepository()),
),
BlocProvider<OrganizationBloc>(
lazy: true,
create: (context) => OrganizationBloc(
context.read<PermissionBloc>(),
context.read<FileBrowserBloc>(),
context.read<UploadBloc>(),
),
),
],
child: MaterialApp.router(
routerConfig: _router,

View File

@@ -1,8 +1,14 @@
import 'dart:ui';
import 'dart:ui' as ui;
import 'package:b0esche_cloud/blocs/organization/organization_state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../blocs/auth/auth_bloc.dart';
import '../blocs/auth/auth_state.dart';
import '../blocs/organization/organization_bloc.dart';
import '../blocs/organization/organization_event.dart';
import '../blocs/permission/permission_bloc.dart';
import '../blocs/file_browser/file_browser_bloc.dart';
import '../blocs/upload/upload_bloc.dart';
import '../theme/app_theme.dart';
import 'login_form.dart';
import 'file_explorer.dart';
@@ -33,6 +39,217 @@ class _HomePageState extends State<HomePage> with TickerProviderStateMixin {
super.dispose();
}
void _showCreateOrgDialog(BuildContext context) {
final controller = TextEditingController();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Create New Organization'),
content: TextField(
controller: controller,
decoration: const InputDecoration(labelText: 'Organization Name'),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
final name = controller.text.trim();
if (name.isNotEmpty) {
context.read<OrganizationBloc>().add(CreateOrganization(name));
Navigator.of(context).pop();
}
},
child: const Text('Create'),
),
],
),
);
}
Widget _buildOrgRow(BuildContext context) {
return BlocBuilder<OrganizationBloc, OrganizationState>(
builder: (context, state) {
if (state is OrganizationLoaded) {
final orgs = state.organizations;
return Row(
children: [
...orgs.map(
(org) => Row(
children: [
_buildOrgButton(org, org.id == state.selectedOrg?.id, () {
context.read<OrganizationBloc>().add(
SelectOrganization(org.id),
);
}),
const SizedBox(width: 16),
],
),
),
_buildAddButton(() => _showCreateOrgDialog(context)),
],
);
} else {
return const SizedBox.shrink();
}
},
);
}
Widget _buildOrgButton(Organization org, bool selected, VoidCallback onTap) {
final highlightColor = const Color.fromARGB(255, 100, 200, 255);
final defaultColor = AppTheme.secondaryText;
return TextButton(
onPressed: onTap,
child: Text(
org.name,
style: TextStyle(
color: selected ? highlightColor : defaultColor,
fontWeight: selected ? FontWeight.bold : FontWeight.normal,
),
),
);
}
Widget _buildAddButton(VoidCallback onTap) {
final defaultColor = AppTheme.secondaryText;
return TextButton(
onPressed: onTap,
child: Text('+ Add Organization', style: TextStyle(color: defaultColor)),
);
}
Widget _buildDrive(OrganizationState state) {
if (state is OrganizationLoaded && state.selectedOrg != null) {
return FileExplorer(orgId: state.selectedOrg!.id);
} else {
return const FileExplorer(orgId: 'org1');
}
}
Widget _buildPersonalContent(OrganizationState state) {
if (state is OrganizationLoaded && state.selectedOrg != null) {
return FileExplorer(orgId: state.selectedOrg!.id);
} else {
return const FileExplorer(orgId: 'org1');
}
}
Widget _buildOrganizationsContent(OrganizationState state) {
if (state is OrganizationLoading) {
return const Center(child: CircularProgressIndicator());
} else if (state is OrganizationError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Error: ${state.error}'),
ElevatedButton(
onPressed: () =>
context.read<OrganizationBloc>().add(LoadOrganizations()),
child: const Text('Retry'),
),
],
),
);
} else if (state is OrganizationLoaded) {
final orgs = state.organizations;
if (orgs.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('No organizations yet. Create one to get started.'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => _showCreateOrgDialog(context),
child: const Text('+ Add organization'),
),
],
),
);
}
return Column(
children: [
Expanded(
child: ListView.builder(
itemCount: orgs.length,
itemBuilder: (context, index) {
final org = orgs[index];
final isCurrent = state.selectedOrg?.id == org.id;
return ListTile(
title: Text(org.name),
subtitle: isCurrent ? const Text('(current)') : null,
trailing: isCurrent
? null
: TextButton(
onPressed: () {
context.read<OrganizationBloc>().add(
SelectOrganization(org.id),
);
},
child: const Text('Switch'),
),
);
},
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: ElevatedButton(
onPressed: () => _showCreateOrgDialog(context),
child: const Text('+ Add organization'),
),
),
],
);
} else {
return const Center(child: Text('Loading organizations...'));
}
}
Widget _buildNavButton(String label, IconData icon, {bool isAvatar = false}) {
final isSelected = _selectedTab == label;
final highlightColor = const Color.fromARGB(255, 100, 200, 255);
final defaultColor = AppTheme.secondaryText;
return GestureDetector(
onTap: () {
setState(() {
_selectedTab = label;
});
},
child: isAvatar
? CircleAvatar(
backgroundColor: isSelected ? highlightColor : defaultColor,
child: Icon(icon, color: AppTheme.primaryBackground),
)
: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
color: isSelected ? highlightColor : defaultColor,
size: 24,
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
color: isSelected ? highlightColor : defaultColor,
fontSize: 12,
fontWeight: isSelected
? FontWeight.bold
: FontWeight.normal,
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -60,13 +277,37 @@ class _HomePageState extends State<HomePage> with TickerProviderStateMixin {
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
filter: ui.ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Stack(
children: [
Container(
decoration: AppTheme.glassDecoration,
child: isLoggedIn
? const FileExplorer(orgId: 'org1')
? BlocBuilder<
OrganizationBloc,
OrganizationState
>(
builder: (context, orgState) {
if (orgState is OrganizationInitial) {
WidgetsBinding.instance
.addPostFrameCallback((_) {
context
.read<OrganizationBloc>()
.add(LoadOrganizations());
});
}
return Column(
children: [
const SizedBox(height: 8),
_buildOrgRow(context),
const Divider(height: 1),
Expanded(
child: _buildDrive(orgState),
),
],
);
},
)
: const LoginForm(),
),
// Top-left radial glow - primary accent light
@@ -213,7 +454,7 @@ class _HomePageState extends State<HomePage> with TickerProviderStateMixin {
color: AppTheme.primaryText,
decoration: TextDecoration.underline,
decorationColor: AppTheme.primaryText,
fontFeatures: [const FontFeature.slashedZero()],
fontFeatures: const [FontFeature.slashedZero()],
),
),
),
@@ -253,44 +494,4 @@ class _HomePageState extends State<HomePage> with TickerProviderStateMixin {
),
);
}
Widget _buildNavButton(String label, IconData icon, {bool isAvatar = false}) {
final isSelected = _selectedTab == label;
final highlightColor = Color.fromARGB(255, 100, 200, 255);
final defaultColor = AppTheme.secondaryText;
return GestureDetector(
onTap: () {
setState(() {
_selectedTab = label;
});
},
child: isAvatar
? CircleAvatar(
backgroundColor: isSelected ? highlightColor : defaultColor,
child: Icon(icon, color: AppTheme.primaryBackground),
)
: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
color: isSelected ? highlightColor : defaultColor,
size: 24,
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
color: isSelected ? highlightColor : defaultColor,
fontSize: 12,
fontWeight: isSelected
? FontWeight.bold
: FontWeight.normal,
),
),
],
),
);
}
}