결재 단계 편집 다이얼로그 구현
This commit is contained in:
@@ -5,6 +5,7 @@ import 'package:superport_v2/core/network/api_client.dart';
|
||||
import 'package:superport_v2/features/approvals/step/domain/entities/approval_step_record.dart';
|
||||
import 'package:superport_v2/features/approvals/step/domain/repositories/approval_step_repository.dart';
|
||||
import '../dtos/approval_step_record_dto.dart';
|
||||
import '../../domain/entities/approval_step_input.dart';
|
||||
|
||||
class ApprovalStepRepositoryRemote implements ApprovalStepRepository {
|
||||
ApprovalStepRepositoryRemote({required ApiClient apiClient})
|
||||
@@ -48,4 +49,32 @@ class ApprovalStepRepositoryRemote implements ApprovalStepRepository {
|
||||
final data = (response.data?['data'] as Map<String, dynamic>?) ?? const {};
|
||||
return ApprovalStepRecordDto.fromJson(data).toEntity();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ApprovalStepRecord> create(ApprovalStepInput input) async {
|
||||
final response = await _api.post<Map<String, dynamic>>(
|
||||
_basePath,
|
||||
data: input.toPayload(),
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
final raw = response.data;
|
||||
final data =
|
||||
(raw?['data'] as Map<String, dynamic>?) ??
|
||||
(raw is Map<String, dynamic> ? raw : const <String, dynamic>{});
|
||||
return ApprovalStepRecordDto.fromJson(data).toEntity();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ApprovalStepRecord> update(int id, ApprovalStepInput input) async {
|
||||
final response = await _api.patch<Map<String, dynamic>>(
|
||||
'$_basePath/$id',
|
||||
data: input.toPayload(),
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
final raw = response.data;
|
||||
final data =
|
||||
(raw?['data'] as Map<String, dynamic>?) ??
|
||||
(raw is Map<String, dynamic> ? raw : const <String, dynamic>{});
|
||||
return ApprovalStepRecordDto.fromJson(data).toEntity();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/// 결재 단계 생성/수정 입력 모델
|
||||
///
|
||||
/// - 단계 순서, 승인자, 비고 등의 값을 API 페이로드로 직렬화한다.
|
||||
/// - `approvalId`는 생성 시에만 필요하며 수정 시에는 null로 둘 수 있다.
|
||||
class ApprovalStepInput {
|
||||
ApprovalStepInput({
|
||||
this.approvalId,
|
||||
required this.stepOrder,
|
||||
required this.approverId,
|
||||
this.statusId,
|
||||
this.assignedAt,
|
||||
this.decidedAt,
|
||||
this.note,
|
||||
}) : assert(stepOrder > 0, '단계 순서는 1 이상의 정수여야 합니다.'),
|
||||
assert(approverId > 0, '승인자 ID는 양수여야 합니다.');
|
||||
|
||||
final int? approvalId;
|
||||
final int stepOrder;
|
||||
final int approverId;
|
||||
final int? statusId;
|
||||
final DateTime? assignedAt;
|
||||
final DateTime? decidedAt;
|
||||
final String? note;
|
||||
|
||||
/// API 요청 페이로드를 구성한다.
|
||||
Map<String, dynamic> toPayload() {
|
||||
final payload = <String, dynamic>{
|
||||
'step_order': stepOrder,
|
||||
'approver_id': approverId,
|
||||
if (statusId != null) 'status_id': statusId,
|
||||
if (assignedAt != null)
|
||||
'assigned_at': assignedAt!.toUtc().toIso8601String(),
|
||||
if (decidedAt != null) 'decided_at': decidedAt!.toUtc().toIso8601String(),
|
||||
if (note != null && note!.trim().isNotEmpty) 'note': note,
|
||||
};
|
||||
if (approvalId != null) {
|
||||
payload['approval_id'] = approvalId;
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
ApprovalStepInput copyWith({
|
||||
int? approvalId,
|
||||
int? stepOrder,
|
||||
int? approverId,
|
||||
int? statusId,
|
||||
DateTime? assignedAt,
|
||||
DateTime? decidedAt,
|
||||
String? note,
|
||||
}) {
|
||||
return ApprovalStepInput(
|
||||
approvalId: approvalId ?? this.approvalId,
|
||||
stepOrder: stepOrder ?? this.stepOrder,
|
||||
approverId: approverId ?? this.approverId,
|
||||
statusId: statusId ?? this.statusId,
|
||||
assignedAt: assignedAt ?? this.assignedAt,
|
||||
decidedAt: decidedAt ?? this.decidedAt,
|
||||
note: note ?? this.note,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
|
||||
import '../entities/approval_step_input.dart';
|
||||
import '../entities/approval_step_record.dart';
|
||||
|
||||
abstract class ApprovalStepRepository {
|
||||
@@ -13,4 +14,10 @@ abstract class ApprovalStepRepository {
|
||||
});
|
||||
|
||||
Future<ApprovalStepRecord> fetchDetail(int id);
|
||||
|
||||
/// 결재 단계를 생성한다.
|
||||
Future<ApprovalStepRecord> create(ApprovalStepInput input);
|
||||
|
||||
/// 결재 단계를 수정한다.
|
||||
Future<ApprovalStepRecord> update(int id, ApprovalStepInput input);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
|
||||
import '../../domain/entities/approval_step_input.dart';
|
||||
import '../../domain/entities/approval_step_record.dart';
|
||||
import '../../domain/repositories/approval_step_repository.dart';
|
||||
|
||||
@@ -12,6 +13,7 @@ class ApprovalStepController extends ChangeNotifier {
|
||||
|
||||
PaginatedResult<ApprovalStepRecord>? _result;
|
||||
bool _isLoading = false;
|
||||
bool _isSaving = false;
|
||||
String _query = '';
|
||||
int? _statusId;
|
||||
int? _approverId;
|
||||
@@ -21,6 +23,7 @@ class ApprovalStepController extends ChangeNotifier {
|
||||
|
||||
PaginatedResult<ApprovalStepRecord>? get result => _result;
|
||||
bool get isLoading => _isLoading;
|
||||
bool get isSaving => _isSaving;
|
||||
String get query => _query;
|
||||
int? get statusId => _statusId;
|
||||
int? get approverId => _approverId;
|
||||
@@ -101,4 +104,43 @@ class ApprovalStepController extends ChangeNotifier {
|
||||
_approverId = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<ApprovalStepRecord?> createStep(ApprovalStepInput input) async {
|
||||
_isSaving = true;
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
try {
|
||||
final created = await _repository.create(input);
|
||||
final nextPage = _result?.page ?? 1;
|
||||
await fetch(page: nextPage);
|
||||
return created;
|
||||
} catch (e) {
|
||||
_errorMessage = e.toString();
|
||||
return null;
|
||||
} finally {
|
||||
_isSaving = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<ApprovalStepRecord?> updateStep(
|
||||
int id,
|
||||
ApprovalStepInput input,
|
||||
) async {
|
||||
_isSaving = true;
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
try {
|
||||
final updated = await _repository.update(id, input);
|
||||
final nextPage = _result?.page ?? 1;
|
||||
await fetch(page: nextPage);
|
||||
return updated;
|
||||
} catch (e) {
|
||||
_errorMessage = e.toString();
|
||||
return null;
|
||||
} finally {
|
||||
_isSaving = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import '../../../../../widgets/app_layout.dart';
|
||||
import '../../../../../widgets/components/filter_bar.dart';
|
||||
import '../../../../../widgets/spec_page.dart';
|
||||
import '../controllers/approval_step_controller.dart';
|
||||
import '../../domain/entities/approval_step_input.dart';
|
||||
import '../../domain/entities/approval_step_record.dart';
|
||||
import '../../domain/repositories/approval_step_repository.dart';
|
||||
|
||||
@@ -141,6 +142,7 @@ class _ApprovalStepEnabledPageState extends State<_ApprovalStepEnabledPage> {
|
||||
final selectedStatus = _controller.statusId ?? -1;
|
||||
final approverOptions = _buildApproverOptions(records);
|
||||
final selectedApprover = _controller.approverId ?? -1;
|
||||
final isSaving = _controller.isSaving;
|
||||
|
||||
return AppLayout(
|
||||
title: '결재 단계 관리',
|
||||
@@ -151,13 +153,19 @@ class _ApprovalStepEnabledPageState extends State<_ApprovalStepEnabledPage> {
|
||||
AppBreadcrumbItem(label: '결재 단계'),
|
||||
],
|
||||
actions: [
|
||||
Tooltip(
|
||||
message: '결재 단계 생성은 정책 정리 후 제공됩니다.',
|
||||
child: ShadButton(
|
||||
onPressed: null,
|
||||
leading: const Icon(lucide.LucideIcons.plus, size: 16),
|
||||
child: const Text('단계 추가'),
|
||||
),
|
||||
ShadButton(
|
||||
key: const ValueKey('approval_step_create'),
|
||||
onPressed: (_controller.isLoading || isSaving)
|
||||
? null
|
||||
: _openCreateStepForm,
|
||||
leading: isSaving
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(lucide.LucideIcons.plus, size: 16),
|
||||
child: Text(isSaving ? '저장 중...' : '단계 추가'),
|
||||
),
|
||||
],
|
||||
toolbar: FilterBar(
|
||||
@@ -225,12 +233,16 @@ class _ApprovalStepEnabledPageState extends State<_ApprovalStepEnabledPage> {
|
||||
),
|
||||
),
|
||||
ShadButton.outline(
|
||||
onPressed: _controller.isLoading ? null : _applyFilters,
|
||||
onPressed: (_controller.isLoading || isSaving)
|
||||
? null
|
||||
: _applyFilters,
|
||||
child: const Text('검색 적용'),
|
||||
),
|
||||
ShadButton.ghost(
|
||||
onPressed:
|
||||
!_controller.isLoading && _controller.hasActiveFilters
|
||||
!_controller.isLoading &&
|
||||
!isSaving &&
|
||||
_controller.hasActiveFilters
|
||||
? _resetFilters
|
||||
: null,
|
||||
child: const Text('필터 초기화'),
|
||||
@@ -319,15 +331,35 @@ class _ApprovalStepEnabledPageState extends State<_ApprovalStepEnabledPage> {
|
||||
ShadTableCell(
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: ShadButton.outline(
|
||||
key: ValueKey(
|
||||
'step_detail_${step.id ?? record.approvalId}_${step.stepOrder}',
|
||||
),
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: step.id == null
|
||||
? null
|
||||
: () => _openDetail(record),
|
||||
child: const Text('상세'),
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
children: [
|
||||
ShadButton.outline(
|
||||
key: ValueKey(
|
||||
'step_detail_${step.id ?? record.approvalId}_${step.stepOrder}',
|
||||
),
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed:
|
||||
step.id == null ||
|
||||
_controller.isLoading ||
|
||||
isSaving
|
||||
? null
|
||||
: () => _openDetail(record),
|
||||
child: const Text('상세'),
|
||||
),
|
||||
if (step.id != null)
|
||||
ShadButton(
|
||||
key: ValueKey(
|
||||
'step_edit_${step.id}_${step.stepOrder}',
|
||||
),
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed:
|
||||
_controller.isLoading || isSaving
|
||||
? null
|
||||
: () => _openEditStepForm(record),
|
||||
child: const Text('수정'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -345,7 +377,9 @@ class _ApprovalStepEnabledPageState extends State<_ApprovalStepEnabledPage> {
|
||||
ShadButton.outline(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed:
|
||||
_controller.isLoading || currentPage <= 1
|
||||
_controller.isLoading ||
|
||||
isSaving ||
|
||||
currentPage <= 1
|
||||
? null
|
||||
: () => _controller.fetch(
|
||||
page: currentPage - 1,
|
||||
@@ -355,7 +389,10 @@ class _ApprovalStepEnabledPageState extends State<_ApprovalStepEnabledPage> {
|
||||
const SizedBox(width: 8),
|
||||
ShadButton.outline(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: _controller.isLoading || !hasNext
|
||||
onPressed:
|
||||
_controller.isLoading ||
|
||||
isSaving ||
|
||||
!hasNext
|
||||
? null
|
||||
: () => _controller.fetch(
|
||||
page: currentPage + 1,
|
||||
@@ -413,6 +450,67 @@ class _ApprovalStepEnabledPageState extends State<_ApprovalStepEnabledPage> {
|
||||
_searchFocus.requestFocus();
|
||||
}
|
||||
|
||||
Future<void> _openCreateStepForm() async {
|
||||
final input = await showDialog<ApprovalStepInput>(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
return _StepFormDialog(
|
||||
title: '결재 단계 추가',
|
||||
submitLabel: '저장',
|
||||
isEditing: false,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (!mounted || input == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final created = await _controller.createStep(input);
|
||||
if (!mounted || created == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('결재번호 ${created.approvalNo} 단계가 추가되었습니다.')),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _openEditStepForm(ApprovalStepRecord record) async {
|
||||
final stepId = record.step.id;
|
||||
if (stepId == null) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('저장되지 않은 단계는 수정할 수 없습니다.')));
|
||||
return;
|
||||
}
|
||||
|
||||
final input = await showDialog<ApprovalStepInput>(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
return _StepFormDialog(
|
||||
title: '결재 단계 수정',
|
||||
submitLabel: '저장',
|
||||
isEditing: true,
|
||||
initialRecord: record,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (!mounted || input == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final updated = await _controller.updateStep(stepId, input);
|
||||
if (!mounted || updated == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('결재번호 ${updated.approvalNo} 단계 정보를 수정했습니다.')),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _openDetail(ApprovalStepRecord record) async {
|
||||
final stepId = record.step.id;
|
||||
if (stepId == null) {
|
||||
@@ -564,3 +662,263 @@ class _DetailRow extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StepFormDialog extends StatefulWidget {
|
||||
const _StepFormDialog({
|
||||
required this.title,
|
||||
required this.submitLabel,
|
||||
required this.isEditing,
|
||||
this.initialRecord,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String submitLabel;
|
||||
final bool isEditing;
|
||||
final ApprovalStepRecord? initialRecord;
|
||||
|
||||
@override
|
||||
State<_StepFormDialog> createState() => _StepFormDialogState();
|
||||
}
|
||||
|
||||
class _StepFormDialogState extends State<_StepFormDialog> {
|
||||
late final TextEditingController _approvalIdController;
|
||||
late final TextEditingController _approvalNoController;
|
||||
late final TextEditingController _stepOrderController;
|
||||
late final TextEditingController _approverIdController;
|
||||
late final TextEditingController _noteController;
|
||||
Map<String, String?> _errors = const {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final record = widget.initialRecord;
|
||||
_approvalIdController = TextEditingController(
|
||||
text: widget.isEditing && record != null
|
||||
? record.approvalId.toString()
|
||||
: '',
|
||||
);
|
||||
_approvalNoController = TextEditingController(
|
||||
text: record?.approvalNo ?? '',
|
||||
);
|
||||
_stepOrderController = TextEditingController(
|
||||
text: record?.step.stepOrder.toString() ?? '',
|
||||
);
|
||||
_approverIdController = TextEditingController(
|
||||
text: record?.step.approver.id.toString() ?? '',
|
||||
);
|
||||
_noteController = TextEditingController(text: record?.step.note ?? '');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_approvalIdController.dispose();
|
||||
_approvalNoController.dispose();
|
||||
_stepOrderController.dispose();
|
||||
_approverIdController.dispose();
|
||||
_noteController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = ShadTheme.of(context);
|
||||
final materialTheme = Theme.of(context);
|
||||
|
||||
return Dialog(
|
||||
insetPadding: const EdgeInsets.all(24),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: ShadCard(
|
||||
title: Text(widget.title, style: theme.textTheme.h3),
|
||||
footer: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
ShadButton.ghost(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('취소'),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
ShadButton(
|
||||
key: const ValueKey('step_form_submit'),
|
||||
onPressed: _handleSubmit,
|
||||
child: Text(widget.submitLabel),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!widget.isEditing)
|
||||
_FormFieldBlock(
|
||||
label: '결재 ID',
|
||||
errorText: _errors['approvalId'],
|
||||
child: ShadInput(
|
||||
key: const ValueKey('step_form_approval_id'),
|
||||
controller: _approvalIdController,
|
||||
onChanged: (_) => _clearError('approvalId'),
|
||||
),
|
||||
)
|
||||
else ...[
|
||||
_FormFieldBlock(
|
||||
label: '결재 ID',
|
||||
child: ShadInput(
|
||||
controller: _approvalIdController,
|
||||
readOnly: true,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_FormFieldBlock(
|
||||
label: '결재번호',
|
||||
child: ShadInput(
|
||||
controller: _approvalNoController,
|
||||
readOnly: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (!widget.isEditing) const SizedBox(height: 16),
|
||||
_FormFieldBlock(
|
||||
label: '단계 순서',
|
||||
errorText: _errors['stepOrder'],
|
||||
child: ShadInput(
|
||||
key: const ValueKey('step_form_step_order'),
|
||||
controller: _stepOrderController,
|
||||
onChanged: (_) => _clearError('stepOrder'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_FormFieldBlock(
|
||||
label: '승인자 ID',
|
||||
errorText: _errors['approverId'],
|
||||
child: ShadInput(
|
||||
key: const ValueKey('step_form_approver_id'),
|
||||
controller: _approverIdController,
|
||||
onChanged: (_) => _clearError('approverId'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_FormFieldBlock(
|
||||
label: '비고',
|
||||
helperText: '필요 시 단계에 대한 참고 내용을 남길 수 있습니다.',
|
||||
child: ShadTextarea(
|
||||
key: const ValueKey('step_form_note'),
|
||||
controller: _noteController,
|
||||
minHeight: 100,
|
||||
maxHeight: 200,
|
||||
),
|
||||
),
|
||||
if (_errors['form'] != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: Text(
|
||||
_errors['form']!,
|
||||
style: theme.textTheme.small.copyWith(
|
||||
color: materialTheme.colorScheme.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleSubmit() {
|
||||
final Map<String, String?> nextErrors = {};
|
||||
int? approvalId;
|
||||
if (widget.isEditing) {
|
||||
approvalId = widget.initialRecord?.approvalId;
|
||||
} else {
|
||||
approvalId = int.tryParse(_approvalIdController.text.trim());
|
||||
if (approvalId == null || approvalId <= 0) {
|
||||
nextErrors['approvalId'] = '결재 ID를 1 이상의 숫자로 입력하세요.';
|
||||
}
|
||||
}
|
||||
|
||||
final stepOrder = int.tryParse(_stepOrderController.text.trim());
|
||||
if (stepOrder == null || stepOrder <= 0) {
|
||||
nextErrors['stepOrder'] = '단계 순서를 1 이상의 숫자로 입력하세요.';
|
||||
}
|
||||
|
||||
final approverId = int.tryParse(_approverIdController.text.trim());
|
||||
if (approverId == null || approverId <= 0) {
|
||||
nextErrors['approverId'] = '승인자 ID를 1 이상의 숫자로 입력하세요.';
|
||||
}
|
||||
|
||||
setState(() => _errors = nextErrors);
|
||||
if (nextErrors.isNotEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final note = _noteController.text.trim();
|
||||
final input = ApprovalStepInput(
|
||||
approvalId: approvalId,
|
||||
stepOrder: stepOrder!,
|
||||
approverId: approverId!,
|
||||
note: note.isEmpty ? null : note,
|
||||
statusId: widget.initialRecord?.step.status.id,
|
||||
);
|
||||
|
||||
Navigator.of(context).pop(input);
|
||||
}
|
||||
|
||||
void _clearError(String field) {
|
||||
if (_errors[field] == null) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
final updated = Map<String, String?>.from(_errors);
|
||||
updated.remove(field);
|
||||
_errors = updated;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _FormFieldBlock extends StatelessWidget {
|
||||
const _FormFieldBlock({
|
||||
required this.label,
|
||||
this.errorText,
|
||||
this.helperText,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final Widget child;
|
||||
final String? errorText;
|
||||
final String? helperText;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = ShadTheme.of(context);
|
||||
final materialTheme = Theme.of(context);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: theme.textTheme.small.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
child,
|
||||
if (errorText != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 6),
|
||||
child: Text(
|
||||
errorText!,
|
||||
style: theme.textTheme.small.copyWith(
|
||||
color: materialTheme.colorScheme.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (helperText != null && helperText!.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 6),
|
||||
child: Text(helperText!, style: theme.textTheme.muted),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user