Merge branch 'Service_Project_Feature' of https://git.marcoaiot.com/admin/marco.pms.mobileapp into Service_Project_Feature

This commit is contained in:
Vaibhav Surve 2025-11-11 14:21:01 +05:30
commit f232ab42b0
2 changed files with 552 additions and 0 deletions

View File

@ -0,0 +1,124 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/controller/project_controller.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
class ServiceProjectDetailsScreen extends StatefulWidget {
const ServiceProjectDetailsScreen({super.key});
@override
State<ServiceProjectDetailsScreen> createState() =>
_ServiceProjectDetailsScreenState();
}
class _ServiceProjectDetailsScreenState
extends State<ServiceProjectDetailsScreen> with SingleTickerProviderStateMixin {
late TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
appBar: PreferredSize(
preferredSize: const Size.fromHeight(72),
child: AppBar(
backgroundColor: const Color(0xFFF5F5F5),
elevation: 0.5,
automaticallyImplyLeading: false,
titleSpacing: 0,
title: Padding(
padding: MySpacing.xy(16, 0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.arrow_back_ios_new,
color: Colors.black, size: 20),
onPressed: () => Get.offNamed('/dashboard'),
),
MySpacing.width(8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
MyText.titleLarge(
'Service Projects',
fontWeight: 700,
color: Colors.black,
),
MySpacing.height(2),
GetBuilder<ProjectController>(
builder: (projectController) {
final projectName =
projectController.selectedProject?.name ??
'Select Project';
return Row(
children: [
const Icon(Icons.work_outline,
size: 14, color: Colors.grey),
MySpacing.width(4),
Expanded(
child: MyText.bodySmall(
projectName,
fontWeight: 600,
overflow: TextOverflow.ellipsis,
color: Colors.grey[700],
),
),
],
);
},
),
],
),
),
],
),
),
),
),
body: Column(
children: [
// ---------------- TabBar ----------------
Container(
color: Colors.white,
child: TabBar(
controller: _tabController,
labelColor: Colors.black,
unselectedLabelColor: Colors.grey,
indicatorColor: Colors.red,
tabs: const [
Tab(text: "Profile"),
Tab(text: "Jobs"),
],
),
),
// ---------------- TabBarView ----------------
Expanded(
child: TabBarView(
controller: _tabController,
children: const [
// Add your tab content here later
],
),
),
],
),
);
}
}

View File

