diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index d1850e1..a9ac9a1 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -21,6 +21,8 @@ import SettingsScreen from '../screens/SettingsScreen'; import AccountingExportScreen from '../screens/AccountingExportScreen'; import WebhookSettingsScreen from '../screens/WebhookSettingsScreen'; import ErrorDashboardScreen from '../screens/ErrorDashboardScreen'; +import ImportScreen from '../screens/ImportScreen'; +import ExportScreen from '../screens/ExportScreen'; import AdminDashboardScreen from '../screens/AdminDashboardScreen'; import InvoiceListScreen from '../screens/InvoiceListScreen'; import InvoiceDetailScreen from '../screens/InvoiceDetailScreen'; @@ -160,6 +162,16 @@ const SettingsStack = () => ( component={ErrorDashboardScreen} options={{ title: 'Error Dashboard', headerShown: true }} /> + + ); diff --git a/src/navigation/types.ts b/src/navigation/types.ts index 194cc44..a75efed 100644 --- a/src/navigation/types.ts +++ b/src/navigation/types.ts @@ -20,6 +20,8 @@ export type RootStackParamList = { LanguageSettings: undefined; SessionManagement: undefined; ErrorDashboard: undefined; + Import: undefined; + Export: undefined; SegmentManagement: undefined; SegmentDetail: { segmentId: string }; Gamification: undefined; diff --git a/src/screens/ExportScreen.tsx b/src/screens/ExportScreen.tsx new file mode 100644 index 0000000..df35be6 --- /dev/null +++ b/src/screens/ExportScreen.tsx @@ -0,0 +1,511 @@ +import React, { useState, useCallback } from 'react'; +import { + View, + Text, + StyleSheet, + SafeAreaView, + ScrollView, + TouchableOpacity, + Alert, + Share, + ActivityIndicator, + Clipboard, + Platform, +} from 'react-native'; +import { useNavigation } from '@react-navigation/native'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { RootStackParamList } from '../navigation/types'; +import { colors, spacing, typography, borderRadius } from '../utils/constants'; +import { Button } from '../components/common/Button'; +import { Card } from '../components/common/Card'; +import { + generateCSV, + exportToJSON, + ExportData, + Subscription, +} from '../utils/importExport'; +import { useSubscriptionStore } from '../store'; + +type ExportScreenNavigationProp = NativeStackNavigationProp; + +type ExportFormat = 'json' | 'csv'; + +const ExportScreen: React.FC = () => { + const navigation = useNavigation(); + const { subscriptions } = useSubscriptionStore(); + + const [exportFormat, setExportFormat] = useState('json'); + const [isExporting, setIsExporting] = useState(false); + const [exportedData, setExportedData] = useState(null); + const [showPreview, setShowPreview] = useState(false); + + const handleExport = useCallback(async () => { + if (subscriptions.length === 0) { + Alert.alert('No Data', 'There are no subscriptions to export.'); + return; + } + + setIsExporting(true); + + try { + let data: string; + let preview: string; + + if (exportFormat === 'json') { + data = exportToJSON(subscriptions); + preview = JSON.stringify(JSON.parse(data), null, 2); + } else { + data = generateCSV(subscriptions); + preview = data; + } + + setExportedData(data); + setShowPreview(true); + + Alert.alert( + 'Export Ready', + `Exported ${subscriptions.length} subscription(s) as ${exportFormat.toUpperCase()}.`, + [ + { + text: 'Cancel', + style: 'cancel', + }, + { + text: 'Share', + onPress: () => shareData(data), + }, + { + text: 'Copy to Clipboard', + onPress: () => copyToClipboard(data), + }, + ] + ); + } catch (error) { + Alert.alert( + 'Error', + error instanceof Error ? error.message : 'Failed to export data' + ); + } finally { + setIsExporting(false); + } + }, [subscriptions, exportFormat]); + + const shareData = async (data: string) => { + try { + await Share.share({ + message: data, + title: `SubTrackr Export (${exportFormat.toUpperCase()})`, + }); + } catch (error) { + Alert.alert('Error', 'Failed to share data'); + } + }; + + const copyToClipboard = (data: string) => { + Clipboard.setString(data); + Alert.alert('Copied', `${exportFormat.toUpperCase()} data copied to clipboard`); + }; + + const downloadFile = () => { + if (!exportedData) return; + + // In a real implementation, this would use a file system library + // like expo-file-system to save the file + Alert.alert( + 'Download', + 'In a production app, this would save the file to the device storage.', + [ + { text: 'OK' }, + ] + ); + }; + + const renderFormatSelector = () => ( + + Export Format + + setExportFormat('json')} + > + + JSON + + + Full data with metadata + + + setExportFormat('csv')} + > + + CSV + + + Spreadsheet compatible + + + + + ); + + const renderSubscriptionStats = () => ( + + Export Summary + + + {subscriptions.length} + Total Subscriptions + + + + {subscriptions.filter((s) => s.isActive).length} + + Active + + + + {subscriptions.filter((s) => !s.isActive).length} + + Paused + + + + By Category + {getCategoryStats().map((cat) => ( + + {cat.name} + {cat.count} + + ))} + + + ); + + const getCategoryStats = () => { + const categoryMap = new Map(); + subscriptions.forEach((sub) => { + const count = categoryMap.get(sub.category) || 0; + categoryMap.set(sub.category, count + 1); + }); + + return Array.from(categoryMap.entries()) + .map(([name, count]) => ({ name: name.charAt(0).toUpperCase() + name.slice(1), count })) + .sort((a, b) => b.count - a.count); + }; + + const renderPreview = () => { + if (!showPreview || !exportedData) return null; + + const previewText = exportedData.length > 500 + ? exportedData.substring(0, 500) + '...' + : exportedData; + + return ( + + + Preview + setShowPreview(false)}> + Hide + + + + {previewText} + + + ); + }; + + const renderActions = () => ( + +