Files
menulio/apps/mobile/src/components/menu/ItemDetailSheet.tsx
T
2026-08-20 01:51:59 +03:00

434 lines
14 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 { useEffect, useState } from "react";
import {
Alert,
Image,
KeyboardAvoidingView,
Modal,
Platform,
Pressable,
ScrollView,
Switch,
Text,
TextInput,
View,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
import * as Haptics from "expo-haptics";
import * as ImagePicker from "expo-image-picker";
import type { MenuItemData } from "./MenuItemCard";
interface ItemDetailSheetProps {
item: MenuItemData | null;
visible: boolean;
onClose: () => void;
onSave: (updatedItem: MenuItemData) => Promise<void>;
onDelete: (itemId: string) => Promise<void>;
}
export function ItemDetailSheet({
item,
visible,
onClose,
onSave,
onDelete,
}: ItemDetailSheetProps) {
const [name, setName] = useState("");
const [price, setPrice] = useState("");
const [description, setDescription] = useState("");
const [imageUrl, setImageUrl] = useState<string | null>(null);
const [isActive, setIsActive] = useState(true);
const [saving, setSaving] = useState(false);
const [deleting, setDeleting] = useState(false);
useEffect(() => {
if (item) {
setName(item.name);
setPrice(String(item.price));
setDescription(item.description ?? "");
setImageUrl(item.image_url ?? null);
setIsActive(item.is_active);
}
}, [item]);
if (!item) return null;
async function handlePickImage() {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {});
const permission = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (!permission.granted) {
Alert.alert("İzin Gerekli", "Ürün görseli seçebilmek için fotoğraf galerisi erişim izni gereklidir.");
return;
}
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ["images"],
allowsEditing: true,
aspect: [4, 3],
quality: 0.6,
base64: true,
});
if (!result.canceled && result.assets[0]) {
const asset = result.assets[0];
if (asset.base64) {
setImageUrl(`data:image/jpeg;base64,${asset.base64}`);
} else {
setImageUrl(asset.uri);
}
}
}
async function handleSave() {
const parsedPrice = Number(price.replace(",", "."));
if (!name.trim()) {
Alert.alert("Eksik Bilgi", "Lütfen ürün adını giriniz.");
return;
}
if (Number.isNaN(parsedPrice) || parsedPrice < 0) {
Alert.alert("Geçersiz Fiyat", "Lütfen geçerli bir fiyat giriniz.");
return;
}
setSaving(true);
try {
await onSave({
...item!,
name: name.trim(),
price: parsedPrice,
description: description.trim() || null,
image_url: imageUrl || null,
is_active: isActive,
});
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {});
onClose();
} catch (err) {
Alert.alert("Hata", err instanceof Error ? err.message : "Kaydedilemedi.");
} finally {
setSaving(false);
}
}
function confirmDelete() {
Alert.alert(
"Ürünü Sil",
`"${item?.name}" menüden silinecek. Emin misiniz?`,
[
{ text: "Vazgeç", style: "cancel" },
{
text: "Sil",
style: "destructive",
onPress: async () => {
setDeleting(true);
try {
await onDelete(item!.id);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning).catch(() => {});
onClose();
} catch (err) {
Alert.alert("Hata", err instanceof Error ? err.message : "Silinemedi.");
} finally {
setDeleting(false);
}
},
},
],
);
}
return (
<Modal
visible={visible}
animationType="slide"
transparent
onRequestClose={onClose}
>
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : undefined}
style={{ flex: 1, backgroundColor: "rgba(0,0,0,0.5)", justifyContent: "flex-end" }}
>
<Pressable style={{ flex: 1 }} onPress={onClose} />
<View
style={{
backgroundColor: "#FFFFFF",
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
maxHeight: "90%",
paddingBottom: Platform.OS === "ios" ? 34 : 20,
}}
>
{/* Header Handle & Bar */}
<View style={{ alignItems: "center", paddingTop: 10, paddingBottom: 6 }}>
<View
style={{
width: 36,
height: 4,
borderRadius: 2,
backgroundColor: "#E7E5E4",
}}
/>
</View>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: 20,
paddingVertical: 12,
borderBottomWidth: 1,
borderBottomColor: "#F5F5F4",
}}
>
<Text style={{ fontSize: 18, fontWeight: "700", color: "#1C1917" }}>
Ürün Detayları
</Text>
<Pressable
onPress={onClose}
style={{
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: "#F5F5F4",
alignItems: "center",
justifyContent: "center",
}}
>
<Ionicons name="close" size={20} color="#78716C" />
</Pressable>
</View>
<ScrollView contentContainerStyle={{ padding: 20, gap: 16 }}>
{/* Availability Toggle Box */}
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
backgroundColor: isActive ? "#F0FDF4" : "#FEF2F2",
borderWidth: 1,
borderColor: isActive ? "#BBF7D0" : "#FECACA",
borderRadius: 14,
padding: 14,
}}
>
<View>
<Text
style={{
fontSize: 15,
fontWeight: "700",
color: isActive ? "#15803D" : "#B91C1C",
}}
>
{isActive ? "Menüde Aktif & Siparişe Açık" : "Tükendi / Menüde Gizli"}
</Text>
<Text style={{ fontSize: 12, color: "#78716C", marginTop: 2 }}>
{isActive
? "Müşteriler bu ürünü QR menüde görebilir."
: "Müşteriler ürünü Tükendi olarak görür."}
</Text>
</View>
<Switch
value={isActive}
onValueChange={(val) => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium).catch(() => {});
setIsActive(val);
}}
trackColor={{ false: "#D6D3D1", true: "#059669" }}
thumbColor="#FFFFFF"
/>
</View>
{/* Image Picker / Preview */}
<View>
<Text style={{ fontSize: 13, fontWeight: "700", color: "#44403C", marginBottom: 8 }}>
Ürün Fotoğrafı
</Text>
{imageUrl ? (
<View style={{ position: "relative", width: "100%", height: 160, borderRadius: 14, overflow: "hidden", borderWidth: 1, borderColor: "#E7E5E4" }}>
<Image source={{ uri: imageUrl }} style={{ width: "100%", height: 160 }} resizeMode="cover" />
<Pressable
onPress={() => setImageUrl(null)}
style={{
position: "absolute",
top: 10,
right: 10,
backgroundColor: "rgba(0,0,0,0.65)",
borderRadius: 14,
padding: 6,
}}
>
<Ionicons name="trash-outline" size={18} color="#FFFFFF" />
</Pressable>
<Pressable
onPress={handlePickImage}
style={{
position: "absolute",
bottom: 10,
right: 10,
backgroundColor: "#1C1917",
borderRadius: 8,
paddingHorizontal: 10,
paddingVertical: 6,
flexDirection: "row",
alignItems: "center",
gap: 4,
}}
>
<Ionicons name="camera-outline" size={14} color="#FFFFFF" />
<Text style={{ color: "#FFFFFF", fontSize: 11, fontWeight: "700" }}>Değiştir</Text>
</Pressable>
</View>
) : (
<Pressable
onPress={handlePickImage}
style={({ pressed }) => ({
height: 100,
borderRadius: 14,
borderWidth: 1.5,
borderStyle: "dashed",
borderColor: "#D6D3D1",
backgroundColor: "#FAF8F5",
alignItems: "center",
justifyContent: "center",
gap: 6,
opacity: pressed ? 0.8 : 1,
})}
>
<Ionicons name="camera-outline" size={26} color="#C8A96B" />
<Text style={{ fontSize: 13, fontWeight: "600", color: "#78716C" }}>
Galeriden Fotoğraf Seç
</Text>
</Pressable>
)}
</View>
{/* Name Input */}
<View>
<Text style={{ fontSize: 13, fontWeight: "700", color: "#44403C", marginBottom: 6 }}>
Ürün Adı *
</Text>
<TextInput
value={name}
onChangeText={setName}
placeholder="Örn: Izgara Levrek"
placeholderTextColor="#A8A29E"
style={{
backgroundColor: "#FAFAFA",
borderWidth: 1,
borderColor: "#E7E5E4",
borderRadius: 12,
padding: 14,
fontSize: 15,
fontWeight: "600",
color: "#1C1917",
}}
/>
</View>
{/* Price Input */}
<View>
<Text style={{ fontSize: 13, fontWeight: "700", color: "#44403C", marginBottom: 6 }}>
Fiyat () *
</Text>
<View
style={{
flexDirection: "row",
alignItems: "center",
backgroundColor: "#FAFAFA",
borderWidth: 1,
borderColor: "#E7E5E4",
borderRadius: 12,
paddingHorizontal: 14,
}}
>
<TextInput
value={price}
onChangeText={setPrice}
placeholder="0.00"
placeholderTextColor="#A8A29E"
keyboardType="decimal-pad"
style={{
flex: 1,
paddingVertical: 14,
fontSize: 16,
fontWeight: "700",
color: "#C8A96B",
}}
/>
<Text style={{ fontSize: 16, fontWeight: "700", color: "#1C1917" }}></Text>
</View>
</View>
{/* Description Input */}
<View>
<Text style={{ fontSize: 13, fontWeight: "700", color: "#44403C", marginBottom: 6 }}>
Açıklama / Malzemeler
</Text>
<TextInput
value={description}
onChangeText={setDescription}
placeholder="İçindekiler, porsiyon bilgisi, pişirme tarzı..."
placeholderTextColor="#A8A29E"
multiline
numberOfLines={3}
style={{
backgroundColor: "#FAFAFA",
borderWidth: 1,
borderColor: "#E7E5E4",
borderRadius: 12,
padding: 14,
fontSize: 14,
color: "#1C1917",
minHeight: 80,
textAlignVertical: "top",
}}
/>
</View>
{/* Action Buttons */}
<View style={{ gap: 10, marginTop: 10 }}>
<Pressable
onPress={handleSave}
disabled={saving || deleting}
style={({ pressed }) => ({
backgroundColor: "#1C1917",
paddingVertical: 16,
borderRadius: 14,
alignItems: "center",
opacity: pressed || saving ? 0.85 : 1,
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 6,
})}
>
<Text style={{ color: "#FFFFFF", fontSize: 15, fontWeight: "700" }}>
{saving ? "Kaydediliyor..." : "Değişiklikleri Kaydet"}
</Text>
</Pressable>
<Pressable
onPress={confirmDelete}
disabled={saving || deleting}
style={({ pressed }) => ({
backgroundColor: "#FEF2F2",
borderWidth: 1,
borderColor: "#FEE2E2",
paddingVertical: 14,
borderRadius: 14,
alignItems: "center",
opacity: pressed || deleting ? 0.85 : 1,
})}
>
<Text style={{ color: "#DC2626", fontSize: 14, fontWeight: "700" }}>
{deleting ? "Siliniyor..." : "Bu Ürünü Menüden Sil"}
</Text>
</Pressable>
</View>
</ScrollView>
</View>
</KeyboardAvoidingView>
</Modal>
);
}