added pull down refresh
This commit is contained in:
parent
0e177e5a1f
commit
2fb3c36ba4
@ -173,17 +173,25 @@ class ExpenseController extends GetxController {
|
||||
if (result != null) {
|
||||
try {
|
||||
final expenseResponse = ExpenseResponse.fromJson(result);
|
||||
expenses.assignAll(expenseResponse.data.data);
|
||||
|
||||
logSafe("Expenses loaded: ${expenses.length}");
|
||||
logSafe(
|
||||
"Pagination Info: Page ${expenseResponse.data.currentPage} of ${expenseResponse.data.totalPages} | Total: ${expenseResponse.data.totalEntites}");
|
||||
// If the backend returns no data, treat it as empty list
|
||||
if (expenseResponse.data.data.isEmpty) {
|
||||
expenses.clear();
|
||||
errorMessage.value = ''; // no error
|
||||
logSafe("Expense list is empty.");
|
||||
} else {
|
||||
expenses.assignAll(expenseResponse.data.data);
|
||||
logSafe("Expenses loaded: ${expenses.length}");
|
||||
logSafe(
|
||||
"Pagination Info: Page ${expenseResponse.data.currentPage} of ${expenseResponse.data.totalPages} | Total: ${expenseResponse.data.totalEntites}");
|
||||
}
|
||||
} catch (e) {
|
||||
errorMessage.value = 'Failed to parse expenses: $e';
|
||||
logSafe("Parse error in fetchExpenses: $e", level: LogLevel.error);
|
||||
}
|
||||
} else {
|
||||
errorMessage.value = 'Failed to fetch expenses from server.';
|
||||
// Only treat as error if this means a network or server failure
|
||||
errorMessage.value = 'Unable to connect to the server.';
|
||||
logSafe("fetchExpenses failed: null response", level: LogLevel.error);
|
||||
}
|
||||
} catch (e, stack) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
class ApiEndpoints {
|
||||
static const String baseUrl = "https://stageapi.marcoaiot.com/api";
|
||||
// static const String baseUrl = "https://api.marcoaiot.com/api";
|
||||
// static const String baseUrl = "https://stageapi.marcoaiot.com/api";
|
||||
static const String baseUrl = "https://api.marcoaiot.com/api";
|
||||
|
||||
// Dashboard Module API Endpoints
|
||||
static const String getDashboardAttendanceOverview = "/dashboard/attendance-overview";
|
||||
|
@ -436,18 +436,16 @@ class ApiService {
|
||||
int pageSize = 20,
|
||||
int pageNumber = 1,
|
||||
}) async {
|
||||
// Build the endpoint with query parameters
|
||||
String endpoint = ApiEndpoints.getExpenseList;
|
||||
final queryParams = <String, String>{
|
||||
final queryParams = {
|
||||
'pageSize': pageSize.toString(),
|
||||
'pageNumber': pageNumber.toString(),
|
||||
};
|
||||
|
||||
if (filter != null && filter.isNotEmpty) {
|
||||
queryParams['filter'] = filter;
|
||||
if (filter?.isNotEmpty ?? false) {
|
||||
queryParams['filter'] = filter!;
|
||||
}
|
||||
|
||||
// Build the full URI
|
||||
final uri = Uri.parse(endpoint).replace(queryParameters: queryParams);
|
||||
logSafe("Fetching expense list with URI: $uri");
|
||||
|
||||
@ -456,20 +454,22 @@ class ApiService {
|
||||
if (response == null) {
|
||||
logSafe("Expense list request failed: null response",
|
||||
level: LogLevel.error);
|
||||
return null;
|
||||
return null; // real failure
|
||||
}
|
||||
|
||||
// Directly parse and return the entire JSON response
|
||||
final body = response.body.trim();
|
||||
if (body.isEmpty) {
|
||||
logSafe("Expense list response body is empty", level: LogLevel.error);
|
||||
return null;
|
||||
logSafe("Expense list response body is empty", level: LogLevel.warning);
|
||||
return {
|
||||
"status": true,
|
||||
"data": {"data": [], "totalPages": 0, "currentPage": pageNumber}
|
||||
}; // treat as empty list
|
||||
}
|
||||
|
||||
final jsonResponse = jsonDecode(body);
|
||||
if (jsonResponse is Map<String, dynamic>) {
|
||||
logSafe("Expense list response parsed successfully");
|
||||
return jsonResponse; // Return the entire API response
|
||||
return jsonResponse; // always return valid JSON, even if data list is empty
|
||||
} else {
|
||||
logSafe("Unexpected response structure: $jsonResponse",
|
||||
level: LogLevel.error);
|
||||
|
@ -76,14 +76,12 @@ class SearchAndFilter extends StatelessWidget {
|
||||
final TextEditingController controller;
|
||||
final ValueChanged<String> onChanged;
|
||||
final VoidCallback onFilterTap;
|
||||
final VoidCallback onRefreshTap;
|
||||
final ExpenseController expenseController;
|
||||
|
||||
const SearchAndFilter({
|
||||
required this.controller,
|
||||
required this.onChanged,
|
||||
required this.onFilterTap,
|
||||
required this.onRefreshTap,
|
||||
required this.expenseController,
|
||||
super.key,
|
||||
});
|
||||
@ -119,14 +117,6 @@ class SearchAndFilter extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
MySpacing.width(8),
|
||||
Tooltip(
|
||||
message: 'Refresh Data',
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.refresh, color: Colors.green, size: 24),
|
||||
onPressed: onRefreshTap,
|
||||
),
|
||||
),
|
||||
MySpacing.width(4),
|
||||
Obx(() {
|
||||
return IconButton(
|
||||
|
@ -33,6 +33,18 @@ class SkeletonLoaders {
|
||||
);
|
||||
}
|
||||
|
||||
// Date Skeleton Loader
|
||||
static Widget dateSkeletonLoader() {
|
||||
return Container(
|
||||
height: 14,
|
||||
width: 90,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Employee List - Card Style
|
||||
static Widget employeeListSkeletonLoader() {
|
||||
return Column(
|
||||
|
32
lib/helpers/widgets/my_refresh_indicator.dart
Normal file
32
lib/helpers/widgets/my_refresh_indicator.dart
Normal file
@ -0,0 +1,32 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MyRefreshIndicator extends StatelessWidget {
|
||||
final Future<void> Function() onRefresh;
|
||||
final Widget child;
|
||||
final Color color;
|
||||
final Color backgroundColor;
|
||||
final double strokeWidth;
|
||||
final double displacement;
|
||||
|
||||
const MyRefreshIndicator({
|
||||
super.key,
|
||||
required this.onRefresh,
|
||||
required this.child,
|
||||
this.color = Colors.white,
|
||||
this.backgroundColor = Colors.blueAccent,
|
||||
this.strokeWidth = 3.0,
|
||||
this.displacement = 40.0,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: onRefresh,
|
||||
color: color,
|
||||
backgroundColor: backgroundColor,
|
||||
strokeWidth: strokeWidth,
|
||||
displacement: displacement,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
@ -42,11 +42,7 @@ class AttendanceLogsTab extends StatelessWidget {
|
||||
children: [
|
||||
MyText.titleMedium("Attendance Logs", fontWeight: 600),
|
||||
controller.isLoading.value
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: LinearProgressIndicator(),
|
||||
)
|
||||
? SkeletonLoaders.dateSkeletonLoader()
|
||||
: MyText.bodySmall(
|
||||
dateRangeText,
|
||||
fontWeight: 600,
|
||||
|
@ -13,7 +13,7 @@ import 'package:marco/controller/project_controller.dart';
|
||||
import 'package:marco/view/dashboard/Attendence/regularization_requests_tab.dart';
|
||||
import 'package:marco/view/dashboard/Attendence/attendance_logs_tab.dart';
|
||||
import 'package:marco/view/dashboard/Attendence/todays_attendance_tab.dart';
|
||||
|
||||
import 'package:marco/helpers/widgets/my_refresh_indicator.dart';
|
||||
class AttendanceScreen extends StatefulWidget {
|
||||
const AttendanceScreen({super.key});
|
||||
|
||||
@ -70,7 +70,8 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new, color: Colors.black, size: 20),
|
||||
icon: const Icon(Icons.arrow_back_ios_new,
|
||||
color: Colors.black, size: 20),
|
||||
onPressed: () => Get.offNamed('/dashboard'),
|
||||
),
|
||||
MySpacing.width(8),
|
||||
@ -78,14 +79,18 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.titleLarge('Attendance', fontWeight: 700, color: Colors.black),
|
||||
MyText.titleLarge('Attendance',
|
||||
fontWeight: 700, color: Colors.black),
|
||||
MySpacing.height(2),
|
||||
GetBuilder<ProjectController>(
|
||||
builder: (projectController) {
|
||||
final projectName = projectController.selectedProject?.name ?? 'Select Project';
|
||||
final projectName =
|
||||
projectController.selectedProject?.name ??
|
||||
'Select Project';
|
||||
return Row(
|
||||
children: [
|
||||
const Icon(Icons.work_outline, size: 14, color: Colors.grey),
|
||||
const Icon(Icons.work_outline,
|
||||
size: 14, color: Colors.grey),
|
||||
MySpacing.width(4),
|
||||
Expanded(
|
||||
child: MyText.bodySmall(
|
||||
@ -133,18 +138,24 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
final selectedProjectId = projectController.selectedProjectId.value;
|
||||
final selectedProjectId =
|
||||
projectController.selectedProjectId.value;
|
||||
final selectedView = result['selectedTab'] as String?;
|
||||
|
||||
if (selectedProjectId.isNotEmpty) {
|
||||
try {
|
||||
await attendanceController.fetchEmployeesByProject(selectedProjectId);
|
||||
await attendanceController.fetchAttendanceLogs(selectedProjectId);
|
||||
await attendanceController.fetchRegularizationLogs(selectedProjectId);
|
||||
await attendanceController.fetchProjectData(selectedProjectId);
|
||||
await attendanceController
|
||||
.fetchEmployeesByProject(selectedProjectId);
|
||||
await attendanceController
|
||||
.fetchAttendanceLogs(selectedProjectId);
|
||||
await attendanceController
|
||||
.fetchRegularizationLogs(selectedProjectId);
|
||||
await attendanceController
|
||||
.fetchProjectData(selectedProjectId);
|
||||
} catch (_) {}
|
||||
|
||||
attendanceController.update(['attendance_dashboard_controller']);
|
||||
attendanceController
|
||||
.update(['attendance_dashboard_controller']);
|
||||
}
|
||||
|
||||
if (selectedView != null && selectedView != selectedTab) {
|
||||
@ -154,20 +165,7 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Icon(Icons.tune, color: Colors.blueAccent, size: 20),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
MyText.bodyMedium("Refresh", fontWeight: 600),
|
||||
Tooltip(
|
||||
message: 'Refresh Data',
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
onTap: _refreshData,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Icon(Icons.refresh, color: Colors.green, size: 22),
|
||||
child: Icon(Icons.tune, size: 18),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -203,7 +201,10 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: PreferredSize(preferredSize: const Size.fromHeight(72), child: _buildAppBar()),
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(72),
|
||||
child: _buildAppBar(),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: GetBuilder<AttendanceController>(
|
||||
init: attendanceController,
|
||||
@ -212,25 +213,29 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||
final selectedProjectId = projectController.selectedProjectId.value;
|
||||
final noProjectSelected = selectedProjectId.isEmpty;
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: MySpacing.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MySpacing.height(flexSpacing),
|
||||
_buildFilterAndRefreshRow(),
|
||||
MySpacing.height(flexSpacing),
|
||||
MyFlex(
|
||||
children: [
|
||||
MyFlexItem(
|
||||
sizes: 'lg-12 md-12 sm-12',
|
||||
child: noProjectSelected
|
||||
? _buildNoProjectWidget()
|
||||
: _buildSelectedTabContent(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
return MyRefreshIndicator(
|
||||
onRefresh: _refreshData,
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: MySpacing.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MySpacing.height(flexSpacing),
|
||||
_buildFilterAndRefreshRow(),
|
||||
MySpacing.height(flexSpacing),
|
||||
MyFlex(
|
||||
children: [
|
||||
MyFlexItem(
|
||||
sizes: 'lg-12 md-12 sm-12',
|
||||
child: noProjectSelected
|
||||
? _buildNoProjectWidget()
|
||||
: _buildSelectedTabContent(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -15,6 +15,7 @@ import 'package:flutter_quill_delta_from_html/flutter_quill_delta_from_html.dart
|
||||
import 'package:marco/model/directory/add_comment_bottom_sheet.dart';
|
||||
import 'package:marco/helpers/utils/date_time_utils.dart';
|
||||
import 'package:marco/model/directory/add_contact_bottom_sheet.dart';
|
||||
import 'package:marco/helpers/widgets/my_refresh_indicator.dart';
|
||||
|
||||
// HELPER: Delta to HTML conversion
|
||||
String _convertDeltaToHtml(dynamic delta) {
|
||||
@ -120,8 +121,10 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new, color: Colors.black, size: 20),
|
||||
onPressed: () => Get.offAllNamed('/dashboard/directory-main-page'),
|
||||
icon: const Icon(Icons.arrow_back_ios_new,
|
||||
color: Colors.black, size: 20),
|
||||
onPressed: () =>
|
||||
Get.offAllNamed('/dashboard/directory-main-page'),
|
||||
),
|
||||
MySpacing.width(8),
|
||||
Expanded(
|
||||
@ -129,7 +132,8 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
MyText.titleLarge('Contact Profile', fontWeight: 700, color: Colors.black),
|
||||
MyText.titleLarge('Contact Profile',
|
||||
fontWeight: 700, color: Colors.black),
|
||||
MySpacing.height(2),
|
||||
GetBuilder<ProjectController>(
|
||||
builder: (p) => ProjectLabel(p.selectedProject?.name),
|
||||
@ -145,7 +149,8 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
|
||||
Widget _buildSubHeader() {
|
||||
final firstName = contact.name.split(" ").first;
|
||||
final lastName = contact.name.split(" ").length > 1 ? contact.name.split(" ").last : "";
|
||||
final lastName =
|
||||
contact.name.split(" ").length > 1 ? contact.name.split(" ").last : "";
|
||||
|
||||
return Padding(
|
||||
padding: MySpacing.xy(16, 12),
|
||||
@ -153,21 +158,27 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(children: [
|
||||
Avatar(firstName: firstName, lastName: lastName, size: 35, backgroundColor: Colors.indigo),
|
||||
Avatar(
|
||||
firstName: firstName,
|
||||
lastName: lastName,
|
||||
size: 35,
|
||||
backgroundColor: Colors.indigo),
|
||||
MySpacing.width(12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.titleSmall(contact.name, fontWeight: 600, color: Colors.black),
|
||||
MyText.titleSmall(contact.name,
|
||||
fontWeight: 600, color: Colors.black),
|
||||
MySpacing.height(2),
|
||||
MyText.bodySmall(contact.organization, fontWeight: 500, color: Colors.grey[700]),
|
||||
MyText.bodySmall(contact.organization,
|
||||
fontWeight: 500, color: Colors.grey[700]),
|
||||
],
|
||||
),
|
||||
]),
|
||||
TabBar(
|
||||
labelColor: Colors.red,
|
||||
unselectedLabelColor: Colors.black,
|
||||
indicator: MaterialIndicator(
|
||||
indicator: MaterialIndicator(
|
||||
color: Colors.red,
|
||||
height: 4,
|
||||
topLeftRadius: 8,
|
||||
@ -193,25 +204,38 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
?.name)
|
||||
.whereType<String>()
|
||||
.join(", ");
|
||||
final projectNames = contact.projectIds?.map((id) =>
|
||||
projectController.projects.firstWhereOrNull((p) => p.id == id)?.name).whereType<String>().join(", ") ?? "-";
|
||||
final projectNames = contact.projectIds
|
||||
?.map((id) => projectController.projects
|
||||
.firstWhereOrNull((p) => p.id == id)
|
||||
?.name)
|
||||
.whereType<String>()
|
||||
.join(", ") ??
|
||||
"-";
|
||||
final category = contact.contactCategory?.name ?? "-";
|
||||
|
||||
Widget multiRows({required List<dynamic> items, required IconData icon, required String label, required String typeLabel, required Function(String)? onTap, required Function(String)? onLongPress}) {
|
||||
Widget multiRows(
|
||||
{required List<dynamic> items,
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required String typeLabel,
|
||||
required Function(String)? onTap,
|
||||
required Function(String)? onLongPress}) {
|
||||
return items.isNotEmpty
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_iconInfoRow(icon, label, items.first, onTap: () => onTap?.call(items.first), onLongPress: () => onLongPress?.call(items.first)),
|
||||
_iconInfoRow(icon, label, items.first,
|
||||
onTap: () => onTap?.call(items.first),
|
||||
onLongPress: () => onLongPress?.call(items.first)),
|
||||
...items.skip(1).map(
|
||||
(val) => _iconInfoRow(
|
||||
null,
|
||||
'',
|
||||
val,
|
||||
onTap: () => onTap?.call(val),
|
||||
onLongPress: () => onLongPress?.call(val),
|
||||
),
|
||||
),
|
||||
(val) => _iconInfoRow(
|
||||
null,
|
||||
'',
|
||||
val,
|
||||
onTap: () => onTap?.call(val),
|
||||
onLongPress: () => onLongPress?.call(val),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: _iconInfoRow(icon, label, "-");
|
||||
@ -228,32 +252,38 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
// BASIC INFO CARD
|
||||
_infoCard("Basic Info", [
|
||||
multiRows(
|
||||
items: contact.contactEmails.map((e) => e.emailAddress).toList(),
|
||||
items:
|
||||
contact.contactEmails.map((e) => e.emailAddress).toList(),
|
||||
icon: Icons.email,
|
||||
label: "Email",
|
||||
typeLabel: "Email",
|
||||
onTap: (email) => LauncherUtils.launchEmail(email),
|
||||
onLongPress: (email) => LauncherUtils.copyToClipboard(email, typeLabel: "Email"),
|
||||
onLongPress: (email) =>
|
||||
LauncherUtils.copyToClipboard(email, typeLabel: "Email"),
|
||||
),
|
||||
multiRows(
|
||||
items: contact.contactPhones.map((p) => p.phoneNumber).toList(),
|
||||
items:
|
||||
contact.contactPhones.map((p) => p.phoneNumber).toList(),
|
||||
icon: Icons.phone,
|
||||
label: "Phone",
|
||||
typeLabel: "Phone",
|
||||
onTap: (phone) => LauncherUtils.launchPhone(phone),
|
||||
onLongPress: (phone) => LauncherUtils.copyToClipboard(phone, typeLabel: "Phone"),
|
||||
onLongPress: (phone) =>
|
||||
LauncherUtils.copyToClipboard(phone, typeLabel: "Phone"),
|
||||
),
|
||||
_iconInfoRow(Icons.location_on, "Address", contact.address),
|
||||
]),
|
||||
// ORGANIZATION CARD
|
||||
_infoCard("Organization", [
|
||||
_iconInfoRow(Icons.business, "Organization", contact.organization),
|
||||
_iconInfoRow(
|
||||
Icons.business, "Organization", contact.organization),
|
||||
_iconInfoRow(Icons.category, "Category", category),
|
||||
]),
|
||||
// META INFO CARD
|
||||
_infoCard("Meta Info", [
|
||||
_iconInfoRow(Icons.label, "Tags", tags.isNotEmpty ? tags : "-"),
|
||||
_iconInfoRow(Icons.folder_shared, "Contact Buckets", bucketNames.isNotEmpty ? bucketNames : "-"),
|
||||
_iconInfoRow(Icons.folder_shared, "Contact Buckets",
|
||||
bucketNames.isNotEmpty ? bucketNames : "-"),
|
||||
_iconInfoRow(Icons.work_outline, "Projects", projectNames),
|
||||
]),
|
||||
// DESCRIPTION CARD
|
||||
@ -285,15 +315,16 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
);
|
||||
if (result == true) {
|
||||
await directoryController.fetchContacts();
|
||||
final updated =
|
||||
directoryController.allContacts.firstWhereOrNull((c) => c.id == contact.id);
|
||||
final updated = directoryController.allContacts
|
||||
.firstWhereOrNull((c) => c.id == contact.id);
|
||||
if (updated != null) {
|
||||
setState(() => contact = updated);
|
||||
}
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.edit, color: Colors.white),
|
||||
label: const Text("Edit Contact", style: TextStyle(color: Colors.white)),
|
||||
label: const Text("Edit Contact",
|
||||
style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -306,24 +337,49 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
if (!directoryController.contactCommentsMap.containsKey(contactId)) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
final comments = directoryController.getCommentsForContact(contactId).reversed.toList();
|
||||
|
||||
final comments = directoryController
|
||||
.getCommentsForContact(contactId)
|
||||
.reversed
|
||||
.toList();
|
||||
final editingId = directoryController.editingCommentId.value;
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
comments.isEmpty
|
||||
? Center(
|
||||
child: MyText.bodyLarge("No comments yet.", color: Colors.grey),
|
||||
)
|
||||
: Padding(
|
||||
padding: MySpacing.xy(12, 12),
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.only(bottom: 100),
|
||||
itemCount: comments.length,
|
||||
separatorBuilder: (_, __) => MySpacing.height(14),
|
||||
itemBuilder: (_, index) => _buildCommentItem(comments[index], editingId, contact.id),
|
||||
MyRefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await directoryController.fetchCommentsForContact(contactId);
|
||||
},
|
||||
child: comments.isEmpty
|
||||
? ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: [
|
||||
SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.6,
|
||||
child: Center(
|
||||
child: MyText.bodyLarge(
|
||||
"No comments yet.",
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Padding(
|
||||
padding: MySpacing.xy(12, 12),
|
||||
child: ListView.separated(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.only(bottom: 100),
|
||||
itemCount: comments.length,
|
||||
separatorBuilder: (_, __) => MySpacing.height(14),
|
||||
itemBuilder: (_, index) => _buildCommentItem(
|
||||
comments[index],
|
||||
editingId,
|
||||
contact.id,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (editingId == null)
|
||||
Positioned(
|
||||
bottom: 20,
|
||||
@ -336,11 +392,15 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
isScrollControlled: true,
|
||||
);
|
||||
if (result == true) {
|
||||
await directoryController.fetchCommentsForContact(contactId);
|
||||
await directoryController
|
||||
.fetchCommentsForContact(contactId);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.add_comment, color: Colors.white),
|
||||
label: const Text("Add Comment", style: TextStyle(color: Colors.white)),
|
||||
label: const Text(
|
||||
"Add Comment",
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -371,7 +431,9 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
color: isEditing ? Colors.indigo : Colors.grey.shade300,
|
||||
width: 1.2,
|
||||
),
|
||||
boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 4, offset: Offset(0, 2))],
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Colors.black12, blurRadius: 4, offset: Offset(0, 2))
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -406,7 +468,8 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
color: Colors.indigo,
|
||||
),
|
||||
onPressed: () {
|
||||
directoryController.editingCommentId.value = isEditing ? null : comment.id;
|
||||
directoryController.editingCommentId.value =
|
||||
isEditing ? null : comment.id;
|
||||
},
|
||||
),
|
||||
],
|
||||
@ -467,7 +530,8 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (label.isNotEmpty)
|
||||
MyText.bodySmall(label, fontWeight: 600, color: Colors.black87),
|
||||
MyText.bodySmall(label,
|
||||
fontWeight: 600, color: Colors.black87),
|
||||
if (label.isNotEmpty) MySpacing.height(2),
|
||||
MyText.bodyMedium(value, color: Colors.grey[800]),
|
||||
],
|
||||
@ -489,7 +553,8 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.titleSmall(title, fontWeight: 700, color: Colors.indigo[700]),
|
||||
MyText.titleSmall(title,
|
||||
fontWeight: 700, color: Colors.indigo[700]),
|
||||
MySpacing.height(8),
|
||||
...children,
|
||||
],
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:marco/helpers/widgets/my_refresh_indicator.dart';
|
||||
|
||||
import 'package:marco/controller/directory/directory_controller.dart';
|
||||
import 'package:marco/controller/directory/create_bucket_controller.dart';
|
||||
@ -24,7 +25,8 @@ class DirectoryView extends StatefulWidget {
|
||||
class _DirectoryViewState extends State<DirectoryView> {
|
||||
final DirectoryController controller = Get.find();
|
||||
final TextEditingController searchController = TextEditingController();
|
||||
final PermissionController permissionController = Get.put(PermissionController());
|
||||
final PermissionController permissionController =
|
||||
Get.put(PermissionController());
|
||||
|
||||
Future<void> _refreshDirectory() async {
|
||||
try {
|
||||
@ -185,19 +187,7 @@ class _DirectoryViewState extends State<DirectoryView> {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
MySpacing.width(8),
|
||||
Tooltip(
|
||||
message: 'Refresh Data',
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
onTap: _refreshDirectory,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(0),
|
||||
child: Icon(Icons.refresh, color: Colors.green, size: 28),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
MySpacing.width(8),
|
||||
Obx(() {
|
||||
final isFilterActive = controller.hasActiveFilters();
|
||||
@ -382,178 +372,231 @@ class _DirectoryViewState extends State<DirectoryView> {
|
||||
),
|
||||
Expanded(
|
||||
child: Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return ListView.separated(
|
||||
itemCount: 10,
|
||||
separatorBuilder: (_, __) => MySpacing.height(12),
|
||||
itemBuilder: (_, __) => SkeletonLoaders.contactSkeletonCard(),
|
||||
);
|
||||
}
|
||||
|
||||
if (controller.filteredContacts.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.contact_page_outlined,
|
||||
size: 60, color: Colors.grey),
|
||||
const SizedBox(height: 12),
|
||||
MyText.bodyMedium('No contacts found.', fontWeight: 500),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
padding: MySpacing.only(left: 8, right: 8, top: 4, bottom: 80),
|
||||
itemCount: controller.filteredContacts.length,
|
||||
separatorBuilder: (_, __) => MySpacing.height(12),
|
||||
itemBuilder: (_, index) {
|
||||
final contact = controller.filteredContacts[index];
|
||||
final nameParts = contact.name.trim().split(" ");
|
||||
final firstName = nameParts.first;
|
||||
final lastName = nameParts.length > 1 ? nameParts.last : "";
|
||||
final tags = contact.tags.map((tag) => tag.name).toList();
|
||||
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
Get.to(() => ContactDetailScreen(contact: contact));
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 10, 12, 0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Avatar(
|
||||
firstName: firstName,
|
||||
lastName: lastName,
|
||||
size: 35),
|
||||
MySpacing.width(12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.titleSmall(contact.name,
|
||||
fontWeight: 600,
|
||||
overflow: TextOverflow.ellipsis),
|
||||
MyText.bodySmall(contact.organization,
|
||||
color: Colors.grey[700],
|
||||
overflow: TextOverflow.ellipsis),
|
||||
MySpacing.height(8),
|
||||
|
||||
// Show only the first email (if present)
|
||||
if (contact.contactEmails.isNotEmpty)
|
||||
GestureDetector(
|
||||
onTap: () => LauncherUtils.launchEmail(
|
||||
contact.contactEmails.first.emailAddress),
|
||||
onLongPress: () =>
|
||||
LauncherUtils.copyToClipboard(
|
||||
contact.contactEmails.first.emailAddress,
|
||||
typeLabel: 'Email',
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.email_outlined,
|
||||
size: 16, color: Colors.indigo),
|
||||
MySpacing.width(4),
|
||||
Expanded(
|
||||
child: MyText.labelSmall(
|
||||
contact.contactEmails.first.emailAddress,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: Colors.indigo,
|
||||
decoration:
|
||||
TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
return MyRefreshIndicator(
|
||||
onRefresh: _refreshDirectory,
|
||||
backgroundColor: Colors.indigo,
|
||||
color: Colors.white,
|
||||
child: controller.isLoading.value
|
||||
? ListView.separated(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
itemCount: 10,
|
||||
separatorBuilder: (_, __) => MySpacing.height(12),
|
||||
itemBuilder: (_, __) =>
|
||||
SkeletonLoaders.contactSkeletonCard(),
|
||||
)
|
||||
: controller.filteredContacts.isEmpty
|
||||
? ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: [
|
||||
SizedBox(
|
||||
height:
|
||||
MediaQuery.of(context).size.height * 0.6,
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.contact_page_outlined,
|
||||
size: 60, color: Colors.grey),
|
||||
const SizedBox(height: 12),
|
||||
MyText.bodyMedium('No contacts found.',
|
||||
fontWeight: 500),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: ListView.separated(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: MySpacing.only(
|
||||
left: 8, right: 8, top: 4, bottom: 80),
|
||||
itemCount: controller.filteredContacts.length,
|
||||
separatorBuilder: (_, __) => MySpacing.height(12),
|
||||
itemBuilder: (_, index) {
|
||||
final contact =
|
||||
controller.filteredContacts[index];
|
||||
final nameParts = contact.name.trim().split(" ");
|
||||
final firstName = nameParts.first;
|
||||
final lastName =
|
||||
nameParts.length > 1 ? nameParts.last : "";
|
||||
final tags =
|
||||
contact.tags.map((tag) => tag.name).toList();
|
||||
|
||||
// Show only the first phone (if present)
|
||||
if (contact.contactPhones.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 8, top: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => LauncherUtils
|
||||
.launchPhone(contact
|
||||
.contactPhones
|
||||
.first
|
||||
.phoneNumber),
|
||||
onLongPress: () =>
|
||||
LauncherUtils.copyToClipboard(
|
||||
contact.contactPhones.first
|
||||
.phoneNumber,
|
||||
typeLabel: 'Phone',
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.phone_outlined,
|
||||
size: 16,
|
||||
color: Colors.indigo),
|
||||
MySpacing.width(4),
|
||||
Expanded(
|
||||
child: MyText.labelSmall(
|
||||
contact.contactPhones.first
|
||||
.phoneNumber,
|
||||
overflow:
|
||||
TextOverflow.ellipsis,
|
||||
color: Colors.indigo,
|
||||
decoration: TextDecoration
|
||||
.underline,
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
Get.to(() =>
|
||||
ContactDetailScreen(contact: contact));
|
||||
},
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.fromLTRB(12, 10, 12, 0),
|
||||
child: Row(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Avatar(
|
||||
firstName: firstName,
|
||||
lastName: lastName,
|
||||
size: 35),
|
||||
MySpacing.width(12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.titleSmall(contact.name,
|
||||
fontWeight: 600,
|
||||
overflow:
|
||||
TextOverflow.ellipsis),
|
||||
MyText.bodySmall(
|
||||
contact.organization,
|
||||
color: Colors.grey[700],
|
||||
overflow:
|
||||
TextOverflow.ellipsis),
|
||||
MySpacing.height(8),
|
||||
if (contact
|
||||
.contactEmails.isNotEmpty)
|
||||
GestureDetector(
|
||||
onTap: () =>
|
||||
LauncherUtils.launchEmail(
|
||||
contact
|
||||
.contactEmails
|
||||
.first
|
||||
.emailAddress),
|
||||
onLongPress: () => LauncherUtils
|
||||
.copyToClipboard(
|
||||
contact.contactEmails.first
|
||||
.emailAddress,
|
||||
typeLabel: 'Email',
|
||||
),
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(
|
||||
bottom: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.email_outlined,
|
||||
size: 16,
|
||||
color: Colors.indigo),
|
||||
MySpacing.width(4),
|
||||
Expanded(
|
||||
child:
|
||||
MyText.labelSmall(
|
||||
contact
|
||||
.contactEmails
|
||||
.first
|
||||
.emailAddress,
|
||||
overflow: TextOverflow
|
||||
.ellipsis,
|
||||
color: Colors.indigo,
|
||||
decoration:
|
||||
TextDecoration
|
||||
.underline,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (contact
|
||||
.contactPhones.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 8, top: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => LauncherUtils
|
||||
.launchPhone(contact
|
||||
.contactPhones
|
||||
.first
|
||||
.phoneNumber),
|
||||
onLongPress: () =>
|
||||
LauncherUtils
|
||||
.copyToClipboard(
|
||||
contact
|
||||
.contactPhones
|
||||
.first
|
||||
.phoneNumber,
|
||||
typeLabel: 'Phone',
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons
|
||||
.phone_outlined,
|
||||
size: 16,
|
||||
color: Colors
|
||||
.indigo),
|
||||
MySpacing.width(4),
|
||||
Expanded(
|
||||
child: MyText
|
||||
.labelSmall(
|
||||
contact
|
||||
.contactPhones
|
||||
.first
|
||||
.phoneNumber,
|
||||
overflow:
|
||||
TextOverflow
|
||||
.ellipsis,
|
||||
color: Colors
|
||||
.indigo,
|
||||
decoration:
|
||||
TextDecoration
|
||||
.underline,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
MySpacing.width(8),
|
||||
GestureDetector(
|
||||
onTap: () => LauncherUtils
|
||||
.launchWhatsApp(
|
||||
contact
|
||||
.contactPhones
|
||||
.first
|
||||
.phoneNumber),
|
||||
child: const FaIcon(
|
||||
FontAwesomeIcons
|
||||
.whatsapp,
|
||||
color: Colors.green,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (tags.isNotEmpty) ...[
|
||||
MySpacing.height(2),
|
||||
MyText.labelSmall(tags.join(', '),
|
||||
color: Colors.grey[500],
|
||||
maxLines: 1,
|
||||
overflow:
|
||||
TextOverflow.ellipsis),
|
||||
],
|
||||
],
|
||||
),
|
||||
MySpacing.width(8),
|
||||
GestureDetector(
|
||||
onTap: () =>
|
||||
LauncherUtils.launchWhatsApp(
|
||||
contact.contactPhones.first
|
||||
.phoneNumber),
|
||||
child: const FaIcon(
|
||||
FontAwesomeIcons.whatsapp,
|
||||
color: Colors.green,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
const Icon(Icons.arrow_forward_ios,
|
||||
color: Colors.grey, size: 16),
|
||||
MySpacing.height(8),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
if (tags.isNotEmpty) ...[
|
||||
MySpacing.height(2),
|
||||
MyText.labelSmall(tags.join(', '),
|
||||
color: Colors.grey[500],
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
const Icon(Icons.arrow_forward_ios,
|
||||
color: Colors.grey, size: 16),
|
||||
MySpacing.height(8),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -3,6 +3,7 @@ import 'package:get/get.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart' as quill;
|
||||
import 'package:flutter_quill_delta_from_html/flutter_quill_delta_from_html.dart';
|
||||
import 'package:flutter_html/flutter_html.dart' as html;
|
||||
import 'package:marco/helpers/widgets/my_refresh_indicator.dart';
|
||||
|
||||
import 'package:marco/controller/directory/notes_controller.dart';
|
||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||
@ -103,22 +104,7 @@ class NotesView extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
MySpacing.width(8),
|
||||
Tooltip(
|
||||
message: 'Refresh Notes',
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
onTap: _refreshNotes,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(4),
|
||||
child: Icon(Icons.refresh, color: Colors.green, size: 26),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -133,145 +119,163 @@ class NotesView extends StatelessWidget {
|
||||
final notes = controller.filteredNotesList;
|
||||
|
||||
if (notes.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
return MyRefreshIndicator(
|
||||
onRefresh: _refreshNotes,
|
||||
child: ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: [
|
||||
const Icon(Icons.note_alt_outlined,
|
||||
size: 60, color: Colors.grey),
|
||||
const SizedBox(height: 12),
|
||||
MyText.bodyMedium('No notes found.', fontWeight: 500),
|
||||
SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.6,
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.note_alt_outlined,
|
||||
size: 60, color: Colors.grey),
|
||||
const SizedBox(height: 12),
|
||||
MyText.bodyMedium('No notes found.',
|
||||
fontWeight: 500),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
padding: MySpacing.only(left: 8, right: 8, top: 4, bottom: 80),
|
||||
itemCount: notes.length,
|
||||
separatorBuilder: (_, __) => MySpacing.height(12),
|
||||
itemBuilder: (_, index) {
|
||||
final note = notes[index];
|
||||
return MyRefreshIndicator(
|
||||
onRefresh: _refreshNotes,
|
||||
child: ListView.separated(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: MySpacing.only(left: 8, right: 8, top: 4, bottom: 80),
|
||||
itemCount: notes.length,
|
||||
separatorBuilder: (_, __) => MySpacing.height(12),
|
||||
itemBuilder: (_, index) {
|
||||
final note = notes[index];
|
||||
|
||||
return Obx(() {
|
||||
final isEditing = controller.editingNoteId.value == note.id;
|
||||
return Obx(() {
|
||||
final isEditing = controller.editingNoteId.value == note.id;
|
||||
|
||||
final initials = note.contactName.trim().isNotEmpty
|
||||
? note.contactName
|
||||
.trim()
|
||||
.split(' ')
|
||||
.map((e) => e[0])
|
||||
.take(2)
|
||||
.join()
|
||||
.toUpperCase()
|
||||
: "NA";
|
||||
final initials = note.contactName.trim().isNotEmpty
|
||||
? note.contactName
|
||||
.trim()
|
||||
.split(' ')
|
||||
.map((e) => e[0])
|
||||
.take(2)
|
||||
.join()
|
||||
.toUpperCase()
|
||||
: "NA";
|
||||
|
||||
final createdDate = DateTimeUtils.convertUtcToLocal(
|
||||
note.createdAt.toString(),
|
||||
format: 'dd MMM yyyy');
|
||||
final createdTime = DateTimeUtils.convertUtcToLocal(
|
||||
note.createdAt.toString(),
|
||||
format: 'hh:mm a');
|
||||
final createdDate = DateTimeUtils.convertUtcToLocal(
|
||||
note.createdAt.toString(),
|
||||
format: 'dd MMM yyyy');
|
||||
final createdTime = DateTimeUtils.convertUtcToLocal(
|
||||
note.createdAt.toString(),
|
||||
format: 'hh:mm a');
|
||||
|
||||
final decodedDelta = HtmlToDelta().convert(note.note);
|
||||
final quillController = isEditing
|
||||
? quill.QuillController(
|
||||
document: quill.Document.fromDelta(decodedDelta),
|
||||
selection: TextSelection.collapsed(
|
||||
offset: decodedDelta.length),
|
||||
)
|
||||
: null;
|
||||
final decodedDelta = HtmlToDelta().convert(note.note);
|
||||
final quillController = isEditing
|
||||
? quill.QuillController(
|
||||
document: quill.Document.fromDelta(decodedDelta),
|
||||
selection: TextSelection.collapsed(
|
||||
offset: decodedDelta.length),
|
||||
)
|
||||
: null;
|
||||
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
padding: MySpacing.xy(12, 12),
|
||||
decoration: BoxDecoration(
|
||||
color: isEditing ? Colors.indigo[50] : Colors.white,
|
||||
border: Border.all(
|
||||
color: isEditing ? Colors.indigo : Colors.grey.shade300,
|
||||
width: 1.1,
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
padding: MySpacing.xy(12, 12),
|
||||
decoration: BoxDecoration(
|
||||
color: isEditing ? Colors.indigo[50] : Colors.white,
|
||||
border: Border.all(
|
||||
color:
|
||||
isEditing ? Colors.indigo : Colors.grey.shade300,
|
||||
width: 1.1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Colors.black12,
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 2)),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Colors.black12,
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 2)),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
/// Header Row
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Avatar(firstName: initials, lastName: '', size: 40),
|
||||
MySpacing.width(12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.titleSmall(
|
||||
"${note.contactName} (${note.organizationName})",
|
||||
fontWeight: 600,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: Colors.indigo[800],
|
||||
),
|
||||
MyText.bodySmall(
|
||||
"by ${note.createdBy.firstName} • $createdDate, $createdTime",
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
],
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
/// Header Row
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Avatar(
|
||||
firstName: initials, lastName: '', size: 40),
|
||||
MySpacing.width(12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.titleSmall(
|
||||
"${note.contactName} (${note.organizationName})",
|
||||
fontWeight: 600,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: Colors.indigo[800],
|
||||
),
|
||||
MyText.bodySmall(
|
||||
"by ${note.createdBy.firstName} • $createdDate, $createdTime",
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
isEditing ? Icons.close : Icons.edit,
|
||||
color: Colors.indigo,
|
||||
size: 20,
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
isEditing ? Icons.close : Icons.edit,
|
||||
color: Colors.indigo,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () {
|
||||
controller.editingNoteId.value =
|
||||
isEditing ? null : note.id;
|
||||
},
|
||||
),
|
||||
onPressed: () {
|
||||
controller.editingNoteId.value =
|
||||
isEditing ? null : note.id;
|
||||
],
|
||||
),
|
||||
|
||||
MySpacing.height(12),
|
||||
|
||||
/// Content
|
||||
if (isEditing && quillController != null)
|
||||
CommentEditorCard(
|
||||
controller: quillController,
|
||||
onCancel: () =>
|
||||
controller.editingNoteId.value = null,
|
||||
onSave: (quillCtrl) async {
|
||||
final delta = quillCtrl.document.toDelta();
|
||||
final htmlOutput = _convertDeltaToHtml(delta);
|
||||
final updated = note.copyWith(note: htmlOutput);
|
||||
await controller.updateNote(updated);
|
||||
controller.editingNoteId.value = null;
|
||||
},
|
||||
)
|
||||
else
|
||||
html.Html(
|
||||
data: note.note,
|
||||
style: {
|
||||
"body": html.Style(
|
||||
margin: html.Margins.zero,
|
||||
padding: html.HtmlPaddings.zero,
|
||||
fontSize: html.FontSize.medium,
|
||||
color: Colors.black87,
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
MySpacing.height(12),
|
||||
|
||||
/// Content
|
||||
if (isEditing && quillController != null)
|
||||
CommentEditorCard(
|
||||
controller: quillController,
|
||||
onCancel: () =>
|
||||
controller.editingNoteId.value = null,
|
||||
onSave: (quillCtrl) async {
|
||||
final delta = quillCtrl.document.toDelta();
|
||||
final htmlOutput = _convertDeltaToHtml(delta);
|
||||
final updated = note.copyWith(note: htmlOutput);
|
||||
await controller.updateNote(updated);
|
||||
controller.editingNoteId.value = null;
|
||||
},
|
||||
)
|
||||
else
|
||||
html.Html(
|
||||
data: note.note,
|
||||
style: {
|
||||
"body": html.Style(
|
||||
margin: html.Margins.zero,
|
||||
padding: html.HtmlPaddings.zero,
|
||||
fontSize: html.FontSize.medium,
|
||||
color: Colors.black87,
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
|
@ -10,6 +10,7 @@ import 'package:marco/view/employees/assign_employee_bottom_sheet.dart';
|
||||
import 'package:marco/helpers/utils/launcher_utils.dart';
|
||||
import 'package:marco/controller/permission_controller.dart';
|
||||
import 'package:marco/helpers/utils/permission_constants.dart';
|
||||
import 'package:marco/helpers/widgets/my_refresh_indicator.dart';
|
||||
|
||||
class EmployeeDetailPage extends StatefulWidget {
|
||||
final String employeeId;
|
||||
@ -255,40 +256,46 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> {
|
||||
}
|
||||
|
||||
return SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(12, 20, 12, 80),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Avatar(
|
||||
firstName: employee.firstName,
|
||||
lastName: employee.lastName,
|
||||
size: 45,
|
||||
),
|
||||
MySpacing.width(16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.titleMedium(
|
||||
'${employee.firstName} ${employee.lastName}',
|
||||
fontWeight: 700,
|
||||
),
|
||||
MySpacing.height(6),
|
||||
MyText.bodySmall(
|
||||
_getDisplayValue(employee.jobRole),
|
||||
fontWeight: 500,
|
||||
),
|
||||
],
|
||||
child: MyRefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await controller.fetchEmployeeDetails(widget.employeeId);
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.fromLTRB(12, 20, 12, 80),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Avatar(
|
||||
firstName: employee.firstName,
|
||||
lastName: employee.lastName,
|
||||
size: 45,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
MySpacing.height(14),
|
||||
_buildInfoCard(employee),
|
||||
],
|
||||
MySpacing.width(16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.titleMedium(
|
||||
'${employee.firstName} ${employee.lastName}',
|
||||
fontWeight: 700,
|
||||
),
|
||||
MySpacing.height(6),
|
||||
MyText.bodySmall(
|
||||
_getDisplayValue(employee.jobRole),
|
||||
fontWeight: 500,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
MySpacing.height(14),
|
||||
_buildInfoCard(employee),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -15,6 +15,7 @@ import 'package:marco/helpers/utils/launcher_utils.dart';
|
||||
import 'package:marco/view/employees/assign_employee_bottom_sheet.dart';
|
||||
import 'package:marco/controller/permission_controller.dart';
|
||||
import 'package:marco/helpers/utils/permission_constants.dart';
|
||||
import 'package:marco/helpers/widgets/my_refresh_indicator.dart';
|
||||
|
||||
class EmployeesScreen extends StatefulWidget {
|
||||
const EmployeesScreen({super.key});
|
||||
@ -151,19 +152,24 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
|
||||
tag: 'employee_screen_controller',
|
||||
builder: (_) {
|
||||
_filterEmployees(_searchController.text);
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(bottom: 40),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MySpacing.height(flexSpacing),
|
||||
_buildSearchAndActionRow(),
|
||||
MySpacing.height(flexSpacing),
|
||||
Padding(
|
||||
padding: MySpacing.x(flexSpacing),
|
||||
child: _buildEmployeeList(),
|
||||
),
|
||||
],
|
||||
|
||||
return MyRefreshIndicator(
|
||||
onRefresh: _refreshEmployees,
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.only(bottom: 40),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MySpacing.height(flexSpacing),
|
||||
_buildSearchAndActionRow(),
|
||||
MySpacing.height(flexSpacing),
|
||||
Padding(
|
||||
padding: MySpacing.x(flexSpacing),
|
||||
child: _buildEmployeeList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@ -266,8 +272,6 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
|
||||
children: [
|
||||
Expanded(child: _buildSearchField()),
|
||||
const SizedBox(width: 8),
|
||||
_buildRefreshButton(),
|
||||
const SizedBox(width: 4),
|
||||
_buildPopupMenu(),
|
||||
],
|
||||
),
|
||||
@ -315,20 +319,6 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRefreshButton() {
|
||||
return Tooltip(
|
||||
message: 'Refresh Data',
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
onTap: _refreshEmployees,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Icon(Icons.refresh, color: Colors.green, size: 28),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPopupMenu() {
|
||||
if (!_permissionController.hasPermission(Permissions.viewAllEmployees)) {
|
||||
return const SizedBox.shrink();
|
||||
|
@ -13,6 +13,7 @@ import 'package:marco/model/expense/reimbursement_bottom_sheet.dart';
|
||||
import 'package:marco/controller/expense/add_expense_controller.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:marco/helpers/widgets/my_refresh_indicator.dart';
|
||||
|
||||
import 'package:marco/helpers/widgets/expense_detail_helpers.dart';
|
||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||
@ -92,36 +93,41 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen> {
|
||||
colorCode: expense.status.color);
|
||||
final formattedAmount = formatExpenseAmount(expense.amount);
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
8, 8, 8, 30 + MediaQuery.of(context).padding.bottom),
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 520),
|
||||
child: Card(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
elevation: 3,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 14, horizontal: 14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_InvoiceHeader(expense: expense),
|
||||
const Divider(height: 30, thickness: 1.2),
|
||||
_InvoiceParties(expense: expense),
|
||||
const Divider(height: 30, thickness: 1.2),
|
||||
_InvoiceDetailsTable(expense: expense),
|
||||
const Divider(height: 30, thickness: 1.2),
|
||||
_InvoiceDocuments(documents: expense.documents),
|
||||
const Divider(height: 30, thickness: 1.2),
|
||||
_InvoiceTotals(
|
||||
expense: expense,
|
||||
formattedAmount: formattedAmount,
|
||||
statusColor: statusColor,
|
||||
),
|
||||
],
|
||||
return MyRefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await controller.fetchExpenseDetails();
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
8, 8, 8, 30 + MediaQuery.of(context).padding.bottom),
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 520),
|
||||
child: Card(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
elevation: 3,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 14, horizontal: 14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_InvoiceHeader(expense: expense),
|
||||
const Divider(height: 30, thickness: 1.2),
|
||||
_InvoiceParties(expense: expense),
|
||||
const Divider(height: 30, thickness: 1.2),
|
||||
_InvoiceDetailsTable(expense: expense),
|
||||
const Divider(height: 30, thickness: 1.2),
|
||||
_InvoiceDocuments(documents: expense.documents),
|
||||
const Divider(height: 30, thickness: 1.2),
|
||||
_InvoiceTotals(
|
||||
expense: expense,
|
||||
formattedAmount: formattedAmount,
|
||||
statusColor: statusColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -10,6 +10,7 @@ import 'package:marco/model/expense/add_expense_bottom_sheet.dart';
|
||||
import 'package:marco/view/expense/expense_filter_bottom_sheet.dart';
|
||||
import 'package:marco/helpers/widgets/expense_main_components.dart';
|
||||
import 'package:marco/helpers/utils/permission_constants.dart';
|
||||
import 'package:marco/helpers/widgets/my_refresh_indicator.dart';
|
||||
|
||||
class ExpenseMainScreen extends StatefulWidget {
|
||||
const ExpenseMainScreen({super.key});
|
||||
@ -31,7 +32,9 @@ class _ExpenseMainScreenState extends State<ExpenseMainScreen> {
|
||||
expenseController.fetchExpenses();
|
||||
}
|
||||
|
||||
void _refreshExpenses() => expenseController.fetchExpenses();
|
||||
Future<void> _refreshExpenses() async {
|
||||
await expenseController.fetchExpenses();
|
||||
}
|
||||
|
||||
void _openFilterBottomSheet() {
|
||||
showModalBottomSheet(
|
||||
@ -81,7 +84,6 @@ class _ExpenseMainScreenState extends State<ExpenseMainScreen> {
|
||||
controller: searchController,
|
||||
onChanged: (_) => setState(() {}),
|
||||
onFilterTap: _openFilterBottomSheet,
|
||||
onRefreshTap: _refreshExpenses,
|
||||
expenseController: expenseController,
|
||||
),
|
||||
ToggleButtonsRow(
|
||||
@ -90,38 +92,55 @@ class _ExpenseMainScreenState extends State<ExpenseMainScreen> {
|
||||
),
|
||||
Expanded(
|
||||
child: Obx(() {
|
||||
// Loader while fetching first time
|
||||
if (expenseController.isLoading.value &&
|
||||
expenseController.expenses.isEmpty) {
|
||||
return SkeletonLoaders.expenseListSkeletonLoader();
|
||||
}
|
||||
|
||||
if (expenseController.errorMessage.isNotEmpty) {
|
||||
return Center(
|
||||
child: MyText.bodyMedium(
|
||||
expenseController.errorMessage.value,
|
||||
color: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final filteredList = _getFilteredExpenses();
|
||||
|
||||
return NotificationListener<ScrollNotification>(
|
||||
onNotification: (ScrollNotification scrollInfo) {
|
||||
if (scrollInfo.metrics.pixels ==
|
||||
scrollInfo.metrics.maxScrollExtent &&
|
||||
!expenseController.isLoading.value) {
|
||||
expenseController.loadMoreExpenses();
|
||||
}
|
||||
return false;
|
||||
},
|
||||
child: ExpenseList(
|
||||
expenseList: filteredList,
|
||||
onViewDetail: () => expenseController.fetchExpenses(),
|
||||
),
|
||||
return MyRefreshIndicator(
|
||||
onRefresh: _refreshExpenses,
|
||||
child: filteredList.isEmpty
|
||||
? ListView(
|
||||
physics:
|
||||
const AlwaysScrollableScrollPhysics(), // important
|
||||
children: [
|
||||
SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.5,
|
||||
child: Center(
|
||||
child: MyText.bodyMedium(
|
||||
expenseController.errorMessage.isNotEmpty
|
||||
? expenseController.errorMessage.value
|
||||
: "No expenses found",
|
||||
color:
|
||||
expenseController.errorMessage.isNotEmpty
|
||||
? Colors.red
|
||||
: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: NotificationListener<ScrollNotification>(
|
||||
onNotification: (scrollInfo) {
|
||||
if (scrollInfo.metrics.pixels ==
|
||||
scrollInfo.metrics.maxScrollExtent &&
|
||||
!expenseController.isLoading.value) {
|
||||
expenseController.loadMoreExpenses();
|
||||
}
|
||||
return false;
|
||||
},
|
||||
child: ExpenseList(
|
||||
expenseList: filteredList,
|
||||
onViewDetail: () =>
|
||||
expenseController.fetchExpenses(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -16,6 +16,7 @@ import 'package:marco/controller/project_controller.dart';
|
||||
import 'package:marco/model/dailyTaskPlaning/task_action_buttons.dart';
|
||||
import 'package:marco/helpers/widgets/my_custom_skeleton.dart';
|
||||
import 'package:marco/helpers/utils/permission_constants.dart';
|
||||
import 'package:marco/helpers/widgets/my_refresh_indicator.dart';
|
||||
|
||||
class DailyProgressReportScreen extends StatefulWidget {
|
||||
const DailyProgressReportScreen({super.key});
|
||||
@ -127,24 +128,32 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: MySpacing.x(0),
|
||||
child: GetBuilder<DailyTaskController>(
|
||||
init: dailyTaskController,
|
||||
tag: 'daily_progress_report_controller',
|
||||
builder: (controller) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MySpacing.height(flexSpacing),
|
||||
_buildActionBar(),
|
||||
Padding(
|
||||
padding: MySpacing.x(8),
|
||||
child: _buildDailyProgressReportTab(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
child: MyRefreshIndicator(
|
||||
onRefresh: _refreshData,
|
||||
child: CustomScrollView(
|
||||
physics:
|
||||
const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: GetBuilder<DailyTaskController>(
|
||||
init: dailyTaskController,
|
||||
tag: 'daily_progress_report_controller',
|
||||
builder: (controller) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MySpacing.height(flexSpacing),
|
||||
_buildActionBar(),
|
||||
Padding(
|
||||
padding: MySpacing.x(8),
|
||||
child: _buildDailyProgressReportTab(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -163,14 +172,6 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
|
||||
tooltip: 'Filter Project',
|
||||
onTap: _openFilterSheet,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildActionItem(
|
||||
label: "Refresh",
|
||||
icon: Icons.refresh,
|
||||
tooltip: 'Refresh Data',
|
||||
color: Colors.green,
|
||||
onTap: _refreshData,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -468,7 +469,8 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
|
||||
.toString()
|
||||
.isEmpty) &&
|
||||
permissionController.hasPermission(
|
||||
Permissions.assignReportTask)) ...[
|
||||
Permissions
|
||||
.assignReportTask)) ...[
|
||||
TaskActionButtons.reportButton(
|
||||
context: context,
|
||||
task: task,
|
||||
@ -478,8 +480,7 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
|
||||
const SizedBox(width: 4),
|
||||
] else if (task.approvedBy == null &&
|
||||
permissionController.hasPermission(
|
||||
Permissions
|
||||
.approveTask)) ...[
|
||||
Permissions.approveTask)) ...[
|
||||
TaskActionButtons.reportActionButton(
|
||||
context: context,
|
||||
task: task,
|
||||
|
@ -12,6 +12,7 @@ import 'package:percent_indicator/percent_indicator.dart';
|
||||
import 'package:marco/model/dailyTaskPlaning/assign_task_bottom_sheet .dart';
|
||||
import 'package:marco/helpers/widgets/my_custom_skeleton.dart';
|
||||
import 'package:marco/helpers/utils/permission_constants.dart';
|
||||
import 'package:marco/helpers/widgets/my_refresh_indicator.dart';
|
||||
|
||||
class DailyTaskPlaningScreen extends StatefulWidget {
|
||||
DailyTaskPlaningScreen({super.key});
|
||||
@ -112,60 +113,45 @@ class _DailyTaskPlaningScreenState extends State<DailyTaskPlaningScreen>
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: MySpacing.x(0),
|
||||
child: GetBuilder<DailyTaskPlaningController>(
|
||||
init: dailyTaskPlaningController,
|
||||
tag: 'daily_task_planing_controller',
|
||||
builder: (controller) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MySpacing.height(flexSpacing),
|
||||
Padding(
|
||||
padding: MySpacing.x(flexSpacing),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
const SizedBox(width: 8),
|
||||
MyText.bodyMedium("Refresh", fontWeight: 600),
|
||||
Tooltip(
|
||||
message: 'Refresh Data',
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
onTap: () async {
|
||||
final projectId =
|
||||
projectController.selectedProjectId.value;
|
||||
if (projectId.isNotEmpty) {
|
||||
try {
|
||||
await dailyTaskPlaningController
|
||||
.fetchTaskData(projectId);
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'Error refreshing task data: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
},
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Icon(Icons.refresh,
|
||||
color: Colors.green, size: 28),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: MySpacing.x(8),
|
||||
child: dailyProgressReportTab(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
child: MyRefreshIndicator(
|
||||
onRefresh: () async {
|
||||
final projectId = projectController.selectedProjectId.value;
|
||||
if (projectId.isNotEmpty) {
|
||||
try {
|
||||
await dailyTaskPlaningController.fetchTaskData(projectId);
|
||||
} catch (e) {
|
||||
debugPrint('Error refreshing task data: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
physics:
|
||||
const AlwaysScrollableScrollPhysics(), // <-- always allow drag
|
||||
padding: MySpacing.x(0),
|
||||
child: ConstrainedBox(
|
||||
// <-- ensures full screen height
|
||||
constraints: BoxConstraints(
|
||||
minHeight: MediaQuery.of(context).size.height -
|
||||
kToolbarHeight -
|
||||
MediaQuery.of(context).padding.top,
|
||||
),
|
||||
child: GetBuilder<DailyTaskPlaningController>(
|
||||
init: dailyTaskPlaningController,
|
||||
tag: 'daily_task_planing_controller',
|
||||
builder: (controller) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MySpacing.height(flexSpacing),
|
||||
Padding(
|
||||
padding: MySpacing.x(8),
|
||||
child: dailyProgressReportTab(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
Loading…
x
Reference in New Issue
Block a user