add flutter

This commit is contained in:
Ariska
2026-03-11 15:29:37 +07:00
parent c253e1a370
commit 619d758027
9490 changed files with 135801 additions and 1353 deletions
@@ -0,0 +1,7 @@
class AppConfig {
static const String baseUrl = 'https://apitest.semestaterpadu.my.id/';
static const String apiBase = '${baseUrl}api/';
static const String imagesMerchant = '${baseUrl}images/merchant/';
static const String imagesItem = '${baseUrl}images/itemmerchant/';
static const String imagesBank = '${baseUrl}images/bank/';
}
@@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:ontime_merchant_flutter/features/auth/presentation/login_screen.dart';
import 'package:ontime_merchant_flutter/features/home/presentation/home_shell.dart';
final _rootNavKey = GlobalKey<NavigatorState>(debugLabel: 'root');
GoRouter createAppRouter() {
return GoRouter(
navigatorKey: _rootNavKey,
initialLocation: '/auth/login',
routes: [
GoRoute(
path: '/auth/login',
name: 'login',
pageBuilder: (_, __) => const MaterialPage(child: LoginScreen()),
),
GoRoute(
path: '/home',
name: 'home',
pageBuilder: (_, __) => const MaterialPage(child: HomeShell()),
),
],
);
}
@@ -0,0 +1,13 @@
import 'package:flutter/material.dart';
class AppTheme {
static ThemeData light() {
final base = ThemeData.light(useMaterial3: true);
return base.copyWith(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF00AEEF)),
scaffoldBackgroundColor: Colors.grey[50],
appBarTheme: const AppBarTheme(elevation: 0, centerTitle: true),
inputDecorationTheme: const InputDecorationTheme(border: OutlineInputBorder()),
);
}
}
@@ -0,0 +1,24 @@
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
/// FCM token for backend (Merchant uses `token` / `token_merchant`).
class FcmService {
static bool _initialized = false;
static Future<void> init() async {
if (_initialized) return;
try {
await Firebase.initializeApp();
_initialized = true;
} catch (_) {}
}
static Future<String?> getToken() async {
if (!_initialized) return null;
try {
return await FirebaseMessaging.instance.getToken();
} catch (_) {
return null;
}
}
}
@@ -0,0 +1,24 @@
import 'package:dio/dio.dart';
import 'package:ontime_merchant_flutter/core/app_config.dart';
class ApiClient {
ApiClient({
required String? basicAuthUser,
required String? basicAuthPassword,
}) : _dio = Dio(
BaseOptions(
baseUrl: AppConfig.apiBase,
connectTimeout: const Duration(seconds: 15),
receiveTimeout: const Duration(seconds: 20),
),
) {
if (basicAuthUser != null && basicAuthPassword != null) {
final auth = '$basicAuthUser:$basicAuthPassword';
final encoded = String.fromCharCodes(auth.codeUnits);
_dio.options.headers['Authorization'] = 'Basic $encoded';
}
}
final Dio _dio;
Dio get raw => _dio;
}
@@ -0,0 +1,77 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:ontime_merchant_flutter/data/api/api_client.dart';
import 'package:ontime_merchant_flutter/features/auth/data/merchant_auth_api.dart';
import 'package:ontime_merchant_flutter/features/auth/data/models/merchant_model.dart';
import 'package:shared_preferences/shared_preferences.dart';
class AuthState {
const AuthState({
this.merchant,
this.isLoading = false,
this.errorMessage,
});
final MerchantModel? merchant;
final bool isLoading;
final String? errorMessage;
AuthState copyWith({
MerchantModel? merchant,
bool? isLoading,
String? errorMessage,
}) {
return AuthState(
merchant: merchant ?? this.merchant,
isLoading: isLoading ?? this.isLoading,
errorMessage: errorMessage,
);
}
}
final authControllerProvider =
StateNotifierProvider<AuthController, AuthState>((ref) => AuthController());
class AuthController extends StateNotifier<AuthState> {
AuthController() : super(const AuthState());
late final MerchantAuthApi _api =
MerchantAuthApi(ApiClient(basicAuthUser: null, basicAuthPassword: null));
Future<void> login({
required String noTelepon,
required String password,
String? fcmToken,
}) async {
state = state.copyWith(isLoading: true, errorMessage: null);
try {
final merchant = await _api.login(
noTelepon: noTelepon,
password: password,
fcmToken: fcmToken,
);
state = state.copyWith(merchant: merchant, isLoading: false);
final prefs = await SharedPreferences.getInstance();
await prefs.setString('merchant_id_mitra', merchant.idMitra);
await prefs.setString('merchant_id_merchant', merchant.idMerchant);
await prefs.setString('merchant_phone', merchant.teleponMitra);
await prefs.setString('merchant_token', merchant.tokenMerchant);
} catch (e) {
state = state.copyWith(
isLoading: false,
errorMessage: e.toString().replaceFirst('Exception: ', ''),
);
}
}
void logout() {
state = const AuthState();
}
static Future<void> clearStored() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove('merchant_id_mitra');
await prefs.remove('merchant_id_merchant');
await prefs.remove('merchant_phone');
await prefs.remove('merchant_token');
}
}
@@ -0,0 +1,35 @@
import 'package:dio/dio.dart';
import 'package:ontime_merchant_flutter/data/api/api_client.dart';
import 'package:ontime_merchant_flutter/features/auth/data/models/merchant_model.dart';
class MerchantAuthApi {
MerchantAuthApi(this._client);
final ApiClient _client;
Future<MerchantModel> login({
required String noTelepon,
required String password,
String? fcmToken,
}) async {
final payload = <String, dynamic>{
'no_telepon': noTelepon,
'password': password,
};
if (fcmToken != null && fcmToken.isNotEmpty) payload['token'] = fcmToken;
final Response<dynamic> response =
await _client.raw.post('Merchant/login', data: payload);
final data = response.data is Map<String, dynamic>
? response.data as Map<String, dynamic>
: <String, dynamic>{};
if (data['message'] == 'banned') throw Exception('banned');
if (data['code']?.toString() != '200') {
throw Exception(data['message'] ?? 'Login failed');
}
final list = data['data'] as List<dynamic>? ?? [];
if (list.isEmpty) throw Exception('Merchant data not found');
return MerchantModel.fromJson(list.first as Map<String, dynamic>);
}
}
@@ -0,0 +1,37 @@
import 'package:json_annotation/json_annotation.dart';
part 'merchant_model.g.dart';
@JsonSerializable()
class MerchantModel {
MerchantModel({
required this.idMitra,
required this.idMerchant,
required this.teleponMitra,
required this.tokenMerchant,
this.namaMerchant,
this.emailMitra,
});
@JsonKey(name: 'id_mitra')
final String idMitra;
@JsonKey(name: 'id_merchant')
final String idMerchant;
@JsonKey(name: 'telepon_mitra')
final String teleponMitra;
@JsonKey(name: 'token_merchant')
final String tokenMerchant;
@JsonKey(name: 'nama_merchant')
final String? namaMerchant;
@JsonKey(name: 'email_mitra')
final String? emailMitra;
factory MerchantModel.fromJson(Map<String, dynamic> json) =>
_$MerchantModelFromJson(json);
Map<String, dynamic> toJson() => _$MerchantModelToJson(this);
}
@@ -0,0 +1,22 @@
part of 'merchant_model.dart';
MerchantModel _$MerchantModelFromJson(Map<String, dynamic> json) {
return MerchantModel(
idMitra: json['id_mitra'] as String? ?? '',
idMerchant: json['id_merchant'] as String? ?? '',
teleponMitra: json['telepon_mitra'] as String? ?? '',
tokenMerchant: json['token_merchant'] as String? ?? '',
namaMerchant: json['nama_merchant'] as String?,
emailMitra: json['email_mitra'] as String?,
);
}
Map<String, dynamic> _$MerchantModelToJson(MerchantModel instance) =>
<String, dynamic>{
'id_mitra': instance.idMitra,
'id_merchant': instance.idMerchant,
'telepon_mitra': instance.teleponMitra,
'token_merchant': instance.tokenMerchant,
'nama_merchant': instance.namaMerchant,
'email_mitra': instance.emailMitra,
};
@@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:ontime_merchant_flutter/core/fcm_service.dart';
import 'package:ontime_merchant_flutter/features/auth/application/auth_controller.dart';
class LoginScreen extends ConsumerStatefulWidget {
const LoginScreen({super.key});
@override
ConsumerState<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends ConsumerState<LoginScreen> {
final _formKey = GlobalKey<FormState>();
final _phoneController = TextEditingController();
final _passwordController = TextEditingController();
@override
void dispose() {
_phoneController.dispose();
_passwordController.dispose();
super.dispose();
}
Future<void> _submit() async {
if (!_formKey.currentState!.validate()) return;
final fcmToken = await FcmService.getToken();
final auth = ref.read(authControllerProvider.notifier);
await auth.login(
noTelepon: _phoneController.text.trim(),
password: _passwordController.text,
fcmToken: fcmToken,
);
final state = ref.read(authControllerProvider);
if (state.merchant != null && mounted) context.go('/home');
}
@override
Widget build(BuildContext context) {
final state = ref.watch(authControllerProvider);
return Scaffold(
appBar: AppBar(title: const Text('Masuk Merchant')),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
controller: _phoneController,
decoration: const InputDecoration(labelText: 'No. Telepon'),
keyboardType: TextInputType.phone,
validator: (v) =>
(v == null || v.isEmpty) ? 'No. telepon wajib diisi' : null,
),
const SizedBox(height: 12),
TextFormField(
controller: _passwordController,
decoration: const InputDecoration(labelText: 'Kata sandi'),
obscureText: true,
validator: (v) =>
(v == null || v.isEmpty) ? 'Password wajib diisi' : null,
),
if (state.errorMessage != null) ...[
const SizedBox(height: 8),
Text(
state.errorMessage!,
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
],
const Spacer(),
ElevatedButton(
onPressed: state.isLoading ? null : _submit,
child: state.isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Masuk'),
),
],
),
),
),
),
);
}
}
@@ -0,0 +1,99 @@
import 'package:dio/dio.dart';
import 'package:ontime_merchant_flutter/data/api/api_client.dart';
class MerchantHomeApi {
MerchantHomeApi(this._client);
final ApiClient _client;
Future<Map<String, dynamic>> home({
required String noTelepon,
required String idMitra,
required String idMerchant,
}) async {
final Response<dynamic> response = await _client.raw.post(
'Merchant/home',
data: {
'no_telepon': noTelepon,
'idmitra': idMitra,
'idmerchant': idMerchant,
},
);
final data = response.data is Map<String, dynamic>
? response.data as Map<String, dynamic>
: <String, dynamic>{};
if (data['code']?.toString() != '200') {
throw Exception(data['message'] ?? 'Failed');
}
return data;
}
Future<void> onoff({
required String idMerchant,
required String token,
required int status,
}) async {
await _client.raw.post(
'Merchant/onoff',
data: {
'idmerchant': idMerchant,
'token': token,
'status': status,
},
);
}
Future<Map<String, dynamic>> history({
required String noTelepon,
required String idMerchant,
required String day,
}) async {
final Response<dynamic> response = await _client.raw.post(
'Merchant/history',
data: {
'no_telepon': noTelepon,
'idmerchant': idMerchant,
'day': day,
},
);
final data = response.data is Map<String, dynamic>
? response.data as Map<String, dynamic>
: <String, dynamic>{};
if (data['code']?.toString() != '200') throw Exception('Failed');
return data;
}
Future<Map<String, dynamic>> kategori({
required String noTelepon,
required String idMerchant,
}) async {
final Response<dynamic> response = await _client.raw.post(
'Merchant/kategori',
data: {'no_telepon': noTelepon, 'idmerchant': idMerchant},
);
final data = response.data is Map<String, dynamic>
? response.data as Map<String, dynamic>
: <String, dynamic>{};
if (data['code']?.toString() != '200') throw Exception('Failed');
return data;
}
Future<List<dynamic>> item({
required String noTelepon,
required String idMerchant,
required String idKategori,
}) async {
final Response<dynamic> response = await _client.raw.post(
'Merchant/item',
data: {
'no_telepon': noTelepon,
'idmerchant': idMerchant,
'idkategori': idKategori,
},
);
final data = response.data is Map<String, dynamic>
? response.data as Map<String, dynamic>
: <String, dynamic>{};
if (data['code']?.toString() != '200') throw Exception('Failed');
return data['data'] as List<dynamic>? ?? [];
}
}
@@ -0,0 +1,86 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:ontime_merchant_flutter/features/auth/application/auth_controller.dart';
class HomeShell extends ConsumerStatefulWidget {
const HomeShell({super.key});
@override
ConsumerState<HomeShell> createState() => _HomeShellState();
}
class _HomeShellState extends ConsumerState<HomeShell> {
int _index = 0;
@override
Widget build(BuildContext context) {
final merchant = ref.watch(authControllerProvider).merchant;
final pages = <Widget>[
const Center(child: Text('Toko pesanan & saldo')),
const Center(child: Text('Riwayat')),
const Center(child: Text('Chat')),
const Center(child: Text('Menu (kategori & item)')),
_ProfileTab(),
];
return Scaffold(
appBar: AppBar(
title: Text(merchant?.namaMerchant ?? 'Merchant'),
),
body: SafeArea(child: pages[_index]),
bottomNavigationBar: NavigationBar(
selectedIndex: _index,
onDestinationSelected: (i) => setState(() => _index = i),
destinations: const [
NavigationDestination(
icon: Icon(Icons.store_outlined),
selectedIcon: Icon(Icons.store),
label: 'Toko',
),
NavigationDestination(
icon: Icon(Icons.history),
selectedIcon: Icon(Icons.history),
label: 'Riwayat',
),
NavigationDestination(
icon: Icon(Icons.chat_outlined),
selectedIcon: Icon(Icons.chat),
label: 'Chat',
),
NavigationDestination(
icon: Icon(Icons.restaurant_menu_outlined),
selectedIcon: Icon(Icons.restaurant_menu),
label: 'Menu',
),
NavigationDestination(
icon: Icon(Icons.settings_outlined),
selectedIcon: Icon(Icons.settings),
label: 'Pengaturan',
),
],
),
);
}
}
class _ProfileTab extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ElevatedButton(
onPressed: () async {
ref.read(authControllerProvider.notifier).logout();
await AuthController.clearStored();
if (context.mounted) context.go('/auth/login');
},
child: const Text('Keluar'),
),
],
),
);
}
}
+21
View File
@@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'core/app_router.dart';
import 'core/app_theme.dart';
import 'core/fcm_service.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await FcmService.init();
runApp(
ProviderScope(
child: MaterialApp.router(
debugShowCheckedModeBanner: false,
title: 'Ontime Merchant',
theme: AppTheme.light(),
routerConfig: createAppRouter(),
),
),
);
}