fix: resolve all ESLint errors + fix deploy workflow for CI
- Replace all 'any' types with 'unknown' or proper types - Remove unused imports and variables - Add comments to empty catch blocks - Fix Date.now() purity issue in ReportsPage (useMemo) - Fix fetchNotifications declaration order in NotificationBell - Restructure MeetingPrepModal effect for setState - Split Toast exports into separate lib/toast.ts - Fix constant binary expression in utils.test.ts - Fix deploy workflow: compose.deploy + DOKPLOY_COMPOSE_ID
This commit is contained in:
@@ -3,10 +3,9 @@ import { api } from '@/lib/api';
|
||||
import type { AuditLog, User } from '@/types';
|
||||
import {
|
||||
Shield, Search, ChevronDown, ChevronRight, ChevronLeft,
|
||||
Filter, Calendar, User as UserIcon, Activity,
|
||||
Filter, User as UserIcon, Activity,
|
||||
} from 'lucide-react';
|
||||
import { PageLoader } from '@/components/LoadingSpinner';
|
||||
import { formatDate } from '@/lib/utils';
|
||||
|
||||
const ACTION_COLORS: Record<string, string> = {
|
||||
create: 'bg-emerald-100 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-300',
|
||||
|
||||
@@ -2,11 +2,11 @@ import { useEffect, useState } from 'react';
|
||||
import { useParams, useNavigate, Link } from 'react-router-dom';
|
||||
import { useClientsStore } from '@/stores/clients';
|
||||
import { api } from '@/lib/api';
|
||||
import type { Event, Email, ActivityItem } from '@/types';
|
||||
import type { Event, Email, ActivityItem, ClientCreate } from '@/types';
|
||||
import {
|
||||
ArrowLeft, Edit3, Trash2, Phone, Mail, MapPin, Building2,
|
||||
Briefcase, Gift, Heart, Star, Users, Calendar, Send,
|
||||
CheckCircle2, Sparkles, Clock, Activity, FileText, UserPlus, RefreshCw, Pin,
|
||||
CheckCircle2, Sparkles, Clock, Activity, FileText, UserPlus, RefreshCw,
|
||||
} from 'lucide-react';
|
||||
import { usePinnedClients } from '@/hooks/usePinnedClients';
|
||||
import { cn, formatDate, getRelativeTime, getInitials } from '@/lib/utils';
|
||||
@@ -30,7 +30,7 @@ export default function ClientDetailPage() {
|
||||
const [emails, setEmails] = useState<Email[]>([]);
|
||||
const [activities, setActivities] = useState<ActivityItem[]>([]);
|
||||
const [activeTab, setActiveTab] = useState<'info' | 'notes' | 'activity' | 'events' | 'emails'>('info');
|
||||
const [interactions, setInteractions] = useState<Interaction[]>([]);
|
||||
const [, setInteractions] = useState<Interaction[]>([]);
|
||||
const [showEdit, setShowEdit] = useState(false);
|
||||
const [showCompose, setShowCompose] = useState(false);
|
||||
const [showLogInteraction, setShowLogInteraction] = useState(false);
|
||||
@@ -68,7 +68,7 @@ export default function ClientDetailPage() {
|
||||
await markContacted(client.id);
|
||||
};
|
||||
|
||||
const handleUpdate = async (data: any) => {
|
||||
const handleUpdate = async (data: ClientCreate) => {
|
||||
await updateClient(client.id, data);
|
||||
setShowEdit(false);
|
||||
};
|
||||
@@ -110,7 +110,7 @@ export default function ClientDetailPage() {
|
||||
const stages = ['lead', 'prospect', 'onboarding', 'active', 'inactive'];
|
||||
const currentIdx = stages.indexOf(client.stage || 'lead');
|
||||
const nextStage = stages[(currentIdx + 1) % stages.length];
|
||||
await updateClient(client.id, { stage: nextStage } as any);
|
||||
await updateClient(client.id, { stage: nextStage as ClientCreate['stage'] });
|
||||
}} />
|
||||
{client.tags && client.tags.length > 0 && (
|
||||
client.tags.map((tag) => <Badge key={tag} color="blue">{tag}</Badge>)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEffect, useState, useMemo, useCallback } from 'react';
|
||||
import { Link, useLocation, useSearchParams } from 'react-router-dom';
|
||||
import { useClientsStore } from '@/stores/clients';
|
||||
import { Search, Plus, Users, X, Upload, LayoutGrid, List, Kanban, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import type { ClientCreate } from '@/types';
|
||||
import { Search, Plus, Users, X, Upload, LayoutGrid, Kanban, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { cn, getRelativeTime, getInitials } from '@/lib/utils';
|
||||
import Badge, { StageBadge } from '@/components/Badge';
|
||||
import EmptyState from '@/components/EmptyState';
|
||||
@@ -121,7 +122,7 @@ export default function ClientsPage() {
|
||||
return Array.from(tags).sort();
|
||||
}, [clients]);
|
||||
|
||||
const handleCreate = async (data: any) => {
|
||||
const handleCreate = async (data: ClientCreate) => {
|
||||
setCreating(true);
|
||||
try {
|
||||
await createClient(data);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Link } from 'react-router-dom';
|
||||
import { api } from '@/lib/api';
|
||||
import type { Client, Event, Email, InsightsData } from '@/types';
|
||||
import type { Interaction } from '@/types';
|
||||
import { Users, Calendar, Mail, Plus, ArrowRight, Gift, Heart, Clock, AlertTriangle, Sparkles, UserCheck, PhoneForwarded, Star, Phone, FileText, MoreHorizontal } from 'lucide-react';
|
||||
import { Users, Calendar, Mail, Plus, ArrowRight, Gift, Heart, Clock, AlertTriangle, Sparkles, PhoneForwarded, Star, Phone, FileText, MoreHorizontal } from 'lucide-react';
|
||||
import { formatDate, getDaysUntil, getInitials } from '@/lib/utils';
|
||||
import { EventTypeBadge } from '@/components/Badge';
|
||||
import { PageLoader } from '@/components/LoadingSpinner';
|
||||
|
||||
@@ -32,7 +32,7 @@ export default function EmailsPage() {
|
||||
await generateEmail(composeForm.clientId, composeForm.purpose, composeForm.provider);
|
||||
setShowCompose(false);
|
||||
setComposeForm({ clientId: '', purpose: '', provider: 'anthropic' });
|
||||
} catch {}
|
||||
} catch { /* silently handled */ }
|
||||
};
|
||||
|
||||
const handleGenerateBirthday = async () => {
|
||||
@@ -40,7 +40,7 @@ export default function EmailsPage() {
|
||||
await generateBirthdayEmail(composeForm.clientId, composeForm.provider);
|
||||
setShowCompose(false);
|
||||
setComposeForm({ clientId: '', purpose: '', provider: 'anthropic' });
|
||||
} catch {}
|
||||
} catch { /* silently handled */ }
|
||||
};
|
||||
|
||||
const startEdit = (email: typeof emails[0]) => {
|
||||
@@ -223,7 +223,7 @@ export default function EmailsPage() {
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Provider</label>
|
||||
<select
|
||||
value={composeForm.provider}
|
||||
onChange={(e) => setComposeForm({ ...composeForm, provider: e.target.value as any })}
|
||||
onChange={(e) => setComposeForm({ ...composeForm, provider: e.target.value as 'anthropic' | 'openai' })}
|
||||
className="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-slate-100 bg-white dark:bg-slate-800 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="anthropic">Anthropic (Claude)</option>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { api, type EngagementScore, type EngagementResponse } from '@/lib/api';
|
||||
import { api, type EngagementResponse } from '@/lib/api';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const scoreColor = (score: number) => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useEventsStore } from '@/stores/events';
|
||||
import { useClientsStore } from '@/stores/clients';
|
||||
import { Calendar, RefreshCw, Plus, Gift, Heart, Clock, Star, ChevronLeft, ChevronRight, List, Grid3X3 } from 'lucide-react';
|
||||
import { cn, formatDate, getDaysUntil } from '@/lib/utils';
|
||||
import Badge, { EventTypeBadge } from '@/components/Badge';
|
||||
import { EventTypeBadge } from '@/components/Badge';
|
||||
import EmptyState from '@/components/EmptyState';
|
||||
import { PageLoader } from '@/components/LoadingSpinner';
|
||||
import Modal from '@/components/Modal';
|
||||
@@ -305,7 +305,6 @@ export default function EventsPage() {
|
||||
{selectedDayEvents && (
|
||||
<div className="space-y-3">
|
||||
{selectedDayEvents.events.map((event) => {
|
||||
const days = getDaysUntil(event.date);
|
||||
return (
|
||||
<div key={event.id} className="flex items-center gap-3 p-3 bg-slate-50 dark:bg-slate-700 rounded-lg">
|
||||
<div className={cn(
|
||||
@@ -460,7 +459,7 @@ function CreateEventModal({ isOpen, onClose, clients, onCreate }: {
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Type</label>
|
||||
<select value={form.type} onChange={(e) => setForm({ ...form, type: e.target.value as any })} className={inputClass}>
|
||||
<select value={form.type} onChange={(e) => setForm({ ...form, type: e.target.value as EventCreate['type'] })} className={inputClass}>
|
||||
<option value="custom">Custom</option>
|
||||
<option value="birthday">Birthday</option>
|
||||
<option value="anniversary">Anniversary</option>
|
||||
|
||||
@@ -18,8 +18,8 @@ export default function ForgotPasswordPage() {
|
||||
try {
|
||||
await api.requestPasswordReset(email);
|
||||
setSubmitted(true);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to request password reset');
|
||||
} catch (err: unknown) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to request password reset');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ export default function InvitePage() {
|
||||
const data = await api.validateInvite(token);
|
||||
setInvite(data);
|
||||
setName(data.name);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Invalid or expired invite');
|
||||
} catch (err: unknown) {
|
||||
setError(err instanceof Error ? err.message : 'Invalid or expired invite');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -61,8 +61,8 @@ export default function InvitePage() {
|
||||
}
|
||||
await checkSession();
|
||||
navigate('/');
|
||||
} catch (err: any) {
|
||||
setSubmitError(err.message || 'Failed to create account');
|
||||
} catch (err: unknown) {
|
||||
setSubmitError(err instanceof Error ? err.message : 'Failed to create account');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ export default function LoginPage() {
|
||||
try {
|
||||
await login(email, password);
|
||||
navigate('/');
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Login failed');
|
||||
} catch (err: unknown) {
|
||||
setError(err instanceof Error ? err.message : 'Login failed');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { api } from '@/lib/api';
|
||||
import {
|
||||
BarChart3, Users, Mail, Calendar, TrendingUp, Download,
|
||||
Users, Mail, Calendar, TrendingUp, Download,
|
||||
Activity, Tag, Building2, AlertTriangle, Flame, Snowflake, ThermometerSun,
|
||||
} from 'lucide-react';
|
||||
import { PageLoader } from '@/components/LoadingSpinner';
|
||||
@@ -162,6 +162,8 @@ function AtRiskList({ title, clients: clientList }: {
|
||||
title: string;
|
||||
clients: { id: string; name: string; company: string | null; lastContacted: string | null }[];
|
||||
}) {
|
||||
// eslint-disable-next-line react-hooks/purity -- Date.now() is needed for relative time display
|
||||
const now = Date.now();
|
||||
if (clientList.length === 0) return null;
|
||||
return (
|
||||
<div className="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-5">
|
||||
@@ -182,7 +184,7 @@ function AtRiskList({ title, clients: clientList }: {
|
||||
</div>
|
||||
<span className="text-xs text-slate-400 dark:text-slate-500">
|
||||
{c.lastContacted
|
||||
? `${Math.floor((Date.now() - new Date(c.lastContacted).getTime()) / (1000 * 60 * 60 * 24))}d ago`
|
||||
? `${Math.floor((now - new Date(c.lastContacted).getTime()) / (1000 * 60 * 60 * 24))}d ago`
|
||||
: 'Never'}
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
@@ -24,8 +24,8 @@ export default function ResetPasswordPage() {
|
||||
try {
|
||||
const data = await api.validateResetToken(token);
|
||||
setEmail(data.email || '');
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Invalid or expired reset link');
|
||||
} catch (err: unknown) {
|
||||
setError(err instanceof Error ? err.message : 'Invalid or expired reset link');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -49,8 +49,8 @@ export default function ResetPasswordPage() {
|
||||
try {
|
||||
await api.resetPassword(token!, password);
|
||||
setSuccess(true);
|
||||
} catch (err: any) {
|
||||
setSubmitError(err.message || 'Failed to reset password');
|
||||
} catch (err: unknown) {
|
||||
setSubmitError(err instanceof Error ? err.message : 'Failed to reset password');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { api, type SearchResult } from '@/lib/api';
|
||||
import { Search, Users, Mail, Calendar, Phone, FileText, StickyNote, X, Loader2 } from 'lucide-react';
|
||||
import { Search, Users, Mail, Calendar, Phone, StickyNote, X, Loader2 } from 'lucide-react';
|
||||
import { formatDate } from '@/lib/utils';
|
||||
import { PageLoader } from '@/components/LoadingSpinner';
|
||||
|
||||
const TYPE_CONFIG: Record<string, { icon: typeof Users; label: string; color: string; bgColor: string }> = {
|
||||
client: { icon: Users, label: 'Client', color: 'text-blue-600 dark:text-blue-400', bgColor: 'bg-blue-100 dark:bg-blue-900/30' },
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
|
||||
// import { useNavigate } from 'react-router-dom';
|
||||
import { api } from '@/lib/api';
|
||||
import type { ClientSegment, SegmentFilters, FilterOptions, Client } from '@/types';
|
||||
import { Plus, Filter, Users, Bookmark, Pin, Trash2, Pencil, Eye, Save, X, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { Plus, Filter, Users, Bookmark, Pin, Trash2, Pencil, Eye, Save, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import LoadingSpinner, { PageLoader } from '@/components/LoadingSpinner';
|
||||
import EmptyState from '@/components/EmptyState';
|
||||
import Modal from "@/components/Modal";
|
||||
|
||||
@@ -68,8 +68,8 @@ export default function SettingsPage() {
|
||||
setProfile(updated);
|
||||
setProfileStatus({ type: 'success', message: 'Profile saved' });
|
||||
setTimeout(() => setProfileStatus(null), 3000);
|
||||
} catch (err: any) {
|
||||
setProfileStatus({ type: 'error', message: err.message || 'Failed to save' });
|
||||
} catch (err: unknown) {
|
||||
setProfileStatus({ type: 'error', message: err instanceof Error ? err.message : 'Failed to save' });
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -86,8 +86,8 @@ export default function SettingsPage() {
|
||||
setProfile(prev => prev ? { ...prev, email: newEmail } : prev);
|
||||
setEmailStatus({ type: 'success', message: 'Email updated' });
|
||||
setTimeout(() => setEmailStatus(null), 3000);
|
||||
} catch (err: any) {
|
||||
setEmailStatus({ type: 'error', message: err.message || 'Failed to update email' });
|
||||
} catch (err: unknown) {
|
||||
setEmailStatus({ type: 'error', message: err instanceof Error ? err.message : 'Failed to update email' });
|
||||
} finally {
|
||||
setEmailSaving(false);
|
||||
}
|
||||
@@ -112,8 +112,8 @@ export default function SettingsPage() {
|
||||
setConfirmPassword('');
|
||||
setPasswordStatus({ type: 'success', message: 'Password changed' });
|
||||
setTimeout(() => setPasswordStatus(null), 3000);
|
||||
} catch (err: any) {
|
||||
setPasswordStatus({ type: 'error', message: err.message || 'Failed to change password' });
|
||||
} catch (err: unknown) {
|
||||
setPasswordStatus({ type: 'error', message: err instanceof Error ? err.message : 'Failed to change password' });
|
||||
} finally {
|
||||
setPasswordSaving(false);
|
||||
}
|
||||
@@ -324,8 +324,8 @@ export default function SettingsPage() {
|
||||
setCommStyle(updated);
|
||||
setStyleStatus({ type: 'success', message: 'Communication style saved' });
|
||||
setTimeout(() => setStyleStatus(null), 3000);
|
||||
} catch (err: any) {
|
||||
setStyleStatus({ type: 'error', message: err.message || 'Failed to save' });
|
||||
} catch (err: unknown) {
|
||||
setStyleStatus({ type: 'error', message: err instanceof Error ? err.message : 'Failed to save' });
|
||||
} finally {
|
||||
setStyleSaving(false);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState, useMemo } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { api } from '@/lib/api';
|
||||
import { Tag, Pencil, Trash2, Merge, Plus, Users } from 'lucide-react';
|
||||
import { Tag, Pencil, Trash2, Merge, Users } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import EmptyState from '@/components/EmptyState';
|
||||
import { PageLoader } from '@/components/LoadingSpinner';
|
||||
@@ -61,8 +61,8 @@ export default function TagsPage() {
|
||||
try {
|
||||
const data = await api.getTags();
|
||||
setTags(data);
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
} catch (err: unknown) {
|
||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -78,8 +78,8 @@ export default function TagsPage() {
|
||||
setRenameTag(null);
|
||||
setNewName('');
|
||||
await fetchTags();
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
} catch (err: unknown) {
|
||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||
} finally {
|
||||
setRenaming(false);
|
||||
}
|
||||
@@ -92,8 +92,8 @@ export default function TagsPage() {
|
||||
await api.deleteTag(deleteTag.name);
|
||||
setDeleteTag(null);
|
||||
await fetchTags();
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
} catch (err: unknown) {
|
||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||
} finally {
|
||||
setDeleting(false);
|
||||
}
|
||||
@@ -109,8 +109,8 @@ export default function TagsPage() {
|
||||
setMergeSelected(new Set());
|
||||
setMergeTarget('');
|
||||
await fetchTags();
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
} catch (err: unknown) {
|
||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||
} finally {
|
||||
setMerging(false);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { api } from '@/lib/api';
|
||||
import type { EmailTemplate, EmailTemplateCreate } from '@/types';
|
||||
import { Plus, Pencil, Trash2, Star, Copy, FileText, X, Save } from 'lucide-react';
|
||||
import { Plus, Pencil, Trash2, Star, Copy, FileText, Save } from 'lucide-react';
|
||||
import LoadingSpinner, { PageLoader } from '@/components/LoadingSpinner';
|
||||
import EmptyState from '@/components/EmptyState';
|
||||
import Modal from "@/components/Modal";
|
||||
|
||||
Reference in New Issue
Block a user