Explorar el Código

update statemanagement: history detil masih eror

Yulian hace 1 mes
padre
commit
9197045e72

+ 24 - 17
lib/main.dart

@@ -9,6 +9,7 @@ import 'package:firebase_messaging/firebase_messaging.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:flutter_local_notifications/flutter_local_notifications.dart';
 // import 'package:flutter_native_splash/flutter_native_splash.dart';
 import 'package:fluttertoast/fluttertoast.dart';
@@ -17,6 +18,7 @@ import 'package:provider/provider.dart';
 import 'package:quick_notify_2/quick_notify.dart';
 import 'package:shared_preferences/shared_preferences.dart';
 import 'package:telnow_mobile_new/src/api/api_auth_provider.dart';
+import 'package:telnow_mobile_new/src/cubit/user_data_cubit.dart';
 import 'package:telnow_mobile_new/src/injector/injector.dart';
 import 'package:telnow_mobile_new/src/layouts/mobile/history_forum.dart';
 import 'package:telnow_mobile_new/src/layouts/mobile/message_list.dart';
@@ -119,23 +121,28 @@ class MyApp extends StatelessWidget {
         ChangeNotifierProvider(create: (_) => CreateSerModule()),
         ChangeNotifierProvider(create: (_) => MessageModule()),
       ],
-      child: MaterialApp.router(
-        // routerDelegate: _appRouter.delegate(),
-        // routeInformationParser: _appRouter.defaultRouteParser(),
-        scrollBehavior: MyCustomScrollBehavior(),
-        debugShowCheckedModeBanner: false,
-        localizationsDelegates: context.localizationDelegates,
-        supportedLocales: context.supportedLocales,
-        locale: context.locale,
-        key: NavigationService.navigatorKey,
-        theme: ThemeData(useMaterial3: false, fontFamily: 'SF Compact Display', appBarTheme: AppBarTheme(
-          systemOverlayStyle: SystemUiOverlayStyle(
-              statusBarColor: Colors.transparent,
-              statusBarIconBrightness: Brightness.dark,
-              statusBarBrightness: Brightness.dark
-          ),
-        ), colorScheme: ColorScheme.fromSeed(seedColor: Color(0xff078C84))),
-        routerConfig: _appRouter.config(),
+      child: MultiBlocProvider(
+        providers: [
+          BlocProvider(create: (_) => UserCubit()),
+        ],
+        child: MaterialApp.router(
+          // routerDelegate: _appRouter.delegate(),
+          // routeInformationParser: _appRouter.defaultRouteParser(),
+          scrollBehavior: MyCustomScrollBehavior(),
+          debugShowCheckedModeBanner: false,
+          localizationsDelegates: context.localizationDelegates,
+          supportedLocales: context.supportedLocales,
+          locale: context.locale,
+          key: NavigationService.navigatorKey,
+          theme: ThemeData(useMaterial3: false, fontFamily: 'SF Compact Display', appBarTheme: AppBarTheme(
+            systemOverlayStyle: SystemUiOverlayStyle(
+                statusBarColor: Colors.transparent,
+                statusBarIconBrightness: Brightness.dark,
+                statusBarBrightness: Brightness.dark
+            ),
+          ), colorScheme: ColorScheme.fromSeed(seedColor: Color(0xff078C84))),
+          routerConfig: _appRouter.config(),
+        ),
       ),
     );
   }

+ 0 - 44
lib/src/cubit/history_cubit.dart

@@ -1,44 +0,0 @@
-import 'package:equatable/equatable.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
-
-import '../api/api_auth_provider.dart';
-
-class HistoryState extends Equatable {
-  final List<dynamic> data;
-  final String keyword;
-  final int page;
-  final bool isLoading;
-
-  const HistoryState({
-    this.data = const [],
-    this.keyword = '',
-    this.page = 0,
-    this.isLoading = true});
-
-  HistoryState copyWith({
-    List<dynamic>? data,
-    String? keyword,
-    int? page,
-    bool? isLoading,
-  }){
-    return HistoryState(
-      data: data ?? this.data,
-      keyword: keyword ?? this.keyword,
-      page: page ?? this.page,
-      isLoading: isLoading ?? this.isLoading,
-    );
-  }
-  @override
-  List<Object?> get props => [data, keyword, page, isLoading];
-}
-
-class HistoryCubit extends Cubit<HistoryState> {
-  HistoryCubit() : super(const HistoryState());
-  final ApiAuthProvider _apiAuthProvider = ApiAuthProvider();
-
-  void getData({String keyword = '', int page = 0}) async {
-    String filter = keyword == '' ? '{"f":["1","EQ","1"]}' : '{"or":[{"f":["requestNote","LIKE","%$keyword%"]},{"f":["requestSubject","LIKE","%$keyword%"]}]}';
-    // Lanjut nanti setelah fitur asset dah jadi! 5.3.26
-  }
-
-}

+ 174 - 0
lib/src/cubit/menu_history_cubit.dart

