Skip to content

Commit 8fe7a55

Browse files
author
Dang
committed
add BE
1 parent 596b74a commit 8fe7a55

31 files changed

+3777
-25
lines changed

e_shoppe/lib/config.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/// Global configuration values
2+
/// Pass a different API base at runtime with:
3+
/// flutter run --dart-define=API_BASE=https://api.myshop.com
4+
const String kApiBase = String.fromEnvironment(
5+
'API_BASE',
6+
defaultValue: 'http://localhost:8080',
7+
);

e_shoppe/lib/data/models/user.dart

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,29 +8,41 @@ class User extends Equatable {
88
required this.email,
99
required this.name,
1010
this.address,
11+
this.phone,
1112
});
1213

1314
final String id;
1415
final String email;
1516
final String name;
1617
final String? address;
18+
final String? phone;
1719

1820
factory User.empty() => const User(id: '', email: '', name: '');
1921

2022
bool get isEmpty => id.isEmpty;
2123

2224
factory User.fromJson(Map<String, dynamic> json) => User(
23-
id: json['id'] as String,
25+
id: json['id'].toString(),
2426
email: json['email'] as String,
2527
name: json['name'] as String,
2628
address: json['address'] as String?,
29+
phone: json['phone'] as String?,
30+
);
31+
32+
factory User.fromJsonDummy(Map<String, dynamic> json) => User(
33+
id: json['id'].toString(),
34+
email: json['email'] as String,
35+
name: '${json['firstName']} ${json['lastName']}',
36+
address: json['address']?['address'] as String?,
37+
phone: json['phone'] as String?,
2738
);
2839

2940
Map<String, dynamic> toJson() => {
3041
'id': id,
3142
'email': email,
3243
'name': name,
3344
'address': address,
45+
'phone': phone,
3446
};
3547

