8bbc9dbff2
- Flutter + PocketBase dental lab management system - Clinic & lab dashboards, job tracking, patient management - Product catalog, finance tracking, multi-language support - AI assistant integration, realtime notifications - Windows installer (Inno Setup) included - Developed by kovakyazilim.com
941 lines
32 KiB
Dart
941 lines
32 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
|
||
import '../../../core/api/pocketbase_client.dart';
|
||
import '../../../core/providers/auth_provider.dart';
|
||
import '../../../core/theme/app_theme.dart';
|
||
import '../../../core/widgets/gradient_app_bar.dart';
|
||
import '../../../models/clinic_discount.dart';
|
||
import 'discount_repository.dart';
|
||
|
||
// Simple local record for clinic picker
|
||
class _ClinicOption {
|
||
const _ClinicOption({required this.id, required this.name});
|
||
final String id;
|
||
final String name;
|
||
}
|
||
|
||
class DiscountsScreen extends ConsumerStatefulWidget {
|
||
const DiscountsScreen({super.key});
|
||
|
||
@override
|
||
ConsumerState<DiscountsScreen> createState() => _DiscountsScreenState();
|
||
}
|
||
|
||
class _DiscountsScreenState extends ConsumerState<DiscountsScreen> {
|
||
late Future<List<ClinicDiscount>> _future;
|
||
String _searchQuery = '';
|
||
final _searchController = TextEditingController();
|
||
|
||
@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 = DiscountRepository.instance.listDiscounts(tenantId);
|
||
});
|
||
}
|
||
|
||
Future<void> _showSheet({ClinicDiscount? existing}) async {
|
||
final tenantId = ref.read(authProvider).activeTenant!.tenant.id;
|
||
final result = await showModalBottomSheet<bool>(
|
||
context: context,
|
||
isScrollControlled: true,
|
||
backgroundColor: Colors.transparent,
|
||
builder: (_) => _DiscountSheet(
|
||
labTenantId: tenantId,
|
||
existing: existing,
|
||
),
|
||
);
|
||
if (result == true) _load();
|
||
}
|
||
|
||
Future<void> _delete(ClinicDiscount discount) async {
|
||
final confirmed = await showDialog<bool>(
|
||
context: context,
|
||
builder: (ctx) => AlertDialog(
|
||
title: const Text('İndirimi Sil'),
|
||
content: Text(
|
||
'${discount.clinicName ?? 'Tüm Klinikler'} — ${discount.displayValue} indirimi silinsin mi?'),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.pop(ctx, false),
|
||
child: const Text('İptal')),
|
||
FilledButton(
|
||
style:
|
||
FilledButton.styleFrom(backgroundColor: AppColors.cancelled),
|
||
onPressed: () => Navigator.pop(ctx, true),
|
||
child: const Text('Sil'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
if (confirmed != true) return;
|
||
try {
|
||
await DiscountRepository.instance.deleteDiscount(discount.id);
|
||
_load();
|
||
} catch (e) {
|
||
if (mounted) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text('Hata: $e'),
|
||
backgroundColor: AppColors.cancelled),
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
Future<void> _toggleActive(ClinicDiscount discount) async {
|
||
try {
|
||
await DiscountRepository.instance
|
||
.updateDiscount(discount.id, isActive: !discount.isActive);
|
||
_load();
|
||
} catch (e) {
|
||
if (mounted) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text('Hata: $e'),
|
||
backgroundColor: AppColors.cancelled),
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
backgroundColor: AppColors.background,
|
||
appBar: GradientAppBar(
|
||
title: 'İndirimler',
|
||
category: 'LABORATUVAR',
|
||
searchController: _searchController,
|
||
onSearchChanged: (v) => setState(() => _searchQuery = v),
|
||
searchHint: 'Klinik veya ürün tipi ara...',
|
||
),
|
||
floatingActionButton: FloatingActionButton.extended(
|
||
onPressed: () => _showSheet(),
|
||
backgroundColor: AppColors.accent,
|
||
foregroundColor: Colors.white,
|
||
icon: const Icon(Icons.add),
|
||
label: const Text('Yeni İndirim'),
|
||
),
|
||
body: FutureBuilder<List<ClinicDiscount>>(
|
||
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: [
|
||
const Icon(Icons.wifi_off_rounded,
|
||
color: AppColors.cancelled, size: 40),
|
||
const SizedBox(height: 12),
|
||
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: 16),
|
||
label: const Text('Tekrar Dene')),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
final allDiscounts = snap.data!;
|
||
final q = _searchQuery.toLowerCase().trim();
|
||
final discounts = q.isEmpty
|
||
? allDiscounts
|
||
: allDiscounts
|
||
.where((d) =>
|
||
(d.clinicName ?? 'tüm klinikler')
|
||
.toLowerCase()
|
||
.contains(q) ||
|
||
d.prostheticLabel.toLowerCase().contains(q))
|
||
.toList();
|
||
|
||
if (allDiscounts.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.discount_outlined,
|
||
size: 32, color: AppColors.inProgress),
|
||
),
|
||
const SizedBox(height: 16),
|
||
const Text('Henüz indirim tanımlanmadı',
|
||
style: TextStyle(
|
||
fontSize: 16,
|
||
fontWeight: FontWeight.w600,
|
||
color: AppColors.textPrimary)),
|
||
const SizedBox(height: 8),
|
||
const Text(
|
||
'Klinik ve ürün bazlı özel indirimler ekleyin.',
|
||
style: TextStyle(
|
||
color: AppColors.textSecondary, fontSize: 13),
|
||
textAlign: TextAlign.center),
|
||
const SizedBox(height: 20),
|
||
FilledButton.icon(
|
||
onPressed: () => _showSheet(),
|
||
icon: const Icon(Icons.add),
|
||
label: const Text('İlk İndirimi Ekle'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
if (discounts.isEmpty) {
|
||
return const Center(
|
||
child: Text('Sonuç bulunamadı',
|
||
style: TextStyle(color: AppColors.textSecondary)),
|
||
);
|
||
}
|
||
|
||
final active = discounts.where((d) => d.isActive).toList();
|
||
final inactive = discounts.where((d) => !d.isActive).toList();
|
||
|
||
return RefreshIndicator(
|
||
color: AppColors.accent,
|
||
onRefresh: () async => _load(),
|
||
child: ListView(
|
||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 100),
|
||
children: [
|
||
if (active.isNotEmpty) ...[
|
||
_GroupHeader('Aktif (${active.length})'),
|
||
for (final d in active)
|
||
_DiscountCard(
|
||
discount: d,
|
||
onEdit: () => _showSheet(existing: d),
|
||
onDelete: () => _delete(d),
|
||
onToggle: () => _toggleActive(d),
|
||
),
|
||
],
|
||
if (inactive.isNotEmpty) ...[
|
||
const SizedBox(height: 8),
|
||
_GroupHeader('Pasif (${inactive.length})'),
|
||
for (final d in inactive)
|
||
_DiscountCard(
|
||
discount: d,
|
||
onEdit: () => _showSheet(existing: d),
|
||
onDelete: () => _delete(d),
|
||
onToggle: () => _toggleActive(d),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
);
|
||
},
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ── Group header ──────────────────────────────────────────────────────────────
|
||
|
||
class _GroupHeader extends StatelessWidget {
|
||
const _GroupHeader(this.text);
|
||
final String text;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Padding(
|
||
padding: const EdgeInsets.only(bottom: 8, top: 4),
|
||
child: Text(text,
|
||
style: const TextStyle(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w700,
|
||
color: AppColors.textMuted,
|
||
letterSpacing: 0.5)),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ── Discount card ─────────────────────────────────────────────────────────────
|
||
|
||
class _DiscountCard extends StatelessWidget {
|
||
const _DiscountCard({
|
||
required this.discount,
|
||
required this.onEdit,
|
||
required this.onDelete,
|
||
required this.onToggle,
|
||
});
|
||
|
||
final ClinicDiscount discount;
|
||
final VoidCallback onEdit;
|
||
final VoidCallback onDelete;
|
||
final VoidCallback onToggle;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final d = discount;
|
||
final isActive = d.isActive;
|
||
|
||
return Padding(
|
||
padding: const EdgeInsets.only(bottom: 10),
|
||
child: Material(
|
||
color: isActive ? AppColors.surface : AppColors.surfaceVariant,
|
||
borderRadius: BorderRadius.circular(14),
|
||
child: InkWell(
|
||
onTap: onEdit,
|
||
borderRadius: BorderRadius.circular(14),
|
||
child: Container(
|
||
decoration: BoxDecoration(
|
||
borderRadius: BorderRadius.circular(14),
|
||
border: Border.all(
|
||
color: isActive ? AppColors.border : AppColors.muted),
|
||
boxShadow: isActive
|
||
? [
|
||
BoxShadow(
|
||
color: Colors.black.withValues(alpha: 0.03),
|
||
blurRadius: 6,
|
||
offset: const Offset(0, 2))
|
||
]
|
||
: [],
|
||
),
|
||
child: IntrinsicHeight(
|
||
child: Row(
|
||
children: [
|
||
Container(
|
||
width: 4,
|
||
decoration: BoxDecoration(
|
||
color:
|
||
isActive ? AppColors.success : AppColors.border,
|
||
borderRadius: const BorderRadius.only(
|
||
topLeft: Radius.circular(14),
|
||
bottomLeft: Radius.circular(14),
|
||
),
|
||
),
|
||
),
|
||
Expanded(
|
||
child: Padding(
|
||
padding: const EdgeInsets.fromLTRB(12, 12, 4, 12),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 10, vertical: 4),
|
||
decoration: BoxDecoration(
|
||
color: isActive
|
||
? AppColors.successBg
|
||
: AppColors.background,
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: Text(
|
||
'${d.displayValue} İndirim',
|
||
style: TextStyle(
|
||
fontSize: 15,
|
||
fontWeight: FontWeight.w800,
|
||
color: isActive
|
||
? AppColors.success
|
||
: AppColors.textMuted,
|
||
),
|
||
),
|
||
),
|
||
if (d.minQuantity > 0) ...[
|
||
const SizedBox(width: 8),
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 7, vertical: 4),
|
||
decoration: BoxDecoration(
|
||
color: AppColors.pendingBg,
|
||
borderRadius: BorderRadius.circular(6),
|
||
),
|
||
child: Text(
|
||
'≥${d.minQuantity} adet',
|
||
style: const TextStyle(
|
||
fontSize: 11,
|
||
fontWeight: FontWeight.w600,
|
||
color: AppColors.pending),
|
||
),
|
||
),
|
||
],
|
||
const Spacer(),
|
||
Transform.scale(
|
||
scale: 0.8,
|
||
child: Switch(
|
||
value: isActive,
|
||
onChanged: (_) => onToggle(),
|
||
activeColor: AppColors.success,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 8),
|
||
Row(
|
||
children: [
|
||
_Tag(
|
||
icon: Icons.local_hospital_outlined,
|
||
label: d.appliesToAll
|
||
? 'Tüm Klinikler'
|
||
: (d.clinicName ?? 'Klinik'),
|
||
color: AppColors.inProgress,
|
||
),
|
||
const SizedBox(width: 6),
|
||
_Tag(
|
||
icon: Icons.science_outlined,
|
||
label: d.prostheticLabel,
|
||
color: AppColors.accent,
|
||
),
|
||
],
|
||
),
|
||
if (d.notes != null && d.notes!.isNotEmpty) ...[
|
||
const SizedBox(height: 6),
|
||
Text(d.notes!,
|
||
style: const TextStyle(
|
||
fontSize: 12,
|
||
color: AppColors.textMuted),
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis),
|
||
],
|
||
],
|
||
),
|
||
),
|
||
),
|
||
IconButton(
|
||
onPressed: onDelete,
|
||
icon: const Icon(Icons.delete_outline_rounded,
|
||
size: 18, color: AppColors.cancelled),
|
||
tooltip: 'Sil',
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _Tag extends StatelessWidget {
|
||
const _Tag(
|
||
{required this.icon, required this.label, required this.color});
|
||
final IconData icon;
|
||
final String label;
|
||
final Color color;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3),
|
||
decoration: BoxDecoration(
|
||
color: color.withValues(alpha: 0.1),
|
||
borderRadius: BorderRadius.circular(6),
|
||
),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Icon(icon, size: 11, color: color),
|
||
const SizedBox(width: 4),
|
||
Text(label,
|
||
style: TextStyle(
|
||
fontSize: 11,
|
||
fontWeight: FontWeight.w600,
|
||
color: color)),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ── Discount sheet ────────────────────────────────────────────────────────────
|
||
|
||
class _DiscountSheet extends StatefulWidget {
|
||
const _DiscountSheet({required this.labTenantId, this.existing});
|
||
final String labTenantId;
|
||
final ClinicDiscount? existing;
|
||
|
||
@override
|
||
State<_DiscountSheet> createState() => _DiscountSheetState();
|
||
}
|
||
|
||
class _DiscountSheetState extends State<_DiscountSheet> {
|
||
final _valueCtrl = TextEditingController();
|
||
final _minQtyCtrl = TextEditingController();
|
||
final _notesCtrl = TextEditingController();
|
||
|
||
DiscountType _discountType = DiscountType.percentage;
|
||
String? _selectedClinicId;
|
||
String? _selectedType;
|
||
bool _isActive = true;
|
||
bool _saving = false;
|
||
|
||
List<_ClinicOption>? _clinics;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
final e = widget.existing;
|
||
if (e != null) {
|
||
_valueCtrl.text = e.discountValue.toStringAsFixed(
|
||
e.discountValue % 1 == 0 ? 0 : 2);
|
||
_minQtyCtrl.text =
|
||
e.minQuantity > 0 ? e.minQuantity.toString() : '';
|
||
_notesCtrl.text = e.notes ?? '';
|
||
_discountType = e.discountType;
|
||
_selectedClinicId = e.clinicTenantId;
|
||
_selectedType = e.prostheticType;
|
||
_isActive = e.isActive;
|
||
}
|
||
_loadClinics();
|
||
}
|
||
|
||
Future<void> _loadClinics() async {
|
||
try {
|
||
final pb = PocketBaseClient.instance.pb;
|
||
final result = await pb.collection('tenant_connections').getList(
|
||
filter:
|
||
'lab_tenant_id = "${widget.labTenantId}" && status = "approved"',
|
||
expand: 'clinic_tenant_id',
|
||
perPage: 100,
|
||
);
|
||
final clinics = result.items.map((r) {
|
||
final j = r.toJson();
|
||
final expand = j['expand'] as Map<String, dynamic>?;
|
||
final clinic =
|
||
expand?['clinic_tenant_id'] as Map<String, dynamic>?;
|
||
return _ClinicOption(
|
||
id: j['clinic_tenant_id'] as String? ?? '',
|
||
name: clinic?['company_name'] as String? ?? 'Klinik',
|
||
);
|
||
}).where((c) => c.id.isNotEmpty).toList();
|
||
if (mounted) setState(() => _clinics = clinics);
|
||
} catch (_) {
|
||
if (mounted) setState(() => _clinics = []);
|
||
}
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_valueCtrl.dispose();
|
||
_minQtyCtrl.dispose();
|
||
_notesCtrl.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
Future<void> _save() async {
|
||
final valueStr = _valueCtrl.text.trim().replaceAll(',', '.');
|
||
final value = double.tryParse(valueStr);
|
||
if (value == null || value <= 0) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(
|
||
content: Text('Geçerli bir indirim değeri girin.'),
|
||
backgroundColor: AppColors.cancelled),
|
||
);
|
||
return;
|
||
}
|
||
if (_discountType == DiscountType.percentage && value > 100) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(
|
||
content: Text("Yüzde indirim 100'ü geçemez."),
|
||
backgroundColor: AppColors.cancelled),
|
||
);
|
||
return;
|
||
}
|
||
|
||
final minQty = int.tryParse(_minQtyCtrl.text.trim()) ?? 0;
|
||
setState(() => _saving = true);
|
||
final navigator = Navigator.of(context);
|
||
final messenger = ScaffoldMessenger.of(context);
|
||
try {
|
||
if (widget.existing != null) {
|
||
await DiscountRepository.instance.updateDiscount(
|
||
widget.existing!.id,
|
||
clinicTenantId: _selectedClinicId ?? '',
|
||
prostheticType: _selectedType ?? '',
|
||
discountType: _discountType,
|
||
discountValue: value,
|
||
minQuantity: minQty,
|
||
isActive: _isActive,
|
||
notes: _notesCtrl.text.trim(),
|
||
);
|
||
} else {
|
||
await DiscountRepository.instance.createDiscount(
|
||
labTenantId: widget.labTenantId,
|
||
clinicTenantId: _selectedClinicId,
|
||
prostheticType: _selectedType,
|
||
discountType: _discountType,
|
||
discountValue: value,
|
||
minQuantity: minQty,
|
||
isActive: _isActive,
|
||
notes: _notesCtrl.text.trim(),
|
||
);
|
||
}
|
||
navigator.pop(true);
|
||
} catch (e) {
|
||
if (mounted) setState(() => _saving = false);
|
||
messenger.showSnackBar(
|
||
SnackBar(
|
||
content: Text('Hata: $e'),
|
||
backgroundColor: AppColors.cancelled),
|
||
);
|
||
}
|
||
}
|
||
|
||
static const _prostheticTypes = [
|
||
('', 'Tüm Türler'),
|
||
('metal_porselen', 'Metal Porselen'),
|
||
('zirkonyum', 'Zirkonyum'),
|
||
('implant_ustu_zirkonyum', 'İmplant Üstü Zirkonyum'),
|
||
('gecici', 'Geçici'),
|
||
('e_max', 'E-Max'),
|
||
('tam_protez', 'Tam Protez'),
|
||
('parsiyel', 'Parsiyel Protez'),
|
||
('diger', 'Diğer'),
|
||
];
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final bottom = MediaQuery.paddingOf(context).bottom;
|
||
return Container(
|
||
decoration: const BoxDecoration(
|
||
color: AppColors.surface,
|
||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||
),
|
||
padding: EdgeInsets.only(bottom: bottom),
|
||
child: SingleChildScrollView(
|
||
padding: EdgeInsets.only(
|
||
left: 20,
|
||
right: 20,
|
||
top: 20,
|
||
bottom: MediaQuery.viewInsetsOf(context).bottom + 20,
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Center(
|
||
child: Container(
|
||
width: 40,
|
||
height: 4,
|
||
decoration: BoxDecoration(
|
||
color: AppColors.border,
|
||
borderRadius: BorderRadius.circular(2)),
|
||
),
|
||
),
|
||
const SizedBox(height: 16),
|
||
Text(
|
||
widget.existing != null
|
||
? 'İndirimi Düzenle'
|
||
: 'Yeni İndirim',
|
||
style: const TextStyle(
|
||
fontSize: 18,
|
||
fontWeight: FontWeight.w700,
|
||
color: AppColors.textPrimary),
|
||
),
|
||
const SizedBox(height: 20),
|
||
|
||
const Text('İndirim Türü',
|
||
style: TextStyle(
|
||
fontSize: 13,
|
||
fontWeight: FontWeight.w600,
|
||
color: AppColors.textSecondary)),
|
||
const SizedBox(height: 8),
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: _TypeButton(
|
||
label: 'Yüzde (%)',
|
||
icon: Icons.percent_rounded,
|
||
selected: _discountType == DiscountType.percentage,
|
||
onTap: () => setState(
|
||
() => _discountType = DiscountType.percentage),
|
||
),
|
||
),
|
||
const SizedBox(width: 10),
|
||
Expanded(
|
||
child: _TypeButton(
|
||
label: 'Sabit Tutar',
|
||
icon: Icons.currency_lira_rounded,
|
||
selected: _discountType == DiscountType.fixed,
|
||
onTap: () =>
|
||
setState(() => _discountType = DiscountType.fixed),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 16),
|
||
|
||
const Text('İndirim Değeri',
|
||
style: TextStyle(
|
||
fontSize: 13,
|
||
fontWeight: FontWeight.w600,
|
||
color: AppColors.textSecondary)),
|
||
const SizedBox(height: 8),
|
||
TextField(
|
||
controller: _valueCtrl,
|
||
keyboardType:
|
||
const TextInputType.numberWithOptions(decimal: true),
|
||
decoration: InputDecoration(
|
||
hintText: _discountType == DiscountType.percentage
|
||
? 'Örn: 10'
|
||
: 'Örn: 150',
|
||
suffixText:
|
||
_discountType == DiscountType.percentage ? '%' : 'TL',
|
||
border: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(12)),
|
||
),
|
||
),
|
||
const SizedBox(height: 16),
|
||
|
||
const Text('Klinik',
|
||
style: TextStyle(
|
||
fontSize: 13,
|
||
fontWeight: FontWeight.w600,
|
||
color: AppColors.textSecondary)),
|
||
const SizedBox(height: 8),
|
||
_ClinicDropdown(
|
||
selectedId: _selectedClinicId,
|
||
clinics: _clinics,
|
||
onChanged: (id, _) => setState(() {
|
||
_selectedClinicId = id;
|
||
}),
|
||
),
|
||
const SizedBox(height: 16),
|
||
|
||
const Text('Ürün Tipi',
|
||
style: TextStyle(
|
||
fontSize: 13,
|
||
fontWeight: FontWeight.w600,
|
||
color: AppColors.textSecondary)),
|
||
const SizedBox(height: 8),
|
||
DropdownButtonFormField<String>(
|
||
value: _selectedType ?? '',
|
||
decoration: InputDecoration(
|
||
border: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(12)),
|
||
contentPadding: const EdgeInsets.symmetric(
|
||
horizontal: 14, vertical: 12),
|
||
),
|
||
items: _prostheticTypes
|
||
.map((t) =>
|
||
DropdownMenuItem(value: t.$1, child: Text(t.$2)))
|
||
.toList(),
|
||
onChanged: (v) =>
|
||
setState(() => _selectedType = v == '' ? null : v),
|
||
),
|
||
const SizedBox(height: 16),
|
||
|
||
const Text('Minimum Sipariş Adedi (İsteğe Bağlı)',
|
||
style: TextStyle(
|
||
fontSize: 13,
|
||
fontWeight: FontWeight.w600,
|
||
color: AppColors.textSecondary)),
|
||
const SizedBox(height: 4),
|
||
const Text(
|
||
'Aylık bu adede ulaşılınca indirim devreye girer. 0 = koşulsuz.',
|
||
style:
|
||
TextStyle(fontSize: 11, color: AppColors.textMuted)),
|
||
const SizedBox(height: 8),
|
||
TextField(
|
||
controller: _minQtyCtrl,
|
||
keyboardType: TextInputType.number,
|
||
decoration: InputDecoration(
|
||
hintText: '0',
|
||
suffixText: 'adet',
|
||
border: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(12)),
|
||
),
|
||
),
|
||
const SizedBox(height: 16),
|
||
|
||
const Text('Not (İsteğe Bağlı)',
|
||
style: TextStyle(
|
||
fontSize: 13,
|
||
fontWeight: FontWeight.w600,
|
||
color: AppColors.textSecondary)),
|
||
const SizedBox(height: 8),
|
||
TextField(
|
||
controller: _notesCtrl,
|
||
maxLines: 2,
|
||
decoration: InputDecoration(
|
||
hintText: 'Açıklama...',
|
||
border: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(12)),
|
||
),
|
||
),
|
||
const SizedBox(height: 16),
|
||
|
||
Row(
|
||
children: [
|
||
const Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text('Aktif',
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.w600,
|
||
color: AppColors.textPrimary)),
|
||
Text('Pasif indirimler uygulanmaz.',
|
||
style: TextStyle(
|
||
fontSize: 12, color: AppColors.textMuted)),
|
||
],
|
||
),
|
||
),
|
||
Switch(
|
||
value: _isActive,
|
||
onChanged: (v) => setState(() => _isActive = v),
|
||
activeColor: AppColors.success,
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 24),
|
||
|
||
SizedBox(
|
||
width: double.infinity,
|
||
height: 50,
|
||
child: FilledButton(
|
||
onPressed: _saving ? null : _save,
|
||
style: FilledButton.styleFrom(
|
||
backgroundColor: AppColors.accent,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(14)),
|
||
),
|
||
child: _saving
|
||
? const SizedBox(
|
||
width: 20,
|
||
height: 20,
|
||
child: CircularProgressIndicator(
|
||
strokeWidth: 2, color: Colors.white))
|
||
: Text(
|
||
widget.existing != null ? 'Güncelle' : 'Kaydet',
|
||
style: const TextStyle(
|
||
fontSize: 16,
|
||
fontWeight: FontWeight.w600)),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _TypeButton extends StatelessWidget {
|
||
const _TypeButton({
|
||
required this.label,
|
||
required this.icon,
|
||
required this.selected,
|
||
required this.onTap,
|
||
});
|
||
final String label;
|
||
final IconData icon;
|
||
final bool selected;
|
||
final VoidCallback onTap;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return GestureDetector(
|
||
onTap: onTap,
|
||
child: AnimatedContainer(
|
||
duration: const Duration(milliseconds: 150),
|
||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||
decoration: BoxDecoration(
|
||
color: selected ? AppColors.accent : AppColors.background,
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(
|
||
color: selected ? AppColors.accent : AppColors.border),
|
||
),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Icon(icon,
|
||
size: 16,
|
||
color: selected ? Colors.white : AppColors.textSecondary),
|
||
const SizedBox(width: 6),
|
||
Text(label,
|
||
style: TextStyle(
|
||
fontSize: 13,
|
||
fontWeight: FontWeight.w600,
|
||
color: selected
|
||
? Colors.white
|
||
: AppColors.textSecondary)),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _ClinicDropdown extends StatelessWidget {
|
||
const _ClinicDropdown({
|
||
required this.selectedId,
|
||
required this.clinics,
|
||
required this.onChanged,
|
||
});
|
||
|
||
final String? selectedId;
|
||
final List<_ClinicOption>? clinics;
|
||
final void Function(String? id, String? name) onChanged;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
if (clinics == null) {
|
||
return Container(
|
||
height: 48,
|
||
decoration: BoxDecoration(
|
||
border: Border.all(color: AppColors.border),
|
||
borderRadius: BorderRadius.circular(12),
|
||
),
|
||
child: const Center(
|
||
child: SizedBox(
|
||
width: 16,
|
||
height: 16,
|
||
child: CircularProgressIndicator(strokeWidth: 2))),
|
||
);
|
||
}
|
||
|
||
final items = <DropdownMenuItem<String>>[
|
||
const DropdownMenuItem(value: '', child: Text('Tüm Klinikler')),
|
||
for (final c in clinics!)
|
||
DropdownMenuItem(value: c.id, child: Text(c.name)),
|
||
];
|
||
|
||
return DropdownButtonFormField<String>(
|
||
value: selectedId ?? '',
|
||
decoration: InputDecoration(
|
||
border:
|
||
OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
||
contentPadding:
|
||
const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||
),
|
||
items: items,
|
||
onChanged: (v) {
|
||
if (v == null || v.isEmpty) {
|
||
onChanged(null, null);
|
||
} else {
|
||
final clinic = clinics!.firstWhere((c) => c.id == v);
|
||
onChanged(v, clinic.name);
|
||
}
|
||
},
|
||
);
|
||
}
|
||
}
|