@@ -0,0 +1,174 @@
+import 'dart:convert';
+
+import 'package:easy_localization/easy_localization.dart';
+import 'package:equatable/equatable.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import '../repository/history_repository.dart';
+import '../storage/sharedpreferences/shared_preferences_manager.dart';
+import '../utils/U.dart';
+import '../utils/cache_manager.dart';
+
+enum HistoryTab {
+  ongoing,
+  done;
+
+  List get filters {
+    switch(this){
+      case HistoryTab.ongoing:
+        return [
+          {'title': 'stateQueue'.tr(), 'value': 'queue', 'filter': '{"or":[{"f":["currentState","EQ","DIANTRIKAN"]},{"f":["currentState","EQ","DIPROSES"]}]}'},
+          {'title': 'stateDone'.tr(), 'value': 'done', 'filter': '{"f":["currentState","EQ","DIMULAI"]}'},
+          {'title': 'hold'.tr(), 'value': 'hold', 'filter': '{"f":["currentState","EQ","HOLD"]}'}
+        ];
+      case HistoryTab.done:
+        return [
+          {'title': 'stateFinish'.tr(), 'value': 'finish', 'filter': '{"or":[{"f":["currentState","EQ","DISELESAIKAN"]},{"f":["currentState","EQ","TUNTAS"]}]}'},
+          {'title': 'stateCancel'.tr(), 'value': 'cancel', 'filter': '{"f":["currentState","EQ","DIBATALKAN"]}'}
+        ];
+    }
+  }
+}
+
+class MenuHistoryState extends Equatable {
+  final HistoryTab activeTab;
+  final List<dynamic> pendingData;
+  final List activeForum;
+  final bool multiSelectMode;
+  final List<int> selectedIndex;
+
+  const MenuHistoryState({
+    this.activeTab = HistoryTab.ongoing,
+    this.pendingData = const [],
+    this.activeForum = const [],
+    this.multiSelectMode = false,
+    this.selectedIndex = const [],
+  });
+
+  MenuHistoryState copyWith({
+    HistoryTab? activeTab,
+    List<dynamic>? pendingData,
+    List? activeForum,
+    bool? multiSelectMode,
+    List<int>? selectedIndex,
+  }){
+    return MenuHistoryState(
+      activeTab: activeTab ?? this.activeTab,
+      pendingData: pendingData ?? this.pendingData,
+      activeForum: activeForum ?? this.activeForum,
+      multiSelectMode: multiSelectMode ?? this.multiSelectMode,
+      selectedIndex: selectedIndex ?? this.selectedIndex,
+    );
+  }
+
+  @override
+  List<Object?> get props => [activeTab, pendingData, activeForum, multiSelectMode, selectedIndex];
+}
+
+class ReloadPage extends MenuHistoryState {}
+
+class CreateNewRequest extends MenuHistoryState {
+  final Map<String,dynamic> request;
+  const CreateNewRequest(this.request);
+}
+
+class ShowError extends MenuHistoryState {
+  final String error;
+  const ShowError(this.error);
+}
+
+class MenuHistoryCubit extends Cubit<MenuHistoryState> {
+  MenuHistoryCubit() : super(MenuHistoryState());
+  final SharedPreferencesManager sharedPreferencesManager = SharedPreferencesManager();
+
+  void init() {
+    setActiveTab(HistoryTab.ongoing);
+    getActiveForum();
+    getPendingData();
+  }
+
+  void getPendingData() async {
+    if(!U.getInternetStatus()){
+      List pendingList = sharedPreferencesManager.isKeyExists(SharedPreferencesManager.keyPendingData)!?jsonDecode(sharedPreferencesManager.getString(SharedPreferencesManager.keyPendingData)!):[];
+      setPendingData(pendingList);
+    }
+  }
+
+  void setPendingData(List<dynamic> data){
+    emit(state.copyWith(pendingData: data));
+  }
+
+  void setActiveTab(HistoryTab tab){
+    emit(state.copyWith(activeTab: tab));
+  }
+
+  Future<void> getActiveForum() async {
+    String url = '/api/notifications/search/forumNotification';
+    var forum = [];
+
+    // # check cache
+    var cacheData = await CacheMan.readData(url);
+    if(cacheData?['data'] != null){
+      emit(state.copyWith(activeForum: cacheData['data']));
+    }
+
+    forum = await HistoryRepository().getActiveForum(url);
+    emit(state.copyWith(activeForum: forum));
+  }
+
+  void setMultiSelectMode(bool val){
+    emit(state.copyWith(multiSelectMode: val));
+  }
+
+  void selectIndex(int index){
+    final updated = List<int>.from(state.selectedIndex);
+
+    if(updated.contains(index)) {
+      updated.remove(index);
+    } else {
+      updated.add(index);
+    }
+
+    if(updated.isEmpty){
+      emit(state.copyWith(selectedIndex: [], multiSelectMode: false));
+    } else {
+      emit(state.copyWith(selectedIndex: updated, multiSelectMode: true));
+    }
+  }
+
+  void deleteData(List<dynamic> data){
+    List<String> params = [];
+
+    for (var e in state.selectedIndex) {
+      params.add(data[e]["ticketNo"]);
+    }
+
+    var res = HistoryRepository().deleteData(params);
+    if (res != null) {
+      emit(ReloadPage());
+    }
+  }
+
+  Future<void> requestAgain(Map<String, dynamic> data) async {
+    try {
+      String tenant = '';
+      String filter = '';
+
+      if ((data['requestGroupCode']).split(" ").length != 1) {
+        tenant = ',{"f":["tenantCode","EQ","${(data['requestGroupCode']).split(" ").first}"]}';
+      }
+      filter = '{"and":[{"f":["code","EQ","${data['requestCode']}"]}$tenant]}';
+
+      var res = await HistoryRepository().checkRequest(filter);
+      if (res.isNotEmpty) {
+        if (res.containsKey('_embedded')) {
+          emit(CreateNewRequest(res['_embedded']['requests'][0]));
+        } else {
+          emit(ShowError('reqCodeNotFound'.tr()));
+        }
+      }
+    } catch (e) {
+      emit(ShowError("Err: ${e.toString()}"));
+    }
+  }
+}

+ 220 - 0
lib/src/cubit/tab_history_cubit.dart

@@ -0,0 +1,220 @@
+import 'package:equatable/equatable.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import '../repository/history_repository.dart';
+import '../utils/cache_manager.dart';
+
+class TabState extends Equatable {
+  final List data;
+  final bool isLoading;
+  final bool stopLoad;
+  final int page;
+  final String filter;
+  final List<dynamic> activeForum;
+  final bool reload;
+
+  const TabState({
+    this.data = const [],
+    this.isLoading = false,
+    this.stopLoad = false,
+    this.page = 0,
+    this.filter = 'none',
+    this.activeForum = const [],
+    this.reload = false
+  });
+
+  TabState copyWith({
+    List? data,
+    bool? isLoading,
+    bool? stopLoad,
+    int? page,
+    String? filter,
+    List? activeForum,
+    bool? reload
+  }) {
+    return TabState(
+      data: data ?? this.data,
+      isLoading: isLoading ?? this.isLoading,
+      stopLoad: stopLoad ?? this.stopLoad,
+      page: page ?? this.page,
+      filter: filter ?? this.filter,
+      activeForum: activeForum ?? this.activeForum,
+      reload: reload ?? this.reload,
+    );
+  }
+
+  @override
+  List<Object?> get props => [data, isLoading, stopLoad, page, filter, activeForum, reload];
+}
+
+class ReloadContainer extends TabState {}
+
+// # Cubit for Ongoing Tab
+class OngoingCubit extends Cubit<TabState> {
+  final HistoryRepository historyRepo = HistoryRepository();
+
+  OngoingCubit() : super(const TabState());
+
+  Future<void> loadOngoing() async {
+    if(state.stopLoad) return;
+
+    if(state.data.isNotEmpty){
+      emit(state.copyWith(isLoading: false));
+    }
+
+    if(state.page == 0) emit(state.copyWith(isLoading: true));
+
+    String filter = '';
+    int page = 0;
+    int size = 30;
+    bool stopLoad = false;
+
+    switch(state.filter){
+      case 'none':
+        filter = '{"or":[{"f":["currentState","EQ","DIANTRIKAN"]},{"f":["currentState","EQ","DIPROSES"]},{"f":["currentState","EQ","DIMULAI"]}]}';
+        break;
+      case 'queue':
+        filter = '{"or":[{"f":["currentState","EQ","DIANTRIKAN"]},{"f":["currentState","EQ","DIPROSES"]}]}';
+        break;
+      case 'done':
+        filter = '{"f":["currentState","EQ","DIMULAI"]}';
+        break;
+      case 'hold':
+        filter = '{"f":["currentState","EQ","HOLD"]}';
+        break;
+      default:
+        filter = '{"or":[{"f":["currentState","EQ","DIANTRIKAN"]},{"f":["currentState","EQ","DIPROSES"]},{"f":["currentState","EQ","DIMULAI"]}]}';
+    }
+
+    if(state.page != 0) page = state.page;
+
+    // cache manager
+    String key = '#ongoing#$filter';
+    var val = await CacheMan.readData(key);
+    if(val != null && page == 0){
+      emit(state.copyWith(data: val['data'], isLoading: false, stopLoad: stopLoad));
+    }
+
+    try {
+      List tempData = List<dynamic>.from(state.data);
+
+      final result = await historyRepo.getData(key, filter, page, size, state.activeForum);
+      if(result.length < size) stopLoad = true;
+
+      if(state.page > 0){
+        for (var element in result) {
+          tempData.add(element);
+        }
+      } else {
+        tempData = result;
+      }
+
+      emit(state.copyWith(data: tempData, isLoading: false, stopLoad: stopLoad));
+    } catch (e) {
+      emit(state.copyWith(data: [], isLoading: false));
+    }
+  }
+
+  void setFilter(currentFilter) {
+    emit(state.copyWith(data: [], filter: currentFilter, page: 0, stopLoad: false));
+    loadOngoing();
+  }
+
+  void loadNextPage() {
+    emit(state.copyWith(page: state.page + 1));
+    loadOngoing();
+  }
+}
+
+// # Cubit for Done Tab
+class DoneCubit extends Cubit<TabState> {
+  final HistoryRepository historyRepo = HistoryRepository();
+
+  DoneCubit() : super(const TabState());
+
+  Future<void> loadDone() async {
+    if(state.stopLoad || state.isLoading) return;
+    emit(state.copyWith(isLoading: true));
+
+    if(!state.reload) {
+      if (state.data.isNotEmpty) {
+        emit(state.copyWith(isLoading: false));
+      }
+
+      if (state.page == 0) emit(state.copyWith(isLoading: true));
+    }
+
+    String filter = '';
+    int page = 0;
+    int size = 30;
+    bool stopLoad = false;
+
+    switch(state.filter){
+      case 'none':
+        filter = '{"or":[{"f":["currentState","EQ","DISELESAIKAN"]},{"f":["currentState","EQ","TUNTAS"]},{"f":["currentState","EQ","DIBATALKAN"]}]}';
+        break;
+      case 'finish':
+        filter = '{"or":[{"f":["currentState","EQ","DISELESAIKAN"]},{"f":["currentState","EQ","TUNTAS"]}]}';
+        break;
+      case 'cancel':
+        filter = '{"f":["currentState","EQ","DIBATALKAN"]}';
+        break;
+      default:
+        filter = '{"or":[{"f":["currentState","EQ","DISELESAIKAN"]},{"f":["currentState","EQ","TUNTAS"]},{"f":["currentState","EQ","DIBATALKAN"]}]}';
+    }
+
+    if(state.page != 0) page = state.page;
+
+    // # cache manager
+    String key = '#done#$filter';
+    if(!state.reload) {
+      var val = await CacheMan.readData(key);
+      if (val != null && page == 0) {
+        emit(state.copyWith(
+            data: val['data'], isLoading: false, stopLoad: stopLoad));
+      }
+    }
+
+    try {
+      List tempData = List<dynamic>.from(state.data);
+
+      final result = await historyRepo.getData(key, filter, page, size, state.activeForum);
+
+      if(result.length < size) stopLoad = true;
+
+      if(state.page > 0){
+        for (var element in result) {
+          tempData.add(element);
+        }
+      } else {
+        tempData = result;
+      }
+
+      emit(state.copyWith(data: tempData, isLoading: false, stopLoad: stopLoad));
+    } catch (e) {
+      emit(state.copyWith(data: [], isLoading: false));
+    }
+  }
+
+  void setFilter(currentFilter) {
+    emit(state.copyWith(filter: currentFilter, page: 0, stopLoad: false));
+    loadDone();
+  }
+
+  void loadNextPage() {
+    emit(state.copyWith(page: state.page + 1));
+    loadDone();
+  }
+
+  void reloadPage(){
+    emit(state.copyWith(data: [], page: 0, stopLoad: false, isLoading: true, reload: true));
+  }
+
+  void setSatisfaction(int index, int tempRating) {
+    emit(state.copyWith(isLoading: true));
+    List tempData = List<dynamic>.from(state.data);
+    tempData[index]['satisfactionRate'] = tempRating;
+
+    emit(state.copyWith(data: tempData, isLoading: false));
+  }
+}

