Subversion Repositories SmartDukaan

Rev

Rev 36686 | Rev 36807 | Go to most recent revision | Show entire file | Ignore whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 36686 Rev 36689
Line 176... Line 176...
176
 
176
 
177
        .form-control:focus {
177
        .form-control:focus {
178
            border-color: #ed1c24;
178
            border-color: #ed1c24;
179
            box-shadow: 0 0 0 0.2rem rgba(237, 28, 36, 0.20);
179
            box-shadow: 0 0 0 0.2rem rgba(237, 28, 36, 0.20);
180
        }
180
        }
-
 
181
 
-
 
182
        /* Live camera view */
-
 
183
        .camera-wrap {
-
 
184
            position: relative;
-
 
185
            background: #000;
-
 
186
            border-radius: 10px;
-
 
187
            overflow: hidden;
-
 
188
            aspect-ratio: 4 / 3;
-
 
189
            margin-bottom: 12px;
-
 
190
        }
-
 
191
 
-
 
192
        .camera-wrap video,
-
 
193
        .camera-wrap img {
-
 
194
            width: 100%;
-
 
195
            height: 100%;
-
 
196
            object-fit: cover;
-
 
197
            display: block;
-
 
198
        }
-
 
199
 
-
 
200
        .camera-wrap .cam-overlay {
-
 
201
            position: absolute;
-
 
202
            inset: 0;
-
 
203
            display: flex;
-
 
204
            align-items: center;
-
 
205
            justify-content: center;
-
 
206
            color: #fff;
-
 
207
            font-size: 13px;
-
 
208
            background: rgba(0, 0, 0, 0.5);
-
 
209
            text-align: center;
-
 
210
            padding: 16px;
-
 
211
        }
-
 
212
 
-
 
213
        .btn-capture {
-
 
214
            background: #fff;
-
 
215
            color: #ed1c24;
-
 
216
            border: 3px solid #ed1c24;
-
 
217
            border-radius: 50%;
-
 
218
            width: 64px;
-
 
219
            height: 64px;
-
 
220
            display: inline-flex;
-
 
221
            align-items: center;
-
 
222
            justify-content: center;
-
 
223
            cursor: pointer;
-
 
224
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
-
 
225
        }
-
 
226
 
-
 
227
        .btn-capture:disabled {
-
 
228
            opacity: 0.5;
-
 
229
            cursor: not-allowed;
-
 
230
        }
-
 
231
 
-
 
