Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions IMPLEMENTATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# The Implementation

## Main Dashboard Content

The main dashboard content is organized within a dedicated folder in the `features` directory. Each dashboard feature is treated as a self-contained unit, consisting of:

* A UI component (`.tsx`) responsible for rendering the card
* A corresponding data or service file (`.ts`) responsible for retrieving and updating data

This separation allows each card to be developed, maintained, and iterated on independently, which supports scalability as more dashboard features are introduced.

Each content card communicates with its own API route, exposing clearly defined methods (such as GET, POST, or PUT). From a UI standpoint, this ensures that each card only requests the data it needs at runtime, rather than relying on a single dashboard request.

Ideally, a WebSocket connection would be established per authenticated user. This would enable real-time updates across the dashboard, which is particularly important in an organizational context where multiple users may be interacting with the same data simultaneously (for example, shared tasks or urgent updates).

---

## The Urgent Funnel Updates Card

The Urgent Funnel Updates card is designed to surface time-sensitive items that require immediate attention.

* The card is connected to a **GET request** that retrieves updates sorted by proximity to their deadline. From a UI perspective, this allows the most critical items to be visually prioritized at the top of the card.
* Each update includes contextual metadata (such as due date, responsible person, and status), which informs how the item is styled (e.g., warning states or emphasis).

The **Reminder** button triggers a **POST request** that sends a notification or reminder to the appropriate individual. This interaction is intentionally lightweight: the user takes a single action, receives immediate visual feedback (such as a success state or confirmation toast), and the system handles the delivery logic in the background.

---

## The Daily Agenda Card

The Daily Agenda card functions as a focused, personal task list tied to individual users and their organizations.

* The card supports **GET, POST, and PUT** interactions.
* On initial load, a GET request retrieves any tasks that are not yet completed, ensuring the user sees only relevant, actionable items.

Conceptually, the underlying query filters by user and completion state, for example:

```sql
SELECT * FROM todo
WHERE user_id = user_id
AND NOT is_completed;
```

From a UI standpoint:

* Incomplete tasks are displayed as active items.
* Completed tasks are either hidden or visually de-emphasized to reduce clutter.

The **Create Task** button enables users to add tasks directly from the dashboard. This action maps to a POST request, allowing users to remain in context rather than navigating to a separate task management page.

When a task is marked as completed, a PUT request updates the task’s status. The UI immediately reflects this change, while the update is persisted in the background.

---

## The Calendar Card

The Calendar card is intended to provide time-based context and coordination.

One approach is to integrate a third-party API such as **Google Calendar**, which provides built-in methods for creating, updating, and sharing events. From a UI design perspective, this reduces implementation complexity while offering familiar interactions to users.

This integration also enables:

* Assigning events or tasks to other team members
* Visualizing shared schedules in collaborative environments
* Maintaining consistency across tools users may already rely on

The calendar card acts as a coordination surface rather than a full calendar replacement.

---

## Metrics and Analysis

The Metrics and Analysis section at the bottom of the dashboard is designed as a **read-only, insight-focused area**.

* These cards are powered by internally run database queries that execute at predefined intervals, ensuring consistent performance and avoiding unnecessary computation during dashboard load.
* Users can optionally configure which metrics are visible, allowing the dashboard to adapt to different business priorities or roles.

The raw data is transformed into visual representations using a JavaScript charting library such as **Chart.js**, enabling:

* Trend visualization
* Quick comparison of key performance indicators (KPIs)
* At-a-glance comprehension without requiring users to interpret raw numbers

From a UI standpoint, these cards emphasize clarity, consistency, and visual hierarchy.

---

## Database Code (High-Level)

At a high level, the database is structured around feature-specific schemas, with records consistently associated with both an **organization ID** and a **user ID** as foreign keys.

Each request includes identifying information (such as organization and user context), allowing the system to fetch only the data relevant to the current user and their organization. This ensures:

* Proper data isolation between organizations
* Accurate personalization of dashboard content

Using Supabase as the backend service simplifies this architecture by combining database access, authentication, and API handling into a unified platform, allowing frontend components to interact with data in a predictable and secure manner.

---

## Data Flows

The dashboard supports two primary data flow patterns: **read** and **write**.

### Read Flow

* Data is fetched in the background as the dashboard loads.
* Loading states and placeholders are displayed to maintain a smooth user experience.
* Once retrieved, data is cached at the component level to avoid unnecessary refetching.

### Write Flow

* User actions (such as creating tasks or sending reminders) trigger POST or PUT requests through their respective API routes.
* The UI provides immediate feedback based on success or failure responses.
* Updates are reflected visually without requiring a full page refresh.

For collaborative scenarios, a WebSocket-based approach would allow changes made by one user to propagate in real time to other users within the same organization, reinforcing a sense of shared workspace and reducing the need for manual refreshes.

48 changes: 48 additions & 0 deletions app/features/ai-sidebar/ai-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useState } from "react";

import { cn } from "~/lib/utils";
import { AiSidebarBubble } from "./bubble/ai-sidebar-bubble";
import type { AiAssistantChatMessage } from "./chat/ai-sidebar-chat";
import { AiSidebarChat } from "./chat/ai-sidebar-chat";
import { AiSidebarContextualActions } from "./contextual-actions/ai-sidebar-contextual-actions";

