template-saas/apps/frontend/src/pages/sales/activities/index.tsx
Adrian Flores Cortes 529ea53b5e
Some checks are pending
CI / Backend CI (push) Waiting to run
CI / Frontend CI (push) Waiting to run
CI / Security Scan (push) Waiting to run
CI / CI Summary (push) Blocked by required conditions
[SAAS-018] feat: Complete Sales Foundation module implementation
## Backend (NestJS)
- Entities: Lead, Opportunity, PipelineStage, Activity with TypeORM
- Services: LeadsService, OpportunitiesService, PipelineService, ActivitiesService, SalesDashboardService
- Controllers: LeadsController, OpportunitiesController, PipelineController, ActivitiesController, DashboardController
- DTOs: Full set of Create/Update/Convert DTOs with validation
- Tests: 5 test suites with comprehensive coverage

## Frontend (React)
- Pages: /sales, /sales/leads, /sales/leads/[id], /sales/opportunities, /sales/opportunities/[id], /sales/activities
- Components: SalesDashboard, ConversionFunnel, LeadsList, LeadForm, LeadCard, PipelineBoard, OpportunityCard, OpportunityForm, ActivityTimeline, ActivityForm
- Hooks: useLeads, useOpportunities, usePipeline, useActivities, useSalesDashboard
- Services: leads.api, opportunities.api, activities.api, pipeline.api, dashboard.api

## Documentation
- Updated SAAS-018-sales.md with implementation details
- Updated MASTER_INVENTORY.yml - status changed from specified to completed

Story Points: 21
Sprint: 6 - Sales Foundation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 20:49:59 -06:00

205 lines
7.5 KiB
TypeScript

import { useState } from 'react';
import { Plus, Calendar, Clock, AlertCircle } from 'lucide-react';
import { useActivities, useUpcomingActivities, useOverdueActivities, useActivityStats } from '../../../hooks/sales';
import { ActivityForm } from '../../../components/sales/ActivityForm';
import { ActivityType, ActivityStatus } from '../../../services/sales/activities.api';
export default function ActivitiesPage() {
const [showForm, setShowForm] = useState(false);
const [typeFilter, setTypeFilter] = useState<ActivityType | ''>('');
const [statusFilter, setStatusFilter] = useState<ActivityStatus | ''>('');
const [page, setPage] = useState(1);
const { data: activities, isLoading } = useActivities({
type: typeFilter || undefined,
status: statusFilter || undefined,
page,
limit: 20,
});
const { data: upcoming } = useUpcomingActivities(7);
const { data: overdue } = useOverdueActivities();
const { data: stats } = useActivityStats();
const getTypeIcon = (type: ActivityType) => {
switch (type) {
case 'call':
return '📞';
case 'meeting':
return '👥';
case 'task':
return '✅';
case 'email':
return '📧';
case 'note':
return '📝';
default:
return '📋';
}
};
const getStatusColor = (status: ActivityStatus) => {
switch (status) {
case 'pending':
return 'bg-yellow-100 text-yellow-800';
case 'completed':
return 'bg-green-100 text-green-800';
case 'cancelled':
return 'bg-gray-100 text-gray-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">Activities</h1>
<p className="text-sm text-gray-500">
{stats?.total || 0} total activities
</p>
</div>
<button
onClick={() => setShowForm(true)}
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
<Plus className="w-4 h-4" />
Add Activity
</button>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-4 gap-4">
<div className="bg-white p-4 rounded-lg shadow-sm border">
<div className="flex items-center gap-2">
<Clock className="w-5 h-5 text-yellow-500" />
<div>
<p className="text-sm text-gray-500">Pending</p>
<p className="text-2xl font-bold">{stats?.pending || 0}</p>
</div>
</div>
</div>
<div className="bg-white p-4 rounded-lg shadow-sm border">
<div className="flex items-center gap-2">
<AlertCircle className="w-5 h-5 text-red-500" />
<div>
<p className="text-sm text-gray-500">Overdue</p>
<p className="text-2xl font-bold text-red-600">{stats?.overdue || 0}</p>
</div>
</div>
</div>
<div className="bg-white p-4 rounded-lg shadow-sm border">
<div className="flex items-center gap-2">
<Calendar className="w-5 h-5 text-blue-500" />
<div>
<p className="text-sm text-gray-500">Upcoming (7 days)</p>
<p className="text-2xl font-bold">{upcoming?.length || 0}</p>
</div>
</div>
</div>
<div className="bg-white p-4 rounded-lg shadow-sm border">
<p className="text-sm text-gray-500">Completed</p>
<p className="text-2xl font-bold text-green-600">{stats?.completed || 0}</p>
</div>
</div>
{/* Filters */}
<div className="flex items-center gap-4 bg-white p-4 rounded-lg shadow-sm border">
<select
value={typeFilter}
onChange={(e) => setTypeFilter(e.target.value as ActivityType | '')}
className="px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
>
<option value="">All Types</option>
<option value="call">Calls</option>
<option value="meeting">Meetings</option>
<option value="task">Tasks</option>
<option value="email">Emails</option>
<option value="note">Notes</option>
</select>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as ActivityStatus | '')}
className="px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
>
<option value="">All Status</option>
<option value="pending">Pending</option>
<option value="completed">Completed</option>
<option value="cancelled">Cancelled</option>
</select>
</div>
{/* Activities List */}
{isLoading ? (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" />
</div>
) : (
<div className="bg-white rounded-lg shadow-sm border divide-y">
{activities?.data.length === 0 ? (
<div className="p-8 text-center text-gray-500">
No activities found
</div>
) : (
activities?.data.map((activity) => (
<div key={activity.id} className="p-4 hover:bg-gray-50">
<div className="flex items-start gap-4">
<span className="text-2xl">{getTypeIcon(activity.type)}</span>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<h3 className="font-medium truncate">{activity.subject}</h3>
<span className={`px-2 py-0.5 rounded-full text-xs ${getStatusColor(activity.status)}`}>
{activity.status}
</span>
</div>
{activity.description && (
<p className="text-sm text-gray-500 line-clamp-1 mt-1">
{activity.description}
</p>
)}
<div className="flex items-center gap-4 mt-2 text-xs text-gray-400">
{activity.due_date && (
<span>Due: {new Date(activity.due_date).toLocaleDateString()}</span>
)}
<span>Created: {new Date(activity.created_at).toLocaleDateString()}</span>
</div>
</div>
</div>
</div>
))
)}
</div>
)}
{/* Pagination */}
{activities && activities.totalPages > 1 && (
<div className="flex items-center justify-center gap-2">
<button
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page === 1}
className="px-4 py-2 border rounded-lg disabled:opacity-50"
>
Previous
</button>
<span className="text-sm text-gray-500">
Page {page} of {activities.totalPages}
</span>
<button
onClick={() => setPage((p) => Math.min(activities.totalPages, p + 1))}
disabled={page === activities.totalPages}
className="px-4 py-2 border rounded-lg disabled:opacity-50"
>
Next
</button>
</div>
)}
{/* Activity Form Modal */}
{showForm && (
<ActivityForm onClose={() => setShowForm(false)} />
)}
</div>
);
}