232
        .btn-capture .inner {
-
 
233
            width: 48px;
-
 
234
            height: 48px;
-
 
235
            border-radius: 50%;
-
 
236
            background: linear-gradient(135deg, #f7931e 0%, #ed1c24 100%);
-
 
237
        }
-
 
238
 
-
 
239
        .capture-row {
-
 
240
            display: flex;
-
 
241
            align-items: center;
-
 
242
            justify-content: center;
-
 
243
            gap: 14px;
-
 
244
            margin: 8px 0 14px;
-
 
245
        }
-
 
246
 
-
 
247
        .btn-retake {
-
 
248
            background: transparent;
-
 
249
            border: 1px solid #b0bec5;
-
 
250
            color: #455a64;
-
 
251
            border-radius: 8px;
-
 
252
            padding: 8px 14px;
-
 
253
            cursor: pointer;
-
 
254
            font-size: 13px;
-
 
255
        }
181
    </style>
256
    </style>
182
</head>
257
</head>
183
<body>
258
<body>
184
 
259
 
185
<div class="geo-card">
260
<div class="geo-card">
Line 192... Line 267...
192
    </div>
267
    </div>
193
    <div class="geo-body">
268
    <div class="geo-body">
194
        <div class="step-indicator">
269
        <div class="step-indicator">
195
            <div class="step-dot current" id="dot1"></div>
270
            <div class="step-dot current" id="dot1"></div>
196
            <div class="step-dot" id="dot2"></div>
271
            <div class="step-dot" id="dot2"></div>
197
            <div class="step-dot" id="dot3"></div>
-
 
198
        </div>
272
        </div>
199
 
273
 
200
        <p class="text-center text-muted mb-3" style="font-size:14px;">
274
        <p class="text-center text-muted mb-3" style="font-size:14px;">
201
            Hello <strong>$!leadName</strong>, please verify your location.
275
            Hello <strong>$!leadName</strong>, please verify your location.
202
        </p>
276
        </p>
Line 212... Line 286...
212
            <button class="btn btn-geo" id="btnVerifyMobile">
286
            <button class="btn btn-geo" id="btnVerifyMobile">
213
                Verify Mobile
287
                Verify Mobile
214
            </button>
288
            </button>
215
        </div>
289
        </div>
216
 
290
 
217
        <!-- STEP 2: Capture Location -->
291
        <!-- STEP 2: Live camera capture (location is grabbed silently when photo is taken) -->
218
        <div class="step" id="step2">
292
        <div class="step" id="step2">
219
            <h6 class="mb-3">Step 2: Capture Your Live Location</h6>
293
            <h6 class="mb-3">Step 2: Capture Your Store Photo</h6>
220
            <p class="text-muted" style="font-size:13px;">
294
            <p class="text-muted" style="font-size:13px;">
221
                Please allow location access when prompted. Stand at your store/shop location.
295
                Stand at your store and tap the red button to take a live photo.
-
 
296
                Your location will be recorded automatically.
222
            </p>
297
            </p>
223
            <button class="btn btn-geo" id="btnCaptureLocation">
-
 
224
                Capture My Location
-
 
225
            </button>
-
 
226
            <div id="locationInfo" class="loc-info" style="display:none;"></div>
-
 
227
            <input type="hidden" id="latitude" value="">
-
 
228
            <input type="hidden" id="longitude" value="">
-
 
229
            <button class="btn btn-geo mt-3" id="btnNextToUpload" style="display:none;">
-
 
230
                Next: Upload Photo
-
 
231
            </button>
-
 
232
        </div>
-
 
233
 
298
 
234
        <!-- STEP 3: Upload Photo & Submit -->
299
            <div class="camera-wrap" id="cameraWrap">
235
        <div class="step" id="step3">
300
                <video id="cameraVideo" autoplay playsinline muted></video>
236
            <h6 class="mb-3">Step 3: Upload Store Photo</h6>
301
                <img id="photoPreview" style="display:none;" alt="Captured photo">
237
            <div class="upload-area" id="uploadArea" onclick="document.getElementById('photoInput').click();">
302
                <div class="cam-overlay" id="cameraOverlay">Starting camera...</div>
238
                <div id="uploadPlaceholder">
303
            </div>
239
                    <svg xmlns="http://www.w3.org/2000/svg" width="42" height="42" viewBox="0 0 24 24" fill="none"
-
 
240
                         stroke="#ed1c24" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"
-
 
-
 
304
 
241
                         style="display:block; margin: 0 auto 8px;">
305
            <div class="capture-row">
242
                        <path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path>
306
                <button type="button" class="btn-capture" id="btnCapture" disabled>
243
                        <circle cx="12" cy="13" r="4"></circle>
307
                    <span class="inner"></span>
244
                    </svg>
308
                </button>
245
                    <p class="text-muted mb-0">Tap to capture or upload store photo</p>
309
                <button type="button" class="btn-retake" id="btnRetake" style="display:none;">
246
                </div>
310
                    Retake
247
                <img id="photoPreview" class="preview-img" style="display:none;">
311
                </button>
248
                <input type="file" id="photoInput" accept="image/*" capture="environment" style="display:none;">
-
 
249
            </div>
312
            </div>
-
 
313
 
250
            <div id="uploadStatus" class="loc-info" style="display:none;"></div>
314
            <div id="captureStatus" class="loc-info" style="display:none;"></div>
-
 
315
 
-
 
316
            <input type="hidden" id="latitude" value="">
-
 
317
            <input type="hidden" id="longitude" value="">
251
            <input type="hidden" id="imageDocumentId" value="0">
318
            <input type="hidden" id="imageDocumentId" value="0">
-
 
319
 
252
            <button class="btn btn-geo mt-3" id="btnSubmit" disabled>
320
            <button class="btn btn-geo mt-2" id="btnSubmit" disabled>
253
                Submit Location
321
                Submit Location
254
            </button>
322
            </button>
255
        </div>
323
        </div>
256
    </div>
324
    </div>
257
</div>
325
</div>
258
 
326
 
-
 
327
<!-- Hidden canvas used to grab a still frame from the live video stream -->
-
 
328
<canvas id="captureCanvas" style="display:none;"></canvas>
-
 
329
 
259
<input type="hidden" id="leadId" value="$leadId">
330
<input type="hidden" id="leadId" value="$leadId">
260
 
331
 
261
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
332
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
262
<script>
333
<script>
263
    var contextPath = "$rc.contextPath";
334
    var contextPath = "$rc.contextPath";
264
 
335
 
-
 
336
    var mediaStream = null; // live camera stream
-
 
337
 
265
    function showStep(num) {
338
    function showStep(num) {
266
        $('.step').removeClass('active');
339
        $('.step').removeClass('active');
267
        $('#step' + num).addClass('active');
340
        $('#step' + num).addClass('active');
268
        for (var i = 1; i <= 3; i++) {
341
        for (var i = 1; i <= 2; i++) {
269
            var dot = $('#dot' + i);
342
            var dot = $('#dot' + i);
270
            dot.removeClass('current done');
343
            dot.removeClass('current done');
271
            if (i < num) dot.addClass('done');
344
            if (i < num) dot.addClass('done');
272
            else if (i === num) dot.addClass('current');
345
            else if (i === num) dot.addClass('current');
273
        }
346
        }
Line 289... Line 362...
289
            method: 'POST',
362
            method: 'POST',
290
            data: {leadId: $('#leadId').val(), mobileNumber: mobile},
363
            data: {leadId: $('#leadId').val(), mobileNumber: mobile},
291
            success: function (resp) {
364
            success: function (resp) {
292
                if (resp.response && resp.response.verified) {
365
                if (resp.response && resp.response.verified) {
293
                    showStep(2);
366
                    showStep(2);
-
 
367
                    startCamera();
294
                } else {
368
                } else {
295
                    $('#mobileError').text('Verification failed. Please try again.').show();
369
                    $('#mobileError').text('Verification failed. Please try again.').show();
296
                }
370
                }
297
            },
371
            },
298
            error: function (xhr) {
372
            error: function (xhr) {
Line 307... Line 381...
307
                btn.prop('disabled', false).text('Verify Mobile');
381
                btn.prop('disabled', false).text('Verify Mobile');
308
            }
382
            }
309
        });
383
        });
