Subversion Repositories SmartDukaan

Rev

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

Rev Author Line No. Line
36642 ranu 1
<html>
2
<head>
3
    <meta charset="UTF-8">
4
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
5
    <title>Location Verification - SmartDukaan</title>
6
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css">
7
    <style>
8
        body {
9
            background: #f4f6f9;
36686 ranu 10
            font-family: 'Segoe UI', Roboto, sans-serif;
36642 ranu 11
        }
12
 
13
        .geo-card {
14
            max-width: 480px;
15
            margin: 30px auto;
16
            background: #fff;
36686 ranu 17
            border-radius: 14px;
18
            box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
36642 ranu 19
            overflow: hidden;
20
        }
21
 
22
        .geo-header {
36686 ranu 23
            background: linear-gradient(135deg, #f7931e 0%, #ed1c24 100%);
36642 ranu 24
            color: #fff;
36686 ranu 25
            padding: 22px 24px;
36642 ranu 26
            text-align: center;
27
        }
28
 
36686 ranu 29
        .geo-header .brand-row {
30
            display: flex;
31
            align-items: center;
32
            justify-content: center;
33
            gap: 10px;
34
            margin-bottom: 6px;
35
        }
36
 
37
        .geo-header .brand-mark {
38
            width: 36px;
39
            height: 36px;
40
            background: #fff;
41
            color: #ed1c24;
42
            border-radius: 8px;
43
            display: inline-flex;
44
            align-items: center;
45
            justify-content: center;
46
            font-weight: 800;
47
            font-size: 20px;
48
            letter-spacing: -1px;
49
            box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
50
        }
51
 
36642 ranu 52
        .geo-header h4 {
53
            margin: 0;
36686 ranu 54
            font-weight: 700;
55
            letter-spacing: 0.3px;
36642 ranu 56
        }
57
 
58
        .geo-header p {
59
            margin: 8px 0 0;
36686 ranu 60
            opacity: 0.95;
61
            font-size: 13px;
36642 ranu 62
        }
63
 
64
        .geo-body {
65
            padding: 24px;
66
        }
67
 
68
        .step {
69
            display: none;
70
        }
71
 
72
        .step.active {
73
            display: block;
74
        }
75
 
76
        .step-indicator {
77
            display: flex;
78
            justify-content: center;
79
            gap: 8px;
80
            margin-bottom: 20px;
81
        }
82
 
83
        .step-dot {
84
            width: 10px;
85
            height: 10px;
86
            border-radius: 50%;
87
            background: #ddd;
88
            transition: background 0.3s;
89
        }
90
 
91
        .step-dot.done {
92
            background: #4caf50;
93
        }
94
 
95
        .step-dot.current {
36686 ranu 96
            background: #ed1c24;
36642 ranu 97
        }
98
 
99
        .btn-geo {
36686 ranu 100
            background: linear-gradient(135deg, #f7931e 0%, #ed1c24 100%);
36642 ranu 101
            color: #fff;
102
            border: none;
103
            border-radius: 8px;
104
            padding: 12px 24px;
105
            font-size: 16px;
106
            width: 100%;
107
            cursor: pointer;
36686 ranu 108
            font-weight: 600;
109
            box-shadow: 0 2px 8px rgba(237, 28, 36, 0.25);
36642 ranu 110
        }
111
 
112
        .btn-geo:hover {
36686 ranu 113
            filter: brightness(0.95);
36642 ranu 114
            color: #fff;
115
        }
116
 
117
        .btn-geo:disabled {
118
            background: #b0bec5;
119
            cursor: not-allowed;
120
        }
121
 
122
        .loc-info {
123
            background: #e8f5e9;
124
            border-radius: 8px;
125
            padding: 12px;
126
            margin: 12px 0;
127
            font-size: 13px;
128
            color: #2e7d32;
129
        }
130
 
131
        .loc-info.error {
132
            background: #fce4ec;
133
            color: #c62828;
134
        }
135
 
136
        .upload-area {
137
            border: 2px dashed #b0bec5;
138
            border-radius: 8px;
139
            padding: 24px;
140
            text-align: center;
141
            cursor: pointer;
142
            transition: border-color 0.3s;
143
        }
144
 
145
        .upload-area:hover {
36686 ranu 146
            border-color: #ed1c24;
36642 ranu 147
        }
148
 
149
        .upload-area.uploaded {
150
            border-color: #4caf50;
151
            background: #e8f5e9;
152
        }
153
 
154
        .preview-img {
155
            max-width: 100%;
156
            max-height: 200px;
157
            border-radius: 8px;
158
            margin-top: 12px;
159
        }
160
 
161
        .spinner {
162
            display: inline-block;
163
            width: 20px;
164
            height: 20px;
165
            border: 3px solid #fff;
166
            border-top-color: transparent;
167
            border-radius: 50%;
168
            animation: spin 0.8s linear infinite;
169
        }
170
 
171
        @keyframes spin {
172
            to {
173
                transform: rotate(360deg);
174
            }
175
        }
176
 
177
        .form-control:focus {
36686 ranu 178
            border-color: #ed1c24;
179
            box-shadow: 0 0 0 0.2rem rgba(237, 28, 36, 0.20);
36642 ranu 180
        }
36689 ranu 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
        }
36642 ranu 256
    </style>
257
</head>
258
<body>
259
 
260
<div class="geo-card">
261
    <div class="geo-header">
36686 ranu 262
        <div class="brand-row">
263
            <span class="brand-mark">S</span>
264
            <h4>SmartDukaan</h4>
265
        </div>
36642 ranu 266
        <p>Location Verification</p>
267
    </div>
268
    <div class="geo-body">
269
        <div class="step-indicator">
270
            <div class="step-dot current" id="dot1"></div>
271
            <div class="step-dot" id="dot2"></div>
272
        </div>
273
 
274
        <p class="text-center text-muted mb-3" style="font-size:14px;">
275
            Hello <strong>$!leadName</strong>, please verify your location.
276
        </p>
277
 
278
        <!-- STEP 1: Mobile Verification -->
279
        <div class="step active" id="step1">
280
            <h6 class="mb-3">Step 1: Verify Mobile Number</h6>
281
            <div class="form-group">
282
                <input type="tel" class="form-control" id="mobileNumber" maxlength="10"
283
                       placeholder="Enter your registered mobile number" autocomplete="off">
284
            </div>
285
            <div id="mobileError" class="loc-info error" style="display:none;"></div>
286
            <button class="btn btn-geo" id="btnVerifyMobile">
287
                Verify Mobile
288
            </button>
289
        </div>
290
 
36689 ranu 291
        <!-- STEP 2: Live camera capture (location is grabbed silently when photo is taken) -->
36642 ranu 292
        <div class="step" id="step2">
36689 ranu 293
            <h6 class="mb-3">Step 2: Capture Your Store Photo</h6>
36642 ranu 294
            <p class="text-muted" style="font-size:13px;">
36689 ranu 295
                Stand at your store and tap the red button to take a live photo.
296
                Your location will be recorded automatically.
36642 ranu 297
            </p>
36689 ranu 298
 
299
            <div class="camera-wrap" id="cameraWrap">
300
                <video id="cameraVideo" autoplay playsinline muted></video>
301
                <img id="photoPreview" style="display:none;" alt="Captured photo">
302
                <div class="cam-overlay" id="cameraOverlay">Starting camera...</div>
303
            </div>
304
 
305
            <div class="capture-row">
306
                <button type="button" class="btn-capture" id="btnCapture" disabled>
307
                    <span class="inner"></span>
308
                </button>
309
                <button type="button" class="btn-retake" id="btnRetake" style="display:none;">
310
                    Retake
311
                </button>
312
            </div>
313
 
314
            <div id="captureStatus" class="loc-info" style="display:none;"></div>
315
 
36642 ranu 316
            <input type="hidden" id="latitude" value="">
317
            <input type="hidden" id="longitude" value="">
36689 ranu 318
            <input type="hidden" id="imageDocumentId" value="0">
36642 ranu 319
 
36689 ranu 320
            <button class="btn btn-geo mt-2" id="btnSubmit" disabled>
36642 ranu 321
                Submit Location
322
            </button>
323
        </div>
324
    </div>
325
</div>
326
 
36689 ranu 327
<!-- Hidden canvas used to grab a still frame from the live video stream -->
328
<canvas id="captureCanvas" style="display:none;"></canvas>
329
 
36642 ranu 330
<input type="hidden" id="leadId" value="$leadId">
331
 
332
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
333
<script>
334
    var contextPath = "$rc.contextPath";
335
 
36689 ranu 336
    var mediaStream = null; // live camera stream
337
 
36642 ranu 338
    function showStep(num) {
339
        $('.step').removeClass('active');
340
        $('#step' + num).addClass('active');
36689 ranu 341
        for (var i = 1; i <= 2; i++) {
36642 ranu 342
            var dot = $('#dot' + i);
343
            dot.removeClass('current done');
344
            if (i < num) dot.addClass('done');
345
            else if (i === num) dot.addClass('current');
346
        }
347
    }
348
 
349
    // STEP 1: Verify Mobile
350
    $('#btnVerifyMobile').click(function () {
351
        var mobile = $('#mobileNumber').val().trim();
352
        if (!mobile || mobile.length < 10) {
353
            $('#mobileError').text('Please enter a valid 10-digit mobile number').show();
354
            return;
355
        }
356
        var btn = $(this);
357
        btn.prop('disabled', true).html('<span class="spinner"></span> Verifying...');
358
        $('#mobileError').hide();
359
 
360
        $.ajax({
361
            url: contextPath + '/lead-geo/verify-mobile',
362
            method: 'POST',
363
            data: {leadId: $('#leadId').val(), mobileNumber: mobile},
364
            success: function (resp) {
365
                if (resp.response && resp.response.verified) {
366
                    showStep(2);
36689 ranu 367
                    startCamera();
36642 ranu 368
                } else {
369
                    $('#mobileError').text('Verification failed. Please try again.').show();
370
                }
371
            },
372
            error: function (xhr) {
373
                var msg = 'Verification failed';
374
                try {
375
                    msg = JSON.parse(xhr.responseText).message || msg;
376
                } catch (e) {
377
                }
378
                $('#mobileError').text(msg).show();
379
            },
380
            complete: function () {
381
                btn.prop('disabled', false).text('Verify Mobile');
382
            }
383
        });
384
    });
385
 
36689 ranu 386
    // STEP 2: Live camera — uses getUserMedia so only an in-page live capture
387
    // is possible (no gallery picker, no file input). Geolocation is grabbed
388
    // silently when the user taps the capture button, so no separate step.
389
    function startCamera() {
390
        var overlay = $('#cameraOverlay');
391
        if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
392
            overlay.text('Your browser does not support live camera. Please open this link in Chrome/Safari on your phone.');
36642 ranu 393
            return;
394
        }
36689 ranu 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
    }
