dockerfiles
Some checks are pending
Deploy Admin Panel / Deploy Admin Panel (push) Waiting to run
Deploy Admin Panel / Admin Panel Verification (push) Blocked by required conditions
Deploy Mnemo Cards / Deploy Backend (push) Waiting to run
Deploy Mnemo Cards / Deploy Web App (push) Blocked by required conditions
Deploy Mnemo Cards / Final Verification (push) Blocked by required conditions
Deploy Telegram Bot / Deploy Telegram Bot (push) Waiting to run
Some checks are pending
Deploy Admin Panel / Deploy Admin Panel (push) Waiting to run
Deploy Admin Panel / Admin Panel Verification (push) Blocked by required conditions
Deploy Mnemo Cards / Deploy Backend (push) Waiting to run
Deploy Mnemo Cards / Deploy Web App (push) Blocked by required conditions
Deploy Mnemo Cards / Final Verification (push) Blocked by required conditions
Deploy Telegram Bot / Deploy Telegram Bot (push) Waiting to run
This commit is contained in:
parent
7295a15759
commit
1e424031dd
79 changed files with 158 additions and 3137 deletions
37
mnemo_cards_admin/Dockerfile
Normal file
37
mnemo_cards_admin/Dockerfile
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
# syntax=docker/dockerfile:1.7
|
||||||
|
|
||||||
|
# --- Build stage ------------------------------------------------------------
|
||||||
|
FROM node:20-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files for dependency caching
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Copy source and build
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Verify build output
|
||||||
|
RUN ls -la dist/ && test -f dist/index.html || (echo "ERROR: index.html not found!" && exit 1)
|
||||||
|
|
||||||
|
# --- Runtime stage ----------------------------------------------------------
|
||||||
|
FROM nginx:1.27-alpine AS runtime
|
||||||
|
|
||||||
|
# Install wget for healthcheck
|
||||||
|
RUN apk add --no-cache wget
|
||||||
|
|
||||||
|
# Copy nginx config
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
# Copy built app
|
||||||
|
COPY --from=build /app/dist/ /usr/share/nginx/html/
|
||||||
|
|
||||||
|
# Verify files were copied
|
||||||
|
RUN ls -la /usr/share/nginx/html/ && test -f /usr/share/nginx/html/index.html || (echo "ERROR: index.html not copied!" && exit 1)
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
# Healthcheck
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=10s \
|
||||||
|
CMD wget --no-verbose --tries=1 --spider http://127.0.0.1/ || exit 1
|
||||||
41
mnemo_cards_admin/nginx.conf
Normal file
41
mnemo_cards_admin/nginx.conf
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# Gzip compression
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
gzip_types text/plain text/css text/xml text/javascript application/javascript application/json;
|
||||||
|
|
||||||
|
# Security headers (enhanced for admin panel)
|
||||||
|
add_header X-Frame-Options "DENY" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
|
||||||
|
# Cache static assets
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
|
expires 1h;
|
||||||
|
add_header Cache-Control "private, must-revalidate";
|
||||||
|
}
|
||||||
|
|
||||||
|
# React SPA routing
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Health check endpoint
|
||||||
|
location /health {
|
||||||
|
access_log off;
|
||||||
|
return 200 "OK";
|
||||||
|
add_header Content-Type text/plain;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Security - deny access to hidden files
|
||||||
|
location ~ /\. {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,332 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:convert';
|
|
||||||
import 'package:mnemo_cards/domain/router/dialogs.dart';
|
|
||||||
import 'package:universal_io/io.dart';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:file_picker/file_picker.dart';
|
|
||||||
import 'package:mnemo_cards/admin/admin_api.dart';
|
|
||||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
|
||||||
import 'package:mnemo_cards_frontend_common/mnemo_cards_frontend_common.dart';
|
|
||||||
|
|
||||||
import '../main.dart';
|
|
||||||
|
|
||||||
class EditCard {
|
|
||||||
GameCardDto gameCardDto;
|
|
||||||
AdminApi adminApi;
|
|
||||||
bool imageChanged = false;
|
|
||||||
|
|
||||||
EditCard(GameCardDto? cardDto, this.adminApi)
|
|
||||||
: gameCardDto =
|
|
||||||
cardDto ??
|
|
||||||
const GameCardDto(
|
|
||||||
id: -1,
|
|
||||||
image: null,
|
|
||||||
mnemo: null,
|
|
||||||
original: null,
|
|
||||||
translation: null,
|
|
||||||
transcription: null,
|
|
||||||
transcriptionMnemo: null,
|
|
||||||
imageBack: null,
|
|
||||||
back: null,
|
|
||||||
);
|
|
||||||
|
|
||||||
void _saveCard() async {
|
|
||||||
final dto = imageChanged ? gameCardDto : gameCardDto.copyWith.image(null);
|
|
||||||
final _progress = ValueNotifier<(int, int)>((0, 1));
|
|
||||||
final _dialogCompleter = Completer();
|
|
||||||
showInfoDialog(
|
|
||||||
'',
|
|
||||||
child: ValueListenableBuilder(
|
|
||||||
valueListenable: _progress,
|
|
||||||
builder: (c, v, _) => Text(
|
|
||||||
(v.$1 == v.$2 || v.$2 < 0)
|
|
||||||
? 'Waiting for response...'
|
|
||||||
: 'Uploading ${v.$1}/${v.$2}...',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
completer: _dialogCompleter,
|
|
||||||
);
|
|
||||||
final result = await adminApi.addCard(
|
|
||||||
dto,
|
|
||||||
progressCallback: (a, b) => _progress.value = (a, b),
|
|
||||||
);
|
|
||||||
_dialogCompleter.complete();
|
|
||||||
if (!result) {
|
|
||||||
ScaffoldMessenger.of(
|
|
||||||
scaffoldKey.currentContext!,
|
|
||||||
).showSnackBar(SnackBar(content: Text('ERROR')));
|
|
||||||
} else {
|
|
||||||
ScaffoldMessenger.of(
|
|
||||||
scaffoldKey.currentContext!,
|
|
||||||
).showSnackBar(SnackBar(content: Text('Success!!!')));
|
|
||||||
appRouter.maybePop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _deleteCard() async {
|
|
||||||
final result = await adminApi.deleteCard(gameCardDto);
|
|
||||||
if (!result) {
|
|
||||||
ScaffoldMessenger.of(
|
|
||||||
scaffoldKey.currentContext!,
|
|
||||||
).showSnackBar(SnackBar(content: Text('ERROR')));
|
|
||||||
} else {
|
|
||||||
ScaffoldMessenger.of(
|
|
||||||
scaffoldKey.currentContext!,
|
|
||||||
).showSnackBar(SnackBar(content: Text('Success!!!')));
|
|
||||||
appRouter.maybePop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void addCard(BuildContext context) {
|
|
||||||
final theme = Theme.of(context).textTheme;
|
|
||||||
|
|
||||||
final TextEditingController originalController = TextEditingController(
|
|
||||||
text: gameCardDto.original,
|
|
||||||
);
|
|
||||||
final TextEditingController translationController = TextEditingController(
|
|
||||||
text: gameCardDto.translation,
|
|
||||||
);
|
|
||||||
final TextEditingController transcriptionController = TextEditingController(
|
|
||||||
text: gameCardDto.transcription,
|
|
||||||
);
|
|
||||||
final TextEditingController transcriptionMnemoController =
|
|
||||||
TextEditingController(text: gameCardDto.transcriptionMnemo);
|
|
||||||
final TextEditingController mnemoController = TextEditingController(
|
|
||||||
text: gameCardDto.mnemo,
|
|
||||||
);
|
|
||||||
final TextEditingController backController = TextEditingController(
|
|
||||||
text: gameCardDto.back,
|
|
||||||
);
|
|
||||||
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (c) {
|
|
||||||
return Center(
|
|
||||||
child: Scaffold(
|
|
||||||
body: Material(
|
|
||||||
child: ListView(
|
|
||||||
children: [
|
|
||||||
Header(
|
|
||||||
'${gameCardDto.id}',
|
|
||||||
hasPopButton: true,
|
|
||||||
trail: Row(
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
onPressed: _saveCard,
|
|
||||||
icon: Icon(Icons.save),
|
|
||||||
),
|
|
||||||
if (gameCardDto.id >= 0)
|
|
||||||
GestureDetector(
|
|
||||||
onLongPress: _deleteCard,
|
|
||||||
child: Icon(Icons.delete),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
StatefulBuilder(
|
|
||||||
builder: (context, setState) {
|
|
||||||
return Stack(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.all(8.0),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
||||||
children: [
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
TextField(
|
|
||||||
controller: originalController,
|
|
||||||
onChanged: (v) {
|
|
||||||
gameCardDto = gameCardDto.copyWith(
|
|
||||||
original: v,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: 'Original (cerdo)',
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
|
||||||
style: theme.displayMedium?.copyWith(
|
|
||||||
fontSize:
|
|
||||||
theme.displayMedium!.fontSize!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextField(
|
|
||||||
controller: translationController,
|
|
||||||
onChanged: (v) {
|
|
||||||
gameCardDto = gameCardDto.copyWith(
|
|
||||||
translation: v,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: 'Translation (свинья)',
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
|
||||||
style: theme.headlineSmall?.copyWith(
|
|
||||||
color: theme.headlineSmall!.color!
|
|
||||||
.withOpacity(0.5),
|
|
||||||
fontSize:
|
|
||||||
theme.headlineSmall!.fontSize!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Flexible(
|
|
||||||
child: TextField(
|
|
||||||
controller: transcriptionController,
|
|
||||||
onChanged: (v) {
|
|
||||||
gameCardDto = gameCardDto
|
|
||||||
.copyWith(transcription: v);
|
|
||||||
},
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: 'Transcription',
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
|
||||||
style: theme.headlineSmall
|
|
||||||
?.copyWith(
|
|
||||||
color: theme
|
|
||||||
.headlineSmall!
|
|
||||||
.color!,
|
|
||||||
fontSize: theme
|
|
||||||
.headlineSmall!
|
|
||||||
.fontSize!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 4.0),
|
|
||||||
Flexible(
|
|
||||||
child: TextField(
|
|
||||||
controller:
|
|
||||||
transcriptionMnemoController,
|
|
||||||
onChanged: (v) {
|
|
||||||
gameCardDto = gameCardDto
|
|
||||||
.copyWith(
|
|
||||||
transcriptionMnemo: v,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: 'Transcription mnemo',
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
|
||||||
style: theme.headlineSmall
|
|
||||||
?.copyWith(
|
|
||||||
color: theme
|
|
||||||
.headlineSmall!
|
|
||||||
.color!,
|
|
||||||
fontSize: theme
|
|
||||||
.headlineSmall!
|
|
||||||
.fontSize!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: MediaQuery.of(context).size.width - 20,
|
|
||||||
height:
|
|
||||||
MediaQuery.of(context).size.width - 20,
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () async {
|
|
||||||
FilePickerResult? result =
|
|
||||||
await FilePicker.platform.pickFiles();
|
|
||||||
if (result != null &&
|
|
||||||
result.files.isNotEmpty) {
|
|
||||||
imageChanged = true;
|
|
||||||
final file = result.files.first;
|
|
||||||
final encoded = base64.normalize(
|
|
||||||
base64.encode(
|
|
||||||
File(file.path!).readAsBytesSync(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
gameCardDto = gameCardDto.copyWith(
|
|
||||||
image: encoded,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
child: gameCardDto.image == null
|
|
||||||
? Container(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
color: Colors.white,
|
|
||||||
child: Icon(
|
|
||||||
Icons.image,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Stack(
|
|
||||||
children: [
|
|
||||||
Image(
|
|
||||||
key: ValueKey(base64),
|
|
||||||
image: MemoryImage(
|
|
||||||
base64.decode(
|
|
||||||
gameCardDto.image!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
gaplessPlayback: true,
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
top: 8.0,
|
|
||||||
right: 8.0,
|
|
||||||
child: IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
gameCardDto = gameCardDto
|
|
||||||
.copyWith(image: null);
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
icon: Icon(Icons.delete),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextField(
|
|
||||||
controller: mnemoController,
|
|
||||||
onChanged: (v) {
|
|
||||||
gameCardDto = gameCardDto.copyWith(
|
|
||||||
mnemo: v,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: 'Mnemo ([свинья] с сердечком)',
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
|
||||||
style: theme.headlineSmall?.copyWith(
|
|
||||||
fontSize: theme.headlineSmall!.fontSize!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextField(
|
|
||||||
controller: backController,
|
|
||||||
onChanged: (v) {
|
|
||||||
gameCardDto = gameCardDto.copyWith(back: v);
|
|
||||||
},
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: 'Задняя сторона',
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
|
||||||
style: theme.headlineSmall?.copyWith(
|
|
||||||
fontSize: theme.headlineSmall!.fontSize!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,383 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:developer';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:mnemo_cards/admin/admin_api.dart';
|
|
||||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
|
||||||
import 'package:mnemo_cards_frontend_common/mnemo_cards_frontend_common.dart';
|
|
||||||
|
|
||||||
import '../di/injector.dart';
|
|
||||||
import '../main.dart';
|
|
||||||
import 'date_param.dart';
|
|
||||||
import 'enum_param.dart';
|
|
||||||
import 'int_param.dart';
|
|
||||||
import 'text_param.dart';
|
|
||||||
|
|
||||||
class AddDiscountCampaign {
|
|
||||||
late DiscountCampaignDto campaignDto;
|
|
||||||
late AdminApi adminApi;
|
|
||||||
List<DiscountCampaignDto> _campaigns = [];
|
|
||||||
|
|
||||||
AddDiscountCampaign() {
|
|
||||||
this.adminApi = getIt.get<AdminApi>();
|
|
||||||
_init(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _init(DiscountCampaignDto? dto) {
|
|
||||||
final now = DateTime.now();
|
|
||||||
campaignDto = dto ??
|
|
||||||
DiscountCampaignDto(
|
|
||||||
start: now,
|
|
||||||
finish: now.add(Duration(days: 30)),
|
|
||||||
status: DiscountCampaignStatus.disabled,
|
|
||||||
name: null,
|
|
||||||
tags: [],
|
|
||||||
discounts: [],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> _loadCampaigns() async {
|
|
||||||
try {
|
|
||||||
_campaigns = await adminApi.getDiscountCampaigns();
|
|
||||||
} catch (e, s) {
|
|
||||||
log('Error', error: e, stackTrace: s);
|
|
||||||
print('Error $e');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _savePromoCodeCampaign() async {
|
|
||||||
for (final d in campaignDto.discounts) {
|
|
||||||
final products = d.products.map((p) => p.toProduct());
|
|
||||||
d.products.replaceRange(0, products.length, products);
|
|
||||||
}
|
|
||||||
|
|
||||||
final result = await adminApi.addDiscountCampaign(campaignDto);
|
|
||||||
if (!result) {
|
|
||||||
ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar(
|
|
||||||
content: Text('ERROR'),
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar(
|
|
||||||
content: Text('Success!!!'),
|
|
||||||
));
|
|
||||||
appRouter.maybePop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _deleteDiscountCampaign() async {
|
|
||||||
if (campaignDto.id == null) return;
|
|
||||||
final result =
|
|
||||||
await adminApi.deleteDiscountCampaign(campaignDto.id.toString());
|
|
||||||
if (result != null) {
|
|
||||||
ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar(
|
|
||||||
content: Text('ERROR'),
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar(
|
|
||||||
content: Text('Success!!!'),
|
|
||||||
));
|
|
||||||
appRouter.maybePop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void addCampaign(BuildContext context) {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (c) {
|
|
||||||
return FutureBuilder(
|
|
||||||
future: _loadCampaigns(),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (snapshot.data != true) {
|
|
||||||
return Center(
|
|
||||||
child: Material(
|
|
||||||
child: Center(child: CircularProgressIndicator()),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Center(
|
|
||||||
child: Scaffold(
|
|
||||||
body: Material(
|
|
||||||
child: StatefulBuilder(builder: (context, setCampaign) {
|
|
||||||
return ListView(
|
|
||||||
children: [
|
|
||||||
Header(
|
|
||||||
'${campaignDto.id}',
|
|
||||||
hasPopButton: true,
|
|
||||||
trail: Row(
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
onPressed: _savePromoCodeCampaign,
|
|
||||||
icon: Icon(Icons.save),
|
|
||||||
),
|
|
||||||
if (campaignDto.id != null)
|
|
||||||
IconButton(
|
|
||||||
onPressed: () async {
|
|
||||||
campaignDto =
|
|
||||||
campaignDto.copyWith(id: null);
|
|
||||||
_savePromoCodeCampaign();
|
|
||||||
},
|
|
||||||
icon: Icon(Icons.copy),
|
|
||||||
),
|
|
||||||
if (campaignDto.id != null)
|
|
||||||
GestureDetector(
|
|
||||||
onLongPress: _deleteDiscountCampaign,
|
|
||||||
child: Icon(Icons.delete),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
DropdownMenu<int?>(
|
|
||||||
initialSelection: null,
|
|
||||||
onSelected: (int? value) {
|
|
||||||
setCampaign(() {
|
|
||||||
_init(
|
|
||||||
_campaigns
|
|
||||||
.firstWhereOrNull((v) => v.id == value),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
dropdownMenuEntries: [
|
|
||||||
..._campaigns,
|
|
||||||
null,
|
|
||||||
].map((campaign) {
|
|
||||||
return DropdownMenuEntry(
|
|
||||||
value: campaign?.id,
|
|
||||||
label: campaign == null
|
|
||||||
? 'New'
|
|
||||||
: '${campaign.name ?? campaign.id} ${campaign.status.name}',
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
StatefulBuilder(builder: (context, setState) {
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.all(8.0),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
TextParam(
|
|
||||||
'Name',
|
|
||||||
campaignDto.name,
|
|
||||||
(v) {
|
|
||||||
campaignDto = campaignDto.copyWith(
|
|
||||||
name: v,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
DateParam(
|
|
||||||
'Start',
|
|
||||||
campaignDto.start,
|
|
||||||
(v) {
|
|
||||||
setState(() {
|
|
||||||
campaignDto =
|
|
||||||
campaignDto.copyWith(start: v);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
DateParam(
|
|
||||||
'Finish',
|
|
||||||
campaignDto.finish,
|
|
||||||
(v) {
|
|
||||||
setState(() {
|
|
||||||
campaignDto =
|
|
||||||
campaignDto.copyWith(finish: v);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
EnumParam(
|
|
||||||
'Status',
|
|
||||||
DiscountCampaignStatus.values,
|
|
||||||
campaignDto.status, (v) {
|
|
||||||
setState(() {
|
|
||||||
campaignDto =
|
|
||||||
campaignDto.copyWith(status: v);
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
Text(
|
|
||||||
'Created to create or change\n'
|
|
||||||
'Disabled to disable\n'
|
|
||||||
'Dont use other statuses!',
|
|
||||||
style: TextStyle(fontSize: 12),
|
|
||||||
),
|
|
||||||
Text('Discounts:'),
|
|
||||||
ListView.builder(
|
|
||||||
shrinkWrap: true,
|
|
||||||
itemCount:
|
|
||||||
campaignDto.discounts.length + 1,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
if (index ==
|
|
||||||
campaignDto.discounts.length) {
|
|
||||||
return Container(
|
|
||||||
child: SimpleTile(
|
|
||||||
title: Text('Add'),
|
|
||||||
onTap: () async {
|
|
||||||
final discount =
|
|
||||||
await _showDiscountDialog(
|
|
||||||
context, null);
|
|
||||||
if (discount != null) {
|
|
||||||
setState(() {
|
|
||||||
final ex = campaignDto
|
|
||||||
.discounts
|
|
||||||
.firstWhereOrNull((d) =>
|
|
||||||
d.id ==
|
|
||||||
discount.id);
|
|
||||||
if (ex != null) {
|
|
||||||
campaignDto.discounts
|
|
||||||
.remove(ex);
|
|
||||||
}
|
|
||||||
campaignDto.discounts
|
|
||||||
.add(discount);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
final item =
|
|
||||||
campaignDto.discounts[index];
|
|
||||||
return ListTile(
|
|
||||||
title: Text(
|
|
||||||
'${item.type.name} ${item.id ?? ''}'),
|
|
||||||
onTap: () async {
|
|
||||||
final p = await _showDiscountDialog(
|
|
||||||
context,
|
|
||||||
item,
|
|
||||||
);
|
|
||||||
if (p != null) {
|
|
||||||
setState(() {
|
|
||||||
campaignDto.discounts.add(p);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
trailing: IconButton(
|
|
||||||
icon: Icon(Icons.delete),
|
|
||||||
onPressed: () {
|
|
||||||
setState(() {
|
|
||||||
campaignDto.discounts
|
|
||||||
.remove(item);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<DiscountDto?> _showDiscountDialog(
|
|
||||||
BuildContext context,
|
|
||||||
DiscountDto? initialDiscount,
|
|
||||||
) async {
|
|
||||||
var prod = initialDiscount ?? DiscountDto(discountPercent: 0, products: []);
|
|
||||||
final result = await showDialog<DiscountDto?>(
|
|
||||||
context: context,
|
|
||||||
builder: (c) => Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Material(
|
|
||||||
child: FutureBuilder(
|
|
||||||
future: () async {
|
|
||||||
final allPacks = await adminApi.packs();
|
|
||||||
final allPlans = await adminApi.getSubscriptionPlans();
|
|
||||||
return [...allPacks, ...allPlans];
|
|
||||||
}.call(),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (!snapshot.hasData) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Text('Loading products...'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
final products = snapshot.requireData;
|
|
||||||
return SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text('Adding discount'),
|
|
||||||
TextParam(
|
|
||||||
'Id',
|
|
||||||
prod.id,
|
|
||||||
(v) {
|
|
||||||
prod = prod.copyWith(id: v);
|
|
||||||
},
|
|
||||||
description: 'Leave empty for new',
|
|
||||||
),
|
|
||||||
IntParam('Discount 0-100', prod.discountPercent.toInt(),
|
|
||||||
(v) {
|
|
||||||
prod = prod.copyWith(
|
|
||||||
discountPercent: v?.toDouble() ?? 0.0);
|
|
||||||
}),
|
|
||||||
StatefulBuilder(builder: (context, ss) {
|
|
||||||
return ListView.builder(
|
|
||||||
shrinkWrap: true,
|
|
||||||
itemCount: prod.products.length + 1,
|
|
||||||
itemBuilder: (BuildContext context, int index) {
|
|
||||||
if (index == prod.products.length) {
|
|
||||||
return EnumParam(
|
|
||||||
'Add product',
|
|
||||||
products,
|
|
||||||
null,
|
|
||||||
(p) {
|
|
||||||
if (p != null) {
|
|
||||||
ss(() {
|
|
||||||
prod.products.add(p);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getName: (p) => _productName(p, products),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
final p = prod.products[index];
|
|
||||||
return ListTile(
|
|
||||||
title: Text(_productName(p, products)),
|
|
||||||
onTap: () async {
|
|
||||||
ss(() {
|
|
||||||
prod.products.remove(p);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
MaterialButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).pop(prod);
|
|
||||||
},
|
|
||||||
child: Text('Save'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
String _productName(MnemoCardsProductDto? p, List<MnemoCardsProductDto> products) {
|
|
||||||
if (p is CardPackPreviewDto) {
|
|
||||||
return '${p.id} ${p.title} ${p.price} pack';
|
|
||||||
} else if (p is SubscriptionPlanAdminDto) {
|
|
||||||
return '${p.id} ${p.ui.titleLead ?? ''} ${p.ui.title ?? ''} ${p.paymentSystem.name} ${p.price} plan';
|
|
||||||
}
|
|
||||||
final matchedProduct = products.firstWhereOrNull((m) => m.productEquals(p));
|
|
||||||
if (matchedProduct != null) {
|
|
||||||
return _productName(matchedProduct, []);
|
|
||||||
}
|
|
||||||
return '${p?.type.name} ${p?.id}';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,298 +0,0 @@
|
||||||
import 'dart:convert';
|
|
||||||
import 'package:universal_io/io.dart';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
|
|
||||||
import 'package:mnemo_cards/admin/admin_api.dart';
|
|
||||||
import 'package:mnemo_cards/managers/repository/api.dart';
|
|
||||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
|
||||||
import 'package:file_picker/file_picker.dart';
|
|
||||||
import 'package:mnemo_cards_frontend_common/mnemo_cards_frontend_common.dart';
|
|
||||||
import '../utils/string_helper.dart';
|
|
||||||
|
|
||||||
import '../main.dart';
|
|
||||||
import 'text_param.dart';
|
|
||||||
|
|
||||||
class AddPackage with Api {
|
|
||||||
EditCardPackDto cardPackDto;
|
|
||||||
AdminApi adminApi;
|
|
||||||
bool palleteVisible = false;
|
|
||||||
|
|
||||||
AddPackage(EditCardPackDto? cardPackDto, this.adminApi)
|
|
||||||
: cardPackDto = cardPackDto ?? EditCardPackDto.fromJson({});
|
|
||||||
|
|
||||||
void _deletePack() async {
|
|
||||||
final delete = await showDialog(
|
|
||||||
context: scaffoldKey.currentContext!,
|
|
||||||
builder: (c) => Center(
|
|
||||||
child: Container(
|
|
||||||
color: Colors.white,
|
|
||||||
width: 200,
|
|
||||||
child: IconButton(
|
|
||||||
icon: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Text('Удалить '),
|
|
||||||
Icon(Icons.delete),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(c).pop(true);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
));
|
|
||||||
if (!delete) return;
|
|
||||||
final result = await adminApi.deletePack(cardPackDto);
|
|
||||||
if (!result) {
|
|
||||||
ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar(
|
|
||||||
content: Text('ERROR'),
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar(
|
|
||||||
content: Text('Success!!!'),
|
|
||||||
));
|
|
||||||
appRouter.maybePop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _savePack() async {
|
|
||||||
final result = await adminApi.editPack(cardPackDto);
|
|
||||||
if (!result) {
|
|
||||||
ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar(
|
|
||||||
content: Text('ERROR'),
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar(
|
|
||||||
content: Text('Success!!!'),
|
|
||||||
));
|
|
||||||
appRouter.maybePop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void addPack(BuildContext context) {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (c) {
|
|
||||||
return Center(
|
|
||||||
child: Card(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: ListView(
|
|
||||||
children: [
|
|
||||||
Header(
|
|
||||||
'${cardPackDto.id}',
|
|
||||||
hasPopButton: true,
|
|
||||||
trail: Row(
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
onPressed: _savePack,
|
|
||||||
icon: Icon(Icons.save),
|
|
||||||
),
|
|
||||||
if (cardPackDto.id != null)
|
|
||||||
GestureDetector(
|
|
||||||
onLongPress: _deletePack,
|
|
||||||
child: Icon(Icons.delete),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextParam(
|
|
||||||
'Title',
|
|
||||||
cardPackDto.title,
|
|
||||||
(v) {
|
|
||||||
cardPackDto = cardPackDto.copyWith.title(v);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
TextParam(
|
|
||||||
'Subtitle',
|
|
||||||
cardPackDto.subtitle,
|
|
||||||
(v) {
|
|
||||||
cardPackDto = cardPackDto.copyWith.subtitle(v);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
TextParam(
|
|
||||||
'Description',
|
|
||||||
cardPackDto.description,
|
|
||||||
(v) => cardPackDto = cardPackDto.copyWith.description(v),
|
|
||||||
),
|
|
||||||
TextParam(
|
|
||||||
'Size',
|
|
||||||
cardPackDto.size?.toString(),
|
|
||||||
(v) {
|
|
||||||
cardPackDto = cardPackDto.copyWith.size(
|
|
||||||
int.tryParse(v),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
TextParam(
|
|
||||||
'Google play id',
|
|
||||||
cardPackDto.googlePlayId,
|
|
||||||
(v) {
|
|
||||||
cardPackDto = cardPackDto.copyWith.googlePlayId(v);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
TextParam(
|
|
||||||
'Rustore id',
|
|
||||||
cardPackDto.rustoreId,
|
|
||||||
(v) {
|
|
||||||
cardPackDto = cardPackDto.copyWith.rustoreId(v);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
TextParam(
|
|
||||||
'Appstore id',
|
|
||||||
cardPackDto.appStoreId,
|
|
||||||
(v) {
|
|
||||||
cardPackDto = cardPackDto.copyWith.appStoreId(v);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
TextParam(
|
|
||||||
'Price',
|
|
||||||
cardPackDto.price,
|
|
||||||
(v) {
|
|
||||||
cardPackDto = cardPackDto.copyWith.price(v);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(child: Text('Enabled ')),
|
|
||||||
StatefulBuilder(builder: (context, setState) {
|
|
||||||
return Switch(
|
|
||||||
value: cardPackDto.enabled ?? false,
|
|
||||||
onChanged: (v) {
|
|
||||||
setState(() {
|
|
||||||
cardPackDto = cardPackDto.copyWith.enabled(v);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
StatefulBuilder(builder: (context, setState) {
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(child: Text('Поменять цвет ')),
|
|
||||||
Switch(
|
|
||||||
value: palleteVisible,
|
|
||||||
onChanged: (v) => setState(() {
|
|
||||||
palleteVisible = !palleteVisible;
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (palleteVisible)
|
|
||||||
ColorPicker(
|
|
||||||
pickerAreaHeightPercent: 0.5,
|
|
||||||
paletteType: PaletteType.hsvWithHue,
|
|
||||||
pickerColor: cardPackDto.color?.asColor ??
|
|
||||||
Colors.transparent,
|
|
||||||
onColorChanged: (color) {
|
|
||||||
String? colorString;
|
|
||||||
if (color.alpha != 0) {
|
|
||||||
colorString = '#${color.toHexString()}';
|
|
||||||
}
|
|
||||||
cardPackDto =
|
|
||||||
cardPackDto.copyWith.color(colorString);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
StatefulBuilder(builder: (context, setState) {
|
|
||||||
return SizedBox(
|
|
||||||
width: MediaQuery.of(context).size.width - 20,
|
|
||||||
height: MediaQuery.of(context).size.width - 20,
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () async {
|
|
||||||
FilePickerResult? result =
|
|
||||||
await FilePicker.platform.pickFiles();
|
|
||||||
if (result != null && result.files.isNotEmpty) {
|
|
||||||
final file = result.files.first;
|
|
||||||
final encoded = base64.normalize(
|
|
||||||
base64.encode(
|
|
||||||
File(file.path!).readAsBytesSync(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
cardPackDto = cardPackDto.copyWith.cover(
|
|
||||||
encoded,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
child: cardPackDto.cover == null
|
|
||||||
? Container(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
color: Colors.white,
|
|
||||||
child: Icon(
|
|
||||||
Icons.image,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Stack(
|
|
||||||
children: [
|
|
||||||
ImageFade(
|
|
||||||
key: ValueKey(cardPackDto.id),
|
|
||||||
image: MemoryImage(
|
|
||||||
base64.decode(cardPackDto.cover!)),
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
syncDuration: Duration.zero,
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
top: 8.0,
|
|
||||||
right: 8.0,
|
|
||||||
child: IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
cardPackDto = cardPackDto.copyWith(
|
|
||||||
cover: null,
|
|
||||||
);
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
icon: Icon(Icons.delete)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
TextParam(
|
|
||||||
'Preview cards',
|
|
||||||
cardPackDto.previewCards?.toString(),
|
|
||||||
(v) {
|
|
||||||
final cards = v
|
|
||||||
.decodeList<int>()
|
|
||||||
.map((e) => e.toString())
|
|
||||||
.toList();
|
|
||||||
cardPackDto = cardPackDto.copyWith.previewCards(cards);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
TextParam(
|
|
||||||
'Order (lower is higher)',
|
|
||||||
cardPackDto.order?.toString(),
|
|
||||||
(v) {
|
|
||||||
cardPackDto = cardPackDto.copyWith.order(
|
|
||||||
int.tryParse(v),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
TextParam(
|
|
||||||
'Cards order (ids)',
|
|
||||||
cardPackDto.cardsOrder?.join(','),
|
|
||||||
(v) {
|
|
||||||
final ids = v
|
|
||||||
.split(',')
|
|
||||||
.map((s) => int.tryParse(s.trim()))
|
|
||||||
.whereNotNull()
|
|
||||||
.toList();
|
|
||||||
cardPackDto = cardPackDto.copyWith(cardsOrder: ids);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,389 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:mnemo_cards/admin/admin_api.dart';
|
|
||||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
|
||||||
import 'package:mnemo_cards_frontend_common/mnemo_cards_frontend_common.dart';
|
|
||||||
|
|
||||||
import '../di/injector.dart';
|
|
||||||
import '../main.dart';
|
|
||||||
import 'text_param.dart';
|
|
||||||
|
|
||||||
class AddPlan {
|
|
||||||
List<SubscriptionPlanAdminDto> allPlans = [];
|
|
||||||
List<SubscriptionFeatureEnum> selectedFeatures = [];
|
|
||||||
late SubscriptionPlanAdminDto planDto;
|
|
||||||
late AdminApi adminApi;
|
|
||||||
|
|
||||||
final availableFeatures = [
|
|
||||||
SubscriptionFeatureEnum.ads,
|
|
||||||
SubscriptionFeatureEnum.packs,
|
|
||||||
];
|
|
||||||
|
|
||||||
AddPlan() {
|
|
||||||
this.adminApi = getIt.get<AdminApi>();
|
|
||||||
_init(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _init(SubscriptionPlanAdminDto? dto) {
|
|
||||||
planDto = dto ??
|
|
||||||
const SubscriptionPlanAdminDto(
|
|
||||||
ui: SubscriptionPlanUIDto(
|
|
||||||
currency: 'руб',
|
|
||||||
),
|
|
||||||
price: '123',
|
|
||||||
currency: 'руб',
|
|
||||||
durationDays: 0,
|
|
||||||
features: [
|
|
||||||
SubscriptionFeatureEnum.ads,
|
|
||||||
SubscriptionFeatureEnum.packs,
|
|
||||||
],
|
|
||||||
paymentId: null,
|
|
||||||
paymentSystem: PaymentSystem.unknown,
|
|
||||||
);
|
|
||||||
selectedFeatures = planDto.features.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> _loadPlans() async {
|
|
||||||
final result = await adminApi.getSubscriptionPlans();
|
|
||||||
allPlans = result;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _saveSubscriptionPlan() async {
|
|
||||||
final result = await adminApi.addSubscriptionPlan(
|
|
||||||
planDto.copyWith(
|
|
||||||
features: selectedFeatures,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (!result) {
|
|
||||||
ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar(
|
|
||||||
content: Text('ERROR'),
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar(
|
|
||||||
content: Text('Success!!!'),
|
|
||||||
));
|
|
||||||
appRouter.maybePop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _deletePLan() async {
|
|
||||||
if (planDto.id == null) return;
|
|
||||||
final result = await adminApi.deleteSubscriptionPlan(planDto.id.toString());
|
|
||||||
if (!result) {
|
|
||||||
ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar(
|
|
||||||
content: Text('ERROR'),
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar(
|
|
||||||
content: Text('Success!!!'),
|
|
||||||
));
|
|
||||||
appRouter.maybePop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void addPlan(BuildContext context) {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (c) {
|
|
||||||
return FutureBuilder(
|
|
||||||
future: _loadPlans(),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (snapshot.data != true) {
|
|
||||||
return Center(
|
|
||||||
child: Material(
|
|
||||||
child: Center(child: CircularProgressIndicator()),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Center(
|
|
||||||
child: Scaffold(
|
|
||||||
body: Material(
|
|
||||||
child: StatefulBuilder(builder: (context, setPlan) {
|
|
||||||
return ListView(
|
|
||||||
children: [
|
|
||||||
Header(
|
|
||||||
'${planDto.id}',
|
|
||||||
hasPopButton: true,
|
|
||||||
trail: Row(
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
onPressed: () async {
|
|
||||||
await _loadPlans();
|
|
||||||
setPlan(() {});
|
|
||||||
},
|
|
||||||
icon: Icon(Icons.sync),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
onPressed: _saveSubscriptionPlan,
|
|
||||||
icon: Icon(Icons.save),
|
|
||||||
),
|
|
||||||
if (planDto.id != null)
|
|
||||||
IconButton(
|
|
||||||
onPressed: () async {
|
|
||||||
planDto = planDto.copyWith(id: null);
|
|
||||||
_saveSubscriptionPlan();
|
|
||||||
},
|
|
||||||
icon: Icon(Icons.copy),
|
|
||||||
),
|
|
||||||
if (planDto.id != null)
|
|
||||||
GestureDetector(
|
|
||||||
onLongPress: _deletePLan,
|
|
||||||
child: Icon(Icons.delete),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
DropdownMenu<String?>(
|
|
||||||
initialSelection: null,
|
|
||||||
onSelected: (String? value) {
|
|
||||||
setPlan(() {
|
|
||||||
_init(
|
|
||||||
allPlans
|
|
||||||
.firstWhereOrNull((v) => v.id == value),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
dropdownMenuEntries: [
|
|
||||||
...allPlans,
|
|
||||||
null,
|
|
||||||
].map((plan) {
|
|
||||||
return DropdownMenuEntry(
|
|
||||||
value: plan?.id,
|
|
||||||
label: plan == null
|
|
||||||
? 'New'
|
|
||||||
: '${plan.ui.titleLead ?? ''} ${plan.ui.title ?? ''} ${plan.paymentSystem.name}',
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
StatefulBuilder(builder: (context, setState) {
|
|
||||||
return Stack(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.all(8.0),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
mainAxisAlignment:
|
|
||||||
MainAxisAlignment.spaceEvenly,
|
|
||||||
children: [
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
TextParam(
|
|
||||||
'Currency',
|
|
||||||
planDto.currency,
|
|
||||||
(v) {
|
|
||||||
planDto = planDto.copyWith(
|
|
||||||
currency: v);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
TextParam(
|
|
||||||
'Price',
|
|
||||||
planDto.price,
|
|
||||||
(v) {
|
|
||||||
planDto =
|
|
||||||
planDto.copyWith(price: v);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
TextParam(
|
|
||||||
'Days',
|
|
||||||
planDto.durationDays.toString(),
|
|
||||||
(v) {
|
|
||||||
final days = int.parse(v);
|
|
||||||
planDto = planDto.copyWith(
|
|
||||||
durationDays: days,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Text('Features:'),
|
|
||||||
ListView.builder(
|
|
||||||
shrinkWrap: true,
|
|
||||||
itemCount:
|
|
||||||
availableFeatures.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final item =
|
|
||||||
availableFeatures[index];
|
|
||||||
return ListTile(
|
|
||||||
title: Text(item.name),
|
|
||||||
onTap: () {
|
|
||||||
setState(() {
|
|
||||||
if (selectedFeatures
|
|
||||||
.contains(item)) {
|
|
||||||
selectedFeatures
|
|
||||||
.remove(item);
|
|
||||||
} else {
|
|
||||||
selectedFeatures
|
|
||||||
.add(item);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
selected: selectedFeatures
|
|
||||||
.contains(item),
|
|
||||||
selectedTileColor: Colors.blue
|
|
||||||
.withOpacity(0.5),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Text('UI:'),
|
|
||||||
TextParam(
|
|
||||||
'UI Currency',
|
|
||||||
planDto.ui.currency,
|
|
||||||
(v) {
|
|
||||||
final ui = planDto.ui.copyWith(
|
|
||||||
currency: v,
|
|
||||||
);
|
|
||||||
planDto =
|
|
||||||
planDto.copyWith.ui(ui);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
TextParam(
|
|
||||||
'UI Title lead',
|
|
||||||
planDto.ui.titleLead,
|
|
||||||
(v) {
|
|
||||||
final ui = planDto.ui.copyWith(
|
|
||||||
titleLead: v,
|
|
||||||
);
|
|
||||||
planDto =
|
|
||||||
planDto.copyWith.ui(ui);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
TextParam(
|
|
||||||
'UI Title',
|
|
||||||
planDto.ui.title,
|
|
||||||
(v) {
|
|
||||||
final ui = planDto.ui.copyWith(
|
|
||||||
title: v,
|
|
||||||
);
|
|
||||||
planDto =
|
|
||||||
planDto.copyWith.ui(ui);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
TextParam(
|
|
||||||
'UI Price per month',
|
|
||||||
planDto.ui.pricePerMonth,
|
|
||||||
(v) {
|
|
||||||
final ui = planDto.ui.copyWith(
|
|
||||||
pricePerMonth: v,
|
|
||||||
);
|
|
||||||
planDto =
|
|
||||||
planDto.copyWith.ui(ui);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
TextParam(
|
|
||||||
'UI Subtitle crossed',
|
|
||||||
planDto.ui.subtitleCrossed,
|
|
||||||
(v) {
|
|
||||||
final ui = planDto.ui.copyWith(
|
|
||||||
subtitleCrossed: v,
|
|
||||||
);
|
|
||||||
planDto =
|
|
||||||
planDto.copyWith.ui(ui);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
TextParam(
|
|
||||||
'UI Subtitle',
|
|
||||||
planDto.ui.subtitle,
|
|
||||||
(v) {
|
|
||||||
final ui = planDto.ui.copyWith(
|
|
||||||
subtitle: v,
|
|
||||||
);
|
|
||||||
planDto =
|
|
||||||
planDto.copyWith.ui(ui);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
TextParam(
|
|
||||||
'UI Tip',
|
|
||||||
planDto.ui.tip,
|
|
||||||
(v) {
|
|
||||||
final ui = planDto.ui.copyWith(
|
|
||||||
tip: v,
|
|
||||||
);
|
|
||||||
planDto =
|
|
||||||
planDto.copyWith.ui(ui);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
TextParam(
|
|
||||||
'UI Tip color',
|
|
||||||
planDto.ui.tipColor,
|
|
||||||
(v) {
|
|
||||||
final ui = planDto.ui.copyWith(
|
|
||||||
tipColor: v,
|
|
||||||
);
|
|
||||||
planDto =
|
|
||||||
planDto.copyWith.ui(ui);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.all(8.0),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Text('Payment type '),
|
|
||||||
Expanded(
|
|
||||||
child: StatefulBuilder(
|
|
||||||
builder: (context,
|
|
||||||
setState) {
|
|
||||||
return DropdownButton<
|
|
||||||
PaymentSystem>(
|
|
||||||
value: planDto
|
|
||||||
.paymentSystem,
|
|
||||||
items:
|
|
||||||
PaymentSystem.values
|
|
||||||
.map(
|
|
||||||
(v) =>
|
|
||||||
DropdownMenuItem(
|
|
||||||
child: Text(
|
|
||||||
v.name),
|
|
||||||
value: v,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
onChanged: (v) {
|
|
||||||
setState(() {
|
|
||||||
planDto = planDto
|
|
||||||
.copyWith(
|
|
||||||
paymentSystem: v ??
|
|
||||||
PaymentSystem
|
|
||||||
.unknown,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextParam(
|
|
||||||
'Payment id',
|
|
||||||
planDto.paymentId,
|
|
||||||
(v) {
|
|
||||||
planDto = planDto.copyWith(
|
|
||||||
paymentId: v,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget uiEditor() {
|
|
||||||
return Column(
|
|
||||||
children: [],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,399 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:developer';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
|
|
||||||
import 'package:mnemo_cards/admin/admin_api.dart';
|
|
||||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
|
||||||
import 'package:mnemo_cards_frontend_common/mnemo_cards_frontend_common.dart';
|
|
||||||
|
|
||||||
import '../di/injector.dart';
|
|
||||||
import '../main.dart';
|
|
||||||
import 'date_param.dart';
|
|
||||||
import 'enum_param.dart';
|
|
||||||
import 'int_param.dart';
|
|
||||||
import 'text_param.dart';
|
|
||||||
|
|
||||||
class AddPromocodeCampaign {
|
|
||||||
late PromoCodesCampaignDto campaignDto;
|
|
||||||
late AdminApi adminApi;
|
|
||||||
List<PromoCodesCampaignDto> _campaigns = [];
|
|
||||||
|
|
||||||
final availableStatuses = [
|
|
||||||
PromoCodeCampaignStatus.created,
|
|
||||||
PromoCodeCampaignStatus.preparing,
|
|
||||||
PromoCodeCampaignStatus.ready,
|
|
||||||
PromoCodeCampaignStatus.active,
|
|
||||||
PromoCodeCampaignStatus.disabled,
|
|
||||||
];
|
|
||||||
|
|
||||||
AddPromocodeCampaign() {
|
|
||||||
this.adminApi = getIt.get<AdminApi>();
|
|
||||||
_init(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _init(PromoCodesCampaignDto? dto) {
|
|
||||||
final now = DateTime.now();
|
|
||||||
campaignDto = dto ??
|
|
||||||
PromoCodesCampaignDto(
|
|
||||||
products: [],
|
|
||||||
activationsPerCode: 1,
|
|
||||||
activationsPerUser: 1,
|
|
||||||
generationSize: 100,
|
|
||||||
start: now,
|
|
||||||
finish: now.add(Duration(days: 60)),
|
|
||||||
status: PromoCodeCampaignStatus.disabled,
|
|
||||||
name: null,
|
|
||||||
template: r'$s$s$s$s$s$s$s$s',
|
|
||||||
tags: [],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> _loadCampaigns() async {
|
|
||||||
try {
|
|
||||||
_campaigns = await adminApi.getPromoCodeCampaigns();
|
|
||||||
} catch (e, s) {
|
|
||||||
log('Error', error: e, stackTrace: s);
|
|
||||||
print('Error $e');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> _loadCampaign(String id) async {
|
|
||||||
try {
|
|
||||||
final campaign = await adminApi.getPromoCodeCampaign(id);
|
|
||||||
if (campaign != null) {
|
|
||||||
_campaigns.removeWhere((v) => v.id.toString() == id);
|
|
||||||
_campaigns.add(campaign);
|
|
||||||
campaignDto = campaign;
|
|
||||||
}
|
|
||||||
} catch (e, s) {
|
|
||||||
log('Error', error: e, stackTrace: s);
|
|
||||||
print('Error $e');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _savePromoCodeCampaign() async {
|
|
||||||
final result = await adminApi.addPromoCodeCampaign(campaignDto);
|
|
||||||
if (!result) {
|
|
||||||
ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar(
|
|
||||||
content: Text('ERROR'),
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar(
|
|
||||||
content: Text('Success!!!'),
|
|
||||||
));
|
|
||||||
appRouter.maybePop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _deletePromoCodeCampaign() async {
|
|
||||||
if (campaignDto.id == null) return;
|
|
||||||
final result =
|
|
||||||
await adminApi.deletePromoCodeCampaign(campaignDto.id.toString());
|
|
||||||
if (!result) {
|
|
||||||
ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar(
|
|
||||||
content: Text('ERROR'),
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar(
|
|
||||||
content: Text('Success!!!'),
|
|
||||||
));
|
|
||||||
appRouter.maybePop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void addCampaign(BuildContext context) {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (c) {
|
|
||||||
return FutureBuilder(
|
|
||||||
future: _loadCampaigns(),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (snapshot.data != true) {
|
|
||||||
return Center(
|
|
||||||
child: Material(
|
|
||||||
child: Center(child: CircularProgressIndicator()),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Center(
|
|
||||||
child: Scaffold(
|
|
||||||
body: Material(
|
|
||||||
child: StatefulBuilder(builder: (context, setCampaign) {
|
|
||||||
final codes = campaignDto.promoCodes;
|
|
||||||
|
|
||||||
return ListView(
|
|
||||||
children: [
|
|
||||||
Header(
|
|
||||||
'${campaignDto.id}',
|
|
||||||
hasPopButton: true,
|
|
||||||
trail: Row(
|
|
||||||
children: [
|
|
||||||
if (campaignDto.id != null)
|
|
||||||
IconButton(
|
|
||||||
onPressed: () async {
|
|
||||||
await _loadCampaign(
|
|
||||||
campaignDto.id.toString(),
|
|
||||||
);
|
|
||||||
setCampaign(() {});
|
|
||||||
},
|
|
||||||
icon: Icon(Icons.note),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
onPressed: _savePromoCodeCampaign,
|
|
||||||
icon: Icon(Icons.save),
|
|
||||||
),
|
|
||||||
if (campaignDto.id != null)
|
|
||||||
IconButton(
|
|
||||||
onPressed: () async {
|
|
||||||
campaignDto =
|
|
||||||
campaignDto.copyWith(id: null);
|
|
||||||
_savePromoCodeCampaign();
|
|
||||||
},
|
|
||||||
icon: Icon(Icons.copy),
|
|
||||||
),
|
|
||||||
if (campaignDto.id != null)
|
|
||||||
GestureDetector(
|
|
||||||
onLongPress: _deletePromoCodeCampaign,
|
|
||||||
child: Icon(Icons.delete),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
DropdownMenu<int?>(
|
|
||||||
initialSelection: null,
|
|
||||||
onSelected: (int? value) {
|
|
||||||
setCampaign(() {
|
|
||||||
_init(
|
|
||||||
_campaigns
|
|
||||||
.firstWhereOrNull((v) => v.id == value),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
dropdownMenuEntries: [
|
|
||||||
..._campaigns,
|
|
||||||
null,
|
|
||||||
].map((campaign) {
|
|
||||||
return DropdownMenuEntry(
|
|
||||||
value: campaign?.id,
|
|
||||||
label: campaign == null
|
|
||||||
? 'New'
|
|
||||||
: '${campaign.name ?? campaign.id} ${campaign.status.name}',
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
StatefulBuilder(builder: (context, setState) {
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.all(8.0),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
TextParam(
|
|
||||||
'Name',
|
|
||||||
campaignDto.name,
|
|
||||||
(v) {
|
|
||||||
campaignDto = campaignDto.copyWith(
|
|
||||||
name: v,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
TextParam(
|
|
||||||
'Template',
|
|
||||||
campaignDto.template,
|
|
||||||
(v) {
|
|
||||||
campaignDto = campaignDto.copyWith(
|
|
||||||
template: v,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
description:
|
|
||||||
r'$d - digit $l - letter $s - any',
|
|
||||||
),
|
|
||||||
IntParam(
|
|
||||||
'Generation size',
|
|
||||||
campaignDto.generationSize,
|
|
||||||
(v) {
|
|
||||||
campaignDto = campaignDto.copyWith(
|
|
||||||
generationSize: v);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
IntParam(
|
|
||||||
'Activations per code',
|
|
||||||
campaignDto.activationsPerCode,
|
|
||||||
(v) {
|
|
||||||
campaignDto = campaignDto.copyWith(
|
|
||||||
activationsPerCode: v,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
IntParam(
|
|
||||||
'Activations per user',
|
|
||||||
campaignDto.activationsPerUser,
|
|
||||||
(v) {
|
|
||||||
campaignDto = campaignDto.copyWith(
|
|
||||||
activationsPerUser: v,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
DateParam(
|
|
||||||
'Start',
|
|
||||||
campaignDto.start,
|
|
||||||
(v) {
|
|
||||||
setState(() {
|
|
||||||
campaignDto =
|
|
||||||
campaignDto.copyWith(start: v);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
DateParam(
|
|
||||||
'Finish',
|
|
||||||
campaignDto.finish,
|
|
||||||
(v) {
|
|
||||||
setState(() {
|
|
||||||
campaignDto =
|
|
||||||
campaignDto.copyWith(finish: v);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
EnumParam(
|
|
||||||
'Status',
|
|
||||||
PromoCodeCampaignStatus.values,
|
|
||||||
campaignDto.status, (v) {
|
|
||||||
setState(() {
|
|
||||||
campaignDto =
|
|
||||||
campaignDto.copyWith(status: v);
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
Text(
|
|
||||||
'Created to generate promocodes\n'
|
|
||||||
'Disabled to disable\n'
|
|
||||||
'Dont use other statuses!',
|
|
||||||
style: TextStyle(fontSize: 12),
|
|
||||||
),
|
|
||||||
Text('Products:'),
|
|
||||||
ListView.builder(
|
|
||||||
shrinkWrap: true,
|
|
||||||
itemCount:
|
|
||||||
campaignDto.products.length + 1,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
if (index ==
|
|
||||||
campaignDto.products.length) {
|
|
||||||
return ListTile(
|
|
||||||
title: Text('Add'),
|
|
||||||
onTap: () async {
|
|
||||||
final p =
|
|
||||||
await _showProductDialog(
|
|
||||||
context, null);
|
|
||||||
if (p != null) {
|
|
||||||
setState(() {
|
|
||||||
campaignDto.products.add(p);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
final item =
|
|
||||||
campaignDto.products[index];
|
|
||||||
return ListTile(
|
|
||||||
title: Text(
|
|
||||||
'${item.type.name} ${item.id ?? ''}'),
|
|
||||||
onTap: () async {
|
|
||||||
final p = await _showProductDialog(
|
|
||||||
context,
|
|
||||||
item,
|
|
||||||
);
|
|
||||||
if (p != null) {
|
|
||||||
setState(() {
|
|
||||||
campaignDto.products.add(p);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
trailing: IconButton(
|
|
||||||
icon: Icon(Icons.delete),
|
|
||||||
onPressed: () {
|
|
||||||
setState(() {
|
|
||||||
campaignDto.products
|
|
||||||
.remove(item);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
SimpleTile.text(
|
|
||||||
text: codes == null
|
|
||||||
? 'Codes not loaded'
|
|
||||||
: 'Codes: ${codes.length == 1 ? codes.first : codes.length}',
|
|
||||||
onTap: () async {
|
|
||||||
if (codes != null) {
|
|
||||||
await Clipboard.setData(
|
|
||||||
ClipboardData(
|
|
||||||
text: codes.join('\n'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<MnemoCardsProductDto?> _showProductDialog(
|
|
||||||
BuildContext context,
|
|
||||||
MnemoCardsProductDto? initialProduct,
|
|
||||||
) async {
|
|
||||||
var prod = initialProduct ?? MnemoCardsProductDto();
|
|
||||||
final result = await showDialog<MnemoCardsProductDto?>(
|
|
||||||
context: context,
|
|
||||||
builder: (c) => Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Material(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
TextParam('Id', prod.id, (v) {
|
|
||||||
prod = prod.copyWith(id: v);
|
|
||||||
}),
|
|
||||||
EnumParam(
|
|
||||||
'Type',
|
|
||||||
MnemoCardsProductType.values,
|
|
||||||
prod.type,
|
|
||||||
(v) {
|
|
||||||
prod = prod.copyWith(type: v);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
MaterialButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).pop(prod);
|
|
||||||
},
|
|
||||||
child: Text('Save'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget uiEditor() {
|
|
||||||
return Column(
|
|
||||||
children: [],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,228 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:mnemo_cards/admin/admin_api.dart';
|
|
||||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
|
||||||
import 'package:mnemo_cards_frontend_common/mnemo_cards_frontend_common.dart';
|
|
||||||
|
|
||||||
import '../main.dart';
|
|
||||||
|
|
||||||
class EditUser {
|
|
||||||
UserDto userDto;
|
|
||||||
AdminApi adminApi;
|
|
||||||
bool imageChanged = false;
|
|
||||||
|
|
||||||
EditUser(UserDto userDto, this.adminApi) : userDto = userDto;
|
|
||||||
|
|
||||||
void _saveUser() async {
|
|
||||||
final dto = userDto;
|
|
||||||
final result = await adminApi.editUser(dto);
|
|
||||||
if (!result) {
|
|
||||||
ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar(
|
|
||||||
content: Text('ERROR'),
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar(
|
|
||||||
content: Text('Success!!!'),
|
|
||||||
));
|
|
||||||
appRouter.maybePop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _deleteCard() async {
|
|
||||||
final result = await adminApi.deleteUser(userDto);
|
|
||||||
if (!result) {
|
|
||||||
ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar(
|
|
||||||
content: Text('ERROR'),
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar(
|
|
||||||
content: Text('Success!!!'),
|
|
||||||
));
|
|
||||||
appRouter.maybePop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void addUser(BuildContext context) {
|
|
||||||
final theme = Theme.of(context).textTheme;
|
|
||||||
|
|
||||||
final TextEditingController nameController =
|
|
||||||
TextEditingController(text: userDto.name);
|
|
||||||
|
|
||||||
final TextEditingController packsController =
|
|
||||||
TextEditingController(text: userDto.packs.join(','));
|
|
||||||
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (c) {
|
|
||||||
return Center(
|
|
||||||
child: Scaffold(
|
|
||||||
body: Material(
|
|
||||||
child: ListView(
|
|
||||||
children: [
|
|
||||||
Header(
|
|
||||||
'${userDto.id}',
|
|
||||||
hasPopButton: true,
|
|
||||||
trail: Row(
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
onPressed: _saveUser,
|
|
||||||
icon: Icon(Icons.save),
|
|
||||||
),
|
|
||||||
if (userDto.id != null && userDto.id! >= 0)
|
|
||||||
GestureDetector(
|
|
||||||
onLongPress: _deleteCard,
|
|
||||||
child: Icon(Icons.delete),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
StatefulBuilder(builder: (context, setState) {
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.all(8.0),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
if (userDto.id != null) {
|
|
||||||
Clipboard.setData(ClipboardData(
|
|
||||||
text: userDto.id.toString()));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Text('ID: ${userDto.id}'),
|
|
||||||
),
|
|
||||||
Text('Admin: ${userDto.admin}'),
|
|
||||||
TextField(
|
|
||||||
controller: nameController,
|
|
||||||
onChanged: (v) {
|
|
||||||
userDto = userDto.copyWith(name: v);
|
|
||||||
},
|
|
||||||
decoration: InputDecoration(hintText: 'Name'),
|
|
||||||
maxLines: 1,
|
|
||||||
style: theme.displayMedium?.copyWith(
|
|
||||||
fontSize: theme.displayMedium!.fontSize!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(child: Text('Subscription ')),
|
|
||||||
StatefulBuilder(builder: (context, setState) {
|
|
||||||
return Switch(
|
|
||||||
value: userDto.subscription ?? false,
|
|
||||||
onChanged: (v) {
|
|
||||||
setState(() {
|
|
||||||
userDto =
|
|
||||||
userDto.copyWith.subscription(v);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
TextField(
|
|
||||||
controller: packsController,
|
|
||||||
onChanged: (v) {
|
|
||||||
final packs = v
|
|
||||||
.split(',')
|
|
||||||
.map((s) =>
|
|
||||||
int.tryParse(s.trim())?.toString())
|
|
||||||
.whereNotNull()
|
|
||||||
.toList();
|
|
||||||
userDto = userDto.copyWith(packs: packs);
|
|
||||||
final packsText = packs.join(',');
|
|
||||||
if (!packsController.text
|
|
||||||
.startsWith(packsText)) {
|
|
||||||
packsController.text = packsText;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
decoration: InputDecoration(hintText: 'Packs'),
|
|
||||||
maxLines: 1,
|
|
||||||
style: theme.displayMedium?.copyWith(
|
|
||||||
fontSize: theme.displayMedium!.fontSize!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
_Payments(adminApi, userDto),
|
|
||||||
Text(
|
|
||||||
'Stats: ${userDto.userDataDto?.allWordsStatistics?.total} words',
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'Stats: ${userDto.userDataDto?.allTestsStatistics?.tests.length} tests',
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'Last test session: ${userDto.userDataDto?.allTestsStatistics?.lastSessionToken}',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _Payments extends StatelessWidget {
|
|
||||||
final AdminApi adminApi;
|
|
||||||
final UserDto userDto;
|
|
||||||
|
|
||||||
_Payments(this.adminApi, this.userDto);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final sc = StreamController<List<PaymentDto>?>();
|
|
||||||
final stream = sc.stream;
|
|
||||||
|
|
||||||
return ExpansionTile(
|
|
||||||
title: Text('Транзакции'),
|
|
||||||
maintainState: true,
|
|
||||||
onExpansionChanged: (v) async {
|
|
||||||
if (v) {
|
|
||||||
sc.add(null);
|
|
||||||
final payments =
|
|
||||||
await adminApi.getUserPurchases(userDto.id.toString());
|
|
||||||
sc.add(payments);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
StreamBuilder<List<PaymentDto>?>(
|
|
||||||
stream: stream,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
final payments = snapshot.data;
|
|
||||||
if (payments == null) {
|
|
||||||
return Text('Loading...');
|
|
||||||
}
|
|
||||||
if (payments.isEmpty) {
|
|
||||||
return Text('Нет покупок');
|
|
||||||
}
|
|
||||||
return Column(
|
|
||||||
children: payments.map((p) {
|
|
||||||
return ListTile(
|
|
||||||
title: Text('${p.amount} ${p.currency}'),
|
|
||||||
subtitle: Text(dateString(p.date)),
|
|
||||||
trailing: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
Text(p.status.name),
|
|
||||||
Text(p.paymentSystem.name),
|
|
||||||
Text(
|
|
||||||
p.products.map((p) => '${p.type}: ${p.id}').join('\n'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String dateString(DateTime date) =>
|
|
||||||
'${date.year}.${date.month.toString().padLeft(2, '0')}.${date.day.toString().padLeft(2, '0')} ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}:${date.second.toString().padLeft(2, '0')}';
|
|
||||||
}
|
|
||||||
|
|
@ -1,196 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:developer';
|
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:mnemo_cards/di/injector.dart';
|
|
||||||
import 'package:mnemo_cards/features/packs/packs_api.dart';
|
|
||||||
import 'package:mnemo_cards/features/worker/worker_ext.dart';
|
|
||||||
import 'package:mnemo_cards/managers/repository/api.dart';
|
|
||||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
|
||||||
|
|
||||||
class AdminApi with Api {
|
|
||||||
final Dio _dio;
|
|
||||||
|
|
||||||
AdminApi(this._dio);
|
|
||||||
|
|
||||||
Future<bool> addCard(
|
|
||||||
GameCardDto dto, {
|
|
||||||
ProgressCallback? progressCallback,
|
|
||||||
}) async {
|
|
||||||
final r = await _dio.post<String>(
|
|
||||||
'$path/cards/add',
|
|
||||||
data: jsonEncode(dto.toJson()),
|
|
||||||
options: Options(sendTimeout: Duration(seconds: 30)),
|
|
||||||
onSendProgress: (a, b) {
|
|
||||||
log('Card send progress $a/$b');
|
|
||||||
progressCallback?.call(a, b);
|
|
||||||
},
|
|
||||||
onReceiveProgress: (a, b) {
|
|
||||||
log('Card receive progress $a/$b');
|
|
||||||
progressCallback?.call(a, b);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return r.statusCode == 200;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> deleteCard(GameCardDto dto) async {
|
|
||||||
final r = await _dio.post<String>(
|
|
||||||
'$path/cards/delete',
|
|
||||||
data: jsonEncode({'id': dto.id.toString()}),
|
|
||||||
);
|
|
||||||
return r.statusCode == 200;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<String>> getCardIds() async {
|
|
||||||
final r = await _dio.get<String>('$path/cards/ids');
|
|
||||||
return (jsonDecode(r.data!)['ids'] as String).split(',').cast();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<GameCardDto>> getCards(List<String> ids) async {
|
|
||||||
final r = await _dio.get<String>(
|
|
||||||
'$path/cards',
|
|
||||||
queryParameters: {'ids': ids.join(',')},
|
|
||||||
);
|
|
||||||
return (r.data as String).decodeList(GameCardDto.fromJson);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<UserDto>> getUsers(List<String> ids) async {
|
|
||||||
final r = await _dio.get<String>(
|
|
||||||
'$path/users',
|
|
||||||
queryParameters: {'ids': ids.join(',')},
|
|
||||||
);
|
|
||||||
return (jsonDecode(r.data!) as List)
|
|
||||||
.map((e) => UserDto.fromJson(e as Map<String, dynamic>))
|
|
||||||
.toList()
|
|
||||||
.cast();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<String>> getUserIds() async {
|
|
||||||
final r = await _dio.get<String>('$path/users/ids');
|
|
||||||
return (jsonDecode(r.data!)['ids'] as String).split(',').cast();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<PaymentDto>> getUserPurchases(String userId) async {
|
|
||||||
final r = await _dio.get<String>(
|
|
||||||
'$path/user/purchases',
|
|
||||||
queryParameters: {'id': userId},
|
|
||||||
);
|
|
||||||
return (jsonDecode(r.data!)['payments'] as List)
|
|
||||||
.cast<Map<String, Object?>>()
|
|
||||||
.map(PaymentDto.fromJson)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> deleteUser(UserDto dto) async {
|
|
||||||
final r = await _dio.post<String>(
|
|
||||||
'$path/users/delete',
|
|
||||||
data: jsonEncode({'id': dto.id.toString()}),
|
|
||||||
);
|
|
||||||
return r.statusCode == 200;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> editUser(UserDto dto) async {
|
|
||||||
final r = await _dio.post<String>(
|
|
||||||
'$path/users/add',
|
|
||||||
data: jsonEncode(dto.toJson()),
|
|
||||||
);
|
|
||||||
return r.statusCode == 200;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<EditCardPackDto?> getEditPack(String id) async {
|
|
||||||
final r = await _dio.get<String>('$path/pack/edit/$id');
|
|
||||||
return (r.data as String).decode(EditCardPackDto.fromJson);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> deletePack(EditCardPackDto dto) async {
|
|
||||||
final r = await _dio.post<String>(
|
|
||||||
'$path/pack/delete',
|
|
||||||
data: jsonEncode({'id': dto.id.toString()}),
|
|
||||||
);
|
|
||||||
return r.statusCode == 200;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> editPack(EditCardPackDto dto) async {
|
|
||||||
final r = await _dio.post<String>(
|
|
||||||
'$path/pack/edit',
|
|
||||||
data: jsonEncode(dto.toJson()),
|
|
||||||
);
|
|
||||||
return r.statusCode == 200;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<CardPackPreviewDto>> packs() =>
|
|
||||||
getIt.get<PacksApi>().getPacksPreviews(null);
|
|
||||||
|
|
||||||
Future<List<SubscriptionPlanAdminDto>> getSubscriptionPlans() async {
|
|
||||||
final r = await _dio.get<String>('$path/subscriptions');
|
|
||||||
return (jsonDecode(r.data!)['plans'] as List)
|
|
||||||
.cast<Map<String, Object?>>()
|
|
||||||
.map(SubscriptionPlanAdminDto.fromJson)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> addSubscriptionPlan(SubscriptionPlanAdminDto dto) async {
|
|
||||||
final r = await _dio.post<String>(
|
|
||||||
'$path/subscription/add',
|
|
||||||
data: jsonEncode(dto.toJson()),
|
|
||||||
);
|
|
||||||
return r.statusCode == 200;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> deleteSubscriptionPlan(String id) async {
|
|
||||||
final r = await _dio.post<String>('$path/subscription/delete/$id');
|
|
||||||
return r.statusCode == 200;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> deletePromoCodeCampaign(String id) async {
|
|
||||||
final r = await _dio.post<String>('$path/promocode/delete/$id');
|
|
||||||
return r.statusCode == 200;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> addPromoCodeCampaign(PromoCodesCampaignDto dto) async {
|
|
||||||
final r = await _dio.post<String>(
|
|
||||||
'$path/promocode/add',
|
|
||||||
data: dto.encode(),
|
|
||||||
);
|
|
||||||
return r.statusCode == 200;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<PromoCodesCampaignDto>> getPromoCodeCampaigns() async {
|
|
||||||
final r = await _dio.get<String>('$path/promocode/list');
|
|
||||||
return (jsonDecode(r.data!)['campaigns'] as List)
|
|
||||||
.cast<Map<String, Object?>>()
|
|
||||||
.map(PromoCodesCampaignDto.fromJson)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<PromoCodesCampaignDto?> getPromoCodeCampaign(String id) async {
|
|
||||||
final r = await _dio.get<String>('$path/promocode/$id');
|
|
||||||
return r.data?.isolatedDecode(PromoCodesCampaignDto.fromJson);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> addDiscountCampaign(DiscountCampaignDto dto) async {
|
|
||||||
final r = await _dio.post<String>(
|
|
||||||
'$path/discounts/add',
|
|
||||||
data: dto.encode(),
|
|
||||||
);
|
|
||||||
return r.statusCode == 200;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<DiscountCampaignDto>> getDiscountCampaigns() async {
|
|
||||||
final r = await _dio.get<String>('$path/discounts/list');
|
|
||||||
return (jsonDecode(r.data!)['campaigns'] as List)
|
|
||||||
.cast<Map<String, Object?>>()
|
|
||||||
.map(DiscountCampaignDto.fromJson)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<String?> deleteDiscountCampaign(String id) async {
|
|
||||||
try {
|
|
||||||
final r = await _dio.post<String>('$path/discounts/delete/$id');
|
|
||||||
return (jsonDecode(r.data!)['error'] as String?);
|
|
||||||
} catch (e, s) {
|
|
||||||
return e.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,367 +0,0 @@
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
||||||
import 'package:mnemo_cards/admin/add_card.dart';
|
|
||||||
import 'package:mnemo_cards/admin/admin_api.dart';
|
|
||||||
import 'package:mnemo_cards/di/injector.dart';
|
|
||||||
import 'package:mnemo_cards/managers/repository/dio_provider.dart';
|
|
||||||
import 'package:mnemo_cards/theme/themes.dart';
|
|
||||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
|
||||||
import 'package:mnemo_cards_frontend_common/mnemo_cards_frontend_common.dart';
|
|
||||||
|
|
||||||
import '../main.dart';
|
|
||||||
import 'add_package.dart';
|
|
||||||
|
|
||||||
class AllCards extends StatefulWidget {
|
|
||||||
@override
|
|
||||||
State<StatefulWidget> createState() => _AllCardsState();
|
|
||||||
|
|
||||||
const AllCards();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AllCardsState extends State<AllCards> {
|
|
||||||
static List<String> ids = [];
|
|
||||||
static Set<GameCardDto> cards = {};
|
|
||||||
static String filter = '';
|
|
||||||
static List<CardPackPreviewDto> _packPreviews = [];
|
|
||||||
static EditCardPackDto? _currentPack;
|
|
||||||
static String? _currentId;
|
|
||||||
static List<String> _selectedCards = [];
|
|
||||||
static Map<String, String> _cache = {};
|
|
||||||
|
|
||||||
bool get selectingMode => _selectedCards.isNotEmpty;
|
|
||||||
|
|
||||||
Dio get dio => getIt.get<DioProvider>().dio;
|
|
||||||
|
|
||||||
AdminApi get api => getIt.get<AdminApi>();
|
|
||||||
|
|
||||||
Future<void> loadCards() async {
|
|
||||||
var loadingIds = ids.toList();
|
|
||||||
cards.clear();
|
|
||||||
_cache.clear();
|
|
||||||
while (loadingIds.isNotEmpty) {
|
|
||||||
try {
|
|
||||||
final ids = loadingIds.take(10).toList();
|
|
||||||
loadingIds.removeRange(0, ids.length);
|
|
||||||
final cardsPage = await api.getCards(ids);
|
|
||||||
for (final card in cardsPage) {
|
|
||||||
final imageString =
|
|
||||||
card.image?.substring(0, min(100, card.image!.length)) ?? '';
|
|
||||||
_cache[card.id.toString()] =
|
|
||||||
'${card.id}${card.mnemo}${card.transcription}${card.transcriptionMnemo}${card.original}${card.translation}${imageString}';
|
|
||||||
}
|
|
||||||
cards.addAll(cardsPage);
|
|
||||||
setState(() {});
|
|
||||||
} catch (e) {
|
|
||||||
print(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> loadIds() async {
|
|
||||||
ids = await api.getCardIds();
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> loadPacks() async {
|
|
||||||
_packPreviews = await api.packs();
|
|
||||||
clearCurrentPack();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> loadCurrentPack() async {
|
|
||||||
if (_currentId != null) {
|
|
||||||
_currentPack = await api.getEditPack(_currentId!);
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void clearCurrentPack() {
|
|
||||||
_currentId = null;
|
|
||||||
_currentPack = null;
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> editPack(EditCardPackDto pack) async {
|
|
||||||
final result = await api.editPack(pack);
|
|
||||||
if (!result) {
|
|
||||||
ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar(
|
|
||||||
content: Text('ERROR'),
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar(
|
|
||||||
content: Text('Success!!!'),
|
|
||||||
));
|
|
||||||
loadCurrentPack();
|
|
||||||
_selectedCards.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void addOrEditPack() => AddPackage(_currentPack, api).addPack(context);
|
|
||||||
|
|
||||||
List<GameCardDto> buildCards() {
|
|
||||||
final filteredCards = cards.where((dto) {
|
|
||||||
final filterOk =
|
|
||||||
filter.isEmpty || _cache[dto.id.toString()]?.contains(filter) == true;
|
|
||||||
final packFilter =
|
|
||||||
_currentPack?.addCardIds?.contains(dto.id.toString()) ?? true;
|
|
||||||
return filterOk && packFilter;
|
|
||||||
});
|
|
||||||
return filteredCards.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final filteredCards = buildCards();
|
|
||||||
return Scaffold(
|
|
||||||
body: SafeArea(
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
ListView.builder(
|
|
||||||
itemExtentBuilder: (index, dims) => index >= 1 ? 160.h : 350,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
if (index == 0)
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
Header(
|
|
||||||
_currentId == null
|
|
||||||
? 'Все карточки'
|
|
||||||
: _currentPack?.title ??
|
|
||||||
'Пак ${_currentPack?.id}',
|
|
||||||
subtitle: filteredCards.isEmpty
|
|
||||||
? 'Нет карточек'
|
|
||||||
: '${filteredCards.length} карточек',
|
|
||||||
hasPopButton: true,
|
|
||||||
trail: IconButton(
|
|
||||||
onPressed: () async {
|
|
||||||
setState(() {});
|
|
||||||
await loadIds();
|
|
||||||
await loadPacks();
|
|
||||||
await loadCards();
|
|
||||||
},
|
|
||||||
icon: Icon(Icons.sync)),
|
|
||||||
),
|
|
||||||
dropdownButton(context),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: TextField(
|
|
||||||
decoration: InputDecoration(hintText: 'Поиск'),
|
|
||||||
onChanged: (v) {
|
|
||||||
if (filter != v.toLowerCase()) {
|
|
||||||
filter = v.toLowerCase();
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () => EditCard(null, api).addCard(context),
|
|
||||||
child: Container(
|
|
||||||
color: Colors.green.withOpacity(0.1),
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: Icon(Icons.add, size: 30),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
if (filteredCards.length >= (index - 1) * 3) {
|
|
||||||
final cardIndex = (index - 1) * 3;
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
if (filteredCards.length > cardIndex)
|
|
||||||
cardWidget(filteredCards[cardIndex]),
|
|
||||||
if (filteredCards.length > cardIndex + 1)
|
|
||||||
cardWidget(filteredCards[cardIndex + 1]),
|
|
||||||
if (filteredCards.length > cardIndex + 2)
|
|
||||||
cardWidget(filteredCards[cardIndex + 2]),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}),
|
|
||||||
if (selectingMode)
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
child: Container(
|
|
||||||
height: 100,
|
|
||||||
padding: EdgeInsets.all(10.0),
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
_selectedCards.clear();
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
color: Colors.blue,
|
|
||||||
padding: EdgeInsets.all(8.0),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Text('${_selectedCards.length} '),
|
|
||||||
Icon(Icons.clear)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (_currentPack != null)
|
|
||||||
if (_selectedCards.every((c) =>
|
|
||||||
_currentPack!.addCardIds?.contains(c) ?? false))
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
SizedBox(width: 8.0),
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
final updated =
|
|
||||||
_currentPack!.copyWith.removeCardIds(
|
|
||||||
_selectedCards.toList(),
|
|
||||||
);
|
|
||||||
editPack(updated);
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
padding: EdgeInsets.all(8.0),
|
|
||||||
color: Colors.blue,
|
|
||||||
child: Text('Убрать из пака'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 8.0),
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
final updated =
|
|
||||||
_currentPack!.copyWith.previewCards(
|
|
||||||
_selectedCards.toList(),
|
|
||||||
);
|
|
||||||
editPack(updated);
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
padding: EdgeInsets.all(8.0),
|
|
||||||
color: Colors.blue,
|
|
||||||
child: Text('Как превью'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
else
|
|
||||||
MaterialButton(
|
|
||||||
onPressed: () {
|
|
||||||
final updated = _currentPack!.copyWith.addCardIds(
|
|
||||||
_selectedCards.toList(),
|
|
||||||
);
|
|
||||||
editPack(updated);
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
padding: EdgeInsets.all(8.0),
|
|
||||||
color: Colors.blue,
|
|
||||||
child: Text('Добавить в пак'),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget cardWidget(GameCardDto card) {
|
|
||||||
void toggleSelect() {
|
|
||||||
final id = card.id.toString();
|
|
||||||
if (_selectedCards.contains(id)) {
|
|
||||||
_selectedCards.remove(id);
|
|
||||||
} else {
|
|
||||||
_selectedCards.add(card.id.toString());
|
|
||||||
}
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
if (selectingMode) {
|
|
||||||
toggleSelect();
|
|
||||||
} else {
|
|
||||||
EditCard(card, api).addCard(context);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onLongPress: toggleSelect,
|
|
||||||
child: _Card(card, api, _selectedCards.contains(card.id.toString())),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onPackChanged(String? id) {
|
|
||||||
if (id == null) {
|
|
||||||
clearCurrentPack();
|
|
||||||
} else {
|
|
||||||
_currentId = id;
|
|
||||||
loadCurrentPack();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget dropdownButton(BuildContext context) {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: DropdownButton(
|
|
||||||
items: [..._packPreviews, null]
|
|
||||||
.map((p) => DropdownMenuItem<String?>(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: p == null
|
|
||||||
? Text('Все паки')
|
|
||||||
: Text('${p.title} ${p.cards}'),
|
|
||||||
),
|
|
||||||
value: p?.id,
|
|
||||||
))
|
|
||||||
.toList(),
|
|
||||||
value: _currentId,
|
|
||||||
onChanged: (v) => _onPackChanged(v),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
onPressed: addOrEditPack,
|
|
||||||
icon: _currentPack == null ? Icon(Icons.add) : Icon(Icons.edit),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _Card extends StatelessWidget {
|
|
||||||
final GameCardDto cardDto;
|
|
||||||
final AdminApi api;
|
|
||||||
final bool selected;
|
|
||||||
|
|
||||||
_Card(this.cardDto, this.api, this.selected);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final image = cardDto.image != null ? base64.decode(cardDto.image!) : null;
|
|
||||||
return Container(
|
|
||||||
padding: EdgeInsets.all(8.0),
|
|
||||||
width: MediaQuery.of(context).size.width / 3,
|
|
||||||
height: 160.h,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(color: selected ? Colors.blue : borderGray),
|
|
||||||
color: selected ? Colors.blue.withOpacity(0.2) : null,
|
|
||||||
),
|
|
||||||
child: Column(children: [
|
|
||||||
Text(cardDto.translation ?? ''),
|
|
||||||
Text(cardDto.original ?? ''),
|
|
||||||
if (image != null)
|
|
||||||
Expanded(
|
|
||||||
child: Image.memory(
|
|
||||||
key: ObjectKey(cardDto.image),
|
|
||||||
gaplessPlayback: true,
|
|
||||||
image,
|
|
||||||
fit: BoxFit.scaleDown,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,230 +0,0 @@
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:mnemo_cards/admin/add_user.dart';
|
|
||||||
import 'package:mnemo_cards/admin/admin_api.dart';
|
|
||||||
import 'package:mnemo_cards/di/injector.dart';
|
|
||||||
import 'package:mnemo_cards/di/locator.dart';
|
|
||||||
import 'package:mnemo_cards/managers/repository/dio_provider.dart';
|
|
||||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
|
||||||
import 'package:mnemo_cards_frontend_common/mnemo_cards_frontend_common.dart';
|
|
||||||
|
|
||||||
import '../main.dart';
|
|
||||||
|
|
||||||
class AllUsers extends StatefulWidget {
|
|
||||||
@override
|
|
||||||
State<StatefulWidget> createState() => _AllUsersState();
|
|
||||||
|
|
||||||
const AllUsers();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AllUsersState extends State<AllUsers> {
|
|
||||||
static List<String> ids = [];
|
|
||||||
static Set<UserDto> users = {};
|
|
||||||
static String filter = '';
|
|
||||||
static UserDto? _currentUser;
|
|
||||||
static String? _currentId;
|
|
||||||
|
|
||||||
bool get selectingMode => false; //_selectedCards.isNotEmpty;
|
|
||||||
|
|
||||||
Dio get dio => getIt.get<DioProvider>().dio;
|
|
||||||
|
|
||||||
AdminApi get api => getIt.get<AdminApi>();
|
|
||||||
|
|
||||||
Future<void> loadUsers() async {
|
|
||||||
var loadingIds = ids.toList();
|
|
||||||
users.clear();
|
|
||||||
while (loadingIds.isNotEmpty) {
|
|
||||||
try {
|
|
||||||
final ids = loadingIds.take(10).toList();
|
|
||||||
loadingIds.removeRange(0, ids.length);
|
|
||||||
final usersPage = await api.getUsers(ids);
|
|
||||||
users.addAll(usersPage);
|
|
||||||
setState(() {});
|
|
||||||
} catch (e) {
|
|
||||||
print(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> loadIds() async {
|
|
||||||
ids = await api.getUserIds();
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> loadCurrentUser() async {
|
|
||||||
if (_currentId != null) {
|
|
||||||
_currentUser = (await api.getUsers([_currentId!])).firstOrNull;
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void clearCurrentUser() {
|
|
||||||
_currentId = null;
|
|
||||||
_currentUser = null;
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> editUser(UserDto user) async {
|
|
||||||
final result = await api.editUser(user);
|
|
||||||
if (!result) {
|
|
||||||
ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar(
|
|
||||||
content: Text('ERROR'),
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar(
|
|
||||||
content: Text('Success!!!'),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<UserDto> buildUsersList() {
|
|
||||||
final filteredUsers = users.where((dto) {
|
|
||||||
final filterOk = filter.isEmpty ||
|
|
||||||
dto.name.toString().toLowerCase().contains(filter) ||
|
|
||||||
dto.id.toString().toLowerCase().contains(filter) ||
|
|
||||||
dto.packs.any((p) => p.toLowerCase().contains(filter));
|
|
||||||
return filterOk;
|
|
||||||
});
|
|
||||||
return filteredUsers.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final filteredUsers = buildUsersList();
|
|
||||||
return Scaffold(
|
|
||||||
body: SafeArea(
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
ListView.builder(itemBuilder: (context, index) {
|
|
||||||
if (index == 0)
|
|
||||||
return Container(
|
|
||||||
height: 270,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Header(
|
|
||||||
_currentId == null
|
|
||||||
? 'Все'
|
|
||||||
: _currentUser?.name ?? 'Пак ${_currentUser?.id}',
|
|
||||||
subtitle: filteredUsers.isEmpty
|
|
||||||
? 'Нет пользователей'
|
|
||||||
: '${filteredUsers.length} пользователя',
|
|
||||||
hasPopButton: true,
|
|
||||||
trail: IconButton(
|
|
||||||
onPressed: () async {
|
|
||||||
setState(() {});
|
|
||||||
await loadIds();
|
|
||||||
await loadUsers();
|
|
||||||
},
|
|
||||||
icon: Icon(Icons.sync)),
|
|
||||||
),
|
|
||||||
dropdownButton(context),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: TextField(
|
|
||||||
decoration: InputDecoration(hintText: 'Поиск'),
|
|
||||||
onChanged: (v) {
|
|
||||||
if (filter != v.toLowerCase()) {
|
|
||||||
filter = v.toLowerCase();
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// Expanded(
|
|
||||||
// child: GestureDetector(
|
|
||||||
// onTap: () => EditUser(null, api).addCard(context),
|
|
||||||
// child: Container(
|
|
||||||
// color: Colors.green.withOpacity(0.1),
|
|
||||||
// alignment: Alignment.center,
|
|
||||||
// child: Icon(Icons.add, size: 30),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
else if (filteredUsers.length > index - 1)
|
|
||||||
return userWidget(filteredUsers[index - 1]);
|
|
||||||
else
|
|
||||||
return null;
|
|
||||||
}),
|
|
||||||
if (selectingMode)
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
child: Container(
|
|
||||||
height: 100,
|
|
||||||
padding: EdgeInsets.all(10.0),
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
color: Colors.blue,
|
|
||||||
padding: EdgeInsets.all(8.0),
|
|
||||||
child: Row(
|
|
||||||
children: [Icon(Icons.clear)],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget userWidget(UserDto user) {
|
|
||||||
void toggleSelect() {
|
|
||||||
final id = user.id.toString();
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
if (selectingMode) {
|
|
||||||
toggleSelect();
|
|
||||||
} else {
|
|
||||||
EditUser(user, api).addUser(context);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onLongPress: toggleSelect,
|
|
||||||
child: _User(user, api, false),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget dropdownButton(BuildContext context) {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
onPressed: null,
|
|
||||||
icon: _currentUser == null ? Icon(Icons.add) : Icon(Icons.edit),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _User extends StatelessWidget {
|
|
||||||
final UserDto userDto;
|
|
||||||
final AdminApi api;
|
|
||||||
final bool selected;
|
|
||||||
|
|
||||||
_User(this.userDto, this.api, this.selected);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final isCurrent = locator.userManager.userStateHolder.user?.id == userDto.id;
|
|
||||||
return ListTile(
|
|
||||||
title: Text('${userDto.id}${isCurrent ? ' (you)' : ''}'),
|
|
||||||
subtitle: Text(userDto.name ?? ''),
|
|
||||||
trailing: userDto.admin ? Text('admin') : null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
|
||||||
|
|
||||||
class DateParam extends StatelessWidget {
|
|
||||||
final String title;
|
|
||||||
final DateTime? initial;
|
|
||||||
final Function(DateTime? v) onChanged;
|
|
||||||
|
|
||||||
DateParam(this.title, this.initial, this.onChanged);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(4.0),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Text(title),
|
|
||||||
SizedBox(
|
|
||||||
width: 8,
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Container(
|
|
||||||
padding: EdgeInsets.all(8.0),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(),
|
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
|
||||||
),
|
|
||||||
child: GestureDetector(
|
|
||||||
behavior: HitTestBehavior.opaque,
|
|
||||||
onTap: () async {
|
|
||||||
final date = await showDatePicker(
|
|
||||||
context: context,
|
|
||||||
initialEntryMode: DatePickerEntryMode.calendar,
|
|
||||||
initialDate: initial ?? DateTime.now(),
|
|
||||||
firstDate: DateTime.utc(2000),
|
|
||||||
lastDate: DateTime.utc(2050),
|
|
||||||
).then((selectedDate) {
|
|
||||||
// After selecting the date, display the time picker.
|
|
||||||
if (selectedDate != null) {
|
|
||||||
return showTimePicker(
|
|
||||||
context: context,
|
|
||||||
initialTime: TimeOfDay.now(),
|
|
||||||
).then((selectedTime) {
|
|
||||||
// Handle the selected date and time here.
|
|
||||||
if (selectedTime != null) {
|
|
||||||
DateTime selectedDateTime = DateTime(
|
|
||||||
selectedDate.year,
|
|
||||||
selectedDate.month,
|
|
||||||
selectedDate.day,
|
|
||||||
selectedTime.hour,
|
|
||||||
selectedTime.minute,
|
|
||||||
);
|
|
||||||
return selectedDateTime;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
onChanged(date);
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
initial?.let(
|
|
||||||
(d) =>
|
|
||||||
'${d.year}.${d.month}.${d.day} ${d.hour}:${d.minute}',
|
|
||||||
) ??
|
|
||||||
'No date',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
|
|
||||||
class EnumParam<T> extends StatelessWidget {
|
|
||||||
final String title;
|
|
||||||
final String? description;
|
|
||||||
final T initial;
|
|
||||||
final List<T> values;
|
|
||||||
final Function(T v) onChanged;
|
|
||||||
final Function(T v)? getName;
|
|
||||||
|
|
||||||
EnumParam(
|
|
||||||
this.title,
|
|
||||||
this.values,
|
|
||||||
this.initial,
|
|
||||||
this.onChanged, {
|
|
||||||
this.description,
|
|
||||||
this.getName,
|
|
||||||
});
|
|
||||||
|
|
||||||
String _defaultName(T v) => ((v is Enum) ? v.name : v.toString());
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
T value = initial;
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(title),
|
|
||||||
if (description != null)
|
|
||||||
Text(
|
|
||||||
description!,
|
|
||||||
style: TextStyle(fontSize: 12),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: 2,
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: StatefulBuilder(builder: (context, setState) {
|
|
||||||
return DropdownButton<T>(
|
|
||||||
value: value,
|
|
||||||
items: values
|
|
||||||
.map(
|
|
||||||
(v) => DropdownMenuItem(
|
|
||||||
child: Text(getName?.call(v) ?? _defaultName(v)),
|
|
||||||
value: v,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
onChanged: (v) => setState(() {
|
|
||||||
value = v!;
|
|
||||||
onChanged(v);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
|
|
||||||
class IntParam extends StatelessWidget {
|
|
||||||
final String title;
|
|
||||||
final int? initial;
|
|
||||||
final Function(int? v) onChanged;
|
|
||||||
|
|
||||||
IntParam(this.title, this.initial, this.onChanged);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Text(title+':'),
|
|
||||||
SizedBox(
|
|
||||||
width: 3,
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: TextField(
|
|
||||||
controller:
|
|
||||||
TextEditingController(text: initial?.toString() ?? ''),
|
|
||||||
onChanged: (v) => onChanged(int.tryParse(v)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
|
|
||||||
class TextParam extends StatelessWidget {
|
|
||||||
final String title;
|
|
||||||
final String? description;
|
|
||||||
final String? initial;
|
|
||||||
final Function(String v) onChanged;
|
|
||||||
|
|
||||||
TextParam(this.title, this.initial, this.onChanged, {this.description});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(title),
|
|
||||||
if (description != null)
|
|
||||||
Text(
|
|
||||||
description!,
|
|
||||||
style: TextStyle(fontSize: 12),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: 2,
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: TextField(
|
|
||||||
controller: TextEditingController(text: initial),
|
|
||||||
onChanged: onChanged,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 4 KiB After Width: | Height: | Size: 4 KiB |
|
|
@ -1,25 +0,0 @@
|
||||||
# syntax=docker/dockerfile:1.7
|
|
||||||
|
|
||||||
# --- Build stage ------------------------------------------------------------
|
|
||||||
FROM node:20-alpine AS build
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY package*.json ./
|
|
||||||
RUN npm ci
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
# --- Runtime stage ----------------------------------------------------------
|
|
||||||
FROM nginx:1.27-alpine AS runtime
|
|
||||||
WORKDIR /usr/share/nginx/html
|
|
||||||
|
|
||||||
# Replace default config with SPA-aware config
|
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
|
||||||
|
|
||||||
COPY --from=build /app/dist/ ./
|
|
||||||
|
|
||||||
EXPOSE 80
|
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
|
|
||||||
CMD wget -qO- http://127.0.0.1/ >/dev/null 2>&1 || exit 1
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
# HTTP server (SSL will be configured later)
|
|
||||||
# server {
|
|
||||||
# listen 80;
|
|
||||||
# server_name admin.mnemo-cards.online;
|
|
||||||
#
|
|
||||||
# # Redirect HTTP to HTTPS
|
|
||||||
# return 301 https://$server_name$request_uri;
|
|
||||||
# }
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name admin.mnemo-cards.online;
|
|
||||||
|
|
||||||
# Note: SSL certificate not yet configured - using HTTP for now
|
|
||||||
# TODO: Set up Let's Encrypt certificate for HTTPS
|
|
||||||
# ssl_certificate /etc/letsencrypt/live/admin.mnemo-cards.online/fullchain.pem;
|
|
||||||
# ssl_certificate_key /etc/letsencrypt/live/admin.mnemo-cards.online/privkey.pem;
|
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
|
||||||
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
|
|
||||||
ssl_prefer_server_ciphers off;
|
|
||||||
|
|
||||||
# Security headers (enhanced for admin panel)
|
|
||||||
add_header X-Frame-Options "DENY" always;
|
|
||||||
add_header X-Content-Type-Options "nosniff" always;
|
|
||||||
add_header X-XSS-Protection "1; mode=block" always;
|
|
||||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
||||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://api.mnemo-cards.online;" always;
|
|
||||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
|
||||||
|
|
||||||
# Root directory for admin panel
|
|
||||||
root /var/www/mnemo_cards_admin;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
# Gzip compression
|
|
||||||
gzip on;
|
|
||||||
gzip_vary on;
|
|
||||||
gzip_min_length 1024;
|
|
||||||
gzip_types text/plain text/css text/xml text/javascript application/javascript application/json;
|
|
||||||
|
|
||||||
# Main location block
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ /index.html;
|
|
||||||
|
|
||||||
# Cache static assets
|
|
||||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
|
||||||
expires 1h; # Shorter cache for admin panel
|
|
||||||
add_header Cache-Control "private, must-revalidate";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Security - deny access to hidden files and sensitive paths
|
|
||||||
location ~ /\. {
|
|
||||||
deny all;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ ^/admin/ {
|
|
||||||
deny all; # Prevent access to any /admin/ paths that might be exposed
|
|
||||||
}
|
|
||||||
|
|
||||||
# Rate limiting for admin panel
|
|
||||||
limit_req zone=admin burst=10 nodelay;
|
|
||||||
limit_req_status 429;
|
|
||||||
|
|
||||||
# Log admin access
|
|
||||||
access_log /var/log/nginx/admin_access.log;
|
|
||||||
error_log /var/log/nginx/admin_error.log;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Rate limiting zone for admin panel
|
|
||||||
limit_req_zone $binary_remote_addr zone=admin:10m rate=5r/m;
|
|
||||||
19
mnemo_cards_telegram_bot/.dockerignore
Normal file
19
mnemo_cards_telegram_bot/.dockerignore
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
.git
|
||||||
|
.gitmodules
|
||||||
|
.github
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
.cursor
|
||||||
|
build
|
||||||
|
.dart_tool
|
||||||
|
pubspec.lock
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
|
**/.DS_Store
|
||||||
|
test/
|
||||||
|
*.exe
|
||||||
|
*.dylib
|
||||||
|
*.so
|
||||||
|
zips/
|
||||||
|
backups/
|
||||||
|
apks/
|
||||||
61
mnemo_cards_telegram_bot/Dockerfile
Normal file
61
mnemo_cards_telegram_bot/Dockerfile
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
# syntax=docker/dockerfile:1.7
|
||||||
|
|
||||||
|
# --- Build stage ------------------------------------------------------------
|
||||||
|
# Note: This Dockerfile should be built from the parent directory:
|
||||||
|
# docker build -f mnemo_cards_telegram_bot/Dockerfile -t mnemo_cards_telegram_bot .
|
||||||
|
|
||||||
|
FROM dart:stable-sdk AS build
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy all local dependencies
|
||||||
|
COPY mnemo_cards_common /app/mnemo_cards_common
|
||||||
|
COPY mnemo_cards_common_backend /app/mnemo_cards_common_backend
|
||||||
|
COPY mnemo_cards_telegram_bot /app/mnemo_cards_telegram_bot
|
||||||
|
|
||||||
|
# Get dependencies (order matters due to path dependencies)
|
||||||
|
WORKDIR /app/mnemo_cards_common
|
||||||
|
RUN dart pub get
|
||||||
|
|
||||||
|
WORKDIR /app/mnemo_cards_common_backend
|
||||||
|
RUN dart pub get
|
||||||
|
|
||||||
|
WORKDIR /app/mnemo_cards_telegram_bot
|
||||||
|
RUN dart pub get
|
||||||
|
|
||||||
|
# Compile to native executable
|
||||||
|
RUN dart compile exe bin/main.dart -o /app/bot
|
||||||
|
|
||||||
|
# --- Runtime stage ----------------------------------------------------------
|
||||||
|
FROM debian:bookworm-slim AS runtime
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install runtime dependencies
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
ca-certificates \
|
||||||
|
curl \
|
||||||
|
zip \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy bot binary
|
||||||
|
COPY --from=build /app/bot /app/bot
|
||||||
|
|
||||||
|
# Copy required files
|
||||||
|
COPY --from=build /app/mnemo_cards_telegram_bot/admins /app/admins
|
||||||
|
|
||||||
|
# Create required directories
|
||||||
|
RUN mkdir -p /app/backups /app/zips /app/apks && \
|
||||||
|
chmod -R 755 /app/backups /app/zips /app/apks
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
ENV BACKEND_URL=https://api.mnemo-cards.online \
|
||||||
|
BOT_SHARE_DAILY_LIMIT=1
|
||||||
|
|
||||||
|
# No EXPOSE needed - bot uses outbound connections only (polling)
|
||||||
|
|
||||||
|
# Healthcheck - check if process is running
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=10s \
|
||||||
|
CMD pgrep -f "/app/bot" > /dev/null || exit 1
|
||||||
|
|
||||||
|
# Start bot
|
||||||
|
CMD ["/app/bot"]
|
||||||
Loading…
Reference in a new issue