From 1243a4f17f1974a1c09c9793150bceda50861f38 Mon Sep 17 00:00:00 2001 From: Test User Date: Tue, 16 Dec 2025 14:49:22 +0700 Subject: [PATCH 1/2] feat: Implement authentication and user role management - Added AuthService for managing authentication tokens and user data. - Integrated Dio for API calls in Login and Register pages. - Enhanced login functionality with role-based navigation. - Added loading indicators during login and registration processes. - Implemented user role checks in MarketplaceWarga for conditional UI elements. - Created ApiClient for centralized API management. - Updated main.dart to determine initial route based on authentication status. - Added CORS warning for web platforms in Login page. - Refactored LainnyaPage to include logout functionality. - Improved error handling and user feedback in login and registration processes. - Updated dependencies in pubspec.yaml for Dio and secure storage. --- .../lib/src/model/activity_create.g.dart | 131 +++++++ .../lib/src/model/cart_add.g.dart | 103 ++++++ .../lib/src/model/family_member_create.g.dart | 120 +++++++ .../lib/src/model/finance_create.g.dart | 185 ++++++++++ .../lib/src/model/login_request.g.dart | 104 ++++++ .../lib/src/model/product_create.g.dart | 128 +++++++ .../lib/src/model/profile_update.g.dart | 113 ++++++ .../lib/src/model/register_request.g.dart | 118 +++++++ .../lib/src/model/role_update.g.dart | 142 ++++++++ .../flutter_client/lib/src/serializers.g.dart | 23 ++ .../features/dashboard/warga/dashboard.dart | 39 +- .../lib/features/lainnya/admin/lainnya.dart | 50 ++- .../lainnya/admin/seller_requests_page.dart | 332 ++++++++++++++++++ .../lib/features/lainnya/profil_saya.dart | 154 ++++++-- .../lainnya/upgrade_to_seller_page.dart | 280 +++++++++++++++ .../lib/features/lainnya/warga/lainnya.dart | 23 +- apps/mobile/lib/features/login.dart | 219 ++++++++++-- .../marketplace/warga/marketplace_warga.dart | 66 ++-- .../marketplace/warga/sell_product_page.dart | 2 +- .../register/pages/warga/register_warga.dart | 110 +++++- apps/mobile/lib/main.dart | 75 +++- apps/mobile/lib/services/api_client.dart | 97 +++++ apps/mobile/lib/services/auth_service.dart | 79 +++++ .../flutter/generated_plugin_registrant.cc | 4 + .../linux/flutter/generated_plugins.cmake | 1 + .../Flutter/GeneratedPluginRegistrant.swift | 4 + apps/mobile/pubspec.lock | 207 ++++++++++- apps/mobile/pubspec.yaml | 5 + .../flutter/generated_plugin_registrant.cc | 3 + .../windows/flutter/generated_plugins.cmake | 1 + 30 files changed, 2784 insertions(+), 134 deletions(-) create mode 100644 apps/flutter_client/lib/src/model/activity_create.g.dart create mode 100644 apps/flutter_client/lib/src/model/cart_add.g.dart create mode 100644 apps/flutter_client/lib/src/model/family_member_create.g.dart create mode 100644 apps/flutter_client/lib/src/model/finance_create.g.dart create mode 100644 apps/flutter_client/lib/src/model/login_request.g.dart create mode 100644 apps/flutter_client/lib/src/model/product_create.g.dart create mode 100644 apps/flutter_client/lib/src/model/profile_update.g.dart create mode 100644 apps/flutter_client/lib/src/model/register_request.g.dart create mode 100644 apps/flutter_client/lib/src/model/role_update.g.dart create mode 100644 apps/flutter_client/lib/src/serializers.g.dart create mode 100644 apps/mobile/lib/features/lainnya/admin/seller_requests_page.dart create mode 100644 apps/mobile/lib/features/lainnya/upgrade_to_seller_page.dart create mode 100644 apps/mobile/lib/services/api_client.dart create mode 100644 apps/mobile/lib/services/auth_service.dart diff --git a/apps/flutter_client/lib/src/model/activity_create.g.dart b/apps/flutter_client/lib/src/model/activity_create.g.dart new file mode 100644 index 0000000..a01c28f --- /dev/null +++ b/apps/flutter_client/lib/src/model/activity_create.g.dart @@ -0,0 +1,131 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'activity_create.dart'; + +// ************************************************************************** +// BuiltValueGenerator +// ************************************************************************** + +class _$ActivityCreate extends ActivityCreate { + @override + final String title; + @override + final String? description; + @override + final Date date; + @override + final String? location; + + factory _$ActivityCreate([void Function(ActivityCreateBuilder)? updates]) => + (ActivityCreateBuilder()..update(updates))._build(); + + _$ActivityCreate._( + {required this.title, + this.description, + required this.date, + this.location}) + : super._(); + @override + ActivityCreate rebuild(void Function(ActivityCreateBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + ActivityCreateBuilder toBuilder() => ActivityCreateBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is ActivityCreate && + title == other.title && + description == other.description && + date == other.date && + location == other.location; + } + + @override + int get hashCode { + var _$hash = 0; + _$hash = $jc(_$hash, title.hashCode); + _$hash = $jc(_$hash, description.hashCode); + _$hash = $jc(_$hash, date.hashCode); + _$hash = $jc(_$hash, location.hashCode); + _$hash = $jf(_$hash); + return _$hash; + } + + @override + String toString() { + return (newBuiltValueToStringHelper(r'ActivityCreate') + ..add('title', title) + ..add('description', description) + ..add('date', date) + ..add('location', location)) + .toString(); + } +} + +class ActivityCreateBuilder + implements Builder { + _$ActivityCreate? _$v; + + String? _title; + String? get title => _$this._title; + set title(String? title) => _$this._title = title; + + String? _description; + String? get description => _$this._description; + set description(String? description) => _$this._description = description; + + Date? _date; + Date? get date => _$this._date; + set date(Date? date) => _$this._date = date; + + String? _location; + String? get location => _$this._location; + set location(String? location) => _$this._location = location; + + ActivityCreateBuilder() { + ActivityCreate._defaults(this); + } + + ActivityCreateBuilder get _$this { + final $v = _$v; + if ($v != null) { + _title = $v.title; + _description = $v.description; + _date = $v.date; + _location = $v.location; + _$v = null; + } + return this; + } + + @override + void replace(ActivityCreate other) { + _$v = other as _$ActivityCreate; + } + + @override + void update(void Function(ActivityCreateBuilder)? updates) { + if (updates != null) updates(this); + } + + @override + ActivityCreate build() => _build(); + + _$ActivityCreate _build() { + final _$result = _$v ?? + _$ActivityCreate._( + title: BuiltValueNullFieldError.checkNotNull( + title, r'ActivityCreate', 'title'), + description: description, + date: BuiltValueNullFieldError.checkNotNull( + date, r'ActivityCreate', 'date'), + location: location, + ); + replace(_$result); + return _$result; + } +} + +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_client/lib/src/model/cart_add.g.dart b/apps/flutter_client/lib/src/model/cart_add.g.dart new file mode 100644 index 0000000..5c15e04 --- /dev/null +++ b/apps/flutter_client/lib/src/model/cart_add.g.dart @@ -0,0 +1,103 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'cart_add.dart'; + +// ************************************************************************** +// BuiltValueGenerator +// ************************************************************************** + +class _$CartAdd extends CartAdd { + @override + final num productId; + @override + final num quantity; + + factory _$CartAdd([void Function(CartAddBuilder)? updates]) => + (CartAddBuilder()..update(updates))._build(); + + _$CartAdd._({required this.productId, required this.quantity}) : super._(); + @override + CartAdd rebuild(void Function(CartAddBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + CartAddBuilder toBuilder() => CartAddBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is CartAdd && + productId == other.productId && + quantity == other.quantity; + } + + @override + int get hashCode { + var _$hash = 0; + _$hash = $jc(_$hash, productId.hashCode); + _$hash = $jc(_$hash, quantity.hashCode); + _$hash = $jf(_$hash); + return _$hash; + } + + @override + String toString() { + return (newBuiltValueToStringHelper(r'CartAdd') + ..add('productId', productId) + ..add('quantity', quantity)) + .toString(); + } +} + +class CartAddBuilder implements Builder { + _$CartAdd? _$v; + + num? _productId; + num? get productId => _$this._productId; + set productId(num? productId) => _$this._productId = productId; + + num? _quantity; + num? get quantity => _$this._quantity; + set quantity(num? quantity) => _$this._quantity = quantity; + + CartAddBuilder() { + CartAdd._defaults(this); + } + + CartAddBuilder get _$this { + final $v = _$v; + if ($v != null) { + _productId = $v.productId; + _quantity = $v.quantity; + _$v = null; + } + return this; + } + + @override + void replace(CartAdd other) { + _$v = other as _$CartAdd; + } + + @override + void update(void Function(CartAddBuilder)? updates) { + if (updates != null) updates(this); + } + + @override + CartAdd build() => _build(); + + _$CartAdd _build() { + final _$result = _$v ?? + _$CartAdd._( + productId: BuiltValueNullFieldError.checkNotNull( + productId, r'CartAdd', 'productId'), + quantity: BuiltValueNullFieldError.checkNotNull( + quantity, r'CartAdd', 'quantity'), + ); + replace(_$result); + return _$result; + } +} + +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_client/lib/src/model/family_member_create.g.dart b/apps/flutter_client/lib/src/model/family_member_create.g.dart new file mode 100644 index 0000000..da1db43 --- /dev/null +++ b/apps/flutter_client/lib/src/model/family_member_create.g.dart @@ -0,0 +1,120 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'family_member_create.dart'; + +// ************************************************************************** +// BuiltValueGenerator +// ************************************************************************** + +class _$FamilyMemberCreate extends FamilyMemberCreate { + @override + final String name; + @override + final String relation; + @override + final Date? birthDate; + + factory _$FamilyMemberCreate( + [void Function(FamilyMemberCreateBuilder)? updates]) => + (FamilyMemberCreateBuilder()..update(updates))._build(); + + _$FamilyMemberCreate._( + {required this.name, required this.relation, this.birthDate}) + : super._(); + @override + FamilyMemberCreate rebuild( + void Function(FamilyMemberCreateBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + FamilyMemberCreateBuilder toBuilder() => + FamilyMemberCreateBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is FamilyMemberCreate && + name == other.name && + relation == other.relation && + birthDate == other.birthDate; + } + + @override + int get hashCode { + var _$hash = 0; + _$hash = $jc(_$hash, name.hashCode); + _$hash = $jc(_$hash, relation.hashCode); + _$hash = $jc(_$hash, birthDate.hashCode); + _$hash = $jf(_$hash); + return _$hash; + } + + @override + String toString() { + return (newBuiltValueToStringHelper(r'FamilyMemberCreate') + ..add('name', name) + ..add('relation', relation) + ..add('birthDate', birthDate)) + .toString(); + } +} + +class FamilyMemberCreateBuilder + implements Builder { + _$FamilyMemberCreate? _$v; + + String? _name; + String? get name => _$this._name; + set name(String? name) => _$this._name = name; + + String? _relation; + String? get relation => _$this._relation; + set relation(String? relation) => _$this._relation = relation; + + Date? _birthDate; + Date? get birthDate => _$this._birthDate; + set birthDate(Date? birthDate) => _$this._birthDate = birthDate; + + FamilyMemberCreateBuilder() { + FamilyMemberCreate._defaults(this); + } + + FamilyMemberCreateBuilder get _$this { + final $v = _$v; + if ($v != null) { + _name = $v.name; + _relation = $v.relation; + _birthDate = $v.birthDate; + _$v = null; + } + return this; + } + + @override + void replace(FamilyMemberCreate other) { + _$v = other as _$FamilyMemberCreate; + } + + @override + void update(void Function(FamilyMemberCreateBuilder)? updates) { + if (updates != null) updates(this); + } + + @override + FamilyMemberCreate build() => _build(); + + _$FamilyMemberCreate _build() { + final _$result = _$v ?? + _$FamilyMemberCreate._( + name: BuiltValueNullFieldError.checkNotNull( + name, r'FamilyMemberCreate', 'name'), + relation: BuiltValueNullFieldError.checkNotNull( + relation, r'FamilyMemberCreate', 'relation'), + birthDate: birthDate, + ); + replace(_$result); + return _$result; + } +} + +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_client/lib/src/model/finance_create.g.dart b/apps/flutter_client/lib/src/model/finance_create.g.dart new file mode 100644 index 0000000..7f5f18c --- /dev/null +++ b/apps/flutter_client/lib/src/model/finance_create.g.dart @@ -0,0 +1,185 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'finance_create.dart'; + +// ************************************************************************** +// BuiltValueGenerator +// ************************************************************************** + +const FinanceCreateTypeEnum _$financeCreateTypeEnum_income = + const FinanceCreateTypeEnum._('income'); +const FinanceCreateTypeEnum _$financeCreateTypeEnum_expense = + const FinanceCreateTypeEnum._('expense'); + +FinanceCreateTypeEnum _$financeCreateTypeEnumValueOf(String name) { + switch (name) { + case 'income': + return _$financeCreateTypeEnum_income; + case 'expense': + return _$financeCreateTypeEnum_expense; + default: + throw ArgumentError(name); + } +} + +final BuiltSet _$financeCreateTypeEnumValues = + BuiltSet(const [ + _$financeCreateTypeEnum_income, + _$financeCreateTypeEnum_expense, +]); + +Serializer _$financeCreateTypeEnumSerializer = + _$FinanceCreateTypeEnumSerializer(); + +class _$FinanceCreateTypeEnumSerializer + implements PrimitiveSerializer { + static const Map _toWire = const { + 'income': 'income', + 'expense': 'expense', + }; + static const Map _fromWire = const { + 'income': 'income', + 'expense': 'expense', + }; + + @override + final Iterable types = const [FinanceCreateTypeEnum]; + @override + final String wireName = 'FinanceCreateTypeEnum'; + + @override + Object serialize(Serializers serializers, FinanceCreateTypeEnum object, + {FullType specifiedType = FullType.unspecified}) => + _toWire[object.name] ?? object.name; + + @override + FinanceCreateTypeEnum deserialize(Serializers serializers, Object serialized, + {FullType specifiedType = FullType.unspecified}) => + FinanceCreateTypeEnum.valueOf( + _fromWire[serialized] ?? (serialized is String ? serialized : '')); +} + +class _$FinanceCreate extends FinanceCreate { + @override + final FinanceCreateTypeEnum type; + @override + final num amount; + @override + final String? description; + @override + final Date date; + + factory _$FinanceCreate([void Function(FinanceCreateBuilder)? updates]) => + (FinanceCreateBuilder()..update(updates))._build(); + + _$FinanceCreate._( + {required this.type, + required this.amount, + this.description, + required this.date}) + : super._(); + @override + FinanceCreate rebuild(void Function(FinanceCreateBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + FinanceCreateBuilder toBuilder() => FinanceCreateBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is FinanceCreate && + type == other.type && + amount == other.amount && + description == other.description && + date == other.date; + } + + @override + int get hashCode { + var _$hash = 0; + _$hash = $jc(_$hash, type.hashCode); + _$hash = $jc(_$hash, amount.hashCode); + _$hash = $jc(_$hash, description.hashCode); + _$hash = $jc(_$hash, date.hashCode); + _$hash = $jf(_$hash); + return _$hash; + } + + @override + String toString() { + return (newBuiltValueToStringHelper(r'FinanceCreate') + ..add('type', type) + ..add('amount', amount) + ..add('description', description) + ..add('date', date)) + .toString(); + } +} + +class FinanceCreateBuilder + implements Builder { + _$FinanceCreate? _$v; + + FinanceCreateTypeEnum? _type; + FinanceCreateTypeEnum? get type => _$this._type; + set type(FinanceCreateTypeEnum? type) => _$this._type = type; + + num? _amount; + num? get amount => _$this._amount; + set amount(num? amount) => _$this._amount = amount; + + String? _description; + String? get description => _$this._description; + set description(String? description) => _$this._description = description; + + Date? _date; + Date? get date => _$this._date; + set date(Date? date) => _$this._date = date; + + FinanceCreateBuilder() { + FinanceCreate._defaults(this); + } + + FinanceCreateBuilder get _$this { + final $v = _$v; + if ($v != null) { + _type = $v.type; + _amount = $v.amount; + _description = $v.description; + _date = $v.date; + _$v = null; + } + return this; + } + + @override + void replace(FinanceCreate other) { + _$v = other as _$FinanceCreate; + } + + @override + void update(void Function(FinanceCreateBuilder)? updates) { + if (updates != null) updates(this); + } + + @override + FinanceCreate build() => _build(); + + _$FinanceCreate _build() { + final _$result = _$v ?? + _$FinanceCreate._( + type: BuiltValueNullFieldError.checkNotNull( + type, r'FinanceCreate', 'type'), + amount: BuiltValueNullFieldError.checkNotNull( + amount, r'FinanceCreate', 'amount'), + description: description, + date: BuiltValueNullFieldError.checkNotNull( + date, r'FinanceCreate', 'date'), + ); + replace(_$result); + return _$result; + } +} + +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_client/lib/src/model/login_request.g.dart b/apps/flutter_client/lib/src/model/login_request.g.dart new file mode 100644 index 0000000..341e51c --- /dev/null +++ b/apps/flutter_client/lib/src/model/login_request.g.dart @@ -0,0 +1,104 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'login_request.dart'; + +// ************************************************************************** +// BuiltValueGenerator +// ************************************************************************** + +class _$LoginRequest extends LoginRequest { + @override + final String email; + @override + final String password; + + factory _$LoginRequest([void Function(LoginRequestBuilder)? updates]) => + (LoginRequestBuilder()..update(updates))._build(); + + _$LoginRequest._({required this.email, required this.password}) : super._(); + @override + LoginRequest rebuild(void Function(LoginRequestBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + LoginRequestBuilder toBuilder() => LoginRequestBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is LoginRequest && + email == other.email && + password == other.password; + } + + @override + int get hashCode { + var _$hash = 0; + _$hash = $jc(_$hash, email.hashCode); + _$hash = $jc(_$hash, password.hashCode); + _$hash = $jf(_$hash); + return _$hash; + } + + @override + String toString() { + return (newBuiltValueToStringHelper(r'LoginRequest') + ..add('email', email) + ..add('password', password)) + .toString(); + } +} + +class LoginRequestBuilder + implements Builder { + _$LoginRequest? _$v; + + String? _email; + String? get email => _$this._email; + set email(String? email) => _$this._email = email; + + String? _password; + String? get password => _$this._password; + set password(String? password) => _$this._password = password; + + LoginRequestBuilder() { + LoginRequest._defaults(this); + } + + LoginRequestBuilder get _$this { + final $v = _$v; + if ($v != null) { + _email = $v.email; + _password = $v.password; + _$v = null; + } + return this; + } + + @override + void replace(LoginRequest other) { + _$v = other as _$LoginRequest; + } + + @override + void update(void Function(LoginRequestBuilder)? updates) { + if (updates != null) updates(this); + } + + @override + LoginRequest build() => _build(); + + _$LoginRequest _build() { + final _$result = _$v ?? + _$LoginRequest._( + email: BuiltValueNullFieldError.checkNotNull( + email, r'LoginRequest', 'email'), + password: BuiltValueNullFieldError.checkNotNull( + password, r'LoginRequest', 'password'), + ); + replace(_$result); + return _$result; + } +} + +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_client/lib/src/model/product_create.g.dart b/apps/flutter_client/lib/src/model/product_create.g.dart new file mode 100644 index 0000000..6249449 --- /dev/null +++ b/apps/flutter_client/lib/src/model/product_create.g.dart @@ -0,0 +1,128 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'product_create.dart'; + +// ************************************************************************** +// BuiltValueGenerator +// ************************************************************************** + +class _$ProductCreate extends ProductCreate { + @override + final String name; + @override + final String? description; + @override + final num price; + @override + final num? stock; + + factory _$ProductCreate([void Function(ProductCreateBuilder)? updates]) => + (ProductCreateBuilder()..update(updates))._build(); + + _$ProductCreate._( + {required this.name, this.description, required this.price, this.stock}) + : super._(); + @override + ProductCreate rebuild(void Function(ProductCreateBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + ProductCreateBuilder toBuilder() => ProductCreateBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is ProductCreate && + name == other.name && + description == other.description && + price == other.price && + stock == other.stock; + } + + @override + int get hashCode { + var _$hash = 0; + _$hash = $jc(_$hash, name.hashCode); + _$hash = $jc(_$hash, description.hashCode); + _$hash = $jc(_$hash, price.hashCode); + _$hash = $jc(_$hash, stock.hashCode); + _$hash = $jf(_$hash); + return _$hash; + } + + @override + String toString() { + return (newBuiltValueToStringHelper(r'ProductCreate') + ..add('name', name) + ..add('description', description) + ..add('price', price) + ..add('stock', stock)) + .toString(); + } +} + +class ProductCreateBuilder + implements Builder { + _$ProductCreate? _$v; + + String? _name; + String? get name => _$this._name; + set name(String? name) => _$this._name = name; + + String? _description; + String? get description => _$this._description; + set description(String? description) => _$this._description = description; + + num? _price; + num? get price => _$this._price; + set price(num? price) => _$this._price = price; + + num? _stock; + num? get stock => _$this._stock; + set stock(num? stock) => _$this._stock = stock; + + ProductCreateBuilder() { + ProductCreate._defaults(this); + } + + ProductCreateBuilder get _$this { + final $v = _$v; + if ($v != null) { + _name = $v.name; + _description = $v.description; + _price = $v.price; + _stock = $v.stock; + _$v = null; + } + return this; + } + + @override + void replace(ProductCreate other) { + _$v = other as _$ProductCreate; + } + + @override + void update(void Function(ProductCreateBuilder)? updates) { + if (updates != null) updates(this); + } + + @override + ProductCreate build() => _build(); + + _$ProductCreate _build() { + final _$result = _$v ?? + _$ProductCreate._( + name: BuiltValueNullFieldError.checkNotNull( + name, r'ProductCreate', 'name'), + description: description, + price: BuiltValueNullFieldError.checkNotNull( + price, r'ProductCreate', 'price'), + stock: stock, + ); + replace(_$result); + return _$result; + } +} + +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_client/lib/src/model/profile_update.g.dart b/apps/flutter_client/lib/src/model/profile_update.g.dart new file mode 100644 index 0000000..6accf64 --- /dev/null +++ b/apps/flutter_client/lib/src/model/profile_update.g.dart @@ -0,0 +1,113 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'profile_update.dart'; + +// ************************************************************************** +// BuiltValueGenerator +// ************************************************************************** + +class _$ProfileUpdate extends ProfileUpdate { + @override + final String? address; + @override + final String? phone; + @override + final Date? birthDate; + + factory _$ProfileUpdate([void Function(ProfileUpdateBuilder)? updates]) => + (ProfileUpdateBuilder()..update(updates))._build(); + + _$ProfileUpdate._({this.address, this.phone, this.birthDate}) : super._(); + @override + ProfileUpdate rebuild(void Function(ProfileUpdateBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + ProfileUpdateBuilder toBuilder() => ProfileUpdateBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is ProfileUpdate && + address == other.address && + phone == other.phone && + birthDate == other.birthDate; + } + + @override + int get hashCode { + var _$hash = 0; + _$hash = $jc(_$hash, address.hashCode); + _$hash = $jc(_$hash, phone.hashCode); + _$hash = $jc(_$hash, birthDate.hashCode); + _$hash = $jf(_$hash); + return _$hash; + } + + @override + String toString() { + return (newBuiltValueToStringHelper(r'ProfileUpdate') + ..add('address', address) + ..add('phone', phone) + ..add('birthDate', birthDate)) + .toString(); + } +} + +class ProfileUpdateBuilder + implements Builder { + _$ProfileUpdate? _$v; + + String? _address; + String? get address => _$this._address; + set address(String? address) => _$this._address = address; + + String? _phone; + String? get phone => _$this._phone; + set phone(String? phone) => _$this._phone = phone; + + Date? _birthDate; + Date? get birthDate => _$this._birthDate; + set birthDate(Date? birthDate) => _$this._birthDate = birthDate; + + ProfileUpdateBuilder() { + ProfileUpdate._defaults(this); + } + + ProfileUpdateBuilder get _$this { + final $v = _$v; + if ($v != null) { + _address = $v.address; + _phone = $v.phone; + _birthDate = $v.birthDate; + _$v = null; + } + return this; + } + + @override + void replace(ProfileUpdate other) { + _$v = other as _$ProfileUpdate; + } + + @override + void update(void Function(ProfileUpdateBuilder)? updates) { + if (updates != null) updates(this); + } + + @override + ProfileUpdate build() => _build(); + + _$ProfileUpdate _build() { + final _$result = _$v ?? + _$ProfileUpdate._( + address: address, + phone: phone, + birthDate: birthDate, + ); + replace(_$result); + return _$result; + } +} + +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_client/lib/src/model/register_request.g.dart b/apps/flutter_client/lib/src/model/register_request.g.dart new file mode 100644 index 0000000..92c0001 --- /dev/null +++ b/apps/flutter_client/lib/src/model/register_request.g.dart @@ -0,0 +1,118 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'register_request.dart'; + +// ************************************************************************** +// BuiltValueGenerator +// ************************************************************************** + +class _$RegisterRequest extends RegisterRequest { + @override + final String name; + @override + final String email; + @override + final String password; + + factory _$RegisterRequest([void Function(RegisterRequestBuilder)? updates]) => + (RegisterRequestBuilder()..update(updates))._build(); + + _$RegisterRequest._( + {required this.name, required this.email, required this.password}) + : super._(); + @override + RegisterRequest rebuild(void Function(RegisterRequestBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + RegisterRequestBuilder toBuilder() => RegisterRequestBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is RegisterRequest && + name == other.name && + email == other.email && + password == other.password; + } + + @override + int get hashCode { + var _$hash = 0; + _$hash = $jc(_$hash, name.hashCode); + _$hash = $jc(_$hash, email.hashCode); + _$hash = $jc(_$hash, password.hashCode); + _$hash = $jf(_$hash); + return _$hash; + } + + @override + String toString() { + return (newBuiltValueToStringHelper(r'RegisterRequest') + ..add('name', name) + ..add('email', email) + ..add('password', password)) + .toString(); + } +} + +class RegisterRequestBuilder + implements Builder { + _$RegisterRequest? _$v; + + String? _name; + String? get name => _$this._name; + set name(String? name) => _$this._name = name; + + String? _email; + String? get email => _$this._email; + set email(String? email) => _$this._email = email; + + String? _password; + String? get password => _$this._password; + set password(String? password) => _$this._password = password; + + RegisterRequestBuilder() { + RegisterRequest._defaults(this); + } + + RegisterRequestBuilder get _$this { + final $v = _$v; + if ($v != null) { + _name = $v.name; + _email = $v.email; + _password = $v.password; + _$v = null; + } + return this; + } + + @override + void replace(RegisterRequest other) { + _$v = other as _$RegisterRequest; + } + + @override + void update(void Function(RegisterRequestBuilder)? updates) { + if (updates != null) updates(this); + } + + @override + RegisterRequest build() => _build(); + + _$RegisterRequest _build() { + final _$result = _$v ?? + _$RegisterRequest._( + name: BuiltValueNullFieldError.checkNotNull( + name, r'RegisterRequest', 'name'), + email: BuiltValueNullFieldError.checkNotNull( + email, r'RegisterRequest', 'email'), + password: BuiltValueNullFieldError.checkNotNull( + password, r'RegisterRequest', 'password'), + ); + replace(_$result); + return _$result; + } +} + +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_client/lib/src/model/role_update.g.dart b/apps/flutter_client/lib/src/model/role_update.g.dart new file mode 100644 index 0000000..a41865f --- /dev/null +++ b/apps/flutter_client/lib/src/model/role_update.g.dart @@ -0,0 +1,142 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'role_update.dart'; + +// ************************************************************************** +// BuiltValueGenerator +// ************************************************************************** + +const RoleUpdateRoleEnum _$roleUpdateRoleEnum_buyer = + const RoleUpdateRoleEnum._('buyer'); +const RoleUpdateRoleEnum _$roleUpdateRoleEnum_seller = + const RoleUpdateRoleEnum._('seller'); + +RoleUpdateRoleEnum _$roleUpdateRoleEnumValueOf(String name) { + switch (name) { + case 'buyer': + return _$roleUpdateRoleEnum_buyer; + case 'seller': + return _$roleUpdateRoleEnum_seller; + default: + throw ArgumentError(name); + } +} + +final BuiltSet _$roleUpdateRoleEnumValues = + BuiltSet(const [ + _$roleUpdateRoleEnum_buyer, + _$roleUpdateRoleEnum_seller, +]); + +Serializer _$roleUpdateRoleEnumSerializer = + _$RoleUpdateRoleEnumSerializer(); + +class _$RoleUpdateRoleEnumSerializer + implements PrimitiveSerializer { + static const Map _toWire = const { + 'buyer': 'Buyer', + 'seller': 'Seller', + }; + static const Map _fromWire = const { + 'Buyer': 'buyer', + 'Seller': 'seller', + }; + + @override + final Iterable types = const [RoleUpdateRoleEnum]; + @override + final String wireName = 'RoleUpdateRoleEnum'; + + @override + Object serialize(Serializers serializers, RoleUpdateRoleEnum object, + {FullType specifiedType = FullType.unspecified}) => + _toWire[object.name] ?? object.name; + + @override + RoleUpdateRoleEnum deserialize(Serializers serializers, Object serialized, + {FullType specifiedType = FullType.unspecified}) => + RoleUpdateRoleEnum.valueOf( + _fromWire[serialized] ?? (serialized is String ? serialized : '')); +} + +class _$RoleUpdate extends RoleUpdate { + @override + final RoleUpdateRoleEnum role; + + factory _$RoleUpdate([void Function(RoleUpdateBuilder)? updates]) => + (RoleUpdateBuilder()..update(updates))._build(); + + _$RoleUpdate._({required this.role}) : super._(); + @override + RoleUpdate rebuild(void Function(RoleUpdateBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + RoleUpdateBuilder toBuilder() => RoleUpdateBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is RoleUpdate && role == other.role; + } + + @override + int get hashCode { + var _$hash = 0; + _$hash = $jc(_$hash, role.hashCode); + _$hash = $jf(_$hash); + return _$hash; + } + + @override + String toString() { + return (newBuiltValueToStringHelper(r'RoleUpdate')..add('role', role)) + .toString(); + } +} + +class RoleUpdateBuilder implements Builder { + _$RoleUpdate? _$v; + + RoleUpdateRoleEnum? _role; + RoleUpdateRoleEnum? get role => _$this._role; + set role(RoleUpdateRoleEnum? role) => _$this._role = role; + + RoleUpdateBuilder() { + RoleUpdate._defaults(this); + } + + RoleUpdateBuilder get _$this { + final $v = _$v; + if ($v != null) { + _role = $v.role; + _$v = null; + } + return this; + } + + @override + void replace(RoleUpdate other) { + _$v = other as _$RoleUpdate; + } + + @override + void update(void Function(RoleUpdateBuilder)? updates) { + if (updates != null) updates(this); + } + + @override + RoleUpdate build() => _build(); + + _$RoleUpdate _build() { + final _$result = _$v ?? + _$RoleUpdate._( + role: BuiltValueNullFieldError.checkNotNull( + role, r'RoleUpdate', 'role'), + ); + replace(_$result); + return _$result; + } +} + +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_client/lib/src/serializers.g.dart b/apps/flutter_client/lib/src/serializers.g.dart new file mode 100644 index 0000000..cb8b11c --- /dev/null +++ b/apps/flutter_client/lib/src/serializers.g.dart @@ -0,0 +1,23 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'serializers.dart'; + +// ************************************************************************** +// BuiltValueGenerator +// ************************************************************************** + +Serializers _$serializers = (Serializers().toBuilder() + ..add(ActivityCreate.serializer) + ..add(CartAdd.serializer) + ..add(FamilyMemberCreate.serializer) + ..add(FinanceCreate.serializer) + ..add(FinanceCreateTypeEnum.serializer) + ..add(LoginRequest.serializer) + ..add(ProductCreate.serializer) + ..add(ProfileUpdate.serializer) + ..add(RegisterRequest.serializer) + ..add(RoleUpdate.serializer) + ..add(RoleUpdateRoleEnum.serializer)) + .build(); + +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/mobile/lib/features/dashboard/warga/dashboard.dart b/apps/mobile/lib/features/dashboard/warga/dashboard.dart index f951804..bbc12ec 100644 --- a/apps/mobile/lib/features/dashboard/warga/dashboard.dart +++ b/apps/mobile/lib/features/dashboard/warga/dashboard.dart @@ -5,10 +5,33 @@ import 'chart_demografi.dart'; import 'kegiatan_terdekat.dart'; import 'chart_keuangan.dart'; import '../../../models/stat_card.dart'; +import '../../../services/auth_service.dart'; -class WargaDashboard extends StatelessWidget { +class WargaDashboard extends StatefulWidget { const WargaDashboard({super.key}); + @override + State createState() => _WargaDashboardState(); +} + +class _WargaDashboardState extends State { + String userName = "Warga"; + + @override + void initState() { + super.initState(); + _loadUserData(); + } + + Future _loadUserData() async { + final userData = await AuthService().getUserData(); + if (userData != null && mounted) { + setState(() { + userName = userData['name'] ?? 'Warga'; + }); + } + } + @override Widget build(BuildContext context) { const Color cyan = Color(0xFF00AFC1); @@ -22,7 +45,6 @@ class WargaDashboard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // ==================================================== // HEADER // ==================================================== @@ -32,10 +54,7 @@ class WargaDashboard extends StatelessWidget { decoration: BoxDecoration( color: cyan, gradient: LinearGradient( - colors: [ - cyan, - cyan.withOpacity(0.88), - ], + colors: [cyan, cyan.withOpacity(0.88)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), @@ -48,7 +67,6 @@ class WargaDashboard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( "Selamat Datang", style: TextStyle(color: Colors.white, fontSize: 14), @@ -56,9 +74,9 @@ class WargaDashboard extends StatelessWidget { const SizedBox(height: 4), - const Text( - "Halo, Warga šŸ‘‹", - style: TextStyle( + Text( + "Halo, $userName šŸ‘‹", + style: const TextStyle( color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold, @@ -143,4 +161,3 @@ class WargaDashboard extends StatelessWidget { ); } } - diff --git a/apps/mobile/lib/features/lainnya/admin/lainnya.dart b/apps/mobile/lib/features/lainnya/admin/lainnya.dart index d59a7bc..ae93e63 100644 --- a/apps/mobile/lib/features/lainnya/admin/lainnya.dart +++ b/apps/mobile/lib/features/lainnya/admin/lainnya.dart @@ -4,6 +4,9 @@ import '../widgets/lainnya_widgets.dart'; import '../profil_saya.dart'; import '../kegiatan_warga.dart'; import '../notifikasi.dart'; +import '../../../services/auth_service.dart'; +import '../../../routes/app_routes.dart'; +import 'seller_requests_page.dart'; class LainnyaPage extends StatelessWidget { const LainnyaPage({super.key}); @@ -88,6 +91,32 @@ class LainnyaPage extends StatelessWidget { ], ), + const SizedBox(height: 12), + + Row( + children: [ + Expanded( + child: MenuIconCard( + icon: Icons.store, + label: "Seller Request", + color: Colors.green, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const SellerRequestsPage(), + ), + ); + }, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Container(), // Placeholder + ), + ], + ), + const SizedBox(height: 24), // Akun Section @@ -147,10 +176,7 @@ class LainnyaPage extends StatelessWidget { const SizedBox(height: 4), Text( "Versi 1.0.0", - style: TextStyle( - color: Colors.grey[500], - fontSize: 12, - ), + style: TextStyle(color: Colors.grey[500], fontSize: 12), ), ], ), @@ -165,9 +191,7 @@ class LainnyaPage extends StatelessWidget { showDialog( context: context, builder: (context) => Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), child: Padding( padding: const EdgeInsets.all(24), child: Column( @@ -230,9 +254,17 @@ class LainnyaPage extends StatelessWidget { const SizedBox(width: 12), Expanded( child: ElevatedButton( - onPressed: () { + onPressed: () async { Navigator.pop(context); - // Implement logout logic here + // Logout logic + await AuthService().clearAuth(); + if (context.mounted) { + Navigator.pushNamedAndRemoveUntil( + context, + AppRoutes.login, + (route) => false, + ); + } }, style: ElevatedButton.styleFrom( backgroundColor: Colors.red, diff --git a/apps/mobile/lib/features/lainnya/admin/seller_requests_page.dart b/apps/mobile/lib/features/lainnya/admin/seller_requests_page.dart new file mode 100644 index 0000000..e3ebf05 --- /dev/null +++ b/apps/mobile/lib/features/lainnya/admin/seller_requests_page.dart @@ -0,0 +1,332 @@ +import 'package:flutter/material.dart'; +import '../../../services/api_client.dart'; + +class SellerRequestsPage extends StatefulWidget { + const SellerRequestsPage({super.key}); + + @override + State createState() => _SellerRequestsPageState(); +} + +class _SellerRequestsPageState extends State { + List> _requests = []; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _loadRequests(); + } + + Future _loadRequests() async { + setState(() => _isLoading = true); + try { + final response = await ApiClient().dio.get('/admin/seller-requests'); + if (response.statusCode == 200) { + setState(() { + _requests = List>.from(response.data ?? []); + _isLoading = false; + }); + } + } catch (e) { + setState(() => _isLoading = false); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Gagal memuat data: ${e.toString()}'), + backgroundColor: Colors.red, + ), + ); + } + } + } + + Future _approveRequest(int requestId, int userId) async { + try { + await ApiClient().dio.post( + '/admin/approve-seller/$userId', + data: {'requestId': requestId}, + ); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Permohonan disetujui!'), + backgroundColor: Colors.green, + ), + ); + _loadRequests(); // Reload data + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Gagal menyetujui: ${e.toString()}'), + backgroundColor: Colors.red, + ), + ); + } + } + } + + Future _rejectRequest(int requestId) async { + try { + await ApiClient().dio.post('/admin/reject-seller/$requestId'); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Permohonan ditolak'), + backgroundColor: Colors.orange, + ), + ); + _loadRequests(); // Reload data + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Gagal menolak: ${e.toString()}'), + backgroundColor: Colors.red, + ), + ); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF7F9FB), + appBar: AppBar( + elevation: 0, + backgroundColor: Colors.white, + foregroundColor: Colors.black, + title: const Text('Permohonan Seller'), + ), + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _requests.isEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.inbox_outlined, size: 80, color: Colors.grey[400]), + const SizedBox(height: 16), + Text( + 'Tidak ada permohonan', + style: TextStyle(fontSize: 16, color: Colors.grey[600]), + ), + ], + ), + ) + : RefreshIndicator( + onRefresh: _loadRequests, + child: ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: _requests.length, + itemBuilder: (context, index) { + final request = _requests[index]; + return _buildRequestCard(request); + }, + ), + ), + ); + } + + Widget _buildRequestCard(Map request) { + final requestId = request['id'] ?? 0; + final userId = request['userId'] ?? 0; + final userName = request['userName'] ?? 'Unknown'; + final userEmail = request['email'] ?? ''; + final businessName = request['businessName'] ?? ''; + final businessAddress = request['businessAddress'] ?? ''; + final businessDesc = request['businessDescription'] ?? ''; + final phone = request['phone'] ?? ''; + final status = request['status'] ?? 'pending'; + + return Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFF00BCD4).withOpacity(0.1), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: const BoxDecoration( + color: Color(0xFF00BCD4), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.person, + color: Colors.white, + size: 24, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + userName, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF1E1E1E), + ), + ), + const SizedBox(height: 4), + Text( + userEmail, + style: TextStyle(fontSize: 13, color: Colors.grey[600]), + ), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: status == 'pending' + ? Colors.orange + : status == 'approved' + ? Colors.green + : Colors.red, + borderRadius: BorderRadius.circular(20), + ), + child: Text( + status == 'pending' + ? 'Pending' + : status == 'approved' + ? 'Disetujui' + : 'Ditolak', + style: const TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + + // Content + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildInfoRow('Nama Usaha', businessName, Icons.store), + const SizedBox(height: 12), + _buildInfoRow('Telepon', phone, Icons.phone), + const SizedBox(height: 12), + _buildInfoRow('Alamat', businessAddress, Icons.location_on), + const SizedBox(height: 12), + _buildInfoRow('Deskripsi', businessDesc, Icons.description), + + if (status == 'pending') ...[ + const SizedBox(height: 20), + const Divider(), + const SizedBox(height: 12), + + // Action Buttons + Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: () => _rejectRequest(requestId), + icon: const Icon(Icons.close, size: 18), + label: const Text('Tolak'), + style: OutlinedButton.styleFrom( + foregroundColor: Colors.red, + side: const BorderSide(color: Colors.red, width: 2), + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton.icon( + onPressed: () => _approveRequest(requestId, userId), + icon: const Icon(Icons.check, size: 18), + label: const Text('Setujui'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ), + ), + ], + ), + ], + ], + ), + ), + ], + ), + ); + } + + Widget _buildInfoRow(String label, String value, IconData icon) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(icon, size: 18, color: Colors.grey[600]), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle(fontSize: 12, color: Colors.grey[600]), + ), + const SizedBox(height: 2), + Text( + value, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Color(0xFF1E1E1E), + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/apps/mobile/lib/features/lainnya/profil_saya.dart b/apps/mobile/lib/features/lainnya/profil_saya.dart index 4d8edac..f9ea154 100644 --- a/apps/mobile/lib/features/lainnya/profil_saya.dart +++ b/apps/mobile/lib/features/lainnya/profil_saya.dart @@ -1,11 +1,69 @@ import 'package:flutter/material.dart'; import 'ubah_kata_sandi.dart'; +import '../../services/auth_service.dart'; +import 'upgrade_to_seller_page.dart'; -class ProfilSayaPage extends StatelessWidget { +class ProfilSayaPage extends StatefulWidget { const ProfilSayaPage({super.key}); + @override + State createState() => _ProfilSayaPageState(); +} + +class _ProfilSayaPageState extends State { + Map? userData; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _loadUserData(); + } + + Future _loadUserData() async { + final data = await AuthService().getUserData(); + if (mounted) { + setState(() { + userData = data; + _isLoading = false; + }); + } + } + + String _getRoleBadgeText() { + final role = userData?['role'] ?? 'Buyer'; + if (role == 'Admin' || role == 'RT' || role == 'RW') return 'Admin'; + if (role == 'Seller') return 'Seller'; + return 'Buyer'; + } + + Color _getRoleBadgeColor() { + final role = userData?['role'] ?? 'Buyer'; + if (role == 'Admin' || role == 'RT' || role == 'RW') return Colors.red; + if (role == 'Seller') return Colors.green; + return Colors.blue; + } + @override Widget build(BuildContext context) { + if (_isLoading) { + return Scaffold( + backgroundColor: const Color(0xFFF7F9FB), + appBar: AppBar( + elevation: 0, + backgroundColor: Colors.white, + foregroundColor: Colors.black, + centerTitle: true, + title: const Text("Profil Saya"), + ), + body: const Center(child: CircularProgressIndicator()), + ); + } + + final userName = userData?['name'] ?? 'User'; + final userEmail = userData?['email'] ?? ''; + final userRole = userData?['role'] ?? 'Buyer'; + return Scaffold( backgroundColor: const Color(0xFFF7F9FB), appBar: AppBar( @@ -54,20 +112,31 @@ class ProfilSayaPage extends StatelessWidget { ), ), const SizedBox(height: 16), - const Text( - "Ketua RT 03", - style: TextStyle( + Text( + userName, + style: const TextStyle( color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold, ), ), - const SizedBox(height: 4), - Text( - "RT 03 / RW 05", - style: TextStyle( - color: Colors.white.withOpacity(0.9), - fontSize: 14, + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 6, + ), + decoration: BoxDecoration( + color: _getRoleBadgeColor(), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + _getRoleBadgeText(), + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.bold, + ), ), ), ], @@ -107,7 +176,7 @@ class ProfilSayaPage extends StatelessWidget { _buildInfoRow( Icons.person_outline, "Nama Lengkap", - "Ketua RT 03", + userName, Colors.blue, ), const Divider(height: 24), @@ -123,11 +192,19 @@ class ProfilSayaPage extends StatelessWidget { _buildInfoRow( Icons.email_outlined, "Email", - "ketuart03@jawara.id", + userEmail, Colors.purple, ), const Divider(height: 24), + _buildInfoRow( + Icons.badge_outlined, + "Role", + _getRoleBadgeText(), + _getRoleBadgeColor(), + ), + const Divider(height: 24), + _buildInfoRow( Icons.location_on_outlined, "Alamat", @@ -216,6 +293,43 @@ class ProfilSayaPage extends StatelessWidget { const SizedBox(height: 24), + // Upgrade to Seller Button (only for Buyer) + if (userRole == 'Buyer') + Container( + margin: const EdgeInsets.symmetric(horizontal: 20), + width: double.infinity, + child: ElevatedButton.icon( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const UpgradeToSellerPage(), + ), + ); + }, + icon: const Icon(Icons.upgrade, color: Colors.white), + label: const Text( + "Upgrade ke Seller", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 2, + ), + ), + ), + + const SizedBox(height: 16), + // Edit Profile Button Container( margin: const EdgeInsets.symmetric(horizontal: 20), @@ -235,10 +349,7 @@ class ProfilSayaPage extends StatelessWidget { ), child: const Text( "Edit Profil", - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), ), ), @@ -251,7 +362,11 @@ class ProfilSayaPage extends StatelessWidget { } static Widget _buildInfoRow( - IconData icon, String label, String value, Color color) { + IconData icon, + String label, + String value, + Color color, + ) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -270,10 +385,7 @@ class ProfilSayaPage extends StatelessWidget { children: [ Text( label, - style: TextStyle( - color: Colors.grey[600], - fontSize: 12, - ), + style: TextStyle(color: Colors.grey[600], fontSize: 12), ), const SizedBox(height: 4), Text( diff --git a/apps/mobile/lib/features/lainnya/upgrade_to_seller_page.dart b/apps/mobile/lib/features/lainnya/upgrade_to_seller_page.dart new file mode 100644 index 0000000..8cac891 --- /dev/null +++ b/apps/mobile/lib/features/lainnya/upgrade_to_seller_page.dart @@ -0,0 +1,280 @@ +import 'package:flutter/material.dart'; +import '../../services/api_client.dart'; +import '../../services/auth_service.dart'; + +class UpgradeToSellerPage extends StatefulWidget { + const UpgradeToSellerPage({super.key}); + + @override + State createState() => _UpgradeToSellerPageState(); +} + +class _UpgradeToSellerPageState extends State { + final _formKey = GlobalKey(); + final _businessNameController = TextEditingController(); + final _businessAddressController = TextEditingController(); + final _businessDescController = TextEditingController(); + final _phoneController = TextEditingController(); + bool _isLoading = false; + + @override + void dispose() { + _businessNameController.dispose(); + _businessAddressController.dispose(); + _businessDescController.dispose(); + _phoneController.dispose(); + super.dispose(); + } + + Future _submitRequest() async { + if (!_formKey.currentState!.validate()) { + return; + } + + setState(() => _isLoading = true); + + try { + // Call API to request seller upgrade + final userData = await AuthService().getUserData(); + final userId = userData?['id'] ?? 0; + + await ApiClient().dio.post( + '/admin/seller-request', + data: { + 'userId': userId, + 'businessName': _businessNameController.text.trim(), + 'businessAddress': _businessAddressController.text.trim(), + 'businessDescription': _businessDescController.text.trim(), + 'phone': _phoneController.text.trim(), + }, + ); + + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Permohonan menjadi seller berhasil dikirim! Tunggu persetujuan admin.', + ), + backgroundColor: Colors.green, + ), + ); + + Navigator.pop(context); + } catch (e) { + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Gagal mengirim permohonan: ${e.toString()}'), + backgroundColor: Colors.red, + ), + ); + } finally { + if (mounted) { + setState(() => _isLoading = false); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF7F9FB), + appBar: AppBar( + elevation: 0, + backgroundColor: Colors.white, + foregroundColor: Colors.black, + title: const Text('Upgrade ke Seller'), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Info Card + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.blue.shade200), + ), + child: Row( + children: [ + Icon(Icons.info_outline, color: Colors.blue.shade700), + const SizedBox(width: 12), + Expanded( + child: Text( + 'Dengan menjadi Seller, Anda dapat menjual produk batik di marketplace. Permohonan akan diverifikasi oleh admin.', + style: TextStyle( + color: Colors.blue.shade900, + fontSize: 13, + ), + ), + ), + ], + ), + ), + + const SizedBox(height: 24), + + // Form Fields + _buildTextField( + controller: _businessNameController, + label: 'Nama Usaha', + hint: 'Contoh: Batik Nusantara', + icon: Icons.store, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Nama usaha tidak boleh kosong'; + } + return null; + }, + ), + + const SizedBox(height: 16), + + _buildTextField( + controller: _phoneController, + label: 'Nomor Telepon', + hint: '08123456789', + icon: Icons.phone, + keyboardType: TextInputType.phone, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Nomor telepon tidak boleh kosong'; + } + if (!value.startsWith('08')) { + return 'Nomor telepon harus dimulai dengan 08'; + } + return null; + }, + ), + + const SizedBox(height: 16), + + _buildTextField( + controller: _businessAddressController, + label: 'Alamat Usaha', + hint: 'Jl. Contoh No. 123', + icon: Icons.location_on, + maxLines: 2, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Alamat usaha tidak boleh kosong'; + } + return null; + }, + ), + + const SizedBox(height: 16), + + _buildTextField( + controller: _businessDescController, + label: 'Deskripsi Usaha', + hint: 'Ceritakan tentang usaha batik Anda...', + icon: Icons.description, + maxLines: 4, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Deskripsi usaha tidak boleh kosong'; + } + if (value.length < 20) { + return 'Deskripsi minimal 20 karakter'; + } + return null; + }, + ), + + const SizedBox(height: 32), + + // Submit Button + SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton( + onPressed: _isLoading ? null : _submitRequest, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF00BCD4), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: _isLoading + ? const SizedBox( + height: 24, + width: 24, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ) + : const Text( + 'Kirim Permohonan', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildTextField({ + required TextEditingController controller, + required String label, + required String hint, + required IconData icon, + int maxLines = 1, + TextInputType? keyboardType, + String? Function(String?)? validator, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Color(0xFF1E1E1E), + ), + ), + const SizedBox(height: 8), + TextFormField( + controller: controller, + maxLines: maxLines, + keyboardType: keyboardType, + validator: validator, + decoration: InputDecoration( + hintText: hint, + prefixIcon: Icon(icon, color: const Color(0xFF00BCD4)), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: Color(0xFF00BCD4), width: 2), + ), + filled: true, + fillColor: Colors.white, + ), + ), + ], + ); + } +} diff --git a/apps/mobile/lib/features/lainnya/warga/lainnya.dart b/apps/mobile/lib/features/lainnya/warga/lainnya.dart index ea69f7f..8d10298 100644 --- a/apps/mobile/lib/features/lainnya/warga/lainnya.dart +++ b/apps/mobile/lib/features/lainnya/warga/lainnya.dart @@ -4,6 +4,8 @@ import '../widgets/lainnya_widgets.dart'; import '../profil_saya.dart'; import '../kegiatan_warga.dart'; import '../notifikasi.dart'; +import '../../../services/auth_service.dart'; +import '../../../routes/app_routes.dart'; class LainnyaPage extends StatelessWidget { const LainnyaPage({super.key}); @@ -147,10 +149,7 @@ class LainnyaPage extends StatelessWidget { const SizedBox(height: 4), Text( "Versi 1.0.0", - style: TextStyle( - color: Colors.grey[500], - fontSize: 12, - ), + style: TextStyle(color: Colors.grey[500], fontSize: 12), ), ], ), @@ -165,9 +164,7 @@ class LainnyaPage extends StatelessWidget { showDialog( context: context, builder: (context) => Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), child: Padding( padding: const EdgeInsets.all(24), child: Column( @@ -230,9 +227,17 @@ class LainnyaPage extends StatelessWidget { const SizedBox(width: 12), Expanded( child: ElevatedButton( - onPressed: () { + onPressed: () async { Navigator.pop(context); - // Implement logout logic here + // Logout logic + await AuthService().clearAuth(); + if (context.mounted) { + Navigator.pushNamedAndRemoveUntil( + context, + AppRoutes.login, + (route) => false, + ); + } }, style: ElevatedButton.styleFrom( backgroundColor: Colors.red, diff --git a/apps/mobile/lib/features/login.dart b/apps/mobile/lib/features/login.dart index 64c9960..c1ddf5b 100644 --- a/apps/mobile/lib/features/login.dart +++ b/apps/mobile/lib/features/login.dart @@ -1,6 +1,9 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:dio/dio.dart'; import '../routes/app_routes.dart'; +import '../services/api_client.dart'; +import '../services/auth_service.dart'; class LoginPage extends StatefulWidget { const LoginPage({super.key}); @@ -11,6 +14,98 @@ class LoginPage extends StatefulWidget { class _LoginPageState extends State { bool _obscurePassword = true; + bool _isLoading = false; + + final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); + final _formKey = GlobalKey(); + + @override + void dispose() { + _emailController.dispose(); + _passwordController.dispose(); + super.dispose(); + } + + Future _handleLogin() async { + if (!_formKey.currentState!.validate()) { + return; + } + + setState(() => _isLoading = true); + + try { + // Using Dio directly with JSON + final response = await ApiClient().dio.post( + '/auth/login', + data: { + 'email': _emailController.text.trim(), + 'password': _passwordController.text, + }, + ); + + if (response.statusCode == 200) { + final data = response.data as Map?; + if (data == null) { + throw Exception('Invalid response from server'); + } + + // Save authentication data + await AuthService().saveAuth( + token: data['token'] ?? '', + userId: data['user']?['id'] ?? 0, + name: data['user']?['name'] ?? '', + email: data['user']?['email'] ?? '', + role: data['user']?['role'] ?? 'Buyer', + ); + + if (!mounted) return; + + // Navigate based on role + final role = data['user']?['role'] ?? 'Buyer'; + if (role == 'Admin' || role == 'RT' || role == 'RW') { + Navigator.pushReplacementNamed(context, AppRoutes.dashboardAdmin); + } else { + Navigator.pushReplacementNamed(context, AppRoutes.dashboardWarga); + } + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Login berhasil!'), + backgroundColor: Colors.green, + ), + ); + } + } catch (e) { + if (!mounted) return; + + String errorMessage = 'Login gagal'; + + if (e is DioException) { + if (e.type == DioExceptionType.connectionError || + e.type == DioExceptionType.unknown) { + errorMessage = + 'āš ļø Tidak dapat terhubung ke server.\n\nCatatan: Flutter Web memiliki limitasi CORS.\nSilakan gunakan:\n• Flutter Mobile (Android/iOS)\n• Flutter Desktop (macOS/Windows)\n\nAtau aktifkan CORS di backend.'; + } else if (e.response?.statusCode == 401) { + errorMessage = 'Email atau password salah'; + } else if (e.response != null) { + errorMessage = e.response?.data['message'] ?? 'Login gagal'; + } + } + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(errorMessage), + backgroundColor: Colors.red, + duration: const Duration(seconds: 5), + ), + ); + } finally { + if (mounted) { + setState(() => _isLoading = false); + } + } + } @override Widget build(BuildContext context) { @@ -28,7 +123,13 @@ class _LoginPageState extends State { _buildLogo(), const SizedBox(height: 32), _buildTitle(), - const SizedBox(height: 40), + const SizedBox(height: 24), + // CORS Warning Banner for Web + if (Theme.of(context).platform == TargetPlatform.windows || + Theme.of(context).platform == TargetPlatform.linux || + Theme.of(context).platform == TargetPlatform.macOS) + _buildWebWarningBanner(), + const SizedBox(height: 16), _buildForm(), const SizedBox(height: 10), _buildActionsRow(), @@ -100,37 +201,79 @@ class _LoginPageState extends State { ); } + Widget _buildWebWarningBanner() { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.orange.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.orange.shade300), + ), + child: Row( + children: [ + Icon( + Icons.warning_amber_rounded, + color: Colors.orange.shade700, + size: 24, + ), + const SizedBox(width: 12), + Expanded( + child: Text( + 'Web version has CORS limitations. Please use mobile or desktop app for full functionality.', + style: TextStyle(fontSize: 12, color: Colors.orange.shade900), + ), + ), + ], + ), + ); + } + Widget _buildForm() { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text("Username"), - const SizedBox(height: 6), - _buildTextField(hint: "Masukkan username", icon: Icons.person_outline), - const SizedBox(height: 20), + return Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text("Email"), + const SizedBox(height: 6), + _buildEmailField(), + const SizedBox(height: 20), - const Text("Password"), - const SizedBox(height: 6), - _buildPasswordField(), + const Text("Password"), + const SizedBox(height: 6), + _buildPasswordField(), - const SizedBox(height: 20), - _buildLoginButton(), - ], + const SizedBox(height: 20), + _buildLoginButton(), + ], + ), ); } - Widget _buildTextField({required String hint, required IconData icon}) { - return TextField( + Widget _buildEmailField() { + return TextFormField( + controller: _emailController, + keyboardType: TextInputType.emailAddress, decoration: InputDecoration( - prefixIcon: Icon(icon), - hintText: hint, + prefixIcon: const Icon(Icons.email_outlined), + hintText: "Masukkan email", border: OutlineInputBorder(borderRadius: BorderRadius.circular(28)), ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Email tidak boleh kosong'; + } + if (!value.contains('@')) { + return 'Email tidak valid'; + } + return null; + }, ); } Widget _buildPasswordField() { - return TextField( + return TextFormField( + controller: _passwordController, obscureText: _obscurePassword, decoration: InputDecoration( prefixIcon: const Icon(Icons.lock_outline), @@ -145,6 +288,15 @@ class _LoginPageState extends State { hintText: "Masukkan password", border: OutlineInputBorder(borderRadius: BorderRadius.circular(28)), ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Password tidak boleh kosong'; + } + if (value.length < 6) { + return 'Password minimal 6 karakter'; + } + return null; + }, ); } @@ -160,17 +312,24 @@ class _LoginPageState extends State { borderRadius: BorderRadius.circular(28), ), ), - onPressed: () { - Navigator.pushNamed(context, AppRoutes.dashboardWarga); - }, - child: const Text( - "Masuk", - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), + onPressed: _isLoading ? null : _handleLogin, + child: _isLoading + ? const SizedBox( + height: 24, + width: 24, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ) + : const Text( + "Masuk", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), ), ); } diff --git a/apps/mobile/lib/features/marketplace/warga/marketplace_warga.dart b/apps/mobile/lib/features/marketplace/warga/marketplace_warga.dart index cad05c4..726c68c 100644 --- a/apps/mobile/lib/features/marketplace/warga/marketplace_warga.dart +++ b/apps/mobile/lib/features/marketplace/warga/marketplace_warga.dart @@ -8,6 +8,7 @@ import 'my_products_page.dart'; import 'widget/product_card_warga.dart'; import 'widget/category_chip.dart'; import '../../../models/bottom_navbar_warga.dart'; +import '../../../services/auth_service.dart'; class MarketplaceWarga extends StatefulWidget { const MarketplaceWarga({super.key}); @@ -20,6 +21,20 @@ class _MarketplaceWargaState extends State { String selectedCategory = 'Semua'; String? detectedMotif; // Motif dari hasil camera detection final TextEditingController _searchController = TextEditingController(); + String? userRole; + + @override + void initState() { + super.initState(); + _loadUserRole(); + } + + Future _loadUserRole() async { + final role = await AuthService().getUserRole(); + setState(() { + userRole = role; + }); + } // Dummy data produk batik final List> products = [ @@ -149,16 +164,19 @@ class _MarketplaceWargaState extends State { backgroundColor: Colors.white, elevation: 0, actions: [ - IconButton( - icon: const Icon(Icons.store_outlined), - tooltip: 'Produk Saya', - onPressed: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const MyProductsPage()), - ); - }, - ), + if (userRole == 'Seller') // Only show for Seller + IconButton( + icon: const Icon(Icons.store_outlined), + tooltip: 'Produk Saya', + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const MyProductsPage(), + ), + ); + }, + ), IconButton( icon: Badge( label: const Text('2'), @@ -621,17 +639,23 @@ class _MarketplaceWargaState extends State { ), ], ), - floatingActionButton: FloatingActionButton.extended( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const SellProductPage()), - ); - }, - label: const Text('Jual Batik'), - icon: const Icon(Icons.add), - backgroundColor: const Color(0xFF00AFC1), - ), + floatingActionButton: + userRole == + 'Seller' // Only show for Seller + ? FloatingActionButton.extended( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const SellProductPage(), + ), + ); + }, + label: const Text('Jual Batik'), + icon: const Icon(Icons.add), + backgroundColor: const Color(0xFF00AFC1), + ) + : null, bottomNavigationBar: const AppBottomNavBar(currentIndex: 3), ); } diff --git a/apps/mobile/lib/features/marketplace/warga/sell_product_page.dart b/apps/mobile/lib/features/marketplace/warga/sell_product_page.dart index 3729b20..68194ff 100644 --- a/apps/mobile/lib/features/marketplace/warga/sell_product_page.dart +++ b/apps/mobile/lib/features/marketplace/warga/sell_product_page.dart @@ -31,7 +31,7 @@ class _SellProductPageState extends State { final ImagePicker _picker = ImagePicker(); static const String _openRouterApiKey = - 'YOUR_OPENROUTER_API_KEY_HERE'; + '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'; static const String _openRouterModel = 'meta-llama/llama-3.2-3b-instruct:free'; diff --git a/apps/mobile/lib/features/register/pages/warga/register_warga.dart b/apps/mobile/lib/features/register/pages/warga/register_warga.dart index f712e57..e0e1142 100644 --- a/apps/mobile/lib/features/register/pages/warga/register_warga.dart +++ b/apps/mobile/lib/features/register/pages/warga/register_warga.dart @@ -1,10 +1,10 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:image_picker/image_picker.dart' as img; +import 'package:dio/dio.dart'; import '../../../../models/text_field.dart'; -import '../../../../models/dropdown_field.dart'; -import '../../../../models/date_picker_field.dart'; import '../../controller/controller_warga.dart'; +import '../../../../services/api_client.dart'; +import '../../../../routes/app_routes.dart'; class RegisterWargaPage extends StatefulWidget { const RegisterWargaPage({super.key}); @@ -16,10 +16,72 @@ class RegisterWargaPage extends StatefulWidget { class _RegisterWargaPageState extends State { final vars = RegisterWargaVariables(); File? ktpImage; + bool _isLoading = false; - Future pickImage() async { - final picker = img.ImagePicker(); + Future _handleRegister() async { + // Validate inputs + if (vars.namaC.text.trim().isEmpty) { + _showError('Nama tidak boleh kosong'); + return; + } + + if (vars.emailC.text.trim().isEmpty || !vars.emailC.text.contains('@')) { + _showError('Email tidak valid'); + return; + } + + if (vars.passwordC.text.length < 6) { + _showError('Password minimal 6 karakter'); + return; + } + + setState(() => _isLoading = true); + + try { + // Using Dio directly with JSON + final response = await ApiClient().dio.post( + '/auth/register', + data: { + 'name': vars.namaC.text.trim(), + 'email': vars.emailC.text.trim(), + 'password': vars.passwordC.text, + }, + ); + + if (response.statusCode == 200 || response.statusCode == 201) { + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Registrasi berhasil! Silakan login.'), + backgroundColor: Colors.green, + ), + ); + + // Navigate to login page + Navigator.pushNamedAndRemoveUntil( + context, + AppRoutes.login, + (route) => false, + ); + } + } catch (e) { + if (!mounted) return; + _showError('Registrasi gagal: ${e.toString()}'); + } finally { + if (mounted) { + setState(() => _isLoading = false); + } + } + } + void _showError(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message), backgroundColor: Colors.red), + ); + } + + Future pickImage() async { showModalBottomSheet( context: context, shape: const RoundedRectangleBorder( @@ -55,16 +117,13 @@ class _RegisterWargaPageState extends State { appBar: AppBar( elevation: 0, backgroundColor: Colors.cyan[700], - title: const Text( - "Register Warga", - ), + title: const Text("Register Warga"), ), body: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( children: [ - // **************** CARD FORM **************** Container( padding: const EdgeInsets.all(18), @@ -76,15 +135,18 @@ class _RegisterWargaPageState extends State { color: Colors.black.withOpacity(0.05), blurRadius: 12, offset: const Offset(0, 4), - ) + ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - - ModernTextField(controller: vars.namaC, label: "Nama Lengkap", prefixIcon: Icons.badge), + ModernTextField( + controller: vars.namaC, + label: "Nama Lengkap", + prefixIcon: Icons.badge, + ), ModernTextField( controller: vars.emailC, label: "Email (harus @gmail)", @@ -97,7 +159,6 @@ class _RegisterWargaPageState extends State { prefixIcon: Icons.lock, keyboardType: TextInputType.visiblePassword, ), - ], ), ), @@ -108,7 +169,7 @@ class _RegisterWargaPageState extends State { SizedBox( width: double.infinity, child: ElevatedButton( - onPressed: () {}, + onPressed: _isLoading ? null : _handleRegister, style: ElevatedButton.styleFrom( backgroundColor: Colors.cyan[700], padding: const EdgeInsets.symmetric(vertical: 16), @@ -117,10 +178,23 @@ class _RegisterWargaPageState extends State { ), elevation: 3, ), - child: const Text( - "Simpan Data", - style: TextStyle(fontSize: 16, color: Colors.white, fontWeight: FontWeight.w600), - ), + child: _isLoading + ? const SizedBox( + height: 24, + width: 24, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ) + : const Text( + "Simpan Data", + style: TextStyle( + fontSize: 16, + color: Colors.white, + fontWeight: FontWeight.w600, + ), + ), ), ), ], diff --git a/apps/mobile/lib/main.dart b/apps/mobile/lib/main.dart index aafa240..aad693d 100644 --- a/apps/mobile/lib/main.dart +++ b/apps/mobile/lib/main.dart @@ -1,25 +1,80 @@ import 'package:flutter/material.dart'; import 'routes/app_pages.dart'; import 'routes/app_routes.dart'; +import 'services/auth_service.dart'; +import 'features/login.dart'; +import 'features/dashboard/admin/dashboard.dart'; +import 'features/dashboard/warga/dashboard.dart'; -void main() { +void main() async { + WidgetsFlutterBinding.ensureInitialized(); runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); + Future _getInitialPage() async { + final isLoggedIn = await AuthService().isLoggedIn(); + if (isLoggedIn) { + final role = await AuthService().getUserRole(); + if (role == 'Admin' || role == 'RT' || role == 'RW') { + return const AdminDashboard(); + } + return const WargaDashboard(); + } + return const LoginPage(); + } + @override Widget build(BuildContext context) { - return MaterialApp( - title: 'Jawara Pintar', - debugShowCheckedModeBanner: false, - routes: AppPages.routes, - initialRoute: AppRoutes.login, // Start from login page - theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF00AFC1)), - useMaterial3: true, - ), + return FutureBuilder( + future: _getInitialPage(), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return MaterialApp( + debugShowCheckedModeBanner: false, + home: Scaffold( + body: Center( + child: CircularProgressIndicator( + color: const Color(0xFF00AFC1), + ), + ), + ), + ); + } + + return MaterialApp( + title: 'Jawara Pintar', + debugShowCheckedModeBanner: false, + routes: AppPages.routes, + home: snapshot.data!, + theme: ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: const Color(0xFF00AFC1), + ), + useMaterial3: true, + inputDecorationTheme: InputDecorationTheme( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(28), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 16, + ), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + elevation: 2, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(28), + ), + ), + ), + ), + ); + }, ); } } diff --git a/apps/mobile/lib/services/api_client.dart b/apps/mobile/lib/services/api_client.dart new file mode 100644 index 0000000..f46b279 --- /dev/null +++ b/apps/mobile/lib/services/api_client.dart @@ -0,0 +1,97 @@ +import 'package:dio/dio.dart'; +import 'package:openapi/openapi.dart'; +import 'package:built_value/serializer.dart'; +import 'auth_service.dart'; + +class ApiClient { + static final ApiClient _instance = ApiClient._internal(); + late Dio _dio; + late Serializers _serializers; + + // API instances + late AuthApi authApi; + late ProfileApi profileApi; + late ProductsApi productsApi; + late BuyerApi buyerApi; + late SellerApi sellerApi; + late ActivitiesApi activitiesApi; + late FinanceApi financeApi; + late DashboardApi dashboardApi; + late AdminApi adminApi; + + factory ApiClient() { + return _instance; + } + + ApiClient._internal() { + _initialize(); + } + + void _initialize() { + // Initialize Dio with base configuration + _dio = Dio( + BaseOptions( + baseUrl: 'https://apps-jawa-backend.vercel.app', + connectTimeout: const Duration(seconds: 30), + receiveTimeout: const Duration(seconds: 30), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + ), + ); + + // Add interceptors for logging and auth + _dio.interceptors.add( + LogInterceptor( + requestBody: true, + responseBody: true, + error: true, + requestHeader: true, + responseHeader: false, + ), + ); + + // Add auth interceptor + _dio.interceptors.add( + InterceptorsWrapper( + onRequest: (options, handler) async { + final token = await AuthService().getToken(); + if (token != null) { + options.headers['Authorization'] = 'Bearer $token'; + } + return handler.next(options); + }, + onError: (error, handler) async { + if (error.response?.statusCode == 401) { + // Token expired or invalid + await AuthService().clearAuth(); + } + return handler.next(error); + }, + ), + ); + + // Initialize serializers + _serializers = standardSerializers; + + // Initialize API instances + authApi = AuthApi(_dio, _serializers); + profileApi = ProfileApi(_dio, _serializers); + productsApi = ProductsApi(_dio, _serializers); + buyerApi = BuyerApi(_dio, _serializers); + sellerApi = SellerApi(_dio, _serializers); + activitiesApi = ActivitiesApi(_dio, _serializers); + financeApi = FinanceApi(_dio, _serializers); + dashboardApi = DashboardApi(_dio, _serializers); + adminApi = AdminApi(_dio, _serializers); + } + + // Getter for dio instance if needed for custom requests + Dio get dio => _dio; + + // Method to update base URL if needed + void updateBaseUrl(String baseUrl) { + _dio.options.baseUrl = baseUrl; + } +} diff --git a/apps/mobile/lib/services/auth_service.dart b/apps/mobile/lib/services/auth_service.dart new file mode 100644 index 0000000..c46bc21 --- /dev/null +++ b/apps/mobile/lib/services/auth_service.dart @@ -0,0 +1,79 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class AuthService { + static final AuthService _instance = AuthService._internal(); + final _secureStorage = const FlutterSecureStorage(); + + static const String _tokenKey = 'auth_token'; + static const String _userIdKey = 'user_id'; + static const String _userNameKey = 'user_name'; + static const String _userEmailKey = 'user_email'; + static const String _userRoleKey = 'user_role'; + + factory AuthService() { + return _instance; + } + + AuthService._internal(); + + // Save authentication data + Future saveAuth({ + required String token, + required int userId, + required String name, + required String email, + required String role, + }) async { + await _secureStorage.write(key: _tokenKey, value: token); + + final prefs = await SharedPreferences.getInstance(); + await prefs.setInt(_userIdKey, userId); + await prefs.setString(_userNameKey, name); + await prefs.setString(_userEmailKey, email); + await prefs.setString(_userRoleKey, role); + } + + // Get token + Future getToken() async { + return await _secureStorage.read(key: _tokenKey); + } + + // Get user data + Future?> getUserData() async { + final prefs = await SharedPreferences.getInstance(); + final userId = prefs.getInt(_userIdKey); + + if (userId == null) return null; + + return { + 'id': userId, + 'name': prefs.getString(_userNameKey), + 'email': prefs.getString(_userEmailKey), + 'role': prefs.getString(_userRoleKey), + }; + } + + // Check if user is logged in + Future isLoggedIn() async { + final token = await getToken(); + return token != null && token.isNotEmpty; + } + + // Clear all auth data + Future clearAuth() async { + await _secureStorage.delete(key: _tokenKey); + + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(_userIdKey); + await prefs.remove(_userNameKey); + await prefs.remove(_userEmailKey); + await prefs.remove(_userRoleKey); + } + + // Get user role + Future getUserRole() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString(_userRoleKey); + } +} diff --git a/apps/mobile/linux/flutter/generated_plugin_registrant.cc b/apps/mobile/linux/flutter/generated_plugin_registrant.cc index 64a0ece..85a2413 100644 --- a/apps/mobile/linux/flutter/generated_plugin_registrant.cc +++ b/apps/mobile/linux/flutter/generated_plugin_registrant.cc @@ -7,9 +7,13 @@ #include "generated_plugin_registrant.h" #include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); + flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); } diff --git a/apps/mobile/linux/flutter/generated_plugins.cmake b/apps/mobile/linux/flutter/generated_plugins.cmake index 2db3c22..62e3ed5 100644 --- a/apps/mobile/linux/flutter/generated_plugins.cmake +++ b/apps/mobile/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_linux + flutter_secure_storage_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/apps/mobile/macos/Flutter/GeneratedPluginRegistrant.swift b/apps/mobile/macos/Flutter/GeneratedPluginRegistrant.swift index b878e03..404af0d 100644 --- a/apps/mobile/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/apps/mobile/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,9 +6,13 @@ import FlutterMacOS import Foundation import file_selector_macos +import flutter_secure_storage_macos import path_provider_foundation +import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) } diff --git a/apps/mobile/pubspec.lock b/apps/mobile/pubspec.lock index 5e917ed..7850418 100644 --- a/apps/mobile/pubspec.lock +++ b/apps/mobile/pubspec.lock @@ -33,6 +33,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "426cf75afdb23aa74bd4e471704de3f9393f3c7b04c1e2d9c6f1073ae0b8b139" + url: "https://pub.dev" + source: hosted + version: "8.12.1" camera: dependency: "direct main" description: @@ -145,6 +161,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + dio: + dependency: "direct main" + description: + name: dio + sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9 + url: "https://pub.dev" + source: hosted + version: "5.9.0" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + url: "https://pub.dev" + source: hosted + version: "2.1.1" dropdown_search: dependency: "direct main" description: @@ -177,6 +209,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" file_selector_linux: dependency: transitive description: @@ -209,6 +249,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.3+5" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" fl_chart: dependency: "direct main" description: @@ -254,6 +302,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.33" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea" + url: "https://pub.dev" + source: hosted + version: "9.2.4" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247" + url: "https://pub.dev" + source: hosted + version: "3.1.3" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 + url: "https://pub.dev" + source: hosted + version: "3.1.2" flutter_test: dependency: "direct dev" description: flutter @@ -368,6 +464,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.20.2" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" json_annotation: dependency: transitive description: @@ -452,10 +556,10 @@ packages: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.16.0" mgrs_dart: dependency: transitive description: @@ -472,6 +576,29 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + one_of: + dependency: transitive + description: + name: one_of + sha256: "25fe0fcf181e761c6fcd604caf9d5fdf952321be17584ba81c72c06bdaa511f0" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + one_of_serializer: + dependency: transitive + description: + name: one_of_serializer + sha256: "3f3dfb5c1578ba3afef1cb47fcc49e585e797af3f2b6c2cc7ed90aad0c5e7b83" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + openapi: + dependency: "direct main" + description: + path: "../flutter_client" + relative: true + source: path + version: "1.0.0" path: dependency: "direct main" description: @@ -576,6 +703,70 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + quiver: + dependency: transitive + description: + name: quiver + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "83af5c682796c0f7719c2bbf74792d113e40ae97981b8f266fa84574573556bc" + url: "https://pub.dev" + source: hosted + version: "2.4.18" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" sky_engine: dependency: transitive description: flutter @@ -633,10 +824,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.6" typed_data: dependency: transitive description: @@ -677,6 +868,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" wkt_parser: dependency: transitive description: diff --git a/apps/mobile/pubspec.yaml b/apps/mobile/pubspec.yaml index 9105514..5167db2 100644 --- a/apps/mobile/pubspec.yaml +++ b/apps/mobile/pubspec.yaml @@ -24,6 +24,11 @@ dependencies: fl_chart: ^1.1.1 flutter_map: ^7.0.2 latlong2: ^0.9.1 + openapi: + path: ../flutter_client + dio: ^5.7.0 + shared_preferences: ^2.2.2 + flutter_secure_storage: ^9.0.0 dev_dependencies: flutter_test: diff --git a/apps/mobile/windows/flutter/generated_plugin_registrant.cc b/apps/mobile/windows/flutter/generated_plugin_registrant.cc index 77ab7a0..b53f20e 100644 --- a/apps/mobile/windows/flutter/generated_plugin_registrant.cc +++ b/apps/mobile/windows/flutter/generated_plugin_registrant.cc @@ -7,8 +7,11 @@ #include "generated_plugin_registrant.h" #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); } diff --git a/apps/mobile/windows/flutter/generated_plugins.cmake b/apps/mobile/windows/flutter/generated_plugins.cmake index a423a02..2b9f993 100644 --- a/apps/mobile/windows/flutter/generated_plugins.cmake +++ b/apps/mobile/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_windows + flutter_secure_storage_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST From 88ed61b87a3644b4d5ea8bb2c4a522d009631c25 Mon Sep 17 00:00:00 2001 From: Test User Date: Sat, 27 Dec 2025 22:41:34 +0700 Subject: [PATCH 2/2] feat: Implement product streaming in MarketplaceWarga --- README.md | 183 +++++------------- .../marketplace/warga/marketplace_warga.dart | 141 ++++++++------ .../marketplace/warga/sell_product_page.dart | 2 +- 3 files changed, 134 insertions(+), 192 deletions(-) diff --git a/README.md b/README.md index 240569f..4da8b47 100644 --- a/README.md +++ b/README.md @@ -1,135 +1,48 @@ -# Turborepo starter - -This Turborepo starter is maintained by the Turborepo core team. - -## Using this example - -Run the following command: - -```sh -npx create-turbo@latest -``` - -## What's inside? - -This Turborepo includes the following packages/apps: - -### Apps and Packages - -- `docs`: a [Next.js](https://nextjs.org/) app -- `web`: another [Next.js](https://nextjs.org/) app -- `@repo/ui`: a stub React component library shared by both `web` and `docs` applications -- `@repo/eslint-config`: `eslint` configurations (includes `eslint-config-next` and `eslint-config-prettier`) -- `@repo/typescript-config`: `tsconfig.json`s used throughout the monorepo - -Each package/app is 100% [TypeScript](https://www.typescriptlang.org/). - -### Utilities - -This Turborepo has some additional tools already setup for you: - -- [TypeScript](https://www.typescriptlang.org/) for static type checking -- [ESLint](https://eslint.org/) for code linting -- [Prettier](https://prettier.io) for code formatting - -### Build - -To build all apps and packages, run the following command: - -``` -cd my-turborepo - -# With [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation) installed (recommended) -turbo build - -# Without [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation), use your package manager -npx turbo build -yarn dlx turbo build -pnpm exec turbo build -``` - -You can build a specific package by using a [filter](https://turborepo.com/docs/crafting-your-repository/running-tasks#using-filters): - -``` -# With [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation) installed (recommended) -turbo build --filter=docs - -# Without [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation), use your package manager -npx turbo build --filter=docs -yarn exec turbo build --filter=docs -pnpm exec turbo build --filter=docs -``` - -### Develop - -To develop all apps and packages, run the following command: - -``` -cd my-turborepo - -# With [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation) installed (recommended) -turbo dev - -# Without [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation), use your package manager -npx turbo dev -yarn exec turbo dev -pnpm exec turbo dev -``` - -You can develop a specific package by using a [filter](https://turborepo.com/docs/crafting-your-repository/running-tasks#using-filters): - -``` -# With [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation) installed (recommended) -turbo dev --filter=web - -# Without [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation), use your package manager -npx turbo dev --filter=web -yarn exec turbo dev --filter=web -pnpm exec turbo dev --filter=web -``` - -### Remote Caching - -> [!TIP] -> Vercel Remote Cache is free for all plans. Get started today at [vercel.com](https://vercel.com/signup?/signup?utm_source=remote-cache-sdk&utm_campaign=free_remote_cache). - -Turborepo can use a technique known as [Remote Caching](https://turborepo.com/docs/core-concepts/remote-caching) to share cache artifacts across machines, enabling you to share build caches with your team and CI/CD pipelines. - -By default, Turborepo will cache locally. To enable Remote Caching you will need an account with Vercel. If you don't have an account you can [create one](https://vercel.com/signup?utm_source=turborepo-examples), then enter the following commands: - -``` -cd my-turborepo - -# With [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation) installed (recommended) -turbo login - -# Without [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation), use your package manager -npx turbo login -yarn exec turbo login -pnpm exec turbo login -``` - -This will authenticate the Turborepo CLI with your [Vercel account](https://vercel.com/docs/concepts/personal-accounts/overview). - -Next, you can link your Turborepo to your Remote Cache by running the following command from the root of your Turborepo: - -``` -# With [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation) installed (recommended) -turbo link - -# Without [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation), use your package manager -npx turbo link -yarn exec turbo link -pnpm exec turbo link -``` - -## Useful Links - -Learn more about the power of Turborepo: - -- [Tasks](https://turborepo.com/docs/crafting-your-repository/running-tasks) -- [Caching](https://turborepo.com/docs/crafting-your-repository/caching) -- [Remote Caching](https://turborepo.com/docs/core-concepts/remote-caching) -- [Filtering](https://turborepo.com/docs/crafting-your-repository/running-tasks#using-filters) -- [Configuration Options](https://turborepo.com/docs/reference/configuration) -- [CLI Usage](https://turborepo.com/docs/reference/command-line-reference) +# Project Marketplace Batik + +## About This Project +This project is a marketplace application for buying and selling batik products. It is built using Flutter for mobile and web platforms, with a backend powered by Node.js and TypeScript. The application supports role-based features for Buyers, Sellers, and Admins, providing a seamless experience for all users. + +## Features +- Role-based UI for Buyers and Sellers +- Product search and filtering +- Camera-based batik motif detection +- Dynamic product loading with streams +- Cart and checkout functionality +- Admin management for seller requests + +## Team Members +| Name | ID | Contributions | +|-------------------------------|------------|-----------------------| +| AFIFAH KHOIRUNNISA | 2341720250 | Mengerjakan bagian warga | +| KEVIN ADIKA SAPUTRA | 2341720017 | Mengarahkan dan menyambungkan API dan Database ke APP | +| MAULANA RENGGA RAMADAN | 2341720160 | Mengerjakan bagian Admin | +| VIDI JOSHUBZKY SAVIOLA | 2341720112 | Mengerjakan bagian Marketplacenya | + +## How to Run +### Prerequisites +- Flutter SDK installed +- Node.js and npm installed +- Dart SDK installed + +### Steps +1. Clone the repository: + ```sh + git clone https://github.com/KevinASaputra/apps_jawa.git + ``` +2. Navigate to the project directory: + ```sh + cd apps_jawa + ``` +3. Install dependencies: + ```sh + flutter pub get + npm install + ``` +4. Run the app: + ```sh + flutter run + ``` + +## License +This project is licensed under the MIT License. diff --git a/apps/mobile/lib/features/marketplace/warga/marketplace_warga.dart b/apps/mobile/lib/features/marketplace/warga/marketplace_warga.dart index 726c68c..7bb0135 100644 --- a/apps/mobile/lib/features/marketplace/warga/marketplace_warga.dart +++ b/apps/mobile/lib/features/marketplace/warga/marketplace_warga.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'batik_detection_page.dart'; import 'batik_camera_page.dart'; @@ -23,10 +24,15 @@ class _MarketplaceWargaState extends State { final TextEditingController _searchController = TextEditingController(); String? userRole; + // Stream controller for products + final StreamController>> _productStreamController = + StreamController(); + @override void initState() { super.initState(); _loadUserRole(); + _startProductStream(); } Future _loadUserRole() async { @@ -36,6 +42,13 @@ class _MarketplaceWargaState extends State { }); } + @override + void dispose() { + _searchController.dispose(); + _productStreamController.close(); // Close the stream controller + super.dispose(); + } + // Dummy data produk batik final List> products = [ { @@ -97,10 +110,34 @@ class _MarketplaceWargaState extends State { 'Stok Terbanyak', ]; - @override - void dispose() { - _searchController.dispose(); - super.dispose(); + // Function to simulate product stream + void _startProductStream() { + List> allProducts = List.from( + products, + ); // Clone the initial products + + // Simulate adding more products over time + Timer.periodic(const Duration(seconds: 2), (timer) { + if (allProducts.length >= 100) { + timer.cancel(); // Stop adding products after reaching 100 + } else { + allProducts.add({ + 'id': allProducts.length + 1, + 'name': 'Batik ${allProducts.length + 1}', + 'motif': 'Motif ${allProducts.length + 1}', + 'price': 200000 + (allProducts.length * 1000), + 'seller': 'Seller ${allProducts.length + 1}', + 'image': 'https://via.placeholder.com/150', + 'rating': 4.0 + (allProducts.length % 5) * 0.1, + 'sold': allProducts.length * 10, + 'stock': 50 - (allProducts.length % 10), + 'description': 'Deskripsi produk batik ${allProducts.length + 1}', + }); + _productStreamController.add( + List.from(allProducts), + ); // Add updated list to the stream + } + }); } List> get filteredProducts { @@ -582,60 +619,52 @@ class _MarketplaceWargaState extends State { ), ), - // Product Grid + // Product StreamBuilder Expanded( - child: filteredProducts.isEmpty - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.inventory_2_outlined, - size: 80, - color: Colors.grey[400], - ), - const SizedBox(height: 16), - Text( - detectedMotif != null - ? 'Tidak ada produk dengan motif "$detectedMotif"' - : _searchController.text.isNotEmpty - ? 'Tidak ada hasil untuk "${_searchController.text}"' - : 'Tidak ada produk', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16, - color: Colors.grey[600], - ), - ), - ], - ), - ) - : GridView.builder( - padding: const EdgeInsets.all(16), - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - childAspectRatio: 0.7, - crossAxisSpacing: 12, - mainAxisSpacing: 12, - ), - itemCount: filteredProducts.length, - itemBuilder: (context, index) { - final product = filteredProducts[index]; - return ProductCardWarga( - product: product, - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - DetailProdukBatik(product: product), - ), - ); - }, - ); - }, + child: StreamBuilder>>( + stream: _productStreamController.stream, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + + if (snapshot.hasError) { + return Center(child: Text('Error: ${snapshot.error}')); + } + + if (!snapshot.hasData || snapshot.data!.isEmpty) { + return const Center(child: Text('Tidak ada produk')); + } + + final products = snapshot.data!; + + return GridView.builder( + padding: const EdgeInsets.all(16), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 0.7, + crossAxisSpacing: 12, + mainAxisSpacing: 12, ), + itemCount: products.length, + itemBuilder: (context, index) { + final product = products[index]; + return ProductCardWarga( + product: product, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + DetailProdukBatik(product: product), + ), + ); + }, + ); + }, + ); + }, + ), ), ], ), diff --git a/apps/mobile/lib/features/marketplace/warga/sell_product_page.dart b/apps/mobile/lib/features/marketplace/warga/sell_product_page.dart index 68194ff..cd82b54 100644 --- a/apps/mobile/lib/features/marketplace/warga/sell_product_page.dart +++ b/apps/mobile/lib/features/marketplace/warga/sell_product_page.dart @@ -31,7 +31,7 @@ class _SellProductPageState extends State { final ImagePicker _picker = ImagePicker(); static const String _openRouterApiKey = - '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'; + 'API_KEY_YANG_HARUS_DIGANTI_DENGAN_MILIK_ANDA'; static const String _openRouterModel = 'meta-llama/llama-3.2-3b-instruct:free';