Subversion Repositories SmartDukaan

Rev

Rev 37519 | Rev 37586 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
33917 ranu 1
package com.spice.profitmandi.service;
2
 
35631 ranu 3
import com.spice.profitmandi.common.enumuration.ActivationType;
4
import com.spice.profitmandi.common.exception.ProfitMandiBusinessException;
5
import com.spice.profitmandi.common.model.CustomRetailer;
33917 ranu 6
import com.spice.profitmandi.common.model.ProfitMandiConstants;
35631 ranu 7
import com.spice.profitmandi.dao.entity.auth.AuthUser;
8
import com.spice.profitmandi.dao.entity.auth.PartnerCollectionRemark;
9
import com.spice.profitmandi.dao.entity.auth.RbmCallSequenceLog;
35761 ranu 10
import com.spice.profitmandi.dao.entity.cs.AgentCallLog;
35631 ranu 11
import com.spice.profitmandi.dao.entity.cs.Position;
12
import com.spice.profitmandi.dao.entity.cs.Ticket;
13
import com.spice.profitmandi.dao.entity.fofo.FofoStore;
34397 ranu 14
import com.spice.profitmandi.dao.entity.fofo.MonthlyTarget;
35759 ranu 15
import com.spice.profitmandi.dao.entity.fofo.RetailerContact;
33985 ranu 16
import com.spice.profitmandi.dao.entity.inventory.RbmAchievements;
33926 ranu 17
import com.spice.profitmandi.dao.entity.inventory.RbmTargets;
37497 ranu 18
import com.spice.profitmandi.dao.entity.inventory.SalesAchievements;
19
import com.spice.profitmandi.dao.entity.inventory.SalesTargets;
35759 ranu 20
import com.spice.profitmandi.dao.entity.user.Address;
35631 ranu 21
import com.spice.profitmandi.dao.enumuration.auth.CollectionRemark;
22
import com.spice.profitmandi.dao.enumuration.cs.EscalationType;
33917 ranu 23
import com.spice.profitmandi.dao.model.*;
35631 ranu 24
import com.spice.profitmandi.dao.repository.auth.AuthRepository;
25
import com.spice.profitmandi.dao.repository.auth.PartnerCollectionRemarkRepository;
26
import com.spice.profitmandi.dao.repository.auth.RbmCallSequenceLogRepository;
33985 ranu 27
import com.spice.profitmandi.dao.repository.catalog.RbmAchievementsRepository;
33926 ranu 28
import com.spice.profitmandi.dao.repository.catalog.RbmTargetsRepository;
37497 ranu 29
import com.spice.profitmandi.dao.repository.catalog.SalesAchievementsRepository;
30
import com.spice.profitmandi.dao.repository.catalog.SalesTargetsRepository;
35631 ranu 31
import com.spice.profitmandi.dao.repository.cs.CsService;
32
import com.spice.profitmandi.dao.repository.cs.PositionRepository;
33
import com.spice.profitmandi.dao.repository.dtr.FofoStoreRepository;
35759 ranu 34
import com.spice.profitmandi.dao.repository.dtr.RetailerContactRepository;
34397 ranu 35
import com.spice.profitmandi.dao.repository.fofo.MonthlyTargetRepository;
34880 ranu 36
import com.spice.profitmandi.dao.repository.logistics.PublicHolidaysRepository;
35631 ranu 37
import com.spice.profitmandi.dao.repository.transaction.LoanRepository;
34397 ranu 38
import com.spice.profitmandi.dao.repository.transaction.OrderRepository;
35759 ranu 39
import com.spice.profitmandi.dao.repository.user.AddressRepository;
35631 ranu 40
import com.spice.profitmandi.service.user.RetailerService;
33917 ranu 41
import org.apache.logging.log4j.LogManager;
42
import org.apache.logging.log4j.Logger;
43
import org.hibernate.Session;
44
import org.hibernate.SessionFactory;
45
import org.hibernate.query.NativeQuery;
46
import org.springframework.beans.factory.annotation.Autowired;
36334 ranu 47
import org.springframework.cache.annotation.Cacheable;
33917 ranu 48
import org.springframework.stereotype.Component;
49
 
50
import javax.persistence.TypedQuery;
34880 ranu 51
import java.time.*;
35631 ranu 52
import java.time.format.DateTimeFormatter;
34880 ranu 53
import java.time.temporal.ChronoUnit;
35631 ranu 54
import java.util.*;
33997 ranu 55
import java.util.stream.Collectors;
33917 ranu 56
 
