diff --git a/src/components/DashboardOverview.tsx b/src/components/DashboardOverview.tsx
index eb35779..601e9bc 100644
--- a/src/components/DashboardOverview.tsx
+++ b/src/components/DashboardOverview.tsx
@@ -161,7 +161,7 @@ export default function DashboardOverview() {
];
const pipelineChartData = {
- labels: ['Lead', 'Qualified', 'Potential', 'Demo', 'Won'],
+ labels: ['Lead', 'Qualified', 'Potential', 'SALES'],
datasets: [
{
label: 'Deals',
@@ -456,7 +456,7 @@ export default function DashboardOverview() {
diff --git a/src/components/ExpenseApproval.tsx b/src/components/ExpenseApproval.tsx
index 9eb3632..9ef0e17 100644
--- a/src/components/ExpenseApproval.tsx
+++ b/src/components/ExpenseApproval.tsx
@@ -3,6 +3,7 @@
import React, { useEffect, useState } from 'react';
import api from '../lib/axios';
import { format } from 'date-fns';
+import { Eye, Check, X } from 'lucide-react';
interface Expense {
id: string;
@@ -20,6 +21,8 @@ export default function ExpenseApproval() {
const [expenses, setExpenses] = useState([]);
const [loading, setLoading] = useState(true);
+ const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
+
useEffect(() => {
fetchExpenses();
}, []);
@@ -57,19 +60,35 @@ export default function ExpenseApproval() {
| User |
Description |
Amount |
+ Bill |
Status |
Actions |
- {loading ? | Loading... |
:
- expenses.length === 0 ? | No expenses found |
:
+ {loading ? | Loading... |
:
+ expenses.length === 0 ? | No expenses found |
:
expenses.map(expense => (
| {format(new Date(expense.createdAt), 'MMM dd, yyyy')} |
{expense.user.name} |
{expense.description} |
₹{expense.amount} |
+
+ {expense.imageUrl ? (
+
+
+ View Bill
+
+ ) : (
+ No Bill
+ )}
+ |
{expense.status === 'PENDING' && (
<>
-
-
+
+
>
)}
|
diff --git a/src/components/FloatingEventButton.tsx b/src/components/FloatingEventButton.tsx
new file mode 100644
index 0000000..f24d25b
--- /dev/null
+++ b/src/components/FloatingEventButton.tsx
@@ -0,0 +1,266 @@
+'use client';
+
+import React, { useState, useEffect } from 'react';
+import { Plus, X, Phone, MessageCircle, Calendar, CheckCircle2, FileSearch, Send, MapPin, ClipboardCheck, Handshake, ListTodo, User, Building2, Clock } from 'lucide-react';
+import api from '../lib/axios';
+import { useAuth } from '@/context/AuthContext';
+import clsx from 'clsx';
+
+export default function FloatingEventButton() {
+ const { user } = useAuth();
+ const [isOpen, setIsOpen] = useState(false);
+ const [clients, setClients] = useState([]);
+ const [users, setUsers] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [formData, setFormData] = useState({
+ type: 'CALL',
+ clientId: '',
+ userId: user?.id || '',
+ notes: '',
+ stage: 'LEAD',
+ date: new Date().toISOString().split('T')[0],
+ time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false }),
+ });
+
+ useEffect(() => {
+ if (isOpen) {
+ if (clients.length === 0) fetchClients();
+ if (users.length === 0) fetchUsers();
+ }
+ }, [isOpen]);
+
+ const fetchClients = async () => {
+ setLoading(true);
+ try {
+ const res = await api.get('/clients');
+ setClients(res.data);
+ } catch (e) {
+ console.error('Failed to fetch clients', e);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const fetchUsers = async () => {
+ try {
+ const res = await api.get('/users');
+ setUsers(res.data.filter((u: any) => u.status === 'APPROVED'));
+ } catch (e) {
+ console.error('Failed to fetch users', e);
+ }
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!formData.clientId || !formData.notes) {
+ alert('Please select a client and add notes.');
+ return;
+ }
+
+ try {
+ const dateStr = `${formData.date}T${formData.time}:00`;
+ await api.post('/followups', {
+ ...formData,
+ date: new Date(dateStr).toISOString(),
+ status: 'PENDING'
+ });
+ setIsOpen(false);
+ setFormData({
+ ...formData,
+ notes: '',
+ date: new Date().toISOString().split('T')[0],
+ });
+ alert('Event scheduled successfully!');
+ // Optional: Trigger a global refresh if needed
+ window.dispatchEvent(new CustomEvent('activityCreated'));
+ } catch (e) {
+ alert('Failed to schedule event.');
+ }
+ };
+
+ const types = [
+ { id: 'CALL', label: 'Call', icon: Phone, color: 'text-green-600', bg: 'bg-green-50', border: 'border-green-200' },
+ { id: 'MESSAGE', label: 'Message', icon: MessageCircle, color: 'text-cyan-600', bg: 'bg-cyan-50', border: 'border-cyan-200' },
+ { id: 'DEMO_SCHEDULED', label: 'Demo Sch', icon: Calendar, color: 'text-blue-600', bg: 'bg-blue-50', border: 'border-blue-200' },
+ { id: 'DEMO_COMPLETED', label: 'Demo Done', icon: CheckCircle2, color: 'text-emerald-600', bg: 'bg-emerald-50', border: 'border-emerald-200' },
+ { id: 'QUOTE_REQUEST', label: 'Quote Req', icon: FileSearch, color: 'text-purple-600', bg: 'bg-purple-50', border: 'border-purple-200' },
+ { id: 'QUOTE_SEND', label: 'Quote Send', icon: Send, color: 'text-indigo-600', bg: 'bg-indigo-50', border: 'border-indigo-200' },
+ { id: 'VISIT_SCHEDULED', label: 'Visit Sch', icon: MapPin, color: 'text-orange-600', bg: 'bg-orange-50', border: 'border-orange-200' },
+ { id: 'VISIT_COMPLETED', label: 'Visit Done', icon: ClipboardCheck, color: 'text-red-600', bg: 'bg-red-50', border: 'border-red-200' },
+ { id: 'NEGOTIATION', label: 'Negotiate', icon: Handshake, color: 'text-amber-600', bg: 'bg-amber-50', border: 'border-amber-200' },
+ { id: 'FOLLOWUP', label: 'Other', icon: ListTodo, color: 'text-slate-600', bg: 'bg-slate-50', border: 'border-slate-200' },
+ ];
+
+ return (
+ <>
+ {/* Floating Button */}
+
+
+ {/* Modal */}
+ {isOpen && (
+
+
+ {/* Header */}
+
+
+
Quick Event
+
Schedule activity from anywhere
+
+
+
+
+
+
+
+ )}
+ >
+ );
+}
diff --git a/src/components/OpportunityBoard.tsx b/src/components/OpportunityBoard.tsx
index e84a1b5..a9d69d1 100644
--- a/src/components/OpportunityBoard.tsx
+++ b/src/components/OpportunityBoard.tsx
@@ -31,7 +31,7 @@ interface Opportunity {
id: string;
title: string;
value: number;
- stage: 'LEAD' | 'QUALIFIED' | 'POTENTIAL' | 'WON' | 'LOST';
+ stage: 'LEAD' | 'QUALIFIED' | 'POTENTIAL' | 'SALES' | 'LOST';
isDemoDone?: boolean;
client: {
name: string;
@@ -72,7 +72,7 @@ interface ColumnProps {
// --- Configuration ---
const STAGE_CONFIG: Record = {
'LEAD': {
- title: 'New Lead',
+ title: 'Lead',
bg: 'bg-[#f8f9fa]',
text: 'text-gray-700',
accent: 'bg-gray-400',
@@ -92,8 +92,8 @@ const STAGE_CONFIG: Record
prev.map((item) =>
@@ -526,7 +532,7 @@ export default function OpportunityBoard() {
try {
await api.patch(`/opportunities/${activeId}`, { stage: newStage });
} catch (error: any) {
- if (newStage === 'WON') {
+ if (newStage === 'SALES') {
// Open modal and explicitly set the target stage
setEditingId(activeItem.id);
setNewItemData({
@@ -549,7 +555,9 @@ export default function OpportunityBoard() {
creatorId: (activeItem as any).creatorId || '',
demoOwnerId: (activeItem as any).demoOwnerId || '',
closingOwnerId: (activeItem as any).closingOwnerId || '',
- isDemoDone: !!activeItem.isDemoDone
+ isDemoDone: !!activeItem.isDemoDone,
+ closingProbability: (activeItem as any).closingProbability || 0,
+ expectedClosingTimeframe: (activeItem as any).expectedClosingTimeframe || ''
});
setIsModalOpen(true);
} else {
@@ -561,7 +569,7 @@ export default function OpportunityBoard() {
}
};
- const stages = ['LEAD', 'QUALIFIED', 'POTENTIAL', 'WON'];
+ const stages = ['LEAD', 'QUALIFIED', 'POTENTIAL', 'SALES'];
const getColumnTotal = (stage: string) => items.filter(i => i.stage === stage).reduce((sum, i) => sum + i.value, 0);
return (
@@ -731,6 +739,37 @@ export default function OpportunityBoard() {
/>
+
+
+
+
+
+
+
+
+
+
@@ -835,16 +874,7 @@ export default function OpportunityBoard() {
-
- setNewItemData({ ...newItemData, isDemoDone: e.target.checked })}
- />
-
-
+ {/* Demo Completed checkbox removed from OpportunityBoard */}
@@ -877,67 +907,10 @@ export default function OpportunityBoard() {
- {/* DEMO ACTIVITY FIELDS */}
- {newItemData.isDemoDone && (
-
-
- DEMO ACTIVITY DETAILS
-
-
-
- setNewItemData({ ...newItemData, demoPersonName: e.target.value })}
- />
-
-
-
-
-
-
-
- setNewItemData({ ...newItemData, demoContactDetails: e.target.value })}
- />
-
-
-
- setNewItemData({ ...newItemData, competitorMention: e.target.value })}
- />
-
-
-
-
-
- )}
+ {/* Demo activity details fields removed from OpportunityBoard */}
{/* CLOSING STAGE FIELDS */}
- {newItemData.stage === 'WON' && (
+ {newItemData.stage === 'SALES' && (
CLOSING STAGE INFORMATION (MANDATORY)
diff --git a/src/components/TargetManager.tsx b/src/components/TargetManager.tsx
index a5da9d3..9947e5b 100644
--- a/src/components/TargetManager.tsx
+++ b/src/components/TargetManager.tsx
@@ -39,6 +39,7 @@ interface TargetData {
requiredClosures: number;
avgDealValue: number;
user?: { name: string };
+ createdAt?: string;
}
export default function TargetManager() {
@@ -50,24 +51,25 @@ export default function TargetManager() {
// Form state
const [editingTarget, setEditingTarget] = useState
| null>(null);
const [selectedUserId, setSelectedUserId] = useState('');
- const [monthlyTarget, setMonthlyTarget] = useState(400000);
- const [minTarget, setMinTarget] = useState(200000);
- const [avgDealValue, setAvgDealValue] = useState(40000);
- const [month, setMonth] = useState(new Date().getMonth() + 1);
- const [year, setYear] = useState(new Date().getFullYear());
+ const [monthlyTarget, setMonthlyTarget] = useState(400000);
+ const [minTarget, setMinTarget] = useState(200000);
+ const [avgDealValue, setAvgDealValue] = useState(40000);
const [searchTerm, setSearchTerm] = useState('');
+ const [historyUserId, setHistoryUserId] = useState(null);
// Dynamic Engine State
- const [requiredClosures, setRequiredClosures] = useState(0);
- const [requiredDemos, setRequiredDemos] = useState(0);
- const [requiredPotential, setRequiredPotential] = useState(0);
- const [requiredLeads, setRequiredLeads] = useState(0);
- const [dailyLeadTarget, setDailyLeadTarget] = useState(0);
+ const [requiredClosures, setRequiredClosures] = useState(0);
+ const [requiredDemos, setRequiredDemos] = useState(0);
+ const [requiredPotential, setRequiredPotential] = useState(0);
+ const [requiredLeads, setRequiredLeads] = useState(0);
+ const [dailyLeadTarget, setDailyLeadTarget] = useState(0);
// Auto-calculate defaults when core values change
useEffect(() => {
if (!editingTarget) {
- const calcClosures = Math.ceil(monthlyTarget / avgDealValue);
+ const mTarget = Number(monthlyTarget) || 0;
+ const aDealValue = Number(avgDealValue) || 1; // prevent divide by zero
+ const calcClosures = Math.ceil(mTarget / aDealValue);
const calcDemos = calcClosures * 3;
const calcPotential = calcDemos * 2;
const calcLeads = calcPotential * 5 * 2;
@@ -105,8 +107,8 @@ export default function TargetManager() {
try {
const payload = {
userId: selectedUserId,
- month: Number(month),
- year: Number(year),
+ month: new Date().getMonth() + 1,
+ year: new Date().getFullYear(),
monthlyTarget: Number(monthlyTarget),
minTarget: Number(minTarget),
avgDealValue: Number(avgDealValue),
@@ -117,11 +119,8 @@ export default function TargetManager() {
dailyLeadTarget: Number(dailyLeadTarget)
};
- if (editingTarget?.id) {
- await api.patch(`/targets/${editingTarget.id}`, payload);
- } else {
- await api.post('/targets', payload);
- }
+ // Always create a new target to preserve history
+ await api.post('/targets', payload);
setIsModalOpen(false);
setEditingTarget(null);
@@ -148,8 +147,6 @@ export default function TargetManager() {
setMonthlyTarget(target.monthlyTarget);
setMinTarget(target.minTarget);
setAvgDealValue(target.avgDealValue || 40000);
- setMonth(target.month);
- setYear(target.year);
// Load existing benchmarks
setRequiredClosures(target.requiredClosures || Math.ceil(target.monthlyTarget / (target.avgDealValue || 40000)));
@@ -161,10 +158,26 @@ export default function TargetManager() {
setIsModalOpen(true);
};
- const filteredTargets = targets.filter(t =>
+ const latestTargets = targets.reduce((acc: TargetData[], current) => {
+ const existingIndex = acc.findIndex(t => t.userId === current.userId);
+ if (existingIndex === -1) {
+ acc.push(current);
+ } else {
+ const existingTime = acc[existingIndex].createdAt ? new Date(acc[existingIndex].createdAt!).getTime() : 0;
+ const currentTime = current.createdAt ? new Date(current.createdAt).getTime() : 0;
+ if (currentTime > existingTime) {
+ acc[existingIndex] = current;
+ }
+ }
+ return acc;
+ }, []);
+
+ const filteredTargets = latestTargets.filter(t =>
t.user?.name.toLowerCase().includes(searchTerm.toLowerCase())
);
+ const historyTargets = historyUserId ? targets.filter(t => t.userId === historyUserId).sort((a, b) => new Date(b.createdAt || 0).getTime() - new Date(a.createdAt || 0).getTime()) : [];
+
return (
{/* Header */}
@@ -225,16 +238,26 @@ export default function TargetManager() {
{target.user?.name}
- Target Period: {new Date(target.year, target.month-1).toLocaleString('default', { month: 'long', year: 'numeric' })}
+ Active Target Configuration
-
+
+
+
+
{/* Revenue Stats */}
@@ -327,7 +350,7 @@ export default function TargetManager() {
setMonthlyTarget(Number(e.target.value))}
+ onChange={e => setMonthlyTarget(e.target.value === '' ? '' : Number(e.target.value))}
className="w-full bg-slate-50 border-none rounded-2xl py-4 px-6 text-sm font-bold text-slate-700 outline-none focus:ring-2 focus:ring-odoo-primary/20 transition-all"
required
/>
@@ -337,7 +360,7 @@ export default function TargetManager() {
setMinTarget(Number(e.target.value))}
+ onChange={e => setMinTarget(e.target.value === '' ? '' : Number(e.target.value))}
className="w-full bg-slate-50 border-none rounded-2xl py-4 px-6 text-sm font-bold text-slate-700 outline-none focus:ring-2 focus:ring-odoo-primary/20 transition-all"
required
/>
@@ -347,21 +370,11 @@ export default function TargetManager() {
setAvgDealValue(Number(e.target.value))}
+ onChange={e => setAvgDealValue(e.target.value === '' ? '' : Number(e.target.value))}
className="w-full bg-slate-50 border-none rounded-2xl py-4 px-6 text-sm font-bold text-slate-700 outline-none focus:ring-2 focus:ring-odoo-primary/20 transition-all"
required
/>
-
@@ -419,7 +432,7 @@ export default function TargetManager() {
setRequiredPotential(Number(e.target.value))}
+ onChange={e => setRequiredPotential(e.target.value === '' ? '' : Number(e.target.value))}
className="w-20 bg-white/10 text-white text-sm font-black text-right px-2 py-1 rounded outline-none focus:ring-1 focus:ring-white/50"
/>
@@ -428,7 +441,7 @@ export default function TargetManager() {
setRequiredLeads(Number(e.target.value))}
+ onChange={e => setRequiredLeads(e.target.value === '' ? '' : Number(e.target.value))}
className="w-20 bg-white/10 text-white text-sm font-black text-right px-2 py-1 rounded outline-none focus:ring-1 focus:ring-white/50"
/>
@@ -437,7 +450,7 @@ export default function TargetManager() {
setRequiredClosures(Number(e.target.value))}
+ onChange={e => setRequiredClosures(e.target.value === '' ? '' : Number(e.target.value))}
className="w-20 bg-emerald-500/20 text-emerald-400 text-sm font-black text-right px-2 py-1 rounded outline-none focus:ring-1 focus:ring-emerald-500/50"
/>
@@ -464,6 +477,65 @@ export default function TargetManager() {
)}
+
+ {/* History Modal */}
+ {historyUserId && (
+
+
setHistoryUserId(null)}>
+
+
+
+
Target History
+
+ Historical configurations for {historyTargets[0]?.user?.name || 'User'}
+
+
+
+
+
+ {historyTargets.length === 0 ? (
+
No history available.
+ ) : (
+ historyTargets.map((t, idx) => (
+
+
+
+
+
+ {idx === 0 ? 'Current Active Target' : 'Historical Target'}
+
+
+ {t.createdAt ? new Date(t.createdAt).toLocaleString() : 'Unknown Date'}
+
+
+
+
+
Monthly Target
+
₹{t.monthlyTarget.toLocaleString()}
+
+
+
Daily Leads
+
{t.dailyLeadTarget}
+
+
+
Required Demos
+
{t.requiredDemos}
+
+
+
Required Closures
+
{t.requiredClosures}
+
+
+
+
+ ))
+ )}
+
+
+
+ )}
);
}
diff --git a/src/components/UserManager.tsx b/src/components/UserManager.tsx
index 5cf4965..d33dcde 100644
--- a/src/components/UserManager.tsx
+++ b/src/components/UserManager.tsx
@@ -3,7 +3,8 @@
import React, { useEffect, useState } from 'react';
import api from '../lib/axios';
import { useAuth } from '@/context/AuthContext';
-import { Check, X } from 'lucide-react';
+import { Check, X, Shield, Lock } from 'lucide-react';
+import clsx from 'clsx';
interface User {
id: string;
@@ -13,8 +14,28 @@ interface User {
status: string;
managerId?: string;
manager?: { name: string };
+ permissions?: string;
}
+const ALL_PERMISSIONS = [
+ { id: 'dashboard', label: 'Dashboard', description: 'Access to main overview' },
+ { id: 'tracking', label: 'Live Tracking', description: 'Real-time location of field staff' },
+ { id: 'opportunities', label: 'Opportunities', description: 'Pipeline and sales board' },
+ { id: 'clients', label: 'Clients', icon: 'Users', description: 'Client management' },
+ { id: 'quotes', label: 'Quotes', description: 'Quote generation and tracking' },
+ { id: 'expenses', label: 'Expenses', description: 'Approval of expense claims' },
+ { id: 'incentives', label: 'Incentives', description: 'Incentive tracking' },
+ { id: 'reports', label: 'Reports', description: 'Data analytics and exports' },
+ { id: 'targets', label: 'Targets', description: 'Assigning sales goals' },
+ { id: 'activities', label: 'Activities', description: 'Task and event logs' },
+ { id: 'call-logs', label: 'Call Logs', description: 'Mobile call history' },
+ { id: 'funnel-analysis', label: 'Funnel Analysis', description: 'Sales conversion metrics' },
+ { id: 'pipeline-engine', label: 'Pipeline Engine', description: 'Calculations and targets engine' },
+ { id: 'products', label: 'Products', description: 'Inventory/Product catalog' },
+ { id: 'users', label: 'Users', description: 'Managing user access' },
+ { id: 'settings', label: 'Settings', description: 'System configurations' },
+];
+
export default function UserManager() {
const { user: currentUser } = useAuth();
const [users, setUsers] = useState([]);
@@ -25,6 +46,10 @@ export default function UserManager() {
const [role, setRole] = useState('TELESALES_EXECUTIVE');
const [managerId, setManagerId] = useState('');
const [creating, setCreating] = useState(false);
+ const [selectedUser, setSelectedUser] = useState(null);
+ const [showPermissionModal, setShowPermissionModal] = useState(false);
+ const [userPermissions, setUserPermissions] = useState([]);
+ const [savingPermissions, setSavingPermissions] = useState(false);
const isAdminOrGM = currentUser?.role === 'ADMIN' || currentUser?.role === 'GENERAL_MANAGER';
@@ -79,6 +104,36 @@ export default function UserManager() {
}
};
+ const handleManagePermissions = (user: User) => {
+ setSelectedUser(user);
+ setUserPermissions(user.permissions ? JSON.parse(user.permissions) : []);
+ setShowPermissionModal(true);
+ };
+
+ const togglePermission = (permId: string) => {
+ setUserPermissions(prev =>
+ prev.includes(permId) ? prev.filter(p => p !== permId) : [...prev, permId]
+ );
+ };
+
+ const savePermissions = async () => {
+ if (!selectedUser) return;
+ setSavingPermissions(true);
+ try {
+ await api.patch(`/users/${selectedUser.id}`, {
+ permissions: JSON.stringify(userPermissions)
+ });
+ setShowPermissionModal(false);
+ fetchUsers();
+ alert('Permissions updated successfully');
+ } catch (error) {
+ console.error(error);
+ alert('Failed to update permissions');
+ } finally {
+ setSavingPermissions(false);
+ }
+ };
+
const pendingUsers = users.filter(u => u.status === 'PENDING');
const approvedUsers = users.filter(u => u.status === 'APPROVED');
@@ -177,6 +232,7 @@ export default function UserManager() {
Role |
Reporting To |
Status |
+ {isAdminOrGM && Actions | }
@@ -205,11 +261,88 @@ export default function UserManager() {
Active
+ {isAdminOrGM && (
+
+
+ |
+ )}
))}
+
+ {/* Permissions Modal */}
+ {showPermissionModal && selectedUser && (
+