Dashboard
The professional dashboard provides an overview of appointments, clients, and plan status, with a persistent sidebar for navigation.
Dashboard
The professional dashboard serves as the central hub for professionals using Odys, offering a consolidated view of their daily operations and key metrics. This page, primarily rendered by src/app/dashboard/page.tsx, is complemented by a persistent navigation sidebar defined in src/app/dashboard/sidebar.tsx, all wrapped within a layout component, src/app/dashboard/layout.tsx, which handles authentication and initial data loading.
Overview
The dashboard experience is structured around three core components:
src/app/dashboard/layout.tsx: This is the root layout for all routes under/dashboard. It's responsible for authenticating the user, fetching the professional's profile, and rendering theSidebarand the main content (children).src/app/dashboard/sidebar.tsx: A client-side component that provides persistent navigation across dashboard sub-routes. It displays the professional's name, current plan, and trial status, dynamically adjusting available navigation links based on the active plan.src/app/dashboard/page.tsx: The main dashboard content page, which is a server component. It fetches and displays today's and upcoming appointments, client counts, and revenue, along with various informational banners related to trial status and plan limits.
Together, these files create a cohesive and interactive environment for professionals to manage their practice within the Odys platform.
Dashboard Layout
The src/app/dashboard/layout.tsx file acts as the entry point for the entire dashboard section. As an async server component, its primary responsibility is to establish the user's authenticated context before rendering any child routes. It uses createClient from @/lib/supabase/server to interact with Supabase for authentication. If no user is found, it immediately redirects to /login, ensuring that only authenticated users can access the dashboard.
Once a user is authenticated, the layout proceeds to fetch the corresponding professional's profile from the professionals table using Drizzle ORM. This is a critical step, as the entire dashboard experience is tailored to the professional's specific data. If a professional profile does not exist for the authenticated user (e.g., a new user who hasn't completed onboarding), the layout redirects them to /onboarding. This ensures a smooth first-time user experience, guiding them to set up their profile.
The fetched professional data, including their name, slug, plan, and trialEndsAt, is then passed as props to the Sidebar component. This strategy centralizes the initial data fetching for the professional's identity, making it available to the navigation. Note that src/app/dashboard/page.tsx currently performs its own fetch for the professional, leading to redundant queries. The layout also includes PostHogIdentify, which is likely used for analytics, associating the user's ID with their activity within the dashboard. The main content of the dashboard, represented by {children}, is rendered within a scrollable container, allowing sub-routes to display their specific content.
Sidebar Navigation
The src/app/dashboard/sidebar.tsx component provides the primary navigation for the professional dashboard. Declared with "use client", it manages its own state for mobile responsiveness, allowing the sidebar to toggle between a hidden and visible drawer on smaller screens.
The sidebar dynamically renders navigation links based on the professional's active plan. A NAV array defines the structure of the navigation, including href, label, and an icon for each link. Crucially, some navigation items are marked with proOnly: true, indicating they are exclusive to paid plans. The trialDaysLeft and effectivePlan utility functions from @/lib/plan-guard are used to determine the professional's current plan status, including whether they are on a trial. If a proOnly feature is accessed by a user on the "free" plan (or a trial that has ended), the link is rendered as "locked" and redirects to the /dashboard/plans page, encouraging an upgrade.
The sidebar also displays the professional's initials, full name, and their current plan (e.g., "Free", "Pro", "Premium"), with distinct PLAN_COLOR and PLAN_LABEL mappings for visual clarity. A logout button, which uses createClient from @/lib/supabase/client to sign out the user and redirect them to /login, is also prominently featured. The component includes NotificationBell and ThemeToggle components, providing quick access to notifications and theme switching.
Dashboard Page Content
The src/app/dashboard/page.tsx component renders the main content area of the dashboard. As an async server component, it performs several data fetches to populate the dashboard with relevant information. It first authenticates the user and fetches the professional's profile, similar to the layout, redirecting if the user is not authenticated or lacks a professional profile.
The page then queries the database for:
- Today's Appointments: Using
getToday()to define the start and end of the current day, it fetches appointments for the logged-in professional, joining with theclientstable to display client names and phones. - Upcoming Appointments: It fetches up to 20 future appointments, ordered by
startsAt, providing a glimpse into the professional's schedule beyond the current day. - Total Clients: A count of all clients associated with the professional.
- Monthly Appointments: A count of appointments scheduled within the current month.
These fetched data points are used to display various informational banners and statistics:
WelcomeCard: Shown only whentotalClientsis 0, this card provides a first-run experience, guiding new professionals to share their public booking page.- Plan Limit Banners: The page calculates
isAtClientLimit,isNearClientLimit,isAtMonthlyLimit, andisNearMonthlyLimitbased on the professional'splanand thePLANSconfiguration from@/lib/stripe. These banners alert professionals when they are approaching or have reached their plan's limits for clients or monthly appointments, prompting them to upgrade. - Trial Banners: The page displays two types of trial-related banners. A
TrialBannercomponent is shown to free plan users who have not yet started a trial, encouraging them to opt-in. If the professional is on an active trial (trialEndsAtis set), a separate banner displays thedaysLeftcalculated bytrialDaysLeftfrom@/lib/plan-guard, encouraging subscription. - Key Statistics: A grid displays the number of appointments today, pending confirmations, and total clients.
- Appointment Lists: Both "Agenda de hoje" and "Próximos agendamentos" sections list appointments, showing time, client name, phone, and status. The
STATUS_LABELobject provides consistent styling and text for different appointment statuses. AppointmentActions: Forpending_confirmationorconfirmedappointments, this component allows professionals to change the appointment status directly from the dashboard.- Revenue: If
todayAppointmentsinclude anycapturedpayments, the totalprofessional.sessionPricefor those appointments is summed and displayed as "Receita de hoje" usingformatCurrency.
Utility functions like getToday, formatTime, and formatCurrency are defined locally to format dates, times, and monetary values for display.
Design decisions
The architecture of the dashboard reflects several deliberate design choices:
- Server Components for Initial Data: Both
src/app/dashboard/layout.tsxandsrc/app/dashboard/page.tsxare server components. This allows for direct, secure database access using Drizzle ORM and Supabase's server client (createClientfrom@/lib/supabase/server) without exposing database credentials to the client. Fetching data on the server reduces the amount of JavaScript shipped to the client and can improve initial page load performance. The trade-off is that subsequent data updates (e.g., after anAppointmentActionsinteraction) might require a full page refresh or a separate API call. - Centralized Professional Profile Fetching: The
professionalobject is fetched once insrc/app/dashboard/layout.tsxand passed to theSidebar. However,src/app/dashboard/page.tsxalso performs an identical database query for the professional's details, resulting in redundant database calls. This is noted as a potential improvement. - Client-Side Interactivity for Sidebar: The
src/app/dashboard/sidebar.tsxis a client component ("use client"). This choice enables interactive features like the mobile drawer state management and the client-side logout functionality. While it introduces client-side JavaScript, the benefits of a responsive and interactive navigation outweigh the minimal overhead for this specific component. - Modular UI Components: Components like
AppointmentActions,TrialBanner, andWelcomeCardare extracted into separate files. This promotes reusability, improves code organization, and makes the mainpage.tsxmore readable by abstracting away specific UI logic. - Drizzle ORM for Database Interactions: The use of Drizzle ORM (
@/lib/db) provides a type-safe and expressive way to interact with the PostgreSQL database. This reduces the likelihood of SQL injection vulnerabilities and type-related errors, enhancing developer experience and code maintainability. - Plan and Trial Logic Encapsulation: The
trialDaysLeftandeffectivePlanfunctions are placed in@/lib/plan-guard. This centralizes the business logic related to subscription plans and trials, ensuring consistent application across the dashboard (e.g., inpage.tsxfor banners andsidebar.tsxfor navigation locks).
Potential improvements
- Optimize Professional Data Fetching: The
professionaldata is fetched insrc/app/dashboard/layout.tsxto pass to theSidebar, butsrc/app/dashboard/page.tsxalso performs an identical database query to retrieve the professional's details. This results in redundant database calls. Theprofessionalobject could be fetched once in the layout and then passed down topage.tsxas a prop, or a React Context could be used to make the professional data available to all child components without re-fetching. - Centralize Appointment Status Labels: The
STATUS_LABELobject, which maps appointment statuses to their display text and Tailwind CSS classes, is currently defined directly withinsrc/app/dashboard/page.tsx. If other parts of the application (e.g., a dedicated appointments page, client view) also need to display appointment statuses, this object would likely be duplicated. MovingSTATUS_LABELto a shared utility file (e.g.,src/lib/constants/appointments.ts) would promote consistency and easier maintenance. - Client-Side Appointment Updates: When a professional uses the
AppointmentActionscomponent to change an appointment's status (e.g., confirm or reject), the current server component architecture means the page would typically need a full refresh to reflect the change. Implementing a client-side data revalidation strategy (e.g., usingrouter.refresh()from Next.js, or a data fetching library like SWR or React Query) after an action would provide a more fluid user experience by updating only the relevant parts of the UI without a full page reload.
References
src/app/dashboard/page.tsxsrc/app/dashboard/sidebar.tsxsrc/app/dashboard/layout.tsx