feat: Refactor code structure and optimize performance across multiple modules

This commit is contained in:
Achintha Isuru
2025-11-17 23:29:28 -05:00
parent 831570f2e0
commit a64cbd9edf
1508 changed files with 105319 additions and 0 deletions

View File

@@ -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();
}
}

View File

@@ -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();
}

View File

@@ -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',
};
}

View File

@@ -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;

View File

@@ -0,0 +1,6 @@
extension StringExtensions on String {
String capitalize() {
if (isEmpty) return this;
return this[0].toUpperCase() + substring(1).toLowerCase();
}
}

View File

@@ -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));
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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);
}
}

View 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 => [];
}

View File

@@ -0,0 +1,6 @@
enum StateStatus {
idle,
loading,
error,
success,
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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'
};
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View 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);
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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(),
);
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View 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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View 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;
}
}

View 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;
}
}

View File

@@ -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),
);
}
}

View 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;
}
}

View 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
};
}
}

View File

@@ -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),
);
}

View File

@@ -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);
}

View File

@@ -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,
),
);
}
}

View File

@@ -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()),
)
],
),
),
),
);
}
}

View File

@@ -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,
),
],
),
)
],
),
);
}
}

View File

@@ -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,
),
)
],
),
),
),
),
),
],
);
}
}

View File

@@ -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,
),
),
),
),
],
),
);
}
}

View File

@@ -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;
},
);
}
}

View File

@@ -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 Didnt 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',
);
}
}

View File

@@ -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),
)
],
),
),
]
],
);
}
}

View File

@@ -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;
}
}

View File

@@ -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,
),
);
}
}

View File

@@ -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,
);
},
);
}
}

View File

@@ -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();
}
},
),
];
}
}

View File

@@ -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,
);
}
}

View File

@@ -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);
}
}

View File

@@ -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});
}

View File

@@ -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();
}
}

View File

@@ -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(),
),
),
],
)
]
],
),
);
}
}

View File

@@ -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();
}
}

View File

@@ -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]),
),
),
),
),
],
),
],
),
);
},
)
],
);
}
}

View File

@@ -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,
),
),
),
);
}
}

View File

@@ -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);
}
}

View File

@@ -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,
});
}

View File

@@ -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,
);
}
}

View File

@@ -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,
);
}
}

View 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';
}
},
),
],
),
);
}
},
);
}
}

View File

@@ -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();
}
}

View File

@@ -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!;
// }
}

View File

@@ -0,0 +1,8 @@
const String getStaffStatusQuery = '''
query GetMe {
me {
id
status
}
}
''';

View File

@@ -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(),
);
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}
}

View File

@@ -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
}
}
''';

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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,
}

View File

@@ -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));
}
}