first commit

This commit is contained in:
2026-02-07 15:57:09 +07:00
commit 157096f164
1153 changed files with 415766 additions and 0 deletions

View File

@@ -0,0 +1,280 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; // Import Riverpod
import 'package:iconly/iconly.dart';
import 'package:intl/intl.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
import '../../../constant.dart';
// --- Riverpod Imports (Assuming these paths are correct) ---
import 'package:mobile_pos/Screens/hrm/holiday/model/holiday_list_model.dart';
import 'package:mobile_pos/Screens/hrm/holiday/repo/holiday_repo.dart';
// -----------------------------------------------------------
// Accept optional HolidayData for editing (renamed from isEdit)
class AddNewHoliday extends ConsumerStatefulWidget {
final HolidayData? holidayData;
// Changed constructor to use key and remove isEdit
const AddNewHoliday({super.key, this.holidayData});
@override
ConsumerState<AddNewHoliday> createState() => _AddNewHolidayState();
}
class _AddNewHolidayState extends ConsumerState<AddNewHoliday> {
final nameController = TextEditingController();
final startDateController = TextEditingController();
final endDateController = TextEditingController();
final descriptionController = TextEditingController();
final GlobalKey<FormState> _key = GlobalKey();
// Variables to hold parsed dates for comparison/API formatting
DateTime? _selectedStartDate;
DateTime? _selectedEndDate;
bool get isEditing => widget.holidayData != null;
final DateFormat _displayFormat = DateFormat('dd/MM/yyyy');
final DateFormat _apiFormat = DateFormat('yyyy-MM-dd'); // API typically needs YYYY-MM-DD
@override
void initState() {
super.initState();
if (isEditing) {
final holiday = widget.holidayData!;
nameController.text = holiday.name ?? '';
descriptionController.text = holiday.description ?? '';
try {
if (holiday.startDate != null) {
_selectedStartDate = DateTime.parse(holiday.startDate!);
startDateController.text = _displayFormat.format(_selectedStartDate!);
}
if (holiday.endDate != null) {
_selectedEndDate = DateTime.parse(holiday.endDate!);
endDateController.text = _displayFormat.format(_selectedEndDate!);
}
} catch (e) {
// Handle date parsing failure if API format is inconsistent
debugPrint('Error parsing date for editing: $e');
startDateController.text = holiday.startDate ?? '';
endDateController.text = holiday.endDate ?? '';
}
}
}
@override
void dispose() {
nameController.dispose();
startDateController.dispose();
endDateController.dispose();
descriptionController.dispose();
super.dispose();
}
Future<void> _selectDate(BuildContext context, TextEditingController controller, bool isStart) async {
DateTime initialDate = DateTime.now();
if (isStart) {
initialDate = _selectedStartDate ?? initialDate;
} else {
initialDate = _selectedEndDate ?? _selectedStartDate ?? initialDate;
}
final DateTime? picked = await showDatePicker(
initialDate: initialDate,
firstDate: DateTime(2015, 8),
lastDate: DateTime(2101),
context: context,
);
if (picked != null) {
setState(() {
controller.text = _displayFormat.format(picked);
if (isStart) {
_selectedStartDate = picked;
// Auto-adjust end date if it is before the new start date
if (_selectedEndDate != null && _selectedEndDate!.isBefore(picked)) {
_selectedEndDate = picked;
endDateController.text = _displayFormat.format(picked);
}
} else {
_selectedEndDate = picked;
}
});
}
}
void _submit() async {
if (_key.currentState!.validate()) {
// Validate that dates are not null and end date is not before start date
if (_selectedStartDate == null || _selectedEndDate == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(lang.S.of(context).pleaseSelectValidStartAndEndDates)),
);
return;
}
if (_selectedEndDate!.isBefore(_selectedStartDate!)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(lang.S.of(context).endDateCannotBeBeforeStartDate)),
);
return;
}
final repo = HolidayRepo();
final String apiStartDate = _apiFormat.format(_selectedStartDate!);
final String apiEndDate = _apiFormat.format(_selectedEndDate!);
if (isEditing) {
// --- UPDATE HOLIDAY ---
await repo.updateHolidays(
ref: ref,
context: context,
id: widget.holidayData!.id!.toInt(),
name: nameController.text,
startDate: apiStartDate,
endDate: apiEndDate,
description: descriptionController.text,
);
} else {
// --- CREATE HOLIDAY ---
await repo.createHolidays(
ref: ref,
context: context,
name: nameController.text,
startDate: apiStartDate,
endDate: apiEndDate,
description: descriptionController.text,
);
}
// Note: The repo functions already handle Navigator.pop(context) and SnackBar
}
}
@override
Widget build(BuildContext context) {
final _lang = lang.S.of(context);
return Scaffold(
backgroundColor: kWhite,
appBar: AppBar(
centerTitle: true,
title: Text(
isEditing ? _lang.editHoliday : _lang.addNewHoliday,
),
bottom: const PreferredSize(
preferredSize: Size.fromHeight(1),
child: Divider(
height: 2,
color: kBackgroundColor,
),
),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Form(
key: _key,
child: Column(
children: [
TextFormField(
controller: nameController,
decoration: InputDecoration(
labelText: _lang.name,
hintText: _lang.enterHolidayName,
),
validator: (value) => value!.isEmpty ? _lang.pleaseEnterHolidayName : null,
),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: TextFormField(
keyboardType: TextInputType.name,
readOnly: true,
controller: startDateController,
decoration: InputDecoration(
labelText: _lang.startDate,
hintText: _lang.pleaseEnterDate,
suffixIcon: IconButton(
padding: EdgeInsets.zero,
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
onPressed: () => _selectDate(context, startDateController, true),
icon: const Icon(IconlyLight.calendar, size: 22),
),
),
validator: (value) => value!.isEmpty ? _lang.pleaseSelectStartDate : null,
),
),
const SizedBox(width: 16),
Expanded(
child: TextFormField(
keyboardType: TextInputType.name,
readOnly: true,
controller: endDateController,
decoration: InputDecoration(
labelText: _lang.endDate,
hintText: _lang.pleaseEnterDate,
suffixIcon: IconButton(
padding: EdgeInsets.zero,
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
onPressed: () => _selectDate(context, endDateController, false),
icon: const Icon(IconlyLight.calendar, size: 22),
),
),
validator: (value) {
if (value!.isEmpty) {
return _lang.pleaseEnterEndDate;
}
if (_selectedStartDate != null &&
_selectedEndDate != null &&
_selectedEndDate!.isBefore(_selectedStartDate!)) {
return _lang.endDateBeforeStartDate;
}
return null;
},
),
),
],
),
const SizedBox(height: 20),
TextFormField(
controller: descriptionController,
decoration: InputDecoration(
labelText: _lang.description,
hintText: '${_lang.enterDescription}...',
),
),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () {
// Reset functionality
setState(() {
_key.currentState?.reset();
nameController.clear();
descriptionController.clear();
startDateController.clear();
endDateController.clear();
_selectedStartDate = null;
_selectedEndDate = null;
});
},
child: Text(_lang.resets),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: _submit, // Call the submission function
child: Text(
isEditing ? _lang.update : _lang.save,
),
),
),
],
),
],
)),
),
);
}
}

