Initial commit — DLS lab-app Flutter project
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
import '../../../core/api/pocketbase_client.dart';
|
||||
import '../../../models/prosthetic_product.dart';
|
||||
|
||||
class LabProductsRepository {
|
||||
LabProductsRepository._();
|
||||
static final instance = LabProductsRepository._();
|
||||
|
||||
PocketBase get _pb => PocketBaseClient.instance.pb;
|
||||
|
||||
Future<List<ProstheticProduct>> listProducts(
|
||||
String labTenantId, {
|
||||
bool? isActive,
|
||||
}) async {
|
||||
final filterParts = ['lab_tenant_id = "$labTenantId"'];
|
||||
if (isActive != null) filterParts.add('is_active = $isActive');
|
||||
|
||||
final result = await _pb.collection('prosthetic_products').getList(
|
||||
filter: filterParts.join(' && '),
|
||||
perPage: 200,
|
||||
);
|
||||
return (result.items.map((r) => ProstheticProduct.fromJson(r.toJson())).toList()
|
||||
..sort((a, b) => a.name.compareTo(b.name)));
|
||||
}
|
||||
|
||||
Future<ProstheticProduct> createProduct(ProstheticProduct product) async {
|
||||
final record = await _pb.collection('prosthetic_products').create(body: product.toJson());
|
||||
return ProstheticProduct.fromJson(record.toJson());
|
||||
}
|
||||
|
||||
Future<ProstheticProduct> updateProduct(String id, Map<String, dynamic> patch) async {
|
||||
final record = await _pb.collection('prosthetic_products').update(id, body: patch);
|
||||
return ProstheticProduct.fromJson(record.toJson());
|
||||
}
|
||||
|
||||
Future<void> deleteProduct(String id) async {
|
||||
await _pb.collection('prosthetic_products').delete(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,618 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../core/providers/auth_provider.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../core/widgets/gradient_app_bar.dart';
|
||||
import '../../../models/prosthetic_product.dart';
|
||||
import 'lab_products_repository.dart';
|
||||
|
||||
const _prostheticTypes = [
|
||||
('metal_porselen', 'Metal Porselen'),
|
||||
('zirkonyum', 'Zirkonyum'),
|
||||
('implant_ustu_zirkonyum', 'İmplant Üstü Zirkonyum'),
|
||||
('gecici', 'Geçici'),
|
||||
('e_max', 'E-Max'),
|
||||
('diger', 'Diğer'),
|
||||
];
|
||||
|
||||
String _typeLabel(String value) {
|
||||
for (final t in _prostheticTypes) {
|
||||
if (t.$1 == value) return t.$2;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// ── Adaptive sheet helper ────────────────────────────────────────────────────
|
||||
|
||||
void _showAdaptive(BuildContext context, Widget content) {
|
||||
final isDesktop = MediaQuery.sizeOf(context).width > AppLayout.sidebarBreakpoint;
|
||||
if (isDesktop) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => Dialog(
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 560),
|
||||
child: content,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => content,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LabProductsScreen extends ConsumerStatefulWidget {
|
||||
const LabProductsScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<LabProductsScreen> createState() => _LabProductsScreenState();
|
||||
}
|
||||
|
||||
class _LabProductsScreenState extends ConsumerState<LabProductsScreen> {
|
||||
late Future<List<ProstheticProduct>> _future;
|
||||
final _searchController = TextEditingController();
|
||||
String _searchQuery = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_load();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _load() {
|
||||
final tenantId = ref.read(authProvider).activeTenant!.tenant.id;
|
||||
setState(() {
|
||||
_future = LabProductsRepository.instance.listProducts(tenantId);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _toggleActive(ProstheticProduct product) async {
|
||||
try {
|
||||
await LabProductsRepository.instance
|
||||
.updateProduct(product.id, {'is_active': !product.isActive});
|
||||
_load();
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Hata: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deleteProduct(ProstheticProduct product) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Ürünü Sil'),
|
||||
content: Text(
|
||||
'"${product.name}" ürününü silmek istediğinize emin misiniz?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(false),
|
||||
child: const Text('İptal'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(true),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: AppColors.cancelled),
|
||||
child: const Text('Sil'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed != true) return;
|
||||
try {
|
||||
await LabProductsRepository.instance.deleteProduct(product.id);
|
||||
_load();
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Ürün silindi')),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Hata: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _showProductSheet({ProstheticProduct? existing}) {
|
||||
final tenantId = ref.read(authProvider).activeTenant!.tenant.id;
|
||||
_showAdaptive(
|
||||
context,
|
||||
_ProductForm(
|
||||
labTenantId: tenantId,
|
||||
existing: existing,
|
||||
onSaved: () {
|
||||
Navigator.of(context).pop();
|
||||
_load();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
appBar: GradientAppBar(
|
||||
title: 'Ürün Kataloğu',
|
||||
category: 'LABORATUVAR',
|
||||
searchController: _searchController,
|
||||
onSearchChanged: (v) => setState(() => _searchQuery = v),
|
||||
searchHint: 'Ürün adı veya türü ara...',
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () => _showProductSheet(),
|
||||
backgroundColor: AppColors.accent,
|
||||
foregroundColor: Colors.white,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Yeni Ürün'),
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
color: AppColors.accent,
|
||||
onRefresh: () async => _load(),
|
||||
child: FutureBuilder<List<ProstheticProduct>>(
|
||||
future: _future,
|
||||
builder: (ctx, snap) {
|
||||
if (snap.connectionState == ConnectionState.waiting) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(color: AppColors.accent));
|
||||
}
|
||||
if (snap.hasError) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 64,
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.cancelledBg,
|
||||
borderRadius: BorderRadius.circular(16)),
|
||||
child: const Icon(Icons.wifi_off_rounded,
|
||||
color: AppColors.cancelled, size: 30),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('Hata: ${snap.error}',
|
||||
style: const TextStyle(
|
||||
color: AppColors.textSecondary)),
|
||||
const SizedBox(height: 12),
|
||||
FilledButton.icon(
|
||||
onPressed: _load,
|
||||
icon: const Icon(Icons.refresh_rounded, size: 18),
|
||||
label: const Text('Tekrar Dene')),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final allProducts = snap.data!;
|
||||
final q = _searchQuery.toLowerCase().trim();
|
||||
final products = q.isEmpty
|
||||
? allProducts
|
||||
: allProducts.where((p) =>
|
||||
p.name.toLowerCase().contains(q) ||
|
||||
_typeLabel(p.prostheticType).toLowerCase().contains(q)).toList();
|
||||
|
||||
if (allProducts.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 72,
|
||||
height: 72,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.inProgressBg,
|
||||
borderRadius: BorderRadius.circular(20)),
|
||||
child: const Icon(Icons.inventory_2_outlined,
|
||||
size: 32, color: AppColors.inProgress),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
q.isNotEmpty ? 'Sonuç bulunamadı' : 'Henüz ürün eklenmedi',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimary),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (q.isEmpty) FilledButton.icon(
|
||||
onPressed: () => _showProductSheet(),
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('İlk Ürünü Ekle'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (products.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 72,
|
||||
height: 72,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.inProgressBg,
|
||||
borderRadius: BorderRadius.circular(20)),
|
||||
child: const Icon(Icons.search_off_rounded,
|
||||
size: 32, color: AppColors.inProgress),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Sonuç bulunamadı',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.textPrimary),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 80),
|
||||
itemCount: products.length,
|
||||
itemBuilder: (ctx, i) {
|
||||
final product = products[i];
|
||||
final statusColor =
|
||||
product.isActive ? AppColors.inProgress : AppColors.textMuted;
|
||||
final statusBg =
|
||||
product.isActive ? AppColors.inProgressBg : AppColors.surfaceVariant;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: GestureDetector(
|
||||
onLongPress: () => _deleteProduct(product),
|
||||
child: Material(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: InkWell(
|
||||
onTap: () => _showProductSheet(existing: product),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: AppColors.border),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color:
|
||||
Colors.black.withValues(alpha: 0.04),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2))
|
||||
]),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: statusBg,
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
child: Icon(Icons.medical_services_outlined,
|
||||
color: statusColor, size: 22),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
product.name,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: product.isActive
|
||||
? AppColors.textPrimary
|
||||
: AppColors.textMuted),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
_typeLabel(product.prostheticType),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.textSecondary),
|
||||
),
|
||||
if (product.unitPrice != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'${product.unitPrice!.toStringAsFixed(2)} ${product.currency ?? 'TRY'}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.success),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Switch(
|
||||
value: product.isActive,
|
||||
onChanged: (_) => _toggleActive(product),
|
||||
activeTrackColor: AppColors.accent,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit_outlined,
|
||||
color: AppColors.textSecondary,
|
||||
size: 20),
|
||||
onPressed: () =>
|
||||
_showProductSheet(existing: product),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Product Form ─────────────────────────────────────────────────────────────
|
||||
|
||||
class _ProductForm extends StatefulWidget {
|
||||
const _ProductForm({
|
||||
required this.labTenantId,
|
||||
required this.onSaved,
|
||||
this.existing,
|
||||
});
|
||||
|
||||
final String labTenantId;
|
||||
final ProstheticProduct? existing;
|
||||
final VoidCallback onSaved;
|
||||
|
||||
@override
|
||||
State<_ProductForm> createState() => _ProductFormState();
|
||||
}
|
||||
|
||||
class _ProductFormState extends State<_ProductForm> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late final TextEditingController _nameCtrl;
|
||||
late final TextEditingController _priceCtrl;
|
||||
late final TextEditingController _descCtrl;
|
||||
late String _selectedType;
|
||||
late String _currency;
|
||||
late bool _isActive;
|
||||
bool _saving = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final p = widget.existing;
|
||||
_nameCtrl = TextEditingController(text: p?.name ?? '');
|
||||
_priceCtrl = TextEditingController(
|
||||
text: p?.unitPrice != null ? p!.unitPrice!.toString() : '');
|
||||
_descCtrl = TextEditingController(text: p?.description ?? '');
|
||||
_selectedType = p?.prostheticType ?? _prostheticTypes.first.$1;
|
||||
_currency = p?.currency ?? 'TRY';
|
||||
_isActive = p?.isActive ?? true;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameCtrl.dispose();
|
||||
_priceCtrl.dispose();
|
||||
_descCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _save() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
setState(() => _saving = true);
|
||||
|
||||
final price = double.tryParse(_priceCtrl.text.trim());
|
||||
final product = ProstheticProduct(
|
||||
id: widget.existing?.id ?? '',
|
||||
labTenantId: widget.labTenantId,
|
||||
name: _nameCtrl.text.trim(),
|
||||
prostheticType: _selectedType,
|
||||
unitPrice: price,
|
||||
currency: _currency,
|
||||
isActive: _isActive,
|
||||
description:
|
||||
_descCtrl.text.trim().isEmpty ? null : _descCtrl.text.trim(),
|
||||
);
|
||||
|
||||
try {
|
||||
if (widget.existing != null) {
|
||||
await LabProductsRepository.instance.updateProduct(
|
||||
widget.existing!.id,
|
||||
product.toJson(),
|
||||
);
|
||||
} else {
|
||||
await LabProductsRepository.instance.createProduct(product);
|
||||
}
|
||||
widget.onSaved();
|
||||
} catch (e) {
|
||||
setState(() => _saving = false);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Hata: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDesktop = MediaQuery.sizeOf(context).width > AppLayout.sidebarBreakpoint;
|
||||
final isEdit = widget.existing != null;
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: isDesktop ? Radius.zero : const Radius.circular(20),
|
||||
),
|
||||
),
|
||||
padding: EdgeInsets.only(
|
||||
left: 20,
|
||||
right: 20,
|
||||
top: 24,
|
||||
bottom: isDesktop
|
||||
? 24
|
||||
: MediaQuery.of(context).viewInsets.bottom + 24,
|
||||
),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
isEdit ? 'Ürünü Düzenle' : 'Yeni Ürün',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.textPrimary),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close,
|
||||
color: AppColors.textSecondary),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Name
|
||||
TextFormField(
|
||||
controller: _nameCtrl,
|
||||
decoration:
|
||||
const InputDecoration(labelText: 'Ürün Adı *'),
|
||||
validator: (v) =>
|
||||
v == null || v.trim().isEmpty ? 'Ürün adı gerekli' : null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Prosthetic type dropdown
|
||||
DropdownButtonFormField<String>(
|
||||
initialValue: _selectedType,
|
||||
decoration:
|
||||
const InputDecoration(labelText: 'Protez Tipi *'),
|
||||
items: _prostheticTypes
|
||||
.map((t) => DropdownMenuItem(
|
||||
value: t.$1,
|
||||
child: Text(t.$2),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: (v) => setState(() => _selectedType = v!),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Price + currency row
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: TextFormField(
|
||||
controller: _priceCtrl,
|
||||
decoration:
|
||||
const InputDecoration(labelText: 'Birim Fiyat'),
|
||||
keyboardType: const TextInputType.numberWithOptions(
|
||||
decimal: true),
|
||||
validator: (v) {
|
||||
if (v != null && v.isNotEmpty) {
|
||||
if (double.tryParse(v) == null) {
|
||||
return 'Geçerli fiyat girin';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: DropdownButtonFormField<String>(
|
||||
initialValue: _currency,
|
||||
decoration:
|
||||
const InputDecoration(labelText: 'Para Birimi'),
|
||||
items: ['TRY', 'USD', 'EUR']
|
||||
.map((c) => DropdownMenuItem(
|
||||
value: c,
|
||||
child: Text(c),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: (v) => setState(() => _currency = v!),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Description
|
||||
TextFormField(
|
||||
controller: _descCtrl,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Açıklama (isteğe bağlı)'),
|
||||
maxLines: 2,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Active toggle
|
||||
SwitchListTile(
|
||||
title: const Text('Aktif',
|
||||
style: TextStyle(color: AppColors.textPrimary)),
|
||||
value: _isActive,
|
||||
onChanged: (v) => setState(() => _isActive = v),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
activeTrackColor: AppColors.accent,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
FilledButton(
|
||||
onPressed: _saving ? null : _save,
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: const Size.fromHeight(48)),
|
||||
child: _saving
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2, color: Colors.white),
|
||||
)
|
||||
: Text(isEdit ? 'Kaydet' : 'Ekle'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user