feat: Complete AI Chat Agent, Admin Settings (MCP/AI), and Translations
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -4,7 +4,7 @@ import { Task } from '@shared/schema';
|
||||
import TaskCard from './TaskCard';
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Trophy, Target, Calendar as CalendarIcon, ArrowRight, GripVertical } from 'lucide-react';
|
||||
import { Trophy, Target, Calendar as CalendarIcon, ArrowRight, GripVertical, Sun, Moon } from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
DndContext,
|
||||
@@ -190,13 +190,35 @@ export default function FocusMode({
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="grid grid-rows-2 gap-4 h-full"
|
||||
>
|
||||
<Card className="p-6 h-full flex flex-col justify-center items-center text-center bg-card hover:shadow-md transition-all cursor-pointer" onClick={onNavigateToTasks}>
|
||||
<Target className="w-12 h-12 text-muted-foreground/20 mb-4" />
|
||||
<h3 className="font-semibold text-lg">{t('focus.viewAll.title')}</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">{t('focus.viewAll.description')}</p>
|
||||
<Button variant="outline" className="gap-2 group">
|
||||
{t('focus.viewAll.button')} <ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
|
||||
{/* Routine Actions */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-full flex flex-col items-center justify-center gap-2 hover:bg-orange-50 hover:text-orange-600 dark:hover:bg-orange-950/30 border-dashed"
|
||||
onClick={() => window.location.href = '/focus/routine/morning'}
|
||||
>
|
||||
<Sun className="w-5 h-5 text-orange-500" />
|
||||
<span className="text-xs font-semibold">{t('focus.routine.morning', 'Morning Plan')}</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-full flex flex-col items-center justify-center gap-2 hover:bg-indigo-50 hover:text-indigo-600 dark:hover:bg-indigo-950/30 border-dashed"
|
||||
onClick={() => window.location.href = '/focus/routine/evening'}
|
||||
>
|
||||
<Moon className="w-5 h-5 text-indigo-500" />
|
||||
<span className="text-xs font-semibold">{t('focus.routine.evening', 'Evening Review')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card className="p-4 flex flex-col justify-center items-center text-center bg-card hover:shadow-md transition-all cursor-pointer" onClick={onNavigateToTasks}>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Target className="w-5 h-5 text-muted-foreground/50" />
|
||||
<h3 className="font-semibold text-sm">{t('focus.viewAll.title')}</h3>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" className="gap-1 text-xs text-muted-foreground group">
|
||||
{t('focus.viewAll.button')} <ArrowRight className="w-3 h-3 group-hover:translate-x-1 transition-transform" />
|
||||
</Button>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
@@ -31,6 +31,10 @@ import { triggerConfetti } from "@/lib/confetti";
|
||||
import { playSuccessSound } from "@/lib/sounds";
|
||||
import { simulateAIDecomposition } from "@/lib/ai-simulator";
|
||||
import { Wand2, Loader2 } from "lucide-react";
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
|
||||
|
||||
|
||||
interface TaskCardProps {
|
||||
task: Task;
|
||||
@@ -120,6 +124,7 @@ function SharedMenuItem({ task, onShare }: { task: Task, onShare: () => void })
|
||||
|
||||
export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDelete, onStatusChange, onUpdate, isDragging }: TaskCardProps) {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const [isAnalyzing, setIsAnalyzing] = useState(false);
|
||||
const [isShareModalOpen, setIsShareModalOpen] = useState(false);
|
||||
|
||||
@@ -201,6 +206,56 @@ export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDe
|
||||
return `${hours}h ${mins}m`;
|
||||
};
|
||||
|
||||
/* AI Follow-up Check */
|
||||
const checkFollowUp = async () => {
|
||||
// Fire and forget - don't block UI
|
||||
try {
|
||||
const res = await apiRequest("POST", "/api/ai/analyze-completion", { taskId: task.id });
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
if (data.needed && data.title) {
|
||||
toast({
|
||||
title: t('ai.followUpSuggestion', 'Follow-up Suggested'),
|
||||
description: `${data.title} - ${data.description || ''}`,
|
||||
action: (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation(); // prevent toast click from doing generic things
|
||||
// Create the task immediately
|
||||
try {
|
||||
await apiRequest("POST", "/api/tasks", {
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
status: "todo",
|
||||
priority: "medium",
|
||||
labelId: task.labelId, // Inherit label
|
||||
dueDate: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString() // Tomorrow
|
||||
});
|
||||
toast({ title: t('ai.taskCreated', 'Follow-up task created!') });
|
||||
// Invalidate queries to show new task
|
||||
// queryClient.invalidateQueries... (Need queryClient access)
|
||||
// Since we don't have queryClient here easily without import useQueryClient
|
||||
// We can just reload or rely on auto-refetch
|
||||
window.location.reload(); // Crude but effective for now, or useQueryClient
|
||||
} catch (err) {
|
||||
toast({ title: t('ai.errorCreating', 'Failed to create task'), variant: "destructive" });
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('common.create', 'Create')}
|
||||
</Button>
|
||||
),
|
||||
duration: 8000,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Follow-up check failed", e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleComplete = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (isBlocked) return;
|
||||
@@ -210,6 +265,8 @@ export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDe
|
||||
triggerConfetti(e.clientX / window.innerWidth, e.clientY / window.innerHeight);
|
||||
playSuccessSound();
|
||||
onStatusChange?.('done');
|
||||
// Trigger AI check
|
||||
checkFollowUp();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -38,6 +38,12 @@ export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreat
|
||||
|
||||
const [isCalendarOpen, setIsCalendarOpen] = useState(false);
|
||||
const [isStartDateOpen, setIsStartDateOpen] = useState(false);
|
||||
|
||||
const [isRecurring, setIsRecurring] = useState(false);
|
||||
const [recurrenceInterval, setRecurrenceInterval] = useState<string | undefined>();
|
||||
const [recurrenceIntervalValue, setRecurrenceIntervalValue] = useState(1);
|
||||
const [recurrenceEnd, setRecurrenceEnd] = useState<Date | undefined>();
|
||||
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Fetch labels
|
||||
@@ -62,6 +68,11 @@ export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreat
|
||||
energyLevel,
|
||||
estimatedDuration,
|
||||
|
||||
isRecurring,
|
||||
recurrenceInterval,
|
||||
recurrenceIntervalValue,
|
||||
recurrenceEnd,
|
||||
|
||||
startDate,
|
||||
dueDate,
|
||||
labelId,
|
||||
@@ -80,6 +91,10 @@ export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreat
|
||||
setPriority('medium');
|
||||
setDueDate(undefined);
|
||||
setStartDate(undefined);
|
||||
setIsRecurring(false);
|
||||
setRecurrenceInterval(undefined);
|
||||
setRecurrenceIntervalValue(1);
|
||||
setRecurrenceEnd(undefined);
|
||||
setLabelId(undefined);
|
||||
setDependencies([]);
|
||||
setError(null);
|
||||
@@ -99,6 +114,10 @@ export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreat
|
||||
setPriority('medium');
|
||||
setDueDate(undefined);
|
||||
setStartDate(undefined);
|
||||
setIsRecurring(false);
|
||||
setRecurrenceInterval(undefined);
|
||||
setRecurrenceIntervalValue(1);
|
||||
setRecurrenceEnd(undefined);
|
||||
setLabelId(undefined);
|
||||
setDependencies([]);
|
||||
setError(null);
|
||||
@@ -283,6 +302,42 @@ export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreat
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recurrence Selection */}
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-sm font-medium flex-none">{t('taskCreation.recurrence.label')}</label>
|
||||
<Select value={recurrenceInterval || 'none'} onValueChange={(v) => {
|
||||
setRecurrenceInterval(v === 'none' ? undefined : v);
|
||||
setIsRecurring(v !== 'none');
|
||||
}}>
|
||||
<SelectTrigger className="h-8">
|
||||
<SelectValue placeholder={t('taskCreation.recurrence.none')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">{t('taskCreation.recurrence.none')}</SelectItem>
|
||||
<SelectItem value="daily">{t('taskCreation.recurrence.daily')}</SelectItem>
|
||||
<SelectItem value="weekly">{t('taskCreation.recurrence.weekly')}</SelectItem>
|
||||
<SelectItem value="monthly">{t('taskCreation.recurrence.monthly')}</SelectItem>
|
||||
<SelectItem value="yearly">{t('taskCreation.recurrence.yearly')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{isRecurring && (
|
||||
<div className="flex items-center gap-2 animate-in fade-in slide-in-from-left-2">
|
||||
<span className="text-sm">{t('taskCreation.recurrence.every')}</span>
|
||||
<Input
|
||||
type="number"
|
||||
className="w-16 h-8"
|
||||
min={1}
|
||||
value={recurrenceIntervalValue}
|
||||
onChange={(e) => setRecurrenceIntervalValue(parseInt(e.target.value) || 1)}
|
||||
/>
|
||||
<span className="text-sm">
|
||||
{recurrenceInterval && t(`taskCreation.recurrence.units.${recurrenceInterval}`)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Dependencies Selector */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium flex items-center gap-2">
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select";
|
||||
import { Loader2, Bot, RefreshCw, Download, Server } from "lucide-react";
|
||||
@@ -25,6 +26,7 @@ export function AiSettingsCard() {
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [model, setModel] = useState("gpt-4o");
|
||||
const [baseUrl, setBaseUrl] = useState("");
|
||||
const [systemPrompt, setSystemPrompt] = useState("");
|
||||
|
||||
// Ollama specific state
|
||||
const [ollamaModels, setOllamaModels] = useState<string[]>([]);
|
||||
@@ -36,6 +38,7 @@ export function AiSettingsCard() {
|
||||
setApiKey(settings.ai_api_key || "");
|
||||
setModel(settings.ai_model || "gpt-4o");
|
||||
setBaseUrl(settings.ai_base_url || "");
|
||||
setSystemPrompt(settings.ai_system_prompt || "");
|
||||
}
|
||||
}, [settings]);
|
||||
|
||||
@@ -46,10 +49,10 @@ export function AiSettingsCard() {
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/admin/settings'] });
|
||||
toast({ title: "Settings saved" });
|
||||
toast({ title: t('settings.ai.saved', "Settings saved") });
|
||||
},
|
||||
onError: () => {
|
||||
toast({ title: "Failed to save settings", variant: "destructive" });
|
||||
toast({ title: t('settings.ai.error', "Failed to save settings"), variant: "destructive" });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -62,10 +65,10 @@ export function AiSettingsCard() {
|
||||
onSuccess: (data: any) => {
|
||||
const models = data.models?.map((m: any) => m.name) || [];
|
||||
setOllamaModels(models);
|
||||
toast({ title: `Found ${models.length} models` });
|
||||
toast({ title: t('settings.ai.ollama.found', { count: models.length }) });
|
||||
},
|
||||
onError: (err: Error) => {
|
||||
toast({ title: "Could not connect to Ollama", description: err.message, variant: "destructive" });
|
||||
toast({ title: t('settings.ai.ollama.connectError', "Could not connect to Ollama"), description: err.message, variant: "destructive" });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -76,12 +79,12 @@ export function AiSettingsCard() {
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast({ title: "Model pulled successfully" });
|
||||
toast({ title: t('settings.ai.ollama.pullSuccess', "Model pulled successfully") });
|
||||
setPullModelName("");
|
||||
fetchOllamaModelsMutation.mutate(); // Refresh list
|
||||
},
|
||||
onError: (err: Error) => {
|
||||
toast({ title: "Failed to pull model", description: err.message, variant: "destructive" });
|
||||
toast({ title: t('settings.ai.ollama.pullError', "Failed to pull model"), description: err.message, variant: "destructive" });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -91,7 +94,8 @@ export function AiSettingsCard() {
|
||||
ai_provider: provider,
|
||||
ai_api_key: apiKey,
|
||||
ai_model: model,
|
||||
ai_base_url: baseUrl
|
||||
ai_base_url: baseUrl,
|
||||
ai_system_prompt: systemPrompt
|
||||
});
|
||||
};
|
||||
|
||||
@@ -136,7 +140,7 @@ export function AiSettingsCard() {
|
||||
type="password"
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
placeholder={provider === 'ollama' ? 'Optional for Ollama' : 'sk-...'}
|
||||
placeholder={provider === 'ollama' ? t('settings.ai.ollama.placeholder', 'Optional for Ollama') : 'sk-...'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -217,6 +221,21 @@ export function AiSettingsCard() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid gap-2 pt-4 border-t">
|
||||
<Label className="flex items-center justify-between">
|
||||
{t('settings.ai.systemPrompt', 'System Prompt')}
|
||||
<span className="text-xs text-muted-foreground font-normal">
|
||||
Available: {"${user.username}"}, {"${currentDate...}"}, {"${context}"}
|
||||
</span>
|
||||
</Label>
|
||||
<Textarea
|
||||
className="font-mono text-sm min-h-[300px]"
|
||||
value={systemPrompt}
|
||||
onChange={(e) => setSystemPrompt(e.target.value)}
|
||||
placeholder="You are TaskFlow AI..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button onClick={handleSave} disabled={mutation.isPending}>
|
||||
|
||||
@@ -42,18 +42,18 @@ export function AuditLogsTable() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Audit Logs</CardTitle>
|
||||
<CardDescription>Track all system changes and AI actions.</CardDescription>
|
||||
<CardTitle>{t('settings.auditLogs.title')}</CardTitle>
|
||||
<CardDescription>{t('settings.auditLogs.description')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Time</TableHead>
|
||||
<TableHead>Source</TableHead>
|
||||
<TableHead>Action</TableHead>
|
||||
<TableHead>Entity</TableHead>
|
||||
<TableHead>Details</TableHead>
|
||||
<TableHead>{t('settings.auditLogs.table.time')}</TableHead>
|
||||
<TableHead>{t('settings.auditLogs.table.source')}</TableHead>
|
||||
<TableHead>{t('settings.auditLogs.table.action')}</TableHead>
|
||||
<TableHead>{t('settings.auditLogs.table.entity')}</TableHead>
|
||||
<TableHead>{t('settings.auditLogs.table.details')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -82,7 +82,7 @@ export function AuditLogsTable() {
|
||||
{(!logs || logs.length === 0) && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="text-center py-8 text-muted-foreground">
|
||||
No logs found.
|
||||
{t('settings.auditLogs.table.empty')}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
|
||||
@@ -48,7 +48,7 @@ export function McpSettingsCard() {
|
||||
|
||||
const copyToClipboard = (text: string) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
toast({ title: "Copied!" });
|
||||
toast({ title: t('settings.mcp.copied', "Copied!") });
|
||||
}
|
||||
|
||||
const { data: settings, isLoading } = useQuery<any>({
|
||||
@@ -97,19 +97,19 @@ export function McpSettingsCard() {
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<Server className="h-5 w-5 text-primary" />
|
||||
<CardTitle>MCP Server Configuration</CardTitle>
|
||||
<CardTitle>{t('settings.mcp.title')}</CardTitle>
|
||||
</div>
|
||||
<CardDescription>
|
||||
Configure the Model Context Protocol (MCP) server settings.
|
||||
The MCP server runs on the same port as the application (/api/mcp).
|
||||
{t('settings.mcp.description')}
|
||||
<span className="block mt-1 text-xs">{t('settings.mcp.descriptionDetail', "The MCP server runs on the same port as the application (/api/mcp).")}</span>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="mcp_enabled" className="flex flex-col gap-1">
|
||||
<span>Enable MCP Server</span>
|
||||
<span>{t('settings.mcp.enableLabel')}</span>
|
||||
<span className="font-normal text-xs text-muted-foreground">
|
||||
Allow external AI tools to connect via MCP protocol.
|
||||
{t('settings.mcp.enableDesc')}
|
||||
</span>
|
||||
</Label>
|
||||
<Switch
|
||||
@@ -169,7 +169,7 @@ export function McpSettingsCard() {
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="mcp_port">Port (Informational)</Label>
|
||||
<Label htmlFor="mcp_port">{t('settings.mcp.portLabel')}</Label>
|
||||
<Input
|
||||
id="mcp_port"
|
||||
value={formData.mcp_port}
|
||||
@@ -179,7 +179,7 @@ export function McpSettingsCard() {
|
||||
className="bg-muted"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Currently runs on the main application port. Separate port configuration coming soon.
|
||||
{t('settings.mcp.portDesc')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Download, Loader2, FileJson } from "lucide-react";
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
|
||||
|
||||
import { User } from "@shared/schema";
|
||||
|
||||
interface DataExportCardProps {
|
||||
user: User | undefined | null;
|
||||
}
|
||||
|
||||
export function DataExportCard({ user }: DataExportCardProps) {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
|
||||
|
||||
const [includeTasks, setIncludeTasks] = useState(true);
|
||||
const [includeLabels, setIncludeLabels] = useState(true);
|
||||
const [includeSettings, setIncludeSettings] = useState(false);
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
setIsExporting(true);
|
||||
const response = await apiRequest("POST", "/api/user/export", {
|
||||
includeTasks,
|
||||
includeLabels,
|
||||
includeSettings
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("Export failed");
|
||||
|
||||
// Handle blob download
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `taskflow_export_${user?.username}_${new Date().toISOString().split('T')[0]}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
|
||||
toast({ title: t('settings.export.success', "Data exported successfully") });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast({
|
||||
title: t('settings.export.error', "Failed to export data"),
|
||||
variant: "destructive"
|
||||
});
|
||||
} finally {
|
||||
setIsExporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<FileJson className="w-5 h-5" />
|
||||
{t('settings.export.title')}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{t('settings.export.description')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="tasks"
|
||||
checked={includeTasks}
|
||||
onCheckedChange={(c) => setIncludeTasks(!!c)}
|
||||
/>
|
||||
<Label htmlFor="tasks">{t('settings.export.tasks')}</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="labels"
|
||||
checked={includeLabels}
|
||||
onCheckedChange={(c) => setIncludeLabels(!!c)}
|
||||
/>
|
||||
<Label htmlFor="labels">{t('settings.export.labels')}</Label>
|
||||
</div>
|
||||
|
||||
{user?.role === 'admin' && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="settings"
|
||||
checked={includeSettings}
|
||||
onCheckedChange={(c) => setIncludeSettings(!!c)}
|
||||
/>
|
||||
<Label htmlFor="settings">
|
||||
{t('settings.export.systemSettings')}
|
||||
</Label>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button
|
||||
onClick={handleExport}
|
||||
disabled={isExporting || (!includeTasks && !includeLabels && !includeSettings)}
|
||||
>
|
||||
{isExporting ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Download className="mr-2 h-4 w-4" />}
|
||||
{t('settings.export.button')}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user