Files
superport/lib/screens/model/model_list_screen.dart
JiWoong Sul 655d473413
Some checks failed
Flutter Test & Quality Check / Test on macos-latest (push) Has been cancelled
Flutter Test & Quality Check / Test on ubuntu-latest (push) Has been cancelled
Flutter Test & Quality Check / Build APK (push) Has been cancelled
web: migrate health notifications to js_interop; add browser hook
- Replace dart:js with package:js in health_check_service_web.dart\n- Implement showHealthCheckNotification in web/index.html\n- Pin js dependency to ^0.6.7 for flutter_secure_storage_web compatibility

auth: harden AuthInterceptor + tests

- Allow overrideAuthRepository injection for testing\n- Normalize imports to package: paths\n- Add unit test covering token attach, 401→refresh→retry, and failure path\n- Add integration test skeleton gated by env vars

ui/data: map User.companyName to list column

- Add companyName to domain User\n- Map UserDto.company?.name\n- Render companyName in user_list

cleanup: remove legacy equipment table + unused code; minor warnings

- Remove _buildFlexibleTable and unused helpers\n- Remove unused zipcode details and cache retry constant\n- Fix null-aware and non-null assertions\n- Address child-last warnings in administrator dialog

docs: update AGENTS.md session context
2025-09-08 17:39:00 +09:00

