refactor(schema): rename received_items and timesheets to their real names
RECEIVED_ITEMS -> DELIVERY_NOTE_ITEMS (55 rows): they are the lines on a delivery note; the old name described an event, not a thing. TIMESHEETS -> TIMESHEET_ENTRIES (1367 rows): this collection is the time log, not a timesheet. The name is freed for the uploaded-document collection created in Task 6. PocketBase stores relations by collection id, so every relation survived untouched; row counts asserted intact on both renames. Two call sites the plan missed: src/app/api/timesheets/import/route.ts and src/lib/ocr-result-tier.ts. Deliberately preserved the "timesheets" string literals in pb-types.ts and the import route — those are data_import_batches.batch_type select values, not collection names, and changing them would have violated the select constraint. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
a0d335b235
commit
0cdf385df2
12 changed files with 380 additions and 39 deletions
147
fuel_logs_duplicates_2026-09-10.csv
Normal file
147
fuel_logs_duplicates_2026-09-10.csv
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
date_logged,vehicle_id,vehicle_description,unit_type,vehicle_qr,litres,odo_reading,latest_consumption,project_code,site_name,record_id,batch_reference,recommended_action
|
||||
2026-09-01,03ofzpncx5t82z9,N1 Plant Excavator PC210,plant,1974,96,979,12,C26/09,,hemesmi423ny97p,,DELETE (orphan)
|
||||
2026-09-01,03ofzpncx5t82z9,N1 Plant Excavator PC210,plant,1974,96,979,12,C26/09,,8oiywnjdeqv35f4,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-01,3jhs3ogv7f8ruj6,TLB centre point,plant,1604,54,5374,4.15384615384615,C26/09,,7ln21ii1bpzy1ol,,DELETE (orphan)
|
||||
2026-09-01,3jhs3ogv7f8ruj6,TLB centre point,plant,1604,54,5374,4.15384615384615,C26/09,,z60njnwzuj5nvfj,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-01,6rsd1ugsd6q9msk,Roller ARD Yellow Group,plant,1625,58,12866,6.44444444444444,C26/09,,o36399oyj7gpo8g,,DELETE (orphan)
|
||||
2026-09-01,6rsd1ugsd6q9msk,Roller ARD Yellow Group,plant,1625,58,12866,6.44444444444444,C26/09,,mu1mnferoat7ght,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-01,9pdbdmmwtehi6x9,MERC WB02 Mega Plant,plant,1091,84,39933,4.66666666666667,C26/09,,06891ife3d610rg,,DELETE (orphan)
|
||||
2026-09-01,9pdbdmmwtehi6x9,MERC WB02 Mega Plant,plant,1091,84,39933,4.66666666666667,C26/09,,6p8vandlyomkwse,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-01,aau3dz4w8g2zled,N1 Plant Excavator CAT,plant,1983,123,7052,13.6666666666667,C26/09,,6kj8i22ryt4bz3m,,DELETE (orphan)
|
||||
2026-09-01,aau3dz4w8g2zled,N1 Plant Excavator CAT,plant,1983,123,7052,13.6666666666667,C26/09,,e0matzjgzmi2fgq,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-01,abkx0zcfst41i8y,Bell TLB04 Mega Plant,plant,1167,70,15721,4.375,C26/09,,4phyh051xettimi,,DELETE (orphan)
|
||||
2026-09-01,abkx0zcfst41i8y,Bell TLB04 Mega Plant,plant,1167,70,15721,4.375,C26/09,,vyajro6x40oh2rl,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-01,bxk0ezqopjpdo7u,Roller,plant,1007,44,2686,8.8,C26/09,,d7umdm54j8ns7k4,,DELETE (orphan)
|
||||
2026-09-01,bxk0ezqopjpdo7u,Roller,plant,1007,44,2686,8.8,C26/09,,6b5m8tf4eqsqb1g,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-01,jby0yvg41384wtx,Loader Jupiter 821C,plant,1113,156,8048,14.1818181818182,C26/09,,ti96xa169870d6h,,DELETE (orphan)
|
||||
2026-09-01,jby0yvg41384wtx,Loader Jupiter 821C,plant,1113,156,8048,14.1818181818182,C26/09,,f5k4ppn6ilnazyk,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-01,muppqwvwgh5e3m6,Isuzu Johannes,,1992,32,31156,10.65625,C26/09,,q77gmakv0f5g9hj,,DELETE (orphan)
|
||||
2026-09-01,muppqwvwgh5e3m6,Isuzu Johannes,,1992,32,31156,10.65625,C26/09,,dt3pzdn6ya3qwch,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-01,phcy5a8untqbwpm,Center Point TLB 473,plant,1516,79,4525,5.26666666666667,C26/09,,q0qp5gcmcoba6qp,,DELETE (orphan)
|
||||
2026-09-01,phcy5a8untqbwpm,Center Point TLB 473,plant,1516,79,4525,5.26666666666667,C26/09,,z7oix88n6bsdgdk,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-01,pkq2cjn4oata4t6,Grader Case Hire,plant,1590,133,932,19,C26/09,,n9k9j1obvt6hlvw,,DELETE (orphan)
|
||||
2026-09-01,pkq2cjn4oata4t6,Grader Case Hire,plant,1590,133,932,19,C26/09,,anohde1jtenmqvr,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-01,shjv24qw7p4sksa,Center Point TLB 515,plant,1959,101,4060,3.88461538461538,C26/09,,tmvtzgvq996pudh,,DELETE (orphan)
|
||||
2026-09-01,shjv24qw7p4sksa,Center Point TLB 515,plant,1959,101,4060,3.88461538461538,C26/09,,023eyqwcyllzziz,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-01,sw75yhfj05asoef,Isuzu HY58HVGP,,1934,38,15685,11.2894736842105,C26/09,,z6hdjgs9222jyuy,,DELETE (orphan)
|
||||
2026-09-01,sw75yhfj05asoef,Isuzu HY58HVGP,,1934,38,15685,11.2894736842105,C26/09,,c2cwjyy1olgssbg,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-01,un37e6yl59z8ol8,Center Point Excavator CAT,plant,1985,214,6067,14.2666666666667,C26/09,,s47pcli75n3fyp1,,DELETE (orphan)
|
||||
2026-09-01,un37e6yl59z8ol8,Center Point Excavator CAT,plant,1985,214,6067,14.2666666666667,C26/09,,hyrfeyej9c3v3ai,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-01,vac49hzww505q6t,Center Point Grader,plant,1626,137,5231,0.0583941605839416,C26/09,,b33027ha9r9gv5s,,DELETE (orphan)
|
||||
2026-09-01,vac49hzww505q6t,Center Point Grader,plant,1626,137,5231,0.0583941605839416,C26/09,,104gv6pe71mfx09,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-01,vnyq71ybkgcyeqo,Crushing Machine Jupiter,plant,1622,97,0,0,C26/09,,vjgsxb8r01oxnv0,,DELETE (orphan)
|
||||
2026-09-01,vnyq71ybkgcyeqo,Crushing Machine Jupiter,plant,1622,97,0,0,C26/09,,2c1aiwkleydljpf,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-01,vq4du1j6kf5jui3,N1 Plant Excavator PC200,plant,1572,127,8196,12.7,C26/09,,yci03ne1xbcdtvy,,DELETE (orphan)
|
||||
2026-09-01,vq4du1j6kf5jui3,N1 Plant Excavator PC200,plant,1572,127,8196,12.7,C26/09,,siw2fqo7uy01lqv,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-01,z5ta7k16siceztm,TLB centre point,plant,1586,41,8389,3.72727272727273,C26/09,,cdt2owko229yh2z,,DELETE (orphan)
|
||||
2026-09-01,z5ta7k16siceztm,TLB centre point,plant,1586,41,8389,3.72727272727273,C26/09,,s6u774l9r977byf,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-03,03ofzpncx5t82z9,N1 Plant Excavator PC210,plant,1974,141,988,15.6666666666667,C26/09,,wc9sxrskwlhnft8,,DELETE (orphan)
|
||||
2026-09-03,03ofzpncx5t82z9,N1 Plant Excavator PC210,plant,1974,141,988,15.6666666666667,C26/09,,pf9bo40b2mgi1xj,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-03,6rsd1ugsd6q9msk,Roller ARD Yellow Group,plant,1625,46,12871,9.2,C26/09,,0ieslcsz6zgfpq3,,DELETE (orphan)
|
||||
2026-09-03,6rsd1ugsd6q9msk,Roller ARD Yellow Group,plant,1625,46,12871,9.2,C26/09,,taof4t1e95vn1uw,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-03,9terh30urf4b3r5,Water Truck Go 2Guys,plant,1614,194,870880,0.402061855670103,C26/09,,h2mcu71rc9o5738,,DELETE (orphan)
|
||||
2026-09-03,9terh30urf4b3r5,Water Truck Go 2Guys,plant,1614,194,870880,0.402061855670103,C26/09,,uagxthxdnjnifv0,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-03,aau3dz4w8g2zled,N1 Plant Excavator CAT,plant,1983,155,7062,15.5,C26/09,,dnw32c0ds73wpgd,,DELETE (orphan)
|
||||
2026-09-03,aau3dz4w8g2zled,N1 Plant Excavator CAT,plant,1983,155,7062,15.5,C26/09,,9hnl28jj8echcxk,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-03,abkx0zcfst41i8y,Bell TLB04 Mega Plant,plant,1167,57,15733,4.75,C26/09,,337solxjygn5sde,,DELETE (orphan)
|
||||
2026-09-03,abkx0zcfst41i8y,Bell TLB04 Mega Plant,plant,1167,57,15733,4.75,C26/09,,b5k8ptg5g16ax4m,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-03,dfrv3e65t9q7aem,Isuzu MF88YZGP Justice,,1999,36,38977,10.1111111111111,C26/09,,7scnb0zagq5p114,,DELETE (orphan)
|
||||
2026-09-03,dfrv3e65t9q7aem,Isuzu MF88YZGP Justice,,1999,36,38977,10.1111111111111,C26/09,,d13t7nxobo7y711,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-03,jby0yvg41384wtx,Loader Jupiter 821C,plant,1113,214,8062,15.2857142857143,C26/09,,t9gfvosgy52quhs,,DELETE (orphan)
|
||||
2026-09-03,jby0yvg41384wtx,Loader Jupiter 821C,plant,1113,214,8062,15.2857142857143,C26/09,,atnkay0zikdpxa0,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-03,pkq2cjn4oata4t6,Grader Case Hire,plant,1590,308,946,22,C26/09,,a7nplkkg0j6m54u,,DELETE (orphan)
|
||||
2026-09-03,pkq2cjn4oata4t6,Grader Case Hire,plant,1590,308,946,22,C26/09,,x25iqpi7gczvlx0,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-03,qnig4z1earte98c,Isuzu MY85HGGP,,1936,50,8951,10,C26/09,,8pwp9xjfqzks94i,,DELETE (orphan)
|
||||
2026-09-03,qnig4z1earte98c,Isuzu MY85HGGP,,1936,50,8951,10,C26/09,,kl9zupnrtfchoel,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-03,vac49hzww505q6t,Center Point Grader,plant,1626,156,5241,0.0641025641025641,C26/09,,0pdgblrmhsqh88e,,DELETE (orphan)
|
||||
2026-09-03,vac49hzww505q6t,Center Point Grader,plant,1626,156,5241,0.0641025641025641,C26/09,,uryd5j1cc2q6lkd,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-03,vnyq71ybkgcyeqo,Crushing Machine Jupiter,plant,1622,147,0,0,C26/09,,e1c8af1g0pur2l5,,DELETE (orphan)
|
||||
2026-09-03,vnyq71ybkgcyeqo,Crushing Machine Jupiter,plant,1622,147,0,0,C26/09,,s7jw449xz7498sj,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-07,03ofzpncx5t82z9,N1 Plant Excavator PC210,plant,1974,81,995,11.5714285714286,C26/09,,4w5csmbbznlppce,,DELETE (orphan)
|
||||
2026-09-07,03ofzpncx5t82z9,N1 Plant Excavator PC210,plant,1974,81,995,11.5714285714286,C26/09,,pglg45tp8ivvqqb,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-07,3jhs3ogv7f8ruj6,TLB centre point,plant,1604,122,5399,4.88,C26/09,,5pcy2uwei818fsf,,DELETE (orphan)
|
||||
2026-09-07,3jhs3ogv7f8ruj6,TLB centre point,plant,1604,122,5399,4.88,C26/09,,evzmk4jr6ugi4du,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-07,6rsd1ugsd6q9msk,Roller ARD Yellow Group,plant,1625,35,12874,11.6666666666667,C26/09,,orxpy7y0pzj5pvx,,DELETE (orphan)
|
||||
2026-09-07,6rsd1ugsd6q9msk,Roller ARD Yellow Group,plant,1625,35,12874,11.6666666666667,C26/09,,i71znao8gzlrnv1,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-07,9pdbdmmwtehi6x9,MERC WB02 Mega Plant,plant,1091,116,39951,6.44444444444444,C26/09,,af05gxh7bsvavbz,,DELETE (orphan)
|
||||
2026-09-07,9pdbdmmwtehi6x9,MERC WB02 Mega Plant,plant,1091,116,39951,6.44444444444444,C26/09,,shlizhr6au4hwhr,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-07,aau3dz4w8g2zled,N1 Plant Excavator CAT,plant,1983,49,7067,9.8,C26/09,,zwtwuctx6sqbvly,,DELETE (orphan)
|
||||
2026-09-07,aau3dz4w8g2zled,N1 Plant Excavator CAT,plant,1983,49,7067,9.8,C26/09,,e0lzl3ddqgmzt17,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-07,abkx0zcfst41i8y,Bell TLB04 Mega Plant,plant,1167,47,15741,5.875,C26/09,,5h6zseiwqz6yio5,,DELETE (orphan)
|
||||
2026-09-07,abkx0zcfst41i8y,Bell TLB04 Mega Plant,plant,1167,47,15741,5.875,C26/09,,6uiwgnm5m4p6z47,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-07,b6s0ptwj7s6vgky,Tipper truck The Big AB,plant,1623,166,0,0,C26/09,,bvvaxb5t346vu5q,,DELETE (orphan)
|
||||
2026-09-07,b6s0ptwj7s6vgky,Tipper truck The Big AB,plant,1623,166,0,0,C26/09,,od3e950lh35psyq,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-07,b6s0ptwj7s6vgky,Tipper truck The Big AB,plant,1623,200,32623,0.00681082921845735,C26/09,,6u4g1emkg6y1csi,,DELETE (orphan)
|
||||
2026-09-07,b6s0ptwj7s6vgky,Tipper truck The Big AB,plant,1623,200,32623,0.00681082921845735,C26/09,,wj3iq0u14g61sr2,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-07,bxk0ezqopjpdo7u,Roller,plant,1007,84,2694,10.5,C26/09,,vc8cd44fhtmq7zj,,DELETE (orphan)
|
||||
2026-09-07,bxk0ezqopjpdo7u,Roller,plant,1007,84,2694,10.5,C26/09,,pezmylawmu5oxw8,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-07,e7yb7w7ug54yacf,Tipper truck,plant,1547,150,580613,0.973333333333333,C26/09,,tb1b9p2wgksyv8u,,DELETE (orphan)
|
||||
2026-09-07,e7yb7w7ug54yacf,Tipper truck,plant,1547,150,580613,0.973333333333333,C26/09,,alrygnvmbpr8hi3,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-07,jby0yvg41384wtx,Loader Jupiter 821C,plant,1113,113,8070,14.125,C26/09,,9yix9ryf7r1v51y,,DELETE (orphan)
|
||||
2026-09-07,jby0yvg41384wtx,Loader Jupiter 821C,plant,1113,113,8070,14.125,C26/09,,e41izw0bi7b4zvp,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-07,muppqwvwgh5e3m6,Isuzu Johannes,,1992,31,31481,10.4838709677419,C26/09,,pvn9t2g9frahvz4,,DELETE (orphan)
|
||||
2026-09-07,muppqwvwgh5e3m6,Isuzu Johannes,,1992,31,31481,10.4838709677419,C26/09,,a8cgeictqrx9n8s,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-07,n21wwm4ndbfvktw,Tipper truck Jupiter,plant,1570,118,3829,2.8780487804878,C26/09,,rxrr4mdmcs343n7,,DELETE (orphan)
|
||||
2026-09-07,n21wwm4ndbfvktw,Tipper truck Jupiter,plant,1570,118,3829,2.8780487804878,C26/09,,padsixufp084cnh,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-07,phcy5a8untqbwpm,Center Point TLB 473,plant,1516,118,4548,5.1304347826087,C26/09,,mft2g80hvzogfl2,,DELETE (orphan)
|
||||
2026-09-07,phcy5a8untqbwpm,Center Point TLB 473,plant,1516,118,4548,5.1304347826087,C26/09,,4dnfys22rbvx3k8,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-07,pkq2cjn4oata4t6,Grader Case Hire,plant,1590,89,952,14.8333333333333,C26/09,,e1o35zc7vbwyjoh,,DELETE (orphan)
|
||||
2026-09-07,pkq2cjn4oata4t6,Grader Case Hire,plant,1590,89,952,14.8333333333333,C26/09,,zjqspco93lajnt0,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-07,rkp6s3b40mvhqfr,Pick Up Truck 001,plant,1938,271,300,7.52777777777778,C26/09,,infpasex4akd61l,,DELETE (orphan)
|
||||
2026-09-07,rkp6s3b40mvhqfr,Pick Up Truck 001,plant,1938,271,300,7.52777777777778,C26/09,,vs5e0zkuggswc0a,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-07,shjv24qw7p4sksa,Center Point TLB 515,plant,1959,102,4083,4.43478260869565,C26/09,,azz2a3t2xwccxhd,,DELETE (orphan)
|
||||
2026-09-07,shjv24qw7p4sksa,Center Point TLB 515,plant,1959,102,4083,4.43478260869565,C26/09,,kesbj0zzyc2g6vd,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-07,sw75yhfj05asoef,Isuzu HY58HVGP,,1934,54,16300,11.3888888888889,C26/09,,oaykvw612kz1t57,,DELETE (orphan)
|
||||
2026-09-07,sw75yhfj05asoef,Isuzu HY58HVGP,,1934,54,16300,11.3888888888889,C26/09,,pw37zyfyjedegvd,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-07,un37e6yl59z8ol8,Center Point Excavator CAT,plant,1985,124,6080,9.53846153846154,C26/09,,wqss7j5vey6bds8,,DELETE (orphan)
|
||||
2026-09-07,un37e6yl59z8ol8,Center Point Excavator CAT,plant,1985,124,6080,9.53846153846154,C26/09,,r5cd5nikfrleucy,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-07,vac49hzww505q6t,Center Point Grader,plant,1626,105,5249,0.0761904761904762,C26/09,,3bo6qn2bp4tmij2,,DELETE (orphan)
|
||||
2026-09-07,vac49hzww505q6t,Center Point Grader,plant,1626,105,5249,0.0761904761904762,C26/09,,8t1o0ydx7mfo5hn,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-07,vq4du1j6kf5jui3,N1 Plant Excavator PC200,plant,1572,319,8219,13.8695652173913,C26/09,,w0k3wzxgk818j9c,,DELETE (orphan)
|
||||
2026-09-07,vq4du1j6kf5jui3,N1 Plant Excavator PC200,plant,1572,319,8219,13.8695652173913,C26/09,,zp9zqonrwfw6iwc,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-07,z5ta7k16siceztm,TLB centre point,plant,1586,106,8408,5.57894736842105,C26/09,,xyjpd57h9gyhcpn,,DELETE (orphan)
|
||||
2026-09-07,z5ta7k16siceztm,TLB centre point,plant,1586,106,8408,5.57894736842105,C26/09,,okv117c28fi8eja,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-09,03ofzpncx5t82z9,N1 Plant Excavator PC210,plant,1974,112,1006,10.1818181818182,C26/09,,lngcy4x8y1txcmd,,DELETE (orphan)
|
||||
2026-09-09,03ofzpncx5t82z9,N1 Plant Excavator PC210,plant,1974,112,1006,10.1818181818182,C26/09,,sdz1n4pt1usjdsp,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-09,3jhs3ogv7f8ruj6,TLB centre point,plant,1604,59,5411,4.91666666666667,C26/09,,g6fpxjttm2qd3g2,,DELETE (orphan)
|
||||
2026-09-09,3jhs3ogv7f8ruj6,TLB centre point,plant,1604,59,5411,4.91666666666667,C26/09,,8sdh9gpgcagzbxl,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-09,68n3smofgkrwq93,Can The Hills,,1611,42,0,0,C26/09,,snle25g7jbx7gly,,DELETE (orphan)
|
||||
2026-09-09,68n3smofgkrwq93,Can The Hills,,1611,42,0,0,C26/09,,cyofbtfuzdx89em,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-09,6rsd1ugsd6q9msk,Roller ARD Yellow Group,plant,1625,38,12879,7.6,C26/09,,so29f0s9bqps82w,,DELETE (orphan)
|
||||
2026-09-09,6rsd1ugsd6q9msk,Roller ARD Yellow Group,plant,1625,38,12879,7.6,C26/09,,tpvswbwvmtjqhge,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-09,76tm3k0o32jneto,Tipper Truck,plant,1589,326,290,0.0950920245398773,C26/09,,k0l2jl6olrwvfh5,,DELETE (orphan)
|
||||
2026-09-09,76tm3k0o32jneto,Tipper Truck,plant,1589,326,290,0.0950920245398773,C26/09,,vxqbj9feakucdcc,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-09,9terh30urf4b3r5,Water Truck Go 2Guys,plant,1614,164,870912,0.195121951219512,C26/09,,crff7d5h2l51xso,,DELETE (orphan)
|
||||
2026-09-09,9terh30urf4b3r5,Water Truck Go 2Guys,plant,1614,164,870912,0.195121951219512,C26/09,,ec8dshvhv05u6pr,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-09,aau3dz4w8g2zled,N1 Plant Excavator CAT,plant,1983,138,7075,17.25,C26/09,,bomiofuxtb783ui,,DELETE (orphan)
|
||||
2026-09-09,aau3dz4w8g2zled,N1 Plant Excavator CAT,plant,1983,138,7075,17.25,C26/09,,s9462qzgj6d88j6,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-09,abkx0zcfst41i8y,Bell TLB04 Mega Plant,plant,1167,73,15755,5.21428571428571,C26/09,,wut55nj2oi94hhk,,DELETE (orphan)
|
||||
2026-09-09,abkx0zcfst41i8y,Bell TLB04 Mega Plant,plant,1167,73,15755,5.21428571428571,C26/09,,686mq8ybdfylby8,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-09,bxk0ezqopjpdo7u,Roller,plant,1007,74,2702,9.25,C26/09,,3mr9cfvvbwcnt8a,,DELETE (orphan)
|
||||
2026-09-09,bxk0ezqopjpdo7u,Roller,plant,1007,74,2702,9.25,C26/09,,vayf90mfk46ahs2,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-09,dfrv3e65t9q7aem,Isuzu MF88YZGP Justice,,1999,42,39400,10.0714285714286,C26/09,,1dkeu2ni6fmcsv7,,DELETE (orphan)
|
||||
2026-09-09,dfrv3e65t9q7aem,Isuzu MF88YZGP Justice,,1999,42,39400,10.0714285714286,C26/09,,fi9k9icty9wtycu,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-09,jby0yvg41384wtx,Loader Jupiter 821C,plant,1113,118,8079,13.1111111111111,C26/09,,cq41xzj4pc70e0x,,DELETE (orphan)
|
||||
2026-09-09,jby0yvg41384wtx,Loader Jupiter 821C,plant,1113,118,8079,13.1111111111111,C26/09,,cts0lo786nd2pqn,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-09,muppqwvwgh5e3m6,Isuzu Johannes,,1992,17,31657,10.3529411764706,C26/09,,py3w6s073384e5g,,DELETE (orphan)
|
||||
2026-09-09,muppqwvwgh5e3m6,Isuzu Johannes,,1992,17,31657,10.3529411764706,C26/09,,pep0z111ozg9g7i,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-09,phcy5a8untqbwpm,Center Point TLB 473,plant,1516,69,4560,5.75,C26/09,,rvtmdxz09604m8o,,DELETE (orphan)
|
||||
2026-09-09,phcy5a8untqbwpm,Center Point TLB 473,plant,1516,69,4560,5.75,C26/09,,42cz372hr3at7mz,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-09,pkq2cjn4oata4t6,Grader Case Hire,plant,1590,219,963,19.9090909090909,C26/09,,mtitlhdlyq83e3p,,DELETE (orphan)
|
||||
2026-09-09,pkq2cjn4oata4t6,Grader Case Hire,plant,1590,219,963,19.9090909090909,C26/09,,9zaht402raqm4rb,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-09,qnig4z1earte98c,Isuzu MY85HGGP,,1936,33,9285,10.1212121212121,C26/09,,9hhfz4xcyya9mrq,,DELETE (orphan)
|
||||
2026-09-09,qnig4z1earte98c,Isuzu MY85HGGP,,1936,33,9285,10.1212121212121,C26/09,,z810mt12qdl4hnl,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-09,shjv24qw7p4sksa,Center Point TLB 515,plant,1959,26,4091,3.25,C26/09,,hja6auvyiam4e8a,,DELETE (orphan)
|
||||
2026-09-09,shjv24qw7p4sksa,Center Point TLB 515,plant,1959,26,4091,3.25,C26/09,,m4qr9mphiosb3z2,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-09,sw75yhfj05asoef,Isuzu HY58HVGP,,1934,24,16595,12.2916666666667,C26/09,,cer4dfv3ag6bpve,,DELETE (orphan)
|
||||
2026-09-09,sw75yhfj05asoef,Isuzu HY58HVGP,,1934,24,16595,12.2916666666667,C26/09,,t122e5655oz2r9y,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-09,un37e6yl59z8ol8,Center Point Excavator CAT,plant,1985,215,6097,12.6470588235294,C26/09,,mc6h5h5vp8e1s6s,,DELETE (orphan)
|
||||
2026-09-09,un37e6yl59z8ol8,Center Point Excavator CAT,plant,1985,215,6097,12.6470588235294,C26/09,,64tgdibvqabfv9x,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-09,vac49hzww505q6t,Center Point Grader,plant,1626,130,5259,0.0769230769230769,C26/09,,r5ou0332lmc3o7y,,DELETE (orphan)
|
||||
2026-09-09,vac49hzww505q6t,Center Point Grader,plant,1626,130,5259,0.0769230769230769,C26/09,,bk9i2t1cbnwl1ez,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-09,vnyq71ybkgcyeqo,Crushing Machine Jupiter,plant,1622,170,0,0,C26/09,,8c25qnehft0tj16,,DELETE (orphan)
|
||||
2026-09-09,vnyq71ybkgcyeqo,Crushing Machine Jupiter,plant,1622,170,0,0,C26/09,,8u1kdvcx5c554ck,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-09,vq4du1j6kf5jui3,N1 Plant Excavator PC200,plant,1572,118,8229,11.8,C26/09,,mddwgitouz6g5ob,,DELETE (orphan)
|
||||
2026-09-09,vq4du1j6kf5jui3,N1 Plant Excavator PC200,plant,1572,118,8229,11.8,C26/09,,tpvf5ckja07mlvt,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
2026-09-09,z5ta7k16siceztm,TLB centre point,plant,1586,87,8425,5.11764705882353,C26/09,,tws6zsmny9gwurz,,DELETE (orphan)
|
||||
2026-09-09,z5ta7k16siceztm,TLB centre point,plant,1586,87,8425,5.11764705882353,C26/09,,g5nqf70j9lrvrwh,xbbzre09uup8nz1,KEEP (yesterday import)
|
||||
|
182
scripts/migrations/2026-09-12-rename-collections.py
Normal file
182
scripts/migrations/2026-09-12-rename-collections.py
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Rename two collections as part of the ingestion restructure.
|
||||
|
||||
PB_URL=... PB_ADMIN_EMAIL=... PB_ADMIN_PASSWORD=... \\
|
||||
python3 scripts/migrations/2026-09-12-rename-collections.py [--apply]
|
||||
|
||||
DRY RUN BY DEFAULT. Pass --apply to rename.
|
||||
|
||||
RECEIVED_ITEMS -> DELIVERY_NOTE_ITEMS
|
||||
They are the lines on a delivery note. The old name describes the
|
||||
event, not the thing.
|
||||
|
||||
TIMESHEETS -> TIMESHEET_ENTRIES
|
||||
This collection is the time log (employee, date, hours, site), not
|
||||
a timesheet. The name is freed for the uploaded-document
|
||||
collection created in Task 6.
|
||||
|
||||
WHY THIS IS SAFE
|
||||
----------------
|
||||
PocketBase relations store collectionId, not name, so every existing
|
||||
relation survives a rename untouched. Collection names are
|
||||
case-insensitive, so lowercase call sites in `src/` keep resolving until
|
||||
the code is updated - the rename and the code change do not have to be
|
||||
simultaneous.
|
||||
|
||||
THE SAFETY PROPERTY
|
||||
-------------------
|
||||
A rename must move a name and nothing else. So each rename asserts, on
|
||||
the collection id it just patched, that the new name stuck AND that the
|
||||
row count is byte-identical to the count read immediately before. A
|
||||
count that moves means something other than a rename happened, and the
|
||||
run stops there rather than carrying on to the second collection with
|
||||
an unexplained delta behind it.
|
||||
|
||||
Refuses to rename onto a name that is already taken by a different
|
||||
collection. Idempotent: a collection already carrying the new name is
|
||||
reported and skipped.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
APPLY = "--apply" in sys.argv
|
||||
|
||||
RENAMES = [
|
||||
("received_items", "DELIVERY_NOTE_ITEMS"),
|
||||
("timesheets", "TIMESHEET_ENTRIES"),
|
||||
]
|
||||
|
||||
|
||||
def admin_auth(base_url):
|
||||
body = json.dumps(
|
||||
{
|
||||
"identity": os.environ["PB_ADMIN_EMAIL"],
|
||||
"password": os.environ["PB_ADMIN_PASSWORD"],
|
||||
}
|
||||
).encode()
|
||||
req = urllib.request.Request(
|
||||
f"{base_url}/api/collections/_superusers/auth-with-password",
|
||||
data=body,
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
return json.loads(resp.read())["token"]
|
||||
|
||||
|
||||
def api(base_url, token, method, path, body=None):
|
||||
"""
|
||||
A 404 is only soft for GET - "this collection is absent" is a
|
||||
legitimate answer to a read, and is how we detect an already-renamed
|
||||
collection. On a write it is not: a PATCH against a since-renamed
|
||||
collection returning None would be counted as a successful rename.
|
||||
Mutations raise.
|
||||
"""
|
||||
data = json.dumps(body).encode() if body is not None else None
|
||||
req = urllib.request.Request(
|
||||
f"{base_url}{path}",
|
||||
data=data,
|
||||
method=method,
|
||||
headers={"Authorization": token, "Content-Type": "application/json"},
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
txt = resp.read().decode()
|
||||
return json.loads(txt) if txt else None
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 404 and method == "GET":
|
||||
return None
|
||||
raise RuntimeError(
|
||||
f"{method} {path}: {e.code} {e.read().decode() if e.fp else ''}"
|
||||
) from e
|
||||
|
||||
|
||||
def row_count(base_url, token, coll_ref):
|
||||
"""Total rows, read by collection id so it is stable across a rename."""
|
||||
return api(
|
||||
base_url,
|
||||
token,
|
||||
"GET",
|
||||
f"/api/collections/{coll_ref}/records?perPage=1&fields=id",
|
||||
)["totalItems"]
|
||||
|
||||
|
||||
def main():
|
||||
base_url = os.environ.get("PB_URL", "http://127.0.0.1:8090").rstrip("/")
|
||||
token = admin_auth(base_url)
|
||||
# Say where we are writing. This script mutates production by default
|
||||
# target, and a run whose output names no host is a run you cannot
|
||||
# audit afterwards.
|
||||
print(f"target: {base_url} mode: {'APPLY' if APPLY else 'DRY RUN'}")
|
||||
|
||||
all_colls = api(base_url, token, "GET", "/api/collections?perPage=200")
|
||||
names_lower = {c["name"].lower(): c["name"] for c in all_colls["items"]}
|
||||
|
||||
renamed, skipped = 0, 0
|
||||
for old, new in RENAMES:
|
||||
cur = api(base_url, token, "GET", f"/api/collections/{old}")
|
||||
if cur is None:
|
||||
print(f"\nSKIP {old}: not found (already renamed?)")
|
||||
skipped += 1
|
||||
continue
|
||||
if cur["name"].lower() == new.lower():
|
||||
print(f"\nSKIP {old}: already named {cur['name']}")
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
# A name collision is not a skip. Renaming onto a live collection
|
||||
# is not something this script can do safely, and continuing to
|
||||
# the next rename would leave the restructure half-applied.
|
||||
taken = names_lower.get(new.lower())
|
||||
if taken is not None and taken.lower() != cur["name"].lower():
|
||||
print(f"\nABORT {old} -> {new}: target name already taken")
|
||||
return 1
|
||||
|
||||
count = row_count(base_url, token, old)
|
||||
print(f"\n{cur['name']} -> {new} (id={cur['id']}, {count} rows)")
|
||||
|
||||
if not APPLY:
|
||||
continue
|
||||
|
||||
api(base_url, token, "PATCH", f"/api/collections/{cur['id']}", {"name": new})
|
||||
|
||||
# Re-read by id, not by either name: reading by the new name
|
||||
# would prove only that *a* collection has that name, not that
|
||||
# this one does.
|
||||
after = api(base_url, token, "GET", f"/api/collections/{cur['id']}")
|
||||
if after is None:
|
||||
print(f" FAILED: collection id={cur['id']} vanished after rename")
|
||||
return 1
|
||||
if after["name"] != new:
|
||||
print(f" FAILED: rename did not stick: {after['name']!r} != {new!r}")
|
||||
return 1
|
||||
|
||||
after_count = row_count(base_url, token, cur["id"])
|
||||
if after_count != count:
|
||||
print(
|
||||
f" FAILED: row count changed during rename: "
|
||||
f"{count} -> {after_count}. Stopping before the next rename."
|
||||
)
|
||||
return 1
|
||||
|
||||
print(f" OK, {after_count} rows intact")
|
||||
renamed += 1
|
||||
|
||||
if not APPLY:
|
||||
print("\nDRY RUN. Re-run with --apply to rename.")
|
||||
return 0
|
||||
|
||||
print(f"\nrenamed {renamed} skipped {skipped} of {len(RENAMES)}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
sys.exit(main())
|
||||
except KeyError as e:
|
||||
print(f"ERROR: missing environment variable {e}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
|
@ -40,15 +40,15 @@ import { validateMatchedPoItems } from "@/lib/validate-matched-po-items";
|
|||
* }
|
||||
*
|
||||
* Behaviour (see resolveOcrTier for the authoritative rules):
|
||||
* - Overall confidence >= 0.8 AND PO resolved: creates received_items,
|
||||
* - Overall confidence >= 0.8 AND PO resolved: creates delivery_note_items,
|
||||
* sets processing_status="auto-verified" and is_verified=true.
|
||||
* - Overall confidence >= 0.8 BUT PO did NOT resolve (OCR was
|
||||
* confident in the slip but the PO number didn't fuzzy-match
|
||||
* any PO we have on file): DOWNGRADES to draft — we never
|
||||
* auto-verify an orphaned DN.
|
||||
* - 0.5..0.79: creates received_items as a draft, sets
|
||||
* - 0.5..0.79: creates delivery_note_items as a draft, sets
|
||||
* processing_status="needs-review" and is_verified=false.
|
||||
* - < 0.5: doesn't create received_items, sets
|
||||
* - < 0.5: doesn't create delivery_note_items, sets
|
||||
* processing_status="needs-review" and is_verified=false.
|
||||
*
|
||||
* PO matching (in priority order): exact → normalised
|
||||
|
|
@ -57,7 +57,7 @@ import { validateMatchedPoItems } from "@/lib/validate-matched-po-items";
|
|||
* purchasing exception inbox can show "matched by Levenshtein, 0.9"
|
||||
* instead of a silent "no match".
|
||||
*
|
||||
* Idempotency: if the webhook is replayed, received_items already
|
||||
* Idempotency: if the webhook is replayed, delivery_note_items already
|
||||
* linked to this DN are updated rather than duplicated.
|
||||
*/
|
||||
export async function POST(
|
||||
|
|
@ -198,8 +198,8 @@ export async function POST(
|
|||
// >= 0.8 BUT items unverifiable (empty local PO items but
|
||||
// n8n supplied ids) → draft (don't claim auto when we
|
||||
// couldn't cross-check the links)
|
||||
// 0.5..0.79 → draft, received_items created for review
|
||||
// < 0.5 → manual, no received_items (user captures manually)
|
||||
// 0.5..0.79 → draft, delivery_note_items created for review
|
||||
// < 0.5 → manual, no delivery_note_items (user captures manually)
|
||||
const poId = matchedPoId ?? dn.purchase_order ?? null;
|
||||
let poItems: PurchaseOrderItemRecord[] = [];
|
||||
if (poId) {
|
||||
|
|
@ -239,7 +239,7 @@ export async function POST(
|
|||
const isVerified = tier === "auto";
|
||||
|
||||
// 5. Update the DN with the OCR fields regardless of tier.
|
||||
// We do this BEFORE creating received_items so the audit trail shows
|
||||
// We do this BEFORE creating delivery_note_items so the audit trail shows
|
||||
// the OCR happened even if item creation fails.
|
||||
try {
|
||||
await pb.collection("delivery_notes").update(dnId, {
|
||||
|
|
@ -298,7 +298,7 @@ export async function POST(
|
|||
);
|
||||
}
|
||||
|
||||
// 6. Build received_items only for the auto / draft tiers.
|
||||
// 6. Build delivery_note_items only for the auto / draft tiers.
|
||||
// The manual tier gets no items — the user will fill them in via the
|
||||
// exception inbox.
|
||||
if (tier === "manual") {
|
||||
|
|
@ -318,21 +318,21 @@ export async function POST(
|
|||
});
|
||||
}
|
||||
|
||||
// 7. Idempotently upsert received_items keyed by (delivery_note, po_item).
|
||||
// 7. Idempotently upsert delivery_note_items keyed by (delivery_note, po_item).
|
||||
// The PO items + matched-id validation already happened above (so it
|
||||
// could feed the tier decision). Here we just consume the result.
|
||||
// We delete the existing set first (within this DN) and recreate — atomic
|
||||
// from the caller's perspective and avoids stale matches if the OCR
|
||||
// rerun finds different items.
|
||||
const existing = await pb.collection("received_items").getFullList({
|
||||
const existing = await pb.collection("delivery_note_items").getFullList({
|
||||
filter: `delivery_note='${dnId}'`,
|
||||
fields: "id,purchase_order_item",
|
||||
});
|
||||
for (const r of existing) {
|
||||
try {
|
||||
await pb.collection("received_items").delete(r.id);
|
||||
await pb.collection("delivery_note_items").delete(r.id);
|
||||
} catch (err) {
|
||||
console.warn(`[dn-ocr-result] delete received_item ${r.id} failed:`, err);
|
||||
console.warn(`[dn-ocr-result] delete delivery_note_item ${r.id} failed:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -359,7 +359,7 @@ export async function POST(
|
|||
.join(" · ");
|
||||
|
||||
try {
|
||||
const created = await pb.collection("received_items").create({
|
||||
const created = await pb.collection("delivery_note_items").create({
|
||||
delivery_note: dnId,
|
||||
// Only link to a PO item if we got a verified match. Otherwise
|
||||
// we still create the row (so the user sees what OCR found) but
|
||||
|
|
@ -385,11 +385,11 @@ export async function POST(
|
|||
const isFkError = Boolean(validPoItemId) && status === 400;
|
||||
if (isFkError) {
|
||||
console.warn(
|
||||
`[dn-ocr-result] received_item create failed for matched_po_item_id=${validPoItemId} (id no longer exists in PB) — retrying without link:`,
|
||||
`[dn-ocr-result] delivery_note_item create failed for matched_po_item_id=${validPoItemId} (id no longer exists in PB) — retrying without link:`,
|
||||
message,
|
||||
);
|
||||
try {
|
||||
const created = await pb.collection("received_items").create({
|
||||
const created = await pb.collection("delivery_note_items").create({
|
||||
delivery_note: dnId,
|
||||
purchase_order_item: null,
|
||||
qty_received: qty,
|
||||
|
|
@ -402,12 +402,12 @@ export async function POST(
|
|||
});
|
||||
} catch (err2) {
|
||||
console.warn(
|
||||
"[dn-ocr-result] received_item create (no-link retry) failed:",
|
||||
"[dn-ocr-result] delivery_note_item create (no-link retry) failed:",
|
||||
err2,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.warn("[dn-ocr-result] received_item create failed:", err);
|
||||
console.warn("[dn-ocr-result] delivery_note_item create failed:", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ export async function POST(request: Request) {
|
|||
}
|
||||
|
||||
try {
|
||||
await pb.collection("timesheets").create({
|
||||
await pb.collection("timesheet_entries").create({
|
||||
employee: employeeId,
|
||||
batch_reference: batch.id,
|
||||
date_logged: new Date(row.date).toISOString(),
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ export default async function EmployeesPage() {
|
|||
|
||||
const [employees, timesheets, sites] = await Promise.all([
|
||||
pb.collection("employees").getFullList({ sort: "name", expand: "site" }),
|
||||
pb.collection("timesheets").getFullList(),
|
||||
pb.collection("timesheet_entries").getFullList(),
|
||||
pb.collection("sites").getFullList({ sort: "name" }),
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export default async function PurchaseOrderDetailPage({
|
|||
pb.collection("delivery_notes").getFullList({
|
||||
filter: `purchase_order='${poId}'`,
|
||||
}),
|
||||
pb.collection("received_items").getFullList({
|
||||
pb.collection("delivery_note_items").getFullList({
|
||||
fields: "id,delivery_note,purchase_order_item,qty_received,condition_notes",
|
||||
}),
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -25,10 +25,10 @@ export default async function PurchasingPage() {
|
|||
}),
|
||||
pb.collection("sites").getFullList({ sort: "name" }),
|
||||
// Authoritative source for received quantities. Use getFullList so
|
||||
// there is no silent truncation if `received_items` ever exceeds
|
||||
// there is no silent truncation if `delivery_note_items` ever exceeds
|
||||
// 500 rows — earlier this used `getList(1, 500, ...)` which capped
|
||||
// and produced 0% fulfillment whenever low-id POs were dropped.
|
||||
pb.collection("received_items").getFullList({
|
||||
pb.collection("delivery_note_items").getFullList({
|
||||
fields: "id,purchase_order_item,qty_received",
|
||||
}),
|
||||
]);
|
||||
|
|
@ -46,7 +46,7 @@ export default async function PurchasingPage() {
|
|||
dnsByPo.set(dn.purchase_order, list);
|
||||
}
|
||||
|
||||
// Sum received_items.qty_received per PO item so we can compute
|
||||
// Sum delivery_note_items.qty_received per PO item so we can compute
|
||||
// per-PO fulfilment % from authoritative data instead of the stale
|
||||
// denormalized field on purchase_order_items. Rows without a
|
||||
// purchase_order_item are collected as orphans (they can't roll up to
|
||||
|
|
@ -67,7 +67,7 @@ export default async function PurchasingPage() {
|
|||
}
|
||||
if (orphanIds.length > 0) {
|
||||
console.warn(
|
||||
`[purchasing] ${orphanIds.length} received_items row(s) have empty purchase_order_item and are excluded from fulfilment. ids=${orphanIds.join(",")}`,
|
||||
`[purchasing] ${orphanIds.length} delivery_note_items row(s) have empty purchase_order_item and are excluded from fulfilment. ids=${orphanIds.join(",")}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ export default async function SiteDetailPage({
|
|||
fuelPrices,
|
||||
] = await Promise.all([
|
||||
pb.collection("fixed_costs").getFullList({ filter: `site='${id}'` }),
|
||||
pb.collection("timesheets").getFullList(),
|
||||
pb.collection("timesheet_entries").getFullList(),
|
||||
pb.collection("employees").getFullList({ fields: "id,rate_per_hour,site,name" }),
|
||||
pb.collection("purchase_orders").getFullList({ filter: `site='${id}'` }),
|
||||
pb.collection("fuel_logs").getFullList({ filter: `site='${id}'` }),
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export default async function SitesPage() {
|
|||
] = await Promise.all([
|
||||
pb.collection("sites").getFullList(),
|
||||
pb.collection("fixed_costs").getFullList(),
|
||||
pb.collection("timesheets").getFullList(),
|
||||
pb.collection("timesheet_entries").getFullList(),
|
||||
pb.collection("employees").getFullList({
|
||||
fields: "id,rate_per_hour,site,name",
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export async function loadSiteBundle(siteId: string) {
|
|||
] = await Promise.all([
|
||||
pb.collection("sites").getOne(siteId),
|
||||
pb.collection("fixed_costs").getFullList({ filter: `site='${siteId}'` }),
|
||||
pb.collection("timesheets").getFullList(),
|
||||
pb.collection("timesheet_entries").getFullList(),
|
||||
pb.collection("employees").getFullList({
|
||||
fields: "id,rate_per_hour,site,name",
|
||||
}),
|
||||
|
|
@ -75,7 +75,7 @@ export async function loadAllSitesBundle() {
|
|||
] = await Promise.all([
|
||||
pb.collection("sites").getFullList({ sort: "name" }),
|
||||
pb.collection("fixed_costs").getFullList(),
|
||||
pb.collection("timesheets").getFullList(),
|
||||
pb.collection("timesheet_entries").getFullList(),
|
||||
pb.collection("employees").getFullList({
|
||||
fields: "id,rate_per_hour,site,name",
|
||||
}),
|
||||
|
|
@ -191,7 +191,7 @@ export async function loadDeliveryNoteByNumber(dnNumber: string) {
|
|||
if (!dn) return null;
|
||||
|
||||
// Expand the PO item so each received line has a description.
|
||||
const items = await pb.collection("received_items").getFullList({
|
||||
const items = await pb.collection("delivery_note_items").getFullList({
|
||||
filter: `delivery_note='${dn.id}'`,
|
||||
expand: "purchase_order_item",
|
||||
});
|
||||
|
|
@ -219,7 +219,7 @@ export async function loadWorkforce() {
|
|||
pb.collection("employees").getFullList({
|
||||
fields: "id,name,rate_per_hour,site,position,worker_type",
|
||||
}),
|
||||
pb.collection("timesheets").getFullList(),
|
||||
pb.collection("timesheet_entries").getFullList(),
|
||||
pb.collection("sites").getFullList({ fields: "id,name,project_code" }),
|
||||
]);
|
||||
return { employees, timesheets, sites };
|
||||
|
|
@ -241,7 +241,7 @@ export async function loadDeliveryNotes(args: { siteId?: string; verified?: bool
|
|||
});
|
||||
|
||||
// One query for all received items, then bucket — avoids N round-trips.
|
||||
const received = await pb.collection("received_items").getFullList({
|
||||
const received = await pb.collection("delivery_note_items").getFullList({
|
||||
expand: "purchase_order_item",
|
||||
});
|
||||
const itemsByNote = new Map<string, typeof received>();
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
*
|
||||
* Why the PO-resolved downgrade matters:
|
||||
* Before this, a slip with confidence 0.92 but a garbled PO
|
||||
* number would be marked auto-verified with every received_item
|
||||
* number would be marked auto-verified with every delivery_note_item
|
||||
* linked to nothing. The user only noticed when reconciling the
|
||||
* exception inbox — by which point the audit trail had already
|
||||
* been stamped as "verified". Now the tier falls back to "draft"
|
||||
|
|
@ -34,7 +34,7 @@ export type OcrMatchMethod = "exact" | "normalised" | "levenshtein" | "none";
|
|||
* didn't bother to cross-check the matched ids). Pass `false` when
|
||||
* the local PO-items fetch was empty but n8n supplied ids we had to
|
||||
* trust anyway — in that case we don't have contradicting local
|
||||
* evidence, but we also can't claim the received_items were
|
||||
* evidence, but we also can't claim the delivery_note_items were
|
||||
* correctly linked. Force the user to review.
|
||||
*
|
||||
* Confidence is clamped to [0, 1]; values > 1 are treated as percentages
|
||||
|
|
|
|||
|
|
@ -187,13 +187,19 @@ export interface PurchaseOrderItemRecord extends RecordModel {
|
|||
expand?: { purchase_order?: PurchaseOrderRecord };
|
||||
}
|
||||
|
||||
export interface TimesheetRecord extends RecordModel {
|
||||
collectionName: "timesheets";
|
||||
/**
|
||||
* One logged shift: this employee worked these hours on this date. The
|
||||
* collection was called `timesheets` until the 2026-09-12 rename; that
|
||||
* name now belongs to the uploaded timesheet document.
|
||||
*/
|
||||
export interface TimesheetEntryRecord extends RecordModel {
|
||||
collectionName: "timesheet_entries";
|
||||
employee: string;
|
||||
batch_reference: string;
|
||||
/** Stamped from the employee's site at import time. Never joined at read time. */
|
||||
site: string;
|
||||
date_logged: string;
|
||||
hours_worked: number;
|
||||
expand?: { employee?: EmployeeRecord; batch_reference?: DataImportBatchRecord };
|
||||
expand?: { employee?: EmployeeRecord; site?: SiteRecord };
|
||||
}
|
||||
|
||||
export interface FuelLogRecord extends RecordModel {
|
||||
|
|
@ -275,9 +281,15 @@ export type DeliveryNoteProcessingStatus =
|
|||
| "needs-review"
|
||||
| "manual-verified"
|
||||
| "failed";
|
||||
export interface ReceivedItemRecord extends RecordModel {
|
||||
collectionName: "received_items";
|
||||
/**
|
||||
* One line on a delivery note. The collection was called
|
||||
* `received_items` until the 2026-09-12 rename — the old name described
|
||||
* the receiving event rather than the thing received.
|
||||
*/
|
||||
export interface DeliveryNoteItemRecord extends RecordModel {
|
||||
collectionName: "delivery_note_items";
|
||||
delivery_note: string;
|
||||
/** Optional — empty when OCR could not match the line to a PO item. */
|
||||
purchase_order_item: string;
|
||||
qty_received: number;
|
||||
condition_notes: string;
|
||||
|
|
|
|||
Loading…
Reference in a new issue