* Extended functionalities
* Added different UIs
This commit is contained in:
110
meal-plan-frontend-mobile/src/components/ExcelImportPanel.tsx
Normal file
110
meal-plan-frontend-mobile/src/components/ExcelImportPanel.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import { useState } from 'react';
|
||||
import { Alert, StyleSheet, Text, View } from 'react-native';
|
||||
import * as DocumentPicker from 'expo-document-picker';
|
||||
import * as FileSystem from 'expo-file-system/legacy';
|
||||
import * as Sharing from 'expo-sharing';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { downloadImportTemplate, importRecipesFromExcel } from '@/src/api/recipe-management.api';
|
||||
import { Button } from '@/src/components/Button';
|
||||
import { useAppearance } from '@/src/context/AppearanceContext';
|
||||
import { extractApiError } from '@/src/utils/apiError';
|
||||
|
||||
interface ExcelImportPanelProps {
|
||||
onImported?: () => void;
|
||||
}
|
||||
|
||||
export function ExcelImportPanel({ onImported }: ExcelImportPanelProps) {
|
||||
const { t } = useTranslation();
|
||||
const { colors } = useAppearance();
|
||||
const [selectedFile, setSelectedFile] = useState<{ uri: string; name: string } | null>(null);
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
|
||||
const importMutation = useMutation({
|
||||
mutationFn: () => {
|
||||
if (!selectedFile) throw new Error('No file');
|
||||
return importRecipesFromExcel(selectedFile.uri, selectedFile.name);
|
||||
},
|
||||
onSuccess: (result) => {
|
||||
setSelectedFile(null);
|
||||
onImported?.();
|
||||
Alert.alert(
|
||||
t('manage.importComplete'),
|
||||
`${t('manage.createdCount', { count: result.createdCount })} · ${t('manage.skippedCount', { count: result.skippedCount })}`,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
async function handleDownloadTemplate() {
|
||||
setDownloading(true);
|
||||
try {
|
||||
const blob = await downloadImportTemplate();
|
||||
const reader = new FileReader();
|
||||
const base64 = await new Promise<string>((resolve, reject) => {
|
||||
reader.onloadend = () => {
|
||||
const dataUrl = reader.result as string;
|
||||
resolve(dataUrl.split(',')[1] ?? '');
|
||||
};
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
const path = `${FileSystem.cacheDirectory}DailyMeals-recipe-import-template.xlsx`;
|
||||
await FileSystem.writeAsStringAsync(path, base64, { encoding: FileSystem.EncodingType.Base64 });
|
||||
if (await Sharing.isAvailableAsync()) {
|
||||
await Sharing.shareAsync(path);
|
||||
}
|
||||
} catch (err) {
|
||||
Alert.alert(t('errors.generic'), extractApiError(err, t('errors.saveRecipeFailed')));
|
||||
} finally {
|
||||
setDownloading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePickFile() {
|
||||
const result = await DocumentPicker.getDocumentAsync({
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
copyToCacheDirectory: true,
|
||||
});
|
||||
if (!result.canceled && result.assets[0]) {
|
||||
setSelectedFile({ uri: result.assets[0].uri, name: result.assets[0].name });
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={{ color: colors.text, fontWeight: '700', fontSize: 16 }}>{t('manage.importTitle')}</Text>
|
||||
<Text style={{ color: colors.textMuted, fontSize: 13 }}>{t('manage.importDescription')}</Text>
|
||||
<Button
|
||||
title={t('manage.downloadTemplate')}
|
||||
variant="secondary"
|
||||
loading={downloading}
|
||||
onPress={() => void handleDownloadTemplate()}
|
||||
/>
|
||||
<Button title={t('manage.uploadLabel')} variant="secondary" onPress={() => void handlePickFile()} />
|
||||
{selectedFile ? (
|
||||
<Text style={{ color: colors.text, fontSize: 13 }}>{selectedFile.name}</Text>
|
||||
) : null}
|
||||
{importMutation.isError ? (
|
||||
<Text style={{ color: colors.danger }}>{extractApiError(importMutation.error, t('errors.saveRecipeFailed'))}</Text>
|
||||
) : null}
|
||||
<Button
|
||||
title={importMutation.isPending ? t('manage.importing') : t('manage.importRecipes')}
|
||||
loading={importMutation.isPending}
|
||||
disabled={!selectedFile}
|
||||
onPress={() => importMutation.mutate()}
|
||||
/>
|
||||
<View style={[styles.hint, { borderColor: colors.border }]}>
|
||||
<Text style={{ color: colors.text, fontWeight: '600' }}>{t('manage.excelFormatTitle')}</Text>
|
||||
<Text style={{ color: colors.textMuted, fontSize: 13 }}>• {t('manage.excelRecipesSheet')}</Text>
|
||||
<Text style={{ color: colors.textMuted, fontSize: 13 }}>• {t('manage.excelIngredientsSheet')}</Text>
|
||||
<Text style={{ color: colors.textMuted, fontSize: 13 }}>• {t('manage.excelStepsSheet')}</Text>
|
||||
<Text style={{ color: colors.textMuted, fontSize: 13, marginTop: 4 }}>{t('manage.duplicateSkipped')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { gap: 12 },
|
||||
hint: { borderWidth: 1, borderStyle: 'dashed', borderRadius: 12, padding: 12, gap: 4 },
|
||||
});
|
||||
Reference in New Issue
Block a user