+ 101 - 0
lib/src/cubit/user_data_cubit.dart

@@ -0,0 +1,101 @@
+import 'package:equatable/equatable.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import '../api/jwt_token.dart';
+
+class UserState extends Equatable {
+  final String serialNumber;
+  final Map<String, dynamic> user;
+  final Map<String, dynamic> profile;
+  final bool reset;
+  final bool dnd;
+  final bool servantDisplay;
+  final bool houseKeeping;
+  final String text;
+
+  const UserState({
+    this.serialNumber = '',
+    this.user = const {},
+    this.profile = const {},
+    this.reset = false,
+    this.dnd = false,
+    this.servantDisplay = false,
+    this.houseKeeping = false,
+    this.text = '',
+  });
+
+  UserState copyWith({
+    String? serialNumber,
+    Map<String, dynamic>? user,
+    Map<String, dynamic>? profile,
+    bool? reset,
+    bool? dnd,
+    bool? servantDisplay,
+    bool? houseKeeping,
+    String? text,
+  }) {
+    return UserState(
+      serialNumber: serialNumber ?? this.serialNumber,
+      user: user ?? this.user,
+      profile: profile ?? this.profile,
+      reset: reset ?? this.reset,
+      dnd: dnd ?? this.dnd,
+      servantDisplay: servantDisplay ?? this.servantDisplay,
+      houseKeeping: houseKeeping ?? this.houseKeeping,
+      text: text ?? this.text,
+    );
+  }
+
+  @override
+  List<Object?> get props => [serialNumber, user, profile, reset, dnd, servantDisplay, houseKeeping, text];
+}
+
+class UserCubit extends Cubit<UserState> {
+  UserCubit() : super(const UserState());
+  final JwtToken token = JwtToken();
+
+  void setSerialNumber(String value) {
+    emit(state.copyWith(serialNumber: value));
+  }
+
+  void setUser(Map<String, dynamic> value) {
+    emit(state.copyWith(user: value));
+  }
+
+  void setProfile(Map<String, dynamic> value) {
+    emit(state.copyWith(profile: value));
+  }
+
+  void setReset(bool value) {
+    emit(state.copyWith(reset: value));
+  }
+
+  void setDnd(bool value) {
+    emit(state.copyWith(dnd: value));
+  }
+
+  void setServantDisplay(bool value) {
+    emit(state.copyWith(servantDisplay: value));
+  }
+
+  void setHouseKeeping(bool value) {
+    emit(state.copyWith(houseKeeping: value));
+  }
+
+  void setText(String value) {
+    emit(state.copyWith(text: value));
+  }
+
+  void getUser() async {
+    try {
+      var res = await token.getUserData();
+      if (res != null) {
+        setUser(res);
+      } else {
+        setReset(true);
+      }
+    } catch (e) {
+      setReset(true);
+    }
+  }
+}

+ 256 - 0
lib/src/layouts/components/rate_mission.dart

