824 lines
28 KiB
TypeScript
824 lines
28 KiB
TypeScript
import React, { useState, useEffect, useMemo } from "react";
|
|
import {
|
|
View,
|
|
Pressable,
|
|
TextInput,
|
|
StyleSheet,
|
|
ActivityIndicator,
|
|
ScrollView,
|
|
Modal,
|
|
Dimensions,
|
|
} from "react-native";
|
|
import { useColorScheme } from "nativewind";
|
|
import * as DocumentPicker from "expo-document-picker";
|
|
import { Text } from "@/components/ui/text";
|
|
import {
|
|
ChevronDown,
|
|
Upload,
|
|
X,
|
|
FileText,
|
|
Plus,
|
|
Link2,
|
|
Search,
|
|
} from "@/lib/icons";
|
|
import { ScreenWrapper } from "@/components/ScreenWrapper";
|
|
import { useSirouRouter } from "@sirou/react-native";
|
|
import { useLocalSearchParams } from "expo-router";
|
|
import { AppRoutes } from "@/lib/routes";
|
|
import { api, BASE_URL } from "@/lib/api";
|
|
import { toast } from "@/lib/toast-store";
|
|
import { useAuthStore } from "@/lib/auth-store";
|
|
import { PickerModal, SelectOption } from "@/components/PickerModal";
|
|
import { FormFlow } from "@/components/FormFlow";
|
|
import { getPlaceholderColor } from "@/lib/colors";
|
|
|
|
const { height: SCREEN_HEIGHT } = Dimensions.get("window");
|
|
|
|
type DeclarationType = "VAT" | "WITHHOLDING_TAX";
|
|
type Period = "MONTHLY" | "QUARTERLY" | "SEMI_ANNUAL" | "ANNUAL" | "ONE_TIME" | "";
|
|
type FileEntry = { uri: string; name: string; type: string };
|
|
|
|
const DECLARATION_TYPES: SelectOption[] = [
|
|
{ label: "VAT", value: "VAT" },
|
|
{ label: "Withholding Tax", value: "WITHHOLDING_TAX" },
|
|
];
|
|
|
|
const PERIODS: SelectOption[] = [
|
|
{ label: "Monthly", value: "MONTHLY" },
|
|
{ label: "Quarterly", value: "QUARTERLY" },
|
|
{ label: "Semi-Annual", value: "SEMI_ANNUAL" },
|
|
{ label: "Annual", value: "ANNUAL" },
|
|
{ label: "One-Time", value: "ONE_TIME" },
|
|
];
|
|
|
|
const S = StyleSheet.create({
|
|
input: {
|
|
paddingHorizontal: 12,
|
|
paddingVertical: 12,
|
|
fontSize: 12,
|
|
fontWeight: "500",
|
|
borderRadius: 6,
|
|
borderWidth: 1,
|
|
textAlignVertical: "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,
|
|
flex,
|
|
multiline = false,
|
|
required = false,
|
|
}: {
|
|
label: string;
|
|
value: string;
|
|
onChangeText: (v: string) => void;
|
|
placeholder: string;
|
|
numeric?: boolean;
|
|
flex?: number;
|
|
multiline?: boolean;
|
|
required?: 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}
|
|
{required && <Text className="text-red-500"> *</Text>}
|
|
</Text>
|
|
<TextInput
|
|
style={[
|
|
S.input,
|
|
{ backgroundColor: c.bg, borderColor: c.border, color: c.text },
|
|
multiline ? { height: 80, paddingTop: 10 } : {},
|
|
]}
|
|
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,
|
|
required = false,
|
|
}: {
|
|
label: string;
|
|
value: string;
|
|
onPress: () => void;
|
|
required?: boolean;
|
|
}) {
|
|
const c = useInputColors();
|
|
return (
|
|
<View className="flex-1">
|
|
<Text className="text-[14px] font-sans-bold mb-1.5 ml-1 text-foreground">
|
|
{label}
|
|
{required && <Text className="text-red-500"> *</Text>}
|
|
</Text>
|
|
<Pressable
|
|
onPress={onPress}
|
|
className="h-11 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}
|
|
</Text>
|
|
<ChevronDown size={14} color={c.text} strokeWidth={3} />
|
|
</Pressable>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
export default function EditDeclarationScreen() {
|
|
const nav = useSirouRouter<AppRoutes>();
|
|
const { id } = useLocalSearchParams<{ id: string }>();
|
|
const { colorScheme } = useColorScheme();
|
|
const isDark = colorScheme === "dark";
|
|
const c = useInputColors();
|
|
|
|
const [initialLoading, setInitialLoading] = useState(true);
|
|
const [step, setStep] = useState(0);
|
|
const [submitting, setSubmitting] = useState(false);
|
|
|
|
const [declarationNumber, setDeclarationNumber] = useState("");
|
|
const [type, setType] = useState<DeclarationType>("VAT");
|
|
const [title, setTitle] = useState("");
|
|
const [period, setPeriod] = useState<Period>("");
|
|
const [periodStart, setPeriodStart] = useState("");
|
|
const [periodEnd, setPeriodEnd] = useState("");
|
|
|
|
const [tin, setTin] = useState("");
|
|
const [taxAccountNumber, setTaxAccountNumber] = useState("");
|
|
const [taxCentre, setTaxCentre] = useState("");
|
|
const [selectedInvoices, setSelectedInvoices] = useState<any[]>([]);
|
|
|
|
const [declarationFile, setDeclarationFile] = useState<FileEntry | null>(null);
|
|
const [receiptFiles, setReceiptFiles] = useState<FileEntry[]>([]);
|
|
const [suggestedFilename, setSuggestedFilename] = useState("");
|
|
|
|
const [notes, setNotes] = useState("");
|
|
const [dueDate, setDueDate] = useState("");
|
|
|
|
const [showTypePicker, setShowTypePicker] = useState(false);
|
|
const [showPeriodPicker, setShowPeriodPicker] = useState(false);
|
|
const [showInvoicePicker, setShowInvoicePicker] = useState(false);
|
|
const [invoices, setInvoices] = useState<any[]>([]);
|
|
const [invoiceSearch, setInvoiceSearch] = useState("");
|
|
const [loadingInvoices, setLoadingInvoices] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const load = async () => {
|
|
try {
|
|
const res = await api.declarations.getById({ params: { id } });
|
|
const d = res?.data ?? res;
|
|
if (!d) throw new Error("Not found");
|
|
|
|
setDeclarationNumber(d.declarationNumber || "");
|
|
setType(d.type || "VAT");
|
|
setTitle(d.title || "");
|
|
setPeriod(d.period || "");
|
|
setPeriodStart(
|
|
d.periodStart
|
|
? new Date(d.periodStart).toISOString().split("T")[0]
|
|
: "",
|
|
);
|
|
setPeriodEnd(
|
|
d.periodEnd
|
|
? new Date(d.periodEnd).toISOString().split("T")[0]
|
|
: "",
|
|
);
|
|
setTin(d.tin || "");
|
|
setTaxAccountNumber(d.taxAccountNumber || "");
|
|
setTaxCentre(d.taxCentre || "");
|
|
setSelectedInvoices(d.invoices || []);
|
|
setNotes(d.notes || "");
|
|
setDueDate(
|
|
d.dueDate
|
|
? new Date(d.dueDate).toISOString().split("T")[0]
|
|
: "",
|
|
);
|
|
setSuggestedFilename(d.suggestedFilename || "");
|
|
} catch (err: any) {
|
|
toast.error("Error", "Failed to load declaration");
|
|
nav.back();
|
|
} finally {
|
|
setInitialLoading(false);
|
|
}
|
|
};
|
|
load();
|
|
}, [id]);
|
|
|
|
const steps = [
|
|
{ key: "details", label: "Declaration Detail" },
|
|
{ key: "tax", label: "Tax Info" },
|
|
{ key: "files", label: "Files" },
|
|
{ key: "notes", label: "Notes" },
|
|
{ key: "review", label: "Review" },
|
|
];
|
|
|
|
const openInvoicePicker = async () => {
|
|
setLoadingInvoices(true);
|
|
try {
|
|
const response = await api.invoices.getAll({ query: { limit: 50 } });
|
|
setInvoices(Array.isArray(response) ? response : response.data || []);
|
|
setInvoiceSearch("");
|
|
setShowInvoicePicker(true);
|
|
} catch {
|
|
toast.error("Error", "Failed to fetch invoices");
|
|
} finally {
|
|
setLoadingInvoices(false);
|
|
}
|
|
};
|
|
|
|
const toggleInvoice = (inv: any) => {
|
|
setSelectedInvoices((prev) =>
|
|
prev.find((i) => i.id === inv.id)
|
|
? prev.filter((i) => i.id !== inv.id)
|
|
: [...prev, inv],
|
|
);
|
|
};
|
|
|
|
const filteredInvoices = useMemo(() => {
|
|
if (!invoiceSearch) return invoices;
|
|
const q = invoiceSearch.toLowerCase();
|
|
return invoices.filter(
|
|
(inv) =>
|
|
(inv.invoiceNumber || "").toLowerCase().includes(q) ||
|
|
(inv.customerName || "").toLowerCase().includes(q),
|
|
);
|
|
}, [invoices, invoiceSearch]);
|
|
|
|
const pickFile = async () => {
|
|
try {
|
|
const result = await DocumentPicker.getDocumentAsync({
|
|
type: "application/pdf",
|
|
copyToCacheDirectory: true,
|
|
});
|
|
if (!result.canceled && result.assets?.length > 0) {
|
|
const asset = result.assets[0];
|
|
setDeclarationFile({
|
|
uri: asset.uri,
|
|
name: asset.name,
|
|
type: asset.mimeType || "application/pdf",
|
|
});
|
|
}
|
|
} catch {
|
|
toast.warning("Picker", "Could not open document picker");
|
|
}
|
|
};
|
|
|
|
const pickReceipts = async () => {
|
|
try {
|
|
const result = await DocumentPicker.getDocumentAsync({
|
|
type: "*/*",
|
|
copyToCacheDirectory: true,
|
|
multiple: true,
|
|
});
|
|
if (!result.canceled && result.assets?.length > 0) {
|
|
setReceiptFiles((prev) => [
|
|
...prev,
|
|
...result.assets.map((a: any) => ({
|
|
uri: a.uri,
|
|
name: a.name,
|
|
type: a.mimeType || "application/octet-stream",
|
|
})),
|
|
]);
|
|
}
|
|
} catch {
|
|
toast.warning("Picker", "Could not open document picker");
|
|
}
|
|
};
|
|
|
|
const handleNext = () => {
|
|
if (step === 0) {
|
|
if (!declarationNumber.trim()) {
|
|
toast.error("Validation", "Declaration number is required");
|
|
return;
|
|
}
|
|
if (!title.trim()) {
|
|
toast.error("Validation", "Title is required");
|
|
return;
|
|
}
|
|
if (!periodStart.trim()) {
|
|
toast.error("Validation", "Period start is required");
|
|
return;
|
|
}
|
|
if (!periodEnd.trim()) {
|
|
toast.error("Validation", "Period end is required");
|
|
return;
|
|
}
|
|
}
|
|
setStep(step + 1);
|
|
};
|
|
|
|
const handleSubmit = async () => {
|
|
setSubmitting(true);
|
|
try {
|
|
const payload: Record<string, any> = {
|
|
declarationNumber: declarationNumber.trim(),
|
|
type,
|
|
period,
|
|
title: title.trim(),
|
|
periodStart: new Date(periodStart).toISOString(),
|
|
periodEnd: new Date(periodEnd).toISOString(),
|
|
};
|
|
if (tin.trim()) payload.tin = tin.trim();
|
|
if (taxAccountNumber.trim()) payload.taxAccountNumber = taxAccountNumber.trim();
|
|
if (taxCentre.trim()) payload.taxCentre = taxCentre.trim();
|
|
if (notes.trim()) payload.notes = notes.trim();
|
|
if (dueDate.trim()) payload.dueDate = new Date(dueDate).toISOString();
|
|
if (suggestedFilename.trim()) payload.suggestedFilename = suggestedFilename.trim();
|
|
if (selectedInvoices.length > 0) {
|
|
payload.invoiceIds = selectedInvoices.map((i) => i.id);
|
|
}
|
|
|
|
const hasFiles = declarationFile !== null || receiptFiles.length > 0;
|
|
|
|
if (hasFiles) {
|
|
const { accessToken } = useAuthStore.getState();
|
|
const formData = new FormData();
|
|
formData.append("data", JSON.stringify(payload));
|
|
if (declarationFile) {
|
|
formData.append("file", {
|
|
uri: declarationFile.uri,
|
|
name: declarationFile.name,
|
|
type: declarationFile.type,
|
|
} as any);
|
|
}
|
|
receiptFiles.forEach((rf) => {
|
|
formData.append("receipts", {
|
|
uri: rf.uri,
|
|
name: rf.name,
|
|
type: rf.type,
|
|
} as any);
|
|
});
|
|
const response = await fetch(`${BASE_URL}declarations/${id}`, {
|
|
method: "PUT",
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
"Content-Type": "multipart/form-data",
|
|
},
|
|
body: formData,
|
|
});
|
|
if (!response.ok) throw new Error(await response.text());
|
|
} else {
|
|
await api.declarations.update({ params: { id }, body: payload });
|
|
}
|
|
|
|
toast.success("Success", "Declaration updated");
|
|
nav.back();
|
|
} catch (error: any) {
|
|
toast.error(
|
|
"Error",
|
|
error?.response?.data?.message ||
|
|
error?.data?.message ||
|
|
error?.message ||
|
|
"Failed to update declaration",
|
|
);
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
};
|
|
|
|
if (initialLoading) {
|
|
return (
|
|
<ScreenWrapper className="bg-background">
|
|
<View className="flex-1 items-center justify-center">
|
|
<ActivityIndicator size="large" color="#E46212" />
|
|
</View>
|
|
</ScreenWrapper>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<ScreenWrapper className="bg-background">
|
|
<FormFlow
|
|
steps={steps}
|
|
currentStep={step}
|
|
onNext={handleNext}
|
|
onBack={() => setStep(step - 1)}
|
|
onComplete={handleSubmit}
|
|
loading={submitting}
|
|
completeLabel="Update Declaration"
|
|
>
|
|
{step === 0 && (
|
|
<View className="gap-5">
|
|
<Text className="text-[18px] font-sans-bold text-foreground tracking-tight">
|
|
Declaration Detail
|
|
</Text>
|
|
<View className="gap-4">
|
|
<Field
|
|
label="Declaration Number"
|
|
value={declarationNumber}
|
|
onChangeText={setDeclarationNumber}
|
|
placeholder="e.g. MOR-2024-001"
|
|
required
|
|
/>
|
|
<View className="flex-row gap-4">
|
|
<PickerField
|
|
label="Type"
|
|
value={type === "WITHHOLDING_TAX" ? "Withholding Tax" : type}
|
|
onPress={() => setShowTypePicker(true)}
|
|
/>
|
|
<PickerField
|
|
label="Period"
|
|
value={period || "Select"}
|
|
onPress={() => setShowPeriodPicker(true)}
|
|
/>
|
|
</View>
|
|
<Field
|
|
label="Title"
|
|
value={title}
|
|
onChangeText={setTitle}
|
|
placeholder="e.g. Monthly VAT - Jan 2024"
|
|
required
|
|
/>
|
|
<View className="flex-row gap-4">
|
|
<Field
|
|
label="Period Start"
|
|
value={periodStart}
|
|
onChangeText={setPeriodStart}
|
|
placeholder="2024-01-01"
|
|
required
|
|
flex={1}
|
|
/>
|
|
<Field
|
|
label="Period End"
|
|
value={periodEnd}
|
|
onChangeText={setPeriodEnd}
|
|
placeholder="2024-01-31"
|
|
required
|
|
flex={1}
|
|
/>
|
|
</View>
|
|
<Field
|
|
label="Due Date"
|
|
value={dueDate}
|
|
onChangeText={setDueDate}
|
|
placeholder="2024-02-15 (optional)"
|
|
/>
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
{step === 1 && (
|
|
<View className="gap-5">
|
|
<Text className="text-[18px] font-sans-bold text-foreground tracking-tight">
|
|
Tax Info
|
|
</Text>
|
|
<View className="gap-4">
|
|
<View className="flex-row gap-4">
|
|
<Field
|
|
label="TIN"
|
|
value={tin}
|
|
onChangeText={setTin}
|
|
placeholder="Tax ID (optional)"
|
|
flex={1}
|
|
/>
|
|
<Field
|
|
label="Tax Account"
|
|
value={taxAccountNumber}
|
|
onChangeText={setTaxAccountNumber}
|
|
placeholder="Account # (optional)"
|
|
flex={1}
|
|
/>
|
|
</View>
|
|
<Field
|
|
label="Tax Centre"
|
|
value={taxCentre}
|
|
onChangeText={setTaxCentre}
|
|
placeholder="e.g. Addis Ababa (optional)"
|
|
/>
|
|
<View>
|
|
<Text className="text-[14px] font-sans-bold mb-1.5 ml-1 text-foreground">
|
|
Link Invoices
|
|
</Text>
|
|
<Pressable
|
|
onPress={openInvoicePicker}
|
|
className="h-11 px-3 border border-border rounded-[6px] flex-row items-center justify-between"
|
|
style={{ backgroundColor: c.bg, borderColor: c.border }}
|
|
>
|
|
{loadingInvoices ? (
|
|
<ActivityIndicator color="#E46212" size="small" />
|
|
) : (
|
|
<>
|
|
<Text
|
|
className="text-xs font-sans-medium flex-1"
|
|
style={{
|
|
color:
|
|
selectedInvoices.length > 0
|
|
? c.text
|
|
: c.placeholder,
|
|
}}
|
|
numberOfLines={1}
|
|
>
|
|
{selectedInvoices.length > 0
|
|
? `${selectedInvoices.length} invoice${selectedInvoices.length > 1 ? "s" : ""} linked`
|
|
: "Select invoices to link (optional)"}
|
|
</Text>
|
|
<Link2 size={14} color="#E46212" strokeWidth={2.5} />
|
|
</>
|
|
)}
|
|
</Pressable>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
{step === 2 && (
|
|
<View className="gap-5">
|
|
<Text className="text-[18px] font-sans-bold text-foreground tracking-tight">
|
|
File Uploads
|
|
</Text>
|
|
<View className="gap-4">
|
|
<View>
|
|
<Text className="text-[14px] font-sans-bold mb-1.5 ml-1 text-foreground">
|
|
MoR Declaration PDF
|
|
</Text>
|
|
{declarationFile ? (
|
|
<View className="flex-row items-center px-3 py-2.5 rounded-[6px] border border-border bg-card">
|
|
<FileText size={16} color="#E46212" strokeWidth={2} />
|
|
<Text
|
|
className="flex-1 text-xs font-sans-medium ml-2 text-foreground"
|
|
numberOfLines={1}
|
|
>
|
|
{declarationFile.name}
|
|
</Text>
|
|
<Pressable onPress={() => setDeclarationFile(null)}>
|
|
<X size={14} color="#ef4444" strokeWidth={2.5} />
|
|
</Pressable>
|
|
</View>
|
|
) : (
|
|
<Pressable
|
|
onPress={pickFile}
|
|
className="h-14 rounded-[6px] border border-dashed border-border items-center justify-center"
|
|
>
|
|
<Upload size={18} color="#E46212" strokeWidth={2} />
|
|
<Text className="text-primary text-xs font-sans-bold mt-1">
|
|
Tap to select PDF
|
|
</Text>
|
|
</Pressable>
|
|
)}
|
|
</View>
|
|
|
|
<View>
|
|
<Text className="text-[14px] font-sans-bold mb-1.5 ml-1 text-foreground">
|
|
Payment Receipts
|
|
</Text>
|
|
{receiptFiles.length > 0 && (
|
|
<View className="gap-2 mb-3">
|
|
{receiptFiles.map((rf, idx) => (
|
|
<View
|
|
key={idx}
|
|
className="flex-row items-center px-3 py-2.5 rounded-[6px] border border-border bg-card"
|
|
>
|
|
<FileText size={14} color="#94a3b8" strokeWidth={2} />
|
|
<Text
|
|
className="flex-1 text-xs font-sans-medium ml-2 text-foreground"
|
|
numberOfLines={1}
|
|
>
|
|
{rf.name}
|
|
</Text>
|
|
<Pressable
|
|
onPress={() =>
|
|
setReceiptFiles((p) =>
|
|
p.filter((_, i) => i !== idx),
|
|
)
|
|
}
|
|
>
|
|
<X size={14} color="#ef4444" strokeWidth={2.5} />
|
|
</Pressable>
|
|
</View>
|
|
))}
|
|
</View>
|
|
)}
|
|
<Pressable
|
|
onPress={pickReceipts}
|
|
className="h-12 rounded-[6px] border border-dashed border-border items-center justify-center flex-row gap-2"
|
|
>
|
|
<Plus size={16} color="#E46212" strokeWidth={2.5} />
|
|
<Text className="text-primary text-xs font-sans-bold">
|
|
Add Receipt
|
|
</Text>
|
|
</Pressable>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
{step === 3 && (
|
|
<View className="gap-5">
|
|
<Field
|
|
label="Notes"
|
|
value={notes}
|
|
onChangeText={setNotes}
|
|
placeholder="Any additional notes (optional)"
|
|
multiline
|
|
/>
|
|
</View>
|
|
)}
|
|
|
|
{step === 4 && (
|
|
<View className="gap-5">
|
|
<Text className="text-[18px] font-sans-bold text-foreground tracking-tight">
|
|
Summary
|
|
</Text>
|
|
<View className="bg-card rounded-[6px] border border-border p-4 gap-3">
|
|
<SummaryRow label="Declaration #" value={declarationNumber} />
|
|
<SummaryRow
|
|
label="Type"
|
|
value={type === "WITHHOLDING_TAX" ? "Withholding Tax" : type}
|
|
/>
|
|
<SummaryRow label="Title" value={title} />
|
|
<SummaryRow label="Period" value={period} />
|
|
<SummaryRow label="Period Start" value={periodStart} />
|
|
<SummaryRow label="Period End" value={periodEnd} />
|
|
{dueDate ? <SummaryRow label="Due Date" value={dueDate} /> : null}
|
|
{tin ? <SummaryRow label="TIN" value={tin} /> : null}
|
|
{taxAccountNumber ? (
|
|
<SummaryRow label="Tax Account" value={taxAccountNumber} />
|
|
) : null}
|
|
{taxCentre ? (
|
|
<SummaryRow label="Tax Centre" value={taxCentre} />
|
|
) : null}
|
|
{selectedInvoices.length > 0 ? (
|
|
<SummaryRow
|
|
label="Linked Invoices"
|
|
value={`${selectedInvoices.length} invoice${selectedInvoices.length > 1 ? "s" : ""}`}
|
|
/>
|
|
) : null}
|
|
{notes && <SummaryRow label="Notes" value={notes} />}
|
|
</View>
|
|
{declarationFile && (
|
|
<View className="bg-card rounded-[6px] p-4 gap-3">
|
|
<SummaryRow label="Declaration PDF" value={declarationFile.name} />
|
|
</View>
|
|
)}
|
|
{receiptFiles.length > 0 && (
|
|
<View className="bg-card rounded-[6px] p-4 gap-2">
|
|
<Text className="text-[14px] text-foreground font-sans-bold">
|
|
Receipts ({receiptFiles.length})
|
|
</Text>
|
|
{receiptFiles.map((rf, i) => (
|
|
<Text key={i} className="text-[14px] text-foreground font-sans-bold">
|
|
{i + 1}. {rf.name}
|
|
</Text>
|
|
))}
|
|
</View>
|
|
)}
|
|
</View>
|
|
)}
|
|
</FormFlow>
|
|
|
|
<Modal
|
|
visible={showInvoicePicker}
|
|
transparent
|
|
animationType="slide"
|
|
onRequestClose={() => setShowInvoicePicker(false)}
|
|
>
|
|
<Pressable
|
|
className="flex-1 bg-black/40"
|
|
onPress={() => setShowInvoicePicker(false)}
|
|
>
|
|
<View className="flex-1 justify-end">
|
|
<Pressable
|
|
className="bg-card rounded-t-[36px] overflow-hidden border-t-[3px] border-border/20"
|
|
style={{ maxHeight: SCREEN_HEIGHT * 0.75 }}
|
|
onPress={(e) => e.stopPropagation()}
|
|
>
|
|
<View className="px-6 pb-4 pt-4 flex-row justify-between items-center border-b border-border/40">
|
|
<Text className="text-foreground font-sans-bold text-[18px]">
|
|
Link Invoices
|
|
</Text>
|
|
<Pressable
|
|
onPress={() => setShowInvoicePicker(false)}
|
|
className="h-8 w-8 bg-secondary/80 rounded-full items-center justify-center border border-border/10"
|
|
>
|
|
<Text className="text-foreground text-xs font-sans-bold">✕</Text>
|
|
</Pressable>
|
|
</View>
|
|
<View className="px-5 py-3">
|
|
<View className="flex-row items-center bg-card border border-border rounded-xl px-3 h-10">
|
|
<Search size={16} color="#94a3b8" strokeWidth={2} />
|
|
<TextInput
|
|
className="flex-1 ml-2 text-foreground text-sm"
|
|
placeholder="Search invoices..."
|
|
placeholderTextColor={getPlaceholderColor(isDark)}
|
|
value={invoiceSearch}
|
|
onChangeText={setInvoiceSearch}
|
|
autoCapitalize="none"
|
|
/>
|
|
</View>
|
|
</View>
|
|
<ScrollView className="px-5" showsVerticalScrollIndicator={false}>
|
|
{filteredInvoices.map((inv: any) => {
|
|
const selected = selectedInvoices.find((i) => i.id === inv.id);
|
|
return (
|
|
<Pressable
|
|
key={inv.id}
|
|
onPress={() => toggleInvoice(inv)}
|
|
className={`flex-row items-center py-3 px-3 rounded-[8px] mb-1 ${
|
|
selected ? "bg-primary/10" : ""
|
|
}`}
|
|
>
|
|
<View className="flex-1">
|
|
<Text className="text-foreground font-sans-bold text-sm">
|
|
{inv.customerName || "Unknown"}
|
|
</Text>
|
|
<Text className="text-muted-foreground text-[11px] font-sans-medium">
|
|
#{inv.invoiceNumber} · {inv.currency}{" "}
|
|
{Number(inv.amount?.value || inv.amount || 0).toLocaleString()}
|
|
</Text>
|
|
</View>
|
|
{selected && (
|
|
<View className="h-5 w-5 rounded-full bg-primary items-center justify-center">
|
|
<Text className="text-white text-[10px] font-sans-bold">✓</Text>
|
|
</View>
|
|
)}
|
|
</Pressable>
|
|
);
|
|
})}
|
|
{filteredInvoices.length === 0 && (
|
|
<Text className="text-muted-foreground text-sm font-sans-medium text-center py-8">
|
|
No invoices found
|
|
</Text>
|
|
)}
|
|
</ScrollView>
|
|
</Pressable>
|
|
</View>
|
|
</Pressable>
|
|
</Modal>
|
|
|
|
<PickerModal
|
|
visible={showTypePicker}
|
|
title="Declaration Type"
|
|
onClose={() => setShowTypePicker(false)}
|
|
>
|
|
{DECLARATION_TYPES.map((opt) => (
|
|
<SelectOption
|
|
key={opt.value}
|
|
label={opt.label}
|
|
value={opt.value}
|
|
selected={type === opt.value}
|
|
onSelect={(v) => {
|
|
setType(v as DeclarationType);
|
|
setShowTypePicker(false);
|
|
}}
|
|
/>
|
|
))}
|
|
</PickerModal>
|
|
<PickerModal
|
|
visible={showPeriodPicker}
|
|
title="Period"
|
|
onClose={() => setShowPeriodPicker(false)}
|
|
>
|
|
{PERIODS.map((opt) => (
|
|
<SelectOption
|
|
key={opt.value}
|
|
label={opt.label}
|
|
value={opt.value}
|
|
selected={period === opt.value}
|
|
onSelect={(v) => {
|
|
setPeriod(v as Period);
|
|
setShowPeriodPicker(false);
|
|
}}
|
|
/>
|
|
))}
|
|
</PickerModal>
|
|
</ScreenWrapper>
|
|
);
|
|
}
|
|
|
|
function SummaryRow({ label, value }: { label: string; value: string }) {
|
|
return (
|
|
<View className="flex-row justify-between">
|
|
<Text className="text-[14px] text-foreground font-sans-bold">{label}</Text>
|
|
<Text className="text-[14px] text-foreground font-sans-bold text-right flex-1 ml-4">
|
|
{value}
|
|
</Text>
|
|
</View>
|
|
);
|
|
}
|