Subversion Repositories SmartDukaan

Rev

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

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