DEV Community

Aris Candra Muzaffar
Aris Candra Muzaffar

Posted on

Panduan Pengujian: Implementasi Widget, Properti, dan Arsitektur SaryPOS

Dokumen ini merangkum hasil audit codebase aplikasi SaryPOS (folder sarypos/) untuk persiapan demonstrasi dan tanya jawab dengan dosen. Setiap bagian menyertakan lokasi berkas dan cuplikan kode yang dapat Anda tunjukkan langsung di IDE.

Konteks proyek: aplikasi POS (Point of Sale) ritel untuk Sary Mart, dibuat dengan Flutter dan backend Supabase (PostgreSQL + Storage), dengan pemisahan peran owner vs karyawan/kasir.


1. Ringkasan arsitektur (kalimat untuk pembuka presentasi)

SaryPOS memuat konfigurasi dan font di main, menginisialisasi Supabase, lalu menjalankan GetMaterialApp dengan tumpukan warisan (InheritedNotifier) untuk tema, sesi pengguna, dan notifikasi in-app. State utama dikelola lewat ChangeNotifier (PengaturSesi, PengaturTema, PengelolaNotifikasiInApp) dan diwariskan ke seluruh pohon widget. Navigasi tab ada di HalamanUtamaDenganNav (IndexedStack); halaman detail banyak memakai Navigator.push + MaterialPageRoute, sementara guard owner memakai GetX (Get.to, Get.dialog).


2. Stack teknologi dan dependensi utama

Area Paket / pilihan Berkas acuan
Framework Flutter (SDK Dart ^3.11.1 di pubspec.yaml) sarypos/pubspec.yaml
Backend klien supabase_flutter lib/data/sources/supabase_klien.dart
Navigasi/GetX get — terutama GetMaterialApp, Get.to, Get.dialog lib/main.dart, lib/core/penjaga_rute_owner.dart
Konfigurasi rahasia flutter_dotenv + --dart-define lib/config/supabase_konfigurasi.dart
Preferensi lokal shared_preferences (tema, “lanjut kasir tanpa owner”) lib/core/pengatur_tema.dart, lib/core/pengatur_sesi.dart
UI google_fonts, intl, flutter_typeahead, image_picker, file_picker, barcode, http pubspec.yaml
Ekspor pdf, printing, share_plus, csv, path_provider lib/core/ekspor/

State management yang dipakai di kode: tidak ada Provider/Bloc/Riverpod; yang dominan adalah ChangeNotifier + ListenableBuilder/InheritedNotifier dan GetX sebagian untuk dialog/rute owner.


3. Titik masuk aplikasi dan pemilihan “home”

Berkas: sarypos/lib/main.dart

Alur singkat:

  1. WidgetsFlutterBinding.ensureInitialized()
  2. Muat .env (bukan web; error ditelan jika berkas tidak ada)
  3. Preload font Plus Jakarta Sans
  4. inisialisasiSupabase()
  5. PengaturTema.muatAwal() lalu runApp(MainApp(...))
  6. MainApp membungkus anak dengan WarisanTemaWarisanSesiWarisanNotifikasiInAppGetMaterialApp
  7. home ditentukan oleh _pilihHome(): loading → error koneksi → pembuka owner pertama → HalamanUtamaDenganNav
Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  if (!kIsWeb) {
    try {
      await dotenv.load(fileName: '.env');
    } catch (_) {}
  }
  try {
    await GoogleFonts.pendingFonts([GoogleFonts.plusJakartaSans()]);
  } catch (_) {}

  await inisialisasiSupabase();

  final pengaturTema = PengaturTema();
  await pengaturTema.muatAwal();

  runApp(MainApp(pengaturTema: pengaturTema));
}
// ...
              child: GetMaterialApp(
                title: 'SaryPOS',
                theme: temaSaryposTerang(),
                darkTheme: temaSaryposGelap(),
                themeMode: widget.pengaturTema.modeMaterial,
                home: _pilihHome(),
                builder: (context, child) {
                  return Stack(
                    fit: StackFit.expand,
                    children: [
                      child ?? const SizedBox.shrink(),
                      const Positioned(
                        top: 0,
                        left: 0,
                        right: 0,
                        child: SafeArea(
                          bottom: false,
                          child: Padding(
                            padding: EdgeInsets.fromLTRB(16, 8, 16, 0),
                            child: BannerNotifikasiInApp(),
                          ),
                        ),
                      ),
                    ],
                  );
                },
              ),
