大幅変更&環境バージョンアップ

This commit is contained in:
2024-08-22 14:35:09 +09:00
parent 56e9861c7a
commit dc58dc0584
446 changed files with 29645 additions and 8315 deletions

View File

@ -0,0 +1,11 @@
import 'package:get/get.dart';
import 'package:gifunavi/pages/entry/entry_controller.dart';
import 'package:gifunavi/services/api_service.dart';
class EntryBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<ApiService>(() => ApiService());
Get.lazyPut<EntryController>(() => EntryController());
}
}

View File

@ -0,0 +1,310 @@
// lib/entry/entry_controller.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:gifunavi/model/entry.dart';
import 'package:gifunavi/model/event.dart';
import 'package:gifunavi/model/team.dart';
import 'package:gifunavi/model/category.dart';
import 'package:gifunavi/services/api_service.dart';
import '../index/index_controller.dart';
import 'package:timezone/timezone.dart' as tz;
class EntryController extends GetxController {
late ApiService _apiService;
final entries = <Entry>[].obs;
final events = <Event>[].obs;
final teams = <Team>[].obs;
final categories = <NewCategory>[].obs;
final selectedEvent = Rx<Event?>(null);
final selectedTeam = Rx<Team?>(null);
final selectedCategory = Rx<NewCategory?>(null);
final selectedDate = Rx<DateTime?>(null);
final currentEntry = Rx<Entry?>(null);
final isLoading = true.obs;
@override
void onInit() async {
super.onInit();
await initializeApiService();
await loadInitialData();
}
Future<void> initializeApiService() async {
try {
_apiService = await Get.putAsync(() => ApiService().init());
} catch (e) {
print('Error initializing ApiService: $e');
Get.snackbar('Error', 'Failed to initialize API service');
}
}
Future<void> loadInitialData() async {
try {
isLoading.value = true;
await Future.wait([
fetchEntries(),
fetchEvents(),
fetchTeams(),
fetchCategories(),
]);
if (Get.arguments != null && Get.arguments['entry'] != null) {
currentEntry.value = Get.arguments['entry'];
initializeEditMode(currentEntry.value!);
} else {
// 新規作成モードの場合、最初のイベントを選択
if (events.isNotEmpty) {
selectedEvent.value = events.first;
selectedDate.value = events.first.startDatetime;
}
}
} catch(e) {
print('Error initializing data: $e');
Get.snackbar('Error', 'Failed to load initial data');
} finally {
isLoading.value = false;
}
}
void initializeEditMode(Entry entry) {
currentEntry.value = entry;
selectedEvent.value = entry.event;
selectedTeam.value = entry.team;
selectedCategory.value = entry.category;
selectedDate.value = entry.date;
}
void updateEvent(Event? value) {
selectedEvent.value = value;
if (value != null) {
// イベント変更時に日付を調整
if (selectedDate.value == null ||
selectedDate.value!.isBefore(value.startDatetime) ||
selectedDate.value!.isAfter(value.endDatetime)) {
selectedDate.value = value.startDatetime;
}
}
}
void updateTeam(Team? value) {
selectedTeam.value = value;
if (value != null) {
selectedCategory.value = value.category;
}
}
//void updateTeam(Team? value) => selectedTeam.value = value;
void updateCategory(NewCategory? value) => selectedCategory.value = value;
//void updateDate(DateTime value) => selectedDate.value = value;
void updateDate(DateTime value) {
selectedDate.value = tz.TZDateTime.from(value, tz.getLocation('Asia/Tokyo'));
}
/*
void updateDate(DateTime value){
selectedDate.value = DateFormat('yyyy-MM-dd').format(value!) as DateTime?;
}
*/
void _initializeEntryData() {
if (currentEntry.value != null) {
selectedEvent.value = currentEntry.value!.event;
selectedTeam.value = currentEntry.value!.team;
selectedCategory.value = currentEntry.value!.category;
selectedDate.value = currentEntry.value!.date;
}
}
Future<void> fetchEntries() async {
try {
final fetchedEntries = await _apiService.getEntries();
entries.assignAll(fetchedEntries);
} catch (e) {
print('Error fetching entries: $e');
Get.snackbar('Error', 'Failed to fetch entries');
}
}
Future<void> fetchEvents() async {
try {
final fetchedEvents = await _apiService.getEvents();
events.assignAll(fetchedEvents.map((event) {
// end_dateの7日前を締め切りとして設定
final deadlineDateTime = event.endDatetime.subtract(const Duration(days: 7));
return Event(
id: event.id,
eventName: event.eventName,
startDatetime: event.startDatetime,
endDatetime: event.endDatetime,
deadlineDateTime: deadlineDateTime,
);
}).toList());
} catch (e) {
print('Error fetching events: $e');
Get.snackbar('Error', 'Failed to fetch events');
}
}
Future<void> fetchEvents_old() async {
try {
final fetchedEvents = await _apiService.getEvents();
events.assignAll(fetchedEvents);
} catch (e) {
print('Error fetching events: $e');
Get.snackbar('Error', 'Failed to fetch events');
}
}
Future<void> fetchTeams() async {
try {
final fetchedTeams = await _apiService.getTeams();
teams.assignAll(fetchedTeams);
} catch (e) {
print('Error fetching teams: $e');
Get.snackbar('Error', 'Failed to fetch team');
}
}
Future<void> fetchCategories() async {
try {
final fetchedCategories = await _apiService.getCategories();
categories.assignAll(fetchedCategories);
} catch (e) {
print('Error fetching categories: $e');
Get.snackbar('Error', 'Failed to fetch categories');
}
}
Future<void> createEntry() async {
if (selectedEvent.value == null || selectedTeam.value == null ||
selectedCategory.value == null || selectedDate.value == null) {
Get.snackbar('Error', 'Please fill all fields');
return;
}
try {
isLoading.value = true;
// Get zekken number
final updatedCategory = await _apiService.getZekkenNumber(selectedCategory.value!.id);
final zekkenNumber = updatedCategory.categoryNumber.toString();
final newEntry = await _apiService.createEntry(
selectedTeam.value!.id,
selectedEvent.value!.id,
selectedCategory.value!.id,
selectedDate.value!,
zekkenNumber,
);
entries.add(newEntry);
Get.back();
} catch (e) {
print('Error creating entry: $e');
Get.snackbar('Error', 'Failed to create entry');
} finally {
isLoading.value = false;
}
}
Future<void> updateEntryAndRefreshMap() async {
await updateEntry();
// エントリーが正常に更新された後、マップをリフレッシュ
final indexController = Get.find<IndexController>();
final eventCode = currentEntry.value?.event.eventName ?? '';
indexController.reloadMap(eventCode);
}
bool isEntryEditable(Event event) {
return DateTime.now().isBefore(event.deadlineDateTime);
}
Future<void> updateEntry() async {
if (currentEntry.value == null) {
Get.snackbar('Error', 'No entry selected for update');
return;
}
if (!isEntryEditable(currentEntry.value!.event)) {
Get.dialog(
AlertDialog(
title: Text('エントリー変更不可'),
content: Text('締め切りを過ぎているため、エントリーの変更はできません。変更が必要な場合は事務局にお問い合わせください。'),
actions: [
TextButton(
child: Text('OK'),
onPressed: () => Get.back(),
),
],
),
);
return;
}
try {
isLoading.value = true;
final updatedEntry = await _apiService.updateEntry(
currentEntry.value!.id,
currentEntry.value!.team.id,
selectedEvent.value!.id,
selectedCategory.value!.id,
selectedDate.value!,
currentEntry.value!.zekkenNumber,
);
final index = entries.indexWhere((entry) => entry.id == updatedEntry.id);
if (index != -1) {
entries[index] = updatedEntry;
}
Get.back();
} catch (e) {
print('Error updating entry: $e');
Get.snackbar('Error', 'Failed to update entry');
} finally {
isLoading.value = false;
}
}
Future<void> updateEntryCategory(int entryId, int newCategoryId) async {
try {
//await _apiService.updateEntryCategory(entryId, newCategoryId);
final updatedEntry = await _apiService.updateEntry(
currentEntry.value!.id,
currentEntry.value!.team.id,
selectedEvent.value!.id,
newCategoryId,
currentEntry.value!.date!,
currentEntry.value!.zekkenNumber,
);
await fetchEntries();
} catch (e) {
print('Error updating entry category: $e');
Get.snackbar('エラー', 'エントリーのカテゴリ更新に失敗しました');
}
}
Future<void> deleteEntry() async {
if (currentEntry.value == null) {
Get.snackbar('Error', 'No entry selected for deletion');
return;
}
try {
isLoading.value = true;
await _apiService.deleteEntry(currentEntry.value!.id);
entries.removeWhere((entry) => entry.id == currentEntry.value!.id);
Get.back();
} catch (e) {
print('Error deleting entry: $e');
Get.snackbar('Error', 'Failed to delete entry');
} finally {
isLoading.value = false;
}
}
bool isOwner() {
// Implement logic to check if the current user is the owner of the entry
return true; // Placeholder
}
}

