473 lines
13 KiB
TypeScript
473 lines
13 KiB
TypeScript
import React, { useState } from "react";
|
|
import {
|
|
View,
|
|
Text,
|
|
TouchableOpacity,
|
|
StyleSheet,
|
|
Platform,
|
|
TextInput,
|
|
Modal,
|
|
ScrollView,
|
|
} from "react-native";
|
|
import { Ionicons } from "@expo/vector-icons";
|
|
import { useI18n } from "@/hooks/use-i18n";
|
|
import { useThemeContext } from "@/hooks/use-theme-context";
|
|
|
|
interface TripCost {
|
|
id: string;
|
|
type: string;
|
|
amount: number;
|
|
unit: string;
|
|
cost_per_unit: number;
|
|
total_cost: number;
|
|
}
|
|
|
|
interface MaterialCostListProps {
|
|
items: TripCost[];
|
|
onChange: (items: TripCost[]) => void;
|
|
}
|
|
|
|
// Predefined cost types
|
|
const COST_TYPES = [
|
|
{ value: "fuel", label: "Nhiên liệu" },
|
|
{ value: "food", label: "Thực phẩm" },
|
|
{ value: "crew_salary", label: "Lương thuyền viên" },
|
|
{ value: "ice_salt_cost", label: "Muối đá" },
|
|
{ value: "other", label: "Khác" },
|
|
];
|
|
|
|
export default function MaterialCostList({
|
|
items,
|
|
onChange,
|
|
}: MaterialCostListProps) {
|
|
const { t } = useI18n();
|
|
const { colors } = useThemeContext();
|
|
const [typeDropdownVisible, setTypeDropdownVisible] = useState<string | null>(null);
|
|
|
|
const handleAddMaterial = () => {
|
|
const newMaterial: TripCost = {
|
|
id: Date.now().toString(),
|
|
type: "",
|
|
amount: 0,
|
|
unit: "",
|
|
cost_per_unit: 0,
|
|
total_cost: 0,
|
|
};
|
|
onChange([...items, newMaterial]);
|
|
};
|
|
|
|
const handleRemoveMaterial = (id: string) => {
|
|
onChange(items.filter((item) => item.id !== id));
|
|
};
|
|
|
|
const handleDuplicateMaterial = (material: TripCost) => {
|
|
const duplicatedMaterial: TripCost = {
|
|
id: Date.now().toString(),
|
|
type: material.type,
|
|
amount: material.amount,
|
|
unit: material.unit,
|
|
cost_per_unit: material.cost_per_unit,
|
|
total_cost: material.total_cost,
|
|
};
|
|
onChange([...items, duplicatedMaterial]);
|
|
};
|
|
|
|
const handleUpdateMaterial = (id: string, field: keyof TripCost, value: string | number) => {
|
|
onChange(
|
|
items.map((item) => {
|
|
if (item.id === id) {
|
|
const updatedItem = { ...item, [field]: value };
|
|
// Auto-calculate total_cost when amount or cost_per_unit changes
|
|
if (field === "amount" || field === "cost_per_unit") {
|
|
const amount = field === "amount" ? Number(value) : item.amount;
|
|
const costPerUnit = field === "cost_per_unit" ? Number(value) : item.cost_per_unit;
|
|
updatedItem.total_cost = amount * costPerUnit;
|
|
}
|
|
return updatedItem;
|
|
}
|
|
return item;
|
|
})
|
|
);
|
|
};
|
|
|
|
const getTypeLabel = (value: string) => {
|
|
const type = COST_TYPES.find((t) => t.value === value);
|
|
return type ? type.label : value || t("diary.selectType");
|
|
};
|
|
|
|
const formatCurrency = (amount: number) => {
|
|
return new Intl.NumberFormat("vi-VN").format(amount);
|
|
};
|
|
|
|
const themedStyles = {
|
|
sectionTitle: { color: colors.text },
|
|
fieldLabel: { color: colors.text },
|
|
input: {
|
|
backgroundColor: colors.card,
|
|
borderColor: colors.border,
|
|
color: colors.text,
|
|
},
|
|
dropdown: {
|
|
backgroundColor: colors.card,
|
|
borderColor: colors.border,
|
|
},
|
|
dropdownText: { color: colors.text },
|
|
placeholder: { color: colors.textSecondary },
|
|
addButton: {
|
|
borderColor: colors.primary,
|
|
},
|
|
addButtonText: { color: colors.primary },
|
|
modalOverlay: {
|
|
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
|
},
|
|
modalContent: {
|
|
backgroundColor: colors.card,
|
|
},
|
|
option: {
|
|
borderBottomColor: colors.separator,
|
|
},
|
|
optionText: { color: colors.text },
|
|
};
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<Text style={[styles.sectionTitle, themedStyles.sectionTitle]}>
|
|
{t("diary.materialCostList")}
|
|
</Text>
|
|
|
|
{/* Cost Items List */}
|
|
{items.map((cost) => (
|
|
<View key={cost.id} style={styles.costRow}>
|
|
<View style={styles.formRow}>
|
|
{/* Type Dropdown */}
|
|
<View style={[styles.inputGroup, styles.typeGroup]}>
|
|
<Text style={[styles.fieldLabel, themedStyles.fieldLabel]}>
|
|
{t("diary.costType")}
|
|
</Text>
|
|
<TouchableOpacity
|
|
style={[styles.dropdown, themedStyles.dropdown]}
|
|
onPress={() => setTypeDropdownVisible(cost.id)}
|
|
activeOpacity={0.7}
|
|
>
|
|
<Text
|
|
style={[
|
|
styles.dropdownText,
|
|
themedStyles.dropdownText,
|
|
!cost.type && themedStyles.placeholder,
|
|
]}
|
|
>
|
|
{getTypeLabel(cost.type)}
|
|
</Text>
|
|
<Ionicons
|
|
name="chevron-down"
|
|
size={16}
|
|
color={colors.textSecondary}
|
|
/>
|
|
</TouchableOpacity>
|
|
|
|
{/* Type Dropdown Modal */}
|
|
<Modal
|
|
visible={typeDropdownVisible === cost.id}
|
|
transparent
|
|
animationType="fade"
|
|
onRequestClose={() => setTypeDropdownVisible(null)}
|
|
>
|
|
<TouchableOpacity
|
|
style={[styles.modalOverlay, themedStyles.modalOverlay]}
|
|
activeOpacity={1}
|
|
onPress={() => setTypeDropdownVisible(null)}
|
|
>
|
|
<View style={[styles.modalContent, themedStyles.modalContent]}>
|
|
<ScrollView>
|
|
{COST_TYPES.map((type) => (
|
|
<TouchableOpacity
|
|
key={type.value}
|
|
style={[styles.option, themedStyles.option]}
|
|
onPress={() => {
|
|
handleUpdateMaterial(cost.id, "type", type.value);
|
|
setTypeDropdownVisible(null);
|
|
}}
|
|
>
|
|
<Text
|
|
style={[styles.optionText, themedStyles.optionText]}
|
|
>
|
|
{type.label}
|
|
</Text>
|
|
{cost.type === type.value && (
|
|
<Ionicons
|
|
name="checkmark"
|
|
size={20}
|
|
color={colors.primary}
|
|
/>
|
|
)}
|
|
</TouchableOpacity>
|
|
))}
|
|
</ScrollView>
|
|
</View>
|
|
</TouchableOpacity>
|
|
</Modal>
|
|
</View>
|
|
|
|
{/* Amount Input */}
|
|
<View style={[styles.inputGroup, styles.smallGroup]}>
|
|
<Text style={[styles.fieldLabel, themedStyles.fieldLabel]}>
|
|
{t("diary.amount")}
|
|
</Text>
|
|
<TextInput
|
|
style={[styles.input, styles.smallInput, themedStyles.input]}
|
|
value={cost.amount.toString()}
|
|
onChangeText={(value) =>
|
|
handleUpdateMaterial(cost.id, "amount", Number(value) || 0)
|
|
}
|
|
placeholder="0"
|
|
placeholderTextColor={colors.textSecondary}
|
|
keyboardType="numeric"
|
|
/>
|
|
</View>
|
|
|
|
{/* Unit Input */}
|
|
<View style={[styles.inputGroup, styles.smallGroup]}>
|
|
<Text style={[styles.fieldLabel, themedStyles.fieldLabel]}>
|
|
{t("diary.unit")}
|
|
</Text>
|
|
<TextInput
|
|
style={[styles.input, styles.smallInput, themedStyles.input]}
|
|
value={cost.unit}
|
|
onChangeText={(value) =>
|
|
handleUpdateMaterial(cost.id, "unit", value)
|
|
}
|
|
placeholder={t("diary.unitPlaceholder")}
|
|
placeholderTextColor={colors.textSecondary}
|
|
/>
|
|
</View>
|
|
</View>
|
|
|
|
<View style={styles.formRow}>
|
|
{/* Cost Per Unit Input */}
|
|
<View style={[styles.inputGroup, styles.mediumGroup]}>
|
|
<Text style={[styles.fieldLabel, themedStyles.fieldLabel]}>
|
|
{t("diary.costPerUnit")}
|
|
</Text>
|
|
<TextInput
|
|
style={[styles.input, styles.mediumInput, themedStyles.input]}
|
|
value={cost.cost_per_unit.toString()}
|
|
onChangeText={(value) =>
|
|
handleUpdateMaterial(
|
|
cost.id,
|
|
"cost_per_unit",
|
|
Number(value) || 0
|
|
)
|
|
}
|
|
placeholder="0"
|
|
placeholderTextColor={colors.textSecondary}
|
|
keyboardType="numeric"
|
|
/>
|
|
</View>
|
|
|
|
{/* Total Cost (Read-only, auto-calculated) */}
|
|
<View style={[styles.inputGroup, styles.mediumGroup]}>
|
|
<Text style={[styles.fieldLabel, themedStyles.fieldLabel]}>
|
|
{t("diary.totalCost")}
|
|
</Text>
|
|
<View style={[styles.input, styles.mediumInput, themedStyles.input, styles.readOnlyInput]}>
|
|
<Text style={[styles.readOnlyText, themedStyles.dropdownText]}>
|
|
{formatCurrency(cost.total_cost)}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Action Buttons */}
|
|
<View style={styles.actionButtons}>
|
|
<TouchableOpacity
|
|
onPress={() => handleDuplicateMaterial(cost)}
|
|
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
|
>
|
|
<Ionicons name="copy-outline" size={20} color={colors.text} />
|
|
</TouchableOpacity>
|
|
<TouchableOpacity
|
|
onPress={() => handleRemoveMaterial(cost.id)}
|
|
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
|
>
|
|
<Ionicons
|
|
name="trash-outline"
|
|
size={20}
|
|
color={colors.error}
|
|
/>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
))}
|
|
|
|
{/* Add Button */}
|
|
<TouchableOpacity
|
|
style={[styles.addButton, themedStyles.addButton]}
|
|
onPress={handleAddMaterial}
|
|
activeOpacity={0.7}
|
|
>
|
|
<Ionicons name="add" size={20} color={colors.primary} />
|
|
<Text style={[styles.addButtonText, themedStyles.addButtonText]}>
|
|
{t("diary.addMaterialCost")}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
marginBottom: 20,
|
|
},
|
|
sectionTitle: {
|
|
fontSize: 16,
|
|
fontWeight: "700",
|
|
marginBottom: 16,
|
|
fontFamily: Platform.select({
|
|
ios: "System",
|
|
android: "Roboto",
|
|
default: "System",
|
|
}),
|
|
},
|
|
costRow: {
|
|
marginBottom: 20,
|
|
paddingBottom: 16,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: "#E5E5E5",
|
|
},
|
|
formRow: {
|
|
flexDirection: "row",
|
|
alignItems: "flex-end",
|
|
gap: 12,
|
|
marginBottom: 12,
|
|
},
|
|
inputGroup: {
|
|
flex: 1,
|
|
},
|
|
typeGroup: {
|
|
flex: 1.5,
|
|
},
|
|
smallGroup: {
|
|
flex: 0.8,
|
|
},
|
|
mediumGroup: {
|
|
flex: 1,
|
|
},
|
|
fieldLabel: {
|
|
fontSize: 14,
|
|
fontWeight: "500",
|
|
marginBottom: 6,
|
|
fontFamily: Platform.select({
|
|
ios: "System",
|
|
android: "Roboto",
|
|
default: "System",
|
|
}),
|
|
},
|
|
input: {
|
|
borderWidth: 1,
|
|
borderRadius: 8,
|
|
paddingHorizontal: 12,
|
|
paddingVertical: 10,
|
|
fontSize: 15,
|
|
fontFamily: Platform.select({
|
|
ios: "System",
|
|
android: "Roboto",
|
|
default: "System",
|
|
}),
|
|
},
|
|
smallInput: {},
|
|
mediumInput: {},
|
|
dropdown: {
|
|
flexDirection: "row",
|
|
justifyContent: "space-between",
|
|
alignItems: "center",
|
|
borderWidth: 1,
|
|
borderRadius: 8,
|
|
paddingHorizontal: 12,
|
|
paddingVertical: 10,
|
|
},
|
|
dropdownText: {
|
|
fontSize: 15,
|
|
flex: 1,
|
|
fontFamily: Platform.select({
|
|
ios: "System",
|
|
android: "Roboto",
|
|
default: "System",
|
|
}),
|
|
},
|
|
readOnlyInput: {
|
|
justifyContent: "center",
|
|
opacity: 0.7,
|
|
},
|
|
readOnlyText: {
|
|
fontSize: 15,
|
|
fontFamily: Platform.select({
|
|
ios: "System",
|
|
android: "Roboto",
|
|
default: "System",
|
|
}),
|
|
},
|
|
actionButtons: {
|
|
flexDirection: "row",
|
|
gap: 12,
|
|
alignItems: "center",
|
|
paddingBottom: 10,
|
|
},
|
|
addButton: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
gap: 8,
|
|
paddingVertical: 12,
|
|
paddingHorizontal: 16,
|
|
borderWidth: 1.5,
|
|
borderRadius: 8,
|
|
borderStyle: "dashed",
|
|
marginTop: 4,
|
|
},
|
|
addButtonText: {
|
|
fontSize: 14,
|
|
fontWeight: "600",
|
|
fontFamily: Platform.select({
|
|
ios: "System",
|
|
android: "Roboto",
|
|
default: "System",
|
|
}),
|
|
},
|
|
// Modal styles
|
|
modalOverlay: {
|
|
flex: 1,
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
},
|
|
modalContent: {
|
|
borderRadius: 12,
|
|
width: "80%",
|
|
maxHeight: "50%",
|
|
overflow: "hidden",
|
|
shadowColor: "#000",
|
|
shadowOffset: {
|
|
width: 0,
|
|
height: 4,
|
|
},
|
|
shadowOpacity: 0.3,
|
|
shadowRadius: 8,
|
|
elevation: 8,
|
|
},
|
|
option: {
|
|
flexDirection: "row",
|
|
justifyContent: "space-between",
|
|
alignItems: "center",
|
|
paddingHorizontal: 20,
|
|
paddingVertical: 16,
|
|
borderBottomWidth: 1,
|
|
},
|
|
optionText: {
|
|
fontSize: 16,
|
|
fontFamily: Platform.select({
|
|
ios: "System",
|
|
android: "Roboto",
|
|
default: "System",
|
|
}),
|
|
},
|
|
});
|