프로젝트 최초 커밋
This commit is contained in:
398
lib/screens/company/company_form.dart
Normal file
398
lib/screens/company/company_form.dart
Normal file
@@ -0,0 +1,398 @@
|
||||
/// 회사 등록 및 수정 화면
|
||||
///
|
||||
/// SRP(단일 책임 원칙)에 따라 컴포넌트를 분리하여 구현한 리팩토링 버전
|
||||
/// - 컨트롤러: CompanyFormController - 비즈니스 로직 담당
|
||||
/// - 위젯:
|
||||
/// - CompanyFormHeader: 회사명 및 주소 입력
|
||||
/// - ContactInfoForm: 담당자 정보 입력
|
||||
/// - BranchCard: 지점 정보 카드
|
||||
/// - CompanyNameAutocomplete: 회사명 자동완성
|
||||
/// - MapDialog: 지도 다이얼로그
|
||||
/// - DuplicateCompanyDialog: 중복 회사 확인 다이얼로그
|
||||
/// - CompanyTypeSelector: 회사 유형 선택 라디오 버튼
|
||||
/// - 유틸리티:
|
||||
/// - PhoneUtils: 전화번호 관련 유틸리티
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:superport/models/address_model.dart';
|
||||
import 'package:superport/models/company_model.dart';
|
||||
import 'package:superport/screens/common/custom_widgets.dart';
|
||||
import 'package:superport/screens/common/theme_tailwind.dart';
|
||||
import 'package:superport/screens/company/controllers/company_form_controller.dart';
|
||||
import 'package:superport/screens/company/widgets/branch_card.dart';
|
||||
import 'package:superport/screens/company/widgets/company_form_header.dart';
|
||||
import 'package:superport/screens/company/widgets/contact_info_form.dart';
|
||||
import 'package:superport/screens/company/widgets/duplicate_company_dialog.dart';
|
||||
import 'package:superport/screens/company/widgets/map_dialog.dart';
|
||||
import 'package:superport/screens/company/widgets/branch_form_widget.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
import 'package:superport/screens/company/controllers/branch_form_controller.dart';
|
||||
|
||||
/// 회사 유형 선택 위젯 (체크박스)
|
||||
class CompanyTypeSelector extends StatelessWidget {
|
||||
final List<CompanyType> selectedTypes;
|
||||
final Function(CompanyType, bool) onTypeChanged;
|
||||
|
||||
const CompanyTypeSelector({
|
||||
Key? key,
|
||||
required this.selectedTypes,
|
||||
required this.onTypeChanged,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('회사 유형', style: AppThemeTailwind.formLabelStyle),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
// 고객사 체크박스
|
||||
Checkbox(
|
||||
value: selectedTypes.contains(CompanyType.customer),
|
||||
onChanged: (checked) {
|
||||
onTypeChanged(CompanyType.customer, checked ?? false);
|
||||
},
|
||||
),
|
||||
const Text('고객사'),
|
||||
const SizedBox(width: 24),
|
||||
// 파트너사 체크박스
|
||||
Checkbox(
|
||||
value: selectedTypes.contains(CompanyType.partner),
|
||||
onChanged: (checked) {
|
||||
onTypeChanged(CompanyType.partner, checked ?? false);
|
||||
},
|
||||
),
|
||||
const Text('파트너사'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CompanyFormScreen extends StatefulWidget {
|
||||
final Map? args;
|
||||
const CompanyFormScreen({Key? key, this.args}) : super(key: key);
|
||||
|
||||
@override
|
||||
_CompanyFormScreenState createState() => _CompanyFormScreenState();
|
||||
}
|
||||
|
||||
class _CompanyFormScreenState extends State<CompanyFormScreen> {
|
||||
late CompanyFormController _controller;
|
||||
bool isBranch = false;
|
||||
String? mainCompanyName;
|
||||
int? companyId;
|
||||
int? branchId;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// controller는 didChangeDependencies에서 초기화
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
final args = widget.args;
|
||||
if (args != null) {
|
||||
isBranch = args['isBranch'] ?? false;
|
||||
mainCompanyName = args['mainCompanyName'];
|
||||
companyId = args['companyId'];
|
||||
branchId = args['branchId'];
|
||||
}
|
||||
_controller = CompanyFormController(
|
||||
dataService: MockDataService(),
|
||||
companyId: companyId,
|
||||
);
|
||||
// 지점 수정 모드일 때 branchId로 branch 정보 세팅
|
||||
if (isBranch && branchId != null) {
|
||||
final company = MockDataService().getCompanyById(companyId!);
|
||||
// 디버그: 진입 시 companyId, branchId, company, branches 정보 출력
|
||||
print('[DEBUG] 지점 수정 진입: companyId=$companyId, branchId=$branchId');
|
||||
if (company != null && company.branches != null) {
|
||||
print(
|
||||
'[DEBUG] 불러온 company.name=${company.name}, branches=${company.branches!.map((b) => 'id:${b.id}, name:${b.name}, remark:${b.remark}').toList()}',
|
||||
);
|
||||
final branch = company.branches!.firstWhere(
|
||||
(b) => b.id == branchId,
|
||||
orElse: () => company.branches!.first,
|
||||
);
|
||||
print(
|
||||
'[DEBUG] 선택된 branch: id=${branch.id}, name=${branch.name}, remark=${branch.remark}',
|
||||
);
|
||||
// 폼 컨트롤러의 각 필드에 branch 정보 세팅
|
||||
_controller.nameController.text = branch.name;
|
||||
_controller.companyAddress = branch.address;
|
||||
_controller.contactNameController.text = branch.contactName ?? '';
|
||||
_controller.contactPositionController.text =
|
||||
branch.contactPosition ?? '';
|
||||
_controller.selectedPhonePrefix = extractPhonePrefix(
|
||||
branch.contactPhone ?? '',
|
||||
_controller.phonePrefixes,
|
||||
);
|
||||
_controller
|
||||
.contactPhoneController
|
||||
.text = extractPhoneNumberWithoutPrefix(
|
||||
branch.contactPhone ?? '',
|
||||
_controller.phonePrefixes,
|
||||
);
|
||||
_controller.contactEmailController.text = branch.contactEmail ?? '';
|
||||
// 지점 단일 입력만 허용 (branchControllers 초기화)
|
||||
_controller.branchControllers.clear();
|
||||
_controller.branchControllers.add(
|
||||
BranchFormController(
|
||||
branch: branch,
|
||||
positions: _controller.positions,
|
||||
phonePrefixes: _controller.phonePrefixes,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// 지점 추가 후 스크롤 처리 (branchControllers 기반)
|
||||
void _scrollToAddedBranchCard() {
|
||||
if (_controller.branchControllers.isEmpty ||
|
||||
!_controller.scrollController.hasClients) {
|
||||
return;
|
||||
}
|
||||
// 추가 버튼 위치까지 스크롤 - 지점 추가 버튼이 있는 위치를 계산하여 그 위치로 스크롤
|
||||
final double additionalOffset = 80.0;
|
||||
final maxPos = _controller.scrollController.position.maxScrollExtent;
|
||||
final currentPos = _controller.scrollController.position.pixels;
|
||||
final targetPos = math.min(currentPos + additionalOffset, maxPos - 20.0);
|
||||
_controller.scrollController.animateTo(
|
||||
targetPos,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOutQuad,
|
||||
);
|
||||
}
|
||||
|
||||
// 지점 추가
|
||||
void _addBranch() {
|
||||
setState(() {
|
||||
_controller.addBranch();
|
||||
});
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
_scrollToAddedBranchCard();
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
// 마지막 지점의 포커스 노드로 포커스 이동
|
||||
if (_controller.branchControllers.isNotEmpty) {
|
||||
_controller.branchControllers.last.focusNode.requestFocus();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 회사 저장
|
||||
void _saveCompany() {
|
||||
final duplicateCompany = _controller.checkDuplicateCompany();
|
||||
if (duplicateCompany != null) {
|
||||
DuplicateCompanyDialog.show(context, duplicateCompany);
|
||||
return;
|
||||
}
|
||||
if (_controller.saveCompany()) {
|
||||
Navigator.pop(context, true);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isEditMode = companyId != null;
|
||||
final String title =
|
||||
isBranch
|
||||
? '${mainCompanyName ?? ''} 지점 정보 수정'
|
||||
: (isEditMode ? '회사 정보 수정' : '회사 등록');
|
||||
final String nameLabel = isBranch ? '지점명' : '회사명';
|
||||
final String nameHint = isBranch ? '지점명을 입력하세요' : '회사명을 입력하세요';
|
||||
|
||||
// 지점 수정 모드일 때는 BranchFormWidget만 단독 노출
|
||||
if (isBranch && branchId != null) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(title)),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Form(
|
||||
key: _controller.formKey,
|
||||
child: BranchFormWidget(
|
||||
controller: _controller.branchControllers[0],
|
||||
index: 0,
|
||||
onRemove: null,
|
||||
onAddressChanged: (address) {
|
||||
setState(() {
|
||||
_controller.updateBranchAddress(0, address);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// ... 기존 본사/신규 등록 모드 렌더링
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
if (_controller.showCompanyNameDropdown) {
|
||||
_controller.showCompanyNameDropdown = false;
|
||||
}
|
||||
});
|
||||
FocusScope.of(context).unfocus();
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: AppBar(title: Text(title)),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Form(
|
||||
key: _controller.formKey,
|
||||
child: SingleChildScrollView(
|
||||
controller: _controller.scrollController,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 회사 유형 선택 (체크박스)
|
||||
CompanyTypeSelector(
|
||||
selectedTypes: _controller.selectedCompanyTypes,
|
||||
onTypeChanged: (type, checked) {
|
||||
setState(() {
|
||||
_controller.toggleCompanyType(type, checked);
|
||||
});
|
||||
},
|
||||
),
|
||||
// 회사 기본 정보 헤더 (회사명/지점명 + 주소)
|
||||
CompanyFormHeader(
|
||||
nameController: _controller.nameController,
|
||||
nameFocusNode: _controller.nameFocusNode,
|
||||
companyNames: _controller.companyNames,
|
||||
filteredCompanyNames: _controller.filteredCompanyNames,
|
||||
showCompanyNameDropdown:
|
||||
_controller.showCompanyNameDropdown,
|
||||
onCompanyNameSelected: (name) {
|
||||
setState(() {
|
||||
_controller.selectCompanyName(name);
|
||||
});
|
||||
},
|
||||
onShowMapPressed: () {
|
||||
final fullAddress = _controller.companyAddress.toString();
|
||||
MapDialog.show(context, fullAddress);
|
||||
},
|
||||
onNameSaved: (value) {},
|
||||
onAddressChanged: (address) {
|
||||
setState(() {
|
||||
_controller.updateCompanyAddress(address);
|
||||
});
|
||||
},
|
||||
initialAddress: _controller.companyAddress,
|
||||
nameLabel: nameLabel,
|
||||
nameHint: nameHint,
|
||||
remarkController: _controller.remarkController,
|
||||
),
|
||||
// 담당자 정보
|
||||
ContactInfoForm(
|
||||
contactNameController: _controller.contactNameController,
|
||||
contactPositionController:
|
||||
_controller.contactPositionController,
|
||||
contactPhoneController: _controller.contactPhoneController,
|
||||
contactEmailController: _controller.contactEmailController,
|
||||
positions: _controller.positions,
|
||||
selectedPhonePrefix: _controller.selectedPhonePrefix,
|
||||
phonePrefixes: _controller.phonePrefixes,
|
||||
onPhonePrefixChanged: (value) {
|
||||
setState(() {
|
||||
_controller.selectedPhonePrefix = value;
|
||||
});
|
||||
},
|
||||
onNameSaved: (value) {},
|
||||
onPositionSaved: (value) {},
|
||||
onPhoneSaved: (value) {},
|
||||
onEmailSaved: (value) {},
|
||||
),
|
||||
// 지점 정보(하단) 및 +지점추가 버튼은 본사/신규 등록일 때만 노출
|
||||
if (!(isBranch && branchId != null)) ...[
|
||||
if (_controller.branchControllers.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0, bottom: 8.0),
|
||||
child: Text(
|
||||
'지점 정보',
|
||||
style: AppThemeTailwind.subheadingStyle,
|
||||
),
|
||||
),
|
||||
if (_controller.branchControllers.isNotEmpty)
|
||||
for (
|
||||
int i = 0;
|
||||
i < _controller.branchControllers.length;
|
||||
i++
|
||||
)
|
||||
BranchFormWidget(
|
||||
controller: _controller.branchControllers[i],
|
||||
index: i,
|
||||
onRemove: () {
|
||||
setState(() {
|
||||
_controller.removeBranch(i);
|
||||
});
|
||||
},
|
||||
onAddressChanged: (address) {
|
||||
setState(() {
|
||||
_controller.updateBranchAddress(i, address);
|
||||
});
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _addBranch,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('지점 추가'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: const Size.fromHeight(48),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
// 저장 버튼
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 24.0, bottom: 16.0),
|
||||
child: ElevatedButton(
|
||||
onPressed: _saveCompany,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppThemeTailwind.primary,
|
||||
minimumSize: const Size.fromHeight(48),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
isEditMode ? '수정 완료' : '등록 완료',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
501
lib/screens/company/company_list.dart
Normal file
501
lib/screens/company/company_list.dart
Normal file
@@ -0,0 +1,501 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:superport/models/company_model.dart';
|
||||
import 'package:superport/screens/common/theme_tailwind.dart';
|
||||
import 'package:superport/screens/common/main_layout.dart';
|
||||
import 'package:superport/screens/common/custom_widgets.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
import 'package:superport/screens/common/widgets/pagination.dart';
|
||||
import 'package:superport/screens/company/widgets/company_branch_dialog.dart';
|
||||
|
||||
class CompanyListScreen extends StatefulWidget {
|
||||
const CompanyListScreen({super.key});
|
||||
|
||||
@override
|
||||
State<CompanyListScreen> createState() => _CompanyListScreenState();
|
||||
}
|
||||
|
||||
class _CompanyListScreenState extends State<CompanyListScreen> {
|
||||
final MockDataService _dataService = MockDataService();
|
||||
List<Company> _companies = [];
|
||||
// 페이지네이션 상태 추가
|
||||
int _currentPage = 1; // 현재 페이지 (1부터 시작)
|
||||
final int _pageSize = 10; // 페이지당 개수
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadData();
|
||||
}
|
||||
|
||||
void _loadData() {
|
||||
setState(() {
|
||||
_companies = _dataService.getAllCompanies();
|
||||
// 데이터가 변경되면 첫 페이지로 이동
|
||||
_currentPage = 1;
|
||||
});
|
||||
}
|
||||
|
||||
void _navigateToAddScreen() async {
|
||||
final result = await Navigator.pushNamed(context, '/company/add');
|
||||
if (result == true) {
|
||||
_loadData();
|
||||
}
|
||||
}
|
||||
|
||||
void _navigateToEditScreen(int id) async {
|
||||
final result = await Navigator.pushNamed(
|
||||
context,
|
||||
'/company/edit',
|
||||
arguments: id,
|
||||
);
|
||||
if (result == true) {
|
||||
_loadData();
|
||||
}
|
||||
}
|
||||
|
||||
void _deleteCompany(int id) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: const Text('삭제 확인'),
|
||||
content: const Text('이 회사 정보를 삭제하시겠습니까?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('취소'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_dataService.deleteCompany(id);
|
||||
Navigator.pop(context);
|
||||
_loadData();
|
||||
},
|
||||
child: const Text('삭제'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 회사 유형에 따라 칩 위젯 생성 (복수)
|
||||
Widget _buildCompanyTypeChips(List<CompanyType> types) {
|
||||
return Row(
|
||||
children:
|
||||
types.map((type) {
|
||||
final Color textColor =
|
||||
type == CompanyType.customer
|
||||
? Colors.blue.shade800
|
||||
: Colors.green.shade800;
|
||||
final String label = companyTypeToString(type);
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(right: 4),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: textColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
// 본사/지점 구분 표시 위젯
|
||||
Widget _buildCompanyTypeLabel(bool isBranch, {String? mainCompanyName}) {
|
||||
if (isBranch) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.account_tree, size: 16, color: Colors.blue.shade600),
|
||||
const SizedBox(width: 4),
|
||||
const Text('지점'),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.business, size: 16, color: Colors.grey.shade700),
|
||||
const SizedBox(width: 4),
|
||||
const Text('본사'),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 회사 이름 표시 위젯 (지점인 경우 "본사명 > 지점명" 형식)
|
||||
Widget _buildCompanyNameText(
|
||||
Company company,
|
||||
bool isBranch, {
|
||||
String? mainCompanyName,
|
||||
}) {
|
||||
if (isBranch && mainCompanyName != null) {
|
||||
return Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: isBranch ? '▶ ' : '',
|
||||
style: TextStyle(color: Colors.grey.shade600, fontSize: 14),
|
||||
),
|
||||
TextSpan(
|
||||
text: isBranch ? '$mainCompanyName > ' : '',
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade700,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: company.name,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Text(
|
||||
company.name,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 지점(본사+지점)만 보여주는 팝업 오픈 함수
|
||||
void _showBranchDialog(Company mainCompany) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => CompanyBranchDialog(mainCompany: mainCompany),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 대시보드 폭에 맞게 조정
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final maxContentWidth = screenWidth > 1200 ? 1200.0 : screenWidth - 32;
|
||||
|
||||
// 본사와 지점 구분하기 위한 데이터 준비
|
||||
final List<Map<String, dynamic>> displayCompanies = [];
|
||||
for (final company in _companies) {
|
||||
displayCompanies.add({
|
||||
'company': company,
|
||||
'isBranch': false,
|
||||
'mainCompanyName': null,
|
||||
});
|
||||
if (company.branches != null) {
|
||||
for (final branch in company.branches!) {
|
||||
displayCompanies.add({
|
||||
'branch': branch, // 지점 객체 자체 저장
|
||||
'companyId': company.id, // 본사 id 저장
|
||||
'isBranch': true,
|
||||
'mainCompanyName': company.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 페이지네이션 데이터 슬라이싱
|
||||
final int totalCount = displayCompanies.length;
|
||||
final int startIndex = (_currentPage - 1) * _pageSize;
|
||||
final int endIndex =
|
||||
(startIndex + _pageSize) > totalCount
|
||||
? totalCount
|
||||
: (startIndex + _pageSize);
|
||||
final List<Map<String, dynamic>> pagedCompanies = displayCompanies.sublist(
|
||||
startIndex,
|
||||
endIndex,
|
||||
);
|
||||
|
||||
return MainLayout(
|
||||
title: '회사 관리',
|
||||
currentRoute: Routes.company,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: _loadData,
|
||||
color: Colors.grey,
|
||||
),
|
||||
],
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
PageTitle(
|
||||
title: '회사 목록',
|
||||
width: maxContentWidth - 32,
|
||||
rightWidget: ElevatedButton.icon(
|
||||
onPressed: _navigateToAddScreen,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('추가'),
|
||||
style: AppThemeTailwind.primaryButtonStyle,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: DataTableCard(
|
||||
width: maxContentWidth - 32,
|
||||
child:
|
||||
pagedCompanies.isEmpty
|
||||
? const Center(child: Text('등록된 회사 정보가 없습니다.'))
|
||||
: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Container(
|
||||
width: maxContentWidth - 32,
|
||||
constraints: BoxConstraints(
|
||||
minWidth: maxContentWidth - 64,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.vertical,
|
||||
child: DataTable(
|
||||
columns: const [
|
||||
DataColumn(label: Text('번호')),
|
||||
DataColumn(label: Text('구분')),
|
||||
DataColumn(label: Text('회사명')),
|
||||
DataColumn(label: Text('유형')),
|
||||
DataColumn(label: Text('주소')),
|
||||
DataColumn(label: Text('지점 수 (본사만 표시)')),
|
||||
DataColumn(label: Text('관리')),
|
||||
],
|
||||
rows:
|
||||
pagedCompanies.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final data = entry.value;
|
||||
final bool isBranch =
|
||||
data['isBranch'] as bool;
|
||||
final String? mainCompanyName =
|
||||
data['mainCompanyName'] as String?;
|
||||
|
||||
if (isBranch) {
|
||||
final Branch branch =
|
||||
data['branch'] as Branch;
|
||||
final int companyId =
|
||||
data['companyId'] as int;
|
||||
return DataRow(
|
||||
cells: [
|
||||
DataCell(
|
||||
Text('${startIndex + index + 1}'),
|
||||
),
|
||||
DataCell(
|
||||
_buildCompanyTypeLabel(
|
||||
true,
|
||||
mainCompanyName:
|
||||
mainCompanyName,
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
_buildCompanyNameText(
|
||||
Company(
|
||||
id: branch.id,
|
||||
name: branch.name,
|
||||
address: branch.address,
|
||||
contactName:
|
||||
branch.contactName,
|
||||
contactPosition:
|
||||
branch.contactPosition,
|
||||
contactPhone:
|
||||
branch.contactPhone,
|
||||
contactEmail:
|
||||
branch.contactEmail,
|
||||
companyTypes: [],
|
||||
remark: branch.remark,
|
||||
),
|
||||
true,
|
||||
mainCompanyName:
|
||||
mainCompanyName,
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
_buildCompanyTypeChips([]),
|
||||
),
|
||||
DataCell(
|
||||
Text(branch.address.toString()),
|
||||
),
|
||||
DataCell(const Text('')),
|
||||
DataCell(
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.edit,
|
||||
color:
|
||||
AppThemeTailwind
|
||||
.primary,
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
'/company/edit',
|
||||
arguments: {
|
||||
'companyId':
|
||||
companyId,
|
||||
'isBranch': true,
|
||||
'mainCompanyName':
|
||||
mainCompanyName,
|
||||
'branchId': branch.id,
|
||||
},
|
||||
).then((result) {
|
||||
if (result == true)
|
||||
_loadData();
|
||||
});
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.delete,
|
||||
color:
|
||||
AppThemeTailwind
|
||||
.danger,
|
||||
),
|
||||
onPressed: () {
|
||||
// 지점 삭제 로직 필요시 구현
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
final Company company =
|
||||
data['company'] as Company;
|
||||
return DataRow(
|
||||
cells: [
|
||||
DataCell(
|
||||
Text('${startIndex + index + 1}'),
|
||||
),
|
||||
DataCell(
|
||||
_buildCompanyTypeLabel(false),
|
||||
),
|
||||
DataCell(
|
||||
_buildCompanyNameText(
|
||||
company,
|
||||
false,
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
_buildCompanyTypeChips(
|
||||
company.companyTypes,
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Text(company.address.toString()),
|
||||
),
|
||||
DataCell(
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
if ((company
|
||||
.branches
|
||||
?.isNotEmpty ??
|
||||
false)) {
|
||||
_showBranchDialog(company);
|
||||
}
|
||||
},
|
||||
child: MouseRegion(
|
||||
cursor:
|
||||
SystemMouseCursors.click,
|
||||
child: Text(
|
||||
'${(company.branches?.length ?? 0)}',
|
||||
style: TextStyle(
|
||||
color:
|
||||
(company
|
||||
.branches
|
||||
?.isNotEmpty ??
|
||||
false)
|
||||
? Colors.blue
|
||||
: Colors.black,
|
||||
decoration:
|
||||
(company
|
||||
.branches
|
||||
?.isNotEmpty ??
|
||||
false)
|
||||
? TextDecoration
|
||||
.underline
|
||||
: TextDecoration
|
||||
.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.edit,
|
||||
color:
|
||||
AppThemeTailwind
|
||||
.primary,
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
'/company/edit',
|
||||
arguments: {
|
||||
'companyId':
|
||||
company.id,
|
||||
'isBranch': false,
|
||||
},
|
||||
).then((result) {
|
||||
if (result == true)
|
||||
_loadData();
|
||||
});
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.delete,
|
||||
color:
|
||||
AppThemeTailwind
|
||||
.danger,
|
||||
),
|
||||
onPressed: () {
|
||||
_deleteCompany(
|
||||
company.id!,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 페이지네이션 위젯 추가
|
||||
if (totalCount > _pageSize)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Pagination(
|
||||
totalCount: totalCount,
|
||||
currentPage: _currentPage,
|
||||
pageSize: _pageSize,
|
||||
onPageChanged: (page) {
|
||||
setState(() {
|
||||
_currentPage = page;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
121
lib/screens/company/controllers/branch_form_controller.dart
Normal file
121
lib/screens/company/controllers/branch_form_controller.dart
Normal file
@@ -0,0 +1,121 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:superport/models/address_model.dart';
|
||||
import 'package:superport/models/company_model.dart';
|
||||
import 'package:superport/utils/phone_utils.dart';
|
||||
|
||||
/// 지점(Branch) 폼 컨트롤러
|
||||
///
|
||||
/// 각 지점의 상태, 컨트롤러, 포커스, 드롭다운, 전화번호 등 관리를 담당
|
||||
class BranchFormController {
|
||||
// 지점 데이터
|
||||
Branch branch;
|
||||
|
||||
// 입력 컨트롤러
|
||||
final TextEditingController nameController;
|
||||
final TextEditingController contactNameController;
|
||||
final TextEditingController contactPositionController;
|
||||
final TextEditingController contactPhoneController;
|
||||
final TextEditingController contactEmailController;
|
||||
final TextEditingController remarkController;
|
||||
|
||||
// 포커스 노드
|
||||
final FocusNode focusNode;
|
||||
// 카드 키(위젯 식별용)
|
||||
final GlobalKey cardKey;
|
||||
// 직책 드롭다운 상태
|
||||
final ValueNotifier<bool> positionDropdownNotifier;
|
||||
// 전화번호 접두사
|
||||
String selectedPhonePrefix;
|
||||
|
||||
// 직책 목록(공통 상수로 관리 권장)
|
||||
final List<String> positions;
|
||||
// 전화번호 접두사 목록(공통 상수로 관리 권장)
|
||||
final List<String> phonePrefixes;
|
||||
|
||||
BranchFormController({
|
||||
required this.branch,
|
||||
required this.positions,
|
||||
required this.phonePrefixes,
|
||||
}) : nameController = TextEditingController(text: branch.name),
|
||||
contactNameController = TextEditingController(
|
||||
text: branch.contactName ?? '',
|
||||
),
|
||||
contactPositionController = TextEditingController(
|
||||
text: branch.contactPosition ?? '',
|
||||
),
|
||||
contactPhoneController = TextEditingController(
|
||||
text: PhoneUtils.extractPhoneNumberWithoutPrefix(
|
||||
branch.contactPhone ?? '',
|
||||
phonePrefixes,
|
||||
),
|
||||
),
|
||||
contactEmailController = TextEditingController(
|
||||
text: branch.contactEmail ?? '',
|
||||
),
|
||||
remarkController = TextEditingController(text: branch.remark ?? ''),
|
||||
focusNode = FocusNode(),
|
||||
cardKey = GlobalKey(),
|
||||
positionDropdownNotifier = ValueNotifier<bool>(false),
|
||||
selectedPhonePrefix = PhoneUtils.extractPhonePrefix(
|
||||
branch.contactPhone ?? '',
|
||||
phonePrefixes,
|
||||
);
|
||||
|
||||
/// 주소 업데이트
|
||||
void updateAddress(Address address) {
|
||||
branch = branch.copyWith(address: address);
|
||||
}
|
||||
|
||||
/// 필드별 값 업데이트
|
||||
void updateField(String fieldName, String value) {
|
||||
switch (fieldName) {
|
||||
case 'name':
|
||||
branch = branch.copyWith(name: value);
|
||||
break;
|
||||
case 'contactName':
|
||||
branch = branch.copyWith(contactName: value);
|
||||
break;
|
||||
case 'contactPosition':
|
||||
branch = branch.copyWith(contactPosition: value);
|
||||
break;
|
||||
case 'contactPhone':
|
||||
branch = branch.copyWith(
|
||||
contactPhone: PhoneUtils.getFullPhoneNumber(
|
||||
selectedPhonePrefix,
|
||||
value,
|
||||
),
|
||||
);
|
||||
break;
|
||||
case 'contactEmail':
|
||||
branch = branch.copyWith(contactEmail: value);
|
||||
break;
|
||||
case 'remark':
|
||||
branch = branch.copyWith(remark: value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// 전화번호 접두사 변경
|
||||
void updatePhonePrefix(String prefix) {
|
||||
selectedPhonePrefix = prefix;
|
||||
branch = branch.copyWith(
|
||||
contactPhone: PhoneUtils.getFullPhoneNumber(
|
||||
prefix,
|
||||
contactPhoneController.text,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 리소스 해제
|
||||
void dispose() {
|
||||
nameController.dispose();
|
||||
contactNameController.dispose();
|
||||
contactPositionController.dispose();
|
||||
contactPhoneController.dispose();
|
||||
contactEmailController.dispose();
|
||||
remarkController.dispose();
|
||||
focusNode.dispose();
|
||||
positionDropdownNotifier.dispose();
|
||||
// cardKey는 위젯에서 자동 관리
|
||||
}
|
||||
}
|
||||
BIN
lib/screens/company/controllers/company_form_controller.dart
Normal file
BIN
lib/screens/company/controllers/company_form_controller.dart
Normal file
Binary file not shown.
141
lib/screens/company/widgets/branch_card.dart
Normal file
141
lib/screens/company/widgets/branch_card.dart
Normal file
@@ -0,0 +1,141 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:superport/models/address_model.dart';
|
||||
import 'package:superport/models/company_model.dart';
|
||||
import 'package:superport/screens/common/custom_widgets.dart';
|
||||
import 'package:superport/screens/common/theme_tailwind.dart';
|
||||
import 'package:superport/screens/common/widgets/address_input.dart';
|
||||
import 'package:superport/screens/company/widgets/contact_info_widget.dart';
|
||||
import 'package:superport/utils/validators.dart';
|
||||
import 'package:superport/utils/phone_utils.dart';
|
||||
|
||||
class BranchCard extends StatefulWidget {
|
||||
final GlobalKey cardKey;
|
||||
final int index;
|
||||
final Branch branch;
|
||||
final TextEditingController nameController;
|
||||
final TextEditingController contactNameController;
|
||||
final TextEditingController contactPositionController;
|
||||
final TextEditingController contactPhoneController;
|
||||
final TextEditingController contactEmailController;
|
||||
final FocusNode focusNode;
|
||||
final List<String> positions;
|
||||
final List<String> phonePrefixes;
|
||||
final String selectedPhonePrefix;
|
||||
final ValueChanged<String> onNameChanged;
|
||||
final ValueChanged<Address> onAddressChanged;
|
||||
final ValueChanged<String> onContactNameChanged;
|
||||
final ValueChanged<String> onContactPositionChanged;
|
||||
final ValueChanged<String> onContactPhoneChanged;
|
||||
final ValueChanged<String> onContactEmailChanged;
|
||||
final ValueChanged<String> onPhonePrefixChanged;
|
||||
final VoidCallback onDelete;
|
||||
|
||||
const BranchCard({
|
||||
Key? key,
|
||||
required this.cardKey,
|
||||
required this.index,
|
||||
required this.branch,
|
||||
required this.nameController,
|
||||
required this.contactNameController,
|
||||
required this.contactPositionController,
|
||||
required this.contactPhoneController,
|
||||
required this.contactEmailController,
|
||||
required this.focusNode,
|
||||
required this.positions,
|
||||
required this.phonePrefixes,
|
||||
required this.selectedPhonePrefix,
|
||||
required this.onNameChanged,
|
||||
required this.onAddressChanged,
|
||||
required this.onContactNameChanged,
|
||||
required this.onContactPositionChanged,
|
||||
required this.onContactPhoneChanged,
|
||||
required this.onContactEmailChanged,
|
||||
required this.onPhonePrefixChanged,
|
||||
required this.onDelete,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_BranchCardState createState() => _BranchCardState();
|
||||
}
|
||||
|
||||
class _BranchCardState extends State<BranchCard> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
// 화면의 빈 공간 터치 시 포커스 해제
|
||||
FocusScope.of(context).unfocus();
|
||||
},
|
||||
child: Card(
|
||||
key: widget.cardKey,
|
||||
margin: const EdgeInsets.only(bottom: 16.0),
|
||||
elevation: 2,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'지점 #${widget.index + 1}',
|
||||
style: AppThemeTailwind.subheadingStyle,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete, color: Colors.red),
|
||||
onPressed: widget.onDelete,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
FormFieldWrapper(
|
||||
label: '지점명',
|
||||
isRequired: true,
|
||||
child: TextFormField(
|
||||
controller: widget.nameController,
|
||||
focusNode: widget.focusNode,
|
||||
decoration: const InputDecoration(hintText: '지점명을 입력하세요'),
|
||||
onChanged: widget.onNameChanged,
|
||||
validator: FormValidator.required('지점명은 필수입니다'),
|
||||
),
|
||||
),
|
||||
AddressInput(
|
||||
initialZipCode: widget.branch.address.zipCode,
|
||||
initialRegion: widget.branch.address.region,
|
||||
initialDetailAddress: widget.branch.address.detailAddress,
|
||||
onAddressChanged: (zipCode, region, detailAddress) {
|
||||
final address = Address(
|
||||
zipCode: zipCode,
|
||||
region: region,
|
||||
detailAddress: detailAddress,
|
||||
);
|
||||
widget.onAddressChanged(address);
|
||||
},
|
||||
),
|
||||
|
||||
// 담당자 정보 - ContactInfoWidget 사용
|
||||
ContactInfoWidget(
|
||||
title: '담당자 정보',
|
||||
contactNameController: widget.contactNameController,
|
||||
contactPositionController: widget.contactPositionController,
|
||||
contactPhoneController: widget.contactPhoneController,
|
||||
contactEmailController: widget.contactEmailController,
|
||||
positions: widget.positions,
|
||||
selectedPhonePrefix: widget.selectedPhonePrefix,
|
||||
phonePrefixes: widget.phonePrefixes,
|
||||
onPhonePrefixChanged: widget.onPhonePrefixChanged,
|
||||
onContactNameChanged: widget.onContactNameChanged,
|
||||
onContactPositionChanged: widget.onContactPositionChanged,
|
||||
onContactPhoneChanged: widget.onContactPhoneChanged,
|
||||
onContactEmailChanged: widget.onContactEmailChanged,
|
||||
compactMode: false, // compactMode를 false로 변경하여 한 줄로 표시
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
112
lib/screens/company/widgets/branch_form_widget.dart
Normal file
112
lib/screens/company/widgets/branch_form_widget.dart
Normal file
@@ -0,0 +1,112 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../controllers/branch_form_controller.dart';
|
||||
import 'package:superport/models/address_model.dart';
|
||||
import 'package:superport/screens/company/widgets/contact_info_form.dart';
|
||||
import 'package:superport/screens/common/widgets/address_input.dart';
|
||||
import 'package:superport/screens/common/widgets/remark_input.dart';
|
||||
|
||||
/// 지점 입력 폼 위젯
|
||||
///
|
||||
/// BranchFormController를 받아서 입력 필드, 드롭다운, 포커스, 전화번호 등 UI/상태를 관리한다.
|
||||
class BranchFormWidget extends StatelessWidget {
|
||||
final BranchFormController controller;
|
||||
final int index;
|
||||
final void Function()? onRemove;
|
||||
final void Function(Address)? onAddressChanged;
|
||||
|
||||
const BranchFormWidget({
|
||||
Key? key,
|
||||
required this.controller,
|
||||
required this.index,
|
||||
this.onRemove,
|
||||
this.onAddressChanged,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
key: controller.cardKey,
|
||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: controller.nameController,
|
||||
focusNode: controller.focusNode,
|
||||
decoration: const InputDecoration(labelText: '지점명'),
|
||||
onChanged: (value) => controller.updateField('name', value),
|
||||
),
|
||||
),
|
||||
if (onRemove != null)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete, color: Colors.red),
|
||||
onPressed: onRemove,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// 주소 입력: 회사와 동일한 AddressInput 위젯 사용
|
||||
AddressInput(
|
||||
initialZipCode: controller.branch.address.zipCode,
|
||||
initialRegion: controller.branch.address.region,
|
||||
initialDetailAddress: controller.branch.address.detailAddress,
|
||||
isRequired: false,
|
||||
onAddressChanged: (zipCode, region, detailAddress) {
|
||||
controller.updateAddress(
|
||||
Address(
|
||||
zipCode: zipCode,
|
||||
region: region,
|
||||
detailAddress: detailAddress,
|
||||
),
|
||||
);
|
||||
if (onAddressChanged != null) {
|
||||
onAddressChanged!(
|
||||
Address(
|
||||
zipCode: zipCode,
|
||||
region: region,
|
||||
detailAddress: detailAddress,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// 담당자 정보 입력: ContactInfoForm 위젯으로 대체 (회사 담당자와 동일 UI)
|
||||
ContactInfoForm(
|
||||
contactNameController: controller.contactNameController,
|
||||
contactPositionController: controller.contactPositionController,
|
||||
contactPhoneController: controller.contactPhoneController,
|
||||
contactEmailController: controller.contactEmailController,
|
||||
positions: controller.positions,
|
||||
selectedPhonePrefix: controller.selectedPhonePrefix,
|
||||
phonePrefixes: controller.phonePrefixes,
|
||||
onPhonePrefixChanged: (value) {
|
||||
controller.updatePhonePrefix(value);
|
||||
},
|
||||
onNameSaved: (value) {
|
||||
controller.updateField('contactName', value ?? '');
|
||||
},
|
||||
onPositionSaved: (value) {
|
||||
controller.updateField('contactPosition', value ?? '');
|
||||
},
|
||||
onPhoneSaved: (value) {
|
||||
controller.updateField('contactPhone', value ?? '');
|
||||
},
|
||||
onEmailSaved: (value) {
|
||||
controller.updateField('contactEmail', value ?? '');
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// 비고 입력란
|
||||
RemarkInput(controller: controller.remarkController),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
374
lib/screens/company/widgets/company_branch_dialog.dart
Normal file
374
lib/screens/company/widgets/company_branch_dialog.dart
Normal file
@@ -0,0 +1,374 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:superport/models/company_model.dart';
|
||||
import 'package:superport/screens/company/widgets/company_info_card.dart';
|
||||
import 'package:pdf/widgets.dart' as pw; // PDF 생성용
|
||||
import 'package:printing/printing.dart'; // PDF 프린트/미리보기용
|
||||
import 'dart:typed_data'; // Uint8List
|
||||
import 'package:pdf/pdf.dart'; // PdfColors, PageFormat 등 전체 임포트
|
||||
import 'package:superport/screens/common/custom_widgets.dart'; // DataTableCard 사용을 위한 import
|
||||
import 'package:flutter/services.dart'; // rootBundle 사용을 위한 import
|
||||
|
||||
/// 본사와 지점 리스트를 보여주는 다이얼로그 위젯
|
||||
class CompanyBranchDialog extends StatelessWidget {
|
||||
final Company mainCompany;
|
||||
|
||||
const CompanyBranchDialog({super.key, required this.mainCompany});
|
||||
|
||||
// 본사+지점 정보를 PDF로 생성하는 함수
|
||||
Future<Uint8List> _buildPdf(final pw.Document pdf) async {
|
||||
// 한글 폰트 로드 (lib/assets/fonts/NotoSansKR-VariableFont_wght.ttf)
|
||||
final fontData = await rootBundle.load(
|
||||
'lib/assets/fonts/NotoSansKR-VariableFont_wght.ttf',
|
||||
);
|
||||
final ttf = pw.Font.ttf(fontData);
|
||||
final List<Branch> branchList = mainCompany.branches ?? [];
|
||||
pdf.addPage(
|
||||
pw.Page(
|
||||
build: (pw.Context context) {
|
||||
return pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.Text(
|
||||
'본사 및 지점 목록',
|
||||
style: pw.TextStyle(
|
||||
font: ttf, // 한글 폰트 적용
|
||||
fontSize: 20,
|
||||
fontWeight: pw.FontWeight.bold,
|
||||
),
|
||||
),
|
||||
pw.SizedBox(height: 16),
|
||||
pw.Table(
|
||||
border: pw.TableBorder.all(color: PdfColors.grey800),
|
||||
defaultVerticalAlignment: pw.TableCellVerticalAlignment.middle,
|
||||
children: [
|
||||
pw.TableRow(
|
||||
decoration: pw.BoxDecoration(color: PdfColors.grey300),
|
||||
children: [
|
||||
pw.Padding(
|
||||
padding: const pw.EdgeInsets.all(4),
|
||||
child: pw.Text('구분', style: pw.TextStyle(font: ttf)),
|
||||
),
|
||||
pw.Padding(
|
||||
padding: const pw.EdgeInsets.all(4),
|
||||
child: pw.Text('이름', style: pw.TextStyle(font: ttf)),
|
||||
),
|
||||
pw.Padding(
|
||||
padding: const pw.EdgeInsets.all(4),
|
||||
child: pw.Text('우편번호', style: pw.TextStyle(font: ttf)),
|
||||
),
|
||||
pw.Padding(
|
||||
padding: const pw.EdgeInsets.all(4),
|
||||
child: pw.Text('담당자', style: pw.TextStyle(font: ttf)),
|
||||
),
|
||||
pw.Padding(
|
||||
padding: const pw.EdgeInsets.all(4),
|
||||
child: pw.Text('직책', style: pw.TextStyle(font: ttf)),
|
||||
),
|
||||
pw.Padding(
|
||||
padding: const pw.EdgeInsets.all(4),
|
||||
child: pw.Text('전화번호', style: pw.TextStyle(font: ttf)),
|
||||
),
|
||||
pw.Padding(
|
||||
padding: const pw.EdgeInsets.all(4),
|
||||
child: pw.Text('이메일', style: pw.TextStyle(font: ttf)),
|
||||
),
|
||||
],
|
||||
),
|
||||
// 본사
|
||||
pw.TableRow(
|
||||
children: [
|
||||
pw.Padding(
|
||||
padding: const pw.EdgeInsets.all(4),
|
||||
child: pw.Text('본사', style: pw.TextStyle(font: ttf)),
|
||||
),
|
||||
pw.Padding(
|
||||
padding: const pw.EdgeInsets.all(4),
|
||||
child: pw.Text(
|
||||
mainCompany.name,
|
||||
style: pw.TextStyle(font: ttf),
|
||||
),
|
||||
),
|
||||
pw.Padding(
|
||||
padding: const pw.EdgeInsets.all(4),
|
||||
child: pw.Text(
|
||||
mainCompany.address.zipCode,
|
||||
style: pw.TextStyle(font: ttf),
|
||||
),
|
||||
),
|
||||
pw.Padding(
|
||||
padding: const pw.EdgeInsets.all(4),
|
||||
child: pw.Text(
|
||||
mainCompany.contactName ?? '',
|
||||
style: pw.TextStyle(font: ttf),
|
||||
),
|
||||
),
|
||||
pw.Padding(
|
||||
padding: const pw.EdgeInsets.all(4),
|
||||
child: pw.Text(
|
||||
mainCompany.contactPosition ?? '',
|
||||
style: pw.TextStyle(font: ttf),
|
||||
),
|
||||
),
|
||||
pw.Padding(
|
||||
padding: const pw.EdgeInsets.all(4),
|
||||
child: pw.Text(
|
||||
mainCompany.contactPhone ?? '',
|
||||
style: pw.TextStyle(font: ttf),
|
||||
),
|
||||
),
|
||||
pw.Padding(
|
||||
padding: const pw.EdgeInsets.all(4),
|
||||
child: pw.Text(
|
||||
mainCompany.contactEmail ?? '',
|
||||
style: pw.TextStyle(font: ttf),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// 지점
|
||||
...branchList.map(
|
||||
(branch) => pw.TableRow(
|
||||
children: [
|
||||
pw.Padding(
|
||||
padding: const pw.EdgeInsets.all(4),
|
||||
child: pw.Text('지점', style: pw.TextStyle(font: ttf)),
|
||||
),
|
||||
pw.Padding(
|
||||
padding: const pw.EdgeInsets.all(4),
|
||||
child: pw.Text(
|
||||
branch.name,
|
||||
style: pw.TextStyle(font: ttf),
|
||||
),
|
||||
),
|
||||
pw.Padding(
|
||||
padding: const pw.EdgeInsets.all(4),
|
||||
child: pw.Text(
|
||||
branch.address.zipCode,
|
||||
style: pw.TextStyle(font: ttf),
|
||||
),
|
||||
),
|
||||
pw.Padding(
|
||||
padding: const pw.EdgeInsets.all(4),
|
||||
child: pw.Text(
|
||||
branch.contactName ?? '',
|
||||
style: pw.TextStyle(font: ttf),
|
||||
),
|
||||
),
|
||||
pw.Padding(
|
||||
padding: const pw.EdgeInsets.all(4),
|
||||
child: pw.Text(
|
||||
branch.contactPosition ?? '',
|
||||
style: pw.TextStyle(font: ttf),
|
||||
),
|
||||
),
|
||||
pw.Padding(
|
||||
padding: const pw.EdgeInsets.all(4),
|
||||
child: pw.Text(
|
||||
branch.contactPhone ?? '',
|
||||
style: pw.TextStyle(font: ttf),
|
||||
),
|
||||
),
|
||||
pw.Padding(
|
||||
padding: const pw.EdgeInsets.all(4),
|
||||
child: pw.Text(
|
||||
branch.contactEmail ?? '',
|
||||
style: pw.TextStyle(font: ttf),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
return pdf.save();
|
||||
}
|
||||
|
||||
// 프린트 버튼 클릭 시 PDF 미리보기 및 인쇄
|
||||
void _printPopupData() async {
|
||||
final pdf = pw.Document();
|
||||
await Printing.layoutPdf(
|
||||
onLayout: (format) async {
|
||||
return _buildPdf(pdf);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<Branch> branchList = mainCompany.branches ?? [];
|
||||
// 본사와 지점 정보를 한 리스트로 합침
|
||||
final List<Map<String, dynamic>> displayList = [
|
||||
{
|
||||
'type': '본사',
|
||||
'name': mainCompany.name,
|
||||
'companyTypes': mainCompany.companyTypes,
|
||||
'address': mainCompany.address,
|
||||
'contactName': mainCompany.contactName,
|
||||
'contactPosition': mainCompany.contactPosition,
|
||||
'contactPhone': mainCompany.contactPhone,
|
||||
'contactEmail': mainCompany.contactEmail,
|
||||
},
|
||||
...branchList.map(
|
||||
(branch) => {
|
||||
'type': '지점',
|
||||
'name': branch.name,
|
||||
'companyTypes': mainCompany.companyTypes,
|
||||
'address': branch.address,
|
||||
'contactName': branch.contactName,
|
||||
'contactPosition': branch.contactPosition,
|
||||
'contactPhone': branch.contactPhone,
|
||||
'contactEmail': branch.contactEmail,
|
||||
},
|
||||
),
|
||||
];
|
||||
final double maxDialogHeight = MediaQuery.of(context).size.height * 0.7;
|
||||
final double maxDialogWidth = MediaQuery.of(context).size.width * 0.8;
|
||||
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: [
|
||||
const Text(
|
||||
'본사 및 지점 목록',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.print),
|
||||
tooltip: '프린트',
|
||||
onPressed: _printPopupData,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Expanded(
|
||||
child: DataTableCard(
|
||||
width: maxDialogWidth - 48,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Container(
|
||||
width: maxDialogWidth - 48,
|
||||
constraints: BoxConstraints(minWidth: 900),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.vertical,
|
||||
child: DataTable(
|
||||
columns: const [
|
||||
DataColumn(label: Text('번호')),
|
||||
DataColumn(label: Text('구분')),
|
||||
DataColumn(label: Text('회사명')),
|
||||
DataColumn(label: Text('유형')),
|
||||
DataColumn(label: Text('주소')),
|
||||
DataColumn(label: Text('담당자')),
|
||||
DataColumn(label: Text('직책')),
|
||||
DataColumn(label: Text('전화번호')),
|
||||
DataColumn(label: Text('이메일')),
|
||||
],
|
||||
rows:
|
||||
displayList.asMap().entries.map((entry) {
|
||||
final int index = entry.key;
|
||||
final data = entry.value;
|
||||
return DataRow(
|
||||
cells: [
|
||||
DataCell(Text('${index + 1}')),
|
||||
DataCell(Text(data['type'])),
|
||||
DataCell(Text(data['name'])),
|
||||
DataCell(
|
||||
Row(
|
||||
children:
|
||||
(data['companyTypes']
|
||||
as List<CompanyType>)
|
||||
.map(
|
||||
(type) => Container(
|
||||
margin:
|
||||
const EdgeInsets.only(
|
||||
right: 4,
|
||||
),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
type ==
|
||||
CompanyType
|
||||
.customer
|
||||
? Colors
|
||||
.blue
|
||||
.shade50
|
||||
: Colors
|
||||
.green
|
||||
.shade50,
|
||||
borderRadius:
|
||||
BorderRadius.circular(
|
||||
8,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
companyTypeToString(type),
|
||||
style: TextStyle(
|
||||
color:
|
||||
type ==
|
||||
CompanyType
|
||||
.customer
|
||||
? Colors
|
||||
.blue
|
||||
.shade800
|
||||
: Colors
|
||||
.green
|
||||
.shade800,
|
||||
fontWeight:
|
||||
FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
DataCell(Text(data['address'].toString())),
|
||||
DataCell(Text(data['contactName'] ?? '')),
|
||||
DataCell(
|
||||
Text(data['contactPosition'] ?? ''),
|
||||
),
|
||||
DataCell(Text(data['contactPhone'] ?? '')),
|
||||
DataCell(Text(data['contactEmail'] ?? '')),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
85
lib/screens/company/widgets/company_form_header.dart
Normal file
85
lib/screens/company/widgets/company_form_header.dart
Normal file
@@ -0,0 +1,85 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:superport/models/address_model.dart';
|
||||
import 'package:superport/screens/common/custom_widgets.dart';
|
||||
import 'package:superport/screens/common/theme_tailwind.dart';
|
||||
import 'package:superport/screens/common/widgets/address_input.dart';
|
||||
import 'package:superport/utils/validators.dart';
|
||||
import 'package:superport/screens/company/widgets/company_name_autocomplete.dart';
|
||||
import 'package:superport/screens/common/widgets/remark_input.dart';
|
||||
|
||||
class CompanyFormHeader extends StatelessWidget {
|
||||
final TextEditingController nameController;
|
||||
final FocusNode nameFocusNode;
|
||||
final List<String> companyNames;
|
||||
final List<String> filteredCompanyNames;
|
||||
final bool showCompanyNameDropdown;
|
||||
final Function(String) onCompanyNameSelected;
|
||||
final Function() onShowMapPressed;
|
||||
final ValueChanged<String?> onNameSaved;
|
||||
final ValueChanged<Address> onAddressChanged;
|
||||
final Address initialAddress;
|
||||
final String nameLabel;
|
||||
final String nameHint;
|
||||
final TextEditingController remarkController;
|
||||
|
||||
const CompanyFormHeader({
|
||||
Key? key,
|
||||
required this.nameController,
|
||||
required this.nameFocusNode,
|
||||
required this.companyNames,
|
||||
required this.filteredCompanyNames,
|
||||
required this.showCompanyNameDropdown,
|
||||
required this.onCompanyNameSelected,
|
||||
required this.onShowMapPressed,
|
||||
required this.onNameSaved,
|
||||
required this.onAddressChanged,
|
||||
this.initialAddress = const Address(),
|
||||
required this.nameLabel,
|
||||
required this.nameHint,
|
||||
required this.remarkController,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 회사명/지점명
|
||||
FormFieldWrapper(
|
||||
label: nameLabel,
|
||||
isRequired: true,
|
||||
child: CompanyNameAutocomplete(
|
||||
nameController: nameController,
|
||||
nameFocusNode: nameFocusNode,
|
||||
companyNames: companyNames,
|
||||
filteredCompanyNames: filteredCompanyNames,
|
||||
showCompanyNameDropdown: showCompanyNameDropdown,
|
||||
onCompanyNameSelected: onCompanyNameSelected,
|
||||
onNameSaved: onNameSaved,
|
||||
label: nameLabel,
|
||||
hint: nameHint,
|
||||
),
|
||||
),
|
||||
|
||||
// 주소 입력 위젯 (SRP에 따라 별도 컴포넌트로 분리)
|
||||
AddressInput(
|
||||
initialZipCode: initialAddress.zipCode,
|
||||
initialRegion: initialAddress.region,
|
||||
initialDetailAddress: initialAddress.detailAddress,
|
||||
isRequired: false,
|
||||
onAddressChanged: (zipCode, region, detailAddress) {
|
||||
final address = Address(
|
||||
zipCode: zipCode,
|
||||
region: region,
|
||||
detailAddress: detailAddress,
|
||||
);
|
||||
onAddressChanged(address);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 비고 입력란
|
||||
RemarkInput(controller: remarkController),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
93
lib/screens/company/widgets/company_info_card.dart
Normal file
93
lib/screens/company/widgets/company_info_card.dart
Normal file
@@ -0,0 +1,93 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:superport/models/company_model.dart';
|
||||
import 'package:superport/models/address_model.dart';
|
||||
|
||||
/// 회사/지점 정보를 1행(1열)로 보여주는 재활용 위젯
|
||||
class CompanyInfoCard extends StatelessWidget {
|
||||
final String title; // 본사/지점 구분
|
||||
final String name;
|
||||
final List<CompanyType> companyTypes;
|
||||
final Address address;
|
||||
final String? contactName;
|
||||
final String? contactPosition;
|
||||
final String? contactPhone;
|
||||
final String? contactEmail;
|
||||
|
||||
const CompanyInfoCard({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.name,
|
||||
required this.companyTypes,
|
||||
required this.address,
|
||||
this.contactName,
|
||||
this.contactPosition,
|
||||
this.contactPhone,
|
||||
this.contactEmail,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 각 데이터가 없으면 빈 문자열로 표기
|
||||
final String zipCode = address.zipCode.isNotEmpty ? address.zipCode : '';
|
||||
final String displayName = name.isNotEmpty ? name : '';
|
||||
final String displayContactName =
|
||||
contactName != null && contactName!.isNotEmpty ? contactName! : '';
|
||||
final String displayContactPosition =
|
||||
contactPosition != null && contactPosition!.isNotEmpty
|
||||
? contactPosition!
|
||||
: '';
|
||||
final String displayContactPhone =
|
||||
contactPhone != null && contactPhone!.isNotEmpty ? contactPhone! : '';
|
||||
final String displayContactEmail =
|
||||
contactEmail != null && contactEmail!.isNotEmpty ? contactEmail! : '';
|
||||
|
||||
return Card(
|
||||
color: Colors.grey.shade50,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 본사/지점 구분만 상단에 표기 (텍스트 크기 14로 축소)
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 2), // 간격도 절반으로 축소
|
||||
// 1행(1열)로 데이터만 표기
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(displayName, style: const TextStyle(fontSize: 13)),
|
||||
const SizedBox(width: 12),
|
||||
Text(zipCode, style: const TextStyle(fontSize: 13)),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
displayContactName,
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
displayContactPosition,
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
displayContactPhone,
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
displayContactEmail,
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
185
lib/screens/company/widgets/company_name_autocomplete.dart
Normal file
185
lib/screens/company/widgets/company_name_autocomplete.dart
Normal file
@@ -0,0 +1,185 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:superport/utils/validators.dart';
|
||||
|
||||
class CompanyNameAutocomplete extends StatelessWidget {
|
||||
final TextEditingController nameController;
|
||||
final FocusNode nameFocusNode;
|
||||
final List<String> companyNames;
|
||||
final List<String> filteredCompanyNames;
|
||||
final bool showCompanyNameDropdown;
|
||||
final Function(String) onCompanyNameSelected;
|
||||
final ValueChanged<String?> onNameSaved;
|
||||
final String label;
|
||||
final String hint;
|
||||
|
||||
const CompanyNameAutocomplete({
|
||||
Key? key,
|
||||
required this.nameController,
|
||||
required this.nameFocusNode,
|
||||
required this.companyNames,
|
||||
required this.filteredCompanyNames,
|
||||
required this.showCompanyNameDropdown,
|
||||
required this.onCompanyNameSelected,
|
||||
required this.onNameSaved,
|
||||
required this.label,
|
||||
required this.hint,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: nameController,
|
||||
focusNode: nameFocusNode,
|
||||
textInputAction: TextInputAction.next,
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
hintText: hint,
|
||||
suffixIcon:
|
||||
nameController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
nameController.clear();
|
||||
},
|
||||
)
|
||||
: IconButton(
|
||||
icon: const Icon(Icons.arrow_drop_down),
|
||||
onPressed: () {},
|
||||
),
|
||||
),
|
||||
validator: (value) => validateRequired(value, label),
|
||||
onFieldSubmitted: (_) {
|
||||
if (filteredCompanyNames.length == 1 && showCompanyNameDropdown) {
|
||||
onCompanyNameSelected(filteredCompanyNames[0]);
|
||||
}
|
||||
},
|
||||
onTap: () {},
|
||||
onSaved: onNameSaved,
|
||||
),
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
height:
|
||||
showCompanyNameDropdown
|
||||
? (filteredCompanyNames.length > 4
|
||||
? 200
|
||||
: filteredCompanyNames.length * 50.0)
|
||||
: 0,
|
||||
margin: EdgeInsets.only(top: showCompanyNameDropdown ? 4 : 0),
|
||||
child: SingleChildScrollView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
child: GestureDetector(
|
||||
onTap: () {},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withAlpha(77),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 3,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child:
|
||||
filteredCompanyNames.isEmpty
|
||||
? const Padding(
|
||||
padding: EdgeInsets.all(12.0),
|
||||
child: Text('검색 결과가 없습니다'),
|
||||
)
|
||||
: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: filteredCompanyNames.length,
|
||||
separatorBuilder:
|
||||
(context, index) => Divider(
|
||||
height: 1,
|
||||
color: Colors.grey.shade200,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final companyName = filteredCompanyNames[index];
|
||||
final text = nameController.text.toLowerCase();
|
||||
|
||||
if (text.isEmpty) {
|
||||
return ListTile(
|
||||
dense: true,
|
||||
title: Text(companyName),
|
||||
onTap: () => onCompanyNameSelected(companyName),
|
||||
);
|
||||
}
|
||||
|
||||
// 일치하는 부분 찾기
|
||||
final matchIndex = companyName
|
||||
.toLowerCase()
|
||||
.indexOf(text.toLowerCase());
|
||||
if (matchIndex < 0) {
|
||||
return ListTile(
|
||||
dense: true,
|
||||
title: Text(companyName),
|
||||
onTap: () => onCompanyNameSelected(companyName),
|
||||
);
|
||||
}
|
||||
|
||||
return ListTile(
|
||||
dense: true,
|
||||
title: RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
// 일치 이전 부분
|
||||
if (matchIndex > 0)
|
||||
TextSpan(
|
||||
text: companyName.substring(
|
||||
0,
|
||||
matchIndex,
|
||||
),
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
// 일치하는 부분
|
||||
TextSpan(
|
||||
text: companyName.substring(
|
||||
matchIndex,
|
||||
matchIndex + text.length,
|
||||
),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
// 일치 이후 부분
|
||||
if (matchIndex + text.length <
|
||||
companyName.length)
|
||||
TextSpan(
|
||||
text: companyName.substring(
|
||||
matchIndex + text.length,
|
||||
),
|
||||
style: TextStyle(
|
||||
color:
|
||||
matchIndex == 0
|
||||
? Colors.grey[600]
|
||||
: Colors.black,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onTap: () => onCompanyNameSelected(companyName),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
58
lib/screens/company/widgets/contact_info_form.dart
Normal file
58
lib/screens/company/widgets/contact_info_form.dart
Normal file
@@ -0,0 +1,58 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:superport/screens/company/widgets/contact_info_widget.dart';
|
||||
|
||||
/// 담당자 정보 폼
|
||||
///
|
||||
/// 회사 등록 및 수정 화면에서 사용되는 담당자 정보 입력 폼
|
||||
/// 내부적으로 공통 ContactInfoWidget을 사용하여 코드 재사용성 확보
|
||||
class ContactInfoForm extends StatelessWidget {
|
||||
final TextEditingController contactNameController;
|
||||
final TextEditingController contactPositionController;
|
||||
final TextEditingController contactPhoneController;
|
||||
final TextEditingController contactEmailController;
|
||||
final List<String> positions;
|
||||
final String selectedPhonePrefix;
|
||||
final List<String> phonePrefixes;
|
||||
final ValueChanged<String> onPhonePrefixChanged;
|
||||
final ValueChanged<String?> onNameSaved;
|
||||
final ValueChanged<String?> onPositionSaved;
|
||||
final ValueChanged<String?> onPhoneSaved;
|
||||
final ValueChanged<String?> onEmailSaved;
|
||||
|
||||
const ContactInfoForm({
|
||||
Key? key,
|
||||
required this.contactNameController,
|
||||
required this.contactPositionController,
|
||||
required this.contactPhoneController,
|
||||
required this.contactEmailController,
|
||||
required this.positions,
|
||||
required this.selectedPhonePrefix,
|
||||
required this.phonePrefixes,
|
||||
required this.onPhonePrefixChanged,
|
||||
required this.onNameSaved,
|
||||
required this.onPositionSaved,
|
||||
required this.onPhoneSaved,
|
||||
required this.onEmailSaved,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// ContactInfoWidget을 사용하여 담당자 정보 UI 구성
|
||||
return ContactInfoWidget(
|
||||
contactNameController: contactNameController,
|
||||
contactPositionController: contactPositionController,
|
||||
contactPhoneController: contactPhoneController,
|
||||
contactEmailController: contactEmailController,
|
||||
positions: positions,
|
||||
selectedPhonePrefix: selectedPhonePrefix,
|
||||
phonePrefixes: phonePrefixes,
|
||||
onPhonePrefixChanged: onPhonePrefixChanged,
|
||||
|
||||
// 각 콜백 함수를 ContactInfoWidget의 onChanged 콜백과 연결
|
||||
onContactNameChanged: (value) => onNameSaved?.call(value),
|
||||
onContactPositionChanged: (value) => onPositionSaved?.call(value),
|
||||
onContactPhoneChanged: (value) => onPhoneSaved?.call(value),
|
||||
onContactEmailChanged: (value) => onEmailSaved?.call(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
702
lib/screens/company/widgets/contact_info_widget.dart
Normal file
702
lib/screens/company/widgets/contact_info_widget.dart
Normal file
@@ -0,0 +1,702 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'dart:developer' as developer;
|
||||
import 'package:superport/screens/common/custom_widgets.dart';
|
||||
import 'package:superport/utils/validators.dart';
|
||||
import 'package:superport/utils/phone_utils.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
/// 담당자 정보 위젯
|
||||
///
|
||||
/// 회사 및 지점의 담당자 정보를 입력받는 공통 위젯
|
||||
/// SRP(단일 책임 원칙)에 따라 담당자 정보 입력 로직을 분리
|
||||
class ContactInfoWidget extends StatefulWidget {
|
||||
/// 위젯 제목
|
||||
final String title;
|
||||
|
||||
/// 담당자 이름 컨트롤러
|
||||
final TextEditingController contactNameController;
|
||||
|
||||
/// 담당자 직책 컨트롤러
|
||||
final TextEditingController contactPositionController;
|
||||
|
||||
/// 담당자 전화번호 컨트롤러
|
||||
final TextEditingController contactPhoneController;
|
||||
|
||||
/// 담당자 이메일 컨트롤러
|
||||
final TextEditingController contactEmailController;
|
||||
|
||||
/// 직책 목록
|
||||
final List<String> positions;
|
||||
|
||||
/// 선택된 전화번호 접두사
|
||||
final String selectedPhonePrefix;
|
||||
|
||||
/// 전화번호 접두사 목록
|
||||
final List<String> phonePrefixes;
|
||||
|
||||
/// 직책 컴팩트 모드 (Row 또는 Column 레이아웃 결정)
|
||||
final bool compactMode;
|
||||
|
||||
/// 전화번호 접두사 변경 콜백
|
||||
final ValueChanged<String> onPhonePrefixChanged;
|
||||
|
||||
/// 담당자 이름 변경 콜백
|
||||
final ValueChanged<String> onContactNameChanged;
|
||||
|
||||
/// 담당자 직책 변경 콜백
|
||||
final ValueChanged<String> onContactPositionChanged;
|
||||
|
||||
/// 담당자 전화번호 변경 콜백
|
||||
final ValueChanged<String> onContactPhoneChanged;
|
||||
|
||||
/// 담당자 이메일 변경 콜백
|
||||
final ValueChanged<String> onContactEmailChanged;
|
||||
|
||||
const ContactInfoWidget({
|
||||
Key? key,
|
||||
this.title = '담당자 정보',
|
||||
required this.contactNameController,
|
||||
required this.contactPositionController,
|
||||
required this.contactPhoneController,
|
||||
required this.contactEmailController,
|
||||
required this.positions,
|
||||
required this.selectedPhonePrefix,
|
||||
required this.phonePrefixes,
|
||||
required this.onPhonePrefixChanged,
|
||||
required this.onContactNameChanged,
|
||||
required this.onContactPositionChanged,
|
||||
required this.onContactPhoneChanged,
|
||||
required this.onContactEmailChanged,
|
||||
this.compactMode = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<ContactInfoWidget> createState() => _ContactInfoWidgetState();
|
||||
}
|
||||
|
||||
class _ContactInfoWidgetState extends State<ContactInfoWidget> {
|
||||
bool _showPositionDropdown = false;
|
||||
bool _showPhonePrefixDropdown = false;
|
||||
final LayerLink _positionLayerLink = LayerLink();
|
||||
final LayerLink _phonePrefixLayerLink = LayerLink();
|
||||
|
||||
OverlayEntry? _positionOverlayEntry;
|
||||
OverlayEntry? _phonePrefixOverlayEntry;
|
||||
|
||||
final FocusNode _positionFocusNode = FocusNode();
|
||||
final FocusNode _phonePrefixFocusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
developer.log('ContactInfoWidget 초기화 완료', name: 'ContactInfoWidget');
|
||||
|
||||
_positionFocusNode.addListener(() {
|
||||
if (_positionFocusNode.hasFocus) {
|
||||
developer.log('직책 필드 포커스 얻음', name: 'ContactInfoWidget');
|
||||
} else {
|
||||
developer.log('직책 필드 포커스 잃음', name: 'ContactInfoWidget');
|
||||
}
|
||||
});
|
||||
|
||||
_phonePrefixFocusNode.addListener(() {
|
||||
if (_phonePrefixFocusNode.hasFocus) {
|
||||
developer.log('전화번호 접두사 필드 포커스 얻음', name: 'ContactInfoWidget');
|
||||
} else {
|
||||
developer.log('전화번호 접두사 필드 포커스 잃음', name: 'ContactInfoWidget');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_removeAllOverlays();
|
||||
_positionFocusNode.dispose();
|
||||
_phonePrefixFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _togglePositionDropdown() {
|
||||
developer.log(
|
||||
'직책 드롭다운 토글: $_showPositionDropdown -> ${!_showPositionDropdown}',
|
||||
name: 'ContactInfoWidget',
|
||||
);
|
||||
setState(() {
|
||||
if (_showPositionDropdown) {
|
||||
_removePositionOverlay();
|
||||
} else {
|
||||
_showPositionDropdown = true;
|
||||
_showPhonePrefixDropdown = false;
|
||||
_removePhonePrefixOverlay();
|
||||
_showPositionOverlay();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _togglePhonePrefixDropdown() {
|
||||
developer.log(
|
||||
'전화번호 접두사 드롭다운 토글: $_showPhonePrefixDropdown -> ${!_showPhonePrefixDropdown}',
|
||||
name: 'ContactInfoWidget',
|
||||
);
|
||||
setState(() {
|
||||
if (_showPhonePrefixDropdown) {
|
||||
_removePhonePrefixOverlay();
|
||||
} else {
|
||||
_showPhonePrefixDropdown = true;
|
||||
_showPositionDropdown = false;
|
||||
_removePositionOverlay();
|
||||
_showPhonePrefixOverlay();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _removePositionOverlay() {
|
||||
_positionOverlayEntry?.remove();
|
||||
_positionOverlayEntry = null;
|
||||
_showPositionDropdown = false;
|
||||
}
|
||||
|
||||
void _removePhonePrefixOverlay() {
|
||||
_phonePrefixOverlayEntry?.remove();
|
||||
_phonePrefixOverlayEntry = null;
|
||||
_showPhonePrefixDropdown = false;
|
||||
}
|
||||
|
||||
void _removeAllOverlays() {
|
||||
_removePositionOverlay();
|
||||
_removePhonePrefixOverlay();
|
||||
}
|
||||
|
||||
void _closeAllDropdowns() {
|
||||
if (_showPositionDropdown || _showPhonePrefixDropdown) {
|
||||
developer.log('모든 드롭다운 닫기', name: 'ContactInfoWidget');
|
||||
setState(() {
|
||||
_removeAllOverlays();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _showPositionOverlay() {
|
||||
final RenderBox renderBox = context.findRenderObject() as RenderBox;
|
||||
final size = renderBox.size;
|
||||
final offset = renderBox.localToGlobal(Offset.zero);
|
||||
|
||||
final availableHeight =
|
||||
MediaQuery.of(context).size.height - offset.dy - 100;
|
||||
final maxHeight = math.min(300.0, availableHeight);
|
||||
|
||||
_positionOverlayEntry = OverlayEntry(
|
||||
builder:
|
||||
(context) => Positioned(
|
||||
width: 200,
|
||||
child: CompositedTransformFollower(
|
||||
link: _positionLayerLink,
|
||||
showWhenUnlinked: false,
|
||||
offset: const Offset(0, 45),
|
||||
child: Material(
|
||||
elevation: 4,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.3),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 3,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
constraints: BoxConstraints(maxHeight: maxHeight),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
...widget.positions.map(
|
||||
(position) => InkWell(
|
||||
onTap: () {
|
||||
developer.log(
|
||||
'직책 선택됨: $position',
|
||||
name: 'ContactInfoWidget',
|
||||
);
|
||||
setState(() {
|
||||
widget.contactPositionController.text =
|
||||
position;
|
||||
widget.onContactPositionChanged(position);
|
||||
_removePositionOverlay();
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
width: double.infinity,
|
||||
child: Text(position),
|
||||
),
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
developer.log(
|
||||
'직책 기타(직접 입력) 선택됨',
|
||||
name: 'ContactInfoWidget',
|
||||
);
|
||||
_removePositionOverlay();
|
||||
widget.contactPositionController.clear();
|
||||
widget.onContactPositionChanged('');
|
||||
_positionFocusNode.requestFocus();
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
width: double.infinity,
|
||||
child: const Text('기타 (직접 입력)'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Overlay.of(context).insert(_positionOverlayEntry!);
|
||||
}
|
||||
|
||||
void _showPhonePrefixOverlay() {
|
||||
final RenderBox renderBox = context.findRenderObject() as RenderBox;
|
||||
final size = renderBox.size;
|
||||
final offset = renderBox.localToGlobal(Offset.zero);
|
||||
|
||||
final availableHeight =
|
||||
MediaQuery.of(context).size.height - offset.dy - 100;
|
||||
final maxHeight = math.min(300.0, availableHeight);
|
||||
|
||||
_phonePrefixOverlayEntry = OverlayEntry(
|
||||
builder:
|
||||
(context) => Positioned(
|
||||
width: 200,
|
||||
child: CompositedTransformFollower(
|
||||
link: _phonePrefixLayerLink,
|
||||
showWhenUnlinked: false,
|
||||
offset: const Offset(0, 45),
|
||||
child: Material(
|
||||
elevation: 4,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.3),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 3,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
constraints: BoxConstraints(maxHeight: maxHeight),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
...widget.phonePrefixes.map(
|
||||
(prefix) => InkWell(
|
||||
onTap: () {
|
||||
developer.log(
|
||||
'전화번호 접두사 선택됨: $prefix',
|
||||
name: 'ContactInfoWidget',
|
||||
);
|
||||
widget.onPhonePrefixChanged(prefix);
|
||||
setState(() {
|
||||
_removePhonePrefixOverlay();
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
width: double.infinity,
|
||||
child: Text(prefix),
|
||||
),
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
developer.log(
|
||||
'전화번호 접두사 직접 입력 선택됨',
|
||||
name: 'ContactInfoWidget',
|
||||
);
|
||||
_removePhonePrefixOverlay();
|
||||
_phonePrefixFocusNode.requestFocus();
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
width: double.infinity,
|
||||
child: const Text('기타 (직접 입력)'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Overlay.of(context).insert(_phonePrefixOverlayEntry!);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
developer.log(
|
||||
'ContactInfoWidget 빌드 시작: 직책 드롭다운=$_showPositionDropdown, 전화번호 접두사 드롭다운=$_showPhonePrefixDropdown',
|
||||
name: 'ContactInfoWidget',
|
||||
);
|
||||
|
||||
// 컴팩트 모드에 따라 다른 레이아웃 생성
|
||||
return FormFieldWrapper(
|
||||
label: widget.title,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children:
|
||||
widget.compactMode ? _buildCompactLayout() : _buildDefaultLayout(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 기본 레이아웃 (한 줄에 모든 필드 표시)
|
||||
List<Widget> _buildDefaultLayout() {
|
||||
return [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 담당자 이름
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: TextFormField(
|
||||
controller: widget.contactNameController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '이름',
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 15,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
developer.log('이름 필드 터치됨', name: 'ContactInfoWidget');
|
||||
_closeAllDropdowns();
|
||||
},
|
||||
onChanged: widget.onContactNameChanged,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// 담당자 직책
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: CompositedTransformTarget(
|
||||
link: _positionLayerLink,
|
||||
child: Stack(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: widget.contactPositionController,
|
||||
focusNode: _positionFocusNode,
|
||||
decoration: InputDecoration(
|
||||
hintText: '직책',
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 15,
|
||||
),
|
||||
suffixIcon: IconButton(
|
||||
icon: const Icon(Icons.arrow_drop_down, size: 20),
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () {
|
||||
developer.log(
|
||||
'직책 드롭다운 버튼 클릭됨',
|
||||
name: 'ContactInfoWidget',
|
||||
);
|
||||
_togglePositionDropdown();
|
||||
},
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
// 필드를 터치했을 때는 드롭다운을 열지 않고 직접 입력 모드로 진입
|
||||
_closeAllDropdowns();
|
||||
},
|
||||
onChanged: widget.onContactPositionChanged,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// 전화번호 접두사
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: CompositedTransformTarget(
|
||||
link: _phonePrefixLayerLink,
|
||||
child: Stack(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: TextEditingController(
|
||||
text: widget.selectedPhonePrefix,
|
||||
),
|
||||
focusNode: _phonePrefixFocusNode,
|
||||
decoration: InputDecoration(
|
||||
hintText: '국가번호',
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 15,
|
||||
),
|
||||
suffixIcon: IconButton(
|
||||
icon: const Icon(Icons.arrow_drop_down, size: 20),
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () {
|
||||
developer.log(
|
||||
'전화번호 접두사 드롭다운 버튼 클릭됨',
|
||||
name: 'ContactInfoWidget',
|
||||
);
|
||||
_togglePhonePrefixDropdown();
|
||||
},
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
// 필드를 터치했을 때는 드롭다운을 열지 않고 직접 입력 모드로 진입
|
||||
_closeAllDropdowns();
|
||||
},
|
||||
onChanged: (value) {
|
||||
if (value.isNotEmpty) {
|
||||
widget.onPhonePrefixChanged(value);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// 담당자 전화번호
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: TextFormField(
|
||||
controller: widget.contactPhoneController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '전화번호',
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 15,
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.phone,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
PhoneUtils.phoneInputFormatter,
|
||||
],
|
||||
onTap: () {
|
||||
developer.log('전화번호 필드 터치됨', name: 'ContactInfoWidget');
|
||||
_closeAllDropdowns();
|
||||
},
|
||||
validator: validatePhoneNumber,
|
||||
onChanged: widget.onContactPhoneChanged,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// 담당자 이메일
|
||||
Expanded(
|
||||
flex: 6,
|
||||
child: TextFormField(
|
||||
controller: widget.contactEmailController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '이메일',
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 15,
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
onTap: () {
|
||||
developer.log('이메일 필드 터치됨', name: 'ContactInfoWidget');
|
||||
_closeAllDropdowns();
|
||||
},
|
||||
validator: FormValidator.email(),
|
||||
onChanged: widget.onContactEmailChanged,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
// 컴팩트 레이아웃 (여러 줄에 필드 표시)
|
||||
List<Widget> _buildCompactLayout() {
|
||||
return [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 담당자 이름
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: widget.contactNameController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '담당자 이름',
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 15,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
developer.log('이름 필드 터치됨', name: 'ContactInfoWidget');
|
||||
_closeAllDropdowns();
|
||||
},
|
||||
onChanged: widget.onContactNameChanged,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// 담당자 직책
|
||||
Expanded(
|
||||
child: CompositedTransformTarget(
|
||||
link: _positionLayerLink,
|
||||
child: InkWell(
|
||||
onTap: _togglePositionDropdown,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 15,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.shade400),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.contactPositionController.text.isEmpty
|
||||
? '직책 선택'
|
||||
: widget.contactPositionController.text,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color:
|
||||
widget.contactPositionController.text.isEmpty
|
||||
? Colors.grey.shade600
|
||||
: Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Icon(Icons.arrow_drop_down),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 전화번호 (접두사 + 번호)
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
// 전화번호 접두사
|
||||
CompositedTransformTarget(
|
||||
link: _phonePrefixLayerLink,
|
||||
child: InkWell(
|
||||
onTap: _togglePhonePrefixDropdown,
|
||||
child: Container(
|
||||
width: 70,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 14,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.shade400),
|
||||
borderRadius: const BorderRadius.horizontal(
|
||||
left: Radius.circular(4),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
widget.selectedPhonePrefix,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
const Icon(Icons.arrow_drop_down, size: 18),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 전화번호
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: widget.contactPhoneController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '전화번호',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.horizontal(
|
||||
left: Radius.zero,
|
||||
right: Radius.circular(4),
|
||||
),
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 15,
|
||||
),
|
||||
),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
PhoneUtils.phoneInputFormatter,
|
||||
],
|
||||
keyboardType: TextInputType.phone,
|
||||
onTap: _closeAllDropdowns,
|
||||
onChanged: widget.onContactPhoneChanged,
|
||||
validator: validatePhoneNumber,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// 이메일
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: widget.contactEmailController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '이메일 주소',
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 15,
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
onTap: _closeAllDropdowns,
|
||||
onChanged: widget.onContactEmailChanged,
|
||||
validator: FormValidator.email(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
67
lib/screens/company/widgets/duplicate_company_dialog.dart
Normal file
67
lib/screens/company/widgets/duplicate_company_dialog.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:superport/models/company_model.dart';
|
||||
|
||||
/// 중복된 회사명을 확인하는 대화상자
|
||||
class DuplicateCompanyDialog extends StatelessWidget {
|
||||
final Company company;
|
||||
|
||||
const DuplicateCompanyDialog({Key? key, required this.company})
|
||||
: super(key: key);
|
||||
|
||||
static void show(BuildContext context, Company company) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => DuplicateCompanyDialog(company: company),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('중복된 회사'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('동일한 이름의 회사가 이미 등록되어 있습니다.'),
|
||||
const SizedBox(height: 16),
|
||||
Text('회사명: ${company.name}'),
|
||||
Text('주소: ${company.address ?? ''}'),
|
||||
Text('담당자: ${company.contactName ?? ''}'),
|
||||
Text('직책: ${company.contactPosition ?? ''}'),
|
||||
Text('연락처: ${company.contactPhone ?? ''}'),
|
||||
Text('이메일: ${company.contactEmail ?? ''}'),
|
||||
const SizedBox(height: 8),
|
||||
if (company.branches != null && company.branches!.isNotEmpty)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'지점 정보:',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
...company.branches!.map(
|
||||
(branch) => Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0, top: 4.0),
|
||||
child: Text(
|
||||
'${branch.name}: ${branch.address ?? ''} (담당자: ${branch.contactName ?? ''}, 직책: ${branch.contactPosition ?? ''}, 연락처: ${branch.contactPhone ?? ''})',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('확인'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
99
lib/screens/company/widgets/map_dialog.dart
Normal file
99
lib/screens/company/widgets/map_dialog.dart
Normal file
@@ -0,0 +1,99 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:superport/screens/common/theme_tailwind.dart';
|
||||
|
||||
/// 주소에 대한 지도 대화상자를 표시합니다.
|
||||
class MapDialog extends StatelessWidget {
|
||||
final String address;
|
||||
|
||||
const MapDialog({Key? key, required this.address}) : super(key: key);
|
||||
|
||||
static void show(BuildContext context, String address) {
|
||||
if (address.trim().isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('주소를 먼저 입력해주세요.'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return MapDialog(address: address);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
height: MediaQuery.of(context).size.height * 0.7,
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'주소 지도 보기',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text('주소: $address', style: const TextStyle(fontSize: 14)),
|
||||
const SizedBox(height: 16),
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade200,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.map,
|
||||
size: 64,
|
||||
color: AppThemeTailwind.primary,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'여기에 주소 "$address"에 대한\n지도가 표시됩니다.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Colors.grey.shade700),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'실제 구현 시에는 Google Maps 또는\n다른 지도 서비스 API를 연동하세요.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade600,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user