Add admin review debug screen
Some checks failed
Build and deploy Flutter Web / build (push) Failing after 14s

This commit is contained in:
Ruslan Bakiev
2026-05-14 08:44:20 +07:00
parent cdf6a43d49
commit fbf9104d2d
5 changed files with 264 additions and 31 deletions

View File

@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math' as math;
import 'package:flutter/material.dart';
@@ -97,13 +98,26 @@ class _MapContent extends ConsumerWidget {
SafeArea(
child: Align(
alignment: Alignment.topLeft,
child: _UserAvatar(
user: state.currentUser,
onLogout: () {
telegram_session.clearMapflowSession();
ref.invalidate(placeControllerProvider);
telegram_session.reloadApp();
},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_UserAvatar(
user: state.currentUser,
onLogout: () {
telegram_session.clearMapflowSession();
ref.invalidate(placeControllerProvider);
telegram_session.reloadApp();
},
),
if (state.currentUser?.isAdmin == true)
_AdminReviewsButton(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const AdminVoiceExperiencesScreen(),
),
),
),
],
),
),
),
@@ -259,6 +273,30 @@ class _UserAvatar extends StatelessWidget {
}
}
class _AdminReviewsButton extends StatelessWidget {
const _AdminReviewsButton({required this.onPressed});
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 8),
child: FilledButton.icon(
onPressed: onPressed,
icon: const Icon(Icons.table_rows_outlined, size: 18),
label: const Text('Отзывы'),
style: FilledButton.styleFrom(
backgroundColor: const Color(0xFFFFFBF5),
foregroundColor: const Color(0xFF17211D),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
),
),
);
}
}
enum _AvatarAction { logout }
class _AvatarImage extends StatelessWidget {
@@ -842,7 +880,18 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
onSelect: (place) async {
setState(() => _submitting = true);
controller.setReviewPlace(place.name);
await controller.publishReview(place: place);
final file = _waveController.file;
if (file == null) {
throw StateError('Voice recording file is required.');
}
final bytes = await file.readAsBytes();
await controller.publishReview(
place: place,
audioObjectKey:
'web-recording-${DateTime.now().microsecondsSinceEpoch}-${file.name}',
audioContentBase64: base64Encode(bytes),
audioMimeType: file.mimeType ?? 'audio/wav',
);
if (!context.mounted) {
return;
}
@@ -884,6 +933,114 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
}
}
class AdminVoiceExperiencesScreen extends StatefulWidget {
const AdminVoiceExperiencesScreen({super.key});
@override
State<AdminVoiceExperiencesScreen> createState() =>
_AdminVoiceExperiencesScreenState();
}
class _AdminVoiceExperiencesScreenState
extends State<AdminVoiceExperiencesScreen> {
late final Future<List<VoiceExperienceDebug>> _future = MapflowApi()
.fetchVoiceExperiences();
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFFFFBF5),
appBar: AppBar(title: const Text('Отзывы')),
body: FutureBuilder<List<VoiceExperienceDebug>>(
future: _future,
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text(snapshot.error.toString()));
}
final reviews = snapshot.data ?? const <VoiceExperienceDebug>[];
return ListView.separated(
padding: const EdgeInsets.all(12),
itemCount: reviews.length,
separatorBuilder: (_, _) => const SizedBox(height: 8),
itemBuilder: (context, index) {
return _AdminVoiceExperienceRow(review: reviews[index]);
},
);
},
),
);
}
}
class _AdminVoiceExperienceRow extends StatelessWidget {
const _AdminVoiceExperienceRow({required this.review});
final VoiceExperienceDebug review;
@override
Widget build(BuildContext context) {
final tags = review.analysis?['tags'];
final tagText = tags is List ? tags.join(', ') : '';
return Material(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
review.placeName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w900),
),
),
Text(
review.status,
style: const TextStyle(fontWeight: FontWeight.w700),
),
],
),
const SizedBox(height: 6),
Text(
'${review.userName} · ${review.durationSeconds}s',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Color(0xFF6B6258)),
),
if (review.transcript?.trim().isNotEmpty == true) ...[
const SizedBox(height: 8),
Text(
review.transcript!,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
],
if (tagText.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
tagText,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Color(0xFFE11D48)),
),
],
],
),
),
);
}
}
class _IntroStep extends StatelessWidget {
const _IntroStep({required this.onNext});