Subversion Repositories SmartDukaan

Rev

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