3648
String toRawJson() => jsonEncode(toJson());
@@ -39,5 +51,5 @@ class User extends Equatable {
3951
User.fromJson(jsonDecode(source) as Map<String, dynamic>);
4052

4153
@override
42-
List<Object?> get props => [id, email, name, address];
54+
List<Object?> get props => [id, email, name, address, phone];
4355
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import 'package:dio/dio.dart';
2+
import '../models/user.dart';
3+
4+
class CustomerRepository {
5+
CustomerRepository({Dio? dio}) : _dio = dio ?? Dio();
6+
final Dio _dio;
7+
static const _baseUrl = 'https://dummyjson.com';
8+
9+
Future<List<User>> searchCustomers(String query) async {
10+
if (query.isEmpty) return [];
11+
final response =
12+
await _dio.get('$_baseUrl/users/search', queryParameters: {'q': query});
13+
if (response.statusCode != 200) throw Exception('Failed');
14+
final List<dynamic> usersJson = (response.data['users'] as List<dynamic>);
15+
return usersJson
16+
.map((e) => User.fromJsonDummy(e as Map<String, dynamic>))
17+
.toList();
18+
}
19+
20+
void dispose() => _dio.close(force: true);
21+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import 'package:flutter_riverpod/flutter_riverpod.dart';
2+
3+
import '../../services/api_client.dart';
4+
import '../models/cart_item.dart';
5+
import '../models/user.dart';
6+
7+
class OrderRepository {
8+
OrderRepository(this._api);
9+
final ApiClient _api;
10+
11+
Future<Map<String, dynamic>> createOrder(
12+
User customer, List<CartItem> items) {
13+
final body = {
14+
'customer': {
15+
'id': customer.id,
16+
'name': customer.name,
17+
'email': customer.email,
18+
},
19+
'items': items
20+
.map((e) => {
21+
'productId': e.product.id.toString(),
22+
'quantity': e.quantity,
23+
})
24+
.toList(),
25+
};
26+
return _api.post('/orders', body).then((e) => e as Map<String, dynamic>);
27+
}
28+
29+
Future<List<Map<String, dynamic>>> listOrders() async {
30+
final List<dynamic> json = await _api.get('/orders') as List<dynamic>;
31+
return json.cast<Map<String, dynamic>>();
32+
}
33+
34+
Future<Map<String, dynamic>> getOrder(String id) async {
35+
return _api.get('/orders/$id').then((e) => e as Map<String, dynamic>);
36+
}
37+
38+
Future<void> updateStatus(String id, String status) async {
39+
await _api.patch('/orders/$id/status', {'status': status});
40+
}
41+
}
42+
43+
final orderRepositoryProvider = Provider<OrderRepository>((ref) {
44+
final api = ref.read(apiProvider);
45+
return OrderRepository(api);
46+
});
Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,28 @@
11
import '../models/product.dart';
2-
import 'package:dio/dio.dart';
2+
import 'package:flutter_riverpod/flutter_riverpod.dart';
33

4-
class ProductRepository {
5-
ProductRepository({Dio? dio}) : _dio = dio ?? Dio();
6-
7-
static const _baseUrl = 'https://fakestoreapi.com';
4+
import '../../services/api_client.dart';
85

9-
final Dio _dio;
10-
11-
Future<List<Product>> fetchProducts(
12-
{String? query, int? page, int limit = 20}) async {
13-
final response = await _dio.get('$_baseUrl/products', queryParameters: {
14-
if (page != null) 'limit': limit, // fakestoreapi supports limit
15-
if (page != null) 'sort': 'desc',
16-
});
6+
class ProductRepository {
7+
ProductRepository(this._api);
178

18-
if (response.statusCode != 200) {
19-
throw Exception('Failed to load products');
20-
}
9+
final ApiClient _api;
2110

22-
final List<dynamic> data = response.data as List<dynamic>;
11+
Future<List<Product>> fetchProducts({String? query}) async {
12+
final List<dynamic> json = await _api.get('/products') as List<dynamic>;
2313
var products =
24-
data.map((e) => Product.fromJson(e as Map<String, dynamic>)).toList();
25-
14+
json.map((e) => Product.fromJson(e as Map<String, dynamic>)).toList();
2615
if (query == null || query.isEmpty) return products;
2716
final lower = query.toLowerCase();
28-
products = products
17+
return products
2918
.where((p) =>
3019
p.title.toLowerCase().contains(lower) ||
3120
p.description.toLowerCase().contains(lower))
3221
.toList();
33-
return products;
3422
}
35-
36-
void dispose() => _dio.close(force: true);
3723
}
24+
25+
final productRepositoryProvider = Provider<ProductRepository>((ref) {
26+
final api = ref.read(apiProvider);
27+
return ProductRepository(api);
28+
});
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_riverpod/flutter_riverpod.dart';
3+
import 'package:e_shoppe/data/models/user.dart';
4+
import 'package:e_shoppe/data/repositories/customer_repository.dart';
5+
import 'order_app_bar.dart';
6+
7+
class CustomerSearchPage extends ConsumerStatefulWidget {
8+
const CustomerSearchPage({super.key});
9+
10+
@override
11+
ConsumerState<CustomerSearchPage> createState() => _CustomerSearchPageState();
12+
}
13+
14+
class _CustomerSearchPageState extends ConsumerState<CustomerSearchPage> {
15+
final _searchCtrl = TextEditingController();
16+
final _repo = CustomerRepository();
17+
Future<List<User>>? _future;
18+
19+
void _search() {
20+
setState(() {
21+
_future = _repo.searchCustomers(_searchCtrl.text);
22+
});
23+
}
24+
25+
@override
26+
void dispose() {
27+
_repo.dispose();
28+
_searchCtrl.dispose();
29+
super.dispose();
30+
}
31+
32+
@override
33+
Widget build(BuildContext context) {
34+
return Scaffold(
35+
backgroundColor: const Color(0xFFE0E0E0),
36+
appBar: const OrderAppBar(),
37+
body: Column(
38+
children: [
39+
Padding(
40+
padding: const EdgeInsets.all(12.0),
41+
child: Row(
42+
children: [
43+
Expanded(
44+
child: TextField(
45+
controller: _searchCtrl,
46+
decoration: const InputDecoration(
47+
prefixIcon: Icon(Icons.search),
48+
hintText: 'Nhập SDT/ tên khách hàng',
49+
border: OutlineInputBorder(),
50+
),
51+
onSubmitted: (_) => _search(),
52+
),
53+
),
54+
const SizedBox(width: 8),
55+
ElevatedButton(onPressed: _search, child: const Text('Tìm')),
56+
],
57+
),
58+
),
59+
if (_future == null)
60+
const Expanded(
61+
child: Center(child: Text('Nhập từ khoá để tìm khách hàng')))
62+
else
63+
Expanded(
64+
child: FutureBuilder<List<User>>(
65+
future: _future,
66+
builder: (context, snapshot) {
67+
if (snapshot.connectionState == ConnectionState.waiting) {
68+
return const Center(child: CircularProgressIndicator());
69+
}
70+
if (snapshot.hasError) {
71+
return Center(child: Text('Error: ${snapshot.error}'));
72+
}
73+
final users = snapshot.data ?? [];
74+
if (users.isEmpty) {
75+
return const Center(
76+
child: Text('Không tìm thấy khách hàng'));
77+
}
78+
return ListView.separated(
79+
itemCount: users.length,
80+
separatorBuilder: (_, __) => const Divider(height: 1),
81+
itemBuilder: (context, index) {
82+
final u = users[index];
83+
return ListTile(
84+
title: Text(u.name),
85+
subtitle: Text(u.phone ?? u.email),
86+
onTap: () {
87+
Navigator.of(context).pop(u);
88+
},
89+
);
90+
},
91+
);
92+
},
93+
),
94+
),
95+
],
96+
),
97+
);
98+
}
99+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import 'package:flutter/material.dart';
2+
3+
class OrderAppBar extends StatelessWidget implements PreferredSizeWidget {
4+
const OrderAppBar({super.key});
5+
6+
@override
7+
Widget build(BuildContext context) {
8+
return Container(
9+
color: const Color(0xFF00CFFF),
10+
child: SafeArea(
11+
bottom: false,
12+
child: Container(
13+
height: kToolbarHeight,
14+
padding: const EdgeInsets.symmetric(horizontal: 12),
15+
child: Row(
16+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
17+
children: [
18+
// Placeholder logo
19+
const Icon(Icons.image, color: Colors.white),
20+
const CircleAvatar(
21+
radius: 18,
22+
backgroundColor: Colors.white,
23+
child: Icon(
24+
Icons.person_outline,
25+
color: Color(0xFF00CFFF),
26+
),
27+
),
28+
],
29+
),
30+
),
31+
),
32+
);
33+
}
34+
35+
@override
36+
Size get preferredSize => const Size.fromHeight(kToolbarHeight + 24);
37+
}

0 commit comments

Comments
 (0)