import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; import '../api.dart'; import '../pickers.dart'; import '../theme.dart'; import '../widgets.dart'; const _tabs = { 'pending': 'Waiting', 'needs_info': 'Asked for more', 'approved': 'Approved', 'rejected': 'Rejected', }; /// Singers and creators asking to be verified (the "I'm a singer" form in the app). class ApplicationsScreen extends StatefulWidget { const ApplicationsScreen({super.key}); @override State createState() => _ApplicationsScreenState(); } class _ApplicationsScreenState extends State { String _status = 'pending'; late Future> _data = AdminApi.applications(_status); void _reload() => setState(() => _data = AdminApi.applications(_status)); Future _approve(CreatorApplication app) async { final artists = await AdminApi.artists(); if (!mounted) return; var link = [if (app.artistId != null) app.artistId!]; final note = TextEditingController(); final ok = await showDialog( context: context, builder: (context) => StatefulBuilder( builder: (context, setLocal) => AlertDialog( title: Text('Approve ${app.stageName}'), content: DialogBody(children: [ const Text('If this singer is already in the catalog, choose them so their songs are linked to this ' 'account. Otherwise a new verified singer is created.'), SingerPicker( artists: artists, selected: link, onChanged: (v) => setLocal(() => link = v.isEmpty ? v : [v.last]), ), TextField(controller: note, decoration: const InputDecoration(labelText: 'Note (optional)')), ]), actions: [ TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')), FilledButton(onPressed: () => Navigator.pop(context, true), child: const Text('Approve')), ], ), ), ); if (ok == true) { await _review(app, 'approved', trimmedOrNull(note.text), link.isEmpty ? null : link.single); } } Future _decline(CreatorApplication app, String decision) async { final note = TextEditingController(); final ask = decision == 'needs_info'; final ok = await showDialog( context: context, builder: (context) => AlertDialog( title: Text(ask ? 'Ask ${app.stageName} for more' : 'Reject ${app.stageName}'), content: DialogBody(children: [ TextField( controller: note, autofocus: true, maxLines: 3, decoration: InputDecoration(labelText: ask ? 'What do you need from them?' : 'Reason'), ), ]), actions: [ TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')), FilledButton( style: ask ? null : FilledButton.styleFrom(backgroundColor: NkColors.roofRed, foregroundColor: NkColors.cream), onPressed: () => Navigator.pop(context, true), child: Text(ask ? 'Send back' : 'Reject'), ), ], ), ); if (ok == true) await _review(app, decision, trimmedOrNull(note.text), null); } Future _review(CreatorApplication app, String decision, String? note, String? artistId) async { try { await AdminApi.reviewApplication(app.id, decision, note: note, artistId: artistId); if (mounted) showMessage(context, '${app.stageName}: ${_tabs[decision]!.toLowerCase()}'); _reload(); } catch (e) { if (mounted) showError(context, e); } } Future _openDocument(String path) async { try { await launchUrl(Uri.parse(await AdminApi.idDocumentUrl(path))); } catch (e) { if (mounted) showError(context, e); } } @override Widget build(BuildContext context) => Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ PageHeader('Creator applications', subtitle: 'Verify singers and creators before they can upload', actions: [ SegmentedButton( segments: [for (final e in _tabs.entries) ButtonSegment(value: e.key, label: Text(e.value))], selected: {_status}, onSelectionChanged: (s) { _status = s.first; _reload(); }, ), ]), Expanded( child: Loader( future: _data, onRetry: _reload, builder: (context, apps) => apps.isEmpty ? const EmptyState('No applications here.', icon: Icons.how_to_reg_outlined) : ListView.separated( padding: const EdgeInsets.fromLTRB(24, 0, 24, 24), itemCount: apps.length, separatorBuilder: (_, _) => const SizedBox(height: 12), itemBuilder: (context, i) => _card(apps[i]), ), ), ), ], ); Widget _card(CreatorApplication a) { final small = Theme.of(context).textTheme.bodySmall; final open = a.status == 'pending' || a.status == 'needs_info'; Widget fact(String label, String? value, {String? link}) => value == null || value.isEmpty ? const SizedBox.shrink() : Padding( padding: const EdgeInsets.only(bottom: 4), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(width: 120, child: Text(label, style: small)), Expanded( child: link == null ? SelectableText(value) : InkWell( onTap: () => launchUrl(Uri.parse(link)), child: Text(value, style: const TextStyle(color: NkColors.sun, decoration: TextDecoration.underline)), ), ), ], ), ); String? web(String? s) => s == null || s.isEmpty ? null : (s.startsWith('http') ? s : 'https://$s'); String? instagram(String? s) => s == null || s.isEmpty ? null : (s.startsWith('http') ? s : 'https://instagram.com/${s.replaceAll('@', '')}'); String? whatsapp(String? s) => s == null || s.isEmpty ? null : 'https://wa.me/${s.replaceAll(RegExp(r'[^0-9]'), '')}'; return Card( child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(a.stageName, style: Theme.of(context).textTheme.titleLarge), Text('Applied ${formatDate(a.createdAt)}', style: small), ], ), ), if (a.idDocumentPath != null) OutlinedButton.icon( onPressed: () => _openDocument(a.idDocumentPath!), icon: const Icon(Icons.badge_outlined, size: 18), label: const Text('View ID'), ) else const Tag('No ID uploaded', color: NkColors.roofRed), ], ), const SizedBox(height: 14), fact('Real name', a.realName), fact('Account', [?a.displayName, ?a.email].join(' ยท ')), fact('Phone', a.phone), fact('Village / hatti', a.hattiName), fact('Instagram', a.instagram, link: instagram(a.instagram)), fact('WhatsApp', a.whatsapp, link: whatsapp(a.whatsapp)), for (final l in a.workLinks) fact('Work', l, link: web(l)), fact('Their songs', a.claimedSongs), fact('Message', a.message), fact('Review note', a.reviewNote), if (open) ...[ const SizedBox(height: 12), Wrap( spacing: 8, runSpacing: 8, children: [ FilledButton.icon( onPressed: () => _approve(a), icon: const Icon(Icons.verified_outlined, size: 18), label: const Text('Approve'), ), if (a.status == 'pending') OutlinedButton(onPressed: () => _decline(a, 'needs_info'), child: const Text('Ask for more')), OutlinedButton(onPressed: () => _decline(a, 'rejected'), child: const Text('Reject')), ], ), ], ], ), ), ); } }