Add API contract Dart models
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
class ApiError {
|
||||
const ApiError({
|
||||
required this.error,
|
||||
required this.code,
|
||||
required this.details,
|
||||
});
|
||||
|
||||
final String error;
|
||||
final String code;
|
||||
final Map<String, dynamic> details;
|
||||
|
||||
factory ApiError.fromJson(Map<String, dynamic> json) {
|
||||
return ApiError(
|
||||
error: json['error'] as String? ?? '',
|
||||
code: json['code'] as String? ?? '',
|
||||
details: json['details'] as Map<String, dynamic>? ?? {},
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {'error': error, 'code': code, 'details': details};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
import '../../directory/domain/employee.dart';
|
||||
|
||||
class LoginDeviceInfo {
|
||||
const LoginDeviceInfo({
|
||||
required this.platform,
|
||||
required this.appVersion,
|
||||
required this.deviceName,
|
||||
});
|
||||
|
||||
final String platform;
|
||||
final String appVersion;
|
||||
final String deviceName;
|
||||
|
||||
factory LoginDeviceInfo.fromJson(Map<String, dynamic> json) {
|
||||
return LoginDeviceInfo(
|
||||
platform: json['platform'] as String? ?? '',
|
||||
appVersion: json['appVersion'] as String? ?? '',
|
||||
deviceName: json['deviceName'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'platform': platform,
|
||||
'appVersion': appVersion,
|
||||
'deviceName': deviceName,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class PhoneLoginRequest {
|
||||
const PhoneLoginRequest({required this.phoneNumber, required this.device});
|
||||
|
||||
final String phoneNumber;
|
||||
final LoginDeviceInfo device;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {'phoneNumber': phoneNumber, 'device': device.toJson()};
|
||||
}
|
||||
}
|
||||
|
||||
class LoginUser {
|
||||
const LoginUser({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.phoneNumber,
|
||||
required this.tenantId,
|
||||
required this.tenantName,
|
||||
required this.tenantSlug,
|
||||
this.department,
|
||||
this.grade,
|
||||
this.position,
|
||||
this.jobTitle,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String name;
|
||||
final String phoneNumber;
|
||||
final String tenantId;
|
||||
final String tenantName;
|
||||
final String tenantSlug;
|
||||
final String? department;
|
||||
final String? grade;
|
||||
final String? position;
|
||||
final String? jobTitle;
|
||||
|
||||
factory LoginUser.fromJson(Map<String, dynamic> json) {
|
||||
return LoginUser(
|
||||
id: json['id'] as String? ?? '',
|
||||
name: json['name'] as String? ?? '',
|
||||
phoneNumber: json['phoneNumber'] as String? ?? '',
|
||||
tenantId: json['tenantId'] as String? ?? '',
|
||||
tenantName: json['tenantName'] as String? ?? '',
|
||||
tenantSlug: json['tenantSlug'] as String? ?? '',
|
||||
department: json['department'] as String?,
|
||||
grade: json['grade'] as String?,
|
||||
position: json['position'] as String?,
|
||||
jobTitle: json['jobTitle'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'phoneNumber': phoneNumber,
|
||||
'tenantId': tenantId,
|
||||
'tenantName': tenantName,
|
||||
'tenantSlug': tenantSlug,
|
||||
'department': department,
|
||||
'grade': grade,
|
||||
'position': position,
|
||||
'jobTitle': jobTitle,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class PhoneLoginResponse {
|
||||
const PhoneLoginResponse({
|
||||
required this.status,
|
||||
required this.token,
|
||||
required this.expiresAt,
|
||||
required this.user,
|
||||
});
|
||||
|
||||
final String status;
|
||||
final String token;
|
||||
final DateTime expiresAt;
|
||||
final LoginUser user;
|
||||
|
||||
factory PhoneLoginResponse.fromJson(Map<String, dynamic> json) {
|
||||
return PhoneLoginResponse(
|
||||
status: json['status'] as String? ?? '',
|
||||
token: json['token'] as String? ?? '',
|
||||
expiresAt: DateTime.parse(json['expiresAt'] as String),
|
||||
user: LoginUser.fromJson(json['user'] as Map<String, dynamic>? ?? {}),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'status': status,
|
||||
'token': token,
|
||||
'expiresAt': expiresAt.toUtc().toIso8601String(),
|
||||
'user': user.toJson(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class UserPermissions {
|
||||
const UserPermissions({
|
||||
required this.directory,
|
||||
required this.organization,
|
||||
required this.favoritesSync,
|
||||
});
|
||||
|
||||
final bool directory;
|
||||
final bool organization;
|
||||
final bool favoritesSync;
|
||||
|
||||
factory UserPermissions.fromJson(Map<String, dynamic> json) {
|
||||
return UserPermissions(
|
||||
directory: json['directory'] as bool? ?? false,
|
||||
organization: json['organization'] as bool? ?? false,
|
||||
favoritesSync: json['favoritesSync'] as bool? ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'directory': directory,
|
||||
'organization': organization,
|
||||
'favoritesSync': favoritesSync,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class CurrentUser {
|
||||
const CurrentUser({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.phoneNumber,
|
||||
required this.email,
|
||||
required this.tenantId,
|
||||
required this.tenantName,
|
||||
required this.tenantSlug,
|
||||
required this.permissions,
|
||||
this.department,
|
||||
this.grade,
|
||||
this.position,
|
||||
this.jobTitle,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String name;
|
||||
final String phoneNumber;
|
||||
final String email;
|
||||
final String tenantId;
|
||||
final String tenantName;
|
||||
final String tenantSlug;
|
||||
final UserPermissions permissions;
|
||||
final String? department;
|
||||
final String? grade;
|
||||
final String? position;
|
||||
final String? jobTitle;
|
||||
|
||||
factory CurrentUser.fromJson(Map<String, dynamic> json) {
|
||||
return CurrentUser(
|
||||
id: json['id'] as String? ?? '',
|
||||
name: json['name'] as String? ?? '',
|
||||
phoneNumber: json['phoneNumber'] as String? ?? '',
|
||||
email: json['email'] as String? ?? '',
|
||||
tenantId: json['tenantId'] as String? ?? '',
|
||||
tenantName: json['tenantName'] as String? ?? '',
|
||||
tenantSlug: json['tenantSlug'] as String? ?? '',
|
||||
permissions: UserPermissions.fromJson(
|
||||
json['permissions'] as Map<String, dynamic>? ?? {},
|
||||
),
|
||||
department: json['department'] as String?,
|
||||
grade: json['grade'] as String?,
|
||||
position: json['position'] as String?,
|
||||
jobTitle: json['jobTitle'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Employee toEmployee() {
|
||||
return Employee(
|
||||
id: id,
|
||||
name: name,
|
||||
phoneNumber: phoneNumber,
|
||||
email: email,
|
||||
tenantId: tenantId,
|
||||
tenantName: tenantName,
|
||||
tenantSlug: tenantSlug,
|
||||
department: department,
|
||||
grade: grade,
|
||||
position: position,
|
||||
jobTitle: jobTitle,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'phoneNumber': phoneNumber,
|
||||
'email': email,
|
||||
'tenantId': tenantId,
|
||||
'tenantName': tenantName,
|
||||
'tenantSlug': tenantSlug,
|
||||
'department': department,
|
||||
'grade': grade,
|
||||
'position': position,
|
||||
'jobTitle': jobTitle,
|
||||
'permissions': permissions.toJson(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
class Employee {
|
||||
const Employee({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.phoneNumber,
|
||||
this.phoneDisplay,
|
||||
this.email,
|
||||
required this.tenantId,
|
||||
required this.tenantName,
|
||||
required this.tenantSlug,
|
||||
this.department,
|
||||
this.grade,
|
||||
this.position,
|
||||
this.jobTitle,
|
||||
this.status,
|
||||
this.profileImageUrl,
|
||||
this.sortOrder,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String name;
|
||||
final String phoneNumber;
|
||||
final String? phoneDisplay;
|
||||
final String? email;
|
||||
final String tenantId;
|
||||
final String tenantName;
|
||||
final String tenantSlug;
|
||||
final String? department;
|
||||
final String? grade;
|
||||
final String? position;
|
||||
final String? jobTitle;
|
||||
final String? status;
|
||||
final String? profileImageUrl;
|
||||
final int? sortOrder;
|
||||
|
||||
factory Employee.fromJson(Map<String, dynamic> json) {
|
||||
return Employee(
|
||||
id: json['id'] as String? ?? '',
|
||||
name: json['name'] as String? ?? '',
|
||||
phoneNumber: json['phoneNumber'] as String? ?? '',
|
||||
phoneDisplay: json['phoneDisplay'] as String?,
|
||||
email: json['email'] as String?,
|
||||
tenantId: json['tenantId'] as String? ?? '',
|
||||
tenantName: json['tenantName'] as String? ?? '',
|
||||
tenantSlug: json['tenantSlug'] as String? ?? '',
|
||||
department: json['department'] as String?,
|
||||
grade: json['grade'] as String?,
|
||||
position: json['position'] as String?,
|
||||
jobTitle: json['jobTitle'] as String?,
|
||||
status: json['status'] as String?,
|
||||
profileImageUrl: json['profileImageUrl'] as String?,
|
||||
sortOrder: json['sortOrder'] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'phoneNumber': phoneNumber,
|
||||
'phoneDisplay': phoneDisplay,
|
||||
'email': email,
|
||||
'tenantId': tenantId,
|
||||
'tenantName': tenantName,
|
||||
'tenantSlug': tenantSlug,
|
||||
'department': department,
|
||||
'grade': grade,
|
||||
'position': position,
|
||||
'jobTitle': jobTitle,
|
||||
'status': status,
|
||||
'profileImageUrl': profileImageUrl,
|
||||
'sortOrder': sortOrder,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class TenantRef {
|
||||
const TenantRef({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.slug,
|
||||
required this.type,
|
||||
this.parentId,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String name;
|
||||
final String slug;
|
||||
final String type;
|
||||
final String? parentId;
|
||||
|
||||
factory TenantRef.fromJson(Map<String, dynamic> json) {
|
||||
return TenantRef(
|
||||
id: json['id'] as String? ?? '',
|
||||
name: json['name'] as String? ?? '',
|
||||
slug: json['slug'] as String? ?? '',
|
||||
type: json['type'] as String? ?? '',
|
||||
parentId: json['parentId'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'slug': slug,
|
||||
'type': type,
|
||||
'parentId': parentId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class EmployeeActions {
|
||||
const EmployeeActions({
|
||||
required this.call,
|
||||
required this.sms,
|
||||
required this.email,
|
||||
});
|
||||
|
||||
final bool call;
|
||||
final bool sms;
|
||||
final bool email;
|
||||
|
||||
factory EmployeeActions.fromJson(Map<String, dynamic> json) {
|
||||
return EmployeeActions(
|
||||
call: json['call'] as bool? ?? false,
|
||||
sms: json['sms'] as bool? ?? false,
|
||||
email: json['email'] as bool? ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {'call': call, 'sms': sms, 'email': email};
|
||||
}
|
||||
}
|
||||
|
||||
class EmployeeDetail extends Employee {
|
||||
const EmployeeDetail({
|
||||
required super.id,
|
||||
required super.name,
|
||||
required super.phoneNumber,
|
||||
super.phoneDisplay,
|
||||
super.email,
|
||||
required super.tenantId,
|
||||
required super.tenantName,
|
||||
required super.tenantSlug,
|
||||
super.department,
|
||||
super.grade,
|
||||
super.position,
|
||||
super.jobTitle,
|
||||
super.status,
|
||||
super.profileImageUrl,
|
||||
super.sortOrder,
|
||||
required this.joinedTenants,
|
||||
required this.actions,
|
||||
});
|
||||
|
||||
final List<TenantRef> joinedTenants;
|
||||
final EmployeeActions actions;
|
||||
|
||||
factory EmployeeDetail.fromJson(Map<String, dynamic> json) {
|
||||
return EmployeeDetail(
|
||||
id: json['id'] as String? ?? '',
|
||||
name: json['name'] as String? ?? '',
|
||||
phoneNumber: json['phoneNumber'] as String? ?? '',
|
||||
phoneDisplay: json['phoneDisplay'] as String?,
|
||||
email: json['email'] as String?,
|
||||
tenantId: json['tenantId'] as String? ?? '',
|
||||
tenantName: json['tenantName'] as String? ?? '',
|
||||
tenantSlug: json['tenantSlug'] as String? ?? '',
|
||||
department: json['department'] as String?,
|
||||
grade: json['grade'] as String?,
|
||||
position: json['position'] as String?,
|
||||
jobTitle: json['jobTitle'] as String?,
|
||||
status: json['status'] as String?,
|
||||
profileImageUrl: json['profileImageUrl'] as String?,
|
||||
sortOrder: json['sortOrder'] as int?,
|
||||
joinedTenants: (json['joinedTenants'] as List<dynamic>? ?? [])
|
||||
.map((item) => TenantRef.fromJson(item as Map<String, dynamic>))
|
||||
.toList(),
|
||||
actions: EmployeeActions.fromJson(
|
||||
json['actions'] as Map<String, dynamic>? ?? {},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
...super.toJson(),
|
||||
'joinedTenants': joinedTenants.map((tenant) => tenant.toJson()).toList(),
|
||||
'actions': actions.toJson(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class EmployeeListResponse {
|
||||
const EmployeeListResponse({
|
||||
required this.items,
|
||||
required this.limit,
|
||||
required this.offset,
|
||||
required this.total,
|
||||
required this.nextCursor,
|
||||
});
|
||||
|
||||
final List<Employee> items;
|
||||
final int limit;
|
||||
final int offset;
|
||||
final int total;
|
||||
final String nextCursor;
|
||||
|
||||
factory EmployeeListResponse.fromJson(Map<String, dynamic> json) {
|
||||
return EmployeeListResponse(
|
||||
items: (json['items'] as List<dynamic>? ?? [])
|
||||
.map((item) => Employee.fromJson(item as Map<String, dynamic>))
|
||||
.toList(),
|
||||
limit: json['limit'] as int? ?? 0,
|
||||
offset: json['offset'] as int? ?? 0,
|
||||
total: json['total'] as int? ?? 0,
|
||||
nextCursor: json['nextCursor'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'items': items.map((employee) => employee.toJson()).toList(),
|
||||
'limit': limit,
|
||||
'offset': offset,
|
||||
'total': total,
|
||||
'nextCursor': nextCursor,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
class FavoriteEmployee {
|
||||
const FavoriteEmployee({required this.employeeId, required this.createdAt});
|
||||
|
||||
final String employeeId;
|
||||
final DateTime createdAt;
|
||||
|
||||
factory FavoriteEmployee.fromJson(Map<String, dynamic> json) {
|
||||
return FavoriteEmployee(
|
||||
employeeId: json['employeeId'] as String? ?? '',
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'employeeId': employeeId,
|
||||
'createdAt': createdAt.toUtc().toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import '../../directory/domain/employee.dart';
|
||||
|
||||
class TenantSummary {
|
||||
const TenantSummary({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.slug,
|
||||
required this.type,
|
||||
this.parentId,
|
||||
required this.memberCount,
|
||||
required this.totalMemberCount,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String name;
|
||||
final String slug;
|
||||
final String type;
|
||||
final String? parentId;
|
||||
final int memberCount;
|
||||
final int totalMemberCount;
|
||||
|
||||
factory TenantSummary.fromJson(Map<String, dynamic> json) {
|
||||
return TenantSummary(
|
||||
id: json['id'] as String? ?? '',
|
||||
name: json['name'] as String? ?? '',
|
||||
slug: json['slug'] as String? ?? '',
|
||||
type: json['type'] as String? ?? '',
|
||||
parentId: json['parentId'] as String?,
|
||||
memberCount: json['memberCount'] as int? ?? 0,
|
||||
totalMemberCount: json['totalMemberCount'] as int? ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'slug': slug,
|
||||
'type': type,
|
||||
'parentId': parentId,
|
||||
'memberCount': memberCount,
|
||||
'totalMemberCount': totalMemberCount,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class TenantListResponse {
|
||||
const TenantListResponse({required this.items, required this.generatedAt});
|
||||
|
||||
final List<TenantSummary> items;
|
||||
final DateTime generatedAt;
|
||||
|
||||
factory TenantListResponse.fromJson(Map<String, dynamic> json) {
|
||||
return TenantListResponse(
|
||||
items: (json['items'] as List<dynamic>? ?? [])
|
||||
.map((item) => TenantSummary.fromJson(item as Map<String, dynamic>))
|
||||
.toList(),
|
||||
generatedAt: DateTime.parse(json['generatedAt'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'items': items.map((tenant) => tenant.toJson()).toList(),
|
||||
'generatedAt': generatedAt.toUtc().toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class OrgChartCacheInfo {
|
||||
const OrgChartCacheInfo({
|
||||
required this.source,
|
||||
required this.hit,
|
||||
this.ttlSeconds,
|
||||
});
|
||||
|
||||
final String source;
|
||||
final bool hit;
|
||||
final int? ttlSeconds;
|
||||
|
||||
factory OrgChartCacheInfo.fromJson(Map<String, dynamic> json) {
|
||||
return OrgChartCacheInfo(
|
||||
source: json['source'] as String? ?? '',
|
||||
hit: json['hit'] as bool? ?? false,
|
||||
ttlSeconds: json['ttlSeconds'] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {'source': source, 'hit': hit, 'ttlSeconds': ttlSeconds};
|
||||
}
|
||||
}
|
||||
|
||||
class OrgChartSnapshot {
|
||||
const OrgChartSnapshot({
|
||||
required this.tenants,
|
||||
required this.employees,
|
||||
required this.generatedAt,
|
||||
this.cache,
|
||||
});
|
||||
|
||||
final List<TenantSummary> tenants;
|
||||
final List<Employee> employees;
|
||||
final DateTime generatedAt;
|
||||
final OrgChartCacheInfo? cache;
|
||||
|
||||
factory OrgChartSnapshot.fromJson(Map<String, dynamic> json) {
|
||||
return OrgChartSnapshot(
|
||||
tenants: (json['tenants'] as List<dynamic>? ?? [])
|
||||
.map((item) => TenantSummary.fromJson(item as Map<String, dynamic>))
|
||||
.toList(),
|
||||
employees: (json['employees'] as List<dynamic>? ?? [])
|
||||
.map((item) => Employee.fromJson(item as Map<String, dynamic>))
|
||||
.toList(),
|
||||
generatedAt: DateTime.parse(json['generatedAt'] as String),
|
||||
cache: json['cache'] == null
|
||||
? null
|
||||
: OrgChartCacheInfo.fromJson(json['cache'] as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'tenants': tenants.map((tenant) => tenant.toJson()).toList(),
|
||||
'employees': employees.map((employee) => employee.toJson()).toList(),
|
||||
'generatedAt': generatedAt.toUtc().toIso8601String(),
|
||||
'cache': cache?.toJson(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tdc114plus/src/core/network/api_error.dart';
|
||||
|
||||
void main() {
|
||||
test('ApiError parses common error payload', () {
|
||||
final error = ApiError.fromJson({
|
||||
'error': 'Unauthorized',
|
||||
'code': 'unauthorized',
|
||||
'details': {'reason': 'expired'},
|
||||
});
|
||||
|
||||
expect(error.code, 'unauthorized');
|
||||
expect(error.details['reason'], 'expired');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tdc114plus/src/features/auth/domain/auth_models.dart';
|
||||
|
||||
void main() {
|
||||
test('PhoneLoginRequest serializes phone login payload', () {
|
||||
const request = PhoneLoginRequest(
|
||||
phoneNumber: '01012345678',
|
||||
device: LoginDeviceInfo(
|
||||
platform: 'android',
|
||||
appVersion: '0.1.0',
|
||||
deviceName: 'Pixel 8',
|
||||
),
|
||||
);
|
||||
|
||||
expect(request.toJson(), {
|
||||
'phoneNumber': '01012345678',
|
||||
'device': {
|
||||
'platform': 'android',
|
||||
'appVersion': '0.1.0',
|
||||
'deviceName': 'Pixel 8',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('PhoneLoginResponse parses session token response', () {
|
||||
final response = PhoneLoginResponse.fromJson({
|
||||
'status': 'ok',
|
||||
'token': 'baron-sso-session-token',
|
||||
'expiresAt': '2026-07-02T12:00:00Z',
|
||||
'user': {
|
||||
'id': 'user-uuid',
|
||||
'name': 'User One',
|
||||
'phoneNumber': '+821012345678',
|
||||
'tenantId': 'tenant-uuid',
|
||||
'tenantName': 'Hanmac',
|
||||
'tenantSlug': 'hanmac',
|
||||
'department': 'Engineering',
|
||||
'grade': 'Lead',
|
||||
'position': 'Manager',
|
||||
'jobTitle': 'Developer',
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status, 'ok');
|
||||
expect(response.token, 'baron-sso-session-token');
|
||||
expect(
|
||||
response.expiresAt.toUtc().toIso8601String(),
|
||||
'2026-07-02T12:00:00.000Z',
|
||||
);
|
||||
expect(response.user.tenantSlug, 'hanmac');
|
||||
});
|
||||
|
||||
test('CurrentUser parses permissions and can map to employee', () {
|
||||
final user = CurrentUser.fromJson({
|
||||
'id': 'user-uuid',
|
||||
'name': 'User One',
|
||||
'phoneNumber': '+821012345678',
|
||||
'email': 'user@example.com',
|
||||
'tenantId': 'tenant-uuid',
|
||||
'tenantName': 'Hanmac',
|
||||
'tenantSlug': 'hanmac',
|
||||
'department': 'Engineering',
|
||||
'permissions': {
|
||||
'directory': true,
|
||||
'organization': true,
|
||||
'favoritesSync': false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(user.permissions.directory, isTrue);
|
||||
expect(user.permissions.favoritesSync, isFalse);
|
||||
expect(user.toEmployee().department, 'Engineering');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tdc114plus/src/features/directory/domain/employee.dart';
|
||||
|
||||
void main() {
|
||||
test('EmployeeListResponse parses paginated employee list', () {
|
||||
final response = EmployeeListResponse.fromJson({
|
||||
'items': [
|
||||
{
|
||||
'id': 'user-uuid',
|
||||
'name': 'User One',
|
||||
'phoneNumber': '+821012345678',
|
||||
'phoneDisplay': '010-1234-5678',
|
||||
'email': 'user@example.com',
|
||||
'tenantId': 'tenant-uuid',
|
||||
'tenantName': 'Hanmac',
|
||||
'tenantSlug': 'hanmac',
|
||||
'department': 'Engineering',
|
||||
'grade': 'Lead',
|
||||
'position': 'Manager',
|
||||
'jobTitle': 'Developer',
|
||||
'status': 'active',
|
||||
'profileImageUrl': null,
|
||||
'sortOrder': 100,
|
||||
},
|
||||
],
|
||||
'limit': 50,
|
||||
'offset': 0,
|
||||
'total': 1,
|
||||
'nextCursor': '',
|
||||
});
|
||||
|
||||
expect(response.items, hasLength(1));
|
||||
expect(response.items.first.phoneDisplay, '010-1234-5678');
|
||||
expect(response.total, 1);
|
||||
});
|
||||
|
||||
test('EmployeeDetail parses joined tenants and available actions', () {
|
||||
final detail = EmployeeDetail.fromJson({
|
||||
'id': 'user-uuid',
|
||||
'name': 'User One',
|
||||
'phoneNumber': '+821012345678',
|
||||
'email': 'user@example.com',
|
||||
'tenantId': 'tenant-uuid',
|
||||
'tenantName': 'Hanmac',
|
||||
'tenantSlug': 'hanmac',
|
||||
'joinedTenants': [
|
||||
{
|
||||
'id': 'tenant-uuid',
|
||||
'name': 'Hanmac',
|
||||
'slug': 'hanmac',
|
||||
'type': 'COMPANY',
|
||||
'parentId': 'root-tenant-uuid',
|
||||
},
|
||||
],
|
||||
'status': 'active',
|
||||
'actions': {'call': true, 'sms': true, 'email': true},
|
||||
});
|
||||
|
||||
expect(detail.joinedTenants.first.type, 'COMPANY');
|
||||
expect(detail.actions.call, isTrue);
|
||||
expect(detail.actions.sms, isTrue);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tdc114plus/src/features/organization/domain/organization_models.dart';
|
||||
|
||||
void main() {
|
||||
test('TenantListResponse parses organization filter tenants', () {
|
||||
final response = TenantListResponse.fromJson({
|
||||
'items': [
|
||||
{
|
||||
'id': 'tenant-uuid',
|
||||
'name': 'Hanmac',
|
||||
'slug': 'hanmac',
|
||||
'type': 'COMPANY',
|
||||
'parentId': 'family-root',
|
||||
'memberCount': 120,
|
||||
'totalMemberCount': 350,
|
||||
},
|
||||
],
|
||||
'generatedAt': '2026-07-02T12:00:00Z',
|
||||
});
|
||||
|
||||
expect(response.items.first.slug, 'hanmac');
|
||||
expect(response.items.first.totalMemberCount, 350);
|
||||
});
|
||||
|
||||
test('OrgChartSnapshot parses tenants, employees, and cache metadata', () {
|
||||
final snapshot = OrgChartSnapshot.fromJson({
|
||||
'tenants': [
|
||||
{
|
||||
'id': 'tenant-uuid',
|
||||
'name': 'Engineering',
|
||||
'slug': 'engineering',
|
||||
'type': 'ORGANIZATION',
|
||||
'parentId': 'company-tenant-uuid',
|
||||
'memberCount': 12,
|
||||
'totalMemberCount': 38,
|
||||
},
|
||||
],
|
||||
'employees': [
|
||||
{
|
||||
'id': 'user-uuid',
|
||||
'name': 'User One',
|
||||
'phoneNumber': '+821012345678',
|
||||
'phoneDisplay': '010-1234-5678',
|
||||
'tenantId': 'tenant-uuid',
|
||||
'tenantName': 'Engineering',
|
||||
'tenantSlug': 'engineering',
|
||||
'department': 'Engineering',
|
||||
'grade': 'Lead',
|
||||
'position': 'Manager',
|
||||
'jobTitle': 'Developer',
|
||||
'status': 'active',
|
||||
},
|
||||
],
|
||||
'generatedAt': '2026-07-02T12:00:00Z',
|
||||
'cache': {'source': 'redis', 'hit': true, 'ttlSeconds': 300},
|
||||
});
|
||||
|
||||
expect(snapshot.tenants.first.type, 'ORGANIZATION');
|
||||
expect(snapshot.employees.first.tenantSlug, 'engineering');
|
||||
expect(snapshot.cache?.source, 'redis');
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user