57
@Component
58
public class RbmTargetServiceImpl implements RbmTargetService {
59
    private static final Logger LOGGER = LogManager.getLogger(RbmTargetServiceImpl.class);
60
 
61
    @Autowired
62
    SessionFactory sessionFactory;
63
 
33926 ranu 64
    @Autowired
65
    RbmTargetsRepository rbmTargetsRepository;
33917 ranu 66
 
33985 ranu 67
    @Autowired
37497 ranu 68
    SalesTargetsRepository salesTargetsRepository;
69
 
70
    @Autowired
71
    SalesAchievementsRepository salesAchievementsRepository;
72
 
73
    @Autowired
74
    AuthService authService;
75
 
76
    @Autowired
33985 ranu 77
    RbmAchievementsRepository rbmAchievementsRepository;
78
 
34397 ranu 79
    @Autowired
80
    MonthlyTargetRepository monthlyTargetRepository;
81
 
34880 ranu 82
    @Autowired
83
    PublicHolidaysRepository publicHolidaysRepository;
84
 
35631 ranu 85
    @Autowired
86
    RetailerService retailerService;
87
 
33917 ranu 88
    @Override
89
    public List<WarehouseRbmTargetModel> getWarehouseWiseRbmMonthlyTarget() {
90
        Session session = sessionFactory.getCurrentSession();
91
        final TypedQuery<WarehouseRbmTargetModel> typedQuerySimilar = session.createNamedQuery("RbmTarget.getWarehouseWiseMonthlyTarget", WarehouseRbmTargetModel.class);
92
 
93
        return typedQuerySimilar.getResultList();
94
 
95
    }
96
 
97
    @Override
98
    public List<MTDAchievedTargetModel> getDateWiseAchievedTargetOfRbm(LocalDate startDate, LocalDate endDate) {
99
        Session session = sessionFactory.getCurrentSession();
100
        final TypedQuery<MTDAchievedTargetModel> typedQuerySimilar = session.createNamedQuery("RbmTarget.getRbmAchievedMonthlyTarget", MTDAchievedTargetModel.class);
101
        typedQuerySimilar.setParameter("startDate", startDate);
102
        typedQuerySimilar.setParameter("endDate", endDate);
103
        return typedQuerySimilar.getResultList();
104
 
105
    }
106
 
107
    @Override
34055 ranu 108
    public List<TodayAchievedMovementModel> getMovementWiseAchievementByDate(LocalDate startDate, LocalDate endDate) {
33917 ranu 109
        LOGGER.info("start date {}, end date {}", startDate, endDate);
110
        Session session = sessionFactory.getCurrentSession();
34055 ranu 111
        final TypedQuery<TodayAchievedMovementModel> typedQuerySimilar = session.createNamedQuery("RBMTarget.TodayAchivementByMovement", TodayAchievedMovementModel.class);
33917 ranu 112
        typedQuerySimilar.setParameter("startDate", startDate);
113
        typedQuerySimilar.setParameter("endDate", endDate);
114
        return typedQuerySimilar.getResultList();
115
 
116
    }
117
 
118
    @Override
37442 ranu 119
    public List<RbmBrandSoldModel> getBrandsSoldByRbm(int authId, int warehouseId, LocalDate startDate, LocalDate endDate) {
120
        Session session = sessionFactory.getCurrentSession();
121
        final TypedQuery<RbmBrandSoldModel> q = session.createNamedQuery("RbmDrr.BrandSoldByRbm", RbmBrandSoldModel.class);
122
        q.setParameter("authId", authId);
123
        q.setParameter("warehouseId", warehouseId);
124
        q.setParameter("startDate", startDate);
125
        q.setParameter("endDate", endDate);
126
        return q.getResultList();
127
    }
128
 
129
    @Override
37460 ranu 130
    public List<RbmBrandPartnerCountModel> getBrandPartnerCountByRbm(int authId, int warehouseId, LocalDate startDate, LocalDate endDate) {
131
        Session session = sessionFactory.getCurrentSession();
132
        final TypedQuery<RbmBrandPartnerCountModel> q = session.createNamedQuery("RbmDrr.BrandPartnerCountByRbm", RbmBrandPartnerCountModel.class);
133
        q.setParameter("authId", authId);
134
        q.setParameter("warehouseId", warehouseId);
135
        q.setParameter("startDate", startDate);
136
        q.setParameter("endDate", endDate);
137
        return q.getResultList();
138
    }
139
 
140
    @Override
37487 ranu 141
    public List<WarehouseBuyingByMovementModel> getWarehouseBuyingByMovement(int warehouseId, LocalDate startDate, LocalDate endDate) {
142
        Session session = sessionFactory.getCurrentSession();
143
        final TypedQuery<WarehouseBuyingByMovementModel> q = session.createNamedQuery("RbmDrr.WarehouseBuyingByMovement", WarehouseBuyingByMovementModel.class);
144
        q.setParameter("warehouseId", warehouseId);
145
        q.setParameter("startDate", startDate);
146
        q.setParameter("endDate", endDate);
147
        return q.getResultList();
148
    }
149
 
150
    @Override
33917 ranu 151
    public List<WarehouseMobileStockByMovementModel> getWarehouseMobileStockByMovement() {
152
        Session session = sessionFactory.getCurrentSession();
153
        final TypedQuery<WarehouseMobileStockByMovementModel> typedQuerySimilar = session.createNamedQuery("WarehouseStock.MovementWiseMobileStock", WarehouseMobileStockByMovementModel.class);
154
 
155
        return typedQuerySimilar.getResultList();
156
 
157
    }
158
 
33991 ranu 159
    @Override
160
    public List<SoldCatalogsReportModel> getCatalogSoldReport(LocalDate startDate, LocalDate endDate) {
161
        Session session = sessionFactory.getCurrentSession();
162
        final TypedQuery<SoldCatalogsReportModel> typedQuerySimilar = session.createNamedQuery("CatalogsReport.SoldCatalogsReport", SoldCatalogsReportModel.class);
163
        typedQuerySimilar.setParameter("startDate", startDate);
164
        typedQuerySimilar.setParameter("endDate", endDate);
165
        return typedQuerySimilar.getResultList();
33917 ranu 166
 
33991 ranu 167
    }
168
 
169
 
33917 ranu 170
    public int getWorkingDaysCount(LocalDate startDate) {
171
        Session session = sessionFactory.getCurrentSession();
172
 
173
        // Convert the LocalDate to a format MySQL can interpret
174
        String startDateString = startDate.toString();
175
 
176
        final NativeQuery<?> nativeQuery = session.createNativeQuery(
177
                "SELECT (DATEDIFF(LAST_DAY(:startDate), :startDate) + 1) " +
178
                        " - (FLOOR((DATEDIFF(LAST_DAY(:startDate), :startDate) + (WEEKDAY(:startDate) + 1)) / 7)) " +
179
                        " - (SELECT COUNT(*) " +
180
                        " FROM logistics.publicholidays " +
181
                        " WHERE date BETWEEN :startDate AND LAST_DAY(:startDate) " +
182
                        " AND WEEKDAY(date) != 6) AS working_days"
183
        );
184
 
185
        // Set the start date parameter for each placeholder
186
        nativeQuery.setParameter("startDate", startDateString);
187
 
188
        Object result = nativeQuery.getSingleResult();
189
        return result != null ? ((Number) result).intValue() : 0;
190
    }
191
 
192
 
35044 ranu 193
 
33917 ranu 194
    @Override
195
    public List<RbmArrViewModel> getRbmTodayArr() throws Exception {
196
        LocalDate todayDate = LocalDate.now();
197
        return getRbmTodayArr(todayDate);
198
    }
199
 
200
    @Override
33997 ranu 201
    public List<RbmTargetAndAchievementsModel> getRbmTargetsAndAchievemnts(LocalDate startDate, LocalDate endDate) {
202
 
34002 ranu 203
        List<RbmTargetsModel> rbmTargetsList = rbmTargetsRepository.selectTargetsModelListByDates(startDate.atStartOfDay(), endDate.atTime(LocalTime.MAX));
33997 ranu 204
 
205
        LOGGER.info("rbmTargetsList {}", rbmTargetsList);
206
        // Group Targtes by RBM and Warehouse
34002 ranu 207
        Map<String, RbmTargetsModel> targetsMap = rbmTargetsList.stream()
33997 ranu 208
                .collect(Collectors.toMap(
209
                        a -> a.getRbmAuthId() + "-" + a.getWarehouseId(),
210
                        a -> a,
211
                        (a1, a2) -> mergeTargets(a1, a2) // Handle duplicates by merging
212
                ));
213
 
214
 
34002 ranu 215
        List<RbmAchievementsModel> rbmAchievements = rbmAchievementsRepository.selectAchievementsModelListByDates(startDate.atStartOfDay(), endDate.atTime(LocalTime.MAX));
33997 ranu 216
        LOGGER.info("rbmTargetsList {}", rbmAchievements);
217
        // Group achievements by RBM and Warehouse
34002 ranu 218
        Map<String, RbmAchievementsModel> achievementMap = rbmAchievements.stream()
33997 ranu 219
                .collect(Collectors.toMap(
220
                        a -> a.getRbmAuthId() + "-" + a.getWarehouseId(),
221
                        a -> a,
222
                        (a1, a2) -> mergeAchievements(a1, a2) // Handle duplicates by merging
223
                ));
224
 
225
        return targetsMap.keySet().stream()
226
                .map(key -> {
227
                    String[] parts = key.split("-");
228
                    int rbmAuthId = Integer.parseInt(parts[0]);
229
                    int warehouseId = Integer.parseInt(parts[1]);
230
 
34002 ranu 231
                    RbmTargetsModel target = targetsMap.get(key);
232
                    RbmAchievementsModel achievement = achievementMap.getOrDefault(key, new RbmAchievementsModel());
33997 ranu 233
 
234
                    RbmTargetAndAchievementsModel model = new RbmTargetAndAchievementsModel();
235
                    model.setAuthId(rbmAuthId);
236
                    model.setRbmName(target.getRbmName());
237
                    model.setWarehouseName(ProfitMandiConstants.WAREHOUSE_MAP.getOrDefault(warehouseId, "Unknown"));
238
 
239
                    // Set target values
34006 ranu 240
                    model.setHidTarget((long) target.getHidTarget());
34098 ranu 241
                    model.setRunningTarget((long) target.getRunningTarget());
34006 ranu 242
                    model.setFastMovingTarget((long) target.getFastMovingTarget());
243
                    model.setSlowMovingTarget((long) target.getSlowMovingTarget());
244
                    model.setOtherMovingTarget((long) target.getOtherTarget());
33997 ranu 245
 
246
                    // Set achievement values
34006 ranu 247
                    model.setAchievedHid((long) achievement.getAchievedHidTarget());
34098 ranu 248
                    model.setAchievedRunning((long) achievement.getAchievedRunningTarget());
34006 ranu 249
                    model.setAchievedFastMoving((long) achievement.getAchievedFastMovingTarget());
250
                    model.setAchievedSlowMoving((long) achievement.getAchievedSlowMovingTarget());
251
                    model.setAchievedOtherMoving((long) achievement.getAchievedOtherTarget());
33997 ranu 252
 
253
                    model.setTotalTarget(
34006 ranu 254
                            (long) target.getHidTarget() +
34098 ranu 255
                                    (long) target.getRunningTarget() +
34006 ranu 256
                                    (long) target.getFastMovingTarget() +
257
                                    (long) target.getSlowMovingTarget() +
258
                                    (long) target.getOtherTarget()
33997 ranu 259
                    );
260
                    model.setTotalAchievemnt(
34006 ranu 261
                            (long) achievement.getAchievedHidTarget() +
34098 ranu 262
                                    (long) achievement.getAchievedRunningTarget() +
34006 ranu 263
                                    (long) achievement.getAchievedFastMovingTarget() +
264
                                    (long) achievement.getAchievedSlowMovingTarget() +
265
                                    (long) achievement.getAchievedOtherTarget()
33997 ranu 266
                    );
267
 
268
                    return model;
269
                })
270
                .collect(Collectors.toList());
271
 
272
    }
273
 
34002 ranu 274
    private RbmTargetsModel mergeTargets(RbmTargetsModel a1, RbmTargetsModel a2) {
33997 ranu 275
 
276
        // Merge logic for achievements (aggregate the target and achieved values)
34006 ranu 277
        a1.setHidTarget((a1.getHidTarget()) +
278
                (a2.getHidTarget()));
33997 ranu 279
 
34006 ranu 280
        a1.setFastMovingTarget((a1.getFastMovingTarget()) +
281
                (a2.getFastMovingTarget()));
33997 ranu 282
 
34006 ranu 283
        a1.setSlowMovingTarget((a1.getSlowMovingTarget()) +
284
                (a2.getSlowMovingTarget()));
33997 ranu 285
 
34098 ranu 286
        a1.setRunningTarget((a1.getRunningTarget()) +
287
                (a2.getRunningTarget()));
33997 ranu 288
 
34006 ranu 289
        a1.setOtherTarget((a1.getOtherTarget()) +
290
                (a2.getOtherTarget()));
33997 ranu 291
        return a1;
292
    }
293
 
34002 ranu 294
    private RbmAchievementsModel mergeAchievements(RbmAchievementsModel a1, RbmAchievementsModel a2) {
33997 ranu 295
        // Merge logic for achievements (aggregate the target and achieved values)
34006 ranu 296
        a1.setAchievedHidTarget((a1.getAchievedHidTarget()) +
297
                (a2.getAchievedHidTarget()));
33997 ranu 298
 
34098 ranu 299
        a1.setAchievedRunningTarget((a1.getAchievedRunningTarget()) +
300
                (a2.getAchievedRunningTarget()));
33997 ranu 301
 
34006 ranu 302
        a1.setAchievedFastMovingTarget((a1.getAchievedFastMovingTarget()) +
303
                (a2.getAchievedFastMovingTarget()));
33997 ranu 304
 
34006 ranu 305
        a1.setAchievedSlowMovingTarget((a1.getAchievedSlowMovingTarget()) +
306
                (a2.getAchievedSlowMovingTarget()));
33997 ranu 307
 
34006 ranu 308
        a1.setAchievedOtherTarget((a1.getAchievedOtherTarget()) +
309
                (a2.getAchievedOtherTarget()));
33997 ranu 310
 
311
        return a1;
312
    }
34002 ranu 313
 
33997 ranu 314
    @Override
33917 ranu 315
    public List<RbmArrViewModel> getRbmTodayArr(LocalDate todayDate) throws Exception {
316
 
317
        LocalDate startDateOfMonthDay1 = LocalDate.now().withDayOfMonth(1);
318
 
34289 ranu 319
        List<WarehouseRbmTargetModel> warehouseRbmTargetModelList = this.getWarehouseWiseRbmMonthlyTarget();
320
        List<WarehouseRbmTargetModel> warehouseRbmTargetModels = warehouseRbmTargetModelList.stream().filter(x -> x.getMonthlyTarget() > 0).collect(Collectors.toList());
321
        LOGGER.info("warehouseRbmTargetModels {}", warehouseRbmTargetModels);
322
        List<TodayAchievedMovementModel> todayAchievedMovementModels = getMovementWiseAchievementByDate(todayDate, todayDate.plusDays(1));
33917 ranu 323
 
324
        List<MTDAchievedTargetModel> mtdAchievedTargetModels = getDateWiseAchievedTargetOfRbm(startDateOfMonthDay1, todayDate);
325
 
35044 ranu 326
        int remainingWorkingDaysCount = (int) getRemainingDaysInMonth(todayDate);
33917 ranu 327
 
33926 ranu 328
        List<RbmTargets> todayRbmTargetsList = rbmTargetsRepository.selectTargetsByDates(todayDate.atStartOfDay(), todayDate.atTime(LocalTime.MAX));
33917 ranu 329
 
33926 ranu 330
        LOGGER.info("todayRbmTargetsList {}", todayRbmTargetsList);
33917 ranu 331
 
332
        List<RbmArrViewModel> rbmArrViewModels = new ArrayList<>();
333
 
34028 ranu 334
        if (!todayRbmTargetsList.isEmpty()) {
33917 ranu 335
 
35454 amit 336
            // OPTIMIZED: Pre-build maps for O(1) lookup instead of O(n) filter in each iteration
337
            // Map key: "authId-warehouseId"
338
            Map<String, Double> mtdAchievedMap = mtdAchievedTargetModels.stream()
339
                    .collect(Collectors.groupingBy(
340
                            x -> x.getAuthId() + "-" + x.getWarehouseId(),
341
                            Collectors.summingDouble(MTDAchievedTargetModel::getAcheivedMonthlyTarget)
342
                    ));
343
 
344
            Map<String, TodayAchievedMovementModel> todayAchievedMap = todayAchievedMovementModels.stream()
345
                    .collect(Collectors.toMap(
346
                            x -> x.getAuthId() + "-" + x.getWarehouseId(),
347
                            x -> x,
348
                            (a, b) -> a
349
                    ));
350
 
351
            Map<String, RbmTargets> todayRbmTargetsMap = todayRbmTargetsList.stream()
352
                    .collect(Collectors.toMap(
353
                            x -> x.getRbmAuthId() + "-" + x.getWarehouseId(),
354
                            x -> x,
355
                            (a, b) -> a
356
                    ));
357
 
34028 ranu 358
            for (WarehouseRbmTargetModel rbmTarget : warehouseRbmTargetModels) {
33917 ranu 359
 
35454 amit 360
                String lookupKey = rbmTarget.getAuthId() + "-" + rbmTarget.getWarehouseId();
361
 
34028 ranu 362
                float monthlyTarget = rbmTarget.getMonthlyTarget();
35454 amit 363
                float achievedSoFar = mtdAchievedMap.getOrDefault(lookupKey, 0.0).floatValue();
33917 ranu 364
 
34028 ranu 365
                float remainingTarget = monthlyTarget - achievedSoFar;
33953 ranu 366
 
34028 ranu 367
                float todayTarget = (remainingWorkingDaysCount > 0 && remainingTarget > 0) ? remainingTarget / remainingWorkingDaysCount : 0;
368
 
33926 ranu 369
                String warehouseName = ProfitMandiConstants.WAREHOUSE_MAP.getOrDefault(rbmTarget.getWarehouseId(), "Unknown");
33917 ranu 370
 
34288 ranu 371
                LOGGER.info("rbmTarget ==== {}", rbmTarget);
34281 ranu 372
 
35454 amit 373
                TodayAchievedMovementModel todayAchievedMovementModel = todayAchievedMap.get(lookupKey);
34283 ranu 374
 
35454 amit 375
                RbmTargets todayRbmTargets = todayRbmTargetsMap.get(lookupKey);
34285 ranu 376
 
35454 amit 377
                if (todayRbmTargets != null) {
34034 ranu 378
                    LOGGER.info("todayRbmTargets {}", todayRbmTargets);
379
                    RbmArrViewModel viewModel = new RbmArrViewModel();
33917 ranu 380
 
34034 ranu 381
                    viewModel.setAuthId(rbmTarget.getAuthId());
382
                    viewModel.setRbmName(rbmTarget.getRbmName());
383
                    viewModel.setWarehouseName(warehouseName);
384
                    viewModel.setTodayTarget(Math.round(todayTarget));
385
                    viewModel.setMonthlyTarget(Math.round(monthlyTarget));
386
                    viewModel.setMtdAchievedTarget(Math.round(achievedSoFar));
33926 ranu 387
 
34034 ranu 388
                    viewModel.setTodayHidTarget(Math.round((todayRbmTargets.getHidTarget())));
389
                    viewModel.setTodayFastMovingTarget(Math.round(todayRbmTargets.getFastMovingTarget()));
34098 ranu 390
                    viewModel.setTodaySlowMovingTarget(Math.round(todayRbmTargets.getSlowMovingtarget()));
391
                    viewModel.setTodayRunningTarget(Math.round(todayRbmTargets.getRunningtarget()));
34034 ranu 392
                    viewModel.setTodayOtherMovingTarget(Math.round(todayRbmTargets.getOtherTarget()));
33926 ranu 393
 
34283 ranu 394
                    if (todayAchievedMovementModel != null) {
395
                        viewModel.setTodayAchievedHidTarget(Math.round(todayAchievedMovementModel.getHidBilled()));
396
                        viewModel.setTodayAchievedFastMovingTarget(Math.round(todayAchievedMovementModel.getFastMovingBilled()));
397
                        viewModel.setTodayAchievedSlowMovingTarget(Math.round(todayAchievedMovementModel.getSlowMovinBilled()));
398
                        viewModel.setTodayAchievedRunningTarget(Math.round(todayAchievedMovementModel.getRunningBilled()));
399
                        viewModel.setTodayAchievedOtherMovingTarget(Math.round(todayAchievedMovementModel.getOtherBilled()));
400
                        viewModel.setTotalAchievedTarget(Math.round(todayAchievedMovementModel.getHidBilled() + todayAchievedMovementModel.getFastMovingBilled() + todayAchievedMovementModel.getSlowMovinBilled() + todayAchievedMovementModel.getRunningBilled() + todayAchievedMovementModel.getOtherBilled()));
401
                    } else {
402
                        viewModel.setTodayAchievedHidTarget(0);
403
                        viewModel.setTodayAchievedFastMovingTarget(0);
404
                        viewModel.setTodayAchievedSlowMovingTarget(0);
405
                        viewModel.setTodayAchievedRunningTarget(0);
406
                        viewModel.setTodayAchievedOtherMovingTarget(0);
407
                        viewModel.setTotalAchievedTarget(0);
408
                    }
34034 ranu 409
                    rbmArrViewModels.add(viewModel);
410
                } else {
411
                    LOGGER.info("No matching RbmTargets found for AuthId: {} and rbmname {} and WarehouseId: {}", rbmTarget.getAuthId(), rbmTarget.getRbmName(), rbmTarget.getWarehouseId());
412
                }
33917 ranu 413
 
34034 ranu 414
 
415
 
34028 ranu 416
            }
33917 ranu 417
        }
418
 
419
        LOGGER.info("rbmArrViewModels {}", rbmArrViewModels);
420
        return rbmArrViewModels;
421
    }
422
 
33926 ranu 423
    @Override
424
    public void setMovementWiseRbmTargets() {
425
        LocalDate todayDate = LocalDate.now();
33917 ranu 426
 
33926 ranu 427
        LocalDate startDateOfMonthDay1 = LocalDate.now().withDayOfMonth(1);
428
 
429
        List<WarehouseRbmTargetModel> warehouseRbmTargetModels = this.getWarehouseWiseRbmMonthlyTarget();
430
 
431
        List<MTDAchievedTargetModel> mtdAchievedTargetModels = getDateWiseAchievedTargetOfRbm(startDateOfMonthDay1, todayDate);
432
 
433
 
35044 ranu 434
        int remainingWorkingDaysCount = (int) getRemainingDaysInMonth(todayDate);
33926 ranu 435
 
436
        List<WarehouseMobileStockByMovementModel> warehouseMobileStockByMovementModels = getWarehouseMobileStockByMovement();
437
 
35454 amit 438
        // OPTIMIZED: Pre-build maps for O(1) lookup instead of O(n) filter in each iteration
439
        Map<String, Double> mtdAchievedMap = mtdAchievedTargetModels.stream()
440
                .collect(Collectors.groupingBy(
441
                        x -> x.getAuthId() + "-" + x.getWarehouseId(),
442
                        Collectors.summingDouble(MTDAchievedTargetModel::getAcheivedMonthlyTarget)
443
                ));
444
 
445
        Map<Integer, WarehouseMobileStockByMovementModel> warehouseStockMap = warehouseMobileStockByMovementModels.stream()
446
                .collect(Collectors.toMap(
447
                        WarehouseMobileStockByMovementModel::getWarehouseId,
448
                        x -> x,
449
                        (a, b) -> a
450
                ));
451
 
33926 ranu 452
        for (WarehouseRbmTargetModel rbmTarget : warehouseRbmTargetModels) {
453
 
35454 amit 454
            String lookupKey = rbmTarget.getAuthId() + "-" + rbmTarget.getWarehouseId();
455
 
33926 ranu 456
            float monthlyTarget = rbmTarget.getMonthlyTarget();
35454 amit 457
            float achievedSoFar = mtdAchievedMap.getOrDefault(lookupKey, 0.0).floatValue();
33926 ranu 458
 
459
 
460
            float remainingTarget = monthlyTarget - achievedSoFar;
33953 ranu 461
            LOGGER.info("remainingTarget {}", remainingTarget);
33926 ranu 462
 
33953 ranu 463
            float todayTarget = (remainingWorkingDaysCount > 0 && remainingTarget > 0) ? remainingTarget / remainingWorkingDaysCount : 0;
464
            LOGGER.info("todayTarget {}", todayTarget);
465
 
33926 ranu 466
            // Get the warehouse stock data
35454 amit 467
            WarehouseMobileStockByMovementModel warehouseMobileStockByMovementModel = warehouseStockMap.get(rbmTarget.getWarehouseId());
33926 ranu 468
 
469
 
470
            if (warehouseMobileStockByMovementModel != null) {
471
 
472
                // Total stock value for this warehouse
473
                float totalStockValue = warehouseMobileStockByMovementModel.getTotalAvailabilityPrice();
474
 
475
                // Calculate target allocation based on stock value proportion
476
                float hidTarget = (warehouseMobileStockByMovementModel.getTotalHidCatalogPrice() / totalStockValue) * todayTarget;
477
                float fastMovingTarget = (warehouseMobileStockByMovementModel.getTotalFastMovingCatalogPrice() / totalStockValue) * todayTarget;
478
                float slowMovingTarget = (warehouseMobileStockByMovementModel.getTotalSlowMovingCatalogPrice() / totalStockValue) * todayTarget;
34098 ranu 479
                float runningTarget = (warehouseMobileStockByMovementModel.getTotalRunningCatalogPrice() / totalStockValue) * todayTarget;
33926 ranu 480
                float otherTarget = (warehouseMobileStockByMovementModel.getTotalOtherCategoryCatalogPrice() / totalStockValue) * todayTarget;
481
 
482
                RbmTargets rbmTargets = new RbmTargets();
483
                rbmTargets.setWarehouseId(rbmTarget.getWarehouseId());
484
                rbmTargets.setRbmAuthId(rbmTarget.getAuthId());
485
                rbmTargets.setRbmName(rbmTarget.getRbmName());
34098 ranu 486
                rbmTargets.setRunningtarget(runningTarget);
33926 ranu 487
                rbmTargets.setHidTarget(hidTarget);
488
                rbmTargets.setFastMovingTarget(fastMovingTarget);
34098 ranu 489
                rbmTargets.setSlowMovingtarget(slowMovingTarget);
33926 ranu 490
                rbmTargets.setOtherTarget(otherTarget);
491
                rbmTargets.setCreateTimestamp(LocalDateTime.now());
492
 
493
                rbmTargetsRepository.persist(rbmTargets);
494
 
495
            }
496
        }
497
 
498
    }
499
 
500
 
37497 ranu 501
    /**
502
     * Daily writer for Sales-hierarchy targets. Rolls up today's persisted
503
     * inventory.rbm_targets snapshots (produced by {@link #setMovementWiseRbmTargets})
504
     * to every active Sales person whose downline includes those RBMs, and
505
     * inserts one row per (sales, warehouse) into inventory.sales_targets.
506
     *
507
     * <p>Chain order matters: run this AFTER pushRbmTargets so today's RBM
508
     * snapshot rows exist when we roll up.
509
     */
33985 ranu 510
    @Override
37497 ranu 511
    public void setSalesTargets() {
512
        setSalesTargetsForDate(LocalDate.now());
513
    }
514
 
515
    @Override
516
    public void setSalesTargetsForDate(LocalDate date) {
37505 ranu 517
        // Sales target for a person = (own_monthly_target − MTD_own_achievement) /
518
        // remainingDays, then split by warehouse stock mix — exactly like RBM, but
519
        // scoped to the Sales person's OWN assigned partners (no RBM roll-up).
520
        LocalDate monthStart = date.withDayOfMonth(1);
521
        int remainingWorkingDays = (int) getRemainingDaysInMonth(date);
37497 ranu 522
 
37505 ranu 523
        // Warehouse-level stock mix (same source used by setMovementWiseRbmTargets).
524
        Map<Integer, WarehouseMobileStockByMovementModel> whStockByWh = new HashMap<>();
525
        for (WarehouseMobileStockByMovementModel s : getWarehouseMobileStockByMovement()) {
526
            whStockByWh.put(s.getWarehouseId(), s);
37497 ranu 527
        }
528
 
529
        String[][] roles = {
530
                {"ASM", "4", "L1"}, {"CM", "4", "L2"}, {"DM", "4", "L3"},
531
                {"BM", "4", "L4"}, {"RSM", "4", "L5"}, {"NSM", "4", "L6"}
532
        };
533
 
534
        LocalDate today = LocalDate.now();
535
        LocalDateTime stamp = date.equals(today) ? LocalDateTime.now() : date.atTime(java.time.LocalTime.NOON);
37505 ranu 536
        int written = 0, zeroTargetSkipped = 0;
537
 
37519 ranu 538
        // Cache monthly targets + MTD ACH per level so the RSM/NSM roll-up
539
        // pass can reuse them without repeating queries.
540
        Map<String, List<SalesHierarchyMonthlyTargetModel>> mtByLevel = new HashMap<>();
541
        Map<String, Map<String, Double>> mtdByLevelKey = new HashMap<>();
542
 
37497 ranu 543
        for (String[] r : roles) {
544
            String role = r[0];
545
            int catId = Integer.parseInt(r[1]);
37505 ranu 546
            String levelStr = r[2];
37497 ranu 547
 
37505 ranu 548
            List<SalesHierarchyMonthlyTargetModel> monthlyTgts = getSalesMonthlyTargetByRole(catId, levelStr, date);
37519 ranu 549
            mtByLevel.put(role, monthlyTgts);
37497 ranu 550
 
37505 ranu 551
            Map<String, Double> mtdByKey = new HashMap<>();
552
            for (SalesHierarchyAchievedByMovementModel a :
553
                    getSalesAchievementByRole(catId, levelStr, monthStart, date.plusDays(1))) {
554
                double totBilled =
555
                        a.getHidBilled()
556
                                + a.getFastMovingBilled()
557
                                + (a.getRunningBilled() != null ? a.getRunningBilled() : 0)
558
                                + (a.getSlowMovingBilled() != null ? a.getSlowMovingBilled() : 0)
559
                                + (a.getOtherBilled() != null ? a.getOtherBilled() : 0);
560
                mtdByKey.merge(a.getAuthId() + "-" + a.getWarehouseId(), totBilled, Double::sum);
561
            }
37519 ranu 562
            mtdByLevelKey.put(role, mtdByKey);
37505 ranu 563
 
37519 ranu 564
            // Direct-role writer — one row per (authId × warehouse) at this role.
565
            if (monthlyTgts.isEmpty()) continue;
37505 ranu 566
            for (SalesHierarchyMonthlyTargetModel mt : monthlyTgts) {
567
                if (mt.getMonthlyTarget() <= 0) {
568
                    zeroTargetSkipped++;
569
                    continue;
37497 ranu 570
                }
37505 ranu 571
                double achieved = mtdByKey.getOrDefault(mt.getAuthId() + "-" + mt.getWarehouseId(), 0.0);
572
                double remaining = mt.getMonthlyTarget() - achieved;
573
                double todayTarget = (remainingWorkingDays > 0 && remaining > 0)
574
                        ? remaining / remainingWorkingDays : 0;
37497 ranu 575
 
37505 ranu 576
                WarehouseMobileStockByMovementModel stock = whStockByWh.get(mt.getWarehouseId());
577
                if (stock == null || stock.getTotalAvailabilityPrice() <= 0) continue;
578
                float total = stock.getTotalAvailabilityPrice();
579
 
580
                SalesTargets st = new SalesTargets();
581
                st.setSalesAuthId(mt.getAuthId());
582
                st.setSalesName(mt.getName());
583
                st.setRole(role);
584
                st.setWarehouseId(mt.getWarehouseId());
585
                st.setHidTarget((float) ((stock.getTotalHidCatalogPrice() / total) * todayTarget));
586
                st.setFastMovingTarget((float) ((stock.getTotalFastMovingCatalogPrice() / total) * todayTarget));
587
                st.setSlowMovingTarget((float) ((stock.getTotalSlowMovingCatalogPrice() / total) * todayTarget));
588
                st.setRunningTarget((float) ((stock.getTotalRunningCatalogPrice() / total) * todayTarget));
589
                st.setOtherTarget((float) ((stock.getTotalOtherCategoryCatalogPrice() / total) * todayTarget));
590
                st.setCreateTimestamp(stamp);
591
                salesTargetsRepository.persist(st);
592
                written++;
37497 ranu 593
            }
594
        }
37519 ranu 595
 
596
        // Roll-up pass for RSM (L5) and NSM (L6) — uses distinct-partner dedup
597
        // queries so partners assigned to multiple positions in the downline
598
        // aren't counted more than once.
599
        String[] rollUpRoles = {"RSM", "NSM"};
600
        for (String rollUpRole : rollUpRoles) {
601
            String levelStr = "RSM".equals(rollUpRole) ? "L5" : "L6";
602
            for (AuthUser top : csService.getAuthUserByCategoryId(4, EscalationType.valueOf(levelStr))) {
603
                if (!top.isActive()) continue;
604
                Set<Integer> tree = new HashSet<>();
605
                tree.add(top.getId());
606
                List<Integer> reportees = authService.getAllReportees(top.getId());
607
                if (reportees != null) tree.addAll(reportees);
608
                if (tree.isEmpty()) continue;
609
 
610
                // Distinct-partner monthly TGT per warehouse for the downline.
611
                Map<Integer, Double> monthlyByWh = new LinkedHashMap<>();
612
                for (com.spice.profitmandi.dao.model.SalesDownlineMonthlyTargetModel d :
613
                        getSalesDownlineMonthlyTarget(tree, date)) {
614
                    monthlyByWh.put(d.getWarehouseId(), (double) d.getMonthlyTarget());
615
                }
616
                // Distinct-partner MTD ACH per warehouse for the downline.
617
                Map<Integer, Double> mtdByWh = new HashMap<>();
618
                for (com.spice.profitmandi.dao.model.SalesDownlineAchievedByMovementModel d :
619
                        getSalesDownlineAchievement(tree, monthStart, date.plusDays(1))) {
620
                    double tot = d.getHidBilled() + d.getFastMovingBilled()
621
                            + (d.getRunningBilled() != null ? d.getRunningBilled() : 0)
622
                            + (d.getSlowMovingBilled() != null ? d.getSlowMovingBilled() : 0)
623
                            + (d.getOtherBilled() != null ? d.getOtherBilled() : 0);
624
                    mtdByWh.put(d.getWarehouseId(), tot);
625
                }
626
 
627
                for (Map.Entry<Integer, Double> e : monthlyByWh.entrySet()) {
628
                    int whId = e.getKey();
629
                    double monthlyTgt = e.getValue();
630
                    if (monthlyTgt <= 0) {
631
                        zeroTargetSkipped++;
632
                        continue;
633
                    }
634
                    double achieved = mtdByWh.getOrDefault(whId, 0.0);
635
                    double remaining = monthlyTgt - achieved;
636
                    double todayTarget = (remainingWorkingDays > 0 && remaining > 0)
637
                            ? remaining / remainingWorkingDays : 0;
638
 
639
                    WarehouseMobileStockByMovementModel stock = whStockByWh.get(whId);
640
                    if (stock == null || stock.getTotalAvailabilityPrice() <= 0) continue;
641
                    float total = stock.getTotalAvailabilityPrice();
642
 
643
                    SalesTargets st = new SalesTargets();
644
                    st.setSalesAuthId(top.getId());
645
                    st.setSalesName(top.getFullName());
646
                    st.setRole(rollUpRole);
647
                    st.setWarehouseId(whId);
648
                    st.setHidTarget((float) ((stock.getTotalHidCatalogPrice() / total) * todayTarget));
649
                    st.setFastMovingTarget((float) ((stock.getTotalFastMovingCatalogPrice() / total) * todayTarget));
650
                    st.setSlowMovingTarget((float) ((stock.getTotalSlowMovingCatalogPrice() / total) * todayTarget));
651
                    st.setRunningTarget((float) ((stock.getTotalRunningCatalogPrice() / total) * todayTarget));
652
                    st.setOtherTarget((float) ((stock.getTotalOtherCategoryCatalogPrice() / total) * todayTarget));
653
                    st.setCreateTimestamp(stamp);
654
                    salesTargetsRepository.persist(st);
655
                    written++;
656
                }
657
            }
658
        }
37505 ranu 659
        LOGGER.info("setSalesTargetsForDate: wrote {} sales_targets rows for {} ({} rows skipped due to zero monthly target)",
660
                written, date, zeroTargetSkipped);
37497 ranu 661
    }
662
 
663
    @Override
664
    public List<SalesTargets> getSalesTargetsByDates(LocalDate startDate, LocalDate endDate) {
665
        return salesTargetsRepository.selectTargetsByDates(startDate.atStartOfDay(), endDate.atStartOfDay());
666
    }
667
 
668
    @Override
37505 ranu 669
    public List<SalesHierarchyAchievedByMovementModel> getSalesAchievementByRole(
670
            int categoryId, String escalationType, LocalDate startDate, LocalDate endDate) {
671
        Session session = sessionFactory.getCurrentSession();
672
        TypedQuery<SalesHierarchyAchievedByMovementModel> q = session.createNamedQuery(
673
                "RbmDrr.SalesHierarchyAchievedByMovement", SalesHierarchyAchievedByMovementModel.class);
674
        q.setParameter("categoryId", categoryId);
675
        q.setParameter("escalationType", escalationType);
676
        q.setParameter("startDate", startDate);
677
        q.setParameter("endDate", endDate);
678
        return q.getResultList();
679
    }
680
 
681
    @Override
682
    public List<SalesHierarchyMonthlyTargetModel> getSalesMonthlyTargetByRole(
683
            int categoryId, String escalationType, LocalDate refDate) {
684
        Session session = sessionFactory.getCurrentSession();
685
        TypedQuery<SalesHierarchyMonthlyTargetModel> q = session.createNamedQuery(
686
                "RbmDrr.SalesHierarchyMonthlyTarget", SalesHierarchyMonthlyTargetModel.class);
687
        q.setParameter("categoryId", categoryId);
688
        q.setParameter("escalationType", escalationType);
689
        q.setParameter("refDate", refDate);
690
        return q.getResultList();
691
    }
692
 
693
    @Override
37519 ranu 694
    public List<com.spice.profitmandi.dao.model.SalesDownlineAchievedByMovementModel>
695
    getSalesDownlineAchievement(Collection<Integer> authIds, LocalDate startDate, LocalDate endDate) {
696
        if (authIds == null || authIds.isEmpty()) return new ArrayList<>();
697
        Session session = sessionFactory.getCurrentSession();
698
        TypedQuery<com.spice.profitmandi.dao.model.SalesDownlineAchievedByMovementModel> q =
699
                session.createNamedQuery("RbmDrr.SalesDownlineAchievedByMovement",
700
                        com.spice.profitmandi.dao.model.SalesDownlineAchievedByMovementModel.class);
701
        q.setParameter("authIds", authIds);
702
        q.setParameter("startDate", startDate);
703
        q.setParameter("endDate", endDate);
704
        return q.getResultList();
705
    }
706
 
707
    @Override
708
    public List<com.spice.profitmandi.dao.model.SalesDownlineMonthlyTargetModel>
709
    getSalesDownlineMonthlyTarget(Collection<Integer> authIds, LocalDate refDate) {
710
        if (authIds == null || authIds.isEmpty()) return new ArrayList<>();
711
        Session session = sessionFactory.getCurrentSession();
712
        TypedQuery<com.spice.profitmandi.dao.model.SalesDownlineMonthlyTargetModel> q =
713
                session.createNamedQuery("RbmDrr.SalesDownlineMonthlyTarget",
714
                        com.spice.profitmandi.dao.model.SalesDownlineMonthlyTargetModel.class);
715
        q.setParameter("authIds", authIds);
716
        q.setParameter("refDate", refDate);
717
        return q.getResultList();
718
    }
719
 
720
    @Override
37533 ranu 721
    public List<com.spice.profitmandi.dao.model.SalesHierarchyBrandSoldModel>
722
    getSalesBrandsSold(Collection<Integer> authIds, int warehouseId, LocalDate startDate, LocalDate endDate) {
723
        if (authIds == null || authIds.isEmpty()) return new ArrayList<>();
724
        Session session = sessionFactory.getCurrentSession();
725
        TypedQuery<com.spice.profitmandi.dao.model.SalesHierarchyBrandSoldModel> q =
726
                session.createNamedQuery("RbmDrr.SalesBrandSold",
727
                        com.spice.profitmandi.dao.model.SalesHierarchyBrandSoldModel.class);
728
        q.setParameter("authIds", authIds);
729
        q.setParameter("warehouseId", warehouseId);
730
        q.setParameter("startDate", startDate);
731
        q.setParameter("endDate", endDate);
732
        return q.getResultList();
733
    }
734
 
735
    @Override
736
    public List<com.spice.profitmandi.dao.model.SalesHierarchyBrandPartnerCountModel>
737
    getSalesBrandPartnerCount(Collection<Integer> authIds, int warehouseId, LocalDate startDate, LocalDate endDate) {
738
        if (authIds == null || authIds.isEmpty()) return new ArrayList<>();
739
        Session session = sessionFactory.getCurrentSession();
740
        TypedQuery<com.spice.profitmandi.dao.model.SalesHierarchyBrandPartnerCountModel> q =
741
                session.createNamedQuery("RbmDrr.SalesBrandPartnerCount",
742
                        com.spice.profitmandi.dao.model.SalesHierarchyBrandPartnerCountModel.class);
743
        q.setParameter("authIds", authIds);
744
        q.setParameter("warehouseId", warehouseId);
745
        q.setParameter("startDate", startDate);
746
        q.setParameter("endDate", endDate);
747
        return q.getResultList();
748
    }
749
 
750
    @Override
37497 ranu 751
    public void setSalesAchievements() {
752
        setSalesAchievementsForDate(LocalDate.now());
753
    }
754
 
755
    @Override
756
    public void setSalesAchievementsForDate(LocalDate date) {
37505 ranu 757
        // Sales ACH per (auth × warehouse) — direct query scoped to each Sales
758
        // person's OWN partners (no RBM roll-up). Same source-of-truth orders SQL.
37497 ranu 759
        String[][] roles = {
760
                {"ASM", "4", "L1"}, {"CM", "4", "L2"}, {"DM", "4", "L3"},
761
                {"BM", "4", "L4"}, {"RSM", "4", "L5"}, {"NSM", "4", "L6"}
762
        };
763
 
764
        LocalDate today = LocalDate.now();
765
        LocalDateTime stamp = date.equals(today) ? LocalDateTime.now() : date.atTime(java.time.LocalTime.NOON);
37505 ranu 766
        int written = 0;
37497 ranu 767
 
37519 ranu 768
        // Cache per-level results so the RSM/NSM roll-up pass reuses them.
769
        Map<String, List<SalesHierarchyAchievedByMovementModel>> byLevel = new HashMap<>();
37497 ranu 770
        for (String[] r : roles) {
771
            String role = r[0];
772
            int catId = Integer.parseInt(r[1]);
37505 ranu 773
            String levelStr = r[2];
37497 ranu 774
 
37519 ranu 775
            List<SalesHierarchyAchievedByMovementModel> list =
776
                    getSalesAchievementByRole(catId, levelStr, date, date.plusDays(1));
777
            byLevel.put(role, list);
778
            for (SalesHierarchyAchievedByMovementModel a : list) {
37505 ranu 779
                SalesAchievements sa = new SalesAchievements();
780
                sa.setSalesAuthId(a.getAuthId());
781
                sa.setSalesName(a.getName());
782
                sa.setRole(role);
783
                sa.setWarehouseId(a.getWarehouseId());
784
                sa.setAchievedHidTarget(a.getHidBilled());
785
                sa.setAchievedFastMovingTarget(a.getFastMovingBilled());
786
                sa.setAchievedRunningTarget(a.getRunningBilled() != null ? a.getRunningBilled() : 0f);
787
                sa.setAchievedSlowMovingTarget(a.getSlowMovingBilled() != null ? a.getSlowMovingBilled() : 0f);
788
                sa.setAchievedOtherTarget(a.getOtherBilled() != null ? a.getOtherBilled() : 0f);
789
                sa.setCreateTimestamp(stamp);
790
                salesAchievementsRepository.persist(sa);
791
                written++;
37497 ranu 792
            }
793
        }
37519 ranu 794
 
795
        // Roll-up pass for RSM (L5) and NSM (L6) — uses distinct-partner dedup
796
        // query so partners assigned to multiple positions in the downline
797
        // don't have their orders counted more than once.
798
        String[] rollUpRoles = {"RSM", "NSM"};
799
        for (String rollUpRole : rollUpRoles) {
800
            String levelStr = "RSM".equals(rollUpRole) ? "L5" : "L6";
801
            for (AuthUser top : csService.getAuthUserByCategoryId(4, EscalationType.valueOf(levelStr))) {
802
                if (!top.isActive()) continue;
803
                Set<Integer> tree = new HashSet<>();
804
                tree.add(top.getId());
805
                List<Integer> reportees = authService.getAllReportees(top.getId());
806
                if (reportees != null) tree.addAll(reportees);
807
                if (tree.isEmpty()) continue;
808
 
809
                for (com.spice.profitmandi.dao.model.SalesDownlineAchievedByMovementModel d :
810
                        getSalesDownlineAchievement(tree, date, date.plusDays(1))) {
811
                    SalesAchievements sa = new SalesAchievements();
812
                    sa.setSalesAuthId(top.getId());
813
                    sa.setSalesName(top.getFullName());
814
                    sa.setRole(rollUpRole);
815
                    sa.setWarehouseId(d.getWarehouseId());
816
                    sa.setAchievedHidTarget(d.getHidBilled());
817
                    sa.setAchievedFastMovingTarget(d.getFastMovingBilled());
818
                    sa.setAchievedRunningTarget(d.getRunningBilled() != null ? d.getRunningBilled() : 0f);
819
                    sa.setAchievedSlowMovingTarget(d.getSlowMovingBilled() != null ? d.getSlowMovingBilled() : 0f);
820
                    sa.setAchievedOtherTarget(d.getOtherBilled() != null ? d.getOtherBilled() : 0f);
821
                    sa.setCreateTimestamp(stamp);
822
                    salesAchievementsRepository.persist(sa);
823
                    written++;
824
                }
825
            }
826
        }
37497 ranu 827
        LOGGER.info("setSalesAchievementsForDate: wrote {} sales_achievements rows for {}", written, date);
828
    }
829
 
830
    @Override
33985 ranu 831
    public void setMovementWiseRbmAchievement() {
832
        LocalDate todayDate = LocalDate.now();
833
 
34055 ranu 834
        List<TodayAchievedMovementModel> todayAchievedMovementModels = getMovementWiseAchievementByDate(todayDate, todayDate.plusDays(1));
33985 ranu 835
 
836
 
837
        for (TodayAchievedMovementModel achievement : todayAchievedMovementModels) {
838
 
839
            RbmAchievements rbmAchievements = new RbmAchievements();
840
 
841
            rbmAchievements.setRbmAuthId(achievement.getAuthId());
842
            rbmAchievements.setRbmName(achievement.getRbmName());
843
            rbmAchievements.setWarehouseId(achievement.getWarehouseId());
844
            rbmAchievements.setAchievedHidTarget(achievement.getHidBilled());
845
            rbmAchievements.setAchievedFastMovingTarget(achievement.getFastMovingBilled());
34098 ranu 846
            rbmAchievements.setAchievedSlowMovingTarget(achievement.getSlowMovinBilled());
847
            rbmAchievements.setAchievedRunningTarget(achievement.getRunningBilled());
34012 ranu 848
            rbmAchievements.setAchievedOtherTarget(achievement.getOtherBilled());
33985 ranu 849
            rbmAchievements.setCreateTimestamp(LocalDateTime.now());
850
 
851
            rbmAchievementsRepository.persist(rbmAchievements);
852
 
853
        }
854
 
855
    }
856
 
34055 ranu 857
    @Override
858
    public List<Sold15daysOldAgingModel> getAgingSale(LocalDate startDate, LocalDate endDate) {
859
        Session session = sessionFactory.getCurrentSession();
860
        final TypedQuery<Sold15daysOldAgingModel> typedQuerySimilar = session.createNamedQuery("Aging.SoldAgingModel", Sold15daysOldAgingModel.class);
34056 ranu 861
        typedQuerySimilar.setParameter("startDate", startDate);
862
        typedQuerySimilar.setParameter("endDate", endDate);
34055 ranu 863
        return typedQuerySimilar.getResultList();
33985 ranu 864
 
34055 ranu 865
    }
866
 
34103 ranu 867
    @Override
868
    public List<RbmBilledFofoIdsModel> getDateWiseBilledFofoIdByRbm(LocalDate startDate, LocalDate endDate) {
869
        Session session = sessionFactory.getCurrentSession();
870
        final TypedQuery<RbmBilledFofoIdsModel> typedQuerySimilar = session.createNamedQuery("RBM.RbmBilledFofoId", RbmBilledFofoIdsModel.class);
871
        typedQuerySimilar.setParameter("startDate", startDate);
872
        typedQuerySimilar.setParameter("endDate", endDate);
873
        return typedQuerySimilar.getResultList();
35453 amit 874
    }
34103 ranu 875
 
35453 amit 876
    @Override
36296 amit 877
    @Cacheable(value = "rbmWeeklyBilling",
36329 amit 878
            cacheManager = "fiveMintimeoutCacheManager",
879
            sync = true)
35453 amit 880
    public List<RbmWeeklyBillingModel> getWeeklyBillingDataForMonth(LocalDate monthStart, LocalDate monthEnd) {
881
        Session session = sessionFactory.getCurrentSession();
882
        final TypedQuery<RbmWeeklyBillingModel> typedQuery = session.createNamedQuery("RBM.WeeklyBilling", RbmWeeklyBillingModel.class);
883
        typedQuery.setParameter("startDate", monthStart);
884
        typedQuery.setParameter("endDate", monthEnd);
885
        return typedQuery.getResultList();
34103 ranu 886
    }
887
 
34055 ranu 888
    public List<Our15DaysOldAgingStock> our15DaysAgingStock() {
889
        Session session = sessionFactory.getCurrentSession();
890
        final TypedQuery<Our15DaysOldAgingStock> typedQuerySimilar = session.createNamedQuery("Aging.15DaysOurStock", Our15DaysOldAgingStock.class);
891
        return typedQuerySimilar.getResultList();
36334 ranu 892
    }
34055 ranu 893
 
36334 ranu 894
    @Override
895
    public List<WarehouseAgingStockModel> getWarehouseWiseAgingStock() {
896
        Session session = sessionFactory.getCurrentSession();
897
        final TypedQuery<WarehouseAgingStockModel> typedQuery = session.createNamedQuery("Aging.15DaysWarehouseWiseStock", WarehouseAgingStockModel.class);
898
        return typedQuery.getResultList();
34055 ranu 899
    }
900
 
34397 ranu 901
    @Autowired
902
    OrderRepository orderRepository;
34055 ranu 903
 
34397 ranu 904
    @Override
34641 ranu 905
    public double calculateFofoIdTodayTarget(int fofoId, double secondryMtd,LocalDate date) {
34397 ranu 906
 
907
        MonthlyTarget monthlyTarget = monthlyTargetRepository.selectByDateAndFofoId(YearMonth.now(), fofoId);
34404 ranu 908
        if (monthlyTarget == null) {
909
            // Log or handle as needed
910
            return 0; // or -1 or some fallback
911
        }
34397 ranu 912
 
913
        double remainingTarget = monthlyTarget.getPurchaseTarget() - secondryMtd;
34880 ranu 914
//        double remainingWorkingDays = getWorkingDaysCount(date);
915
        double remainingWorkingDays = (double) getRemainingDaysInMonth(date);
34397 ranu 916
 
917
 
34716 ranu 918
 
34397 ranu 919
        if (remainingWorkingDays == 0) return remainingTarget; // Last day
34716 ranu 920
        LOGGER.info("remainingWorkingDays {}", remainingWorkingDays);
921
        LOGGER.info("remainingTarget {}", remainingTarget);
34397 ranu 922
 
923
        return (int) Math.ceil(remainingTarget / remainingWorkingDays);
924
    }
925
 
34880 ranu 926
    @Override
927
    public long getRemainingDaysInMonth(LocalDate date) {
928
        LocalDate lastDayOfMonth = YearMonth.from(date).atEndOfMonth();
34397 ranu 929
 
34880 ranu 930
        long totalDays = ChronoUnit.DAYS.between(date, lastDayOfMonth) + 1;
34397 ranu 931
 
34880 ranu 932
        // Count Sundays manually
933
        long sundayCount = 0;
934
        LocalDate current = date;
935
        while (!current.isAfter(lastDayOfMonth)) {
936
            if (current.getDayOfWeek() == DayOfWeek.SUNDAY) {
937
                sundayCount++;
938
            }
939
            current = current.plusDays(1);
940
        }
941
 
942
        // Public holidays in the range
943
        long publicHolidays = publicHolidaysRepository
944
                .selectAllBetweenDates(date, lastDayOfMonth)
945
                .size();
946
 
947
        long remainingDays = totalDays - sundayCount - publicHolidays;
948
 
949
        LOGGER.info("remainingDays {}", remainingDays);
950
        LOGGER.info("totalDays {}", totalDays);
951
        LOGGER.info("sundays {}", sundayCount);
952
        LOGGER.info("publicHolidays {}", publicHolidays);
953
 
954
        return remainingDays;
955
    }
956
 
35631 ranu 957
    @Autowired
958
    PositionRepository positionRepository;
34880 ranu 959
 
35631 ranu 960
    @Autowired
961
    CsService csService;
34880 ranu 962
 
35631 ranu 963
    @Autowired
964
    FofoStoreRepository fofoStoreRepository;
965
 
966
    @Autowired
967
    AuthRepository authRepository;
968
 
969
    @Autowired
970
    LoanRepository loanRepository;
971
 
972
    @Autowired
973
    PartnerCollectionService partnerCollectionService;
974
 
975
    @Autowired
976
    PartnerCollectionRemarkRepository partnerCollectionRemarkRepository;
977
 
978
    @Autowired
979
    RbmCallSequenceLogRepository rbmCallSequenceLogRepository;
980
 
981
    @Autowired
982
    com.spice.profitmandi.dao.repository.cs.TicketRepository ticketRepository;
983
 
35702 ranu 984
    @Autowired
985
    com.spice.profitmandi.dao.repository.cs.AgentCallLogRepository agentCallLogRepository;
986
 
35759 ranu 987
    @Autowired
988
    RetailerContactRepository retailerContactRepository;
989
 
990
    @Autowired
991
    AddressRepository addressRepository;
992
 
36225 ranu 993
    @Autowired
994
    com.spice.profitmandi.dao.repository.cs.PartnerPositionRepository partnerPositionRepository;
995
 
35631 ranu 996
    @Override
997
    public List<RbmCallTargetModel> getRbmCallTargetModels() throws Exception {
36234 ranu 998
        return getRbmCallTargetModels(LocalDate.now());
999
    }
1000
 
37010 ranu 1001
    // Matches "No Answer", "NO_ANSWER", "no-answer", "noanswer", etc.
1002
    private static boolean isNoAnswerStatus(String status) {
1003
        if (status == null) return false;
1004
        String s = status.trim().toLowerCase().replace('_', ' ').replace('-', ' ').replaceAll("\\s+", " ");
1005
        return s.equals("no answer") || s.equals("noanswer");
1006
    }
1007
 
1008
    @Override
1009
    public List<OutOfSequenceDetailModel> getOutOfSequenceDetails(int authId) {
1010
 
1011
        LocalDate today = LocalDate.now();
1012
        LocalDateTime start = today.atStartOfDay();
1013
        LocalDateTime end = today.plusDays(1).atStartOfDay();
1014
 
1015
        List<RbmCallSequenceLog> logs =
1016
                rbmCallSequenceLogRepository.selectByAuthIdAndDateRange(authId, start, end);
1017
 
1018
        Map<Integer, RbmCallSequenceLog> uniqueOosLogsByFofoId = new LinkedHashMap<>();
1019
 
1020
        for (RbmCallSequenceLog log : logs) {
1021
            if (log.isOutOfSequence()) {
1022
                // Keep only the first occurrence per fofoId (latest entry since ordered by id DESC)
1023
                uniqueOosLogsByFofoId.putIfAbsent(log.getFofoId(), log);
1024
            }
1025
        }
1026
 
1027
        if (uniqueOosLogsByFofoId.isEmpty()) {
1028
            return Collections.emptyList();
1029
        }
1030
 
1031
        Set<Integer> fofoIds = uniqueOosLogsByFofoId.keySet();
1032
        Map<Integer, CustomRetailer> retailerMap = Collections.emptyMap();
1033
        try {
1034
            retailerMap = retailerService.getFofoRetailers(new ArrayList<>(fofoIds));
1035
        } catch (ProfitMandiBusinessException e) {
1036
            LOGGER.error("Error fetching fofo stores", e);
1037
        }
1038
 
1039
        DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("hh:mm a");
1040
        List<OutOfSequenceDetailModel> result = new ArrayList<>();
1041
 
1042
        for (RbmCallSequenceLog log : uniqueOosLogsByFofoId.values()) {
1043
            CustomRetailer retailer = retailerMap.get(log.getFofoId());
1044
            String partyName = retailer != null
1045
                    ? retailer.getBusinessName()
1046
                    : "Unknown (" + log.getFofoId() + ")";
1047
            String code = retailer != null
1048
                    ? retailer.getCode()
1049
                    : "-";
1050
 
1051
            String time = log.getCreateTimestamp() != null
1052
                    ? log.getCreateTimestamp().format(timeFormatter)
1053
                    : "-";
1054
 
1055
            result.add(new OutOfSequenceDetailModel(partyName, code, time));
1056
        }
1057
 
1058
        return result;
1059
    }
1060
 
1061
    @Override
1062
    public List<CalledPartnerDetailModel> getCalledPartnerDetails(int authId) throws ProfitMandiBusinessException {
1063
        return getCalledPartnerDetails(authId, LocalDate.now());
1064
    }
1065
 
1066
    @Override
1067
    public List<CalledPartnerDetailModel> getCalledPartnerDetails(int authId, LocalDate date) throws ProfitMandiBusinessException {
1068
        // Get all call logs for this auth user on this date
1069
        LOGGER.info("getCalledPartnerDetails: authId={}, date={}", authId, date);
1070
        List<AgentCallLog> callLogs = agentCallLogRepository.findByAuthIdAndDate(authId, date);
1071
        LOGGER.info("Found {} call logs for authId={} on date={}", callLogs.size(), authId, date);
1072
 
1073
        if (callLogs.isEmpty()) {
1074
            return Collections.emptyList();
1075
        }
1076
 
1077
        // Build a map of normalized customer number -> fofoId
1078
        Map<String, Integer> customerToFofoIdMap = new HashMap<>();
1079
        Set<String> normalizedNumbers = new HashSet<>();
1080
 
1081
        for (AgentCallLog callLog : callLogs) {
1082
            String customerNumber = callLog.getCustomerNumber();
1083
            if (customerNumber != null) {
1084
                String normalized = customerNumber.startsWith("+91") ? customerNumber.substring(3) : customerNumber;
1085
                normalizedNumbers.add(normalized);
1086
            }
1087
        }
1088
 
1089
        // For each normalized number, find fofoId from retailer_contact first, then address
1090
        for (String mobile : normalizedNumbers) {
1091
            Integer fofoId = findFofoIdByMobile(mobile);
1092
            if (fofoId != null) {
1093
                customerToFofoIdMap.put(mobile, fofoId);
1094
            }
1095
        }
1096
 
1097
        // Get unique fofoIds for retailer lookup
1098
        Set<Integer> fofoIds = new HashSet<>(customerToFofoIdMap.values());
1099
        Map<Integer, CustomRetailer> retailerMap = Collections.emptyMap();
1100
        if (!fofoIds.isEmpty()) {
1101
            try {
1102
                retailerMap = retailerService.getFofoRetailers(new ArrayList<>(fofoIds));
1103
            } catch (ProfitMandiBusinessException e) {
1104
                LOGGER.error("Error fetching fofo stores", e);
1105
            }
1106
        }
1107
 
1108
        // Get today's remarks for these fofoIds
1109
        Map<Integer, List<PartnerCollectionRemark>> fofoRemarkMap = new HashMap<>();
1110
        if (!fofoIds.isEmpty()) {
1111
            List<PartnerCollectionRemark> todayRemarks = partnerCollectionRemarkRepository
1112
                    .selectAllByFofoIdsOnDate(new ArrayList<>(fofoIds), date);
1113
            for (PartnerCollectionRemark remark : todayRemarks) {
1114
                fofoRemarkMap.computeIfAbsent(remark.getFofoId(), k -> new ArrayList<>()).add(remark);
1115
            }
1116
        }
1117
 
1118
        DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("hh:mm a");
1119
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("dd-MM-yyyy hh:mm a");
1120
        List<CalledPartnerDetailModel> result = new ArrayList<>();
1121
 
1122
        for (com.spice.profitmandi.dao.entity.cs.AgentCallLog callLog : callLogs) {
1123
            String customerNumber = callLog.getCustomerNumber();
1124
            if (customerNumber == null) {
1125
                continue;
1126
            }
1127
 
1128
            String normalized = customerNumber.startsWith("+91") ? customerNumber.substring(3) : customerNumber;
1129
            Integer fofoId = customerToFofoIdMap.get(normalized);
1130
 
1131
            String partyName = "Unknown";
1132
            String code = "-";
1133
 
1134
            if (fofoId != null) {
1135
                CustomRetailer retailer = retailerMap.get(fofoId);
1136
                if (retailer != null) {
1137
                    partyName = retailer.getBusinessName();
1138
                    code = retailer.getCode();
1139
                } else {
1140
                    partyName = "Unknown (" + fofoId + ")";
1141
                }
1142
            } else {
1143
                partyName = "Unknown (" + normalized + ")";
1144
            }
1145
 
1146
            // Get remark if available
1147
            String remarkValue = "-";
1148
            String messageValue = "-";
1149
            String remarkTime = "-";
1150
 
1151
            if (fofoId != null && fofoRemarkMap.containsKey(fofoId)) {
1152
                List<PartnerCollectionRemark> remarks = fofoRemarkMap.get(fofoId);
1153
                if (!remarks.isEmpty()) {
1154
                    PartnerCollectionRemark remark = remarks.get(0);
1155
                    remarkValue = remark.getRemark() != null ? remark.getRemark().getValue() : "-";
1156
                    messageValue = remark.getMessage() != null ? remark.getMessage() : "-";
1157
                    remarkTime = remark.getCreateTimestamp() != null ? remark.getCreateTimestamp().format(timeFormatter) : "-";
1158
                }
1159
            }
1160
 
1161
            // Build call log data
1162
            String recordingUrl = callLog.getRecordingUrl();
1163
            String callStatus = callLog.getCallStatus();
1164
            String callDuration = callLog.getCallDuration();
1165
            String callDateTime = null;
1166
            if (callLog.getCallDate() != null && callLog.getCallTime() != null) {
1167
                LocalDateTime callDateTimeObj = LocalDateTime.of(callLog.getCallDate(), callLog.getCallTime());
1168
                callDateTime = callDateTimeObj.format(dateTimeFormatter);
1169
            }
1170
 
1171
            result.add(new CalledPartnerDetailModel(partyName, code, remarkValue, messageValue, remarkTime,
1172
                    recordingUrl, callStatus, callDuration, callDateTime));
1173
        }
1174
 
1175
        return result;
1176
    }
1177
 
1178
    private Integer findFofoIdByMobile(String mobile) {
1179
        // First check retailer_contact
1180
        List<RetailerContact> contacts = retailerContactRepository.selectByMobile(mobile);
1181
        if (contacts != null && !contacts.isEmpty()) {
1182
            return contacts.get(0).getFofoId();
1183
        }
1184
 
1185
        // Fallback to user.address
1186
        List<Address> addresses = addressRepository.selectAllByPhoneNumber(mobile);
1187
        if (addresses != null && !addresses.isEmpty()) {
1188
            return addresses.get(0).getRetaierId();
1189
        }
1190
 
1191
        return null;
1192
    }
1193
 
1194
    private List<CalledPartnerDetailModel> buildCalledPartnerResult(List<PartnerCollectionRemark> allRemarks) {
1195
        if (allRemarks.isEmpty()) {
1196
            return Collections.emptyList();
1197
        }
1198
 
1199
        // Get unique fofoIds for retailer lookup
1200
        Set<Integer> fofoIds = allRemarks.stream()
1201
                .map(PartnerCollectionRemark::getFofoId)
1202
                .collect(Collectors.toSet());
1203
        Map<Integer, CustomRetailer> retailerMap = Collections.emptyMap();
1204
        try {
1205
            retailerMap = retailerService.getFofoRetailers(new ArrayList<>(fofoIds));
1206
        } catch (ProfitMandiBusinessException e) {
1207
            LOGGER.error("Error fetching fofo stores", e);
1208
        }
1209
 
1210
        // Fetch call logs for remarks that have agentCallLogId
1211
        Map<Long, com.spice.profitmandi.dao.entity.cs.AgentCallLog> callLogMap = new HashMap<>();
1212
        try {
1213
            List<Long> callLogIds = allRemarks.stream()
1214
                    .filter(r -> r.getAgentCallLogId() > 0)
1215
                    .map(PartnerCollectionRemark::getAgentCallLogId)
1216
                    .collect(Collectors.toList());
1217
 
1218
            if (!callLogIds.isEmpty()) {
1219
                List<com.spice.profitmandi.dao.entity.cs.AgentCallLog> callLogs = agentCallLogRepository.findByIds(callLogIds);
1220
                if (callLogs != null) {
1221
                    callLogMap = callLogs.stream()
1222
                            .collect(Collectors.toMap(com.spice.profitmandi.dao.entity.cs.AgentCallLog::getId, c -> c, (a, b) -> a));
1223
                }
1224
            }
1225
        } catch (Exception e) {
1226
            LOGGER.error("Error fetching call logs by ids", e);
1227
        }
1228
 
1229
        DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("hh:mm a");
1230
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("dd-MM-yyyy hh:mm a");
1231
        List<CalledPartnerDetailModel> result = new ArrayList<>();
1232
 
1233
        for (PartnerCollectionRemark remark : allRemarks) {
1234
            CustomRetailer retailer = retailerMap.get(remark.getFofoId());
1235
            String partyName = retailer != null
1236
                    ? retailer.getBusinessName()
1237
                    : "Unknown (" + remark.getFofoId() + ")";
1238
            String code = retailer != null
1239
                    ? retailer.getCode()
1240
                    : "-";
1241
 
1242
            String remarkValue = remark.getRemark() != null
1243
                    ? remark.getRemark().getValue()
1244
                    : "-";
1245
 
1246
            String messageValue = remark.getMessage() != null
1247
                    ? remark.getMessage()
1248
                    : "-";
1249
 
1250
            String time = remark.getCreateTimestamp() != null
1251
                    ? remark.getCreateTimestamp().format(timeFormatter)
1252
                    : "-";
1253
 
1254
            // Get call log data if available
1255
            String recordingUrl = null;
1256
            String callStatus = null;
1257
            String callDuration = null;
1258
            String callDateTime = null;
1259
 
1260
            try {
1261
                if (remark.getAgentCallLogId() > 0 && callLogMap.containsKey(remark.getAgentCallLogId())) {
1262
                    com.spice.profitmandi.dao.entity.cs.AgentCallLog callLog = callLogMap.get(remark.getAgentCallLogId());
1263
                    recordingUrl = callLog.getRecordingUrl();
1264
                    callStatus = callLog.getCallStatus();
1265
                    callDuration = callLog.getCallDuration();
1266
                    if (callLog.getCallDate() != null && callLog.getCallTime() != null) {
1267
                        LocalDateTime callDateTimeObj = LocalDateTime.of(callLog.getCallDate(), callLog.getCallTime());
1268
                        callDateTime = callDateTimeObj.format(dateTimeFormatter);
1269
                    }
1270
                }
1271
            } catch (Exception e) {
1272
                LOGGER.error("Error processing call log for remark id: {}", remark.getId(), e);
1273
            }
1274
 
1275
            result.add(new CalledPartnerDetailModel(partyName, code, remarkValue, messageValue, time,
1276
                    recordingUrl, callStatus, callDuration, callDateTime));
1277
        }
1278
 
1279
        return result;
1280
    }
1281
 
1282
    @Override
1283
    public List<List<String>> getRbmCallTargetRawDataByAuthId(int authId) throws Exception {
1284
        List<List<String>> rows = new ArrayList<>();
1285
 
1286
        // Get auth user
1287
        List<AuthUser> authUsers = authRepository.selectByIds(Collections.singletonList(authId));
1288
        if (authUsers.isEmpty()) {
1289
            return rows;
1290
        }
1291
        AuthUser authUser = authUsers.get(0);
1292
 
1293
        // Get positions to determine if L2
1294
        List<Position> positions = positionRepository.selectPositionByAuthIds(Collections.singletonList(authId));
1295
        boolean isL2 = positions.stream()
1296
                .anyMatch(p -> ProfitMandiConstants.TICKET_CATEGORY_RBM == p.getCategoryId()
1297
                        && EscalationType.L2.equals(p.getEscalationType()));
1298
 
1299
        LocalDateTime startDate = LocalDate.now().atStartOfDay();
1300
        LocalDate firstOfMonth = LocalDate.now().withDayOfMonth(1);
1301
        LocalDate endOfMonth = LocalDate.now().withDayOfMonth(LocalDate.now().lengthOfMonth()).plusDays(1);
1302
 
1303
        // Get fofo IDs from mtdBillingData (same source as Partner Count in getRbmCallTargetModels)
1304
        List<RbmWeeklyBillingModel> mtdBillingData = getWeeklyBillingDataForMonth(firstOfMonth, endOfMonth);
1305
 
1306
        List<Integer> fofoIdList;
1307
        if (isL2) {
37145 ranu 1308
            // L2: get fofo IDs from escalated tickets (same as getRbmCallTargetModels).
1309
            // selectOpenEscalatedTicketsByAuthIds already filters to tickets where authId is the
1310
            // current assignee AND the ticket has moved past L1 — no post-filter needed.
37010 ranu 1311
            List<Ticket> escalatedTickets = ticketRepository.selectOpenEscalatedTicketsByAuthIds(Collections.singletonList(authId));
1312
            fofoIdList = escalatedTickets.stream()
1313
                    .map(Ticket::getFofoId)
1314
                    .distinct()
1315
                    .collect(Collectors.toList());
1316
        } else {
1317
            // L1: get fofo IDs from mtdBillingData with isTargetedPartner (same as Partner Count)
1318
            fofoIdList = mtdBillingData.stream()
1319
                    .filter(RbmWeeklyBillingModel::isTargetedPartner)
1320
                    .filter(m -> m.getAuthId() == authId)
1321
                    .map(RbmWeeklyBillingModel::getFofoId)
1322
                    .distinct()
1323
                    .collect(Collectors.toList());
1324
        }
1325
 
1326
        if (fofoIdList.isEmpty()) {
1327
            return rows;
1328
        }
1329
 
1330
        // MTD billed fofoIds for zero billing check
1331
        Set<Integer> mtdBilledFofoIds = mtdBillingData.stream()
1332
                .filter(RbmWeeklyBillingModel::isMtdBilled)
1333
                .map(RbmWeeklyBillingModel::getFofoId)
1334
                .collect(Collectors.toSet());
1335
 
1336
        // Collection rank map for status calculation
1337
        Map<Integer, Integer> collectionRankMap = new HashMap<>();
1338
        try {
1339
            collectionRankMap = partnerCollectionService.getCollectionRankMap(fofoIdList, startDate);
1340
        } catch (ProfitMandiBusinessException e) {
1341
            LOGGER.error("Error fetching collection rank map", e);
1342
        }
1343
 
1344
        // Resolve partner names/codes
1345
        Map<Integer, CustomRetailer> retailerMap = Collections.emptyMap();
1346
        if (!fofoIdList.isEmpty()) {
1347
            try {
1348
                retailerMap = retailerService.getFofoRetailers(fofoIdList);
1349
            } catch (ProfitMandiBusinessException e) {
1350
                LOGGER.error("Error fetching fofo retailers for raw data", e);
1351
            }
1352
        }
1353
 
1354
        String rbmName = authUser.getFullName() + (isL2 ? " (L2)" : "");
1355
 
1356
        // Build rows for ALL partners (same count as Partner Count)
1357
        for (Integer fofoId : fofoIdList) {
1358
            // Default to rank 5 (Normal) for partners without collection plan
1359
            int rank = collectionRankMap.getOrDefault(fofoId, 5);
1360
            boolean hasZeroBilling = !mtdBilledFofoIds.contains(fofoId);
1361
 
1362
            // Status assignment with same priority as getRbmCallTargetModels
1363
            String status;
1364
            if (rank == 1) {
1365
                status = "Plan Today";
1366
            } else if (rank == 2) {
1367
                status = "Carry Forward";
1368
            } else if (hasZeroBilling) {
1369
                status = "Zero Billing";
1370
            } else if (rank == 3) {
1371
                status = "Untouched";
1372
            } else if (rank == 4) {
1373
                status = "Future Plan";
1374
            } else {
1375
                status = "Normal";
1376
            }
1377
 
1378
            CustomRetailer retailer = retailerMap.get(fofoId);
1379
            String partnerName = retailer != null ? retailer.getBusinessName() : "Unknown (" + fofoId + ")";
1380
            String partnerCode = retailer != null ? retailer.getCode() : "-";
1381
 
1382
            rows.add(Arrays.asList(partnerName, partnerCode, status, rbmName));
1383
        }
1384
 
1385
        return rows;
1386
    }
1387
 
1388
    @Override
1389
    public List<List<String>> getAllRbmCallTargetRawData() throws Exception {
1390
        List<List<String>> rows = new ArrayList<>();
1391
 
1392
        // Get all L1 RBM positions (rows are emitted per L1 RBM).
1393
        // Call-log lookup happens against ALL callers via findAllByDate — so an L2/L3 or admin call
1394
        // still counts as "tried" for the partner.
1395
        List<Position> allRbmPositions = positionRepository
1396
                .selectPositionByCategoryId(ProfitMandiConstants.TICKET_CATEGORY_RBM).stream()
1397
                .filter(x -> EscalationType.L1.equals(x.getEscalationType()))
1398
                .collect(Collectors.toList());
1399
 
1400
        List<Integer> l1AuthIds = allRbmPositions.stream()
1401
                .map(Position::getAuthUserId).distinct().collect(Collectors.toList());
1402
 
1403
        if (l1AuthIds.isEmpty()) {
1404
            return rows;
1405
        }
1406
 
1407
        // Get auth user map
1408
        Map<Integer, AuthUser> authUserMap = authRepository.selectByIds(l1AuthIds).stream()
1409
                .collect(Collectors.toMap(AuthUser::getId, au -> au));
1410
 
1411
        // Positions per auth user (needed early to build L1 partner assignments below)
1412
        Map<Integer, List<Position>> positionsByAuthId = positionRepository.selectPositionByAuthIds(l1AuthIds).stream()
1413
                .collect(Collectors.groupingBy(Position::getAuthUserId));
1414
 
1415
        // Build L1 partner assignments from partner_position — SAME source the UI summary uses for
1416
        // Calling Target. storeGuyMap (email-based CS mapping) was returning smaller/different lists
1417
        // for users who are also L2, causing summary=43 but download=3 for Ebadullah.
1418
        Map<Integer, Integer> l1PositionIdToAuthId = new HashMap<>();
1419
        for (int rbmAuthId : l1AuthIds) {
1420
            List<Position> positions = positionsByAuthId.getOrDefault(rbmAuthId, Collections.emptyList());
1421
            for (Position p : positions) {
1422
                if (ProfitMandiConstants.TICKET_CATEGORY_RBM == p.getCategoryId()
1423
                        && EscalationType.L1.equals(p.getEscalationType())) {
1424
                    l1PositionIdToAuthId.put(p.getId(), rbmAuthId);
1425
                }
1426
            }
1427
        }
1428
 
1429
        Map<Integer, List<Integer>> rbmToFofoIdsMap = new HashMap<>();
1430
        Set<Integer> allFofoIds = new HashSet<>();
1431
        if (!l1PositionIdToAuthId.isEmpty()) {
1432
            List<com.spice.profitmandi.dao.entity.cs.PartnerPosition> allL1PPs =
1433
                    partnerPositionRepository.selectByPositionIds(new ArrayList<>(l1PositionIdToAuthId.keySet()));
1434
            // Group by owning RBM; keep partners distinct within each RBM.
1435
            Map<Integer, Set<Integer>> perRbmSets = new HashMap<>();
1436
            for (com.spice.profitmandi.dao.entity.cs.PartnerPosition pp : allL1PPs) {
1437
                Integer ownerAuthId = l1PositionIdToAuthId.get(pp.getPositionId());
1438
                if (ownerAuthId == null) continue;
1439
                perRbmSets.computeIfAbsent(ownerAuthId, k -> new HashSet<>()).add(pp.getFofoId());
1440
                allFofoIds.add(pp.getFofoId());
1441
            }
1442
            for (Map.Entry<Integer, Set<Integer>> e : perRbmSets.entrySet()) {
1443
                rbmToFofoIdsMap.put(e.getKey(), new ArrayList<>(e.getValue()));
1444
            }
1445
        }
1446
 
1447
        if (allFofoIds.isEmpty()) {
1448
            return rows;
1449
        }
1450
 
1451
        // Get fofo stores for filtering and name resolution
1452
        Map<Integer, FofoStore> fofoStoresMap = new HashMap<>();
1453
        try {
1454
            fofoStoresMap = fofoStoreRepository.selectByRetailerIds(new ArrayList<>(allFofoIds)).stream()
1455
                    .collect(Collectors.toMap(FofoStore::getId, x -> x, (a, b) -> a));
1456
        } catch (ProfitMandiBusinessException e) {
1457
            LOGGER.error("Error fetching fofo stores for all raw data", e);
1458
        }
1459
 
1460
        // Batch fetch collection rank map
1461
        LocalDateTime startDate = LocalDate.now().atStartOfDay();
1462
        Map<Integer, Integer> allCollectionRankMap = new HashMap<>();
1463
        try {
1464
            allCollectionRankMap = partnerCollectionService.getCollectionRankMap(new ArrayList<>(allFofoIds), startDate);
1465
        } catch (ProfitMandiBusinessException e) {
1466
            LOGGER.error("Error fetching collection rank map for all raw data", e);
1467
        }
1468
 
1469
        // MTD billing data
1470
        LocalDate firstOfMonth = LocalDate.now().withDayOfMonth(1);
1471
        LocalDate endOfMonth = LocalDate.now().withDayOfMonth(LocalDate.now().lengthOfMonth()).plusDays(1);
1472
        List<RbmWeeklyBillingModel> mtdBillingData = getWeeklyBillingDataForMonth(firstOfMonth, endOfMonth);
1473
        Set<Integer> allMtdBilledFofoIds = mtdBillingData.stream()
1474
                .filter(RbmWeeklyBillingModel::isMtdBilled)
1475
                .map(RbmWeeklyBillingModel::getFofoId)
1476
                .collect(Collectors.toSet());
1477
 
1478
        // Batch fetch partner collection remarks for escalation filtering
1479
        Map<Integer, PartnerCollectionRemark> allPartnerCollectionRemarks = new HashMap<>();
1480
        if (!allFofoIds.isEmpty()) {
1481
            List<Integer> allRemarkIds = partnerCollectionRemarkRepository.selectMaxRemarkId(new ArrayList<>(allFofoIds));
1482
            if (!allRemarkIds.isEmpty()) {
1483
                allPartnerCollectionRemarks = partnerCollectionRemarkRepository.selectByIds(allRemarkIds).stream()
1484
                        .collect(Collectors.toMap(PartnerCollectionRemark::getFofoId, x -> x, (a, b) -> a));
1485
            }
1486
        }
1487
 
1488
        // Resolve partner names/codes in batch
1489
        Map<Integer, CustomRetailer> retailerMap = Collections.emptyMap();
1490
        try {
1491
            retailerMap = retailerService.getFofoRetailers(new ArrayList<>(allFofoIds));
1492
        } catch (ProfitMandiBusinessException e) {
1493
            LOGGER.error("Error fetching fofo retailers for all raw data", e);
1494
        }
1495
 
1496
        // Batch-fetch EVERY call log for today (across all callers, not just RBMs) and build a
1497
        // fofoId -> latest AgentCallLog map. We use findAllByDate here so that calls placed by
1498
        // admins, escalation agents, or anyone whose authId isn't in the RBM position table are
1499
        // still surfaced. Downstream we only read this map for fofoIds we actually emit, so
1500
        // extra entries are harmless.
37013 ranu 1501
        // Group ALL today's call logs by fofoId so we can apply the Tried rule per partner
1502
        // (uses customerStatus + persistence, same as the summary's Tried metric).
1503
        Map<Integer, List<AgentCallLog>> callsByFofoId = new HashMap<>();
37010 ranu 1504
        try {
1505
            List<AgentCallLog> todayCallLogs = agentCallLogRepository.findAllByDate(LocalDate.now());
1506
            Set<String> normalizedMobiles = new HashSet<>();
1507
            for (AgentCallLog log : todayCallLogs) {
1508
                if (log.getCustomerNumber() != null) {
1509
                    String n = log.getCustomerNumber();
1510
                    normalizedMobiles.add(n.startsWith("+91") ? n.substring(3) : n);
1511
                }
1512
            }
1513
            Map<String, Integer> mobileToFofoIdMap = buildMobileToFofoIdMap(normalizedMobiles);
1514
            for (AgentCallLog log : todayCallLogs) {
1515
                if (log.getCustomerNumber() == null) continue;
1516
                String n = log.getCustomerNumber();
1517
                String normalized = n.startsWith("+91") ? n.substring(3) : n;
1518
                Integer fofoId = mobileToFofoIdMap.get(normalized);
1519
                if (fofoId == null) continue;
37013 ranu 1520
                callsByFofoId.computeIfAbsent(fofoId, k -> new ArrayList<>()).add(log);
37010 ranu 1521
            }
1522
        } catch (Exception e) {
37013 ranu 1523
            LOGGER.error("Error building call logs map for all raw data", e);
37010 ranu 1524
        }
1525
 
1526
        // Process each L1 RBM
1527
        // Note: no cross-RBM dedup — a partner assigned to multiple RBMs appears once per RBM,
1528
        // matching the UI summary counts so row totals per RBM equal that RBM's "Calling Target".
1529
        for (int rbmAuthId : l1AuthIds) {
1530
            AuthUser authUser = authUserMap.get(rbmAuthId);
1531
            if (authUser == null) {
1532
                continue;
1533
            }
1534
 
1535
            List<Integer> fofoIdList = rbmToFofoIdsMap.getOrDefault(rbmAuthId, Collections.emptyList());
1536
            if (fofoIdList.isEmpty()) {
1537
                continue; // No L1 partner assignments for this RBM
1538
            }
1539
 
1540
            // Filter escalated partners for L1 RBMs
1541
            List<Position> positions = positionsByAuthId.getOrDefault(rbmAuthId, Collections.emptyList());
1542
            boolean isRBMAndL1 = positions.stream()
1543
                    .anyMatch(position ->
1544
                            ProfitMandiConstants.TICKET_CATEGORY_RBM == position.getCategoryId()
1545
                                    && EscalationType.L1.equals(position.getEscalationType()));
1546
 
1547
            List<Integer> fofoIds = fofoIdList;
1548
            if (isRBMAndL1) {
1549
                Map<Integer, PartnerCollectionRemark> partnerRemarks = new HashMap<>();
1550
                for (Integer fofoId : fofoIdList) {
1551
                    if (allPartnerCollectionRemarks.containsKey(fofoId)) {
1552
                        partnerRemarks.put(fofoId, allPartnerCollectionRemarks.get(fofoId));
1553
                    }
1554
                }
1555
                Map<Integer, PartnerCollectionRemark> finalPartnerRemarks = partnerRemarks;
1556
                fofoIds = fofoIdList.stream()
1557
                        .filter(fofoId -> {
1558
                            if (!finalPartnerRemarks.containsKey(fofoId)) return true;
1559
                            PartnerCollectionRemark pcr = finalPartnerRemarks.get(fofoId);
1560
                            return !(CollectionRemark.RBM_L2_ESCALATION.equals(pcr.getRemark())
1561
                                    || CollectionRemark.SALES_ESCALATION.equals(pcr.getRemark()));
1562
                        })
1563
                        .collect(Collectors.toList());
1564
            }
1565
 
1566
            // Filter to only external, ACTIVE or REVIVAL stores
1567
            Map<Integer, FofoStore> finalFofoStoresMap = fofoStoresMap;
1568
            List<Integer> validFofoIds = fofoIds.stream()
1569
                    .filter(fofoId -> {
1570
                        FofoStore store = finalFofoStoresMap.get(fofoId);
1571
                        if (store == null || store.isInternal()) return false;
1572
                        return ActivationType.ACTIVE.equals(store.getActivationType())
1573
                                || ActivationType.REVIVAL.equals(store.getActivationType());
1574
                    })
1575
                    .collect(Collectors.toList());
1576
 
1577
            String rbmName = authUser.getFullName();
1578
 
1579
            // Categorize each partner and add a row per RBM assignment.
1580
            for (Integer fofoId : validFofoIds) {
1581
                int rank = allCollectionRankMap.getOrDefault(fofoId, 5);
1582
                boolean hasZeroBilling = !allMtdBilledFofoIds.contains(fofoId);
1583
 
1584
                String status;
1585
                if (rank == 1) {
1586
                    status = "Plan Today";
1587
                } else if (rank == 2) {
1588
                    status = "Carry Forward";
1589
                } else if (hasZeroBilling) {
1590
                    status = "Zero Billing";
1591
                } else if (rank == 3) {
1592
                    status = "Untouched";
1593
                } else {
1594
                    continue; // Skip Future Plan and Normal — only include calling target parties
1595
                }
1596
 
1597
                CustomRetailer retailer = retailerMap.get(fofoId);
1598
                String partnerName = retailer != null ? retailer.getBusinessName() : "Unknown (" + fofoId + ")";
1599
                String partnerCode = retailer != null ? retailer.getCode() : "-";
1600
 
37013 ranu 1601
                // Show the latest customerStatus from today's call logs for this partner.
1602
                // "Did Not Try" only when there is no call log at all today.
1603
                List<AgentCallLog> partnerCalls = callsByFofoId.getOrDefault(fofoId, Collections.emptyList());
37010 ranu 1604
                String latestCallStatus = "Did Not Try";
37013 ranu 1605
                if (!partnerCalls.isEmpty()) {
1606
                    AgentCallLog latest = null;
1607
                    for (AgentCallLog l : partnerCalls) {
1608
                        if (latest == null || (l.getId() != null && latest.getId() != null && l.getId() > latest.getId())) {
1609
                            latest = l;
1610
                        }
1611
                    }
1612
                    if (latest != null && latest.getCustomerStatus() != null && !latest.getCustomerStatus().isEmpty()) {
1613
                        latestCallStatus = latest.getCustomerStatus();
1614
                    }
37010 ranu 1615
                }
1616
 
1617
                rows.add(Arrays.asList(partnerName, partnerCode, status, rbmName, latestCallStatus));
1618
            }
1619
        }
1620
 
1621
        return rows;
1622
    }
1623
 
1624
    /**
1625
     * Get count of distinct partners called today based on call logs.
1626
     * Maps customerNumber from call log to fofoId using retailer_contact and address.
1627
     * If same fofoId is called multiple times, counts only once.
1628
     * Numbers without fofoId mapping are also counted (by distinct customer number).
1629
     *
1630
     * @param authId the RBM auth ID
1631
     * @return count of distinct partners/numbers called today
1632
     */
1633
    public long getCalledCountFromCallLogs(long authId) {
1634
        return getCalledCountFromCallLogs(authId, LocalDate.now());
1635
    }
1636
 
1637
    public long getCalledCountFromCallLogs(long authId, LocalDate date) {
1638
        return getCallStats(authId, date)[0];
1639
    }
1640
 
1641
    /**
1642
     * Batch-builds a mobile (normalized, +91 stripped) -> fofoId mapping for all the given mobiles
1643
     * in just two queries (retailer_contact, then address fallback for unmapped numbers).
1644
     * Replaces the previous per-call-log findFofoIdByMobile() N+1 lookups.
1645
     */
1646
    private Map<String, Integer> buildMobileToFofoIdMap(Set<String> normalizedMobiles) {
1647
        Map<String, Integer> result = new HashMap<>();
1648
        if (normalizedMobiles == null || normalizedMobiles.isEmpty()) {
1649
            return result;
1650
        }
1651
        List<String> mobilesList = new ArrayList<>(normalizedMobiles);
1652
 
1653
        // First pass: retailer_contact
1654
        List<RetailerContact> contacts = retailerContactRepository.selectByMobiles(mobilesList);
1655
        for (RetailerContact rc : contacts) {
1656
            // Keep the first fofoId we see per mobile (matches old single-mobile behavior of get(0))
1657
            result.putIfAbsent(rc.getMobile(), rc.getFofoId());
1658
        }
1659
 
1660
        // Second pass: address fallback for mobiles not yet mapped
1661
        List<String> unmapped = mobilesList.stream()
1662
                .filter(m -> !result.containsKey(m))
1663
                .collect(Collectors.toList());
1664
        if (!unmapped.isEmpty()) {
1665
            List<Address> addresses = addressRepository.selectAllByPhoneNumbers(unmapped);
1666
            for (Address addr : addresses) {
1667
                result.putIfAbsent(addr.getPhoneNumber(), addr.getRetaierId());
1668
            }
1669
        }
1670
        return result;
1671
    }
1672
 
1673
    /**
1674
     * In-memory variant of getCallStats() that uses pre-fetched call logs and mobile→fofoId mapping.
1675
     * Used by the batch-fetched RBM Call Target loop to avoid N+1 query patterns.
1676
     */
1677
    private long[] getCallStatsFromLogs(List<AgentCallLog> callLogs, Map<String, Integer> mobileToFofoIdMap) {
1678
        if (callLogs == null || callLogs.isEmpty()) {
1679
            return new long[]{0, 0, 0};
1680
        }
1681
 
1682
        Set<Integer> calledFofoIds = new HashSet<>();
1683
        Set<String> calledNumbersWithoutFofoId = new HashSet<>();
1684
        long totalRecordingCalls = 0;
1685
        Set<String> uniqueRecordingNumbers = new HashSet<>();
1686
 
1687
        for (AgentCallLog callLog : callLogs) {
1688
            String customerNumber = callLog.getCustomerNumber();
1689
            if (customerNumber != null) {
1690
                String normalized = customerNumber.startsWith("+91") ? customerNumber.substring(3) : customerNumber;
1691
                Integer fofoId = mobileToFofoIdMap.get(normalized);
1692
                if (fofoId != null) {
1693
                    calledFofoIds.add(fofoId);
1694
                } else {
1695
                    calledNumbersWithoutFofoId.add(normalized);
1696
                }
1697
 
1698
                if (callLog.getRecordingUrl() != null && !callLog.getRecordingUrl().isEmpty()
1699
                        && !"None".equalsIgnoreCase(callLog.getRecordingUrl())) {
1700
                    totalRecordingCalls++;
1701
                    uniqueRecordingNumbers.add(normalized);
1702
                }
1703
            }
1704
        }
1705
 
1706
        long calledCount = calledFofoIds.size() + calledNumbersWithoutFofoId.size();
1707
        return new long[]{calledCount, totalRecordingCalls, uniqueRecordingNumbers.size()};
1708
    }
1709
 
37022 ranu 1710
    // Same rule as computeUniqueTriedCount but restricted to fofoIds in the target set.
1711
    // Used to compute "TGT Calls" — how many of THIS RBM's Calling Target partners were
1712
    // actually engaged today. Off-target activity is silently excluded.
1713
    private long computeUniqueTriedCountForTargets(List<AgentCallLog> callLogs,
1714
                                                   Map<String, Integer> mobileToFofoIdMap,
1715
                                                   Set<Integer> targetFofoIds) {
1716
        if (callLogs == null || callLogs.isEmpty()) return 0L;
1717
        if (targetFofoIds == null || targetFofoIds.isEmpty()) return 0L;
1718
        Map<Integer, List<AgentCallLog>> byFofoId = new HashMap<>();
1719
        for (AgentCallLog log : callLogs) {
1720
            String num = log.getCustomerNumber();
1721
            if (num == null) continue;
1722
            String normalized = num.startsWith("+91") ? num.substring(3) : num;
1723
            Integer fofoId = mobileToFofoIdMap != null ? mobileToFofoIdMap.get(normalized) : null;
1724
            if (fofoId == null) continue;
1725
            if (!targetFofoIds.contains(fofoId)) continue; // off-target, skip
1726
            byFofoId.computeIfAbsent(fofoId, k -> new ArrayList<>()).add(log);
1727
        }
1728
        long count = 0L;
1729
        for (List<AgentCallLog> logs : byFofoId.values()) {
1730
            boolean hasNonMissed = false;
1731
            for (AgentCallLog l : logs) {
1732
                if (!isMissedCustomerStatus(l.getCustomerStatus())) {
1733
                    hasNonMissed = true;
1734
                    break;
1735
                }
1736
            }
1737
            if (hasNonMissed) count++;
1738
            else if (logs.size() >= 3) count++;
1739
        }
1740
        return count;
1741
    }
1742
 
37012 ranu 1743
    // Matches "Missed", "MISSED", "No Answer", "NO_ANSWER", "no-answer" etc.
1744
    // Kommuno currently emits "Missed" — kept liberal so we're covered if the tag changes.
1745
    private static boolean isMissedCustomerStatus(String status) {
1746
        if (status == null) return false;
1747
        String s = status.trim().toLowerCase().replace('_', ' ').replace('-', ' ').replaceAll("\\s+", " ");
1748
        return s.equals("missed") || s.equals("no answer") || s.equals("noanswer");
1749
    }
1750
 
37013 ranu 1751
 
36234 ranu 1752
    public List<RbmCallTargetModel> getRbmCallTargetModels(LocalDate queryDate) throws Exception {
35631 ranu 1753
        long methodStart = System.currentTimeMillis();
1754
        List<RbmCallTargetModel> rbmCallTargetModels = new ArrayList<>();
1755
 
1756
        // Get all RBM positions (L1 and L2)
1757
        long start = System.currentTimeMillis();
1758
        List<Position> allRbmPositions = positionRepository
1759
                .selectPositionByCategoryId(ProfitMandiConstants.TICKET_CATEGORY_RBM).stream()
36210 ranu 1760
                .filter(x -> Arrays.asList(EscalationType.L1, EscalationType.L2, EscalationType.L3).contains(x.getEscalationType()))
35631 ranu 1761
                .collect(Collectors.toList());
1762
 
36210 ranu 1763
        // Separate L1, L2 and L3 auth IDs
35631 ranu 1764
        List<Integer> l1AuthIds = allRbmPositions.stream()
1765
                .filter(p -> EscalationType.L1.equals(p.getEscalationType()))
1766
                .map(Position::getAuthUserId).distinct().collect(Collectors.toList());
1767
        List<Integer> l2AuthIds = allRbmPositions.stream()
1768
                .filter(p -> EscalationType.L2.equals(p.getEscalationType()))
1769
                .map(Position::getAuthUserId).distinct().collect(Collectors.toList());
36210 ranu 1770
        List<Integer> l3AuthIds = allRbmPositions.stream()
1771
                .filter(p -> EscalationType.L3.equals(p.getEscalationType()))
1772
                .map(Position::getAuthUserId).distinct().collect(Collectors.toList());
35631 ranu 1773
 
1774
        // Union of all auth IDs for batch fetching
1775
        List<Integer> rbmPositionsAuthIds = allRbmPositions.stream()
1776
                .map(Position::getAuthUserId).distinct().collect(Collectors.toList());
36210 ranu 1777
        LOGGER.info("RBM Call Target - RBM positions fetch: {}ms, L1: {}, L2: {}, L3: {}", System.currentTimeMillis() - start, l1AuthIds.size(), l2AuthIds.size(), l3AuthIds.size());
35631 ranu 1778
 
1779
        start = System.currentTimeMillis();
1780
        Map<String, Set<Integer>> storeGuyMap = csService.getAuthUserPartnerIdMapping();
1781
        LOGGER.info("RBM Call Target - StoreGuyMap fetch: {}ms", System.currentTimeMillis() - start);
1782
 
36234 ranu 1783
        LocalDateTime startDate = queryDate.atStartOfDay();
1784
        LocalDate firstOfMonth = queryDate.withDayOfMonth(1);
1785
        LocalDate endOfMonth = queryDate.withDayOfMonth(queryDate.lengthOfMonth()).plusDays(1);
35631 ranu 1786
 
1787
        // Get auth user map
1788
        start = System.currentTimeMillis();
1789
        Map<Integer, AuthUser> authUserMap = authRepository.selectByIds(rbmPositionsAuthIds).stream()
1790
                .collect(Collectors.toMap(AuthUser::getId, au -> au));
1791
        LOGGER.info("RBM Call Target - AuthUser fetch: {}ms", System.currentTimeMillis() - start);
1792
 
1793
        // Batch fetch positions by auth IDs (to check if RBM is L1)
1794
        start = System.currentTimeMillis();
1795
        Map<Integer, List<Position>> positionsByAuthId = positionRepository.selectPositionByAuthIds(rbmPositionsAuthIds).stream()
1796
                .collect(Collectors.groupingBy(Position::getAuthUserId));
1797
        LOGGER.info("RBM Call Target - Positions by AuthId fetch: {}ms", System.currentTimeMillis() - start);
1798
 
1799
        // Get all fofo IDs for all RBMs
1800
        Set<Integer> allFofoIds = new HashSet<>();
1801
        Map<Integer, List<Integer>> rbmToFofoIdsMap = new HashMap<>();
1802
        for (int rbmAuthId : rbmPositionsAuthIds) {
1803
            AuthUser au = authUserMap.get(rbmAuthId);
1804
            if (au != null && storeGuyMap.containsKey(au.getEmailId())) {
1805
                List<Integer> fofoIds = new ArrayList<>(storeGuyMap.get(au.getEmailId()));
1806
                allFofoIds.addAll(fofoIds);
1807
                rbmToFofoIdsMap.put(rbmAuthId, fofoIds);
1808
            }
1809
        }
35816 ranu 1810
        // Initialize L2 calling list map - will be populated after fetching remarks
35631 ranu 1811
        Map<Integer, List<Integer>> l2AuthIdToFofoIds = new HashMap<>();
35816 ranu 1812
        for (int l2AuthId : l2AuthIds) {
1813
            l2AuthIdToFofoIds.put(l2AuthId, new ArrayList<>());
35631 ranu 1814
        }
36210 ranu 1815
        // Initialize L3 calling list map - will be populated after fetching remarks
1816
        Map<Integer, List<Integer>> l3AuthIdToFofoIds = new HashMap<>();
1817
        for (int l3AuthId : l3AuthIds) {
1818
            l3AuthIdToFofoIds.put(l3AuthId, new ArrayList<>());
1819
        }
35631 ranu 1820
        LOGGER.info("RBM Call Target - Total fofo IDs to process: {}", allFofoIds.size());
1821
 
1822
        // Get only needed fofo stores (OPTIMIZED - was fetching ALL stores before)
1823
        start = System.currentTimeMillis();
1824
        Map<Integer, FofoStore> fofoStoresMap = new HashMap<>();
1825
        if (!allFofoIds.isEmpty()) {
1826
            try {
1827
                fofoStoresMap = fofoStoreRepository.selectByRetailerIds(new ArrayList<>(allFofoIds)).stream()
1828
                        .collect(Collectors.toMap(FofoStore::getId, x -> x, (a, b) -> a));
1829
            } catch (ProfitMandiBusinessException e) {
1830
                LOGGER.error("Error fetching fofo stores", e);
1831
            }
1832
        }
1833
        LOGGER.info("RBM Call Target - FofoStores fetch (only needed): {}ms, count: {}", System.currentTimeMillis() - start, fofoStoresMap.size());
1834
 
1835
        // Batch fetch max remark ids for all fofoIds (for escalation filtering)
1836
        start = System.currentTimeMillis();
1837
        Map<Integer, PartnerCollectionRemark> allPartnerCollectionRemarks = new HashMap<>();
1838
        if (!allFofoIds.isEmpty()) {
1839
            List<Integer> allRemarkIds = partnerCollectionRemarkRepository.selectMaxRemarkId(new ArrayList<>(allFofoIds));
1840
            if (!allRemarkIds.isEmpty()) {
1841
                allPartnerCollectionRemarks = partnerCollectionRemarkRepository.selectByIds(allRemarkIds).stream()
1842
                        .collect(Collectors.toMap(PartnerCollectionRemark::getFofoId, x -> x, (a, b) -> a));
1843
            }
1844
        }
1845
        LOGGER.info("RBM Call Target - PartnerCollectionRemarks fetch: {}ms", System.currentTimeMillis() - start);
1846
 
35816 ranu 1847
        // Populate L2 calling list based on partners whose latest remark is RBM_L2_ESCALATION
1848
        // Find the L1 who has the partner and add to that L1's manager (L2) calling list
1849
        for (Map.Entry<Integer, PartnerCollectionRemark> entry : allPartnerCollectionRemarks.entrySet()) {
1850
            Integer fofoId = entry.getKey();
1851
            PartnerCollectionRemark remark = entry.getValue();
1852
 
1853
            if (CollectionRemark.RBM_L2_ESCALATION.equals(remark.getRemark())) {
1854
                // Find which L1 RBM has this partner assigned
1855
                for (int l1AuthId : l1AuthIds) {
1856
                    List<Integer> l1FofoIds = rbmToFofoIdsMap.getOrDefault(l1AuthId, Collections.emptyList());
1857
                    if (l1FofoIds.contains(fofoId)) {
1858
                        // Get L1's manager (L2)
1859
                        AuthUser l1User = authUserMap.get(l1AuthId);
1860
                        if (l1User != null && l2AuthIdToFofoIds.containsKey(l1User.getManagerId())) {
1861
                            int l2ManagerId = l1User.getManagerId();
1862
                            l2AuthIdToFofoIds.get(l2ManagerId).add(fofoId);
1863
                        }
1864
                        break; // Found the L1 for this fofoId
1865
                    }
1866
                }
1867
            }
1868
        }
1869
        LOGGER.info("RBM Call Target - L2 calling lists populated from RBM_L2_ESCALATION remarks");
1870
 
36210 ranu 1871
        // Populate L3 calling list based on partners whose latest remark is RBM_L3_ESCALATION
36212 ranu 1872
        // Find the L1 who originally has the partner, then:
1873
        // Case 1: L1 -> L3 directly (if L1's manager IS L3)
1874
        // Case 2: L1 -> L2 -> L3 (if L1's manager is L2, then L2's manager is L3)
36210 ranu 1875
        for (Map.Entry<Integer, PartnerCollectionRemark> entry : allPartnerCollectionRemarks.entrySet()) {
1876
            Integer fofoId = entry.getKey();
1877
            PartnerCollectionRemark remark = entry.getValue();
1878
 
1879
            if (CollectionRemark.RBM_L3_ESCALATION.equals(remark.getRemark())) {
1880
                // Find which L1 RBM originally has this partner assigned
1881
                for (int l1AuthId : l1AuthIds) {
1882
                    List<Integer> l1FofoIds = rbmToFofoIdsMap.getOrDefault(l1AuthId, Collections.emptyList());
1883
                    if (l1FofoIds.contains(fofoId)) {
1884
                        AuthUser l1User = authUserMap.get(l1AuthId);
1885
                        if (l1User != null) {
36212 ranu 1886
                            int l1ManagerId = l1User.getManagerId();
1887
                            // Case 1: L1's manager IS L3 directly (L1 → L3, no L2 in between)
1888
                            if (l3AuthIdToFofoIds.containsKey(l1ManagerId)) {
1889
                                l3AuthIdToFofoIds.get(l1ManagerId).add(fofoId);
1890
                                LOGGER.debug("L3 Calling List (direct): fofoId={} -> L1={} -> L3={}",
1891
                                        fofoId, l1AuthId, l1ManagerId);
1892
                            } else {
1893
                                // Case 2: L1 -> L2 -> L3
1894
                                AuthUser l2User = authUserMap.get(l1ManagerId);
1895
                                if (l2User != null && l3AuthIdToFofoIds.containsKey(l2User.getManagerId())) {
1896
                                    int l3ManagerId = l2User.getManagerId();
1897
                                    l3AuthIdToFofoIds.get(l3ManagerId).add(fofoId);
1898
                                    LOGGER.debug("L3 Calling List: fofoId={} -> L1={} -> L2={} -> L3={}",
1899
                                            fofoId, l1AuthId, l1ManagerId, l3ManagerId);
1900
                                }
36210 ranu 1901
                            }
1902
                        }
1903
                        break; // Found the L1 for this fofoId
1904
                    }
1905
                }
1906
            }
1907
        }
1908
        LOGGER.info("RBM Call Target - L3 calling lists populated from RBM_L3_ESCALATION remarks");
1909
 
35631 ranu 1910
        // Batch fetch collection RANK map for all fofoIds (OPTIMIZED - only fetches rank, not full model)
1911
        start = System.currentTimeMillis();
1912
        Map<Integer, Integer> allCollectionRankMap = new HashMap<>();
1913
        if (!allFofoIds.isEmpty()) {
1914
            try {
1915
                allCollectionRankMap = partnerCollectionService.getCollectionRankMap(new ArrayList<>(allFofoIds), startDate);
1916
            } catch (ProfitMandiBusinessException e) {
1917
                LOGGER.error("Error fetching collection rank map for all fofoIds", e);
1918
            }
1919
        }
1920
        LOGGER.info("RBM Call Target - CollectionRankMap fetch (OPTIMIZED): {}ms", System.currentTimeMillis() - start);
1921
 
35669 ranu 1922
        // Get MTD billing data for zero billing calculation and partner counts
35631 ranu 1923
        start = System.currentTimeMillis();
1924
        List<RbmWeeklyBillingModel> mtdBillingData = getWeeklyBillingDataForMonth(firstOfMonth, endOfMonth);
1925
        Set<Integer> allMtdBilledFofoIds = mtdBillingData.stream()
1926
                .filter(RbmWeeklyBillingModel::isMtdBilled)
1927
                .map(RbmWeeklyBillingModel::getFofoId)
1928
                .collect(Collectors.toSet());
35669 ranu 1929
        // Build partner count and fofoIds per RBM from mtdBillingData (same source as Today ARR page)
1930
        Map<Integer, Set<Integer>> mtdFofoIdsByAuthId = mtdBillingData.stream()
1931
                .filter(RbmWeeklyBillingModel::isTargetedPartner)
1932
                .collect(Collectors.groupingBy(RbmWeeklyBillingModel::getAuthId,
1933
                        Collectors.mapping(RbmWeeklyBillingModel::getFofoId, Collectors.toSet())));
35631 ranu 1934
        LOGGER.info("RBM Call Target - MTD Billing fetch: {}ms", System.currentTimeMillis() - start);
1935
 
1936
        // Batch fetch today's remarks for all auth IDs (to calculate Value Achieved)
1937
        start = System.currentTimeMillis();
1938
        Map<Integer, List<PartnerCollectionRemark>> remarksByAuthId = partnerCollectionRemarkRepository
1939
                .selectAllByAuthIdsOnDate(rbmPositionsAuthIds, LocalDate.now()).stream()
1940
                .collect(Collectors.groupingBy(PartnerCollectionRemark::getAuthId));
1941
        LOGGER.info("RBM Call Target - Today Remarks fetch: {}ms", System.currentTimeMillis() - start);
1942
 
1943
        // Batch fetch today's out-of-sequence logs for all RBMs
1944
        start = System.currentTimeMillis();
1945
        LocalDateTime todayStart = LocalDate.now().atStartOfDay();
1946
        LocalDateTime todayEnd = LocalDate.now().plusDays(1).atStartOfDay();
1947
        List<RbmCallSequenceLog> outOfSequenceLogs = rbmCallSequenceLogRepository.selectOutOfSequenceByDateRange(todayStart, todayEnd);
1948
        Map<Integer, Long> outOfSequenceCountByAuthId = outOfSequenceLogs.stream()
35654 ranu 1949
                .collect(Collectors.groupingBy(RbmCallSequenceLog::getAuthId,
1950
                        Collectors.mapping(RbmCallSequenceLog::getFofoId, Collectors.collectingAndThen(Collectors.toSet(), s -> (long) s.size()))));
35631 ranu 1951
        LOGGER.info("RBM Call Target - Out of Sequence fetch: {}ms", System.currentTimeMillis() - start);
1952
 
36284 ranu 1953
        // BATCH FETCH: All call logs for all RBMs (L1 + L2 + L3) in a single query.
1954
        // Replaces the previous N+1 pattern where getCallStats() was called per RBM.
1955
        start = System.currentTimeMillis();
1956
        List<AgentCallLog> allCallLogs = agentCallLogRepository.findByAuthIdsAndDate(rbmPositionsAuthIds, queryDate);
1957
        Map<Long, List<AgentCallLog>> callLogsByAuthId = allCallLogs.stream()
1958
                .collect(Collectors.groupingBy(AgentCallLog::getAuthId));
1959
        LOGGER.info("RBM Call Target - Call logs batch fetch: {}ms ({} logs across {} RBMs)",
1960
                System.currentTimeMillis() - start, allCallLogs.size(), callLogsByAuthId.size());
1961
 
1962
        // BATCH FETCH: Build a single mobile -> fofoId map from all unique customer numbers across all call logs.
1963
        // Replaces the previous N+1 pattern where findFofoIdByMobile() was called per call log entry.
1964
        start = System.currentTimeMillis();
1965
        Set<String> allNormalizedMobiles = new HashSet<>();
1966
        for (AgentCallLog callLog : allCallLogs) {
1967
            String customerNumber = callLog.getCustomerNumber();
1968
            if (customerNumber != null) {
1969
                String normalized = customerNumber.startsWith("+91") ? customerNumber.substring(3) : customerNumber;
1970
                allNormalizedMobiles.add(normalized);
1971
            }
1972
        }
1973
        Map<String, Integer> mobileToFofoIdMap = buildMobileToFofoIdMap(allNormalizedMobiles);
1974
        LOGGER.info("RBM Call Target - Mobile→FofoId batch fetch: {}ms ({} unique mobiles, {} mapped)",
1975
                System.currentTimeMillis() - start, allNormalizedMobiles.size(), mobileToFofoIdMap.size());
1976
 
36225 ranu 1977
        // Identify users who are both L1 and L2 — they will be shown only as L2
1978
        Set<Integer> l2AuthIdSet = new HashSet<>(l2AuthIds);
1979
 
1980
        // Process L1 RBMs (skip users who are also L2 — their data will be merged into L2 model)
35631 ranu 1981
        for (int rbmAuthId : l1AuthIds) {
36225 ranu 1982
            if (l2AuthIdSet.contains(rbmAuthId)) {
1983
                continue; // Will be handled in L2 processing with merged L1 data
1984
            }
35631 ranu 1985
            AuthUser authUser = authUserMap.get(rbmAuthId);
1986
            if (authUser == null || !storeGuyMap.containsKey(authUser.getEmailId())) {
1987
                continue;
1988
            }
1989
 
1990
            List<Integer> fofoIdList = rbmToFofoIdsMap.getOrDefault(rbmAuthId, Collections.emptyList());
1991
 
1992
            // Check if RBM is L1 (same logic as getSummaryModel)
1993
            List<Position> positions = positionsByAuthId.getOrDefault(authUser.getId(), Collections.emptyList());
1994
            boolean isRBMAndL1 = positions.stream()
1995
                    .anyMatch(position ->
1996
                            ProfitMandiConstants.TICKET_CATEGORY_RBM == position.getCategoryId()
1997
                                    && EscalationType.L1.equals(position.getEscalationType()));
1998
 
1999
            // Filter escalated partners for L1 RBMs (same logic as getSummaryModel)
2000
            List<Integer> fofoIds = fofoIdList;
2001
            if (isRBMAndL1) {
2002
                Map<Integer, PartnerCollectionRemark> partnerCollectionRemarks = new HashMap<>();
2003
                for (Integer fofoId : fofoIdList) {
2004
                    if (allPartnerCollectionRemarks.containsKey(fofoId)) {
2005
                        partnerCollectionRemarks.put(fofoId, allPartnerCollectionRemarks.get(fofoId));
2006
                    }
2007
                }
2008
                fofoIds = partnerCollectionRemarks.entrySet().stream()
2009
                        .filter(entry -> {
2010
                            PartnerCollectionRemark pcrMap = entry.getValue();
2011
                            return !(CollectionRemark.RBM_L2_ESCALATION.equals(pcrMap.getRemark())
2012
                                    || CollectionRemark.SALES_ESCALATION.equals(pcrMap.getRemark()));
2013
                        })
2014
                        .map(Map.Entry::getKey)
2015
                        .collect(Collectors.toList());
2016
            }
2017
 
36181 ranu 2018
            // Filter to only external, ACTIVE or REVIVAL stores (collection plan not required)
35631 ranu 2019
            Map<Integer, Integer> finalAllCollectionRankMap = allCollectionRankMap;
2020
            Map<Integer, FofoStore> finalFofoStoresMap = fofoStoresMap;
2021
            List<Integer> validFofoIds = fofoIds.stream()
2022
                    .filter(fofoId -> {
2023
                        FofoStore store = finalFofoStoresMap.get(fofoId);
2024
                        if (store == null || store.isInternal()) {
2025
                            return false;
2026
                        }
36181 ranu 2027
                        // Only include ACTIVE or REVIVAL partners (not Low Sale, not Disputed, not Billing Pending)
2028
                        return ActivationType.ACTIVE.equals(store.getActivationType())
2029
                                || ActivationType.REVIVAL.equals(store.getActivationType());
35631 ranu 2030
                    })
2031
                    .collect(Collectors.toList());
2032
 
2033
            if (validFofoIds.isEmpty()) {
2034
                continue;
2035
            }
2036
 
2037
            RbmCallTargetModel targetModel = new RbmCallTargetModel();
2038
            targetModel.setAuthId(rbmAuthId);
2039
            targetModel.setRbmName(authUser.getFullName());
35669 ranu 2040
            // Use partner count from mtdBillingData (same source as Today ARR page)
2041
            Set<Integer> mtdFofoIds = mtdFofoIdsByAuthId.getOrDefault(rbmAuthId, Collections.emptySet());
2042
            targetModel.setPartnerCount(mtdFofoIds.size());
35631 ranu 2043
 
2044
            // Categorize each partner - each partner belongs to ONE category only
35665 ranu 2045
            // Priority: PlanToday > CarryForward > ZeroBilling > Untouched > FuturePlan > Normal
36181 ranu 2046
            // Revival is counted separately (just for display, doesn't affect categorization)
35631 ranu 2047
            Set<Integer> planTodayPartners = new HashSet<>();
2048
            Set<Integer> carryForwardPartners = new HashSet<>();
2049
            Set<Integer> untouchedPartners = new HashSet<>();
2050
            Set<Integer> zeroBillingPartners = new HashSet<>();
2051
            Set<Integer> futurePlanPartners = new HashSet<>();
2052
            Set<Integer> normalPartners = new HashSet<>();
36181 ranu 2053
            Set<Integer> revivalPartners = new HashSet<>();
35631 ranu 2054
 
2055
            for (Integer fofoId : validFofoIds) {
2056
                // Get collection plan rank (from optimized rank map)
2057
                int rank = allCollectionRankMap.getOrDefault(fofoId, 5); // default to Normal if no plan
2058
 
2059
                // Check if partner has zero billing in MTD
2060
                boolean hasZeroBilling = !allMtdBilledFofoIds.contains(fofoId);
2061
 
36181 ranu 2062
                // Count REVIVAL partners separately (just for display, doesn't affect categorization)
2063
                FofoStore store = finalFofoStoresMap.get(fofoId);
2064
                if (store != null && ActivationType.REVIVAL.equals(store.getActivationType())) {
2065
                    revivalPartners.add(fofoId);
2066
                }
2067
 
35631 ranu 2068
                // Assign to category based on priority
2069
                if (rank == 1) {
2070
                    planTodayPartners.add(fofoId);
2071
                } else if (rank == 2) {
2072
                    carryForwardPartners.add(fofoId);
35665 ranu 2073
                } else if (hasZeroBilling) {
2074
                    zeroBillingPartners.add(fofoId);
35631 ranu 2075
                } else if (rank == 3) {
2076
                    untouchedPartners.add(fofoId);
2077
                } else if (rank == 4) {
2078
                    futurePlanPartners.add(fofoId);
2079
                } else {
2080
                    normalPartners.add(fofoId);
2081
                }
2082
            }
2083
 
2084
            // Set counts
2085
            targetModel.setCreditCollection(0); // Credit collection is handled in separate list
2086
            targetModel.setPlanToday(planTodayPartners.size());
2087
            targetModel.setCarryForward(carryForwardPartners.size());
2088
            targetModel.setUntouched(untouchedPartners.size());
2089
            targetModel.setZeroBilling(zeroBillingPartners.size());
2090
            targetModel.setFuturePlan(futurePlanPartners.size());
2091
            targetModel.setNormal(normalPartners.size());
36181 ranu 2092
            targetModel.setRevival(revivalPartners.size());
35631 ranu 2093
 
2094
            // Today Target = PlanToday + CarryForward + ZeroBilling + Untouched
2095
            // These are mutually exclusive now, so we can sum them
2096
            long todayTarget = planTodayPartners.size() +
2097
                    carryForwardPartners.size() + zeroBillingPartners.size() + untouchedPartners.size();
2098
            targetModel.setTodayTargetOfCall(todayTarget);
2099
 
2100
            // Create set of partners in Today Target categories
2101
            Set<Integer> todayTargetPartners = new HashSet<>();
2102
            todayTargetPartners.addAll(planTodayPartners);
2103
            todayTargetPartners.addAll(carryForwardPartners);
2104
            todayTargetPartners.addAll(zeroBillingPartners);
2105
            todayTargetPartners.addAll(untouchedPartners);
2106
 
36284 ranu 2107
            // Value Achieved = All distinct partners called today (from pre-fetched call logs)
37010 ranu 2108
            List<AgentCallLog> l1Logs = callLogsByAuthId.get((long) rbmAuthId);
2109
            long[] callStats = getCallStatsFromLogs(l1Logs, mobileToFofoIdMap);
36276 ranu 2110
            targetModel.setValueTargetAchieved(callStats[0]);
2111
            targetModel.setTotalRecordingCalls(callStats[1]);
2112
            targetModel.setUniqueRecordingCalls(callStats[2]);
37022 ranu 2113
            targetModel.setTriedUniqueCalls(computeUniqueTriedCount(l1Logs, mobileToFofoIdMap));
2114
            targetModel.setTgtCalls(computeUniqueTriedCountForTargets(l1Logs, mobileToFofoIdMap, todayTargetPartners));
35631 ranu 2115
 
37012 ranu 2116
            // Keep todayRemarks for movedToFuture calculation
35843 ranu 2117
            List<PartnerCollectionRemark> todayRemarks = remarksByAuthId.getOrDefault(rbmAuthId, Collections.emptyList());
2118
 
35631 ranu 2119
            // Moved to Future = Partners in Future Plan category who have a remark today
2120
            // These are partners who were contacted today but moved to a future date
2121
            Set<Integer> todayRemarkedFofoIds = todayRemarks.stream()
2122
                    .map(PartnerCollectionRemark::getFofoId)
2123
                    .collect(Collectors.toSet());
2124
            long movedToFuture = futurePlanPartners.stream()
2125
                    .filter(todayRemarkedFofoIds::contains)
2126
                    .count();
2127
            targetModel.setMovedToFuture(movedToFuture);
2128
 
2129
            // Set out of sequence count for this RBM
2130
            targetModel.setOutOfSequenceCount(outOfSequenceCountByAuthId.getOrDefault(rbmAuthId, 0L));
2131
 
2132
            rbmCallTargetModels.add(targetModel);
2133
        }
2134
 
2135
        // Process L2 RBMs (escalated ticket logic with categorization)
36225 ranu 2136
        // For users who are both L1 and L2, merge their L1 calling target into L2 model
35631 ranu 2137
        for (int l2AuthId : l2AuthIds) {
2138
            AuthUser authUser = authUserMap.get(l2AuthId);
2139
            if (authUser == null) {
2140
                continue;
2141
            }
2142
 
2143
            List<Integer> l2FofoIdList = l2AuthIdToFofoIds.getOrDefault(l2AuthId, Collections.emptyList());
2144
 
35816 ranu 2145
            // For L2, use unique fofoIds with RBM_L2_ESCALATION remark as target
35662 ranu 2146
            Set<Integer> l2TargetFofoIds = new HashSet<>(l2FofoIdList);
35631 ranu 2147
 
37022 ranu 2148
            // Collected from the L1-merge branch below when the user is also L1. Used later
2149
            // for the TGT Calls scope: L2 escalation + L1 target buckets.
2150
            Set<Integer> l1MergedTargetPartners = new HashSet<>();
2151
 
35631 ranu 2152
            RbmCallTargetModel l2Model = new RbmCallTargetModel();
2153
            l2Model.setAuthId(l2AuthId);
2154
            l2Model.setRbmName(authUser.getFullName() + " (L2)");
2155
            l2Model.setL2Position(true);
35816 ranu 2156
            l2Model.setL2CallingList(l2TargetFofoIds.size());
36228 ranu 2157
            // Partner count: if user is also L1, use L1 partner count (MTD targeted partners)
2158
            if (l1AuthIds.contains(l2AuthId)) {
2159
                Set<Integer> mtdFofoIds = mtdFofoIdsByAuthId.getOrDefault(l2AuthId, Collections.emptySet());
2160
                l2Model.setPartnerCount(mtdFofoIds.size());
2161
            } else {
2162
                List<Integer> l2AssignedFofoIds = rbmToFofoIdsMap.getOrDefault(l2AuthId, Collections.emptyList());
2163
                l2Model.setPartnerCount(l2AssignedFofoIds.size());
2164
            }
35631 ranu 2165
 
36229 ranu 2166
            // If user is also L1, calculate full L1 breakdown and merge into L2 model
36225 ranu 2167
            if (l1AuthIds.contains(l2AuthId) && storeGuyMap.containsKey(authUser.getEmailId())) {
36229 ranu 2168
                // Get only L1 RBM position partners (not L2 partners) from partner_position
36225 ranu 2169
                List<Position> positions = positionsByAuthId.getOrDefault(l2AuthId, Collections.emptyList());
36229 ranu 2170
                Set<Integer> l1RbmPositionIds = positions.stream()
2171
                        .filter(p -> ProfitMandiConstants.TICKET_CATEGORY_RBM == p.getCategoryId()
2172
                                && EscalationType.L1.equals(p.getEscalationType()))
2173
                        .map(Position::getId)
2174
                        .collect(Collectors.toSet());
2175
                // Fetch partner_position for only L1 RBM positions of this user
2176
                List<Integer> fofoIdList = partnerPositionRepository
2177
                        .selectByPositionIds(new ArrayList<>(l1RbmPositionIds)).stream()
2178
                        .map(pp -> pp.getFofoId())
2179
                        .distinct()
2180
                        .collect(Collectors.toList());
2181
                boolean isRBMAndL1 = !l1RbmPositionIds.isEmpty();
36225 ranu 2182
                List<Integer> l1FofoIds = new ArrayList<>(fofoIdList);
2183
                if (isRBMAndL1) {
2184
                    Map<Integer, PartnerCollectionRemark> partnerCollectionRemarks = new HashMap<>();
2185
                    for (Integer fofoId : fofoIdList) {
2186
                        if (allPartnerCollectionRemarks.containsKey(fofoId)) {
2187
                            partnerCollectionRemarks.put(fofoId, allPartnerCollectionRemarks.get(fofoId));
2188
                        }
2189
                    }
2190
                    l1FofoIds = partnerCollectionRemarks.entrySet().stream()
2191
                            .filter(entry -> {
2192
                                PartnerCollectionRemark pcrMap = entry.getValue();
2193
                                return !(CollectionRemark.RBM_L2_ESCALATION.equals(pcrMap.getRemark())
2194
                                        || CollectionRemark.SALES_ESCALATION.equals(pcrMap.getRemark()));
2195
                            })
2196
                            .map(Map.Entry::getKey)
2197
                            .collect(Collectors.toList());
2198
                }
2199
                Map<Integer, FofoStore> finalFofoStoresMap2 = fofoStoresMap;
2200
                List<Integer> validL1FofoIds = l1FofoIds.stream()
2201
                        .filter(fofoId -> {
2202
                            FofoStore store = finalFofoStoresMap2.get(fofoId);
2203
                            if (store == null || store.isInternal()) return false;
2204
                            return ActivationType.ACTIVE.equals(store.getActivationType())
2205
                                    || ActivationType.REVIVAL.equals(store.getActivationType());
2206
                        })
2207
                        .collect(Collectors.toList());
35631 ranu 2208
 
36229 ranu 2209
                // Categorize L1 partners — same logic as L1 processing
2210
                Set<Integer> planTodayPartners = new HashSet<>();
2211
                Set<Integer> carryForwardPartners = new HashSet<>();
2212
                Set<Integer> untouchedPartners = new HashSet<>();
2213
                Set<Integer> zeroBillingPartners = new HashSet<>();
2214
                Set<Integer> futurePlanPartners = new HashSet<>();
2215
                Set<Integer> normalPartners = new HashSet<>();
2216
                Set<Integer> revivalPartners = new HashSet<>();
2217
 
36225 ranu 2218
                for (Integer fofoId : validL1FofoIds) {
2219
                    int rank = allCollectionRankMap.getOrDefault(fofoId, 5);
2220
                    boolean hasZeroBilling = !allMtdBilledFofoIds.contains(fofoId);
36229 ranu 2221
                    FofoStore store = finalFofoStoresMap2.get(fofoId);
2222
                    if (store != null && ActivationType.REVIVAL.equals(store.getActivationType())) {
2223
                        revivalPartners.add(fofoId);
36225 ranu 2224
                    }
36229 ranu 2225
                    if (rank == 1) {
2226
                        planTodayPartners.add(fofoId);
2227
                    } else if (rank == 2) {
2228
                        carryForwardPartners.add(fofoId);
2229
                    } else if (hasZeroBilling) {
2230
                        zeroBillingPartners.add(fofoId);
2231
                    } else if (rank == 3) {
2232
                        untouchedPartners.add(fofoId);
2233
                    } else if (rank == 4) {
2234
                        futurePlanPartners.add(fofoId);
2235
                    } else {
2236
                        normalPartners.add(fofoId);
2237
                    }
36225 ranu 2238
                }
36229 ranu 2239
 
2240
                // Set L1 breakdown fields on L2 model
2241
                l2Model.setPlanToday(planTodayPartners.size());
2242
                l2Model.setCarryForward(carryForwardPartners.size());
2243
                l2Model.setZeroBilling(zeroBillingPartners.size());
2244
                l2Model.setUntouched(untouchedPartners.size());
2245
                l2Model.setFuturePlan(futurePlanPartners.size());
2246
                l2Model.setNormal(normalPartners.size());
2247
                l2Model.setRevival(revivalPartners.size());
2248
 
2249
                long l1OwnTarget = planTodayPartners.size() + carryForwardPartners.size()
2250
                        + zeroBillingPartners.size() + untouchedPartners.size();
2251
 
37022 ranu 2252
                // Feed the L1 target sets up to the outer scope so TGT Calls can include them.
2253
                l1MergedTargetPartners.addAll(planTodayPartners);
2254
                l1MergedTargetPartners.addAll(carryForwardPartners);
2255
                l1MergedTargetPartners.addAll(zeroBillingPartners);
2256
                l1MergedTargetPartners.addAll(untouchedPartners);
2257
 
36229 ranu 2258
                // Today Target = own L1 target + L2 escalation
2259
                l2Model.setTodayTargetOfCall(l2TargetFofoIds.size() + l1OwnTarget);
2260
 
2261
                // Moved to Future from L1 partners
2262
                List<PartnerCollectionRemark> todayRemarks = remarksByAuthId.getOrDefault(l2AuthId, Collections.emptyList());
2263
                Set<Integer> todayRemarkedFofoIds = todayRemarks.stream()
2264
                        .map(PartnerCollectionRemark::getFofoId)
2265
                        .collect(Collectors.toSet());
2266
                long movedToFuture = futurePlanPartners.stream()
2267
                        .filter(todayRemarkedFofoIds::contains)
2268
                        .count();
2269
                l2Model.setMovedToFuture(movedToFuture);
2270
            } else {
2271
                // Pure L2 (not also L1) — target is only L2 escalation
2272
                l2Model.setTodayTargetOfCall(l2TargetFofoIds.size());
36225 ranu 2273
            }
2274
 
36284 ranu 2275
            // Value Achieved = All distinct partners called today (from pre-fetched call logs)
37010 ranu 2276
            List<AgentCallLog> l2Logs = callLogsByAuthId.get((long) l2AuthId);
2277
            long[] l2CallStats = getCallStatsFromLogs(l2Logs, mobileToFofoIdMap);
36276 ranu 2278
            l2Model.setValueTargetAchieved(l2CallStats[0]);
2279
            l2Model.setTotalRecordingCalls(l2CallStats[1]);
2280
            l2Model.setUniqueRecordingCalls(l2CallStats[2]);
37022 ranu 2281
            l2Model.setTriedUniqueCalls(computeUniqueTriedCount(l2Logs, mobileToFofoIdMap));
35631 ranu 2282
 
37022 ranu 2283
            // TGT Calls scope: L2 escalation + (if user is also L1) the L1 target buckets.
2284
            Set<Integer> l2TgtSet = new HashSet<>(l2TargetFofoIds);
2285
            l2TgtSet.addAll(l1MergedTargetPartners);
2286
            l2Model.setTgtCalls(computeUniqueTriedCountForTargets(l2Logs, mobileToFofoIdMap, l2TgtSet));
2287
 
35631 ranu 2288
            l2Model.setOutOfSequenceCount(outOfSequenceCountByAuthId.getOrDefault(l2AuthId, 0L));
2289
            rbmCallTargetModels.add(l2Model);
2290
        }
2291
 
36210 ranu 2292
        // Process L3 RBMs (escalated ticket logic with categorization)
2293
        for (int l3AuthId : l3AuthIds) {
2294
            AuthUser authUser = authUserMap.get(l3AuthId);
2295
            if (authUser == null) {
2296
                continue;
2297
            }
2298
 
2299
            List<Integer> l3FofoIdList = l3AuthIdToFofoIds.getOrDefault(l3AuthId, Collections.emptyList());
2300
 
2301
            // For L3, use unique fofoIds with RBM_L3_ESCALATION remark as target
2302
            Set<Integer> l3TargetFofoIds = new HashSet<>(l3FofoIdList);
2303
 
2304
            RbmCallTargetModel l3Model = new RbmCallTargetModel();
2305
            l3Model.setAuthId(l3AuthId);
2306
            l3Model.setRbmName(authUser.getFullName() + " (L3)");
2307
            l3Model.setL3Position(true);
2308
            l3Model.setL3CallingList(l3TargetFofoIds.size());
36359 ranu 2309
            // Partner count = total assigned partners (excluding internal)
2310
            Map<Integer, FofoStore> finalFofoStoresMapL3 = fofoStoresMap;
36210 ranu 2311
            List<Integer> l3AssignedFofoIds = rbmToFofoIdsMap.getOrDefault(l3AuthId, Collections.emptyList());
36359 ranu 2312
            long l3ExternalPartnerCount = l3AssignedFofoIds.stream()
2313
                    .filter(fofoId -> {
2314
                        FofoStore store = finalFofoStoresMapL3.get(fofoId);
2315
                        return store != null && !store.isInternal();
2316
                    })
2317
                    .count();
2318
            l3Model.setPartnerCount(l3ExternalPartnerCount);
36210 ranu 2319
 
2320
            // L3 Target = partners with RBM_L3_ESCALATION as latest remark
2321
            l3Model.setTodayTargetOfCall(l3TargetFofoIds.size());
2322
 
36284 ranu 2323
            // Value Achieved = All distinct partners called today (from pre-fetched call logs)
37010 ranu 2324
            List<AgentCallLog> l3Logs = callLogsByAuthId.get((long) l3AuthId);
2325
            long[] l3CallStats = getCallStatsFromLogs(l3Logs, mobileToFofoIdMap);
36276 ranu 2326
            l3Model.setValueTargetAchieved(l3CallStats[0]);
2327
            l3Model.setTotalRecordingCalls(l3CallStats[1]);
2328
            l3Model.setUniqueRecordingCalls(l3CallStats[2]);
37022 ranu 2329
            l3Model.setTriedUniqueCalls(computeUniqueTriedCount(l3Logs, mobileToFofoIdMap));
2330
            // L3 TGT Calls scope: L3 escalation partners.
2331
            l3Model.setTgtCalls(computeUniqueTriedCountForTargets(l3Logs, mobileToFofoIdMap, l3TargetFofoIds));
36210 ranu 2332
 
2333
            l3Model.setOutOfSequenceCount(outOfSequenceCountByAuthId.getOrDefault(l3AuthId, 0L));
2334
            rbmCallTargetModels.add(l3Model);
2335
        }
2336
 
36225 ranu 2337
        // Group models by escalation level
36210 ranu 2338
        Map<Integer, RbmCallTargetModel> l3ModelsByAuthId = new HashMap<>();
35631 ranu 2339
        Map<Integer, RbmCallTargetModel> l2ModelsByAuthId = new HashMap<>();
2340
        Map<Integer, RbmCallTargetModel> l1ModelsByAuthId = new HashMap<>();
2341
        for (RbmCallTargetModel m : rbmCallTargetModels) {
36210 ranu 2342
            if (m.isL3Position()) {
2343
                l3ModelsByAuthId.put(m.getAuthId(), m);
2344
            } else if (m.isL2Position()) {
35631 ranu 2345
                l2ModelsByAuthId.put(m.getAuthId(), m);
2346
            } else {
2347
                l1ModelsByAuthId.put(m.getAuthId(), m);
2348
            }
2349
        }
2350
 
36225 ranu 2351
        // Build partner-based hierarchy using partner_position table
2352
        // Map positionId -> Position for quick lookup
2353
        Map<Integer, Position> positionByIdMap = allRbmPositions.stream()
2354
                .collect(Collectors.toMap(Position::getId, p -> p, (a, b) -> a));
2355
 
2356
        // Get all RBM position IDs and fetch their partner assignments
2357
        List<Integer> allRbmPositionIds = allRbmPositions.stream()
2358
                .map(Position::getId).collect(Collectors.toList());
2359
        List<com.spice.profitmandi.dao.entity.cs.PartnerPosition> allPartnerPositions =
2360
                partnerPositionRepository.selectByPositionIds(allRbmPositionIds);
2361
 
2362
        // Build partnerId -> map of escalationType -> Set<authUserIds>
2363
        // This tells us for each partner, who is their L1, L2, L3
2364
        Map<Integer, Map<EscalationType, Set<Integer>>> partnerToAuthByLevel = new HashMap<>();
2365
        for (com.spice.profitmandi.dao.entity.cs.PartnerPosition pp : allPartnerPositions) {
2366
            Position pos = positionByIdMap.get(pp.getPositionId());
2367
            if (pos != null && pos.getEscalationType() != null) {
2368
                partnerToAuthByLevel
2369
                        .computeIfAbsent(pp.getFofoId(), k -> new HashMap<>())
2370
                        .computeIfAbsent(pos.getEscalationType(), k -> new HashSet<>())
2371
                        .add(pos.getAuthUserId());
2372
            }
2373
        }
2374
 
2375
        // Build L2 -> L1 team map based on shared partners
2376
        // If an L1 and L2 share partners (L1 at L1 level, L2 at L2 level), that L1 belongs under that L2
2377
        Map<Integer, List<RbmCallTargetModel>> l2TeamMap = new LinkedHashMap<>();
2378
        for (RbmCallTargetModel l2Model : l2ModelsByAuthId.values()) {
2379
            l2TeamMap.put(l2Model.getAuthId(), new ArrayList<>());
2380
        }
2381
 
2382
        // Build L3 -> L2 team map based on shared partners
36210 ranu 2383
        Map<Integer, List<RbmCallTargetModel>> l3TeamMap = new LinkedHashMap<>();
2384
        for (RbmCallTargetModel l3Model : l3ModelsByAuthId.values()) {
2385
            l3TeamMap.put(l3Model.getAuthId(), new ArrayList<>());
2386
        }
2387
 
36225 ranu 2388
        // For each L1, find which L2 shares the most partners -> that's their L2
2389
        Set<Integer> addedL1AuthIds = new HashSet<>();
2390
        for (RbmCallTargetModel l1Model : l1ModelsByAuthId.values()) {
2391
            Map<Integer, Integer> l2SharedCount = new HashMap<>(); // l2AuthId -> shared partner count
2392
            for (Map.Entry<Integer, Map<EscalationType, Set<Integer>>> entry : partnerToAuthByLevel.entrySet()) {
2393
                Map<EscalationType, Set<Integer>> levelMap = entry.getValue();
2394
                Set<Integer> l1IdsForPartner = levelMap.getOrDefault(EscalationType.L1, Collections.emptySet());
2395
                Set<Integer> l2AuthIdsForPartner = levelMap.getOrDefault(EscalationType.L2, Collections.emptySet());
2396
                if (l1IdsForPartner.contains(l1Model.getAuthId())) {
2397
                    for (int l2Id : l2AuthIdsForPartner) {
2398
                        if (l2ModelsByAuthId.containsKey(l2Id) && l2Id != l1Model.getAuthId()) {
2399
                            l2SharedCount.merge(l2Id, 1, Integer::sum);
2400
                        }
2401
                    }
2402
                }
2403
            }
2404
            // Assign L1 to the L2 with most shared partners
2405
            if (!l2SharedCount.isEmpty()) {
2406
                int bestL2 = l2SharedCount.entrySet().stream()
2407
                        .max(Map.Entry.comparingByValue()).get().getKey();
2408
                l2TeamMap.get(bestL2).add(l1Model);
2409
                addedL1AuthIds.add(l1Model.getAuthId());
2410
            }
2411
        }
2412
 
2413
        // For each L2, find which L3 shares the most partners -> that's their L3
36210 ranu 2414
        Set<Integer> addedL2AuthIds = new HashSet<>();
2415
        for (RbmCallTargetModel l2Model : l2ModelsByAuthId.values()) {
36225 ranu 2416
            Map<Integer, Integer> l3SharedCount = new HashMap<>();
2417
            for (Map.Entry<Integer, Map<EscalationType, Set<Integer>>> entry : partnerToAuthByLevel.entrySet()) {
2418
                Map<EscalationType, Set<Integer>> levelMap = entry.getValue();
2419
                Set<Integer> l2AuthIdsForPartner = levelMap.getOrDefault(EscalationType.L2, Collections.emptySet());
2420
                Set<Integer> l3AuthIdsForPartner = levelMap.getOrDefault(EscalationType.L3, Collections.emptySet());
2421
                if (l2AuthIdsForPartner.contains(l2Model.getAuthId())) {
2422
                    for (int l3Id : l3AuthIdsForPartner) {
2423
                        if (l3ModelsByAuthId.containsKey(l3Id)) {
2424
                            l3SharedCount.merge(l3Id, 1, Integer::sum);
2425
                        }
2426
                    }
2427
                }
2428
            }
2429
            if (!l3SharedCount.isEmpty()) {
2430
                int bestL3 = l3SharedCount.entrySet().stream()
2431
                        .max(Map.Entry.comparingByValue()).get().getKey();
2432
                l3TeamMap.get(bestL3).add(l2Model);
36210 ranu 2433
                addedL2AuthIds.add(l2Model.getAuthId());
2434
            }
2435
        }
2436
 
36225 ranu 2437
        // For L1s not mapped to any L2, check if they map directly to an L3 via shared partners
2438
        Map<Integer, List<RbmCallTargetModel>> l3DirectL1Map = new LinkedHashMap<>();
2439
        for (RbmCallTargetModel l3Model : l3ModelsByAuthId.values()) {
2440
            l3DirectL1Map.put(l3Model.getAuthId(), new ArrayList<>());
35631 ranu 2441
        }
2442
        for (RbmCallTargetModel l1Model : l1ModelsByAuthId.values()) {
36225 ranu 2443
            if (addedL1AuthIds.contains(l1Model.getAuthId())) continue;
2444
            Map<Integer, Integer> l3SharedCount = new HashMap<>();
2445
            for (Map.Entry<Integer, Map<EscalationType, Set<Integer>>> entry : partnerToAuthByLevel.entrySet()) {
2446
                Map<EscalationType, Set<Integer>> levelMap = entry.getValue();
2447
                Set<Integer> l1IdsForPartner = levelMap.getOrDefault(EscalationType.L1, Collections.emptySet());
2448
                Set<Integer> l3AuthIdsForPartner = levelMap.getOrDefault(EscalationType.L3, Collections.emptySet());
2449
                if (l1IdsForPartner.contains(l1Model.getAuthId())) {
2450
                    for (int l3Id : l3AuthIdsForPartner) {
2451
                        if (l3ModelsByAuthId.containsKey(l3Id)) {
2452
                            l3SharedCount.merge(l3Id, 1, Integer::sum);
2453
                        }
2454
                    }
2455
                }
2456
            }
2457
            if (!l3SharedCount.isEmpty()) {
2458
                int bestL3 = l3SharedCount.entrySet().stream()
2459
                        .max(Map.Entry.comparingByValue()).get().getKey();
2460
                l3DirectL1Map.get(bestL3).add(l1Model);
35631 ranu 2461
                addedL1AuthIds.add(l1Model.getAuthId());
2462
            }
2463
        }
2464
 
36225 ranu 2465
        // Build sorted result: L3 -> L2 -> L1 (with direct L1s under L3 if no L2)
35631 ranu 2466
        List<RbmCallTargetModel> sortedModels = new ArrayList<>();
2467
 
36210 ranu 2468
        List<RbmCallTargetModel> l3Sorted = new ArrayList<>(l3ModelsByAuthId.values());
2469
        l3Sorted.sort(Comparator.comparing(RbmCallTargetModel::getRbmName));
35631 ranu 2470
 
36210 ranu 2471
        for (RbmCallTargetModel l3Model : l3Sorted) {
2472
            sortedModels.add(l3Model);
2473
            List<RbmCallTargetModel> l2Team = l3TeamMap.getOrDefault(l3Model.getAuthId(), Collections.emptyList());
2474
            l2Team.sort(Comparator.comparing(RbmCallTargetModel::getRbmName));
2475
            for (RbmCallTargetModel l2Model : l2Team) {
2476
                sortedModels.add(l2Model);
2477
                List<RbmCallTargetModel> l1Team = l2TeamMap.getOrDefault(l2Model.getAuthId(), Collections.emptyList());
2478
                l1Team.sort(Comparator.comparing(RbmCallTargetModel::getRbmName));
2479
                sortedModels.addAll(l1Team);
2480
            }
36225 ranu 2481
            // Add L1s that report directly to this L3 (no L2 in between)
2482
            List<RbmCallTargetModel> directL1Team = l3DirectL1Map.getOrDefault(l3Model.getAuthId(), Collections.emptyList());
2483
            directL1Team.sort(Comparator.comparing(RbmCallTargetModel::getRbmName));
2484
            sortedModels.addAll(directL1Team);
36210 ranu 2485
        }
2486
 
36225 ranu 2487
        // Add L2s not mapped to any L3
36210 ranu 2488
        List<RbmCallTargetModel> unmappedL2 = new ArrayList<>();
2489
        for (RbmCallTargetModel l2Model : l2ModelsByAuthId.values()) {
2490
            if (!addedL2AuthIds.contains(l2Model.getAuthId())) {
2491
                unmappedL2.add(l2Model);
2492
            }
2493
        }
2494
        unmappedL2.sort(Comparator.comparing(RbmCallTargetModel::getRbmName));
2495
        for (RbmCallTargetModel l2Model : unmappedL2) {
35631 ranu 2496
            sortedModels.add(l2Model);
2497
            List<RbmCallTargetModel> team = l2TeamMap.getOrDefault(l2Model.getAuthId(), Collections.emptyList());
2498
            team.sort(Comparator.comparing(RbmCallTargetModel::getRbmName));
2499
            sortedModels.addAll(team);
2500
        }
2501
 
36225 ranu 2502
        // Add any L1s not mapped to any L2 or L3
35631 ranu 2503
        List<RbmCallTargetModel> unmappedL1 = new ArrayList<>();
2504
        for (RbmCallTargetModel m : l1ModelsByAuthId.values()) {
2505
            if (!addedL1AuthIds.contains(m.getAuthId())) {
2506
                unmappedL1.add(m);
2507
            }
2508
        }
2509
        unmappedL1.sort(Comparator.comparing(RbmCallTargetModel::getRbmName));
2510
        sortedModels.addAll(unmappedL1);
2511
 
2512
        LOGGER.info("RBM Call Target - TOTAL TIME: {}ms, RBM count: {}", System.currentTimeMillis() - methodStart, sortedModels.size());
2513
        return sortedModels;
2514
    }
2515
 
35843 ranu 2516
    /**
37011 ranu 2517
     * Rule: count each distinct customer number this RBM attempted today, based on
37012 ranu 2518
     * the vendor-reported customer-side outcome (AgentCallLog.customerStatus):
2519
     *   - If ANY call to this number has customerStatus that is not "Missed"
2520
     *     → count as 1 (customer was reached at some point).
2521
     *   - Else (every call to this number was Missed) → count only if attempts >= 3
37011 ranu 2522
     *     (persistence credit).
37012 ranu 2523
     * null/empty customerStatus is treated as "not Missed" (favours counting when
2524
     * the vendor didn't classify the outcome).
35843 ranu 2525
     */
37022 ranu 2526
    private long computeUniqueTriedCount(List<AgentCallLog> callLogs,
2527
                                         Map<String, Integer> mobileToFofoIdMap) {
37010 ranu 2528
        if (callLogs == null || callLogs.isEmpty()) return 0L;
37022 ranu 2529
        // Group by fofoId (not raw number) — numbers that don't resolve to a partner via
2530
        // retailer_contact/address are skipped. Once a number gets added to retailer_contact
2531
        // it'll automatically resolve and start counting on the next page load.
2532
        Map<Integer, List<AgentCallLog>> byFofoId = new HashMap<>();
37010 ranu 2533
        for (AgentCallLog log : callLogs) {
2534
            String num = log.getCustomerNumber();
2535
            if (num == null) continue;
2536
            String normalized = num.startsWith("+91") ? num.substring(3) : num;
37022 ranu 2537
            Integer fofoId = mobileToFofoIdMap != null ? mobileToFofoIdMap.get(normalized) : null;
2538
            if (fofoId == null) continue; // unknown number — excluded from Tried
2539
            byFofoId.computeIfAbsent(fofoId, k -> new ArrayList<>()).add(log);
36284 ranu 2540
        }
37010 ranu 2541
        long count = 0L;
37022 ranu 2542
        for (List<AgentCallLog> logs : byFofoId.values()) {
37012 ranu 2543
            boolean hasNonMissed = false;
2544
            for (AgentCallLog l : logs) {
2545
                if (!isMissedCustomerStatus(l.getCustomerStatus())) {
2546
                    hasNonMissed = true;
37010 ranu 2547
                    break;
36284 ranu 2548
                }
2549
            }
37012 ranu 2550
            if (hasNonMissed) count++;
2551
            else if (logs.size() >= 3) count++;
36284 ranu 2552
        }
37010 ranu 2553
        return count;
36284 ranu 2554
    }
2555
 
2556
    /**
36276 ranu 2557
     * Returns call stats: [0] = called count, [1] = total recording calls, [2] = unique recording calls
2558
     */
2559
    public long[] getCallStats(long authId, LocalDate date) {
36234 ranu 2560
        List<AgentCallLog> callLogs = agentCallLogRepository.findByAuthIdAndDate(authId, date);
2561
 
35843 ranu 2562
        if (callLogs == null || callLogs.isEmpty()) {
36276 ranu 2563
            return new long[]{0, 0, 0};
35843 ranu 2564
        }
2565
 
2566
        Set<Integer> calledFofoIds = new HashSet<>();
2567
        Set<String> calledNumbersWithoutFofoId = new HashSet<>();
36276 ranu 2568
        long totalRecordingCalls = 0;
2569
        Set<String> uniqueRecordingNumbers = new HashSet<>();
35843 ranu 2570
 
2571
        for (AgentCallLog callLog : callLogs) {
2572
            String customerNumber = callLog.getCustomerNumber();
2573
            if (customerNumber != null) {
2574
                // Normalize the phone number (remove +91 prefix if present)
2575
                String normalized = customerNumber.startsWith("+91") ? customerNumber.substring(3) : customerNumber;
2576
 
2577
                // Find fofoId from retailer_contact or address
2578
                Integer fofoId = findFofoIdByMobile(normalized);
2579
                if (fofoId != null) {
2580
                    calledFofoIds.add(fofoId);
2581
                } else {
2582
                    // Number not found in retailer_contact or address, count by distinct number
2583
                    calledNumbersWithoutFofoId.add(normalized);
2584
                }
36276 ranu 2585
 
2586
                // Count calls with recordings
2587
                if (callLog.getRecordingUrl() != null && !callLog.getRecordingUrl().isEmpty()
2588
                        && !"None".equalsIgnoreCase(callLog.getRecordingUrl())) {
2589
                    totalRecordingCalls++;
2590
                    uniqueRecordingNumbers.add(normalized);
2591
                }
35843 ranu 2592
            }
2593
        }
2594
 
2595
        // Total called = distinct fofoIds + distinct numbers without fofoId mapping
36276 ranu 2596
        long calledCount = calledFofoIds.size() + calledNumbersWithoutFofoId.size();
2597
        return new long[]{calledCount, totalRecordingCalls, uniqueRecordingNumbers.size()};
35843 ranu 2598
    }
2599
 
35852 ranu 2600
    @Override
2601
    public List<List<String>> getAllCallDataByDate(LocalDate date) throws Exception {
2602
        List<List<String>> rows = new ArrayList<>();
2603
 
2604
        // Add header row
2605
        rows.add(Arrays.asList("RBM Name", "Partner Name", "Code", "Remark", "Call Status", "Call Duration", "Call Date Time", "Recording URL"));
2606
 
2607
        // Get all call logs for the date
2608
        List<AgentCallLog> allCallLogs = agentCallLogRepository.findAllByDate(date);
2609
        LOGGER.info("getAllCallDataByDate: Found {} call logs for date {}", allCallLogs.size(), date);
2610
 
2611
        if (allCallLogs.isEmpty()) {
2612
            return rows;
2613
        }
2614
 
2615
        // Get unique authIds from call logs
2616
        Set<Long> authIds = allCallLogs.stream()
2617
                .map(AgentCallLog::getAuthId)
2618
                .collect(Collectors.toSet());
2619
 
2620
        // Get auth users for RBM names
2621
        List<Integer> authIdInts = authIds.stream().map(Long::intValue).collect(Collectors.toList());
2622
        Map<Integer, AuthUser> authUserMap = authRepository.selectByIds(authIdInts).stream()
2623
                .collect(Collectors.toMap(AuthUser::getId, au -> au, (a, b) -> a));
2624
 
2625
        // Build a map of normalized customer number -> fofoId
2626
        Map<String, Integer> customerToFofoIdMap = new HashMap<>();
2627
        Set<String> normalizedNumbers = new HashSet<>();
2628
 
2629
        for (AgentCallLog callLog : allCallLogs) {
2630
            String customerNumber = callLog.getCustomerNumber();
2631
            if (customerNumber != null) {
2632
                String normalized = customerNumber.startsWith("+91") ? customerNumber.substring(3) : customerNumber;
2633
                normalizedNumbers.add(normalized);
2634
            }
2635
        }
2636
 
2637
        // For each normalized number, find fofoId
2638
        for (String mobile : normalizedNumbers) {
2639
            Integer fofoId = findFofoIdByMobile(mobile);
2640
            if (fofoId != null) {
2641
                customerToFofoIdMap.put(mobile, fofoId);
2642
            }
2643
        }
2644
 
2645
        // Get unique fofoIds for retailer lookup
2646
        Set<Integer> fofoIds = new HashSet<>(customerToFofoIdMap.values());
2647
        Map<Integer, CustomRetailer> retailerMap = Collections.emptyMap();
2648
        if (!fofoIds.isEmpty()) {
2649
            try {
2650
                retailerMap = retailerService.getFofoRetailers(new ArrayList<>(fofoIds));
2651
            } catch (ProfitMandiBusinessException e) {
2652
                LOGGER.error("Error fetching fofo stores", e);
2653
            }
2654
        }
2655
 
2656
        // Get remarks for these fofoIds on this date
2657
        Map<Integer, List<PartnerCollectionRemark>> fofoRemarkMap = new HashMap<>();
2658
        if (!fofoIds.isEmpty()) {
2659
            List<PartnerCollectionRemark> dateRemarks = partnerCollectionRemarkRepository
2660
                    .selectAllByFofoIdsOnDate(new ArrayList<>(fofoIds), date);
2661
            for (PartnerCollectionRemark remark : dateRemarks) {
2662
                fofoRemarkMap.computeIfAbsent(remark.getFofoId(), k -> new ArrayList<>()).add(remark);
2663
            }
2664
        }
2665
 
2666
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("dd-MM-yyyy hh:mm a");
2667
 
2668
        // Build rows
2669
        for (AgentCallLog callLog : allCallLogs) {
2670
            String customerNumber = callLog.getCustomerNumber();
2671
            if (customerNumber == null) {
2672
                continue;
2673
            }
2674
 
2675
            // Get RBM Name
2676
            AuthUser authUser = authUserMap.get((int) callLog.getAuthId());
2677
            String rbmName = authUser != null ? authUser.getFullName() : "Unknown";
2678
 
2679
            String normalized = customerNumber.startsWith("+91") ? customerNumber.substring(3) : customerNumber;
2680
            Integer fofoId = customerToFofoIdMap.get(normalized);
2681
 
2682
            String partyName = "Unknown";
2683
            String code = "-";
2684
 
2685
            if (fofoId != null) {
2686
                CustomRetailer retailer = retailerMap.get(fofoId);
2687
                if (retailer != null) {
2688
                    partyName = retailer.getBusinessName();
2689
                    code = retailer.getCode();
2690
                } else {
2691
                    partyName = "Unknown (" + fofoId + ")";
2692
                }
2693
            } else {
2694
                partyName = "Unknown (" + normalized + ")";
2695
            }
2696
 
2697
            // Get remark if available
2698
            String remarkValue = "-";
2699
            if (fofoId != null && fofoRemarkMap.containsKey(fofoId)) {
2700
                List<PartnerCollectionRemark> remarks = fofoRemarkMap.get(fofoId);
2701
                if (!remarks.isEmpty()) {
2702
                    PartnerCollectionRemark remark = remarks.get(0);
2703
                    remarkValue = remark.getRemark() != null ? remark.getRemark().getValue() : "-";
2704
                    if (remark.getMessage() != null && !remark.getMessage().isEmpty()) {
2705
                        remarkValue = remarkValue + " - " + remark.getMessage();
2706
                    }
2707
                }
2708
            }
2709
 
2710
            // Call status from customerStatus field
2711
            String callStatus = callLog.getCustomerStatus() != null ? callLog.getCustomerStatus() : "-";
2712
            String callDuration = callLog.getCallDuration() != null ? callLog.getCallDuration() : "-";
2713
 
2714
            String callDateTime = "-";
2715
            if (callLog.getCallDate() != null && callLog.getCallTime() != null) {
2716
                LocalDateTime callDateTimeObj = LocalDateTime.of(callLog.getCallDate(), callLog.getCallTime());
2717
                callDateTime = callDateTimeObj.format(dateTimeFormatter);
2718
            }
2719
 
2720
            String recordingUrl = callLog.getRecordingUrl() != null ? callLog.getRecordingUrl() : "-";
2721
 
2722
            rows.add(Arrays.asList(rbmName, partyName, code, remarkValue, callStatus, callDuration, callDateTime, recordingUrl));
2723
        }
2724
 
2725
        return rows;
2726
    }
2727
 
33917 ranu 2728
}