@@ -0,0 +1,256 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import 'package:telnow_mobile_new/src/layouts/components/template.dart';
+import 'package:telnow_mobile_new/src/utils/provider.dart';
+
+import '../../api/api_auth_provider.dart';
+import '../../cubit/tab_history_cubit.dart';
+import '../../utils/U.dart';
+import '../../utils/extensions.dart';
+
+class RateMission extends StatefulWidget {
+  final BuildContext parentContext;
+  final Map<String, dynamic> list;
+  final List<dynamic> rating;
+  final int index;
+  final TabState state;
+
+  const RateMission({
+    super.key,
+    required this.parentContext,
+    required this.list,
+    required this.index,
+    required this.rating,
+    required this.state,
+  });
+
+  @override
+  State<RateMission> createState() => _RateMissionState();
+}
+
+class _RateMissionState extends State<RateMission> {
+  int tempRating = 0;
+  String description = '';
+  String ratingAspect = '';
+  late TextEditingController controllerOptOther;
+  late List aspectList;
+
+  @override
+  void initState() {
+    super.initState();
+    controllerOptOther = TextEditingController();
+
+    String locale = widget.parentContext.locale.toString();
+    aspectList = widget.list['_ratingAspect${locale[0].toUpperCase()}${locale[1]}'] != null &&
+        widget.list['_ratingAspect${locale[0].toUpperCase()}${locale[1]}'].trim() != ''
+        ? widget.list['_ratingAspect${locale[0].toUpperCase()}${locale[1]}'].split(';')
+        : [];
+
+    if (aspectList.isNotEmpty) {
+      aspectList.add('OTHER_OPTION');
+    } else {
+      ratingAspect = 'OTHER_OPTION';
+    }
+  }
+
+  @override
+  void dispose() {
+    controllerOptOther.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    HistoryModule historyModule = Provider.of<HistoryModule>(context, listen: false);
+    return AlertDialog(
+      backgroundColor: Colors.transparent,
+      contentPadding: EdgeInsets.zero,
+      content: Container(
+        decoration: BoxDecoration(
+          color: secondaryColor,
+          borderRadius: BorderRadius.circular(25),
+        ),
+        child: Column(
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            // Bagian header
+            Padding(
+              padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
+              child: Column(
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: [
+                  Text(widget.list[U.langColumn(context, 'requestSubject')],
+                      style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w600)),
+                  const SizedBox(height: 4),
+                  Row(
+                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                    children: [
+                      Text('${'ticketNumber'.tr()}: ${widget.list['ticketNo']}',
+                          style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.w300)),
+                      Text(convertDate(widget.list['datetimeRequest'], context.locale.toString()),
+                          style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.w300)),
+                    ],
+                  ),
+                ],
+              ),
+            ),
+
+            // Bagian rating
+            Container(
+              padding: const EdgeInsets.fromLTRB(30, 15, 30, 15),
+              decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(25)),
+              child: Column(
+                children: [
+                  Text('satisfactionAsk'.tr(), style: const TextStyle(fontSize: 16)),
+                  const SizedBox(height: 12),
+                  Row(
+                    mainAxisAlignment: MainAxisAlignment.center,
+                    children: List.generate(widget.rating.length, (i) {
+                      return Expanded(
+                        child: GestureDetector(
+                          child: Image(
+                            image: AssetImage(widget.rating[i]['image'].toString()),
+                            color: Colors.white.withValues(alpha: tempRating == widget.rating[i]['key'] ? 1 : 0.3),
+                            colorBlendMode: BlendMode.modulate,
+                          ),
+                          onTap: () {
+                            setState(() {
+                              tempRating = widget.rating[i]['key'] as int;
+                              description = widget.rating[i]['label'] as String;
+                            });
+                          },
+                        ).withHover(),
+                      );
+                    }),
+                  ),
+                  const SizedBox(height: 6),
+                  Text(description,
+                      style: const TextStyle(color: textColor, fontSize: 14, fontWeight: FontWeight.w300),
+                      textAlign: TextAlign.center),
+
+                  // Bagian aspek tambahan
+                  if (tempRating != 0) ...[
+                    const SizedBox(height: 20),
+                    separator(),
+                    const SizedBox(height: 20),
+                    Column(
+                      crossAxisAlignment: CrossAxisAlignment.start,
+                      children: [
+                        Text('${'whatMakesYou'.tr()} $description?', style: const TextStyle(fontSize: 16)),
+                        const SizedBox(height: 12),
+                        Wrap(
+                          runSpacing: 10,
+                          spacing: 10,
+                          children: List.generate(aspectList.length, (i) {
+                            return GestureDetector(
+                              child: Container(
+                                padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10),
+                                decoration: BoxDecoration(
+                                  color: aspectList[i] == ratingAspect
+                                      ? primaryColor.withValues(alpha: 0.3)
+                                      : Colors.white,
+                                  border: Border.all(
+                                      color: aspectList[i] == ratingAspect ? primaryColor : textColor),
+                                  borderRadius: const BorderRadius.all(Radius.circular(50)),
+                                ),
+                                child: Text(aspectList[i] == 'OTHER_OPTION' ? 'letMeWrite'.tr() : aspectList[i],
+                                    style: const TextStyle(color: textColor)),
+                              ),
+                              onTap: () {
+                                setState(() {
+                                  ratingAspect = ratingAspect == aspectList[i] ? '' : aspectList[i];
+                                });
+                              },
+                            ).withHover();
+                          }),
+                        ),
+                        if (ratingAspect == 'OTHER_OPTION')
+                          TextFormField(
+                            maxLines: 1,
+                            maxLength: 50,
+                            autofocus: true,
+                            controller: controllerOptOther,
+                            style: const TextStyle(fontSize: 14, color: Colors.black),
+                            decoration: InputDecoration(
+                              counterText: '',
+                              hintText: 'writeHere'.tr(),
+                              hintStyle: TextStyle(color: textColor.withValues(alpha: 0.5), fontSize: 14),
+                              filled: true,
+                              fillColor: Colors.white,
+                              isDense: true,
+                              contentPadding: const EdgeInsets.all(13),
+                              prefixIcon: Padding(
+                                padding: const EdgeInsets.only(left: 13, right: 13),
+                                child: U.iconsax('edit-2', color: textColor, size: 22),
+                              ),
+                              enabledBorder: OutlineInputBorder(
+                                  borderRadius: BorderRadius.circular(12),
+                                  borderSide: BorderSide(color: const Color(0xff262626).withValues(alpha: 0.5))),
+                              focusedBorder: OutlineInputBorder(
+                                  borderRadius: BorderRadius.circular(12),
+                                  borderSide: const BorderSide(color: primaryColor)),
+                            ),
+                          ),
+                      ],
+                    ),
+                  ],
+
+                  // Tombol aksi
+                  const SizedBox(height: 20),
+                  Opacity(
+                    opacity: tempRating > 0 ? 1 : 0,
+                    child: Row(
+                      mainAxisAlignment: MainAxisAlignment.center,
+                      children: [
+                        Expanded(
+                          child: buttonTemplate(
+                            text: 'textCancel'.tr(),
+                            backgroundColor: Colors.black12,
+                            borderColor: Colors.black12,
+                            textColor: textColor,
+                            height: 40,
+                            action: () => navigateBack(context),
+                          ),
+                        ),
+                        const SizedBox(width: 20),
+                        Expanded(
+                          child: buttonTemplate(
+                            text: 'save_rating'.tr(),
+                            backgroundColor: primaryColor,
+                            borderColor: primaryColor,
+                            height: 40,
+                            action: () async {
+                              if (tempRating > 0) {
+                                var data = {
+                                  "ticketNo": widget.list['ticketNo'],
+                                  "rateSatisfy": tempRating,
+                                };
+                                if (ratingAspect != 'OTHER_OPTION') {
+                                  data['aspect'] = ratingAspect;
+                                } else if (controllerOptOther.text.isNotEmpty) {
+                                  data['aspect'] = controllerOptOther.text;
+                                }
+
+                                var res = await ApiAuthProvider()
+                                    .postData('/api/requestHistories/rateSatisfy', null, data);
+                                if (res != null) {
+                                  widget.parentContext.read<DoneCubit>().setSatisfaction(widget.index, tempRating);
+                                  navigateBack(context);
+                                }
+                              }
+                            },
+                          ),
+                        ),
+                      ],
+                    ),
+                  ),
+                ],
+              ),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}

+ 7 - 1
lib/src/layouts/components/responsive.dart

@@ -1,5 +1,6 @@
 import 'package:auto_route/auto_route.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:telnow_mobile_new/src/layouts/mobile/app_mobile.dart';
 import 'package:telnow_mobile_new/src/layouts/web/app_web.dart';
 import 'package:telnow_mobile_new/src/layouts/web/menu_account.dart';
@@ -7,6 +8,8 @@ import 'package:telnow_mobile_new/src/layouts/web/menu_history.dart';
 import 'package:telnow_mobile_new/src/layouts/web/menu_home.dart';
 import 'package:telnow_mobile_new/src/utils/U.dart';
 
+import '../../cubit/menu_history_cubit.dart';
+
 @RoutePage()
 class AppResponsive extends StatelessWidget {
   const AppResponsive({super.key});
@@ -33,7 +36,10 @@ class HistoryResponsive extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    return U.webView(context) ? WebHistoryPage() : MobMenuTemplate();
+    return BlocProvider(
+      create: (context) => MenuHistoryCubit(),
+      child: U.webView(context) ? WebHistoryPage() : MobMenuTemplate(),
+    );
   }
 }
 

+ 8 - 7
lib/src/layouts/web/history_detail.dart

@@ -8,6 +8,7 @@ import 'package:telnow_mobile_new/src/layouts/functions/detail.dart';
 import 'package:telnow_mobile_new/src/layouts/components/template.dart';
 import 'package:telnow_mobile_new/src/layouts/web/history_forum.dart';
 import 'package:telnow_mobile_new/src/utils/U.dart';
+import 'package:telnow_mobile_new/src/utils/extensions.dart';
 import 'package:telnow_mobile_new/src/utils/provider.dart';
 import 'package:timelines_plus/timelines_plus.dart';
 import 'package:url_launcher/url_launcher.dart';
