commit 230d5c89f0061dc7bb012eab514b96e3b6757d9e Author: Vishnu Date: Thu May 8 21:27:29 2025 +0530 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b93444e --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +node_modules +.pnp +.pnp.js + +# testing +coverage + +# next.js +.next/ +out/ +build +.swc/ + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# turbo +.turbo + +# ui +dist/ + +# env +*.env \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..ded82e2 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +auto-install-peers = true diff --git a/README.md b/README.md new file mode 100644 index 0000000..438763d --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# Turborepo Tailwind CSS starter + +This Turborepo starter is maintained by the Turborepo core team. + +## Using this example + +Run the following command: + +```sh +npx create-turbo@latest -e with-tailwind +``` + +## What's inside? + +This Turborepo includes the following packages/apps: + +### Apps and Packages + +- `docs`: a [Next.js](https://nextjs.org/) app with [Tailwind CSS](https://tailwindcss.com/) +- `web`: another [Next.js](https://nextjs.org/) app with [Tailwind CSS](https://tailwindcss.com/) +- `ui`: a stub React component library with [Tailwind CSS](https://tailwindcss.com/) shared by both `web` and `docs` applications +- `@repo/eslint-config`: `eslint` configurations (includes `eslint-config-next` and `eslint-config-prettier`) +- `@repo/typescript-config`: `tsconfig.json`s used throughout the monorepo + +Each package/app is 100% [TypeScript](https://www.typescriptlang.org/). + +### Building packages/ui + +This example is set up to produce compiled styles for `ui` components into the `dist` directory. The component `.tsx` files are consumed by the Next.js apps directly using `transpilePackages` in `next.config.ts`. This was chosen for several reasons: + +- Make sharing one `tailwind.config.ts` to apps and packages as easy as possible. +- Make package compilation simple by only depending on the Next.js Compiler and `tailwindcss`. +- Ensure Tailwind classes do not overwrite each other. The `ui` package uses a `ui-` prefix for it's classes. +- Maintain clear package export boundaries. + +Another option is to consume `packages/ui` directly from source without building. If using this option, you will need to update the `tailwind.config.ts` in your apps to be aware of your package locations, so it can find all usages of the `tailwindcss` class names for CSS compilation. + +For example, in [tailwind.config.ts](packages/tailwind-config/tailwind.config.ts): + +```js + content: [ + // app content + `src/**/*.{js,ts,jsx,tsx}`, + // include packages if not transpiling + "../../packages/ui/*.{js,ts,jsx,tsx}", + ], +``` + +If you choose this strategy, you can remove the `tailwindcss` and `autoprefixer` dependencies from the `ui` package. + +### Utilities + +This Turborepo has some additional tools already setup for you: + +- [Tailwind CSS](https://tailwindcss.com/) for styles +- [TypeScript](https://www.typescriptlang.org/) for static type checking +- [ESLint](https://eslint.org/) for code linting +- [Prettier](https://prettier.io) for code formatting diff --git a/apps/Frontend/.gitignore b/apps/Frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/apps/Frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/apps/Frontend/README.md b/apps/Frontend/README.md new file mode 100644 index 0000000..da98444 --- /dev/null +++ b/apps/Frontend/README.md @@ -0,0 +1,54 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default tseslint.config({ + extends: [ + // Remove ...tseslint.configs.recommended and replace with this + ...tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + ...tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + ...tseslint.configs.stylisticTypeChecked, + ], + languageOptions: { + // other options... + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + }, +}) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default tseslint.config({ + plugins: { + // Add the react-x and react-dom plugins + 'react-x': reactX, + 'react-dom': reactDom, + }, + rules: { + // other rules... + // Enable its recommended typescript rules + ...reactX.configs['recommended-typescript'].rules, + ...reactDom.configs.recommended.rules, + }, +}) +``` diff --git a/apps/Frontend/eslint.config.js b/apps/Frontend/eslint.config.js new file mode 100644 index 0000000..092408a --- /dev/null +++ b/apps/Frontend/eslint.config.js @@ -0,0 +1,28 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' + +export default tseslint.config( + { ignores: ['dist'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +) diff --git a/apps/Frontend/index.html b/apps/Frontend/index.html new file mode 100644 index 0000000..e4b78ea --- /dev/null +++ b/apps/Frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + TS + + +
+ + + diff --git a/apps/Frontend/package.json b/apps/Frontend/package.json new file mode 100644 index 0000000..d075fe5 --- /dev/null +++ b/apps/Frontend/package.json @@ -0,0 +1,94 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@repo/ui": "*", + "@repo/db": "*", + "@hookform/resolvers": "^3.10.0", + "@jridgewell/trace-mapping": "^0.3.25", + "@radix-ui/react-accordion": "^1.2.4", + "@radix-ui/react-alert-dialog": "^1.1.7", + "@radix-ui/react-aspect-ratio": "^1.1.3", + "@radix-ui/react-avatar": "^1.1.4", + "@radix-ui/react-checkbox": "^1.1.5", + "@radix-ui/react-collapsible": "^1.1.4", + "@radix-ui/react-context-menu": "^2.2.7", + "@radix-ui/react-dialog": "^1.1.7", + "@radix-ui/react-dropdown-menu": "^2.1.7", + "@radix-ui/react-hover-card": "^1.1.7", + "@radix-ui/react-label": "^2.1.3", + "@radix-ui/react-menubar": "^1.1.7", + "@radix-ui/react-navigation-menu": "^1.2.6", + "@radix-ui/react-popover": "^1.1.7", + "@radix-ui/react-progress": "^1.1.3", + "@radix-ui/react-radio-group": "^1.2.4", + "@radix-ui/react-scroll-area": "^1.2.4", + "@radix-ui/react-select": "^2.1.7", + "@radix-ui/react-separator": "^1.1.3", + "@radix-ui/react-slider": "^1.2.4", + "@radix-ui/react-slot": "^1.2.0", + "@radix-ui/react-switch": "^1.1.4", + "@radix-ui/react-tabs": "^1.1.4", + "@radix-ui/react-toast": "^1.2.7", + "@radix-ui/react-toggle": "^1.1.3", + "@radix-ui/react-toggle-group": "^1.1.3", + "@radix-ui/react-tooltip": "^1.2.0", + "@replit/vite-plugin-shadcn-theme-json": "^0.0.4", + "@tailwindcss/vite": "^4.1.3", + "@tanstack/react-query": "^5.60.5", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "connect-pg-simple": "^10.0.0", + "cross-env": "^7.0.3", + "date-fns": "^4.1.0", + "dotenv": "^16.5.0", + "embla-carousel-react": "^8.6.0", + "framer-motion": "^11.13.1", + "input-otp": "^1.4.2", + "lucide-react": "^0.453.0", + "memorystore": "^1.6.7", + "next-themes": "^0.4.6", + "passport": "^0.7.0", + "passport-local": "^1.0.0", + "react": "^19.1.0", + "react-contexify": "^6.0.0", + "react-day-picker": "^8.10.1", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", + "react-dom": "^19.1.0", + "react-hook-form": "^7.55.0", + "react-icons": "^5.4.0", + "react-resizable-panels": "^2.1.7", + "recharts": "^2.15.2", + "tailwind-merge": "^2.6.0", + "tailwindcss-animate": "^1.0.7", + "tw-animate-css": "^1.2.5", + "vaul": "^1.1.2", + "wouter": "^3.7.0", + "ws": "^8.18.0", + "zod": "^3.24.2", + "zod-validation-error": "^3.4.0" + }, + "devDependencies": { + "@eslint/js": "^9.25.0", + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.2", + "@vitejs/plugin-react": "^4.4.1", + "eslint": "^9.25.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^16.0.0", + "typescript": "~5.8.3", + "typescript-eslint": "^8.30.1", + "vite": "^6.3.5" + } +} diff --git a/apps/Frontend/src/App.tsx b/apps/Frontend/src/App.tsx new file mode 100644 index 0000000..7ba67ab --- /dev/null +++ b/apps/Frontend/src/App.tsx @@ -0,0 +1,39 @@ +import { Switch, Route } from "wouter"; +import { queryClient } from "./lib/queryClient"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { Toaster } from "./components/ui/toaster"; +import { TooltipProvider } from "./components/ui/tooltip"; +import NotFound from "./pages/not-found"; +import Dashboard from "./pages/dashboard"; +import AuthPage from "./pages/auth-page"; +import AppointmentsPage from "./pages/appointments-page"; +import PatientsPage from "./pages/patients-page"; +import { ProtectedRoute } from "./lib/protected-route"; +import { AuthProvider } from "./hooks/use-auth"; + +function Router() { + return ( + + + + + + + + ); +} + +function App() { + return ( + + + + + + + + + ); +} + +export default App; diff --git a/apps/Frontend/src/components/appointments/add-appointment-modal.tsx b/apps/Frontend/src/components/appointments/add-appointment-modal.tsx new file mode 100644 index 0000000..1d1bd74 --- /dev/null +++ b/apps/Frontend/src/components/appointments/add-appointment-modal.tsx @@ -0,0 +1,45 @@ +import { useState } from "react"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { AppointmentForm } from "./appointment-form"; +import { Appointment, InsertAppointment, UpdateAppointment, Patient } from "@shared/schema"; + +interface AddAppointmentModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onSubmit: (data: InsertAppointment | UpdateAppointment) => void; + isLoading: boolean; + appointment?: Appointment; + patients: Patient[]; +} + +export function AddAppointmentModal({ + open, + onOpenChange, + onSubmit, + isLoading, + appointment, + patients, +}: AddAppointmentModalProps) { + return ( + + + + + {appointment ? "Edit Appointment" : "Add New Appointment"} + + +
+ { + onSubmit(data); + onOpenChange(false); + }} + isLoading={isLoading} + /> +
+
+
+ ); +} \ No newline at end of file diff --git a/apps/Frontend/src/components/appointments/appointment-form.tsx b/apps/Frontend/src/components/appointments/appointment-form.tsx new file mode 100644 index 0000000..7d6cc6e --- /dev/null +++ b/apps/Frontend/src/components/appointments/appointment-form.tsx @@ -0,0 +1,462 @@ +import { useState, useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { format } from "date-fns"; +import { InsertAppointment, UpdateAppointment, Appointment, Patient } from "@shared/schema"; + +// Define staff members (should match those in appointments-page.tsx) +const staffMembers = [ + { id: "doctor1", name: "Dr. Kai Gao", role: "doctor" }, + { id: "doctor2", name: "Dr. Jane Smith", role: "doctor" }, + { id: "hygienist1", name: "Hygienist One", role: "hygienist" }, + { id: "hygienist2", name: "Hygienist Two", role: "hygienist" }, + { id: "hygienist3", name: "Hygienist Three", role: "hygienist" }, +]; +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Calendar } from "@/components/ui/calendar"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { CalendarIcon, Clock } from "lucide-react"; +import { cn } from "@/lib/utils"; + +// Create a schema for appointment validation +const appointmentSchema = z.object({ + patientId: z.coerce.number().positive(), + title: z.string().optional(), + date: z.date({ + required_error: "Appointment date is required", + }), + startTime: z.string().regex(/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/, { + message: "Start time must be in format HH:MM", + }), + endTime: z.string().regex(/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/, { + message: "End time must be in format HH:MM", + }), + type: z.string().min(1, "Appointment type is required"), + notes: z.string().optional(), + status: z.string().default("scheduled"), + staff: z.string().default(staffMembers[0].id), +}); + +export type AppointmentFormValues = z.infer; + +interface AppointmentFormProps { + appointment?: Appointment; + patients: Patient[]; + onSubmit: (data: InsertAppointment | UpdateAppointment) => void; + isLoading?: boolean; +} + +export function AppointmentForm({ + appointment, + patients, + onSubmit, + isLoading = false +}: AppointmentFormProps) { + // Get the stored data from session storage + const storedDataString = sessionStorage.getItem('newAppointmentData'); + let parsedStoredData = null; + + // Try to parse it if it exists + if (storedDataString) { + try { + parsedStoredData = JSON.parse(storedDataString); + console.log('Initial appointment data from storage:', parsedStoredData); + + // LOG the specific time values for debugging + console.log('Time values in stored data:', { + startTime: parsedStoredData.startTime, + endTime: parsedStoredData.endTime + }); + } catch (error) { + console.error('Error parsing stored appointment data:', error); + } + } + + // Format the date and times for the form + const defaultValues: Partial = appointment + ? { + patientId: appointment.patientId, + title: appointment.title, + date: new Date(appointment.date), + startTime: appointment.startTime.slice(0, 5), // HH:MM from HH:MM:SS + endTime: appointment.endTime.slice(0, 5), // HH:MM from HH:MM:SS + type: appointment.type, + notes: appointment.notes || "", + status: appointment.status || "scheduled", + } + : parsedStoredData + ? { + patientId: parsedStoredData.patientId, + date: new Date(parsedStoredData.date), + title: parsedStoredData.title || "", + startTime: parsedStoredData.startTime, // This should now be correctly applied + endTime: parsedStoredData.endTime, + type: parsedStoredData.type || "checkup", + status: parsedStoredData.status || "scheduled", + notes: parsedStoredData.notes || "", + staff: parsedStoredData.staff || staffMembers[0].id, + } + : { + date: new Date(), + title: "", + startTime: "09:00", + endTime: "09:30", + type: "checkup", + status: "scheduled", + staff: "doctor1", + }; + + const form = useForm({ + resolver: zodResolver(appointmentSchema), + defaultValues, + }); + + // Force form field values to update and clean up storage + useEffect(() => { + if (parsedStoredData) { + // Force-update the form with the stored values + console.log("Force updating form fields with:", parsedStoredData); + + // Update form field values directly + if (parsedStoredData.startTime) { + form.setValue('startTime', parsedStoredData.startTime); + console.log(`Setting startTime to: ${parsedStoredData.startTime}`); + } + + if (parsedStoredData.endTime) { + form.setValue('endTime', parsedStoredData.endTime); + console.log(`Setting endTime to: ${parsedStoredData.endTime}`); + } + + if (parsedStoredData.staff) { + form.setValue('staff', parsedStoredData.staff); + } + + if (parsedStoredData.date) { + form.setValue('date', new Date(parsedStoredData.date)); + } + + // Clean up session storage + sessionStorage.removeItem('newAppointmentData'); + } + }, [form]); + + const handleSubmit = (data: AppointmentFormValues) => { + // Convert date to string format for the API and ensure patientId is properly parsed as a number + console.log("Form data before submission:", data); + + // Make sure patientId is a number + const patientId = typeof data.patientId === 'string' + ? parseInt(data.patientId, 10) + : data.patientId; + + // Get patient name for the title + const patient = patients.find(p => p.id === patientId); + const patientName = patient ? `${patient.firstName} ${patient.lastName}` : 'Patient'; + + // Auto-create title if it's empty + let title = data.title; + if (!title || title.trim() === '') { + // Format: "April 19" - just the date + title = format(data.date, 'MMMM d'); + } + + // Make sure notes include staff information (needed for appointment display in columns) + let notes = data.notes || ''; + + // Get the selected staff member + const selectedStaff = staffMembers.find(staff => staff.id === data.staff) || staffMembers[0]; + + // If there's no staff information in the notes, add it + if (!notes.includes('Appointment with')) { + notes = notes ? `${notes}\nAppointment with ${selectedStaff.name}` : `Appointment with ${selectedStaff.name}`; + } + + onSubmit({ + ...data, + title, + notes, + patientId, // Ensure patientId is a number + date: format(data.date, 'yyyy-MM-dd'), + }); + }; + + return ( +
+ + ( + + Patient + + + + )} + /> + + ( + + Appointment Title (optional) + + + + + + )} + /> + + ( + + Date + + + + + + + + + date < new Date(new Date().setHours(0, 0, 0, 0)) + } + initialFocus + /> + + + + + )} + /> + +
+ ( + + Start Time + +
+ + +
+
+ +
+ )} + /> + + ( + + End Time + +
+ + +
+
+ +
+ )} + /> +
+ + ( + + Appointment Type + + + + )} + /> + + ( + + Status + + + + )} + /> + + ( + + Doctor/Hygienist + + + + )} + /> + + ( + + Notes + +