Skip to content
Merged
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
30 changes: 30 additions & 0 deletions docs/drafts-dashboard-architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Drafts Dashboard & Expiry Scheduling Architecture

This document tracks the UI/UX redesign and backend refactoring to move public form expiration logic out of the template builders and into a dedicated Drafts management interface.

## 1. Objective
Provide recruiters with a cleaner template building experience by removing the "expiration dropdown" from the builder UI. Instead, saved templates remain strictly in a "Draft" status (un-shareable and inaccessible) until the recruiter goes to the Drafts Dashboard, selects a specific date/time calendar option, and activates the link.

## 2. Backend Updates (`server/`)

### Mongoose Models
- **`FormDraft.js`**: `expiresAt` is no longer required upon creation. When `expiresAt` is `null`, the draft is considered inactive. The `status` will now properly flow through `draft` -> `active` -> `expired`.

### API Endpoints
- **`POST /api/drafts`**: Simplified to only take `title` and `config`. Generates a Draft with a `null` expiration and `draft` status.
- **`GET /api/drafts`**: New endpoint that queries the DB for all `FormDraft` records where `recruiterId` matches the authenticated `req.user.uid`.
- **`PUT /api/drafts/:draftId/activate`**: New endpoint that takes an exact ISO `expiresAt` timestamp and updates the draft's status to `active`.
- **`GET /api/forms/:draftId` (Public)**: Updated to strictly enforce 410 (Gone) or 403 (Forbidden) if the draft's `expiresAt` is `null` (not yet activated) or if the current time exceeds `expiresAt`.

## 3. Frontend Updates (`src/`)

### Cleaned Template Builders
- Removed `isSaving`, `draftLink`, and `expiresInHours` state.
- Removed the public link preview box and copy buttons.
- The UI simply has a "Save as Draft" button which persists the toggles.

### Drafts Dashboard (`src/pages/DraftsView.jsx`)
- **Split-Pane Layout**: A modern white-and-grey UI featuring a list of templates on the right, and an active configuration screen on the left.
- **Draft List**: Displays the template title, creation date, and a pill-badge for status (`Draft`, `Active`, `Expired`).
- **Calendar UI**: Standard HTML5 `<input type="date">` and `<input type="time">` elements, elegantly styled to match the minimalist aesthetic.
- **Activation Flow**: Upon selecting a date and time, the UI hits the `PUT /api/drafts/:draftId/activate` endpoint, updates the state, and immediately renders the copyable public link.
65 changes: 65 additions & 0 deletions docs/feature-drafts-and-email.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Feature: Drafts, Temporary Links, & Email Automation

This document outlines the architecture, data models, and logic implemented to support the creation of form drafts, temporary public links, and automated email notifications in the Leddger AI platform.

## 1. Overview
The goal of this feature is to allow recruiters to configure a custom form template (e.g., Team Builder), save it as a "Draft" with a strict expiration timeline, and generate a temporary URL to share with end users. When users submit the form, the data is stored securely in MongoDB, and the recruiter receives an instant email notification via an automated OAuth2 Gmail integration.

## 2. Frontend Updates

### Sidebar Navigation (`src/App.jsx`)
- Restructured the sidebar navigation layout to group `Sent`, `Draft`, and `Schedule` items logically.
- Added dividers (`isDivider: true`) to distinctly section off these features from the core inbox items and team spaces, matching the new minimalist UI.

### Team Template Builder (`src/pages/TeamTemplateBuilder.jsx`)
- Introduced a **"Save & Generate Link"** control panel.
- Allows recruiters to set an expiration period (`24 Hours`, `3 Days`, `7 Days`).
- **Draft Creation Flow**:
1. Compiles the current UI configuration (`toggles`, `selectedDepartments`, `leadRestriction`, etc.) into a config object.
2. Sends an authenticated `POST` request to `/api/drafts` with the configuration and expiration time.
3. Receives a `draftId` and generates a public-facing URL: `/form/[Title]/[draftId]`.

### Public Form View (`src/pages/PublicFormView.jsx`)
- Created a brand new React route (`/form/:title/:draftId`) designed to be accessed without authentication.
- **Expiration Protection**: Automatically queries the backend to verify the link's validity. If the backend returns a `410 Gone` status, it strictly blocks access and renders a "Link Expired" warning.
- Dynamically generates form inputs (`Team Name`, `Department`, `Team Lead`, `Objective`) strictly based on what the recruiter originally configured in the Template Builder.

