- 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).
260 lines
9.8 KiB
Dart
260 lines
9.8 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:superport/screens/common/theme_shadcn.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:shadcn_ui/shadcn_ui.dart';
|
|
import 'controllers/equipment_history_controller.dart';
|
|
|
|
class InventoryDashboard extends StatefulWidget {
|
|
const InventoryDashboard({super.key});
|
|
|
|
@override
|
|
State<InventoryDashboard> createState() => _InventoryDashboardState();
|
|
}
|
|
|
|
class _InventoryDashboardState extends State<InventoryDashboard> {
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
final controller = context.read<EquipmentHistoryController>();
|
|
controller.loadStockStatus(); // 재고 현황 로드
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = ShadTheme.of(context);
|
|
|
|
return Scaffold(
|
|
backgroundColor: theme.colorScheme.background,
|
|
body: Consumer<EquipmentHistoryController>(
|
|
builder: (context, controller, child) {
|
|
if (controller.isLoading) {
|
|
return const Center(
|
|
child: CircularProgressIndicator(),
|
|
);
|
|
}
|
|
|
|
return SingleChildScrollView(
|
|
padding: const EdgeInsets.all(24),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// 헤더
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'재고 대시보드',
|
|
style: theme.textTheme.h2,
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'실시간 재고 현황 및 경고 알림',
|
|
style: theme.textTheme.muted,
|
|
),
|
|
],
|
|
),
|
|
Row(
|
|
children: [
|
|
ShadButton.outline(
|
|
child: const Row(
|
|
children: [
|
|
Icon(Icons.refresh, size: 16),
|
|
SizedBox(width: 8),
|
|
Text('새로고침'),
|
|
],
|
|
),
|
|
onPressed: () {
|
|
controller.loadStockStatus();
|
|
controller.loadHistories();
|
|
},
|
|
),
|
|
const SizedBox(width: 8),
|
|
ShadButton(
|
|
child: const Row(
|
|
children: [
|
|
Icon(Icons.download, size: 16),
|
|
SizedBox(width: 8),
|
|
Text('보고서 다운로드'),
|
|
],
|
|
),
|
|
onPressed: () {
|
|
// 보고서 다운로드 기능
|
|
},
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// 통계 카드
|
|
GridView.count(
|
|
shrinkWrap: true,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
crossAxisCount: MediaQuery.of(context).size.width > 1200 ? 4 : 2,
|
|
mainAxisSpacing: 16,
|
|
crossAxisSpacing: 16,
|
|
childAspectRatio: 1.5,
|
|
children: [
|
|
_buildStatCard(
|
|
theme,
|
|
title: '총 입고',
|
|
value: '${controller.getStockSummary()['totalIn']}',
|
|
unit: '개',
|
|
icon: Icons.input,
|
|
color: Colors.green,
|
|
),
|
|
_buildStatCard(
|
|
theme,
|
|
title: '총 출고',
|
|
value: '${controller.getStockSummary()['totalOut']}',
|
|
unit: '개',
|
|
icon: Icons.output,
|
|
color: Colors.red,
|
|
),
|
|
_buildStatCard(
|
|
theme,
|
|
title: '현재 재고',
|
|
value: '${controller.getStockSummary()['totalStock']}',
|
|
unit: '개',
|
|
icon: Icons.inventory_2,
|
|
color: theme.colorScheme.primary,
|
|
),
|
|
_buildStatCard(
|
|
theme,
|
|
title: '총 거래',
|
|
value: '${controller.histories.length}',
|
|
unit: '건',
|
|
icon: Icons.history,
|
|
color: Colors.blue,
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 32),
|
|
|
|
// 최근 거래 이력 (백엔드 스키마 기반)
|
|
Text(
|
|
'최근 거래 이력',
|
|
style: theme.textTheme.h3,
|
|
),
|
|
const SizedBox(height: 16),
|
|
ShadCard(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: controller.histories.isEmpty
|
|
? const Center(
|
|
child: Text('거래 이력이 없습니다.'),
|
|
)
|
|
: Column(
|
|
children: controller.histories.take(10).map((history) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
flex: 2,
|
|
child: Text(
|
|
DateFormat('MM/dd HH:mm').format(history.transactedAt),
|
|
style: theme.textTheme.small,
|
|
),
|
|
),
|
|
Expanded(
|
|
flex: 1,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: history.transactionType == 'I' ? Colors.green : Colors.red,
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
child: Text(
|
|
history.transactionType == 'I' ? '입고' : '출고',
|
|
style: TextStyle(color: ShadcnTheme.primaryForeground, fontSize: 12),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
flex: 1,
|
|
child: Text(
|
|
'${history.quantity}개',
|
|
style: theme.textTheme.small,
|
|
textAlign: TextAlign.right,
|
|
),
|
|
),
|
|
Expanded(
|
|
flex: 2,
|
|
child: Text(
|
|
history.remark ?? '-',
|
|
style: theme.textTheme.small,
|
|
textAlign: TextAlign.right,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildStatCard(
|
|
ShadThemeData theme, {
|
|
required String title,
|
|
required String value,
|
|
required String unit,
|
|
required IconData icon,
|
|
required Color color,
|
|
}) {
|
|
return ShadCard(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Icon(icon, color: color, size: 24),
|
|
Text(
|
|
unit,
|
|
style: theme.textTheme.muted,
|
|
),
|
|
],
|
|
),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
value,
|
|
style: theme.textTheme.h2,
|
|
),
|
|
Text(
|
|
title,
|
|
style: theme.textTheme.small.copyWith(
|
|
color: theme.colorScheme.mutedForeground,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|