Files
superport_v2/lib/features/dashboard/presentation/pages/dashboard_page.dart

316 lines
10 KiB
Dart

import 'package:flutter/material.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart' as lucide;
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:superport_v2/widgets/app_layout.dart';
import 'package:superport_v2/widgets/components/empty_state.dart';
/// Superport 메인 대시보드 화면.
class DashboardPage extends StatelessWidget {
const DashboardPage({super.key});
static const _recentTransactions = [
('IN-20240312-003', '2024-03-12', '입고', '승인완료', '김담당'),
('OUT-20240311-005', '2024-03-11', '출고', '출고대기', '이물류'),
('RENT-20240310-001', '2024-03-10', '대여', '대여중', '박대여'),
('APP-20240309-004', '2024-03-09', '결재', '진행중', '최결재'),
];
static const _pendingApprovals = [
('APP-20240312-010', '설비 구매', '2/4 단계 진행 중'),
('APP-20240311-004', '창고 정기 점검', '승인 대기'),
('APP-20240309-002', '계약 연장', '반려 후 재상신'),
];
@override
Widget build(BuildContext context) {
return AppLayout(
title: '대시보드',
subtitle: '입·출·대여 현황과 결재 대기를 한 눈에 확인합니다.',
breadcrumbs: const [AppBreadcrumbItem(label: '대시보드')],
child: SingleChildScrollView(
padding: const EdgeInsets.only(right: 12, bottom: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Wrap(
spacing: 16,
runSpacing: 16,
children: const [
_KpiCard(
icon: lucide.LucideIcons.packagePlus,
label: '오늘 입고',
value: '12건',
trend: '+3 vs 어제',
),
_KpiCard(
icon: lucide.LucideIcons.packageMinus,
label: '오늘 출고',
value: '9건',
trend: '-2 vs 어제',
),
_KpiCard(
icon: lucide.LucideIcons.messageSquareWarning,
label: '결재 대기',
value: '5건',
trend: '평균 12시간 지연',
),
_KpiCard(
icon: lucide.LucideIcons.users,
label: '고객사 문의',
value: '7건',
trend: '지원팀 확인 중',
),
],
),
const SizedBox(height: 24),
LayoutBuilder(
builder: (context, constraints) {
final showSidePanel = constraints.maxWidth > 920;
return Flex(
direction: showSidePanel ? Axis.horizontal : Axis.vertical,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 3,
child: _RecentTransactionsCard(
transactions: _recentTransactions,
),
),
if (showSidePanel)
const SizedBox(width: 16)
else
const SizedBox(height: 16),
Flexible(
flex: 2,
child: _PendingApprovalCard(approvals: _pendingApprovals),
),
],
);
},
),
const SizedBox(height: 24),
const _ReminderPanel(),
],
),
),
);
}
}
class _KpiCard extends StatelessWidget {
const _KpiCard({
required this.icon,
required this.label,
required this.value,
required this.trend,
});
final IconData icon;
final String label;
final String value;
final String trend;
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
return ConstrainedBox(
constraints: const BoxConstraints(minWidth: 220, maxWidth: 260),
child: ShadCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 20, color: theme.colorScheme.primary),
const SizedBox(height: 12),
Text(label, style: theme.textTheme.small),
const SizedBox(height: 6),
Text(value, style: theme.textTheme.h3),
const SizedBox(height: 8),
Text(trend, style: theme.textTheme.muted),
],
),
),
);
}
}
class _RecentTransactionsCard extends StatelessWidget {
const _RecentTransactionsCard({required this.transactions});
final List<(String, String, String, String, String)> transactions;
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
return ShadCard(
title: Text('최근 트랜잭션', style: theme.textTheme.h3),
description: Text(
'최근 7일간의 입·출고 및 대여/결재 흐름입니다.',
style: theme.textTheme.muted,
),
child: SizedBox(
height: 320,
child: ShadTable.list(
header: const [
ShadTableCell.header(child: Text('번호')),
ShadTableCell.header(child: Text('일자')),
ShadTableCell.header(child: Text('유형')),
ShadTableCell.header(child: Text('상태')),
ShadTableCell.header(child: Text('작성자')),
],
children: [
for (final row in transactions)
[
ShadTableCell(child: Text(row.$1)),
ShadTableCell(child: Text(row.$2)),
ShadTableCell(child: Text(row.$3)),
ShadTableCell(child: Text(row.$4)),
ShadTableCell(child: Text(row.$5)),
],
],
columnSpanExtent: (index) => const FixedTableSpanExtent(140),
rowSpanExtent: (index) => const FixedTableSpanExtent(52),
),
),
);
}
}
class _PendingApprovalCard extends StatelessWidget {
const _PendingApprovalCard({required this.approvals});
final List<(String, String, String)> approvals;
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
if (approvals.isEmpty) {
return ShadCard(
title: Text('내 결재 대기', style: theme.textTheme.h3),
description: Text(
'현재 승인 대기 중인 결재 요청입니다.',
style: theme.textTheme.muted,
),
child: const SuperportEmptyState(
title: '대기 중인 결재가 없습니다',
description: '새로운 결재 요청이 등록되면 이곳에서 바로 확인할 수 있습니다.',
),
);
}
return ShadCard(
title: Text('내 결재 대기', style: theme.textTheme.h3),
description: Text('현재 승인 대기 중인 결재 요청입니다.', style: theme.textTheme.muted),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (final approval in approvals)
Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
lucide.LucideIcons.bell,
size: 18,
color: theme.colorScheme.primary,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(approval.$1, style: theme.textTheme.small),
const SizedBox(height: 4),
Text(approval.$2, style: theme.textTheme.h4),
const SizedBox(height: 4),
Text(approval.$3, style: theme.textTheme.muted),
],
),
),
ShadButton.ghost(
size: ShadButtonSize.sm,
child: const Text('상세'),
onPressed: () {},
),
],
),
),
],
),
);
}
}
class _ReminderPanel extends StatelessWidget {
const _ReminderPanel();
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
return ShadCard(
title: Text('주의/알림', style: theme.textTheme.h3),
description: Text(
'지연된 결재나 시스템 점검 일정을 확인하세요.',
style: theme.textTheme.muted,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: const [
_ReminderItem(
icon: lucide.LucideIcons.clock,
label: '결재 지연',
message: '영업부 장비 구매 결재가 2일째 대기 중입니다.',
),
SizedBox(height: 12),
_ReminderItem(
icon: lucide.LucideIcons.triangleAlert,
label: '시스템 점검',
message: '2024-03-15 22:00 ~ 23:00 서버 점검이 예정되어 있습니다.',
),
SizedBox(height: 12),
_ReminderItem(
icon: lucide.LucideIcons.mail,
label: '고객 문의',
message: '3건의 신규 고객 문의가 접수되었습니다.',
),
],
),
);
}
}
class _ReminderItem extends StatelessWidget {
const _ReminderItem({
required this.icon,
required this.label,
required this.message,
});
final IconData icon;
final String label;
final String message;
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 18, color: theme.colorScheme.secondary),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: theme.textTheme.small),
const SizedBox(height: 4),
Text(message, style: theme.textTheme.p),
],
),
),
],
);
}
}