Initial commit
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:onsolgo/core/constants.dart';
|
||||
import 'dart:math';
|
||||
|
||||
class AdminDashboard extends StatelessWidget {
|
||||
const AdminDashboard({super.key});
|
||||
|
||||
// --- LOGIC: SYSTEM BROADCAST ---
|
||||
void _editAnnouncement(BuildContext context) async {
|
||||
final tC = TextEditingController();
|
||||
final mC = TextEditingController();
|
||||
bool active = true;
|
||||
|
||||
try {
|
||||
var existing = await FirebaseFirestore.instance.collection('system').doc('announcement').get();
|
||||
if (existing.exists) {
|
||||
final map = existing.data();
|
||||
if (map != null) {
|
||||
tC.text = map['title']?.toString() ?? '';
|
||||
mC.text = map['message']?.toString() ?? '';
|
||||
active = map['isActive'] as bool? ?? true;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("Load Error: $e");
|
||||
}
|
||||
|
||||
if (!context.mounted) return;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => StatefulBuilder(
|
||||
builder: (ctx, setS) => AlertDialog(
|
||||
backgroundColor: Colors.grey[900],
|
||||
title: const Text("SYSTEM BROADCAST", style: TextStyle(color: kOnsolGold, fontSize: 14, fontWeight: FontWeight.bold)),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(controller: tC, decoration: const InputDecoration(hintText: "Headline")),
|
||||
const SizedBox(height: 10),
|
||||
TextField(controller: mC, maxLines: 3, decoration: const InputDecoration(hintText: "Message")),
|
||||
SwitchListTile(
|
||||
title: const Text("Show Popup", style: TextStyle(fontSize: 12)),
|
||||
value: active,
|
||||
activeThumbColor: kOnsolGold,
|
||||
onChanged: (v) => setS(() => active = v)
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text("CANCEL", style: TextStyle(color: Colors.grey))),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
await FirebaseFirestore.instance.collection('system').doc('announcement').set({
|
||||
'title': tC.text.trim(),
|
||||
'message': mC.text.trim(),
|
||||
'isActive': active,
|
||||
'updatedAt': FieldValue.serverTimestamp()
|
||||
});
|
||||
if (context.mounted) Navigator.pop(ctx);
|
||||
},
|
||||
child: const Text("TRANSMIT"),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DefaultTabController(
|
||||
length: 4,
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.red[900],
|
||||
title: const Text("COMMAND CENTER", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14)),
|
||||
actions: [
|
||||
IconButton(icon: const Icon(Icons.podcasts), onPressed: () => _editAnnouncement(context))
|
||||
],
|
||||
bottom: const TabBar(
|
||||
labelStyle: TextStyle(fontSize: 8, fontWeight: FontWeight.bold),
|
||||
indicatorColor: Colors.white,
|
||||
tabs: [
|
||||
Tab(text: "SERIES", icon: Icon(Icons.library_books, size: 18)),
|
||||
Tab(text: "MERCH", icon: Icon(Icons.shopping_bag, size: 18)),
|
||||
Tab(text: "CITIZENS", icon: Icon(Icons.people, size: 18)),
|
||||
Tab(text: "INVITES", icon: Icon(Icons.vpn_key, size: 18)),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: const TabBarView(
|
||||
children: [
|
||||
_ManageSeriesTab(),
|
||||
_ManageMerchTab(),
|
||||
_ManageUsersTab(),
|
||||
_ManageInvitesTab(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- TAB 3: CITIZEN MODERATION (Energy & Ranks) ---
|
||||
class _ManageUsersTab extends StatelessWidget {
|
||||
const _ManageUsersTab();
|
||||
|
||||
void _showModeration(BuildContext context, DocumentSnapshot user) {
|
||||
var d = user.data() as Map<String, dynamic>;
|
||||
int rank = safeInt(d['rankLevel'] ?? 1);
|
||||
bool ver = d['isVerified'] ?? (rank == 5);
|
||||
bool ban = d['isBanned'] ?? false;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => StatefulBuilder(
|
||||
builder: (ctx, setS) => AlertDialog(
|
||||
backgroundColor: Colors.grey[900],
|
||||
title: Text("MODERATING: ${d['username'] ?? 'Citizen'}"),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
DropdownButton<int>(
|
||||
value: rank,
|
||||
isExpanded: true,
|
||||
dropdownColor: Colors.black,
|
||||
items: [1, 2, 3, 4, 5].map((v) => DropdownMenuItem(value: v, child: Text(getRankName(v)))).toList(),
|
||||
onChanged: (v) => setS(() {
|
||||
rank = v!;
|
||||
if (rank == 5) ver = true;
|
||||
}),
|
||||
),
|
||||
SwitchListTile(title: const Text("Verified"), value: ver, activeThumbColor: kOnsolGold, onChanged: (v) => setS(() => ver = v)),
|
||||
SwitchListTile(title: const Text("Exiled"), value: ban, activeThumbColor: Colors.red, onChanged: (v) => setS(() => ban = v)),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// --- ENERGY REFILL BUTTON (2/2) ---
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.orangeAccent, foregroundColor: Colors.black, minimumSize: const Size(double.infinity, 40)),
|
||||
onPressed: () async {
|
||||
await user.reference.update({
|
||||
'energy': 2,
|
||||
'lastEnergyRefill': FieldValue.serverTimestamp()
|
||||
});
|
||||
if (ctx.mounted) Navigator.pop(ctx);
|
||||
},
|
||||
child: const Text("REFILL ENERGY (2/2)", style: TextStyle(fontWeight: FontWeight.bold))
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text("CANCEL")),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
await user.reference.update({'rankLevel': rank, 'isVerified': ver, 'isBanned': ban});
|
||||
if (ctx.mounted) Navigator.pop(ctx);
|
||||
},
|
||||
child: const Text("APPLY"),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder<QuerySnapshot>(
|
||||
stream: FirebaseFirestore.instance.collection('users').snapshots(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) return const Center(child: CircularProgressIndicator());
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(15),
|
||||
children: snapshot.data!.docs.map((u) {
|
||||
var d = u.data() as Map<String, dynamic>;
|
||||
int r = safeInt(d['rankLevel'] ?? 1);
|
||||
bool isBan = d['isBanned'] ?? false;
|
||||
return Card(
|
||||
color: Colors.grey[900],
|
||||
child: ListTile(
|
||||
leading: Icon(getRankIcon(r), color: isBan ? Colors.red : getRankColor(r)),
|
||||
title: Row(children: [
|
||||
Text(d['username'] ?? 'Anonymous', style: TextStyle(color: isBan ? Colors.red : Colors.white)),
|
||||
verifiedBadge(d['isVerified'] ?? (r == 5))
|
||||
]),
|
||||
subtitle: Text(isBan ? "EXILED" : getRankName(r)),
|
||||
trailing: IconButton(icon: const Icon(Icons.shield, color: Colors.amber), onPressed: () => _showModeration(context, u)),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- OTHER TABS ---
|
||||
|
||||
class _ManageSeriesTab extends StatelessWidget {
|
||||
const _ManageSeriesTab();
|
||||
void _add(BuildContext context) {
|
||||
final t = TextEditingController();
|
||||
showDialog(context: context, builder: (ctx) => AlertDialog(
|
||||
backgroundColor: Colors.grey[900], title: const Text("New Series"),
|
||||
content: TextField(controller: t, decoration: const InputDecoration(hintText: "Series Title")),
|
||||
actions: [ElevatedButton(onPressed: () { FirebaseFirestore.instance.collection('manga').add({'title': t.text, 'reads': 0, 'readingMode': 'RL', 'author': 'Artist', 'coverUrl': ''}); Navigator.pop(ctx); }, child: const Text("ADD"))],
|
||||
));
|
||||
}
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(backgroundColor: Colors.transparent, floatingActionButton: FloatingActionButton(backgroundColor: Colors.white, child: const Icon(Icons.add, color: Colors.black), onPressed: () => _add(context)), body: StreamBuilder<QuerySnapshot>(stream: FirebaseFirestore.instance.collection('manga').snapshots(), builder: (context, snapshot) { if (!snapshot.hasData) return const CircularProgressIndicator(); return ListView(padding: const EdgeInsets.all(15), children: snapshot.data!.docs.map((doc) => Card(color: Colors.grey[900], child: ListTile(title: Text(doc['title'] ?? 'Untitled'), trailing: IconButton(icon: const Icon(Icons.delete_outline, color: Colors.red), onPressed: () => doc.reference.delete())))).toList()); }));
|
||||
}
|
||||
}
|
||||
|
||||
class _ManageMerchTab extends StatelessWidget {
|
||||
const _ManageMerchTab();
|
||||
void _add(BuildContext context) {
|
||||
final n = TextEditingController(); final p = TextEditingController(); final u = TextEditingController(); String cat = 'vault';
|
||||
showDialog(context: context, builder: (ctx) => StatefulBuilder(builder: (context, setS) => AlertDialog(backgroundColor: Colors.grey[900], title: const Text("New Item"),
|
||||
content: SingleChildScrollView(child: Column(mainAxisSize: MainAxisSize.min, children: [TextField(controller: n, decoration: const InputDecoration(hintText: "Name")), TextField(controller: p, decoration: const InputDecoration(hintText: "Price")), TextField(controller: u, decoration: const InputDecoration(hintText: "Buy URL")), DropdownButton<String>(value: cat, isExpanded: true, dropdownColor: Colors.black, items: ['vault', 'prints', 'merch'].map((s) => DropdownMenuItem(value: s, child: Text(s.toUpperCase()))).toList(), onChanged: (v) => setS(() => cat = v!))])),
|
||||
actions: [ElevatedButton(onPressed: () { FirebaseFirestore.instance.collection('marketplace').add({'name': n.text, 'price': p.text, 'buyUrl': u.text, 'category': cat, 'imageUrl': ''}); Navigator.pop(ctx); }, child: const Text("ADD"))],
|
||||
)));
|
||||
}
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(backgroundColor: Colors.transparent, floatingActionButton: FloatingActionButton(backgroundColor: kOnsolGold, child: const Icon(Icons.add_shopping_cart, color: Colors.black), onPressed: () => _add(context)), body: StreamBuilder<QuerySnapshot>(stream: FirebaseFirestore.instance.collection('marketplace').snapshots(), builder: (context, snapshot) { if (!snapshot.hasData) return const CircularProgressIndicator(); return ListView(padding: const EdgeInsets.all(15), children: snapshot.data!.docs.map((doc) => Card(color: Colors.grey[900], child: ListTile(title: Text(doc['name'] ?? 'Item'), subtitle: Text(doc['price'] ?? ''), trailing: IconButton(icon: const Icon(Icons.delete_outline, color: Colors.red), onPressed: () => doc.reference.delete())))).toList()); }));
|
||||
}
|
||||
}
|
||||
|
||||
class _ManageInvitesTab extends StatelessWidget {
|
||||
const _ManageInvitesTab();
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(children: [
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton(style: ElevatedButton.styleFrom(backgroundColor: Colors.white, foregroundColor: Colors.black), onPressed: () {
|
||||
String code = List.generate(6, (i) => "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"[Random().nextInt(30)]).join();
|
||||
FirebaseFirestore.instance.collection('invite_codes').doc(code).set({'isUsed': false, 'createdAt': FieldValue.serverTimestamp()});
|
||||
}, child: const Text("GENERATE INVITE")),
|
||||
Expanded(child: StreamBuilder<QuerySnapshot>(stream: FirebaseFirestore.instance.collection('invite_codes').snapshots(), builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) return const Center(child: CircularProgressIndicator());
|
||||
var docs = snapshot.data!.docs;
|
||||
docs.sort((a,b) {
|
||||
var at = (a.data() as Map)['createdAt'] as Timestamp? ?? Timestamp.now();
|
||||
var bt = (b.data() as Map)['createdAt'] as Timestamp? ?? Timestamp.now();
|
||||
return bt.compareTo(at);
|
||||
});
|
||||
return ListView(padding: const EdgeInsets.all(20), children: docs.map((doc) {
|
||||
var d = doc.data() as Map<String, dynamic>;
|
||||
return ListTile(title: Text(doc.id, style: const TextStyle(letterSpacing: 4, fontWeight: FontWeight.bold, color: Colors.white)), trailing: (d['isUsed'] ?? false) ? const Icon(Icons.check_circle, color: Colors.green) : const Icon(Icons.hourglass_empty, color: Colors.amber));
|
||||
}).toList());
|
||||
}))
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user