import React, { useState, useEffect } from "react";
import {
View,
ScrollView,
Pressable,
TextInput,
StyleSheet,
ActivityIndicator,
useColorScheme,
Platform,
} from "react-native";
import { Text } from "@/components/ui/text";
import { Button } from "@/components/ui/button";
import {
ArrowLeft,
Plus,
Calendar,
ChevronDown,
FileText,
Trash2,
DollarSign,
Send,
CalendarSearch,
Upload,
} from "@/lib/icons";
import { ScreenWrapper } from "@/components/ScreenWrapper";
import { useSirouRouter } from "@sirou/react-native";
import { AppRoutes } from "@/lib/routes";
import { Stack } from "expo-router";
import { ShadowWrapper } from "@/components/ShadowWrapper";
import { api, BASE_URL } from "@/lib/api";
import { toast } from "@/lib/toast-store";
import { useAuthStore } from "@/lib/auth-store";
import * as ImagePicker from "expo-image-picker";
import { PickerModal, SelectOption } from "@/components/PickerModal";
import { CalendarGrid } from "@/components/CalendarGrid";
import { StandardHeader } from "@/components/StandardHeader";
import { getPlaceholderColor } from "@/lib/colors";
type Item = { id: number; description: string; qty: string; price: string };
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,
multiline = false,
}: {
label: string;
value: string;
onChangeText: (v: string) => void;
placeholder: string;
numeric?: boolean;
center?: boolean;
flex?: number;
multiline?: boolean;
}) {
const c = useInputColors();
return (
{label}
);
}
export default function CreateInvoiceScreen() {
const nav = useSirouRouter();
const [submitting, setSubmitting] = useState(false);
// Form Fields
const [invoiceNumber, setInvoiceNumber] = useState("");
const [customerName, setCustomerName] = useState("");
const [customerEmail, setCustomerEmail] = useState("");
const [customerPhone, setCustomerPhone] = useState("");
const [description, setDescription] = useState("");
const [currency, setCurrency] = useState("USD");
const [type, setType] = useState("SALES");
const [status, setStatus] = useState("DRAFT");
const [taxAmount, setTaxAmount] = useState("0");
const [discountAmount, setDiscountAmount] = useState("0");
const [notes, setNotes] = useState("");
// Dates
const [issueDate, setIssueDate] = useState(
new Date().toISOString().split("T")[0],
);
const [dueDate, setDueDate] = useState("");
// Items List
const [items, setItems] = useState- ([
{ id: 1, description: "", qty: "1", price: "" },
]);
// Modal states
const [showCurrency, setShowCurrency] = useState(false);
const [showType, setShowType] = useState(false);
const [showStatus, setShowStatus] = useState(false);
const [showIssueDate, setShowIssueDate] = useState(false);
const [showDueDate, setShowDueDate] = useState(false);
const [scanning, setScanning] = useState(false);
const token = useAuthStore((s) => s.token);
const handlePickImage = async () => {
try {
const { status } =
await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== "granted") {
toast.error(
"Permission Denied",
"We need access to your gallery to upload invoices.",
);
return;
}
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
quality: 0.8,
});
if (!result.canceled && result.assets && result.assets.length > 0) {
const uri = result.assets[0].uri;
await handleProcessImage(uri);
}
} catch (e: any) {
console.error("[CreateInvoice] Pick Image Error:", e);
toast.error("Picker Failed", "Could not launch gallery picker.");
}
};
const handleProcessImage = async (uri: string) => {
setScanning(true);
toast.info("Processing...", "Uploading invoice to AI extraction engine.");
try {
const formData = new FormData();
const fileExt = uri.split(".").pop() || "jpg";
const fileName = `invoice-${Date.now()}.${fileExt}`;
const type = `image/${fileExt === "jpg" ? "jpeg" : fileExt}`;
formData.append("file", {
uri: Platform.OS === "android" ? uri : uri.replace("file://", ""),
name: fileName,
type: type,
} as any);
const response = await fetch(`${BASE_URL}scan/invoice`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/json",
},
body: formData,
});
if (!response.ok) {
const err = await response
.json()
.catch(() => ({ message: "Scan processing failed." }));
throw new Error(err.message || "AI extraction failed.");
}
const scanResult = await response.json();
console.log("[CreateInvoice] Extracted scan result:", scanResult);
if (!scanResult.success) {
throw new Error(
scanResult.message || "AI extraction was unsuccessful.",
);
}
toast.success("Success!", "Data extracted successfully.");
const ocr = scanResult.data || {};
if (ocr.invoiceNumber) setInvoiceNumber(ocr.invoiceNumber);
let name = ocr.customerName?.trim() || "";
name = name
.replace(/^Customer Name:\s*/i, "")
.replace(/^Bill To:\s*/i, "");
if (name) setCustomerName(name);
if (ocr.customerEmail) setCustomerEmail(ocr.customerEmail);
if (ocr.customerPhone) setCustomerPhone(ocr.customerPhone);
if (ocr.description) setDescription(ocr.description);
if (ocr.currency) setCurrency(ocr.currency);
if (ocr.taxAmount != null) setTaxAmount(String(ocr.taxAmount));
if (ocr.issueDate) {
try {
const formattedIssue = new Date(ocr.issueDate)
.toISOString()
.split("T")[0];
setIssueDate(formattedIssue);
} catch (de) {
console.warn("[CreateInvoice] Issue Date parse error:", de);
}
}
if (ocr.dueDate) {
try {
const formattedDue = new Date(ocr.dueDate)
.toISOString()
.split("T")[0];
setDueDate(formattedDue);
} catch (de) {
console.warn("[CreateInvoice] Due Date parse error:", de);
}
}
if (ocr.items && ocr.items.length > 0) {
setItems(
ocr.items.map((item: any, idx: number) => ({
id: idx + 1,
description: item.description || "Web Development Service",
qty: String(item.quantity || "1"),
price: String(item.unitPrice || item.total || "0"),
})),
);
}
} catch (err: any) {
console.error("[CreateInvoice] Extraction Error:", err);
toast.error(
"Extraction Failed",
err.message || "AI was unable to extract invoice data.",
);
} finally {
setScanning(false);
}
};
const colorScheme = useColorScheme();
const isDark = colorScheme === "dark";
const c = useInputColors();
// Auto-generate invoice number and set default due date on mount
useEffect(() => {
const year = new Date().getFullYear();
const random = Math.floor(1000 + Math.random() * 9000);
setInvoiceNumber(`INV-${year}-${random}`);
// Default Due Date: 30 days from now
const d = new Date();
d.setDate(d.getDate() + 30);
setDueDate(d.toISOString().split("T")[0]);
}, []);
const addItem = () => {
const newId =
items.length > 0 ? Math.max(...items.map((i) => i.id)) + 1 : 1;
setItems([...items, { id: newId, description: "", qty: "1", price: "" }]);
};
const removeItem = (id: number) => {
if (items.length > 1) {
setItems(items.filter((i) => i.id !== id));
}
};
const updateField = (id: number, field: keyof Item, value: string) => {
setItems(items.map((i) => (i.id === id ? { ...i, [field]: value } : i)));
};
const subtotal = items.reduce(
(sum, item) =>
sum + (parseFloat(item.qty) || 0) * (parseFloat(item.price) || 0),
0,
);
const total =
subtotal + (parseFloat(taxAmount) || 0) - (parseFloat(discountAmount) || 0);
const handleSubmit = async () => {
if (!invoiceNumber) {
toast.error("Validation Error", "Invoice Number is required");
return;
}
if (!customerName) {
toast.error("Validation Error", "Customer Name is required");
return;
}
setSubmitting(true);
try {
const payload = {
invoiceNumber,
customerName,
customerEmail,
customerPhone,
amount: Number(total.toFixed(2)),
currency,
type,
status,
issueDate: new Date(issueDate).toISOString(),
dueDate: new Date(dueDate).toISOString(),
description: description || `Invoice for ${customerName}`,
notes,
taxAmount: parseFloat(taxAmount) || 0,
discountAmount: parseFloat(discountAmount) || 0,
isScanned: false,
scannedData: {
sellerTIN: "123456",
items: [],
},
items: items.map((item) => ({
description: item.description || "Web Development Service",
quantity: parseFloat(item.qty) || 0,
unitPrice: parseFloat(item.price) || 0,
total: Number(
(
(parseFloat(item.qty) || 0) * (parseFloat(item.price) || 0)
).toFixed(2),
),
})),
};
await api.invoices.create({ body: payload });
toast.success("Success", "Invoice created successfully!");
nav.back();
} catch (error: any) {
console.error("[CreateInvoice] Error:", error);
toast.error("Error", error.message || "Failed to create invoice");
} finally {
setSubmitting(false);
}
};
const currencies = ["USD", "ETB", "EUR", "GBP", "KES", "ZAR"];
const invoiceTypes = ["SALES", "PURCHASE", "SERVICE"];
const invoiceStatuses = ["DRAFT", "PENDING", "PAID"];
return (
{/* Gallery Scanner */}
{scanning ? (
) : (
)}
{scanning ? "Extracting Data..." : "Scan From Gallery"}
Upload invoice image to automatically prefill form
{/* General Info */}
{/* Customer Details */}
{/* Schedule & Configuration */}
Issue Date
setShowIssueDate(true)}
className="h-11 px-3 border border-border rounded-[6px] flex-row items-center justify-between"
style={{ backgroundColor: c.bg, borderColor: c.border }}
>
{issueDate}
Due Date
setShowDueDate(true)}
className="h-11 px-3 border border-border rounded-[6px] flex-row items-center justify-between"
style={{ backgroundColor: c.bg, borderColor: c.border }}
>
{dueDate || "Select Date"}
Currency
setShowCurrency(true)}
className="h-11 px-3 border border-border rounded-[6px] flex-row items-center justify-between"
style={{ backgroundColor: c.bg, borderColor: c.border }}
>
{currency}
Type
setShowType(true)}
className="h-11 px-3 border border-border rounded-[6px] flex-row items-center justify-between"
style={{ backgroundColor: c.bg, borderColor: c.border }}
>
{type}
Status
setShowStatus(true)}
className="h-11 px-3 border border-border rounded-[6px] flex-row items-center justify-between"
style={{ backgroundColor: c.bg, borderColor: c.border }}
>
{status}
{/* Billable Items */}
Add Item
{items.map((item, index) => (
Item {index + 1}
{items.length > 1 && (
removeItem(item.id)} hitSlop={8}>
)}
updateField(item.id, "description", v)}
/>
updateField(item.id, "qty", v)}
flex={1}
/>
updateField(item.id, "price", v)}
flex={2}
/>
Total
{currency}
{(
(parseFloat(item.qty) || 0) *
(parseFloat(item.price) || 0)
).toFixed(2)}
))}
{/* Totals & Taxes */}
Subtotal
{currency} {subtotal.toLocaleString()}
{/* Notes */}
{/* Footer */}
Total Amount
{currency}{" "}
{total.toLocaleString("en-US", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}
{/* Currency Modal */}
setShowCurrency(false)}
title="Select Currency"
>
{currencies.map((curr) => (
{
setCurrency(v);
setShowCurrency(false);
}}
/>
))}
{/* Type Modal */}
setShowType(false)}
title="Select Invoice Type"
>
{invoiceTypes.map((t) => (
{
setType(v);
setShowType(false);
}}
/>
))}
{/* Status Modal */}
setShowStatus(false)}
title="Select Invoice Status"
>
{invoiceStatuses.map((s) => (
{
setStatus(v);
setShowStatus(false);
}}
/>
))}
{/* Issue Date Modal */}
setShowIssueDate(false)}
title="Select Issue Date"
>
{
setIssueDate(v);
setShowIssueDate(false);
}}
/>
{/* Due Date Modal */}
setShowDueDate(false)}
title="Select Due Date"
>
{
setDueDate(v);
setShowDueDate(false);
}}
/>
);
}
function Label({
children,
noMargin,
}: {
children: string;
noMargin?: boolean;
}) {
return (
{children}
);
}