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 | | Route | Description |
|-------|-------------| |-------|-------------|
| `/(tabs)` | Bottom tabs: Home, Scan, Proforma, Payments, Profile | | `/(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)/scan` | Scan invoice — camera placeholder, recent scans |
| `/(tabs)/proforma` | Proforma list — create, list requests; tap → detail | | `/(tabs)/proforma` | Proforma list — create, list requests; tap → detail |
| `/(tabs)/payments` | Payments — pending match, reconciled list; tap pending → 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 | | `/proforma/[id]` | Proforma request detail — items, send to contacts, submissions |
| `/payments/[id]` | Payment detail — associate to invoice | | `/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` | Notifications list — link to settings |
| `/notifications/settings` | Notification settings — toggles | | `/notifications/settings` | Notification settings — toggles (swagger: GET/PUT /notifications/settings) |
| `/login` | Sign in — email/password, Google | | `/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 { Tabs } from 'expo-router';
import { View } from 'react-native'; import { Home, ScanLine, FileText, Wallet, User } from '@/lib/icons';
const NAV_BG = '#2d2d2d'; const NAV_BG = '#2d2d2d';
const ACTIVE_TINT = '#ea580c'; const ACTIVE_TINT = '#ea580c';
@ -11,11 +11,12 @@ export default function TabsLayout() {
screenOptions={{ screenOptions={{
headerStyle: { backgroundColor: NAV_BG }, headerStyle: { backgroundColor: NAV_BG },
headerTintColor: '#ffffff', headerTintColor: '#ffffff',
headerTitleStyle: { fontWeight: '600' }, headerTitleStyle: { fontWeight: '600', fontSize: 18 },
tabBarStyle: { backgroundColor: NAV_BG }, tabBarStyle: { backgroundColor: NAV_BG, paddingTop: 8 },
tabBarActiveTintColor: ACTIVE_TINT, tabBarActiveTintColor: ACTIVE_TINT,
tabBarInactiveTintColor: INACTIVE_TINT, tabBarInactiveTintColor: INACTIVE_TINT,
tabBarLabelStyle: { fontSize: 12 }, tabBarLabelStyle: { fontSize: 11 },
tabBarShowLabel: true,
}} }}
> >
<Tabs.Screen <Tabs.Screen
@ -23,6 +24,7 @@ export default function TabsLayout() {
options={{ options={{
title: 'Home', title: 'Home',
tabBarLabel: 'Home', tabBarLabel: 'Home',
tabBarIcon: ({ color, size }) => <Home color={color} size={size ?? 22} strokeWidth={2} />,
}} }}
/> />
<Tabs.Screen <Tabs.Screen
@ -30,6 +32,7 @@ export default function TabsLayout() {
options={{ options={{
title: 'Scan Invoice', title: 'Scan Invoice',
tabBarLabel: 'Scan', tabBarLabel: 'Scan',
tabBarIcon: ({ color, size }) => <ScanLine color={color} size={size ?? 22} strokeWidth={2} />,
}} }}
/> />
<Tabs.Screen <Tabs.Screen
@ -37,6 +40,7 @@ export default function TabsLayout() {
options={{ options={{
title: 'Proforma', title: 'Proforma',
tabBarLabel: 'Proforma', tabBarLabel: 'Proforma',
tabBarIcon: ({ color, size }) => <FileText color={color} size={size ?? 22} strokeWidth={2} />,
}} }}
/> />
<Tabs.Screen <Tabs.Screen
@ -44,6 +48,7 @@ export default function TabsLayout() {
options={{ options={{
title: 'Payments', title: 'Payments',
tabBarLabel: 'Payments', tabBarLabel: 'Payments',
tabBarIcon: ({ color, size }) => <Wallet color={color} size={size ?? 22} strokeWidth={2} />,
}} }}
/> />
<Tabs.Screen <Tabs.Screen
@ -51,6 +56,7 @@ export default function TabsLayout() {
options={{ options={{
title: 'Profile', title: 'Profile',
tabBarLabel: 'Profile', tabBarLabel: 'Profile',
tabBarIcon: ({ color, size }) => <User color={color} size={size ?? 22} strokeWidth={2} />,
}} }}
/> />
</Tabs> </Tabs>

View File

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

View File

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

View File

@ -4,54 +4,115 @@ import { Text } from '@/components/ui/text';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { MOCK_USER } from '@/lib/mock-data'; 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() { export default function ProfileScreen() {
return ( 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-6 items-center">
<View className="mb-3 h-20 w-20 items-center justify-center rounded-full bg-primary"> <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> <Text className="text-3xl font-bold text-primary-foreground">{MOCK_USER.name[0]}</Text>
</View> </View>
<Text className="text-xl font-semibold text-gray-900">{MOCK_USER.name}</Text> <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> </View>
<Card className="mb-4"> <Card className="mb-4 overflow-hidden rounded-xl border border-border bg-white">
<CardHeader> <CardHeader className="pb-2">
<CardTitle>Account</CardTitle> <CardTitle className="text-base">Account</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="gap-2"> <CardContent className="gap-0">
<View className="flex-row justify-between py-2"> <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> <Text className="text-muted-foreground">Email</Text>
</View>
<Text className="text-gray-900">{MOCK_USER.email}</Text> <Text className="text-gray-900">{MOCK_USER.email}</Text>
</View> </View>
<View className="flex-row justify-between py-2"> <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> <Text className="text-muted-foreground">Language</Text>
</View>
<Text className="text-gray-900">English</Text> <Text className="text-gray-900">English</Text>
</View> </View>
<Pressable onPress={() => router.push('/notifications')} className="flex-row justify-between py-2"> <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> <Text className="text-muted-foreground">Notifications</Text>
</View>
<View className="flex-row items-center gap-1">
<Text className="text-primary font-medium">Manage</Text> <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> </Pressable>
</CardContent> </CardContent>
</Card> </Card>
<Card className="mb-4"> <Card className="mb-4 overflow-hidden rounded-xl border border-border bg-white">
<CardHeader> <CardHeader className="pb-2">
<CardTitle>About</CardTitle> <View className="flex-row items-center gap-2">
<Info color="#71717a" size={18} strokeWidth={2} />
<CardTitle className="text-base">About</CardTitle>
</View>
</CardHeader> </CardHeader>
<CardContent> <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. Yaltopia Tickets App Scan. Send. Reconcile. Companion to the Yaltopia Tickets web app.
</Text> </Text>
</CardContent> </CardContent>
</Card> </Card>
<Button variant="outline" className="mt-2" onPress={() => router.push('/login')}> <Button
<Text className="font-medium">Sign in (different account)</Text> 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>
<Button variant="destructive" className="mt-2"> <Button variant="destructive" className="mt-3 min-h-12 rounded-xl">
<Text className="font-medium">Log out</Text> <LogOut color="#ffffff" size={20} strokeWidth={2} />
<Text className="ml-2 font-medium">Log out</Text>
</Button> </Button>
</ScrollView> </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 { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from '@/components/ui/card';
import { MOCK_PROFORMA } from '@/lib/mock-data'; import { MOCK_PROFORMA } from '@/lib/mock-data';
import { router } from 'expo-router'; import { router } from 'expo-router';
import { Plus, Send, FileText, ChevronRight, Calendar } from '@/lib/icons';
const PRIMARY = '#ea580c';
export default function ProformaScreen() { export default function ProformaScreen() {
return ( return (
<ScrollView className="flex-1 bg-background" contentContainerStyle={{ padding: 16, paddingBottom: 32 }}> <ScrollView
<Text className="text-muted-foreground mb-4"> 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. Create or select proforma requests and share with contacts via email or SMS.
</Text> </Text>
<Button className="mb-4 bg-primary" onPress={() => {}}> <Button className="mb-5 min-h-12 rounded-xl bg-primary" onPress={() => {}}>
<Text className="text-primary-foreground font-medium">Create new proforma</Text> <Plus color="#ffffff" size={20} strokeWidth={2} />
<Text className="ml-2 text-primary-foreground font-medium">Create new proforma</Text>
</Button> </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) => ( {MOCK_PROFORMA.map((pf) => (
<Pressable key={pf.id} onPress={() => router.push(`/proforma/${pf.id}`)}> <Pressable key={pf.id} onPress={() => router.push(`/proforma/${pf.id}`)}>
<Card className="mb-3"> <Card className="mb-3 overflow-hidden rounded-xl border border-border bg-white">
<CardHeader> <CardHeader className="pb-2">
<CardTitle>{pf.title}</CardTitle> <CardTitle className="text-base">{pf.title}</CardTitle>
<CardDescription>{pf.description}</CardDescription> <CardDescription className="mt-0.5">{pf.description}</CardDescription>
<Text className="text-muted-foreground mt-1 text-xs">Deadline: {pf.deadline} · {pf.itemCount} items</Text> <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> </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> <Text className="text-muted-foreground text-sm">Sent to {pf.sentCount} contacts</Text>
<Button variant="outline" size="sm" onPress={() => router.push(`/proforma/${pf.id}`)}> <View className="flex-row items-center gap-1.5">
<Text className="font-medium">Send to contacts</Text> <Send color={PRIMARY} size={16} strokeWidth={2} />
</Button> <Text className="text-primary font-medium text-sm">Send to contacts</Text>
<ChevronRight color="#a1a1aa" size={18} strokeWidth={2} />
</View>
</CardFooter> </CardFooter>
</Card> </Card>
</Pressable> </Pressable>

View File

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

View File

@ -25,6 +25,11 @@ export default function RootLayout() {
<Stack.Screen name="notifications" options={{ title: 'Notifications' }} /> <Stack.Screen name="notifications" options={{ title: 'Notifications' }} />
<Stack.Screen name="notifications/settings" options={{ title: 'Notification settings' }} /> <Stack.Screen name="notifications/settings" options={{ title: 'Notification settings' }} />
<Stack.Screen name="login" options={{ title: 'Sign in', headerShown: false }} /> <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> </Stack>
<PortalHost /> <PortalHost />
</View> </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 { router } from 'expo-router';
import { Text } from '@/components/ui/text'; import { Text } from '@/components/ui/text';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Mail, ArrowLeft } from '@/lib/icons';
export default function LoginScreen() { export default function LoginScreen() {
return ( return (
<ScrollView className="flex-1 bg-background" contentContainerStyle={{ padding: 16, paddingVertical: 48 }}> <ScrollView
<Text className="mb-6 text-center text-2xl font-bold text-gray-900">Yaltopia Tickets</Text> className="flex-1 bg-[#f5f5f5]"
<Card className="mb-4"> 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> <CardHeader>
<CardTitle>Sign in</CardTitle> <CardTitle className="text-lg">Sign in</CardTitle>
<CardDescription>Use the same account as the web app.</CardDescription> <CardDescription className="mt-1">Use the same account as the web app.</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="gap-3"> <CardContent className="gap-3">
<Button className="bg-primary"> <Button className="min-h-12 rounded-xl bg-primary">
<Text className="text-primary-foreground font-medium">Email & password</Text> <Mail color="#ffffff" size={20} strokeWidth={2} />
<Text className="ml-2 text-primary-foreground font-medium">Email & password</Text>
</Button> </Button>
<Button variant="outline"> <Button variant="outline" className="min-h-12 rounded-xl border-border">
<Text className="font-medium">Continue with Google</Text> <Text className="font-medium text-gray-700">Continue with Google</Text>
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>
<Button variant="ghost" onPress={() => router.back()}> <Pressable onPress={() => router.push('/register')} className="mt-4">
<Text className="font-medium">Back</Text> <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> </Button>
</ScrollView> </ScrollView>
); );

View File

@ -2,6 +2,7 @@ import { View, ScrollView, Pressable } from 'react-native';
import { router } from 'expo-router'; import { router } from 'expo-router';
import { Text } from '@/components/ui/text'; import { Text } from '@/components/ui/text';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
import { Bell, Settings, ChevronRight } from '@/lib/icons';
const MOCK_NOTIFICATIONS = [ const MOCK_NOTIFICATIONS = [
{ id: '1', title: 'Invoice reminder', body: 'Invoice #2 to Robin Murray is due in 2 days.', time: '2h ago', read: false }, { 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 ( return (
<ScrollView className="flex-1 bg-background" contentContainerStyle={{ padding: 16, paddingBottom: 32 }}> <ScrollView className="flex-1 bg-background" contentContainerStyle={{ padding: 16, paddingBottom: 32 }}>
<View className="mb-4 flex-row items-center justify-between"> <View className="mb-4 flex-row items-center justify-between">
<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> <Text className="text-xl font-semibold text-gray-900">Notifications</Text>
<Pressable onPress={() => router.push('/notifications/settings')}> </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> <Text className="text-primary font-medium">Settings</Text>
</Pressable> </Pressable>
</View> </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, paidThisMonth: 4120,
paidCount: 4, 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-linking": "^8.0.11",
"expo-router": "^6.0.23", "expo-router": "^6.0.23",
"expo-status-bar": "~3.0.9", "expo-status-bar": "~3.0.9",
"lucide-react-native": "^0.575.0",
"nativewind": "^4.2.2", "nativewind": "^4.2.2",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
@ -28,6 +29,7 @@
"react-native-reanimated": "^4.2.2", "react-native-reanimated": "^4.2.2",
"react-native-safe-area-context": "^5.6.2", "react-native-safe-area-context": "^5.6.2",
"react-native-screens": "^4.23.0", "react-native-screens": "^4.23.0",
"react-native-svg": "^15.15.3",
"react-native-web": "^0.21.0", "react-native-web": "^0.21.0",
"tailwind-merge": "^3.5.0", "tailwind-merge": "^3.5.0",
"tailwindcss-animate": "^1.0.7" "tailwindcss-animate": "^1.0.7"
@ -4143,6 +4145,12 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/bplist-creator": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz", "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz",
@ -4740,6 +4748,56 @@
"hyphenate-style-name": "^1.0.3" "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": { "node_modules/cssesc": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@ -4870,6 +4928,61 @@
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
"license": "MIT" "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": { "node_modules/dotenv": {
"version": "16.4.7", "version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
@ -4924,6 +5037,18 @@
"node": ">= 0.8" "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": { "node_modules/env-editor": {
"version": "0.4.2", "version": "0.4.2",
"resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz", "resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz",
@ -7344,6 +7469,17 @@
"yallist": "^3.0.2" "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": { "node_modules/makeerror": {
"version": "1.0.12", "version": "1.0.12",
"resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz",
@ -7359,6 +7495,12 @@
"integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==", "integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==",
"license": "Apache-2.0" "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": { "node_modules/memoize-one": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", "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": "^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": { "node_modules/nullthrows": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
@ -9171,6 +9325,21 @@
"react-native": "*" "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": { "node_modules/react-native-web": {
"version": "0.21.2", "version": "0.21.2",
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz", "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-linking": "^8.0.11",
"expo-router": "^6.0.23", "expo-router": "^6.0.23",
"expo-status-bar": "~3.0.9", "expo-status-bar": "~3.0.9",
"lucide-react-native": "^0.575.0",
"nativewind": "^4.2.2", "nativewind": "^4.2.2",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
@ -29,6 +30,7 @@
"react-native-reanimated": "^4.2.2", "react-native-reanimated": "^4.2.2",
"react-native-safe-area-context": "^5.6.2", "react-native-safe-area-context": "^5.6.2",
"react-native-screens": "^4.23.0", "react-native-screens": "^4.23.0",
"react-native-svg": "^15.15.3",
"react-native-web": "^0.21.0", "react-native-web": "^0.21.0",
"tailwind-merge": "^3.5.0", "tailwind-merge": "^3.5.0",
"tailwindcss-animate": "^1.0.7" "tailwindcss-animate": "^1.0.7"