Skip to content

Commit ff93d3f

Browse files
committed
feat: add API endpoints and service methods for onboarding, completed, and dropped CRM entries
1 parent e6541eb commit ff93d3f

File tree

6 files changed

+154
-47
lines changed

6 files changed

+154
-47
lines changed

client/pages/CompaniesPage.tsx

Lines changed: 47 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,37 @@ export const CompaniesPage: React.FC = () => {
2424
const [isViewModalOpen, setIsViewModalOpen] = useState(false);
2525
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
2626
const [deleteId, setDeleteId] = useState<number | null>(null);
27+
28+
// Track which tabs have been loaded to avoid duplicate calls
29+
const [loadedTabs, setLoadedTabs] = useState<Set<string>>(new Set(['active']));
2730

2831
const [filters, setFilters] = useState<CompanyFilterState>({
2932
search: '',
3033
status: '',
3134
workType: ''
3235
});
3336

34-
const fetchData = async () => {
37+
const fetchDataForTab = async (tab: 'active' | 'dropped' | 'past') => {
3538
setIsLoading(true);
3639
try {
37-
const data = await companiesApi.getAll();
40+
let data: CRMEntry[];
41+
42+
switch (tab) {
43+
case 'active':
44+
data = await companiesApi.getOnboarded();
45+
break;
46+
case 'dropped':
47+
data = await companiesApi.getClosed();
48+
break;
49+
case 'past':
50+
data = await companiesApi.getDone();
51+
break;
52+
default:
53+
data = [];
54+
}
55+
3856
setCrmEntries(data);
57+
setLoadedTabs(prev => new Set(prev).add(tab));
3958
} catch (error) {
4059
console.error("Failed to fetch data", error);
4160
showToast("Failed to load companies", "error");
@@ -44,36 +63,28 @@ export const CompaniesPage: React.FC = () => {
4463
}
4564
};
4665

66+
// Initial load - fetch only onboarded (active) companies
4767
useEffect(() => {
4868
let mounted = true;
4969
const loadData = async () => {
5070
if (mounted) {
51-
await fetchData();
71+
await fetchDataForTab('active');
5272
}
5373
};
5474
loadData();
5575
return () => { mounted = false; };
5676
}, []);
5777

58-
const allCompanies = useMemo(() => {
59-
// Registry now strictly includes those that have passed initial lead stages
60-
return crmEntries.filter(entry => ['onboarded', 'on progress', 'Quote Sent', 'completed', 'drop'].includes(entry.status));
61-
}, [crmEntries]);
62-
63-
const categorizedData = useMemo(() => {
64-
// Onboarded, On Progress, and Quote Sent are considered active Focus nodes in the registry
65-
const active = allCompanies.filter(c => ['onboarded', 'on progress', 'Quote Sent'].includes(c.status));
66-
const dropped = allCompanies.filter(c => c.status === 'drop');
67-
const past = allCompanies.filter(c => c.status === 'completed');
68-
return { active, dropped, past };
69-
}, [allCompanies]);
78+
// Handle tab changes - fetch data only if not already loaded
79+
useEffect(() => {
80+
if (!loadedTabs.has(activeTab)) {
81+
fetchDataForTab(activeTab);
82+
}
83+
}, [activeTab]);
7084

85+
// No need for categorization - data comes pre-filtered from backend
7186
const displayData = useMemo(() => {
72-
let sourceList = categorizedData.active;
73-
if (activeTab === 'dropped') sourceList = categorizedData.dropped;
74-
if (activeTab === 'past') sourceList = categorizedData.past;
75-
76-
let result = sourceList.filter(item => {
87+
let result = crmEntries.filter(item => {
7788
const matchesSearch = filters.search === '' ||
7889
(item.company || '').toLowerCase().includes(filters.search.toLowerCase()) ||
7990
(item.contactName && item.contactName.toLowerCase().includes(filters.search.toLowerCase())) ||
@@ -86,7 +97,7 @@ export const CompaniesPage: React.FC = () => {
8697
});
8798

8899
return result.sort((a, b) => b.id - a.id);
89-
}, [categorizedData, activeTab, filters]);
100+
}, [crmEntries, filters]);
90101

91102
const handleEdit = (company: CRMEntry) => {
92103
setEditingCompany(company);
@@ -107,8 +118,10 @@ export const CompaniesPage: React.FC = () => {
107118
try {
108119
await companiesApi.update(updatedEntry.id, updatedEntry);
109120
showToast("Company details updated", "success");
121+
// Refresh current tab data
122+
await fetchDataForTab(activeTab);
110123
} catch(e) {
111-
fetchData();
124+
await fetchDataForTab(activeTab);
112125
showToast("Failed to update company", "error");
113126
}
114127
}
@@ -125,12 +138,15 @@ export const CompaniesPage: React.FC = () => {
125138
try {
126139
await companiesApi.update(company.id, updatedEntry);
127140
showToast(`Status updated to ${newStatus}`, "success");
128-
// If status changes out of registry criteria, re-fetch to update view
129-
if (!['onboarded', 'on progress', 'Quote Sent', 'completed', 'drop'].includes(newStatus)) {
130-
fetchData();
131-
}
141+
142+
// Status change may move company to different tab
143+
// Clear loaded tabs cache to force refresh when switching tabs
144+
setLoadedTabs(new Set([activeTab]));
145+
146+
// Refresh current tab to reflect changes
147+
await fetchDataForTab(activeTab);
132148
} catch (e) {
133-
fetchData();
149+
await fetchDataForTab(activeTab);
134150
showToast("Failed to update status", "error");
135151
}
136152
};
@@ -177,7 +193,7 @@ export const CompaniesPage: React.FC = () => {
177193
}`}
178194
>
179195
Active Focus
180-
<span className="ml-2 px-1.5 lg:px-2 py-0.5 rounded bg-brand-50 text-brand-600 text-[9px] lg:text-[10px]">{categorizedData.active.length}</span>
196+
{activeTab === 'active' && <span className="ml-2 px-1.5 lg:px-2 py-0.5 rounded bg-brand-50 text-brand-600 text-[9px] lg:text-[10px]">{crmEntries.length}</span>}
181197
</button>
182198
<button
183199
onClick={() => setActiveTab('past')}
@@ -188,7 +204,7 @@ export const CompaniesPage: React.FC = () => {
188204
}`}
189205
>
190206
Historical
191-
<span className="ml-2 px-1.5 lg:px-2 py-0.5 rounded bg-emerald-50 text-emerald-600 text-[9px] lg:text-[10px]">{categorizedData.past.length}</span>
207+
{activeTab === 'past' && <span className="ml-2 px-1.5 lg:px-2 py-0.5 rounded bg-emerald-50 text-emerald-600 text-[9px] lg:text-[10px]">{crmEntries.length}</span>}
192208
</button>
193209
<button
194210
onClick={() => setActiveTab('dropped')}
@@ -199,12 +215,12 @@ export const CompaniesPage: React.FC = () => {
199215
}`}
200216
>
201217
Archived
202-
<span className="ml-2 px-1.5 lg:px-2 py-0.5 rounded bg-rose-50 text-rose-600 text-[9px] lg:text-[10px]">{categorizedData.dropped.length}</span>
218+
{activeTab === 'dropped' && <span className="ml-2 px-1.5 lg:px-2 py-0.5 rounded bg-rose-50 text-rose-600 text-[9px] lg:text-[10px]">{crmEntries.length}</span>}
203219
</button>
204220
</div>
205221
</div>
206222

207-
<CompaniesFilters filters={filters} setFilters={setFilters} onRefresh={fetchData} />
223+
<CompaniesFilters filters={filters} setFilters={setFilters} onRefresh={() => fetchDataForTab(activeTab)} />
208224

209225
<div className="pt-4 pb-10 overflow-x-auto">
210226
<CompaniesTable

client/services/api.ts

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@ import { CRMEntry, Task, Meeting, AuthResponse, User, ForgotPasswordRequest, Ver
55
// ⚙️ API CONFIGURATION
66
// ============================================================================
77

8-
//const API_URL = import.meta.env.VITE_API_URL || '/api/v1';
9-
10-
const API_URL = 'http://localhost:8080/api/v1';
8+
const API_URL = import.meta.env.VITE_API_URL || '/api/v1';
119

10+
//const API_URL = 'http://localhost:8080/api/v1';
1211

1312

1413
const api = axios.create({
@@ -178,18 +177,47 @@ export const companiesApi = {
178177
...item,
179178
company: item.name || item.company
180179
}));
181-
} catch (error: any) {
182-
console.warn("Primary companies endpoint failed, attempting fallback to CRM...", error.message);
183-
try {
184-
const res = await api.get("/crm/all");
185-
const data = res.data.crmList || [];
186-
return data.map((item: any) => ({
187-
...item,
188-
company: item.name || item.company
189-
}));
190-
} catch (fallbackError) {
191-
throw handleApiError(error);
192-
}
180+
} catch (error: any) {
181+
throw handleApiError(error);
182+
}
183+
},
184+
185+
getOnboarded: async (): Promise<CRMEntry[]> => {
186+
try {
187+
const res = await api.get("/crm/onboarded");
188+
const data = Array.isArray(res.data) ? res.data : [];
189+
return data.map((item: any) => ({
190+
...item,
191+
company: item.name || item.company
192+
}));
193+
} catch (error: any) {
194+
throw handleApiError(error);
195+
}
196+
},
197+
198+
getDone: async (): Promise<CRMEntry[]> => {
199+
try {
200+
const res = await api.get("/crm/done");
201+
const data = Array.isArray(res.data) ? res.data : [];
202+
return data.map((item: any) => ({
203+
...item,
204+
company: item.name || item.company
205+
}));
206+
} catch (error: any) {
207+
throw handleApiError(error);
208+
}
209+
},
210+
211+
getClosed: async (): Promise<CRMEntry[]> => {
212+
try {
213+
const res = await api.get("/crm/closed");
214+
const data = Array.isArray(res.data) ? res.data : [];
215+
return data.map((item: any) => ({
216+
...item,
217+
company: item.name || item.company
218+
}));
219+
} catch (error: any) {
220+
throw handleApiError(error);
193221
}
194222
},
195223

server/src/main/java/com/incial/crm/controller/CrmController.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,30 @@ public ResponseEntity<Map<String, List<CrmEntryDto>>> getAllEntries() {
2828
return ResponseEntity.ok(crmService.getAllEntries());
2929
}
3030

31+
@GetMapping("/onboarded")
32+
@PreAuthorize(
33+
"hasAuthority('ROLE_ADMIN') or hasAuthority('ROLE_SUPER_ADMIN') or hasAuthority('ROLE_EMPLOYEE')"
34+
)
35+
public ResponseEntity<List<CrmEntryDto>> getOnboardedEntries() {
36+
return ResponseEntity.ok(crmService.getOnboardedEntries());
37+
}
38+
39+
@GetMapping("/done")
40+
@PreAuthorize(
41+
"hasAuthority('ROLE_ADMIN') or hasAuthority('ROLE_SUPER_ADMIN') or hasAuthority('ROLE_EMPLOYEE')"
42+
)
43+
public ResponseEntity<List<CrmEntryDto>> getCompletedEntries() {
44+
return ResponseEntity.ok(crmService.getCompletedEntries());
45+
}
46+
47+
@GetMapping("/closed")
48+
@PreAuthorize(
49+
"hasAuthority('ROLE_ADMIN') or hasAuthority('ROLE_SUPER_ADMIN') or hasAuthority('ROLE_EMPLOYEE')"
50+
)
51+
public ResponseEntity<List<CrmEntryDto>> getDroppedEntries() {
52+
return ResponseEntity.ok(crmService.getDroppedEntries());
53+
}
54+
3155
@GetMapping("/details/{id}")
3256
@PreAuthorize(
3357
"hasAuthority('ROLE_ADMIN') or hasAuthority('ROLE_SUPER_ADMIN') or hasAuthority('ROLE_EMPLOYEE')"

server/src/main/java/com/incial/crm/entity/CrmEntry.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
import java.util.Map;
1515

1616
@Entity
17-
@Table(name = "crm_entries")
17+
@Table(name = "crm_entries", indexes = {
18+
@Index(name = "idx_crm_status", columnList = "status")
19+
})
1820
@Data
1921
@Builder
2022
@NoArgsConstructor

server/src/main/java/com/incial/crm/repository/CrmEntryRepository.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,24 @@
22

33
import com.incial.crm.entity.CrmEntry;
44
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.data.jpa.repository.Query;
56
import org.springframework.stereotype.Repository;
67

8+
import java.util.List;
9+
710
@Repository
811
public interface CrmEntryRepository extends JpaRepository<CrmEntry, Long> {
12+
13+
// Query for onboarded companies (active registry entries)
14+
// Includes: onboarded, on progress, Quote Sent
15+
@Query("SELECT c FROM CrmEntry c WHERE LOWER(c.status) IN ('onboarded', 'on progress', 'quote sent')")
16+
List<CrmEntry> findOnboardedEntries();
17+
18+
// Query for completed companies
19+
@Query("SELECT c FROM CrmEntry c WHERE LOWER(c.status) = 'completed'")
20+
List<CrmEntry> findCompletedEntries();
21+
22+
// Query for dropped companies
23+
@Query("SELECT c FROM CrmEntry c WHERE LOWER(c.status) = 'drop'")
24+
List<CrmEntry> findDroppedEntries();
925
}

server/src/main/java/com/incial/crm/service/CrmService.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,27 @@ public Map<String, List<CrmEntryDto>> getAllEntries() {
2929
return response;
3030
}
3131

32+
public List<CrmEntryDto> getOnboardedEntries() {
33+
List<CrmEntry> entries = crmEntryRepository.findOnboardedEntries();
34+
return entries.stream()
35+
.map(this::convertToDto)
36+
.collect(Collectors.toList());
37+
}
38+
39+
public List<CrmEntryDto> getCompletedEntries() {
40+
List<CrmEntry> entries = crmEntryRepository.findCompletedEntries();
41+
return entries.stream()
42+
.map(this::convertToDto)
43+
.collect(Collectors.toList());
44+
}
45+
46+
public List<CrmEntryDto> getDroppedEntries() {
47+
List<CrmEntry> entries = crmEntryRepository.findDroppedEntries();
48+
return entries.stream()
49+
.map(this::convertToDto)
50+
.collect(Collectors.toList());
51+
}
52+
3253
public CrmEntryDto createEntry(CrmEntryDto dto) {
3354
CrmEntry entry = convertToEntity(dto);
3455
CrmEntry saved = crmEntryRepository.save(entry);

0 commit comments

Comments
 (0)