// ...
  Widget _pilihHome() {
    if (_sesi.sedangMemeriksaSesi) {
      return const Scaffold(
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              CircularProgressIndicator(),
              SizedBox(height: 16),
              Text('Menyiapkan SaryPOS…'),
            ],
          ),
        ),
      );
    }
    if (_sesi.sedangMengalamiError) {
      return HalamanErrorKoneksi(
        pesan: _sesi.pesanErrorSesi ?? 'Gagal menyiapkan SaryPOS.',
        cobaLagi: _sesi.muatUlangSesi,
      );
    }
    if (_sesi.perluHalamanPembukaOwner) {
      return HalamanPembukaOwnerPertama(pengatur: _sesi);
    }
    return const HalamanUtamaDenganNav();
  }
}
Enter fullscreen mode Exit fullscreen mode

Properti / perilaku penting untuk ditanya dosen:

  • Tidak ada routes/onGenerateRoute — satu home dinamis.
  • Banner notifikasi selalu di atas konten lewat Stack di builder GetMaterialApp.

4. Konfigurasi Supabase (URL, anon key, service role)

Berkas: sarypos/lib/config/supabase_konfigurasi.dart

  • SUPABASE_URL dan SUPABASE_ANON_KEY wajib (dari String.fromEnvironment atau .env).
  • SUPABASE_SERVICE_ROLE_KEY opsional; jika ada, dipakai untuk operasi admin tertentu (mis. form karyawan) — ini topik sensitif keamanan untuk diskusi dengan dosen (anon key untuk klien umum; service role sebaiknya tidak tersebar di aplikasi produksi).
String _ambilKonfigurasiWajib({
  required String namaKunci,
  required String nilaiDartDefine,
}) {
  final nilaiDefine = nilaiDartDefine.trim();
  if (nilaiDefine.isNotEmpty) {
    return nilaiDefine;
  }

  final nilaiEnv = (dotenv.env[namaKunci] ?? '').trim();
  if (nilaiEnv.isNotEmpty) {
    return nilaiEnv;
  }

  throw StateError(
    'Konfigurasi `$namaKunci` belum tersedia. '
    'Isi di .env atau kirim lewat --dart-define.',
  );
}
// ...
String? get supabaseServiceRoleKey {
  final nilaiDefine = _supabaseServiceRoleDartDefine.trim();
  if (nilaiDefine.isNotEmpty) {
    return nilaiDefine;
  }
  final nilaiEnv = (dotenv.env['SUPABASE_SERVICE_ROLE_KEY'] ?? '').trim();
  if (nilaiEnv.isNotEmpty) {
    return nilaiEnv;
  }
  return null;
}
Enter fullscreen mode Exit fullscreen mode

5. Sesi pengguna (owner, kasir tanpa owner, retry)

Berkas: sarypos/lib/core/pengatur_sesi.dart

Getter penting untuk logika UI:

  • perluHalamanPembukaOwner — true jika belum ada owner aktif, belum login, dan pengguna belum memilih “lanjut kasir tanpa owner”.
  • Retry 3 kali, timeout 5 detik per percobaan (konstanta di kelas).
class PengaturSesi extends ChangeNotifier {
  PengaturSesi() {
    _inisialisasi();
  }

  static const int _maksPercobaanMuatSesi = 3;
  static const Duration _timeoutPercobaanMuatSesi = Duration(seconds: 5);

