Files
superport/lib/screens/company/widgets/company_branch_dialog.dart
JiWoong Sul 49b203d366
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
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).
2025-09-09 22:38:08 +09:00

472 lines
16 KiB
Dart

import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:superport/models/company_model.dart';
import 'package:superport/services/company_service.dart';
import 'package:superport/screens/common/theme_shadcn.dart';
import 'package:superport/screens/common/components/shadcn_components.dart';
import 'package:superport/core/utils/error_handler.dart';
/// 본사와 지점 관리를 위한 개선된 다이얼로그 위젯
/// 새로운 계층형 Company 구조 기반 (Clean Architecture)
class CompanyBranchDialog extends StatefulWidget {
final Company mainCompany;
const CompanyBranchDialog({super.key, required this.mainCompany});
@override
State<CompanyBranchDialog> createState() => _CompanyBranchDialogState();
}
class _CompanyBranchDialogState extends State<CompanyBranchDialog> {
late final CompanyService _companyService;
List<Company> _branches = [];
bool _isLoading = true;
String? _error;
@override
void initState() {
super.initState();
_companyService = GetIt.instance<CompanyService>();
_loadBranches();
}
/// 지점 목록 로드 (SRP - 데이터 로딩 단일 책임)
Future<void> _loadBranches() async {
try {
setState(() {
_isLoading = true;
_error = null;
});
// 전체 회사 목록에서 현재 본사의 지점들 필터링
final allCompanies = await ErrorHandler.handleApiCall(
() => _companyService.getCompanies(
page: 1,
perPage: 1000, // 충분히 큰 수로 전체 조회
),
onError: (failure) => throw failure,
);
if (allCompanies != null) {
// parentCompanyId가 현재 본사 ID인 항목들만 필터링
_branches = allCompanies.items
.where((company) => company.parentCompanyId == widget.mainCompany.id)
.toList();
}
} catch (e) {
if (mounted) {
setState(() {
_error = e.toString();
});
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
/// 지점 추가 화면 이동
void _addBranch() {
Navigator.pushNamed(
context,
'/company/branch/add',
arguments: {
'parentCompanyId': widget.mainCompany.id,
'parentCompanyName': widget.mainCompany.name,
},
).then((result) {
if (result == true) {
_loadBranches(); // 지점 목록 새로고침
}
});
}
/// 지점 수정 화면 이동
void _editBranch(Company branch) {
Navigator.pushNamed(
context,
'/company/branch/edit',
arguments: {
'companyId': branch.id,
'parentCompanyId': widget.mainCompany.id,
'parentCompanyName': widget.mainCompany.name,
},
).then((result) {
if (result == true) {
_loadBranches(); // 지점 목록 새로고침
}
});
}
/// 지점 삭제
Future<void> _deleteBranch(Company branch) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('지점 삭제'),
content: Text('${branch.name} 지점을 삭제하시겠습니까?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('취소'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('삭제'),
),
],
),
);
if (confirmed == true) {
try {
await ErrorHandler.handleApiCall(
() => _companyService.deleteCompany(branch.id!),
onError: (failure) => throw failure,
);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${branch.name} 지점이 삭제되었습니다.'),
backgroundColor: Colors.green,
),
);
_loadBranches(); // 지점 목록 새로고침
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('삭제 실패: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
}
/// 본사 정보 카드 구성
Widget _buildHeadquartersCard() {
return ShadcnCard(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
ShadcnBadge(
text: '본사',
variant: ShadcnBadgeVariant.companyHeadquarters,
size: ShadcnBadgeSize.small,
),
const SizedBox(width: 12),
Text(
widget.mainCompany.name,
style: ShadcnTheme.headingH5.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 8),
_buildCompanyInfo(widget.mainCompany),
],
),
),
);
}
/// 지점 정보 카드 구성
Widget _buildBranchCard(Company branch) {
return ShadcnCard(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
ShadcnBadge(
text: '지점',
variant: ShadcnBadgeVariant.companyBranch,
size: ShadcnBadgeSize.small,
),
const SizedBox(width: 12),
Text(
branch.name,
style: ShadcnTheme.headingH5.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
Row(
children: [
IconButton(
icon: const Icon(Icons.edit, size: 20),
onPressed: () => _editBranch(branch),
tooltip: '지점 수정',
),
IconButton(
icon: const Icon(Icons.delete, size: 20),
onPressed: () => _deleteBranch(branch),
tooltip: '지점 삭제',
),
],
),
],
),
const SizedBox(height: 8),
_buildCompanyInfo(branch),
],
),
),
);
}
/// 회사 정보 공통 구성 (SRP - 정보 표시 단일 책임)
Widget _buildCompanyInfo(Company company) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (company.address.toString().isNotEmpty) ...[
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.location_on_outlined,
size: 16,
color: ShadcnTheme.muted,
),
const SizedBox(width: 8),
Expanded(
child: Text(
company.address.toString(),
style: ShadcnTheme.bodySmall,
),
),
],
),
const SizedBox(height: 4),
],
if (company.contactName?.isNotEmpty == true) ...[
Row(
children: [
Icon(
Icons.person_outline,
size: 16,
color: ShadcnTheme.muted,
),
const SizedBox(width: 8),
Text(
company.contactName!,
style: ShadcnTheme.bodySmall,
),
if (company.contactPosition?.isNotEmpty == true) ...[
Text(
' (${company.contactPosition})',
style: ShadcnTheme.bodySmall.copyWith(
color: ShadcnTheme.muted,
),
),
],
],
),
const SizedBox(height: 4),
],
if (company.contactPhone?.isNotEmpty == true ||
company.contactEmail?.isNotEmpty == true) ...[
Row(
children: [
Icon(
Icons.contact_phone_outlined,
size: 16,
color: ShadcnTheme.muted,
),
const SizedBox(width: 8),
Text(
company.contactPhone ?? '',
style: ShadcnTheme.bodySmall,
),
if (company.contactEmail?.isNotEmpty == true) ...[
Text(
' | ${company.contactEmail}',
style: ShadcnTheme.bodySmall.copyWith(
color: ShadcnTheme.muted,
),
),
],
],
),
],
],
);
}
@override
Widget build(BuildContext context) {
final maxDialogHeight = MediaQuery.of(context).size.height * 0.8;
final maxDialogWidth = MediaQuery.of(context).size.width * 0.7;
return Dialog(
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: maxDialogHeight,
maxWidth: maxDialogWidth,
),
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 헤더
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'본사 및 지점 관리',
style: ShadcnTheme.headingH4.copyWith(
fontWeight: FontWeight.bold,
),
),
Row(
children: [
ElevatedButton.icon(
onPressed: _addBranch,
icon: const Icon(Icons.add, size: 16),
label: const Text('지점 추가'),
style: ElevatedButton.styleFrom(
backgroundColor: ShadcnTheme.primary,
foregroundColor: ShadcnTheme.primaryForeground,
minimumSize: const Size(100, 36),
),
),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
tooltip: '닫기',
),
],
),
],
),
const SizedBox(height: 24),
// 콘텐츠
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: _error != null
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 48,
color: ShadcnTheme.destructive,
),
const SizedBox(height: 16),
Text(
'데이터 로드 실패',
style: ShadcnTheme.headingH5,
),
const SizedBox(height: 8),
Text(
_error!,
style: ShadcnTheme.bodySmall.copyWith(
color: ShadcnTheme.muted,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _loadBranches,
child: const Text('다시 시도'),
),
],
),
)
: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 본사 정보
_buildHeadquartersCard(),
const SizedBox(height: 16),
// 지점 목록
if (_branches.isNotEmpty) ...[
Text(
'지점 목록 (${_branches.length}개)',
style: ShadcnTheme.headingH5.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
..._branches.map((branch) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: _buildBranchCard(branch),
)),
] else ...[
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: ShadcnTheme.muted.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: ShadcnTheme.border,
style: BorderStyle.solid,
),
),
child: Column(
children: [
Icon(
Icons.domain_outlined,
size: 48,
color: ShadcnTheme.muted,
),
const SizedBox(height: 12),
Text(
'등록된 지점이 없습니다',
style: ShadcnTheme.bodyMedium.copyWith(
color: ShadcnTheme.muted,
),
),
const SizedBox(height: 8),
Text(
'지점 추가 버튼을 클릭하여 첫 지점을 등록해보세요',
style: ShadcnTheme.bodySmall.copyWith(
color: ShadcnTheme.muted,
),
textAlign: TextAlign.center,
),
],
),
),
],
],
),
),
),
],
),
),
),
);
}
}