diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..fce8d56 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +node_modules +client/node_modules +client/build +*.db +.env +.git diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..3d528e4 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +PORT=5000 +JWT_SECRET=change-this-to-a-random-secret-string +NODE_ENV=development diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e3f76e5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +client/node_modules/ +client/build/ +.env +*.db +.DS_Store diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6dc26e5 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/SETUP.md b/SETUP.md new file mode 100644 index 0000000..2e200cd --- /dev/null +++ b/SETUP.md @@ -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: `admin@crm.local` +- 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 ` 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 | diff --git a/client/package.json b/client/package.json new file mode 100644 index 0000000..c4c4350 --- /dev/null +++ b/client/package.json @@ -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"] + } +} diff --git a/client/public/index.html b/client/public/index.html new file mode 100644 index 0000000..033e3cb --- /dev/null +++ b/client/public/index.html @@ -0,0 +1,27 @@ + + + + + + CRM Dashboard — Intui.travel BD + + + + + +
+ + diff --git a/client/src/App.js b/client/src/App.js new file mode 100644 index 0000000..eab11b0 --- /dev/null +++ b/client/src/App.js @@ -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
Loading...
; + if (!user) return ; + return children; +} + +function App() { + return ( + + + + } /> + }> + } /> + } /> + } /> + } /> + } /> + } /> + + + + + ); +} + +export default App; diff --git a/client/src/api.js b/client/src/api.js new file mode 100644 index 0000000..be02180 --- /dev/null +++ b/client/src/api.js @@ -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(); +} diff --git a/client/src/components/Layout.js b/client/src/components/Layout.js new file mode 100644 index 0000000..2b2b87b --- /dev/null +++ b/client/src/components/Layout.js @@ -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 = ( +
+
+

CRM Dashboard

+

Intui.travel BD

+
+ +
+
{user?.name}
+ +
+
+ ); + + return ( +
+ {/* Mobile overlay */} + {sidebarOpen && ( +
setSidebarOpen(false)} /> + )} + + {/* Sidebar */} + + + {/* Main content */} +
+
+ +

CRM Dashboard

+
+
+ +
+
+
+ ); +} diff --git a/client/src/components/LeadModal.js b/client/src/components/LeadModal.js new file mode 100644 index 0000000..7bb7369 --- /dev/null +++ b/client/src/components/LeadModal.js @@ -0,0 +1,132 @@ +import React, { useState, useEffect } from 'react'; + +const SOURCES = ['Google Maps', 'LinkedIn', 'Event', 'Referral', 'Email Outreach', 'Other']; +const STATUSES = ['New', 'Contacted', 'Replied', 'Registered', 'Activated', 'Rejected']; + +export default function LeadModal({ lead, onClose, onSave }) { + const [form, setForm] = useState({ + company_name: '', country: '', city: '', contact_person: '', email: '', + linkedin: '', source: 'Other', status: 'New', notes: '', + last_contact_date: '', next_followup_date: '', + }); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(''); + + useEffect(() => { + if (lead) { + setForm({ + company_name: lead.company_name || '', + country: lead.country || '', + city: lead.city || '', + contact_person: lead.contact_person || '', + email: lead.email || '', + linkedin: lead.linkedin || '', + source: lead.source || 'Other', + status: lead.status || 'New', + notes: lead.notes || '', + last_contact_date: lead.last_contact_date || '', + next_followup_date: lead.next_followup_date || '', + }); + } + }, [lead]); + + const set = (field) => (e) => setForm(prev => ({ ...prev, [field]: e.target.value })); + + async function handleSubmit(e) { + e.preventDefault(); + if (!form.company_name.trim()) { setError('Company name is required'); return; } + setSaving(true); + setError(''); + try { + await onSave(form); + onClose(); + } catch (err) { + setError(err.message); + } finally { + setSaving(false); + } + } + + return ( +
+
e.stopPropagation()}> +
+

{lead ? 'Edit Lead' : 'Add New Lead'}

+ +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +