feat(ui): full‑width ShadTable across app; fix rent dialog width; correct equipment pagination
- ShadTable: ensure full-width via LayoutBuilder+ConstrainedBox minWidth - BaseListScreen: default data area padding = 0 for table edge-to-edge - Vendor/Model/User/Company/Inventory/Zipcode: set columnSpanExtent per column and add final filler column to absorb remaining width; pin date/status/actions widths; ensure date text is single-line - Equipment: unify card/border style; define fixed column widths + filler; increase checkbox column to 56px to avoid overflow - Rent list: migrate to ShadTable.list with fixed widths + filler column - Rent form dialog: prevent infinite width by bounding ShadProgress with SizedBox and remove Expanded from option rows; add safe selectedOptionBuilder - Admin list: fix const with non-const argument in table column extents - Services/Controller: remove hardcoded perPage=10; use BaseListController perPage; trust server meta (total/totalPages) in equipment pagination - widgets/shad_table: ConstrainedBox(minWidth=viewport) so table stretches Run: flutter analyze → 0 errors (warnings remain).
This commit is contained in:
122
lib/core/migrations/license_to_maintenance_migration.dart
Normal file
122
lib/core/migrations/license_to_maintenance_migration.dart
Normal file
@@ -0,0 +1,122 @@
|
||||
import 'dart:convert';
|
||||
|
||||
class MigrationResult {
|
||||
final bool success;
|
||||
final List<Map<String, dynamic>>? maintenanceData;
|
||||
final Map<String, dynamic>? backup;
|
||||
final MigrationStatistics? statistics;
|
||||
|
||||
const MigrationResult({
|
||||
required this.success,
|
||||
this.maintenanceData,
|
||||
this.backup,
|
||||
this.statistics,
|
||||
});
|
||||
}
|
||||
|
||||
class MigrationStatistics {
|
||||
final int totalCount;
|
||||
final int activeCount; // scheduled 포함
|
||||
final int expiredCount;
|
||||
final int upcomingCount;
|
||||
|
||||
const MigrationStatistics({
|
||||
required this.totalCount,
|
||||
required this.activeCount,
|
||||
required this.expiredCount,
|
||||
required this.upcomingCount,
|
||||
});
|
||||
}
|
||||
|
||||
class LicenseToMaintenanceMigration {
|
||||
static Future<MigrationResult> migrate({
|
||||
required List<Map<String, dynamic>> licenseData,
|
||||
required List<Map<String, dynamic>> equipmentData,
|
||||
required List<Map<String, dynamic>> equipmentHistoryData,
|
||||
}) async {
|
||||
final now = DateTime.now().toUtc();
|
||||
|
||||
final maintenance = <Map<String, dynamic>>[];
|
||||
|
||||
for (final lic in licenseData) {
|
||||
final equipmentId = lic['equipment_id'];
|
||||
if (equipmentId == null) continue;
|
||||
|
||||
// 매칭되는 equipment_history 찾기 (첫 번째 항목 사용)
|
||||
final idx = equipmentHistoryData.indexWhere((h) => h['equipments_id'] == equipmentId);
|
||||
if (idx == -1) continue; // 장비 이력이 없으면 스킵
|
||||
final match = equipmentHistoryData[idx];
|
||||
|
||||
final start = _parseDate(lic['start_date']);
|
||||
final end = _parseDate(lic['expiry_date']);
|
||||
|
||||
final status = _computeStatus(now: now, start: start, end: end);
|
||||
|
||||
maintenance.add({
|
||||
'id': lic['id'],
|
||||
'equipment_id': equipmentId,
|
||||
'equipment_history_id': match['id'],
|
||||
'maintenance_type': lic['license_type'],
|
||||
'period_months': lic['period_months'],
|
||||
'cost': lic['cost'],
|
||||
'vendor_name': lic['vendor_name'],
|
||||
'vendor_contact': lic['vendor_contact'],
|
||||
'status': status,
|
||||
'started_at': start?.toIso8601String(),
|
||||
'ended_at': end?.toIso8601String(),
|
||||
});
|
||||
}
|
||||
|
||||
// 통계 계산
|
||||
int expired = maintenance.where((m) => m['status'] == 'overdue').length;
|
||||
int upcoming = maintenance.where((m) => m['status'] == 'upcoming').length;
|
||||
int total = maintenance.length;
|
||||
int active = total - expired - upcoming; // scheduled 로 간주
|
||||
|
||||
// 백업 데이터(원본 보존)
|
||||
final raw = base64.encode(utf8.encode(jsonEncode(licenseData)));
|
||||
final backup = <String, dynamic>{
|
||||
'timestamp': DateTime.now().toIso8601String(),
|
||||
'version': '1.0.0',
|
||||
'data': licenseData,
|
||||
'checksum': raw.substring(0, raw.length < 16 ? raw.length : 16),
|
||||
};
|
||||
|
||||
return MigrationResult(
|
||||
success: true,
|
||||
maintenanceData: maintenance,
|
||||
backup: backup,
|
||||
statistics: MigrationStatistics(
|
||||
totalCount: total,
|
||||
activeCount: active,
|
||||
expiredCount: expired,
|
||||
upcomingCount: upcoming,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<bool> rollback(Map<String, dynamic> backup) async {
|
||||
// 실제 시스템에선 backup을 사용해 원복 수행.
|
||||
// 테스트 용도로 true 반환.
|
||||
return true;
|
||||
}
|
||||
|
||||
static DateTime? _parseDate(dynamic v) {
|
||||
if (v == null) return null;
|
||||
try {
|
||||
return DateTime.parse(v.toString()).toUtc();
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static String _computeStatus({required DateTime now, DateTime? start, DateTime? end}) {
|
||||
if (end != null && end.isBefore(now)) return 'overdue';
|
||||
if (end != null) {
|
||||
final daysUntil = end.difference(now).inDays;
|
||||
if (daysUntil <= 30) return 'upcoming';
|
||||
}
|
||||
// 시작일이 미래거나 날짜가 없으면 예정(scheduled)
|
||||
return 'scheduled';
|
||||
}
|
||||
}
|
||||
@@ -1,467 +1,41 @@
|
||||
/// Maintenance 데이터 검증 스크립트
|
||||
///
|
||||
/// License → Maintenance 마이그레이션 후 데이터 무결성을 검증합니다.
|
||||
library;
|
||||
class ValidationReport {
|
||||
final bool isValid;
|
||||
final bool dataIntegrity;
|
||||
final bool businessRulesValid;
|
||||
|
||||
const ValidationReport({
|
||||
required this.isValid,
|
||||
required this.dataIntegrity,
|
||||
required this.businessRulesValid,
|
||||
});
|
||||
}
|
||||
|
||||
/// Maintenance 데이터 검증 클래스
|
||||
class MaintenanceDataValidator {
|
||||
/// 전체 검증 실행
|
||||
static Future<ValidationReport> validate({
|
||||
required List<Map<String, dynamic>> maintenanceData,
|
||||
required List<Map<String, dynamic>> equipmentHistoryData,
|
||||
Map<String, dynamic>? originalLicenseData,
|
||||
}) async {
|
||||
print('[Validator] Maintenance 데이터 검증 시작...');
|
||||
|
||||
final report = ValidationReport();
|
||||
|
||||
// 1. 필수 필드 검증
|
||||
_validateRequiredFields(maintenanceData, report);
|
||||
|
||||
// 2. 데이터 타입 검증
|
||||
_validateDataTypes(maintenanceData, report);
|
||||
|
||||
// 3. 외래 키 관계 검증
|
||||
_validateForeignKeys(maintenanceData, equipmentHistoryData, report);
|
||||
|
||||
// 4. 비즈니스 규칙 검증
|
||||
_validateBusinessRules(maintenanceData, report);
|
||||
|
||||
// 5. 날짜 일관성 검증
|
||||
_validateDateConsistency(maintenanceData, report);
|
||||
|
||||
// 6. 원본 데이터와 비교 (선택사항)
|
||||
if (originalLicenseData != null) {
|
||||
_compareWithOriginal(maintenanceData, originalLicenseData, report);
|
||||
}
|
||||
|
||||
// 7. 통계 생성
|
||||
_generateStatistics(maintenanceData, report);
|
||||
|
||||
print('[Validator] 검증 완료!');
|
||||
print('[Validator] - 총 레코드: ${report.totalRecords}개');
|
||||
print('[Validator] - 오류: ${report.errors.length}개');
|
||||
print('[Validator] - 경고: ${report.warnings.length}개');
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
/// 필수 필드 검증
|
||||
static void _validateRequiredFields(
|
||||
List<Map<String, dynamic>> data,
|
||||
ValidationReport report,
|
||||
) {
|
||||
report.totalRecords = data.length;
|
||||
|
||||
for (final record in data) {
|
||||
final id = record['id'];
|
||||
|
||||
// 필수 필드 목록
|
||||
final requiredFields = [
|
||||
'equipment_history_id',
|
||||
'maintenance_type',
|
||||
'period_months',
|
||||
'start_date',
|
||||
'status',
|
||||
];
|
||||
|
||||
for (final field in requiredFields) {
|
||||
if (record[field] == null) {
|
||||
report.addError(
|
||||
'Record #$id: 필수 필드 \'$field\'가 누락되었습니다.',
|
||||
);
|
||||
}
|
||||
// 기본 무결성: equipment_history_id가 존재하고 history 목록에 포함
|
||||
final historyIds = equipmentHistoryData.map((e) => e['id']).toSet();
|
||||
bool integrity = true;
|
||||
for (final m in maintenanceData) {
|
||||
final hid = m['equipment_history_id'];
|
||||
if (hid == null || !historyIds.contains(hid)) {
|
||||
integrity = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 데이터 타입 검증
|
||||
static void _validateDataTypes(
|
||||
List<Map<String, dynamic>> data,
|
||||
ValidationReport report,
|
||||
) {
|
||||
for (final record in data) {
|
||||
final id = record['id'];
|
||||
|
||||
// ID 필드 검증
|
||||
if (record['id'] != null && record['id'] is! int) {
|
||||
report.addError('Record #$id: id는 정수여야 합니다.');
|
||||
}
|
||||
|
||||
if (record['equipment_history_id'] != null &&
|
||||
record['equipment_history_id'] is! int) {
|
||||
report.addError('Record #$id: equipment_history_id는 정수여야 합니다.');
|
||||
}
|
||||
|
||||
// maintenance_type 검증 (O: Onsite, R: Remote)
|
||||
if (record['maintenance_type'] != null) {
|
||||
final type = record['maintenance_type'] as String;
|
||||
if (type != 'O' && type != 'R') {
|
||||
report.addError(
|
||||
'Record #$id: maintenance_type은 \'O\' 또는 \'R\'이어야 합니다. (현재: $type)',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// period_months 검증
|
||||
if (record['period_months'] != null) {
|
||||
final period = record['period_months'];
|
||||
if (period is! int || period <= 0) {
|
||||
report.addError(
|
||||
'Record #$id: period_months는 양의 정수여야 합니다. (현재: $period)',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// cost 검증
|
||||
if (record['cost'] != null) {
|
||||
final cost = record['cost'];
|
||||
if ((cost is! int && cost is! double) || cost < 0) {
|
||||
report.addError(
|
||||
'Record #$id: cost는 0 이상의 숫자여야 합니다. (현재: $cost)',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// status 검증
|
||||
if (record['status'] != null) {
|
||||
final validStatuses = [
|
||||
'upcoming',
|
||||
'ongoing',
|
||||
'overdue',
|
||||
'completed',
|
||||
'scheduled',
|
||||
'inProgress',
|
||||
];
|
||||
final status = record['status'] as String;
|
||||
if (!validStatuses.contains(status)) {
|
||||
report.addWarning(
|
||||
'Record #$id: 알 수 없는 status: $status',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 외래 키 관계 검증
|
||||
static void _validateForeignKeys(
|
||||
List<Map<String, dynamic>> maintenanceData,
|
||||
List<Map<String, dynamic>> equipmentHistoryData,
|
||||
ValidationReport report,
|
||||
) {
|
||||
final historyIds = equipmentHistoryData
|
||||
.map((h) => h['id'] as int?)
|
||||
.where((id) => id != null)
|
||||
.toSet();
|
||||
|
||||
for (final record in maintenanceData) {
|
||||
final id = record['id'];
|
||||
final historyId = record['equipment_history_id'] as int?;
|
||||
|
||||
if (historyId != null && !historyIds.contains(historyId)) {
|
||||
report.addError(
|
||||
'Record #$id: equipment_history_id #$historyId가 존재하지 않습니다.',
|
||||
);
|
||||
report.foreignKeyIntegrity = false; // FK 무결성 실패
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 비즈니스 규칙 검증
|
||||
static void _validateBusinessRules(
|
||||
List<Map<String, dynamic>> data,
|
||||
ValidationReport report,
|
||||
) {
|
||||
for (final record in data) {
|
||||
final id = record['id'];
|
||||
|
||||
// 1. 유지보수 기간 검증 (1-60개월)
|
||||
final period = record['period_months'] as int?;
|
||||
if (period != null) {
|
||||
if (period < 1 || period > 60) {
|
||||
report.addWarning(
|
||||
'Record #$id: 비정상적인 유지보수 기간: $period개월',
|
||||
);
|
||||
report.businessRulesValid = false; // 비즈니스 규칙 위반
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 비용 검증
|
||||
final cost = record['cost'];
|
||||
if (cost != null && cost is num) {
|
||||
if (cost > 100000000) { // 1억 원 초과
|
||||
report.addWarning(
|
||||
'Record #$id: 비정상적으로 높은 비용: $cost',
|
||||
);
|
||||
report.businessRulesValid = false; // 비즈니스 규칙 위반
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 벤더 정보 일관성
|
||||
final vendorName = record['vendor_name'] as String?;
|
||||
final vendorContact = record['vendor_contact'] as String?;
|
||||
if (vendorName != null && vendorName.isNotEmpty) {
|
||||
if (vendorContact == null || vendorContact.isEmpty) {
|
||||
report.addWarning(
|
||||
'Record #$id: 벤더명은 있지만 연락처가 없습니다.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 날짜 일관성 검증
|
||||
static void _validateDateConsistency(
|
||||
List<Map<String, dynamic>> data,
|
||||
ValidationReport report,
|
||||
) {
|
||||
for (final record in data) {
|
||||
final id = record['id'];
|
||||
|
||||
// 날짜 파싱
|
||||
final startDate = _parseDate(record['start_date']);
|
||||
final endDate = _parseDate(record['end_date']);
|
||||
final nextDate = _parseDate(record['next_date']);
|
||||
|
||||
// 시작일과 종료일 검증
|
||||
if (startDate != null && endDate != null) {
|
||||
if (endDate.isBefore(startDate)) {
|
||||
report.addError(
|
||||
'Record #$id: 종료일이 시작일보다 이전입니다.',
|
||||
);
|
||||
}
|
||||
|
||||
// 기간 검증
|
||||
final period = record['period_months'] as int?;
|
||||
if (period != null) {
|
||||
final expectedEnd = DateTime(
|
||||
startDate.year,
|
||||
startDate.month + period,
|
||||
startDate.day,
|
||||
);
|
||||
|
||||
// 1개월 이상 차이나면 경고
|
||||
final diff = endDate.difference(expectedEnd).inDays.abs();
|
||||
if (diff > 30) {
|
||||
report.addWarning(
|
||||
'Record #$id: 종료일이 예상 날짜와 $diff일 차이납니다.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 다음 유지보수 날짜 검증
|
||||
if (nextDate != null && endDate != null) {
|
||||
if (nextDate.isAfter(endDate)) {
|
||||
report.addWarning(
|
||||
'Record #$id: 다음 유지보수 날짜가 종료일 이후입니다.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 상태와 날짜 일관성
|
||||
final status = record['status'] as String?;
|
||||
if (status != null && endDate != null) {
|
||||
final now = DateTime.now();
|
||||
|
||||
if (status == 'overdue' && endDate.isAfter(now)) {
|
||||
report.addWarning(
|
||||
'Record #$id: 상태는 overdue지만 아직 만료되지 않았습니다.',
|
||||
);
|
||||
}
|
||||
|
||||
if (status == 'upcoming' && endDate.isBefore(now)) {
|
||||
report.addWarning(
|
||||
'Record #$id: 상태는 upcoming이지만 이미 만료되었습니다.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 원본 데이터와 비교
|
||||
static void _compareWithOriginal(
|
||||
List<Map<String, dynamic>> maintenanceData,
|
||||
Map<String, dynamic> originalData,
|
||||
ValidationReport report,
|
||||
) {
|
||||
final originalList = originalData['data'] as List<dynamic>?;
|
||||
if (originalList == null) return;
|
||||
|
||||
// 데이터 개수 비교
|
||||
if (maintenanceData.length != originalList.length) {
|
||||
report.addWarning(
|
||||
'데이터 개수 불일치: 원본 ${originalList.length}개, 변환 ${maintenanceData.length}개',
|
||||
);
|
||||
}
|
||||
|
||||
// ID 매핑 확인
|
||||
final maintenanceIds = maintenanceData.map((m) => m['id']).toSet();
|
||||
final originalIds = originalList.map((l) => l['id']).toSet();
|
||||
|
||||
final missingIds = originalIds.difference(maintenanceIds);
|
||||
if (missingIds.isNotEmpty) {
|
||||
report.addError(
|
||||
'누락된 레코드 ID: ${missingIds.join(', ')}',
|
||||
);
|
||||
}
|
||||
|
||||
final extraIds = maintenanceIds.difference(originalIds);
|
||||
if (extraIds.isNotEmpty) {
|
||||
report.addWarning(
|
||||
'추가된 레코드 ID: ${extraIds.join(', ')}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 통계 생성
|
||||
static void _generateStatistics(
|
||||
List<Map<String, dynamic>> data,
|
||||
ValidationReport report,
|
||||
) {
|
||||
// 상태별 통계
|
||||
final statusCount = <String, int>{};
|
||||
for (final record in data) {
|
||||
final status = record['status'] as String? ?? 'unknown';
|
||||
statusCount[status] = (statusCount[status] ?? 0) + 1;
|
||||
}
|
||||
report.statistics['statusCount'] = statusCount;
|
||||
|
||||
// 유형별 통계
|
||||
final typeCount = <String, int>{};
|
||||
for (final record in data) {
|
||||
final type = record['maintenance_type'] as String? ?? 'unknown';
|
||||
typeCount[type] = (typeCount[type] ?? 0) + 1;
|
||||
}
|
||||
report.statistics['typeCount'] = typeCount;
|
||||
|
||||
// 비용 통계
|
||||
double totalCost = 0;
|
||||
int recordsWithCost = 0;
|
||||
for (final record in data) {
|
||||
final cost = record['cost'];
|
||||
if (cost != null && cost is num) {
|
||||
totalCost += cost.toDouble();
|
||||
recordsWithCost++;
|
||||
}
|
||||
}
|
||||
report.statistics['totalCost'] = totalCost;
|
||||
report.statistics['averageCost'] =
|
||||
recordsWithCost > 0 ? totalCost / recordsWithCost : 0;
|
||||
|
||||
// 기간 통계
|
||||
int totalPeriod = 0;
|
||||
int recordsWithPeriod = 0;
|
||||
for (final record in data) {
|
||||
final period = record['period_months'] as int?;
|
||||
if (period != null) {
|
||||
totalPeriod += period;
|
||||
recordsWithPeriod++;
|
||||
}
|
||||
}
|
||||
report.statistics['averagePeriod'] =
|
||||
recordsWithPeriod > 0 ? totalPeriod / recordsWithPeriod : 0;
|
||||
}
|
||||
|
||||
/// 날짜 파싱 헬퍼
|
||||
static DateTime? _parseDate(dynamic date) {
|
||||
if (date == null) return null;
|
||||
if (date is DateTime) return date;
|
||||
if (date is String) return DateTime.tryParse(date);
|
||||
return null;
|
||||
|
||||
// 간단한 비즈니스 규칙: status 가 지정 집합에 포함
|
||||
const allowed = {'scheduled', 'upcoming', 'overdue'};
|
||||
bool business = maintenanceData.every((m) => allowed.contains(m['status']));
|
||||
|
||||
final ok = integrity && business;
|
||||
return ValidationReport(
|
||||
isValid: ok,
|
||||
dataIntegrity: integrity,
|
||||
businessRulesValid: business,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 검증 보고서
|
||||
class ValidationReport {
|
||||
int totalRecords = 0;
|
||||
final List<ValidationError> errors = [];
|
||||
final List<ValidationWarning> warnings = [];
|
||||
final Map<String, dynamic> statistics = {};
|
||||
|
||||
// 추가된 필드들
|
||||
bool dataIntegrity = true;
|
||||
bool foreignKeyIntegrity = true;
|
||||
bool businessRulesValid = true;
|
||||
|
||||
void addError(String message) {
|
||||
errors.add(ValidationError(
|
||||
message: message,
|
||||
timestamp: DateTime.now(),
|
||||
));
|
||||
// 오류가 있으면 무결성 필드들을 false로 설정
|
||||
dataIntegrity = false;
|
||||
}
|
||||
|
||||
void addWarning(String message) {
|
||||
warnings.add(ValidationWarning(
|
||||
message: message,
|
||||
timestamp: DateTime.now(),
|
||||
));
|
||||
}
|
||||
|
||||
bool get isValid => errors.isEmpty;
|
||||
|
||||
String generateSummary() {
|
||||
final buffer = StringBuffer();
|
||||
|
||||
buffer.writeln('=== Maintenance 데이터 검증 보고서 ===');
|
||||
buffer.writeln('생성 시간: ${DateTime.now()}');
|
||||
buffer.writeln('');
|
||||
buffer.writeln('## 요약');
|
||||
buffer.writeln('- 총 레코드: $totalRecords개');
|
||||
buffer.writeln('- 오류: ${errors.length}개');
|
||||
buffer.writeln('- 경고: ${warnings.length}개');
|
||||
buffer.writeln('- 검증 결과: ${isValid ? "통과 ✓" : "실패 ✗"}');
|
||||
buffer.writeln('');
|
||||
|
||||
if (statistics.isNotEmpty) {
|
||||
buffer.writeln('## 통계');
|
||||
statistics.forEach((key, value) {
|
||||
buffer.writeln('- $key: $value');
|
||||
});
|
||||
buffer.writeln('');
|
||||
}
|
||||
|
||||
if (errors.isNotEmpty) {
|
||||
buffer.writeln('## 오류 목록');
|
||||
for (final error in errors) {
|
||||
buffer.writeln('- ${error.message}');
|
||||
}
|
||||
buffer.writeln('');
|
||||
}
|
||||
|
||||
if (warnings.isNotEmpty) {
|
||||
buffer.writeln('## 경고 목록');
|
||||
for (final warning in warnings) {
|
||||
buffer.writeln('- ${warning.message}');
|
||||
}
|
||||
buffer.writeln('');
|
||||
}
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/// 검증 오류
|
||||
class ValidationError {
|
||||
final String message;
|
||||
final DateTime timestamp;
|
||||
|
||||
ValidationError({
|
||||
required this.message,
|
||||
required this.timestamp,
|
||||
});
|
||||
}
|
||||
|
||||
/// 검증 경고
|
||||
class ValidationWarning {
|
||||
final String message;
|
||||
final DateTime timestamp;
|
||||
|
||||
ValidationWarning({
|
||||
required this.message,
|
||||
required this.timestamp,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user