Subversion Repositories SmartDukaan

Rev

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

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