310
    });
384
    });
311
 
385
 
312
    // STEP 2: Capture Location
386
    // STEP 2: Live camera — uses getUserMedia so only an in-page live capture
313
    $('#btnCaptureLocation').click(function () {
387
    // is possible (no gallery picker, no file input). Geolocation is grabbed
314
        var btn = $(this);
-
 
315
        btn.prop('disabled', true).html('<span class="spinner"></span> Capturing...');
388
    // silently when the user taps the capture button, so no separate step.
316
        $('#locationInfo').hide();
389
    function startCamera() {
317
 
-
 
318
        if (!navigator.geolocation) {
390
        var overlay = $('#cameraOverlay');
319
            $('#locationInfo').addClass('error').text('Geolocation is not supported by your browser').show();
391
        if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
320
            btn.prop('disabled', false).text('Capture My Location');
392
            overlay.text('Your browser does not support live camera. Please open this link in Chrome/Safari on your phone.');
321
            return;
393
            return;
322
        }
394
        }
-
 
395
        overlay.text('Starting camera...');
-
 
396
        navigator.mediaDevices.getUserMedia({
-
 
397
            video: {facingMode: {ideal: 'environment'}}, // prefer rear camera
-
 
398
            audio: false
-
 
399
        }).then(function (stream) {
-
 
400
            mediaStream = stream;
-
 
401
            var video = document.getElementById('cameraVideo');
-
 
402
            video.srcObject = stream;
-
 
403
            video.onloadedmetadata = function () {
-
 
404
                overlay.hide();
-
 
405
                $('#btnCapture').prop('disabled', false);
-
 
406
            };
-
 
407
        }).catch(function (err) {
-
 
408
            var msg = 'Camera access denied. ';
-
 
409
            if (err && err.name === 'NotAllowedError') msg += 'Please allow camera permission and reload.';
-
 
410
            else if (err && err.name === 'NotFoundError') msg += 'No camera was found on this device.';
-
 
411
            else if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
-
 
412
                msg = 'Live camera requires HTTPS. Please open this link over a secure connection.';
-
 
413
            }
-
 
414
            overlay.text(msg);
-
 
415
        });
-
 
416
    }
323
 
417
 
