feat: finalize app with swagger-based pages, Lucide icons, mobile UI

- Add Lucide React Native icon library and use across tabs and screens
- Mobile-like design: rounded cards (xl/2xl), section dividers, icon chips, chevrons
- New pages from swagger: register, invoices/[id], reports, documents, settings
- Invoice detail: amount, bill to, items, Share/PDF actions (GET /invoices/{id})
- Register screen with link to login (POST /auth/register)
- Reports list with mock data and download (GET /reports)
- Documents list with upload CTA (GET /documents)
- Settings: notifications link, language, about
- Profile: links to Notifications, Reports, Documents, Settings
- Home: invoice rows navigate to /invoices/[id]
- Login ↔ Register navigation
- Keep orange (#ea580c) and dark navbar (#2d2d2d) theme throughout
- README: update screens table with new routes

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
“kirukib” 2026-02-22 23:04:04 +03:00
parent b42b3d7602
commit 3b471df8d5
19 changed files with 894 additions and 178 deletions

View File

@ -125,16 +125,21 @@ Use this list to wire the app to the live API. Each item maps to swagger endpoin
| Route | Description |
|-------|-------------|
| `/(tabs)` | Bottom tabs: Home, Scan, Proforma, Payments, Profile |
| `/(tabs)/index` | Home — earnings summary, quick actions, invoice list |
| `/(tabs)/index` | Home — earnings summary, quick actions, invoice list; tap invoice → detail |
| `/(tabs)/scan` | Scan invoice — camera placeholder, recent scans |
| `/(tabs)/proforma` | Proforma list — create, list requests; tap → detail |
| `/(tabs)/payments` | Payments — pending match, reconciled list; tap pending → detail |
| `/(tabs)/profile` | Profile — account, notifications link, login, logout |
| `/(tabs)/profile` | Profile — account, notifications, reports, documents, settings, login, logout |
| `/proforma/[id]` | Proforma request detail — items, send to contacts, submissions |
| `/payments/[id]` | Payment detail — associate to invoice |
| `/invoices/[id]` | Invoice detail — amount, bill to, items, Share, PDF (swagger: GET /invoices/{id}) |
| `/notifications` | Notifications list — link to settings |
| `/notifications/settings` | Notification settings — toggles |
| `/login` | Sign in — email/password, Google |
| `/notifications/settings` | Notification settings — toggles (swagger: GET/PUT /notifications/settings) |
| `/reports` | Reports list — monthly reports, download PDF (swagger: GET /reports) |
| `/documents` | Documents list — upload, view (swagger: GET /documents) |
| `/settings` | App settings — notifications link, language, about |
| `/login` | Sign in — email/password, Google; link to register (swagger: POST /auth/login) |
| `/register` | Create account (swagger: POST /auth/register) |
---

View File

@ -1,5 +1,5 @@
import { Tabs } from 'expo-router';
import { View } from 'react-native';
import { Home, ScanLine, FileText, Wallet, User } from '@/lib/icons';
const NAV_BG = '#2d2d2d';
const ACTIVE_TINT = '#ea580c';
@ -11,11 +11,12 @@ export default function TabsLayout() {
screenOptions={{
headerStyle: { backgroundColor: NAV_BG },
headerTintColor: '#ffffff',
headerTitleStyle: { fontWeight: '600' },
tabBarStyle: { backgroundColor: NAV_BG },
headerTitleStyle: { fontWeight: '600', fontSize: 18 },
tabBarStyle: { backgroundColor: NAV_BG, paddingTop: 8 },
tabBarActiveTintColor: ACTIVE_TINT,
tabBarInactiveTintColor: INACTIVE_TINT,
tabBarLabelStyle: { fontSize: 12 },
tabBarLabelStyle: { fontSize: 11 },
tabBarShowLabel: true,
}}
>
<Tabs.Screen
@ -23,6 +24,7 @@ export default function TabsLayout() {
options={{
title: 'Home',
tabBarLabel: 'Home',
tabBarIcon: ({ color, size }) => <Home color={color} size={size ?? 22} strokeWidth={2} />,
}}
/>
<Tabs.Screen
@ -30,6 +32,7 @@ export default function TabsLayout() {
options={{
title: 'Scan Invoice',
tabBarLabel: 'Scan',
tabBarIcon: ({ color, size }) => <ScanLine color={color} size={size ?? 22} strokeWidth={2} />,
}}
/>
<Tabs.Screen
@ -37,6 +40,7 @@ export default function TabsLayout() {
options={{
title: 'Proforma',
tabBarLabel: 'Proforma',
tabBarIcon: ({ color, size }) => <FileText color={color} size={size ?? 22} strokeWidth={2} />,
}}
/>
<Tabs.Screen
@ -44,6 +48,7 @@ export default function TabsLayout() {
options={{
title: 'Payments',
tabBarLabel: 'Payments',
tabBarIcon: ({ color, size }) => <Wallet color={color} size={size ?? 22} strokeWidth={2} />,
}}
/>
<Tabs.Screen
@ -51,6 +56,7 @@ export default function TabsLayout() {
options={{
title: 'Profile',
tabBarLabel: 'Profile',
tabBarIcon: ({ color, size }) => <User color={color} size={size ?? 22} strokeWidth={2} />,
}}
/>
</Tabs>

View File

@ -1,10 +1,12 @@
import { View, ScrollView, Pressable } from 'react-native';
import { Text } from '@/components/ui/text';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Card, CardContent } from '@/components/ui/card';
import { EARNINGS_SUMMARY, MOCK_INVOICES, MOCK_USER } from '@/lib/mock-data';
import { router } from 'expo-router';
import { Camera, Send, ChevronRight, Wallet, DollarSign, Clock } from '@/lib/icons';
const PRIMARY = '#ea580c';
const statusColor: Record<string, string> = {
Waiting: 'bg-amber-500/20 text-amber-700',
Paid: 'bg-emerald-500/20 text-emerald-700',
@ -14,88 +16,131 @@ const statusColor: Record<string, string> = {
export default function HomeScreen() {
return (
<ScrollView className="flex-1 bg-[#f5f5f5]" contentContainerStyle={{ padding: 16, paddingBottom: 32 }}>
<View className="mb-4">
<ScrollView
className="flex-1 bg-[#f5f5f5]"
contentContainerStyle={{ padding: 20, paddingBottom: 40 }}
showsVerticalScrollIndicator={false}
>
<View className="mb-5">
<Text className="text-2xl font-bold text-gray-900">Hi {MOCK_USER.name},</Text>
<Text className="text-muted-foreground mt-1">Take a look at your last activity.</Text>
<Text className="text-muted-foreground mt-1 text-base">Take a look at your last activity.</Text>
</View>
<Card className="mb-4 overflow-hidden border-0">
<View className="bg-primary/10 p-4">
<Card className="mb-5 overflow-hidden rounded-2xl border-0 shadow-sm">
<View className="bg-primary/10 px-5 py-5">
<Text className="text-muted-foreground text-sm">Earnings balance</Text>
<Text className="text-2xl font-bold text-gray-900">${EARNINGS_SUMMARY.balance.toLocaleString()}</Text>
<Text className="mt-1 text-3xl font-bold text-gray-900">${EARNINGS_SUMMARY.balance.toLocaleString()}</Text>
</View>
<CardContent className="flex-row border-t border-border">
<Pressable className="flex-1 py-3" onPress={() => router.push('/(tabs)/payments')}>
<Text className="text-muted-foreground text-xs">Waiting for pay</Text>
<Text className="font-semibold text-gray-900">${EARNINGS_SUMMARY.waitingAmount.toLocaleString()}</Text>
<Text className="text-muted-foreground text-xs">{EARNINGS_SUMMARY.waitingCount} Waiting invoice</Text>
<View className="flex-row border-t border-border">
<Pressable
className="flex-1 flex-row items-center gap-3 px-5 py-4"
onPress={() => router.push('/(tabs)/payments')}
>
<View className="rounded-xl bg-primary/15 p-2">
<Clock color={PRIMARY} size={20} strokeWidth={2} />
</View>
<View>
<Text className="text-muted-foreground text-xs">Waiting for pay</Text>
<Text className="font-semibold text-gray-900">${EARNINGS_SUMMARY.waitingAmount.toLocaleString()}</Text>
<Text className="text-muted-foreground text-xs">{EARNINGS_SUMMARY.waitingCount} Waiting invoice</Text>
</View>
</Pressable>
<View className="w-px bg-border" />
<Pressable className="flex-1 py-3">
<Text className="text-muted-foreground text-xs">Paid this month</Text>
<Text className="font-semibold text-gray-900">${EARNINGS_SUMMARY.paidThisMonth.toLocaleString()}</Text>
<Text className="text-muted-foreground text-xs">{EARNINGS_SUMMARY.paidCount} Paid invoice</Text>
<Pressable className="flex-1 flex-row items-center gap-3 px-5 py-4">
<View className="rounded-xl bg-emerald-500/15 p-2">
<DollarSign color="#059669" size={20} strokeWidth={2} />
</View>
<View>
<Text className="text-muted-foreground text-xs">Paid this month</Text>
<Text className="font-semibold text-gray-900">${EARNINGS_SUMMARY.paidThisMonth.toLocaleString()}</Text>
<Text className="text-muted-foreground text-xs">{EARNINGS_SUMMARY.paidCount} Paid invoice</Text>
</View>
</Pressable>
</CardContent>
</View>
</Card>
<View className="mb-4 flex-row gap-3">
<Button className="flex-1 bg-primary" onPress={() => router.push('/(tabs)/scan')}>
<Text className="text-primary-foreground font-medium">Scan invoice</Text>
<View className="mb-5 flex-row gap-3">
<Button className="min-h-12 flex-1 rounded-xl bg-primary" onPress={() => router.push('/(tabs)/scan')}>
<Camera color="#ffffff" size={20} strokeWidth={2} />
<Text className="ml-2 text-primary-foreground font-medium">Scan invoice</Text>
</Button>
<Button variant="outline" className="flex-1" onPress={() => router.push('/(tabs)/proforma')}>
<Text className="font-medium">Send proforma</Text>
<Button
variant="outline"
className="min-h-12 flex-1 rounded-xl border-border"
onPress={() => router.push('/(tabs)/proforma')}
>
<Send color={PRIMARY} size={20} strokeWidth={2} />
<Text className="ml-2 font-medium text-gray-700">Send proforma</Text>
</Button>
</View>
<View className="mb-2 flex-row gap-2">
{['All', 'Draft', 'Waiting', 'Paid', 'Unpaid'].map((filter) => (
<View
key={filter}
className={`rounded-full px-3 py-1.5 ${filter === 'Waiting' ? 'bg-primary' : 'bg-muted'}`}
>
<Text className={filter === 'Waiting' ? 'text-primary-foreground text-sm font-medium' : 'text-muted-foreground text-sm'}>
{filter}
</Text>
</View>
))}
</View>
<ScrollView horizontal showsHorizontalScrollIndicator={false} className="-mx-1 mb-4">
<View className="flex-row gap-2 px-1">
{['All', 'Draft', 'Waiting', 'Paid', 'Unpaid'].map((filter) => (
<Pressable
key={filter}
className={`rounded-full px-4 py-2.5 ${filter === 'Waiting' ? 'bg-primary' : 'bg-white'} border border-border`}
>
<Text
className={
filter === 'Waiting' ? 'text-primary-foreground text-sm font-medium' : 'text-muted-foreground text-sm'
}
>
{filter}
</Text>
</Pressable>
))}
</View>
</ScrollView>
<Text className="text-muted-foreground mb-2 text-sm">Today</Text>
<View className="mb-2 flex-row items-center gap-2">
<View className="h-px flex-1 bg-border" />
<Text className="text-muted-foreground text-xs font-medium">Today</Text>
<View className="h-px flex-1 bg-border" />
</View>
{MOCK_INVOICES.filter((i) => i.status === 'Waiting').map((inv) => (
<Card key={inv.id} className="mb-2">
<CardContent className="flex-row items-center justify-between py-3">
<View className="flex-1">
<Text className="font-semibold text-gray-900">{inv.recipient}</Text>
<Text className="text-muted-foreground text-sm">Invoice #{inv.invoiceNumber} - Due {inv.dueDate}</Text>
</View>
<View className="items-end">
<Text className="font-semibold text-gray-900">${inv.amount.toLocaleString()}</Text>
<View className={`mt-1 rounded-full px-2 py-0.5 ${statusColor[inv.status]}`}>
<Text className="text-xs font-medium">{inv.status}</Text>
<Pressable key={inv.id} onPress={() => router.push(`/invoices/${inv.id}`)}>
<Card className="mb-2.5 overflow-hidden rounded-xl border border-border bg-white">
<CardContent className="flex-row items-center justify-between py-4 pl-4 pr-3">
<View className="flex-1">
<Text className="font-semibold text-gray-900">{inv.recipient}</Text>
<Text className="text-muted-foreground mt-0.5 text-sm">Invoice #{inv.invoiceNumber} · Due {inv.dueDate}</Text>
</View>
</View>
</CardContent>
</Card>
<View className="items-end gap-1">
<Text className="font-semibold text-gray-900">${inv.amount.toLocaleString()}</Text>
<View className={`rounded-full px-2.5 py-1 ${statusColor[inv.status]}`}>
<Text className="text-xs font-medium">{inv.status}</Text>
</View>
</View>
<ChevronRight className="ml-2 text-muted-foreground" color="#71717a" size={20} strokeWidth={2} />
</CardContent>
</Card>
</Pressable>
))}
<Text className="text-muted-foreground mb-2 mt-4 text-sm">Yesterday</Text>
<View className="mb-2 mt-6 flex-row items-center gap-2">
<View className="h-px flex-1 bg-border" />
<Text className="text-muted-foreground text-xs font-medium">Yesterday</Text>
<View className="h-px flex-1 bg-border" />
</View>
{MOCK_INVOICES.filter((i) => i.status === 'Paid').map((inv) => (
<Card key={inv.id} className="mb-2">
<CardContent className="flex-row items-center justify-between py-3">
<View className="flex-1">
<Text className="font-semibold text-gray-900">{inv.recipient}</Text>
<Text className="text-muted-foreground text-sm">Invoice #{inv.invoiceNumber} - {inv.dueDate}</Text>
</View>
<View className="items-end">
<Text className="font-semibold text-gray-900">${inv.amount.toLocaleString()}</Text>
<View className={`mt-1 rounded-full px-2 py-0.5 ${statusColor[inv.status]}`}>
<Text className="text-xs font-medium">{inv.status}</Text>
<Pressable key={inv.id} onPress={() => router.push(`/invoices/${inv.id}`)}>
<Card className="mb-2.5 overflow-hidden rounded-xl border border-border bg-white">
<CardContent className="flex-row items-center justify-between py-4 pl-4 pr-3">
<View className="flex-1">
<Text className="font-semibold text-gray-900">{inv.recipient}</Text>
<Text className="text-muted-foreground mt-0.5 text-sm">Invoice #{inv.invoiceNumber} · {inv.dueDate}</Text>
</View>
</View>
</CardContent>
</Card>
<View className="items-end gap-1">
<Text className="font-semibold text-gray-900">${inv.amount.toLocaleString()}</Text>
<View className={`rounded-full px-2.5 py-1 ${statusColor[inv.status]}`}>
<Text className="text-xs font-medium">{inv.status}</Text>
</View>
</View>
<ChevronRight className="ml-2 text-muted-foreground" color="#71717a" size={20} strokeWidth={2} />
</CardContent>
</Card>
</Pressable>
))}
</ScrollView>
);

View File

@ -4,53 +4,70 @@ import { Text } from '@/components/ui/text';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { MOCK_PAYMENTS } from '@/lib/mock-data';
import { ScanLine, Link2, CheckCircle2, Wallet, ChevronRight } from '@/lib/icons';
const PRIMARY = '#ea580c';
export default function PaymentsScreen() {
const matched = MOCK_PAYMENTS.filter((p) => p.matched);
const pending = MOCK_PAYMENTS.filter((p) => !p.matched);
return (
<ScrollView className="flex-1 bg-background" contentContainerStyle={{ padding: 16, paddingBottom: 32 }}>
<Text className="text-muted-foreground mb-4">
<ScrollView
className="flex-1 bg-[#f5f5f5]"
contentContainerStyle={{ padding: 20, paddingBottom: 40 }}
showsVerticalScrollIndicator={false}
>
<Text className="text-muted-foreground mb-5 text-base">
Match payment SMS (e.g. bank or Telebirr) to invoices for quick reconciliation.
</Text>
<Button className="mb-4 bg-primary">
<Text className="text-primary-foreground font-medium">Scan SMS now</Text>
<Button className="mb-5 min-h-12 rounded-xl bg-primary">
<ScanLine color="#ffffff" size={20} strokeWidth={2} />
<Text className="ml-2 text-primary-foreground font-medium">Scan SMS now</Text>
</Button>
<Text className="text-muted-foreground mb-2 text-sm">Pending match</Text>
<View className="mb-3 flex-row items-center gap-2">
<Link2 color="#71717a" size={18} strokeWidth={2} />
<Text className="text-muted-foreground text-sm font-medium">Pending match</Text>
</View>
{pending.map((pay) => (
<Card key={pay.id} className="mb-2 border-amber-500/30">
<CardContent className="py-3">
<View className="flex-row items-center justify-between">
<View>
<Text className="font-semibold text-gray-900">${pay.amount.toLocaleString()}</Text>
<Text className="text-muted-foreground text-sm">{pay.source} · {pay.date}</Text>
</View>
<Button variant="outline" size="sm" onPress={() => router.push(`/payments/${pay.id}`)}>
<Text className="font-medium">Match to invoice</Text>
</Button>
<Card key={pay.id} className="mb-2.5 overflow-hidden rounded-xl border-2 border-amber-500/30 bg-white">
<CardContent className="flex-row items-center py-4 pl-4 pr-3">
<View className="mr-3 rounded-xl bg-primary/10 p-2">
<Wallet color={PRIMARY} size={22} strokeWidth={2} />
</View>
<View className="flex-1">
<Text className="font-semibold text-gray-900">${pay.amount.toLocaleString()}</Text>
<Text className="text-muted-foreground text-sm">{pay.source} · {pay.date}</Text>
</View>
<Button variant="outline" size="sm" className="rounded-lg" onPress={() => router.push(`/payments/${pay.id}`)}>
<Text className="font-medium">Match</Text>
</Button>
</CardContent>
</Card>
))}
<Text className="text-muted-foreground mb-2 mt-4 text-sm">Reconciled</Text>
<View className="mb-3 mt-6 flex-row items-center gap-2">
<CheckCircle2 color="#059669" size={18} strokeWidth={2} />
<Text className="text-muted-foreground text-sm font-medium">Reconciled</Text>
</View>
{matched.map((pay) => (
<Card key={pay.id} className="mb-2">
<CardContent className="py-3">
<View className="flex-row items-center justify-between">
<View>
<Text className="font-semibold text-gray-900">${pay.amount.toLocaleString()}</Text>
<Text className="text-muted-foreground text-sm">
{pay.source} · {pay.date} {pay.reference && `· ${pay.reference}`}
</Text>
</View>
<View className="rounded-full bg-emerald-500/20 px-2 py-0.5">
<Text className="text-xs font-medium text-emerald-700">Matched</Text>
</View>
<Card key={pay.id} className="mb-2.5 overflow-hidden rounded-xl border border-border bg-white">
<CardContent className="flex-row items-center py-4 pl-4 pr-3">
<View className="mr-3 rounded-xl bg-emerald-500/15 p-2">
<CheckCircle2 color="#059669" size={22} strokeWidth={2} />
</View>
<View className="flex-1">
<Text className="font-semibold text-gray-900">${pay.amount.toLocaleString()}</Text>
<Text className="text-muted-foreground text-sm">
{pay.source} · {pay.date} {pay.reference && `· ${pay.reference}`}
</Text>
</View>
<View className="rounded-full bg-emerald-500/20 px-2.5 py-1">
<Text className="text-xs font-medium text-emerald-700">Matched</Text>
</View>
<ChevronRight className="ml-2 text-muted-foreground" color="#71717a" size={20} strokeWidth={2} />
</CardContent>
</Card>
))}

View File

@ -4,54 +4,115 @@ import { Text } from '@/components/ui/text';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { MOCK_USER } from '@/lib/mock-data';
import { User, Mail, Globe, Bell, ChevronRight, Info, LogOut, LogIn, FileText, FolderOpen, Settings } from '@/lib/icons';
const PRIMARY = '#ea580c';
export default function ProfileScreen() {
return (
<ScrollView className="flex-1 bg-background" contentContainerStyle={{ padding: 16, paddingBottom: 32 }}>
<ScrollView
className="flex-1 bg-[#f5f5f5]"
contentContainerStyle={{ padding: 20, paddingBottom: 40 }}
showsVerticalScrollIndicator={false}
>
<View className="mb-6 items-center">
<View className="mb-3 h-20 w-20 items-center justify-center rounded-full bg-primary">
<Text className="text-3xl font-bold text-primary-foreground">{MOCK_USER.name[0]}</Text>
</View>
<Text className="text-xl font-semibold text-gray-900">{MOCK_USER.name}</Text>
<Text className="text-muted-foreground text-sm">{MOCK_USER.email}</Text>
<Text className="text-muted-foreground mt-1 text-sm">{MOCK_USER.email}</Text>
</View>
<Card className="mb-4">
<CardHeader>
<CardTitle>Account</CardTitle>
<Card className="mb-4 overflow-hidden rounded-xl border border-border bg-white">
<CardHeader className="pb-2">
<CardTitle className="text-base">Account</CardTitle>
</CardHeader>
<CardContent className="gap-2">
<View className="flex-row justify-between py-2">
<Text className="text-muted-foreground">Email</Text>
<CardContent className="gap-0">
<View className="flex-row items-center justify-between border-b border-border py-3">
<View className="flex-row items-center gap-3">
<Mail color="#71717a" size={20} strokeWidth={2} />
<Text className="text-muted-foreground">Email</Text>
</View>
<Text className="text-gray-900">{MOCK_USER.email}</Text>
</View>
<View className="flex-row justify-between py-2">
<Text className="text-muted-foreground">Language</Text>
<View className="flex-row items-center justify-between border-b border-border py-3">
<View className="flex-row items-center gap-3">
<Globe color="#71717a" size={20} strokeWidth={2} />
<Text className="text-muted-foreground">Language</Text>
</View>
<Text className="text-gray-900">English</Text>
</View>
<Pressable onPress={() => router.push('/notifications')} className="flex-row justify-between py-2">
<Text className="text-muted-foreground">Notifications</Text>
<Text className="text-primary font-medium">Manage</Text>
<Pressable
onPress={() => router.push('/notifications')}
className="flex-row items-center justify-between border-b border-border py-3"
>
<View className="flex-row items-center gap-3">
<Bell color="#71717a" size={20} strokeWidth={2} />
<Text className="text-muted-foreground">Notifications</Text>
</View>
<View className="flex-row items-center gap-1">
<Text className="text-primary font-medium">Manage</Text>
<ChevronRight color={PRIMARY} size={18} strokeWidth={2} />
</View>
</Pressable>
<Pressable
onPress={() => router.push('/reports')}
className="flex-row items-center justify-between border-b border-border py-3"
>
<View className="flex-row items-center gap-3">
<FileText color="#71717a" size={20} strokeWidth={2} />
<Text className="text-muted-foreground">Reports</Text>
</View>
<ChevronRight color="#a1a1aa" size={18} strokeWidth={2} />
</Pressable>
<Pressable
onPress={() => router.push('/documents')}
className="flex-row items-center justify-between border-b border-border py-3"
>
<View className="flex-row items-center gap-3">
<FolderOpen color="#71717a" size={20} strokeWidth={2} />
<Text className="text-muted-foreground">Documents</Text>
</View>
<ChevronRight color="#a1a1aa" size={18} strokeWidth={2} />
</Pressable>
<Pressable
onPress={() => router.push('/settings')}
className="flex-row items-center justify-between py-3"
>
<View className="flex-row items-center gap-3">
<Settings color="#71717a" size={20} strokeWidth={2} />
<Text className="text-muted-foreground">Settings</Text>
</View>
<ChevronRight color="#a1a1aa" size={18} strokeWidth={2} />
</Pressable>
</CardContent>
</Card>
<Card className="mb-4">
<CardHeader>
<CardTitle>About</CardTitle>
<Card className="mb-4 overflow-hidden rounded-xl border border-border bg-white">
<CardHeader className="pb-2">
<View className="flex-row items-center gap-2">
<Info color="#71717a" size={18} strokeWidth={2} />
<CardTitle className="text-base">About</CardTitle>
</View>
</CardHeader>
<CardContent>
<Text className="text-muted-foreground text-sm">
<Text className="text-muted-foreground text-sm leading-5">
Yaltopia Tickets App Scan. Send. Reconcile. Companion to the Yaltopia Tickets web app.
</Text>
</CardContent>
</Card>
<Button variant="outline" className="mt-2" onPress={() => router.push('/login')}>
<Text className="font-medium">Sign in (different account)</Text>
<Button
variant="outline"
className="mt-2 min-h-12 rounded-xl border-border"
onPress={() => router.push('/login')}
>
<LogIn color={PRIMARY} size={20} strokeWidth={2} />
<Text className="ml-2 font-medium text-gray-700">Sign in (different account)</Text>
</Button>
<Button variant="destructive" className="mt-2">
<Text className="font-medium">Log out</Text>
<Button variant="destructive" className="mt-3 min-h-12 rounded-xl">
<LogOut color="#ffffff" size={20} strokeWidth={2} />
<Text className="ml-2 font-medium">Log out</Text>
</Button>
</ScrollView>
);

View File

@ -4,32 +4,48 @@ import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from '@/components/ui/card';
import { MOCK_PROFORMA } from '@/lib/mock-data';
import { router } from 'expo-router';
import { Plus, Send, FileText, ChevronRight, Calendar } from '@/lib/icons';
const PRIMARY = '#ea580c';
export default function ProformaScreen() {
return (
<ScrollView className="flex-1 bg-background" contentContainerStyle={{ padding: 16, paddingBottom: 32 }}>
<Text className="text-muted-foreground mb-4">
<ScrollView
className="flex-1 bg-[#f5f5f5]"
contentContainerStyle={{ padding: 20, paddingBottom: 40 }}
showsVerticalScrollIndicator={false}
>
<Text className="text-muted-foreground mb-5 text-base">
Create or select proforma requests and share with contacts via email or SMS.
</Text>
<Button className="mb-4 bg-primary" onPress={() => {}}>
<Text className="text-primary-foreground font-medium">Create new proforma</Text>
<Button className="mb-5 min-h-12 rounded-xl bg-primary" onPress={() => {}}>
<Plus color="#ffffff" size={20} strokeWidth={2} />
<Text className="ml-2 text-primary-foreground font-medium">Create new proforma</Text>
</Button>
<Text className="text-muted-foreground mb-2 text-sm">Your proforma requests</Text>
<View className="mb-3 flex-row items-center gap-2">
<FileText color="#71717a" size={18} strokeWidth={2} />
<Text className="text-muted-foreground text-sm font-medium">Your proforma requests</Text>
</View>
{MOCK_PROFORMA.map((pf) => (
<Pressable key={pf.id} onPress={() => router.push(`/proforma/${pf.id}`)}>
<Card className="mb-3">
<CardHeader>
<CardTitle>{pf.title}</CardTitle>
<CardDescription>{pf.description}</CardDescription>
<Text className="text-muted-foreground mt-1 text-xs">Deadline: {pf.deadline} · {pf.itemCount} items</Text>
<Card className="mb-3 overflow-hidden rounded-xl border border-border bg-white">
<CardHeader className="pb-2">
<CardTitle className="text-base">{pf.title}</CardTitle>
<CardDescription className="mt-0.5">{pf.description}</CardDescription>
<View className="mt-2 flex-row items-center gap-1.5">
<Calendar color="#71717a" size={14} strokeWidth={2} />
<Text className="text-muted-foreground text-xs">Deadline {pf.deadline} · {pf.itemCount} items</Text>
</View>
</CardHeader>
<CardFooter className="flex-row justify-between">
<CardFooter className="flex-row items-center justify-between border-t border-border pt-3">
<Text className="text-muted-foreground text-sm">Sent to {pf.sentCount} contacts</Text>
<Button variant="outline" size="sm" onPress={() => router.push(`/proforma/${pf.id}`)}>
<Text className="font-medium">Send to contacts</Text>
</Button>
<View className="flex-row items-center gap-1.5">
<Send color={PRIMARY} size={16} strokeWidth={2} />
<Text className="text-primary font-medium text-sm">Send to contacts</Text>
<ChevronRight color="#a1a1aa" size={18} strokeWidth={2} />
</View>
</CardFooter>
</Card>
</Pressable>

View File

@ -1,55 +1,64 @@
import { View, ScrollView } from 'react-native';
import { Text } from '@/components/ui/text';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Card, CardContent } from '@/components/ui/card';
import { Camera, FileText, ChevronRight } from '@/lib/icons';
const PRIMARY = '#ea580c';
export default function ScanScreen() {
return (
<ScrollView className="flex-1 bg-background" contentContainerStyle={{ padding: 16, paddingBottom: 32 }}>
<Text className="text-muted-foreground mb-4">
<ScrollView
className="flex-1 bg-[#f5f5f5]"
contentContainerStyle={{ padding: 20, paddingBottom: 40 }}
showsVerticalScrollIndicator={false}
>
<Text className="text-muted-foreground mb-5 text-base">
Capture paper or digital invoices with your camera. We'll extract vendor, amount, date, and line items.
</Text>
<Card className="mb-4 border-2 border-dashed border-border">
<CardContent className="items-center justify-center py-16">
<View className="mb-4 h-20 w-20 items-center justify-center rounded-full bg-primary/10">
<Text className="text-4xl">📷</Text>
<Card className="mb-5 overflow-hidden rounded-2xl border-2 border-dashed border-border bg-white">
<CardContent className="items-center justify-center py-14">
<View className="mb-5 h-24 w-24 items-center justify-center rounded-full bg-primary/10">
<Camera color={PRIMARY} size={40} strokeWidth={2} />
</View>
<Text className="mb-2 text-center text-lg font-semibold text-gray-900">Scan invoice</Text>
<Text className="text-muted-foreground mb-4 text-center text-sm">
<Text className="text-muted-foreground mb-6 text-center text-sm">
Tap below to open camera and capture an invoice
</Text>
<Button className="bg-primary min-w-[200]">
<Text className="text-primary-foreground font-medium">Open camera</Text>
<Button className="min-h-12 rounded-xl bg-primary px-8">
<Camera color="#ffffff" size={20} strokeWidth={2} />
<Text className="ml-2 text-primary-foreground font-medium">Open camera</Text>
</Button>
</CardContent>
</Card>
<Text className="text-muted-foreground mb-2 text-sm">Recent scans</Text>
<Card className="mb-2">
<CardContent className="py-3">
<View className="flex-row items-center justify-between">
<View>
<Text className="font-medium text-gray-900">Acme Corp - Invoice #101</Text>
<Text className="text-muted-foreground text-sm">Scanned Sep 12, 2022 · $1,240</Text>
</View>
<View className="rounded-full bg-amber-500/20 px-2 py-0.5">
<Text className="text-xs font-medium text-amber-700">Pending</Text>
</View>
<View className="mb-3 flex-row items-center gap-2">
<FileText color="#71717a" size={18} strokeWidth={2} />
<Text className="text-muted-foreground text-sm font-medium">Recent scans</Text>
</View>
<Card className="mb-2.5 overflow-hidden rounded-xl border border-border bg-white">
<CardContent className="flex-row items-center py-4 pl-4 pr-3">
<View className="flex-1">
<Text className="font-medium text-gray-900">Acme Corp - Invoice #101</Text>
<Text className="text-muted-foreground mt-0.5 text-sm">Scanned Sep 12, 2022 · $1,240</Text>
</View>
<View className="rounded-full bg-amber-500/20 px-2.5 py-1">
<Text className="text-xs font-medium text-amber-700">Pending</Text>
</View>
<ChevronRight className="ml-2 text-muted-foreground" color="#71717a" size={20} strokeWidth={2} />
</CardContent>
</Card>
<Card className="mb-2">
<CardContent className="py-3">
<View className="flex-row items-center justify-between">
<View>
<Text className="font-medium text-gray-900">Tech Supplies Ltd - Invoice #88</Text>
<Text className="text-muted-foreground text-sm">Scanned Sep 11, 2022 · $890</Text>
</View>
<View className="rounded-full bg-emerald-500/20 px-2 py-0.5">
<Text className="text-xs font-medium text-emerald-700">Saved</Text>
</View>
<Card className="mb-2.5 overflow-hidden rounded-xl border border-border bg-white">
<CardContent className="flex-row items-center py-4 pl-4 pr-3">
<View className="flex-1">
<Text className="font-medium text-gray-900">Tech Supplies Ltd - Invoice #88</Text>
<Text className="text-muted-foreground mt-0.5 text-sm">Scanned Sep 11, 2022 · $890</Text>
</View>
<View className="rounded-full bg-emerald-500/20 px-2.5 py-1">
<Text className="text-xs font-medium text-emerald-700">Saved</Text>
</View>
<ChevronRight className="ml-2 text-muted-foreground" color="#71717a" size={20} strokeWidth={2} />
</CardContent>
</Card>
</ScrollView>

View File

@ -25,6 +25,11 @@ export default function RootLayout() {
<Stack.Screen name="notifications" options={{ title: 'Notifications' }} />
<Stack.Screen name="notifications/settings" options={{ title: 'Notification settings' }} />
<Stack.Screen name="login" options={{ title: 'Sign in', headerShown: false }} />
<Stack.Screen name="register" options={{ title: 'Create account', headerShown: false }} />
<Stack.Screen name="invoices/[id]" options={{ title: 'Invoice' }} />
<Stack.Screen name="reports" options={{ title: 'Reports' }} />
<Stack.Screen name="documents" options={{ title: 'Documents' }} />
<Stack.Screen name="settings" options={{ title: 'Settings' }} />
</Stack>
<PortalHost />
</View>

53
app/documents/index.tsx Normal file
View File

@ -0,0 +1,53 @@
import { View, ScrollView, Pressable } from 'react-native';
import { router } from 'expo-router';
import { Text } from '@/components/ui/text';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { FileText, ChevronRight, FolderOpen, Upload } from '@/lib/icons';
import { MOCK_DOCUMENTS } from '@/lib/mock-data';
const PRIMARY = '#ea580c';
export default function DocumentsScreen() {
return (
<ScrollView
className="flex-1 bg-[#f5f5f5]"
contentContainerStyle={{ padding: 20, paddingBottom: 40 }}
showsVerticalScrollIndicator={false}
>
<View className="mb-4 flex-row items-center gap-2">
<FolderOpen color="#18181b" size={22} strokeWidth={2} />
<Text className="text-xl font-semibold text-gray-900">Documents</Text>
</View>
<Text className="text-muted-foreground mb-5 text-sm">
Uploaded invoices, scans, and attachments. Synced with your account.
</Text>
<Button variant="outline" className="mb-5 min-h-12 rounded-xl border-border" onPress={() => {}}>
<Upload color={PRIMARY} size={20} strokeWidth={2} />
<Text className="ml-2 font-medium text-gray-700">Upload document</Text>
</Button>
{MOCK_DOCUMENTS.map((d) => (
<Card key={d.id} className="mb-2.5 overflow-hidden rounded-xl border border-border bg-white">
<Pressable>
<CardContent className="flex-row items-center py-4 pl-4 pr-3">
<View className="mr-3 rounded-xl bg-primary/10 p-2">
<FileText color={PRIMARY} size={22} strokeWidth={2} />
</View>
<View className="flex-1">
<Text className="font-medium text-gray-900" numberOfLines={1}>{d.name}</Text>
<Text className="text-muted-foreground mt-0.5 text-sm">{d.size} · {d.uploadedAt}</Text>
</View>
<ChevronRight color="#71717a" size={20} strokeWidth={2} />
</CardContent>
</Pressable>
</Card>
))}
<Button variant="outline" className="mt-4 rounded-xl border-border" onPress={() => router.back()}>
<Text className="font-medium">Back</Text>
</Button>
</ScrollView>
);
}

100
app/invoices/[id].tsx Normal file
View File

@ -0,0 +1,100 @@
import { View, ScrollView } from 'react-native';
import { useLocalSearchParams, router } from 'expo-router';
import { Text } from '@/components/ui/text';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { FileText, Calendar, User, Share2, Download, ChevronRight } from '@/lib/icons';
import { MOCK_INVOICES } from '@/lib/mock-data';
const PRIMARY = '#ea580c';
const MOCK_ITEMS = [
{ description: 'Marketing Landing Page Package', qty: 1, unitPrice: 1000, total: 1000 },
{ description: 'Instagram Post Initial Design', qty: 4, unitPrice: 100, total: 400 },
];
export default function InvoiceDetailScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
const invoice = MOCK_INVOICES.find((i) => i.id === id);
return (
<ScrollView
className="flex-1 bg-[#f5f5f5]"
contentContainerStyle={{ padding: 20, paddingBottom: 40 }}
showsVerticalScrollIndicator={false}
>
<Card className="mb-4 overflow-hidden rounded-xl border border-border bg-white">
<CardContent className="p-5">
<View className="flex-row items-center justify-between">
<View className="flex-row items-center gap-2">
<FileText color={PRIMARY} size={22} strokeWidth={2} />
<Text className="font-semibold text-gray-900">Invoice #{invoice?.invoiceNumber ?? id}</Text>
</View>
<View className="rounded-full bg-amber-500/20 px-2.5 py-1">
<Text className="text-xs font-medium text-amber-700">{invoice?.status ?? 'Waiting'}</Text>
</View>
</View>
<Text className="text-muted-foreground mt-2 text-sm">Amount due</Text>
<Text className="mt-1 text-2xl font-bold text-gray-900">${invoice?.amount.toLocaleString() ?? '—'}</Text>
<View className="mt-3 flex-row gap-4">
<View className="flex-row items-center gap-1.5">
<Calendar color="#71717a" size={16} strokeWidth={2} />
<Text className="text-muted-foreground text-sm">Due {invoice?.dueDate ?? '—'}</Text>
</View>
<View className="flex-row items-center gap-1.5">
<Calendar color="#71717a" size={16} strokeWidth={2} />
<Text className="text-muted-foreground text-sm">Issued {invoice?.createdAt ?? '—'}</Text>
</View>
</View>
</CardContent>
</Card>
<Card className="mb-4 overflow-hidden rounded-xl border border-border bg-white">
<CardHeader className="pb-2">
<View className="flex-row items-center gap-2">
<User color="#71717a" size={18} strokeWidth={2} />
<CardTitle className="text-base">Bill to</CardTitle>
</View>
</CardHeader>
<CardContent>
<Text className="font-medium text-gray-900">{invoice?.recipient ?? '—'}</Text>
<Text className="text-muted-foreground text-sm">{invoice?.recipientEmail ?? '—'}</Text>
</CardContent>
</Card>
<Card className="mb-4 overflow-hidden rounded-xl border border-border bg-white">
<CardHeader className="pb-2">
<CardTitle className="text-base">Items</CardTitle>
</CardHeader>
<CardContent className="gap-2">
{MOCK_ITEMS.map((item, i) => (
<View key={i} className="flex-row justify-between border-b border-border py-2 last:border-0">
<Text className="text-gray-700">{item.description} × {item.qty}</Text>
<Text className="font-medium text-gray-900">${item.total.toLocaleString()}</Text>
</View>
))}
<View className="mt-2 border-t border-border pt-3">
<View className="flex-row justify-between">
<Text className="font-semibold text-gray-900">Total</Text>
<Text className="font-semibold text-gray-900">${invoice?.amount.toLocaleString() ?? '1,540'}</Text>
</View>
</View>
</CardContent>
</Card>
<View className="flex-row gap-3">
<Button variant="outline" className="min-h-12 flex-1 rounded-xl border-border" onPress={() => {}}>
<Share2 color={PRIMARY} size={20} strokeWidth={2} />
<Text className="ml-2 font-medium text-gray-700">Share</Text>
</Button>
<Button variant="outline" className="min-h-12 flex-1 rounded-xl border-border" onPress={() => {}}>
<Download color={PRIMARY} size={20} strokeWidth={2} />
<Text className="ml-2 font-medium text-gray-700">PDF</Text>
</Button>
</View>
<Button variant="ghost" className="mt-4 rounded-xl" onPress={() => router.back()}>
<ChevronRight className="rotate-180" color="#71717a" size={20} strokeWidth={2} />
<Text className="ml-2 font-medium text-muted-foreground">Back</Text>
</Button>
</ScrollView>
);
}

View File

@ -1,29 +1,39 @@
import { View, ScrollView } from 'react-native';
import { View, ScrollView, Pressable } from 'react-native';
import { router } from 'expo-router';
import { Text } from '@/components/ui/text';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Mail, ArrowLeft } from '@/lib/icons';
export default function LoginScreen() {
return (
<ScrollView className="flex-1 bg-background" contentContainerStyle={{ padding: 16, paddingVertical: 48 }}>
<Text className="mb-6 text-center text-2xl font-bold text-gray-900">Yaltopia Tickets</Text>
<Card className="mb-4">
<ScrollView
className="flex-1 bg-[#f5f5f5]"
contentContainerStyle={{ padding: 24, paddingVertical: 48 }}
showsVerticalScrollIndicator={false}
>
<Text className="mb-8 text-center text-2xl font-bold text-gray-900">Yaltopia Tickets</Text>
<Card className="mb-5 overflow-hidden rounded-2xl border border-border bg-white">
<CardHeader>
<CardTitle>Sign in</CardTitle>
<CardDescription>Use the same account as the web app.</CardDescription>
<CardTitle className="text-lg">Sign in</CardTitle>
<CardDescription className="mt-1">Use the same account as the web app.</CardDescription>
</CardHeader>
<CardContent className="gap-3">
<Button className="bg-primary">
<Text className="text-primary-foreground font-medium">Email & password</Text>
<Button className="min-h-12 rounded-xl bg-primary">
<Mail color="#ffffff" size={20} strokeWidth={2} />
<Text className="ml-2 text-primary-foreground font-medium">Email & password</Text>
</Button>
<Button variant="outline">
<Text className="font-medium">Continue with Google</Text>
<Button variant="outline" className="min-h-12 rounded-xl border-border">
<Text className="font-medium text-gray-700">Continue with Google</Text>
</Button>
</CardContent>
</Card>
<Button variant="ghost" onPress={() => router.back()}>
<Text className="font-medium">Back</Text>
<Pressable onPress={() => router.push('/register')} className="mt-4">
<Text className="text-center text-primary font-medium">Create account</Text>
</Pressable>
<Button variant="ghost" className="mt-4 rounded-xl" onPress={() => router.back()}>
<ArrowLeft color="#71717a" size={20} strokeWidth={2} />
<Text className="ml-2 font-medium text-muted-foreground">Back</Text>
</Button>
</ScrollView>
);

View File

@ -2,6 +2,7 @@ import { View, ScrollView, Pressable } from 'react-native';
import { router } from 'expo-router';
import { Text } from '@/components/ui/text';
import { Card, CardContent } from '@/components/ui/card';
import { Bell, Settings, ChevronRight } from '@/lib/icons';
const MOCK_NOTIFICATIONS = [
{ id: '1', title: 'Invoice reminder', body: 'Invoice #2 to Robin Murray is due in 2 days.', time: '2h ago', read: false },
@ -13,8 +14,12 @@ export default function NotificationsScreen() {
return (
<ScrollView className="flex-1 bg-background" contentContainerStyle={{ padding: 16, paddingBottom: 32 }}>
<View className="mb-4 flex-row items-center justify-between">
<Text className="text-xl font-semibold text-gray-900">Notifications</Text>
<Pressable onPress={() => router.push('/notifications/settings')}>
<View className="flex-row items-center gap-2">
<Bell color="#18181b" size={22} strokeWidth={2} />
<Text className="text-xl font-semibold text-gray-900">Notifications</Text>
</View>
<Pressable className="flex-row items-center gap-1" onPress={() => router.push('/notifications/settings')}>
<Settings color="#ea580c" size={18} strokeWidth={2} />
<Text className="text-primary font-medium">Settings</Text>
</Pressable>
</View>

40
app/register.tsx Normal file
View File

@ -0,0 +1,40 @@
import { View, ScrollView, Pressable } from 'react-native';
import { router } from 'expo-router';
import { Text } from '@/components/ui/text';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Mail, ArrowLeft, UserPlus } from '@/lib/icons';
export default function RegisterScreen() {
return (
<ScrollView
className="flex-1 bg-[#f5f5f5]"
contentContainerStyle={{ padding: 24, paddingVertical: 48 }}
showsVerticalScrollIndicator={false}
>
<Text className="mb-8 text-center text-2xl font-bold text-gray-900">Yaltopia Tickets</Text>
<Card className="mb-5 overflow-hidden rounded-2xl border border-border bg-white">
<CardHeader>
<CardTitle className="text-lg">Create account</CardTitle>
<CardDescription className="mt-1">Register with the same account format as the web app.</CardDescription>
</CardHeader>
<CardContent className="gap-3">
<Button className="min-h-12 rounded-xl bg-primary">
<UserPlus color="#ffffff" size={20} strokeWidth={2} />
<Text className="ml-2 text-primary-foreground font-medium">Email & password</Text>
</Button>
<Button variant="outline" className="min-h-12 rounded-xl border-border">
<Text className="font-medium text-gray-700">Continue with Google</Text>
</Button>
</CardContent>
</Card>
<Pressable onPress={() => router.push('/login')} className="mt-2">
<Text className="text-center text-primary font-medium">Already have an account? Sign in</Text>
</Pressable>
<Button variant="ghost" className="mt-4 rounded-xl" onPress={() => router.back()}>
<ArrowLeft color="#71717a" size={20} strokeWidth={2} />
<Text className="ml-2 font-medium text-muted-foreground">Back</Text>
</Button>
</ScrollView>
);
}

54
app/reports/index.tsx Normal file
View File

@ -0,0 +1,54 @@
import { View, ScrollView, Pressable } from 'react-native';
import { router } from 'expo-router';
import { Text } from '@/components/ui/text';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { FileText, Download, ChevronRight, BarChart3 } from '@/lib/icons';
import { MOCK_REPORTS } from '@/lib/mock-data';
const PRIMARY = '#ea580c';
export default function ReportsScreen() {
return (
<ScrollView
className="flex-1 bg-[#f5f5f5]"
contentContainerStyle={{ padding: 20, paddingBottom: 40 }}
showsVerticalScrollIndicator={false}
>
<View className="mb-4 flex-row items-center gap-2">
<BarChart3 color="#18181b" size={22} strokeWidth={2} />
<Text className="text-xl font-semibold text-gray-900">Reports</Text>
</View>
<Text className="text-muted-foreground mb-5 text-sm">
Monthly reports and PDF exports. Generate from the web app or view here.
</Text>
{MOCK_REPORTS.map((r) => (
<Card key={r.id} className="mb-2.5 overflow-hidden rounded-xl border border-border bg-white">
<Pressable>
<CardContent className="flex-row items-center py-4 pl-4 pr-3">
<View className="mr-3 rounded-xl bg-primary/10 p-2">
<FileText color={PRIMARY} size={22} strokeWidth={2} />
</View>
<View className="flex-1">
<Text className="font-semibold text-gray-900">{r.title}</Text>
<Text className="text-muted-foreground mt-0.5 text-sm">{r.period}</Text>
<Text className="text-muted-foreground mt-0.5 text-xs">Generated {r.generatedAt}</Text>
</View>
<View className="flex-row items-center gap-2">
<Pressable className="rounded-lg bg-primary/10 p-2">
<Download color={PRIMARY} size={18} strokeWidth={2} />
</Pressable>
<ChevronRight color="#71717a" size={20} strokeWidth={2} />
</View>
</CardContent>
</Pressable>
</Card>
))}
<Button variant="outline" className="mt-4 rounded-xl border-border" onPress={() => router.back()}>
<Text className="font-medium">Back</Text>
</Button>
</ScrollView>
);
}

72
app/settings.tsx Normal file
View File

@ -0,0 +1,72 @@
import { View, ScrollView, Pressable } from 'react-native';
import { router } from 'expo-router';
import { Text } from '@/components/ui/text';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Settings, Bell, Globe, ChevronRight, Info } from '@/lib/icons';
const PRIMARY = '#ea580c';
export default function SettingsScreen() {
return (
<ScrollView
className="flex-1 bg-[#f5f5f5]"
contentContainerStyle={{ padding: 20, paddingBottom: 40 }}
showsVerticalScrollIndicator={false}
>
<View className="mb-5 flex-row items-center gap-2">
<Settings color="#18181b" size={22} strokeWidth={2} />
<Text className="text-xl font-semibold text-gray-900">Settings</Text>
</View>
<Card className="mb-4 overflow-hidden rounded-xl border border-border bg-white">
<CardHeader className="pb-2">
<CardTitle className="text-base">Preferences</CardTitle>
</CardHeader>
<CardContent className="gap-0">
<Pressable
className="flex-row items-center justify-between border-b border-border py-3"
onPress={() => router.push('/notifications/settings')}
>
<View className="flex-row items-center gap-3">
<Bell color="#71717a" size={20} strokeWidth={2} />
<Text className="text-muted-foreground">Notifications</Text>
</View>
<ChevronRight color="#a1a1aa" size={18} strokeWidth={2} />
</Pressable>
<View className="flex-row items-center justify-between py-3">
<View className="flex-row items-center gap-3">
<Globe color="#71717a" size={20} strokeWidth={2} />
<Text className="text-muted-foreground">Language</Text>
</View>
<Text className="text-gray-900">English</Text>
</View>
</CardContent>
</Card>
<Card className="mb-4 overflow-hidden rounded-xl border border-border bg-white">
<CardHeader className="pb-2">
<View className="flex-row items-center gap-2">
<Info color="#71717a" size={18} strokeWidth={2} />
<CardTitle className="text-base">About</CardTitle>
</View>
</CardHeader>
<CardContent>
<Text className="text-muted-foreground text-sm leading-5">
Yaltopia Tickets App v1.0 Scan. Send. Reconcile.
</Text>
</CardContent>
</Card>
<View className="rounded-xl border border-border bg-white p-4">
<Text className="text-muted-foreground text-xs">
API: Invoices, Proforma, Payments, Reports, Documents, Notifications see swagger.json and README for integration.
</Text>
</View>
<Button variant="outline" className="mt-6 rounded-xl border-border" onPress={() => router.back()}>
<Text className="font-medium">Back</Text>
</Button>
</ScrollView>
);
}

37
lib/icons.tsx Normal file
View File

@ -0,0 +1,37 @@
/**
* Re-export Lucide icons for consistent use. Use these with color="#ea580c" for primary, "#ffffff" on dark bar.
*/
export {
Home,
ScanLine,
FileText,
Wallet,
User,
Camera,
Send,
ChevronRight,
Bell,
Settings,
LogOut,
LogIn,
Plus,
Link2,
CheckCircle2,
Clock,
Copy,
Calendar,
Menu,
ArrowLeft,
MoreVertical,
AlertCircle,
DollarSign,
Mail,
Globe,
Info,
FolderOpen,
Share2,
Download,
BarChart3,
Upload,
UserPlus,
} from 'lucide-react-native';

View File

@ -92,3 +92,13 @@ export const EARNINGS_SUMMARY = {
paidThisMonth: 4120,
paidCount: 4,
};
export const MOCK_REPORTS = [
{ id: 'r1', title: 'September 2022', period: 'Sep 1 Sep 30, 2022', generatedAt: 'Oct 1, 2022', downloadUrl: '#' },
{ id: 'r2', title: 'August 2022', period: 'Aug 1 Aug 31, 2022', generatedAt: 'Sep 1, 2022', downloadUrl: '#' },
];
export const MOCK_DOCUMENTS = [
{ id: 'd1', name: 'Invoice #2 - Robin Murray.pdf', size: '124 KB', uploadedAt: 'Sep 8, 2022' },
{ id: 'd2', name: 'Scan - Acme Corp.pdf', size: '89 KB', uploadedAt: 'Sep 12, 2022' },
];

169
package-lock.json generated
View File

@ -20,6 +20,7 @@
"expo-linking": "^8.0.11",
"expo-router": "^6.0.23",
"expo-status-bar": "~3.0.9",
"lucide-react-native": "^0.575.0",
"nativewind": "^4.2.2",
"react": "19.1.0",
"react-dom": "19.1.0",
@ -28,6 +29,7 @@
"react-native-reanimated": "^4.2.2",
"react-native-safe-area-context": "^5.6.2",
"react-native-screens": "^4.23.0",
"react-native-svg": "^15.15.3",
"react-native-web": "^0.21.0",
"tailwind-merge": "^3.5.0",
"tailwindcss-animate": "^1.0.7"
@ -4143,6 +4145,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
"license": "ISC"
},
"node_modules/bplist-creator": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz",
@ -4740,6 +4748,56 @@
"hyphenate-style-name": "^1.0.3"
}
},
"node_modules/css-select": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
"integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0",
"css-what": "^6.1.0",
"domhandler": "^5.0.2",
"domutils": "^3.0.1",
"nth-check": "^2.0.1"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/css-tree": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
"integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
"license": "MIT",
"dependencies": {
"mdn-data": "2.0.14",
"source-map": "^0.6.1"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/css-tree/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/css-what": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
"integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
"license": "BSD-2-Clause",
"engines": {
"node": ">= 6"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@ -4870,6 +4928,61 @@
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
"license": "MIT"
},
"node_modules/dom-serializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.2",
"entities": "^4.2.0"
},
"funding": {
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
}
},
"node_modules/domelementtype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "BSD-2-Clause"
},
"node_modules/domhandler": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
"license": "BSD-2-Clause",
"dependencies": {
"domelementtype": "^2.3.0"
},
"engines": {
"node": ">= 4"
},
"funding": {
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/domutils": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
"license": "BSD-2-Clause",
"dependencies": {
"dom-serializer": "^2.0.0",
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3"
},
"funding": {
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/dotenv": {
"version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
@ -4924,6 +5037,18 @@
"node": ">= 0.8"
}
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/env-editor": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz",
@ -7344,6 +7469,17 @@
"yallist": "^3.0.2"
}
},
"node_modules/lucide-react-native": {
"version": "0.575.0",
"resolved": "https://registry.npmjs.org/lucide-react-native/-/lucide-react-native-0.575.0.tgz",
"integrity": "sha512-kdGcjF4Rm1YKuNs3IaW5lDAqVKn9RBj1Fmjt3JBr08PMIXpVV7iL0ICNF/awiPZQicHlx/v9xgyZZS4TAFxDNg==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-native": "*",
"react-native-svg": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0"
}
},
"node_modules/makeerror": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz",
@ -7359,6 +7495,12 @@
"integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==",
"license": "Apache-2.0"
},
"node_modules/mdn-data": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
"license": "CC0-1.0"
},
"node_modules/memoize-one": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
@ -7977,6 +8119,18 @@
"node": "^16.14.0 || >=18.0.0"
}
},
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0"
},
"funding": {
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
"node_modules/nullthrows": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
@ -9171,6 +9325,21 @@
"react-native": "*"
}
},
"node_modules/react-native-svg": {
"version": "15.15.3",
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.15.3.tgz",
"integrity": "sha512-/k4KYwPBLGcx2f5d4FjE+vCScK7QOX14cl2lIASJ28u4slHHtIhL0SZKU7u9qmRBHxTCKPoPBtN6haT1NENJNA==",
"license": "MIT",
"dependencies": {
"css-select": "^5.1.0",
"css-tree": "^1.1.3",
"warn-once": "0.1.1"
},
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native-web": {
"version": "0.21.2",
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz",

View File

@ -21,6 +21,7 @@
"expo-linking": "^8.0.11",
"expo-router": "^6.0.23",
"expo-status-bar": "~3.0.9",
"lucide-react-native": "^0.575.0",
"nativewind": "^4.2.2",
"react": "19.1.0",
"react-dom": "19.1.0",
@ -29,6 +30,7 @@
"react-native-reanimated": "^4.2.2",
"react-native-safe-area-context": "^5.6.2",
"react-native-screens": "^4.23.0",
"react-native-svg": "^15.15.3",
"react-native-web": "^0.21.0",
"tailwind-merge": "^3.5.0",
"tailwindcss-animate": "^1.0.7"