View File

@ -0,0 +1,193 @@
// lib/pages/entry/entry_detail_page.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:gifunavi/pages/entry/entry_controller.dart';
import 'package:gifunavi/model/event.dart';
import 'package:gifunavi/model/category.dart';
import 'package:gifunavi/model/team.dart';
import 'package:intl/intl.dart';
import 'package:timezone/timezone.dart' as tz;
class EntryDetailPage extends GetView<EntryController> {
const EntryDetailPage({super.key});
@override
Widget build(BuildContext context) {
final Map<String, dynamic> arguments = Get.arguments ?? {};
final mode = Get.arguments['mode'] as String? ?? 'new';
final entry = Get.arguments['entry'];
if (mode == 'edit' && entry != null) {
controller.initializeEditMode(entry);
}
return Scaffold(
appBar: AppBar(
title: Text(mode == 'new' ? 'エントリー登録' : 'エントリー詳細'),
),
body: Obx(() {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
return Padding(
padding: const EdgeInsets.all(16.0),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDropdown<Event>(
label: 'イベント',
items: controller.events,
selectedId: controller.selectedEvent.value?.id,
onChanged: (eventId) => controller.updateEvent(
controller.events.firstWhere((e) => e.id == eventId)
),
getDisplayName: (event) => event.eventName,
getId: (event) => event.id,
),
const SizedBox(height: 16),
_buildDropdown<Team>(
label: 'チーム',
items: controller.teams,
selectedId: controller.selectedTeam.value?.id,
onChanged: (teamId) => controller.updateTeam(
controller.teams.firstWhere((t) => t.id == teamId)
),
getDisplayName: (team) => team.teamName,
getId: (team) => team.id,
),
const SizedBox(height: 16),
_buildCategoryDropdown(),
/*
_buildDropdown<NewCategory>()
label: 'カテゴリ',
items: controller.categories,
selectedId: controller.selectedCategory.value?.id,
onChanged: (categoryId) => controller.updateCategory(
controller.categories.firstWhere((c) => c.id == categoryId)
),
getDisplayName: (category) => category.categoryName,
getId: (category) => category.id,
),
*/
const SizedBox(height: 16),
ListTile(
title: const Text('日付'),
subtitle: Text(
controller.selectedDate.value != null
? DateFormat('yyyy-MM-dd').format(tz.TZDateTime.from(controller.selectedDate.value!, tz.getLocation('Asia/Tokyo')))
: '日付を選択してください',
),
onTap: () async {
if (controller.selectedEvent.value == null) {
Get.snackbar('Error', 'Please select an event first');
return;
}
final tz.TZDateTime now = tz.TZDateTime.now(tz.getLocation('Asia/Tokyo'));
final tz.TZDateTime eventStart = tz.TZDateTime.from(controller.selectedEvent.value!.startDatetime, tz.getLocation('Asia/Tokyo'));
final tz.TZDateTime eventEnd = tz.TZDateTime.from(controller.selectedEvent.value!.endDatetime, tz.getLocation('Asia/Tokyo'));
final tz.TZDateTime initialDate = controller.selectedDate.value != null
? tz.TZDateTime.from(controller.selectedDate.value!, tz.getLocation('Asia/Tokyo'))
: (now.isAfter(eventStart) ? now : eventStart);
// 選択可能な最初の日付を設定(今日かイベント開始日のうち、より後の日付)
final tz.TZDateTime firstDate = now.isAfter(eventStart) ? now : eventStart;
final DateTime? picked = await showDatePicker(
context: context,
initialDate: initialDate.isAfter(firstDate) ? initialDate : firstDate,
firstDate: firstDate,
lastDate: eventEnd,
);
if (picked != null) {
controller.updateDate(tz.TZDateTime.from(picked, tz.getLocation('Asia/Tokyo')));
}
},
),
const SizedBox(height: 32),
if (mode == 'new')
ElevatedButton(
onPressed: () => controller.createEntry(),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 50),
),
child: const Text('エントリーを作成'),
)
else
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () => controller.deleteEntry(),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
minimumSize: const Size(0, 50),
),
child: const Text('エントリーを削除'),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: () => controller.updateEntry(),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.lightBlue,
foregroundColor: Colors.white,
minimumSize: const Size(0, 50),
),
child: const Text('エントリーを更新'),
),
),
],
),
],
),
),
);
}),
);
}
Widget _buildDropdown<T>({
required String label,
required List<T> items,
required int? selectedId,
required void Function(int?) onChanged,
required String Function(T) getDisplayName,
required int Function(T) getId,
}) {
return DropdownButtonFormField<int>(
decoration: InputDecoration(labelText: label),
value: selectedId,
items: items.map((item) => DropdownMenuItem<int>(
value: getId(item),
child: Text(getDisplayName(item)),
)).toList(),
onChanged: onChanged,
);
}
Widget _buildCategoryDropdown() {
final eligibleCategories = controller.categories.where((c) =>
c.baseCategory == controller.selectedCategory.value?.baseCategory
).toList();
return DropdownButtonFormField<NewCategory>(
decoration: InputDecoration(labelText: 'カテゴリ'),
value: controller.selectedCategory.value,
items: eligibleCategories.map((category) => DropdownMenuItem<NewCategory>(
value: category,
child: Text(category.categoryName),
)).toList(),
onChanged: (value) => controller.updateCategory(value),
);
}
}

