Files
app-grosir/lib/screens/produk_screen.dart

972 lines
39 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:image_picker/image_picker.dart';
import 'package:provider/provider.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../models/product.dart';
import '../state/app_state.dart';
import '../utils/colors.dart';
import '../utils/format.dart';
import '../utils/rupiah_input.dart';
import 'scan_screen.dart';
const String kProductImageBucket = 'product-images';
/// URL publik gambar produk dari Supabase Storage (null jika tidak ada).
String? productImageUrl(String? path) {
if (path == null || path.isEmpty) return null;
return Supabase.instance.client.storage.from(kProductImageBucket).getPublicUrl(path);
}
/// Pilih gambar (galeri/kamera), kompres otomatis, lalu upload ke Storage.
/// Mengembalikan path file di bucket, atau null jika batal/gagal.
Future<String?> pickAndUploadProductImage(BuildContext context,
{ImageSource source = ImageSource.gallery}) async {
final picker = ImagePicker();
try {
final picked = await picker.pickImage(
source: source,
maxWidth: 800,
maxHeight: 800,
imageQuality: 80,
);
if (picked == null) return null;
final bytes = await picked.readAsBytes();
var ext = 'jpg';
final dot = picked.name.lastIndexOf('.');
if (dot > -1) {
final raw = picked.name.substring(dot + 1).toLowerCase();
if (['jpg', 'jpeg', 'png', 'webp'].contains(raw)) ext = raw;
}
// MIME resmi: jpg -> image/jpeg (bukan image/jpg, ditolak server 415)
final mime = ext == 'jpg' ? 'image/jpeg' : 'image/$ext';
final path =
'prod-${DateTime.now().millisecondsSinceEpoch}.$ext';
await Supabase.instance.client.storage
.from(kProductImageBucket)
.uploadBinary(path, bytes,
fileOptions: FileOptions(contentType: mime, upsert: true));
return path;
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('Upload gambar gagal: $e'),
backgroundColor: kDanger,
));
}
return null;
}
}
/// Widget preview + tombol pilih/hapus gambar untuk form produk.
Widget productImageField({
required BuildContext context,
required void Function(String? path) onChanged,
required String? currentPath,
bool enabled = true,
}) {
final url = productImageUrl(currentPath);
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Row(
children: [
Container(
width: 72,
height: 72,
decoration: BoxDecoration(
color: kAccent.withValues(alpha: 0.10),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: kBorder),
),
clipBehavior: Clip.antiAlias,
child: url != null
? Image.network(url, fit: BoxFit.cover, errorBuilder: (_, __, ___) =>
const Center(child: Icon(Icons.broken_image_outlined, color: kMuted, size: 26)))
: const Center(
child: Icon(Icons.image_outlined, color: kMuted, size: 26)),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Foto produk',
style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600)),
const SizedBox(height: 2),
Text(
url != null ? 'Foto tersimpan' : 'Belum ada foto (opsional)',
style: const TextStyle(fontSize: 11, color: kMuted),
),
const SizedBox(height: 6),
Wrap(
spacing: 6,
runSpacing: 6,
children: [
ActionChip(
avatar: const Icon(Icons.photo_camera_outlined, size: 16),
label: const Text('Kamera', style: TextStyle(fontSize: 12)),
onPressed: !enabled
? null
: () async {
final p = await pickAndUploadProductImage(context,
source: ImageSource.camera);
if (p != null) onChanged(p);
},
),
ActionChip(
avatar: const Icon(Icons.photo_library_outlined, size: 16),
label: const Text('Galeri', style: TextStyle(fontSize: 12)),
onPressed: !enabled
? null
: () async {
final p = await pickAndUploadProductImage(context);
if (p != null) onChanged(p);
},
),
if (currentPath != null && currentPath.isNotEmpty)
ActionChip(
avatar: const Icon(Icons.delete_outline,
size: 16, color: kDanger),
label: const Text('Hapus',
style: TextStyle(fontSize: 12, color: kDanger)),
side: const BorderSide(color: kBorder),
onPressed: !enabled
? null
: () {
// Best-effort hapus file lama di storage
Supabase.instance.client.storage
.from(kProductImageBucket)
.remove([currentPath]).ignore();
onChanged(null);
},
),
],
),
],
),
),
],
),
);
}
Future<bool> confirmResellerWarning(BuildContext context, double general, double reseller) async {
if (reseller <= 0 || reseller <= general) return true;
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
icon: Icon(Icons.warning_amber_rounded, color: kAccent, size: 28),
title: const Text('Peringatan'),
content: Text('Harga reseller (${formatRupiah(reseller)}) lebih tinggi dari harga umum (${formatRupiah(general)}).'),
actions: [
SizedBox(
width: double.infinity,
child: OutlinedButton(
onPressed: () => Navigator.pop(ctx, false),
style: OutlinedButton.styleFrom(
foregroundColor: kMuted,
side: const BorderSide(color: kBorder),
),
child: const Text('Batal'),
),
),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: () => Navigator.pop(ctx, true),
style: FilledButton.styleFrom(backgroundColor: kAccent),
child: const Text('Tetap Simpan'),
),
),
],
),
);
return confirmed ?? false;
}
class ProdukScreen extends StatefulWidget {
const ProdukScreen({super.key});
@override
State<ProdukScreen> createState() => _ProdukScreenState();
}
class _ProdukScreenState extends State<ProdukScreen> {
final TextEditingController _search = TextEditingController();
@override
void initState() {
super.initState();
_search.addListener(() {
if (mounted) context.read<AppState>().resetPagination();
});
}
@override
void dispose() {
_search.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final state = context.watch<AppState>();
if (state.loading) {
return const Center(child: CircularProgressIndicator(color: kAccent));
}
final query = _search.text.toLowerCase();
final filtered = state.products
.where((p) =>
p.name.toLowerCase().contains(query) ||
p.barcode.toLowerCase().contains(query))
.toList();
final products = filtered.take(state.visibleProductCount).toList();
return RefreshIndicator(
onRefresh: () async {
context.read<AppState>().recordManualRefresh();
await context.read<AppState>().refresh();
},
color: kAccent,
child: Stack(
children: [
SingleChildScrollView(
padding: const EdgeInsets.all(12),
child: Column(
children: [
TextField(
controller: _search,
onChanged: (_) => setState(() {}),
style: const TextStyle(color: kText),
decoration: InputDecoration(
hintText: 'Cari produk...',
prefixIcon: const Icon(Icons.search, color: kMuted),
suffixIcon: _search.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.close, color: kMuted),
onPressed: () {
_search.clear();
setState(() {});
},
)
: null,
),
),
const SizedBox(height: 12),
Row(
children: [
Text(
'Daftar Produk',
style: const TextStyle(
fontSize: 15, fontWeight: FontWeight.w700),
),
const Spacer(),
FilledButton.icon(
onPressed: () => _showAddForm(context, state),
icon: const Icon(Icons.add, size: 18),
label: const Text('Tambah Produk'),
style: FilledButton.styleFrom(
backgroundColor: kAccent,
foregroundColor: const Color(0xFF06210F),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
visualDensity: VisualDensity.compact,
),
),
],
),
const SizedBox(height: 12),
if (products.isEmpty)
const Center(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 40),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.inventory_2_outlined, size: 48, color: kMuted),
SizedBox(height: 8),
Text('Belum ada produk', style: TextStyle(color: kMuted)),
],
),
),
)
else
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: products.length,
separatorBuilder: (_, __) => const SizedBox(height: 10),
itemBuilder: (context, i) =>
_ProductRow(p: products[i], state: state),
),
if (state.hasMoreProducts)
Padding(
padding: const EdgeInsets.only(top: 12),
child: FilledButton.icon(
onPressed: () => state.loadMoreProducts(),
icon: const Icon(Icons.expand_more, size: 18),
label: Text('Muat lebih banyak (${state.products.length - state.visibleProductCount} tersisa)'),
),
),
],
),
),
],
),
);
}
}
void _showAddForm(BuildContext context, AppState state) {
final name = TextEditingController();
final barcode = TextEditingController();
final price = TextEditingController();
final priceReseller = TextEditingController();
final stock = TextEditingController();
final minStockCtrl = TextEditingController(text: '5');
final hargaPokokCtrl = TextEditingController();
String? imagePath; // path gambar di Supabase Storage
final focusNodes = <FocusNode>[
FocusNode(),
FocusNode(),
FocusNode(),
FocusNode(),
FocusNode(),
FocusNode(),
FocusNode(),
];
bool listenersAdded = false;
final sheetController = DraggableScrollableController();
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (sheetContext) => DraggableScrollableSheet(
controller: sheetController,
initialChildSize: 0.65,
minChildSize: 0.5,
maxChildSize: 0.92,
builder: (_, scrollController) {
if (!listenersAdded) {
listenersAdded = true;
for (final node in focusNodes) {
node.addListener(() {
if (node.hasFocus && context.mounted) {
sheetController.animateTo(
0.92,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
Future.delayed(const Duration(milliseconds: 350), () {
if (node.context != null && node.context!.mounted) {
Scrollable.ensureVisible(
node.context!,
duration: const Duration(milliseconds: 250),
alignment: 0.0,
);
}
});
}
});
}
}
return Container(
decoration: const BoxDecoration(
color: kSurface,
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
border: Border(top: BorderSide(color: kBorder)),
),
child: Column(
children: [
Container(
margin: const EdgeInsets.only(top: 12),
width: 40,
height: 4,
decoration: BoxDecoration(
color: kBorder,
borderRadius: BorderRadius.circular(4),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
child: Row(
children: [
const Icon(Icons.add_box_outlined, color: kAccent),
const SizedBox(width: 8),
const Text('Tambah Produk',
style: TextStyle(
fontSize: 16, fontWeight: FontWeight.w700)),
const Spacer(),
IconButton(
icon: const Icon(Icons.close, color: kMuted),
onPressed: () => Navigator.pop(sheetContext),
tooltip: 'Tutup',
),
],
),
),
const Divider(height: 12),
Expanded(
child: SingleChildScrollView(
controller: scrollController,
padding: EdgeInsets.fromLTRB(16, 8, 16, MediaQuery.of(context).viewInsets.bottom + 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
StatefulBuilder(builder: (ctx, setSheetState) {
return productImageField(
context: context,
currentPath: imagePath,
onChanged: (p) {
imagePath = p;
setSheetState(() {});
},
);
}),
TextField(
controller: barcode,
focusNode: focusNodes[0],
decoration: InputDecoration(
labelText: 'Barcode / No. Produk',
prefixIcon: const Icon(Icons.qr_code_2_outlined),
suffixIcon: IconButton(
icon: const Icon(Icons.qr_code_scanner_outlined, color: kAccent),
onPressed: () async {
final code = await Navigator.push<String>(
context,
MaterialPageRoute(
builder: (_) => const ScanScreen(
title: 'Scan Barcode Produk',
),
),
);
if (code != null && context.mounted) {
barcode.text = code;
}
},
tooltip: 'Scan barcode',
)),
),
const SizedBox(height: 10),
TextField(
controller: name,
focusNode: focusNodes[1],
decoration: const InputDecoration(
labelText: 'Nama produk',
prefixIcon: Icon(Icons.label_outline)),
),
const SizedBox(height: 10),
Row(
children: [
Expanded(
child: TextField(
controller: price,
focusNode: focusNodes[2],
keyboardType: TextInputType.number,
inputFormatters: [RupiahInputFormatter()],
decoration: const InputDecoration(
labelText: 'Harga Umum',
prefixText: 'Rp ',
),
),
),
const SizedBox(width: 10),
Expanded(
child: TextField(
controller: priceReseller,
focusNode: focusNodes[3],
keyboardType: TextInputType.number,
inputFormatters: [RupiahInputFormatter()],
decoration: const InputDecoration(
labelText: 'Harga Reseller',
prefixText: 'Rp ',
),
),
),
],
),
const SizedBox(height: 10),
TextField(
controller: stock,
focusNode: focusNodes[4],
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: 'Stok awal',
prefixIcon: Icon(Icons.numbers),
),
),
const SizedBox(height: 10),
TextField(
controller: hargaPokokCtrl,
focusNode: focusNodes[5],
keyboardType: TextInputType.number,
inputFormatters: [RupiahInputFormatter()],
decoration: const InputDecoration(
labelText: 'Harga Pokok (modal)',
prefixText: 'Rp ',
helperText: 'Kosongkan jika tidak diketahui',
),
),
const SizedBox(height: 10),
TextField(
controller: minStockCtrl,
focusNode: focusNodes[6],
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: 'Stok minimum (warning)',
prefixIcon: Icon(Icons.warning_outlined),
helperText: 'Default: 5',
),
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: () async {
final n = name.text.trim();
final bc = barcode.text.trim();
final pr = (RupiahInputFormatter.getRawValue(price.text) ?? 0)
.toDouble();
final prR =
(RupiahInputFormatter.getRawValue(priceReseller.text) ?? pr)
.toDouble();
final st = int.tryParse(stock.text) ?? 0;
final ms = int.tryParse(minStockCtrl.text) ?? 5;
final hp = (RupiahInputFormatter.getRawValue(hargaPokokCtrl.text) ?? 0)
.toDouble();
if (n.isEmpty || pr <= 0) return;
if (!await confirmResellerWarning(context, pr, prR)) return;
final err = await state.addProduct(n, bc, pr, prR, st, ms, hp, imagePath: imagePath);
if (!context.mounted) return;
final messenger = ScaffoldMessenger.of(context);
if (err == null) {
name.clear();
barcode.clear();
price.clear();
priceReseller.clear();
stock.clear();
minStockCtrl.clear();
hargaPokokCtrl.clear();
messenger.showSnackBar(
const SnackBar(
content: Text('Produk berhasil ditambahkan'),
backgroundColor: kSuccess,
),
);
Navigator.pop(sheetContext);
} else {
messenger.showSnackBar(
SnackBar(
content: Text('Gagal menambah produk: '),
backgroundColor: kDanger,
),
);
}
},
icon: const Icon(Icons.add),
label: const Text('Tambah Produk'),
),
),
],
),
),
),
],
),
);
},
),
);
}
void _showEditDialog(BuildContext context, AppState state, Product product) {
final name = TextEditingController(text: product.name);
final barcode = TextEditingController(text: product.barcode);
final price = TextEditingController(
text: RupiahInputFormatter.formatNumber(product.price.toInt()));
final priceReseller = TextEditingController(
text: RupiahInputFormatter.formatNumber(product.priceReseller.toInt()));
final stock = TextEditingController(text: product.stock.toString());
final minStockCtrl = TextEditingController(text: product.minStock.toString());
final hargaPokokCtrl = TextEditingController(
text: product.hargaPokok > 0
? RupiahInputFormatter.formatNumber(product.hargaPokok.toInt())
: '');
String? imagePath = product.imagePath; // path gambar saat ini (null = tidak ada)
final focusNodes = <FocusNode>[
FocusNode(),
FocusNode(),
FocusNode(),
FocusNode(),
FocusNode(),
FocusNode(),
FocusNode(),
];
bool listenersAdded = false;
final sheetController = DraggableScrollableController();
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (sheetContext) {
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
return DraggableScrollableSheet(
controller: sheetController,
initialChildSize: 0.65,
minChildSize: 0.5,
maxChildSize: 0.92,
builder: (_, scrollController) {
if (!listenersAdded) {
listenersAdded = true;
for (final node in focusNodes) {
node.addListener(() {
if (node.hasFocus && context.mounted) {
sheetController.animateTo(
0.92,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
Future.delayed(const Duration(milliseconds: 350), () {
if (node.context != null && node.context!.mounted) {
Scrollable.ensureVisible(
node.context!,
duration: const Duration(milliseconds: 250),
alignment: 0.0,
);
}
});
}
});
}
}
return Container(
decoration: const BoxDecoration(
color: kSurface,
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
border: Border(top: BorderSide(color: kBorder)),
),
child: Column(
children: [
Container(
margin: const EdgeInsets.only(top: 12),
width: 40,
height: 4,
decoration: BoxDecoration(
color: kBorder,
borderRadius: BorderRadius.circular(4),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
child: Row(
children: [
const Icon(Icons.edit_outlined, color: kAccent),
const SizedBox(width: 8),
const Text('Edit Produk',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700)),
const Spacer(),
IconButton(
icon: const Icon(Icons.close, color: kMuted),
onPressed: () => Navigator.pop(sheetContext),
tooltip: 'Tutup',
),
],
),
),
const Divider(height: 12),
Expanded(
child: SingleChildScrollView(
controller: scrollController,
padding: EdgeInsets.fromLTRB(16, 8, 16, 16 + bottomInset),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
StatefulBuilder(builder: (ctx, setSheetState) {
return productImageField(
context: context,
currentPath: imagePath,
onChanged: (p) {
imagePath = p;
setSheetState(() {});
},
);
}),
TextField(
controller: barcode,
focusNode: focusNodes[0],
decoration: const InputDecoration(
labelText: 'Barcode / No. Produk',
prefixIcon: Icon(Icons.qr_code_2_outlined)),
),
const SizedBox(height: 10),
TextField(
controller: name,
focusNode: focusNodes[1],
decoration: const InputDecoration(labelText: 'Nama'),
),
const SizedBox(height: 10),
TextField(
controller: price,
focusNode: focusNodes[2],
keyboardType: TextInputType.number,
inputFormatters: [RupiahInputFormatter()],
decoration: const InputDecoration(labelText: 'Harga Umum', prefixText: 'Rp '),
),
const SizedBox(height: 10),
TextField(
controller: priceReseller,
focusNode: focusNodes[3],
keyboardType: TextInputType.number,
inputFormatters: [RupiahInputFormatter()],
decoration: const InputDecoration(labelText: 'Harga Reseller', prefixText: 'Rp '),
),
const SizedBox(height: 10),
TextField(
controller: hargaPokokCtrl,
focusNode: focusNodes[4],
keyboardType: TextInputType.number,
inputFormatters: [RupiahInputFormatter()],
decoration: const InputDecoration(
labelText: 'Harga Pokok (modal)',
prefixText: 'Rp ',
helperText: 'Kosongkan jika tidak diketahui',
),
),
const SizedBox(height: 10),
TextField(
controller: stock,
focusNode: focusNodes[5],
keyboardType: TextInputType.number,
decoration: const InputDecoration(labelText: 'Stok'),
),
const SizedBox(height: 10),
TextField(
controller: minStockCtrl,
focusNode: focusNodes[6],
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: 'Stok minimum (warning)',
helperText: 'Default: 5',
),
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: () async {
final n = name.text.trim();
final bc = barcode.text.trim();
final pr = (RupiahInputFormatter.getRawValue(price.text) ?? 0)
.toDouble();
final prR =
(RupiahInputFormatter.getRawValue(priceReseller.text) ?? pr)
.toDouble();
final st = int.tryParse(stock.text) ?? 0;
final ms = int.tryParse(minStockCtrl.text) ?? 5;
final hp = (RupiahInputFormatter.getRawValue(hargaPokokCtrl.text) ?? 0)
.toDouble();
if (n.isEmpty || pr <= 0) return;
if (!await confirmResellerWarning(context, pr, prR)) return;
final err = await state.updateProduct(product.id, n, bc, pr, prR, st, ms, hp, imagePath: imagePath);
if (!context.mounted) return;
final messenger = ScaffoldMessenger.of(context);
if (err == null) {
messenger.showSnackBar(
const SnackBar(
content: Text('Produk berhasil diperbarui'),
backgroundColor: kSuccess,
),
);
Navigator.pop(sheetContext);
} else {
messenger.showSnackBar(
SnackBar(
content: Text('Gagal memperbarui produk: '),
backgroundColor: kDanger,
),
);
}
},
icon: const Icon(Icons.save),
label: const Text('Simpan'),
),
),
],
),
),
),
],
),
);
},
);
},
);
}
class _ProductRow extends StatelessWidget {
final Product p;
final AppState state;
const _ProductRow({required this.p, required this.state});
@override
Widget build(BuildContext context) {
final out = p.stock <= 0;
final lowStock = p.stock > 0 && p.stock <= p.minStock;
return Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
CircleAvatar(
radius: 22,
backgroundColor: kAccent.withValues(alpha: 0.15),
backgroundImage: productImageUrl(p.imagePath) != null
? NetworkImage(productImageUrl(p.imagePath)!)
: null,
child: productImageUrl(p.imagePath) == null
? Text(
p.name.isNotEmpty ? p.name[0].toUpperCase() : '?',
style:
TextStyle(color: kAccent, fontWeight: FontWeight.w700),
)
: null,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(p.name,
style: const TextStyle(
fontSize: 15, fontWeight: FontWeight.w600)),
if (p.barcode.isNotEmpty) ...[
const SizedBox(height: 2),
Text(
p.barcode,
style: const TextStyle(
fontSize: 11, color: kMuted, fontFamily: 'monospace'),
),
],
const SizedBox(height: 4),
Wrap(
spacing: 10,
runSpacing: 6,
children: [
_Tag(formatRupiah(p.price), kSuccess),
if (p.priceReseller > 0)
_Tag('Rsl ${formatRupiah(p.priceReseller)}', kAccent),
if (p.hargaPokok > 0)
_Tag('Modal ${formatRupiah(p.hargaPokok)}', kAccent),
if (out)
_Tag('Stok habis', kDanger)
else if (lowStock)
_Tag('Stok rendah (${p.stock}/${p.minStock})', kAccent)
else
_Tag('Stok ${p.stock}', kMuted),
],
),
],
),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit_outlined, size: 20),
color: kAccent,
onPressed: () => _showEditDialog(context, state, p),
),
IconButton(
icon: const Icon(Icons.delete_outline, size: 20),
color: kDanger,
onPressed: () async {
HapticFeedback.mediumImpact();
final messenger = ScaffoldMessenger.of(context);
final confirm = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
icon: Icon(Icons.delete_outline, color: kDanger, size: 28),
title: const Text('Hapus Produk?'),
content: Text('Yakin ingin menghapus "${p.name}"?'),
actions: [
SizedBox(
width: double.infinity,
child: OutlinedButton(
onPressed: () => Navigator.pop(ctx, false),
style: OutlinedButton.styleFrom(
foregroundColor: kMuted,
side: const BorderSide(color: kBorder),
),
child: const Text('Batal'),
),
),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: () => Navigator.pop(ctx, true),
style: FilledButton.styleFrom(
backgroundColor: kDanger,
),
child: const Text('Hapus'),
),
),
],
),
);
if (confirm == true && context.mounted) {
final err = await state.deleteProduct(p.id);
if (err == null) {
messenger.showSnackBar(
const SnackBar(
content: Text('Produk berhasil dihapus'),
backgroundColor: kSuccess,
),
);
} else {
messenger.showSnackBar(
SnackBar(
content: Text('Gagal menghapus produk: $err'),
backgroundColor: kDanger,
),
);
}
}
},
),
],
),
],
),
),
);
}
}
class _Tag extends StatelessWidget {
final String label;
final Color color;
const _Tag(this.label, this.color);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(8),
),
child: Text(label,
style: TextStyle(
fontSize: 11, fontWeight: FontWeight.w600, color: color)),
);
}
}