438 lines
13 KiB
Dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:superport/data/models/model/model_dto.dart';
import 'package:superport/screens/model/controllers/model_controller.dart';
import 'package:superport/screens/model/model_form_dialog.dart';
import 'package:superport/screens/common/layouts/base_list_screen.dart';
import 'package:superport/screens/common/widgets/standard_action_bar.dart';
import 'package:superport/screens/common/widgets/pagination.dart';
import 'package:superport/screens/common/theme_shadcn.dart';
import 'package:superport/injection_container.dart' as di;
class ModelListScreen extends StatefulWidget {
const ModelListScreen({super.key});
@override
State<ModelListScreen> createState() => _ModelListScreenState();
}
class _ModelListScreenState extends State<ModelListScreen> {
late final ModelController _controller;
// 클라이언트 사이드 페이지네이션
int _currentPage = 1;
static const int _pageSize = 10;
@override
void initState() {
super.initState();
_controller = di.sl<ModelController>();
WidgetsBinding.instance.addPostFrameCallback((_) {
_controller.loadInitialData();
});
}
// 현재 페이지의 모델 목록 반환
List<ModelDto> _getCurrentPageModels() {
final allModels = _controller.models;
final startIndex = (_currentPage - 1) * _pageSize;
final endIndex = startIndex + _pageSize;
if (startIndex >= allModels.length) return [];
if (endIndex >= allModels.length) return allModels.sublist(startIndex);
return allModels.sublist(startIndex, endIndex);
}
// 총 페이지 수 계산
int _getTotalPages() {
return (_controller.models.length / _pageSize).ceil();
}
// 페이지 이동
void _goToPage(int page) {
final totalPages = _getTotalPages();
if (page < 1 || page > totalPages) return;
setState(() {
_currentPage = page;
});
}
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider.value(
value: _controller,
child: Scaffold(
backgroundColor: ShadcnTheme.background,
appBar: AppBar(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'모델 관리',
style: ShadcnTheme.headingH4,
),
Text(
'장비 모델 정보를 관리합니다',
style: ShadcnTheme.bodySmall,
),
],
),
backgroundColor: ShadcnTheme.background,
elevation: 0,
),
body: Consumer<ModelController>(
builder: (context, controller, child) {
return BaseListScreen(
headerSection: _buildStatisticsCards(controller),
searchBar: _buildSearchBar(controller),
actionBar: _buildActionBar(),
dataTable: _buildDataTable(controller),
pagination: _buildPagination(controller),
isLoading: controller.isLoading,
error: controller.errorMessage,
onRefresh: () => controller.loadInitialData(),
);
},
),
),
);
}
Widget _buildStatisticsCards(ModelController controller) {
if (controller.isLoading) return const SizedBox();
return Row(
children: [
_buildStatCard(
'전체 모델',
controller.models.length.toString(),
Icons.category,
ShadcnTheme.companyCustomer,
),
const SizedBox(width: ShadcnTheme.spacing4),
_buildStatCard(
'제조사',
controller.vendors.length.toString(),
Icons.business,
ShadcnTheme.companyPartner,
),
const SizedBox(width: ShadcnTheme.spacing4),
_buildStatCard(
'활성 모델',
controller.models.where((m) => !m.isDeleted).length.toString(),
Icons.check_circle,
ShadcnTheme.equipmentIn,
),
],
);
}
Widget _buildSearchBar(ModelController controller) {
return Row(
children: [
Expanded(
flex: 2,
child: ShadInput(
placeholder: const Text('모델명 검색...'),
onChanged: (value) {
setState(() {
_currentPage = 1; // 검색 시 첫 페이지로 리셋
});
controller.setSearchQuery(value);
},
),
),
const SizedBox(width: ShadcnTheme.spacing4),
Expanded(
child: ShadSelect<int?>(
placeholder: const Text('제조사 선택'),
options: [
const ShadOption<int?>(
value: null,
child: Text('전체'),
),
...controller.vendors.map(
(vendor) => ShadOption<int?>(
value: vendor.id,
child: Text(vendor.name),
),
),
],
selectedOptionBuilder: (context, value) {
if (value == null) {
return const Text('전체');
}
try {
final vendor = controller.vendors.firstWhere((v) => v.id == value);
return Text(vendor.name);
} catch (_) {
return const Text('전체');
}
},
onChanged: (value) {
setState(() {
_currentPage = 1; // 필터 변경 시 첫 페이지로 리셋
});
controller.setVendorFilter(value);
},
),
),
],
);
}
Widget _buildActionBar() {
return StandardActionBar(
totalCount: _controller.totalCount,
leftActions: const [
Text('모델 목록', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
],
rightActions: [
ShadButton.outline(
onPressed: () => _controller.refreshModels(),
child: const Icon(Icons.refresh, size: 16),
),
const SizedBox(width: ShadcnTheme.spacing2),
ShadButton(
onPressed: _showCreateDialog,
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.add, size: 16),
SizedBox(width: ShadcnTheme.spacing1),
Text('새 모델 등록'),
],
),
),
],
);
}
Widget _buildDataTable(ModelController controller) {
final allModels = controller.models;
final currentPageModels = _getCurrentPageModels();
if (allModels.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.category_outlined,
size: 64,
color: ShadcnTheme.mutedForeground,
),
const SizedBox(height: ShadcnTheme.spacing4),
Text(
'등록된 모델이 없습니다',
style: ShadcnTheme.bodyMedium.copyWith(
color: ShadcnTheme.mutedForeground,
),
),
],
),
);
}
return Container(
decoration: BoxDecoration(
border: Border.all(color: ShadcnTheme.border),
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: ShadTable.list(
header: const [
ShadTableCell.header(child: Text('ID')),
ShadTableCell.header(child: Text('제조사')),
ShadTableCell.header(child: Text('모델명')),
ShadTableCell.header(child: Text('등록일')),
ShadTableCell.header(child: Text('상태')),
ShadTableCell.header(child: Text('작업')),
],
children: currentPageModels.map((model) {
final vendor = _controller.getVendorById(model.vendorsId);
return [
ShadTableCell(child: Text(model.id.toString(), style: ShadcnTheme.bodySmall)),
ShadTableCell(child: Text(vendor?.name ?? '알 수 없음', overflow: TextOverflow.ellipsis)),
ShadTableCell(child: Text(model.name, overflow: TextOverflow.ellipsis, style: ShadcnTheme.bodyMedium.copyWith(fontWeight: FontWeight.w500))),
ShadTableCell(child: Text(model.registeredAt != null ? DateFormat('yyyy-MM-dd').format(model.registeredAt) : '-', style: ShadcnTheme.bodySmall)),
ShadTableCell(child: _buildStatusChip(model.isDeleted)),
ShadTableCell(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
ShadButton.ghost(
onPressed: () => _showEditDialog(model),
child: const Icon(Icons.edit, size: 16),
),
const SizedBox(width: ShadcnTheme.spacing1),
ShadButton.ghost(
onPressed: () => _showDeleteConfirmDialog(model),
child: Icon(Icons.delete, size: 16, color: ShadcnTheme.destructive),
),
],
),
),
];
}).toList(),
),
),
);
}
Widget _buildPagination(ModelController controller) {
final totalCount = controller.models.length;
final totalPages = _getTotalPages();
if (totalCount <= _pageSize) {
return Container(
padding: const EdgeInsets.all(ShadcnTheme.spacing3),
decoration: BoxDecoration(
color: ShadcnTheme.card,
border: Border(
top: BorderSide(color: ShadcnTheme.border),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'$totalCount개 모델',
style: ShadcnTheme.bodySmall,
),
],
),
);
}
return Container(
padding: const EdgeInsets.all(ShadcnTheme.spacing3),
decoration: BoxDecoration(
color: ShadcnTheme.card,
border: Border(
top: BorderSide(color: ShadcnTheme.border),
),
),
child: Pagination(
totalCount: totalCount,
currentPage: _currentPage,
pageSize: _pageSize,
onPageChanged: _goToPage,
),
);
}
Widget _buildStatCard(
String title,
String value,
IconData icon,
Color color,
) {
return Expanded(
child: ShadCard(
child: Padding(
padding: const EdgeInsets.all(ShadcnTheme.spacing4),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(ShadcnTheme.spacing3),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(ShadcnTheme.radiusLg),
),
child: Icon(
icon,
color: color,
size: 20,
),
),
const SizedBox(width: ShadcnTheme.spacing3),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: ShadcnTheme.bodySmall,
),
Text(
value,
style: ShadcnTheme.headingH6.copyWith(
color: color,
),
),
],
),
),
],
),
),
),
);
}
Widget _buildStatusChip(bool isDeleted) {
if (isDeleted) {
return ShadBadge(
backgroundColor: ShadcnTheme.equipmentDisposal.withValues(alpha: 0.1),
foregroundColor: ShadcnTheme.equipmentDisposal,
child: const Text('비활성'),
);
} else {
return ShadBadge(
backgroundColor: ShadcnTheme.equipmentIn.withValues(alpha: 0.1),
foregroundColor: ShadcnTheme.equipmentIn,
child: const Text('활성'),
);
}
}
void _showCreateDialog() {
showDialog(
context: context,
builder: (context) => ModelFormDialog(
controller: _controller,
),
);
}
void _showEditDialog(ModelDto model) {
showDialog(
context: context,
builder: (context) => ModelFormDialog(
controller: _controller,
model: model,
),
);
}
void _showDeleteConfirmDialog(ModelDto model) {
showDialog(
context: context,
builder: (context) => ShadDialog(
title: const Text('모델 삭제'),
description: Text('${model.name} 모델을 삭제하시겠습니까?'),
actions: [
ShadButton.outline(
onPressed: () => Navigator.of(context).pop(),
child: const Text('취소'),
),
ShadButton.destructive(
onPressed: () async {
Navigator.of(context).pop();
await _controller.deleteModel(model.id);
},
child: const Text('삭제'),
),
],
),
);
}
}