Commit f6fbe101 authored by Mohit Kumar's avatar Mohit Kumar
Browse files

AttendanceList

RewardList
TourExpenses
Implementation
parent 6d1deaf2
import 'package:dotted_line/dotted_line.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../Notifiers/hrmProvider/leaveApplicationDetailsProvider.dart';
import '../../Notifiers/HomeScreenNotifier.dart';
import '../../Utils/app_colors.dart';
import '../finance/FileViewer.dart';
/// Screen for leave application details
class LeaveApplicationDetailScreen extends StatefulWidget {
final String leaveRequestId;
const LeaveApplicationDetailScreen({super.key, required this.leaveRequestId});
@override
State<LeaveApplicationDetailScreen> createState() => _LeaveApplicationDetailScreenState();
}
class _LeaveApplicationDetailScreenState extends State<LeaveApplicationDetailScreen> {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => LeaveApplicationDetailsProvider()..fetchLeaveApplicationDetails(context, widget.leaveRequestId),
child: Consumer<LeaveApplicationDetailsProvider>(
builder: (context, provider, child) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
backgroundColor: const Color(0xFFFFFFFF),
title: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
InkResponse(
onTap: () => Navigator.pop(context, true),
child: SvgPicture.asset(
"assets/svg/appbar_back_button.svg",
height: 25,
),
),
const SizedBox(width: 10),
InkResponse(
onTap: () => Navigator.pop(context, true),
child: Text(
"Leave Application Details",
style: TextStyle(
fontSize: 18,
height: 1.1,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w600,
color: AppColors.semi_black,
),
),
),
],
),
),
backgroundColor: const Color(0xFFF6F6F8),
body: Builder(
builder: (context) {
if (provider.isLoading) {
return const Center(child: CircularProgressIndicator(color: Colors.blue));
}
if (provider.errorMessage != null) {
return Center(child: Text(provider.errorMessage!));
}
if (provider.response?.requestDetails == null) {
return const Center(child: Text("No details found"));
}
final details = provider.response!.requestDetails!;
/// Screen content
return SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// Header with status
Container(
margin: const EdgeInsets.only(bottom: 0.5),
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 2),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
/// Left Avatar
Container(
height: 48,
width: 48,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: const Color(0xFFEDF8FF), // icon bg
),
child: Center(
child: SvgPicture.asset(
height: 28,
width: 28,
"assets/svg/hrm/leaveApplication.svg", // Use appropriate icon
fit: BoxFit.contain,
),
),
),
const SizedBox(width: 12),
/// Middle text
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
details.leaveType ?? "-",
style: TextStyle(
decoration: TextDecoration.underline,
decorationStyle:
TextDecorationStyle.dotted,
decorationColor: AppColors.grey_thick,
height: 1.2,
fontFamily: "JakartaRegular",
fontSize: 14,
color: AppColors.semi_black,
),
),
const SizedBox(height: 2),
Text(
"Applied: ${details.appliedDate ?? "-"}",
style: TextStyle(
fontFamily: "JakartaRegular",
fontSize: 14,
color: AppColors.app_blue,
),
),
],
),
),
/// Right side status badge
Container(
height: 30,
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: _getStatusBackgroundColor(details.status),
),
child: Center(
child: Text(
details.status ?? "-",
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: "JakartaMedium",
fontSize: 12,
color: _getStatusTextColor(details.status),
),
),
),
),
],
),
),
/// Leave Details
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
_buildSectionHeader("Leave Details"),
_buildDetailTile("Application ID", details.id),
_buildDetailTile("Applied Date", details.appliedDate),
_buildDetailTile("Leave Type", details.leaveType),
_buildDateRangeTile("Leave Period", details.fromDate, details.toDate),
_buildTimeRangeTile("Time Period", details.fromTime, details.toTime),
_buildDetailTile("Reason", details.reason),
/// Approval Details
_buildSectionHeader("Approval Details"),
_buildDetailTile("Requested To", details.requestedTo),
_buildDetailTile("Approved By", details.approvedBy),
_buildDetailTile("Approved Date", details.approvedDate),
_buildDetailTile("Approval Remarks", details.approvalRemarks),
/// Additional Information
_buildSectionHeader("Additional Information"),
_buildDetailTile("Status", details.status),
_buildDetailTile("From Time", details.fromTime),
_buildDetailTile("To Time", details.toTime),
],
),
),
],
),
),
),
const SizedBox(height: 30),
],
),
);
},
),
);
},
),
);
}
/// Reusable Row Widget for details
Widget _buildDetailTile(String label, String? value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: Row(
children: [
Expanded(
flex: 6,
child: Text(
label,
style: TextStyle(
fontFamily: "JakartaRegular",
fontSize: 14,
color: AppColors.semi_black,
),
),
),
Expanded(
flex: 0,
child: Text(
value ?? "-",
style: const TextStyle(
fontSize: 14,
color: Color(0xff818181),
fontWeight: FontWeight.w400,
),
),
),
],
),
);
}
/// For date range display
Widget _buildDateRangeTile(String label, String? fromDate, String? toDate) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: Row(
children: [
Expanded(
flex: 6,
child: Text(
label,
style: TextStyle(
fontFamily: "JakartaRegular",
fontSize: 14,
color: AppColors.semi_black,
),
),
),
Expanded(
flex: 0,
child: Text(
'${fromDate ?? "-"} to ${toDate ?? "-"}',
style: const TextStyle(
fontSize: 14,
color: Color(0xff818181),
fontWeight: FontWeight.w400,
),
),
),
],
),
);
}
/// For time range display
Widget _buildTimeRangeTile(String label, String? fromTime, String? toTime) {
if ((fromTime == null || fromTime.isEmpty) && (toTime == null || toTime.isEmpty)) {
return const SizedBox.shrink(); // Hide if no time data
}
return Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: Row(
children: [
Expanded(
flex: 6,
child: Text(
label,
style: const TextStyle(
fontSize: 14,
color: Color(0xff2D2D2D),
fontStyle: FontStyle.normal,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w400,
),
),
),
Expanded(
flex: 0,
child: Text(
'${fromTime ?? "-"} to ${toTime ?? "-"}',
style: const TextStyle(
fontSize: 14,
color: Color(0xff818181),
fontWeight: FontWeight.w400,
),
),
),
],
),
);
}
/// Section header with dotted line
Widget _buildSectionHeader(String title) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Text(
title,
style: TextStyle(
fontSize: 14,
fontFamily: "JakartaSemiBold",
),
),
const SizedBox(width: 10),
Expanded(
child: DottedLine(
dashGapLength: 4,
dashGapColor: Colors.white,
dashColor: AppColors.grey_semi,
dashLength: 2,
lineThickness: 0.5,
),
),
],
),
);
}
/// Status background color
Color _getStatusBackgroundColor(String? status) {
switch (status?.toLowerCase()) {
case 'approved':
return AppColors.approved_bg_color;
case 'rejected':
return AppColors.rejected_bg_color;
case 'requested':
default:
return AppColors.requested_bg_color;
}
}
/// Status text color
Color _getStatusTextColor(String? status) {
switch (status?.toLowerCase()) {
case 'approved':
return AppColors.approved_text_color;
case 'rejected':
return AppColors.rejected_text_color;
case 'requested':
default:
return AppColors.requested_text_color;
}
}
}
\ No newline at end of file
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/svg.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import '../../Notifiers/hrmProvider/leaveApplicationListProvider.dart';
import '../../Utils/app_colors.dart';
import '../../Utils/commonWidgets.dart';
import '../commonDateRangeFilter.dart';
import 'AddLeaveRequestScreen.dart';
import 'LeaveApplicationDetailScreen.dart';
class LeaveApplicationScreen extends StatefulWidget {
const LeaveApplicationScreen({Key? key}) : super(key: key);
class LeaveApplicationListScreen extends StatefulWidget {
const LeaveApplicationListScreen({super.key});
@override
State<LeaveApplicationScreen> createState() => _LeaveApplicationScreenState();
State<LeaveApplicationListScreen> createState() => _LeaveApplicationListScreenState();
}
class _LeaveApplicationScreenState extends State<LeaveApplicationScreen> {
final TextEditingController _reasonController = TextEditingController();
DateTime? _startDate;
DateTime? _endDate;
Future<void> _pickDate({required bool isStart}) async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2020),
lastDate: DateTime(2100),
);
if (picked != null) {
setState(() {
if (isStart) {
_startDate = picked;
} else {
_endDate = picked;
}
});
}
}
class _LeaveApplicationListScreenState extends State<LeaveApplicationListScreen> {
// @override
// void initState() {
// super.initState();
// WidgetsBinding.instance.addPostFrameCallback((_) {
// final provider = Provider.of<LeaveApplicationListProvider>(context, listen: false);
// provider.fetchLeaveApplications(context);
// });
// }
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) {
final provider = LeaveApplicationListProvider();
Future.microtask(() {
provider.fetchLeaveApplications(context);
});
return provider;
},
builder: (context, child) {
return Consumer<LeaveApplicationListProvider>(
builder: (context, provider, child) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
backgroundColor: Colors.white,
title: Row(
appBar: appbar2New(
context,
"Leave Application List",
provider.resetForm,
Row(
children: [
InkResponse(
onTap: () => Navigator.pop(context, true),
child: SvgPicture.asset(
"assets/svg/appbar_back_button.svg",
height: 25,
onTap: () async {
var cf = Commondaterangefilter();
var result = await cf.showFilterBottomSheet(context);
if (result != null) {
var dateRange = result['dateRange'] as DateTimeRange?;
var formatted = result['formatted'] as List<String>;
if (formatted.isNotEmpty) {
provider.setDateRangeFilter("Custom", customRange: dateRange);
provider.fetchLeaveApplications(
context,
dateRange: "Custom",
customRange: dateRange,
);
}
}
},
child: SvgPicture.asset("assets/svg/filter_ic.svg", height: 25),
),
],
),
const SizedBox(width: 10),
const Text(
"Leave Application",
style: TextStyle(
fontSize: 18,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w600,
color: Colors.black87,
0xFFFFFFFF,
),
backgroundColor: const Color(0xFFF6F6F8),
body: Column(
children: [
/// Filter chips (if you want visible filter indicators)
// if (provider.selectedStatus != "All" || provider.selectedDateRange != "This Month")
// Container(
// padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
// color: Colors.white,
// child: Row(
// children: [
// if (provider.selectedStatus != "All")
// Chip(
// label: Text('Status: ${provider.selectedStatus}'),
// onDeleted: () {
// provider.setStatusFilter("All");
// provider.fetchLeaveApplications(context);
// },
// ),
// if (provider.selectedDateRange != "This Month")
// Chip(
// label: Text('Date: ${provider.selectedDateRange}'),
// onDeleted: () {
// provider.setDateRangeFilter("This Month");
// provider.fetchLeaveApplications(context);
// },
// ),
// ],
// ),
// ),
/// Leave application list
Expanded(
child: Builder(
builder: (context) {
if (provider.isLoading) {
return const Center(child: CircularProgressIndicator(color: Colors.blue));
}
if (provider.errorMessage != null) {
return Center(child: Text(provider.errorMessage!));
}
if (provider.response?.requestList == null ||
provider.response!.requestList!.isEmpty) {
return const Center(child: Text("No leave applications found"));
}
final list = provider.response!.requestList!;
return ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: list.length,
itemBuilder: (context, index) {
final item = list[index];
// Parse the full string into a DateTime object
DateTime parsedFromDate = DateFormat("dd MMM yyyy, hh:mm a").parse(item.fromPeriod.toString());
String dateFromMonth = DateFormat("dd MMM").format(parsedFromDate);
// Parse the full string into a DateTime object
DateTime parsedToDate = DateFormat("dd MMM yyyy, hh:mm a").parse(item.toPeriod.toString());
String dateToMonth = DateFormat("dd MMM yyyy").format(parsedToDate);
return InkWell(
borderRadius: BorderRadius.circular(16),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => LeaveApplicationDetailScreen(
leaveRequestId: item.id.toString(),
),
],
),
).then((_) {
provider.fetchLeaveApplications(context);
});
},
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8.5, vertical: 5),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
child: Row(
children: [
Align(
alignment: Alignment.topRight,
/// Left Status Circle
Container(
height: 48,
width: 48,
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: _getStatusBackgroundColor(item.status),
shape: BoxShape.circle,
),
child: Center(
child: Text(
"Dummy Screen !",
_getStatusInitials(item.status),
style: TextStyle(
fontSize: 10,
height: 1,
color: _getStatusTextColor(item.status),
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
),
const Text(
"Apply for Leave",
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 20),
TextField(
controller: _reasonController,
decoration: const InputDecoration(
labelText: "Reason for Leave",
border: OutlineInputBorder(),
const SizedBox(width: 12),
/// Middle Section - Leave Details
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.leaveType ?? "-",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontFamily: "JakartaRegular",
fontSize: 14,
color: AppColors.semi_black,
),
maxLines: 3,
),
const SizedBox(height: 20),
const SizedBox(height: 4),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () => _pickDate(isStart: true),
child: Text(_startDate == null
? "Select Start Date"
: "Start: ${_startDate!.toLocal()}".split(' ')[0]),
Text(
dateFromMonth ?? "-",
style: TextStyle(
fontFamily: "JakartaRegular",
fontSize: 14,
color: AppColors.grey_semi,
),
),
const SizedBox(width: 10),
Expanded(
child: ElevatedButton(
onPressed: () => _pickDate(isStart: false),
child: Text(_endDate == null
? "Select End Date"
: "End: ${_endDate!.toLocal()}".split(' ')[0]),
Text(
" - ${dateToMonth}" ?? "-",
style: TextStyle(
fontFamily: "JakartaRegular",
fontSize: 14,
color: AppColors.grey_semi,
),
),
],
),
// const SizedBox(height: 2),
// Text(
// "Period: ${item.fromPeriod ?? "-"} to ${item.toPeriod ?? "-"}",
// style: const TextStyle(
// fontSize: 12.5,
// color: Color(0xff818181),
// fontFamily: "Plus Jakarta Sans",
// ),
// ),
],
),
),
// /// Right Status
// Container(
// padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
// decoration: BoxDecoration(
// color: _getStatusBackgroundColor(item.status),
// borderRadius: BorderRadius.circular(10),
// ),
// child: Text(
// item.status ?? "-",
// style: TextStyle(
// fontFamily: "JakartaMedium",
// fontSize: 13,
// color: _getStatusTextColor(item.status),
// ),
// ),
// ),
],
),
const Spacer(),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
// Handle submission logic here
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Leave application submitted")),
),
);
},
);
},
child: const Text("Submit Application"),
),
),
)
],
),
floatingActionButtonLocation:
FloatingActionButtonLocation.centerFloat,
floatingActionButton: InkResponse(
onTap: () {
HapticFeedback.selectionClick();
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ChangeNotifierProvider(
create: (_) => LeaveApplicationListProvider(),
child: AddLeaveRequest(pageTitleName: "Add Leave Request"),
),
),
).then((_) {
provider.fetchLeaveApplications(context);
});
// show add bill screen here
},
child: Container(
height: 45,
alignment: Alignment.center,
margin: EdgeInsets.symmetric(horizontal: 20),
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: AppColors.app_blue,
borderRadius: BorderRadius.circular(15),
),
child: Text(
"Add Leave Request",
style: TextStyle(
fontSize: 15,
fontFamily: "JakartaMedium",
color: Colors.white,
),
),
),
),
);
},
);
},
);
}
/// Get status background color
Color _getStatusBackgroundColor(String? status) {
switch (status?.toLowerCase()) {
case 'approved':
return AppColors.approved_bg_color;
case 'rejected':
return AppColors.rejected_bg_color;
case 'requested':
default:
return AppColors.requested_bg_color;
}
}
/// Get status text color
Color _getStatusTextColor(String? status) {
switch (status?.toLowerCase()) {
case 'approved':
return AppColors.approved_text_color;
case 'rejected':
return AppColors.rejected_text_color;
case 'requested':
default:
return AppColors.requested_text_color;
}
}
/// Get status initials
String _getStatusInitials(String? status) {
switch (status?.toLowerCase()) {
case 'approved':
return "A";
case 'rejected':
return "R";
case 'requested':
default:
return "P"; // Pending
}
}
}
\ No newline at end of file
import 'package:flutter/material.dart';
class OrganizationStructureScreen extends StatelessWidget {
final List<Department> departments = [
Department(
name: "Engineering",
teams: [
Team(name: "Mobile Team", members: ["Mohit", "Srinivas", ]),
Team(name: "Backend Team", members: ["Dheeraj", "Satya","Sneha"]),
],
),
Department(
name: "Sales & Marketing",
teams: [
Team(name: "Digital Marketing", members: ["Kiran", "Priya"]),
Team(name: "Field Sales", members: ["Raj", "Anjali"]),
],
),
Department(
name: "HR & Admin",
teams: [
Team(name: "Recruitment", members: ["Naresh"]),
Team(name: "Operations", members: ["Suresh", "Divya"]),
],
),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Organization Structure")),
body: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: departments.length,
itemBuilder: (context, deptIndex) {
final dept = departments[deptIndex];
return Card(
margin: const EdgeInsets.only(bottom: 16),
elevation: 2,
child: ExpansionTile(
title: Text(
"${dept.name} not ready",
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
children: dept.teams.map((team) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
team.name,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
const SizedBox(height: 4),
Wrap(
spacing: 8,
children: team.members.map((member) {
return Chip(
label: Text(member),
backgroundColor: Colors.blue.shade50,
);
}).toList(),
),
const SizedBox(height: 8),
],
),
);
}).toList(),
),
);
},
),
);
}
}
class Department {
final String name;
final List<Team> teams;
Department({required this.name, required this.teams});
}
class Team {
final String name;
final List<String> members;
Team({required this.name, required this.members});
}
......@@ -47,27 +47,27 @@ class _RewardListScreenState extends State<RewardListScreen> {
),
],
),
actions: [
InkResponse(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RewardSearchScreen(),
settings: const RouteSettings(
name: 'AddLiveAttendanceScreen',
),
),
).then((_) {
});
},
child: SvgPicture.asset(
"assets/svg/search_ic.svg",
height: 25,
),
),
const SizedBox(width: 20),
],
// actions: [
// InkResponse(
// onTap: () {
// Navigator.push(
// context,
// MaterialPageRoute(
// builder: (context) => RewardSearchScreen(),
// settings: const RouteSettings(
// name: 'AddLiveAttendanceScreen',
// ),
// ),
// ).then((_) {
// });
// },
// child: SvgPicture.asset(
// "assets/svg/search_ic.svg",
// height: 25,
// ),
// ),
// const SizedBox(width: 20),
// ],
),
backgroundColor: Color(0xFFF6F6F8),
......@@ -95,6 +95,8 @@ class _RewardListScreenState extends State<RewardListScreen> {
children: [
/// --- Top Summary Cards ---
Stack(
children: [
Container(
height: 110,
width: double.infinity,
......@@ -113,7 +115,8 @@ class _RewardListScreenState extends State<RewardListScreen> {
fontSize: 20,
color: Color(0xff0D9C00),
fontStyle: FontStyle.normal,
fontWeight: FontWeight.w500),
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 10),
const Text(
......@@ -122,11 +125,36 @@ class _RewardListScreenState extends State<RewardListScreen> {
fontSize: 14,
color: Color(0xff2D2D2D),
fontStyle: FontStyle.normal,
fontWeight: FontWeight.w400),
fontWeight: FontWeight.w400,
),
),
],
),
),
// Positioned SVG Icon
Positioned(
bottom: 8,
right: 12,
child: Container(
height: 42,
width: 42,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: const Color(0xA0FFFFFF), // icon bg
),
child: Center(
child: SvgPicture.asset(
height: 25,
width: 25,
"assets/svg/hrm/achievement_ic.svg",
fit: BoxFit.contain,
),
),
),
),
],
),
const SizedBox(height: 12),
Row(
......@@ -139,7 +167,9 @@ class _RewardListScreenState extends State<RewardListScreen> {
color: const Color(0xffe8ddff),
borderRadius: BorderRadius.circular(16),
),
child: Column(
child: Stack(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
......@@ -147,16 +177,40 @@ class _RewardListScreenState extends State<RewardListScreen> {
style: const TextStyle(
fontSize: 20,
color: Color(0xff493272),
fontStyle: FontStyle.normal,
fontWeight: FontWeight.w500),
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
const Text("Disbursed Amount",
const Text(
"Disbursed \nAmount",
style: TextStyle(
fontSize: 14,
color: Color(0xff2D2D2D),
fontStyle: FontStyle.normal,
fontWeight: FontWeight.w400)),
fontWeight: FontWeight.w400,
),
),
],
),
Positioned(
bottom: 2,
right: 2,
child: Container(
height: 42,
width: 42,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: const Color(0xA0FFFFFF), // icon bg
),
child: Center(
child: SvgPicture.asset(
height: 25,
width: 25,
"assets/svg/hrm/location_ic.svg",
fit: BoxFit.contain,
),
),
),
),
],
),
),
......@@ -170,7 +224,9 @@ class _RewardListScreenState extends State<RewardListScreen> {
color: const Color(0xfffffbc3),
borderRadius: BorderRadius.circular(16),
),
child: Column(
child: Stack(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
......@@ -178,18 +234,39 @@ class _RewardListScreenState extends State<RewardListScreen> {
style: const TextStyle(
fontSize: 18,
color: Color(0xff605C00),
fontFamily: "Plus Jakarta Sans",
fontStyle: FontStyle.normal,
fontWeight: FontWeight.w500),
),
),
const SizedBox(height: 8),
const Text("Balance Amount",
const Text(
"Balance \nAmount",
style: TextStyle(
fontSize: 14,
color: Color(0xff2D2D2D),
fontFamily: "Plus Jakarta Sans",
fontStyle: FontStyle.normal,
fontWeight: FontWeight.w400)),
fontWeight: FontWeight.w400,
),
),
],
),
Positioned(
bottom: 2,
right: 2,
child: Container(
height: 42,
width: 42,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: const Color(0xA0FFFFFF), // icon bg
),
child: Center(
child: SvgPicture.asset(
height: 25,
width: 25,
"assets/svg/hrm/ballance_ic.svg",
fit: BoxFit.contain,
),
),
),
),
],
),
),
......@@ -303,20 +380,19 @@ class _RewardListScreenState extends State<RewardListScreen> {
const Text(
"Amount Details",
style: TextStyle(
fontSize: 15,
color: Color(0xff2D2D2D),
fontFamily: "Plus Jakarta Sans",
fontStyle: FontStyle.normal,
fontWeight: FontWeight.w600),
fontSize: 14,
fontFamily: "JakartaSemiBold",
),
),
const SizedBox(width: 10),
Expanded(
child: DottedLine(
dashLength: 4,
dashGapLength: 2,
lineThickness: 1,
dashColor: Color(0xff999999),
)
dashGapLength: 4,
dashGapColor: Colors.white,
dashColor: AppColors.grey_semi,
dashLength: 2,
lineThickness: 0.5,
),
),
],
),
......@@ -337,20 +413,19 @@ class _RewardListScreenState extends State<RewardListScreen> {
const Text(
"Employee Details",
style: TextStyle(
fontSize: 15,
color: Color(0xff2D2D2D),
fontFamily: "Plus Jakarta Sans",
fontStyle: FontStyle.normal,
fontWeight: FontWeight.w600),
fontSize: 14,
fontFamily: "JakartaSemiBold",
),
),
const SizedBox(width: 10),
Expanded(
child: DottedLine(
dashLength: 4,
dashGapLength: 2,
lineThickness: 1,
dashColor: Color(0xff999999),
)
dashGapLength: 4,
dashGapColor: Colors.white,
dashColor: AppColors.grey_semi,
dashLength: 2,
lineThickness: 0.5,
),
),
],
),
......@@ -369,20 +444,19 @@ class _RewardListScreenState extends State<RewardListScreen> {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(key, style: const TextStyle(
Text(key, style: TextStyle(
fontFamily: "JakartaRegular",
fontSize: 14,
color: Color(0xff2D2D2D),
fontFamily: "Plus Jakarta Sans",
fontStyle: FontStyle.normal,
fontWeight: FontWeight.w400)
color: AppColors.semi_black,
),
),
Text(value, style: const TextStyle(
Text(
value,
style: const TextStyle(
fontSize: 14,
color: Color(0xff2D2D2D),
fontFamily: "Plus Jakarta Sans",
fontStyle: FontStyle.normal,
fontWeight: FontWeight.w400)
color: Color(0xFF818181),
),
),
],
),
......
......@@ -8,17 +8,20 @@ import '../../Utils/app_colors.dart';
import '../finance/FileViewer.dart';
class TourExpensesDetailsScreen extends StatelessWidget {
class TourExpensesDetailsScreen extends StatefulWidget {
final String tourBillId;
const TourExpensesDetailsScreen({Key? key, required this.tourBillId})
: super(key: key);
@override
State<TourExpensesDetailsScreen> createState() => _TourExpensesDetailsScreenState();
}
class _TourExpensesDetailsScreenState extends State<TourExpensesDetailsScreen>{
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => TourExpensesDetailsProvider()
..fetchTourExpensesDetails(context, tourBillId),
..fetchTourExpensesDetails(context, widget.tourBillId),
child: Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
......@@ -66,17 +69,22 @@ class TourExpensesDetailsScreen extends StatelessWidget {
if (response == null) {
return const Center(child: Text("No data available"));
}
debugPrint("==================requestDetails: ${response.requestDetails?.approvalStatus}");
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// Header Card at the very top
_expenseHeaderCard(
title: response.requestDetails?.placeOfVisit ?? "Tour",
date: response.tourExpenses?.fromDate ?? "-",
status: response.requestDetails?.approvalStatus ?? "-",
status: (response.requestDetails?.approvalStatus?.isNotEmpty ?? false)
? response.requestDetails!.approvalStatus!
: "No Status",
details: [
{"key": "TL Pending Approval Amount", "value": "-"},
{"key": "Total Approved Amount", "value": response.tourExpenses?.appliedAmount ?? "-"},
......@@ -167,17 +175,12 @@ class TourExpensesDetailsScreen extends StatelessWidget {
debugPrint("Open: ${t.imageDirFilePath}");
//Fileviewer(fileName: "", fileUrl: t.imageDirFilePath.toString())
showDialog(
context: context,
builder: (_) => Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(t.imageDirFilePath.toString())
),
Navigator.push(
context,
MaterialPageRoute(
builder:
(context) => Image.network(t.imageDirFilePath.toString()),
// Fileviewer(fileName: label, fileUrl: "assets/images/capa.svg"),
),
);
},
......
......@@ -129,20 +129,18 @@ class _TourExpensesListScreenState extends State<TourExpensesListScreen> {
item.placeOfVisit ?? "-",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
style: TextStyle(
fontFamily: "JakartaRegular",
fontSize: 14,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w400,
color: Color(0xff2d2d2d),
color: AppColors.semi_black,
),
),
Text(
item.appliedDate ?? "-",
style: const TextStyle(
fontSize: 12,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w400,
color: Color(0xff818181),
style: TextStyle(
fontFamily: "JakartaRegular",
fontSize: 14,
color: AppColors.grey_semi,
),
),
],
......@@ -153,9 +151,8 @@ class _TourExpensesListScreenState extends State<TourExpensesListScreen> {
Text(
"₹${item.appliedAmount ?? '0'}",
style: const TextStyle(
fontFamily: "JakartaMedium",
fontSize: 14,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w500,
color: Color(0xff1487c9),
)
),
......@@ -185,7 +182,7 @@ class _TourExpensesListScreenState extends State<TourExpensesListScreen> {
name: 'AddTourExpBillScreen'),
),
).then((_) {
provider.fetchTourExpenses(context, "1");
});
// show add bill screen here
},
......@@ -280,7 +277,7 @@ class _TourExpensesListScreenState extends State<TourExpensesListScreen> {
case 'Updated':
return "U";
default:
return "Requested";
return "R";
}
}
......
......@@ -62,4 +62,6 @@ export 'package:generp/Notifiers/hrmProvider/AttendanceDetailsProvider.dart';
export 'package:generp/Notifiers/hrmProvider/tourExpensesProvider.dart';
export 'package:generp/Notifiers/hrmProvider/tourExpensesDetailsProvider.dart';
export 'package:generp/Notifiers/hrmProvider/rewardListProvider.dart';
export 'package:generp/Notifiers/hrmProvider/LeaveApplicationListProvider.dart';
export 'package:generp/Notifiers/hrmProvider/LeaveApplicationDetailsProvider.dart';
......@@ -26,7 +26,10 @@ import 'package:generp/Models/crmModels/crmProspectDetailsResponse.dart';
import 'package:generp/Models/financeModels/addDirectPaymentResponse.dart';
import 'package:generp/Models/financeModels/paymentRequisitionPaymentsListResponse.dart';
import 'package:generp/Models/hrmModels/attendanceRequestListResponse.dart';
import 'package:generp/Models/hrmModels/leaveApplicationDetailsResponse.dart';
import 'package:generp/Models/hrmModels/leaveApplicationLIstResponse.dart';
import 'package:generp/Models/hrmModels/rewardListResponse.dart';
import 'package:generp/Models/hrmModels/tourExpensesAddViewResponse.dart';
import 'package:generp/Models/hrmModels/tourExpensesDetailsResponse.dart';
import 'package:generp/Models/hrmModels/tourExpensesListResponse.dart';
import 'package:generp/Models/ordersModels/PendingTPCAgentListResponse.dart';
......@@ -35,6 +38,7 @@ import 'package:generp/Models/ordersModels/TPCListResponse.dart';
import 'package:generp/Models/ordersModels/orderDashboardResponse.dart';
import 'package:generp/services/api_names.dart';
import 'package:generp/services/api_post_request.dart';
import 'package:http/http.dart' as http;
import 'package:path_provider/path_provider.dart';
import '../Models/AccountSuggestionResponse.dart';
......@@ -97,6 +101,7 @@ import '../Models/financeModels/paymentRequisitionPaymentsReceiptsDetailsRespons
import '../Models/financeModels/paymentRequisitionPaymentsReceiptsListResponse.dart';
import '../Models/generatorComplaintResponse.dart';
import '../Models/hrmModels/attendanceRequestDetailsResponse.dart';
import '../Models/hrmModels/hrmAccessiblePagesResponse.dart';
import '../Models/loadGeneratorDetailsResponse.dart';
import '../Models/financeModels/financeDashboardPagesResponse.dart';
import '../Models/ordersModels/AddOrderPaymentSelectAccountResponse.dart';
......@@ -121,6 +126,7 @@ import '../Models/ordersModels/paymentListByModeResponse.dart';
import '../Models/ordersModels/technicianAddPaymentResendOTPResponse.dart';
import '../Notifiers/financeProvider/approveRejectPaymentRequestResponse.dart';
import '../Utils/commonServices.dart';
import 'package:http_parser/http_parser.dart';
class ApiCalling {
static Future download_files(empId, session, url, cntxt) async {
......@@ -4905,6 +4911,30 @@ class ApiCalling {
///hrm modules
///
static Future<hrmAccessiblePagesResponse?> hrmAccessiblePagesAPI(
empId,
session,
) async {
try {
Map<String, String> data = {
'emp_id': (empId).toString(),
'session_id': (session).toString(),
};
final res = await post(data, HrmAccessiblePagesUrl, {});
if (res != null) {
print(data);
debugPrint(res.body);
return hrmAccessiblePagesResponse.fromJson(jsonDecode(res.body));
} else {
debugPrint("Null Response");
return null;
}
} catch (e) {
debugPrint('hello bev=bug $e ');
return null;
}
}
static Future<attendanceRequestListResponse?> attendanceRequestListAPI(
empId,
session,
......@@ -4961,6 +4991,82 @@ class ApiCalling {
}
}
static Future<CommonResponse?> addAttendanceRequestAPI({
required String sessionId,
required String empId,
required String process,
required String type,
required String loc,
required String checkDate,
String? checkInTime,
String? checkInLoc,
File? checkInProof,
String? checkOutTime,
String? checkOutLoc,
File? checkOutProof,
String? note,
}) async {
try {
var request = http.MultipartRequest('POST', Uri.parse(AddAttendanceRequestUrl));
// Add basic fields that are always required
Map<String, String> fields = {
"session_id": sessionId,
"emp_id": empId,
"process": process,
"type": type,
"loc": loc,
"check_date": checkDate,
"note": note ?? "",
};
// Conditionally add check-in fields based on type
if (type == "Check In" || type == "Check In/Out") {
fields["check_in_time"] = checkInTime ?? "";
fields["check_in_loc"] = checkInLoc ?? "";
if (checkInProof != null) {
request.files.add(await http.MultipartFile.fromPath("check_in_proof", checkInProof.path));
}
}
// Conditionally add check-out fields based on type
if (type == "Check Out" || type == "Check In/Out") {
fields["check_out_time"] = checkOutTime ?? "";
fields["check_out_loc"] = checkOutLoc ?? "";
if (checkOutProof != null) {
request.files.add(await http.MultipartFile.fromPath("check_out_proof", checkOutProof.path));
}
}
// Add all fields to the request
request.fields.addAll(fields);
// Log the actual fields being sent
debugPrint("addAttendanceRequestAPI - Type: $type");
debugPrint("addAttendanceRequestAPI - Fields: $fields");
debugPrint("addAttendanceRequestAPI - Files: ${request.files.map((f) => f.filename).toList()}");
var response = await request.send();
var resBody = await response.stream.bytesToString();
debugPrint("Server Response: $resBody");
if (response.statusCode == 200) {
return CommonResponse.fromJson(jsonDecode(resBody));
} else {
return null;
}
} catch (e) {
debugPrint("API Error: $e");
return null;
}
}
//reward list
static Future<rewardListResponse?> rewardListAPI(
empId,
......@@ -5039,6 +5145,215 @@ class ApiCalling {
}
}
static Future<tourExpensesAddViewResponse?> tourExpensesAddViewAPI(
empId,
session,
tourBillId,
) async {
try {
Map<String, String> data = {
'session_id': (session).toString(),
'emp_id': (empId).toString(),
'tour_bill_id': (tourBillId),
};
final res = await post(data, TourExpensesAddViewUrl, {});
if (res != null) {
print(data);
debugPrint(res.body);
return tourExpensesAddViewResponse.fromJson(jsonDecode(res.body));
} else {
debugPrint("Null Response");
return null;
}
} catch (e) {
debugPrint('hello bev=bug $e ');
return null;
}
}
static Future<CommonResponse?> addTourBillAPI({
required String sessionId,
required String empId,
required String placeOfVisit,
required String daAmount,
required String tourType,
required String tourDate,
required List<Map<String, dynamic>> travelExpenses,
required List<Map<String, dynamic>> hotelExpenses,
required List<Map<String, dynamic>> otherExpenses,
List<File>? travelImages,
List<File>? hotelImages,
List<File>? otherImages,
}) async {
try {
var request = http.MultipartRequest("POST", Uri.parse(AddTourExpensesUrl));
/// Add text fields
request.fields['session_id'] = sessionId;
request.fields['emp_id'] = empId;
request.fields['place_of_visit'] = placeOfVisit;
request.fields['da_amount'] = daAmount;
request.fields['tour_type'] = tourType;
request.fields['tour_date'] = tourDate;
/// Convert expense lists to JSON string
request.fields['travel_expenses'] = jsonEncode(travelExpenses);
request.fields['hotel_expenses'] = jsonEncode(hotelExpenses);
request.fields['other_expenses'] = jsonEncode(otherExpenses);
/// Add hotel images
if (hotelImages!.isNotEmpty) {
for (var file in hotelImages) {
if (file.path.isNotEmpty) {
request.files.add(
await http.MultipartFile.fromPath(
"hotel_images[]",
file.path,
),
);
}
}
}
/// Add travel images
if (travelImages!.isNotEmpty) {
for (var file in travelImages) {
if (file.path.isNotEmpty) {
request.files.add(
await http.MultipartFile.fromPath(
"travel_images[]",
file.path,
),
);
}
}
}
/// Add other images
if (otherImages!.isNotEmpty) {
for (var file in otherImages) {
if (file.path.isNotEmpty) {
request.files.add(
await http.MultipartFile.fromPath(
"other_images[]",
file.path,
),
);
}
}
}
/// Send request
var response = await request.send();
var resBody = await response.stream.bytesToString();
debugPrint("Request Fields: ${request.fields}");
debugPrint("Response: $resBody");
if (response.statusCode == 200) {
return CommonResponse.fromJson(jsonDecode(resBody));
} else {
debugPrint("Error: ${response.statusCode} - $resBody");
return null;
}
} catch (e) {
debugPrint("Error in addTourBillAPI: $e");
return null;
}
}
// Leave Application api
static Future<leaveApplicationLIstResponse?> leaveApplicationListAPI(
session,
empId,
dateFrom,
dateTo
) async {
try {
Map<String, String> data = {
'session_id': (session).toString(),
'emp_id': (empId).toString(),
'requested_date_from': (dateFrom),
'requested_date_to': (dateTo),
};
final res = await post(data, LeaveApplicationListUrl, {});
if (res != null) {
print(data);
debugPrint(res.body);
return leaveApplicationLIstResponse.fromJson(jsonDecode(res.body));
} else {
debugPrint("Null Response");
return null;
}
} catch (e) {
debugPrint('hello bev=bug $e ');
return null;
}
}
static Future<leaveApplicationDetailsResponse?> leaveApplicationDetailAPI(
session,
empId,
leaveRequestId,
) async {
try {
Map<String, String> data = {
'session_id': (session).toString(),
'emp_id': (empId).toString(),
'leave_request_id': (leaveRequestId),
};
final res = await post(data, LeaveApplicationDetailsUrl, {});
if (res != null) {
print(data);
debugPrint(res.body);
return leaveApplicationDetailsResponse.fromJson(jsonDecode(res.body));
} else {
debugPrint("Null Response");
return null;
}
} catch (e) {
debugPrint('hello bev=bug $e ');
return null;
}
}
//add leave request
static Future<CommonResponse?> leaveRequestAddAPI(
session,
empId,
fromDate,
fromTime,
toDate,
toTime,
leaveType,
reason
) async {
try {
Map<String, String> data = {
'session_id': (session).toString(),
'emp_id': (empId).toString(),
'from_date': (fromDate).toString(),
'from_time': (fromTime).toString(),
'to_date': (toDate).toString(),
'to_time': (toTime).toString(),
'leave_type': (leaveType).toString(),
'reason': (reason).toString(),
};
final res = await post(data, LeaveRequestAdditionUrl, {});
if (res != null) {
print(data);
debugPrint(res.body);
return CommonResponse.fromJson(jsonDecode(res.body));
} else {
debugPrint("Null Response");
return null;
}
} catch (e) {
debugPrint('hello bev=bug $e ');
return null;
}
}
// static Future<CommonResponse?> TpcIssueListApprovalAPI(
// empId,
......
......@@ -180,6 +180,7 @@ const crmDashboardQuotationsUrl = "${baseUrl_test}crm_dashboard_quotations_list"
///HRM
//Attendance
const HrmAccessiblePagesUrl ="${baseUrl_test}hrm_accessible_pages";
const AttendanceRequestListUrl ="${baseUrl_test}attendance_request_list";
const AttendanceRequestDetailsUrl ="${baseUrl_test}attendance_request_details";
const AddAttendanceRequestUrl ="${baseUrl_test}add_attendance_request";
......@@ -188,6 +189,12 @@ const RewardListUrl ="${baseUrl_test}hrm_emp_self_rewards";
// Tour Expenses hrm_emp_self_rewards
const TourExpensesListUrl ="${baseUrl_test}tour_bill_list";
const TourExpensesDetailsUrl ="${baseUrl_test}tour_bill_details";
const TourExpensesAddViewUrl ="${baseUrl_test}add_tour_bill_view";
const AddTourExpensesUrl ="${baseUrl_test}add_tour_bill";
//leave applications
const LeaveApplicationListUrl ="${baseUrl_test}leave_request_list";
const LeaveApplicationDetailsUrl ="${baseUrl_test}leave_request_details";
const LeaveRequestAdditionUrl ="${baseUrl_test}add_leave_request";
......
......@@ -181,6 +181,76 @@ Future<String?> postImageNew(
return null;
}
}
//travel_image
//hotel_image
//other_image
Future<String?> PostMultipleImagesNew(
Map<String, String> body,
String urlLink,
Map<String, String> headers,
List<http.MultipartFile> newList,
List<http.MultipartFile> newList1,
List<http.MultipartFile> newList2,
) async {
try {
var req = http.MultipartRequest('POST', Uri.parse(urlLink));
req.headers.addAll(headers);
req.files.addAll(newList);
req.files.addAll(newList1);
req.files.addAll(newList2);
req.fields.addAll(body);
var res = await req.send();
final resBody = await res.stream.bytesToString();
if (res.statusCode >= 200 && res.statusCode < 300) {
print("**** $resBody .... $res");
return resBody;
} else {
print("error: ${res.reasonPhrase}");
return null;
}
} catch (e) {
debugPrint(e.toString());
return null;
}
}
Future<String?> PostMultipleImagesNew2(
Map<String, String> body,
String urlLink,
Map<String, String> headers,
List<http.MultipartFile> newList,
List<http.MultipartFile> newList1,
) async {
try {
var req = http.MultipartRequest('POST', Uri.parse(urlLink));
req.headers.addAll(headers);
req.files.addAll(newList);
req.files.addAll(newList1);
req.fields.addAll(body);
var res = await req.send();
final resBody = await res.stream.bytesToString();
if (res.statusCode >= 200 && res.statusCode < 300) {
print("**** $resBody .... $res");
return resBody;
} else {
print("error: ${res.reasonPhrase}");
return null;
}
} catch (e) {
debugPrint(e.toString());
return null;
}
}
Future<String?> PostMultipleImages(
Map<String, String> body,
......
......@@ -8,6 +8,7 @@ import Foundation
import app_settings
import connectivity_plus
import device_info_plus
import file_picker
import file_selector_macos
import firebase_core
import firebase_messaging
......@@ -30,6 +31,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AppSettingsPlugin.register(with: registry.registrar(forPlugin: "AppSettingsPlugin"))
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
......
......@@ -465,6 +465,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.1"
file_picker:
dependency: "direct main"
description:
name: file_picker
sha256: ab13ae8ef5580a411c458d6207b6774a6c237d77ac37011b13994879f68a8810
url: "https://pub.dev"
source: hosted
version: "8.3.7"
file_selector_linux:
dependency: transitive
description:
......
......@@ -89,6 +89,7 @@ dependencies:
pinput: ^5.0.1
build_runner: ^2.4.0
build_web_compilers: ^4.0.4
file_picker: ^8.0.0
dev_dependencies:
flutter_test:
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment