From 538d933baef56d7ee76f78617b553d63713efa24 Mon Sep 17 00:00:00 2001
From: Biswa Kalyan Bhuyan <biswa@surgot.in>
Date: Sun, 27 Apr 2025 23:02:42 +0530
Subject: finance: feat: added the goal page with some improvements of ui

---
 .../src/app/(main)/goals/components/goal-form.tsx  | 349 +++++++++++++++++++++
 1 file changed, 349 insertions(+)
 create mode 100644 frontend/src/app/(main)/goals/components/goal-form.tsx

(limited to 'frontend/src/app/(main)/goals/components/goal-form.tsx')

diff --git a/frontend/src/app/(main)/goals/components/goal-form.tsx b/frontend/src/app/(main)/goals/components/goal-form.tsx
new file mode 100644
index 0000000..6b1cbac
--- /dev/null
+++ b/frontend/src/app/(main)/goals/components/goal-form.tsx
@@ -0,0 +1,349 @@
+"use client";
+
+import { useState, useEffect, useCallback } from "react";
+import { useRouter } from "next/navigation";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import * as z from "zod";
+import { CalendarIcon } from "lucide-react";
+import { format } from "date-fns";
+
+import { Button } from "@/components/ui/button";
+import {
+  Form,
+  FormControl,
+  FormDescription,
+  FormField,
+  FormItem,
+  FormLabel,
+  FormMessage,
+} from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import { Card, CardContent } from "@/components/ui/card";
+import {
+  Select,
+  SelectContent,
+  SelectItem,
+  SelectTrigger,
+  SelectValue,
+} from "@/components/ui/select";
+import {
+  Popover,
+  PopoverContent,
+  PopoverTrigger,
+} from "@/components/ui/popover";
+import { Calendar } from "@/components/ui/calendar";
+import { useToast } from "@/components/ui/use-toast";
+import { api } from "@/lib/api";
+
+// Validation schema
+const formSchema = z.object({
+  name: z
+    .string()
+    .min(3, { message: "Name must be at least 3 characters" })
+    .max(100, { message: "Name must be less than 100 characters" }),
+  targetAmount: z
+    .number()
+    .min(1, { message: "Target amount must be greater than 0" }),
+  currentAmount: z
+    .number()
+    .min(0, { message: "Current amount cannot be negative" })
+    .optional(),
+  targetDate: z.date().optional(),
+  status: z.enum(["Active", "Paused", "Achieved", "Cancelled"]),
+});
+
+type FormValues = z.infer<typeof formSchema>;
+
+interface GoalFormProps {
+  goalId?: number;
+  isEditing?: boolean;
+  onSuccess?: () => void;
+}
+
+export function GoalForm({ 
+  goalId, 
+  isEditing = false,
+  onSuccess
+}: GoalFormProps) {
+  const [loading, setLoading] = useState(false);
+  const [initialLoading, setInitialLoading] = useState(false);
+  const router = useRouter();
+  const { toast } = useToast();
+
+  // Set up form with validation
+  const form = useForm<FormValues>({
+    resolver: zodResolver(formSchema),
+    defaultValues: {
+      name: "",
+      targetAmount: 0,
+      currentAmount: 0,
+      status: "Active",
+    },
+  });
+
+  const fetchGoalData = useCallback(async () => {
+    setInitialLoading(true);
+    try {
+      const response = await api.get(`/goals/${goalId}`);
+      const goalData = response.data;
+      
+      // Set form values
+      form.reset({
+        name: goalData.name,
+        targetAmount: goalData.targetAmount,
+        currentAmount: goalData.currentAmount,
+        status: goalData.status as "Active" | "Paused" | "Achieved" | "Cancelled",
+        ...(goalData.targetDate && { targetDate: new Date(goalData.targetDate) }),
+      });
+    } catch (error) {
+      toast({
+        title: "Error",
+        description: "Failed to fetch goal data. Please try again.",
+        variant: "destructive",
+      });
+      console.error("Error fetching goal:", error);
+      router.push("/goals");
+    } finally {
+      setInitialLoading(false);
+    }
+  }, [goalId, form, toast, router]);
+
+  // Fetch goal data if editing
+  useEffect(() => {
+    if (isEditing && goalId) {
+      fetchGoalData();
+    }
+  }, [isEditing, goalId, fetchGoalData]);
+
+  const onSubmit = async (values: FormValues) => {
+    try {
+      setLoading(true);
+      
+      // Format data for API
+      const formattedData = {
+        ...values,
+        targetDate: values.targetDate ? format(values.targetDate, "yyyy-MM-dd") : undefined,
+      };
+      
+      console.log("Submitting goal:", formattedData);
+      
+      if (isEditing) {
+        // Update existing goal
+        await api.put(`/goals/${goalId}`, formattedData);
+        toast({
+          title: "Goal updated",
+          description: "Your goal has been updated successfully.",
+        });
+      } else {
+        // Create new goal
+        const response = await api.post("/goals", formattedData);
+        console.log("Goal created response:", response.data);
+        toast({
+          title: "Goal created",
+          description: "Your new goal has been created successfully.",
+        });
+      }
+      
+      // Call onSuccess callback if provided
+      if (onSuccess) {
+        onSuccess();
+      } else {
+        // Force a full page reload directly to the goals page
+        window.location.href = "/goals";
+      }
+      
+    } catch (error) {
+      toast({
+        title: "Error",
+        description: `Failed to ${isEditing ? "update" : "create"} goal. Please try again.`,
+        variant: "destructive",
+      });
+      console.error(`Error ${isEditing ? "updating" : "creating"} goal:`, error);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  if (initialLoading) {
+    return <div className="text-center py-8">Loading goal data...</div>;
+  }
+
+  return (
+    <Card>
+      <CardContent className="pt-6">
+        <Form {...form}>
+          <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
+            <FormField
+              control={form.control}
+              name="name"
+              render={({ field }) => (
+                <FormItem>
+                  <FormLabel>Goal Name</FormLabel>
+                  <FormControl>
+                    <Input placeholder="e.g., Down Payment for House" {...field} />
+                  </FormControl>
+                  <FormDescription>
+                    A descriptive name for your financial goal
+                  </FormDescription>
+                  <FormMessage />
+                </FormItem>
+              )}
+            />
+
+            <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
+              <FormField
+                control={form.control}
+                name="targetAmount"
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel>Target Amount</FormLabel>
+                    <FormControl>
+                      <Input
+                        type="number"
+                        placeholder="10000"
+                        {...field}
+                        onChange={(e) => field.onChange(Number(e.target.value))}
+                      />
+                    </FormControl>
+                    <FormDescription>
+                      The total amount you want to save
+                    </FormDescription>
+                    <FormMessage />
+                  </FormItem>
+                )}
+              />
+
+              <FormField
+                control={form.control}
+                name="currentAmount"
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel>Current Amount</FormLabel>
+                    <FormControl>
+                      <Input
+                        type="number"
+                        placeholder="0"
+                        {...field}
+                        value={field.value || ""}
+                        onChange={(e) => field.onChange(Number(e.target.value) || 0)}
+                      />
+                    </FormControl>
+                    <FormDescription>
+                      How much you&apos;ve already saved towards this goal
+                    </FormDescription>
+                    <FormMessage />
+                  </FormItem>
+                )}
+              />
+            </div>
+
+            <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
+              <FormField
+                control={form.control}
+                name="targetDate"
+                render={({ field }) => (
+                  <FormItem className="flex flex-col">
+                    <FormLabel>Target Date (Optional)</FormLabel>
+                    <Popover>
+                      <PopoverTrigger asChild>
+                        <FormControl>
+                          <Button
+                            variant={"outline"}
+                            className={`w-full pl-3 text-left font-normal ${
+                              !field.value ? "text-muted-foreground" : ""
+                            }`}
+                          >
+                            {field.value ? (
+                              format(field.value, "PPP")
+                            ) : (
+                              <span>Pick a date</span>
+                            )}
+                            <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
+                          </Button>
+                        </FormControl>
+                      </PopoverTrigger>
+                      <PopoverContent className="w-auto p-0" align="start">
+                        <Calendar
+                          mode="single"
+                          selected={field.value || undefined}
+                          onSelect={field.onChange}
+                          disabled={(date) => date < new Date()}
+                          initialFocus
+                        />
+                      </PopoverContent>
+                    </Popover>
+                    <FormDescription>
+                      When you aim to achieve this goal
+                    </FormDescription>
+                    <FormMessage />
+                  </FormItem>
+                )}
+              />
+
+              <FormField
+                control={form.control}
+                name="status"
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel>Status</FormLabel>
+                    <Select
+                      onValueChange={field.onChange}
+                      defaultValue={field.value}
+                      value={field.value}
+                    >
+                      <FormControl>
+                        <SelectTrigger>
+                          <SelectValue placeholder="Select a status" />
+                        </SelectTrigger>
+                      </FormControl>
+                      <SelectContent>
+                        <SelectItem value="Active">Active</SelectItem>
+                        <SelectItem value="Paused">Paused</SelectItem>
+                        <SelectItem value="Achieved">Achieved</SelectItem>
+                        <SelectItem value="Cancelled">Cancelled</SelectItem>
+                      </SelectContent>
+                    </Select>
+                    <FormDescription>
+                      The current status of your goal
+                    </FormDescription>
+                    <FormMessage />
+                  </FormItem>
+                )}
+              />
+            </div>
+
+            <div className="mb-6">
+              <h3 className="text-sm font-medium text-muted-foreground mb-2">
+                Don&apos;t see the amount you need?
+              </h3>
+              <p className="text-sm">
+                Use the calculator to determine your target amount.
+              </p>
+            </div>
+
+            <div className="flex justify-end space-x-4">
+              <Button
+                type="button"
+                variant="outline"
+                onClick={() => router.push("/goals")}
+                disabled={loading}
+              >
+                Cancel
+              </Button>
+              <Button type="submit" disabled={loading}>
+                {loading
+                  ? isEditing
+                    ? "Updating..."
+                    : "Creating..."
+                  : isEditing
+                  ? "Update Goal"
+                  : "Create Goal"}
+              </Button>
+            </div>
+          </form>
+        </Form>
+      </CardContent>
+    </Card>
+  );
+} 
\ No newline at end of file
-- 
cgit v1.2.3-59-g8ed1b

