/// 组织节点模型。 /// /// 对应后端 /sys/org/* 接口的组织实体,支持树形结构。 /// type 取值:0=平台 1=渠道 2=代理商 3=门店 4=设备分组。 class OrgNode { const OrgNode({ required this.id, required this.name, required this.type, required this.parentId, this.children = const [], required this.address, required this.contactPhone, required this.activationCode, this.province = '', this.city = '', this.themeId, DateTime? createdAt, this.expireTime, int? groupLimit, this.orderNo = 0, this.status = 'ACTIVE', this.mpath = '', }) : createdAt = createdAt, groupLimit = groupLimit; /// 组织节点 ID。 final String id; /// 组织名称。 final String name; /// 类型(0=平台 1=渠道 2=代理商 3=门店 4=设备分组)。 final int type; /// 父级 ID。 final String parentId; /// 子节点列表(树形结构)。 final List children; /// 地址。 final String address; /// 联系电话。 final String contactPhone; /// 省(新接口 /sys/org/tree 返回 province)。 final String province; /// 市(新接口 /sys/org/tree 返回 city)。 final String city; /// 主题 ID(新接口 /sys/org/tree 返回 themeId)。 final int? themeId; /// 激活码。 final String activationCode; /// 创建时间(组织树接口返回 createdAt,ISO8601 字符串)。 /// /// 门店列表需按建店时间倒序排列,故从组织树节点透传该字段; /// 老记录可能缺省(null),此时排序回退到 orgId 数值倒序。 final DateTime? createdAt; /// 到期时间(组织树/门店接口可能返回 expireTime/expire_time/expiredTime)。 /// /// 门店预览列表需展示到期状态(已过期 / 无到期时间),故从组织树节点透传该字段。 final DateTime? expireTime; /// 设备总数上限(组织树接口返回 groupLimit)。 /// /// 「建店时设置的设备上限」需回显并随门店保存,故从组织树节点透传。 final int? groupLimit; /// 排序号(组织接口返回 orderNo,渠道商管理页按此排序展示)。 final int orderNo; /// 状态:'ACTIVE'=启用,'DISABLED'=禁用。 /// /// 后端可能返回字符串(ACTIVE/DISABLED)或整型(0=正常/1=禁用, /// 与 [AppVersion.status] 约定一致),统一规整为字符串便于 UI 判断。 final String status; /// 组织编码(mpath 路径编码,渠道商管理页「组织编码」列展示)。 /// /// 后端未返回时回退为空串,UI 层再降级显示节点 id。 final String mpath; /// 后端组织类型字符串到数字的映射。 /// 旧接口用 int:0=平台 1=渠道 2=代理商 3=门店 4=设备分组。 /// 新接口 /sys/org/tree 用字符串 type:CHANNEL / SERVICE_PROVIDER / STORE / GROUP。 /// 兼容 `yyyy-MM-dd HH:mm:ss` 与 ISO 8601(含时区、毫秒)两种时间格式。 static DateTime? _parseDateTime(dynamic value) { if (value == null) return null; final text = value.toString().trim(); if (text.isEmpty) return null; return DateTime.tryParse(text) ?? DateTime.tryParse(text.replaceFirst(' ', 'T')); } /// 解析可空整型(设备上限 groupLimit 等)。 static int? _parseInt(dynamic value) { if (value == null) return null; return int.tryParse(value.toString()); } static int _parseType(dynamic value) { if (value == null) return 0; if (value is int) return value; final s = value.toString().toUpperCase(); switch (s) { case 'CHANNEL': case '渠道': return 1; case 'SERVICE_PROVIDER': case '代理商': return 2; case 'STORE': case '门店': return 3; case 'GROUP': case '设备分组': return 4; default: return int.tryParse(s) ?? 0; } } /// 解析组织状态,统一规整为 'ACTIVE' / 'DISABLED'。 /// /// 兼容后端两种形态:字符串(ACTIVE/DISABLED、启用/禁用)与整型 /// (与 [AppVersion.status] 一致:0=正常 1=禁用)。 static String _parseStatus(dynamic value) { if (value == null) return 'ACTIVE'; if (value is int) return value == 0 ? 'ACTIVE' : 'DISABLED'; final s = value.toString().toUpperCase(); if (s == 'DISABLED' || s == '禁用') return 'DISABLED'; return 'ACTIVE'; } factory OrgNode.fromJson(Map json) { final childrenRaw = json['list'] ?? json['children']; return OrgNode( // 对齐 Web 端 normalizeOrg:id 兼容 orgId / org_id,parentId 兼容 parent_id, // 避免后端偶发返回 orgId/parent_id 时节点 id 为空导致展开/编辑/删除失效。 id: json['id']?.toString() ?? json['orgId']?.toString() ?? json['org_id']?.toString() ?? '', name: json['name']?.toString() ?? '', type: _parseType(json['type']), parentId: json['parentId']?.toString() ?? json['parent_id']?.toString() ?? '', children: childrenRaw is List ? childrenRaw .whereType>() .map(OrgNode.fromJson) .toList() : const [], address: json['address']?.toString() ?? '', contactPhone: json['contactPhone']?.toString() ?? json['phone']?.toString() ?? '', activationCode: json['activationCode']?.toString() ?? '', createdAt: _parseDateTime( json['createdAt'] ?? json['createTime'] ?? json['created_time'] ?? json['created_at'], ), expireTime: _parseDateTime( json['expireTime'] ?? json['expire_time'] ?? json['expiredTime'], ), groupLimit: _parseInt( json['groupLimit'] ?? json['deviceLimit'] ?? json['device_limit'], ), province: json['province']?.toString() ?? '', city: json['city']?.toString() ?? '', themeId: json['themeId'] is int ? json['themeId'] as int : int.tryParse(json['themeId']?.toString() ?? ''), orderNo: _parseInt(json['orderNo'] ?? json['order_no']) ?? 0, status: _parseStatus(json['status']), mpath: json['mpath']?.toString() ?? json['orgCode']?.toString() ?? json['org_code']?.toString() ?? '', ); } Map toJson() { return { 'id': id, 'name': name, 'type': type, 'parentId': parentId, 'children': children.map((e) => e.toJson()).toList(), 'address': address, 'contactPhone': contactPhone, 'activationCode': activationCode, 'province': province, 'city': city, 'themeId': themeId, 'orderNo': orderNo, 'status': status, 'mpath': mpath, 'expireTime': expireTime?.toIso8601String(), }; } /// 不可变拷贝(主要用于渠道商管理页给节点挂载子节点树)。 OrgNode copyWith({ String? id, String? name, int? type, String? parentId, List? children, String? address, String? contactPhone, String? activationCode, String? province, String? city, int? themeId, DateTime? createdAt, DateTime? expireTime, int? groupLimit, int? orderNo, String? status, String? mpath, }) { return OrgNode( id: id ?? this.id, name: name ?? this.name, type: type ?? this.type, parentId: parentId ?? this.parentId, children: children ?? this.children, address: address ?? this.address, contactPhone: contactPhone ?? this.contactPhone, activationCode: activationCode ?? this.activationCode, province: province ?? this.province, city: city ?? this.city, themeId: themeId ?? this.themeId, createdAt: createdAt ?? this.createdAt, expireTime: expireTime ?? this.expireTime, groupLimit: groupLimit ?? this.groupLimit, orderNo: orderNo ?? this.orderNo, status: status ?? this.status, mpath: mpath ?? this.mpath, ); } /// 类型展示文案(渠道商管理页徽章用)。 String get typeLabel { switch (type) { case 1: return '渠道'; case 2: return '代理'; case 3: return '门店'; case 4: return '设备分组'; default: return '平台'; } } /// 是否已禁用。 bool get isDisabled => status.toUpperCase() == 'DISABLED'; /// 「组织编码」列展示值:优先 mpath,缺省时降级为节点 id。 String get orgCodeLabel => mpath.isNotEmpty ? mpath : id; }