794 lines
24 KiB
TypeScript
794 lines
24 KiB
TypeScript
import React, { useState, useEffect } from "react";
|
|
import { View, Pressable, TextInput, StyleSheet, Switch } from "react-native";
|
|
import { useColorScheme } from "nativewind";
|
|
import { Text } from "@/components/ui/text";
|
|
import { ChevronDown, Plus, Trash2, X } from "@/lib/icons";
|
|
import { ScreenWrapper } from "@/components/ScreenWrapper";
|
|
import { useSirouRouter } from "@sirou/react-native";
|
|
import { AppRoutes } from "@/lib/routes";
|
|
import { api } from "@/lib/api";
|
|
import { toast } from "@/lib/toast-store";
|
|
import { PickerModal, SelectOption } from "@/components/PickerModal";
|
|
import { CalendarGrid } from "@/components/CalendarGrid";
|
|
import { FormFlow } from "@/components/FormFlow";
|
|
import { CustomerPicker } from "@/components/CustomerPicker";
|
|
|
|
type Item = {
|
|
id: number;
|
|
itemName: string;
|
|
itemDescription: string;
|
|
quantity: string;
|
|
unitOfMeasure: string;
|
|
};
|
|
|
|
const S = StyleSheet.create({
|
|
input: {
|
|
paddingHorizontal: 12,
|
|
paddingVertical: 12,
|
|
fontSize: 12,
|
|
fontWeight: "500",
|
|
borderRadius: 6,
|
|
borderWidth: 1,
|
|
textAlignVertical: "center",
|
|
},
|
|
inputCenter: {
|
|
paddingHorizontal: 12,
|
|
paddingVertical: 10,
|
|
fontSize: 14,
|
|
fontWeight: "500",
|
|
borderRadius: 6,
|
|
borderWidth: 1,
|
|
textAlignVertical: "center",
|
|
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,
|
|
multiline = false,
|
|
}: {
|
|
label: string;
|
|
value: string;
|
|
onChangeText: (v: string) => void;
|
|
placeholder: string;
|
|
numeric?: boolean;
|
|
center?: boolean;
|
|
flex?: number;
|
|
multiline?: boolean;
|
|
}) {
|
|
const c = useInputColors();
|
|
return (
|
|
<View style={flex != null ? { flex } : undefined}>
|
|
<Text className="text-[14px] font-sans-bold mb-1.5 ml-1 text-foreground">
|
|
{label}
|
|
</Text>
|
|
<TextInput
|
|
style={[
|
|
center ? S.inputCenter : S.input,
|
|
{ backgroundColor: c.bg, borderColor: c.border, color: c.text },
|
|
multiline
|
|
? { height: 80, paddingTop: 10, textAlignVertical: "top" }
|
|
: {},
|
|
]}
|
|
placeholder={placeholder}
|
|
placeholderTextColor={c.placeholder}
|
|
value={value}
|
|
onChangeText={onChangeText}
|
|
keyboardType={numeric ? "numeric" : "default"}
|
|
multiline={multiline}
|
|
autoCorrect={false}
|
|
autoCapitalize="none"
|
|
/>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function PickerField({
|
|
label,
|
|
value,
|
|
onPress,
|
|
flex,
|
|
}: {
|
|
label: string;
|
|
value: string;
|
|
onPress: () => void;
|
|
flex?: number;
|
|
}) {
|
|
const c = useInputColors();
|
|
return (
|
|
<View style={flex != null ? { flex } : undefined}>
|
|
<Text className="text-[14px] font-sans-bold mb-1.5 ml-1 text-foreground">
|
|
{label}
|
|
</Text>
|
|
<Pressable
|
|
onPress={onPress}
|
|
className="h-10 px-3 border border-border rounded-[6px] flex-row items-center justify-between"
|
|
style={{ backgroundColor: c.bg, borderColor: c.border }}
|
|
>
|
|
<Text className="text-xs font-sans-bold" style={{ color: c.text }}>
|
|
{value || "Select"}
|
|
</Text>
|
|
<ChevronDown size={14} color={c.text} strokeWidth={3} />
|
|
</Pressable>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const CATEGORIES = ["EQUIPMENT", "SERVICE", "MIXED"];
|
|
const INCOTERMS = ["EXW", "FOB", "CIF", "DAP", "DDP"];
|
|
const INVITE_CHANNELS = ["EMAIL", "PHONE"];
|
|
const UNITS = ["unit", "kg", "m", "m²", "m³", "litre", "hour", "service"];
|
|
|
|
const STEPS = [
|
|
{ key: "details", label: "Details" },
|
|
{ key: "items", label: "Items" },
|
|
{ key: "terms", label: "Terms" },
|
|
{ key: "schedule", label: "Schedule" },
|
|
{ key: "summary", label: "Summary" },
|
|
];
|
|
|
|
export default function CreateProformaRequestScreen() {
|
|
const nav = useSirouRouter<AppRoutes>();
|
|
const [step, setStep] = useState(0);
|
|
const [submitting, setSubmitting] = useState(false);
|
|
|
|
// Step 1 — Details
|
|
const [title, setTitle] = useState("");
|
|
const [description, setDescription] = useState("");
|
|
const [category, setCategory] = useState("EQUIPMENT");
|
|
|
|
// Step 2 — Items
|
|
const [items, setItems] = useState<Item[]>([
|
|
{
|
|
id: 1,
|
|
itemName: "",
|
|
itemDescription: "",
|
|
quantity: "1",
|
|
unitOfMeasure: "unit",
|
|
},
|
|
]);
|
|
|
|
// Step 3 — Terms
|
|
const [paymentTerms, setPaymentTerms] = useState("Net 30 days");
|
|
const [incoterms, setIncoterms] = useState("EXW");
|
|
const [taxIncluded, setTaxIncluded] = useState(false);
|
|
const [discountStructure, setDiscountStructure] = useState("");
|
|
const [validityPeriod, setValidityPeriod] = useState("30");
|
|
const [allowRevisions, setAllowRevisions] = useState(true);
|
|
|
|
// Step 4 — Schedule & Invite
|
|
const [submissionDeadline, setSubmissionDeadline] = useState("");
|
|
const [inviteChannel, setInviteChannel] = useState("EMAIL");
|
|
const [customerIds, setCustomerIds] = useState<string[]>([]);
|
|
const [selectedCustomers, setSelectedCustomers] = useState<
|
|
{ id: string; name: string; email: string; phone: string }[]
|
|
>([]);
|
|
|
|
// Modal visibility
|
|
const [showCategory, setShowCategory] = useState(false);
|
|
const [showIncoterms, setShowIncoterms] = useState(false);
|
|
const [showInviteChannel, setShowInviteChannel] = useState(false);
|
|
const [showDeadline, setShowDeadline] = useState(false);
|
|
|
|
const c = useInputColors();
|
|
|
|
useEffect(() => {
|
|
const d = new Date();
|
|
d.setDate(d.getDate() + 14);
|
|
setSubmissionDeadline(d.toISOString().split("T")[0]);
|
|
}, []);
|
|
|
|
const updateItem = (id: number, field: keyof Item, value: string) =>
|
|
setItems((prev) =>
|
|
prev.map((it) => (it.id === id ? { ...it, [field]: value } : it)),
|
|
);
|
|
|
|
const addItem = () =>
|
|
setItems((prev) => [
|
|
...prev,
|
|
{
|
|
id: Date.now(),
|
|
itemName: "",
|
|
itemDescription: "",
|
|
quantity: "1",
|
|
unitOfMeasure: "unit",
|
|
},
|
|
]);
|
|
|
|
const removeItem = (id: number) => {
|
|
if (items.length > 1) setItems((prev) => prev.filter((it) => it.id !== id));
|
|
};
|
|
|
|
const setItemUnit = (id: number, unit: string) =>
|
|
updateItem(id, "unitOfMeasure", unit);
|
|
|
|
const handleNext = () => {
|
|
if (step === 0 && !title.trim()) {
|
|
toast.error("Validation", "Title is required");
|
|
return;
|
|
}
|
|
if (step === 1) {
|
|
if (items.length === 0) {
|
|
toast.error("Validation", "Add at least one item");
|
|
return;
|
|
}
|
|
if (items.every((it) => !it.itemName.trim())) {
|
|
toast.error("Validation", "At least one item must have a name");
|
|
return;
|
|
}
|
|
}
|
|
setStep((s) => s + 1);
|
|
};
|
|
|
|
const handleSubmit = async () => {
|
|
if (!title.trim()) {
|
|
toast.error("Validation", "Title is required");
|
|
return;
|
|
}
|
|
if (!submissionDeadline) {
|
|
toast.error("Validation", "Submission deadline is required");
|
|
return;
|
|
}
|
|
|
|
const payload = {
|
|
title: title.trim(),
|
|
description: description.trim() || undefined,
|
|
category,
|
|
submissionDeadline: new Date(submissionDeadline).toISOString(),
|
|
allowRevisions,
|
|
paymentTerms: paymentTerms.trim() || undefined,
|
|
incoterms: incoterms || undefined,
|
|
taxIncluded,
|
|
discountStructure: discountStructure.trim() || undefined,
|
|
validityPeriod: parseInt(validityPeriod, 10) || 30,
|
|
items: items
|
|
.filter((it) => it.itemName.trim())
|
|
.map((it) => ({
|
|
itemName: it.itemName.trim(),
|
|
itemDescription: it.itemDescription.trim() || undefined,
|
|
quantity: parseInt(it.quantity, 10) || 1,
|
|
unitOfMeasure: it.unitOfMeasure || "unit",
|
|
})),
|
|
customerIds: customerIds.length > 0 ? customerIds : undefined,
|
|
inviteChannel,
|
|
};
|
|
|
|
try {
|
|
setSubmitting(true);
|
|
await api.proformaRequests.create({
|
|
body: payload,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
toast.success("Success", "Proforma request created successfully!");
|
|
nav.back();
|
|
} catch (err: any) {
|
|
console.error("[ProformaRequestCreate] Error:", err);
|
|
toast.error(
|
|
"Error",
|
|
err?.response?.data?.message ||
|
|
err?.message ||
|
|
"Failed to create request",
|
|
);
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const totalQuantity = items.reduce(
|
|
(s, it) => s + (parseInt(it.quantity, 10) || 0),
|
|
0,
|
|
);
|
|
const namedItemCount = items.filter((it) => it.itemName.trim()).length;
|
|
|
|
return (
|
|
<ScreenWrapper className="bg-background">
|
|
<FormFlow
|
|
steps={STEPS}
|
|
currentStep={step}
|
|
onNext={handleNext}
|
|
onBack={() => setStep(step - 1)}
|
|
onComplete={handleSubmit}
|
|
loading={submitting}
|
|
completeLabel="Create Request"
|
|
>
|
|
{step === 0 && (
|
|
<View className="gap-5">
|
|
<Text className="text-[18px] font-sans-bold text-foreground tracking-tight">
|
|
Request Details
|
|
</Text>
|
|
<View className="bg-card rounded-[6px] gap-4">
|
|
<Field
|
|
label="Title"
|
|
value={title}
|
|
onChangeText={setTitle}
|
|
placeholder="e.g. Office Equipment Procurement 2024"
|
|
/>
|
|
<Field
|
|
label="Description"
|
|
value={description}
|
|
onChangeText={setDescription}
|
|
placeholder="Briefly describe what you're requesting"
|
|
multiline
|
|
/>
|
|
<PickerField
|
|
label="Category"
|
|
value={category}
|
|
onPress={() => setShowCategory(true)}
|
|
/>
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
{step === 1 && (
|
|
<View className="gap-5">
|
|
<View className="flex-row items-center justify-between">
|
|
<Text className="text-[18px] font-sans-bold text-foreground tracking-tight">
|
|
Requested Items
|
|
</Text>
|
|
<Pressable
|
|
onPress={addItem}
|
|
className="flex-row items-center gap-1 px-3 py-1.5 rounded-[6px] bg-primary/10 border border-primary/20"
|
|
>
|
|
<Plus color="#ea580c" size={14} strokeWidth={2.5} />
|
|
<Text className="text-primary text-[10px] font-sans-bold">
|
|
Add
|
|
</Text>
|
|
</Pressable>
|
|
</View>
|
|
<View className="gap-3">
|
|
{items.map((item, index) => (
|
|
<ItemRow
|
|
key={item.id}
|
|
item={item}
|
|
index={index}
|
|
canRemove={items.length > 1}
|
|
onUpdate={updateItem}
|
|
onRemove={removeItem}
|
|
onPickUnit={(u) => setItemUnit(item.id, u)}
|
|
/>
|
|
))}
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
{step === 2 && (
|
|
<View className="gap-5">
|
|
<Text className="text-[18px] font-sans-bold text-foreground tracking-tight">
|
|
Commercial Terms
|
|
</Text>
|
|
<View className="bg-card rounded-[6px] gap-4">
|
|
<Field
|
|
label="Payment Terms"
|
|
value={paymentTerms}
|
|
onChangeText={setPaymentTerms}
|
|
placeholder="e.g. Net 30 days"
|
|
/>
|
|
<View className="flex-row gap-4">
|
|
<PickerField
|
|
label="Incoterms"
|
|
value={incoterms}
|
|
onPress={() => setShowIncoterms(true)}
|
|
flex={2}
|
|
/>
|
|
<Field
|
|
label="Validity (days)"
|
|
value={validityPeriod}
|
|
onChangeText={setValidityPeriod}
|
|
placeholder="30"
|
|
numeric
|
|
center
|
|
flex={1}
|
|
/>
|
|
</View>
|
|
<Field
|
|
label="Discount Structure"
|
|
value={discountStructure}
|
|
onChangeText={setDiscountStructure}
|
|
placeholder="e.g. 5% discount for orders > 100 units"
|
|
multiline
|
|
/>
|
|
</View>
|
|
|
|
<Text className="text-[18px] font-sans-bold text-foreground tracking-tight">
|
|
Options
|
|
</Text>
|
|
<View className="bg-card rounded-[6px] gap-5">
|
|
<ToggleRow
|
|
title="Tax Included"
|
|
subtitle="Prices quoted include applicable taxes"
|
|
value={taxIncluded}
|
|
onValueChange={setTaxIncluded}
|
|
/>
|
|
<View className="h-px bg-border" />
|
|
<ToggleRow
|
|
title="Allow Revisions"
|
|
subtitle="Suppliers may submit revised quotes"
|
|
value={allowRevisions}
|
|
onValueChange={setAllowRevisions}
|
|
/>
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
{step === 3 && (
|
|
<View className="gap-5">
|
|
<Text className="text-[18px] font-sans-bold text-foreground tracking-tight">
|
|
Deadline & Invitation
|
|
</Text>
|
|
<View className="bg-card rounded-[6px] gap-4">
|
|
<PickerField
|
|
label="Submission Deadline"
|
|
value={submissionDeadline}
|
|
onPress={() => setShowDeadline(true)}
|
|
/>
|
|
<PickerField
|
|
label="Invite Channel"
|
|
value={inviteChannel}
|
|
onPress={() => setShowInviteChannel(true)}
|
|
/>
|
|
<View>
|
|
<Text className="text-[14px] font-sans-bold mb-1.5 ml-1 text-foreground">
|
|
Customers
|
|
</Text>
|
|
<CustomerPicker
|
|
selectedIds={customerIds}
|
|
selectedCustomers={selectedCustomers}
|
|
onSelect={(ids, customers) => {
|
|
setCustomerIds(ids);
|
|
setSelectedCustomers(customers);
|
|
}}
|
|
placeholder="Select customers to invite"
|
|
/>
|
|
{selectedCustomers.length > 0 && (
|
|
<View className="flex-row flex-wrap gap-2 mt-2">
|
|
{selectedCustomers.map((cust) => (
|
|
<View
|
|
key={cust.id}
|
|
className="bg-primary/10 rounded-[4px] pl-3 pr-2 py-1.5 flex-row items-center gap-1.5"
|
|
>
|
|
<Text className="text-primary text-[11px] font-sans-bold">
|
|
{cust.name}
|
|
</Text>
|
|
<Pressable
|
|
onPress={() => {
|
|
setCustomerIds((ids) =>
|
|
ids.filter((id) => id !== cust.id),
|
|
);
|
|
setSelectedCustomers((prev) =>
|
|
prev.filter((c) => c.id !== cust.id),
|
|
);
|
|
}}
|
|
hitSlop={6}
|
|
>
|
|
<X size={12} color="#E46212" strokeWidth={3} />
|
|
</Pressable>
|
|
</View>
|
|
))}
|
|
</View>
|
|
)}
|
|
</View>
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
{step === 4 && (
|
|
<View className="gap-5 pb-4">
|
|
<Text className="text-[18px] font-sans-bold text-foreground tracking-tight">
|
|
Summary
|
|
</Text>
|
|
<View className="bg-card rounded-[6px] gap-3">
|
|
<Row label="Title" value={title} />
|
|
{description ? (
|
|
<Row label="Description" value={description} multiline />
|
|
) : null}
|
|
<Row label="Category" value={category} />
|
|
<View className="border-t border-border/40 my-1" />
|
|
{namedItemCount > 0 ? (
|
|
<View>
|
|
<Text className="text-[12px] text-muted-foreground font-sans-bold uppercase tracking-widest mb-2">
|
|
Items ({namedItemCount})
|
|
</Text>
|
|
{items
|
|
.filter((it) => it.itemName.trim())
|
|
.map((it, i) => (
|
|
<View
|
|
key={it.id}
|
|
className="flex-row justify-between py-1"
|
|
>
|
|
<Text
|
|
className="text-[13px] text-foreground font-sans-medium flex-1"
|
|
numberOfLines={1}
|
|
>
|
|
{it.itemName}
|
|
</Text>
|
|
<Text className="text-[13px] text-foreground font-sans-bold ml-3">
|
|
{it.quantity} {it.unitOfMeasure}
|
|
</Text>
|
|
</View>
|
|
))}
|
|
</View>
|
|
) : null}
|
|
<View className="border-t border-border/40 my-1" />
|
|
<Row label="Payment Terms" value={paymentTerms || "—"} />
|
|
<Row label="Incoterms" value={incoterms} />
|
|
<Row label="Validity" value={`${validityPeriod || 30} days`} />
|
|
{discountStructure ? (
|
|
<Row label="Discount" value={discountStructure} multiline />
|
|
) : null}
|
|
<View className="flex-row justify-between">
|
|
<Text className="text-[14px] text-muted-foreground font-sans-medium">
|
|
Tax Included
|
|
</Text>
|
|
<Text className="text-[14px] text-foreground font-sans-bold">
|
|
{taxIncluded ? "Yes" : "No"}
|
|
</Text>
|
|
</View>
|
|
<View className="flex-row justify-between">
|
|
<Text className="text-[14px] text-muted-foreground font-sans-medium">
|
|
Allow Revisions
|
|
</Text>
|
|
<Text className="text-[14px] text-foreground font-sans-bold">
|
|
{allowRevisions ? "Yes" : "No"}
|
|
</Text>
|
|
</View>
|
|
<View className="border-t border-border/40 my-1" />
|
|
<Row label="Deadline" value={submissionDeadline} />
|
|
<Row label="Invite Channel" value={inviteChannel} />
|
|
{selectedCustomers.length > 0 ? (
|
|
<View>
|
|
<Text className="text-[12px] text-muted-foreground font-sans-bold uppercase tracking-widest mb-2">
|
|
Customers ({selectedCustomers.length})
|
|
</Text>
|
|
{selectedCustomers.map((cust) => (
|
|
<Text
|
|
key={cust.id}
|
|
className="text-[13px] text-foreground font-sans-medium mb-1"
|
|
>
|
|
{cust.name}
|
|
</Text>
|
|
))}
|
|
</View>
|
|
) : null}
|
|
</View>
|
|
</View>
|
|
)}
|
|
</FormFlow>
|
|
|
|
<PickerModal
|
|
visible={showCategory}
|
|
onClose={() => setShowCategory(false)}
|
|
title="Select Category"
|
|
>
|
|
{CATEGORIES.map((cat) => (
|
|
<SelectOption
|
|
key={cat}
|
|
label={cat}
|
|
value={cat}
|
|
selected={category === cat}
|
|
onSelect={(v) => {
|
|
setCategory(v);
|
|
setShowCategory(false);
|
|
}}
|
|
/>
|
|
))}
|
|
</PickerModal>
|
|
|
|
<PickerModal
|
|
visible={showIncoterms}
|
|
onClose={() => setShowIncoterms(false)}
|
|
title="Select Incoterms"
|
|
>
|
|
{INCOTERMS.map((t) => (
|
|
<SelectOption
|
|
key={t}
|
|
label={t}
|
|
value={t}
|
|
selected={incoterms === t}
|
|
onSelect={(v) => {
|
|
setIncoterms(v);
|
|
setShowIncoterms(false);
|
|
}}
|
|
/>
|
|
))}
|
|
</PickerModal>
|
|
|
|
<PickerModal
|
|
visible={showInviteChannel}
|
|
onClose={() => setShowInviteChannel(false)}
|
|
title="Invite Channel"
|
|
>
|
|
{INVITE_CHANNELS.map((ch) => (
|
|
<SelectOption
|
|
key={ch}
|
|
label={ch}
|
|
value={ch}
|
|
selected={inviteChannel === ch}
|
|
onSelect={(v) => {
|
|
setInviteChannel(v);
|
|
setShowInviteChannel(false);
|
|
}}
|
|
/>
|
|
))}
|
|
</PickerModal>
|
|
|
|
<PickerModal
|
|
visible={showDeadline}
|
|
onClose={() => setShowDeadline(false)}
|
|
title="Submission Deadline"
|
|
>
|
|
<CalendarGrid
|
|
selectedDate={submissionDeadline}
|
|
onSelect={(v) => {
|
|
setSubmissionDeadline(v);
|
|
setShowDeadline(false);
|
|
}}
|
|
/>
|
|
</PickerModal>
|
|
</ScreenWrapper>
|
|
);
|
|
}
|
|
|
|
function ToggleRow({
|
|
title,
|
|
subtitle,
|
|
value,
|
|
onValueChange,
|
|
}: {
|
|
title: string;
|
|
subtitle: string;
|
|
value: boolean;
|
|
onValueChange: (v: boolean) => void;
|
|
}) {
|
|
return (
|
|
<View className="flex-row items-center justify-between p-1">
|
|
<View className="flex-1 pr-4">
|
|
<Text className="text-[14px] font-sans-bold text-foreground">
|
|
{title}
|
|
</Text>
|
|
<Text className="text-[12px] text-muted-foreground mt-0.5 font-sans-medium">
|
|
{subtitle}
|
|
</Text>
|
|
</View>
|
|
<Switch
|
|
value={value}
|
|
onValueChange={onValueChange}
|
|
trackColor={{ false: "#334155", true: "#ea580c" }}
|
|
thumbColor={value ? "#fff" : "#64748b"}
|
|
/>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function ItemRow({
|
|
item,
|
|
index,
|
|
canRemove,
|
|
onUpdate,
|
|
onRemove,
|
|
onPickUnit,
|
|
}: {
|
|
item: Item;
|
|
index: number;
|
|
canRemove: boolean;
|
|
onUpdate: (id: number, field: keyof Item, value: string) => void;
|
|
onRemove: (id: number) => void;
|
|
onPickUnit: (u: string) => void;
|
|
}) {
|
|
const [showUnits, setShowUnits] = useState(false);
|
|
const c = useInputColors();
|
|
return (
|
|
<View className="bg-card pb-4">
|
|
<View className="flex-row justify-between items-center mb-3">
|
|
<Text className="text-[16px] font-sans-bold text-foreground">
|
|
Item {index + 1}
|
|
</Text>
|
|
{canRemove && (
|
|
<Pressable onPress={() => onRemove(item.id)} hitSlop={8}>
|
|
<Trash2 color="#ef4444" size={16} />
|
|
</Pressable>
|
|
)}
|
|
</View>
|
|
<Field
|
|
label="Item Name"
|
|
placeholder="e.g. Laptop Computer"
|
|
value={item.itemName}
|
|
onChangeText={(v) => onUpdate(item.id, "itemName", v)}
|
|
/>
|
|
<View className="mt-4">
|
|
<Field
|
|
label="Item Description"
|
|
placeholder="Optional details or specs"
|
|
value={item.itemDescription}
|
|
onChangeText={(v) => onUpdate(item.id, "itemDescription", v)}
|
|
multiline
|
|
/>
|
|
</View>
|
|
<View className="flex-row gap-3 mt-4">
|
|
<Field
|
|
label="Qty"
|
|
placeholder="1"
|
|
numeric
|
|
center
|
|
value={item.quantity}
|
|
onChangeText={(v) => onUpdate(item.id, "quantity", v)}
|
|
flex={1}
|
|
/>
|
|
<View className="flex-1">
|
|
<Text className="text-[14px] font-sans-bold mb-1.5 ml-1 text-foreground">
|
|
Unit
|
|
</Text>
|
|
<Pressable
|
|
onPress={() => setShowUnits(true)}
|
|
className="h-10 px-3 border border-border rounded-[6px] flex-row items-center justify-between"
|
|
style={{ backgroundColor: c.bg, borderColor: c.border }}
|
|
>
|
|
<Text className="text-xs font-sans-bold" style={{ color: c.text }}>
|
|
{item.unitOfMeasure || "unit"}
|
|
</Text>
|
|
<ChevronDown size={14} color={c.text} strokeWidth={3} />
|
|
</Pressable>
|
|
</View>
|
|
</View>
|
|
<PickerModal
|
|
visible={showUnits}
|
|
onClose={() => setShowUnits(false)}
|
|
title="Unit of Measure"
|
|
>
|
|
{UNITS.map((u) => (
|
|
<SelectOption
|
|
key={u}
|
|
label={u}
|
|
value={u}
|
|
selected={item.unitOfMeasure === u}
|
|
onSelect={(v) => {
|
|
onPickUnit(v);
|
|
setShowUnits(false);
|
|
}}
|
|
/>
|
|
))}
|
|
</PickerModal>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function Row({
|
|
label,
|
|
value,
|
|
multiline,
|
|
}: {
|
|
label: string;
|
|
value: string;
|
|
multiline?: boolean;
|
|
}) {
|
|
return (
|
|
<View className="flex-row justify-between">
|
|
<Text className="text-[14px] text-muted-foreground font-sans-medium">
|
|
{label}
|
|
</Text>
|
|
<Text
|
|
className="text-[14px] text-foreground font-sans-bold text-right flex-1 ml-4"
|
|
numberOfLines={multiline ? undefined : 2}
|
|
>
|
|
{value}
|
|
</Text>
|
|
</View>
|
|
);
|
|
}
|