Add admin review debug screen
Some checks failed
Build and deploy Flutter Web / build (push) Failing after 14s
Some checks failed
Build and deploy Flutter Web / build (push) Failing after 14s
This commit is contained in:
@@ -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});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user