tien_nemo

temp

Showing 1 changed file with 663 additions and 46 deletions
...@@ -21,8 +21,14 @@ ...@@ -21,8 +21,14 @@
21 cursor: pointer; 21 cursor: pointer;
22 } 22 }
23 .bbox.active { 23 .bbox.active {
24 - /*border-color: #2196F3;*/ 24 + border-color: #199601 !important;
25 - background-color: rgb(33 243 132 / 30%); 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); }
26 } 32 }
27 select { 33 select {
28 position: absolute; 34 position: absolute;
...@@ -54,8 +60,8 @@ ...@@ -54,8 +60,8 @@
54 60
55 </head> 61 </head>
56 <body> 62 <body>
63 +<meta name="csrf-token" content="{{ csrf_token() }}">
57 <div id="app"> 64 <div id="app">
58 -
59 <!-- Right: PDF viewer + select tool --> 65 <!-- Right: PDF viewer + select tool -->
60 <div class="right-panel" > 66 <div class="right-panel" >
61 <div class="pdf-container" ref="pdfContainer" 67 <div class="pdf-container" ref="pdfContainer"
...@@ -83,7 +89,7 @@ ...@@ -83,7 +89,7 @@
83 :class="{ active: index === activeIndex }" 89 :class="{ active: index === activeIndex }"
84 :data-field="item.field" 90 :data-field="item.field"
85 :style="getBoxStyle(item, index)" 91 :style="getBoxStyle(item, index)"
86 - @click="selectingIndex = index"> 92 + @click="onBoxClick(index)">
87 93
88 <button v-if="item.isManual && item.showDelete" 94 <button v-if="item.isManual && item.showDelete"
89 class="delete-btn" 95 class="delete-btn"
...@@ -92,13 +98,13 @@ ...@@ -92,13 +98,13 @@
92 98
93 99
94 <!-- Dropdown OCR --> 100 <!-- Dropdown OCR -->
95 - <select v-if="selectingIndex !== null" 101 + <select v-if="selectingIndex !== null && ocrData[selectingIndex]"
96 :style="getSelectStyle(ocrData[selectingIndex])" 102 :style="getSelectStyle(ocrData[selectingIndex])"
97 v-model="ocrData[selectingIndex].field" 103 v-model="ocrData[selectingIndex].field"
98 @change="applyMapping" 104 @change="applyMapping"
99 > 105 >
100 <option disabled value="">-- Chọn trường dữ liệu --</option> 106 <option disabled value="">-- Chọn trường dữ liệu --</option>
101 - <option v-for="field in fieldOptions" :value="field.value">{{ field.label }}</option> 107 + <option v-for="field in fieldData" :value="field.value">@{{ field.label }}</option>
102 </select> 108 </select>
103 109
104 <!-- Dropdown thủ công --> 110 <!-- Dropdown thủ công -->
...@@ -109,7 +115,7 @@ ...@@ -109,7 +115,7 @@
109 @click.stop 115 @click.stop
110 > 116 >
111 <option disabled value="">-- Chọn trường dữ liệu --</option> 117 <option disabled value="">-- Chọn trường dữ liệu --</option>
112 - <option v-for="field in fieldOptions" :value="field.value">{{ field.label }}</option> 118 + <option v-for="field in fieldData" :value="field.value">@{{ field.label }}</option>
113 </select> 119 </select>
114 </div> 120 </div>
115 </div> 121 </div>
...@@ -117,9 +123,15 @@ ...@@ -117,9 +123,15 @@
117 <!-- Left: Form inputs --> 123 <!-- Left: Form inputs -->
118 <div class="left-panel"> 124 <div class="left-panel">
119 <div v-for="field in fieldOptions" :key="field.value" class="form-group"> 125 <div v-for="field in fieldOptions" :key="field.value" class="form-group">
120 - <label>{{ field.label }}</label> 126 + <label>@{{ field.label }}</label>
121 - <input v-model="formData[field.value]" @focus="highlightField(field.value)"> 127 + <input v-model="formData[field.value]"
128 + @focus="highlightField(field.value)"
129 + @click="onInputClick(field.value)"
130 + @blur="onInputBlur(field.value)"
131 + :readonly="field.value === 'customer_name' && !hasCustomerNameXY"
132 + >
122 </div> 133 </div>
134 + <button @click="saveTemplate">💾Save</button>
123 </div> 135 </div>
124 136
125 </div> 137 </div>
...@@ -136,24 +148,232 @@ ...@@ -136,24 +148,232 @@
136 isSelecting: false, 148 isSelecting: false,
137 activeIndex: null, 149 activeIndex: null,
138 manualField: "", 150 manualField: "",
139 - formData: { export_date: "", order_code: "", customer: "", address: "", staff: "" }, 151 + formData: {},
140 - fieldOptions: [ 152 + manualBoxData: {},
141 - { value: "export_date", label: "Ngày xuất" }, 153 + fieldOptions: [],
142 - { value: "order_code", label: "Mã đơn hàng" }, 154 + customer_name_xy: '',
143 - { value: "customer", label: "Khách hàng" }, 155 + hasCustomerNameXY: false,
144 - { value: "address", label: "Địa chỉ" },
145 - { value: "staff", label: "Nhân viên" }
146 - ],
147 ocrData: [], 156 ocrData: [],
148 selectBox: { show: false, showDropdown: false, x: 0, y: 0, width: 0, height: 0, startX: 0, startY: 0 }, 157 selectBox: { show: false, showDropdown: false, x: 0, y: 0, width: 0, height: 0, startX: 0, startY: 0 },
149 manualIndex: null 158 manualIndex: null
150 } 159 }
151 }, 160 },
161 + created() {
162 + // Chỉ tạo formData cho các field cần mapping
163 + this.fieldOptions
164 + .filter(f => f.value !== "template_name")
165 + .forEach(f => {
166 + this.$set(this.formData, f.value, "");
167 + });
168 + },
152 mounted() { 169 mounted() {
153 - this.pdfImageUrl = "/public/image/data_picking_detail_1754967679.jpg"; // ảnh xuất từ Python 170 + this.loadOCRData();
154 - this.initData(); 171 +
172 + // Thêm event listener để xóa focus khi click ra ngoài
173 + document.addEventListener('click', (e) => {
174 + // Nếu click không phải vào input hoặc box
175 + if (!e.target.closest('.left-panel') && !e.target.closest('.bbox')) {
176 + this.removeAllFocus();
177 + }
178 + });
179 + },
180 + computed: {
181 + fieldData() {
182 + // Lọc bỏ template_name nếu không cần cho phần form mapping
183 + return this.fieldOptions.filter(f => f.value !== "template_name");
184 + }
155 }, 185 },
156 methods: { 186 methods: {
187 + removeManualBoxes() {
188 + this.ocrData = this.ocrData.filter(b => !b.isManual);
189 + this.activeIndex = null;
190 + },
191 + // Map field cho box (không set active, chỉ dùng để load data từ DB)
192 + mapFieldToBox(index, fieldName, text = null) {
193 + if (index == null) return;
194 +
195 + // Xóa tất cả box có fieldName trùng lặp, chỉ giữ lại box hiện tại
196 + this.ocrData = this.ocrData.filter((box, i) => {
197 + if (i === index) return true; // Giữ lại box hiện tại
198 + if (box.field === fieldName) {
199 + // Xóa box có field trùng lặp
200 + return false;
201 + }
202 + return true; // Giữ lại các box khác
203 + });
204 +
205 + // Cập nhật lại index sau khi filter
206 + const newIndex = this.ocrData.findIndex(box => box.bbox === this.ocrData[index]?.bbox);
207 + if (newIndex === -1) return;
208 +
209 + // Nếu box này từng gán field khác thì bỏ reset flag và tọa độ liên quan.
210 + const prev = this.ocrData[newIndex].field;
211 + if (prev && prev !== fieldName) {
212 + if (prev === 'customer_name') {
213 + this.hasCustomerNameXY = false;
214 + this.customer_name_xy = '';
215 + }
216 + this.ocrData[newIndex].field = null;
217 + this.ocrData[newIndex].field_xy = null;
218 + }
219 +
220 + // Gán field mới
221 + const bbox = this.ocrData[newIndex].bbox; // tọa độ OCR gốc [x1, y1, x2, y2]
222 +
223 + const x1 = bbox[0];
224 + const y1 = bbox[1];
225 + const w = bbox[2];
226 + const h = bbox[3];
227 +
228 + const xyStr = `${x1},${y1},${w},${h}`;
229 +
230 + this.ocrData[newIndex].field = fieldName;
231 + this.ocrData[newIndex].field_xy = xyStr;
232 +
233 + // Set text
234 + this.formData[fieldName] = (text !== null ? text : (this.ocrData[newIndex].text || '')).trim();
235 +
236 + // KHÔNG set active index (không focus)
237 +
238 + // Nếu là customer_name
239 + if (fieldName === 'customer_name') {
240 + this.hasCustomerNameXY = true;
241 + this.customer_name_xy = xyStr;
242 + }
243 + },
244 +
245 + assignFieldToBox(index, fieldName, text = null) {
246 + console.log(`Assigning field "${fieldName}" to box at index ${index} with text: "${text}"`);
247 + if (index == null) return;
248 +
249 + // 1. Xóa tất cả box có fieldName trùng lặp, chỉ giữ lại box hiện tại
250 + this.ocrData = this.ocrData.filter((box, i) => {
251 + if (i === index) return true; // Giữ lại box hiện tại
252 + if (box.field === fieldName) {
253 + // Xóa box có field trùng lặp
254 + return false;
255 + }
256 + return true; // Giữ lại các box khác
257 + });
258 +
259 + // 2. Cập nhật lại index sau khi filter
260 + const newIndex = this.ocrData.findIndex(box => box.bbox === this.ocrData[index]?.bbox);
261 + if (newIndex === -1) return;
262 +
263 + // 3. Nếu box này từng gán field khác thì bỏ
264 + const prev = this.ocrData[newIndex]?.field;
265 + if (prev && prev !== fieldName) {
266 + if (prev === 'customer_name') {
267 + this.hasCustomerNameXY = false;
268 + this.customer_name_xy = '';
269 + }
270 + this.ocrData[newIndex].field = null;
271 + this.ocrData[newIndex].field_xy = null;
272 + }
273 +
274 + // 4. Gán field mới
275 + const bbox = this.ocrData[newIndex].bbox; // tọa độ OCR gốc [x1, y1, x2, y2]
276 + console.log('bbox', bbox);
277 + const [x1, y1, w, h] = bbox;
278 + const xyStr = `${x1},${y1},${w},${h}`;
279 +
280 + this.ocrData[newIndex].field = fieldName;
281 + this.ocrData[newIndex].field_xy = xyStr;
282 +
283 + // 5. Gán text
284 + const finalText = text !== null ? text : (this.ocrData[newIndex].text || '');
285 + this.formData[fieldName] = finalText.trim();
286 + console.log(`formData ${fieldName}`, this.formData[fieldName]);
287 +
288 + // Nếu trong manualBoxData tồn tại field này hoặc muốn tạo lại manual box từ ocrData
289 + if (this.manualBoxData[fieldName]) {
290 + // Lấy tọa độ + text từ box hiện tại để tạo manual box mới
291 + this.createManualBoxFromDB(fieldName, this.ocrData[newIndex].bbox, finalText);
292 + // Cập nhật manualBoxData
293 + this.manualBoxData[fieldName] = {
294 + coords: this.ocrData[newIndex].bbox,
295 + text: finalText
296 + };
297 + }
298 +
299 + // 6. Active index
300 + this.activeIndex = newIndex;
301 + // 7. Nếu là customer_name
302 + if (fieldName === 'customer_name') {
303 + this.hasCustomerNameXY = true;
304 + this.customer_name_xy = xyStr;
305 + }
306 + },
307 +
308 +
309 + async saveTemplate() {
310 +
311 + let customer_name = null;
312 + let customer_coords = null;
313 + let fields = [];
314 + if (this.manualBoxData.customer_name) {
315 + // Lấy từ manualBoxData nếu có
316 + customer_name = this.manualBoxData.customer_name.text;
317 + customer_coords = this.manualBoxData.customer_name.coords.join(',');
318 + fields = this.manualBoxData;
319 + } else {
320 + // Không có → tìm trong ocrData
321 + const found = this.ocrData.find(item => item.field === 'customer_name');
322 + if (found) {
323 + customer_name = found.text;
324 + customer_coords = found.field_xy;
325 + }
326 +
327 + const fieldsByName = {};
328 + this.ocrData.forEach(box => {
329 + if (box.field && !box.isDeleted) {
330 + // chỉ giữ 1 bản ghi cuối cùng cho mỗi field (box gần nhất)
331 + fieldsByName[box.field] = {
332 + text: box.field,
333 + coords: box.field_xy || ''
334 + };
335 + }
336 + });
337 + // // convert to array
338 + fields = (fieldsByName);
339 + }
340 +
341 + console.log('fields:', fields);
342 +
343 + if (!customer_coords) {
344 + alert("Bạn phải map customer_name (quét/select) trước khi lưu.");
345 + return;
346 + }
347 +
348 + const payload = {
349 + customer_name_text: customer_name || '',
350 + template_name: this.formData.template_name || this.formData.customer_name,
351 + customer_name_xy: customer_coords || [],
352 + fields: fields
353 + };
354 + // console.log(fields);
355 + try {
356 + const res = await fetch('/ocr/save-template', {
357 + method: 'POST',
358 + headers: {
359 + 'Content-Type': 'application/json',
360 + 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
361 + },
362 + body: JSON.stringify(payload)
363 + });
364 + const json = await res.json();
365 + if (json.success) {
366 + alert(json.message);
367 + } else {
368 + alert('Save failed');
369 + }
370 + } catch (err) {
371 + console.error(err);
372 + alert('Save error');
373 + }
374 + },
375 +
376 +
157 deleteBox(index) { 377 deleteBox(index) {
158 const item = this.ocrData[index]; 378 const item = this.ocrData[index];
159 if (item.isManual) { 379 if (item.isManual) {
...@@ -181,20 +401,78 @@ ...@@ -181,20 +401,78 @@
181 } 401 }
182 }, 402 },
183 403
184 - async initData() { 404 +
185 - await this.loadOCRData();
186 - },
187 async loadOCRData() { 405 async loadOCRData() {
188 - const res = await fetch("/public/image/data_picking_detail_1754967679.json"); 406 +
189 - this.ocrData = await res.json(); 407 + try {
408 + const res = await fetch(`/ocr/data-list`);
409 + const data = await res.json();
410 +
411 + if (data.error) {
412 + console.error('Error loading data:', data.error);
413 + return;
414 + }
415 +
416 + this.ocrData = data.ocrData;
417 + this.pdfImageUrl = data.pdfImageUrl;
418 + this.formData = data.formData;
419 + this.fieldOptions = data.fieldOptions;
420 + this.dataMapping = data.dataMapping;
421 + // Đợi image load xong trước khi xử lý
422 + if (this.$refs.pdfImage && this.$refs.pdfImage.complete) {
423 + this.processLoadedData();
424 + } else {
425 + console.log('Image not loaded yet, waiting for onImageLoad');
426 + // Image sẽ được xử lý trong onImageLoad
427 + }
428 +
429 +
430 + } catch (error) {
431 + console.error('Error in loadOCRData:', error);
432 + }
433 + },
434 +
435 + // Xử lý data sau khi image đã load
436 + processLoadedData() {
437 + // Tự động map field cho các box OCR dựa trên formData đã load
438 + this.autoMapFieldsFromFormData();
439 + // Kiểm tra và sửa lại tọa độ của các box manual
440 +
441 + // Force re-render để đảm bảo các box được vẽ
442 + this.$nextTick(() => {
443 + this.$forceUpdate();
444 + });
445 + },
446 +
447 + // Tự động map field cho các box OCR dựa trên formData đã load từ DB
448 + autoMapFieldsFromFormData() {
449 + this.manualBoxData = {}; // reset
450 + Object.keys(this.dataMapping).forEach(fieldName => {
451 + const { text, coords } = this.dataMapping[fieldName];
452 + // Ví dụ: tạo box từ dữ liệu
453 + //this.createManualBoxFromDB(fieldName, coords, text);
454 +
455 + this.manualBoxData[fieldName] = { text, coords };
456 + });
457 +
190 }, 458 },
459 +
191 onImageLoad() { 460 onImageLoad() {
192 const img = this.$refs.pdfImage; 461 const img = this.$refs.pdfImage;
193 this.imageWidth = img.naturalWidth; 462 this.imageWidth = img.naturalWidth;
194 this.imageHeight = img.naturalHeight; 463 this.imageHeight = img.naturalHeight;
464 + // Nếu đã có data, xử lý ngay
465 + if (this.ocrData && this.ocrData.length > 0) {
466 + console.log('Image loaded and data exists, processing now');
467 + this.processLoadedData();
468 + } else {
469 + console.log('Image loaded but no data yet');
470 + }
195 }, 471 },
196 getBoxStyle(item, index) { 472 getBoxStyle(item, index) {
197 - if (!this.imageWidth || !this.imageHeight || !this.$refs.pdfImage) return {}; 473 + if (!this.imageWidth || !this.imageHeight || !this.$refs.pdfImage) {
474 + return {};
475 + }
198 476
199 const [x1, y1, x2, y2] = item.bbox; 477 const [x1, y1, x2, y2] = item.bbox;
200 const displayedWidth = this.$refs.pdfImage.clientWidth; 478 const displayedWidth = this.$refs.pdfImage.clientWidth;
...@@ -203,22 +481,60 @@ ...@@ -203,22 +481,60 @@
203 const scaleX = displayedWidth / this.imageWidth; 481 const scaleX = displayedWidth / this.imageWidth;
204 const scaleY = displayedHeight / this.imageHeight; 482 const scaleY = displayedHeight / this.imageHeight;
205 483
484 + const left = Math.round(x1 * scaleX);
485 + const top = Math.round(y1 * scaleY);
486 + const width = Math.round((x2 - x1) * scaleX);
487 + const height = Math.round((y2 - y1) * scaleY);
488 +
206 return { 489 return {
207 position: 'absolute', 490 position: 'absolute',
208 - left: `${Math.round(x1 * scaleX)}px`, 491 + left: `${left}px`,
209 - top: `${Math.round(y1 * scaleY)}px`, 492 + top: `${top}px`,
210 - width: `${Math.round((x2 - x1) * scaleX)}px`, 493 + width: `${width}px`,
211 - height: `${Math.round((y2 - y1) * scaleY)}px`, 494 + height: `${height}px`,
212 - border: item.hideBorder ? 'none' : '2px solid ' + (index === this.activeIndex ? '#199601' : '#ff5252'), 495 + border: item.hideBorder ? '2px solid #ccc' : '2px solid ' + (index === this.activeIndex ? '#199601' : '#ff5252'),
213 - //backgroundColor: item.hideBorder ? 'transparent' : (this.activeIndex === item.field ? 'rgba(33,150,243,0.3)' : 'rgba(255,82,82,0.2)'),
214 boxSizing: 'border-box', 496 boxSizing: 'border-box',
215 cursor: 'pointer', 497 cursor: 'pointer',
216 zIndex: item.isManual ? 30 : 10 498 zIndex: item.isManual ? 30 : 10
217 }; 499 };
218 }, 500 },
219 -
220 highlightField(field) { 501 highlightField(field) {
221 - // tìm box gần nhất match field này 502 + let coords, text;
503 + let isFromDB = false;
504 +
505 + // Kiểm tra xem field này có phải từ DB không
506 + if (this.dataMapping && this.dataMapping[field]) {
507 + isFromDB = true;
508 + coords = this.dataMapping[field].coords;
509 + text = this.dataMapping[field].text;
510 + console.log(`Using dataMapping for field "${field}":`, coords, text);
511 + } else {
512 + // Kiểm tra xem ocrData đã có box nào với field này chưa
513 + const existingBox = this.ocrData.find(b => b.field === field && !b.isDeleted);
514 + if (existingBox) {
515 + coords = existingBox.bbox;
516 + text = existingBox.text;
517 + console.log(`Using existing box for field "${field}":`, coords, text);
518 + } else if (this.manualBoxData[field]) {
519 + // Nếu không có trong ocrData, dùng manualBoxData (tọa độ mới)
520 + coords = this.manualBoxData[field].coords;
521 + text = this.manualBoxData[field].text;
522 + console.log(`Using manualBoxData (new coordinates) for field "${field}":`, coords, text);
523 + }
524 + }
525 +
526 + // Nếu có coords thì tạo hoặc hiển thị lại box
527 + if (coords) {
528 + if (isFromDB) {
529 + // Tạo box manual từ DB (không có nút xóa)
530 + this.createManualBoxFromDB(field, coords, text);
531 + } else {
532 + // Hiển thị lại box manual đã quét chọn (có nút xóa)
533 + this.showManualBox(field, coords, text);
534 + }
535 + }
536 +
537 + // Tìm lại index của box để set active
222 let idx = -1; 538 let idx = -1;
223 for (let i = this.ocrData.length - 1; i >= 0; i--) { 539 for (let i = this.ocrData.length - 1; i >= 0; i--) {
224 const it = this.ocrData[i]; 540 const it = this.ocrData[i];
...@@ -227,9 +543,131 @@ ...@@ -227,9 +543,131 @@
227 break; 543 break;
228 } 544 }
229 } 545 }
230 - this.activeIndex = idx; // nếu không tìm thấy thì = -1 546 +
547 + if (idx !== -1) {
548 + this.activeIndex = idx;
549 + this.scrollToBox(idx);
550 + // Reset selectingIndex để không hiển thị dropdown khi highlight từ input
551 + this.selectingIndex = null;
552 + } else {
553 + this.activeIndex = null;
554 + }
231 }, 555 },
232 556
557 +
558 + // Scroll đến box tương ứng
559 + scrollToBox(index) {
560 + if (!this.$refs.pdfContainer || index < 0 || index >= this.ocrData.length) return;
561 +
562 + const item = this.ocrData[index];
563 + if (!item || item.isDeleted) return;
564 +
565 + // Tính vị trí hiển thị của box
566 + const [x1, y1, x2, y2] = item.bbox;
567 + //const [x1, y1, x2, y2] = item.field_xy.split(',').map(Number);
568 + if (!this.imageWidth || !this.imageHeight || !this.$refs.pdfImage) return;
569 +
570 + const displayedWidth = this.$refs.pdfImage.clientWidth;
571 + const displayedHeight = this.$refs.pdfImage.clientHeight;
572 + const scaleX = displayedWidth / this.imageWidth;
573 + const scaleY = displayedHeight / this.imageHeight;
574 +
575 + const displayX = Math.round(x1 * scaleX);
576 + const displayY = Math.round(y1 * scaleY);
577 +
578 + // Scroll đến vị trí box
579 + const container = this.$refs.pdfContainer;
580 + const containerRect = container.getBoundingClientRect();
581 + const scrollTop = container.scrollTop;
582 + const scrollLeft = container.scrollLeft;
583 +
584 + // Tính vị trí scroll để box nằm ở giữa viewport
585 + const targetScrollTop = scrollTop + displayY - (containerRect.height / 2);
586 + const targetScrollLeft = scrollLeft + displayX - (containerRect.width / 2);
587 +
588 + container.scrollTo({
589 + top: Math.max(0, targetScrollTop),
590 + left: Math.max(0, targetScrollLeft),
591 + behavior: 'smooth'
592 + });
593 + },
594 +
595 +
596 +
597 + // Xử lý khi click vào input
598 + onInputClick(fieldName) {
599 + // Kiểm tra xem field này có data không
600 + const fieldValue = this.formData[fieldName];
601 + if (fieldValue && fieldValue.trim()) {
602 + // Nếu có data từ DB, highlight và focus vào box tương ứng
603 + this.highlightField(fieldName);
604 + }
605 + },
606 +
607 + // Xử lý khi click ra ngoài input (blur)
608 + onInputBlur(fieldName) {
609 + // Khi không focus vào input nào, xóa tất cả focus
610 + this.removeAllFocus();
611 + },
612 +
613 + // Xóa tất cả focus
614 + removeAllFocus() {
615 + // Reset active index (chỉ mất màu xanh, không xóa box)
616 + this.activeIndex = null;
617 +
618 + // Ẩ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)
619 + this.ocrData.forEach(item => {
620 + if (item.isManual && !item.showDelete) {
621 + // Box manual từ DB (không có nút xóa) - ẩn hoàn toàn
622 + item.isDeleted = true;
623 + }
624 + });
625 +
626 + // Đảm bảo tất cả box OCR đều hiển thị (chỉ ẩn border khi cần thiết)
627 + this.ocrData.forEach(item => {
628 + if (!item.isManual && item.hideBorder) {
629 + // Chỉ ẩn border cho box OCR nằm trong vùng manual
630 + item.hideBorder = true;
631 + }
632 + });
633 + },
634 +
635 + // Xử lý khi click vào box
636 + onBoxClick(index) {
637 + const item = this.ocrData[index];
638 +
639 + // Kiểm tra xem field này có phải từ DB không
640 + const isFromDB = this.dataMapping && this.dataMapping[item.field];
641 +
642 + // Kiểm tra xem data có được ghi đè không (so sánh với data gốc từ DB)
643 + const isDataOverridden = item.field && isFromDB &&
644 + this.formData[item.field] !== this.dataMapping[item.field].text;
645 +
646 + if (item.isManual) {
647 + // Manual box
648 + if (isFromDB && !isDataOverridden) {
649 + // Manual box từ DB chưa ghi đè, KHÔNG cho chọn option
650 + this.activeIndex = index;
651 + this.selectingIndex = null;
652 + } else {
653 + // Manual box từ DB có data ghi đè HOẶC manual box bình thường, CHO PHÉP chọn option
654 + this.selectingIndex = index;
655 + }
656 + } else if (item.field) {
657 + // Box OCR có field
658 + if (isFromDB && !isDataOverridden) {
659 + // Box có field từ DB chưa ghi đè, KHÔNG cho chọn option
660 + this.activeIndex = index;
661 + this.selectingIndex = null;
662 + } else {
663 + // Box có field từ DB đã ghi đè HOẶC field mới, CHO PHÉP chọn option
664 + this.selectingIndex = index;
665 + }
666 + } else {
667 + // Box OCR thông thường (chưa có field), cho phép hiển thị dropdown
668 + this.selectingIndex = index;
669 + }
670 + },
233 startSelect(e) { 671 startSelect(e) {
234 if (this.isMappingManually || e.button !== 0) return; 672 if (this.isMappingManually || e.button !== 0) return;
235 this.isSelecting = true; 673 this.isSelecting = true;
...@@ -271,7 +709,7 @@ ...@@ -271,7 +709,7 @@
271 const dispX2 = this.selectBox.x + this.selectBox.width; 709 const dispX2 = this.selectBox.x + this.selectBox.width;
272 const dispY2 = this.selectBox.y + this.selectBox.height; 710 const dispY2 = this.selectBox.y + this.selectBox.height;
273 711
274 - // scale: displayed -> original 712 + // scale: displayed -> original (sửa lại để chính xác hơn)
275 const displayedWidth = this.$refs.pdfImage.clientWidth; 713 const displayedWidth = this.$refs.pdfImage.clientWidth;
276 const displayedHeight = this.$refs.pdfImage.clientHeight; 714 const displayedHeight = this.$refs.pdfImage.clientHeight;
277 const scaleX = this.imageWidth / displayedWidth; 715 const scaleX = this.imageWidth / displayedWidth;
...@@ -309,67 +747,147 @@ ...@@ -309,67 +747,147 @@
309 747
310 e.stopPropagation(); 748 e.stopPropagation();
311 e.preventDefault(); 749 e.preventDefault();
750 +
312 } 751 }
313 , 752 ,
314 applyMapping() { 753 applyMapping() {
315 const item = this.ocrData[this.selectingIndex]; 754 const item = this.ocrData[this.selectingIndex];
755 + if (!item) return;
316 756
317 - if (item && item.isManual) { 757 + if (item.isManual) {
758 + // Nếu là manual box, chuyển sang chế độ manual mapping
318 this.manualIndex = this.selectingIndex; 759 this.manualIndex = this.selectingIndex;
319 this.manualField = item.field || ""; 760 this.manualField = item.field || "";
320 this.applyManualMapping(); 761 this.applyManualMapping();
321 return; 762 return;
322 } 763 }
323 764
765 + // Xử lý box OCR (có thể chưa có field hoặc đã có field)
324 if (item.field) { 766 if (item.field) {
325 - this.formData[item.field] = item.text; 767 + // Nếu box đã có field, cập nhật lại
768 + const oldField = item.field;
769 +
770 + // Cập nhật formData để hiển thị trong input
771 + this.formData[item.field] = item.text || '';
772 +
773 + // Cập nhật manualBoxData với tọa độ mới (box OCR không có nút xóa)
774 + this.manualBoxData[item.field] = {
775 + coords: item.bbox,
776 + text: item.text || '',
777 + isFromOCR: true // Đánh dấu đây là box OCR, không phải quét chọn
778 + };
779 +
780 + // Xóa dataMapping cũ để tránh focus về tọa độ cũ
781 + if (this.dataMapping && this.dataMapping[item.field]) {
782 + delete this.dataMapping[item.field];
783 + }
784 +
785 + // Set active index
326 this.activeIndex = this.selectingIndex; 786 this.activeIndex = this.selectingIndex;
787 +
788 + console.log(`Updated field "${item.field}" for box OCR at index ${this.selectingIndex} with new coordinates`);
327 } 789 }
790 +
328 this.selectingIndex = null; 791 this.selectingIndex = null;
329 }, 792 },
330 applyManualMapping() { 793 applyManualMapping() {
331 if (!this.manualField) return; 794 if (!this.manualField) return;
332 const manualIndex = this.manualIndex; 795 const manualIndex = this.manualIndex;
796 +
333 const newBbox = this.ocrData[manualIndex].bbox; 797 const newBbox = this.ocrData[manualIndex].bbox;
334 798
335 let combinedText = []; 799 let combinedText = [];
800 + let foundItems = [];
801 +
802 + // Tìm tất cả các box OCR nằm trong vùng manual
336 this.ocrData.forEach(item => { 803 this.ocrData.forEach(item => {
337 if (!item.isManual && this.isBoxInside(item.bbox, newBbox) && item.text.trim()) { 804 if (!item.isManual && this.isBoxInside(item.bbox, newBbox) && item.text.trim()) {
338 - const partial = this.getPartialText(item.text, item.bbox, newBbox); 805 + foundItems.push({
339 - if (partial) combinedText.push(partial); 806 + text: item.text,
340 - // combinedText.push(item.text.trim()); 807 + bbox: item.bbox,
808 + index: this.ocrData.indexOf(item)
809 + });
341 } 810 }
342 }); 811 });
343 812
813 + // Sắp xếp các item theo vị trí (từ trái sang phải, từ trên xuống dưới)
814 + foundItems.sort((a, b) => {
815 + // Ưu tiên theo Y trước (hàng), sau đó theo X (cột)
816 + if (Math.abs(a.bbox[1] - b.bbox[1]) < 20) { // Cùng hàng (tolerance 20px)
817 + return a.bbox[0] - b.bbox[0]; // Sắp xếp theo X
818 + }
819 + return a.bbox[1] - b.bbox[1]; // Sắp xếp theo Y
820 + });
821 +
822 + // Gộp text theo thứ tự đã sắp xếp
823 + foundItems.forEach(item => {
824 + combinedText.push(item.text.trim());
825 + });
826 +
344 const finalText = combinedText.join(" "); 827 const finalText = combinedText.join(" ");
828 + console.log('Combined text:', finalText);
829 +
830 + // Gán field và text cho box manual
831 + console.log(`Assigning manual field "${this.manualField}" to box at index ${manualIndex} with text: "${finalText}"`);
832 +
833 + // Gán field trực tiếp cho box manual
345 this.ocrData[manualIndex].field = this.manualField; 834 this.ocrData[manualIndex].field = this.manualField;
346 - this.formData[this.manualField] = finalText;
347 - this.activeIndex = manualIndex;
348 835
349 - console.log('manualField', this.manualField, this.manualIndex) 836 + // Cập nhật formData để hiển thị trong input
837 + this.formData[this.manualField] = finalText.trim();
838 +
839 + // Cập nhật manualBoxData (box quét chọn có nút xóa)
840 + this.manualBoxData[this.manualField] = {
841 + coords: newBbox,
842 + text: finalText.trim(),
843 + isFromOCR: false // Đánh dấu đây là box quét chọn, không phải OCR
844 + };
845 +
846 + // Xóa dataMapping cũ để tránh focus về tọa độ cũ
847 + if (this.dataMapping && this.dataMapping[this.manualField]) {
848 + delete this.dataMapping[this.manualField];
849 + }
850 +
350 // Reset trạng thái chọn 851 // Reset trạng thái chọn
351 this.isMappingManually = false; 852 this.isMappingManually = false;
352 this.selectBox.show = false; 853 this.selectBox.show = false;
353 this.selectBox.showDropdown = false; 854 this.selectBox.showDropdown = false;
354 - // this.manualField = ""; 855 + this.manualField = "";
355 - // this.manualIndex = null; 856 + this.manualIndex = null;
356 }, 857 },
357 858
358 isBoxInside(inner, outer) { 859 isBoxInside(inner, outer) {
359 - return !( 860 + // inner: bbox của OCR item [x1, y1, x2, y2]
861 + // outer: bbox của vùng manual [x1, y1, x2, y2]
862 +
863 + // Kiểm tra xem box OCR có nằm hoàn toàn trong vùng manual không
864 + const isFullyInside = (
865 + inner[0] >= outer[0] && // left edge
866 + inner[1] >= outer[1] && // top edge
867 + inner[2] <= outer[2] && // right edge
868 + inner[3] <= outer[3] // bottom edge
869 + );
870 +
871 + // Kiểm tra xem box OCR có giao nhau với vùng manual không
872 + const isOverlapping = !(
360 inner[2] < outer[0] || // box bên trái vùng chọn 873 inner[2] < outer[0] || // box bên trái vùng chọn
361 inner[0] > outer[2] || // box bên phải vùng chọn 874 inner[0] > outer[2] || // box bên phải vùng chọn
362 inner[3] < outer[1] || // box phía trên vùng chọn 875 inner[3] < outer[1] || // box phía trên vùng chọn
363 inner[1] > outer[3] // box phía dưới vùng chọn 876 inner[1] > outer[3] // box phía dưới vùng chọn
364 ); 877 );
878 +
879 + // Trả về true nếu box OCR nằm hoàn toàn trong hoặc giao nhau đáng kể
880 + return isFullyInside || isOverlapping;
365 }, 881 },
366 882
883 +
367 getPartialText(text, bbox, selectBbox) { 884 getPartialText(text, bbox, selectBbox) {
368 const [x1, y1, x2, y2] = bbox; 885 const [x1, y1, x2, y2] = bbox;
369 const [sx1, sy1, sx2, sy2] = selectBbox; 886 const [sx1, sy1, sx2, sy2] = selectBbox;
370 887
371 // Chiều rộng box OCR 888 // Chiều rộng box OCR
372 const boxWidth = x2 - x1; 889 const boxWidth = x2 - x1;
890 + const boxHeight = y2 - y1;
373 891
374 // Vị trí start và end tương đối trong text 892 // Vị trí start và end tương đối trong text
375 let startRatio = Math.max(0, (sx1 - x1) / boxWidth); 893 let startRatio = Math.max(0, (sx1 - x1) / boxWidth);
...@@ -395,6 +913,105 @@ ...@@ -395,6 +913,105 @@
395 top: `${Math.round(y2 * scaleY)}px`, 913 top: `${Math.round(y2 * scaleY)}px`,
396 zIndex: 9999 914 zIndex: 9999
397 }; 915 };
916 + },
917 +
918 + // Tạo box manual từ tọa độ trong DB
919 + createManualBoxFromDB(fieldName, coordinates, text) {
920 + if (!this.imageWidth || !this.imageHeight) {
921 + console.log('Cannot create manual box: Image not loaded');
922 + return;
923 + }
924 +
925 + // Parse coordinates từ string "x1,y1,x2,y2"
926 + let coords;
927 + if (typeof coordinates === 'string') {
928 + coords = coordinates.split(',').map(Number);
929 + } else if (Array.isArray(coordinates)) {
930 + coords = coordinates;
931 + } else {
932 + console.error('Invalid coordinates format:', coordinates);
933 + return;
934 + }
935 +
936 + const [x1, y1, x2, y2] = coords;
937 +
938 + // Kiểm tra tọa độ có hợp lệ không
939 + if (x1 >= 0 && y1 >= 0 && x2 > x1 && y2 > y1 &&
940 + x2 <= this.imageWidth && y2 <= this.imageHeight) {
941 +
942 + // Xóa box cũ có cùng fieldName trước khi tạo mới (chỉ xóa manual box)
943 + this.ocrData = this.ocrData.filter(box => !(box.field === fieldName && box.isManual));
944 +
945 + // Tạo box manual từ DB (không có nút xóa)
946 + const manualBox = {
947 + text: text || '',
948 + bbox: coords,
949 + field: fieldName,
950 + isManual: true,
951 + showDelete: false,
952 + isDeleted: false,
953 + hideBorder: true
954 + };
955 +
956 + this.ocrData.push(manualBox);
957 + // console.log('Manual box created successfully:', manualBox);
958 +
959 + // Force re-render
960 + this.$forceUpdate();
961 + } else {
962 + console.warn('Invalid coordinates for manual box:', coords);
963 + }
964 + },
965 +
966 + // Hiển thị lại box manual đã quét chọn (có nút xóa)
967 + showManualBox(fieldName, coordinates, text) {
968 + if (!this.imageWidth || !this.imageHeight) {
969 + console.log('Cannot show manual box: Image not loaded');
970 + return;
971 + }
972 +
973 + // Parse coordinates
974 + let coords;
975 + if (typeof coordinates === 'string') {
976 + coords = coordinates.split(',').map(Number);
977 + } else if (Array.isArray(coordinates)) {
978 + coords = coordinates;
979 + } else {
980 + console.error('Invalid coordinates format:', coordinates);
981 + return;
982 + }
983 +
984 + const [x1, y1, x2, y2] = coords;
985 +
986 + // Kiểm tra tọa độ có hợp lệ không
987 + if (x1 >= 0 && y1 >= 0 && x2 > x1 && y2 > y1 &&
988 + x2 <= this.imageWidth && y2 <= this.imageHeight) {
989 +
990 + // Xóa box cũ có cùng fieldName trước khi hiển thị lại
991 + this.ocrData = this.ocrData.filter(box => !(box.field === fieldName && box.isManual));
992 +
993 + // Kiểm tra xem đây có phải box quét chọn hay box OCR
994 + const isFromOCR = this.manualBoxData[fieldName] && this.manualBoxData[fieldName].isFromOCR;
995 +
996 + // 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)
997 + const manualBox = {
998 + text: text || '',
999 + bbox: coords,
1000 + field: fieldName,
1001 + isManual: true,
1002 + showDelete: !isFromOCR, // Chỉ hiển thị nút xóa nếu KHÔNG phải từ OCR
1003 + isDeleted: false,
1004 + hideBorder: false
1005 + };
1006 +
1007 + this.ocrData.push(manualBox);
1008 + console.log(`Manual box shown successfully: ${isFromOCR ? 'from OCR (no delete btn)' : 'from manual selection (with delete btn)'}`);
1009 +
1010 + // Force re-render
1011 + this.$forceUpdate();
1012 + } else {
1013 + console.warn('Invalid coordinates for manual box:', coords);
1014 + }
398 } 1015 }
399 1016
400 } 1017 }
......