@@ -91,7 +92,7 @@ class _WebHistoryDetailPageState extends State<WebHistoryDetailPage> {
                 GestureDetector(
                   child: Text('buttonBack'.tr(), style: TextStyle(color: primaryColor, fontSize: 14)),
                   onTap: ()=>navigateBack(context),
-                )
+                ).withHover(),
               ],
             ),
           ),
@@ -121,7 +122,7 @@ class _WebHistoryDetailPageState extends State<WebHistoryDetailPage> {
                           Provider.of<HistoryModule>(context, listen: false).setForumFalse(widget.index);
                           navigateTo(context, WebHistoryForumPage(data: list, user: user));
                         },
-                      ) : Container()
+                      ).withHover() : Container()
                     ],
                   ),
                   SizedBox(height: 20),
@@ -175,7 +176,7 @@ class _WebHistoryDetailPageState extends State<WebHistoryDetailPage> {
                                           )
                                       ),
                                       onTap: () => detFunc.openAttachment(list)
-                                  ) : GestureDetector(
+                                  ).withHover() : GestureDetector(
                                       child: LayoutBuilder(
                                         builder: (context, constraints) {
                                           return Image.network(list['_mobileResponseAttachment'], fit: BoxFit.cover, width: double.infinity, height: constraints.maxWidth/(1.7), loadingBuilder:(BuildContext? context, Widget? child,ImageChunkEvent? loadingProgress) {
@@ -192,7 +193,7 @@ class _WebHistoryDetailPageState extends State<WebHistoryDetailPage> {
                                         },
                                       ),
                                       onTap: ()=>navigateTo(context, PhotoPreview('image'.tr(), list['_mobileResponseAttachment'], true))
-                                  ) : Container()
+                                  ).withHover() : Container()
                                 ],
                               ) : attachment_new(list),
 
@@ -368,7 +369,7 @@ class _WebHistoryDetailPageState extends State<WebHistoryDetailPage> {
                                               padding: const EdgeInsets.all(12.0),
                                               child: Icon(Icons.delete_rounded, size: 24, color: Colors.red.withValues(alpha: 0.85),),
                                             ),
-                                          )
+                                          ).withHover()
                                         ],
                                       ),
                                     ),
@@ -577,7 +578,7 @@ class _WebHistoryDetailPageState extends State<WebHistoryDetailPage> {
                       ),
                     ),
                     onTap: ()=>navigateTo(context, PhotoPreviewGallery(title: 'image'.tr(), imageList: imageList, startIndex: i))
-                );
+                ).withHover();
               }),
             );
           },
@@ -620,7 +621,7 @@ class _WebHistoryDetailPageState extends State<WebHistoryDetailPage> {
                       ),
                     ),
                     onTap: ()=>navigateTo(context, PhotoPreviewGallery(title: 'finishAttachment'.tr(), imageList: imageList, startIndex: i))
-                );
+                ).withHover();
               }),
             );
           },

+ 157 - 0
lib/src/layouts/web/menu-history/build_filter.dart

@@ -0,0 +1,157 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import '../../../cubit/menu_history_cubit.dart';
+import '../../../cubit/tab_history_cubit.dart';
+import '../../../utils/U.dart';
+import '../../../utils/extensions.dart';
+
+class BuildFilter extends StatelessWidget {
+  const BuildFilter({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<MenuHistoryCubit, MenuHistoryState>(
+        builder: (context, state) {
+          HistoryTab tab = state.activeTab;
+          List currentFilters = tab.filters;
+
+          return SingleChildScrollView(
+            scrollDirection: Axis.horizontal,
+            child: tab == HistoryTab.ongoing
+                ? _ongoingFilter(currentFilters)
+                : _doneFilter(currentFilters),
+          );
+        }
+    );
+  }
+
+  _ongoingFilter(List<dynamic> currentFilters) {
+    return BlocBuilder<OngoingCubit, TabState>(
+      builder: (context, state) {
+        return Row(
+          children: List.generate(currentFilters.length, (i) {
+            return GestureDetector(
+              child: _filterItem(state, currentFilters, i),
+              onTap: () {
+                String currentFilter = '';
+
+                if (state.filter != currentFilters[i]['value']) {
+                  currentFilter = currentFilters[i]['value'];
+                } else {
+                  currentFilter = 'none';
+                }
+                context.read<OngoingCubit>().setFilter(currentFilter);
+                // context.read<OngoingCubit>().loadOngoing();
+              },
+            ).withHover();
+          }),
+        );
+      }
+    );
+  }
+
+  _doneFilter(List<dynamic> currentFilters) {
+    return BlocBuilder<DoneCubit, TabState>(
+      builder: (context, state) {
+        return Row(
+          children: List.generate(currentFilters.length, (i) {
+            return GestureDetector(
+              child: _filterItem(state, currentFilters, i),
+              onTap: () {
+                String currentFilter = '';
+
+                if (state.filter != currentFilters[i]['value']) {
+                  currentFilter = currentFilters[i]['value'];
+                } else {
+                  currentFilter = 'none';
+                }
+                context.read<DoneCubit>().setFilter(currentFilter);
+                // context.read<DoneCubit>().loadDone();
+              },
+            ).withHover();
+          }),
+        );
+      }
+    );
+  }
+
+  Widget? _filterItem(TabState state, List<dynamic> currentFilters, int i) {
+    return Container(
+      margin: EdgeInsets.only(left: 16),
+      padding: EdgeInsets.symmetric(vertical: 8, horizontal: 16),
+      decoration: BoxDecoration(
+          color: state.filter == currentFilters[i]['value']
+              ? primaryColor
+              : Color(0xffF2F8F7),
+          border: Border.all(
+              color: state.filter == currentFilters[i]['value']
+                  ? primaryColor
+                  : Color(0xffBEC1C1)),
+          borderRadius: BorderRadius.all(Radius.circular(50))),
+      child: Text(currentFilters[i]['title'], style: TextStyle(
+          color: state.filter == currentFilters[i]['value'] ? Colors
+              .white : textColor, fontSize: 16)),
+    );
+  }
+}
+
+class FilterItem extends StatelessWidget {
+  final TabState state;
+  final List<dynamic> currentFilters;
+  final int i;
+
+  const FilterItem({super.key, required this.state, required this.currentFilters, required this.i});
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      margin: EdgeInsets.only(left: 16),
+      padding: EdgeInsets.symmetric(vertical: 8, horizontal: 16),
+      decoration: BoxDecoration(
+          color: state.filter == currentFilters[i]['value']
+              ? primaryColor
+              : Color(0xffF2F8F7),
+          border: Border.all(
+              color: state.filter == currentFilters[i]['value']
+                  ? primaryColor
+                  : Color(0xffBEC1C1)),
+          borderRadius: BorderRadius.all(Radius.circular(50))),
+      child: Text(currentFilters[i]['title'], style: TextStyle(
+          color: state.filter == currentFilters[i]['value'] ? Colors
+              .white : textColor, fontSize: 16)),
+    );
+  }
+}
+
+class OngoingFilter extends StatelessWidget {
+  final List<dynamic> currentFilters;
+
+  const OngoingFilter({super.key, this.currentFilters = const []});
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<OngoingCubit, TabState>(
+        builder: (context, state) {
+          return Row(
+            children: List.generate(currentFilters.length, (i) {
+              return GestureDetector(
+                child: FilterItem(state: state, currentFilters: currentFilters, i: i),
+                onTap: () {
+                  String currentFilter = '';
+
+                  if (state.filter != currentFilters[i]['value']) {
+                    currentFilter = currentFilters[i]['value'];
+                  } else {
+                    currentFilter = 'none';
+                  }
+                  context.read<OngoingCubit>().setFilter(currentFilter);
+                  // context.read<OngoingCubit>().loadOngoing();
+                },
+              ).withHover();
+            }),
+          );
+        }
+    );
+  }
+}

