Files
jrshikoku/components/ActionSheetComponents/EachTrainInfoCore/TrainSourcesPanel.tsx

540 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { FC, useMemo } from "react";
import {
View,
Text,
TouchableOpacity,
StyleSheet,
ScrollView,
Image,
} from "react-native";
import { MaterialCommunityIcons, Ionicons } from "@expo/vector-icons";
import { SheetManager } from "react-native-actions-sheet";
import type { NavigateFunction } from "@/types";
import type { OperationLogs } from "@/lib/CommonTypes";
import type { UnyohubData } from "@/types/unyohub";
const HUB_LOGO_PNG = require("@/assets/icons/hub_logo.png");
type Props = {
trainNum: string;
unyohubEntries: UnyohubData[];
todayOperation: OperationLogs[];
navigate: NavigateFunction;
expoPushToken: string;
onClose: () => void;
/** true = 上り (vehicle_img) / false = 下り (vehicle_img_right) */
direction: boolean;
};
/** 情報ソース別カードを並べて表示するパネル */
export const TrainSourcesPanel: FC<Props> = ({
trainNum,
unyohubEntries,
todayOperation,
navigate,
expoPushToken,
onClose,
direction,
}) => {
// 連結番号train_ids のカンマ後数値)で昇順ソート
const sortedTodayOperation = useMemo(() => {
const extractOrder = (trainId: string): number => {
const parts = trainId.split(",");
if (parts.length > 1) {
const n = parseInt(parts[1].trim(), 10);
return isNaN(n) ? Infinity : n;
}
return Infinity;
};
const findId = (op: OperationLogs): string | null => {
for (const id of [...(op.train_ids ?? []), ...(op.related_train_ids ?? [])]) {
if (id.split(",")[0] === trainNum) return id;
}
return null;
};
return [...todayOperation].sort((a, b) => {
const aId = findId(a);
const bId = findId(b);
if (!aId || !bId) return aId ? -1 : bId ? 1 : 0;
return extractOrder(aId) - extractOrder(bId);
});
}, [todayOperation, trainNum]);
return (
<View style={styles.container}>
{/* ヘッダー */}
<View style={styles.panelHeader}>
<Text style={styles.panelHeaderText}></Text>
<TouchableOpacity onPress={onClose} style={styles.closeButton}>
<Ionicons name="close" size={20} color="#0099CC" />
</TouchableOpacity>
</View>
<ScrollView
style={styles.scroll}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
{/* ──────────────────────────────────────── */}
{/* JR四国データベース常に表示 */}
{/* ──────────────────────────────────────── */}
<SourceCard
iconName="database"
iconColor="#0099CC"
tag="公式"
tagColor="#0099CC"
title="JR四国データベース"
description="コミュニティが管理する列車データ。編成・運用の詳細を確認・編集できます。"
available
onPress={() => {
const uri = `https://jr-shikoku-data-system.pages.dev/trainData/${trainNum}?userID=${expoPushToken}&from=eachTrainInfo`;
navigate("generalWebView", { uri, useExitButton: false });
SheetManager.hide("EachTrainInfo");
onClose();
}}
/>
{/* ──────────────────────────────────────── */}
{/* 鉄道運用Hub */}
{/* ──────────────────────────────────────── */}
{unyohubEntries.length > 0 ? (
<>
<SectionLabel label="鉄道運用Hub" imagePng={HUB_LOGO_PNG} />
{unyohubEntries.map((entry, i) => (
<UnyohubCard key={`unyo-${i}`} entry={entry} trainNum={trainNum} navigate={navigate} onClose={onClose} />
))}
</>
) : (
<SourceCard
iconImagePng={HUB_LOGO_PNG}
iconColor="#333"
tag="運用Hub"
tagColor="#555"
title="鉄道運用Hub"
description="この列車の運用データはありません。"
available={false}
/>
)}
{/* ──────────────────────────────────────── */}
{/* 今日の運用情報 */}
{/* ──────────────────────────────────────── */}
{sortedTodayOperation.length > 0 ? (
<>
<SectionLabel label="今日の運用情報" icon="history" />
<DirectionBanner direction={direction} />
{sortedTodayOperation.map((op, i) => (
<OperationCard
key={`op-${i}`}
op={op}
index={i}
direction={direction}
navigate={navigate}
onClose={onClose}
/>
))}
</>
) : (
<SourceCard
iconName="history"
iconColor="#aaa"
tag="記録"
tagColor="#aaa"
title="今日の運用情報"
description="本日の運用記録はありません。"
available={false}
/>
)}
</ScrollView>
</View>
);
};
/* ------------------------------------------------------------------ */
/* サブコンポーネント */
/* ------------------------------------------------------------------ */
/** セクションラベル */
const SectionLabel: FC<{ label: string; icon?: string; imagePng?: any }> = ({ label, icon, imagePng }) => (
<View style={styles.sectionLabel}>
{imagePng ? (
<Image source={imagePng} style={{ width: 14, height: 14 }} />
) : (
<MaterialCommunityIcons name={(icon ?? "information") as any} size={14} color="#888" />
)}
<Text style={styles.sectionLabelText}>{label}</Text>
</View>
);
/** 汎用ソースカード */
const SourceCard: FC<{
iconName?: string;
iconImagePng?: any;
iconColor: string;
tag: string;
tagColor: string;
title: string;
description: string;
available: boolean;
onPress?: () => void;
}> = ({ iconName, iconImagePng, iconColor, tag, tagColor, title, description, available, onPress }) => (
<TouchableOpacity
style={[styles.card, !available && styles.cardDisabled]}
onPress={available ? onPress : undefined}
activeOpacity={available ? 0.7 : 1}
>
<View style={[styles.cardIconWrap, { backgroundColor: iconColor + "22" }]}>
{iconImagePng ? (
<Image source={iconImagePng} style={{ width: 24, height: 24 }} />
) : (
<MaterialCommunityIcons name={(iconName ?? "information") as any} size={22} color={iconColor} />
)}
</View>
<View style={styles.cardBody}>
<View style={styles.cardTitleRow}>
<Text style={[styles.cardTitle, !available && styles.cardTitleDisabled]}>{title}</Text>
<View style={[styles.tag, { backgroundColor: tagColor + "33" }]}>
<Text style={[styles.tagText, { color: tagColor }]}>{tag}</Text>
</View>
</View>
<Text style={styles.cardDesc}>{description}</Text>
</View>
{available && (
<MaterialCommunityIcons name="chevron-right" size={18} color="#ccc" />
)}
</TouchableOpacity>
);
/** 鉄道運用Hubカード1編成分 */
const UnyohubCard: FC<{
entry: UnyohubData;
trainNum: string;
navigate: NavigateFunction;
onClose: () => void;
}> = ({ entry, trainNum, navigate, onClose }) => {
const matchedTrain = entry.trains?.find((t) => t.train_number === trainNum);
const positionText = matchedTrain
? `位置: ${matchedTrain.position_forward}${matchedTrain.position_rear}両目`
: null;
const carInfo = `${entry.car_count}両 (${entry.min_car_count}${entry.max_car_count}両)`;
const timeInfo =
entry.starting_time && entry.ending_time
? `${entry.starting_time}${entry.ending_time}`
: null;
return (
<TouchableOpacity
style={styles.card}
activeOpacity={0.7}
onPress={() => {
// 編成番号+列番でページを開く将来的にunyohub公式ページへの対応も想定
const uri = `https://jr-shikoku-data-system.pages.dev/trainData/${trainNum}?formation=${encodeURIComponent(entry.formations)}&source=unyohub`;
navigate("generalWebView", { uri, useExitButton: true });
SheetManager.hide("EachTrainInfo");
onClose();
}}
>
{/* 編成色インジケーター */}
<View
style={[
styles.cardIconWrap,
{ backgroundColor: "#33333318" },
]}
>
<Image source={HUB_LOGO_PNG} style={{ width: 24, height: 24 }} />
</View>
<View style={styles.cardBody}>
<View style={styles.cardTitleRow}>
<Text style={styles.cardTitle}>{entry.formations}</Text>
<View style={[styles.tag, { backgroundColor: "#FF980033" }]}>
<Text style={[styles.tagText, { color: "#FF9800" }]}>Hub</Text>
</View>
{entry.from_beginner && (
<View style={[styles.tag, { backgroundColor: "#9C27B033" }]}>
<Text style={[styles.tagText, { color: "#9C27B0" }]}></Text>
</View>
)}
</View>
{/* 詳細行 */}
<View style={styles.unyoDetailRow}>
{positionText && (
<DetailChip icon="seat" text={positionText} />
)}
<DetailChip icon="train-car" text={carInfo} />
{timeInfo && <DetailChip icon="clock-outline" text={timeInfo} />}
{entry.starting_location && (
<DetailChip icon="map-marker" text={entry.starting_location} />
)}
</View>
{entry.comment && (
<Text style={styles.unyoComment}>{entry.comment}</Text>
)}
<View style={styles.postsRow}>
<MaterialCommunityIcons name="account-group" size={12} color="#888" />
<Text style={styles.postsText}>{entry.posts_count}稿</Text>
</View>
</View>
<MaterialCommunityIcons name="chevron-right" size={18} color="#ccc" />
</TouchableOpacity>
);
};
/** 進行方向バナー */
const DirectionBanner: FC<{ direction: boolean }> = ({ direction }) => (
<View style={styles.directionBanner}>
{direction ? (
// 上り:矢印は左向き(← 先頭)
<>
<MaterialCommunityIcons name="arrow-left" size={14} color="#0099CC" />
<Text style={styles.directionText}></Text>
</>
) : (
// 下り:矢印は右向き(先頭 →)
<>
<Text style={styles.directionText}></Text>
<MaterialCommunityIcons name="arrow-right" size={14} color="#0099CC" />
</>
)}
</View>
);
/** 今日の運用情報カード */
const OperationCard: FC<{
op: OperationLogs;
index: number;
direction: boolean;
navigate: NavigateFunction;
onClose: () => void;
}> = ({ op, index, direction, navigate, onClose }) => {
// 進行方向に応じて車両画像を選択trainIconStatus.tsx と同ロジック)
const thumbUri = direction
? (op.vehicle_img || op.vehicle_img_right)
: (op.vehicle_img_right || op.vehicle_img);
const hasUrl = !!op.vehicle_info_url;
const trainIds = [...(op.train_ids || []), ...(op.related_train_ids || [])];
return (
<TouchableOpacity
style={[styles.card, !hasUrl && styles.cardNoArrow]}
activeOpacity={hasUrl ? 0.7 : 1}
onPress={
hasUrl
? () => {
navigate("howto", { info: op.vehicle_info_url, goTo: "menu" });
SheetManager.hide("EachTrainInfo");
onClose();
}
: undefined
}
>
{/* 車両サムネイル or アイコン(進行方向考慮) */}
{thumbUri ? (
<Image
source={{ uri: thumbUri }}
style={styles.vehicleThumb}
resizeMode="contain"
/>
) : (
<View style={[styles.cardIconWrap, { backgroundColor: "#4CAF5022" }]}>
<MaterialCommunityIcons name="train" size={22} color="#4CAF50" />
</View>
)}
<View style={styles.cardBody}>
<View style={styles.cardTitleRow}>
<Text style={styles.cardTitle}> #{index + 1}</Text>
<View style={[styles.tag, { backgroundColor: "#4CAF5033" }]}>
<Text style={[styles.tagText, { color: "#4CAF50" }]}></Text>
</View>
</View>
{trainIds.length > 0 && (
<Text style={styles.cardDesc} numberOfLines={1}>
: {trainIds.slice(0, 4).join(" / ")}
{trainIds.length > 4 ? " ..." : ""}
</Text>
)}
{op.date && (
<Text style={styles.cardDesc}>: {op.date}</Text>
)}
</View>
{hasUrl && (
<MaterialCommunityIcons name="chevron-right" size={18} color="#ccc" />
)}
</TouchableOpacity>
);
};
/** 小さい詳細チップ */
const DetailChip: FC<{ icon: string; text: string }> = ({ icon, text }) => (
<View style={styles.chip}>
<MaterialCommunityIcons name={icon as any} size={11} color="#666" />
<Text style={styles.chipText}>{text}</Text>
</View>
);
/* ------------------------------------------------------------------ */
/* スタイル */
/* ------------------------------------------------------------------ */
const styles = StyleSheet.create({
container: {
backgroundColor: "#fff",
borderTopWidth: 1,
borderTopColor: "#e0e0e0",
},
panelHeader: {
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 14,
paddingVertical: 10,
borderBottomWidth: 1,
borderBottomColor: "#f0f0f0",
},
panelHeaderText: {
flex: 1,
fontSize: 14,
fontWeight: "bold",
color: "#333",
},
closeButton: {
padding: 4,
},
scroll: {
maxHeight: 360,
},
scrollContent: {
paddingHorizontal: 12,
paddingVertical: 8,
paddingBottom: 16,
},
sectionLabel: {
flexDirection: "row",
alignItems: "center",
marginTop: 10,
marginBottom: 4,
gap: 4,
},
sectionLabelText: {
fontSize: 12,
color: "#888",
fontWeight: "600",
letterSpacing: 0.5,
},
card: {
flexDirection: "row",
alignItems: "center",
backgroundColor: "#f9f9f9",
borderRadius: 10,
marginBottom: 8,
padding: 10,
borderWidth: 1,
borderColor: "#eeeeee",
gap: 10,
},
cardDisabled: {
opacity: 0.5,
},
cardNoArrow: {
// no change needed
},
cardIconWrap: {
width: 40,
height: 40,
borderRadius: 10,
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
},
vehicleThumb: {
width: 50,
height: 40,
borderRadius: 6,
flexShrink: 0,
backgroundColor: "#eee",
},
cardBody: {
flex: 1,
gap: 3,
},
cardTitleRow: {
flexDirection: "row",
alignItems: "center",
gap: 6,
flexWrap: "wrap",
},
cardTitle: {
fontSize: 14,
fontWeight: "bold",
color: "#222",
},
cardTitleDisabled: {
color: "#aaa",
},
cardDesc: {
fontSize: 12,
color: "#666",
},
tag: {
borderRadius: 4,
paddingHorizontal: 5,
paddingVertical: 1,
},
tagText: {
fontSize: 10,
fontWeight: "bold",
},
unyoDetailRow: {
flexDirection: "row",
flexWrap: "wrap",
gap: 4,
marginTop: 2,
},
unyoComment: {
fontSize: 11,
color: "#888",
fontStyle: "italic",
marginTop: 2,
},
postsRow: {
flexDirection: "row",
alignItems: "center",
gap: 3,
marginTop: 2,
},
postsText: {
fontSize: 11,
color: "#888",
},
chip: {
flexDirection: "row",
alignItems: "center",
backgroundColor: "#f0f0f0",
borderRadius: 4,
paddingHorizontal: 5,
paddingVertical: 2,
gap: 3,
},
chipText: {
fontSize: 11,
color: "#555",
},
directionBanner: {
flexDirection: "row",
alignItems: "center",
gap: 4,
backgroundColor: "#E3F4FB",
borderRadius: 6,
paddingHorizontal: 8,
paddingVertical: 4,
marginBottom: 6,
alignSelf: "flex-start",
},
directionText: {
fontSize: 11,
fontWeight: "600",
color: "#0099CC",
},
});