324
        navigator.geolocation.getCurrentPosition(
-
 
325
                function (position) {
418
    function stopCamera() {
326
                    var lat = position.coords.latitude;
-
 
327
                    var lng = position.coords.longitude;
-
 
328
                    var acc = position.coords.accuracy;
-
 
329
 
-
 
330
                    $('#latitude').val(lat);
419
        if (mediaStream) {
331
                    $('#longitude').val(lng);
-
 
332
                    $('#locationInfo').removeClass('error')
420
            mediaStream.getTracks().forEach(function (t) {
333
                            .html('Location captured successfully!<br>Accuracy: ' + acc.toFixed(0) + ' meters')
-
 
334
                            .show();
421
                t.stop();
335
                    btn.prop('disabled', false).text('Recapture Location');
-
 
336
                    $('#btnNextToUpload').show();
-
 
337
                },
422
            });
338
                function (error) {
-
 
339
                    var msg = 'Unable to get location. ';
-
 
340
                    switch (error.code) {
-
 
341
                        case 1:
423
            mediaStream = null;
342
                            msg += 'Please allow location permission.';
-
 
343
                            break;
424
        }
344
                        case 2:
425
    }
-
 
426
 
345
                            msg += 'Location unavailable.';
427
    // Capture frame + silently grab geolocation
346
                            break;
428
    $('#btnCapture').click(function () {
347
                        case 3:
429
        var btn = $(this);
348
                            msg += 'Request timed out. Try again.';
-
 
349
                            break;
430
        btn.prop('disabled', true);
350
                    }
-
 
351
                    $('#locationInfo').addClass('error').text(msg).show();
-
 
352
                    btn.prop('disabled', false).text('Capture My Location');
431
        $('#captureStatus').removeClass('error').text('Capturing photo and your location...').show();
353
                },
-
 
354
                {enableHighAccuracy: true, timeout: 15000, maximumAge: 0}
-
 
355
        );
-
 
356
    });
-
 
357
 
432
 
-
 
433
        var video = document.getElementById('cameraVideo');
-
 
434
        var canvas = document.getElementById('captureCanvas');
-
 
435
        canvas.width = video.videoWidth;
-
 
436
        canvas.height = video.videoHeight;
-
 
437
        canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);
-
 
438
 
-
 
439
        // Show the still preview, hide the live video, allow retake
-
 
440
        var dataUrl = canvas.toDataURL('image/jpeg', 0.9);
-
 
441
        $('#photoPreview').attr('src', dataUrl).show();
-
 
442
        $('#cameraVideo').hide();
-
 
443
        $('#btnRetake').show();
-
 
444
 
358
    $('#btnNextToUpload').click(function () {
445
        canvas.toBlob(function (blob) {
-
 
446
            if (!blob) {
-
 
447
                btn.prop('disabled', false);
-
 
448
                $('#captureStatus').addClass('error').text('Could not capture photo. Try again.').show();
-
 
449
                return;
-
 
450
            }
-
 
451
            // Upload photo + grab geolocation in parallel
-
 
452
            var uploadDone = uploadPhoto(blob);
-
 
453
            var geoDone = captureGeo();
-
 
454
            $.when(uploadDone, geoDone).done(function () {
-
 
455
                $('#captureStatus').removeClass('error').text('Photo and location captured.').show();
-
 
456
                $('#btnSubmit').prop('disabled', false);
-
 
457
                stopCamera();
-
 
458
            }).fail(function (msg) {
-
 
459
                btn.prop('disabled', false);
-
 
460
                $('#captureStatus').addClass('error').text(msg || 'Capture failed. Try again.').show();
359
        showStep(3);
461
            });
-
 
462
        }, 'image/jpeg', 0.9);
360
    });
463
    });
361
 
464
 
362
    // STEP 3: Upload Photo
