Subversion Repositories SmartDukaan

Rev

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