first commit
This commit is contained in:
88
lib/Screens/hrm/shift/Model/shift_list_model.dart
Normal file
88
lib/Screens/hrm/shift/Model/shift_list_model.dart
Normal file
@@ -0,0 +1,88 @@
|
||||
class ShiftListModel {
|
||||
ShiftListModel({
|
||||
this.message,
|
||||
this.data,
|
||||
});
|
||||
|
||||
ShiftListModel.fromJson(dynamic json) {
|
||||
message = json['message'];
|
||||
if (json['data'] != null) {
|
||||
data = [];
|
||||
json['data'].forEach((v) {
|
||||
data?.add(ShiftData.fromJson(v));
|
||||
});
|
||||
}
|
||||
}
|
||||
String? message;
|
||||
List<ShiftData>? 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 ShiftData {
|
||||
ShiftData({
|
||||
this.id,
|
||||
this.name,
|
||||
this.businessId,
|
||||
this.startTime,
|
||||
this.endTime,
|
||||
this.startBreakTime,
|
||||
this.endBreakTime,
|
||||
this.breakTime,
|
||||
this.breakStatus,
|
||||
this.status,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
});
|
||||
|
||||
ShiftData.fromJson(dynamic json) {
|
||||
id = json['id'];
|
||||
name = json['name'];
|
||||
businessId = json['business_id'];
|
||||
startTime = json['start_time'];
|
||||
endTime = json['end_time'];
|
||||
startBreakTime = json['start_break_time'];
|
||||
endBreakTime = json['end_break_time'];
|
||||
breakTime = json['break_time'];
|
||||
breakStatus = json['break_status'];
|
||||
status = json['status'];
|
||||
createdAt = json['created_at'];
|
||||
updatedAt = json['updated_at'];
|
||||
}
|
||||
num? id;
|
||||
String? name;
|
||||
num? businessId;
|
||||
String? startTime;
|
||||
String? endTime;
|
||||
String? startBreakTime;
|
||||
String? endBreakTime;
|
||||
String? breakTime;
|
||||
String? breakStatus;
|
||||
num? status;
|
||||
String? createdAt;
|
||||
String? updatedAt;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['id'] = id;
|
||||
map['name'] = name;
|
||||
map['business_id'] = businessId;
|
||||
map['start_time'] = startTime;
|
||||
map['end_time'] = endTime;
|
||||
map['start_break_time'] = startBreakTime;
|
||||
map['end_break_time'] = endBreakTime;
|
||||
map['break_time'] = breakTime;
|
||||
map['break_status'] = breakStatus;
|
||||
map['status'] = status;
|
||||
map['created_at'] = createdAt;
|
||||
map['updated_at'] = updatedAt;
|
||||
return map;
|
||||
}
|
||||
}
|
||||
316
lib/Screens/hrm/shift/add_new_shift.dart
Normal file
316
lib/Screens/hrm/shift/add_new_shift.dart
Normal file
@@ -0,0 +1,316 @@
|
||||
// File: add_new_shift.dart (Shift Name Changed back to Dropdown)
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mobile_pos/Screens/hrm/shift/Model/shift_list_model.dart';
|
||||
import 'package:mobile_pos/Screens/hrm/shift/repo/shift_repo.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as l;
|
||||
import 'package:icons_plus/icons_plus.dart';
|
||||
import '../../../constant.dart';
|
||||
import '../widgets/set_time.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class AddNewShift extends ConsumerStatefulWidget {
|
||||
const AddNewShift({super.key, this.isEdit = false, this.shift});
|
||||
|
||||
final bool isEdit;
|
||||
final ShiftData? shift;
|
||||
|
||||
@override
|
||||
ConsumerState<AddNewShift> createState() => _AddNewShiftState();
|
||||
}
|
||||
|
||||
class _AddNewShiftState extends ConsumerState<AddNewShift> {
|
||||
final GlobalKey<FormState> _key = GlobalKey();
|
||||
|
||||
// *** CHANGED: Shift Name is now managed by selectedShift String? ***
|
||||
String? selectedShift;
|
||||
// shiftNameController is now unnecessary for dropdown, but kept for cleanup clarity
|
||||
final TextEditingController shiftNameController = TextEditingController();
|
||||
|
||||
String? selectedBreakStatus;
|
||||
String? _selectedStatus;
|
||||
|
||||
final startTimeController = TextEditingController();
|
||||
final endTimeController = TextEditingController();
|
||||
final startBreakTimeController = TextEditingController();
|
||||
final endBreakTimeController = TextEditingController();
|
||||
|
||||
final List<String> _statusOptions = ['Active', 'Inactive'];
|
||||
final List<String> _shiftNameOptions = ['Morning', "Day", "Evening", 'Night']; // Fixed list for dropdown
|
||||
|
||||
String formatTime(String time24) {
|
||||
try {
|
||||
DateTime parsedTime = DateFormat("HH:mm:ss").parse(time24);
|
||||
return DateFormat("h:mm a").format(parsedTime);
|
||||
} catch (e) {
|
||||
return time24;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.isEdit && widget.shift != null) {
|
||||
final data = widget.shift!;
|
||||
|
||||
// *** FIX: Use Shift Name from data for selectedShift state ***
|
||||
selectedShift = data.name;
|
||||
|
||||
selectedBreakStatus = data.breakStatus == 'yes' ? "Yes" : "No";
|
||||
_selectedStatus = data.status == 1 ? 'Active' : 'Inactive';
|
||||
|
||||
startTimeController.text = formatTime(data.startTime ?? '');
|
||||
endTimeController.text = formatTime(data.endTime ?? '');
|
||||
startBreakTimeController.text = formatTime(data.startBreakTime ?? '');
|
||||
endBreakTimeController.text = formatTime(data.endBreakTime ?? '');
|
||||
} else {
|
||||
_selectedStatus = 'Active';
|
||||
selectedBreakStatus = 'No';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// shiftNameController.dispose(); // No longer needed if using dropdown
|
||||
startTimeController.dispose();
|
||||
endTimeController.dispose();
|
||||
startBreakTimeController.dispose();
|
||||
endBreakTimeController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _saveOrUpdateShift(BuildContext context) async {
|
||||
if (!_key.currentState!.validate()) return;
|
||||
|
||||
// Check if the required state variables are set by the dropdowns
|
||||
if (selectedShift == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Please select Shift Name')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final repo = ShiftRepo();
|
||||
final int apiStatus = _selectedStatus == 'Active' ? 1 : 0;
|
||||
|
||||
// Ensure times are in HH:mm format before sending (setTime uses HH:mm a,
|
||||
// but the repo needs to handle conversion to HH:mm:ss if necessary)
|
||||
|
||||
if (widget.isEdit) {
|
||||
await repo.updateShift(
|
||||
ref: ref,
|
||||
context: context,
|
||||
id: widget.shift!.id!.round(),
|
||||
shiftName: selectedShift!,
|
||||
breakStatus: selectedBreakStatus!,
|
||||
startTime: startTimeController.text,
|
||||
endTime: endTimeController.text,
|
||||
breakStartTime: startBreakTimeController.text.isEmpty ? null : startBreakTimeController.text,
|
||||
breakEndTime: endBreakTimeController.text.isEmpty ? null : endBreakTimeController.text,
|
||||
status: apiStatus.toString(),
|
||||
);
|
||||
} else {
|
||||
await repo.createShift(
|
||||
ref: ref,
|
||||
context: context,
|
||||
shiftName: selectedShift!,
|
||||
breakStatus: selectedBreakStatus ?? "No",
|
||||
startTime: startTimeController.text,
|
||||
endTime: endTimeController.text,
|
||||
breakStartTime: startBreakTimeController.text.isEmpty ? null : startBreakTimeController.text,
|
||||
breakEndTime: endBreakTimeController.text.isEmpty ? null : endBreakTimeController.text,
|
||||
status: apiStatus.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _resetForm() {
|
||||
_key.currentState?.reset();
|
||||
setState(() {
|
||||
selectedShift = null; // Reset dropdown selection
|
||||
selectedBreakStatus = 'No';
|
||||
_selectedStatus = 'Active';
|
||||
|
||||
// Reset time controllers
|
||||
startTimeController.clear();
|
||||
endTimeController.clear();
|
||||
startBreakTimeController.clear();
|
||||
endBreakTimeController.clear();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _lang = l.S.of(context);
|
||||
return Scaffold(
|
||||
backgroundColor: kWhite,
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
widget.isEdit ? _lang.editShift : _lang.addNewShift,
|
||||
),
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(1),
|
||||
child: Divider(height: 2, color: kBackgroundColor),
|
||||
),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Form(
|
||||
key: _key,
|
||||
child: Column(
|
||||
children: [
|
||||
// 1. Shift Name (FIXED: Changed back to Dropdown)
|
||||
DropdownButtonFormField<String>(
|
||||
value: selectedShift,
|
||||
icon: const Icon(Icons.keyboard_arrow_down, color: kNeutral800),
|
||||
decoration: InputDecoration(labelText: _lang.shiftName, hintText: _lang.selectOne),
|
||||
items: _shiftNameOptions
|
||||
.map((String value) => DropdownMenuItem(value: value, child: Text(value)))
|
||||
.toList(),
|
||||
onChanged: (String? newValue) => setState(() => selectedShift = newValue),
|
||||
validator: (value) => value == null ? _lang.pleaseSelectAShift : null,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 2. Break Status Dropdown
|
||||
DropdownButtonFormField<String>(
|
||||
value: selectedBreakStatus,
|
||||
icon: const Icon(Icons.keyboard_arrow_down, color: kNeutral800),
|
||||
decoration: InputDecoration(
|
||||
labelText: _lang.breakStatus,
|
||||
hintText: _lang.selectOne,
|
||||
),
|
||||
items: const ['Yes', 'No']
|
||||
.map((String value) => DropdownMenuItem(value: value, child: Text(value)))
|
||||
.toList(),
|
||||
onChanged: (String? newValue) => setState(() => selectedBreakStatus = newValue),
|
||||
validator: (value) => value == null ? _lang.pleaseSelectBreakStatus : null,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 3. Status Dropdown
|
||||
DropdownButtonFormField<String>(
|
||||
value: _selectedStatus,
|
||||
icon: const Icon(Icons.keyboard_arrow_down, color: kNeutral800),
|
||||
decoration: InputDecoration(
|
||||
labelText: _lang.status,
|
||||
hintText: _lang.selectOne,
|
||||
),
|
||||
items:
|
||||
_statusOptions.map((String value) => DropdownMenuItem(value: value, child: Text(value))).toList(),
|
||||
onChanged: (String? newValue) => setState(() => _selectedStatus = newValue),
|
||||
validator: (value) => value == null ? _lang.pleaseSelectAStatus : null,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 4. Start Time & End Time
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
onTap: () => setTime(startTimeController, context),
|
||||
readOnly: true,
|
||||
controller: startTimeController,
|
||||
validator: (value) => value.isNullOrEmpty() ? _lang.startTimeIsRequired : null,
|
||||
decoration: InputDecoration(
|
||||
labelText: _lang.startTime,
|
||||
hintText: _lang.enterStartTime,
|
||||
suffixIcon: Icon(
|
||||
AntDesign.clock_circle_outline,
|
||||
size: 18,
|
||||
color: kNeutral800,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
readOnly: true,
|
||||
controller: endTimeController,
|
||||
onTap: () => setTime(endTimeController, context),
|
||||
validator: (value) => value.isNullOrEmpty() ? _lang.endTimeIsRequired : null,
|
||||
decoration: InputDecoration(
|
||||
labelText: _lang.endTime,
|
||||
hintText: _lang.enterEndTime,
|
||||
suffixIcon: Icon(
|
||||
AntDesign.clock_circle_outline,
|
||||
size: 18,
|
||||
color: kNeutral800,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 5. Break Time (Conditional)
|
||||
if (selectedBreakStatus == 'Yes')
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
onTap: () => setTime(startBreakTimeController, context),
|
||||
readOnly: true,
|
||||
controller: startBreakTimeController,
|
||||
decoration: InputDecoration(
|
||||
labelText: _lang.startBreakTime,
|
||||
hintText: _lang.enterBreakTime,
|
||||
suffixIcon: Icon(
|
||||
AntDesign.clock_circle_outline,
|
||||
size: 18,
|
||||
color: kNeutral800,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
readOnly: true,
|
||||
controller: endBreakTimeController,
|
||||
onTap: () => setTime(endBreakTimeController, context),
|
||||
decoration: InputDecoration(
|
||||
labelText: _lang.endBreakTime,
|
||||
hintText: _lang.enterBreakTime,
|
||||
suffixIcon: Icon(
|
||||
AntDesign.clock_circle_outline,
|
||||
size: 18,
|
||||
color: kNeutral800,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 6. Action Buttons
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(onPressed: _resetForm, child: Text(_lang.resets)),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () => _saveOrUpdateShift(context),
|
||||
child: Text(widget.isEdit ? _lang.update : _lang.save)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Extension to help with simple validation checks
|
||||
extension on String? {
|
||||
bool isNullOrEmpty() => this == null || this!.isEmpty;
|
||||
}
|
||||
8
lib/Screens/hrm/shift/provider/shift_list_provider.dart
Normal file
8
lib/Screens/hrm/shift/provider/shift_list_provider.dart
Normal 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/shift/Model/shift_list_model.dart';
|
||||
import 'package:mobile_pos/Screens/hrm/shift/repo/shift_repo.dart';
|
||||
|
||||
ShiftRepo repo = ShiftRepo();
|
||||
final shiftListProvider = FutureProvider<ShiftListModel>((ref) => repo.fetchAllShifts());
|
||||
185
lib/Screens/hrm/shift/repo/shift_repo.dart
Normal file
185
lib/Screens/hrm/shift/repo/shift_repo.dart
Normal file
@@ -0,0 +1,185 @@
|
||||
// ignore_for_file: use_build_context_synchronously
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:mobile_pos/Screens/hrm/shift/Model/shift_list_model.dart';
|
||||
import 'package:mobile_pos/Screens/hrm/shift/provider/shift_list_provider.dart';
|
||||
import '../../../../Const/api_config.dart';
|
||||
import '../../../../http_client/custome_http_client.dart';
|
||||
import '../../../../http_client/customer_http_client_get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class ShiftRepo {
|
||||
String convertTo24HourFormat(String time12h) {
|
||||
// Example input: "8:00 PM"
|
||||
final dateTime = DateFormat('h:mm a').parse(time12h);
|
||||
return DateFormat('HH:mm').format(dateTime); // Output: "20:00"
|
||||
}
|
||||
|
||||
/// Fetch all shifts
|
||||
Future<ShiftListModel> fetchAllShifts() async {
|
||||
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
|
||||
final uri = Uri.parse('${APIConfig.url}/shifts');
|
||||
|
||||
final response = await clientGet.get(url: uri);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final parsedData = jsonDecode(response.body);
|
||||
return ShiftListModel.fromJson(parsedData);
|
||||
} else {
|
||||
throw Exception('Failed to fetch Shift list');
|
||||
}
|
||||
}
|
||||
|
||||
/// Create new shift (form-data format)
|
||||
Future<void> createShift({
|
||||
required WidgetRef ref,
|
||||
required BuildContext context,
|
||||
required String shiftName,
|
||||
required String breakStatus,
|
||||
required String startTime,
|
||||
required String endTime,
|
||||
required String status,
|
||||
String? breakStartTime,
|
||||
String? breakEndTime,
|
||||
}) async {
|
||||
final uri = Uri.parse('${APIConfig.url}/shifts');
|
||||
|
||||
// Build form-data map (Postman style)
|
||||
final Map<String, String> body = {
|
||||
'name': shiftName,
|
||||
'start_time': convertTo24HourFormat(startTime),
|
||||
'status': status,
|
||||
'end_time': convertTo24HourFormat(endTime),
|
||||
'break_status': breakStatus.toLowerCase(), // yes/no
|
||||
if (breakStartTime != null && breakStartTime.isNotEmpty) 'start_break_time': breakStatus.toLowerCase() == 'no' ? '' : convertTo24HourFormat(breakStartTime),
|
||||
if (breakEndTime != null && breakEndTime.isNotEmpty) 'end_break_time': breakStatus.toLowerCase() == 'no' ? '' : convertTo24HourFormat(breakEndTime),
|
||||
};
|
||||
|
||||
try {
|
||||
EasyLoading.show(status: 'Saving...');
|
||||
CustomHttpClient client = CustomHttpClient(client: http.Client(), context: context, ref: ref);
|
||||
|
||||
print("POST Data For: $body");
|
||||
|
||||
final response = await client.post(
|
||||
url: uri,
|
||||
body: body, // form-data
|
||||
addContentTypeInHeader: false, // important for form-data
|
||||
);
|
||||
|
||||
EasyLoading.dismiss();
|
||||
|
||||
final parsed = jsonDecode(response.body);
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Shift created successfully!')),
|
||||
);
|
||||
ref.refresh(shiftListProvider);
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Failed to create shift: ${parsed['message']}')),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
EasyLoading.dismiss();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Error: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Update existing shift (form-data format)
|
||||
Future<void> updateShift({
|
||||
required WidgetRef ref,
|
||||
required BuildContext context,
|
||||
required int id,
|
||||
required String shiftName,
|
||||
required String breakStatus,
|
||||
required String startTime,
|
||||
required String endTime,
|
||||
required String status,
|
||||
String? breakStartTime,
|
||||
String? breakEndTime,
|
||||
}) async {
|
||||
final uri = Uri.parse('${APIConfig.url}/shifts/$id');
|
||||
|
||||
final Map<String, String> body = {
|
||||
"_method": 'put',
|
||||
'name': shiftName,
|
||||
'status': status,
|
||||
'start_time': convertTo24HourFormat(startTime),
|
||||
'end_time': convertTo24HourFormat(endTime),
|
||||
'break_status': breakStatus.toLowerCase(),
|
||||
if (breakStartTime != null && breakStartTime.isNotEmpty) 'start_break_time': breakStatus.toLowerCase() == 'no' ? '' : convertTo24HourFormat(breakStartTime),
|
||||
if (breakEndTime != null && breakEndTime.isNotEmpty) 'end_break_time': breakStatus.toLowerCase() == 'no' ? '' : convertTo24HourFormat(breakEndTime),
|
||||
};
|
||||
|
||||
try {
|
||||
EasyLoading.show(status: 'Updating...');
|
||||
CustomHttpClient client = CustomHttpClient(client: http.Client(), context: context, ref: ref);
|
||||
|
||||
final response = await client.post(
|
||||
url: uri,
|
||||
body: body,
|
||||
addContentTypeInHeader: false, // still form-data
|
||||
);
|
||||
|
||||
EasyLoading.dismiss();
|
||||
|
||||
final parsed = jsonDecode(response.body);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Shift updated successfully!')),
|
||||
);
|
||||
ref.refresh(shiftListProvider);
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Failed to update shift: ${parsed['message']}')),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
EasyLoading.dismiss();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Error: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete shift
|
||||
Future<bool> deleteShift({
|
||||
required WidgetRef ref,
|
||||
required BuildContext context,
|
||||
required int id,
|
||||
}) async {
|
||||
final uri = Uri.parse('${APIConfig.url}/shifts/$id');
|
||||
|
||||
try {
|
||||
EasyLoading.show(status: 'Deleting...');
|
||||
CustomHttpClient client = CustomHttpClient(client: http.Client(), context: context, ref: ref);
|
||||
final response = await client.delete(url: uri);
|
||||
|
||||
EasyLoading.dismiss();
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return true;
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Failed to delete shift: ${response.body}')),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
EasyLoading.dismiss();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Error: $e')),
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
287
lib/Screens/hrm/shift/shift_screen.dart
Normal file
287
lib/Screens/hrm/shift/shift_screen.dart
Normal file
@@ -0,0 +1,287 @@
|
||||
// ignore_for_file: use_build_context_synchronously
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:hugeicons/hugeicons.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:mobile_pos/Screens/hrm/shift/add_new_shift.dart';
|
||||
import 'package:mobile_pos/Screens/hrm/shift/provider/shift_list_provider.dart';
|
||||
import 'package:mobile_pos/Screens/hrm/shift/repo/shift_repo.dart';
|
||||
import 'package:mobile_pos/Screens/hrm/shift/Model/shift_list_model.dart';
|
||||
import 'package:mobile_pos/Screens/hrm/widgets/global_search_appbar.dart';
|
||||
import 'package:mobile_pos/Screens/hrm/widgets/model_bottom_sheet.dart';
|
||||
import 'package:mobile_pos/Screens/hrm/widgets/deleteing_alart_dialog.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
|
||||
import '../../../generated/l10n.dart' as lang;
|
||||
import '../../../service/check_user_role_permission_provider.dart';
|
||||
import '../../../widgets/empty_widget/_empty_widget.dart';
|
||||
|
||||
class ShiftScreen extends ConsumerStatefulWidget {
|
||||
const ShiftScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ShiftScreen> createState() => _ShiftScreenState();
|
||||
}
|
||||
|
||||
class _ShiftScreenState extends ConsumerState<ShiftScreen> {
|
||||
bool _isSearch = false;
|
||||
final _searchController = TextEditingController();
|
||||
String _searchQuery = '';
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _refreshList() async {
|
||||
ref.refresh(shiftListProvider);
|
||||
}
|
||||
|
||||
/// ✅ Convert "HH:mm" to "hh:mm a" format safely
|
||||
String _formatToAmPm(String? time) {
|
||||
if (time == null || time.isEmpty || !time.contains(':')) return 'n/a';
|
||||
try {
|
||||
final date = DateFormat("HH:mm").parse(time);
|
||||
return DateFormat("hh:mm a").format(date);
|
||||
} catch (_) {
|
||||
return time; // fallback
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final asyncShifts = ref.watch(shiftListProvider);
|
||||
final permissionService = PermissionService(ref);
|
||||
final _lang = lang.S.of(context);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: GlobalSearchAppBar(
|
||||
isSearch: _isSearch,
|
||||
onSearchToggle: () => setState(() {
|
||||
_isSearch = !_isSearch;
|
||||
_searchQuery = '';
|
||||
_searchController.clear();
|
||||
}),
|
||||
title: _lang.shift,
|
||||
controller: _searchController,
|
||||
onChanged: (query) {
|
||||
setState(() {
|
||||
_searchQuery = query.toLowerCase().trim();
|
||||
});
|
||||
},
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: _refreshList,
|
||||
child: asyncShifts.when(
|
||||
data: (shiftList) {
|
||||
if (!permissionService.hasPermission(Permit.shiftsRead.value)) {
|
||||
return const Center(child: PermitDenyWidget());
|
||||
}
|
||||
final allShifts = shiftList.data ?? [];
|
||||
|
||||
// ✅ Apply search filter
|
||||
final shifts = allShifts.where((shift) {
|
||||
final name = (shift.name ?? '').toLowerCase();
|
||||
return name.contains(_searchQuery);
|
||||
}).toList();
|
||||
|
||||
if (shifts.isEmpty) {
|
||||
return Center(child: Text(_lang.noShiftFound));
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: shifts.length,
|
||||
separatorBuilder: (_, __) => const Divider(
|
||||
color: kBackgroundColor,
|
||||
height: 2,
|
||||
),
|
||||
itemBuilder: (_, index) {
|
||||
final shift = shifts[index];
|
||||
return _buildShiftItem(context, shift);
|
||||
},
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (e, _) => Center(child: Text('Error: $e')),
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: permissionService.hasPermission(Permit.shiftsCreate.value)
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const AddNewShift(isEdit: false),
|
||||
),
|
||||
),
|
||||
icon: const Icon(Icons.add, color: Colors.white),
|
||||
label: Text(_lang.addShift),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildShiftItem(BuildContext context, ShiftData shift) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return InkWell(
|
||||
onTap: () => viewModalSheet(
|
||||
context: context,
|
||||
item: {
|
||||
lang.S.of(context).shift: shift.name ?? 'n/a',
|
||||
lang.S.of(context).startTime: _formatToAmPm(shift.startTime),
|
||||
lang.S.of(context).endTime: _formatToAmPm(shift.endTime),
|
||||
lang.S.of(context).breakTime: (shift.startBreakTime == null ||
|
||||
shift.startBreakTime!.isEmpty ||
|
||||
shift.endBreakTime == null ||
|
||||
shift.endBreakTime!.isEmpty)
|
||||
? 'N/A'
|
||||
: "${_formatToAmPm(shift.startBreakTime)} - ${_formatToAmPm(shift.endBreakTime)}",
|
||||
lang.S.of(context).breakDuration: shift.breakTime?.isEmpty ?? true ? 'N/A' : shift.breakTime!,
|
||||
lang.S.of(context).status: shift.status == 1 ? lang.S.of(context).active : lang.S.of(context).inactive,
|
||||
},
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13.5),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
shift.name ?? 'n/a',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
if (shift.breakStatus == 'yes')
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
text: '${lang.S.of(context).breakTime}: ',
|
||||
style: const TextStyle(color: kNeutral800),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '${_formatToAmPm(shift.startBreakTime)} - ${_formatToAmPm(shift.endBreakTime)}',
|
||||
style: const TextStyle(color: kTitleColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: 20.5),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildTimeColumn(_formatToAmPm(shift.startTime), lang.S.of(context).startTime, theme),
|
||||
_buildTimeColumn(_formatToAmPm(shift.endTime), lang.S.of(context).endTime, theme),
|
||||
_buildActionButtons(context, shift),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTimeColumn(String time, String label, ThemeData theme) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
time,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
label,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(color: kNeutral800),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButtons(BuildContext context, shift) {
|
||||
final _lang = lang.S.of(context);
|
||||
final permissionService = PermissionService(ref);
|
||||
return Column(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
if (!permissionService.hasPermission(Permit.shiftsUpdate.value)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: Colors.red,
|
||||
content: Text(_lang.youDoNotToHavePermissionToUpdateShift),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => AddNewShift(
|
||||
isEdit: true,
|
||||
shift: shift,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const HugeIcon(
|
||||
icon: HugeIcons.strokeRoundedPencilEdit02,
|
||||
color: kSuccessColor,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
if (!permissionService.hasPermission(Permit.shiftsDelete.value)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: Colors.red,
|
||||
content: Text(lang.S.of(context).youDoNotToHavePermissionToDeleteShift),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final _lang = lang.S.of(context);
|
||||
final confirm = await showDeleteConfirmationDialog(
|
||||
itemName: _lang.shift,
|
||||
context: context,
|
||||
);
|
||||
|
||||
if (confirm) {
|
||||
EasyLoading.show(status: _lang.deleting);
|
||||
final repo = ShiftRepo();
|
||||
try {
|
||||
final result = await repo.deleteShift(id: shift.id, ref: ref, context: context);
|
||||
if (result) {
|
||||
ref.refresh(shiftListProvider);
|
||||
EasyLoading.showSuccess(_lang.deletedSuccessFully);
|
||||
} else {
|
||||
EasyLoading.showError('Failed to delete the Shift');
|
||||
}
|
||||
} catch (e) {
|
||||
EasyLoading.showError('Error: $e');
|
||||
} finally {
|
||||
EasyLoading.dismiss();
|
||||
}
|
||||
}
|
||||
},
|
||||
child: const HugeIcon(
|
||||
icon: HugeIcons.strokeRoundedDelete03,
|
||||
color: Colors.red,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user