  static const _kunciPrefLanjutKasir = 'sarypos_lanjut_tanpa_owner';
// ...
  bool get perluHalamanPembukaOwner =>
      _adaOwnerAktif == false && _pengguna == null && !_lanjutKasirTanpaOwner;
Enter fullscreen mode Exit fullscreen mode

6. Warisan state: InheritedNotifier

6.1 WarisanSesi

Berkas: sarypos/lib/core/warisan_sesi.dart

class WarisanSesi extends InheritedNotifier<PengaturSesi> {
  const WarisanSesi({
    super.key,
    required PengaturSesi pengatur,
    required super.child,
  }) : super(notifier: pengatur);

  static PengaturSesi dari(BuildContext context) {
    final w = context.dependOnInheritedWidgetOfExactType<WarisanSesi>();
    assert(w != null, 'WarisanSesi tidak ditemukan di pohon widget');
    return w!.notifier!;
  }

  static PengaturSesi? mungkinDari(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<WarisanSesi>()?.notifier;
  }
}
Enter fullscreen mode Exit fullscreen mode

Jawaban singkat ujian: dependOnInheritedWidgetOfExactType membuat widget rebuild saat notifier (PengaturSesi) memanggil notifyListeners.

6.2 WarisanTema

Berkas: sarypos/lib/core/warisan_tema.dart

class WarisanTema extends InheritedNotifier<PengaturTema> {
  const WarisanTema({
    super.key,
    required PengaturTema pengatur,
    required super.child,
  }) : super(notifier: pengatur);

  static PengaturTema dari(BuildContext context) {
    final w = context.dependOnInheritedWidgetOfExactType<WarisanTema>();
    assert(w != null, 'WarisanTema tidak ditemukan di pohon widget');
    return w!.notifier!;
  }
}
Enter fullscreen mode Exit fullscreen mode

7. Penjaga akses: login dan owner

7.1 Cegah aksi jika belum login

Berkas: sarypos/lib/core/penjaga_aksi_masuk.dart

bool cegahJikaBelumLogin(
  BuildContext context, {
  String? pesan,
}) {
  final s = WarisanSesi.dari(context);
  if (s.sedangMemeriksaSesi) {
    return true;
  }
  if (s.pengguna != null) {
    return false;
  }
  tampilkanSnackbarSarypos(
    context,
    tipe: TipeSnackbarSarypos.info,
    pesan:
        pesan ??
        'Masuk ke akun terlebih dahulu untuk mengakses fitur ini.',
  );
  return true;
}
Enter fullscreen mode Exit fullscreen mode

Properti perilaku: mengembalikan true artinya aksi harus dibatalkan (caller biasanya if (cegahJikaBelumLogin(context)) return;).

7.2 Hanya owner yang boleh membuka halaman tertentu

Berkas: sarypos/lib/core/penjaga_rute_owner.dart

Future<T?> dorongJikaOwner<T extends Object?>(
  BuildContext context,
  WidgetBuilder builder, {
  String pesanDitolak =
      'Hanya pemilik toko (owner) yang dapat membuka halaman ini.',
}) async {
  final sesi = WarisanSesi.dari(context);
  final p = sesi.pengguna;
  if (p == null || !p.isOwner) {
    if (!context.mounted) {
      return null;
    }
    await Get.dialog<void>(
      AlertDialog(
        title: const Text('Akses Ditolak'),
        content: Text(pesanDitolak),
        actions: [
          TextButton(
            onPressed: () => Get.back<void>(),
            child: const Text('Oke'),
          ),
        ],
      ),
    );
    return null;
  }

  if (!context.mounted) {
    return null;
  }
  return Get.to<T>(() => builder(context));
}

bool penggunaAdalahOwner(BuildContext context) {
  return WarisanSesi.dari(context).pengguna?.isOwner ?? false;
}
Enter fullscreen mode Exit fullscreen mode

Catatan ujian: pengecekan owner memakai PenggunaModel.isOwner (peran == 'owner' di model).


8. Shell navigasi utama (tab Beranda / Kasir / Saya)

Berkas: sarypos/lib/features/dashboard/halaman_utama_dengan_nav.dart

