feat: Refactor code structure and optimize performance across multiple modules
This commit is contained in:
18
mobile-apps/client-app/lib/app.dart
Normal file
18
mobile-apps/client-app/lib/app.dart
Normal file
@@ -0,0 +1,18 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:krow/core/application/routing/routes.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
|
||||
final appRouter = AppRouter();
|
||||
|
||||
class KrowApp extends StatelessWidget {
|
||||
const KrowApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp.router(
|
||||
routerConfig: appRouter.config(
|
||||
deepLinkBuilder: (l) => appRouter.deepLinkBuilder(l)),
|
||||
theme: KWTheme.lightTheme,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:graphql_flutter/graphql_flutter.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
class GraphQLConfig {
|
||||
static HttpLink httpLink = HttpLink(
|
||||
dotenv.env['BASE_URL']!,
|
||||
defaultHeaders: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
);
|
||||
static AuthLink authLink = AuthLink(
|
||||
getToken: () async {
|
||||
User? user = FirebaseAuth.instance.currentUser;
|
||||
if (user != null) {
|
||||
return 'Bearer ${await user.getIdToken()}';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
static Link link = authLink.concat(httpLink);
|
||||
|
||||
Future<GraphQLClient> configClient() async {
|
||||
var path = await getApplicationDocumentsDirectory();
|
||||
final store = await HiveStore.open(path: '${path.path}/gql_cache');
|
||||
|
||||
return GraphQLClient(
|
||||
link: link,
|
||||
cache: GraphQLCache(store: store, ),
|
||||
defaultPolicies: DefaultPolicies(
|
||||
mutate: Policies(
|
||||
cacheReread: CacheRereadPolicy.ignoreOptimisitic,
|
||||
error: ErrorPolicy.all,
|
||||
fetch: FetchPolicy.networkOnly,
|
||||
),
|
||||
query: Policies(
|
||||
cacheReread: CacheRereadPolicy.ignoreOptimisitic,
|
||||
error: ErrorPolicy.all,
|
||||
fetch: FetchPolicy.networkOnly,
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@Singleton()
|
||||
class ApiClient {
|
||||
static final GraphQLConfig _graphQLConfig = GraphQLConfig();
|
||||
final Future<GraphQLClient> _client = _graphQLConfig.configClient();
|
||||
|
||||
Future<QueryResult> query(
|
||||
{required String schema, Map<String, dynamic>? body}) async {
|
||||
final QueryOptions options = QueryOptions(
|
||||
document: gql(schema),
|
||||
variables: body ?? {},
|
||||
);
|
||||
return (await _client).query(options).timeout(Duration(seconds: 30));
|
||||
}
|
||||
|
||||
Stream<QueryResult?> queryWithCache({
|
||||
required String schema,
|
||||
Map<String, dynamic>? body,
|
||||
}) async* {
|
||||
final WatchQueryOptions options = WatchQueryOptions(
|
||||
document: gql(schema),
|
||||
variables: body ?? {},
|
||||
fetchPolicy: FetchPolicy.cacheAndNetwork);
|
||||
|
||||
var result = (await _client).watchQuery(options).fetchResults();
|
||||
yield result.eagerResult;
|
||||
yield await result.networkResult;
|
||||
}
|
||||
|
||||
Future<QueryResult> mutate(
|
||||
{required String schema, Map<String, dynamic>? body}) async {
|
||||
final MutationOptions options = MutationOptions(
|
||||
document: gql(schema),
|
||||
variables: body ?? {},
|
||||
);
|
||||
return (await _client).mutate(options);
|
||||
}
|
||||
|
||||
void dropCache() async {
|
||||
(await _client).cache.store.reset();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'package:graphql_flutter/graphql_flutter.dart';
|
||||
|
||||
interface class DisplayableException {
|
||||
final String message;
|
||||
|
||||
DisplayableException(this.message);
|
||||
}
|
||||
|
||||
class NetworkException implements Exception, DisplayableException {
|
||||
@override
|
||||
final String message;
|
||||
|
||||
NetworkException([this.message = 'No internet connection']);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
class GraphQLException implements Exception, DisplayableException {
|
||||
@override
|
||||
final String message;
|
||||
|
||||
GraphQLException(this.message);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
class UnknownException implements Exception, DisplayableException {
|
||||
@override
|
||||
final String message;
|
||||
|
||||
UnknownException([this.message = 'Something went wrong']);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
Exception parseBackendError(dynamic error) {
|
||||
if (error is OperationException) {
|
||||
if (error.graphqlErrors.isNotEmpty) {
|
||||
return GraphQLException(
|
||||
error.graphqlErrors.firstOrNull?.message ?? 'GraphQL error occurred');
|
||||
}
|
||||
return NetworkException('Network error occurred');
|
||||
}
|
||||
return UnknownException();
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
extension IntExtensions on int {
|
||||
String toOrdinal() {
|
||||
if (this >= 11 && this <= 13) {
|
||||
return '${this}th';
|
||||
}
|
||||
switch (this % 10) {
|
||||
case 1:
|
||||
return '${this}st';
|
||||
case 2:
|
||||
return '${this}nd';
|
||||
case 3:
|
||||
return '${this}rd';
|
||||
default:
|
||||
return '${this}th';
|
||||
}
|
||||
}
|
||||
|
||||
String getWeekdayId() => switch (this) {
|
||||
0 => 'Monday',
|
||||
1 => 'Tuesday',
|
||||
2 => 'Wednesday',
|
||||
3 => 'Thursday',
|
||||
4 => 'Friday',
|
||||
5 => 'Saturday',
|
||||
_ => 'Sunday',
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class Logger {
|
||||
static void error(e, {String? message}) {
|
||||
log(
|
||||
e.toString(),
|
||||
error: message ?? e,
|
||||
level: 2000,
|
||||
stackTrace: StackTrace.current,
|
||||
name: 'ERROR',
|
||||
);
|
||||
}
|
||||
|
||||
static debug(dynamic msg, {String? message}) {
|
||||
if (kDebugMode) {
|
||||
log(
|
||||
msg.toString(),
|
||||
name: message ?? msg.runtimeType.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const error = Logger.error;
|
||||
const debug = Logger.debug;
|
||||
@@ -0,0 +1,6 @@
|
||||
extension StringExtensions on String {
|
||||
String capitalize() {
|
||||
if (isEmpty) return this;
|
||||
return this[0].toUpperCase() + substring(1).toLowerCase();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class DateTextFormatter extends TextInputFormatter {
|
||||
@override
|
||||
TextEditingValue formatEditUpdate(
|
||||
TextEditingValue oldValue, TextEditingValue newValue) {
|
||||
if (oldValue.text.length >= newValue.text.length) {
|
||||
return newValue;
|
||||
}
|
||||
|
||||
String filteredText = newValue.text.replaceAll(RegExp(r'[^0-9.]'), '');
|
||||
|
||||
var dateText = _addSeparators(filteredText, '.');
|
||||
return newValue.copyWith(
|
||||
text: dateText, selection: updateCursorPosition(dateText));
|
||||
}
|
||||
|
||||
String _addSeparators(String value, String separator) {
|
||||
value = value.replaceAll('.', '');
|
||||
var newString = '';
|
||||
for (int i = 0; i < value.length; i++) {
|
||||
newString += value[i];
|
||||
if (i == 1) {
|
||||
newString += separator;
|
||||
}
|
||||
if (i == 3) {
|
||||
newString += separator;
|
||||
}
|
||||
}
|
||||
return newString;
|
||||
}
|
||||
|
||||
TextSelection updateCursorPosition(String text) {
|
||||
return TextSelection.fromPosition(TextPosition(offset: text.length));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
abstract class EmailValidator {
|
||||
static String? validate(String? email, {bool isRequired = true}) {
|
||||
// Regular expression for validating emails
|
||||
final RegExp phoneRegExp = RegExp(
|
||||
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
|
||||
);
|
||||
|
||||
if (isRequired && (email == null || email.isEmpty)) {
|
||||
return 'Email is required';
|
||||
} else if (email != null && !phoneRegExp.hasMatch(email)) {
|
||||
return 'Invalid email';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
abstract class PhoneValidator {
|
||||
static String? validate(String? phoneNumber, {bool isRequired = true}) {
|
||||
// Regular expression for validating phone numbers
|
||||
final RegExp phoneRegExp = RegExp(r'^\+?[1-9]\d{1,14}$');
|
||||
|
||||
if (isRequired && (phoneNumber==null || phoneNumber.isEmpty)) {
|
||||
return 'Phone number is required';
|
||||
} else if (phoneNumber !=null && !phoneRegExp.hasMatch(phoneNumber)) {
|
||||
return 'Invalid phone number';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
class SkillExpValidator {
|
||||
SkillExpValidator._();
|
||||
|
||||
static String? validate(String value) {
|
||||
if (value.isEmpty) {
|
||||
return 'Experience is required';
|
||||
}
|
||||
var parsed = int.tryParse(value);
|
||||
if (value.isNotEmpty && parsed == null) {
|
||||
return 'Experience must be a number';
|
||||
}
|
||||
|
||||
if (value.isNotEmpty && parsed! > 20 || parsed! < 1) {
|
||||
return 'Experience must be between 1 and 20';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
import 'injectable.config.dart';
|
||||
|
||||
final getIt = GetIt.instance;
|
||||
|
||||
@InjectableInit()
|
||||
void configureDependencies(String env) => GetIt.instance.init(environment: env);
|
||||
@@ -0,0 +1,41 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:krow/core/application/routing/routes.gr.dart';
|
||||
import 'package:krow/core/sevices/auth_state_service/auth_service.dart';
|
||||
|
||||
class SplashRedirectGuard extends AutoRouteGuard {
|
||||
final AuthService authService;
|
||||
|
||||
SplashRedirectGuard(this.authService);
|
||||
|
||||
@override
|
||||
Future<void> onNavigation(
|
||||
NavigationResolver resolver, StackRouter router) async {
|
||||
final status = await authService.getAuthStatus();
|
||||
switch (status) {
|
||||
case AuthStatus.authenticated:
|
||||
router.replace(const HomeRoute());
|
||||
break;
|
||||
case AuthStatus.adminValidation:
|
||||
// router.replace(const WaitingValidationRoute());
|
||||
break;
|
||||
case AuthStatus.prepareProfile:
|
||||
if(kDebugMode) {
|
||||
// router.replace(const HomeRoute());
|
||||
}else {
|
||||
// router.replace(const CheckListFlowRoute());
|
||||
}
|
||||
break;
|
||||
case AuthStatus.unauthenticated:
|
||||
// router.replace(const HomeRoute());
|
||||
router.replace( const SignInFlowRoute());
|
||||
break;
|
||||
case AuthStatus.error:
|
||||
//todo(Artem):show error screen
|
||||
router.replace( const SignInFlowRoute());
|
||||
}
|
||||
|
||||
// resolver.next(false);
|
||||
// resolver.next(true);
|
||||
}
|
||||
}
|
||||
122
mobile-apps/client-app/lib/core/application/routing/routes.dart
Normal file
122
mobile-apps/client-app/lib/core/application/routing/routes.dart
Normal file
@@ -0,0 +1,122 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:krow/core/application/di/injectable.dart';
|
||||
import 'package:krow/core/application/routing/guards.dart';
|
||||
import 'package:krow/core/application/routing/routes.gr.dart';
|
||||
import 'package:krow/core/sevices/auth_state_service/auth_service.dart';
|
||||
|
||||
@AutoRouterConfig(
|
||||
replaceInRouteName: 'Screen|Page,Route',
|
||||
)
|
||||
class AppRouter extends RootStackRouter {
|
||||
@override
|
||||
RouteType get defaultRouteType => const RouteType.material();
|
||||
|
||||
Future<DeepLink> deepLinkBuilder(PlatformDeepLink deepLink) async {
|
||||
final uri = deepLink.uri;
|
||||
if (uri.queryParameters.containsKey('oobCode')) {
|
||||
final code = uri.queryParameters['oobCode'];
|
||||
popUntil((route) => false);
|
||||
return DeepLink([
|
||||
const WelcomeRoute(),
|
||||
const SignInRoute(),
|
||||
EnterNewPassRoute(code: code),
|
||||
]);
|
||||
}
|
||||
return deepLink;
|
||||
}
|
||||
|
||||
@override
|
||||
List<AutoRoute> get routes => [
|
||||
AdaptiveRoute(
|
||||
path: '/',
|
||||
page: SplashRoute.page,
|
||||
initial: true,
|
||||
guards: [SplashRedirectGuard(getIt<AuthService>())],
|
||||
),
|
||||
createEventFlow,
|
||||
authFlow,
|
||||
homeFlow,
|
||||
];
|
||||
|
||||
get authFlow => AdaptiveRoute(
|
||||
path: '/auth',
|
||||
page: SignInFlowRoute.page,
|
||||
children: [
|
||||
AutoRoute(path: 'welcome', page: WelcomeRoute.page, initial: true),
|
||||
AutoRoute(
|
||||
path: 'sign_in',
|
||||
page: SignInRoute.page,
|
||||
),
|
||||
AutoRoute(
|
||||
path: 'reset_pass',
|
||||
page: ResetPassRoute.page,
|
||||
),
|
||||
AutoRoute(
|
||||
path: 'pass',
|
||||
page: EnterNewPassRoute.page,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
get createEventFlow =>
|
||||
AutoRoute(path: '/create', page: CreateEventFlowRoute.page, children: [
|
||||
AutoRoute(path: 'edit', page: CreateEventRoute.page, initial: true),
|
||||
AutoRoute(
|
||||
path: 'preview',
|
||||
page: EventDetailsRoute.page,
|
||||
),
|
||||
]);
|
||||
|
||||
get homeFlow => AdaptiveRoute(
|
||||
path: '/home',
|
||||
page: HomeRoute.page,
|
||||
children: [eventsFlow, invoiceFlow, notificationsFlow, profileFlow]);
|
||||
|
||||
get eventsFlow =>
|
||||
AdaptiveRoute(path: 'events', page: EventsFlowRoute.page, children: [
|
||||
AutoRoute(path: 'list', page: EventsListMainRoute.page, initial: true),
|
||||
AutoRoute(path: 'details', page: EventDetailsRoute.page),
|
||||
AutoRoute(path: 'assigned', page: AssignedStaffRoute.page),
|
||||
AutoRoute(path: 'assigned_manual', page: ClockManualRoute.page),
|
||||
AutoRoute(path: 'rate_staff', page: RateStaffRoute.page),
|
||||
]);
|
||||
|
||||
get invoiceFlow =>
|
||||
AdaptiveRoute(path: 'invoice', page: InvoiceFlowRoute.page, children: [
|
||||
AutoRoute(
|
||||
path: 'list', page: InvoicesListMainRoute.page, initial: true),
|
||||
AutoRoute(
|
||||
path: 'details', page: InvoiceDetailsRoute.page),
|
||||
AutoRoute(
|
||||
path: 'event',
|
||||
page: EventDetailsRoute.page,
|
||||
),
|
||||
]);
|
||||
|
||||
get notificationsFlow => AdaptiveRoute(
|
||||
path: 'notifications',
|
||||
page: NotificationFlowRoute.page,
|
||||
children: [
|
||||
AutoRoute(
|
||||
path: 'list', page: NotificationsListRoute.page, initial: true),
|
||||
AutoRoute(
|
||||
path: 'details',
|
||||
page: NotificationDetailsRoute.page,
|
||||
),
|
||||
]);
|
||||
|
||||
get profileFlow =>
|
||||
AdaptiveRoute(path: 'profile', page: ProfileFlowRoute.page, children: [
|
||||
AutoRoute(
|
||||
path: 'preview', page: ProfilePreviewRoute.page, initial: true),
|
||||
AutoRoute(
|
||||
path: 'edit',
|
||||
page: PersonalInfoRoute.page,
|
||||
),
|
||||
]);
|
||||
|
||||
@override
|
||||
List<AutoRouteGuard> get guards => [];
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
enum StateStatus {
|
||||
idle,
|
||||
loading,
|
||||
error,
|
||||
success,
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:krow/core/data/models/event/business_member_model.dart';
|
||||
|
||||
part 'client.g.dart';
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||
class ClientModel {
|
||||
String id;
|
||||
String firstName;
|
||||
String lastName;
|
||||
String? avatar;
|
||||
|
||||
AuthInfo? authInfo;
|
||||
|
||||
ClientModel(
|
||||
this.firstName,
|
||||
this.lastName,
|
||||
this.avatar,
|
||||
this.id,
|
||||
this.authInfo,
|
||||
);
|
||||
|
||||
factory ClientModel.fromJson(Map<String, dynamic> json) {
|
||||
return _$ClientModelFromJson(json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$ClientModelToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'addon_model.g.dart';
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||
class AddonModel {
|
||||
String id;
|
||||
String? name;
|
||||
int? price;
|
||||
|
||||
AddonModel({required this.id, this.name, this.price});
|
||||
|
||||
factory AddonModel.fromJson(Map<String, dynamic> json) {
|
||||
return _$AddonModelFromJson(json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$AddonModelToJson(this);
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode ^ name.hashCode ^ price.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! AddonModel) return false;
|
||||
return id == other.id && name == other.name && price == other.price;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'business_contact_model.g.dart';
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||
class BusinessContactModel{
|
||||
final String id;
|
||||
final String? firstName;
|
||||
final String? lastName;
|
||||
final String? email;
|
||||
final String? phone;
|
||||
final String? address;
|
||||
|
||||
BusinessContactModel({
|
||||
required this.id,
|
||||
this.firstName,
|
||||
this.lastName,
|
||||
this.email,
|
||||
this.phone,
|
||||
this.address,
|
||||
});
|
||||
|
||||
factory BusinessContactModel.fromJson(Map<String, dynamic> json) {
|
||||
return _$BusinessContactModelFromJson(json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$BusinessContactModelToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'business_member_model.g.dart';
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||
class BusinessMemberModel {
|
||||
final String id;
|
||||
final String firstName;
|
||||
final String lastName;
|
||||
final String title;
|
||||
final AuthInfo? authInfo;
|
||||
|
||||
BusinessMemberModel({
|
||||
required this.id,
|
||||
required this.firstName,
|
||||
required this.lastName,
|
||||
required this.title,
|
||||
this.authInfo,
|
||||
});
|
||||
|
||||
factory BusinessMemberModel.fromJson(Map<String, dynamic> json) {
|
||||
return _$BusinessMemberModelFromJson(json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$BusinessMemberModelToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||
class AuthInfo {
|
||||
final String email;
|
||||
final String phone;
|
||||
|
||||
AuthInfo({
|
||||
required this.email,
|
||||
required this.phone,
|
||||
});
|
||||
|
||||
factory AuthInfo.fromJson(Map<String, dynamic> json) {
|
||||
return _$AuthInfoFromJson(json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$AuthInfoToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:krow/core/data/models/event/addon_model.dart';
|
||||
import 'package:krow/core/data/models/event/business_contact_model.dart';
|
||||
|
||||
part 'business_model.g.dart';
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||
class BusinessModel {
|
||||
String? name;
|
||||
String? avatar;
|
||||
String? registration;
|
||||
List<AddonModel>? addons;
|
||||
BusinessContactModel? contact;
|
||||
|
||||
BusinessModel({this.name, this.avatar, this.addons, this.registration, this.contact});
|
||||
|
||||
factory BusinessModel.fromJson(Map<String, dynamic> json) {
|
||||
return _$BusinessModelFromJson(json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$BusinessModelToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:krow/core/data/models/event/addon_model.dart';
|
||||
import 'package:krow/core/data/models/event/business_model.dart';
|
||||
import 'package:krow/core/data/models/event/hub_model.dart';
|
||||
import 'package:krow/core/data/models/event/tag_model.dart';
|
||||
import 'package:krow/core/data/models/shift/shift_model.dart';
|
||||
import 'package:krow/core/entity/event_entity.dart';
|
||||
|
||||
part 'event_model.g.dart';
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||
class EventModel {
|
||||
final String id;
|
||||
final BusinessModel? business;
|
||||
final HubModel? hub;
|
||||
final String name;
|
||||
@JsonKey(unknownEnumValue: EventStatus.draft)
|
||||
final EventStatus status;
|
||||
final String date;
|
||||
final String startTime;
|
||||
final String endTime;
|
||||
final String? purchaseOrder;
|
||||
// @JsonKey(unknownEnumValue: EventContractType.direct)
|
||||
// final EventContractType contractType;
|
||||
@JsonKey(unknownEnumValue: EventScheduleType.oneTime)
|
||||
final EventScheduleType? scheduleType;
|
||||
final String? additionalInfo;
|
||||
@JsonKey(defaultValue: [])
|
||||
final List<AddonModel> addons;
|
||||
@JsonKey(defaultValue: [])
|
||||
final List<TagModel> tags;
|
||||
@JsonKey(defaultValue: [])
|
||||
final List<ShiftModel> shifts;
|
||||
|
||||
EventModel(
|
||||
{required this.id,
|
||||
required this.business,
|
||||
required this.hub,
|
||||
required this.name,
|
||||
required this.status,
|
||||
required this.date,
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
required this.purchaseOrder,
|
||||
// required this.contractType,
|
||||
required this.scheduleType,
|
||||
required this.additionalInfo,
|
||||
required this.addons,
|
||||
required this.tags,
|
||||
required this.shifts});
|
||||
|
||||
factory EventModel.fromJson(Map<String, dynamic> json) {
|
||||
return _$EventModelFromJson(json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$EventModelToJson(this);
|
||||
}
|
||||
|
||||
@JsonEnum(fieldRename: FieldRename.snake)
|
||||
enum EventContractType {
|
||||
direct,
|
||||
contract,
|
||||
purchaseOrder,
|
||||
}
|
||||
|
||||
@JsonEnum(fieldRename: FieldRename.snake)
|
||||
enum EventScheduleType {
|
||||
oneTime,
|
||||
recurring,
|
||||
}
|
||||
|
||||
extension on EventScheduleType {
|
||||
String get formattedName {
|
||||
return switch (this) {
|
||||
EventScheduleType.oneTime => 'One Time',
|
||||
EventScheduleType.recurring => 'Recurring'
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'full_address_model.g.dart';
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||
class FullAddress {
|
||||
String? streetNumber;
|
||||
String? zipCode;
|
||||
double? latitude;
|
||||
double? longitude;
|
||||
String? formattedAddress;
|
||||
String? street;
|
||||
String? region;
|
||||
String? city;
|
||||
String? country;
|
||||
|
||||
FullAddress({
|
||||
this.streetNumber,
|
||||
this.zipCode,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
this.formattedAddress,
|
||||
this.street,
|
||||
this.region,
|
||||
this.city,
|
||||
this.country,
|
||||
});
|
||||
|
||||
factory FullAddress.fromJson(Map<String, dynamic> json) {
|
||||
return _$FullAddressFromJson(json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$FullAddressToJson(this);
|
||||
|
||||
static FullAddress fromGoogle(Map<String, dynamic> fullAddress) {
|
||||
return FullAddress(
|
||||
streetNumber: fullAddress['street_number'],
|
||||
zipCode: fullAddress['postal_code'],
|
||||
latitude: fullAddress['lat'],
|
||||
longitude: fullAddress['lng'],
|
||||
formattedAddress: fullAddress['formatted_address'],
|
||||
street: fullAddress['street'],
|
||||
region: fullAddress['state'],
|
||||
city: fullAddress['city'],
|
||||
country: fullAddress['country'],
|
||||
);
|
||||
}
|
||||
|
||||
bool isValid() {
|
||||
return formattedAddress != null &&
|
||||
latitude != null &&
|
||||
longitude != null &&
|
||||
streetNumber != null &&
|
||||
city != null &&
|
||||
street != null &&
|
||||
region != null &&
|
||||
zipCode != null &&
|
||||
country != null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:krow/core/data/models/event/full_address_model.dart';
|
||||
|
||||
part 'hub_model.g.dart';
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||
class HubModel {
|
||||
String id;
|
||||
String? name;
|
||||
String? address;
|
||||
FullAddress? fullAddress;
|
||||
|
||||
HubModel({required this.id, this.name, this.address, this.fullAddress});
|
||||
|
||||
factory HubModel.fromJson(Map<String, dynamic> json) {
|
||||
return _$HubModelFromJson(json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$HubModelToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'role_kit.g.dart';
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||
class RoleKit {
|
||||
final String id;
|
||||
final String? name;
|
||||
final bool? isRequired;
|
||||
final bool? photoRequired;
|
||||
|
||||
RoleKit({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.isRequired,
|
||||
required this.photoRequired,
|
||||
});
|
||||
|
||||
factory RoleKit.fromJson(Map<String, dynamic> json) =>
|
||||
_$RoleKitFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$RoleKitToJson(this);
|
||||
}
|
||||
30
mobile-apps/client-app/lib/core/data/models/event/skill.dart
Normal file
30
mobile-apps/client-app/lib/core/data/models/event/skill.dart
Normal file
@@ -0,0 +1,30 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:krow/core/data/models/event/role_kit.dart';
|
||||
import 'package:krow/core/data/models/event/skill_category.dart';
|
||||
|
||||
part 'skill.g.dart';
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||
class Skill {
|
||||
final String? id;
|
||||
final String? name;
|
||||
final String? slug;
|
||||
final double? price;
|
||||
final List<RoleKit>? uniforms;
|
||||
final List<RoleKit>? equipments;
|
||||
final SkillCategory? category;
|
||||
|
||||
Skill({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.slug,
|
||||
required this.price,
|
||||
required this.uniforms,
|
||||
required this.equipments,
|
||||
this.category,
|
||||
});
|
||||
|
||||
factory Skill.fromJson(Map<String, dynamic> json) => _$SkillFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$SkillToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'skill_category.g.dart';
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||
class SkillCategory {
|
||||
final String name;
|
||||
final String slug;
|
||||
|
||||
SkillCategory({
|
||||
required this.name,
|
||||
required this.slug,
|
||||
});
|
||||
|
||||
factory SkillCategory.fromJson(Map<String, dynamic> json) =>
|
||||
_$SkillCategoryFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$SkillCategoryToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'tag_model.g.dart';
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||
class TagModel {
|
||||
String id;
|
||||
String name;
|
||||
|
||||
TagModel({required this.id, required this.name});
|
||||
|
||||
factory TagModel.fromJson(Map<String, dynamic> json) {
|
||||
return _$TagModelFromJson(json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$TagModelToJson(this);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'TagModel{id: $id, name: $name}';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is TagModel && other.id == id && other.name == name;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return id.hashCode ^ name.hashCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'pagination_wrapper.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class PageInfo {
|
||||
final bool hasNextPage;
|
||||
final bool? hasPreviousPage;
|
||||
final String? startCursor;
|
||||
final String? endCursor;
|
||||
|
||||
PageInfo({
|
||||
required this.hasNextPage,
|
||||
required this.hasPreviousPage,
|
||||
this.startCursor,
|
||||
this.endCursor,
|
||||
});
|
||||
|
||||
factory PageInfo.fromJson(Map<String, dynamic> json) {
|
||||
return _$PageInfoFromJson(json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$PageInfoToJson(this);
|
||||
}
|
||||
|
||||
class Edge<T> {
|
||||
final String cursor;
|
||||
final T node;
|
||||
|
||||
Edge({
|
||||
required this.cursor,
|
||||
required this.node,
|
||||
});
|
||||
|
||||
factory Edge.fromJson(Map<String, dynamic> json) {
|
||||
return Edge(
|
||||
cursor: json['cursor'],
|
||||
node: json['node'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PaginationWrapper<T> {
|
||||
final PageInfo pageInfo;
|
||||
final List<Edge<T>> edges;
|
||||
|
||||
PaginationWrapper({
|
||||
required this.pageInfo,
|
||||
required this.edges,
|
||||
});
|
||||
|
||||
factory PaginationWrapper.fromJson(
|
||||
Map<String, dynamic> json, T Function(Map<String, dynamic>)? convertor) {
|
||||
return PaginationWrapper(
|
||||
pageInfo: PageInfo.fromJson(json['pageInfo']),
|
||||
edges: (json['edges'] as List).map((e) {
|
||||
return Edge(cursor: e['cursor'], node: convertor?.call(e['node']) as T);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:krow/core/data/models/event/skill.dart';
|
||||
|
||||
part 'business_skill_model.g.dart';
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||
class BusinessSkillModel {
|
||||
final String? id;
|
||||
final Skill? skill;
|
||||
final double? price;
|
||||
final bool? isActive;
|
||||
|
||||
BusinessSkillModel({
|
||||
required this.id,
|
||||
required this.skill,
|
||||
this.price = 0,
|
||||
this.isActive = true,
|
||||
});
|
||||
|
||||
factory BusinessSkillModel.fromJson(Map<String, dynamic> json) {
|
||||
try {
|
||||
return _$BusinessSkillModelFromJson(json);
|
||||
} catch (e) {
|
||||
return BusinessSkillModel(
|
||||
id: '',
|
||||
skill: null,
|
||||
price: 0.0,
|
||||
isActive: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$BusinessSkillModelToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'department_model.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class DepartmentModel {
|
||||
final String id;
|
||||
final String name;
|
||||
|
||||
DepartmentModel({
|
||||
required this.id,
|
||||
required this.name,
|
||||
});
|
||||
|
||||
factory DepartmentModel.fromJson(Map<String, dynamic> json) {
|
||||
return _$DepartmentModelFromJson(json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$DepartmentModelToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:krow/core/data/models/shift/business_skill_model.dart';
|
||||
import 'package:krow/core/data/models/shift/department_model.dart';
|
||||
import 'package:krow/core/data/models/staff/staff_model.dart';
|
||||
|
||||
part 'event_shift_position_model.g.dart';
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||
class EventShiftPositionModel {
|
||||
final String id;
|
||||
|
||||
final int count;
|
||||
final String startTime;
|
||||
final String endTime;
|
||||
final double rate;
|
||||
@JsonKey(name: 'break')
|
||||
final int breakTime;
|
||||
final BusinessSkillModel businessSkill;
|
||||
final List<StaffModel>? staff;
|
||||
final DepartmentModel? department;
|
||||
|
||||
EventShiftPositionModel({
|
||||
required this.id,
|
||||
required this.count,
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
required this.rate,
|
||||
required this.breakTime,
|
||||
required this.businessSkill,
|
||||
required this.staff,
|
||||
required this.department,
|
||||
});
|
||||
|
||||
factory EventShiftPositionModel.fromJson(Map<String, dynamic> json) {
|
||||
return _$EventShiftPositionModelFromJson(json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$EventShiftPositionModelToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:krow/core/data/models/event/business_member_model.dart';
|
||||
import 'package:krow/core/data/models/event/full_address_model.dart';
|
||||
import 'package:krow/core/data/models/shift/event_shift_position_model.dart';
|
||||
|
||||
part 'shift_model.g.dart';
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||
class ShiftModel {
|
||||
final String id;
|
||||
final String name;
|
||||
final FullAddress fullAddress;
|
||||
final List<BusinessMemberModel>? contacts;
|
||||
final List<EventShiftPositionModel>? positions;
|
||||
|
||||
ShiftModel({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.fullAddress,
|
||||
required this.contacts,
|
||||
required this.positions,
|
||||
});
|
||||
|
||||
factory ShiftModel.fromJson(Map<String, dynamic> json) {
|
||||
return _$ShiftModelFromJson(json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$ShiftModelToJson(this);
|
||||
}
|
||||
99
mobile-apps/client-app/lib/core/data/models/staff/pivot.dart
Normal file
99
mobile-apps/client-app/lib/core/data/models/staff/pivot.dart
Normal file
@@ -0,0 +1,99 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:krow/core/data/models/shift/event_shift_position_model.dart';
|
||||
import 'package:krow/core/data/models/staff/staff_cancel_reason.dart';
|
||||
|
||||
part 'pivot.g.dart';
|
||||
|
||||
@JsonEnum(fieldRename: FieldRename.snake)
|
||||
enum PivotStatus {
|
||||
assigned,
|
||||
confirmed,
|
||||
ongoing,
|
||||
completed,
|
||||
declineByStaff,
|
||||
canceledByStaff,
|
||||
canceledByBusiness,
|
||||
canceledByAdmin,
|
||||
requestedReplace,
|
||||
noShowed,
|
||||
|
||||
}
|
||||
|
||||
extension PivotStatusToString on PivotStatus {
|
||||
String get formattedName {
|
||||
switch (this) {
|
||||
case PivotStatus.assigned:
|
||||
return 'Assigned';
|
||||
case PivotStatus.confirmed:
|
||||
return 'Confirmed';
|
||||
case PivotStatus.ongoing:
|
||||
return 'Ongoing';
|
||||
case PivotStatus.completed:
|
||||
return 'Completed';
|
||||
case PivotStatus.declineByStaff:
|
||||
return 'Declined by Staff';
|
||||
case PivotStatus.canceledByStaff:
|
||||
return 'Canceled by Staff';
|
||||
case PivotStatus.canceledByBusiness:
|
||||
return 'Canceled by Business';
|
||||
case PivotStatus.canceledByAdmin:
|
||||
return 'Canceled by Admin';
|
||||
case PivotStatus.requestedReplace:
|
||||
return 'Requested Replace';
|
||||
case PivotStatus.noShowed:
|
||||
return 'No Showed';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||
class Pivot {
|
||||
String id;
|
||||
@JsonKey(unknownEnumValue: PivotStatus.assigned)
|
||||
PivotStatus status;
|
||||
String? statusUpdatedAt;
|
||||
String startAt;
|
||||
String endAt;
|
||||
String? clockIn;
|
||||
String? clockOut;
|
||||
String? breakIn;
|
||||
String? breakOut;
|
||||
EventShiftPositionModel? position;
|
||||
List<StaffCancelReason>? cancelReason;
|
||||
Rating? rating;
|
||||
|
||||
Pivot({
|
||||
required this.id,
|
||||
required this.status,
|
||||
this.statusUpdatedAt,
|
||||
required this.startAt,
|
||||
required this.endAt,
|
||||
this.clockIn,
|
||||
this.clockOut,
|
||||
this.breakIn,
|
||||
this.breakOut,
|
||||
this.position,
|
||||
this.cancelReason,
|
||||
});
|
||||
|
||||
factory Pivot.fromJson(Map<String, dynamic> json) {
|
||||
return _$PivotFromJson(json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$PivotToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||
class Rating {
|
||||
final String id;
|
||||
final double rating;
|
||||
|
||||
Rating({required this.id, required this.rating});
|
||||
|
||||
factory Rating.fromJson(Map<String, dynamic> json) {
|
||||
return _$RatingFromJson(json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$RatingToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'staff_cancel_reason.g.dart';
|
||||
|
||||
@JsonEnum(fieldRename: FieldRename.snake)
|
||||
enum StaffCancelReasonType {
|
||||
cancelShift,
|
||||
declineShift,
|
||||
noBreak
|
||||
}
|
||||
|
||||
@JsonEnum(fieldRename: FieldRename.snake,)
|
||||
enum ShiftCancelReason {
|
||||
sickLeave,
|
||||
vacation,
|
||||
other,
|
||||
health,
|
||||
transportation,
|
||||
personal,
|
||||
scheduleConflict,
|
||||
}
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||
class StaffCancelReason {
|
||||
@JsonKey(unknownEnumValue: StaffCancelReasonType.cancelShift)
|
||||
final StaffCancelReasonType? type;
|
||||
@JsonKey(unknownEnumValue: ShiftCancelReason.sickLeave)
|
||||
final ShiftCancelReason? reason;
|
||||
final String? details;
|
||||
|
||||
StaffCancelReason(
|
||||
{required this.type, required this.reason, required this.details});
|
||||
|
||||
factory StaffCancelReason.fromJson(Map<String, dynamic> json) {
|
||||
return _$StaffCancelReasonFromJson(json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$StaffCancelReasonToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:krow/core/data/models/staff/pivot.dart';
|
||||
|
||||
part 'staff_model.g.dart';
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||
class StaffModel {
|
||||
String? id;
|
||||
String? firstName;
|
||||
String? lastName;
|
||||
String? email;
|
||||
String? phone;
|
||||
String? avatar;
|
||||
Pivot? pivot;
|
||||
|
||||
StaffModel({
|
||||
required this.id,
|
||||
required this.firstName,
|
||||
required this.lastName,
|
||||
required this.email,
|
||||
required this.phone,
|
||||
required this.avatar,
|
||||
required this.pivot,
|
||||
});
|
||||
|
||||
factory StaffModel.fromJson(Map<String, dynamic> json) {
|
||||
return _$StaffModelFromJson(json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$StaffModelToJson(this);
|
||||
}
|
||||
231
mobile-apps/client-app/lib/core/entity/event_entity.dart
Normal file
231
mobile-apps/client-app/lib/core/entity/event_entity.dart
Normal file
@@ -0,0 +1,231 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:krow/core/data/models/event/addon_model.dart';
|
||||
import 'package:krow/core/data/models/event/business_member_model.dart';
|
||||
import 'package:krow/core/data/models/event/event_model.dart';
|
||||
import 'package:krow/core/data/models/event/hub_model.dart';
|
||||
import 'package:krow/core/data/models/event/tag_model.dart';
|
||||
import 'package:krow/core/entity/position_entity.dart';
|
||||
import 'package:krow/core/entity/shift_entity.dart';
|
||||
|
||||
enum EventStatus {
|
||||
pending,
|
||||
assigned,
|
||||
confirmed,
|
||||
active,
|
||||
finished,
|
||||
completed,
|
||||
closed,
|
||||
canceled,
|
||||
draft
|
||||
}
|
||||
|
||||
class EventEntity {
|
||||
final String id;
|
||||
final EventStatus? status;
|
||||
final String name;
|
||||
DateTime? startDate;
|
||||
DateTime? endDate;
|
||||
final String? cursor;
|
||||
final ValueNotifier<double> totalCost = ValueNotifier(0.0);
|
||||
final HubModel? hub;
|
||||
final List<AddonModel>? addons;
|
||||
final BusinessMemberModel? completedBy;
|
||||
final String? completedNode;
|
||||
final String? additionalInfo;
|
||||
|
||||
// final EventContractType contractType;
|
||||
final List<ShiftEntity>? shifts;
|
||||
final String? poNumber;
|
||||
|
||||
// final String? contractNumber;
|
||||
final EventModel? dto;
|
||||
final List<TagModel>? tags;
|
||||
|
||||
EventEntity(
|
||||
{required this.id,
|
||||
this.status,
|
||||
required this.name,
|
||||
required this.startDate,
|
||||
required this.endDate,
|
||||
this.completedBy,
|
||||
this.completedNode,
|
||||
this.hub,
|
||||
this.addons,
|
||||
this.additionalInfo,
|
||||
// this.contractType = EventContractType.direct,
|
||||
this.shifts,
|
||||
this.cursor,
|
||||
this.poNumber,
|
||||
// this.contractNumber,
|
||||
this.dto,
|
||||
this.tags});
|
||||
|
||||
static EventEntity fromEventDto(EventModel event, {String? cursor}) {
|
||||
|
||||
var date = DateFormat('yyyy-MM-dd').parse(event.date);
|
||||
|
||||
var entity = EventEntity(
|
||||
id: event.id,
|
||||
status: event.status,
|
||||
name: event.name,
|
||||
startDate: date,
|
||||
endDate: date,
|
||||
completedBy: null,
|
||||
completedNode: null,
|
||||
hub: event.hub,
|
||||
shifts: event.shifts
|
||||
.map<ShiftEntity>((shift) => ShiftEntity.fromDto(shift, date))
|
||||
.toList(),
|
||||
addons: event.addons,
|
||||
additionalInfo: event.additionalInfo,
|
||||
// contractType: event.contractType,
|
||||
poNumber: event.purchaseOrder,
|
||||
cursor: cursor,
|
||||
dto: event,
|
||||
tags: event.tags);
|
||||
|
||||
entity.totalCost.value =
|
||||
EventEntity.getTotalCost(entity); // Calculate total cost
|
||||
|
||||
try {
|
||||
entity.shifts?.forEach((element) {
|
||||
element.parentEvent = entity;
|
||||
});
|
||||
} catch (e) {
|
||||
print(e);
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
EventEntity copyWith(
|
||||
{String? id,
|
||||
EventStatus? status,
|
||||
String? name,
|
||||
DateTime? startDate,
|
||||
DateTime? endDate,
|
||||
BusinessMemberModel? completedBy,
|
||||
String? completedNode,
|
||||
HubModel? hub,
|
||||
List<AddonModel>? addons,
|
||||
String? additionalInfo,
|
||||
EventContractType? contractType,
|
||||
List<ShiftEntity>? shifts,
|
||||
String? poNumber,
|
||||
String? contractNumber,
|
||||
String? cursor,
|
||||
List<TagModel>? tags}) {
|
||||
var entity = EventEntity(
|
||||
id: id ?? this.id,
|
||||
status: status ?? this.status,
|
||||
name: name ?? this.name,
|
||||
startDate: startDate ?? this.startDate,
|
||||
endDate: endDate ?? this.endDate,
|
||||
hub: hub ?? this.hub,
|
||||
completedBy: completedBy ?? this.completedBy,
|
||||
completedNode: completedNode ?? this.completedNode,
|
||||
addons: addons ?? this.addons,
|
||||
additionalInfo: additionalInfo ?? this.additionalInfo,
|
||||
// contractType: contractType ?? this.contractType,
|
||||
shifts: shifts ?? this.shifts,
|
||||
poNumber: poNumber ?? this.poNumber,
|
||||
// contractNumber: contractNumber ?? this.contractNumber,
|
||||
cursor: cursor ?? this.cursor,
|
||||
tags: tags ?? this.tags);
|
||||
|
||||
entity.totalCost.value =
|
||||
EventEntity.getTotalCost(entity); // Calculate total cost
|
||||
|
||||
entity.shifts?.forEach((element) {
|
||||
element.parentEvent = entity;
|
||||
});
|
||||
return entity;
|
||||
}
|
||||
|
||||
static empty() {
|
||||
var entity = EventEntity(
|
||||
id: '',
|
||||
name: '',
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
hub: null,
|
||||
completedBy: null,
|
||||
completedNode: null,
|
||||
addons: [],
|
||||
additionalInfo: '',
|
||||
// contractType: EventContractType.direct,
|
||||
shifts: [ShiftEntity.empty()],
|
||||
poNumber: '',
|
||||
// contractNumber: '',
|
||||
cursor: null,
|
||||
tags: []);
|
||||
entity.shifts?.forEach((element) {
|
||||
element.parentEvent = entity;
|
||||
});
|
||||
return entity;
|
||||
}
|
||||
|
||||
static double getTotalCost(EventEntity event) {
|
||||
foldPosition(previousValue, PositionEntity position) {
|
||||
return (previousValue ?? 0) +
|
||||
((((position.businessSkill?.price ?? 0) * (position.count ?? 0)) /
|
||||
60) *
|
||||
(position.endTime.difference(position.startTime).inMinutes));
|
||||
}
|
||||
|
||||
foldRoles(previousValue, ShiftEntity shift) {
|
||||
return previousValue + (shift.positions.fold(0.0, foldPosition));
|
||||
}
|
||||
|
||||
var eventCost = event.shifts?.fold(0.0, foldRoles) ?? 0;
|
||||
|
||||
double totalCost = eventCost +
|
||||
(event.addons?.fold(0.0,
|
||||
(previousValue, addon) => previousValue + (addon.price ?? 0)) ??
|
||||
0);
|
||||
|
||||
return totalCost;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
id.hashCode ^
|
||||
status.hashCode ^
|
||||
name.hashCode ^
|
||||
startDate.hashCode ^
|
||||
endDate.hashCode ^
|
||||
cursor.hashCode ^
|
||||
totalCost.hashCode ^
|
||||
hub.hashCode ^
|
||||
addons.hashCode ^
|
||||
completedBy.hashCode ^
|
||||
completedNode.hashCode ^
|
||||
additionalInfo.hashCode ^
|
||||
shifts.hashCode ^
|
||||
poNumber.hashCode ^
|
||||
dto.hashCode ^
|
||||
tags.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return super == (other) &&
|
||||
other is EventEntity &&
|
||||
id == other.id &&
|
||||
status == other.status &&
|
||||
name == other.name &&
|
||||
startDate == other.startDate &&
|
||||
endDate == other.endDate &&
|
||||
cursor == other.cursor &&
|
||||
totalCost.value == other.totalCost.value &&
|
||||
hub == other.hub &&
|
||||
addons == other.addons &&
|
||||
completedBy == other.completedBy &&
|
||||
completedNode == other.completedNode &&
|
||||
additionalInfo == other.additionalInfo &&
|
||||
shifts == other.shifts &&
|
||||
poNumber == other.poNumber &&
|
||||
dto == other.dto &&
|
||||
tags == other.tags;
|
||||
}
|
||||
}
|
||||
221
mobile-apps/client-app/lib/core/entity/position_entity.dart
Normal file
221
mobile-apps/client-app/lib/core/entity/position_entity.dart
Normal file
@@ -0,0 +1,221 @@
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:krow/core/data/models/shift/business_skill_model.dart';
|
||||
import 'package:krow/core/data/models/shift/department_model.dart';
|
||||
import 'package:krow/core/data/models/shift/event_shift_position_model.dart';
|
||||
import 'package:krow/core/data/models/staff/pivot.dart';
|
||||
import 'package:krow/core/entity/event_entity.dart';
|
||||
import 'package:krow/core/entity/role_schedule_entity.dart';
|
||||
import 'package:krow/core/entity/shift_entity.dart';
|
||||
import 'package:krow/core/entity/staff_contact_entity.dart';
|
||||
|
||||
class PositionEntity {
|
||||
final String id;
|
||||
final DepartmentModel? department;
|
||||
final int? count;
|
||||
final int? breakDuration;
|
||||
final DateTime startTime;
|
||||
final DateTime endTime;
|
||||
final double price;
|
||||
final List<StaffContact> staffContacts;
|
||||
final EventShiftPositionModel? dto;
|
||||
final BusinessSkillModel? businessSkill;
|
||||
final List<RoleScheduleEntity>? schedule;
|
||||
ShiftEntity? parentShift;
|
||||
|
||||
PositionEntity(
|
||||
{required this.id,
|
||||
this.department,
|
||||
this.count,
|
||||
this.breakDuration = 15,
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
this.price = 0,
|
||||
this.staffContacts = const [],
|
||||
this.schedule,
|
||||
this.businessSkill,
|
||||
this.dto});
|
||||
|
||||
PositionEntity copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
DepartmentModel? department,
|
||||
int? count,
|
||||
DateTime? startTime,
|
||||
DateTime? endTime,
|
||||
int? price,
|
||||
int? breakDuration,
|
||||
List<StaffContact>? staffContacts,
|
||||
BusinessSkillModel? businessSkill,
|
||||
EventShiftPositionModel? dto,
|
||||
List<RoleScheduleEntity>? schedule,
|
||||
}) {
|
||||
if (schedule != null) {
|
||||
schedule.sort((a, b) => a.dayIndex.compareTo(b.dayIndex));
|
||||
}
|
||||
|
||||
var timeResult = _calcTimeSlot(startTime, endTime);
|
||||
|
||||
var start = timeResult.start;
|
||||
var end = timeResult.end;
|
||||
businessSkill = businessSkill ?? this.businessSkill;
|
||||
|
||||
var newEntity = PositionEntity(
|
||||
id: id ?? this.id,
|
||||
department: department ?? this.department,
|
||||
count: count ?? this.count,
|
||||
breakDuration: breakDuration ?? this.breakDuration,
|
||||
startTime: start,
|
||||
endTime: end,
|
||||
price: _getTotalCost(
|
||||
businessSkill?.price ?? 0, start, end, count ?? this.count ?? 0),
|
||||
staffContacts: staffContacts ?? this.staffContacts,
|
||||
businessSkill: businessSkill,
|
||||
dto: dto ?? this.dto,
|
||||
schedule: schedule ?? this.schedule,
|
||||
);
|
||||
|
||||
newEntity.parentShift = parentShift;
|
||||
int index =
|
||||
parentShift?.positions.indexWhere((item) => item.id == this.id) ?? -1;
|
||||
if (index != -1) {
|
||||
parentShift?.positions[index] = newEntity;
|
||||
}
|
||||
|
||||
var event = parentShift?.parentEvent;
|
||||
event?.totalCost.value = EventEntity.getTotalCost(event);
|
||||
|
||||
return newEntity;
|
||||
}
|
||||
|
||||
({DateTime start, DateTime end}) _calcTimeSlot(
|
||||
DateTime? startTime, DateTime? endTime) {
|
||||
if (startTime != null) {
|
||||
startTime = startTime.copyWith(
|
||||
day: this.endTime.day,
|
||||
month: this.endTime.month,
|
||||
year: this.endTime.year);
|
||||
} else if (endTime != null) {
|
||||
endTime = endTime.copyWith(
|
||||
day: this.startTime.day,
|
||||
month: this.startTime.month,
|
||||
year: this.startTime.year);
|
||||
}
|
||||
|
||||
const Duration minDuration = Duration(hours: 5);
|
||||
DateTime? updatedStartTime = startTime ?? this.startTime;
|
||||
DateTime? updatedEndTime = endTime ?? this.endTime;
|
||||
|
||||
if (startTime != null && this.startTime != startTime) {
|
||||
Duration diff = updatedEndTime.difference(startTime);
|
||||
if (diff < minDuration) {
|
||||
updatedEndTime = startTime.add(minDuration);
|
||||
}
|
||||
} else if (endTime != null && this.endTime != endTime) {
|
||||
Duration diff = endTime.difference(updatedStartTime);
|
||||
if (diff < minDuration) {
|
||||
updatedStartTime = endTime.subtract(minDuration);
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedStartTime.day != updatedEndTime.day) {
|
||||
final DateTime midnight = DateTime(
|
||||
updatedStartTime.year,
|
||||
updatedStartTime.month,
|
||||
updatedStartTime.day,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
updatedStartTime = midnight.subtract(const Duration(hours: 5));
|
||||
updatedEndTime = midnight;
|
||||
}
|
||||
|
||||
({DateTime start, DateTime end}) timeResult =
|
||||
(start: updatedStartTime, end: updatedEndTime);
|
||||
return timeResult;
|
||||
}
|
||||
|
||||
static PositionEntity fromDto(EventShiftPositionModel model, DateTime date) {
|
||||
final DateFormat timeFormat = DateFormat('HH:mm');
|
||||
final DateTime start = timeFormat.parse(model.startTime);
|
||||
final DateTime end = timeFormat.parse(model.endTime);
|
||||
return PositionEntity(
|
||||
id: model.id,
|
||||
breakDuration: model.breakTime,
|
||||
department: model.department,
|
||||
dto: model,
|
||||
startTime: DateTime(date.year, date.month, date.day, start.hour,
|
||||
start.minute - start.minute % 10),
|
||||
endTime: DateTime(date.year, date.month, date.day, end.hour,
|
||||
end.minute - end.minute % 10),
|
||||
count: model.count,
|
||||
businessSkill: model.businessSkill,
|
||||
price: _getTotalCost(
|
||||
model.businessSkill.price ?? 0, start, end, model.count),
|
||||
staffContacts: model.staff?.map((e) {
|
||||
return StaffContact(
|
||||
id: e.pivot?.id ?? '',
|
||||
photoUrl: e.avatar ?? '',
|
||||
firstName: e.firstName ?? '',
|
||||
lastName: e.lastName ?? '',
|
||||
phoneNumber: e.phone ?? '',
|
||||
email: e.email ?? '',
|
||||
rate: model.businessSkill.price ?? 0,
|
||||
status: e.pivot?.status ?? PivotStatus.assigned,
|
||||
startAt: e.pivot?.startAt ?? '',
|
||||
endAt: e.pivot?.endAt ?? '',
|
||||
//todo
|
||||
isFavorite: false,
|
||||
isBlackListed: false,
|
||||
skillName: e.pivot?.position?.businessSkill.skill?.name ?? '',
|
||||
)..rating.value = e.pivot?.rating?.rating ?? 0;
|
||||
}).toList() ??
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
static empty() {
|
||||
return PositionEntity(
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
startTime: DateTime.now().copyWith(hour: 9, minute: 0),
|
||||
endTime: DateTime.now().copyWith(hour: 14, minute: 0),
|
||||
);
|
||||
}
|
||||
|
||||
static double _getTotalCost(
|
||||
double ratePerHour, DateTime startTime, DateTime endTime, int count) {
|
||||
final double hours = endTime.difference(startTime).inMinutes.abs() / 60.0;
|
||||
final double price = hours * ratePerHour;
|
||||
return price * count;
|
||||
}
|
||||
|
||||
@override
|
||||
// TODO: implement hashCode
|
||||
int get hashCode =>
|
||||
id.hashCode ^
|
||||
department.hashCode ^
|
||||
count.hashCode ^
|
||||
breakDuration.hashCode ^
|
||||
startTime.hashCode ^
|
||||
endTime.hashCode ^
|
||||
price.hashCode ^
|
||||
staffContacts.hashCode ^
|
||||
dto.hashCode ^
|
||||
businessSkill.hashCode ^
|
||||
schedule.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is PositionEntity &&
|
||||
id == other.id &&
|
||||
department == other.department &&
|
||||
count == other.count &&
|
||||
breakDuration == other.breakDuration &&
|
||||
startTime == other.startTime &&
|
||||
endTime == other.endTime &&
|
||||
price == other.price &&
|
||||
staffContacts == other.staffContacts &&
|
||||
dto == other.dto &&
|
||||
businessSkill == other.businessSkill &&
|
||||
schedule == other.schedule;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'package:krow/core/sevices/time_slot_service.dart';
|
||||
|
||||
class RoleScheduleEntity {
|
||||
final int dayIndex;
|
||||
final DateTime startTime;
|
||||
final DateTime endTime;
|
||||
|
||||
RoleScheduleEntity(
|
||||
{required this.dayIndex, required this.startTime, required this.endTime});
|
||||
|
||||
RoleScheduleEntity copyWith({
|
||||
int? dayIndex,
|
||||
DateTime? startTime,
|
||||
DateTime? endTime,
|
||||
}) {
|
||||
if (startTime != null) {
|
||||
startTime =
|
||||
startTime.copyWith(day: this.endTime.day, month: this.endTime.month);
|
||||
} else if (endTime != null) {
|
||||
endTime = endTime.copyWith(
|
||||
day: this.startTime.day, month: this.startTime.month);
|
||||
}
|
||||
|
||||
return RoleScheduleEntity(
|
||||
dayIndex: dayIndex ?? this.dayIndex,
|
||||
startTime: startTime ??
|
||||
TimeSlotService.calcTime(
|
||||
currentStartTime: this.startTime,
|
||||
currentEndTime: this.endTime,
|
||||
endTime: endTime ?? this.endTime),
|
||||
endTime: endTime ??
|
||||
TimeSlotService.calcTime(
|
||||
currentStartTime: this.startTime,
|
||||
currentEndTime: this.endTime,
|
||||
startTime: startTime ?? this.startTime),
|
||||
);
|
||||
}
|
||||
}
|
||||
99
mobile-apps/client-app/lib/core/entity/shift_entity.dart
Normal file
99
mobile-apps/client-app/lib/core/entity/shift_entity.dart
Normal file
@@ -0,0 +1,99 @@
|
||||
import 'package:krow/core/data/models/event/business_member_model.dart';
|
||||
import 'package:krow/core/data/models/event/full_address_model.dart';
|
||||
import 'package:krow/core/data/models/shift/shift_model.dart';
|
||||
import 'package:krow/core/entity/event_entity.dart';
|
||||
import 'package:krow/core/entity/position_entity.dart';
|
||||
|
||||
class ShiftEntity {
|
||||
final String id;
|
||||
final FullAddress? fullAddress;
|
||||
final List<BusinessMemberModel> managers;
|
||||
final List<PositionEntity> positions;
|
||||
EventEntity? parentEvent;
|
||||
|
||||
final ShiftModel? dto;
|
||||
|
||||
ShiftEntity({
|
||||
required this.id,
|
||||
this.fullAddress,
|
||||
this.managers = const [],
|
||||
this.positions = const [],
|
||||
this.dto,
|
||||
});
|
||||
|
||||
ShiftEntity copyWith({String? id,
|
||||
FullAddress? fullAddress,
|
||||
List<BusinessMemberModel>? managers,
|
||||
List<PositionEntity>? positions,
|
||||
ShiftModel? dto}) {
|
||||
var newEntity = ShiftEntity(
|
||||
id: id ?? this.id,
|
||||
fullAddress: fullAddress ?? this.fullAddress,
|
||||
managers: managers ?? this.managers,
|
||||
positions: positions ?? this.positions,
|
||||
dto: dto ?? this.dto,
|
||||
);
|
||||
newEntity.parentEvent = parentEvent;
|
||||
int index =
|
||||
parentEvent?.shifts?.indexWhere((item) => item.id == this.id) ?? -1;
|
||||
if (index != -1) {
|
||||
parentEvent?.shifts?[index] = newEntity;
|
||||
}
|
||||
return newEntity;
|
||||
}
|
||||
|
||||
static fromDto(ShiftModel model, DateTime date) {
|
||||
var entity = ShiftEntity(
|
||||
id: model.id,
|
||||
fullAddress: model.fullAddress,
|
||||
managers: model.contacts ?? [],
|
||||
positions: model.positions
|
||||
?.map<PositionEntity>((e) => PositionEntity.fromDto(e, date))
|
||||
.toList() ??
|
||||
[],
|
||||
dto: model,
|
||||
);
|
||||
|
||||
for (var element in entity.positions) {
|
||||
element.parentShift = entity;
|
||||
for (var contact in element.staffContacts) {
|
||||
contact.parentPosition = element;
|
||||
}
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
static empty() {
|
||||
var entity = ShiftEntity(
|
||||
id: DateTime
|
||||
.now()
|
||||
.millisecondsSinceEpoch
|
||||
.toString(),
|
||||
positions: [
|
||||
PositionEntity.empty(),
|
||||
]);
|
||||
|
||||
for (var element in entity.positions) {
|
||||
element.parentShift = entity;
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
id.hashCode ^ fullAddress.hashCode ^ managers.hashCode ^ positions
|
||||
.hashCode ^ (dto?.hashCode ?? 0);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
if (other is! ShiftEntity) return false;
|
||||
|
||||
return other.id == id &&
|
||||
other.fullAddress == fullAddress &&
|
||||
other.managers == managers &&
|
||||
other.positions == positions &&
|
||||
other.dto == dto;
|
||||
}
|
||||
}
|
||||
107
mobile-apps/client-app/lib/core/entity/staff_contact_entity.dart
Normal file
107
mobile-apps/client-app/lib/core/entity/staff_contact_entity.dart
Normal file
@@ -0,0 +1,107 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:krow/core/data/models/staff/pivot.dart';
|
||||
import 'package:krow/core/entity/position_entity.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
|
||||
class StaffContact {
|
||||
final String id;
|
||||
PivotStatus status;
|
||||
final String? photoUrl;
|
||||
final String firstName;
|
||||
final String lastName;
|
||||
final String? phoneNumber;
|
||||
final String? email;
|
||||
final double rate;
|
||||
final ValueNotifier<double> rating = ValueNotifier(0);
|
||||
final bool isFavorite;
|
||||
final bool isBlackListed;
|
||||
final String startAt;
|
||||
final String endAt;
|
||||
final String breakIn;
|
||||
final String breakOut;
|
||||
final String skillName;
|
||||
PositionEntity? parentPosition;
|
||||
|
||||
StaffContact(
|
||||
{required this.id,
|
||||
required this.firstName,
|
||||
required this.lastName,
|
||||
this.photoUrl,
|
||||
this.status = PivotStatus.assigned,
|
||||
this.phoneNumber,
|
||||
this.email,
|
||||
this.rate = 0,
|
||||
this.isFavorite = false,
|
||||
this.isBlackListed = false,
|
||||
this.startAt = '',
|
||||
this.endAt = '',
|
||||
this.breakIn = '',
|
||||
this.breakOut = '',
|
||||
this.parentPosition,
|
||||
this.skillName = ''});
|
||||
|
||||
StaffContact copyWith({
|
||||
String? id,
|
||||
PivotStatus? status,
|
||||
String? photoUrl,
|
||||
String? firstName,
|
||||
String? lastName,
|
||||
String? phoneNumber,
|
||||
String? email,
|
||||
double? rate,
|
||||
double? rating,
|
||||
bool? isFavorite,
|
||||
bool? isBlackListed,
|
||||
String? startAt,
|
||||
String? endAt,
|
||||
String? skillName,
|
||||
PositionEntity? parentPosition,
|
||||
}) {
|
||||
return StaffContact(
|
||||
id: id ?? this.id,
|
||||
status: status ?? this.status,
|
||||
photoUrl: photoUrl ?? this.photoUrl,
|
||||
firstName: firstName ?? this.firstName,
|
||||
lastName: lastName ?? this.lastName,
|
||||
phoneNumber: phoneNumber ?? this.phoneNumber,
|
||||
email: email ?? this.email,
|
||||
rate: rate ?? this.rate,
|
||||
isFavorite: isFavorite ?? this.isFavorite,
|
||||
isBlackListed: isBlackListed ?? this.isBlackListed,
|
||||
startAt: startAt ?? this.startAt,
|
||||
endAt: endAt ?? this.endAt,
|
||||
skillName: skillName ?? this.skillName,
|
||||
parentPosition: parentPosition ?? this.parentPosition,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension PivotStatusStatusX on PivotStatus {
|
||||
Color getStatusTextColor() {
|
||||
return switch (this) {
|
||||
PivotStatus.assigned || PivotStatus.confirmed => AppColors.primaryBlue,
|
||||
PivotStatus.ongoing || PivotStatus.completed => AppColors.statusSuccess,
|
||||
PivotStatus.canceledByStaff ||
|
||||
PivotStatus.canceledByBusiness ||
|
||||
PivotStatus.canceledByAdmin ||
|
||||
PivotStatus.requestedReplace ||
|
||||
PivotStatus.noShowed ||
|
||||
PivotStatus.declineByStaff =>
|
||||
AppColors.statusError
|
||||
};
|
||||
}
|
||||
|
||||
Color getStatusBorderColor() {
|
||||
return switch (this) {
|
||||
PivotStatus.assigned || PivotStatus.confirmed => AppColors.tintBlue,
|
||||
PivotStatus.ongoing || PivotStatus.completed => AppColors.tintGreen,
|
||||
PivotStatus.canceledByStaff ||
|
||||
PivotStatus.canceledByBusiness ||
|
||||
PivotStatus.canceledByAdmin ||
|
||||
PivotStatus.declineByStaff ||
|
||||
PivotStatus.noShowed ||
|
||||
PivotStatus.requestedReplace =>
|
||||
AppColors.tintRed
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
|
||||
abstract class KwBoxDecorations {
|
||||
static BoxDecoration primaryLight8 = BoxDecoration(
|
||||
color: AppColors.grayPrimaryFrame,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
);
|
||||
|
||||
static BoxDecoration primaryLight12 = BoxDecoration(
|
||||
color: AppColors.grayPrimaryFrame,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
);
|
||||
|
||||
static BoxDecoration white24 = BoxDecoration(
|
||||
color: AppColors.grayWhite,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
);
|
||||
|
||||
static BoxDecoration white12 = BoxDecoration(
|
||||
color: AppColors.grayWhite,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
);
|
||||
|
||||
static BoxDecoration white8 = BoxDecoration(
|
||||
color: AppColors.grayWhite,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:krow/core/presentation/gen/fonts.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
|
||||
abstract class AppTextStyles {
|
||||
static const TextStyle headingH0 = TextStyle(
|
||||
fontFamily: FontFamily.poppins,
|
||||
fontWeight: FontWeight.w600, // SemiBold
|
||||
fontSize: 48,
|
||||
color: AppColors.blackBlack);
|
||||
|
||||
static const TextStyle headingH1 = TextStyle(
|
||||
fontFamily: FontFamily.poppins,
|
||||
fontWeight: FontWeight.w600, // SemiBold
|
||||
fontSize: 28,
|
||||
height: 1,
|
||||
color: AppColors.blackBlack);
|
||||
|
||||
static const TextStyle headingH2 = TextStyle(
|
||||
fontFamily: FontFamily.poppins,
|
||||
fontWeight: FontWeight.w500, // Medium
|
||||
fontSize: 20,
|
||||
color: AppColors.blackBlack);
|
||||
|
||||
static const TextStyle headingH3 = TextStyle(
|
||||
fontFamily: FontFamily.poppins,
|
||||
fontWeight: FontWeight.w500, // Medium
|
||||
fontSize: 18,
|
||||
height: 1,
|
||||
color: AppColors.blackBlack);
|
||||
|
||||
static const TextStyle bodyLargeReg = TextStyle(
|
||||
fontFamily: FontFamily.poppins,
|
||||
fontWeight: FontWeight.w400,
|
||||
// Regular
|
||||
fontSize: 16,
|
||||
letterSpacing: -0.5,
|
||||
color: AppColors.blackBlack);
|
||||
|
||||
static const TextStyle bodyLargeMed = TextStyle(
|
||||
fontFamily: FontFamily.poppins,
|
||||
fontWeight: FontWeight.w500,
|
||||
// Medium
|
||||
fontSize: 16,
|
||||
letterSpacing: -0.5,
|
||||
color: AppColors.blackBlack);
|
||||
|
||||
static const TextStyle bodyMediumReg = TextStyle(
|
||||
fontFamily: FontFamily.poppins,
|
||||
fontWeight: FontWeight.w400,
|
||||
// Regular
|
||||
fontSize: 14,
|
||||
letterSpacing: -0.5,
|
||||
height: 1.2,
|
||||
color: AppColors.blackBlack);
|
||||
|
||||
static const TextStyle bodyMediumMed = TextStyle(
|
||||
fontFamily: FontFamily.poppins,
|
||||
fontWeight: FontWeight.w500,
|
||||
// Medium
|
||||
fontSize: 14,
|
||||
letterSpacing: -0.5,
|
||||
height: 1.2,
|
||||
color: AppColors.blackBlack);
|
||||
|
||||
static const TextStyle bodyMediumSmb = TextStyle(
|
||||
fontFamily: FontFamily.poppins,
|
||||
fontWeight: FontWeight.w600,
|
||||
// SemiBold
|
||||
letterSpacing: -0.5,
|
||||
fontSize: 14,
|
||||
height: 1.2,
|
||||
color: AppColors.blackBlack);
|
||||
|
||||
static const TextStyle bodySmallReg = TextStyle(
|
||||
fontFamily: FontFamily.poppins,
|
||||
fontWeight: FontWeight.w400,
|
||||
// Regular
|
||||
fontSize: 12,
|
||||
letterSpacing: -0.5,
|
||||
height: 1.3,
|
||||
color: AppColors.blackBlack);
|
||||
|
||||
static const TextStyle bodySmallMed = TextStyle(
|
||||
fontFamily: FontFamily.poppins,
|
||||
fontWeight: FontWeight.w500,
|
||||
// Medium
|
||||
fontSize: 12,
|
||||
letterSpacing: -0.5,
|
||||
color: AppColors.blackBlack);
|
||||
|
||||
static const TextStyle bodyTinyReg = TextStyle(
|
||||
fontFamily: FontFamily.poppins,
|
||||
fontWeight: FontWeight.w400,
|
||||
// Regular
|
||||
fontSize: 10,
|
||||
height: 1.2,
|
||||
color: AppColors.blackBlack);
|
||||
|
||||
static const TextStyle bodyTinyMed = TextStyle(
|
||||
fontFamily: FontFamily.poppins,
|
||||
fontWeight: FontWeight.w500,
|
||||
// Medium
|
||||
fontSize: 10,
|
||||
height: 1.2,
|
||||
color: AppColors.blackBlack);
|
||||
|
||||
static const TextStyle badgeRegular = TextStyle(
|
||||
fontFamily: FontFamily.poppins,
|
||||
fontWeight: FontWeight.w400, // Regular
|
||||
fontSize: 12,
|
||||
color: AppColors.blackBlack);
|
||||
|
||||
static const TextStyle captionReg = TextStyle(
|
||||
fontFamily: FontFamily.poppins,
|
||||
fontWeight: FontWeight.w400, // Regular
|
||||
fontSize: 10,
|
||||
color: AppColors.blackBlack);
|
||||
|
||||
static const TextStyle captionBold = TextStyle(
|
||||
fontFamily: FontFamily.poppins,
|
||||
fontWeight: FontWeight.w600, // SemiBold
|
||||
fontSize: 10,
|
||||
color: AppColors.blackBlack);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
abstract class AppColors {
|
||||
static const Color bgColorLight = Color(0xFFF0F2FF);
|
||||
static const Color bgColorDark = Color(0xFF040B45);
|
||||
static const Color blackBlack = Color(0xFF02071D);
|
||||
static const Color blackGray = Color(0xFF656773);
|
||||
static const Color blackCaptionText = Color(0xFFAEAEB1);
|
||||
static const Color blackCaptionBlue = Color(0xFF8485A4);
|
||||
static const Color primaryYellow = Color(0xFFFFF3A8);
|
||||
static const Color primaryYellowDark = Color(0xFFEEE39F);
|
||||
static const Color primaryYolk = Color(0xFFFFF321);
|
||||
static const Color primaryBlue = Color(0xFF002AE8);
|
||||
static const Color primaryMint = Color(0xFFDFEDE3);
|
||||
static const Color grayWhite = Color(0xFFFFFFFF);
|
||||
static const Color grayStroke = Color(0xFFA8AABD);
|
||||
static const Color grayDisable = Color(0xFFD8D9E0);
|
||||
static const Color grayPrimaryFrame = Color(0xFFFAFAFF);
|
||||
static const Color grayTintStroke = Color(0xFFE1E2E8);
|
||||
static const Color statusError = Color(0xFFF45E5E);
|
||||
static const Color statusSuccess = Color(0xFF14A858);
|
||||
static const Color statusWarning = Color(0xFFED7021);
|
||||
static const Color statusWarningBody = Color(0xFF906F07);
|
||||
static const Color statusRate = Color(0xFFF7CE39);
|
||||
static const Color tintGreen = Color(0xFFF3FCF7);
|
||||
static const Color tintDarkGreen = Color(0xFFC8EFDB);
|
||||
static const Color tintRed = Color(0xFFFEF2F2);
|
||||
static const Color tintYellow = Color(0xFFFEF7E0);
|
||||
static const Color tintBlue = Color(0xFFF0F3FF);
|
||||
static const Color tintDarkBlue = Color(0xFF7A88BE);
|
||||
static const Color tintGray = Color(0xFFEBEBEB);
|
||||
static const Color tintDarkRed = Color(0xFFFDB9B9);
|
||||
static const Color tintDropDownButton = Color(0xFFBEC5FE);
|
||||
static const Color tintOrange = Color(0xFFFAEBE3);
|
||||
static const Color navBarDisabled = Color(0xFF5C6081);
|
||||
static const Color darkBgBgElements = Color(0xFF1B1F41);
|
||||
static const Color darkBgActiveButtonState = Color(0xFF252A5A);
|
||||
static const Color darkBgStroke = Color(0xFF4E537E);
|
||||
static const Color darkBgInactive = Color(0xFF7A7FA9);
|
||||
static const Color buttonPrimaryYellowDrop = Color(0xFFFFEB6B);
|
||||
static const Color buttonPrimaryYellowActive = Color(0xFFFFF7C7);
|
||||
static const Color buttonPrimaryYellowActiveDrop = Color(0xFFFFF2A3);
|
||||
static const Color buttonOutline = Color(0xFFBEC5FE);
|
||||
static const Color buttonTertiaryActive = Color(0xFFEBEDFF);
|
||||
static const Color bgProfileCard = Color(0xff405FED);
|
||||
}
|
||||
|
||||
class KWTheme {
|
||||
static ThemeData get lightTheme {
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
scaffoldBackgroundColor: AppColors.bgColorLight,
|
||||
progressIndicatorTheme:
|
||||
const ProgressIndicatorThemeData(color: AppColors.bgColorDark),
|
||||
fontFamily: 'Poppins',
|
||||
//unused
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: AppColors.bgColorLight,
|
||||
),
|
||||
colorScheme: ColorScheme.fromSwatch().copyWith(
|
||||
secondary: AppColors.statusSuccess,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/application/common/str_extensions.dart';
|
||||
import 'package:krow/core/application/routing/routes.gr.dart';
|
||||
import 'package:krow/core/data/models/staff/pivot.dart';
|
||||
import 'package:krow/core/entity/event_entity.dart';
|
||||
import 'package:krow/core/entity/staff_contact_entity.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/staff_contact_info_popup.dart';
|
||||
|
||||
class AssignedStaffItemWidget extends StatelessWidget {
|
||||
final StaffContact staffContact;
|
||||
final String department;
|
||||
|
||||
const AssignedStaffItemWidget(
|
||||
{super.key, required this.staffContact, required this.department});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var showRating =
|
||||
staffContact.parentPosition?.parentShift?.parentEvent?.status ==
|
||||
EventStatus.completed;
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
StaffContactInfoPopup.show(context, staffContact, department);
|
||||
},
|
||||
child: Container(
|
||||
height: 62,
|
||||
padding: const EdgeInsets.all(12),
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
decoration: KwBoxDecorations.white8,
|
||||
child: Row(
|
||||
mainAxisAlignment: showRating
|
||||
? MainAxisAlignment.spaceBetween
|
||||
: MainAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Row(
|
||||
children: [
|
||||
CachedNetworkImage(
|
||||
useOldImageOnUrlChange: true,
|
||||
fadeOutDuration: Duration.zero,
|
||||
placeholderFadeInDuration: Duration.zero,
|
||||
fadeInDuration: Duration.zero,
|
||||
imageUrl: staffContact.photoUrl ?? '',
|
||||
imageBuilder: (context, imageProvider) => Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
image: DecorationImage(
|
||||
image: imageProvider,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
errorWidget: (context, url, error) =>
|
||||
const Icon(Icons.error)),
|
||||
const Gap(12),
|
||||
Flexible(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${staffContact.firstName} ${staffContact.lastName}',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
),
|
||||
const Gap(4),
|
||||
Text(
|
||||
staffContact.phoneNumber ?? '',
|
||||
style: AppTextStyles.bodySmallReg
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
],
|
||||
),
|
||||
),
|
||||
showRating ? _buildRating(context) : buildStatus()
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRating(BuildContext context) {
|
||||
return ValueListenableBuilder<double>(
|
||||
valueListenable: staffContact.rating,
|
||||
builder: (context, rating, child) {
|
||||
return rating == 0
|
||||
? GestureDetector(
|
||||
onTap: () {
|
||||
context.pushRoute(RateStaffRoute(
|
||||
staff: staffContact,
|
||||
));
|
||||
},
|
||||
child: Container(
|
||||
height: 36,
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.tintBlue,
|
||||
borderRadius: BorderRadius.circular(56),
|
||||
border: Border.all(
|
||||
color: AppColors.tintDropDownButton, width: 1),
|
||||
),
|
||||
child: Text('Rate',
|
||||
style: AppTextStyles.bodyMediumMed
|
||||
.copyWith(color: AppColors.primaryBlue)),
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
height: 36,
|
||||
alignment: Alignment.topRight,
|
||||
child: Row(
|
||||
children: [
|
||||
Assets.images.icons.ratingStar.star
|
||||
.svg(height: 16, width: 16),
|
||||
Gap(4),
|
||||
Text(rating.toStringAsFixed(1),
|
||||
style: AppTextStyles.bodyMediumMed),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
SizedBox buildStatus() {
|
||||
return SizedBox(
|
||||
height: 40,
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: staffContact.status.getStatusBorderColor(),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
padding: const EdgeInsets.all(3),
|
||||
decoration: BoxDecoration(
|
||||
color: staffContact.status.getStatusTextColor(),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
),
|
||||
const Gap(2),
|
||||
Text(
|
||||
staffContact.status.formattedName.capitalize(),
|
||||
style: AppTextStyles.bodyTinyMed
|
||||
.copyWith(color: staffContact.status.getStatusTextColor()),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
|
||||
class IconRowInfoWidget extends StatelessWidget {
|
||||
final Widget icon;
|
||||
final String title;
|
||||
final String value;
|
||||
|
||||
const IconRowInfoWidget(
|
||||
{super.key,
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.value});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 36,
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
height: 36,
|
||||
width: 36,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: AppColors.grayTintStroke, width: 1),
|
||||
),
|
||||
child: Center(child: icon),
|
||||
),
|
||||
const Gap(12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title,
|
||||
style: AppTextStyles.bodySmallReg
|
||||
.copyWith(color: AppColors.blackGray)),
|
||||
const Gap(2),
|
||||
Text(
|
||||
value,
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/features/home/presentation/home_screen.dart';
|
||||
|
||||
class KwTimeSlotInput extends StatefulWidget {
|
||||
const KwTimeSlotInput({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.onChange,
|
||||
required this.initialValue,
|
||||
});
|
||||
|
||||
static final _timeFormat = DateFormat('h:mma');
|
||||
|
||||
final String label;
|
||||
final Function(DateTime value) onChange;
|
||||
final DateTime initialValue;
|
||||
|
||||
@override
|
||||
State<KwTimeSlotInput> createState() => _KwTimeSlotInputState();
|
||||
}
|
||||
|
||||
class _KwTimeSlotInputState extends State<KwTimeSlotInput> {
|
||||
late DateTime _currentValue = widget.initialValue;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
_currentValue = widget.initialValue;
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant KwTimeSlotInput oldWidget) {
|
||||
_currentValue = widget.initialValue;
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16, bottom: 4),
|
||||
child: Text(
|
||||
widget.label,
|
||||
style: AppTextStyles.bodyTinyReg.copyWith(
|
||||
color: AppColors.blackGray,
|
||||
),
|
||||
),
|
||||
),
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(24),
|
||||
),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: homeContext!,
|
||||
isScrollControlled: false,
|
||||
builder: (context) {
|
||||
return Container(
|
||||
alignment: Alignment.topCenter,
|
||||
height: 216 + 48, //_kPickerHeight + 24top+24bot
|
||||
child: CupertinoDatePicker(
|
||||
mode: CupertinoDatePickerMode.time,
|
||||
initialDateTime: _currentValue,
|
||||
minuteInterval: 10,
|
||||
itemExtent: 36,
|
||||
onDateTimeChanged: (DateTime value) {
|
||||
setState(() => _currentValue = value);
|
||||
widget.onChange.call(value);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
height: 48,
|
||||
alignment: Alignment.centerLeft,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: AppColors.grayStroke),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
KwTimeSlotInput._timeFormat.format(_currentValue),
|
||||
style: AppTextStyles.bodyMediumReg,
|
||||
),
|
||||
Assets.images.icons.caretDown.svg(
|
||||
height: 16,
|
||||
width: 16,
|
||||
colorFilter: const ColorFilter.mode(
|
||||
AppColors.blackBlack,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_image_animated_placeholder.dart';
|
||||
|
||||
import '../styles/theme.dart';
|
||||
|
||||
class ProfileIcon extends StatefulWidget {
|
||||
const ProfileIcon({
|
||||
super.key,
|
||||
required this.onChange,
|
||||
this.diameter = 64,
|
||||
this.imagePath,
|
||||
this.imageUrl,
|
||||
this.imageQuality = 80,
|
||||
});
|
||||
|
||||
final double diameter;
|
||||
final String? imagePath;
|
||||
final String? imageUrl;
|
||||
final int imageQuality;
|
||||
final Function(String) onChange;
|
||||
|
||||
@override
|
||||
State<ProfileIcon> createState() => _ProfileIconState();
|
||||
}
|
||||
|
||||
class _ProfileIconState extends State<ProfileIcon> {
|
||||
String? _imagePath;
|
||||
String? _imageUrl;
|
||||
final ImagePicker _picker = ImagePicker();
|
||||
|
||||
Future<void> _pickImage([ImageSource source = ImageSource.gallery]) async {
|
||||
final XFile? image = await _picker.pickImage(
|
||||
source: source,
|
||||
imageQuality: widget.imageQuality,
|
||||
);
|
||||
|
||||
if (image != null) {
|
||||
setState(() => _imagePath = image.path);
|
||||
|
||||
widget.onChange(image.path);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_imagePath = widget.imagePath;
|
||||
_imageUrl = widget.imageUrl;
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant ProfileIcon oldWidget) {
|
||||
_imagePath = widget.imagePath;
|
||||
_imageUrl = widget.imageUrl;
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isImageAvailable = _imagePath != null || widget.imageUrl != null;
|
||||
Widget avatar;
|
||||
|
||||
if (_imagePath != null) {
|
||||
avatar = Image.file(
|
||||
File(_imagePath!),
|
||||
fit: BoxFit.cover,
|
||||
frameBuilder: (_, child, frame, __) {
|
||||
return frame != null ? child : const KwAnimatedImagePlaceholder();
|
||||
},
|
||||
);
|
||||
} else if (_imageUrl != null) {
|
||||
avatar = Image.network(
|
||||
_imageUrl!,
|
||||
fit: BoxFit.cover,
|
||||
frameBuilder: (_, child, frame, __) {
|
||||
return frame != null ? child : const KwAnimatedImagePlaceholder();
|
||||
},
|
||||
);
|
||||
} else {
|
||||
avatar = DecoratedBox(
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.grayWhite,
|
||||
),
|
||||
child: Assets.images.icons.person.svg(
|
||||
height: widget.diameter/2,
|
||||
width: widget.diameter/2,
|
||||
fit: BoxFit.scaleDown,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: _pickImage,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
ClipOval(
|
||||
child: SizedBox(
|
||||
height: widget.diameter,
|
||||
width: widget.diameter,
|
||||
child: avatar,
|
||||
),
|
||||
),
|
||||
PositionedDirectional(
|
||||
bottom: 0,
|
||||
end: -5,
|
||||
child: Container(
|
||||
height: widget.diameter/4+10,
|
||||
width: widget.diameter/4+10,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: isImageAvailable
|
||||
? AppColors.grayWhite
|
||||
: AppColors.bgColorDark,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: AppColors.bgColorLight,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: isImageAvailable
|
||||
? Assets.images.icons.edit.svg(
|
||||
width: widget.diameter/4,
|
||||
height: widget.diameter/4,
|
||||
)
|
||||
: Assets.images.icons.add.svg(
|
||||
width: widget.diameter/4,
|
||||
height: widget.diameter/4,
|
||||
fit: BoxFit.scaleDown,
|
||||
colorFilter: const ColorFilter.mode(
|
||||
AppColors.grayWhite,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Helps to create a scrollable layout with two widgets in a scrollable column
|
||||
/// and space between them
|
||||
class ScrollLayoutHelper extends StatelessWidget {
|
||||
final Widget upperWidget;
|
||||
final Widget lowerWidget;
|
||||
final EdgeInsets padding;
|
||||
|
||||
final ScrollController? controller;
|
||||
|
||||
final Future Function()? onRefresh;
|
||||
|
||||
const ScrollLayoutHelper(
|
||||
{super.key,
|
||||
required this.upperWidget,
|
||||
required this.lowerWidget,
|
||||
this.padding = const EdgeInsets.symmetric(horizontal: 16),
|
||||
this.controller,
|
||||
this.onRefresh});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
Widget content = SingleChildScrollView(
|
||||
physics:
|
||||
onRefresh != null ? const AlwaysScrollableScrollPhysics() : null,
|
||||
controller: controller,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: constraints.maxHeight,
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
upperWidget,
|
||||
lowerWidget,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (onRefresh != null) {
|
||||
content = RefreshIndicator(
|
||||
onRefresh: () => onRefresh!.call(),
|
||||
child: content,
|
||||
);
|
||||
}
|
||||
|
||||
return content;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:krow/core/data/models/staff/pivot.dart';
|
||||
import 'package:krow/core/entity/event_entity.dart';
|
||||
import 'package:krow/core/entity/staff_contact_entity.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/staff_position_details_widget.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/dialogs/kw_dialog.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_input.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_popup_button.dart';
|
||||
import 'package:krow/features/events/domain/blocs/details/event_details_bloc.dart';
|
||||
import 'package:modal_progress_hud_nsn/modal_progress_hud_nsn.dart';
|
||||
|
||||
class StaffContactInfoPopup {
|
||||
static Future<void> show(
|
||||
BuildContext context,
|
||||
StaffContact staff,
|
||||
String department,
|
||||
) async {
|
||||
var bloc = (BlocProvider.of<EventDetailsBloc>(context));
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return Center(
|
||||
child: _StaffPopupWidget(
|
||||
staff,
|
||||
bloc: bloc,
|
||||
department: department,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _StaffPopupWidget extends StatelessWidget {
|
||||
final StaffContact staff;
|
||||
|
||||
final EventDetailsBloc bloc;
|
||||
|
||||
final String department;
|
||||
|
||||
const _StaffPopupWidget(this.staff,
|
||||
{required this.bloc, required this.department});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<EventDetailsBloc, EventDetailsState>(
|
||||
listener: (context, state) {
|
||||
geoFencingServiceDialog(context, state);
|
||||
},
|
||||
bloc: bloc,
|
||||
builder: (context, state) {
|
||||
return ModalProgressHUD(
|
||||
inAsyncCall: state.inLoading,
|
||||
child: Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
// margin: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: KwBoxDecorations.white24,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: _ongoingBtn(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _ongoingBtn(BuildContext context) {
|
||||
return [
|
||||
const Gap(32),
|
||||
StaffPositionAvatar(
|
||||
imageUrl: staff.photoUrl,
|
||||
userName: '${staff.firstName} ${staff.lastName}',
|
||||
status: staff.status,
|
||||
),
|
||||
const Gap(16),
|
||||
StaffContactsWidget(staff: staff),
|
||||
StaffPositionDetailsWidget(staff: staff),
|
||||
if (staff.status == PivotStatus.confirmed &&
|
||||
staff.parentPosition?.parentShift?.parentEvent?.status ==
|
||||
EventStatus.active)
|
||||
..._confirmedBtn(context),
|
||||
if (staff.status == PivotStatus.confirmed &&
|
||||
staff.parentPosition?.parentShift?.parentEvent?.status ==
|
||||
EventStatus.confirmed)
|
||||
_buildReplaceStaffButton(context),
|
||||
if (staff.status == PivotStatus.ongoing) ...[
|
||||
KwButton.primary(
|
||||
onPressed: () {
|
||||
_clockOutDialog(context);
|
||||
},
|
||||
label: 'Clock Out',
|
||||
),
|
||||
],
|
||||
const Gap(12),
|
||||
];
|
||||
}
|
||||
|
||||
List<Widget> _confirmedBtn(BuildContext context) {
|
||||
return [
|
||||
KwButton.primary(
|
||||
onPressed: () {
|
||||
_clockinDialog(context);
|
||||
},
|
||||
label: 'Clock In',
|
||||
),
|
||||
const Gap(8),
|
||||
_buildReplaceStaffButton(context),
|
||||
];
|
||||
}
|
||||
|
||||
Widget _buildReplaceStaffButton(BuildContext context) {
|
||||
return KwPopUpButton(
|
||||
label: 'Staff Didn’t Show Up',
|
||||
colorPallet: KwPopupButtonColorPallet.transparent(),
|
||||
withBorder: true,
|
||||
popUpPadding: 28,
|
||||
items: [
|
||||
KwPopUpButtonItem(
|
||||
title: 'Replace Staff',
|
||||
onTap: () {
|
||||
// Navigator.of(context).pop();
|
||||
_replaceStaff(context);
|
||||
},
|
||||
color: AppColors.statusError),
|
||||
KwPopUpButtonItem(
|
||||
title: 'Do Nothing',
|
||||
onTap: () {
|
||||
bloc.add(NotShowedPositionStaffEvent(staff.id));
|
||||
|
||||
Navigator.of(context).pop();
|
||||
})
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _replaceStaff(BuildContext context) async {
|
||||
var controller = TextEditingController();
|
||||
StateSetter? setStateInDialog;
|
||||
var inputError = false;
|
||||
var result = await KwDialog.show(
|
||||
context: context,
|
||||
icon: Assets.images.icons.profileDelete,
|
||||
title: 'Request Staff Replacement',
|
||||
message:
|
||||
'Please provide a reason for the staff replacement request in the text area below:',
|
||||
state: KwDialogState.negative,
|
||||
child: StatefulBuilder(builder: (context, setDialogState) {
|
||||
setStateInDialog = setDialogState;
|
||||
return KwTextInput(
|
||||
controller: controller,
|
||||
maxLength: 300,
|
||||
showCounter: true,
|
||||
showError: inputError,
|
||||
minHeight: 144,
|
||||
hintText: 'Enter your reason here...',
|
||||
title: 'Reason',
|
||||
);
|
||||
}),
|
||||
|
||||
primaryButtonLabel: 'Submit Request',
|
||||
onPrimaryButtonPressed: (dialogContext) {
|
||||
if (controller.text.isEmpty) {
|
||||
setStateInDialog?.call(() {
|
||||
inputError = true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
bloc.add(ReplacePositionStaffEvent(staff.id, controller.text));
|
||||
Navigator.of(dialogContext).pop(true);
|
||||
},
|
||||
secondaryButtonLabel: 'Cancel',
|
||||
);
|
||||
|
||||
if (result && context.mounted) {
|
||||
await KwDialog.show(
|
||||
context: context,
|
||||
state: KwDialogState.negative,
|
||||
icon: Assets.images.icons.documentUpload,
|
||||
title: 'Request is Under Review',
|
||||
message:
|
||||
'Thank you! Your request for staff replacement is now under review. You will be notified of the outcome shortly.',
|
||||
primaryButtonLabel: 'Back to Event',
|
||||
onPrimaryButtonPressed: (dialogContext) {
|
||||
Navigator.of(dialogContext).pop();
|
||||
Navigator.of(context).maybePop();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void geoFencingServiceDialog(BuildContext context, EventDetailsState state) {
|
||||
switch (state.geofencingDialogState) {
|
||||
case GeofencingDialogState.tooFar:
|
||||
KwDialog.show(
|
||||
context: context,
|
||||
icon: Assets.images.icons.alertTriangle,
|
||||
state: KwDialogState.warning,
|
||||
title: "You're too far",
|
||||
message: 'Please move closer to the designated location.',
|
||||
primaryButtonLabel: 'OK',
|
||||
);
|
||||
break;
|
||||
case GeofencingDialogState.locationDisabled:
|
||||
KwDialog.show(
|
||||
context: context,
|
||||
icon: Assets.images.icons.alertTriangle,
|
||||
state: KwDialogState.negative,
|
||||
title: 'Location Disabled',
|
||||
message: 'Please enable location services to continue.',
|
||||
primaryButtonLabel: 'Go to Settings',
|
||||
onPrimaryButtonPressed: (dialogContext) {
|
||||
Geolocator.openLocationSettings();
|
||||
Navigator.of(dialogContext).pop();
|
||||
},
|
||||
);
|
||||
break;
|
||||
case GeofencingDialogState.goToSettings:
|
||||
KwDialog.show(
|
||||
context: context,
|
||||
icon: Assets.images.icons.alertTriangle,
|
||||
state: KwDialogState.info,
|
||||
title: 'Permission Required',
|
||||
message: 'You need to allow location access in settings.',
|
||||
primaryButtonLabel: 'Open Settings',
|
||||
onPrimaryButtonPressed: (dialogContext) {
|
||||
Geolocator.openAppSettings();
|
||||
Navigator.of(dialogContext).pop();
|
||||
},
|
||||
);
|
||||
break;
|
||||
case GeofencingDialogState.permissionDenied:
|
||||
KwDialog.show(
|
||||
context: context,
|
||||
icon: Assets.images.icons.alertTriangle,
|
||||
state: KwDialogState.negative,
|
||||
title: 'Permission Denied',
|
||||
message: 'You have denied location access. Please allow it manually.',
|
||||
primaryButtonLabel: 'OK',
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<dynamic> _clockinDialog(BuildContext context) {
|
||||
return KwDialog.show(
|
||||
context: context,
|
||||
icon: Assets.images.icons.profileAdd,
|
||||
state: KwDialogState.warning,
|
||||
title: 'Do you want to Clock In this Staff member?',
|
||||
primaryButtonLabel: 'Yes',
|
||||
onPrimaryButtonPressed: (dialogContext) async {
|
||||
bloc.add(TrackClientClockin(staff));
|
||||
await Navigator.of(dialogContext).maybePop();
|
||||
Navigator.of(context).maybePop();
|
||||
},
|
||||
secondaryButtonLabel: 'No',
|
||||
);
|
||||
}
|
||||
|
||||
Future<dynamic> _clockOutDialog(BuildContext context) {
|
||||
return KwDialog.show(
|
||||
context: context,
|
||||
icon: Assets.images.icons.profileDelete,
|
||||
state: KwDialogState.warning,
|
||||
title: 'Do you want to Clock Out this Staff member?',
|
||||
primaryButtonLabel: 'Yes',
|
||||
onPrimaryButtonPressed: (dialogContext) async {
|
||||
bloc.add(TrackClientClockout(staff));
|
||||
await Navigator.of(dialogContext).maybePop();
|
||||
Navigator.of(context).maybePop();
|
||||
},
|
||||
secondaryButtonLabel: 'No',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:krow/core/application/common/str_extensions.dart';
|
||||
import 'package:krow/core/data/models/staff/pivot.dart';
|
||||
import 'package:krow/core/entity/staff_contact_entity.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class StaffPositionDetailsWidget extends StatelessWidget {
|
||||
final StaffContact staff;
|
||||
|
||||
const StaffPositionDetailsWidget({
|
||||
super.key,
|
||||
required this.staff,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
margin: const EdgeInsets.all(12),
|
||||
decoration: KwBoxDecorations.primaryLight8,
|
||||
child: Column(
|
||||
children: [
|
||||
_textRow('Role', staff.skillName),
|
||||
_textRow('Department', staff.parentPosition?.department?.name ?? ''),
|
||||
_textRow(
|
||||
'Start Time',
|
||||
DateFormat('hh:mm a').format(
|
||||
DateFormat('yyyy-MM-dd hh:mm:ss').parse(staff.startAt))),
|
||||
_textRow(
|
||||
'End Time',
|
||||
DateFormat('hh:mm a').format(
|
||||
DateFormat('yyyy-MM-dd hh:mm:ss').parse(staff.endAt))),
|
||||
_textRow('Cost', '\$${staff.rate}/h'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _textRow(String title, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: AppTextStyles.bodySmallReg.copyWith(
|
||||
color: AppColors.blackGray,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: AppTextStyles.bodySmallMed,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class StaffPositionAvatar extends StatelessWidget {
|
||||
final String? imageUrl;
|
||||
final String? userName;
|
||||
final PivotStatus? status;
|
||||
|
||||
const StaffPositionAvatar(
|
||||
{super.key, this.imageUrl, this.userName, this.status});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Container(
|
||||
width: 96,
|
||||
height: 96,
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: AppColors.darkBgActiveButtonState,
|
||||
),
|
||||
child: imageUrl == null || imageUrl!.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
getInitials(userName),
|
||||
style: AppTextStyles.headingH1.copyWith(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
)
|
||||
: ClipOval(
|
||||
child: Image.network(
|
||||
imageUrl ?? '',
|
||||
fit: BoxFit.cover,
|
||||
width: 96,
|
||||
height: 96,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (status != null) Positioned(bottom: -7, child: buildStatus(status!)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildStatus(PivotStatus status) {
|
||||
return Container(
|
||||
height: 20,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.grayWhite,
|
||||
border: Border.all(color: status.getStatusBorderColor()),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
padding: const EdgeInsets.all(3),
|
||||
decoration: BoxDecoration(
|
||||
color: status.getStatusTextColor(),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
),
|
||||
const Gap(2),
|
||||
Text(
|
||||
status.formattedName.capitalize(),
|
||||
style: AppTextStyles.bodyTinyMed
|
||||
.copyWith(color: status.getStatusTextColor()),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String getInitials(String? name) {
|
||||
try {
|
||||
if (name == null || name.isEmpty) return 'X';
|
||||
List<String> nameParts = name.split(' ');
|
||||
if (nameParts.length == 1) {
|
||||
return nameParts[0].substring(0, 1).toUpperCase();
|
||||
}
|
||||
return (nameParts[0][0] + nameParts[1][0]).toUpperCase();
|
||||
} catch (e) {
|
||||
return 'X';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class StaffContactsWidget extends StatelessWidget {
|
||||
final StaffContact staff;
|
||||
|
||||
const StaffContactsWidget({super.key, required this.staff});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
'${staff.firstName} ${staff.lastName}',
|
||||
style: AppTextStyles.headingH3,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (staff.email != null && (staff.email?.isNotEmpty ?? false)) ...[
|
||||
const Gap(8),
|
||||
GestureDetector(
|
||||
onDoubleTap: () {
|
||||
launchUrlString('mailto:${staff.email}');
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Assets.images.icons.userProfile.sms.svg(
|
||||
colorFilter: const ColorFilter.mode(
|
||||
AppColors.blackGray,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
Text(
|
||||
staff.email ?? '',
|
||||
style: AppTextStyles.bodyMediumReg
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
if (staff.phoneNumber != null &&
|
||||
(staff.phoneNumber?.isNotEmpty ?? false)) ...[
|
||||
const Gap(8),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
launchUrlString('tel:${staff.phoneNumber}');
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Assets.images.icons.userProfile.call.svg(
|
||||
colorFilter: const ColorFilter.mode(
|
||||
AppColors.blackGray,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
Text(
|
||||
staff.phoneNumber ?? '',
|
||||
style: AppTextStyles.bodyMediumReg
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
]
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,447 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum PressType {
|
||||
longPress,
|
||||
singleClick,
|
||||
}
|
||||
|
||||
enum PreferredPosition {
|
||||
top,
|
||||
bottom,
|
||||
}
|
||||
|
||||
class CustomPopupMenuController extends ChangeNotifier {
|
||||
bool menuIsShowing = false;
|
||||
|
||||
void setState() {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void showMenu() {
|
||||
menuIsShowing = true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void hideMenu() {
|
||||
menuIsShowing = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void toggleMenu() {
|
||||
menuIsShowing = !menuIsShowing;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Rect _menuRect = Rect.zero;
|
||||
|
||||
class CustomPopupMenu extends StatefulWidget {
|
||||
const CustomPopupMenu({super.key,
|
||||
required this.child,
|
||||
required this.menuBuilder,
|
||||
required this.pressType,
|
||||
this.controller,
|
||||
this.arrowColor = const Color(0xFF4C4C4C),
|
||||
this.showArrow = true,
|
||||
this.barrierColor = Colors.black12,
|
||||
this.arrowSize = 10.0,
|
||||
this.horizontalMargin = 10.0,
|
||||
this.verticalMargin = 10.0,
|
||||
this.position,
|
||||
this.menuOnChange,
|
||||
this.enablePassEvent = true,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final PressType pressType;
|
||||
final bool showArrow;
|
||||
final Color arrowColor;
|
||||
final Color barrierColor;
|
||||
final double horizontalMargin;
|
||||
final double verticalMargin;
|
||||
final double arrowSize;
|
||||
final CustomPopupMenuController? controller;
|
||||
final Widget Function()? menuBuilder;
|
||||
final PreferredPosition? position;
|
||||
final void Function(bool)? menuOnChange;
|
||||
|
||||
/// Pass tap event to the widgets below the mask.
|
||||
/// It only works when [barrierColor] is transparent.
|
||||
final bool enablePassEvent;
|
||||
|
||||
|
||||
@override
|
||||
_CustomPopupMenuState createState() => _CustomPopupMenuState();
|
||||
}
|
||||
|
||||
class _CustomPopupMenuState extends State<CustomPopupMenu> {
|
||||
RenderBox? _childBox;
|
||||
RenderBox? _parentBox;
|
||||
static OverlayEntry? overlayEntry;
|
||||
CustomPopupMenuController? _controller;
|
||||
bool _canResponse = true;
|
||||
|
||||
_showMenu() {
|
||||
if (widget.menuBuilder == null) {
|
||||
_hideMenu();
|
||||
return;
|
||||
}
|
||||
Widget arrow = ClipPath(
|
||||
clipper: _ArrowClipper(),
|
||||
child: Container(
|
||||
width: widget.arrowSize,
|
||||
height: widget.arrowSize,
|
||||
color: widget.arrowColor,
|
||||
),
|
||||
);
|
||||
if(overlayEntry!=null){
|
||||
overlayEntry?.remove();
|
||||
}
|
||||
overlayEntry = OverlayEntry(
|
||||
builder: (context) {
|
||||
Widget menu = Center(
|
||||
child: Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: _parentBox!.size.width - 2 * widget.horizontalMargin,
|
||||
minWidth: 0,
|
||||
),
|
||||
child: CustomMultiChildLayout(
|
||||
delegate: _MenuLayoutDelegate(
|
||||
anchorSize: _childBox!.size,
|
||||
anchorOffset: _childBox!.localToGlobal(
|
||||
Offset(-widget.horizontalMargin, 0),
|
||||
),
|
||||
verticalMargin: widget.verticalMargin,
|
||||
position: widget.position,
|
||||
),
|
||||
children: <Widget>[
|
||||
if (widget.showArrow)
|
||||
LayoutId(
|
||||
id: _MenuLayoutId.arrow,
|
||||
child: arrow,
|
||||
),
|
||||
if (widget.showArrow)
|
||||
LayoutId(
|
||||
id: _MenuLayoutId.downArrow,
|
||||
child: Transform.rotate(
|
||||
angle: math.pi,
|
||||
child: arrow,
|
||||
),
|
||||
),
|
||||
LayoutId(
|
||||
id: _MenuLayoutId.content,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: widget.menuBuilder?.call() ?? Container(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
return Listener(
|
||||
behavior: widget.enablePassEvent
|
||||
? HitTestBehavior.translucent
|
||||
: HitTestBehavior.opaque,
|
||||
onPointerDown: (PointerDownEvent event) {
|
||||
Offset offset = event.localPosition;
|
||||
// If tap position in menu
|
||||
if (_menuRect.contains(
|
||||
Offset(offset.dx - widget.horizontalMargin, offset.dy))) {
|
||||
return;
|
||||
}
|
||||
_controller?.hideMenu();
|
||||
// When [enablePassEvent] works and we tap the [child] to [hideMenu],
|
||||
// but the passed event would trigger [showMenu] again.
|
||||
// So, we use time threshold to solve this bug.
|
||||
_canResponse = false;
|
||||
Future.delayed(const Duration(milliseconds: 300))
|
||||
.then((_) => _canResponse = true);
|
||||
},
|
||||
child: widget.barrierColor == Colors.transparent
|
||||
? menu
|
||||
: Container(
|
||||
color: widget.barrierColor,
|
||||
child: menu,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
if (overlayEntry != null) {
|
||||
try {
|
||||
overlayEntry?.remove();
|
||||
} catch (e) {
|
||||
print(e);
|
||||
}
|
||||
Overlay.of(context).insert(overlayEntry!);
|
||||
}
|
||||
}
|
||||
|
||||
_hideMenu() {
|
||||
if (overlayEntry != null) {
|
||||
overlayEntry?.remove();
|
||||
overlayEntry = null;
|
||||
}
|
||||
}
|
||||
|
||||
_updateView() {
|
||||
bool menuIsShowing = _controller?.menuIsShowing ?? false;
|
||||
widget.menuOnChange?.call(menuIsShowing);
|
||||
if (menuIsShowing) {
|
||||
_showMenu();
|
||||
} else {
|
||||
_hideMenu();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = widget.controller;
|
||||
_controller ??= CustomPopupMenuController();
|
||||
_controller?.addListener(_updateView);
|
||||
WidgetsBinding.instance.addPostFrameCallback((call) {
|
||||
if (mounted) {
|
||||
_childBox = context.findRenderObject() as RenderBox?;
|
||||
_parentBox =
|
||||
Overlay.of(context).context.findRenderObject() as RenderBox?;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_hideMenu();
|
||||
_controller?.removeListener(_updateView);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var child = Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
hoverColor: Colors.transparent,
|
||||
focusColor: Colors.transparent,
|
||||
splashColor: Colors.transparent,
|
||||
highlightColor: Colors.transparent,
|
||||
child: widget.child,
|
||||
onTap: () {
|
||||
if (widget.pressType == PressType.singleClick && _canResponse) {
|
||||
_controller?.showMenu();
|
||||
}
|
||||
},
|
||||
onLongPress: () {
|
||||
if (widget.pressType == PressType.longPress && _canResponse) {
|
||||
_controller?.showMenu();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
if (Platform.isIOS) {
|
||||
return child;
|
||||
} else {
|
||||
return WillPopScope(
|
||||
onWillPop: () {
|
||||
_hideMenu();
|
||||
return Future.value(true);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum _MenuLayoutId {
|
||||
arrow,
|
||||
downArrow,
|
||||
content,
|
||||
}
|
||||
|
||||
enum _MenuPosition {
|
||||
bottomLeft,
|
||||
bottomCenter,
|
||||
bottomRight,
|
||||
topLeft,
|
||||
topCenter,
|
||||
topRight,
|
||||
}
|
||||
|
||||
class _MenuLayoutDelegate extends MultiChildLayoutDelegate {
|
||||
_MenuLayoutDelegate({
|
||||
required this.anchorSize,
|
||||
required this.anchorOffset,
|
||||
required this.verticalMargin,
|
||||
this.position,
|
||||
});
|
||||
|
||||
final Size anchorSize;
|
||||
final Offset anchorOffset;
|
||||
final double verticalMargin;
|
||||
final PreferredPosition? position;
|
||||
|
||||
@override
|
||||
void performLayout(Size size) {
|
||||
Size contentSize = Size.zero;
|
||||
Size arrowSize = Size.zero;
|
||||
Offset contentOffset = const Offset(0, 0);
|
||||
Offset arrowOffset = const Offset(0, 0);
|
||||
|
||||
double anchorCenterX = anchorOffset.dx + anchorSize.width / 2;
|
||||
double anchorTopY = anchorOffset.dy;
|
||||
double anchorBottomY = anchorTopY + anchorSize.height;
|
||||
_MenuPosition menuPosition = _MenuPosition.bottomCenter;
|
||||
|
||||
if (hasChild(_MenuLayoutId.content)) {
|
||||
contentSize = layoutChild(
|
||||
_MenuLayoutId.content,
|
||||
BoxConstraints.loose(size),
|
||||
);
|
||||
}
|
||||
if (hasChild(_MenuLayoutId.arrow)) {
|
||||
arrowSize = layoutChild(
|
||||
_MenuLayoutId.arrow,
|
||||
BoxConstraints.loose(size),
|
||||
);
|
||||
}
|
||||
if (hasChild(_MenuLayoutId.downArrow)) {
|
||||
layoutChild(
|
||||
_MenuLayoutId.downArrow,
|
||||
BoxConstraints.loose(size),
|
||||
);
|
||||
}
|
||||
|
||||
bool isTop = false;
|
||||
if (position == null) {
|
||||
// auto calculate position
|
||||
isTop = anchorBottomY > size.height / 2;
|
||||
} else {
|
||||
isTop = position == PreferredPosition.top;
|
||||
}
|
||||
if (anchorCenterX - contentSize.width / 2 < 0) {
|
||||
menuPosition = isTop ? _MenuPosition.topLeft : _MenuPosition.bottomLeft;
|
||||
} else if (anchorCenterX + contentSize.width / 2 > size.width) {
|
||||
menuPosition = isTop ? _MenuPosition.topRight : _MenuPosition.bottomRight;
|
||||
} else {
|
||||
menuPosition =
|
||||
isTop ? _MenuPosition.topCenter : _MenuPosition.bottomCenter;
|
||||
}
|
||||
|
||||
switch (menuPosition) {
|
||||
case _MenuPosition.bottomCenter:
|
||||
arrowOffset = Offset(
|
||||
anchorCenterX - arrowSize.width / 2,
|
||||
anchorBottomY + verticalMargin,
|
||||
);
|
||||
contentOffset = Offset(
|
||||
anchorCenterX - contentSize.width / 2,
|
||||
anchorBottomY + verticalMargin + arrowSize.height,
|
||||
);
|
||||
break;
|
||||
case _MenuPosition.bottomLeft:
|
||||
arrowOffset = Offset(anchorCenterX - arrowSize.width / 2,
|
||||
anchorBottomY + verticalMargin);
|
||||
contentOffset = Offset(
|
||||
0,
|
||||
anchorBottomY + verticalMargin + arrowSize.height,
|
||||
);
|
||||
break;
|
||||
case _MenuPosition.bottomRight:
|
||||
arrowOffset = Offset(anchorCenterX - arrowSize.width / 2,
|
||||
anchorBottomY + verticalMargin);
|
||||
contentOffset = Offset(
|
||||
size.width - contentSize.width,
|
||||
anchorBottomY + verticalMargin + arrowSize.height,
|
||||
);
|
||||
break;
|
||||
case _MenuPosition.topCenter:
|
||||
arrowOffset = Offset(
|
||||
anchorCenterX - arrowSize.width / 2,
|
||||
anchorTopY - verticalMargin - arrowSize.height,
|
||||
);
|
||||
contentOffset = Offset(
|
||||
anchorCenterX - contentSize.width / 2,
|
||||
anchorTopY - verticalMargin - arrowSize.height - contentSize.height,
|
||||
);
|
||||
break;
|
||||
case _MenuPosition.topLeft:
|
||||
arrowOffset = Offset(
|
||||
anchorCenterX - arrowSize.width / 2,
|
||||
anchorTopY - verticalMargin - arrowSize.height,
|
||||
);
|
||||
contentOffset = Offset(
|
||||
0,
|
||||
anchorTopY - verticalMargin - arrowSize.height - contentSize.height,
|
||||
);
|
||||
break;
|
||||
case _MenuPosition.topRight:
|
||||
arrowOffset = Offset(
|
||||
anchorCenterX - arrowSize.width / 2,
|
||||
anchorTopY - verticalMargin - arrowSize.height,
|
||||
);
|
||||
contentOffset = Offset(
|
||||
size.width - contentSize.width,
|
||||
anchorTopY - verticalMargin - arrowSize.height - contentSize.height,
|
||||
);
|
||||
break;
|
||||
}
|
||||
if (hasChild(_MenuLayoutId.content)) {
|
||||
positionChild(_MenuLayoutId.content, contentOffset);
|
||||
}
|
||||
|
||||
_menuRect = Rect.fromLTWH(
|
||||
contentOffset.dx,
|
||||
contentOffset.dy,
|
||||
contentSize.width,
|
||||
contentSize.height,
|
||||
);
|
||||
bool isBottom = false;
|
||||
if (_MenuPosition.values.indexOf(menuPosition) < 3) {
|
||||
// bottom
|
||||
isBottom = true;
|
||||
}
|
||||
if (hasChild(_MenuLayoutId.arrow)) {
|
||||
positionChild(
|
||||
_MenuLayoutId.arrow,
|
||||
isBottom
|
||||
? Offset(arrowOffset.dx, arrowOffset.dy + 0.1)
|
||||
: const Offset(-100, 0),
|
||||
);
|
||||
}
|
||||
if (hasChild(_MenuLayoutId.downArrow)) {
|
||||
positionChild(
|
||||
_MenuLayoutId.downArrow,
|
||||
!isBottom
|
||||
? Offset(arrowOffset.dx, arrowOffset.dy - 0.1)
|
||||
: const Offset(-100, 0),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => false;
|
||||
}
|
||||
|
||||
class _ArrowClipper extends CustomClipper<Path> {
|
||||
@override
|
||||
Path getClip(Size size) {
|
||||
Path path = Path();
|
||||
path.moveTo(0, size.height);
|
||||
path.lineTo(size.width / 2, size.height / 2);
|
||||
path.lineTo(size.width, size.height);
|
||||
return path;
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldReclip(CustomClipper<Path> oldClipper) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
|
||||
enum CheckBoxStyle {
|
||||
green,
|
||||
black,
|
||||
red,
|
||||
}
|
||||
|
||||
class KWCheckBox extends StatelessWidget {
|
||||
final bool value;
|
||||
final CheckBoxStyle? style;
|
||||
|
||||
const KWCheckBox({
|
||||
super.key,
|
||||
required this.value,
|
||||
this.style = CheckBoxStyle.green,
|
||||
});
|
||||
|
||||
Color get _color {
|
||||
switch (style!) {
|
||||
case CheckBoxStyle.green:
|
||||
return value ? AppColors.statusSuccess : Colors.white;
|
||||
case CheckBoxStyle.black:
|
||||
return value ? AppColors.bgColorDark : Colors.white;
|
||||
case CheckBoxStyle.red:
|
||||
return value ? AppColors.statusError : Colors.white;
|
||||
}
|
||||
}
|
||||
|
||||
Widget get _icon {
|
||||
switch (style!) {
|
||||
case CheckBoxStyle.green:
|
||||
case CheckBoxStyle.black:
|
||||
return Assets.images.icons.checkBox.check.svg();
|
||||
case CheckBoxStyle.red:
|
||||
return Assets.images.icons.checkBox.x.svg();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
width: 16,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: _color,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: value
|
||||
? null
|
||||
: Border.all(color: AppColors.grayTintStroke, width: 1),
|
||||
),
|
||||
child: Center(
|
||||
child: value ? _icon : null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
|
||||
class ImagePreviewDialog extends StatelessWidget {
|
||||
final String title;
|
||||
final String imageUrl;
|
||||
|
||||
const ImagePreviewDialog({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.imageUrl,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Center(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.grayWhite,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: AppTextStyles.bodyLargeMed,
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Assets.images.icons.x.svg(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
//rounded corner image
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: _buildImage(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImage(context) {
|
||||
if (Uri.parse(imageUrl).isAbsolute) {
|
||||
return Image.network(
|
||||
imageUrl,
|
||||
width: MediaQuery.of(context).size.width - 80,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
} else {
|
||||
return Image.file(
|
||||
File(imageUrl),
|
||||
width: MediaQuery.of(context).size.width - 80,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static void show(BuildContext context, String title, String imageUrl) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return ImagePreviewDialog(
|
||||
title: title,
|
||||
imageUrl: imageUrl,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
|
||||
|
||||
enum KwDialogState { neutral, positive, negative, warning, info }
|
||||
|
||||
class KwDialog extends StatefulWidget {
|
||||
final SvgGenImage icon;
|
||||
final KwDialogState state;
|
||||
final String title;
|
||||
final String? message;
|
||||
final String? primaryButtonLabel;
|
||||
final String? secondaryButtonLabel;
|
||||
final void Function(BuildContext dialogContext)? onPrimaryButtonPressed;
|
||||
final void Function(BuildContext dialogContext)? onSecondaryButtonPressed;
|
||||
final Widget? child;
|
||||
|
||||
const KwDialog(
|
||||
{super.key,
|
||||
required this.icon,
|
||||
required this.state,
|
||||
required this.title,
|
||||
this.message,
|
||||
this.primaryButtonLabel,
|
||||
this.secondaryButtonLabel,
|
||||
this.onPrimaryButtonPressed,
|
||||
this.onSecondaryButtonPressed,
|
||||
this.child});
|
||||
|
||||
@override
|
||||
State<KwDialog> createState() => _KwDialogState();
|
||||
|
||||
static Future<R?> show<R>(
|
||||
{required BuildContext context,
|
||||
required SvgGenImage icon,
|
||||
KwDialogState state = KwDialogState.neutral,
|
||||
required String title,
|
||||
String? message,
|
||||
String? primaryButtonLabel,
|
||||
String? secondaryButtonLabel,
|
||||
void Function(BuildContext dialogContext)? onPrimaryButtonPressed,
|
||||
void Function(BuildContext dialogContext)? onSecondaryButtonPressed,
|
||||
Widget? child}) async {
|
||||
return showDialog<R>(
|
||||
context: context,
|
||||
builder: (context) => KwDialog(
|
||||
icon: icon,
|
||||
state: state,
|
||||
title: title,
|
||||
message: message,
|
||||
primaryButtonLabel: primaryButtonLabel,
|
||||
secondaryButtonLabel: secondaryButtonLabel,
|
||||
onPrimaryButtonPressed: onPrimaryButtonPressed,
|
||||
onSecondaryButtonPressed: onSecondaryButtonPressed,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _KwDialogState extends State<KwDialog> {
|
||||
Color get _iconColor {
|
||||
switch (widget.state) {
|
||||
case KwDialogState.neutral:
|
||||
return AppColors.blackBlack;
|
||||
case KwDialogState.positive:
|
||||
return AppColors.statusSuccess;
|
||||
case KwDialogState.negative:
|
||||
return AppColors.statusError;
|
||||
case KwDialogState.warning:
|
||||
return AppColors.statusWarning;
|
||||
case KwDialogState.info:
|
||||
return AppColors.primaryBlue;
|
||||
}
|
||||
}
|
||||
|
||||
Color get _iconBgColor {
|
||||
switch (widget.state) {
|
||||
case KwDialogState.neutral:
|
||||
return AppColors.tintGray;
|
||||
case KwDialogState.positive:
|
||||
return AppColors.tintGreen;
|
||||
case KwDialogState.negative:
|
||||
return AppColors.tintRed;
|
||||
case KwDialogState.warning:
|
||||
return AppColors.tintYellow;
|
||||
|
||||
case KwDialogState.info:
|
||||
return AppColors.tintBlue;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: AnimatedSize(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
decoration: KwBoxDecorations.white24,
|
||||
child: RawScrollbar(
|
||||
thumbVisibility: true,
|
||||
thumbColor: AppColors.blackCaptionText,
|
||||
radius: const Radius.circular(20),
|
||||
crossAxisMargin: 4,
|
||||
mainAxisMargin: 24,
|
||||
thickness: 6,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 56),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
height: 64,
|
||||
width: 64,
|
||||
decoration: BoxDecoration(
|
||||
color: _iconBgColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Center(
|
||||
child: widget.icon.svg(
|
||||
width: 32,
|
||||
height: 32,
|
||||
colorFilter:
|
||||
ColorFilter.mode(_iconColor, BlendMode.srcIn),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(32),
|
||||
Text(
|
||||
widget.title,
|
||||
style: AppTextStyles.headingH1.copyWith(height: 1),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (widget.message != null) ...[
|
||||
const Gap(8),
|
||||
Text(
|
||||
widget.message ?? '',
|
||||
style: AppTextStyles.bodyMediumReg
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
if (widget.child != null) ...[
|
||||
const Gap(8),
|
||||
widget.child!,
|
||||
],
|
||||
const Gap(24),
|
||||
..._buttonGroup(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buttonGroup() {
|
||||
return [
|
||||
if (widget.primaryButtonLabel != null)
|
||||
KwButton.primary(
|
||||
label: widget.primaryButtonLabel ?? '',
|
||||
onPressed: () {
|
||||
if (widget.onPrimaryButtonPressed != null) {
|
||||
widget.onPrimaryButtonPressed?.call(context);
|
||||
} else {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
),
|
||||
if (widget.primaryButtonLabel != null &&
|
||||
widget.secondaryButtonLabel != null)
|
||||
const Gap(8),
|
||||
if (widget.secondaryButtonLabel != null)
|
||||
KwButton.outlinedPrimary(
|
||||
label: widget.secondaryButtonLabel ?? '',
|
||||
onPressed: () {
|
||||
if (widget.onSecondaryButtonPressed != null) {
|
||||
widget.onSecondaryButtonPressed?.call(context);
|
||||
} else {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
|
||||
enum AppBarIconColorStyle { normal, inverted }
|
||||
|
||||
class KwAppBar extends AppBar {
|
||||
final bool showNotification;
|
||||
final Color? contentColor;
|
||||
final AppBarIconColorStyle iconColorStyle;
|
||||
|
||||
final String? titleText;
|
||||
|
||||
KwAppBar({
|
||||
this.titleText,
|
||||
this.showNotification = false,
|
||||
this.contentColor,
|
||||
bool centerTitle = true,
|
||||
this.iconColorStyle = AppBarIconColorStyle.normal,
|
||||
super.key,
|
||||
super.backgroundColor,
|
||||
}) : super(
|
||||
leadingWidth: centerTitle ? null : 0,
|
||||
elevation: 0,
|
||||
centerTitle: centerTitle,
|
||||
);
|
||||
|
||||
@override
|
||||
List<Widget> get actions {
|
||||
return [
|
||||
if (showNotification)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(right: 16),
|
||||
height: 48,
|
||||
width: 48,
|
||||
color: Colors.transparent,
|
||||
child: Center(
|
||||
child: Assets.images.icons.appBar.notification.svg(
|
||||
colorFilter: ColorFilter.mode(
|
||||
iconColorStyle == AppBarIconColorStyle.normal
|
||||
? AppColors.blackBlack
|
||||
: AppColors.grayWhite,
|
||||
BlendMode.srcIn)),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget? get title {
|
||||
return titleText != null
|
||||
? Text(
|
||||
titleText!,
|
||||
style: _titleTextStyle(contentColor),
|
||||
)
|
||||
: Assets.images.logo.svg(
|
||||
colorFilter: ColorFilter.mode(
|
||||
contentColor ?? AppColors.bgColorDark, BlendMode.srcIn));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget? get leading {
|
||||
return Builder(builder: (context) {
|
||||
return AutoRouter.of(context).canPop()
|
||||
? GestureDetector(
|
||||
onTap: () {
|
||||
AutoRouter.of(context).maybePop();
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 20.0),
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
width: 40,
|
||||
child: Center(
|
||||
child: Assets.images.icons.appBar.appbarLeading.svg(
|
||||
colorFilter: ColorFilter.mode(
|
||||
iconColorStyle == AppBarIconColorStyle.normal
|
||||
? AppColors.blackBlack
|
||||
: Colors.white,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
});
|
||||
}
|
||||
|
||||
static TextStyle _titleTextStyle(contentColor) {
|
||||
return AppTextStyles.headingH2.copyWith(
|
||||
color: contentColor ?? AppColors.blackBlack,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
|
||||
///The [KwButtonFit] enum defines two possible values for the button fit:
|
||||
/// *[expanded]: The button will expand to fill the available space.
|
||||
/// *[shrinkWrap]: The button will wrap its content, taking up only as much space as needed.
|
||||
enum KwButtonFit { expanded, shrinkWrap, circular }
|
||||
|
||||
class KwButton extends StatefulWidget {
|
||||
final String? label;
|
||||
final SvgGenImage? leftIcon;
|
||||
final SvgGenImage? rightIcon;
|
||||
final bool disabled;
|
||||
final Color color;
|
||||
final Color pressedColor;
|
||||
final Color disabledColor;
|
||||
final Color borderColor;
|
||||
final Color? textColors;
|
||||
final bool isOutlined;
|
||||
final bool isFilledOutlined;
|
||||
final VoidCallback onPressed;
|
||||
final KwButtonFit? fit;
|
||||
final double height;
|
||||
final bool originalIconsColors;
|
||||
final double? iconSize;
|
||||
|
||||
const KwButton._({
|
||||
required this.onPressed,
|
||||
required this.color,
|
||||
required this.isOutlined,
|
||||
required this.pressedColor,
|
||||
required this.disabledColor,
|
||||
required this.borderColor,
|
||||
this.disabled = false,
|
||||
this.label,
|
||||
this.leftIcon,
|
||||
this.rightIcon,
|
||||
this.textColors,
|
||||
this.height = 52,
|
||||
this.fit = KwButtonFit.expanded,
|
||||
this.originalIconsColors = false,
|
||||
this.isFilledOutlined = false,
|
||||
this.iconSize,
|
||||
}) : assert(label != null || leftIcon != null || rightIcon != null,
|
||||
'title or icon must be provided');
|
||||
|
||||
/// Creates a standard dark button.
|
||||
const KwButton.primary(
|
||||
{super.key,
|
||||
this.label,
|
||||
this.leftIcon,
|
||||
this.rightIcon,
|
||||
this.disabled = false,
|
||||
this.height = 52,
|
||||
this.fit = KwButtonFit.expanded,
|
||||
required this.onPressed,
|
||||
this.iconSize})
|
||||
: color = AppColors.bgColorDark,
|
||||
borderColor = AppColors.bgColorDark,
|
||||
pressedColor = AppColors.darkBgActiveButtonState,
|
||||
disabledColor = AppColors.grayDisable,
|
||||
textColors = Colors.white,
|
||||
originalIconsColors = true,
|
||||
isFilledOutlined = false,
|
||||
isOutlined = false;
|
||||
|
||||
// /// Creates a white button.
|
||||
const KwButton.secondary(
|
||||
{super.key,
|
||||
this.label,
|
||||
this.leftIcon,
|
||||
this.rightIcon,
|
||||
this.disabled = false,
|
||||
this.height = 52,
|
||||
this.fit = KwButtonFit.expanded,
|
||||
required this.onPressed,
|
||||
this.iconSize})
|
||||
: color = Colors.white,
|
||||
borderColor = Colors.white,
|
||||
pressedColor = AppColors.buttonTertiaryActive,
|
||||
disabledColor = Colors.white,
|
||||
textColors = disabled ? AppColors.grayDisable : AppColors.blackBlack,
|
||||
originalIconsColors = true,
|
||||
isFilledOutlined = false,
|
||||
isOutlined = false;
|
||||
|
||||
/// Creates a yellow button.
|
||||
const KwButton.accent(
|
||||
{super.key,
|
||||
this.label,
|
||||
this.leftIcon,
|
||||
this.rightIcon,
|
||||
this.disabled = false,
|
||||
this.height = 52,
|
||||
this.fit = KwButtonFit.expanded,
|
||||
required this.onPressed,
|
||||
this.iconSize})
|
||||
: color = AppColors.primaryYellow,
|
||||
borderColor = AppColors.primaryYellow,
|
||||
|
||||
pressedColor = AppColors.primaryYellowDark,
|
||||
disabledColor = AppColors.navBarDisabled,
|
||||
textColors = disabled ? AppColors.darkBgInactive : AppColors.blackBlack,
|
||||
originalIconsColors = true,
|
||||
isFilledOutlined = false,
|
||||
isOutlined = false;
|
||||
|
||||
const KwButton.outlinedPrimary(
|
||||
{super.key,
|
||||
this.label,
|
||||
this.leftIcon,
|
||||
this.rightIcon,
|
||||
this.disabled = false,
|
||||
this.height = 52,
|
||||
this.fit = KwButtonFit.expanded,
|
||||
required this.onPressed,
|
||||
this.iconSize})
|
||||
: color = AppColors.bgColorDark,
|
||||
borderColor = AppColors.bgColorDark,
|
||||
pressedColor = AppColors.darkBgActiveButtonState,
|
||||
disabledColor = AppColors.grayDisable,
|
||||
isOutlined = true,
|
||||
originalIconsColors = true,
|
||||
isFilledOutlined = false,
|
||||
textColors = null;
|
||||
|
||||
const KwButton.outlinedAccent(
|
||||
{super.key,
|
||||
this.label,
|
||||
this.leftIcon,
|
||||
this.rightIcon,
|
||||
this.disabled = false,
|
||||
this.height = 52,
|
||||
this.fit = KwButtonFit.expanded,
|
||||
required this.onPressed,
|
||||
this.iconSize})
|
||||
: color = AppColors.primaryYellow,
|
||||
borderColor = AppColors.primaryYellow,
|
||||
pressedColor = AppColors.darkBgActiveButtonState,
|
||||
disabledColor = AppColors.navBarDisabled,
|
||||
isOutlined = true,
|
||||
originalIconsColors = true,
|
||||
isFilledOutlined = false,
|
||||
textColors = null;
|
||||
|
||||
KwButton copyWith({
|
||||
String? label,
|
||||
SvgGenImage? icon,
|
||||
bool? disabled,
|
||||
Color? color,
|
||||
Color? pressedColor,
|
||||
Color? disabledColor,
|
||||
Color? textColors,
|
||||
Color? borderColor,
|
||||
bool? isOutlined,
|
||||
VoidCallback? onPressed,
|
||||
KwButtonFit? fit,
|
||||
double? height,
|
||||
double? iconSize,
|
||||
bool? originalIconsColors,
|
||||
bool? isFilledOutlined,
|
||||
}) {
|
||||
return KwButton._(
|
||||
label: label ?? this.label,
|
||||
leftIcon: icon ?? leftIcon,
|
||||
rightIcon: icon ?? rightIcon,
|
||||
disabled: disabled ?? this.disabled,
|
||||
color: color ?? this.color,
|
||||
pressedColor: pressedColor ?? this.pressedColor,
|
||||
disabledColor: disabledColor ?? this.disabledColor,
|
||||
textColors: textColors ?? this.textColors,
|
||||
borderColor: borderColor ?? this.borderColor,
|
||||
isOutlined: isOutlined ?? this.isOutlined,
|
||||
onPressed: onPressed ?? this.onPressed,
|
||||
fit: fit ?? this.fit,
|
||||
height: height ?? this.height,
|
||||
iconSize: iconSize ?? this.iconSize,
|
||||
isFilledOutlined: isFilledOutlined ?? this.isFilledOutlined,
|
||||
originalIconsColors: originalIconsColors ?? this.originalIconsColors,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
State<KwButton> createState() => _KwButtonState();
|
||||
}
|
||||
|
||||
class _KwButtonState extends State<KwButton> {
|
||||
bool pressed = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.fit == KwButtonFit.shrinkWrap
|
||||
? Row(children: [_buildButton(context)])
|
||||
: _buildButton(context);
|
||||
}
|
||||
|
||||
Widget _buildButton(BuildContext context) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
height: widget.height,
|
||||
width: widget.fit == KwButtonFit.circular ? widget.height : null,
|
||||
decoration: BoxDecoration(
|
||||
color: _getColor(),
|
||||
border: _getBorder(),
|
||||
borderRadius: BorderRadius.circular(widget.height / 2),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTapDown: widget.disabled ? null : _onTapDown,
|
||||
onTapCancel: widget.disabled ? null : _onTapCancel,
|
||||
onTapUp: widget.disabled ? null : _onTapUp,
|
||||
borderRadius: BorderRadius.circular(widget.height / 2),
|
||||
highlightColor:
|
||||
( widget.isOutlined && !widget.isFilledOutlined) ? Colors.transparent : widget.pressedColor,
|
||||
splashColor:
|
||||
( widget.isOutlined && !widget.isFilledOutlined) ? Colors.transparent : widget.pressedColor,
|
||||
child: _buildButtonContent(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Center _buildButtonContent(BuildContext context) {
|
||||
return Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildHorizontalPadding(),
|
||||
if (widget.leftIcon != null)
|
||||
Center(
|
||||
child: widget.leftIcon!.svg(
|
||||
height: widget.iconSize ?? 16,
|
||||
width: widget.iconSize ?? 16,
|
||||
colorFilter: widget.originalIconsColors
|
||||
? null
|
||||
: ColorFilter.mode(_getTextColor(), BlendMode.srcIn)),
|
||||
),
|
||||
if (widget.leftIcon != null && widget.label != null)
|
||||
const SizedBox(width: 4),
|
||||
if (widget.label != null)
|
||||
Text(
|
||||
widget.label!,
|
||||
style: AppTextStyles.bodyMediumMed.copyWith(
|
||||
color: _getTextColor(),
|
||||
),
|
||||
),
|
||||
if (widget.rightIcon != null && widget.label != null)
|
||||
const SizedBox(width: 4),
|
||||
if (widget.rightIcon != null)
|
||||
Center(
|
||||
child: widget.rightIcon!.svg(
|
||||
height: widget.iconSize ?? 16,
|
||||
width: widget.iconSize ?? 16,
|
||||
colorFilter: widget.originalIconsColors
|
||||
? null
|
||||
: ColorFilter.mode(_getTextColor(), BlendMode.srcIn)),
|
||||
),
|
||||
_buildHorizontalPadding()
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Gap _buildHorizontalPadding() => Gap(
|
||||
widget.fit == KwButtonFit.circular ? 0 : (widget.height < 40 ? 12 : 20));
|
||||
|
||||
void _onTapDown(details) {
|
||||
setState(() {
|
||||
pressed = true;
|
||||
});
|
||||
}
|
||||
|
||||
void _onTapCancel() {
|
||||
setState(() {
|
||||
pressed = false;
|
||||
});
|
||||
}
|
||||
|
||||
void _onTapUp(details) {
|
||||
Future.delayed(const Duration(milliseconds: 50), _onTapCancel);
|
||||
widget.onPressed();
|
||||
}
|
||||
|
||||
Border? _getBorder() {
|
||||
return widget.isOutlined
|
||||
? Border.all(
|
||||
color: widget.disabled
|
||||
? widget.disabledColor
|
||||
: pressed
|
||||
? widget.pressedColor
|
||||
: (widget.borderColor??widget.color),
|
||||
width: 1)
|
||||
: null;
|
||||
}
|
||||
|
||||
Color _getColor() {
|
||||
return widget.isOutlined && !widget.isFilledOutlined
|
||||
? Colors.transparent
|
||||
: widget.disabled
|
||||
? widget.disabledColor
|
||||
: widget.color;
|
||||
}
|
||||
|
||||
Color _getTextColor() {
|
||||
return widget.textColors ??
|
||||
(pressed
|
||||
? widget.pressedColor
|
||||
: widget.disabled
|
||||
? widget.disabledColor
|
||||
: widget.color);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_popup_menu.dart';
|
||||
|
||||
class KwDropdown<R> extends StatefulWidget {
|
||||
final String? title;
|
||||
final String hintText;
|
||||
final KwDropDownItem<R>? selectedItem;
|
||||
final Iterable<KwDropDownItem<R>> items;
|
||||
final Function(R item) onSelected;
|
||||
final double horizontalPadding;
|
||||
final Color? backgroundColor;
|
||||
final Color? borderColor;
|
||||
|
||||
const KwDropdown(
|
||||
{super.key,
|
||||
required this.hintText,
|
||||
this.horizontalPadding = 0,
|
||||
required this.items,
|
||||
required this.onSelected,
|
||||
this.backgroundColor,
|
||||
this.borderColor,
|
||||
this.title,
|
||||
this.selectedItem});
|
||||
|
||||
@override
|
||||
State<KwDropdown<R>> createState() => _KwDropdownState<R>();
|
||||
}
|
||||
|
||||
class _KwDropdownState<R> extends State<KwDropdown<R>> {
|
||||
KwDropDownItem<R>? _selectedItem;
|
||||
|
||||
@override
|
||||
didUpdateWidget(KwDropdown<R> oldWidget) {
|
||||
if (oldWidget.selectedItem != widget.selectedItem) {
|
||||
_selectedItem = widget.selectedItem;
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_selectedItem ??= widget.selectedItem;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_selectedItem = widget.selectedItem;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (widget.title != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16,bottom: 4),
|
||||
child: Text(
|
||||
widget.title!,
|
||||
style: AppTextStyles.bodyTinyReg.copyWith(
|
||||
color: AppColors.blackGray,
|
||||
),
|
||||
),
|
||||
),
|
||||
IgnorePointer(
|
||||
ignoring: widget.items.isEmpty,
|
||||
child: KwPopupMenu(
|
||||
horizontalPadding: widget.horizontalPadding,
|
||||
fit: KwPopupMenuFit.expand,
|
||||
customButtonBuilder: (context, isOpened) {
|
||||
return _buildMenuButton(isOpened);
|
||||
},
|
||||
menuItems: widget.items
|
||||
.map((item) => KwPopupMenuItem(
|
||||
title: item.title,
|
||||
icon: item.icon,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_selectedItem = item;
|
||||
});
|
||||
widget.onSelected(item.data);
|
||||
}))
|
||||
.toList()),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Container _buildMenuButton(bool isOpened) {
|
||||
return Container(
|
||||
height: 48,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: widget.backgroundColor,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(
|
||||
color: isOpened
|
||||
? AppColors.bgColorDark
|
||||
: widget.borderColor ?? AppColors.grayStroke,
|
||||
width: 1),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
_selectedItem?.title ?? widget.hintText,
|
||||
style: AppTextStyles.bodyMediumReg.copyWith(
|
||||
color: _selectedItem == null
|
||||
? AppColors.blackGray
|
||||
: AppColors.blackBlack),
|
||||
)),
|
||||
AnimatedRotation(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
turns: isOpened ? -0.5 : 0,
|
||||
child: Assets.images.icons.caretDown.svg(
|
||||
width: 16,
|
||||
height: 16,
|
||||
colorFilter: const ColorFilter.mode(
|
||||
AppColors.blackGray, BlendMode.srcIn),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class KwDropDownItem<R> {
|
||||
final String title;
|
||||
final R data;
|
||||
final Widget? icon;
|
||||
|
||||
const KwDropDownItem({required this.data, required this.title, this.icon});
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class KwAnimatedImagePlaceholder extends StatefulWidget {
|
||||
const KwAnimatedImagePlaceholder({
|
||||
super.key,
|
||||
this.height = double.maxFinite,
|
||||
this.width = double.maxFinite,
|
||||
});
|
||||
|
||||
final double height;
|
||||
final double width;
|
||||
|
||||
@override
|
||||
State<KwAnimatedImagePlaceholder> createState() =>
|
||||
_KwAnimatedImagePlaceholderState();
|
||||
}
|
||||
|
||||
class _KwAnimatedImagePlaceholderState extends State<KwAnimatedImagePlaceholder>
|
||||
with TickerProviderStateMixin {
|
||||
late final AnimationController _controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: Durations.long4,
|
||||
animationBehavior: AnimationBehavior.preserve,
|
||||
)
|
||||
..forward()
|
||||
..addListener(
|
||||
() {
|
||||
if (!_controller.isCompleted) return;
|
||||
|
||||
if (_controller.value == 0) {
|
||||
_controller.forward();
|
||||
} else {
|
||||
_controller.reverse();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
late final Animation<double> _opacity = Tween(
|
||||
begin: 0.6,
|
||||
end: 0.2,
|
||||
).animate(_controller);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _controller,
|
||||
child: DecoratedBox(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.grey,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: widget.height,
|
||||
width: widget.width,
|
||||
),
|
||||
),
|
||||
builder: (context, child) {
|
||||
return Opacity(
|
||||
opacity: _opacity.value,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
|
||||
class KwTextInput extends StatefulWidget {
|
||||
final TextEditingController? controller;
|
||||
final void Function(String)? onChanged;
|
||||
final void Function(String)? onFieldSubmitted;
|
||||
final String? title;
|
||||
final String? hintText;
|
||||
final String? helperText;
|
||||
final bool obscureText;
|
||||
final bool enabled;
|
||||
final bool readOnly;
|
||||
final bool showError;
|
||||
final TextInputType? keyboardType;
|
||||
final List<TextInputFormatter>? inputFormatters;
|
||||
final bool showCounter;
|
||||
final FocusNode? focusNode;
|
||||
final TextInputAction? textInputAction;
|
||||
final double minHeight;
|
||||
final TextStyle? textStyle;
|
||||
final Widget? suffixIcon;
|
||||
final int? minLines;
|
||||
final int? maxLines;
|
||||
final int? maxLength;
|
||||
final double radius;
|
||||
final Color? borderColor;
|
||||
final Null Function(bool hasFocus)? onFocusChanged;
|
||||
|
||||
const KwTextInput({
|
||||
super.key,
|
||||
required this.controller,
|
||||
this.title,
|
||||
this.onChanged,
|
||||
this.onFieldSubmitted,
|
||||
this.hintText,
|
||||
this.helperText,
|
||||
this.minHeight = standardHeight,
|
||||
this.suffixIcon,
|
||||
this.obscureText = false,
|
||||
this.showError = false,
|
||||
this.enabled = true,
|
||||
this.readOnly = false,
|
||||
this.keyboardType,
|
||||
this.inputFormatters,
|
||||
this.showCounter = false,
|
||||
this.focusNode,
|
||||
this.textInputAction,
|
||||
this.textStyle,
|
||||
this.maxLength,
|
||||
this.minLines,
|
||||
this.maxLines,
|
||||
this.radius = 12,
|
||||
this.borderColor,
|
||||
this.onFocusChanged,
|
||||
});
|
||||
|
||||
static const standardHeight = 48.0;
|
||||
|
||||
@override
|
||||
State<KwTextInput> createState() => _KwTextInputState();
|
||||
}
|
||||
|
||||
class _KwTextInputState extends State<KwTextInput> {
|
||||
late FocusNode _focusNode;
|
||||
|
||||
@override
|
||||
initState() {
|
||||
_focusNode = widget.focusNode ?? FocusNode();
|
||||
_focusNode.addListener(() {
|
||||
setState(() {});
|
||||
widget.onFocusChanged?.call(_focusNode.hasFocus);
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Color _helperTextColor() {
|
||||
if (widget.showError) {
|
||||
return AppColors.statusError;
|
||||
} else {
|
||||
if (!widget.enabled) {
|
||||
return AppColors.grayDisable;
|
||||
}
|
||||
return AppColors.bgColorDark;
|
||||
}
|
||||
}
|
||||
|
||||
Color _borderColor() {
|
||||
if (!widget.enabled) {
|
||||
return AppColors.grayDisable;
|
||||
}
|
||||
|
||||
if (widget.showError ||
|
||||
widget.maxLength != null &&
|
||||
(widget.controller?.text.length ?? 0) > widget.maxLength!) {
|
||||
return AppColors.statusError;
|
||||
}
|
||||
|
||||
if (_focusNode.hasFocus) {
|
||||
return AppColors.bgColorDark;
|
||||
}
|
||||
|
||||
return widget.borderColor ?? AppColors.grayStroke;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.title != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
child: Text(
|
||||
widget.title!,
|
||||
style: AppTextStyles.bodyTinyReg.copyWith(
|
||||
color: widget.enabled
|
||||
? AppColors.blackGray
|
||||
: AppColors.grayDisable,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.title != null) const SizedBox(height: 4),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
if (widget.enabled && !widget.readOnly) {
|
||||
_focusNode.requestFocus();
|
||||
}
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.only(bottom: widget.showCounter ? 24 : 0),
|
||||
alignment: Alignment.topCenter,
|
||||
constraints: BoxConstraints(
|
||||
minHeight: widget.minHeight,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.all(Radius.circular(
|
||||
widget.minHeight > KwTextInput.standardHeight
|
||||
? widget.radius
|
||||
: widget.minHeight / 2)),
|
||||
border: Border.all(
|
||||
color: _borderColor(),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
focusNode: _focusNode,
|
||||
inputFormatters: widget.inputFormatters,
|
||||
keyboardType: widget.keyboardType,
|
||||
enabled: widget.enabled && !widget.readOnly,
|
||||
controller: widget.controller,
|
||||
obscureText: widget.obscureText,
|
||||
maxLines: widget.maxLines,
|
||||
minLines: widget.minLines ?? 1,
|
||||
maxLength: widget.maxLength,
|
||||
onChanged: widget.onChanged,
|
||||
textInputAction: widget.textInputAction,
|
||||
onFieldSubmitted: widget.onFieldSubmitted,
|
||||
onTapOutside: (_) {
|
||||
_focusNode.unfocus();
|
||||
},
|
||||
style: widget.textStyle ??
|
||||
AppTextStyles.bodyMediumReg.copyWith(
|
||||
color: !widget.enabled
|
||||
? AppColors.grayDisable
|
||||
: null,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
counter: const SizedBox.shrink(),
|
||||
fillColor: Colors.transparent,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16, vertical: 8),
|
||||
hintText: widget.hintText,
|
||||
// errorStyle: p2pTextStyles.paragraphSmall(
|
||||
// color: p2pColors.borderDanger),
|
||||
hintStyle: widget.textStyle ??
|
||||
AppTextStyles.bodyMediumReg.copyWith(
|
||||
color: widget.enabled
|
||||
? AppColors.blackGray
|
||||
: AppColors.grayDisable,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.suffixIcon != null)
|
||||
SizedBox(
|
||||
child: widget.suffixIcon!,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.showCounter)
|
||||
Positioned(
|
||||
bottom: 12,
|
||||
left: 12,
|
||||
child: Text(
|
||||
'${widget.controller?.text.length}/'
|
||||
'${(widget.maxLength ?? 0)}',
|
||||
style: AppTextStyles.bodySmallReg.copyWith(
|
||||
color: (widget.controller?.text.length ?? 0) >
|
||||
(widget.maxLength ?? 0)
|
||||
? AppColors.statusError
|
||||
: AppColors.blackGray),
|
||||
),
|
||||
),
|
||||
if (widget.minHeight > KwTextInput.standardHeight)
|
||||
Positioned(
|
||||
bottom: 12,
|
||||
right: 12,
|
||||
child: Assets.images.icons.textFieldNotches.svg(
|
||||
height: 12,
|
||||
width: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.helperText != null &&
|
||||
(widget.helperText?.isNotEmpty ?? false)) ...[
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
const Gap(16),
|
||||
Text(
|
||||
widget.helperText!,
|
||||
style: AppTextStyles.bodyTinyReg.copyWith(
|
||||
height: 1,
|
||||
color: _helperTextColor(),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
]
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class KwLoadingOverlay extends StatefulWidget {
|
||||
const KwLoadingOverlay({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.controller,
|
||||
this.shouldShowLoading,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final OverlayPortalController? controller;
|
||||
final bool? shouldShowLoading;
|
||||
|
||||
@override
|
||||
State<KwLoadingOverlay> createState() => _KwLoadingOverlayState();
|
||||
}
|
||||
|
||||
class _KwLoadingOverlayState extends State<KwLoadingOverlay> {
|
||||
late final OverlayPortalController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_controller = widget.controller ?? OverlayPortalController();
|
||||
super.initState();
|
||||
|
||||
if (widget.shouldShowLoading ?? false) {
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => _controller.show(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant KwLoadingOverlay oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) {
|
||||
if (widget.shouldShowLoading == null) return;
|
||||
|
||||
if (widget.shouldShowLoading!) {
|
||||
_controller.show();
|
||||
} else {
|
||||
_controller.hide();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return OverlayPortal(
|
||||
controller: _controller,
|
||||
overlayChildBuilder: (context) {
|
||||
return const SizedBox(
|
||||
height: double.maxFinite,
|
||||
width: double.maxFinite,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black38,
|
||||
),
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (context.mounted) _controller.hide();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
|
||||
class KwOptionSelector extends StatelessWidget {
|
||||
const KwOptionSelector({
|
||||
super.key,
|
||||
required this.selectedIndex,
|
||||
required this.onChanged,
|
||||
this.title,
|
||||
required this.items,
|
||||
this.height = 46,
|
||||
this.spacer = 4,
|
||||
this.backgroundColor,
|
||||
this.selectedColor = AppColors.bgColorDark,
|
||||
this.itemColor,
|
||||
this.itemBorder,
|
||||
this.borderRadius,
|
||||
this.itemAlign,
|
||||
this.selectedTextStyle,
|
||||
this.textStyle,
|
||||
double? selectorHeight,
|
||||
}) : _selectorHeight = selectorHeight ?? height;
|
||||
|
||||
final int? selectedIndex;
|
||||
final double height;
|
||||
final double spacer;
|
||||
final Function(int index) onChanged;
|
||||
final String? title;
|
||||
final List<String> items;
|
||||
final Color? backgroundColor;
|
||||
final BorderRadius? borderRadius;
|
||||
final Color selectedColor;
|
||||
final Color? itemColor;
|
||||
final Border? itemBorder;
|
||||
final double _selectorHeight;
|
||||
final Alignment? itemAlign;
|
||||
|
||||
final TextStyle? textStyle;
|
||||
final TextStyle? selectedTextStyle;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var borderRadius = BorderRadius.all(Radius.circular(height / 2));
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (title != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16, bottom: 4),
|
||||
child: Text(
|
||||
title!,
|
||||
style: AppTextStyles.bodyTinyReg
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
),
|
||||
LayoutBuilder(
|
||||
builder: (builderContext, constraints) {
|
||||
final itemWidth =
|
||||
(constraints.maxWidth - spacer * (items.length - 1)) /
|
||||
items.length;
|
||||
|
||||
return Container(
|
||||
height: height,
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
if (selectedIndex != null)
|
||||
AnimatedAlign(
|
||||
alignment: Alignment(
|
||||
selectedIndex! * 2 / (items.length - 1) - 1,
|
||||
1,
|
||||
),
|
||||
duration: Durations.short4,
|
||||
child: Container(
|
||||
height: _selectorHeight,
|
||||
width: itemWidth,
|
||||
decoration: BoxDecoration(
|
||||
color: selectedColor,
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
spacing: spacer,
|
||||
children: [
|
||||
for (int index = 0; index < items.length; index++)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
onChanged(index);
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
height: height,
|
||||
width: itemWidth,
|
||||
decoration: BoxDecoration(
|
||||
color: index == selectedIndex ? null : itemColor,
|
||||
borderRadius: borderRadius,
|
||||
border:
|
||||
index == selectedIndex ? null : itemBorder,
|
||||
),
|
||||
duration: Durations.short2,
|
||||
child: Align(
|
||||
alignment: itemAlign ?? Alignment.center,
|
||||
child: AnimatedDefaultTextStyle(
|
||||
duration: Durations.short4,
|
||||
style: index == selectedIndex
|
||||
? (selectedTextStyle ??
|
||||
AppTextStyles.bodyMediumMed.copyWith(
|
||||
color: AppColors.grayWhite))
|
||||
: (textStyle ??
|
||||
AppTextStyles.bodyMediumReg.copyWith(
|
||||
color: AppColors.blackGray)),
|
||||
child: Text(items[index]),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
|
||||
class KwPhoneInput extends StatefulWidget {
|
||||
final String? title;
|
||||
final String? error;
|
||||
final TextEditingController? controller;
|
||||
final void Function(String)? onChanged;
|
||||
final Color? borderColor;
|
||||
final FocusNode? focusNode;
|
||||
final bool showError;
|
||||
final String? helperText;
|
||||
final bool enabled;
|
||||
|
||||
const KwPhoneInput({
|
||||
super.key,
|
||||
this.title,
|
||||
this.error,
|
||||
this.borderColor ,
|
||||
this.controller,
|
||||
this.onChanged,
|
||||
this.focusNode,
|
||||
this.showError = false,
|
||||
this.helperText,
|
||||
this.enabled = true,
|
||||
});
|
||||
|
||||
@override
|
||||
State<KwPhoneInput> createState() => _KWPhoneInputState();
|
||||
}
|
||||
|
||||
class _KWPhoneInputState extends State<KwPhoneInput> {
|
||||
late FocusNode _focusNode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_focusNode = widget.focusNode ?? FocusNode();
|
||||
_focusNode.addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Color _borderColor() {
|
||||
if (!widget.enabled) {
|
||||
return AppColors.grayDisable;
|
||||
}
|
||||
|
||||
if (_focusNode.hasFocus) {
|
||||
return AppColors.bgColorDark;
|
||||
}
|
||||
|
||||
return widget.borderColor ?? AppColors.grayStroke;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if(widget.title != null) ...[
|
||||
_buildLabel(),
|
||||
const SizedBox(height: 4),
|
||||
],
|
||||
_buildInputRow(),
|
||||
_buildError()
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Container _buildInputRow() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(24),
|
||||
),
|
||||
border: Border.all(
|
||||
color: _borderColor(),
|
||||
width: 1,
|
||||
),
|
||||
color: Colors.transparent,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildCountryPicker(),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
focusNode: _focusNode,
|
||||
controller: widget.controller,
|
||||
onChanged: widget.onChanged,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Enter your number',
|
||||
hintStyle: AppTextStyles.bodyMediumReg.copyWith(
|
||||
color: AppColors.blackGray,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
filled: true,
|
||||
fillColor: Colors.transparent,
|
||||
),
|
||||
style: AppTextStyles.bodyMediumReg,
|
||||
keyboardType: TextInputType.phone,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Padding _buildLabel() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
child: Text(
|
||||
widget.title!,
|
||||
style: AppTextStyles.bodyTinyReg.copyWith(
|
||||
color: AppColors.blackGray,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCountryPicker() {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Feedback.forTap(context);
|
||||
//TODO(Heorhii): Add country selection functionality
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
const Gap(12),
|
||||
const CircleAvatar(
|
||||
radius: 12,
|
||||
backgroundImage: NetworkImage(
|
||||
'https://flagcdn.com/w320/us.png',
|
||||
),
|
||||
),
|
||||
//TODO// dont show arrow
|
||||
// const Gap(6),
|
||||
// const Icon(
|
||||
// Icons.keyboard_arrow_down_rounded,
|
||||
// color: AppColors.blackGray,
|
||||
// opticalSize: 16,
|
||||
// ),
|
||||
const Gap(12),
|
||||
SizedBox(
|
||||
height: 48,
|
||||
child: VerticalDivider(
|
||||
width: 1,
|
||||
color: _borderColor(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_buildError() {
|
||||
return AnimatedSize(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Container(
|
||||
height: widget.error == null ? 0 : 24,
|
||||
clipBehavior: Clip.none,
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: Text(
|
||||
widget.error ?? '',
|
||||
style: AppTextStyles.bodyTinyMed.copyWith(
|
||||
color: AppColors.statusError,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,514 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
|
||||
class KwPopUpButton extends StatefulWidget {
|
||||
final double height;
|
||||
final bool disabled;
|
||||
final bool withBorder;
|
||||
final KwPopupButtonColorPallet colorPallet;
|
||||
final double popUpPadding;
|
||||
final String label;
|
||||
final List<KwPopUpButtonItem> items;
|
||||
|
||||
KwPopUpButton(
|
||||
{super.key,
|
||||
required this.label,
|
||||
required this.items,
|
||||
colorPallet,
|
||||
this.height = 52,
|
||||
this.withBorder = false,
|
||||
this.disabled = false,
|
||||
required this.popUpPadding})
|
||||
: colorPallet = colorPallet ?? KwPopupButtonColorPallet.dark();
|
||||
|
||||
@override
|
||||
State<KwPopUpButton> createState() => _KwPopUpButtonState();
|
||||
}
|
||||
|
||||
class _KwPopUpButtonState extends State<KwPopUpButton> {
|
||||
final _layerLink = LayerLink();
|
||||
double opacity = 0.0;
|
||||
final _KwButtonListenableOverlayPortalController _controller =
|
||||
_KwButtonListenableOverlayPortalController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller.addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
|
||||
_controller.addOnHideListener(_hide);
|
||||
_controller.addOnShowListener(_show);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _buildButton(context);
|
||||
}
|
||||
|
||||
Widget _buildButton(BuildContext context) {
|
||||
return _KwButtonPopUpOverlayMenu(
|
||||
opacity: opacity,
|
||||
controller: _controller,
|
||||
layerLink: _layerLink,
|
||||
popUpPadding: widget.popUpPadding,
|
||||
items: widget.items,
|
||||
child: CompositedTransformTarget(
|
||||
link: _layerLink,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
height: widget.height,
|
||||
decoration: BoxDecoration(
|
||||
color: _getBgColor(),
|
||||
border: _getBorder(),
|
||||
borderRadius: BorderRadius.circular(widget.height / 2),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTapDown: widget.disabled ? null : _onTapDown,
|
||||
borderRadius: BorderRadius.circular(widget.height / 2),
|
||||
highlightColor: widget.colorPallet.pressedBdColor,
|
||||
splashColor: widget.colorPallet.pressedBdColor,
|
||||
child: _buildButtonContent(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_buildButtonContent(BuildContext context) {
|
||||
return Center(
|
||||
child: Row(
|
||||
children: [
|
||||
const Gap(52),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Text(
|
||||
widget.label,
|
||||
style:
|
||||
AppTextStyles.bodyMediumMed.copyWith(color: _getTextColor()),
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
height: 52,
|
||||
width: 52,
|
||||
decoration: BoxDecoration(
|
||||
color: _getDropDownBgColor(),
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(widget.height / 2),
|
||||
bottomRight: Radius.circular(widget.height / 2),
|
||||
),
|
||||
),
|
||||
child: AnimatedRotation(
|
||||
turns: opacity * 0.5,
|
||||
duration: const Duration(milliseconds: 150),
|
||||
child: Icon(
|
||||
Icons.keyboard_arrow_down_rounded,
|
||||
color: _getIconColor(),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onTapDown(details) {
|
||||
if (_controller.isShowing) {
|
||||
_controller.hide();
|
||||
} else {
|
||||
_controller.show();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _hide() async {
|
||||
setState(() {
|
||||
opacity = 0.0;
|
||||
});
|
||||
await Future.delayed(const Duration(milliseconds: 150), () {});
|
||||
}
|
||||
|
||||
Future<void> _show() async {
|
||||
WidgetsBinding.instance.addPostFrameCallback((call) {
|
||||
setState(() {
|
||||
opacity = 1.0;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Border? _getBorder() {
|
||||
return widget.withBorder
|
||||
? Border.all(
|
||||
color: widget.disabled
|
||||
? widget.colorPallet.dropDownDisabledBgColor
|
||||
: _controller.isShowing
|
||||
? widget.colorPallet.dropDownPressedBgColor
|
||||
: widget.colorPallet.dropDownBgColor,
|
||||
width: 1)
|
||||
: null;
|
||||
}
|
||||
|
||||
Color _getDropDownBgColor() {
|
||||
return widget.disabled
|
||||
? widget.colorPallet.dropDownDisabledBgColor
|
||||
: _controller.isShowing
|
||||
? widget.colorPallet.dropDownPressedBgColor
|
||||
: widget.colorPallet.dropDownBgColor;
|
||||
}
|
||||
|
||||
Color _getBgColor() {
|
||||
return widget.disabled
|
||||
? widget.colorPallet.disabledBdColor
|
||||
: widget.colorPallet.bgColor;
|
||||
}
|
||||
|
||||
Color _getTextColor() {
|
||||
return (_controller.isShowing
|
||||
? widget.colorPallet.textPressedColor
|
||||
: widget.disabled
|
||||
? widget.colorPallet.textDisabledColor
|
||||
: widget.colorPallet.textColor);
|
||||
}
|
||||
|
||||
Color _getIconColor() {
|
||||
return (_controller.isShowing
|
||||
? widget.colorPallet.iconPressedColor
|
||||
: widget.disabled
|
||||
? widget.colorPallet.iconDisabledColor
|
||||
: widget.colorPallet.iconColor);
|
||||
}
|
||||
}
|
||||
|
||||
class KwPopUpButtonItem {
|
||||
final String title;
|
||||
final VoidCallback onTap;
|
||||
final Color color;
|
||||
|
||||
KwPopUpButtonItem(
|
||||
{required this.title,
|
||||
required this.onTap,
|
||||
this.color = AppColors.blackBlack});
|
||||
}
|
||||
|
||||
class KwPopupButtonColorPallet {
|
||||
final Color textColor;
|
||||
final Color textPressedColor;
|
||||
final Color textDisabledColor;
|
||||
final Color bgColor;
|
||||
final Color pressedBdColor;
|
||||
final Color disabledBdColor;
|
||||
final Color iconColor;
|
||||
final Color iconPressedColor;
|
||||
final Color iconDisabledColor;
|
||||
final Color dropDownBgColor;
|
||||
final Color dropDownPressedBgColor;
|
||||
final Color dropDownDisabledBgColor;
|
||||
|
||||
const KwPopupButtonColorPallet._(
|
||||
{required this.textColor,
|
||||
required this.textPressedColor,
|
||||
required this.textDisabledColor,
|
||||
required this.bgColor,
|
||||
required this.pressedBdColor,
|
||||
required this.disabledBdColor,
|
||||
required this.iconColor,
|
||||
required this.iconPressedColor,
|
||||
required this.iconDisabledColor,
|
||||
required this.dropDownBgColor,
|
||||
required this.dropDownPressedBgColor,
|
||||
required this.dropDownDisabledBgColor});
|
||||
|
||||
factory KwPopupButtonColorPallet.dark() {
|
||||
return const KwPopupButtonColorPallet._(
|
||||
textColor: AppColors.grayWhite,
|
||||
textPressedColor: AppColors.grayWhite,
|
||||
textDisabledColor: AppColors.grayWhite,
|
||||
bgColor: AppColors.bgColorDark,
|
||||
pressedBdColor: AppColors.darkBgActiveButtonState,
|
||||
disabledBdColor: AppColors.grayDisable,
|
||||
iconColor: AppColors.grayWhite,
|
||||
iconPressedColor: AppColors.grayWhite,
|
||||
iconDisabledColor: AppColors.grayWhite,
|
||||
dropDownBgColor: AppColors.darkBgBgElements,
|
||||
dropDownPressedBgColor: AppColors.darkBgStroke,
|
||||
dropDownDisabledBgColor: AppColors.grayDisable,
|
||||
);
|
||||
}
|
||||
|
||||
factory KwPopupButtonColorPallet.yellow() {
|
||||
return const KwPopupButtonColorPallet._(
|
||||
textColor: AppColors.blackBlack,
|
||||
textPressedColor: AppColors.blackBlack,
|
||||
textDisabledColor: AppColors.grayWhite,
|
||||
bgColor: AppColors.primaryYellow,
|
||||
pressedBdColor: AppColors.buttonPrimaryYellowActive,
|
||||
disabledBdColor: AppColors.grayDisable,
|
||||
iconColor: AppColors.blackBlack,
|
||||
iconPressedColor: AppColors.blackBlack,
|
||||
iconDisabledColor: AppColors.grayWhite,
|
||||
dropDownBgColor: AppColors.buttonPrimaryYellowDrop,
|
||||
dropDownPressedBgColor: AppColors.buttonPrimaryYellowActiveDrop,
|
||||
dropDownDisabledBgColor: AppColors.grayDisable,
|
||||
);
|
||||
}
|
||||
|
||||
factory KwPopupButtonColorPallet.transparent() {
|
||||
return const KwPopupButtonColorPallet._(
|
||||
textColor: AppColors.blackBlack,
|
||||
textPressedColor: AppColors.blackBlack,
|
||||
textDisabledColor: AppColors.grayDisable,
|
||||
bgColor: Colors.transparent,
|
||||
pressedBdColor: Colors.transparent,
|
||||
disabledBdColor: Colors.transparent,
|
||||
iconColor: AppColors.blackBlack,
|
||||
iconPressedColor: AppColors.grayWhite,
|
||||
iconDisabledColor: AppColors.grayWhite,
|
||||
dropDownBgColor: AppColors.buttonOutline,
|
||||
dropDownPressedBgColor: AppColors.darkBgActiveButtonState,
|
||||
dropDownDisabledBgColor: AppColors.grayDisable,
|
||||
);
|
||||
}
|
||||
|
||||
KwPopupButtonColorPallet copyWith({
|
||||
Color? textColor,
|
||||
Color? textPressedColor,
|
||||
Color? textDisabledColor,
|
||||
Color? bgColor,
|
||||
Color? pressedBdColor,
|
||||
Color? disabledBdColor,
|
||||
Color? iconColor,
|
||||
Color? iconPressedColor,
|
||||
Color? iconDisabledColor,
|
||||
Color? dropDownBgColor,
|
||||
Color? dropDownPressedBgColor,
|
||||
Color? dropDownDisabledBgColor,
|
||||
}) {
|
||||
return KwPopupButtonColorPallet._(
|
||||
textColor: textColor ?? this.textColor,
|
||||
textPressedColor: textPressedColor ?? this.textPressedColor,
|
||||
textDisabledColor: textDisabledColor ?? this.textDisabledColor,
|
||||
bgColor: bgColor ?? this.bgColor,
|
||||
pressedBdColor: pressedBdColor ?? this.pressedBdColor,
|
||||
disabledBdColor: disabledBdColor ?? this.disabledBdColor,
|
||||
iconColor: iconColor ?? this.iconColor,
|
||||
iconPressedColor: iconPressedColor ?? this.iconPressedColor,
|
||||
iconDisabledColor: iconDisabledColor ?? this.iconDisabledColor,
|
||||
dropDownBgColor: dropDownBgColor ?? this.dropDownBgColor,
|
||||
dropDownPressedBgColor:
|
||||
dropDownPressedBgColor ?? this.dropDownPressedBgColor,
|
||||
dropDownDisabledBgColor:
|
||||
dropDownDisabledBgColor ?? this.dropDownDisabledBgColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _KwButtonPopUpOverlayMenu extends StatefulWidget {
|
||||
const _KwButtonPopUpOverlayMenu({
|
||||
required this.child,
|
||||
required this.controller,
|
||||
required this.opacity,
|
||||
required this.layerLink,
|
||||
required this.popUpPadding,
|
||||
required this.items,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final _KwButtonListenableOverlayPortalController controller;
|
||||
|
||||
final double opacity;
|
||||
|
||||
final LayerLink layerLink;
|
||||
|
||||
final double popUpPadding;
|
||||
|
||||
final List<KwPopUpButtonItem> items;
|
||||
|
||||
@override
|
||||
State<_KwButtonPopUpOverlayMenu> createState() =>
|
||||
_KwButtonPopUpOverlayMenuState();
|
||||
}
|
||||
|
||||
class _KwButtonPopUpOverlayMenuState extends State<_KwButtonPopUpOverlayMenu> {
|
||||
late final _KwButtonListenableOverlayPortalController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_controller = widget.controller;
|
||||
|
||||
_controller.addListener(() {
|
||||
try {
|
||||
if (context.mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
} catch (e) {
|
||||
print(e);
|
||||
}
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return OverlayPortal(
|
||||
controller: _controller,
|
||||
overlayChildBuilder: (context) {
|
||||
return CompositedTransformFollower(
|
||||
followerAnchor: Alignment.bottomCenter,
|
||||
targetAnchor: Alignment.topCenter,
|
||||
offset: const Offset(0, -8),
|
||||
link: widget.layerLink,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
_controller.hide();
|
||||
},
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
child: Padding(
|
||||
padding:
|
||||
EdgeInsets.symmetric(horizontal: widget.popUpPadding),
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
opacity: widget.opacity,
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.grayWhite,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(
|
||||
color: AppColors.grayTintStroke, width: 1),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color:
|
||||
Colors.black.withAlpha((255 * 0.07).toInt()),
|
||||
offset: const Offset(0, 8),
|
||||
blurRadius: 17,
|
||||
),
|
||||
BoxShadow(
|
||||
color:
|
||||
Colors.black.withAlpha((255 * 0.06).toInt()),
|
||||
offset: const Offset(0, 30),
|
||||
blurRadius: 30,
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: widget.items
|
||||
.expand((e) => [
|
||||
_buildItem(e.title, e.onTap, e.color),
|
||||
if (widget.items.last != e)
|
||||
const Divider(
|
||||
color: AppColors.grayTintStroke,
|
||||
height: 0,
|
||||
),
|
||||
])
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)),
|
||||
);
|
||||
},
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildItem(String title, VoidCallback onTap, Color color) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
onTap();
|
||||
_controller.hide();
|
||||
},
|
||||
child: Container(
|
||||
height: 52,
|
||||
color: Colors.transparent,
|
||||
child: Center(
|
||||
child: Text(
|
||||
title,
|
||||
style: AppTextStyles.bodyMediumMed.copyWith(color: color),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (context.mounted) _controller.hide();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class _KwButtonListenableOverlayPortalController
|
||||
extends OverlayPortalController {
|
||||
List<VoidCallback> listeners = [];
|
||||
Future<void> Function()? onShow;
|
||||
Future<void> Function()? onHide;
|
||||
|
||||
_KwButtonListenableOverlayPortalController();
|
||||
|
||||
addOnShowListener(Future<void> Function() listener) {
|
||||
onShow = listener;
|
||||
}
|
||||
|
||||
addOnHideListener(Future<void> Function() listener) {
|
||||
onHide = listener;
|
||||
}
|
||||
|
||||
@override
|
||||
void show() async {
|
||||
super.show();
|
||||
try {
|
||||
for (var element in listeners) {
|
||||
element();
|
||||
}
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (onShow != null) {
|
||||
await onShow!();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void hide() async {
|
||||
if (onHide != null) {
|
||||
await onHide!();
|
||||
}
|
||||
try {
|
||||
super.hide();
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
for (var element in listeners) {
|
||||
try {
|
||||
element();
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void addListener(VoidCallback listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/_custom_popup_menu.dart';
|
||||
|
||||
enum KwPopupMenuFit { expand, loose }
|
||||
|
||||
class KwPopupMenu extends StatefulWidget {
|
||||
final Widget Function(BuildContext, bool menuIsShowmn)? customButtonBuilder;
|
||||
final List<KwPopupMenuItem> menuItems;
|
||||
final double? horizontalMargin;
|
||||
final KwPopupMenuFit fit;
|
||||
final double horizontalPadding;
|
||||
final CustomPopupMenuController? controller;
|
||||
|
||||
|
||||
const KwPopupMenu({
|
||||
this.customButtonBuilder,
|
||||
required this.menuItems,
|
||||
this.fit = KwPopupMenuFit.loose,
|
||||
this.horizontalMargin,
|
||||
this.horizontalPadding = 0,
|
||||
this.controller,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<KwPopupMenu> createState() => _KwPopupMenuState();
|
||||
}
|
||||
|
||||
class _KwPopupMenuState extends State<KwPopupMenu> {
|
||||
late CustomPopupMenuController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_controller = widget.controller ?? CustomPopupMenuController();
|
||||
super.initState();
|
||||
_controller.addListener(() {
|
||||
if (mounted) setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomPopupMenu(
|
||||
horizontalMargin: widget.horizontalMargin ?? 0,
|
||||
controller: _controller,
|
||||
verticalMargin: 4,
|
||||
position: PreferredPosition.bottom,
|
||||
showArrow: false,
|
||||
enablePassEvent: false,
|
||||
barrierColor: Colors.transparent,
|
||||
menuBuilder: widget.menuItems.isEmpty
|
||||
? null
|
||||
: () {
|
||||
return Row(
|
||||
mainAxisSize: widget.fit == KwPopupMenuFit.expand
|
||||
? MainAxisSize.max
|
||||
: MainAxisSize.min,
|
||||
children: [
|
||||
widget.fit == KwPopupMenuFit.expand
|
||||
? Expanded(
|
||||
child: _buildItem(),
|
||||
)
|
||||
: _buildItem()
|
||||
],
|
||||
);
|
||||
},
|
||||
pressType: PressType.singleClick,
|
||||
child: widget.customButtonBuilder
|
||||
?.call(context, _controller.menuIsShowing) ??
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
height: 32,
|
||||
width: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: _controller.menuIsShowing
|
||||
? AppColors.grayTintStroke
|
||||
: AppColors.grayWhite,
|
||||
shape: BoxShape.circle,
|
||||
border: _controller.menuIsShowing
|
||||
? null
|
||||
: Border.all(color: AppColors.grayTintStroke, width: 1)),
|
||||
child: Center(child: Assets.images.icons.more.svg()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Container _buildItem() {
|
||||
return Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: widget.horizontalPadding),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: AppColors.grayTintStroke, width: 1),
|
||||
color: AppColors.grayWhite,
|
||||
),
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: 210,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: IntrinsicWidth(
|
||||
child: Column(
|
||||
children: [
|
||||
for (var i = 0; i < widget.menuItems.length; i++)
|
||||
Material(
|
||||
type: MaterialType.transparency,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
widget.menuItems[i].onTap();
|
||||
_controller.hideMenu();
|
||||
},
|
||||
child: Container(
|
||||
height: 42,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: i == 0
|
||||
? null
|
||||
: const BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: AppColors.grayTintStroke,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (widget.menuItems[i].icon != null) ...[
|
||||
widget.menuItems[i].icon!,
|
||||
const Gap(4),
|
||||
],
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.menuItems[i].title,
|
||||
style: widget.menuItems[i].textStyle ??
|
||||
AppTextStyles.bodyMediumReg,
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class KwPopupMenuItem {
|
||||
final String title;
|
||||
final Widget? icon;
|
||||
|
||||
final VoidCallback onTap;
|
||||
|
||||
final TextStyle? textStyle;
|
||||
|
||||
const KwPopupMenuItem({
|
||||
required this.title,
|
||||
required this.onTap,
|
||||
this.icon,
|
||||
this.textStyle,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/_custom_popup_menu.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_input.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_popup_menu.dart';
|
||||
|
||||
@immutable
|
||||
class KwSuggestionInput<R> extends StatefulWidget {
|
||||
final debounceDuration = const Duration(milliseconds: 400);
|
||||
|
||||
final String? title;
|
||||
final String? hintText;
|
||||
final Iterable<R> items;
|
||||
final String Function(R item) itemToStringBuilder;
|
||||
final Function(R item) onSelected;
|
||||
final Function(String query) onQueryChanged;
|
||||
final String? initialText;
|
||||
final double horizontalPadding;
|
||||
final Color? backgroundColor;
|
||||
final Color? borderColor;
|
||||
|
||||
const KwSuggestionInput({
|
||||
super.key,
|
||||
this.initialText,
|
||||
required this.hintText,
|
||||
required this.itemToStringBuilder,
|
||||
required this.items,
|
||||
required this.onSelected,
|
||||
required this.onQueryChanged,
|
||||
this.horizontalPadding = 0,
|
||||
this.backgroundColor,
|
||||
this.borderColor,
|
||||
this.title,
|
||||
});
|
||||
|
||||
@override
|
||||
State<KwSuggestionInput<R>> createState() => _KwSuggestionInputState<R>();
|
||||
}
|
||||
|
||||
class _KwSuggestionInputState<R> extends State<KwSuggestionInput<R>> {
|
||||
R? selectedItem;
|
||||
var dropdownController = CustomPopupMenuController();
|
||||
late TextEditingController _textController;
|
||||
late FocusNode _focusNode;
|
||||
|
||||
Timer? _debounce;
|
||||
|
||||
UniqueKey key = UniqueKey();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_textController = TextEditingController(text: widget.initialText);
|
||||
_focusNode = FocusNode();
|
||||
_textController.addListener(_onTextChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_textController.removeListener(_onTextChanged);
|
||||
_textController.dispose();
|
||||
_debounce?.cancel();
|
||||
dropdownController.dispose();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onTextChanged() {
|
||||
if (_debounce?.isActive ?? false) _debounce?.cancel();
|
||||
_debounce = Timer(widget.debounceDuration, () {
|
||||
if (selectedItem == null ||
|
||||
widget.itemToStringBuilder(selectedItem as R) !=
|
||||
_textController.text) {
|
||||
widget.onQueryChanged(_textController.text);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant KwSuggestionInput<R> oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
if (oldWidget.initialText != widget.initialText) {
|
||||
_textController.text = widget.initialText!;
|
||||
}
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) {
|
||||
if (oldWidget.items != widget.items) {
|
||||
dropdownController.showMenu();
|
||||
} else {
|
||||
dropdownController.setState();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
KwPopupMenu(
|
||||
controller: dropdownController,
|
||||
horizontalPadding: widget.horizontalPadding,
|
||||
fit: KwPopupMenuFit.expand,
|
||||
customButtonBuilder: (context, isOpened) {
|
||||
return _buildMenuButton(isOpened);
|
||||
},
|
||||
menuItems: widget.items
|
||||
.map((item) => KwPopupMenuItem(
|
||||
title: widget.itemToStringBuilder(item),
|
||||
onTap: () {
|
||||
selectedItem = item;
|
||||
_textController.text = widget.itemToStringBuilder(item);
|
||||
dropdownController.hideMenu();
|
||||
widget.onSelected(item);
|
||||
}))
|
||||
.toList()),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMenuButton(bool isOpened) {
|
||||
return KwTextInput(
|
||||
title: widget.title,
|
||||
hintText: widget.hintText,
|
||||
controller: _textController,
|
||||
focusNode: _focusNode,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
|
||||
class KwTabBar extends StatefulWidget {
|
||||
final List<String> tabs;
|
||||
final void Function(int index) onTap;
|
||||
final List<int>? flexes;
|
||||
final bool forceScroll;
|
||||
|
||||
const KwTabBar(
|
||||
{super.key,
|
||||
required this.tabs,
|
||||
required this.onTap,
|
||||
this.flexes,
|
||||
this.forceScroll = false});
|
||||
|
||||
@override
|
||||
State<KwTabBar> createState() => _KwTabBarState();
|
||||
}
|
||||
|
||||
class _KwTabBarState extends State<KwTabBar>
|
||||
with SingleTickerProviderStateMixin {
|
||||
var keyMaps = <int, GlobalKey>{};
|
||||
var tabPadding = 4.0;
|
||||
int _selectedIndex = 0;
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _animation;
|
||||
late ScrollController _horScrollController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_horScrollController = ScrollController();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
vsync: this,
|
||||
);
|
||||
_animation = Tween<double>(begin: 0, end: 0).animate(_controller);
|
||||
}
|
||||
|
||||
@override
|
||||
dispose() {
|
||||
_controller.dispose();
|
||||
_horScrollController.dispose();
|
||||
_animation.removeListener(() {});
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _setSelectedIndex(int index) {
|
||||
if (_horScrollController.hasClients) {
|
||||
_scrollToSelected(index);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_selectedIndex = index;
|
||||
_animation = Tween<double>(
|
||||
begin: _animation.value,
|
||||
end: index.toDouble(),
|
||||
).animate(CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeInOut,
|
||||
));
|
||||
_controller.forward(from: 0);
|
||||
});
|
||||
|
||||
widget.onTap(index);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
double totalWidth = widget.tabs
|
||||
.fold(0, (sum, tab) => sum + _calculateTabWidth(tab, 0, context));
|
||||
|
||||
totalWidth += (widget.tabs.length) * tabPadding + 26; //who is 26?
|
||||
bool needScroll = widget.forceScroll ||
|
||||
widget.flexes == null ||
|
||||
totalWidth > constraints.maxWidth;
|
||||
return _buildTabsRow(context, needScroll, constraints.maxWidth);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTabsRow(BuildContext context, bool needScroll, maxWidth) {
|
||||
return SizedBox(
|
||||
width: maxWidth,
|
||||
child: needScroll
|
||||
? SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
controller: _horScrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Stack(
|
||||
children: [
|
||||
_buildAnimatedUnderline(false),
|
||||
_buildRow(
|
||||
context,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Stack(
|
||||
children: [
|
||||
_buildAnimatedUnderline(true),
|
||||
_buildRow(context, fixedWidth: true),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRow(BuildContext context, {bool fixedWidth = false}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: widget.tabs
|
||||
.asMap()
|
||||
.map((index, tab) => MapEntry(
|
||||
index,
|
||||
_buildSingleTab(index, tab, context, fixedWidth: fixedWidth),
|
||||
))
|
||||
.values
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSingleTab(int index, String tab, BuildContext context,
|
||||
{required bool fixedWidth}) {
|
||||
double? itemWidth;
|
||||
|
||||
var d = (MediaQuery.of(context).size.width -
|
||||
(tabPadding * widget.tabs.length) -
|
||||
32);
|
||||
|
||||
if (widget.flexes != null) {
|
||||
itemWidth = d *
|
||||
(widget.flexes?[index] ?? 1) /
|
||||
(widget.flexes?.reduce((a, b) => a + b) ?? 1);
|
||||
} else {
|
||||
itemWidth = (d / (widget.tabs.length));
|
||||
}
|
||||
if (keyMaps[index] == null) {
|
||||
keyMaps[index] = GlobalKey();
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
key: keyMaps[index],
|
||||
onTap: () {
|
||||
_setSelectedIndex(index);
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(23),
|
||||
color: Colors.transparent,
|
||||
border: Border.all(
|
||||
color: _selectedIndex != index
|
||||
? AppColors.grayStroke
|
||||
: Colors.transparent,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
margin: EdgeInsets.only(right: tabPadding),
|
||||
height: 46,
|
||||
width: fixedWidth ? itemWidth : null,
|
||||
alignment: Alignment.center,
|
||||
child: AnimatedDefaultTextStyle(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
style: _selectedIndex == index
|
||||
? AppTextStyles.bodySmallReg.copyWith(color: AppColors.grayWhite)
|
||||
: AppTextStyles.bodySmallMed.copyWith(color: AppColors.blackGray),
|
||||
child: Text(tab),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAnimatedUnderline(bool fixedWidth) {
|
||||
double? tabWidth;
|
||||
|
||||
var d = (MediaQuery.of(context).size.width -
|
||||
(tabPadding * widget.tabs.length) -
|
||||
32);
|
||||
|
||||
if (!fixedWidth) {
|
||||
tabWidth = null;
|
||||
} else if (widget.flexes != null) {
|
||||
tabWidth = d *
|
||||
(widget.flexes?[_selectedIndex] ?? 1) /
|
||||
(widget.flexes?.reduce((a, b) => a + b) ?? 1);
|
||||
} else {
|
||||
tabWidth = (d / (widget.tabs.length));
|
||||
}
|
||||
|
||||
var content = AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
width: tabWidth ??
|
||||
_calculateTabWidth(
|
||||
widget.tabs[_selectedIndex], _selectedIndex, context),
|
||||
height: 46,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.bgColorDark,
|
||||
borderRadius: BorderRadius.circular(23),
|
||||
),
|
||||
);
|
||||
|
||||
return fixedWidth && widget.flexes == null
|
||||
? AnimatedBuilder(
|
||||
animation: _animation,
|
||||
builder: (context, child) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||
child: Align(
|
||||
alignment: Alignment(
|
||||
(_animation.value * 2 / (widget.tabs.length - 1)) - 1,
|
||||
1,
|
||||
),
|
||||
child: content,
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: animatedPadding(content);
|
||||
}
|
||||
|
||||
AnimatedPadding animatedPadding(AnimatedContainer content) {
|
||||
return AnimatedPadding(
|
||||
curve: Curves.easeIn,
|
||||
padding: EdgeInsets.only(left: calcTabOffset(_selectedIndex)),
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: content,
|
||||
);
|
||||
}
|
||||
|
||||
double _calculateTabWidth(String tab, int index, BuildContext context) {
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: tab,
|
||||
style: _selectedIndex == index
|
||||
? AppTextStyles.bodySmallReg
|
||||
: AppTextStyles.bodySmallMed,
|
||||
),
|
||||
maxLines: 1,
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout();
|
||||
|
||||
return textPainter.width + 36; // 36?
|
||||
}
|
||||
|
||||
double calcTabOffset(index) {
|
||||
var scrollOffset =
|
||||
_horScrollController.hasClients ? _horScrollController.offset : 0.0;
|
||||
final keyContext = keyMaps[index]?.currentContext;
|
||||
if (keyContext != null) {
|
||||
final box = keyContext.findRenderObject() as RenderBox;
|
||||
return (box.localToGlobal(Offset.zero).dx + scrollOffset)
|
||||
.clamp(0, double.infinity);
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
void _scrollToSelected(int index) {
|
||||
print(index);
|
||||
double offset = 0;
|
||||
double tabWidth = _calculateTabWidth(widget.tabs[index], index, context);
|
||||
double screenWidth = MediaQuery.of(context).size.width;
|
||||
|
||||
offset = calcTabOffset(index);
|
||||
|
||||
double maxScrollExtent = _horScrollController.position.maxScrollExtent;
|
||||
double targetOffset = offset - (screenWidth - tabWidth) / 2;
|
||||
|
||||
if (targetOffset < 0) {
|
||||
targetOffset = 0;
|
||||
} else if (targetOffset > maxScrollExtent) {
|
||||
targetOffset = maxScrollExtent;
|
||||
}
|
||||
|
||||
_horScrollController.animateTo(
|
||||
targetOffset,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
}
|
||||
126
mobile-apps/client-app/lib/core/sevices/app_update_service.dart
Normal file
126
mobile-apps/client-app/lib/core/sevices/app_update_service.dart
Normal file
@@ -0,0 +1,126 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:firebase_remote_config/firebase_remote_config.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
@singleton
|
||||
class AppUpdateService {
|
||||
Future<void> checkForUpdate(BuildContext context) async {
|
||||
final remoteConfig = FirebaseRemoteConfig.instance;
|
||||
|
||||
await remoteConfig.setConfigSettings(RemoteConfigSettings(
|
||||
fetchTimeout: const Duration(seconds: 10),
|
||||
minimumFetchInterval: const Duration(seconds: 1),
|
||||
));
|
||||
|
||||
await remoteConfig.fetchAndActivate();
|
||||
|
||||
final minBuildNumber = remoteConfig.getInt(Platform.isIOS?'min_build_number_client_ios':'min_build_number_client_android');
|
||||
final canSkip = remoteConfig.getBool('can_skip_client');
|
||||
final canIgnore = remoteConfig.getBool('can_ignore_client');
|
||||
final message = remoteConfig.getString('message_client');
|
||||
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
final currentBuildNumber = int.parse(packageInfo.buildNumber);
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final skippedVersion = prefs.getInt('skipped_version') ?? 0;
|
||||
|
||||
if (minBuildNumber > currentBuildNumber &&
|
||||
minBuildNumber > skippedVersion) {
|
||||
await _showUpdateDialog(context, message, canSkip, canIgnore, minBuildNumber);
|
||||
}
|
||||
}
|
||||
|
||||
_showUpdateDialog(
|
||||
BuildContext context, String message, bool canSkip, bool canIgnore, int minBuildNumber) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
barrierDismissible: canIgnore,
|
||||
builder: (BuildContext context) {
|
||||
if (Theme.of(context).platform == TargetPlatform.iOS) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async => canIgnore,
|
||||
child: CupertinoAlertDialog(
|
||||
title: const Text('Update Available'),
|
||||
content: Text(
|
||||
message),
|
||||
actions: <Widget>[
|
||||
if (canSkip)
|
||||
CupertinoDialogAction(
|
||||
child: const Text('Skip this version'),
|
||||
onPressed: () async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt('skipped_version', minBuildNumber);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
CupertinoDialogAction(
|
||||
onPressed:
|
||||
canIgnore ? () => Navigator.of(context).pop() : null,
|
||||
child: const Text('Maybe later'),
|
||||
),
|
||||
CupertinoDialogAction(
|
||||
child: const Text('Update'),
|
||||
onPressed: () async {
|
||||
var url = dotenv.env['IOS_STORE_URL'] ??
|
||||
'';
|
||||
if (await canLaunchUrlString(url)) {
|
||||
await launchUrlString(url);
|
||||
} else {
|
||||
throw 'Could not launch $url';
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return WillPopScope(
|
||||
onWillPop: () async => canIgnore,
|
||||
child: AlertDialog(
|
||||
title: const Text('Update Available'),
|
||||
content: Text(
|
||||
message),
|
||||
actions: <Widget>[
|
||||
if (canSkip)
|
||||
TextButton(
|
||||
child: const Text('Skip this version'),
|
||||
onPressed: () async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt('skipped_version', minBuildNumber);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
onPressed:
|
||||
canIgnore ? () => Navigator.of(context).pop() : null,
|
||||
child: const Text('Maybe later'),
|
||||
),
|
||||
TextButton(
|
||||
child: const Text('Update'),
|
||||
onPressed: () async {
|
||||
var url = dotenv.env['ANDROID_STORE_URL'] ??
|
||||
'';
|
||||
if (await canLaunchUrlString(url)) {
|
||||
await launchUrlString(url);
|
||||
} else {
|
||||
throw 'Could not launch $url';
|
||||
}
|
||||
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:krow/core/application/clients/api/api_client.dart';
|
||||
import 'package:krow/core/application/di/injectable.dart';
|
||||
import 'package:krow/core/sevices/auth_state_service/auth_service_data_provider.dart';
|
||||
|
||||
enum AuthStatus {
|
||||
authenticated,
|
||||
adminValidation,
|
||||
prepareProfile,
|
||||
unauthenticated,
|
||||
error
|
||||
}
|
||||
|
||||
@injectable
|
||||
class AuthService {
|
||||
final AuthServiceDataProvider _dataProvider;
|
||||
|
||||
AuthService(this._dataProvider);
|
||||
|
||||
Future<AuthStatus> getAuthStatus() async {
|
||||
User? user;
|
||||
try {
|
||||
user = FirebaseAuth.instance.currentUser;
|
||||
} catch (e) {
|
||||
return AuthStatus.error;
|
||||
}
|
||||
|
||||
if (user == null) {
|
||||
return AuthStatus.unauthenticated;
|
||||
} else {
|
||||
return AuthStatus.authenticated;
|
||||
}
|
||||
|
||||
//todo get client? check if client is inactive - logout
|
||||
|
||||
// final staffStatus = await getCachedStaffStatus();
|
||||
//
|
||||
// if (staffStatus == StaffStatus.deactivated) {
|
||||
// return AuthStatus.unauthenticated;
|
||||
// } else if (staffStatus == StaffStatus.pending) {
|
||||
// return AuthStatus.adminValidation;
|
||||
// } else if (staffStatus == StaffStatus.registered ||
|
||||
// staffStatus == StaffStatus.declined) {
|
||||
// return AuthStatus.prepareProfile;
|
||||
// } else {
|
||||
return AuthStatus.authenticated;
|
||||
// }
|
||||
}
|
||||
|
||||
void logout() {
|
||||
FirebaseAuth.instance.signOut();
|
||||
getIt<ApiClient>().dropCache();
|
||||
}
|
||||
|
||||
Future<void> deleteAccount() async {
|
||||
await FirebaseAuth.instance.currentUser!.delete();
|
||||
getIt<ApiClient>().dropCache();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:krow/core/application/clients/api/api_client.dart';
|
||||
|
||||
@injectable
|
||||
class AuthServiceDataProvider {
|
||||
final ApiClient _client;
|
||||
|
||||
AuthServiceDataProvider({required ApiClient client}) : _client = client;
|
||||
|
||||
// Future<void> getStaffStatus() async {
|
||||
// final QueryResult result = await _client.query(schema: getStaffStatusQuery);
|
||||
//
|
||||
// if (result.hasException) {
|
||||
// throw Exception(result.exception.toString());
|
||||
// }
|
||||
//
|
||||
// final Map<String, dynamic> data = result.data!['me'];
|
||||
// return Staff.fromJson(data).status!;
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
const String getStaffStatusQuery = '''
|
||||
query GetMe {
|
||||
me {
|
||||
id
|
||||
status
|
||||
}
|
||||
}
|
||||
''';
|
||||
@@ -0,0 +1,120 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:krow/core/application/clients/api/api_exception.dart';
|
||||
import 'package:krow/core/application/di/injectable.dart';
|
||||
import 'package:krow/core/data/models/event/event_model.dart';
|
||||
import 'package:krow/core/entity/event_entity.dart';
|
||||
import 'package:krow/core/entity/position_entity.dart';
|
||||
import 'package:krow/core/entity/shift_entity.dart';
|
||||
import 'package:krow/core/sevices/create_event_service/create_event_service_repository.dart';
|
||||
import 'package:krow/core/sevices/create_event_service/data/models/create_event_input_model.dart';
|
||||
import 'package:krow/core/sevices/create_event_service/data/models/create_event_shift_input_model.dart';
|
||||
import 'package:krow/core/sevices/create_event_service/data/models/create_event_shift_position_input_model.dart';
|
||||
import 'package:krow/features/events/domain/events_repository.dart';
|
||||
|
||||
@lazySingleton
|
||||
class CreateEventService {
|
||||
EventsRepository? _eventsRepository;
|
||||
|
||||
set eventsRepository(EventsRepository eventsRepository) {
|
||||
_eventsRepository = eventsRepository;
|
||||
}
|
||||
|
||||
Future<String> createEventService(EventEntity entity) async {
|
||||
late CreateEventInputModel createEventInputModel;
|
||||
try {
|
||||
createEventInputModel = createInput(entity);
|
||||
} catch (e) {
|
||||
debugPrint('Create event model error: $e');
|
||||
throw DisplayableException('Field must be filled');
|
||||
}
|
||||
|
||||
var id = await getIt<CreateEventServiceRepository>()
|
||||
.createEvent(createEventInputModel);
|
||||
|
||||
_eventsRepository?.refreshEvents(EventStatus.draft);
|
||||
return id;
|
||||
}
|
||||
|
||||
Future<bool> validateEventInfo(EventEntity entity) async {
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<bool> validateShiftInfo(ShiftEntity entity) async {
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<bool> validateShiftPositionInfo(PositionEntity entity) async {
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> deleteDraft(EventEntity entity) async {
|
||||
if (entity.id.isEmpty) {
|
||||
return;
|
||||
}
|
||||
await getIt<CreateEventServiceRepository>().deleteDraft(entity.id);
|
||||
_eventsRepository?.refreshEvents(EventStatus.draft);
|
||||
}
|
||||
|
||||
Future<void> publishEvent(EventEntity entity) async {
|
||||
var id;
|
||||
if (entity.id.isEmpty) {
|
||||
id = await createEventService(entity);
|
||||
} else {
|
||||
await updateEvent(entity);
|
||||
id = entity.id;
|
||||
}
|
||||
|
||||
try {
|
||||
if (entity.status == EventStatus.draft || entity.status == null) {
|
||||
await getIt<CreateEventServiceRepository>().publishEvent(id);
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
|
||||
_eventsRepository?.refreshEvents(EventStatus.draft);
|
||||
_eventsRepository?.refreshEvents(EventStatus.pending);
|
||||
}
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateEvent(EventEntity entity) async {
|
||||
await getIt<CreateEventServiceRepository>()
|
||||
.updateEvent(createInput(entity));
|
||||
_eventsRepository?.refreshEvents(entity.status ?? EventStatus.draft);
|
||||
}
|
||||
|
||||
CreateEventInputModel createInput(EventEntity entity) {
|
||||
return CreateEventInputModel(
|
||||
id: entity.id.isEmpty ? null : entity.id,
|
||||
name: entity.name,
|
||||
date: DateFormat('yyyy-MM-dd').format(entity.startDate ?? DateTime.now()),
|
||||
hubId: entity.hub!.id,
|
||||
// contractId: entity.contractNumber,
|
||||
purchaseOrder: entity.poNumber,
|
||||
contractType: EventContractType.purchaseOrder,
|
||||
additionalInfo: entity.additionalInfo,
|
||||
tags: entity.tags?.map((e) => e.id).toList(),
|
||||
addons: entity.addons?.map((e) => e.id).toList(),
|
||||
shifts: entity.shifts?.indexed.map((e) {
|
||||
var (index, shiftState) = e;
|
||||
return CreateEventShiftInputModel(
|
||||
name: 'Shift #${index + 1}',
|
||||
address: shiftState.fullAddress!,
|
||||
contacts: shiftState.managers.map((e) => e.id).toList(),
|
||||
positions: shiftState.positions.map((roleEntity) {
|
||||
return CreateEventShiftPositionInputModel(
|
||||
businessSkillId: roleEntity.businessSkill!.id!,
|
||||
hubDepartmentId: roleEntity.department!.id,
|
||||
count: roleEntity.count!,
|
||||
startTime: DateFormat('HH:mm').format(roleEntity.startTime!),
|
||||
endTime: DateFormat('HH:mm').format(roleEntity.endTime!),
|
||||
rate: roleEntity.businessSkill!.skill!.price!,
|
||||
breakDuration: roleEntity.breakDuration!,
|
||||
);
|
||||
}).toList());
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import 'package:krow/core/entity/event_entity.dart';
|
||||
import 'package:krow/core/sevices/create_event_service/data/models/create_event_input_model.dart';
|
||||
|
||||
abstract class CreateEventServiceRepository {
|
||||
Future<String> createEvent(CreateEventInputModel input);
|
||||
|
||||
Future<void> deleteDraft(String id);
|
||||
Future<void> publishEvent(String id);
|
||||
|
||||
Future<void> updateEvent(CreateEventInputModel input);
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:krow/core/application/clients/api/api_client.dart';
|
||||
import 'package:krow/core/application/clients/api/api_exception.dart';
|
||||
import 'package:krow/core/sevices/create_event_service/data/create_event_service_gql.dart';
|
||||
import 'package:krow/core/sevices/create_event_service/data/models/create_event_input_model.dart';
|
||||
|
||||
@Injectable()
|
||||
class CreateEventServiceApiProvider {
|
||||
final ApiClient _client;
|
||||
|
||||
CreateEventServiceApiProvider({required ApiClient client}) : _client = client;
|
||||
|
||||
Future<String> createEvent(CreateEventInputModel input) async {
|
||||
var result = await _client.mutate(
|
||||
schema: createEventMutation,
|
||||
body: {
|
||||
'input': input.toJson()..remove('id'),
|
||||
},
|
||||
);
|
||||
|
||||
if (result.hasException) {
|
||||
throw parseBackendError(result.exception);
|
||||
}
|
||||
|
||||
return result.data?['client_create_event']['id'] ?? '';
|
||||
}
|
||||
|
||||
Future<void> deleteDraft(String id) async {
|
||||
var result = await _client.mutate(
|
||||
schema: deleteDraftMutation,
|
||||
body: {
|
||||
'id': id,
|
||||
},
|
||||
);
|
||||
|
||||
if (result.hasException) {
|
||||
print(result.exception);
|
||||
throw parseBackendError(result.exception);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> publishEvent(String id) async {
|
||||
var result = await _client.mutate(
|
||||
schema: publishDraftMutation,
|
||||
body: {
|
||||
'id': id,
|
||||
},
|
||||
);
|
||||
|
||||
if (result.hasException) {
|
||||
throw parseBackendError(result.exception);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateEvent(CreateEventInputModel input) async {
|
||||
var result = await _client.mutate(
|
||||
schema: updateEventMutation,
|
||||
body: {
|
||||
'input': input.toJson(),
|
||||
},
|
||||
);
|
||||
|
||||
if (result.hasException) {
|
||||
throw parseBackendError(result.exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
const String createEventMutation = r'''
|
||||
mutation createEvent ($input: CreateEventInput!) {
|
||||
client_create_event(input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String updateEventMutation = r'''
|
||||
mutation updateEvent ($input: UpdateEventInput!) {
|
||||
client_update_event(input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String deleteDraftMutation = r'''
|
||||
mutation deleteDraft ($id: ID!) {
|
||||
delete_client_event(event_id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String publishDraftMutation = r'''
|
||||
mutation publicsDraft ($id: ID!) {
|
||||
client_publish_event(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
''';
|
||||
@@ -0,0 +1,34 @@
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:krow/core/entity/event_entity.dart';
|
||||
import 'package:krow/core/sevices/create_event_service/create_event_service_repository.dart';
|
||||
import 'package:krow/core/sevices/create_event_service/data/create_event_service_api_provider.dart';
|
||||
import 'package:krow/core/sevices/create_event_service/data/models/create_event_input_model.dart';
|
||||
|
||||
@Singleton(as: CreateEventServiceRepository)
|
||||
class CreateEventServiceRepositoryImpl extends CreateEventServiceRepository {
|
||||
final CreateEventServiceApiProvider _apiProvider;
|
||||
|
||||
CreateEventServiceRepositoryImpl(
|
||||
{required CreateEventServiceApiProvider apiProvider})
|
||||
: _apiProvider = apiProvider;
|
||||
|
||||
@override
|
||||
Future<String> createEvent(CreateEventInputModel input) async {
|
||||
return _apiProvider.createEvent(input);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteDraft(String id) {
|
||||
return _apiProvider.deleteDraft(id);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> publishEvent(String id) {
|
||||
return _apiProvider.publishEvent(id);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateEvent(CreateEventInputModel input) {
|
||||
return _apiProvider.updateEvent(input);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:krow/core/data/models/event/event_model.dart';
|
||||
import 'package:krow/core/sevices/create_event_service/data/models/create_event_shift_input_model.dart';
|
||||
|
||||
part 'create_event_input_model.g.dart';
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||
class CreateEventInputModel {
|
||||
final String? id;
|
||||
final String name;
|
||||
final String date;
|
||||
final String hubId;
|
||||
|
||||
// final String? contractId;
|
||||
final String? purchaseOrder;
|
||||
final EventContractType contractType;
|
||||
final String? additionalInfo;
|
||||
final List<String>? tags;
|
||||
final List<String>? addons;
|
||||
final List<CreateEventShiftInputModel>? shifts;
|
||||
|
||||
CreateEventInputModel(
|
||||
{this.id,
|
||||
required this.name,
|
||||
required this.date,
|
||||
required this.hubId,
|
||||
// required this.contractId,
|
||||
required this.purchaseOrder,
|
||||
required this.contractType,
|
||||
required this.additionalInfo,
|
||||
required this.tags,
|
||||
required this.addons,
|
||||
required this.shifts});
|
||||
|
||||
factory CreateEventInputModel.fromJson(Map<String, dynamic> json) {
|
||||
return _$CreateEventInputModelFromJson(json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$CreateEventInputModelToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:krow/core/data/models/event/full_address_model.dart';
|
||||
import 'package:krow/core/sevices/create_event_service/data/models/create_event_shift_position_input_model.dart';
|
||||
|
||||
part 'create_event_shift_input_model.g.dart';
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||
class CreateEventShiftInputModel {
|
||||
final String name;
|
||||
final FullAddress address;
|
||||
final List<String>? contacts;
|
||||
final List<CreateEventShiftPositionInputModel> positions;
|
||||
|
||||
CreateEventShiftInputModel(
|
||||
{required this.name,
|
||||
required this.address,
|
||||
required this.contacts,
|
||||
required this.positions});
|
||||
|
||||
factory CreateEventShiftInputModel.fromJson(Map<String, dynamic> json) {
|
||||
return _$CreateEventShiftInputModelFromJson(json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$CreateEventShiftInputModelToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'create_event_shift_position_input_model.g.dart';
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||
class CreateEventShiftPositionInputModel {
|
||||
final String businessSkillId;
|
||||
final String hubDepartmentId;
|
||||
final int count;
|
||||
final String startTime;
|
||||
final String endTime;
|
||||
final double rate;
|
||||
@JsonKey(name: 'break')
|
||||
final int breakDuration;
|
||||
|
||||
CreateEventShiftPositionInputModel(
|
||||
{required this.businessSkillId,
|
||||
required this.hubDepartmentId,
|
||||
required this.count,
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
required this.rate,
|
||||
required this.breakDuration});
|
||||
|
||||
factory CreateEventShiftPositionInputModel.fromJson(
|
||||
Map<String, dynamic> json) {
|
||||
return _$CreateEventShiftPositionInputModelFromJson(json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() =>
|
||||
_$CreateEventShiftPositionInputModelToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
|
||||
class GeofencingService {
|
||||
Future<GeolocationStatus> requestGeolocationPermission() async {
|
||||
LocationPermission permission;
|
||||
|
||||
permission = await Geolocator.checkPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
permission = await Geolocator.requestPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
return GeolocationStatus.denied;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (permission == LocationPermission.deniedForever){
|
||||
return GeolocationStatus.prohibited;
|
||||
}
|
||||
if (!(await Geolocator.isLocationServiceEnabled())) {
|
||||
// Location services are not enabled
|
||||
return GeolocationStatus.disabled;
|
||||
}
|
||||
return GeolocationStatus.enabled;
|
||||
}
|
||||
|
||||
bool _isInRange(
|
||||
Position currentPosition,
|
||||
double pointLat,
|
||||
double pointLon,
|
||||
int range,
|
||||
) {
|
||||
double distance = Geolocator.distanceBetween(
|
||||
currentPosition.latitude,
|
||||
currentPosition.longitude,
|
||||
pointLat,
|
||||
pointLon,
|
||||
);
|
||||
|
||||
return distance <= range;
|
||||
}
|
||||
|
||||
/// Checks if the user's current location is within [range] meters
|
||||
/// of the given [pointLatitude] and [pointLongitude].
|
||||
Future<bool> isInRangeCheck({
|
||||
required double pointLatitude,
|
||||
required double pointLongitude,
|
||||
int range = 500,
|
||||
}) async {
|
||||
try {
|
||||
Position position = await Geolocator.getCurrentPosition();
|
||||
|
||||
return _isInRange(position, pointLatitude, pointLongitude, range);
|
||||
} catch (except) {
|
||||
log('Error getting location: $except');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Constantly checks if the user's current location is within [range] meters
|
||||
/// of the given [pointLatitude] and [pointLongitude].
|
||||
Stream<bool> isInRangeStream({
|
||||
required double pointLatitude,
|
||||
required double pointLongitude,
|
||||
int range = 500,
|
||||
}) async* {
|
||||
await for (final position in Geolocator.getPositionStream()) {
|
||||
try {
|
||||
yield _isInRange(position, pointLatitude, pointLongitude, range);
|
||||
} catch (except) {
|
||||
log('Error getting location: $except');
|
||||
yield false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// [disabled] indicates that the user should enable geolocation on the device.
|
||||
/// [denied] indicates that permission should be requested or re-requested.
|
||||
/// [prohibited] indicates that permission is denied and can only be changed
|
||||
/// via the Settings.
|
||||
/// [enabled] - geolocation service is allowed and available for usage.
|
||||
enum GeolocationStatus {
|
||||
disabled,
|
||||
denied,
|
||||
prohibited,
|
||||
enabled,
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
class TimeSlotService {
|
||||
static DateTime calcTime({
|
||||
required DateTime? currentStartTime,
|
||||
required DateTime? currentEndTime,
|
||||
DateTime? startTime,
|
||||
DateTime? endTime,
|
||||
}) {
|
||||
|
||||
try {
|
||||
if (endTime != null) {
|
||||
return endTime.difference(currentStartTime!).inHours < 5 ||
|
||||
endTime.isBefore(currentStartTime!)
|
||||
? endTime.subtract(const Duration(hours: 5))
|
||||
: currentStartTime;
|
||||
} else if (startTime != null) {
|
||||
return startTime.difference(currentEndTime!).inHours < 5 ||
|
||||
startTime.isAfter(currentEndTime)
|
||||
? startTime.add(const Duration(hours: 5))
|
||||
: currentEndTime;
|
||||
}
|
||||
} catch (e) {
|
||||
return DateTime.now().subtract(Duration(minutes: DateTime.now().minute % 10));
|
||||
}
|
||||
return DateTime.now().subtract(Duration(minutes: DateTime.now().minute % 10));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:krow/core/entity/staff_contact_entity.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
part 'assigned_staff_event.dart';
|
||||
part 'assigned_staff_state.dart';
|
||||
|
||||
class AssignedStaffBloc extends Bloc<AssignedStaffEvent, AssignedStaffState> {
|
||||
AssignedStaffBloc(
|
||||
{required List<StaffContact> staffContacts, required String department})
|
||||
: super(AssignedStaffState(
|
||||
staffContacts: staffContacts,
|
||||
department: department,
|
||||
)) {}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
part of 'assigned_staff_bloc.dart';
|
||||
|
||||
@immutable
|
||||
sealed class AssignedStaffEvent {}
|
||||
@@ -0,0 +1,23 @@
|
||||
part of 'assigned_staff_bloc.dart';
|
||||
|
||||
@immutable
|
||||
class AssignedStaffState {
|
||||
final List<StaffContact> staffContacts;
|
||||
final String department;
|
||||
final bool inLoading;
|
||||
|
||||
const AssignedStaffState({required this.staffContacts, required this.department, this.inLoading = false});
|
||||
|
||||
copyWith({
|
||||
List<StaffContact>? staffContacts,
|
||||
String? department,
|
||||
bool? canManualAssign,
|
||||
bool? inLoading,
|
||||
}) {
|
||||
return AssignedStaffState(
|
||||
staffContacts: staffContacts ?? this.staffContacts,
|
||||
department: department ?? this.department,
|
||||
inLoading: inLoading ?? this.inLoading,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow/core/entity/staff_contact_entity.dart';
|
||||
import 'package:krow/core/presentation/widgets/assigned_staff_item_widget.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_app_bar.dart';
|
||||
import 'package:krow/features/assigned_staff_screen/domain/assigned_staff_bloc.dart';
|
||||
|
||||
@RoutePage()
|
||||
class AssignedStaffScreen extends StatelessWidget implements AutoRouteWrapper {
|
||||
final List<StaffContact> staffContacts;
|
||||
|
||||
final String department;
|
||||
|
||||
const AssignedStaffScreen(
|
||||
{super.key, required this.staffContacts, required this.department});
|
||||
|
||||
@override
|
||||
Widget wrappedRoute(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => AssignedStaffBloc(
|
||||
staffContacts: staffContacts, department: department),
|
||||
child: this);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<AssignedStaffBloc, AssignedStaffState>(
|
||||
builder: (context, state) {
|
||||
return Scaffold(
|
||||
appBar: KwAppBar(
|
||||
titleText: 'Assigned Staff',
|
||||
),
|
||||
body: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: staffContacts.length,
|
||||
itemBuilder: (context, index) {
|
||||
return AssignedStaffItemWidget(
|
||||
staffContact: staffContacts[index], department: department);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'package:graphql_flutter/graphql_flutter.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:krow/core/application/clients/api/api_client.dart';
|
||||
import 'package:krow/core/application/clients/api/api_exception.dart';
|
||||
import 'package:krow/features/clock_manual/data/clock_manual_gql.dart';
|
||||
|
||||
@singleton
|
||||
class ClockManualApiProvider {
|
||||
final ApiClient _client;
|
||||
|
||||
ClockManualApiProvider({required ApiClient client}) : _client = client;
|
||||
|
||||
Future<void> trackClientClockin(String positionStaffId) async {
|
||||
final QueryResult result = await _client.mutate(
|
||||
schema: trackClientClockinMutation,
|
||||
body: {'position_staff_id': positionStaffId},
|
||||
);
|
||||
if (result.hasException) {
|
||||
throw parseBackendError(result.exception!);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> trackClientClockout(String positionStaffId) async {
|
||||
final QueryResult result = await _client.mutate(
|
||||
schema: trackClientClockoutMutation,
|
||||
body: {'position_staff_id': positionStaffId},
|
||||
);
|
||||
if (result.hasException) {
|
||||
throw parseBackendError(result.exception!);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> cancelClockin(String positionStaffId) async {
|
||||
final QueryResult result = await _client.mutate(
|
||||
schema: cancelClockinMutation,
|
||||
body: {'position_staff_id': positionStaffId},
|
||||
);
|
||||
if (result.hasException) {
|
||||
throw parseBackendError(result.exception!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
const String trackClientClockinMutation = r'''
|
||||
mutation track_client_clockin($position_staff_id: ID!) {
|
||||
track_client_clockin(position_staff_id: $position_staff_id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String trackClientClockoutMutation = r'''
|
||||
mutation track_client_clockout($position_staff_id: ID!) {
|
||||
track_client_clockout(position_staff_id: $position_staff_id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String cancelClockinMutation = r'''
|
||||
mutation cancel_client_clockin($position_staff_id: ID!) {
|
||||
cancel_client_clockin(position_staff_id: $position_staff_id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
''';
|
||||
@@ -0,0 +1,43 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:krow/features/clock_manual/data/clock_manual_api_provider.dart';
|
||||
import 'package:krow/features/clock_manual/domain/clock_manual_repository.dart';
|
||||
|
||||
@Singleton(as: ClockManualRepository)
|
||||
class ClockManualRepositoryImpl implements ClockManualRepository {
|
||||
final ClockManualApiProvider apiProvider;
|
||||
|
||||
ClockManualRepositoryImpl({required this.apiProvider});
|
||||
|
||||
@override
|
||||
Future<void> trackClientClockin(String positionStaffId) async {
|
||||
try {
|
||||
await apiProvider.trackClientClockin(positionStaffId);
|
||||
} catch (exception) {
|
||||
debugPrint(exception.toString());
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> trackClientClockout(String positionStaffId) async {
|
||||
try {
|
||||
await apiProvider.trackClientClockout(positionStaffId);
|
||||
} catch (exception) {
|
||||
debugPrint(exception.toString());
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> cancelClockin(String positionStaffId) async{
|
||||
try {
|
||||
await apiProvider.cancelClockin(positionStaffId);
|
||||
} catch (exception) {
|
||||
debugPrint(exception.toString());
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:krow/core/application/clients/api/api_exception.dart';
|
||||
import 'package:krow/core/application/di/injectable.dart';
|
||||
import 'package:krow/core/data/models/staff/pivot.dart';
|
||||
import 'package:krow/core/entity/staff_contact_entity.dart';
|
||||
import 'package:krow/features/clock_manual/domain/clock_manual_repository.dart';
|
||||
|
||||
part 'clock_manual_event.dart';
|
||||
part 'clock_manual_state.dart';
|
||||
|
||||
class ClockManualBloc extends Bloc<ClockManualEvent, ClockManualState> {
|
||||
var staffContacts = <StaffContact>[];
|
||||
|
||||
ClockManualBloc({required this.staffContacts})
|
||||
: super(ClockManualState(
|
||||
timers: Map.fromEntries(
|
||||
staffContacts.map((e) => MapEntry(e.id, ValueNotifier<int>(0)))),
|
||||
ongoingStaffContacts: staffContacts
|
||||
.where((element) => element.status == PivotStatus.ongoing)
|
||||
.toList()
|
||||
..sort((a, b) => a.startAt.compareTo(b.startAt)),
|
||||
confirmedStaffContacts: staffContacts
|
||||
.where((element) => element.status == PivotStatus.confirmed)
|
||||
.toList()
|
||||
..sort((a, b) => a.startAt.compareTo(b.startAt)),
|
||||
)) {
|
||||
on<ChangeSelectedTabIndexClockManual>(_onChangeSelectedTabIndexStaffManual);
|
||||
on<ClockInManual>(_onClockInStaffManual);
|
||||
on<SortStaffContactsClockManual>(_sortStaffContactsClockManual);
|
||||
on<ClockOutManual>(_onClockOutStaff);
|
||||
on<CancelClockInManual>(_onCancelClockInStaffManual);
|
||||
}
|
||||
|
||||
void _onChangeSelectedTabIndexStaffManual(
|
||||
ChangeSelectedTabIndexClockManual event, Emitter<ClockManualState> emit) {
|
||||
emit(state.copyWith(selectedTabIndex: event.index));
|
||||
}
|
||||
|
||||
void _onClockInStaffManual(ClockInManual event, emit) async {
|
||||
emit(state.copyWith(inLoading: true));
|
||||
try {
|
||||
await getIt<ClockManualRepository>()
|
||||
.trackClientClockin(event.staffContact.id);
|
||||
|
||||
_startCountdownTimer(
|
||||
state.timers[event.staffContact.id]!, event.staffContact);
|
||||
} catch (e) {
|
||||
if (e is DisplayableException) {
|
||||
emit(state.copyWith(
|
||||
errorMessage: e.message,
|
||||
));
|
||||
}
|
||||
}
|
||||
emit(state.copyWith(inLoading: false));
|
||||
}
|
||||
|
||||
void _onClockOutStaff(ClockOutManual event, emit) async {
|
||||
emit(state.copyWith(inLoading: false));
|
||||
try {
|
||||
await getIt<ClockManualRepository>()
|
||||
.trackClientClockout(event.staffContact.id);
|
||||
event.staffContact.status = PivotStatus.completed;
|
||||
add(SortStaffContactsClockManual());
|
||||
} catch (e) {
|
||||
if (e is DisplayableException) {
|
||||
emit(state.copyWith(
|
||||
errorMessage: e.message,
|
||||
));
|
||||
}
|
||||
}
|
||||
emit(state.copyWith(inLoading: false));
|
||||
}
|
||||
|
||||
void _onCancelClockInStaffManual(CancelClockInManual event, emit) async {
|
||||
emit(state.copyWith(inLoading: true));
|
||||
try {
|
||||
await getIt<ClockManualRepository>().cancelClockin(event.staffContact.id);
|
||||
state.timers[event.staffContact.id]!.value = -1;
|
||||
event.staffContact.status = PivotStatus.confirmed;
|
||||
add(SortStaffContactsClockManual());
|
||||
} catch (e) {
|
||||
if (e is DisplayableException) {
|
||||
emit(state.copyWith(
|
||||
errorMessage: e.message,
|
||||
));
|
||||
}
|
||||
}
|
||||
emit(state.copyWith(inLoading: false));
|
||||
}
|
||||
|
||||
void _sortStaffContactsClockManual(SortStaffContactsClockManual event, emit) {
|
||||
emit(state.copyWith(
|
||||
ongoingStaffContacts: staffContacts
|
||||
.where((element) => element.status == PivotStatus.ongoing)
|
||||
.toList()
|
||||
..sort((a, b) => a.startAt.compareTo(b.startAt)),
|
||||
confirmedStaffContacts: staffContacts
|
||||
.where((element) => element.status == PivotStatus.confirmed)
|
||||
.toList()
|
||||
..sort((a, b) => a.startAt.compareTo(b.startAt)),
|
||||
));
|
||||
}
|
||||
|
||||
void _startCountdownTimer(ValueNotifier<int> cancelTimer, staffContact) {
|
||||
cancelTimer.value = 5;
|
||||
Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (cancelTimer.value > 0) {
|
||||
cancelTimer.value -= 1;
|
||||
} else if (cancelTimer.value == 0) {
|
||||
staffContact.status = PivotStatus.ongoing;
|
||||
add(SortStaffContactsClockManual());
|
||||
timer.cancel();
|
||||
} else {
|
||||
timer.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
part of 'clock_manual_bloc.dart';
|
||||
|
||||
@immutable
|
||||
sealed class ClockManualEvent {}
|
||||
|
||||
class ChangeSelectedTabIndexClockManual extends ClockManualEvent {
|
||||
final int index;
|
||||
|
||||
ChangeSelectedTabIndexClockManual(this.index);
|
||||
}
|
||||
|
||||
class ClockInManual extends ClockManualEvent {
|
||||
final StaffContact staffContact;
|
||||
|
||||
ClockInManual(this.staffContact);
|
||||
}
|
||||
|
||||
class SortStaffContactsClockManual extends ClockManualEvent {}
|
||||
|
||||
class ClockOutManual extends ClockManualEvent {
|
||||
final StaffContact staffContact;
|
||||
|
||||
ClockOutManual(this.staffContact);
|
||||
}
|
||||
|
||||
class CancelClockInManual extends ClockManualEvent {
|
||||
final StaffContact staffContact;
|
||||
|
||||
CancelClockInManual(this.staffContact);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
part of 'clock_manual_bloc.dart';
|
||||
|
||||
@immutable
|
||||
class ClockManualState {
|
||||
final List<StaffContact> confirmedStaffContacts;
|
||||
final List<StaffContact> ongoingStaffContacts;
|
||||
final int selectedTabIndex;
|
||||
final bool inLoading;
|
||||
final String? errorMessage;
|
||||
|
||||
final Map<String, ValueNotifier<int>> timers ;
|
||||
|
||||
const ClockManualState(
|
||||
{required this.confirmedStaffContacts,
|
||||
required this.ongoingStaffContacts,
|
||||
this.inLoading = false,
|
||||
this.errorMessage,
|
||||
this.timers = const {},
|
||||
this.selectedTabIndex = 0});
|
||||
|
||||
copyWith({
|
||||
List<StaffContact>? confirmedStaffContacts,
|
||||
List<StaffContact>? ongoingStaffContacts,
|
||||
bool? inLoading,
|
||||
String? errorMessage,
|
||||
int? selectedTabIndex,
|
||||
Map<String, ValueNotifier<int>>? timers,
|
||||
}) {
|
||||
return ClockManualState(
|
||||
timers: timers ?? this.timers,
|
||||
confirmedStaffContacts:
|
||||
confirmedStaffContacts ?? this.confirmedStaffContacts,
|
||||
ongoingStaffContacts: ongoingStaffContacts ?? this.ongoingStaffContacts,
|
||||
inLoading: inLoading ?? this.inLoading,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
selectedTabIndex: selectedTabIndex ?? this.selectedTabIndex,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
abstract class ClockManualRepository {
|
||||
Future<void> trackClientClockin(String positionStaffId);
|
||||
|
||||
Future<void> trackClientClockout(String positionStaffId);
|
||||
|
||||
Future<void> cancelClockin(String positionStaffId);
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:krow/core/application/common/str_extensions.dart';
|
||||
import 'package:krow/core/data/models/staff/pivot.dart';
|
||||
import 'package:krow/core/entity/staff_contact_entity.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
|
||||
import 'package:krow/features/clock_manual/domain/bloc/clock_manual_bloc.dart';
|
||||
|
||||
class AssignedStaffManualItemWidget extends StatelessWidget {
|
||||
final StaffContact staffContact;
|
||||
|
||||
final ValueNotifier<int> timer;
|
||||
|
||||
const AssignedStaffManualItemWidget(
|
||||
{super.key, required this.staffContact, required this.timer});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: timer,
|
||||
builder: (BuildContext context, int value, Widget? child) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
decoration: KwBoxDecorations.white8,
|
||||
child: Column(
|
||||
children: [
|
||||
_staffInfo(),
|
||||
const Gap(12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(staffContact.status == PivotStatus.ongoing?'End Time': 'Start Time',
|
||||
style: AppTextStyles.bodySmallReg
|
||||
.copyWith(color: AppColors.blackGray)),
|
||||
Text(
|
||||
DateFormat('hh:mm a').format(
|
||||
(staffContact.status == PivotStatus.ongoing
|
||||
? staffContact.parentPosition?.endTime
|
||||
: staffContact.parentPosition?.startTime) ??
|
||||
DateTime.now()),
|
||||
style: AppTextStyles.bodySmallMed,
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(12),
|
||||
if (staffContact.status == PivotStatus.confirmed && value <= 0)
|
||||
_buildClockIn(context),
|
||||
if (staffContact.status == PivotStatus.ongoing)
|
||||
_buildClockOut(context),
|
||||
if (value > 0) _buildCancel(context),
|
||||
if (kDebugMode) _buildCancel(context),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
KwButton _buildCancel(BuildContext context) {
|
||||
return KwButton.outlinedPrimary(
|
||||
onPressed: () {
|
||||
BlocProvider.of<ClockManualBloc>(context)
|
||||
.add(CancelClockInManual(staffContact));
|
||||
},
|
||||
label: 'Click again to cancel (${timer.value}) ',
|
||||
height: 36)
|
||||
.copyWith(
|
||||
textColors: AppColors.blackGray,
|
||||
color: AppColors.grayWhite,
|
||||
isFilledOutlined: true,
|
||||
borderColor: AppColors.grayTintStroke,
|
||||
pressedColor: AppColors.grayWhite);
|
||||
}
|
||||
|
||||
KwButton _buildClockIn(BuildContext context) {
|
||||
return KwButton.outlinedPrimary(
|
||||
onPressed: () {
|
||||
BlocProvider.of<ClockManualBloc>(context)
|
||||
.add(ClockInManual(staffContact));
|
||||
},
|
||||
label: 'Clock In',
|
||||
height: 36)
|
||||
.copyWith(
|
||||
textColors: AppColors.statusSuccess,
|
||||
color: AppColors.tintGreen,
|
||||
isFilledOutlined: true,
|
||||
borderColor: AppColors.tintDarkGreen,
|
||||
pressedColor: AppColors.tintDarkGreen);
|
||||
}
|
||||
|
||||
KwButton _buildClockOut(BuildContext context) {
|
||||
return KwButton.outlinedPrimary(
|
||||
onPressed: () {
|
||||
BlocProvider.of<ClockManualBloc>(context)
|
||||
.add(ClockOutManual(staffContact));
|
||||
},
|
||||
label: 'Clock Out',
|
||||
height: 36)
|
||||
.copyWith(
|
||||
textColors: AppColors.statusError,
|
||||
color: AppColors.tintRed,
|
||||
isFilledOutlined: true,
|
||||
borderColor: AppColors.tintDarkRed,
|
||||
pressedColor: AppColors.tintDarkRed);
|
||||
}
|
||||
|
||||
Row _staffInfo() {
|
||||
return Row(
|
||||
children: [
|
||||
if ((staffContact.photoUrl ?? '').isNotEmpty)
|
||||
CircleAvatar(
|
||||
radius: 18,
|
||||
backgroundImage: NetworkImage(staffContact.photoUrl ?? ''),
|
||||
),
|
||||
const Gap(12),
|
||||
Flexible(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${staffContact.firstName} ${staffContact.lastName}',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
),
|
||||
const Gap(4),
|
||||
Text(
|
||||
staffContact.phoneNumber ?? '',
|
||||
style: AppTextStyles.bodySmallReg
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
SizedBox(
|
||||
height: 40,
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Container(
|
||||
height: 20,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: staffContact.status.getStatusBorderColor(),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
padding: const EdgeInsets.all(0),
|
||||
decoration: BoxDecoration(
|
||||
color: staffContact.status.getStatusTextColor(),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
),
|
||||
const Gap(2),
|
||||
Text(
|
||||
staffContact.status.formattedName.capitalize(),
|
||||
style: AppTextStyles.bodyTinyMed.copyWith(
|
||||
color: staffContact.status.getStatusTextColor()),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow/core/entity/staff_contact_entity.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_app_bar.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_option_selector.dart';
|
||||
import 'package:krow/features/clock_manual/domain/bloc/clock_manual_bloc.dart';
|
||||
import 'package:krow/features/clock_manual/presentation/clock_manual_list_item.dart';
|
||||
|
||||
@RoutePage()
|
||||
class ClockManualScreen extends StatelessWidget implements AutoRouteWrapper {
|
||||
final List<StaffContact> staffContacts;
|
||||
|
||||
const ClockManualScreen({
|
||||
super.key,
|
||||
required this.staffContacts,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget wrappedRoute(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => ClockManualBloc(staffContacts: staffContacts),
|
||||
child: this);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<ClockManualBloc, ClockManualState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.errorMessage != current.errorMessage,
|
||||
listener: (context, state) {
|
||||
if (state.errorMessage != null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(state.errorMessage ?? ''),
|
||||
));
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
var items = state.selectedTabIndex == 0
|
||||
? state.confirmedStaffContacts
|
||||
: state.ongoingStaffContacts;
|
||||
return Scaffold(
|
||||
appBar: KwAppBar(
|
||||
titleText: 'Assigned Staff',
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, top: 16),
|
||||
child: KwOptionSelector(
|
||||
selectedIndex: state.selectedTabIndex,
|
||||
onChanged: (index) {
|
||||
BlocProvider.of<ClockManualBloc>(context)
|
||||
.add(ChangeSelectedTabIndexClockManual(index));
|
||||
},
|
||||
height: 26,
|
||||
selectorHeight: 4,
|
||||
textStyle: AppTextStyles.bodyMediumReg
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
selectedTextStyle: AppTextStyles.bodyMediumMed,
|
||||
itemAlign: Alignment.topCenter,
|
||||
items: const [
|
||||
'Clock In',
|
||||
'Clock Out',
|
||||
]),
|
||||
),
|
||||
ListView.builder(
|
||||
primary: false,
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
return AssignedStaffManualItemWidget(
|
||||
staffContact: items[index],
|
||||
timer: state.timers[items[index].id]!,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@RoutePage()
|
||||
class CreateEventFlowScreen extends StatelessWidget {
|
||||
const CreateEventFlowScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const AutoRouter();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import 'package:graphql/client.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:krow/core/application/clients/api/api_client.dart';
|
||||
import 'package:krow/core/data/models/event/addon_model.dart';
|
||||
import 'package:krow/core/data/models/event/business_member_model.dart';
|
||||
import 'package:krow/core/data/models/event/hub_model.dart';
|
||||
import 'package:krow/core/data/models/event/tag_model.dart';
|
||||
import 'package:krow/core/data/models/shift/business_skill_model.dart';
|
||||
import 'package:krow/core/data/models/shift/department_model.dart';
|
||||
import 'package:krow/features/create_event/data/create_event_gql.dart';
|
||||
|
||||
@Injectable()
|
||||
class CreateEventApiProvider {
|
||||
final ApiClient _client;
|
||||
|
||||
CreateEventApiProvider({required ApiClient client}) : _client = client;
|
||||
|
||||
Future<List<HubModel>> getHubs() async {
|
||||
QueryResult result = await _client.query(
|
||||
schema: getClientHubsQuery,
|
||||
);
|
||||
if (result.hasException) {
|
||||
throw Exception(result.exception.toString());
|
||||
}
|
||||
|
||||
if (result.data == null || result.data!['client_hubs'] == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (result.data!['client_hubs'] as List)
|
||||
.map((e) => HubModel.fromJson(e))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<List<AddonModel>> getAddons() async {
|
||||
QueryResult result = await _client.query(
|
||||
schema: getClientAddonsQuery,
|
||||
);
|
||||
if (result.hasException) {
|
||||
throw Exception(result.exception.toString());
|
||||
}
|
||||
|
||||
if (result.data == null || result.data!['client_business_addons'] == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (result.data!['client_business_addons'] as List)
|
||||
.map((e) => AddonModel.fromJson(e))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<List<TagModel>> getTags() async {
|
||||
QueryResult result = await _client.query(
|
||||
schema: getClientTagsQuery,
|
||||
);
|
||||
if (result.hasException) {
|
||||
throw Exception(result.exception.toString());
|
||||
}
|
||||
|
||||
if (result.data == null || result.data!['client_tags'] == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (result.data!['client_tags'] as List)
|
||||
.map((e) => TagModel.fromJson(e))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<List<BusinessMemberModel>> getContacts() async {
|
||||
QueryResult result = await _client.query(
|
||||
schema: getClientMembersQuery,
|
||||
);
|
||||
if (result.hasException) {
|
||||
throw Exception(result.exception.toString());
|
||||
}
|
||||
|
||||
if (result.data == null || result.data!['client_shift_contacts'] == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (result.data!['client_shift_contacts'] as List)
|
||||
.map((e) => BusinessMemberModel.fromJson(e))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<List<BusinessSkillModel>> getSkill() async {
|
||||
QueryResult result = await _client.query(
|
||||
schema: getClientBusinessSkillQuery,
|
||||
);
|
||||
if (result.hasException) {
|
||||
throw Exception(result.exception.toString());
|
||||
}
|
||||
|
||||
if (result.data == null || result.data!['client_business_skills'] == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (result.data!['client_business_skills'] as List)
|
||||
.map((e) => BusinessSkillModel.fromJson(e))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<List<DepartmentModel>> getDepartments(String hubId) async {
|
||||
QueryResult result = await _client.query(
|
||||
schema: getClientDepartmentsQuery,
|
||||
body: {'hub_id': hubId},
|
||||
);
|
||||
if (result.hasException) {
|
||||
throw Exception(result.exception.toString());
|
||||
}
|
||||
if (result.data == null || result.data!['client_hub_departments'] == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (result.data!['client_hub_departments'] as List)
|
||||
.map((e) => DepartmentModel.fromJson(e))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
const String getClientHubsQuery = r'''
|
||||
query getClientHubs() {
|
||||
client_hubs {
|
||||
id
|
||||
name
|
||||
address
|
||||
full_address {
|
||||
street_number
|
||||
zip_code
|
||||
latitude
|
||||
longitude
|
||||
formatted_address
|
||||
street
|
||||
region
|
||||
city
|
||||
country
|
||||
}
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String getClientAddonsQuery = r'''
|
||||
query getClientAddons() {
|
||||
client_business_addons {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String getClientTagsQuery = r'''
|
||||
query getClientTags() {
|
||||
client_tags {
|
||||
id
|
||||
name
|
||||
slug
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String getClientBusinessSkillQuery = r'''
|
||||
query getClientSkills() {
|
||||
client_business_skills {
|
||||
id
|
||||
skill {
|
||||
id
|
||||
name
|
||||
slug
|
||||
price
|
||||
}
|
||||
price
|
||||
is_active
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String getClientMembersQuery = r'''
|
||||
query getClientMembers() {
|
||||
client_shift_contacts() {
|
||||
id
|
||||
first_name
|
||||
last_name
|
||||
title
|
||||
avatar
|
||||
auth_info {
|
||||
email
|
||||
phone
|
||||
}
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String getClientDepartmentsQuery = r'''
|
||||
query getClientSkills($hub_id: ID!) {
|
||||
client_hub_departments(hub_id: $hub_id) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:krow/core/data/models/event/addon_model.dart';
|
||||
import 'package:krow/core/data/models/event/business_member_model.dart';
|
||||
import 'package:krow/core/data/models/event/hub_model.dart';
|
||||
import 'package:krow/core/data/models/event/tag_model.dart';
|
||||
import 'package:krow/core/data/models/shift/business_skill_model.dart';
|
||||
import 'package:krow/core/data/models/shift/department_model.dart';
|
||||
import 'package:krow/features/create_event/data/create_event_api_provider.dart';
|
||||
import 'package:krow/features/create_event/domain/create_event_repository.dart';
|
||||
|
||||
@Singleton(as: CreateEventRepository)
|
||||
class CreateEventRepositoryImpl extends CreateEventRepository {
|
||||
final CreateEventApiProvider _apiProvider;
|
||||
|
||||
CreateEventRepositoryImpl({required CreateEventApiProvider apiProvider})
|
||||
: _apiProvider = apiProvider;
|
||||
|
||||
@override
|
||||
Future<List<HubModel>> getHubs() async {
|
||||
return _apiProvider.getHubs();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<AddonModel>> getAddons() async {
|
||||
return _apiProvider.getAddons();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<TagModel>> getTags() async {
|
||||
return _apiProvider.getTags();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<BusinessMemberModel>> getContacts() async {
|
||||
return _apiProvider.getContacts();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<BusinessSkillModel>> getSkills() {
|
||||
return _apiProvider.getSkill();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<DepartmentModel>> getDepartments(String hubId) {
|
||||
return _apiProvider.getDepartments(hubId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow/core/application/di/injectable.dart';
|
||||
import 'package:krow/core/data/models/event/addon_model.dart';
|
||||
import 'package:krow/core/data/models/event/business_member_model.dart';
|
||||
import 'package:krow/core/data/models/event/event_model.dart';
|
||||
import 'package:krow/core/data/models/event/hub_model.dart';
|
||||
import 'package:krow/core/data/models/event/tag_model.dart';
|
||||
import 'package:krow/core/data/models/shift/business_skill_model.dart';
|
||||
import 'package:krow/core/data/models/shift/department_model.dart';
|
||||
import 'package:krow/core/entity/event_entity.dart';
|
||||
import 'package:krow/core/entity/shift_entity.dart';
|
||||
import 'package:krow/core/sevices/create_event_service/create_event_service.dart';
|
||||
import 'package:krow/features/create_event/domain/create_event_repository.dart';
|
||||
import 'package:krow/features/create_event/domain/input_validator.dart';
|
||||
import 'package:krow/features/create_event/presentation/create_shift_details_section/bloc/create_shift_details_bloc.dart';
|
||||
|
||||
part 'create_event_event.dart';
|
||||
part 'create_event_state.dart';
|
||||
|
||||
class CreateEventBloc extends Bloc<CreateEventEvent, CreateEventState> {
|
||||
CreateEventBloc() : super(CreateEventState(entity: EventEntity.empty())) {
|
||||
on<CreateEventInit>(_onInit);
|
||||
// on<CreateEventChangeContractType>(_onChangeContractType);
|
||||
on<CreateEventChangeHub>(_onChangeHub);
|
||||
// on<CreateEventChangeContractNumber>(_onChangeContractNumber);
|
||||
on<CreateEventChangePoNumber>(_onChangePoNumber);
|
||||
on<CreateEventTagSelected>(_onTagSelected);
|
||||
on<CreateEventAddShift>(_onAddShift);
|
||||
on<CreateEventRemoveShift>(_onRemoveShift);
|
||||
on<CreateEventAddInfoChange>(_onAddInfoChange);
|
||||
on<CreateEventNameChange>(_onNameChange);
|
||||
on<CreateEventToggleAddon>(_onToggleAddon);
|
||||
on<CreateEventEntityUpdatedEvent>(_onEntityUpdated);
|
||||
on<CreateEventValidateAndPreview>(_onValidateAndPreview);
|
||||
on<DeleteDraftEvent>(_onDeleteDraft);
|
||||
}
|
||||
|
||||
Future<void> _onInit(
|
||||
CreateEventInit event, Emitter<CreateEventState> emit) async {
|
||||
emit(state.copyWith(inLoading: true));
|
||||
late EventEntity entity;
|
||||
bool? isEdit = event.eventModel != null;
|
||||
if (isEdit) {
|
||||
entity = EventEntity.fromEventDto(event.eventModel!);
|
||||
} else {
|
||||
entity = EventEntity.empty();
|
||||
}
|
||||
List<ShiftViewModel> shiftViewModels = [
|
||||
...entity.shifts?.map((shiftEntity) {
|
||||
return ShiftViewModel(
|
||||
id: shiftEntity.id,
|
||||
bloc: CreateShiftDetailsBloc(expanded: !isEdit)
|
||||
..add(CreateShiftInitializeEvent(shiftEntity)),
|
||||
);
|
||||
}).toList() ??
|
||||
[],
|
||||
];
|
||||
|
||||
emit(state.copyWith(
|
||||
recurringType: event.recurringType,
|
||||
entity: entity,
|
||||
tags: [
|
||||
//placeholder for tags - prevent UI jumping
|
||||
TagModel(id: '1', name: ' '),
|
||||
TagModel(id: '2', name: ' '),
|
||||
TagModel(id: '3', name: ' ')
|
||||
],
|
||||
shifts: shiftViewModels));
|
||||
List<Object> results;
|
||||
try {
|
||||
results = await Future.wait([
|
||||
getIt<CreateEventRepository>().getHubs().onError((e, s) {
|
||||
return [];
|
||||
}),
|
||||
getIt<CreateEventRepository>().getAddons().onError((e, s) => []),
|
||||
getIt<CreateEventRepository>().getTags().onError((e, s) => []),
|
||||
getIt<CreateEventRepository>().getContacts().onError((e, s) => []),
|
||||
getIt<CreateEventRepository>().getSkills().onError((e, s) => []),
|
||||
]);
|
||||
} catch (e) {
|
||||
emit(state.copyWith(inLoading: false));
|
||||
return;
|
||||
}
|
||||
var hubs = results[0] as List<HubModel>;
|
||||
var addons = results[1] as List<AddonModel>;
|
||||
var tags = results[2] as List<TagModel>;
|
||||
var contacts = results[3] as List<BusinessMemberModel>;
|
||||
var skills = results[4] as List<BusinessSkillModel>;
|
||||
|
||||
emit(state.copyWith(
|
||||
inLoading: false,
|
||||
hubs: hubs,
|
||||
tags: tags,
|
||||
addons: addons,
|
||||
contacts: contacts,
|
||||
skills: skills));
|
||||
}
|
||||
|
||||
void _onChangeHub(
|
||||
CreateEventChangeHub event, Emitter<CreateEventState> emit) async {
|
||||
if (event.hub == state.entity.hub) return;
|
||||
emit(state.copyWith(
|
||||
entity: state.entity.copyWith(hub: event.hub),
|
||||
));
|
||||
|
||||
state.shifts.forEach((element) {
|
||||
element.bloc
|
||||
.add(CreateShiftAddressSelectEvent(address: event.hub.fullAddress));
|
||||
});
|
||||
|
||||
var departments =
|
||||
await getIt<CreateEventRepository>().getDepartments(event.hub.id);
|
||||
|
||||
emit(state.copyWith(
|
||||
departments: departments,
|
||||
));
|
||||
}
|
||||
|
||||
// void _onChangeContractType(
|
||||
// CreateEventChangeContractType event, Emitter<CreateEventState> emit) {
|
||||
// if (event.contractType == state.entity.contractType) return;
|
||||
// emit(state.copyWith(
|
||||
// entity: state.entity.copyWith(contractType: event.contractType),
|
||||
// ));
|
||||
// }
|
||||
|
||||
// void _onChangeContractNumber(
|
||||
// CreateEventChangeContractNumber event, Emitter<CreateEventState> emit) {
|
||||
// emit(state.copyWith(
|
||||
// entity: state.entity.copyWith(contractNumber: event.contractNumber),
|
||||
// ));
|
||||
// }
|
||||
//
|
||||
void _onChangePoNumber(
|
||||
CreateEventChangePoNumber event, Emitter<CreateEventState> emit) {
|
||||
emit(state.copyWith(
|
||||
entity: state.entity.copyWith(poNumber: event.poNumber),
|
||||
));
|
||||
}
|
||||
|
||||
void _onTagSelected(
|
||||
CreateEventTagSelected event, Emitter<CreateEventState> emit) {
|
||||
final tags = List<TagModel>.of(state.entity.tags ?? []);
|
||||
if (tags.any((e) => e.id == event.tag.id)) {
|
||||
tags.removeWhere((e) => e.id == event.tag.id);
|
||||
} else {
|
||||
tags.add(event.tag);
|
||||
}
|
||||
emit(state.copyWith(
|
||||
entity: state.entity.copyWith(tags: tags),
|
||||
));
|
||||
}
|
||||
|
||||
void _onAddShift(CreateEventAddShift event, Emitter<CreateEventState> emit) {
|
||||
final id = DateTime.now().millisecondsSinceEpoch.toString();
|
||||
ShiftEntity newShiftEntity = ShiftEntity.empty();
|
||||
|
||||
final bloc = CreateShiftDetailsBloc(expanded: true)
|
||||
..add(CreateShiftInitializeEvent(
|
||||
newShiftEntity,
|
||||
));
|
||||
|
||||
newShiftEntity.parentEvent = state.entity;
|
||||
state.entity.shifts?.add(newShiftEntity);
|
||||
|
||||
emit(state.copyWith(
|
||||
shifts: [
|
||||
...state.shifts,
|
||||
ShiftViewModel(id: id, bloc: bloc),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
void _onRemoveShift(
|
||||
CreateEventRemoveShift event, Emitter<CreateEventState> emit) {
|
||||
emit(state.copyWith(
|
||||
entity: state.entity.copyWith(
|
||||
shifts: state.entity.shifts
|
||||
?.where((element) => element.id != event.id)
|
||||
.toList()),
|
||||
shifts: state.shifts.where((element) => element.id != event.id).toList(),
|
||||
));
|
||||
}
|
||||
|
||||
void _onNameChange(
|
||||
CreateEventNameChange event, Emitter<CreateEventState> emit) {
|
||||
emit(state.copyWith(
|
||||
entity: state.entity.copyWith(name: event.value),
|
||||
));
|
||||
}
|
||||
|
||||
void _onAddInfoChange(
|
||||
CreateEventAddInfoChange event, Emitter<CreateEventState> emit) {
|
||||
emit(state.copyWith(
|
||||
entity: state.entity.copyWith(additionalInfo: event.value),
|
||||
));
|
||||
}
|
||||
|
||||
void _onToggleAddon(
|
||||
CreateEventToggleAddon event, Emitter<CreateEventState> emit) {
|
||||
final addons = List<AddonModel>.of(state.entity.addons ?? []);
|
||||
if (addons.any((e) => e.id == event.addon.id)) {
|
||||
addons.removeWhere((e) => e.id == event.addon.id);
|
||||
} else {
|
||||
addons.add(event.addon);
|
||||
}
|
||||
emit(state.copyWith(
|
||||
entity: state.entity.copyWith(addons: addons),
|
||||
));
|
||||
}
|
||||
|
||||
void _onEntityUpdated(
|
||||
CreateEventEntityUpdatedEvent event, Emitter<CreateEventState> emit) {
|
||||
emit(state.copyWith());
|
||||
}
|
||||
|
||||
void _onValidateAndPreview(
|
||||
CreateEventValidateAndPreview event, Emitter<CreateEventState> emit) {
|
||||
var newState = CreateEventInputValidator.validateInputs(
|
||||
state.copyWith(entity: state.entity.copyWith()));
|
||||
emit(newState);
|
||||
emit(newState.copyWith(valid: false));
|
||||
}
|
||||
|
||||
void _onDeleteDraft(
|
||||
DeleteDraftEvent event, Emitter<CreateEventState> emit) async {
|
||||
await getIt<CreateEventService>().deleteDraft(state.entity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
part of 'create_event_bloc.dart';
|
||||
|
||||
@immutable
|
||||
sealed class CreateEventEvent {}
|
||||
|
||||
class CreateEventInit extends CreateEventEvent {
|
||||
final EventModel? eventModel;
|
||||
final EventScheduleType? recurringType;
|
||||
|
||||
CreateEventInit(this.recurringType, this.eventModel);
|
||||
}
|
||||
|
||||
// class CreateEventChangeContractType extends CreateEventEvent {
|
||||
// final EventContractType contractType;
|
||||
//
|
||||
// CreateEventChangeContractType(this.contractType);
|
||||
// }
|
||||
|
||||
class CreateEventChangeHub extends CreateEventEvent {
|
||||
final HubModel hub;
|
||||
|
||||
CreateEventChangeHub(this.hub);
|
||||
}
|
||||
|
||||
class CreateEventValidateAndPreview extends CreateEventEvent {
|
||||
CreateEventValidateAndPreview();
|
||||
}
|
||||
|
||||
// class CreateEventChangeContractNumber extends CreateEventEvent {
|
||||
// final String contractNumber;
|
||||
//
|
||||
// CreateEventChangeContractNumber(this.contractNumber);
|
||||
// }
|
||||
|
||||
class CreateEventChangePoNumber extends CreateEventEvent {
|
||||
final String poNumber;
|
||||
|
||||
CreateEventChangePoNumber(this.poNumber);
|
||||
}
|
||||
|
||||
class CreateEventNameChange extends CreateEventEvent {
|
||||
final String value;
|
||||
|
||||
CreateEventNameChange(this.value);
|
||||
}
|
||||
|
||||
class CreateEventAddInfoChange extends CreateEventEvent {
|
||||
final String value;
|
||||
|
||||
CreateEventAddInfoChange(this.value);
|
||||
}
|
||||
|
||||
class CreateEventTagSelected extends CreateEventEvent {
|
||||
final TagModel tag;
|
||||
|
||||
CreateEventTagSelected(this.tag);
|
||||
}
|
||||
|
||||
class CreateEventAddShift extends CreateEventEvent {
|
||||
CreateEventAddShift();
|
||||
}
|
||||
|
||||
class CreateEventRemoveShift extends CreateEventEvent {
|
||||
final String id;
|
||||
|
||||
CreateEventRemoveShift(this.id);
|
||||
}
|
||||
|
||||
class CreateEventToggleAddon extends CreateEventEvent {
|
||||
final AddonModel addon;
|
||||
|
||||
CreateEventToggleAddon(this.addon);
|
||||
}
|
||||
|
||||
class CreateEventEntityUpdatedEvent extends CreateEventEvent {
|
||||
CreateEventEntityUpdatedEvent();
|
||||
}
|
||||
|
||||
class DeleteDraftEvent extends CreateEventEvent {
|
||||
DeleteDraftEvent();
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
part of 'create_event_bloc.dart';
|
||||
|
||||
@immutable
|
||||
class CreateEventState {
|
||||
final bool inLoading;
|
||||
final bool success;
|
||||
final bool valid;
|
||||
final EventEntity entity;
|
||||
final EventScheduleType recurringType;
|
||||
final List<HubModel> hubs;
|
||||
final List<TagModel> tags;
|
||||
final List<BusinessMemberModel> contacts;
|
||||
final List<ShiftViewModel> shifts;
|
||||
final List<AddonModel> addons;
|
||||
final List<BusinessSkillModel> skills;
|
||||
final List<DepartmentModel> departments;
|
||||
final EventValidationState? validationState;
|
||||
|
||||
final bool showEmptyFieldError;
|
||||
|
||||
const CreateEventState(
|
||||
{required this.entity,
|
||||
this.inLoading = false,
|
||||
this.valid = false,
|
||||
this.showEmptyFieldError = false,
|
||||
this.success = false,
|
||||
this.recurringType = EventScheduleType.oneTime,
|
||||
this.hubs = const [],
|
||||
this.tags = const [],
|
||||
this.contacts = const [],
|
||||
this.addons = const [],
|
||||
this.skills = const [],
|
||||
this.shifts = const [],
|
||||
this.departments = const [],
|
||||
this.validationState});
|
||||
|
||||
CreateEventState copyWith({
|
||||
bool? inLoading,
|
||||
bool? success,
|
||||
bool? valid,
|
||||
bool? showEmptyFieldError,
|
||||
EventEntity? entity,
|
||||
EventScheduleType? recurringType,
|
||||
List<HubModel>? hubs,
|
||||
List<TagModel>? tags,
|
||||
List<BusinessMemberModel>? contacts,
|
||||
List<ShiftViewModel>? shifts,
|
||||
List<AddonModel>? addons,
|
||||
List<BusinessSkillModel>? skills,
|
||||
List<DepartmentModel>? departments,
|
||||
EventValidationState? validationState,
|
||||
}) {
|
||||
return CreateEventState(
|
||||
success: success ?? this.success,
|
||||
valid: valid ?? this.valid,
|
||||
inLoading: inLoading ?? this.inLoading,
|
||||
showEmptyFieldError: showEmptyFieldError ?? this.showEmptyFieldError,
|
||||
entity: entity ?? this.entity,
|
||||
recurringType: recurringType ?? this.recurringType,
|
||||
hubs: hubs ?? this.hubs,
|
||||
tags: tags ?? this.tags,
|
||||
contacts: contacts ?? this.contacts,
|
||||
shifts: shifts ?? this.shifts,
|
||||
addons: addons ?? this.addons,
|
||||
skills: skills ?? this.skills,
|
||||
departments: departments ?? this.departments,
|
||||
validationState: validationState,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ShiftViewModel {
|
||||
final String id;
|
||||
final CreateShiftDetailsBloc bloc;
|
||||
|
||||
ShiftViewModel({required this.id, required this.bloc});
|
||||
}
|
||||
|
||||
class EventValidationState {
|
||||
final String? nameError;
|
||||
final String? startDateError;
|
||||
final String? endDateError;
|
||||
final String? hubError;
|
||||
// final String? contractNumberError;
|
||||
final String? poNumberError;
|
||||
final String? shiftsError;
|
||||
|
||||
bool showed = false;
|
||||
|
||||
bool get hasError =>
|
||||
nameError != null ||
|
||||
startDateError != null ||
|
||||
endDateError != null ||
|
||||
hubError != null ||
|
||||
// contractNumberError != null ||
|
||||
poNumberError != null ||
|
||||
shiftsError != null;
|
||||
|
||||
String? get message {
|
||||
return nameError ??
|
||||
startDateError ??
|
||||
endDateError ??
|
||||
hubError ??
|
||||
// contractNumberError ??
|
||||
poNumberError ??
|
||||
shiftsError ??
|
||||
'';
|
||||
}
|
||||
|
||||
EventValidationState(
|
||||
{this.nameError,
|
||||
this.startDateError,
|
||||
this.endDateError,
|
||||
this.hubError,
|
||||
// this.contractNumberError,
|
||||
this.poNumberError,
|
||||
this.shiftsError});
|
||||
|
||||
EventValidationState copyWith({
|
||||
String? nameError,
|
||||
String? startDateError,
|
||||
String? endDateError,
|
||||
String? hubError,
|
||||
// String? contractNumberError,
|
||||
String? poNumberError,
|
||||
String? shiftsError,
|
||||
}) {
|
||||
return EventValidationState(
|
||||
nameError: nameError ?? this.nameError,
|
||||
startDateError: startDateError ?? this.startDateError,
|
||||
endDateError: endDateError ?? this.endDateError,
|
||||
hubError: hubError ?? this.hubError,
|
||||
// contractNumberError: contractNumberError ?? this.contractNumberError,
|
||||
poNumberError: poNumberError ?? this.poNumberError,
|
||||
shiftsError: shiftsError ?? this.shiftsError,
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user