Roadbound-BRR/lib/pages/home/widgets/home_dialogs.dart

384 lines
12 KiB
Dart

import "dart:convert";
import "package:bus_running_record/constants.dart";
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: Text("https://$kAppHostname/#/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.text(
onPressed: () {
Navigator.of(dialogContext).pop();
showCreateOrganizationDialog(context);
},
child: const Text("Create one instead"),
),
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";
}
}