Add new pages to access key features and fix page navigation
Adds Tasks, Finances, Voice, AI, Analytics, and Settings pages with routing in App.tsx. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 10e60398-05df-4c0e-8698-578a1494e818 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/81470e0d-8ae8-4335-9301-cd9a69e670fa/29a30ac5-78e2-49a4-b73b-7a3f9967ce79.jpg
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
import { useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import Sidebar from "@/components/layout/Sidebar";
|
||||
import { Brain, MessageSquare, Lightbulb, Laugh, Send } from "lucide-react";
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
|
||||
export default function AIPage() {
|
||||
const { toast } = useToast();
|
||||
const [message, setMessage] = useState("");
|
||||
const [chatHistory, setChatHistory] = useState<any[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { data: dailyJoke } = useQuery({
|
||||
queryKey: ["/api/ai/daily-joke"],
|
||||
});
|
||||
|
||||
const { data: status } = useQuery({
|
||||
queryKey: ["/api/ai/status"],
|
||||
});
|
||||
|
||||
const { data: interactions = [] } = useQuery({
|
||||
queryKey: ["/api/ai/interactions"],
|
||||
});
|
||||
|
||||
const handleSendMessage = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!message.trim()) return;
|
||||
|
||||
const userMessage = { role: "user", content: message, timestamp: new Date() };
|
||||
setChatHistory(prev => [...prev, userMessage]);
|
||||
setMessage("");
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const response = await apiRequest("/api/ai/chat", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ message }),
|
||||
});
|
||||
|
||||
const aiMessage = {
|
||||
role: "assistant",
|
||||
content: response.content || "I'm here to help! How can I assist you today?",
|
||||
timestamp: new Date()
|
||||
};
|
||||
setChatHistory(prev => [...prev, aiMessage]);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to send message. Please try again.",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const generatePersonalizedJoke = async () => {
|
||||
try {
|
||||
const response = await apiRequest("/api/ai/joke", {
|
||||
method: "POST",
|
||||
});
|
||||
toast({
|
||||
title: "Here's a joke for you!",
|
||||
description: response.content || "Why don't tasks ever get lonely? Because they always have deadlines to meet!",
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to generate joke. Please try again.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const generateInsight = async (type: string) => {
|
||||
try {
|
||||
const response = await apiRequest("/api/ai/insight", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ type }),
|
||||
});
|
||||
toast({
|
||||
title: `${type.charAt(0).toUpperCase() + type.slice(1)} Insight`,
|
||||
description: response.content || "Here's an insight based on your data!",
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to generate insight. Please try again.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-gray-50 dark:bg-gray-900">
|
||||
<Sidebar className="w-64 border-r" />
|
||||
<div className="flex-1 overflow-auto">
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">AI Assistant</h1>
|
||||
<p className="text-gray-600 dark:text-gray-300">
|
||||
Chat with your AI assistant and get personalized insights
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Chat Interface */}
|
||||
<div className="lg:col-span-2">
|
||||
<Card className="h-[600px] flex flex-col">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<MessageSquare className="w-5 h-5" />
|
||||
AI Chat
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Have a conversation with your AI assistant
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 flex flex-col">
|
||||
<div className="flex-1 overflow-auto space-y-4 mb-4">
|
||||
{chatHistory.length === 0 ? (
|
||||
<div className="text-center text-gray-500 mt-8">
|
||||
<Brain className="w-12 h-12 mx-auto mb-4 text-gray-400" />
|
||||
<p>Start a conversation with your AI assistant!</p>
|
||||
<p className="text-sm mt-2">Try asking about your tasks, finances, or request insights.</p>
|
||||
</div>
|
||||
) : (
|
||||
chatHistory.map((msg, index) => (
|
||||
<div key={index} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
|
||||
<div className={`max-w-[80%] p-3 rounded-lg ${
|
||||
msg.role === 'user'
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white'
|
||||
}`}>
|
||||
<p>{msg.content}</p>
|
||||
<p className="text-xs opacity-70 mt-1">
|
||||
{new Date(msg.timestamp).toLocaleTimeString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
{isLoading && (
|
||||
<div className="flex justify-start">
|
||||
<div className="bg-gray-100 dark:bg-gray-800 p-3 rounded-lg">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="animate-spin w-4 h-4 border-2 border-gray-300 border-t-gray-600 rounded-full"></div>
|
||||
<span className="text-gray-600 dark:text-gray-300">AI is thinking...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<form onSubmit={handleSendMessage} className="flex gap-2">
|
||||
<Input
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
placeholder="Type your message..."
|
||||
disabled={isLoading}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button type="submit" disabled={isLoading || !message.trim()}>
|
||||
<Send className="w-4 h-4" />
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* AI Features Sidebar */}
|
||||
<div className="space-y-6">
|
||||
{/* AI Status */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Brain className="w-5 h-5" />
|
||||
AI Status
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm">Model Status</span>
|
||||
<Badge variant={status?.loaded ? "default" : "secondary"}>
|
||||
{status?.loaded ? "Ready" : "Loading"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm">Interactions Today</span>
|
||||
<Badge variant="outline">
|
||||
{interactions.length}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Quick Actions</CardTitle>
|
||||
<CardDescription>
|
||||
Get instant AI-powered assistance
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<Button onClick={generatePersonalizedJoke} className="w-full" variant="outline">
|
||||
<Laugh className="w-4 h-4 mr-2" />
|
||||
Get a Joke
|
||||
</Button>
|
||||
<Button onClick={() => generateInsight('productivity')} className="w-full" variant="outline">
|
||||
<Lightbulb className="w-4 h-4 mr-2" />
|
||||
Productivity Insight
|
||||
</Button>
|
||||
<Button onClick={() => generateInsight('financial')} className="w-full" variant="outline">
|
||||
<Lightbulb className="w-4 h-4 mr-2" />
|
||||
Financial Insight
|
||||
</Button>
|
||||
<Button onClick={() => generateInsight('general')} className="w-full" variant="outline">
|
||||
<Lightbulb className="w-4 h-4 mr-2" />
|
||||
General Insight
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Daily Joke */}
|
||||
{dailyJoke && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Laugh className="w-5 h-5" />
|
||||
Daily Joke
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm italic">"{dailyJoke.joke}"</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Recent Interactions */}
|
||||
{interactions.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Recent Interactions</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{interactions.slice(0, 5).map((interaction: any) => (
|
||||
<div key={interaction.id} className="text-xs p-2 bg-gray-50 dark:bg-gray-800 rounded">
|
||||
<div className="font-medium">{interaction.type}</div>
|
||||
<div className="text-gray-600 dark:text-gray-300">
|
||||
{new Date(interaction.createdAt).toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import Sidebar from "@/components/layout/Sidebar";
|
||||
import { BarChart3, TrendingUp, TrendingDown, Calendar, Target, DollarSign } from "lucide-react";
|
||||
|
||||
export default function AnalyticsPage() {
|
||||
const { data: tasks = [], isLoading: tasksLoading } = useQuery({
|
||||
queryKey: ["/api/tasks"],
|
||||
});
|
||||
|
||||
const { data: financialSummary = { income: 0, expenses: 0, net: 0 }, isLoading: financialLoading } = useQuery({
|
||||
queryKey: ["/api/financial/summary"],
|
||||
});
|
||||
|
||||
const { data: records = [], isLoading: recordsLoading } = useQuery({
|
||||
queryKey: ["/api/financial/records"],
|
||||
});
|
||||
|
||||
if (tasksLoading || financialLoading || recordsLoading) {
|
||||
return (
|
||||
<div className="flex h-screen bg-gray-50 dark:bg-gray-900">
|
||||
<Sidebar className="w-64 border-r" />
|
||||
<div className="flex-1 overflow-auto p-6">
|
||||
<div className="animate-pulse space-y-4">
|
||||
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-1/4"></div>
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/2"></div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<div key={i} className="h-32 bg-gray-200 dark:bg-gray-700 rounded"></div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate task analytics
|
||||
const completedTasks = tasks.filter((task: any) => task.status === 'completed').length;
|
||||
const pendingTasks = tasks.filter((task: any) => task.status === 'pending').length;
|
||||
const inProgressTasks = tasks.filter((task: any) => task.status === 'in_progress').length;
|
||||
const totalTasks = tasks.length;
|
||||
const completionRate = totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0;
|
||||
|
||||
// Calculate financial analytics
|
||||
const thisMonth = new Date().getMonth();
|
||||
const thisYear = new Date().getFullYear();
|
||||
const monthlyRecords = records.filter((record: any) => {
|
||||
const recordDate = new Date(record.createdAt);
|
||||
return recordDate.getMonth() === thisMonth && recordDate.getFullYear() === thisYear;
|
||||
});
|
||||
|
||||
const monthlyIncome = monthlyRecords
|
||||
.filter((record: any) => record.type === 'income')
|
||||
.reduce((sum: number, record: any) => sum + record.amount, 0);
|
||||
|
||||
const monthlyExpenses = monthlyRecords
|
||||
.filter((record: any) => record.type === 'expense')
|
||||
.reduce((sum: number, record: any) => sum + record.amount, 0);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-gray-50 dark:bg-gray-900">
|
||||
<Sidebar className="w-64 border-r" />
|
||||
<div className="flex-1 overflow-auto">
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Analytics</h1>
|
||||
<p className="text-gray-600 dark:text-gray-300">
|
||||
Insights and performance metrics for your tasks and finances
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Task Analytics */}
|
||||
<div className="mb-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">Task Performance</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Tasks</CardTitle>
|
||||
<Target className="h-4 w-4 text-blue-600" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{totalTasks}</div>
|
||||
<p className="text-xs text-muted-foreground">All time</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Completed</CardTitle>
|
||||
<TrendingUp className="h-4 w-4 text-green-600" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-green-600">{completedTasks}</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{completionRate.toFixed(1)}% completion rate
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">In Progress</CardTitle>
|
||||
<Calendar className="h-4 w-4 text-orange-600" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-orange-600">{inProgressTasks}</div>
|
||||
<p className="text-xs text-muted-foreground">Active tasks</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Pending</CardTitle>
|
||||
<TrendingDown className="h-4 w-4 text-red-600" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-red-600">{pendingTasks}</div>
|
||||
<p className="text-xs text-muted-foreground">Awaiting action</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Financial Analytics */}
|
||||
<div className="mb-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">Financial Overview</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Income</CardTitle>
|
||||
<TrendingUp className="h-4 w-4 text-green-600" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
${financialSummary.income.toFixed(2)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
This month: ${monthlyIncome.toFixed(2)}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Expenses</CardTitle>
|
||||
<TrendingDown className="h-4 w-4 text-red-600" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-red-600">
|
||||
${financialSummary.expenses.toFixed(2)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
This month: ${monthlyExpenses.toFixed(2)}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Net Balance</CardTitle>
|
||||
<DollarSign className="h-4 w-4 text-blue-600" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className={`text-2xl font-bold ${financialSummary.net >= 0 ? 'text-green-600' : 'text-red-600'}`}>
|
||||
${financialSummary.net.toFixed(2)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Monthly: ${(monthlyIncome - monthlyExpenses).toFixed(2)}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Performance Insights */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<BarChart3 className="w-5 h-5" />
|
||||
Task Distribution
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Breakdown of your task statuses
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 bg-green-500 rounded"></div>
|
||||
<span className="text-sm">Completed</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium">{completedTasks}</span>
|
||||
<Badge variant="secondary">{totalTasks > 0 ? ((completedTasks / totalTasks) * 100).toFixed(0) : 0}%</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 bg-orange-500 rounded"></div>
|
||||
<span className="text-sm">In Progress</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium">{inProgressTasks}</span>
|
||||
<Badge variant="secondary">{totalTasks > 0 ? ((inProgressTasks / totalTasks) * 100).toFixed(0) : 0}%</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 bg-red-500 rounded"></div>
|
||||
<span className="text-sm">Pending</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium">{pendingTasks}</span>
|
||||
<Badge variant="secondary">{totalTasks > 0 ? ((pendingTasks / totalTasks) * 100).toFixed(0) : 0}%</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Financial Health</CardTitle>
|
||||
<CardDescription>
|
||||
Your financial performance indicators
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm">Savings Rate</span>
|
||||
<Badge variant={financialSummary.income > 0 && (financialSummary.net / financialSummary.income) > 0.2 ? "default" : "secondary"}>
|
||||
{financialSummary.income > 0 ? ((financialSummary.net / financialSummary.income) * 100).toFixed(1) : 0}%
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm">Monthly Trends</span>
|
||||
<Badge variant={(monthlyIncome - monthlyExpenses) >= 0 ? "default" : "destructive"}>
|
||||
{(monthlyIncome - monthlyExpenses) >= 0 ? "Positive" : "Negative"}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm">Transaction Count</span>
|
||||
<Badge variant="outline">
|
||||
{records.length} total
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm">This Month</span>
|
||||
<Badge variant="outline">
|
||||
{monthlyRecords.length} transactions
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Recommendations */}
|
||||
<Card className="mt-6">
|
||||
<CardHeader>
|
||||
<CardTitle>Recommendations</CardTitle>
|
||||
<CardDescription>
|
||||
Suggestions to improve your productivity and financial health
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{completionRate < 70 && (
|
||||
<div className="p-3 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg">
|
||||
<p className="text-sm text-yellow-800 dark:text-yellow-200">
|
||||
<strong>Task Management:</strong> Your completion rate is {completionRate.toFixed(1)}%.
|
||||
Consider breaking down large tasks into smaller, manageable pieces.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{pendingTasks > inProgressTasks && pendingTasks > 0 && (
|
||||
<div className="p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||||
<p className="text-sm text-blue-800 dark:text-blue-200">
|
||||
<strong>Productivity:</strong> You have {pendingTasks} pending tasks.
|
||||
Start working on them to improve your productivity flow.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{financialSummary.net < 0 && (
|
||||
<div className="p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
||||
<p className="text-sm text-red-800 dark:text-red-200">
|
||||
<strong>Financial Health:</strong> Your expenses exceed your income.
|
||||
Review your spending patterns and consider creating a budget.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{financialSummary.income > 0 && (financialSummary.net / financialSummary.income) > 0.3 && (
|
||||
<div className="p-3 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
|
||||
<p className="text-sm text-green-800 dark:text-green-200">
|
||||
<strong>Great Job!</strong> You're saving over 30% of your income.
|
||||
Consider investing your savings for long-term growth.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
import { useState } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import Sidebar from "@/components/layout/Sidebar";
|
||||
import { Plus, DollarSign, TrendingUp, TrendingDown } from "lucide-react";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
|
||||
export default function FinancesPage() {
|
||||
const { toast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [newRecord, setNewRecord] = useState({
|
||||
type: "expense",
|
||||
amount: "",
|
||||
description: "",
|
||||
category: "",
|
||||
});
|
||||
|
||||
const { data: records = [], isLoading: recordsLoading } = useQuery({
|
||||
queryKey: ["/api/financial/records"],
|
||||
});
|
||||
|
||||
const { data: summary = { income: 0, expenses: 0, net: 0 }, isLoading: summaryLoading } = useQuery({
|
||||
queryKey: ["/api/financial/summary"],
|
||||
});
|
||||
|
||||
const createRecordMutation = useMutation({
|
||||
mutationFn: (record: typeof newRecord) =>
|
||||
apiRequest("/api/financial/records", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
...record,
|
||||
amount: parseFloat(record.amount),
|
||||
}),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/financial/records"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/financial/summary"] });
|
||||
setOpen(false);
|
||||
setNewRecord({ type: "expense", amount: "", description: "", category: "" });
|
||||
toast({
|
||||
title: "Record created",
|
||||
description: "Your financial record has been successfully created.",
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to create record. Please try again.",
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleCreateRecord = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!newRecord.amount || isNaN(parseFloat(newRecord.amount))) {
|
||||
toast({
|
||||
title: "Invalid amount",
|
||||
description: "Please enter a valid amount.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
createRecordMutation.mutate(newRecord);
|
||||
};
|
||||
|
||||
const categories = {
|
||||
income: ["Salary", "Freelance", "Investment", "Business", "Other"],
|
||||
expense: ["Food", "Transportation", "Housing", "Entertainment", "Healthcare", "Shopping", "Other"],
|
||||
};
|
||||
|
||||
if (recordsLoading || summaryLoading) {
|
||||
return (
|
||||
<div className="flex h-screen bg-gray-50 dark:bg-gray-900">
|
||||
<Sidebar className="w-64 border-r" />
|
||||
<div className="flex-1 overflow-auto p-6">
|
||||
<div className="animate-pulse space-y-4">
|
||||
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-1/4"></div>
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/2"></div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="h-24 bg-gray-200 dark:bg-gray-700 rounded"></div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-gray-50 dark:bg-gray-900">
|
||||
<Sidebar className="w-64 border-r" />
|
||||
<div className="flex-1 overflow-auto">
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Finances</h1>
|
||||
<p className="text-gray-600 dark:text-gray-300">
|
||||
Track your income, expenses, and financial goals
|
||||
</p>
|
||||
</div>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Add Record
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add Financial Record</DialogTitle>
|
||||
<DialogDescription>
|
||||
Add a new income or expense record.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleCreateRecord} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="type">Type</Label>
|
||||
<Select value={newRecord.type} onValueChange={(value) => setNewRecord({ ...newRecord, type: value, category: "" })}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="income">Income</SelectItem>
|
||||
<SelectItem value="expense">Expense</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="amount">Amount</Label>
|
||||
<Input
|
||||
id="amount"
|
||||
type="number"
|
||||
step="0.01"
|
||||
placeholder="0.00"
|
||||
value={newRecord.amount}
|
||||
onChange={(e) => setNewRecord({ ...newRecord, amount: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="category">Category</Label>
|
||||
<Select value={newRecord.category} onValueChange={(value) => setNewRecord({ ...newRecord, category: value })}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select category" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categories[newRecord.type as keyof typeof categories].map((category) => (
|
||||
<SelectItem key={category} value={category}>
|
||||
{category}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Input
|
||||
id="description"
|
||||
placeholder="Brief description"
|
||||
value={newRecord.description}
|
||||
onChange={(e) => setNewRecord({ ...newRecord, description: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" disabled={createRecordMutation.isPending}>
|
||||
{createRecordMutation.isPending ? "Adding..." : "Add Record"}
|
||||
</Button>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{/* Summary Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Income</CardTitle>
|
||||
<TrendingUp className="h-4 w-4 text-green-600" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
${summary.income.toFixed(2)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Expenses</CardTitle>
|
||||
<TrendingDown className="h-4 w-4 text-red-600" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-red-600">
|
||||
${summary.expenses.toFixed(2)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Net Balance</CardTitle>
|
||||
<DollarSign className="h-4 w-4 text-blue-600" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className={`text-2xl font-bold ${summary.net >= 0 ? 'text-green-600' : 'text-red-600'}`}>
|
||||
${summary.net.toFixed(2)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Records List */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Recent Transactions</CardTitle>
|
||||
<CardDescription>
|
||||
Your latest financial transactions
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{records.length === 0 ? (
|
||||
<div className="text-center py-6">
|
||||
<DollarSign className="w-12 h-12 mx-auto text-gray-400 mb-4" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||
No records yet
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Start tracking your finances by adding your first record.
|
||||
</p>
|
||||
<Button onClick={() => setOpen(true)}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Add Record
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{records.map((record: any) => (
|
||||
<div key={record.id} className="flex items-center justify-between p-4 border rounded-lg">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`p-2 rounded-full ${record.type === 'income' ? 'bg-green-100 text-green-600' : 'bg-red-100 text-red-600'}`}>
|
||||
{record.type === 'income' ? <TrendingUp className="w-4 h-4" /> : <TrendingDown className="w-4 h-4" />}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium">{record.description || record.category}</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-300">
|
||||
{record.category} • {new Date(record.createdAt).toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className={`font-semibold ${record.type === 'income' ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{record.type === 'income' ? '+' : '-'}${record.amount.toFixed(2)}
|
||||
</div>
|
||||
<Badge variant={record.type === 'income' ? 'default' : 'secondary'}>
|
||||
{record.type}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
import { useState } from "react";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import Sidebar from "@/components/layout/Sidebar";
|
||||
import { User, Bell, Shield, Palette } from "lucide-react";
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { user, updateProfile } = useAuth();
|
||||
const { toast } = useToast();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [profile, setProfile] = useState({
|
||||
username: user?.username || "",
|
||||
email: user?.email || "",
|
||||
fullName: user?.fullName || "",
|
||||
});
|
||||
const [notifications, setNotifications] = useState({
|
||||
emailNotifications: true,
|
||||
pushNotifications: true,
|
||||
taskReminders: true,
|
||||
financialAlerts: true,
|
||||
});
|
||||
|
||||
const handleProfileUpdate = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
try {
|
||||
await updateProfile(profile);
|
||||
toast({
|
||||
title: "Profile updated",
|
||||
description: "Your profile has been successfully updated.",
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to update profile. Please try again.",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-gray-50 dark:bg-gray-900">
|
||||
<Sidebar className="w-64 border-r" />
|
||||
<div className="flex-1 overflow-auto">
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Settings</h1>
|
||||
<p className="text-gray-600 dark:text-gray-300">
|
||||
Manage your account settings and preferences
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="profile" className="space-y-6">
|
||||
<TabsList>
|
||||
<TabsTrigger value="profile">
|
||||
<User className="w-4 h-4 mr-2" />
|
||||
Profile
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="notifications">
|
||||
<Bell className="w-4 h-4 mr-2" />
|
||||
Notifications
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="security">
|
||||
<Shield className="w-4 h-4 mr-2" />
|
||||
Security
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="appearance">
|
||||
<Palette className="w-4 h-4 mr-2" />
|
||||
Appearance
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="profile">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Profile Information</CardTitle>
|
||||
<CardDescription>
|
||||
Update your personal information and account details.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleProfileUpdate} className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="username">Username</Label>
|
||||
<Input
|
||||
id="username"
|
||||
value={profile.username}
|
||||
onChange={(e) => setProfile({ ...profile, username: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={profile.email}
|
||||
onChange={(e) => setProfile({ ...profile, email: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="fullName">Full Name</Label>
|
||||
<Input
|
||||
id="fullName"
|
||||
value={profile.fullName}
|
||||
onChange={(e) => setProfile({ ...profile, fullName: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? "Updating..." : "Update Profile"}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="notifications">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Notification Preferences</CardTitle>
|
||||
<CardDescription>
|
||||
Choose what notifications you want to receive.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label>Email Notifications</Label>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Receive notifications via email
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={notifications.emailNotifications}
|
||||
onCheckedChange={(checked) =>
|
||||
setNotifications({ ...notifications, emailNotifications: checked })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label>Push Notifications</Label>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Receive push notifications in your browser
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={notifications.pushNotifications}
|
||||
onCheckedChange={(checked) =>
|
||||
setNotifications({ ...notifications, pushNotifications: checked })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label>Task Reminders</Label>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Get reminded about upcoming tasks and deadlines
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={notifications.taskReminders}
|
||||
onCheckedChange={(checked) =>
|
||||
setNotifications({ ...notifications, taskReminders: checked })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label>Financial Alerts</Label>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Receive alerts about financial goals and budgets
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={notifications.financialAlerts}
|
||||
onCheckedChange={(checked) =>
|
||||
setNotifications({ ...notifications, financialAlerts: checked })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="security">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Security Settings</CardTitle>
|
||||
<CardDescription>
|
||||
Manage your account security and privacy settings.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="currentPassword">Current Password</Label>
|
||||
<Input id="currentPassword" type="password" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="newPassword">New Password</Label>
|
||||
<Input id="newPassword" type="password" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="confirmPassword">Confirm New Password</Label>
|
||||
<Input id="confirmPassword" type="password" />
|
||||
</div>
|
||||
<Button>Update Password</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="appearance">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Appearance Settings</CardTitle>
|
||||
<CardDescription>
|
||||
Customize the look and feel of your application.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label>Dark Mode</Label>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Switch between light and dark themes
|
||||
</p>
|
||||
</div>
|
||||
<Switch />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
import { useState } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import Sidebar from "@/components/layout/Sidebar";
|
||||
import { Plus, Calendar, Clock, AlertCircle } from "lucide-react";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
|
||||
export default function TasksPage() {
|
||||
const { toast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [newTask, setNewTask] = useState({
|
||||
title: "",
|
||||
description: "",
|
||||
priority: "medium",
|
||||
dueDate: "",
|
||||
});
|
||||
|
||||
const { data: tasks = [], isLoading } = useQuery({
|
||||
queryKey: ["/api/tasks"],
|
||||
});
|
||||
|
||||
const createTaskMutation = useMutation({
|
||||
mutationFn: (task: typeof newTask) =>
|
||||
apiRequest("/api/tasks", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(task),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/tasks"] });
|
||||
setOpen(false);
|
||||
setNewTask({ title: "", description: "", priority: "medium", dueDate: "" });
|
||||
toast({
|
||||
title: "Task created",
|
||||
description: "Your task has been successfully created.",
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to create task. Please try again.",
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const updateTaskMutation = useMutation({
|
||||
mutationFn: ({ id, status }: { id: number; status: string }) =>
|
||||
apiRequest(`/api/tasks/${id}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ status }),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/tasks"] });
|
||||
toast({
|
||||
title: "Task updated",
|
||||
description: "Task status has been updated.",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleCreateTask = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
createTaskMutation.mutate(newTask);
|
||||
};
|
||||
|
||||
const getPriorityColor = (priority: string) => {
|
||||
switch (priority) {
|
||||
case "high": return "destructive";
|
||||
case "medium": return "default";
|
||||
case "low": return "secondary";
|
||||
default: return "default";
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case "completed": return "secondary";
|
||||
case "in_progress": return "default";
|
||||
case "pending": return "outline";
|
||||
default: return "outline";
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-screen bg-gray-50 dark:bg-gray-900">
|
||||
<Sidebar className="w-64 border-r" />
|
||||
<div className="flex-1 overflow-auto p-6">
|
||||
<div className="animate-pulse space-y-4">
|
||||
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-1/4"></div>
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/2"></div>
|
||||
<div className="space-y-3">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="h-24 bg-gray-200 dark:bg-gray-700 rounded"></div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-gray-50 dark:bg-gray-900">
|
||||
<Sidebar className="w-64 border-r" />
|
||||
<div className="flex-1 overflow-auto">
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Tasks</h1>
|
||||
<p className="text-gray-600 dark:text-gray-300">
|
||||
Manage your tasks and track your progress
|
||||
</p>
|
||||
</div>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Add Task
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create New Task</DialogTitle>
|
||||
<DialogDescription>
|
||||
Add a new task to your todo list.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleCreateTask} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="title">Title</Label>
|
||||
<Input
|
||||
id="title"
|
||||
value={newTask.title}
|
||||
onChange={(e) => setNewTask({ ...newTask, title: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
value={newTask.description}
|
||||
onChange={(e) => setNewTask({ ...newTask, description: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="priority">Priority</Label>
|
||||
<Select value={newTask.priority} onValueChange={(value) => setNewTask({ ...newTask, priority: value })}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="low">Low</SelectItem>
|
||||
<SelectItem value="medium">Medium</SelectItem>
|
||||
<SelectItem value="high">High</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dueDate">Due Date</Label>
|
||||
<Input
|
||||
id="dueDate"
|
||||
type="date"
|
||||
value={newTask.dueDate}
|
||||
onChange={(e) => setNewTask({ ...newTask, dueDate: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="submit" disabled={createTaskMutation.isPending}>
|
||||
{createTaskMutation.isPending ? "Creating..." : "Create Task"}
|
||||
</Button>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{tasks.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="p-6 text-center">
|
||||
<Calendar className="w-12 h-12 mx-auto text-gray-400 mb-4" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||
No tasks yet
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
Get started by creating your first task.
|
||||
</p>
|
||||
<Button onClick={() => setOpen(true)}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Add Task
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
tasks.map((task: any) => (
|
||||
<Card key={task.id}>
|
||||
<CardHeader>
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<CardTitle className="text-lg">{task.title}</CardTitle>
|
||||
{task.description && (
|
||||
<CardDescription className="mt-1">
|
||||
{task.description}
|
||||
</CardDescription>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Badge variant={getPriorityColor(task.priority)}>
|
||||
{task.priority}
|
||||
</Badge>
|
||||
<Badge variant={getStatusColor(task.status)}>
|
||||
{task.status?.replace('_', ' ')}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-4 text-sm text-gray-600 dark:text-gray-300">
|
||||
{task.dueDate && (
|
||||
<div className="flex items-center gap-1">
|
||||
<Clock className="w-4 h-4" />
|
||||
Due: {new Date(task.dueDate).toLocaleDateString()}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-1">
|
||||
<Calendar className="w-4 h-4" />
|
||||
Created: {new Date(task.createdAt).toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{task.status !== "completed" && (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => updateTaskMutation.mutate({ id: task.id, status: "completed" })}
|
||||
disabled={updateTaskMutation.isPending}
|
||||
>
|
||||
Mark Complete
|
||||
</Button>
|
||||
)}
|
||||
{task.status === "pending" && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => updateTaskMutation.mutate({ id: task.id, status: "in_progress" })}
|
||||
disabled={updateTaskMutation.isPending}
|
||||
>
|
||||
Start Task
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
import { useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import Sidebar from "@/components/layout/Sidebar";
|
||||
import { Mic, MicOff, Volume2, Settings, PlayCircle, StopCircle } from "lucide-react";
|
||||
import { useVoice } from "@/hooks/useVoice";
|
||||
|
||||
export default function VoicePage() {
|
||||
const { toast } = useToast();
|
||||
const [isListening, setIsListening] = useState(false);
|
||||
const { isEnabled, isSupported, startListening, stopListening } = useVoice();
|
||||
|
||||
const { data: commands = [], isLoading } = useQuery({
|
||||
queryKey: ["/api/voice/commands"],
|
||||
});
|
||||
|
||||
const { data: status } = useQuery({
|
||||
queryKey: ["/api/voice/status"],
|
||||
});
|
||||
|
||||
const handleToggleListening = () => {
|
||||
if (isListening) {
|
||||
stopListening();
|
||||
setIsListening(false);
|
||||
toast({
|
||||
title: "Voice recognition stopped",
|
||||
description: "Voice commands are no longer being listened for.",
|
||||
});
|
||||
} else {
|
||||
startListening();
|
||||
setIsListening(true);
|
||||
toast({
|
||||
title: "Voice recognition started",
|
||||
description: "Say a command to interact with the application.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const testTTS = () => {
|
||||
if ('speechSynthesis' in window) {
|
||||
const utterance = new SpeechSynthesisUtterance("Voice assistant is working correctly!");
|
||||
speechSynthesis.speak(utterance);
|
||||
} else {
|
||||
toast({
|
||||
title: "Text-to-speech not supported",
|
||||
description: "Your browser does not support text-to-speech.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-gray-50 dark:bg-gray-900">
|
||||
<Sidebar className="w-64 border-r" />
|
||||
<div className="flex-1 overflow-auto">
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Voice Commands</h1>
|
||||
<p className="text-gray-600 dark:text-gray-300">
|
||||
Control your application using voice commands
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||
{/* Voice Control */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Mic className="w-5 h-5" />
|
||||
Voice Control
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Start or stop voice recognition
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">Voice Recognition</span>
|
||||
<Badge variant={isSupported ? "default" : "destructive"}>
|
||||
{isSupported ? "Supported" : "Not Supported"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">Status</span>
|
||||
<Badge variant={isListening ? "default" : "secondary"}>
|
||||
{isListening ? "Listening" : "Stopped"}
|
||||
</Badge>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleToggleListening}
|
||||
disabled={!isSupported}
|
||||
className="w-full"
|
||||
variant={isListening ? "destructive" : "default"}
|
||||
>
|
||||
{isListening ? (
|
||||
<>
|
||||
<MicOff className="w-4 h-4 mr-2" />
|
||||
Stop Listening
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Mic className="w-4 h-4 mr-2" />
|
||||
Start Listening
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Text-to-Speech */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Volume2 className="w-5 h-5" />
|
||||
Text-to-Speech
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Test and configure speech output
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">TTS Support</span>
|
||||
<Badge variant={'speechSynthesis' in window ? "default" : "destructive"}>
|
||||
{'speechSynthesis' in window ? "Available" : "Not Available"}
|
||||
</Badge>
|
||||
</div>
|
||||
<Button onClick={testTTS} className="w-full" variant="outline">
|
||||
<PlayCircle className="w-4 h-4 mr-2" />
|
||||
Test Speech
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Available Commands */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Available Voice Commands</CardTitle>
|
||||
<CardDescription>
|
||||
These are the voice commands you can use
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium">Task Management</h4>
|
||||
<ul className="space-y-1 text-sm text-gray-600 dark:text-gray-300">
|
||||
<li>"Create task [task name]"</li>
|
||||
<li>"Mark task [task name] complete"</li>
|
||||
<li>"Show my tasks"</li>
|
||||
<li>"What are my pending tasks?"</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium">Financial Commands</h4>
|
||||
<ul className="space-y-1 text-sm text-gray-600 dark:text-gray-300">
|
||||
<li>"Add expense [amount] for [description]"</li>
|
||||
<li>"Add income [amount] from [source]"</li>
|
||||
<li>"Show my balance"</li>
|
||||
<li>"What did I spend this month?"</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium">Navigation</h4>
|
||||
<ul className="space-y-1 text-sm text-gray-600 dark:text-gray-300">
|
||||
<li>"Go to dashboard"</li>
|
||||
<li>"Open tasks"</li>
|
||||
<li>"Show finances"</li>
|
||||
<li>"Open settings"</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium">AI Assistant</h4>
|
||||
<ul className="space-y-1 text-sm text-gray-600 dark:text-gray-300">
|
||||
<li>"Tell me a joke"</li>
|
||||
<li>"Give me productivity tips"</li>
|
||||
<li>"Analyze my spending"</li>
|
||||
<li>"What should I focus on today?"</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Recent Voice Commands */}
|
||||
{!isLoading && commands.length > 0 && (
|
||||
<Card className="mt-6">
|
||||
<CardHeader>
|
||||
<CardTitle>Recent Commands</CardTitle>
|
||||
<CardDescription>
|
||||
Your recent voice command history
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{commands.slice(0, 10).map((command: any) => (
|
||||
<div key={command.id} className="flex items-center justify-between p-3 border rounded-lg">
|
||||
<div>
|
||||
<div className="font-medium">{command.transcription}</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-300">
|
||||
{new Date(command.createdAt).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant={command.processed ? "default" : "secondary"}>
|
||||
{command.processed ? "Processed" : "Pending"}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Setup Instructions */}
|
||||
<Card className="mt-6">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Settings className="w-5 h-5" />
|
||||
Setup Instructions
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">Getting Started</h4>
|
||||
<ol className="list-decimal list-inside space-y-1 text-sm text-gray-600 dark:text-gray-300">
|
||||
<li>Make sure your browser supports voice recognition (Chrome, Edge recommended)</li>
|
||||
<li>Allow microphone access when prompted</li>
|
||||
<li>Click "Start Listening" to begin voice recognition</li>
|
||||
<li>Speak clearly and wait for the system to process your command</li>
|
||||
<li>Commands are processed automatically and actions are executed</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">Tips for Better Recognition</h4>
|
||||
<ul className="list-disc list-inside space-y-1 text-sm text-gray-600 dark:text-gray-300">
|
||||
<li>Speak clearly and at a normal pace</li>
|
||||
<li>Use the exact command phrases listed above</li>
|
||||
<li>Ensure you're in a quiet environment</li>
|
||||
<li>Wait for the previous command to finish processing</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user