thêm tab "Xem chi tiết chuyến đi", "Xem chi tiết thành viên chuyến đi", tái sử dụng lại components modal tripForm
This commit is contained in:
285
components/diary/TripCrewModal/CrewCard.tsx
Normal file
285
components/diary/TripCrewModal/CrewCard.tsx
Normal file
@@ -0,0 +1,285 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
Platform,
|
||||
Image,
|
||||
ActivityIndicator,
|
||||
TouchableOpacity,
|
||||
} from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||
import { useI18n } from "@/hooks/use-i18n";
|
||||
import dayjs from "dayjs";
|
||||
import { queryCrewImage } from "@/controller/TripCrewController";
|
||||
import { Buffer } from "buffer";
|
||||
|
||||
interface CrewCardProps {
|
||||
crew: Model.TripCrews;
|
||||
onEdit?: (crew: Model.TripCrews) => void;
|
||||
onDelete?: (crew: Model.TripCrews) => void;
|
||||
}
|
||||
|
||||
export default function CrewCard({ crew, onEdit, onDelete }: CrewCardProps) {
|
||||
const { colors } = useThemeContext();
|
||||
const { t } = useI18n();
|
||||
|
||||
const person = crew.Person;
|
||||
const joinedDate = crew.joined_at ? dayjs(crew.joined_at).format("DD/MM/YYYY") : "-";
|
||||
const leftDate = crew.left_at ? dayjs(crew.left_at).format("DD/MM/YYYY") : null;
|
||||
|
||||
// State for image
|
||||
const [imageUri, setImageUri] = useState<string | null>(null);
|
||||
const [imageLoading, setImageLoading] = useState(true);
|
||||
const [imageError, setImageError] = useState(false);
|
||||
|
||||
// Fetch crew image
|
||||
useEffect(() => {
|
||||
const fetchImage = async () => {
|
||||
if (!person?.personal_id) {
|
||||
setImageLoading(false);
|
||||
setImageError(true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await queryCrewImage(person.personal_id);
|
||||
if (response.data) {
|
||||
// Convert arraybuffer to base64
|
||||
const base64 = Buffer.from(response.data as ArrayBuffer).toString("base64");
|
||||
setImageUri(`data:image/jpeg;base64,${base64}`);
|
||||
} else {
|
||||
setImageError(true);
|
||||
}
|
||||
} catch (err) {
|
||||
setImageError(true);
|
||||
} finally {
|
||||
setImageLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchImage();
|
||||
}, [person?.personal_id]);
|
||||
|
||||
const themedStyles = {
|
||||
card: {
|
||||
backgroundColor: colors.card,
|
||||
borderColor: colors.separator,
|
||||
},
|
||||
name: {
|
||||
color: colors.text,
|
||||
},
|
||||
role: {
|
||||
color: colors.primary,
|
||||
backgroundColor: colors.primary + "20",
|
||||
},
|
||||
label: {
|
||||
color: colors.textSecondary,
|
||||
},
|
||||
value: {
|
||||
color: colors.text,
|
||||
},
|
||||
iconColor: colors.textSecondary,
|
||||
imagePlaceholder: {
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={[styles.card, themedStyles.card]}>
|
||||
{/* Left Image Section (1/3 width) */}
|
||||
<View style={[styles.imageSection, themedStyles.imagePlaceholder]}>
|
||||
{imageLoading ? (
|
||||
<ActivityIndicator size="small" color={colors.primary} />
|
||||
) : imageUri && !imageError ? (
|
||||
<Image
|
||||
source={{ uri: imageUri }}
|
||||
style={styles.crewImage}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
) : (
|
||||
<View style={styles.imagePlaceholder}>
|
||||
<Ionicons name="person" size={40} color={colors.textSecondary} />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Right Content Section (2/3 width) */}
|
||||
<View style={styles.contentSection}>
|
||||
{/* Name & Role */}
|
||||
<View style={styles.header}>
|
||||
<Text style={[styles.name, themedStyles.name]} numberOfLines={1}>
|
||||
{person?.name || "-"}
|
||||
</Text>
|
||||
<View style={[styles.roleBadge, { backgroundColor: themedStyles.role.backgroundColor }]}>
|
||||
<Text style={[styles.roleText, { color: themedStyles.role.color }]}>
|
||||
{crew.role || t("diary.crew.member")}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Info Grid */}
|
||||
<View style={styles.infoGrid}>
|
||||
{/* Phone */}
|
||||
<View style={styles.infoRow}>
|
||||
<Ionicons name="call-outline" size={14} color={themedStyles.iconColor} />
|
||||
<Text style={[styles.value, themedStyles.value]} numberOfLines={1}>
|
||||
{person?.phone || "-"}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Personal ID */}
|
||||
<View style={styles.infoRow}>
|
||||
<Ionicons name="card-outline" size={14} color={themedStyles.iconColor} />
|
||||
<Text style={[styles.value, themedStyles.value]} numberOfLines={1}>
|
||||
{person?.personal_id || "-"}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Joined Date */}
|
||||
<View style={styles.infoRow}>
|
||||
<Ionicons name="calendar-outline" size={14} color={themedStyles.iconColor} />
|
||||
<Text style={[styles.value, themedStyles.value]}>{joinedDate}</Text>
|
||||
</View>
|
||||
|
||||
{/* Left Date (only show if exists) */}
|
||||
{leftDate && (
|
||||
<View style={styles.infoRow}>
|
||||
<Ionicons name="exit-outline" size={14} color={themedStyles.iconColor} />
|
||||
<Text style={[styles.value, themedStyles.value]}>{leftDate}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Action Buttons */}
|
||||
{(onEdit || onDelete) && (
|
||||
<View style={styles.actionRow}>
|
||||
{onEdit && (
|
||||
<TouchableOpacity
|
||||
style={[styles.actionButton, { backgroundColor: colors.primary + "15" }]}
|
||||
onPress={() => onEdit(crew)}
|
||||
>
|
||||
<Ionicons name="pencil-outline" size={14} color={colors.primary} />
|
||||
<Text style={[styles.actionText, { color: colors.primary }]}>
|
||||
{t("common.edit")}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{onDelete && (
|
||||
<TouchableOpacity
|
||||
style={[styles.actionButton, { backgroundColor: (colors.error || "#FF3B30") + "15" }]}
|
||||
onPress={() => onDelete(crew)}
|
||||
>
|
||||
<Ionicons name="trash-outline" size={14} color={colors.error || "#FF3B30"} />
|
||||
<Text style={[styles.actionText, { color: colors.error || "#FF3B30" }]}>
|
||||
{t("common.delete")}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
flexDirection: "row",
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
marginBottom: 12,
|
||||
overflow: "hidden",
|
||||
maxHeight: 150,
|
||||
},
|
||||
imageSection: {
|
||||
width: 130,
|
||||
alignSelf: "stretch",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
crewImage: {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
},
|
||||
imagePlaceholder: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
},
|
||||
contentSection: {
|
||||
flex: 1,
|
||||
padding: 10,
|
||||
justifyContent: "center",
|
||||
},
|
||||
header: {
|
||||
marginBottom: 8,
|
||||
},
|
||||
name: {
|
||||
fontSize: 17,
|
||||
fontWeight: "700",
|
||||
marginBottom: 4,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
roleBadge: {
|
||||
alignSelf: "flex-start",
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 2,
|
||||
borderRadius: 10,
|
||||
},
|
||||
roleText: {
|
||||
fontSize: 12,
|
||||
fontWeight: "600",
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
infoGrid: {
|
||||
gap: 4,
|
||||
},
|
||||
infoRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 5,
|
||||
},
|
||||
value: {
|
||||
fontSize: 14,
|
||||
fontWeight: "500",
|
||||
flex: 1,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
actionRow: {
|
||||
flexDirection: "row",
|
||||
gap: 8,
|
||||
marginTop: 8,
|
||||
},
|
||||
actionButton: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 5,
|
||||
borderRadius: 6,
|
||||
},
|
||||
actionText: {
|
||||
fontSize: 12,
|
||||
fontWeight: "600",
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user