+ 63 - 0
lib/src/layouts/web/menu-history/build_tab.dart

@@ -0,0 +1,63 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import '../../../cubit/menu_history_cubit.dart';
+import '../../../utils/U.dart';
+import '../../../utils/extensions.dart';
+
+class BuildTab extends StatelessWidget {
+  final TabController controller;
+  const BuildTab({super.key, required this.controller});
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<MenuHistoryCubit, MenuHistoryState>(
+        builder: (context, state) {
+          return Row(
+            children: [
+              GestureDetector(
+                child: Container(
+                  margin: EdgeInsets.only(left: 16),
+                  padding: EdgeInsets.symmetric(vertical: 5, horizontal: 16),
+                  decoration: BoxDecoration(
+                      color: Colors.white,
+                      border: Border(bottom: BorderSide(
+                          color: state.activeTab.name == "ongoing"
+                              ? primaryColor
+                              : primaryColor.withValues(alpha: 0.3),
+                          width: state.activeTab.name == "ongoing" ? 2 : 1))),
+                  child: Text('ongoing'.tr(), style: TextStyle(
+                      color: state.activeTab.name == "ongoing"
+                          ? textColor
+                          : textColor.withValues(alpha: 0.65), fontSize: 16)),
+                ),
+                onTap: () {
+                  controller.animateTo(0);
+                  context.read<MenuHistoryCubit>().setActiveTab(HistoryTab.ongoing);
+                },
+              ).withHover(),
+              GestureDetector(
+                child: Container(
+                  padding: EdgeInsets.symmetric(vertical: 5, horizontal: 16),
+                  decoration: BoxDecoration(color: Colors.white,
+                      border: Border(bottom: BorderSide(
+                          color: state.activeTab.name == "done"
+                              ? primaryColor
+                              : primaryColor.withValues(alpha: 0.3),
+                          width: state.activeTab.name == "done" ? 2 : 1))),
+                  child: Text('done'.tr(), style: TextStyle(
+                      color: state.activeTab.name == "done"
+                          ? textColor
+                          : textColor.withValues(alpha: 0.65), fontSize: 16)),
+                ),
+                onTap: () {
+                  controller.animateTo(1);
+                  context.read<MenuHistoryCubit>().setActiveTab(HistoryTab.done);
+                },
+              ).withHover(),
+            ],
+          );;
+        });
+  }
+}

+ 93 - 0
lib/src/layouts/web/menu-history/done_container.dart

@@ -0,0 +1,93 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:lazy_load_scrollview/lazy_load_scrollview.dart';
+import 'package:provider/provider.dart';
+import 'package:telnow_mobile_new/src/layouts/web/menu-history/history_item_widget.dart';
+
+import '../../../cubit/menu_history_cubit.dart';
+import '../../../cubit/tab_history_cubit.dart';
+import '../../../utils/U.dart';
+import '../../../utils/extensions.dart';
+import '../../../utils/provider.dart';
+import '../../components/rate_mission.dart';
+import '../../components/template.dart';
+import '../../functions/history.dart' hide HistoryTab;
+import '../history_detail.dart';
+
+class DoneContainer extends StatefulWidget {
+  const DoneContainer({super.key});
+
+  @override
+  State<DoneContainer> createState() => _DoneContainerState();
+}
+
+class _DoneContainerState extends State<DoneContainer>
+    with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin {
+  late AnimationController _animationController;
+
+  @override
+  bool get wantKeepAlive => true;
+
+  @override
+  void initState() {
+    _animationController = AnimationController(
+      vsync: this,
+      duration: const Duration(milliseconds: 800),
+    )..repeat(reverse: true);
+    super.initState();
+  }
+
+  @override
+  void dispose() {
+    _animationController.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    super.build(context);
+    HistoryModule historyModule = Provider.of<HistoryModule>(context, listen: false);
+    HistoryFunction historyFunction = HistoryFunction();
+
+    List<dynamic> rating = [
+      {'key': 1, 'image': "assets/image/icon/very_dissatisfied.png", 'label': 'disatisfied'.tr()},
+      {'key': 2, 'image': "assets/image/icon/dissatisfied.png", 'label': 'lessSatisfied'.tr()},
+      {'key': 3, 'image': "assets/image/icon/neutral.png", 'label': 'satisfied'.tr()},
+      {'key': 4, 'image': "assets/image/icon/satisfied.png", 'label': 'verySatisfied'.tr()},
+      {'key': 5, 'image': "assets/image/icon/very_satisfied.png", 'label': 'reallyPleased'.tr()},
+    ];
+
+    return BlocBuilder<DoneCubit, TabState>(
+      builder: (context, state) {
+        UserModule userModule = Provider.of<UserModule>(context, listen: false);
+        var data = state.data;
+
+        if(state.isLoading) {
+          return loadingTemplateNoVoid();
+        } else if(data.isEmpty){
+          return NoDataPage();
+        }
+
+        return LazyLoadScrollView(
+          onEndOfPage: () => context.read<DoneCubit>().loadNextPage(),
+          scrollOffset: 300,
+          child: ListView.builder(
+            itemCount: data.length,
+            itemBuilder: (context, i){
+              return HistoryItemWidget(
+                tab: 'done',
+                state: state,
+                index: i,
+                item: data[i],
+                userModule: userModule,
+                rating: rating,
+                animationController: _animationController,
+              );
+            }
+          ),
+        );
+      },
+    );
+  }
+}

+ 282 - 0
lib/src/layouts/web/menu-history/history_item_widget.dart

