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
6 changes: 6 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules
client/node_modules
client/build
*.db
.env
.git
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
PORT=5000
JWT_SECRET=change-this-to-a-random-secret-string
NODE_ENV=development
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules/
client/node_modules/
client/build/
.env
*.db
.DS_Store
22 changes: 22 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
FROM node:20-alpine

WORKDIR /app

# Install backend dependencies
COPY package.json package-lock.json* ./
RUN npm install --production

# Install and build frontend
COPY client/package.json client/package-lock.json* ./client/
RUN cd client && npm install
COPY client/ ./client/
RUN cd client && npm run build

# Copy backend
COPY server/ ./server/

ENV NODE_ENV=production
ENV PORT=5000
EXPOSE 5000

CMD ["node", "server/index.js"]
139 changes: 139 additions & 0 deletions SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# CRM Dashboard — Setup & Deployment Guide

## Quick Start (Local Development)

### Prerequisites
- Node.js 18+
- npm

### 1. Install dependencies
```bash
npm run install-all
```

### 2. Configure environment
```bash
cp .env.example .env
# Edit .env and set a strong JWT_SECRET
```

### 3. Run development servers
```bash
npm run dev
```

This starts:
- Backend API on http://localhost:5000
- React dev server on http://localhost:3000 (proxied to backend)

### Default Login
- Email: `[email protected]`
- Password: `admin123`

> Change the default password after first login by registering a new account.

---

## Project Structure

```
├── server/
│ ├── index.js # Express entry point
│ ├── database.js # SQLite setup & schema
│ ├── middleware/
│ │ └── auth.js # JWT auth middleware
│ └── routes/
│ ├── auth.js # Login/register
│ ├── leads.js # Lead CRUD + follow-up + bulk ops
│ ├── kpi.js # Dashboard analytics
│ ├── targets.js # Monthly target management
│ └── csv.js # CSV import/export
├── client/
│ ├── public/index.html # Tailwind CSS via CDN
│ └── src/
│ ├── App.js # Router setup
│ ├── api.js # API helper (fetch + auth)
│ ├── context/
│ │ └── AuthContext.js # Auth state management
│ ├── components/
│ │ ├── Layout.js # Sidebar navigation
│ │ └── LeadModal.js # Add/edit lead form
│ └── pages/
│ ├── LoginPage.js
│ ├── DashboardPage.js
│ ├── LeadsPage.js
│ ├── PipelinePage.js
│ ├── FollowUpsPage.js
│ ├── TargetsPage.js
│ └── ImportExportPage.js
├── Dockerfile
├── .env.example
└── package.json
```

---

## Features

| Feature | Description |
|---------|-------------|
| **Lead Database** | Full table view with search, filter by status/source, sortable columns, pagination |
| **Pipeline (Kanban)** | Drag-and-drop cards between status columns |
| **KPI Dashboard** | Stats cards, conversion funnel, monthly progress vs targets |
| **Follow-up System** | Shows overdue/today follow-ups, mark done & schedule next |
| **Monthly Targets** | Set targets per month, track progress on dashboard |
| **CSV Import/Export** | Download leads as CSV, upload CSV to bulk-import |
| **Authentication** | Email + password login with JWT tokens |

---

## Deployment Options

### Option 1: Docker
```bash
docker build -t crm-dashboard .
docker run -p 5000:5000 -e JWT_SECRET=your-secret -v crm-data:/app crm-dashboard
```

### Option 2: Production Build
```bash
npm run build # Build React frontend
npm start # Serve everything from Express
```

### Option 3: Railway / Render / Fly.io
1. Push to a Git repo
2. Connect to your deployment platform
3. Set environment variables:
- `JWT_SECRET` — a random string
- `NODE_ENV` — `production`
- `PORT` — usually auto-assigned
4. Build command: `npm run install-all && npm run build`
5. Start command: `npm start`

---

## API Reference

All API routes require `Authorization: Bearer <token>` header (except auth endpoints).

