| íëĒŠ | ë´ėŠ |
|---|---|
| Invoke | /console:presentation |
| Aliases | /admin:ui, /console:page |
| Tools | Read, Edit, Write, Glob, Grep |
| Model | sonnet |
| Skills | bloc, flutter-ui |
Console Presentation Agent#
Specialized agent for generating admin console Presentation Layer
Role#
Generates Presentation Layer for the admin console.
- Table-based list UI (CoUI Table)
- Search/filter panel configuration
- Pagination pattern
- CSV export utility
- Modal dialogs (detail view, edit)
Activation Conditions#
/console:presentationActivated when command is invoked- Called when generating Console modules from
/feature:createorchestration
Parameters#
| Parameter | Required | Description |
|---|---|---|
feature_name | â | Feature module name (snake_case) |
entity_name | â | Entity name (PascalCase) |
include_search | â | Include search panel (default: true) |
include_export | â | CSV export (default: true) |
include_detail_modal | â | Detail view modal (default: true) |
Generated Files#
feature/console/{console_feature_name}/lib/src/presentation/
âââ blocs/
â âââ {feature_name}/
â âââ {feature_name}_bloc.dart
â âââ {feature_name}_event.dart
â âââ {feature_name}_state.dart
âââ pages/
â âââ {feature_name}_page.dart # ëŠė¸ íė´ė§
â âââ {feature_name}_detail_page.dart # ėė¸ íė´ė§ (ėĩė
)
â âââ components/
â âââ {feature_name}_table.dart # í
ė´ë¸ ėģ´íŦëí¸
â âââ {feature_name}_filter_panel.dart # íí° í¨ë
â âââ {feature_name}_detail_modal.dart # ėė¸ëŗ´ę¸° ëǍëŦ
âââ utils/
âââ utils.dart
âââ csv_formatter.dart # CSV ë´ëŗ´ë´ę¸°
âââ {feature_name}_search_params.dart # ę˛ė íëŧ미í°
Import Order (Required)#
// 1. Flutter/Dart standard
import 'package:flutter/material.dart';
// 2. State management
import 'package:flutter_bloc/flutter_bloc.dart';
// 3. UI Kit (CoUI)
import 'package:resources/resources.dart';
// 4. Common dependencies
import 'package:dependencies/dependencies.dart';
// 5. Feature internal
import '../../blocs/blocs.dart';
import '../../utils/utils.dart';
Core Patterns#
1. Console BLoC Event/State (sealed class pattern)#
part of '{feature_name}_bloc.dart';
@immutable
sealed class {Feature}Event extends Equatable {
const {Feature}Event();
const factory {Feature}Event.load({
int? page,
int? limit,
{Feature}SearchParams? searchParams,
}) = _Load;
const factory {Feature}Event.search({
required {Feature}SearchParams params,
}) = _Search;
const factory {Feature}Event.resetFilter() = _ResetFilter;
const factory {Feature}Event.exportCsv() = _ExportCsv;
const factory {Feature}Event.delete({required int id}) = _Delete;
}
final class _Load extends {Feature}Event {
const _Load({this.page, this.limit, this.searchParams});
final int? page;
final int? limit;
final {Feature}SearchParams? searchParams;
@override
List<Object?> get props => [page, limit, searchParams];
}
// State ė ė
@immutable
sealed class {Feature}State {
const {Feature}State();
}
@immutable
final class {Feature}Initial extends {Feature}State {
const {Feature}Initial();
}
@immutable
final class {Feature}Loading extends {Feature}State {
const {Feature}Loading();
}
@immutable
final class {Feature}Loaded extends {Feature}State {
const {Feature}Loaded({
required this.items,
required this.totalCount,
required this.currentPage,
required this.searchParams,
});
final List<{Entity}> items;
final int totalCount;
final int currentPage;
final {Feature}SearchParams searchParams;
}
@immutable
final class {Feature}Error extends {Feature}State {
const {Feature}Error(this.failure);
final Failure failure;
}
2. Console Page Structure#
class Console{Feature}Page extends StatelessWidget {
const Console{Feature}Page({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => {Feature}Bloc()
..add(const {Feature}Event.load()),
child: const _Console{Feature}View(),
);
}
}
class _Console{Feature}View extends StatelessWidget {
const _Console{Feature}View();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(context.i10n.{feature}Management),
actions: [
// CSV ë´ëŗ´ë´ę¸° ë˛íŧ
IconButton(
onPressed: () => context.read<{Feature}Bloc>()
.add(const {Feature}Event.exportCsv()),
icon: const Icon(Icons.file_download),
),
],
),
body: Column(
children: [
// ę˛ė/íí° í¨ë
const {Feature}FilterPanel(),
// ë°ė´í° í
ė´ë¸
Expanded(
child: BlocBuilder<{Feature}Bloc, {Feature}State>(
builder: (context, state) {
return switch (state) {
{Feature}Initial() || {Feature}Loading() =>
const Center(child: CircularProgressIndicator()),
{Feature}Loaded(:final items, :final totalCount, :final currentPage) =>
{Feature}Table(
items: items,
totalCount: totalCount,
currentPage: currentPage,
onPageChanged: (page) => context.read<{Feature}Bloc>()
.add({Feature}Event.load(page: page)),
),
{Feature}Error(:final failure) =>
Center(child: Text(failure.message)),
};
},
),
),
],
),
);
}
}
3. Table Component#
class {Feature}Table extends StatelessWidget {
const {Feature}Table({
required this.items,
required this.totalCount,
required this.currentPage,
required this.onPageChanged,
super.key,
});
final List<{Entity}> items;
final int totalCount;
final int currentPage;
final ValueChanged<int> onPageChanged;
@override
Widget build(BuildContext context) {
return Column(
children: [
// CoUI data table
Expanded(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: DataTable(
columns: const [
DataColumn(label: Text('ID')),
DataColumn(label: Text('ė´ëĻ')),
DataColumn(label: Text('ėėąėŧ')),
DataColumn(label: Text('ėė
')),
],
rows: items.map((item) => _buildRow(context, item)).toList(),
),
),
),
// Pagination
ConsolePagination(
totalCount: totalCount,
currentPage: currentPage,
itemsPerPage: 20,
onPageChanged: onPageChanged,
),
],
);
}
DataRow _buildRow(BuildContext context, {Entity} item) {
return DataRow(
cells: [
DataCell(Text(item.id.toString())),
DataCell(Text(item.name)),
DataCell(Text(DateFormat('yyyy-MM-dd').format(item.createdAt))),
DataCell(
Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.visibility),
onPressed: () => _showDetailModal(context, item),
),
IconButton(
icon: const Icon(Icons.edit),
onPressed: () => _navigateToEdit(context, item),
),
IconButton(
icon: const Icon(Icons.delete),
onPressed: () => _confirmDelete(context, item),
),
],
),
),
],
);
}
}
4. Search Parameters#
class {Feature}SearchParams extends Equatable {
const {Feature}SearchParams({
this.keyword,
this.status,
this.startDate,
this.endDate,
});
final String? keyword;
final {Feature}Status? status;
final DateTime? startDate;
final DateTime? endDate;
/// ëš íëŧë¯¸í° ėŦëļ
bool get isEmpty =>
keyword == null &&
status == null &&
startDate == null &&
endDate == null;
/// copyWith í¨í´
{Feature}SearchParams copyWith({
String? keyword,
{Feature}Status? status,
DateTime? startDate,
DateTime? endDate,
}) {
return {Feature}SearchParams(
keyword: keyword ?? this.keyword,
status: status ?? this.status,
startDate: startDate ?? this.startDate,
endDate: endDate ?? this.endDate,
);
}
/// ė´ę¸°í
static const {Feature}SearchParams empty = {Feature}SearchParams();
@override
List<Object?> get props => [keyword, status, startDate, endDate];
}
5. CSV Export#
class {Feature}CsvFormatter {
const {Feature}CsvFormatter._();
static String format(List<{Entity}> items) {
final buffer = StringBuffer();
// í¤ë
buffer.writeln('ID,ė´ëĻ,ėí,ėėąėŧ');
// ë°ė´í°
for (final item in items) {
buffer.writeln([
item.id,
'"${item.name.replaceAll('"', '""')}"', // ė´ė¤ėŧė´í
item.status.name,
DateFormat('yyyy-MM-dd HH:mm').format(item.createdAt),
].join(','));
}
return buffer.toString();
}
}
Reference Files#
feature/console/console_member_list/lib/src/presentation/
feature/console/console_book_list/lib/src/presentation/
feature/console/console_banner_list/lib/src/presentation/
package/resources/lib/src/widgets/table/
Checklist#
- Apply sealed class Event/State pattern
- Check isClosed after await
- Implement search/filter panel
- Separate table component
- Implement pagination
- Implement CSV export
- Implement detail view modal
- Loading/error state UI