@@ -0,0 +1,282 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import 'package:telnow_mobile_new/src/cubit/menu_history_cubit.dart' hide HistoryTab;
+
+import '../../../cubit/tab_history_cubit.dart';
+import '../../../utils/U.dart';
+import '../../../utils/extensions.dart';
+import '../../../utils/provider.dart';
+import '../../components/rate_mission.dart';
+import '../../components/template.dart';
+import '../history_detail.dart';
+
+class HistoryItemWidget extends StatelessWidget {
+  final Map<String, dynamic> item;
+  final UserModule userModule;
+  final List<dynamic> rating;
+  final AnimationController animationController;
+  final int index;
+  final TabState state;
+  final String tab;
+
+  const HistoryItemWidget({
+    super.key,
+    required this.item,
+    required this.userModule,
+    required this.rating,
+    required this.animationController,
+    required this.index,
+    required this.state,
+    required this.tab,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return GestureDetector(
+      onTap: () {
+        if (context.read<MenuHistoryCubit>().state.multiSelectMode && tab == 'done') {
+          context.read<MenuHistoryCubit>().selectIndex(index);
+        } else {
+          navigateTo(
+            context,
+            WebHistoryDetailPage(
+              index: index,
+              locale: context.locale.toString(),
+            ),
+          );
+        }
+      },
+      onLongPress: () => tab == 'done' && !context.read<MenuHistoryCubit>().state.multiSelectMode
+          ? context.read<MenuHistoryCubit>().selectIndex(index) : null,
+      child: Container(
+        width: double.infinity,
+        padding: const EdgeInsets.all(15),
+        margin: const EdgeInsets.only(bottom: 15),
+        decoration: BoxDecoration(
+          color: context.read<MenuHistoryCubit>().state.selectedIndex.contains(index)
+              ? primaryColor.withValues(alpha: 0.15) : Colors.white,
+          border: Border.all(color: textColor.withValues(alpha: 0.15)),
+          borderRadius: const BorderRadius.all(Radius.circular(12)),
+        ),
+        child: Column(
+          spacing: 10,
+          children: [
+            _buildHeader(context),
+            _buildBody(context),
+            _buildFooter(context),
+          ],
+        ),
+      ),
+    ).withHover();
+  }
+
+  Widget _buildHeader(BuildContext context) {
+    return Row(
+      children: [
+        Text(
+          convertDate(item['datetimeRequest'], context.locale.toString()),
+          style: const TextStyle(color: primaryColor, fontSize: 14),
+        ),
+        const SizedBox(width: 15),
+        Expanded(
+          child: Text(
+            '${'ticketNumber'.tr()}: ${item['ticketNo']}',
+            style: const TextStyle(color: textColor, fontSize: 14),
+            overflow: TextOverflow.ellipsis,
+          ),
+        ),
+        _renderStatus(item['currentState'])
+      ],
+    );
+  }
+
+  Widget _buildBody(BuildContext context) {
+    return Column(
+      children: [
+        divider(),
+        const SizedBox(height: 10),
+        Row(
+          children: [
+            imageTiles(imageUrl: item['_requestImage'] ?? "null", width: 150, height: 120),
+            const SizedBox(width: 25),
+            Expanded(
+              child: Column(
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: [
+                  Text(item[U.langColumn(context, 'requestGroupDescription')],
+                      style: const TextStyle(color: textColor, fontSize: 16),
+                      overflow: TextOverflow.ellipsis),
+                  const SizedBox(height: 10),
+                  Text(item[U.langColumn(context, 'requestSubject')],
+                      style: const TextStyle(color: textColor, fontWeight: FontWeight.w600),
+                      overflow: TextOverflow.ellipsis),
+                  const SizedBox(height: 5),
+                  Text(item[U.langColumn(context, '_subjectDescription')] ?? '',
+                      style: const TextStyle(color: textColor),
+                      overflow: TextOverflow.ellipsis),
+                  dashed(top: 10, bottom: 10),
+                  _renderRequested(userModule, item),
+                  Text('${'location'.tr()}: ${item['ipphoneExtLocation']}',
+                      style: const TextStyle(color: textColor, fontSize: 14),
+                      overflow: TextOverflow.ellipsis),
+                ],
+              ),
+            )
+          ],
+        ),
+      ],
+    );
+  }
+
+  Widget _buildFooter(BuildContext context) {
+    bool doneStatus = item['currentState'] == 'DISELESAIKAN' ||
+        item['currentState'] == 'TUNTAS' ||
+        item['currentState'] == 'DIBATALKAN';
+
+    final rowChildren = <Widget>[
+      if ((item['satisfactionRate'] == 0 && item['servantGroup'] != "#autoresponse") && doneStatus)
+        GestureDetector(
+          child: Container(
+            padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 16),
+            decoration: BoxDecoration(
+              color: Colors.white,
+              border: Border.all(color: secondaryColor),
+              borderRadius: const BorderRadius.all(Radius.circular(50)),
+            ),
+            child: Text('rateReq'.tr(),
+                style: const TextStyle(color: secondaryColor, fontSize: 14),
+                overflow: TextOverflow.ellipsis),
+          ),
+          onTap: () {
+            showDialog(
+              context: context,
+              builder: (_) => RateMission(
+                parentContext: context,
+                list: item,
+                index: index,
+                rating: rating,
+                state: state,
+              ),
+            );
+          },
+        ).withHover()
+      else if (item['satisfactionRate'] > 0)
+        Row(
+          mainAxisAlignment: MainAxisAlignment.end,
+          children: [
+            Text(rating[item['satisfactionRate']-1]['label'].toString(), style: TextStyle(color: textColor, fontSize: 14), overflow: TextOverflow.ellipsis),
+            SizedBox(width: 5),
+            Image(image: AssetImage(rating[item['satisfactionRate']-1]['image'].toString()), width: 25),
+          ],
+        )
+      else
+        SizedBox(),
+
+      if(item['_hasForum']) _message(item['_hasForum'], item['_forumMsg'] ?? "null"),
+
+      if(doneStatus) GestureDetector(
+        child: Container(
+          padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 16),
+          decoration: BoxDecoration(
+            color: Colors.white,
+            border: Border.all(color: primaryColor),
+            borderRadius: const BorderRadius.all(Radius.circular(50)),
+          ),
+          child: Text('reqAgain'.tr(),
+              style: const TextStyle(color: primaryColor, fontSize: 14)),
+        ),
+        onTap: () => context.read<MenuHistoryCubit>().requestAgain(item) //historyFunction.requestAgainAction(index),
+      ).withHover()
+    ];
+
+    // kalau rowChildren kosong → return shrink
+    if (rowChildren.isEmpty) {
+      return const SizedBox.shrink();
+    }
+
+    return Column(
+      children: [
+        divider(),
+        const SizedBox(height: 10),
+        Row(
+          mainAxisAlignment: MainAxisAlignment.spaceBetween,
+          children: rowChildren,
+        ),
+      ],
+    );
+  }
+
+
+  Widget _renderStatus(String currentState) {
+    Map<String, Map<String, dynamic>> states = {
+      'DIMULAI': {'text': 'onProgress'.tr(), 'color': Color(0xffCCA600).withValues(alpha: 0.2)},
+      'HOLD': {'text': 'hold'.tr(), 'color': Color(0xffD3D3D3)},
+      'DIPROSES': {'text': 'queued'.tr(), 'color': Color(0xff02C539).withValues(alpha: 0.2)},
+      'DIANTRIKAN': {'text': 'queued'.tr(), 'color': Color(0xff02C539).withValues(alpha: 0.2)},
+      'DISELESAIKAN': {'text': 'stateFinish'.tr(), 'color': primaryColor.withValues(alpha: 0.4)},
+      'TUNTAS': {'text': 'stateFinish'.tr(), 'color': primaryColor.withValues(alpha: 0.4)},
+      'DIBATALKAN': {'text': 'stateCancel'.tr(), 'color': const Color(0xffD81010).withValues(alpha: 0.4)},
+    };
+    
+    Color color = states[currentState]?['color'] ?? Colors.transparent;
+    final text = states[currentState]?['text'] ?? '';
+  
+    return Container(
+      padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 10),
+      margin: const EdgeInsets.only(left: 10),
+      decoration: BoxDecoration(color: color, borderRadius: const BorderRadius.all(Radius.circular(3))),
+      child: Text(text, style: const TextStyle(fontSize: 12, color: textColor)),
+    );
+  }
+
+  Widget _renderRequested(userModule, list) {
+    if (list['receptionistId'] != null) {
+      var user = userModule.user();
+      if (user['roomAttendant'] &&
+          user['userId'] != list['informantUserId'] &&
+          user['userId'] == list['receptionistId']) {
+        return Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            Text('${'requestedFor'.tr()}: ${list['informantName'] ?? '-'}',
+                style: const TextStyle(color: textColor, fontSize: 13, fontWeight: FontWeight.w300)),
+            const SizedBox(height: 6),
+          ],
+        );
+      }
+
+      if (user['userId'] == list['informantUserId'] &&
+          user['userId'] != list['receptionistId']) {
+        return Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            Text('${'requestedBy'.tr()}: ${list['receptionistName'] ?? ''}',
+                style: const TextStyle(color: textColor, fontSize: 13, fontWeight: FontWeight.w300)),
+            const SizedBox(height: 6),
+          ],
+        );
+      }
+    }
+
+    return Container();
+  }
+
+  Widget _message(bool hasForum, String forumMsg) {
+    if (hasForum) {
+      return Row(
+        children: [
+          FadeTransition(
+            opacity: animationController,
+            child: U.iconsax('messages-3', color: primaryColor),
+          ),
+          const SizedBox(width: 8),
+          Text('“$forumMsg”',
+              style: TextStyle(color: textColor.withValues(alpha: 0.75), fontSize: 14, fontStyle: FontStyle.italic),
+              overflow: TextOverflow.ellipsis),
+        ],
+      );
+    }
+    return const SizedBox();
+  }
+}

