tien_nemo

demo2

...@@ -22,6 +22,7 @@ class OcrController extends Controller ...@@ -22,6 +22,7 @@ class OcrController extends Controller
22 'customer_name_xy' => 'required|string', 22 'customer_name_xy' => 'required|string',
23 ]); 23 ]);
24 $dataDetail = $request->fields ?? []; 24 $dataDetail = $request->fields ?? [];
25 + $tableColumns = $request->table_columns ?? [];
25 try { 26 try {
26 $masterTemplate = MstTemplate::updateOrCreate( 27 $masterTemplate = MstTemplate::updateOrCreate(
27 ['tpl_name' => $request->template_name], 28 ['tpl_name' => $request->template_name],
...@@ -49,6 +50,24 @@ class OcrController extends Controller ...@@ -49,6 +50,24 @@ class OcrController extends Controller
49 ); 50 );
50 51
51 } 52 }
53 +
54 + // Lưu mapping cột bảng (lưu col index vào field_xy với field_name đặc biệt)
55 + if (!empty($tableColumns) && is_array($tableColumns)) {
56 + foreach ($tableColumns as $name => $colIdx) {
57 + if ($colIdx === null || $colIdx === '' || $colIdx === false) {
58 + continue;
59 + }
60 + DtTemplate::updateOrInsert(
61 + [
62 + 'tpl_id' => $masterTemplate->id,
63 + 'field_name' => '__table_col__' . $name,
64 + ],
65 + [
66 + 'field_xy' => (string) $colIdx,
67 + ]
68 + );
69 + }
70 + }
52 return response()->json([ 71 return response()->json([
53 'success' => true, 72 'success' => true,
54 'message' => 'Lưu template thành công', 73 'message' => 'Lưu template thành công',
...@@ -70,8 +89,8 @@ class OcrController extends Controller ...@@ -70,8 +89,8 @@ class OcrController extends Controller
70 $templateName = $request->get('template_name', ''); 89 $templateName = $request->get('template_name', '');
71 90
72 // Giả sử file OCR JSON & ảnh nằm trong storage/app/public/image/ 91 // Giả sử file OCR JSON & ảnh nằm trong storage/app/public/image/
73 - $jsonPath = public_path("image/3_1757295841_with_table.json"); 92 + $jsonPath = public_path("image/nemo_new_1757393338_with_table.json");
74 - $imgPath = ("image/3_1757295841.jpg"); 93 + $imgPath = ("image/nemo_new_1757393338.jpg");
75 94
76 if (!file_exists($jsonPath)) { 95 if (!file_exists($jsonPath)) {
77 return response()->json(['error' => 'File OCR JSON không tìm thấy'], 404); 96 return response()->json(['error' => 'File OCR JSON không tìm thấy'], 404);
...@@ -100,7 +119,13 @@ class OcrController extends Controller ...@@ -100,7 +119,13 @@ class OcrController extends Controller
100 // Lấy detail của template 119 // Lấy detail của template
101 $details = DtTemplate::where('tpl_id', $mst->id)->get(); 120 $details = DtTemplate::where('tpl_id', $mst->id)->get();
102 121
122 + $tableColumnMapping = [];
103 foreach ($details as $detail) { 123 foreach ($details as $detail) {
124 + if (strpos($detail->field_name, '__table_col__') === 0) {
125 + $name = substr($detail->field_name, strlen('__table_col__'));
126 + $tableColumnMapping[$name] = is_numeric($detail->field_xy) ? intval($detail->field_xy) : null;
127 + continue;
128 + }
104 $coords = array_map('intval', explode(',', $detail->field_xy)); 129 $coords = array_map('intval', explode(',', $detail->field_xy));
105 // coords = [x1, y1, x2, y2] 130 // coords = [x1, y1, x2, y2]
106 131
...@@ -130,6 +155,7 @@ class OcrController extends Controller ...@@ -130,6 +155,7 @@ class OcrController extends Controller
130 'pdfImageUrl' => $imgPath, 155 'pdfImageUrl' => $imgPath,
131 'dataMapping' => $dataMapping, 156 'dataMapping' => $dataMapping,
132 'is_template' => $is_template, 157 'is_template' => $is_template,
158 + 'tableColumnMapping' => $tableColumnMapping ?? [], //?? new \stdClass(),
133 'fieldOptions' => [ 159 'fieldOptions' => [
134 [ 'value' => 'template_name', 'label' => 'Tên Mẫu PDF' ], 160 [ 'value' => 'template_name', 'label' => 'Tên Mẫu PDF' ],
135 [ 'value' => 'customer_name', 'label' => 'Tên khách hàng' ], 161 [ 'value' => 'customer_name', 'label' => 'Tên khách hàng' ],
......
...@@ -11,14 +11,14 @@ from PIL import Image, ImageEnhance ...@@ -11,14 +11,14 @@ from PIL import Image, ImageEnhance
11 11
12 # ==== Config ==== 12 # ==== Config ====
13 BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")) 13 BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
14 -# PDF_NAME = 'aaaa' 14 +PDF_NAME = 'nemo_new'
15 15
16 # PDF path 16 # PDF path
17 -pdf_path = Path(BASE_DIR) / "storage" / "pdf" / "3.pdf" 17 +pdf_path = Path(BASE_DIR) / "storage" / "pdf" / "2.pdf"
18 # Output folder 18 # Output folder
19 output_folder = Path(BASE_DIR) / "public" / "image" 19 output_folder = Path(BASE_DIR) / "public" / "image"
20 20
21 -PDF_NAME = pdf_path.stem # Get the stem of the PDF file 21 +# PDF_NAME = pdf_path.stem # Get the stem of the PDF file
22 #print(PDF_NAME) 22 #print(PDF_NAME)
23 23
24 os.makedirs(output_folder, exist_ok=True) 24 os.makedirs(output_folder, exist_ok=True)
...@@ -151,9 +151,31 @@ for table in table_info: ...@@ -151,9 +151,31 @@ for table in table_info:
151 x1, y1, x2, y2 = cell["cell"] 151 x1, y1, x2, y2 = cell["cell"]
152 cell_texts = [] 152 cell_texts = []
153 153
154 + # Helper: compute overlap ratio of bbox against cell
155 + def overlap_ratio(bbox, cell_box):
156 + ix1 = max(bbox[0], cell_box[0])
157 + iy1 = max(bbox[1], cell_box[1])
158 + ix2 = min(bbox[2], cell_box[2])
159 + iy2 = min(bbox[3], cell_box[3])
160 + iw = max(0, ix2 - ix1)
161 + ih = max(0, iy2 - iy1)
162 + inter = iw * ih
163 + bbox_area = max(1, (bbox[2] - bbox[0]) * (bbox[3] - bbox[1]))
164 + return inter / float(bbox_area)
165 +
166 + # Helper: check center inside cell
167 + def center_inside(bbox, cell_box):
168 + cx = (bbox[0] + bbox[2]) / 2.0
169 + cy = (bbox[1] + bbox[3]) / 2.0
170 + return (cx >= cell_box[0] and cx <= cell_box[2] and
171 + cy >= cell_box[1] and cy <= cell_box[3])
172 +
173 + cell_box = [x1, y1, x2, y2]
154 for item in ocr_data_list: 174 for item in ocr_data_list:
155 bx1, by1, bx2, by2 = item["bbox"] 175 bx1, by1, bx2, by2 = item["bbox"]
156 - if bx1 >= x1 and by1 >= y1 and bx2 <= x2 and by2 <= y2: 176 + bbox = [bx1, by1, bx2, by2]
177 + # Accept if bbox is largely inside the cell, or its center lies inside the cell
178 + if overlap_ratio(bbox, cell_box) >= 0.3 or center_inside(bbox, cell_box):
157 cell_texts.append(item["text"]) 179 cell_texts.append(item["text"])
158 180
159 # thêm vào cell gốc 181 # thêm vào cell gốc
......
...@@ -2,46 +2,73 @@ import cv2 ...@@ -2,46 +2,73 @@ import cv2
2 import numpy as np 2 import numpy as np
3 import os 3 import os
4 4
5 -def detect_tables(image_path): 5 +def filter_horizontal_lines(lines_h, img_width, min_h_len_ratio=0.8, tol_y=10):
6 + if lines_h is None:
7 + return [], []
8 +
9 + ys_candidates = []
10 + for l in lines_h:
11 + x1, y1, x2, y2 = l[0]
12 + if abs(y1 - y2) <= 3: # ngang
13 + line_len = abs(x2 - x1)
14 + y_mid = int(round((y1 + y2) / 2))
15 + ys_candidates.append((y_mid, line_len, x1, x2))
16 +
17 + ys_candidates.sort(key=lambda x: x[0])
18 + filtered_lines, line_segments, current_group = [], [], []
19 +
20 + for y, length, x1, x2 in ys_candidates:
21 + if not current_group:
22 + current_group.append((y, length, x1, x2))
23 + else:
24 + if abs(y - current_group[-1][0]) <= tol_y:
25 + current_group.append((y, length, x1, x2))
26 + else:
27 + longest = max(current_group, key=lambda x: x[1])
28 + if longest[1] >= min_h_len_ratio * img_width:
29 + filtered_lines.append(longest[0])
30 + line_segments.append((longest[2], longest[3], longest[0]))
31 + else:
32 + break
33 + current_group = [(y, length, x1, x2)]
34 +
35 + if current_group:
36 + longest = max(current_group, key=lambda x: x[1])
37 + if longest[1] >= min_h_len_ratio * img_width:
38 + filtered_lines.append(longest[0])
39 + line_segments.append((longest[2], longest[3], longest[0]))
40 +
41 + total_rows = max(0, len(filtered_lines) - 1)
42 + print(f"Detected {total_rows} rows")
43 + return filtered_lines, line_segments
44 +
45 +
46 +def detect_tables(image_path, gap_threshold=50):
6 img = cv2.imread(image_path) 47 img = cv2.imread(image_path)
7 if img is None: 48 if img is None:
8 raise FileNotFoundError(f"Không đọc được ảnh: {image_path}") 49 raise FileNotFoundError(f"Không đọc được ảnh: {image_path}")
9 50
10 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) 51 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
11 blur = cv2.GaussianBlur(gray, (3, 3), 0) 52 blur = cv2.GaussianBlur(gray, (3, 3), 0)
12 -
13 - # Edge detection
14 edges = cv2.Canny(blur, 50, 150, apertureSize=3) 53 edges = cv2.Canny(blur, 50, 150, apertureSize=3)
15 54
16 # --- Horizontal lines --- 55 # --- Horizontal lines ---
17 lines_h = cv2.HoughLinesP(edges, 1, np.pi/180, threshold=120, 56 lines_h = cv2.HoughLinesP(edges, 1, np.pi/180, threshold=120,
18 minLineLength=int(img.shape[1] * 0.6), maxLineGap=20) 57 minLineLength=int(img.shape[1] * 0.6), maxLineGap=20)
19 - ys_candidates, line_segments = [], [] 58 + img_height, img_width = img.shape[:2]
20 - if lines_h is not None: 59 + ys, line_segments = filter_horizontal_lines(lines_h, img_width, min_h_len_ratio=0.8, tol_y=10)
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 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) 60 total_rows = max(0, len(ys) - 1)
35 61
36 # --- Vertical lines --- 62 # --- Vertical lines ---
37 lines_v = cv2.HoughLinesP(edges, 1, np.pi/180, threshold=100, 63 lines_v = cv2.HoughLinesP(edges, 1, np.pi/180, threshold=100,
38 - minLineLength=int(img.shape[0] * 0.5), maxLineGap=20) 64 + minLineLength=int(img.shape[0] * 0.4), maxLineGap=20)
39 - xs = [] 65 + v_lines, xs = [], []
40 if lines_v is not None: 66 if lines_v is not None:
41 for l in lines_v: 67 for l in lines_v:
42 x1, y1, x2, y2 = l[0] 68 x1, y1, x2, y2 = l[0]
43 if abs(x1 - x2) <= 3: 69 if abs(x1 - x2) <= 3:
44 xs.append(int(round((x1 + x2) / 2))) 70 xs.append(int(round((x1 + x2) / 2)))
71 + v_lines.append((int(round((x1 + x2) / 2)), min(y1, y2), max(y1, y2)))
45 72
46 # gom nhóm x 73 # gom nhóm x
47 x_pos, tol_v = [], 10 74 x_pos, tol_v = [], 10
...@@ -50,26 +77,66 @@ def detect_tables(image_path): ...@@ -50,26 +77,66 @@ def detect_tables(image_path):
50 x_pos.append(v) 77 x_pos.append(v)
51 78
52 total_cols = max(0, len(x_pos) - 1) 79 total_cols = max(0, len(x_pos) - 1)
53 -
54 tables = [] 80 tables = []
81 +
55 if total_rows > 0 and total_cols > 0: 82 if total_rows > 0 and total_cols > 0:
56 y_min, y_max = ys[0], ys[-1] 83 y_min, y_max = ys[0], ys[-1]
57 x_min, x_max = x_pos[0], x_pos[-1] 84 x_min, x_max = x_pos[0], x_pos[-1]
58 table_box = (x_min, y_min, x_max, y_max) 85 table_box = (x_min, y_min, x_max, y_max)
59 86
60 - # build cells
61 rows_data = [] 87 rows_data = []
62 for i in range(total_rows): 88 for i in range(total_rows):
63 row_cells = [] 89 row_cells = []
64 - for j in range(total_cols): 90 + j = 0
91 + while j < total_cols:
65 cell_box = (x_pos[j], ys[i], x_pos[j+1], ys[i+1]) 92 cell_box = (x_pos[j], ys[i], x_pos[j+1], ys[i+1])
93 + row_height = cell_box[3] - cell_box[1]
94 +
95 + # Check vertical line coverage (>=70% chiều cao hàng)
96 + has_left = any(
97 + abs(x - cell_box[0]) <= tol_v and
98 + (min(y_end, cell_box[3]) - max(y_start, cell_box[1])) >= 0.7 * row_height
99 + for x, y_start, y_end in v_lines
100 + )
101 + has_right = any(
102 + abs(x - cell_box[2]) <= tol_v and
103 + (min(y_end, cell_box[3]) - max(y_start, cell_box[1])) >= 0.7 * row_height
104 + for x, y_start, y_end in v_lines
105 + )
106 +
107 + if has_left and has_right:
108 + col_start = j
109 + col_end = j
110 + # nếu cột tiếp theo không có line → merge
111 + while col_end + 1 < total_cols:
112 + next_box = (x_pos[col_end+1], ys[i], x_pos[col_end+2], ys[i+1])
113 + has_next_left = any(
114 + abs(x - next_box[0]) <= tol_v and
115 + (min(y_end, next_box[3]) - max(y_start, next_box[1])) >= 0.7 * row_height
116 + for x, y_start, y_end in v_lines
117 + )
118 + if not has_next_left: # merge tiếp
119 + col_end += 1
120 + else:
121 + break
122 +
123 + merged_box = (x_pos[col_start], ys[i], x_pos[col_end+1], ys[i+1])
124 + if col_start == col_end:
125 + col_id = col_start
126 + else:
127 + col_id = f"{col_start}-{col_end}"
128 +
66 row_cells.append({ 129 row_cells.append({
67 - "cell": cell_box, 130 + "cell": merged_box,
68 "row_idx": i, 131 "row_idx": i,
69 - "col_idx": j 132 + "col_idx": col_id
70 }) 133 })
71 - # Vẽ ô 134 + cv2.rectangle(img, (merged_box[0], merged_box[1]),
72 - cv2.rectangle(img, (cell_box[0], cell_box[1]), (cell_box[2], cell_box[3]), (0, 255, 255), 1) 135 + (merged_box[2], merged_box[3]), (0, 255, 255), 1)
136 + j = col_end + 1
137 + else:
138 + j += 1 # skip ô lỗi (không có line đầy đủ)
139 +
73 rows_data.append(row_cells) 140 rows_data.append(row_cells)
74 141
75 tables.append({ 142 tables.append({
...@@ -78,11 +145,9 @@ def detect_tables(image_path): ...@@ -78,11 +145,9 @@ def detect_tables(image_path):
78 "table_box": table_box, 145 "table_box": table_box,
79 "cells": rows_data 146 "cells": rows_data
80 }) 147 })
81 -
82 - # vẽ viền bảng
83 cv2.rectangle(img, (x_min, y_min), (x_max, y_max), (255, 0, 0), 2) 148 cv2.rectangle(img, (x_min, y_min), (x_max, y_max), (255, 0, 0), 2)
84 149
85 - debug_path = os.path.splitext(image_path)[0] + "_debug.jpg" 150 + debug_path = os.path.splitext(image_path)[0] + "_fix_debug.jpg"
86 cv2.imwrite(debug_path, img) 151 cv2.imwrite(debug_path, img)
87 152
88 return tables 153 return tables
......
...@@ -79,7 +79,7 @@ ...@@ -79,7 +79,7 @@
79 @change="applyMapping" 79 @change="applyMapping"
80 > 80 >
81 <option disabled value="">-- Chọn trường dữ liệu --</option> 81 <option disabled value="">-- Chọn trường dữ liệu --</option>
82 - <option v-for="field in fieldData" :value="field.value">@{{ field.label }}</option> 82 + <option v-for="field in getAllowedOptionsForBox(ocrData[selectingIndex])" :value="field.value">@{{ field.label }}</option>
83 </select> 83 </select>
84 84
85 <!-- Dropdown thủ công --> 85 <!-- Dropdown thủ công -->
...@@ -93,7 +93,7 @@ ...@@ -93,7 +93,7 @@
93 @click.stop 93 @click.stop
94 > 94 >
95 <option disabled value="">-- Chọn trường dữ liệu --</option> 95 <option disabled value="">-- Chọn trường dữ liệu --</option>
96 - <option v-for="field in fieldData" :value="field.value">@{{ field.label }}</option> 96 + <option v-for="field in getAllowedOptionsForManual()" :value="field.value">@{{ field.label }}</option>
97 </select> 97 </select>
98 </div> 98 </div>
99 </div> 99 </div>
...@@ -118,24 +118,30 @@ ...@@ -118,24 +118,30 @@
118 </thead> 118 </thead>
119 <tbody> 119 <tbody>
120 <tr class="table-detail" 120 <tr class="table-detail"
121 - v-for="(row, rowIndex) in total_rows" 121 + v-for="(row, rowIndex) in dataRowsCount"
122 :key="rowIndex" 122 :key="rowIndex"
123 :data-row="rowIndex" 123 :data-row="rowIndex"
124 > 124 >
125 <td class="form-group"> 125 <td class="form-group">
126 <input 126 <input
127 + @click="onTableInputClick('product_name', rowIndex)"
128 + v-model="tableForm[rowIndex].product_name"
127 :data-row="rowIndex" 129 :data-row="rowIndex"
128 :data-field="'product_name'" 130 :data-field="'product_name'"
129 placeholder="Enter name"> 131 placeholder="Enter name">
130 </td> 132 </td>
131 <td class="form-group"> 133 <td class="form-group">
132 <input 134 <input
135 + @click="onTableInputClick('product_code', rowIndex)"
136 + v-model="tableForm[rowIndex].product_code"
133 :data-row="rowIndex" 137 :data-row="rowIndex"
134 :data-field="'product_code'" 138 :data-field="'product_code'"
135 placeholder="Enter code"> 139 placeholder="Enter code">
136 </td> 140 </td>
137 <td class="form-group"> 141 <td class="form-group">
138 <input 142 <input
143 + @click="onTableInputClick('quantity', rowIndex)"
144 + v-model.number="tableForm[rowIndex].quantity"
139 :data-row="rowIndex" 145 :data-row="rowIndex"
140 :data-field="'quantity'" 146 :data-field="'quantity'"
141 type="number" 147 type="number"
...@@ -166,8 +172,11 @@ ...@@ -166,8 +172,11 @@
166 tableInfo: {}, 172 tableInfo: {},
167 total_rows: 0, 173 total_rows: 0,
168 table_box: [], 174 table_box: [],
175 + tableColumnMapping: { product_name: null, product_code: null, quantity: null },
176 + tableForm: [],
169 manualBoxData: {}, 177 manualBoxData: {},
170 fieldOptions: [], 178 fieldOptions: [],
179 + tempFocusIndex: null,
171 customer_name_xy: '', 180 customer_name_xy: '',
172 hasCustomerNameXY: false, 181 hasCustomerNameXY: false,
173 ocrData: [], 182 ocrData: [],
...@@ -220,9 +229,156 @@ ...@@ -220,9 +229,156 @@
220 fieldData() { 229 fieldData() {
221 // Lọc bỏ template_name nếu không cần cho phần form mapping 230 // Lọc bỏ template_name nếu không cần cho phần form mapping
222 return this.fieldOptions.filter(f => f.value !== "template_name"); 231 return this.fieldOptions.filter(f => f.value !== "template_name");
232 + },
233 + dataRowsCount() {
234 + return this.total_rows || 0;
223 } 235 }
224 }, 236 },
225 methods: { 237 methods: {
238 + onTableInputClick(field, uiRowIndex) {
239 + // Determine column from saved mapping
240 + const colIdx = this.tableColumnMapping ? this.tableColumnMapping[field] : null;
241 + if (colIdx === null || colIdx === undefined) return;
242 + // uiRowIndex starts at 0 for first data row; tableInfo row_idx=uiRowIndex+1
243 + const tableRowIdx = (uiRowIndex + 1);
244 + if (!Array.isArray(this.tableInfo) || !this.tableInfo[tableRowIdx]) return;
245 + const row = this.tableInfo[tableRowIdx];
246 + const cell = row.find(c => c && c.col_idx === colIdx);
247 + if (!cell || !Array.isArray(cell.cell)) return;
248 +
249 + const coords = cell.cell; // [x1,y1,x2,y2]
250 + // Remove previous temp focus
251 + if (this.tempFocusIndex !== null && this.ocrData[this.tempFocusIndex]) {
252 + this.$set(this.ocrData[this.tempFocusIndex], 'isDeleted', true);
253 + this.tempFocusIndex = null;
254 + }
255 + // Create a temporary manual focus box for highlighting
256 + const tmpBox = {
257 + text: '',
258 + bbox: coords,
259 + field: field,
260 + isManual: true,
261 + showDelete: false,
262 + isDeleted: false,
263 + hideBorder: false
264 + };
265 + this.ocrData.push(tmpBox);
266 + this.tempFocusIndex = this.ocrData.length - 1;
267 + this.activeIndex = this.tempFocusIndex;
268 + this.selectingIndex = null;
269 + // Ensure it renders
270 + this.$nextTick(() => this.$forceUpdate());
271 + },
272 + ensureTableFormSize() {
273 + const rows = this.dataRowsCount;
274 + if (!Array.isArray(this.tableForm)) this.tableForm = [];
275 + for (let i = 0; i < rows; i++) {
276 + if (!this.tableForm[i]) {
277 + this.$set(this.tableForm, i, { product_name: '', product_code: '', quantity: null });
278 + }
279 + }
280 + if (this.tableForm.length > rows) {
281 + this.tableForm.splice(rows);
282 + }
283 + },
284 + getAllowedOptionsForBox(item) {
285 + const isInTable = this.isBboxInTable(item.bbox);
286 + if (isInTable) {
287 + return [
288 + { value: 'product_name', label: 'Product Name' },
289 + { value: 'product_code', label: 'Product Code' },
290 + { value: 'quantity', label: 'Quantity' },
291 + ];
292 + }
293 + return this.fieldData;
294 + },
295 + getAllowedOptionsForManual() {
296 + // Use current selection box to determine
297 + const origBbox = this.getCurrentOrigSelectBbox();
298 + const isInTable = origBbox ? this.isBboxInTable(origBbox) : false;
299 + if (isInTable) {
300 + return [
301 + { value: 'product_name', label: 'Product Name' },
302 + { value: 'product_code', label: 'Product Code' },
303 + { value: 'quantity', label: 'Quantity' },
304 + ];
305 + }
306 + return this.fieldData;
307 + },
308 + getCurrentOrigSelectBbox() {
309 + if (!this.$refs.pdfImage || !this.imageWidth || !this.imageHeight) return null;
310 + const displayedWidth = this.$refs.pdfImage.clientWidth;
311 + const displayedHeight = this.$refs.pdfImage.clientHeight;
312 + const scaleX = this.imageWidth / displayedWidth;
313 + const scaleY = this.imageHeight / displayedHeight;
314 + return [
315 + Math.round(this.selectBox.x * scaleX),
316 + Math.round(this.selectBox.y * scaleY),
317 + Math.round((this.selectBox.x + this.selectBox.width) * scaleX),
318 + Math.round((this.selectBox.y + this.selectBox.height) * scaleY)
319 + ];
320 + },
321 + isBboxInTable(bbox) {
322 + if (!this.table_box || this.table_box.length !== 4) return false;
323 + return this.isBoxInside(bbox, this.table_box);
324 + },
325 + getColumnIndexForBbox(bbox) {
326 + // Determine which column this bbox belongs to using table header row (row_idx = 0)
327 + if (!Array.isArray(this.tableInfo) || this.tableInfo.length === 0) return null;
328 + const headerRow = this.tableInfo[0];
329 + if (!Array.isArray(headerRow)) return null;
330 +
331 + // Use x-overlap with header cells to pick the best matching column
332 + let bestCol = null;
333 + let bestOverlap = 0;
334 + const [bx1, by1, bx2, by2] = bbox;
335 + headerRow.forEach(cell => {
336 + if (!cell || !Array.isArray(cell.cell)) return;
337 + const [cx1, cy1, cx2, cy2] = cell.cell;
338 + const overlapX = Math.max(0, Math.min(bx2, cx2) - Math.max(bx1, cx1));
339 + const headerWidth = cx2 - cx1;
340 + const ratio = headerWidth > 0 ? overlapX / headerWidth : 0;
341 + if (ratio > bestOverlap) {
342 + bestOverlap = ratio;
343 + bestCol = cell.col_idx;
344 + }
345 + });
346 + if (bestOverlap > 0) return bestCol;
347 + return null;
348 + },
349 + populateTableColumn(field, colIdx) {
350 + // Fill tableForm values from table cells for the given column index, skipping header (row_idx 0)
351 + this.ensureTableFormSize();
352 + if (!Array.isArray(this.tableInfo)) return;
353 + for (let r = 1; r < this.tableInfo.length; r++) {
354 + const row = this.tableInfo[r];
355 + const dataRowIndex = r - 1; // because header is row 0
356 + if (!row || !Array.isArray(row)) continue;
357 + const cell = row.find(c => c && c.col_idx === colIdx);
358 + const value = cell ? (cell.text || (Array.isArray(cell.texts) ? cell.texts.join(' ') : '')) : '';
359 + if (!this.tableForm[dataRowIndex]) {
360 + this.$set(this.tableForm, dataRowIndex, { product_name: '', product_code: '', quantity: null });
361 + }
362 + if (field === 'quantity') {
363 + const num = value ? Number((value + '').replace(/[^0-9.-]/g, '')) : null;
364 + this.$set(this.tableForm[dataRowIndex], field, isNaN(num) ? null : num);
365 + } else {
366 + this.$set(this.tableForm[dataRowIndex], field, (value || '').toString());
367 + }
368 + }
369 + },
370 + applyTableMappingIfInTable(itemField, bbox) {
371 + if (!itemField) return false;
372 + if (!this.isBboxInTable(bbox)) return false;
373 + // Restrict to table fields only
374 + if (!['product_name','product_code','quantity'].includes(itemField)) return false;
375 + const colIdx = this.getColumnIndexForBbox(bbox);
376 + console.log(`Mapping field "${itemField}" to table column index:`, colIdx);
377 + if (colIdx === null || colIdx === undefined) return false;
378 + this.$set(this.tableColumnMapping, itemField, colIdx);
379 + this.populateTableColumn(itemField, colIdx);
380 + return true;
381 + },
226 startResize(e, index, handle) { 382 startResize(e, index, handle) {
227 e.stopPropagation(); 383 e.stopPropagation();
228 this.resizing = { 384 this.resizing = {
...@@ -505,6 +661,13 @@ ...@@ -505,6 +661,13 @@
505 this.activeIndex = null; 661 this.activeIndex = null;
506 this.selectingIndex = null; 662 this.selectingIndex = null;
507 663
664 + // Cleanup temporary focus box if any
665 + if (this.tempFocusIndex !== null) {
666 + const box = this.ocrData[this.tempFocusIndex];
667 + if (box) this.$set(box, 'isDeleted', true);
668 + this.tempFocusIndex = null;
669 + }
670 +
508 // Không ẩn nút xóa các box để tránh nhầm lẫn 671 // Không ẩn nút xóa các box để tránh nhầm lẫn
509 // Chỉ reset focus 672 // Chỉ reset focus
510 }, 673 },
...@@ -671,16 +834,34 @@ ...@@ -671,16 +834,34 @@
671 } 834 }
672 }); 835 });
673 if (item.isManual) { 836 if (item.isManual) {
674 - console.log('aaaaaaaaaa')
675 // Nếu là manual box, chuyển sang chế độ manual mapping 837 // Nếu là manual box, chuyển sang chế độ manual mapping
676 this.manualIndex = this.selectingIndex; 838 this.manualIndex = this.selectingIndex;
677 this.manualField = item.field || ""; 839 this.manualField = item.field || "";
840 + // Nếu trong vùng bảng, xử lý theo bảng trước
841 + const handled = this.applyTableMappingIfInTable(this.manualField, this.ocrData[this.manualIndex].bbox);
842 + if (!handled) {
678 this.applyManualMapping(); 843 this.applyManualMapping();
844 + } else {
845 + // reset selectingIndex if handled by table mapping
846 + this.selectingIndex = null;
847 + this.selectBox.show = false;
848 + this.selectBox.showDropdown = false;
849 + this.isMappingManually = false;
850 + this.manualField = "";
851 + this.manualIndex = null;
852 + }
679 return; 853 return;
680 } 854 }
681 855
682 // Xử lý box OCR (có thể chưa có field hoặc đã có field) 856 // Xử lý box OCR (có thể chưa có field hoặc đã có field)
683 if (item.field) { 857 if (item.field) {
858 + // Nếu trong vùng bảng, xử lý theo bảng
859 + const handled = this.applyTableMappingIfInTable(item.field, item.bbox);
860 + console.log(`applyMapping: field=${item.field}, handled by table=${handled}`);
861 + if (handled) {
862 + this.selectingIndex = null;
863 + return;
864 + }
684 865
685 // Cập nhật formData để hiển thị trong input 866 // Cập nhật formData để hiển thị trong input
686 this.formData[item.field] = item.text || ''; 867 this.formData[item.field] = item.text || '';
...@@ -706,6 +887,17 @@ ...@@ -706,6 +887,17 @@
706 887
707 const newBbox = this.ocrData[manualIndex].bbox; 888 const newBbox = this.ocrData[manualIndex].bbox;
708 889
890 + // Nếu trong vùng bảng, ưu tiên xử lý bảng và thoát
891 + const handled = this.applyTableMappingIfInTable(this.manualField, newBbox);
892 + if (handled) {
893 + this.isMappingManually = false;
894 + this.selectBox.show = false;
895 + this.selectBox.showDropdown = false;
896 + this.manualField = "";
897 + this.manualIndex = null;
898 + return;
899 + }
900 +
709 //console.log(`manual for field "${this.manualField}" at index ${manualIndex} with bbox:`, newBbox); 901 //console.log(`manual for field "${this.manualField}" at index ${manualIndex} with bbox:`, newBbox);
710 this.ocrData.forEach((box, i) => { 902 this.ocrData.forEach((box, i) => {
711 if (i !== manualIndex && box.field === this.ocrData[manualIndex].field) { 903 if (i !== manualIndex && box.field === this.ocrData[manualIndex].field) {
...@@ -885,6 +1077,17 @@ ...@@ -885,6 +1077,17 @@
885 this.fieldOptions = data.fieldOptions; 1077 this.fieldOptions = data.fieldOptions;
886 this.dataMapping = data.dataMapping; 1078 this.dataMapping = data.dataMapping;
887 this.is_template = data.is_template; 1079 this.is_template = data.is_template;
1080 + this.tableColumnMapping = data.tableColumnMapping || { product_name: null, product_code: null, quantity: null };
1081 +
1082 + // Prepare table form structure
1083 + this.ensureTableFormSize();
1084 + // If template provides column mapping, populate values now
1085 + Object.keys(this.tableColumnMapping || {}).forEach(k => {
1086 + const col = this.tableColumnMapping[k];
1087 + if (col !== null && col !== undefined) {
1088 + this.populateTableColumn(k, col);
1089 + }
1090 + });
888 //console.log('Loaded OCR data:', this.ocrData); 1091 //console.log('Loaded OCR data:', this.ocrData);
889 1092
890 } catch (error) { 1093 } catch (error) {
...@@ -896,6 +1099,7 @@ ...@@ -896,6 +1099,7 @@
896 let customer_name = null; 1099 let customer_name = null;
897 let customer_coords = null; 1100 let customer_coords = null;
898 let fields = []; 1101 let fields = [];
1102 + const table_columns = this.tableColumnMapping || {};
899 if (this.manualBoxData.customer_name) { 1103 if (this.manualBoxData.customer_name) {
900 // Lấy từ manualBoxData nếu có 1104 // Lấy từ manualBoxData nếu có
901 customer_name = this.manualBoxData.customer_name.text; 1105 customer_name = this.manualBoxData.customer_name.text;
...@@ -931,7 +1135,8 @@ ...@@ -931,7 +1135,8 @@
931 customer_name_text: customer_name || '', 1135 customer_name_text: customer_name || '',
932 template_name: this.formData.template_name || this.formData.customer_name, 1136 template_name: this.formData.template_name || this.formData.customer_name,
933 customer_name_xy: customer_coords || [], 1137 customer_name_xy: customer_coords || [],
934 - fields: fields 1138 + fields: fields,
1139 + table_columns: table_columns
935 }; 1140 };
936 1141
937 try { 1142 try {
......