376 lines
11 KiB
Dart
376 lines
11 KiB
Dart
import "dart:convert";
|
|
|
|
import "package:bus_running_record/provider/collaboration_state.dart";
|
|
import "package:bus_running_record/provider/supabase_state.dart";
|
|
import "package:provider/provider.dart";
|
|
import "package:shadcn_flutter/shadcn_flutter.dart";
|
|
|
|
Future<void> showCreateOrganizationDialog(BuildContext context) async {
|
|
final result = await showDialog<String>(
|
|
context: context,
|
|
builder: (dialogContext) {
|
|
String name = "";
|
|
|
|
return AlertDialog(
|
|
title: const Text("Create Organization"),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text("Name your new organization."),
|
|
const Gap(12),
|
|
ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 400),
|
|
child: TextField(
|
|
autofocus: true,
|
|
placeholder: const Text("Enter organization name"),
|
|
onChanged: (value) {
|
|
name = value;
|
|
},
|
|
onSubmitted: (_) {
|
|
Navigator.of(dialogContext).pop(name);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
actions: [
|
|
Button.text(
|
|
onPressed: () => Navigator.of(dialogContext).pop(),
|
|
child: const Text("Cancel"),
|
|
),
|
|
Button.primary(
|
|
onPressed: () => Navigator.of(dialogContext).pop(name),
|
|
child: const Text("Create"),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
|
|
final name = (result ?? "").trim();
|
|
if (name.isEmpty) {
|
|
return;
|
|
}
|
|
|
|
if (!context.mounted) return;
|
|
await context.read<CollaborationProvider>().createOrganization(name);
|
|
}
|
|
|
|
Future<void> showCreateChannelDialog(
|
|
BuildContext context, {
|
|
required String organizationId,
|
|
}) async {
|
|
final result = await showDialog<Map<String, String>>(
|
|
context: context,
|
|
builder: (dialogContext) => const _CreateChannelDialog(),
|
|
);
|
|
|
|
final channelName = (result?["name"] ?? "").trim();
|
|
final channelDescription = (result?["description"] ?? "").trim();
|
|
final channelType = (result?["type"] ?? "text").trim();
|
|
if (channelName.isEmpty) return;
|
|
|
|
if (!context.mounted) return;
|
|
try {
|
|
await context.read<CollaborationProvider>().createChannel(
|
|
organizationId: organizationId,
|
|
name: channelName,
|
|
description: channelDescription,
|
|
type: channelType,
|
|
);
|
|
} catch (error, stackTrace) {
|
|
debugPrint("[HomePage] createChannel dialog failed: $error");
|
|
debugPrintStack(stackTrace: stackTrace);
|
|
if (!context.mounted) return;
|
|
await showDialog<void>(
|
|
context: context,
|
|
builder: (dialogContext) => AlertDialog(
|
|
title: const Text("Create Channel Failed"),
|
|
content: Text(error.toString()),
|
|
actions: [
|
|
Button.primary(
|
|
onPressed: () => Navigator.of(dialogContext).pop(),
|
|
child: const Text("Close"),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
String extractInviteToken(String input) {
|
|
final trimmed = input.trim();
|
|
if (trimmed.isEmpty) return "";
|
|
|
|
final invitePathMatch = RegExp(r"/invite/([0-9a-fA-F]+)").firstMatch(trimmed);
|
|
if (invitePathMatch != null) {
|
|
return (invitePathMatch.group(1) ?? "").toLowerCase();
|
|
}
|
|
|
|
final hashInviteMatch = RegExp(
|
|
r"#/invite/([0-9a-fA-F]+)",
|
|
).firstMatch(trimmed);
|
|
if (hashInviteMatch != null) {
|
|
return (hashInviteMatch.group(1) ?? "").toLowerCase();
|
|
}
|
|
|
|
return trimmed.toLowerCase();
|
|
}
|
|
|
|
Future<void> showJoinOrganizationDialog(BuildContext context) async {
|
|
final result = await showDialog<String>(
|
|
context: context,
|
|
builder: (dialogContext) {
|
|
String inviteInput = "";
|
|
|
|
return AlertDialog(
|
|
title: const Text("Join Organization"),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text("Paste an invite link or invite token."),
|
|
const Gap(12),
|
|
ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 420),
|
|
child: TextField(
|
|
autofocus: true,
|
|
placeholder: const Text("https://.../#/invite/<token>"),
|
|
onChanged: (value) {
|
|
inviteInput = value;
|
|
},
|
|
onSubmitted: (_) {
|
|
Navigator.of(dialogContext).pop(inviteInput);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
actions: [
|
|
Button.text(
|
|
onPressed: () => Navigator.of(dialogContext).pop(),
|
|
child: const Text("Cancel"),
|
|
),
|
|
Button.primary(
|
|
onPressed: () => Navigator.of(dialogContext).pop(inviteInput),
|
|
child: const Text("Join"),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
|
|
final token = extractInviteToken(result ?? "");
|
|
if (token.isEmpty) return;
|
|
|
|
if (!context.mounted) return;
|
|
try {
|
|
await context.read<CollaborationProvider>().acceptInviteToken(token);
|
|
if (!context.mounted) return;
|
|
await showDialog<void>(
|
|
context: context,
|
|
builder: (dialogContext) => AlertDialog(
|
|
title: const Text("Joined"),
|
|
content: const Text("You have joined the organization."),
|
|
actions: [
|
|
Button.primary(
|
|
onPressed: () => Navigator.of(dialogContext).pop(),
|
|
child: const Text("Close"),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
} catch (error, stackTrace) {
|
|
debugPrint("[HomePage] join organization failed: $error");
|
|
debugPrintStack(stackTrace: stackTrace);
|
|
if (!context.mounted) return;
|
|
await showDialog<void>(
|
|
context: context,
|
|
builder: (dialogContext) => AlertDialog(
|
|
title: const Text("Join Failed"),
|
|
content: Text(error.toString()),
|
|
actions: [
|
|
Button.primary(
|
|
onPressed: () => Navigator.of(dialogContext).pop(),
|
|
child: const Text("Close"),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<void> runAuthDebug(BuildContext context) async {
|
|
final client = context.read<SupabaseProvider>().client;
|
|
final encoder = const JsonEncoder.withIndent(" ");
|
|
|
|
try {
|
|
final response = await client.functions.invoke("auth-debug", body: {});
|
|
final payload = response.data;
|
|
final formatted = payload is Map || payload is List
|
|
? encoder.convert(payload)
|
|
: payload.toString();
|
|
|
|
if (!context.mounted) return;
|
|
await showDialog<void>(
|
|
context: context,
|
|
builder: (dialogContext) => AlertDialog(
|
|
title: const Text("Auth Debug Response"),
|
|
content: SizedBox(
|
|
width: 600,
|
|
child: SingleChildScrollView(child: Text(formatted).small),
|
|
),
|
|
actions: [
|
|
Button.primary(
|
|
onPressed: () => Navigator.of(dialogContext).pop(),
|
|
child: const Text("Close"),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
} catch (error, stackTrace) {
|
|
debugPrint("[HomePage] auth-debug failed (${error.runtimeType}): $error");
|
|
debugPrintStack(stackTrace: stackTrace);
|
|
|
|
if (!context.mounted) return;
|
|
await showDialog<void>(
|
|
context: context,
|
|
builder: (dialogContext) => AlertDialog(
|
|
title: const Text("Auth Debug Failed"),
|
|
content: Text(error.toString()),
|
|
actions: [
|
|
Button.primary(
|
|
onPressed: () => Navigator.of(dialogContext).pop(),
|
|
child: const Text("Close"),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _CreateChannelDialog extends StatelessWidget {
|
|
const _CreateChannelDialog();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final selectedType = ValueNotifier<int>(1);
|
|
final channelName = ValueNotifier<String>("");
|
|
final channelDescription = ValueNotifier<String>("");
|
|
|
|
return AlertDialog(
|
|
title: const Text("Create Channel"),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
ValueListenableBuilder(
|
|
valueListenable: selectedType,
|
|
builder: (context, value, child) {
|
|
return RadioGroup<int>(
|
|
value: value,
|
|
onChanged: (v) {
|
|
selectedType.value = v;
|
|
},
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
spacing: 8,
|
|
children: [
|
|
RadioItem(
|
|
value: 1,
|
|
trailing: Basic(
|
|
title: Text("Text Channel").large,
|
|
subtitle: Text(
|
|
"Standard chat channel for communications and discussions.",
|
|
),
|
|
),
|
|
),
|
|
RadioItem(
|
|
value: 2,
|
|
trailing: Basic(
|
|
title: Text("Operations Channel").large,
|
|
subtitle: Text(
|
|
"Upload a schedule document and interact with it in a way that matters.",
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
const Gap(12),
|
|
const Text("Channel Name").medium.semiBold,
|
|
const Gap(8),
|
|
ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 440),
|
|
child: TextField(
|
|
autofocus: true,
|
|
features: [
|
|
InputFeature.leading(
|
|
Icon(
|
|
LucideIcons.hash,
|
|
color: Theme.of(context).colorScheme.mutedForeground,
|
|
).iconSmall,
|
|
),
|
|
],
|
|
placeholder: const Text("general"),
|
|
onChanged: (value) {
|
|
channelName.value = value;
|
|
},
|
|
onSubmitted: (_) {
|
|
Navigator.of(context).pop({
|
|
"name": channelName.value,
|
|
"type": _channelTypeFromValue(selectedType.value),
|
|
});
|
|
},
|
|
),
|
|
),
|
|
const Gap(12),
|
|
const Text("Channel Description").medium.semiBold,
|
|
const Gap(8),
|
|
ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 440),
|
|
child: TextField(
|
|
placeholder: const Text("What this channel is for"),
|
|
onChanged: (value) {
|
|
channelDescription.value = value;
|
|
},
|
|
onSubmitted: (_) {
|
|
Navigator.of(context).pop({
|
|
"name": channelName.value,
|
|
"description": channelDescription.value,
|
|
"type": _channelTypeFromValue(selectedType.value),
|
|
});
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
actions: [
|
|
Button.text(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
child: const Text("Cancel"),
|
|
),
|
|
Button.primary(
|
|
onPressed: () => Navigator.of(context).pop({
|
|
"name": channelName.value,
|
|
"description": channelDescription.value,
|
|
"type": _channelTypeFromValue(selectedType.value),
|
|
}),
|
|
child: const Text("Create"),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
String _channelTypeFromValue(int value) {
|
|
switch (value) {
|
|
case 2:
|
|
return "operations";
|
|
default:
|
|
return "text";
|
|
}
|
|
}
|