371 lines
12 KiB
TypeScript
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>
|
|
);
|
|
}
|