[SAAS-021] feat: Implement MLM module frontend
- 4 API services: structures, ranks, nodes, commissions - 30+ React Query hooks in useMlm.ts - My network/dashboard hooks for user experience - Tree visualization support hooks Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
69a6a2c4ea
commit
eff0f70b7f
@ -15,3 +15,4 @@ export * from './useMfa';
|
|||||||
export * from './useWhatsApp';
|
export * from './useWhatsApp';
|
||||||
export * from './usePortfolio';
|
export * from './usePortfolio';
|
||||||
export * from './useGoals';
|
export * from './useGoals';
|
||||||
|
export * from './useMlm';
|
||||||
|
|||||||
302
src/hooks/useMlm.ts
Normal file
302
src/hooks/useMlm.ts
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import {
|
||||||
|
structuresApi,
|
||||||
|
ranksApi,
|
||||||
|
nodesApi,
|
||||||
|
commissionsApi,
|
||||||
|
type StructureFilters,
|
||||||
|
type CreateStructureDto,
|
||||||
|
type UpdateStructureDto,
|
||||||
|
type RankFilters,
|
||||||
|
type CreateRankDto,
|
||||||
|
type UpdateRankDto,
|
||||||
|
type NodeFilters,
|
||||||
|
type CreateNodeDto,
|
||||||
|
type UpdateNodeDto,
|
||||||
|
type NodeStatus,
|
||||||
|
type CommissionFilters,
|
||||||
|
type CalculateCommissionsDto,
|
||||||
|
type CommissionStatus,
|
||||||
|
} from '@/services/mlm';
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Structures Hooks
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export function useStructures(filters?: StructureFilters) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['mlm', 'structures', filters],
|
||||||
|
queryFn: () => structuresApi.list(filters),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useStructure(id: string) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['mlm', 'structures', id],
|
||||||
|
queryFn: () => structuresApi.get(id),
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCreateStructure() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (data: CreateStructureDto) => structuresApi.create(data),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['mlm', 'structures'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateStructure() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id, data }: { id: string; data: UpdateStructureDto }) =>
|
||||||
|
structuresApi.update(id, data),
|
||||||
|
onSuccess: (_, { id }) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['mlm', 'structures'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['mlm', 'structures', id] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteStructure() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: string) => structuresApi.delete(id),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['mlm', 'structures'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Ranks Hooks
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export function useRanks(filters?: RankFilters) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['mlm', 'ranks', filters],
|
||||||
|
queryFn: () => ranksApi.list(filters),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRank(id: string) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['mlm', 'ranks', id],
|
||||||
|
queryFn: () => ranksApi.get(id),
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCreateRank() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (data: CreateRankDto) => ranksApi.create(data),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['mlm', 'ranks'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateRank() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id, data }: { id: string; data: UpdateRankDto }) =>
|
||||||
|
ranksApi.update(id, data),
|
||||||
|
onSuccess: (_, { id }) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['mlm', 'ranks'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['mlm', 'ranks', id] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteRank() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: string) => ranksApi.delete(id),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['mlm', 'ranks'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useEvaluateRanks() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (structureId: string) => ranksApi.evaluate(structureId),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['mlm', 'nodes'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Nodes Hooks
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export function useNodes(filters?: NodeFilters) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['mlm', 'nodes', filters],
|
||||||
|
queryFn: () => nodesApi.list(filters),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNode(id: string) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['mlm', 'nodes', id],
|
||||||
|
queryFn: () => nodesApi.get(id),
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCreateNode() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (data: CreateNodeDto) => nodesApi.create(data),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['mlm', 'nodes'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['mlm', 'my'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateNode() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id, data }: { id: string; data: UpdateNodeDto }) =>
|
||||||
|
nodesApi.update(id, data),
|
||||||
|
onSuccess: (_, { id }) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['mlm', 'nodes'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['mlm', 'nodes', id] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateNodeStatus() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id, status }: { id: string; status: NodeStatus }) =>
|
||||||
|
nodesApi.updateStatus(id, { status }),
|
||||||
|
onSuccess: (_, { id }) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['mlm', 'nodes'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['mlm', 'nodes', id] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNodeDownline(nodeId: string, maxDepth?: number) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['mlm', 'nodes', nodeId, 'downline', maxDepth],
|
||||||
|
queryFn: () => nodesApi.getDownline(nodeId, maxDepth),
|
||||||
|
enabled: !!nodeId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNodeUpline(nodeId: string) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['mlm', 'nodes', nodeId, 'upline'],
|
||||||
|
queryFn: () => nodesApi.getUpline(nodeId),
|
||||||
|
enabled: !!nodeId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNodeTree(nodeId: string, maxDepth?: number) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['mlm', 'nodes', nodeId, 'tree', maxDepth],
|
||||||
|
queryFn: () => nodesApi.getTree(nodeId, maxDepth),
|
||||||
|
enabled: !!nodeId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// My Network Hooks
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export function useMyDashboard() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['mlm', 'my', 'dashboard'],
|
||||||
|
queryFn: () => nodesApi.getMyDashboard(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMyNetwork(maxDepth?: number) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['mlm', 'my', 'network', maxDepth],
|
||||||
|
queryFn: () => nodesApi.getMyNetwork(maxDepth),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMyEarnings() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['mlm', 'my', 'earnings'],
|
||||||
|
queryFn: () => nodesApi.getMyEarnings(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMyRank() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['mlm', 'my', 'rank'],
|
||||||
|
queryFn: () => nodesApi.getMyRank(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGenerateInviteLink() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: () => nodesApi.generateInviteLink(),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['mlm', 'my'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Commissions Hooks
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export function useMLMCommissions(filters?: CommissionFilters) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['mlm', 'commissions', filters],
|
||||||
|
queryFn: () => commissionsApi.list(filters),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMLMCommission(id: string) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['mlm', 'commissions', id],
|
||||||
|
queryFn: () => commissionsApi.get(id),
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCalculateCommissions() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (data: CalculateCommissionsDto) => commissionsApi.calculate(data),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['mlm', 'commissions'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['mlm', 'nodes'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['mlm', 'my'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateCommissionStatus() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id, status }: { id: string; status: CommissionStatus }) =>
|
||||||
|
commissionsApi.updateStatus(id, { status }),
|
||||||
|
onSuccess: (_, { id }) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['mlm', 'commissions'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['mlm', 'commissions', id] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCommissionsByLevel(nodeId?: string) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['mlm', 'commissions', 'by-level', nodeId],
|
||||||
|
queryFn: () => commissionsApi.getByLevel(nodeId),
|
||||||
|
});
|
||||||
|
}
|
||||||
138
src/services/mlm/commissions.api.ts
Normal file
138
src/services/mlm/commissions.api.ts
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import api from '../api';
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Types
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
export type CommissionType = 'level' | 'matching' | 'infinity' | 'leadership' | 'pool';
|
||||||
|
export type CommissionStatus = 'pending' | 'approved' | 'paid' | 'cancelled';
|
||||||
|
export type BonusType = 'rank_achievement' | 'rank_maintenance' | 'fast_start' | 'pool_share';
|
||||||
|
|
||||||
|
export interface Commission {
|
||||||
|
id: string;
|
||||||
|
tenantId: string;
|
||||||
|
nodeId: string;
|
||||||
|
sourceNodeId: string;
|
||||||
|
type: CommissionType;
|
||||||
|
level: number;
|
||||||
|
sourceAmount: number;
|
||||||
|
rateApplied: number;
|
||||||
|
commissionAmount: number;
|
||||||
|
currency: string;
|
||||||
|
periodId: string | null;
|
||||||
|
sourceReference: string | null;
|
||||||
|
status: CommissionStatus;
|
||||||
|
paidAt: string | null;
|
||||||
|
notes: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
node?: {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
user?: {
|
||||||
|
email: string;
|
||||||
|
firstName: string | null;
|
||||||
|
lastName: string | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
sourceNode?: {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
user?: {
|
||||||
|
email: string;
|
||||||
|
firstName: string | null;
|
||||||
|
lastName: string | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CalculateCommissionsDto {
|
||||||
|
sourceNodeId: string;
|
||||||
|
amount: number;
|
||||||
|
currency?: string;
|
||||||
|
sourceReference?: string;
|
||||||
|
periodId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateCommissionStatusDto {
|
||||||
|
status: CommissionStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommissionFilters {
|
||||||
|
nodeId?: string;
|
||||||
|
sourceNodeId?: string;
|
||||||
|
type?: CommissionType;
|
||||||
|
level?: number;
|
||||||
|
status?: CommissionStatus;
|
||||||
|
periodId?: string;
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginatedCommissions {
|
||||||
|
items: Commission[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommissionsByLevel {
|
||||||
|
level: number;
|
||||||
|
count: number;
|
||||||
|
totalAmount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EarningsSummary {
|
||||||
|
totalCommissions: number;
|
||||||
|
totalBonuses: number;
|
||||||
|
totalEarnings: number;
|
||||||
|
pendingAmount: number;
|
||||||
|
paidAmount: number;
|
||||||
|
byLevel: CommissionsByLevel[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Bonus {
|
||||||
|
id: string;
|
||||||
|
tenantId: string;
|
||||||
|
nodeId: string;
|
||||||
|
rankId: string | null;
|
||||||
|
type: BonusType;
|
||||||
|
amount: number;
|
||||||
|
currency: string;
|
||||||
|
periodId: string | null;
|
||||||
|
status: CommissionStatus;
|
||||||
|
paidAt: string | null;
|
||||||
|
achievedAt: string;
|
||||||
|
notes: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// API Service
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const commissionsApi = {
|
||||||
|
list: async (params?: CommissionFilters): Promise<PaginatedCommissions> => {
|
||||||
|
const response = await api.get<PaginatedCommissions>('/mlm/commissions', { params });
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
get: async (id: string): Promise<Commission> => {
|
||||||
|
const response = await api.get<Commission>(`/mlm/commissions/${id}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
calculate: async (data: CalculateCommissionsDto): Promise<Commission[]> => {
|
||||||
|
const response = await api.post<Commission[]>('/mlm/commissions/calculate', data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
updateStatus: async (id: string, data: UpdateCommissionStatusDto): Promise<Commission> => {
|
||||||
|
const response = await api.patch<Commission>(`/mlm/commissions/${id}/status`, data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
getByLevel: async (nodeId?: string): Promise<CommissionsByLevel[]> => {
|
||||||
|
const response = await api.get<CommissionsByLevel[]>('/mlm/commissions/by-level', {
|
||||||
|
params: { nodeId },
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
};
|
||||||
4
src/services/mlm/index.ts
Normal file
4
src/services/mlm/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export * from './structures.api';
|
||||||
|
export * from './ranks.api';
|
||||||
|
export * from './nodes.api';
|
||||||
|
export * from './commissions.api';
|
||||||
208
src/services/mlm/nodes.api.ts
Normal file
208
src/services/mlm/nodes.api.ts
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
import api from '../api';
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Types
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
export type NodeStatus = 'pending' | 'active' | 'inactive' | 'suspended';
|
||||||
|
|
||||||
|
export interface Node {
|
||||||
|
id: string;
|
||||||
|
tenantId: string;
|
||||||
|
structureId: string;
|
||||||
|
userId: string;
|
||||||
|
parentId: string | null;
|
||||||
|
sponsorId: string | null;
|
||||||
|
position: number | null;
|
||||||
|
path: string | null;
|
||||||
|
depth: number;
|
||||||
|
rankId: string | null;
|
||||||
|
highestRankId: string | null;
|
||||||
|
personalVolume: number;
|
||||||
|
groupVolume: number;
|
||||||
|
directReferrals: number;
|
||||||
|
totalDownline: number;
|
||||||
|
totalEarnings: number;
|
||||||
|
status: NodeStatus;
|
||||||
|
joinedAt: string;
|
||||||
|
inviteCode: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
user?: {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
firstName: string | null;
|
||||||
|
lastName: string | null;
|
||||||
|
};
|
||||||
|
rank?: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
level: number;
|
||||||
|
color: string | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateNodeDto {
|
||||||
|
structureId: string;
|
||||||
|
userId: string;
|
||||||
|
parentId?: string;
|
||||||
|
sponsorId?: string;
|
||||||
|
position?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateNodeDto {
|
||||||
|
parentId?: string;
|
||||||
|
position?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateNodeStatusDto {
|
||||||
|
status: NodeStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NodeFilters {
|
||||||
|
structureId?: string;
|
||||||
|
parentId?: string;
|
||||||
|
sponsorId?: string;
|
||||||
|
status?: NodeStatus;
|
||||||
|
minDepth?: number;
|
||||||
|
maxDepth?: number;
|
||||||
|
search?: string;
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginatedNodes {
|
||||||
|
items: Node[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TreeNode {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
depth: number;
|
||||||
|
position: number | null;
|
||||||
|
personalVolume: number;
|
||||||
|
groupVolume: number;
|
||||||
|
directReferrals: number;
|
||||||
|
status: NodeStatus;
|
||||||
|
user?: {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
firstName: string | null;
|
||||||
|
lastName: string | null;
|
||||||
|
};
|
||||||
|
rank?: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
level: number;
|
||||||
|
color: string | null;
|
||||||
|
badgeUrl: string | null;
|
||||||
|
};
|
||||||
|
children: TreeNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InviteLink {
|
||||||
|
inviteCode: string;
|
||||||
|
inviteUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MyNetworkSummary {
|
||||||
|
totalDownline: number;
|
||||||
|
directReferrals: number;
|
||||||
|
activeDownline: number;
|
||||||
|
personalVolume: number;
|
||||||
|
groupVolume: number;
|
||||||
|
totalEarnings: number;
|
||||||
|
currentRank: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
level: number;
|
||||||
|
} | null;
|
||||||
|
nextRank: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
level: number;
|
||||||
|
requirements: Record<string, unknown>;
|
||||||
|
progress: Record<string, number>;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// API Service
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const nodesApi = {
|
||||||
|
// Nodes CRUD
|
||||||
|
list: async (params?: NodeFilters): Promise<PaginatedNodes> => {
|
||||||
|
const response = await api.get<PaginatedNodes>('/mlm/nodes', { params });
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
get: async (id: string): Promise<Node> => {
|
||||||
|
const response = await api.get<Node>(`/mlm/nodes/${id}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
create: async (data: CreateNodeDto): Promise<Node> => {
|
||||||
|
const response = await api.post<Node>('/mlm/nodes', data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
update: async (id: string, data: UpdateNodeDto): Promise<Node> => {
|
||||||
|
const response = await api.patch<Node>(`/mlm/nodes/${id}`, data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
updateStatus: async (id: string, data: UpdateNodeStatusDto): Promise<Node> => {
|
||||||
|
const response = await api.patch<Node>(`/mlm/nodes/${id}/status`, data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Network navigation
|
||||||
|
getDownline: async (nodeId: string, maxDepth?: number): Promise<Node[]> => {
|
||||||
|
const response = await api.get<Node[]>(`/mlm/nodes/${nodeId}/downline`, {
|
||||||
|
params: { maxDepth },
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
getUpline: async (nodeId: string): Promise<Node[]> => {
|
||||||
|
const response = await api.get<Node[]>(`/mlm/nodes/${nodeId}/upline`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
getTree: async (nodeId: string, maxDepth?: number): Promise<TreeNode> => {
|
||||||
|
const response = await api.get<TreeNode>(`/mlm/nodes/${nodeId}/tree`, {
|
||||||
|
params: { maxDepth },
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// My Network
|
||||||
|
getMyDashboard: async (): Promise<MyNetworkSummary> => {
|
||||||
|
const response = await api.get<MyNetworkSummary>('/mlm/my/dashboard');
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
getMyNetwork: async (maxDepth?: number): Promise<TreeNode | null> => {
|
||||||
|
const response = await api.get<TreeNode | null>('/mlm/my/network', {
|
||||||
|
params: { maxDepth },
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
getMyEarnings: async () => {
|
||||||
|
const response = await api.get('/mlm/my/earnings');
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
getMyRank: async () => {
|
||||||
|
const response = await api.get('/mlm/my/rank');
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
generateInviteLink: async (): Promise<InviteLink> => {
|
||||||
|
const response = await api.post<InviteLink>('/mlm/my/invite');
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
};
|
||||||
94
src/services/mlm/ranks.api.ts
Normal file
94
src/services/mlm/ranks.api.ts
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import api from '../api';
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Types
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface RankRequirements {
|
||||||
|
personalVolume?: number;
|
||||||
|
groupVolume?: number;
|
||||||
|
directReferrals?: number;
|
||||||
|
activeLegs?: number;
|
||||||
|
rankInLegs?: {
|
||||||
|
rankLevel: number;
|
||||||
|
count: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RankBenefits {
|
||||||
|
discount?: number;
|
||||||
|
access?: string[];
|
||||||
|
features?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Rank {
|
||||||
|
id: string;
|
||||||
|
tenantId: string;
|
||||||
|
structureId: string;
|
||||||
|
name: string;
|
||||||
|
level: number;
|
||||||
|
badgeUrl: string | null;
|
||||||
|
color: string | null;
|
||||||
|
requirements: RankRequirements;
|
||||||
|
bonusRate: number | null;
|
||||||
|
benefits: RankBenefits;
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateRankDto {
|
||||||
|
structureId: string;
|
||||||
|
name: string;
|
||||||
|
level: number;
|
||||||
|
badgeUrl?: string;
|
||||||
|
color?: string;
|
||||||
|
requirements?: RankRequirements;
|
||||||
|
bonusRate?: number;
|
||||||
|
benefits?: RankBenefits;
|
||||||
|
isActive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UpdateRankDto = Partial<CreateRankDto>;
|
||||||
|
|
||||||
|
export interface RankFilters {
|
||||||
|
structureId?: string;
|
||||||
|
isActive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// API Service
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const ranksApi = {
|
||||||
|
list: async (params?: RankFilters): Promise<Rank[]> => {
|
||||||
|
const response = await api.get<Rank[]>('/mlm/ranks', { params });
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
get: async (id: string): Promise<Rank> => {
|
||||||
|
const response = await api.get<Rank>(`/mlm/ranks/${id}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
create: async (data: CreateRankDto): Promise<Rank> => {
|
||||||
|
const response = await api.post<Rank>('/mlm/ranks', data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
update: async (id: string, data: UpdateRankDto): Promise<Rank> => {
|
||||||
|
const response = await api.patch<Rank>(`/mlm/ranks/${id}`, data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
delete: async (id: string): Promise<void> => {
|
||||||
|
await api.delete(`/mlm/ranks/${id}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
evaluate: async (structureId: string): Promise<number> => {
|
||||||
|
const response = await api.post<number>('/mlm/ranks/evaluate', null, {
|
||||||
|
params: { structureId },
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
};
|
||||||
82
src/services/mlm/structures.api.ts
Normal file
82
src/services/mlm/structures.api.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import api from '../api';
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Types
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
export type StructureType = 'unilevel' | 'binary' | 'matrix' | 'hybrid';
|
||||||
|
|
||||||
|
export interface LevelRate {
|
||||||
|
level: number;
|
||||||
|
rate: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StructureConfig {
|
||||||
|
maxWidth?: number | null;
|
||||||
|
maxDepth?: number;
|
||||||
|
spillover?: 'left_first' | 'weak_leg' | 'balanced';
|
||||||
|
width?: number;
|
||||||
|
depth?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Structure {
|
||||||
|
id: string;
|
||||||
|
tenantId: string;
|
||||||
|
name: string;
|
||||||
|
description: string | null;
|
||||||
|
type: StructureType;
|
||||||
|
config: StructureConfig;
|
||||||
|
levelRates: LevelRate[];
|
||||||
|
matchingRates: LevelRate[];
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateStructureDto {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
type: StructureType;
|
||||||
|
config?: StructureConfig;
|
||||||
|
levelRates?: LevelRate[];
|
||||||
|
matchingRates?: LevelRate[];
|
||||||
|
isActive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UpdateStructureDto = Partial<CreateStructureDto>;
|
||||||
|
|
||||||
|
export interface StructureFilters {
|
||||||
|
type?: StructureType;
|
||||||
|
isActive?: boolean;
|
||||||
|
search?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// API Service
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const structuresApi = {
|
||||||
|
list: async (params?: StructureFilters): Promise<Structure[]> => {
|
||||||
|
const response = await api.get<Structure[]>('/mlm/structures', { params });
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
get: async (id: string): Promise<Structure> => {
|
||||||
|
const response = await api.get<Structure>(`/mlm/structures/${id}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
create: async (data: CreateStructureDto): Promise<Structure> => {
|
||||||
|
const response = await api.post<Structure>('/mlm/structures', data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
update: async (id: string, data: UpdateStructureDto): Promise<Structure> => {
|
||||||
|
const response = await api.patch<Structure>(`/mlm/structures/${id}`, data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
delete: async (id: string): Promise<void> => {
|
||||||
|
await api.delete(`/mlm/structures/${id}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user