From ff549aed8b35c5d1133cb8c35de5d357fcfedda8 Mon Sep 17 00:00:00 2001 From: ghaddaditw <40211818-ghaddaditw@users.noreply.replit.com> Date: Sun, 8 Jun 2025 06:43:12 +0000 Subject: [PATCH] Add language switching and enhance task/financial management features Implement i18next for language switching in the header, enhance task and financial schemas, and add BudgetPlanner.tsx. Replit-Commit-Author: Agent Replit-Commit-Session-Id: ff0be73b-afdd-4747-978b-bb8301fb0a82 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9777c70b-fc38-4831-8d6b-78dfffe041b0/012e00c6-f380-4e55-bcb9-10f9adbfbb7b.jpg --- .../components/financial/BudgetPlanner.tsx | 472 ++++++++++++++++++ client/src/components/layout/Header.tsx | 6 + shared/schema.ts | 123 ++++- 3 files changed, 598 insertions(+), 3 deletions(-) create mode 100644 client/src/components/financial/BudgetPlanner.tsx diff --git a/client/src/components/financial/BudgetPlanner.tsx b/client/src/components/financial/BudgetPlanner.tsx new file mode 100644 index 0000000..27916a8 --- /dev/null +++ b/client/src/components/financial/BudgetPlanner.tsx @@ -0,0 +1,472 @@ +import { useState } from "react"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { useTranslation } from "react-i18next"; +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 { Progress } from "@/components/ui/progress"; +import { Badge } from "@/components/ui/badge"; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { Textarea } from "@/components/ui/textarea"; +import { Switch } from "@/components/ui/switch"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { useToast } from "@/hooks/use-toast"; +import { apiRequest } from "@/lib/queryClient"; +import { Plus, TrendingUp, TrendingDown, AlertTriangle, Edit, Trash2 } from "lucide-react"; + +const budgetSchema = z.object({ + name: z.string().min(1, "Budget name is required"), + amount: z.number().min(0.01, "Amount must be greater than 0"), + period: z.enum(["weekly", "monthly", "yearly"]), + categoryId: z.number().optional(), + alertThreshold: z.number().min(1).max(100).default(80), + isActive: z.boolean().default(true), + startDate: z.string(), + endDate: z.string().optional(), +}); + +type BudgetFormData = z.infer; + +interface Budget { + id: number; + name: string; + amount: number; + period: string; + categoryId?: number; + category?: { name: string; color: string }; + alertThreshold: number; + isActive: boolean; + startDate: string; + endDate?: string; + used: number; + remaining: number; + percentage: number; + createdAt: string; +} + +export function BudgetPlanner() { + const { t } = useTranslation(); + const { toast } = useToast(); + const queryClient = useQueryClient(); + const [open, setOpen] = useState(false); + const [editingBudget, setEditingBudget] = useState(null); + + const form = useForm({ + resolver: zodResolver(budgetSchema), + defaultValues: { + name: "", + amount: 0, + period: "monthly", + alertThreshold: 80, + isActive: true, + startDate: new Date().toISOString().split('T')[0], + }, + }); + + const { data: budgets = [], isLoading } = useQuery({ + queryKey: ["/api/finances/budgets"], + }); + + const { data: categories = [] } = useQuery({ + queryKey: ["/api/finances/categories"], + }); + + const createBudgetMutation = useMutation({ + mutationFn: (data: BudgetFormData) => apiRequest("/api/finances/budgets", { + method: "POST", + body: JSON.stringify(data), + }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/finances/budgets"] }); + setOpen(false); + form.reset(); + toast({ + title: t("success.created"), + description: t("finances.budgetCreated"), + }); + }, + }); + + const updateBudgetMutation = useMutation({ + mutationFn: ({ id, ...data }: BudgetFormData & { id: number }) => + apiRequest(`/api/finances/budgets/${id}`, { + method: "PATCH", + body: JSON.stringify(data), + }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/finances/budgets"] }); + setEditingBudget(null); + form.reset(); + toast({ + title: t("success.updated"), + description: t("finances.budgetUpdated"), + }); + }, + }); + + const deleteBudgetMutation = useMutation({ + mutationFn: (id: number) => apiRequest(`/api/finances/budgets/${id}`, { + method: "DELETE", + }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/finances/budgets"] }); + toast({ + title: t("success.deleted"), + description: t("finances.budgetDeleted"), + }); + }, + }); + + const handleSubmit = (data: BudgetFormData) => { + if (editingBudget) { + updateBudgetMutation.mutate({ ...data, id: editingBudget.id }); + } else { + createBudgetMutation.mutate(data); + } + }; + + const handleEdit = (budget: Budget) => { + setEditingBudget(budget); + form.reset({ + name: budget.name, + amount: budget.amount, + period: budget.period as "weekly" | "monthly" | "yearly", + categoryId: budget.categoryId, + alertThreshold: budget.alertThreshold, + isActive: budget.isActive, + startDate: budget.startDate.split('T')[0], + endDate: budget.endDate?.split('T')[0], + }); + setOpen(true); + }; + + const getBudgetStatus = (budget: Budget) => { + if (budget.percentage >= budget.alertThreshold) { + return { color: "destructive", icon: AlertTriangle }; + } else if (budget.percentage >= 70) { + return { color: "yellow", icon: TrendingUp }; + } + return { color: "green", icon: TrendingDown }; + }; + + if (isLoading) { + return ( +
+ {[...Array(3)].map((_, i) => ( + + +
+
+
+ +
+
+
+
+
+
+
+
+
+ ))} +
+ ); + } + + return ( +
+
+

{t("finances.budgetPlanning")}

+ + + + + + + + {editingBudget ? t("finances.editBudget") : t("finances.createBudget")} + + + {t("finances.budgetDescription")} + + +
+ + ( + + {t("finances.budgetName")} + + + + + + )} + /> + +
+ ( + + {t("finances.amount")} + + field.onChange(parseFloat(e.target.value) || 0)} + /> + + + + )} + /> + + ( + + {t("finances.period")} + + + + )} + /> +
+ + ( + + {t("finances.category")} ({t("common.optional")}) + + + + )} + /> + + ( + + {t("finances.alertThreshold")} (%) + + field.onChange(parseInt(e.target.value) || 80)} + /> + + + + )} + /> + +
+ ( + + {t("finances.startDate")} + + + + + + )} + /> + + ( + + {t("finances.endDate")} ({t("common.optional")}) + + + + + + )} + /> +
+ + ( + +
+ {t("finances.activeBudget")} +
+ {t("finances.activeBudgetDescription")} +
+
+ + + +
+ )} + /> + +
+ + +
+ + +
+
+
+ +
+ {budgets.map((budget: Budget) => { + const status = getBudgetStatus(budget); + const StatusIcon = status.icon; + + return ( + + +
+ {budget.name} +
+ + +
+
+ + {budget.category?.name && ( + +
+ {budget.category.name} + + )} + {t(`finances.${budget.period}`)} + + + +
+
+ ${budget.used.toFixed(2)} / ${budget.amount.toFixed(2)} +
+ + {budget.percentage.toFixed(0)}% +
+
+ + + +
+
+

{t("finances.budgetRemaining")}

+

${budget.remaining.toFixed(2)}

+
+
+

{t("finances.alertThreshold")}

+

{budget.alertThreshold}%

+
+
+ + {!budget.isActive && ( + + {t("finances.inactive")} + + )} +
+
+ + ); + })} +
+ + {budgets.length === 0 && ( + + + +

{t("finances.noBudgets")}

+

+ {t("finances.noBudgetsDescription")} +

+ +
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/client/src/components/layout/Header.tsx b/client/src/components/layout/Header.tsx index 0ecb709..54d069d 100644 --- a/client/src/components/layout/Header.tsx +++ b/client/src/components/layout/Header.tsx @@ -12,6 +12,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { LanguageSwitcher } from "@/components/LanguageSwitcher"; import { Moon, Sun, @@ -21,10 +22,12 @@ import { LogOut, Activity } from "lucide-react"; +import { useTranslation } from "react-i18next"; export default function Header() { const { user, logout } = useAuth(); const { theme, toggleTheme } = useTheme(); + const { t } = useTranslation(); const [, setLocation] = useLocation(); const handleLogout = async () => { @@ -76,6 +79,9 @@ export default function Header() { Voice Ready
+ {/* Language Switcher */} + + {/* Theme Toggle */}