View File

@ -0,0 +1,116 @@
// lib/pages/entry/entry_list_page.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:gifunavi/pages/entry/entry_controller.dart';
import 'package:gifunavi/routes/app_pages.dart';
import 'package:timezone/timezone.dart' as tz;
class EntryListPage extends GetView<EntryController> {
const EntryListPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('エントリー管理'),
actions: [
IconButton(
icon: const Icon(Icons.add),
onPressed: () => Get.toNamed(AppPages.ENTRY_DETAIL, arguments: {'mode': 'new'}),
),
],
),
body: Obx(() {
if (controller.entries.isEmpty) {
return const Center(
child: Text('表示するエントリーがありません。'),
);
}
return ListView.builder(
itemCount: controller.entries.length,
itemBuilder: (context, index) {
final entry = controller.entries[index];
return ListTile(
title: Row(
children: [
Expanded(
child: Text('${_formatDate(entry.date)}: ${entry.event.eventName}'),
),
Text(entry.team.teamName, style: const TextStyle(fontWeight: FontWeight.bold)),
],
),
subtitle: Row(
children: [
Expanded(
child: Text('カテゴリー: ${entry.category.categoryName}'),
),
Text('ゼッケン: ${entry.zekkenNumber ?? "未設定"}'),
],
),
onTap: () =>
Get.toNamed(AppPages.ENTRY_DETAIL,
arguments: {'mode': 'edit', 'entry': entry}),
);
},
);
}),
);
}
String _formatDate(DateTime? date) {
if (date == null) {
return '日時未設定';
}
final jstDate = tz.TZDateTime.from(date, tz.getLocation('Asia/Tokyo'));
return DateFormat('yyyy-MM-dd').format(jstDate);
}
}
class EntryListPage_old extends GetView<EntryController> {
const EntryListPage_old({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('エントリー管理'),
actions: [
IconButton(
icon: const Icon(Icons.add),
onPressed: () => Get.toNamed(AppPages.ENTRY_DETAIL, arguments: {'mode': 'new'}),
),
],
),
body: Obx((){
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
// エントリーを日付昇順にソート
final sortedEntries = controller.entries.toList()
..sort((a, b) => (a.date ?? DateTime(0)).compareTo(b.date ?? DateTime(0)));
return ListView.builder(
itemCount: sortedEntries.length,
itemBuilder: (context, index) {
final entry = sortedEntries[index];
return ListTile(
title: Text(entry.event.eventName ?? 'イベント未設定'),
subtitle: Text(
'${entry.team.teamName ?? 'チーム未設定'} - ${entry.category
.categoryName ?? 'カテゴリ未設定'}'),
trailing: Text(
entry.date?.toString().substring(0, 10) ?? '日付未設定'),
onTap: () =>
Get.toNamed(AppPages.ENTRY_DETAIL,
arguments: {'mode': 'edit', 'entry': entry}),
);
},
);
}),
);
}
}

