Yaltopia-Tickets-App/app/proforma/create.tsx
2026-03-01 14:43:12 +03:00

371 lines
12 KiB
TypeScript

import React, { useState } from "react";
import {
View,
ScrollView,
Pressable,
TextInput,
StyleSheet,
Platform,
} from "react-native";
import { Text } from "@/components/ui/text";
import { Button } from "@/components/ui/button";
import { ArrowLeft, Trash2, Send, Plus } from "@/lib/icons";
import { ScreenWrapper } from "@/components/ScreenWrapper";
import { router, Stack } from "expo-router";
import { useColorScheme } from "nativewind";
import { ShadowWrapper } from "@/components/ShadowWrapper";
type Item = { id: number; description: string; qty: string; price: string };
// All TextInput styles are native StyleSheet — NO className on TextInput
// NativeWind className on TextInput causes focus loop because it re-processes
// styles each render and resets the responder chain.
const S = StyleSheet.create({
input: {
height: 44,
paddingHorizontal: 12,
fontSize: 12,
fontWeight: "500",
borderRadius: 6,
borderWidth: 1,
},
inputCenter: {
height: 44,
paddingHorizontal: 12,
fontSize: 14,
fontWeight: "500",
borderRadius: 6,
borderWidth: 1,
textAlign: "center",
},
});
function useInputColors() {
const { colorScheme } = useColorScheme();
const dark = colorScheme === "dark";
return {
bg: dark ? "rgba(30,30,30,0.8)" : "rgba(241,245,249,0.2)",
border: dark ? "rgba(255,255,255,0.08)" : "rgba(203,213,225,0.6)",
text: dark ? "#f1f5f9" : "#0f172a",
placeholder: "rgba(100,116,139,0.45)",
};
}
function Field({
label,
value,
onChangeText,
placeholder,
numeric = false,
center = false,
flex,
}: {
label: string;
value: string;
onChangeText: (v: string) => void;
placeholder: string;
numeric?: boolean;
center?: boolean;
flex?: number;
}) {
const c = useInputColors();
return (
<View style={flex != null ? { flex } : undefined}>
<Text variant="muted" className="font-semibold text-xs mb-1.5">
{label}
</Text>
<TextInput
style={[
center ? S.inputCenter : S.input,
{ backgroundColor: c.bg, borderColor: c.border, color: c.text },
]}
placeholder={placeholder}
placeholderTextColor={c.placeholder}
value={value}
onChangeText={onChangeText}
keyboardType={numeric ? "numeric" : "default"}
autoCorrect={false}
autoCapitalize="none"
returnKeyType="next"
/>
</View>
);
}
export default function CreateProformaScreen() {
const [company, setCompany] = useState("");
const [project, setProject] = useState("");
const [validity, setValidity] = useState("");
const [terms, setTerms] = useState("");
const [items, setItems] = useState<Item[]>([
{ id: 1, description: "", qty: "1", price: "" },
]);
const c = useInputColors();
const updateField = (id: number, field: keyof Item, value: string) =>
setItems((prev) =>
prev.map((item) => (item.id === id ? { ...item, [field]: value } : item)),
);
const addItem = () =>
setItems((prev) => [
...prev,
{ id: Date.now(), description: "", qty: "1", price: "" },
]);
const removeItem = (id: number) => {
if (items.length > 1)
setItems((prev) => prev.filter((item) => item.id !== id));
};
const total = items.reduce(
(sum, item) =>
sum + (parseFloat(item.qty) || 0) * (parseFloat(item.price) || 0),
0,
);
return (
<ScreenWrapper className="bg-background">
<Stack.Screen options={{ headerShown: false }} />
<View className="px-6 pt-4 flex-row justify-between items-center">
<Pressable
onPress={() => router.back()}
className="h-10 w-10 rounded-[10px] bg-card items-center justify-center border border-border"
>
<ArrowLeft color="#0f172a" size={20} />
</Pressable>
<Text variant="h4" className="text-foreground font-semibold">
New Proforma
</Text>
<View className="w-9" />
</View>
<ScrollView
className="flex-1"
contentContainerStyle={{ padding: 16, paddingBottom: 140 }}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
>
{/* Recipient */}
<Label>Recipient</Label>
<ShadowWrapper>
<View className="bg-card rounded-[6px] p-4 mb-5 gap-4">
<Field
label="Company / Name"
value={company}
onChangeText={setCompany}
placeholder="e.g. Acme Corp"
/>
<Field
label="Project Title"
value={project}
onChangeText={setProject}
placeholder="e.g. Website Redesign"
/>
</View>
</ShadowWrapper>
{/* Terms */}
<Label>Terms & Validity</Label>
<ShadowWrapper>
<View className="bg-card rounded-[6px] p-4 mb-5">
<View className="flex-row gap-4">
<Field
label="Validity (days)"
value={validity}
onChangeText={setValidity}
placeholder="30"
numeric
flex={1}
/>
<Field
label="Payment Terms"
value={terms}
onChangeText={setTerms}
placeholder="e.g. 50% upfront"
flex={2}
/>
</View>
</View>
</ShadowWrapper>
{/* Items */}
<View className="flex-row items-center justify-between mb-3">
<Label noMargin>Billable Items</Label>
<Pressable
onPress={addItem}
className="flex-row items-center gap-1 px-3 py-1 rounded-[6px] bg-primary/10 border border-primary/20"
>
<Plus color="#ea580c" size={10} strokeWidth={2.5} />
<Text className="text-primary text-[8px] font-bold uppercase tracking-widest">
Add Item
</Text>
</Pressable>
</View>
<View className="gap-3 mb-5">
{items.map((item, index) => (
<ShadowWrapper>
<View key={item.id} className="bg-card rounded-[6px] p-4">
<View className="flex-row justify-between items-center mb-3">
<Text
variant="muted"
className="text-[12px] font-bold uppercase tracking-wide"
>
Item {index + 1}
</Text>
{items.length > 1 && (
<Pressable onPress={() => removeItem(item.id)} hitSlop={8}>
<Trash2 color="#ef4444" size={13} />
</Pressable>
)}
</View>
<Text
variant="muted"
className="text-[11px] font-semibold mb-1.5"
>
Description
</Text>
<TextInput
style={[
S.input,
{
backgroundColor: c.bg,
borderColor: c.border,
color: c.text,
marginBottom: 12,
},
]}
placeholder="e.g. Web Design Package"
placeholderTextColor={c.placeholder}
value={item.description}
onChangeText={(v) => updateField(item.id, "description", v)}
autoCorrect={false}
autoCapitalize="none"
returnKeyType="next"
/>
<View className="flex-row gap-3">
<View className="flex-1">
<Text
variant="muted"
className="text-[11px] font-semibold mb-1.5"
>
Qty
</Text>
<TextInput
style={[
S.inputCenter,
{
backgroundColor: c.bg,
borderColor: c.border,
color: c.text,
},
]}
placeholder="1"
placeholderTextColor={c.placeholder}
keyboardType="numeric"
value={item.qty}
onChangeText={(v) => updateField(item.id, "qty", v)}
returnKeyType="next"
/>
</View>
<View className="flex-[2]">
<Text
variant="muted"
className="text-[11px] font-semibold mb-1.5"
>
Unit Price ($)
</Text>
<TextInput
style={[
S.input,
{
backgroundColor: c.bg,
borderColor: c.border,
color: c.text,
},
]}
placeholder="0.00"
placeholderTextColor={c.placeholder}
keyboardType="numeric"
value={item.price}
onChangeText={(v) => updateField(item.id, "price", v)}
returnKeyType="done"
/>
</View>
<View className="flex-1 items-end justify-end pb-1">
<Text variant="muted" className="text-[10px]">
Total
</Text>
<Text
variant="p"
className="text-foreground font-bold text-sm"
>
$
{(
(parseFloat(item.qty) || 0) *
(parseFloat(item.price) || 0)
).toFixed(2)}
</Text>
</View>
</View>
</View>
</ShadowWrapper>
))}
</View>
{/* Summary */}
<View className="border border-border/60 rounded-[6px] p-4 bg-secondary/10 mb-6">
<View className="flex-row justify-between items-center mb-4">
<Text variant="muted" className="font-semibold text-sm">
Estimated Total
</Text>
<Text variant="h4" className="text-foreground font-semibold">
$
{total.toLocaleString("en-US", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}
</Text>
</View>
<View className="flex-row gap-3">
<Button
variant="outline"
className="flex-1 h-11 rounded-[6px] border-border bg-card"
onPress={() => router.back()}
>
<Text className="text-foreground font-semibold text-[11px] uppercase tracking-widest">
Cancel
</Text>
</Button>
<Button className="flex-1 h-11 rounded-[6px] bg-primary">
<Send color="white" size={14} strokeWidth={2.5} />
<Text className=" text-white font-bold text-[11px] uppercase tracking-widest">
Create & Share
</Text>
</Button>
</View>
</View>
</ScrollView>
</ScreenWrapper>
);
}
function Label({
children,
noMargin,
}: {
children: string;
noMargin?: boolean;
}) {
return (
<Text variant="muted" className={`font-semibold ${noMargin ? "" : "mb-3"}`}>
{children}
</Text>
);
}