export type { AiAssistantChatMessage };

export type AiSidebarProps = {
chatMessages: AiAssistantChatMessage[];
};

export function AiSidebar({ chatMessages }: AiSidebarProps) {
const [isCollapsed, setIsCollapsed] = useState(false);

const toggleCollapse = () => {
setIsCollapsed(!isCollapsed);
};

return (
<>
{/* Floating button when collapsed */}
{isCollapsed && <AiSidebarBubble onOpen={toggleCollapse} />}

{/* Sidebar container - always in layout but hidden when collapsed */}
<div
className={cn(
"bg-background border-l flex flex-col transition-all duration-200 ease-linear overflow-hidden",
"self-stretch",
isCollapsed ? "w-0 border-0" : "w-80",
)}
>
{!isCollapsed && (
<>
<AiSidebarChat
chatMessages={chatMessages}
onCollapse={toggleCollapse}
/>
<AiSidebarContextualActions />
</>
)}
</div>
</>
);
}

23 changes: 23 additions & 0 deletions app/features/ai-sidebar/bubble/ai-sidebar-bubble.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Bot } from "lucide-react";

import { Button } from "~/components/ui/button";

type AiSidebarBubbleProps = {
onOpen: () => void;
};

export function AiSidebarBubble({ onOpen }: AiSidebarBubbleProps) {
return (
<div className="fixed bottom-6 right-6 z-50">
<Button
onClick={onOpen}
size="icon"
className="h-14 w-14 rounded-full shadow-lg bg-primary hover:bg-primary/90"
aria-label="Open AI Assistant"
>
<Bot className="size-6" />
</Button>
</div>
);
}

92 changes: 92 additions & 0 deletions app/features/ai-sidebar/chat/ai-sidebar-chat.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { Bot, Minimize2 } from "lucide-react";

import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import { Separator } from "~/components/ui/separator";

export type AiAssistantChatMessage = {
id: number;
type: "ai" | "user";
message: string;
};

type AiSidebarChatProps = {
chatMessages: AiAssistantChatMessage[];
onCollapse: () => void;
};

export function AiSidebarChat({
chatMessages,
onCollapse,
}: AiSidebarChatProps) {
return (
<>
{/* Header */}
<div className="flex items-center justify-between p-4 border-b">
<h2 className="text-lg font-semibold">AI Assistant</h2>
<Button
variant="ghost"
size="icon"
onClick={onCollapse}
className="h-8 w-8"
aria-label="Collapse AI Assistant"
>
<Minimize2 className="size-4" />
</Button>
</div>

{/* Chat Content */}
<div className="flex-1 overflow-y-auto px-4 pt-8 pb-4 space-y-4">
{chatMessages.map((message) => {
if (message.type === "user") {
return (
<div key={message.id} className="flex gap-3 justify-end">
<div className="flex-1 flex justify-end">
<div className="bg-primary text-primary-foreground rounded-lg p-3 max-w-[80%]">
<p className="text-sm">{message.message}</p>
</div>
</div>
</div>
);
}

return (
<div key={message.id} className="flex gap-3">
<div className="flex-shrink-0">
<div className="h-8 w-8 rounded-full bg-primary/10 flex items-center justify-center">
<Bot className="size-4 text-primary" />
</div>
</div>
<div className="flex-1">
<div className="bg-muted rounded-lg p-3">
<p className="text-sm">{message.message}</p>
</div>
</div>
</div>
);
})}
</div>

<Separator />

{/* Input Area */}
<div className="p-4">
<div className="flex gap-2">
<Input
placeholder="Ask me anything..."
className="flex-1"
disabled
/>
<Button
size="default"
className="bg-foreground/80 text-background"
disabled
>
Send
</Button>
</div>
</div>
</>
);
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Calendar, FileText, Mail, UserPlus } from "lucide-react";

import { Button } from "~/components/ui/button";

export function AiSidebarContextualActions() {
return (
<div className="p-4 pt-0 space-y-2">
<p className="text-xs font-medium text-muted-foreground">
Contextual Actions:
</p>
<div className="grid grid-cols-1 gap-2">
<Button
variant="outline"
size="sm"
className="justify-start gap-2 h-auto py-2"
disabled
>
<Calendar className="size-4" />
<span className="text-xs">Schedule Interview</span>
</Button>
<Button
variant="outline"
size="sm"
className="justify-start gap-2 h-auto py-2"
disabled
>
<FileText className="size-4" />
<span className="text-xs">Summarize Candidate</span>
</Button>
<Button
variant="outline"
size="sm"
className="justify-start gap-2 h-auto py-2"
disabled
>
<Mail className="size-4" />
<span className="text-xs">Send To Marketplace</span>
</Button>
<Button
variant="outline"
size="sm"
className="justify-start gap-2 h-auto py-2"
disabled
>
<UserPlus className="size-4" />
<span className="text-xs">Move to Next Stage</span>
</Button>
</div>
</div>
);
}

Loading