Subversion Repositories SmartDukaan

Rev

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

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