From 0cdf385df202f90947fbb1250294d76588df9a21 Mon Sep 17 00:00:00 2001 From: DEls Date: Fri, 11 Sep 2026 11:01:40 +0200 Subject: [PATCH] refactor(schema): rename received_items and timesheets to their real names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- fuel_logs_duplicates_2026-09-10.csv | 147 ++++++++++++++ .../2026-09-12-rename-collections.py | 182 ++++++++++++++++++ .../delivery-notes/[id]/ocr-result/route.ts | 34 ++-- src/app/api/timesheets/import/route.ts | 2 +- src/app/dashboard/employees/page.tsx | 2 +- src/app/dashboard/purchasing/[poId]/page.tsx | 2 +- src/app/dashboard/purchasing/page.tsx | 8 +- src/app/dashboard/sites/[id]/page.tsx | 2 +- src/app/dashboard/sites/page.tsx | 2 +- src/lib/agent/load.ts | 10 +- src/lib/ocr-result-tier.ts | 4 +- src/lib/pb-types.ts | 24 ++- 12 files changed, 380 insertions(+), 39 deletions(-) create mode 100644 fuel_logs_duplicates_2026-09-10.csv create mode 100644 scripts/migrations/2026-09-12-rename-collections.py diff --git a/fuel_logs_duplicates_2026-09-10.csv b/fuel_logs_duplicates_2026-09-10.csv new file mode 100644 index 0000000..3d1905a --- /dev/null +++ b/fuel_logs_duplicates_2026-09-10.csv @@ -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) diff --git a/scripts/migrations/2026-09-12-rename-collections.py b/scripts/migrations/2026-09-12-rename-collections.py new file mode 100644 index 0000000..4abab69 --- /dev/null +++ b/scripts/migrations/2026-09-12-rename-collections.py @@ -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) diff --git a/src/app/api/delivery-notes/[id]/ocr-result/route.ts b/src/app/api/delivery-notes/[id]/ocr-result/route.ts index cdf16e6..e25ee32 100644 --- a/src/app/api/delivery-notes/[id]/ocr-result/route.ts +++ b/src/app/api/delivery-notes/[id]/ocr-result/route.ts @@ -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); } } } diff --git a/src/app/api/timesheets/import/route.ts b/src/app/api/timesheets/import/route.ts index 802e54a..9399bc2 100644 --- a/src/app/api/timesheets/import/route.ts +++ b/src/app/api/timesheets/import/route.ts @@ -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(), diff --git a/src/app/dashboard/employees/page.tsx b/src/app/dashboard/employees/page.tsx index bd58ec6..b5f15c7 100644 --- a/src/app/dashboard/employees/page.tsx +++ b/src/app/dashboard/employees/page.tsx @@ -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" }), ]); diff --git a/src/app/dashboard/purchasing/[poId]/page.tsx b/src/app/dashboard/purchasing/[poId]/page.tsx index b3cc5e5..3da8f32 100644 --- a/src/app/dashboard/purchasing/[poId]/page.tsx +++ b/src/app/dashboard/purchasing/[poId]/page.tsx @@ -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", }), ]); diff --git a/src/app/dashboard/purchasing/page.tsx b/src/app/dashboard/purchasing/page.tsx index 1faf517..4a5ce03 100644 --- a/src/app/dashboard/purchasing/page.tsx +++ b/src/app/dashboard/purchasing/page.tsx @@ -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(",")}`, ); } diff --git a/src/app/dashboard/sites/[id]/page.tsx b/src/app/dashboard/sites/[id]/page.tsx index d6c5cbc..d09467a 100644 --- a/src/app/dashboard/sites/[id]/page.tsx +++ b/src/app/dashboard/sites/[id]/page.tsx @@ -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}'` }), diff --git a/src/app/dashboard/sites/page.tsx b/src/app/dashboard/sites/page.tsx index dba45d5..76a847c 100644 --- a/src/app/dashboard/sites/page.tsx +++ b/src/app/dashboard/sites/page.tsx @@ -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", }), diff --git a/src/lib/agent/load.ts b/src/lib/agent/load.ts index 7a93aa1..0ba7512 100644 --- a/src/lib/agent/load.ts +++ b/src/lib/agent/load.ts @@ -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(); diff --git a/src/lib/ocr-result-tier.ts b/src/lib/ocr-result-tier.ts index c6bf9cc..9812e01 100644 --- a/src/lib/ocr-result-tier.ts +++ b/src/lib/ocr-result-tier.ts @@ -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 diff --git a/src/lib/pb-types.ts b/src/lib/pb-types.ts index f19c9c7..757bf3c 100644 --- a/src/lib/pb-types.ts +++ b/src/lib/pb-types.ts @@ -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;