Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions app/screens/ChurnPredictionScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet, ScrollView, ActivityIndicator } from 'react-native';
import { colors, spacing } from '../../src/utils/constants';
import { Card } from '../../src/components/common/Card';
import { PredictionService, ChurnPrediction } from '../../backend/services/predictionService';
import { PredictionService, ChurnPrediction, RiskFactor } from '../../backend/services/analytics/predictionService';

const ChurnPredictionScreen = () => {
const [loading, setLoading] = useState(true);
Expand Down Expand Up @@ -78,13 +78,13 @@ const ChurnPredictionScreen = () => {

<View style={styles.section}>
<Text style={styles.sectionTitle}>Key Risk Factors</Text>
{prediction.riskFactors.map((factor, index) => (
{prediction.riskFactors.map((factor: RiskFactor, index: number) => (
<Card key={index} style={styles.factorCard}>
<View style={styles.factorRow}>
<Text style={styles.factorName}>
{factor.factor
.split('_')
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
.map((w: string) => w.charAt(0).toUpperCase() + w.slice(1))
.join(' ')}
</Text>
<Text style={styles.factorImpact}>
Expand Down
104 changes: 104 additions & 0 deletions app/screens/InvoiceAnalyticsScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import React, { useMemo } from 'react';
import { View, Text, StyleSheet, ScrollView, SafeAreaView } from 'react-native';
import { spacing, typography, borderRadius } from '../../src/utils/constants';
import { Card } from '../../src/components/common/Card';
import { useThemeColors } from '../../src/hooks/useThemeColors';
import { useInvoiceStore } from '../../src/store/invoiceStore';
import { InvoiceStatus } from '../../src/types/invoice';

export const InvoiceAnalyticsScreen: React.FC = () => {
const colors = useThemeColors();
const styles = useMemo(() => createStyles(colors), [colors]);
const { invoices } = useInvoiceStore();

const totalInvoices = invoices.length;
const sentInvoices = invoices.filter(i => i.status === InvoiceStatus.SENT).length;
const paidInvoices = invoices.filter(i => i.status === InvoiceStatus.PAID).length;
const draftInvoices = invoices.filter(i => i.status === InvoiceStatus.DRAFT).length;
const voidInvoices = invoices.filter(i => i.status === InvoiceStatus.VOID).length;

const totalRevenue = invoices
.filter(i => i.status === InvoiceStatus.PAID)
.reduce((sum, i) => sum + i.total, 0);

return (
<SafeAreaView style={styles.container}>
<ScrollView style={styles.scrollView}>
<View style={styles.header}>
<Text style={styles.title}>Invoice Analytics</Text>
<Text style={styles.subtitle}>Track delivery and payment performance</Text>
</View>

<View style={styles.summaryContainer}>
<Card style={styles.summaryCard}>
<Text style={styles.summaryLabel}>Total Collected</Text>
<Text style={styles.summaryValue}>${(totalRevenue / 100).toFixed(2)}</Text>
</Card>
<Card style={styles.summaryCard}>
<Text style={styles.summaryLabel}>Total Invoices</Text>
<Text style={styles.summaryValue}>{totalInvoices}</Text>
</Card>
</View>

<Card style={styles.card}>
<Text style={styles.sectionTitle}>Status Breakdown</Text>
<View style={styles.statRow}>
<Text style={styles.statLabel}>Paid</Text>
<Text style={[styles.statValue, { color: colors.status.success }]}>{paidInvoices}</Text>
</View>
<View style={styles.statRow}>
<Text style={styles.statLabel}>Sent (Pending)</Text>
<Text style={[styles.statValue, { color: colors.status.warning }]}>{sentInvoices}</Text>
</View>
<View style={styles.statRow}>
<Text style={styles.statLabel}>Drafts</Text>
<Text style={styles.statValue}>{draftInvoices}</Text>
</View>
<View style={styles.statRow}>
<Text style={styles.statLabel}>Voided</Text>
<Text style={[styles.statValue, { color: colors.status.error }]}>{voidInvoices}</Text>
</View>
</Card>

<Card style={styles.card}>
<Text style={styles.sectionTitle}>Automated Delivery</Text>
<Text style={styles.label}>
{sentInvoices + paidInvoices > 0 ? "100% of sent invoices were delivered automatically." : "No invoices delivered yet."}
</Text>
</Card>

</ScrollView>
</SafeAreaView>
);
};

function createStyles(colors: any) {
return StyleSheet.create({
container: { flex: 1, backgroundColor: colors.background.primary },
scrollView: { flex: 1 },
header: { padding: spacing.lg, paddingBottom: spacing.md },
title: { ...typography.h1, color: colors.text.primary, marginBottom: spacing.xs },
subtitle: { ...typography.body, color: colors.textSecondary },
summaryContainer: {
flexDirection: 'row',
paddingHorizontal: spacing.lg,
marginBottom: spacing.md,
gap: spacing.md,
},
summaryCard: { flex: 1, alignItems: 'center' },
summaryLabel: { ...typography.caption, color: colors.textSecondary, marginBottom: spacing.xs },
summaryValue: { ...typography.h2, color: colors.text.primary },
card: { marginHorizontal: spacing.lg, marginBottom: spacing.md, padding: spacing.md },
sectionTitle: { ...typography.h3, color: colors.text.primary, marginBottom: spacing.md },
statRow: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingVertical: spacing.sm,
borderBottomWidth: 1,
borderBottomColor: colors.border.default,
},
statLabel: { ...typography.body, color: colors.text.primary },
statValue: { ...typography.body, fontWeight: 'bold', color: colors.text.primary },
label: { ...typography.body, color: colors.textSecondary },
});
}
141 changes: 141 additions & 0 deletions app/screens/InvoiceCustomizationScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import React, { useState, useMemo } from 'react';
import { View, Text, StyleSheet, ScrollView, SafeAreaView, TouchableOpacity, TextInput } from 'react-native';
import { spacing, typography, borderRadius } from '../../src/utils/constants';
import { Card } from '../../src/components/common/Card';
import { useThemeColors } from '../../src/hooks/useThemeColors';
import { useInvoiceStore } from '../../src/store/invoiceStore';

export const InvoiceCustomizationScreen: React.FC = () => {
const colors = useThemeColors();
const styles = useMemo(() => createStyles(colors), [colors]);

const { config, templates, setInvoiceBranding, setDefaultTemplate } = useInvoiceStore();
const branding = config.defaultBranding || { logoUrl: '', primaryColor: '#000000', fontFamily: 'Inter' };

const [logoUrl, setLogoUrl] = useState(branding.logoUrl || '');
const [primaryColor, setPrimaryColor] = useState(branding.primaryColor || '#000000');
const [fontFamily, setFontFamily] = useState(branding.fontFamily || 'Inter');
const [selectedTemplate, setSelectedTemplate] = useState(config.defaultTemplateId || templates[0]?.id);

const handleSave = () => {
setInvoiceBranding({ logoUrl, primaryColor, fontFamily });
if (selectedTemplate) setDefaultTemplate(selectedTemplate);
alert('Invoice branding saved!');
};

return (
<SafeAreaView style={styles.container}>
<ScrollView style={styles.scrollView}>
<View style={styles.header}>
<Text style={styles.title}>Invoice Customization</Text>
<Text style={styles.subtitle}>Customize your per-tenant branding</Text>
</View>

<Card style={styles.card}>
<Text style={styles.sectionTitle}>Branding</Text>
<Text style={styles.label}>Logo URL</Text>
<TextInput
style={styles.input}
value={logoUrl}
onChangeText={setLogoUrl}
placeholder="https://example.com/logo.png"
/>

<Text style={styles.label}>Primary Color</Text>
<TextInput
style={styles.input}
value={primaryColor}
onChangeText={setPrimaryColor}
placeholder="#HEXCODE"
/>

<Text style={styles.label}>Font Family</Text>
<TextInput
style={styles.input}
value={fontFamily}
onChangeText={setFontFamily}
placeholder="e.g. Inter, Roboto"
/>
</Card>

<Card style={styles.card}>
<Text style={styles.sectionTitle}>Template Selection</Text>
{templates.map(tpl => (
<TouchableOpacity
key={tpl.id}
style={[
styles.templateItem,
selectedTemplate === tpl.id && { borderColor: colors.primary, backgroundColor: colors.surface }
]}
onPress={() => setSelectedTemplate(tpl.id)}
>
<Text style={styles.templateName}>{tpl.name}</Text>
<Text style={{color: colors.textSecondary}}>{tpl.layout} layout</Text>
</TouchableOpacity>
))}
</Card>

<Card style={styles.card}>
<Text style={styles.sectionTitle}>Preview</Text>
<View style={[styles.previewBox, { borderColor: primaryColor }]}>
<Text style={{ fontFamily, color: primaryColor, fontSize: 24, fontWeight: 'bold' }}>INVOICE</Text>
{logoUrl ? <Text style={{ fontFamily }}>Logo: {logoUrl}</Text> : null}
<Text style={{ fontFamily, marginTop: 10 }}>This is a preview of your custom invoice styling.</Text>
</View>
</Card>

<TouchableOpacity style={styles.actionButton} onPress={handleSave}>
<Text style={styles.actionButtonText}>Save Customization</Text>
</TouchableOpacity>
</ScrollView>
</SafeAreaView>
);
};

function createStyles(colors: any) {
return StyleSheet.create({
container: { flex: 1, backgroundColor: colors.background.primary },
scrollView: { flex: 1 },
header: { padding: spacing.lg, paddingBottom: spacing.md },
title: { ...typography.h1, color: colors.text.primary, marginBottom: spacing.xs },
subtitle: { ...typography.body, color: colors.textSecondary },
card: { marginHorizontal: spacing.lg, marginBottom: spacing.md, padding: spacing.md },
sectionTitle: { ...typography.h3, color: colors.text.primary, marginBottom: spacing.md },
label: { ...typography.caption, color: colors.textSecondary, marginBottom: spacing.xs },
input: {
borderWidth: 1,
borderColor: colors.border.default,
borderRadius: borderRadius.sm,
padding: spacing.sm,
marginBottom: spacing.md,
color: colors.text.primary,
},
templateItem: {
borderWidth: 1,
borderColor: colors.border.default,
borderRadius: borderRadius.md,
padding: spacing.md,
marginBottom: spacing.sm,
},
templateName: { ...typography.body, fontWeight: 'bold', color: colors.text.primary },
previewBox: {
borderWidth: 2,
borderRadius: borderRadius.md,
padding: spacing.lg,
minHeight: 150,
backgroundColor: '#fff',
},
actionButton: {
marginHorizontal: spacing.lg,
marginBottom: spacing.xl,
backgroundColor: colors.primary,
padding: spacing.md,
borderRadius: borderRadius.md,
alignItems: 'center'
},
actionButtonText: {
...typography.button,
color: colors.text.inverse
},
});
}
98 changes: 98 additions & 0 deletions app/screens/InvoiceMarketplaceScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import React, { useMemo } from 'react';
import { View, Text, StyleSheet, ScrollView, SafeAreaView, TouchableOpacity } from 'react-native';
import { spacing, typography, borderRadius } from '../../src/utils/constants';
import { Card } from '../../src/components/common/Card';
import { useThemeColors } from '../../src/hooks/useThemeColors';
import { useInvoiceStore } from '../../src/store/invoiceStore';

const MARKETPLACE_TEMPLATES = [
{ id: 'tpl-1', name: 'Standard Layout', layout: 'standard', price: 'Free' },
{ id: 'tpl-2', name: 'Modern Minimalist', layout: 'modern', price: 'Free' },
{ id: 'tpl-3', name: 'Premium Corporate', layout: 'premium', price: '$4.99' },
{ id: 'tpl-4', name: 'Creative Studio', layout: 'creative', price: '$2.99' },
];

export const InvoiceMarketplaceScreen: React.FC = () => {
const colors = useThemeColors();
const styles = useMemo(() => createStyles(colors), [colors]);
const { templates, addTemplate } = useInvoiceStore();

const handleInstall = (template: any) => {
const isInstalled = templates.some(t => t.id === template.id);
if (isInstalled) {
alert('Template already installed!');
return;
}

// Simulate purchase/installation
addTemplate({ id: template.id, name: template.name, layout: template.layout as any });
alert(`${template.name} has been added to your templates!`);
};

return (
<SafeAreaView style={styles.container}>
<ScrollView style={styles.scrollView}>
<View style={styles.header}>
<Text style={styles.title}>Template Marketplace</Text>
<Text style={styles.subtitle}>Discover and install new invoice layouts</Text>
</View>

{MARKETPLACE_TEMPLATES.map(tpl => {
const isInstalled = templates.some(t => t.id === tpl.id);

return (
<Card key={tpl.id} style={styles.card}>
<View style={styles.row}>
<View style={styles.info}>
<Text style={styles.templateName}>{tpl.name}</Text>
<Text style={styles.templateDesc}>Style: {tpl.layout}</Text>
</View>
<View style={styles.action}>
<Text style={styles.price}>{tpl.price}</Text>
<TouchableOpacity
style={[styles.button, isInstalled && styles.buttonInstalled]}
onPress={() => handleInstall(tpl)}
disabled={isInstalled}
>
<Text style={styles.buttonText}>{isInstalled ? 'Installed' : 'Get'}</Text>
</TouchableOpacity>
</View>
</View>
</Card>
);
})}
</ScrollView>
</SafeAreaView>
);
};

function createStyles(colors: any) {
return StyleSheet.create({
container: { flex: 1, backgroundColor: colors.background.primary },
scrollView: { flex: 1 },
header: { padding: spacing.lg, paddingBottom: spacing.md },
title: { ...typography.h1, color: colors.text.primary, marginBottom: spacing.xs },
subtitle: { ...typography.body, color: colors.textSecondary },
card: { marginHorizontal: spacing.lg, marginBottom: spacing.md, padding: spacing.md },
row: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
info: { flex: 1 },
templateName: { ...typography.h3, color: colors.text.primary, marginBottom: spacing.xs },
templateDesc: { ...typography.caption, color: colors.textSecondary },
action: { alignItems: 'flex-end' },
price: { ...typography.body, fontWeight: 'bold', color: colors.text.primary, marginBottom: spacing.xs },
button: {
backgroundColor: colors.primary,
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
borderRadius: borderRadius.md,
},
buttonInstalled: {
backgroundColor: colors.border.default,
},
buttonText: {
...typography.button,
color: colors.text.inverse,
fontSize: 12
}
});
}
Loading