View File

@@ -0,0 +1,324 @@
// File: holiday_list.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hugeicons/hugeicons.dart';
import 'package:intl/intl.dart'; // Import for DateFormat
// --- Local Imports (Assuming correct paths) ---
import 'package:mobile_pos/Screens/hrm/holiday/add_new_holiday.dart';
import 'package:mobile_pos/Screens/hrm/widgets/model_bottom_sheet.dart';
import 'package:mobile_pos/constant.dart';
import '../../../service/check_user_role_permission_provider.dart'; // PermissionService
import '../../../widgets/empty_widget/_empty_widget.dart'; // PermitDenyWidget (Assuming this exists)
import '../widgets/deleteing_alart_dialog.dart';
import '../widgets/global_search_appbar.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
// --- Data Layer Imports ---
import 'package:mobile_pos/Screens/hrm/holiday/model/holiday_list_model.dart';
import 'package:mobile_pos/Screens/hrm/holiday/repo/holiday_repo.dart';
import 'package:mobile_pos/Screens/hrm/holiday/provider/holidays_list_provider.dart';
class HolidayList extends ConsumerStatefulWidget {
const HolidayList({super.key});
@override
ConsumerState<HolidayList> createState() => _HolidayListState();
}
class _HolidayListState extends ConsumerState<HolidayList> {
final TextEditingController _searchController = TextEditingController();
List<HolidayData> _filteredHolidays = [];
bool _isSearch = false;
@override
void initState() {
super.initState();
_searchController.addListener(_onSearchChanged);
}
@override
void dispose() {
_searchController.removeListener(_onSearchChanged);
_searchController.dispose();
super.dispose();
}
// --- Date Formatting Utility (FIX) ---
String _formatDateForDisplay(String? date) {
if (date == null || date.isEmpty) return 'N/A';
try {
// Parse YYYY-MM-DD from API
final dateTime = DateFormat('yyyy-MM-dd').parse(date);
// Format to dd MMM, yyyy (e.g., 02 Jun, 2025)
return DateFormat('dd MMM, yyyy').format(dateTime);
} catch (_) {
return date;
}
}
// --- End Date Formatting Utility ---
void _onSearchChanged() {
setState(() {
// Trigger rebuild to re-apply filter
});
}
void _filterHolidays(List<HolidayData> allHolidays) {
final query = _searchController.text.toLowerCase().trim();
if (query.isEmpty) {
_filteredHolidays = allHolidays;
} else {
_filteredHolidays = allHolidays.where((holiday) {
final nameMatch = (holiday.name ?? '').toLowerCase().contains(query);
final branchMatch = (holiday.branch?.name ?? '').toLowerCase().contains(query);
final startDateMatch = (holiday.startDate ?? '').toLowerCase().contains(query);
final endDateMatch = (holiday.endDate ?? '').toLowerCase().contains(query);
return nameMatch || branchMatch || startDateMatch || endDateMatch;
}).toList();
}
}
@override
Widget build(BuildContext context) {
final _lang = lang.S.of(context);
final holidayListAsync = ref.watch(holidayListProvider);
// Assuming PermissionService and Permit enum exist globally or are accessible
final permissionService = PermissionService(ref);
return Scaffold(
backgroundColor: Colors.white,
appBar: GlobalSearchAppBar(
isSearch: _isSearch,
onSearchToggle: () {
setState(() {
_isSearch = !_isSearch;
if (!_isSearch) {
_searchController.clear();
}
});
},
title: _lang.holidayList,
controller: _searchController,
onChanged: (query) {
// Handled by _searchController.addListener
},
),
body: holidayListAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, stack) => Center(
child: Text('Failed to load holidays: $err'),
),
data: (model) {
// Check read permission
if (!permissionService.hasPermission(Permit.holidaysRead.value)) {
return const Center(child: PermitDenyWidget());
}
final allHolidays = model.data ?? [];
// Apply filter
_filterHolidays(allHolidays);
if (_filteredHolidays.isEmpty) {
return Center(
child: Text(
_searchController.text.isEmpty
? _lang.noHolidayFound
: '${_lang.noHolidayFundMatching}"${_searchController.text}".',
style: Theme.of(context).textTheme.titleMedium,
),
);
}
return ListView.separated(
padding: EdgeInsets.zero,
itemCount: _filteredHolidays.length,
separatorBuilder: (_, __) => const Divider(
color: kBackgroundColor,
height: 1.5,
),
itemBuilder: (_, index) => _buildHolidayItem(
context: context,
ref: ref,
holiday: _filteredHolidays[index], // Use filtered list
),
);
},
),
bottomNavigationBar: permissionService.hasPermission(Permit.holidaysCreate.value)
? Padding(
padding: const EdgeInsets.all(16),
child: ElevatedButton.icon(
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const AddNewHoliday(),
),
),
icon: const Icon(Icons.add, color: Colors.white),
label: Text(_lang.addHoliday),
),
)
: null,
);
}
// --- Helper Methods ---
Widget _buildHolidayItem({
required BuildContext context,
required WidgetRef ref,
required HolidayData holiday,
}) {
final theme = Theme.of(context);
// FIX: Formatting the dates for display
final String startDateDisplay = _formatDateForDisplay(holiday.startDate);
final String endDateDisplay = _formatDateForDisplay(holiday.endDate);
final String description = holiday.description ?? 'N/A';
return InkWell(
onTap: () => viewModalSheet(
context: context,
item: {
lang.S.of(context).name: holiday.name ?? 'N/A',
lang.S.of(context).startDate: startDateDisplay,
lang.S.of(context).endDate: endDateDisplay,
},
description: description,
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13.5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
holiday.name ?? 'n/a',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
_buildActionButtons(context, ref, holiday),
],
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildTimeColumn(
time: startDateDisplay, // Use formatted date
label: lang.S.of(context).startDate,
theme: theme,
),
_buildTimeColumn(
time: endDateDisplay, // Use formatted date
label: lang.S.of(context).endDate,
theme: theme,
),
const SizedBox(width: 50),
],
),
],
),
),
);
}
Widget _buildTimeColumn({
required String time,
required String label,
required ThemeData theme,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
time,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 6),
Text(
label,
style: theme.textTheme.bodyMedium?.copyWith(
color: kNeutral800,
),
),
],
);
}
Widget _buildActionButtons(BuildContext context, WidgetRef ref, HolidayData holiday) {
final permissionService = PermissionService(ref);
return Column(
children: [
GestureDetector(
onTap: () {
if (!permissionService.hasPermission(Permit.holidaysUpdate.value)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.red,
content: Text(lang.S.of(context).youDoNotHavePermissionToUpgradeHoliday),
),
);
return;
}
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AddNewHoliday(holidayData: holiday),
),
);
},
child: const HugeIcon(
icon: HugeIcons.strokeRoundedPencilEdit02,
color: kSuccessColor,
size: 20,
),
),
const SizedBox(height: 8),
GestureDetector(
onTap: () {
if (!permissionService.hasPermission(Permit.holidaysDelete.value)) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
backgroundColor: Colors.red,
content: Text("You do not have permission to delete Holidays."),
),
);
return;
}
if (holiday.id != null) {
_showDeleteConfirmationDialog(context, ref, holiday.id!);
}
},
child: const HugeIcon(
icon: HugeIcons.strokeRoundedDelete03,
color: Colors.red,
size: 20,
),
),
],
);
}
void _showDeleteConfirmationDialog(BuildContext context, WidgetRef ref, num id) async {
bool result = await showDeleteConfirmationDialog(
context: context,
itemName: lang.S.of(context).holiday,
);
if (result) {
final repo = HolidayRepo();
await repo.deleteHolidays(id: id, context: context, ref: ref);
// The repo method handles refreshing the list
}
}
}

