Selasa, 26 Agustus 2025

Aplikasi Jastip Warung Teh Ulpa (Flutter Web)


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.

Stack singkat: Flutter Web • Provider • Intl (locale id_ID) • Aset gambar lokal (statis) • Pembayaran COD (tip dibayar langsung, tidak dihitung aplikasi).

Daftar Isi

  1. Tampilan Awal (Katalog)
  2. Menu Checkout
  3. Struk Belanja
  4. Potongan Kode Penting
  5. Penutup

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.


Gambar 1. Katalog produk Warung Teh Ulpa (klik gambar produk untuk preview besar).

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.


Gambar 2. Checkout dengan ringkasan belanja & form data pembeli.

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.


Gambar 3. Struk pesanan siap ditunjukkan ke jastip.

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.

Tidak ada komentar:

Posting Komentar

WARUNG GORENGAN - SENDI

🍽️ Warung Gorengan Aplikasi Jastip Gorengan Kantin - Solusi Modern untuk Pesan...