36642 ranu 417
 
36689 ranu 418
    function stopCamera() {
419
        if (mediaStream) {
420
            mediaStream.getTracks().forEach(function (t) {
421
                t.stop();
422
            });
423
            mediaStream = null;
424
        }
425
    }
36642 ranu 426
 
36689 ranu 427
    // Capture frame + silently grab geolocation
428
    $('#btnCapture').click(function () {
429
        var btn = $(this);
430
        btn.prop('disabled', true);
431
        $('#captureStatus').removeClass('error').text('Capturing photo and your location...').show();
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
 
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();
461
            });
462
        }, 'image/jpeg', 0.9);
36642 ranu 463
    });
464
 
36689 ranu 465
    $('#btnRetake').click(function () {
466
        $('#photoPreview').hide();
467
        $('#cameraVideo').show();
468
        $(this).hide();
469
        $('#imageDocumentId').val('0');
470
        $('#latitude').val('');
471
        $('#longitude').val('');
472
        $('#btnSubmit').prop('disabled', true);
473
        $('#captureStatus').hide();
474
        if (!mediaStream) startCamera();
475
        else $('#btnCapture').prop('disabled', false);
36642 ranu 476
    });
477
 
36689 ranu 478
    function uploadPhoto(blob) {
479
        var d = $.Deferred();
36642 ranu 480
        var formData = new FormData();
36689 ranu 481
        formData.append('file', blob, 'store-photo.jpg');
36642 ranu 482
        $.ajax({
483
            url: contextPath + '/lead-geo/upload-image',
484
            method: 'POST',
485
            data: formData,
486
            processData: false,
487
            contentType: false,
488
            success: function (resp) {
489
                if (resp.response && resp.response.document_id) {
490
                    $('#imageDocumentId').val(resp.response.document_id);
36689 ranu 491
                    d.resolve();
492
                } else {
493
                    d.reject('Upload failed.');
36642 ranu 494
                }
495
            },
496
            error: function () {
36689 ranu 497
                d.reject('Upload failed. Please try again.');
36642 ranu 498
            }
499
        });
36689 ranu 500
        return d.promise();
501
    }
