diff --git a/ApiCore-Frontend/src/app/(app)/settings/automation-rules/page.tsx b/ApiCore-Frontend/src/app/(app)/settings/automation/automation-rules-tab.tsx similarity index 92% rename from ApiCore-Frontend/src/app/(app)/settings/automation-rules/page.tsx rename to ApiCore-Frontend/src/app/(app)/settings/automation/automation-rules-tab.tsx index 5b40a5a..3d42426 100644 --- a/ApiCore-Frontend/src/app/(app)/settings/automation-rules/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/settings/automation/automation-rules-tab.tsx @@ -1,7 +1,6 @@ "use client"; import { useState } from "react"; -import { Zap } from "lucide-react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; import { fetchAutomationRules, updateAutomationRule } from "@/lib/automation/api"; @@ -10,7 +9,6 @@ import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { PageHeader } from "@/components/page-header"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; const TRIGGER_LABELS: Record = { @@ -115,21 +113,14 @@ function RuleCard({ rule }: { rule: AutomationRule }) { ); } -export default function AutomationRulesPage() { +export function AutomationRulesTab() { const { data: rules, isLoading, isError } = useQuery({ queryKey: ["automation-rules"], queryFn: fetchAutomationRules, }); return ( -
- - +
{isLoading ?

Lädt…

: null} {isError ? (

@@ -142,6 +133,6 @@ export default function AutomationRulesPage() { ))}

