diff --git a/public/locales/en/settings.json b/public/locales/en/settings.json
index d03ef6d7..29036c52 100644
--- a/public/locales/en/settings.json
+++ b/public/locales/en/settings.json
@@ -168,7 +168,29 @@
"developer": {
"title": "Developer Settings",
"description": "View technical information about authentication tokens, SSE connections, and debug data."
+ },
+ "importBetidy": {
+ "title": "Import from BeTidy",
+ "description": "Migrate your tasks from the BeTidy app"
}
}
+ },
+ "betidyImport": {
+ "title": "Import from BeTidy",
+ "description": "Upload a data bundle exported from the BeTidy app with the community tool",
+ "fileLabel": "Export bundle (betidy_export.json)",
+ "chooseFile": "Choose file",
+ "taskCount": "{{count}} tasks",
+ "timezone": "Timezone",
+ "includeInactive": "Also import inactive / finished tasks",
+ "import": "Import",
+ "invalidFile": "Invalid file",
+ "invalidFileHint": "Please select a valid betidy_export.json file.",
+ "successTitle": "Import complete",
+ "successMessage": "Imported {{count}} chores.",
+ "errorTitle": "Import failed",
+ "resultTitle": "Import complete",
+ "resultSummary": "Imported {{imported}}, skipped {{skipped}}, labels created {{labels}}.",
+ "errorsSuffix": "task(s) could not be imported (see server logs)."
}
}
diff --git a/src/contexts/RouterContext.jsx b/src/contexts/RouterContext.jsx
index 906a6fb8..235d0fa1 100644
--- a/src/contexts/RouterContext.jsx
+++ b/src/contexts/RouterContext.jsx
@@ -3,6 +3,7 @@ import ChoreEdit from '@/views/ChoreEdit/ChoreEdit'
import Error from '@/views/Error'
import AccountSettings from '@/views/Settings/AccountSettings'
import AdvancedSettings from '@/views/Settings/AdvancedSettings'
+import BeTidyImportSettings from '@/views/Settings/BeTidyImportSettings'
import ChildUserSettings from '@/views/Settings/ChildUserSettings'
import CircleSettings from '@/views/Settings/CircleSettings'
import DeveloperSettings from '@/views/Settings/DeveloperSettings'
@@ -128,6 +129,10 @@ const Router = createBrowserRouter([
path: 'developer',
element: ,
},
+ {
+ path: 'import-betidy',
+ element: ,
+ },
],
},
{
diff --git a/src/utils/Fetcher.jsx b/src/utils/Fetcher.jsx
index 65375e71..9f75d633 100644
--- a/src/utils/Fetcher.jsx
+++ b/src/utils/Fetcher.jsx
@@ -975,8 +975,18 @@ const TrackFilterUsage = id => {
})
}
+// Import a BeTidy export bundle (see github.com/mschabhuettl/betidy-export).
+const ImportBeTidy = bundle => {
+ return Fetch(`/chores/import/betidy`, {
+ method: 'POST',
+ headers: HEADERS(),
+ body: JSON.stringify(bundle),
+ })
+}
+
export {
AcceptCircleMemberRequest,
+ ImportBeTidy,
DeleteChoreAttachment,
DeleteDraftAttachment,
GetChoreAttachments,
diff --git a/src/views/Settings/BeTidyImportSettings.jsx b/src/views/Settings/BeTidyImportSettings.jsx
new file mode 100644
index 00000000..bd1890d9
--- /dev/null
+++ b/src/views/Settings/BeTidyImportSettings.jsx
@@ -0,0 +1,202 @@
+import { CloudUpload } from '@mui/icons-material'
+import {
+ Alert,
+ Box,
+ Button,
+ Card,
+ Checkbox,
+ Chip,
+ Divider,
+ FormControl,
+ FormLabel,
+ Input,
+ Link,
+ Typography,
+} from '@mui/joy'
+import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { useNotification } from '../../service/NotificationProvider'
+import { ImportBeTidy } from '../../utils/Fetcher'
+import SettingsLayout from './SettingsLayout'
+
+const guessTimezone = () => {
+ try {
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'
+ } catch {
+ return 'UTC'
+ }
+}
+
+const BeTidyImportSettings = () => {
+ const { t } = useTranslation('settings')
+ const { showNotification } = useNotification()
+
+ const [bundle, setBundle] = useState(null)
+ const [fileName, setFileName] = useState('')
+ const [taskCount, setTaskCount] = useState(0)
+ const [timezone, setTimezone] = useState(guessTimezone())
+ const [includeInactive, setIncludeInactive] = useState(false)
+ const [loading, setLoading] = useState(false)
+ const [result, setResult] = useState(null)
+
+ const handleFile = event => {
+ const file = event.target.files?.[0]
+ if (!file) return
+ setResult(null)
+ const reader = new FileReader()
+ reader.onload = e => {
+ try {
+ const parsed = JSON.parse(e.target.result)
+ if (!parsed || !Array.isArray(parsed.tasks)) {
+ throw new Error('missing tasks')
+ }
+ setBundle(parsed)
+ setFileName(file.name)
+ setTaskCount(parsed.tasks.length)
+ } catch {
+ setBundle(null)
+ setFileName('')
+ setTaskCount(0)
+ showNotification({
+ type: 'error',
+ title: t('betidyImport.invalidFile'),
+ message: t('betidyImport.invalidFileHint'),
+ })
+ }
+ }
+ reader.readAsText(file)
+ }
+
+ const handleImport = async () => {
+ if (!bundle) return
+ setLoading(true)
+ setResult(null)
+ try {
+ const response = await ImportBeTidy({
+ ...bundle,
+ timezone,
+ includeInactive,
+ })
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}`)
+ }
+ const data = await response.json()
+ const res = data.res || data
+ setResult(res)
+ showNotification({
+ type: 'success',
+ title: t('betidyImport.successTitle'),
+ message: t('betidyImport.successMessage', { count: res.imported }),
+ })
+ } catch (error) {
+ showNotification({
+ type: 'error',
+ title: t('betidyImport.errorTitle'),
+ message: String(error?.message || error),
+ })
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+
+
+ {t('betidyImport.description')}{' '}
+
+ betidy-export
+
+ .
+
+
+
+
+ {t('betidyImport.fileLabel')}
+ }
+ sx={{ alignSelf: 'flex-start' }}
+ >
+ {t('betidyImport.chooseFile')}
+
+
+ {fileName && (
+
+ {fileName}{' '}
+
+ {t('betidyImport.taskCount', { count: taskCount })}
+
+
+ )}
+
+
+
+
+
+ {t('betidyImport.timezone')}
+ setTimezone(e.target.value)}
+ placeholder='UTC'
+ sx={{ maxWidth: 320 }}
+ />
+
+
+ setIncludeInactive(e.target.checked)}
+ />
+
+
+
+
+ {result && (
+
+
+
+ {t('betidyImport.resultTitle')}
+
+
+ {t('betidyImport.resultSummary', {
+ imported: result.imported,
+ skipped: result.skipped,
+ labels: result.labelsCreated,
+ })}
+
+ {Array.isArray(result.errors) && result.errors.length > 0 && (
+
+ {result.errors.length} {t('betidyImport.errorsSuffix')}
+
+ )}
+
+
+ )}
+
+
+ )
+}
+
+export default BeTidyImportSettings
diff --git a/src/views/Settings/SettingsOverview.jsx b/src/views/Settings/SettingsOverview.jsx
index eadb8f9a..9cf8c48e 100644
--- a/src/views/Settings/SettingsOverview.jsx
+++ b/src/views/Settings/SettingsOverview.jsx
@@ -4,6 +4,7 @@ import {
ChevronRight,
Circle,
Code,
+ Download,
FamilyRestroom,
Language,
Notifications,
@@ -122,6 +123,12 @@ const SettingsOverview = () => {
description: t('overview.sections.developer.description'),
icon: ,
},
+ {
+ id: 'import-betidy',
+ title: t('overview.sections.importBetidy.title'),
+ description: t('overview.sections.importBetidy.description'),
+ icon: ,
+ },
]
const handleCardClick = settingId => {