* Extended functionalities

* Added different UIs
This commit is contained in:
2026-06-24 21:43:40 +02:00
parent 282f4864c4
commit f15bb7a916
348 changed files with 59057 additions and 498 deletions

View 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 },
});