diff --git a/.windsurf/workflows/login.md b/.windsurf/workflows/login.md new file mode 100644 index 0000000..e69de29 diff --git a/app.json b/app.json index e2aa970..8f16cbe 100644 --- a/app.json +++ b/app.json @@ -13,7 +13,8 @@ "backgroundColor": "#ffffff" }, "ios": { - "supportsTablet": true + "supportsTablet": true, + "bundleIdentifier": "com.yaltopia.ticketapp" }, "android": { "adaptiveIcon": { @@ -22,8 +23,23 @@ }, "edgeToEdgeEnabled": true, "predictiveBackGestureEnabled": false, - "package": "com.yaltopia.ticketapp" + "package": "com.yaltopia.ticketapp", + "permissions": [ + "android.permission.READ_SMS", + "android.permission.RECEIVE_SMS", + "android.permission.CAMERA", + "android.permission.RECORD_AUDIO" + ] }, + "plugins": [ + [ + "expo-camera", + { + "cameraPermission": "Allow Yaltopia Tickets App to access your camera to scan invoices." + } + ], + ["@react-native-google-signin/google-signin"] + ], "web": { "favicon": "./assets/favicon.png", "bundler": "metro" diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index e366d7f..53f4750 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -1,13 +1,19 @@ import { Tabs, router } from "expo-router"; import { Home, ScanLine, FileText, Wallet, Newspaper, Scan } from "@/lib/icons"; +import { useColorScheme } from "nativewind"; import { Platform, View, Pressable } from "react-native"; -import { ShadowWrapper } from "@/components/ShadowWrapper"; -const NAV_BG = "#ffffff"; -const ACTIVE_TINT = "#ea580c"; +const ACTIVE_TINT = "rgba(228, 98, 18, 1)"; const INACTIVE_TINT = "#94a3b8"; export default function TabsLayout() { + const { colorScheme } = useColorScheme(); + const isDark = colorScheme === "dark"; + + const NAV_BG = + colorScheme === "dark" ? "rgba(31,31,31, 1)" : "rgba(255,255,255, 1)"; + const BORDER_COLOR = isDark ? "#1e293b" : "#ffffff"; + return ( ( + + ), tabBarLabelStyle: { fontSize: 9, fontWeight: "700", @@ -25,7 +34,11 @@ export default function TabsLayout() { tabBarStyle: { backgroundColor: NAV_BG, borderTopWidth: 0, - elevation: 10, + elevation: isDark ? 0 : 6, + shadowColor: "#000", + shadowOffset: { width: 0, height: -10 }, + shadowOpacity: isDark ? 0 : 0.1, + shadowRadius: 20, height: Platform.OS === "ios" ? 75 : 75, paddingBottom: Platform.OS === "ios" ? 30 : 10, paddingTop: 10, @@ -35,10 +48,6 @@ export default function TabsLayout() { left: 20, right: 20, borderRadius: 32, - shadowColor: "#000", - shadowOffset: { width: 0, height: 10 }, - shadowOpacity: 0.12, - shadowRadius: 20, }, }} > @@ -74,11 +83,14 @@ export default function TabsLayout() { color: INACTIVE_TINT, }, tabBarIcon: ({ focused }) => ( - - + + - + ), }} /> diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index d8d6a04..9cec6e1 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -1,19 +1,26 @@ import React, { useState } from "react"; -import { View, ScrollView, Pressable, ActivityIndicator } from "react-native"; +import { + View, + ScrollView, + Pressable, + ActivityIndicator, + useColorScheme, +} from "react-native"; import { api } from "@/lib/api"; import { Text } from "@/components/ui/text"; +import { EmptyState } from "@/components/EmptyState"; import { Card, CardContent } from "@/components/ui/card"; import { useSirouRouter } from "@sirou/react-native"; import { AppRoutes } from "@/lib/routes"; import { Plus, - Send, History as HistoryIcon, Briefcase, ChevronRight, Clock, DollarSign, FileText, + ScanLine, } from "@/lib/icons"; import { ScreenWrapper } from "@/components/ScreenWrapper"; import { ShadowWrapper } from "@/components/ShadowWrapper"; @@ -74,22 +81,24 @@ export default function HomeScreen() { setLoading(false); } }; + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; return ( - - + {/* Balance Card Section */} + - + - {/* Circular Quick Actions Section */} } + icon={ + + } label="Company" onPress={() => nav.go("company")} /> } - label="Send" - onPress={() => nav.go("(tabs)/proforma")} + icon={ + + } + label="Scan SMS" + onPress={() => nav.go("sms-scan")} /> + } - label="History" - onPress={() => nav.go("history")} - /> - } + icon={ + + } label="Create Proforma" onPress={() => nav.go("proforma/create")} /> + + } + label="History" + onPress={() => nav.go("history")} + /> {/* Recent Activity Header */} @@ -253,7 +286,7 @@ export default function HomeScreen() { ${Number(inv.amount).toLocaleString()} )) ) : ( - - No transactions found + + nav.go("proforma/create")} + previewLines={3} + /> )} + ); @@ -294,16 +335,18 @@ function QuickAction({ onPress?: () => void; }) { return ( - - + + {icon} - - + {label} diff --git a/app/(tabs)/news.tsx b/app/(tabs)/news.tsx index 3374226..d48dac7 100644 --- a/app/(tabs)/news.tsx +++ b/app/(tabs)/news.tsx @@ -15,8 +15,11 @@ import { AppRoutes } from "@/lib/routes"; import { Newspaper, ChevronRight, Clock } from "@/lib/icons"; import { ScreenWrapper } from "@/components/ScreenWrapper"; import { StandardHeader } from "@/components/StandardHeader"; +import { EmptyState } from "@/components/EmptyState"; +import { PERMISSION_MAP, hasPermission } from "@/lib/permissions"; import { api, newsApi } from "@/lib/api"; import { ShadowWrapper } from "@/components/ShadowWrapper"; +import { useAuthStore } from "@/lib/auth-store"; const { width } = Dimensions.get("window"); const LATEST_CARD_WIDTH = width * 0.8; @@ -33,6 +36,7 @@ interface NewsItem { export default function NewsScreen() { const nav = useSirouRouter(); + const permissions = useAuthStore((s: { permissions: string[] }) => s.permissions); // Safe accessor to handle initialization race conditions const getNewsApi = () => { @@ -53,6 +57,8 @@ export default function NewsScreen() { const [refreshing, setRefreshing] = useState(false); + // Check permissions (none for viewing news) + const fetchLatest = async () => { try { setLoadingLatest(true); @@ -217,8 +223,6 @@ export default function NewsScreen() { return ( - - } > + {/* Latest News Section */} @@ -251,10 +256,13 @@ export default function NewsScreen() { ))} ) : ( - - - No latest items - + + )} @@ -289,19 +297,13 @@ export default function NewsScreen() { )} ) : ( - - + - - No news items available - )} diff --git a/app/(tabs)/payments.tsx b/app/(tabs)/payments.tsx index ce0a05d..1526c3a 100644 --- a/app/(tabs)/payments.tsx +++ b/app/(tabs)/payments.tsx @@ -19,11 +19,14 @@ import { Wallet, ChevronRight, AlertTriangle, + Plus, } from "@/lib/icons"; import { ScreenWrapper } from "@/components/ScreenWrapper"; import { StandardHeader } from "@/components/StandardHeader"; import { toast } from "@/lib/toast-store"; import { useAuthStore } from "@/lib/auth-store"; +import { EmptyState } from "@/components/EmptyState"; +import { PERMISSION_MAP, hasPermission } from "@/lib/permissions"; const PRIMARY = "#ea580c"; @@ -52,6 +55,7 @@ interface Payment { export default function PaymentsScreen() { const nav = useSirouRouter(); + const permissions = useAuthStore((s) => s.permissions); const [payments, setPayments] = useState([]); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); @@ -59,6 +63,9 @@ export default function PaymentsScreen() { const [hasMore, setHasMore] = useState(true); const [loadingMore, setLoadingMore] = useState(false); + // Check permissions + const canCreatePayments = hasPermission(permissions, PERMISSION_MAP["payments:create"]); + const fetchPayments = useCallback( async (pageNum: number, isRefresh = false) => { const { isAuthenticated } = useAuthStore.getState(); @@ -199,10 +206,9 @@ export default function PaymentsScreen() { return ( - { const isCloseToBottom = @@ -213,15 +219,18 @@ export default function PaymentsScreen() { }} scrollEventThrottle={400} > - + + + + {/* Flagged Section */} {categorized.flagged.length > 0 && ( @@ -247,9 +256,14 @@ export default function PaymentsScreen() { {categorized.pending.length > 0 ? ( categorized.pending.map((p) => renderPaymentItem(p, "pending")) ) : ( - - No pending matches. - + + + )} @@ -265,9 +279,14 @@ export default function PaymentsScreen() { renderPaymentItem(p, "reconciled"), ) ) : ( - - No reconciled payments. - + + + )} @@ -276,6 +295,7 @@ export default function PaymentsScreen() { )} + ); diff --git a/app/(tabs)/proforma.tsx b/app/(tabs)/proforma.tsx index 3071bca..10b4189 100644 --- a/app/(tabs)/proforma.tsx +++ b/app/(tabs)/proforma.tsx @@ -10,26 +10,68 @@ import { Text } from "@/components/ui/text"; import { Card } from "@/components/ui/card"; import { useSirouRouter } from "@sirou/react-native"; import { AppRoutes } from "@/lib/routes"; -import { Plus, Send, FileText, Clock } from "@/lib/icons"; +import { Plus, Send, FileText, Clock, ChevronRight } from "@/lib/icons"; import { ScreenWrapper } from "@/components/ScreenWrapper"; import { StandardHeader } from "@/components/StandardHeader"; import { Button } from "@/components/ui/button"; +import { EmptyState } from "@/components/EmptyState"; import { api } from "@/lib/api"; import { useAuthStore } from "@/lib/auth-store"; +import { PERMISSION_MAP, hasPermission } from "@/lib/permissions"; interface ProformaItem { id: string; proformaNumber: string; customerName: string; + customerEmail: string; + customerPhone: string; amount: any; currency: string; issueDate: string; dueDate: string; description: string; + notes: string; + taxAmount: any; + discountAmount: any; + pdfPath: string; + userId: string; + items: any[]; + createdAt: string; + updatedAt: string; } +const dummyData: ProformaItem = { + id: "dummy-1", + proformaNumber: "PF-001", + customerName: "John Doe", + customerEmail: "john@example.com", + customerPhone: "+1234567890", + amount: { value: 1000, currency: "USD" }, + currency: "USD", + issueDate: "2026-03-10T11:51:36.134Z", + dueDate: "2026-03-10T11:51:36.134Z", + description: "Dummy proforma", + notes: "Test notes", + taxAmount: { value: 100, currency: "USD" }, + discountAmount: { value: 50, currency: "USD" }, + pdfPath: "dummy.pdf", + userId: "user-1", + items: [ + { + id: "item-1", + description: "Test item", + quantity: 1, + unitPrice: { value: 1000, currency: "USD" }, + total: { value: 1000, currency: "USD" } + } + ], + createdAt: "2026-03-10T11:51:36.134Z", + updatedAt: "2026-03-10T11:51:36.134Z" +}; + export default function ProformaScreen() { const nav = useSirouRouter(); + const permissions = useAuthStore((s) => s.permissions); const [proformas, setProformas] = useState([]); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); @@ -37,6 +79,9 @@ export default function ProformaScreen() { const [hasMore, setHasMore] = useState(true); const [loadingMore, setLoadingMore] = useState(false); + // Check permissions + const canCreateProformas = hasPermission(permissions, PERMISSION_MAP["proforma:create"]); + const fetchProformas = useCallback( async (pageNum: number, isRefresh = false) => { const { isAuthenticated } = useAuthStore.getState(); @@ -51,7 +96,10 @@ export default function ProformaScreen() { query: { page: pageNum, limit: 10 }, }); - const newData = response.data; + let newProformas = response.data; + + + const newData = newProformas; if (isRefresh) { setProformas(newData); } else { @@ -59,11 +107,11 @@ export default function ProformaScreen() { pageNum === 1 ? newData : [...prev, ...newData], ); } - setHasMore(response.meta.hasNextPage); setPage(pageNum); } catch (err: any) { console.error("[Proforma] Fetch error:", err); + setHasMore(false); } finally { setLoading(false); setRefreshing(false); @@ -91,101 +139,90 @@ export default function ProformaScreen() { const renderProformaItem: ListRenderItem = ({ item }) => { const amountVal = typeof item.amount === "object" ? item.amount.value : item.amount; - const dateStr = new Date(item.issueDate).toLocaleDateString(); + const issuedStr = item.issueDate + ? new Date(item.issueDate).toLocaleDateString() + : ""; + const dueStr = item.dueDate ? new Date(item.dueDate).toLocaleDateString() : ""; + const itemsCount = Array.isArray(item.items) ? item.items.length : 0; return ( - nav.go("proforma/[id]", { id: item.id })} - className="mb-3" - > - - - - - - - - - {item.currency || "$"} - {amountVal?.toLocaleString()} - - - {item.proformaNumber} - - - - - - {item.customerName} - - {item.description && ( - - {item.description} - - )} - - - - - - - + + nav.go("proforma/[id]", { id: item.id })} + className="mb-3" + > + + + + + - - Issued: {dateStr} - - - { - e.stopPropagation(); - // Handle share - }} - > - - - Share - - + + + + + {item.proformaNumber || "Proforma"} + + + {item.customerName || "Customer"} + + + + + + {item.currency || "$"} + {amountVal?.toLocaleString?.() ?? amountVal ?? "0"} + + + + + + + + Issued: {issuedStr} | Due: {dueStr} | {itemsCount} item{itemsCount !== 1 ? "s" : ""} + + + + + + - - - + + + ); }; return ( - - item.id} - contentContainerStyle={{ padding: 20, paddingBottom: 150 }} + contentContainerStyle={{ paddingBottom: 150 }} showsVerticalScrollIndicator={false} onRefresh={onRefresh} refreshing={refreshing} onEndReached={loadMore} onEndReachedThreshold={0.5} ListHeaderComponent={ - + <> + + + {/* {canCreateProformas && ( */} + + {/* )} */} + + } ListFooterComponent={ loadingMore ? ( @@ -194,8 +231,13 @@ export default function ProformaScreen() { } ListEmptyComponent={ !loading ? ( - - No proformas found + + ) : ( diff --git a/app/(tabs)/scan.tsx b/app/(tabs)/scan.tsx index 195a28b..23d576d 100644 --- a/app/(tabs)/scan.tsx +++ b/app/(tabs)/scan.tsx @@ -118,7 +118,7 @@ export default function ScanScreen() { if (!permission.granted) { return ( - + @@ -175,9 +175,9 @@ export default function ScanScreen() { {/* Scan Frame */} - - - + + + Align Invoice Within Frame diff --git a/app/_layout.tsx b/app/_layout.tsx index 08a54cf..20d8d6d 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -6,15 +6,18 @@ import { GestureHandlerRootView } from "react-native-gesture-handler"; import { Toast } from "@/components/Toast"; import "@/global.css"; import { SafeAreaProvider } from "react-native-safe-area-context"; -import { View, ActivityIndicator } from "react-native"; -import { useRestoreTheme } from "@/lib/theme"; +import { View, ActivityIndicator, InteractionManager } from "react-native"; +import { useRestoreTheme, NAV_THEME } from "@/lib/theme"; import { SirouRouterProvider, useSirouRouter } from "@sirou/react-native"; +import { NavigationContainer, NavigationIndependentTree, ThemeProvider } from "@react-navigation/native"; import { routes } from "@/lib/routes"; import { authGuard, guestGuard } from "@/lib/auth-guards"; import { useAuthStore } from "@/lib/auth-store"; +import { useFonts } from 'expo-font'; import { api } from "@/lib/api"; +import { useColorScheme } from 'react-native'; -import { useSegments, router as expoRouter } from "expo-router"; +import { useSegments } from "expo-router"; function BackupGuard() { const segments = useSegments(); @@ -28,13 +31,9 @@ function BackupGuard() { useEffect(() => { if (!isMounted) return; - const rootSegment = segments[0]; - const isPublic = rootSegment === "login" || rootSegment === "register"; - - if (!isAuthed && !isPublic && segments.length > 0) { - console.log("[BackupGuard] Safety redirect to /login"); - expoRouter.replace("/login"); - } + // Intentionally disabled: redirecting here can happen before the root layout + // navigator is ready and cause "Attempted to navigate before mounting". + // Sirou guards handle redirects. }, [segments, isAuthed, isMounted]); return null; @@ -64,8 +63,10 @@ function SirouBridge() { const result = await (sirou as any).checkGuards(routeName); if (!result.allowed && result.redirect) { console.log(`[SirouBridge] REDIRECT -> ${result.redirect}`); - // Use expoRouter for filesystem navigation - expoRouter.replace(`/${result.redirect}`); + // Use Sirou navigation safely + InteractionManager.runAfterInteractions(() => { + sirou.go(result.redirect); + }); } } catch (e: any) { console.warn( @@ -82,9 +83,21 @@ function SirouBridge() { } export default function RootLayout() { + const colorScheme = useColorScheme(); useRestoreTheme(); const [isMounted, setIsMounted] = useState(false); const [hasHydrated, setHasHydrated] = useState(false); + const [fontsLoaded] = useFonts({ + 'DMSans-Regular': require('../assets/fonts/DMSans-Regular.ttf'), + 'DMSans-Bold': require('../assets/fonts/DMSans-Bold.ttf'), + 'DMSans-Medium': require('../assets/fonts/DMSans-Medium.ttf'), + 'DMSans-SemiBold': require('../assets/fonts/DMSans-SemiBold.ttf'), + 'DMSans-Light': require('../assets/fonts/DMSans-Light.ttf'), + 'DMSans-ExtraLight': require('../assets/fonts/DMSans-ExtraLight.ttf'), + 'DMSans-Thin': require('../assets/fonts/DMSans-Thin.ttf'), + 'DMSans-Black': require('../assets/fonts/DMSans-Black.ttf'), + 'DMSans-ExtraBold': require('../assets/fonts/DMSans-ExtraBold.ttf'), + }); useEffect(() => { setIsMounted(true); @@ -103,14 +116,14 @@ export default function RootLayout() { initializeAuth(); }, []); - if (!isMounted || !hasHydrated) { + if (!isMounted || !hasHydrated || !fontsLoaded) { return ( @@ -119,70 +132,88 @@ export default function RootLayout() { } return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); } diff --git a/app/company-details.tsx b/app/company-details.tsx new file mode 100644 index 0000000..5fe9b38 --- /dev/null +++ b/app/company-details.tsx @@ -0,0 +1,220 @@ +import React, { useEffect, useState } from "react"; +import { View, ActivityIndicator, ScrollView, Image } from "react-native"; +import { ScreenWrapper } from "@/components/ScreenWrapper"; +import { StandardHeader } from "@/components/StandardHeader"; +import { Text } from "@/components/ui/text"; +import { Card, CardContent } from "@/components/ui/card"; +import { api } from "@/lib/api"; + +export default function CompanyDetailsScreen() { + const [loading, setLoading] = useState(true); + const [company, setCompany] = useState(null); + + useEffect(() => { + const load = async () => { + try { + setLoading(true); + const res = await api.company.get(); + setCompany(res?.data ?? res); + } finally { + setLoading(false); + } + }; + load(); + }, []); + + return ( + + + + {loading ? ( + + + + ) : ( + + {/* Logo */} + {company?.logoPath && ( + + + + + + )} + + {/* Basic Info */} + + + + + Company Name + + + {company?.name ?? "—"} + + + + {company?.tin && ( + + + TIN + + + {company.tin} + + + )} + + + + {/* Contact */} + + + + Contact Information + + + + + + Phone + + + {company?.phone ?? "—"} + + + + + + Email + + + {company?.email ?? "—"} + + + + {company?.website && ( + + + Website + + + {company.website} + + + )} + + + + + {/* Address */} + + + + Address + + + + + + Street Address + + + {company?.address ?? "—"} + + + + + + + City + + + {company?.city ?? "—"} + + + + + + State + + + {company?.state ?? "—"} + + + + + + + + Zip Code + + + {company?.zipCode ?? "—"} + + + + + + Country + + + {company?.country ?? "—"} + + + + + + + + {/* System Info */} + + + + System Information + + + + + + User ID + + + {company?.userId ?? "—"} + + + + + + Created + + + {company?.createdAt ? new Date(company.createdAt).toLocaleString() : "—"} + + + + + + Last Updated + + + {company?.updatedAt ? new Date(company.updatedAt).toLocaleString() : "—"} + + + + + + + + + )} + + ); +} diff --git a/app/company.tsx b/app/company.tsx index 814920a..13fd82d 100644 --- a/app/company.tsx +++ b/app/company.tsx @@ -16,7 +16,9 @@ import { ShadowWrapper } from "@/components/ShadowWrapper"; import { useSirouRouter } from "@sirou/react-native"; import { AppRoutes } from "@/lib/routes"; import { Stack } from "expo-router"; +import { useAuthStore } from "@/lib/auth-store"; import { api } from "@/lib/api"; +import { getPlaceholderColor } from "@/lib/colors"; import { UserPlus, Search, @@ -24,6 +26,7 @@ import { Phone, ChevronRight, Briefcase, + Info, } from "@/lib/icons"; export default function CompanyScreen() { @@ -67,7 +70,7 @@ export default function CompanyScreen() { return ( - + {/* Search Bar */} @@ -77,7 +80,7 @@ export default function CompanyScreen() { diff --git a/app/edit-profile.tsx b/app/edit-profile.tsx index e4be4b0..921fe5e 100644 --- a/app/edit-profile.tsx +++ b/app/edit-profile.tsx @@ -89,7 +89,7 @@ export default function EditProfileScreen() { > First Name - + Last Name - + Appearance and choose Light, Dark, or System.", + }, + { + q: "Where can I find my invoices and proformas?", + a: "Use the tabs on the bottom navigation to browse Invoices/Payments and Proformas.", + }, + { + q: "Why am I seeing an API error?", + a: "If your backend is rate-limiting or the database schema is missing columns, the app may show errors. Contact your admin or check the server logs.", + }, +]; + +export default function HelpScreen() { + return ( + + + + + + + + FAQ + + + Quick answers to common questions. + + + + + {FAQ.map((item) => ( + + + {item.q} + {item.a} + + + ))} + + + + Need more help? + + Placeholder — add contact info (email/phone/WhatsApp) or a support chat link here. + + + + + + ); +} diff --git a/app/history.tsx b/app/history.tsx index a46ae68..1340def 100644 --- a/app/history.tsx +++ b/app/history.tsx @@ -14,6 +14,7 @@ import { import { ScreenWrapper } from "@/components/ScreenWrapper"; import { ShadowWrapper } from "@/components/ShadowWrapper"; import { StandardHeader } from "@/components/StandardHeader"; +import { EmptyState } from "@/components/EmptyState"; import { api } from "@/lib/api"; import { Stack } from "expo-router"; @@ -174,11 +175,15 @@ export default function HistoryScreen() { )) ) : ( - - - - No activity found - + + nav.go("proforma/create")} + previewLines={4} + /> )} diff --git a/app/invoices/[id].tsx b/app/invoices/[id].tsx index 267d592..a7fee7f 100644 --- a/app/invoices/[id].tsx +++ b/app/invoices/[id].tsx @@ -212,25 +212,65 @@ export default function InvoiceDetailScreen() { + {/* Notes Section (New) */} + {invoice.notes && ( + + + + Additional Notes + + + {invoice.notes} + + + + )} + + {/* Timeline Section (New) */} + + + + Created + + + {new Date(invoice.createdAt).toLocaleString()} + + + + + Last Updated + + + {new Date(invoice.updatedAt).toLocaleString()} + + + + {/* Actions */} diff --git a/app/login.tsx b/app/login.tsx index 468ef08..95b61ec 100644 --- a/app/login.tsx +++ b/app/login.tsx @@ -14,19 +14,36 @@ import { useSirouRouter } from "@sirou/react-native"; import { AppRoutes } from "@/lib/routes"; import { Text } from "@/components/ui/text"; import { Button } from "@/components/ui/button"; -import { Mail, Lock, ArrowRight, Eye, EyeOff, Chrome, User } from "@/lib/icons"; +import { Mail, Lock, ArrowRight, Eye, EyeOff, Chrome, User, Globe } from "@/lib/icons"; import { ScreenWrapper } from "@/components/ScreenWrapper"; import { useAuthStore } from "@/lib/auth-store"; import * as Linking from "expo-linking"; -import { api, BASE_URL } from "@/lib/api"; +import { api, BASE_URL, rbacApi } from "@/lib/api"; import { useColorScheme } from "nativewind"; import { toast } from "@/lib/toast-store"; +import { useLanguageStore, AppLanguage } from "@/lib/language-store"; +import { getPlaceholderColor } from "@/lib/colors"; +import { LanguageModal } from "@/components/LanguageModal"; +import { + GoogleSignin, + statusCodes, +} from "@react-native-google-signin/google-signin"; + +GoogleSignin.configure({ + webClientId: + "1028200585478-cnp6lpmp75mj4b79pnijiefv5rl1etqi.apps.googleusercontent.com", + iosClientId: + "1028200585478-cnp6lpmp75mj4b79pnijiefv5rl1etqi.apps.googleusercontent.com", // Placeholder: replace with your actual iOS Client ID from Google Cloud Console + offlineAccess: true, +}); export default function LoginScreen() { const nav = useSirouRouter(); const setAuth = useAuthStore((state) => state.setAuth); const { colorScheme } = useColorScheme(); const isDark = colorScheme === "dark"; + const { language, setLanguage } = useLanguageStore(); + const [languageModalVisible, setLanguageModalVisible] = useState(false); const [identifier, setIdentifier] = useState(""); const [password, setPassword] = useState(""); @@ -53,8 +70,16 @@ export default function LoginScreen() { // Using the new api.auth.login which is powered by simple-api const response = await api.auth.login({ body: payload }); - // Store user, access token, and refresh token - setAuth(response.user, response.accessToken, response.refreshToken); + // Store user, access token, refresh token, and permissions + // // Fetch roles to get permissions + // const rolesResponse = await rbacApi.roles(); + // const userRole = response.user.role; + // const roleData = rolesResponse.find((r: any) => r.role === userRole); + // const permissions = roleData ? roleData.permissions : []; + const permissions: string[] = []; + + // Store user, access token, refresh token, and permissions + setAuth(response.user, response.accessToken, response.refreshToken, permissions); toast.success("Welcome Back!", "You have successfully logged in."); // Explicitly navigate to home @@ -67,35 +92,71 @@ export default function LoginScreen() { }; const handleGoogleLogin = async () => { - setLoading(true); try { - // Hit api.auth.google directly — that's it - const response = await api.auth.google(); - setAuth(response.user, response.accessToken, response.refreshToken); + setLoading(true); + await GoogleSignin.hasPlayServices(); + const userInfo = await GoogleSignin.signIn(); + + // In newer versions of the library, the response is in data + // If using idToken, ensure you configured webClientId + const idToken = userInfo.data?.idToken || (userInfo as any).idToken; + + if (!idToken) { + throw new Error("Failed to obtain Google ID Token"); + } + + // Send idToken to our new consolidated endpoint + const response = await api.auth.googleMobile({ body: { idToken } }); + + // Fetch roles to get permissions + // const rolesResponse = await rbacApi.roles(); + // const userRole = response.user.role; + // const roleData = rolesResponse.find((r: any) => r.role === userRole); + // const permissions = roleData ? roleData.permissions : []; + const permissions: string[] = []; + + setAuth(response.user, response.accessToken, response.refreshToken, permissions); toast.success("Welcome!", "Signed in with Google."); nav.go("(tabs)"); - } catch (err: any) { - console.error("[Login] Google Login Error:", err); - toast.error( - "Google Login Failed", - err.message || "An unexpected error occurred.", - ); + } catch (error: any) { + if (error.code === statusCodes.SIGN_IN_CANCELLED) { + // User cancelled the login flow + } else if (error.code === statusCodes.IN_PROGRESS) { + toast.error("Login in progress", "Please wait..."); + } else if (error.code === statusCodes.PLAY_SERVICES_NOT_AVAILABLE) { + toast.error("Play Services", "Google Play Services not available"); + } else { + console.error("[Login] Google Error:", error); + toast.error( + "Google Login Failed", + error.message || "An error occurred", + ); + } } finally { setLoading(false); } }; return ( - + + + setLanguageModalVisible(true)} + className="p-2 rounded-full bg-card border border-border" + > + + + + {/* Logo / Branding */} @@ -112,12 +173,12 @@ export default function LoginScreen() { Email or Phone Number - + Password - + - + {loading ? ( + + + + ) : ( + + + + Preferences + + + + + + + + + + + Invoice reminders + + + Get reminders before invoices are due + + + + + + + + + + + + + Days before due date + + + Currently: {daysBeforeDueDate} days + + + setDaysModalVisible(true)}> + + + + + + + + + + + News alerts + + Product updates and announcements + + + + + + + + + + + Report ready + + Notify when reports are generated + + + + + + + + + )} + + + + + + setDaysModalVisible(false)} + > + {daysOptions.map((option) => ( + { + setDaysBeforeDueDate(value); + setDaysModalVisible(false); + }} + /> + ))} + + ); } diff --git a/app/payment-requests/create.tsx b/app/payment-requests/create.tsx new file mode 100644 index 0000000..e79e189 --- /dev/null +++ b/app/payment-requests/create.tsx @@ -0,0 +1,795 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { + ActivityIndicator, + Pressable, + ScrollView, + StyleSheet, + TextInput, + View, +} from "react-native"; +import { Stack } from "expo-router"; +import { useSirouRouter } from "@sirou/react-native"; +import { colorScheme, useColorScheme } from "nativewind"; + +import { api } from "@/lib/api"; +import { AppRoutes } from "@/lib/routes"; +import { toast } from "@/lib/toast-store"; +import { getPlaceholderColor } from "@/lib/colors"; + +import { ScreenWrapper } from "@/components/ScreenWrapper"; +import { StandardHeader } from "@/components/StandardHeader"; +import { ShadowWrapper } from "@/components/ShadowWrapper"; +import { PickerModal, SelectOption } from "@/components/PickerModal"; +import { CalendarGrid } from "@/components/CalendarGrid"; +import { Button } from "@/components/ui/button"; +import { Text } from "@/components/ui/text"; + +import { + Calendar, + CalendarSearch, + ChevronDown, + Plus, + Send, + Trash2, +} from "@/lib/icons"; + +type Item = { id: number; description: string; qty: string; price: string }; + +type Account = { + id: number; + bankName: string; + accountName: string; + accountNumber: string; + currency: 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: scheme } = useColorScheme(); + const dark = scheme === "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(); + const isDark = colorScheme.get() === "dark"; + + return ( + + + {label} + + + + ); +} + +function Label({ + children, + noMargin, +}: { + children: string; + noMargin?: boolean; +}) { + return ( + + {children} + + ); +} + +const CURRENCIES = ["USD", "ETB", "EUR", "GBP", "KES", "ZAR"]; +const STATUSES = ["DRAFT", "PENDING", "PAID", "CANCELLED"]; + +export default function CreatePaymentRequestScreen() { + const nav = useSirouRouter(); + + const [submitting, setSubmitting] = useState(false); + + const [paymentRequestNumber, setPaymentRequestNumber] = useState(""); + const [customerName, setCustomerName] = useState(""); + const [customerEmail, setCustomerEmail] = useState(""); + const [customerPhone, setCustomerPhone] = useState(""); + const [customerId, setCustomerId] = useState(""); + + const [amount, setAmount] = useState(""); + const [currency, setCurrency] = useState("USD"); + + const [description, setDescription] = useState(""); + const [notes, setNotes] = useState(""); + + const [taxAmount, setTaxAmount] = useState("0"); + const [discountAmount, setDiscountAmount] = useState("0"); + + const [issueDate, setIssueDate] = useState( + new Date().toISOString().split("T")[0], + ); + const [dueDate, setDueDate] = useState(""); + + const [paymentId, setPaymentId] = useState(""); + const [status, setStatus] = useState("DRAFT"); + + const [items, setItems] = useState([ + { id: 1, description: "", qty: "1", price: "" }, + ]); + + const [accounts, setAccounts] = useState([ + { + id: 1, + bankName: "", + accountName: "", + accountNumber: "", + currency: "ETB", + }, + ]); + + const c = useInputColors(); + + const [showCurrency, setShowCurrency] = useState(false); + const [showIssueDate, setShowIssueDate] = useState(false); + const [showDueDate, setShowDueDate] = useState(false); + const [showStatus, setShowStatus] = useState(false); + + useEffect(() => { + const year = new Date().getFullYear(); + const random = Math.floor(1000 + Math.random() * 9000); + setPaymentRequestNumber(`PAYREQ-${year}-${random}`); + + const d = new Date(); + d.setDate(d.getDate() + 30); + setDueDate(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(), description: "", qty: "1", price: "" }, + ]); + + const removeItem = (id: number) => { + if (items.length > 1) setItems((prev) => prev.filter((it) => it.id !== id)); + }; + + const updateAccount = (id: number, field: keyof Account, value: string) => + setAccounts((prev) => + prev.map((acc) => (acc.id === id ? { ...acc, [field]: value } : acc)), + ); + + const addAccount = () => + setAccounts((prev) => [ + ...prev, + { + id: Date.now(), + bankName: "", + accountName: "", + accountNumber: "", + currency: "ETB", + }, + ]); + + const removeAccount = (id: number) => { + if (accounts.length > 1) + setAccounts((prev) => prev.filter((acc) => acc.id !== id)); + }; + + const subtotal = useMemo( + () => + items.reduce( + (sum, item) => + sum + (parseFloat(item.qty) || 0) * (parseFloat(item.price) || 0), + 0, + ), + [items], + ); + + const computedTotal = useMemo( + () => subtotal + (parseFloat(taxAmount) || 0) - (parseFloat(discountAmount) || 0), + [subtotal, taxAmount, discountAmount], + ); + + const isDark = colorScheme.get() === "dark"; + + const handleSubmit = async () => { + if (!customerName) { + toast.error("Validation Error", "Please enter a customer name"); + return; + } + + const formattedPhone = customerPhone.startsWith("+") + ? customerPhone + : customerPhone.length > 0 + ? `+251${customerPhone}` + : ""; + + const body = { + paymentRequestNumber, + customerName, + customerEmail, + customerPhone: formattedPhone, + amount: amount ? Number(amount) : Number(computedTotal.toFixed(2)), + currency, + issueDate: new Date(issueDate).toISOString(), + dueDate: new Date(dueDate).toISOString(), + description: description || `Payment request for ${customerName}`, + notes, + taxAmount: parseFloat(taxAmount) || 0, + discountAmount: parseFloat(discountAmount) || 0, + status, + paymentId, + accounts: accounts.map((a) => ({ + bankName: a.bankName, + accountName: a.accountName, + accountNumber: a.accountNumber, + currency: a.currency, + })), + items: items.map((i) => ({ + description: i.description || "Item", + quantity: parseFloat(i.qty) || 0, + unitPrice: parseFloat(i.price) || 0, + total: Number( + ((parseFloat(i.qty) || 0) * (parseFloat(i.price) || 0)).toFixed(2), + ), + })), + customerId: customerId || undefined, + }; + + try { + setSubmitting(true); + await api.paymentRequests.create({ body }); + toast.success("Success", "Payment request created successfully!"); + nav.back(); + } catch (err: any) { + console.error("[PaymentRequestCreate] Error:", err); + toast.error("Error", err?.message || "Failed to create payment request"); + } finally { + setSubmitting(false); + } + }; + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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} + + + + + + + + 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} + + + + + + + + + + + + + + + + + + + Add Item + + + + + + {items.map((item, index) => ( + + + + + Item {index + 1} + + {items.length > 1 && ( + removeItem(item.id)} hitSlop={8}> + + + )} + + + updateItem(item.id, "description", v)} + /> + + + updateItem(item.id, "qty", v)} + flex={1} + /> + updateItem(item.id, "price", v)} + flex={2} + /> + + + Total + + + {currency} + {( + (parseFloat(item.qty) || 0) * + (parseFloat(item.price) || 0) + ).toFixed(2)} + + + + + + ))} + + + + + + + + Add Account + + + + + + {accounts.map((acc, index) => ( + + + + + Account {index + 1} + + {accounts.length > 1 && ( + removeAccount(acc.id)} hitSlop={8}> + + + )} + + + + updateAccount(acc.id, "bankName", v)} + placeholder="e.g. Yaltopia Bank" + /> + updateAccount(acc.id, "accountName", v)} + placeholder="e.g. Yaltopia Tech PLC" + /> + + + updateAccount(acc.id, "accountNumber", v) + } + placeholder="123456789" + flex={1} + /> + updateAccount(acc.id, "currency", v)} + placeholder="ETB" + flex={1} + /> + + + + + ))} + + + + + + + + Subtotal + + + {currency} {subtotal.toLocaleString()} + + + + + + + + + + + + + + + + + + + + Total Amount + + + {currency}{" "} + {(amount ? Number(amount) : computedTotal).toLocaleString("en-US", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} + + + + + + + + + + + + setShowCurrency(false)} + title="Select Currency" + > + {CURRENCIES.map((curr) => ( + { + setCurrency(v); + setShowCurrency(false); + }} + /> + ))} + + + setShowStatus(false)} + title="Select Status" + > + {STATUSES.map((s) => ( + { + setStatus(v); + setShowStatus(false); + }} + /> + ))} + + + setShowIssueDate(false)} + title="Select Issue Date" + > + { + setIssueDate(v); + setShowIssueDate(false); + }} + /> + + + setShowDueDate(false)} + title="Select Due Date" + > + { + setDueDate(v); + setShowDueDate(false); + }} + /> + + + ); +} diff --git a/app/privacy.tsx b/app/privacy.tsx new file mode 100644 index 0000000..10753e9 --- /dev/null +++ b/app/privacy.tsx @@ -0,0 +1,81 @@ +import { View, ScrollView } from "react-native"; +import { ScreenWrapper } from "@/components/ScreenWrapper"; +import { StandardHeader } from "@/components/StandardHeader"; +import { Text } from "@/components/ui/text"; + +export default function PrivacyScreen() { + return ( + + + + + + Privacy Policy + + + Last updated: March 10, 2026 + + + + 1. Introduction + + + This Privacy Policy describes how we collect, use, and share your personal information when you use our mobile application ("App"). + + + + 2. Information We Collect + + + We may collect information about you in various ways, including: + + + • Personal information you provide directly to us + + + • Information we collect automatically when you use the App + + + • Information from third-party services + + + + 3. How We Use Your Information + + + We use the information we collect to: + + + • Provide and maintain our services + + + • Process transactions and send related information + + + • Communicate with you about our services + + + + 4. Information Sharing + + + We do not sell, trade, or otherwise transfer your personal information to third parties without your consent, except as described in this policy. + + + + 5. Data Security + + + We implement appropriate security measures to protect your personal information against unauthorized access, alteration, disclosure, or destruction. + + + + 6. Contact Us + + + If you have any questions about this Privacy Policy, please contact us at privacy@example.com. + + + + ); +} diff --git a/app/profile.tsx b/app/profile.tsx index 8e47db0..b50ae26 100644 --- a/app/profile.tsx +++ b/app/profile.tsx @@ -5,9 +5,7 @@ import { Pressable, Image, Switch, - Modal, - TouchableOpacity, - TouchableWithoutFeedback, + InteractionManager, } from "react-native"; import { useSirouRouter } from "@sirou/react-native"; import { AppRoutes } from "@/lib/routes"; @@ -25,12 +23,16 @@ import { LogOut, User, Lock, + Globe, } from "@/lib/icons"; import { ScreenWrapper } from "@/components/ScreenWrapper"; import { ShadowWrapper } from "@/components/ShadowWrapper"; import { useColorScheme } from "nativewind"; import { saveTheme, AppTheme } from "@/lib/theme"; import { useAuthStore } from "@/lib/auth-store"; +import { useLanguageStore, AppLanguage } from "@/lib/language-store"; +import { LanguageModal } from "@/components/LanguageModal"; +import { ThemeModal } from "@/components/ThemeModal"; // ── Constants ───────────────────────────────────────────────────── const AVATAR_FALLBACK_BASE = @@ -45,65 +47,12 @@ const THEME_OPTIONS = [ type ThemeOption = (typeof THEME_OPTIONS)[number]["value"]; -function ThemeSheet({ - visible, - current, - onSelect, - onClose, -}: { - visible: boolean; - current: ThemeOption; - onSelect: (v: ThemeOption) => void; - onClose: () => void; -}) { - return ( - - - - +const LANGUAGE_OPTIONS = [ + { value: "en", label: "English" }, + { value: "am", label: "Amharic" }, +] as const; - - {/* Handle */} - - - - Appearance - - - {THEME_OPTIONS.map((opt, i) => { - const selected = current === opt.value; - const isLast = i === THEME_OPTIONS.length - 1; - return ( - { - onSelect(opt.value); - onClose(); - }} - className={`flex-row items-center justify-between py-3.5 px-1 ${!isLast ? "border-b border-border/40" : ""}`} - > - - {opt.label} - - {selected && } - - ); - })} - - - ); -} +type LanguageOption = (typeof LANGUAGE_OPTIONS)[number]["value"]; // ── Shared menu components ──────────────────────────────────────── function MenuGroup({ @@ -167,7 +116,11 @@ function MenuItem({ ) : null} - {right !== undefined ? right : } + {right !== undefined ? ( + right + ) : ( + + )} ); } @@ -177,13 +130,17 @@ export default function ProfileScreen() { const nav = useSirouRouter(); const { user, logout } = useAuthStore(); const { setColorScheme, colorScheme } = useColorScheme(); + const { language, setLanguage } = useLanguageStore(); const [notifications, setNotifications] = useState(true); const [themeSheetVisible, setThemeSheetVisible] = useState(false); + const [languageSheetVisible, setLanguageSheetVisible] = useState(false); const currentTheme: ThemeOption = (colorScheme as ThemeOption) ?? "system"; + const currentLanguage: LanguageOption = (language as LanguageOption) ?? "en"; const handleThemeSelect = (val: AppTheme) => { - setColorScheme(val === "system" ? "system" : val); + // NativeWind 4 handles system/light/dark + setColorScheme(val); saveTheme(val); // persist across restarts }; @@ -195,7 +152,10 @@ export default function ProfileScreen() { onPress={() => nav.back()} className="h-10 w-10 rounded-[10px] bg-card items-center justify-center border border-border" > - + Profile @@ -205,7 +165,10 @@ export default function ProfileScreen() { onPress={() => nav.go("edit-profile")} className="h-10 w-10 rounded-[10px] bg-card items-center justify-center border border-border" > - + @@ -237,26 +200,40 @@ export default function ProfileScreen() { - {/* Account */} - } + {/* + } label="Subscription" sublabel="Pro Plan — active" onPress={() => {}} - /> + /> */} } + icon={ + + } label="Transaction History" - onPress={() => {}} + onPress={() => nav.go("history")} isLast /> {/* Preferences */} - } + {/* + } label="Push Notifications" right={ } - /> + /> */} } + icon={ + + } label="Appearance" sublabel={ THEME_OPTIONS.find((o) => o.value === currentTheme)?.label ?? @@ -276,7 +258,26 @@ export default function ProfileScreen() { onPress={() => setThemeSheetVisible(true)} /> } + icon={ + + } + label="Language" + sublabel={ + LANGUAGE_OPTIONS.find((o) => o.value === currentLanguage)?.label ?? + "English" + } + onPress={() => setLanguageSheetVisible(true)} + /> + + } label="Security" sublabel="PIN & Biometrics" onPress={() => {}} @@ -287,19 +288,34 @@ export default function ProfileScreen() { {/* Support & Legal */} } + icon={ + + } label="Help & Support" - onPress={() => {}} + onPress={() => nav.go("help")} /> } + icon={ + + } label="Privacy Policy" - onPress={() => {}} + onPress={() => nav.go("privacy")} /> } + icon={ + + } label="Terms of Use" - onPress={() => {}} + onPress={() => nav.go("terms")} isLast /> @@ -308,10 +324,18 @@ export default function ProfileScreen() { } + icon={ + + } label="Log Out" destructive - onPress={logout} + onPress={async () => { + await logout(); + nav.go("login"); + }} right={null} isLast /> @@ -320,12 +344,19 @@ export default function ProfileScreen() { {/* Theme sheet */} - handleThemeSelect(theme)} onClose={() => setThemeSheetVisible(false)} /> + + setLanguage(lang)} + onClose={() => setLanguageSheetVisible(false)} + /> ); } diff --git a/app/proforma/[id].tsx b/app/proforma/[id].tsx index 5f769e4..7ca6e15 100644 --- a/app/proforma/[id].tsx +++ b/app/proforma/[id].tsx @@ -3,6 +3,7 @@ import { View, ScrollView, Pressable, ActivityIndicator } from "react-native"; import { useSirouRouter } from "@sirou/react-native"; import { AppRoutes } from "@/lib/routes"; import { Stack, useLocalSearchParams } from "expo-router"; +import { useRouter } from "expo-router"; import { Text } from "@/components/ui/text"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; @@ -20,8 +21,38 @@ import { StandardHeader } from "@/components/StandardHeader"; import { api } from "@/lib/api"; import { toast } from "@/lib/toast-store"; +const dummyData = { + id: "dummy-1", + proformaNumber: "PF-001", + customerName: "John Doe", + customerEmail: "john@example.com", + customerPhone: "+1234567890", + amount: { value: 1000, currency: "USD" }, + currency: "USD", + issueDate: "2026-03-10T11:51:36.134Z", + dueDate: "2026-03-10T11:51:36.134Z", + description: "Dummy proforma", + notes: "Test notes", + taxAmount: { value: 100, currency: "USD" }, + discountAmount: { value: 50, currency: "USD" }, + pdfPath: "dummy.pdf", + userId: "user-1", + items: [ + { + id: "item-1", + description: "Test item", + quantity: 1, + unitPrice: { value: 1000, currency: "USD" }, + total: { value: 1000, currency: "USD" } + } + ], + createdAt: "2026-03-10T11:51:36.134Z", + updatedAt: "2026-03-10T11:51:36.134Z" +}; + export default function ProformaDetailScreen() { const nav = useSirouRouter(); + const router = useRouter(); const { id } = useLocalSearchParams(); const [loading, setLoading] = useState(true); @@ -39,6 +70,7 @@ export default function ProformaDetailScreen() { } catch (error: any) { console.error("[ProformaDetail] Error:", error); toast.error("Error", "Failed to load proforma details"); + setProforma(dummyData); // Use dummy data for testing } finally { setLoading(false); } @@ -86,77 +118,76 @@ export default function ProformaDetailScreen() { contentContainerStyle={{ padding: 16, paddingBottom: 120 }} showsVerticalScrollIndicator={false} > - {/* Blue Summary Card */} - - - - - + {/* Proforma Info Card */} + + + + + - - - ACTIVE - - - - - - Customer: {proforma.customerName} - - - {proforma.description || "Proforma Request"} - - - - - - - Due {new Date(proforma.dueDate).toLocaleDateString()} - - - - - {proforma.proformaNumber} + + Proforma Details + + + + Proforma Number + {proforma.proformaNumber} + + + Issued Date + {new Date(proforma.issueDate).toLocaleDateString()} + + + Due Date + {new Date(proforma.dueDate).toLocaleDateString()} + + + Currency + {proforma.currency} + + {proforma.description && ( + + Description + {proforma.description} + + )} + - {/* Customer Info Strip (Added for functionality while keeping style) */} - - - - - - Email - - - {proforma.customerEmail || "N/A"} - + {/* Customer Info Card */} + + + + + + + Customer Information + - - - - - Phone - - - {proforma.customerPhone || "N/A"} - + + + + Name + {proforma.customerName} + + + Email + {proforma.customerEmail || "N/A"} + + + Phone + {proforma.customerPhone || "N/A"} + + {/* Line Items Card */} @@ -271,24 +302,26 @@ export default function ProformaDetailScreen() { )} {/* Actions */} - + + - + diff --git a/app/proforma/create.tsx b/app/proforma/create.tsx index 6e2638b..f6d7ac3 100644 --- a/app/proforma/create.tsx +++ b/app/proforma/create.tsx @@ -23,13 +23,14 @@ import { ScreenWrapper } from "@/components/ScreenWrapper"; import { useSirouRouter } from "@sirou/react-native"; import { AppRoutes } from "@/lib/routes"; import { Stack } from "expo-router"; -import { useColorScheme } from "nativewind"; +import { colorScheme, useColorScheme } from "nativewind"; import { ShadowWrapper } from "@/components/ShadowWrapper"; import { api } from "@/lib/api"; import { toast } from "@/lib/toast-store"; 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 }; @@ -82,6 +83,7 @@ function Field({ flex?: number; }) { const c = useInputColors(); + const isDark = colorScheme.get() === "dark"; return ( @@ -491,7 +495,7 @@ export default function CreateProformaScreen() { }, ]} placeholder="e.g. Payment due within 30 days" - placeholderTextColor={c.placeholder} + placeholderTextColor={getPlaceholderColor(isDark)} value={notes} onChangeText={setNotes} multiline @@ -500,7 +504,7 @@ export default function CreateProformaScreen() { {/* Footer */} - + + + + {items.map((item) => ( + + updateItem(item.id, "description", v)} + placeholder="Item description" + /> + updateItem(item.id, "qty", v)} + placeholder="0" + numeric + center + /> + updateItem(item.id, "price", v)} + placeholder="0.00" + numeric + center + /> + removeItem(item.id)} + > + + + + ))} + + + + {/* Totals */} + + + + Totals + + + + + Subtotal + + {currency} {calculateSubtotal().toFixed(2)} + + + + + + + + + Total + + {currency} {calculateTotal().toFixed(2)} + + + + + + + {/* Currency */} + + + + Currency + + + setCurrencyModal(true)} + > + {currency} + + + + + + + {/* Bottom Action */} + + + + + {/* Modals */} + setCurrencyModal(false)} + > + {currencies.map((curr) => ( + { + setCurrency(v); + setCurrencyModal(false); + }} + /> + ))} + + + // @ts-ignore + { + setIssueDate(new Date(dateStr)); + setIssueModal(false); + }} + onClose={() => setIssueModal(false)} + /> + + // @ts-ignore + { + setDueDate(new Date(dateStr)); + setDueModal(false); + }} + onClose={() => setDueModal(false)} + /> + + ); +} diff --git a/app/register.tsx b/app/register.tsx index 2dd00d5..2055aec 100644 --- a/app/register.tsx +++ b/app/register.tsx @@ -23,18 +23,24 @@ import { Eye, EyeOff, Chrome, + Globe, } from "@/lib/icons"; import { ScreenWrapper } from "@/components/ScreenWrapper"; import { useAuthStore } from "@/lib/auth-store"; import { api } from "@/lib/api"; import { useColorScheme } from "nativewind"; import { toast } from "@/lib/toast-store"; +import { useLanguageStore, AppLanguage } from "@/lib/language-store"; +import { getPlaceholderColor } from "@/lib/colors"; +import { LanguageModal } from "@/components/LanguageModal"; export default function RegisterScreen() { const nav = useSirouRouter(); const setAuth = useAuthStore((state) => state.setAuth); const { colorScheme } = useColorScheme(); const isDark = colorScheme === "dark"; + const { language, setLanguage } = useLanguageStore(); + const [languageModalVisible, setLanguageModalVisible] = useState(false); const [form, setForm] = useState({ firstName: "", @@ -92,10 +98,19 @@ export default function RegisterScreen() { > - + + setLanguageModalVisible(true)} + className="p-2 rounded-full bg-card border border-border" + > + + + + + First Name - + updateForm("firstName", v)} /> @@ -127,11 +142,11 @@ export default function RegisterScreen() { Last Name - + updateForm("lastName", v)} /> @@ -143,12 +158,12 @@ export default function RegisterScreen() { Email Address - + updateForm("email", v)} autoCapitalize="none" @@ -161,7 +176,7 @@ export default function RegisterScreen() { Phone Number - + @@ -170,7 +185,7 @@ export default function RegisterScreen() { updateForm("phone", v)} keyboardType="phone-pad" @@ -183,12 +198,12 @@ export default function RegisterScreen() { Password - + updateForm("password", v)} secureTextEntry @@ -197,7 +212,7 @@ export default function RegisterScreen() {