+ 139 - 0
lib/src/layouts/web/menu-history/ongoing_container.dart

@@ -0,0 +1,139 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:lazy_load_scrollview/lazy_load_scrollview.dart';
+import 'package:provider/provider.dart';
+import 'package:telnow_mobile_new/src/layouts/components/template.dart';
+import 'package:telnow_mobile_new/src/layouts/web/menu-history/history_item_widget.dart';
+
+import '../../../cubit/menu_history_cubit.dart';
+import '../../../cubit/tab_history_cubit.dart';
+import '../../../utils/U.dart';
+import '../../../utils/provider.dart';
+import '../../functions/history.dart' hide HistoryTab;
+
+class OngoingContainer extends StatefulWidget {
+  const OngoingContainer({super.key});
+
+  @override
+  State<StatefulWidget> createState() => _OngoingContainerState();
+}
+
+class _OngoingContainerState extends State<OngoingContainer>
+    with TickerProviderStateMixin, AutomaticKeepAliveClientMixin {
+  late AnimationController _animationController;
+
+
+  @override
+  bool get wantKeepAlive => true;
+
+  @override
+  void initState() {
+    _animationController = AnimationController(
+      vsync: this,
+      duration: const Duration(milliseconds: 800),
+    )..repeat(reverse: true);
+    // TODO: implement initState
+    super.initState();
+  }
+
+  @override
+  void dispose() {
+    _animationController.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    super.build(context);
+    return BlocBuilder<OngoingCubit, TabState>(
+      builder: (BuildContext context, state) {
+        UserModule userModule = Provider.of<UserModule>(context, listen: false);
+        HistoryFunction historyFunction = HistoryFunction();
+        var pendingData = context.read<MenuHistoryCubit>().state.pendingData;
+        List<dynamic> rating = [];
+
+        if(state.isLoading) {
+          return loadingTemplateNoVoid();
+        }
+
+        if(!U.getInternetStatus() && pendingData.isNotEmpty) {
+          return ListView.builder(
+            itemCount: state.data.length,
+            itemBuilder: (context, i) {
+              return HistoryItemWidget(
+                tab: 'ongoing',
+                state: state,
+                index: i,
+                item: pendingData[i],
+                userModule: userModule,
+                rating: rating,
+                animationController: _animationController
+              );
+            });
+        }
+
+        if(state.data.isEmpty) {
+          return NoDataPage();
+        }
+
+        return LazyLoadScrollView(
+          onEndOfPage: () => context.read<OngoingCubit>().loadNextPage(),
+          scrollOffset: 300,
+          child: ListView.builder(
+            itemCount: state.data.length,
+            itemBuilder: (context, i) {
+              var dataRequest = state.data;
+
+              return HistoryItemWidget(
+                tab: 'ongoing',
+                state: state,
+                index: i,
+                item: dataRequest[i],
+                userModule: userModule,
+                rating: rating,
+                animationController: _animationController
+              );
+            }),
+        );
+      }
+    );
+  }
+
+  // renderStatus(text, color){
+  //   return Container(
+  //     padding: EdgeInsets.symmetric(vertical: 3, horizontal: 10),
+  //     margin: EdgeInsets.only(left: 10),
+  //     decoration: BoxDecoration(
+  //         color: color, borderRadius: BorderRadius.all(Radius.circular(3))
+  //     ),
+  //     child: Text(text, style: TextStyle(fontSize: 12, color: textColor)),
+  //   );
+  // }
+  //
+  // Widget renderRequested(list){
+  //   if(list['receptionistId'] != null){
+  //     var user = userModule.user();
+  //     if(user['roomAttendant'] && user['userId'] != list['informantUserId'] && user['userId'] == list['receptionistId']){
+  //       return Column(
+  //         crossAxisAlignment: CrossAxisAlignment.start,
+  //         children: [
+  //           Text('${'requestedFor'.tr()}: ${list['informantName']??'-'}' ,style: TextStyle(color: textColor, fontSize: 13, fontWeight: FontWeight.w300)),
+  //           SizedBox(height: 6),
+  //         ],
+  //       );
+  //     }
+  //
+  //     if(user['userId'] == list['informantUserId'] && user['userId'] != list['receptionistId']){
+  //       return Column(
+  //         crossAxisAlignment: CrossAxisAlignment.start,
+  //         children: [
+  //           Text('${'requestedBy'.tr()}: ${list['receptionistName']??''}' ,style: TextStyle(color: textColor, fontSize: 13, fontWeight: FontWeight.w300)),
+  //           SizedBox(height: 6),
+  //         ],
+  //       );
+  //     }
+  //   }
+  //
+  //   return Container();
+  // }
+}

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 140 - 794
lib/src/layouts/web/menu_history.dart


+ 1 - 1
lib/src/layouts/web/request_create.dart

@@ -29,7 +29,7 @@ class WebReqCreatePage extends StatefulWidget {
 }
 
 getColorScheme(val){
-  var color;
+  Color color;
   switch (val){
     case 0: color = Color(0xFF4FB66C); break;
     case 50: color = Color(0xFFFFA800); break;

+ 89 - 0
lib/src/repository/history_repository.dart

@@ -0,0 +1,89 @@
+import 'package:flutter/material.dart';
+
+import '../api/api_auth_provider.dart';
+import '../utils/cache_manager.dart';
+
+class HistoryRepository {
+  final ApiAuthProvider _apiAuthProvider = ApiAuthProvider();
+
+  //## cuma request doang, balikin hasil request ke cubit
+  Future<List<dynamic>> getData(String key, String filter, int page, int size, List<dynamic> activeForum) async {
+    try{
+      String url = '/api/requestHistories/search/myReqHistory';
+      var sort = ['datetimeRequest,desc'];
+
+      var mission = await _apiAuthProvider.getData(url, {'size': size.toString(), 'page': page.toString(), 'sort': sort, 'filter': filter});
+      List tempData = [];
+
+      if (mission != null){
+        if (mission.containsKey('_embedded')) {
+          for (int i = 0; i < mission['_embedded']['requestHistories'].length; i++) {
+            var isFrm = false;
+            String id = "";
+            String msg = "";
+
+            activeForum.forEach((el) {
+              if (el['ticketId'] == mission['_embedded']['requestHistories'][i]['ticketNo']) {
+                id = el['id'];
+                msg = el['desc'];
+              }
+            });
+
+            if (id != "") isFrm = true;
+            mission['_embedded']['requestHistories'][i]['_hasForum'] = isFrm;
+            mission['_embedded']['requestHistories'][i]['_forumId'] = id;
+            mission['_embedded']['requestHistories'][i]['_forumMsg'] = msg;
+
+            tempData.add(mission['_embedded']['requestHistories'][i]);
+          }
+        }
+      }
+
+      // # cache manager
+      CacheMan.writeData(key, tempData);
+
+      return tempData;
+    } catch(e){
+      debugPrint(e.toString());
+    }
+
+    return [];
+  }
+
+  deleteData(List<String> params) async{
+    var res = await _apiAuthProvider.postData("/api/requestHistories/deleteMyHistory", {"ticketNumbers": params}, null);
+
+    if (res != null) {
+      return res;
+    }
+
+    return null;
+  }
+
+  Future<List<dynamic>> getActiveForum(String url) async {
+    try {
+      var data = await _apiAuthProvider.getData(url, null);
+      if (data != null) {
+        // # caching disini
+        CacheMan.writeData(url, data);
+
+        return data;
+      }
+    } catch (e) {
+      debugPrint(e.toString());
+    }
+
+    return [];
+  }
+
+  Future<Map<String, dynamic>> checkRequest(String filter) async {
+    try {
+      var res = await _apiAuthProvider.getData(
+          '/api/requests/search/customFind', {'filter': filter});
+      return res;
+    } catch(e){
+      debugPrint(e.toString());
+    }
+    return {};
+  }
+}