| Method | Path | Description |
|--------|------|-------------|
| POST | `/api/auth/register` | Register new user |
| POST | `/api/auth/login` | Login |
| GET | `/api/auth/me` | Get current user |
| GET | `/api/leads` | List leads (with filters, sorting, pagination) |
| POST | `/api/leads` | Create lead |
| GET | `/api/leads/:id` | Get lead details + activity log |
| PUT | `/api/leads/:id` | Update lead |
| DELETE | `/api/leads/:id` | Delete lead |
| POST | `/api/leads/:id/followup` | Mark follow-up done |
| PATCH | `/api/leads/bulk-status` | Bulk status update |
| GET | `/api/kpi/overview` | Dashboard overview stats |
| GET | `/api/kpi/daily` | Daily stats (charts) |
| GET | `/api/kpi/funnel` | Conversion funnel |
| GET | `/api/kpi/monthly-progress` | Progress vs targets |
| GET | `/api/targets/:month` | Get monthly target |
| PUT | `/api/targets/:month` | Set monthly target |
| GET | `/api/csv/export` | Export leads as CSV |
| POST | `/api/csv/import` | Import leads from CSV |
20 changes: 20 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "crm-dashboard-client",
"version": "1.0.0",
"private": true,
"proxy": "http://localhost:5000",
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.28.0",
"react-scripts": "5.0.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build"
},
"browserslist": {
"production": [">0.2%", "not dead", "not op_mini all"],
"development": ["last 1 chrome version", "last 1 firefox version", "last 1 safari version"]
}
}
27 changes: 27 additions & 0 deletions client/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>CRM Dashboard — Intui.travel BD</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: { 50: '#eff6ff', 100: '#dbeafe', 200: '#bfdbfe', 300: '#93c5fd', 400: '#60a5fa', 500: '#3b82f6', 600: '#2563eb', 700: '#1d4ed8', 800: '#1e40af', 900: '#1e3a8a' },
}
}
}
}
</script>
<style>
.kanban-column { min-height: 400px; }
.drag-over { background-color: #eff6ff; border-color: #3b82f6; }
</style>
</head>
<body class="bg-gray-50">
<div id="root"></div>
</body>
</html>
40 changes: 40 additions & 0 deletions client/src/App.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { AuthProvider, useAuth } from './context/AuthContext';
import Layout from './components/Layout';
import LoginPage from './pages/LoginPage';
import DashboardPage from './pages/DashboardPage';
import LeadsPage from './pages/LeadsPage';
import PipelinePage from './pages/PipelinePage';
import FollowUpsPage from './pages/FollowUpsPage';
import ImportExportPage from './pages/ImportExportPage';
import TargetsPage from './pages/TargetsPage';

function ProtectedRoute({ children }) {
const { user, loading } = useAuth();
if (loading) return <div className="flex items-center justify-center h-screen"><div className="text-gray-500">Loading...</div></div>;
if (!user) return <Navigate to="/login" />;
return children;
}

function App() {
return (
<AuthProvider>
<BrowserRouter>
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/" element={<ProtectedRoute><Layout /></ProtectedRoute>}>
<Route index element={<DashboardPage />} />
<Route path="leads" element={<LeadsPage />} />
<Route path="pipeline" element={<PipelinePage />} />
<Route path="followups" element={<FollowUpsPage />} />
<Route path="import-export" element={<ImportExportPage />} />
<Route path="targets" element={<TargetsPage />} />
</Route>
</Routes>
</BrowserRouter>
</AuthProvider>
);
}

export default App;
43 changes: 43 additions & 0 deletions client/src/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
const API = process.env.REACT_APP_API_URL || '';

export async function apiFetch(path, options = {}) {
const token = localStorage.getItem('crm_token');
const headers = { 'Content-Type': 'application/json', ...options.headers };
if (token) headers['Authorization'] = `Bearer ${token}`;

const res = await fetch(`${API}${path}`, { ...options, headers });

if (res.status === 401) {
localStorage.removeItem('crm_token');
window.location.href = '/login';
throw new Error('Unauthorized');
}

if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body.error || `Request failed (${res.status})`);
}

if (res.headers.get('content-type')?.includes('text/csv')) {
return res.text();
}
return res.json();
}

export async function apiUpload(path, file) {
const token = localStorage.getItem('crm_token');
const formData = new FormData();
formData.append('file', file);

const res = await fetch(`${API}${path}`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
body: formData,
});

if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body.error || 'Upload failed');
}
return res.json();
}
82 changes: 82 additions & 0 deletions client/src/components/Layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React, { useState } from 'react';
import { Outlet, NavLink, useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';

const navItems = [
{ to: '/', label: 'Dashboard', icon: 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-4 0h4' },
{ to: '/leads', label: 'Leads', icon: 'M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z' },
{ to: '/pipeline', label: 'Pipeline', icon: 'M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2' },
{ to: '/followups', label: 'Follow-ups', icon: 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z' },
{ to: '/targets', label: 'Targets', icon: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z' },
{ to: '/import-export', label: 'CSV', icon: 'M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10' },
];

export default function Layout() {
const { user, logout } = useAuth();
const navigate = useNavigate();
const [sidebarOpen, setSidebarOpen] = useState(false);

const handleLogout = () => { logout(); navigate('/login'); };

const linkClass = ({ isActive }) =>
`flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-colors ${
isActive ? 'bg-primary-600 text-white' : 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'
}`;

const sidebar = (
<div className="flex flex-col h-full">
<div className="px-4 py-5 border-b border-gray-200">
<h1 className="text-lg font-bold text-gray-900">CRM Dashboard</h1>
<p className="text-xs text-gray-500 mt-0.5">Intui.travel BD</p>
</div>
<nav className="flex-1 px-3 py-4 space-y-1">
{navItems.map(item => (
<NavLink key={item.to} to={item.to} end={item.to === '/'} className={linkClass} onClick={() => setSidebarOpen(false)}>
<svg className="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={item.icon} />
</svg>
{item.label}
</NavLink>
))}
</nav>
<div className="px-3 py-4 border-t border-gray-200">
<div className="text-sm text-gray-700 font-medium mb-2 px-3">{user?.name}</div>
<button onClick={handleLogout} className="flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-red-600 hover:bg-red-50 w-full">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
Sign out
</button>
</div>
</div>
);

return (
<div className="flex h-screen">
{/* Mobile overlay */}
{sidebarOpen && (
<div className="fixed inset-0 bg-black/30 z-40 lg:hidden" onClick={() => setSidebarOpen(false)} />
)}

{/* Sidebar */}
<aside className={`fixed inset-y-0 left-0 z-50 w-64 bg-white border-r border-gray-200 transform transition-transform lg:relative lg:translate-x-0 ${sidebarOpen ? 'translate-x-0' : '-translate-x-full'}`}>
{sidebar}
</aside>

{/* Main content */}
<div className="flex-1 flex flex-col min-w-0">
<header className="bg-white border-b border-gray-200 px-4 py-3 flex items-center gap-4 lg:hidden">
<button onClick={() => setSidebarOpen(true)} className="p-1 rounded hover:bg-gray-100">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
<h1 className="text-lg font-bold text-gray-900">CRM Dashboard</h1>
</header>
<main className="flex-1 overflow-auto p-4 lg:p-6">
<Outlet />
</main>
</div>
</div>
);
}
Loading