tien_nemo

Merge branch '2025/ntctien/14567_edit_template' into 'dev'

2025/ntctien/14567 edit template

See merge request !4
1 -<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400"></a></p> 1 +- **run project**
2 - 2 + php artisan serve
3 -<p align="center"> 3 + http://127.0.0.1:8000/ocr
4 -<a href="https://travis-ci.org/laravel/framework"><img src="https://travis-ci.org/laravel/framework.svg" alt="Build Status"></a> 4 +
5 -<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a> 5 +- **create database**
6 -<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a> 6 + CREATE TABLE `mst_template` (
7 -<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a> 7 + `id` INT(11) NOT NULL AUTO_INCREMENT,
8 -</p> 8 + `tpl_name` VARCHAR(50) NOT NULL COLLATE 'utf8mb4_general_ci',
9 - 9 + `tpl_text` VARCHAR(50) NOT NULL COLLATE 'utf8mb4_general_ci',
10 -## About Laravel 10 + `tpl_xy` VARCHAR(50) NOT NULL COLLATE 'utf8mb4_general_ci',
11 - 11 + `in_date` DATETIME NOT NULL DEFAULT current_timestamp(),
12 -Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as: 12 + `up_date` DATETIME NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
13 - 13 + PRIMARY KEY (`id`) USING BTREE,
14 -- [Simple, fast routing engine](https://laravel.com/docs/routing). 14 + UNIQUE INDEX `tpl_name` (`tpl_name`) USING BTREE
15 -- [Powerful dependency injection container](https://laravel.com/docs/container). 15 + )
16 -- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage. 16 + COLLATE='utf8mb4_general_ci'
17 -- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent). 17 + ENGINE=MyISAM
18 -- Database agnostic [schema migrations](https://laravel.com/docs/migrations). 18 + AUTO_INCREMENT=11
19 -- [Robust background job processing](https://laravel.com/docs/queues). 19 + ;
20 -- [Real-time event broadcasting](https://laravel.com/docs/broadcasting). 20 +
21 - 21 + CREATE TABLE `dt_template` (
22 -Laravel is accessible, powerful, and provides tools required for large, robust applications. 22 + `tpl_detail_id` INT(11) NOT NULL AUTO_INCREMENT,
23 - 23 + `tpl_id` INT(11) NOT NULL,
24 -## Learning Laravel 24 + `field_name` VARCHAR(50) NULL DEFAULT NULL COLLATE 'utf8mb4_general_ci',
25 - 25 + `field_xy` VARCHAR(50) NULL DEFAULT NULL COLLATE 'utf8mb4_general_ci',
26 -Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. 26 + `in_date` DATETIME NOT NULL DEFAULT current_timestamp(),
27 - 27 + `up_date` DATETIME NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
28 -If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains over 1500 video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library. 28 + PRIMARY KEY (`tpl_detail_id`) USING BTREE,
29 - 29 + INDEX `tpl_id` (`tpl_id`) USING BTREE
30 -## Laravel Sponsors 30 + )
31 - 31 + COLLATE='utf8mb4_general_ci'
32 -We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the Laravel [Patreon page](https://patreon.com/taylorotwell). 32 + ENGINE=MyISAM
33 - 33 + AUTO_INCREMENT=7
34 -### Premium Partners 34 + ;
35 - 35 +
36 -- **[Vehikl](https://vehikl.com/)** 36 +
37 -- **[Tighten Co.](https://tighten.co)** 37 +
38 -- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)** 38 +- /ocrpdf/app/Services/OCR/read_pdf.py : read pdf
39 -- **[64 Robots](https://64robots.com)**
40 -- **[Cubet Techno Labs](https://cubettech.com)**
41 -- **[Cyber-Duck](https://cyber-duck.co.uk)**
42 -- **[Many](https://www.many.co.uk)**
43 -- **[Webdock, Fast VPS Hosting](https://www.webdock.io/en)**
44 -- **[DevSquad](https://devsquad.com)**
45 -- **[Curotec](https://www.curotec.com/services/technologies/laravel/)**
46 -- **[OP.GG](https://op.gg)**
47 -- **[WebReinvent](https://webreinvent.com/?utm_source=laravel&utm_medium=github&utm_campaign=patreon-sponsors)**
48 -- **[Lendio](https://lendio.com)**
49 -
50 -## Contributing
51 -
52 -Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
53 -
54 -## Code of Conduct
55 -
56 -In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
57 -
58 -## Security Vulnerabilities
59 -
60 -If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
61 -
62 -## License
63 -
64 -The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
......
1 +from paddleocr import PaddleOCR
2 +from pdf2image import convert_from_path
3 +import os
4 +import time
5 +import numpy as np
6 +import json
7 +from pathlib import Path
8 +import cv2
9 +from table_detector import detect_tables
10 +
11 +# ==== Config ====
12 +BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
13 +PDF_NAME = 'aaaa'
14 +
15 +# PDF path
16 +pdf_path = Path(BASE_DIR) / "storage" / "pdf" / "fax.pdf"
17 +# Output folder
18 +output_folder = Path(BASE_DIR) / "public" / "image"
19 +
20 +#PDF_NAME = pdf_path.stem # Get the stem of the PDF file
21 +#print(PDF_NAME)
22 +
23 +os.makedirs(output_folder, exist_ok=True)
24 +
25 +timestamp = int(time.time())
26 +img_base_name = f"{PDF_NAME}_{timestamp}"
27 +
28 +# ==== OCR Init ====
29 +ocr = PaddleOCR(
30 + use_doc_orientation_classify=False,
31 + use_doc_unwarping=False,
32 + use_textline_orientation=False
33 +)
34 +
35 +# ==== PDF to Image ====
36 +pages = convert_from_path(pdf_path, first_page=1, last_page=1)
37 +image_path = os.path.join(output_folder, f"{img_base_name}.jpg")
38 +pages[0].save(image_path, "JPEG")
39 +
40 +# ==== Run OCR ====
41 +image_np = np.array(pages[0])
42 +results = ocr.predict(image_np)
43 +
44 +# ==== Convert polygon to bbox ====
45 +def poly_to_bbox(poly):
46 + xs = [p[0] for p in poly]
47 + ys = [p[1] for p in poly]
48 + return [int(min(xs)), int(min(ys)), int(max(xs)), int(max(ys))]
49 +
50 +# ==== Build ocrData ====
51 +ocr_data_list = []
52 +for res in results:
53 + for text, poly in zip(res['rec_texts'], res['rec_polys']):
54 + bbox = poly_to_bbox(poly)
55 + ocr_data_list.append({
56 + "text": text,
57 + "bbox": bbox,
58 + "field": "",
59 + "hideBorder": False
60 + })
61 +
62 +# ==== Detect table ====
63 +table_info = detect_tables(image_path)
64 +
65 +# ==== Build JSON ====
66 +final_json = {
67 + "ocr_data": ocr_data_list,
68 + "tables": table_info
69 +}
70 +
71 +
72 +# ==== Save JSON ====
73 +json_path = os.path.join(output_folder, f"{PDF_NAME}_{timestamp}_with_table.json")
74 +with open(json_path, "w", encoding="utf-8") as f:
75 + json.dump(final_json, f, ensure_ascii=False, indent=2)
76 +
77 +print(f"Saved OCR + Table JSON to: {json_path}")
1 +import cv2
2 +import numpy as np
3 +import os
4 +
5 +def detect_tables(image_path):
6 + img = cv2.imread(image_path)
7 + if img is None:
8 + raise FileNotFoundError(f"Không đọc được ảnh: {image_path}")
9 +
10 + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
11 + blur = cv2.GaussianBlur(gray, (3, 3), 0)
12 +
13 + # Edge detection
14 + edges = cv2.Canny(blur, 50, 150, apertureSize=3)
15 +
16 + # --- Horizontal lines ---
17 + lines_h = cv2.HoughLinesP(edges, 1, np.pi/180, threshold=120,
18 + minLineLength=int(img.shape[1] * 0.6), maxLineGap=20)
19 + ys_candidates, line_segments = [], []
20 + if lines_h is not None:
21 + for l in lines_h:
22 + x1, y1, x2, y2 = l[0]
23 + if abs(y1 - y2) <= 3: # ngang
24 + y_mid = int(round((y1 + y2) / 2))
25 + ys_candidates.append(y_mid)
26 + line_segments.append((x1, x2, y_mid))
27 +
28 + # gom nhóm các y
29 + ys, tol_y = [], 10
30 + for y in sorted(ys_candidates):
31 + if not ys or abs(y - ys[-1]) > tol_y:
32 + ys.append(y)
33 +
34 + total_rows = max(0, len(ys) - 1)
35 +
36 + # --- Vertical lines ---
37 + lines_v = cv2.HoughLinesP(edges, 1, np.pi/180, threshold=100,
38 + minLineLength=int(img.shape[0] * 0.5), maxLineGap=20)
39 + xs = []
40 + if lines_v is not None:
41 + for l in lines_v:
42 + x1, y1, x2, y2 = l[0]
43 + if abs(x1 - x2) <= 3:
44 + xs.append(int(round((x1 + x2) / 2)))
45 +
46 + # gom nhóm cột
47 + x_pos, tol_v = [], 10
48 + for v in sorted(xs):
49 + if not x_pos or v - x_pos[-1] > tol_v:
50 + x_pos.append(v)
51 +
52 + total_cols = max(0, len(x_pos) - 1)
53 +
54 + tables = []
55 + if len(ys) >= 3 and line_segments:
56 + y_min, y_max = ys[0], ys[-1]
57 + min_x = min(seg[0] for seg in line_segments)
58 + max_x = max(seg[1] for seg in line_segments)
59 + table_box = (min_x, y_min, max_x, y_max)
60 +
61 + rows = []
62 + for i in range(len(ys) - 1):
63 + row_box = (min_x, ys[i], max_x, ys[i+1])
64 + rows.append({"row": tuple(int(v) for v in row_box)})
65 + cv2.rectangle(img, (row_box[0], row_box[1]), (row_box[2], row_box[3]), (0, 255, 255), 2)
66 +
67 + tables.append({
68 + "total_rows": int(total_rows),
69 + "total_cols": int(total_cols),
70 + "table_box": tuple(int(v) for v in table_box),
71 + "rows_box": rows
72 + })
73 +
74 + cv2.rectangle(img, (min_x, y_min), (max_x, y_max), (255, 0, 0), 3)
75 +
76 + debug_path = os.path.splitext(image_path)[0] + "_debug.jpg"
77 + cv2.imwrite(debug_path, img)
78 +
79 + return tables
1 +
2 +body {
3 + font-family: sans-serif; background: #f5f5f5;
4 +}
5 +
6 +#app {
7 + display: flex; gap: 20px; padding: 20px;
8 +}
9 +
10 +.right-panel {
11 + width: 500px; background: #fff; padding: 15px;
12 + border-radius: 8px; box-shadow: 0 0 5px rgba(0,0,0,0.1);
13 +}
14 +
15 +.form-group {
16 + margin-bottom: 15px;
17 +}
18 +
19 +.form-group label {
20 + font-weight: bold; display: block; margin-bottom: 5px;
21 +}
22 +
23 +.form-group input {
24 + width: 100%;
25 + padding: 6px;
26 + border: 1px solid #ccc;
27 + border-radius: 4px;
28 +}
29 +
30 +.left-panel {
31 + flex: 1;
32 + position: relative;
33 + background: #eee;
34 + border-radius: 8px;
35 + overflow: hidden;
36 + user-select: none;
37 +}
38 +
39 +.pdf-container {
40 + position: relative; display: inline-block;
41 +}
42 +
43 +.bbox {
44 + position: absolute;
45 + border: 2px solid #ff5252;
46 + /*background-color: rgba(255, 82, 82, 0.2);*/
47 + cursor: pointer;
48 +}
49 +
50 +.bbox.active {
51 + border-color: #199601 !important;
52 + background-color: rgba(25, 150, 1, 0.4) !important;
53 +}
54 +
55 +@keyframes focusPulse {
56 + 0% { transform: scale(1); }
57 + 50% { transform: scale(1.05); }
58 + 100% { transform: scale(1); }
59 +}
60 +
61 +select {
62 + position: absolute;
63 + z-index: 10;
64 + background: #fff;
65 + border: 1px solid #ccc;
66 +}
67 +
68 +.select-box {
69 + position: absolute;
70 + /*border: 2px dashed #2196F3;*/
71 + background-color: rgba(33, 150, 243, 0.2);
72 + pointer-events: none;
73 + z-index: 5;
74 +}
75 +
76 +.delete-btn {
77 + position: absolute;
78 + top: 50%;
79 + right: -35px;
80 + transform: translateY(-50%);
81 + cursor: pointer;
82 + padding: 3px 6px;
83 + z-index: 20;
84 +}
85 +
86 +
87 +.edge {
88 + position: absolute;
89 + z-index: 25;
90 +
91 +}
92 +
93 +.edge.top, .edge.bottom {
94 + height: 8px;
95 + cursor: ns-resize;;
96 +}
97 +.edge.left, .edge.right {
98 + width: 8px;
99 + cursor: ew-resize;
100 +}
101 +
102 +.edge.top {
103 + top: -4px;
104 + left: 0;
105 + right: 0;
106 +}
107 +
108 +.edge.bottom {
109 + bottom: -4px;
110 + left: 0;
111 + right: 0;
112 +}
113 +
114 +.edge.left {
115 + top: 0;
116 + bottom: 0;
117 + left: -4px;
118 +}
119 +
120 +.edge.right {
121 + top: 0;
122 + bottom: 0;
123 + right: -4px;
124 +}
125 +
126 +.corner {
127 + position: absolute;
128 + width: 14px;
129 + height: 14px;
130 + background: transparent;
131 + border: 2px solid transparent; /* mặc định trong suốt, chỉ tô 2 cạnh */
132 + z-index: 30;
133 + opacity: .95;
134 + transition: border-width .08s ease, transform .08s ease, opacity .08s ease;
135 + pointer-events: auto; /* bắt sự kiện kéo resize */
136 +}
137 +
138 +
139 +/* Mỗi góc hiện 2 cạnh + bo tròn đúng góc */
140 +.corner.top-left {
141 + top: -8px; left: -8px;
142 + /*border-left-color: var(--corner-color);*/
143 + /*border-top-color: var(--corner-color);*/
144 + border-top-left-radius: 6px;
145 + cursor: nwse-resize;
146 +}
147 +
148 +.corner.top-right {
149 + top: -8px; right: -8px;
150 + /*border-right-color: var(--corner-color);*/
151 + /*border-top-color: var(--corner-color);*/
152 + border-top-right-radius: 6px;
153 + cursor: nesw-resize;
154 +}
155 +
156 +.corner.bottom-left {
157 + bottom: -8px; left: -8px;
158 + /*border-left-color: var(--corner-color);*/
159 + /*border-bottom-color: var(--corner-color);*/
160 + border-bottom-left-radius: 6px;
161 + cursor: nesw-resize;
162 +}
163 +
164 +.corner.bottom-right {
165 + bottom: -8px; right: -8px;
166 + /*border-right-color: var(--corner-color);*/
167 + /*border-bottom-color: var(--corner-color);*/
168 + border-bottom-right-radius: 6px;
169 + cursor: nwse-resize;
170 +}
171 +
172 +/* Hiệu ứng khi hover – dày hơn, rõ hơn */
173 +.corner:hover {
174 + border-width: 3px;
175 + opacity: 1;
176 + transform: scale(1.02);
177 +}
1 -<html lang="en"><head> 1 +<html lang="en">
2 +<head>
2 <meta charset="UTF-8"> 3 <meta charset="UTF-8">
3 <title>OCR Mapping with Manual Select Tool</title> 4 <title>OCR Mapping with Manual Select Tool</title>
4 <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script> 5 <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
5 - <style> 6 + <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" rel="stylesheet">
6 - body { font-family: sans-serif; background: #f5f5f5; } 7 + <link rel="stylesheet" href="{{ asset('css/ocr.css') }}">
7 - #app { display: flex; gap: 20px; padding: 20px; }
8 - .left-panel {
9 - width: 500px; background: #fff; padding: 15px;
10 - border-radius: 8px; box-shadow: 0 0 5px rgba(0,0,0,0.1);
11 - }
12 - .form-group { margin-bottom: 15px; }
13 - .form-group label { font-weight: bold; display: block; margin-bottom: 5px; }
14 - .form-group input { width: 100%; padding: 6px; border: 1px solid #ccc; border-radius: 4px; }
15 - .right-panel { flex: 1; position: relative; background: #eee; border-radius: 8px; overflow: hidden; user-select: none; }
16 - .pdf-container { position: relative; display: inline-block; }
17 - .bbox {
18 - position: absolute;
19 - border: 2px solid #ff5252;
20 - /*background-color: rgba(255, 82, 82, 0.2);*/
21 - cursor: pointer;
22 - }
23 - .bbox.active {
24 - border-color: #199601 !important;
25 - background-color: rgba(25, 150, 1, 0.4) !important;
26 - }
27 -
28 - @keyframes focusPulse {
29 - 0% { transform: scale(1); }
30 - 50% { transform: scale(1.05); }
31 - 100% { transform: scale(1); }
32 - }
33 - select {
34 - position: absolute;
35 - z-index: 10;
36 - background: #fff;
37 - border: 1px solid #ccc;
38 - }
39 - .select-box {
40 - position: absolute;
41 - /*border: 2px dashed #2196F3;*/
42 - background-color: rgba(33, 150, 243, 0.2);
43 - pointer-events: none;
44 - z-index: 5;
45 - }
46 - .delete-btn {
47 - position: absolute;
48 - bottom: -10px;
49 - right: -10px;
50 - background: #ff4d4d;
51 - color: #fff;
52 - border: none;
53 - border-radius: 50%;
54 - cursor: pointer;
55 - font-size: 14px;
56 - padding: 3px 6px;
57 - z-index: 20;
58 - }
59 - </style>
60 -
61 </head> 8 </head>
62 <body> 9 <body>
63 <meta name="csrf-token" content="{{ csrf_token() }}"> 10 <meta name="csrf-token" content="{{ csrf_token() }}">
64 <div id="app"> 11 <div id="app">
65 - <div class="right-panel" > 12 + <div class="left-panel" >
66 <div class="pdf-container" ref="pdfContainer" 13 <div class="pdf-container" ref="pdfContainer"
67 @mousedown="startSelect" 14 @mousedown="startSelect"
68 @mousemove="onSelect" 15 @mousemove="onSelect"
...@@ -86,13 +33,42 @@ ...@@ -86,13 +33,42 @@
86 v-if="!item.isDeleted" 33 v-if="!item.isDeleted"
87 class="bbox" 34 class="bbox"
88 :class="{ active: index === activeIndex }" 35 :class="{ active: index === activeIndex }"
36 + :data-index="index"
89 :data-field="item.field" 37 :data-field="item.field"
90 :style="getBoxStyle(item, index)" 38 :style="getBoxStyle(item, index)"
91 @click="onBoxClick(index)"> 39 @click="onBoxClick(index)">
92 40
93 - <button v-if="item.isManual && item.showDelete" 41 + <div class="edge top" data-handle="top"
94 - class="delete-btn" 42 + @mousedown="startResize($event, index, 'top')">
95 - @click.stop="deleteBox(index)">🗑</button> 43 + </div>
44 + <div class="edge right" data-handle="right"
45 + @mousedown="startResize($event, index, 'right')">
46 + </div>
47 + <div class="edge bottom" data-handle="bottom"
48 + @mousedown="startResize($event, index, 'bottom')">
49 + </div>
50 + <div class="edge left" data-handle="left"
51 + @mousedown="startResize($event, index, 'left')">
52 + </div>
53 + <!-- 4 góc -->
54 + <div class="corner top-left" data-handle="top-left"
55 + @mousedown="startResize($event, index, 'top-left')">
56 + </div>
57 + <div class="corner top-right" data-handle="top-right"
58 + @mousedown="startResize($event, index, 'top-right')">
59 + </div>
60 + <div class="corner bottom-left" data-handle="bottom-left"
61 + @mousedown="startResize($event, index, 'bottom-left')">
62 + </div>
63 + <div class="corner bottom-right" data-handle="bottom-right"
64 + @mousedown="startResize($event, index, 'bottom-right')">
65 + </div>
66 +
67 + <div v-if="item.isManual && item.showDelete"
68 + class="delete-btn"
69 + @click.stop="deleteBox(index)">
70 + <img src="{{ asset('icon/btn-delete.png') }}" alt="delete box" style="width: 20px; height: 20px;">
71 + </div>
96 </div> 72 </div>
97 73
98 74
...@@ -108,9 +84,12 @@ ...@@ -108,9 +84,12 @@
108 84
109 <!-- Dropdown thủ công --> 85 <!-- Dropdown thủ công -->
110 <select v-if="selectBox.showDropdown" 86 <select v-if="selectBox.showDropdown"
111 - :style="{ left: selectBox.x + 'px', top: (selectBox.y + selectBox.height) + 'px' }" 87 + class="manual-select"
88 + :style="{ left: selectBox.x + 'px', top: (selectBox.y + selectBox.height) + 'px', zIndex: 10000, position: 'absolute' }"
112 v-model="manualField" 89 v-model="manualField"
113 @change="applyManualMapping" 90 @change="applyManualMapping"
91 + @mousedown.stop
92 + @mouseup.stop
114 @click.stop 93 @click.stop
115 > 94 >
116 <option disabled value="">-- Chọn trường dữ liệu --</option> 95 <option disabled value="">-- Chọn trường dữ liệu --</option>
...@@ -119,13 +98,12 @@ ...@@ -119,13 +98,12 @@
119 </div> 98 </div>
120 </div> 99 </div>
121 100
122 - <div class="left-panel"> 101 + <div class="right-panel">
123 <div v-for="field in fieldOptions" :key="field.value" class="form-group"> 102 <div v-for="field in fieldOptions" :key="field.value" class="form-group">
124 <label>@{{ field.label }}</label> 103 <label>@{{ field.label }}</label>
125 <input v-model="formData[field.value]" 104 <input v-model="formData[field.value]"
126 @click="onInputClick(field.value)" 105 @click="onInputClick(field.value)"
127 @blur="removeAllFocus()" 106 @blur="removeAllFocus()"
128 -
129 > 107 >
130 </div> 108 </div>
131 <button @click="saveTemplate">💾Save</button> 109 <button @click="saveTemplate">💾Save</button>
...@@ -152,7 +130,8 @@ ...@@ -152,7 +130,8 @@
152 hasCustomerNameXY: false, 130 hasCustomerNameXY: false,
153 ocrData: [], 131 ocrData: [],
154 selectBox: { show: false, showDropdown: false, x: 0, y: 0, width: 0, height: 0, startX: 0, startY: 0 }, 132 selectBox: { show: false, showDropdown: false, x: 0, y: 0, width: 0, height: 0, startX: 0, startY: 0 },
155 - manualIndex: null 133 + manualIndex: null,
134 + suppressNextDocumentClick: false
156 } 135 }
157 }, 136 },
158 created() { 137 created() {
...@@ -163,14 +142,35 @@ ...@@ -163,14 +142,35 @@
163 }, 142 },
164 mounted() { 143 mounted() {
165 this.loadOCRData(); 144 this.loadOCRData();
166 - //Thêm event listener để xóa focus khi click ra ngoài 145 + // Thêm event listener để xử lý click ra ngoài
167 document.addEventListener('click', (e) => { 146 document.addEventListener('click', (e) => {
168 - if ( 147 + // Bỏ qua click ngay sau khi thả chuột tạo box
169 - !e.target.closest('.left-panel') && 148 + if (this.suppressNextDocumentClick) {
170 - !e.target.closest('.bbox') && 149 + this.suppressNextDocumentClick = false;
171 - !e.target.closest('select') 150 + return;
172 - ) { 151 + }
173 - this.removeAllFocus(); 152 + // Nếu đang có box thủ công mới quét và chưa chọn field
153 + if (this.manualIndex !== null) {
154 + const currentIdx = this.manualIndex;
155 + const currentBox = this.ocrData[currentIdx];
156 + if (currentBox && currentBox.isManual && !currentBox.field && !currentBox.isDeleted) {
157 + const bboxEl = e.target.closest('.bbox');
158 + const manualSelectEl = e.target.closest('.manual-select');
159 + const clickedInsideCurrentBox = bboxEl && String(bboxEl.getAttribute('data-index')) === String(currentIdx);
160 + const clickedInsideManualSelect = !!manualSelectEl;
161 + if (!clickedInsideCurrentBox && !clickedInsideManualSelect) {
162 + // Click ra ngoài box/ select → xóa box thủ công vừa quét
163 + this.deleteBox(currentIdx);
164 + // Đóng dropdown thủ công nếu còn mở
165 + this.selectBox.show = false;
166 + this.selectBox.showDropdown = false;
167 + this.isMappingManually = false;
168 + this.manualField = "";
169 + this.manualIndex = null;
170 + this.activeIndex = null;
171 + this.selectingIndex = null;
172 + }
173 + }
174 } 174 }
175 }); 175 });
176 }, 176 },
...@@ -181,116 +181,110 @@ ...@@ -181,116 +181,110 @@
181 } 181 }
182 }, 182 },
183 methods: { 183 methods: {
184 - // Map field cho box (không set active, chỉ dùng để load data từ DB) 184 + startResize(e, index, handle) {
185 - mapFieldToBox(index, fieldName, text = null) { 185 + e.stopPropagation();
186 - if (index == null) return; 186 + this.resizing = {
187 - // Xóa tất cả box có fieldName trùng lặp, chỉ giữ lại box hiện tại 187 + index,
188 - this.ocrData = this.ocrData.filter((box, i) => { 188 + handle,
189 - if (i === index) return true; 189 + startX: e.clientX,
190 - if (box.field === fieldName) { 190 + startY: e.clientY,
191 - return false; 191 + origBox: [...this.ocrData[index].bbox], // [x1, y1, x2, y2]
192 - } 192 + };
193 - return true;
194 - });
195 193
196 - // Cập nhật lại index sau khi filter 194 + document.addEventListener("mousemove", this.onResizing);
197 - const newIndex = this.ocrData.findIndex(box => box.bbox === this.ocrData[index]?.bbox); 195 + document.addEventListener("mouseup", this.stopResize);
198 - if (newIndex === -1) return; 196 + },
197 + onResizing(e) {
198 + if (!this.resizing) return;
199 +
200 + const { index, handle, startX, startY, origBox } = this.resizing;
201 + const dx = (e.clientX - startX) * (this.imageWidth / this.$refs.pdfImage.clientWidth);
202 + const dy = (e.clientY - startY) * (this.imageHeight / this.$refs.pdfImage.clientHeight);
203 +
204 + let [x1, y1, x2, y2] = origBox;
205 +
206 + if (handle === 'top-left') {
207 + x1 += dx; y1 += dy;
208 + } else if (handle === 'top-right') {
209 + x2 += dx; y1 += dy;
210 + } else if (handle === 'bottom-left') {
211 + x1 += dx; y2 += dy;
212 + } else if (handle === 'bottom-right') {
213 + x2 += dx; y2 += dy;
214 + } else if (handle === 'top') {
215 + y1 += dy;
216 + } else if (handle === 'bottom') {
217 + y2 += dy;
218 + } else if (handle === 'left') {
219 + x1 += dx;
220 + } else if (handle === 'right') {
221 + x2 += dx;
222 + }
199 223
200 - // Nếu box này từng gán field khác thì bỏ reset flag và tọa độ liên quan. 224 + // giữ không cho x1 > x2, y1 > y2
201 - const prev = this.ocrData[newIndex].field; 225 + if (x1 < x2 && y1 < y2) {
202 - if (prev && prev !== fieldName) { 226 + const scaleX = this.$refs.pdfImage.clientWidth / this.imageWidth;
203 - if (prev === 'customer_name') { 227 + const scaleY = this.$refs.pdfImage.clientHeight / this.imageHeight;
204 - this.hasCustomerNameXY = false; 228 + const newBbox = [
205 - this.customer_name_xy = ''; 229 + Math.round(x1),
230 + Math.round(y1),
231 + Math.round(x2),
232 + Math.round(y2)
233 + ];
234 + if (this.ocrData[index].isManual) {
235 + this.$set(this.ocrData[index], "bbox", newBbox);
236 + } else {
237 + this.selectBox = {
238 + ...this.selectBox,
239 + x: Math.round(x1 * scaleX),
240 + y: Math.round(y1 * scaleY),
241 + width: Math.round((x2 - x1) * scaleX),
242 + height: Math.round((y2 - y1) * scaleY),
243 + show: true,
244 + };
245 + this.resizing.newBbox = newBbox;
206 } 246 }
207 - this.ocrData[newIndex].field = null;
208 - this.ocrData[newIndex].field_xy = null;
209 } 247 }
210 248
211 - const bbox = this.ocrData[newIndex].bbox;
212 -
213 - const x1 = bbox[0];
214 - const y1 = bbox[1];
215 - const w = bbox[2];
216 - const h = bbox[3];
217 -
218 - const xyStr = `${x1},${y1},${w},${h}`;
219 - this.ocrData[newIndex].field = fieldName;
220 - this.ocrData[newIndex].field_xy = xyStr;
221 -
222 - this.formData[fieldName] = (text !== null ? text : (this.ocrData[newIndex].text || '')).trim();
223 -
224 - if (fieldName === 'customer_name') {
225 - this.hasCustomerNameXY = true;
226 - this.customer_name_xy = xyStr;
227 - }
228 }, 249 },
229 - 250 + stopResize() {
230 - async saveTemplate() { 251 + document.removeEventListener("mousemove", this.onResizing);
231 - 252 + document.removeEventListener("mouseup", this.stopResize);
232 - let customer_name = null; 253 +
233 - let customer_coords = null; 254 + if (!this.resizing) return;
234 - let fields = []; 255 + const { index, newBbox } = this.resizing;
235 - if (this.manualBoxData.customer_name) { 256 + const targetBox = this.ocrData[index];
236 - // Lấy từ manualBoxData nếu có 257 +
237 - customer_name = this.manualBoxData.customer_name.text; 258 + if (targetBox) {
238 - customer_coords = this.manualBoxData.customer_name.coords.join(','); 259 + if (!targetBox.isManual && newBbox) {
239 - fields = this.manualBoxData; 260 + const newBox = {
240 - console.log('Using manualBoxData for customer_name:', customer_name, customer_coords); 261 + ...targetBox,
241 - } else { 262 + bbox: newBbox,
242 - const found = this.ocrData.find(item => item.field === 'customer_name'); 263 + isManual: true,
243 - if (found) { 264 + isDeleted: false,
244 - customer_name = found.text; 265 + showDelete: true,
245 - customer_coords = found.field_xy; 266 + text: "",
267 + };
268 + this.$set(this.ocrData[index], "isDeleted", true);
269 + this.ocrData.push(newBox);
270 + this.selectingIndex = this.ocrData.length - 1;
271 + this.updateHiddenBorders(newBox.bbox);
272 +
273 + } else if (targetBox.isManual) {
274 + this.updateHiddenBorders(targetBox.bbox);
246 } 275 }
247 -
248 - const fieldsByName = {};
249 - this.ocrData.forEach(box => {
250 - if (box.field && !box.isDeleted) {
251 - fieldsByName[box.field] = {
252 - text: box.field,
253 - coords: box.field_xy || ''
254 - };
255 - }
256 - });
257 - fields = (fieldsByName);
258 - console.log('Using ocrData for customer_name:', customer_name, customer_coords);
259 - }
260 -
261 - if (!customer_coords || !customer_name) {
262 - alert("Bạn phải map customer_name (quét/select) trước khi lưu.");
263 - return;
264 } 276 }
265 277
266 - const payload = { 278 + this.resizing = null;
267 - customer_name_text: customer_name || '', 279 + this.selectBox.show = false;
268 - template_name: this.formData.template_name || this.formData.customer_name, 280 + },
269 - customer_name_xy: customer_coords || [], 281 + updateHiddenBorders(manualBox) {
270 - fields: fields 282 + this.ocrData.forEach(item => {
271 - }; 283 + if (!item.isManual) {
272 - 284 + item.hideBorder = this.isBoxInside(item.bbox, manualBox);
273 - try {
274 - const res = await fetch('/ocr/save-template', {
275 - method: 'POST',
276 - headers: {
277 - 'Content-Type': 'application/json',
278 - 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
279 - },
280 - body: JSON.stringify(payload)
281 - });
282 - const json = await res.json();
283 - if (json.success) {
284 - alert(json.message);
285 - } else {
286 - alert('Save failed');
287 } 285 }
288 - } catch (err) { 286 + });
289 - console.error(err);
290 - alert('Save error');
291 - }
292 }, 287 },
293 -
294 deleteBox(index) { 288 deleteBox(index) {
295 const item = this.ocrData[index]; 289 const item = this.ocrData[index];
296 if (item.isManual) { 290 if (item.isManual) {
...@@ -314,32 +308,9 @@ ...@@ -314,32 +308,9 @@
314 this.manualIndex = null; 308 this.manualIndex = null;
315 } 309 }
316 this.selectingIndex = null; 310 this.selectingIndex = null;
311 + this.activeIndex = null;
317 } 312 }
318 }, 313 },
319 -
320 -
321 - async loadOCRData() {
322 -
323 - try {
324 - const res = await fetch(`/ocr/data-list`);
325 - const data = await res.json();
326 -
327 - if (data.error) {
328 - console.error('Error loading data:', data.error);
329 - return;
330 - }
331 -
332 - this.ocrData = data.ocrData;
333 - this.pdfImageUrl = data.pdfImageUrl;
334 - this.fieldOptions = data.fieldOptions;
335 - this.dataMapping = data.dataMapping;
336 - this.is_template = data.is_template;
337 -
338 - } catch (error) {
339 - console.error('Error in loadOCRData:', error);
340 - }
341 - },
342 -
343 // Xử lý data sau khi image đã load 314 // Xử lý data sau khi image đã load
344 processLoadedData() { 315 processLoadedData() {
345 this.autoMapFieldsFromFormData(); 316 this.autoMapFieldsFromFormData();
...@@ -348,7 +319,6 @@ ...@@ -348,7 +319,6 @@
348 this.$forceUpdate(); 319 this.$forceUpdate();
349 }); 320 });
350 }, 321 },
351 -
352 autoMapFieldsFromFormData() { 322 autoMapFieldsFromFormData() {
353 this.manualBoxData = {}; 323 this.manualBoxData = {};
354 if(this.is_template) { 324 if(this.is_template) {
...@@ -388,7 +358,6 @@ ...@@ -388,7 +358,6 @@
388 }); 358 });
389 359
390 }, 360 },
391 -
392 onImageLoad() { 361 onImageLoad() {
393 const img = this.$refs.pdfImage; 362 const img = this.$refs.pdfImage;
394 this.imageWidth = img.naturalWidth; 363 this.imageWidth = img.naturalWidth;
...@@ -455,13 +424,7 @@ ...@@ -455,13 +424,7 @@
455 } 424 }
456 425
457 if (coords) { 426 if (coords) {
458 - if (isFromDB) { 427 + this.createManualBoxFromDB(field, coords, text);
459 - // Tạo box manual từ DB (không có nút xóa)
460 - this.createManualBoxFromDB(field, coords, text);
461 - } else {
462 - // Hiển thị lại box manual đã quét chọn (có nút xóa)
463 - this.showManualBox(field, coords, text);
464 - }
465 } 428 }
466 429
467 // Tìm lại index của box để set active 430 // Tìm lại index của box để set active
...@@ -476,49 +439,12 @@ ...@@ -476,49 +439,12 @@
476 439
477 if (idx !== -1) { 440 if (idx !== -1) {
478 this.activeIndex = idx; 441 this.activeIndex = idx;
479 - this.scrollToBox(idx);
480 // Reset selectingIndex để không hiển thị dropdown khi highlight từ input 442 // Reset selectingIndex để không hiển thị dropdown khi highlight từ input
481 this.selectingIndex = null; 443 this.selectingIndex = null;
482 } else { 444 } else {
483 this.activeIndex = null; 445 this.activeIndex = null;
484 } 446 }
485 }, 447 },
486 -
487 - scrollToBox(index) {
488 - if (!this.$refs.pdfContainer || index < 0 || index >= this.ocrData.length) return;
489 -
490 - const item = this.ocrData[index];
491 - if (!item || item.isDeleted) return;
492 -
493 - // Tính vị trí hiển thị của box
494 - const [x1, y1, x2, y2] = item.bbox;
495 - if (!this.imageWidth || !this.imageHeight || !this.$refs.pdfImage) return;
496 -
497 - const displayedWidth = this.$refs.pdfImage.clientWidth;
498 - const displayedHeight = this.$refs.pdfImage.clientHeight;
499 - const scaleX = displayedWidth / this.imageWidth;
500 - const scaleY = displayedHeight / this.imageHeight;
501 -
502 - const displayX = Math.round(x1 * scaleX);
503 - const displayY = Math.round(y1 * scaleY);
504 -
505 - // Scroll đến vị trí box
506 - const container = this.$refs.pdfContainer;
507 - const containerRect = container.getBoundingClientRect();
508 - const scrollTop = container.scrollTop;
509 - const scrollLeft = container.scrollLeft;
510 -
511 - // Tính vị trí scroll để box nằm ở giữa viewport
512 - const targetScrollTop = scrollTop + displayY - (containerRect.height / 2);
513 - const targetScrollLeft = scrollLeft + displayX - (containerRect.width / 2);
514 -
515 - container.scrollTo({
516 - top: Math.max(0, targetScrollTop),
517 - left: Math.max(0, targetScrollLeft),
518 - behavior: 'smooth'
519 - });
520 - },
521 -
522 // Xử lý khi click vào input 448 // Xử lý khi click vào input
523 onInputClick(fieldName) { 449 onInputClick(fieldName) {
524 // Kiểm tra xem field này có data không 450 // Kiểm tra xem field này có data không
...@@ -526,32 +452,39 @@ ...@@ -526,32 +452,39 @@
526 if (fieldValue && fieldValue.trim()) { 452 if (fieldValue && fieldValue.trim()) {
527 // Nếu có data từ DB, highlight và focus vào box tương ứng 453 // Nếu có data từ DB, highlight và focus vào box tương ứng
528 this.highlightField(fieldName); 454 this.highlightField(fieldName);
455 +
456 + // Không ẩn nút xóa các box khác để tránh nhầm lẫn
457 + // Chỉ highlight box tương ứng
529 } 458 }
530 }, 459 },
531 -
532 // Xóa tất cả focus 460 // Xóa tất cả focus
533 removeAllFocus() { 461 removeAllFocus() {
534 // Reset active index (chỉ mất màu xanh, không xóa box) 462 // Reset active index (chỉ mất màu xanh, không xóa box)
535 this.activeIndex = null; 463 this.activeIndex = null;
536 this.selectingIndex = null; 464 this.selectingIndex = null;
537 465
538 - // Ẩn hoàn toàn các box manual được tạo từ DB (không phải quét chọn thủ công) 466 + // Không ẩn nút xóa các box để tránh nhầm lẫn
539 - this.ocrData.forEach(item => { 467 + // Chỉ reset focus
540 - if (item.isManual && !item.showDelete) { 468 + },
541 - // Box manual từ DB (không có nút xóa) - ẩn hoàn toàn
542 - item.isDeleted = true;
543 - }
544 - });
545 469
546 - // Đảm bảo tất cả box OCR đều hiển thị (chỉ ẩn border khi cần thiết) 470 + // Xóa box quét chọn chưa hoàn thành
547 - this.ocrData.forEach(item => { 471 + removeIncompleteBoxes() {
548 - if (!item.isManual && item.hideBorder) { 472 + // Xóa box quét chọn chưa hoàn thành (chưa có field)
549 - // Chỉ ẩn border cho box OCR nằm trong vùng manual 473 + this.ocrData = this.ocrData.filter(item => {
550 - item.hideBorder = true; 474 + if (item.isManual && !item.field && item.showDelete) {
475 + // Chỉ xóa box manual chưa có field
476 + return false;
551 } 477 }
478 + return true;
552 }); 479 });
553 - },
554 480
481 + // Reset trạng thái quét chọn
482 + this.selectBox.show = false;
483 + this.selectBox.showDropdown = false;
484 + this.isMappingManually = false;
485 + this.manualField = "";
486 + this.manualIndex = null;
487 + },
555 // Xử lý khi click vào box 488 // Xử lý khi click vào box
556 onBoxClick(index) { 489 onBoxClick(index) {
557 const item = this.ocrData[index]; 490 const item = this.ocrData[index];
...@@ -563,11 +496,21 @@ ...@@ -563,11 +496,21 @@
563 const isDataOverridden = item.field && isFromDB && 496 const isDataOverridden = item.field && isFromDB &&
564 this.formData[item.field] !== this.dataMapping[item.field].text; 497 this.formData[item.field] !== this.dataMapping[item.field].text;
565 498
499 + // Set active index và hiển thị nút xóa cho box được click
500 + this.activeIndex = index;
501 +
502 + // Hiển thị nút xóa cho box được click (nếu là manual box)
503 + if (item.isManual) {
504 + item.showDelete = true;
505 + }
506 +
507 + // Không ẩn nút xóa các box khác để tránh nhầm lẫn
508 + // Chỉ hiển thị nút xóa cho box được click
509 +
566 if (item.isManual) { 510 if (item.isManual) {
567 // Manual box 511 // Manual box
568 if (isFromDB && !isDataOverridden) { 512 if (isFromDB && !isDataOverridden) {
569 // Manual box từ DB chưa ghi đè, KHÔNG cho chọn option 513 // Manual box từ DB chưa ghi đè, KHÔNG cho chọn option
570 - this.activeIndex = index;
571 this.selectingIndex = null; 514 this.selectingIndex = null;
572 } else { 515 } else {
573 // Manual box từ DB có data ghi đè HOẶC manual box bình thường, CHO PHÉP chọn option 516 // Manual box từ DB có data ghi đè HOẶC manual box bình thường, CHO PHÉP chọn option
...@@ -577,7 +520,6 @@ ...@@ -577,7 +520,6 @@
577 // Box OCR có field 520 // Box OCR có field
578 if (isFromDB && !isDataOverridden) { 521 if (isFromDB && !isDataOverridden) {
579 // Box có field từ DB chưa ghi đè, KHÔNG cho chọn option 522 // Box có field từ DB chưa ghi đè, KHÔNG cho chọn option
580 - this.activeIndex = index;
581 this.selectingIndex = null; 523 this.selectingIndex = null;
582 } else { 524 } else {
583 // Box có field từ DB đã ghi đè HOẶC field mới, CHO PHÉP chọn option 525 // Box có field từ DB đã ghi đè HOẶC field mới, CHO PHÉP chọn option
...@@ -589,7 +531,9 @@ ...@@ -589,7 +531,9 @@
589 } 531 }
590 }, 532 },
591 startSelect(e) { 533 startSelect(e) {
592 - if (this.isMappingManually || e.button !== 0) return; 534 + if (e.button !== 0) return; // Chỉ cho phép left click
535 + // Bỏ qua nếu click trên dropdown thủ công
536 + if (e.target.closest && e.target.closest('.manual-select')) return;
593 this.isSelecting = true; 537 this.isSelecting = true;
594 const rect = this.$refs.pdfContainer.getBoundingClientRect(); 538 const rect = this.$refs.pdfContainer.getBoundingClientRect();
595 this.selectBox.startX = e.clientX - rect.left; 539 this.selectBox.startX = e.clientX - rect.left;
...@@ -602,7 +546,6 @@ ...@@ -602,7 +546,6 @@
602 this.selectBox.showDropdown = false; 546 this.selectBox.showDropdown = false;
603 this.manualField = ""; 547 this.manualField = "";
604 }, 548 },
605 -
606 onSelect(e) { 549 onSelect(e) {
607 if (!this.isSelecting) return; 550 if (!this.isSelecting) return;
608 const rect = this.$refs.pdfContainer.getBoundingClientRect(); 551 const rect = this.$refs.pdfContainer.getBoundingClientRect();
...@@ -613,7 +556,6 @@ ...@@ -613,7 +556,6 @@
613 this.selectBox.width = Math.abs(currentX - this.selectBox.startX); 556 this.selectBox.width = Math.abs(currentX - this.selectBox.startX);
614 this.selectBox.height = Math.abs(currentY - this.selectBox.startY); 557 this.selectBox.height = Math.abs(currentY - this.selectBox.startY);
615 }, 558 },
616 -
617 endSelect(e) { 559 endSelect(e) {
618 if (!this.isSelecting) return; 560 if (!this.isSelecting) return;
619 this.isSelecting = false; 561 this.isSelecting = false;
...@@ -656,7 +598,7 @@ ...@@ -656,7 +598,7 @@
656 bbox: origBbox, 598 bbox: origBbox,
657 field: "", 599 field: "",
658 isManual: true, 600 isManual: true,
659 - showDelete: true, 601 + showDelete: true, // Hiển thị nút xóa ngay khi quét chọn
660 isDeleted: false, 602 isDeleted: false,
661 hideBorder: false 603 hideBorder: false
662 }); 604 });
...@@ -665,20 +607,29 @@ ...@@ -665,20 +607,29 @@
665 this.isMappingManually = true; 607 this.isMappingManually = true;
666 this.selectBox.showDropdown = true; 608 this.selectBox.showDropdown = true;
667 609
668 - e.stopPropagation(); 610 + // Set active index cho box mới tạo
669 - e.preventDefault(); 611 + this.activeIndex = this.ocrData.length - 1;
612 +
613 + // Sau khi thả chuột, bỏ qua click document kế tiếp
614 + this.suppressNextDocumentClick = true;
615 +
616 + // Không stopPropagation để event có thể lan truyền bình thường
617 + // e.stopPropagation();
618 + // e.preventDefault();
670 619
671 }, 620 },
672 applyMapping() { 621 applyMapping() {
673 const item = this.ocrData[this.selectingIndex]; 622 const item = this.ocrData[this.selectingIndex];
674 if (!item) return; 623 if (!item) return;
675 624
625 + console.log(`Applying mapping for index:`, item);
676 this.ocrData.forEach((box, i) => { 626 this.ocrData.forEach((box, i) => {
677 if (i !== this.selectingIndex && box.field === item.field) { 627 if (i !== this.selectingIndex && box.field === item.field) {
678 box.field = ''; 628 box.field = '';
679 } 629 }
680 }); 630 });
681 if (item.isManual) { 631 if (item.isManual) {
632 + console.log('aaaaaaaaaa')
682 // Nếu là manual box, chuyển sang chế độ manual mapping 633 // Nếu là manual box, chuyển sang chế độ manual mapping
683 this.manualIndex = this.selectingIndex; 634 this.manualIndex = this.selectingIndex;
684 this.manualField = item.field || ""; 635 this.manualField = item.field || "";
...@@ -713,7 +664,7 @@ ...@@ -713,7 +664,7 @@
713 664
714 const newBbox = this.ocrData[manualIndex].bbox; 665 const newBbox = this.ocrData[manualIndex].bbox;
715 666
716 - console.log(`manual for field "${this.manualField}" at index ${manualIndex} with bbox:`, newBbox); 667 + //console.log(`manual for field "${this.manualField}" at index ${manualIndex} with bbox:`, newBbox);
717 this.ocrData.forEach((box, i) => { 668 this.ocrData.forEach((box, i) => {
718 if (i !== manualIndex && box.field === this.ocrData[manualIndex].field) { 669 if (i !== manualIndex && box.field === this.ocrData[manualIndex].field) {
719 box.field = ''; 670 box.field = '';
...@@ -753,6 +704,9 @@ ...@@ -753,6 +704,9 @@
753 const finalText = combinedText.join(" "); 704 const finalText = combinedText.join(" ");
754 705
755 this.ocrData[manualIndex].field = this.manualField; 706 this.ocrData[manualIndex].field = this.manualField;
707 + // this.ocrData[manualIndex].showDelete = true; // Hiển thị nút xóa sau khi hoàn thành mapping
708 +
709 + console.log('123',this.ocrData[manualIndex])
756 this.formData[this.manualField] = finalText.trim(); 710 this.formData[this.manualField] = finalText.trim();
757 this.manualBoxData[this.manualField] = { 711 this.manualBoxData[this.manualField] = {
758 coords: newBbox, 712 coords: newBbox,
...@@ -771,8 +725,11 @@ ...@@ -771,8 +725,11 @@
771 this.selectBox.showDropdown = false; 725 this.selectBox.showDropdown = false;
772 this.manualField = ""; 726 this.manualField = "";
773 this.manualIndex = null; 727 this.manualIndex = null;
774 - },
775 728
729 + // Giữ nguyên nút xóa cho box vừa hoàn thành
730 + // Không ẩn nút xóa các box khác để tránh nhầm lẫn
731 +
732 + },
776 isBoxInside(inner, outer) { 733 isBoxInside(inner, outer) {
777 // inner: bbox của OCR item [x1, y1, x2, y2] 734 // inner: bbox của OCR item [x1, y1, x2, y2]
778 // outer: bbox của vùng manual [x1, y1, x2, y2] 735 // outer: bbox của vùng manual [x1, y1, x2, y2]
...@@ -792,13 +749,10 @@ ...@@ -792,13 +749,10 @@
792 inner[3] < outer[1] || // inner.bottom < outer.top → hoàn toàn phía trên 749 inner[3] < outer[1] || // inner.bottom < outer.top → hoàn toàn phía trên
793 inner[1] > outer[3] // inner.top > outer.bottom → hoàn toàn phía dưới 750 inner[1] > outer[3] // inner.top > outer.bottom → hoàn toàn phía dưới
794 ); 751 );
795 - 752 + // console.log(`isBoxInside: inner=${inner}, outer=${outer}, isFullyInside=${isFullyInside}, isOverlapping=${isOverlapping}`);
796 // Trả về true nếu box OCR nằm hoàn toàn trong hoặc giao nhau đáng kể 753 // Trả về true nếu box OCR nằm hoàn toàn trong hoặc giao nhau đáng kể
797 return isFullyInside || isOverlapping; 754 return isFullyInside || isOverlapping;
798 }, 755 },
799 -
800 -
801 -
802 getPartialText(text, bbox, selectBbox) { 756 getPartialText(text, bbox, selectBbox) {
803 const [x1, y1, x2, y2] = bbox; 757 const [x1, y1, x2, y2] = bbox;
804 const [sx1, sy1, sx2, sy2] = selectBbox; 758 const [sx1, sy1, sx2, sy2] = selectBbox;
...@@ -827,8 +781,6 @@ ...@@ -827,8 +781,6 @@
827 zIndex: 9999 781 zIndex: 9999
828 }; 782 };
829 }, 783 },
830 -
831 - // Tạo box manual từ tọa độ trong DB
832 createManualBoxFromDB(fieldName, coordinates, text) { 784 createManualBoxFromDB(fieldName, coordinates, text) {
833 if (!this.imageWidth || !this.imageHeight) { 785 if (!this.imageWidth || !this.imageHeight) {
834 console.log('Cannot create manual box: Image not loaded'); 786 console.log('Cannot create manual box: Image not loaded');
...@@ -859,7 +811,7 @@ ...@@ -859,7 +811,7 @@
859 bbox: coords, 811 bbox: coords,
860 field: fieldName, 812 field: fieldName,
861 isManual: true, 813 isManual: true,
862 - showDelete: false, 814 + showDelete: true,
863 isDeleted: false, 815 isDeleted: false,
864 hideBorder: true 816 hideBorder: true
865 }; 817 };
...@@ -872,55 +824,91 @@ ...@@ -872,55 +824,91 @@
872 console.warn('Invalid coordinates for manual box:', coords); 824 console.warn('Invalid coordinates for manual box:', coords);
873 } 825 }
874 }, 826 },
827 + async loadOCRData() {
875 828
876 - // Hiển thị lại box manual đã quét chọn (có nút xóa) 829 + try {
877 - showManualBox(fieldName, coordinates, text) { 830 + const res = await fetch(`/ocr/data-list`);
878 - if (!this.imageWidth || !this.imageHeight) { 831 + const data = await res.json();
879 - console.log('Cannot show manual box: Image not loaded');
880 - return;
881 - }
882 832
883 - // Parse coordinates 833 + if (data.error) {
884 - let coords; 834 + console.error('Error loading data:', data.error);
885 - if (typeof coordinates === 'string') { 835 + return;
886 - coords = coordinates.split(',').map(Number); 836 + }
887 - } else if (Array.isArray(coordinates)) {
888 - coords = coordinates;
889 - } else {
890 - console.error('Invalid coordinates format:', coordinates);
891 - return;
892 - }
893 837
894 - const [x1, y1, x2, y2] = coords; 838 + this.ocrData = data.ocrData;
839 + this.pdfImageUrl = data.pdfImageUrl;
840 + this.fieldOptions = data.fieldOptions;
841 + this.dataMapping = data.dataMapping;
842 + this.is_template = data.is_template;
843 + console.log('Loaded OCR data:', this.ocrData);
895 844
896 - // Kiểm tra tọa độ có hợp lệ không 845 + } catch (error) {
897 - if (x1 >= 0 && y1 >= 0 && x2 > x1 && y2 > y1 && 846 + console.error('Error in loadOCRData:', error);
898 - x2 <= this.imageWidth && y2 <= this.imageHeight) { 847 + }
848 + },
849 + async saveTemplate() {
899 850
900 - // Xóa box cũ có cùng fieldName trước khi hiển thị lại 851 + let customer_name = null;
901 - this.ocrData = this.ocrData.filter(box => !(box.field === fieldName && box.isManual)); 852 + let customer_coords = null;
853 + let fields = [];
854 + if (this.manualBoxData.customer_name) {
855 + // Lấy từ manualBoxData nếu có
856 + customer_name = this.manualBoxData.customer_name.text;
857 + customer_coords = this.manualBoxData.customer_name.coords.join(',');
858 + fields = this.manualBoxData;
859 + console.log('Using manualBoxData for customer_name:', customer_name, customer_coords);
860 + } else {
861 + const found = this.ocrData.find(item => item.field === 'customer_name');
862 + if (found) {
863 + customer_name = found.text;
864 + customer_coords = found.field_xy;
865 + }
902 866
903 - // Kiểm tra xem đây có phải box quét chọn hay box OCR 867 + const fieldsByName = {};
904 - const isFromOCR = this.manualBoxData[fieldName] && this.manualBoxData[fieldName].isFromOCR; 868 + this.ocrData.forEach(box => {
869 + if (box.field && !box.isDeleted) {
870 + fieldsByName[box.field] = {
871 + text: box.field,
872 + coords: box.field_xy || ''
873 + };
874 + }
875 + });
876 + fields = (fieldsByName);
877 + console.log('Using ocrData for customer_name:', customer_name, customer_coords);
878 + }
905 879
906 - // Hiển thị lại box manual (có nút xóa nếu là quét chọn, không có nút xóa nếu là OCR) 880 + if (!customer_coords || !customer_name) {
907 - const manualBox = { 881 + alert("Bạn phải map customer_name (quét/select) trước khi lưu.");
908 - text: text || '', 882 + return;
909 - bbox: coords, 883 + }
910 - field: fieldName,
911 - isManual: true,
912 - showDelete: !isFromOCR, // Chỉ hiển thị nút xóa nếu KHÔNG phải từ OCR
913 - isDeleted: false,
914 - hideBorder: false
915 - };
916 884
917 - this.ocrData.push(manualBox); 885 + const payload = {
918 - // Force re-render 886 + customer_name_text: customer_name || '',
919 - this.$forceUpdate(); 887 + template_name: this.formData.template_name || this.formData.customer_name,
920 - } else { 888 + customer_name_xy: customer_coords || [],
921 - console.warn('Invalid coordinates for manual box:', coords); 889 + fields: fields
890 + };
891 +
892 + try {
893 + const res = await fetch('/ocr/save-template', {
894 + method: 'POST',
895 + headers: {
896 + 'Content-Type': 'application/json',
897 + 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
898 + },
899 + body: JSON.stringify(payload)
900 + });
901 + const json = await res.json();
902 + if (json.success) {
903 + alert(json.message);
904 + } else {
905 + alert('Save failed');
906 + }
907 + } catch (err) {
908 + console.error(err);
909 + alert('Save error');
922 } 910 }
923 - } 911 + },
924 912
925 } 913 }
926 }); 914 });
......