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:
WidgetsFlutterBinding.ensureInitialized()- Muat
.env(bukan web; error ditelan jika berkas tidak ada) - Preload font Plus Jakarta Sans
inisialisasiSupabase()-
PengaturTema.muatAwal()lalurunApp(MainApp(...)) -
MainAppmembungkus anak denganWarisanTema→WarisanSesi→WarisanNotifikasiInApp→GetMaterialApp -
homeditentukan 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();
}
}
Properti / perilaku penting untuk ditanya dosen:
-
Tidak ada
routes/onGenerateRoute— satuhomedinamis. -
Banner notifikasi selalu di atas konten lewat
StackdibuilderGetMaterialApp.
4. Konfigurasi Supabase (URL, anon key, service role)
Berkas: sarypos/lib/config/supabase_konfigurasi.dart
-
SUPABASE_URLdanSUPABASE_ANON_KEYwajib (dariString.fromEnvironmentatau.env). -
SUPABASE_SERVICE_ROLE_KEYopsional; 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;
}
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;
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;
}
}
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!;
}
}
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;
}
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;
}
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
-
IndexedStackmenjaga 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(),
],
),
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);
// ...
}
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(),
},
);
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;
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;
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 ikutiSistem → terang → gelap 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;
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;
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()lalushowSnackBardengan konten gradien custom (bukan warna flat default)
enum TipeSnackbarSarypos { sukses, error, info, peringatan }
void tampilkanSnackbarSarypos(
BuildContext context, {
required TipeSnackbarSarypos tipe,
required String pesan,
}) {
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;
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();
});
}
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;
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(),
);
}
}
11.2 Penyimpanan transaksi + detail + kurangi stok
Berkas: sarypos/lib/data/sources/transaksi_sumber.dart
Alur: hitung subtotal dari ItemKeranjang → validasi totalAkhir → insert ke tabel transaksi → insert 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;
}
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)
Mengapa memakai
IndexedStackuntuk tab?
Agar state tiap tab (mis. scroll posisi) tidak hilang saat ganti tab; hanya tab aktif yang “terlihat”, sisanya tetap di pohon widget.Bagaimana cara aplikasi tahu user owner?
Dari barispenggunadi Supabase: fieldperan; di kodePenggunaModel.isOwnermembandingkan dengan'owner'.Di mana pembatasan fitur owner diimplementasikan?
KombinasidorongJikaOwner,penggunaAdalahOwner,cegahJikaBelumLogin, dan kondisi UI diHalamanUtamaDenganNav/ halaman pengaturan.Bagaimana state tema diwariskan?
WarisanTema(InheritedNotifier<PengaturTema>);AppBarSaryposmemanggilWarisanTema.dari(context)untuk mengubah tema.GetX dipakai untuk apa saja?
TerutamaGetMaterialAppdan navigasi/dialog didorongJikaOwner; tidak ada pola reactive massal (Obxmassal).Bagaimana alur simpan transaksi?
TransaksiSumber.simpanTransaksi: insert headertransaksi, insert linesdetail_transaksi, lalu kurangi stok per produk.Di mana konfigurasi Supabase?
supabase_konfigurasi.dart; wajib URL + anon key dari.envatau--dart-define.Apa risiko
SUPABASE_SERVICE_ROLE_KEYdi 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.dart→MainApp→GetMaterialApp+Stackbanner. -
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 dilib/data/sources/, klien tunggalsupabaseKlien. -
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)