465
    $('#btnRetake').click(function () {
363
    $('#photoInput').change(function () {
466
        $('#photoPreview').hide();
364
        var file = this.files[0];
467
        $('#cameraVideo').show();
365
        if (!file) return;
468
        $(this).hide();
366
 
-
 
367
        // Preview
469
        $('#imageDocumentId').val('0');
368
        var reader = new FileReader();
470
        $('#latitude').val('');
369
        reader.onload = function (e) {
471
        $('#longitude').val('');
370
            $('#photoPreview').attr('src', e.target.result).show();
472
        $('#btnSubmit').prop('disabled', true);
371
            $('#uploadPlaceholder').hide();
473
        $('#captureStatus').hide();
-
 
474
        if (!mediaStream) startCamera();
372
            $('#uploadArea').addClass('uploaded');
475
        else $('#btnCapture').prop('disabled', false);
373
        };
476
    });
374
        reader.readAsDataURL(file);
-
 
375
 
477
 
376
        // Upload
478
    function uploadPhoto(blob) {
377
        $('#uploadStatus').removeClass('error').text('Uploading...').show();
479
        var d = $.Deferred();
378
        var formData = new FormData();
480
        var formData = new FormData();
379
        formData.append('file', file);
481
        formData.append('file', blob, 'store-photo.jpg');
380
 
-
 
381
        $.ajax({
482
        $.ajax({
382
            url: contextPath + '/lead-geo/upload-image',
483
            url: contextPath + '/lead-geo/upload-image',
383
            method: 'POST',
484
            method: 'POST',
384
            data: formData,
485
            data: formData,
385
            processData: false,
486
            processData: false,
386
            contentType: false,
487
            contentType: false,
387
            success: function (resp) {
488
            success: function (resp) {
388
                if (resp.response && resp.response.document_id) {
489
                if (resp.response && resp.response.document_id) {
389
                    $('#imageDocumentId').val(resp.response.document_id);
490
                    $('#imageDocumentId').val(resp.response.document_id);
390
                    $('#uploadStatus').removeClass('error').text('Photo uploaded successfully!').show();
491
                    d.resolve();
-
 
492
                } else {
391
                    $('#btnSubmit').prop('disabled', false);
493
                    d.reject('Upload failed.');
392
                }
494
                }
393
            },
495
            },
394
            error: function () {
496
            error: function () {
395
                $('#uploadStatus').addClass('error').text('Upload failed. Please try again.').show();
497
                d.reject('Upload failed. Please try again.');
396
            }
498
            }
397
        });
499
        });
-
 
500
        return d.promise();
398
    });
501
    }
-
 
502
 
-
 
503
    function captureGeo() {
-
 
504
        var d = $.Deferred();
-
 
505
        if (!navigator.geolocation) {
-
 
506
            d.reject('Geolocation not supported.');
-
 
507
            return d.promise();
-
 
508
        }
-
 
509
        navigator.geolocation.getCurrentPosition(
-
 
510
                function (pos) {
-
 
511
                    $('#latitude').val(pos.coords.latitude);
-
 
512
                    $('#longitude').val(pos.coords.longitude);
-
 
513
                    d.resolve();
-
 
514
                },
-
 
515
                function (err) {
-
 
516
                    var msg = 'Unable to get location. ';
-
 
517
                    if (err.code === 1) msg += 'Please allow location permission.';
-
 
518
                    else if (err.code === 2) msg += 'Location unavailable.';
-
 
519
                    else if (err.code === 3) msg += 'Timed out. Try again.';
-
 
520
                    d.reject(msg);
-
 
521
                },
-
 
522
                {enableHighAccuracy: true, timeout: 15000, maximumAge: 0}
-
 
523
        );
-
 
524
        return d.promise();
-
 
525
    }
399
 
526
 
400
    // SUBMIT
527
    // SUBMIT
401
    $('#btnSubmit').click(function () {
528
    $('#btnSubmit').click(function () {
402
        var btn = $(this);
529
        var btn = $(this);
403
        btn.prop('disabled', true).html('<span class="spinner"></span> Submitting...');
530
        btn.prop('disabled', true).html('<span class="spinner"></span> Submitting...');
Line 424... Line 551...
424
                        '<h5 class="mt-3">Thank You!</h5>' +
551
                        '<h5 class="mt-3">Thank You!</h5>' +
425
                        '<p class="text-muted">Your location has been submitted successfully.<br>Our team will review it shortly.</p>' +
552
                        '<p class="text-muted">Your location has been submitted successfully.<br>Our team will review it shortly.</p>' +
426
                        '</div>'
553
                        '</div>'
427
                );
554
                );
428
                $('.step-indicator').remove();
555
                $('.step-indicator').remove();
-
 
556
                stopCamera();
429
            },
557
            },
430
            error: function () {
558
            error: function () {
431
                btn.prop('disabled', false).text('Submit Location');
559
                btn.prop('disabled', false).text('Submit Location');
432
                alert('Submission failed. Please try again.');
560
                alert('Submission failed. Please try again.');
433
            }
561
            }