Aplikasi Jastip Warung Teh Ulpa (Flutter Web)
Demo: https://zwbm067uwbn0.zapp.page/#/
Proyek ini membantu siswa/siswi SMKN 1 Banjar memesan makanan di kantin Warung Teh Ulpa via aplikasi web. Fitur utama: katalog menu, keranjang, catatan per item, checkout (wajib isi Nama/Kelas dan No. Telepon), serta struk belanja untuk ditunjukkan ke jastip/tukang.
Daftar Isi
1) Tampilan Awal (Katalog)
Pada halaman awal, pengguna melihat daftar produk dengan gambar, judul, subjudul, harga, serta kontrol tambah/kurang. Produk tertentu bergabung sebagai varian (misalnya Bala-bala / Karoket dan Gurandil / Rencang) dengan tombol pilihan varian.
2) Menu Checkout
Setelah memilih produk, pengguna masuk ke halaman checkout. Di sini tampil ringkasan pesanan, tombol edit catatan per item, dan form wajib Nama/Kelas serta Nomor Telepon. Metode pembayaran hanya COD.
3) Struk Belanja
Setelah menekan tombol Buat Struk, aplikasi menampilkan struk bergaya minimarket yang memuat Order ID, Tanggal, Nama/Kelas, No. Telepon, serta daftar item dan total. Struk ini cukup ditunjukkan ke jastip/tukang sebagai daftar belanja.
4) Potongan Kode Penting
A. Inisialisasi Locale (hindari LocaleDataException)
// main.dart (bagian atas)
import 'package:intl/intl.dart';
import 'package:intl/date_symbol_data_local.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await initializeDateFormatting('id_ID', null); // penting untuk Flutter Web
Intl.defaultLocale = 'id_ID';
runApp(const JastipApp());
}
B. Data Produk (termasuk varian gabungan)
// CatalogProvider: contoh produk + varian
final List<Product> _items = const [
Product(
id: 'gehu',
name: 'Gehu',
price: 1000,
assetPath: 'assets/images/gehu.jpg',
subtitle: 'Tahu isi, cocok pakai cabe',
),
// Bala-bala / Karoket (1 gambar, 2 varian harga sama)
Product(
id: 'balakaroket',
name: 'Bala-bala / Karoket',
assetPath: 'assets/images/karoket.jpg',
subtitle: 'Pilih salah satu',
variants: [
ProductVariant(key: 'balabala', name: 'Bala-bala', price: 1000),
ProductVariant(key: 'karoket', name: 'Karoket', price: 1000),
],
),
// Gurandil / Rencang (harga berbeda)
Product(
id: 'guranren',
name: 'Gurandil / Rencang',
assetPath: 'assets/images/rencang_gurandil.jpg',
subtitle: 'Pilih salah satu',
variants: [
ProductVariant(key: 'gurandil', name: 'Gurandil', price: 3000),
ProductVariant(key: 'rencang', name: 'Rencang', price: 3500),
],
),
];
C. Form Checkout (wajib Nama/Kelas & Nomor Telepon)
// Di CheckoutPage: form validasi data pembeli
final _formKey = GlobalKey<FormState>();
final _buyerClassCtrl = TextEditingController();
final _buyerPhoneCtrl = TextEditingController();
Form(
key: _formKey,
child: Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
children: [
TextFormField(
controller: _buyerClassCtrl,
decoration: const InputDecoration(
labelText: 'Nama/Kelas',
hintText: 'Contoh: Andi • XI RPL 2',
),
validator: (v) {
if (v == null || v.trim().isEmpty) return 'Nama/Kelas wajib diisi';
if (v.length < 3) return 'Terlalu pendek';
return null;
},
),
SizedBox(height: 12),
TextFormField(
controller: _buyerPhoneCtrl,
keyboardType: TextInputType.phone,
decoration: const InputDecoration(
labelText: 'Nomor Telepon',
hintText: 'Contoh: 08xxxxxxxxxx',
),
validator: (v) {
if (v == null || v.trim().isEmpty) return 'Nomor telepon wajib diisi';
final d = v.replaceAll(RegExp(r'[^0-9]'), '');
if (d.length < 10 || d.length > 15) return 'Nomor telepon tidak valid';
return null;
},
),
],
),
),
),
);
D. Membuat Struk & Navigasi
// Di tombol "Buat Struk" (CheckoutPage)
onPressed: () {
if (!_formKey.currentState!.validate()) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Lengkapi data pembeli dulu ya.')),
);
return;
}
final orderId = generateOrderId();
final data = OrderTicketData(
orderId: orderId,
total: cart.subtotal,
createdAt: DateTime.now(),
buyerClass: _buyerClassCtrl.text.trim(),
buyerPhone: _buyerPhoneCtrl.text.trim(),
items: cart.items.values.map((e) => TicketItem(
name: e.displayName, qty: e.qty, price: e.unitPrice, note: e.note,
)).toList(),
);
cart.clear();
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => OrderReceiptPage(data: data)),
);
},
E. Tampilan Struk (ringkas)
// OrderReceiptPage: menampilkan detail struk
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Warung Teh Ulpa', style: TextStyle(fontWeight: FontWeight.w800)),
Text('Order ID: ${data.orderId}'),
Text('Tanggal: ${formatDateTime(data.createdAt)}'),
Text('Nama/Kelas: ${data.buyerClass}'),
Text('No. Telepon: ${data.buyerPhone}'),
Divider(),
...data.items.map((e) => Row(
children: [
Expanded(child: Text('${e.name} x${e.qty}')),
Text(rupiah(e.price * e.qty)),
],
)),
Divider(),
Row(
children: [
Expanded(child: Text('Total Bayar (COD)', style: TextStyle(fontWeight: FontWeight.w800))),
Text(rupiah(data.total), style: TextStyle(fontWeight: FontWeight.w900)),
],
),
],
)
5) Penutup
Dengan pendekatan data statis + varian per kartu, catatan per item, dan struk bergaya minimarket, alur jastip menjadi jelas: pembeli memilih menu, menambahkan catatan, mengisi data, lalu menunjukkan struk ke jastip/tukang. Pembayaran COD memudahkan di lingkungan sekolah, sementara tip dibayar langsung di tempat.
Tips: Untuk menambah gambar baru, letakkan di assets/images/ dan pastikan path sama dengan di assetPath produk.