## 3. Backend Architecture (Node.js & MongoDB)

### Data Models
Two new Mongoose schemas were introduced to `server/models/`:

1. **`FormDraft.js`**
- Serves as the blueprint saved by the recruiter.
- `draftId`: Unique UUID.
- `recruiterId`: The Firebase UID of the creator.
- `config`: JSON object storing all toggle states.
- `expiresAt`: Date object defining the exact moment the link dies.
- `status`: String (`draft`, `active`, `expired`).

2. **`FormSubmission.js`**
- Captures the actual data submitted by the end user.
- `submissionId`: Unique UUID.
- `draftId`: Foreign key linking back to the `FormDraft`.
- `submittedData`: The JSON payload of what the user typed.

### API Endpoints (`server/index.js`)
- `POST /api/drafts` (Protected): Generates the expiration date, creates the UUID, and saves the `FormDraft` model to MongoDB.
- `GET /api/forms/:draftId` (Public): Validates the `draftId`. Performs a strict check: `if (new Date() > draft.expiresAt)`. If expired, it permanently updates the DB status to `expired` and rejects the request with a `410`.
- `POST /api/forms/:draftId/submit` (Public): Receives the payload, saves it as a `FormSubmission`, and triggers the automated email script in the background.

## 4. Gmail OAuth2 Automation

### Email Service (`server/utils/emailService.js`)
- Replaced generic SMTP configurations with a secure Google OAuth2 implementation utilizing `nodemailer` and `googleapis`.
- Rather than using an app password, the application dynamically generates short-lived access tokens using a persistent `GOOGLE_REFRESH_TOKEN`.
- **Workflow**:
1. The user submits the public form.
2. The backend fires `sendFormSubmissionEmail(title, submittedData)`.
3. `emailService.js` creates an OAuth2 transporter using the Client ID, Client Secret, and Refresh Token.
4. Formats the submitted JSON data into a clean HTML email and sends it to the configured `GOOGLE_EMAIL` address.