36642 ranu 502
 
36689 ranu 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
    }
526
 
36642 ranu 527
    // SUBMIT
528
    $('#btnSubmit').click(function () {
529
        var btn = $(this);
530
        btn.prop('disabled', true).html('<span class="spinner"></span> Submitting...');
531
 
532
        $.ajax({
533
            url: contextPath + '/lead-geo/submit',
534
            method: 'POST',
535
            data: {
536
                leadId: $('#leadId').val(),
537
                mobileNumber: $('#mobileNumber').val().trim(),
538
                latitude: $('#latitude').val(),
539
                longitude: $('#longitude').val(),
540
                imageDocumentId: $('#imageDocumentId').val()
541
            },
542
            success: function (resp) {
36686 ranu 543
                // Replace body with thank you (SVG check icon — no emoji to avoid charset issues)
36642 ranu 544
                $('.geo-body').html(
545
                        '<div class="text-center py-4">' +
36686 ranu 546
                        '<svg xmlns="http://www.w3.org/2000/svg" width="72" height="72" viewBox="0 0 24 24"' +
547
                        ' fill="none" stroke="#22c55e" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"' +
548
                        ' style="display:block; margin: 0 auto;">' +
549
                        '<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline>' +
550
                        '</svg>' +
36642 ranu 551
                        '<h5 class="mt-3">Thank You!</h5>' +
552
                        '<p class="text-muted">Your location has been submitted successfully.<br>Our team will review it shortly.</p>' +
553
                        '</div>'
554
                );
555
                $('.step-indicator').remove();
36689 ranu 556
                stopCamera();
36642 ranu 557
            },
558
            error: function () {
559
                btn.prop('disabled', false).text('Submit Location');
560
                alert('Submission failed. Please try again.');
561
            }
562
        });
563
    });
564
 
565
    // Allow Enter key on mobile input
566
    $('#mobileNumber').keypress(function (e) {
567
        if (e.which === 13) $('#btnVerifyMobile').click();
568
    });
569
</script>
570
</body>
571
</html>