You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
494 lines
14 KiB
494 lines
14 KiB
import 'dart:convert';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:supplier_new/common/settings.dart';
|
|
import 'package:supplier_new/plans/plan_details.dart';
|
|
import 'package:supplier_new/plans/plan_requests.dart';
|
|
import 'package:supplier_new/plans/search_plan_appbar.dart';
|
|
|
|
/// =====================
|
|
/// MODEL
|
|
/// =====================
|
|
class PlansModel {
|
|
final String id;
|
|
final String status;
|
|
final String apartment;
|
|
final String liters;
|
|
final String price;
|
|
final String advance;
|
|
final String deliveries;
|
|
final String frequency;
|
|
final String waterType;
|
|
|
|
PlansModel({
|
|
required this.id,
|
|
required this.status,
|
|
required this.apartment,
|
|
required this.liters,
|
|
required this.price,
|
|
required this.advance,
|
|
required this.deliveries,
|
|
required this.frequency,
|
|
required this.waterType,
|
|
});
|
|
|
|
factory PlansModel.fromJson(Map<String, dynamic> json) {
|
|
final supplier = json["my_supplier"] ?? {};
|
|
final List dates = json["dates"] ?? [];
|
|
|
|
return PlansModel(
|
|
id: json["_id"],
|
|
status: json["status"] == "processed" ? "Active" : "Pending",
|
|
apartment: json["customerId"],
|
|
liters: "${json["capacity"]} - ${json["type_of_water"]}",
|
|
price: "₹${supplier["quoted_amount"] ?? "--"}",
|
|
advance: "--",
|
|
deliveries: "${dates.length} Deliveries",
|
|
frequency: "${json["weekly_count"]}/week",
|
|
waterType: json["type_of_water"] ?? "",
|
|
);
|
|
}
|
|
}
|
|
|
|
/// =====================
|
|
/// SCREEN
|
|
/// =====================
|
|
///
|
|
Future<bool> showAcceptConfirmDialog(BuildContext context) async {
|
|
return await showDialog<bool>(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (ctx) {
|
|
return AlertDialog(
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
title: const Text("Confirm Acceptance"),
|
|
content: const Text(
|
|
"Are you sure you want to accept this recurring plan?",
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(ctx, false),
|
|
child: const Text("Cancel"),
|
|
),
|
|
ElevatedButton(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0XFF8270DB),
|
|
),
|
|
onPressed: () => Navigator.pop(ctx, true),
|
|
child: const Text("Accept"),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
) ??
|
|
false;
|
|
}
|
|
|
|
///
|
|
class AllPlans extends StatefulWidget {
|
|
final String navigationFrom;
|
|
const AllPlans({super.key, required this.navigationFrom});
|
|
|
|
@override
|
|
State<AllPlans> createState() => _AllPlansState();
|
|
}
|
|
|
|
class _AllPlansState extends State<AllPlans> {
|
|
final TextEditingController searchController = TextEditingController();
|
|
|
|
bool isLoading = true;
|
|
|
|
/// UI MODELS
|
|
List<PlansModel> plans = [];
|
|
|
|
/// RAW API DATA (IMPORTANT)
|
|
List<Map<String, dynamic>> rawPlans = [];
|
|
|
|
int activeCount = 0;
|
|
int boreCount = 0;
|
|
int drinkingCount = 0;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
fetchPlans();
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// =====================
|
|
/// FETCH API
|
|
/// =====================
|
|
Future<void> fetchPlans() async {
|
|
try {
|
|
final response = await AppSettings.getAcceptedRecurringBookings();
|
|
final decoded = jsonDecode(response);
|
|
|
|
final List data = decoded["data"] ?? [];
|
|
|
|
/// ✅ FILTER ONLY payment_completed
|
|
rawPlans = List<Map<String, dynamic>>.from(
|
|
data.where((e) =>
|
|
["processed", "payment_completed"]
|
|
.contains((e["status"] ?? "").toString().toLowerCase())),
|
|
);
|
|
|
|
plans = rawPlans.map((e) => PlansModel.fromJson(e)).toList();
|
|
|
|
/// COUNTS (NOW ONLY payment_completed DATA)
|
|
activeCount = plans.length;
|
|
|
|
boreCount =
|
|
plans.where((p) => p.waterType.toLowerCase().contains("bore")).length;
|
|
|
|
drinkingCount = plans
|
|
.where((p) => p.waterType.toLowerCase().contains("drinking"))
|
|
.length;
|
|
|
|
setState(() => isLoading = false);
|
|
} catch (e) {
|
|
setState(() => isLoading = false);
|
|
}
|
|
}
|
|
|
|
/// =====================
|
|
/// UI
|
|
/// =====================
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
|
|
|
|
return Scaffold(
|
|
backgroundColor: Colors.white,
|
|
appBar: widget.navigationFrom.toLowerCase() == 'dashboard'
|
|
? SearchPlanAppBar(
|
|
controller: searchController,
|
|
onBack: () => Navigator.pop(context),
|
|
onHelp: () {},
|
|
)
|
|
: null,
|
|
body: isLoading
|
|
? const Center(child: CircularProgressIndicator())
|
|
: SingleChildScrollView(
|
|
child: Column(
|
|
children: [
|
|
/// 🔹 TOP SECTION
|
|
Container(
|
|
decoration: const BoxDecoration(
|
|
color: Color(0XFFF2F2F2),
|
|
borderRadius: BorderRadius.only(
|
|
bottomLeft: Radius.circular(24),
|
|
bottomRight: Radius.circular(24),
|
|
),
|
|
),
|
|
padding: const EdgeInsets.all(24),
|
|
child: Column(
|
|
children: [
|
|
Text(
|
|
activeCount.toString().padLeft(2, '0'),
|
|
style: fontTextStyle(
|
|
64, const Color(0XFF2D2E30), FontWeight.w700),
|
|
),
|
|
Text(
|
|
"Active Plans",
|
|
style: fontTextStyle(
|
|
24, const Color(0XFF2D2E30), FontWeight.w600),
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: [
|
|
PlanCategoryCard(
|
|
image: Image.asset('images/bore-water.png',
|
|
height: 40, width: 40),
|
|
value: boreCount.toString().padLeft(2, '0'),
|
|
label: "Bore Water",
|
|
),
|
|
PlanCategoryCard(
|
|
image: Image.asset('images/drinking-water.png',
|
|
height: 40, width: 40),
|
|
value:
|
|
drinkingCount.toString().padLeft(2, '0'),
|
|
label: "Drinking Water",
|
|
),
|
|
],
|
|
),
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0XFF8270DB),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(32),
|
|
),
|
|
padding: const EdgeInsets.all(16),
|
|
),
|
|
onPressed: () {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (_) =>
|
|
const PlanRequestsPage()),
|
|
);
|
|
},
|
|
child: Text(
|
|
"View Plan Requests",
|
|
style: fontTextStyle(
|
|
16, Colors.white, FontWeight.w500),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
/// 🔹 PLAN LIST
|
|
|
|
|
|
isLoading
|
|
? Center(
|
|
child: CircularProgressIndicator())
|
|
: (plans.isEmpty
|
|
? Center(
|
|
child: Padding(
|
|
padding:
|
|
const EdgeInsets.symmetric(
|
|
vertical: 50),
|
|
child: Text(
|
|
'No Data Available',
|
|
style: fontTextStyle(
|
|
12,
|
|
const Color(0XFF2D2E30),
|
|
FontWeight.w500),
|
|
),
|
|
),
|
|
)
|
|
: ListView.builder(
|
|
padding: const EdgeInsets.all(12),
|
|
shrinkWrap: true,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
itemCount: plans.length,
|
|
itemBuilder: (_, index) {
|
|
return PlansCard(
|
|
delivery: plans[index],
|
|
planJson: rawPlans[index], // ✅ FIXED
|
|
);
|
|
},
|
|
)),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// =====================
|
|
/// PLAN CARD
|
|
/// =====================
|
|
class PlansCard extends StatelessWidget {
|
|
final PlansModel delivery;
|
|
final Map<String, dynamic> planJson;
|
|
|
|
const PlansCard({
|
|
super.key,
|
|
required this.delivery,
|
|
required this.planJson,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
bool isActive = delivery.status == "Active";
|
|
|
|
return GestureDetector(
|
|
onTap: () {
|
|
if (delivery.status != "Active") return;
|
|
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (_) => PlanDetails(plan: planJson),
|
|
),
|
|
);
|
|
},
|
|
child: Container(
|
|
margin: const EdgeInsets.only(bottom: 12),
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: const Color(0xFFE5E5E5)),
|
|
color: Colors.white,
|
|
),
|
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: isActive ? const Color(0xFFE9F9EE) : Colors.grey.shade200,
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
child: Text(
|
|
delivery.status,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: isActive
|
|
? const Color(0xFF3BB273)
|
|
: Colors.grey,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(delivery.apartment,
|
|
style: const TextStyle(
|
|
fontSize: 15, fontWeight: FontWeight.w600)),
|
|
Text(delivery.price,
|
|
style: const TextStyle(
|
|
fontSize: 15, fontWeight: FontWeight.w600)),
|
|
],
|
|
),
|
|
const SizedBox(height: 4),
|
|
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(delivery.liters,
|
|
style: const TextStyle(
|
|
fontSize: 14, color: Color(0xFF7B5AF4))),
|
|
Text(delivery.advance,
|
|
style: const TextStyle(fontSize: 12)),
|
|
],
|
|
),
|
|
Visibility(
|
|
visible: delivery.status=='payment_completed',
|
|
child: Row(
|
|
children: [
|
|
GestureDetector(
|
|
onTap: (){
|
|
|
|
},
|
|
child:
|
|
ColorFiltered(
|
|
colorFilter: ColorFilter.mode(
|
|
Colors.transparent,
|
|
BlendMode.multiply),
|
|
child: Image
|
|
.asset(
|
|
'images/wrong_button.png',
|
|
width:
|
|
44,
|
|
height:
|
|
44,
|
|
),
|
|
),
|
|
),
|
|
SizedBox(
|
|
width: MediaQuery.of(context)
|
|
.size
|
|
.width *
|
|
.012,
|
|
),
|
|
GestureDetector(
|
|
onTap: () async {
|
|
final confirmed = await showAcceptConfirmDialog(context);
|
|
if (!confirmed) return;
|
|
|
|
try {
|
|
final res = await AppSettings.respondRecurringBooking(
|
|
bookingId: planJson["_id"],
|
|
supplierId: planJson["my_supplier"]["supplierId"],
|
|
action: "accept",
|
|
);
|
|
|
|
final List dates = res["data"]["dates"] ?? [];
|
|
|
|
if (dates.isEmpty) return;
|
|
|
|
/* Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (_) => PlanCalendarScreen(dates: dates),
|
|
),
|
|
);*/
|
|
} catch (e) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text("Failed to accept plan")),
|
|
);
|
|
}
|
|
},
|
|
child: Image.asset(
|
|
'images/right_button.png',
|
|
width: 44,
|
|
height: 44,
|
|
),
|
|
),
|
|
|
|
|
|
],
|
|
),),
|
|
|
|
const SizedBox(height: 12),
|
|
|
|
Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFF8F6FF),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(delivery.deliveries,
|
|
style: const TextStyle(fontSize: 13)),
|
|
Text(delivery.frequency,
|
|
style: const TextStyle(
|
|
fontSize: 13, color: Color(0xFF7B5AF4))),
|
|
],
|
|
),
|
|
),
|
|
]),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// =====================
|
|
/// CATEGORY CARD
|
|
/// =====================
|
|
class PlanCategoryCard extends StatelessWidget {
|
|
final Image image;
|
|
final String value;
|
|
final String label;
|
|
|
|
const PlanCategoryCard({
|
|
super.key,
|
|
required this.image,
|
|
required this.value,
|
|
required this.label,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
children: [
|
|
SizedBox(width: 40, height: 40, child: image),
|
|
const SizedBox(height: 8),
|
|
Text(value,
|
|
style:
|
|
fontTextStyle(20, const Color(0XFF515253), FontWeight.w700)),
|
|
Text(label,
|
|
style:
|
|
fontTextStyle(16, const Color(0XFF515253), FontWeight.w400)),
|
|
],
|
|
);
|
|
}
|
|
}
|