## 5. Security & Authentication Overhaul
- Upgraded the Firebase Admin SDK initialization in `server/middleware/auth.js`.
- Shifted from using a hardcoded `serviceAccountKey.json` file to securely parsing environment variables (`FIREBASE_PROJECT_ID`, `FIREBASE_CLIENT_EMAIL`, `FIREBASE_PRIVATE_KEY`).
- This change ensures that production secrets never accidentally leak into version control, satisfying enterprise-level security standards.
Binary file added public/Sucess.webm
Binary file not shown.
56 changes: 50 additions & 6 deletions server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,7 @@ app.post('/api/user/departments', verifyToken, async (req, res) => {
*/
app.post('/api/drafts', verifyToken, async (req, res) => {
try {
const { title, config, expiresInHours } = req.body;

// Calculate expiration date
const hours = parseInt(expiresInHours, 10) || 24;
const expiresAt = new Date(Date.now() + hours * 60 * 60 * 1000);
const { title, config } = req.body;

const draftId = uuidv4();

Expand All @@ -91,7 +87,8 @@ app.post('/api/drafts', verifyToken, async (req, res) => {
recruiterId: req.user.uid,
title,
config,
expiresAt
status: 'draft',
expiresAt: null
});

res.json({ message: 'Draft created', draftId: newDraft.draftId });
Expand All @@ -101,6 +98,49 @@ app.post('/api/drafts', verifyToken, async (req, res) => {
}
});

/**
* GET /api/drafts
* Fetch all form drafts for the logged in recruiter
*/
app.get('/api/drafts', verifyToken, async (req, res) => {
try {
const drafts = await FormDraft.find({ recruiterId: req.user.uid }).sort({ createdAt: -1 });
res.json({ drafts });
} catch (error) {
console.error('Error fetching drafts:', error);
res.status(500).json({ error: 'Failed to fetch drafts' });
}
});

/**
* PUT /api/drafts/:draftId/activate
* Activate a form draft by setting its expiration date
*/
app.put('/api/drafts/:draftId/activate', verifyToken, async (req, res) => {
try {
const { expiresAt } = req.body;

if (!expiresAt) {
return res.status(400).json({ error: 'expiresAt is required' });
}

const draft = await FormDraft.findOne({ draftId: req.params.draftId, recruiterId: req.user.uid });

if (!draft) {
return res.status(404).json({ error: 'Draft not found' });
}

draft.expiresAt = new Date(expiresAt);
draft.status = 'active';
await draft.save();
Comment on lines +121 to +135

res.json({ message: 'Draft activated', draft });
} catch (error) {
console.error('Error activating draft:', error);
res.status(500).json({ error: 'Failed to activate draft' });
}
});

/**
* GET /api/forms/:draftId
* Fetch public form config (no auth required)
Expand All @@ -113,6 +153,10 @@ app.get('/api/forms/:draftId', async (req, res) => {
return res.status(404).json({ error: 'Form not found' });
}

if (draft.status === 'draft' || !draft.expiresAt) {
return res.status(403).json({ error: 'This form link is not yet active.' });
}
Comment on lines +156 to +158

if (new Date() > draft.expiresAt || draft.status === 'expired') {
// Mark as expired in DB if not already
if (draft.status !== 'expired') {
Expand Down
4 changes: 2 additions & 2 deletions server/models/FormDraft.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ const FormDraftSchema = new mongoose.Schema({
},
expiresAt: {
type: Date,
required: true
default: null
},
status: {
type: String,
enum: ['draft', 'active', 'expired'],
default: 'active'
default: 'draft'
},
createdAt: {
type: Date,
Expand Down
19 changes: 17 additions & 2 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ const AnalyticsEngine = lazy(() => import('./pages/AnalyticsEngine.jsx'));
const Welcome = lazy(() => import('./pages/Welcome.jsx'));
const PrivacyPolicy = lazy(() => import('./pages/PrivacyPolicy.jsx'));
const TermsOfService = lazy(() => import('./pages/TermsOfService.jsx'));
const PublicFormView = lazy(() => import('./pages/PublicFormView.jsx'));
const DraftsView = lazy(() => import('./pages/DraftsView.jsx'));


// Pre-defined avatars from public sources
Expand Down Expand Up @@ -115,6 +117,8 @@ export default function App() {
'/dashboard/alerts': 'Alerts',
'/dashboard/settings': 'Settings',
'/dashboard/knowledge-base': 'Knowledge Base',
'/dashboard/templates/drafts': 'Drafts',
'/dashboard/templates/sent': 'Sent Forms'
};

const TAB_PATH_MAP = Object.fromEntries(
Expand All @@ -125,7 +129,7 @@ export default function App() {

const calculatePrimaryNav = (tab) => {
if (['Dashboard', 'Alerts'].includes(tab)) return 'Home';
if (['Student Template', 'Employee Template', 'Team Template'].includes(tab)) return 'Templates';
if (['Student Template', 'Employee Template', 'Team Template', 'Drafts', 'Sent Forms'].includes(tab)) return 'Templates';
if (['Projects', 'Teams', 'Sourcing', 'Calendar'].includes(tab)) return 'Workspace';
if (['Analysis', 'Reports', 'Export'].includes(tab)) return 'Analytics';
if (['Knowledge Base'].includes(tab)) return 'Intelligence';
Expand Down Expand Up @@ -182,7 +186,10 @@ export default function App() {
Templates: [
{ id: 'Student Template', label: 'Student', icon: GraduationCap },
{ id: 'Employee Template', label: 'Employee', icon: Briefcase },
{ id: 'Team Template', label: 'Team', icon: Users }
{ id: 'Team Template', label: 'Team', icon: Users },
{ id: 'dividerTemplates', isDivider: true },
{ id: 'Drafts', label: 'Drafts', icon: File },
{ id: 'Sent Forms', label: 'Sent', icon: Send }
],
Analytics: [
{ id: 'Analysis', label: 'Analysis', icon: BarChart3 },
Expand Down Expand Up @@ -1456,6 +1463,14 @@ export default function App() {
<EmployeeTemplateBuilder />
) : activeTab === 'Team Template' ? (
<TeamTemplateBuilder />
) : activeTab === 'Drafts' ? (
<DraftsView />
) : activeTab === 'Sent Forms' ? (
<div style={{ padding: '40px', color: '#64748B', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', gap: '16px' }}>
<Send size={48} style={{ color: '#CBD5E1' }} />
<h2>Sent Forms</h2>
<p>Responses and statuses for your sent forms will appear here.</p>
</div>
) : activeTab === 'Projects' ? (
<ProjectsView meetings={meetings} onUpdateMeetingProject={handleUpdateMeetingProject} />
) : activeTab === 'Teams' ? (
Expand Down
Loading