upgrade send report
| 
						 | 
				
			
			@ -3,15 +3,20 @@
 | 
			
		|||
namespace Modules\Admin\app\Http\Controllers;
 | 
			
		||||
 | 
			
		||||
use App\Http\Controllers\Controller;
 | 
			
		||||
use App\Mail\WorklogReport;
 | 
			
		||||
use App\Traits\HasFilterRequest;
 | 
			
		||||
use App\Traits\HasOrderByRequest;
 | 
			
		||||
use App\Traits\HasSearchRequest;
 | 
			
		||||
use Illuminate\Http\Request;
 | 
			
		||||
use Illuminate\Http\Response;
 | 
			
		||||
use App\Services\JiraService;
 | 
			
		||||
use Carbon\Carbon;
 | 
			
		||||
use DateTime;
 | 
			
		||||
use DateTimeZone;
 | 
			
		||||
use GuzzleHttp\Promise\Utils;
 | 
			
		||||
use Maatwebsite\Excel\Facades\Excel;
 | 
			
		||||
use Illuminate\Http\JsonResponse;
 | 
			
		||||
use Illuminate\Support\Facades\Mail;
 | 
			
		||||
 | 
			
		||||
class JiraController extends Controller
 | 
			
		||||
{
 | 
			
		||||
| 
						 | 
				
			
			@ -91,7 +96,7 @@ class JiraController extends Controller
 | 
			
		|||
 | 
			
		||||
    public function fetchIssuesByProject(Request $request)
 | 
			
		||||
    {
 | 
			
		||||
        $project = ['key'=>$request->key, 'name'=> $request->name];
 | 
			
		||||
        $project = ['key' => $request->key, 'name' => $request->name];
 | 
			
		||||
        $allIssues = [];
 | 
			
		||||
 | 
			
		||||
        if ($project['key'] !== 'GCT') {
 | 
			
		||||
| 
						 | 
				
			
			@ -102,27 +107,27 @@ class JiraController extends Controller
 | 
			
		|||
            $checked = true;
 | 
			
		||||
 | 
			
		||||
            // while ($checked) {
 | 
			
		||||
                $response = $this->jiraService->getIssues($project['key'], $startAt);
 | 
			
		||||
                $total = $response['total'];
 | 
			
		||||
                $issueLength = count($response['issues']);
 | 
			
		||||
            $response = $this->jiraService->getIssues($project['key'], $startAt);
 | 
			
		||||
            $total = $response['total'];
 | 
			
		||||
            $issueLength = count($response['issues']);
 | 
			
		||||
 | 
			
		||||
                foreach ($response['issues'] as $issue) {
 | 
			
		||||
                    $issues[] = [
 | 
			
		||||
                        'summary' => $issue['fields']['summary'] ?? null,
 | 
			
		||||
                        'desc' => $issue['fields']['description']['content'][0] ?? null,
 | 
			
		||||
                        'assignee' => $issue['fields']['assignee']['displayName'] ?? null,
 | 
			
		||||
                        'status' => $issue['fields']['status']['name'] ?? null,
 | 
			
		||||
                        'worklogs' => json_encode(array_map(function ($log) {
 | 
			
		||||
                            return [
 | 
			
		||||
                                'author' => $log['author']['displayName'] ?? null,
 | 
			
		||||
                                'timeSpent' => $log['timeSpent'] ?? null,
 | 
			
		||||
                                'started' => $log['started'] ?? null,
 | 
			
		||||
                                'updated' => $log['updated'] ?? null,
 | 
			
		||||
                            ];
 | 
			
		||||
                        }, $issue['fields']['worklog']['worklogs'] ?? []), JSON_PRETTY_PRINT),
 | 
			
		||||
                        'originalEstimate' => isset($issue['fields']['timeoriginalestimate']) ? $issue['fields']['timeoriginalestimate'] / 60 / 60 : null,
 | 
			
		||||
                        'timeSpent' => isset($issue['fields']['timespent']) ? $issue['fields']['timespent'] / 60 / 60 : null
 | 
			
		||||
                    ];
 | 
			
		||||
            foreach ($response['issues'] as $issue) {
 | 
			
		||||
                $issues[] = [
 | 
			
		||||
                    'summary' => $issue['fields']['summary'] ?? null,
 | 
			
		||||
                    'desc' => $issue['fields']['description']['content'][0] ?? null,
 | 
			
		||||
                    'assignee' => $issue['fields']['assignee']['displayName'] ?? null,
 | 
			
		||||
                    'status' => $issue['fields']['status']['name'] ?? null,
 | 
			
		||||
                    'worklogs' => json_encode(array_map(function ($log) {
 | 
			
		||||
                        return [
 | 
			
		||||
                            'author' => $log['author']['displayName'] ?? null,
 | 
			
		||||
                            'timeSpent' => $log['timeSpent'] ?? null,
 | 
			
		||||
                            'started' => $log['started'] ?? null,
 | 
			
		||||
                            'updated' => $log['updated'] ?? null,
 | 
			
		||||
                        ];
 | 
			
		||||
                    }, $issue['fields']['worklog']['worklogs'] ?? []), JSON_PRETTY_PRINT),
 | 
			
		||||
                    'originalEstimate' => isset($issue['fields']['timeoriginalestimate']) ? $issue['fields']['timeoriginalestimate'] / 60 / 60 : null,
 | 
			
		||||
                    'timeSpent' => isset($issue['fields']['timespent']) ? $issue['fields']['timespent'] / 60 / 60 : null
 | 
			
		||||
                ];
 | 
			
		||||
                // }
 | 
			
		||||
 | 
			
		||||
                // if (($startAt + $issueLength >= $total && $total !== 0) || $total === 0) {
 | 
			
		||||
| 
						 | 
				
			
			@ -147,6 +152,7 @@ class JiraController extends Controller
 | 
			
		|||
            'status' => false
 | 
			
		||||
        ], 500);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function exportToExcel()
 | 
			
		||||
    {
 | 
			
		||||
        $allIssues = $this->fetchAllIssues()->original;
 | 
			
		||||
| 
						 | 
				
			
			@ -169,4 +175,76 @@ class JiraController extends Controller
 | 
			
		|||
            return response()->json(['error' => $e->getMessage()], 500);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function sendReport()
 | 
			
		||||
    {
 | 
			
		||||
        $now = new DateTime('now', new DateTimeZone('Asia/Bangkok'));
 | 
			
		||||
        $dateFormatted = $now->format('Y-m-d');
 | 
			
		||||
        $workLogs = $this->jiraService->getAllUserWorkLogs('2024-06-14', '2024-06-14');
 | 
			
		||||
        // $tasks = [
 | 
			
		||||
        //     [
 | 
			
		||||
        //         'status' => 'Done',
 | 
			
		||||
        //         'summary' => '[LT-37] TEST TỔNG THỂ & BUILD PRODUCTION GD 1',
 | 
			
		||||
        //         'estimate' => '8H',
 | 
			
		||||
        //         'total_time_spent' => '8.5H',
 | 
			
		||||
        //         'time_spent' => '2H',
 | 
			
		||||
        //         'start_date' => '00:30 2024/05/28',
 | 
			
		||||
        //         'comment' => 'Check on stage'
 | 
			
		||||
        //     ],
 | 
			
		||||
        //     [
 | 
			
		||||
        //         'status' => 'In Progress',
 | 
			
		||||
        //         'summary' => '[IPSPro-826] TEST TASK ERP',
 | 
			
		||||
        //         'estimate' => '0H',
 | 
			
		||||
        //         'total_time_spent' => '13H',
 | 
			
		||||
        //         'time_spent' => '1H',
 | 
			
		||||
        //         'start_date' => '03:30 2024/05/28',
 | 
			
		||||
        //         'comment' => 'check ERP update Incoming & PO add SN'
 | 
			
		||||
        //     ]
 | 
			
		||||
        // ];
 | 
			
		||||
 | 
			
		||||
        $tasksByUser = $this->formatWorkLogsByUser($workLogs);
 | 
			
		||||
        Mail::to('luanlt632000@gmail.com')->send(new WorklogReport($tasksByUser));
 | 
			
		||||
 | 
			
		||||
        // return "Email sent successfully!";
 | 
			
		||||
        return response()->json([
 | 
			
		||||
            'data' => $tasksByUser,
 | 
			
		||||
            'status' => true
 | 
			
		||||
        ], 200);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function formatWorkLogsByUser($workLogs)
 | 
			
		||||
    {
 | 
			
		||||
        // Assuming each workLog has a 'user' field
 | 
			
		||||
        $tasksByUser = [];
 | 
			
		||||
 | 
			
		||||
        foreach ($workLogs as $log) {
 | 
			
		||||
            $user = $log['username'];
 | 
			
		||||
 | 
			
		||||
            if (!isset($tasksByUser[$user])) {
 | 
			
		||||
                $tasksByUser[$user] = [];
 | 
			
		||||
            }
 | 
			
		||||
            foreach ($log['information']['issues'] as $issue) {
 | 
			
		||||
 | 
			
		||||
                // $today = Carbon::today('Asia/Ho_Chi_Minh');
 | 
			
		||||
                $today = Carbon::create('2024','6','14');
 | 
			
		||||
                $filteredWorklogs = [];
 | 
			
		||||
 | 
			
		||||
                foreach ($issue['fields']['worklog']['worklogs'] as $worklog) {
 | 
			
		||||
                    $started = Carbon::parse($worklog['started']);
 | 
			
		||||
                    if ($started->isSameDay($today) && $worklog['updateAuthor']['displayName'] == $user) {
 | 
			
		||||
                        $filteredWorklogs[] = $worklog;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                $tasksByUser[$user][] = [
 | 
			
		||||
                    'status' => $issue['fields']['status']['name'],
 | 
			
		||||
                    'summary' => $issue['fields']['summary'],
 | 
			
		||||
                    'estimate' => $issue['fields']['timeoriginalestimate'],
 | 
			
		||||
                    'time_spent' => $issue['fields']['timespent'],
 | 
			
		||||
                    'worklogs' => $filteredWorklogs,
 | 
			
		||||
                ];
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $tasksByUser;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -97,34 +97,38 @@ Route::middleware('api')
 | 
			
		|||
                'prefix' => 'jira',
 | 
			
		||||
            ], function () {
 | 
			
		||||
                Route::get('/fetch-issues', [JiraController::class, 'fetchAllIssues']);
 | 
			
		||||
                Route::get('/export-issues', [JiraController::class, 'exportToExcel']);
 | 
			
		||||
                Route::get('/all-project', [JiraController::class, 'getAllProject']);
 | 
			
		||||
                Route::get('/all-issue-by-project', [JiraController::class, 'fetchIssuesByProject']);
 | 
			
		||||
                Route::get('/worklogs', [JiraController::class, 'getAllUserWorkLogs'])->middleware('check.permission:admin');
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            Route::group([
 | 
			
		||||
                'prefix' => 'timekeeping',
 | 
			
		||||
            ], function () {
 | 
			
		||||
                Route::get('/', [TimekeepingController::class, 'get'])->middleware('check.permission:admin.hr.staff');
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            Route::group([
 | 
			
		||||
                'prefix' => 'tracking',
 | 
			
		||||
            ], function () {
 | 
			
		||||
                Route::post('/create', [TrackingController::class, 'create'])->middleware('check.permission:admin.hr');
 | 
			
		||||
                });
 | 
			
		||||
                
 | 
			
		||||
                Route::group([
 | 
			
		||||
                    'prefix' => 'timekeeping',
 | 
			
		||||
                    ], function () {
 | 
			
		||||
                        Route::get('/', [TimekeepingController::class, 'get'])->middleware('check.permission:admin.hr.staff');
 | 
			
		||||
                        });
 | 
			
		||||
                        
 | 
			
		||||
                        Route::group([
 | 
			
		||||
                            'prefix' => 'tracking',
 | 
			
		||||
                            ], function () {
 | 
			
		||||
                                Route::post('/create', [TrackingController::class, 'create'])->middleware('check.permission:admin.hr');
 | 
			
		||||
                Route::post('/update', [TrackingController::class, 'update'])->middleware('check.permission:admin.hr');
 | 
			
		||||
                Route::get('/delete', [TrackingController::class, 'delete'])->middleware('check.permission:admin.hr');
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
                });
 | 
			
		||||
                });
 | 
			
		||||
                });
 | 
			
		||||
                
 | 
			
		||||
                Route::group([
 | 
			
		||||
                    'prefix' => 'v1/admin/tracking',
 | 
			
		||||
], function () {
 | 
			
		||||
    Route::get('/', [TrackingController::class, 'get'])->middleware('check.permission:admin.hr.staff');
 | 
			
		||||
    Route::post('/scan-create', [TrackingController::class, 'create']);
 | 
			
		||||
    Route::post('/send-image', [TrackingController::class, 'saveImage']);
 | 
			
		||||
    // Route::get('/clear-cache', [SettingController::class, 'clearCache']);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    Route::group([
 | 
			
		||||
        'prefix' => 'v1/admin/tracking',
 | 
			
		||||
    ], function () {
 | 
			
		||||
        Route::get('/', [TrackingController::class, 'get'])->middleware('check.permission:admin.hr.staff');
 | 
			
		||||
        Route::post('/scan-create', [TrackingController::class, 'create']);
 | 
			
		||||
        Route::post('/send-image', [TrackingController::class, 'saveImage']);
 | 
			
		||||
        // Route::get('/clear-cache', [SettingController::class, 'clearCache']);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
        'prefix' => 'v1/admin/jira',
 | 
			
		||||
        ], function () {
 | 
			
		||||
    Route::get('/send-worklog-report', [JiraController::class, 'sendReport']);
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,26 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Console\Commands;
 | 
			
		||||
 | 
			
		||||
use Illuminate\Console\Command;
 | 
			
		||||
use Illuminate\Support\Facades\Http;
 | 
			
		||||
 | 
			
		||||
class DailyAPICall extends Command
 | 
			
		||||
{
 | 
			
		||||
    protected $signature = 'daily:api-call';
 | 
			
		||||
 | 
			
		||||
    protected $description = 'Call API daily and store results';
 | 
			
		||||
 | 
			
		||||
    public function handle()
 | 
			
		||||
    {
 | 
			
		||||
        // Gọi API
 | 
			
		||||
        $response = Http::get(env('APP_URL').'/api/v1/admin/jira/send-worklog-report');
 | 
			
		||||
 | 
			
		||||
        // Xử lý dữ liệu và lưu vào cơ sở dữ liệu (ví dụ)
 | 
			
		||||
        // $data = $response->json();
 | 
			
		||||
        // Nếu cần lưu dữ liệu vào database, bạn có thể sử dụng Eloquent
 | 
			
		||||
 | 
			
		||||
        // Ghi log hoặc thông báo khi cần thiết
 | 
			
		||||
        $this->info('API called successfully!');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -13,6 +13,9 @@ class Kernel extends ConsoleKernel
 | 
			
		|||
    protected function schedule(Schedule $schedule): void
 | 
			
		||||
    {
 | 
			
		||||
        $schedule->command('clean_logs')->daily();
 | 
			
		||||
         // Chạy command 'daily:api-call' vào mỗi ngày lúc 9h sáng
 | 
			
		||||
        // $schedule->command('daily:api-call')
 | 
			
		||||
        // ->dailyAt('18:00');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,29 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Mail;
 | 
			
		||||
 | 
			
		||||
use Illuminate\Bus\Queueable;
 | 
			
		||||
use Illuminate\Contracts\Queue\ShouldQueue;
 | 
			
		||||
use Illuminate\Mail\Mailable;
 | 
			
		||||
use Illuminate\Mail\Mailables\Content;
 | 
			
		||||
use Illuminate\Mail\Mailables\Envelope;
 | 
			
		||||
use Illuminate\Queue\SerializesModels;
 | 
			
		||||
 | 
			
		||||
class WorklogReport extends Mailable
 | 
			
		||||
{
 | 
			
		||||
    use Queueable, SerializesModels;
 | 
			
		||||
 | 
			
		||||
    public $tasksByUser;
 | 
			
		||||
 | 
			
		||||
    public function __construct($tasksByUser)
 | 
			
		||||
    {
 | 
			
		||||
        $this->tasksByUser = $tasksByUser;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function build()
 | 
			
		||||
    {
 | 
			
		||||
        return $this->markdown('email.worklog')
 | 
			
		||||
                    ->subject('Worklog Report')
 | 
			
		||||
                    ->with('tasksByUser', $this->tasksByUser);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,99 @@
 | 
			
		|||
@component('mail::message')
 | 
			
		||||
<div id="main">
 | 
			
		||||
<div id="list">
 | 
			
		||||
@foreach($tasksByUser as $user => $tasks)
 | 
			
		||||
<a href="#{{str_replace(' ', '', $user)}}">{{ $user }}</a><br>
 | 
			
		||||
@endforeach
 | 
			
		||||
</div>
 | 
			
		||||
<div id="list-user">
 | 
			
		||||
<strong>Worklog Report</strong>
 | 
			
		||||
 | 
			
		||||
@foreach($tasksByUser as $user => $tasks)
 | 
			
		||||
<div class="user" id="{{str_replace(' ', '', $user)}}">
 | 
			
		||||
<strong style="margin-bottom: 15px;">{{ $user }}</strong>
 | 
			
		||||
 | 
			
		||||
@foreach($tasks as $task)
 | 
			
		||||
<div class="task {{ strtolower(str_replace(' ', '', $task['status'])) }}">
 | 
			
		||||
<strong>{{ $task['summary'] }}</strong><br>
 | 
			
		||||
Status: {{ $task['status'] }}<br>
 | 
			
		||||
Estimate: {{ (int)$task['estimate']/60/60 }}h<br>
 | 
			
		||||
@foreach($task['worklogs'] as $worklog)
 | 
			
		||||
<div class="worklog">
 | 
			
		||||
Time spent: {{ $worklog['timeSpent'] }}<br>
 | 
			
		||||
Start date: {{ $worklog['started'] }}<br>
 | 
			
		||||
{{ isset($worklog['comment']) && isset($worklog['comment']['content'][0]['content'][0])&& isset($worklog['comment']['content'][0]['content'][0]['text']) ? 'Comment: ' . $worklog['comment']['content'][0]['content'][0]['text'] : "" }}
 | 
			
		||||
</div>
 | 
			
		||||
<br>
 | 
			
		||||
@endforeach
 | 
			
		||||
</div>
 | 
			
		||||
@endforeach
 | 
			
		||||
</div>
 | 
			
		||||
@endforeach
 | 
			
		||||
</div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
Thanks,<br>
 | 
			
		||||
{{ config('app.name') }}
 | 
			
		||||
@endcomponent
 | 
			
		||||
<style>
 | 
			
		||||
    #main{
 | 
			
		||||
        position: relative;
 | 
			
		||||
        display: flex;
 | 
			
		||||
        width: 1000px;
 | 
			
		||||
    }
 | 
			
		||||
    #list{
 | 
			
		||||
        position: sticky;
 | 
			
		||||
        top: 10px;
 | 
			
		||||
        left: 10px;
 | 
			
		||||
        z-index: 1000;
 | 
			
		||||
        width: 20%;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    #list-user{
 | 
			
		||||
        z-index: 100;
 | 
			
		||||
        width: 80%;
 | 
			
		||||
        max-height: 700px;
 | 
			
		||||
        overflow: auto;
 | 
			
		||||
    }
 | 
			
		||||
    .user{
 | 
			
		||||
        border: solid 2px gray;
 | 
			
		||||
        border-radius: 5px;
 | 
			
		||||
        padding: 10px;
 | 
			
		||||
        margin-bottom: 10px;
 | 
			
		||||
        
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .task{
 | 
			
		||||
        margin-bottom: 5px;
 | 
			
		||||
    }
 | 
			
		||||
    .task.done {
 | 
			
		||||
        background-color: #e0ffe0;
 | 
			
		||||
        padding: 10px;
 | 
			
		||||
        border-radius: 5px;
 | 
			
		||||
    }
 | 
			
		||||
    .task.inprogress {
 | 
			
		||||
        background-color: #fff8e0;
 | 
			
		||||
        padding: 10px;
 | 
			
		||||
        border-radius: 5px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .task.backlog {
 | 
			
		||||
        background-color: #EEEEEE;
 | 
			
		||||
        padding: 10px;
 | 
			
		||||
        border-radius: 5px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .task.todo {
 | 
			
		||||
        background-color: #ADD8E6;
 | 
			
		||||
        padding: 10px;
 | 
			
		||||
        border-radius: 5px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .worklog{
 | 
			
		||||
        border: solid 1px gray;
 | 
			
		||||
        border-radius: 5px;
 | 
			
		||||
        padding: 10px;
 | 
			
		||||
        font-size: 13px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 553 B  | 
| 
		 After Width: | Height: | Size: 567 B  | 
| 
		 After Width: | Height: | Size: 549 B  | 
| 
		 After Width: | Height: | Size: 550 B  | 
| 
		 After Width: | Height: | Size: 589 B  | 
| 
		 After Width: | Height: | Size: 579 B  | 
| 
		 After Width: | Height: | Size: 583 B  | 
| 
		 After Width: | Height: | Size: 556 B  | 
| 
		 After Width: | Height: | Size: 555 B  | 
| 
		 After Width: | Height: | Size: 552 B  | 
| 
		 After Width: | Height: | Size: 536 B  | 
| 
		 After Width: | Height: | Size: 558 B  | 
| 
		 After Width: | Height: | Size: 582 B  | 
| 
						 | 
				
			
			@ -0,0 +1,4 @@
 | 
			
		|||
## Create QR code for users
 | 
			
		||||
1. Add users to the listUser.xlsx file.
 | 
			
		||||
2. Run script file createQR.py
 | 
			
		||||
3. The code will be saved in the QRCode folder
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,45 @@
 | 
			
		|||
import tkinter as tk
 | 
			
		||||
from tkinter import messagebox
 | 
			
		||||
import qrcode
 | 
			
		||||
import openpyxl
 | 
			
		||||
 | 
			
		||||
def create_qr_codes():
 | 
			
		||||
    # Đọc dữ liệu từ tệp Excel
 | 
			
		||||
    wb = openpyxl.load_workbook('listUser.xlsx')
 | 
			
		||||
    sheet = wb.active
 | 
			
		||||
    first_row_skipped = False
 | 
			
		||||
    # Duyệt qua từng dòng trong tệp Excel và tạo mã QR code
 | 
			
		||||
    for row in sheet.iter_rows(values_only=True):
 | 
			
		||||
        name, role = row[0], row[1]
 | 
			
		||||
 | 
			
		||||
        # Bỏ qua dòng đầu tiên
 | 
			
		||||
        if not first_row_skipped:
 | 
			
		||||
            first_row_skipped = True
 | 
			
		||||
            continue
 | 
			
		||||
 | 
			
		||||
        if name != None and role != None:
 | 
			
		||||
            # Tạo nội dung cho mã QR code
 | 
			
		||||
            qr_data = f"{name}\n{role}\n\n"
 | 
			
		||||
 | 
			
		||||
            # Tạo mã QR code
 | 
			
		||||
            qr = qrcode.QRCode(
 | 
			
		||||
                version=1,
 | 
			
		||||
                error_correction=qrcode.constants.ERROR_CORRECT_L,
 | 
			
		||||
                box_size=10,
 | 
			
		||||
                border=4,
 | 
			
		||||
            )
 | 
			
		||||
            qr.add_data(qr_data)
 | 
			
		||||
            qr.make(fit=True)
 | 
			
		||||
 | 
			
		||||
            # Lấy hình ảnh mã QR code
 | 
			
		||||
            img = qr.make_image(fill_color="black", back_color="white")
 | 
			
		||||
 | 
			
		||||
            # Lưu hình ảnh mã QR code vào tệp
 | 
			
		||||
            img_file_name = f"./QRCode/{name}_{role}_qr_code.png"
 | 
			
		||||
            img.save(img_file_name)
 | 
			
		||||
 | 
			
		||||
            print(f"QR code created for {name}, {role}")
 | 
			
		||||
 | 
			
		||||
    messagebox.showinfo("Success", "QR codes created successfully.")
 | 
			
		||||
 | 
			
		||||
create_qr_codes()
 | 
			
		||||
| 
		 After Width: | Height: | Size: 408 KiB  | 
| 
		 After Width: | Height: | Size: 406 KiB  | 
| 
		 After Width: | Height: | Size: 396 KiB  | 
| 
		 After Width: | Height: | Size: 420 KiB  | 
| 
		 After Width: | Height: | Size: 452 KiB  | 
| 
		 After Width: | Height: | Size: 443 KiB  | 
| 
		 After Width: | Height: | Size: 391 KiB  | 
| 
		 After Width: | Height: | Size: 392 KiB  | 
| 
		 After Width: | Height: | Size: 427 KiB  | 
| 
		 After Width: | Height: | Size: 424 KiB  | 
| 
		 After Width: | Height: | Size: 373 KiB  | 
| 
		 After Width: | Height: | Size: 392 KiB  | 
| 
		 After Width: | Height: | Size: 406 KiB  | 
| 
		 After Width: | Height: | Size: 435 KiB  | 
| 
		 After Width: | Height: | Size: 387 KiB  | 
| 
		 After Width: | Height: | Size: 413 KiB  | 
| 
		 After Width: | Height: | Size: 411 KiB  | 
| 
		 After Width: | Height: | Size: 486 KiB  | 
| 
		 After Width: | Height: | Size: 98 KiB  | 
| 
		 After Width: | Height: | Size: 491 KiB  | 
| 
		 After Width: | Height: | Size: 485 KiB  | 
| 
		 After Width: | Height: | Size: 488 KiB  | 
| 
		 After Width: | Height: | Size: 492 KiB  | 
| 
		 After Width: | Height: | Size: 89 KiB  | 
| 
		 After Width: | Height: | Size: 467 KiB  | 
| 
		 After Width: | Height: | Size: 480 KiB  | 
| 
		 After Width: | Height: | Size: 503 KiB  | 
| 
		 After Width: | Height: | Size: 498 KiB  | 
| 
		 After Width: | Height: | Size: 501 KiB  | 
| 
		 After Width: | Height: | Size: 496 KiB  | 
| 
		 After Width: | Height: | Size: 102 KiB  | 
| 
		 After Width: | Height: | Size: 491 KiB  | 
| 
		 After Width: | Height: | Size: 659 KiB  | 
| 
		 After Width: | Height: | Size: 618 KiB  | 
| 
		 After Width: | Height: | Size: 654 KiB  | 
| 
		 After Width: | Height: | Size: 663 KiB  | 
| 
		 After Width: | Height: | Size: 667 KiB  | 
| 
		 After Width: | Height: | Size: 634 KiB  | 
| 
		 After Width: | Height: | Size: 258 KiB  | 
| 
		 After Width: | Height: | Size: 690 KiB  | 
| 
		 After Width: | Height: | Size: 681 KiB  | 
| 
		 After Width: | Height: | Size: 670 KiB  | 
| 
		 After Width: | Height: | Size: 648 KiB  | 
| 
		 After Width: | Height: | Size: 618 KiB  | 
| 
		 After Width: | Height: | Size: 621 KiB  | 
| 
		 After Width: | Height: | Size: 644 KiB  | 
| 
		 After Width: | Height: | Size: 663 KiB  | 
| 
		 After Width: | Height: | Size: 648 KiB  | 
| 
		 After Width: | Height: | Size: 614 KiB  | 
| 
		 After Width: | Height: | Size: 612 KiB  | 
| 
		 After Width: | Height: | Size: 621 KiB  | 
| 
		 After Width: | Height: | Size: 633 KiB  | 
| 
		 After Width: | Height: | Size: 642 KiB  | 
| 
		 After Width: | Height: | Size: 659 KiB  | 
| 
		 After Width: | Height: | Size: 662 KiB  | 
| 
		 After Width: | Height: | Size: 692 KiB  | 
| 
		 After Width: | Height: | Size: 674 KiB  | 
| 
		 After Width: | Height: | Size: 671 KiB  | 
| 
		 After Width: | Height: | Size: 629 KiB  | 
| 
		 After Width: | Height: | Size: 578 KiB  | 
| 
		 After Width: | Height: | Size: 619 KiB  | 
| 
		 After Width: | Height: | Size: 568 KiB  | 
| 
		 After Width: | Height: | Size: 240 KiB  | 
| 
		 After Width: | Height: | Size: 600 KiB  | 
| 
		 After Width: | Height: | Size: 603 KiB  | 
| 
		 After Width: | Height: | Size: 590 KiB  | 
| 
		 After Width: | Height: | Size: 596 KiB  | 
| 
		 After Width: | Height: | Size: 611 KiB  | 
| 
		 After Width: | Height: | Size: 603 KiB  | 
| 
		 After Width: | Height: | Size: 609 KiB  | 
| 
		 After Width: | Height: | Size: 592 KiB  | 
| 
						 | 
				
			
			@ -0,0 +1,6 @@
 | 
			
		|||
pyttsx3
 | 
			
		||||
openpyxl
 | 
			
		||||
pyzbar
 | 
			
		||||
opencv-python
 | 
			
		||||
qrcode
 | 
			
		||||
pyautogui
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,228 @@
 | 
			
		|||
import os
 | 
			
		||||
import cv2 as cv
 | 
			
		||||
import openpyxl
 | 
			
		||||
import pyttsx3
 | 
			
		||||
import pyautogui
 | 
			
		||||
from pyzbar import pyzbar
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
import requests
 | 
			
		||||
# Khởi tạo danh sách rỗng để lưu trữ thông tin người dùng
 | 
			
		||||
user_data = []
 | 
			
		||||
history = []
 | 
			
		||||
screen_width = 1250 
 | 
			
		||||
screen_height = 1100
 | 
			
		||||
WINDOW_QR_CODE = "QR Code"
 | 
			
		||||
WINDOW_TRACKING = "Tracking"
 | 
			
		||||
WINDOW_HISTORY = "History"
 | 
			
		||||
URL_API = "http://localhost:8000/api/v1"
 | 
			
		||||
data = [0]
 | 
			
		||||
# Hàm thông báo bằng giọng nói
 | 
			
		||||
def speak(text):
 | 
			
		||||
    engine = pyttsx3.init()
 | 
			
		||||
    engine.say(text)
 | 
			
		||||
    engine.runAndWait()
 | 
			
		||||
 | 
			
		||||
def send_image(id, file_name):
 | 
			
		||||
    # Ensure id is a string if it's an integer
 | 
			
		||||
    id = str(id)
 | 
			
		||||
    
 | 
			
		||||
    today_date = datetime.now().strftime("%Y_%m_%d")
 | 
			
		||||
    folder_path = f"./images/{today_date}"
 | 
			
		||||
    
 | 
			
		||||
    # Ensure the directory exists
 | 
			
		||||
    if not os.path.exists(folder_path):
 | 
			
		||||
        os.makedirs(folder_path)
 | 
			
		||||
    
 | 
			
		||||
    # Ensure the full file path is correct
 | 
			
		||||
    file_path = os.path.join(folder_path, file_name)
 | 
			
		||||
    
 | 
			
		||||
    # Check if the file exists
 | 
			
		||||
    if not os.path.isfile(file_path):
 | 
			
		||||
        print(f"Error: The file {file_path} does not exist.")
 | 
			
		||||
        return {"status": False, "message": f"The file {file_path} does not exist."}
 | 
			
		||||
    
 | 
			
		||||
    with open(file_path, 'rb') as image_file:
 | 
			
		||||
        files = {'image': image_file}
 | 
			
		||||
        # Correct payload for the data parameter
 | 
			
		||||
        data = {'id': id, 'file_name': file_name}
 | 
			
		||||
        print(files)
 | 
			
		||||
        try:
 | 
			
		||||
            response = requests.post(URL_API + "/admin/tracking/send-image", data=data, files=files)
 | 
			
		||||
            response.raise_for_status()
 | 
			
		||||
            res = response.json()
 | 
			
		||||
        except requests.exceptions.RequestException as e:
 | 
			
		||||
            print(f"HTTP Request failed: {e}")
 | 
			
		||||
            return {"status": False, "message": str(e)}
 | 
			
		||||
    
 | 
			
		||||
    # Check if the request was successful
 | 
			
		||||
    if res.get('status') == True:
 | 
			
		||||
        # Process the returned data
 | 
			
		||||
        print(res)
 | 
			
		||||
        return res
 | 
			
		||||
    else:
 | 
			
		||||
        return res
 | 
			
		||||
def create_history(frame, data):
 | 
			
		||||
    # Gửi yêu cầu POST với dữ liệu đã chỉ định
 | 
			
		||||
    response = requests.post(URL_API+"/admin/tracking/scan-create", data=data)
 | 
			
		||||
    res = response.json()
 | 
			
		||||
    # Kiểm tra xem gửi yêu cầu có thành công hay không
 | 
			
		||||
    if res.get('status') == True:
 | 
			
		||||
        # Xử lý dữ liệu trả về
 | 
			
		||||
        print(res)
 | 
			
		||||
        return res
 | 
			
		||||
    else:
 | 
			
		||||
        display_text(frame, res.get('data'), (25, 25), 0.7, (6, 6, 255), 2)
 | 
			
		||||
        speak(res.get('data'))
 | 
			
		||||
        return res
 | 
			
		||||
# Hàm để ghi thông tin vào tệp Excel
 | 
			
		||||
def write_to_excel(name, time, check_type):
 | 
			
		||||
    try:
 | 
			
		||||
        # Mở workbook hiện có
 | 
			
		||||
        workbook = openpyxl.load_workbook("./data/"+time.strftime("%Y_%m")+"_check_in_out.xlsx")
 | 
			
		||||
        sheet = workbook.active
 | 
			
		||||
    except FileNotFoundError:
 | 
			
		||||
        # Tạo workbook mới nếu tệp không tồn tại
 | 
			
		||||
        workbook = openpyxl.Workbook()
 | 
			
		||||
        sheet = workbook.active
 | 
			
		||||
        sheet.append(["Name", "Role", "Time", "Check Type"])
 | 
			
		||||
 | 
			
		||||
    # Ghi thông tin vào các ô trong tệp Excel
 | 
			
		||||
    sheet.append([name.split('\n')[0].strip(), name.split('\n')[1].strip(), time, check_type])
 | 
			
		||||
 | 
			
		||||
    # Lưu tệp Excel
 | 
			
		||||
    workbook.save("./data/"+time.strftime("%Y_%m")+"_check_in_out.xlsx")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def check_response(res, frame, name, timestamp, text):
 | 
			
		||||
    if res.get('status'):
 | 
			
		||||
        display_text(frame, f"{text.split('\n')[0].strip()} {res.get('check_status')}", (25, 25), 0.7, (0, 255, 10), 2)
 | 
			
		||||
        display_text(frame, f"{text.split('\n')[1]}", (25, 50), 0.7, (0, 255, 10), 2)
 | 
			
		||||
        display_text(frame, f"{datetime.now()}", (25, 75), 0.7, (0, 255, 10), 2)
 | 
			
		||||
        display_text(frame, f"Sucessful", (25, 100), 0.7, (0, 255, 10), 2)
 | 
			
		||||
        write_to_excel(name, timestamp, res.get('check_status'))
 | 
			
		||||
        speak(res.get('check_status') + " success")
 | 
			
		||||
        return True
 | 
			
		||||
    else:
 | 
			
		||||
        display_text(frame, f"Call API fail", (25, 50), 0.7, (6, 6, 255), 2)
 | 
			
		||||
        speak("Call API fail")
 | 
			
		||||
        cv.waitKey(2000)
 | 
			
		||||
        return False
 | 
			
		||||
    
 | 
			
		||||
# Hàm để thêm thông tin mới vào danh sách
 | 
			
		||||
def check_in(name, frame, text):
 | 
			
		||||
    timestamp = datetime.now()
 | 
			
		||||
    user_data.append({"name": name, "check_in_time": timestamp})
 | 
			
		||||
    res = create_history(frame, {"name": name.split('\n')[0], "time_string": f"{datetime.now()}", "status": "check in"})
 | 
			
		||||
    result = check_response(res, frame, name, timestamp, text)
 | 
			
		||||
    return res
 | 
			
		||||
    # cv.waitKey(5000)
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
# Hàm để xóa thông tin khi check out
 | 
			
		||||
def check_out(name, frame, text):
 | 
			
		||||
    for user in user_data:
 | 
			
		||||
        if user["name"] == name:
 | 
			
		||||
            timestamp = datetime.now()
 | 
			
		||||
            user["check_out_time"] = timestamp
 | 
			
		||||
            print(f"{name} đã check out lúc {timestamp}")
 | 
			
		||||
            user_data.remove(user)
 | 
			
		||||
            res = create_history(frame, {"name": name.split('\n')[0], "time_string": f"{datetime.now()}", "status": "check out"})
 | 
			
		||||
            result = check_response(res, frame, name, timestamp, text)
 | 
			
		||||
            if result:
 | 
			
		||||
                return res
 | 
			
		||||
# Hàm để hiển thị văn bản lên hình ảnh
 | 
			
		||||
def display_text(frame, text, position, font_scale, color, thickness):
 | 
			
		||||
    cv.putText(frame, text, position, cv.FONT_HERSHEY_COMPLEX, font_scale, color, thickness)
 | 
			
		||||
 | 
			
		||||
def screenshot_window(save_path):
 | 
			
		||||
        today_date = datetime.now().strftime("%Y_%m_%d")
 | 
			
		||||
        folder_path = f"./images/{today_date}"
 | 
			
		||||
        # Kiểm tra xem thư mục đã tồn tại chưa
 | 
			
		||||
        if not os.path.exists(folder_path):
 | 
			
		||||
            # Nếu thư mục chưa tồn tại, tạo mới
 | 
			
		||||
            os.makedirs(folder_path)
 | 
			
		||||
            print(f"Folder '{today_date}' created successfully!")
 | 
			
		||||
 | 
			
		||||
        screenshot = pyautogui.screenshot(region=(10, 10, screen_width, screen_height))
 | 
			
		||||
        screenshot.save(folder_path+"/"+save_path)
 | 
			
		||||
        print("Screenshot saved successfully!")
 | 
			
		||||
 | 
			
		||||
# Hàm để xử lý quá trình quét mã QR code
 | 
			
		||||
def process_qr_code(frame):
 | 
			
		||||
    decoded_objects = pyzbar.decode(frame)
 | 
			
		||||
    for obj in decoded_objects:
 | 
			
		||||
        (x, y, w, h) = obj.rect
 | 
			
		||||
        cv.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 255), 2)
 | 
			
		||||
        text = obj.data.decode('utf-8')
 | 
			
		||||
        file_name = ""
 | 
			
		||||
        status = ""
 | 
			
		||||
        id_log = 0
 | 
			
		||||
 | 
			
		||||
        if text.endswith("\n\n"):
 | 
			
		||||
            if text not in [user["name"] for user in user_data]:
 | 
			
		||||
                print(f"{text} đã check in lúc {datetime.now()}")
 | 
			
		||||
                status += "check in"
 | 
			
		||||
                file_name+=text.split('\n')[0]+"_"+f"{status}_at_{datetime.now().strftime("%Y_%m_%d_%H_%M_%S")}.png"
 | 
			
		||||
                # screenshot_window(file_name)
 | 
			
		||||
                res = check_in(text, frame, text)
 | 
			
		||||
                id_log = res.get('data').get('id')
 | 
			
		||||
            else:
 | 
			
		||||
                print(f"{text} đã check out lúc {datetime.now()}")
 | 
			
		||||
                status += "check out"
 | 
			
		||||
                file_name+=text.split('\n')[0]+"_"+f"{status}_at_{datetime.now().strftime("%Y_%m_%d_%H_%M_%S")}.png"
 | 
			
		||||
                # screenshot_window(file_name)
 | 
			
		||||
                res = check_out(text, frame, text)
 | 
			
		||||
                id_log = res.get('data').get('id')
 | 
			
		||||
            cv.namedWindow(WINDOW_QR_CODE, cv.WINDOW_NORMAL)
 | 
			
		||||
            cv.resizeWindow(WINDOW_QR_CODE, screen_width, screen_height)
 | 
			
		||||
            cv.imshow(WINDOW_QR_CODE, frame)
 | 
			
		||||
            cv.moveWindow(WINDOW_QR_CODE, 10, 10)
 | 
			
		||||
            cv.waitKey(5000)  # Chờ 5 giây
 | 
			
		||||
            screenshot_window(file_name)
 | 
			
		||||
            send_image(id_log, file_name)
 | 
			
		||||
            cv.destroyWindow(WINDOW_QR_CODE)
 | 
			
		||||
        else:
 | 
			
		||||
            display_text(frame, f"QR invalid", (25, 25), 0.7, (6, 6, 255), 2)
 | 
			
		||||
            display_text(frame, f"Failed", (25, 50), 0.7, (6, 6, 255), 2)
 | 
			
		||||
            speak("Failed")   
 | 
			
		||||
            cv.namedWindow(WINDOW_QR_CODE, cv.WINDOW_NORMAL)
 | 
			
		||||
            cv.resizeWindow(WINDOW_QR_CODE, screen_width, screen_height)
 | 
			
		||||
            cv.imshow(WINDOW_QR_CODE, frame)
 | 
			
		||||
            cv.moveWindow(WINDOW_QR_CODE, 10, 10)
 | 
			
		||||
            cv.waitKey(2000)
 | 
			
		||||
            cv.destroyWindow(WINDOW_QR_CODE)
 | 
			
		||||
    return frame
 | 
			
		||||
 | 
			
		||||
# Khởi tạo camera
 | 
			
		||||
def main():
 | 
			
		||||
    cap = cv.VideoCapture(0)
 | 
			
		||||
    face_cascade = cv.CascadeClassifier(cv.data.haarcascades + 'haarcascade_frontalface_default.xml')
 | 
			
		||||
    cv.namedWindow(WINDOW_TRACKING, cv.WINDOW_NORMAL)
 | 
			
		||||
    cv.resizeWindow(WINDOW_TRACKING, screen_width, screen_height)
 | 
			
		||||
    while True:
 | 
			
		||||
        ret, frame = cap.read()
 | 
			
		||||
        if not ret:
 | 
			
		||||
            break
 | 
			
		||||
        # Convert the frame to grayscale
 | 
			
		||||
        gray_frame = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)
 | 
			
		||||
 | 
			
		||||
        # Detect faces in the frame
 | 
			
		||||
        faces = face_cascade.detectMultiScale(gray_frame, scaleFactor=1.1, minNeighbors=25, minSize=(30, 30))
 | 
			
		||||
 | 
			
		||||
        # Draw rectangles around the faces
 | 
			
		||||
        if len(faces) == 1:
 | 
			
		||||
            for (x, y, w, h) in faces:
 | 
			
		||||
                cv.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2)
 | 
			
		||||
                display_text(frame, f"Face detected", (430, 25), 0.7, (0, 255, 0), 2)
 | 
			
		||||
                frame = process_qr_code(frame)
 | 
			
		||||
        else:
 | 
			
		||||
            display_text(frame, f"Face not found", (430, 25), 0.7, (6, 6, 255), 2)
 | 
			
		||||
        cv.imshow(WINDOW_TRACKING, frame)
 | 
			
		||||
        cv.moveWindow(WINDOW_TRACKING, 10, 10)
 | 
			
		||||
        if cv.waitKey(1) == ord('q'):
 | 
			
		||||
            break
 | 
			
		||||
 | 
			
		||||
    cap.release()
 | 
			
		||||
    cv.destroyAllWindows()
 | 
			
		||||
 | 
			
		||||
main()
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,21 @@
 | 
			
		|||
const express = require('express');
 | 
			
		||||
const app = express();
 | 
			
		||||
const port = 3000;
 | 
			
		||||
 | 
			
		||||
// Middleware to parse JSON bodies
 | 
			
		||||
app.use(express.json());
 | 
			
		||||
 | 
			
		||||
// Define a simple route
 | 
			
		||||
app.get('/', (req, res) => {
 | 
			
		||||
  res.send('Hello, World!');
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Define another route
 | 
			
		||||
app.get('/webhooks/webhook1', (req, res) => {
 | 
			
		||||
  console.log(req.body)
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Start the server
 | 
			
		||||
app.listen(port, () => {
 | 
			
		||||
  console.log(`Server is running on http://localhost:${port}`);
 | 
			
		||||
});
 | 
			
		||||