  • IndexedStack menjaga state tiap tab tetap hidup saat pindah tab.
  • AppBar menampilkan ikon log aktivitas hanya jika tab Beranda dan pengguna owner.
  • FAB tengah mengarah ke tab Kasir.
  @override
  Widget build(BuildContext context) {
    final isKasirAktif = _indeksSaatIni == 1;
    final pemilikBeranda =
        _indeksSaatIni == 0 &&
        (WarisanSesi.dari(context).pengguna?.isOwner ?? false);
    return Scaffold(
      extendBody: true,
      appBar: AppBarSarypos(
        judul: _judulTab[_indeksSaatIni],
        aksi: pemilikBeranda
            ? [
                IconButton(
                  onPressed: () async {
                    await dorongJikaOwner(
                      context,
                      (_) => const HalamanLogAktivitas(),
                    );
                  },
                  icon: const Icon(Icons.notifications_none_outlined),
                  tooltip: 'Log Aktivitas',
                ),
              ]
            : null,
      ),
      body: IndexedStack(
        index: _indeksSaatIni,
        children: [
          HalamanDashboard(key: _kunciDashboard),
          const HalamanKasirMenu(),
          const HalamanUserPengaturan(),
        ],
      ),
Enter fullscreen mode Exit fullscreen mode

9. Tema dan palet warna merek

Berkas: sarypos/lib/config/theme/sarypos_theme.dart

Kelas WarnaSarypos memusatkan warna merek (merah Sary, teal, emas, abu hangat, hijau sukses). Fungsi temaSaryposTerang() / temaSaryposGelap() membangun ThemeData Material 3 dengan teks Plus Jakarta Sans.

class WarnaSarypos {
  static const Color saryRed = Color(0xFFD3291E);
  static const Color deepTeal = Color(0xFF1C4546);
  static const Color saryGold = Color(0xFFE4AF1A);
  static const Color cleanWhite = Color(0xFFFEFEFE);
  static const Color warmGray = Color(0xFFCDC5BB);
  static const Color darkStone = Color(0xFF6E6B61);

  static const Color hijauSukses = Color(0xFF1E8E3E);
// ...
}
Enter fullscreen mode Exit fullscreen mode

Transisi halaman: PageTransitionsTheme memakai CupertinoPageTransitionsBuilder untuk Android/iOS/macOS (geser seperti iOS).

const PageTransitionsTheme _transisiHalamanStandar = PageTransitionsTheme(
  builders: <TargetPlatform, PageTransitionsBuilder>{
    TargetPlatform.android: CupertinoPageTransitionsBuilder(),
    TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
    TargetPlatform.macOS: CupertinoPageTransitionsBuilder(),
    TargetPlatform.linux: FadeUpwardsPageTransitionsBuilder(),
    TargetPlatform.windows: FadeUpwardsPageTransitionsBuilder(),
    TargetPlatform.fuchsia: FadeUpwardsPageTransitionsBuilder(),
  },
);
Enter fullscreen mode Exit fullscreen mode

10. Widget reusable di lib/widgets/ — properti dan implementasi

10.1 CardSarypos

Berkas: sarypos/lib/widgets/card_sarypos.dart

Jenis: StatelessWidget

Properti konstruktor:

Properti Tipe Default Fungsi
child Widget wajib Isi kartu
margin EdgeInsetsGeometry? null Margin Card
borderRadius BorderRadius? 12 px all Sudut membulat
elevation double? dari cardTheme atau 2 Bayangan; jika ≤0, “datar”
onTap VoidCallback? null Jika ada, dibungkus InkWell
tampilkanKonturTipis bool false Garis tipis saat elevasi datar
class CardSarypos extends StatelessWidget {
  const CardSarypos({
    super.key,
    required this.child,
    this.margin,
    this.borderRadius,
    this.elevation,
    this.onTap,
    this.tampilkanKonturTipis = false,
  });

  final Widget child;
  final EdgeInsetsGeometry? margin;
  final BorderRadius? borderRadius;

  final double? elevation;
  final VoidCallback? onTap;

  final bool tampilkanKonturTipis;
Enter fullscreen mode Exit fullscreen mode

Ide desain: gradien latar dari cleanWhite / surfaceContainerHighest ke aksen teal transparan, dengan “blob” dekoratif emas/merah sangat halus.

10.2 CardRingkasan

Berkas: sarypos/lib/widgets/card_ringkasan.dart

Membungkus konten dengan CardSarypos(elevation: 0) dan layout baris: teks kiri, ikon bundar kanan.

Properti Tipe Keterangan
judul String Label ringkas
nilaiUtama String Angka/teks besar (mis. format rupiah)
ikon IconData Ikon ringkasan
warnaAksen Color? Opsional; default teal (terang) / secondary (gelap)
margin EdgeInsetsGeometry? Default EdgeInsets.zero
class CardRingkasan extends StatelessWidget {
  const CardRingkasan({
    super.key,
    required this.judul,
    required this.nilaiUtama,
    required this.ikon,
    this.warnaAksen,
    this.margin,
  });

  final String judul;
  final String nilaiUtama;
  final IconData ikon;
  final Color? warnaAksen;
  final EdgeInsetsGeometry? margin;
Enter fullscreen mode Exit fullscreen mode

10.3 AppBarSarypos

Berkas: sarypos/lib/widgets/appbar_sarypos.dart

Mengimplementasikan PreferredSizeWidget (preferredSize = tinggi toolbar standar).

Properti Tipe Keterangan
judul String Judul utama AppBar
aksi List<Widget>? Aksi kustom; digabung dengan tombol siklus tema

Perilaku tema: membaca WarisanTema.dari(context), siklus ikutiSistemteranggelap lewat pengaturTema.atur(...).

class AppBarSarypos extends StatelessWidget implements PreferredSizeWidget {
  const AppBarSarypos({super.key, required this.judul, this.aksi});

  final String judul;
  final List<Widget>? aksi;
Enter fullscreen mode Exit fullscreen mode

10.4 EmptyStateGenerik

Berkas: sarypos/lib/widgets/empty_state_generik.dart

Memakai LayoutBuilder untuk menyesuaikan padding dan ukuran ikon jika ruang vertikal sangat kecil (maxHeight < 90).

Properti Tipe Keterangan
ikon IconData? Opsional
judul String? Opsional
pesan String Wajib — pesan utama
labelTombol String? Perlu dipasangkan dengan onTekanTombol
onTekanTombol VoidCallback? Menampilkan ElevatedButton jika keduanya non-null
class EmptyStateGenerik extends StatelessWidget {
  const EmptyStateGenerik({
    super.key,
    this.ikon,
    this.judul,
    required this.pesan,
    this.labelTombol,
    this.onTekanTombol,
  });

  final IconData? ikon;
  final String? judul;
  final String pesan;
  final String? labelTombol;
  final VoidCallback? onTekanTombol;
Enter fullscreen mode Exit fullscreen mode

10.5 tampilkanSnackbarSarypos dan TipeSnackbarSarypos

Berkas: sarypos/lib/widgets/snackbar_sarypos.dart

Bukan widget, tetapi API imperatif untuk feedback konsisten.

  • Enum: sukses, error, info, peringatan
  • Memanggil ScaffoldMessenger.of(context).clearSnackBars() lalu showSnackBar dengan konten gradien custom (bukan warna flat default)
enum TipeSnackbarSarypos { sukses, error, info, peringatan }

void tampilkanSnackbarSarypos(
  BuildContext context, {
  required TipeSnackbarSarypos tipe,
  required String pesan,
}) {
Enter fullscreen mode Exit fullscreen mode

10.6 Skeleton loading (SkeletonBox, SkeletonLine, SkeletonCircle)

Berkas: sarypos/lib/widgets/skeleton_sarypos.dart

SkeletonBox memakai _ShimmerSkeletonCore (AnimationController.repeat) untuk gelombang sorot.

Widget Properti utama
SkeletonBox width, height, borderRadius (default 12), baseColor?, highlightColor?, duration (default 1300 ms)
SkeletonLine width, height (12), borderRadius (999)
SkeletonCircle diameter
class SkeletonBox extends StatelessWidget {
  const SkeletonBox({
    super.key,
    required this.width,
    required this.height,
    this.borderRadius = 12,
    this.baseColor,
    this.highlightColor,
    this.duration = const Duration(milliseconds: 1300),
  });

  final double width;
  final double height;
  final double borderRadius;
  final Color? baseColor;
  final Color? highlightColor;
  final Duration duration;
Enter fullscreen mode Exit fullscreen mode

10.7 BannerNotifikasiInApp

Berkas: sarypos/lib/widgets/banner_notifikasi_in_app.dart

Tanpa properti publik; membaca WarisanNotifikasiInApp.mungkinDari(context) dan ListenableBuilder pada pengelola. Tap menutup lewat pengelola.tutup().

10.8 PengelolaNotifikasiInApp (inti logika banner)

Berkas: sarypos/lib/core/pengelola_notifikasi_in_app.dart

  • tampilkan(tipe, pesan, durasi) — deduplikasi pesan identik dalam 2 detik
  • Timer otomatis menghapus notifikasi setelah durasi (default 4 detik)
class PengelolaNotifikasiInApp extends ChangeNotifier {
  ItemNotifikasiInApp? _aktif;
  Timer? _timer;
  String? _pesanTerakhir;
  DateTime? _waktuPesanTerakhir;

  ItemNotifikasiInApp? get aktif => _aktif;

  void tampilkan({
    required TipeNotifikasiInApp tipe,
    required String pesan,
    Duration durasi = const Duration(seconds: 4),
  }) {
    final sekarang = DateTime.now();
    if (_pesanTerakhir == pesan &&
        _waktuPesanTerakhir != null &&
        sekarang.difference(_waktuPesanTerakhir!) <
            const Duration(seconds: 2)) {
      return;
    }
    _pesanTerakhir = pesan;
    _waktuPesanTerakhir = sekarang;

    _timer?.cancel();
    _aktif = ItemNotifikasiInApp(
      id: '${sekarang.microsecondsSinceEpoch}',
      tipe: tipe,
      pesan: pesan,
    );
    notifyListeners();

    _timer = Timer(durasi, () {
      _aktif = null;
      notifyListeners();
    });
  }
Enter fullscreen mode Exit fullscreen mode

10.9 HalamanErrorKoneksi

Berkas: sarypos/lib/widgets/halaman_error_koneksi.dart

Properti Tipe Keterangan
pesan String Detail error untuk pengguna
cobaLagi Future<void> Function() Async callback tombol “Coba Lagi”
class HalamanErrorKoneksi extends StatelessWidget {
  const HalamanErrorKoneksi({
    super.key,
    required this.pesan,
    required this.cobaLagi,
  });

  final String pesan;
  final Future<void> Function() cobaLagi;
Enter fullscreen mode Exit fullscreen mode

11. Lapisan data: contoh model dan alur transaksi

11.1 Model pengguna dan peran owner

Berkas: sarypos/lib/data/models/pengguna_model.dart

class PenggunaModel {
  const PenggunaModel({
    required this.id,
    required this.idAuth,
    required this.namaLengkap,
    required this.email,
    required this.peran,
    required this.aktif,
    this.sandiLogin,
  });

  final String id;
  final String idAuth;
  final String namaLengkap;
  final String email;
  final String peran;
  final bool aktif;
  final String? sandiLogin;

  bool get isOwner => peran == 'owner';

  factory PenggunaModel.dariBaris(Map<String, dynamic> row) {
    return PenggunaModel(
      id: row['id'].toString(),
      idAuth: row['id_auth'].toString(),
      namaLengkap: row['nama_lengkap']?.toString() ?? '',
      email: row['email']?.toString() ?? '',
      peran: row['peran']?.toString() ?? 'karyawan',
      aktif: row['aktif'] == true,
      sandiLogin: row['sandi_login']?.toString(),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

11.2 Penyimpanan transaksi + detail + kurangi stok

Berkas: sarypos/lib/data/sources/transaksi_sumber.dart

Alur: hitung subtotal dari ItemKeranjang → validasi totalAkhirinsert ke tabel transaksiinsert batch ke detail_transaksi → loop kurangiStokSetelahPenjualan per item.

  Future<String?> simpanTransaksi({
    String? idPengguna,
    required List<ItemKeranjang> itemKeranjang,
    required String metodePembayaran,
    int? totalAkhir,
  }) async {
    if (itemKeranjang.isEmpty) {
      return null;
    }

    final subtotal = itemKeranjang.fold<int>(
      0,
      (sebelumnya, item) => sebelumnya + item.subtotal,
    );
    final total = totalAkhir ?? subtotal;
    if (total < 0 || total > subtotal) {
      throw ArgumentError(
        'totalAkhir harus antara 0 dan subtotal ($subtotal), dapat: $total',
      );
    }
    final potongan = subtotal - total;

    final barisTransaksi = <String, dynamic>{
      'waktu': DateTime.now().toUtc().toIso8601String(),
      'subtotal': subtotal,
      'potongan': potongan,
      'total': total,
      'metode_pembayaran': metodePembayaran,
    };
    if (idPengguna != null) {
      barisTransaksi['id_pengguna'] = idPengguna;
    }

    final responsTransaksi = await supabaseKlien
        .from('transaksi')
        .insert(barisTransaksi)
        .select('id')
        .single();
// ...
    await supabaseKlien.from('detail_transaksi').insert(detail);

    for (final item in itemKeranjang) {
      await _stokSumber.kurangiStokSetelahPenjualan(
        produkId: item.produk.id,
        kuantitasTerjual: item.kuantitas,
      );
    }

    return transaksiId;
  }
Enter fullscreen mode Exit fullscreen mode

11.3 Peta berkas sumber data (ringkas)

Berkas Tabel / bucket (umum)
pengguna_sumber.dart pengguna
profil_karyawan_sumber.dart profil_karyawan, bucket karyawan-foto
produk_dan_stok_sumber.dart produk, stok, bucket produk-gambar
transaksi_sumber.dart transaksi, detail_transaksi
dashboard_sumber.dart agregat dari transaksi
laporan_sumber.dart transaksi (+ relasi ke pengguna)
log_aktivitas_sumber.dart log_aktivitas
supabase_klien.dart inisialisasi + tes koneksi ke pengguna

12. Modul fitur (lib/features/) — cakupan untuk demo

Folder Layar utama Catatan ujian
auth/ Pembuka owner pertama, login, daftar owner Alur belum ada owner di DB vs lanjut kasir
dashboard/ Dashboard, shell nav Carousel, pintasan ke produk/stok/laporan
pos/ Menu kasir, POS, pencatatan transaksi Keranjang, diskon, notifikasi in-app
produk/ Daftar & form produk Upload gambar ke storage
stok/ Stok Penyesuaian kuantitas
karyawan/ Daftar & form Operasi admin (kaitkan dengan service role)
laporan/ Laporan penjualan PDF/CSV
log_aktivitas/ Log aktivitas Owner
pengaturan/ Profil/saya, tentang, panel transaksi terakhir karyawan Perbedaan menu owner vs karyawan

13. Pengujian otomatis

Berkas: sarypos/test/skeleton_sarypos_test.dart — hanya uji widget skeleton. Tidak ada test integrasi Supabase atau alur POS; ini jawaban jujur jika dosen menanyakan cakupan test.


14. Pertanyaan yang sering muncul saat pengujian (beserta jawaban singkat)

  1. Mengapa memakai IndexedStack untuk tab?

    Agar state tiap tab (mis. scroll posisi) tidak hilang saat ganti tab; hanya tab aktif yang “terlihat”, sisanya tetap di pohon widget.

  2. Bagaimana cara aplikasi tahu user owner?

    Dari baris pengguna di Supabase: field peran; di kode PenggunaModel.isOwner membandingkan dengan 'owner'.

  3. Di mana pembatasan fitur owner diimplementasikan?

    Kombinasi dorongJikaOwner, penggunaAdalahOwner, cegahJikaBelumLogin, dan kondisi UI di HalamanUtamaDenganNav / halaman pengaturan.

  4. Bagaimana state tema diwariskan?

    WarisanTema (InheritedNotifier<PengaturTema>); AppBarSarypos memanggil WarisanTema.dari(context) untuk mengubah tema.

  5. GetX dipakai untuk apa saja?

    Terutama GetMaterialApp dan navigasi/dialog di dorongJikaOwner; tidak ada pola reactive massal (Obx massal).

  6. Bagaimana alur simpan transaksi?

    TransaksiSumber.simpanTransaksi: insert header transaksi, insert lines detail_transaksi, lalu kurangi stok per produk.

  7. Di mana konfigurasi Supabase?

    supabase_konfigurasi.dart; wajib URL + anon key dari .env atau --dart-define.

  8. Apa risiko SUPABASE_SERVICE_ROLE_KEY di aplikasi?

    Kunci ini melewati RLS; untuk produksi seharusnya tidak dibundel di klien. Untuk proyek kuliah, siapkan penjelasan pembatasan dan skenario ideal (Edge Function/server).


15. Teks pegangan (ringkas, untuk dibaca sebelum masuk ruang ujian)

  • Akar widget: main.dartMainAppGetMaterialApp + Stack banner.
  • Tiga warisan: Tema (WarisanTema), Sesi (WarisanSesi), Notifikasi (WarisanNotifikasiInApp).
  • Home dinamis: loading → error (HalamanErrorKoneksi) → pembuka owner → HalamanUtamaDenganNav.
  • Tab: Beranda = HalamanDashboard, Kasir = HalamanKasirMenu, Saya = HalamanUserPengaturan.
  • Kartu UI: CardSarypos = wadah visual; CardRingkasan = pola ringkasan angka + ikon.
  • Feedback: snackbar = tampilkanSnackbarSarypos; banner atas = PengelolaNotifikasiInApp + BannerNotifikasiInApp.
  • Guard: cegahJikaBelumLogin (snackbar info); dorongJikaOwner (dialog + Get.to).
  • Data: model di lib/data/models/, akses Supabase di lib/data/sources/, klien tunggal supabaseKlien.
  • Tema warna: WarnaSarypos + temaSaryposTerang/Gelap + font Plus Jakarta.
  • Transaksi: tabel transaksi + detail_transaksi, stok dikurangi setelah sukses insert.
  • Bukti di kode: selalu bisa buka berkas yang dicantumkan di atas dan tunjukkan blok baris yang sama dengan dokumen ini.

Dokumen ini disusun berdasarkan audit struktural codebase SaryPOS pada repositori lokal Anda; jika ada perubahan signifikan setelah commit tertentu, sesuaikan referensi baris dengan mencari ulang simbol yang sama di IDE.

Top comments (0)