사용하지 않는 파일 정리 전 백업 (Phase 10 완료 후 상태)
This commit is contained in:
320
lib/screens/rent/controllers/rent_controller.dart
Normal file
320
lib/screens/rent/controllers/rent_controller.dart
Normal file
@@ -0,0 +1,320 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
import '../../../data/models/rent_dto.dart';
|
||||
import '../../../domain/usecases/rent_usecase.dart';
|
||||
|
||||
@injectable
|
||||
class RentController with ChangeNotifier {
|
||||
final RentUseCase _rentUseCase;
|
||||
|
||||
// 상태 관리 (단순화)
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
List<RentDto> _rents = []; // 단순한 List 구조
|
||||
RentDto? _selectedRent;
|
||||
|
||||
// 필터링 상태
|
||||
String? _selectedStatus;
|
||||
int? _selectedEquipmentHistoryId;
|
||||
|
||||
RentController(this._rentUseCase);
|
||||
|
||||
// Getters (단순화)
|
||||
bool get isLoading => _isLoading;
|
||||
String? get error => _error;
|
||||
bool get hasError => _error != null;
|
||||
RentDto? get selectedRent => _selectedRent;
|
||||
String? get selectedStatus => _selectedStatus;
|
||||
int? get selectedEquipmentHistoryId => _selectedEquipmentHistoryId;
|
||||
|
||||
// 편의 메서드 (백엔드 실제 구조)
|
||||
List<RentDto> get rents => _rents;
|
||||
int get totalRents => _rents.length;
|
||||
|
||||
// 페이징 관련 getter (UI 호환성)
|
||||
int get currentPage => 1; // 단순화된 페이징 구조
|
||||
int get totalPages => (_rents.length / 10).ceil();
|
||||
int get totalItems => _rents.length;
|
||||
|
||||
// Dashboard 관련 getter
|
||||
Map<String, int> get rentStats => getRentStats();
|
||||
List<RentDto> get activeRents => getActiveRents();
|
||||
List<RentDto> get overdueRents => getOverdueRents();
|
||||
|
||||
void _setLoading(bool loading) {
|
||||
_isLoading = loading;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _setError(String? error) {
|
||||
_error = error;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void clearError() {
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 임대 목록 조회
|
||||
Future<void> loadRents({
|
||||
int page = 1,
|
||||
int pageSize = 10,
|
||||
String? search,
|
||||
bool refresh = false,
|
||||
}) async {
|
||||
try {
|
||||
if (refresh) {
|
||||
_rents.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
_setLoading(true);
|
||||
|
||||
final response = await _rentUseCase.getRents(
|
||||
page: page,
|
||||
pageSize: pageSize,
|
||||
search: search,
|
||||
// status: _selectedStatus, // 삭제된 파라미터
|
||||
equipmentHistoryId: _selectedEquipmentHistoryId,
|
||||
);
|
||||
|
||||
// response를 List<RentDto>로 캐스팅
|
||||
_rents = response as List<RentDto>;
|
||||
|
||||
clearError();
|
||||
} catch (e) {
|
||||
_setError('임대 목록을 불러오는데 실패했습니다: $e');
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 임대 상세 조회
|
||||
Future<void> loadRent(int id) async {
|
||||
try {
|
||||
_setLoading(true);
|
||||
|
||||
final response = await _rentUseCase.getRent(id);
|
||||
_selectedRent = response; // response가 직접 RentDto
|
||||
|
||||
clearError();
|
||||
} catch (e) {
|
||||
_setError('임대 상세 정보를 불러오는데 실패했습니다: $e');
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 임대 생성 (백엔드 실제 스키마)
|
||||
Future<bool> createRent({
|
||||
required int equipmentHistoryId,
|
||||
required DateTime startedAt,
|
||||
required DateTime endedAt,
|
||||
}) async {
|
||||
try {
|
||||
_setLoading(true);
|
||||
|
||||
await _rentUseCase.createRent(
|
||||
RentRequestDto(
|
||||
equipmentHistoryId: equipmentHistoryId,
|
||||
startedAt: startedAt,
|
||||
endedAt: endedAt,
|
||||
),
|
||||
);
|
||||
|
||||
// 목록 새로고침
|
||||
await loadRents(refresh: true);
|
||||
|
||||
clearError();
|
||||
return true;
|
||||
} catch (e) {
|
||||
_setError('임대 생성에 실패했습니다: $e');
|
||||
return false;
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 임대 수정 (백엔드 실제 스키마)
|
||||
Future<bool> updateRent({
|
||||
required int id,
|
||||
DateTime? startedAt,
|
||||
DateTime? endedAt,
|
||||
}) async {
|
||||
try {
|
||||
_setLoading(true);
|
||||
|
||||
await _rentUseCase.updateRent(
|
||||
id,
|
||||
RentUpdateRequestDto(
|
||||
startedAt: startedAt,
|
||||
endedAt: endedAt,
|
||||
),
|
||||
);
|
||||
|
||||
// 목록 새로고침
|
||||
await loadRents(refresh: true);
|
||||
|
||||
clearError();
|
||||
return true;
|
||||
} catch (e) {
|
||||
_setError('임대 수정에 실패했습니다: $e');
|
||||
return false;
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 임대 삭제
|
||||
Future<bool> deleteRent(int id) async {
|
||||
try {
|
||||
_setLoading(true);
|
||||
|
||||
await _rentUseCase.deleteRent(id);
|
||||
|
||||
// 목록 새로고침
|
||||
await loadRents(refresh: true);
|
||||
|
||||
clearError();
|
||||
return true;
|
||||
} catch (e) {
|
||||
_setError('임대 삭제에 실패했습니다: $e');
|
||||
return false;
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// 진행 중인 임대 간단 통계 (백엔드 데이터 기반)
|
||||
List<RentDto> getActiveRents() {
|
||||
final now = DateTime.now();
|
||||
return _rents.where((rent) =>
|
||||
rent.startedAt.isBefore(now) && rent.endedAt.isAfter(now)
|
||||
).toList();
|
||||
}
|
||||
|
||||
// 연체된 임대 간단 통계 (백엔드 데이터 기반)
|
||||
List<RentDto> getOverdueRents() {
|
||||
final now = DateTime.now();
|
||||
return _rents.where((rent) => rent.endedAt.isBefore(now)).toList();
|
||||
}
|
||||
|
||||
// 간단한 통계 (백엔드 데이터 기반)
|
||||
Map<String, int> getRentStats() {
|
||||
final now = DateTime.now();
|
||||
return {
|
||||
'total': _rents.length,
|
||||
'active': _rents.where((r) => r.startedAt.isBefore(now) && r.endedAt.isAfter(now)).length,
|
||||
'overdue': _rents.where((r) => r.endedAt.isBefore(now)).length,
|
||||
'upcoming': _rents.where((r) => r.startedAt.isAfter(now)).length,
|
||||
};
|
||||
}
|
||||
|
||||
// 백엔드에서 반납/연장 처리는 endedAt 수정으로 처리
|
||||
Future<bool> updateRentEndDate(int id, DateTime newEndDate) async {
|
||||
return await updateRent(
|
||||
id: id,
|
||||
endedAt: newEndDate,
|
||||
);
|
||||
}
|
||||
|
||||
/// 상태 필터 설정
|
||||
void setStatusFilter(String? status) {
|
||||
_selectedStatus = status;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 장비 이력 필터 설정
|
||||
void setEquipmentHistoryFilter(int? equipmentHistoryId) {
|
||||
_selectedEquipmentHistoryId = equipmentHistoryId;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 필터 초기화
|
||||
void clearFilters() {
|
||||
_selectedStatus = null;
|
||||
_selectedEquipmentHistoryId = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 선택된 임대 초기화
|
||||
void clearSelectedRent() {
|
||||
_selectedRent = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 간단한 기간 계산 (클라이언트 사이드)
|
||||
int calculateRentDays(DateTime startDate, DateTime endDate) {
|
||||
return endDate.difference(startDate).inDays;
|
||||
}
|
||||
|
||||
// 임대 상태 간단 판단
|
||||
String getRentStatus(RentDto rent) {
|
||||
final now = DateTime.now();
|
||||
if (rent.startedAt.isAfter(now)) return '예약';
|
||||
if (rent.endedAt.isBefore(now)) return '종료';
|
||||
return '진행중';
|
||||
}
|
||||
|
||||
// UI 호환성을 위한 상태 표시명
|
||||
String getRentStatusDisplayName(String status) {
|
||||
switch (status.toLowerCase()) {
|
||||
case '예약':
|
||||
case 'reserved':
|
||||
return '예약';
|
||||
case '진행중':
|
||||
case 'active':
|
||||
return '진행중';
|
||||
case '종료':
|
||||
case 'completed':
|
||||
return '종료';
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
/// 새로고침
|
||||
Future<void> refresh() async {
|
||||
await loadRents(refresh: true);
|
||||
}
|
||||
|
||||
/// 임대 반납 처리 (endedAt를 현재 시간으로 수정)
|
||||
Future<bool> returnRent(int id) async {
|
||||
return await updateRent(
|
||||
id: id,
|
||||
endedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 임대 총 비용 계산 (문자열 날짜 기반)
|
||||
double calculateTotalRent(String startDate, String endDate, double dailyRate) {
|
||||
try {
|
||||
final start = DateTime.parse(startDate);
|
||||
final end = DateTime.parse(endDate);
|
||||
final days = end.difference(start).inDays;
|
||||
return days * dailyRate;
|
||||
} catch (e) {
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Dashboard용 통계 로드 (기존 데이터 기반)
|
||||
Future<void> loadRentStats() async {
|
||||
// 현재 로드된 데이터 기반으로 통계 계산 (별도 API 호출 없음)
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Dashboard용 활성 임대 로드 (기존 데이터 기반)
|
||||
Future<void> loadActiveRents() async {
|
||||
// 현재 로드된 데이터 기반으로 활성 임대 필터링 (별도 API 호출 없음)
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Dashboard용 연체 임대 로드 (기존 데이터 기반)
|
||||
Future<void> loadOverdueRents() async {
|
||||
// 현재 로드된 데이터 기반으로 연체 임대 필터링 (별도 API 호출 없음)
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
253
lib/screens/rent/rent_dashboard.dart
Normal file
253
lib/screens/rent/rent_dashboard.dart
Normal file
@@ -0,0 +1,253 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../common/theme_shadcn.dart';
|
||||
import '../../injection_container.dart';
|
||||
import '../common/widgets/standard_states.dart';
|
||||
import 'controllers/rent_controller.dart';
|
||||
|
||||
class RentDashboard extends StatefulWidget {
|
||||
const RentDashboard({super.key});
|
||||
|
||||
@override
|
||||
State<RentDashboard> createState() => _RentDashboardState();
|
||||
}
|
||||
|
||||
class _RentDashboardState extends State<RentDashboard> {
|
||||
late final RentController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = getIt<RentController>();
|
||||
_loadData();
|
||||
}
|
||||
|
||||
Future<void> _loadData() async {
|
||||
await Future.wait([
|
||||
_controller.loadRentStats(),
|
||||
_controller.loadActiveRents(),
|
||||
_controller.loadOverdueRents(),
|
||||
]);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: ShadcnTheme.background,
|
||||
body: ChangeNotifierProvider.value(
|
||||
value: _controller,
|
||||
child: Consumer<RentController>(
|
||||
builder: (context, controller, child) {
|
||||
if (controller.isLoading) {
|
||||
return const StandardLoadingState(message: '임대 정보를 불러오는 중...');
|
||||
}
|
||||
|
||||
if (controller.hasError) {
|
||||
return StandardErrorState(
|
||||
message: controller.error!,
|
||||
onRetry: _loadData,
|
||||
);
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 제목
|
||||
Text('임대 현황', style: ShadcnTheme.headingH3),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 통계 카드
|
||||
_buildStatsCards(controller.rentStats ?? {}),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// 진행 중인 임대
|
||||
Text('진행 중인 임대', style: ShadcnTheme.headingH4),
|
||||
const SizedBox(height: 16),
|
||||
_buildActiveRentsList(controller.activeRents),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// 연체된 임대
|
||||
if (controller.overdueRents.isNotEmpty) ...[
|
||||
Text('연체된 임대', style: ShadcnTheme.headingH4),
|
||||
const SizedBox(height: 16),
|
||||
_buildOverdueRentsList(controller.overdueRents),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatsCards(Map<String, dynamic> stats) {
|
||||
return GridView.count(
|
||||
crossAxisCount: 4,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisSpacing: 16,
|
||||
childAspectRatio: 1.5,
|
||||
children: [
|
||||
_buildStatCard(
|
||||
title: '전체 임대',
|
||||
value: stats['total_rents']?.toString() ?? '0',
|
||||
icon: Icons.receipt_long,
|
||||
color: Colors.blue,
|
||||
),
|
||||
_buildStatCard(
|
||||
title: '진행 중',
|
||||
value: stats['active_rents']?.toString() ?? '0',
|
||||
icon: Icons.play_circle_filled,
|
||||
color: Colors.green,
|
||||
),
|
||||
_buildStatCard(
|
||||
title: '연체',
|
||||
value: stats['overdue_rents']?.toString() ?? '0',
|
||||
icon: Icons.warning,
|
||||
color: Colors.red,
|
||||
),
|
||||
_buildStatCard(
|
||||
title: '월 수익',
|
||||
value: '₩${_formatCurrency(stats['monthly_revenue'])}',
|
||||
icon: Icons.attach_money,
|
||||
color: Colors.orange,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatCard({
|
||||
required String title,
|
||||
required String value,
|
||||
required IconData icon,
|
||||
required Color color,
|
||||
}) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, size: 32, color: color),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
value,
|
||||
style: ShadcnTheme.headingH4.copyWith(color: color),
|
||||
),
|
||||
Text(
|
||||
title,
|
||||
style: ShadcnTheme.bodyMedium.copyWith(color: ShadcnTheme.foregroundMuted),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActiveRentsList(List rents) {
|
||||
if (rents.isEmpty) {
|
||||
return const Card(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(24),
|
||||
child: Center(
|
||||
child: Text('진행 중인 임대가 없습니다'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Card(
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: rents.length > 5 ? 5 : rents.length, // 최대 5개만 표시
|
||||
separatorBuilder: (context, index) => const Divider(),
|
||||
itemBuilder: (context, index) {
|
||||
final rent = rents[index];
|
||||
final startDate = rent.startedAt?.toString().substring(0, 10) ?? 'Unknown';
|
||||
final endDate = rent.endedAt?.toString().substring(0, 10) ?? 'Unknown';
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Colors.blue,
|
||||
child: const Icon(Icons.calendar_today, color: Colors.white),
|
||||
),
|
||||
title: Text('임대 ID: ${rent.id ?? 'N/A'}'),
|
||||
subtitle: Text('$startDate ~ $endDate'),
|
||||
trailing: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.successLight,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
'진행중',
|
||||
style: TextStyle(color: ShadcnTheme.success, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOverdueRentsList(List rents) {
|
||||
return Card(
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: rents.length > 5 ? 5 : rents.length, // 최대 5개만 표시
|
||||
separatorBuilder: (context, index) => const Divider(),
|
||||
itemBuilder: (context, index) {
|
||||
final rent = rents[index];
|
||||
final endDate = rent.endedAt;
|
||||
final overdueDays = endDate != null
|
||||
? DateTime.now().difference(endDate).inDays
|
||||
: 0;
|
||||
final endDateStr = endDate?.toString().substring(0, 10) ?? 'Unknown';
|
||||
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Colors.red,
|
||||
child: const Icon(Icons.warning, color: Colors.white),
|
||||
),
|
||||
title: Text('임대 ID: ${rent.id ?? 'N/A'}'),
|
||||
subtitle: Text('연체 ${overdueDays}일'),
|
||||
trailing: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.errorLight,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
'연체',
|
||||
style: TextStyle(color: ShadcnTheme.error, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'종료일: $endDateStr',
|
||||
style: const TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatCurrency(dynamic amount) {
|
||||
if (amount == null) return '0';
|
||||
final num = amount is String ? double.tryParse(amount) ?? 0 : amount.toDouble();
|
||||
return num.toStringAsFixed(0);
|
||||
}
|
||||
}
|
||||
284
lib/screens/rent/rent_form_dialog.dart
Normal file
284
lib/screens/rent/rent_form_dialog.dart
Normal file
@@ -0,0 +1,284 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../../data/models/rent_dto.dart';
|
||||
|
||||
class RentFormDialog extends StatefulWidget {
|
||||
final RentDto? rent;
|
||||
final Future<bool> Function(RentRequestDto) onSubmit;
|
||||
|
||||
const RentFormDialog({
|
||||
super.key,
|
||||
this.rent,
|
||||
required this.onSubmit,
|
||||
});
|
||||
|
||||
@override
|
||||
State<RentFormDialog> createState() => _RentFormDialogState();
|
||||
}
|
||||
|
||||
class _RentFormDialogState extends State<RentFormDialog> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
// 백엔드 스키마에 맞는 필드만 유지
|
||||
int? _selectedEquipmentHistoryId;
|
||||
DateTime _startDate = DateTime.now();
|
||||
DateTime? _endDate;
|
||||
|
||||
bool _isLoading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.rent != null) {
|
||||
_initializeForm(widget.rent!);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _initializeForm(RentDto rent) {
|
||||
_selectedEquipmentHistoryId = rent.equipmentHistoryId;
|
||||
_startDate = rent.startedAt;
|
||||
_endDate = rent.endedAt;
|
||||
}
|
||||
|
||||
Future<void> _selectStartDate() async {
|
||||
final date = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _startDate,
|
||||
firstDate: DateTime(2020),
|
||||
lastDate: DateTime(2030),
|
||||
);
|
||||
|
||||
if (date != null) {
|
||||
setState(() {
|
||||
_startDate = date;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _selectEndDate() async {
|
||||
final date = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _endDate ?? DateTime.now().add(const Duration(days: 30)),
|
||||
firstDate: _startDate,
|
||||
lastDate: DateTime(2030),
|
||||
);
|
||||
|
||||
if (date != null) {
|
||||
setState(() {
|
||||
_endDate = date;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
if (_selectedEquipmentHistoryId == null || _endDate == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('장비 이력과 종료일을 선택해주세요')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
try {
|
||||
final request = RentRequestDto(
|
||||
equipmentHistoryId: _selectedEquipmentHistoryId!,
|
||||
startedAt: _startDate,
|
||||
endedAt: _endDate!,
|
||||
);
|
||||
|
||||
await widget.onSubmit(request);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('오류가 발생했습니다: $e')),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
child: Container(
|
||||
width: 500,
|
||||
constraints: const BoxConstraints(maxHeight: 600),
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 제목
|
||||
Text(
|
||||
widget.rent != null ? '임대 계약 수정' : '새 임대 계약',
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 폼 (백엔드 스키마 3개 필드만 포함)
|
||||
Expanded(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 장비 이력 ID (백엔드 필수 필드)
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: '장비 이력 ID *',
|
||||
border: OutlineInputBorder(),
|
||||
helperText: '임대할 장비의 이력 ID를 입력하세요',
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '장비 이력 ID는 필수입니다';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onChanged: (value) {
|
||||
_selectedEquipmentHistoryId = int.tryParse(value);
|
||||
},
|
||||
initialValue: _selectedEquipmentHistoryId?.toString(),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 임대 기간 (백엔드 필수 필드)
|
||||
Text('임대 기간', style: Theme.of(context).textTheme.titleMedium),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
onTap: _selectStartDate,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.calendar_today),
|
||||
const SizedBox(width: 8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('시작일 *', style: TextStyle(fontSize: 12, color: Colors.grey)),
|
||||
Text(
|
||||
'${_startDate.year}-${_startDate.month.toString().padLeft(2, '0')}-${_startDate.day.toString().padLeft(2, '0')}',
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
onTap: _selectEndDate,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.calendar_today),
|
||||
const SizedBox(width: 8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('종료일 *', style: TextStyle(fontSize: 12, color: Colors.grey)),
|
||||
Text(
|
||||
_endDate != null
|
||||
? '${_endDate!.year}-${_endDate!.month.toString().padLeft(2, '0')}-${_endDate!.day.toString().padLeft(2, '0')}'
|
||||
: '날짜 선택',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: _endDate != null ? Colors.black : Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 임대 기간 표시
|
||||
if (_endDate != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.blue.shade200),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.info, color: Colors.blue),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'임대 기간: ${_endDate!.difference(_startDate).inDays + 1}일',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 버튼
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: _isLoading ? null : () => Navigator.of(context).pop(),
|
||||
child: const Text('취소'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: _isLoading ? null : _submit,
|
||||
child: _isLoading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: Text(widget.rent != null ? '수정' : '생성'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
363
lib/screens/rent/rent_list_screen.dart
Normal file
363
lib/screens/rent/rent_list_screen.dart
Normal file
@@ -0,0 +1,363 @@
|
||||
import 'package:flutter/material.dart' hide DataColumn; // Flutter DataColumn 숨기기
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
// import '../../core/theme/app_theme.dart'; // 존재하지 않는 파일 - 주석 처리
|
||||
import '../../data/models/rent_dto.dart';
|
||||
import '../../injection_container.dart';
|
||||
import '../common/widgets/standard_data_table.dart'; // StandardDataTable의 DataColumn 사용
|
||||
import '../common/widgets/standard_action_bar.dart';
|
||||
import '../common/widgets/standard_states.dart';
|
||||
import '../common/widgets/pagination.dart';
|
||||
import 'controllers/rent_controller.dart';
|
||||
import 'rent_form_dialog.dart';
|
||||
|
||||
class RentListScreen extends StatefulWidget {
|
||||
const RentListScreen({super.key});
|
||||
|
||||
@override
|
||||
State<RentListScreen> createState() => _RentListScreenState();
|
||||
}
|
||||
|
||||
class _RentListScreenState extends State<RentListScreen> {
|
||||
late final RentController _controller;
|
||||
final _searchController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = getIt<RentController>();
|
||||
_loadData();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadData() async {
|
||||
await _controller.loadRents();
|
||||
}
|
||||
|
||||
Future<void> _refresh() async {
|
||||
await _controller.loadRents(refresh: true);
|
||||
}
|
||||
|
||||
void _showCreateDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => RentFormDialog(
|
||||
onSubmit: (request) async {
|
||||
final success = await _controller.createRent(
|
||||
equipmentHistoryId: request.equipmentHistoryId,
|
||||
startedAt: request.startedAt,
|
||||
endedAt: request.endedAt,
|
||||
);
|
||||
if (success && mounted) {
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('임대 계약이 생성되었습니다')),
|
||||
);
|
||||
}
|
||||
return success;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showEditDialog(RentDto rent) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => RentFormDialog(
|
||||
rent: rent,
|
||||
onSubmit: (request) async {
|
||||
final success = await _controller.updateRent(
|
||||
id: rent.id!,
|
||||
startedAt: request.startedAt,
|
||||
endedAt: request.endedAt,
|
||||
);
|
||||
if (success && mounted) {
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('임대 계약이 수정되었습니다')),
|
||||
);
|
||||
}
|
||||
return success;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _deleteRent(RentDto rent) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('임대 계약 삭제'),
|
||||
content: Text('ID ${rent.id}번 임대 계약을 삭제하시겠습니까?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('취소'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||
child: const Text('삭제'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed == true) {
|
||||
final success = await _controller.deleteRent(rent.id!);
|
||||
if (success && mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('임대 계약이 삭제되었습니다')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _returnRent(RentDto rent) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('장비 반납'),
|
||||
content: Text('ID ${rent.id}번 임대 계약의 장비를 반납 처리하시겠습니까?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('취소'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: const Text('반납 처리'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed == true) {
|
||||
final success = await _controller.returnRent(rent.id!);
|
||||
if (success && mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('장비가 반납 처리되었습니다')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void _onStatusFilter(String? status) {
|
||||
_controller.setStatusFilter(status);
|
||||
_controller.loadRents();
|
||||
}
|
||||
|
||||
List<DataColumn> _buildColumns() {
|
||||
return [
|
||||
DataColumn(label: 'ID'),
|
||||
DataColumn(label: '장비 이력 ID'),
|
||||
DataColumn(label: '시작일'),
|
||||
DataColumn(label: '종료일'),
|
||||
DataColumn(label: '기간 (일)'),
|
||||
DataColumn(label: '상태'),
|
||||
DataColumn(label: '작업'),
|
||||
];
|
||||
}
|
||||
|
||||
StandardDataRow _buildRow(RentDto rent, int index) {
|
||||
final days = _controller.calculateRentDays(rent.startedAt, rent.endedAt);
|
||||
final status = _controller.getRentStatus(rent);
|
||||
|
||||
return StandardDataRow(
|
||||
index: index,
|
||||
columns: _buildColumns(),
|
||||
cells: [
|
||||
Text(rent.id?.toString() ?? '-'),
|
||||
Text(rent.equipmentHistoryId.toString()),
|
||||
Text('${rent.startedAt.year}-${rent.startedAt.month.toString().padLeft(2, '0')}-${rent.startedAt.day.toString().padLeft(2, '0')}'),
|
||||
Text('${rent.endedAt.year}-${rent.endedAt.month.toString().padLeft(2, '0')}-${rent.endedAt.day.toString().padLeft(2, '0')}'),
|
||||
Text('${days}일'),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: _getStatusColor(status),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
status,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 12),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit, size: 18),
|
||||
onPressed: () => _showEditDialog(rent),
|
||||
tooltip: '수정',
|
||||
),
|
||||
if (status == '진행중')
|
||||
IconButton(
|
||||
icon: const Icon(Icons.assignment_return, size: 18),
|
||||
onPressed: () => _returnRent(rent),
|
||||
tooltip: '반납 처리',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete, size: 18, color: Colors.red),
|
||||
onPressed: () => _deleteRent(rent),
|
||||
tooltip: '삭제',
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Color _getStatusColor(String? status) {
|
||||
switch (status) {
|
||||
case '진행중':
|
||||
return Colors.blue;
|
||||
case '종료':
|
||||
return Colors.green;
|
||||
case '예약':
|
||||
return Colors.orange;
|
||||
default:
|
||||
return Colors.grey;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildDataTableSection(RentController controller) {
|
||||
// 로딩 상태
|
||||
if (controller.isLoading) {
|
||||
return const StandardLoadingState();
|
||||
}
|
||||
|
||||
// 에러 상태
|
||||
if (controller.error != null) {
|
||||
return StandardErrorState(
|
||||
message: controller.error!,
|
||||
onRetry: _refresh,
|
||||
);
|
||||
}
|
||||
|
||||
// 데이터가 없는 경우
|
||||
if (controller.rents.isEmpty) {
|
||||
return const StandardEmptyState(
|
||||
message: '임대 계약이 없습니다',
|
||||
);
|
||||
}
|
||||
|
||||
// 데이터 테이블
|
||||
return StandardDataTable(
|
||||
columns: _buildColumns(),
|
||||
rows: controller.rents
|
||||
.asMap()
|
||||
.entries
|
||||
.map((entry) => _buildRow(entry.value, entry.key))
|
||||
.toList(),
|
||||
emptyWidget: const StandardEmptyState(
|
||||
message: '임대 계약이 없습니다',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[50], // AppTheme 대신 직접 색상 지정
|
||||
body: ChangeNotifierProvider.value(
|
||||
value: _controller,
|
||||
child: Consumer<RentController>(
|
||||
builder: (context, controller, child) {
|
||||
return Column(
|
||||
children: [
|
||||
// 액션 바
|
||||
StandardActionBar(
|
||||
totalCount: _controller.totalRents,
|
||||
onRefresh: _refresh,
|
||||
rightActions: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: _showCreateDialog,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('새 임대 계약'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 버튼 및 필터 섹션 (수동 구성)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
// 상태 필터
|
||||
SizedBox(
|
||||
width: 120,
|
||||
child: DropdownButtonFormField<String?>(
|
||||
value: controller.selectedStatus,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '상태',
|
||||
border: OutlineInputBorder(),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
),
|
||||
items: [
|
||||
const DropdownMenuItem<String?>(
|
||||
value: null,
|
||||
child: Text('전체'),
|
||||
),
|
||||
const DropdownMenuItem<String>(
|
||||
value: 'active',
|
||||
child: Text('진행 중'),
|
||||
),
|
||||
const DropdownMenuItem<String>(
|
||||
value: 'overdue',
|
||||
child: Text('연체'),
|
||||
),
|
||||
const DropdownMenuItem<String>(
|
||||
value: 'completed',
|
||||
child: Text('완료'),
|
||||
),
|
||||
const DropdownMenuItem<String>(
|
||||
value: 'cancelled',
|
||||
child: Text('취소'),
|
||||
),
|
||||
],
|
||||
onChanged: _onStatusFilter,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _showCreateDialog,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('새 임대'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 데이터 테이블
|
||||
Expanded(
|
||||
child: _buildDataTableSection(controller),
|
||||
),
|
||||
|
||||
// 페이지네이션
|
||||
if (controller.totalPages > 1)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Pagination(
|
||||
totalCount: controller.totalRents,
|
||||
currentPage: controller.currentPage,
|
||||
pageSize: 20,
|
||||
onPageChanged: (page) => controller.loadRents(page: page),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
193
lib/screens/rent/rent_list_screen_simple.dart
Normal file
193
lib/screens/rent/rent_list_screen_simple.dart
Normal file
@@ -0,0 +1,193 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../injection_container.dart';
|
||||
import 'controllers/rent_controller.dart';
|
||||
import 'rent_form_dialog.dart';
|
||||
|
||||
class RentListScreen extends StatefulWidget {
|
||||
const RentListScreen({super.key});
|
||||
|
||||
@override
|
||||
State<RentListScreen> createState() => _RentListScreenState();
|
||||
}
|
||||
|
||||
class _RentListScreenState extends State<RentListScreen> {
|
||||
late final RentController _controller;
|
||||
final _searchController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = getIt<RentController>();
|
||||
_loadData();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadData() async {
|
||||
await _controller.loadRents();
|
||||
}
|
||||
|
||||
Future<void> _refresh() async {
|
||||
await _controller.loadRents(refresh: true);
|
||||
}
|
||||
|
||||
void _showCreateDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => RentFormDialog(
|
||||
onSubmit: (request) async {
|
||||
final success = await _controller.createRent(
|
||||
equipmentHistoryId: request.equipmentHistoryId,
|
||||
startedAt: request.startedAt,
|
||||
endedAt: request.endedAt,
|
||||
);
|
||||
if (success && mounted) {
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('임대 계약이 생성되었습니다')),
|
||||
);
|
||||
}
|
||||
return success;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onSearch(String query) {
|
||||
_controller.loadRents(search: query.isEmpty ? null : query);
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: ChangeNotifierProvider.value(
|
||||
value: _controller,
|
||||
child: Consumer<RentController>(
|
||||
builder: (context, controller, child) {
|
||||
return Column(
|
||||
children: [
|
||||
// 헤더
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'임대 관리',
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
const Spacer(),
|
||||
// 검색 필드
|
||||
SizedBox(
|
||||
width: 300,
|
||||
child: TextFormField(
|
||||
controller: _searchController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '검색...',
|
||||
prefixIcon: Icon(Icons.search),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onFieldSubmitted: _onSearch,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// 새로고침 버튼
|
||||
IconButton(
|
||||
onPressed: _refresh,
|
||||
icon: const Icon(Icons.refresh),
|
||||
tooltip: '새로고침',
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _showCreateDialog,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('새 임대'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 콘텐츠
|
||||
Expanded(
|
||||
child: controller.isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: controller.hasError
|
||||
? Center(child: Text('오류: ${controller.error}'))
|
||||
: controller.rents.isEmpty
|
||||
? const Center(child: Text('임대 계약이 없습니다'))
|
||||
: ListView.builder(
|
||||
itemCount: controller.rents.length,
|
||||
itemBuilder: (context, index) {
|
||||
final rent = controller.rents[index];
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
child: Text(
|
||||
'${rent.id ?? 0}',
|
||||
),
|
||||
),
|
||||
title: Text('임대 #${rent.id ?? 0}'),
|
||||
subtitle: Text(
|
||||
'${_formatDate(rent.startedAt)} ~ ${_formatDate(rent.endedAt)}',
|
||||
),
|
||||
trailing: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'${_calculateDays(rent.startedAt, rent.endedAt)}일',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
controller.getRentStatusDisplayName(controller.getRentStatus(rent)),
|
||||
style: TextStyle(
|
||||
color: _getStatusColor(controller.getRentStatus(rent)),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color _getStatusColor(String status) {
|
||||
switch (status) {
|
||||
case '진행중':
|
||||
return Colors.blue;
|
||||
case '종료':
|
||||
return Colors.green;
|
||||
case '예약':
|
||||
return Colors.orange;
|
||||
default:
|
||||
return Colors.grey;
|
||||
}
|
||||
}
|
||||
|
||||
String _formatDate(DateTime date) {
|
||||
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
int _calculateDays(DateTime startDate, DateTime endDate) {
|
||||
return endDate.difference(startDate).inDays;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user