316 lines
10 KiB
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),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|