- 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).
176 lines
7.2 KiB
Dart
176 lines
7.2 KiB
Dart
import 'package:flutter/material.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({
|
|
super.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,
|
|
});
|
|
|
|
@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: Theme.of(context).cardColor,
|
|
borderRadius: BorderRadius.circular(4),
|
|
border: Border.all(color: Theme.of(context).dividerColor),
|
|
boxShadow: const [
|
|
BoxShadow(color: Colors.transparent),
|
|
],
|
|
),
|
|
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: Theme.of(context).dividerColor),
|
|
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: TextStyle(
|
|
color: Theme.of(context).textTheme.bodyMedium?.color,
|
|
),
|
|
),
|
|
// 일치하는 부분
|
|
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
|
|
? Theme.of(context).textTheme.bodySmall?.color?.withOpacity(0.7)
|
|
: Theme.of(context).textTheme.bodyMedium?.color,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
onTap: () => onCompanyNameSelected(companyName),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|