diff --git a/apps/frontend/next.config.ts b/apps/frontend/next.config.ts index e9ffa30..45d300b 100644 --- a/apps/frontend/next.config.ts +++ b/apps/frontend/next.config.ts @@ -1,7 +1,30 @@ -import type { NextConfig } from "next"; +import type { NextConfig } from 'next'; const nextConfig: NextConfig = { - /* config options here */ + async rewrites() { + return [ + { + source: '/auth/:path*', + destination: 'http://localhost:3006/auth/:path*', + }, + { + source: '/expenditures/:path*', + destination: 'http://localhost:3004/expenditures/:path*', + }, + { + source: '/expenditures', + destination: 'http://localhost:3004/expenditures', + }, + { + source: '/projects/:path*', + destination: 'http://localhost:3002/projects/:path*', + }, + { + source: '/projects', + destination: 'http://localhost:3002/projects', + }, + ]; + }, }; export default nextConfig; diff --git a/apps/frontend/src/app/components/AddExpenseModal.tsx b/apps/frontend/src/app/components/AddExpenseModal.tsx new file mode 100644 index 0000000..efb9fb9 --- /dev/null +++ b/apps/frontend/src/app/components/AddExpenseModal.tsx @@ -0,0 +1,297 @@ +'use client'; + +import { useState } from 'react'; +import { Button, Dialog, Portal, CloseButton, Stack } from '@chakra-ui/react'; +import DropdownSelector from './DropdownSelector'; +import { apiFetch } from '@/lib/api'; + +interface Project { + project_id: number; + name: string; +} + +interface AddExpenseModalProps { + open: boolean; + onClose: () => void; + onSuccess: () => void; + token: string; + categories: string[]; + projects: Project[]; +} + +export default function AddExpenseModal({ + open, + onClose, + onSuccess, + token, + categories, + projects, +}: AddExpenseModalProps) { + const [newDate, setNewDate] = useState(''); + const [newType, setNewType] = useState(''); + const [newDescription, setNewDescription] = useState(''); + const [newAmount, setNewAmount] = useState(''); + const [newProject, setNewProject] = useState(''); + + const [dateError, setDateError] = useState(false); + const [typeError, setTypeError] = useState(false); + const [descError, setDescError] = useState(false); + const [amountError, setAmountError] = useState(false); + const [projectError, setProjectError] = useState(false); + const [submitError, setSubmitError] = useState(null); + + function resetForm() { + setNewDate(''); + setNewType(''); + setNewDescription(''); + setNewAmount(''); + setNewProject(''); + setDateError(false); + setTypeError(false); + setDescError(false); + setAmountError(false); + setProjectError(false); + setSubmitError(null); + } + + function handleClose() { + resetForm(); + onClose(); + } + + async function handleSubmit() { + const hasDateError = !newDate.trim(); + const hasTypeError = !newType.trim(); + const hasDescError = !newDescription.trim(); + const hasAmountError = !newAmount.trim() || isNaN(Number(newAmount)) || Number(newAmount) < 0; + const hasProjectError = !newProject.trim(); + + setDateError(hasDateError); + setTypeError(hasTypeError); + setDescError(hasDescError); + setAmountError(hasAmountError); + setProjectError(hasProjectError); + + if (hasDateError || hasTypeError || hasDescError || hasAmountError || hasProjectError) return; + + const selectedProject = projects.find((p) => p.name === newProject); + if (!selectedProject) { + setProjectError(true); + return; + } + + try { + await apiFetch('/expenditures', { + method: 'POST', + token, + body: JSON.stringify({ + projectID: selectedProject.project_id, + amount: Number(newAmount), + category: newType, + description: newDescription, + spentOn: newDate, + }), + }); + + resetForm(); + onSuccess(); + } catch (err) { + setSubmitError(err instanceof Error ? err.message : 'Failed to create expense'); + } + } + + return ( + { if (!e.open) handleClose(); }}> + + + + + + + Add New Expense + + + + + + {/* Date */} +
+ + { + setNewDate(e.target.value); + setDateError(false); + }} + style={{ + border: `1px solid ${dateError ? 'var(--color-error-red)' : '#CBD5E0'}`, + borderRadius: '6px', + padding: '8px 12px', + fontSize: '14px', + outline: 'none', + width: '100%', + fontFamily: 'inherit', + color: newDate ? 'inherit' : '#A0AEC0', + }} + /> + {dateError && ( + + Select a date + + )} +
+ + {/* Type of Expense */} +
+ + { + setNewType(val as string); + setTypeError(false); + }} + /> + {typeError && ( + + Select a type of expense + + )} +
+ + {/* Description */} +
+ +