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');
|
||||
});
|
||||
}
|
||||
@@ -45,8 +45,8 @@
|
||||
| 11 | tdc114plus 개발 정책 정리 | 완료 | Baron SSO API 생성 우선 원칙, VS Code 기반 AI 개발 방식, Flutter/플랫폼 구현 원칙 정리 | `docs/tdc114plus-development-policy-2026-07-02.md` |
|
||||
| 12 | API 계약 정리 | 완료 | Baron SSO 로그인 API, orgFront 직원/조직 API 계약 정리 | `docs/tdc114plus-api-contract-2026-07-02.md` |
|
||||
| 13 | API 계약 검토 및 확정 | 완료 | API 계약 초안의 추가 확인사항 검토 후 1차 구현 기준 확정 | `docs/tdc114plus-api-contract-2026-07-02.md` |
|
||||
| 14 | 데이터 모델 설계 | 다음 작업 | 직원, 조직, 가족사, 즐겨찾기 모델 정의 | Dart model |
|
||||
| 15 | Mock 데이터 기반 화면 확장 | 대기 | 직원목록, 검색, 가족사 필터, 조직도 화면을 mock 데이터로 우선 구현 | 동작 가능한 UI |
|
||||
| 14 | 데이터 모델 설계 | 완료 | 직원, 조직, 가족사, 즐겨찾기 모델 정의 | Dart model 및 model test |
|
||||
| 15 | Mock 데이터 기반 화면 확장 | 다음 작업 | 직원목록, 검색, 가족사 필터, 조직도 화면을 mock 데이터로 우선 구현 | 동작 가능한 UI |
|
||||
| 16 | Baron SSO 로그인 연동 | 대기 | 전화번호 입력 후 SSO 등록 인원 여부 확인 연동 | 로그인 client/repository |
|
||||
| 17 | orgFront 데이터 연동 | 대기 | 직원/조직 데이터 API 연동 | directory/organization client |
|
||||
| 18 | 전화/문자 액션 구현 | 대기 | `url_launcher` 기반 전화걸기/문자보내기 구현 | 연락 액션 |
|
||||
@@ -62,15 +62,15 @@
|
||||
| Phase 2 | 완료 | Baron SSO 참조 기준 확정 및 API 계약 준비 | 공식 `origin/dev` 기준 확인, API 개발 worktree 생성, Baron Safe 참고 정책 확인 | `feature/tdc114plus-api` worktree 생성 |
|
||||
| Phase 2-1 | 완료 | API 계약 초안 작성 | 개발 정책 확인 후 SSO 로그인 API, orgFront 직원/조직 API, 응답 필드, 오류 정책 정리 | API 계약 초안 작성 완료 |
|
||||
| Phase 2-2 | 완료 | API 계약 검토 및 확정 | token 종류, 전화번호 로그인 보안 수준, 개인정보 마스킹 범위, 즐겨찾기 동기화 여부 확인 | API 계약 확정본 |
|
||||
| Phase 2-3 | 다음 | Dart 데이터 모델 설계 | 확정된 API 계약 기준으로 직원, 조직, 로그인, 즐겨찾기 model 정의 | model 초안 |
|
||||
| Phase 3 | 이후 | Mock 기반 1차 UI 완성 | 직원목록, 검색, 가족사 필터, 조직도, 상세 화면 구성 | API 없이 화면 흐름 확인 가능 |
|
||||
| Phase 2-3 | 완료 | Dart 데이터 모델 설계 | 확정된 API 계약 기준으로 직원, 조직, 로그인, 즐겨찾기 model 정의 | analyze/test 통과 |
|
||||
| Phase 3 | 다음 | Mock 기반 1차 UI 완성 | 직원목록, 검색, 가족사 필터, 조직도, 상세 화면 구성 | API 없이 화면 흐름 확인 가능 |
|
||||
| Phase 4 | 이후 | 실제 API 연동 | SSO 로그인, orgFront 직원/조직 데이터 연동 | 등록 사용자 로그인 및 직원목록 조회 |
|
||||
| Phase 5 | 이후 | 핵심 액션 완성 | 전화걸기, 문자보내기, 즐겨찾기 저장 | 1차 기본 기능 수동 검증 |
|
||||
| Phase 6 | 이후 | 빌드/배포 준비 | Android debug APK, README 실행법, 잔여 이슈 정리 | APK 빌드 성공 |
|
||||
|
||||
## 4. 다음 작업 판단
|
||||
|
||||
현재 다음 작업은 **Phase 2-3: Dart 데이터 모델 설계**이다.
|
||||
현재 다음 작업은 **Phase 3: Mock 기반 1차 UI 완성**이다.
|
||||
|
||||
공식 Baron SSO `origin/dev` 기준의 API 개발용 worktree는 `/home/ubuntu/workspace/baron-sso-tdc114plus-api`에 생성 완료했다. 기존 `baron-sso` 작업 브랜치는 수정/미추적 파일이 많으므로 직접 merge/rebase하지 않는다.
|
||||
|
||||
@@ -133,4 +133,4 @@ git -C /home/ubuntu/workspace/tdc114plus log --oneline origin/main -n 10
|
||||
git -C /home/ubuntu/workspace/tdc114plus status --short --branch
|
||||
```
|
||||
|
||||
현재 문서 기준 다음 작업은 `Phase 2-3: Dart 데이터 모델 설계`이다.
|
||||
현재 문서 기준 다음 작업은 `Phase 3: Mock 기반 1차 UI 완성`이다.
|
||||
|
||||
Reference in New Issue
Block a user