@ -0,0 +1,428 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_refresh_indicator.dart';
import 'package:marco/helpers/widgets/avatar.dart';
import 'package:marco/controller/project_controller.dart';
class ServiceProjectScreen extends StatefulWidget {
const ServiceProjectScreen({super.key});
@override
State<ServiceProjectScreen> createState() => _ServiceProjectScreenState();
}
class _ServiceProjectScreenState extends State<ServiceProjectScreen>
with UIMixin {
final TextEditingController searchController = TextEditingController();
final RxList<Map<String, dynamic>> allProjects = <Map<String, dynamic>>[].obs;
final RxList<Map<String, dynamic>> filteredProjects =
<Map<String, dynamic>>[].obs;
@override
void initState() {
super.initState();
_loadProjects();
}
void _loadProjects() {
final staticProjects = [
{
"name": "Website Redesign",
"description": "Revamping the corporate website UI/UX",
"status": "In Progress",
"manager": "John Doe",
"email": "john@company.com",
"phone": "+91 9876543210",
"tags": ["UI", "Frontend", "High Priority"]
},
{
"name": "Mobile App Development",
"description": "Cross-platform mobile app for customers",
"status": "Completed",
"manager": "Priya Sharma",
"email": "priya@company.com",
"phone": "+91 9812345678",
"tags": ["Flutter", "Backend"]
},
{
"name": "Data Migration",
"description": "Migrating legacy data to AWS",
"status": "Pending",
"manager": "Arun Mehta",
"email": "arun@company.com",
"phone": "+91 9999988888",
"tags": ["Database", "Cloud"]
},
];
allProjects.assignAll(staticProjects);
filteredProjects.assignAll(staticProjects);
}
void _filterProjects(String query) {
if (query.isEmpty) {
filteredProjects.assignAll(allProjects);
} else {
filteredProjects.assignAll(allProjects
.where((p) =>
p["name"].toLowerCase().contains(query.toLowerCase()) ||
p["manager"].toLowerCase().contains(query.toLowerCase()))
.toList());
}
}
Future<void> _refreshProjects() async {
await Future.delayed(const Duration(seconds: 1));
}
Widget _buildProjectCard(Map<String, dynamic> project) {
return Card(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
elevation: 3,
shadowColor: Colors.grey.withOpacity(0.3),
color: Colors.white,
child: InkWell(
borderRadius: BorderRadius.circular(5),
onTap: () {
// TODO: Navigate to Project Details screen
},
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Avatar(
firstName: project["name"].split(" ").first,
lastName: project["name"].split(" ").length > 1
? project["name"].split(" ").last
: "",
size: 40,
),
MySpacing.width(12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.titleSmall(project["name"],
fontWeight: 600, overflow: TextOverflow.ellipsis),
MyText.bodySmall(project["description"],
color: Colors.grey[700],
overflow: TextOverflow.ellipsis),
MySpacing.height(6),
Row(
children: [
Icon(Icons.person_outline,
size: 16, color: Colors.indigo),
MySpacing.width(4),
MyText.labelSmall(project["manager"],
color: Colors.indigo),
],
),
MySpacing.height(4),
Row(
children: [
Icon(Icons.email_outlined,
size: 16, color: Colors.indigo),
MySpacing.width(4),
Expanded(
child: MyText.labelSmall(
project["email"],
color: Colors.indigo,
decoration: TextDecoration.underline,
overflow: TextOverflow.ellipsis,
),
),
],
),
MySpacing.height(4),
Row(
children: [
Icon(Icons.phone_outlined,
size: 16, color: Colors.indigo),
MySpacing.width(4),
Expanded(
child: MyText.labelSmall(
project["phone"],
color: Colors.indigo,
decoration: TextDecoration.underline,
overflow: TextOverflow.ellipsis,
),
),
MySpacing.width(8),
const FaIcon(FontAwesomeIcons.whatsapp,
color: Colors.green, size: 20),
],
),
MySpacing.height(6),
Wrap(
spacing: 6,
runSpacing: 2,
children: (project["tags"] as List<String>)
.map((tag) => Chip(
label: Text(tag),
backgroundColor: Colors.indigo.shade50,
labelStyle: const TextStyle(
color: Colors.indigo, fontSize: 12),
visualDensity: VisualDensity.compact,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5),
),
))
.toList(),
),
],
),
),
Column(
children: [
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: project["status"] == "Completed"
? Colors.green.shade100
: project["status"] == "In Progress"
? Colors.orange.shade100
: Colors.red.shade100,
borderRadius: BorderRadius.circular(12),
),
child: MyText.labelSmall(
project["status"],
fontWeight: 600,
color: project["status"] == "Completed"
? Colors.green
: project["status"] == "In Progress"
? Colors.orange
: Colors.red,
),
),
const SizedBox(height: 10),
const Icon(Icons.arrow_forward_ios,
color: Colors.grey, size: 20),
],
),
],
),
),
),
);
}
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.work_outline, size: 60, color: Colors.grey),
MySpacing.height(18),
MyText.titleMedium('No matching projects found.',
fontWeight: 600, color: Colors.grey),
MySpacing.height(10),
MyText.bodySmall('Try adjusting your filters or refresh.',
color: Colors.grey),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
/// --- SAME APPBAR AS DETAILS SCREEN ---
appBar: PreferredSize(
preferredSize: const Size.fromHeight(72),
child: AppBar(
backgroundColor: const Color(0xFFF5F5F5),
elevation: 0.5,
automaticallyImplyLeading: false,
titleSpacing: 0,
title: Padding(
padding: MySpacing.xy(16, 0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.arrow_back_ios_new,
color: Colors.black, size: 20),
onPressed: () => Get.offNamed('/dashboard'),
),
MySpacing.width(8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
MyText.titleLarge(
'Service Projects',
fontWeight: 700,
color: Colors.black,
),
MySpacing.height(2),
GetBuilder<ProjectController>(
builder: (projectController) {
final projectName =
projectController.selectedProject?.name ??
'All Projects';
return Row(
children: [
const Icon(Icons.work_outline,
size: 14, color: Colors.grey),
MySpacing.width(4),
Expanded(
child: MyText.bodySmall(
projectName,
fontWeight: 600,
overflow: TextOverflow.ellipsis,
color: Colors.grey[700],
),
),
],
);
},
),
],
),
),
],
),
),
),
),
body: Column(
children: [
/// --- SEARCH + FILTER BAR ---
Padding(
padding: MySpacing.xy(8, 8),
child: Row(
children: [
Expanded(
child: SizedBox(
height: 35,
child: TextField(
controller: searchController,
onChanged: _filterProjects,
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(horizontal: 12),
prefixIcon: const Icon(Icons.search,
size: 20, color: Colors.grey),
suffixIcon: ValueListenableBuilder<TextEditingValue>(
valueListenable: searchController,
builder: (context, value, _) {
if (value.text.isEmpty) {
return const SizedBox.shrink();
}
return IconButton(
icon: const Icon(Icons.clear,
size: 20, color: Colors.grey),
onPressed: () {
searchController.clear();
_filterProjects('');
},
);
},
),
hintText: 'Search projects...',
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: BorderSide(color: Colors.grey.shade300),
),
),
),
),
),
MySpacing.width(8),
Container(
height: 35,
width: 35,
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(5),
),
child: IconButton(
icon:
const Icon(Icons.tune, size: 20, color: Colors.black87),
onPressed: () {
// TODO: Open filter bottom sheet
},
),
),
MySpacing.width(10),
Container(
height: 35,
width: 35,
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(5),
),
child: PopupMenuButton<int>(
padding: EdgeInsets.zero,
icon: const Icon(Icons.more_vert,
size: 20, color: Colors.black87),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5)),
itemBuilder: (context) => [
const PopupMenuItem<int>(
enabled: false,
height: 30,
child: Text("Actions",
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.grey)),
),
const PopupMenuItem<int>(
value: 1,
child: Row(
children: [
SizedBox(width: 10),
Expanded(child: Text("Manage Projects")),
Icon(Icons.chevron_right,
size: 20, color: Colors.indigo),
],
),
),
],
),
),
],
),
),
/// --- PROJECT LIST ---
Expanded(
child: Obx(() => MyRefreshIndicator(
onRefresh: _refreshProjects,
backgroundColor: Colors.indigo,
color: Colors.white,
child: filteredProjects.isEmpty
? _buildEmptyState()
: ListView.separated(
physics: const AlwaysScrollableScrollPhysics(),
padding: MySpacing.only(
left: 8, right: 8, top: 4, bottom: 80),
itemCount: filteredProjects.length,
separatorBuilder: (_, __) => MySpacing.height(12),
itemBuilder: (_, index) =>
_buildProjectCard(filteredProjects[index]),
),
)),
),
],
),
);
}
}