View File

@@ -0,0 +1,103 @@
class HolidayListModel {
HolidayListModel({
this.message,
this.data,
});
HolidayListModel.fromJson(dynamic json) {
message = json['message'];
if (json['data'] != null) {
data = [];
json['data'].forEach((v) {
data?.add(HolidayData.fromJson(v));
});
}
}
String? message;
List<HolidayData>? data;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['message'] = message;
if (data != null) {
map['data'] = data?.map((v) => v.toJson()).toList();
}
return map;
}
}
class HolidayData {
HolidayData({
this.id,
this.businessId,
this.branchId,
this.name,
this.startDate,
this.endDate,
this.description,
this.createdAt,
this.updatedAt,
this.branch,
});
HolidayData.fromJson(dynamic json) {
id = json['id'];
businessId = json['business_id'];
branchId = json['branch_id'];
name = json['name'];
startDate = json['start_date'];
endDate = json['end_date'];
description = json['description'];
createdAt = json['created_at'];
updatedAt = json['updated_at'];
branch = json['branch'] != null ? Branch.fromJson(json['branch']) : null;
}
num? id;
num? businessId;
num? branchId;
String? name;
String? startDate;
String? endDate;
String? description;
String? createdAt;
String? updatedAt;
Branch? branch;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['id'] = id;
map['business_id'] = businessId;
map['branch_id'] = branchId;
map['name'] = name;
map['start_date'] = startDate;
map['end_date'] = endDate;
map['description'] = description;
map['created_at'] = createdAt;
map['updated_at'] = updatedAt;
if (branch != null) {
map['branch'] = branch?.toJson();
}
return map;
}
}
class Branch {
Branch({
this.id,
this.name,
});
Branch.fromJson(dynamic json) {
id = json['id'];
name = json['name'];
}
num? id;
String? name;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['id'] = id;
map['name'] = name;
return map;
}
}