View File

@ -0,0 +1,9 @@
import 'package:get/get.dart';
import 'package:gifunavi/pages/entry/event_entries_controller.dart';
class EventEntriesBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<EventEntriesController>(() => EventEntriesController());
}
}

View File

@ -0,0 +1,129 @@
import 'package:get/get.dart';
import 'package:gifunavi/model/entry.dart';
import 'package:gifunavi/pages/index/index_controller.dart';
import 'package:gifunavi/pages/destination/destination_controller.dart';
import 'package:gifunavi/services/api_service.dart';
import 'package:flutter/material.dart';
import 'package:timezone/timezone.dart' as tz;
import 'package:timezone/data/latest.dart' as tz;
class EventEntriesController extends GetxController {
final ApiService _apiService = Get.find<ApiService>();
final IndexController _indexController = Get.find<IndexController>();
late final DestinationController _destinationController;
final entries = <Entry>[].obs;
final filteredEntries = <Entry>[].obs;
final showTodayEntries = true.obs;
static bool _timezoneInitialized = false;
@override
void onInit() {
super.onInit();
_initializeTimezone();
// DestinationControllerが登録されていない場合に備えて、lazyPutを使用
Get.lazyPut<DestinationController>(() => DestinationController(), fenix: true);
_destinationController = Get.find<DestinationController>();
fetchEntries();
}
void _initializeTimezone() {
if (!_timezoneInitialized) {
tz.initializeTimeZones();
_timezoneInitialized = true;
}
}
Future<void> fetchEntries() async {
try {
final fetchedEntries = await _apiService.getEntries();
entries.assignAll(fetchedEntries);
filterEntries();
} catch (e) {
print('Error fetching entries: $e');
// エラー処理を追加
}
}
void filterEntries() {
if (showTodayEntries.value) {
filterEntriesForToday();
} else {
filteredEntries.assignAll(entries);
}
}
void filterEntriesForToday() {
final now = tz.TZDateTime.now(tz.getLocation('Asia/Tokyo'));
filteredEntries.assignAll(entries.where((entry) {
final entryDate = tz.TZDateTime.from(entry.date!, tz.getLocation('Asia/Tokyo'));
return entryDate.year == now.year &&
entryDate.month == now.month &&
entryDate.day == now.day;
}));
}
void filterEntriesForToday_old() {
final now = DateTime.now();
filteredEntries.assignAll(entries.where((entry) =>
entry.date?.year == now.year &&
entry.date?.month == now.month &&
entry.date?.day == now.day
));
}
void toggleShowTodayEntries() {
showTodayEntries.toggle();
filterEntries();
}
void refreshMap() {
final tk = _indexController.currentUser[0]["token"];
if (tk != null) {
_destinationController.fixMapBound(tk);
}
}
Future<void> joinEvent(Entry entry) async {
//final now = DateTime.now();
final now = tz.TZDateTime.now(tz.getLocation('Asia/Tokyo'));
final entryDate = tz.TZDateTime.from(entry.date!, tz.getLocation('Asia/Tokyo'));
bool isToday = entryDate.year == now.year &&
entryDate.month == now.month &&
entryDate.day == now.day;
_indexController.setReferenceMode(!isToday);
_indexController.setSelectedEventName(entry.event.eventName);
final userid = _indexController.currentUser[0]["user"]["id"];
await _apiService.updateUserInfo(userid,entry);
_indexController.currentUser[0]["user"]["event_date"] = entryDate; // 追加2024-8-9
_indexController.currentUser[0]["user"]["event_code"] = entry.event.eventName;
_indexController.currentUser[0]["user"]["team_name"] = entry.team.teamName;
_indexController.currentUser[0]["user"]["group"] = entry.team.category.categoryName;
_indexController.currentUser[0]["user"]["zekken_number"] = entry.zekkenNumber;
Get.back(); // エントリー一覧ページを閉じる
//_indexController.isLoading.value = true;
_indexController.reloadMap(entry.event.eventName);
refreshMap();
if (isToday) {
Get.snackbar('成功', 'イベントに参加しました。',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white);
} else {
Get.snackbar('参照モード', '過去または未来のイベントを参照しています。ロゲの開始やチェックインはできません。',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.orange,
colorText: Colors.white);
}
}
}