-
+ ); } diff --git a/ApiCore-Frontend/src/app/(app)/settings/task-templates/create-task-template-dialog.tsx b/ApiCore-Frontend/src/app/(app)/settings/automation/create-task-template-dialog.tsx similarity index 100% rename from ApiCore-Frontend/src/app/(app)/settings/task-templates/create-task-template-dialog.tsx rename to ApiCore-Frontend/src/app/(app)/settings/automation/create-task-template-dialog.tsx diff --git a/ApiCore-Frontend/src/app/(app)/settings/webhooks/create-webhook-dialog.tsx b/ApiCore-Frontend/src/app/(app)/settings/automation/create-webhook-dialog.tsx similarity index 100% rename from ApiCore-Frontend/src/app/(app)/settings/webhooks/create-webhook-dialog.tsx rename to ApiCore-Frontend/src/app/(app)/settings/automation/create-webhook-dialog.tsx diff --git a/ApiCore-Frontend/src/app/(app)/settings/task-templates/edit-task-template-dialog.tsx b/ApiCore-Frontend/src/app/(app)/settings/automation/edit-task-template-dialog.tsx similarity index 100% rename from ApiCore-Frontend/src/app/(app)/settings/task-templates/edit-task-template-dialog.tsx rename to ApiCore-Frontend/src/app/(app)/settings/automation/edit-task-template-dialog.tsx diff --git a/ApiCore-Frontend/src/app/(app)/settings/label-templates/page.tsx b/ApiCore-Frontend/src/app/(app)/settings/automation/label-templates-tab.tsx similarity index 83% rename from ApiCore-Frontend/src/app/(app)/settings/label-templates/page.tsx rename to ApiCore-Frontend/src/app/(app)/settings/automation/label-templates-tab.tsx index 3e827a7..d633e66 100644 --- a/ApiCore-Frontend/src/app/(app)/settings/label-templates/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/settings/automation/label-templates-tab.tsx @@ -7,11 +7,10 @@ import { toast } from "sonner"; import { deactivateLabelTemplate, fetchLabelTemplates } from "@/lib/label-templates/api"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { PageHeader } from "@/components/page-header"; import { EmptyState } from "@/components/empty-state"; import { EntityCard } from "@/components/entity-card"; -export default function LabelTemplatesPage() { +export function LabelTemplatesTab() { const queryClient = useQueryClient(); const { data: templates, isLoading, isError } = useQuery({ queryKey: ["label-templates"], @@ -30,19 +29,13 @@ export default function LabelTemplatesPage() { const activeTemplates = (templates ?? []).filter((template) => template.active); return ( -
- }> - - Neue Etikettenvorlage - - } - /> +
+
+ +
{isLoading ?

Lädt…

: null} {isError ? ( @@ -96,6 +89,6 @@ export default function LabelTemplatesPage() { ))}
) : null} -
+ ); } diff --git a/ApiCore-Frontend/src/app/(app)/settings/automation/page.tsx b/ApiCore-Frontend/src/app/(app)/settings/automation/page.tsx new file mode 100644 index 0000000..1f2b17f --- /dev/null +++ b/ApiCore-Frontend/src/app/(app)/settings/automation/page.tsx @@ -0,0 +1,63 @@ +"use client"; + +import { Suspense } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { Zap } from "lucide-react"; +import { PageHeader } from "@/components/page-header"; +import { TabSwitcher } from "@/components/tab-switcher"; +import { WebhooksTab } from "./webhooks-tab"; +import { TaskTemplatesTab } from "./task-templates-tab"; +import { LabelTemplatesTab } from "./label-templates-tab"; +import { SocialMediaTab } from "./social-media-tab"; +import { AutomationRulesTab } from "./automation-rules-tab"; + +type Tab = "webhooks" | "task-templates" | "label-templates" | "social-media" | "rules"; + +const TABS: { value: Tab; label: string }[] = [ + { value: "webhooks", label: "Webhooks" }, + { value: "task-templates", label: "Wiederkehrende Aufgaben" }, + { value: "label-templates", label: "Etikettenvorlagen" }, + { value: "social-media", label: "Social Media" }, + { value: "rules", label: "Regeln" }, +]; + +function isTab(value: string | null): value is Tab { + return TABS.some((tab) => tab.value === value); +} + +function AutomationPageContent() { + const router = useRouter(); + const searchParams = useSearchParams(); + const tab: Tab = isTab(searchParams.get("tab")) ? (searchParams.get("tab") as Tab) : "webhooks"; + + function setTab(next: Tab) { + router.replace(`/settings/automation?tab=${next}`); + } + + return ( +
+ + + + + {tab === "webhooks" ? : null} + {tab === "task-templates" ? : null} + {tab === "label-templates" ? : null} + {tab === "social-media" ? : null} + {tab === "rules" ? : null} +
+ ); +} + +export default function AutomationPage() { + return ( + + + + ); +} diff --git a/ApiCore-Frontend/src/app/(app)/settings/website/page.tsx b/ApiCore-Frontend/src/app/(app)/settings/automation/social-media-tab.tsx similarity index 90% rename from ApiCore-Frontend/src/app/(app)/settings/website/page.tsx rename to ApiCore-Frontend/src/app/(app)/settings/automation/social-media-tab.tsx index 8837602..1a34a8d 100644 --- a/ApiCore-Frontend/src/app/(app)/settings/website/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/settings/automation/social-media-tab.tsx @@ -1,7 +1,6 @@ "use client"; import { FormEvent, useState } from "react"; -import { Globe } from "lucide-react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; import { fetchSiteSettings, updateSiteSettings } from "@/lib/site-settings/api"; @@ -9,7 +8,6 @@ import { SiteSettings } from "@/lib/site-settings/types"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { PageHeader } from "@/components/page-header"; import { Card, CardContent } from "@/components/ui/card"; // Mounted only once `settings` has arrived, so its useState initial values @@ -85,21 +83,14 @@ function WebsiteSettingsForm({ settings }: { settings: SiteSettings }) { ); } -export default function WebsiteSettingsPage() { +export function SocialMediaTab() { const { data: settings, isLoading, isError } = useQuery({ queryKey: ["site-settings"], queryFn: fetchSiteSettings, }); return ( -
- - +
{isLoading ?

Lädt…

: null} {isError ? (

@@ -108,6 +99,6 @@ export default function WebsiteSettingsPage() { ) : null} {settings ? : null} -

+ ); } diff --git a/ApiCore-Frontend/src/app/(app)/settings/task-templates/page.tsx b/ApiCore-Frontend/src/app/(app)/settings/automation/task-templates-tab.tsx similarity index 87% rename from ApiCore-Frontend/src/app/(app)/settings/task-templates/page.tsx rename to ApiCore-Frontend/src/app/(app)/settings/automation/task-templates-tab.tsx index 485ac7e..b5ffd7e 100644 --- a/ApiCore-Frontend/src/app/(app)/settings/task-templates/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/settings/automation/task-templates-tab.tsx @@ -8,7 +8,6 @@ import { deactivateTaskTemplate, fetchTaskTemplates } from "@/lib/task-templates import { TaskTemplate, TaskTemplateRecurrenceType } from "@/lib/task-templates/types"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { PageHeader } from "@/components/page-header"; import { EmptyState } from "@/components/empty-state"; import { EntityCard } from "@/components/entity-card"; import { ActiveHistoryTabs, type EntityLifecycleTab } from "@/components/active-history-tabs"; @@ -35,7 +34,7 @@ function formatDate(value: string): string { return new Date(value).toLocaleDateString("de-DE"); } -export default function TaskTemplatesPage() { +export function TaskTemplatesTab() { const queryClient = useQueryClient(); const [tab, setTab] = useState("active"); const { data: templates, isLoading, isError } = useQuery({ @@ -57,21 +56,16 @@ export default function TaskTemplatesPage() { const visibleTemplates = tab === "active" ? activeTemplates : historyTemplates; return ( -
- } - /> - - +
+
+ + +
{isLoading ?

Lädt…

: null} {isError ? ( @@ -127,6 +121,6 @@ export default function TaskTemplatesPage() { ))}
) : null} -
+ ); } diff --git a/ApiCore-Frontend/src/app/(app)/settings/webhooks/page.tsx b/ApiCore-Frontend/src/app/(app)/settings/automation/webhooks-tab.tsx similarity index 88% rename from ApiCore-Frontend/src/app/(app)/settings/webhooks/page.tsx rename to ApiCore-Frontend/src/app/(app)/settings/automation/webhooks-tab.tsx index 3b3c05c..f97df17 100644 --- a/ApiCore-Frontend/src/app/(app)/settings/webhooks/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/settings/automation/webhooks-tab.tsx @@ -1,12 +1,10 @@ "use client"; -import { Webhook } from "lucide-react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; import { deleteWebhook, fetchWebhooks } from "@/lib/webhooks/api"; import { Badge } from "@/components/ui/badge"; import { ConfirmDialog } from "@/components/confirm-dialog"; -import { PageHeader } from "@/components/page-header"; import { Table, TableBody, @@ -17,7 +15,7 @@ import { } from "@/components/ui/table"; import { CreateWebhookDialog } from "./create-webhook-dialog"; -export default function WebhooksPage() { +export function WebhooksTab() { const queryClient = useQueryClient(); const { data: webhooks, isLoading, isError } = useQuery({ queryKey: ["webhooks"], @@ -34,14 +32,10 @@ export default function WebhooksPage() { }); return ( -
- } - /> +
+
+ +
{isLoading ?

Lädt…

: null} {isError ? ( @@ -101,6 +95,6 @@ export default function WebhooksPage() {
) : null} -
+ ); } diff --git a/ApiCore-Frontend/src/app/(app)/settings/import/page.tsx b/ApiCore-Frontend/src/app/(app)/settings/backup/page.tsx similarity index 60% rename from ApiCore-Frontend/src/app/(app)/settings/import/page.tsx rename to ApiCore-Frontend/src/app/(app)/settings/backup/page.tsx index 6fcd2f9..fc6fd7f 100644 --- a/ApiCore-Frontend/src/app/(app)/settings/import/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/settings/backup/page.tsx @@ -1,15 +1,17 @@ "use client"; import { ChangeEvent, useState } from "react"; -import { Upload } from "lucide-react"; +import { Database } from "lucide-react"; import { useMutation } from "@tanstack/react-query"; import { toast } from "sonner"; +import { exportBackup, importBackup } from "@/lib/backup/api"; import { importCsv } from "@/lib/import/api"; import { ImportEntity, ImportReport, ImportRowStatus } from "@/lib/import/types"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { PageHeader } from "@/components/page-header"; +import { ConfirmDialog } from "@/components/confirm-dialog"; import { Select, SelectContent, @@ -27,6 +29,90 @@ import { TableRow, } from "@/components/ui/table"; +function downloadBlob(blob: Blob, filename: string): void { + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = filename; + link.click(); + URL.revokeObjectURL(url); +} + +function BackupSection() { + const [file, setFile] = useState(null); + + const exportMutation = useMutation({ + mutationFn: exportBackup, + onSuccess: ({ blob, filename }) => { + downloadBlob(blob, filename); + toast.success("Backup heruntergeladen"); + }, + onError: () => toast.error("Backup konnte nicht exportiert werden"), + }); + + const importMutation = useMutation({ + mutationFn: () => { + if (!file) { + throw new Error("Keine Datei ausgewählt"); + } + return importBackup(file); + }, + onSuccess: (summary) => { + const total = Object.values(summary.imported).reduce((sum, count) => sum + count, 0); + toast.success(`Backup eingespielt: ${total} Datensätze verarbeitet`); + setFile(null); + }, + onError: () => toast.error("Backup konnte nicht eingespielt werden"), + }); + + function handleFileChange(event: ChangeEvent) { + setFile(event.target.files?.[0] ?? null); + } + + return ( + + + Backup + + +

+ Sichert Bienenstände bis Etikettenvorlagen (Betriebsdaten) in einer JSON-Datei - + für Backups und zur Übertragung auf ein anderes Deployment. Benutzer, Rollen, + Audit-Log und hochgeladene Dokumente sind nicht enthalten. +

+ + +
+ + + importMutation.mutate()} + /> +
+
+
+ ); +} + const ENTITY_LABELS: Record = { apiaries: "Bienenstände", colonies: "Völker", @@ -53,7 +139,7 @@ const ROW_STATUS_LABELS: Record = { error: "Fehler", }; -export default function ImportPage() { +function CsvImportSection() { const [entity, setEntity] = useState("apiaries"); const [file, setFile] = useState(null); const [report, setReport] = useState(null); @@ -87,19 +173,15 @@ export default function ImportPage() { } return ( -
- - +
- Datei hochladen + CSV-Import +

+ Einzelne Stammdaten (kein Backup) aus einer CSV-Tabelle neu anlegen. +