View File

@@ -0,0 +1,8 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mobile_pos/Screens/hrm/department/model/department_list_model.dart';
import 'package:mobile_pos/Screens/hrm/department/repo/department_repo.dart';
import 'package:mobile_pos/Screens/hrm/holiday/model/holiday_list_model.dart';
import 'package:mobile_pos/Screens/hrm/holiday/repo/holiday_repo.dart';
final repo = HolidayRepo();
final holidayListProvider = FutureProvider<HolidayListModel>((ref) => repo.fetchAllHolidays());

View File

@@ -0,0 +1,171 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:http/http.dart' as http;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mobile_pos/Screens/hrm/holiday/model/holiday_list_model.dart';
import 'package:mobile_pos/Screens/hrm/holiday/provider/holidays_list_provider.dart';
import '../../../../Const/api_config.dart';
import '../../../../http_client/custome_http_client.dart';
import '../../../../http_client/customer_http_client_get.dart';
class HolidayRepo {
///---------------- FETCH HOLIDAYS ----------------///
Future<HolidayListModel> fetchAllHolidays() async {
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
final uri = Uri.parse('${APIConfig.url}/holidays');
final response = await clientGet.get(url: uri);
if (response.statusCode == 200) {
final parsedData = jsonDecode(response.body);
return HolidayListModel.fromJson(parsedData);
} else {
throw Exception('Failed to fetch Holidays list');
}
}
///---------------- CREATE HOLIDAY ----------------///
Future<void> createHolidays({
required WidgetRef ref,
required BuildContext context,
required String name,
required String startDate,
required String endDate,
required String description,
}) async {
final uri = Uri.parse('${APIConfig.url}/holidays'); // Modified endpoint
final requestBody = jsonEncode({
'name': name,
'start_date': startDate, // Field names match the model/API
'end_date': endDate, // Field names match the model/API
'description': description,
});
try {
EasyLoading.show(status: 'Creating Holiday...');
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
var responseData = await customHttpClient.post(
url: uri,
addContentTypeInHeader: true,
body: requestBody,
);
final parsedData = jsonDecode(responseData.body);
EasyLoading.dismiss();
if (responseData.statusCode == 200) {
ref.refresh(holidayListProvider); // Refresh the list after creation
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(parsedData['message'] ?? 'Holiday created successfully')),
);
Navigator.pop(context);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Holiday creation failed: ${parsedData['message'] ?? 'Unknown error'}')),
);
}
} catch (error) {
EasyLoading.dismiss();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('An error occurred: $error')),
);
}
}
///---------------- UPDATE HOLIDAY ----------------///
Future<void> updateHolidays({
// Renamed function
required WidgetRef ref,
required BuildContext context,
required int id,
required String name,
required String startDate,
required String endDate,
required String description,
}) async {
final uri = Uri.parse('${APIConfig.url}/holidays/$id'); // Modified endpoint
final requestBody = jsonEncode({
'_method': 'put', // Required for PUT/PATCH via POST on some APIs
'name': name,
'start_date': startDate,
'end_date': endDate,
'description': description,
});
try {
EasyLoading.show(status: 'Updating Holiday...');
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
var responseData = await customHttpClient.post(
// Assuming the PUT is sent via POST with '_method': 'put'
url: uri,
addContentTypeInHeader: true,
body: requestBody,
);
final parsedData = jsonDecode(responseData.body);
EasyLoading.dismiss();
if (responseData.statusCode == 200) {
ref.refresh(holidayListProvider); // Refresh the list after update
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(parsedData['message'] ?? 'Holiday updated successfully')),
);
Navigator.pop(context);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Holiday update failed: ${parsedData['message'] ?? 'Unknown error'}')),
);
}
} catch (error) {
EasyLoading.dismiss();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('An error occurred: $error')),
);
}
}
///---------------- DELETE HOLIDAY ----------------///
Future<bool> deleteHolidays({
// Renamed function
required num id, // Changed to num to match model's id type
required BuildContext context,
required WidgetRef ref,
}) async {
try {
EasyLoading.show(status: 'Deleting...');
final url = Uri.parse('${APIConfig.url}/holidays/$id'); // Modified endpoint
CustomHttpClient customHttpClient = CustomHttpClient(ref: ref, context: context, client: http.Client());
final response = await customHttpClient.delete(url: url);
EasyLoading.dismiss();
if (response.statusCode == 200) {
ref.refresh(holidayListProvider); // Refresh the list after deletion
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Holiday deleted successfully')),
);
return true;
} else {
final parsedData = jsonDecode(response.body);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Deletion failed: ${parsedData['message'] ?? 'Unknown error'}')),
);
return false;
}
} catch (error) {
EasyLoading.dismiss();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('An error occurred during deletion: $error')),
);
return false;
}
}
}