Skip to content

Commit e294e7d

Browse files
committed
refactor: AI refactor - Not fully reviewed!
1 parent eba5325 commit e294e7d

14 files changed

Lines changed: 2062 additions & 2369 deletions

lib/pages/shopinbit/shopinbit_step_4.dart

Lines changed: 58 additions & 2369 deletions
Large diffs are not rendered by default.
Lines changed: 347 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,347 @@
1+
import "dart:async";
2+
3+
import "package:flutter/material.dart";
4+
import "package:flutter/services.dart";
5+
6+
import "../../../db/isar/main_db.dart";
7+
import "../../../models/shopinbit/shopinbit_order_model.dart";
8+
import "../../../themes/stack_colors.dart";
9+
import "../../../utilities/text_styles.dart";
10+
import "../../../utilities/util.dart";
11+
import "../../../widgets/rounded_white_container.dart";
12+
import "../shopinbit_car_fee_view.dart";
13+
import "../shopinbit_tickets_view.dart";
14+
import "shopinbit_country_picker.dart";
15+
import "shopinbit_labeled_checkbox.dart";
16+
import "shopinbit_privacy_checkbox.dart";
17+
import "shopinbit_step4_dropdown.dart";
18+
import "shopinbit_step4_header.dart";
19+
import "shopinbit_step4_submit_button.dart";
20+
import "shopinbit_step4_text_field.dart";
21+
22+
const List<String> _carConditions = ["NEW", "PREOWNED"];
23+
24+
const int _minCarBudget = 20000;
25+
const int _minCarFieldLength = 3;
26+
27+
class ShopInBitCarResearchForm extends StatefulWidget {
28+
const ShopInBitCarResearchForm({super.key, required this.model});
29+
30+
final ShopInBitOrderModel model;
31+
32+
@override
33+
State<ShopInBitCarResearchForm> createState() =>
34+
_ShopInBitCarResearchFormState();
35+
}
36+
37+
class _ShopInBitCarResearchFormState extends State<ShopInBitCarResearchForm> {
38+
final TextEditingController _brandController = TextEditingController();
39+
final FocusNode _brandFocusNode = FocusNode();
40+
bool _brandTouched = false;
41+
42+
final TextEditingController _modelController = TextEditingController();
43+
final FocusNode _modelFocusNode = FocusNode();
44+
bool _modelTouched = false;
45+
46+
final TextEditingController _carDescriptionController =
47+
TextEditingController();
48+
final FocusNode _carDescriptionFocusNode = FocusNode();
49+
bool _carDescriptionTouched = false;
50+
51+
final TextEditingController _carBudgetController = TextEditingController();
52+
final FocusNode _carBudgetFocusNode = FocusNode();
53+
bool _carBudgetTouched = false;
54+
55+
String? _selectedCarCondition;
56+
bool _feeAcknowledged = false;
57+
String? _selectedCountryIso;
58+
bool _privacyAccepted = false;
59+
bool _submitting = false;
60+
61+
@override
62+
void initState() {
63+
super.initState();
64+
_wireTouchOnBlur(_brandFocusNode, () => _brandTouched = true);
65+
_wireTouchOnBlur(_modelFocusNode, () => _modelTouched = true);
66+
_wireTouchOnBlur(
67+
_carDescriptionFocusNode,
68+
() => _carDescriptionTouched = true,
69+
);
70+
_wireTouchOnBlur(_carBudgetFocusNode, () => _carBudgetTouched = true);
71+
if (widget.model.deliveryCountry.isNotEmpty) {
72+
_selectedCountryIso = widget.model.deliveryCountry;
73+
}
74+
}
75+
76+
void _wireTouchOnBlur(FocusNode node, VoidCallback markTouched) {
77+
node.addListener(() {
78+
if (!node.hasFocus) markTouched();
79+
setState(() {});
80+
});
81+
}
82+
83+
@override
84+
void dispose() {
85+
_brandController.dispose();
86+
_brandFocusNode.dispose();
87+
_modelController.dispose();
88+
_modelFocusNode.dispose();
89+
_carDescriptionController.dispose();
90+
_carDescriptionFocusNode.dispose();
91+
_carBudgetController.dispose();
92+
_carBudgetFocusNode.dispose();
93+
super.dispose();
94+
}
95+
96+
bool get _canContinue {
97+
final int? carBudgetValue = int.tryParse(_carBudgetController.text.trim());
98+
return !_submitting &&
99+
_privacyAccepted &&
100+
_feeAcknowledged &&
101+
_brandController.text.trim().length >= _minCarFieldLength &&
102+
_modelController.text.trim().length >= _minCarFieldLength &&
103+
_carDescriptionController.text.trim().length >= _minCarFieldLength &&
104+
_selectedCarCondition != null &&
105+
carBudgetValue != null &&
106+
carBudgetValue >= _minCarBudget &&
107+
_selectedCountryIso != null;
108+
}
109+
110+
Future<void> _submit() async {
111+
setState(() => _submitting = true);
112+
try {
113+
final String countryIso = _selectedCountryIso!;
114+
115+
widget.model
116+
..requestDescription =
117+
"Brand: ${_brandController.text.trim()}\n"
118+
"Model: ${_modelController.text.trim()}\n"
119+
"Condition: $_selectedCarCondition\n"
120+
"Description: ${_carDescriptionController.text.trim()}\n"
121+
"Budget: ${_carBudgetController.text.trim()} EUR\n"
122+
"Delivery country: $countryIso"
123+
..deliveryCountry = countryIso;
124+
125+
// Block if another car research flow is already in progress.
126+
final existingPending = MainDB.instance
127+
.getShopInBitTickets()
128+
.where((t) => t.isPendingPayment)
129+
.toList();
130+
131+
if (existingPending.isNotEmpty && mounted) {
132+
final bool? resumePrevious = await showDialog<bool>(
133+
context: context,
134+
barrierDismissible: false,
135+
builder: (ctx) => AlertDialog(
136+
title: const Text("In-Progress Car Research"),
137+
content: const Text(
138+
"You have an unfinished car research payment. "
139+
"Would you like to resume it or start a new search?",
140+
),
141+
actions: [
142+
TextButton(
143+
onPressed: () => Navigator.of(ctx).pop(true),
144+
child: const Text("Resume Previous"),
145+
),
146+
TextButton(
147+
onPressed: () => Navigator.of(ctx).pop(false),
148+
child: const Text("Start New"),
149+
),
150+
],
151+
),
152+
);
153+
154+
if (resumePrevious == true && mounted) {
155+
unawaited(
156+
Navigator.of(context).pushNamedAndRemoveUntil(
157+
ShopInBitTicketsView.routeName,
158+
(route) => route.isFirst,
159+
),
160+
);
161+
return;
162+
}
163+
}
164+
165+
if (!mounted) return;
166+
167+
if (Util.isDesktop) {
168+
Navigator.of(context, rootNavigator: true).pop();
169+
unawaited(
170+
showDialog<void>(
171+
context: context,
172+
builder: (_) => ShopInBitCarFeeView(model: widget.model),
173+
),
174+
);
175+
} else {
176+
unawaited(
177+
Navigator.of(
178+
context,
179+
).pushNamed(ShopInBitCarFeeView.routeName, arguments: widget.model),
180+
);
181+
}
182+
} finally {
183+
if (mounted) setState(() => _submitting = false);
184+
}
185+
}
186+
187+
@override
188+
Widget build(BuildContext context) {
189+
final bool isDesktop = Util.isDesktop;
190+
191+
final String? brandError =
192+
_brandTouched &&
193+
_brandController.text.trim().length < _minCarFieldLength
194+
? "Minimum $_minCarFieldLength characters"
195+
: null;
196+
197+
final String? modelError =
198+
_modelTouched &&
199+
_modelController.text.trim().length < _minCarFieldLength
200+
? "Minimum $_minCarFieldLength characters"
201+
: null;
202+
203+
final String? carDescriptionError =
204+
_carDescriptionTouched &&
205+
_carDescriptionController.text.trim().length < _minCarFieldLength
206+
? "Minimum $_minCarFieldLength characters"
207+
: null;
208+
209+
final String carBudgetText = _carBudgetController.text.trim();
210+
final int? carBudgetValue = int.tryParse(carBudgetText);
211+
final String? carBudgetError =
212+
_carBudgetTouched &&
213+
(carBudgetText.isEmpty ||
214+
carBudgetValue == null ||
215+
carBudgetValue < _minCarBudget)
216+
? "Minimum budget is 20,000\u20AC"
217+
: null;
218+
219+
return Column(
220+
crossAxisAlignment: CrossAxisAlignment.stretch,
221+
children: [
222+
const ShopInBitStep4Header(
223+
title: "Car Research request",
224+
subtitle: "Tell us about the car you're looking for.",
225+
),
226+
SizedBox(height: isDesktop ? 32 : 24),
227+
ShopInBitCountryPicker(
228+
selectedIso: _selectedCountryIso,
229+
onChanged: (iso) => setState(() => _selectedCountryIso = iso),
230+
),
231+
SizedBox(height: isDesktop ? 24 : 16),
232+
ShopInBitStep4TextField(
233+
controller: _brandController,
234+
focusNode: _brandFocusNode,
235+
hintText: "Car brand (e.g., BMW, Mercedes, Toyota...)",
236+
errorText: brandError,
237+
onChanged: (_) => setState(() {}),
238+
),
239+
SizedBox(height: isDesktop ? 24 : 16),
240+
ShopInBitStep4TextField(
241+
controller: _modelController,
242+
focusNode: _modelFocusNode,
243+
hintText: "Car model (e.g., 3 Series, E-Class, Camry...)",
244+
errorText: modelError,
245+
onChanged: (_) => setState(() {}),
246+
),
247+
SizedBox(height: isDesktop ? 24 : 16),
248+
ShopInBitStep4Dropdown(
249+
value: _selectedCarCondition,
250+
items: _carConditions,
251+
hintText: "Condition",
252+
onChanged: (value) => setState(() => _selectedCarCondition = value),
253+
),
254+
SizedBox(height: isDesktop ? 24 : 16),
255+
ShopInBitStep4TextField(
256+
controller: _carDescriptionController,
257+
focusNode: _carDescriptionFocusNode,
258+
hintText:
259+
"Describe your requirements "
260+
"(year, mileage, features...)",
261+
minLines: 3,
262+
maxLines: 6,
263+
errorText: carDescriptionError,
264+
onChanged: (_) => setState(() {}),
265+
),
266+
SizedBox(height: isDesktop ? 24 : 16),
267+
ShopInBitStep4TextField(
268+
controller: _carBudgetController,
269+
focusNode: _carBudgetFocusNode,
270+
hintText: "Budget (\u20AC, minimum 20,000)",
271+
keyboardType: TextInputType.number,
272+
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
273+
suffixText: "\u20AC",
274+
errorText: carBudgetError,
275+
onChanged: (_) => setState(() {}),
276+
),
277+
SizedBox(height: isDesktop ? 24 : 16),
278+
_CarResearchFeeInfo(isDesktop: isDesktop),
279+
SizedBox(height: isDesktop ? 16 : 12),
280+
ShopInBitLabeledCheckbox(
281+
value: _feeAcknowledged,
282+
onChanged: (v) => setState(() => _feeAcknowledged = v),
283+
label: "I acknowledge the \u20AC223 research fee",
284+
),
285+
SizedBox(height: isDesktop ? 16 : 12),
286+
ShopInBitPrivacyCheckbox(
287+
value: _privacyAccepted,
288+
onChanged: (v) => setState(() => _privacyAccepted = v),
289+
),
290+
SizedBox(height: isDesktop ? 16 : 12),
291+
ShopInBitStep4SubmitButton(
292+
submitting: _submitting,
293+
enabled: _canContinue,
294+
onPressed: _submit,
295+
),
296+
],
297+
);
298+
}
299+
}
300+
301+
/// Info box showing the €223 (incl. VAT) research fee disclosure.
302+
class _CarResearchFeeInfo extends StatelessWidget {
303+
const _CarResearchFeeInfo({required this.isDesktop});
304+
305+
final bool isDesktop;
306+
307+
@override
308+
Widget build(BuildContext context) {
309+
final TextStyle baseStyle = isDesktop
310+
? STextStyles.desktopTextSmall(context)
311+
: STextStyles.w500_14(context);
312+
313+
return RoundedWhiteContainer(
314+
child: Row(
315+
crossAxisAlignment: CrossAxisAlignment.center,
316+
children: [
317+
Icon(
318+
Icons.info_outline,
319+
size: 20,
320+
color: Theme.of(
321+
context,
322+
).extension<StackColors>()!.textFieldActiveSearchIconLeft,
323+
),
324+
const SizedBox(width: 12),
325+
Expanded(
326+
child: RichText(
327+
text: TextSpan(
328+
style: baseStyle,
329+
children: [
330+
TextSpan(
331+
text: "Research fee: ",
332+
style: baseStyle.copyWith(fontWeight: FontWeight.bold),
333+
),
334+
const TextSpan(
335+
text:
336+
"\u20AC223 (incl. VAT): one-time payment, "
337+
"credited toward your purchase.",
338+
),
339+
],
340+
),
341+
),
342+
),
343+
],
344+
),
345+
);
346+
}
347+
}

0 commit comments

Comments
 (0)