Subversion Repositories SmartDukaan

Rev

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