View File

@ -0,0 +1,108 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:gifunavi/pages/entry/event_entries_controller.dart';
import 'package:timezone/timezone.dart' as tz;
class EventEntriesPage_old extends GetView<EventEntriesController> {
const EventEntriesPage_old({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('イベント参加')),
body: Obx(() => ListView.builder(
itemCount: controller.entries.length,
itemBuilder: (context, index) {
final entry = controller.entries[index];
return ListTile(
title: Text(entry.event.eventName),
subtitle: Text('${entry.category.categoryName} - ${entry.date}'),
onTap: () => controller.joinEvent(entry),
);
},
)),
);
}
}
class EventEntriesPage extends GetView<EventEntriesController> {
const EventEntriesPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Obx(() => Text(controller.showTodayEntries.value ? 'イベント参加' : 'イベント参照')),
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Obx(() => Text(
controller.showTodayEntries.value ? '本日のエントリー' : 'すべてのエントリー',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
)),
Obx(() => Switch(
value: !controller.showTodayEntries.value,
onChanged: (value) {
controller.toggleShowTodayEntries();
},
activeColor: Colors.blue,
)),
],
),
),
Expanded(
child: Obx(() {
if (controller.filteredEntries.isEmpty) {
return const Center(
child: Text('表示するエントリーがありません。'),
);
}
return ListView.builder(
itemCount: controller.filteredEntries.length,
itemBuilder: (context, index) {
final entry = controller.filteredEntries[index];
return ListTile(
title: Row(
children: [
Expanded(
child: Text('${_formatDate(entry.date)}: ${entry.event.eventName}'),
),
Text(entry.team.teamName, style: const TextStyle(fontWeight: FontWeight.bold)),
],
),
subtitle: Row(
children: [
Expanded(
child: Text('カテゴリー: ${entry.category.categoryName}'),
),
Text('ゼッケン: ${entry.zekkenNumber ?? "未設定"}'),
],
),
onTap: () async {
await controller.joinEvent(entry);
},
);
},
);
}),
),
],
),
);
}
String _formatDate(DateTime? date) {
if (date == null) {
return '日時未設定';
}
final jstDate = tz.TZDateTime.from(date, tz.getLocation('Asia/Tokyo'));
return DateFormat('yyyy-MM-dd').format(jstDate);
}
}