feat: update backend OCR parser, web app, mobile app camera/preview UI, tests, and documentation with sample images

This commit is contained in:
Rafhan Mazaya Fathurrahman committed 2026-07-02 13:33:57 +07:00
1 parent aa3233e411
commit bdb3a49742
71 files changed
+3360 -764

No files matched your search

+206
View File
@@ -0,0 +1,206 @@
# Penjelasan Aturan Regex & Simulasi Proses Ekstraksi Dokumen DO PFM
Dokumen ini menjelaskan spesifikasi ekspresi reguler (Regex) yang digunakan dalam sistem PFM OCR serta menyimulasikan bagaimana teks mentah (*Raw Markdown*) hasil bacaan vLLM diuraikan langkah demi langkah melalui tiap lapisan deteksi hingga menjadi data JSON bersih di database.
---
## Bagian 1: Spesifikasi Aturan Regex Utama
Semua logika parsing ini didefinisikan dalam file utilitas pembantu [parser.ts](file:///d:/Client/Data%20Bisnis%20Solusi/app-pfm-ocr-v2/backend/pfm-web-app/src/utils/parser.ts).
### 1. Deteksi Nomor PO (Purchase Order)
* **Regex Ekstraksi**: `/(?:No\.?[ \t]*PO|PO[ \t]*No\.?)[ \t]*[:\-][ \t]*([A-Z0-9\-\/]+)/i`
* **Logika Pembersihan (`cleanAndFormatPO`)**:
* Menghapus label bising di depan (seperti `No. PO : `).
* **Pola 1 (Dengan Garis Miring)**: `PO/YY/NNNN+` (Tahun dipaksa menggunakan tahun berjalan saat ini untuk mengoreksi kesalahan OCR pada angka tahun).
* **Pola 2 (Angka Fused Tanpa Slash)**: Misalnya `PO1207000019` diubah paksa menjadi format `PO/26/7000019` menggunakan regex `/^(?:PO|P0|F0|...)(\d+)$/i`.
### 2. Deteksi Nomor SO (Sales Order) & DO (Delivery Order)
* **Regex SO**: `/(?:No\.?[ \t]*SO|SO[ \t]*No\.?)[ \t]*[:\-][ \t]*([A-Z0-9\-]+)/i`
* **Regex DO**: `/(?:No\.?[ \t]*DO|Delivery Order[ \t]*No|D\.O\.[ \t]*No|Order[ \t]*No)[ \t]*[:\- \t]*([A-Z0-9\-]+)/i`
* **Aturan Validasi**: Harus berupa angka numerik sepanjang **7 s.d. 12 digit** (biasanya diawali angka `16`).
### 3. Deteksi Tanggal
* **Regex Ekstraksi**: `/Tanggal\s*[:\-.]?\s*([^\n]{6,100})/i`
* **Regex Validasi Format (`cleanDateValue`)**: `/\b(\d{1,2})[ \t\-\/]*(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)([a-zA-Z]*)[ \t\-\/]*(\d{4})\b/i`
* Mengekstrak hari (1-31), nama bulan (Jan-Dec), dan tahun (4 digit).
* Menstandarkan nama bulan (kapital depan, sisanya huruf kecil, contoh: `Jun` $\rightarrow$ `June`).
### 4. Deteksi Plat Truk
* **Regex Deteksi Eksplisit (Dengan Label)**:
`/(?:No\.?\s*(?:Polisi|Pol|Kendaraan|Mobil|Truck|Pol\.?)|Plat(?:\s*No)?|Truck\s*No\.?)\s*[:\-.]?\s*\b([A-Z]{1,2})[ \t\-]*(\d{1,4})[ \t\-]*([A-Z]{1,3})\b/i`
* **Regex Pencarian Global (Tanpa Label)**: `/\b([A-Z]{1,2})[ \t\-]*(\d{1,4})[ \t\-]*([A-Z]{1,3})\b/gi`
* **Aturan Validasi**: Kode wilayah (huruf depan) harus terdaftar di array plat nomor valid Indonesia (seperti `B`, `D`, `AB`, `DK`, dll.).
---
## Bagian 2: Contoh Teks Mentah (Raw Markdown) Hasil OCR
Berikut adalah contoh teks Markdown hasil pembacaan vLLM (Langkah B) yang miringnya sudah diluruskan tetapi tata letaknya sedikit berantakan:
```markdown
PT. CHAROEN POKPHAND INDONESIA TBK
Kawasan Industri Ancol Barat No. 10
Jakarta Utara
===================================================
Kepada Yth: PT.PRIMAFOOD INTERNATIONAL
Order Untuk: PFM ANCOL 2 (02-ANC2)
Alamat Kirim: Jl. Ancol Barat VIII No. 1, Kel. Ancol, Kec. Pademangan
No. SO :
No. DO :
No. PO :
Tanggal :
1602987162
1602877112
PO/26/902871
30 Jun 2026
Plat No : B 9283 PQR
Nama Driver: Budi Santoso
| No. | Kode Item | Deskripsi Produk | Banyak | Jumlah |
|-----|-----------|------------------|--------|--------|
| 1 | 11310024 | Griller 0.8-0.9 | 10 | 450000 |
| 2 | 11640053 | Bone In Leg | 5 BAG | 250000 |
| 3 | 999999 | Item Sampah OCR | 2 | 10000 |
```
---
## Bagian 3: Simulasi Proses Deteksi Tiap Layer
Berikut adalah urutan bagaimana parser memproses Raw Markdown di atas:
### Layer 1: Deteksi Awal & Penyelarasan Layout Tergeser (*Shifted Layout*)
Pada Raw Markdown di atas, nilai label **SO, DO, PO, dan Tanggal** bergeser ke baris bawahnya karena tata letak visual kertas yang terpotong bergeser:
```text
No. SO :
No. DO :
1602987162
1602877112
```
* **Deteksi Pergeseran**: Parser mendeteksi bahwa nilai `No. SO` dan `No. DO` kosong (terbaca `"Not Found"` pada pencarian regex langsung).
* **Eksekusi Koreksi**: Logika realinyasi diaktifkan karena ditemukan pola 10 digit berturut-turut di baris setelah kumpulan label label kosong tersebut.
* **Hasil Realinyasi**:
* `noSO` diarahkan mengambil angka 10-digit pertama $\rightarrow$ `"1602987162"`
* `noDO` diarahkan mengambil angka 10-digit kedua $\rightarrow$ `"1602877112"`
* `noPO` diarahkan mengambil pola PO $\rightarrow$ `"PO/26/902871"`
* `tanggal` diarahkan mengambil pola tanggal terdekat $\rightarrow$ `"30 Jun 2026"`
### Layer 2: Sanitasi Nilai Ketat (`sanitizeParsedMetadata`)
Fungsi ini menyaring dan memvalidasi tipe data agar sesuai dengan standar database:
* **Tanggal**: Nilai `"30 Jun 2026"` dicocokkan dengan regex tanggal. Hari (`30`), bulan (`Jun`), dan tahun (`2026`) valid. Nama bulan distandarkan menjadi format bahasa Inggris penuh $\rightarrow$ `"30 June 2026"`.
* **No PO**: String `"PO/26/902871"` divalidasi. Karena tahun berjalan adalah `26`, formatnya valid $\rightarrow$ `"PO/26/902871"`.
* **No SO & DO**: `"1602987162"` dan `"1602877112"` divalidasi dengan regex `/^\d{7,12}$/`. Keduanya lolos karena berisi tepat 10 digit angka numerik.
* **Plat Nomor**: `"B 9283 PQR"` divalidasi. Huruf depan `"B"` dicocokkan ke array plat valid Indonesia. Terbukti valid dan distandarkan spasinya $\rightarrow$ `"B 9283 PQR"`.
### Layer 3: Pencocokan Toko Fuzzy Database (`resolveStoreFromText`)
Teks *"PFM ANCOL 2 (02-ANC2) Jl. Ancol Barat VIII No. 1..."* dianalisis untuk mencari toko aktif di database:
* **Tokenisasi**: Teks dipecah menjadi token-token kata: `['pfm', 'ancol', 'anc2', 'barat', 'pademangan']` (kata tidak penting seperti "untuk", "jalan", "no" dihapus).
* **Fuzzy Match**: Token-token ini dicocokkan dengan data tabel `store_master`.
* **Hasil**: Ditemukan baris database `nama_toko: "Prima Fresh Mart Ancol 2"` dan `alamat: "Jl. Ancol Barat VIII..."` memiliki kecocokan token tertinggi. Kolom metadata `orderUntuk` dan `alamat` diisi otomatis dengan data valid dari database tersebut.
### Layer 4: Parsing Tabel SKU & Pembersihan Item
Parser membedah baris-baris tabel HTML/Markdown:
* **Baris 1**: Kode `"11310024"` valid 8-digit. Deskripsi `"Griller 0.8-0.9"`. Kuantitas `"10"` (hanya angka murni).
* **Auto-Complete Unit**: Karena SKU `"11310024"` dikenali sebagai produk griller karung, satuannya ditambahkan otomatis $\rightarrow$ `"10 KRG"`.
* **Baris 2**: Kode `"11640053"` valid 8-digit. Deskripsi `"Bone In Leg"`. Kuantitas `"5 BAG"` valid.
* **Baris 3**: Kode `"999999"` **TIDAK VALID** karena panjangnya hanya 6 digit (bukan standar SKU 8-digit). Baris ini **dibuang otomatis** untuk mencegah sampah hasil pembacaan OCR masuk ke database.
SOLUSI: ketika SKU dicari itu tidak ada yang match, barulah coba baca nama barangnya dan lakukan fuzzy ke sku nama barang, untuk mendapatkan nomor SKUnya jadi saling crosscheck gitu, begitupun kalo SKU sudah dapat duluan cari nama barangnya dan cocokan ke sku nama barang untuk verified dua arah.
---
## Bagian 4: Hasil Data JSON Final yang Disimpan ke Database
Setelah melalui seluruh layer pemrosesan di atas, dokumen disimpan dengan objek metadata bersih seperti ini:
```json
{
"tanggal": "30 June 2026",
"noPO": "PO/26/902871",
"noSO": "1602987162",
"noDO": "1602877112",
"customerInfo": "PT.PRIMAFOOD INTERNATIONAL",
"orderUntuk": "Prima Fresh Mart Ancol 2",
"alamat": "Jl. Ancol Barat VIII No. 1, Kel. Ancol, Kec. Pademangan",
"platTruk": "B 9283 PQR",
"items": [
{
"kodeBarang": "11310024",
"namaBarang": "Griller 0.8-0.9",
"banyak": "10 KRG",
"jumlah": "450000"
},
{
"kodeBarang": "11640053",
"namaBarang": "Bone In Leg",
"banyak": "5 BAG",
"jumlah": "250000"
}
]
}
```
Kolom `ocr_items` database juga akan diisi secara rapi dengan 2 baris item SKU di atas.
---
## Bagian 5: Contoh Kasus Gagal Ekstraksi (SO, DO, PO Tidak Ditemukan)
Di bawah ini adalah simulasi kasus kegagalan ekstraksi karena kualitas gambar yang buruk atau label yang tidak terbaca sama sekali.
### 1. Contoh Raw Markdown yang Bermasalah (Bad OCR / Noise)
```markdown
PT. PRIMAFOOD INTERNATIONAL
Jl. Ancol Barat VIII No. 1
No. SO : 16O29B7162 <-- OCR salah membaca huruf 'O' (harusnya '0') dan 'B' (harusnya '8')
No. DO : 160287 <-- Terpotong secara visual, hanya terbaca 6 digit
No. PO : PQ/26/902 <-- Terbaca 'PQ' (harusnya 'PO') dan nomor belakang terpotong
Tanggal : 30-Hv-2026 <-- Nama bulan rusak parah ("Hv" bukan nama bulan yang valid)
```
### 2. Jalur Pemrosesan dan Penyebab Kegagalan
* **Kegagalan No SO**:
* *Teks OCR*: `"16O29B7162"` (mengandung huruf 'O' dan 'B').
* *Hasil Regex*: Regex lapis 1 mungkin menangkap teks ini, tetapi ketika masuk ke fungsi [sanitizeParsedMetadata()](file:///d:/Client/Data%20Bisnis%20Solusi/app-pfm-ocr-v2/backend/pfm-web-app/src/utils/parser.ts#L657-L664), ia dicocokkan dengan aturan `/^\d{7,12}$/` (wajib angka numerik murni).
* *Keputusan*: Karena mengandung huruf 'O' dan 'B', validasi **Gagal**. Nilai `noSO` diganti menjadi `"Not Found"`.
* **Kegagalan No DO**:
* *Teks OCR*: `"160287"` (hanya 6 digit).
* *Hasil Regex*: Lolos regex lapis 1, tetapi saat masuk ke [sanitizeParsedMetadata()](file:///d:/Client/Data%20Bisnis%20Solusi/app-pfm-ocr-v2/backend/pfm-web-app/src/utils/parser.ts#L666-L673), ia **Gagal** karena panjangnya kurang dari 7 digit.
* *Keputusan*: Nilai `noDO` diubah menjadi `"Not Found"`.
* **Kegagalan No PO**:
* *Teks OCR*: `"PQ/26/902"`.
* *Hasil Regex*: Lapis 1 gagal mencocokkan karena polanya diawali dengan huruf `"PQ"` (tidak sesuai dengan daftar awalan valid: `PO`, `P0`, `F0`, `O0`, `Q0`, dll.).
* *Keputusan*: Nilai `noPO` diganti menjadi `"Not Found"`.
SOLUSI: karena no SO atau DO itu numerik jadi O -> 0 dan B -> 8 dan seterusnya untuk angka yang hampir sama jangan malah dihapus menjadi not found, kecuali memang dari awal datanya ga ada atau tidak sesuainya jauh
* **Kegagalan Tanggal**:
* *Teks OCR*: `"30-Hv-2026"`.
* *Hasil Regex*: Pola `"Hv"` tidak cocok dengan pola regex bulan valid (`Jan|Feb|Mar|...`).
* *Keputusan*: Nilai `tanggal` diganti menjadi `"Not Found"`.
SOLUSI itu pola bukan Jan Feb tapi emang kata katanya full seperti January February May dll. lalu ketika misal terbaca DD-Hv-YYYY ini jangan langsung dihapus tapi ganti saja jadi bulan ini, atau misal yang hilang DD ganti jadi tanggal hari ini, begitupun tahun. ketika semua tidak terbaca barulah boleh not found
### 3. Hasil JSON Final yang Disimpan ke Database
Akibat kegagalan penyaringan di atas, dokumen tetap disimpan tetapi dengan nilai fallback `"Not Found"` pada field yang gagal divalidasi:
```json
{
"tanggal": "Not Found",
"noPO": "Not Found",
"noSO": "Not Found",
"noDO": "Not Found",
"customerInfo": "PT.PRIMAFOOD INTERNATIONAL",
"orderUntuk": "Prima Fresh Mart Ancol 1",
"alamat": "Jl. Ancol Barat VIII No. 1",
"platTruk": "",
"items": []
}
```
*Catatan: Nilai fallback `"Not Found"` ini akan ditampilkan sebagai warning berwarna merah di web UI agar divalidasi/diisi secara manual oleh user.*
+264
View File
@@ -0,0 +1,264 @@
# Dokumentasi Lengkap Alur Ekstraksi: Dari Teks Mentah (Raw Markdown) ke Data Bersih (JSON/Database)
Dokumen ini mendokumentasikan secara rinci bagaimana teks mentah (*Raw Markdown*) hasil pembacaan OCR & vLLM diolah langkah demi langkah oleh **Next.js API Gateway** (melalui utilitas [parser.ts](file:///d:/Client/Data%20Bisnis%20Solusi/app-pfm-ocr-v2/backend/pfm-web-app/src/utils/parser.ts)) hingga menjadi data terstruktur bersih (JSON) yang disimpan di database PostgreSQL.
---
## 1. Input: Teks Mentah (Raw Markdown) Hasil OCR & vLLM
Di bawah ini adalah contoh representatif dari teks Markdown mentah yang dihasilkan oleh pipeline **FastAPI & vLLM** (menggunakan model **PaddleOCR-VL-1.6**).
Teks ini memiliki beberapa masalah umum OCR:
* **Visual Distortions / Typo**: OCR salah mengenali karakter (misal `PQ` untuk `PO`, huruf `O`/`I`/`S` tercampur di dalam angka).
* **Shifted Layout**: Teks label terpisah dari nilainya karena tata letak baris yang bergeser.
* **Messy HTML Tables**: Struktur tabel mengandung tag HTML, spasi bising, baris kosong, dan data tanda air (*watermark*).
```markdown
PT. CHAROEN POKPHAND INDONESIA TBK
Kawasan Industri Modern Cikande, Serang, Banten
===================================================
Kepada Yth : PT.PRIMAFOOD INTERNATIONAL
Order Untuk : PFM ANCOL II (ANC-02)
Alamat Kirim : JL. ANCOL BARAT VIII NO. 1, PADEMANGAN, JAKARTA UTARA
Tanggal :
No. SO :
No. DO :
No. PO :
23 Jun 2026
16O19B032l
165998O277
PQ/26/000023082B
Plat Nomor : B 9427 UXT (ASLI)
Nama Driver : Ahmad Supriadi
<table border=1>
<tr>
<td>Kode Barang</td>
<td>Nama Barang</td>
<td>Banyak</td>
<td>Jumlah</td>
</tr>
<tr>
<td>11310014</td>
<td>Griller Size 2 (0.8-0.9) KG Frozen</td>
<td>2</td>
<td>40 PC</td>
</tr>
<tr>
<td>1131OO24</td>
<td>AYAM SIZE A PR FROZEN (O.9-1)KG/PC</td>
<td>1</td>
<td>20 PC</td>
</tr>
<tr>
<td>999999</td>
<td>Tanda Tangan Supir (Noise)</td>
<td></td>
<td></td>
</tr>
<tr>
<td colspan="2">Barang dikirim dalam keadaan baik</td>
<td>Jumlah</td>
<td>60 PC</td>
</tr>
</table>
Lembar 3 : Customer (ASLI)
```
---
## 2. Tahapan Pemrosesan & Ekstraksi Data (Step-by-Step)
Proses pemrosesan data mentah di atas dibagi menjadi 5 tahapan utama:
### Tahap 1: Ekstraksi Regex Awal (Lapis 1)
Sistem pertama kali mencoba mengekstrak field utama menggunakan pola Regex.
* **Ekstraksi Tanggal**:
* *Regex*: `/Tanggal\s*[:\-.]?\s*([^\n]{6,100})/i`
* *Hasil*: Karena baris di samping label `"Tanggal :"` kosong, pencarian langsung menghasilkan **`"Not Found"`**.
* **Ekstraksi No SO, DO, PO**:
* *Regex SO*: `/(?:No\.?[ \t]*SO|SO[ \t]*No\.?)[ \t]*[:\-][ \t]*([A-Z0-9\-]+)/i`
* *Regex DO*: `/(?:No\.?[ \t]*DO|Delivery Order[ \t]*No|D\.O\.[ \t]*No|Order[ \t]*No)[ \t]*[:\- \t]*([A-Z0-9\-]+)/i`
* *Regex PO*: `/(?:No\.?[ \t]*PO|PO[ \t]*No\.?)[ \t]*[:\-][ \t]*([A-Z0-9\-\/]+)/i`
* *Hasil*: Sama seperti tanggal, label `"No. SO"`, `"No. DO"`, dan `"No. PO"` tidak memiliki nilai di baris yang sama, sehingga ketiganya menghasilkan **`"Not Found"`**.
---
### Tahap 2: Penyelarasan Layout Tergeser (*Shifted Layout Re-alignment*)
Karena semua field bernilai `"Not Found"`, sistem mendeteksi adanya pergeseran layout. Program mengaktifkan algoritma pencarian baris terdekat (realinyasi indeks baris):
1. Program mengambil potongan baris tepat di bawah baris label yang kosong.
2. Ditemukan kumpulan baris berisi nilai:
* Baris 1: `23 Jun 2026`
* Baris 2: `16O19B032l`
* Baris 3: `165998O277`
* Baris 4: `PQ/26/000023082B`
3. **Pemetaan Realinyasi**:
* `tanggal` diarahkan mengambil baris yang memiliki pola tanggal $\rightarrow$ `"23 Jun 2026"`.
* `noSO` diarahkan mengambil angka 10-digit pertama (termasuk karakter typo) $\rightarrow$ `"16O19B032l"`.
* `noDO` diarahkan mengambil angka 10-digit kedua $\rightarrow$ `"165998O277"`.
* `noPO` diarahkan mengambil baris berpola PO $\rightarrow$ `"PQ/26/000023082B"`.
---
### Tahap 3: Sanitasi Data & Koreksi Karakter Typo (Lapis 2)
Setelah field berhasil dipetakan, program menjalankan fungsi `sanitizeParsedMetadata()` untuk membersihkan kesalahan visual OCR:
#### 1. Koreksi & Sanitasi Tanggal
* *Teks Awal*: `"23 Jun 2026"`
* *Proses*: Regex membagi string menjadi Hari (`23`), Bulan (`Jun`), dan Tahun (`2026`). Bulan `"Jun"` dicocokkan ke map kamus bulan untuk dikembangkan.
* *Hasil Bersih*: **`"23 June 2026"`**
#### 2. Koreksi & Sanitasi Nomor SO
* *Teks Awal*: `"16O19B032l"`
* *Proses*: Fungsi `correctVisualDigits()` memindai karakter non-angka dan menggantinya berdasarkan tabel kemiripan bentuk:
* Huruf `O` diganti menjadi angka `0` (indeks 2).
* Huruf `B` diganti menjadi angka `8` (indeks 5).
* Huruf `l` (L kecil) diganti menjadi angka `1` (indeks 9).
* *Hasil Bersih*: **`"1601980321"`** (Lolos validasi panjang 7-12 digit angka).
#### 3. Koreksi & Sanitasi Nomor DO
* *Teks Awal*: `"165998O277"`
* *Proses*: Huruf `O` diganti menjadi angka `0` (indeks 6).
* *Hasil Bersih*: **`"1659980277"`** (Lolos validasi panjang 7-12 digit angka).
#### 4. Koreksi & Sanitasi Nomor PO
* *Teks Awal*: `"PQ/26/000023082B"`
* *Proses*:
* Huruf `PQ` di depan diidentifikasi sebagai kesalahan baca dari label `PO`. Kode dibersihkan via `cleanAndFormatPO()`.
* Huruf `B` di bagian belakang angka diganti menjadi angka `8`.
* Tahun berjalan disesuaikan dengan segmentasi tahun dokumen (`26`).
* *Hasil Bersih*: **`"PO/26/0000230828"`**
#### 5. Koreksi Plat Nomor
* *Teks Awal*: `"B 9427 UXT (ASLI)"`
* *Proses*: Kata sampingan `(ASLI)` dibuang. Pola plat nomor Indonesia (`B 9427 UXT`) dicocokkan dengan kode prefix wilayah terdaftar.
* *Hasil Bersih*: **`"B 9427 UXT"`**
---
### Tahap 4: Fuzzy Store Resolution (Pencocokan Toko)
Teks customer *"PT.PRIMAFOOD INTERNATIONAL Order Untuk: PFM ANCOL II (ANC-02) Alamat Kirim: JL. ANCOL BARAT VIII..."* dianalisis:
1. **Tokenisasi**: Teks dipecah menjadi token-token kata: `['pfm', 'ancol', 'anc', '02', 'pademangan']` (kata seperti "jalan", "ke", "untuk" dibuang).
2. **Kalkulasi Irisan Token**:
Sistem mengompilasi token store dari database `store_master`.
* *Entri Toko*: `nama_toko: "PX HEAD OFFICE ANCOL"`, `alamat: "JL. ANCOL BARAT VIII NO. 1"`
* *Irisan*: Token `'ancol'`, `'barat'`, `'viii'`, dan `'pademangan'` memiliki kecocokan tinggi (skor $>0.8$).
3. **Hasil Resolusi**:
* `orderUntuk` $\rightarrow$ **`"PX HEAD OFFICE ANCOL"`** (Nama resmi dari DB).
* `alamat` $\rightarrow$ **`"JL. ANCOL BARAT VIII/1 KEL. ANCOL, KEC. PADEMANGAN JAKARTA UTARA, DKI JAKARTA"`** (Alamat resmi dari DB).
---
### Tahap 5: Parsing Tabel & Validasi Triple-Check Barang
Sistem mengurai tag `<table>` menjadi grid dua dimensi (baris & kolom), lalu menyaring baris:
```markdown
Baris 1: | 11310014 | Griller Size 2 (0.8-0.9) KG Frozen | 2 | 40 PC |
Baris 2: | 1131OO24 | AYAM SIZE A PR FROZEN (O.9-1)KG/PC | 1 | 20 PC |
Baris 3: | 999999 | Tanda Tangan Supir (Noise) | | |
```
#### Baris 1:
* **OCR SKU**: `"11310014"` (Valid 8-digit).
* **Fuzzy Database Matching (Triple-Check)**:
* Dilakukan pencarian di database `sku_master`. Kode SKU `"11310014"` terdaftar sebagai `'AYAM SIZE 2 FROZEN (0.8-0.9)KG(*)'`.
* Karena SKU cocok 100%, sistem mengunci data master ini (Skor = 1.0).
* **Standardisasi Satuan**:
* Kuantitas `"2"` diautocomplete dengan kemasan luar master (`jenis_outer: "Karung"`) $\rightarrow$ **`"2 KRG"`**.
* Total `"40 PC"` dikoreksi dengan kemasan dalam master (`standar_jumlah: "PC"`) $\rightarrow$ **`"40 PC"`**.
#### Baris 2:
* **OCR SKU**: `"1131OO24"` (Typo huruf `O`).
* **Koreksi Visual Digit**: Huruf `O` diubah menjadi angka `0` $\rightarrow$ `"11310024"`.
* **Fuzzy Database Matching (Triple-Check)**:
* SKU hasil koreksi `"11310024"` dicari di DB, terdaftar sebagai `'AYAM SIZE A FROZEN (0.9-1)KG/PC(*)'`.
* SKU cocok 100%. Data master dikunci.
* **Standardisasi Satuan**:
* Kuantitas `"1"` diautocomplete dengan kemasan luar master (`jenis_outer: "Karung"`) $\rightarrow$ **`"1 KRG"`**.
* Total `"20 PC"` distandardisasi menjadi **`"20 PC"`**.
#### Baris 3:
* **OCR SKU**: `"999999"`.
* **Validasi SKU**: Gagal karena panjang hanya 6 digit.
* **Hasil**: Baris dibuang sebagai noise.
#### Baris 4 (Baris Total):
* Terbaca `"Jumlah"` dan `"60 PC"`.
* Dideteksi sebagai watermark/kolom ringkasan melalui fungsi `isWatermark()`.
* **Hasil**: Baris dibuang.
---
## 3. Output: JSON Hasil Akhir & Struktur Database
Setelah melalui seluruh pipeline pemrosesan di atas, data yang dikembalikan ke aplikasi mobile Flutter dan disimpan ke database PostgreSQL berbentuk data bersih berikut:
### Objek JSON Metadata Akhir
```json
{
"id": "42",
"filePath": "1782870899198-sample_do.jpeg",
"createdAt": "2026-07-02T03:56:00.000Z",
"header": {
"tanggal": "23 June 2026",
"no_po": "PO/26/0000230828",
"no_so": "1601980321",
"no_do": "1659980277"
},
"shipment": {
"kepada_yth": "PT.PRIMAFOOD INTERNATIONAL",
"order_untuk": "PX HEAD OFFICE ANCOL",
"alamat": "JL. ANCOL BARAT VIII/1 KEL. ANCOL, KEC. PADEMANGAN JAKARTA UTARA, DKI JAKARTA",
"plat_truk": "B 9427 UXT",
"nama_driver": "Ahmad Supriadi",
"nama_penerima": ""
},
"items": [
{
"nomor_sku": "11310014",
"nama_barang": "AYAM SIZE 2 FROZEN (0.8-0.9)KG(*)",
"banyak": "2 KRG",
"jumlah": "40 PC"
},
{
"nomor_sku": "11310024",
"nama_barang": "AYAM SIZE A FROZEN (0.9-1)KG/PC(*)",
"banyak": "1 KRG",
"jumlah": "20 PC"
}
],
"latitude": -6.1284,
"longitude": 106.8427
}
```
### Penyimpanan ke Tabel PostgreSQL
#### 1. Baris Baru di Tabel `documents`
```sql
INSERT INTO documents (id, filename, upload_time, size, parsed, metadata, is_sample, file_hash, latitude, longitude)
VALUES (
42,
'1782870899198-sample_do.jpeg',
'2026-07-02 03:56:00',
151816,
true,
'{"header": {"tanggal": "23 June 2026", "no_po": "PO/26/0000230828", ...}, "shipment": {...}}',
false,
'd4a183...561fe99',
-6.1284,
106.8427
);
```
#### 2. Baris Baru di Tabel `ocr_items` (Bulk Insert)
```sql
INSERT INTO ocr_items (document_id, row_index, kode_barang_original, kode_barang, nama_barang, banyak_original, banyak, jumlah_original, jumlah, is_flagged, remark)
VALUES
(42, 0, '11310014', '11310014', 'AYAM SIZE 2 FROZEN (0.8-0.9)KG(*)', '2', '2 KRG', '40 PC', '40 PC', false, ''),
(42, 1, '1131OO24', '11310024', 'AYAM SIZE A FROZEN (0.9-1)KG/PC(*)', '1', '1 KRG', '20 PC', '20 PC', false, '');
```
+234
View File
@@ -0,0 +1,234 @@
# Dokumentasi Detail Alur Kerja PFM OCR (Prima Fresh Mart)
Dokumen ini memuat penjelasan lengkap mengenai alur kerja (workflow) pengolahan gambar dokumen Delivery Order (DO) hingga data tersimpan di database dan divisualisasikan pada dashboard web.
---
## Ringkasan Alur Kerja
Berikut adalah flowchart lengkap yang menggambarkan aliran data dan pemrosesan dari aplikasi mobile, gateway API, pipeline OCR, hingga penyimpanan database:
```mermaid
graph TD
%% Sisi Client (Flutter)
subgraph Client [1. Client-Side: Flutter App]
Start([Mulai: Supir Scan DO]) --> Capture[Ambil Foto via Camera API]
Capture --> GPS[Dapatkan Koordinat via Geolocator]
GPS --> BlurCheck{Cek Kualitas via BlurDetector}
BlurCheck -- Blur/Jelek --> Capture
BlurCheck -- Tajam/Baik --> Upload[Kirim Form Data via Dio HTTP Client]
end
%% Web Gateway (Nginx & Next.js)
subgraph Gateway [2. Gateway & API: Next.js & Nginx]
Upload --> Proxy[Nginx Proxy: Terima Request di Port 8000]
Proxy --> UploadAPI[Next.js Upload API: Simpan Gambar ke /uploads]
UploadAPI --> SQLInit[pg Driver: Simpan Dokumen ke DB - parsed=false]
SQLInit --> TriggerParse[Panggil API /api/parse & Kirim Base64]
end
%% Pipeline OCR (FastAPI)
subgraph OCR [3. Layout Parsing & OCR Pipeline: FastAPI]
TriggerParse --> OriCheck[Deteksi Orientasi via PP-LCNet]
OriCheck --> TiltCalc[Hitung Sudut Kemiringan via block_polygon_points]
TiltCalc --> TiltCheck{Kemiringan > 1.0 deg?}
TiltCheck -- Ya --> Unwarp[Lakukan Unwarping via UVDoc]
Unwarp --> ReRun[Proses Ulang Gambar Lurus]
TiltCheck -- Tidak/Sudah Lurus --> LayoutDet[Segmentasi Tata Letak via PP-DocLayoutV3]
ReRun --> LayoutDet
LayoutDet --> TextRec[Deteksi & Pengenalan Teks via PP-OCRv6_medium]
TextRec --> MarkdownGen[Susun Output Menjadi Teks Markdown]
end
%% VLM Server
subgraph VLM [4. Vision-Language Server: vLLM]
MarkdownGen --> QueryVLM[Query VLM Lokal via Port 8118]
QueryVLM --> ModelVLM[Proses Model PaddleOCR-VL-1.6-0.9B]
ModelVLM --> ResVLM[Kembalikan Hasil Teks Terstruktur]
end
%% Next.js Data Processing
subgraph Processing [5. Pengolahan Data & Validasi: Next.js]
ResVLM --> RegEx[Ekstraksi Regex PO/SO/DO via parseDOMetadata]
RegEx --> StoreMatch[Pencocokan Toko via resolveStoreFromText ke DB]
StoreMatch --> Sanitize[Sanitasi & Standardisasi Data via sanitizeParsedMetadata]
Sanitize --> SQLSave[Update Dokumen & ocr_items ke DB - parsed=true]
end
%% Polling & Selesai
subgraph Polling [6. Sinkronisasi Akhir: Flutter App]
SQLSave --> PollRequest[Dio Polling GET /documents tiap 2s]
PollRequest --> PollCheck{Status parsed = true?}
PollCheck -- Belum --> PollRequest
PollCheck -- Ya --> StateUpdate[Riverpod: Perbarui State UI & Tampilkan Data]
StateUpdate --> PrintPDF[Cetak Label PDF via pdf & printing lib]
PrintPDF --> End([Selesai])
end
```
---
## Tech Stack (Teknologi yang Digunakan)
Sistem ini dibangun menggunakan arsitektur modern yang membagi tugas antara aplikasi mobile client, API gateway, database, dan engine AI/OCR khusus:
### 1. Client-Side (Aplikasi Mobile)
* **Framework**: Flutter (Dart SDK `>=3.2.0`)
* **State Management & DI**: Riverpod (`flutter_riverpod`)
* **Routing**: GoRouter (`go_router`)
* **HTTP Client**: Dio (`dio`)
* **Local Storage / Cache**: Hive (`hive`, `hive_flutter`) & SharedPreferences
* **Sensors & Utilities**: Camera API (`camera`), Exif Data (`exif`), Location Service (`geolocator`), dan Blur Detection (`sensors_plus`)
* **PDF & Printing**: PDF Generator & Printing (`pdf`, `printing`)
### 2. Backend Gateway & Dashboard Web Admin
* **Framework**: Next.js 16 (React 19 & TypeScript)
* **Styling (CSS)**: TailwindCSS v4
* **Database Driver**: Node-postgres (`pg`)
* **Deployment/Runtime**: Node.js & Docker
### 3. Database
* **Engine**: PostgreSQL 15 (di-deploy via Docker)
### 4. Mesin AI & Pipeline OCR (Python & Docker)
* **Framework OCR**: PaddleX / PaddleOCR (menggunakan GPU CUDA 12.6 devel base image)
* **Model Deteksi Tata Letak**: `PP-DocLayoutV3` (untuk segmentasi blok teks/tabel)
* **Model Koreksi Kemiringan**: `PP-LCNet_x1_0_doc_ori` & `UVDoc` (untuk *auto-deskew* & *unwarping*)
* **Vision-Language Model (VLM)**: `PaddleOCR-VL-1.6-0.9B` (VLM 0.9B lokal)
* **VLM Inference Engine**: `vllm` server (berfungsi sebagai back-end inference manager untuk akselerasi GPU)
* **Python Web Framework**: FastAPI & Uvicorn (untuk expose `/layout-parsing` API endpoint)
### 5. Web Proxy / Server Gateway
* **Engine**: Nginx (Alpine) sebagai reverse proxy untuk routing port internal kontainer Next.js, FastAPI (Pipeline-API), dan vLLM.
---
## Interaksi Alur Kerja & Tech Stack
Di bawah ini adalah pemetaan langkah per langkah bagaimana komponen-komponen tech stack di atas berinteraksi secara real-time:
```mermaid
sequenceDiagram
autonumber
actor User as Pengguna
participant Mobile as Flutter App (Dio & Riverpod)
participant Nginx as Nginx Reverse Proxy
participant Gateway as Next.js Gateway (TypeScript & pg)
participant DB as PostgreSQL 15
participant Pipeline as FastAPI OCR (PaddleX & Python)
participant VLM as vLLM Server (PaddleOCR-VL-1.6)
User->>Mobile: Ambil foto DO
Note over Mobile: Kamera & GPS diakses via camera & geolocator.<br/>Pemeriksaan blur via BlurDetector.
Mobile->>Nginx: POST /api/v1/documents/upload (Base64/Form Data via Dio)
Nginx->>Gateway: Forward ke Next.js (port 3000)
Gateway->>DB: Simpan data awal dokumen (pg driver)
Gateway->>Pipeline: POST /layout-parsing (Base64)
Note over Pipeline: 1. Koreksi kemiringan (PP-LCNet & UVDoc)<br/>2. Deteksi layout tabel & teks (PP-DocLayoutV3)<br/>3. Ekstraksi teks (PP-OCRv6)<br/>4. Hasil akhir diubah ke Markdown.
Pipeline->>VLM: Query VLM lokal (port 8118 via vLLM)
VLM-->>Pipeline: Respons teks terstruktur dari model VL-1.6-0.9B
Pipeline-->>Gateway: Kembalikan Markdown lengkap & koordinat polygon
Note over Gateway: 1. parseDOMetadata() mengurai PO, SO, DO via Regex.<br/>2. resolveStoreFromText() mencocokkan toko ke DB.<br/>3. sanitizeParsedMetadata() menstandardisasi tanggal & plat.
Gateway->>DB: INSERT/UPDATE metadata & ocr_items (parsed = true)
loop Polling (tiap 2 detik)
Mobile->>Gateway: GET /documents (Status check via Dio)
Gateway-->>Mobile: Kirim data final (di-render via Riverpod)
end
User->>Mobile: Cetak Label
Note over Mobile: Generate & cetak dokumen PDF via pdf & printing lib.
```
### Penjelasan Alur Data & Teknologi:
1. **Pengambilan Gambar (Flutter Client)**: Pengguna mengambil foto Delivery Order (DO). Aplikasi Flutter mengakses kamera via **Camera API** dan mengambil GPS via **Geolocator**. Setelah lolos uji kualitas blur via `BlurDetector`, data dikirim menggunakan **Dio HTTP Client** via request `POST` Multipart Form ke Nginx pada port **`8000` (eksternal)** ke endpoint **`/api/v1/documents/upload`** (untuk supir/mobile) atau **`/api/upload`** (untuk dashboard web admin).
2. **Reverse Proxy (Nginx)**: Kontainer **`paddleocr-nginx`** mendengarkan di port **`80` (internal) / `8000` (eksternal)**. Nginx merutekan request dari client eksternal secara internal di dalam Docker network:
* Request ke `/api/*` diteruskan ke kontainer **`paddleocr-pfm-web-app`** pada port **`3000`**.
* Request langsung ke `/layout-parsing` diteruskan ke kontainer **`paddleocr-pipeline-api-v10`** pada port **`8090`**.
* Request langsung ke `/v1` diteruskan ke kontainer **`paddleocr-vllm-server`** pada port **`8118`**.
3. **Penyimpanan Dokumen Awal (Next.js Gateway)**: Kontainer **`paddleocr-pfm-web-app`** menerima request upload di port **`3000`**, menulis gambar fisik ke folder `/uploads`, dan menyimpannya ke database **`paddleocr-db`** pada port **`5432`** dengan query `INSERT` (status `parsed = false`) menggunakan driver **pg (node-postgres)**. Setelah itu, Next.js mengirimkan data Base64 via request `POST` secara internal ke kontainer **`paddleocr-pipeline-api-v10`** pada port **`8090`** ke endpoint **`/layout-parsing`**.
4. **Layout-Parsing (FastAPI OCR Pipeline)**: Kontainer **`paddleocr-pipeline-api-v10`** menerima request di port **`8090`** pada endpoint **`/layout-parsing`**. Menjalankan kode Python dengan model-model:
* Klasifikasi orientasi & unwarping kemiringan via **PP-LCNet_x1_0_doc_ori** & **UVDoc**.
* Deteksi tata letak via **PP-DocLayoutV3**.
* Pengenalan teks via **PP-OCRv6_medium** dengan akselerasi GPU CUDA 12.6.
* Hasil parsing ditransformasi menjadi teks Markdown mentah.
5. **Pemahaman Konten Dokumen (vLLM Server)**: FastAPI (`pipeline-api`) meneruskan request secara internal ke kontainer **`paddleocr-vllm-server`** di port **`8118`** pada endpoint **`/v1/chat/completions`** dengan menggunakan Vision-Language Model **`PaddleOCR-VL-1.6-0.9B`** untuk menganalisis teks secara kontekstual.
6. **Ekstraksi Metadata & Fuzzy Matching (Next.js)**: Next.js menerima respons balik, mengekstrak data PO, SO, DO via Regex (`parseDOMetadata`), mencocokkan toko penerima ke database PostgreSQL port **`5432`** (`resolveStoreFromText`), menstandardisasi format data (`sanitizeParsedMetadata`), lalu memperbarui status dokumen di PostgreSQL menjadi `parsed = true` dan menyimpan item barang ke tabel `ocr_items`.
7. **Sinkronisasi (Flutter Polling)**: Aplikasi Flutter mengirim request `GET` secara berkala setiap 2 detik menggunakan **Dio** ke port **`8000`** (eksternal) ke endpoint **`/api/documents`** (atau `/api/documents/[id]`) untuk memantau status parsing. Begitu status berubah menjadi `true`, **Riverpod** memperbarui UI secara real-time pada perangkat supir. Pengguna juga dapat mengekspor data ke format PDF menggunakan pustaka **pdf** & **printing** Flutter.
---
## Detail Langkah Per Langkah
### Langkah 1: Client-Side (Flutter App)
1. **Layar Konfirmasi**: Setelah memotret dokumen, pengguna diarahkan ke [image_preview_screen.dart](file:///d:/Client/Data%20Bisnis%20Solusi/app-pfm-ocr-v2/lib/features/camera/image_preview_screen.dart). Di layar ini, sistem mendeteksi apakah gambar blur menggunakan kelas `BlurDetector`.
2. **Unggah Dokumen**: Pengguna menekan tombol "Unggah Dokumen". Ini memicu aksi pada `PendingDocumentsNotifier` di [pending_documents_provider.dart](file:///d:/Client/Data%20Bisnis%20Solusi/app-pfm-ocr-v2/lib/features/documents/pending_documents_provider.dart):
* Status dokumen diatur ke `PendingDocumentStatus.uploading`.
* Koordinat GPS (latitude & longitude) diambil menggunakan `LocationService`.
* Gambar dikirim menggunakan form data ke backend (endpoint `/documents/upload`).
* Halaman otomatis beralih ke daftar dokumen (`/documents`) untuk memulai *background polling* setiap 2 detik.
---
### Langkah 2: Gateway Penerimaan (Next.js Upload)
Ada dua gerbang masuk upload:
1. **API Web** ([route.ts (api/upload)](file:///d:/Client/Data%20Bisnis%20Solusi/app-pfm-ocr-v2/backend/pfm-web-app/src/app/api/upload/route.ts)): Digunakan oleh dashboard website admin.
2. **API Mobile v1** ([route.ts (api/v1/documents/upload)](file:///d:/Client/Data%20Bisnis%20Solusi/app-pfm-ocr-v2/backend/pfm-web-app/src/app/api/v1/documents/upload/route.ts)): Digunakan oleh aplikasi Flutter.
**Mekanisme Penyimpanan:**
* Berkas disimpan ke folder `/uploads` dengan nama unik (`Date.now()-filename`).
* Koordinat GPS disimpan.
* Memicu pemanggilan API parse sinkron `/api/parse` dengan menyertakan nama file.
---
### Langkah 3: Mesin Ekstraksi & OCR Pipeline (`/api/parse`)
Di dalam file [route.ts (api/parse)](file:///d:/Client/Data%20Bisnis%20Solusi/app-pfm-ocr-v2/backend/pfm-web-app/src/app/api/parse/route.ts):
#### 1. Layout-Parsing (OCR Pipeline)
* Berkas dikonversi menjadi format Base64 dan diposkan ke API Pipeline (`http://localhost:7871/layout-parsing`).
* Pipeline membedah tata letak halaman (tabel, teks, paragraf) dan menghasilkan format terstruktur (Markdown) beserta informasi polygon koordinat tiap blok teks.
* Konfigurasi `markdown_ignore_labels` disetel kosong (`[]`) pada berkas `pipeline_config_vllm.yaml` agar elemen tata letak seperti `header`, `footer`, `footnote`, `number`, dan `aside_text` tidak di-filter oleh detektor layout. Hal ini memastikan informasi administratif penting (seperti nomor PO/SO/Tanggal) yang terletak di bagian atas atau bawah halaman tetap terekstrak secara akurat.
#### 2. Auto-Deskew & Unwarping (Rotasi Gambar Otomatis)
* Sudut kemiringan dihitung secara trigonometris melalui `calculateAverageTilt(data)` dengan melihat titik polygon `block_polygon_points`:
$$\text{Angle} = \arctan\left(\frac{dy}{dx}\right) \times \frac{180}{\pi}$$
* Jika kemiringan rata-rata melampaui **1.0 derajat**, backend otomatis memicu pemrosesan ulang dengan menyalakan flag:
* `useDocUnwarping: true`
* `useDocOrientationClassify: true`
* Hal ini menjamin pembacaan teks lurus dan akurat walaupun dokumen difoto miring.
#### 3. Ekstraksi Metadata Lapis 1 (Regex & Alinyemen Layout)
Fungsi `parseDOMetadata(markdownText)` mengurai konten Markdown hasil OCR:
* **Vendor & Customer**: Mencari baris teks berawalan `PT.` (Vendor dibedakan dengan Customer/Primafood).
* **Nomor Dokumen**:
* **No PO**: Pola `PO/YY/XXXX` (dipaksa tahun berjalan).
* **No SO & DO**: Pola numerik 10 digit (misal: dimulai dengan `16`).
* **Koreksi Layout Tergeser (*Shifted Layout*)**: Jika hasil OCR tidak menyejajarkan teks label dengan nilainya (misal, nilai nomor SO tergeser di bawah label DO), parser memindai baris terdekat untuk melakukan pemetaan ulang berdasarkan kedekatan indeks baris.
#### 4. Sanitasi Lapis 2 (Validator Sanity Check)
Fungsi `sanitizeParsedMetadata()` menjamin kepatuhan data sebelum disimpan ke database:
* **Tanggal**: Wajib berformat `"dd Month yyyy"` (Januari s.d. Desember Inggris). Jika tidak valid atau di luar rentang (2010 s.d. tahun depan), nilainya diganti `"Not Found"`.
* **SO & DO**: Wajib berupa digit angka murni sepanjang 7 s.d. 12 karakter.
* **Plat Nomor**: Wajib memiliki format plat nomor Indonesia yang valid (misalnya: `B 1234 ABC`).
#### 5. Pencocokan Toko (Fuzzy Database Match)
Fungsi `resolveStoreFromText()` mencocokkan teks penerima dengan data toko aktif di database (`store_master`):
* Kata umum seperti *"dan", "dki", "jalan", "raya", "kelurahan"* dihapus dari token OCR.
* Dilakukan penghitungan rasio irisan token (*token intersection score*) dengan seluruh kombinasi `nama_toko` dan `alamat` di database.
* Toko dengan kecocokan minimal 2 token dan skor tertinggi dipilih untuk mengisi kolom `orderUntuk` dan `alamat`.
#### 6. Parsing Tabel Item SKU
* Backend memindai tag `<table>` di Markdown.
* Kolom kode barang dibersihkan dan difilter ketat untuk hanya menerima format **8 digit angka murni** (sesuai standar SKU item internal).
* Jika kolom kuantitas (`banyak`) tidak terbaca satuannya (misal hanya angka `"10"`), backend melengkapinya berdasarkan pola SKU (contoh: SKU `11310024` dilengkapi menjadi `"10 KRG"`).
---
### Langkah 4: Penyimpanan Database (PostgreSQL)
1. Metadata dokumen disimpan atau diperbarui pada tabel `documents`:
* Kolom status parsing diubah menjadi `parsed = true`.
* Kolom JSONB `metadata` menyimpan struktur PO, SO, DO, Plat Truk, Tanggal, dll.
* Kolom JSONB `layout_parsing_result` menyimpan mentahan respons Pipeline OCR (termasuk data tilt).
* Kolom JSONB `processing_logs` menyimpan log terperinci tiap tahapan ekstraksi.
2. Item lama yang terkait dengan dokumen di tabel `ocr_items` dihapus untuk menghindari data usang, kemudian data item hasil ekstraksi baru dimasukkan kembali via bulk-insert.
+314
View File
@@ -0,0 +1,314 @@
# Analisis Detail Alur Kerja Aplikasi PFM OCR (Mobile & Backend API)
Dokumen ini memuat analisis mendalam mengenai seluruh alur kerja (*workflow*) sistem **Prima Fresh Mart (PFM) Scanner OCR v2**, dengan memfokuskan analisis secara eksklusif pada interaksi antara **Aplikasi Mobile Flutter** dan **Layanan Backend API** (API Gateway Next.js, Database PostgreSQL, Pipeline OCR FastAPI, vLLM Server, dan Server Klasifikasi YOLO).
Asumsi analisis ini didasarkan sepenuhnya pada peniadaan antarmuka web (*web interface/dashboard*), sehingga sistem diposisikan murni sebagai aplikasi mobile tersinkronisasi dengan backend OCR.
---
## 1. Arsitektur Aliran Data (Tanpa Web Interface)
Berikut adalah diagram alur kerja yang menggambarkan interaksi ujung-ke-ujung (*end-to-end*) antara aplikasi mobile Flutter dan komponen backend:
```mermaid
graph TD
%% Sisi Client (Flutter)
subgraph Client [Aplikasi Mobile Flutter]
Start([Mulai: Supir/Operator Scan DO]) --> Capture[Ambil Foto via Camera API]
Capture --> GPS[Ambil Geoposition via Geolocator]
GPS --> BlurCheck{Cek Kualitas via BlurDetector}
BlurCheck -- Blurry / Skor < 80 --> Capture
BlurCheck -- Tajam / Skor >= 80 --> Queue[Tambahkan ke Pending Queue]
Queue --> Upload[Kirim Multipart POST ke /api/v1/documents/upload]
%% Polling
Upload --> StartPoll[Mulai Polling GET /api/v1/documents]
StartPoll --> PollRequest{Cek Dokumen di List?}
PollRequest -- Belum Parsed / Timeout --> StartPoll
PollRequest -- Berhasil Parsed --> StateUpdate[Perbarui UI via Riverpod]
%% Editor & PDF
StateUpdate --> Editor[Manual Correction & Validation]
Editor --> SaveEdit[Kirim PUT ke /api/v1/documents/:id]
SaveEdit --> PrintPDF[Cetak Label / A4 PDF Natively]
PrintPDF --> End([Selesai])
end
%% Sisi Backend (Docker Stack)
subgraph Backend [Backend API & AI Engine]
%% Nginx & API Gateway
Upload --> Nginx[Nginx Router :8000]
Nginx --> UploadAPI[Next.js API Gateway :3000]
UploadAPI --> DBInsert[SQL: Simpan Dokumen Awal - parsed=false]
DBInsert --> TriggerParse[Panggil Internal POST /api/parse]
%% Pipeline API
TriggerParse --> Pipeline[FastAPI Pipeline API :8090]
Pipeline --> Deskew{Hitung Sudut Rata-rata - Tilt > 1.0 deg?}
Deskew -- Ya --> Unwarp[Unwarping via UVDoc & Re-Run]
Deskew -- Tidak --> LayoutDet[Layout Detection via PP-DocLayoutV3]
Unwarp --> LayoutDet
%% Text Extraction & LLM
LayoutDet --> TextOCR[PaddleOCR Text Recognition]
TextOCR --> FormatMD[Format Hasil ke Markdown]
FormatMD --> VllmProxy[vLLM Server: PaddleOCR-VL-1.6 :8118]
VllmProxy --> SendMD[Kembalikan Hasil Teks Terstruktur]
%% Next.js Post-processing
SendMD --> ParserTS[parseDOMetadata - Regex Extraction]
ParserTS --> StoreMatch[resolveStoreFromText - Fuzzy Toko]
StoreMatch --> TripleCheck[Intelligent Triple-Check SKU / Nama / Unit]
TripleCheck --> DBSave[SQL: UPDATE documents - parsed=true & ocr_items]
DBSave --> PollRequest
SaveEdit --> Nginx
end
```
---
## 2. Analisis Alur Kerja Sisi Client (Aplikasi Mobile Flutter)
Aplikasi mobile Flutter bertindak sebagai gerbang pertama pengumpulan data di lapangan oleh supir/operator pengiriman.
### A. Pengambilan Gambar & Geolokasi
1. Operator menggunakan modul kamera bawaan aplikasi Flutter untuk memotret kertas dokumen *Delivery Order* (DO).
2. Bersamaan dengan pengambilan foto, aplikasi mengakses layanan lokasi perangkat menggunakan paket `geolocator` untuk mengambil titik koordinat GPS (`latitude` dan `longitude`). Koordinat ini akan disematkan sebagai penanda lokasi pengunggahan (*geolocation tags*).
### B. Pendeteksian Blur (*Blur Detection*) secara Real-time
Untuk memastikan gambar yang dikirim memiliki kualitas yang layak dibaca oleh mesin OCR, aplikasi menjalankan deteksi ketajaman gambar via kelas [BlurDetector](file:///d:/Client/Data%20Bisnis%20Solusi/app-pfm-ocr-v2/lib/features/camera/blur_detector.dart) sebelum diunggah:
* **Isolate Execution**: Agar antarmuka (UI) tidak mengalami patah-patah (*stuttering*), pemrosesan gambar dipindahkan ke Dart Isolate terpisah menggunakan fungsi `compute`.
* **Preprocessing Gambar**:
1. Gambar asli diperkecil skalanya (*resized*) menjadi lebar **300 piksel** untuk mempercepat kalkulasi.
2. Dilakukan pemotongan area tengah sebesar **60%** (*center crop*), dengan tujuan mengabaikan latar belakang (meja, lantai) dan fokus sepenuhnya pada teks dokumen.
3. Mengaplikasikan *Gaussian Blur* dengan radius `1` untuk menyaring noise sensor kamera.
4. Mengonversi gambar hasil crop ke format skala abu-abu (*grayscale*).
* **Algoritma Varians Laplacian**:
* Aplikasi memindai piksel gambar dan menerapkan matriks konvolusi Laplacian 3x3 untuk mendeteksi tepi (*edge detection*):
$$\text{Laplacian} = 4 \times \text{Center} - \text{Left} - \text{Right} - \text{Top} - \text{Bottom}$$
* Menghitung nilai varians dari seluruh piksel Laplacian. Nilai varians yang tinggi mengindikasikan banyaknya perubahan intensitas piksel yang tajam (tepi teks jelas), sedangkan varians rendah berarti transisi piksel halus (gambar blur).
* **Penentuan Batas Kelolosan (Thresholding)**:
* Varians mentah dipetakan ke skala 0 hingga 100 dengan batas skor kelolosan sebesar **80.0** (setara dengan nilai varians mentah sebesar **400.0** dari batas maksimum **500.0**).
* Jika gambar di bawah skor 80.0, aplikasi memblokir proses unggah dan meminta pengguna mengambil ulang foto.
### C. Antrean Unggah & Sinkronisasi Latar Belakang (*Background Polling*)
Setelah lolos uji blur, berkas gambar didaftarkan ke state management Riverpod melalui kelas `PendingDocumentsNotifier`:
1. Status dokumen diatur menjadi `PendingDocumentStatus.uploading`.
2. Aplikasi mengirimkan berkas gambar bersama koordinat GPS menggunakan objek `FormData` melalui pustaka `Dio` HTTP Client ke endpoint backend:
`POST /api/v1/documents/upload`
3. Begitu API mengonfirmasi bahwa gambar sukses diunggah ke server (HTTP status `201`), aplikasi mengubah status menjadi `PendingDocumentStatus.processing`.
4. **Mekanisme Polling**:
Aplikasi Flutter memulai loop polling otomatis di latar belakang dengan melakukan request `GET /api/v1/documents` setiap **2 detik**.
* Aplikasi membandingkan ID dokumen yang sedang diunggah dengan daftar dokumen yang dikembalikan oleh server.
* Server hanya akan menampilkan dokumen pada daftar tersebut jika kolom `parsed` di database sudah bernilai `true`.
* Loop polling dibatasi maksimal **60 kali percobaan** (timeout 2 menit). Begitu terdeteksi di server, status diubah menjadi `PendingDocumentStatus.success` dan data terisi ke UI.
### D. Koreksi Manual & Standardisasi Sisi Editor
Jika terdapat karakter OCR yang tidak sempurna, operator dapat melakukan penyelarasan langsung pada aplikasi mobile (`editor_screen.dart`):
* Menyesuaikan nomor dokumen (PO, SO, DO) atau detail pengiriman (Plat truk, nama driver, nama penerima).
* Melakukan penyuntingan baris item barang (SKU, kuantitas/banyak, jumlah unit).
* Memberi tanda bendera (`is_flagged`) dan catatan khusus (`remark`) jika ada ketidaksesuaian barang.
* Menyimpan seluruh perubahan kembali ke database backend melalui endpoint:
`PUT /api/v1/documents/[id]`
### E. Ekspor PDF & Cetak Struk Nativel
Setelah data terverifikasi bersih, operator dapat langsung mencetak tanda terima fisik:
1. Pustaka `pdf` menyusun tata letak lembaran A4 yang memuat logo PFM, ringkasan PO/SO/DO, informasi pengiriman (Driver, plat nomor, alamat toko), tabel barang hasil OCR yang sudah bersih, serta kolom tanda tangan basah untuk supir dan penjaga toko.
2. Pustaka `printing` memicu manajer cetak bawaan sistem operasi (*Native OS Print Spooler*) untuk mencetak dokumen langsung ke printer thermal/portable via Bluetooth/Wi-Fi atau menyimpannya sebagai berkas PDF lokal.
---
## 3. Analisis Alur Kerja Sisi Backend (Gateway API & Database)
Backend bertindak sebagai API gateway dan orkestrator yang mengarahkan gambar dokumen ke unit pemrosesan AI, melakukan sanitasi, dan menyimpannya ke database relasional.
### A. Penerimaan Berkas & Inisialisasi Database (`/api/v1/documents/upload`)
Ketika file diunggah dari aplikasi mobile, Next.js API Gateway memprosesnya sebagai berikut:
1. Menyimpan berkas gambar fisik ke direktori `/uploads` dengan nama unik terstempel waktu (`Date.now() - safe_name.jpg`).
2. Menghitung nilai hash SHA-256 dari berkas biner untuk melacak integritas data dan mencegah duplikasi konten.
3. Melakukan insert record ke tabel `documents` dengan kolom `parsed = false` dan menyimpan metadata awal beserta koordinat GPS.
4. Memicu eksekusi parsing secara sinkron dan cepat dengan menembak API internal `/api/parse` membawa payload nama file.
### B. Alur Ekstraksi & Pipeline OCR (`/api/parse`)
Pada endpoint ini, Next.js meneruskan base64 gambar ke kontainer Python `paddleocr-pipeline-api` (FastAPI pada port `8090`) yang menjalankan pipeline pengolahan gambar berbasis GPU:
#### 1. Deteksi Sudut & Koreksi Kemiringan Otomatis (*Auto-Deskew & Unwarping*)
Dokumen yang difoto secara manual oleh supir sering kali miring atau melengkung.
* Sistem mendeteksi poligon koordinat blok teks menggunakan model **PP-LCNet_x1_0_doc_ori** untuk klasifikasi arah orientasi dokumen.
* Sistem menghitung kemiringan rata-rata teks secara trigonometri dari koordinat sudut poligon blok teks (`block_polygon_points`):
$$\text{Kemiringan Rata-rata} = \frac{1}{N} \sum_{i=1}^{N} \arctan\left(\frac{y_1 - y_0}{x_1 - x_0}\right) \times \frac{180}{\pi}$$
* Jika rata-rata kemiringan melampaui batas toleransi **1.0 derajat**, backend akan menolak hasil bacaan awal dan memicu pemrosesan ulang dengan mengaktifkan modul unwarping dokumen **UVDoc** (`useDocUnwarping: true`). Modul ini merentangkan kembali kertas yang melengkung/miring agar teks sejajar horizontal sebelum dibaca ulang.
#### 2. Segmentasi Tata Letak & Deteksi Teks
* Model **PP-DocLayoutV3** membagi area dokumen ke dalam beberapa tipe tata letak (Tabel, Paragraf, Judul, Header, Footer).
* Untuk memastikan informasi penting di bagian tepi kertas (seperti Nomor PO, SO, dan Tanggal pengiriman) tidak dibuang oleh OCR, parameter `markdown_ignore_labels` pada `pipeline_config_vllm.yaml` disetel kosong (`[]`).
* Teks dibaca per wilayah menggunakan engine **PaddleOCR v6** dan disusun kembali menjadi representasi teks terstruktur berformat Markdown mentah.
* Pipeline FastAPI mengirimkan teks Markdown tersebut ke kontainer **vLLM Server** (port `8118`) yang memuat model lokal **PaddleOCR-VL-1.6-0.9B** untuk interpretasi kontekstual lanjutan guna menyusun kembali kalimat-kalimat yang terpecah akibat deteksi tabel.
---
## 4. Pengolahan Data Lanjutan & Validasi Lapis Dua (Next.js)
Setelah menerima respons Markdown terstruktur dari pipeline OCR, Next.js Gateway melakukan serangkaian pembersihan data menggunakan aturan logika bisnis yang ketat:
### A. Ekstraksi Informasi Utama (Regex Matching)
Fungsi [parser.ts](file:///d:/Client/Data%20Bisnis%20Solusi/app-pfm-ocr-v2/backend/pfm-web-app/src/utils/parser.ts) menggunakan ekspresi reguler (Regex) untuk menarik data dari teks mentah:
* **No PO (Purchase Order)**:
* Mengekstrak kode PO menggunakan pola regex `/No\.?\s*PO\s*[:\-]\s*([A-Z0-9\-\/]+)/i`.
* Mengoreksi kegagalan bacaan OCR yang sering menyatukan karakter (*fused digits*) seperti `PO1207000019` dengan memisahkannya kembali menjadi format standar `PO/YY/NNNN+` (di mana `YY` disamakan dengan tahun pada tanggal dokumen).
* **No SO & DO**:
* Mencari pola 10 digit angka numerik (yang biasanya diawali angka `16` atau `10`).
* **Plat Nomor Kendaraan**:
* Mencari format plat nomor Indonesia dengan pola `[KODE_WILAYAH] [NOMOR_POLISI] [SERI_AKHIR]`. Huruf depan divalidasi ke daftar kode plat resmi (seperti `B`, `D`, `F`, `DK`, dll.).
* **Koreksi Layout Tergeser (*Shifted Layout Correction*)**:
* Kadang-kadang teks label ("Tanggal :", "No. SO :") tercetak terpisah jauh di atas nilainya karena tata letak visual bergeser. Jika parser mendeteksi nilai-nilai kunci kosong tetapi menemukan deretan angka 10-digit berurutan di baris-baris bawahnya, parser otomatis melakukan realinyasi indeks baris untuk menarik nilai ke kunci yang sesuai.
### B. Sanitasi & Standardisasi Data (`sanitizeParsedMetadata`)
Semua metadata yang berhasil ditarik disaring kembali untuk menjamin kepatuhan data sebelum masuk ke database:
* **Tanggal**: Harus lolos pola `"dd Month yyyy"` dengan nama bulan bahasa Inggris lengkap (contoh: `"02 February 2026"`). Jika OCR menghasilkan tanggal yang salah format atau di luar rentang wajar (2010 s.d. tahun depan), nilainya diganti menjadi `"Not Found"` agar memaksa operator melakukan koreksi manual, alih-alih melakukan *auto-fallback* ke tanggal hari ini yang berpotensi memalsukan tanggal dokumen asli.
* **SO & DO**: Wajib berupa digit angka murni sepanjang 7 s.d. 12 karakter. Sistem otomatis mengoreksi kesalahan visual karakter OCR yang umum terjadi (*visual digit correction*):
* Huruf `O` atau `o` di tengah angka $\rightarrow$ `0`
* Huruf `I`, `i`, `l`, atau simbol `|` di tengah angka $\rightarrow$ `1`
* Huruf `S` atau `s` $\rightarrow$ `5`
* Huruf `Z` atau `z` $\rightarrow$ `2`
* Huruf `B` atau `b` $\rightarrow$ `8`
* Angka bulan salah seperti `012` atau `112` diubah menjadi `02` (Februari).
### C. Pencocokan Toko Penerima (*Fuzzy Store Matching*)
Toko penerima ditentukan secara cerdas dari teks area customer (`customerInfo`):
1. Sistem mengambil teks mentah customer, melakukan normalisasi ke huruf kecil, membuang tanda baca, dan memotongnya menjadi token kata-kata unik (*tokenization*).
2. Sistem menyaring kata umum atau kata henti (*stop words*) seperti *"dan", "dki", "area", "yth", "kepada", "jalan", "raya", "kelurahan", "kecamatan"*.
3. Token tersebut kemudian diiriskan (*intersection*) dengan seluruh data toko yang terdaftar di database `store_master`.
4. Toko yang memiliki kecocokan minimal **2 token** dengan skor bobot tertinggi dipilih secara otomatis. Alamat resmi dan nama toko dari database kemudian disematkan pada kolom `orderUntuk` dan `alamat`.
### D. Sistem Validasi Barang Pintar (*Intelligent Triple-Check Matcher*)
Baris barang pada tabel DO rentan mengalami kesalahan pembacaan kode SKU maupun nama item. Next.js mengatasinya melalui mekanisme **Triple-Check Matcher**:
1. Sistem memuat seluruh master barang dari database `sku_master` (meliputi nomor SKU, nama item, standar jumlah inner, dan jenis kemasan outer).
2. Setiap baris barang hasil pembacaan tabel OCR dibandingkan dengan master menggunakan bobot gabungan (*Weighted Combined Score*):
* **Bobot SKU (50%)**: Skor kemiripan antara kode SKU hasil pembacaan dan SKU master menggunakan algoritma Levenshtein Distance. Jika ditemukan kecocokan SKU 100% presisi, sistem langsung menetapkan item tersebut sebagai kecocokan mutlak.
* **Bobot Nama Barang (40%)**: Skor kemiripan string nama barang hasil pembacaan dengan nama barang master.
* **Bobot Kemasan & Satuan (10%)**: Bonus kecocokan jika satuan kuantitas yang dibaca (misal: `KRG`, `BOX`, `BAG` sebagai kemasan luar/outer) atau harga unit (`PAC`, `PC`, `KG` sebagai kemasan dalam/inner) sesuai dengan definisi jenis kemasan item tersebut di database master.
3. **Penyelarasan & Pembersihan**:
* Jika skor kecocokan gabungan mencapai **$\ge$ 0.6**, baris tersebut dianggap valid. Sistem otomatis mengganti nomor SKU dan nama barang dengan data master yang resmi, serta menstandardisasi satuan kuantitasnya (contoh: kuantitas angka murni `"15"` diautocomplete menjadi `"15 KRG"` berdasarkan jenis kemasan luar item bebek/ayam).
* Jika skor kecocokan di bawah 0.6 tetapi nomor SKU yang terbaca merupakan angka valid **8 digit**, baris item tetap dipertahaman (untuk mendukung pengenalan SKU baru yang belum terdaftar di database master).
* Jika tidak ada kecocokan dan SKU tidak valid 8-digit, baris tersebut langsung **dibuang** karena diidentifikasi sebagai kebisingan layout tabel (*table noise* seperti garis pembatas tabel, tanda tangan supir, atau coretan kupon).
---
## 5. Alur Kerja Deteksi & Klasifikasi Produk (Scan Produk Tunggal)
Selain mengolah dokumen DO utuh, aplikasi mobile juga mendukung pemindaian produk/kemasan secara individual untuk memverifikasi kecocokan barang dan mendeteksi tanggal kedaluwarsa (*expiry date*).
```mermaid
graph TD
StartScan([Mulai: Scan Produk Tunggal]) --> SendImg[POST /api/scan-pfm dengan Base64 Gambar]
SendImg --> ForwardAPI[Next.js Meneruskan ke Python FastAPI :8120]
subgraph PyServer [FastAPI Classify-OCR Server]
ForwardAPI --> YOLO[1. YOLO Classification: Prediksi Kelas Produk]
YOLO --> Paddle[2. PaddleOCR: Deteksi & Ekstraksi Seluruh Baris Teks]
Paddle --> ExpExtract[3. extract_expired_date: Regex Pencarian Tanggal]
ExpExtract --> VisualDraw[4. Menggambar Bounding Box: Teal=Teks, Amber=EXP]
VisualDraw --> CropExp[5. Crop Region Tanggal Exp sebagai Base64]
CropExp --> SpotAPI[6. Call Layout-Parsing untuk Spotting Image]
end
PyServer --> ReturnData[Kembalikan Hasil OCR & Visualisasi ke Next.js]
ReturnData --> FuzzyMatch[Levenshtein Match: Hasil YOLO vs sku_master]
FuzzyMatch --> Top5[Ambil Top 5 Hasil dengan Skor Sim > 0.1]
Top5 --> RespondMobile[Kirim Hasil Akhir JSON ke Flutter Client]
RespondMobile --> EndScan([Selesai])
```
### A. Klasifikasi YOLO & Deteksi OCR
1. Aplikasi Flutter mengambil foto produk tunggal dan mengirimkannya ke endpoint Next.js `/api/scan-pfm`, yang diteruskan ke server FastAPI khusus (`classify_ocr_server.py` pada port `8120`).
2. **YOLO Classification**: Server memuat bobot model YOLO yang telah dilatih (`produk-pfm-classifier-26n-*e-*.pt`) untuk memprediksi kelas produk (nama produk) beserta probabilitas keyakinannya (*confidence score*).
3. **PaddleOCR Text Extraction**: Server menjalankan deteksi teks untuk mengambil seluruh baris tulisan yang tercetak pada label/kemasan produk.
### B. Ekstraksi & Pembersihan Tanggal Kedaluwarsa (*Expiry Date Spotting*)
Pendeteksian tanggal kedaluwarsa pada kemasan produk sangat menantang karena kualitas cetakan dot-matrix pada plastik kemasan sering kali buruk. Server FastAPI menggunakan serangkaian aturanRegex berlapis:
* **Pencarian Kata Kunci EXP**: Mencari kata kunci kedaluwarsa seperti `EXP`, `EXPIRY`, `TGL EXP`, `BBD`, `BEST BEFORE`, atau `BB`.
* **Regex Format Tanggal**:
* Pola tanggal rapat tanpa spasi/karakter pembatas (contoh: `05032027` via `DDMMYYYY_RE`).
* Pola tanggal dengan pembatas spasi, titik, hubung, atau garis miring (contoh: `05-03-2027` via `DD_MM_YYYY_RE`).
* Pola tanggal langsung setelah kata kunci (contoh: `BB05032027` via `BB_ATTACHED_DATE_RE`).
* **Pembersihan Noise Karakter Khusus**:
* Mengganti noise cetakan tanda kurung `()` atau karakter tidak sengaja `1)` menjadi angka `0`.
* Memperbaiki kesalahan interpretasi huruf/angka kembar seperti `B8`, `8B`, atau `88` menjadi `BB` (Best Before) jika diikuti oleh digit angka.
* Mengoreksi kegagalan bacaan bulan cetak rapat seperti `012` atau `112` di belakang tanggal menjadi bulan `02` (Februari) (contoh: `020122027` dikoreksi menjadi `02/02/2027`).
* **Visualisasi & Pemotongan Area (*Visual Cropping*)**:
* Server memotong poligon area koordinat teks kedaluwarsa yang berhasil diidentifikasi dan mengembalikannya sebagai gambar thumbnail terpotong berformat Base64 (`expired_date_crop_base64`). Hal ini berguna untuk validasi visual oleh pengguna pada aplikasi mobile.
* Menggambar kotak pembatas (*bounding box*) pada gambar produk: kotak **Teal** untuk tulisan biasa, dan kotak **Amber (oranye)** dengan label "EXP" untuk menyoroti letak teks tanggal kedaluwarsa.
### C. Fuzzy Similarity Matching (Next.js)
Begitu Next.js menerima hasil klasifikasi YOLO dan ekstraksi OCR:
1. Sistem menghitung kemiripan string menggunakan jarak Levenshtein antara prediksi nama produk YOLO dengan data `nama_item` pada tabel `sku_master`.
2. Mengembalikan **Top 5 kecocokan terbaik** yang memiliki skor kemiripan $>0.1$, dengan menetapkan item teratas sebagai `isBestMatch = true`.
3. Seluruh hasil klasifikasi produk, kecocokan master SKU, teks kedaluwarsa yang dideteksi, beserta visualisasi gambar kotak pembatas dikirimkan kembali ke aplikasi mobile Flutter.
---
## 6. Spesifikasi Skema Database PostgreSQL
Berikut adalah spesifikasi detail tabel-tabel PostgreSQL yang digunakan dalam workflow aplikasi mobile dan backend:
### A. Tabel `documents`
Menyimpan berkas informasi utama dokumen DO yang diunggah.
| Nama Kolom | Tipe Data | Deskripsi |
| :--- | :--- | :--- |
| `id` | SERIAL (PK) | ID unik dokumen (Auto-increment). |
| `filename` | VARCHAR(255) | Nama file unik yang tersimpan di `/uploads` (Unique). |
| `upload_time` | TIMESTAMP | Tanggal dan waktu pengunggahan dokumen. |
| `size` | INTEGER | Ukuran berkas gambar dalam satuan bytes. |
| `parsed` | BOOLEAN | Penanda status apakah dokumen telah selesai di-OCR (`true`/`false`). |
| `metadata` | JSONB | Menyimpan hasil ekstraksi terstruktur (PO, SO, DO, Tanggal, Plat nomor, Driver). |
| `layout_parsing_result` | JSONB | Menyimpan mentahan respons JSON dari kontainer Pipeline OCR FastAPI. |
| `is_sample` | BOOLEAN | Menandai apakah dokumen merupakan data contoh bawaan sistem (`true`/`false`). |
| `file_hash` | VARCHAR(64) | Hash SHA-256 berkas gambar untuk mencegah duplikasi data biner. |
| `processing_logs` | JSONB | Log jejak audit detail dari seluruh tahapan pemrosesan sistem OCR. |
| `latitude` | NUMERIC | Titik koordinat garis lintang (GPS) lokasi unggah dari perangkat mobile. |
| `longitude` | NUMERIC | Titik koordinat garis bujur (GPS) lokasi unggah dari perangkat mobile. |
### B. Tabel `ocr_items`
Menyimpan baris item produk yang tertera pada tabel dokumen DO. Terikat relasi *One-to-Many* terhadap tabel `documents`.
| Nama Kolom | Tipe Data | Deskripsi |
| :--- | :--- | :--- |
| `id` | SERIAL (PK) | ID unik baris item (Auto-increment). |
| `document_id` | INTEGER (FK) | Relasi kunci asing merujuk ke `documents(id)` dengan aksi `ON DELETE CASCADE`. |
| `row_index` | INTEGER | Nomor urut baris tabel item dalam satu dokumen. |
| `kode_barang_original` | VARCHAR(255) | Kode SKU barang asli hasil pembacaan awal mesin OCR sebelum dicocokkan. |
| `kode_barang` | VARCHAR(255) | Kode SKU barang hasil verifikasi akhir / standardisasi master. |
| `nama_barang` | VARCHAR(255) | Nama/deskripsi barang yang dikirim. |
| `banyak_original` | VARCHAR(255) | Kuantitas barang asli hasil pembacaan awal OCR. |
| `banyak` | VARCHAR(255) | Kuantitas barang hasil standardisasi (misalnya: `"15 KRG"`). |
| `jumlah_original` | VARCHAR(255) | Nilai total barang asli hasil pembacaan awal OCR. |
| `jumlah` | VARCHAR(255) | Nilai total barang hasil standardisasi (misalnya: `"450 PAC"`). |
| `is_flagged` | BOOLEAN | Penanda bendera untuk perhatian khusus / manual check (`true`/`false`). |
| `remark` | VARCHAR(1000) | Catatan koreksi atau alasan penandaan bendera pada baris item. |
*Catatan: Terdapat constraint `UNIQUE(document_id, row_index)` untuk mencegah tumpang tindih indeks baris.*
### C. Tabel `sku_master`
Menyimpan daftar master barang PFM untuk validasi Triple-Check.
| Nama Kolom | Tipe Data | Deskripsi |
| :--- | :--- | :--- |
| `id` | SERIAL (PK) | ID unik master SKU. |
| `no_sku` | VARCHAR(255) | Nomor SKU barang sepanjang 8 digit angka unik (Unique). |
| `nama_item` | VARCHAR(255) | Deskripsi nama resmi barang. |
| `standar_jumlah` | VARCHAR(255) | Satuan kemasan dalam/inner (contoh: `PAC`, `PC`, `KG`). |
| `jenis_outer` | VARCHAR(255) | Satuan kemasan luar/outer (contoh: `Karung`, `Box`, `Bag`). |
| `created_at` | TIMESTAMP | Tanggal pendaftaran SKU ke database. |
### D. Tabel `store_master`
Menyimpan data master toko Prima Fresh Mart untuk pencocokan tujuan pengiriman DO.
| Nama Kolom | Tipe Data | Deskripsi |
| :--- | :--- | :--- |
| `id` | SERIAL (PK) | ID unik master toko. |
| `nama_toko` | VARCHAR(255) | Nama resmi outlet/toko PFM. |
| `kode_toko` | VARCHAR(255) | Kode unik toko (Unique, contoh: `02-ANC2`). |
| `alamat` | TEXT | Alamat lengkap toko PFM untuk tujuan pengiriman. |
| `created_at` | TIMESTAMP | Tanggal pendaftaran toko ke database. |